@crouton-kit/crouter 0.3.66 → 0.3.68
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 +1 -0
- package/dist/builtin-memory/05-kinds/developer/00-base.md +1 -1
- package/dist/builtin-memory/05-kinds/developer/01-orchestrator.md +2 -0
- package/dist/clients/attach/__tests__/attach-chrome-remote.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/attach-chrome-remote.test.js +371 -0
- package/dist/clients/attach/__tests__/attach-remote-readonly.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/attach-remote-readonly.test.js +191 -0
- package/dist/clients/attach/__tests__/transport-relay.test.js +38 -1
- package/dist/clients/attach/attach-cmd.d.ts +30 -2
- package/dist/clients/attach/attach-cmd.js +624 -624
- package/dist/clients/attach/canvas-panels.js +9 -3
- package/dist/clients/attach/graph-overlay.d.ts +5 -0
- package/dist/clients/attach/graph-overlay.js +40 -7
- package/dist/clients/attach/input-controller.d.ts +9 -0
- package/dist/clients/attach/input-controller.js +47 -8
- package/dist/clients/attach/slash-commands.d.ts +11 -4
- package/dist/clients/attach/slash-commands.js +44 -9
- package/dist/clients/attach/transport-relay.d.ts +5 -5
- package/dist/clients/attach/transport-relay.js +3 -30
- package/dist/commands/__tests__/canvas-config.test.d.ts +1 -0
- package/dist/commands/__tests__/canvas-config.test.js +107 -0
- package/dist/commands/canvas-browse.js +26 -1
- package/dist/commands/canvas-config.d.ts +2 -0
- package/dist/commands/canvas-config.js +210 -0
- package/dist/commands/canvas-use.d.ts +1 -0
- package/dist/commands/canvas-use.js +63 -0
- package/dist/commands/canvas.js +4 -2
- package/dist/commands/capture.d.ts +2 -0
- package/dist/commands/capture.js +28 -0
- package/dist/commands/node.js +41 -11
- package/dist/core/__tests__/remote-canvas-target.test.d.ts +1 -0
- package/dist/core/__tests__/remote-canvas-target.test.js +87 -0
- package/dist/core/__tests__/revive.test.js +70 -1
- package/dist/core/__tests__/secrets.test.d.ts +1 -0
- package/dist/core/__tests__/secrets.test.js +55 -0
- package/dist/core/canvas/__tests__/remote-canvas-source.test.d.ts +1 -0
- package/dist/core/canvas/__tests__/remote-canvas-source.test.js +371 -0
- package/dist/core/canvas/__tests__/remote-event-stream.test.d.ts +1 -0
- package/dist/core/canvas/__tests__/remote-event-stream.test.js +144 -0
- package/dist/core/canvas/__tests__/render-remote.test.d.ts +1 -0
- package/dist/core/canvas/__tests__/render-remote.test.js +221 -0
- package/dist/core/canvas/__tests__/source-resolve.test.d.ts +1 -0
- package/dist/core/canvas/__tests__/source-resolve.test.js +77 -0
- package/dist/core/canvas/browse/__tests__/rebuild-coalescer.test.d.ts +1 -0
- package/dist/core/canvas/browse/__tests__/rebuild-coalescer.test.js +117 -0
- package/dist/core/canvas/browse/app.d.ts +14 -0
- package/dist/core/canvas/browse/app.js +97 -15
- package/dist/core/canvas/browse/rebuild-coalescer.d.ts +7 -0
- package/dist/core/canvas/browse/rebuild-coalescer.js +61 -0
- package/dist/core/canvas/browse/render.d.ts +4 -0
- package/dist/core/canvas/browse/render.js +3 -1
- package/dist/core/canvas/nav-model.d.ts +19 -10
- package/dist/core/canvas/nav-model.js +30 -12
- package/dist/core/canvas/remote-canvas-source.d.ts +62 -0
- package/dist/core/canvas/remote-canvas-source.js +222 -0
- package/dist/core/canvas/remote-event-stream.d.ts +24 -0
- package/dist/core/canvas/remote-event-stream.js +94 -0
- package/dist/core/canvas/render.d.ts +13 -1
- package/dist/core/canvas/render.js +56 -37
- package/dist/core/canvas/source.d.ts +9 -0
- package/dist/core/canvas/source.js +15 -0
- package/dist/core/command.d.ts +15 -0
- package/dist/core/command.js +72 -0
- package/dist/core/config.js +4 -3
- package/dist/core/profiles/select.d.ts +4 -1
- package/dist/core/profiles/select.js +356 -81
- package/dist/core/runtime/__tests__/node-env.test.d.ts +1 -0
- package/dist/core/runtime/__tests__/node-env.test.js +91 -0
- package/dist/core/runtime/nodes.js +10 -0
- package/dist/core/runtime/revive.js +176 -139
- package/dist/core/runtime/tmux.js +4 -1
- package/dist/core/secrets.d.ts +25 -0
- package/dist/core/secrets.js +55 -0
- package/dist/core/view/__tests__/transport-remote.test.d.ts +1 -0
- package/dist/core/view/__tests__/transport-remote.test.js +95 -0
- package/dist/core/view/remote-canvas-target.d.ts +2 -1
- package/dist/core/view/remote-canvas-target.js +12 -7
- package/dist/core/view/transport-local.js +12 -3
- package/dist/core/view/transport-remote.d.ts +2 -0
- package/dist/core/view/transport-remote.js +53 -0
- package/dist/types.d.ts +19 -11
- package/dist/types.js +1 -1
- package/dist/web-client/assets/{index-BvzxXXGU.js → index-CnF5r8ky.js} +18 -18
- package/dist/web-client/index.html +1 -1
- package/dist/web-client/sw.js +1 -1
- package/package.json +1 -1
|
@@ -11,9 +11,11 @@ import { execFileSync } from 'node:child_process';
|
|
|
11
11
|
import { resolve } from 'node:path';
|
|
12
12
|
import { dashboardRowsAllFromSource, enrichRowsFromSource, loadPreviewFromSource, renderForestFromSource } from '../render.js';
|
|
13
13
|
import { localCanvasSource } from '../source.js';
|
|
14
|
+
import { RemoteCanvasSource } from '../remote-canvas-source.js';
|
|
14
15
|
import { closeNode } from '../../runtime/close.js';
|
|
15
16
|
import { setupTerminal, restoreTerminal, getTerminalSize, parseKeypress, } from '../../tui/terminal.js';
|
|
16
17
|
import { buildTree, flatten, pruneNode, TABS } from './model.js';
|
|
18
|
+
import { createCoalescedRebuilder } from './rebuild-coalescer.js';
|
|
17
19
|
// Sort cycle for the `s` key. Starts on the default `attention` ordering; one `s`
|
|
18
20
|
// press restores the structural `tree` view, then relevance/recency, then back.
|
|
19
21
|
const SORT_CYCLE = ['attention', 'tree', 'relevance', 'recency'];
|
|
@@ -26,8 +28,22 @@ function viewportHeight(rowsTotal, search, previewOn) {
|
|
|
26
28
|
const previewH = previewOn ? PREVIEW_HEIGHT : 0;
|
|
27
29
|
return Math.max(1, rows - headerHeight(search) - 1 /* footer */ - previewH);
|
|
28
30
|
}
|
|
31
|
+
/** Wires a RemoteEventStream's `nodes`/`inbox` events through the coalescer
|
|
32
|
+
* (`rebuild-coalescer.ts`) so a burst of SSE events collapses to ONE rebuild
|
|
33
|
+
* round, never one N+1 remote-subprocess rebuild per event. Exported (rather
|
|
34
|
+
* than left inline in `runBrowse`) so the coalescer integration test can
|
|
35
|
+
* drive this ACTUAL wiring, not just `createCoalescedRebuilder` in isolation
|
|
36
|
+
* — the test asserts this function routes through the coalescer rather than
|
|
37
|
+
* calling `rebuild()` directly per event. */
|
|
38
|
+
export function wireEventStreamRebuild(eventStream, rebuild) {
|
|
39
|
+
const rebuilder = createCoalescedRebuilder(rebuild);
|
|
40
|
+
const unsubscribe = eventStream.subscribe(() => rebuilder.trigger());
|
|
41
|
+
eventStream.start();
|
|
42
|
+
return { rebuilder, unsubscribe };
|
|
43
|
+
}
|
|
29
44
|
export async function runBrowse(opts = {}) {
|
|
30
45
|
const source = opts.source ?? localCanvasSource;
|
|
46
|
+
const isRemote = source instanceof RemoteCanvasSource;
|
|
31
47
|
// No TTY → print the static forest and exit 0 (no raw mode).
|
|
32
48
|
if (!process.stdin.isTTY) {
|
|
33
49
|
process.stdout.write(await renderForestFromSource(source) + '\n');
|
|
@@ -35,18 +51,41 @@ export async function runBrowse(opts = {}) {
|
|
|
35
51
|
}
|
|
36
52
|
// Snapshot the canvas. Drop kind:'human' control-plane decks — they have no pi
|
|
37
53
|
// session, so `node focus` refuses them; they are never a navigation/resume
|
|
38
|
-
// target (mirrors the node focus guard).
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
54
|
+
// target (mirrors the node focus guard). Factored into a function so the SSE
|
|
55
|
+
// listener below can re-pull the tree on a `nodes`/`inbox` event instead of
|
|
56
|
+
// recomputing over a stale snapshot.
|
|
57
|
+
let rows;
|
|
58
|
+
let nodeRows;
|
|
59
|
+
let rootIds;
|
|
60
|
+
let childIds;
|
|
61
|
+
let tree;
|
|
62
|
+
let totalNodes;
|
|
63
|
+
// Remote-only: pending-ask counts per node, merged from one
|
|
64
|
+
// `askCountsForView` call per forest root (RemoteCanvasSource.askCountsForView
|
|
65
|
+
// already shells a single `canvas attention map --view <root>` per root, same
|
|
66
|
+
// cost class as the per-node `subscriptionsOf` fan-out this snapshot already
|
|
67
|
+
// does below) — fetched once per snapshot rebuild, not per flush/keystroke, so
|
|
68
|
+
// enrichRowsFromSource's per-row loop stays a pure sync-shaped map lookup.
|
|
69
|
+
let remoteAskCounts = {};
|
|
70
|
+
const rebuildSnapshot = async () => {
|
|
71
|
+
rows = (await dashboardRowsAllFromSource(source)).filter((r) => r.kind !== 'human');
|
|
72
|
+
nodeRows = await source.listNodes();
|
|
73
|
+
rootIds = nodeRows
|
|
74
|
+
.filter((n) => n.parent === null && n.kind !== 'human')
|
|
75
|
+
.map((n) => n.node_id);
|
|
76
|
+
const nextChildIds = new Map();
|
|
77
|
+
await Promise.all(nodeRows.map(async (n) => {
|
|
78
|
+
nextChildIds.set(n.node_id, (await source.subscriptionsOf(n.node_id).catch(() => [])).map((s) => s.node_id));
|
|
79
|
+
}));
|
|
80
|
+
childIds = nextChildIds;
|
|
81
|
+
tree = buildTree(rows, rootIds, (id) => childIds.get(id) ?? []);
|
|
82
|
+
totalNodes = tree.nodes.size;
|
|
83
|
+
if (isRemote) {
|
|
84
|
+
const maps = await Promise.all(rootIds.map((root) => source.askCountsForView(root).catch(() => ({}))));
|
|
85
|
+
remoteAskCounts = Object.assign({}, ...maps);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
await rebuildSnapshot();
|
|
50
89
|
// Default cwd scope = the dir browse was launched from (the request). The popup
|
|
51
90
|
// / command passes --cwd; resolve it so it compares cleanly against stored cwds.
|
|
52
91
|
// null when unknown → All dirs (the toggle's other state).
|
|
@@ -82,10 +121,43 @@ export async function runBrowse(opts = {}) {
|
|
|
82
121
|
restoreTerminal();
|
|
83
122
|
}
|
|
84
123
|
catch { /* best-effort */ }
|
|
124
|
+
unsubscribeEventStream?.();
|
|
125
|
+
rebuilder?.stop();
|
|
126
|
+
opts.eventStream?.stop();
|
|
85
127
|
};
|
|
86
128
|
// Safety net: an uncaught throw in the (un-unit-tested) keystroke path must
|
|
87
129
|
// never strand the tty in raw + alt-screen + hidden-cursor.
|
|
88
130
|
process.once('exit', cleanup);
|
|
131
|
+
// Remote-only live invalidation: `canvas browse` has no local fs-watch/poll to
|
|
132
|
+
// replace (there is none — this is a one-shot snapshot TUI, re-rendering only
|
|
133
|
+
// on keystrokes). This is net-new plumbing for the remote case only: when a
|
|
134
|
+
// pre-built RemoteEventStream is threaded in (canvas-browse.ts, once it has
|
|
135
|
+
// already resolved the target's credentials), a `nodes`/`inbox` SSE event
|
|
136
|
+
// invalidates the remote source's cache and triggers the same recompute +
|
|
137
|
+
// coalesced repaint a keystroke would. The local (no eventStream) path is
|
|
138
|
+
// untouched.
|
|
139
|
+
let unsubscribeEventStream;
|
|
140
|
+
let rebuilder;
|
|
141
|
+
if (opts.eventStream !== undefined) {
|
|
142
|
+
if (source instanceof RemoteCanvasSource)
|
|
143
|
+
source.invalidate();
|
|
144
|
+
// `trigger()` debounces a burst into one rebuild, and single-flights it
|
|
145
|
+
// against an already-in-flight rebuild — see wireEventStreamRebuild above.
|
|
146
|
+
const wired = wireEventStreamRebuild(opts.eventStream, async () => {
|
|
147
|
+
try {
|
|
148
|
+
if (source instanceof RemoteCanvasSource)
|
|
149
|
+
source.invalidate();
|
|
150
|
+
await rebuildSnapshot();
|
|
151
|
+
recompute(curRow()?.id);
|
|
152
|
+
scheduleFlush();
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
logAsyncFailure('SSE rebuild failed', err);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
rebuilder = wired.rebuilder;
|
|
159
|
+
unsubscribeEventStream = wired.unsubscribe;
|
|
160
|
+
}
|
|
89
161
|
const logAsyncFailure = (label, err) => {
|
|
90
162
|
process.stderr.write(`[crtr browse] ${label}: ${String(err?.message ?? err)}\n`);
|
|
91
163
|
};
|
|
@@ -154,7 +226,7 @@ export async function runBrowse(opts = {}) {
|
|
|
154
226
|
if (r !== undefined)
|
|
155
227
|
slice.push(r);
|
|
156
228
|
}
|
|
157
|
-
await enrichRowsFromSource(source, slice);
|
|
229
|
+
await enrichRowsFromSource(source, slice, remoteAskCounts);
|
|
158
230
|
const cur = curRow();
|
|
159
231
|
if (cur !== undefined) {
|
|
160
232
|
const r = rowOf(cur.id);
|
|
@@ -166,6 +238,7 @@ export async function runBrowse(opts = {}) {
|
|
|
166
238
|
query: state.query, search: state.search, totalNodes,
|
|
167
239
|
cwdScope: state.cwdScope, sort: state.sort, preview: state.preview,
|
|
168
240
|
residentsOnly: state.residentsOnly, pendingClose: state.pendingClose,
|
|
241
|
+
readOnly: isRemote,
|
|
169
242
|
}, size, caps);
|
|
170
243
|
process.stdout.write(frame);
|
|
171
244
|
};
|
|
@@ -459,7 +532,10 @@ export async function runBrowse(opts = {}) {
|
|
|
459
532
|
}
|
|
460
533
|
// Close-out the selected node (+ its exclusive subtree). If anything in that
|
|
461
534
|
// subtree is actively streaming, confirm first (y/n); otherwise close at once.
|
|
535
|
+
// No-op against a remote canvas — mutations are disabled (footer says so).
|
|
462
536
|
if (input === 'x') {
|
|
537
|
+
if (isRemote)
|
|
538
|
+
return;
|
|
463
539
|
if (row === undefined) {
|
|
464
540
|
scheduleFlush();
|
|
465
541
|
return;
|
|
@@ -510,7 +586,10 @@ export async function runBrowse(opts = {}) {
|
|
|
510
586
|
// waiting out the 5-min auto-revive grace. Shells `node lifecycle revive <id>
|
|
511
587
|
// --now` (browse goes through commands, never touching the runtime directly). 'k' is
|
|
512
588
|
// vim-up here, so the kick is on SHIFT-K.
|
|
589
|
+
// No-op against a remote canvas — mutations are disabled (footer says so).
|
|
513
590
|
if (input === 'K') {
|
|
591
|
+
if (isRemote)
|
|
592
|
+
return;
|
|
514
593
|
if (row !== undefined) {
|
|
515
594
|
const r = rowOf(row.id);
|
|
516
595
|
if (r?.hanging != null)
|
|
@@ -519,8 +598,11 @@ export async function runBrowse(opts = {}) {
|
|
|
519
598
|
scheduleFlush();
|
|
520
599
|
return;
|
|
521
600
|
}
|
|
522
|
-
// Resume.
|
|
601
|
+
// Resume. No-op against a remote canvas — mutations/focus are disabled
|
|
602
|
+
// (footer says so).
|
|
523
603
|
if (key.return) {
|
|
604
|
+
if (isRemote)
|
|
605
|
+
return;
|
|
524
606
|
if (row !== undefined)
|
|
525
607
|
selectAndFocus(row.id);
|
|
526
608
|
return;
|
|
@@ -566,7 +648,7 @@ export async function runBrowse(opts = {}) {
|
|
|
566
648
|
return;
|
|
567
649
|
const end = Math.min(warmIdx + WARM_CHUNK, warmRows.length);
|
|
568
650
|
const slice = warmRows.slice(warmIdx, end);
|
|
569
|
-
await enrichRowsFromSource(source, slice);
|
|
651
|
+
await enrichRowsFromSource(source, slice, remoteAskCounts);
|
|
570
652
|
await Promise.all(slice.map((r) => loadPreviewFromSource(source, r)));
|
|
571
653
|
warmIdx = end;
|
|
572
654
|
// A live search may match the freshly-warmed prompts → recompute + repaint.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface CoalescedRebuilder {
|
|
2
|
+
/** Called on every SSE event. Cheap — only schedules/marks work. */
|
|
3
|
+
trigger: () => void;
|
|
4
|
+
/** Cancels any pending debounce timer (does not abort an in-flight rebuild). */
|
|
5
|
+
stop: () => void;
|
|
6
|
+
}
|
|
7
|
+
export declare function createCoalescedRebuilder(rebuild: () => Promise<void>, debounceMs?: number): CoalescedRebuilder;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// rebuild-coalescer.ts — single-flight + debounce coalescing for a burst of
|
|
2
|
+
// SSE-triggered rebuilds (`canvas browse`'s remote path — see app.ts).
|
|
3
|
+
//
|
|
4
|
+
// A remote `RemoteEventStream` `nodes`/`inbox` event means "invalidate + re-pull
|
|
5
|
+
// the tree", which for a remote source costs one `node inspect list` plus N
|
|
6
|
+
// `node inspect show` round trips. A burst of events (several nodes changing at
|
|
7
|
+
// once) must NOT fan into one overlapping rebuild per event — it must collapse
|
|
8
|
+
// to one rebuild per burst. This module is the pure, TTY-free coalescing policy
|
|
9
|
+
// so it can be unit-tested without the raw-mode stdin loop in app.ts:
|
|
10
|
+
//
|
|
11
|
+
// - `trigger()` debounces a burst into ONE rebuild call after `debounceMs` of
|
|
12
|
+
// quiet.
|
|
13
|
+
// - if a rebuild is already in flight when the debounce fires (or another
|
|
14
|
+
// trigger lands mid-rebuild), it does NOT launch an overlapping rebuild —
|
|
15
|
+
// it marks exactly one trailing rebuild to run immediately after the
|
|
16
|
+
// in-flight one finishes.
|
|
17
|
+
//
|
|
18
|
+
// Net effect: N events in a burst → 1 rebuild (or, if the burst spans an
|
|
19
|
+
// in-flight rebuild, 2: the one already running + one trailing catch-up) —
|
|
20
|
+
// never N overlapping rebuilds.
|
|
21
|
+
export function createCoalescedRebuilder(rebuild, debounceMs = 200) {
|
|
22
|
+
let timer;
|
|
23
|
+
let inFlight = false;
|
|
24
|
+
let pending = false;
|
|
25
|
+
const run = () => {
|
|
26
|
+
inFlight = true;
|
|
27
|
+
void rebuild().finally(() => {
|
|
28
|
+
inFlight = false;
|
|
29
|
+
if (pending) {
|
|
30
|
+
// A trailing catch-up was armed while this rebuild ran — fire it
|
|
31
|
+
// immediately (no further debounce: the just-finished rebuild WAS the
|
|
32
|
+
// quiet period), collapsing every event that landed mid-flight into
|
|
33
|
+
// exactly one follow-up rebuild, not one per event.
|
|
34
|
+
pending = false;
|
|
35
|
+
run();
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
const trigger = () => {
|
|
40
|
+
if (inFlight) {
|
|
41
|
+
// Already rebuilding — mark one trailing catch-up instead of starting a
|
|
42
|
+
// second overlapping rebuild or re-arming the debounce timer (which
|
|
43
|
+
// would keep pushing the catch-up back on every further event).
|
|
44
|
+
pending = true;
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (timer !== undefined)
|
|
48
|
+
return; // already debounced in this quiet window
|
|
49
|
+
timer = setTimeout(() => {
|
|
50
|
+
timer = undefined;
|
|
51
|
+
run();
|
|
52
|
+
}, debounceMs);
|
|
53
|
+
};
|
|
54
|
+
const stop = () => {
|
|
55
|
+
if (timer !== undefined) {
|
|
56
|
+
clearTimeout(timer);
|
|
57
|
+
timer = undefined;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
return { trigger, stop };
|
|
61
|
+
}
|
|
@@ -29,6 +29,10 @@ export interface RenderState {
|
|
|
29
29
|
* a descendant) is actively streaming. Holds the node id being confirmed; the
|
|
30
30
|
* footer becomes the y/n prompt. null/undefined = no pending confirm. */
|
|
31
31
|
pendingClose?: string | null;
|
|
32
|
+
/** Set when the source is a remote canvas: mutation/focus keys (`x`/`K`/Enter)
|
|
33
|
+
* are disabled, so the footer drops their hints instead of advertising a
|
|
34
|
+
* dead action. undefined/false = local, byte-identical footer. */
|
|
35
|
+
readOnly?: boolean;
|
|
32
36
|
}
|
|
33
37
|
/**
|
|
34
38
|
* Render the whole frame. Returns a single string that, written as-is, repaints
|
|
@@ -479,7 +479,9 @@ export function renderFrame(state, size, caps = { color: false, color256: false
|
|
|
479
479
|
const hangingHint = cursorRow?.hanging != null ? 'K revive now ' : '';
|
|
480
480
|
const footer = state.search
|
|
481
481
|
? '⏎ commit Esc cancel ⌫ delete'
|
|
482
|
-
:
|
|
482
|
+
: state.readOnly === true
|
|
483
|
+
? '↑↓ move →/← tree Tab tabs / search s sort c cwd r residents p preview q quit — read-only remote canvas'
|
|
484
|
+
: `${hangingHint}↑↓ move →/← tree ⏎ resume x close Tab tabs / search s sort c cwd r residents p preview q quit`;
|
|
483
485
|
lines.push(`${DIM}${clip(footer, width)}${RESET}`);
|
|
484
486
|
}
|
|
485
487
|
// Assemble: home, each line cleared to EOL, then clear below.
|
|
@@ -19,8 +19,10 @@ export declare const YELLOW = "\u001B[33m";
|
|
|
19
19
|
export declare const CYAN = "\u001B[36m";
|
|
20
20
|
export declare const GRAY = "\u001B[90m";
|
|
21
21
|
/** Overlay-aware glyph for a node: hanging ⚠ outranks streaming ⟳, which
|
|
22
|
-
* outranks the base status glyph.
|
|
23
|
-
|
|
22
|
+
* outranks the base status glyph. `remote` skips the local `readFault` /
|
|
23
|
+
* `isPidAlive` / `isBusy` reads entirely (a remote node id may collide with a
|
|
24
|
+
* local one) and renders the bare status glyph instead. */
|
|
25
|
+
export declare function nodeGlyph(node: NodeMeta | null, remote?: boolean): string;
|
|
24
26
|
/** Visible width, ignoring ANSI escapes. */
|
|
25
27
|
export declare function visibleWidth(s: string): number;
|
|
26
28
|
/** Truncate to `max` VISIBLE columns: escape sequences are copied through
|
|
@@ -59,11 +61,14 @@ export interface Telemetry {
|
|
|
59
61
|
export declare function readTelemetry(nodeId: string): Telemetry;
|
|
60
62
|
export declare function fmtTokens(n: number): string;
|
|
61
63
|
/** The context-window cell — live window fill rounded to the nearest 1k (the same
|
|
62
|
-
* figure pi's footer shows), NOT a per-turn token delta.
|
|
63
|
-
|
|
64
|
+
* figure pi's footer shows), NOT a per-turn token delta. `remote` skips the local
|
|
65
|
+
* `telemetry.json` read entirely and reports 0 (a remote node id may collide
|
|
66
|
+
* with a local one). */
|
|
67
|
+
export declare function tokensCell(id: string, remote?: boolean): string;
|
|
64
68
|
/** Dimmed live "what is it doing" cue for an ACTIVE node — the last tool it ran.
|
|
65
|
-
* Empty for non-active rows (stale once a node parks) and when none is recorded.
|
|
66
|
-
|
|
69
|
+
* Empty for non-active rows (stale once a node parks) and when none is recorded.
|
|
70
|
+
* `remote` skips the local `telemetry.json` read entirely and reports empty. */
|
|
71
|
+
export declare function activityCell(id: string, node: NodeMeta | null, remote?: boolean): string;
|
|
67
72
|
/** Revive-count badge (meta.cycles). Hidden on the first cycle (0) to cut noise. */
|
|
68
73
|
export declare function cycleBadge(node: NodeMeta | null): string;
|
|
69
74
|
/** Short on-screen label: the explicit handle when set, else the pi-generated
|
|
@@ -71,10 +76,14 @@ export declare function cycleBadge(node: NodeMeta | null): string;
|
|
|
71
76
|
* the nav chrome shows just the first so rows stay compact. */
|
|
72
77
|
export declare function navLabel(node: NodeMeta | null, id: string): string;
|
|
73
78
|
export declare function shortId(id: string): string;
|
|
74
|
-
/** Node ids currently shown in a tmux focus viewport. Built once per render.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
export declare function
|
|
79
|
+
/** Node ids currently shown in a tmux focus viewport. Built once per render.
|
|
80
|
+
* `remote` skips the local `listFocuses()` SQLite read entirely and reports
|
|
81
|
+
* none focused (a remote node id may collide with a local one). */
|
|
82
|
+
export declare function focusedNodeIds(remote?: boolean): Set<string>;
|
|
83
|
+
/** True when a human is watching `id` right now (tmux focus or broker viewer).
|
|
84
|
+
* `remote` skips the local `attach.json` read entirely and reports false (a
|
|
85
|
+
* remote node id may collide with a local one). */
|
|
86
|
+
export declare function isAttached(id: string, node: NodeMeta | null, focused: ReadonlySet<string>, remote?: boolean): boolean;
|
|
78
87
|
export declare function fetchAsksMap(rootId: string): Record<string, number>;
|
|
79
88
|
/** First manager (by created) — the UP step for the ancestry spine. */
|
|
80
89
|
export declare function managerOf(id: string): string | undefined;
|
|
@@ -63,11 +63,16 @@ function streamingFor(node) {
|
|
|
63
63
|
return isBusy(node.node_id);
|
|
64
64
|
}
|
|
65
65
|
/** Overlay-aware glyph for a node: hanging ⚠ outranks streaming ⟳, which
|
|
66
|
-
* outranks the base status glyph.
|
|
67
|
-
|
|
66
|
+
* outranks the base status glyph. `remote` skips the local `readFault` /
|
|
67
|
+
* `isPidAlive` / `isBusy` reads entirely (a remote node id may collide with a
|
|
68
|
+
* local one) and renders the bare status glyph instead. */
|
|
69
|
+
export function nodeGlyph(node, remote = false) {
|
|
68
70
|
if (node === null)
|
|
69
71
|
return '?';
|
|
70
|
-
const v = resolveNodeVisual(node.status, {
|
|
72
|
+
const v = resolveNodeVisual(node.status, {
|
|
73
|
+
streaming: remote ? false : streamingFor(node),
|
|
74
|
+
hanging: remote ? null : hangingFor(node),
|
|
75
|
+
});
|
|
71
76
|
return `${ESC}${v.color}m${v.glyph}${RESET}`;
|
|
72
77
|
}
|
|
73
78
|
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
@@ -205,13 +210,18 @@ export function fmtTokens(n) {
|
|
|
205
210
|
return n < 1_000 ? `${n}` : `${Math.round(n / 1_000)}k`;
|
|
206
211
|
}
|
|
207
212
|
/** The context-window cell — live window fill rounded to the nearest 1k (the same
|
|
208
|
-
* figure pi's footer shows), NOT a per-turn token delta.
|
|
209
|
-
|
|
210
|
-
|
|
213
|
+
* figure pi's footer shows), NOT a per-turn token delta. `remote` skips the local
|
|
214
|
+
* `telemetry.json` read entirely and reports 0 (a remote node id may collide
|
|
215
|
+
* with a local one). */
|
|
216
|
+
export function tokensCell(id, remote = false) {
|
|
217
|
+
return fmtTokens(remote ? 0 : (readTelemetry(id).context_tokens ?? 0));
|
|
211
218
|
}
|
|
212
219
|
/** Dimmed live "what is it doing" cue for an ACTIVE node — the last tool it ran.
|
|
213
|
-
* Empty for non-active rows (stale once a node parks) and when none is recorded.
|
|
214
|
-
|
|
220
|
+
* Empty for non-active rows (stale once a node parks) and when none is recorded.
|
|
221
|
+
* `remote` skips the local `telemetry.json` read entirely and reports empty. */
|
|
222
|
+
export function activityCell(id, node, remote = false) {
|
|
223
|
+
if (remote)
|
|
224
|
+
return '';
|
|
215
225
|
if (node?.status !== 'active')
|
|
216
226
|
return '';
|
|
217
227
|
const a = (readTelemetry(id).last_activity ?? '').trim();
|
|
@@ -248,8 +258,12 @@ export function shortId(id) {
|
|
|
248
258
|
// (src/core/runtime/broker.ts). Trusted only while the node is
|
|
249
259
|
// 'active': a broker crash can leave a stale file.
|
|
250
260
|
// ---------------------------------------------------------------------------
|
|
251
|
-
/** Node ids currently shown in a tmux focus viewport. Built once per render.
|
|
252
|
-
|
|
261
|
+
/** Node ids currently shown in a tmux focus viewport. Built once per render.
|
|
262
|
+
* `remote` skips the local `listFocuses()` SQLite read entirely and reports
|
|
263
|
+
* none focused (a remote node id may collide with a local one). */
|
|
264
|
+
export function focusedNodeIds(remote = false) {
|
|
265
|
+
if (remote)
|
|
266
|
+
return new Set();
|
|
253
267
|
try {
|
|
254
268
|
return new Set(listFocuses().map((f) => f.node_id));
|
|
255
269
|
}
|
|
@@ -257,8 +271,12 @@ export function focusedNodeIds() {
|
|
|
257
271
|
return new Set();
|
|
258
272
|
}
|
|
259
273
|
}
|
|
260
|
-
/** True when a human is watching `id` right now (tmux focus or broker viewer).
|
|
261
|
-
|
|
274
|
+
/** True when a human is watching `id` right now (tmux focus or broker viewer).
|
|
275
|
+
* `remote` skips the local `attach.json` read entirely and reports false (a
|
|
276
|
+
* remote node id may collide with a local one). */
|
|
277
|
+
export function isAttached(id, node, focused, remote = false) {
|
|
278
|
+
if (remote)
|
|
279
|
+
return false;
|
|
262
280
|
if (focused.has(id))
|
|
263
281
|
return true;
|
|
264
282
|
if (node?.status !== 'active')
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { CanvasSource } from './source.js';
|
|
2
|
+
import type { NodeMeta, NodeRow, NodeStatus, SubscriptionRef } from './types.js';
|
|
3
|
+
/** The exact JSON shape `crtr --json node inspect show <id>` emits (see
|
|
4
|
+
* `nodeShow`'s `run`, `src/commands/node.ts`) — mirrored here, not imported,
|
|
5
|
+
* to avoid a commands→canvas value dependency. */
|
|
6
|
+
export interface RemoteShowResponse {
|
|
7
|
+
node: NodeMeta;
|
|
8
|
+
reports: {
|
|
9
|
+
node_id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
kind: string;
|
|
12
|
+
status: string;
|
|
13
|
+
active: boolean;
|
|
14
|
+
}[];
|
|
15
|
+
managers: {
|
|
16
|
+
node_id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
kind: string;
|
|
19
|
+
status: string;
|
|
20
|
+
active: boolean;
|
|
21
|
+
}[];
|
|
22
|
+
artifacts: {
|
|
23
|
+
report: number;
|
|
24
|
+
doc: number;
|
|
25
|
+
roadmap: number;
|
|
26
|
+
};
|
|
27
|
+
pending_triggers: number;
|
|
28
|
+
paths: {
|
|
29
|
+
context_dir: string;
|
|
30
|
+
reports_dir: string;
|
|
31
|
+
session_file: string | null;
|
|
32
|
+
};
|
|
33
|
+
follow_up: string;
|
|
34
|
+
}
|
|
35
|
+
export declare class RemoteCanvasSource implements CanvasSource {
|
|
36
|
+
private readonly previewEndpoint;
|
|
37
|
+
private readonly relayToken;
|
|
38
|
+
private readonly exec;
|
|
39
|
+
private nodeCache;
|
|
40
|
+
private rosterCache;
|
|
41
|
+
private viewCache;
|
|
42
|
+
constructor(previewEndpoint: string, relayToken: string);
|
|
43
|
+
/** Drops every cached entry — called on a `RemoteEventStream` `'nodes'` (or
|
|
44
|
+
* `'inbox'`) event so the next read re-fetches fresh state. */
|
|
45
|
+
invalidate(): void;
|
|
46
|
+
private execJson;
|
|
47
|
+
private fetchShow;
|
|
48
|
+
/** The full passthrough object `node inspect show` produces — used by
|
|
49
|
+
* `nodeShow`'s remote branch (`src/commands/node.ts`) to return the exact
|
|
50
|
+
* same shape a local read would, without re-deriving it. */
|
|
51
|
+
rawShow(id: string): Promise<RemoteShowResponse | null>;
|
|
52
|
+
getNode(id: string): Promise<NodeMeta | null>;
|
|
53
|
+
getRow(id: string): Promise<NodeRow | null>;
|
|
54
|
+
listNodes(filter?: {
|
|
55
|
+
status?: NodeStatus | NodeStatus[];
|
|
56
|
+
}): Promise<NodeRow[]>;
|
|
57
|
+
subscriptionsOf(id: string): Promise<SubscriptionRef[]>;
|
|
58
|
+
subscribersOf(id: string): Promise<SubscriptionRef[]>;
|
|
59
|
+
view(root: string): Promise<string[]>;
|
|
60
|
+
askCountsForView(root: string): Promise<Record<string, number>>;
|
|
61
|
+
hasActiveLiveSubscription(id: string): Promise<boolean>;
|
|
62
|
+
}
|