@north-light/crouter 0.3.162 → 0.3.164
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/builtin-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/internal/INDEX.md +1 -0
- package/dist/builtin-memory/internal/memory-loading.md +45 -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 +34 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +14 -3
- package/dist/clients/attach/__tests__/crtr-output-coverage.test.js +1 -1
- 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 +35 -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 +54 -38
- package/dist/clients/attach/render/crtr-output.d.ts +2 -1
- package/dist/clients/attach/render/crtr-output.js +33 -4
- package/dist/clients/attach/render/edit-diff.d.ts +21 -6
- package/dist/clients/attach/render/edit-diff.js +104 -81
- package/dist/clients/attach/render/tool-calls.d.ts +21 -20
- package/dist/clients/attach/render/tool-calls.js +89 -43
- 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 +628 -624
- 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__/migration.test.js +8 -3
- 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__/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/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/command-manifests/schema.js +18 -5
- package/dist/core/command.js +17 -7
- 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/runtime/bearings.d.ts +8 -8
- package/dist/core/runtime/bearings.js +20 -16
- package/dist/core/runtime/broker.js +3 -13
- 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 +1 -1
- package/dist/core/runtime/launch.js +1 -1
- package/dist/core/runtime/tmux.js +136 -100
- package/dist/core/scope.js +4 -5
- 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/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 +77 -0
- package/package.json +2 -2
- package/runtime.lock.json +6 -6
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/crouter-help.ts +0 -95
|
@@ -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 { FoldedToolCallController } 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')
|
|
@@ -661,7 +688,7 @@ export class CrtrOutputMessageComponent extends Box {
|
|
|
661
688
|
this.addChild(this.body);
|
|
662
689
|
}
|
|
663
690
|
}
|
|
664
|
-
export function createCrtrBashToolDefinition(args) {
|
|
691
|
+
export function createCrtrBashToolDefinition(args, fold = new FoldedToolCallController()) {
|
|
665
692
|
const command = commandFromArgs(args);
|
|
666
693
|
if (!isCrtrBashCommand(command))
|
|
667
694
|
return undefined;
|
|
@@ -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,8 +711,10 @@ 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
|
|
688
|
-
|
|
714
|
+
const styledPath = path
|
|
715
|
+
? ` ${fold.folded ? styleFoldedCrtrTokens(pathTokens, theme) : theme.fg('toolTitle', theme.bold(path))}`
|
|
716
|
+
: '';
|
|
717
|
+
return fold.capture(new Text(`${styledPrefix}${styledPath}`, 0, 0));
|
|
689
718
|
},
|
|
690
719
|
renderResult(result, options, theme, context) {
|
|
691
720
|
return new CrtrResultComponent(resultText(result), options, context, theme, command);
|
|
@@ -1,6 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import { FoldedToolCallController } from './tool-calls.js';
|
|
2
|
+
type ToolResultLike = {
|
|
3
|
+
content?: Array<{
|
|
4
|
+
type?: unknown;
|
|
5
|
+
text?: unknown;
|
|
6
|
+
}>;
|
|
7
|
+
details?: {
|
|
8
|
+
diff?: unknown;
|
|
9
|
+
};
|
|
10
|
+
isError?: boolean;
|
|
11
|
+
};
|
|
12
|
+
export type EditResultObserver = (result: ToolResultLike, isPartial: boolean) => void;
|
|
13
|
+
export type CrtrEditToolView = {
|
|
14
|
+
definition: unknown;
|
|
15
|
+
observeResult: EditResultObserver;
|
|
16
|
+
};
|
|
17
|
+
/** Crouter owns edit presentation while the imported tool owns execution. The
|
|
18
|
+
* card has two explicit phases: streamed argument diff, then executed result
|
|
19
|
+
* diff. No asynchronous preview can write after settlement. */
|
|
20
|
+
export declare function createCrtrEditToolDefinition(cwd: string, fold?: FoldedToolCallController): CrtrEditToolView;
|
|
21
|
+
export {};
|
|
@@ -1,21 +1,13 @@
|
|
|
1
|
-
// render/edit-diff.ts —
|
|
1
|
+
// render/edit-diff.ts — crouter-owned edit presentation for the attach viewer.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
// the async preview computation (computeEditsDiff keyed off ctx.state) and
|
|
12
|
-
// the header/status-bg lifecycle — and only swap the rendered diff body.
|
|
13
|
-
// The raw diff string lives on the call component (`preview.diff`), so no
|
|
14
|
-
// ANSI re-parsing of pi's output is ever needed.
|
|
15
|
-
import { createEditToolDefinition } from '@earendil-works/pi-coding-agent';
|
|
16
|
-
import { Container, Spacer, Text } from '@earendil-works/pi-tui';
|
|
17
|
-
import { TOOL_ICON } from '../../../core/preview-registry.js';
|
|
18
|
-
import { foldedFileCallLine, isFoldedContext, prefixToolText } from './tool-calls.js';
|
|
3
|
+
// The card renders replacement arguments synchronously while they stream, then
|
|
4
|
+
// switches to the executed tool result's full-file diff when the tool settles.
|
|
5
|
+
// Added and removed rows receive full-width tinted backgrounds, with the changed
|
|
6
|
+
// span inside a 1↔1 line modification emphasized in a brighter tint. The imported
|
|
7
|
+
// pi package remains responsible for edit execution, not viewer lifecycle.
|
|
8
|
+
import { generateDiffString } from '@earendil-works/pi-coding-agent';
|
|
9
|
+
import { Box, Container, Spacer, Text } from '@earendil-works/pi-tui';
|
|
10
|
+
import { FoldedToolCallController, foldedFileCallLine } from './tool-calls.js';
|
|
19
11
|
const FALLBACK_ADDED = { r: 181, g: 189, b: 104 }; // pi dark green
|
|
20
12
|
const FALLBACK_REMOVED = { r: 204, g: 102, b: 102 }; // pi dark red
|
|
21
13
|
const FALLBACK_CARD_BG = { r: 40, g: 50, b: 40 }; // pi dark toolSuccessBg
|
|
@@ -102,6 +94,44 @@ function parseDiffLine(line) {
|
|
|
102
94
|
return null;
|
|
103
95
|
return { prefix: match[1], lineNum: match[2], content: match[3] };
|
|
104
96
|
}
|
|
97
|
+
/** Count only changed rows in pi's display diff; context and ellipsis rows do
|
|
98
|
+
* not contribute to the folded file summary. */
|
|
99
|
+
function countDiffLines(diff) {
|
|
100
|
+
let added = 0;
|
|
101
|
+
let removed = 0;
|
|
102
|
+
for (const line of diff.split('\n')) {
|
|
103
|
+
const parsed = parseDiffLine(line);
|
|
104
|
+
if (parsed?.prefix === '+')
|
|
105
|
+
added++;
|
|
106
|
+
if (parsed?.prefix === '-')
|
|
107
|
+
removed++;
|
|
108
|
+
}
|
|
109
|
+
return { added, removed };
|
|
110
|
+
}
|
|
111
|
+
/** Extract every complete replacement pair currently present in the streamed
|
|
112
|
+
* arguments. A pair becomes previewable as soon as both strings exist. */
|
|
113
|
+
function editPairs(args) {
|
|
114
|
+
const value = args;
|
|
115
|
+
if (Array.isArray(value?.edits)) {
|
|
116
|
+
return value.edits.flatMap((edit) => {
|
|
117
|
+
const pair = edit;
|
|
118
|
+
return typeof pair?.oldText === 'string' && typeof pair?.newText === 'string'
|
|
119
|
+
? [{ oldText: pair.oldText, newText: pair.newText }]
|
|
120
|
+
: [];
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return typeof value?.oldText === 'string' && typeof value?.newText === 'string'
|
|
124
|
+
? [{ oldText: value.oldText, newText: value.newText }]
|
|
125
|
+
: [];
|
|
126
|
+
}
|
|
127
|
+
/** The pre-execution preview is deliberately replacement-local: it can update
|
|
128
|
+
* synchronously as arguments stream and never races filesystem mutation. */
|
|
129
|
+
function streamedArgumentDiff(args) {
|
|
130
|
+
const diffs = editPairs(args)
|
|
131
|
+
.map(({ oldText, newText }) => generateDiffString(oldText, newText).diff)
|
|
132
|
+
.filter((diff) => diff.trim().length > 0);
|
|
133
|
+
return diffs.length > 0 ? diffs.join('\n') : undefined;
|
|
134
|
+
}
|
|
105
135
|
function replaceTabs(text) {
|
|
106
136
|
return text.replace(/\t/g, ' ');
|
|
107
137
|
}
|
|
@@ -202,81 +232,74 @@ function buildDiffBody(diffText, theme, restoreBg) {
|
|
|
202
232
|
}
|
|
203
233
|
return container;
|
|
204
234
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
// (mirrors pi's getEditHeaderBg), so tinted lines restore to that color.
|
|
222
|
-
box.addChild(buildDiffBody(preview.diff, theme, theme.getBgAnsi('toolSuccessBg')));
|
|
235
|
+
class EditCallComponent extends Box {
|
|
236
|
+
constructor() {
|
|
237
|
+
super(1, 1, (text) => text);
|
|
238
|
+
}
|
|
239
|
+
update(header, diff, state, theme) {
|
|
240
|
+
const background = state.settled
|
|
241
|
+
? state.isError ? 'toolErrorBg' : 'toolSuccessBg'
|
|
242
|
+
: 'toolPendingBg';
|
|
243
|
+
this.setBgFn((text) => theme.bg(background, text));
|
|
244
|
+
this.clear();
|
|
245
|
+
this.addChild(new Text(header, 0, 0));
|
|
246
|
+
if (!diff)
|
|
247
|
+
return;
|
|
248
|
+
this.addChild(new Spacer(1));
|
|
249
|
+
this.addChild(buildDiffBody(diff, theme, theme.getBgAnsi(background)));
|
|
250
|
+
}
|
|
223
251
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
function prefixEditHeaderIcon(box, theme) {
|
|
228
|
-
prefixToolText(box.children[0], `${theme.fg('accent', TOOL_ICON.edit)} `);
|
|
252
|
+
function editPath(args) {
|
|
253
|
+
const value = args;
|
|
254
|
+
return typeof value?.file_path === 'string' ? value.file_path : typeof value?.path === 'string' ? value.path : '';
|
|
229
255
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
256
|
+
function errorResultText(result) {
|
|
257
|
+
return (result.content ?? [])
|
|
258
|
+
.filter((part) => part.type === 'text' && typeof part.text === 'string')
|
|
259
|
+
.map((part) => part.text)
|
|
260
|
+
.join('\n');
|
|
261
|
+
}
|
|
262
|
+
/** Crouter owns edit presentation while the imported tool owns execution. The
|
|
263
|
+
* card has two explicit phases: streamed argument diff, then executed result
|
|
264
|
+
* diff. No asynchronous preview can write after settlement. */
|
|
265
|
+
export function createCrtrEditToolDefinition(cwd, fold = new FoldedToolCallController()) {
|
|
266
|
+
const state = { settled: false, isError: false };
|
|
267
|
+
const call = new EditCallComponent();
|
|
268
|
+
const observeResult = (result, isPartial) => {
|
|
269
|
+
if (isPartial)
|
|
270
|
+
return;
|
|
271
|
+
state.settled = true;
|
|
272
|
+
state.isError = result.isError === true;
|
|
273
|
+
const resultDiff = result.details?.diff;
|
|
274
|
+
state.diff = !state.isError && typeof resultDiff === 'string' ? resultDiff : undefined;
|
|
275
|
+
};
|
|
276
|
+
const definition = {
|
|
238
277
|
name: 'edit',
|
|
239
278
|
label: 'edit',
|
|
240
|
-
description: 'Render edit
|
|
279
|
+
description: 'Render streamed edit arguments and the executed full-file diff in the attach viewer.',
|
|
241
280
|
parameters: {},
|
|
242
281
|
renderShell: 'self',
|
|
243
282
|
renderCall(args, theme, context) {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
if (
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
}
|
|
252
|
-
const component = builtin.renderCall(args, theme, context);
|
|
253
|
-
const callBox = context.state.callComponent;
|
|
254
|
-
if (callBox) {
|
|
255
|
-
restyleEditCallBox(callBox, theme);
|
|
256
|
-
prefixEditHeaderIcon(callBox, theme);
|
|
257
|
-
}
|
|
258
|
-
return component;
|
|
283
|
+
const diff = state.settled ? state.diff : streamedArgumentDiff(args);
|
|
284
|
+
const changes = state.settled && !state.isError && state.diff ? countDiffLines(state.diff) : undefined;
|
|
285
|
+
const header = foldedFileCallLine('edit', editPath(args), theme, context.cwd ?? cwd, changes);
|
|
286
|
+
if (fold.folded)
|
|
287
|
+
return fold.capture(new Text(header, 0, 0));
|
|
288
|
+
call.update(header, diff, state, theme);
|
|
289
|
+
return fold.capture(call);
|
|
259
290
|
},
|
|
260
|
-
renderResult(result,
|
|
261
|
-
const component =
|
|
262
|
-
|
|
263
|
-
if (
|
|
264
|
-
// builtin renderResult re-ran buildEditCallComponent (pi styling) —
|
|
265
|
-
// restyle the refreshed body.
|
|
266
|
-
restyleEditCallBox(callBox, theme);
|
|
267
|
-
prefixEditHeaderIcon(callBox, theme);
|
|
291
|
+
renderResult(result, _options, theme, context) {
|
|
292
|
+
const component = context.lastComponent instanceof Container ? context.lastComponent : new Container();
|
|
293
|
+
component.clear();
|
|
294
|
+
if (!context.isError)
|
|
268
295
|
return component;
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
if (!context.isError && typeof resultDiff === 'string') {
|
|
274
|
-
const container = new Container();
|
|
275
|
-
container.addChild(new Spacer(1));
|
|
276
|
-
container.addChild(buildDiffBody(resultDiff, theme, '\x1b[49m'));
|
|
277
|
-
return container;
|
|
296
|
+
const output = errorResultText(result);
|
|
297
|
+
if (output) {
|
|
298
|
+
component.addChild(new Spacer(1));
|
|
299
|
+
component.addChild(new Text(theme.fg('error', output), 1, 0));
|
|
278
300
|
}
|
|
279
301
|
return component;
|
|
280
302
|
},
|
|
281
303
|
};
|
|
304
|
+
return { definition, observeResult };
|
|
282
305
|
}
|
|
@@ -1,32 +1,33 @@
|
|
|
1
|
+
import { type Component } from '@earendil-works/pi-tui';
|
|
1
2
|
type Theme = {
|
|
2
3
|
fg: (name: string, text: string) => string;
|
|
3
4
|
bold: (text: string) => string;
|
|
4
5
|
};
|
|
5
|
-
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
export
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
*
|
|
21
|
-
export declare function
|
|
6
|
+
/** Per-tool fold state owned by the attach viewer. Tool definitions capture the
|
|
7
|
+
* call component they render while folded, so the viewer never reaches into
|
|
8
|
+
* `ToolExecutionComponent`'s private renderer fields to recover it. */
|
|
9
|
+
export declare class FoldedToolCallController {
|
|
10
|
+
folded: boolean;
|
|
11
|
+
private component;
|
|
12
|
+
setFolded(folded: boolean): void;
|
|
13
|
+
capture<T extends Component>(component: T): T;
|
|
14
|
+
callComponent(): Component | undefined;
|
|
15
|
+
}
|
|
16
|
+
export type FileLineChanges = {
|
|
17
|
+
added: number;
|
|
18
|
+
removed: number;
|
|
19
|
+
};
|
|
20
|
+
/** The one-line folded form of a file tool's call: `<icon> <label> <path>`,
|
|
21
|
+
* optionally followed by its green/red changed-line counts. */
|
|
22
|
+
export declare function foldedFileCallLine(tool: 'read' | 'write' | 'edit', rawPath: string, theme: Theme, cwd: string, changes?: FileLineChanges): string;
|
|
22
23
|
/** Attach-viewer `read`: an icon + the path (plus any `:offset-limit` range).
|
|
23
24
|
* The call is a single line by construction, so a folded read shows exactly
|
|
24
25
|
* what it read and nothing else. */
|
|
25
|
-
export declare function createCrtrReadToolDefinition(cwd: string): unknown;
|
|
26
|
+
export declare function createCrtrReadToolDefinition(cwd: string, fold?: FoldedToolCallController): unknown;
|
|
26
27
|
/** Attach-viewer `write`: pi's builtin call renderer (it owns the
|
|
27
28
|
* syntax-highlighted content preview and its expand hint) with the icon
|
|
28
29
|
* prefixed onto its header line. Folded, only that header survives. */
|
|
29
|
-
export declare function createCrtrWriteToolDefinition(cwd: string): unknown;
|
|
30
|
+
export declare function createCrtrWriteToolDefinition(cwd: string, fold?: FoldedToolCallController): unknown;
|
|
30
31
|
/** Attach-viewer `bash` for anything that is not a `crtr` invocation. */
|
|
31
|
-
export declare function createCrtrPlainBashToolDefinition(cwd: string): unknown;
|
|
32
|
+
export declare function createCrtrPlainBashToolDefinition(cwd: string, fold?: FoldedToolCallController): unknown;
|
|
32
33
|
export {};
|
|
@@ -19,18 +19,52 @@ import { TOOL_ICON } from '../../../core/preview-registry.js';
|
|
|
19
19
|
function str(value) {
|
|
20
20
|
return typeof value === 'string' ? value : '';
|
|
21
21
|
}
|
|
22
|
-
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
22
|
+
/** Per-tool fold state owned by the attach viewer. Tool definitions capture the
|
|
23
|
+
* call component they render while folded, so the viewer never reaches into
|
|
24
|
+
* `ToolExecutionComponent`'s private renderer fields to recover it. */
|
|
25
|
+
export class FoldedToolCallController {
|
|
26
|
+
folded = false;
|
|
27
|
+
component;
|
|
28
|
+
setFolded(folded) {
|
|
29
|
+
this.folded = folded;
|
|
30
|
+
}
|
|
31
|
+
capture(component) {
|
|
32
|
+
this.component = component;
|
|
33
|
+
return component;
|
|
34
|
+
}
|
|
35
|
+
callComponent() {
|
|
36
|
+
return this.component;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Count file lines the way a diff does: a trailing newline terminates the last
|
|
40
|
+
* line instead of creating an extra empty one. */
|
|
41
|
+
function countFileLines(content) {
|
|
42
|
+
if (content.length === 0)
|
|
43
|
+
return 0;
|
|
44
|
+
const newlines = content.match(/\n/g)?.length ?? 0;
|
|
45
|
+
return newlines + (content.endsWith('\n') ? 0 : 1);
|
|
46
|
+
}
|
|
47
|
+
/** The green/red changed-line suffix shown after a folded edit path. */
|
|
48
|
+
function lineChangesDisplay(changes, theme) {
|
|
49
|
+
if (!changes)
|
|
50
|
+
return '';
|
|
51
|
+
const counts = [];
|
|
52
|
+
if (changes.added > 0)
|
|
53
|
+
counts.push(theme.fg('toolDiffAdded', `+${changes.added}`));
|
|
54
|
+
if (changes.removed > 0)
|
|
55
|
+
counts.push(theme.fg('toolDiffRemoved', `-${changes.removed}`));
|
|
56
|
+
return counts.length > 0 ? ` ${counts.join(' ')}` : '';
|
|
57
|
+
}
|
|
58
|
+
/** A write has no before/after diff, so report its output size neutrally. */
|
|
59
|
+
function lineCountDisplay(lines, theme) {
|
|
60
|
+
if (lines === undefined)
|
|
61
|
+
return '';
|
|
62
|
+
return ` ${theme.fg('muted', `${lines} ${lines === 1 ? 'line' : 'lines'}`)}`;
|
|
63
|
+
}
|
|
64
|
+
/** The one-line folded form of a file tool's call: `<icon> <label> <path>`,
|
|
65
|
+
* optionally followed by its green/red changed-line counts. */
|
|
66
|
+
export function foldedFileCallLine(tool, rawPath, theme, cwd, changes) {
|
|
67
|
+
return `${header(TOOL_ICON[tool], tool, theme)} ${pathDisplay(rawPath, theme, cwd)}${lineChangesDisplay(changes, theme)}`;
|
|
34
68
|
}
|
|
35
69
|
/** `~`-shorten an absolute path the way pi's renderToolPath does (its
|
|
36
70
|
* render-utils module is not exported from the package root). */
|
|
@@ -52,18 +86,29 @@ function pathDisplay(rawPath, theme, cwd) {
|
|
|
52
86
|
function header(icon, label, theme) {
|
|
53
87
|
return `${theme.fg('accent', icon)} ${theme.fg('toolTitle', theme.bold(label))}`;
|
|
54
88
|
}
|
|
55
|
-
/**
|
|
56
|
-
*
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
89
|
+
/** Add an icon to a tool call without mutating the imported renderer's private
|
|
90
|
+
* text storage. The imported component remains responsible for its own body. */
|
|
91
|
+
class IconPrefixedToolCall {
|
|
92
|
+
child;
|
|
93
|
+
prefix;
|
|
94
|
+
constructor(child, prefix) {
|
|
95
|
+
this.child = child;
|
|
96
|
+
this.prefix = prefix;
|
|
97
|
+
}
|
|
98
|
+
update(child, prefix) {
|
|
99
|
+
this.child = child;
|
|
100
|
+
this.prefix = prefix;
|
|
101
|
+
}
|
|
102
|
+
invalidate() {
|
|
103
|
+
this.child.invalidate();
|
|
104
|
+
}
|
|
105
|
+
render(width) {
|
|
106
|
+
const prefixWidth = visibleWidth(this.prefix);
|
|
107
|
+
const lines = this.child.render(Math.max(1, width - prefixWidth));
|
|
108
|
+
if (lines.length === 0)
|
|
109
|
+
return [truncateToWidth(this.prefix.trimEnd(), width, '')];
|
|
110
|
+
return lines.map((line, index) => index === 0 ? truncateToWidth(`${this.prefix}${line}`, width, '') : line);
|
|
111
|
+
}
|
|
67
112
|
}
|
|
68
113
|
// ---------------------------------------------------------------------------
|
|
69
114
|
// read
|
|
@@ -71,7 +116,7 @@ export function prefixToolText(component, prefix) {
|
|
|
71
116
|
/** Attach-viewer `read`: an icon + the path (plus any `:offset-limit` range).
|
|
72
117
|
* The call is a single line by construction, so a folded read shows exactly
|
|
73
118
|
* what it read and nothing else. */
|
|
74
|
-
export function createCrtrReadToolDefinition(cwd) {
|
|
119
|
+
export function createCrtrReadToolDefinition(cwd, fold = new FoldedToolCallController()) {
|
|
75
120
|
const builtin = createReadToolDefinition(cwd);
|
|
76
121
|
return {
|
|
77
122
|
name: 'read',
|
|
@@ -90,7 +135,7 @@ export function createCrtrReadToolDefinition(cwd) {
|
|
|
90
135
|
const line = `${header(TOOL_ICON.read, 'read', theme)} ${pathDisplay(raw, theme, context.cwd ?? cwd)}${range}`;
|
|
91
136
|
const component = context.lastComponent ?? new Text('', 0, 0);
|
|
92
137
|
component.setText(line);
|
|
93
|
-
return component;
|
|
138
|
+
return fold.capture(component);
|
|
94
139
|
},
|
|
95
140
|
renderResult(result, options, theme, context) {
|
|
96
141
|
return builtin.renderResult(result, options, theme, context);
|
|
@@ -103,29 +148,30 @@ export function createCrtrReadToolDefinition(cwd) {
|
|
|
103
148
|
/** Attach-viewer `write`: pi's builtin call renderer (it owns the
|
|
104
149
|
* syntax-highlighted content preview and its expand hint) with the icon
|
|
105
150
|
* prefixed onto its header line. Folded, only that header survives. */
|
|
106
|
-
export function createCrtrWriteToolDefinition(cwd) {
|
|
151
|
+
export function createCrtrWriteToolDefinition(cwd, fold = new FoldedToolCallController()) {
|
|
107
152
|
const builtin = createWriteToolDefinition(cwd);
|
|
153
|
+
let builtinCall;
|
|
154
|
+
let decoratedCall;
|
|
108
155
|
return {
|
|
109
156
|
name: 'write',
|
|
110
157
|
label: 'write',
|
|
111
158
|
description: 'Render write calls with a crtr icon header in the attach viewer.',
|
|
112
159
|
parameters: {},
|
|
113
160
|
renderCall(args, theme, context) {
|
|
114
|
-
if (
|
|
161
|
+
if (fold.folded) {
|
|
115
162
|
const a = args;
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (last && typeof last.setText === 'function') {
|
|
121
|
-
last.setText(line);
|
|
122
|
-
return last;
|
|
123
|
-
}
|
|
124
|
-
return new Text(line, 0, 0);
|
|
163
|
+
const lines = context.isError ? undefined : countFileLines(str(a?.content));
|
|
164
|
+
const line = foldedFileCallLine('write', str(a?.file_path ?? a?.path), theme, context.cwd ?? cwd) +
|
|
165
|
+
lineCountDisplay(lines, theme);
|
|
166
|
+
return fold.capture(new Text(line, 0, 0));
|
|
125
167
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
168
|
+
builtinCall = builtin.renderCall(args, theme, { ...context, lastComponent: builtinCall });
|
|
169
|
+
const prefix = `${theme.fg('accent', TOOL_ICON.write)} `;
|
|
170
|
+
if (decoratedCall)
|
|
171
|
+
decoratedCall.update(builtinCall, prefix);
|
|
172
|
+
else
|
|
173
|
+
decoratedCall = new IconPrefixedToolCall(builtinCall, prefix);
|
|
174
|
+
return fold.capture(decoratedCall);
|
|
129
175
|
},
|
|
130
176
|
renderResult(result, options, theme, context) {
|
|
131
177
|
return builtin.renderResult(result, options, theme, context);
|
|
@@ -221,7 +267,7 @@ class BashCallComponent {
|
|
|
221
267
|
}
|
|
222
268
|
}
|
|
223
269
|
/** Attach-viewer `bash` for anything that is not a `crtr` invocation. */
|
|
224
|
-
export function createCrtrPlainBashToolDefinition(cwd) {
|
|
270
|
+
export function createCrtrPlainBashToolDefinition(cwd, fold = new FoldedToolCallController()) {
|
|
225
271
|
const builtin = createBashToolDefinition(cwd);
|
|
226
272
|
return {
|
|
227
273
|
name: 'bash',
|
|
@@ -241,8 +287,8 @@ export function createCrtrPlainBashToolDefinition(cwd) {
|
|
|
241
287
|
const component = context.lastComponent instanceof BashCallComponent ? context.lastComponent : new BashCallComponent();
|
|
242
288
|
component.update(theme, str(a?.command),
|
|
243
289
|
// Folded wins over ctrl+o expansion: a folded bash is always its one-line form.
|
|
244
|
-
Boolean(context.expanded) && !
|
|
245
|
-
return component;
|
|
290
|
+
Boolean(context.expanded) && !fold.folded, typeof a?.timeout === 'number' ? a.timeout : undefined);
|
|
291
|
+
return fold.capture(component);
|
|
246
292
|
},
|
|
247
293
|
renderResult(result, options, theme, context) {
|
|
248
294
|
return builtin.renderResult(result, options, theme, context);
|
|
@@ -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
|