@crouton-kit/crouter 0.3.70 → 0.3.79
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/build-root.d.ts +12 -4
- package/dist/build-root.js +25 -6
- 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-memory/crouter-development/plugins.md +82 -5
- 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 +1115 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +234 -71
- 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 +2 -516
- 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/clients/attach/__tests__/attach-chrome-remote.test.js +11 -6
- package/dist/clients/attach/__tests__/attach-keybindings.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/attach-keybindings.test.js +113 -0
- 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 +28 -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 +9 -1
- package/dist/clients/attach/attach-cmd.js +841 -792
- package/dist/clients/attach/auth-pickers.d.ts +0 -12
- package/dist/clients/attach/auth-pickers.js +64 -15
- 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/graph-overlay.d.ts +12 -2
- package/dist/clients/attach/graph-overlay.js +83 -33
- 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 +11 -17
- package/dist/clients/attach/slash-commands.d.ts +9 -0
- package/dist/clients/attach/slash-commands.js +127 -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/human/queue.js +3 -4
- package/dist/commands/memory/lint.js +39 -5
- package/dist/commands/memory/write.js +1 -0
- package/dist/commands/node.js +9 -2
- package/dist/commands/pkg/plugin-inspect.js +22 -1
- package/dist/commands/pkg/plugin-manage.js +31 -9
- 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/__tests__/setup-core.test.js +158 -1
- package/dist/commands/sys/config.js +18 -21
- package/dist/commands/sys/doctor.js +42 -4
- package/dist/commands/sys/setup-core.d.ts +49 -1
- package/dist/commands/sys/setup-core.js +161 -4
- package/dist/commands/sys/setup.d.ts +88 -0
- package/dist/commands/sys/setup.js +943 -169
- package/dist/commands/view-pick.d.ts +4 -0
- package/dist/commands/view-pick.js +17 -7
- 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__/canvas-inbox-watcher.test.js +34 -9
- package/dist/core/__tests__/command-plugins-surfaces.test.d.ts +1 -0
- package/dist/core/__tests__/command-plugins-surfaces.test.js +298 -0
- package/dist/core/__tests__/command-plugins.test.d.ts +1 -0
- package/dist/core/__tests__/command-plugins.test.js +444 -0
- package/dist/core/__tests__/fault-classifier.test.js +15 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +6 -0
- package/dist/core/__tests__/fixtures/fake-engine.js +9 -1
- package/dist/core/__tests__/full/broker-dialogs.test.js +7 -2
- package/dist/core/__tests__/host-teardown-process-group.test.js +15 -4
- package/dist/core/__tests__/preview-registry-sync.test.js +30 -1
- package/dist/core/__tests__/scope-crouter-home-fence.test.d.ts +1 -0
- package/dist/core/__tests__/scope-crouter-home-fence.test.js +55 -0
- 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/browse/__tests__/model.test.js +23 -5
- package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
- package/dist/core/canvas/browse/app.d.ts +6 -0
- package/dist/core/canvas/browse/app.js +230 -41
- 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/command-plugins/adapter.d.ts +15 -0
- package/dist/core/command-plugins/adapter.js +145 -0
- package/dist/core/command-plugins/compose.d.ts +5 -0
- package/dist/core/command-plugins/compose.js +56 -0
- package/dist/core/command-plugins/discovery.d.ts +104 -0
- package/dist/core/command-plugins/discovery.js +565 -0
- package/dist/core/config.d.ts +48 -0
- 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__/bespoke-consumers.test.d.ts +1 -0
- package/dist/core/keybindings/__tests__/bespoke-consumers.test.js +40 -0
- 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 +257 -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/profiles/select.d.ts +6 -0
- package/dist/core/profiles/select.js +86 -52
- package/dist/core/provider-management.d.ts +12 -0
- package/dist/core/provider-management.js +24 -0
- package/dist/core/runtime/broker-protocol.d.ts +33 -5
- package/dist/core/runtime/broker.js +164 -14
- package/dist/core/runtime/launch.d.ts +39 -6
- package/dist/core/runtime/launch.js +78 -20
- 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/pi-vendored.d.ts +8 -0
- package/dist/core/runtime/pi-vendored.js +14 -0
- 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 +75 -47
- package/dist/core/runtime/recycle.js +6 -1
- package/dist/core/runtime/reset.js +5 -0
- package/dist/core/runtime/session-list-cache.d.ts +33 -0
- package/dist/core/runtime/session-list-cache.js +338 -0
- package/dist/core/runtime/spawn.js +52 -45
- 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 +217 -110
- package/dist/core/scope.js +27 -4
- package/dist/core/subscription-state.d.ts +90 -0
- package/dist/core/subscription-state.js +762 -0
- 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/contract.d.ts +17 -3
- package/dist/daemon/crtrd.js +253 -12
- 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 +73 -46
- package/dist/pi-extensions/canvas-stophook.d.ts +11 -0
- package/dist/pi-extensions/canvas-stophook.js +33 -4
- package/dist/types.d.ts +9 -26
- package/dist/types.js +1 -28
- package/dist/web-client/assets/{index-DiFuLcp6.js → index--md2ylfi.js} +1 -1
- package/dist/web-client/index.html +1 -1
- package/dist/web-client/sw.js +1 -1
- package/package.json +8 -4
- 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
|
@@ -15,7 +15,30 @@ 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';
|
|
20
|
+
import { matchesTerminalInput, resolveUserKeybindings, } from '../../keybindings/index.js';
|
|
21
|
+
const BROWSE_NAV_ACTIONS = [
|
|
22
|
+
'crtr.browse.quit', 'crtr.browse.down', 'crtr.browse.up', 'crtr.browse.first', 'crtr.browse.last',
|
|
23
|
+
'crtr.browse.expand-or-child', 'crtr.browse.collapse-or-parent', 'crtr.browse.toggle-fold',
|
|
24
|
+
'crtr.browse.tab-next', 'crtr.browse.tab-previous', 'crtr.browse.tab.1', 'crtr.browse.tab.2',
|
|
25
|
+
'crtr.browse.tab.3', 'crtr.browse.tab.4', 'crtr.browse.tab.5', 'crtr.browse.close-subtree', 'crtr.browse.sort',
|
|
26
|
+
'crtr.browse.scope', 'crtr.browse.residents', 'crtr.browse.preview', 'crtr.browse.search',
|
|
27
|
+
'crtr.browse.revive-now', 'crtr.browse.focus',
|
|
28
|
+
];
|
|
29
|
+
const BROWSE_SEARCH_ACTIONS = [
|
|
30
|
+
'crtr.browse.search.cancel', 'crtr.browse.search.commit', 'crtr.browse.search.delete',
|
|
31
|
+
'crtr.browse.search.quit',
|
|
32
|
+
];
|
|
33
|
+
function firstMatching(bindings, ids, raw) {
|
|
34
|
+
return ids.find((id) => matchesTerminalInput(bindings, id, raw)) ?? null;
|
|
35
|
+
}
|
|
36
|
+
export function resolveBrowseNavAction(bindings, raw) {
|
|
37
|
+
return firstMatching(bindings, BROWSE_NAV_ACTIONS, raw);
|
|
38
|
+
}
|
|
39
|
+
export function resolveBrowseSearchAction(bindings, raw) {
|
|
40
|
+
return firstMatching(bindings, BROWSE_SEARCH_ACTIONS, raw);
|
|
41
|
+
}
|
|
19
42
|
// Sort cycle for the `s` key. Starts on the default `attention` ordering; one `s`
|
|
20
43
|
// press restores the structural `tree` view, then relevance/recency, then back.
|
|
21
44
|
const SORT_CYCLE = ['attention', 'tree', 'relevance', 'recency'];
|
|
@@ -23,10 +46,10 @@ import { renderFrame, detectColorCaps, headerHeight, PREVIEW_HEIGHT } from './re
|
|
|
23
46
|
/** Viewport (body) height = total rows minus the header renderFrame draws (see
|
|
24
47
|
* render.ts headerHeight), the footer, and the preview panel when shown. Kept
|
|
25
48
|
* in lockstep with render.ts via the shared headerHeight/PREVIEW_HEIGHT. */
|
|
26
|
-
function viewportHeight(rowsTotal,
|
|
49
|
+
function viewportHeight(rowsTotal, inputLine, previewOn) {
|
|
27
50
|
const rows = Math.max(8, rowsTotal);
|
|
28
51
|
const previewH = previewOn ? PREVIEW_HEIGHT : 0;
|
|
29
|
-
return Math.max(1, rows - headerHeight(
|
|
52
|
+
return Math.max(1, rows - headerHeight(inputLine) - 1 /* footer */ - previewH);
|
|
30
53
|
}
|
|
31
54
|
/** Wires a RemoteEventStream's `nodes`/`inbox` events through the coalescer
|
|
32
55
|
* (`rebuild-coalescer.ts`) so a burst of SSE events collapses to ONE rebuild
|
|
@@ -44,6 +67,7 @@ export function wireEventStreamRebuild(eventStream, rebuild) {
|
|
|
44
67
|
export async function runBrowse(opts = {}) {
|
|
45
68
|
const source = opts.source ?? localCanvasSource;
|
|
46
69
|
const isRemote = source instanceof RemoteCanvasSource;
|
|
70
|
+
const bindings = resolveUserKeybindings();
|
|
47
71
|
// No TTY → print the static forest and exit 0 (no raw mode).
|
|
48
72
|
if (!process.stdin.isTTY) {
|
|
49
73
|
process.stdout.write(await renderForestFromSource(source) + '\n');
|
|
@@ -103,6 +127,14 @@ export async function runBrowse(opts = {}) {
|
|
|
103
127
|
preview: true, // default ON (decision)
|
|
104
128
|
residentsOnly: true, // default ON: hide one-shot workers (decision)
|
|
105
129
|
pendingClose: null,
|
|
130
|
+
pinned: loadPins(),
|
|
131
|
+
help: false,
|
|
132
|
+
rename: false,
|
|
133
|
+
renameBuf: '',
|
|
134
|
+
renameTarget: null,
|
|
135
|
+
message: false,
|
|
136
|
+
messageBuf: '',
|
|
137
|
+
messageTarget: null,
|
|
106
138
|
};
|
|
107
139
|
let visible = [];
|
|
108
140
|
// The DashboardRow ref the tree holds for a visible id. Enrichment mutates this
|
|
@@ -193,6 +225,7 @@ export async function runBrowse(opts = {}) {
|
|
|
193
225
|
cwdScope: state.cwdScope,
|
|
194
226
|
residentsOnly: state.residentsOnly,
|
|
195
227
|
sort: state.sort,
|
|
228
|
+
pinned: state.pinned,
|
|
196
229
|
});
|
|
197
230
|
if (keepId !== undefined) {
|
|
198
231
|
const idx = visible.findIndex((v) => v.id === keepId);
|
|
@@ -207,7 +240,8 @@ export async function runBrowse(opts = {}) {
|
|
|
207
240
|
const flush = async () => {
|
|
208
241
|
const size = getTerminalSize();
|
|
209
242
|
const previewOn = state.preview && visible.length > 0;
|
|
210
|
-
const
|
|
243
|
+
const inputMode = state.search || state.rename || state.message;
|
|
244
|
+
const viewport = viewportHeight(size.rows, inputMode, previewOn);
|
|
211
245
|
// Keep the cursor inside the viewport window.
|
|
212
246
|
if (state.cursor < state.scrollOffset)
|
|
213
247
|
state.scrollOffset = state.cursor;
|
|
@@ -238,6 +272,9 @@ export async function runBrowse(opts = {}) {
|
|
|
238
272
|
query: state.query, search: state.search, totalNodes,
|
|
239
273
|
cwdScope: state.cwdScope, sort: state.sort, preview: state.preview,
|
|
240
274
|
residentsOnly: state.residentsOnly, pendingClose: state.pendingClose,
|
|
275
|
+
pinned: state.pinned, help: state.help,
|
|
276
|
+
rename: state.rename, renameBuf: state.renameBuf,
|
|
277
|
+
message: state.message, messageBuf: state.messageBuf,
|
|
241
278
|
readOnly: isRemote,
|
|
242
279
|
}, size, caps);
|
|
243
280
|
process.stdout.write(frame);
|
|
@@ -296,6 +333,13 @@ export async function runBrowse(opts = {}) {
|
|
|
296
333
|
n.row.status = m.status; // real post-close status (done / canceled)
|
|
297
334
|
n.row.streaming = false;
|
|
298
335
|
n.row.viewed = false;
|
|
336
|
+
// A finalized node is no longer hanging and owes no answer — clear both so it
|
|
337
|
+
// actually LEAVES the Attention tab (asks>0 || hanging) and stops rendering the
|
|
338
|
+
// stale `⚠ down` rail. Without this, closing a faulted/asking node from the
|
|
339
|
+
// Attention tab looks like a no-op: the row was finalized in the db but its
|
|
340
|
+
// stale fault/ask kept it matching the tab, so it never disappeared.
|
|
341
|
+
n.row.hanging = null;
|
|
342
|
+
n.row.asks = 0;
|
|
299
343
|
}
|
|
300
344
|
// Keep the cursor at the same INDEX (the row that shifts up into the closed
|
|
301
345
|
// slot), not pinned to the now-gone node — recompute() leaves state.cursor
|
|
@@ -375,7 +419,8 @@ export async function runBrowse(opts = {}) {
|
|
|
375
419
|
});
|
|
376
420
|
};
|
|
377
421
|
const onKeySearch = (input, key) => {
|
|
378
|
-
|
|
422
|
+
const action = resolveBrowseSearchAction(bindings, { input, ...key });
|
|
423
|
+
if (action === 'crtr.browse.search.cancel') {
|
|
379
424
|
// Cancel the search: drop the query AND the relevance ranking it switched
|
|
380
425
|
// on, returning to the tree. Synchronous: we're leaving search mode.
|
|
381
426
|
state.search = false;
|
|
@@ -385,7 +430,7 @@ export async function runBrowse(opts = {}) {
|
|
|
385
430
|
scheduleFlush();
|
|
386
431
|
return;
|
|
387
432
|
}
|
|
388
|
-
if (
|
|
433
|
+
if (action === 'crtr.browse.search.commit') {
|
|
389
434
|
// Commit: keep the filter, drop search mode, land on the first match. Must
|
|
390
435
|
// recompute synchronously first so the match index reflects the final query
|
|
391
436
|
// even if a coalesced repaint is still pending.
|
|
@@ -397,17 +442,16 @@ export async function runBrowse(opts = {}) {
|
|
|
397
442
|
scheduleFlush();
|
|
398
443
|
return;
|
|
399
444
|
}
|
|
400
|
-
if (
|
|
445
|
+
if (action === 'crtr.browse.search.delete') {
|
|
401
446
|
state.query = state.query.slice(0, -1);
|
|
402
447
|
scheduleSearchRepaint();
|
|
403
448
|
return;
|
|
404
449
|
}
|
|
405
|
-
//
|
|
406
|
-
if (
|
|
407
|
-
|
|
408
|
-
|
|
450
|
+
// Bound quit acts before text capture; other ctrl-combos are swallowed.
|
|
451
|
+
if (action === 'crtr.browse.search.quit')
|
|
452
|
+
quit();
|
|
453
|
+
if (key.ctrl)
|
|
409
454
|
return;
|
|
410
|
-
}
|
|
411
455
|
// Append every printable char in the chunk. Fast typing batches multiple
|
|
412
456
|
// chars into one stdin read, so we must take them all (not just a length-1
|
|
413
457
|
// chunk) or keystrokes get dropped. Filtering to >= ' ' strips control/escape
|
|
@@ -419,18 +463,114 @@ export async function runBrowse(opts = {}) {
|
|
|
419
463
|
scheduleSearchRepaint();
|
|
420
464
|
}
|
|
421
465
|
};
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
466
|
+
// Recenter the cursor on the launching node (CRTR_NODE_ID), expanding its
|
|
467
|
+
// ancestors so it is actually visible. No-op outside a node or when it's filtered
|
|
468
|
+
// out of the current tab/scope. Same move the boot performs, on demand (`.`).
|
|
469
|
+
const recenterOnSelf = () => {
|
|
470
|
+
const selfId = process.env['CRTR_NODE_ID'];
|
|
471
|
+
if (selfId === undefined || selfId === '' || !tree.nodes.has(selfId))
|
|
472
|
+
return;
|
|
473
|
+
for (let p = tree.nodes.get(selfId)?.parentId ?? null; p !== null; p = tree.nodes.get(p)?.parentId ?? null) {
|
|
474
|
+
state.collapsed.delete(p);
|
|
475
|
+
}
|
|
476
|
+
recompute(selfId);
|
|
477
|
+
};
|
|
478
|
+
// Toggle the pin on a node: mutate the in-memory set, persist best-effort, and
|
|
479
|
+
// recompute so it floats (flat sorts) / (dis)appears from the Pinned tab.
|
|
480
|
+
const togglePin = (id) => {
|
|
481
|
+
if (state.pinned.has(id))
|
|
482
|
+
state.pinned.delete(id);
|
|
483
|
+
else
|
|
484
|
+
state.pinned.add(id);
|
|
485
|
+
savePins(state.pinned);
|
|
486
|
+
recompute(id);
|
|
487
|
+
};
|
|
488
|
+
// Commit an inline rename: `crtr node config <id> --name <name>` (also renames a
|
|
489
|
+
// live viewer window), then reflect the new name into the snapshot in place.
|
|
490
|
+
const commitRename = () => {
|
|
491
|
+
const id = state.renameTarget;
|
|
492
|
+
const name = state.renameBuf.trim();
|
|
493
|
+
state.rename = false;
|
|
494
|
+
if (id !== null && name !== '') {
|
|
495
|
+
try {
|
|
496
|
+
execFileSync('crtr', ['node', 'config', id, '--name', name], { stdio: 'ignore' });
|
|
497
|
+
}
|
|
498
|
+
catch {
|
|
499
|
+
/* best-effort — a rename failure never wedges the picker */
|
|
500
|
+
}
|
|
501
|
+
const n = tree.nodes.get(id);
|
|
502
|
+
if (n !== undefined)
|
|
503
|
+
n.row.name = name;
|
|
504
|
+
}
|
|
505
|
+
recompute(state.renameTarget ?? undefined);
|
|
506
|
+
scheduleFlush();
|
|
507
|
+
};
|
|
508
|
+
// Commit an inline message: `crtr node msg --to <id> <body>` (immediate inbox
|
|
509
|
+
// delivery, best-effort revive if dormant). Fire-and-forget — browse stays put.
|
|
510
|
+
const commitMessage = () => {
|
|
511
|
+
const id = state.messageTarget;
|
|
512
|
+
const body = state.messageBuf.trim();
|
|
513
|
+
state.message = false;
|
|
514
|
+
if (id !== null && body !== '') {
|
|
515
|
+
try {
|
|
516
|
+
execFileSync('crtr', ['node', 'msg', '--to', id, body], { stdio: 'ignore' });
|
|
517
|
+
}
|
|
518
|
+
catch {
|
|
519
|
+
/* best-effort — delivery failure never wedges the picker */
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
scheduleFlush();
|
|
523
|
+
};
|
|
524
|
+
// Shared text-input keystroke handling for the rename/message sub-modes: Esc
|
|
525
|
+
// cancels, Enter commits, Backspace deletes, Ctrl+C quits, printable chars append.
|
|
526
|
+
const onKeyText = (input, key, get, set, commit, cancel) => {
|
|
527
|
+
if (key.escape) {
|
|
528
|
+
cancel();
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
if (key.return) {
|
|
532
|
+
commit();
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
if (key.backspace) {
|
|
536
|
+
set(get().slice(0, -1));
|
|
537
|
+
scheduleFlush();
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
425
540
|
if (key.ctrl) {
|
|
426
541
|
if (input === 'c')
|
|
427
542
|
quit();
|
|
428
543
|
return;
|
|
429
544
|
}
|
|
430
|
-
|
|
431
|
-
|
|
545
|
+
const printable = [...input].filter((c) => c >= ' ').join('');
|
|
546
|
+
if (printable.length > 0) {
|
|
547
|
+
set(get() + printable);
|
|
548
|
+
scheduleFlush();
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
const onKeyRename = (input, key) => onKeyText(input, key, () => state.renameBuf, (s) => { state.renameBuf = s; }, commitRename, () => { state.rename = false; scheduleFlush(); });
|
|
552
|
+
const onKeyMessage = (input, key) => onKeyText(input, key, () => state.messageBuf, (s) => { state.messageBuf = s; }, commitMessage, () => { state.message = false; scheduleFlush(); });
|
|
553
|
+
// Help overlay: any key dismisses (Ctrl+C still quits outright).
|
|
554
|
+
const onKeyHelp = (input, key) => {
|
|
555
|
+
if (key.ctrl && input === 'c') {
|
|
556
|
+
quit();
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
state.help = false;
|
|
560
|
+
scheduleFlush();
|
|
561
|
+
};
|
|
562
|
+
const onKeyNav = (input, key) => {
|
|
563
|
+
const raw = { input, ...key };
|
|
564
|
+
const action = resolveBrowseNavAction(bindings, raw);
|
|
565
|
+
// Unbound ctrl-combos are swallowed so they never masquerade as printable commands.
|
|
566
|
+
if (key.ctrl && action === null)
|
|
567
|
+
return;
|
|
568
|
+
// Preserve an explicitly bound control-combo quit while the close confirmation is open.
|
|
569
|
+
// Other non-confirming input dismisses the modal without also running navigation actions.
|
|
570
|
+
if (state.pendingClose !== null && action === 'crtr.browse.quit' && key.ctrl)
|
|
571
|
+
quit();
|
|
432
572
|
if (state.pendingClose !== null) {
|
|
433
|
-
if (
|
|
573
|
+
if (matchesTerminalInput(bindings, 'crtr.browse.confirm-close', raw)) {
|
|
434
574
|
const target = state.pendingClose;
|
|
435
575
|
void doClose(target).then(() => scheduleFlush()).catch(() => scheduleFlush());
|
|
436
576
|
return;
|
|
@@ -441,32 +581,31 @@ export async function runBrowse(opts = {}) {
|
|
|
441
581
|
return;
|
|
442
582
|
}
|
|
443
583
|
const row = curRow();
|
|
444
|
-
// Quit.
|
|
445
|
-
if (
|
|
584
|
+
// Quit and move.
|
|
585
|
+
if (action === 'crtr.browse.quit')
|
|
446
586
|
quit();
|
|
447
|
-
|
|
448
|
-
if (key.upArrow || input === 'k') {
|
|
587
|
+
if (action === 'crtr.browse.up') {
|
|
449
588
|
state.cursor = Math.max(0, state.cursor - 1);
|
|
450
589
|
scheduleFlush();
|
|
451
590
|
return;
|
|
452
591
|
}
|
|
453
|
-
if (
|
|
592
|
+
if (action === 'crtr.browse.down') {
|
|
454
593
|
state.cursor = Math.max(0, Math.min(visible.length - 1, state.cursor + 1));
|
|
455
594
|
scheduleFlush();
|
|
456
595
|
return;
|
|
457
596
|
}
|
|
458
|
-
if (
|
|
597
|
+
if (action === 'crtr.browse.first') {
|
|
459
598
|
state.cursor = 0;
|
|
460
599
|
scheduleFlush();
|
|
461
600
|
return;
|
|
462
601
|
}
|
|
463
|
-
if (
|
|
602
|
+
if (action === 'crtr.browse.last') {
|
|
464
603
|
state.cursor = Math.max(0, visible.length - 1);
|
|
465
604
|
scheduleFlush();
|
|
466
605
|
return;
|
|
467
606
|
}
|
|
468
607
|
// Expand / descend.
|
|
469
|
-
if (
|
|
608
|
+
if (action === 'crtr.browse.expand-or-child') {
|
|
470
609
|
if (row !== undefined && row.hasChildren) {
|
|
471
610
|
if (!isExpanded(row.id)) {
|
|
472
611
|
state.collapsed.delete(row.id);
|
|
@@ -480,7 +619,7 @@ export async function runBrowse(opts = {}) {
|
|
|
480
619
|
return;
|
|
481
620
|
}
|
|
482
621
|
// Collapse / ascend.
|
|
483
|
-
if (
|
|
622
|
+
if (action === 'crtr.browse.collapse-or-parent') {
|
|
484
623
|
if (row !== undefined && row.hasChildren && isExpanded(row.id)) {
|
|
485
624
|
state.collapsed.add(row.id);
|
|
486
625
|
recompute(row.id);
|
|
@@ -497,7 +636,7 @@ export async function runBrowse(opts = {}) {
|
|
|
497
636
|
return;
|
|
498
637
|
}
|
|
499
638
|
// Toggle collapse.
|
|
500
|
-
if (
|
|
639
|
+
if (action === 'crtr.browse.toggle-fold') {
|
|
501
640
|
if (row !== undefined && row.hasChildren) {
|
|
502
641
|
if (isExpanded(row.id))
|
|
503
642
|
state.collapsed.add(row.id);
|
|
@@ -508,19 +647,20 @@ export async function runBrowse(opts = {}) {
|
|
|
508
647
|
scheduleFlush();
|
|
509
648
|
return;
|
|
510
649
|
}
|
|
511
|
-
// Tabs.
|
|
512
|
-
|
|
650
|
+
// Tabs. next/previous and tab.1–5 (including Pinned) all resolve through the
|
|
651
|
+
// catalog, so each numbered tab is user-remappable like any other binding.
|
|
652
|
+
if (action === 'crtr.browse.tab-next') {
|
|
513
653
|
cycleTab(1);
|
|
514
654
|
scheduleFlush();
|
|
515
655
|
return;
|
|
516
656
|
}
|
|
517
|
-
if (
|
|
657
|
+
if (action === 'crtr.browse.tab-previous') {
|
|
518
658
|
cycleTab(-1);
|
|
519
659
|
scheduleFlush();
|
|
520
660
|
return;
|
|
521
661
|
}
|
|
522
|
-
if (
|
|
523
|
-
const idx = Number(
|
|
662
|
+
if (action?.startsWith('crtr.browse.tab.')) {
|
|
663
|
+
const idx = Number(action.slice(-1)) - 1;
|
|
524
664
|
if (idx < TABS.length) {
|
|
525
665
|
state.tab = TABS[idx];
|
|
526
666
|
state.cursor = 0;
|
|
@@ -533,7 +673,7 @@ export async function runBrowse(opts = {}) {
|
|
|
533
673
|
// Close-out the selected node (+ its exclusive subtree). If anything in that
|
|
534
674
|
// subtree is actively streaming, confirm first (y/n); otherwise close at once.
|
|
535
675
|
// No-op against a remote canvas — mutations are disabled (footer says so).
|
|
536
|
-
if (
|
|
676
|
+
if (action === 'crtr.browse.close-subtree') {
|
|
537
677
|
if (isRemote)
|
|
538
678
|
return;
|
|
539
679
|
if (row === undefined) {
|
|
@@ -549,29 +689,72 @@ export async function runBrowse(opts = {}) {
|
|
|
549
689
|
return;
|
|
550
690
|
}
|
|
551
691
|
// Sort / scope / residents / preview.
|
|
552
|
-
if (
|
|
692
|
+
if (action === 'crtr.browse.sort') {
|
|
553
693
|
cycleSort();
|
|
554
694
|
scheduleFlush();
|
|
555
695
|
return;
|
|
556
696
|
}
|
|
557
|
-
if (
|
|
697
|
+
if (action === 'crtr.browse.scope') {
|
|
558
698
|
toggleScope();
|
|
559
699
|
scheduleFlush();
|
|
560
700
|
return;
|
|
561
701
|
}
|
|
562
|
-
if (
|
|
702
|
+
if (action === 'crtr.browse.residents') {
|
|
563
703
|
toggleResidents();
|
|
564
704
|
scheduleFlush();
|
|
565
705
|
return;
|
|
566
706
|
}
|
|
567
|
-
if (
|
|
707
|
+
if (action === 'crtr.browse.preview') {
|
|
568
708
|
state.preview = !state.preview;
|
|
569
709
|
scheduleFlush();
|
|
570
710
|
return;
|
|
571
711
|
}
|
|
712
|
+
// Help overlay.
|
|
713
|
+
if (input === '?') {
|
|
714
|
+
state.help = true;
|
|
715
|
+
scheduleFlush();
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
// Pin / unpin (local, view-only — works against a remote canvas too).
|
|
719
|
+
if (input === 'f') {
|
|
720
|
+
if (row !== undefined)
|
|
721
|
+
togglePin(row.id);
|
|
722
|
+
scheduleFlush();
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
// Recenter on the launching node.
|
|
726
|
+
if (input === '.') {
|
|
727
|
+
recenterOnSelf();
|
|
728
|
+
scheduleFlush();
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
// Rename the node inline (mutation — no-op on a remote canvas).
|
|
732
|
+
if (input === 'R') {
|
|
733
|
+
if (isRemote)
|
|
734
|
+
return;
|
|
735
|
+
if (row !== undefined) {
|
|
736
|
+
state.rename = true;
|
|
737
|
+
state.renameTarget = row.id;
|
|
738
|
+
state.renameBuf = rowOf(row.id)?.name ?? '';
|
|
739
|
+
}
|
|
740
|
+
scheduleFlush();
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
// Message the node without resuming it (mutation — no-op on a remote canvas).
|
|
744
|
+
if (input === 'm') {
|
|
745
|
+
if (isRemote)
|
|
746
|
+
return;
|
|
747
|
+
if (row !== undefined) {
|
|
748
|
+
state.message = true;
|
|
749
|
+
state.messageTarget = row.id;
|
|
750
|
+
state.messageBuf = '';
|
|
751
|
+
}
|
|
752
|
+
scheduleFlush();
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
572
755
|
// Search. Starting a search ranks by relevance (decision) so the best prompt/
|
|
573
756
|
// name match floats to the top as you type.
|
|
574
|
-
if (
|
|
757
|
+
if (action === 'crtr.browse.search') {
|
|
575
758
|
state.search = true;
|
|
576
759
|
state.query = '';
|
|
577
760
|
state.sort = 'relevance';
|
|
@@ -587,7 +770,7 @@ export async function runBrowse(opts = {}) {
|
|
|
587
770
|
// --now` (browse goes through commands, never touching the runtime directly). 'k' is
|
|
588
771
|
// vim-up here, so the kick is on SHIFT-K.
|
|
589
772
|
// No-op against a remote canvas — mutations are disabled (footer says so).
|
|
590
|
-
if (
|
|
773
|
+
if (action === 'crtr.browse.revive-now') {
|
|
591
774
|
if (isRemote)
|
|
592
775
|
return;
|
|
593
776
|
if (row !== undefined) {
|
|
@@ -600,7 +783,7 @@ export async function runBrowse(opts = {}) {
|
|
|
600
783
|
}
|
|
601
784
|
// Resume. No-op against a remote canvas — mutations/focus are disabled
|
|
602
785
|
// (footer says so).
|
|
603
|
-
if (
|
|
786
|
+
if (action === 'crtr.browse.focus') {
|
|
604
787
|
if (isRemote)
|
|
605
788
|
return;
|
|
606
789
|
if (row !== undefined)
|
|
@@ -664,8 +847,14 @@ export async function runBrowse(opts = {}) {
|
|
|
664
847
|
const onData = (data) => {
|
|
665
848
|
try {
|
|
666
849
|
const { input, key } = parseKeypress(data);
|
|
667
|
-
if (state.
|
|
850
|
+
if (state.help)
|
|
851
|
+
onKeyHelp(input, key);
|
|
852
|
+
else if (state.search)
|
|
668
853
|
onKeySearch(input, key);
|
|
854
|
+
else if (state.rename)
|
|
855
|
+
onKeyRename(input, key);
|
|
856
|
+
else if (state.message)
|
|
857
|
+
onKeyMessage(input, key);
|
|
669
858
|
else
|
|
670
859
|
onKeyNav(input, key);
|
|
671
860
|
}
|
|
@@ -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. */
|