@gavin-hu/my-pi-agent 0.19.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -11
- package/README.zh-CN.md +13 -8
- package/docs/extensions/README.md +1 -1
- package/docs/extensions/board.md +286 -123
- package/docs/extensions/bytefly.md +381 -223
- package/docs/extensions/file-browse.md +14 -7
- package/docs/extensions/job.md +1 -2
- package/docs/extensions/plan.md +56 -11
- package/docs/extensions/session-bus.md +23 -10
- package/docs/extensions/team.md +45 -17
- package/docs/extensions/wechat.md +2 -1
- package/docs/extensions/worktree.md +30 -15
- package/extensions/board/commands.ts +23 -5
- package/extensions/board/format.ts +107 -9
- package/extensions/board/join.ts +1 -1
- package/extensions/board/page.ts +170 -94
- package/extensions/board/result.ts +9 -0
- package/extensions/board/schema.ts +22 -7
- package/extensions/board/seat.ts +9 -10
- package/extensions/board/snapshot.ts +1 -0
- package/extensions/board/tools.ts +186 -21
- package/extensions/board/transport/router.ts +27 -108
- package/extensions/board/transport/server.ts +18 -29
- package/extensions/board/types.ts +67 -0
- package/extensions/board/view/client.ts +23 -13
- package/extensions/board/view/dialog.ts +43 -0
- package/extensions/board/view/keys.ts +28 -24
- package/extensions/board/view/live.ts +5 -171
- package/extensions/board/view/names.ts +91 -0
- package/extensions/board/view/page.ts +48 -88
- package/extensions/board/view/render.ts +213 -190
- package/extensions/board/view/styles.ts +65 -48
- package/extensions/board/view/team.ts +63 -41
- package/extensions/bytefly/footer/budget.ts +6 -5
- package/extensions/bytefly/footer/collect/entries.ts +12 -4
- package/extensions/bytefly/footer/collect/snapshot.ts +8 -17
- package/extensions/bytefly/footer/config.ts +19 -8
- package/extensions/bytefly/footer/price/price.ts +30 -8
- package/extensions/bytefly/footer/register.ts +5 -5
- package/extensions/bytefly/footer/render/format.ts +18 -0
- package/extensions/bytefly/footer/render/lines.ts +23 -7
- package/extensions/bytefly/footer/render/segments.ts +117 -31
- package/extensions/bytefly/footer/types.ts +29 -5
- package/extensions/bytefly/footer/unpriced.ts +42 -17
- package/extensions/bytefly/panel/commands.ts +28 -1
- package/extensions/bytefly/panel/filter.ts +68 -0
- package/extensions/bytefly/panel/format.ts +16 -15
- package/extensions/bytefly/panel/join.ts +56 -13
- package/extensions/bytefly/panel/prices.ts +43 -0
- package/extensions/bytefly/panel/settings.ts +72 -23
- package/extensions/bytefly/panel/tui/entries.ts +26 -8
- package/extensions/bytefly/panel/tui/frame.ts +130 -93
- package/extensions/bytefly/panel/tui/keys.ts +352 -0
- package/extensions/bytefly/panel/tui/list-view.ts +25 -46
- package/extensions/bytefly/panel/tui/panes.ts +76 -0
- package/extensions/bytefly/panel/tui/section-view.ts +126 -0
- package/extensions/bytefly/panel/tui/settings-section.ts +54 -125
- package/extensions/bytefly/panel/tui/settings-view.ts +30 -131
- package/extensions/bytefly/panel/tui/side-pane.ts +224 -0
- package/extensions/bytefly/panel/tui.ts +306 -259
- package/extensions/check/commands.ts +2 -1
- package/extensions/file-browse/files/classify.ts +136 -0
- package/extensions/file-browse/files/files.ts +8 -547
- package/extensions/file-browse/files/index.ts +90 -0
- package/extensions/file-browse/files/list.ts +261 -0
- package/extensions/file-browse/files/tree.ts +101 -0
- package/extensions/file-browse/transport/router.ts +0 -2
- package/extensions/file-browse/view/emit.ts +51 -0
- package/extensions/file-browse/view/html.ts +6 -7
- package/extensions/file-browse/view/markdown.ts +11 -13
- package/extensions/file-browse/view/raw-html.ts +18 -13
- package/extensions/job/runtime.ts +4 -4
- package/extensions/plan/feedback.ts +40 -16
- package/extensions/plan/format.ts +10 -3
- package/extensions/plan/index.ts +17 -1
- package/extensions/plan/plan-files.ts +88 -0
- package/extensions/plan/plans.ts +19 -23
- package/extensions/plan/policy.ts +10 -5
- package/extensions/plan/runtime.ts +31 -2
- package/extensions/plan/todos.ts +51 -3
- package/extensions/plan/tools/exit.ts +10 -1
- package/extensions/plan/tools/todo-seed.ts +4 -2
- package/extensions/plan/tools/write.ts +12 -1
- package/extensions/plan/types.ts +15 -0
- package/extensions/plan/web-blocks.ts +89 -0
- package/extensions/plan/web-chrome.ts +3 -5
- package/extensions/plan/web-client.ts +179 -330
- package/extensions/plan/web-collab.ts +3 -3
- package/extensions/plan/web-comments.ts +18 -8
- package/extensions/plan/web-composer.ts +80 -0
- package/extensions/plan/web-draft.ts +40 -0
- package/extensions/plan/web-keys.ts +134 -0
- package/extensions/plan/web-outline.ts +192 -0
- package/extensions/plan/web-page.ts +4 -1
- package/extensions/plan/web-plan.ts +150 -0
- package/extensions/plan/web-router.ts +0 -1
- package/extensions/plan/web-settle.ts +194 -0
- package/extensions/rewind/commands/review.ts +2 -1
- package/extensions/session-bus/rows.ts +7 -3
- package/extensions/session-bus/runtime.ts +100 -38
- package/extensions/session-bus/store.ts +14 -2
- package/extensions/session-bus/tui.ts +1 -1
- package/extensions/session-bus/types.ts +10 -1
- package/extensions/subagent/commands.ts +2 -1
- package/extensions/team/commands.ts +41 -8
- package/extensions/team/format.ts +14 -0
- package/extensions/team/index.ts +2 -1
- package/extensions/team/tools.ts +30 -9
- package/extensions/team/types.ts +17 -0
- package/extensions/todo/index.ts +4 -1
- package/extensions/todo/nudge.ts +8 -2
- package/extensions/wechat/bridge.ts +43 -172
- package/extensions/wechat/intake.ts +245 -0
- package/extensions/worktree/commands.ts +24 -17
- package/extensions/worktree/exit.ts +12 -6
- package/extensions/worktree/git.ts +22 -14
- package/extensions/worktree/guard.ts +15 -285
- package/extensions/worktree/handoff.ts +10 -2
- package/extensions/worktree/index.ts +2 -1
- package/extensions/worktree/lifecycle.ts +2 -0
- package/extensions/worktree/registry.ts +20 -11
- package/extensions/worktree/root-tools.ts +13 -3
- package/extensions/worktree/runtime.ts +8 -2
- package/extensions/worktree/screen.ts +15 -8
- package/extensions/worktree/shell.ts +303 -0
- package/extensions/worktree/state.ts +1 -1
- package/extensions/worktree/status.ts +42 -9
- package/extensions/worktree/tools.ts +19 -13
- package/lib/README.md +6 -5
- package/lib/board/card.ts +192 -3
- package/lib/board/links.ts +26 -2
- package/lib/board/page-pointer.ts +86 -32
- package/lib/board/round-text.ts +11 -2
- package/lib/board/round.ts +25 -20
- package/lib/board/rules.ts +144 -11
- package/lib/board/store.ts +239 -52
- package/lib/board/view.ts +94 -10
- package/lib/format.ts +22 -0
- package/lib/http/serve.ts +3 -0
- package/lib/http/sink.ts +34 -9
- package/lib/plan-entry.ts +11 -0
- package/lib/policy.ts +14 -1
- package/lib/session-roster.ts +74 -3
- package/lib/spawn.ts +37 -3
- package/lib/stray-fields.ts +45 -0
- package/lib/tool-names.ts +13 -0
- package/lib/tui/glyphs.ts +2 -3
- package/lib/web/README.md +52 -2
- package/lib/web/client-runtime.ts +123 -0
- package/lib/web/index.ts +4 -2
- package/lib/web/markup.ts +88 -1
- package/lib/web/names.ts +42 -0
- package/package.json +1 -1
- package/extensions/board/view/actions.ts +0 -306
- package/extensions/board/writes.ts +0 -99
- package/extensions/bytefly/panel/tui/extensions-view.ts +0 -40
- package/extensions/bytefly/panel/tui/skills-view.ts +0 -50
- package/extensions/job/process.ts +0 -35
|
@@ -1,306 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The client's writes and its two kit dialogs: one request per named action, the
|
|
3
|
-
* confirmation a claim, a reclaim, or a deletion asks for first, and the card dialog
|
|
4
|
-
* that shows a card whole.
|
|
5
|
-
*
|
|
6
|
-
* A browser is not an identity — the page writes as the session that serves it — so
|
|
7
|
-
* the three actions that move a card between people ask before they act, and the
|
|
8
|
-
* answer names the card and the session. Everything else is a single request whose
|
|
9
|
-
* refusal becomes a banner, never an `alert()`.
|
|
10
|
-
*
|
|
11
|
-
* Both dialogs are the kit's (`bf-dialog` + `dialogScript`): the Tab trap, `Esc`,
|
|
12
|
-
* the backdrop click, and the focus return are `lib/web`'s. This module only fills
|
|
13
|
-
* them, answers them, and notices when the user dismissed one.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
export const BOARD_ACTIONS_JS = String.raw`(function () {
|
|
17
|
-
"use strict";
|
|
18
|
-
var board = window.bfBoard || (window.bfBoard = {});
|
|
19
|
-
var token = new URLSearchParams(window.location.search).get("token") || "";
|
|
20
|
-
|
|
21
|
-
/** The one place a message reaches the reader: a banner, cleared by the next success. */
|
|
22
|
-
function banner(message) {
|
|
23
|
-
var node = document.getElementById("board-banner");
|
|
24
|
-
if (!node) return;
|
|
25
|
-
node.hidden = !message;
|
|
26
|
-
node.textContent = message || "";
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** One write. The token rides in the query string, the body is JSON. */
|
|
30
|
-
function request(path, method, body) {
|
|
31
|
-
var url = path + "?token=" + encodeURIComponent(token);
|
|
32
|
-
var options = { method: method };
|
|
33
|
-
if (body !== undefined) {
|
|
34
|
-
options.headers = { "Content-Type": "application/json" };
|
|
35
|
-
options.body = JSON.stringify(body);
|
|
36
|
-
}
|
|
37
|
-
return fetch(url, options).then(function (response) {
|
|
38
|
-
return response.json().catch(function () { return {}; }).then(function (payload) {
|
|
39
|
-
if (!response.ok) {
|
|
40
|
-
throw new Error(payload && payload.error ? payload.error : "The board refused that (" + response.status + ").");
|
|
41
|
-
}
|
|
42
|
-
banner("");
|
|
43
|
-
return payload;
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function post(path, body) {
|
|
49
|
-
return request(path, "POST", body);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function remove(path) {
|
|
53
|
-
return request(path, "DELETE");
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Call settle(false) when the kit closes the dialog without this module asking. */
|
|
57
|
-
function whenClosed(dialog, settle) {
|
|
58
|
-
var observer = new MutationObserver(function () {
|
|
59
|
-
if (dialog.hidden) {
|
|
60
|
-
observer.disconnect();
|
|
61
|
-
settle(false);
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
observer.observe(dialog, { attributes: true, attributeFilter: ["hidden"] });
|
|
65
|
-
return function () {
|
|
66
|
-
observer.disconnect();
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Ask one question in the confirm dialog, and answer whether it was accepted.
|
|
72
|
-
*
|
|
73
|
-
* Esc and a backdrop click are the kit's to handle, and both mean "no": the
|
|
74
|
-
* dialog's hidden attribute going back up is what settles the promise, so a
|
|
75
|
-
* dismissed question never leaves the caller waiting.
|
|
76
|
-
*/
|
|
77
|
-
function confirm(options) {
|
|
78
|
-
var dialog = document.getElementById("board-confirm");
|
|
79
|
-
var ok = document.getElementById("confirm-ok");
|
|
80
|
-
var cancel = document.getElementById("confirm-cancel");
|
|
81
|
-
if (!dialog || !ok || !cancel || !window.bfConfirm) return Promise.resolve(true);
|
|
82
|
-
document.getElementById("confirm-title").textContent = options.title;
|
|
83
|
-
document.getElementById("confirm-body").textContent = options.body || "";
|
|
84
|
-
ok.textContent = options.ok || "Confirm";
|
|
85
|
-
ok.className = "bf-btn " + (options.danger ? "bf-btn--danger" : "bf-btn--primary");
|
|
86
|
-
return new Promise(function (resolve) {
|
|
87
|
-
var answered = false;
|
|
88
|
-
function finish(value) {
|
|
89
|
-
if (answered) return;
|
|
90
|
-
answered = true;
|
|
91
|
-
ok.removeEventListener("click", accept);
|
|
92
|
-
cancel.removeEventListener("click", dismiss);
|
|
93
|
-
stop();
|
|
94
|
-
if (!dialog.hidden) window.bfConfirm.close();
|
|
95
|
-
resolve(value);
|
|
96
|
-
}
|
|
97
|
-
function accept() { finish(true); }
|
|
98
|
-
function dismiss() { finish(false); }
|
|
99
|
-
var stop = whenClosed(dialog, finish);
|
|
100
|
-
ok.addEventListener("click", accept);
|
|
101
|
-
cancel.addEventListener("click", dismiss);
|
|
102
|
-
window.bfConfirm.open();
|
|
103
|
-
cancel.focus();
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** The question a claim, a reclaim, or a deletion asks, or none for the rest. */
|
|
108
|
-
function question(verb, card) {
|
|
109
|
-
var me = board.view ? board.view.me.label : "this session";
|
|
110
|
-
if (verb === "claim") {
|
|
111
|
-
return { title: "Claim #" + card.id + "?", body: "Claim #" + card.id + " as " + me + "?", ok: "Claim" };
|
|
112
|
-
}
|
|
113
|
-
if (verb === "reclaim") {
|
|
114
|
-
return {
|
|
115
|
-
title: "Reclaim #" + card.id + "?",
|
|
116
|
-
body: "Reclaim #" + card.id + " from " + (card.owner ? card.owner.label : "nobody") + "? It returns to To do and keeps its notes.",
|
|
117
|
-
ok: "Reclaim",
|
|
118
|
-
danger: true
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
if (verb === "drop") {
|
|
122
|
-
var notes = card.notes.length;
|
|
123
|
-
return {
|
|
124
|
-
title: "Delete #" + card.id + "?",
|
|
125
|
-
body: "Delete #" + card.id + (notes > 0 ? " and its " + notes + " notes?" : "?"),
|
|
126
|
-
ok: "Delete",
|
|
127
|
-
danger: true
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
return undefined;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Run one named action against a card, after its question when it has one.
|
|
135
|
-
*
|
|
136
|
-
* A refusal is a banner; a success needs nothing, because the next frame is the
|
|
137
|
-
* truth and it arrives within a second.
|
|
138
|
-
*/
|
|
139
|
-
board.act = function (verb, card, extra) {
|
|
140
|
-
var ask = question(verb, card);
|
|
141
|
-
var proceed = ask ? confirm(ask) : Promise.resolve(true);
|
|
142
|
-
return proceed.then(function (accepted) {
|
|
143
|
-
if (!accepted) return undefined;
|
|
144
|
-
var path = "/api/cards/" + encodeURIComponent(card.id);
|
|
145
|
-
var done = verb === "drop" ? remove(path) : post(path + "/" + verb, extra || {});
|
|
146
|
-
return done.catch(function (error) {
|
|
147
|
-
banner(error.message);
|
|
148
|
-
return undefined;
|
|
149
|
-
});
|
|
150
|
-
});
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Hand an unowned card to a teammate, from the board rather than the bus.
|
|
155
|
-
*
|
|
156
|
-
* The page records the owner; it cannot send, so the card's history says no handoff
|
|
157
|
-
* went out and the peer reads it the next time it looks. The team tool's assign is
|
|
158
|
-
* the action that messages somebody.
|
|
159
|
-
*/
|
|
160
|
-
board.assignTo = function (card, label) {
|
|
161
|
-
return post("/api/cards/" + encodeURIComponent(card.id) + "/assign", { to: label }).catch(function (error) {
|
|
162
|
-
banner(error.message);
|
|
163
|
-
return undefined;
|
|
164
|
-
});
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
/** Add a card from the composer. */
|
|
168
|
-
board.add = function (input) {
|
|
169
|
-
return post("/api/cards", input).catch(function (error) {
|
|
170
|
-
banner(error.message);
|
|
171
|
-
return undefined;
|
|
172
|
-
});
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
/** Note the card whose dialog is open, from its own field. */
|
|
176
|
-
board.addNote = function () {
|
|
177
|
-
var input = document.getElementById("card-dialog-note");
|
|
178
|
-
if (!input || board.openId === undefined) return undefined;
|
|
179
|
-
var text = input.value.trim();
|
|
180
|
-
if (!text) return undefined;
|
|
181
|
-
var card = board.findCard(board.view, board.openId);
|
|
182
|
-
if (!card) return undefined;
|
|
183
|
-
return post("/api/cards/" + encodeURIComponent(card.id) + "/note", { text: text })
|
|
184
|
-
.then(function (payload) {
|
|
185
|
-
input.value = "";
|
|
186
|
-
return payload;
|
|
187
|
-
})
|
|
188
|
-
.catch(function (error) {
|
|
189
|
-
banner(error.message);
|
|
190
|
-
return undefined;
|
|
191
|
-
});
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
/** Fill and open the card dialog; the second argument names what to focus. */
|
|
195
|
-
board.openCard = function (id, focus) {
|
|
196
|
-
var view = board.view;
|
|
197
|
-
if (!view) return;
|
|
198
|
-
var card = board.findCard(view, id);
|
|
199
|
-
if (!card || !window.bfCard) return;
|
|
200
|
-
board.openId = id;
|
|
201
|
-
board.fillDialog(card, view);
|
|
202
|
-
window.bfCard.open();
|
|
203
|
-
if (focus === "note") {
|
|
204
|
-
var input = document.getElementById("card-dialog-note");
|
|
205
|
-
if (input) input.focus();
|
|
206
|
-
}
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
board.closeCard = function () {
|
|
210
|
-
if (window.bfCard) window.bfCard.close();
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
board.dialogOpen = function () {
|
|
214
|
-
return window.bfCard ? window.bfCard.isOpen() : false;
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Assign to: a teammate, then the button.
|
|
219
|
-
*
|
|
220
|
-
* The names come from the roster the page already has, live first: a session that has
|
|
221
|
-
* gone quiet is still a label a card can record, but it is not the one to pick.
|
|
222
|
-
*/
|
|
223
|
-
function assignControl(card, view) {
|
|
224
|
-
var row = board.el("div", "board-dialog__assign");
|
|
225
|
-
var select = board.el("select", "bf-input board-dialog__assign-select");
|
|
226
|
-
select.setAttribute("aria-label", "Assign to");
|
|
227
|
-
var mates = view.teammates === undefined ? [] : view.teammates.map(function (mate) { return mate.label; });
|
|
228
|
-
// Your own terminal is not somebody to hand a card to, and neither is the seat.
|
|
229
|
-
var self = view.session ? view.session.label : "";
|
|
230
|
-
var others = mates.filter(function (label) { return label.toLowerCase() !== self.toLowerCase(); });
|
|
231
|
-
if (others.length > 0) mates = others;
|
|
232
|
-
if (mates.length === 0) mates = ["team-lead"];
|
|
233
|
-
for (var i = 0; i < mates.length; i++) {
|
|
234
|
-
var option = board.el("option", "", mates[i]);
|
|
235
|
-
option.value = mates[i];
|
|
236
|
-
select.appendChild(option);
|
|
237
|
-
}
|
|
238
|
-
row.appendChild(board.el("span", "board-dialog__label", "Assign to"));
|
|
239
|
-
row.appendChild(select);
|
|
240
|
-
row.appendChild(board.actionButton("Assign", function () { board.assignTo(card, select.value); }));
|
|
241
|
-
return row;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* The card dialog's footer: the claim an unowned card is waiting for, the lane
|
|
246
|
-
* buttons its owner may move to, then its destructive ones. Nothing here is a
|
|
247
|
-
* card-face action, so a reader cannot delete a card from the lane by hitting the
|
|
248
|
-
* wrong button.
|
|
249
|
-
*/
|
|
250
|
-
board.fillDialogActions = function (card, view) {
|
|
251
|
-
var footer = document.getElementById("card-dialog-actions");
|
|
252
|
-
if (!footer) return;
|
|
253
|
-
footer.textContent = "";
|
|
254
|
-
// Which verb a card allows is board.may's answer, the same one the card face and a
|
|
255
|
-
// queue row ask; this list only decides which of them the footer shows, and in what
|
|
256
|
-
// order. An unclaimed card is waiting for its first owner — without Claim the dialog
|
|
257
|
-
// offered a reader only Drop and Close on a card they had just opened.
|
|
258
|
-
if (board.may(card, view, "claim")) {
|
|
259
|
-
footer.appendChild(board.actionButton("Claim", function () { board.act("claim", card); }, "primary"));
|
|
260
|
-
}
|
|
261
|
-
if (board.may(card, view, "move")) {
|
|
262
|
-
// Only the lanes this card can reach: the lane it is in is the head's status, and
|
|
263
|
-
// a segmented control whose selected item is a disabled button reads as a live one.
|
|
264
|
-
var targets = ["doing", "blocked", "done"].filter(function (lane) { return lane !== card.status; });
|
|
265
|
-
var move = board.el("div", "board-dialog__move");
|
|
266
|
-
move.appendChild(board.el("span", "board-dialog__label", "Move to"));
|
|
267
|
-
var lanes = board.el("div", "bf-segmented");
|
|
268
|
-
for (var i = 0; i < targets.length; i++) {
|
|
269
|
-
(function (lane) {
|
|
270
|
-
var item = board.el("button", "bf-segmented__item", board.laneLabel(lane));
|
|
271
|
-
item.type = "button";
|
|
272
|
-
item.addEventListener("click", function () { board.act("move", card, { status: lane }); });
|
|
273
|
-
lanes.appendChild(item);
|
|
274
|
-
})(targets[i]);
|
|
275
|
-
}
|
|
276
|
-
move.appendChild(lanes);
|
|
277
|
-
footer.appendChild(move);
|
|
278
|
-
}
|
|
279
|
-
if (board.may(card, view, "assign")) footer.appendChild(assignControl(card, view));
|
|
280
|
-
if (board.may(card, view, "release")) {
|
|
281
|
-
footer.appendChild(board.actionButton("Release", function () { board.act("release", card); }));
|
|
282
|
-
}
|
|
283
|
-
// Fixing what the card says follows the same rule the rules do: the holder decides,
|
|
284
|
-
// and anyone decides while nobody holds it — including a finished card.
|
|
285
|
-
if (board.may(card, view, "edit")) {
|
|
286
|
-
footer.appendChild(board.actionButton("Edit", function () { board.editCard(card); }));
|
|
287
|
-
}
|
|
288
|
-
if (board.may(card, view, "drop")) {
|
|
289
|
-
footer.appendChild(board.actionButton("Drop", function () { board.act("drop", card); }, "danger"));
|
|
290
|
-
}
|
|
291
|
-
footer.appendChild(board.actionButton("Close", function () { board.closeCard(); }));
|
|
292
|
-
};
|
|
293
|
-
|
|
294
|
-
// A dialog the kit closed — Esc, a backdrop click, or its own close button —
|
|
295
|
-
// forgets which card it showed, so the next frame stops filling a hidden panel.
|
|
296
|
-
var cardDialog = document.getElementById("board-card-dialog");
|
|
297
|
-
if (cardDialog) {
|
|
298
|
-
new MutationObserver(function () {
|
|
299
|
-
if (cardDialog.hidden) board.openId = undefined;
|
|
300
|
-
}).observe(cardDialog, { attributes: true, attributeFilter: ["hidden"] });
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
board.banner = banner;
|
|
304
|
-
board.confirm = confirm;
|
|
305
|
-
board.post = post;
|
|
306
|
-
})();`;
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The page's write path: one request from a browser, one call into the store.
|
|
3
|
-
*
|
|
4
|
-
* A browser is not an identity, so every write here is made as the actor the page
|
|
5
|
-
* serves as — the session that started it, until a page has a seat. Two statuses matter
|
|
6
|
-
* to a client that retries: a field it can fix is 422, and a card somebody else moved is
|
|
7
|
-
* 409 with the card as it now stands. Everything else about a card's lifecycle is
|
|
8
|
-
* `lib/board/rules.ts`.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { normalizeCardId } from "../../lib/board/card.ts";
|
|
12
|
-
import type { Actor, CardOp } from "../../lib/board/rules.ts";
|
|
13
|
-
import type { BoardStore } from "../../lib/board/store.ts";
|
|
14
|
-
import { normalizeLabel } from "../../lib/session-label.ts";
|
|
15
|
-
import { createInput } from "./input.ts";
|
|
16
|
-
import type { PageResult, PageWrite } from "./transport/router.ts";
|
|
17
|
-
|
|
18
|
-
/** What a write needs: the store, who it is made as, and the clock. */
|
|
19
|
-
export interface WriteContext {
|
|
20
|
-
store: BoardStore;
|
|
21
|
-
actor: () => Actor;
|
|
22
|
-
now: () => number;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** One write, with the status the page shows as a banner. */
|
|
26
|
-
export async function writeOf(context: WriteContext, request: PageWrite): Promise<PageResult> {
|
|
27
|
-
const me = context.actor();
|
|
28
|
-
if (request.kind === "add") {
|
|
29
|
-
const parsed = createInput(request.body);
|
|
30
|
-
if ("error" in parsed) return { ok: false, status: 422, error: parsed.error };
|
|
31
|
-
const card = await context.store.create(parsed.input, me, context.now());
|
|
32
|
-
return { ok: true, card };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const id = normalizeCardId(request.id);
|
|
36
|
-
if (id === undefined) return { ok: false, status: 422, error: `"${request.id}" is not a card id.` };
|
|
37
|
-
const op = opOf(request);
|
|
38
|
-
if ("error" in op) return { ok: false, status: 422, error: op.error };
|
|
39
|
-
const result = await context.store.mutate(id, op.op, me, context.now());
|
|
40
|
-
if (!result.ok)
|
|
41
|
-
return {
|
|
42
|
-
ok: false,
|
|
43
|
-
status: 409,
|
|
44
|
-
error: result.error,
|
|
45
|
-
...(result.card === undefined ? {} : { card: result.card }),
|
|
46
|
-
};
|
|
47
|
-
return { ok: true, card: result.card };
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** The operation a card route names, or the reason it cannot be one. */
|
|
51
|
-
export function opOf(request: PageWrite): { op: CardOp } | { error: string } {
|
|
52
|
-
switch (request.kind) {
|
|
53
|
-
case "claim":
|
|
54
|
-
return { op: { kind: "claim" } };
|
|
55
|
-
case "release":
|
|
56
|
-
return { op: { kind: "release" } };
|
|
57
|
-
case "reclaim":
|
|
58
|
-
return { op: { kind: "reclaim" } };
|
|
59
|
-
case "assign": {
|
|
60
|
-
// A page cannot message a peer — a command context has no tool calls — so this
|
|
61
|
-
// records the owner and says in the card's history that nothing was sent.
|
|
62
|
-
const to = normalizeLabel(typeof request.body.to === "string" ? request.body.to : "", "");
|
|
63
|
-
if (to === "") return { error: "assign needs a teammate to hand the card to." };
|
|
64
|
-
return {
|
|
65
|
-
op: {
|
|
66
|
-
kind: "assign",
|
|
67
|
-
to: { label: to },
|
|
68
|
-
note: `assigned to ${to} from the board page; no handoff was sent`,
|
|
69
|
-
},
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
case "drop":
|
|
73
|
-
return { op: { kind: "drop" } };
|
|
74
|
-
case "note":
|
|
75
|
-
return { op: { kind: "note", text: request.body.text } };
|
|
76
|
-
case "plan":
|
|
77
|
-
return { op: { kind: "plan", path: request.body.path } };
|
|
78
|
-
case "edit":
|
|
79
|
-
// Every field is passed through as the browser sent it, including the absent
|
|
80
|
-
// ones: `applyOp` leaves an absent field alone, which is what keeps the page
|
|
81
|
-
// from clearing a link it never showed.
|
|
82
|
-
return {
|
|
83
|
-
op: {
|
|
84
|
-
kind: "edit",
|
|
85
|
-
title: request.body.title,
|
|
86
|
-
paths: request.body.paths,
|
|
87
|
-
priority: request.body.priority,
|
|
88
|
-
dependsOn: request.body.dependsOn,
|
|
89
|
-
},
|
|
90
|
-
};
|
|
91
|
-
case "move": {
|
|
92
|
-
const status = request.body.status;
|
|
93
|
-
if (status === "doing" || status === "blocked" || status === "done") return { op: { kind: "move", status } };
|
|
94
|
-
return { error: 'move needs a target lane: "doing", "blocked", or "done".' };
|
|
95
|
-
}
|
|
96
|
-
default:
|
|
97
|
-
return { error: "add is not a card route." };
|
|
98
|
-
}
|
|
99
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The Extensions section's focused-row pane.
|
|
3
|
-
*
|
|
4
|
-
* The row, the summary, and the key hints are the shared list grammar; what is
|
|
5
|
-
* specific here is the doctor's findings, including the resource path when a row
|
|
6
|
-
* has no verdict.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
10
|
-
import { sanitizeLine } from "../../../../lib/format.ts";
|
|
11
|
-
import { clip } from "../../../../lib/tui/columns.ts";
|
|
12
|
-
import { paint } from "../../../../lib/tui/tokens.ts";
|
|
13
|
-
import { detailLines as reportDetailLines } from "../../doctor/format.ts";
|
|
14
|
-
import { rowBadge } from "../format.ts";
|
|
15
|
-
import type { PanelRow } from "../types.ts";
|
|
16
|
-
import { savedLine } from "./frame.ts";
|
|
17
|
-
|
|
18
|
-
/** The focused extension's findings and the pending-changes note. */
|
|
19
|
-
export function extensionDetail(
|
|
20
|
-
theme: Theme,
|
|
21
|
-
width: number,
|
|
22
|
-
row: PanelRow | undefined,
|
|
23
|
-
budget: number,
|
|
24
|
-
expanded: boolean,
|
|
25
|
-
dirty: boolean,
|
|
26
|
-
): string[] {
|
|
27
|
-
const lines: string[] = [];
|
|
28
|
-
const badge = row ? rowBadge(row) : undefined;
|
|
29
|
-
// The focused row directly above already carries the entry's name and verdict,
|
|
30
|
-
// so the pane starts with what the row cannot say: the findings, or the
|
|
31
|
-
// resource path when nothing was reported.
|
|
32
|
-
if (badge?.text === undefined && row?.item) {
|
|
33
|
-
lines.push(clip(` ${paint(theme, "meta", sanitizeLine(row.item.path))}`, width));
|
|
34
|
-
}
|
|
35
|
-
if (row?.report && budget > 0) {
|
|
36
|
-
lines.push(...reportDetailLines(row.report, theme, width, budget, expanded));
|
|
37
|
-
}
|
|
38
|
-
lines.push(...savedLine(theme, width, dirty));
|
|
39
|
-
return lines;
|
|
40
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The Skills section: the focused skill's path, description, and refusal.
|
|
3
|
-
*
|
|
4
|
-
* Rows, the summary, and the key hints come from the shared list view, and the
|
|
5
|
-
* row verdict is already `rowBadge`'s. The pane therefore starts with what the
|
|
6
|
-
* row cannot say: the resolved file and what its frontmatter declares, or why Pi
|
|
7
|
-
* refused it. Pure functions over the merged row; the component owns the cursor,
|
|
8
|
-
* the scope, and the expansion.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
12
|
-
import { wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
13
|
-
import { sanitizeLine } from "../../../../lib/format.ts";
|
|
14
|
-
import { clip } from "../../../../lib/tui/columns.ts";
|
|
15
|
-
import { BODY_INDENT } from "../../../../lib/tui/indent.ts";
|
|
16
|
-
import { paint } from "../../../../lib/tui/tokens.ts";
|
|
17
|
-
import type { PanelRow } from "../types.ts";
|
|
18
|
-
import { INDENT, savedLine } from "./frame.ts";
|
|
19
|
-
|
|
20
|
-
/** The focused skill's file, description (or refusal), and pending changes. */
|
|
21
|
-
export function skillDetail(options: {
|
|
22
|
-
theme: Theme;
|
|
23
|
-
width: number;
|
|
24
|
-
row: PanelRow | undefined;
|
|
25
|
-
budget: number;
|
|
26
|
-
expanded: boolean;
|
|
27
|
-
dirty: boolean;
|
|
28
|
-
}): string[] {
|
|
29
|
-
const { theme, width, row, budget, expanded, dirty } = options;
|
|
30
|
-
// The focused row above already carries the name and verdict, so the pane
|
|
31
|
-
// starts with the file it resolves to.
|
|
32
|
-
const lines: string[] = [];
|
|
33
|
-
if (row?.item) lines.push(clip(`${INDENT}${paint(theme, "meta", sanitizeLine(row.item.path))}`, width));
|
|
34
|
-
const saved = savedLine(theme, width, dirty);
|
|
35
|
-
const skill = row?.skill;
|
|
36
|
-
const text = skill?.problem !== undefined ? `Not loaded: ${skill.problem}` : skill?.description;
|
|
37
|
-
if (text && budget > 1) {
|
|
38
|
-
// One line while collapsed, the whole wrapped description when expanded,
|
|
39
|
-
// minus the file line already pushed and the saved marker below it.
|
|
40
|
-
const available = Math.max(0, budget - lines.length - saved.length);
|
|
41
|
-
const room = expanded ? available : Math.min(1, available);
|
|
42
|
-
const bodyWidth = Math.max(1, width - BODY_INDENT);
|
|
43
|
-
const role = skill?.problem !== undefined ? "warn" : "meta";
|
|
44
|
-
for (const line of wrapTextWithAnsi(sanitizeLine(text), bodyWidth).slice(0, room)) {
|
|
45
|
-
lines.push(clip(`${INDENT}${paint(theme, role, line)}`, width));
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
lines.push(...saved);
|
|
49
|
-
return lines;
|
|
50
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Process plumbing that only `job` needs.
|
|
3
|
-
*
|
|
4
|
-
* The shared spawn, tree-kill, and liveness seams live in `lib/spawn.ts`, so
|
|
5
|
-
* what stays here is the job-specific remainder: the pid liveness and start-time
|
|
6
|
-
* seams the runtime injects, and the `ps` token that guards against pid reuse
|
|
7
|
-
* when a reattached job is settled.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { execFileSync } from "node:child_process";
|
|
11
|
-
|
|
12
|
-
/** Whether a pid is still alive. */
|
|
13
|
-
export type LivenessFn = (pid: number) => boolean;
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Best-effort OS start-time token for a pid, used to detect pid reuse.
|
|
17
|
-
* Returns undefined when the platform cannot provide one (for example Windows,
|
|
18
|
-
* or a process that is already gone), in which case callers fall back to
|
|
19
|
-
* liveness alone.
|
|
20
|
-
*/
|
|
21
|
-
export type StartTokenFn = (pid: number) => string | undefined;
|
|
22
|
-
|
|
23
|
-
/** Default start-time token via `ps`. Undefined where `ps` is unavailable. */
|
|
24
|
-
export const defaultStartToken: StartTokenFn = (pid) => {
|
|
25
|
-
try {
|
|
26
|
-
const out = execFileSync("ps", ["-p", String(pid), "-o", "lstart="], {
|
|
27
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
28
|
-
timeout: 2000,
|
|
29
|
-
});
|
|
30
|
-
const text = out.toString().trim();
|
|
31
|
-
return text || undefined;
|
|
32
|
-
} catch {
|
|
33
|
-
return undefined;
|
|
34
|
-
}
|
|
35
|
-
};
|