@crouton-kit/crouter 0.3.16 → 0.3.18

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.
Files changed (104) hide show
  1. package/dist/builtin-personas/design/{base.md → PERSONA.md} +4 -0
  2. package/dist/builtin-personas/developer/{base.md → PERSONA.md} +4 -0
  3. package/dist/builtin-personas/explore/{base.md → PERSONA.md} +4 -0
  4. package/dist/builtin-personas/general/{base.md → PERSONA.md} +4 -0
  5. package/dist/builtin-personas/orchestration-kernel.md +6 -6
  6. package/dist/builtin-personas/plan/{base.md → PERSONA.md} +5 -1
  7. package/dist/builtin-personas/plan/reviewers/architecture-fit/{base.md → PERSONA.md} +1 -1
  8. package/dist/builtin-personas/plan/reviewers/code-smells/{base.md → PERSONA.md} +1 -1
  9. package/dist/builtin-personas/plan/reviewers/pattern-consistency/{base.md → PERSONA.md} +1 -1
  10. package/dist/builtin-personas/plan/reviewers/requirements-coverage/{base.md → PERSONA.md} +1 -1
  11. package/dist/builtin-personas/plan/reviewers/security/{base.md → PERSONA.md} +1 -1
  12. package/dist/builtin-personas/review/{base.md → PERSONA.md} +4 -0
  13. package/dist/builtin-personas/spec/{base.md → PERSONA.md} +5 -1
  14. package/dist/builtin-skills/skills/crouter-development/personas/SKILL.md +24 -14
  15. package/dist/builtin-skills/skills/crouter-development/personas/base-prompt/SKILL.md +4 -4
  16. package/dist/commands/canvas-browse.d.ts +2 -0
  17. package/dist/commands/canvas-browse.js +45 -0
  18. package/dist/commands/canvas-prune.js +11 -2
  19. package/dist/commands/canvas.js +3 -2
  20. package/dist/commands/daemon.js +1 -1
  21. package/dist/commands/human/prompts.js +3 -9
  22. package/dist/commands/human/shared.d.ts +26 -1
  23. package/dist/commands/human/shared.js +48 -10
  24. package/dist/commands/node.js +66 -4
  25. package/dist/commands/skill/author.js +2 -2
  26. package/dist/core/__tests__/cascade-close.test.js +199 -0
  27. package/dist/core/__tests__/daemon-boot.test.js +7 -0
  28. package/dist/core/__tests__/daemon-liveness.test.js +59 -4
  29. package/dist/core/__tests__/dead-pane-regression.test.js +151 -0
  30. package/dist/core/__tests__/fixtures/fake-pi-host.d.ts +2 -0
  31. package/dist/core/__tests__/fixtures/fake-pi-host.js +301 -0
  32. package/dist/core/__tests__/flagship-lifecycle.test.js +273 -0
  33. package/dist/core/__tests__/grace-clock.test.d.ts +1 -0
  34. package/dist/core/__tests__/grace-clock.test.js +115 -0
  35. package/dist/core/__tests__/helpers/harness.d.ts +78 -0
  36. package/dist/core/__tests__/helpers/harness.js +406 -0
  37. package/dist/core/__tests__/human-surface-target.test.d.ts +1 -0
  38. package/dist/core/__tests__/human-surface-target.test.js +98 -0
  39. package/dist/core/__tests__/lifecycle.test.js +6 -13
  40. package/dist/core/__tests__/live-mutation.test.d.ts +1 -0
  41. package/dist/core/__tests__/live-mutation.test.js +341 -0
  42. package/dist/core/__tests__/persona-subkind.test.js +18 -15
  43. package/dist/core/__tests__/placement-focus.test.js +53 -15
  44. package/dist/core/__tests__/relaunch.test.js +12 -12
  45. package/dist/core/__tests__/reset.test.js +11 -6
  46. package/dist/core/__tests__/spike-harness.test.d.ts +1 -0
  47. package/dist/core/__tests__/spike-harness.test.js +241 -0
  48. package/dist/core/__tests__/subscription-delivery.test.d.ts +1 -0
  49. package/dist/core/__tests__/subscription-delivery.test.js +233 -0
  50. package/dist/core/canvas/browse/__tests__/model.test.d.ts +1 -0
  51. package/dist/core/canvas/browse/__tests__/model.test.js +142 -0
  52. package/dist/core/canvas/browse/__tests__/render.test.d.ts +1 -0
  53. package/dist/core/canvas/browse/__tests__/render.test.js +102 -0
  54. package/dist/core/canvas/browse/app.d.ts +4 -0
  55. package/dist/core/canvas/browse/app.js +349 -0
  56. package/dist/core/canvas/browse/model.d.ts +97 -0
  57. package/dist/core/canvas/browse/model.js +258 -0
  58. package/dist/core/canvas/browse/render.d.ts +41 -0
  59. package/dist/core/canvas/browse/render.js +387 -0
  60. package/dist/core/canvas/browse/terminal.d.ts +23 -0
  61. package/dist/core/canvas/browse/terminal.js +100 -0
  62. package/dist/core/canvas/canvas.d.ts +9 -2
  63. package/dist/core/canvas/canvas.js +41 -3
  64. package/dist/core/canvas/paths.d.ts +4 -1
  65. package/dist/core/canvas/paths.js +10 -4
  66. package/dist/core/canvas/render.d.ts +10 -0
  67. package/dist/core/canvas/render.js +25 -1
  68. package/dist/core/canvas/types.js +2 -2
  69. package/dist/core/feed/inbox.d.ts +0 -3
  70. package/dist/core/feed/inbox.js +1 -5
  71. package/dist/core/help.d.ts +6 -0
  72. package/dist/core/help.js +7 -0
  73. package/dist/core/personas/index.d.ts +4 -3
  74. package/dist/core/personas/index.js +3 -2
  75. package/dist/core/personas/loader.d.ts +34 -16
  76. package/dist/core/personas/loader.js +102 -29
  77. package/dist/core/personas/resolve.d.ts +4 -4
  78. package/dist/core/personas/resolve.js +16 -14
  79. package/dist/core/runtime/busy.d.ts +8 -0
  80. package/dist/core/runtime/busy.js +46 -0
  81. package/dist/core/runtime/lifecycle.d.ts +1 -1
  82. package/dist/core/runtime/lifecycle.js +12 -4
  83. package/dist/core/runtime/naming.d.ts +3 -3
  84. package/dist/core/runtime/naming.js +6 -6
  85. package/dist/core/runtime/placement.d.ts +32 -5
  86. package/dist/core/runtime/placement.js +81 -14
  87. package/dist/core/runtime/reset.d.ts +11 -8
  88. package/dist/core/runtime/reset.js +23 -18
  89. package/dist/core/spawn.d.ts +20 -1
  90. package/dist/core/spawn.js +52 -5
  91. package/dist/daemon/crtrd.js +43 -21
  92. package/dist/pi-extensions/canvas-nav.js +106 -55
  93. package/dist/pi-extensions/canvas-resume.d.ts +0 -1
  94. package/dist/pi-extensions/canvas-resume.js +35 -126
  95. package/dist/pi-extensions/canvas-stophook.d.ts +1 -1
  96. package/dist/pi-extensions/canvas-stophook.js +16 -0
  97. package/dist/prompts/skill.js +6 -1
  98. package/package.json +1 -1
  99. package/dist/commands/__tests__/skill.test.js +0 -290
  100. package/dist/core/__tests__/pkg.test.js +0 -218
  101. package/dist/core/__tests__/sys.test.js +0 -208
  102. /package/dist/{commands/__tests__/skill.test.d.ts → core/__tests__/cascade-close.test.d.ts} +0 -0
  103. /package/dist/core/__tests__/{pkg.test.d.ts → dead-pane-regression.test.d.ts} +0 -0
  104. /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[];