@gavin-hu/my-pi-agent 0.20.0 → 0.22.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 +46 -30
- package/README.zh-CN.md +40 -25
- package/assets/screens/board.png +0 -0
- package/assets/screens/chrome.png +0 -0
- package/assets/screens/file-browse.png +0 -0
- package/assets/screens/plan-review.png +0 -0
- package/assets/screens/plan.png +0 -0
- package/docs/extensions/README.md +22 -23
- package/docs/extensions/board.md +397 -168
- package/docs/extensions/bytefly.md +234 -197
- package/docs/extensions/check.md +15 -17
- package/docs/extensions/context.md +35 -15
- package/docs/extensions/file-browse.md +57 -63
- package/docs/extensions/file-open.md +9 -11
- package/docs/extensions/file-read.md +12 -14
- package/docs/extensions/goal.md +15 -17
- package/docs/extensions/job.md +54 -42
- package/docs/extensions/memory.md +17 -19
- package/docs/extensions/plan.md +197 -140
- package/docs/extensions/question.md +16 -18
- package/docs/extensions/rewind.md +17 -19
- package/docs/extensions/serve.md +145 -0
- package/docs/extensions/session-bus.md +38 -16
- package/docs/extensions/subagent.md +23 -25
- package/docs/extensions/team.md +83 -44
- package/docs/extensions/todo.md +18 -18
- package/docs/extensions/watch.md +31 -31
- package/docs/extensions/web-access.md +2 -4
- package/docs/extensions/wechat.md +34 -35
- package/docs/extensions/worktree.md +56 -50
- package/extensions/board/commands.ts +79 -25
- package/extensions/board/format.ts +43 -24
- package/extensions/board/index.ts +17 -4
- package/extensions/board/input.ts +4 -9
- package/extensions/board/join.ts +5 -20
- package/extensions/board/lazy-page.ts +40 -0
- package/extensions/board/page.ts +65 -93
- package/extensions/board/result.ts +30 -39
- package/extensions/board/rows.ts +62 -25
- package/extensions/board/schema.ts +49 -11
- package/extensions/board/snapshot.ts +8 -6
- package/extensions/board/tone.ts +22 -0
- package/extensions/board/tools.ts +154 -55
- package/extensions/board/transport/router.ts +3 -5
- package/extensions/board/transport/server.ts +59 -28
- package/extensions/board/types.ts +36 -42
- package/extensions/board/view/card.ts +229 -0
- package/extensions/board/view/client.ts +35 -16
- package/extensions/board/view/dialog.ts +169 -6
- package/extensions/board/view/empty.ts +47 -0
- package/extensions/board/view/keys.ts +13 -2
- package/extensions/board/view/live.ts +90 -28
- package/extensions/board/view/names.ts +13 -4
- package/extensions/board/view/page.ts +38 -13
- package/extensions/board/view/render.ts +219 -277
- package/extensions/board/view/styles.ts +130 -32
- package/extensions/board/view/team.ts +297 -76
- package/extensions/bytefly/block/fallback.ts +1 -1
- package/extensions/bytefly/doctor/runtime.ts +5 -2
- package/extensions/bytefly/footer/collect/label.ts +60 -0
- package/extensions/bytefly/footer/collect/snapshot.ts +8 -1
- package/extensions/bytefly/footer/config.ts +7 -8
- package/extensions/bytefly/footer/register.ts +33 -4
- package/extensions/bytefly/footer/render/footer.ts +18 -4
- package/extensions/bytefly/footer/render/key.ts +46 -0
- package/extensions/bytefly/footer/render/layout.ts +13 -3
- package/extensions/bytefly/footer/render/lines.ts +10 -7
- package/extensions/bytefly/footer/render/segments.ts +41 -38
- package/extensions/bytefly/footer/render/status.ts +0 -11
- package/extensions/bytefly/footer/types.ts +7 -0
- package/extensions/bytefly/header/component.ts +4 -20
- package/extensions/bytefly/header/config.ts +1 -1
- package/extensions/bytefly/index.ts +32 -6
- package/extensions/bytefly/mode/editor.ts +115 -0
- package/extensions/bytefly/panel/commands.ts +210 -120
- package/extensions/bytefly/panel/format.ts +4 -4
- package/extensions/bytefly/panel/settings.ts +73 -6
- package/extensions/bytefly/panel/tui/keys.ts +2 -6
- package/extensions/bytefly/update/cache.ts +1 -1
- package/extensions/bytefly/update/register.ts +4 -4
- package/extensions/bytefly/working/config.ts +5 -7
- package/extensions/bytefly/working/format.ts +10 -13
- package/extensions/bytefly/working/register.ts +22 -27
- package/extensions/check/commands.ts +3 -2
- package/extensions/check/render.ts +10 -1
- package/extensions/check/{tui.ts → view/check-screens.ts} +14 -19
- package/extensions/context/commands.ts +2 -2
- package/extensions/context/{tui.ts → view/context-report.ts} +51 -19
- package/extensions/file-browse/config/config.ts +4 -19
- package/extensions/file-browse/index.ts +89 -79
- package/extensions/file-browse/live/live.ts +1 -1
- package/extensions/file-browse/transport/router.ts +12 -4
- package/extensions/file-browse/transport/server.ts +61 -28
- package/extensions/file-browse/view/chrome.ts +4 -4
- package/extensions/file-browse/view/client.ts +3 -1
- package/extensions/file-browse/view/html.ts +73 -40
- package/extensions/file-browse/view/live-client.ts +2 -2
- package/extensions/file-browse/view/markdown.ts +13 -14
- package/extensions/file-browse/view/palette.ts +2 -2
- package/extensions/file-browse/view/raw-html.ts +9 -9
- package/extensions/file-browse/view/tree.ts +1 -1
- package/extensions/file-browse/view/urls.ts +20 -6
- package/extensions/file-read/render.ts +4 -4
- package/extensions/goal/plan-route.ts +2 -2
- package/extensions/goal/tui.ts +12 -2
- package/extensions/job/child.ts +527 -0
- package/extensions/job/clock.ts +142 -0
- package/extensions/job/commands.ts +4 -2
- package/extensions/job/config.ts +8 -1
- package/extensions/job/format.ts +22 -1
- package/extensions/job/log-writer.ts +13 -2
- package/extensions/job/registry.ts +63 -7
- package/extensions/job/render.ts +34 -0
- package/extensions/job/runtime.ts +296 -655
- package/extensions/job/session.ts +132 -19
- package/extensions/job/store.ts +7 -7
- package/extensions/job/tools.ts +21 -9
- package/extensions/job/types.ts +47 -0
- package/extensions/job/{tui.ts → view/jobs-screen.ts} +19 -99
- package/extensions/job/waiters.ts +44 -1
- package/extensions/memory/commands.ts +4 -2
- package/extensions/memory/{tui.ts → view/memory-picker.ts} +12 -12
- package/extensions/plan/commands.ts +12 -9
- package/extensions/plan/feedback.ts +62 -16
- package/extensions/plan/format.ts +8 -2
- package/extensions/plan/git-bash.ts +115 -16
- package/extensions/plan/index.ts +58 -51
- package/extensions/plan/plan-index.ts +4 -2
- package/extensions/plan/plan-text.ts +114 -0
- package/extensions/plan/plans.ts +5 -54
- package/extensions/plan/policy.ts +8 -10
- package/extensions/plan/review-lazy.ts +85 -0
- package/extensions/plan/review.ts +2 -2
- package/extensions/plan/runtime.ts +61 -41
- package/extensions/plan/sections.ts +1 -1
- package/extensions/plan/steps.ts +3 -3
- package/extensions/plan/todos.ts +1 -1
- package/extensions/plan/tools/cancel.ts +27 -62
- package/extensions/plan/tools/enter.ts +55 -81
- package/extensions/plan/tools/exit.ts +221 -238
- package/extensions/plan/tools/refusal.ts +18 -0
- package/extensions/plan/tools/steps.ts +75 -92
- package/extensions/plan/tools/write.ts +77 -120
- package/extensions/plan/tools.ts +188 -30
- package/extensions/plan/tui.ts +5 -5
- package/extensions/plan/types.ts +48 -5
- package/extensions/plan/web-blocks.ts +76 -5
- package/extensions/plan/web-chrome.ts +4 -0
- package/extensions/plan/web-client.ts +102 -2
- package/extensions/plan/web-collab.ts +4 -4
- package/extensions/plan/web-comments.ts +39 -8
- package/extensions/plan/web-composer.ts +14 -3
- package/extensions/plan/web-keys.ts +149 -25
- package/extensions/plan/web-lazy.ts +53 -0
- package/extensions/plan/web-markdown.ts +10 -68
- package/extensions/plan/web-outline.ts +22 -2
- package/extensions/plan/web-page.ts +56 -86
- package/extensions/plan/web-payload.ts +188 -0
- package/extensions/plan/web-plan.ts +23 -13
- package/extensions/plan/web-router.ts +18 -9
- package/extensions/plan/web-settle.ts +14 -7
- package/extensions/plan/web-share-session.ts +24 -55
- package/extensions/plan/web-share.ts +19 -37
- package/extensions/plan/web-write.ts +112 -0
- package/extensions/plan/web.ts +67 -131
- package/extensions/question/tools.ts +6 -2
- package/extensions/question/{tui.ts → view/question-screen.ts} +11 -11
- package/extensions/rewind/commands/commands.ts +6 -3
- package/extensions/rewind/state/runtime.ts +1 -1
- package/extensions/rewind/{view/status.ts → status.ts} +4 -4
- package/extensions/rewind/view/review-tui.ts +4 -4
- package/extensions/serve/config.ts +39 -0
- package/extensions/serve/index.ts +85 -0
- package/extensions/serve/registry.ts +260 -0
- package/extensions/session-bus/commands.ts +11 -6
- package/extensions/session-bus/join-request.ts +17 -9
- package/extensions/session-bus/mail-watch.ts +26 -17
- package/extensions/session-bus/protocol.ts +15 -3
- package/extensions/session-bus/resolve.ts +5 -7
- package/extensions/session-bus/result.ts +1 -27
- package/extensions/session-bus/runtime.ts +40 -12
- package/extensions/session-bus/store.ts +101 -19
- package/extensions/session-bus/tools.ts +5 -4
- package/extensions/session-bus/types.ts +19 -0
- package/extensions/session-bus/{tui.ts → view/session-list.ts} +16 -16
- package/extensions/subagent/agents.ts +1 -1
- package/extensions/subagent/commands.ts +4 -2
- package/extensions/subagent/format.ts +15 -15
- package/extensions/subagent/index.ts +1 -1
- package/extensions/subagent/render.ts +3 -3
- package/extensions/subagent/{background-tui.ts → view/agents-board.ts} +11 -11
- package/extensions/team/commands.ts +24 -5
- package/extensions/team/dispatch.ts +18 -4
- package/extensions/team/format.ts +17 -2
- package/extensions/team/index.ts +9 -8
- package/extensions/team/schema.ts +6 -5
- package/extensions/team/tools.ts +107 -45
- package/extensions/team/types.ts +3 -2
- package/extensions/todo/commands.ts +4 -2
- package/extensions/todo/format.ts +11 -0
- package/extensions/todo/render.ts +57 -0
- package/extensions/todo/runtime.ts +1 -1
- package/extensions/todo/tools.ts +1 -1
- package/extensions/todo/{tui.ts → view/todo-screen.ts} +18 -90
- package/extensions/todo/widget.ts +47 -0
- package/extensions/watch/commands.ts +4 -2
- package/extensions/watch/format.ts +36 -7
- package/extensions/watch/render.ts +57 -0
- package/extensions/watch/runtime.ts +27 -22
- package/extensions/watch/tools.ts +4 -4
- package/extensions/watch/{tui.ts → view/watch-screen.ts} +18 -80
- package/extensions/web-access/fetch/tool.ts +12 -4
- package/extensions/wechat/commands/commands.ts +18 -14
- package/extensions/wechat/index.ts +72 -37
- package/extensions/wechat/state/credentials.ts +1 -1
- package/extensions/wechat/state/state.ts +1 -1
- package/extensions/wechat/text/status.ts +112 -0
- package/extensions/wechat/types.ts +2 -2
- package/extensions/wechat/view/status-screen.ts +4 -103
- package/extensions/worktree/commands.ts +11 -9
- package/extensions/worktree/enter.ts +2 -2
- package/extensions/worktree/exit.ts +2 -2
- package/extensions/worktree/index.ts +5 -5
- package/extensions/worktree/screen.ts +3 -3
- package/extensions/worktree/status.ts +2 -2
- package/extensions/worktree/tools.ts +113 -116
- package/lib/README.md +28 -13
- package/lib/board/actions.ts +111 -0
- package/lib/board/artifact.ts +149 -0
- package/lib/board/card.ts +163 -52
- package/lib/board/config.ts +58 -0
- package/lib/board/contract.ts +113 -0
- package/lib/board/join.ts +38 -0
- package/lib/board/lanes.ts +182 -0
- package/lib/board/lead.ts +130 -0
- package/lib/board/limits.ts +78 -0
- package/lib/board/links.ts +31 -10
- package/lib/board/page-pointer.ts +30 -5
- package/lib/board/read.ts +83 -0
- package/lib/board/reason.ts +102 -0
- package/lib/board/round-text.ts +68 -6
- package/lib/board/round.ts +31 -15
- package/lib/board/rules.ts +151 -307
- package/lib/board/seat.ts +35 -0
- package/lib/board/session.ts +199 -51
- package/lib/board/skipped.ts +22 -0
- package/lib/board/steps.ts +487 -0
- package/lib/board/store.ts +119 -36
- package/lib/board/tone.ts +92 -0
- package/lib/board/view.ts +81 -63
- package/lib/bus-request.ts +83 -0
- package/{extensions/watch → lib}/child.ts +40 -21
- package/lib/env.ts +65 -7
- package/lib/format.ts +13 -0
- package/lib/http/index.ts +17 -11
- package/lib/http/mount.ts +142 -0
- package/lib/http/port.ts +30 -0
- package/lib/http/serve.ts +170 -46
- package/lib/http/server.ts +3 -3
- package/lib/lazy.ts +45 -0
- package/lib/plan-entry.ts +46 -21
- package/lib/plans/paths.ts +82 -0
- package/lib/policy.ts +9 -6
- package/lib/report.ts +3 -2
- package/lib/requirements.ts +10 -10
- package/lib/session-label.ts +129 -7
- package/lib/session-roster.ts +43 -1
- package/lib/session-tool.ts +39 -22
- package/lib/stray-fields.ts +7 -2
- package/lib/tool-names.ts +5 -10
- package/lib/tui/README.md +29 -11
- package/lib/tui/chrome.ts +16 -15
- package/lib/tui/cursor.ts +18 -3
- package/lib/tui/glyphs.ts +11 -11
- package/lib/tui/render-cache.ts +29 -0
- package/lib/tui/tokens.ts +33 -2
- package/lib/tui/tool.ts +33 -4
- package/lib/web/README.md +21 -0
- package/lib/web/client-runtime.ts +27 -1
- package/lib/web/components.ts +5 -1
- package/lib/web/index.ts +1 -0
- package/lib/web/markdown.ts +83 -0
- package/lib/web/markup.ts +20 -2
- package/lib/web/runtime.ts +9 -10
- package/package.json +8 -2
- package/skills/init/SKILL.md +5 -5
- package/skills/next/SKILL.md +3 -5
- package/extensions/board/config.ts +0 -36
- package/extensions/board/seat.ts +0 -28
- package/lib/tui/index.ts +0 -34
- package/skills/project-docs/SKILL.md +0 -133
- package/skills/project-docs/references/audit.md +0 -131
- package/skills/project-docs/references/changelog.md +0 -105
- package/skills/project-docs/references/discovery.md +0 -127
- package/skills/project-docs/references/readme.md +0 -80
- package/skills/project-docs/references/release.md +0 -84
- package/skills/project-docs/references/roadmap.md +0 -93
- package/skills/project-docs/scripts/state.ts +0 -316
package/lib/board/store.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The board store: one directory of card files per repository, and the only code
|
|
3
3
|
* that writes one.
|
|
4
4
|
*
|
|
5
|
-
* A board is a directory — `<
|
|
5
|
+
* A board is a directory — `<agent-dir>/board/<repo-key>/cards/0004.json` — so a
|
|
6
6
|
* reader needs no index, no daemon, and no dependency; the listing *is* the
|
|
7
7
|
* index. The key comes from `lib/repo-key.ts`, so every worktree of one repository
|
|
8
8
|
* shares one board, and a directory outside git keys on itself.
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
29
|
import { link, mkdir, readFile, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
30
|
-
import { join } from "node:path";
|
|
30
|
+
import { basename, join } from "node:path";
|
|
31
31
|
import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
32
32
|
import { resolveEffectiveCwd } from "../env.ts";
|
|
33
33
|
import { hasErrorCode, messageOf } from "../errors.ts";
|
|
@@ -35,15 +35,17 @@ import { shortenPath } from "../format.ts";
|
|
|
35
35
|
import { repoKeyFor } from "../repo-key.ts";
|
|
36
36
|
import {
|
|
37
37
|
type Card,
|
|
38
|
-
CARD_STATUSES,
|
|
39
|
-
CARD_VERSION,
|
|
40
38
|
cardRef,
|
|
41
39
|
type EndedCard,
|
|
42
|
-
|
|
40
|
+
ENDED_VERSION,
|
|
43
41
|
type Priority,
|
|
44
42
|
priorityRank,
|
|
43
|
+
SUPPORTED_VERSIONS,
|
|
45
44
|
} from "./card.ts";
|
|
45
|
+
import { LANES } from "./lanes.ts";
|
|
46
|
+
import { readCard } from "./read.ts";
|
|
46
47
|
import { indexOf } from "./links.ts";
|
|
48
|
+
import { type BoardConfig, DEFAULT_BOARD_CONFIG, readBoardConfig } from "./config.ts";
|
|
47
49
|
import { type Actor, applyOp, type CardOp, createCard } from "./rules.ts";
|
|
48
50
|
|
|
49
51
|
/** The directory under the agent directory that holds every board. */
|
|
@@ -108,10 +110,35 @@ export interface Skip {
|
|
|
108
110
|
export interface BoardStore {
|
|
109
111
|
/** The board's own directory, for `where` and for a refusal that names it. */
|
|
110
112
|
readonly dir: string;
|
|
113
|
+
/**
|
|
114
|
+
* The repository key this board is filed under.
|
|
115
|
+
*
|
|
116
|
+
* Carried rather than read back off {@link dir}'s last segment: the same key is what
|
|
117
|
+
* `session-bus` files this repository's records under, so a caller that needs it must
|
|
118
|
+
* not learn it from the board's layout — otherwise regrouping the board's own files
|
|
119
|
+
* moves the roster to a directory no bus writes to, and the page reads "no record" for
|
|
120
|
+
* every peer instead of failing where someone would notice.
|
|
121
|
+
*/
|
|
122
|
+
readonly key: string;
|
|
123
|
+
/**
|
|
124
|
+
* The settings this board works by: its WIP limits and the name its two signatures carry.
|
|
125
|
+
*
|
|
126
|
+
* Read once when the store opened, and handed to every operation: the page shows the same
|
|
127
|
+
* numbers a refusal would, so a head cannot claim room a claim would refuse.
|
|
128
|
+
*/
|
|
129
|
+
config(): BoardConfig;
|
|
111
130
|
/** Create the directories and sweep a temp file a dead process left. */
|
|
112
131
|
ensure(): Promise<void>;
|
|
113
|
-
/** Every card on the board, in lane order:
|
|
132
|
+
/** Every card on the board, in lane order: backlog, plan, execute, blocked, done. */
|
|
114
133
|
list(): Promise<Card[]>;
|
|
134
|
+
/**
|
|
135
|
+
* The board in one read: the cards, and the files it refused.
|
|
136
|
+
*
|
|
137
|
+
* A page's snapshot needs both, and asking twice reads and parses every card twice to
|
|
138
|
+
* answer one frame; `list` and `skipped` are the two halves of this for a caller that
|
|
139
|
+
* wants only one.
|
|
140
|
+
*/
|
|
141
|
+
scan(): Promise<{ cards: Card[]; skipped: Skip[] }>;
|
|
115
142
|
/**
|
|
116
143
|
* The cards set aside in the archive, in the same order, and the files there this build
|
|
117
144
|
* refused. Read only when a caller asks: no other read opens the pile.
|
|
@@ -134,6 +161,16 @@ export interface BoardStore {
|
|
|
134
161
|
skipped(): Promise<Skip[]>;
|
|
135
162
|
/** A short, user-facing name for the directory. */
|
|
136
163
|
where(): string;
|
|
164
|
+
/**
|
|
165
|
+
* The repository root this board belongs to, and the directory itself outside one.
|
|
166
|
+
*
|
|
167
|
+
* A board is keyed by a hash, so this is the one part of its identity a reader can
|
|
168
|
+
* say out loud: the page names the repository from it, and offers the whole path on
|
|
169
|
+
* hover, which is what tells two clones of one repository apart.
|
|
170
|
+
*/
|
|
171
|
+
root(): string;
|
|
172
|
+
/** The root's last segment — `my-pi-agent` — which is all a tab has room for. */
|
|
173
|
+
name(): string;
|
|
137
174
|
}
|
|
138
175
|
|
|
139
176
|
/** Where one repository's board lives. */
|
|
@@ -152,11 +189,41 @@ export async function openBoard(
|
|
|
152
189
|
// environment carries the active root, so the two are reconciled before the
|
|
153
190
|
// repository lookup — the same path `plan` and `session-bus` take.
|
|
154
191
|
const repo = await repoKeyFor(pi, resolveEffectiveCwd(cwd));
|
|
155
|
-
|
|
192
|
+
// Read once per store: the settings are a setting, not a card, and a board that re-read
|
|
193
|
+
// a file on every write would spend a stat to answer a question nobody changed.
|
|
194
|
+
const config = readBoardConfig(resolveEffectiveCwd(cwd), { agentDir });
|
|
195
|
+
// The key is handed on, not left to be read off the directory it named: `session-bus`
|
|
196
|
+
// files this repository's records under the same key, so the two must not be able to
|
|
197
|
+
// disagree about it.
|
|
198
|
+
return createStore(boardDirFor(agentDir, repo.key), { root: repo.root, key: repo.key, config });
|
|
156
199
|
}
|
|
157
200
|
|
|
158
201
|
/** Build a store over one directory. Exported for tests and for a second backend. */
|
|
159
|
-
export function createStore(
|
|
202
|
+
export function createStore(
|
|
203
|
+
dir: string,
|
|
204
|
+
options: {
|
|
205
|
+
root?: string;
|
|
206
|
+
key?: string;
|
|
207
|
+
config?: BoardConfig;
|
|
208
|
+
/**
|
|
209
|
+
* A suite's seam: called once an operation is ready to write and before the re-read that
|
|
210
|
+
* decides whether it still may.
|
|
211
|
+
*
|
|
212
|
+
* A race between a write and a pile move cannot be made deterministic from outside — the
|
|
213
|
+
* window is inside one `mutate` — so a test stands here and lets the other write land. The
|
|
214
|
+
* product supplies nothing.
|
|
215
|
+
*/
|
|
216
|
+
beforeWrite?: (id: string, kind: CardOp["kind"]) => Promise<void> | void;
|
|
217
|
+
} = {},
|
|
218
|
+
): BoardStore {
|
|
219
|
+
// A store built over a directory by hand has no repository to ask, so it is keyed by
|
|
220
|
+
// that directory's own name: the one place this fallback lives, and the one reason it
|
|
221
|
+
// is not a proof about `openBoard`'s boards.
|
|
222
|
+
const key = options.key ?? basename(dir);
|
|
223
|
+
// A store built over a directory by hand has no repository to name, so it names its
|
|
224
|
+
// own directory: the page then says something true instead of something blank.
|
|
225
|
+
const root = options.root ?? dir;
|
|
226
|
+
const config = options.config ?? DEFAULT_BOARD_CONFIG;
|
|
160
227
|
const cardsDir = join(dir, CARDS_DIR_NAME);
|
|
161
228
|
const endedDir = join(dir, ENDED_DIR_NAME);
|
|
162
229
|
const archiveDir = join(dir, ARCHIVE_DIR_NAME);
|
|
@@ -170,7 +237,7 @@ export function createStore(dir: string): BoardStore {
|
|
|
170
237
|
};
|
|
171
238
|
|
|
172
239
|
/** Every card file of one pile, read once: what it shows and what it refused. */
|
|
173
|
-
const
|
|
240
|
+
const scanPile = async (pileDir: string): Promise<{ cards: Card[]; skipped: Skip[] }> => {
|
|
174
241
|
const entries = await readdirSafe(pileDir);
|
|
175
242
|
const cards: Card[] = [];
|
|
176
243
|
const skipped: Skip[] = [];
|
|
@@ -184,16 +251,18 @@ export function createStore(dir: string): BoardStore {
|
|
|
184
251
|
return { cards: sortForLane(cards), skipped: skipped.sort((a, b) => a.file.localeCompare(b.file)) };
|
|
185
252
|
};
|
|
186
253
|
|
|
187
|
-
const
|
|
254
|
+
const scan = (): Promise<{ cards: Card[]; skipped: Skip[] }> => scanPile(cardsDir);
|
|
255
|
+
|
|
256
|
+
const list = async (): Promise<Card[]> => (await scan()).cards;
|
|
188
257
|
|
|
189
|
-
const archived = async (): Promise<{ cards: Card[]; skipped: Skip[] }> =>
|
|
258
|
+
const archived = async (): Promise<{ cards: Card[]; skipped: Skip[] }> => scanPile(archiveDir);
|
|
190
259
|
|
|
191
260
|
// A directory listing and a name pattern, with no file opened: a plain `list` prints this
|
|
192
261
|
// count so the archive it left out cannot read as history that was deleted.
|
|
193
262
|
const archivedCount = async (): Promise<number> =>
|
|
194
263
|
(await readdirSafe(archiveDir)).filter((entry) => CARD_FILE.test(entry)).length;
|
|
195
264
|
|
|
196
|
-
const skipped = async (): Promise<Skip[]> => (await scan(
|
|
265
|
+
const skipped = async (): Promise<Skip[]> => (await scan()).skipped;
|
|
197
266
|
|
|
198
267
|
/**
|
|
199
268
|
* One card and the pile its file is in, the board first.
|
|
@@ -242,7 +311,7 @@ export function createStore(dir: string): BoardStore {
|
|
|
242
311
|
if (changedUnderneath) return { ok: false, error: `${cardRef(id)} was dropped.` };
|
|
243
312
|
// A file that is there and refused is not a card that does not exist: the
|
|
244
313
|
// reader is told which file to look at instead of being sent to add one.
|
|
245
|
-
const skip = (await scan(
|
|
314
|
+
const skip = (await scan()).skipped.find((entry) => entry.id === id);
|
|
246
315
|
return {
|
|
247
316
|
ok: false,
|
|
248
317
|
error:
|
|
@@ -255,11 +324,22 @@ export function createStore(dir: string): BoardStore {
|
|
|
255
324
|
// The preconditions are checked against the card as it is *now*, and against the
|
|
256
325
|
// board as it is now, so an operation re-applied to a card that changed is refused
|
|
257
326
|
// rather than forced.
|
|
258
|
-
const result = applyOp(card, op, actor, now, indexOf(await list()));
|
|
327
|
+
const result = applyOp(card, op, actor, now, indexOf(await list()), config);
|
|
259
328
|
if (!result.ok) return { ok: false, error: result.error, card };
|
|
260
329
|
if (!result.changed) {
|
|
261
330
|
return { ok: true, card: result.card, changed: false, dropped: false, archived: false };
|
|
262
331
|
}
|
|
332
|
+
// Last look before the write, asked for every path that writes: the card must still be
|
|
333
|
+
// where it was read and at the revision it had. A writer that landed in between is the
|
|
334
|
+
// re-read the next pass applies against — and the pile is part of the answer, because
|
|
335
|
+
// `archive` moves the file without moving its revision, so a write carrying a stale
|
|
336
|
+
// pile would recreate the card on the board beside its copy in the archive.
|
|
337
|
+
await options.beforeWrite?.(id, op.kind);
|
|
338
|
+
const current = await locate(id);
|
|
339
|
+
if (current === undefined || current.card.rev !== card.rev || current.pile !== pile) {
|
|
340
|
+
changedUnderneath = true;
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
263
343
|
if (op.kind === "drop") {
|
|
264
344
|
// The record lands first: a crash between the two leaves a card that still
|
|
265
345
|
// exists with a note explaining it, never a card that is gone unexplained.
|
|
@@ -276,13 +356,6 @@ export function createStore(dir: string): BoardStore {
|
|
|
276
356
|
await moveCard(cardsDir, archiveDir, id);
|
|
277
357
|
return { ok: true, card: result.card, changed: true, dropped: false, archived: true };
|
|
278
358
|
}
|
|
279
|
-
// Last look before the rename: a writer that landed in between is the
|
|
280
|
-
// re-read the next pass applies against.
|
|
281
|
-
const current = await read(id);
|
|
282
|
-
if (!current || current.rev !== card.rev) {
|
|
283
|
-
changedUnderneath = true;
|
|
284
|
-
continue;
|
|
285
|
-
}
|
|
286
359
|
await writeCardFile(pile, id, result.card);
|
|
287
360
|
return { ok: true, card: result.card, changed: true, dropped: false, archived: false };
|
|
288
361
|
}
|
|
@@ -306,8 +379,11 @@ export function createStore(dir: string): BoardStore {
|
|
|
306
379
|
|
|
307
380
|
return {
|
|
308
381
|
dir,
|
|
382
|
+
key,
|
|
383
|
+
config: () => config,
|
|
309
384
|
ensure,
|
|
310
385
|
list,
|
|
386
|
+
scan,
|
|
311
387
|
archived,
|
|
312
388
|
archivedCount,
|
|
313
389
|
read,
|
|
@@ -316,6 +392,8 @@ export function createStore(dir: string): BoardStore {
|
|
|
316
392
|
bytes,
|
|
317
393
|
skipped,
|
|
318
394
|
where: () => shortenPath(dir),
|
|
395
|
+
root: () => root,
|
|
396
|
+
name: () => basename(root),
|
|
319
397
|
};
|
|
320
398
|
}
|
|
321
399
|
|
|
@@ -340,19 +418,23 @@ async function readCardFile(pileDir: string, entry: string): Promise<{ card: Car
|
|
|
340
418
|
} catch {
|
|
341
419
|
return { skip: { file: entry, id, why: "bad JSON" } };
|
|
342
420
|
}
|
|
343
|
-
// A version
|
|
344
|
-
// written by a newer build, so the board is not empty, it is not readable here.
|
|
421
|
+
// A version this build does not read is the one refusal a reader can act on: the file was
|
|
422
|
+
// written by a newer build, so the board is not empty, it is not readable here. Every
|
|
423
|
+
// version `readCard` can map is read; the number is named rather than guessed at.
|
|
345
424
|
const version = typeof parsed === "object" && parsed !== null ? (parsed as { v?: unknown }).v : undefined;
|
|
346
|
-
if (typeof version === "number" &&
|
|
425
|
+
if (typeof version === "number" && !SUPPORTED_VERSIONS.some((known) => known === version)) {
|
|
347
426
|
return { skip: { file: entry, id, why: `version ${version}` } };
|
|
348
427
|
}
|
|
349
|
-
|
|
428
|
+
// The one gate a file read passes: an older spelling of the lane field is read as
|
|
429
|
+
// this build's, and everything else is a shape this build does not trust.
|
|
430
|
+
const card = readCard(parsed);
|
|
431
|
+
if (card === undefined) return { skip: { file: entry, id, why: "not a card record" } };
|
|
350
432
|
// The file name is the id's canonical spelling: a padded id inside the record
|
|
351
433
|
// (or a record moved under another name) is a disagreement, not a card.
|
|
352
|
-
if (
|
|
353
|
-
return { skip: { file: entry, id, why: `names card ${
|
|
434
|
+
if (card.id !== String(Number(match[1]))) {
|
|
435
|
+
return { skip: { file: entry, id, why: `names card ${card.id}` } };
|
|
354
436
|
}
|
|
355
|
-
return { card
|
|
437
|
+
return { card };
|
|
356
438
|
}
|
|
357
439
|
|
|
358
440
|
/** The highest id a directory holds, or 0 when it holds none. */
|
|
@@ -386,10 +468,10 @@ async function nextCardId(cardsDir: string, endedDir: string, archiveDir: string
|
|
|
386
468
|
async function writeEnded(endedDir: string, card: Card, actor: Actor, now: number): Promise<void> {
|
|
387
469
|
await mkdir(endedDir, { recursive: true, mode: DIR_MODE });
|
|
388
470
|
const ended: EndedCard = {
|
|
389
|
-
v:
|
|
471
|
+
v: ENDED_VERSION,
|
|
390
472
|
id: card.id,
|
|
391
473
|
title: card.title,
|
|
392
|
-
|
|
474
|
+
lane: card.lane,
|
|
393
475
|
by: actor.label,
|
|
394
476
|
...(card.owner === undefined ? {} : { owner: card.owner }),
|
|
395
477
|
at: now,
|
|
@@ -446,7 +528,6 @@ async function writeCardFile(dir: string, id: string, card: Card): Promise<void>
|
|
|
446
528
|
}
|
|
447
529
|
}
|
|
448
530
|
|
|
449
|
-
/** One card's bytes, ending in a newline so the file reads well in an editor. */
|
|
450
531
|
/** One record as the bytes a file holds: tab-indented JSON with a closing newline. */
|
|
451
532
|
function serialize(record: Card | EndedCard): string {
|
|
452
533
|
return `${JSON.stringify(record, null, "\t")}\n`;
|
|
@@ -477,21 +558,23 @@ async function readdirSafe(dir: string): Promise<string[]> {
|
|
|
477
558
|
}
|
|
478
559
|
|
|
479
560
|
/**
|
|
480
|
-
* Lane order, then the lead's order inside it, then
|
|
481
|
-
*
|
|
561
|
+
* Lane order, then the lead's order inside it, then the order the cards were added in: the
|
|
562
|
+
* id, which is allocated once and never reissued.
|
|
482
563
|
*
|
|
483
564
|
* Priority sorts inside a lane rather than beside it: a `high` card in Done is still
|
|
484
565
|
* history, and a lane that mixed the two would have to say so in its head.
|
|
566
|
+
*
|
|
567
|
+
* The id carries the age, and `updatedAt` deliberately does not: every write moves that
|
|
568
|
+
* stamp, so ordering by it makes a note, a claim, or a contract push a card to the end of
|
|
569
|
+
* its priority and reshuffle the listing a reader was working down.
|
|
485
570
|
*/
|
|
486
571
|
function sortForLane(cards: Card[]): Card[] {
|
|
487
|
-
const lane = (card: Card): number =>
|
|
572
|
+
const lane = (card: Card): number => LANES.indexOf(card.lane);
|
|
488
573
|
return cards.sort((a, b) => {
|
|
489
574
|
const byLane = lane(a) - lane(b);
|
|
490
575
|
if (byLane !== 0) return byLane;
|
|
491
576
|
const byPriority = priorityRank(a) - priorityRank(b);
|
|
492
577
|
if (byPriority !== 0) return byPriority;
|
|
493
|
-
const byAge = a.updatedAt - b.updatedAt;
|
|
494
|
-
if (byAge !== 0) return byAge;
|
|
495
578
|
return Number(a.id) - Number(b.id);
|
|
496
579
|
});
|
|
497
580
|
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What tone a lane, a reason, and a Team decision row wear.
|
|
3
|
+
*
|
|
4
|
+
* The page and the terminal each decide a colour, and before this table they decided it in
|
|
5
|
+
* seven places: a lane role in the lane table, a reason class in the browser bundle, two
|
|
6
|
+
* hand-written `[data-kind]` edge rules, a second `blocked` rule for the queue's why line,
|
|
7
|
+
* and a transcript that painted every lane the same accent. A tone stated once is the only
|
|
8
|
+
* version of that which cannot drift.
|
|
9
|
+
*
|
|
10
|
+
* A tone is a word the page's kit already names as a property (`var(--warn)`), so the page
|
|
11
|
+
* wears one directly and needs no map. The terminal speaks its own vocabulary — `ok`, `warn`,
|
|
12
|
+
* `bad`, `meta`, `faint` — and that bridge lives in `extensions/board/tone.ts`, because only
|
|
13
|
+
* the transcript speaks it.
|
|
14
|
+
*
|
|
15
|
+
* The rule behind the words, so a future kind is not a guess: **warn is a person who must act
|
|
16
|
+
* next** (approve, review, decide), **danger is a refusal or a loss** (blocked, gone), and
|
|
17
|
+
* anything merely true of the card — waiting, offered, executing, done, noted — is muted.
|
|
18
|
+
*
|
|
19
|
+
* Value-only: no I/O, no clock, no card.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { LaneRole } from "./lanes.ts";
|
|
23
|
+
import type { ReasonKind } from "./reason.ts";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A colour a board surface may wear.
|
|
27
|
+
*
|
|
28
|
+
* `dim` joins the lane roles because a Team queue row nobody has to act on yet is quieter
|
|
29
|
+
* than a muted one: drawn, not emphasised.
|
|
30
|
+
*/
|
|
31
|
+
export type BoardTone = LaneRole | "dim";
|
|
32
|
+
|
|
33
|
+
/** Every tone, in the order a rule is written for one. */
|
|
34
|
+
export const BOARD_TONES = ["muted", "dim", "warn", "accent", "danger", "success"] as const;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The tone a card's reason line wears, by the kind `reason.ts` reports.
|
|
38
|
+
*
|
|
39
|
+
* `waiting` is here even though `reasonFor` never returns it: the page's card face invents it
|
|
40
|
+
* for a card that names what it waits on (`view/card.ts`), so the table has to answer for it
|
|
41
|
+
* or that line would draw the default.
|
|
42
|
+
*/
|
|
43
|
+
export const REASON_TONE: Record<ReasonKind, BoardTone> = {
|
|
44
|
+
waiting: "muted",
|
|
45
|
+
blocked: "danger",
|
|
46
|
+
offered: "muted",
|
|
47
|
+
contract: "warn",
|
|
48
|
+
approval: "warn",
|
|
49
|
+
executing: "muted",
|
|
50
|
+
review: "warn",
|
|
51
|
+
done: "muted",
|
|
52
|
+
note: "muted",
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The Team queue's decision kinds, in the order a lead reads them.
|
|
57
|
+
*
|
|
58
|
+
* One list for both facts a row needs: the position is the rank (a `gone` owner outranks a
|
|
59
|
+
* card merely sitting still), and {@link DECISION_TONE} is the colour. They were two lists — a
|
|
60
|
+
* `KIND_RANK` object in the bundle and a hand-written set of `[data-kind]` rules in the
|
|
61
|
+
* stylesheet — which is how `gone` came to be ranked second and drawn quietest.
|
|
62
|
+
*/
|
|
63
|
+
export const DECISION_KINDS = ["blocked", "gone", "approval", "waiting", "idle", "unplanned", "unassigned"] as const;
|
|
64
|
+
|
|
65
|
+
/** One reason a card is in the lead's queue. */
|
|
66
|
+
export type DecisionKind = (typeof DECISION_KINDS)[number];
|
|
67
|
+
|
|
68
|
+
/** The tone a decision row's edge, and a danger row's reason line, wear. */
|
|
69
|
+
export const DECISION_TONE: Record<DecisionKind, BoardTone> = {
|
|
70
|
+
blocked: "danger",
|
|
71
|
+
gone: "danger",
|
|
72
|
+
approval: "warn",
|
|
73
|
+
waiting: "dim",
|
|
74
|
+
idle: "warn",
|
|
75
|
+
unplanned: "dim",
|
|
76
|
+
unassigned: "dim",
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* {@link DECISION_KINDS} as the browser bundle's own source.
|
|
81
|
+
*
|
|
82
|
+
* The page ranks its queue by this list, and the bundle is a string, so the order travels as
|
|
83
|
+
* data the way `reasonFor` and `matchesQuery` do: one list in `lib/`, spliced in, never a
|
|
84
|
+
* second copy in the browser. `test` parses it back to the tuple.
|
|
85
|
+
*/
|
|
86
|
+
export const DECISION_RANK_JS = JSON.stringify(DECISION_KINDS);
|
|
87
|
+
|
|
88
|
+
/** {@link DECISION_TONE} as the bundle's own source, spliced in like the rank. */
|
|
89
|
+
export const DECISION_TONE_JS = JSON.stringify(DECISION_TONE);
|
|
90
|
+
|
|
91
|
+
/** {@link REASON_TONE} as the bundle's own source, spliced in like the rank. */
|
|
92
|
+
export const REASON_TONE_JS = JSON.stringify(REASON_TONE);
|
package/lib/board/view.ts
CHANGED
|
@@ -8,21 +8,17 @@
|
|
|
8
8
|
*
|
|
9
9
|
* `where` is the store's own path, shortened, so a reader can tell which directory
|
|
10
10
|
* a shared board is filed under — and so nothing here needs a "remote" variant that
|
|
11
|
-
* no backend produces.
|
|
11
|
+
* no backend produces. `repo` and `root` are the same board said aloud: the repository
|
|
12
|
+
* root's own name, which a tab can carry, and its whole path for the hover title.
|
|
12
13
|
*
|
|
13
14
|
* Pure: the caller supplies the cards, the clock, and the sizes it read.
|
|
14
15
|
*/
|
|
15
16
|
|
|
16
17
|
import { type RosterSession, type RosterState, rosterStateOf } from "../session-roster.ts";
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
laneSince,
|
|
22
|
-
priorityOf,
|
|
23
|
-
type Priority,
|
|
24
|
-
signatureOf,
|
|
25
|
-
} from "./card.ts";
|
|
18
|
+
import { type Card, laneSince, priorityOf, type Priority, signatureOf } from "./card.ts";
|
|
19
|
+
import type { Skip } from "./store.ts";
|
|
20
|
+
import { LANE_RULES, LANES, type Lane, type LaneRole } from "./lanes.ts";
|
|
21
|
+
import { DEFAULT_WIP, limitOf, type WipLimits } from "./limits.ts";
|
|
26
22
|
import { blockersOf, indexOf, lanesText, openDependentsOf } from "./links.ts";
|
|
27
23
|
import type { Actor } from "./rules.ts";
|
|
28
24
|
import { buildRound, type RoundSummary } from "./round.ts";
|
|
@@ -38,7 +34,7 @@ export interface ViewTeammate {
|
|
|
38
34
|
mail: number;
|
|
39
35
|
/** Whether it is listening for mail; absent means its record does not say so. */
|
|
40
36
|
listening?: boolean;
|
|
41
|
-
/** Whether this is the
|
|
37
|
+
/** Whether this is the seat the page is read as. */
|
|
42
38
|
you: boolean;
|
|
43
39
|
}
|
|
44
40
|
|
|
@@ -54,14 +50,16 @@ export interface ViewCard extends Card {
|
|
|
54
50
|
/** The priority it acts on: a card that never set one is ordinary. */
|
|
55
51
|
priority: Priority;
|
|
56
52
|
/**
|
|
57
|
-
* The unfinished cards it waits for, as `#2 (
|
|
53
|
+
* The unfinished cards it waits for, as `#2 (execute) Patch the gateway`; absent when
|
|
54
|
+
* nothing blocks it.
|
|
58
55
|
*
|
|
59
56
|
* `undefined` is the whole fact — a waiting card is one with a `waits` — because a
|
|
60
57
|
* boolean beside it would be one more thing that can disagree with the string.
|
|
61
58
|
*/
|
|
62
59
|
waits?: string;
|
|
63
60
|
/**
|
|
64
|
-
* The unfinished cards waiting on it, as `#9 (
|
|
61
|
+
* The unfinished cards waiting on it, as `#9 (backlog) Ship the picker`; absent when none
|
|
62
|
+
* do.
|
|
65
63
|
*
|
|
66
64
|
* The other side of `waits`: what leaving this card undone holds up. Derived here
|
|
67
65
|
* rather than in the page's script for the same reason, and filtered to the unfinished
|
|
@@ -79,29 +77,31 @@ export interface ViewCard extends Card {
|
|
|
79
77
|
since?: number;
|
|
80
78
|
}
|
|
81
79
|
|
|
82
|
-
/**
|
|
80
|
+
/**
|
|
81
|
+
* One lane, as the page draws it: the name, the words, the colour, the limit, and the cards.
|
|
82
|
+
*
|
|
83
|
+
* The page holds no lane table of its own — it renders this — so a lane's label, its empty
|
|
84
|
+
* word, and its head's `n/limit` cannot read one way here and another in the transcript.
|
|
85
|
+
*/
|
|
83
86
|
export interface ViewLane {
|
|
84
|
-
|
|
87
|
+
key: Lane;
|
|
88
|
+
/** The word a head, a row, and a sentence print. */
|
|
89
|
+
label: string;
|
|
90
|
+
/** What the tray says when it holds nothing. */
|
|
91
|
+
empty: string;
|
|
92
|
+
/** The colour role the lane's dot and its card faces take. */
|
|
93
|
+
role: LaneRole;
|
|
94
|
+
/** The WIP limit this lane is held to, for the heads that show `n/limit`; absent when it is not pulled. */
|
|
95
|
+
limit?: number;
|
|
96
|
+
count: number;
|
|
85
97
|
cards: ViewCard[];
|
|
86
98
|
}
|
|
87
99
|
|
|
88
|
-
/** The per-lane tallies the toolbar shows. */
|
|
89
|
-
export interface ViewCounts {
|
|
90
|
-
total: number;
|
|
91
|
-
todo: number;
|
|
92
|
-
doing: number;
|
|
93
|
-
blocked: number;
|
|
94
|
-
done: number;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
100
|
/** Everything the page is allowed to know. */
|
|
98
101
|
export interface BoardView {
|
|
99
|
-
/** The session a page write is made as — the page's seat, never the browser. */
|
|
100
|
-
me: { id?: string; label: string };
|
|
101
|
-
/** The session the page is served from, when the page knows it: who is reading. */
|
|
102
|
-
session?: { id?: string; label: string };
|
|
103
102
|
lanes: ViewLane[];
|
|
104
|
-
|
|
103
|
+
/** Every card on the board: the sum of the lanes, said once so a head need not add them up. */
|
|
104
|
+
total: number;
|
|
105
105
|
round: RoundSummary<ViewCard>;
|
|
106
106
|
/**
|
|
107
107
|
* The sessions on this repository's bus, or absent when there is no bus here.
|
|
@@ -115,35 +115,45 @@ export interface BoardView {
|
|
|
115
115
|
/**
|
|
116
116
|
* Card files the reader refused, so the footer can say the board is not empty.
|
|
117
117
|
*
|
|
118
|
-
*
|
|
119
|
-
* that separates "no work here" from "this build cannot read your work"
|
|
118
|
+
* An empty list is the ordinary answer and draws nothing; anything in it is the one fact
|
|
119
|
+
* that separates "no work here" from "this build cannot read your work" — and the file
|
|
120
|
+
* names and reasons are what let a reader open the file, so the page carries them whole.
|
|
120
121
|
*/
|
|
121
|
-
skipped:
|
|
122
|
+
skipped: readonly Skip[];
|
|
122
123
|
/** Changes when a card's revision, a teammate's state, or its mail does. */
|
|
123
124
|
sig: string;
|
|
125
|
+
/** The root's last segment, e.g. `my-pi-agent`: what the tab and the footer lead with. */
|
|
126
|
+
repo: string;
|
|
127
|
+
/** The repository root, absolute, so the footer can put the whole path on hover. */
|
|
128
|
+
root: string;
|
|
124
129
|
/** The store's directory, shortened. */
|
|
125
130
|
where: string;
|
|
126
131
|
}
|
|
127
132
|
|
|
128
133
|
/** What the server read before building one snapshot. */
|
|
129
134
|
export interface ViewInput {
|
|
130
|
-
me: Actor;
|
|
131
135
|
/**
|
|
132
|
-
* The
|
|
136
|
+
* The page's own actor: the seat the snapshot is read as.
|
|
133
137
|
*
|
|
134
|
-
* The page
|
|
135
|
-
*
|
|
136
|
-
* and the
|
|
138
|
+
* The page is not a session that happens to be serving — it is a seat that outlives
|
|
139
|
+
* whichever session serves it — so this is the one identity the browser's roster is marked
|
|
140
|
+
* against, and the page shows no other.
|
|
137
141
|
*/
|
|
138
|
-
|
|
142
|
+
me: Actor;
|
|
139
143
|
cards: readonly Card[];
|
|
140
144
|
now: number;
|
|
141
145
|
bytes: number;
|
|
142
146
|
/** Card files the reader refused; absent reads as none, which is the ordinary board. */
|
|
143
|
-
skipped?:
|
|
147
|
+
skipped?: readonly Skip[];
|
|
144
148
|
where: string;
|
|
149
|
+
/** The repository's name, from `BoardStore.name()`. */
|
|
150
|
+
repo: string;
|
|
151
|
+
/** The repository root, from `BoardStore.root()`. */
|
|
152
|
+
root: string;
|
|
145
153
|
/** The bus's records, when the page could read them at all. */
|
|
146
154
|
roster?: readonly RosterSession[];
|
|
155
|
+
/** The board's WIP limits, so a head can say `n/limit`; absent reads as the defaults. */
|
|
156
|
+
limits?: WipLimits;
|
|
147
157
|
}
|
|
148
158
|
|
|
149
159
|
/** Build the page's snapshot. */
|
|
@@ -155,26 +165,41 @@ export function buildView(input: ViewInput): BoardView {
|
|
|
155
165
|
// card the lane does, so a client reading `round.unassigned` is not handed a card
|
|
156
166
|
// missing the facts every other card has.
|
|
157
167
|
const cards = input.cards.map((card) => viewCardOf(card, index));
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
168
|
+
const limits = input.limits ?? DEFAULT_WIP;
|
|
169
|
+
const lanes = LANES.map((key) => {
|
|
170
|
+
const rule = LANE_RULES[key];
|
|
171
|
+
const inLane = cards.filter((card) => card.lane === key);
|
|
172
|
+
const limit = limitOf(key, limits);
|
|
173
|
+
return {
|
|
174
|
+
key,
|
|
175
|
+
label: rule.label,
|
|
176
|
+
empty: rule.empty,
|
|
177
|
+
role: rule.role,
|
|
178
|
+
...(limit === undefined ? {} : { limit }),
|
|
179
|
+
count: inLane.length,
|
|
180
|
+
cards: inLane,
|
|
181
|
+
};
|
|
182
|
+
});
|
|
183
|
+
const reader = input.me;
|
|
163
184
|
const teammates = input.roster === undefined ? undefined : teammatesOf(input.roster, reader.label, input.now);
|
|
164
185
|
return {
|
|
165
|
-
me: input.me.id === undefined ? { label: input.me.label } : { id: input.me.id, label: input.me.label },
|
|
166
|
-
...(input.session === undefined ? {} : { session: input.session }),
|
|
167
186
|
lanes,
|
|
168
|
-
|
|
187
|
+
total: cards.length,
|
|
169
188
|
round: buildRound(cards, input.now),
|
|
170
189
|
...(teammates === undefined ? {} : { teammates }),
|
|
171
190
|
bytes: input.bytes,
|
|
172
|
-
skipped: input.skipped ??
|
|
191
|
+
skipped: input.skipped ?? [],
|
|
173
192
|
// The roster enters the signature at its coarse facts only: a heartbeat every ten
|
|
174
193
|
// seconds is not news, while a join, a quiet and a letter are. A file that became
|
|
175
|
-
// readable, or unreadable, is news too
|
|
176
|
-
//
|
|
177
|
-
|
|
194
|
+
// readable, or unreadable, is news too, and so is a *different* file becoming
|
|
195
|
+
// unreadable: the footer names them, so the signature carries the names and reasons
|
|
196
|
+
// rather than a count that a swap would leave unchanged.
|
|
197
|
+
sig:
|
|
198
|
+
signatureOf(input.cards) +
|
|
199
|
+
rosterSignature(teammates) +
|
|
200
|
+
`|skipped:${(input.skipped ?? []).map((entry) => `${entry.file}:${entry.why}`).join(",")}`,
|
|
201
|
+
repo: input.repo,
|
|
202
|
+
root: input.root,
|
|
178
203
|
where: input.where,
|
|
179
204
|
};
|
|
180
205
|
}
|
|
@@ -186,8 +211,11 @@ export function buildView(input: ViewInput): BoardView {
|
|
|
186
211
|
* derived field written onto one would outlive the read that made it.
|
|
187
212
|
*/
|
|
188
213
|
function viewCardOf(card: Card, index: ReadonlyMap<string, Card>): ViewCard {
|
|
189
|
-
|
|
190
|
-
|
|
214
|
+
// The page draws these two as its own lines, and a reader has to know *which* card: the
|
|
215
|
+
// lane alone is a state, and the title is the card. The transcript asks for no titles —
|
|
216
|
+
// a refusal is a row, and the sentence is the point there.
|
|
217
|
+
const waits = lanesText(blockersOf(card, index), { titles: true });
|
|
218
|
+
const dependents = lanesText(openDependentsOf(card.id, index), { titles: true });
|
|
191
219
|
const since = laneSince(card);
|
|
192
220
|
return {
|
|
193
221
|
...card,
|
|
@@ -222,13 +250,3 @@ function rosterSignature(teammates: readonly ViewTeammate[] | undefined): string
|
|
|
222
250
|
teammates.map((teammate) => `${teammate.label}:${teammate.state}:${teammate.mail}:${teammate.listening}`).join(",")
|
|
223
251
|
);
|
|
224
252
|
}
|
|
225
|
-
|
|
226
|
-
/** The tallies, summed from the lanes so the two can never disagree. */
|
|
227
|
-
function countOf(lanes: readonly ViewLane[]): ViewCounts {
|
|
228
|
-
const counts: ViewCounts = { total: 0, todo: 0, doing: 0, blocked: 0, done: 0 };
|
|
229
|
-
for (const lane of lanes) {
|
|
230
|
-
counts[lane.status] = lane.cards.length;
|
|
231
|
-
counts.total += lane.cards.length;
|
|
232
|
-
}
|
|
233
|
-
return counts;
|
|
234
|
-
}
|