@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
|
@@ -13,11 +13,37 @@
|
|
|
13
13
|
* offset, and an open dialog all survive a peer's write.
|
|
14
14
|
*
|
|
15
15
|
* The client modules share one namespace, `window.bfBoard`: this one paints,
|
|
16
|
-
* `team.ts` draws the round, `
|
|
17
|
-
* the frame stream and the wiring.
|
|
16
|
+
* `team.ts` draws the round, `dialog.ts` opens a card, `keys.ts` binds, and `live.ts`
|
|
17
|
+
* owns the frame stream and the wiring.
|
|
18
|
+
*
|
|
19
|
+
* `plural`, `tabTitle`, `matchesQuery`, `el`, `typing`, and `paintStatus` are not written
|
|
20
|
+
* here. Their one bodies live in `lib/` beside the rules they express, and the assembler
|
|
21
|
+
* hands the source in — a second copy in the bundle is a second wording for `1 card`, a
|
|
22
|
+
* second shape for the tab, a filter that finds a different card than the tool does, or an
|
|
23
|
+
* element builder and a typing test that drift from the review page's.
|
|
18
24
|
*/
|
|
19
25
|
|
|
20
|
-
|
|
26
|
+
/**
|
|
27
|
+
* The renderer, with what it needs spliced in: the counts' wording, the tab title's
|
|
28
|
+
* shape, what a query matches, and the browser runtime's element builder and typing test —
|
|
29
|
+
* each written once in `lib/` and handed in as source.
|
|
30
|
+
*/
|
|
31
|
+
export function renderJs(rules: {
|
|
32
|
+
plural: string;
|
|
33
|
+
tabTitle: string;
|
|
34
|
+
matches: string;
|
|
35
|
+
age: string;
|
|
36
|
+
runtime: string;
|
|
37
|
+
}): string {
|
|
38
|
+
return renderTemplate
|
|
39
|
+
.replace("__PLURAL__", rules.plural)
|
|
40
|
+
.replace("__TAB_TITLE__", rules.tabTitle)
|
|
41
|
+
.replace("__MATCHES__", rules.matches)
|
|
42
|
+
.replace("__AGE__", rules.age)
|
|
43
|
+
.replace("__RUNTIME__", rules.runtime);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const renderTemplate = String.raw`(function () {
|
|
21
47
|
"use strict";
|
|
22
48
|
var board = window.bfBoard || (window.bfBoard = {});
|
|
23
49
|
|
|
@@ -26,14 +52,18 @@ export const BOARD_RENDER_JS = String.raw`(function () {
|
|
|
26
52
|
var LANE_EMPTY = { todo: "nothing waiting", doing: "nothing in flight", blocked: "nothing blocked", done: "nothing finished yet" };
|
|
27
53
|
/** Path chips a lane card shows before it counts the rest. */
|
|
28
54
|
var PATH_CHIPS = 3;
|
|
55
|
+
/**
|
|
56
|
+
* Finished cards a lane may hold before it starts folded.
|
|
57
|
+
*
|
|
58
|
+
* A board of 26 cards is 19 of them finished, and an open Done lane is four times the
|
|
59
|
+
* height of the three that carry work. Three or four is work a reader still checks;
|
|
60
|
+
* past this it is history, and the count in the head says how much.
|
|
61
|
+
*/
|
|
62
|
+
var DONE_FOLD_AT = 6;
|
|
63
|
+
/** The two tones a reason line can take, by the kind reasonOf reports. */
|
|
64
|
+
var REASON_TONE = { blocked: N.cardReasonBlocked, unplanned: N.cardReasonUnplanned };
|
|
29
65
|
|
|
30
|
-
|
|
31
|
-
function el(tag, className, text) {
|
|
32
|
-
var node = document.createElement(tag);
|
|
33
|
-
if (className) node.className = className;
|
|
34
|
-
if (text !== undefined && text !== null) node.textContent = String(text);
|
|
35
|
-
return node;
|
|
36
|
-
}
|
|
66
|
+
__RUNTIME__
|
|
37
67
|
|
|
38
68
|
/**
|
|
39
69
|
* One button that calls back, so nothing on the page is a click handler in markup.
|
|
@@ -55,16 +85,14 @@ export const BOARD_RENDER_JS = String.raw`(function () {
|
|
|
55
85
|
return el("span", "bf-chip" + (tone ? " bf-chip--" + tone : ""), text);
|
|
56
86
|
}
|
|
57
87
|
|
|
58
|
-
/**
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return Math.round(hours / 24) + "d";
|
|
67
|
-
}
|
|
88
|
+
/**
|
|
89
|
+
* "12m", "3h", "2d" — an age, compact enough for a meta line.
|
|
90
|
+
*
|
|
91
|
+
* The transcript prints the same rule for the same fact, so the body is spliced in from
|
|
92
|
+
* lib/format.ts rather than written twice. No backticks in here: this is a template.
|
|
93
|
+
*/
|
|
94
|
+
var ageText = __AGE__;
|
|
95
|
+
function ago(at) { return ageText(Date.now(), at); }
|
|
68
96
|
|
|
69
97
|
/** Bytes, the way the footer says them. */
|
|
70
98
|
function size(bytes) {
|
|
@@ -87,106 +115,41 @@ export const BOARD_RENDER_JS = String.raw`(function () {
|
|
|
87
115
|
return undefined;
|
|
88
116
|
}
|
|
89
117
|
|
|
90
|
-
/** Whether this session holds the card. */
|
|
91
|
-
function mine(card, me) {
|
|
92
|
-
if (!card.owner) return false;
|
|
93
|
-
if (card.owner.id !== undefined && me.id !== undefined) return card.owner.id === me.id;
|
|
94
|
-
return String(card.owner.label).toLowerCase() === String(me.label).toLowerCase();
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** A card's priority; an older card that never recorded one is ordinary. */
|
|
98
|
-
function priorityOf(card) {
|
|
99
|
-
return card.priority || "normal";
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* The cards this one waits for that are not finished yet.
|
|
104
|
-
*
|
|
105
|
-
* The board is the index: a link is one id in the waiting card's file, so the other
|
|
106
|
-
* card's lane can only be read from the frame. A finished dependency has stopped
|
|
107
|
-
* explaining anything, so it drops out of the answer.
|
|
108
|
-
*/
|
|
109
|
-
function blockersOf(card, view) {
|
|
110
|
-
var ids = card.dependsOn || [];
|
|
111
|
-
if (ids.length === 0 || !view) return [];
|
|
112
|
-
var all = cards(view);
|
|
113
|
-
var open = [];
|
|
114
|
-
for (var i = 0; i < ids.length; i++) {
|
|
115
|
-
for (var j = 0; j < all.length; j++) {
|
|
116
|
-
if (all[j].id === ids[i] && all[j].status !== "done") open.push(all[j]);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
return open;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/** How the board says what a card waits for: the id and the lane that explains it. */
|
|
123
|
-
function waitingText(card, view) {
|
|
124
|
-
return blockersOf(card, view)
|
|
125
|
-
.map(function (blocker) { return "#" + blocker.id + " (" + blocker.status + ")"; })
|
|
126
|
-
.join(", ");
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Whether this reader may do one verb to one card.
|
|
131
|
-
*
|
|
132
|
-
* The one table behind three surfaces: a card face, the dialog's footer, and a queue
|
|
133
|
-
* row each draw a different subset — a face offers two actions, the dialog five — but
|
|
134
|
-
* a verb is allowed for the same reader on all of them, and it is the same rule the
|
|
135
|
-
* server applies in lib/board/rules.ts. Open is always allowed: reading is not a write.
|
|
136
|
-
*/
|
|
137
|
-
function may(card, view, verb) {
|
|
138
|
-
// Reading is not a write, so it survives a viewer's page; everything else is what
|
|
139
|
-
// the document's authority says it may be.
|
|
140
|
-
if (verb === "open") return true;
|
|
141
|
-
if (!canWrite()) return false;
|
|
142
|
-
var isMine = mine(card, view.me);
|
|
143
|
-
var waiting = blockersOf(card, view).length > 0;
|
|
144
|
-
var finished = card.status === "done";
|
|
145
|
-
if (verb === "claim") return card.status === "todo" && !waiting;
|
|
146
|
-
if (verb === "move") return isMine && !finished;
|
|
147
|
-
if (verb === "release") return isMine && !finished;
|
|
148
|
-
if (verb === "edit") return card.owner === undefined || isMine;
|
|
149
|
-
if (verb === "drop") return card.status === "todo" || finished || isMine;
|
|
150
|
-
if (verb === "reclaim") return card.owner !== undefined && !finished;
|
|
151
|
-
// Recording an owner is what a page can do where it cannot message: an unowned card
|
|
152
|
-
// that is not waiting for another, which is the same card a claim is offered.
|
|
153
|
-
if (verb === "assign") return card.owner === undefined && !finished && !waiting;
|
|
154
|
-
return false;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Whether this page may change a card at all.
|
|
159
|
-
*
|
|
160
|
-
* The document is rendered per request, so a viewer's shell says data-writes="0":
|
|
161
|
-
* one attribute decides every affordance, and the server refuses anyway — a page
|
|
162
|
-
* that offered what it would be turned down for teaches a reader the wrong thing.
|
|
163
|
-
*/
|
|
164
|
-
function canWrite() {
|
|
165
|
-
return document.documentElement.getAttribute("data-writes") !== "0";
|
|
166
|
-
}
|
|
167
|
-
|
|
168
118
|
/** The one line under a card's meta: why it sits where it does. */
|
|
169
119
|
function reasonOf(card, view) {
|
|
170
120
|
var last = card.notes.length > 0 ? card.notes[card.notes.length - 1] : undefined;
|
|
171
121
|
// An order is the strongest reason a card is not being worked on, and the one a
|
|
172
122
|
// reader cannot see from the lane: it comes before the note that says the same.
|
|
173
|
-
|
|
174
|
-
if (
|
|
123
|
+
// The sentence is the snapshot's, so this line and the tool's refusal name one list.
|
|
124
|
+
if (card.waits !== undefined) return { text: "waits on " + card.waits, kind: "waiting" };
|
|
175
125
|
if (card.status === "blocked") {
|
|
176
126
|
// The lane already says blocked and the tone already says danger; the line says why.
|
|
127
|
+
// The recorded reason comes first: the note is what somebody happened to write last,
|
|
128
|
+
// and it is the reason a move to blocked asks for. The slot carries the reason alone —
|
|
129
|
+
// the lane head already says blocked, and a status prefix in it would be a second voice.
|
|
130
|
+
if (card.blockedBy) return { text: card.blockedBy, kind: "blocked" };
|
|
177
131
|
return { text: last ? last.text : "blocked, with nothing recorded", kind: "blocked" };
|
|
178
132
|
}
|
|
133
|
+
// An offer is a decision the lead already made once: the lane says nobody holds it, and
|
|
134
|
+
// this says how long the broadcast has gone unanswered, which is the difference between
|
|
135
|
+
// nobody having looked and nobody taking it.
|
|
136
|
+
if (card.status === "todo" && card.offeredAt !== undefined) {
|
|
137
|
+
return { text: "offered " + ago(card.offeredAt) + " ago · nobody took it", kind: "waiting" };
|
|
138
|
+
}
|
|
179
139
|
if (card.status === "doing" && !card.plan) {
|
|
180
140
|
return { text: "no plan recorded", kind: "unplanned" };
|
|
181
141
|
}
|
|
182
142
|
if (card.status === "doing") return { text: "planned", kind: "plan" };
|
|
183
|
-
if (card.status === "done")
|
|
143
|
+
if (card.status === "done") {
|
|
144
|
+
var landed = card.landed ? " · landed " + card.landed.commit : "";
|
|
145
|
+
return { text: "✓ done" + (card.owner ? " by " + card.owner.label : "") + landed, kind: "done" };
|
|
146
|
+
}
|
|
184
147
|
return last ? { text: last.text, kind: "note" } : undefined;
|
|
185
148
|
}
|
|
186
149
|
|
|
187
150
|
/** A path hint, or the +N chip that points at the rest. */
|
|
188
151
|
function paths(card, limit) {
|
|
189
|
-
var list = el("ul",
|
|
152
|
+
var list = el("ul", N.cardPaths);
|
|
190
153
|
var shown = card.paths.slice(0, limit);
|
|
191
154
|
for (var i = 0; i < shown.length; i++) {
|
|
192
155
|
var item = el("li", "");
|
|
@@ -203,25 +166,25 @@ export const BOARD_RENDER_JS = String.raw`(function () {
|
|
|
203
166
|
|
|
204
167
|
/** One card in its lane. */
|
|
205
168
|
function cardNode(card, view) {
|
|
206
|
-
var item = el("li",
|
|
169
|
+
var item = el("li", N.card);
|
|
207
170
|
item.setAttribute("data-card", card.id);
|
|
208
171
|
item.setAttribute("data-status", card.status);
|
|
209
172
|
item.setAttribute("data-rev", String(card.rev));
|
|
210
173
|
|
|
211
|
-
var head = el("p",
|
|
212
|
-
head.appendChild(el("span",
|
|
174
|
+
var head = el("p", N.cardHead);
|
|
175
|
+
head.appendChild(el("span", N.cardId, "#" + card.id));
|
|
213
176
|
var title = actionButton(card.title, function () {
|
|
214
177
|
board.openCard(card.id);
|
|
215
178
|
});
|
|
216
|
-
title.className =
|
|
179
|
+
title.className = N.cardTitle;
|
|
217
180
|
head.appendChild(title);
|
|
218
181
|
item.appendChild(head);
|
|
219
182
|
|
|
220
|
-
var meta = el("p",
|
|
183
|
+
var meta = el("p", N.cardMeta);
|
|
221
184
|
meta.appendChild(card.owner ? chip(card.owner.label, "accent") : el("span", "", "unassigned"));
|
|
222
185
|
// The order inside a lane is the priority, so an urgent card says so: an order
|
|
223
186
|
// nobody can see reads as an accident. The lane's colour stays the card's own.
|
|
224
|
-
if (
|
|
187
|
+
if (card.priority !== "normal") meta.appendChild(chip(card.priority));
|
|
225
188
|
// The stamp, so the clock can keep the age honest between frames: a board can be
|
|
226
189
|
// quiet for ten minutes while the page still says "live".
|
|
227
190
|
var age = el("span", "", ago(card.updatedAt));
|
|
@@ -233,37 +196,42 @@ export const BOARD_RENDER_JS = String.raw`(function () {
|
|
|
233
196
|
|
|
234
197
|
var reason = reasonOf(card, view);
|
|
235
198
|
if (reason) {
|
|
236
|
-
|
|
237
|
-
|
|
199
|
+
// The tone is a lookup, not a suffix: a name assembled from a prefix is a name no
|
|
200
|
+
// test can compare with the stylesheet.
|
|
201
|
+
var tone = REASON_TONE[reason.kind];
|
|
202
|
+
var line = el("p", tone === undefined ? N.cardReason : N.cardReason + " " + tone, reason.text);
|
|
238
203
|
// It clamps to one line on the card, so the full sentence is the tooltip.
|
|
239
204
|
line.title = reason.text;
|
|
240
205
|
item.appendChild(line);
|
|
241
206
|
}
|
|
242
207
|
|
|
243
|
-
var actions = el("div", "board-card__actions");
|
|
244
|
-
// A card that waits cannot be claimed, so the card face does not offer it: the reason
|
|
245
|
-
// line above is what it says instead. Move is not a permission but the way into the
|
|
246
|
-
// dialog, so it is drawn for any card still in flight, whoever holds it.
|
|
247
|
-
if (may(card, view, "claim")) {
|
|
248
|
-
actions.appendChild(actionButton("Claim", function () { board.act("claim", card); }, "plain"));
|
|
249
|
-
} else if (canWrite() && card.status !== "todo" && card.status !== "done") {
|
|
250
|
-
// Move is the way into the dialog, not a permission, but a dialog with no actions
|
|
251
|
-
// in it is not worth opening.
|
|
252
|
-
actions.appendChild(actionButton("Move", function () { board.openCard(card.id, "move"); }, "plain"));
|
|
253
|
-
}
|
|
254
|
-
// A note is a write, and a viewer's dialog has no field for one.
|
|
255
|
-
if (canWrite()) actions.appendChild(actionButton("Note", function () { board.openCard(card.id, "note"); }));
|
|
256
|
-
actions.appendChild(actionButton("Open", function () { board.openCard(card.id); }));
|
|
257
|
-
item.appendChild(actions);
|
|
258
208
|
return item;
|
|
259
209
|
}
|
|
260
210
|
|
|
261
|
-
/**
|
|
211
|
+
/**
|
|
212
|
+
* The cards as the page draws them.
|
|
213
|
+
*
|
|
214
|
+
* A folded lane keeps its cards in the document, and so does a panel the other tab
|
|
215
|
+
* hides: the box is the only honest test of whether a reader can see one, and it is
|
|
216
|
+
* what j/k walks and what the dialog's position counts.
|
|
217
|
+
*/
|
|
218
|
+
function drawnCards() {
|
|
219
|
+
var nodes = document.querySelectorAll(".board-card");
|
|
220
|
+
var seen = [];
|
|
221
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
222
|
+
if (nodes[i].hidden) continue;
|
|
223
|
+
if (!nodes[i].getClientRects || nodes[i].getClientRects().length === 0) continue;
|
|
224
|
+
seen.push(nodes[i]);
|
|
225
|
+
}
|
|
226
|
+
return seen;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** The toolbar's identity, progress, footer, and the tab's own name. */
|
|
262
230
|
function renderChrome(view) {
|
|
263
231
|
var me = document.getElementById("board-me");
|
|
264
232
|
if (me) {
|
|
265
233
|
me.textContent = view.me.label;
|
|
266
|
-
me.title = "
|
|
234
|
+
me.title = "This page is served as " + view.me.label + ", and it cannot change a card";
|
|
267
235
|
}
|
|
268
236
|
var progress = document.getElementById("board-progress");
|
|
269
237
|
if (progress) {
|
|
@@ -275,22 +243,25 @@ export const BOARD_RENDER_JS = String.raw`(function () {
|
|
|
275
243
|
var text = document.getElementById("board-progress-text");
|
|
276
244
|
if (text) text.textContent = view.counts.done + "/" + total + " done";
|
|
277
245
|
}
|
|
278
|
-
// A viewer gets the board, not the chance to make one: the composer and the empty
|
|
279
|
-
// state's way into it belong to whoever serves the page.
|
|
280
|
-
var create = document.getElementById("board-new");
|
|
281
|
-
if (create) create.hidden = !canWrite();
|
|
282
|
-
var first = document.getElementById("board-first-card");
|
|
283
|
-
if (first) first.hidden = !canWrite();
|
|
284
246
|
var where = document.getElementById("board-where");
|
|
285
247
|
if (where) {
|
|
286
|
-
|
|
248
|
+
// A footer that only counts read cards would say "2 cards" over a board holding
|
|
249
|
+
// three files, so the unreadable ones are named beside the size.
|
|
250
|
+
where.textContent =
|
|
251
|
+
"store " + view.where + " · " + plural(view.counts.total, "card") + " · " + size(view.bytes) +
|
|
252
|
+
(view.skipped > 0 ? " · " + plural(view.skipped, "unreadable") : "");
|
|
287
253
|
}
|
|
254
|
+
// The tab has to tell two boards apart, and a board's own name is the last segment of
|
|
255
|
+
// its store: one board per repository, so that key is the subject. The count is what
|
|
256
|
+
// a browser full of tabs is read for, and an empty board says nothing rather than 0.
|
|
257
|
+
var open = view.counts.total - view.counts.done;
|
|
258
|
+
document.title = tabTitle("board", view.where.split("/").pop(), open > 0 ? open + " open" : undefined);
|
|
288
259
|
}
|
|
289
260
|
|
|
290
|
-
/** 1 card, 2 cards. */
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
261
|
+
/** 1 card, 2 cards — the one body, handed in by the bundle's assembler. */
|
|
262
|
+
var plural = __PLURAL__;
|
|
263
|
+
/** board · my-pi-agent · 3 open, the tab's one shape, handed in the same way. */
|
|
264
|
+
var tabTitle = __TAB_TITLE__;
|
|
294
265
|
|
|
295
266
|
/**
|
|
296
267
|
* Rewrite every age already on the page from its own timestamp.
|
|
@@ -308,38 +279,31 @@ export const BOARD_RENDER_JS = String.raw`(function () {
|
|
|
308
279
|
}
|
|
309
280
|
}
|
|
310
281
|
|
|
311
|
-
/** Whether the filter keeps a card:
|
|
312
|
-
|
|
313
|
-
if (!query) return true;
|
|
314
|
-
var needle = query.toLowerCase();
|
|
315
|
-
if (card.title.toLowerCase().indexOf(needle) !== -1) return true;
|
|
316
|
-
if (card.owner && card.owner.label.toLowerCase().indexOf(needle) !== -1) return true;
|
|
317
|
-
for (var i = 0; i < card.paths.length; i++) if (card.paths[i].toLowerCase().indexOf(needle) !== -1) return true;
|
|
318
|
-
return false;
|
|
319
|
-
}
|
|
282
|
+
/** Whether the filter keeps a card: the module's one predicate, spliced in as source. */
|
|
283
|
+
var matches = __MATCHES__;
|
|
320
284
|
|
|
321
285
|
/** The lane section for one status, created once and then reused. */
|
|
322
286
|
function laneNode(lane) {
|
|
323
287
|
var section = document.querySelector('.board-lane[data-status="' + lane.status + '"]');
|
|
324
288
|
if (section) return section;
|
|
325
|
-
section = el("section",
|
|
289
|
+
section = el("section", N.lane);
|
|
326
290
|
section.setAttribute("data-status", lane.status);
|
|
327
|
-
var head = el("h2",
|
|
291
|
+
var head = el("h2", N.laneHead);
|
|
328
292
|
var toggle = actionButton(LANE_LABEL[lane.status] || lane.status, function () {
|
|
329
293
|
board.toggleLane(lane.status);
|
|
330
294
|
});
|
|
331
|
-
toggle.className =
|
|
295
|
+
toggle.className = N.laneToggle;
|
|
332
296
|
toggle.setAttribute("aria-expanded", "true");
|
|
333
297
|
toggle.setAttribute("aria-controls", "lane-" + lane.status);
|
|
334
298
|
toggle.id = "lane-head-" + lane.status;
|
|
335
|
-
head.appendChild(el("span",
|
|
299
|
+
head.appendChild(el("span", N.laneDot));
|
|
336
300
|
head.appendChild(toggle);
|
|
337
|
-
head.appendChild(el("span",
|
|
301
|
+
head.appendChild(el("span", N.laneCount));
|
|
338
302
|
section.appendChild(head);
|
|
339
|
-
var list = el("ol",
|
|
303
|
+
var list = el("ol", N.laneList);
|
|
340
304
|
list.id = "lane-" + lane.status;
|
|
341
305
|
section.appendChild(list);
|
|
342
|
-
section.appendChild(el("p",
|
|
306
|
+
section.appendChild(el("p", N.laneEmpty, LANE_EMPTY[lane.status] || "nothing"));
|
|
343
307
|
return section;
|
|
344
308
|
}
|
|
345
309
|
|
|
@@ -360,6 +324,21 @@ export const BOARD_RENDER_JS = String.raw`(function () {
|
|
|
360
324
|
var lane = view.lanes[i];
|
|
361
325
|
var section = laneNode(lane);
|
|
362
326
|
if (section.parentNode !== lanes) lanes.appendChild(section);
|
|
327
|
+
// Finished work starts folded, once: the default is what the page opens with, and
|
|
328
|
+
// toggleLane writes a boolean from then on, so a reader who unfolds Done keeps it
|
|
329
|
+
// unfolded through every frame a peer's write causes.
|
|
330
|
+
if (lane.status === "done" && lane.cards.length >= DONE_FOLD_AT) {
|
|
331
|
+
board.collapsed = board.collapsed || {};
|
|
332
|
+
if (board.collapsed[lane.status] === undefined) board.collapsed[lane.status] = true;
|
|
333
|
+
}
|
|
334
|
+
// A lane holding nothing is a rail: its dot, its word, and its count, in the width
|
|
335
|
+
// the fact deserves. The filter never does this — filtering is a view of the cards,
|
|
336
|
+
// not a statement that the lane has none.
|
|
337
|
+
section.setAttribute("data-empty", lane.cards.length === 0 ? "true" : "false");
|
|
338
|
+
// The lane's share of the row follows the work it holds: the sheet turns this number
|
|
339
|
+
// into a flex-grow, so two cards earn more width than one and a title is not wrapped
|
|
340
|
+
// for want of a column the lane is not using. A count, never text.
|
|
341
|
+
section.style.setProperty("--lane-count", String(lane.cards.length));
|
|
363
342
|
var shown = lane.cards.filter(function (card) { return matches(card, query); });
|
|
364
343
|
var sig = lane.cards.map(function (card) { return card.id + ":" + card.rev; }).join(",") + "|" + query;
|
|
365
344
|
if (section.getAttribute("data-sig") !== sig) {
|
|
@@ -382,6 +361,10 @@ export const BOARD_RENDER_JS = String.raw`(function () {
|
|
|
382
361
|
var collapsed = board.collapsed && board.collapsed[lane.status] === true;
|
|
383
362
|
var list = section.querySelector(".board-lane__list");
|
|
384
363
|
if (list) list.hidden = collapsed;
|
|
364
|
+
// A folded lane shows nothing, so it takes the rail's width the way an empty one
|
|
365
|
+
// does: the count in its head is the whole fact, and the lanes with cards keep the
|
|
366
|
+
// width that would otherwise be a tray of nothing.
|
|
367
|
+
section.setAttribute("data-folded", collapsed ? "true" : "false");
|
|
385
368
|
var toggle = section.querySelector(".board-lane__toggle");
|
|
386
369
|
if (toggle) toggle.setAttribute("aria-expanded", String(!collapsed));
|
|
387
370
|
}
|
|
@@ -409,60 +392,104 @@ export const BOARD_RENDER_JS = String.raw`(function () {
|
|
|
409
392
|
/** Fill the card dialog's contents from a card, without opening or closing it. */
|
|
410
393
|
function fillDialog(card, view) {
|
|
411
394
|
document.getElementById("card-dialog-title").textContent = card.title;
|
|
395
|
+
document.getElementById("card-dialog-id").textContent = "#" + card.id;
|
|
412
396
|
// The lane is a status of its own, not a word inside a sentence: the same dot and
|
|
413
397
|
// label a lane head wears, so the dialog and the board speak the same vocabulary.
|
|
414
398
|
var state = document.getElementById("card-dialog-state");
|
|
415
399
|
state.setAttribute("data-status", card.status);
|
|
416
400
|
state.textContent = card.status;
|
|
417
|
-
|
|
418
|
-
|
|
401
|
+
|
|
402
|
+
// One fact per row of the ladder, so a reader can pick out a value instead of
|
|
403
|
+
// hunting for it in a sentence. A row with nothing to say takes its label with it:
|
|
404
|
+
// an empty cell under a heading reads as a missing value, not as an absence.
|
|
405
|
+
var pair = function (name, shown) {
|
|
406
|
+
var dt = document.getElementById("card-dialog-" + name + "-label");
|
|
407
|
+
var dd = document.getElementById("card-dialog-" + name);
|
|
408
|
+
if (dt) dt.hidden = !shown;
|
|
409
|
+
if (dd) dd.hidden = !shown;
|
|
410
|
+
return dd;
|
|
411
|
+
};
|
|
412
|
+
var setRow = function (name, shown, value, full) {
|
|
413
|
+
var dd = pair(name, shown);
|
|
414
|
+
dd.textContent = shown ? value : "";
|
|
415
|
+
if (full === undefined) dd.removeAttribute("title");
|
|
416
|
+
else dd.setAttribute("title", full);
|
|
417
|
+
};
|
|
418
|
+
|
|
419
419
|
var owner = document.getElementById("card-dialog-owner");
|
|
420
420
|
owner.textContent = "";
|
|
421
421
|
owner.appendChild(card.owner ? chip(card.owner.label, "accent") : document.createTextNode("unassigned"));
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
var
|
|
422
|
+
pair("owner", true);
|
|
423
|
+
|
|
424
|
+
var priority = pair("priority", card.priority !== "normal");
|
|
425
|
+
priority.textContent = "";
|
|
426
|
+
if (card.priority !== "normal") priority.appendChild(chip(card.priority));
|
|
427
|
+
|
|
428
|
+
setRow("updated", true, ago(card.updatedAt));
|
|
429
|
+
// How long the card has been in the lane it is in now, which updatedAt cannot say:
|
|
430
|
+
// a note filed against a blocked card moves that stamp without moving the card. Absent
|
|
431
|
+
// for a card written before the log existed, so the row takes its label with it.
|
|
432
|
+
setRow("lane", card.since !== undefined, card.status + " · " + ago(card.since));
|
|
433
|
+
setRow("by", true, card.by);
|
|
434
|
+
setRow("landed", card.landed !== undefined, card.landed ? card.landed.commit : "", card.landed ? card.landed.commit : undefined);
|
|
435
|
+
|
|
436
|
+
// The dialog and the card face say one thing, from one field: the ids it was linked
|
|
437
|
+
// to are the storage, and a reader is owed the lanes that make them a decision.
|
|
438
|
+
setRow("depends", card.waits !== undefined, card.waits || "", card.waits);
|
|
439
|
+
// The other side of the same link: what leaving this card undone is holding up, which
|
|
440
|
+
// is the one thing a card's own row cannot say.
|
|
441
|
+
setRow("dependents", card.dependents !== undefined, card.dependents || "", card.dependents);
|
|
442
|
+
|
|
443
|
+
// A long plan path elides on one line, and the whole of it is on the hover title and
|
|
444
|
+
// in the text, so it stays copyable: the reason to open a card is often its plan.
|
|
445
|
+
setRow("plan", Boolean(card.plan), card.plan || "", card.plan || undefined);
|
|
446
|
+
|
|
447
|
+
pair("paths", card.paths.length > 0);
|
|
448
|
+
var pathList = document.getElementById("card-dialog-path-list");
|
|
425
449
|
pathList.textContent = "";
|
|
426
450
|
for (var i = 0; i < card.paths.length; i++) {
|
|
427
451
|
var item = el("li", "");
|
|
428
|
-
|
|
452
|
+
var hint = chip(card.paths[i]);
|
|
453
|
+
hint.className += " " + N.dialogPath;
|
|
454
|
+
hint.title = card.paths[i];
|
|
455
|
+
item.appendChild(hint);
|
|
429
456
|
pathList.appendChild(item);
|
|
430
457
|
}
|
|
431
458
|
|
|
432
|
-
var waits = document.getElementById("card-dialog-depends");
|
|
433
|
-
if (waits) {
|
|
434
|
-
var linked = (card.dependsOn || []).map(function (id) { return "#" + id; }).join(", ");
|
|
435
|
-
waits.hidden = linked === "";
|
|
436
|
-
waits.textContent = linked === "" ? "" : "waits on " + linked;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
var plan = document.getElementById("card-dialog-plan");
|
|
440
|
-
plan.hidden = !card.plan;
|
|
441
|
-
plan.textContent = card.plan ? "plan " + card.plan : "";
|
|
442
|
-
|
|
443
459
|
var notes = document.getElementById("card-dialog-notes");
|
|
444
460
|
notes.textContent = "";
|
|
445
461
|
for (var j = 0; j < card.notes.length; j++) {
|
|
446
462
|
var note = card.notes[j];
|
|
447
|
-
//
|
|
448
|
-
//
|
|
449
|
-
var row = el("li",
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
row.appendChild(
|
|
463
|
+
// The prose is the record and reads at body size; who wrote it and when is its
|
|
464
|
+
// footnote, so it follows the sentence instead of standing over it.
|
|
465
|
+
var row = el("li", N.dialogNote);
|
|
466
|
+
row.appendChild(el("p", N.dialogNoteText, note.text));
|
|
467
|
+
var foot = el("p", N.dialogNoteFoot);
|
|
468
|
+
foot.appendChild(el("span", N.dialogNoteBy, note.by));
|
|
469
|
+
foot.appendChild(el("span", N.dialogNoteAt, ago(note.at)));
|
|
470
|
+
row.appendChild(foot);
|
|
455
471
|
notes.appendChild(row);
|
|
456
472
|
}
|
|
473
|
+
var count = document.getElementById("card-dialog-note-count");
|
|
474
|
+
if (count) count.textContent = card.notes.length === 0 ? " · none" : " · " + card.notes.length;
|
|
457
475
|
var earlier = document.getElementById("card-dialog-earlier");
|
|
458
476
|
earlier.hidden = card.notesDropped === 0;
|
|
459
477
|
earlier.textContent = card.notesDropped === 0 ? "" : card.notesDropped + " earlier notes dropped";
|
|
460
478
|
|
|
461
|
-
// A
|
|
462
|
-
//
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
479
|
+
// A modal whose title changes is not announced, so walking with j/k says where it
|
|
480
|
+
// landed: the position is counted over the cards as they are drawn, not as stored, so
|
|
481
|
+
// a filter or a folded lane cannot make the number disagree with the screen.
|
|
482
|
+
var where = document.getElementById("card-dialog-where");
|
|
483
|
+
if (where) {
|
|
484
|
+
var drawn = drawnCards();
|
|
485
|
+
var at = 0;
|
|
486
|
+
for (var k = 0; k < drawn.length; k++) {
|
|
487
|
+
if (drawn[k].getAttribute("data-card") === String(card.id)) at = k + 1;
|
|
488
|
+
}
|
|
489
|
+
// Opened from the Team tab there is no drawn board to count along, so the line
|
|
490
|
+
// names the card instead of claiming a position it cannot know.
|
|
491
|
+
where.textContent = drawn.length === 0 ? card.title : "card " + at + " of " + drawn.length + ": " + card.title;
|
|
492
|
+
}
|
|
466
493
|
}
|
|
467
494
|
|
|
468
495
|
/** Paint a whole snapshot. */
|
|
@@ -479,17 +506,13 @@ export const BOARD_RENDER_JS = String.raw`(function () {
|
|
|
479
506
|
};
|
|
480
507
|
|
|
481
508
|
board.el = el;
|
|
509
|
+
board.typing = typing;
|
|
482
510
|
board.chip = chip;
|
|
483
511
|
board.actionButton = actionButton;
|
|
484
512
|
board.ago = ago;
|
|
485
513
|
board.findCard = findCard;
|
|
486
|
-
board.
|
|
514
|
+
board.drawnCards = drawnCards;
|
|
487
515
|
board.matches = matches;
|
|
488
|
-
board.canWrite = canWrite;
|
|
489
|
-
board.priorityOf = priorityOf;
|
|
490
|
-
board.blockersOf = blockersOf;
|
|
491
|
-
board.waitingText = waitingText;
|
|
492
|
-
board.may = may;
|
|
493
516
|
board.fillDialog = fillDialog;
|
|
494
517
|
board.refreshAges = refreshAges;
|
|
495
518
|
board.plural = plural;
|