@crouton-kit/crouter 0.3.60 → 0.3.62
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/builtin-views/canvas/core.mjs +2 -1
- package/dist/clients/attach/__tests__/context-message.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/context-message.test.js +46 -0
- package/dist/clients/attach/__tests__/transport-relay.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/transport-relay.test.js +176 -0
- package/dist/clients/attach/attach-cmd.js +747 -743
- package/dist/clients/attach/canvas-panels.d.ts +4 -4
- package/dist/clients/attach/canvas-panels.js +108 -37
- package/dist/clients/attach/chat-view.js +11 -0
- package/dist/clients/attach/context-message.d.ts +3 -1
- package/dist/clients/attach/context-message.js +30 -21
- package/dist/clients/attach/graph-overlay.d.ts +8 -1
- package/dist/clients/attach/graph-overlay.js +201 -22
- package/dist/clients/attach/transport-relay.d.ts +51 -0
- package/dist/clients/attach/transport-relay.js +196 -0
- package/dist/clients/attach/transport-socket.d.ts +29 -0
- package/dist/clients/attach/transport-socket.js +164 -0
- package/dist/clients/attach/transport.d.ts +32 -0
- package/dist/clients/attach/transport.js +26 -0
- package/dist/clients/attach/view-socket.d.ts +40 -44
- package/dist/clients/attach/view-socket.js +94 -197
- package/dist/clients/web/server.js +1 -0
- package/dist/commands/memory/delete.js +3 -3
- package/dist/commands/memory/origin.js +1 -1
- package/dist/commands/memory/read.js +3 -3
- package/dist/commands/memory/shared.d.ts +8 -6
- package/dist/commands/memory/shared.js +23 -9
- package/dist/commands/memory/write.js +3 -3
- package/dist/commands/node-worktree.js +9 -9
- package/dist/commands/node.js +71 -23
- package/dist/commands/sys/doctor.js +1 -0
- package/dist/core/__tests__/broker-fork-seam.test.js +17 -2
- package/dist/core/__tests__/broker-snapshot-history.test.js +12 -2
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +79 -0
- package/dist/core/__tests__/context-intro.test.js +11 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +9 -4
- package/dist/core/__tests__/fixtures/fake-engine.js +12 -4
- package/dist/core/__tests__/kickoff.test.js +21 -0
- package/dist/core/__tests__/spawn-root.test.js +60 -0
- package/dist/core/__tests__/worktree.test.js +24 -1
- package/dist/core/canvas/browse/app.d.ts +2 -0
- package/dist/core/canvas/browse/app.js +83 -48
- package/dist/core/canvas/db.js +3 -2
- package/dist/core/canvas/index.d.ts +1 -0
- package/dist/core/canvas/index.js +1 -0
- package/dist/core/canvas/nav-model.d.ts +5 -3
- package/dist/core/canvas/nav-model.js +7 -6
- package/dist/core/canvas/paths.d.ts +8 -0
- package/dist/core/canvas/paths.js +10 -0
- package/dist/core/canvas/render.d.ts +6 -0
- package/dist/core/canvas/render.js +117 -20
- package/dist/core/canvas/source.d.ts +26 -0
- package/dist/core/canvas/source.js +29 -0
- package/dist/core/canvas/status-glyph.js +1 -0
- package/dist/core/canvas/types.d.ts +4 -3
- package/dist/core/config.d.ts +18 -2
- package/dist/core/config.js +88 -10
- package/dist/core/fault-classifier.d.ts +2 -2
- package/dist/core/memory-resolver.d.ts +9 -6
- package/dist/core/memory-resolver.js +28 -4
- package/dist/core/profiles/__tests__/root-profile.test.d.ts +1 -0
- package/dist/core/profiles/__tests__/root-profile.test.js +69 -0
- package/dist/core/profiles/manifest.d.ts +2 -0
- package/dist/core/profiles/manifest.js +40 -7
- package/dist/core/profiles/select.d.ts +4 -4
- package/dist/core/profiles/select.js +13 -13
- package/dist/core/runtime/__tests__/spawn-env.test.d.ts +1 -0
- package/dist/core/runtime/__tests__/spawn-env.test.js +268 -0
- package/dist/core/runtime/bearings.d.ts +5 -4
- package/dist/core/runtime/bearings.js +12 -6
- package/dist/core/runtime/broker.js +59 -7
- package/dist/core/runtime/fault-recovery.js +4 -0
- package/dist/core/runtime/front-door.js +1 -1
- package/dist/core/runtime/host.js +43 -3
- package/dist/core/runtime/kickoff.js +6 -0
- package/dist/core/runtime/launch.d.ts +6 -1
- package/dist/core/runtime/launch.js +7 -1
- package/dist/core/runtime/nodes.d.ts +7 -6
- package/dist/core/runtime/nodes.js +3 -3
- package/dist/core/runtime/situational-context.d.ts +12 -0
- package/dist/core/runtime/situational-context.js +48 -0
- package/dist/core/runtime/spawn-env.d.ts +27 -0
- package/dist/core/runtime/spawn-env.js +169 -0
- package/dist/core/runtime/spawn.d.ts +16 -7
- package/dist/core/runtime/spawn.js +8 -2
- package/dist/core/substrate/session-cache.js +7 -1
- package/dist/core/substrate/subject.d.ts +1 -1
- package/dist/core/view/remote-canvas-target.d.ts +3 -0
- package/dist/core/view/remote-canvas-target.js +25 -0
- package/dist/core/worktree.d.ts +4 -4
- package/dist/core/worktree.js +41 -19
- package/dist/daemon/crtrd.js +32 -0
- package/dist/daemon/predicate-eval.js +12 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -0
- package/dist/pi-extensions/canvas-bash-valve.d.ts +2 -0
- package/dist/pi-extensions/canvas-bash-valve.js +238 -0
- package/dist/pi-extensions/canvas-inbox-watcher.d.ts +12 -0
- package/dist/pi-extensions/canvas-inbox-watcher.js +41 -5
- package/dist/types.d.ts +32 -0
- package/dist/types.js +7 -0
- package/dist/web-client/apple-touch-icon.png +0 -0
- package/dist/web-client/assets/index-BnmSLNLa.css +2 -0
- package/dist/web-client/assets/index-BvzxXXGU.js +77 -0
- package/dist/web-client/assets/workbox-window.prod.es5-Bd17z0YL.js +2 -0
- package/dist/web-client/icon-192.png +0 -0
- package/dist/web-client/icon-512.png +0 -0
- package/dist/web-client/icon-maskable-512.png +0 -0
- package/dist/web-client/index.html +6 -3
- package/dist/web-client/manifest.webmanifest +1 -0
- package/dist/web-client/sw.js +1 -0
- package/docs/public-api.md +5 -3
- package/package.json +4 -3
- package/dist/web-client/assets/fraunces-latin-ext-wght-normal-Ca2vKHc0.woff2 +0 -0
- package/dist/web-client/assets/fraunces-latin-wght-normal-ukD16Tqj.woff2 +0 -0
- package/dist/web-client/assets/fraunces-vietnamese-wght-normal-CnvboYUG.woff2 +0 -0
- package/dist/web-client/assets/index-72YQdji_.js +0 -77
- package/dist/web-client/assets/index-bDhImjJy.css +0 -2
- package/dist/web-client/assets/instrument-sans-latin-ext-wght-normal-B5bTHO_g.woff2 +0 -0
- package/dist/web-client/assets/instrument-sans-latin-wght-normal-BbzFLZTg.woff2 +0 -0
|
@@ -12,6 +12,7 @@ import { nodeDir } from '../canvas/paths.js';
|
|
|
12
12
|
import { spawnNode } from '../runtime/nodes.js';
|
|
13
13
|
import { spawnChild, resolveSpawner } from '../runtime/spawn.js';
|
|
14
14
|
import { headlessBrokerHost } from '../runtime/host.js';
|
|
15
|
+
import { ROOT_PROFILE_ID } from '../profiles/manifest.js';
|
|
15
16
|
let home;
|
|
16
17
|
function spawner(id) {
|
|
17
18
|
return {
|
|
@@ -104,3 +105,62 @@ test('spawnChild crashes the node when launch/preflight throws', async () => {
|
|
|
104
105
|
rmSync(home, { recursive: true, force: true });
|
|
105
106
|
}
|
|
106
107
|
});
|
|
108
|
+
test('spawnChild: a root from a shell gets the stable root profile id', async () => {
|
|
109
|
+
const previousHome = process.env.HOME;
|
|
110
|
+
const previousNodeId = process.env.CRTR_NODE_ID;
|
|
111
|
+
const previousTmux = process.env.TMUX;
|
|
112
|
+
const previousTmuxPane = process.env.TMUX_PANE;
|
|
113
|
+
const env = process.env;
|
|
114
|
+
const localHome = mkdtempSync(join(tmpdir(), 'crtr-spawn-root-home-'));
|
|
115
|
+
const localCwd = mkdtempSync(join(tmpdir(), 'crtr-spawn-root-cwd-'));
|
|
116
|
+
const originalLaunch = headlessBrokerHost.launch;
|
|
117
|
+
const previousStdinTty = Object.getOwnPropertyDescriptor(process.stdin, 'isTTY');
|
|
118
|
+
const previousStdoutTty = Object.getOwnPropertyDescriptor(process.stdout, 'isTTY');
|
|
119
|
+
try {
|
|
120
|
+
process.env.HOME = localHome;
|
|
121
|
+
Object.defineProperty(process.stdin, 'isTTY', { value: false, configurable: true, writable: true });
|
|
122
|
+
Object.defineProperty(process.stdout, 'isTTY', { value: false, configurable: true, writable: true });
|
|
123
|
+
Reflect.deleteProperty(env, 'CRTR_NODE_ID');
|
|
124
|
+
delete process.env.TMUX;
|
|
125
|
+
delete process.env.TMUX_PANE;
|
|
126
|
+
headlessBrokerHost.launch = (_nodeId, _inv, _opts) => ({
|
|
127
|
+
pid: 4242,
|
|
128
|
+
exited: Promise.resolve(),
|
|
129
|
+
});
|
|
130
|
+
const result = await spawnChild({ kind: 'developer', cwd: localCwd, root: true, prompt: 'task' });
|
|
131
|
+
const node = getNode(result.node.node_id);
|
|
132
|
+
assert.equal(node.profile_id, ROOT_PROFILE_ID, 'the root spawn persists the stable root profile id');
|
|
133
|
+
assert.equal(node.parent, null, 'a shell root has no spine parent');
|
|
134
|
+
assert.equal(node.lifecycle, 'resident', 'a shell root stays resident');
|
|
135
|
+
assert.equal(result.node.profile_id, ROOT_PROFILE_ID, 'the returned node view carries the stable root profile id');
|
|
136
|
+
}
|
|
137
|
+
finally {
|
|
138
|
+
headlessBrokerHost.launch = originalLaunch;
|
|
139
|
+
if (previousHome === undefined)
|
|
140
|
+
delete process.env.HOME;
|
|
141
|
+
else
|
|
142
|
+
process.env.HOME = previousHome;
|
|
143
|
+
if (previousNodeId === undefined)
|
|
144
|
+
Reflect.deleteProperty(env, 'CRTR_NODE_ID');
|
|
145
|
+
else
|
|
146
|
+
env.CRTR_NODE_ID = previousNodeId;
|
|
147
|
+
if (previousTmux === undefined)
|
|
148
|
+
delete process.env.TMUX;
|
|
149
|
+
else
|
|
150
|
+
process.env.TMUX = previousTmux;
|
|
151
|
+
if (previousTmuxPane === undefined)
|
|
152
|
+
delete process.env.TMUX_PANE;
|
|
153
|
+
else
|
|
154
|
+
process.env.TMUX_PANE = previousTmuxPane;
|
|
155
|
+
if (previousStdinTty === undefined)
|
|
156
|
+
Reflect.deleteProperty(process.stdin, 'isTTY');
|
|
157
|
+
else
|
|
158
|
+
Object.defineProperty(process.stdin, 'isTTY', previousStdinTty);
|
|
159
|
+
if (previousStdoutTty === undefined)
|
|
160
|
+
Reflect.deleteProperty(process.stdout, 'isTTY');
|
|
161
|
+
else
|
|
162
|
+
Object.defineProperty(process.stdout, 'isTTY', previousStdoutTty);
|
|
163
|
+
rmSync(localHome, { recursive: true, force: true });
|
|
164
|
+
rmSync(localCwd, { recursive: true, force: true });
|
|
165
|
+
}
|
|
166
|
+
});
|
|
@@ -32,7 +32,8 @@ function node(id, over = {}) {
|
|
|
32
32
|
before(() => {
|
|
33
33
|
home = mkdtempSync(join(tmpdir(), 'crtr-worktree-home-'));
|
|
34
34
|
process.env['CRTR_HOME'] = home;
|
|
35
|
-
// Bare "remote" the
|
|
35
|
+
// Bare "remote" wired up as origin so the test can prove landing NEVER pushes
|
|
36
|
+
// to it (managed worktrees are based on and land onto local `main` only).
|
|
36
37
|
remoteDir = mkdtempSync(join(tmpdir(), 'crtr-worktree-remote-'));
|
|
37
38
|
run(['init', '--bare'], remoteDir);
|
|
38
39
|
sourceDir = mkdtempSync(join(tmpdir(), 'crtr-worktree-source-'));
|
|
@@ -83,3 +84,25 @@ test('closeManagedWorktree recreates the worktree path as a tombstone dir so the
|
|
|
83
84
|
const meta = getNode(nodeId);
|
|
84
85
|
assert.equal(meta?.managed_worktree?.state, 'closed');
|
|
85
86
|
});
|
|
87
|
+
// Landing is purely local: a commit made in the managed worktree must advance
|
|
88
|
+
// the source repo's LOCAL main to that commit, and origin/main must stay put
|
|
89
|
+
// (close never pushes to origin).
|
|
90
|
+
test('closeManagedWorktree fast-forwards local main to the worktree tip without touching origin', () => {
|
|
91
|
+
const nodeId = 'wt-close-2';
|
|
92
|
+
const originMainBefore = gitSync(['rev-parse', 'origin/main'], sourceDir).stdout.trim();
|
|
93
|
+
const wt = createManagedWorktree(sourceDir, nodeId);
|
|
94
|
+
createNode(node(nodeId, { cwd: wt.path, managed_worktree: wt }));
|
|
95
|
+
// Commit real work inside the managed worktree.
|
|
96
|
+
writeFileSync(join(wt.path, 'feature.txt'), 'work\n');
|
|
97
|
+
run(['add', '.'], wt.path);
|
|
98
|
+
run(['-c', 'user.email=test@example.com', '-c', 'user.name=Test', 'commit', '-m', 'feature'], wt.path);
|
|
99
|
+
const workSha = gitSync(['rev-parse', 'HEAD'], wt.path).stdout.trim();
|
|
100
|
+
const result = closeManagedWorktree(nodeId);
|
|
101
|
+
assert.equal(result.landed_sha, workSha, 'landed_sha should be the committed work');
|
|
102
|
+
// Local main in the source (primary) checkout advanced to the work.
|
|
103
|
+
const localMainAfter = gitSync(['rev-parse', 'main'], sourceDir).stdout.trim();
|
|
104
|
+
assert.equal(localMainAfter, workSha, 'local main must fast-forward to the worktree tip');
|
|
105
|
+
// origin/main is untouched — nothing was pushed.
|
|
106
|
+
const originMainAfter = gitSync(['rev-parse', 'origin/main'], sourceDir).stdout.trim();
|
|
107
|
+
assert.equal(originMainAfter, originMainBefore, 'origin/main must not move — landing is local-only');
|
|
108
|
+
});
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
// `pi --session` directly (see reviveNode() in revive.ts for the desync hazard).
|
|
10
10
|
import { execFileSync } from 'node:child_process';
|
|
11
11
|
import { resolve } from 'node:path';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
12
|
+
import { dashboardRowsAllFromSource, enrichRowsFromSource, loadPreviewFromSource, renderForestFromSource } from '../render.js';
|
|
13
|
+
import { localCanvasSource } from '../source.js';
|
|
14
14
|
import { closeNode } from '../../runtime/close.js';
|
|
15
15
|
import { setupTerminal, restoreTerminal, getTerminalSize, parseKeypress, } from '../../tui/terminal.js';
|
|
16
16
|
import { buildTree, flatten, pruneNode, TABS } from './model.js';
|
|
@@ -27,19 +27,25 @@ function viewportHeight(rowsTotal, search, previewOn) {
|
|
|
27
27
|
return Math.max(1, rows - headerHeight(search) - 1 /* footer */ - previewH);
|
|
28
28
|
}
|
|
29
29
|
export async function runBrowse(opts = {}) {
|
|
30
|
+
const source = opts.source ?? localCanvasSource;
|
|
30
31
|
// No TTY → print the static forest and exit 0 (no raw mode).
|
|
31
32
|
if (!process.stdin.isTTY) {
|
|
32
|
-
process.stdout.write(
|
|
33
|
+
process.stdout.write(await renderForestFromSource(source) + '\n');
|
|
33
34
|
return;
|
|
34
35
|
}
|
|
35
36
|
// Snapshot the canvas. Drop kind:'human' control-plane decks — they have no pi
|
|
36
37
|
// session, so `node focus` refuses them; they are never a navigation/resume
|
|
37
38
|
// target (mirrors the node focus guard).
|
|
38
|
-
const rows =
|
|
39
|
-
const
|
|
39
|
+
const rows = (await dashboardRowsAllFromSource(source)).filter((r) => r.kind !== 'human');
|
|
40
|
+
const nodeRows = await source.listNodes();
|
|
41
|
+
const rootIds = nodeRows
|
|
40
42
|
.filter((n) => n.parent === null && n.kind !== 'human')
|
|
41
43
|
.map((n) => n.node_id);
|
|
42
|
-
const
|
|
44
|
+
const childIds = new Map();
|
|
45
|
+
await Promise.all(nodeRows.map(async (n) => {
|
|
46
|
+
childIds.set(n.node_id, (await source.subscriptionsOf(n.node_id).catch(() => [])).map((s) => s.node_id));
|
|
47
|
+
}));
|
|
48
|
+
const tree = buildTree(rows, rootIds, (id) => childIds.get(id) ?? []);
|
|
43
49
|
const totalNodes = tree.nodes.size;
|
|
44
50
|
// Default cwd scope = the dir browse was launched from (the request). The popup
|
|
45
51
|
// / command passes --cwd; resolve it so it compares cleanly against stored cwds.
|
|
@@ -80,6 +86,9 @@ export async function runBrowse(opts = {}) {
|
|
|
80
86
|
// Safety net: an uncaught throw in the (un-unit-tested) keystroke path must
|
|
81
87
|
// never strand the tty in raw + alt-screen + hidden-cursor.
|
|
82
88
|
process.once('exit', cleanup);
|
|
89
|
+
const logAsyncFailure = (label, err) => {
|
|
90
|
+
process.stderr.write(`[crtr browse] ${label}: ${String(err?.message ?? err)}\n`);
|
|
91
|
+
};
|
|
83
92
|
/** On-demand kick of a hanging node: shell `node lifecycle revive <id> --now`
|
|
84
93
|
* (which SIGTERMs the live broker — the daemon resumes it). Synchronous +
|
|
85
94
|
* quiet (stdio ignored) so it never takes over the TUI; best-effort. */
|
|
@@ -123,7 +132,7 @@ export async function runBrowse(opts = {}) {
|
|
|
123
132
|
if (state.cursor < 0)
|
|
124
133
|
state.cursor = 0;
|
|
125
134
|
};
|
|
126
|
-
const flush = () => {
|
|
135
|
+
const flush = async () => {
|
|
127
136
|
const size = getTerminalSize();
|
|
128
137
|
const previewOn = state.preview && visible.length > 0;
|
|
129
138
|
const viewport = viewportHeight(size.rows, state.search, previewOn);
|
|
@@ -145,12 +154,12 @@ export async function runBrowse(opts = {}) {
|
|
|
145
154
|
if (r !== undefined)
|
|
146
155
|
slice.push(r);
|
|
147
156
|
}
|
|
148
|
-
|
|
157
|
+
await enrichRowsFromSource(source, slice);
|
|
149
158
|
const cur = curRow();
|
|
150
159
|
if (cur !== undefined) {
|
|
151
160
|
const r = rowOf(cur.id);
|
|
152
161
|
if (r !== undefined)
|
|
153
|
-
|
|
162
|
+
await loadPreviewFromSource(source, r);
|
|
154
163
|
}
|
|
155
164
|
const frame = renderFrame({
|
|
156
165
|
tree, visible, tab: state.tab, cursor: state.cursor, scrollOffset: state.scrollOffset,
|
|
@@ -189,7 +198,7 @@ export async function runBrowse(opts = {}) {
|
|
|
189
198
|
// subtree, tear down each engine, and detach every attached viewer. Then reflect
|
|
190
199
|
// the new terminal statuses back into the live snapshot so the row updates in
|
|
191
200
|
// place (no re-snapshot). Best-effort: a teardown failure never wedges the picker.
|
|
192
|
-
const doClose = (id) => {
|
|
201
|
+
const doClose = async (id) => {
|
|
193
202
|
state.pendingClose = null;
|
|
194
203
|
let closed = [];
|
|
195
204
|
try {
|
|
@@ -199,7 +208,7 @@ export async function runBrowse(opts = {}) {
|
|
|
199
208
|
return; // unknown/already-gone node — nothing to reflect
|
|
200
209
|
}
|
|
201
210
|
for (const cid of closed) {
|
|
202
|
-
const m = getNode(cid);
|
|
211
|
+
const m = await source.getNode(cid);
|
|
203
212
|
if (m === null) {
|
|
204
213
|
// Reaped, not parked: an EMPTY node (0k ctx, no assistant message) is
|
|
205
214
|
// hard-deleted by closeNode (row + dir gone). Splice it out of the tree so
|
|
@@ -258,6 +267,30 @@ export async function runBrowse(opts = {}) {
|
|
|
258
267
|
// phase) has appended its chars, so N rapid keys collapse to ONE recompute+paint
|
|
259
268
|
// and no keystroke is ignored. The sort may still take a moment — that's fine.
|
|
260
269
|
let searchRepaintScheduled = false;
|
|
270
|
+
let flushInFlight = false;
|
|
271
|
+
let flushQueued = false;
|
|
272
|
+
const scheduleFlush = () => {
|
|
273
|
+
flushQueued = true;
|
|
274
|
+
if (flushInFlight)
|
|
275
|
+
return;
|
|
276
|
+
flushInFlight = true;
|
|
277
|
+
void (async () => {
|
|
278
|
+
try {
|
|
279
|
+
while (flushQueued && !restored) {
|
|
280
|
+
flushQueued = false;
|
|
281
|
+
await flush();
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
finally {
|
|
285
|
+
flushInFlight = false;
|
|
286
|
+
if (flushQueued && !restored)
|
|
287
|
+
scheduleFlush();
|
|
288
|
+
}
|
|
289
|
+
})().catch(() => {
|
|
290
|
+
cleanup();
|
|
291
|
+
process.exit(1);
|
|
292
|
+
});
|
|
293
|
+
};
|
|
261
294
|
const scheduleSearchRepaint = () => {
|
|
262
295
|
if (searchRepaintScheduled)
|
|
263
296
|
return;
|
|
@@ -265,7 +298,7 @@ export async function runBrowse(opts = {}) {
|
|
|
265
298
|
setImmediate(() => {
|
|
266
299
|
searchRepaintScheduled = false;
|
|
267
300
|
recompute();
|
|
268
|
-
|
|
301
|
+
scheduleFlush();
|
|
269
302
|
});
|
|
270
303
|
};
|
|
271
304
|
const onKeySearch = (input, key) => {
|
|
@@ -276,7 +309,7 @@ export async function runBrowse(opts = {}) {
|
|
|
276
309
|
state.query = '';
|
|
277
310
|
state.sort = 'tree';
|
|
278
311
|
recompute();
|
|
279
|
-
|
|
312
|
+
scheduleFlush();
|
|
280
313
|
return;
|
|
281
314
|
}
|
|
282
315
|
if (key.return) {
|
|
@@ -288,7 +321,7 @@ export async function runBrowse(opts = {}) {
|
|
|
288
321
|
const firstMatch = visible.findIndex((v) => v.matched);
|
|
289
322
|
if (firstMatch >= 0)
|
|
290
323
|
state.cursor = firstMatch;
|
|
291
|
-
|
|
324
|
+
scheduleFlush();
|
|
292
325
|
return;
|
|
293
326
|
}
|
|
294
327
|
if (key.backspace) {
|
|
@@ -325,13 +358,13 @@ export async function runBrowse(opts = {}) {
|
|
|
325
358
|
// other key so the confirm is modal.
|
|
326
359
|
if (state.pendingClose !== null) {
|
|
327
360
|
if (input === 'y' || input === 'Y') {
|
|
328
|
-
|
|
329
|
-
|
|
361
|
+
const target = state.pendingClose;
|
|
362
|
+
void doClose(target).then(() => scheduleFlush()).catch(() => scheduleFlush());
|
|
330
363
|
return;
|
|
331
364
|
}
|
|
332
365
|
// n / Esc / anything else cancels the close.
|
|
333
366
|
state.pendingClose = null;
|
|
334
|
-
|
|
367
|
+
scheduleFlush();
|
|
335
368
|
return;
|
|
336
369
|
}
|
|
337
370
|
const row = curRow();
|
|
@@ -341,22 +374,22 @@ export async function runBrowse(opts = {}) {
|
|
|
341
374
|
// Move.
|
|
342
375
|
if (key.upArrow || input === 'k') {
|
|
343
376
|
state.cursor = Math.max(0, state.cursor - 1);
|
|
344
|
-
|
|
377
|
+
scheduleFlush();
|
|
345
378
|
return;
|
|
346
379
|
}
|
|
347
380
|
if (key.downArrow || input === 'j') {
|
|
348
381
|
state.cursor = Math.max(0, Math.min(visible.length - 1, state.cursor + 1));
|
|
349
|
-
|
|
382
|
+
scheduleFlush();
|
|
350
383
|
return;
|
|
351
384
|
}
|
|
352
385
|
if (input === 'g') {
|
|
353
386
|
state.cursor = 0;
|
|
354
|
-
|
|
387
|
+
scheduleFlush();
|
|
355
388
|
return;
|
|
356
389
|
}
|
|
357
390
|
if (input === 'G') {
|
|
358
391
|
state.cursor = Math.max(0, visible.length - 1);
|
|
359
|
-
|
|
392
|
+
scheduleFlush();
|
|
360
393
|
return;
|
|
361
394
|
}
|
|
362
395
|
// Expand / descend.
|
|
@@ -370,7 +403,7 @@ export async function runBrowse(opts = {}) {
|
|
|
370
403
|
state.cursor += 1; // already expanded → step onto first child
|
|
371
404
|
}
|
|
372
405
|
}
|
|
373
|
-
|
|
406
|
+
scheduleFlush();
|
|
374
407
|
return;
|
|
375
408
|
}
|
|
376
409
|
// Collapse / ascend.
|
|
@@ -387,7 +420,7 @@ export async function runBrowse(opts = {}) {
|
|
|
387
420
|
state.cursor = idx;
|
|
388
421
|
}
|
|
389
422
|
}
|
|
390
|
-
|
|
423
|
+
scheduleFlush();
|
|
391
424
|
return;
|
|
392
425
|
}
|
|
393
426
|
// Toggle collapse.
|
|
@@ -399,18 +432,18 @@ export async function runBrowse(opts = {}) {
|
|
|
399
432
|
state.collapsed.delete(row.id);
|
|
400
433
|
recompute(row.id);
|
|
401
434
|
}
|
|
402
|
-
|
|
435
|
+
scheduleFlush();
|
|
403
436
|
return;
|
|
404
437
|
}
|
|
405
438
|
// Tabs.
|
|
406
439
|
if (key.tab || input === ']') {
|
|
407
440
|
cycleTab(1);
|
|
408
|
-
|
|
441
|
+
scheduleFlush();
|
|
409
442
|
return;
|
|
410
443
|
}
|
|
411
444
|
if (key.shiftTab || input === '[') {
|
|
412
445
|
cycleTab(-1);
|
|
413
|
-
|
|
446
|
+
scheduleFlush();
|
|
414
447
|
return;
|
|
415
448
|
}
|
|
416
449
|
if (input >= '1' && input <= '4') {
|
|
@@ -421,40 +454,43 @@ export async function runBrowse(opts = {}) {
|
|
|
421
454
|
state.scrollOffset = 0;
|
|
422
455
|
recompute();
|
|
423
456
|
}
|
|
424
|
-
|
|
457
|
+
scheduleFlush();
|
|
425
458
|
return;
|
|
426
459
|
}
|
|
427
460
|
// Close-out the selected node (+ its exclusive subtree). If anything in that
|
|
428
461
|
// subtree is actively streaming, confirm first (y/n); otherwise close at once.
|
|
429
462
|
if (input === 'x') {
|
|
430
|
-
if (row
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
463
|
+
if (row === undefined) {
|
|
464
|
+
scheduleFlush();
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (anyStreaming(row.id)) {
|
|
468
|
+
state.pendingClose = row.id;
|
|
469
|
+
scheduleFlush();
|
|
470
|
+
return;
|
|
435
471
|
}
|
|
436
|
-
|
|
472
|
+
void doClose(row.id).then(() => scheduleFlush()).catch(() => scheduleFlush());
|
|
437
473
|
return;
|
|
438
474
|
}
|
|
439
475
|
// Sort / scope / residents / preview.
|
|
440
476
|
if (input === 's') {
|
|
441
477
|
cycleSort();
|
|
442
|
-
|
|
478
|
+
scheduleFlush();
|
|
443
479
|
return;
|
|
444
480
|
}
|
|
445
481
|
if (input === 'c') {
|
|
446
482
|
toggleScope();
|
|
447
|
-
|
|
483
|
+
scheduleFlush();
|
|
448
484
|
return;
|
|
449
485
|
}
|
|
450
486
|
if (input === 'r') {
|
|
451
487
|
toggleResidents();
|
|
452
|
-
|
|
488
|
+
scheduleFlush();
|
|
453
489
|
return;
|
|
454
490
|
}
|
|
455
491
|
if (input === 'p') {
|
|
456
492
|
state.preview = !state.preview;
|
|
457
|
-
|
|
493
|
+
scheduleFlush();
|
|
458
494
|
return;
|
|
459
495
|
}
|
|
460
496
|
// Search. Starting a search ranks by relevance (decision) so the best prompt/
|
|
@@ -466,7 +502,7 @@ export async function runBrowse(opts = {}) {
|
|
|
466
502
|
state.cursor = 0;
|
|
467
503
|
state.scrollOffset = 0;
|
|
468
504
|
recompute();
|
|
469
|
-
|
|
505
|
+
scheduleFlush();
|
|
470
506
|
return;
|
|
471
507
|
}
|
|
472
508
|
// Revive now (on-demand kick): for a HANGING node, SIGTERM the live broker so
|
|
@@ -480,7 +516,7 @@ export async function runBrowse(opts = {}) {
|
|
|
480
516
|
if (r?.hanging != null)
|
|
481
517
|
reviveNow(row.id);
|
|
482
518
|
}
|
|
483
|
-
|
|
519
|
+
scheduleFlush();
|
|
484
520
|
return;
|
|
485
521
|
}
|
|
486
522
|
// Resume.
|
|
@@ -516,7 +552,7 @@ export async function runBrowse(opts = {}) {
|
|
|
516
552
|
recompute(selfId);
|
|
517
553
|
}
|
|
518
554
|
setupTerminal();
|
|
519
|
-
flush();
|
|
555
|
+
await flush();
|
|
520
556
|
// Background corpus warmer: after the instant first paint, progressively enrich
|
|
521
557
|
// + load-preview every row (in small chunks, off the event loop) so the prompt
|
|
522
558
|
// super-search corpus lights up shortly after the instant name/kind/id search.
|
|
@@ -525,24 +561,23 @@ export async function runBrowse(opts = {}) {
|
|
|
525
561
|
const warmRows = [...tree.nodes.values()].map((n) => n.row);
|
|
526
562
|
let warmIdx = 0;
|
|
527
563
|
const WARM_CHUNK = 30;
|
|
528
|
-
const warmChunk = () => {
|
|
564
|
+
const warmChunk = async () => {
|
|
529
565
|
if (restored)
|
|
530
566
|
return;
|
|
531
567
|
const end = Math.min(warmIdx + WARM_CHUNK, warmRows.length);
|
|
532
568
|
const slice = warmRows.slice(warmIdx, end);
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
loadPreview(r);
|
|
569
|
+
await enrichRowsFromSource(source, slice);
|
|
570
|
+
await Promise.all(slice.map((r) => loadPreviewFromSource(source, r)));
|
|
536
571
|
warmIdx = end;
|
|
537
572
|
// A live search may match the freshly-warmed prompts → recompute + repaint.
|
|
538
573
|
if (state.search && state.query !== '') {
|
|
539
574
|
recompute(curRow()?.id);
|
|
540
|
-
|
|
575
|
+
scheduleFlush();
|
|
541
576
|
}
|
|
542
577
|
if (warmIdx < warmRows.length)
|
|
543
|
-
setImmediate(warmChunk);
|
|
578
|
+
setImmediate(() => { void warmChunk().catch((err) => logAsyncFailure('background warm failed', err)); });
|
|
544
579
|
};
|
|
545
|
-
setImmediate(warmChunk);
|
|
580
|
+
setImmediate(() => { void warmChunk().catch((err) => logAsyncFailure('background warm failed', err)); });
|
|
546
581
|
await new Promise(() => {
|
|
547
582
|
const onData = (data) => {
|
|
548
583
|
try {
|
|
@@ -559,6 +594,6 @@ export async function runBrowse(opts = {}) {
|
|
|
559
594
|
}
|
|
560
595
|
};
|
|
561
596
|
process.stdin.on('data', onData);
|
|
562
|
-
process.stdout.on('resize',
|
|
597
|
+
process.stdout.on('resize', scheduleFlush);
|
|
563
598
|
});
|
|
564
599
|
}
|
package/dist/core/canvas/db.js
CHANGED
|
@@ -353,8 +353,9 @@ CREATE TABLE IF NOT EXISTS canvas_meta (
|
|
|
353
353
|
* (`<slug>-<id>`) a node runs under, chosen once at spawn (inherited from the
|
|
354
354
|
* spawner, or `--profile`). Durable IDENTITY mirrored from meta.json like
|
|
355
355
|
* `cwd`/`host_kind` (NOT runtime like `pane`), so it survives an index rebuild
|
|
356
|
-
* via the `IDENTITY_KEYS`/`upsertRow` path. Defaults NULL
|
|
357
|
-
* no
|
|
356
|
+
* via the `IDENTITY_KEYS`/`upsertRow` path. Defaults NULL for historical
|
|
357
|
+
* root/no-profile rows; new root conversations use the stable root profile
|
|
358
|
+
* id instead. No data backfill. Additive, forward-only. */
|
|
358
359
|
function addProfileIdColumn(db) {
|
|
359
360
|
const columns = nodeColumns(db);
|
|
360
361
|
addNodeColumnIfMissing(db, columns, 'profile_id', `ALTER TABLE nodes ADD COLUMN profile_id TEXT;`);
|
|
@@ -91,14 +91,16 @@ export declare function isHumanAsk(id: string): boolean;
|
|
|
91
91
|
export declare function convoChildIds(id: string): string[];
|
|
92
92
|
/** Live reports (active|idle) of a node — the DOWN set in BASE. */
|
|
93
93
|
export declare function liveReports(id: string): string[];
|
|
94
|
-
/** Direct navigable children — used for the ⤳ badge
|
|
95
|
-
*
|
|
94
|
+
/** Direct navigable children — used for the ⤳ badge (human-ask nodes excluded,
|
|
95
|
+
* so the count matches what the tree actually shows). */
|
|
96
96
|
export declare function childCount(id: string): number;
|
|
97
97
|
/** Climb first-manager edges from `self` to the ancestry root (cycle-guarded). */
|
|
98
98
|
export declare function climbRoot(self: string): string;
|
|
99
99
|
/** Space-joined ids of a node's subtree (cursor-relative {subtree} var). */
|
|
100
100
|
export declare function subtreeIds(root: string): string[];
|
|
101
|
-
/** ⤳M direct-children badge —
|
|
101
|
+
/** ⤳M direct-children badge — the single child-count signal, shown on any row
|
|
102
|
+
* with navigable children (folded or not). In GRAPH the ▸ caret separately marks
|
|
103
|
+
* a collapsed row, so this stays a pure count and never doubles as a fold badge. */
|
|
102
104
|
export declare function childBadge(node: NodeMeta | null): string;
|
|
103
105
|
/** ⚑K pending-asks badge for a node, read from the supplied per-node ask map. */
|
|
104
106
|
export declare function askBadge(id: string, asks: Record<string, number>): string;
|
|
@@ -322,8 +322,8 @@ export function liveReports(id) {
|
|
|
322
322
|
return st === 'active' || st === 'idle';
|
|
323
323
|
});
|
|
324
324
|
}
|
|
325
|
-
/** Direct navigable children — used for the ⤳ badge
|
|
326
|
-
*
|
|
325
|
+
/** Direct navigable children — used for the ⤳ badge (human-ask nodes excluded,
|
|
326
|
+
* so the count matches what the tree actually shows). */
|
|
327
327
|
export function childCount(id) {
|
|
328
328
|
return convoChildIds(id).length;
|
|
329
329
|
}
|
|
@@ -360,9 +360,11 @@ export function subtreeIds(root) {
|
|
|
360
360
|
// ---------------------------------------------------------------------------
|
|
361
361
|
// Shared cell builders
|
|
362
362
|
// ---------------------------------------------------------------------------
|
|
363
|
-
/** ⤳M direct-children badge —
|
|
363
|
+
/** ⤳M direct-children badge — the single child-count signal, shown on any row
|
|
364
|
+
* with navigable children (folded or not). In GRAPH the ▸ caret separately marks
|
|
365
|
+
* a collapsed row, so this stays a pure count and never doubles as a fold badge. */
|
|
364
366
|
export function childBadge(node) {
|
|
365
|
-
if (node === null
|
|
367
|
+
if (node === null)
|
|
366
368
|
return '';
|
|
367
369
|
const m = childCount(node.node_id);
|
|
368
370
|
return m > 0 ? ` ${DIM}⤳${m}${RESET}` : '';
|
|
@@ -484,10 +486,9 @@ export function renderGraphRow(r, isCursor, focused, activeBelow, asks) {
|
|
|
484
486
|
// reads purely as "this unfolds".
|
|
485
487
|
const expandable = r.hasKids && r.collapsed;
|
|
486
488
|
const caret = !isCursor && expandable ? `${DIM}▸${RESET} ` : ' ';
|
|
487
|
-
const fold = expandable ? ` ${DIM}[+${childCount(r.id)}]${RESET}` : '';
|
|
488
489
|
const fault = node !== null ? activeFaultForDisplay(node, readFault(r.id)) : null;
|
|
489
490
|
const faultSuffix = fault !== null ? ` · ${faultSummary(fault)}` : '';
|
|
490
|
-
const line = `${r.branch}${caret}${dot} ${name} ${kind} ${tokens}${cycleBadge(node)}${childBadge(node)}${liveBelowBadge(node, activeBelow)}${
|
|
491
|
+
const line = `${r.branch}${caret}${dot} ${name} ${kind} ${tokens}${cycleBadge(node)}${childBadge(node)}${liveBelowBadge(node, activeBelow)}${askBadge(r.id, asks)}${activityCell(r.id, node)}${faultSuffix}`;
|
|
491
492
|
return wrap(line, isAttached(r.id, node, focused));
|
|
492
493
|
}
|
|
493
494
|
/** Total lines the GRAPH widget may emit. pi hard-caps extension widgets at
|
|
@@ -45,6 +45,14 @@ export declare function reportsDir(nodeId: string): string;
|
|
|
45
45
|
export declare function messagesDir(nodeId: string): string;
|
|
46
46
|
export declare function nodeMetaPath(nodeId: string): string;
|
|
47
47
|
export declare function inboxPath(nodeId: string): string;
|
|
48
|
+
/** The node-owned ambient "current situation" sidecar — the latest hidden
|
|
49
|
+
* `<situational-context>` text injected into bearings/kickoff (see
|
|
50
|
+
* `runtime/situational-context.ts`), upserted in place rather than appended so
|
|
51
|
+
* only the newest applet/situation state is ever carried. Lives beside
|
|
52
|
+
* meta.json/inbox.jsonl (node-runtime state) rather than under context/ (the
|
|
53
|
+
* user-authored artifact store), because it is ambient machine state, not a
|
|
54
|
+
* document a human wrote. */
|
|
55
|
+
export declare function situationalContextPath(nodeId: string): string;
|
|
48
56
|
/** Passive-subscription accumulator. Pushes from publishers this node subscribes
|
|
49
57
|
* to PASSIVELY land here instead of inbox.jsonl — the inbox-watcher never polls
|
|
50
58
|
* it, so they never wake the node. Drained as XML pre-text on the next message. */
|
|
@@ -103,6 +103,16 @@ export function nodeMetaPath(nodeId) {
|
|
|
103
103
|
export function inboxPath(nodeId) {
|
|
104
104
|
return join(nodeDir(nodeId), 'inbox.jsonl');
|
|
105
105
|
}
|
|
106
|
+
/** The node-owned ambient "current situation" sidecar — the latest hidden
|
|
107
|
+
* `<situational-context>` text injected into bearings/kickoff (see
|
|
108
|
+
* `runtime/situational-context.ts`), upserted in place rather than appended so
|
|
109
|
+
* only the newest applet/situation state is ever carried. Lives beside
|
|
110
|
+
* meta.json/inbox.jsonl (node-runtime state) rather than under context/ (the
|
|
111
|
+
* user-authored artifact store), because it is ambient machine state, not a
|
|
112
|
+
* document a human wrote. */
|
|
113
|
+
export function situationalContextPath(nodeId) {
|
|
114
|
+
return join(nodeDir(nodeId), 'situational-context.md');
|
|
115
|
+
}
|
|
106
116
|
/** Passive-subscription accumulator. Pushes from publishers this node subscribes
|
|
107
117
|
* to PASSIVELY land here instead of inbox.jsonl — the inbox-watcher never polls
|
|
108
118
|
* it, so they never wake the node. Drained as XML pre-text on the next message. */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CanvasSource } from './source.js';
|
|
1
2
|
import { type Fault } from '../runtime/fault.js';
|
|
2
3
|
import type { NodeStatus, Lifecycle } from './types.js';
|
|
3
4
|
export declare function activeFaultForDisplay<T extends {
|
|
@@ -20,6 +21,8 @@ export declare function renderTree(rootId: string): string;
|
|
|
20
21
|
* If there are no roots on the canvas, returns a placeholder string.
|
|
21
22
|
*/
|
|
22
23
|
export declare function renderForest(): string;
|
|
24
|
+
export declare function renderTreeFromSource(source: CanvasSource, rootId: string): Promise<string>;
|
|
25
|
+
export declare function renderForestFromSource(source: CanvasSource): Promise<string>;
|
|
23
26
|
export interface DashboardRow {
|
|
24
27
|
node_id: string;
|
|
25
28
|
name: string;
|
|
@@ -116,6 +119,7 @@ export declare function dashboardRows(rootId: string): DashboardRow[];
|
|
|
116
119
|
* viewport on demand. See {@link enrichRow}, {@link enrichRows}, {@link loadPreview}.
|
|
117
120
|
*/
|
|
118
121
|
export declare function dashboardRowsAll(): DashboardRow[];
|
|
122
|
+
export declare function dashboardRowsAllFromSource(source: CanvasSource): Promise<DashboardRow[]>;
|
|
119
123
|
/** Fold the cheap-boot row's deferred fields in for ONE row: the full label
|
|
120
124
|
* (meta.description → fullName), ctx tokens (telemetry.json), and ⚑ asks. One
|
|
121
125
|
* meta read + one telemetry read + one ask scan. Idempotent (no-op once enriched).
|
|
@@ -125,9 +129,11 @@ export declare function enrichRow(row: DashboardRow): DashboardRow;
|
|
|
125
129
|
* distinct cwd's ask inbox exactly ONCE (via `asksForNodes`) instead of per row.
|
|
126
130
|
* Use this for a whole viewport / the full forest; mutates each row in place. */
|
|
127
131
|
export declare function enrichRows(rows: DashboardRow[]): void;
|
|
132
|
+
export declare function enrichRowsFromSource(source: CanvasSource, rows: DashboardRow[]): Promise<void>;
|
|
128
133
|
/** Load the SELECTED row's preview text: the spawn `goal` (initial-prompt.md) plus
|
|
129
134
|
* the whole-conversation `prompts` and the `lastAssistant` reply — the latter two
|
|
130
135
|
* folded into ONE session-file read (see {@link readSessionParts}). Mutates the row
|
|
131
136
|
* in place; idempotent. Call only for the cursor row (and lazily/in-background to
|
|
132
137
|
* warm the prompt super-search corpus), never on the boot path. */
|
|
133
138
|
export declare function loadPreview(row: DashboardRow): DashboardRow;
|
|
139
|
+
export declare function loadPreviewFromSource(source: CanvasSource, row: DashboardRow): Promise<DashboardRow>;
|