@gavin-hu/my-pi-agent 0.18.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 +66 -52
- package/README.zh-CN.md +56 -46
- package/docs/extensions/README.md +34 -0
- package/docs/extensions/board.md +507 -0
- package/{extensions/bytefly/README.md → docs/extensions/bytefly.md} +774 -574
- package/{extensions/check/README.md → docs/extensions/check.md} +83 -68
- package/{extensions/context/README.md → docs/extensions/context.md} +40 -23
- package/docs/extensions/file-browse.md +561 -0
- package/{extensions/file-open/README.md → docs/extensions/file-open.md} +31 -23
- package/{extensions/file-read/README.md → docs/extensions/file-read.md} +47 -55
- package/{extensions/goal/README.md → docs/extensions/goal.md} +115 -131
- package/{extensions/job/README.md → docs/extensions/job.md} +62 -52
- package/{extensions/memory/README.md → docs/extensions/memory.md} +71 -60
- package/{extensions/plan/README.md → docs/extensions/plan.md} +440 -322
- package/{extensions/question/README.md → docs/extensions/question.md} +54 -31
- package/{extensions/rewind/README.md → docs/extensions/rewind.md} +115 -122
- package/docs/extensions/session-bus.md +298 -0
- package/{extensions/subagent/README.md → docs/extensions/subagent.md} +165 -118
- package/docs/extensions/team.md +254 -0
- package/{extensions/todo/README.md → docs/extensions/todo.md} +85 -88
- package/{extensions/watch/README.md → docs/extensions/watch.md} +54 -36
- package/{extensions/web-access/README.md → docs/extensions/web-access.md} +93 -50
- package/{extensions/wechat/README.md → docs/extensions/wechat.md} +260 -163
- package/{extensions/worktree/README.md → docs/extensions/worktree.md} +137 -79
- package/extensions/board/commands.ts +105 -0
- package/extensions/board/config.ts +36 -0
- package/extensions/board/format.ts +170 -0
- package/extensions/board/index.ts +40 -0
- package/extensions/board/input.ts +54 -0
- package/extensions/board/join.ts +42 -0
- package/extensions/board/page.ts +388 -0
- package/extensions/board/result.ts +108 -0
- package/extensions/board/rows.ts +48 -0
- package/extensions/board/schema.ts +90 -0
- package/extensions/board/seat.ts +28 -0
- package/extensions/board/snapshot.ts +42 -0
- package/extensions/board/tools.ts +444 -0
- package/extensions/board/transport/router.ts +91 -0
- package/extensions/board/transport/server.ts +138 -0
- package/extensions/board/types.ts +121 -0
- package/extensions/board/view/client.ts +51 -0
- package/extensions/board/view/dialog.ts +43 -0
- package/extensions/board/view/keys.ts +125 -0
- package/extensions/board/view/live.ts +231 -0
- package/extensions/board/view/names.ts +91 -0
- package/extensions/board/view/page.ts +118 -0
- package/extensions/board/view/render.ts +519 -0
- package/extensions/board/view/styles.ts +228 -0
- package/extensions/board/view/team.ts +327 -0
- 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 +67 -7
- 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-runtime.ts +0 -6
- package/extensions/bytefly/panel/settings.ts +75 -43
- 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/bytefly/picker/discovery.ts +0 -2
- package/extensions/bytefly/picker/types.ts +0 -2
- package/extensions/bytefly/update/check.ts +39 -0
- package/extensions/bytefly/update/format.ts +15 -0
- package/extensions/bytefly/update/register.ts +73 -34
- package/extensions/bytefly/update/state.ts +6 -3
- package/extensions/check/commands.ts +2 -1
- package/extensions/context/tui.ts +1 -1
- package/extensions/file-browse/{config.ts → config/config.ts} +1 -1
- package/extensions/file-browse/files/classify.ts +136 -0
- package/extensions/file-browse/files/files.ts +12 -0
- package/extensions/file-browse/files/index.ts +90 -0
- package/extensions/file-browse/files/list.ts +261 -0
- package/extensions/file-browse/{paths.ts → files/paths.ts} +1 -1
- package/extensions/file-browse/files/tree.ts +101 -0
- package/extensions/file-browse/{git.ts → git/git.ts} +10 -3
- package/extensions/file-browse/index.ts +4 -4
- package/extensions/file-browse/{live.ts → live/live.ts} +20 -69
- package/extensions/file-browse/{router.ts → transport/router.ts} +21 -17
- package/extensions/file-browse/transport/server.ts +110 -0
- package/extensions/file-browse/{chrome.ts → view/chrome.ts} +4 -4
- package/extensions/file-browse/{client.ts → view/client.ts} +1 -1
- package/extensions/file-browse/{diff-html.ts → view/diff-html.ts} +3 -3
- package/extensions/file-browse/view/emit.ts +51 -0
- package/extensions/file-browse/{html.ts → view/html.ts} +19 -18
- package/extensions/file-browse/{icons.ts → view/icons.ts} +1 -1
- package/extensions/file-browse/{markdown.ts → view/markdown.ts} +18 -20
- package/extensions/file-browse/{raw-html.ts → view/raw-html.ts} +19 -14
- package/extensions/file-browse/{styles.ts → view/styles.ts} +29 -5
- package/extensions/file-browse/{tree.ts → view/tree.ts} +1 -1
- package/extensions/file-browse/{urls.ts → view/urls.ts} +1 -1
- package/extensions/file-read/schema.ts +2 -1
- package/extensions/job/runtime.ts +4 -4
- package/extensions/memory/store.ts +12 -5
- package/extensions/plan/blocks.ts +23 -2
- package/extensions/plan/commands.ts +21 -16
- package/extensions/plan/feedback.ts +185 -52
- package/extensions/plan/format.ts +10 -3
- package/extensions/plan/index.ts +23 -5
- package/extensions/plan/plan-files.ts +88 -0
- package/extensions/plan/plan-index.ts +43 -9
- package/extensions/plan/plan-text.ts +33 -23
- package/extensions/plan/plans.ts +27 -34
- package/extensions/plan/policy.ts +30 -17
- package/extensions/plan/review.ts +3 -27
- package/extensions/plan/runtime.ts +52 -11
- package/extensions/plan/sections.ts +112 -0
- package/extensions/plan/steps.ts +3 -22
- package/extensions/plan/todos.ts +51 -3
- package/extensions/plan/tools/exit.ts +20 -95
- package/extensions/plan/tools/todo-seed.ts +4 -2
- package/extensions/plan/tools/write.ts +12 -1
- package/extensions/plan/tui.ts +5 -34
- package/extensions/plan/types.ts +15 -8
- 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 -8
- package/extensions/plan/web-plan.ts +150 -0
- package/extensions/plan/web-router.ts +3 -58
- package/extensions/plan/web-settle.ts +194 -0
- package/extensions/plan/web-share-session.ts +24 -30
- package/extensions/plan/web-streams.ts +14 -14
- package/extensions/plan/web-tokens.ts +2 -2
- package/extensions/plan/web.ts +66 -120
- package/extensions/rewind/{commands.ts → commands/commands.ts} +5 -5
- package/extensions/rewind/{review.ts → commands/review.ts} +15 -14
- package/extensions/rewind/{rewind.ts → commands/rewind.ts} +6 -6
- package/extensions/rewind/{config.ts → config/config.ts} +1 -1
- package/extensions/rewind/index.ts +3 -3
- package/extensions/rewind/{policy.ts → state/policy.ts} +4 -4
- package/extensions/rewind/{runtime.ts → state/runtime.ts} +11 -11
- package/extensions/rewind/{snapshot-run.ts → state/snapshot-run.ts} +1 -1
- package/extensions/rewind/{timeline.ts → state/timeline.ts} +2 -2
- package/extensions/rewind/{format.ts → text/format.ts} +3 -3
- package/extensions/rewind/{git.ts → transport/git.ts} +2 -2
- package/extensions/rewind/{restore.ts → transport/restore.ts} +2 -2
- package/extensions/rewind/{snapshot.ts → transport/snapshot.ts} +1 -1
- package/extensions/rewind/{store.ts → transport/store.ts} +1 -1
- package/extensions/rewind/{checklist.ts → view/checklist.ts} +6 -6
- package/extensions/rewind/{hunk-picker.ts → view/hunk-picker.ts} +8 -8
- package/extensions/rewind/{path-picker.ts → view/path-picker.ts} +7 -7
- package/extensions/rewind/{review-tui.ts → view/review-tui.ts} +11 -11
- package/extensions/rewind/{rewind-tui.ts → view/rewind-tui.ts} +17 -11
- package/extensions/rewind/{status.ts → view/status.ts} +4 -4
- package/extensions/session-bus/commands.ts +17 -69
- package/extensions/session-bus/format.ts +22 -45
- package/extensions/session-bus/index.ts +13 -7
- package/extensions/session-bus/join-request.ts +40 -0
- package/extensions/session-bus/mail-watch.ts +134 -0
- package/extensions/session-bus/notice.ts +32 -4
- package/{lib/claims → extensions/session-bus}/overlap.ts +11 -18
- package/extensions/session-bus/protocol.ts +19 -49
- package/extensions/session-bus/resolve.ts +67 -0
- package/extensions/session-bus/result.ts +16 -51
- package/extensions/session-bus/rows.ts +11 -5
- package/extensions/session-bus/runtime.ts +220 -267
- package/extensions/session-bus/store.ts +55 -7
- package/extensions/session-bus/tools.ts +31 -66
- package/extensions/session-bus/tui.ts +5 -5
- package/extensions/session-bus/types.ts +23 -14
- package/extensions/subagent/agents.ts +9 -8
- package/extensions/subagent/commands.ts +2 -1
- package/extensions/subagent/invocation.ts +4 -2
- package/extensions/team/commands.ts +145 -0
- package/extensions/team/dispatch.ts +133 -0
- package/extensions/team/format.ts +119 -0
- package/extensions/team/index.ts +34 -0
- package/extensions/team/schema.ts +29 -0
- package/extensions/team/tools.ts +267 -0
- package/extensions/team/types.ts +65 -0
- package/extensions/todo/index.ts +4 -1
- package/extensions/todo/nudge.ts +8 -2
- package/extensions/web-access/fetch/page.ts +4 -1
- package/extensions/web-access/fetch/structured.ts +447 -0
- package/extensions/web-access/fetch/tool.ts +2 -1
- package/extensions/web-access/search/tool.ts +2 -1
- package/extensions/wechat/bridge.ts +106 -251
- package/extensions/wechat/{commands.ts → commands/commands.ts} +26 -18
- package/extensions/wechat/{doctor.ts → commands/doctor.ts} +5 -5
- package/extensions/wechat/{history.ts → commands/history.ts} +1 -1
- package/extensions/wechat/{login.ts → commands/login.ts} +3 -3
- package/extensions/wechat/{slash.ts → commands/slash.ts} +38 -2
- package/extensions/wechat/dialog/channels.ts +136 -0
- package/extensions/wechat/{interaction.ts → dialog/interaction.ts} +37 -37
- package/extensions/wechat/{prompt.ts → dialog/prompt.ts} +2 -2
- package/extensions/wechat/{remote-ui.ts → dialog/remote-ui.ts} +1 -1
- package/extensions/wechat/index.ts +10 -10
- package/extensions/wechat/intake.ts +245 -0
- package/extensions/wechat/{admission.ts → peer/admission.ts} +2 -2
- package/extensions/wechat/{peer-session.ts → peer/peer-session.ts} +2 -2
- package/extensions/wechat/{peer-worker.ts → peer/peer-worker.ts} +100 -22
- package/extensions/wechat/{presence.ts → peer/presence.ts} +8 -7
- package/extensions/wechat/{reply-pipeline.ts → peer/reply-pipeline.ts} +1 -1
- package/extensions/wechat/{credentials.ts → state/credentials.ts} +2 -2
- package/extensions/wechat/{state.ts → state/state.ts} +14 -2
- package/extensions/wechat/{format.ts → text/format.ts} +3 -3
- package/extensions/wechat/{notices.ts → text/notices.ts} +8 -0
- package/extensions/wechat/tools.ts +29 -12
- package/extensions/wechat/{client.ts → transport/client.ts} +26 -2
- package/extensions/wechat/{deliver.ts → transport/deliver.ts} +72 -28
- package/extensions/wechat/{inbound.ts → transport/inbound.ts} +5 -5
- package/extensions/wechat/{media.ts → transport/media.ts} +11 -5
- package/extensions/wechat/transport/outbound.ts +142 -0
- package/extensions/wechat/{poll.ts → transport/poll.ts} +3 -3
- package/extensions/wechat/types.ts +10 -1
- package/extensions/wechat/{login-screen.ts → view/login-screen.ts} +4 -4
- package/extensions/wechat/{status-screen.ts → view/status-screen.ts} +31 -26
- 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 +18 -290
- 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 +11 -8
- package/lib/board/card.ts +479 -0
- package/lib/board/links.ts +141 -0
- package/lib/board/page-pointer.ts +310 -0
- package/lib/board/round-text.ts +81 -0
- package/lib/board/round.ts +94 -0
- package/lib/board/rules.ts +437 -0
- package/lib/board/session.ts +105 -0
- package/lib/board/store.ts +497 -0
- package/lib/board/view.ts +234 -0
- package/lib/errors.ts +12 -4
- package/lib/format.ts +35 -0
- package/lib/http/guard.ts +112 -0
- package/lib/http/index.ts +16 -5
- package/lib/http/serve.ts +237 -0
- package/lib/http/server.ts +12 -10
- package/lib/http/sink.ts +135 -0
- package/lib/http/token.ts +38 -0
- package/lib/plan-entry.ts +11 -0
- package/lib/policy.ts +25 -2
- package/lib/requirements.ts +6 -7
- package/lib/session-label.ts +64 -0
- package/lib/session-roster.ts +201 -0
- package/lib/session-tool.ts +184 -0
- package/lib/spawn.ts +37 -3
- package/lib/stray-fields.ts +45 -0
- package/lib/tool-names.ts +38 -2
- package/lib/tui/glyphs.ts +3 -2
- package/lib/web/README.md +57 -4
- package/lib/web/client-runtime.ts +123 -0
- package/lib/web/components.ts +5 -0
- package/lib/web/index.ts +4 -2
- package/lib/web/markup.ts +88 -1
- package/lib/web/names.ts +42 -0
- package/lib/web/runtime.ts +7 -1
- package/lib/web/tokens.ts +25 -2
- package/package.json +26 -14
- package/skills/next/SKILL.md +4 -3
- package/skills/project-docs/SKILL.md +133 -0
- package/skills/project-docs/references/audit.md +131 -0
- package/skills/project-docs/references/changelog.md +105 -0
- package/skills/project-docs/references/discovery.md +127 -0
- package/skills/project-docs/references/readme.md +80 -0
- package/skills/project-docs/references/release.md +84 -0
- package/skills/project-docs/references/roadmap.md +93 -0
- package/skills/project-docs/scripts/state.ts +316 -0
- package/skills/refactor/SKILL.md +100 -125
- package/skills/refactor/references/handoff.md +35 -0
- package/skills/refactor/references/signals.md +54 -0
- package/skills/refactor/scripts/inventory.ts +587 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +0 -40
- package/extensions/bytefly/panel/tui/skills-view.ts +0 -50
- package/extensions/file-browse/README.md +0 -506
- package/extensions/file-browse/files.ts +0 -564
- package/extensions/file-browse/server.ts +0 -141
- package/extensions/job/process.ts +0 -35
- package/extensions/plan/split.ts +0 -87
- package/extensions/session-bus/README.md +0 -247
- package/extensions/session-bus/team.ts +0 -102
- package/extensions/wechat/outbound.ts +0 -100
- package/lib/plans/sections.ts +0 -172
- package/lib/plans/setting.ts +0 -52
- package/lib/plans/split.ts +0 -215
- package/lib/plans/store.ts +0 -19
- package/lib/plans/team.ts +0 -151
- package/skills/changelog/SKILL.md +0 -148
- package/skills/parallel-dev/SKILL.md +0 -150
- /package/extensions/file-browse/{rev.ts → live/rev.ts} +0 -0
- /package/extensions/file-browse/{diff.ts → text/diff.ts} +0 -0
- /package/extensions/file-browse/{fuzzy.ts → text/fuzzy.ts} +0 -0
- /package/extensions/file-browse/{live-client.ts → view/live-client.ts} +0 -0
- /package/extensions/file-browse/{palette.ts → view/palette.ts} +0 -0
- /package/extensions/rewind/{patch.ts → text/patch.ts} +0 -0
- /package/extensions/rewind/{paths.ts → text/paths.ts} +0 -0
- /package/extensions/rewind/{queue.ts → transport/queue.ts} +0 -0
- /package/extensions/rewind/{temp-index.ts → transport/temp-index.ts} +0 -0
- /package/extensions/wechat/{lock.ts → state/lock.ts} +0 -0
- /package/extensions/wechat/{channel.ts → text/channel.ts} +0 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The board page's router: one request in, one response out, and every policy the
|
|
3
|
+
* served surface obeys decided here.
|
|
4
|
+
*
|
|
5
|
+
* The page reads. It cannot change a card, so what is left to decide is the `Host`
|
|
6
|
+
* allowlist, the token in the URL, and which reads exist — a method that would write is
|
|
7
|
+
* refused by the method list before a handler is reached. The guards themselves are
|
|
8
|
+
* `lib/http`'s — this module only decides the order and the sentences.
|
|
9
|
+
*
|
|
10
|
+
* Nothing here knows what a board is. The snapshot arrives as `view()` and the document
|
|
11
|
+
* as `page()`, both supplied by `page.ts`, so this file can be tested with a fake store
|
|
12
|
+
* and no server.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
envelopeRefusal,
|
|
17
|
+
eventHeaders,
|
|
18
|
+
jsonError,
|
|
19
|
+
jsonResponse,
|
|
20
|
+
pageHeaders,
|
|
21
|
+
parseRequestUrl,
|
|
22
|
+
scriptHeaders,
|
|
23
|
+
type ServeResponse,
|
|
24
|
+
} from "../../../lib/http/index.ts";
|
|
25
|
+
import type { BoardView } from "../../../lib/board/view.ts";
|
|
26
|
+
import { THEME_JS, THEME_JS_PATH } from "../../../lib/web/index.ts";
|
|
27
|
+
import type { PageView } from "../view/page.ts";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The methods this surface serves: the two reads and the stream.
|
|
31
|
+
*
|
|
32
|
+
* `PAGE_METHODS` also carries `POST`, because a page that writes needs it. This one does
|
|
33
|
+
* not, so a write is refused by the envelope — before the token is even read — and the
|
|
34
|
+
* `Allow` header says the truth about what a browser can do here.
|
|
35
|
+
*/
|
|
36
|
+
const METHODS: readonly string[] = ["GET", "HEAD"];
|
|
37
|
+
|
|
38
|
+
/** Everything the router needs from the running page. */
|
|
39
|
+
export interface BoardRouterContext {
|
|
40
|
+
/** Whether a token is this page's. */
|
|
41
|
+
read: (given: string | null) => boolean;
|
|
42
|
+
/** The names this server answers to. */
|
|
43
|
+
hosts: () => readonly string[];
|
|
44
|
+
/** The document for one tab. */
|
|
45
|
+
page: (view: PageView) => Promise<string>;
|
|
46
|
+
/** The client script. */
|
|
47
|
+
script: string;
|
|
48
|
+
/** The snapshot the page renders, and the body of `/api/board`. */
|
|
49
|
+
view: () => Promise<BoardView>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The cheap liveness route: a probe reads this, and never the snapshot. */
|
|
53
|
+
export const PING_PATH = "/api/ping";
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Handle one request. `method`, `rawUrl`, and `headers` are the raw HTTP values.
|
|
57
|
+
*/
|
|
58
|
+
export async function handleBoardRequest(
|
|
59
|
+
ctx: BoardRouterContext,
|
|
60
|
+
method: string,
|
|
61
|
+
rawUrl: string,
|
|
62
|
+
headers: Record<string, string | string[] | undefined> = {},
|
|
63
|
+
): Promise<ServeResponse> {
|
|
64
|
+
const envelope = envelopeRefusal({ method, headers, hosts: ctx.hosts(), allowed: METHODS });
|
|
65
|
+
if (envelope) return envelope;
|
|
66
|
+
|
|
67
|
+
const parsed = parseRequestUrl(rawUrl);
|
|
68
|
+
if (!parsed) return jsonError(400, "Malformed request URL.");
|
|
69
|
+
const { pathname, search } = parsed;
|
|
70
|
+
|
|
71
|
+
if (pathname === THEME_JS_PATH) return { status: 200, headers: scriptHeaders(), body: THEME_JS };
|
|
72
|
+
if (pathname === "/app.js") return { status: 200, headers: scriptHeaders(), body: ctx.script };
|
|
73
|
+
|
|
74
|
+
if (!ctx.read(search.get("token"))) return jsonError(403, "Invalid or missing token.");
|
|
75
|
+
|
|
76
|
+
// The liveness answer, and nothing else: a session deciding whether this page is alive
|
|
77
|
+
// must not pay for the snapshot, which is a store read and a roster read on the same
|
|
78
|
+
// event loop the probe is racing.
|
|
79
|
+
if (pathname === PING_PATH) return jsonResponse(200, { ok: true });
|
|
80
|
+
if (pathname === "/api/board") return jsonResponse(200, await ctx.view());
|
|
81
|
+
if (pathname === "/" || pathname === "/index.html") {
|
|
82
|
+
return { status: 200, headers: pageHeaders(), body: await ctx.page(viewOf(search.get("view"))) };
|
|
83
|
+
}
|
|
84
|
+
if (pathname === "/events") return { status: 200, headers: eventHeaders(), stream: true };
|
|
85
|
+
return jsonError(404, "That page does not exist.");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The tab a `?view=` names; anything else is the board. */
|
|
89
|
+
function viewOf(raw: string | null): PageView {
|
|
90
|
+
return raw === "team" ? "team" : "board";
|
|
91
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The board page's server: one loopback listener, the snapshot pushed to every
|
|
3
|
+
* attached tab, and the idle grace that closes the page when the last one leaves.
|
|
4
|
+
*
|
|
5
|
+
* `lib/http` owns the socket — binding, the body cap, the SSE head, and the
|
|
6
|
+
* idempotent close — so this module owns only the board's own decisions: when a
|
|
7
|
+
* frame is pushed (only when a card's revision moved), who receives it, and what
|
|
8
|
+
* stops the ticker.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
type EventSink,
|
|
13
|
+
LOOPBACK_HOSTS,
|
|
14
|
+
type LoopbackServer,
|
|
15
|
+
mintToken,
|
|
16
|
+
serveLoopback,
|
|
17
|
+
tokenMatches,
|
|
18
|
+
} from "../../../lib/http/index.ts";
|
|
19
|
+
import type { IdleReason } from "../../../lib/http/sink.ts";
|
|
20
|
+
import { defaultInterval, type RepeatTimer } from "../../../lib/timer.ts";
|
|
21
|
+
import type { BoardView } from "../../../lib/board/view.ts";
|
|
22
|
+
import type { PageView } from "../view/page.ts";
|
|
23
|
+
import { type BoardRouterContext, handleBoardRequest } from "./router.ts";
|
|
24
|
+
|
|
25
|
+
/** How often the page is offered a frame; a frame is sent only when the signature moved. */
|
|
26
|
+
export const PUSH_MS = 1_000;
|
|
27
|
+
|
|
28
|
+
/** Largest request body the page accepts: a card and a note are small. */
|
|
29
|
+
export const MAX_BODY_BYTES = 64 * 1_024;
|
|
30
|
+
|
|
31
|
+
/** What the page is built from. The lifecycle (`page.ts`) supplies all of it. */
|
|
32
|
+
export interface BoardServerOptions {
|
|
33
|
+
/** Ports tried in order: the session's remembered port, then an OS-chosen one. */
|
|
34
|
+
ports: readonly number[];
|
|
35
|
+
/** The page's token; minted here when the caller has none, so the note carries one. */
|
|
36
|
+
token?: string;
|
|
37
|
+
page: (view: PageView) => Promise<string>;
|
|
38
|
+
script: string;
|
|
39
|
+
view: () => Promise<BoardView>;
|
|
40
|
+
/** Called when the page went idle: no tab ever opened, or the last one left. */
|
|
41
|
+
onIdle?: (reason: IdleReason) => void;
|
|
42
|
+
/** Grace after the last stream leaves; 0 or absent waits for `close()`. */
|
|
43
|
+
idleMs?: number;
|
|
44
|
+
/** Grace for a page no tab ever attached to; absent leaves the server open. */
|
|
45
|
+
firstIdleMs?: number;
|
|
46
|
+
/** The interval seam, so a test drives the ticker instead of waiting. */
|
|
47
|
+
interval?: RepeatTimer;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** A running page. */
|
|
51
|
+
export interface BoardServer {
|
|
52
|
+
port: number;
|
|
53
|
+
/** The URL for this page: one address, because there is one thing a page may do. */
|
|
54
|
+
url: string;
|
|
55
|
+
/** How many tabs are attached, which is what `/board status` reports. */
|
|
56
|
+
watchers: () => number;
|
|
57
|
+
/** The signature of the last frame pushed, so a caller can tell it is live. */
|
|
58
|
+
pushed: () => string;
|
|
59
|
+
close: () => Promise<void>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function createBoardServer(options: BoardServerOptions): Promise<BoardServer> {
|
|
63
|
+
const token = options.token ?? mintToken();
|
|
64
|
+
const clients = new Set<EventSink>();
|
|
65
|
+
const interval = options.interval ?? defaultInterval;
|
|
66
|
+
let pushed = "";
|
|
67
|
+
|
|
68
|
+
const ctx: BoardRouterContext = {
|
|
69
|
+
read: (given) => tokenMatches(token, given ?? ""),
|
|
70
|
+
hosts: () => LOOPBACK_HOSTS,
|
|
71
|
+
page: options.page,
|
|
72
|
+
script: options.script,
|
|
73
|
+
view: options.view,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const push = async (): Promise<void> => {
|
|
77
|
+
if (clients.size === 0) return;
|
|
78
|
+
const view = await options.view();
|
|
79
|
+
// A frame is a snapshot, so sending one that says nothing new costs the page a
|
|
80
|
+
// parse and every reader a flicker. The signature is what changes.
|
|
81
|
+
if (view.sig === pushed) return;
|
|
82
|
+
pushed = view.sig;
|
|
83
|
+
const frame = `data: ${JSON.stringify(view)}\n\n`;
|
|
84
|
+
for (const sink of clients) sink.write(frame);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const server: LoopbackServer = await serveLoopback({
|
|
88
|
+
ports: options.ports,
|
|
89
|
+
maxBodyBytes: MAX_BODY_BYTES,
|
|
90
|
+
...(options.idleMs === undefined || options.onIdle === undefined
|
|
91
|
+
? {}
|
|
92
|
+
: {
|
|
93
|
+
idle: {
|
|
94
|
+
ms: options.idleMs,
|
|
95
|
+
...(options.firstIdleMs === undefined ? {} : { firstMs: options.firstIdleMs }),
|
|
96
|
+
onIdle: options.onIdle,
|
|
97
|
+
},
|
|
98
|
+
}),
|
|
99
|
+
async handler(request) {
|
|
100
|
+
const result = await handleBoardRequest(ctx, request.method, request.url, request.headers);
|
|
101
|
+
if (!result.stream) return { result };
|
|
102
|
+
return {
|
|
103
|
+
result,
|
|
104
|
+
openSink(sink) {
|
|
105
|
+
clients.add(sink);
|
|
106
|
+
// A tab that just attached gets the current frame without waiting for a
|
|
107
|
+
// card to change — otherwise a quiet board looks like a dead page.
|
|
108
|
+
void options.view().then((view) => {
|
|
109
|
+
pushed = view.sig;
|
|
110
|
+
sink.write(`data: ${JSON.stringify(view)}\n\n`);
|
|
111
|
+
});
|
|
112
|
+
return () => {
|
|
113
|
+
clients.delete(sink);
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const stopTicker = interval(PUSH_MS, () => void push());
|
|
121
|
+
let closed = false;
|
|
122
|
+
const close = async (): Promise<void> => {
|
|
123
|
+
if (closed) return;
|
|
124
|
+
closed = true;
|
|
125
|
+
stopTicker();
|
|
126
|
+
for (const sink of clients) sink.end();
|
|
127
|
+
clients.clear();
|
|
128
|
+
await server.close();
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
port: server.port,
|
|
133
|
+
url: `http://127.0.0.1:${server.port}/?token=${token}`,
|
|
134
|
+
watchers: () => clients.size,
|
|
135
|
+
pushed: () => pushed,
|
|
136
|
+
close,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shapes the `board` tool registers and reports.
|
|
3
|
+
*
|
|
4
|
+
* `details` is the result's state: the card an action produced, the cards a list
|
|
5
|
+
* read, the rollup when it was asked for, and — on a refusal — the card as it was,
|
|
6
|
+
* so a retry starts from the truth rather than from what the model thought it
|
|
7
|
+
* knew. The renderer and the tool share these types, and nothing else.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Card } from "../../lib/board/card.ts";
|
|
11
|
+
import type { Actor } from "../../lib/board/rules.ts";
|
|
12
|
+
import type { RoundSummary } from "../../lib/board/round.ts";
|
|
13
|
+
|
|
14
|
+
/** The tool's name. One extension registers it, so it stays here. */
|
|
15
|
+
export const BOARD_TOOL = "board";
|
|
16
|
+
|
|
17
|
+
/** The actions the tool accepts, in the order the description lists them. */
|
|
18
|
+
export const BOARD_ACTIONS = [
|
|
19
|
+
"list",
|
|
20
|
+
"add",
|
|
21
|
+
"claim",
|
|
22
|
+
"release",
|
|
23
|
+
"reclaim",
|
|
24
|
+
"move",
|
|
25
|
+
"note",
|
|
26
|
+
"plan",
|
|
27
|
+
"edit",
|
|
28
|
+
"archive",
|
|
29
|
+
"drop",
|
|
30
|
+
] as const;
|
|
31
|
+
|
|
32
|
+
/** One action. */
|
|
33
|
+
export type BoardAction = (typeof BOARD_ACTIONS)[number];
|
|
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
|
+
|
|
97
|
+
/** A result the transcript renders and the page's routes mirror. */
|
|
98
|
+
export interface BoardDetails {
|
|
99
|
+
action: BoardAction;
|
|
100
|
+
/** The serving session: the id Pi knows, and the label a card records. */
|
|
101
|
+
me: Actor;
|
|
102
|
+
/** The board a `list` read, in lane order. */
|
|
103
|
+
cards?: Card[];
|
|
104
|
+
/** The card an action produced, or the one a refusal left untouched. */
|
|
105
|
+
card?: Card;
|
|
106
|
+
/** The lead's rollup, only for `list` with `round`. */
|
|
107
|
+
round?: RoundSummary;
|
|
108
|
+
/** The id a `drop` removed. */
|
|
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 }>;
|
|
114
|
+
/** A refusal's reason, which the renderer draws instead of the details. */
|
|
115
|
+
error?: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Whether a value is one of the actions, so an unknown one is a refusal and not a crash. */
|
|
119
|
+
export function normalizeBoardAction(raw: unknown): BoardAction | undefined {
|
|
120
|
+
return typeof raw === "string" && BOARD_ACTIONS.includes(raw as BoardAction) ? (raw as BoardAction) : undefined;
|
|
121
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
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
|
+
*
|
|
6
|
+
* Served as one string, because a page that opens six requests before it can paint
|
|
7
|
+
* is a page that looks broken for a moment. Nothing here is typed — no compiler sees
|
|
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, `dialog.ts` opens one card,
|
|
10
|
+
* `keys.ts` binds, and `live.ts` owns the tabs and the stream.
|
|
11
|
+
*
|
|
12
|
+
* Load order is a dependency: `team` takes helpers from `render`, and `live` starts
|
|
13
|
+
* last, after every `board.*` handler it wires exists.
|
|
14
|
+
*/
|
|
15
|
+
|
|
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";
|
|
23
|
+
import { BOARD_KEYS_JS } from "./keys.ts";
|
|
24
|
+
import { BOARD_LIVE_JS } from "./live.ts";
|
|
25
|
+
import { renderJs } from "./render.ts";
|
|
26
|
+
import { BOARD_TEAM_JS } from "./team.ts";
|
|
27
|
+
|
|
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 })}
|
|
37
|
+
${BOARD_TEAM_JS}
|
|
38
|
+
${BOARD_DIALOG_JS}
|
|
39
|
+
${BOARD_KEYS_JS}
|
|
40
|
+
${BOARD_LIVE_JS}
|
|
41
|
+
${THEME_TOGGLE_JS}
|
|
42
|
+
${dialogScript({ dialogId: "board-card-dialog", closeId: "card-dialog-close", globalName: "bfCard" })}
|
|
43
|
+
${keysDialogScript("keys-open")}
|
|
44
|
+
(function () {
|
|
45
|
+
"use strict";
|
|
46
|
+
var board = window.bfBoard;
|
|
47
|
+
if (!board) return;
|
|
48
|
+
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", function () { board.start(); });
|
|
49
|
+
else board.start();
|
|
50
|
+
})();
|
|
51
|
+
`;
|
|
@@ -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
|
+
})();`;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The board page's keyboard: one table, and the two halves that read it.
|
|
3
|
+
*
|
|
4
|
+
* The sheet the user opens with `?` is server-rendered from {@link BOARD_KEYS}, and
|
|
5
|
+
* the bindings that make those keys work are {@link BOARD_KEYS_JS}, so the key a row
|
|
6
|
+
* advertises and the key the client listens for sit in one file — the drift this
|
|
7
|
+
* module exists to prevent, since no type checker sees the bundle.
|
|
8
|
+
*
|
|
9
|
+
* The vocabulary is the TUI's where it means the same thing (`j`/`k`, `?`, `Esc`);
|
|
10
|
+
* `/` and `t` are this page's own. There is no key that changes a card: the page reads.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { KeyHint } from "../../../lib/web/index.ts";
|
|
14
|
+
|
|
15
|
+
/** One binding: the key or keys, and what pressing it does. */
|
|
16
|
+
export const BOARD_KEYS: readonly KeyHint[] = [
|
|
17
|
+
{ keys: "/", label: "Filter the cards" },
|
|
18
|
+
{ keys: ["j", "k"], label: "Next or previous card, or the next card in an open dialog" },
|
|
19
|
+
{ keys: "Enter", label: "Open the focused card" },
|
|
20
|
+
{ keys: "t", label: "Switch Board and Team" },
|
|
21
|
+
{ keys: "Esc", label: "Close a dialog, or clear the filter" },
|
|
22
|
+
{ keys: "?", label: "This sheet" },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The bindings, as the client sees them.
|
|
27
|
+
*
|
|
28
|
+
* Every branch reads `board.` state and calls `board.` handlers that `render.ts`,
|
|
29
|
+
* `team.ts`, and `live.ts` already published, so this module decides only *which
|
|
30
|
+
* key* does what. A field with focus keeps its keys: `n` inside the note input types
|
|
31
|
+
* an `n`.
|
|
32
|
+
*/
|
|
33
|
+
export const BOARD_KEYS_JS = String.raw`(function () {
|
|
34
|
+
"use strict";
|
|
35
|
+
var board = window.bfBoard || (window.bfBoard = {});
|
|
36
|
+
|
|
37
|
+
/** The render bundle's runtime: one typing test for the whole page. */
|
|
38
|
+
var typing = board.typing;
|
|
39
|
+
|
|
40
|
+
/** The card nodes in reading order, so j/k walk the board as it is drawn. */
|
|
41
|
+
function cards() {
|
|
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;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Move focus to the next or previous drawn card, and say so for a screen reader. */
|
|
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;
|
|
66
|
+
var drawn = cards();
|
|
67
|
+
if (drawn.length === 0) return;
|
|
68
|
+
var at = drawn.indexOf(document.activeElement);
|
|
69
|
+
var next = at < 0 ? (delta > 0 ? 0 : drawn.length - 1) : (at + delta + drawn.length) % drawn.length;
|
|
70
|
+
var button = drawn[next].querySelector(".board-card__title");
|
|
71
|
+
if (button) button.focus();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
board.bindKeys = function (doc) {
|
|
75
|
+
doc.addEventListener("keydown", function (event) {
|
|
76
|
+
if (event.metaKey || event.ctrlKey || event.altKey) return;
|
|
77
|
+
var target = event.target;
|
|
78
|
+
if (event.key === "?" && !typing(target)) {
|
|
79
|
+
event.preventDefault();
|
|
80
|
+
board.openKeys();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (event.key === "Escape") {
|
|
84
|
+
if (board.keysOpen && board.keysOpen()) {
|
|
85
|
+
board.closeKeys();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (board.dialogOpen && board.dialogOpen()) {
|
|
89
|
+
board.closeCard();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
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.
|
|
95
|
+
if (board.hasFilter()) {
|
|
96
|
+
board.clearFilter();
|
|
97
|
+
if (typing(target)) target.blur();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (typing(target)) {
|
|
101
|
+
target.blur();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (typing(target)) {
|
|
107
|
+
if (event.key === "Enter" && target.id === "board-filter") board.applyFilter();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (event.key === "/") {
|
|
111
|
+
event.preventDefault();
|
|
112
|
+
board.focusFilter();
|
|
113
|
+
} else if (event.key === "j") {
|
|
114
|
+
event.preventDefault();
|
|
115
|
+
step(1);
|
|
116
|
+
} else if (event.key === "k") {
|
|
117
|
+
event.preventDefault();
|
|
118
|
+
step(-1);
|
|
119
|
+
} else if (event.key === "t") {
|
|
120
|
+
event.preventDefault();
|
|
121
|
+
board.showView(board.viewName() === "team" ? "board" : "team");
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
})();`;
|