@north-light/crouter 0.3.263 → 0.3.269
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 +5 -1
- package/dist/api/client.js +6 -0
- package/dist/api/dto/canvas.d.ts +53 -2
- package/dist/api/dto/nodes.d.ts +6 -2
- package/dist/api/routes.d.ts +1 -0
- package/dist/api/routes.js +1 -0
- package/dist/builtin-memory/05-kinds/advisor/advice-contract.md +2 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/integration/provider-rotation.test.ts +50 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +46 -6
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +6 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +7 -2
- package/dist/clients/attach/session/whip-achievements.d.ts +4 -0
- package/dist/clients/attach/session/whip-achievements.js +145 -0
- package/dist/clients/attach/slash/dispatch.js +5 -3
- package/dist/clients/attach/viewer.js +607 -606
- package/dist/commands/canvas-history/grep.js +3 -22
- package/dist/commands/canvas-history/read.js +5 -3
- package/dist/commands/canvas-history/search.js +3 -22
- package/dist/commands/canvas-history/shared.d.ts +8 -0
- package/dist/commands/canvas-history/shared.js +64 -1
- package/dist/commands/canvas-history/stats.d.ts +1 -0
- package/dist/commands/canvas-history/stats.js +68 -0
- package/dist/commands/canvas-history.js +4 -3
- package/dist/commands/node/create.js +2 -2
- package/dist/commands/sys/config.js +19 -2
- package/dist/commands/sys/panels/models-panel.js +3 -1
- package/dist/core/__tests__/integration/revive.test.js +4 -2
- package/dist/core/__tests__/model-pin-durability.test.js +25 -1
- package/dist/core/__tests__/relaunch-root.test.js +17 -0
- package/dist/core/__tests__/seam/broker-provider-retry.test.js +19 -4
- package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.js +19 -2
- package/dist/core/broker-client/__tests__/transport-relay.test.js +3 -1
- package/dist/core/broker-client/transport-relay.js +5 -1
- package/dist/core/canvas/__tests__/history-transcript.test.d.ts +1 -0
- package/dist/core/canvas/__tests__/history-transcript.test.js +102 -0
- package/dist/core/canvas/browse/app.d.ts +1 -1
- package/dist/core/canvas/browse/app.js +13 -10
- package/dist/core/canvas/browse/model.js +16 -1
- package/dist/core/canvas/browse/render.d.ts +23 -3
- package/dist/core/canvas/browse/render.js +429 -64
- package/dist/core/canvas/canvas.js +12 -8
- package/dist/core/canvas/extensions.d.ts +1 -1
- package/dist/core/canvas/extensions.js +7 -1
- package/dist/core/canvas/history.d.ts +36 -1
- package/dist/core/canvas/history.js +328 -7
- package/dist/core/canvas/labels.d.ts +2 -2
- package/dist/core/canvas/migrations.js +32 -0
- package/dist/core/canvas/node-recap.d.ts +23 -0
- package/dist/core/canvas/node-recap.js +67 -0
- package/dist/core/canvas/render-source.d.ts +33 -1
- package/dist/core/canvas/render-source.js +80 -22
- package/dist/core/canvas/types.d.ts +9 -0
- package/dist/core/config.js +13 -5
- package/dist/core/keybindings/catalog.d.ts +2 -2
- package/dist/core/keybindings/catalog.js +2 -1
- package/dist/core/model-routes.js +1 -1
- package/dist/core/runtime/broker/auth-reload.js +4 -1
- package/dist/core/runtime/broker/fault-retry.js +7 -2
- package/dist/core/runtime/broker/frame-client.js +4 -0
- package/dist/core/runtime/broker/frame-dispatch.js +7 -0
- package/dist/core/runtime/broker/inbox.d.ts +12 -2
- package/dist/core/runtime/broker/inbox.js +68 -38
- package/dist/core/runtime/broker-protocol.d.ts +6 -0
- package/dist/core/runtime/broker.js +14 -0
- package/dist/core/runtime/canvas-extensions.d.ts +1 -0
- package/dist/core/runtime/canvas-extensions.js +2 -0
- package/dist/core/runtime/interactive-deliver.d.ts +8 -2
- package/dist/core/runtime/interactive-deliver.js +12 -3
- package/dist/core/runtime/launch-target.js +1 -1
- package/dist/core/runtime/launch.d.ts +12 -6
- package/dist/core/runtime/launch.js +19 -17
- package/dist/core/runtime/model-swap.d.ts +5 -4
- package/dist/core/runtime/model-swap.js +7 -5
- package/dist/core/runtime/promote.js +2 -1
- package/dist/core/runtime/reset.js +4 -2
- package/dist/core/runtime/revive.js +22 -0
- package/dist/core/runtime/spawn.d.ts +3 -3
- package/dist/core/runtime/spawn.js +20 -3
- package/dist/core/runtime/stamp/channel.d.ts +18 -0
- package/dist/core/runtime/stamp/channel.js +74 -0
- package/dist/core/runtime/stamp/protocol.d.ts +86 -0
- package/dist/core/runtime/stamp/protocol.js +113 -0
- package/dist/core/runtime/tmux-driver.d.ts +11 -1
- package/dist/core/runtime/tmux-driver.js +27 -11
- package/dist/core/self-update.js +2 -5
- package/dist/core/tui/draw.d.ts +1 -0
- package/dist/core/tui/draw.js +5 -1
- package/dist/core/tui/markdown.d.ts +17 -0
- package/dist/core/tui/markdown.js +407 -0
- package/dist/core/user-settings.d.ts +4 -0
- package/dist/core/user-settings.js +1 -0
- package/dist/daemon/api/__tests__/node-model-validation.test.js +21 -0
- package/dist/daemon/api/bridge.d.ts +3 -2
- package/dist/daemon/api/bridge.js +34 -9
- package/dist/daemon/api/handlers/canvas.js +62 -16
- package/dist/daemon/api/handlers/messages.js +12 -1
- package/dist/daemon/api/handlers/nodes.js +10 -3
- package/dist/daemon/api/map.js +2 -0
- package/dist/daemon/messaging/node-message.js +15 -1
- package/dist/daemon/reconcilers/broker-supervision.js +26 -9
- package/dist/daemon/reconcilers/live-obligation.d.ts +8 -1
- package/dist/daemon/reconcilers/live-obligation.js +17 -0
- package/dist/pi-extensions/canvas-inbox-watcher.js +27 -2
- package/dist/pi-extensions/canvas-recap.js +4 -0
- package/dist/pi-extensions/canvas-stamp.d.ts +14 -0
- package/dist/pi-extensions/canvas-stamp.js +77 -0
- package/dist/pi-extensions/canvas-stophook.js +3 -0
- package/dist/shared/__tests__/env-boundary.test.js +4 -2
- package/dist/shared/crtr-version.d.ts +2 -0
- package/dist/shared/crtr-version.js +18 -0
- package/dist/shared/env.d.ts +10 -0
- package/dist/shared/env.js +19 -4
- package/dist/types.d.ts +10 -0
- package/dist/types.js +4 -0
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
|
@@ -115,11 +115,28 @@ test('node-relative memory slash commands omit preview metadata and register inv
|
|
|
115
115
|
assert.equal(loadedCommandPreview('/acceptance-independent first evidence', live.commands), undefined);
|
|
116
116
|
assert.deepEqual(live.notifications, []);
|
|
117
117
|
const sentContent = Object.fromEntries(live.sent.map((send) => [send.name, send.message.content]));
|
|
118
|
-
|
|
118
|
+
const invokedBody = {
|
|
119
119
|
[COMMAND_NAMES[0]]: `Store evidence in ${join(home, 'nodes', nodeId, 'context', 'result.md')}.\n`,
|
|
120
120
|
[COMMAND_NAMES[1]]: `Node ${nodeId} received evidence.\n`,
|
|
121
121
|
[COMMAND_NAMES[2]]: 'Independent payload: evidence.\n',
|
|
122
|
-
}
|
|
122
|
+
};
|
|
123
|
+
// An invoked document is content delivery, so the FIRST invocation carries the
|
|
124
|
+
// same companions `crtr memory read` attaches: the listings that place the doc,
|
|
125
|
+
// plus every corpus doc its memory-read routes match. The doc being delivered is
|
|
126
|
+
// absent from its own listing because its exposure registers before they render.
|
|
127
|
+
const firstSent = sentContent[COMMAND_NAMES[0]];
|
|
128
|
+
assert.ok(firstSent.endsWith(invokedBody[COMMAND_NAMES[0]]), 'the expanded body ends the delivery');
|
|
129
|
+
const companions = firstSent.slice(0, firstSent.length - invokedBody[COMMAND_NAMES[0]].length);
|
|
130
|
+
assert.match(companions, /^<auto-loaded-context>\n[\s\S]*<\/auto-loaded-context>\n\n$/);
|
|
131
|
+
assert.ok(companions.includes('<memory-listing dir="workspace/workspace">\n'
|
|
132
|
+
+ `[[workspace/workspace/${COMMAND_LOCALS[2]}]]: Acceptance fixture.\n`
|
|
133
|
+
+ `[[workspace/workspace/${COMMAND_LOCALS[1]}]]: Acceptance fixture.\n`
|
|
134
|
+
+ '</memory-listing>'), 'the invoked doc\'s own directory listing is attached as a block');
|
|
135
|
+
assert.ok(companions.includes('<memory-listing dir="workspace">\n[[workspace/workspace]]\n</memory-listing>'));
|
|
136
|
+
assert.match(companions, /<memory kind="knowledge" name="memory-read-orientation">/);
|
|
137
|
+
// The later invocations attach nothing: the first delivery already exposed them.
|
|
138
|
+
assert.equal(sentContent[COMMAND_NAMES[1]], invokedBody[COMMAND_NAMES[1]]);
|
|
139
|
+
assert.equal(sentContent[COMMAND_NAMES[2]], invokedBody[COMMAND_NAMES[2]]);
|
|
123
140
|
for (const send of live.sent) {
|
|
124
141
|
assert.deepEqual(send.message, {
|
|
125
142
|
customType: 'memory-slash-command',
|
|
@@ -61,7 +61,9 @@ test('RelayTransport dials crtrd attach with Authorization: Bearer <token>', asy
|
|
|
61
61
|
const relay = await startMockRelay();
|
|
62
62
|
const transport = new RelayTransport('node-auth', relay.target);
|
|
63
63
|
await connectTransport(transport);
|
|
64
|
-
|
|
64
|
+
// `wakeParked=1` is part of the dial contract: a person attached deliberately,
|
|
65
|
+
// so the bridge wakes an exact parked resident for this caller.
|
|
66
|
+
assert.equal(relay.requestUrls[0], '/v1/nodes/node-auth/attach?wakeParked=1');
|
|
65
67
|
assert.equal(relay.authHeaders[0], 'Bearer test-token');
|
|
66
68
|
transport.destroy();
|
|
67
69
|
await relay.close();
|
|
@@ -14,7 +14,11 @@ function relayUrl(target, nodeId) {
|
|
|
14
14
|
const url = new URL(target.previewEndpoint);
|
|
15
15
|
url.protocol = url.protocol === 'http:' ? 'ws:' : 'wss:';
|
|
16
16
|
url.pathname = `/v1/nodes/${encodeURIComponent(nodeId)}/attach`;
|
|
17
|
-
|
|
17
|
+
// A person typed `crtr surface attach` at this node, so an exact parked
|
|
18
|
+
// resident is woken for it — the same thing the LOCAL attach ensure does for
|
|
19
|
+
// a direct attach. The bridge leaves a parked node parked without this, which
|
|
20
|
+
// is what a product page reconnecting its viewport wants.
|
|
21
|
+
url.search = '?wakeParked=1';
|
|
18
22
|
url.hash = '';
|
|
19
23
|
return url.toString();
|
|
20
24
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// history-transcript.test.ts — transcript artifacts carry the origin their
|
|
2
|
+
// stamp recorded.
|
|
3
|
+
//
|
|
4
|
+
// The join is the fragile part: a stamp is physically written BEFORE the
|
|
5
|
+
// message it describes, so correlation runs on the message's own epoch-ms
|
|
6
|
+
// `timestamp` and never on tree position. Getting that wrong yields a corpus
|
|
7
|
+
// where every origin silently reads `unknown` — which looks like a stamping
|
|
8
|
+
// regression rather than a reader bug, so it is worth pinning.
|
|
9
|
+
import { test, before, beforeEach, after } from 'node:test';
|
|
10
|
+
import assert from 'node:assert/strict';
|
|
11
|
+
import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { tmpdir } from 'node:os';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { createNode } from '../canvas.js';
|
|
15
|
+
import { closeDb } from '../db.js';
|
|
16
|
+
import { nodeDir, sessionPtrPath } from '../paths.js';
|
|
17
|
+
import { nodeArtifacts, groupCorpus } from '../history.js';
|
|
18
|
+
const NODE = 'test-transcript-node';
|
|
19
|
+
function meta(id, cwd) {
|
|
20
|
+
return {
|
|
21
|
+
node_id: id,
|
|
22
|
+
name: id,
|
|
23
|
+
created: new Date().toISOString(),
|
|
24
|
+
cwd,
|
|
25
|
+
kind: 'general',
|
|
26
|
+
mode: 'base',
|
|
27
|
+
lifecycle: 'terminal',
|
|
28
|
+
status: 'active',
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function messageEntry(role, text, msgTs) {
|
|
32
|
+
return JSON.stringify({
|
|
33
|
+
type: 'message',
|
|
34
|
+
timestamp: new Date(msgTs).toISOString(),
|
|
35
|
+
message: { role, content: [{ type: 'text', text }], timestamp: msgTs },
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function stampEntry(msgTs, origin) {
|
|
39
|
+
return JSON.stringify({
|
|
40
|
+
type: 'custom',
|
|
41
|
+
customType: 'crtr-stamp',
|
|
42
|
+
data: { v: 1, kind: 'message', payload: { msgTs, origin } },
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/** A session file in the real on-disk shape: stamps precede their messages,
|
|
46
|
+
* one inbound turn is unstamped, and an assistant turn sits between them. */
|
|
47
|
+
function seedSession(home) {
|
|
48
|
+
const dir = nodeDir(NODE);
|
|
49
|
+
mkdirSync(dir, { recursive: true });
|
|
50
|
+
const file = join(home, 'session.jsonl');
|
|
51
|
+
writeFileSync(file, [
|
|
52
|
+
JSON.stringify({ type: 'custom', customType: 'crtr-stamp', data: { v: 1, kind: 'session', payload: { node: NODE, crtr: '0.0.0' } } }),
|
|
53
|
+
stampEntry(1000, { type: 'runtime', kind: 'wake', wake: { kind: 'schedule_revive', ownerId: null, armedAt: 'x' } }),
|
|
54
|
+
messageEntry('user', 'woken by cron', 1000),
|
|
55
|
+
JSON.stringify({ type: 'message', timestamp: new Date(1500).toISOString(), message: { role: 'assistant', content: [{ type: 'thinking', text: 'private' }, { type: 'toolCall', name: 'Bash' }], timestamp: 1500 } }),
|
|
56
|
+
stampEntry(2000, { type: 'runtime', kind: 'inbox', entries: [{ from: 'n1', fromName: 'child-node', kind: 'report', tier: 'normal' }] }),
|
|
57
|
+
messageEntry('user', 'a delivery', 2000),
|
|
58
|
+
messageEntry('user', 'no stamp for this one', 3000),
|
|
59
|
+
].join('\n'), 'utf8');
|
|
60
|
+
writeFileSync(sessionPtrPath(NODE), file, 'utf8');
|
|
61
|
+
}
|
|
62
|
+
let home;
|
|
63
|
+
let cwd;
|
|
64
|
+
before(() => {
|
|
65
|
+
home = mkdtempSync(join(tmpdir(), 'crtr-history-transcript-'));
|
|
66
|
+
cwd = mkdtempSync(join(tmpdir(), 'crtr-history-transcript-cwd-'));
|
|
67
|
+
process.env['CRTR_HOME'] = home;
|
|
68
|
+
});
|
|
69
|
+
beforeEach(() => {
|
|
70
|
+
closeDb();
|
|
71
|
+
});
|
|
72
|
+
after(() => {
|
|
73
|
+
closeDb();
|
|
74
|
+
for (const d of [home, cwd])
|
|
75
|
+
rmSync(d, { recursive: true, force: true });
|
|
76
|
+
delete process.env['CRTR_HOME'];
|
|
77
|
+
});
|
|
78
|
+
test('a stamped message carries its origin; an unstamped one reads unknown', () => {
|
|
79
|
+
createNode(meta(NODE, cwd));
|
|
80
|
+
seedSession(home);
|
|
81
|
+
const arts = nodeArtifacts(NODE, ['transcript']);
|
|
82
|
+
assert.equal(arts.length, 4, 'every message entry becomes one artifact');
|
|
83
|
+
const origins = arts.map((a) => a.origin);
|
|
84
|
+
assert.deepEqual(origins, ['wake', undefined, 'inbox', 'unknown']);
|
|
85
|
+
// Origin is a property of ARRIVAL — a generated turn must not acquire one,
|
|
86
|
+
// which is what lets `--origin` exclude assistant turns without a role filter.
|
|
87
|
+
const assistant = arts.find((a) => a.role === 'assistant');
|
|
88
|
+
assert.equal(assistant?.origin, undefined);
|
|
89
|
+
// Sender attribution rides the inbox stamp's delivered entries.
|
|
90
|
+
assert.equal(arts[2]?.from, 'child-node');
|
|
91
|
+
// Private model reasoning stays out of the searchable body, matching every
|
|
92
|
+
// other human-facing transcript surface.
|
|
93
|
+
assert.ok(!assistant?.loadBody().includes('private'));
|
|
94
|
+
assert.ok(assistant?.loadBody().includes('Tool: Bash'));
|
|
95
|
+
});
|
|
96
|
+
test('grouping by origin counts arrivals only, and unknown is its own bucket', () => {
|
|
97
|
+
createNode(meta(NODE, cwd));
|
|
98
|
+
seedSession(home);
|
|
99
|
+
const groups = groupCorpus(nodeArtifacts(NODE, ['transcript']), 'origin');
|
|
100
|
+
assert.deepEqual(groups.map((g) => g.key).sort(), ['inbox', 'unknown', 'wake'], 'the assistant turn contributes no bucket');
|
|
101
|
+
assert.equal(groups.reduce((n, g) => n + g.count, 0), 3);
|
|
102
|
+
});
|
|
@@ -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.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.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;
|
|
@@ -21,7 +21,8 @@ const BROWSE_NAV_ACTIONS = [
|
|
|
21
21
|
'crtr.browse.expand-or-child', 'crtr.browse.collapse-or-parent', 'crtr.browse.toggle-fold',
|
|
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
|
-
'crtr.browse.scope', 'crtr.browse.profile', 'crtr.browse.residents', 'crtr.browse.preview',
|
|
24
|
+
'crtr.browse.scope', 'crtr.browse.profile', 'crtr.browse.residents', 'crtr.browse.preview',
|
|
25
|
+
'crtr.browse.pin', 'crtr.browse.search',
|
|
25
26
|
'crtr.browse.revive-now', 'crtr.browse.focus', 'crtr.browse.focus-background', 'crtr.browse.yank-id',
|
|
26
27
|
];
|
|
27
28
|
const BROWSE_SEARCH_ACTIONS = [
|
|
@@ -40,14 +41,16 @@ export function resolveBrowseSearchAction(bindings, raw) {
|
|
|
40
41
|
// Sort cycle for the `s` key. Starts on the default `attention` ordering; one `s`
|
|
41
42
|
// press restores the structural `tree` view, then relevance/recency, then back.
|
|
42
43
|
const SORT_CYCLE = ['attention', 'tree', 'relevance', 'recency'];
|
|
43
|
-
import { renderFrame, detectColorCaps, headerHeight, PREVIEW_HEIGHT } from './render.js';
|
|
44
|
+
import { renderFrame, detectColorCaps, headerHeight, previewLayout, PREVIEW_HEIGHT } from './render.js';
|
|
44
45
|
import { writeClipboardTextEverywhere } from '../../clipboard-text.js';
|
|
45
46
|
/** Viewport (body) height = total rows minus the header renderFrame draws (see
|
|
46
|
-
* render.ts headerHeight), the footer, and the preview panel when
|
|
47
|
-
*
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
* render.ts headerHeight), the footer, and the BOTTOM preview panel when that is
|
|
48
|
+
* the layout in play. The side preview column costs columns, not rows, so it does
|
|
49
|
+
* not shrink the viewport. Kept in lockstep with render.ts via the shared
|
|
50
|
+
* headerHeight/previewLayout/PREVIEW_HEIGHT. */
|
|
51
|
+
function viewportHeight(size, inputLine, previewOn) {
|
|
52
|
+
const rows = Math.max(8, size.rows);
|
|
53
|
+
const previewH = previewLayout(size.cols, previewOn).mode === 'bottom' ? PREVIEW_HEIGHT : 0;
|
|
51
54
|
return Math.max(1, rows - headerHeight(inputLine) - 1 /* footer */ - previewH);
|
|
52
55
|
}
|
|
53
56
|
export async function runBrowse(opts) {
|
|
@@ -117,7 +120,7 @@ export async function runBrowse(opts) {
|
|
|
117
120
|
cwdScope: null, // default: All dirs
|
|
118
121
|
profileScope: launchProfileId,
|
|
119
122
|
sort: 'attention', // default: attention ordering (attached/streaming/live first)
|
|
120
|
-
preview:
|
|
123
|
+
preview: false, // default OFF (decision) — `v` opens it for the hovered row
|
|
121
124
|
residentsOnly: true, // default ON: hide one-shot workers (decision)
|
|
122
125
|
pendingClose: null,
|
|
123
126
|
flash: null,
|
|
@@ -218,7 +221,7 @@ export async function runBrowse(opts) {
|
|
|
218
221
|
const size = getTerminalSize();
|
|
219
222
|
const previewOn = state.preview && visible.length > 0;
|
|
220
223
|
const inputMode = state.search || state.rename || state.message;
|
|
221
|
-
const viewport = viewportHeight(size
|
|
224
|
+
const viewport = viewportHeight(size, inputMode, previewOn);
|
|
222
225
|
// Keep the cursor inside the viewport window.
|
|
223
226
|
if (state.cursor < state.scrollOffset)
|
|
224
227
|
state.scrollOffset = state.cursor;
|
|
@@ -708,7 +711,7 @@ export async function runBrowse(opts) {
|
|
|
708
711
|
return;
|
|
709
712
|
}
|
|
710
713
|
// Pin or unpin in the local, view-only pin set.
|
|
711
|
-
if (
|
|
714
|
+
if (action === 'crtr.browse.pin') {
|
|
712
715
|
if (row !== undefined)
|
|
713
716
|
togglePin(row.id);
|
|
714
717
|
scheduleFlush();
|
|
@@ -201,13 +201,28 @@ export function previewSnippet(query, text, width, maxLines) {
|
|
|
201
201
|
// 2. Recompute highlight indices on the FINAL snippet string (no drift), then
|
|
202
202
|
// word-wrap with offset tracking and split the highlight into per-line cols.
|
|
203
203
|
const hi = highlightIndices(query, snippet);
|
|
204
|
-
|
|
204
|
+
const wrapped = wrapTracked(snippet, width, maxLines);
|
|
205
|
+
const lines = wrapped.map(({ text: lt, start }) => {
|
|
205
206
|
const lhi = new Set();
|
|
206
207
|
for (const idx of hi)
|
|
207
208
|
if (idx >= start && idx < start + lt.length)
|
|
208
209
|
lhi.add(idx - start);
|
|
209
210
|
return { text: lt, hi: lhi };
|
|
210
211
|
});
|
|
212
|
+
// 3. Mark a truncation. Without this the last line reads as the end of the text,
|
|
213
|
+
// which is exactly the wrong impression for "where did this node leave off?".
|
|
214
|
+
const last = wrapped[wrapped.length - 1];
|
|
215
|
+
const lastLine = lines[lines.length - 1];
|
|
216
|
+
if (last !== undefined && lastLine !== undefined && last.start + last.text.length < snippet.length) {
|
|
217
|
+
const keep = Math.max(0, Math.min(lastLine.text.length, width - 1));
|
|
218
|
+
const text = lastLine.text.slice(0, keep);
|
|
219
|
+
const lhi = new Set();
|
|
220
|
+
for (const idx of lastLine.hi)
|
|
221
|
+
if (idx < keep)
|
|
222
|
+
lhi.add(idx);
|
|
223
|
+
lines[lines.length - 1] = { text: `${text}…`, hi: lhi };
|
|
224
|
+
}
|
|
225
|
+
return lines;
|
|
211
226
|
}
|
|
212
227
|
function shortId(id) {
|
|
213
228
|
return id.slice(0, 8);
|
|
@@ -2,10 +2,21 @@ import type { Tab, Tree, VisibleRow, SortMode } from './model.js';
|
|
|
2
2
|
import { detectColorCaps, type ColorCaps } from '../../tui/draw.js';
|
|
3
3
|
export { detectColorCaps };
|
|
4
4
|
export type { ColorCaps };
|
|
5
|
-
export declare const
|
|
6
|
-
export declare const
|
|
7
|
-
export declare const PREVIEW_BODY
|
|
5
|
+
export declare const RECAP_LINES = 2;
|
|
6
|
+
export declare const REPORT_LINES = 2;
|
|
7
|
+
export declare const PREVIEW_BODY = 8;
|
|
8
8
|
export declare const PREVIEW_HEIGHT: number;
|
|
9
|
+
export type PreviewMode = 'none' | 'bottom' | 'side';
|
|
10
|
+
/** Where the preview goes at this terminal width, and how the body splits.
|
|
11
|
+
* `rowsW` is the width the row table draws into (full width unless `side`).
|
|
12
|
+
* Exported so app.ts's viewport math can subtract preview ROWS only in `bottom`
|
|
13
|
+
* mode — the side column costs columns, not rows. */
|
|
14
|
+
export interface PreviewLayout {
|
|
15
|
+
mode: PreviewMode;
|
|
16
|
+
panelW: number;
|
|
17
|
+
rowsW: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function previewLayout(cols: number, previewOn: boolean): PreviewLayout;
|
|
9
20
|
export declare function headerHeight(inputLine: boolean): number;
|
|
10
21
|
export interface RenderState {
|
|
11
22
|
tree: Tree;
|
|
@@ -52,6 +63,15 @@ export interface RenderState {
|
|
|
52
63
|
* dead action. undefined/false = local, byte-identical footer. */
|
|
53
64
|
readOnly?: boolean;
|
|
54
65
|
}
|
|
66
|
+
/** How the fixed PREVIEW_BODY budget is divided for one row: the optional blocks
|
|
67
|
+
* take their two lines only when the node actually has them, and the remainder is
|
|
68
|
+
* split between the ask and the reply (ask keeps the odd line). */
|
|
69
|
+
export declare function previewAllocation(hasRecap: boolean, hasReport: boolean): {
|
|
70
|
+
recap: number;
|
|
71
|
+
ask: number;
|
|
72
|
+
reply: number;
|
|
73
|
+
report: number;
|
|
74
|
+
};
|
|
55
75
|
/**
|
|
56
76
|
* Render the whole frame. Returns a single string that, written as-is, repaints
|
|
57
77
|
* the screen in place. `caps` gates all hue (defaults to a no-color frame so
|