@crouton-kit/crouter 0.3.18 → 0.3.19
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 +6 -0
- package/dist/builtin-memory/crouter-development/marketplaces.md +164 -0
- package/dist/builtin-memory/crouter-development/personas/base-prompt.md +58 -0
- package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +60 -0
- package/dist/builtin-memory/crouter-development/personas.md +117 -0
- package/dist/builtin-memory/crouter-development/plugins.md +163 -0
- package/dist/builtin-memory/design.md +60 -0
- package/dist/builtin-memory/development.md +116 -0
- package/dist/builtin-memory/planning.md +66 -0
- package/dist/builtin-memory/spec.md +99 -0
- package/dist/builtin-personas/design/orchestrator.md +1 -1
- package/dist/builtin-personas/developer/orchestrator.md +1 -1
- package/dist/builtin-personas/lifecycle/resident.md +1 -1
- package/dist/builtin-personas/lifecycle/terminal.md +5 -2
- package/dist/builtin-personas/orchestration-kernel.md +15 -24
- package/dist/builtin-personas/plan/orchestrator.md +1 -1
- package/dist/builtin-personas/runtime-base.md +6 -3
- package/dist/builtin-personas/spec/orchestrator.md +1 -1
- package/dist/builtin-personas/waiting.md +8 -0
- package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +1 -1
- package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +2 -2
- package/dist/builtin-views/_lib/states.mjs +161 -0
- package/dist/builtin-views/canvas/client.mjs +303 -0
- package/dist/builtin-views/canvas/view.mjs +576 -0
- package/dist/builtin-views/git-pr/client.mjs +440 -0
- package/dist/builtin-views/git-pr/view.mjs +675 -0
- package/dist/builtin-views/inbox/_lib/render.mjs +177 -0
- package/dist/builtin-views/inbox/sources/gmail.mjs +965 -0
- package/dist/builtin-views/inbox/sources/linkedin.mjs +427 -0
- package/dist/builtin-views/inbox/view.mjs +889 -0
- package/dist/builtin-views/linkedin/client.mjs +610 -0
- package/dist/builtin-views/linkedin/view.mjs +1171 -0
- package/dist/clients/attach/attach-cmd.d.ts +2 -0
- package/dist/clients/attach/attach-cmd.js +354 -0
- package/dist/clients/attach/chat-view.d.ts +77 -0
- package/dist/clients/attach/chat-view.js +450 -0
- package/dist/clients/attach/clipboard-image.d.ts +16 -0
- package/dist/clients/attach/clipboard-image.js +113 -0
- package/dist/clients/attach/config-load.d.ts +31 -0
- package/dist/clients/attach/config-load.js +113 -0
- package/dist/clients/attach/extension-dialogs.d.ts +29 -0
- package/dist/clients/attach/extension-dialogs.js +101 -0
- package/dist/clients/attach/input-controller.d.ts +54 -0
- package/dist/clients/attach/input-controller.js +204 -0
- package/dist/clients/attach/slash-commands.d.ts +36 -0
- package/dist/clients/attach/slash-commands.js +200 -0
- package/dist/clients/attach/view-socket.d.ts +48 -0
- package/dist/clients/attach/view-socket.js +126 -0
- package/dist/commands/attention.js +3 -3
- package/dist/commands/canvas-prune.js +1 -1
- package/dist/commands/daemon.js +3 -2
- package/dist/commands/human/prompts.js +1 -1
- package/dist/commands/human/queue.js +43 -8
- package/dist/commands/human/shared.d.ts +2 -0
- package/dist/commands/memory/find.d.ts +1 -0
- package/dist/commands/memory/find.js +180 -0
- package/dist/commands/memory/lint.d.ts +1 -0
- package/dist/commands/memory/lint.js +140 -0
- package/dist/commands/memory/list.d.ts +1 -0
- package/dist/commands/memory/list.js +79 -0
- package/dist/commands/memory/read.js +103 -0
- package/dist/commands/memory/shared.d.ts +30 -0
- package/dist/commands/memory/shared.js +122 -0
- package/dist/commands/memory/write.d.ts +1 -0
- package/dist/commands/memory/write.js +85 -0
- package/dist/commands/memory.d.ts +2 -0
- package/dist/commands/memory.js +27 -0
- package/dist/commands/node.d.ts +3 -2
- package/dist/commands/node.js +607 -61
- package/dist/commands/pkg/market-manage.js +1 -1
- package/dist/commands/push.js +6 -6
- package/dist/commands/revive.js +1 -1
- package/dist/commands/skill/author.js +1 -1
- package/dist/commands/skill/shared.d.ts +1 -8
- package/dist/commands/skill/shared.js +2 -55
- package/dist/commands/skill.js +9 -14
- package/dist/commands/sys/doctor.js +1 -1
- package/dist/commands/sys/update.js +1 -1
- package/dist/commands/view-cycle.d.ts +2 -0
- package/dist/commands/view-cycle.js +125 -0
- package/dist/commands/view-list.d.ts +2 -0
- package/dist/commands/view-list.js +66 -0
- package/dist/commands/view-new.d.ts +2 -0
- package/dist/commands/view-new.js +70 -0
- package/dist/commands/view-pick.d.ts +2 -0
- package/dist/commands/view-pick.js +119 -0
- package/dist/commands/view-run.d.ts +2 -0
- package/dist/commands/view-run.js +191 -0
- package/dist/commands/view.d.ts +2 -0
- package/dist/commands/view.js +29 -0
- package/dist/core/__tests__/broker-lifecycle.test.d.ts +1 -0
- package/dist/core/__tests__/broker-lifecycle.test.js +677 -0
- package/dist/core/__tests__/broker-sdk-wiring.test.d.ts +1 -0
- package/dist/core/__tests__/broker-sdk-wiring.test.js +166 -0
- package/dist/core/__tests__/cascade-close.test.js +12 -2
- package/dist/core/__tests__/child-death-wake.test.d.ts +1 -0
- package/dist/core/__tests__/child-death-wake.test.js +245 -0
- package/dist/core/__tests__/context-intro.test.js +76 -62
- package/dist/core/__tests__/daemon-boot.test.js +14 -5
- package/dist/core/__tests__/daemon-liveness.test.js +34 -9
- package/dist/core/__tests__/detach-focus.test.d.ts +1 -0
- package/dist/core/__tests__/detach-focus.test.js +206 -0
- package/dist/core/__tests__/draw-style.test.d.ts +1 -0
- package/dist/core/__tests__/draw-style.test.js +258 -0
- package/dist/core/__tests__/fixtures/c3-custom-provider-ext.d.ts +2 -0
- package/dist/core/__tests__/fixtures/c3-custom-provider-ext.js +18 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +138 -0
- package/dist/core/__tests__/fixtures/fake-engine.js +614 -0
- package/dist/core/__tests__/fixtures/fake-pi-host.js +1 -0
- package/dist/core/__tests__/flagship-lifecycle.test.js +7 -1
- package/dist/core/__tests__/frame-decoder-perf.test.d.ts +1 -0
- package/dist/core/__tests__/frame-decoder-perf.test.js +198 -0
- package/dist/core/__tests__/helpers/harness.d.ts +9 -0
- package/dist/core/__tests__/helpers/harness.js +111 -1
- package/dist/core/__tests__/home-session.test.js +41 -8
- package/dist/core/__tests__/human-new-window-regression.test.d.ts +1 -0
- package/dist/core/__tests__/human-new-window-regression.test.js +101 -0
- package/dist/core/__tests__/kickoff.test.js +37 -3
- package/dist/core/__tests__/live-mutation.test.js +50 -33
- package/dist/core/__tests__/memory.test.js +23 -115
- package/dist/core/__tests__/placement-focus.test.js +5 -0
- package/dist/core/__tests__/placement-teardown.test.js +54 -11
- package/dist/core/__tests__/relaunch.test.js +4 -3
- package/dist/core/__tests__/review-render-pane-regression.test.d.ts +1 -0
- package/dist/core/__tests__/review-render-pane-regression.test.js +133 -0
- package/dist/core/__tests__/spawn-root.test.js +10 -0
- package/dist/core/__tests__/spike-harness.test.js +1 -0
- package/dist/core/__tests__/wake-bearings.test.d.ts +1 -0
- package/dist/core/__tests__/wake-bearings.test.js +156 -0
- package/dist/core/__tests__/wake-origin.test.d.ts +1 -0
- package/dist/core/__tests__/wake-origin.test.js +110 -0
- package/dist/core/bootstrap.js +1 -1
- package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
- package/dist/core/canvas/browse/app.js +24 -2
- package/dist/core/canvas/browse/model.d.ts +38 -2
- package/dist/core/canvas/browse/model.js +134 -10
- package/dist/core/canvas/browse/render.d.ts +6 -12
- package/dist/core/canvas/browse/render.js +72 -104
- package/dist/core/canvas/canvas.js +15 -22
- package/dist/core/canvas/db.js +46 -0
- package/dist/core/canvas/index.d.ts +1 -0
- package/dist/core/canvas/index.js +1 -0
- package/dist/core/canvas/pid.d.ts +4 -0
- package/dist/core/canvas/pid.js +23 -0
- package/dist/core/canvas/render.d.ts +11 -2
- package/dist/core/canvas/render.js +69 -0
- package/dist/core/canvas/types.d.ts +85 -3
- package/dist/core/canvas/wakeups.d.ts +76 -0
- package/dist/core/canvas/wakeups.js +185 -0
- package/dist/core/config.js +4 -1
- package/dist/core/frontmatter.js +37 -124
- package/dist/core/memory-resolver.d.ts +49 -0
- package/dist/core/memory-resolver.js +141 -0
- package/dist/core/personas/loader.d.ts +7 -0
- package/dist/core/personas/loader.js +36 -5
- package/dist/core/personas/resolve.js +13 -3
- package/dist/core/predicate.d.ts +63 -0
- package/dist/core/predicate.js +189 -0
- package/dist/core/resolver.js +26 -5
- package/dist/core/runtime/bearings.d.ts +53 -12
- package/dist/core/runtime/bearings.js +132 -59
- package/dist/core/runtime/broker-cli.d.ts +1 -0
- package/dist/core/runtime/broker-cli.js +46 -0
- package/dist/core/runtime/broker-protocol.d.ts +332 -0
- package/dist/core/runtime/broker-protocol.js +153 -0
- package/dist/core/runtime/broker-sdk.d.ts +48 -0
- package/dist/core/runtime/broker-sdk.js +72 -0
- package/dist/core/runtime/broker.d.ts +55 -0
- package/dist/core/runtime/broker.js +1128 -0
- package/dist/core/runtime/close.js +35 -6
- package/dist/core/runtime/host.d.ts +53 -0
- package/dist/core/runtime/host.js +186 -0
- package/dist/core/runtime/kickoff.d.ts +2 -1
- package/dist/core/runtime/kickoff.js +91 -5
- package/dist/core/runtime/launch.d.ts +45 -2
- package/dist/core/runtime/launch.js +65 -2
- package/dist/core/runtime/lifecycle.js +23 -6
- package/dist/core/runtime/memory.d.ts +2 -42
- package/dist/core/runtime/memory.js +11 -162
- package/dist/core/runtime/nodes.d.ts +33 -0
- package/dist/core/runtime/nodes.js +59 -1
- package/dist/core/runtime/persona.js +21 -11
- package/dist/core/runtime/pi-vendored.d.ts +18 -0
- package/dist/core/runtime/pi-vendored.js +49 -0
- package/dist/core/runtime/placement.d.ts +32 -14
- package/dist/core/runtime/placement.js +206 -52
- package/dist/core/runtime/promote.d.ts +0 -6
- package/dist/core/runtime/promote.js +1 -12
- package/dist/core/runtime/{demote.d.ts → recycle.d.ts} +5 -5
- package/dist/core/runtime/{demote.js → recycle.js} +27 -11
- package/dist/core/runtime/reset.js +8 -6
- package/dist/core/runtime/revive.d.ts +2 -0
- package/dist/core/runtime/revive.js +34 -34
- package/dist/core/runtime/spawn.d.ts +19 -0
- package/dist/core/runtime/spawn.js +75 -22
- package/dist/core/runtime/stop-guard.d.ts +1 -1
- package/dist/core/runtime/stop-guard.js +6 -1
- package/dist/core/runtime/tmux-chrome.d.ts +1 -1
- package/dist/core/runtime/tmux-chrome.js +1 -1
- package/dist/core/runtime/tmux.d.ts +28 -0
- package/dist/core/runtime/tmux.js +80 -6
- package/dist/core/scope.d.ts +11 -0
- package/dist/core/scope.js +39 -0
- package/dist/core/spawn.d.ts +5 -0
- package/dist/core/spawn.js +20 -2
- package/dist/core/substrate/gate.d.ts +13 -0
- package/dist/core/substrate/gate.js +21 -0
- package/dist/core/substrate/index.d.ts +7 -0
- package/dist/core/substrate/index.js +18 -0
- package/dist/core/substrate/on-read.d.ts +14 -0
- package/dist/core/substrate/on-read.js +292 -0
- package/dist/core/substrate/render.d.ts +25 -0
- package/dist/core/substrate/render.js +256 -0
- package/dist/core/substrate/schema.d.ts +76 -0
- package/dist/core/substrate/schema.js +124 -0
- package/dist/core/substrate/session-cache.d.ts +30 -0
- package/dist/core/substrate/session-cache.js +77 -0
- package/dist/core/substrate/subject.d.ts +41 -0
- package/dist/core/substrate/subject.js +54 -0
- package/dist/core/tui/contract.d.ts +83 -0
- package/dist/core/tui/contract.js +8 -0
- package/dist/core/tui/draw.d.ts +96 -0
- package/dist/core/tui/draw.js +339 -0
- package/dist/core/tui/host.d.ts +29 -0
- package/dist/core/tui/host.js +379 -0
- package/dist/core/tui/loader.d.ts +16 -0
- package/dist/core/tui/loader.js +94 -0
- package/dist/core/{canvas/browse → tui}/terminal.js +7 -6
- package/dist/core/wake.d.ts +86 -0
- package/dist/core/wake.js +308 -0
- package/dist/daemon/crtrd.d.ts +29 -4
- package/dist/daemon/crtrd.js +662 -46
- package/dist/pi-extensions/__tests__/canvas-context-intro.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +171 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +19 -12
- package/dist/pi-extensions/canvas-commands.d.ts +3 -0
- package/dist/pi-extensions/canvas-commands.js +10 -0
- package/dist/pi-extensions/canvas-context-intro.d.ts +17 -0
- package/dist/pi-extensions/canvas-context-intro.js +55 -15
- package/dist/pi-extensions/canvas-doc-substrate.d.ts +44 -0
- package/dist/pi-extensions/canvas-doc-substrate.js +112 -0
- package/dist/pi-extensions/canvas-nav.d.ts +3 -0
- package/dist/pi-extensions/canvas-nav.js +87 -23
- package/dist/pi-extensions/canvas-stophook.js +17 -8
- package/dist/pi-extensions/canvas-view.d.ts +21 -0
- package/dist/pi-extensions/canvas-view.js +75 -0
- package/dist/prompts/skill.js +19 -26
- package/dist/prompts/view.d.ts +7 -0
- package/dist/prompts/view.js +101 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.js +1 -0
- package/package.json +9 -4
- package/dist/commands/skill/find.d.ts +0 -4
- package/dist/commands/skill/find.js +0 -257
- package/dist/commands/skill/read.js +0 -91
- /package/dist/commands/{skill → memory}/read.d.ts +0 -0
- /package/dist/core/{canvas/browse → tui}/terminal.d.ts +0 -0
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* Canvas Dashboard — the crtr `canvas` view (the monitor archetype).
|
|
4
|
+
*
|
|
5
|
+
* Self-contained ESM. Imports its data layer from `./client.mjs` (which shells
|
|
6
|
+
* the `crtr` binary) and the shared state helpers from `../_lib/states.mjs`. It
|
|
7
|
+
* imports NOTHING from crtr — the host injects the `Draw` + `ViewHost` API and
|
|
8
|
+
* dynamically `import()`s this module's DEFAULT EXPORT.
|
|
9
|
+
*
|
|
10
|
+
* A READ-ONLY monitor of the live agent graph: it rebuilds the forest from each
|
|
11
|
+
* node's `parent` edge, keeps only trees that contain live (active/idle) or
|
|
12
|
+
* human-blocked work, and renders them as a scrollable ASCII tree. It auto-polls
|
|
13
|
+
* (refreshMs) — no node-focus / close / swap (views are sessionless). j/k move a
|
|
14
|
+
* read cursor; g forces a refresh; q quits.
|
|
15
|
+
*
|
|
16
|
+
* VISUAL LANGUAGE (crtr-views-visual-design §2/§3/§4): hierarchy is carried by
|
|
17
|
+
* weight + hue + position, never boxes. The status glyph hues match `canvas
|
|
18
|
+
* browse`'s authoritative palette (active=green, idle=yellow, done=cyan,
|
|
19
|
+
* dead=red, canceled=grey; asks=bright-yellow) — "not a recolor." The chrome's
|
|
20
|
+
* one state chip is host-derived from data freshness (busy→working, an error
|
|
21
|
+
* banner→blocked, an action banner→attention, else ready/idle), driven here by
|
|
22
|
+
* toggling banners + the busy lane. Per-row metadata (the relative age) is
|
|
23
|
+
* right-flushed via `ListItemRow.right`; secondary text recedes via grey+dim so
|
|
24
|
+
* it survives NO_COLOR. The four standard states come from `_lib/states.mjs`.
|
|
25
|
+
*
|
|
26
|
+
* @module canvas/view
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { fetchNodes, fetchAttention } from './client.mjs';
|
|
30
|
+
import { loadingState, emptyState, notReadyState } from '../_lib/states.mjs';
|
|
31
|
+
|
|
32
|
+
/** @typedef {import('./client.mjs').CanvasNode} CanvasNode */
|
|
33
|
+
/** @typedef {import('./client.mjs').ClientError} ClientError */
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* One rendered tree line (a flattened forest node). Width-independent, so it is
|
|
37
|
+
* built once per refresh and re-rendered on resize without a re-fetch.
|
|
38
|
+
* @typedef {Object} TreeRow
|
|
39
|
+
* @property {string} nodeId
|
|
40
|
+
* @property {string} prefix Tree-branch art (e.g. "│ └─ "); '' for a root.
|
|
41
|
+
* @property {string} glyph Status glyph.
|
|
42
|
+
* @property {string} status
|
|
43
|
+
* @property {string} name
|
|
44
|
+
* @property {string} kind
|
|
45
|
+
* @property {string} mode
|
|
46
|
+
* @property {string} lifecycle
|
|
47
|
+
* @property {string} shortId
|
|
48
|
+
* @property {string} created ISO 8601 birth timestamp (drives the right-flush age).
|
|
49
|
+
* @property {boolean} blocked True ⇒ this node has pending human asks.
|
|
50
|
+
* @property {number} askCount
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The view's single mutable state object. The view owns it; hooks mutate it in
|
|
55
|
+
* place.
|
|
56
|
+
* @typedef {Object} CanvasState
|
|
57
|
+
* @property {TreeRow[]} rows Flattened active-tree forest (render source).
|
|
58
|
+
* @property {number} cursor Read cursor into rows (j/k).
|
|
59
|
+
* @property {number} scroll draw.list scroll, stored back each frame.
|
|
60
|
+
* @property {number} shownRoots How many trees survived the active filter.
|
|
61
|
+
* @property {number} totalNodes Total nodes on the canvas (all statuses).
|
|
62
|
+
* @property {number} activeCount Nodes with status 'active' (canvas-wide).
|
|
63
|
+
* @property {number} attnTotal Total pending human asks (canvas-wide).
|
|
64
|
+
* @property {number} lastFetch Epoch ms of the last successful refresh.
|
|
65
|
+
* @property {string|null} sourceError Non-null ⇒ the data source failed; the
|
|
66
|
+
* string is the cause (drives the not-ready takeover when there is no data, and
|
|
67
|
+
* is the dump fallback). Cleared on a successful refresh.
|
|
68
|
+
*/
|
|
69
|
+
|
|
70
|
+
// ── Status vocabulary (mirrors core/canvas/browse/render.ts — single source) ──
|
|
71
|
+
|
|
72
|
+
const LIVE_STATUS = new Set(['active', 'idle']);
|
|
73
|
+
|
|
74
|
+
/** @type {Record<string,string>} */
|
|
75
|
+
const STATUS_GLYPH = {
|
|
76
|
+
active: '●',
|
|
77
|
+
idle: '○',
|
|
78
|
+
done: '✓',
|
|
79
|
+
dead: '✗',
|
|
80
|
+
canceled: '⊘',
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Load-bearing status hue — NUMERIC SGR codes, matching `canvas browse`'s
|
|
85
|
+
* STATUS_COLOR exactly (active=green, idle=yellow, done=cyan, dead=red,
|
|
86
|
+
* canceled=grey). The design is explicit: keep these, "this is not a recolor."
|
|
87
|
+
* @type {Record<string,string>}
|
|
88
|
+
*/
|
|
89
|
+
const STATUS_FG = {
|
|
90
|
+
active: '32', // green
|
|
91
|
+
idle: '33', // yellow
|
|
92
|
+
done: '36', // cyan
|
|
93
|
+
dead: '31', // red
|
|
94
|
+
canceled: '90', // grey
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/** @param {string} status @returns {import('../../core/tui/draw.js').Style|undefined} */
|
|
98
|
+
function glyphStyle(status) {
|
|
99
|
+
const fg = STATUS_FG[status];
|
|
100
|
+
return fg ? { fg } : undefined; // hue only — the glyph SHAPE is the mono carrier
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Name weight = hierarchy (design §2 "weight creates hierarchy"): live work
|
|
105
|
+
* (active) LEADS in bold; terminal nodes (done/dead/canceled) recede dim; idle
|
|
106
|
+
* stays default weight (readable, not shouting). Mono-safe (weight, not hue).
|
|
107
|
+
* @param {string} status @returns {import('../../core/tui/draw.js').Style|undefined}
|
|
108
|
+
*/
|
|
109
|
+
function nameStyle(status) {
|
|
110
|
+
if (status === 'active') return { bold: true };
|
|
111
|
+
if (status === 'done' || status === 'dead' || status === 'canceled') return { dim: true };
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** @param {string} lifecycle @returns {string} */
|
|
116
|
+
function lifeAbbr(lifecycle) {
|
|
117
|
+
if (lifecycle === 'resident') return 'res';
|
|
118
|
+
if (lifecycle === 'terminal') return 'term';
|
|
119
|
+
return lifecycle || '?';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** @param {string} id @returns {string} */
|
|
123
|
+
function shortId(id) {
|
|
124
|
+
const s = String(id || '');
|
|
125
|
+
const dash = s.indexOf('-');
|
|
126
|
+
return dash > 0 ? s.slice(0, dash) : s.slice(0, 8);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Relative-age ladder (design §5): `now` (<60s), `{m}m` (<60m), `{h}h` (<24h),
|
|
133
|
+
* `{d}d` (<7d), else a calendar date `Mon D` (`Mar 4`), prior-year `Mon ʼYY`.
|
|
134
|
+
* Max ~5 cols. Used for the right-flushed per-row age (the node's birth time).
|
|
135
|
+
* @param {string} createdIso @param {number} now @returns {string}
|
|
136
|
+
*/
|
|
137
|
+
function relAge(createdIso, now) {
|
|
138
|
+
const t = Date.parse(createdIso);
|
|
139
|
+
if (Number.isNaN(t)) return '';
|
|
140
|
+
const s = Math.max(0, Math.floor((now - t) / 1000));
|
|
141
|
+
if (s < 60) return 'now';
|
|
142
|
+
const m = Math.floor(s / 60);
|
|
143
|
+
if (m < 60) return `${m}m`;
|
|
144
|
+
const h = Math.floor(m / 60);
|
|
145
|
+
if (h < 24) return `${h}h`;
|
|
146
|
+
const d = Math.floor(h / 24);
|
|
147
|
+
if (d < 7) return `${d}d`;
|
|
148
|
+
const date = new Date(t);
|
|
149
|
+
const mon = MONTHS[date.getMonth()] || '?';
|
|
150
|
+
if (date.getFullYear() === new Date(now).getFullYear()) return `${mon} ${date.getDate()}`;
|
|
151
|
+
return `${mon} ʼ${String(date.getFullYear()).slice(-2)}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** @param {number} n @param {string} w @returns {string} */
|
|
155
|
+
function plural(n, w) {
|
|
156
|
+
return `${n} ${w}${n === 1 ? '' : 's'}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── Error → guidance text ─────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Map a typed {@link ClientError} to guidance text (the host error banner +
|
|
163
|
+
* dump fallback).
|
|
164
|
+
* @param {ClientError} error
|
|
165
|
+
* @returns {string}
|
|
166
|
+
*/
|
|
167
|
+
function bannerFor(error) {
|
|
168
|
+
switch (error && error.kind) {
|
|
169
|
+
case 'crtr-missing':
|
|
170
|
+
return error.message;
|
|
171
|
+
case 'crtr-failed':
|
|
172
|
+
return 'crtr command failed: ' + (error.message || 'unknown error');
|
|
173
|
+
case 'parse':
|
|
174
|
+
return error.message || 'could not parse crtr output';
|
|
175
|
+
case 'error':
|
|
176
|
+
return (error && error.message) || 'Unknown error.';
|
|
177
|
+
default:
|
|
178
|
+
return (error && /** @type {any} */ (error).message) || 'Unknown error.';
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ── Forest builder ───────────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Build the flattened active-tree forest from a flat node list + the
|
|
186
|
+
* human-blocked id→count map.
|
|
187
|
+
*
|
|
188
|
+
* A tree is shown iff its subtree contains any LIVE node (status active|idle) OR
|
|
189
|
+
* any human-blocked node — so a fully-finished tree drops out, but a blocked one
|
|
190
|
+
* always surfaces. Shown trees are rendered IN FULL (finished children included)
|
|
191
|
+
* to preserve context, mirroring `crtr canvas dashboard`.
|
|
192
|
+
*
|
|
193
|
+
* @param {CanvasNode[]} nodes
|
|
194
|
+
* @param {Map<string,number>} blockedById
|
|
195
|
+
* @returns {{rows: TreeRow[], shownRoots: number}}
|
|
196
|
+
*/
|
|
197
|
+
function buildForest(nodes, blockedById) {
|
|
198
|
+
/** @type {Map<string, CanvasNode>} */
|
|
199
|
+
const byId = new Map();
|
|
200
|
+
for (const n of nodes) byId.set(n.nodeId, n);
|
|
201
|
+
|
|
202
|
+
/** @type {Map<string, CanvasNode[]>} */
|
|
203
|
+
const children = new Map();
|
|
204
|
+
/** @type {CanvasNode[]} */
|
|
205
|
+
const roots = [];
|
|
206
|
+
for (const n of nodes) {
|
|
207
|
+
const parent = n.parent && byId.has(n.parent) ? n.parent : null;
|
|
208
|
+
if (parent) {
|
|
209
|
+
const arr = children.get(parent);
|
|
210
|
+
if (arr) arr.push(n);
|
|
211
|
+
else children.set(parent, [n]);
|
|
212
|
+
} else {
|
|
213
|
+
roots.push(n);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// Order siblings + roots by birth time (matches spawn order).
|
|
217
|
+
const cmp = (/** @type {CanvasNode} */ a, /** @type {CanvasNode} */ b) =>
|
|
218
|
+
a.created < b.created ? -1 : a.created > b.created ? 1 : 0;
|
|
219
|
+
for (const arr of children.values()) arr.sort(cmp);
|
|
220
|
+
roots.sort(cmp);
|
|
221
|
+
|
|
222
|
+
// Subtree liveness, memoised + cycle-guarded (the graph is a forest, but the
|
|
223
|
+
// db is mutable — guard defensively).
|
|
224
|
+
/** @type {Map<string, boolean>} */
|
|
225
|
+
const liveMemo = new Map();
|
|
226
|
+
/** @param {string} id @param {Set<string>} guard @returns {boolean} */
|
|
227
|
+
function subtreeLive(id, guard) {
|
|
228
|
+
const memo = liveMemo.get(id);
|
|
229
|
+
if (memo !== undefined) return memo;
|
|
230
|
+
if (guard.has(id)) return false;
|
|
231
|
+
guard.add(id);
|
|
232
|
+
const node = byId.get(id);
|
|
233
|
+
let res = !!node && (LIVE_STATUS.has(node.status) || blockedById.has(id));
|
|
234
|
+
if (!res) {
|
|
235
|
+
for (const c of children.get(id) || []) {
|
|
236
|
+
if (subtreeLive(c.nodeId, guard)) {
|
|
237
|
+
res = true;
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
guard.delete(id);
|
|
243
|
+
liveMemo.set(id, res);
|
|
244
|
+
return res;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** @param {CanvasNode} node @param {string} prefix @returns {TreeRow} */
|
|
248
|
+
function makeRow(node, prefix) {
|
|
249
|
+
const askCount = blockedById.get(node.nodeId) || 0;
|
|
250
|
+
return {
|
|
251
|
+
nodeId: node.nodeId,
|
|
252
|
+
prefix,
|
|
253
|
+
glyph: STATUS_GLYPH[node.status] || '?',
|
|
254
|
+
status: node.status,
|
|
255
|
+
name: node.name,
|
|
256
|
+
kind: node.kind,
|
|
257
|
+
mode: node.mode,
|
|
258
|
+
lifecycle: node.lifecycle,
|
|
259
|
+
shortId: shortId(node.nodeId),
|
|
260
|
+
created: node.created,
|
|
261
|
+
blocked: askCount > 0,
|
|
262
|
+
askCount,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** @type {TreeRow[]} */
|
|
267
|
+
const rows = [];
|
|
268
|
+
/** @type {Set<string>} */
|
|
269
|
+
const visited = new Set();
|
|
270
|
+
/** @param {CanvasNode} node @param {string} indent @param {boolean} isLast @param {boolean} isRoot */
|
|
271
|
+
function walk(node, indent, isLast, isRoot) {
|
|
272
|
+
if (visited.has(node.nodeId)) return; // cycle guard
|
|
273
|
+
visited.add(node.nodeId);
|
|
274
|
+
const prefix = isRoot ? '' : indent + (isLast ? '└─ ' : '├─ ');
|
|
275
|
+
rows.push(makeRow(node, prefix));
|
|
276
|
+
const kids = children.get(node.nodeId) || [];
|
|
277
|
+
const childIndent = isRoot ? '' : indent + (isLast ? ' ' : '│ ');
|
|
278
|
+
for (let i = 0; i < kids.length; i++) {
|
|
279
|
+
walk(kids[i], childIndent, i === kids.length - 1, false);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
let shownRoots = 0;
|
|
284
|
+
for (const r of roots) {
|
|
285
|
+
if (!subtreeLive(r.nodeId, new Set())) continue;
|
|
286
|
+
shownRoots++;
|
|
287
|
+
walk(r, '', true, true);
|
|
288
|
+
}
|
|
289
|
+
return { rows, shownRoots };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ── Chrome copy (subtitle / footer / banner / dump) ───────────────────────────
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Live title subtitle — canvas-wide health (design §3 "a dim ` · <subtitle>`").
|
|
296
|
+
* `null` ⇒ no subtitle (the title leads alone) on an empty canvas.
|
|
297
|
+
* @param {CanvasState} state @returns {string|null}
|
|
298
|
+
*/
|
|
299
|
+
function subtitleFor(state) {
|
|
300
|
+
if (state.totalNodes === 0) return null;
|
|
301
|
+
return `${state.activeCount} active · ${plural(state.totalNodes, 'node')}`;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Footer status (left, transient) — the RENDERED forest scope, distinct from the
|
|
306
|
+
* canvas-wide subtitle. `null` ⇒ nothing (the empty/loading state speaks).
|
|
307
|
+
* @param {CanvasState} state @returns {string|null}
|
|
308
|
+
*/
|
|
309
|
+
function footerSummary(state) {
|
|
310
|
+
if (state.rows.length === 0) return null;
|
|
311
|
+
return `${plural(state.shownRoots, 'live tree')} · ${state.rows.length} shown`;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** @param {CanvasState} state @returns {string} */
|
|
315
|
+
function dumpSummary(state) {
|
|
316
|
+
return (
|
|
317
|
+
`${plural(state.shownRoots, 'tree')} · ${state.rows.length} shown · ` +
|
|
318
|
+
`${state.activeCount} active · ${plural(state.attnTotal, 'ask')} · ` +
|
|
319
|
+
`${state.totalNodes} total`
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ── Row → ListItemRow (left spans + right-flushed age) ─────────────────────────
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Build one list row: a 1-cell left gutter (§2), the tree prefix (dim), the
|
|
327
|
+
* status glyph (hue), the name (weight = status), the dim `[kind/mode]` cue, and
|
|
328
|
+
* a bright-yellow `⚑N` attention flag when blocked — with the relative age
|
|
329
|
+
* RIGHT-FLUSHED into a clean scannable column via `ListItemRow.right`.
|
|
330
|
+
* @param {TreeRow} r @param {number} now
|
|
331
|
+
* @returns {import('../../core/tui/draw.js').ListItemRow}
|
|
332
|
+
*/
|
|
333
|
+
function rowToItem(r, now) {
|
|
334
|
+
/** @type {import('../../core/tui/draw.js').Span[]} */
|
|
335
|
+
const spans = [{ text: ' ', style: undefined }]; // 1-cell gutter (rides the cursor bg)
|
|
336
|
+
if (r.prefix) spans.push({ text: r.prefix, style: { dim: true } });
|
|
337
|
+
spans.push({ text: r.glyph, style: glyphStyle(r.status) });
|
|
338
|
+
spans.push({ text: ' ', style: undefined });
|
|
339
|
+
spans.push({ text: r.name, style: nameStyle(r.status) });
|
|
340
|
+
spans.push({ text: ` [${r.kind}/${r.mode}]`, style: { fg: '90', dim: true } }); // muted: grey + dim (mono-safe)
|
|
341
|
+
if (r.blocked) spans.push({ text: ` ⚑${r.askCount}`, style: { fg: '93', bold: true } }); // attention
|
|
342
|
+
|
|
343
|
+
const age = relAge(r.created, now);
|
|
344
|
+
if (age) {
|
|
345
|
+
return { spans, right: [{ text: age, style: { fg: '90', dim: true } }] };
|
|
346
|
+
}
|
|
347
|
+
return { spans };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ── Refresh (data lane) ──────────────────────────────────────────────────────
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Pull the node graph + attention, rebuild the active-tree forest. Runs in the
|
|
354
|
+
* host's single-flight lane (launch, refreshMs, and `{type:'refresh'}`). Maps any
|
|
355
|
+
* fetch failure to guidance (a banner + the data-freshness chip) instead of
|
|
356
|
+
* crashing, and KEEPS the last-known forest on a transient failure.
|
|
357
|
+
* @param {CanvasState} state
|
|
358
|
+
* @param {import('../../core/tui/contract.js').ViewHost} host
|
|
359
|
+
* @returns {Promise<void>}
|
|
360
|
+
*/
|
|
361
|
+
async function refresh(state, host) {
|
|
362
|
+
host.setStatus('Loading the canvas…');
|
|
363
|
+
|
|
364
|
+
const rn = await fetchNodes();
|
|
365
|
+
if (!rn.ok) {
|
|
366
|
+
// Data source down. KEEP the last-known forest; raise the cause as the error
|
|
367
|
+
// banner → the host derives the BLOCKED (red) chip. The not-ready takeover
|
|
368
|
+
// only owns the screen when there is nothing to keep (see render()).
|
|
369
|
+
state.sourceError = bannerFor(rn.error);
|
|
370
|
+
host.setStatus(null);
|
|
371
|
+
host.setError(state.sourceError);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
state.sourceError = null;
|
|
375
|
+
const nodes = rn.data;
|
|
376
|
+
|
|
377
|
+
// Attention is best-effort: a failure here still renders the graph (just
|
|
378
|
+
// without the blocked flags), so the monitor degrades gracefully.
|
|
379
|
+
/** @type {Map<string,number>} */
|
|
380
|
+
const blockedById = new Map();
|
|
381
|
+
let attnTotal = 0;
|
|
382
|
+
const ra = await fetchAttention();
|
|
383
|
+
if (ra.ok) {
|
|
384
|
+
for (const it of ra.data.items) {
|
|
385
|
+
if (it.nodeId) blockedById.set(it.nodeId, it.count);
|
|
386
|
+
}
|
|
387
|
+
attnTotal = ra.data.total;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const built = buildForest(nodes, blockedById);
|
|
391
|
+
state.rows = built.rows;
|
|
392
|
+
state.shownRoots = built.shownRoots;
|
|
393
|
+
state.totalNodes = nodes.length;
|
|
394
|
+
state.activeCount = nodes.filter((n) => n.status === 'active').length;
|
|
395
|
+
state.attnTotal = attnTotal;
|
|
396
|
+
if (state.cursor >= state.rows.length) state.cursor = Math.max(0, state.rows.length - 1);
|
|
397
|
+
state.lastFetch = Date.now();
|
|
398
|
+
|
|
399
|
+
host.setSubtitle(subtitleFor(state));
|
|
400
|
+
|
|
401
|
+
// Data-freshness → state chip. Pending human asks are the one thing that wants
|
|
402
|
+
// a human: raise an ACTION banner → the host derives the ATTENTION (yellow)
|
|
403
|
+
// chip. Otherwise clear → READY (green). (busy→working is automatic.)
|
|
404
|
+
if (attnTotal > 0) {
|
|
405
|
+
host.setBanner(`${plural(attnTotal, 'ask')} waiting on a human — see the ⚑ rows`, 'action');
|
|
406
|
+
} else {
|
|
407
|
+
host.setError(null);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
host.setStatus(footerSummary(state));
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ── ViewModule ───────────────────────────────────────────────────────────────
|
|
414
|
+
|
|
415
|
+
/** @type {import('../../core/tui/contract.js').ViewModule<CanvasState>} */
|
|
416
|
+
const view = {
|
|
417
|
+
manifest: {
|
|
418
|
+
id: 'canvas',
|
|
419
|
+
title: 'Canvas',
|
|
420
|
+
description: 'Live agent graph — who is working, who is blocked',
|
|
421
|
+
refreshMs: 3000,
|
|
422
|
+
keymap: [
|
|
423
|
+
{ keys: 'j/k', label: 'move' },
|
|
424
|
+
{ keys: 'g', label: 'refresh' },
|
|
425
|
+
{ keys: 'q', label: 'quit' },
|
|
426
|
+
],
|
|
427
|
+
},
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Cheap + synchronous initial state — NO slow fetch (the host paints a loading
|
|
431
|
+
* frame, then calls refresh()).
|
|
432
|
+
* @returns {CanvasState}
|
|
433
|
+
*/
|
|
434
|
+
init() {
|
|
435
|
+
return {
|
|
436
|
+
rows: [],
|
|
437
|
+
cursor: 0,
|
|
438
|
+
scroll: 0,
|
|
439
|
+
shownRoots: 0,
|
|
440
|
+
totalNodes: 0,
|
|
441
|
+
activeCount: 0,
|
|
442
|
+
attnTotal: 0,
|
|
443
|
+
lastFetch: 0,
|
|
444
|
+
sourceError: null,
|
|
445
|
+
};
|
|
446
|
+
},
|
|
447
|
+
|
|
448
|
+
refresh,
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Paint the forest, or one of the four standard states. Pure (reads state,
|
|
452
|
+
* calls draw.*); the only state write is storing draw.list's adjusted scroll
|
|
453
|
+
* back, per the Draw contract.
|
|
454
|
+
* @param {CanvasState} state
|
|
455
|
+
* @param {import('../../core/tui/draw.js').Draw} draw
|
|
456
|
+
* @param {import('../../core/tui/draw.js').Rect} content
|
|
457
|
+
*/
|
|
458
|
+
render(state, draw, content) {
|
|
459
|
+
if (content.width <= 0 || content.height <= 0) return;
|
|
460
|
+
|
|
461
|
+
if (state.rows.length === 0) {
|
|
462
|
+
// Hard not-ready: the data source is down and there is nothing to keep —
|
|
463
|
+
// a guided takeover owns the whole content rect (design §4/§5). The
|
|
464
|
+
// specific cause rides the host error banner (full-width); this names the
|
|
465
|
+
// state + the next action so the view never dead-ends.
|
|
466
|
+
if (state.sourceError) {
|
|
467
|
+
notReadyState(draw, content, {
|
|
468
|
+
glyph: '⚠',
|
|
469
|
+
glyphFg: '31', // red — pairs with the blocked chip
|
|
470
|
+
headline: 'Canvas unavailable',
|
|
471
|
+
explanation: 'crtr could not read the canvas graph.',
|
|
472
|
+
nextStep: 'Press g to retry.',
|
|
473
|
+
});
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
// First load in flight — a skeleton, not a blank screen.
|
|
477
|
+
if (state.lastFetch === 0) {
|
|
478
|
+
loadingState(draw, content, { rows: Math.min(5, content.height), label: 'Loading the canvas…' });
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
// Loaded, nothing to render — the reward state, two flavors.
|
|
482
|
+
if (state.totalNodes === 0) {
|
|
483
|
+
emptyState(draw, content, {
|
|
484
|
+
headline: 'No nodes on the canvas',
|
|
485
|
+
secondary: ['Spawn one with `crtr node new`.', 'Press g to refresh.'],
|
|
486
|
+
});
|
|
487
|
+
} else {
|
|
488
|
+
emptyState(draw, content, {
|
|
489
|
+
headline: 'All caught up',
|
|
490
|
+
secondary: [`${plural(state.totalNodes, 'node')} finished — none active.`, 'Press g to refresh.'],
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// The forest. A 1-row section gap below the header (§2 rhythm) when there is
|
|
497
|
+
// room; full-width list so the cursor highlight + age column reach the edges.
|
|
498
|
+
const now = Date.now();
|
|
499
|
+
const gap = content.height > 4 ? 1 : 0;
|
|
500
|
+
const listRect = {
|
|
501
|
+
row: content.row + gap,
|
|
502
|
+
col: content.col,
|
|
503
|
+
width: content.width,
|
|
504
|
+
height: content.height - gap,
|
|
505
|
+
};
|
|
506
|
+
const items = state.rows.map((r) => rowToItem(r, now));
|
|
507
|
+
const res = draw.list(listRect, items, state.cursor, state.scroll);
|
|
508
|
+
state.scroll = res.scroll; // store adjusted scroll back (Draw.list contract)
|
|
509
|
+
},
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Read-only navigation: j/k move the cursor, g refreshes, q quits. No async
|
|
513
|
+
* actions — this is a monitor, not a controller.
|
|
514
|
+
* @param {import('../../core/tui/contract.js').ViewKey} k
|
|
515
|
+
* @param {CanvasState} state
|
|
516
|
+
* @returns {import('../../core/tui/contract.js').ViewAction}
|
|
517
|
+
*/
|
|
518
|
+
onKey(k, state) {
|
|
519
|
+
const key = k.key;
|
|
520
|
+
const ch = k.input;
|
|
521
|
+
if (ch === 'q') return { type: 'quit' };
|
|
522
|
+
if (ch === 'g') return { type: 'refresh' };
|
|
523
|
+
if (key.downArrow || ch === 'j') {
|
|
524
|
+
if (state.rows.length) state.cursor = Math.min(state.rows.length - 1, state.cursor + 1);
|
|
525
|
+
return { type: 'render' };
|
|
526
|
+
}
|
|
527
|
+
if (key.upArrow || ch === 'k') {
|
|
528
|
+
state.cursor = Math.max(0, state.cursor - 1);
|
|
529
|
+
return { type: 'render' };
|
|
530
|
+
}
|
|
531
|
+
return { type: 'none' };
|
|
532
|
+
},
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Plain-text snapshot for the non-TTY / piped path. No ANSI. The host threads
|
|
536
|
+
* its current banner via `ctx` so guidance (a source error / a pending-ask
|
|
537
|
+
* action) surfaces without the view mirroring it into state.
|
|
538
|
+
* @param {CanvasState} state
|
|
539
|
+
* @param {import('../../core/tui/contract.js').DumpContext} [ctx]
|
|
540
|
+
* @returns {string}
|
|
541
|
+
*/
|
|
542
|
+
dump(state, ctx) {
|
|
543
|
+
/** @type {string[]} */
|
|
544
|
+
const lines = ['Canvas — live agent graph'];
|
|
545
|
+
const banner = ctx && ctx.banner ? ctx.banner : null;
|
|
546
|
+
if (banner) lines.push('', `[${banner.level}] ${banner.msg}`);
|
|
547
|
+
else if (state.sourceError) lines.push('', `[error] ${state.sourceError}`);
|
|
548
|
+
|
|
549
|
+
lines.push('', dumpSummary(state), '');
|
|
550
|
+
|
|
551
|
+
if (state.rows.length === 0) {
|
|
552
|
+
lines.push(
|
|
553
|
+
state.sourceError
|
|
554
|
+
? '(canvas unavailable)'
|
|
555
|
+
: state.lastFetch === 0
|
|
556
|
+
? '(loading…)'
|
|
557
|
+
: state.totalNodes === 0
|
|
558
|
+
? '(no nodes on the canvas)'
|
|
559
|
+
: '(no active trees)',
|
|
560
|
+
);
|
|
561
|
+
return lines.join('\n');
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const now = Date.now();
|
|
565
|
+
for (const r of state.rows) {
|
|
566
|
+
const blk = r.blocked ? ` ⚑${r.askCount}` : '';
|
|
567
|
+
const age = relAge(r.created, now);
|
|
568
|
+
lines.push(
|
|
569
|
+
`${r.prefix}${r.glyph} ${r.name} [${r.kind}/${r.mode}] ${lifeAbbr(r.lifecycle)} ${r.shortId} ${age}${blk}`,
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
return lines.join('\n');
|
|
573
|
+
},
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
export default view;
|