@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,621 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* Workspace Sidebar — the PORTABLE CORE of the `workspace-sidebar` view
|
|
4
|
+
* (manifest · init · sources · commands · intents). The left rail of
|
|
5
|
+
* `crtr workspace`: a narrow, cwd-scoped navigator with two sections —
|
|
6
|
+
*
|
|
7
|
+
* ⌗ this graph the subtree of the node the chat pane is attached to
|
|
8
|
+
* (its forest root), rendered as an ASCII tree.
|
|
9
|
+
* ↪ elsewhere the OTHER top-level nodes (forest roots) started in this
|
|
10
|
+
* cwd — root rows only, ⚑ inbox flags.
|
|
11
|
+
*
|
|
12
|
+
* Runs in BOTH Node (the tmux TUI, via `tui.mjs`) and the browser (the
|
|
13
|
+
* React+Tailwind page, via `web.jsx`), so it imports NOTHING — no `node:*`, no
|
|
14
|
+
* crtr. The data layer is expressed as transport-agnostic `Source`/`Command`
|
|
15
|
+
* descriptors: the core describes WHAT to run (`request()` → a SourceRequest for
|
|
16
|
+
* `crtr`/`tmux`), the host's Transport runs it (local `execFile` for the TUI,
|
|
17
|
+
* the HTTP bridge for web), and the pure `parse()` turns bytes → typed data. The
|
|
18
|
+
* forest model, cwd scoping, attention rollup, and cursor logic are pure string
|
|
19
|
+
* work that runs anywhere.
|
|
20
|
+
*
|
|
21
|
+
* The rail is a CONTROLLER in the TUI: the `open` intent shells
|
|
22
|
+
* `crtr node focus <id> --pane <chatPane>` (a `Command`), swapping the selected
|
|
23
|
+
* node into the OTHER pane. The chat pane + the attached node are resolved each
|
|
24
|
+
* refresh from the window's tmux panes (the `@crtr_node` tag attach self-sets,
|
|
25
|
+
* which survives the swap). On the web target there is no chat pane — the same
|
|
26
|
+
* rail renders as a plain styled list; `open` no-ops with a banner.
|
|
27
|
+
*
|
|
28
|
+
* NOTHING throws. Sources return a `Result<T>` (typed `SourceError` on a hard
|
|
29
|
+
* failure); `refresh` keeps the last-known rail on a transient failure.
|
|
30
|
+
*
|
|
31
|
+
* @module workspace-sidebar/core
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @typedef {import('../../core/view/contract.js').SourceError} SourceError
|
|
36
|
+
* @typedef {import('../../core/view/contract.js').IntentCtx<SidebarState>} Ctx
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
// ── Status vocabulary (single source: core/canvas/browse/render.ts) ───────────
|
|
40
|
+
|
|
41
|
+
const LIVE_STATUS = new Set(['active', 'idle']);
|
|
42
|
+
|
|
43
|
+
// ── Types ─────────────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* One canvas node, flattened from `node inspect list --json`.
|
|
47
|
+
* @typedef {Object} CanvasNode
|
|
48
|
+
* @property {string} nodeId
|
|
49
|
+
* @property {string} name
|
|
50
|
+
* @property {string} kind
|
|
51
|
+
* @property {string} mode
|
|
52
|
+
* @property {string} lifecycle
|
|
53
|
+
* @property {string} status active | idle | done | dead | canceled.
|
|
54
|
+
* @property {string} cwd Originating cwd — the workspace scope key.
|
|
55
|
+
* @property {string|null} parent Spawn/subscription parent id (null ⇒ a root).
|
|
56
|
+
* @property {string} created ISO 8601 birth timestamp (drives ordering).
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* One LOGICAL rail row (each presenter paints it in its own idiom). A node row
|
|
61
|
+
* is selectable; header/chrome rows are inert chrome.
|
|
62
|
+
* @typedef {{kind:'header', text:string}
|
|
63
|
+
* | {kind:'chrome', text:string}
|
|
64
|
+
* | {kind:'node', nodeId:string, name:string, status:string, prefix:string, attached:boolean, asks:number}} RailRow
|
|
65
|
+
*/
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The view's immutable state (the core owns it; intents replace it via ctx.set).
|
|
69
|
+
* @typedef {Object} SidebarState
|
|
70
|
+
* @property {string} cwd Workspace scope key (process.cwd()).
|
|
71
|
+
* @property {string} targetOverride Optional explicit chat pane id (options.target); '' if unset.
|
|
72
|
+
* @property {string} chatPane tmux pane id of the chat pane, resolved each refresh.
|
|
73
|
+
* @property {string|null} attachedNode Node the chat pane currently views (@crtr_node), or null.
|
|
74
|
+
* @property {string|null} currentRoot Forest root of `this graph`.
|
|
75
|
+
* @property {RailRow[]} rows Flattened render list (chrome + node rows).
|
|
76
|
+
* @property {number} cursor Index into `rows` (always lands on a node row).
|
|
77
|
+
* @property {number} scroll draw.list scroll, stored back each frame.
|
|
78
|
+
* @property {number} graphsHere Live forest roots in this cwd.
|
|
79
|
+
* @property {number} nodesHere Total nodes in this cwd.
|
|
80
|
+
* @property {number} asksHere Total pending asks across the rendered set.
|
|
81
|
+
* @property {number} lastFetch Epoch ms of the last successful refresh.
|
|
82
|
+
* @property {SourceError|null} srcError Non-null ⇒ the node source failed (typed).
|
|
83
|
+
*/
|
|
84
|
+
|
|
85
|
+
// ── Result helpers (inlined — the core imports nothing) ───────────────────────
|
|
86
|
+
|
|
87
|
+
/** @template T @param {T} data @returns {{ok:true, data:T}} */
|
|
88
|
+
function ok(data) {
|
|
89
|
+
return { ok: true, data };
|
|
90
|
+
}
|
|
91
|
+
/** @param {SourceError} error @returns {{ok:false, error:SourceError}} */
|
|
92
|
+
function fail(error) {
|
|
93
|
+
return { ok: false, error };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** @param {string} s @returns {string} */
|
|
97
|
+
function firstLine(s) {
|
|
98
|
+
const lines = String(s || '').split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
99
|
+
return lines.length ? lines[0] : '';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** @param {unknown} v @returns {string} */
|
|
103
|
+
function str(v) {
|
|
104
|
+
return v == null ? '' : String(v);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** @param {number} n @param {string} w @returns {string} */
|
|
108
|
+
export function plural(n, w) {
|
|
109
|
+
return `${n} ${w}${n === 1 ? '' : 's'}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** @param {any} n @returns {CanvasNode} */
|
|
113
|
+
function toNode(n) {
|
|
114
|
+
const o = n || {};
|
|
115
|
+
return {
|
|
116
|
+
nodeId: str(o.node_id),
|
|
117
|
+
name: str(o.name) || '(unnamed)',
|
|
118
|
+
kind: str(o.kind) || '?',
|
|
119
|
+
mode: str(o.mode) || '?',
|
|
120
|
+
lifecycle: str(o.lifecycle) || '?',
|
|
121
|
+
status: str(o.status) || '?',
|
|
122
|
+
cwd: str(o.cwd),
|
|
123
|
+
parent: o.parent ? str(o.parent) : null,
|
|
124
|
+
created: str(o.created),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── Typed SourceError displays (presenters render `display` verbatim) ─────────
|
|
129
|
+
|
|
130
|
+
/** @type {SourceError} */
|
|
131
|
+
const CRTR_MISSING = {
|
|
132
|
+
kind: 'crtr-missing',
|
|
133
|
+
display: {
|
|
134
|
+
headline: 'crtr unavailable',
|
|
135
|
+
explanation: 'The crtr binary was not found on PATH.',
|
|
136
|
+
nextStep: 'Install crtr (or set it on PATH), then press g.',
|
|
137
|
+
level: 'error',
|
|
138
|
+
blocking: true,
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
/** @param {string} msg @returns {SourceError} */
|
|
142
|
+
function crtrFailed(msg) {
|
|
143
|
+
return {
|
|
144
|
+
kind: 'crtr-failed',
|
|
145
|
+
display: {
|
|
146
|
+
headline: 'Canvas unavailable',
|
|
147
|
+
explanation: msg || 'crtr could not read the graph.',
|
|
148
|
+
nextStep: 'Press g to retry.',
|
|
149
|
+
level: 'error',
|
|
150
|
+
blocking: false,
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── Sources (reads): a request descriptor + a pure parse ──────────────────────
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Every node on the canvas (all statuses). The view filters to the workspace cwd
|
|
159
|
+
* and rebuilds the forest from `parent` edges itself. The PRIMARY instrument —
|
|
160
|
+
* its parse owns the two hard failures (crtr missing / a failing crtr command).
|
|
161
|
+
* @type {import('../../core/view/contract.js').Source<CanvasNode[]>}
|
|
162
|
+
*/
|
|
163
|
+
export const nodesSource = {
|
|
164
|
+
id: 'nodes',
|
|
165
|
+
request: () => ({ kind: 'exec', bin: 'crtr', args: ['node', 'inspect', 'list', '--json'] }),
|
|
166
|
+
parse: (raw) => {
|
|
167
|
+
if (!raw.ok) return fail(CRTR_MISSING);
|
|
168
|
+
if (raw.exitCode !== 0) return fail(crtrFailed(firstLine(raw.stderr || raw.stdout)));
|
|
169
|
+
const out = String(raw.stdout || '').trim();
|
|
170
|
+
let data;
|
|
171
|
+
try {
|
|
172
|
+
data = out === '' ? {} : JSON.parse(out);
|
|
173
|
+
} catch {
|
|
174
|
+
return fail(crtrFailed('could not parse `crtr node inspect list` output as JSON'));
|
|
175
|
+
}
|
|
176
|
+
const arr = data && Array.isArray(data.nodes) ? data.nodes : [];
|
|
177
|
+
return ok(arr.map(toNode));
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Per-node pending-ask counts for a visible set, in ONE pass: the current
|
|
183
|
+
* graph's whole sub-DAG (`--view <root>`) unioned with explicit `nodeIds` (the
|
|
184
|
+
* other roots in this cwd). BEST-EFFORT — any failure yields {} so the rail
|
|
185
|
+
* still renders without ⚑ flags.
|
|
186
|
+
* @type {import('../../core/view/contract.js').Source<Record<string,number>, {viewRoot:string|null, nodeIds:string[]}>}
|
|
187
|
+
*/
|
|
188
|
+
export const attentionSource = {
|
|
189
|
+
id: 'attention',
|
|
190
|
+
request: ({ viewRoot, nodeIds }) => {
|
|
191
|
+
const args = ['canvas', 'attention', 'map', '--json'];
|
|
192
|
+
if (viewRoot) args.push('--view', viewRoot);
|
|
193
|
+
const extra = (nodeIds || []).filter(Boolean);
|
|
194
|
+
if (extra.length) args.push('--nodes', extra.join(','));
|
|
195
|
+
return { kind: 'exec', bin: 'crtr', args };
|
|
196
|
+
},
|
|
197
|
+
parse: (raw) => {
|
|
198
|
+
if (!raw.ok || raw.exitCode !== 0) return ok({});
|
|
199
|
+
let data;
|
|
200
|
+
try {
|
|
201
|
+
data = JSON.parse(String(raw.stdout || '').trim() || '{}');
|
|
202
|
+
} catch {
|
|
203
|
+
return ok({});
|
|
204
|
+
}
|
|
205
|
+
const counts = data && data.counts && typeof data.counts === 'object' ? data.counts : {};
|
|
206
|
+
/** @type {Record<string, number>} */
|
|
207
|
+
const out = {};
|
|
208
|
+
for (const [k, v] of Object.entries(counts)) out[k] = typeof v === 'number' ? v : 0;
|
|
209
|
+
return ok(out);
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* The host's working directory — the workspace scope key the rail filters on.
|
|
215
|
+
* Resolved through the transport (which runs exec in the host's cwd) so it is
|
|
216
|
+
* uniform across BOTH targets: `process.cwd()` is a Node-only facility the
|
|
217
|
+
* browser can't read, so the web target would otherwise scope to '' and show an
|
|
218
|
+
* empty rail. BEST-EFFORT — a failure falls back to the init-time cwd.
|
|
219
|
+
* @type {import('../../core/view/contract.js').Source<string>}
|
|
220
|
+
*/
|
|
221
|
+
export const cwdSource = {
|
|
222
|
+
id: 'cwd',
|
|
223
|
+
request: () => ({ kind: 'exec', bin: 'pwd', args: [] }),
|
|
224
|
+
parse: (raw) => ok(raw.ok && raw.exitCode === 0 ? String(raw.stdout || '').trim() : ''),
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* The tmux panes in the rail's window, each with its `@crtr_node` tag. The chat
|
|
229
|
+
* pane the rail drives is the one attach has tagged (the tag survives the
|
|
230
|
+
* swap-pane `crtr node focus` does). BEST-EFFORT — no tmux / a failure yields []
|
|
231
|
+
* so the rail still renders (just without a chat-pane controller; the web target
|
|
232
|
+
* always lands here). The self-pane never carries `@crtr_node` (the rail tags
|
|
233
|
+
* `@crtr_view`), so "first pane with a node tag" is the chat pane without any
|
|
234
|
+
* self-exclusion.
|
|
235
|
+
* @type {import('../../core/view/contract.js').Source<Array<{pane:string, node:string}>>}
|
|
236
|
+
*/
|
|
237
|
+
export const panesSource = {
|
|
238
|
+
id: 'tmux-panes',
|
|
239
|
+
request: () => ({ kind: 'exec', bin: 'tmux', args: ['list-panes', '-F', '#{pane_id}\t#{@crtr_node}'] }),
|
|
240
|
+
parse: (raw) => {
|
|
241
|
+
if (!raw.ok || raw.exitCode !== 0) return ok([]);
|
|
242
|
+
/** @type {Array<{pane:string, node:string}>} */
|
|
243
|
+
const rows = [];
|
|
244
|
+
for (const line of String(raw.stdout || '').split(/\r?\n/)) {
|
|
245
|
+
if (!line) continue;
|
|
246
|
+
const tab = line.indexOf('\t');
|
|
247
|
+
const pane = (tab < 0 ? line : line.slice(0, tab)).trim();
|
|
248
|
+
const node = tab < 0 ? '' : line.slice(tab + 1).trim();
|
|
249
|
+
if (pane) rows.push({ pane, node });
|
|
250
|
+
}
|
|
251
|
+
return ok(rows);
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
// ── Commands (writes): invoked by an intent ───────────────────────────────────
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Focus a node into the chat pane (`crtr node focus <id> --pane <pane>`) — the
|
|
259
|
+
* swap-pane call the Alt+G graph overlay uses. The swap lands in the OTHER pane,
|
|
260
|
+
* so the rail neither waits on nor renders its result.
|
|
261
|
+
* @type {import('../../core/view/contract.js').Command<boolean, {nodeId:string, pane:string}>}
|
|
262
|
+
*/
|
|
263
|
+
export const focusCommand = {
|
|
264
|
+
id: 'focus',
|
|
265
|
+
request: ({ nodeId, pane }) => ({ kind: 'exec', bin: 'crtr', args: ['node', 'focus', nodeId, '--pane', pane] }),
|
|
266
|
+
parse: (raw) => ok(raw.ok && raw.exitCode === 0),
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
// ── Forest model (within the cwd-scoped node set; pure) ────────────────────────
|
|
270
|
+
|
|
271
|
+
/** @param {CanvasNode} a @param {CanvasNode} b @returns {number} */
|
|
272
|
+
function byBirth(a, b) {
|
|
273
|
+
return a.created < b.created ? -1 : a.created > b.created ? 1 : 0;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Build id→node, parent→children, and the root list for the cwd-scoped set. A
|
|
278
|
+
* node is a ROOT when it has no parent inside the set.
|
|
279
|
+
* @param {CanvasNode[]} mine
|
|
280
|
+
*/
|
|
281
|
+
function buildForest(mine) {
|
|
282
|
+
/** @type {Map<string, CanvasNode>} */
|
|
283
|
+
const byId = new Map();
|
|
284
|
+
for (const n of mine) byId.set(n.nodeId, n);
|
|
285
|
+
/** @type {Map<string, CanvasNode[]>} */
|
|
286
|
+
const children = new Map();
|
|
287
|
+
/** @type {CanvasNode[]} */
|
|
288
|
+
const roots = [];
|
|
289
|
+
for (const n of mine) {
|
|
290
|
+
const hasParent = n.parent && byId.has(n.parent);
|
|
291
|
+
if (hasParent) {
|
|
292
|
+
const arr = children.get(/** @type {string} */ (n.parent));
|
|
293
|
+
if (arr) arr.push(n);
|
|
294
|
+
else children.set(/** @type {string} */ (n.parent), [n]);
|
|
295
|
+
} else {
|
|
296
|
+
roots.push(n);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
for (const arr of children.values()) arr.sort(byBirth);
|
|
300
|
+
roots.sort(byBirth);
|
|
301
|
+
return { byId, children, roots };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Climb from `id` to its forest root within the set. Cycle-guarded.
|
|
306
|
+
* @param {string} id @param {Map<string, CanvasNode>} byId @returns {string}
|
|
307
|
+
*/
|
|
308
|
+
function climbRoot(id, byId) {
|
|
309
|
+
const seen = new Set();
|
|
310
|
+
let cur = id;
|
|
311
|
+
while (true) {
|
|
312
|
+
if (seen.has(cur)) return cur;
|
|
313
|
+
seen.add(cur);
|
|
314
|
+
const n = byId.get(cur);
|
|
315
|
+
if (!n || !n.parent || !byId.has(n.parent)) return cur;
|
|
316
|
+
cur = n.parent;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* @param {string} id @param {Map<string,CanvasNode[]>} children
|
|
322
|
+
* @param {Map<string,CanvasNode>} byId @param {Set<string>} [guard] @returns {boolean}
|
|
323
|
+
*/
|
|
324
|
+
function subtreeLive(id, children, byId, guard = new Set()) {
|
|
325
|
+
if (guard.has(id)) return false;
|
|
326
|
+
guard.add(id);
|
|
327
|
+
const n = byId.get(id);
|
|
328
|
+
let res = !!n && LIVE_STATUS.has(n.status);
|
|
329
|
+
if (!res) {
|
|
330
|
+
for (const c of children.get(id) || []) {
|
|
331
|
+
if (subtreeLive(c.nodeId, children, byId, guard)) {
|
|
332
|
+
res = true;
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
guard.delete(id);
|
|
338
|
+
return res;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ── Row builders (pure → logical RailRow[]) ───────────────────────────────────
|
|
342
|
+
|
|
343
|
+
/** @param {CanvasNode} n @param {string} prefix @param {boolean} attached @param {number} asks @returns {RailRow} */
|
|
344
|
+
function nodeRow(n, prefix, attached, asks) {
|
|
345
|
+
return { kind: 'node', nodeId: n.nodeId, name: n.name, status: n.status, prefix, attached, asks };
|
|
346
|
+
}
|
|
347
|
+
/** @param {string} text @returns {RailRow} */
|
|
348
|
+
function headerRow(text) {
|
|
349
|
+
return { kind: 'header', text };
|
|
350
|
+
}
|
|
351
|
+
/** @param {string} [text] @returns {RailRow} */
|
|
352
|
+
function chromeRow(text) {
|
|
353
|
+
return { kind: 'chrome', text: text || '' };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Build the cwd-scoped forest + pick `this graph`'s root. Pure. The caller
|
|
358
|
+
* (`refresh`) folds attention counts in and flattens the two sections, since
|
|
359
|
+
* those need the (async) attention source resolved first.
|
|
360
|
+
* @param {CanvasNode[]} mine
|
|
361
|
+
* @param {string|null} attachedNode
|
|
362
|
+
*/
|
|
363
|
+
function buildRail(mine, attachedNode) {
|
|
364
|
+
const { byId, children, roots } = buildForest(mine);
|
|
365
|
+
|
|
366
|
+
// `this graph` root: the attached node's root if known & present, else the
|
|
367
|
+
// newest LIVE root here, else the newest root, else none.
|
|
368
|
+
/** @type {string|null} */
|
|
369
|
+
let currentRoot = null;
|
|
370
|
+
if (attachedNode && byId.has(attachedNode)) {
|
|
371
|
+
currentRoot = climbRoot(attachedNode, byId);
|
|
372
|
+
} else {
|
|
373
|
+
const live = roots.filter((r) => subtreeLive(r.nodeId, children, byId));
|
|
374
|
+
const pool = live.length ? live : roots;
|
|
375
|
+
if (pool.length) currentRoot = pool[pool.length - 1].nodeId; // newest
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
byId, children, roots, currentRoot,
|
|
380
|
+
liveRoots: roots.filter((r) => subtreeLive(r.nodeId, children, byId)),
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ── Cursor model (skips chrome rows) ───────────────────────────────────────────
|
|
385
|
+
|
|
386
|
+
/** @param {RailRow[]} rows @returns {number[]} */
|
|
387
|
+
function selectableIdx(rows) {
|
|
388
|
+
/** @type {number[]} */
|
|
389
|
+
const out = [];
|
|
390
|
+
for (let i = 0; i < rows.length; i++) if (rows[i].kind === 'node') out.push(i);
|
|
391
|
+
return out;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Re-home the cursor after a rebuild: same nodeId if still present, else the
|
|
396
|
+
* attached node, else the first selectable row.
|
|
397
|
+
* @param {RailRow[]} rows @param {string|null} prevId @param {string|null} attachedNode @returns {number}
|
|
398
|
+
*/
|
|
399
|
+
function reanchor(rows, prevId, attachedNode) {
|
|
400
|
+
const sel = selectableIdx(rows);
|
|
401
|
+
if (sel.length === 0) return 0;
|
|
402
|
+
const want = prevId || attachedNode;
|
|
403
|
+
const found = want ? rows.findIndex((r) => r.kind === 'node' && r.nodeId === want) : -1;
|
|
404
|
+
return found >= 0 ? found : sel[0];
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** @param {SidebarState} s @param {1|-1} dir @returns {SidebarState} */
|
|
408
|
+
function moveCursor(s, dir) {
|
|
409
|
+
const sel = selectableIdx(s.rows);
|
|
410
|
+
if (sel.length === 0) return s;
|
|
411
|
+
const pos = sel.indexOf(s.cursor);
|
|
412
|
+
if (pos < 0) return { ...s, cursor: sel[0] };
|
|
413
|
+
const next = Math.max(0, Math.min(sel.length - 1, pos + dir));
|
|
414
|
+
return { ...s, cursor: sel[next] };
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ── The portable core ──────────────────────────────────────────────────────────
|
|
418
|
+
|
|
419
|
+
/** @type {import('../../core/view/contract.js').ViewCore<SidebarState>} */
|
|
420
|
+
const core = {
|
|
421
|
+
manifest: {
|
|
422
|
+
id: 'workspace-sidebar',
|
|
423
|
+
title: 'Workspace',
|
|
424
|
+
description: 'Left rail for `crtr workspace` — this graph + other agents in this cwd',
|
|
425
|
+
refreshMs: 2500,
|
|
426
|
+
},
|
|
427
|
+
|
|
428
|
+
/** Cheap + synchronous initial state — NO fetch. @param {Readonly<Record<string,string>>} opts @returns {SidebarState} */
|
|
429
|
+
init(opts) {
|
|
430
|
+
return {
|
|
431
|
+
cwd: typeof process !== 'undefined' && process.cwd ? process.cwd() : '',
|
|
432
|
+
targetOverride: String((opts && opts.target) || ''),
|
|
433
|
+
chatPane: '',
|
|
434
|
+
attachedNode: null,
|
|
435
|
+
currentRoot: null,
|
|
436
|
+
rows: [],
|
|
437
|
+
cursor: 0,
|
|
438
|
+
scroll: 0,
|
|
439
|
+
graphsHere: 0,
|
|
440
|
+
nodesHere: 0,
|
|
441
|
+
asksHere: 0,
|
|
442
|
+
lastFetch: 0,
|
|
443
|
+
srcError: null,
|
|
444
|
+
};
|
|
445
|
+
},
|
|
446
|
+
|
|
447
|
+
sources: { cwdSource, nodesSource, attentionSource, panesSource },
|
|
448
|
+
commands: { focusCommand },
|
|
449
|
+
|
|
450
|
+
intents: {
|
|
451
|
+
/**
|
|
452
|
+
* Resolve the chat pane + attached node from tmux, pull the node graph,
|
|
453
|
+
* scope to this cwd, and rebuild the two-section rail. Runs in the host's
|
|
454
|
+
* single-flight lane. A blocking node-source failure with no prior rail
|
|
455
|
+
* drops to a guided takeover; a transient failure KEEPS the last-known rail.
|
|
456
|
+
* @param {Ctx} ctx
|
|
457
|
+
*/
|
|
458
|
+
async refresh(ctx) {
|
|
459
|
+
ctx.signal.setStatus('Loading…');
|
|
460
|
+
|
|
461
|
+
// Resolve the chat pane (the @crtr_node-tagged sibling; the launch-time
|
|
462
|
+
// --target is the fallback when no pane is tagged yet) + its attached node.
|
|
463
|
+
const panes = await ctx.resolve(panesSource);
|
|
464
|
+
let chatPane = '';
|
|
465
|
+
/** @type {string|null} */
|
|
466
|
+
let attachedNode = null;
|
|
467
|
+
if (panes.ok) {
|
|
468
|
+
const tagged = panes.data.find((p) => p.node);
|
|
469
|
+
if (tagged) {
|
|
470
|
+
chatPane = tagged.pane;
|
|
471
|
+
attachedNode = tagged.node;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (!chatPane && ctx.state.targetOverride) chatPane = ctx.state.targetOverride;
|
|
475
|
+
|
|
476
|
+
// The workspace scope key — resolved through the transport (uniform across
|
|
477
|
+
// TUI + web), falling back to the init-time cwd.
|
|
478
|
+
const cwdR = await ctx.resolve(cwdSource);
|
|
479
|
+
const cwd = cwdR.ok && cwdR.data ? cwdR.data : ctx.state.cwd;
|
|
480
|
+
|
|
481
|
+
const rn = await ctx.resolve(nodesSource);
|
|
482
|
+
if (!rn.ok) {
|
|
483
|
+
const err = rn.error;
|
|
484
|
+
const hadRail = ctx.state.rows.length > 0;
|
|
485
|
+
const keep = !err.display.blocking && hadRail;
|
|
486
|
+
ctx.set((s) => {
|
|
487
|
+
/** @type {SidebarState} */
|
|
488
|
+
const next = { ...s, cwd, chatPane, attachedNode, srcError: err };
|
|
489
|
+
if (!keep) {
|
|
490
|
+
next.rows = [];
|
|
491
|
+
next.currentRoot = null;
|
|
492
|
+
next.graphsHere = 0;
|
|
493
|
+
next.nodesHere = 0;
|
|
494
|
+
next.asksHere = 0;
|
|
495
|
+
}
|
|
496
|
+
return next;
|
|
497
|
+
});
|
|
498
|
+
// A takeover (empty rail) owns the rect + names the cause; a kept rail
|
|
499
|
+
// raises the cause as a banner.
|
|
500
|
+
if (keep) ctx.signal.setBanner(err.display.explanation, err.display.level);
|
|
501
|
+
else ctx.signal.clearBanner();
|
|
502
|
+
ctx.signal.setStatus(null);
|
|
503
|
+
ctx.signal.setSubtitle(null);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const mine = rn.data.filter((n) => n.cwd === cwd);
|
|
508
|
+
const { byId, children, roots, currentRoot, liveRoots } = buildRail(mine, attachedNode);
|
|
509
|
+
|
|
510
|
+
// Attention in one pass: the current sub-DAG + EVERY root here (so a
|
|
511
|
+
// blocked-but-not-live root still surfaces its ⚑). Best-effort.
|
|
512
|
+
const rootIds = roots.map((r) => r.nodeId);
|
|
513
|
+
let asks = /** @type {Record<string,number>} */ ({});
|
|
514
|
+
if (currentRoot || rootIds.length) {
|
|
515
|
+
const a = await ctx.resolve(attentionSource, { viewRoot: currentRoot, nodeIds: rootIds });
|
|
516
|
+
if (a.ok) asks = a.data;
|
|
517
|
+
}
|
|
518
|
+
let asksHere = 0;
|
|
519
|
+
for (const v of Object.values(asks)) asksHere += v;
|
|
520
|
+
|
|
521
|
+
// `elsewhere` = other roots still LIVE OR with a pending ask. Live first,
|
|
522
|
+
// then newest.
|
|
523
|
+
const others = roots.filter(
|
|
524
|
+
(r) => r.nodeId !== currentRoot && (subtreeLive(r.nodeId, children, byId) || (asks[r.nodeId] || 0) > 0),
|
|
525
|
+
);
|
|
526
|
+
others.sort((a, b) => {
|
|
527
|
+
const la = subtreeLive(a.nodeId, children, byId) ? 1 : 0;
|
|
528
|
+
const lb = subtreeLive(b.nodeId, children, byId) ? 1 : 0;
|
|
529
|
+
if (la !== lb) return lb - la;
|
|
530
|
+
return a.created < b.created ? 1 : -1; // newest first
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
// ── Build the flat render list ───────────────────────────────────────
|
|
534
|
+
/** @type {RailRow[]} */
|
|
535
|
+
const rows = [];
|
|
536
|
+
rows.push(headerRow('⌗ this graph'));
|
|
537
|
+
if (currentRoot && byId.has(currentRoot)) {
|
|
538
|
+
/** @type {Set<string>} */
|
|
539
|
+
const visited = new Set();
|
|
540
|
+
/** @param {CanvasNode} node @param {string} indent @param {boolean} isLast @param {boolean} isRoot */
|
|
541
|
+
const walk = (node, indent, isLast, isRoot) => {
|
|
542
|
+
if (visited.has(node.nodeId)) return;
|
|
543
|
+
visited.add(node.nodeId);
|
|
544
|
+
const prefix = isRoot ? '' : indent + (isLast ? '└─ ' : '├─ ');
|
|
545
|
+
rows.push(nodeRow(node, prefix, node.nodeId === attachedNode, asks[node.nodeId] || 0));
|
|
546
|
+
const kids = children.get(node.nodeId) || [];
|
|
547
|
+
const childIndent = isRoot ? '' : indent + (isLast ? ' ' : '│ ');
|
|
548
|
+
for (let i = 0; i < kids.length; i++) walk(kids[i], childIndent, i === kids.length - 1, false);
|
|
549
|
+
};
|
|
550
|
+
walk(/** @type {CanvasNode} */ (byId.get(currentRoot)), '', true, true);
|
|
551
|
+
} else {
|
|
552
|
+
rows.push(chromeRow(' no agent here yet'));
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
rows.push(chromeRow('')); // section gap
|
|
556
|
+
rows.push(headerRow(`↪ elsewhere${others.length ? ` · ${others.length}` : ''}`));
|
|
557
|
+
if (others.length) {
|
|
558
|
+
for (const r of others) rows.push(nodeRow(r, '', false, asks[r.nodeId] || 0));
|
|
559
|
+
} else {
|
|
560
|
+
rows.push(chromeRow(' —'));
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const prevRow = ctx.state.rows[ctx.state.cursor];
|
|
564
|
+
const cursor = reanchor(rows, prevRow && prevRow.kind === 'node' ? prevRow.nodeId : null, attachedNode);
|
|
565
|
+
|
|
566
|
+
ctx.set((s) => ({
|
|
567
|
+
...s,
|
|
568
|
+
cwd,
|
|
569
|
+
chatPane,
|
|
570
|
+
attachedNode,
|
|
571
|
+
currentRoot,
|
|
572
|
+
rows,
|
|
573
|
+
cursor,
|
|
574
|
+
graphsHere: liveRoots.length,
|
|
575
|
+
nodesHere: mine.length,
|
|
576
|
+
asksHere,
|
|
577
|
+
lastFetch: Date.now(),
|
|
578
|
+
srcError: null,
|
|
579
|
+
}));
|
|
580
|
+
|
|
581
|
+
ctx.signal.setSubtitle(liveRoots.length > 0 ? `${plural(liveRoots.length, 'live graph')}` : null);
|
|
582
|
+
if (asksHere > 0) ctx.signal.setBanner(`${plural(asksHere, 'ask')} waiting on a human — see the ⚑ rows`, 'action');
|
|
583
|
+
else ctx.signal.clearBanner();
|
|
584
|
+
ctx.signal.setStatus(null);
|
|
585
|
+
},
|
|
586
|
+
|
|
587
|
+
/** @param {Ctx} ctx */
|
|
588
|
+
cursorDown: (ctx) => ctx.set((s) => moveCursor(s, 1)),
|
|
589
|
+
/** @param {Ctx} ctx */
|
|
590
|
+
cursorUp: (ctx) => ctx.set((s) => moveCursor(s, -1)),
|
|
591
|
+
/** @param {Ctx} ctx @param {number} [i] */
|
|
592
|
+
select: (ctx, i) =>
|
|
593
|
+
ctx.set((s) => {
|
|
594
|
+
if (typeof i !== 'number') return s;
|
|
595
|
+
return selectableIdx(s.rows).includes(i) ? { ...s, cursor: i } : s;
|
|
596
|
+
}),
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Swap the cursor's node into the chat pane (TUI ↵). No chat pane wired (the
|
|
600
|
+
* web target, or a rail not launched via `crtr workspace`) ⇒ a guidance
|
|
601
|
+
* banner, no-op.
|
|
602
|
+
* @param {Ctx} ctx
|
|
603
|
+
*/
|
|
604
|
+
async open(ctx) {
|
|
605
|
+
const s = ctx.state;
|
|
606
|
+
const row = s.rows[s.cursor];
|
|
607
|
+
if (!row || row.kind !== 'node') return;
|
|
608
|
+
if (!s.chatPane) {
|
|
609
|
+
ctx.signal.setBanner('No chat pane wired — run this rail via `crtr workspace`.', 'error');
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
await ctx.execute(focusCommand, { nodeId: row.nodeId, pane: s.chatPane });
|
|
613
|
+
ctx.signal.setStatus(`→ ${row.name}`);
|
|
614
|
+
},
|
|
615
|
+
|
|
616
|
+
/** @param {Ctx} ctx */
|
|
617
|
+
quit: (ctx) => ctx.signal.quit(),
|
|
618
|
+
},
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
export default core;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* Workspace Sidebar — the text presenter (`dump`) for the non-TTY / piped path
|
|
4
|
+
* (`crtr view run workspace-sidebar | cat`). Node-only, no ANSI. A plain
|
|
5
|
+
* snapshot of the current state; the host threads its current banner via `ctx`.
|
|
6
|
+
*
|
|
7
|
+
* `dump` reads the SAME logical `RailRow[]` the TUI/web presenters do. The pure
|
|
8
|
+
* `plural` helper is imported from `core.mjs`.
|
|
9
|
+
*
|
|
10
|
+
* @module workspace-sidebar/text
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { plural } from './core.mjs';
|
|
14
|
+
|
|
15
|
+
/** @typedef {import('./core.mjs').SidebarState} SidebarState */
|
|
16
|
+
/** @typedef {import('./core.mjs').RailRow} RailRow */
|
|
17
|
+
|
|
18
|
+
const STATUS_GLYPH = { active: '●', idle: '○', done: '✓', dead: '✗', canceled: '⊘' };
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Plain-text snapshot for the non-TTY / piped path.
|
|
22
|
+
* @param {SidebarState} state
|
|
23
|
+
* @param {import('../../core/view/contract.js').DumpContext} [ctx]
|
|
24
|
+
* @returns {string}
|
|
25
|
+
*/
|
|
26
|
+
export function dump(state, ctx) {
|
|
27
|
+
/** @type {string[]} */
|
|
28
|
+
const lines = ['Workspace sidebar'];
|
|
29
|
+
const banner = ctx && ctx.banner ? ctx.banner : null;
|
|
30
|
+
if (banner) lines.push('', `[${banner.level}] ${banner.msg}`);
|
|
31
|
+
else if (state.srcError) lines.push('', `[${state.srcError.display.level}] ${state.srcError.display.explanation}`);
|
|
32
|
+
|
|
33
|
+
lines.push('', `${plural(state.graphsHere, 'graph')} · ${state.nodesHere} nodes · ${plural(state.asksHere, 'ask')}`, '');
|
|
34
|
+
|
|
35
|
+
if (state.rows.length === 0) {
|
|
36
|
+
lines.push(state.srcError ? '(canvas unavailable)' : state.lastFetch === 0 ? '(loading…)' : '(no agents in this cwd)');
|
|
37
|
+
return lines.join('\n');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
for (const r of state.rows) {
|
|
41
|
+
if (r.kind === 'header') {
|
|
42
|
+
lines.push(r.text);
|
|
43
|
+
} else if (r.kind === 'chrome') {
|
|
44
|
+
lines.push(r.text);
|
|
45
|
+
} else {
|
|
46
|
+
const mark = r.attached ? '▸ ' : ' ';
|
|
47
|
+
const glyph = STATUS_GLYPH[r.status] || '?';
|
|
48
|
+
const flag = r.asks > 0 ? ` ⚑${r.asks}` : '';
|
|
49
|
+
lines.push(`${mark}${r.prefix}${glyph} ${r.name}${flag}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return lines.join('\n');
|
|
53
|
+
}
|