@crouton-kit/crouter 0.3.27 → 0.3.29
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/dist/build-root.js +4 -2
- package/dist/builtin-memory/crouter-development/marketplaces.md +7 -6
- package/dist/builtin-memory/crouter-development/personas/base-prompt.md +1 -1
- package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +1 -1
- package/dist/builtin-memory/crouter-development/personas.md +1 -1
- package/dist/builtin-memory/crouter-development/plugins.md +20 -18
- package/dist/builtin-memory/design.md +1 -1
- package/dist/builtin-memory/development.md +1 -1
- package/dist/builtin-memory/internal/INDEX.md +1 -1
- package/dist/builtin-memory/internal/examples/INDEX.md +1 -1
- package/dist/builtin-memory/internal/examples/imessage-assistant.md +2 -2
- package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
- package/dist/builtin-memory/internal/storage-tiers.md +1 -1
- package/dist/builtin-memory/planning.md +1 -1
- package/dist/builtin-memory/spec.md +6 -4
- package/dist/builtin-personas/orchestration-kernel.md +4 -5
- package/dist/builtin-personas/runtime-base.md +8 -4
- package/dist/builtin-personas/spec/PERSONA.md +8 -4
- package/dist/builtin-personas/spec/orchestrator.md +5 -3
- package/dist/builtin-personas/spec/requirements/PERSONA.md +10 -0
- package/dist/builtin-views/canvas/core.mjs +586 -0
- package/dist/builtin-views/canvas/text.mjs +58 -0
- package/dist/builtin-views/canvas/tui.mjs +165 -0
- package/dist/builtin-views/canvas/web.jsx +120 -0
- package/dist/builtin-views/git-pr/core.mjs +673 -0
- package/dist/builtin-views/git-pr/text.mjs +84 -0
- package/dist/builtin-views/git-pr/tui.mjs +302 -0
- package/dist/builtin-views/git-pr/web.jsx +216 -0
- package/dist/builtin-views/inbox/_lib/render.mjs +1 -3
- package/dist/builtin-views/inbox/core.mjs +1273 -0
- package/dist/builtin-views/inbox/text.mjs +73 -0
- package/dist/builtin-views/inbox/tui.mjs +312 -0
- package/dist/builtin-views/inbox/web.jsx +188 -0
- package/dist/builtin-views/linkedin/core.mjs +906 -0
- package/dist/builtin-views/linkedin/text.mjs +69 -0
- package/dist/builtin-views/linkedin/tui.mjs +427 -0
- package/dist/builtin-views/linkedin/web.jsx +206 -0
- package/dist/builtin-views/workspace-sidebar/core.mjs +621 -0
- package/dist/builtin-views/workspace-sidebar/text.mjs +53 -0
- package/dist/builtin-views/workspace-sidebar/tui.mjs +142 -0
- package/dist/builtin-views/workspace-sidebar/web.jsx +109 -0
- package/dist/cli.js +3 -3
- package/dist/clients/attach/__tests__/action-parity.test.js +48 -0
- package/dist/clients/attach/__tests__/clipboard-image.test.js +76 -0
- package/dist/clients/attach/__tests__/editor-newline.test.js +57 -0
- package/dist/clients/attach/__tests__/git-info.test.js +31 -0
- package/dist/clients/attach/__tests__/onrequest-wired.test.js +20 -0
- package/dist/clients/attach/__tests__/reconnect-giveup.test.js +30 -0
- package/dist/clients/attach/__tests__/slash-quit-copy.test.js +42 -0
- package/dist/clients/attach/__tests__/titled-editor.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/titled-editor.test.js +39 -0
- package/dist/clients/attach/attach-cmd.js +445 -41
- package/dist/clients/attach/auth-pickers.d.ts +9 -0
- package/dist/clients/attach/auth-pickers.js +194 -0
- package/dist/clients/attach/canvas-panels.d.ts +12 -0
- package/dist/clients/attach/canvas-panels.js +78 -0
- package/dist/clients/attach/chat-view.d.ts +27 -2
- package/dist/clients/attach/chat-view.js +98 -9
- package/dist/clients/attach/clipboard-image.d.ts +47 -8
- package/dist/clients/attach/clipboard-image.js +150 -26
- package/dist/clients/attach/clipboard-text.d.ts +3 -0
- package/dist/clients/attach/clipboard-text.js +39 -0
- package/dist/clients/attach/config-load.d.ts +54 -7
- package/dist/clients/attach/config-load.js +99 -3
- package/dist/clients/attach/context-message.d.ts +12 -0
- package/dist/clients/attach/context-message.js +72 -0
- package/dist/clients/attach/git-info.d.ts +20 -0
- package/dist/clients/attach/git-info.js +44 -0
- package/dist/clients/attach/graph-overlay.d.ts +43 -0
- package/dist/clients/attach/graph-overlay.js +306 -0
- package/dist/clients/attach/input-controller.d.ts +81 -13
- package/dist/clients/attach/input-controller.js +307 -67
- package/dist/clients/attach/pickers.d.ts +58 -0
- package/dist/clients/attach/pickers.js +171 -0
- package/dist/clients/attach/slash-commands.d.ts +44 -1
- package/dist/clients/attach/slash-commands.js +250 -27
- package/dist/clients/attach/titled-editor.d.ts +33 -0
- package/dist/clients/attach/titled-editor.js +84 -0
- package/dist/clients/attach/view-socket.d.ts +37 -2
- package/dist/clients/attach/view-socket.js +131 -7
- package/dist/clients/web/dev-server.d.ts +7 -0
- package/dist/clients/web/dev-server.js +59 -0
- package/dist/clients/web/events.d.ts +14 -0
- package/dist/clients/web/events.js +151 -0
- package/dist/clients/web/server.d.ts +18 -0
- package/dist/clients/web/server.js +450 -0
- package/dist/clients/web/web-cmd.d.ts +2 -0
- package/dist/clients/web/web-cmd.js +120 -0
- package/dist/commands/canvas.js +1 -2
- package/dist/commands/chord.js +1 -1
- package/dist/commands/dashboard.js +5 -1
- package/dist/commands/human/prompts.js +3 -3
- package/dist/commands/human/queue.d.ts +17 -0
- package/dist/commands/human/queue.js +111 -4
- package/dist/commands/memory/__tests__/lint-schema.test.js +27 -4
- package/dist/commands/memory/lint.d.ts +5 -4
- package/dist/commands/memory/lint.js +23 -7
- package/dist/commands/memory/read.js +2 -2
- package/dist/commands/memory/shared.d.ts +7 -1
- package/dist/commands/memory/shared.js +18 -3
- package/dist/commands/memory/write.js +20 -7
- package/dist/commands/memory.js +4 -4
- package/dist/commands/node.d.ts +1 -1
- package/dist/commands/node.js +64 -65
- package/dist/commands/pkg.js +1 -2
- package/dist/commands/revive.js +11 -3
- package/dist/commands/sys/feedback.d.ts +1 -0
- package/dist/commands/sys/feedback.js +163 -0
- package/dist/commands/sys/sync.d.ts +1 -0
- package/dist/commands/sys/sync.js +187 -0
- package/dist/commands/sys.js +4 -2
- package/dist/commands/view-cycle.js +2 -2
- package/dist/commands/view-list.js +8 -8
- package/dist/commands/view-new.js +21 -17
- package/dist/commands/view-pick.js +1 -1
- package/dist/commands/view-run.js +35 -14
- package/dist/commands/view.js +8 -6
- package/dist/commands/workspace.d.ts +2 -0
- package/dist/commands/workspace.js +161 -0
- package/dist/core/__tests__/broker-double-spawn.test.d.ts +1 -0
- package/dist/core/__tests__/broker-double-spawn.test.js +143 -0
- package/dist/core/__tests__/broker-fork-seam.test.d.ts +1 -0
- package/dist/core/__tests__/broker-fork-seam.test.js +102 -0
- package/dist/core/__tests__/broker-sdk-wiring.test.js +2 -0
- package/dist/core/__tests__/broker-snapshot-history.test.d.ts +1 -0
- package/dist/core/__tests__/broker-snapshot-history.test.js +105 -0
- package/dist/core/__tests__/child-death-wake.test.js +56 -111
- package/dist/core/__tests__/close.test.js +14 -20
- package/dist/core/__tests__/context-intro.test.js +19 -19
- package/dist/core/__tests__/daemon-boot.test.js +30 -23
- package/dist/core/__tests__/draw-style.test.js +1 -1
- package/dist/core/__tests__/error-stall-recycle.test.d.ts +1 -0
- package/dist/core/__tests__/error-stall-recycle.test.js +141 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +24 -1
- package/dist/core/__tests__/fixtures/fake-engine.js +32 -2
- package/dist/core/__tests__/fixtures/fake-pi-host.js +2 -2
- package/dist/core/__tests__/focuses.test.js +25 -13
- package/dist/core/__tests__/fork.test.js +22 -5
- package/dist/core/__tests__/full/broker-control-preempt.test.d.ts +1 -0
- package/dist/core/__tests__/full/broker-control-preempt.test.js +61 -0
- package/dist/core/__tests__/full/broker-model-changed-broadcast.test.d.ts +1 -0
- package/dist/core/__tests__/full/broker-model-changed-broadcast.test.js +57 -0
- package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.d.ts +1 -0
- package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.js +80 -0
- package/dist/core/__tests__/full/broker-pane-resolution.test.js +90 -0
- package/dist/core/__tests__/full/cascade-close.test.js +22 -8
- package/dist/core/__tests__/full/detach-focus.test.js +66 -169
- package/dist/core/__tests__/helpers/harness.d.ts +6 -9
- package/dist/core/__tests__/helpers/harness.js +19 -36
- package/dist/core/__tests__/human-node-not-supervised.test.d.ts +1 -0
- package/dist/core/__tests__/human-node-not-supervised.test.js +83 -0
- package/dist/core/__tests__/human-stranded-deliver.test.d.ts +1 -0
- package/dist/core/__tests__/human-stranded-deliver.test.js +108 -0
- package/dist/core/__tests__/lifecycle.test.js +1 -19
- package/dist/core/__tests__/live-mutation-verbs.test.js +5 -5
- package/dist/core/__tests__/memory-resolver.test.js +1 -1
- package/dist/core/__tests__/on-read-dedup-resume.test.d.ts +1 -0
- package/dist/core/__tests__/on-read-dedup-resume.test.js +81 -0
- package/dist/core/__tests__/refresh-stall-recycle.test.d.ts +1 -0
- package/dist/core/__tests__/refresh-stall-recycle.test.js +88 -0
- package/dist/core/__tests__/relaunch-root.test.d.ts +1 -0
- package/dist/core/__tests__/relaunch-root.test.js +114 -0
- package/dist/core/__tests__/reset.test.js +19 -79
- package/dist/core/__tests__/steer-note.test.js +9 -7
- package/dist/core/__tests__/tmux-surface.test.js +13 -7
- package/dist/core/bootstrap.d.ts +0 -3
- package/dist/core/bootstrap.js +1 -143
- package/dist/core/canvas/browse/__tests__/model.test.js +86 -1
- package/dist/core/canvas/browse/__tests__/render.test.js +2 -1
- package/dist/core/canvas/browse/app.js +131 -8
- package/dist/core/canvas/browse/model.d.ts +23 -5
- package/dist/core/canvas/browse/model.js +68 -13
- package/dist/core/canvas/browse/render.d.ts +7 -1
- package/dist/core/canvas/browse/render.js +178 -47
- package/dist/core/canvas/canvas.js +1 -1
- package/dist/core/canvas/focuses.d.ts +4 -4
- package/dist/core/canvas/focuses.js +7 -7
- package/dist/core/canvas/nav-model.d.ts +162 -0
- package/dist/core/canvas/nav-model.js +487 -0
- package/dist/core/canvas/paths.d.ts +7 -0
- package/dist/core/canvas/paths.js +9 -0
- package/dist/core/canvas/render.d.ts +68 -9
- package/dist/core/canvas/render.js +195 -61
- package/dist/core/canvas/types.d.ts +16 -23
- package/dist/core/config.js +1 -4
- package/dist/core/memory-resolver.js +43 -5
- package/dist/core/runtime/bearings.d.ts +1 -1
- package/dist/core/runtime/bearings.js +8 -8
- package/dist/core/runtime/branded-host.d.ts +16 -0
- package/dist/core/runtime/branded-host.js +127 -0
- package/dist/core/runtime/broker-protocol.d.ts +212 -6
- package/dist/core/runtime/broker-sdk.d.ts +0 -12
- package/dist/core/runtime/broker-sdk.js +77 -6
- package/dist/core/runtime/broker.d.ts +5 -1
- package/dist/core/runtime/broker.js +520 -59
- package/dist/core/runtime/close.d.ts +12 -2
- package/dist/core/runtime/close.js +37 -16
- package/dist/core/runtime/front-door.js +14 -7
- package/dist/core/runtime/host.d.ts +14 -34
- package/dist/core/runtime/host.js +14 -50
- package/dist/core/runtime/launch.d.ts +12 -9
- package/dist/core/runtime/launch.js +27 -15
- package/dist/core/runtime/lifecycle.d.ts +1 -1
- package/dist/core/runtime/lifecycle.js +15 -19
- package/dist/core/runtime/nodes.d.ts +0 -55
- package/dist/core/runtime/nodes.js +6 -74
- package/dist/core/runtime/placement.d.ts +91 -334
- package/dist/core/runtime/placement.js +262 -828
- package/dist/core/runtime/promote.d.ts +2 -0
- package/dist/core/runtime/promote.js +24 -6
- package/dist/core/runtime/recap.d.ts +8 -0
- package/dist/core/runtime/recap.js +107 -0
- package/dist/core/runtime/recycle.js +25 -61
- package/dist/core/runtime/reset.d.ts +43 -37
- package/dist/core/runtime/reset.js +131 -218
- package/dist/core/runtime/revive.d.ts +9 -29
- package/dist/core/runtime/revive.js +45 -135
- package/dist/core/runtime/spawn.d.ts +15 -11
- package/dist/core/runtime/spawn.js +168 -136
- package/dist/core/runtime/surface-bg.d.ts +11 -0
- package/dist/core/runtime/surface-bg.js +68 -0
- package/dist/core/runtime/tmux.d.ts +15 -43
- package/dist/core/runtime/tmux.js +38 -80
- package/dist/core/scope.d.ts +1 -2
- package/dist/core/scope.js +1 -5
- package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.d.ts +1 -0
- package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.js +57 -0
- package/dist/core/skill-sync/builtins.d.ts +42 -0
- package/dist/core/skill-sync/builtins.js +112 -0
- package/dist/core/skill-sync/claude-plugins.d.ts +23 -0
- package/dist/core/skill-sync/claude-plugins.js +71 -0
- package/dist/core/skill-sync/engine.d.ts +42 -0
- package/dist/core/skill-sync/engine.js +633 -0
- package/dist/core/skill-sync/export.d.ts +23 -0
- package/dist/core/skill-sync/export.js +86 -0
- package/dist/core/skill-sync/manifest.d.ts +64 -0
- package/dist/core/skill-sync/manifest.js +181 -0
- package/dist/core/skill-sync/profile.d.ts +76 -0
- package/dist/core/skill-sync/profile.js +173 -0
- package/dist/core/skill-sync/snapshot.d.ts +57 -0
- package/dist/core/skill-sync/snapshot.js +120 -0
- package/dist/core/spawn.d.ts +14 -0
- package/dist/core/spawn.js +29 -9
- package/dist/core/substrate/index.d.ts +2 -2
- package/dist/core/substrate/index.js +7 -7
- package/dist/core/substrate/injected-store.d.ts +10 -0
- package/dist/core/substrate/injected-store.js +55 -0
- package/dist/core/substrate/on-read.js +68 -6
- package/dist/core/substrate/render.d.ts +8 -11
- package/dist/core/substrate/render.js +29 -43
- package/dist/core/substrate/schema.d.ts +16 -11
- package/dist/core/substrate/schema.js +32 -31
- package/dist/core/tui/host.d.ts +12 -4
- package/dist/core/tui/host.js +280 -149
- package/dist/core/view/bridge.d.ts +10 -0
- package/dist/core/view/bridge.js +31 -0
- package/dist/core/view/chrome.d.ts +9 -0
- package/dist/core/view/chrome.js +22 -0
- package/dist/core/view/contract.d.ts +171 -0
- package/dist/core/view/contract.js +23 -0
- package/dist/core/view/loader.d.ts +31 -0
- package/dist/core/view/loader.js +188 -0
- package/dist/core/view/transport-local.d.ts +7 -0
- package/dist/core/view/transport-local.js +70 -0
- package/dist/core/view/transport.d.ts +4 -0
- package/dist/core/view/transport.js +15 -0
- package/dist/daemon/crtrd.d.ts +38 -27
- package/dist/daemon/crtrd.js +303 -376
- package/dist/daemon/manage.js +6 -1
- package/dist/index.js +1 -1
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +28 -20
- package/dist/pi-extensions/canvas-context-intro.js +4 -4
- package/dist/pi-extensions/canvas-doc-substrate.js +29 -20
- package/dist/pi-extensions/canvas-nav.js +35 -385
- package/dist/pi-extensions/canvas-recap.d.ts +37 -0
- package/dist/pi-extensions/canvas-recap.js +236 -0
- package/dist/pi-extensions/canvas-resume.js +2 -1
- package/dist/pi-extensions/canvas-stophook.d.ts +15 -13
- package/dist/pi-extensions/canvas-stophook.js +119 -103
- package/dist/pi-extensions/canvas-view.js +2 -1
- package/dist/pi-extensions/widget-order-bus.d.ts +6 -0
- package/dist/pi-extensions/widget-order-bus.js +34 -0
- package/dist/prompts/view.d.ts +2 -2
- package/dist/prompts/view.js +145 -73
- package/dist/types.d.ts +1 -6
- package/dist/types.js +1 -3
- package/dist/web/ViewChrome.d.ts +7 -0
- package/dist/web/ViewChrome.js +28 -0
- package/dist/web/ViewPane.d.ts +39 -0
- package/dist/web/ViewPane.js +48 -0
- package/dist/web/index.d.ts +6 -0
- package/dist/web/index.js +16 -0
- package/dist/web/runtime.d.ts +39 -0
- package/dist/web/runtime.js +133 -0
- package/dist/web/states.d.ts +24 -0
- package/dist/web/states.js +24 -0
- package/dist/web/transport-http.d.ts +5 -0
- package/dist/web/transport-http.js +28 -0
- package/dist/web-client/assets/index-BUvQb4hR.css +2 -0
- package/dist/web-client/assets/index-ClLQXYAE.js +10 -0
- package/dist/web-client/index.html +13 -0
- package/package.json +20 -6
- package/dist/builtin-views/canvas/client.mjs +0 -303
- package/dist/builtin-views/canvas/view.mjs +0 -576
- package/dist/builtin-views/git-pr/client.mjs +0 -440
- package/dist/builtin-views/git-pr/view.mjs +0 -675
- package/dist/builtin-views/inbox/sources/gmail.mjs +0 -965
- package/dist/builtin-views/inbox/sources/linkedin.mjs +0 -427
- package/dist/builtin-views/inbox/view.mjs +0 -889
- package/dist/builtin-views/linkedin/client.mjs +0 -610
- package/dist/builtin-views/linkedin/view.mjs +0 -1171
- package/dist/commands/pkg/bridge.d.ts +0 -1
- package/dist/commands/pkg/bridge.js +0 -137
- package/dist/commands/skill/author.d.ts +0 -3
- package/dist/commands/skill/author.js +0 -140
- package/dist/commands/skill/shared.d.ts +0 -3
- package/dist/commands/skill/shared.js +0 -19
- package/dist/commands/skill.d.ts +0 -2
- package/dist/commands/skill.js +0 -21
- package/dist/commands/tmux-spread.d.ts +0 -2
- package/dist/commands/tmux-spread.js +0 -144
- package/dist/core/__tests__/full/daemon-liveness-pane.full.test.js +0 -292
- package/dist/core/__tests__/full/placement-focus.test.js +0 -309
- package/dist/core/__tests__/full/placement-reconcile.test.js +0 -212
- package/dist/core/__tests__/full/placement-revive.test.js +0 -238
- package/dist/core/__tests__/full/placement-teardown.test.js +0 -221
- package/dist/core/__tests__/home-session.test.js +0 -186
- package/dist/core/__tests__/relaunch.test.js +0 -335
- package/dist/core/bridge-map.d.ts +0 -19
- package/dist/core/bridge-map.js +0 -73
- package/dist/core/tui/contract.d.ts +0 -83
- package/dist/core/tui/contract.js +0 -8
- package/dist/core/tui/loader.d.ts +0 -16
- package/dist/core/tui/loader.js +0 -94
- package/dist/prompts/skill.d.ts +0 -2
- package/dist/prompts/skill.js +0 -650
- /package/dist/{core/__tests__/full/daemon-liveness-pane.full.test.d.ts → clients/attach/__tests__/action-parity.test.d.ts} +0 -0
- /package/dist/{core/__tests__/full/placement-focus.test.d.ts → clients/attach/__tests__/clipboard-image.test.d.ts} +0 -0
- /package/dist/{core/__tests__/full/placement-reconcile.test.d.ts → clients/attach/__tests__/editor-newline.test.d.ts} +0 -0
- /package/dist/{core/__tests__/full/placement-revive.test.d.ts → clients/attach/__tests__/git-info.test.d.ts} +0 -0
- /package/dist/{core/__tests__/full/placement-teardown.test.d.ts → clients/attach/__tests__/onrequest-wired.test.d.ts} +0 -0
- /package/dist/{core/__tests__/home-session.test.d.ts → clients/attach/__tests__/reconnect-giveup.test.d.ts} +0 -0
- /package/dist/{core/__tests__/relaunch.test.d.ts → clients/attach/__tests__/slash-quit-copy.test.d.ts} +0 -0
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* Canvas Dashboard — the PORTABLE CORE of the crtr `canvas` view (manifest ·
|
|
4
|
+
* init · sources · intents). The READ-ONLY monitor of the live agent graph: one
|
|
5
|
+
* core renders in BOTH targets — the tmux TUI (`crtr view run canvas`, via
|
|
6
|
+
* `tui.mjs`) and the React+Tailwind web page (`crtr view serve canvas`, via
|
|
7
|
+
* `web.jsx`).
|
|
8
|
+
*
|
|
9
|
+
* Runs in BOTH Node and the browser, so it imports NOTHING — no `node:*`, no
|
|
10
|
+
* crtr. The data layer is expressed as transport-agnostic `Source` descriptors:
|
|
11
|
+
* the core describes WHAT to run (`request()` → a SourceRequest for
|
|
12
|
+
* `crtr … --json`), the host's Transport runs it (local `execFile` for the TUI,
|
|
13
|
+
* the HTTP bridge for web), and the pure `parse()` turns bytes → typed data | a
|
|
14
|
+
* typed `SourceError`. The forest building + relative-age / chrome-copy logic is
|
|
15
|
+
* pure string/data work that runs anywhere.
|
|
16
|
+
*
|
|
17
|
+
* It rebuilds the forest from each node's `parent` edge, keeps only trees that
|
|
18
|
+
* contain live (active/idle) or human-blocked work, and renders them as a
|
|
19
|
+
* scrollable ASCII tree (TUI) / DOM tree (web). It auto-polls (refreshMs) — no
|
|
20
|
+
* node-focus / close / swap (views are sessionless). j/k move a read cursor; g
|
|
21
|
+
* forces a refresh; q quits.
|
|
22
|
+
*
|
|
23
|
+
* NOTHING throws. Sources return a `Result<T>` (a typed `SourceError` on
|
|
24
|
+
* failure); the `refresh` intent KEEPS the last-known forest on a transient
|
|
25
|
+
* failure and raises the cause as a banner, dropping to a guided takeover only
|
|
26
|
+
* when there is nothing to keep (graceful partial failure).
|
|
27
|
+
*
|
|
28
|
+
* @module canvas/core
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @typedef {import('../../core/view/contract.js').SourceError} SourceError
|
|
33
|
+
* @typedef {import('../../core/view/contract.js').IntentCtx<CanvasState>} Ctx
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* One canvas node, flattened from `node inspect list --json`.
|
|
38
|
+
* @typedef {Object} CanvasNode
|
|
39
|
+
* @property {string} nodeId Full node id (`<time>-<hash>`).
|
|
40
|
+
* @property {string} name Display name.
|
|
41
|
+
* @property {string} kind Node kind (general/developer/explore/human/…).
|
|
42
|
+
* @property {string} mode base | orchestrator.
|
|
43
|
+
* @property {string} lifecycle resident | terminal.
|
|
44
|
+
* @property {string} status active | idle | done | dead | canceled.
|
|
45
|
+
* @property {string|null} parent Spawn/subscription parent id (null ⇒ a forest root).
|
|
46
|
+
* @property {string} created ISO 8601 birth timestamp (drives child ordering).
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* One cwd with pending human asks, from `canvas attention list --json`.
|
|
51
|
+
* @typedef {Object} AttentionItem
|
|
52
|
+
* @property {string} nodeId
|
|
53
|
+
* @property {string} name
|
|
54
|
+
* @property {string} cwd
|
|
55
|
+
* @property {number} count Pending ask count.
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* One rendered tree line (a flattened forest node). Width-independent, so it is
|
|
60
|
+
* built once per refresh and re-rendered on resize without a re-fetch.
|
|
61
|
+
* @typedef {Object} TreeRow
|
|
62
|
+
* @property {string} nodeId
|
|
63
|
+
* @property {string} prefix Tree-branch art (e.g. "│ └─ "); '' for a root.
|
|
64
|
+
* @property {string} glyph Status glyph.
|
|
65
|
+
* @property {string} status
|
|
66
|
+
* @property {string} name
|
|
67
|
+
* @property {string} kind
|
|
68
|
+
* @property {string} mode
|
|
69
|
+
* @property {string} lifecycle
|
|
70
|
+
* @property {string} shortId
|
|
71
|
+
* @property {string} created ISO 8601 birth timestamp (drives the right-flush age).
|
|
72
|
+
* @property {boolean} blocked True ⇒ this node has pending human asks.
|
|
73
|
+
* @property {number} askCount
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The view's immutable state (the core owns it; intents replace it via ctx.set).
|
|
78
|
+
* @typedef {Object} CanvasState
|
|
79
|
+
* @property {TreeRow[]} rows Flattened active-tree forest (render source).
|
|
80
|
+
* @property {number} cursor Read cursor into rows (j/k).
|
|
81
|
+
* @property {number} scroll draw.list scroll, stored back each frame.
|
|
82
|
+
* @property {number} shownRoots How many trees survived the active filter.
|
|
83
|
+
* @property {number} totalNodes Total nodes on the canvas (all statuses).
|
|
84
|
+
* @property {number} activeCount Nodes with status 'active' (canvas-wide).
|
|
85
|
+
* @property {number} attnTotal Total pending human asks (canvas-wide).
|
|
86
|
+
* @property {number} lastFetch Epoch ms of the last successful refresh.
|
|
87
|
+
* @property {SourceError|null} sourceError Non-null ⇒ the data source failed;
|
|
88
|
+
* presenters render its `display` VERBATIM (the not-ready takeover when there
|
|
89
|
+
* is no data, and the dump fallback). Cleared on a successful refresh.
|
|
90
|
+
*/
|
|
91
|
+
|
|
92
|
+
// ── Result helpers (inlined — the core imports nothing) ───────────────────────
|
|
93
|
+
|
|
94
|
+
/** @template T @param {T} data @returns {{ok:true, data:T}} */
|
|
95
|
+
function ok(data) {
|
|
96
|
+
return { ok: true, data };
|
|
97
|
+
}
|
|
98
|
+
/** @param {SourceError} error @returns {{ok:false, error:SourceError}} */
|
|
99
|
+
function fail(error) {
|
|
100
|
+
return { ok: false, error };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── Field mappers ───────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
/** @param {unknown} v @returns {string} */
|
|
106
|
+
function str(v) {
|
|
107
|
+
return v == null ? '' : String(v);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** @param {any} n @returns {CanvasNode} */
|
|
111
|
+
function toNode(n) {
|
|
112
|
+
const o = n || {};
|
|
113
|
+
return {
|
|
114
|
+
nodeId: str(o.node_id),
|
|
115
|
+
name: str(o.name) || '(unnamed)',
|
|
116
|
+
kind: str(o.kind) || '?',
|
|
117
|
+
mode: str(o.mode) || '?',
|
|
118
|
+
lifecycle: str(o.lifecycle) || '?',
|
|
119
|
+
status: str(o.status) || '?',
|
|
120
|
+
parent: o.parent ? str(o.parent) : null,
|
|
121
|
+
created: str(o.created),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** @param {any} i @returns {AttentionItem} */
|
|
126
|
+
function toAttention(i) {
|
|
127
|
+
const o = i || {};
|
|
128
|
+
return {
|
|
129
|
+
nodeId: str(o.node_id),
|
|
130
|
+
name: str(o.name),
|
|
131
|
+
cwd: str(o.cwd),
|
|
132
|
+
count: typeof o.count === 'number' ? o.count : 0,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ── Status vocabulary (mirrors core/canvas/browse/render.ts — single source) ──
|
|
137
|
+
|
|
138
|
+
const LIVE_STATUS = new Set(['active', 'idle']);
|
|
139
|
+
|
|
140
|
+
/** @type {Record<string,string>} */
|
|
141
|
+
export const STATUS_GLYPH = {
|
|
142
|
+
active: '●',
|
|
143
|
+
idle: '○',
|
|
144
|
+
done: '✓',
|
|
145
|
+
dead: '✗',
|
|
146
|
+
canceled: '⊘',
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/** @param {string} lifecycle @returns {string} */
|
|
150
|
+
export function lifeAbbr(lifecycle) {
|
|
151
|
+
if (lifecycle === 'resident') return 'res';
|
|
152
|
+
if (lifecycle === 'terminal') return 'term';
|
|
153
|
+
return lifecycle || '?';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** @param {string} id @returns {string} */
|
|
157
|
+
function shortId(id) {
|
|
158
|
+
const s = String(id || '');
|
|
159
|
+
const dash = s.indexOf('-');
|
|
160
|
+
return dash > 0 ? s.slice(0, dash) : s.slice(0, 8);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Relative-age ladder (design §5): `now` (<60s), `{m}m` (<60m), `{h}h` (<24h),
|
|
167
|
+
* `{d}d` (<7d), else a calendar date `Mon D` (`Mar 4`), prior-year `Mon ʼYY`.
|
|
168
|
+
* Max ~5 cols. Used for the right-flushed per-row age (the node's birth time).
|
|
169
|
+
* Shared by the TUI render + the text dump (both import it from here).
|
|
170
|
+
* @param {string} createdIso @param {number} now @returns {string}
|
|
171
|
+
*/
|
|
172
|
+
export function relAge(createdIso, now) {
|
|
173
|
+
const t = Date.parse(createdIso);
|
|
174
|
+
if (Number.isNaN(t)) return '';
|
|
175
|
+
const s = Math.max(0, Math.floor((now - t) / 1000));
|
|
176
|
+
if (s < 60) return 'now';
|
|
177
|
+
const m = Math.floor(s / 60);
|
|
178
|
+
if (m < 60) return `${m}m`;
|
|
179
|
+
const h = Math.floor(m / 60);
|
|
180
|
+
if (h < 24) return `${h}h`;
|
|
181
|
+
const d = Math.floor(h / 24);
|
|
182
|
+
if (d < 7) return `${d}d`;
|
|
183
|
+
const date = new Date(t);
|
|
184
|
+
const mon = MONTHS[date.getMonth()] || '?';
|
|
185
|
+
if (date.getFullYear() === new Date(now).getFullYear()) return `${mon} ${date.getDate()}`;
|
|
186
|
+
return `${mon} ʼ${String(date.getFullYear()).slice(-2)}`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** @param {number} n @param {string} w @returns {string} */
|
|
190
|
+
export function plural(n, w) {
|
|
191
|
+
return `${n} ${w}${n === 1 ? '' : 's'}`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ── Typed SourceError displays (the `display`/`kind` split; presenters render
|
|
195
|
+
// `display` verbatim and never branch on `kind`) ────────────────────────────
|
|
196
|
+
|
|
197
|
+
/** @type {SourceError} */
|
|
198
|
+
const CRTR_MISSING = {
|
|
199
|
+
kind: 'crtr-missing',
|
|
200
|
+
display: {
|
|
201
|
+
headline: 'Canvas unavailable',
|
|
202
|
+
explanation: 'crtr could not be found to read the canvas graph.',
|
|
203
|
+
nextStep: 'Install crtr (or set CRTR_BIN on PATH), then press g.',
|
|
204
|
+
level: 'error',
|
|
205
|
+
blocking: true,
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/** @param {string} stderr @returns {SourceError} */
|
|
210
|
+
function crtrFailed(stderr) {
|
|
211
|
+
const s = String(stderr || '');
|
|
212
|
+
if (/help-gate:\s*blocked/i.test(s)) {
|
|
213
|
+
return {
|
|
214
|
+
kind: 'help-gate',
|
|
215
|
+
display: {
|
|
216
|
+
headline: 'Canvas unavailable',
|
|
217
|
+
explanation: 'crtr help-gate blocked the call (run the command with -h once).',
|
|
218
|
+
nextStep: 'Press g to retry.',
|
|
219
|
+
level: 'error',
|
|
220
|
+
blocking: true,
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
kind: 'crtr-failed',
|
|
226
|
+
display: {
|
|
227
|
+
headline: 'Canvas unavailable',
|
|
228
|
+
explanation: 'crtr could not read the canvas graph: ' + (extractMessage(s) || 'unknown error'),
|
|
229
|
+
nextStep: 'Press g to retry.',
|
|
230
|
+
level: 'error',
|
|
231
|
+
blocking: true,
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** @param {string} cmd @returns {SourceError} */
|
|
237
|
+
function parseError(cmd) {
|
|
238
|
+
return {
|
|
239
|
+
kind: 'parse',
|
|
240
|
+
display: {
|
|
241
|
+
headline: 'Canvas unavailable',
|
|
242
|
+
explanation: `Could not parse \`crtr ${cmd}\` output as JSON.`,
|
|
243
|
+
nextStep: 'Press g to retry.',
|
|
244
|
+
level: 'error',
|
|
245
|
+
blocking: true,
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Pull a human message out of crtr stderr: prefer the last `ERROR:` line, else
|
|
252
|
+
* the last non-empty line, else ''.
|
|
253
|
+
* @param {string} stderr @returns {string}
|
|
254
|
+
*/
|
|
255
|
+
function extractMessage(stderr) {
|
|
256
|
+
const lines = String(stderr || '')
|
|
257
|
+
.split(/\r?\n/)
|
|
258
|
+
.map((l) => l.trim())
|
|
259
|
+
.filter(Boolean);
|
|
260
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
261
|
+
if (/^ERROR:/i.test(lines[i])) return lines[i].replace(/^ERROR:\s*/i, '').trim();
|
|
262
|
+
}
|
|
263
|
+
if (lines.length) return lines[lines.length - 1];
|
|
264
|
+
return '';
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ── Sources (reads): a request descriptor + a pure parse. The host's transport
|
|
268
|
+
// runs the request (local execFile for TUI, the HTTP bridge for web). The
|
|
269
|
+
// spawned `crtr` is a grandchild of the view host — not a pi tool call — so
|
|
270
|
+
// the help-gate never intercepts it. ────────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Every node on the canvas (all statuses). The view rebuilds the forest from the
|
|
274
|
+
* `parent` edges and filters to active trees itself. The PRIMARY instrument —
|
|
275
|
+
* its parse owns the hard failure (crtr missing / a failing crtr command).
|
|
276
|
+
* @type {import('../../core/view/contract.js').Source<CanvasNode[]>}
|
|
277
|
+
*/
|
|
278
|
+
export const nodesSource = {
|
|
279
|
+
id: 'nodes',
|
|
280
|
+
request: () => ({ kind: 'exec', bin: 'crtr', args: ['node', 'inspect', 'list', '--json'] }),
|
|
281
|
+
parse: (raw) => {
|
|
282
|
+
if (!raw.ok) return fail(CRTR_MISSING);
|
|
283
|
+
if (raw.exitCode !== 0) return fail(crtrFailed(raw.stderr || raw.stdout));
|
|
284
|
+
const out = String(raw.stdout || '').trim();
|
|
285
|
+
let data;
|
|
286
|
+
try {
|
|
287
|
+
data = out === '' ? {} : JSON.parse(out);
|
|
288
|
+
} catch {
|
|
289
|
+
return fail(parseError('node inspect list'));
|
|
290
|
+
}
|
|
291
|
+
const arr = data && Array.isArray(data.nodes) ? data.nodes : [];
|
|
292
|
+
return ok(arr.map(toNode));
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Pending human asks across the canvas (the "blocked on a human" signal).
|
|
298
|
+
* BEST-EFFORT: any failure degrades to no flags (the graph still renders), so a
|
|
299
|
+
* parse never blocks the monitor — it returns an empty set instead of an error.
|
|
300
|
+
* @type {import('../../core/view/contract.js').Source<{items:AttentionItem[], total:number}>}
|
|
301
|
+
*/
|
|
302
|
+
export const attentionSource = {
|
|
303
|
+
id: 'attention',
|
|
304
|
+
request: () => ({ kind: 'exec', bin: 'crtr', args: ['canvas', 'attention', 'list', '--json'] }),
|
|
305
|
+
parse: (raw) => {
|
|
306
|
+
if (!raw.ok || raw.exitCode !== 0) return ok({ items: [], total: 0 });
|
|
307
|
+
const out = String(raw.stdout || '').trim();
|
|
308
|
+
let data;
|
|
309
|
+
try {
|
|
310
|
+
data = out === '' ? {} : JSON.parse(out);
|
|
311
|
+
} catch {
|
|
312
|
+
return ok({ items: [], total: 0 });
|
|
313
|
+
}
|
|
314
|
+
const items = data && Array.isArray(data.items) ? data.items : [];
|
|
315
|
+
const total = data && typeof data.total === 'number' ? data.total : 0;
|
|
316
|
+
return ok({ items: items.map(toAttention), total });
|
|
317
|
+
},
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
// ── Forest builder ─────────────────────────────────────────────
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Build the flattened active-tree forest from a flat node list + the
|
|
324
|
+
* human-blocked id→count map.
|
|
325
|
+
*
|
|
326
|
+
* A tree is shown iff its subtree contains any LIVE node (status active|idle) OR
|
|
327
|
+
* any human-blocked node — so a fully-finished tree drops out, but a blocked one
|
|
328
|
+
* always surfaces. Shown trees are rendered IN FULL (finished children included)
|
|
329
|
+
* to preserve context, mirroring `crtr canvas dashboard`.
|
|
330
|
+
*
|
|
331
|
+
* @param {CanvasNode[]} nodes
|
|
332
|
+
* @param {Map<string,number>} blockedById
|
|
333
|
+
* @returns {{rows: TreeRow[], shownRoots: number}}
|
|
334
|
+
*/
|
|
335
|
+
export function buildForest(nodes, blockedById) {
|
|
336
|
+
/** @type {Map<string, CanvasNode>} */
|
|
337
|
+
const byId = new Map();
|
|
338
|
+
for (const n of nodes) byId.set(n.nodeId, n);
|
|
339
|
+
|
|
340
|
+
/** @type {Map<string, CanvasNode[]>} */
|
|
341
|
+
const children = new Map();
|
|
342
|
+
/** @type {CanvasNode[]} */
|
|
343
|
+
const roots = [];
|
|
344
|
+
for (const n of nodes) {
|
|
345
|
+
const parent = n.parent && byId.has(n.parent) ? n.parent : null;
|
|
346
|
+
if (parent) {
|
|
347
|
+
const arr = children.get(parent);
|
|
348
|
+
if (arr) arr.push(n);
|
|
349
|
+
else children.set(parent, [n]);
|
|
350
|
+
} else {
|
|
351
|
+
roots.push(n);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
// Order siblings + roots by birth time (matches spawn order).
|
|
355
|
+
const cmp = (/** @type {CanvasNode} */ a, /** @type {CanvasNode} */ b) =>
|
|
356
|
+
a.created < b.created ? -1 : a.created > b.created ? 1 : 0;
|
|
357
|
+
for (const arr of children.values()) arr.sort(cmp);
|
|
358
|
+
roots.sort(cmp);
|
|
359
|
+
|
|
360
|
+
// Subtree liveness, memoised + cycle-guarded (the graph is a forest, but the
|
|
361
|
+
// db is mutable — guard defensively).
|
|
362
|
+
/** @type {Map<string, boolean>} */
|
|
363
|
+
const liveMemo = new Map();
|
|
364
|
+
/** @param {string} id @param {Set<string>} guard @returns {boolean} */
|
|
365
|
+
function subtreeLive(id, guard) {
|
|
366
|
+
const memo = liveMemo.get(id);
|
|
367
|
+
if (memo !== undefined) return memo;
|
|
368
|
+
if (guard.has(id)) return false;
|
|
369
|
+
guard.add(id);
|
|
370
|
+
const node = byId.get(id);
|
|
371
|
+
let res = !!node && (LIVE_STATUS.has(node.status) || blockedById.has(id));
|
|
372
|
+
if (!res) {
|
|
373
|
+
for (const c of children.get(id) || []) {
|
|
374
|
+
if (subtreeLive(c.nodeId, guard)) {
|
|
375
|
+
res = true;
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
guard.delete(id);
|
|
381
|
+
liveMemo.set(id, res);
|
|
382
|
+
return res;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** @param {CanvasNode} node @param {string} prefix @returns {TreeRow} */
|
|
386
|
+
function makeRow(node, prefix) {
|
|
387
|
+
const askCount = blockedById.get(node.nodeId) || 0;
|
|
388
|
+
return {
|
|
389
|
+
nodeId: node.nodeId,
|
|
390
|
+
prefix,
|
|
391
|
+
glyph: STATUS_GLYPH[node.status] || '?',
|
|
392
|
+
status: node.status,
|
|
393
|
+
name: node.name,
|
|
394
|
+
kind: node.kind,
|
|
395
|
+
mode: node.mode,
|
|
396
|
+
lifecycle: node.lifecycle,
|
|
397
|
+
shortId: shortId(node.nodeId),
|
|
398
|
+
created: node.created,
|
|
399
|
+
blocked: askCount > 0,
|
|
400
|
+
askCount,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** @type {TreeRow[]} */
|
|
405
|
+
const rows = [];
|
|
406
|
+
/** @type {Set<string>} */
|
|
407
|
+
const visited = new Set();
|
|
408
|
+
/** @param {CanvasNode} node @param {string} indent @param {boolean} isLast @param {boolean} isRoot */
|
|
409
|
+
function walk(node, indent, isLast, isRoot) {
|
|
410
|
+
if (visited.has(node.nodeId)) return; // cycle guard
|
|
411
|
+
visited.add(node.nodeId);
|
|
412
|
+
const prefix = isRoot ? '' : indent + (isLast ? '└─ ' : '├─ ');
|
|
413
|
+
rows.push(makeRow(node, prefix));
|
|
414
|
+
const kids = children.get(node.nodeId) || [];
|
|
415
|
+
const childIndent = isRoot ? '' : indent + (isLast ? ' ' : '│ ');
|
|
416
|
+
for (let i = 0; i < kids.length; i++) {
|
|
417
|
+
walk(kids[i], childIndent, i === kids.length - 1, false);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
let shownRoots = 0;
|
|
422
|
+
for (const r of roots) {
|
|
423
|
+
if (!subtreeLive(r.nodeId, new Set())) continue;
|
|
424
|
+
shownRoots++;
|
|
425
|
+
walk(r, '', true, true);
|
|
426
|
+
}
|
|
427
|
+
return { rows, shownRoots };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// ── Chrome copy (subtitle / footer / dump; pure) ──────────────────────────────
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Live title subtitle — canvas-wide health (design §3 "a dim ` · <subtitle>`").
|
|
434
|
+
* `null` ⇒ no subtitle (the title leads alone) on an empty canvas.
|
|
435
|
+
* @param {CanvasState} state @returns {string|null}
|
|
436
|
+
*/
|
|
437
|
+
function subtitleFor(state) {
|
|
438
|
+
if (state.totalNodes === 0) return null;
|
|
439
|
+
return `${state.activeCount} active · ${plural(state.totalNodes, 'node')}`;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Footer status (left, transient) — the RENDERED forest scope, distinct from the
|
|
444
|
+
* canvas-wide subtitle. `null` ⇒ nothing (the empty/loading state speaks).
|
|
445
|
+
* @param {CanvasState} state @returns {string|null}
|
|
446
|
+
*/
|
|
447
|
+
function footerSummary(state) {
|
|
448
|
+
if (state.rows.length === 0) return null;
|
|
449
|
+
return `${plural(state.shownRoots, 'live tree')} · ${state.rows.length} shown`;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** @param {CanvasState} state @returns {string} */
|
|
453
|
+
export function dumpSummary(state) {
|
|
454
|
+
return (
|
|
455
|
+
`${plural(state.shownRoots, 'tree')} · ${state.rows.length} shown · ` +
|
|
456
|
+
`${state.activeCount} active · ${plural(state.attnTotal, 'ask')} · ` +
|
|
457
|
+
`${state.totalNodes} total`
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ── The portable core ──────────────────────────────────────────────────────────
|
|
462
|
+
|
|
463
|
+
/** @type {import('../../core/view/contract.js').ViewCore<CanvasState>} */
|
|
464
|
+
const core = {
|
|
465
|
+
manifest: {
|
|
466
|
+
id: 'canvas',
|
|
467
|
+
title: 'Canvas',
|
|
468
|
+
description: 'Live agent graph — who is working, who is blocked',
|
|
469
|
+
refreshMs: 3000,
|
|
470
|
+
},
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Cheap + synchronous initial state — NO slow fetch (the host paints a loading
|
|
474
|
+
* frame, then dispatches the first 'refresh').
|
|
475
|
+
* @returns {CanvasState}
|
|
476
|
+
*/
|
|
477
|
+
init() {
|
|
478
|
+
return {
|
|
479
|
+
rows: [],
|
|
480
|
+
cursor: 0,
|
|
481
|
+
scroll: 0,
|
|
482
|
+
shownRoots: 0,
|
|
483
|
+
totalNodes: 0,
|
|
484
|
+
activeCount: 0,
|
|
485
|
+
attnTotal: 0,
|
|
486
|
+
lastFetch: 0,
|
|
487
|
+
sourceError: null,
|
|
488
|
+
};
|
|
489
|
+
},
|
|
490
|
+
|
|
491
|
+
sources: { nodesSource, attentionSource },
|
|
492
|
+
|
|
493
|
+
intents: {
|
|
494
|
+
/**
|
|
495
|
+
* Pull the node graph + attention, rebuild the active-tree forest. Runs in
|
|
496
|
+
* the host's single-flight lane (launch, refreshMs, and the `refresh`
|
|
497
|
+
* keybind). A nodes failure KEEPS the last-known forest and raises the cause
|
|
498
|
+
* as a banner (the not-ready takeover only owns the screen when there is
|
|
499
|
+
* nothing to keep — see the presenters). Attention is best-effort: a failure
|
|
500
|
+
* still renders the graph, just without the blocked flags.
|
|
501
|
+
* @param {Ctx} ctx
|
|
502
|
+
*/
|
|
503
|
+
async refresh(ctx) {
|
|
504
|
+
ctx.signal.setStatus('Loading the canvas…');
|
|
505
|
+
|
|
506
|
+
const rn = await ctx.resolve(nodesSource);
|
|
507
|
+
if (!rn.ok) {
|
|
508
|
+
// Data source down. KEEP the last-known forest; store the typed error
|
|
509
|
+
// (drives the BLOCKED chip + the not-ready takeover when rows are empty)
|
|
510
|
+
// and raise the cause as the error banner.
|
|
511
|
+
ctx.set((s) => ({ ...s, sourceError: rn.error }));
|
|
512
|
+
ctx.signal.setStatus(null);
|
|
513
|
+
ctx.signal.setBanner(rn.error.display.explanation, rn.error.display.level);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
const nodes = rn.data;
|
|
517
|
+
|
|
518
|
+
/** @type {Map<string,number>} */
|
|
519
|
+
const blockedById = new Map();
|
|
520
|
+
let attnTotal = 0;
|
|
521
|
+
const ra = await ctx.resolve(attentionSource);
|
|
522
|
+
if (ra.ok) {
|
|
523
|
+
for (const it of ra.data.items) {
|
|
524
|
+
if (it.nodeId) blockedById.set(it.nodeId, it.count);
|
|
525
|
+
}
|
|
526
|
+
attnTotal = ra.data.total;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const built = buildForest(nodes, blockedById);
|
|
530
|
+
ctx.set((s) => {
|
|
531
|
+
/** @type {CanvasState} */
|
|
532
|
+
const next = {
|
|
533
|
+
...s,
|
|
534
|
+
sourceError: null,
|
|
535
|
+
rows: built.rows,
|
|
536
|
+
shownRoots: built.shownRoots,
|
|
537
|
+
totalNodes: nodes.length,
|
|
538
|
+
activeCount: nodes.filter((n) => n.status === 'active').length,
|
|
539
|
+
attnTotal,
|
|
540
|
+
lastFetch: Date.now(),
|
|
541
|
+
};
|
|
542
|
+
if (next.cursor >= next.rows.length) next.cursor = Math.max(0, next.rows.length - 1);
|
|
543
|
+
return next;
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
ctx.signal.setSubtitle(subtitleFor(ctx.state));
|
|
547
|
+
|
|
548
|
+
// Data-freshness → state chip. Pending human asks are the one thing that
|
|
549
|
+
// wants a human: raise an ACTION banner → the host derives the ATTENTION
|
|
550
|
+
// (yellow) chip. Otherwise clear → READY (green). (busy→working is
|
|
551
|
+
// automatic.)
|
|
552
|
+
if (attnTotal > 0) {
|
|
553
|
+
ctx.signal.setBanner(`${plural(attnTotal, 'ask')} waiting on a human — see the ⚑ rows`, 'action');
|
|
554
|
+
} else {
|
|
555
|
+
ctx.signal.clearBanner();
|
|
556
|
+
}
|
|
557
|
+
ctx.signal.setStatus(footerSummary(ctx.state));
|
|
558
|
+
},
|
|
559
|
+
|
|
560
|
+
/** @param {Ctx} ctx */
|
|
561
|
+
cursorDown: (ctx) => ctx.set((s) => ({ ...s, cursor: s.rows.length ? Math.min(s.rows.length - 1, s.cursor + 1) : 0 })),
|
|
562
|
+
/** @param {Ctx} ctx */
|
|
563
|
+
cursorUp: (ctx) => ctx.set((s) => ({ ...s, cursor: Math.max(0, s.cursor - 1) })),
|
|
564
|
+
/**
|
|
565
|
+
* Activate (open) a node. STANDALONE this just sets the read cursor to the
|
|
566
|
+
* node's row — the view stays a read-only monitor. In the WEB SHELL the
|
|
567
|
+
* `{nodeId}` payload is what the host's intent TAP observes to open that
|
|
568
|
+
* node's conversation pane (§5): the core never imports the shell and never
|
|
569
|
+
* knows whether anyone is listening; it always emits the intent, and the
|
|
570
|
+
* payload carries the referent. Unknown/missing id ⇒ no-op.
|
|
571
|
+
* @param {Ctx} ctx @param {{nodeId?: string}} [payload]
|
|
572
|
+
*/
|
|
573
|
+
activate: (ctx, payload) => {
|
|
574
|
+
const id = payload && payload.nodeId;
|
|
575
|
+
if (!id) return;
|
|
576
|
+
ctx.set((s) => {
|
|
577
|
+
const i = s.rows.findIndex((r) => r.nodeId === id);
|
|
578
|
+
return i >= 0 ? { ...s, cursor: i } : s;
|
|
579
|
+
});
|
|
580
|
+
},
|
|
581
|
+
/** @param {Ctx} ctx */
|
|
582
|
+
quit: (ctx) => ctx.signal.quit(),
|
|
583
|
+
},
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
export default core;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* Canvas Dashboard — the text presenter (`dump`) for the non-TTY / piped path of
|
|
4
|
+
* the `canvas` view (`crtr view run canvas | cat`). Node-only, no ANSI. A plain
|
|
5
|
+
* snapshot of the current state; the host threads its current banner via `ctx`.
|
|
6
|
+
*
|
|
7
|
+
* A data-source failure is a typed `SourceError` (we render its
|
|
8
|
+
* `display.explanation` VERBATIM, never branching on `kind`). The pure helpers
|
|
9
|
+
* `dump` shares with the TUI (relAge, lifeAbbr, dumpSummary) are imported from
|
|
10
|
+
* `core.mjs`.
|
|
11
|
+
*
|
|
12
|
+
* @module canvas/text
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { relAge, lifeAbbr, dumpSummary } from './core.mjs';
|
|
16
|
+
|
|
17
|
+
/** @typedef {import('./core.mjs').CanvasState} CanvasState */
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Plain-text snapshot for the non-TTY / piped path. No ANSI. The host threads
|
|
21
|
+
* its current banner via `ctx` so guidance (a source error / a pending-ask
|
|
22
|
+
* action) surfaces without the view mirroring it into state.
|
|
23
|
+
* @param {CanvasState} state
|
|
24
|
+
* @param {import('../../core/view/contract.js').DumpContext} [ctx]
|
|
25
|
+
* @returns {string}
|
|
26
|
+
*/
|
|
27
|
+
export function dump(state, ctx) {
|
|
28
|
+
/** @type {string[]} */
|
|
29
|
+
const lines = ['Canvas — live agent graph'];
|
|
30
|
+
const banner = ctx && ctx.banner ? ctx.banner : null;
|
|
31
|
+
if (banner) lines.push('', `[${banner.level}] ${banner.msg}`);
|
|
32
|
+
else if (state.sourceError) lines.push('', `[${state.sourceError.display.level}] ${state.sourceError.display.explanation}`);
|
|
33
|
+
|
|
34
|
+
lines.push('', dumpSummary(state), '');
|
|
35
|
+
|
|
36
|
+
if (state.rows.length === 0) {
|
|
37
|
+
lines.push(
|
|
38
|
+
state.sourceError
|
|
39
|
+
? '(canvas unavailable)'
|
|
40
|
+
: state.lastFetch === 0
|
|
41
|
+
? '(loading…)'
|
|
42
|
+
: state.totalNodes === 0
|
|
43
|
+
? '(no nodes on the canvas)'
|
|
44
|
+
: '(no active trees)',
|
|
45
|
+
);
|
|
46
|
+
return lines.join('\n');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
for (const r of state.rows) {
|
|
51
|
+
const blk = r.blocked ? ` ⚑${r.askCount}` : '';
|
|
52
|
+
const age = relAge(r.created, now);
|
|
53
|
+
lines.push(
|
|
54
|
+
`${r.prefix}${r.glyph} ${r.name} [${r.kind}/${r.mode}] ${lifeAbbr(r.lifecycle)} ${r.shortId} ${age}${blk}`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return lines.join('\n');
|
|
58
|
+
}
|