@crouton-kit/crouter 0.3.68 → 0.3.78
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/README.md +1 -1
- package/dist/builtin-memory/00-runtime-base.md +3 -10
- package/dist/builtin-memory/04-base-worker.md +18 -0
- package/dist/builtin-memory/04-orchestration-kernel.md +1 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +13 -34
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/provider-rotation.test.ts +639 -741
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +241 -58
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/statusline.ts +2 -9
- package/dist/builtin-pi-packages/pi-crtr-extensions/lib/subscription-state.ts +365 -94
- package/dist/builtin-pi-packages/pi-crtr-extensions/package-lock.json +2 -2
- package/dist/builtin-pi-packages/pi-mode-switch/README.md +11 -39
- package/dist/builtin-pi-packages/pi-mode-switch/extensions/index.ts +20 -15
- package/dist/builtin-pi-packages/pi-mode-switch/package.json +1 -1
- package/dist/builtin-views/canvas/tui.mjs +8 -9
- package/dist/builtin-views/chat/tui.mjs +12 -12
- package/dist/builtin-views/git-pr/tui.mjs +7 -8
- package/dist/builtin-views/inbox/tui.mjs +27 -41
- package/dist/builtin-views/linkedin/tui.mjs +23 -37
- package/dist/builtin-views/prompt-review/tui.mjs +11 -11
- package/dist/builtin-views/settings/tui.mjs +11 -11
- package/dist/builtin-views/workspace-sidebar/tui.mjs +8 -9
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +1 -0
- package/dist/clients/attach/__tests__/attach-chrome-remote.test.js +11 -7
- package/dist/clients/attach/__tests__/crtr-output-render.test.js +1 -1
- package/dist/clients/attach/__tests__/mermaid-render.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/mermaid-render.test.js +20 -0
- package/dist/clients/attach/__tests__/oauth-dialog-lifecycle.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/oauth-dialog-lifecycle.test.js +110 -0
- package/dist/clients/attach/attach-cmd.d.ts +1 -1
- package/dist/clients/attach/attach-cmd.js +733 -730
- package/dist/clients/attach/auth-pickers.d.ts +12 -0
- package/dist/clients/attach/auth-pickers.js +11 -0
- package/dist/clients/attach/canvas-panels.js +2 -3
- package/dist/clients/attach/chat-view.d.ts +7 -8
- package/dist/clients/attach/chat-view.js +119 -79
- package/dist/clients/attach/edit-diff-render.d.ts +6 -0
- package/dist/clients/attach/edit-diff-render.js +263 -0
- package/dist/clients/attach/input-controller.d.ts +28 -2
- package/dist/clients/attach/input-controller.js +38 -5
- package/dist/clients/attach/mermaid-render.js +5 -2
- package/dist/clients/attach/pickers.d.ts +8 -7
- package/dist/clients/attach/pickers.js +13 -17
- package/dist/clients/attach/slash-commands.d.ts +9 -0
- package/dist/clients/attach/slash-commands.js +128 -7
- package/dist/clients/attach/titled-editor.d.ts +12 -1
- package/dist/clients/attach/titled-editor.js +103 -8
- package/dist/commands/canvas-browse.js +2 -2
- package/dist/commands/memory/lint.js +39 -5
- package/dist/commands/memory/write.js +1 -0
- package/dist/commands/node.js +12 -5
- package/dist/commands/profile/new.js +30 -5
- package/dist/commands/surface-tmux-spread.js +1 -3
- package/dist/commands/sys/__tests__/config-keybindings.test.d.ts +1 -0
- package/dist/commands/sys/__tests__/config-keybindings.test.js +55 -0
- package/dist/commands/sys/__tests__/config-model-ladders.test.d.ts +1 -0
- package/dist/commands/sys/__tests__/config-model-ladders.test.js +121 -0
- package/dist/commands/sys/config.js +18 -21
- package/dist/commands/sys/setup-core.d.ts +12 -1
- package/dist/commands/sys/setup-core.js +25 -3
- package/dist/commands/sys/setup.js +166 -20
- package/dist/core/__tests__/base-worker-prompt.test.d.ts +1 -0
- package/dist/core/__tests__/base-worker-prompt.test.js +24 -0
- package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +232 -1
- package/dist/core/__tests__/fault-classifier.test.js +15 -0
- package/dist/core/__tests__/full/broker-dialogs.test.js +7 -2
- package/dist/core/__tests__/full/broker-pane-resolution.test.js +9 -6
- package/dist/core/__tests__/full/cascade-close.test.js +5 -2
- package/dist/core/__tests__/full/dead-pane-regression.test.js +6 -3
- package/dist/core/__tests__/full/detach-focus.test.js +10 -5
- package/dist/core/__tests__/full/human-new-window-regression.test.js +6 -3
- package/dist/core/__tests__/full/review-render-pane-regression.test.js +2 -2
- package/dist/core/__tests__/helpers/harness.d.ts +1 -0
- package/dist/core/__tests__/helpers/harness.js +28 -4
- package/dist/core/__tests__/host-teardown-process-group.test.js +15 -4
- package/dist/core/__tests__/live-mutation-verbs.test.js +5 -4
- package/dist/core/__tests__/review-model-floor.test.js +1 -1
- package/dist/core/__tests__/stream-watchdog.test.d.ts +1 -0
- package/dist/core/__tests__/stream-watchdog.test.js +70 -0
- package/dist/core/__tests__/tmux-surface.test.js +72 -0
- package/dist/core/canvas/__tests__/remote-event-stream.test.js +11 -9
- package/dist/core/canvas/__tests__/render-remote.test.js +7 -4
- package/dist/core/canvas/browse/__tests__/model.test.js +23 -5
- package/dist/core/canvas/browse/__tests__/rebuild-coalescer.test.js +30 -13
- package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
- package/dist/core/canvas/browse/app.js +164 -5
- package/dist/core/canvas/browse/model.d.ts +10 -6
- package/dist/core/canvas/browse/model.js +20 -12
- package/dist/core/canvas/browse/pins.d.ts +4 -0
- package/dist/core/canvas/browse/pins.js +29 -0
- package/dist/core/canvas/browse/render.d.ts +13 -1
- package/dist/core/canvas/browse/render.js +72 -15
- package/dist/core/canvas/nav-model.js +5 -11
- package/dist/core/canvas/paths.d.ts +5 -0
- package/dist/core/canvas/paths.js +7 -0
- package/dist/core/canvas/pid.d.ts +14 -10
- package/dist/core/canvas/pid.js +14 -10
- package/dist/core/config.d.ts +53 -2
- package/dist/core/config.js +228 -110
- package/dist/core/fault-classifier.js +1 -1
- package/dist/core/fs-utils.d.ts +1 -0
- package/dist/core/fs-utils.js +15 -1
- package/dist/core/keybindings/__tests__/resolve.test.d.ts +1 -0
- package/dist/core/keybindings/__tests__/resolve.test.js +224 -0
- package/dist/core/keybindings/catalog.d.ts +14 -0
- package/dist/core/keybindings/catalog.js +256 -0
- package/dist/core/keybindings/index.d.ts +5 -0
- package/dist/core/keybindings/index.js +4 -0
- package/dist/core/keybindings/match.d.ts +29 -0
- package/dist/core/keybindings/match.js +99 -0
- package/dist/core/keybindings/persistence.d.ts +14 -0
- package/dist/core/keybindings/persistence.js +31 -0
- package/dist/core/keybindings/resolve.d.ts +14 -0
- package/dist/core/keybindings/resolve.js +305 -0
- package/dist/core/keybindings/types.d.ts +37 -0
- package/dist/core/keybindings/types.js +1 -0
- package/dist/core/memory-resolver.d.ts +1 -1
- package/dist/core/memory-resolver.js +10 -3
- package/dist/core/predicate.d.ts +5 -3
- package/dist/core/predicate.js +5 -3
- package/dist/core/runtime/broker-protocol.d.ts +33 -5
- package/dist/core/runtime/broker.js +165 -14
- package/dist/core/runtime/launch.d.ts +39 -6
- package/dist/core/runtime/launch.js +79 -21
- package/dist/core/runtime/naming.js +3 -3
- package/dist/core/runtime/pi-cli.d.ts +6 -0
- package/dist/core/runtime/pi-cli.js +16 -2
- package/dist/core/runtime/placement.d.ts +2 -2
- package/dist/core/runtime/placement.js +4 -1
- package/dist/core/runtime/promote.js +4 -0
- package/dist/core/runtime/recap.d.ts +3 -3
- package/dist/core/runtime/recap.js +47 -44
- package/dist/core/runtime/recycle.js +6 -1
- package/dist/core/runtime/reset.js +5 -0
- package/dist/core/runtime/revive.js +28 -18
- package/dist/core/runtime/session-list-cache.d.ts +23 -0
- package/dist/core/runtime/session-list-cache.js +270 -0
- package/dist/core/runtime/spawn.js +51 -32
- package/dist/core/runtime/stream-watchdog.d.ts +26 -0
- package/dist/core/runtime/stream-watchdog.js +75 -0
- package/dist/core/runtime/tmux-chrome.d.ts +1 -1
- package/dist/core/runtime/tmux-chrome.js +2 -2
- package/dist/core/runtime/tmux.d.ts +18 -15
- package/dist/core/runtime/tmux.js +216 -110
- package/dist/core/tui/__tests__/host-keybindings.test.d.ts +1 -0
- package/dist/core/tui/__tests__/host-keybindings.test.js +112 -0
- package/dist/core/tui/host.d.ts +30 -1
- package/dist/core/tui/host.js +148 -34
- package/dist/core/view/__tests__/transport-remote.test.js +9 -6
- package/dist/core/view/contract.d.ts +17 -3
- package/dist/daemon/crtrd-cli.d.ts +1 -1
- package/dist/daemon/crtrd-cli.js +1 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +41 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-context-nudge.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-context-nudge.test.js +121 -0
- package/dist/pi-extensions/canvas-inbox-watcher.d.ts +1 -1
- package/dist/pi-extensions/canvas-inbox-watcher.js +203 -25
- package/dist/pi-extensions/canvas-recap.d.ts +4 -0
- package/dist/pi-extensions/canvas-recap.js +38 -37
- package/dist/pi-extensions/canvas-stophook.d.ts +11 -0
- package/dist/pi-extensions/canvas-stophook.js +33 -4
- package/dist/suppress-experimental-warnings.d.ts +1 -0
- package/dist/suppress-experimental-warnings.js +15 -0
- package/dist/types.d.ts +3 -13
- package/dist/types.js +12 -36
- package/dist/web-client/assets/{index-CnF5r8ky.js → index--md2ylfi.js} +18 -17
- package/dist/web-client/assets/index-BUdm9s9s.css +2 -0
- package/dist/web-client/index.html +3 -3
- package/dist/web-client/sw.js +1 -1
- package/package.json +11 -8
- package/scripts/postinstall.mjs +1 -1
- package/dist/pi-extensions/canvas-nav.d.ts +0 -43
- package/dist/pi-extensions/canvas-nav.js +0 -640
- package/dist/pi-extensions/widget-order-bus.d.ts +0 -6
- package/dist/pi-extensions/widget-order-bus.js +0 -34
- package/dist/web-client/assets/index-BnmSLNLa.css +0 -2
|
@@ -108,20 +108,38 @@ test('pruneNode: absent id is a no-op', () => {
|
|
|
108
108
|
assert.equal(t.nodes.size, before);
|
|
109
109
|
});
|
|
110
110
|
// ── tab predicates ───────────────────────────────────────────────────────────
|
|
111
|
-
test('tabPredicate: All / Live / Dormant /
|
|
112
|
-
assert.deepEqual([...TABS], ['All', 'Live', 'Dormant', '
|
|
111
|
+
test('tabPredicate: All / Live / Dormant / Attention / Pinned', () => {
|
|
112
|
+
assert.deepEqual([...TABS], ['All', 'Live', 'Dormant', 'Attention', 'Pinned']);
|
|
113
113
|
const active = ROWS[0]; // root1 active
|
|
114
114
|
const idle = ROWS[1]; // child-a idle
|
|
115
115
|
const done = ROWS[2]; // grand-x done
|
|
116
|
-
const
|
|
116
|
+
const attention = ROWS[3]; // child-b active asks 2
|
|
117
117
|
assert.equal(tabPredicate('All', done), true);
|
|
118
118
|
assert.equal(tabPredicate('Live', active), true);
|
|
119
119
|
assert.equal(tabPredicate('Live', idle), true);
|
|
120
120
|
assert.equal(tabPredicate('Live', done), false);
|
|
121
121
|
assert.equal(tabPredicate('Dormant', done), true);
|
|
122
122
|
assert.equal(tabPredicate('Dormant', active), false);
|
|
123
|
-
|
|
124
|
-
assert.equal(tabPredicate('
|
|
123
|
+
// Attention = pending asks OR a fault.
|
|
124
|
+
assert.equal(tabPredicate('Attention', attention), true);
|
|
125
|
+
assert.equal(tabPredicate('Attention', active), false);
|
|
126
|
+
assert.equal(tabPredicate('Attention', { ...active, asks: 0, hanging: { kind: 'provider' } }), true);
|
|
127
|
+
// Pinned reads the isPinned arg, not the row.
|
|
128
|
+
assert.equal(tabPredicate('Pinned', active), false);
|
|
129
|
+
assert.equal(tabPredicate('Pinned', active, true), true);
|
|
130
|
+
});
|
|
131
|
+
// ── flatten: pins ────────────────────────────────────────────────────────────
|
|
132
|
+
test('flatten: Pinned tab shows only pinned rows', () => {
|
|
133
|
+
const pinned = new Set(['child-c']);
|
|
134
|
+
const v = flatten(tree(), { collapsed: new Set(), tab: 'Pinned', query: '', pinned });
|
|
135
|
+
// child-c is directly matched; root2 rides along as its ancestor (context).
|
|
136
|
+
const matched = v.filter((r) => r.matched).map((r) => r.id);
|
|
137
|
+
assert.deepEqual(matched, ['child-c']);
|
|
138
|
+
});
|
|
139
|
+
test('flatten: pinned rows float to the top in a flat sort', () => {
|
|
140
|
+
const pinned = new Set(['lonely']); // an idle straggler that would otherwise sort late
|
|
141
|
+
const v = flatten(tree(), { collapsed: new Set(), tab: 'All', query: '', sort: 'recency', pinned });
|
|
142
|
+
assert.equal(v[0].id, 'lonely');
|
|
125
143
|
});
|
|
126
144
|
// ── flatten: collapse / expand ───────────────────────────────────────────────
|
|
127
145
|
const allCollapsed = () => new Set(['root1', 'child-a', 'root2']); // every node with children
|
|
@@ -71,19 +71,36 @@ test('a burst of SSE-style triggers produces ONE round of remote requests, not N
|
|
|
71
71
|
res.end(JSON.stringify({ ok: true, exitCode: 0, stdout: JSON.stringify({ nodes: [] }), stderr: '' }));
|
|
72
72
|
});
|
|
73
73
|
});
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
74
|
+
// The server MUST close in a `finally` — a listening TCP server keeps its
|
|
75
|
+
// process's event loop non-empty forever. Under `node --test`, every test
|
|
76
|
+
// file is its own child process; if an assertion in this test throws before
|
|
77
|
+
// the old inline `server.close()` at the end of the function ran, the
|
|
78
|
+
// server (and the process) never exited, wedging one `--test-concurrency`
|
|
79
|
+
// slot permanently and hanging the whole suite (see the CI-hang postmortem).
|
|
80
|
+
try {
|
|
81
|
+
await new Promise((r) => server.listen(0, '127.0.0.1', r));
|
|
82
|
+
const addr = server.address();
|
|
83
|
+
const port = typeof addr === 'object' && addr !== null ? addr.port : 0;
|
|
84
|
+
const source = new RemoteCanvasSource(`http://127.0.0.1:${port}`, 'tok');
|
|
85
|
+
const rebuilder = createCoalescedRebuilder(async () => {
|
|
86
|
+
source.invalidate();
|
|
87
|
+
await source.listNodes(); // one round of remote requests, mirroring app.ts's rebuildSnapshot
|
|
88
|
+
}, 20);
|
|
89
|
+
for (let i = 0; i < 6; i++)
|
|
90
|
+
rebuilder.trigger();
|
|
91
|
+
// Poll for the request to land instead of sleeping a fixed duration —
|
|
92
|
+
// deterministic under CI's variable CPU load, and doesn't slow down the
|
|
93
|
+
// common case (returns as soon as the debounce + request round trip land).
|
|
94
|
+
const deadline = Date.now() + 2000;
|
|
95
|
+
while (hits < 1 && Date.now() < deadline) {
|
|
96
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
97
|
+
}
|
|
98
|
+
assert.equal(hits, 1, 'a burst of 6 SSE events must produce ONE round of remote requests, not 6');
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
server.closeAllConnections();
|
|
102
|
+
await new Promise((r) => server.close(() => r()));
|
|
103
|
+
}
|
|
87
104
|
});
|
|
88
105
|
// Integration proof for browse/app.ts's ACTUAL wiring — not the coalescer
|
|
89
106
|
// helper in isolation. Every test above instantiates `createCoalescedRebuilder`
|
|
@@ -15,6 +15,7 @@ import { RemoteCanvasSource } from '../remote-canvas-source.js';
|
|
|
15
15
|
import { closeNode } from '../../runtime/close.js';
|
|
16
16
|
import { setupTerminal, restoreTerminal, getTerminalSize, parseKeypress, } from '../../tui/terminal.js';
|
|
17
17
|
import { buildTree, flatten, pruneNode, TABS } from './model.js';
|
|
18
|
+
import { loadPins, savePins } from './pins.js';
|
|
18
19
|
import { createCoalescedRebuilder } from './rebuild-coalescer.js';
|
|
19
20
|
// Sort cycle for the `s` key. Starts on the default `attention` ordering; one `s`
|
|
20
21
|
// press restores the structural `tree` view, then relevance/recency, then back.
|
|
@@ -23,10 +24,10 @@ import { renderFrame, detectColorCaps, headerHeight, PREVIEW_HEIGHT } from './re
|
|
|
23
24
|
/** Viewport (body) height = total rows minus the header renderFrame draws (see
|
|
24
25
|
* render.ts headerHeight), the footer, and the preview panel when shown. Kept
|
|
25
26
|
* in lockstep with render.ts via the shared headerHeight/PREVIEW_HEIGHT. */
|
|
26
|
-
function viewportHeight(rowsTotal,
|
|
27
|
+
function viewportHeight(rowsTotal, inputLine, previewOn) {
|
|
27
28
|
const rows = Math.max(8, rowsTotal);
|
|
28
29
|
const previewH = previewOn ? PREVIEW_HEIGHT : 0;
|
|
29
|
-
return Math.max(1, rows - headerHeight(
|
|
30
|
+
return Math.max(1, rows - headerHeight(inputLine) - 1 /* footer */ - previewH);
|
|
30
31
|
}
|
|
31
32
|
/** Wires a RemoteEventStream's `nodes`/`inbox` events through the coalescer
|
|
32
33
|
* (`rebuild-coalescer.ts`) so a burst of SSE events collapses to ONE rebuild
|
|
@@ -103,6 +104,14 @@ export async function runBrowse(opts = {}) {
|
|
|
103
104
|
preview: true, // default ON (decision)
|
|
104
105
|
residentsOnly: true, // default ON: hide one-shot workers (decision)
|
|
105
106
|
pendingClose: null,
|
|
107
|
+
pinned: loadPins(),
|
|
108
|
+
help: false,
|
|
109
|
+
rename: false,
|
|
110
|
+
renameBuf: '',
|
|
111
|
+
renameTarget: null,
|
|
112
|
+
message: false,
|
|
113
|
+
messageBuf: '',
|
|
114
|
+
messageTarget: null,
|
|
106
115
|
};
|
|
107
116
|
let visible = [];
|
|
108
117
|
// The DashboardRow ref the tree holds for a visible id. Enrichment mutates this
|
|
@@ -193,6 +202,7 @@ export async function runBrowse(opts = {}) {
|
|
|
193
202
|
cwdScope: state.cwdScope,
|
|
194
203
|
residentsOnly: state.residentsOnly,
|
|
195
204
|
sort: state.sort,
|
|
205
|
+
pinned: state.pinned,
|
|
196
206
|
});
|
|
197
207
|
if (keepId !== undefined) {
|
|
198
208
|
const idx = visible.findIndex((v) => v.id === keepId);
|
|
@@ -207,7 +217,8 @@ export async function runBrowse(opts = {}) {
|
|
|
207
217
|
const flush = async () => {
|
|
208
218
|
const size = getTerminalSize();
|
|
209
219
|
const previewOn = state.preview && visible.length > 0;
|
|
210
|
-
const
|
|
220
|
+
const inputMode = state.search || state.rename || state.message;
|
|
221
|
+
const viewport = viewportHeight(size.rows, inputMode, previewOn);
|
|
211
222
|
// Keep the cursor inside the viewport window.
|
|
212
223
|
if (state.cursor < state.scrollOffset)
|
|
213
224
|
state.scrollOffset = state.cursor;
|
|
@@ -238,6 +249,9 @@ export async function runBrowse(opts = {}) {
|
|
|
238
249
|
query: state.query, search: state.search, totalNodes,
|
|
239
250
|
cwdScope: state.cwdScope, sort: state.sort, preview: state.preview,
|
|
240
251
|
residentsOnly: state.residentsOnly, pendingClose: state.pendingClose,
|
|
252
|
+
pinned: state.pinned, help: state.help,
|
|
253
|
+
rename: state.rename, renameBuf: state.renameBuf,
|
|
254
|
+
message: state.message, messageBuf: state.messageBuf,
|
|
241
255
|
readOnly: isRemote,
|
|
242
256
|
}, size, caps);
|
|
243
257
|
process.stdout.write(frame);
|
|
@@ -419,6 +433,102 @@ export async function runBrowse(opts = {}) {
|
|
|
419
433
|
scheduleSearchRepaint();
|
|
420
434
|
}
|
|
421
435
|
};
|
|
436
|
+
// Recenter the cursor on the launching node (CRTR_NODE_ID), expanding its
|
|
437
|
+
// ancestors so it is actually visible. No-op outside a node or when it's filtered
|
|
438
|
+
// out of the current tab/scope. Same move the boot performs, on demand (`.`).
|
|
439
|
+
const recenterOnSelf = () => {
|
|
440
|
+
const selfId = process.env['CRTR_NODE_ID'];
|
|
441
|
+
if (selfId === undefined || selfId === '' || !tree.nodes.has(selfId))
|
|
442
|
+
return;
|
|
443
|
+
for (let p = tree.nodes.get(selfId)?.parentId ?? null; p !== null; p = tree.nodes.get(p)?.parentId ?? null) {
|
|
444
|
+
state.collapsed.delete(p);
|
|
445
|
+
}
|
|
446
|
+
recompute(selfId);
|
|
447
|
+
};
|
|
448
|
+
// Toggle the pin on a node: mutate the in-memory set, persist best-effort, and
|
|
449
|
+
// recompute so it floats (flat sorts) / (dis)appears from the Pinned tab.
|
|
450
|
+
const togglePin = (id) => {
|
|
451
|
+
if (state.pinned.has(id))
|
|
452
|
+
state.pinned.delete(id);
|
|
453
|
+
else
|
|
454
|
+
state.pinned.add(id);
|
|
455
|
+
savePins(state.pinned);
|
|
456
|
+
recompute(id);
|
|
457
|
+
};
|
|
458
|
+
// Commit an inline rename: `crtr node config <id> --name <name>` (also renames a
|
|
459
|
+
// live viewer window), then reflect the new name into the snapshot in place.
|
|
460
|
+
const commitRename = () => {
|
|
461
|
+
const id = state.renameTarget;
|
|
462
|
+
const name = state.renameBuf.trim();
|
|
463
|
+
state.rename = false;
|
|
464
|
+
if (id !== null && name !== '') {
|
|
465
|
+
try {
|
|
466
|
+
execFileSync('crtr', ['node', 'config', id, '--name', name], { stdio: 'ignore' });
|
|
467
|
+
}
|
|
468
|
+
catch {
|
|
469
|
+
/* best-effort — a rename failure never wedges the picker */
|
|
470
|
+
}
|
|
471
|
+
const n = tree.nodes.get(id);
|
|
472
|
+
if (n !== undefined)
|
|
473
|
+
n.row.name = name;
|
|
474
|
+
}
|
|
475
|
+
recompute(state.renameTarget ?? undefined);
|
|
476
|
+
scheduleFlush();
|
|
477
|
+
};
|
|
478
|
+
// Commit an inline message: `crtr node msg --to <id> <body>` (immediate inbox
|
|
479
|
+
// delivery, best-effort revive if dormant). Fire-and-forget — browse stays put.
|
|
480
|
+
const commitMessage = () => {
|
|
481
|
+
const id = state.messageTarget;
|
|
482
|
+
const body = state.messageBuf.trim();
|
|
483
|
+
state.message = false;
|
|
484
|
+
if (id !== null && body !== '') {
|
|
485
|
+
try {
|
|
486
|
+
execFileSync('crtr', ['node', 'msg', '--to', id, body], { stdio: 'ignore' });
|
|
487
|
+
}
|
|
488
|
+
catch {
|
|
489
|
+
/* best-effort — delivery failure never wedges the picker */
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
scheduleFlush();
|
|
493
|
+
};
|
|
494
|
+
// Shared text-input keystroke handling for the rename/message sub-modes: Esc
|
|
495
|
+
// cancels, Enter commits, Backspace deletes, Ctrl+C quits, printable chars append.
|
|
496
|
+
const onKeyText = (input, key, get, set, commit, cancel) => {
|
|
497
|
+
if (key.escape) {
|
|
498
|
+
cancel();
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (key.return) {
|
|
502
|
+
commit();
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
if (key.backspace) {
|
|
506
|
+
set(get().slice(0, -1));
|
|
507
|
+
scheduleFlush();
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
if (key.ctrl) {
|
|
511
|
+
if (input === 'c')
|
|
512
|
+
quit();
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
const printable = [...input].filter((c) => c >= ' ').join('');
|
|
516
|
+
if (printable.length > 0) {
|
|
517
|
+
set(get() + printable);
|
|
518
|
+
scheduleFlush();
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
const onKeyRename = (input, key) => onKeyText(input, key, () => state.renameBuf, (s) => { state.renameBuf = s; }, commitRename, () => { state.rename = false; scheduleFlush(); });
|
|
522
|
+
const onKeyMessage = (input, key) => onKeyText(input, key, () => state.messageBuf, (s) => { state.messageBuf = s; }, commitMessage, () => { state.message = false; scheduleFlush(); });
|
|
523
|
+
// Help overlay: any key dismisses (Ctrl+C still quits outright).
|
|
524
|
+
const onKeyHelp = (input, key) => {
|
|
525
|
+
if (key.ctrl && input === 'c') {
|
|
526
|
+
quit();
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
state.help = false;
|
|
530
|
+
scheduleFlush();
|
|
531
|
+
};
|
|
422
532
|
const onKeyNav = (input, key) => {
|
|
423
533
|
// Ctrl-combos first: only Ctrl+C is meaningful (quit); swallow the rest so
|
|
424
534
|
// Ctrl+L / Ctrl+J / Ctrl+Q etc. don't masquerade as l/j/q commands.
|
|
@@ -519,7 +629,7 @@ export async function runBrowse(opts = {}) {
|
|
|
519
629
|
scheduleFlush();
|
|
520
630
|
return;
|
|
521
631
|
}
|
|
522
|
-
if (input >= '1' && input <= '
|
|
632
|
+
if (input >= '1' && input <= '5') {
|
|
523
633
|
const idx = Number(input) - 1;
|
|
524
634
|
if (idx < TABS.length) {
|
|
525
635
|
state.tab = TABS[idx];
|
|
@@ -569,6 +679,49 @@ export async function runBrowse(opts = {}) {
|
|
|
569
679
|
scheduleFlush();
|
|
570
680
|
return;
|
|
571
681
|
}
|
|
682
|
+
// Help overlay.
|
|
683
|
+
if (input === '?') {
|
|
684
|
+
state.help = true;
|
|
685
|
+
scheduleFlush();
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
// Pin / unpin (local, view-only — works against a remote canvas too).
|
|
689
|
+
if (input === 'f') {
|
|
690
|
+
if (row !== undefined)
|
|
691
|
+
togglePin(row.id);
|
|
692
|
+
scheduleFlush();
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
// Recenter on the launching node.
|
|
696
|
+
if (input === '.') {
|
|
697
|
+
recenterOnSelf();
|
|
698
|
+
scheduleFlush();
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
// Rename the node inline (mutation — no-op on a remote canvas).
|
|
702
|
+
if (input === 'R') {
|
|
703
|
+
if (isRemote)
|
|
704
|
+
return;
|
|
705
|
+
if (row !== undefined) {
|
|
706
|
+
state.rename = true;
|
|
707
|
+
state.renameTarget = row.id;
|
|
708
|
+
state.renameBuf = rowOf(row.id)?.name ?? '';
|
|
709
|
+
}
|
|
710
|
+
scheduleFlush();
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
// Message the node without resuming it (mutation — no-op on a remote canvas).
|
|
714
|
+
if (input === 'm') {
|
|
715
|
+
if (isRemote)
|
|
716
|
+
return;
|
|
717
|
+
if (row !== undefined) {
|
|
718
|
+
state.message = true;
|
|
719
|
+
state.messageTarget = row.id;
|
|
720
|
+
state.messageBuf = '';
|
|
721
|
+
}
|
|
722
|
+
scheduleFlush();
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
572
725
|
// Search. Starting a search ranks by relevance (decision) so the best prompt/
|
|
573
726
|
// name match floats to the top as you type.
|
|
574
727
|
if (input === '/') {
|
|
@@ -664,8 +817,14 @@ export async function runBrowse(opts = {}) {
|
|
|
664
817
|
const onData = (data) => {
|
|
665
818
|
try {
|
|
666
819
|
const { input, key } = parseKeypress(data);
|
|
667
|
-
if (state.
|
|
820
|
+
if (state.help)
|
|
821
|
+
onKeyHelp(input, key);
|
|
822
|
+
else if (state.search)
|
|
668
823
|
onKeySearch(input, key);
|
|
824
|
+
else if (state.rename)
|
|
825
|
+
onKeyRename(input, key);
|
|
826
|
+
else if (state.message)
|
|
827
|
+
onKeyMessage(input, key);
|
|
669
828
|
else
|
|
670
829
|
onKeyNav(input, key);
|
|
671
830
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { DashboardRow } from '../render.js';
|
|
2
2
|
import type { NodeStatus } from '../types.js';
|
|
3
|
-
export type Tab = 'All' | 'Live' | 'Dormant' | '
|
|
3
|
+
export type Tab = 'All' | 'Live' | 'Dormant' | 'Attention' | 'Pinned';
|
|
4
4
|
export declare const TABS: readonly Tab[];
|
|
5
5
|
/** How the visible rows are ordered.
|
|
6
6
|
* tree — spanning-tree order, ancestors shown for context (the default).
|
|
@@ -11,11 +11,12 @@ export declare const TABS: readonly Tab[];
|
|
|
11
11
|
export type SortMode = 'tree' | 'attention' | 'relevance' | 'recency';
|
|
12
12
|
export declare const SORTS: readonly SortMode[];
|
|
13
13
|
/** Does a node belong to this tab's slice?
|
|
14
|
-
* All
|
|
15
|
-
* Live
|
|
16
|
-
* Dormant
|
|
17
|
-
*
|
|
18
|
-
|
|
14
|
+
* All — every node.
|
|
15
|
+
* Live — active | idle.
|
|
16
|
+
* Dormant — done | dead | canceled.
|
|
17
|
+
* Attention — needs a human: has pending asks OR is parked on a fault.
|
|
18
|
+
* Pinned — user-starred (a local, view-only preference; `isPinned`). */
|
|
19
|
+
export declare function tabPredicate(tab: Tab, row: DashboardRow, isPinned?: boolean): boolean;
|
|
19
20
|
export interface TreeNode {
|
|
20
21
|
row: DashboardRow;
|
|
21
22
|
depth: number;
|
|
@@ -138,6 +139,9 @@ export interface FlattenOpts {
|
|
|
138
139
|
/** Ordering. `tree` keeps the spanning tree + ancestor context; `relevance` /
|
|
139
140
|
* `recency` produce a FLAT ranked list of directly-matched rows. */
|
|
140
141
|
sort?: SortMode;
|
|
142
|
+
/** User-starred node ids (local, view-only). Gates the Pinned tab, and floats
|
|
143
|
+
* pinned rows to the top of every FLAT sort mode. undefined = none pinned. */
|
|
144
|
+
pinned?: Set<string>;
|
|
141
145
|
}
|
|
142
146
|
/** Is this row inside the active cwd scope? No scope (null/undefined) = All dirs. */
|
|
143
147
|
export declare function cwdMatch(scope: string | null | undefined, row: DashboardRow): boolean;
|
|
@@ -4,19 +4,21 @@
|
|
|
4
4
|
// it is exhaustively unit-testable. The app layer (app.ts) wires the canvas data
|
|
5
5
|
// access (dashboardRowsAll / listNodes / subscriptionsOf) into buildTree, then
|
|
6
6
|
// drives flatten() on each keystroke.
|
|
7
|
-
export const TABS = ['All', 'Live', 'Dormant', '
|
|
7
|
+
export const TABS = ['All', 'Live', 'Dormant', 'Attention', 'Pinned'];
|
|
8
8
|
export const SORTS = ['tree', 'attention', 'relevance', 'recency'];
|
|
9
9
|
/** Does a node belong to this tab's slice?
|
|
10
|
-
* All
|
|
11
|
-
* Live
|
|
12
|
-
* Dormant
|
|
13
|
-
*
|
|
14
|
-
|
|
10
|
+
* All — every node.
|
|
11
|
+
* Live — active | idle.
|
|
12
|
+
* Dormant — done | dead | canceled.
|
|
13
|
+
* Attention — needs a human: has pending asks OR is parked on a fault.
|
|
14
|
+
* Pinned — user-starred (a local, view-only preference; `isPinned`). */
|
|
15
|
+
export function tabPredicate(tab, row, isPinned = false) {
|
|
15
16
|
switch (tab) {
|
|
16
17
|
case 'All': return true;
|
|
17
18
|
case 'Live': return row.status === 'active' || row.status === 'idle';
|
|
18
19
|
case 'Dormant': return row.status === 'done' || row.status === 'dead' || row.status === 'canceled';
|
|
19
|
-
case '
|
|
20
|
+
case 'Attention': return row.asks > 0 || row.hanging != null;
|
|
21
|
+
case 'Pinned': return isPinned;
|
|
20
22
|
}
|
|
21
23
|
}
|
|
22
24
|
/** Sort rank for roots/stragglers — live first (active, then idle), dormant
|
|
@@ -383,7 +385,8 @@ export function lifecycleMatch(residentsOnly, row) {
|
|
|
383
385
|
* always reachable.
|
|
384
386
|
*/
|
|
385
387
|
export function flatten(tree, opts) {
|
|
386
|
-
const { collapsed, tab, query, cwdScope, residentsOnly, sort = 'tree' } = opts;
|
|
388
|
+
const { collapsed, tab, query, cwdScope, residentsOnly, sort = 'tree', pinned } = opts;
|
|
389
|
+
const isPinned = (id) => pinned?.has(id) === true;
|
|
387
390
|
// 1. Directly-matched nodes: tab predicate AND cwd scope AND lifecycle AND query.
|
|
388
391
|
// `matched` honors the residents-only filter (drives top-level rows, ancestor
|
|
389
392
|
// context, force-expand, and the flat search results below). `matchedAll` is
|
|
@@ -394,7 +397,7 @@ export function flatten(tree, opts) {
|
|
|
394
397
|
const matched = new Set();
|
|
395
398
|
const matchedAll = new Set();
|
|
396
399
|
for (const [id, node] of tree.nodes) {
|
|
397
|
-
if (tabPredicate(tab, node.row) && cwdMatch(cwdScope, node.row) && queryMatch(query, node.row)) {
|
|
400
|
+
if (tabPredicate(tab, node.row, isPinned(id)) && cwdMatch(cwdScope, node.row) && queryMatch(query, node.row)) {
|
|
398
401
|
matchedAll.add(id);
|
|
399
402
|
if (lifecycleMatch(residentsOnly, node.row))
|
|
400
403
|
matched.add(id);
|
|
@@ -407,21 +410,26 @@ export function flatten(tree, opts) {
|
|
|
407
410
|
const ids = [...matched];
|
|
408
411
|
const createdOf = (id) => tree.nodes.get(id)?.row.created ?? '';
|
|
409
412
|
const byRecency = (a, b) => createdOf(b).localeCompare(createdOf(a));
|
|
413
|
+
// Pinned rows float above everything in every flat mode — "keep this at the
|
|
414
|
+
// top of my list" is the whole point of a pin. Ordering WITHIN each pin tier
|
|
415
|
+
// still follows the active sort.
|
|
416
|
+
const pinRank = (id) => (isPinned(id) ? 0 : 1);
|
|
410
417
|
if (sort === 'attention') {
|
|
411
418
|
// Tiered (attached/streaming/live first), newest-message-first within tier.
|
|
412
419
|
const rowOf = (id) => tree.nodes.get(id).row;
|
|
413
|
-
ids.sort((a, b) => (
|
|
420
|
+
ids.sort((a, b) => (pinRank(a) - pinRank(b)) ||
|
|
421
|
+
(attentionTier(rowOf(a)) - attentionTier(rowOf(b))) ||
|
|
414
422
|
(attentionMtime(rowOf(b)) - attentionMtime(rowOf(a))) ||
|
|
415
423
|
byRecency(a, b));
|
|
416
424
|
}
|
|
417
425
|
else if (sort === 'recency' || query === '') {
|
|
418
|
-
ids.sort(byRecency);
|
|
426
|
+
ids.sort((a, b) => (pinRank(a) - pinRank(b)) || byRecency(a, b));
|
|
419
427
|
}
|
|
420
428
|
else {
|
|
421
429
|
const score = new Map();
|
|
422
430
|
for (const id of ids)
|
|
423
431
|
score.set(id, scoreRow(query, tree.nodes.get(id).row));
|
|
424
|
-
ids.sort((a, b) => (score.get(b) - score.get(a)) || byRecency(a, b));
|
|
432
|
+
ids.sort((a, b) => (pinRank(a) - pinRank(b)) || (score.get(b) - score.get(a)) || byRecency(a, b));
|
|
425
433
|
}
|
|
426
434
|
return ids.map((id) => ({ id, depth: 0, hasChildren: false, collapsed: false, matched: true }));
|
|
427
435
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Load the pinned-id set. Missing/garbage file → empty set (never throws). */
|
|
2
|
+
export declare function loadPins(): Set<string>;
|
|
3
|
+
/** Persist the pinned-id set. Best-effort — a write failure never wedges the TUI. */
|
|
4
|
+
export declare function savePins(pins: Set<string>): void;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// pins.ts — the durable per-user "pinned" set for the canvas browser.
|
|
2
|
+
//
|
|
3
|
+
// A pin is a LOCAL, view-only preference, NOT a canvas property: starring a node
|
|
4
|
+
// keeps it at the top of YOUR navigator across sessions and machines, and it works
|
|
5
|
+
// against a remote canvas too (you are pinning in your own view, mutating nothing
|
|
6
|
+
// on the canvas). Stored as a flat id list at ~/.crouter/browse-pins.json so every
|
|
7
|
+
// `canvas browse` invocation shares it regardless of cwd/scope. Read once at boot,
|
|
8
|
+
// mutated in memory, written best-effort on each toggle.
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { userScopeRoot } from '../../scope.js';
|
|
11
|
+
import { readJsonIfExists, atomicWriteJson } from '../../fs-utils.js';
|
|
12
|
+
const PINS_FILE = 'browse-pins.json';
|
|
13
|
+
function pinsPath() {
|
|
14
|
+
return join(userScopeRoot(), PINS_FILE);
|
|
15
|
+
}
|
|
16
|
+
/** Load the pinned-id set. Missing/garbage file → empty set (never throws). */
|
|
17
|
+
export function loadPins() {
|
|
18
|
+
const raw = readJsonIfExists(pinsPath());
|
|
19
|
+
return new Set(Array.isArray(raw) ? raw.filter((x) => typeof x === 'string') : []);
|
|
20
|
+
}
|
|
21
|
+
/** Persist the pinned-id set. Best-effort — a write failure never wedges the TUI. */
|
|
22
|
+
export function savePins(pins) {
|
|
23
|
+
try {
|
|
24
|
+
atomicWriteJson(pinsPath(), [...pins]);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
/* best-effort: a pin that doesn't survive the session is a minor loss */
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -6,7 +6,7 @@ export declare const PROMPT_LINES = 3;
|
|
|
6
6
|
export declare const REPLY_LINES = 3;
|
|
7
7
|
export declare const PREVIEW_BODY: number;
|
|
8
8
|
export declare const PREVIEW_HEIGHT: number;
|
|
9
|
-
export declare function headerHeight(
|
|
9
|
+
export declare function headerHeight(inputLine: boolean): number;
|
|
10
10
|
export interface RenderState {
|
|
11
11
|
tree: Tree;
|
|
12
12
|
visible: VisibleRow[];
|
|
@@ -25,6 +25,18 @@ export interface RenderState {
|
|
|
25
25
|
/** Lifecycle filter: when true, `terminal` (one-shot worker) nodes are hidden
|
|
26
26
|
* — surfaced as a status-line cue. */
|
|
27
27
|
residentsOnly: boolean;
|
|
28
|
+
/** User-starred node ids — drives the ★ row marker + the Pinned tab. */
|
|
29
|
+
pinned: Set<string>;
|
|
30
|
+
/** Inline rename input: when true, the header shows a rename prompt bound to
|
|
31
|
+
* `renameBuf` and the footer becomes commit/cancel. */
|
|
32
|
+
rename?: boolean;
|
|
33
|
+
renameBuf?: string;
|
|
34
|
+
/** Inline message input: when true, the header shows a message prompt bound to
|
|
35
|
+
* `messageBuf` (delivered via `crtr node msg`). */
|
|
36
|
+
message?: boolean;
|
|
37
|
+
messageBuf?: string;
|
|
38
|
+
/** When true, the whole body is replaced by the keybinding help overlay. */
|
|
39
|
+
help?: boolean;
|
|
28
40
|
/** When set, a close-out (`x`) is awaiting y/n confirmation because the node (or
|
|
29
41
|
* a descendant) is actively streaming. Holds the node id being confirmed; the
|
|
30
42
|
* footer becomes the y/n prompt. null/undefined = no pending confirm. */
|