@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,228 @@
|
|
|
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 main pane is this page's only child of the shell, so it grows to fill the
|
|
34
|
+
column and the footer stays at the bottom however short the board is. */
|
|
35
|
+
.board-main{flex:1 1 auto}
|
|
36
|
+
/* One panel per tab; the shell's main pane is the only scroll box. */
|
|
37
|
+
.board-panel{display:flex;flex-direction:column;min-width:0}
|
|
38
|
+
.board-panel[hidden]{display:none}
|
|
39
|
+
.board-no-cards{max-width:32rem;margin:auto;padding:var(--space-8) var(--space-6)}
|
|
40
|
+
|
|
41
|
+
/* Lanes: trays of cards, each with a head that sticks while the pane scrolls. A lane is
|
|
42
|
+
sized by the work it holds rather than by its share of the row, so one holding nothing
|
|
43
|
+
narrows to a rail and hands the width to the lanes carrying cards. */
|
|
44
|
+
.board-lanes{display:flex;flex-wrap:nowrap;gap:var(--space-4);align-items:start}
|
|
45
|
+
.board-lane{--lane-count:0;display:flex;flex:1 1 0;flex-grow:calc(1 + var(--lane-count));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)}
|
|
46
|
+
/* The lane count is a count, not a design token: the client sets it from the snapshot and
|
|
47
|
+
declares its default here, so a lane whose count never arrived is an ordinary lane
|
|
48
|
+
rather than a variable nobody defines. */
|
|
49
|
+
/* A lane's share of the row follows the work it holds — two cards earn more width than
|
|
50
|
+
one — and the floor is the narrowest a tray is still a tray at. It is set on the two
|
|
51
|
+
states that draw one, so a rail keeps its own width, and both breakpoints below
|
|
52
|
+
replace the basis, so the floor only ever applies to the row it was chosen for:
|
|
53
|
+
four lanes at 1180px are 60rem, which is inside the row before the wrap. */
|
|
54
|
+
.board-lane[data-empty="false"][data-folded="false"]{min-width:15rem}
|
|
55
|
+
/* The rail: a dot, a word, and a count — the fact that the lane is showing nothing, not a
|
|
56
|
+
tray. A lane is a rail when it holds no cards and when it is folded, because both say
|
|
57
|
+
the same thing: there is nothing here to read. */
|
|
58
|
+
.board-lane[data-empty="true"],.board-lane[data-folded="true"]{flex:0 0 8.5rem}
|
|
59
|
+
.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)}
|
|
60
|
+
.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}
|
|
61
|
+
.board-lane__toggle:hover{color:var(--accent)}
|
|
62
|
+
.board-lane__toggle::after{content:"▾";margin-left:2px;font-size:var(--fs-sm);color:var(--muted)}
|
|
63
|
+
.board-lane__toggle:hover::after{color:var(--accent)}
|
|
64
|
+
.board-lane__toggle[aria-expanded="false"]::after{content:"▸"}
|
|
65
|
+
.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}
|
|
66
|
+
.board-lane__dot{width:var(--space-2);height:var(--space-2);border-radius:var(--radius-pill);background:var(--muted)}
|
|
67
|
+
.board-lane[data-status="doing"] .board-lane__dot{background:var(--accent)}
|
|
68
|
+
.board-lane[data-status="blocked"] .board-lane__dot{background:var(--danger)}
|
|
69
|
+
.board-lane[data-status="done"] .board-lane__dot{background:var(--success)}
|
|
70
|
+
.board-lane__list{display:flex;flex-direction:column;gap:var(--space-3);margin:0;padding:0;list-style:none}
|
|
71
|
+
.board-lane__empty{margin:0;font:var(--fs-sm) var(--font-ui);color:var(--dim)}
|
|
72
|
+
|
|
73
|
+
/* A card: the title, then the facts a reader needs to act on it. Its lane says
|
|
74
|
+
which state it is in, so the card itself carries no second colour bar. */
|
|
75
|
+
.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)}
|
|
76
|
+
.board-card:hover{border-color:var(--border-strong);transform:translateY(-1px)}
|
|
77
|
+
.board-card[hidden]{display:none}
|
|
78
|
+
.board-card__head{display:flex;align-items:baseline;gap:var(--space-2);margin:0;min-width:0}
|
|
79
|
+
.board-card__id{font:var(--fs-xs) var(--font-mono);color:var(--dim)}
|
|
80
|
+
.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}
|
|
81
|
+
.board-card__title:hover{color:var(--accent)}
|
|
82
|
+
/* Finished work recedes: it is history, and the live lanes should not compete with it. */
|
|
83
|
+
.board-card[data-status="done"] .board-card__title{color:var(--muted)}
|
|
84
|
+
.board-card[data-status="done"] .board-card__title:hover{color:var(--accent)}
|
|
85
|
+
.board-card[data-status="done"] .bf-chip--accent{color:var(--muted);border-color:var(--border)}
|
|
86
|
+
.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)}
|
|
87
|
+
.board-card__paths{display:flex;flex-wrap:wrap;gap:var(--space-1);margin:0;padding:0;list-style:none}
|
|
88
|
+
.board-card__reason{margin:0;font:var(--fs-sm)/var(--lh-snug) var(--font-ui);color:var(--muted);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;overflow-wrap:anywhere}
|
|
89
|
+
.board-card__reason--blocked{color:var(--danger)}
|
|
90
|
+
.board-card__reason--unplanned{color:var(--warn)}
|
|
91
|
+
|
|
92
|
+
/* The Team tab: a queue and a roster side by side, then one quiet strip. */
|
|
93
|
+
.board-team{display:grid;gap:var(--space-5);min-width:0}
|
|
94
|
+
.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}
|
|
95
|
+
/* Each panel is its own grid, so both captions land on one line and both lists start on the next. */
|
|
96
|
+
.board-team__group{display:grid;grid-template-rows:auto 1fr;gap:var(--space-3);align-content:start;min-width:0}
|
|
97
|
+
.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)}
|
|
98
|
+
.board-team__caption .bf-chip{margin-left:auto;font-variant-numeric:tabular-nums}
|
|
99
|
+
/* The queue: one row per card, its kind as an edge, its way out in the foot. */
|
|
100
|
+
.board-team__queue{display:flex;flex-direction:column;gap:var(--space-2);min-width:0}
|
|
101
|
+
/* The queue is the tallest thing here and it can be unbounded; capping it keeps the two
|
|
102
|
+
panels on one screen. Stacked below 1180px there is no second column to balance, and a
|
|
103
|
+
nested scroll on a phone is worse than the page's own. */
|
|
104
|
+
@media (min-width:1181px){
|
|
105
|
+
/* A row cut at the edge reads as a bug unless something says the list continues, and a
|
|
106
|
+
mac's scrollbars are hidden until it moves: an inset shade in the page's own colour
|
|
107
|
+
marks the edge without a literal. */
|
|
108
|
+
/* A floor, because a short window must not turn three rows into a scroll box. */
|
|
109
|
+
.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)}
|
|
110
|
+
}
|
|
111
|
+
.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)}
|
|
112
|
+
.board-team__decision:hover{border-color:var(--border-strong);transform:translateY(-1px)}
|
|
113
|
+
.board-team__decision[data-kind="blocked"]{border-left-color:var(--danger)}
|
|
114
|
+
.board-team__decision[data-kind="idle"]{border-left-color:var(--warn)}
|
|
115
|
+
/* Nobody holds it, so the edge is drawn but not filled: a shape, not a claim. */
|
|
116
|
+
.board-team__decision[data-kind="unassigned"]{border-left-style:dashed}
|
|
117
|
+
.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}
|
|
118
|
+
.board-team__open:hover .board-team__title{color:var(--accent)}
|
|
119
|
+
.board-team__ref{font:var(--fs-xs) var(--font-mono);color:var(--muted);flex:0 0 auto}
|
|
120
|
+
.board-team__title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
|
|
121
|
+
/* Two lines, then a tooltip: the "why" is the reason the row is on this tab. */
|
|
122
|
+
.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}
|
|
123
|
+
.board-team__why--blocked{color:var(--danger)}
|
|
124
|
+
.board-team__foot{grid-area:foot;display:flex;align-items:center;justify-content:flex-end;gap:var(--space-2)}
|
|
125
|
+
/* A row with no owner chip still puts its one action on the row's edge. */
|
|
126
|
+
.board-team__foot>.bf-btn:only-child{margin-left:auto}
|
|
127
|
+
/* The roster: who is here and what they hold. No hover fill — nothing to open. */
|
|
128
|
+
.board-team__roster{display:flex;flex-direction:column;gap:var(--space-1);margin:0;padding:0;list-style:none;min-width:0}
|
|
129
|
+
/* No row rule: the caption already delimits the list, and a second hairline beside it
|
|
130
|
+
was the same pen drawn twice. */
|
|
131
|
+
.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}
|
|
132
|
+
.board-team__dot{width:8px;height:8px;border-radius:var(--radius-pill);background:var(--muted);flex:0 0 auto;align-self:center}
|
|
133
|
+
.board-team__mate[data-state="live"] .board-team__dot{background:var(--success)}
|
|
134
|
+
.board-team__mate[data-state="quiet"] .board-team__dot{background:var(--warn)}
|
|
135
|
+
/* A dead session is the one state the page states outright: solid, and in the refusal colour. */
|
|
136
|
+
.board-team__mate[data-state="gone"] .board-team__dot{background:var(--danger)}
|
|
137
|
+
/* A record that is not there is a hollow ring, never a colour that claims it is. */
|
|
138
|
+
.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)}
|
|
139
|
+
.board-team__name{font:var(--fw-medium) var(--fs-md) var(--font-ui);color:var(--text)}
|
|
140
|
+
.board-team__mets{flex:1 0 100%;margin:0;color:var(--muted);font:var(--fs-xs) var(--font-ui)}
|
|
141
|
+
.board-team__empty{margin:0;color:var(--muted);font:var(--fs-sm) var(--font-ui)}
|
|
142
|
+
/* Done stays quiet; its ids are buttons only because they open their card. */
|
|
143
|
+
.board-team__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)}
|
|
144
|
+
/* An id in the Done strip is a button, so it wears the only affordance it has: a
|
|
145
|
+
dotted line, visible before the pointer arrives. */
|
|
146
|
+
.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}
|
|
147
|
+
.board-team__id:hover{color:var(--accent);text-decoration:underline}
|
|
148
|
+
|
|
149
|
+
/* The card dialog: the record of one card, in two named sections. Nothing here scrolls on
|
|
150
|
+
its own — the kit's body is the one scrollbar, so a reader is never inside a scroll
|
|
151
|
+
inside a scroll. */
|
|
152
|
+
/* A little taller than the kit's default: this dialog is the only place a whole card —
|
|
153
|
+
its facts, its plan, and every note it kept — can be read. */
|
|
154
|
+
.board-dialog__panel{width:min(720px,100%);max-height:84vh}
|
|
155
|
+
/* A long title wraps beside its id instead of pushing the close button off the head. */
|
|
156
|
+
.board-dialog__head{flex-wrap:wrap;align-items:baseline;row-gap:var(--space-1)}
|
|
157
|
+
.board-dialog__head h2{flex:1 1 12ch;min-width:0;font-size:var(--fs-lg);line-height:var(--lh-tight);overflow-wrap:anywhere}
|
|
158
|
+
.board-dialog__id{font:var(--fs-sm) var(--font-mono);color:var(--muted)}
|
|
159
|
+
.board-dialog__body{display:flex;flex-direction:column;gap:var(--space-4);padding-bottom:var(--space-4)}
|
|
160
|
+
.board-dialog__section{display:flex;flex-direction:column;gap:var(--space-2);min-width:0}
|
|
161
|
+
/* The heading is a label, not a headline: the title above it is the only headline here. */
|
|
162
|
+
.board-dialog__heading{display:flex;align-items:baseline;gap:var(--space-1);margin:0;padding-bottom:var(--space-1);border-bottom:1px solid var(--border);font:var(--fw-medium) var(--fs-xs) var(--font-ui);letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
|
|
163
|
+
.board-dialog__count{font:var(--fs-xs) var(--font-mono);letter-spacing:0;text-transform:none;color:var(--dim)}
|
|
164
|
+
/* The ladder: labels size to the longest one, values take the width that is left, and
|
|
165
|
+
every row starts at the same column so the eye can run down the facts. */
|
|
166
|
+
.board-dialog__facts{display:grid;grid-template-columns:max-content minmax(0,1fr);gap:var(--space-1) var(--space-3);margin:0;align-items:baseline}
|
|
167
|
+
.board-dialog__facts dt{font:var(--fs-xs) var(--font-ui);letter-spacing:.04em;text-transform:uppercase;color:var(--muted)}
|
|
168
|
+
.board-dialog__facts dd{margin:0;min-width:0;font:var(--fs-sm) var(--font-ui);color:var(--text)}
|
|
169
|
+
/* A path is a value like any other, but a long one elides on its single line and keeps the
|
|
170
|
+
whole of itself on the hover title, so a 90-character plan cannot shred the ladder. */
|
|
171
|
+
.board-dialog__value{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
172
|
+
/* A plan path and the ids a card waits for are code, and a reader copies them. */
|
|
173
|
+
.board-dialog__plan{font-family:var(--font-mono)}
|
|
174
|
+
.board-dialog__paths{display:flex;flex-wrap:wrap;gap:var(--space-1) var(--space-2);margin:0;padding:0;list-style:none;min-width:0}
|
|
175
|
+
.board-dialog__paths li{min-width:0;max-width:100%}
|
|
176
|
+
/* A path is code, not a label: the owner is a chip of prose, a path is something you type. */
|
|
177
|
+
.board-dialog__path{max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:bottom;font-family:var(--font-mono)}
|
|
178
|
+
.board-dialog__notes{display:flex;flex-direction:column;gap:var(--space-3);margin:0;padding:0;list-style:none}
|
|
179
|
+
/* A rule between notes: wrapped prose used to run one note into the next. */
|
|
180
|
+
.board-dialog__note{padding-top:var(--space-3);border-top:1px solid var(--border)}
|
|
181
|
+
.board-dialog__note:first-child{padding-top:0;border-top:0}
|
|
182
|
+
/* The sentence is the record, so it reads at body size on a measure, not the full 720px. */
|
|
183
|
+
.board-dialog__note-text{margin:0 0 var(--space-1);max-width:68ch;font:var(--fs-base)/var(--lh-relaxed) var(--font-ui);color:var(--text);overflow-wrap:anywhere;white-space:pre-wrap}
|
|
184
|
+
/* Who and when is the footnote of the note above it. */
|
|
185
|
+
.board-dialog__note-foot{display:flex;align-items:baseline;gap:var(--space-2);margin:0;max-width:68ch}
|
|
186
|
+
.board-dialog__note-by{font:var(--fw-medium) var(--fs-xs) var(--font-ui);color:var(--muted)}
|
|
187
|
+
.board-dialog__note-at{font:var(--fs-xs) var(--font-mono);color:var(--dim)}
|
|
188
|
+
.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)}
|
|
189
|
+
.board-dialog__state::before{content:"";width:var(--space-2);height:var(--space-2);border-radius:var(--radius-pill);background:var(--muted)}
|
|
190
|
+
.board-dialog__state[data-status="doing"]::before{background:var(--accent)}
|
|
191
|
+
.board-dialog__state[data-status="blocked"]::before{background:var(--danger)}
|
|
192
|
+
.board-dialog__state[data-status="done"]::before{background:var(--success)}
|
|
193
|
+
.board-skeleton{height:4.5rem;border:1px dashed var(--border);border-radius:var(--radius-md);background:var(--surface)}
|
|
194
|
+
|
|
195
|
+
@media (max-width:1180px){
|
|
196
|
+
.board-lanes{flex-wrap:wrap}
|
|
197
|
+
.board-lane{flex:1 1 calc(50% - var(--space-4))}
|
|
198
|
+
.board-progress{display:none}
|
|
199
|
+
/* Two panels need a wide window; below this the queue comes first, the roster after. */
|
|
200
|
+
.board-team__grid{grid-template-columns:minmax(0,1fr)}
|
|
201
|
+
}
|
|
202
|
+
@media (max-width:860px){
|
|
203
|
+
.board-lanes{flex-direction:column;align-items:stretch}
|
|
204
|
+
.board-lane{flex:1 1 auto}
|
|
205
|
+
/* One lane at a time, so a rail buys nothing: the shown lane takes the column. */
|
|
206
|
+
.board-lane[data-empty="true"],.board-lane[data-folded="true"]{flex:1 1 auto}
|
|
207
|
+
.board-lane[hidden]{display:none}
|
|
208
|
+
.board-picker{display:flex}
|
|
209
|
+
.board-toolbar{gap:var(--space-2)}
|
|
210
|
+
.board-toolbar .bf-spacer{display:none}
|
|
211
|
+
/* The icons stay with the tabs and the filter takes the last row whole, so the
|
|
212
|
+
toolbar wraps once, on purpose, instead of leaving two buttons stranded. */
|
|
213
|
+
.board-controls{width:100%;justify-content:flex-start}
|
|
214
|
+
.board-filter{order:9;width:100%}
|
|
215
|
+
.board-team__foot{flex-wrap:wrap}
|
|
216
|
+
}
|
|
217
|
+
@media (prefers-reduced-motion: reduce){
|
|
218
|
+
.board-card,.board-team__decision{transition:none}
|
|
219
|
+
.board-card:hover,.board-team__decision:hover{transform:none}
|
|
220
|
+
}
|
|
221
|
+
@media print{
|
|
222
|
+
.board-lanes,.board-team__grid{grid-template-columns:minmax(0,1fr)}
|
|
223
|
+
.board-card{break-inside:avoid}
|
|
224
|
+
/* The disclosure affordance goes; the lane's name stays with its count. */
|
|
225
|
+
.board-filter,.board-picker{display:none!important}
|
|
226
|
+
.board-lane__toggle::after{content:""}
|
|
227
|
+
}
|
|
228
|
+
`;
|
|
@@ -0,0 +1,327 @@
|
|
|
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 ago = board.ago;
|
|
25
|
+
var plural = board.plural;
|
|
26
|
+
|
|
27
|
+
/** The word a teammate's state is read as; never a colour alone. */
|
|
28
|
+
var STATE_WORD = { live: "live", quiet: "quiet", gone: "gone", absent: "no record", unknown: "\u2014" };
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A panel: its heading row, then its body.
|
|
32
|
+
*
|
|
33
|
+
* With a filter on, the count reads shown/total the way a lane head does, so a
|
|
34
|
+
* reader can tell "nothing here" from "nothing matches".
|
|
35
|
+
*/
|
|
36
|
+
function panel(text, count, total) {
|
|
37
|
+
var node = el("section", N.teamGroup);
|
|
38
|
+
node.setAttribute("aria-label", text);
|
|
39
|
+
var caption = el("h2", N.teamCaption, text);
|
|
40
|
+
caption.appendChild(chip(total !== undefined && total !== count ? count + "/" + total : String(count)));
|
|
41
|
+
node.appendChild(caption);
|
|
42
|
+
return node;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Which cards need the lead, and why — entries, not rows.
|
|
47
|
+
*
|
|
48
|
+
* The renderer filters this list, so it has to be the data: filtering the built
|
|
49
|
+
* rows would test the element instead of the card.
|
|
50
|
+
*
|
|
51
|
+
* The round's groups overlap on purpose — a doing card untouched for half an hour
|
|
52
|
+
* is both idle and unplanned — so a card can arrive twice. On the page one card is
|
|
53
|
+
* one row, and the strongest reason is the one it keeps: a blocker outranks silence,
|
|
54
|
+
* which outranks a missing plan, which outranks nobody holding it.
|
|
55
|
+
*/
|
|
56
|
+
var KIND_RANK = { blocked: 0, gone: 1, waiting: 2, idle: 3, unplanned: 4, unassigned: 5 };
|
|
57
|
+
|
|
58
|
+
/** The labels of the sessions whose process is gone, in the roster's own spelling. */
|
|
59
|
+
function goneLabels(view) {
|
|
60
|
+
var roster = view.teammates;
|
|
61
|
+
var gone = {};
|
|
62
|
+
if (roster === undefined) return gone;
|
|
63
|
+
for (var i = 0; i < roster.length; i++) {
|
|
64
|
+
if (roster[i].state === "gone") gone[roster[i].label.toLowerCase()] = roster[i].label;
|
|
65
|
+
}
|
|
66
|
+
return gone;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function needs(view) {
|
|
70
|
+
var round = view.round;
|
|
71
|
+
var byCard = {};
|
|
72
|
+
var entries = [];
|
|
73
|
+
function add(cards, why, kind) {
|
|
74
|
+
for (var i = 0; i < cards.length; i++) {
|
|
75
|
+
var card = cards[i];
|
|
76
|
+
var entry = { card: card, why: why(card), kind: kind };
|
|
77
|
+
var seen = byCard[card.id];
|
|
78
|
+
if (seen === undefined) {
|
|
79
|
+
byCard[card.id] = entry;
|
|
80
|
+
entries.push(entry);
|
|
81
|
+
} else if (KIND_RANK[kind] < KIND_RANK[seen.kind]) {
|
|
82
|
+
seen.why = entry.why;
|
|
83
|
+
seen.kind = kind;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// A blocked card in this queue is a person's decision: the reason the move recorded
|
|
88
|
+
// comes first, and the last note is only the fallback for a block nobody explained.
|
|
89
|
+
add(round.blocked, function (card) {
|
|
90
|
+
if (card.blockedBy) return card.blockedBy;
|
|
91
|
+
var last = card.notes.length > 0 ? card.notes[card.notes.length - 1] : undefined;
|
|
92
|
+
return last ? last.text : "blocked";
|
|
93
|
+
}, "blocked");
|
|
94
|
+
// A dead owner is a fact, not an inference from silence: the card is anyone's now.
|
|
95
|
+
var gone = goneLabels(view);
|
|
96
|
+
add(
|
|
97
|
+
round.owners.reduce(function (cards, group) {
|
|
98
|
+
return gone[group.label.toLowerCase()] === undefined ? cards : cards.concat(group.cards);
|
|
99
|
+
}, []),
|
|
100
|
+
function (card) { return (card.owner ? card.owner.label : "its owner") + " is not running — the card is free"; },
|
|
101
|
+
"gone",
|
|
102
|
+
);
|
|
103
|
+
add(round.idle, function (card) { return "no touch for " + ago(card.updatedAt); }, "idle");
|
|
104
|
+
// The board card says the same words for the same fact: one voice for one state.
|
|
105
|
+
add(round.unplanned, function () { return "no plan recorded"; }, "unplanned");
|
|
106
|
+
add(round.unassigned, function () { return "nobody holds it"; }, "unassigned");
|
|
107
|
+
// An order outranks nobody holding it: the claim would be refused, so the queue says
|
|
108
|
+
// what it waits for instead of offering a button the rules would turn down.
|
|
109
|
+
add(
|
|
110
|
+
round.unassigned.filter(function (card) { return card.waits !== undefined; }),
|
|
111
|
+
function (card) { return "waits on " + card.waits; },
|
|
112
|
+
"waiting",
|
|
113
|
+
);
|
|
114
|
+
// An offer outranks plain silence: the lead has already tried to hand this card out,
|
|
115
|
+
// and the age is the difference between nobody having looked and nobody taking it. A
|
|
116
|
+
// card that waits keeps that reason, because the claim would be refused either way.
|
|
117
|
+
add(
|
|
118
|
+
round.unassigned.filter(function (card) { return card.offeredAt !== undefined; }),
|
|
119
|
+
function (card) { return "offered " + ago(card.offeredAt) + " ago — nobody took it"; },
|
|
120
|
+
"waiting",
|
|
121
|
+
);
|
|
122
|
+
return entries;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** One decision: the card, why it is here, who holds it, and the way out. */
|
|
126
|
+
function decision(entry, view) {
|
|
127
|
+
var card = entry.card;
|
|
128
|
+
var node = el("article", N.teamDecision);
|
|
129
|
+
node.setAttribute("data-kind", entry.kind);
|
|
130
|
+
|
|
131
|
+
var open = el("button", N.teamOpen);
|
|
132
|
+
open.type = "button";
|
|
133
|
+
open.setAttribute("aria-haspopup", "dialog");
|
|
134
|
+
open.appendChild(el("span", N.teamRef, "#" + card.id));
|
|
135
|
+
var title = el("span", N.teamTitle, card.title);
|
|
136
|
+
// A clamped title has no other way to be read, so it carries its own tooltip.
|
|
137
|
+
title.title = card.title;
|
|
138
|
+
open.appendChild(title);
|
|
139
|
+
open.addEventListener("click", function () { board.openCard(card.id); });
|
|
140
|
+
node.appendChild(open);
|
|
141
|
+
|
|
142
|
+
var foot = el("div", N.teamFoot);
|
|
143
|
+
if (card.owner) foot.appendChild(chip(card.owner.label, "accent"));
|
|
144
|
+
// No button: this page cannot claim or reclaim, and a page that offered either would
|
|
145
|
+
// be teaching a verb the server refuses. The row is a reason to pick a card up in the
|
|
146
|
+
// terminal, which is where a claim is made.
|
|
147
|
+
node.appendChild(foot);
|
|
148
|
+
|
|
149
|
+
// The reason gets the row's whole width and two lines; the tooltip carries the
|
|
150
|
+
// rest, because this sentence is the lead's whole reason to look at the row.
|
|
151
|
+
var why = el("p", N.teamWhy, entry.why);
|
|
152
|
+
why.title = entry.why;
|
|
153
|
+
if (entry.kind === "blocked") why.className = N.teamWhy + " " + N.teamWhyBlocked;
|
|
154
|
+
node.appendChild(why);
|
|
155
|
+
return node;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Every teammate: the bus's sessions and the owners a card names, as one list.
|
|
160
|
+
*
|
|
161
|
+
* An owner with no live session is the interesting row — work is held by a label
|
|
162
|
+
* nothing answers to — so it stays, with "no record" where a heartbeat would be.
|
|
163
|
+
*/
|
|
164
|
+
function teammates(view, roster) {
|
|
165
|
+
var round = view.round;
|
|
166
|
+
var byLabel = {};
|
|
167
|
+
var rows = [];
|
|
168
|
+
function labelOf(label) { return label.toLowerCase(); }
|
|
169
|
+
|
|
170
|
+
for (var i = 0; i < round.owners.length; i++) {
|
|
171
|
+
var group = round.owners[i];
|
|
172
|
+
byLabel[labelOf(group.label)] = { label: group.label, live: group.cards, done: group.done };
|
|
173
|
+
}
|
|
174
|
+
var seen = {};
|
|
175
|
+
if (roster !== null) {
|
|
176
|
+
for (var j = 0; j < roster.length; j++) {
|
|
177
|
+
var entry = roster[j];
|
|
178
|
+
seen[labelOf(entry.label)] = true;
|
|
179
|
+
rows.push({
|
|
180
|
+
label: entry.label,
|
|
181
|
+
state: entry.state,
|
|
182
|
+
seen: entry.seen,
|
|
183
|
+
mail: entry.mail,
|
|
184
|
+
listening: entry.listening,
|
|
185
|
+
you: entry.you,
|
|
186
|
+
cards: byLabel[labelOf(entry.label)] || { live: [], done: [] },
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
for (var key in byLabel) {
|
|
191
|
+
if (seen[key]) continue;
|
|
192
|
+
rows.push({ label: byLabel[key].label, state: roster === null ? "unknown" : "absent", seen: undefined, mail: 0, listening: undefined, you: false, cards: byLabel[key] });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function rank(mate) {
|
|
196
|
+
if (mate.you) return -1;
|
|
197
|
+
if (mate.state === "live") return 0;
|
|
198
|
+
// A crash outranks a stale heartbeat: one is news, the other may be a long turn.
|
|
199
|
+
if (mate.state === "gone") return 1;
|
|
200
|
+
if (mate.state === "quiet") return 2;
|
|
201
|
+
if (mate.state === "absent") return 3;
|
|
202
|
+
return 4;
|
|
203
|
+
}
|
|
204
|
+
rows.sort(function (a, b) { return rank(a) - rank(b) || a.label.localeCompare(b.label); });
|
|
205
|
+
return rows;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** A text node carrying its stamp, so refreshAges rewrites it with every other age. */
|
|
209
|
+
function stamped(text, at) {
|
|
210
|
+
var node = el("span", "", text);
|
|
211
|
+
node.setAttribute("data-at", String(at));
|
|
212
|
+
return node;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** One teammate: who, how they are, and what they hold. Not a button — nothing to open. */
|
|
216
|
+
function mate(mate) {
|
|
217
|
+
var node = el("li", N.teamMate);
|
|
218
|
+
node.setAttribute("data-state", mate.state);
|
|
219
|
+
if (mate.you) node.setAttribute("data-you", "1");
|
|
220
|
+
|
|
221
|
+
var dot = el("span", N.teamDot);
|
|
222
|
+
dot.setAttribute("aria-hidden", "true");
|
|
223
|
+
node.appendChild(dot);
|
|
224
|
+
node.appendChild(el("span", N.teamName, mate.label));
|
|
225
|
+
if (mate.you) node.appendChild(chip("you"));
|
|
226
|
+
// A live peer whose watch is off will read a handoff at its next turn instead of being
|
|
227
|
+
// woken for it, which is the one thing a lead can still act on.
|
|
228
|
+
if (mate.state === "live" && mate.listening === false) {
|
|
229
|
+
var silent = chip("not listening");
|
|
230
|
+
silent.title = "Mail waits for this session's next turn; it is not woken for it.";
|
|
231
|
+
node.appendChild(silent);
|
|
232
|
+
}
|
|
233
|
+
if (mate.mail > 0) node.appendChild(chip(plural(mate.mail, "mail"), "accent"));
|
|
234
|
+
|
|
235
|
+
// "quiet" alone does not tell a lead whether to reclaim; the heartbeat age does.
|
|
236
|
+
var mets = el("p", N.teamMets);
|
|
237
|
+
if (mate.state === "quiet" && mate.seen !== undefined) {
|
|
238
|
+
mets.appendChild(document.createTextNode("quiet for "));
|
|
239
|
+
mets.appendChild(stamped(ago(mate.seen), mate.seen));
|
|
240
|
+
} else {
|
|
241
|
+
mets.appendChild(document.createTextNode(STATE_WORD[mate.state] || mate.state));
|
|
242
|
+
}
|
|
243
|
+
var held = mate.cards.live.length;
|
|
244
|
+
var done = mate.cards.done.length;
|
|
245
|
+
mets.appendChild(document.createTextNode(" \u00B7 " + (held === 0 ? "no cards" : held + " held")));
|
|
246
|
+
if (done > 0) mets.appendChild(document.createTextNode(" \u00B7 " + done + " done"));
|
|
247
|
+
if (held > 0) {
|
|
248
|
+
var at = mate.cards.live[0].updatedAt;
|
|
249
|
+
for (var i = 1; i < mate.cards.live.length; i++) at = Math.min(at, mate.cards.live[i].updatedAt);
|
|
250
|
+
mets.appendChild(document.createTextNode(" \u00B7 oldest "));
|
|
251
|
+
mets.appendChild(stamped(ago(at), at));
|
|
252
|
+
}
|
|
253
|
+
node.appendChild(mets);
|
|
254
|
+
return node;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
board.renderTeam = function (view) {
|
|
258
|
+
var root = document.getElementById("board-team");
|
|
259
|
+
if (!root) return;
|
|
260
|
+
root.textContent = "";
|
|
261
|
+
var round = view.round;
|
|
262
|
+
// Absent, not empty: null means no bus here, so every state reads unknown.
|
|
263
|
+
var roster = view.teammates === undefined ? null : view.teammates;
|
|
264
|
+
// The toolbar's filter is one control for both tabs, so the round reads it too.
|
|
265
|
+
var query = board.query || "";
|
|
266
|
+
var keep = function (card) { return board.matches(card, query); };
|
|
267
|
+
var matchesLabel = function (label) { return label.toLowerCase().indexOf(query.toLowerCase()) !== -1; };
|
|
268
|
+
var filtering = query !== "";
|
|
269
|
+
var noMatch = "no card here matches \"" + query + "\"";
|
|
270
|
+
|
|
271
|
+
var grid = el("div", N.teamGrid);
|
|
272
|
+
|
|
273
|
+
var all = needs(view);
|
|
274
|
+
var decisions = all.filter(function (entry) { return keep(entry.card); });
|
|
275
|
+
var queue = panel("Needs a decision", decisions.length, all.length);
|
|
276
|
+
if (decisions.length === 0) {
|
|
277
|
+
queue.appendChild(el("p", N.teamEmpty, filtering ? noMatch : "nothing needs you: no blockers, nothing idle, every card planned and held"));
|
|
278
|
+
} else {
|
|
279
|
+
var list = el("div", N.teamQueue);
|
|
280
|
+
for (var i = 0; i < decisions.length; i++) list.appendChild(decision(decisions[i], view));
|
|
281
|
+
queue.appendChild(list);
|
|
282
|
+
}
|
|
283
|
+
grid.appendChild(queue);
|
|
284
|
+
|
|
285
|
+
var everyone = teammates(view, roster);
|
|
286
|
+
var shown = everyone.filter(function (m) {
|
|
287
|
+
if (!filtering) return true;
|
|
288
|
+
if (matchesLabel(m.label)) return true;
|
|
289
|
+
var held = m.cards.live.concat(m.cards.done);
|
|
290
|
+
for (var k = 0; k < held.length; k++) if (keep(held[k])) return true;
|
|
291
|
+
return false;
|
|
292
|
+
});
|
|
293
|
+
var people = panel("Teammates", shown.length, everyone.length);
|
|
294
|
+
if (shown.length === 0) {
|
|
295
|
+
people.appendChild(el("p", N.teamEmpty, filtering ? noMatch : roster === null ? "session-bus is not loaded here" : "nobody is here yet"));
|
|
296
|
+
} else {
|
|
297
|
+
var rosterList = el("ul", N.teamRoster);
|
|
298
|
+
for (var j = 0; j < shown.length; j++) rosterList.appendChild(mate(shown[j]));
|
|
299
|
+
people.appendChild(rosterList);
|
|
300
|
+
}
|
|
301
|
+
grid.appendChild(people);
|
|
302
|
+
root.appendChild(grid);
|
|
303
|
+
|
|
304
|
+
var done = round.done.filter(keep);
|
|
305
|
+
var strip = el("p", N.teamDone);
|
|
306
|
+
if (done.length === 0) {
|
|
307
|
+
// While filtering, a third "no card here matches" line adds nothing; the two
|
|
308
|
+
// panels above already said it.
|
|
309
|
+
if (filtering) return root;
|
|
310
|
+
strip.textContent = "nothing finished yet";
|
|
311
|
+
} else {
|
|
312
|
+
strip.appendChild(document.createTextNode("Done \u00B7 "));
|
|
313
|
+
for (var m = 0; m < done.length; m++) {
|
|
314
|
+
(function (card) {
|
|
315
|
+
var id = el("button", N.teamId, "#" + card.id);
|
|
316
|
+
id.type = "button";
|
|
317
|
+
id.setAttribute("aria-haspopup", "dialog");
|
|
318
|
+
id.addEventListener("click", function () { board.openCard(card.id); });
|
|
319
|
+
strip.appendChild(id);
|
|
320
|
+
})(done[m]);
|
|
321
|
+
}
|
|
322
|
+
strip.appendChild(document.createTextNode(" \u00B7 " + plural(done.length, "card") +
|
|
323
|
+
(filtering && done.length !== round.done.length ? " of " + round.done.length : "")));
|
|
324
|
+
}
|
|
325
|
+
root.appendChild(strip);
|
|
326
|
+
};
|
|
327
|
+
})();`;
|
|
@@ -7,12 +7,13 @@
|
|
|
7
7
|
* whether the threshold has been crossed and what the one notice says.
|
|
8
8
|
*
|
|
9
9
|
* Pure: the caller has the priced spend and the currency, this shapes the answer.
|
|
10
|
-
* The amount formatting
|
|
11
|
-
*
|
|
10
|
+
* The amount formatting is the footer's own — `formatCost` for what was spent,
|
|
11
|
+
* `formatLimit` for the ceiling it is held against — so the notice and the status
|
|
12
|
+
* bar cannot disagree about either figure.
|
|
12
13
|
*/
|
|
13
14
|
|
|
14
15
|
import { noticeMessage, type NoticeMessage } from "../../../lib/notice.ts";
|
|
15
|
-
import { formatCost } from "./render/format.ts";
|
|
16
|
+
import { formatCost, formatLimit } from "./render/format.ts";
|
|
16
17
|
import type { Currency } from "./price/types.ts";
|
|
17
18
|
|
|
18
19
|
/** Where the session stands against its budget. */
|
|
@@ -41,7 +42,7 @@ export function budgetState(limit: number, spent: number): BudgetState {
|
|
|
41
42
|
*/
|
|
42
43
|
export function budgetNotice(state: BudgetState, currency: Currency): NoticeMessage {
|
|
43
44
|
const spent = formatCost(state.spent, currency);
|
|
44
|
-
const limit =
|
|
45
|
+
const limit = formatLimit(state.limit, currency);
|
|
45
46
|
return noticeMessage({
|
|
46
47
|
content:
|
|
47
48
|
`Session spend reached the configured budget: ${spent} of ${limit}. ` +
|
|
@@ -60,5 +61,5 @@ export function budgetNotice(state: BudgetState, currency: Currency): NoticeMess
|
|
|
60
61
|
|
|
61
62
|
/** The one-line body an OS notification carries. */
|
|
62
63
|
export function budgetBody(state: BudgetState, currency: Currency): string {
|
|
63
|
-
return `Session budget reached · ${formatCost(state.spent, currency)} of ${
|
|
64
|
+
return `Session budget reached · ${formatCost(state.spent, currency)} of ${formatLimit(state.limit, currency)}`;
|
|
64
65
|
}
|
|
@@ -50,6 +50,17 @@ interface UsageSource {
|
|
|
50
50
|
* not say. A `model_change` entry, and an assistant call that names its model,
|
|
51
51
|
* both move the model in effect for the entries that follow.
|
|
52
52
|
*/
|
|
53
|
+
/**
|
|
54
|
+
* The share of a call's prompt that came from the cache, as a percentage.
|
|
55
|
+
*
|
|
56
|
+
* One body, because two callers ask the same question: the collector for a live session,
|
|
57
|
+
* and the chrome capture, whose frame must print the same number the footer would.
|
|
58
|
+
*/
|
|
59
|
+
export function cacheHitRateOf(input: number, cacheRead: number, cacheWrite: number): number | null {
|
|
60
|
+
const prompt = input + cacheRead + cacheWrite;
|
|
61
|
+
return prompt > 0 ? (cacheRead / prompt) * 100 : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
53
64
|
export function usageRecords(
|
|
54
65
|
entries: Iterable<unknown>,
|
|
55
66
|
active: { provider: string; model: string } | undefined,
|
|
@@ -89,10 +100,7 @@ export function usageRecords(
|
|
|
89
100
|
isAssistant: source.isAssistant,
|
|
90
101
|
});
|
|
91
102
|
|
|
92
|
-
if (source.isAssistant)
|
|
93
|
-
const prompt = input + cacheRead + cacheWrite;
|
|
94
|
-
cacheHitRate = prompt > 0 ? (cacheRead / prompt) * 100 : null;
|
|
95
|
-
}
|
|
103
|
+
if (source.isAssistant) cacheHitRate = cacheHitRateOf(input, cacheRead, cacheWrite);
|
|
96
104
|
}
|
|
97
105
|
|
|
98
106
|
return { records, cacheHitRate };
|