@north-light/crouter 0.3.161 → 0.3.163
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 +3 -1
- package/dist/api/client.js +6 -0
- package/dist/api/dto/modelauth.d.ts +32 -0
- package/dist/api/dto/modelauth.js +2 -1
- package/dist/api/routes.d.ts +1 -0
- package/dist/api/routes.js +1 -0
- package/dist/builtin-memory/00-runtime-base.md +1 -0
- package/dist/builtin-memory/04-orchestration-kernel.md +1 -0
- package/dist/builtin-memory/init.md +38 -0
- package/dist/builtin-memory/wedged-child-on-runaway-bash.md +2 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +0 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/provider-rotation.test.ts +88 -396
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +215 -419
- package/dist/clients/attach/__tests__/crtr-output.test.js +14 -9
- package/dist/clients/attach/__tests__/edit-diff.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/edit-diff.test.js +34 -0
- package/dist/clients/attach/__tests__/editor-frame-title.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/editor-frame-title.test.js +19 -0
- package/dist/clients/attach/render/chat-view.js +8 -3
- package/dist/clients/attach/render/crtr-output.js +31 -2
- package/dist/clients/attach/render/edit-diff.js +26 -2
- package/dist/clients/attach/render/tool-calls.d.ts +7 -2
- package/dist/clients/attach/render/tool-calls.js +25 -4
- package/dist/clients/attach/session/connection.d.ts +1 -1
- package/dist/clients/attach/session/editor-frame.d.ts +5 -1
- package/dist/clients/attach/session/editor-frame.js +21 -15
- package/dist/clients/attach/session/identity.d.ts +1 -1
- package/dist/clients/attach/viewer.js +441 -439
- package/dist/commands/__tests__/api-canvas-source.test.d.ts +1 -0
- package/dist/commands/__tests__/api-canvas-source.test.js +37 -0
- package/dist/commands/__tests__/human.test.js +2 -0
- package/dist/commands/__tests__/search-contents.test.d.ts +1 -0
- package/dist/commands/__tests__/search-contents.test.js +52 -0
- package/dist/commands/api-client.js +1 -0
- package/dist/commands/human/prompts.js +2 -2
- package/dist/commands/human/shared.d.ts +1 -0
- package/dist/commands/human/shared.js +7 -4
- package/dist/commands/memory/lint.d.ts +6 -0
- package/dist/commands/memory/lint.js +177 -35
- package/dist/commands/memory/read.js +22 -1
- package/dist/commands/memory/write.js +18 -4
- package/dist/commands/memory.js +1 -1
- package/dist/commands/search/contents.js +2 -2
- package/dist/commands/sys/__tests__/sync-deps.test.js +12 -21
- package/dist/commands/sys/__tests__/sync-import.test.js +30 -29
- package/dist/commands/sys/setup-wizard.d.ts +8 -1
- package/dist/commands/sys/setup-wizard.js +209 -58
- package/dist/commands/sys/sync-deps.js +14 -18
- package/dist/commands/sys/sync-project-guidance.js +16 -19
- package/dist/core/__tests__/broker-launch-candidates.test.js +13 -3
- package/dist/core/__tests__/broker-sdk-wiring.test.js +53 -25
- package/dist/core/__tests__/full/broker-provider-retry.test.js +6 -122
- package/dist/core/__tests__/migration.test.js +8 -3
- package/dist/core/__tests__/model-pin-durability.test.js +7 -5
- package/dist/core/__tests__/model-routes.test.d.ts +1 -0
- package/dist/core/__tests__/model-routes.test.js +41 -0
- package/dist/core/__tests__/on-read-crouter-home-fence.test.js +7 -10
- package/dist/core/__tests__/on-read-dedup-resume.test.js +13 -25
- package/dist/core/__tests__/on-read-identity.test.js +8 -15
- package/dist/core/__tests__/review-model-floor.test.js +2 -2
- package/dist/core/__tests__/revive.test.js +2 -2
- package/dist/core/__tests__/tmux-surface.test.js +10 -4
- package/dist/core/__tests__/worktree.test.js +3 -3
- package/dist/core/auth-file.d.ts +3 -2
- package/dist/core/auth-file.js +5 -3
- package/dist/core/canvas/extensions.d.ts +1 -1
- package/dist/core/canvas/extensions.js +18 -11
- package/dist/core/canvas/labels.d.ts +4 -5
- package/dist/core/canvas/labels.js +9 -9
- package/dist/core/canvas/types.d.ts +6 -1
- package/dist/core/command-manifests/schema.js +18 -5
- package/dist/core/command.js +17 -7
- package/dist/core/config.d.ts +3 -1
- package/dist/core/config.js +88 -2
- package/dist/core/configured-clis/invoker.js +2 -0
- package/dist/core/help.d.ts +3 -1
- package/dist/core/help.js +5 -2
- package/dist/core/keybindings/__tests__/resolve.test.js +5 -2
- package/dist/core/keybindings/catalog.d.ts +3 -2
- package/dist/core/keybindings/catalog.js +34 -25
- package/dist/core/keybindings/index.d.ts +1 -1
- package/dist/core/keybindings/resolve.js +8 -0
- package/dist/core/keybindings/types.d.ts +6 -1
- package/dist/core/memory/doc-link-grammar.d.ts +20 -0
- package/dist/core/memory/doc-link-grammar.js +110 -0
- package/dist/core/memory-resolver.d.ts +5 -0
- package/dist/core/memory-resolver.js +12 -1
- package/dist/core/model-routes.d.ts +37 -0
- package/dist/core/model-routes.js +140 -0
- package/dist/core/runtime/bearings.d.ts +8 -8
- package/dist/core/runtime/bearings.js +20 -16
- package/dist/core/runtime/broker.d.ts +0 -4
- package/dist/core/runtime/broker.js +119 -385
- package/dist/core/runtime/canvas-extensions.d.ts +1 -0
- package/dist/core/runtime/canvas-extensions.js +2 -0
- package/dist/core/runtime/launch.d.ts +10 -12
- package/dist/core/runtime/launch.js +16 -22
- package/dist/core/runtime/managed-provider-cooling.d.ts +0 -14
- package/dist/core/runtime/managed-provider-cooling.js +0 -19
- package/dist/core/runtime/model-swap.js +7 -1
- package/dist/core/runtime/promote.js +1 -0
- package/dist/core/runtime/tmux.js +136 -100
- package/dist/core/scope.js +4 -5
- package/dist/core/subscription-state.d.ts +12 -36
- package/dist/core/subscription-state.js +19 -203
- package/dist/core/substrate/ceiling.d.ts +3 -6
- package/dist/core/substrate/ceiling.js +13 -15
- package/dist/core/substrate/index.d.ts +2 -3
- package/dist/core/substrate/index.js +2 -2
- package/dist/core/substrate/on-read.d.ts +4 -13
- package/dist/core/substrate/on-read.js +142 -262
- package/dist/core/substrate/render.js +4 -4
- package/dist/core/substrate/schema.d.ts +3 -2
- package/dist/core/substrate/schema.js +5 -5
- package/dist/daemon/api/__tests__/full/api-server.test.js +9 -6
- package/dist/daemon/api/__tests__/full/b10-attach-modelauth.test.js +123 -1
- package/dist/daemon/api/handlers/modelauth.js +51 -2
- package/dist/daemon/api/handlers/nodes.js +2 -0
- package/dist/pi-extensions/__tests__/canvas-tool-guide.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-tool-guide.test.js +96 -0
- package/dist/pi-extensions/canvas-doc-substrate.js +7 -8
- package/dist/pi-extensions/canvas-tool-guide.d.ts +14 -0
- package/dist/pi-extensions/canvas-tool-guide.js +74 -0
- package/dist/types.d.ts +17 -0
- package/dist/web-client/assets/{index-B76ZKfT_.js → index-NIuSCOHM.js} +1 -1
- package/dist/web-client/index.html +1 -1
- package/dist/web-client/sw.js +1 -1
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/crouter-help.ts +0 -95
|
@@ -16,6 +16,11 @@ const plainTheme = {
|
|
|
16
16
|
bg(_name, text) { return text; },
|
|
17
17
|
bold(text) { return text; },
|
|
18
18
|
};
|
|
19
|
+
/** pi calls tool renderers with a shared state bag; a normal (unfolded) call has an empty one. */
|
|
20
|
+
function renderCrtrCall(definition, args) {
|
|
21
|
+
const definitionWithContext = definition;
|
|
22
|
+
return definitionWithContext.renderCall(args, theme, { state: {} });
|
|
23
|
+
}
|
|
19
24
|
function stripAnsi(text) {
|
|
20
25
|
return text.replace(/\x1b\[[0-9;]*m/g, '');
|
|
21
26
|
}
|
|
@@ -31,7 +36,7 @@ test('returns a viewer tool definition only for crtr bash commands', () => {
|
|
|
31
36
|
assert.equal(createCrtrBashToolDefinition({ command: 'echo hi' }), undefined);
|
|
32
37
|
const def = createCrtrBashToolDefinition({ command: 'crtr canvas dashboard' });
|
|
33
38
|
assert.equal(typeof def.renderCall, 'function');
|
|
34
|
-
const rendered = def
|
|
39
|
+
const rendered = renderCrtrCall(def, { command: 'crtr canvas dashboard' }).render(80).join('\n');
|
|
35
40
|
assert.equal(rendered.trimEnd().split('\n').length, 1);
|
|
36
41
|
assert.match(rendered, new RegExp(iconForPath(['canvas', 'dashboard'])));
|
|
37
42
|
assert.match(rendered, /crtr/);
|
|
@@ -165,7 +170,7 @@ test('a branch help preview with more subcommands than the line cap truncates wi
|
|
|
165
170
|
// displayed crtr path and help detection include only the command segment.
|
|
166
171
|
test('a shell redirect on the outer bash call never leaks into the displayed crtr path or help detection', () => {
|
|
167
172
|
const def = createCrtrBashToolDefinition({ command: 'crtr push -h 2>&1' });
|
|
168
|
-
const rendered = def
|
|
173
|
+
const rendered = renderCrtrCall(def, { command: 'crtr push -h 2>&1' }).render(80).join('\n');
|
|
169
174
|
assert.match(rendered, new RegExp(HELP_ICON));
|
|
170
175
|
assert.match(rendered, /crtr help/);
|
|
171
176
|
assert.match(rendered, /<b>push<\/b>/);
|
|
@@ -175,18 +180,18 @@ test('a shell redirect on the outer bash call never leaks into the displayed crt
|
|
|
175
180
|
// Other redirect shapes on the outer call, and mid-argv redirects, must
|
|
176
181
|
// never survive into the path either.
|
|
177
182
|
const other = createCrtrBashToolDefinition({ command: 'crtr node new --name foo > /tmp/log 2>&1' });
|
|
178
|
-
const otherRendered = other
|
|
183
|
+
const otherRendered = renderCrtrCall(other, { command: 'crtr node new --name foo > /tmp/log 2>&1' }).render(80).join('\n');
|
|
179
184
|
assert.match(otherRendered, /node new --name foo/);
|
|
180
185
|
assert.doesNotMatch(otherRendered, /tmp\/log/);
|
|
181
186
|
// Redirect operators glued to their target with NO space (`2>/tmp/out`,
|
|
182
187
|
// `>/tmp/log`) are still shell plumbing and never displayed as path text.
|
|
183
188
|
const noSpace = createCrtrBashToolDefinition({ command: 'crtr push -h 2>/tmp/out' });
|
|
184
|
-
const noSpaceRendered = noSpace
|
|
189
|
+
const noSpaceRendered = renderCrtrCall(noSpace, { command: 'crtr push -h 2>/tmp/out' }).render(80).join('\n');
|
|
185
190
|
assert.match(noSpaceRendered, /<b>push<\/b>/);
|
|
186
191
|
assert.doesNotMatch(noSpaceRendered, /2>/);
|
|
187
192
|
assert.doesNotMatch(noSpaceRendered, /tmp\/out/);
|
|
188
193
|
const noSpaceMid = createCrtrBashToolDefinition({ command: 'crtr node new --name foo >/tmp/log' });
|
|
189
|
-
const noSpaceMidRendered = noSpaceMid
|
|
194
|
+
const noSpaceMidRendered = renderCrtrCall(noSpaceMid, { command: 'crtr node new --name foo >/tmp/log' }).render(80).join('\n');
|
|
190
195
|
assert.match(noSpaceMidRendered, /node new --name foo/);
|
|
191
196
|
assert.doesNotMatch(noSpaceMidRendered, /tmp\/log/);
|
|
192
197
|
});
|
|
@@ -194,7 +199,7 @@ test('a shell redirect on the outer bash call never leaks into the displayed crt
|
|
|
194
199
|
// body contains only synthesized preview content, never a repeated header.
|
|
195
200
|
test('a collapsed help card never shows the help label twice (header + body)', () => {
|
|
196
201
|
const def = createCrtrBashToolDefinition({ command: 'crtr canvas dashboard -h' });
|
|
197
|
-
const header = def
|
|
202
|
+
const header = renderCrtrCall(def, { command: 'crtr canvas dashboard -h' }).render(80).join('\n');
|
|
198
203
|
const body = def
|
|
199
204
|
.renderResult({ content: [{ type: 'text', text: LEAF_SCHEMA }] }, { expanded: false }, theme, {})
|
|
200
205
|
.render(80)
|
|
@@ -414,7 +419,7 @@ test('a crtr memory lint with findings collapses to a findings count and warning
|
|
|
414
419
|
// (possibly collapsed) result renders.
|
|
415
420
|
test('renderCall shows a help-specific header for a -h/--help invocation, no bash chrome', () => {
|
|
416
421
|
const def = createCrtrBashToolDefinition({ command: 'crtr node config -h' });
|
|
417
|
-
const rendered = def
|
|
422
|
+
const rendered = renderCrtrCall(def, { command: 'crtr node config -h' }).render(80).join('\n');
|
|
418
423
|
assert.equal(rendered.trimEnd().split('\n').length, 1);
|
|
419
424
|
assert.match(rendered, new RegExp(HELP_ICON));
|
|
420
425
|
assert.match(rendered, /crtr help/);
|
|
@@ -447,7 +452,7 @@ test('renderCall uses a mapped Nerd Font glyph and one line for every mapped roo
|
|
|
447
452
|
// strip for width purposes), so the visible-width accounting used by the
|
|
448
453
|
// real Text component's word-wrap counts that markup as content width —
|
|
449
454
|
// a generous width keeps every case here well clear of an unrelated wrap.
|
|
450
|
-
const rendered = def
|
|
455
|
+
const rendered = renderCrtrCall(def, { command }).render(200).join('\n');
|
|
451
456
|
assert.equal(rendered.trimEnd().split('\n').length, 1, `expected one line for ${command}`);
|
|
452
457
|
assert.match(rendered, new RegExp(iconForPath(iconTokens)), `expected the ${iconTokens.join(' ')} glyph in ${command}`);
|
|
453
458
|
assert.match(rendered, /crtr/, `expected "crtr" in ${command}`);
|
|
@@ -457,7 +462,7 @@ test('renderCall uses a mapped Nerd Font glyph and one line for every mapped roo
|
|
|
457
462
|
});
|
|
458
463
|
test('renderCall for a normal invocation previews the crtr path, not a raw bash echo', () => {
|
|
459
464
|
const def = createCrtrBashToolDefinition({ command: 'crtr node config set foo bar' });
|
|
460
|
-
const rendered = def
|
|
465
|
+
const rendered = renderCrtrCall(def, { command: 'crtr node config set foo bar' }).render(200).join('\n');
|
|
461
466
|
assert.equal(rendered.trimEnd().split('\n').length, 1);
|
|
462
467
|
assert.match(rendered, new RegExp(iconForPath(['node', 'config'])));
|
|
463
468
|
assert.match(rendered, /crtr/);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { initTheme } from '@earendil-works/pi-coding-agent';
|
|
4
|
+
import { FOLDED_STATE_KEY } from '../render/tool-calls.js';
|
|
5
|
+
import { createCrtrEditToolDefinition } from '../render/edit-diff.js';
|
|
6
|
+
initTheme(undefined, false);
|
|
7
|
+
const theme = {
|
|
8
|
+
fg: (_name, text) => text,
|
|
9
|
+
bg: (_name, text) => text,
|
|
10
|
+
bold: (text) => text,
|
|
11
|
+
getFgAnsi: (_name) => '',
|
|
12
|
+
getBgAnsi: (_name) => '',
|
|
13
|
+
getColorMode: () => 'truecolor',
|
|
14
|
+
};
|
|
15
|
+
test('folded edit counts keep the executed diff when its speculative preview settles later', () => {
|
|
16
|
+
const definition = createCrtrEditToolDefinition(process.cwd());
|
|
17
|
+
const args = { path: 'example.ts', edits: [{ oldText: 'before', newText: 'after' }] };
|
|
18
|
+
const state = {};
|
|
19
|
+
const context = {
|
|
20
|
+
args,
|
|
21
|
+
argsComplete: false,
|
|
22
|
+
cwd: process.cwd(),
|
|
23
|
+
invalidate() { },
|
|
24
|
+
isError: false,
|
|
25
|
+
state,
|
|
26
|
+
};
|
|
27
|
+
definition.renderCall(args, theme, context);
|
|
28
|
+
definition.renderResult({ content: [{ type: 'text', text: 'edited' }], details: { diff: '- 1 before\n+ 1 after' } }, { isPartial: false }, theme, context);
|
|
29
|
+
state.callComponent.preview = { error: 'speculative preview completed after execution' };
|
|
30
|
+
state[FOLDED_STATE_KEY] = true;
|
|
31
|
+
const folded = definition.renderCall(args, theme, context).render(80).join('\n');
|
|
32
|
+
assert.match(folded, /\+1/);
|
|
33
|
+
assert.match(folded, /-1/);
|
|
34
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { editorLabel } from '../../../core/canvas/labels.js';
|
|
4
|
+
import { displayEditorTitle } from '../session/editor-frame.js';
|
|
5
|
+
test('a reloaded renamed session still shows its generated icon', () => {
|
|
6
|
+
const identity = { kind: 'general', mode: 'base', icon: '\uf188' };
|
|
7
|
+
assert.equal(displayEditorTitle('test', identity), '\uf188 test');
|
|
8
|
+
assert.equal(displayEditorTitle('\uf188 test', identity), '\uf188 test');
|
|
9
|
+
});
|
|
10
|
+
test('a node restart restores the explicit editor name instead of the generated description', () => {
|
|
11
|
+
const meta = {
|
|
12
|
+
name: 'test',
|
|
13
|
+
kind: 'general',
|
|
14
|
+
mode: 'base',
|
|
15
|
+
description: 'preserve-icon-on-rename',
|
|
16
|
+
icon: '\uf188',
|
|
17
|
+
};
|
|
18
|
+
assert.equal(editorLabel(meta), '\uf188 test');
|
|
19
|
+
});
|
|
@@ -125,10 +125,15 @@ function stripBackground(line) {
|
|
|
125
125
|
const kept = [];
|
|
126
126
|
for (let i = 0; i < parts.length; i++) {
|
|
127
127
|
const code = Number(parts[i] === '' ? 0 : parts[i]);
|
|
128
|
-
// Extended
|
|
129
|
-
|
|
128
|
+
// Extended colors (`38` foreground, `48` background, `58` underline):
|
|
129
|
+
// consume the full sequence atomically so an RGB channel in the 40-47 or
|
|
130
|
+
// 100-107 range is not mistaken for a standalone background code.
|
|
131
|
+
if (code === 38 || code === 48 || code === 58) {
|
|
130
132
|
const mode = Number(parts[i + 1]);
|
|
131
|
-
i
|
|
133
|
+
const end = i + (mode === 5 ? 2 : mode === 2 ? 4 : 1);
|
|
134
|
+
if (code !== 48)
|
|
135
|
+
kept.push(...parts.slice(i, end + 1));
|
|
136
|
+
i = end;
|
|
132
137
|
continue;
|
|
133
138
|
}
|
|
134
139
|
// Standard (40-47) and bright (100-107) backgrounds. `49` (reset to the
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { truncateToVisualLines } from '@earendil-works/pi-coding-agent';
|
|
2
2
|
import { Box, Text, truncateToWidth, visibleWidth, wrapTextWithAnsi } from '@earendil-works/pi-tui';
|
|
3
3
|
import { HELP_ICON, iconForPath, suppressOutputForPath, summarizePath, } from '../../../core/preview-registry.js';
|
|
4
|
+
import { isFoldedContext } from './tool-calls.js';
|
|
4
5
|
// Hard cap on a collapsed crtr card's total rendered lines (rule 12,
|
|
5
6
|
// inline-ui-placement): help synthesis or a normal truncated result never
|
|
6
7
|
// shows more than this many lines total, blank spacer + status + body +
|
|
@@ -148,6 +149,32 @@ function commandFromArgs(args) {
|
|
|
148
149
|
const command = args.command;
|
|
149
150
|
return typeof command === 'string' ? command : '';
|
|
150
151
|
}
|
|
152
|
+
const CRTR_FLAG_TOKEN = /^-{1,2}[A-Za-z0-9]/;
|
|
153
|
+
const CRTR_ATTACHED_FLAG_VALUE = /^(--?[A-Za-z0-9][^=]*)(=)(.*)$/;
|
|
154
|
+
const CRTR_NUMBER_VALUE = /^-?(?:\d+(?:\.\d+)?|\.\d+)$/;
|
|
155
|
+
/** Syntax-color flags and their values while leaving the command path and
|
|
156
|
+
* positional arguments in the folded row's existing bold title style. */
|
|
157
|
+
function styleFoldedCrtrTokens(tokens, theme) {
|
|
158
|
+
let expectsValue = false;
|
|
159
|
+
return tokens.map((token) => {
|
|
160
|
+
const attached = CRTR_ATTACHED_FLAG_VALUE.exec(token);
|
|
161
|
+
if (attached !== null) {
|
|
162
|
+
expectsValue = false;
|
|
163
|
+
const valueColor = CRTR_NUMBER_VALUE.test(attached[3]) ? 'syntaxNumber' : 'syntaxString';
|
|
164
|
+
return `${theme.fg('syntaxKeyword', attached[1])}${theme.fg('syntaxOperator', attached[2])}${theme.fg(valueColor, attached[3])}`;
|
|
165
|
+
}
|
|
166
|
+
if (expectsValue && !CRTR_FLAG_TOKEN.test(token)) {
|
|
167
|
+
expectsValue = false;
|
|
168
|
+
return theme.fg(CRTR_NUMBER_VALUE.test(token) ? 'syntaxNumber' : 'syntaxString', token);
|
|
169
|
+
}
|
|
170
|
+
if (CRTR_FLAG_TOKEN.test(token)) {
|
|
171
|
+
expectsValue = true;
|
|
172
|
+
return theme.fg('syntaxKeyword', token);
|
|
173
|
+
}
|
|
174
|
+
expectsValue = false;
|
|
175
|
+
return theme.fg('toolTitle', theme.bold(token));
|
|
176
|
+
}).join(' ');
|
|
177
|
+
}
|
|
151
178
|
function resultText(result) {
|
|
152
179
|
return (result.content ?? [])
|
|
153
180
|
.filter((part) => part?.type === 'text' || typeof part?.text === 'string')
|
|
@@ -670,7 +697,7 @@ export function createCrtrBashToolDefinition(args) {
|
|
|
670
697
|
label: 'bash',
|
|
671
698
|
description: 'Render crtr bash output with crtr-specific chrome in the attach viewer.',
|
|
672
699
|
parameters: {},
|
|
673
|
-
renderCall(callArgs, theme) {
|
|
700
|
+
renderCall(callArgs, theme, context) {
|
|
674
701
|
const currentCommand = commandFromArgs(callArgs) || command;
|
|
675
702
|
// Semantic card, not a bash echo: the header names what the invocation
|
|
676
703
|
// IS (help schema vs. a crtr command), and the body previews only the
|
|
@@ -684,7 +711,9 @@ export function createCrtrBashToolDefinition(args) {
|
|
|
684
711
|
const icon = isHelp ? HELP_ICON : iconForPath(pathTokens);
|
|
685
712
|
const prefix = isHelp ? `${icon} crtr help` : `${icon} crtr`;
|
|
686
713
|
const styledPrefix = theme.fg('accent', theme.bold(prefix));
|
|
687
|
-
const styledPath = path
|
|
714
|
+
const styledPath = path
|
|
715
|
+
? ` ${isFoldedContext(context) ? styleFoldedCrtrTokens(pathTokens, theme) : theme.fg('toolTitle', theme.bold(path))}`
|
|
716
|
+
: '';
|
|
688
717
|
return new Text(`${styledPrefix}${styledPath}`, 0, 0);
|
|
689
718
|
},
|
|
690
719
|
renderResult(result, options, theme, context) {
|
|
@@ -102,6 +102,20 @@ function parseDiffLine(line) {
|
|
|
102
102
|
return null;
|
|
103
103
|
return { prefix: match[1], lineNum: match[2], content: match[3] };
|
|
104
104
|
}
|
|
105
|
+
/** Count only changed rows in pi's display diff; context and ellipsis rows do
|
|
106
|
+
* not contribute to the folded file summary. */
|
|
107
|
+
function countDiffLines(diff) {
|
|
108
|
+
let added = 0;
|
|
109
|
+
let removed = 0;
|
|
110
|
+
for (const line of diff.split('\n')) {
|
|
111
|
+
const parsed = parseDiffLine(line);
|
|
112
|
+
if (parsed?.prefix === '+')
|
|
113
|
+
added++;
|
|
114
|
+
if (parsed?.prefix === '-')
|
|
115
|
+
removed++;
|
|
116
|
+
}
|
|
117
|
+
return { added, removed };
|
|
118
|
+
}
|
|
105
119
|
function replaceTabs(text) {
|
|
106
120
|
return text.replace(/\t/g, ' ');
|
|
107
121
|
}
|
|
@@ -247,7 +261,11 @@ export function createCrtrEditToolDefinition(cwd) {
|
|
|
247
261
|
if (isFoldedContext(context)) {
|
|
248
262
|
const a = args;
|
|
249
263
|
const raw = typeof a?.file_path === 'string' ? a.file_path : typeof a?.path === 'string' ? a.path : '';
|
|
250
|
-
|
|
264
|
+
const preview = context.state.callComponent?.preview;
|
|
265
|
+
const changes = context.isError
|
|
266
|
+
? undefined
|
|
267
|
+
: context.state.settledChanges ?? (preview && !('error' in preview) ? countDiffLines(preview.diff) : undefined);
|
|
268
|
+
return new Text(foldedFileCallLine('edit', raw, theme, context.cwd ?? cwd, changes), 0, 0);
|
|
251
269
|
}
|
|
252
270
|
const component = builtin.renderCall(args, theme, context);
|
|
253
271
|
const callBox = context.state.callComponent;
|
|
@@ -258,6 +276,13 @@ export function createCrtrEditToolDefinition(cwd) {
|
|
|
258
276
|
return component;
|
|
259
277
|
},
|
|
260
278
|
renderResult(result, options, theme, context) {
|
|
279
|
+
const resultDiff = result?.details?.diff;
|
|
280
|
+
if (!options?.isPartial) {
|
|
281
|
+
if (!context.isError && typeof resultDiff === 'string')
|
|
282
|
+
context.state.settledChanges = countDiffLines(resultDiff);
|
|
283
|
+
else
|
|
284
|
+
delete context.state.settledChanges;
|
|
285
|
+
}
|
|
261
286
|
const component = builtin.renderResult(result, options, theme, context);
|
|
262
287
|
const callBox = context.state.callComponent;
|
|
263
288
|
if (callBox) {
|
|
@@ -269,7 +294,6 @@ export function createCrtrEditToolDefinition(cwd) {
|
|
|
269
294
|
}
|
|
270
295
|
// Fallback path (no call box state, e.g. a bare result replay): render
|
|
271
296
|
// the result diff standalone. No host Box here, so restore to default bg.
|
|
272
|
-
const resultDiff = result?.details?.diff;
|
|
273
297
|
if (!context.isError && typeof resultDiff === 'string') {
|
|
274
298
|
const container = new Container();
|
|
275
299
|
container.addChild(new Spacer(1));
|
|
@@ -11,8 +11,13 @@ export declare const FOLDED_STATE_KEY = "crtrFolded";
|
|
|
11
11
|
export declare function isFoldedContext(context: {
|
|
12
12
|
state?: object;
|
|
13
13
|
}): boolean;
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
export type FileLineChanges = {
|
|
15
|
+
added: number;
|
|
16
|
+
removed: number;
|
|
17
|
+
};
|
|
18
|
+
/** The one-line folded form of a file tool's call: `<icon> <label> <path>`,
|
|
19
|
+
* optionally followed by its green/red changed-line counts. */
|
|
20
|
+
export declare function foldedFileCallLine(tool: 'read' | 'write' | 'edit', rawPath: string, theme: Theme, cwd: string, changes?: FileLineChanges): string;
|
|
16
21
|
/** Prefix a pi-built `Text` in place (its `text` field is TS-private but a plain
|
|
17
22
|
* runtime property). Used where the header line is baked into the same string as
|
|
18
23
|
* a body we want to keep verbatim — write's syntax-highlighted content preview,
|
|
@@ -28,9 +28,29 @@ export const FOLDED_STATE_KEY = 'crtrFolded';
|
|
|
28
28
|
export function isFoldedContext(context) {
|
|
29
29
|
return context.state?.[FOLDED_STATE_KEY] === true;
|
|
30
30
|
}
|
|
31
|
-
/**
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
/** Count file lines the way a diff does: a trailing newline terminates the last
|
|
32
|
+
* line instead of creating an extra empty one. */
|
|
33
|
+
function countFileLines(content) {
|
|
34
|
+
if (content.length === 0)
|
|
35
|
+
return 0;
|
|
36
|
+
const newlines = content.match(/\n/g)?.length ?? 0;
|
|
37
|
+
return newlines + (content.endsWith('\n') ? 0 : 1);
|
|
38
|
+
}
|
|
39
|
+
/** The green/red line-count suffix shown after a folded write/edit path. */
|
|
40
|
+
function lineChangesDisplay(changes, theme) {
|
|
41
|
+
if (!changes)
|
|
42
|
+
return '';
|
|
43
|
+
const counts = [];
|
|
44
|
+
if (changes.added > 0)
|
|
45
|
+
counts.push(theme.fg('toolDiffAdded', `+${changes.added}`));
|
|
46
|
+
if (changes.removed > 0)
|
|
47
|
+
counts.push(theme.fg('toolDiffRemoved', `-${changes.removed}`));
|
|
48
|
+
return counts.length > 0 ? ` ${counts.join(' ')}` : '';
|
|
49
|
+
}
|
|
50
|
+
/** The one-line folded form of a file tool's call: `<icon> <label> <path>`,
|
|
51
|
+
* optionally followed by its green/red changed-line counts. */
|
|
52
|
+
export function foldedFileCallLine(tool, rawPath, theme, cwd, changes) {
|
|
53
|
+
return `${header(TOOL_ICON[tool], tool, theme)} ${pathDisplay(rawPath, theme, cwd)}${lineChangesDisplay(changes, theme)}`;
|
|
34
54
|
}
|
|
35
55
|
/** `~`-shorten an absolute path the way pi's renderToolPath does (its
|
|
36
56
|
* render-utils module is not exported from the package root). */
|
|
@@ -113,7 +133,8 @@ export function createCrtrWriteToolDefinition(cwd) {
|
|
|
113
133
|
renderCall(args, theme, context) {
|
|
114
134
|
if (isFoldedContext(context)) {
|
|
115
135
|
const a = args;
|
|
116
|
-
const
|
|
136
|
+
const changes = context.isError ? undefined : { added: countFileLines(str(a?.content)), removed: 0 };
|
|
137
|
+
const line = foldedFileCallLine('write', str(a?.file_path ?? a?.path), theme, context.cwd ?? cwd, changes);
|
|
117
138
|
// Reuse the previous component when it is Text-like (the builtin's is a
|
|
118
139
|
// Text subclass), so fold toggles don't churn component identity.
|
|
119
140
|
const last = context.lastComponent;
|
|
@@ -30,7 +30,7 @@ export interface AttachConnection {
|
|
|
30
30
|
readonly meta: Pick<NodeMeta, 'cwd' | 'profile_id'>;
|
|
31
31
|
/** The attached node's persona/lifecycle, when this canvas can tell us —
|
|
32
32
|
* `undefined` for a remote attach until the viewer refetches it. */
|
|
33
|
-
readonly nodeIdentity: Pick<NodeMeta, 'kind' | 'mode' | 'lifecycle'> | undefined;
|
|
33
|
+
readonly nodeIdentity: Pick<NodeMeta, 'kind' | 'mode' | 'lifecycle' | 'icon'> | undefined;
|
|
34
34
|
/** The one socket to the node's headless broker, already connected. */
|
|
35
35
|
readonly socket: BrokerClient;
|
|
36
36
|
/** Re-throw a connect-adjacent failure that landed AFTER `connect` resolved.
|
|
@@ -14,7 +14,7 @@ export declare function resolveEditorFrameStyles(bashMode: 0 | 1 | 2, thinkingLe
|
|
|
14
14
|
export interface EditorChromeHooks {
|
|
15
15
|
/** The viewer's live node identity — a `let` runAttach owns and refreshes
|
|
16
16
|
* (remote attach starts without one), so it MUST stay a getter. */
|
|
17
|
-
nodeIdentity: () => Pick<NodeMeta, 'kind' | 'mode' | 'lifecycle'> | undefined;
|
|
17
|
+
nodeIdentity: () => Pick<NodeMeta, 'kind' | 'mode' | 'lifecycle' | 'icon'> | undefined;
|
|
18
18
|
/** The engine's current thinking level, from live session state. */
|
|
19
19
|
thinkingLevel: () => BrokerSnapshot['state']['thinkingLevel'] | undefined;
|
|
20
20
|
/** Repaint the status line — the bash-level segment tracks `bashMode`. */
|
|
@@ -36,6 +36,10 @@ export interface EditorChrome {
|
|
|
36
36
|
/** Kick an async git-status fetch that repaints the top border. */
|
|
37
37
|
pollGit: () => void;
|
|
38
38
|
}
|
|
39
|
+
/** Render the generated task icon on every non-empty editor title. Session
|
|
40
|
+
* names are persisted independently by pi, so this derives the icon from the
|
|
41
|
+
* canvas identity again whenever a viewer attaches or reloads. */
|
|
42
|
+
export declare function displayEditorTitle(sessionName: string | undefined, identity: Pick<NodeMeta, 'kind' | 'mode' | 'icon'> | undefined): string;
|
|
39
43
|
/** The editor-chrome collaborator: owns the shell/bash level (`deriveBashMode`
|
|
40
44
|
* over `editor.onChange` is its ONE writer) and the `/color` chip override,
|
|
41
45
|
* and paints every frame-attached adornment — border hue, title chip, context
|
|
@@ -37,6 +37,26 @@ export function resolveEditorFrameStyles(bashMode, thinkingLevel, pal) {
|
|
|
37
37
|
titleStyle: thinkingTitleStyle(thinkingLevel, defaultTitleStyle),
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
|
+
/** Render the generated task icon on every non-empty editor title. Session
|
|
41
|
+
* names are persisted independently by pi, so this derives the icon from the
|
|
42
|
+
* canvas identity again whenever a viewer attaches or reloads. */
|
|
43
|
+
export function displayEditorTitle(sessionName, identity) {
|
|
44
|
+
const raw = sessionName ?? '';
|
|
45
|
+
if (raw === '' || identity === undefined)
|
|
46
|
+
return raw;
|
|
47
|
+
const modePrefix = `${modeIcon(identity.mode)} `;
|
|
48
|
+
const withoutMode = raw.startsWith(modePrefix) ? raw.slice(modePrefix.length) : raw;
|
|
49
|
+
const icon = (identity.icon ?? '').trim();
|
|
50
|
+
const iconPrefix = icon === '' ? '' : `${icon} `;
|
|
51
|
+
const withoutTaskIcon = iconPrefix !== '' && withoutMode.startsWith(iconPrefix)
|
|
52
|
+
? withoutMode.slice(iconPrefix.length)
|
|
53
|
+
: withoutMode;
|
|
54
|
+
const withoutKind = withoutTaskIcon.endsWith(` ${identity.kind}`)
|
|
55
|
+
? withoutTaskIcon.slice(0, -identity.kind.length - 1)
|
|
56
|
+
: withoutTaskIcon;
|
|
57
|
+
const name = withoutKind === identity.kind ? '' : withoutKind;
|
|
58
|
+
return name === '' || icon === '' ? name : `${icon} ${name}`;
|
|
59
|
+
}
|
|
40
60
|
/** The editor-chrome collaborator: owns the shell/bash level (`deriveBashMode`
|
|
41
61
|
* over `editor.onChange` is its ONE writer) and the `/color` chip override,
|
|
42
62
|
* and paints every frame-attached adornment — border hue, title chip, context
|
|
@@ -58,20 +78,6 @@ export function createEditorChrome(ctx, hooks) {
|
|
|
58
78
|
const persona = identity ? ` · ${personaLabel(identity)}` : '';
|
|
59
79
|
editor.contextBadge = `${lifecycle}${dir}${persona}`;
|
|
60
80
|
};
|
|
61
|
-
// Normalize an icon-and-kind session label so persona remains exclusively in
|
|
62
|
-
// the bottom-right badge while the viewer is attached.
|
|
63
|
-
const displaySessionTitle = (sessionName) => {
|
|
64
|
-
const identity = hooks.nodeIdentity();
|
|
65
|
-
const raw = sessionName ?? '';
|
|
66
|
-
if (raw === '' || identity === undefined)
|
|
67
|
-
return raw;
|
|
68
|
-
const legacyPrefix = `${modeIcon(identity.mode)} `;
|
|
69
|
-
const withoutIcon = raw.startsWith(legacyPrefix) ? raw.slice(legacyPrefix.length) : raw;
|
|
70
|
-
const withoutKind = withoutIcon.endsWith(` ${identity.kind}`)
|
|
71
|
-
? withoutIcon.slice(0, -identity.kind.length - 1)
|
|
72
|
-
: withoutIcon;
|
|
73
|
-
return withoutKind === identity.kind ? '' : withoutKind;
|
|
74
|
-
};
|
|
75
81
|
// Paint the editor border + title chip. Shell/bash mode (leading `!`) wins and
|
|
76
82
|
// forces the whole frame green (#7); otherwise the hue tracks the thinking
|
|
77
83
|
// level. One place so the onChange bash toggle and live-state updates agree.
|
|
@@ -128,7 +134,7 @@ export function createEditorChrome(ctx, hooks) {
|
|
|
128
134
|
},
|
|
129
135
|
paintFrame,
|
|
130
136
|
setTitle: (sessionName) => {
|
|
131
|
-
editor.title =
|
|
137
|
+
editor.title = displayEditorTitle(sessionName, hooks.nodeIdentity());
|
|
132
138
|
},
|
|
133
139
|
syncContextBadge: () => syncContextBadge(),
|
|
134
140
|
pollGit: () => fetchGitInfo(meta.cwd, renderGitInfo),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { NodeMeta } from '../../../core/canvas/types.js';
|
|
2
2
|
import type { AttachSession } from './context.js';
|
|
3
|
-
export type NodeIdentity = Pick<NodeMeta, 'kind' | 'mode' | 'lifecycle'> | undefined;
|
|
3
|
+
export type NodeIdentity = Pick<NodeMeta, 'kind' | 'mode' | 'lifecycle' | 'icon'> | undefined;
|
|
4
4
|
export interface IdentityHooks {
|
|
5
5
|
/** Drop a late refresh that lands after the viewer is gone. */
|
|
6
6
|
isTornDown: () => boolean;
|