@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.
Files changed (158) hide show
  1. package/README.md +17 -11
  2. package/README.zh-CN.md +13 -8
  3. package/docs/extensions/README.md +1 -1
  4. package/docs/extensions/board.md +286 -123
  5. package/docs/extensions/bytefly.md +381 -223
  6. package/docs/extensions/file-browse.md +14 -7
  7. package/docs/extensions/job.md +1 -2
  8. package/docs/extensions/plan.md +56 -11
  9. package/docs/extensions/session-bus.md +23 -10
  10. package/docs/extensions/team.md +45 -17
  11. package/docs/extensions/wechat.md +2 -1
  12. package/docs/extensions/worktree.md +30 -15
  13. package/extensions/board/commands.ts +23 -5
  14. package/extensions/board/format.ts +107 -9
  15. package/extensions/board/join.ts +1 -1
  16. package/extensions/board/page.ts +170 -94
  17. package/extensions/board/result.ts +9 -0
  18. package/extensions/board/schema.ts +22 -7
  19. package/extensions/board/seat.ts +9 -10
  20. package/extensions/board/snapshot.ts +1 -0
  21. package/extensions/board/tools.ts +186 -21
  22. package/extensions/board/transport/router.ts +27 -108
  23. package/extensions/board/transport/server.ts +18 -29
  24. package/extensions/board/types.ts +67 -0
  25. package/extensions/board/view/client.ts +23 -13
  26. package/extensions/board/view/dialog.ts +43 -0
  27. package/extensions/board/view/keys.ts +28 -24
  28. package/extensions/board/view/live.ts +5 -171
  29. package/extensions/board/view/names.ts +91 -0
  30. package/extensions/board/view/page.ts +48 -88
  31. package/extensions/board/view/render.ts +213 -190
  32. package/extensions/board/view/styles.ts +65 -48
  33. package/extensions/board/view/team.ts +63 -41
  34. package/extensions/bytefly/footer/budget.ts +6 -5
  35. package/extensions/bytefly/footer/collect/entries.ts +12 -4
  36. package/extensions/bytefly/footer/collect/snapshot.ts +8 -17
  37. package/extensions/bytefly/footer/config.ts +19 -8
  38. package/extensions/bytefly/footer/price/price.ts +30 -8
  39. package/extensions/bytefly/footer/register.ts +5 -5
  40. package/extensions/bytefly/footer/render/format.ts +18 -0
  41. package/extensions/bytefly/footer/render/lines.ts +23 -7
  42. package/extensions/bytefly/footer/render/segments.ts +117 -31
  43. package/extensions/bytefly/footer/types.ts +29 -5
  44. package/extensions/bytefly/footer/unpriced.ts +42 -17
  45. package/extensions/bytefly/panel/commands.ts +28 -1
  46. package/extensions/bytefly/panel/filter.ts +68 -0
  47. package/extensions/bytefly/panel/format.ts +16 -15
  48. package/extensions/bytefly/panel/join.ts +56 -13
  49. package/extensions/bytefly/panel/prices.ts +43 -0
  50. package/extensions/bytefly/panel/settings.ts +72 -23
  51. package/extensions/bytefly/panel/tui/entries.ts +26 -8
  52. package/extensions/bytefly/panel/tui/frame.ts +130 -93
  53. package/extensions/bytefly/panel/tui/keys.ts +352 -0
  54. package/extensions/bytefly/panel/tui/list-view.ts +25 -46
  55. package/extensions/bytefly/panel/tui/panes.ts +76 -0
  56. package/extensions/bytefly/panel/tui/section-view.ts +126 -0
  57. package/extensions/bytefly/panel/tui/settings-section.ts +54 -125
  58. package/extensions/bytefly/panel/tui/settings-view.ts +30 -131
  59. package/extensions/bytefly/panel/tui/side-pane.ts +224 -0
  60. package/extensions/bytefly/panel/tui.ts +306 -259
  61. package/extensions/check/commands.ts +2 -1
  62. package/extensions/file-browse/files/classify.ts +136 -0
  63. package/extensions/file-browse/files/files.ts +8 -547
  64. package/extensions/file-browse/files/index.ts +90 -0
  65. package/extensions/file-browse/files/list.ts +261 -0
  66. package/extensions/file-browse/files/tree.ts +101 -0
  67. package/extensions/file-browse/transport/router.ts +0 -2
  68. package/extensions/file-browse/view/emit.ts +51 -0
  69. package/extensions/file-browse/view/html.ts +6 -7
  70. package/extensions/file-browse/view/markdown.ts +11 -13
  71. package/extensions/file-browse/view/raw-html.ts +18 -13
  72. package/extensions/job/runtime.ts +4 -4
  73. package/extensions/plan/feedback.ts +40 -16
  74. package/extensions/plan/format.ts +10 -3
  75. package/extensions/plan/index.ts +17 -1
  76. package/extensions/plan/plan-files.ts +88 -0
  77. package/extensions/plan/plans.ts +19 -23
  78. package/extensions/plan/policy.ts +10 -5
  79. package/extensions/plan/runtime.ts +31 -2
  80. package/extensions/plan/todos.ts +51 -3
  81. package/extensions/plan/tools/exit.ts +10 -1
  82. package/extensions/plan/tools/todo-seed.ts +4 -2
  83. package/extensions/plan/tools/write.ts +12 -1
  84. package/extensions/plan/types.ts +15 -0
  85. package/extensions/plan/web-blocks.ts +89 -0
  86. package/extensions/plan/web-chrome.ts +3 -5
  87. package/extensions/plan/web-client.ts +179 -330
  88. package/extensions/plan/web-collab.ts +3 -3
  89. package/extensions/plan/web-comments.ts +18 -8
  90. package/extensions/plan/web-composer.ts +80 -0
  91. package/extensions/plan/web-draft.ts +40 -0
  92. package/extensions/plan/web-keys.ts +134 -0
  93. package/extensions/plan/web-outline.ts +192 -0
  94. package/extensions/plan/web-page.ts +4 -1
  95. package/extensions/plan/web-plan.ts +150 -0
  96. package/extensions/plan/web-router.ts +0 -1
  97. package/extensions/plan/web-settle.ts +194 -0
  98. package/extensions/rewind/commands/review.ts +2 -1
  99. package/extensions/session-bus/rows.ts +7 -3
  100. package/extensions/session-bus/runtime.ts +100 -38
  101. package/extensions/session-bus/store.ts +14 -2
  102. package/extensions/session-bus/tui.ts +1 -1
  103. package/extensions/session-bus/types.ts +10 -1
  104. package/extensions/subagent/commands.ts +2 -1
  105. package/extensions/team/commands.ts +41 -8
  106. package/extensions/team/format.ts +14 -0
  107. package/extensions/team/index.ts +2 -1
  108. package/extensions/team/tools.ts +30 -9
  109. package/extensions/team/types.ts +17 -0
  110. package/extensions/todo/index.ts +4 -1
  111. package/extensions/todo/nudge.ts +8 -2
  112. package/extensions/wechat/bridge.ts +43 -172
  113. package/extensions/wechat/intake.ts +245 -0
  114. package/extensions/worktree/commands.ts +24 -17
  115. package/extensions/worktree/exit.ts +12 -6
  116. package/extensions/worktree/git.ts +22 -14
  117. package/extensions/worktree/guard.ts +15 -285
  118. package/extensions/worktree/handoff.ts +10 -2
  119. package/extensions/worktree/index.ts +2 -1
  120. package/extensions/worktree/lifecycle.ts +2 -0
  121. package/extensions/worktree/registry.ts +20 -11
  122. package/extensions/worktree/root-tools.ts +13 -3
  123. package/extensions/worktree/runtime.ts +8 -2
  124. package/extensions/worktree/screen.ts +15 -8
  125. package/extensions/worktree/shell.ts +303 -0
  126. package/extensions/worktree/state.ts +1 -1
  127. package/extensions/worktree/status.ts +42 -9
  128. package/extensions/worktree/tools.ts +19 -13
  129. package/lib/README.md +6 -5
  130. package/lib/board/card.ts +192 -3
  131. package/lib/board/links.ts +26 -2
  132. package/lib/board/page-pointer.ts +86 -32
  133. package/lib/board/round-text.ts +11 -2
  134. package/lib/board/round.ts +25 -20
  135. package/lib/board/rules.ts +144 -11
  136. package/lib/board/store.ts +239 -52
  137. package/lib/board/view.ts +94 -10
  138. package/lib/format.ts +22 -0
  139. package/lib/http/serve.ts +3 -0
  140. package/lib/http/sink.ts +34 -9
  141. package/lib/plan-entry.ts +11 -0
  142. package/lib/policy.ts +14 -1
  143. package/lib/session-roster.ts +74 -3
  144. package/lib/spawn.ts +37 -3
  145. package/lib/stray-fields.ts +45 -0
  146. package/lib/tool-names.ts +13 -0
  147. package/lib/tui/glyphs.ts +2 -3
  148. package/lib/web/README.md +52 -2
  149. package/lib/web/client-runtime.ts +123 -0
  150. package/lib/web/index.ts +4 -2
  151. package/lib/web/markup.ts +88 -1
  152. package/lib/web/names.ts +42 -0
  153. package/package.json +1 -1
  154. package/extensions/board/view/actions.ts +0 -306
  155. package/extensions/board/writes.ts +0 -99
  156. package/extensions/bytefly/panel/tui/extensions-view.ts +0 -40
  157. package/extensions/bytefly/panel/tui/skills-view.ts +0 -50
  158. package/extensions/job/process.ts +0 -35
