@north-light/crouter 0.3.277 → 0.3.279
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +11 -3
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +11 -3
- package/dist/clients/attach/chrome/status-line.js +1 -22
- package/dist/clients/attach/input/capabilities.js +2 -1
- package/dist/clients/attach/render/chat-view.d.ts +11 -0
- package/dist/clients/attach/render/chat-view.js +23 -4
- package/dist/clients/attach/render/tool-calls.d.ts +6 -1
- package/dist/clients/attach/render/tool-calls.js +47 -3
- package/dist/clients/attach/session/bindings.d.ts +1 -1
- package/dist/clients/attach/session/bindings.js +1 -1
- package/dist/clients/attach/session/chrome-refresh.d.ts +3 -0
- package/dist/clients/attach/session/chrome-refresh.js +3 -3
- package/dist/clients/attach/slash/dispatch.js +42 -19
- package/dist/clients/attach/viewer.js +564 -564
- package/dist/commands/canvas-browse.js +2 -2
- package/dist/commands/node/bash.js +5 -8
- package/dist/core/__tests__/helpers/fleet.d.ts +1 -1
- package/dist/core/__tests__/helpers/fleet.js +5 -0
- package/dist/core/__tests__/helpers/harness.js +5 -1
- package/dist/core/canvas/browse/app.d.ts +1 -1
- package/dist/core/canvas/browse/app.js +32 -7
- package/dist/core/canvas/browse/render.d.ts +17 -2
- package/dist/core/canvas/browse/render.js +342 -242
- package/dist/core/canvas/render-source.d.ts +26 -0
- package/dist/core/canvas/render-source.js +73 -0
- package/dist/core/keybindings/catalog.d.ts +2 -2
- package/dist/core/keybindings/catalog.js +1 -0
- package/dist/core/runtime/busy.d.ts +13 -0
- package/dist/core/runtime/busy.js +43 -1
- package/dist/core/runtime/revive.js +2 -0
- package/dist/core/runtime/tmux-bindings.js +1 -1
- package/dist/daemon/fleet.d.ts +3 -0
- package/dist/daemon/fleet.js +35 -5
- package/dist/daemon/reconcilers/node-lifecycle/respawn-policy.d.ts +14 -0
- package/dist/daemon/reconcilers/node-lifecycle/respawn-policy.js +18 -0
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
|
@@ -23,14 +23,14 @@ export const browseLeaf = defineLeaf({
|
|
|
23
23
|
name: 'return-pane',
|
|
24
24
|
type: 'string',
|
|
25
25
|
required: false,
|
|
26
|
-
constraint: 'tmux pane id
|
|
26
|
+
constraint: 'tmux pane id the chosen node REPLACES on Enter — that pane is respawned onto its viewer (set by the /resume popup, so resuming swaps the pane you invoked it from). Default: focus into this pane without replacing it.',
|
|
27
27
|
},
|
|
28
28
|
{
|
|
29
29
|
kind: 'flag',
|
|
30
30
|
name: 'cwd',
|
|
31
31
|
type: 'string',
|
|
32
32
|
required: false,
|
|
33
|
-
constraint: 'directory used when you toggle the optional cwd filter (c). The navigator opens with All dirs; /resume
|
|
33
|
+
constraint: 'directory used when you toggle the optional cwd filter (c). The navigator opens with All dirs; /resume passes the launching node\'s cwd. Default: the invocation cwd.',
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
36
|
kind: 'flag',
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import { envNodeId } from '../../shared/env.js';
|
|
3
3
|
import { defineBranch, defineLeaf } from '../../core/command.js';
|
|
4
4
|
import { InputError } from '../../core/io.js';
|
|
5
|
-
import { displayMessage } from '../../core/runtime/tmux-chrome.js';
|
|
6
5
|
import { contextDir } from '../../core/canvas/paths.js';
|
|
7
6
|
import { existsSync } from 'node:fs';
|
|
8
7
|
import { activeBackgroundBashJobs, backgroundRunningBashJobs, bashJobCommandLine, bashJobPaths, clearJobDeadline, formatBashElapsed, isSafeBashJobId, jobStartedAtMs, readJobPgid, stopBackgroundBashJob, tailJobLog, writeJobDeadlineMs } from '../../core/bash-jobs.js';
|
|
@@ -41,19 +40,17 @@ const nodeBashBackground = defineLeaf({
|
|
|
41
40
|
effects: ['Signals every foreground bash command currently running for the node to return from its tool call immediately while its detached supervisor keeps the command running.', 'Each handed-off command retains its log under the node context jobs directory and sends the node an urgent inbox message when it exits, which steers the node mid-turn.'],
|
|
42
41
|
},
|
|
43
42
|
run: async (input) => {
|
|
44
|
-
const { id
|
|
43
|
+
const { id } = resolveBashTarget(input, 'no node found for the bash handoff');
|
|
45
44
|
if ((await getNodeOrNull(id)) === null) {
|
|
46
45
|
throw new InputError({ error: 'not_found', message: `no node: ${id}`, next: 'List nodes with `crtr node inspect list`.' });
|
|
47
46
|
}
|
|
48
47
|
const jobs = backgroundRunningBashJobs(contextDir(id));
|
|
49
48
|
const now = Date.now();
|
|
50
49
|
const elapsed = jobs.map((job) => ({ job_id: job.jobId, elapsed_ms: now - (jobStartedAtMs(job.jobId) ?? now) }));
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
: `crtr: bash handed off (${jobs.length})`;
|
|
56
|
-
displayMessage(toast, pane);
|
|
50
|
+
// No tmux toast. A tmux status-line message commandeers the whole client's
|
|
51
|
+
// status bar for seconds over an action the user just took deliberately;
|
|
52
|
+
// the viewer reports its own handoff in its own status line, which is where
|
|
53
|
+
// someone watching that pane is already looking.
|
|
57
54
|
return { node_id: id, jobs: elapsed };
|
|
58
55
|
},
|
|
59
56
|
render: (result) => {
|
|
@@ -9,4 +9,4 @@ export interface TestFleet {
|
|
|
9
9
|
* process's fleet. Call in `before`/`beforeEach`; `bindFleet` throws on a
|
|
10
10
|
* conflicting rebind, so the previous binding is reset first (same idiom as
|
|
11
11
|
* resetEventSourceForTests). Pass `now` to inject the throttle clock. */
|
|
12
|
-
export declare function bindTestFleet(opts?: Partial<Pick<DaemonFleetOpts, 'epoch' | 'now'>>): TestFleet;
|
|
12
|
+
export declare function bindTestFleet(opts?: Partial<Pick<DaemonFleetOpts, 'epoch' | 'now' | 'probeOnline'>>): TestFleet;
|
|
@@ -30,6 +30,11 @@ export function bindTestFleet(opts = {}) {
|
|
|
30
30
|
});
|
|
31
31
|
},
|
|
32
32
|
...(opts.now !== undefined ? { now: opts.now } : {}),
|
|
33
|
+
// Respawn policy consults network reachability — an offline box parks a
|
|
34
|
+
// node rather than spending its respawn budget. Default it online so tests
|
|
35
|
+
// stay deterministic off the network; pass `probeOnline` to drive the
|
|
36
|
+
// outage branch.
|
|
37
|
+
probeOnline: opts.probeOnline ?? (async () => true),
|
|
33
38
|
});
|
|
34
39
|
bindFleet(fleet);
|
|
35
40
|
return {
|
|
@@ -252,7 +252,11 @@ export async function createHarness(opts = {}) {
|
|
|
252
252
|
await tail;
|
|
253
253
|
} while (tail !== laneTail);
|
|
254
254
|
}
|
|
255
|
-
|
|
255
|
+
// Respawn policy probes network reachability (an offline box parks a node
|
|
256
|
+
// instead of spending its respawn budget). Reachability is an environment
|
|
257
|
+
// condition, not runtime behavior under test — pin it online so a sandboxed
|
|
258
|
+
// or offline runner exercises the same recovery path a connected one does.
|
|
259
|
+
const fleet = new DaemonFleet({ epoch: 'harness-epoch', enqueue: enqueueLaneJob, probeOnline: async () => true });
|
|
256
260
|
bindFleet(fleet);
|
|
257
261
|
// Neutralize ensureDaemon (spawn.ts calls it on every `node new`): write a
|
|
258
262
|
// fake-live-daemon pidfile pointing at OUR (always-alive) pid, so
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { CanvasSource } from '../source.js';
|
|
2
2
|
import { type BindingId, type BindingResolution, type RawTerminalInput } from '../../keybindings/index.js';
|
|
3
|
-
declare const BROWSE_NAV_ACTIONS: readonly ["crtr.browse.quit", "crtr.browse.down", "crtr.browse.up", "crtr.browse.first", "crtr.browse.last", "crtr.browse.expand-or-child", "crtr.browse.collapse-or-parent", "crtr.browse.toggle-fold", "crtr.browse.tab-next", "crtr.browse.tab-previous", "crtr.browse.tab.1", "crtr.browse.tab.2", "crtr.browse.tab.3", "crtr.browse.tab.4", "crtr.browse.tab.5", "crtr.browse.close-subtree", "crtr.browse.sort", "crtr.browse.scope", "crtr.browse.profile", "crtr.browse.residents", "crtr.browse.preview", "crtr.browse.pin", "crtr.browse.search", "crtr.browse.revive-now", "crtr.browse.focus", "crtr.browse.focus-background", "crtr.browse.yank-id"];
|
|
3
|
+
declare const BROWSE_NAV_ACTIONS: readonly ["crtr.browse.quit", "crtr.browse.down", "crtr.browse.up", "crtr.browse.first", "crtr.browse.last", "crtr.browse.expand-or-child", "crtr.browse.collapse-or-parent", "crtr.browse.toggle-fold", "crtr.browse.tab-next", "crtr.browse.tab-previous", "crtr.browse.tab.1", "crtr.browse.tab.2", "crtr.browse.tab.3", "crtr.browse.tab.4", "crtr.browse.tab.5", "crtr.browse.close-subtree", "crtr.browse.sort", "crtr.browse.scope", "crtr.browse.profile", "crtr.browse.residents", "crtr.browse.preview", "crtr.browse.report", "crtr.browse.pin", "crtr.browse.search", "crtr.browse.revive-now", "crtr.browse.focus", "crtr.browse.focus-background", "crtr.browse.yank-id"];
|
|
4
4
|
declare const BROWSE_SEARCH_ACTIONS: readonly ["crtr.browse.search.cancel", "crtr.browse.search.commit", "crtr.browse.search.delete", "crtr.browse.search.quit"];
|
|
5
5
|
export declare function resolveBrowseNavAction(bindings: BindingResolution<BindingId>, raw: RawTerminalInput): (typeof BROWSE_NAV_ACTIONS)[number] | null;
|
|
6
6
|
export declare function resolveBrowseSearchAction(bindings: BindingResolution<BindingId>, raw: RawTerminalInput): (typeof BROWSE_SEARCH_ACTIONS)[number] | null;
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { envNodeId } from '../../../shared/env.js';
|
|
11
11
|
import { execFileSync } from 'node:child_process';
|
|
12
12
|
import { resolve } from 'node:path';
|
|
13
|
-
import { dashboardRowsAllFromSource, enrichRowsFromSource, loadPreviewFromSource, renderForestFromSource } from '../render-source.js';
|
|
13
|
+
import { dashboardRowsAllFromSource, enrichRowsFromSource, loadPreviewFromSource, loadTranscriptFromSource, renderForestFromSource } from '../render-source.js';
|
|
14
14
|
import { fullName } from '../labels.js';
|
|
15
15
|
import { setupTerminal, restoreTerminal, getTerminalSize, parseKeypress, } from '../../tui/terminal.js';
|
|
16
16
|
import { buildTree, flatten, pruneNode, TABS } from './model.js';
|
|
@@ -22,7 +22,7 @@ const BROWSE_NAV_ACTIONS = [
|
|
|
22
22
|
'crtr.browse.tab-next', 'crtr.browse.tab-previous', 'crtr.browse.tab.1', 'crtr.browse.tab.2',
|
|
23
23
|
'crtr.browse.tab.3', 'crtr.browse.tab.4', 'crtr.browse.tab.5', 'crtr.browse.close-subtree', 'crtr.browse.sort',
|
|
24
24
|
'crtr.browse.scope', 'crtr.browse.profile', 'crtr.browse.residents', 'crtr.browse.preview',
|
|
25
|
-
'crtr.browse.pin', 'crtr.browse.search',
|
|
25
|
+
'crtr.browse.report', 'crtr.browse.pin', 'crtr.browse.search',
|
|
26
26
|
'crtr.browse.revive-now', 'crtr.browse.focus', 'crtr.browse.focus-background', 'crtr.browse.yank-id',
|
|
27
27
|
];
|
|
28
28
|
const BROWSE_SEARCH_ACTIONS = [
|
|
@@ -120,7 +120,8 @@ export async function runBrowse(opts) {
|
|
|
120
120
|
cwdScope: null, // default: All dirs
|
|
121
121
|
profileScope: launchProfileId,
|
|
122
122
|
sort: 'attention', // default: attention ordering (attached/streaming/live first)
|
|
123
|
-
preview:
|
|
123
|
+
preview: true, // default ON — the hovered node's conversation is the point
|
|
124
|
+
showReport: false,
|
|
124
125
|
residentsOnly: true, // default ON: hide one-shot workers (decision)
|
|
125
126
|
pendingClose: null,
|
|
126
127
|
flash: null,
|
|
@@ -168,12 +169,24 @@ export async function runBrowse(opts) {
|
|
|
168
169
|
/* best-effort — the SIGTERM is what matters; the daemon owns the resume */
|
|
169
170
|
}
|
|
170
171
|
};
|
|
171
|
-
/** Open the chosen node — the ONLY sanctioned path (reviveNode via surface node focus).
|
|
172
|
+
/** Open the chosen node — the ONLY sanctioned path (reviveNode via surface node focus).
|
|
173
|
+
* With a return pane (the `/resume` popup), the chosen node REPLACES that pane:
|
|
174
|
+
* `--in-place` respawns it onto the node, so resuming swaps the viewer you came
|
|
175
|
+
* from instead of splitting another one beside it. The respawn kills that pane's
|
|
176
|
+
* process tree, so it is dispatched through `tmux run-shell -b` — the tmux server
|
|
177
|
+
* context — and never as a child of this popup, which the same teardown can take
|
|
178
|
+
* with it mid-call. Without a return pane, the caller's own pane hosts the viewer
|
|
179
|
+
* as before. */
|
|
172
180
|
const selectAndFocus = (id) => {
|
|
173
181
|
cleanup();
|
|
174
|
-
const
|
|
182
|
+
const returnPane = opts.returnPane !== undefined && opts.returnPane !== '' ? opts.returnPane : undefined;
|
|
175
183
|
try {
|
|
176
|
-
|
|
184
|
+
if (returnPane !== undefined) {
|
|
185
|
+
execFileSync('tmux', ['run-shell', '-b', `crtr surface node focus ${id} --pane ${returnPane} --in-place >/dev/null 2>&1`], { stdio: 'ignore' });
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
execFileSync('crtr', ['surface', 'node', 'focus', id], { stdio: 'inherit' });
|
|
189
|
+
}
|
|
177
190
|
}
|
|
178
191
|
catch {
|
|
179
192
|
// `surface node focus` swaps panes out from under us; a sync call can be
|
|
@@ -244,13 +257,20 @@ export async function runBrowse(opts) {
|
|
|
244
257
|
const cur = curRow();
|
|
245
258
|
if (cur !== undefined) {
|
|
246
259
|
const r = rowOf(cur.id);
|
|
247
|
-
if (r !== undefined)
|
|
260
|
+
if (r !== undefined) {
|
|
248
261
|
await loadPreviewFromSource(source, r);
|
|
262
|
+
// The chat transcript is the side panel's body, so it is read for the
|
|
263
|
+
// SELECTED row only — never on the background warm pass, where one ordered
|
|
264
|
+
// transcript per node would hold the whole canvas's conversations in memory.
|
|
265
|
+
if (previewLayout(size.cols, previewOn).mode === 'side')
|
|
266
|
+
await loadTranscriptFromSource(source, r);
|
|
267
|
+
}
|
|
249
268
|
}
|
|
250
269
|
const frame = renderFrame({
|
|
251
270
|
tree, visible, tab: state.tab, cursor: state.cursor, scrollOffset: state.scrollOffset,
|
|
252
271
|
query: state.query, search: state.search, totalNodes,
|
|
253
272
|
cwdScope: state.cwdScope, profileScope: state.profileScope, sort: state.sort, preview: state.preview,
|
|
273
|
+
showReport: state.showReport,
|
|
254
274
|
residentsOnly: state.residentsOnly, pendingClose: state.pendingClose, flash: state.flash,
|
|
255
275
|
pinned: state.pinned, help: state.help,
|
|
256
276
|
rename: state.rename, renameBuf: state.renameBuf,
|
|
@@ -704,6 +724,11 @@ export async function runBrowse(opts) {
|
|
|
704
724
|
scheduleFlush();
|
|
705
725
|
return;
|
|
706
726
|
}
|
|
727
|
+
if (action === 'crtr.browse.report') {
|
|
728
|
+
state.showReport = !state.showReport;
|
|
729
|
+
scheduleFlush();
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
707
732
|
// Help overlay.
|
|
708
733
|
if (input === '?') {
|
|
709
734
|
state.help = true;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import type { TranscriptTurn } from '../render-source.js';
|
|
1
2
|
import type { Tab, Tree, VisibleRow, SortMode } from './model.js';
|
|
2
|
-
import { detectColorCaps, type ColorCaps } from '../../tui/draw.js';
|
|
3
|
+
import { detectColorCaps, type Span, type ColorCaps } from '../../tui/draw.js';
|
|
3
4
|
export { detectColorCaps };
|
|
4
5
|
export type { ColorCaps };
|
|
5
6
|
export declare const RECAP_LINES = 2;
|
|
@@ -33,8 +34,12 @@ export interface RenderState {
|
|
|
33
34
|
profileScope: string | undefined;
|
|
34
35
|
/** Active ordering — status line + (flat modes) row presentation. */
|
|
35
36
|
sort: SortMode;
|
|
36
|
-
/** Whether the
|
|
37
|
+
/** Whether the preview panel is drawn. */
|
|
37
38
|
preview: boolean;
|
|
39
|
+
/** Side panel shows the node's newest report instead of the goal/chat/state
|
|
40
|
+
* briefing. Ignored while a search query is live (the transcript owns the panel)
|
|
41
|
+
* and for a node that has pushed nothing. */
|
|
42
|
+
showReport?: boolean;
|
|
38
43
|
/** Lifecycle filter: when true, `terminal` (one-shot worker) nodes are hidden
|
|
39
44
|
* — surfaced as a status-line cue. */
|
|
40
45
|
residentsOnly: boolean;
|
|
@@ -72,6 +77,16 @@ export declare function previewAllocation(hasRecap: boolean, hasReport: boolean)
|
|
|
72
77
|
reply: number;
|
|
73
78
|
report: number;
|
|
74
79
|
};
|
|
80
|
+
/** Re-style every case-insensitive occurrence of `query` inside already-rendered
|
|
81
|
+
* spans, splitting runs at the match edges. Working on the RENDERED text is what
|
|
82
|
+
* lets a transcript keep its markdown and still highlight: the markup is already
|
|
83
|
+
* gone, so no column can drift out from under a highlight. */
|
|
84
|
+
export declare function highlightSpans(spans: Span[], query: string): {
|
|
85
|
+
spans: Span[];
|
|
86
|
+
hits: number;
|
|
87
|
+
};
|
|
88
|
+
/** How many turns contain the query — the count the section rule advertises. */
|
|
89
|
+
export declare function transcriptHitTurns(turns: readonly TranscriptTurn[], query: string): number;
|
|
75
90
|
/**
|
|
76
91
|
* Render the whole frame. Returns a single string that, written as-is, repaints
|
|
77
92
|
* the screen in place. `caps` gates all hue (defaults to a no-color frame so
|