@crouton-kit/crouter 0.3.15 → 0.3.17
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-personas/developer/orchestrator.md +1 -1
- package/dist/builtin-personas/orchestration-kernel.md +6 -6
- package/dist/builtin-personas/plan/base.md +1 -1
- package/dist/builtin-personas/plan/orchestrator.md +1 -1
- package/dist/builtin-personas/spec/base.md +1 -1
- package/dist/commands/canvas-browse.d.ts +2 -0
- package/dist/commands/canvas-browse.js +45 -0
- package/dist/commands/canvas-prune.js +11 -2
- package/dist/commands/canvas.js +3 -2
- package/dist/commands/chord.js +1 -1
- package/dist/commands/human/shared.js +1 -1
- package/dist/commands/node.js +14 -2
- package/dist/commands/skill/author.js +2 -2
- package/dist/commands/tmux-spread.js +2 -3
- package/dist/core/__tests__/cascade-close.test.js +199 -0
- package/dist/core/__tests__/close.test.js +2 -2
- package/dist/core/__tests__/daemon-boot.test.js +7 -0
- package/dist/core/__tests__/daemon-liveness.test.js +59 -4
- package/dist/core/__tests__/dead-pane-regression.test.js +151 -0
- package/dist/core/__tests__/fixtures/fake-pi-host.d.ts +2 -0
- package/dist/core/__tests__/fixtures/fake-pi-host.js +301 -0
- package/dist/core/__tests__/flagship-lifecycle.test.js +273 -0
- package/dist/core/__tests__/focuses.test.js +5 -68
- package/dist/core/__tests__/grace-clock.test.d.ts +1 -0
- package/dist/core/__tests__/grace-clock.test.js +115 -0
- package/dist/core/__tests__/helpers/harness.d.ts +78 -0
- package/dist/core/__tests__/helpers/harness.js +406 -0
- package/dist/core/__tests__/home-session.test.js +1 -1
- package/dist/core/__tests__/lifecycle.test.js +6 -13
- package/dist/core/__tests__/live-mutation.test.d.ts +1 -0
- package/dist/core/__tests__/live-mutation.test.js +341 -0
- package/dist/core/__tests__/placement-focus.test.js +106 -46
- package/dist/core/__tests__/placement-teardown.test.js +4 -9
- package/dist/core/__tests__/relaunch.test.js +22 -16
- package/dist/core/__tests__/reset.test.js +11 -6
- package/dist/core/__tests__/spike-harness.test.d.ts +1 -0
- package/dist/core/__tests__/spike-harness.test.js +241 -0
- package/dist/core/__tests__/subscription-delivery.test.d.ts +1 -0
- package/dist/core/__tests__/subscription-delivery.test.js +233 -0
- package/dist/core/__tests__/tmux-surface.test.js +8 -9
- package/dist/core/canvas/browse/__tests__/model.test.d.ts +1 -0
- package/dist/core/canvas/browse/__tests__/model.test.js +142 -0
- package/dist/core/canvas/browse/__tests__/render.test.d.ts +1 -0
- package/dist/core/canvas/browse/__tests__/render.test.js +102 -0
- package/dist/core/canvas/browse/app.d.ts +4 -0
- package/dist/core/canvas/browse/app.js +349 -0
- package/dist/core/canvas/browse/model.d.ts +97 -0
- package/dist/core/canvas/browse/model.js +258 -0
- package/dist/core/canvas/browse/render.d.ts +41 -0
- package/dist/core/canvas/browse/render.js +387 -0
- package/dist/core/canvas/browse/terminal.d.ts +23 -0
- package/dist/core/canvas/browse/terminal.js +100 -0
- package/dist/core/canvas/canvas.d.ts +9 -2
- package/dist/core/canvas/canvas.js +41 -3
- package/dist/core/canvas/db.js +2 -3
- package/dist/core/canvas/focuses.d.ts +2 -2
- package/dist/core/canvas/focuses.js +4 -3
- package/dist/core/canvas/render.d.ts +10 -0
- package/dist/core/canvas/render.js +25 -1
- package/dist/core/canvas/types.d.ts +1 -1
- package/dist/core/feed/inbox.d.ts +0 -3
- package/dist/core/feed/inbox.js +1 -5
- package/dist/core/runtime/busy.d.ts +8 -0
- package/dist/core/runtime/busy.js +46 -0
- package/dist/core/runtime/close.js +2 -2
- package/dist/core/runtime/demote.js +2 -7
- package/dist/core/runtime/launch.d.ts +3 -1
- package/dist/core/runtime/launch.js +4 -1
- package/dist/core/runtime/lifecycle.d.ts +1 -1
- package/dist/core/runtime/lifecycle.js +12 -4
- package/dist/core/runtime/naming.d.ts +3 -3
- package/dist/core/runtime/naming.js +6 -6
- package/dist/core/runtime/nodes.d.ts +7 -0
- package/dist/core/runtime/nodes.js +10 -1
- package/dist/core/runtime/placement.d.ts +39 -10
- package/dist/core/runtime/placement.js +100 -44
- package/dist/core/runtime/reset.d.ts +11 -8
- package/dist/core/runtime/reset.js +36 -31
- package/dist/core/runtime/revive.d.ts +1 -1
- package/dist/core/runtime/revive.js +2 -2
- package/dist/core/runtime/spawn.js +3 -3
- package/dist/core/runtime/tmux-chrome.d.ts +1 -0
- package/dist/core/runtime/tmux-chrome.js +4 -0
- package/dist/core/runtime/tmux.d.ts +13 -6
- package/dist/core/runtime/tmux.js +21 -12
- package/dist/daemon/crtrd.js +43 -21
- package/dist/pi-extensions/canvas-nav.js +40 -28
- package/dist/pi-extensions/canvas-resume.d.ts +21 -0
- package/dist/pi-extensions/canvas-resume.js +82 -0
- package/dist/pi-extensions/canvas-stophook.d.ts +1 -1
- package/dist/pi-extensions/canvas-stophook.js +21 -9
- package/dist/prompts/skill.js +6 -1
- package/package.json +2 -2
- package/dist/commands/__tests__/skill.test.js +0 -290
- package/dist/core/__tests__/pkg.test.js +0 -218
- package/dist/core/__tests__/sys.test.js +0 -208
- package/dist/core/runtime/presence.d.ts +0 -30
- package/dist/core/runtime/presence.js +0 -178
- /package/dist/{commands/__tests__/skill.test.d.ts → core/__tests__/cascade-close.test.d.ts} +0 -0
- /package/dist/core/__tests__/{pkg.test.d.ts → dead-pane-regression.test.d.ts} +0 -0
- /package/dist/core/__tests__/{sys.test.d.ts → flagship-lifecycle.test.d.ts} +0 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
// app.ts — the interactive `crtr canvas browse` runtime.
|
|
2
|
+
//
|
|
3
|
+
// Owns the browser state + the stdin keystroke loop. Pure logic lives in
|
|
4
|
+
// model.ts (buildTree/flatten/fuzzyMatch) and render.ts (renderFrame); this file
|
|
5
|
+
// wires the canvas data access in, holds mutable state, and translates keys.
|
|
6
|
+
//
|
|
7
|
+
// Resume is one action: Enter routes the chosen node through `crtr node focus`,
|
|
8
|
+
// which goes via reviveNode() — the ONLY sanctioned open. NEVER spawn
|
|
9
|
+
// `pi --session` directly (see canvas-resume.ts header for the desync hazard).
|
|
10
|
+
import { execFileSync } from 'node:child_process';
|
|
11
|
+
import { resolve } from 'node:path';
|
|
12
|
+
import { dashboardRowsAll, renderForest } from '../render.js';
|
|
13
|
+
import { listNodes, subscriptionsOf } from '../canvas.js';
|
|
14
|
+
import { setupTerminal, restoreTerminal, getTerminalSize, parseKeypress, } from './terminal.js';
|
|
15
|
+
import { buildTree, flatten, TABS, SORTS } from './model.js';
|
|
16
|
+
import { renderFrame, detectColorCaps, headerHeight, PREVIEW_HEIGHT } from './render.js';
|
|
17
|
+
/** Viewport (body) height = total rows minus the header renderFrame draws (see
|
|
18
|
+
* render.ts headerHeight), the footer, and the preview panel when shown. Kept
|
|
19
|
+
* in lockstep with render.ts via the shared headerHeight/PREVIEW_HEIGHT. */
|
|
20
|
+
function viewportHeight(rowsTotal, search, previewOn) {
|
|
21
|
+
const rows = Math.max(8, rowsTotal);
|
|
22
|
+
const previewH = previewOn ? PREVIEW_HEIGHT : 0;
|
|
23
|
+
return Math.max(1, rows - headerHeight(search) - 1 /* footer */ - previewH);
|
|
24
|
+
}
|
|
25
|
+
export async function runBrowse(opts = {}) {
|
|
26
|
+
// No TTY → print the static forest and exit 0 (no raw mode).
|
|
27
|
+
if (!process.stdin.isTTY) {
|
|
28
|
+
process.stdout.write(renderForest() + '\n');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
// Snapshot the canvas. Drop kind:'human' control-plane decks — they have no pi
|
|
32
|
+
// session, so `node focus` refuses them; they are never a navigation/resume
|
|
33
|
+
// target (mirrors canvas-resume.ts / the node focus guard).
|
|
34
|
+
const rows = dashboardRowsAll().filter((r) => r.kind !== 'human');
|
|
35
|
+
const rootIds = listNodes()
|
|
36
|
+
.filter((n) => n.parent === null && n.kind !== 'human')
|
|
37
|
+
.map((n) => n.node_id);
|
|
38
|
+
const tree = buildTree(rows, rootIds, (id) => subscriptionsOf(id).map((s) => s.node_id));
|
|
39
|
+
const totalNodes = tree.nodes.size;
|
|
40
|
+
// Default cwd scope = the dir browse was launched from (the request). The popup
|
|
41
|
+
// / command passes --cwd; resolve it so it compares cleanly against stored cwds.
|
|
42
|
+
// null when unknown → All dirs (the toggle's other state).
|
|
43
|
+
const launchCwd = opts.cwd !== undefined && opts.cwd.trim() !== '' ? resolve(opts.cwd) : null;
|
|
44
|
+
const state = {
|
|
45
|
+
tab: 'All',
|
|
46
|
+
cursor: 0,
|
|
47
|
+
// Initial collapse = every node with children → only roots/top-level show.
|
|
48
|
+
collapsed: new Set([...tree.nodes.entries()].filter(([, n]) => n.childIds.length > 0).map(([id]) => id)),
|
|
49
|
+
query: '',
|
|
50
|
+
search: false,
|
|
51
|
+
scrollOffset: 0,
|
|
52
|
+
cwdScope: launchCwd, // default: this dir
|
|
53
|
+
sort: 'tree',
|
|
54
|
+
preview: true, // default ON (decision)
|
|
55
|
+
};
|
|
56
|
+
let visible = [];
|
|
57
|
+
// Color capability is fixed for the session (it's a property of the tty/env).
|
|
58
|
+
const caps = detectColorCaps();
|
|
59
|
+
// Restore the terminal exactly once, however we leave (quit, resume, crash).
|
|
60
|
+
let restored = false;
|
|
61
|
+
const cleanup = () => {
|
|
62
|
+
if (restored)
|
|
63
|
+
return;
|
|
64
|
+
restored = true;
|
|
65
|
+
try {
|
|
66
|
+
restoreTerminal();
|
|
67
|
+
}
|
|
68
|
+
catch { /* best-effort */ }
|
|
69
|
+
};
|
|
70
|
+
// Safety net: an uncaught throw in the (un-unit-tested) keystroke path must
|
|
71
|
+
// never strand the tty in raw + alt-screen + hidden-cursor.
|
|
72
|
+
process.once('exit', cleanup);
|
|
73
|
+
/** Open the chosen node — the ONLY sanctioned path (reviveNode via node focus). */
|
|
74
|
+
const selectAndFocus = (id) => {
|
|
75
|
+
cleanup();
|
|
76
|
+
const args = ['node', 'focus', id, ...(opts.returnPane !== undefined && opts.returnPane !== '' ? ['--pane', opts.returnPane] : [])];
|
|
77
|
+
try {
|
|
78
|
+
execFileSync('crtr', args, { stdio: 'inherit' });
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// `node focus` swaps panes out from under us; a sync call can be
|
|
82
|
+
// interrupted. Best-effort — the swap is what matters.
|
|
83
|
+
}
|
|
84
|
+
process.exit(0);
|
|
85
|
+
};
|
|
86
|
+
const recompute = (keepId) => {
|
|
87
|
+
visible = flatten(tree, {
|
|
88
|
+
collapsed: state.collapsed,
|
|
89
|
+
tab: state.tab,
|
|
90
|
+
query: state.query,
|
|
91
|
+
cwdScope: state.cwdScope,
|
|
92
|
+
sort: state.sort,
|
|
93
|
+
});
|
|
94
|
+
if (keepId !== undefined) {
|
|
95
|
+
const idx = visible.findIndex((v) => v.id === keepId);
|
|
96
|
+
if (idx >= 0)
|
|
97
|
+
state.cursor = idx;
|
|
98
|
+
}
|
|
99
|
+
if (state.cursor > visible.length - 1)
|
|
100
|
+
state.cursor = Math.max(0, visible.length - 1);
|
|
101
|
+
if (state.cursor < 0)
|
|
102
|
+
state.cursor = 0;
|
|
103
|
+
};
|
|
104
|
+
const flush = () => {
|
|
105
|
+
const size = getTerminalSize();
|
|
106
|
+
const previewOn = state.preview && visible.length > 0;
|
|
107
|
+
const viewport = viewportHeight(size.rows, state.search, previewOn);
|
|
108
|
+
// Keep the cursor inside the viewport window.
|
|
109
|
+
if (state.cursor < state.scrollOffset)
|
|
110
|
+
state.scrollOffset = state.cursor;
|
|
111
|
+
if (state.cursor >= state.scrollOffset + viewport)
|
|
112
|
+
state.scrollOffset = state.cursor - viewport + 1;
|
|
113
|
+
if (state.scrollOffset < 0)
|
|
114
|
+
state.scrollOffset = 0;
|
|
115
|
+
const frame = renderFrame({
|
|
116
|
+
tree, visible, tab: state.tab, cursor: state.cursor, scrollOffset: state.scrollOffset,
|
|
117
|
+
query: state.query, search: state.search, totalNodes,
|
|
118
|
+
cwdScope: state.cwdScope, sort: state.sort, preview: state.preview,
|
|
119
|
+
}, size, caps);
|
|
120
|
+
process.stdout.write(frame);
|
|
121
|
+
};
|
|
122
|
+
const quit = () => {
|
|
123
|
+
cleanup();
|
|
124
|
+
process.exit(0);
|
|
125
|
+
};
|
|
126
|
+
const curRow = () => visible[state.cursor];
|
|
127
|
+
const isExpanded = (id) => !state.collapsed.has(id);
|
|
128
|
+
const cycleTab = (dir) => {
|
|
129
|
+
const i = TABS.indexOf(state.tab);
|
|
130
|
+
state.tab = TABS[(i + dir + TABS.length) % TABS.length];
|
|
131
|
+
state.cursor = 0;
|
|
132
|
+
state.scrollOffset = 0;
|
|
133
|
+
recompute();
|
|
134
|
+
};
|
|
135
|
+
// Cycle sort (tree → relevance → recency), keeping the selected node put.
|
|
136
|
+
const cycleSort = () => {
|
|
137
|
+
const keep = curRow()?.id;
|
|
138
|
+
const i = SORTS.indexOf(state.sort);
|
|
139
|
+
state.sort = SORTS[(i + 1) % SORTS.length];
|
|
140
|
+
recompute(keep);
|
|
141
|
+
};
|
|
142
|
+
// Toggle cwd scope between the launch dir and All dirs (no-op if launch dir
|
|
143
|
+
// is unknown — stays All dirs). Keeps the selected node put when still in view.
|
|
144
|
+
const toggleScope = () => {
|
|
145
|
+
const keep = curRow()?.id;
|
|
146
|
+
state.cwdScope = state.cwdScope === null ? launchCwd : null;
|
|
147
|
+
recompute(keep);
|
|
148
|
+
};
|
|
149
|
+
const onKeySearch = (input, key) => {
|
|
150
|
+
if (key.escape) {
|
|
151
|
+
// Cancel the search: drop the query AND the relevance ranking it switched
|
|
152
|
+
// on, returning to the tree.
|
|
153
|
+
state.search = false;
|
|
154
|
+
state.query = '';
|
|
155
|
+
state.sort = 'tree';
|
|
156
|
+
recompute();
|
|
157
|
+
flush();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (key.return) {
|
|
161
|
+
// Commit: keep the filter, drop search mode, land on the first match.
|
|
162
|
+
state.search = false;
|
|
163
|
+
const firstMatch = visible.findIndex((v) => v.matched);
|
|
164
|
+
if (firstMatch >= 0)
|
|
165
|
+
state.cursor = firstMatch;
|
|
166
|
+
recompute();
|
|
167
|
+
flush();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (key.backspace) {
|
|
171
|
+
state.query = state.query.slice(0, -1);
|
|
172
|
+
recompute();
|
|
173
|
+
flush();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
// Any ctrl-combo: Ctrl+C quits; everything else is swallowed (never typed).
|
|
177
|
+
if (key.ctrl) {
|
|
178
|
+
if (input === 'c')
|
|
179
|
+
quit();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
// Printable single char → append. Ignore multi-byte / control chunks.
|
|
183
|
+
if (input.length === 1 && input >= ' ') {
|
|
184
|
+
state.query += input;
|
|
185
|
+
recompute();
|
|
186
|
+
flush();
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
const onKeyNav = (input, key) => {
|
|
190
|
+
// Ctrl-combos first: only Ctrl+C is meaningful (quit); swallow the rest so
|
|
191
|
+
// Ctrl+L / Ctrl+J / Ctrl+Q etc. don't masquerade as l/j/q commands.
|
|
192
|
+
if (key.ctrl) {
|
|
193
|
+
if (input === 'c')
|
|
194
|
+
quit();
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const row = curRow();
|
|
198
|
+
// Quit.
|
|
199
|
+
if (input === 'q' || key.escape)
|
|
200
|
+
quit();
|
|
201
|
+
// Move.
|
|
202
|
+
if (key.upArrow || input === 'k') {
|
|
203
|
+
state.cursor = Math.max(0, state.cursor - 1);
|
|
204
|
+
flush();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (key.downArrow || input === 'j') {
|
|
208
|
+
state.cursor = Math.max(0, Math.min(visible.length - 1, state.cursor + 1));
|
|
209
|
+
flush();
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (input === 'g') {
|
|
213
|
+
state.cursor = 0;
|
|
214
|
+
flush();
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (input === 'G') {
|
|
218
|
+
state.cursor = Math.max(0, visible.length - 1);
|
|
219
|
+
flush();
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
// Expand / descend.
|
|
223
|
+
if (key.rightArrow || input === 'l') {
|
|
224
|
+
if (row !== undefined && row.hasChildren) {
|
|
225
|
+
if (!isExpanded(row.id)) {
|
|
226
|
+
state.collapsed.delete(row.id);
|
|
227
|
+
recompute(row.id);
|
|
228
|
+
}
|
|
229
|
+
else if (state.cursor + 1 < visible.length && visible[state.cursor + 1].depth > row.depth) {
|
|
230
|
+
state.cursor += 1; // already expanded → step onto first child
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
flush();
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
// Collapse / ascend.
|
|
237
|
+
if (key.leftArrow || input === 'h') {
|
|
238
|
+
if (row !== undefined && row.hasChildren && isExpanded(row.id)) {
|
|
239
|
+
state.collapsed.add(row.id);
|
|
240
|
+
recompute(row.id);
|
|
241
|
+
}
|
|
242
|
+
else if (row !== undefined) {
|
|
243
|
+
const parentId = tree.nodes.get(row.id)?.parentId ?? null;
|
|
244
|
+
if (parentId !== null) {
|
|
245
|
+
const idx = visible.findIndex((v) => v.id === parentId);
|
|
246
|
+
if (idx >= 0)
|
|
247
|
+
state.cursor = idx;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
flush();
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
// Toggle collapse.
|
|
254
|
+
if (input === ' ') {
|
|
255
|
+
if (row !== undefined && row.hasChildren) {
|
|
256
|
+
if (isExpanded(row.id))
|
|
257
|
+
state.collapsed.add(row.id);
|
|
258
|
+
else
|
|
259
|
+
state.collapsed.delete(row.id);
|
|
260
|
+
recompute(row.id);
|
|
261
|
+
}
|
|
262
|
+
flush();
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
// Tabs.
|
|
266
|
+
if (key.tab || input === ']') {
|
|
267
|
+
cycleTab(1);
|
|
268
|
+
flush();
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (key.shiftTab || input === '[') {
|
|
272
|
+
cycleTab(-1);
|
|
273
|
+
flush();
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (input >= '1' && input <= '4') {
|
|
277
|
+
const idx = Number(input) - 1;
|
|
278
|
+
if (idx < TABS.length) {
|
|
279
|
+
state.tab = TABS[idx];
|
|
280
|
+
state.cursor = 0;
|
|
281
|
+
state.scrollOffset = 0;
|
|
282
|
+
recompute();
|
|
283
|
+
}
|
|
284
|
+
flush();
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
// Sort / scope / preview.
|
|
288
|
+
if (input === 's') {
|
|
289
|
+
cycleSort();
|
|
290
|
+
flush();
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (input === 'c') {
|
|
294
|
+
toggleScope();
|
|
295
|
+
flush();
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (input === 'p') {
|
|
299
|
+
state.preview = !state.preview;
|
|
300
|
+
flush();
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
// Search. Starting a search ranks by relevance (decision) so the best prompt/
|
|
304
|
+
// name match floats to the top as you type.
|
|
305
|
+
if (input === '/') {
|
|
306
|
+
state.search = true;
|
|
307
|
+
state.query = '';
|
|
308
|
+
state.sort = 'relevance';
|
|
309
|
+
state.cursor = 0;
|
|
310
|
+
state.scrollOffset = 0;
|
|
311
|
+
recompute();
|
|
312
|
+
flush();
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
// Resume.
|
|
316
|
+
if (key.return) {
|
|
317
|
+
if (row !== undefined)
|
|
318
|
+
selectAndFocus(row.id);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
// Boot. If the launch dir holds NO nodes, the default this-dir scope would show
|
|
323
|
+
// a blank canvas — fall back to All dirs so browse is never empty on open.
|
|
324
|
+
recompute();
|
|
325
|
+
if (visible.length === 0 && state.cwdScope !== null) {
|
|
326
|
+
state.cwdScope = null;
|
|
327
|
+
recompute();
|
|
328
|
+
}
|
|
329
|
+
setupTerminal();
|
|
330
|
+
flush();
|
|
331
|
+
await new Promise(() => {
|
|
332
|
+
const onData = (data) => {
|
|
333
|
+
try {
|
|
334
|
+
const { input, key } = parseKeypress(data);
|
|
335
|
+
if (state.search)
|
|
336
|
+
onKeySearch(input, key);
|
|
337
|
+
else
|
|
338
|
+
onKeyNav(input, key);
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
// Never let a keystroke crash leave the tty wedged.
|
|
342
|
+
cleanup();
|
|
343
|
+
process.exit(1);
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
process.stdin.on('data', onData);
|
|
347
|
+
process.stdout.on('resize', flush);
|
|
348
|
+
});
|
|
349
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { DashboardRow } from '../render.js';
|
|
2
|
+
import type { NodeStatus } from '../types.js';
|
|
3
|
+
export type Tab = 'All' | 'Live' | 'Dormant' | 'Flagged';
|
|
4
|
+
export declare const TABS: readonly Tab[];
|
|
5
|
+
/** How the visible rows are ordered.
|
|
6
|
+
* tree — spanning-tree order, ancestors shown for context (the default).
|
|
7
|
+
* relevance — FLAT list, best query match first (super-search).
|
|
8
|
+
* recency — FLAT list, newest `created` first. */
|
|
9
|
+
export type SortMode = 'tree' | 'relevance' | 'recency';
|
|
10
|
+
export declare const SORTS: readonly SortMode[];
|
|
11
|
+
/** Does a node belong to this tab's slice?
|
|
12
|
+
* All — every node.
|
|
13
|
+
* Live — active | idle.
|
|
14
|
+
* Dormant — done | dead | canceled.
|
|
15
|
+
* Flagged — has > 0 pending human asks. */
|
|
16
|
+
export declare function tabPredicate(tab: Tab, row: DashboardRow): boolean;
|
|
17
|
+
export interface TreeNode {
|
|
18
|
+
row: DashboardRow;
|
|
19
|
+
depth: number;
|
|
20
|
+
parentId: string | null;
|
|
21
|
+
childIds: string[];
|
|
22
|
+
}
|
|
23
|
+
export interface Tree {
|
|
24
|
+
/** Ordered root ids (live-first, then stragglers). */
|
|
25
|
+
roots: string[];
|
|
26
|
+
nodes: Map<string, TreeNode>;
|
|
27
|
+
}
|
|
28
|
+
/** Sort rank for roots/stragglers — live first (active, then idle), dormant
|
|
29
|
+
* after. Mirrors render.ts / canvas-resume.ts statusRank. */
|
|
30
|
+
export declare function statusRank(status: NodeStatus): number;
|
|
31
|
+
/**
|
|
32
|
+
* Build a spanning tree of the whole canvas.
|
|
33
|
+
* - `rows` — one DashboardRow per node (display text + status/asks).
|
|
34
|
+
* - `rootIds` — node ids whose `parent === null` (raw, unsorted).
|
|
35
|
+
* - `childIdsOf` — a node's children = the nodes it subscribes to (its
|
|
36
|
+
* reports), in edge order. (= subscriptionsOf(id) node ids.)
|
|
37
|
+
*
|
|
38
|
+
* Roots are sorted live-first. The graph is walked DFS-preorder; the FIRST
|
|
39
|
+
* parent to reach a node owns it (cycle-/multi-parent-safe via `visited`). Any
|
|
40
|
+
* node never reached from a root (orphaned by a missing subscription edge) is
|
|
41
|
+
* appended as a depth-0 straggler so "All" is genuinely the whole canvas.
|
|
42
|
+
*/
|
|
43
|
+
export declare function buildTree(rows: DashboardRow[], rootIds: string[], childIdsOf: (id: string) => string[]): Tree;
|
|
44
|
+
/** Case-insensitive subsequence match: every char of `query` appears in `text`
|
|
45
|
+
* in order (gaps allowed). Empty query matches everything. Substrings are a
|
|
46
|
+
* subsequence, so this subsumes substring matching too. */
|
|
47
|
+
export declare function fuzzyMatch(query: string, text: string): boolean;
|
|
48
|
+
/** Indices in `text` consumed by a greedy left-to-right subsequence match of
|
|
49
|
+
* `query` — the same walk as `fuzzyMatch`, but returning WHICH chars matched so
|
|
50
|
+
* the renderer can highlight them. Empty set when `query` is empty OR does not
|
|
51
|
+
* fully match (no partial highlights). */
|
|
52
|
+
export declare function matchIndices(query: string, text: string): Set<number>;
|
|
53
|
+
/** Does this row match the live query? Super-search spans name (which already
|
|
54
|
+
* folds in the pi-generated description), kind, short-id, AND the spawn prompt
|
|
55
|
+
* (`row.goal`). Empty query matches everything. */
|
|
56
|
+
export declare function queryMatch(query: string, row: DashboardRow): boolean;
|
|
57
|
+
/** Score how well `query` matches one field, 0 (no match) → 1 (exact). Tiers:
|
|
58
|
+
* exact > prefix > word-boundary substring > interior substring > subsequence.
|
|
59
|
+
* An interior match decays slightly the later it starts so leading matches win. */
|
|
60
|
+
export declare function fieldScore(query: string, text: string): number;
|
|
61
|
+
/** Weighted relevance of a row to the query across all searched fields. 0 means
|
|
62
|
+
* no field matched (excluded from relevance results, same as `queryMatch`). */
|
|
63
|
+
export declare function scoreRow(query: string, row: DashboardRow): number;
|
|
64
|
+
export interface VisibleRow {
|
|
65
|
+
id: string;
|
|
66
|
+
depth: number;
|
|
67
|
+
hasChildren: boolean;
|
|
68
|
+
collapsed: boolean;
|
|
69
|
+
matched: boolean;
|
|
70
|
+
}
|
|
71
|
+
export interface FlattenOpts {
|
|
72
|
+
collapsed: Set<string>;
|
|
73
|
+
tab: Tab;
|
|
74
|
+
query: string;
|
|
75
|
+
/** cwd-scope filter: only rows pinned to this dir are directly-matched. null /
|
|
76
|
+
* undefined = All dirs (no cwd filter). Like the tab predicate, it gates the
|
|
77
|
+
* matched set — ancestors from other dirs still render dimmed for tree context. */
|
|
78
|
+
cwdScope?: string | null;
|
|
79
|
+
/** Ordering. `tree` keeps the spanning tree + ancestor context; `relevance` /
|
|
80
|
+
* `recency` produce a FLAT ranked list of directly-matched rows. */
|
|
81
|
+
sort?: SortMode;
|
|
82
|
+
}
|
|
83
|
+
/** Is this row inside the active cwd scope? No scope (null/undefined) = All dirs. */
|
|
84
|
+
export declare function cwdMatch(scope: string | null | undefined, row: DashboardRow): boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Flatten the tree to the ordered list of currently-visible rows.
|
|
87
|
+
*
|
|
88
|
+
* Inclusion: a node is shown when it directly matches (tab predicate AND query)
|
|
89
|
+
* — flagged `matched:true` — OR it is an ANCESTOR of a directly-matched node
|
|
90
|
+
* (shown for tree context, `matched:false`, dimmed by the renderer).
|
|
91
|
+
*
|
|
92
|
+
* Collapse: children are emitted only under an EXPANDED node. A node is expanded
|
|
93
|
+
* when it is not in `collapsed` — except under a non-empty query, where every
|
|
94
|
+
* ancestor-of-a-match is force-expanded regardless of `collapsed` so matches are
|
|
95
|
+
* always reachable.
|
|
96
|
+
*/
|
|
97
|
+
export declare function flatten(tree: Tree, opts: FlattenOpts): VisibleRow[];
|