@north-light/crouter 0.3.200 → 0.3.201
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/api/client.d.ts +2 -1
- package/dist/api/client.js +3 -0
- package/dist/api/dto/inbox.d.ts +1 -1
- package/dist/api/dto/nodes.d.ts +16 -0
- package/dist/clients/attach/ansi-cells.d.ts +13 -4
- package/dist/clients/attach/ansi-cells.js +16 -10
- package/dist/clients/attach/overlays/help.js +6 -0
- package/dist/clients/attach/render/transcript-search.d.ts +28 -0
- package/dist/clients/attach/render/transcript-search.js +153 -0
- package/dist/clients/attach/render/viewport.d.ts +10 -1
- package/dist/clients/attach/render/viewport.js +27 -2
- package/dist/clients/attach/session/frame.d.ts +2 -1
- package/dist/clients/attach/session/frame.js +9 -0
- package/dist/clients/attach/session/keys.d.ts +5 -0
- package/dist/clients/attach/session/keys.js +3 -0
- package/dist/clients/attach/session/layout.d.ts +2 -2
- package/dist/clients/attach/session/layout.js +3 -2
- package/dist/clients/attach/session/transcript-search.d.ts +20 -0
- package/dist/clients/attach/session/transcript-search.js +151 -0
- package/dist/clients/attach/viewer.js +742 -740
- package/dist/core/__tests__/seam/dormancy-release.test.js +2 -0
- package/dist/core/human/scan.d.ts +1 -3
- package/dist/core/human/scan.js +3 -3
- package/dist/core/human/types.d.ts +1 -0
- package/dist/core/keybindings/attach-control.d.ts +3 -0
- package/dist/core/keybindings/attach-control.js +1 -0
- package/dist/core/keybindings/catalog.d.ts +2 -2
- package/dist/core/keybindings/catalog.js +6 -0
- package/dist/core/runtime/node-read.d.ts +11 -0
- package/dist/core/runtime/node-read.js +107 -41
- package/dist/daemon/api/handlers/inbox.js +1 -0
- package/dist/daemon/api/handlers/nodes.js +34 -1
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
|
@@ -93,6 +93,7 @@ test('idle-release settles its real watcher before exit; daemon revives exactly
|
|
|
93
93
|
// settles its own prompt lifecycle: watcher → engine → stophook → settle →
|
|
94
94
|
// broker exit. No test-driven turn or settlement dispatch is involved.
|
|
95
95
|
await awaitReleased(manager);
|
|
96
|
+
await h.awaitFleetExit(manager);
|
|
96
97
|
assert.equal(readCursor(manager), first.entry_id, 'the exact handed-off row commits before clean shutdown');
|
|
97
98
|
const cyclesAfterRelease = h.node(manager).cycles ?? 0;
|
|
98
99
|
await h.tick();
|
|
@@ -112,6 +113,7 @@ test('idle-release settles its real watcher before exit; daemon revives exactly
|
|
|
112
113
|
await h.awaitBoot(manager, { minCount: 2 });
|
|
113
114
|
await h.awaitWake(manager, { sinceCount: injectedBeforeSecond, match: /second completed/ });
|
|
114
115
|
await awaitReleased(manager);
|
|
116
|
+
await h.awaitFleetExit(manager);
|
|
115
117
|
assert.equal(readCursor(manager), second.entry_id, 'the later row commits at its own natural idle release');
|
|
116
118
|
await h.tick();
|
|
117
119
|
assert.equal(h.node(manager).cycles ?? 0, cyclesAfterRelease + 1, 'the settled later row does not cause a duplicate revival');
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import type { PageTicketSummary, TicketSummary } from './types.js';
|
|
2
2
|
/** Read all unresolved page/review tickets, newest first. Progress never affects visibility. */
|
|
3
3
|
export declare function scanInbox(): TicketSummary[];
|
|
4
|
-
export
|
|
5
|
-
state: 'pending' | 'resolved' | 'canceled' | 'passive';
|
|
6
|
-
}
|
|
4
|
+
export type PageHistoryEntry = PageTicketSummary;
|
|
7
5
|
/**
|
|
8
6
|
* Every page ticket raised by one node — pending, resolved or canceled — oldest
|
|
9
7
|
* first. This is the durable discovery seam for transcript placement, so it is a
|
package/dist/core/human/scan.js
CHANGED
|
@@ -56,6 +56,7 @@ function pageSummary(dir, id) {
|
|
|
56
56
|
awaitsResponse: manifest.delivery.reply,
|
|
57
57
|
source: manifest.source ?? {},
|
|
58
58
|
emittedAt,
|
|
59
|
+
state: pageTicketState(dir),
|
|
59
60
|
claim: claimSummary(dir),
|
|
60
61
|
};
|
|
61
62
|
}
|
|
@@ -112,9 +113,8 @@ export function scanPageHistory(nodeId) {
|
|
|
112
113
|
const summary = pageSummary(dir, basename(dir));
|
|
113
114
|
if (summary === null || summary.source.nodeId !== nodeId)
|
|
114
115
|
continue;
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
items.push({ ...summary, state, ...(result?.kind === 'page' ? { answerDigest: result.summary.replace(/\s+/g, ' ').trim() } : {}) });
|
|
116
|
+
const result = summary.state === 'resolved' ? readTicketResult(dir) : null;
|
|
117
|
+
items.push({ ...summary, ...(result?.kind === 'page' ? { answerDigest: result.summary.replace(/\s+/g, ' ').trim() } : {}) });
|
|
118
118
|
}
|
|
119
119
|
return items.sort((a, b) => Date.parse(a.emittedAt) - Date.parse(b.emittedAt) || a.id.localeCompare(b.id));
|
|
120
120
|
}
|
|
@@ -65,6 +65,7 @@ export interface PageTicketSummary {
|
|
|
65
65
|
steps: number;
|
|
66
66
|
slotKinds: string[];
|
|
67
67
|
awaitsResponse: boolean;
|
|
68
|
+
state: 'pending' | 'resolved' | 'canceled' | 'passive';
|
|
68
69
|
}
|
|
69
70
|
/** The only pending-ticket shape scanners expose. */
|
|
70
71
|
export type TicketSummary = ReviewTicketSummary | PageTicketSummary;
|
|
@@ -28,6 +28,9 @@ export declare const ATTACH_CONTROL_BINDINGS: readonly [{
|
|
|
28
28
|
}, {
|
|
29
29
|
readonly menuId: "crtr.tmux.menu.attach.profile-files";
|
|
30
30
|
readonly actionId: "crtr.attach.profile-files";
|
|
31
|
+
}, {
|
|
32
|
+
readonly menuId: "crtr.tmux.menu.attach.search";
|
|
33
|
+
readonly actionId: "crtr.attach.search";
|
|
31
34
|
}, {
|
|
32
35
|
readonly menuId: "crtr.tmux.menu.attach.whip";
|
|
33
36
|
readonly actionId: "crtr.attach.whip";
|
|
@@ -9,6 +9,7 @@ export const ATTACH_CONTROL_BINDINGS = [
|
|
|
9
9
|
{ menuId: 'crtr.tmux.menu.attach.command.inspect', actionId: 'crtr.attach.command.inspect' },
|
|
10
10
|
{ menuId: 'crtr.tmux.menu.attach.file-review', actionId: 'crtr.attach.file-review' },
|
|
11
11
|
{ menuId: 'crtr.tmux.menu.attach.profile-files', actionId: 'crtr.attach.profile-files' },
|
|
12
|
+
{ menuId: 'crtr.tmux.menu.attach.search', actionId: 'crtr.attach.search' },
|
|
12
13
|
{ menuId: 'crtr.tmux.menu.attach.whip', actionId: 'crtr.attach.whip' },
|
|
13
14
|
];
|
|
14
15
|
const ATTACH_CONTROL_PREFIX = '\x1b_crtr;';
|
|
@@ -8,8 +8,8 @@ type ProfileMenuAction = 'up' | 'down' | 'select' | 'accept-default' | 'abort' |
|
|
|
8
8
|
type SetupAction = 'cancel' | 'tab-next' | 'tab-previous' | 'open' | 'list-down' | 'list-up' | 'toggle' | 'details' | 'advance' | 'cell-previous' | 'cell-next';
|
|
9
9
|
type SetupKeybindingAction = 'edit' | 'commit-edit' | 'cancel-edit' | 'unbind' | 'reset-row' | 'reset-all' | 'save' | 'discard' | 'page-up' | 'page-down' | 'filter' | 'filter-delete' | 'filter-clear';
|
|
10
10
|
type SetupModelsAction = 'open' | 'commit' | 'cancel' | 'scope-next';
|
|
11
|
-
export type BindingId = `crtr.tmux.menu.${TmuxMenuAction}` | `crtr.tmux.menu.issue.${'idea' | 'bug'}` | AttachMenuBindingId | `crtr.tmux.menu-global.${TmuxMenuEntryAction}` | `crtr.tmux.node.${'next' | 'previous'}` | 'crtr.tmux.inbox.toggle' | `crtr.attach.scroll.${'up' | 'down' | 'page-up' | 'page-down' | 'live'}` | 'crtr.attach.mouse.toggle' | 'crtr.attach.copy.message' | 'crtr.attach.help.toggle' | AttachBindingId | `crtr.graph.${GraphAction}` | `crtr.graph.confirm.${'accept' | 'cancel'}` | `crtr.inspector.${'force-quit' | 'down' | 'up' | 'section-next' | 'section-prev' | 'page-down' | 'page-up' | 'toggle-detail' | 'cancel-item' | 'open-log' | 'confirm' | 'dismiss' | 'retarget' | 'refresh' | 'quit'}` | `crtr.browse.${BrowseAction}` | `crtr.browse.search.${'cancel' | 'commit' | 'delete' | 'quit'}` | `crtr.profile.menu.${ProfileMenuAction}` | `crtr.profile.search.${'up' | 'down' | 'pick' | 'back' | 'delete' | 'abort'}` | `crtr.setup.${SetupAction}` | `crtr.setup.confirm.${'accept' | 'back'}` | `crtr.setup.keybindings.${SetupKeybindingAction}` | `crtr.setup.models.${SetupModelsAction}`;
|
|
12
|
-
export declare const BINDING_CATALOG: readonly [BindingDefinition<"crtr.tmux.menu.open">, BindingDefinition<"crtr.tmux.menu.attach">, ...(BindingDefinition<BindingId> | BindingDefinition<"crtr.tmux.node.next"> | BindingDefinition<"crtr.tmux.node.previous"> | BindingDefinition<"crtr.tmux.inbox.toggle"> | BindingDefinition<"crtr.attach.scroll.up"> | BindingDefinition<"crtr.attach.scroll.down"> | BindingDefinition<"crtr.attach.scroll.page-up"> | BindingDefinition<"crtr.attach.scroll.page-down"> | BindingDefinition<"crtr.attach.scroll.live"> | BindingDefinition<"crtr.attach.mouse.toggle"> | BindingDefinition<"crtr.attach.copy.message"> | BindingDefinition<"crtr.graph.down"> | BindingDefinition<"crtr.graph.up"> | BindingDefinition<"crtr.graph.first"> | BindingDefinition<"crtr.graph.last"> | BindingDefinition<"crtr.graph.collapse-or-manager"> | BindingDefinition<"crtr.graph.expand-or-child"> | BindingDefinition<"crtr.graph.focus"> | BindingDefinition<"crtr.graph.revive"> | BindingDefinition<"crtr.graph.focus-manager"> | BindingDefinition<"crtr.graph.close-subtree"> | BindingDefinition<"crtr.graph.dismiss"> | BindingDefinition<"crtr.graph.confirm.accept"> | BindingDefinition<"crtr.graph.confirm.cancel"> | BindingDefinition<"crtr.inspector.force-quit"> | BindingDefinition<"crtr.inspector.down"> | BindingDefinition<"crtr.inspector.up"> | BindingDefinition<"crtr.inspector.section-next"> | BindingDefinition<"crtr.inspector.section-prev"> | BindingDefinition<"crtr.inspector.page-down"> | BindingDefinition<"crtr.inspector.page-up"> | BindingDefinition<"crtr.inspector.toggle-detail"> | BindingDefinition<"crtr.inspector.cancel-item"> | BindingDefinition<"crtr.inspector.open-log"> | BindingDefinition<"crtr.inspector.confirm"> | BindingDefinition<"crtr.inspector.dismiss"> | BindingDefinition<"crtr.inspector.retarget"> | BindingDefinition<"crtr.inspector.refresh"> | BindingDefinition<"crtr.inspector.quit"> | BindingDefinition<"crtr.browse.quit"> | BindingDefinition<"crtr.browse.down"> | BindingDefinition<"crtr.browse.up"> | BindingDefinition<"crtr.browse.first"> | BindingDefinition<"crtr.browse.last"> | BindingDefinition<"crtr.browse.expand-or-child"> | BindingDefinition<"crtr.browse.collapse-or-parent"> | BindingDefinition<"crtr.browse.toggle-fold"> | BindingDefinition<"crtr.browse.tab-next"> | BindingDefinition<"crtr.browse.tab-previous"> | BindingDefinition<"crtr.browse.tab.2" | "crtr.browse.tab.1" | "crtr.browse.tab.3" | "crtr.browse.tab.4" | "crtr.browse.tab.5"> | BindingDefinition<"crtr.browse.close-subtree"> | BindingDefinition<"crtr.browse.confirm-close"> | BindingDefinition<"crtr.browse.sort"> | BindingDefinition<"crtr.browse.scope"> | BindingDefinition<"crtr.browse.profile"> | BindingDefinition<"crtr.browse.residents"> | BindingDefinition<"crtr.browse.preview"> | BindingDefinition<"crtr.browse.search"> | BindingDefinition<"crtr.browse.revive-now"> | BindingDefinition<"crtr.browse.focus"> | BindingDefinition<"crtr.browse.focus-background"> | BindingDefinition<"crtr.browse.yank-id"> | BindingDefinition<"crtr.browse.search.cancel"> | BindingDefinition<"crtr.browse.search.commit"> | BindingDefinition<"crtr.browse.search.delete"> | BindingDefinition<"crtr.browse.search.quit"> | BindingDefinition<"crtr.profile.menu.up"> | BindingDefinition<"crtr.profile.menu.down"> | BindingDefinition<"crtr.profile.menu.select"> | BindingDefinition<"crtr.profile.menu.accept-default"> | BindingDefinition<"crtr.profile.menu.abort"> | BindingDefinition<"crtr.profile.menu.slot.2" | "crtr.profile.menu.slot.1" | "crtr.profile.menu.slot.3" | "crtr.profile.menu.slot.4" | "crtr.profile.menu.slot.5" | "crtr.profile.menu.slot.6" | "crtr.profile.menu.slot.7" | "crtr.profile.menu.slot.8" | "crtr.profile.menu.slot.9">)[], BindingDefinition<"crtr.profile.menu.create">, BindingDefinition<"crtr.profile.menu.search-existing">, BindingDefinition<"crtr.profile.menu.use-root">, BindingDefinition<"crtr.profile.menu.set-default">, BindingDefinition<"crtr.profile.menu.add-directory">, BindingDefinition<"crtr.profile.menu.unpin">, BindingDefinition<"crtr.profile.search.up">, BindingDefinition<"crtr.profile.search.down">, BindingDefinition<"crtr.profile.search.pick">, BindingDefinition<"crtr.profile.search.back">, BindingDefinition<"crtr.profile.search.delete">, BindingDefinition<"crtr.profile.search.abort">, BindingDefinition<"crtr.setup.cancel">, BindingDefinition<"crtr.setup.tab-next">, BindingDefinition<"crtr.setup.tab-previous">, BindingDefinition<"crtr.setup.open">, BindingDefinition<"crtr.setup.list-down">, BindingDefinition<"crtr.setup.list-up">, BindingDefinition<"crtr.setup.toggle">, BindingDefinition<"crtr.setup.details">, BindingDefinition<"crtr.setup.advance">, BindingDefinition<"crtr.setup.confirm.accept">, BindingDefinition<"crtr.setup.confirm.back">, BindingDefinition<"crtr.setup.keybindings.edit">, BindingDefinition<"crtr.setup.keybindings.commit-edit">, BindingDefinition<"crtr.setup.keybindings.cancel-edit">, BindingDefinition<"crtr.setup.keybindings.unbind">, BindingDefinition<"crtr.setup.keybindings.reset-row">, BindingDefinition<"crtr.setup.keybindings.reset-all">, BindingDefinition<"crtr.setup.keybindings.save">, BindingDefinition<"crtr.setup.keybindings.discard">, BindingDefinition<"crtr.setup.keybindings.page-up">, BindingDefinition<"crtr.setup.keybindings.page-down">, BindingDefinition<"crtr.setup.keybindings.filter">, BindingDefinition<"crtr.setup.keybindings.filter-delete">, BindingDefinition<"crtr.setup.keybindings.filter-clear">, BindingDefinition<"crtr.setup.cell-previous">, BindingDefinition<"crtr.setup.cell-next">, BindingDefinition<"crtr.setup.models.open">, BindingDefinition<"crtr.setup.models.scope-next">, BindingDefinition<"crtr.setup.models.commit">, BindingDefinition<"crtr.setup.models.cancel">];
|
|
11
|
+
export type BindingId = `crtr.tmux.menu.${TmuxMenuAction}` | `crtr.tmux.menu.issue.${'idea' | 'bug'}` | AttachMenuBindingId | `crtr.tmux.menu-global.${TmuxMenuEntryAction}` | `crtr.tmux.node.${'next' | 'previous'}` | 'crtr.tmux.inbox.toggle' | `crtr.attach.scroll.${'up' | 'down' | 'page-up' | 'page-down' | 'live'}` | 'crtr.attach.mouse.toggle' | 'crtr.attach.copy.message' | 'crtr.attach.help.toggle' | `crtr.attach.search.${'next' | 'previous' | 'edit' | 'close'}` | AttachBindingId | `crtr.graph.${GraphAction}` | `crtr.graph.confirm.${'accept' | 'cancel'}` | `crtr.inspector.${'force-quit' | 'down' | 'up' | 'section-next' | 'section-prev' | 'page-down' | 'page-up' | 'toggle-detail' | 'cancel-item' | 'open-log' | 'confirm' | 'dismiss' | 'retarget' | 'refresh' | 'quit'}` | `crtr.browse.${BrowseAction}` | `crtr.browse.search.${'cancel' | 'commit' | 'delete' | 'quit'}` | `crtr.profile.menu.${ProfileMenuAction}` | `crtr.profile.search.${'up' | 'down' | 'pick' | 'back' | 'delete' | 'abort'}` | `crtr.setup.${SetupAction}` | `crtr.setup.confirm.${'accept' | 'back'}` | `crtr.setup.keybindings.${SetupKeybindingAction}` | `crtr.setup.models.${SetupModelsAction}`;
|
|
12
|
+
export declare const BINDING_CATALOG: readonly [BindingDefinition<"crtr.tmux.menu.open">, BindingDefinition<"crtr.tmux.menu.attach">, ...(BindingDefinition<BindingId> | BindingDefinition<"crtr.tmux.node.next"> | BindingDefinition<"crtr.tmux.node.previous"> | BindingDefinition<"crtr.tmux.inbox.toggle"> | BindingDefinition<"crtr.attach.scroll.up"> | BindingDefinition<"crtr.attach.scroll.down"> | BindingDefinition<"crtr.attach.scroll.page-up"> | BindingDefinition<"crtr.attach.scroll.page-down"> | BindingDefinition<"crtr.attach.scroll.live"> | BindingDefinition<"crtr.attach.mouse.toggle"> | BindingDefinition<"crtr.attach.copy.message"> | BindingDefinition<"crtr.attach.search.next"> | BindingDefinition<"crtr.attach.search.previous"> | BindingDefinition<"crtr.attach.search.edit"> | BindingDefinition<"crtr.attach.search.close"> | BindingDefinition<"crtr.graph.down"> | BindingDefinition<"crtr.graph.up"> | BindingDefinition<"crtr.graph.first"> | BindingDefinition<"crtr.graph.last"> | BindingDefinition<"crtr.graph.collapse-or-manager"> | BindingDefinition<"crtr.graph.expand-or-child"> | BindingDefinition<"crtr.graph.focus"> | BindingDefinition<"crtr.graph.revive"> | BindingDefinition<"crtr.graph.focus-manager"> | BindingDefinition<"crtr.graph.close-subtree"> | BindingDefinition<"crtr.graph.dismiss"> | BindingDefinition<"crtr.graph.confirm.accept"> | BindingDefinition<"crtr.graph.confirm.cancel"> | BindingDefinition<"crtr.inspector.force-quit"> | BindingDefinition<"crtr.inspector.down"> | BindingDefinition<"crtr.inspector.up"> | BindingDefinition<"crtr.inspector.section-next"> | BindingDefinition<"crtr.inspector.section-prev"> | BindingDefinition<"crtr.inspector.page-down"> | BindingDefinition<"crtr.inspector.page-up"> | BindingDefinition<"crtr.inspector.toggle-detail"> | BindingDefinition<"crtr.inspector.cancel-item"> | BindingDefinition<"crtr.inspector.open-log"> | BindingDefinition<"crtr.inspector.confirm"> | BindingDefinition<"crtr.inspector.dismiss"> | BindingDefinition<"crtr.inspector.retarget"> | BindingDefinition<"crtr.inspector.refresh"> | BindingDefinition<"crtr.inspector.quit"> | BindingDefinition<"crtr.browse.quit"> | BindingDefinition<"crtr.browse.down"> | BindingDefinition<"crtr.browse.up"> | BindingDefinition<"crtr.browse.first"> | BindingDefinition<"crtr.browse.last"> | BindingDefinition<"crtr.browse.expand-or-child"> | BindingDefinition<"crtr.browse.collapse-or-parent"> | BindingDefinition<"crtr.browse.toggle-fold"> | BindingDefinition<"crtr.browse.tab-next"> | BindingDefinition<"crtr.browse.tab-previous"> | BindingDefinition<"crtr.browse.tab.2" | "crtr.browse.tab.1" | "crtr.browse.tab.3" | "crtr.browse.tab.4" | "crtr.browse.tab.5"> | BindingDefinition<"crtr.browse.close-subtree"> | BindingDefinition<"crtr.browse.confirm-close"> | BindingDefinition<"crtr.browse.sort"> | BindingDefinition<"crtr.browse.scope"> | BindingDefinition<"crtr.browse.profile"> | BindingDefinition<"crtr.browse.residents"> | BindingDefinition<"crtr.browse.preview"> | BindingDefinition<"crtr.browse.search"> | BindingDefinition<"crtr.browse.revive-now"> | BindingDefinition<"crtr.browse.focus"> | BindingDefinition<"crtr.browse.focus-background"> | BindingDefinition<"crtr.browse.yank-id"> | BindingDefinition<"crtr.browse.search.cancel"> | BindingDefinition<"crtr.browse.search.commit"> | BindingDefinition<"crtr.browse.search.delete"> | BindingDefinition<"crtr.browse.search.quit"> | BindingDefinition<"crtr.profile.menu.up"> | BindingDefinition<"crtr.profile.menu.down"> | BindingDefinition<"crtr.profile.menu.select"> | BindingDefinition<"crtr.profile.menu.accept-default"> | BindingDefinition<"crtr.profile.menu.abort"> | BindingDefinition<"crtr.profile.menu.slot.2" | "crtr.profile.menu.slot.1" | "crtr.profile.menu.slot.3" | "crtr.profile.menu.slot.4" | "crtr.profile.menu.slot.5" | "crtr.profile.menu.slot.6" | "crtr.profile.menu.slot.7" | "crtr.profile.menu.slot.8" | "crtr.profile.menu.slot.9">)[], BindingDefinition<"crtr.profile.menu.create">, BindingDefinition<"crtr.profile.menu.search-existing">, BindingDefinition<"crtr.profile.menu.use-root">, BindingDefinition<"crtr.profile.menu.set-default">, BindingDefinition<"crtr.profile.menu.add-directory">, BindingDefinition<"crtr.profile.menu.unpin">, BindingDefinition<"crtr.profile.search.up">, BindingDefinition<"crtr.profile.search.down">, BindingDefinition<"crtr.profile.search.pick">, BindingDefinition<"crtr.profile.search.back">, BindingDefinition<"crtr.profile.search.delete">, BindingDefinition<"crtr.profile.search.abort">, BindingDefinition<"crtr.setup.cancel">, BindingDefinition<"crtr.setup.tab-next">, BindingDefinition<"crtr.setup.tab-previous">, BindingDefinition<"crtr.setup.open">, BindingDefinition<"crtr.setup.list-down">, BindingDefinition<"crtr.setup.list-up">, BindingDefinition<"crtr.setup.toggle">, BindingDefinition<"crtr.setup.details">, BindingDefinition<"crtr.setup.advance">, BindingDefinition<"crtr.setup.confirm.accept">, BindingDefinition<"crtr.setup.confirm.back">, BindingDefinition<"crtr.setup.keybindings.edit">, BindingDefinition<"crtr.setup.keybindings.commit-edit">, BindingDefinition<"crtr.setup.keybindings.cancel-edit">, BindingDefinition<"crtr.setup.keybindings.unbind">, BindingDefinition<"crtr.setup.keybindings.reset-row">, BindingDefinition<"crtr.setup.keybindings.reset-all">, BindingDefinition<"crtr.setup.keybindings.save">, BindingDefinition<"crtr.setup.keybindings.discard">, BindingDefinition<"crtr.setup.keybindings.page-up">, BindingDefinition<"crtr.setup.keybindings.page-down">, BindingDefinition<"crtr.setup.keybindings.filter">, BindingDefinition<"crtr.setup.keybindings.filter-delete">, BindingDefinition<"crtr.setup.keybindings.filter-clear">, BindingDefinition<"crtr.setup.cell-previous">, BindingDefinition<"crtr.setup.cell-next">, BindingDefinition<"crtr.setup.models.open">, BindingDefinition<"crtr.setup.models.scope-next">, BindingDefinition<"crtr.setup.models.commit">, BindingDefinition<"crtr.setup.models.cancel">];
|
|
13
13
|
export declare const BINDING_IDS: ReadonlySet<string>;
|
|
14
14
|
/** True for an action whose direct gesture is handled inside an attach viewer.
|
|
15
15
|
* Tmux menu selectors and root shortcuts are chrome, not pane input. */
|
|
@@ -77,6 +77,7 @@ export const BINDING_CATALOG = Object.freeze([
|
|
|
77
77
|
...attachEntry('crtr.tmux.menu.attach.command.inspect', 'crtr.attach.command.inspect', 'Inspect loaded command', 'Open the loaded slash command’s expanded prompt.', ['h'], ['alt+shift+h']),
|
|
78
78
|
...attachEntry('crtr.tmux.menu.attach.file-review', 'crtr.attach.file-review', 'Review a file', 'Open a line-anchored review of a file named in the transcript.', [], ['alt+shift+r']),
|
|
79
79
|
...attachEntry('crtr.tmux.menu.attach.profile-files', 'crtr.attach.profile-files', 'Search profile files', 'Search files across the node’s profile projects, profile memory, cwd, and ancestor crouter memory stores.', ['f'], ['ctrl+f']),
|
|
80
|
+
...attachEntry('crtr.tmux.menu.attach.search', 'crtr.attach.search', 'Search transcript', 'Search the transcript as it is currently displayed — folded tool output is not searched until it is unfolded.', ['/'], ['alt+/']),
|
|
80
81
|
...attachEntry('crtr.tmux.menu.attach.whip', 'crtr.attach.whip', 'Whip agent', 'Interrupt the agent and send a playful command.', ['w'], []),
|
|
81
82
|
b('crtr.attach.scroll.up', 'Attach', 'Scroll up', 'Scroll the transcript one line up.', ['shift+up'], 'terminal', ATTACH_CONTEXTS),
|
|
82
83
|
b('crtr.attach.scroll.down', 'Attach', 'Scroll down', 'Scroll the transcript one line down.', ['shift+down'], 'terminal', ATTACH_CONTEXTS),
|
|
@@ -85,6 +86,10 @@ export const BINDING_CATALOG = Object.freeze([
|
|
|
85
86
|
b('crtr.attach.scroll.live', 'Attach', 'Return to live', 'Return the transcript to the live bottom.', ['shift+end'], 'terminal', ATTACH_CONTEXTS),
|
|
86
87
|
b('crtr.attach.mouse.toggle', 'Attach', 'Toggle mouse wheel scrolling', 'Toggle mouse wheel scrolling on or off.', ['alt+shift+o'], 'terminal', ATTACH_CONTEXTS),
|
|
87
88
|
b('crtr.attach.copy.message', 'Attach', 'Copy visible message', 'Copy the newest message visible in the transcript viewport.', ['alt+shift+y'], 'terminal', ATTACH_CONTEXTS),
|
|
89
|
+
b('crtr.attach.search.next', 'Attach', 'Next match', 'Move to the next transcript search match.', ['n'], 'terminal', ['attach.search']),
|
|
90
|
+
b('crtr.attach.search.previous', 'Attach', 'Previous match', 'Move to the previous transcript search match.', ['shift+n'], 'terminal', ['attach.search']),
|
|
91
|
+
b('crtr.attach.search.edit', 'Attach', 'Edit query', 'Return to editing the transcript search query.', ['/'], 'terminal', ['attach.search']),
|
|
92
|
+
b('crtr.attach.search.close', 'Attach', 'Close search', 'Leave transcript search and clear its highlights.', ['escape'], 'terminal', ['attach.search'], 'dismiss-modal'),
|
|
88
93
|
b('crtr.graph.down', 'GRAPH', 'Move down', 'Move to the next graph row.', ['j'], 'terminal', ['graph']),
|
|
89
94
|
b('crtr.graph.up', 'GRAPH', 'Move up', 'Move to the previous graph row.', ['k'], 'terminal', ['graph']),
|
|
90
95
|
b('crtr.graph.first', 'GRAPH', 'First row', 'Move to the first graph row.', ['g'], 'terminal', ['graph']),
|
|
@@ -219,6 +224,7 @@ export const SAFETY_REQUIREMENTS = Object.freeze([
|
|
|
219
224
|
{ context: 'graph.confirm', capability: 'dismiss-modal' },
|
|
220
225
|
{ context: 'inspector', capability: 'leave-screen' },
|
|
221
226
|
{ context: 'inspector', capability: 'dismiss-modal' },
|
|
227
|
+
{ context: 'attach.search', capability: 'dismiss-modal' },
|
|
222
228
|
{ context: 'browse', capability: 'leave-screen' },
|
|
223
229
|
{ context: 'browse.search', capability: 'dismiss-modal' },
|
|
224
230
|
{ context: 'profile.menu', capability: 'leave-screen' },
|
|
@@ -28,9 +28,20 @@ export interface NodeSnapshotRead {
|
|
|
28
28
|
source: 'builtin';
|
|
29
29
|
}>;
|
|
30
30
|
}
|
|
31
|
+
export interface NodeMessagesPageRead {
|
|
32
|
+
messages: unknown[];
|
|
33
|
+
nextCursor: string | null;
|
|
34
|
+
}
|
|
35
|
+
export interface NodeMessagesPageOptions {
|
|
36
|
+
cursor?: string;
|
|
37
|
+
limit?: number;
|
|
38
|
+
}
|
|
31
39
|
/** Reconstruct a persisted session without launching its broker. Backs the
|
|
32
40
|
* machine-readable snapshot endpoint and the human transcript render. */
|
|
33
41
|
export declare function readNodeSnapshot(nodeId: string): Promise<NodeSnapshotRead>;
|
|
42
|
+
/** A backward-paged, chronological window over the same visible history as a
|
|
43
|
+
* snapshot. `nextCursor` walks toward older messages. */
|
|
44
|
+
export declare function readNodeMessagesPage(nodeId: string, { cursor, limit }: NodeMessagesPageOptions): Promise<NodeMessagesPageRead>;
|
|
34
45
|
/** Human-readable history only: text messages plus terse tool records, never
|
|
35
46
|
* private model reasoning. */
|
|
36
47
|
export declare function transcriptMarkdown(nodeId: string): Promise<string>;
|
|
@@ -135,9 +135,10 @@ function readToolGroupSummaries(nodeId) {
|
|
|
135
135
|
return {};
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
|
-
/** Reconstruct
|
|
139
|
-
*
|
|
140
|
-
|
|
138
|
+
/** Reconstruct the visible message history from a stable at-rest session copy.
|
|
139
|
+
* Both snapshot and paged reads use this one path so their histories cannot
|
|
140
|
+
* diverge. */
|
|
141
|
+
async function reconstructVisibleMessages(nodeId) {
|
|
141
142
|
const node = getNode(nodeId);
|
|
142
143
|
if (node === null) {
|
|
143
144
|
throw new InputError({ error: 'not_found', message: `no node: ${nodeId}`, field: 'id', next: 'List nodes with `crtr node inspect list`.' });
|
|
@@ -146,57 +147,122 @@ export async function readNodeSnapshot(nodeId) {
|
|
|
146
147
|
if (sessionFile === null) {
|
|
147
148
|
throw new InputError({ error: 'no_session', message: `node has no readable session file: ${nodeId}`, field: 'id', next: 'Choose a node with captured conversation history.' });
|
|
148
149
|
}
|
|
149
|
-
const tempDir = mkdtempSync(join(tmpdir(), 'crtr-node-
|
|
150
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'crtr-node-session-'));
|
|
150
151
|
try {
|
|
151
152
|
const copy = join(tempDir, basename(sessionFile));
|
|
152
153
|
copyFileSync(sessionFile, copy);
|
|
153
154
|
const manager = SessionManager.open(copy);
|
|
154
155
|
const context = manager.buildSessionContext();
|
|
155
156
|
const messages = visibleMessages(cycleAwareMessages(manager), { boundaryReviewId: node.review_binding?.review_id });
|
|
156
|
-
const sessionId = manager.getSessionId();
|
|
157
|
-
const sessionName = manager.getSessionName();
|
|
158
|
-
const snapshot = {
|
|
159
|
-
messages,
|
|
160
|
-
stats: statsFromMessages(messages, sessionId, sessionFile),
|
|
161
|
-
// pi's `get_state` shape (RpcSessionState), reconstructed offline: the
|
|
162
|
-
// live-only fields report their at-rest values (nothing is streaming or
|
|
163
|
-
// queued for a node with no broker).
|
|
164
|
-
state: {
|
|
165
|
-
model: await resolveRecordedModel(context.model),
|
|
166
|
-
thinkingLevel: context.thinkingLevel,
|
|
167
|
-
isStreaming: false,
|
|
168
|
-
isCompacting: false,
|
|
169
|
-
steeringMode: 'one-at-a-time',
|
|
170
|
-
followUpMode: 'one-at-a-time',
|
|
171
|
-
sessionFile,
|
|
172
|
-
sessionId,
|
|
173
|
-
sessionName,
|
|
174
|
-
autoCompactionEnabled: true,
|
|
175
|
-
messageCount: messages.length,
|
|
176
|
-
pendingMessageCount: 0,
|
|
177
|
-
},
|
|
178
|
-
// A snapshot reconstructed from the .jsonl alone: display state is broker
|
|
179
|
-
// process state (last setStatus/setWidget/setTitle) and is not persisted,
|
|
180
|
-
// so an offline read has none.
|
|
181
|
-
display: { statuses: {}, widgets: {}, title: undefined },
|
|
182
|
-
// The unrun queue lives in the engine's memory; a dormant node has no
|
|
183
|
-
// engine, so nothing is waiting to run.
|
|
184
|
-
queued: { steering: [], followUp: [] },
|
|
185
|
-
// Sidecar summaries survive broker exit so a dormant transcript receives
|
|
186
|
-
// the same completed-group data as a live welcome snapshot. Malformed or
|
|
187
|
-
// absent sidecars are deliberately ignored: tool calls remain visible.
|
|
188
|
-
toolGroupSummaries: readToolGroupSummaries(nodeId),
|
|
189
|
-
};
|
|
190
157
|
return {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
158
|
+
sessionFile,
|
|
159
|
+
messages,
|
|
160
|
+
model: context.model,
|
|
161
|
+
thinkingLevel: context.thinkingLevel,
|
|
162
|
+
sessionId: manager.getSessionId(),
|
|
163
|
+
sessionName: manager.getSessionName(),
|
|
194
164
|
};
|
|
195
165
|
}
|
|
196
166
|
finally {
|
|
197
167
|
rmSync(tempDir, { recursive: true, force: true });
|
|
198
168
|
}
|
|
199
169
|
}
|
|
170
|
+
function invalidCursor() {
|
|
171
|
+
throw new InputError({
|
|
172
|
+
error: 'invalid_cursor',
|
|
173
|
+
message: 'cursor must be a base64url-encoded messages cursor.',
|
|
174
|
+
field: 'cursor',
|
|
175
|
+
next: 'Omit cursor to start from the newest messages.',
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
function decodeMessagesCursor(cursor, messagesLength) {
|
|
179
|
+
if (!/^[A-Za-z0-9_-]+$/u.test(cursor))
|
|
180
|
+
invalidCursor();
|
|
181
|
+
let parsed;
|
|
182
|
+
try {
|
|
183
|
+
const json = Buffer.from(cursor, 'base64url').toString('utf8');
|
|
184
|
+
if (Buffer.from(json, 'utf8').toString('base64url') !== cursor)
|
|
185
|
+
invalidCursor();
|
|
186
|
+
parsed = JSON.parse(json);
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
invalidCursor();
|
|
190
|
+
}
|
|
191
|
+
if (typeof parsed !== 'object'
|
|
192
|
+
|| parsed === null
|
|
193
|
+
|| parsed.v !== 1
|
|
194
|
+
|| !Number.isSafeInteger(parsed.before)) {
|
|
195
|
+
invalidCursor();
|
|
196
|
+
}
|
|
197
|
+
const before = parsed.before;
|
|
198
|
+
if (before < 0 || before > messagesLength)
|
|
199
|
+
invalidCursor();
|
|
200
|
+
return before;
|
|
201
|
+
}
|
|
202
|
+
function encodeMessagesCursor(before) {
|
|
203
|
+
return Buffer.from(JSON.stringify({ v: 1, before }), 'utf8').toString('base64url');
|
|
204
|
+
}
|
|
205
|
+
/** Reconstruct a persisted session without launching its broker. Backs the
|
|
206
|
+
* machine-readable snapshot endpoint and the human transcript render. */
|
|
207
|
+
export async function readNodeSnapshot(nodeId) {
|
|
208
|
+
const read = await reconstructVisibleMessages(nodeId);
|
|
209
|
+
const snapshot = {
|
|
210
|
+
messages: read.messages,
|
|
211
|
+
stats: statsFromMessages(read.messages, read.sessionId, read.sessionFile),
|
|
212
|
+
// pi's `get_state` shape (RpcSessionState), reconstructed offline: the
|
|
213
|
+
// live-only fields report their at-rest values (nothing is streaming or
|
|
214
|
+
// queued for a node with no broker).
|
|
215
|
+
state: {
|
|
216
|
+
model: await resolveRecordedModel(read.model),
|
|
217
|
+
thinkingLevel: read.thinkingLevel,
|
|
218
|
+
isStreaming: false,
|
|
219
|
+
isCompacting: false,
|
|
220
|
+
steeringMode: 'one-at-a-time',
|
|
221
|
+
followUpMode: 'one-at-a-time',
|
|
222
|
+
sessionFile: read.sessionFile,
|
|
223
|
+
sessionId: read.sessionId,
|
|
224
|
+
sessionName: read.sessionName,
|
|
225
|
+
autoCompactionEnabled: true,
|
|
226
|
+
messageCount: read.messages.length,
|
|
227
|
+
pendingMessageCount: 0,
|
|
228
|
+
},
|
|
229
|
+
// A snapshot reconstructed from the .jsonl alone: display state is broker
|
|
230
|
+
// process state (last setStatus/setWidget/setTitle) and is not persisted,
|
|
231
|
+
// so an offline read has none.
|
|
232
|
+
display: { statuses: {}, widgets: {}, title: undefined },
|
|
233
|
+
// The unrun queue lives in the engine's memory; a dormant node has no
|
|
234
|
+
// engine, so nothing is waiting to run.
|
|
235
|
+
queued: { steering: [], followUp: [] },
|
|
236
|
+
// Sidecar summaries survive broker exit so a dormant transcript receives
|
|
237
|
+
// the same completed-group data as a live welcome snapshot. Malformed or
|
|
238
|
+
// absent sidecars are deliberately ignored: tool calls remain visible.
|
|
239
|
+
toolGroupSummaries: readToolGroupSummaries(nodeId),
|
|
240
|
+
};
|
|
241
|
+
return {
|
|
242
|
+
nodeId,
|
|
243
|
+
snapshot,
|
|
244
|
+
commands: BUILTIN_SLASH_COMMANDS.map((c) => ({ ...c, source: 'builtin' })),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
/** A backward-paged, chronological window over the same visible history as a
|
|
248
|
+
* snapshot. `nextCursor` walks toward older messages. */
|
|
249
|
+
export async function readNodeMessagesPage(nodeId, { cursor, limit = 200 }) {
|
|
250
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500) {
|
|
251
|
+
throw new InputError({
|
|
252
|
+
error: 'invalid_limit',
|
|
253
|
+
message: 'limit must be an integer from 1 through 500.',
|
|
254
|
+
field: 'limit',
|
|
255
|
+
next: 'Use a limit from 1 through 500.',
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
const { messages } = await reconstructVisibleMessages(nodeId);
|
|
259
|
+
const before = cursor === undefined ? messages.length : decodeMessagesCursor(cursor, messages.length);
|
|
260
|
+
const start = Math.max(0, before - limit);
|
|
261
|
+
return {
|
|
262
|
+
messages: messages.slice(start, before),
|
|
263
|
+
nextCursor: start === 0 ? null : encodeMessagesCursor(start),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
200
266
|
function textFromContent(content) {
|
|
201
267
|
if (typeof content === 'string')
|
|
202
268
|
return content;
|
|
@@ -14,7 +14,7 @@ import { childrenOf, subscribersOf, subscriptionsOf, setMessageWait, updateNode
|
|
|
14
14
|
import { subtreeIds } from '../../../core/canvas/nav-model.js';
|
|
15
15
|
import { contextDir, reportsDir } from '../../../core/canvas/paths.js';
|
|
16
16
|
import { nodeArtifacts } from '../../../core/canvas/history.js';
|
|
17
|
-
import { readNodeSession, readNodeSnapshot, transcriptMarkdown } from '../../../core/runtime/node-read.js';
|
|
17
|
+
import { readNodeMessagesPage, readNodeSession, readNodeSnapshot, transcriptMarkdown } from '../../../core/runtime/node-read.js';
|
|
18
18
|
import { reviveAll } from '../../../core/runtime/revive-all.js';
|
|
19
19
|
import { promote, requestYield, reshapeNode } from '../../../core/runtime/promote.js';
|
|
20
20
|
import { recycleNode } from '../../../core/runtime/recycle.js';
|
|
@@ -301,6 +301,38 @@ async function handleSnapshot(ctx) {
|
|
|
301
301
|
};
|
|
302
302
|
return { status: 200, body };
|
|
303
303
|
}
|
|
304
|
+
function parseMessagesQuery(ctx) {
|
|
305
|
+
for (const [field] of ctx.query) {
|
|
306
|
+
if (field !== 'cursor' && field !== 'limit')
|
|
307
|
+
throw usage(`unknown messages query parameter: ${field}`);
|
|
308
|
+
}
|
|
309
|
+
const cursors = ctx.query.getAll('cursor');
|
|
310
|
+
const limits = ctx.query.getAll('limit');
|
|
311
|
+
if (cursors.length > 1)
|
|
312
|
+
throw usage('cursor must appear at most once');
|
|
313
|
+
if (limits.length > 1)
|
|
314
|
+
throw usage('limit must appear at most once');
|
|
315
|
+
if (limits.length === 0)
|
|
316
|
+
return cursors.length === 0 ? {} : { cursor: cursors[0] };
|
|
317
|
+
const rawLimit = limits[0];
|
|
318
|
+
const limit = Number(rawLimit);
|
|
319
|
+
if (!Number.isSafeInteger(limit) || rawLimit === '') {
|
|
320
|
+
throw usage('limit must be an integer from 1 through 500');
|
|
321
|
+
}
|
|
322
|
+
return cursors.length === 0 ? { limit } : { cursor: cursors[0], limit };
|
|
323
|
+
}
|
|
324
|
+
/** Offline, backward-paged read of the cycle-flattened visible history. */
|
|
325
|
+
async function handleMessages(ctx) {
|
|
326
|
+
const id = ctx.params['id'];
|
|
327
|
+
const read = await readNodeMessagesPage(id, parseMessagesQuery(ctx));
|
|
328
|
+
const body = {
|
|
329
|
+
node_id: id,
|
|
330
|
+
messages: read.messages,
|
|
331
|
+
next_cursor: read.nextCursor,
|
|
332
|
+
captured_at: nowIso(),
|
|
333
|
+
};
|
|
334
|
+
return { status: 200, body };
|
|
335
|
+
}
|
|
304
336
|
/** The faithful export read: raw `.jsonl` bytes + assembled system prompt.
|
|
305
337
|
* `/snapshot` reconstructs a cycle-flattened `AgentMessage[]` for renderers and
|
|
306
338
|
* drops what a renderer cannot show; this drops nothing. */
|
|
@@ -598,6 +630,7 @@ export const nodeRoutes = [
|
|
|
598
630
|
{ method: 'POST', pattern: '/v1/nodes/revive-all', handler: () => handleReviveAll() },
|
|
599
631
|
{ method: 'GET', pattern: '/v1/nodes/:id', handler: handleDetail },
|
|
600
632
|
{ method: 'GET', pattern: '/v1/nodes/:id/snapshot', handler: handleSnapshot },
|
|
633
|
+
{ method: 'GET', pattern: '/v1/nodes/:id/messages', handler: handleMessages },
|
|
601
634
|
{ method: 'GET', pattern: '/v1/nodes/:id/session', handler: handleSession },
|
|
602
635
|
{ method: 'GET', pattern: '/v1/nodes/:id/transcript', handler: handleTranscript },
|
|
603
636
|
{ method: 'GET', pattern: '/v1/nodes/:id/context', handler: handleContext },
|
package/package.json
CHANGED
package/runtime.lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/crouter",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.201",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@north-light/crouter",
|
|
9
|
-
"version": "0.3.
|
|
9
|
+
"version": "0.3.201",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"dependencies": {
|