@@ -16,16 +16,11 @@ import {
16
16
  serveLoopback,
17
17
  tokenMatches,
18
18
  } from "../../../lib/http/index.ts";
19
+ import type { IdleReason } from "../../../lib/http/sink.ts";
19
20
  import { defaultInterval, type RepeatTimer } from "../../../lib/timer.ts";
20
21
  import type { BoardView } from "../../../lib/board/view.ts";
21
22
  import type { PageView } from "../view/page.ts";
22
- import {
23
- type Authority,
24
- type BoardRouterContext,
25
- handleBoardRequest,
26
- type PageResult,
27
- type PageWrite,
28
- } from "./router.ts";
23
+ import { type BoardRouterContext, handleBoardRequest } from "./router.ts";
29
24
 
30
25
  /** How often the page is offered a frame; a frame is sent only when the signature moved. */
31
26
  export const PUSH_MS = 1_000;
@@ -37,18 +32,17 @@ export const MAX_BODY_BYTES = 64 * 1_024;
37
32
  export interface BoardServerOptions {
38
33
  /** Ports tried in order: the session's remembered port, then an OS-chosen one. */
39
34
  ports: readonly number[];
40
- /** The credential that may change a card; minted here when the caller has none. */
35
+ /** The page's token; minted here when the caller has none, so the note carries one. */
41
36
  token?: string;
42
- /** The credential a viewer carries: every read, and no write. */
43
- viewToken?: string;
44
- page: (view: PageView, authority: Authority) => Promise<string>;
37
+ page: (view: PageView) => Promise<string>;
45
38
  script: string;
46
39
  view: () => Promise<BoardView>;
47
- write: (request: PageWrite) => Promise<PageResult>;
48
- /** Called when the last tab has been gone for the grace period. */
49
- onIdle?: () => void;
40
+ /** Called when the page went idle: no tab ever opened, or the last one left. */
41
+ onIdle?: (reason: IdleReason) => void;
50
42
  /** Grace after the last stream leaves; 0 or absent waits for `close()`. */
51
43
  idleMs?: number;
44
+ /** Grace for a page no tab ever attached to; absent leaves the server open. */
45
+ firstIdleMs?: number;
52
46
  /** The interval seam, so a test drives the ticker instead of waiting. */
53
47
  interval?: RepeatTimer;
54
48
  }
