@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,487 @@
|
|
|
1
|
+
// nav-model.ts — the pure model/render layer of the canvas graph-nav chrome.
|
|
2
|
+
//
|
|
3
|
+
// Extracted from src/pi-extensions/canvas-nav.ts so a SINGLE source feeds both
|
|
4
|
+
// the legacy pi extension (canvas-nav) and the headless `crtr attach` viewer.
|
|
5
|
+
// Everything here is pi-API-free: it reads the canvas (sqlite + meta.json + node
|
|
6
|
+
// telemetry) and produces ANSI-styled strings + a flattened graph model. The
|
|
7
|
+
// extension-host wiring (widgets, key taps, the ask-poll timer, run/focus
|
|
8
|
+
// shelling) stays in canvas-nav.ts and drives these functions.
|
|
9
|
+
//
|
|
10
|
+
// Mutable interaction state that the legacy extension held at module scope —
|
|
11
|
+
// the per-node ask counts and the manual fold overrides — is threaded through
|
|
12
|
+
// parameters here (see `asks` and `FoldState`) so this layer owns no singletons
|
|
13
|
+
// and each consumer keeps its own state.
|
|
14
|
+
import { execFileSync } from 'node:child_process';
|
|
15
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { getNode, subscribersOf, subscriptionsOf, jobDir, listFocuses } from './index.js';
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Tuning constants
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
/** pi's InteractiveMode.MAX_WIDGET_LINES — the hard cap on lines in a string
|
|
22
|
+
* array widget; anything beyond it pi truncates with its own "... (widget
|
|
23
|
+
* truncated)". Our GRAPH viewport stays at/under this and scrolls internally. */
|
|
24
|
+
export const PI_MAX_WIDGET_LINES = 10;
|
|
25
|
+
export const VIEWPORT_FALLBACK_ROWS = 30;
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// ANSI styling. pi renders embedded escapes in widget lines and measures width
|
|
28
|
+
// ANSI-aware, so raw escapes are safe and need no pi-tui dependency. The cursor
|
|
29
|
+
// (selected row) uses a theme-agnostic ATTRIBUTE (reverse), so it reads under
|
|
30
|
+
// NO_COLOR; the attached-row tint is a background COLOUR, while the dot glyph
|
|
31
|
+
// (●/○/✓/✗) carries the running signal independently, even where colour is
|
|
32
|
+
// stripped.
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
export const ESC = '\x1b[';
|
|
35
|
+
export const RESET = `${ESC}0m`;
|
|
36
|
+
export const BOLD = `${ESC}1m`;
|
|
37
|
+
export const DIM = `${ESC}2m`;
|
|
38
|
+
export const REVERSE = `${ESC}7m`;
|
|
39
|
+
/** Dark-green background bar marking an ATTACHED node (a human is currently
|
|
40
|
+
* watching it) — distinct from the cursor's reverse-video bar; chosen so
|
|
41
|
+
* default-fg text stays readable. */
|
|
42
|
+
export const BG_ATTACHED = `${ESC}48;5;22m`;
|
|
43
|
+
export const GREEN = `${ESC}32m`;
|
|
44
|
+
export const RED = `${ESC}31m`;
|
|
45
|
+
export const YELLOW = `${ESC}33m`;
|
|
46
|
+
export const CYAN = `${ESC}36m`;
|
|
47
|
+
export const GRAY = `${ESC}90m`;
|
|
48
|
+
/** Status glyph colored by state: active green, idle dim, done cyan, dead red. */
|
|
49
|
+
export function coloredGlyph(node) {
|
|
50
|
+
if (node === null)
|
|
51
|
+
return '?';
|
|
52
|
+
switch (node.status) {
|
|
53
|
+
case 'active': return `${GREEN}●${RESET}`;
|
|
54
|
+
case 'idle': return `${GRAY}○${RESET}`;
|
|
55
|
+
case 'done': return `${CYAN}✓${RESET}`;
|
|
56
|
+
case 'dead': return `${RED}✗${RESET}`;
|
|
57
|
+
case 'canceled': return `${YELLOW}⊘${RESET}`;
|
|
58
|
+
default: return '?';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
62
|
+
/** Visible width, ignoring ANSI escapes. */
|
|
63
|
+
export function visibleWidth(s) {
|
|
64
|
+
return s.replace(ANSI_RE, '').length;
|
|
65
|
+
}
|
|
66
|
+
/** Truncate to `max` VISIBLE columns: escape sequences are copied through
|
|
67
|
+
* verbatim (so a cut never lands mid-escape) and the result always ends in
|
|
68
|
+
* RESET, so a clipped style can't bleed into the editor below. */
|
|
69
|
+
export function truncate(s, max = fillWidth()) {
|
|
70
|
+
if (visibleWidth(s) <= max)
|
|
71
|
+
return s;
|
|
72
|
+
let out = '';
|
|
73
|
+
let w = 0;
|
|
74
|
+
let i = 0;
|
|
75
|
+
while (i < s.length && w < max - 1) {
|
|
76
|
+
if (s[i] === '\x1b') {
|
|
77
|
+
const m = /^\x1b\[[0-9;]*m/.exec(s.slice(i));
|
|
78
|
+
if (m) {
|
|
79
|
+
out += m[0];
|
|
80
|
+
i += m[0].length;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
out += s[i];
|
|
85
|
+
w++;
|
|
86
|
+
i++;
|
|
87
|
+
}
|
|
88
|
+
return `${out}…${RESET}`;
|
|
89
|
+
}
|
|
90
|
+
/** Visible columns available to ONE widget line — the cap for every line we
|
|
91
|
+
* emit, and the width a full-width reverse-video SELF bar fills to.
|
|
92
|
+
*
|
|
93
|
+
* pi does NOT clip widget lines; it WRAPS them. Each string line is wrapped in
|
|
94
|
+
* a `Text(paddingX = 1)` inside a full-terminal-width container, so the usable
|
|
95
|
+
* content width is `columns - 2` (a 1-col margin on each side). A line wider
|
|
96
|
+
* than that wraps, and the overflow spills onto a second row as a stray
|
|
97
|
+
* reverse-video block (the bug this guards against). Clamp to `columns - 2`. */
|
|
98
|
+
export function fillWidth() {
|
|
99
|
+
return Math.max(20, Math.min((process.stdout.columns ?? 80) - 2, 180));
|
|
100
|
+
}
|
|
101
|
+
/** Wrap `content` in a full-width background bar opened by `open` (REVERSE for
|
|
102
|
+
* the cursor, BG_ATTACHED for a human-watched node). `open` is re-asserted after every
|
|
103
|
+
* embedded RESET so a coloured cell (the status dot) can't punch a hole in the
|
|
104
|
+
* bar; the visible width is padded out to `width`; the line closes with a real
|
|
105
|
+
* RESET so the style never bleeds into the editor below. */
|
|
106
|
+
export function fillBar(content, width, open) {
|
|
107
|
+
const clipped = truncate(content, width);
|
|
108
|
+
const reasserted = clipped.replace(/\x1b\[0m/g, `${RESET}${open}`);
|
|
109
|
+
const pad = Math.max(0, width - visibleWidth(clipped));
|
|
110
|
+
return `${open}${reasserted}${' '.repeat(pad)}${RESET}`;
|
|
111
|
+
}
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// Per-frame read cache
|
|
114
|
+
//
|
|
115
|
+
// One render fans the SAME handful of nodes out 5-10× each: computeSubtreeActivity
|
|
116
|
+
// walks the whole tree (getNode + subscriptions per node), buildGraphModel walks it
|
|
117
|
+
// again (climbRoot, sortedChildIds), then every visible row re-reads getNode +
|
|
118
|
+
// telemetry + subscriptions for its badges. getNode alone is an fs.readFileSync of
|
|
119
|
+
// meta.json plus a sqlite row read, and readTelemetry another readFileSync — so on a
|
|
120
|
+
// modest graph a single 'j' keystroke fanned out to DOZENS of redundant disk reads.
|
|
121
|
+
// That redundancy is the GRAPH view's open/scroll lag.
|
|
122
|
+
//
|
|
123
|
+
// Each render is a synchronous snapshot, so we memoize the read primitives for the
|
|
124
|
+
// duration of one frame and clear at the top of render(): the next keystroke (or the
|
|
125
|
+
// ask-poll / inbox render) re-reads fresh, so a killed node, a new spawn, or a status
|
|
126
|
+
// flip all surface on the very next frame. Nothing here writes, so a stale entry can
|
|
127
|
+
// at worst be one frame old — and render() always reruns immediately after any action.
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
const frameNodes = new Map();
|
|
130
|
+
const frameTelem = new Map();
|
|
131
|
+
const frameSubs = new Map();
|
|
132
|
+
const frameMgrs = new Map();
|
|
133
|
+
export function beginFrame() {
|
|
134
|
+
frameNodes.clear();
|
|
135
|
+
frameTelem.clear();
|
|
136
|
+
frameSubs.clear();
|
|
137
|
+
frameMgrs.clear();
|
|
138
|
+
}
|
|
139
|
+
/** getNode memoized for the current frame (meta.json read + sqlite row). */
|
|
140
|
+
export function cNode(id) {
|
|
141
|
+
if (frameNodes.has(id))
|
|
142
|
+
return frameNodes.get(id) ?? null;
|
|
143
|
+
const v = getNode(id);
|
|
144
|
+
frameNodes.set(id, v);
|
|
145
|
+
return v;
|
|
146
|
+
}
|
|
147
|
+
/** subscriptionsOf (a node's reports) memoized for the current frame. */
|
|
148
|
+
export function cSubscriptions(id) {
|
|
149
|
+
const hit = frameSubs.get(id);
|
|
150
|
+
if (hit !== undefined)
|
|
151
|
+
return hit;
|
|
152
|
+
let v;
|
|
153
|
+
try {
|
|
154
|
+
v = subscriptionsOf(id);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
v = [];
|
|
158
|
+
}
|
|
159
|
+
frameSubs.set(id, v);
|
|
160
|
+
return v;
|
|
161
|
+
}
|
|
162
|
+
/** subscribersOf (a node's managers) memoized for the current frame. */
|
|
163
|
+
export function cSubscribers(id) {
|
|
164
|
+
const hit = frameMgrs.get(id);
|
|
165
|
+
if (hit !== undefined)
|
|
166
|
+
return hit;
|
|
167
|
+
let v;
|
|
168
|
+
try {
|
|
169
|
+
v = subscribersOf(id);
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
v = [];
|
|
173
|
+
}
|
|
174
|
+
frameMgrs.set(id, v);
|
|
175
|
+
return v;
|
|
176
|
+
}
|
|
177
|
+
export function readTelemetry(nodeId) {
|
|
178
|
+
const hit = frameTelem.get(nodeId);
|
|
179
|
+
if (hit !== undefined)
|
|
180
|
+
return hit;
|
|
181
|
+
let v;
|
|
182
|
+
try {
|
|
183
|
+
const p = join(jobDir(nodeId), 'telemetry.json');
|
|
184
|
+
v = existsSync(p) ? JSON.parse(readFileSync(p, 'utf8')) : {};
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
v = {};
|
|
188
|
+
}
|
|
189
|
+
frameTelem.set(nodeId, v);
|
|
190
|
+
return v;
|
|
191
|
+
}
|
|
192
|
+
export function fmtTokens(n) {
|
|
193
|
+
return n < 1_000 ? `${n}` : `${Math.round(n / 1_000)}k`;
|
|
194
|
+
}
|
|
195
|
+
/** The context-window cell — live window fill rounded to the nearest 1k (the same
|
|
196
|
+
* figure pi's footer shows), NOT a per-turn token delta. */
|
|
197
|
+
export function tokensCell(id) {
|
|
198
|
+
return fmtTokens(readTelemetry(id).context_tokens ?? 0);
|
|
199
|
+
}
|
|
200
|
+
/** Dimmed live "what is it doing" cue for an ACTIVE node — the last tool it ran.
|
|
201
|
+
* Empty for non-active rows (stale once a node parks) and when none is recorded. */
|
|
202
|
+
export function activityCell(id, node) {
|
|
203
|
+
if (node?.status !== 'active')
|
|
204
|
+
return '';
|
|
205
|
+
const a = (readTelemetry(id).last_activity ?? '').trim();
|
|
206
|
+
return a === '' ? '' : ` ${DIM}· ${a}${RESET}`;
|
|
207
|
+
}
|
|
208
|
+
/** Revive-count badge (meta.cycles). Hidden on the first cycle (0) to cut noise. */
|
|
209
|
+
export function cycleBadge(node) {
|
|
210
|
+
const c = node?.cycles ?? 0;
|
|
211
|
+
return c > 0 ? ` ${DIM}⟳${c}${RESET}` : '';
|
|
212
|
+
}
|
|
213
|
+
/** Short on-screen label: the explicit handle when set, else the pi-generated
|
|
214
|
+
* description, else the bare name. fullName joins BOTH (`handle description`);
|
|
215
|
+
* the nav chrome shows just the first so rows stay compact. */
|
|
216
|
+
export function navLabel(node, id) {
|
|
217
|
+
if (node === null)
|
|
218
|
+
return shortId(id);
|
|
219
|
+
const handle = node.name && node.name !== node.kind ? node.name : '';
|
|
220
|
+
if (handle !== '')
|
|
221
|
+
return handle;
|
|
222
|
+
const desc = (node.description ?? '').trim();
|
|
223
|
+
return desc !== '' ? desc : node.name;
|
|
224
|
+
}
|
|
225
|
+
export function shortId(id) {
|
|
226
|
+
return id.slice(0, 8);
|
|
227
|
+
}
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
// Attachment — is a human currently WATCHING a node? A separate axis from
|
|
230
|
+
// running (status 'active' = the engine is live on its detached broker, watched
|
|
231
|
+
// or not). Two signals:
|
|
232
|
+
// viewer pane — a `focuses` row points at the node (one cheap sqlite read per
|
|
233
|
+
// render pass; pane-existence alone is NOT the signal).
|
|
234
|
+
// broker — the broker persists its helloed-viewer count (tmux panes AND
|
|
235
|
+
// web tabs) to job/attach.json on every viewer change
|
|
236
|
+
// (src/core/runtime/broker.ts). Trusted only while the node is
|
|
237
|
+
// 'active': a broker crash can leave a stale file.
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
/** Node ids currently shown in a tmux focus viewport. Built once per render. */
|
|
240
|
+
export function focusedNodeIds() {
|
|
241
|
+
try {
|
|
242
|
+
return new Set(listFocuses().map((f) => f.node_id));
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
return new Set();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/** True when a human is watching `id` right now (tmux focus or broker viewer). */
|
|
249
|
+
export function isAttached(id, node, focused) {
|
|
250
|
+
if (focused.has(id))
|
|
251
|
+
return true;
|
|
252
|
+
if (node?.status !== 'active')
|
|
253
|
+
return false; // stale attach.json from a crash
|
|
254
|
+
try {
|
|
255
|
+
const p = join(jobDir(id), 'attach.json');
|
|
256
|
+
if (!existsSync(p))
|
|
257
|
+
return false;
|
|
258
|
+
const rec = JSON.parse(readFileSync(p, 'utf8'));
|
|
259
|
+
return typeof rec.viewers === 'number' && rec.viewers > 0;
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
// Per-node ask counts — ONE shell-out per poll. `crtr canvas attention map`
|
|
267
|
+
// buckets a whole sub-DAG's pending asks by node in a single process, so the
|
|
268
|
+
// timer stays cheap (< 2 s) regardless of how many nodes are visible. --json
|
|
269
|
+
// gives a parseable {counts} blob (the default render is XML chrome).
|
|
270
|
+
// ---------------------------------------------------------------------------
|
|
271
|
+
export function fetchAsksMap(rootId) {
|
|
272
|
+
try {
|
|
273
|
+
const raw = execFileSync('crtr', ['canvas', 'attention', 'map', '--view', rootId, '--json'], {
|
|
274
|
+
timeout: 2_500,
|
|
275
|
+
encoding: 'utf8',
|
|
276
|
+
});
|
|
277
|
+
const parsed = JSON.parse(raw.trim());
|
|
278
|
+
return parsed.counts ?? {};
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
return {};
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
// Graph queries (dependency-free, straight off the canvas db)
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
/** First manager (by created) — the UP step for the ancestry spine. */
|
|
288
|
+
export function managerOf(id) {
|
|
289
|
+
return cSubscribers(id)[0]?.node_id;
|
|
290
|
+
}
|
|
291
|
+
/** A kind:'human' node is a control-plane ASK (a humanloop deck on the human's
|
|
292
|
+
* screen), NOT a pi conversation — it has no session, so focusing/reviving it
|
|
293
|
+
* boots a confused blank "you have been revived" pi. Its pending-ask signal
|
|
294
|
+
* already rides the ⚑ badge on the ASKING node (attention.ts attributes asks by
|
|
295
|
+
* source.nodeId, never to the human node), so the row carries no signal of its
|
|
296
|
+
* own. Drop it from every navigable list (the tree, BASE reports, child counts,
|
|
297
|
+
* subtree expansion) so it can never be selected. */
|
|
298
|
+
export function isHumanAsk(id) {
|
|
299
|
+
return cNode(id)?.kind === 'human';
|
|
300
|
+
}
|
|
301
|
+
/** A node's direct children that are navigable conversations — human-ask nodes
|
|
302
|
+
* dropped. The one place the nav chrome enumerates children. */
|
|
303
|
+
export function convoChildIds(id) {
|
|
304
|
+
return cSubscriptions(id).map((s) => s.node_id).filter((cid) => !isHumanAsk(cid));
|
|
305
|
+
}
|
|
306
|
+
/** Live reports (active|idle) of a node — the DOWN set in BASE. */
|
|
307
|
+
export function liveReports(id) {
|
|
308
|
+
return convoChildIds(id).filter((cid) => {
|
|
309
|
+
const st = cNode(cid)?.status;
|
|
310
|
+
return st === 'active' || st === 'idle';
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
/** Direct navigable children — used for the ⤳ badge and fold counts (human-ask
|
|
314
|
+
* nodes excluded, so the count matches what the tree actually shows). */
|
|
315
|
+
export function childCount(id) {
|
|
316
|
+
return convoChildIds(id).length;
|
|
317
|
+
}
|
|
318
|
+
/** Climb first-manager edges from `self` to the ancestry root (cycle-guarded). */
|
|
319
|
+
export function climbRoot(self) {
|
|
320
|
+
let cur = self;
|
|
321
|
+
const seen = new Set([cur]);
|
|
322
|
+
for (;;) {
|
|
323
|
+
const mgr = managerOf(cur);
|
|
324
|
+
if (mgr === undefined || seen.has(mgr))
|
|
325
|
+
break;
|
|
326
|
+
seen.add(mgr);
|
|
327
|
+
cur = mgr;
|
|
328
|
+
}
|
|
329
|
+
return cur;
|
|
330
|
+
}
|
|
331
|
+
/** Space-joined ids of a node's subtree (cursor-relative {subtree} var). */
|
|
332
|
+
export function subtreeIds(root) {
|
|
333
|
+
const out = [];
|
|
334
|
+
const seen = new Set([root]);
|
|
335
|
+
const q = convoChildIds(root);
|
|
336
|
+
while (q.length > 0) {
|
|
337
|
+
const id = q.shift();
|
|
338
|
+
if (seen.has(id))
|
|
339
|
+
continue;
|
|
340
|
+
seen.add(id);
|
|
341
|
+
out.push(id);
|
|
342
|
+
for (const cid of convoChildIds(id))
|
|
343
|
+
if (!seen.has(cid))
|
|
344
|
+
q.push(cid);
|
|
345
|
+
}
|
|
346
|
+
return out;
|
|
347
|
+
}
|
|
348
|
+
// ---------------------------------------------------------------------------
|
|
349
|
+
// Shared cell builders
|
|
350
|
+
// ---------------------------------------------------------------------------
|
|
351
|
+
/** ⤳M direct-children badge — only on orchestrator rows. */
|
|
352
|
+
export function childBadge(node) {
|
|
353
|
+
if (node === null || node.mode !== 'orchestrator')
|
|
354
|
+
return '';
|
|
355
|
+
const m = childCount(node.node_id);
|
|
356
|
+
return m > 0 ? ` ${DIM}⤳${m}${RESET}` : '';
|
|
357
|
+
}
|
|
358
|
+
/** ⚑K pending-asks badge for a node, read from the supplied per-node ask map. */
|
|
359
|
+
export function askBadge(id, asks) {
|
|
360
|
+
const k = asks[id] ?? 0;
|
|
361
|
+
return k > 0 ? ` ${YELLOW}⚑${k}${RESET}` : '';
|
|
362
|
+
}
|
|
363
|
+
/** "Live work below" badge — green ⇣N when this node is NOT itself active but has
|
|
364
|
+
* N active descendants. The one signal that an idle/parked node isn't dead: real
|
|
365
|
+
* work is still running beneath it. Count comes from the single subtree-activity
|
|
366
|
+
* pass (no per-row walk). Suppressed on active rows (their own ● already says so). */
|
|
367
|
+
export function liveBelowBadge(node, activeBelow) {
|
|
368
|
+
if (node === null || node.status === 'active')
|
|
369
|
+
return '';
|
|
370
|
+
const n = activeBelow.get(node.node_id) ?? 0;
|
|
371
|
+
return n > 0 ? ` ${GREEN}⇣${n}${RESET}` : '';
|
|
372
|
+
}
|
|
373
|
+
/** Sort rank for sibling ordering — live nodes (active, then idle) ahead of
|
|
374
|
+
* terminal ones, so sessions still running surface at the TOP of each child
|
|
375
|
+
* group instead of being buried under finished/failed ones. */
|
|
376
|
+
export function statusRank(id) {
|
|
377
|
+
switch (cNode(id)?.status) {
|
|
378
|
+
case 'active': return 0;
|
|
379
|
+
case 'idle': return 1;
|
|
380
|
+
case 'done': return 2;
|
|
381
|
+
case 'canceled': return 3;
|
|
382
|
+
case 'dead': return 4;
|
|
383
|
+
default: return 5;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
/** Direct children, live-first — the sibling order used both when flattening
|
|
387
|
+
* the tree and when stepping into a subtree (`l`). Array.sort is stable, so
|
|
388
|
+
* equal-status siblings keep their creation order. */
|
|
389
|
+
export function sortedChildIds(id) {
|
|
390
|
+
return convoChildIds(id).sort((a, b) => statusRank(a) - statusRank(b));
|
|
391
|
+
}
|
|
392
|
+
export function computeSubtreeActivity(root, self) {
|
|
393
|
+
const expand = new Set();
|
|
394
|
+
const activeBelow = new Map();
|
|
395
|
+
const seen = new Set();
|
|
396
|
+
// Returns the count of active nodes in subtree(id) INCLUDING id, and whether
|
|
397
|
+
// that subtree is worth revealing (holds an active node or self).
|
|
398
|
+
const visit = (id) => {
|
|
399
|
+
const selfActive = cNode(id)?.status === 'active';
|
|
400
|
+
if (seen.has(id))
|
|
401
|
+
return { active: selfActive ? 1 : 0, reveal: selfActive || id === self };
|
|
402
|
+
seen.add(id);
|
|
403
|
+
let below = 0;
|
|
404
|
+
let childReveal = false;
|
|
405
|
+
for (const c of convoChildIds(id)) {
|
|
406
|
+
const r = visit(c);
|
|
407
|
+
below += r.active;
|
|
408
|
+
if (r.reveal)
|
|
409
|
+
childReveal = true;
|
|
410
|
+
}
|
|
411
|
+
activeBelow.set(id, below);
|
|
412
|
+
if (childReveal)
|
|
413
|
+
expand.add(id); // a descendant is worth revealing → unfold id
|
|
414
|
+
return { active: below + (selfActive ? 1 : 0), reveal: childReveal || selfActive || id === self };
|
|
415
|
+
};
|
|
416
|
+
visit(root);
|
|
417
|
+
return { expand, activeBelow };
|
|
418
|
+
}
|
|
419
|
+
/** Fold policy alone — thin wrapper over computeSubtreeActivity for the keypress
|
|
420
|
+
* path (buildGraphModel without a precomputed set). The render path computes the
|
|
421
|
+
* activity once and threads `expand` in, so it never recomputes here. */
|
|
422
|
+
export function computeDefaultExpanded(root, self) {
|
|
423
|
+
return computeSubtreeActivity(root, self).expand;
|
|
424
|
+
}
|
|
425
|
+
export function buildGraphModel(self, folds, expand) {
|
|
426
|
+
const rootId = climbRoot(self);
|
|
427
|
+
const defaultExpanded = expand ?? computeDefaultExpanded(rootId, self);
|
|
428
|
+
// userExpanded / userCollapsed override the auto policy; absent → policy decides.
|
|
429
|
+
const isFolded = (id) => folds.userExpanded.has(id) ? false : folds.userCollapsed.has(id) ? true : !defaultExpanded.has(id);
|
|
430
|
+
const rows = [];
|
|
431
|
+
const visited = new Set();
|
|
432
|
+
const walk = (id, prefix, isRoot, isLast) => {
|
|
433
|
+
if (visited.has(id)) {
|
|
434
|
+
const connector = isRoot ? '' : isLast ? '└─ ' : '├─ ';
|
|
435
|
+
rows.push({ id, hasKids: false, isSelf: id === self, branch: prefix + connector, cycle: true, collapsed: false });
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
visited.add(id);
|
|
439
|
+
const kids = sortedChildIds(id);
|
|
440
|
+
const folded = isFolded(id);
|
|
441
|
+
const connector = isRoot ? '' : isLast ? '└─ ' : '├─ ';
|
|
442
|
+
rows.push({ id, hasKids: kids.length > 0, isSelf: id === self, branch: prefix + connector, cycle: false, collapsed: folded });
|
|
443
|
+
if (folded)
|
|
444
|
+
return; // folded — don't descend
|
|
445
|
+
const childPrefix = isRoot ? '' : prefix + (isLast ? ' ' : '│ ');
|
|
446
|
+
for (let i = 0; i < kids.length; i++)
|
|
447
|
+
walk(kids[i], childPrefix, false, i === kids.length - 1);
|
|
448
|
+
};
|
|
449
|
+
walk(rootId, '', true, true);
|
|
450
|
+
return rows;
|
|
451
|
+
}
|
|
452
|
+
/** Render one GRAPH row. CURSOR (selected) → reverse-video bar; an ATTACHED
|
|
453
|
+
* (human-watched) node → a coloured background bar; SELF → bold name. The
|
|
454
|
+
* cursor outranks the attached tint when both land on the same row. Running
|
|
455
|
+
* is signaled by the dot glyph alone (● green = active engine). */
|
|
456
|
+
export function renderGraphRow(r, isCursor, focused, activeBelow, asks) {
|
|
457
|
+
const wrap = (line, attached) => isCursor ? fillBar(line, fillWidth(), REVERSE)
|
|
458
|
+
: attached ? fillBar(line, fillWidth(), BG_ATTACHED)
|
|
459
|
+
: truncate(line);
|
|
460
|
+
if (r.cycle) {
|
|
461
|
+
const line = `${r.branch} ${DIM}↺ ${shortId(r.id)}${RESET}`;
|
|
462
|
+
return wrap(line, false);
|
|
463
|
+
}
|
|
464
|
+
const node = cNode(r.id);
|
|
465
|
+
const dot = coloredGlyph(node);
|
|
466
|
+
const rawName = navLabel(node, r.id);
|
|
467
|
+
const name = r.isSelf ? `${BOLD}${rawName}${RESET}` : rawName;
|
|
468
|
+
const kind = `${DIM}${node?.kind ?? ''}${RESET}`;
|
|
469
|
+
const tokens = `${DIM}${tokensCell(r.id)}${RESET}`;
|
|
470
|
+
// ▸ marks an expandable (collapsed-with-kids) row. The cursor row gets no
|
|
471
|
+
// caret — its reverse-video bar already distinguishes it — so the triangle
|
|
472
|
+
// reads purely as "this unfolds".
|
|
473
|
+
const expandable = r.hasKids && r.collapsed;
|
|
474
|
+
const caret = !isCursor && expandable ? `${DIM}▸${RESET} ` : ' ';
|
|
475
|
+
const fold = expandable ? ` ${DIM}[+${childCount(r.id)}]${RESET}` : '';
|
|
476
|
+
const line = `${r.branch}${caret}${dot} ${name} ${kind} ${tokens}${cycleBadge(node)}${childBadge(node)}${liveBelowBadge(node, activeBelow)}${fold}${askBadge(r.id, asks)}${activityCell(r.id, node)}`;
|
|
477
|
+
return wrap(line, isAttached(r.id, node, focused));
|
|
478
|
+
}
|
|
479
|
+
/** Total lines the GRAPH widget may emit. pi hard-caps extension widgets at
|
|
480
|
+
* MAX_WIDGET_LINES — anything past that pi truncates itself, eating our own
|
|
481
|
+
* scroll chrome — so never exceed it (and shrink on a very short terminal).
|
|
482
|
+
* The viewport scrolls WITHIN this cap as the cursor moves. */
|
|
483
|
+
export function graphWidgetBudget() {
|
|
484
|
+
const rows = process.stdout.rows ?? VIEWPORT_FALLBACK_ROWS;
|
|
485
|
+
return Math.max(4, Math.min(PI_MAX_WIDGET_LINES, rows - 4));
|
|
486
|
+
}
|
|
487
|
+
export const GRAPH_HINT = `${DIM}jk move · hl fold · ↵ focus · e expand · x kill · m mgr · esc${RESET}`;
|
|
@@ -17,6 +17,13 @@ export declare function inboxPath(nodeId: string): string;
|
|
|
17
17
|
export declare function passivePath(nodeId: string): string;
|
|
18
18
|
export declare function transcriptPath(nodeId: string): string;
|
|
19
19
|
export declare function sessionPtrPath(nodeId: string): string;
|
|
20
|
+
/** On-read injection dedup set — the doc realpaths already surfaced on-read in
|
|
21
|
+
* this node's CURRENT conversation transcript. Persisted so the once-per-
|
|
22
|
+
* transcript dedup survives a dormancy → revive(resume) cycle: a resume reuses
|
|
23
|
+
* the same .jsonl transcript in a NEW pi process, so the in-memory set would
|
|
24
|
+
* otherwise start empty and re-inject docs already present in the transcript.
|
|
25
|
+
* Deleted by the launch paths that start a FRESH transcript. */
|
|
26
|
+
export declare function injectedDocsPath(nodeId: string): string;
|
|
20
27
|
/** Create the full directory skeleton for a node. Idempotent. */
|
|
21
28
|
export declare function ensureNodeDirs(nodeId: string): void;
|
|
22
29
|
/** Ensure the canvas home exists. Idempotent. */
|
|
@@ -62,6 +62,15 @@ export function transcriptPath(nodeId) {
|
|
|
62
62
|
export function sessionPtrPath(nodeId) {
|
|
63
63
|
return join(nodeDir(nodeId), 'session.ptr');
|
|
64
64
|
}
|
|
65
|
+
/** On-read injection dedup set — the doc realpaths already surfaced on-read in
|
|
66
|
+
* this node's CURRENT conversation transcript. Persisted so the once-per-
|
|
67
|
+
* transcript dedup survives a dormancy → revive(resume) cycle: a resume reuses
|
|
68
|
+
* the same .jsonl transcript in a NEW pi process, so the in-memory set would
|
|
69
|
+
* otherwise start empty and re-inject docs already present in the transcript.
|
|
70
|
+
* Deleted by the launch paths that start a FRESH transcript. */
|
|
71
|
+
export function injectedDocsPath(nodeId) {
|
|
72
|
+
return join(nodeDir(nodeId), 'injected-docs.json');
|
|
73
|
+
}
|
|
65
74
|
/** Create the full directory skeleton for a node. Idempotent. */
|
|
66
75
|
export function ensureNodeDirs(nodeId) {
|
|
67
76
|
for (const d of [contextDir(nodeId), jobDir(nodeId), reportsDir(nodeId)]) {
|
|
@@ -32,18 +32,77 @@ export interface DashboardRow {
|
|
|
32
32
|
* agent you come back to. Drives the browser's resident-only lifecycle filter.
|
|
33
33
|
* Only populated by dashboardRowsAll (the browser snapshot). */
|
|
34
34
|
lifecycle?: Lifecycle;
|
|
35
|
-
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
35
|
+
/** "Most recent activity" sort key — epoch ms. The cheap boot stats the node's
|
|
36
|
+
* job/telemetry.json (rewritten on every turn_end at a DETERMINISTIC path, so
|
|
37
|
+
* no meta read), falling back to `created`'s epoch when a node never ran a turn.
|
|
38
|
+
* This is the cheap proxy for last-message recency the attention sort tie-breaks
|
|
39
|
+
* on. Always set by dashboardRowsAll; optional only so synthetic test rows + the
|
|
40
|
+
* scoped dashboardRows can omit it. (NB: the pi session-file mtime would be truer
|
|
41
|
+
* but its path lives in meta.json at a non-deterministic location — reading it
|
|
42
|
+
* per node would defeat the cheap boot, so telemetry mtime is used instead.) */
|
|
43
|
+
mtimeMs?: number;
|
|
44
|
+
/** The node's spawn prompt (context/initial-prompt.md), trimmed + capped. Populated
|
|
45
|
+
* LAZILY by loadPreview (selected-row preview) — NOT on the cheap boot path.
|
|
46
|
+
* Indexed by super-search and shown in the preview panel when there is no query. */
|
|
39
47
|
goal?: string;
|
|
40
|
-
/** EVERY user prompt across the node's pi session — the whole conversation, not
|
|
41
|
-
*
|
|
42
|
-
* for never-revived nodes (no session
|
|
43
|
-
* + the windowed
|
|
48
|
+
/** EVERY user prompt across the node's pi session — the whole conversation, not just
|
|
49
|
+
* the spawn prompt — joined + capped. Populated LAZILY by loadPreview (ONE session
|
|
50
|
+
* read, folded with lastAssistant); undefined for never-revived nodes (no session
|
|
51
|
+
* file). Powers whole-conversation super-search + the windowed preview snippet. */
|
|
44
52
|
prompts?: string;
|
|
53
|
+
/** The node's LAST assistant message (text only), trimmed + capped. Populated LAZILY
|
|
54
|
+
* by loadPreview (same single session read as `prompts`); undefined for a node whose
|
|
55
|
+
* session has no assistant reply yet. Shown in the preview's reply block. */
|
|
56
|
+
lastAssistant?: string;
|
|
57
|
+
/** True when the node is GENUINELY mid-turn right now — its `busy` marker exists
|
|
58
|
+
* AND its broker pid is alive (not merely an `active`, between-turns node). The
|
|
59
|
+
* live "is it generating?" cue. Computed on the cheap boot path for LIVE nodes
|
|
60
|
+
* only (dormant rows are always false). */
|
|
61
|
+
streaming?: boolean;
|
|
62
|
+
/** True when a viewer (focus row) is attached to the node — i.e. someone has it
|
|
63
|
+
* open on screen. Set on the cheap boot path from the one listFocuses() query. */
|
|
64
|
+
viewed?: boolean;
|
|
65
|
+
/** Lazy-enrichment guard (internal): true once enrichRow/enrichRows has folded in
|
|
66
|
+
* the full label (meta), ctx tokens (telemetry), and ⚑ asks. Idempotency marker so
|
|
67
|
+
* progressive paint can re-call enrich for a viewport every keystroke for free. */
|
|
68
|
+
enriched?: boolean;
|
|
69
|
+
/** Lazy-enrichment guard (internal): true once loadPreview has read goal + the
|
|
70
|
+
* session (prompts + lastAssistant). Idempotency marker for the selected row. */
|
|
71
|
+
previewLoaded?: boolean;
|
|
45
72
|
}
|
|
46
73
|
/** One row per node visible in the sub-DAG of `rootId` (including root). */
|
|
47
74
|
export declare function dashboardRows(rootId: string): DashboardRow[];
|
|
48
|
-
/**
|
|
75
|
+
/**
|
|
76
|
+
* CHEAP boot builder — one row per node across the entire canvas, with only the
|
|
77
|
+
* fields that need no per-node meta/telemetry/session read. Two db queries total:
|
|
78
|
+
* `listNodes()` + `listFocuses()` (the focus set replaces 1473 `getFocusByNode`
|
|
79
|
+
* calls). For each node:
|
|
80
|
+
* - identity/status/kind/mode/cwd/created/lifecycle straight off the db row;
|
|
81
|
+
* - `name` is the db HANDLE only (the informative label needs meta.description —
|
|
82
|
+
* fold it in lazily via {@link enrichRow}/{@link enrichRows});
|
|
83
|
+
* - `viewed` from the one focus-set;
|
|
84
|
+
* - `streaming` only for LIVE nodes (active|idle — the only ones that can be
|
|
85
|
+
* mid-turn); dormant rows are always false (skips an isPidAlive + statSync);
|
|
86
|
+
* - `mtimeMs` via a cheap telemetry statSync (no read) for the attention sort;
|
|
87
|
+
* - `ctx_tokens`/`asks` left 0 and `goal`/`prompts`/`lastAssistant` undefined —
|
|
88
|
+
* all deferred to the lazy enrichment API below.
|
|
89
|
+
*
|
|
90
|
+
* This is the first-frame path: paint from it immediately, then enrich the visible
|
|
91
|
+
* viewport on demand. See {@link enrichRow}, {@link enrichRows}, {@link loadPreview}.
|
|
92
|
+
*/
|
|
49
93
|
export declare function dashboardRowsAll(): DashboardRow[];
|
|
94
|
+
/** Fold the cheap-boot row's deferred fields in for ONE row: the full label
|
|
95
|
+
* (meta.description → fullName), ctx tokens (telemetry.json), and ⚑ asks. One
|
|
96
|
+
* meta read + one telemetry read + one ask scan. Idempotent (no-op once enriched).
|
|
97
|
+
* For a batch, prefer {@link enrichRows} — it scans each cwd's ask inbox once. */
|
|
98
|
+
export declare function enrichRow(row: DashboardRow): DashboardRow;
|
|
99
|
+
/** Batch {@link enrichRow}: enrich every not-yet-enriched row, scanning each
|
|
100
|
+
* distinct cwd's ask inbox exactly ONCE (via `asksForNodes`) instead of per row.
|
|
101
|
+
* Use this for a whole viewport / the full forest; mutates each row in place. */
|
|
102
|
+
export declare function enrichRows(rows: DashboardRow[]): void;
|
|
103
|
+
/** Load the SELECTED row's preview text: the spawn `goal` (initial-prompt.md) plus
|
|
104
|
+
* the whole-conversation `prompts` and the `lastAssistant` reply — the latter two
|
|
105
|
+
* folded into ONE session-file read (see {@link readSessionParts}). Mutates the row
|
|
106
|
+
* in place; idempotent. Call only for the cursor row (and lazily/in-background to
|
|
107
|
+
* warm the prompt super-search corpus), never on the boot path. */
|
|
108
|
+
export declare function loadPreview(row: DashboardRow): DashboardRow;
|