@crouton-kit/crouter 0.3.17 → 0.3.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-root.js +6 -0
- package/dist/builtin-memory/crouter-development/marketplaces.md +164 -0
- package/dist/builtin-memory/crouter-development/personas/base-prompt.md +58 -0
- package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +60 -0
- package/dist/builtin-memory/crouter-development/personas.md +117 -0
- package/dist/builtin-memory/crouter-development/plugins.md +163 -0
- package/dist/builtin-memory/design.md +60 -0
- package/dist/builtin-memory/development.md +116 -0
- package/dist/builtin-memory/planning.md +66 -0
- package/dist/builtin-memory/spec.md +99 -0
- package/dist/builtin-personas/design/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/design/orchestrator.md +1 -1
- package/dist/builtin-personas/developer/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/developer/orchestrator.md +1 -1
- package/dist/builtin-personas/explore/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/general/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/lifecycle/resident.md +1 -1
- package/dist/builtin-personas/lifecycle/terminal.md +5 -2
- package/dist/builtin-personas/orchestration-kernel.md +15 -24
- package/dist/builtin-personas/plan/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/plan/orchestrator.md +1 -1
- package/dist/builtin-personas/plan/reviewers/architecture-fit/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/plan/reviewers/code-smells/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/plan/reviewers/pattern-consistency/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/plan/reviewers/requirements-coverage/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/plan/reviewers/security/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/review/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/runtime-base.md +6 -3
- package/dist/builtin-personas/spec/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/spec/orchestrator.md +1 -1
- package/dist/builtin-personas/waiting.md +8 -0
- package/dist/builtin-skills/skills/crouter-development/personas/SKILL.md +24 -14
- package/dist/builtin-skills/skills/crouter-development/personas/base-prompt/SKILL.md +4 -4
- package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +1 -1
- package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +2 -2
- package/dist/builtin-views/_lib/states.mjs +161 -0
- package/dist/builtin-views/canvas/client.mjs +303 -0
- package/dist/builtin-views/canvas/view.mjs +576 -0
- package/dist/builtin-views/git-pr/client.mjs +440 -0
- package/dist/builtin-views/git-pr/view.mjs +675 -0
- package/dist/builtin-views/inbox/_lib/render.mjs +177 -0
- package/dist/builtin-views/inbox/sources/gmail.mjs +965 -0
- package/dist/builtin-views/inbox/sources/linkedin.mjs +427 -0
- package/dist/builtin-views/inbox/view.mjs +889 -0
- package/dist/builtin-views/linkedin/client.mjs +610 -0
- package/dist/builtin-views/linkedin/view.mjs +1171 -0
- package/dist/clients/attach/attach-cmd.d.ts +2 -0
- package/dist/clients/attach/attach-cmd.js +354 -0
- package/dist/clients/attach/chat-view.d.ts +77 -0
- package/dist/clients/attach/chat-view.js +450 -0
- package/dist/clients/attach/clipboard-image.d.ts +16 -0
- package/dist/clients/attach/clipboard-image.js +113 -0
- package/dist/clients/attach/config-load.d.ts +31 -0
- package/dist/clients/attach/config-load.js +113 -0
- package/dist/clients/attach/extension-dialogs.d.ts +29 -0
- package/dist/clients/attach/extension-dialogs.js +101 -0
- package/dist/clients/attach/input-controller.d.ts +54 -0
- package/dist/clients/attach/input-controller.js +204 -0
- package/dist/clients/attach/slash-commands.d.ts +36 -0
- package/dist/clients/attach/slash-commands.js +200 -0
- package/dist/clients/attach/view-socket.d.ts +48 -0
- package/dist/clients/attach/view-socket.js +126 -0
- package/dist/commands/attention.js +3 -3
- package/dist/commands/canvas-prune.js +1 -1
- package/dist/commands/daemon.js +4 -3
- package/dist/commands/human/prompts.js +4 -10
- package/dist/commands/human/queue.js +43 -8
- package/dist/commands/human/shared.d.ts +28 -1
- package/dist/commands/human/shared.js +48 -10
- package/dist/commands/memory/find.d.ts +1 -0
- package/dist/commands/memory/find.js +180 -0
- package/dist/commands/memory/lint.d.ts +1 -0
- package/dist/commands/memory/lint.js +140 -0
- package/dist/commands/memory/list.d.ts +1 -0
- package/dist/commands/memory/list.js +79 -0
- package/dist/commands/memory/read.js +103 -0
- package/dist/commands/memory/shared.d.ts +30 -0
- package/dist/commands/memory/shared.js +122 -0
- package/dist/commands/memory/write.d.ts +1 -0
- package/dist/commands/memory/write.js +85 -0
- package/dist/commands/memory.d.ts +2 -0
- package/dist/commands/memory.js +27 -0
- package/dist/commands/node.d.ts +3 -2
- package/dist/commands/node.js +660 -65
- package/dist/commands/pkg/market-manage.js +1 -1
- package/dist/commands/push.js +6 -6
- package/dist/commands/revive.js +1 -1
- package/dist/commands/skill/author.js +1 -1
- package/dist/commands/skill/shared.d.ts +1 -8
- package/dist/commands/skill/shared.js +2 -55
- package/dist/commands/skill.js +9 -14
- package/dist/commands/sys/doctor.js +1 -1
- package/dist/commands/sys/update.js +1 -1
- package/dist/commands/view-cycle.d.ts +2 -0
- package/dist/commands/view-cycle.js +125 -0
- package/dist/commands/view-list.d.ts +2 -0
- package/dist/commands/view-list.js +66 -0
- package/dist/commands/view-new.d.ts +2 -0
- package/dist/commands/view-new.js +70 -0
- package/dist/commands/view-pick.d.ts +2 -0
- package/dist/commands/view-pick.js +119 -0
- package/dist/commands/view-run.d.ts +2 -0
- package/dist/commands/view-run.js +191 -0
- package/dist/commands/view.d.ts +2 -0
- package/dist/commands/view.js +29 -0
- package/dist/core/__tests__/broker-lifecycle.test.d.ts +1 -0
- package/dist/core/__tests__/broker-lifecycle.test.js +677 -0
- package/dist/core/__tests__/broker-sdk-wiring.test.d.ts +1 -0
- package/dist/core/__tests__/broker-sdk-wiring.test.js +166 -0
- package/dist/core/__tests__/cascade-close.test.js +12 -2
- package/dist/core/__tests__/child-death-wake.test.d.ts +1 -0
- package/dist/core/__tests__/child-death-wake.test.js +245 -0
- package/dist/core/__tests__/context-intro.test.js +76 -62
- package/dist/core/__tests__/daemon-boot.test.js +14 -5
- package/dist/core/__tests__/daemon-liveness.test.js +34 -9
- package/dist/core/__tests__/detach-focus.test.d.ts +1 -0
- package/dist/core/__tests__/detach-focus.test.js +206 -0
- package/dist/core/__tests__/draw-style.test.d.ts +1 -0
- package/dist/core/__tests__/draw-style.test.js +258 -0
- package/dist/core/__tests__/fixtures/c3-custom-provider-ext.d.ts +2 -0
- package/dist/core/__tests__/fixtures/c3-custom-provider-ext.js +18 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +138 -0
- package/dist/core/__tests__/fixtures/fake-engine.js +614 -0
- package/dist/core/__tests__/fixtures/fake-pi-host.js +1 -0
- package/dist/core/__tests__/flagship-lifecycle.test.js +7 -1
- package/dist/core/__tests__/frame-decoder-perf.test.d.ts +1 -0
- package/dist/core/__tests__/frame-decoder-perf.test.js +198 -0
- package/dist/core/__tests__/helpers/harness.d.ts +9 -0
- package/dist/core/__tests__/helpers/harness.js +111 -1
- package/dist/core/__tests__/home-session.test.js +41 -8
- package/dist/core/__tests__/human-new-window-regression.test.d.ts +1 -0
- package/dist/core/__tests__/human-new-window-regression.test.js +101 -0
- package/dist/core/__tests__/human-surface-target.test.d.ts +1 -0
- package/dist/core/__tests__/human-surface-target.test.js +98 -0
- package/dist/core/__tests__/kickoff.test.js +37 -3
- package/dist/core/__tests__/live-mutation.test.js +50 -33
- package/dist/core/__tests__/memory.test.js +23 -115
- package/dist/core/__tests__/persona-subkind.test.js +18 -15
- package/dist/core/__tests__/placement-focus.test.js +5 -0
- package/dist/core/__tests__/placement-teardown.test.js +54 -11
- package/dist/core/__tests__/relaunch.test.js +4 -3
- package/dist/core/__tests__/review-render-pane-regression.test.d.ts +1 -0
- package/dist/core/__tests__/review-render-pane-regression.test.js +133 -0
- package/dist/core/__tests__/spawn-root.test.js +10 -0
- package/dist/core/__tests__/spike-harness.test.js +1 -0
- package/dist/core/__tests__/wake-bearings.test.d.ts +1 -0
- package/dist/core/__tests__/wake-bearings.test.js +156 -0
- package/dist/core/__tests__/wake-origin.test.d.ts +1 -0
- package/dist/core/__tests__/wake-origin.test.js +110 -0
- package/dist/core/bootstrap.js +1 -1
- package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
- package/dist/core/canvas/browse/app.js +24 -2
- package/dist/core/canvas/browse/model.d.ts +38 -2
- package/dist/core/canvas/browse/model.js +134 -10
- package/dist/core/canvas/browse/render.d.ts +6 -12
- package/dist/core/canvas/browse/render.js +72 -104
- package/dist/core/canvas/canvas.js +15 -22
- package/dist/core/canvas/db.js +46 -0
- package/dist/core/canvas/index.d.ts +1 -0
- package/dist/core/canvas/index.js +1 -0
- package/dist/core/canvas/paths.d.ts +4 -1
- package/dist/core/canvas/paths.js +10 -4
- package/dist/core/canvas/pid.d.ts +4 -0
- package/dist/core/canvas/pid.js +23 -0
- package/dist/core/canvas/render.d.ts +11 -2
- package/dist/core/canvas/render.js +69 -0
- package/dist/core/canvas/types.d.ts +85 -3
- package/dist/core/canvas/types.js +2 -2
- package/dist/core/canvas/wakeups.d.ts +76 -0
- package/dist/core/canvas/wakeups.js +185 -0
- package/dist/core/config.js +4 -1
- package/dist/core/frontmatter.js +37 -124
- package/dist/core/help.d.ts +6 -0
- package/dist/core/help.js +7 -0
- package/dist/core/memory-resolver.d.ts +49 -0
- package/dist/core/memory-resolver.js +141 -0
- package/dist/core/personas/index.d.ts +4 -3
- package/dist/core/personas/index.js +3 -2
- package/dist/core/personas/loader.d.ts +41 -16
- package/dist/core/personas/loader.js +133 -29
- package/dist/core/personas/resolve.d.ts +4 -4
- package/dist/core/personas/resolve.js +28 -16
- package/dist/core/predicate.d.ts +63 -0
- package/dist/core/predicate.js +189 -0
- package/dist/core/resolver.js +26 -5
- package/dist/core/runtime/bearings.d.ts +53 -12
- package/dist/core/runtime/bearings.js +132 -59
- package/dist/core/runtime/broker-cli.d.ts +1 -0
- package/dist/core/runtime/broker-cli.js +46 -0
- package/dist/core/runtime/broker-protocol.d.ts +332 -0
- package/dist/core/runtime/broker-protocol.js +153 -0
- package/dist/core/runtime/broker-sdk.d.ts +48 -0
- package/dist/core/runtime/broker-sdk.js +72 -0
- package/dist/core/runtime/broker.d.ts +55 -0
- package/dist/core/runtime/broker.js +1128 -0
- package/dist/core/runtime/close.js +35 -6
- package/dist/core/runtime/host.d.ts +53 -0
- package/dist/core/runtime/host.js +186 -0
- package/dist/core/runtime/kickoff.d.ts +2 -1
- package/dist/core/runtime/kickoff.js +91 -5
- package/dist/core/runtime/launch.d.ts +45 -2
- package/dist/core/runtime/launch.js +65 -2
- package/dist/core/runtime/lifecycle.js +23 -6
- package/dist/core/runtime/memory.d.ts +2 -42
- package/dist/core/runtime/memory.js +11 -162
- package/dist/core/runtime/nodes.d.ts +33 -0
- package/dist/core/runtime/nodes.js +59 -1
- package/dist/core/runtime/persona.js +21 -11
- package/dist/core/runtime/pi-vendored.d.ts +18 -0
- package/dist/core/runtime/pi-vendored.js +49 -0
- package/dist/core/runtime/placement.d.ts +42 -14
- package/dist/core/runtime/placement.js +228 -38
- package/dist/core/runtime/promote.d.ts +0 -6
- package/dist/core/runtime/promote.js +1 -12
- package/dist/core/runtime/{demote.d.ts → recycle.d.ts} +5 -5
- package/dist/core/runtime/{demote.js → recycle.js} +27 -11
- package/dist/core/runtime/reset.js +8 -6
- package/dist/core/runtime/revive.d.ts +2 -0
- package/dist/core/runtime/revive.js +34 -34
- package/dist/core/runtime/spawn.d.ts +19 -0
- package/dist/core/runtime/spawn.js +75 -22
- package/dist/core/runtime/stop-guard.d.ts +1 -1
- package/dist/core/runtime/stop-guard.js +6 -1
- package/dist/core/runtime/tmux-chrome.d.ts +1 -1
- package/dist/core/runtime/tmux-chrome.js +1 -1
- package/dist/core/runtime/tmux.d.ts +28 -0
- package/dist/core/runtime/tmux.js +80 -6
- package/dist/core/scope.d.ts +11 -0
- package/dist/core/scope.js +39 -0
- package/dist/core/spawn.d.ts +25 -1
- package/dist/core/spawn.js +72 -7
- package/dist/core/substrate/gate.d.ts +13 -0
- package/dist/core/substrate/gate.js +21 -0
- package/dist/core/substrate/index.d.ts +7 -0
- package/dist/core/substrate/index.js +18 -0
- package/dist/core/substrate/on-read.d.ts +14 -0
- package/dist/core/substrate/on-read.js +292 -0
- package/dist/core/substrate/render.d.ts +25 -0
- package/dist/core/substrate/render.js +256 -0
- package/dist/core/substrate/schema.d.ts +76 -0
- package/dist/core/substrate/schema.js +124 -0
- package/dist/core/substrate/session-cache.d.ts +30 -0
- package/dist/core/substrate/session-cache.js +77 -0
- package/dist/core/substrate/subject.d.ts +41 -0
- package/dist/core/substrate/subject.js +54 -0
- package/dist/core/tui/contract.d.ts +83 -0
- package/dist/core/tui/contract.js +8 -0
- package/dist/core/tui/draw.d.ts +96 -0
- package/dist/core/tui/draw.js +339 -0
- package/dist/core/tui/host.d.ts +29 -0
- package/dist/core/tui/host.js +379 -0
- package/dist/core/tui/loader.d.ts +16 -0
- package/dist/core/tui/loader.js +94 -0
- package/dist/core/{canvas/browse → tui}/terminal.js +7 -6
- package/dist/core/wake.d.ts +86 -0
- package/dist/core/wake.js +308 -0
- package/dist/daemon/crtrd.d.ts +29 -4
- package/dist/daemon/crtrd.js +662 -46
- package/dist/pi-extensions/__tests__/canvas-context-intro.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +171 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +19 -12
- package/dist/pi-extensions/canvas-commands.d.ts +3 -0
- package/dist/pi-extensions/canvas-commands.js +10 -0
- package/dist/pi-extensions/canvas-context-intro.d.ts +17 -0
- package/dist/pi-extensions/canvas-context-intro.js +55 -15
- package/dist/pi-extensions/canvas-doc-substrate.d.ts +44 -0
- package/dist/pi-extensions/canvas-doc-substrate.js +112 -0
- package/dist/pi-extensions/canvas-nav.d.ts +3 -0
- package/dist/pi-extensions/canvas-nav.js +145 -34
- package/dist/pi-extensions/canvas-stophook.js +17 -8
- package/dist/pi-extensions/canvas-view.d.ts +21 -0
- package/dist/pi-extensions/canvas-view.js +75 -0
- package/dist/prompts/skill.js +19 -26
- package/dist/prompts/view.d.ts +7 -0
- package/dist/prompts/view.js +101 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.js +1 -0
- package/package.json +9 -4
- package/dist/commands/skill/find.d.ts +0 -4
- package/dist/commands/skill/find.js +0 -257
- package/dist/commands/skill/read.js +0 -91
- /package/dist/commands/{skill → memory}/read.d.ts +0 -0
- /package/dist/core/{canvas/browse → tui}/terminal.d.ts +0 -0
|
@@ -11,7 +11,7 @@ import { execFileSync } from 'node:child_process';
|
|
|
11
11
|
import { resolve } from 'node:path';
|
|
12
12
|
import { dashboardRowsAll, renderForest } from '../render.js';
|
|
13
13
|
import { listNodes, subscriptionsOf } from '../canvas.js';
|
|
14
|
-
import { setupTerminal, restoreTerminal, getTerminalSize, parseKeypress, } from '
|
|
14
|
+
import { setupTerminal, restoreTerminal, getTerminalSize, parseKeypress, } from '../../tui/terminal.js';
|
|
15
15
|
import { buildTree, flatten, TABS, SORTS } from './model.js';
|
|
16
16
|
import { renderFrame, detectColorCaps, headerHeight, PREVIEW_HEIGHT } from './render.js';
|
|
17
17
|
/** Viewport (body) height = total rows minus the header renderFrame draws (see
|
|
@@ -52,6 +52,7 @@ export async function runBrowse(opts = {}) {
|
|
|
52
52
|
cwdScope: launchCwd, // default: this dir
|
|
53
53
|
sort: 'tree',
|
|
54
54
|
preview: true, // default ON (decision)
|
|
55
|
+
residentsOnly: true, // default ON: hide one-shot workers (decision)
|
|
55
56
|
};
|
|
56
57
|
let visible = [];
|
|
57
58
|
// Color capability is fixed for the session (it's a property of the tty/env).
|
|
@@ -89,6 +90,7 @@ export async function runBrowse(opts = {}) {
|
|
|
89
90
|
tab: state.tab,
|
|
90
91
|
query: state.query,
|
|
91
92
|
cwdScope: state.cwdScope,
|
|
93
|
+
residentsOnly: state.residentsOnly,
|
|
92
94
|
sort: state.sort,
|
|
93
95
|
});
|
|
94
96
|
if (keepId !== undefined) {
|
|
@@ -116,6 +118,7 @@ export async function runBrowse(opts = {}) {
|
|
|
116
118
|
tree, visible, tab: state.tab, cursor: state.cursor, scrollOffset: state.scrollOffset,
|
|
117
119
|
query: state.query, search: state.search, totalNodes,
|
|
118
120
|
cwdScope: state.cwdScope, sort: state.sort, preview: state.preview,
|
|
121
|
+
residentsOnly: state.residentsOnly,
|
|
119
122
|
}, size, caps);
|
|
120
123
|
process.stdout.write(frame);
|
|
121
124
|
};
|
|
@@ -146,6 +149,13 @@ export async function runBrowse(opts = {}) {
|
|
|
146
149
|
state.cwdScope = state.cwdScope === null ? launchCwd : null;
|
|
147
150
|
recompute(keep);
|
|
148
151
|
};
|
|
152
|
+
// Toggle the resident-only lifecycle filter (hide/show one-shot worker nodes),
|
|
153
|
+
// keeping the selected node put when it survives the toggle.
|
|
154
|
+
const toggleResidents = () => {
|
|
155
|
+
const keep = curRow()?.id;
|
|
156
|
+
state.residentsOnly = !state.residentsOnly;
|
|
157
|
+
recompute(keep);
|
|
158
|
+
};
|
|
149
159
|
const onKeySearch = (input, key) => {
|
|
150
160
|
if (key.escape) {
|
|
151
161
|
// Cancel the search: drop the query AND the relevance ranking it switched
|
|
@@ -284,7 +294,7 @@ export async function runBrowse(opts = {}) {
|
|
|
284
294
|
flush();
|
|
285
295
|
return;
|
|
286
296
|
}
|
|
287
|
-
// Sort / scope / preview.
|
|
297
|
+
// Sort / scope / residents / preview.
|
|
288
298
|
if (input === 's') {
|
|
289
299
|
cycleSort();
|
|
290
300
|
flush();
|
|
@@ -295,6 +305,11 @@ export async function runBrowse(opts = {}) {
|
|
|
295
305
|
flush();
|
|
296
306
|
return;
|
|
297
307
|
}
|
|
308
|
+
if (input === 'r') {
|
|
309
|
+
toggleResidents();
|
|
310
|
+
flush();
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
298
313
|
if (input === 'p') {
|
|
299
314
|
state.preview = !state.preview;
|
|
300
315
|
flush();
|
|
@@ -322,6 +337,13 @@ export async function runBrowse(opts = {}) {
|
|
|
322
337
|
// Boot. If the launch dir holds NO nodes, the default this-dir scope would show
|
|
323
338
|
// a blank canvas — fall back to All dirs so browse is never empty on open.
|
|
324
339
|
recompute();
|
|
340
|
+
// Relax the resident-only filter first (you're in this dir for a reason), then
|
|
341
|
+
// the cwd scope — so browse is never blank on open even if the launch dir holds
|
|
342
|
+
// only one-shot workers, or only nodes from other dirs.
|
|
343
|
+
if (visible.length === 0 && state.residentsOnly) {
|
|
344
|
+
state.residentsOnly = false;
|
|
345
|
+
recompute();
|
|
346
|
+
}
|
|
325
347
|
if (visible.length === 0 && state.cwdScope !== null) {
|
|
326
348
|
state.cwdScope = null;
|
|
327
349
|
recompute();
|
|
@@ -50,9 +50,36 @@ export declare function fuzzyMatch(query: string, text: string): boolean;
|
|
|
50
50
|
* the renderer can highlight them. Empty set when `query` is empty OR does not
|
|
51
51
|
* fully match (no partial highlights). */
|
|
52
52
|
export declare function matchIndices(query: string, text: string): Set<number>;
|
|
53
|
+
/** One word-wrapped preview line: its text + the column indices WITHIN that text
|
|
54
|
+
* to highlight (the query match). */
|
|
55
|
+
export interface SnippetLine {
|
|
56
|
+
text: string;
|
|
57
|
+
hi: Set<number>;
|
|
58
|
+
}
|
|
59
|
+
/** Highlight indices for the preview: the literal case-insensitive SUBSTRING span
|
|
60
|
+
* when present (so "where does this text appear?" is answered exactly), else the
|
|
61
|
+
* scattered subsequence indices, else empty (empty query). This is a different,
|
|
62
|
+
* stricter model than the subsequence super-search on purpose — a contiguous span
|
|
63
|
+
* is what reads as a highlight. */
|
|
64
|
+
export declare function highlightIndices(query: string, text: string): Set<number>;
|
|
65
|
+
/**
|
|
66
|
+
* Build the preview snippet for `text` under the live `query`: up to `maxLines`
|
|
67
|
+
* word-wrapped lines (each ≤ `width` cols), WINDOWED so the best match is visible
|
|
68
|
+
* (long conversations: the match can be thousands of chars in), with the matched
|
|
69
|
+
* columns flagged for highlight. Empty query (or empty text) → a plain wrap from
|
|
70
|
+
* the start with no highlight. The snippet string itself is what carries the
|
|
71
|
+
* highlight indices, so they never drift across the windowing.
|
|
72
|
+
*/
|
|
73
|
+
export declare function previewSnippet(query: string, text: string, width: number, maxLines: number): SnippetLine[];
|
|
74
|
+
/** The searchable conversation text for a row: EVERY user prompt across the pi
|
|
75
|
+
* session (`prompts`) when present, else the spawn prompt (`goal`) for a
|
|
76
|
+
* never-revived node that has no session yet. Searched by super-search and
|
|
77
|
+
* windowed in the preview, so search matches a prompt from ANYWHERE in the
|
|
78
|
+
* conversation, not just the first one. */
|
|
79
|
+
export declare function promptText(row: DashboardRow): string;
|
|
53
80
|
/** Does this row match the live query? Super-search spans name (which already
|
|
54
|
-
* folds in the pi-generated description), kind, short-id, AND
|
|
55
|
-
* (`
|
|
81
|
+
* folds in the pi-generated description), kind, short-id, AND every user prompt
|
|
82
|
+
* in the conversation (`promptText`). Empty query matches everything. */
|
|
56
83
|
export declare function queryMatch(query: string, row: DashboardRow): boolean;
|
|
57
84
|
/** Score how well `query` matches one field, 0 (no match) → 1 (exact). Tiers:
|
|
58
85
|
* exact > prefix > word-boundary substring > interior substring > subsequence.
|
|
@@ -76,12 +103,21 @@ export interface FlattenOpts {
|
|
|
76
103
|
* undefined = All dirs (no cwd filter). Like the tab predicate, it gates the
|
|
77
104
|
* matched set — ancestors from other dirs still render dimmed for tree context. */
|
|
78
105
|
cwdScope?: string | null;
|
|
106
|
+
/** Lifecycle filter: when true, hide `terminal` (one-shot worker) nodes so only
|
|
107
|
+
* the persistent `resident` agents you come back to show. The resume picker
|
|
108
|
+
* defaults this ON. Undefined/false = every lifecycle. Gates the matched set
|
|
109
|
+
* like the tab + cwd predicates (ancestors of a match still render for context). */
|
|
110
|
+
residentsOnly?: boolean;
|
|
79
111
|
/** Ordering. `tree` keeps the spanning tree + ancestor context; `relevance` /
|
|
80
112
|
* `recency` produce a FLAT ranked list of directly-matched rows. */
|
|
81
113
|
sort?: SortMode;
|
|
82
114
|
}
|
|
83
115
|
/** Is this row inside the active cwd scope? No scope (null/undefined) = All dirs. */
|
|
84
116
|
export declare function cwdMatch(scope: string | null | undefined, row: DashboardRow): boolean;
|
|
117
|
+
/** Is this row inside the active lifecycle filter? `residentsOnly` hides `terminal`
|
|
118
|
+
* nodes; off (false/undefined) = every lifecycle. A row with an unknown lifecycle
|
|
119
|
+
* (older snapshot field absent) is treated as resident so it is never hidden. */
|
|
120
|
+
export declare function lifecycleMatch(residentsOnly: boolean | undefined, row: DashboardRow): boolean;
|
|
85
121
|
/**
|
|
86
122
|
* Flatten the tree to the ordered list of currently-visible rows.
|
|
87
123
|
*
|
|
@@ -124,19 +124,133 @@ export function matchIndices(query, text) {
|
|
|
124
124
|
out.clear(); // no full match → no highlight
|
|
125
125
|
return out;
|
|
126
126
|
}
|
|
127
|
+
/** Chars of leading context kept before the match start when windowing. */
|
|
128
|
+
const SNIPPET_LEAD = 12;
|
|
129
|
+
/** Highlight indices for the preview: the literal case-insensitive SUBSTRING span
|
|
130
|
+
* when present (so "where does this text appear?" is answered exactly), else the
|
|
131
|
+
* scattered subsequence indices, else empty (empty query). This is a different,
|
|
132
|
+
* stricter model than the subsequence super-search on purpose — a contiguous span
|
|
133
|
+
* is what reads as a highlight. */
|
|
134
|
+
export function highlightIndices(query, text) {
|
|
135
|
+
if (query === '')
|
|
136
|
+
return new Set();
|
|
137
|
+
const lower = text.toLowerCase();
|
|
138
|
+
const q = query.toLowerCase();
|
|
139
|
+
const sub = lower.indexOf(q);
|
|
140
|
+
if (sub >= 0) {
|
|
141
|
+
const out = new Set();
|
|
142
|
+
for (let i = sub; i < sub + q.length; i++)
|
|
143
|
+
out.add(i);
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
return matchIndices(query, text);
|
|
147
|
+
}
|
|
148
|
+
/** Greedy word-wrap of single-spaced `s` (caller normalizes whitespace) to `width`
|
|
149
|
+
* cols × `maxLines` lines, returning each line's text AND its start offset in `s`.
|
|
150
|
+
* Only the single space at each wrap break is dropped, so a highlight index maps
|
|
151
|
+
* to a line column as `idx - line.start` with no drift across the wrap. */
|
|
152
|
+
function wrapTracked(s, width, maxLines) {
|
|
153
|
+
const out = [];
|
|
154
|
+
if (width <= 0 || maxLines <= 0)
|
|
155
|
+
return out;
|
|
156
|
+
const n = s.length;
|
|
157
|
+
let i = 0;
|
|
158
|
+
while (i < n && out.length < maxLines) {
|
|
159
|
+
const lineStart = i;
|
|
160
|
+
let lineEnd = i; // exclusive end of committed words
|
|
161
|
+
let j = i;
|
|
162
|
+
while (j < n) {
|
|
163
|
+
let ws = j;
|
|
164
|
+
while (ws < n && s[ws] === ' ')
|
|
165
|
+
ws++;
|
|
166
|
+
let we = ws;
|
|
167
|
+
while (we < n && s[we] !== ' ')
|
|
168
|
+
we++;
|
|
169
|
+
if (ws === we)
|
|
170
|
+
break; // only trailing spaces remain
|
|
171
|
+
if (we - lineStart <= width || lineEnd === lineStart) {
|
|
172
|
+
lineEnd = we; // include this word (always take ≥1; a huge word is clipped below)
|
|
173
|
+
j = we;
|
|
174
|
+
}
|
|
175
|
+
else
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
let text = s.slice(lineStart, lineEnd);
|
|
179
|
+
if (text.length > width)
|
|
180
|
+
text = text.slice(0, width);
|
|
181
|
+
out.push({ text, start: lineStart });
|
|
182
|
+
i = lineEnd;
|
|
183
|
+
if (i < n && s[i] === ' ')
|
|
184
|
+
i++; // drop the single break space
|
|
185
|
+
}
|
|
186
|
+
return out;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Build the preview snippet for `text` under the live `query`: up to `maxLines`
|
|
190
|
+
* word-wrapped lines (each ≤ `width` cols), WINDOWED so the best match is visible
|
|
191
|
+
* (long conversations: the match can be thousands of chars in), with the matched
|
|
192
|
+
* columns flagged for highlight. Empty query (or empty text) → a plain wrap from
|
|
193
|
+
* the start with no highlight. The snippet string itself is what carries the
|
|
194
|
+
* highlight indices, so they never drift across the windowing.
|
|
195
|
+
*/
|
|
196
|
+
export function previewSnippet(query, text, width, maxLines) {
|
|
197
|
+
const norm = text.replace(/\s+/g, ' ').trim();
|
|
198
|
+
if (norm === '' || width <= 0 || maxLines <= 0)
|
|
199
|
+
return [];
|
|
200
|
+
// 1. Locate the match in the normalized text + choose a window start that keeps
|
|
201
|
+
// a little leading context, snapped to a word boundary.
|
|
202
|
+
let matchStart = -1;
|
|
203
|
+
if (query !== '') {
|
|
204
|
+
const sub = norm.toLowerCase().indexOf(query.toLowerCase());
|
|
205
|
+
if (sub >= 0)
|
|
206
|
+
matchStart = sub;
|
|
207
|
+
else {
|
|
208
|
+
const ix = matchIndices(query, norm);
|
|
209
|
+
if (ix.size > 0)
|
|
210
|
+
matchStart = Math.min(...ix);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
let windowStart = 0;
|
|
214
|
+
if (matchStart > SNIPPET_LEAD) {
|
|
215
|
+
let s = matchStart - SNIPPET_LEAD;
|
|
216
|
+
while (s < matchStart && norm[s] !== ' ')
|
|
217
|
+
s++; // forward to the next word boundary
|
|
218
|
+
windowStart = s < matchStart ? s + 1 : matchStart;
|
|
219
|
+
}
|
|
220
|
+
const snippet = (windowStart > 0 ? '… ' : '') + norm.slice(windowStart);
|
|
221
|
+
// 2. Recompute highlight indices on the FINAL snippet string (no drift), then
|
|
222
|
+
// word-wrap with offset tracking and split the highlight into per-line cols.
|
|
223
|
+
const hi = highlightIndices(query, snippet);
|
|
224
|
+
return wrapTracked(snippet, width, maxLines).map(({ text: lt, start }) => {
|
|
225
|
+
const lhi = new Set();
|
|
226
|
+
for (const idx of hi)
|
|
227
|
+
if (idx >= start && idx < start + lt.length)
|
|
228
|
+
lhi.add(idx - start);
|
|
229
|
+
return { text: lt, hi: lhi };
|
|
230
|
+
});
|
|
231
|
+
}
|
|
127
232
|
function shortId(id) {
|
|
128
233
|
return id.slice(0, 8);
|
|
129
234
|
}
|
|
235
|
+
/** The searchable conversation text for a row: EVERY user prompt across the pi
|
|
236
|
+
* session (`prompts`) when present, else the spawn prompt (`goal`) for a
|
|
237
|
+
* never-revived node that has no session yet. Searched by super-search and
|
|
238
|
+
* windowed in the preview, so search matches a prompt from ANYWHERE in the
|
|
239
|
+
* conversation, not just the first one. */
|
|
240
|
+
export function promptText(row) {
|
|
241
|
+
return row.prompts ?? row.goal ?? '';
|
|
242
|
+
}
|
|
130
243
|
/** Does this row match the live query? Super-search spans name (which already
|
|
131
|
-
* folds in the pi-generated description), kind, short-id, AND
|
|
132
|
-
* (`
|
|
244
|
+
* folds in the pi-generated description), kind, short-id, AND every user prompt
|
|
245
|
+
* in the conversation (`promptText`). Empty query matches everything. */
|
|
133
246
|
export function queryMatch(query, row) {
|
|
134
247
|
if (query === '')
|
|
135
248
|
return true;
|
|
249
|
+
const prompt = promptText(row);
|
|
136
250
|
return (fuzzyMatch(query, row.name) ||
|
|
137
251
|
fuzzyMatch(query, row.kind) ||
|
|
138
252
|
fuzzyMatch(query, shortId(row.node_id)) ||
|
|
139
|
-
(
|
|
253
|
+
(prompt !== '' && fuzzyMatch(query, prompt)));
|
|
140
254
|
}
|
|
141
255
|
// ---------------------------------------------------------------------------
|
|
142
256
|
// Relevance scoring (super-search)
|
|
@@ -159,23 +273,30 @@ export function fieldScore(query, text) {
|
|
|
159
273
|
}
|
|
160
274
|
return fuzzyMatch(q, t) ? 0.2 : 0; // scattered subsequence
|
|
161
275
|
}
|
|
162
|
-
/** Per-field weights — name (handle + description) dominates, the
|
|
163
|
-
*
|
|
164
|
-
const FIELD_WEIGHTS = { name: 4, kind: 2, id: 1,
|
|
276
|
+
/** Per-field weights — name (handle + description) dominates, the conversation
|
|
277
|
+
* prompts are the long-tail super-search field. */
|
|
278
|
+
const FIELD_WEIGHTS = { name: 4, kind: 2, id: 1, prompt: 1.5 };
|
|
165
279
|
/** Weighted relevance of a row to the query across all searched fields. 0 means
|
|
166
280
|
* no field matched (excluded from relevance results, same as `queryMatch`). */
|
|
167
281
|
export function scoreRow(query, row) {
|
|
168
282
|
if (query === '')
|
|
169
283
|
return 0;
|
|
284
|
+
const prompt = promptText(row);
|
|
170
285
|
return (FIELD_WEIGHTS.name * fieldScore(query, row.name) +
|
|
171
286
|
FIELD_WEIGHTS.kind * fieldScore(query, row.kind) +
|
|
172
287
|
FIELD_WEIGHTS.id * fieldScore(query, shortId(row.node_id)) +
|
|
173
|
-
FIELD_WEIGHTS.
|
|
288
|
+
FIELD_WEIGHTS.prompt * (prompt !== '' ? fieldScore(query, prompt) : 0));
|
|
174
289
|
}
|
|
175
290
|
/** Is this row inside the active cwd scope? No scope (null/undefined) = All dirs. */
|
|
176
291
|
export function cwdMatch(scope, row) {
|
|
177
292
|
return scope === null || scope === undefined || row.cwd === scope;
|
|
178
293
|
}
|
|
294
|
+
/** Is this row inside the active lifecycle filter? `residentsOnly` hides `terminal`
|
|
295
|
+
* nodes; off (false/undefined) = every lifecycle. A row with an unknown lifecycle
|
|
296
|
+
* (older snapshot field absent) is treated as resident so it is never hidden. */
|
|
297
|
+
export function lifecycleMatch(residentsOnly, row) {
|
|
298
|
+
return residentsOnly !== true || row.lifecycle !== 'terminal';
|
|
299
|
+
}
|
|
179
300
|
/**
|
|
180
301
|
* Flatten the tree to the ordered list of currently-visible rows.
|
|
181
302
|
*
|
|
@@ -189,11 +310,14 @@ export function cwdMatch(scope, row) {
|
|
|
189
310
|
* always reachable.
|
|
190
311
|
*/
|
|
191
312
|
export function flatten(tree, opts) {
|
|
192
|
-
const { collapsed, tab, query, cwdScope, sort = 'tree' } = opts;
|
|
193
|
-
// 1. Directly-matched nodes: tab predicate AND cwd scope AND query.
|
|
313
|
+
const { collapsed, tab, query, cwdScope, residentsOnly, sort = 'tree' } = opts;
|
|
314
|
+
// 1. Directly-matched nodes: tab predicate AND cwd scope AND lifecycle AND query.
|
|
194
315
|
const matched = new Set();
|
|
195
316
|
for (const [id, node] of tree.nodes) {
|
|
196
|
-
if (tabPredicate(tab, node.row) &&
|
|
317
|
+
if (tabPredicate(tab, node.row) &&
|
|
318
|
+
cwdMatch(cwdScope, node.row) &&
|
|
319
|
+
lifecycleMatch(residentsOnly, node.row) &&
|
|
320
|
+
queryMatch(query, node.row)) {
|
|
197
321
|
matched.add(id);
|
|
198
322
|
}
|
|
199
323
|
}
|
|
@@ -1,19 +1,10 @@
|
|
|
1
1
|
import type { Tab, Tree, VisibleRow, SortMode } from './model.js';
|
|
2
|
+
import { detectColorCaps, type ColorCaps } from '../../tui/draw.js';
|
|
3
|
+
export { detectColorCaps };
|
|
4
|
+
export type { ColorCaps };
|
|
2
5
|
export declare const PREVIEW_BODY = 5;
|
|
3
6
|
export declare const PREVIEW_HEIGHT: number;
|
|
4
7
|
export declare function headerHeight(search: boolean): number;
|
|
5
|
-
export interface ColorCaps {
|
|
6
|
-
/** Any hue (fg/bg color) allowed. */
|
|
7
|
-
color: boolean;
|
|
8
|
-
/** 256-color bg allowed — drives the subtle cursor-row background. */
|
|
9
|
-
color256: boolean;
|
|
10
|
-
}
|
|
11
|
-
/** Detect color capability. Honors `NO_COLOR` and `TERM=dumb`, and only emits
|
|
12
|
-
* hue when stdout is a TTY. `color256` additionally requires a 256/truecolor
|
|
13
|
-
* terminal (for the cursor-row background; otherwise we fall back to reverse). */
|
|
14
|
-
export declare function detectColorCaps(stream?: {
|
|
15
|
-
isTTY?: boolean;
|
|
16
|
-
}, env?: NodeJS.ProcessEnv): ColorCaps;
|
|
17
8
|
export interface RenderState {
|
|
18
9
|
tree: Tree;
|
|
19
10
|
visible: VisibleRow[];
|
|
@@ -29,6 +20,9 @@ export interface RenderState {
|
|
|
29
20
|
sort: SortMode;
|
|
30
21
|
/** Whether the bottom preview panel is drawn. */
|
|
31
22
|
preview: boolean;
|
|
23
|
+
/** Lifecycle filter: when true, `terminal` (one-shot worker) nodes are hidden
|
|
24
|
+
* — surfaced as a status-line cue. */
|
|
25
|
+
residentsOnly: boolean;
|
|
32
26
|
}
|
|
33
27
|
/**
|
|
34
28
|
* Render the whole frame. Returns a single string that, written as-is, repaints
|
|
@@ -11,7 +11,12 @@
|
|
|
11
11
|
// Every hue (fg / bg color) is gated on `caps.color`; structural SGR (bold/dim/
|
|
12
12
|
// reverse) is allowed always. See detectColorCaps() for the gate, and the
|
|
13
13
|
// canvas-browse color spec for the rationale + palette.
|
|
14
|
-
import { TABS, matchIndices } from './model.js';
|
|
14
|
+
import { TABS, matchIndices, promptText, previewSnippet } from './model.js';
|
|
15
|
+
// Span/color primitives live in core/tui/draw.ts (one copy, shared with the
|
|
16
|
+
// `crtr view` host). Re-export the color caps so browse's importers + tests keep
|
|
17
|
+
// resolving them from this module.
|
|
18
|
+
import { clip, assemble, detectColorCaps } from '../../tui/draw.js';
|
|
19
|
+
export { detectColorCaps };
|
|
15
20
|
// Fixed chrome heights, shared with app.ts so its viewport math never drifts
|
|
16
21
|
// from what renderFrame actually draws.
|
|
17
22
|
// header = title + tab bar + status line + separator (+ search input when searching)
|
|
@@ -52,16 +57,6 @@ const STATUS_COLOR = {
|
|
|
52
57
|
dead: FG_RED,
|
|
53
58
|
canceled: FG_GRAY,
|
|
54
59
|
};
|
|
55
|
-
/** Detect color capability. Honors `NO_COLOR` and `TERM=dumb`, and only emits
|
|
56
|
-
* hue when stdout is a TTY. `color256` additionally requires a 256/truecolor
|
|
57
|
-
* terminal (for the cursor-row background; otherwise we fall back to reverse). */
|
|
58
|
-
export function detectColorCaps(stream = process.stdout, env = process.env) {
|
|
59
|
-
const term = env['TERM'] ?? '';
|
|
60
|
-
const color = stream.isTTY === true && !env['NO_COLOR'] && term !== 'dumb';
|
|
61
|
-
const colorTerm = env['COLORTERM'] ?? '';
|
|
62
|
-
const color256 = color && (/256|direct/i.test(term) || /truecolor|24bit/i.test(colorTerm));
|
|
63
|
-
return { color, color256 };
|
|
64
|
-
}
|
|
65
60
|
function fmtCtx(tokens) {
|
|
66
61
|
if (tokens <= 0)
|
|
67
62
|
return '0k';
|
|
@@ -76,12 +71,6 @@ function ctxColorCode(tokens) {
|
|
|
76
71
|
return FG_YELLOW;
|
|
77
72
|
return undefined;
|
|
78
73
|
}
|
|
79
|
-
/** Truncate to `max` visible cols (plain text, no ANSI). */
|
|
80
|
-
function clip(text, max) {
|
|
81
|
-
if (max <= 0)
|
|
82
|
-
return '';
|
|
83
|
-
return text.length <= max ? text : text.slice(0, Math.max(0, max - 1)) + '…';
|
|
84
|
-
}
|
|
85
74
|
/** Compact relative age, e.g. `45s` `12m` `3h` `5d` `2w` `4mo`. Empty on a bad
|
|
86
75
|
* timestamp. Drives the per-row recency cue + the preview meta line. */
|
|
87
76
|
function relAge(created, now) {
|
|
@@ -110,74 +99,13 @@ function baseDir(cwd) {
|
|
|
110
99
|
const parts = cwd.replace(/\/+$/, '').split('/');
|
|
111
100
|
return parts[parts.length - 1] || cwd;
|
|
112
101
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
for (const w of words) {
|
|
121
|
-
const next = cur === '' ? w : `${cur} ${w}`;
|
|
122
|
-
if (next.length <= width) {
|
|
123
|
-
cur = next;
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
if (cur !== '')
|
|
127
|
-
out.push(cur);
|
|
128
|
-
cur = w;
|
|
129
|
-
if (out.length >= maxLines)
|
|
130
|
-
break;
|
|
131
|
-
}
|
|
132
|
-
if (out.length < maxLines && cur !== '')
|
|
133
|
-
out.push(cur);
|
|
134
|
-
return out.slice(0, maxLines).map((l) => clip(l, width));
|
|
135
|
-
}
|
|
136
|
-
/** Style one span. Hue is gated on `color`; bold/dim are not. After the span we
|
|
137
|
-
* return to `lineBase` (not a bare reset) so a row-level background/dim persists
|
|
138
|
-
* across the span instead of bleeding or being cleared. */
|
|
139
|
-
function styleSpan(text, span, color, lineBase) {
|
|
140
|
-
if (text === '')
|
|
141
|
-
return '';
|
|
142
|
-
let pre = '';
|
|
143
|
-
if (span.dim)
|
|
144
|
-
pre += DIM;
|
|
145
|
-
if (span.bold)
|
|
146
|
-
pre += BOLD;
|
|
147
|
-
if (color && span.fg)
|
|
148
|
-
pre += `${ESC}${span.fg}m`;
|
|
149
|
-
if (pre === '')
|
|
150
|
-
return text; // inherits lineBase / default
|
|
151
|
-
return `${pre}${text}${RESET}${lineBase}`;
|
|
152
|
-
}
|
|
153
|
-
/** Assemble styled spans into one line clipped to `width` visible cols. When
|
|
154
|
-
* `fill`, pad the remainder with spaces (under `lineBase`) so a cursor-row
|
|
155
|
-
* background spans the full width. Always RESET-terminated so no color bleeds
|
|
156
|
-
* into the next line. */
|
|
157
|
-
function assemble(spans, width, color, lineBase, fill) {
|
|
158
|
-
let used = 0;
|
|
159
|
-
let body = '';
|
|
160
|
-
for (const span of spans) {
|
|
161
|
-
if (used >= width)
|
|
162
|
-
break;
|
|
163
|
-
if (span.text === '')
|
|
164
|
-
continue;
|
|
165
|
-
let t = span.text;
|
|
166
|
-
const remaining = width - used;
|
|
167
|
-
let cut = false;
|
|
168
|
-
if (t.length > remaining) {
|
|
169
|
-
t = t.slice(0, Math.max(0, remaining - 1)) + '…';
|
|
170
|
-
cut = true;
|
|
171
|
-
}
|
|
172
|
-
body += styleSpan(t, span, color, lineBase);
|
|
173
|
-
used += t.length;
|
|
174
|
-
if (cut)
|
|
175
|
-
break;
|
|
176
|
-
}
|
|
177
|
-
if (fill && used < width)
|
|
178
|
-
body += ' '.repeat(width - used);
|
|
179
|
-
return lineBase === '' ? body : `${lineBase}${body}${RESET}`;
|
|
180
|
-
}
|
|
102
|
+
// ── Styled spans ──────────────────────────────────────────────────────────────
|
|
103
|
+
//
|
|
104
|
+
// A row is built from styled spans, then assembled to a width-clipped line. This
|
|
105
|
+
// lets each cell carry its own hue (status glyph, ctx tier, asks, match highlight)
|
|
106
|
+
// while clipping by VISIBLE width — ANSI bytes don't count toward the column
|
|
107
|
+
// budget. `fg` is hue (gated on `color`); `bold`/`dim` are structural (always).
|
|
108
|
+
// Span / styleSpan / assemble now live in core/tui/draw.ts (imported above).
|
|
181
109
|
/** Status tallies across the whole canvas, for the right-aligned header. Memoized
|
|
182
110
|
* per tree — the snapshot is immutable for a browse session, so counting is done
|
|
183
111
|
* once, not O(N) on every keystroke (the "massive canvas" target). */
|
|
@@ -213,7 +141,7 @@ const EMPTY_HI = new Set();
|
|
|
213
141
|
function nameSpans(name, query, style) {
|
|
214
142
|
const hi = query === '' ? EMPTY_HI : matchIndices(query, name);
|
|
215
143
|
if (hi.size === 0)
|
|
216
|
-
return [{ text: name, dim: style.dim, bold: style.bold }];
|
|
144
|
+
return [{ text: name, style: { dim: style.dim, bold: style.bold } }];
|
|
217
145
|
const out = [];
|
|
218
146
|
let buf = '';
|
|
219
147
|
let bufHi = false;
|
|
@@ -221,9 +149,9 @@ function nameSpans(name, query, style) {
|
|
|
221
149
|
if (buf === '')
|
|
222
150
|
return;
|
|
223
151
|
if (bufHi)
|
|
224
|
-
out.push({ text: buf, fg: FG_BRIGHT_CYAN, bold: true });
|
|
152
|
+
out.push({ text: buf, style: { fg: FG_BRIGHT_CYAN, bold: true } });
|
|
225
153
|
else
|
|
226
|
-
out.push({ text: buf, dim: style.dim, bold: style.bold });
|
|
154
|
+
out.push({ text: buf, style: { dim: style.dim, bold: style.bold } });
|
|
227
155
|
buf = '';
|
|
228
156
|
};
|
|
229
157
|
for (let i = 0; i < name.length; i++) {
|
|
@@ -255,19 +183,19 @@ function rowLine(row, tree, width, isCursor, query, caps, showCwd, now) {
|
|
|
255
183
|
const nameStyle = { dim: !isCursor && terminal, bold: isCursor };
|
|
256
184
|
const spans = [
|
|
257
185
|
{ text: `${indent}${collapse} ` },
|
|
258
|
-
{ text: glyph, fg: STATUS_COLOR[r.status] }, // load-bearing status hue
|
|
186
|
+
{ text: glyph, style: { fg: STATUS_COLOR[r.status] } }, // load-bearing status hue
|
|
259
187
|
{ text: ' ' },
|
|
260
188
|
...nameSpans(r.name, query, nameStyle),
|
|
261
|
-
{ text: ` [${r.kind}/${r.mode}]`, fg: FG_GRAY }, // recedes
|
|
262
|
-
{ text: ' ctx ', dim: true },
|
|
263
|
-
{ text: ctxStr, fg: ctxFg, dim: ctxFg === undefined }, // tiered budget cue
|
|
189
|
+
{ text: ` [${r.kind}/${r.mode}]`, style: { fg: FG_GRAY } }, // recedes
|
|
190
|
+
{ text: ' ctx ', style: { dim: true } },
|
|
191
|
+
{ text: ctxStr, style: { fg: ctxFg, dim: ctxFg === undefined } }, // tiered budget cue
|
|
264
192
|
];
|
|
265
193
|
if (age !== '')
|
|
266
|
-
spans.push({ text: ` ${age}`, dim: true }); // recency cue
|
|
194
|
+
spans.push({ text: ` ${age}`, style: { dim: true } }); // recency cue
|
|
267
195
|
if (showCwd)
|
|
268
|
-
spans.push({ text: ` ~${baseDir(r.cwd)}`, fg: FG_GRAY }); // project cue (All dirs)
|
|
196
|
+
spans.push({ text: ` ~${baseDir(r.cwd)}`, style: { fg: FG_GRAY } }); // project cue (All dirs)
|
|
269
197
|
if (r.asks > 0)
|
|
270
|
-
spans.push({ text: ` ⚑${r.asks}`, fg: FG_BRIGHT_YELLOW, bold: true }); // attention
|
|
198
|
+
spans.push({ text: ` ⚑${r.asks}`, style: { fg: FG_BRIGHT_YELLOW, bold: true } }); // attention
|
|
271
199
|
// Row base: cursor → subtle bg (256) or reverse fallback (also covers !color);
|
|
272
200
|
// non-matched ancestor → whole-row dim for tree context (keep prior behavior).
|
|
273
201
|
let lineBase = '';
|
|
@@ -288,15 +216,44 @@ function statusLine(state) {
|
|
|
288
216
|
const dim = (s) => `${DIM}${s}${RESET}`;
|
|
289
217
|
const scope = state.cwdScope === null ? 'all dirs' : baseDir(state.cwdScope);
|
|
290
218
|
const segs = [`${dim('scope')} ${scope}`, `${dim('sort')} ${state.sort}`];
|
|
219
|
+
if (state.residentsOnly)
|
|
220
|
+
segs.push(`${dim('show')} residents`);
|
|
291
221
|
if (!state.search && state.query !== '')
|
|
292
222
|
segs.push(`${dim('filter')} ${state.query}`);
|
|
293
223
|
return segs.join(dim(' · '));
|
|
294
224
|
}
|
|
225
|
+
/** One snippet line → a rendered string with the query-matched columns in bold
|
|
226
|
+
* bright-cyan (mirrors the row name highlight). Unmatched text stays default. */
|
|
227
|
+
function snippetLine(ln, width, caps) {
|
|
228
|
+
if (ln.hi.size === 0)
|
|
229
|
+
return assemble([{ text: ln.text }], width, caps.color, '', false);
|
|
230
|
+
const spans = [];
|
|
231
|
+
let buf = '';
|
|
232
|
+
let bufHi = false;
|
|
233
|
+
const flush = () => {
|
|
234
|
+
if (buf === '')
|
|
235
|
+
return;
|
|
236
|
+
spans.push(bufHi ? { text: buf, style: { fg: FG_BRIGHT_CYAN, bold: true } } : { text: buf });
|
|
237
|
+
buf = '';
|
|
238
|
+
};
|
|
239
|
+
for (let i = 0; i < ln.text.length; i++) {
|
|
240
|
+
const h = ln.hi.has(i);
|
|
241
|
+
if (h !== bufHi) {
|
|
242
|
+
flush();
|
|
243
|
+
bufHi = h;
|
|
244
|
+
}
|
|
245
|
+
buf += ln.text[i];
|
|
246
|
+
}
|
|
247
|
+
flush();
|
|
248
|
+
return assemble(spans, width, caps.color, '', false);
|
|
249
|
+
}
|
|
295
250
|
/** The bottom preview panel — exactly PREVIEW_HEIGHT lines: a separator, a meta
|
|
296
|
-
* line (status · kind/mode · project · age · ctx · asks), then the selected
|
|
297
|
-
*
|
|
251
|
+
* line (status · kind/mode · project · age · ctx · asks), then the selected node's
|
|
252
|
+
* prompt wrapped to PREVIEW_BODY lines. Under a live query the body is WINDOWED to
|
|
253
|
+
* the matching prompt (anywhere in the conversation) with the match highlighted;
|
|
254
|
+
* with no query it shows the spawn prompt from the start. The "which one was this?"
|
|
298
255
|
* answer — paired with super-search. Always full height so viewport math holds. */
|
|
299
|
-
function previewPanel(r, width, caps, now) {
|
|
256
|
+
function previewPanel(r, width, caps, now, query) {
|
|
300
257
|
const out = [`${DIM}${'─'.repeat(width)}${RESET}`];
|
|
301
258
|
if (r === undefined) {
|
|
302
259
|
while (out.length < PREVIEW_HEIGHT)
|
|
@@ -311,10 +268,21 @@ function previewPanel(r, width, caps, now) {
|
|
|
311
268
|
metaPieces.push(`⚑${r.asks}`);
|
|
312
269
|
const metaText = clip(metaPieces.filter((p) => p !== '').join(' · '), Math.max(0, width - 2));
|
|
313
270
|
out.push(caps.color ? `${glyph} ${DIM}${metaText}${RESET}` : `${glyph} ${metaText}`);
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
271
|
+
// With a query, window+highlight the matching prompt from the WHOLE conversation;
|
|
272
|
+
// otherwise show the spawn prompt from the start.
|
|
273
|
+
const sourceText = query !== '' ? promptText(r) : (r.goal ?? '');
|
|
274
|
+
const snippet = previewSnippet(query, sourceText, width, PREVIEW_BODY);
|
|
275
|
+
if (snippet.length === 0) {
|
|
276
|
+
out.push(`${DIM}(no spawn prompt)${RESET}`);
|
|
277
|
+
for (let i = 1; i < PREVIEW_BODY; i++)
|
|
278
|
+
out.push('');
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
for (let i = 0; i < PREVIEW_BODY; i++) {
|
|
282
|
+
const ln = snippet[i];
|
|
283
|
+
out.push(ln === undefined ? '' : snippetLine(ln, width, caps));
|
|
284
|
+
}
|
|
285
|
+
}
|
|
318
286
|
return out;
|
|
319
287
|
}
|
|
320
288
|
/**
|
|
@@ -373,13 +341,13 @@ export function renderFrame(state, size, caps = { color: false, color256: false
|
|
|
373
341
|
if (previewOn) {
|
|
374
342
|
const sel = state.visible[state.cursor];
|
|
375
343
|
const r = sel !== undefined ? state.tree.nodes.get(sel.id)?.row : undefined;
|
|
376
|
-
for (const l of previewPanel(r, width, caps, now))
|
|
344
|
+
for (const l of previewPanel(r, width, caps, now, state.query))
|
|
377
345
|
lines.push(l);
|
|
378
346
|
}
|
|
379
347
|
// footer.
|
|
380
348
|
const footer = state.search
|
|
381
349
|
? '⏎ commit Esc cancel ⌫ delete'
|
|
382
|
-
: '↑↓ move →/← tree ⏎ resume Tab tabs / search s sort c cwd p preview q quit';
|
|
350
|
+
: '↑↓ move →/← tree ⏎ resume Tab tabs / search s sort c cwd r residents p preview q quit';
|
|
383
351
|
lines.push(`${DIM}${clip(footer, width)}${RESET}`);
|
|
384
352
|
// Assemble: home, each line cleared to EOL, then clear below.
|
|
385
353
|
const body = lines.map((l) => `${l}${ESC}K`).join('\r\n');
|