@@ -56,10 +50,8 @@ export interface BoardServerOptions {
56
50
  /** A running page. */
57
51
  export interface BoardServer {
58
52
  port: number;
59
- /** The URL the lead opens: the page, with the credential that may change it. */
53
+ /** The URL for this page: one address, because there is one thing a page may do. */
60
54
  url: string;
61
- /** The URL a teammate opens: the same page, read-only. */
62
- viewUrl: string;
63
55
  /** How many tabs are attached, which is what `/board status` reports. */
64
56
  watchers: () => number;
65
57
  /** The signature of the last frame pushed, so a caller can tell it is live. */
@@ -69,24 +61,16 @@ export interface BoardServer {
69
61
 
70
62
  export async function createBoardServer(options: BoardServerOptions): Promise<BoardServer> {
71
63
  const token = options.token ?? mintToken();
72
- const viewToken = options.viewToken ?? mintToken();
73
64
  const clients = new Set<EventSink>();
74
65
  const interval = options.interval ?? defaultInterval;
75
66
  let pushed = "";
76
67
 
77
68
  const ctx: BoardRouterContext = {
78
- // Two credentials, one page: the lead's may change a card, a teammate's may only
79
- // read it. Which of the two a request carries is the whole authorisation story.
80
- authority: (given) => {
81
- const value = given ?? "";
82
- if (tokenMatches(token, value)) return "write";
83
- return tokenMatches(viewToken, value) ? "view" : undefined;
84
- },
69
+ read: (given) => tokenMatches(token, given ?? ""),
85
70
  hosts: () => LOOPBACK_HOSTS,
86
71
  page: options.page,
87
72
  script: options.script,
88
73
  view: options.view,
89
- write: options.write,
90
74
  };
91
75
 
92
76
  const push = async (): Promise<void> => {
@@ -105,9 +89,15 @@ export async function createBoardServer(options: BoardServerOptions): Promise<Bo
105
89
  maxBodyBytes: MAX_BODY_BYTES,
106
90
  ...(options.idleMs === undefined || options.onIdle === undefined
107
91
  ? {}
108
- : { idle: { ms: options.idleMs, onIdle: options.onIdle } }),
92
+ : {
93
+ idle: {
94
+ ms: options.idleMs,
95
+ ...(options.firstIdleMs === undefined ? {} : { firstMs: options.firstIdleMs }),
96
+ onIdle: options.onIdle,
97
+ },
98
+ }),
109
99
  async handler(request) {
110
- const result = await handleBoardRequest(ctx, request.method, request.url, request.headers, request.body);
100
+ const result = await handleBoardRequest(ctx, request.method, request.url, request.headers);
111
101
  if (!result.stream) return { result };
112
102
  return {
113
103
  result,
@@ -141,7 +131,6 @@ export async function createBoardServer(options: BoardServerOptions): Promise<Bo
141
131
  return {
142
132
  port: server.port,
143
133
  url: `http://127.0.0.1:${server.port}/?token=${token}`,
144
- viewUrl: `http://127.0.0.1:${server.port}/?token=${viewToken}`,
145
134
  watchers: () => clients.size,
146
135
  pushed: () => pushed,
147
136
  close,
@@ -25,12 +25,75 @@ export const BOARD_ACTIONS = [
25
25
  "note",
26
26
  "plan",
27
27
  "edit",
28
+ "archive",
28
29
  "drop",
29
30
  ] as const;
30
31
 
31
32
  /** One action. */
32
33
  export type BoardAction = (typeof BOARD_ACTIONS)[number];
33
34
 
35
+ /**
36
+ * What each verb does, in one sentence.
37
+ *
38
+ * The tool description is built from this table and the parameter's `action`
39
+ * description points at that sentence, so the verbs are stated once. A new action is
40
+ * a row here and an entry in {@link BOARD_ACTIONS}; `test` holds the two together.
41
+ */
42
+ export const BOARD_ACTION_DOCS: Record<BoardAction, string> = {
43
+ list:
44
+ "shows the board — `status` filters one lane, `mine` only what this session holds, " +
45
+ "`query` matches a title, a holder, a path hint, the plan, or a note's text, `card` reads that one card " +
46
+ "instead of the board, and `round` adds the lead's rollup of owners, unassigned, unplanned, blocked, and " +
47
+ "idle, which always covers the whole board — and a listing that names no lane leaves out the `done` cards, " +
48
+ "saying how many it left out",
49
+ add: "creates a `todo` card",
50
+ claim: "takes one for this session",
51
+ release: "gives it back",
52
+ reclaim: "takes it from whoever holds it",
53
+ move: "changes lane",
54
+ note: "appends to its history",
55
+ plan: "records the plan file being executed",
56
+ edit: "fixes what the card says — title, path hints, priority, and what it waits for",
57
+ archive:
58
+ "sets a finished card aside, out of the board and still readable, or every finished card when no `card` is named",
59
+ drop: "deletes a `todo`, a `done`, or one you hold",
60
+ };
61
+
62
+ /**
63
+ * The fields each action acts on: the behaviour, where {@link BOARD_ACTION_DOCS} is prose.
64
+ *
65
+ * The parameters are one flat object, so a field is published to every action at once;
66
+ * that is only harmless while each action reads its own. A call carrying a field its
67
+ * action never reads is refused against this table, because the alternative is what
68
+ * `add` did with `dependsOn`: accepted, ignored, and reported as recorded.
69
+ *
70
+ * `test` holds the other direction — every parameter but `action` is claimed here — so
71
+ * a new field cannot be published without an action that acts on it.
72
+ */
73
+ export const ACTION_FIELDS: Record<BoardAction, readonly string[]> = {
74
+ list: ["card", "status", "mine", "query", "round", "archived"],
75
+ add: ["title", "paths", "priority", "note"],
76
+ claim: ["card"],
77
+ release: ["card"],
78
+ reclaim: ["card"],
79
+ move: ["card", "status", "blockedBy"],
80
+ note: ["card", "text"],
81
+ plan: ["card", "path"],
82
+ edit: ["card", "title", "paths", "priority", "dependsOn"],
83
+ archive: ["card"],
84
+ drop: ["card"],
85
+ };
86
+
87
+ /**
88
+ * The actions that change the board: everything but a read.
89
+ *
90
+ * One definition, read by the tool (does this call need a teammate?) and by plan
91
+ * mode (may this call run while planning?), so "a read" cannot mean two things.
92
+ */
93
+ export const BOARD_MUTATING_ACTIONS: ReadonlySet<BoardAction> = new Set(
94
+ BOARD_ACTIONS.filter((action) => action !== "list"),
95
+ );
96
+
34
97
  /** A result the transcript renders and the page's routes mirror. */
35
98
  export interface BoardDetails {
36
99
  action: BoardAction;
@@ -44,6 +107,10 @@ export interface BoardDetails {
44
107
  round?: RoundSummary;
45
108
  /** The id a `drop` removed. */
46
109
  dropped?: string;
110
+ /** The ids an `archive` set aside, in the order it moved them. */
111
+ archived?: string[];
112
+ /** Cards an `archive` sweep could not set aside, and why: a move that changed underneath it. */
113
+ failed?: Array<{ card: string; error: string }>;
47
114
  /** A refusal's reason, which the renderer draws instead of the details. */
48
115
  error?: string;
49
116
  }
@@ -1,35 +1,45 @@
1
1
  /**
2
- * The client bundle: the five modules that paint, round up, write, bind keys, and
3
- * follow the frame stream, plus the kit's own theme toggle and one `dialogScript`
4
- * per modal, and the one entry point that starts them.
2
+ * The client bundle: the modules that paint, round up, open a card, bind keys, and
3
+ * follow the frame stream, plus the kit's own theme toggle and the two dialog scripts,
4
+ * and the one entry point that starts them.
5
5
  *
6
6
  * Served as one string, because a page that opens six requests before it can paint
7
7
  * is a page that looks broken for a moment. Nothing here is typed — no compiler sees
8
8
  * a template string — so the split is by job and the class-contract test reads the
9
- * result: `render.ts` paints, `team.ts` draws the round, `actions.ts` posts and owns
10
- * the two dialogs, `keys.ts` binds, and `live.ts` owns the tabs and the stream.
9
+ * result: `render.ts` paints, `team.ts` draws the round, `dialog.ts` opens one card,
10
+ * `keys.ts` binds, and `live.ts` owns the tabs and the stream.
11
11
  *
12
12
  * Load order is a dependency: `team` takes helpers from `render`, and `live` starts
13
13
  * last, after every `board.*` handler it wires exists.
14
14
  */
15
15
 
16
- import { THEME_TOGGLE_JS, dialogScript, keysDialogScript } from "../../../lib/web/index.ts";
17
- import { BOARD_ACTIONS_JS } from "./actions.ts";
16
+ import { MATCHES_QUERY_JS } from "../../../lib/board/card.ts";
17
+ import { CLIENT_RUNTIME_JS } from "../../../lib/web/index.ts";
18
+ import { PLURAL_JS } from "../../../lib/board/round-text.ts";
19
+ import { FORMAT_AGE_JS } from "../../../lib/format.ts";
20
+ import { namesScript, TAB_TITLE_JS, THEME_TOGGLE_JS, dialogScript, keysDialogScript } from "../../../lib/web/index.ts";
21
+ import { BOARD_CLASS } from "./names.ts";
22
+ import { BOARD_DIALOG_JS } from "./dialog.ts";
18
23
  import { BOARD_KEYS_JS } from "./keys.ts";
19
24
  import { BOARD_LIVE_JS } from "./live.ts";
20
- import { BOARD_RENDER_JS } from "./render.ts";
25
+ import { renderJs } from "./render.ts";
21
26
  import { BOARD_TEAM_JS } from "./team.ts";
22
27
 
23
- /** The whole client, in load order: paint, round, write, keys, wiring, then start. */
24
- export const BOARD_CLIENT_JS = `${BOARD_RENDER_JS}
28
+ /**
29
+ * The whole client, in load order: paint, round, the card dialog, keys, wiring, start.
30
+ *
31
+ * Four rules cross the boundary as source — the count's wording, the tab title's
32
+ * shape, what a `query` matches, and how an age reads: the browser has no import graph,
33
+ * and a second copy of any of them is exactly what this bundle is not allowed to hold.
34
+ */
35
+ export const BOARD_CLIENT_JS = `${namesScript(BOARD_CLASS)}
36
+ ${renderJs({ plural: PLURAL_JS, tabTitle: TAB_TITLE_JS, matches: MATCHES_QUERY_JS, age: FORMAT_AGE_JS, runtime: CLIENT_RUNTIME_JS })}
25
37
  ${BOARD_TEAM_JS}
26
- ${BOARD_ACTIONS_JS}
38
+ ${BOARD_DIALOG_JS}
27
39
  ${BOARD_KEYS_JS}
28
40
  ${BOARD_LIVE_JS}
29
41
  ${THEME_TOGGLE_JS}
30
- ${dialogScript({ dialogId: "board-new-dialog", globalName: "bfNew" })}
31
42
  ${dialogScript({ dialogId: "board-card-dialog", closeId: "card-dialog-close", globalName: "bfCard" })}
32
- ${dialogScript({ dialogId: "board-confirm", globalName: "bfConfirm" })}
33
43
  ${keysDialogScript("keys-open")}
34
44
  (function () {
35
45
  "use strict";
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The card dialog: one card, whole, as a read view.
3
+ *
4
+ * Opening, closing, and forgetting which card is open is the whole of it. The page
5
+ * cannot change a card, so this module has no request, no confirmation, no banner and
6
+ * no footer actions — the dialogs it used to fill are gone from the document, and the
7
+ * only way a card changes is a tool.
8
+ *
9
+ * The one thing it must not do is leave a stale panel behind: a dialog the kit closed —
10
+ * `Esc`, a backdrop click, its own close button — forgets `openId`, so the next frame
11
+ * stops filling something nobody can see.
12
+ */
13
+
14
+ export const BOARD_DIALOG_JS = String.raw`(function () {
15
+ "use strict";
16
+ var board = window.bfBoard || (window.bfBoard = {});
17
+
18
+ /** Fill and open the card dialog. */
19
+ board.openCard = function (id) {
20
+ var view = board.view;
21
+ if (!view) return;
22
+ var card = board.findCard(view, id);
23
+ if (!card || !window.bfCard) return;
24
+ board.openId = id;
25
+ board.fillDialog(card, view);
26
+ window.bfCard.open();
27
+ };
28
+
29
+ board.closeCard = function () {
30
+ if (window.bfCard) window.bfCard.close();
31
+ };
32
+
33
+ board.dialogOpen = function () {
34
+ return window.bfCard ? window.bfCard.isOpen() : false;
35
+ };
36
+
37
+ var cardDialog = document.getElementById("board-card-dialog");
38
+ if (cardDialog) {
39
+ new MutationObserver(function () {
40
+ if (cardDialog.hidden) board.openId = undefined;
41
+ }).observe(cardDialog, { attributes: true, attributeFilter: ["hidden"] });
42
+ }
43
+ })();`;
@@ -7,16 +7,15 @@
7
7
  * module exists to prevent, since no type checker sees the bundle.
8
8
  *
9
9
  * The vocabulary is the TUI's where it means the same thing (`j`/`k`, `?`, `Esc`);
10
- * `n`, `/` and `t` are this page's own.
10
+ * `/` and `t` are this page's own. There is no key that changes a card: the page reads.
11
11
  */
12
12
 
13
13
  import type { KeyHint } from "../../../lib/web/index.ts";
14
14
 
15
15
  /** One binding: the key or keys, and what pressing it does. */
16
16
  export const BOARD_KEYS: readonly KeyHint[] = [
17
- { keys: "n", label: "New card — open the dialog and focus the title field" },
18
17
  { keys: "/", label: "Filter the cards" },
19
- { keys: ["j", "k"], label: "Next or previous card" },
18
+ { keys: ["j", "k"], label: "Next or previous card, or the next card in an open dialog" },
20
19
  { keys: "Enter", label: "Open the focused card" },
21
20
  { keys: "t", label: "Switch Board and Team" },
22
21
  { keys: "Esc", label: "Close a dialog, or clear the filter" },
@@ -35,23 +34,35 @@ export const BOARD_KEYS_JS = String.raw`(function () {
35
34
  "use strict";
36
35
  var board = window.bfBoard || (window.bfBoard = {});
37
36
 
38
- /** Whether the event landed in a field, where every key is text. */
39
- function typing(target) {
40
- if (!target || !target.tagName) return false;
41
- var tag = target.tagName.toLowerCase();
42
- return tag === "input" || tag === "textarea" || tag === "select" || target.isContentEditable === true;
43
- }
37
+ /** The render bundle's runtime: one typing test for the whole page. */
38
+ var typing = board.typing;
44
39
 
45
40
  /** The card nodes in reading order, so j/k walk the board as it is drawn. */
46
41
  function cards() {
47
- var seen = [];
48
- var nodes = document.querySelectorAll(".board-card");
49
- for (var i = 0; i < nodes.length; i++) if (!nodes[i].hidden) seen.push(nodes[i]);
50
- return seen;
42
+ return board.drawnCards();
43
+ }
44
+
45
+ /** Move the open dialog to the next or previous card; false when no dialog is open. */
46
+ function walk(delta) {
47
+ if (!board.dialogOpen || !board.dialogOpen()) return false;
48
+ var drawn = cards();
49
+ if (drawn.length === 0) return true;
50
+ // The open card is the one marked open, not the focused node: focus sits on the
51
+ // dialog's own panel, which is not one of the cards.
52
+ var at = -1;
53
+ for (var i = 0; i < drawn.length; i++) {
54
+ if (drawn[i].getAttribute("data-card") === String(board.openId)) at = i;
55
+ }
56
+ var next = at < 0 ? (delta > 0 ? 0 : drawn.length - 1) : (at + delta + drawn.length) % drawn.length;
57
+ board.openCard(drawn[next].getAttribute("data-card"));
58
+ return true;
51
59
  }
52
60
 
53
61
  /** Move focus to the next or previous drawn card, and say so for a screen reader. */
54
62
  function step(delta) {
63
+ // An open dialog walks instead of moving focus: the record stays whole, and Esc
64
+ // still returns to the board where it was.
65
+ if (walk(delta)) return;
55
66
  var drawn = cards();
56
67
  if (drawn.length === 0) return;
57
68
  var at = drawn.indexOf(document.activeElement);
@@ -78,9 +89,9 @@ export const BOARD_KEYS_JS = String.raw`(function () {
78
89
  board.closeCard();
79
90
  return;
80
91
  }
81
- // The composer is a kit dialog, so the kit's own Esc closed it in the capture
82
- // phase already; a filter with text is what Escape is for here. An empty one
83
- // gives the keyboard back to the page by leaving the field.
92
+ // What is left for Escape is the filter: a field with text loses it, and the field
93
+ // itself then gives the keyboard back to the page. The keys sheet and the card
94
+ // dialog are above this, in the order a reader is likely to be inside them.
84
95
  if (board.hasFilter()) {
85
96
  board.clearFilter();
86
97
  if (typing(target)) target.blur();
@@ -93,17 +104,10 @@ export const BOARD_KEYS_JS = String.raw`(function () {
93
104
  return;
94
105
  }
95
106
  if (typing(target)) {
96
- if (event.key === "Enter" && target.id === "card-dialog-note") board.addNote();
97
107
  if (event.key === "Enter" && target.id === "board-filter") board.applyFilter();
98
108
  return;
99
109
  }
100
- if (event.key === "n") {
101
- // Two modals over one board is a trap whose edge the reader cannot see, and a
102
- // viewer's page has no composer to open at all.
103
- if (!board.canWrite() || board.dialogOpen() || board.composerOpen()) return;
104
- event.preventDefault();
105
- board.focusComposer();
106
- } else if (event.key === "/") {
110
+ if (event.key === "/") {
107
111
  event.preventDefault();
108
112
  board.focusFilter();
109
113
  } else if (event.key === "j") {
@@ -1,6 +1,5 @@
1
1
  /**
2
- * The client's wiring: the tabs, the lane picker, the new-card dialog, the filter,
3
- * and the live frame stream.
2
+ * The client's wiring: the tabs, the lane picker, the filter, and the live frame stream.
4
3
  *
5
4
  * A frame is a whole snapshot, so every message repaints from it rather than patching
6
5
  * it — the renderer decides what actually changed. The only client state is which tab
@@ -24,7 +23,7 @@ export const BOARD_LIVE_JS = String.raw`(function () {
24
23
  var node = document.getElementById("board-status");
25
24
  if (!node) return;
26
25
  node.setAttribute("data-state", state);
27
- node.className = "bf-status board-status" +
26
+ node.className = "bf-status " + N.status +
28
27
  (state === "live" ? " bf-status--saved" : state === "reconnecting" ? " bf-status--error" : "");
29
28
  // "now" would read as "live · now ago", so a fresh frame says only "live".
30
29
  if (state === "live") {
@@ -87,104 +86,6 @@ export const BOARD_LIVE_JS = String.raw`(function () {
87
86
 
88
87
  board.showView = showView;
89
88
 
90
- /**
91
- * The composer writes one card: a new one, or the one the card dialog was showing.
92
- *
93
- * The mode lives here rather than in the form, because the form is a draft: leaving
94
- * edit mode puts the dialog back the way the toolbar's button opens it, and the text
95
- * it held was the card's, not the reader's.
96
- */
97
- var composer = { mode: "add", card: undefined };
98
-
99
- /** The title's character count, shown from the first character typed. */
100
- function showTitleCount() {
101
- var field = document.getElementById("new-title");
102
- var count = document.getElementById("new-title-count");
103
- if (!field || !count) return;
104
- var max = Number(field.getAttribute("maxlength"));
105
- if (!max) return;
106
- var used = field.value.length;
107
- count.textContent = used + "/" + max;
108
- count.hidden = used === 0;
109
- }
110
-
111
- /** Dress the dialog for a new card or for the one being edited, and fill it in. */
112
- function setComposerMode(mode, card) {
113
- var heading = document.getElementById("board-new-title");
114
- var submit = document.getElementById("new-submit");
115
- var hint = document.getElementById("composer-hint");
116
- var form = document.getElementById("board-composer");
117
- var was = composer.mode;
118
- composer.mode = mode;
119
- composer.card = mode === "edit" ? card : undefined;
120
- if (mode === "edit") {
121
- var title = document.getElementById("new-title");
122
- var paths = document.getElementById("new-paths");
123
- var priority = document.getElementById("new-priority");
124
- if (title) title.value = card.title;
125
- if (paths) paths.value = (card.paths || []).join(" ");
126
- if (priority) priority.value = board.priorityOf(card);
127
- if (heading) heading.textContent = "Edit card #" + card.id;
128
- if (submit) submit.textContent = "Save";
129
- if (hint) hint.textContent = "links are recorded with the board tool, while the card is being planned";
130
- showTitleCount();
131
- return;
132
- }
133
- // Leaving edit mode throws its text away; a draft written here survives Esc.
134
- if (was === "edit" && form) form.reset();
135
- if (heading) heading.textContent = "New card";
136
- if (submit) submit.textContent = "Add card";
137
- if (hint && board.composerHint !== undefined) hint.textContent = board.composerHint;
138
- showTitleCount();
139
- }
140
-
141
- /** Open whichever mode the composer is dressed for, with the caret in the title. */
142
- function openComposerForm() {
143
- if (!window.bfNew) return;
144
- // The kit focuses a dialog's first control, which here is its close button; a card
145
- // starts with a title, so the caret belongs in that field.
146
- window.bfNew.open();
147
- var input = document.getElementById("new-title");
148
- if (input) input.focus();
149
- }
150
-
151
- /** Open the new-card dialog. */
152
- board.openComposer = function () {
153
- if (composer.mode === "edit") setComposerMode("add");
154
- openComposerForm();
155
- };
156
-
157
- /** Open it on one card: the same two fields, prefilled, with Save in place of Add. */
158
- board.editCard = function (card) {
159
- if (!window.bfNew) return;
160
- board.closeCard();
161
- setComposerMode("edit", card);
162
- openComposerForm();
163
- var title = document.getElementById("new-title");
164
- // The end of the title is where a typo is, so that is where the caret starts.
165
- if (title && title.setSelectionRange) title.setSelectionRange(title.value.length, title.value.length);
166
- };
167
-
168
- /** Close it and keep the draft: only Cancel throws one away. */
169
- board.closeComposer = function () {
170
- if (window.bfNew) window.bfNew.close();
171
- };
172
-
173
- /** Discard the draft and close, which is what its own Cancel button does. */
174
- board.cancelComposer = function () {
175
- var form = document.getElementById("board-composer");
176
- if (form) form.reset();
177
- setComposerMode("add");
178
- board.closeComposer();
179
- };
180
-
181
- board.composerOpen = function () {
182
- return window.bfNew ? window.bfNew.isOpen() : false;
183
- };
184
-
185
- board.focusComposer = function () {
186
- board.openComposer();
187
- };
188
89
  board.focusFilter = function () {
189
90
  var input = document.getElementById("board-filter");
190
91
  if (input) {
@@ -234,8 +135,8 @@ export const BOARD_LIVE_JS = String.raw`(function () {
234
135
  if (!lanes || lanes.childElementCount > 0) return;
235
136
  lanes.setAttribute("data-skeleton", "1");
236
137
  for (var i = 0; i < 4; i++) {
237
- var lane = board.el("section", "board-lane");
238
- for (var j = 0; j < 2; j++) lane.appendChild(board.el("div", "board-skeleton"));
138
+ var lane = board.el("section", N.lane);
139
+ for (var j = 0; j < 2; j++) lane.appendChild(board.el("div", N.skeleton));
239
140
  lanes.appendChild(lane);
240
141
  }
241
142
  }
@@ -278,70 +179,7 @@ export const BOARD_LIVE_JS = String.raw`(function () {
278
179
  };
279
180
  }
280
181
 
281
- /** The new-card dialog: a title, space-separated path hints, a priority, and Add. */
282
- function wireComposer() {
283
- var form = document.getElementById("board-composer");
284
- if (!form) return;
285
- // The hint is the page's, not this module's: a second copy of the sentence would drift.
286
- var hint = document.getElementById("composer-hint");
287
- board.composerHint = hint ? hint.textContent : "";
288
- // The field is capped at 120 characters, and a cap nobody can see reads as a bug the
289
- // first time typing stops; the count appears with the first character.
290
- var titleField = document.getElementById("new-title");
291
- if (titleField) titleField.addEventListener("input", showTitleCount);
292
- form.addEventListener("submit", function (event) {
293
- event.preventDefault();
294
- var title = document.getElementById("new-title");
295
- if (!title.value.trim()) {
296
- title.focus();
297
- return;
298
- }
299
- var paths = document.getElementById("new-paths").value.split(/\s+/).filter(Boolean);
300
- var priorityField = document.getElementById("new-priority");
301
- var priority = priorityField ? priorityField.value : "normal";
302
- if (composer.mode === "edit") {
303
- var card = composer.card;
304
- if (!card) return;
305
- // Every field goes, including the unchanged ones: an edit means "this is what it
306
- // should say", and a priority changed back to normal is a change too.
307
- board.act("edit", card, { title: title.value.trim(), paths: paths, priority: priority }).then(function (payload) {
308
- if (payload) board.cancelComposer();
309
- });
310
- return;
311
- }
312
- var input = { title: title.value.trim(), paths: paths };
313
- if (priority !== "normal") input.priority = priority;
314
- board.add(input).then(function (payload) {
315
- if (!payload) return;
316
- // The dialog stays open: a lead publishes several cards in a row.
317
- form.reset();
318
- showTitleCount();
319
- title.focus();
320
- });
321
- });
322
- var trigger = document.getElementById("board-new");
323
- if (trigger) {
324
- trigger.addEventListener("click", function () {
325
- if (board.composerOpen()) board.closeComposer();
326
- else board.openComposer();
327
- });
328
- }
329
- // The dialog's single closer: Esc and the backdrop keep the draft, this does not.
330
- var cancel = document.getElementById("board-cancel");
331
- if (cancel) {
332
- cancel.addEventListener("click", function () {
333
- board.cancelComposer();
334
- });
335
- }
336
- var first = document.getElementById("board-first-card");
337
- if (first) {
338
- first.addEventListener("click", function () {
339
- board.openComposer();
340
- });
341
- }
342
- }
343
-
344
- /** The filter, the note button, and the two tab sets. */
182
+ /** The filter and the two tab sets. */
345
183
  function wireChrome() {
346
184
  var filter = document.getElementById("board-filter");
347
185
  if (filter) {
@@ -349,9 +187,6 @@ export const BOARD_LIVE_JS = String.raw`(function () {
349
187
  board.applyFilter();
350
188
  });
351
189
  }
352
- var noteAdd = document.getElementById("card-dialog-note-add");
353
- if (noteAdd) noteAdd.addEventListener("click", function () { board.addNote(); });
354
-
355
190
  var tabs = document.querySelectorAll("[data-view][role='tab']");
356
191
  for (var i = 0; i < tabs.length; i++) {
357
192
  tabs[i].addEventListener("click", function (event) {
@@ -389,7 +224,6 @@ export const BOARD_LIVE_JS = String.raw`(function () {
389
224
  board.collapsed = {};
390
225
  showView(new URLSearchParams(window.location.search).get("view") === "team" ? "team" : "board");
391
226
  wireChrome();
392
- wireComposer();
393
227
  skeletons();
394
228
  board.bindKeys(document);
395
229
  stream();