@gavin-hu/my-pi-agent 0.17.0 → 0.19.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 +61 -45
- package/README.zh-CN.md +54 -42
- package/docs/extensions/README.md +34 -0
- package/docs/extensions/board.md +344 -0
- package/{extensions/bytefly/README.md → docs/extensions/bytefly.md} +465 -416
- 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 +554 -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} +66 -53
- package/{extensions/memory/README.md → docs/extensions/memory.md} +74 -63
- package/docs/extensions/plan.md +976 -0
- package/{extensions/question/README.md → docs/extensions/question.md} +54 -28
- package/{extensions/rewind/README.md → docs/extensions/rewind.md} +115 -122
- package/docs/extensions/session-bus.md +285 -0
- package/{extensions/subagent/README.md → docs/extensions/subagent.md} +166 -119
- package/docs/extensions/team.md +226 -0
- package/{extensions/todo/README.md → docs/extensions/todo.md} +85 -88
- package/{extensions/watch/README.md → docs/extensions/watch.md} +60 -38
- package/{extensions/web-access/README.md → docs/extensions/web-access.md} +93 -50
- package/{extensions/wechat/README.md → docs/extensions/wechat.md} +275 -192
- package/{extensions/worktree/README.md → docs/extensions/worktree.md} +115 -72
- package/extensions/board/commands.ts +87 -0
- package/extensions/board/config.ts +36 -0
- package/extensions/board/format.ts +72 -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 +312 -0
- package/extensions/board/result.ts +99 -0
- package/extensions/board/rows.ts +48 -0
- package/extensions/board/schema.ts +75 -0
- package/extensions/board/seat.ts +29 -0
- package/extensions/board/snapshot.ts +41 -0
- package/extensions/board/tools.ts +279 -0
- package/extensions/board/transport/router.ts +172 -0
- package/extensions/board/transport/server.ts +149 -0
- package/extensions/board/types.ts +54 -0
- package/extensions/board/view/actions.ts +306 -0
- package/extensions/board/view/client.ts +41 -0
- package/extensions/board/view/keys.ts +121 -0
- package/extensions/board/view/live.ts +397 -0
- package/extensions/board/view/page.ts +158 -0
- package/extensions/board/view/render.ts +496 -0
- package/extensions/board/view/styles.ts +211 -0
- package/extensions/board/view/team.ts +305 -0
- package/extensions/board/writes.ts +99 -0
- package/extensions/bytefly/notify/types.ts +1 -1
- package/extensions/bytefly/panel/commands.ts +40 -7
- package/extensions/bytefly/panel/settings-runtime.ts +0 -6
- package/extensions/bytefly/panel/settings.ts +11 -21
- package/extensions/bytefly/panel/tui/settings-view.ts +1 -1
- 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/context/tui.ts +1 -1
- package/extensions/file-browse/{config.ts → config/config.ts} +13 -4
- package/extensions/file-browse/{files.ts → files/files.ts} +1 -14
- package/extensions/file-browse/{paths.ts → files/paths.ts} +1 -1
- 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} +54 -82
- package/extensions/file-browse/live/rev.ts +42 -0
- package/extensions/file-browse/{router.ts → transport/router.ts} +44 -17
- package/extensions/file-browse/transport/server.ts +110 -0
- package/extensions/file-browse/view/chrome.ts +83 -0
- package/extensions/file-browse/{client.ts → view/client.ts} +63 -1
- package/extensions/file-browse/view/diff-html.ts +94 -0
- package/extensions/file-browse/{html.ts → view/html.ts} +32 -158
- package/extensions/file-browse/{icons.ts → view/icons.ts} +1 -1
- package/extensions/file-browse/{live-client.ts → view/live-client.ts} +47 -1
- package/extensions/file-browse/{markdown.ts → view/markdown.ts} +7 -7
- package/extensions/file-browse/{raw-html.ts → view/raw-html.ts} +1 -1
- package/extensions/file-browse/{styles.ts → view/styles.ts} +31 -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/goal/plan-route.ts +8 -2
- package/extensions/job/index.ts +15 -7
- package/extensions/memory/store.ts +12 -5
- package/extensions/plan/blocks.ts +23 -2
- package/extensions/plan/commands.ts +77 -21
- package/extensions/plan/feedback.ts +409 -63
- package/extensions/plan/index.ts +7 -5
- package/extensions/plan/plan-index.ts +44 -10
- package/extensions/plan/plan-text.ts +51 -24
- package/extensions/plan/plans.ts +162 -57
- package/extensions/plan/policy.ts +20 -12
- package/extensions/plan/review-prompt.ts +93 -0
- package/extensions/plan/review.ts +51 -95
- package/extensions/plan/runtime.ts +22 -10
- package/extensions/plan/sections.ts +112 -0
- package/extensions/plan/steps.ts +63 -95
- package/extensions/plan/tools/enter.ts +8 -3
- package/extensions/plan/tools/exit.ts +13 -19
- package/extensions/plan/tools/write.ts +8 -3
- package/extensions/plan/tools.ts +1 -1
- package/extensions/plan/tui.ts +29 -15
- package/extensions/plan/types.ts +29 -3
- package/extensions/plan/web-chrome.ts +2 -1
- package/extensions/plan/web-client.ts +122 -178
- package/extensions/plan/web-collab.ts +207 -0
- package/extensions/plan/web-comments.ts +269 -0
- package/extensions/plan/web-page.ts +96 -17
- package/extensions/plan/web-router.ts +109 -47
- package/extensions/plan/web-share-session.ts +191 -0
- package/extensions/plan/web-share.ts +169 -0
- package/extensions/plan/web-streams.ts +124 -0
- package/extensions/plan/web-tokens.ts +63 -0
- package/extensions/plan/web.ts +245 -151
- package/extensions/question/dialogs.ts +91 -1
- package/extensions/question/tools.ts +2 -1
- package/extensions/rewind/{commands.ts → commands/commands.ts} +5 -5
- package/extensions/rewind/{review.ts → commands/review.ts} +13 -13
- 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 +113 -0
- package/extensions/session-bus/format.ts +101 -0
- package/extensions/session-bus/index.ts +75 -0
- package/extensions/session-bus/join-request.ts +40 -0
- package/extensions/session-bus/mail-watch.ts +134 -0
- package/extensions/session-bus/notice.ts +86 -0
- package/extensions/session-bus/overlap.ts +36 -0
- package/extensions/session-bus/protocol.ts +104 -0
- package/extensions/session-bus/resolve.ts +67 -0
- package/extensions/session-bus/result.ts +208 -0
- package/extensions/session-bus/rows.ts +171 -0
- package/extensions/session-bus/runtime.ts +466 -0
- package/extensions/session-bus/store.ts +365 -0
- package/extensions/session-bus/tools.ts +192 -0
- package/extensions/session-bus/tui.ts +394 -0
- package/extensions/session-bus/types.ts +98 -0
- package/extensions/subagent/agents.ts +9 -8
- package/extensions/subagent/invocation.ts +4 -2
- package/extensions/team/commands.ts +112 -0
- package/extensions/team/dispatch.ts +133 -0
- package/extensions/team/format.ts +105 -0
- package/extensions/team/index.ts +33 -0
- package/extensions/team/schema.ts +29 -0
- package/extensions/team/tools.ts +246 -0
- package/extensions/team/types.ts +48 -0
- package/extensions/watch/index.ts +15 -9
- 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 +76 -92
- 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} +66 -51
- package/extensions/wechat/{prompt.ts → dialog/prompt.ts} +40 -25
- package/extensions/wechat/{remote-ui.ts → dialog/remote-ui.ts} +1 -1
- package/extensions/wechat/index.ts +10 -10
- 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} +119 -56
- package/extensions/wechat/{presence.ts → peer/presence.ts} +11 -11
- package/extensions/wechat/{reply-pipeline.ts → peer/reply-pipeline.ts} +13 -5
- 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} +15 -7
- 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} +104 -40
- 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/git.ts +5 -0
- package/extensions/worktree/guard.ts +3 -5
- package/extensions/worktree/registry.ts +11 -5
- package/extensions/worktree/status.ts +1 -3
- package/lib/README.md +12 -6
- package/lib/board/card.ts +290 -0
- package/lib/board/links.ts +117 -0
- package/lib/board/page-pointer.ts +256 -0
- package/lib/board/round-text.ts +72 -0
- package/lib/board/round.ts +89 -0
- package/lib/board/rules.ts +304 -0
- package/lib/board/session.ts +105 -0
- package/lib/board/store.ts +310 -0
- package/lib/board/view.ts +150 -0
- package/lib/errors.ts +12 -4
- package/lib/format.ts +13 -0
- package/lib/http/guard.ts +112 -0
- package/lib/http/index.ts +16 -5
- package/lib/http/router.ts +40 -13
- package/lib/http/serve.ts +234 -0
- package/lib/http/server.ts +29 -13
- package/lib/http/sink.ts +110 -0
- package/lib/http/token.ts +38 -0
- package/lib/interaction.ts +10 -0
- package/lib/notice.ts +29 -2
- package/lib/plan-entry.ts +7 -2
- package/lib/policy.ts +12 -2
- package/lib/qr.ts +64 -0
- package/lib/repo-key.ts +74 -0
- package/lib/report.ts +52 -33
- package/lib/requirements.ts +7 -9
- package/lib/session-label.ts +64 -0
- package/lib/session-roster.ts +130 -0
- package/lib/session-tool.ts +184 -0
- package/lib/tool-names.ts +25 -2
- package/lib/tui/README.md +16 -2
- package/lib/tui/dock-screen.ts +85 -17
- package/lib/tui/glyphs.ts +2 -0
- package/lib/tui/line-pane.ts +15 -4
- package/lib/tui/tool.ts +23 -4
- package/lib/web/README.md +5 -2
- package/lib/web/components.ts +6 -1
- package/lib/web/runtime.ts +7 -1
- package/lib/web/tokens.ts +25 -2
- package/package.json +27 -14
- package/skills/init/SKILL.md +80 -45
- package/skills/init/references/agents-md-quality.md +26 -2
- package/skills/init/references/interview-questions.md +36 -14
- package/skills/next/SKILL.md +77 -51
- 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/file-browse/README.md +0 -467
- package/extensions/file-browse/server.ts +0 -134
- package/extensions/plan/README.md +0 -709
- package/extensions/plan/config.ts +0 -27
- package/extensions/wechat/outbound.ts +0 -100
- package/extensions/wechat/qrcode.ts +0 -46
- package/skills/changelog/SKILL.md +0 -148
- /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/{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,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The board page's stylesheet: the shared Bytefly shell and components, then the
|
|
3
|
+
* page-only `board-*` layout rules.
|
|
4
|
+
*
|
|
5
|
+
* Layout only — every colour, size step, and radius is a kit token, so the page
|
|
6
|
+
* follows the theme and `lib/web/web.test.ts`'s "draws every colour through a token"
|
|
7
|
+
* rule holds without an exception here. Colour is a role: a lane's dot, its head,
|
|
8
|
+
* and the reason line read `--muted`, `--accent`, `--danger`, and `--success`
|
|
9
|
+
* through `data-status`, never a literal.
|
|
10
|
+
*
|
|
11
|
+
* The sheet is in three parts, in the order the page stacks: the toolbar, the
|
|
12
|
+
* board (lanes, cards, and the Team tables), then its dialogs.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { WEB_BASE_CSS, WEB_COMPONENTS_CSS, WEB_LAYOUT_CSS, WEB_THEME_CSS } from "../../../lib/web/index.ts";
|
|
16
|
+
|
|
17
|
+
/** The shared shell, the components, and the board's own layout. */
|
|
18
|
+
export const BOARD_CSS = `${WEB_BASE_CSS}
|
|
19
|
+
${WEB_THEME_CSS}
|
|
20
|
+
${WEB_LAYOUT_CSS}
|
|
21
|
+
${WEB_COMPONENTS_CSS}
|
|
22
|
+
|
|
23
|
+
/* Toolbar: identity and state on the left, the controls the reader uses on the right. */
|
|
24
|
+
.board-toolbar{gap:var(--space-3);flex-wrap:wrap}
|
|
25
|
+
.board-status{margin:0}
|
|
26
|
+
/* The right side of the toolbar is one group, so a wrap happens between groups
|
|
27
|
+
rather than between two controls that belong together. */
|
|
28
|
+
.board-controls{display:flex;align-items:center;gap:var(--space-3);flex-wrap:wrap;justify-content:flex-end}
|
|
29
|
+
.board-filter{width:13rem}
|
|
30
|
+
.board-progress{margin:0}
|
|
31
|
+
.board-picker{display:none;margin-bottom:var(--space-3)}
|
|
32
|
+
|
|
33
|
+
/* The new card is a kit dialog, so the board is what the page opens on. The form is
|
|
34
|
+
the panel's own column: its body scrolls and its actions stay pinned. */
|
|
35
|
+
.board-composer{display:flex;flex-direction:column;min-height:0}
|
|
36
|
+
/* One column, label above the box: the dialog is 640px wide, and the 2fr/1fr pair it
|
|
37
|
+
inherited from the drawer left the path hints 197px and their label inside the box. */
|
|
38
|
+
.board-composer__fields{display:flex;flex-direction:column;gap:var(--space-3)}
|
|
39
|
+
.board-composer__field{display:flex;flex-direction:column;gap:var(--space-1);min-width:0}
|
|
40
|
+
.board-composer__label{display:flex;align-items:baseline;justify-content:space-between;gap:var(--space-2);font:var(--fw-medium) var(--fs-xs) var(--font-ui);color:var(--muted)}
|
|
41
|
+
.board-composer__count{font:var(--fs-xs) var(--font-mono);color:var(--dim)}
|
|
42
|
+
/* A select inside a field: the native caret is the affordance, so it keeps it, and the
|
|
43
|
+
field's own surface is what it sits on rather than the platform's white box. */
|
|
44
|
+
.board-composer__select{background:transparent;color:var(--text);cursor:pointer}
|
|
45
|
+
.board-composer__fields .bf-field{min-width:0}
|
|
46
|
+
/* The title is the one thing this dialog exists to ask for, so it leads. */
|
|
47
|
+
.board-composer__field--title .bf-field{padding:var(--space-2) var(--space-3)}
|
|
48
|
+
.board-composer__field--title .bf-input{font-size:var(--fs-md)}
|
|
49
|
+
|
|
50
|
+
/* The main pane is this page's only child of the shell, so it grows to fill the
|
|
51
|
+
column and the footer stays at the bottom however short the board is. */
|
|
52
|
+
.board-main{flex:1 1 auto}
|
|
53
|
+
/* One panel per tab; the shell's main pane is the only scroll box. */
|
|
54
|
+
.board-panel{display:flex;flex-direction:column;min-width:0}
|
|
55
|
+
.board-panel[hidden]{display:none}
|
|
56
|
+
.board-banner{margin:0 0 var(--space-4)}
|
|
57
|
+
.board-no-cards{max-width:32rem;margin:auto;padding:var(--space-8) var(--space-6)}
|
|
58
|
+
|
|
59
|
+
/* Lanes: four trays of cards, each with a head that sticks while the pane scrolls. */
|
|
60
|
+
.board-lanes{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:var(--space-4);align-items:start}
|
|
61
|
+
.board-lane{display:flex;flex-direction:column;gap:var(--space-2);min-width:0;padding:0 var(--space-2) var(--space-3);border:1px solid var(--border);border-radius:var(--radius-lg);background:var(--surface)}
|
|
62
|
+
.board-lane__head{position:sticky;top:0;z-index:1;display:flex;align-items:center;gap:var(--space-2);margin:0 calc(-1 * var(--space-2)) var(--space-1);padding:var(--space-2) var(--space-2);background:var(--surface);border-bottom:1px solid var(--border);border-radius:var(--radius-lg) var(--radius-lg) 0 0;font:var(--fw-semibold) var(--fs-xs) var(--font-ui);letter-spacing:.06em;text-transform:uppercase;color:var(--muted)}
|
|
63
|
+
.board-lane__toggle{appearance:none;display:flex;align-items:center;gap:var(--space-2);border:0;background:none;padding:0;font:inherit;letter-spacing:inherit;text-transform:inherit;color:inherit;cursor:pointer}
|
|
64
|
+
.board-lane__toggle:hover{color:var(--accent)}
|
|
65
|
+
.board-lane__toggle::after{content:"▾";margin-left:2px;font-size:var(--fs-sm);color:var(--muted)}
|
|
66
|
+
.board-lane__toggle:hover::after{color:var(--accent)}
|
|
67
|
+
.board-lane__toggle[aria-expanded="false"]::after{content:"▸"}
|
|
68
|
+
.board-lane__count{font-variant-numeric:tabular-nums;color:var(--muted);background:var(--canvas);border-radius:var(--radius-pill);padding:0 var(--space-2);font-size:var(--fs-xs);letter-spacing:0}
|
|
69
|
+
.board-lane__dot{width:var(--space-2);height:var(--space-2);border-radius:var(--radius-pill);background:var(--muted)}
|
|
70
|
+
.board-lane[data-status="doing"] .board-lane__dot{background:var(--accent)}
|
|
71
|
+
.board-lane[data-status="blocked"] .board-lane__dot{background:var(--danger)}
|
|
72
|
+
.board-lane[data-status="done"]{background:var(--canvas)}
|
|
73
|
+
.board-lane[data-status="done"] .board-lane__dot{background:var(--success)}
|
|
74
|
+
.board-lane__list{display:flex;flex-direction:column;gap:var(--space-3);margin:0;padding:0;list-style:none}
|
|
75
|
+
.board-lane__empty{margin:0;font:var(--fs-sm) var(--font-ui);color:var(--dim)}
|
|
76
|
+
|
|
77
|
+
/* A card: the title, then the facts a reader needs to act on it. Its lane says
|
|
78
|
+
which state it is in, so the card itself carries no second colour bar. */
|
|
79
|
+
.board-card{display:flex;flex-direction:column;gap:var(--space-2);padding:var(--space-3);border:1px solid var(--border);border-radius:var(--radius-md);background:var(--canvas);transition:border-color var(--motion-fast) var(--motion-ease),transform var(--motion-fast) var(--motion-ease)}
|
|
80
|
+
.board-card:hover{border-color:var(--border-strong);transform:translateY(-1px)}
|
|
81
|
+
.board-card[hidden]{display:none}
|
|
82
|
+
.board-card__head{display:flex;align-items:baseline;gap:var(--space-2);margin:0;min-width:0}
|
|
83
|
+
.board-card__id{font:var(--fs-xs) var(--font-mono);color:var(--dim)}
|
|
84
|
+
.board-card__title{margin:0;padding:0;border:0;background:none;text-align:left;cursor:pointer;font:var(--fw-semibold) var(--fs-base)/var(--lh-snug) var(--font-ui);color:var(--text);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
85
|
+
.board-card__title:hover{color:var(--accent)}
|
|
86
|
+
/* Finished work recedes: it is history, and the live lanes should not compete with it. */
|
|
87
|
+
.board-card[data-status="done"] .board-card__title{color:var(--muted)}
|
|
88
|
+
.board-card[data-status="done"] .board-card__title:hover{color:var(--accent)}
|
|
89
|
+
.board-card[data-status="done"] .bf-chip--accent{color:var(--muted);border-color:var(--border)}
|
|
90
|
+
.board-card__meta{display:flex;flex-wrap:wrap;align-items:center;gap:var(--space-2);margin:0;font:var(--fs-xs) var(--font-ui);color:var(--muted)}
|
|
91
|
+
.board-card__paths{display:flex;flex-wrap:wrap;gap:var(--space-1);margin:0;padding:0;list-style:none}
|
|
92
|
+
.board-card__reason{margin:0;font:var(--fs-xs) var(--font-ui);color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
93
|
+
.board-card__reason--blocked{color:var(--danger)}
|
|
94
|
+
.board-card__reason--unplanned{color:var(--warn)}
|
|
95
|
+
.board-card__actions{display:flex;flex-wrap:wrap;gap:var(--space-2);margin-top:var(--space-1)}
|
|
96
|
+
|
|
97
|
+
/* The Team tab: a queue and a roster side by side, then one quiet strip. */
|
|
98
|
+
.board-team{display:grid;gap:var(--space-5);min-width:0}
|
|
99
|
+
.board-team__grid{display:grid;grid-template-columns:minmax(0,1.55fr) minmax(18rem,1fr);gap:var(--space-6);align-items:start;min-width:0}
|
|
100
|
+
/* Each panel is its own grid, so both captions land on one line and both lists start on the next. */
|
|
101
|
+
.board-team__group{display:grid;grid-template-rows:auto 1fr;gap:var(--space-3);align-content:start;min-width:0}
|
|
102
|
+
.board-team__caption{margin:0;padding-bottom:var(--space-2);border-bottom:1px solid var(--border);font:var(--fw-semibold) var(--fs-md) var(--font-ui);color:var(--text);display:flex;align-items:center;gap:var(--space-2)}
|
|
103
|
+
.board-team__caption .bf-chip{margin-left:auto;font-variant-numeric:tabular-nums}
|
|
104
|
+
/* The queue: one row per card, its kind as an edge, its way out in the foot. */
|
|
105
|
+
.board-team__queue{display:flex;flex-direction:column;gap:var(--space-2);min-width:0}
|
|
106
|
+
/* The queue is the tallest thing here and it can be unbounded; capping it keeps the two
|
|
107
|
+
panels on one screen. Stacked below 1180px there is no second column to balance, and a
|
|
108
|
+
nested scroll on a phone is worse than the page's own. */
|
|
109
|
+
@media (min-width:1181px){
|
|
110
|
+
/* A row cut at the edge reads as a bug unless something says the list continues, and a
|
|
111
|
+
mac's scrollbars are hidden until it moves: an inset shade in the page's own colour
|
|
112
|
+
marks the edge without a literal. */
|
|
113
|
+
/* A floor, because a short window must not turn three rows into a scroll box. */
|
|
114
|
+
.board-team__queue{max-height:max(18rem,calc(100dvh - 16rem));overflow:auto;overscroll-behavior:contain;padding-right:var(--space-1);box-shadow:inset 0 calc(-1 * var(--space-6)) var(--space-6) calc(-1 * var(--space-6)) var(--canvas)}
|
|
115
|
+
}
|
|
116
|
+
.board-team__decision{display:grid;grid-template-columns:minmax(0,1fr) auto;grid-template-areas:"open foot" "why why";align-items:center;gap:var(--space-1) var(--space-3);padding:var(--space-2) var(--space-3);background:var(--raised);border:1px solid var(--border);border-left:3px solid var(--dim);border-radius:var(--radius-md);transition:border-color var(--motion-base) var(--motion-ease),transform var(--motion-base) var(--motion-ease)}
|
|
117
|
+
.board-team__decision:hover{border-color:var(--border-strong);transform:translateY(-1px)}
|
|
118
|
+
.board-team__decision[data-kind="blocked"]{border-left-color:var(--danger)}
|
|
119
|
+
.board-team__decision[data-kind="idle"]{border-left-color:var(--warn)}
|
|
120
|
+
/* Nobody holds it, so the edge is drawn but not filled: a shape, not a claim. */
|
|
121
|
+
.board-team__decision[data-kind="unassigned"]{border-left-style:dashed}
|
|
122
|
+
.board-team__open{grid-area:open;display:flex;align-items:baseline;gap:var(--space-2);appearance:none;border:0;background:none;padding:0;margin:0;text-align:left;cursor:pointer;color:var(--text);font:var(--fw-medium) var(--fs-md) var(--font-ui);min-width:0}
|
|
123
|
+
.board-team__open:hover .board-team__title{color:var(--accent)}
|
|
124
|
+
.board-team__ref{font:var(--fs-xs) var(--font-mono);color:var(--muted);flex:0 0 auto}
|
|
125
|
+
.board-team__title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
|
|
126
|
+
/* Two lines, then a tooltip: the "why" is the reason the row is on this tab. */
|
|
127
|
+
.board-team__why{grid-area:why;margin:0;color:var(--muted);font:var(--fs-sm)/var(--lh-snug) var(--font-ui);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;overflow-wrap:anywhere}
|
|
128
|
+
.board-team__why--blocked{color:var(--danger)}
|
|
129
|
+
.board-team__foot{grid-area:foot;display:flex;align-items:center;justify-content:flex-end;gap:var(--space-2)}
|
|
130
|
+
/* A row with no owner chip still puts its one action on the row's edge. */
|
|
131
|
+
.board-team__foot>.bf-btn:only-child{margin-left:auto}
|
|
132
|
+
/* The roster: who is here and what they hold. No hover fill — nothing to open. */
|
|
133
|
+
.board-team__roster{display:flex;flex-direction:column;gap:var(--space-1);margin:0;padding:0;list-style:none;min-width:0}
|
|
134
|
+
/* No row rule: the caption already delimits the list, and a second hairline beside it
|
|
135
|
+
was the same pen drawn twice. */
|
|
136
|
+
.board-team__mate{display:flex;flex-wrap:wrap;align-items:baseline;gap:var(--space-1) var(--space-2);padding:var(--space-2) 0;min-width:0}
|
|
137
|
+
.board-team__dot{width:8px;height:8px;border-radius:var(--radius-pill);background:var(--muted);flex:0 0 auto;align-self:center}
|
|
138
|
+
.board-team__mate[data-state="live"] .board-team__dot{background:var(--success)}
|
|
139
|
+
.board-team__mate[data-state="quiet"] .board-team__dot{background:var(--warn)}
|
|
140
|
+
/* A record that is not there is a hollow ring, never a colour that claims it is. */
|
|
141
|
+
.board-team__mate[data-state="absent"] .board-team__dot,.board-team__mate[data-state="unknown"] .board-team__dot{background:transparent;box-shadow:inset 0 0 0 1px var(--border-strong)}
|
|
142
|
+
.board-team__name{font:var(--fw-medium) var(--fs-md) var(--font-ui);color:var(--text)}
|
|
143
|
+
.board-team__mets{flex:1 0 100%;margin:0;color:var(--muted);font:var(--fs-xs) var(--font-ui)}
|
|
144
|
+
.board-team__empty{margin:0;color:var(--muted);font:var(--fs-sm) var(--font-ui)}
|
|
145
|
+
/* Done stays quiet; its ids are buttons only because they open their card. */
|
|
146
|
+
.board-team__done,.board-done{margin:0;font:var(--fs-sm) var(--font-ui);color:var(--muted);display:flex;flex-wrap:wrap;align-items:baseline;gap:var(--space-2)}
|
|
147
|
+
/* An id in the Done strip is a button, so it wears the only affordance it has: a
|
|
148
|
+
dotted line, visible before the pointer arrives. */
|
|
149
|
+
.board-team__id{appearance:none;border:0;background:none;padding:0;margin:0;cursor:pointer;font:var(--fs-sm) var(--font-mono);color:var(--muted);text-decoration:underline dotted;text-underline-offset:3px}
|
|
150
|
+
.board-team__id:hover{color:var(--accent);text-decoration:underline}
|
|
151
|
+
|
|
152
|
+
/* The card dialog: a meta line, the notes as a timeline, and the actions at its foot. */
|
|
153
|
+
.board-dialog__meta,.board-dialog__owner{margin:0 0 var(--space-2);font:var(--fs-sm) var(--font-ui);color:var(--muted)}
|
|
154
|
+
.board-dialog__plan{margin:0 0 var(--space-2);font:var(--fs-xs) var(--font-mono);color:var(--dim);word-break:break-all}
|
|
155
|
+
/* What the card waits for is a fact about its place in the work, not a record of it. */
|
|
156
|
+
.board-dialog__depends{margin:0 0 var(--space-2);font:var(--fs-xs) var(--font-mono);color:var(--muted);word-break:break-all}
|
|
157
|
+
.board-dialog__notes{display:flex;flex-direction:column;gap:var(--space-3);margin:0 0 var(--space-3);padding:0;list-style:none;max-height:16rem;overflow:auto}
|
|
158
|
+
/* A rule between notes: wrapped prose used to run one note into the next. */
|
|
159
|
+
.board-dialog__note{padding-top:var(--space-3);border-top:1px solid var(--border)}
|
|
160
|
+
.board-dialog__note:first-child{padding-top:0;border-top:0}
|
|
161
|
+
.board-dialog__note-head{display:flex;align-items:baseline;justify-content:space-between;gap:var(--space-2);margin:0 0 2px}
|
|
162
|
+
.board-dialog__note-by{font:var(--fw-medium) var(--fs-xs) var(--font-ui);color:var(--text)}
|
|
163
|
+
.board-dialog__note-at{font:var(--fs-xs) var(--font-mono);color:var(--muted)}
|
|
164
|
+
.board-dialog__note-text{margin:0;font:var(--fs-sm)/var(--lh-normal) var(--font-ui);color:var(--text);overflow-wrap:anywhere}
|
|
165
|
+
.board-dialog__move{display:flex;align-items:center;gap:var(--space-2);margin-right:auto}
|
|
166
|
+
/* Assigning records an owner the page cannot tell anybody about, so it sits with the
|
|
167
|
+
other card actions and takes a name from the roster. */
|
|
168
|
+
.board-dialog__assign{display:flex;align-items:center;gap:var(--space-2);margin-right:auto}
|
|
169
|
+
.board-dialog__assign-select{width:auto;min-width:8rem;border:1px solid var(--border);border-radius:var(--radius-md);background:var(--canvas);padding:2px var(--space-2);cursor:pointer}
|
|
170
|
+
.board-dialog__label{font:var(--fs-xs) var(--font-ui);color:var(--muted)}
|
|
171
|
+
.board-dialog__state{display:inline-flex;align-items:center;gap:var(--space-2);font:var(--fs-xs) var(--font-ui);letter-spacing:.06em;text-transform:uppercase;color:var(--muted)}
|
|
172
|
+
.board-dialog__state::before{content:"";width:var(--space-2);height:var(--space-2);border-radius:var(--radius-pill);background:var(--muted)}
|
|
173
|
+
.board-dialog__state[data-status="doing"]::before{background:var(--accent)}
|
|
174
|
+
.board-dialog__state[data-status="blocked"]::before{background:var(--danger)}
|
|
175
|
+
.board-dialog__state[data-status="done"]::before{background:var(--success)}
|
|
176
|
+
.board-dialog__note-composer{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:var(--space-2);padding:0;border:0;background:none}
|
|
177
|
+
.board-dialog__note-composer .bf-field{margin:0}
|
|
178
|
+
.board-dialog__note-composer .bf-composer__row{margin:0}
|
|
179
|
+
.board-confirm__body{margin:0}
|
|
180
|
+
.board-skeleton{height:4.5rem;border:1px dashed var(--border);border-radius:var(--radius-md);background:var(--surface)}
|
|
181
|
+
|
|
182
|
+
@media (max-width:1180px){
|
|
183
|
+
.board-lanes{grid-template-columns:repeat(2,minmax(0,1fr))}
|
|
184
|
+
.board-progress{display:none}
|
|
185
|
+
/* Two panels need a wide window; below this the queue comes first, the roster after. */
|
|
186
|
+
.board-team__grid{grid-template-columns:minmax(0,1fr)}
|
|
187
|
+
}
|
|
188
|
+
@media (max-width:860px){
|
|
189
|
+
.board-lanes{grid-template-columns:minmax(0,1fr)}
|
|
190
|
+
.board-lane[hidden]{display:none}
|
|
191
|
+
.board-picker{display:flex}
|
|
192
|
+
.board-toolbar{gap:var(--space-2)}
|
|
193
|
+
.board-toolbar .bf-spacer{display:none}
|
|
194
|
+
/* The icons stay with the tabs and the filter takes the last row whole, so the
|
|
195
|
+
toolbar wraps once, on purpose, instead of leaving two buttons stranded. */
|
|
196
|
+
.board-controls{width:100%;justify-content:flex-start}
|
|
197
|
+
.board-filter{order:9;width:100%}
|
|
198
|
+
.board-team__foot{flex-wrap:wrap}
|
|
199
|
+
}
|
|
200
|
+
@media (prefers-reduced-motion: reduce){
|
|
201
|
+
.board-card,.board-team__decision{transition:none}
|
|
202
|
+
.board-card:hover,.board-team__decision:hover{transform:none}
|
|
203
|
+
}
|
|
204
|
+
@media print{
|
|
205
|
+
.board-lanes,.board-team__grid{grid-template-columns:minmax(0,1fr)}
|
|
206
|
+
.board-card{break-inside:avoid}
|
|
207
|
+
/* The disclosure affordance goes; the lane's name stays with its count. */
|
|
208
|
+
.board-card__actions,.board-composer,.board-filter,.board-picker,.board-done{display:none!important}
|
|
209
|
+
.board-lane__toggle::after{content:""}
|
|
210
|
+
}
|
|
211
|
+
`;
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Team tab: the lead's dashboard.
|
|
3
|
+
*
|
|
4
|
+
* Two panels and one strip. **Needs a decision** is the queue — one row per card,
|
|
5
|
+
* carrying why it is there and the action that fixes it — and **Teammates** is the
|
|
6
|
+
* people: the sessions this repository's bus knows, plus every owner a card names,
|
|
7
|
+
* so a session holding nothing and a label that stopped answering are both visible.
|
|
8
|
+
* Done is a quiet strip of ids that open their cards.
|
|
9
|
+
*
|
|
10
|
+
* The roster is optional throughout: `view.teammates` is absent when `session-bus`
|
|
11
|
+
* has no records here, and every state then reads `—` rather than claiming the
|
|
12
|
+
* session is gone. A card is the only thing that can make a row a decision, so the
|
|
13
|
+
* first panel is drawn from the round and the second from the round and the roster
|
|
14
|
+
* together.
|
|
15
|
+
*
|
|
16
|
+
* Same rule as the rest of the client: every value arrives as `textContent`.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export const BOARD_TEAM_JS = String.raw`(function () {
|
|
20
|
+
"use strict";
|
|
21
|
+
var board = window.bfBoard || (window.bfBoard = {});
|
|
22
|
+
var el = board.el;
|
|
23
|
+
var chip = board.chip;
|
|
24
|
+
var actionButton = board.actionButton;
|
|
25
|
+
var ago = board.ago;
|
|
26
|
+
var plural = board.plural;
|
|
27
|
+
|
|
28
|
+
/** The word a teammate's state is read as; never a colour alone. */
|
|
29
|
+
var STATE_WORD = { live: "live", quiet: "quiet", absent: "no record", unknown: "\u2014" };
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A panel: its heading row, then its body.
|
|
33
|
+
*
|
|
34
|
+
* With a filter on, the count reads shown/total the way a lane head does, so a
|
|
35
|
+
* reader can tell "nothing here" from "nothing matches".
|
|
36
|
+
*/
|
|
37
|
+
function panel(text, count, total) {
|
|
38
|
+
var node = el("section", "board-team__group");
|
|
39
|
+
node.setAttribute("aria-label", text);
|
|
40
|
+
var caption = el("h2", "board-team__caption", text);
|
|
41
|
+
caption.appendChild(chip(total !== undefined && total !== count ? count + "/" + total : String(count)));
|
|
42
|
+
node.appendChild(caption);
|
|
43
|
+
return node;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Which cards need the lead, and why — entries, not rows.
|
|
48
|
+
*
|
|
49
|
+
* The renderer filters this list, so it has to be the data: filtering the built
|
|
50
|
+
* rows would test the element instead of the card.
|
|
51
|
+
*
|
|
52
|
+
* The round's groups overlap on purpose — a doing card untouched for half an hour
|
|
53
|
+
* is both idle and unplanned — so a card can arrive twice. On the page one card is
|
|
54
|
+
* one row, and the strongest reason is the one it keeps: a blocker outranks silence,
|
|
55
|
+
* which outranks a missing plan, which outranks nobody holding it.
|
|
56
|
+
*/
|
|
57
|
+
var KIND_RANK = { blocked: 0, waiting: 1, idle: 2, unplanned: 3, unassigned: 4 };
|
|
58
|
+
|
|
59
|
+
function needs(view) {
|
|
60
|
+
var round = view.round;
|
|
61
|
+
var byCard = {};
|
|
62
|
+
var entries = [];
|
|
63
|
+
function add(cards, why, kind) {
|
|
64
|
+
for (var i = 0; i < cards.length; i++) {
|
|
65
|
+
var card = cards[i];
|
|
66
|
+
var entry = { card: card, why: why(card), kind: kind };
|
|
67
|
+
var seen = byCard[card.id];
|
|
68
|
+
if (seen === undefined) {
|
|
69
|
+
byCard[card.id] = entry;
|
|
70
|
+
entries.push(entry);
|
|
71
|
+
} else if (KIND_RANK[kind] < KIND_RANK[seen.kind]) {
|
|
72
|
+
seen.why = entry.why;
|
|
73
|
+
seen.kind = kind;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
add(round.blocked, function (card) {
|
|
78
|
+
var last = card.notes.length > 0 ? card.notes[card.notes.length - 1] : undefined;
|
|
79
|
+
return last ? last.text : "blocked";
|
|
80
|
+
}, "blocked");
|
|
81
|
+
add(round.idle, function (card) { return "no touch for " + ago(card.updatedAt); }, "idle");
|
|
82
|
+
// The board card says the same words for the same fact: one voice for one state.
|
|
83
|
+
add(round.unplanned, function () { return "no plan recorded"; }, "unplanned");
|
|
84
|
+
add(round.unassigned, function () { return "nobody holds it"; }, "unassigned");
|
|
85
|
+
// An order outranks nobody holding it: the claim would be refused, so the queue says
|
|
86
|
+
// what it waits for instead of offering a button the rules would turn down.
|
|
87
|
+
add(
|
|
88
|
+
round.unassigned.filter(function (card) { return board.blockersOf(card, view).length > 0; }),
|
|
89
|
+
function (card) { return "waits on " + board.waitingText(card, view); },
|
|
90
|
+
"waiting",
|
|
91
|
+
);
|
|
92
|
+
return entries;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** One decision: the card, why it is here, who holds it, and the way out. */
|
|
96
|
+
function decision(entry, view) {
|
|
97
|
+
var card = entry.card;
|
|
98
|
+
var node = el("article", "board-team__decision");
|
|
99
|
+
node.setAttribute("data-kind", entry.kind);
|
|
100
|
+
|
|
101
|
+
var open = el("button", "board-team__open");
|
|
102
|
+
open.type = "button";
|
|
103
|
+
open.setAttribute("aria-haspopup", "dialog");
|
|
104
|
+
open.appendChild(el("span", "board-team__ref", "#" + card.id));
|
|
105
|
+
var title = el("span", "board-team__title", card.title);
|
|
106
|
+
// A clamped title has no other way to be read, so it carries its own tooltip.
|
|
107
|
+
title.title = card.title;
|
|
108
|
+
open.appendChild(title);
|
|
109
|
+
open.addEventListener("click", function () { board.openCard(card.id); });
|
|
110
|
+
node.appendChild(open);
|
|
111
|
+
|
|
112
|
+
var foot = el("div", "board-team__foot");
|
|
113
|
+
if (card.owner) foot.appendChild(chip(card.owner.label, "accent"));
|
|
114
|
+
// The same permission table the card face and the dialog ask: a row offers the way out
|
|
115
|
+
// that this reader may actually take, and a card that waits offers none because a
|
|
116
|
+
// claim on it would be refused.
|
|
117
|
+
if (board.may(card, view, "reclaim")) {
|
|
118
|
+
var reclaim = actionButton("Reclaim", function () { board.act("reclaim", card); });
|
|
119
|
+
// Six identical buttons read as six identical buttons; the card is the name.
|
|
120
|
+
reclaim.setAttribute("aria-label", "Reclaim #" + card.id);
|
|
121
|
+
foot.appendChild(reclaim);
|
|
122
|
+
} else if (board.may(card, view, "claim")) {
|
|
123
|
+
var claim = actionButton("Claim", function () { board.act("claim", card); });
|
|
124
|
+
claim.setAttribute("aria-label", "Claim #" + card.id);
|
|
125
|
+
foot.appendChild(claim);
|
|
126
|
+
}
|
|
127
|
+
node.appendChild(foot);
|
|
128
|
+
|
|
129
|
+
// The reason gets the row's whole width and two lines; the tooltip carries the
|
|
130
|
+
// rest, because this sentence is the lead's whole reason to look at the row.
|
|
131
|
+
var why = el("p", "board-team__why", entry.why);
|
|
132
|
+
why.title = entry.why;
|
|
133
|
+
if (entry.kind === "blocked") why.className = "board-team__why board-team__why--blocked";
|
|
134
|
+
node.appendChild(why);
|
|
135
|
+
return node;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Every teammate: the bus's sessions and the owners a card names, as one list.
|
|
140
|
+
*
|
|
141
|
+
* An owner with no live session is the interesting row — work is held by a label
|
|
142
|
+
* nothing answers to — so it stays, with "no record" where a heartbeat would be.
|
|
143
|
+
*/
|
|
144
|
+
function teammates(view, roster) {
|
|
145
|
+
var round = view.round;
|
|
146
|
+
var byLabel = {};
|
|
147
|
+
var rows = [];
|
|
148
|
+
function labelOf(label) { return label.toLowerCase(); }
|
|
149
|
+
|
|
150
|
+
for (var i = 0; i < round.owners.length; i++) {
|
|
151
|
+
var group = round.owners[i];
|
|
152
|
+
byLabel[labelOf(group.label)] = { label: group.label, live: group.cards, done: group.done };
|
|
153
|
+
}
|
|
154
|
+
var seen = {};
|
|
155
|
+
if (roster !== null) {
|
|
156
|
+
for (var j = 0; j < roster.length; j++) {
|
|
157
|
+
var entry = roster[j];
|
|
158
|
+
seen[labelOf(entry.label)] = true;
|
|
159
|
+
rows.push({
|
|
160
|
+
label: entry.label,
|
|
161
|
+
state: entry.state,
|
|
162
|
+
seen: entry.seen,
|
|
163
|
+
mail: entry.mail,
|
|
164
|
+
listening: entry.listening,
|
|
165
|
+
you: entry.you,
|
|
166
|
+
cards: byLabel[labelOf(entry.label)] || { live: [], done: [] },
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
for (var key in byLabel) {
|
|
171
|
+
if (seen[key]) continue;
|
|
172
|
+
rows.push({ label: byLabel[key].label, state: roster === null ? "unknown" : "absent", seen: undefined, mail: 0, listening: undefined, you: false, cards: byLabel[key] });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function rank(mate) {
|
|
176
|
+
if (mate.you) return -1;
|
|
177
|
+
if (mate.state === "live") return 0;
|
|
178
|
+
if (mate.state === "quiet") return 1;
|
|
179
|
+
if (mate.state === "absent") return 2;
|
|
180
|
+
return 3;
|
|
181
|
+
}
|
|
182
|
+
rows.sort(function (a, b) { return rank(a) - rank(b) || a.label.localeCompare(b.label); });
|
|
183
|
+
return rows;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** A text node carrying its stamp, so refreshAges rewrites it with every other age. */
|
|
187
|
+
function stamped(text, at) {
|
|
188
|
+
var node = el("span", "", text);
|
|
189
|
+
node.setAttribute("data-at", String(at));
|
|
190
|
+
return node;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** One teammate: who, how they are, and what they hold. Not a button — nothing to open. */
|
|
194
|
+
function mate(mate) {
|
|
195
|
+
var node = el("li", "board-team__mate");
|
|
196
|
+
node.setAttribute("data-state", mate.state);
|
|
197
|
+
if (mate.you) node.setAttribute("data-you", "1");
|
|
198
|
+
|
|
199
|
+
var dot = el("span", "board-team__dot");
|
|
200
|
+
dot.setAttribute("aria-hidden", "true");
|
|
201
|
+
node.appendChild(dot);
|
|
202
|
+
node.appendChild(el("span", "board-team__name", mate.label));
|
|
203
|
+
if (mate.you) node.appendChild(chip("you"));
|
|
204
|
+
// A live peer whose watch is off will read a handoff at its next turn instead of being
|
|
205
|
+
// woken for it, which is the one thing a lead can still act on.
|
|
206
|
+
if (mate.state === "live" && mate.listening === false) {
|
|
207
|
+
var silent = chip("not listening");
|
|
208
|
+
silent.title = "Mail waits for this session's next turn; it is not woken for it.";
|
|
209
|
+
node.appendChild(silent);
|
|
210
|
+
}
|
|
211
|
+
if (mate.mail > 0) node.appendChild(chip(plural(mate.mail, "mail"), "accent"));
|
|
212
|
+
|
|
213
|
+
// "quiet" alone does not tell a lead whether to reclaim; the heartbeat age does.
|
|
214
|
+
var mets = el("p", "board-team__mets");
|
|
215
|
+
if (mate.state === "quiet" && mate.seen !== undefined) {
|
|
216
|
+
mets.appendChild(document.createTextNode("quiet for "));
|
|
217
|
+
mets.appendChild(stamped(ago(mate.seen), mate.seen));
|
|
218
|
+
} else {
|
|
219
|
+
mets.appendChild(document.createTextNode(STATE_WORD[mate.state] || mate.state));
|
|
220
|
+
}
|
|
221
|
+
var held = mate.cards.live.length;
|
|
222
|
+
var done = mate.cards.done.length;
|
|
223
|
+
mets.appendChild(document.createTextNode(" \u00B7 " + (held === 0 ? "no cards" : held + " held")));
|
|
224
|
+
if (done > 0) mets.appendChild(document.createTextNode(" \u00B7 " + done + " done"));
|
|
225
|
+
if (held > 0) {
|
|
226
|
+
var at = mate.cards.live[0].updatedAt;
|
|
227
|
+
for (var i = 1; i < mate.cards.live.length; i++) at = Math.min(at, mate.cards.live[i].updatedAt);
|
|
228
|
+
mets.appendChild(document.createTextNode(" \u00B7 oldest "));
|
|
229
|
+
mets.appendChild(stamped(ago(at), at));
|
|
230
|
+
}
|
|
231
|
+
node.appendChild(mets);
|
|
232
|
+
return node;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
board.renderTeam = function (view) {
|
|
236
|
+
var root = document.getElementById("board-team");
|
|
237
|
+
if (!root) return;
|
|
238
|
+
root.textContent = "";
|
|
239
|
+
var round = view.round;
|
|
240
|
+
// Absent, not empty: null means no bus here, so every state reads unknown.
|
|
241
|
+
var roster = view.teammates === undefined ? null : view.teammates;
|
|
242
|
+
// The toolbar's filter is one control for both tabs, so the round reads it too.
|
|
243
|
+
var query = board.query || "";
|
|
244
|
+
var keep = function (card) { return board.matches(card, query); };
|
|
245
|
+
var matchesLabel = function (label) { return label.toLowerCase().indexOf(query.toLowerCase()) !== -1; };
|
|
246
|
+
var filtering = query !== "";
|
|
247
|
+
var noMatch = "no card here matches \"" + query + "\"";
|
|
248
|
+
|
|
249
|
+
var grid = el("div", "board-team__grid");
|
|
250
|
+
|
|
251
|
+
var all = needs(view);
|
|
252
|
+
var decisions = all.filter(function (entry) { return keep(entry.card); });
|
|
253
|
+
var queue = panel("Needs a decision", decisions.length, all.length);
|
|
254
|
+
if (decisions.length === 0) {
|
|
255
|
+
queue.appendChild(el("p", "board-team__empty", filtering ? noMatch : "nothing needs you: no blockers, nothing idle, every card planned and held"));
|
|
256
|
+
} else {
|
|
257
|
+
var list = el("div", "board-team__queue");
|
|
258
|
+
for (var i = 0; i < decisions.length; i++) list.appendChild(decision(decisions[i], view));
|
|
259
|
+
queue.appendChild(list);
|
|
260
|
+
}
|
|
261
|
+
grid.appendChild(queue);
|
|
262
|
+
|
|
263
|
+
var everyone = teammates(view, roster);
|
|
264
|
+
var shown = everyone.filter(function (m) {
|
|
265
|
+
if (!filtering) return true;
|
|
266
|
+
if (matchesLabel(m.label)) return true;
|
|
267
|
+
var held = m.cards.live.concat(m.cards.done);
|
|
268
|
+
for (var k = 0; k < held.length; k++) if (keep(held[k])) return true;
|
|
269
|
+
return false;
|
|
270
|
+
});
|
|
271
|
+
var people = panel("Teammates", shown.length, everyone.length);
|
|
272
|
+
if (shown.length === 0) {
|
|
273
|
+
people.appendChild(el("p", "board-team__empty", filtering ? noMatch : roster === null ? "session-bus is not loaded here" : "nobody is here yet"));
|
|
274
|
+
} else {
|
|
275
|
+
var rosterList = el("ul", "board-team__roster");
|
|
276
|
+
for (var j = 0; j < shown.length; j++) rosterList.appendChild(mate(shown[j]));
|
|
277
|
+
people.appendChild(rosterList);
|
|
278
|
+
}
|
|
279
|
+
grid.appendChild(people);
|
|
280
|
+
root.appendChild(grid);
|
|
281
|
+
|
|
282
|
+
var done = round.done.filter(keep);
|
|
283
|
+
var strip = el("p", "board-team__done");
|
|
284
|
+
if (done.length === 0) {
|
|
285
|
+
// While filtering, a third "no card here matches" line adds nothing; the two
|
|
286
|
+
// panels above already said it.
|
|
287
|
+
if (filtering) return root;
|
|
288
|
+
strip.textContent = "nothing finished yet";
|
|
289
|
+
} else {
|
|
290
|
+
strip.appendChild(document.createTextNode("Done \u00B7 "));
|
|
291
|
+
for (var m = 0; m < done.length; m++) {
|
|
292
|
+
(function (card) {
|
|
293
|
+
var id = el("button", "board-team__id", "#" + card.id);
|
|
294
|
+
id.type = "button";
|
|
295
|
+
id.setAttribute("aria-haspopup", "dialog");
|
|
296
|
+
id.addEventListener("click", function () { board.openCard(card.id); });
|
|
297
|
+
strip.appendChild(id);
|
|
298
|
+
})(done[m]);
|
|
299
|
+
}
|
|
300
|
+
strip.appendChild(document.createTextNode(" \u00B7 " + plural(done.length, "card") +
|
|
301
|
+
(filtering && done.length !== round.done.length ? " of " + round.done.length : "")));
|
|
302
|
+
}
|
|
303
|
+
root.appendChild(strip);
|
|
304
|
+
};
|
|
305
|
+
})();`;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The page's write path: one request from a browser, one call into the store.
|
|
3
|
+
*
|
|
4
|
+
* A browser is not an identity, so every write here is made as the actor the page
|
|
5
|
+
* serves as — the session that started it, until a page has a seat. Two statuses matter
|
|
6
|
+
* to a client that retries: a field it can fix is 422, and a card somebody else moved is
|
|
7
|
+
* 409 with the card as it now stands. Everything else about a card's lifecycle is
|
|
8
|
+
* `lib/board/rules.ts`.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { normalizeCardId } from "../../lib/board/card.ts";
|
|
12
|
+
import type { Actor, CardOp } from "../../lib/board/rules.ts";
|
|
13
|
+
import type { BoardStore } from "../../lib/board/store.ts";
|
|
14
|
+
import { normalizeLabel } from "../../lib/session-label.ts";
|
|
15
|
+
import { createInput } from "./input.ts";
|
|
16
|
+
import type { PageResult, PageWrite } from "./transport/router.ts";
|
|
17
|
+
|
|
18
|
+
/** What a write needs: the store, who it is made as, and the clock. */
|
|
19
|
+
export interface WriteContext {
|
|
20
|
+
store: BoardStore;
|
|
21
|
+
actor: () => Actor;
|
|
22
|
+
now: () => number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** One write, with the status the page shows as a banner. */
|
|
26
|
+
export async function writeOf(context: WriteContext, request: PageWrite): Promise<PageResult> {
|
|
27
|
+
const me = context.actor();
|
|
28
|
+
if (request.kind === "add") {
|
|
29
|
+
const parsed = createInput(request.body);
|
|
30
|
+
if ("error" in parsed) return { ok: false, status: 422, error: parsed.error };
|
|
31
|
+
const card = await context.store.create(parsed.input, me, context.now());
|
|
32
|
+
return { ok: true, card };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const id = normalizeCardId(request.id);
|
|
36
|
+
if (id === undefined) return { ok: false, status: 422, error: `"${request.id}" is not a card id.` };
|
|
37
|
+
const op = opOf(request);
|
|
38
|
+
if ("error" in op) return { ok: false, status: 422, error: op.error };
|
|
39
|
+
const result = await context.store.mutate(id, op.op, me, context.now());
|
|
40
|
+
if (!result.ok)
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
status: 409,
|
|
44
|
+
error: result.error,
|
|
45
|
+
...(result.card === undefined ? {} : { card: result.card }),
|
|
46
|
+
};
|
|
47
|
+
return { ok: true, card: result.card };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The operation a card route names, or the reason it cannot be one. */
|
|
51
|
+
export function opOf(request: PageWrite): { op: CardOp } | { error: string } {
|
|
52
|
+
switch (request.kind) {
|
|
53
|
+
case "claim":
|
|
54
|
+
return { op: { kind: "claim" } };
|
|
55
|
+
case "release":
|
|
56
|
+
return { op: { kind: "release" } };
|
|
57
|
+
case "reclaim":
|
|
58
|
+
return { op: { kind: "reclaim" } };
|
|
59
|
+
case "assign": {
|
|
60
|
+
// A page cannot message a peer — a command context has no tool calls — so this
|
|
61
|
+
// records the owner and says in the card's history that nothing was sent.
|
|
62
|
+
const to = normalizeLabel(typeof request.body.to === "string" ? request.body.to : "", "");
|
|
63
|
+
if (to === "") return { error: "assign needs a teammate to hand the card to." };
|
|
64
|
+
return {
|
|
65
|
+
op: {
|
|
66
|
+
kind: "assign",
|
|
67
|
+
to: { label: to },
|
|
68
|
+
note: `assigned to ${to} from the board page; no handoff was sent`,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
case "drop":
|
|
73
|
+
return { op: { kind: "drop" } };
|
|
74
|
+
case "note":
|
|
75
|
+
return { op: { kind: "note", text: request.body.text } };
|
|
76
|
+
case "plan":
|
|
77
|
+
return { op: { kind: "plan", path: request.body.path } };
|
|
78
|
+
case "edit":
|
|
79
|
+
// Every field is passed through as the browser sent it, including the absent
|
|
80
|
+
// ones: `applyOp` leaves an absent field alone, which is what keeps the page
|
|
81
|
+
// from clearing a link it never showed.
|
|
82
|
+
return {
|
|
83
|
+
op: {
|
|
84
|
+
kind: "edit",
|
|
85
|
+
title: request.body.title,
|
|
86
|
+
paths: request.body.paths,
|
|
87
|
+
priority: request.body.priority,
|
|
88
|
+
dependsOn: request.body.dependsOn,
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
case "move": {
|
|
92
|
+
const status = request.body.status;
|
|
93
|
+
if (status === "doing" || status === "blocked" || status === "done") return { op: { kind: "move", status } };
|
|
94
|
+
return { error: 'move needs a target lane: "doing", "blocked", or "done".' };
|
|
95
|
+
}
|
|
96
|
+
default:
|
|
97
|
+
return { error: "add is not a card route." };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -21,7 +21,7 @@ export interface NotifyConfig {
|
|
|
21
21
|
needsInput: boolean;
|
|
22
22
|
/** Notify when a background `job` finishes. */
|
|
23
23
|
jobFinished: boolean;
|
|
24
|
-
/** Notify when a `watch` wake match fires
|
|
24
|
+
/** Notify when a `watch` wake match fires, once the agent is idle. */
|
|
25
25
|
matchFound: boolean;
|
|
26
26
|
/** Delivery method; `auto` chooses by terminal and platform. */
|
|
27
27
|
method: NotifyMethod;
|