@north-light/crouter 0.3.159 → 0.3.160
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/dto/nodes.d.ts +3 -0
- package/dist/builtin-memory/internal/INDEX.md +1 -0
- package/dist/builtin-memory/internal/workflow-codification.md +82 -0
- package/dist/clients/attach/__tests__/bash-call-width.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/bash-call-width.test.js +38 -0
- package/dist/clients/attach/__tests__/chat-view-snapshot-ordering.test.js +20 -0
- package/dist/clients/attach/__tests__/crtr-output-coverage.test.js +3 -1
- package/dist/clients/attach/chrome/bash-jobs.d.ts +5 -3
- package/dist/clients/attach/chrome/bash-jobs.js +15 -8
- package/dist/clients/attach/chrome/canvas-panels.js +10 -3
- package/dist/clients/attach/chrome/roster.d.ts +7 -2
- package/dist/clients/attach/chrome/roster.js +87 -3
- package/dist/clients/attach/input/controller.d.ts +3 -0
- package/dist/clients/attach/input/controller.js +1 -0
- package/dist/clients/attach/render/assistant-message.d.ts +13 -0
- package/dist/clients/attach/render/assistant-message.js +55 -0
- package/dist/clients/attach/render/chat-view.d.ts +29 -0
- package/dist/clients/attach/render/chat-view.js +233 -12
- package/dist/clients/attach/render/context-message.js +1 -1
- package/dist/clients/attach/render/edit-diff.js +20 -1
- package/dist/clients/attach/render/tool-calls.d.ts +32 -0
- package/dist/clients/attach/render/tool-calls.js +251 -0
- package/dist/clients/attach/session/input-wiring.d.ts +3 -0
- package/dist/clients/attach/session/input-wiring.js +3 -0
- package/dist/clients/attach/slash/dispatch.d.ts +4 -0
- package/dist/clients/attach/slash/dispatch.js +8 -0
- package/dist/clients/attach/viewer.js +512 -508
- package/dist/commands/cron.js +2 -2
- package/dist/commands/node.js +3 -3
- package/dist/commands/search.js +1 -1
- package/dist/commands/sys/config.js +10 -2
- package/dist/commands/sys/doctor.js +22 -1
- package/dist/commands/sys/setup-core.d.ts +5 -1
- package/dist/commands/sys/setup-core.js +16 -2
- package/dist/commands/sys/setup-wizard.d.ts +11 -0
- package/dist/commands/sys/setup-wizard.js +111 -6
- package/dist/core/__tests__/tmux-surface.test.js +5 -2
- package/dist/core/canvas/canvas.js +1 -1
- package/dist/core/canvas/labels.d.ts +1 -0
- package/dist/core/canvas/labels.js +3 -1
- package/dist/core/canvas/nav-render.d.ts +6 -0
- package/dist/core/canvas/nav-render.js +8 -0
- package/dist/core/canvas/types.d.ts +6 -0
- package/dist/core/config.js +3 -2
- package/dist/core/keybindings/__tests__/resolve.test.js +1 -1
- package/dist/core/keybindings/catalog.d.ts +2 -2
- package/dist/core/keybindings/catalog.js +2 -0
- package/dist/core/preview-registry.d.ts +5 -0
- package/dist/core/preview-registry.js +10 -0
- package/dist/core/runtime/canvas-extensions.d.ts +4 -0
- package/dist/core/runtime/canvas-extensions.js +4 -0
- package/dist/core/runtime/naming-persist.d.ts +4 -3
- package/dist/core/runtime/naming-persist.js +10 -9
- package/dist/core/runtime/naming.d.ts +28 -8
- package/dist/core/runtime/naming.js +168 -32
- package/dist/core/runtime/nerd-font.d.ts +28 -0
- package/dist/core/runtime/nerd-font.js +127 -0
- package/dist/core/runtime/tmux.js +16 -0
- package/dist/daemon/api/map.js +2 -0
- package/dist/pi-extensions/naming-tool.d.ts +24 -0
- package/dist/pi-extensions/naming-tool.js +67 -0
- package/dist/types.d.ts +5 -0
- package/dist/types.js +1 -0
- package/dist/web-client/assets/index-B76ZKfT_.js +79 -0
- package/dist/web-client/assets/{index-CpEl9LTS.css → index-CqLKj8Xu.css} +1 -1
- package/dist/web-client/index.html +2 -2
- package/dist/web-client/sw.js +1 -1
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- package/dist/web-client/assets/index-CsuwzlcQ.js +0 -79
|
@@ -30,10 +30,12 @@ import { SITUATIONAL_CONTEXT_CUSTOM_TYPE } from '../../../core/runtime/situation
|
|
|
30
30
|
import { transformMessageDiagrams } from './diagram.js';
|
|
31
31
|
import { styleAttachMarkdownHeadings, styleAttachMessageMarkdown, styleAttachSummaryMarkdown, } from './markdown-headings.js';
|
|
32
32
|
import { createCrtrEditToolDefinition } from './edit-diff.js';
|
|
33
|
+
import { FOLDED_STATE_KEY, createCrtrPlainBashToolDefinition, createCrtrReadToolDefinition, createCrtrWriteToolDefinition, } from './tool-calls.js';
|
|
33
34
|
import { ringCompletionBell } from '../chrome/completion-bell.js';
|
|
34
35
|
import { extractFilePaths } from '../visible-paths.js';
|
|
35
36
|
import { FrozenHistoryComponent, keepWhenCondensed, liveRegionStart } from './frozen-history.js';
|
|
36
|
-
import {
|
|
37
|
+
import { ViewerAssistantMessageComponent } from './assistant-message.js';
|
|
38
|
+
import { readConfig, updateRawConfigAtomically } from '../../../core/config.js';
|
|
37
39
|
import { DEFAULT_CONDENSED_HISTORY, DEFAULT_LIVE_CYCLES } from '../../../types.js';
|
|
38
40
|
/** customType stamped on the injected `<crtr-context>` bearings block (mirrors
|
|
39
41
|
* CONTEXT_INTRO_CUSTOM_TYPE in canvas-context-intro.ts — kept as a local literal
|
|
@@ -76,6 +78,27 @@ function readCondensedHistory() {
|
|
|
76
78
|
return DEFAULT_CONDENSED_HISTORY;
|
|
77
79
|
}
|
|
78
80
|
}
|
|
81
|
+
/** The user's `fold_finished_tools` setting — the persisted starting state of the
|
|
82
|
+
* fold view setting. Toggling it in a viewer writes it back (`persistFoldFinishedTools`),
|
|
83
|
+
* so the choice carries into later chats and later viewers. */
|
|
84
|
+
function readFoldFinishedTools() {
|
|
85
|
+
try {
|
|
86
|
+
return readConfig('user').fold_finished_tools;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** Persist the fold view setting to user config. Best-effort: a viewer must never
|
|
93
|
+
* die because the config file is unwritable — the in-viewer toggle still applies. */
|
|
94
|
+
function persistFoldFinishedTools(value) {
|
|
95
|
+
try {
|
|
96
|
+
updateRawConfigAtomically('user', (latest) => ({ ...latest, fold_finished_tools: value }));
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// ignored — the fold still applies to this viewer.
|
|
100
|
+
}
|
|
101
|
+
}
|
|
79
102
|
/** A component supports the Ctrl+O global tool-output toggle. Mirrors pi's
|
|
80
103
|
* `isExpandable` duck-type (interactive-mode.js): any rendered child carrying a
|
|
81
104
|
* `setExpanded(boolean)` method — tool executions, custom/compaction/branch/skill
|
|
@@ -87,6 +110,98 @@ function isExpandable(obj) {
|
|
|
87
110
|
'setExpanded' in obj &&
|
|
88
111
|
typeof obj.setExpanded === 'function');
|
|
89
112
|
}
|
|
113
|
+
/** Every SGR escape, for measuring a line's visible content. */
|
|
114
|
+
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
115
|
+
/** Same match, with the SGR parameter list captured for rewriting. */
|
|
116
|
+
const SGR_PARAMS_RE = /\x1b\[([0-9;]*)m/g;
|
|
117
|
+
/** Drop every background-color SGR parameter from a line, keeping foreground and
|
|
118
|
+
* text styles. The minimized fold is a dense index of what ran, so the tool's
|
|
119
|
+
* pending/success/error background band (pi paints it across the full pane width)
|
|
120
|
+
* reads as noise there — one call per line, unhighlighted. `0` (full reset) is
|
|
121
|
+
* kept: it clears the background too, and nothing follows it to re-set one. */
|
|
122
|
+
function stripBackground(line) {
|
|
123
|
+
return line.replace(SGR_PARAMS_RE, (_whole, params) => {
|
|
124
|
+
const parts = params.split(';');
|
|
125
|
+
const kept = [];
|
|
126
|
+
for (let i = 0; i < parts.length; i++) {
|
|
127
|
+
const code = Number(parts[i] === '' ? 0 : parts[i]);
|
|
128
|
+
// Extended background (`48;5;n` / `48;2;r;g;b`) — skip the code AND its args.
|
|
129
|
+
if (code === 48) {
|
|
130
|
+
const mode = Number(parts[i + 1]);
|
|
131
|
+
i += mode === 5 ? 2 : mode === 2 ? 4 : 1;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
// Standard (40-47) and bright (100-107) backgrounds. `49` (reset to the
|
|
135
|
+
// default background) is KEPT — it is the thing we want, and dropping it
|
|
136
|
+
// would let a neighbouring row's background bleed into this line.
|
|
137
|
+
if ((code >= 40 && code <= 47) || (code >= 100 && code <= 107))
|
|
138
|
+
continue;
|
|
139
|
+
kept.push(String(code));
|
|
140
|
+
}
|
|
141
|
+
return kept.length > 0 ? `\x1b[${kept.join(';')}m` : '';
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
/** A tool component that can fold itself down to its CALL. pi's `ToolExecutionComponent`
|
|
145
|
+
* only knows expanded/collapsed, so the minimized state is crtr's — and the tool's own
|
|
146
|
+
* renderCall owns its folded form: every crtr-owned definition (read/write/edit/bash)
|
|
147
|
+
* reads the fold flag off its shared render-state bag and emits ONLY its one-line
|
|
148
|
+
* `<icon> <tool> <path>` header while folded. Folded render then paints just the call
|
|
149
|
+
* component (result and Box chrome dropped), so a wrapped path keeps its wrap rows
|
|
150
|
+
* naturally — no measuring or slicing of rendered ANSI rows. Tools without a crtr
|
|
151
|
+
* definition are not foldable and always render in full. */
|
|
152
|
+
/** A one-line separator that appears only while its adjacent tool group is
|
|
153
|
+
* folded. It stays in the transcript so toggling folded tools reflows the same
|
|
154
|
+
* message/tool boundary without rebuilding the chat. */
|
|
155
|
+
class FoldedToolSeparator extends Spacer {
|
|
156
|
+
folded = false;
|
|
157
|
+
separatesToolGroup = false;
|
|
158
|
+
setFolded(folded) {
|
|
159
|
+
this.folded = folded;
|
|
160
|
+
this.refresh();
|
|
161
|
+
}
|
|
162
|
+
setSeparatesToolGroup(separatesToolGroup) {
|
|
163
|
+
this.separatesToolGroup = separatesToolGroup;
|
|
164
|
+
this.refresh();
|
|
165
|
+
}
|
|
166
|
+
refresh() {
|
|
167
|
+
this.setLines(this.folded && this.separatesToolGroup ? 1 : 0);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
class MinimizableToolComponent extends ToolExecutionComponent {
|
|
171
|
+
minimized = false;
|
|
172
|
+
foldable = false;
|
|
173
|
+
/** Only tools whose renderCall is fold-aware (the crtr-owned definitions) may
|
|
174
|
+
* fold; chat-view marks them at construction. */
|
|
175
|
+
setFoldable(foldable) {
|
|
176
|
+
this.foldable = foldable;
|
|
177
|
+
}
|
|
178
|
+
/** Flip the fold: expose it on the definitions' shared render-state bag, then
|
|
179
|
+
* re-run `updateDisplay()` (the same path pi's own `setExpanded` uses) so the
|
|
180
|
+
* fold-aware renderCall rebuilds the call component in its new form. NOT
|
|
181
|
+
* `invalidate()` — pi's `ToolExecutionComponent.invalidate` recurses through
|
|
182
|
+
* its renderer children and killed the viewer process. */
|
|
183
|
+
setMinimized(minimized) {
|
|
184
|
+
if (!this.foldable || this.minimized === minimized)
|
|
185
|
+
return;
|
|
186
|
+
this.minimized = minimized;
|
|
187
|
+
const internals = this;
|
|
188
|
+
internals.rendererState[FOLDED_STATE_KEY] = minimized;
|
|
189
|
+
internals.updateDisplay();
|
|
190
|
+
}
|
|
191
|
+
render(width) {
|
|
192
|
+
if (!this.minimized)
|
|
193
|
+
return super.render(width);
|
|
194
|
+
const call = this
|
|
195
|
+
.callRendererComponent;
|
|
196
|
+
if (!call)
|
|
197
|
+
return super.render(width);
|
|
198
|
+
// trimEnd drops right-hand padding, which only existed to carry the
|
|
199
|
+
// background band these lines no longer paint.
|
|
200
|
+
return call.render(width)
|
|
201
|
+
.filter((line) => line.replace(ANSI_RE, '').trim().length > 0)
|
|
202
|
+
.map((line) => stripBackground(line).trimEnd());
|
|
203
|
+
}
|
|
204
|
+
}
|
|
90
205
|
const defaultErrorStyle = (s) => `\x1b[31m${s}\x1b[39m`;
|
|
91
206
|
/** Keep settled conversation history out of picker/editor render hot paths. The
|
|
92
207
|
* root TUI still compares the cached lines, but it no longer re-renders every
|
|
@@ -146,7 +261,12 @@ export class ChatView {
|
|
|
146
261
|
imageWidthCells;
|
|
147
262
|
hideThinking;
|
|
148
263
|
hiddenThinkingLabel;
|
|
264
|
+
/** Ctrl+O: pi's two-state tool-output expansion. */
|
|
149
265
|
toolOutputExpanded;
|
|
266
|
+
/** The view setting (Alt+C → w → f): fold every SETTLED tool call to its single
|
|
267
|
+
* call line. Independent of `toolOutputExpanded` — it survives an expand/collapse
|
|
268
|
+
* and only ever applies to tool calls that are done. */
|
|
269
|
+
foldSettledTools = readFoldFinishedTools();
|
|
150
270
|
onFooterEvent;
|
|
151
271
|
/** Theme-derived colorizers for the activity/status area. */
|
|
152
272
|
spinnerStyle;
|
|
@@ -157,6 +277,12 @@ export class ChatView {
|
|
|
157
277
|
/** The assistant message component currently being streamed (between
|
|
158
278
|
* message_start and message_end for an assistant turn). */
|
|
159
279
|
streamingComponent;
|
|
280
|
+
/** The separator immediately after the live assistant component. It becomes
|
|
281
|
+
* visible when that response has visible prose or thinking and folded tools. */
|
|
282
|
+
streamingToolSeparator;
|
|
283
|
+
/** Content classes for each separator, so Ctrl+T can add/remove thinking gaps
|
|
284
|
+
* without rebuilding the transcript. */
|
|
285
|
+
foldedToolSeparatorContents = new Map();
|
|
160
286
|
/** Tool-call id → its execution component, for the duration of the call. */
|
|
161
287
|
pendingTools = new Map();
|
|
162
288
|
/** The `!` bash component currently streaming (between bash_start and bash_end). */
|
|
@@ -286,15 +412,20 @@ export class ChatView {
|
|
|
286
412
|
snapshot.state.isStreaming &&
|
|
287
413
|
message.stopReason !== 'aborted' &&
|
|
288
414
|
message.stopReason !== 'error';
|
|
415
|
+
let separator;
|
|
289
416
|
if (isStreamingTail) {
|
|
290
|
-
this.streamingComponent = new
|
|
417
|
+
this.streamingComponent = new ViewerAssistantMessageComponent(undefined, this.hideThinking, attachMarkdownTheme(), this.hiddenThinkingLabel);
|
|
291
418
|
this.append(this.streamingComponent);
|
|
292
419
|
this.streamingComponent.updateContent(styleAttachMessageMarkdown(message));
|
|
420
|
+
separator = this.streamingToolSeparator = this.appendFoldedToolSeparator();
|
|
293
421
|
}
|
|
294
422
|
else {
|
|
295
|
-
this.
|
|
423
|
+
separator = this.appendAssistantMessage(message);
|
|
296
424
|
}
|
|
297
|
-
|
|
425
|
+
const toolCalls = this.assistantToolCalls(message);
|
|
426
|
+
if (separator)
|
|
427
|
+
this.setAssistantSeparatorContent(separator, message);
|
|
428
|
+
for (const content of toolCalls) {
|
|
298
429
|
this.pushTranscriptValue(content.arguments);
|
|
299
430
|
const component = this.makeToolComponent(content.name, content.id, content.arguments);
|
|
300
431
|
this.append(component);
|
|
@@ -327,6 +458,10 @@ export class ChatView {
|
|
|
327
458
|
for (const [id, component] of renderedPendingTools) {
|
|
328
459
|
this.pendingTools.set(id, component);
|
|
329
460
|
}
|
|
461
|
+
// A rebuild constructs fresh components, which are born unfolded; re-apply the
|
|
462
|
+
// setting so a reconnect/welcome does not silently unfold the transcript.
|
|
463
|
+
if (this.foldSettledTools)
|
|
464
|
+
this.applyToolDisplay();
|
|
330
465
|
// If we attached mid-turn, the `agent_start` that spins up the working loader
|
|
331
466
|
// already fired before we connected — restore it from the snapshot's streaming
|
|
332
467
|
// state so an actively-working node shows the spinner right away.
|
|
@@ -403,6 +538,7 @@ export class ChatView {
|
|
|
403
538
|
this.historyContainer.removeChild(this.streamingComponent);
|
|
404
539
|
this.streamingComponent = undefined;
|
|
405
540
|
}
|
|
541
|
+
this.streamingToolSeparator = undefined;
|
|
406
542
|
this.pendingTools.clear();
|
|
407
543
|
this.setActivity(undefined);
|
|
408
544
|
this.settleBashComponent();
|
|
@@ -424,13 +560,32 @@ export class ChatView {
|
|
|
424
560
|
* are frozen as text, so they are unaffected by design. */
|
|
425
561
|
toggleToolsExpanded() {
|
|
426
562
|
this.toolOutputExpanded = !this.toolOutputExpanded;
|
|
563
|
+
this.applyToolDisplay();
|
|
564
|
+
return this.toolOutputExpanded;
|
|
565
|
+
}
|
|
566
|
+
/** The `fold finished tool calls` view setting (`/fold-tools`, Alt+C → w → f).
|
|
567
|
+
* Returns the new state so the caller can surface a notice. */
|
|
568
|
+
toggleFoldSettledTools() {
|
|
569
|
+
this.foldSettledTools = !this.foldSettledTools;
|
|
570
|
+
persistFoldFinishedTools(this.foldSettledTools);
|
|
571
|
+
this.applyToolDisplay();
|
|
572
|
+
return this.foldSettledTools;
|
|
573
|
+
}
|
|
574
|
+
/** Push both display settings onto every rendered component. A tool call still in
|
|
575
|
+
* `pendingTools` is in flight, so it is never folded — you keep watching live
|
|
576
|
+
* output, and it folds on its own once `tool_execution_end` settles it. */
|
|
577
|
+
applyToolDisplay() {
|
|
578
|
+
const inFlight = new Set(this.pendingTools.values());
|
|
579
|
+
this.updateFoldedToolSeparators(inFlight);
|
|
427
580
|
for (const child of this.historyContainer.children) {
|
|
581
|
+
if (child instanceof MinimizableToolComponent) {
|
|
582
|
+
child.setMinimized(this.foldSettledTools && !inFlight.has(child));
|
|
583
|
+
}
|
|
428
584
|
if (isExpandable(child))
|
|
429
585
|
child.setExpanded(this.toolOutputExpanded);
|
|
430
586
|
}
|
|
431
587
|
this.historyContainer.markDirty();
|
|
432
588
|
this.tui.requestRender();
|
|
433
|
-
return this.toolOutputExpanded;
|
|
434
589
|
}
|
|
435
590
|
/** Ctrl+T / app.thinking.toggle — flip thinking-block visibility and apply it to
|
|
436
591
|
* every assistant message (the in-flight streaming bubble is itself a child of
|
|
@@ -441,6 +596,9 @@ export class ChatView {
|
|
|
441
596
|
if (child instanceof AssistantMessageComponent)
|
|
442
597
|
child.setHideThinkingBlock(this.hideThinking);
|
|
443
598
|
}
|
|
599
|
+
for (const [separator, content] of this.foldedToolSeparatorContents) {
|
|
600
|
+
separator.setSeparatesToolGroup(content.hasText || (!this.hideThinking && content.hasThinking));
|
|
601
|
+
}
|
|
444
602
|
this.historyContainer.markDirty();
|
|
445
603
|
this.tui.requestRender();
|
|
446
604
|
return this.hideThinking;
|
|
@@ -485,9 +643,11 @@ export class ChatView {
|
|
|
485
643
|
this.addMessageToChat(event.message);
|
|
486
644
|
}
|
|
487
645
|
else if (event.message.role === 'assistant') {
|
|
488
|
-
this.streamingComponent = new
|
|
646
|
+
this.streamingComponent = new ViewerAssistantMessageComponent(undefined, this.hideThinking, attachMarkdownTheme(), this.hiddenThinkingLabel);
|
|
489
647
|
this.append(this.streamingComponent);
|
|
490
648
|
this.streamingComponent.updateContent(styleAttachMessageMarkdown(event.message));
|
|
649
|
+
this.streamingToolSeparator = this.appendFoldedToolSeparator();
|
|
650
|
+
this.setAssistantSeparatorContent(this.streamingToolSeparator, event.message);
|
|
491
651
|
}
|
|
492
652
|
break;
|
|
493
653
|
}
|
|
@@ -501,7 +661,10 @@ export class ChatView {
|
|
|
501
661
|
const updated = styleAttachMessageMarkdown(transformMessageDiagrams(event.message, this.renderWidth()));
|
|
502
662
|
this.streamingComponent.updateContent(updated);
|
|
503
663
|
this.historyContainer.markDirty(this.streamingComponent);
|
|
504
|
-
|
|
664
|
+
const toolCalls = this.assistantToolCalls(event.message);
|
|
665
|
+
if (this.streamingToolSeparator)
|
|
666
|
+
this.setAssistantSeparatorContent(this.streamingToolSeparator, event.message);
|
|
667
|
+
for (const content of toolCalls) {
|
|
505
668
|
this.pushTranscriptValue(content.arguments);
|
|
506
669
|
const existing = this.pendingTools.get(content.id);
|
|
507
670
|
if (existing) {
|
|
@@ -564,6 +727,7 @@ export class ChatView {
|
|
|
564
727
|
}
|
|
565
728
|
this.historyContainer.markDirty(this.streamingComponent);
|
|
566
729
|
this.streamingComponent = undefined;
|
|
730
|
+
this.streamingToolSeparator = undefined;
|
|
567
731
|
}
|
|
568
732
|
break;
|
|
569
733
|
}
|
|
@@ -594,6 +758,10 @@ export class ChatView {
|
|
|
594
758
|
if (component) {
|
|
595
759
|
component.updateResult({ ...event.result, isError: event.isError });
|
|
596
760
|
this.pendingTools.delete(event.toolCallId);
|
|
761
|
+
// Settled → it now obeys the fold setting it was exempt from while live.
|
|
762
|
+
if (this.foldSettledTools)
|
|
763
|
+
component.setMinimized(true);
|
|
764
|
+
this.updateFoldedToolSeparators();
|
|
597
765
|
this.historyContainer.markDirty(component);
|
|
598
766
|
}
|
|
599
767
|
break;
|
|
@@ -787,8 +955,7 @@ export class ChatView {
|
|
|
787
955
|
break;
|
|
788
956
|
}
|
|
789
957
|
case 'assistant': {
|
|
790
|
-
|
|
791
|
-
this.append(new AssistantMessageComponent(styleAttachMessageMarkdown(transformMessageDiagrams(message, this.renderWidth())), this.hideThinking, attachMarkdownTheme(), this.hiddenThinkingLabel));
|
|
958
|
+
this.appendAssistantMessage(message);
|
|
792
959
|
break;
|
|
793
960
|
}
|
|
794
961
|
// toolResult is rendered inline with its tool call (handled in applySnapshot).
|
|
@@ -799,15 +966,67 @@ export class ChatView {
|
|
|
799
966
|
// -------------------------------------------------------------------------
|
|
800
967
|
// Helpers
|
|
801
968
|
// -------------------------------------------------------------------------
|
|
969
|
+
/** Render an assistant reply plus its initially-hidden boundary separator. */
|
|
970
|
+
appendAssistantMessage(message) {
|
|
971
|
+
this.append(new ViewerAssistantMessageComponent(styleAttachMessageMarkdown(transformMessageDiagrams(message, this.renderWidth())), this.hideThinking, attachMarkdownTheme(), this.hiddenThinkingLabel));
|
|
972
|
+
const separator = this.appendFoldedToolSeparator();
|
|
973
|
+
this.setAssistantSeparatorContent(separator, message);
|
|
974
|
+
return separator;
|
|
975
|
+
}
|
|
976
|
+
/** Add a separator in transcript order now; it renders only after a following
|
|
977
|
+
* tool settles in the folded view. */
|
|
978
|
+
appendFoldedToolSeparator() {
|
|
979
|
+
const separator = new FoldedToolSeparator();
|
|
980
|
+
this.append(separator);
|
|
981
|
+
return separator;
|
|
982
|
+
}
|
|
983
|
+
/** Make each separator visible only if its immediately-following tool call is
|
|
984
|
+
* actually folded; in-flight calls remain flush with their live output. */
|
|
985
|
+
updateFoldedToolSeparators(inFlight = new Set(this.pendingTools.values())) {
|
|
986
|
+
const children = this.historyContainer.children;
|
|
987
|
+
for (let i = 0; i < children.length; i++) {
|
|
988
|
+
const separator = children[i];
|
|
989
|
+
if (!(separator instanceof FoldedToolSeparator))
|
|
990
|
+
continue;
|
|
991
|
+
const followingTool = children[i + 1];
|
|
992
|
+
separator.setFolded(this.foldSettledTools &&
|
|
993
|
+
followingTool instanceof MinimizableToolComponent &&
|
|
994
|
+
!inFlight.has(followingTool));
|
|
995
|
+
this.historyContainer.markDirty(separator);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
/** Track whether an assistant response contains prose or thinking, then make
|
|
999
|
+
* its separator eligible only for content the current Ctrl+T setting shows. */
|
|
1000
|
+
setAssistantSeparatorContent(separator, message) {
|
|
1001
|
+
const content = message.content;
|
|
1002
|
+
const parts = Array.isArray(content) ? content : [];
|
|
1003
|
+
const hasText = parts.some((part) => typeof part === 'object' &&
|
|
1004
|
+
part !== null &&
|
|
1005
|
+
part.type === 'text' &&
|
|
1006
|
+
typeof part.text === 'string' &&
|
|
1007
|
+
part.text.trim() !== '');
|
|
1008
|
+
const hasThinking = parts.some((part) => typeof part === 'object' &&
|
|
1009
|
+
part !== null &&
|
|
1010
|
+
part.type === 'thinking' &&
|
|
1011
|
+
typeof part.thinking === 'string' &&
|
|
1012
|
+
part.thinking.trim() !== '');
|
|
1013
|
+
this.foldedToolSeparatorContents.set(separator, { hasText, hasThinking });
|
|
1014
|
+
separator.setSeparatesToolGroup(hasText || (!this.hideThinking && hasThinking));
|
|
1015
|
+
}
|
|
802
1016
|
/** Tool component with native viewer renderers for crouter-owned tool output. */
|
|
803
1017
|
makeToolComponent(name, id, args) {
|
|
804
1018
|
const toolDefinition = name === 'bash'
|
|
805
|
-
? createCrtrBashToolDefinition(args)
|
|
1019
|
+
? (createCrtrBashToolDefinition(args) ?? createCrtrPlainBashToolDefinition(this.cwd))
|
|
806
1020
|
: name === 'edit'
|
|
807
1021
|
? createCrtrEditToolDefinition(this.cwd)
|
|
808
|
-
:
|
|
1022
|
+
: name === 'read'
|
|
1023
|
+
? createCrtrReadToolDefinition(this.cwd)
|
|
1024
|
+
: name === 'write'
|
|
1025
|
+
? createCrtrWriteToolDefinition(this.cwd)
|
|
1026
|
+
: undefined;
|
|
809
1027
|
let component;
|
|
810
|
-
component = new
|
|
1028
|
+
component = new MinimizableToolComponent(name, id, args, { showImages: this.showImages, imageWidthCells: this.imageWidthCells }, toolDefinition, this.componentTui(() => component), this.cwd);
|
|
1029
|
+
component.setFoldable(toolDefinition !== undefined);
|
|
811
1030
|
component.setExpanded(this.toolOutputExpanded);
|
|
812
1031
|
return component;
|
|
813
1032
|
}
|
|
@@ -832,6 +1051,8 @@ export class ChatView {
|
|
|
832
1051
|
this.setActivity(undefined);
|
|
833
1052
|
this.settleBashComponent();
|
|
834
1053
|
this.streamingComponent = undefined;
|
|
1054
|
+
this.streamingToolSeparator = undefined;
|
|
1055
|
+
this.foldedToolSeparatorContents.clear();
|
|
835
1056
|
this.pendingTools.clear();
|
|
836
1057
|
this.lastAssistantText = undefined;
|
|
837
1058
|
this.transcriptTexts.length = 0;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// `renderContextMessage`) and reuses the same collapsed/expand shape for
|
|
9
9
|
// situational context so both render consistently: collapsed → one dim line;
|
|
10
10
|
// expanded (Ctrl+O) → label + full wrapped body. It implements `setExpanded`,
|
|
11
|
-
// so the Ctrl+O tool-
|
|
11
|
+
// so the Ctrl+O tool-display cycle (ChatView.cycleToolDisplay →
|
|
12
12
|
// isExpandable) folds it alongside tool output.
|
|
13
13
|
import { truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
|
|
14
14
|
/** Default label/hint for the `crtr-context` bearings block. Mirrors the
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
// ANSI re-parsing of pi's output is ever needed.
|
|
15
15
|
import { createEditToolDefinition } from '@earendil-works/pi-coding-agent';
|
|
16
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';
|
|
17
19
|
const FALLBACK_ADDED = { r: 181, g: 189, b: 104 }; // pi dark green
|
|
18
20
|
const FALLBACK_REMOVED = { r: 204, g: 102, b: 102 }; // pi dark red
|
|
19
21
|
const FALLBACK_CARD_BG = { r: 40, g: 50, b: 40 }; // pi dark toolSuccessBg
|
|
@@ -219,6 +221,12 @@ function restyleEditCallBox(box, theme) {
|
|
|
219
221
|
// (mirrors pi's getEditHeaderBg), so tinted lines restore to that color.
|
|
220
222
|
box.addChild(buildDiffBody(preview.diff, theme, theme.getBgAnsi('toolSuccessBg')));
|
|
221
223
|
}
|
|
224
|
+
/** Put the edit glyph in front of pi's `edit <path>` header line. The header is
|
|
225
|
+
* the first child of the card and the ONLY line a folded edit keeps, so this is
|
|
226
|
+
* what the human sees when the diff is collapsed away. */
|
|
227
|
+
function prefixEditHeaderIcon(box, theme) {
|
|
228
|
+
prefixToolText(box.children[0], `${theme.fg('accent', TOOL_ICON.edit)} `);
|
|
229
|
+
}
|
|
222
230
|
/**
|
|
223
231
|
* Attach-viewer tool definition for `edit`: pi's builtin definition drives
|
|
224
232
|
* all state (preview computation, header, success/error bg), then the diff
|
|
@@ -233,10 +241,20 @@ export function createCrtrEditToolDefinition(cwd) {
|
|
|
233
241
|
parameters: {},
|
|
234
242
|
renderShell: 'self',
|
|
235
243
|
renderCall(args, theme, context) {
|
|
244
|
+
// Folded: the renderer owns its folded form — one line, `<icon> edit <path>`,
|
|
245
|
+
// no diff. The real edit card persists in context.state.callComponent (the
|
|
246
|
+
// builtin prefers it over a non-Box lastComponent), so unfolding restores it.
|
|
247
|
+
if (isFoldedContext(context)) {
|
|
248
|
+
const a = args;
|
|
249
|
+
const raw = typeof a?.file_path === 'string' ? a.file_path : typeof a?.path === 'string' ? a.path : '';
|
|
250
|
+
return new Text(foldedFileCallLine('edit', raw, theme, context.cwd ?? cwd), 0, 0);
|
|
251
|
+
}
|
|
236
252
|
const component = builtin.renderCall(args, theme, context);
|
|
237
253
|
const callBox = context.state.callComponent;
|
|
238
|
-
if (callBox)
|
|
254
|
+
if (callBox) {
|
|
239
255
|
restyleEditCallBox(callBox, theme);
|
|
256
|
+
prefixEditHeaderIcon(callBox, theme);
|
|
257
|
+
}
|
|
240
258
|
return component;
|
|
241
259
|
},
|
|
242
260
|
renderResult(result, options, theme, context) {
|
|
@@ -246,6 +264,7 @@ export function createCrtrEditToolDefinition(cwd) {
|
|
|
246
264
|
// builtin renderResult re-ran buildEditCallComponent (pi styling) —
|
|
247
265
|
// restyle the refreshed body.
|
|
248
266
|
restyleEditCallBox(callBox, theme);
|
|
267
|
+
prefixEditHeaderIcon(callBox, theme);
|
|
249
268
|
return component;
|
|
250
269
|
}
|
|
251
270
|
// Fallback path (no call box state, e.g. a bare result replay): render
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
type Theme = {
|
|
2
|
+
fg: (name: string, text: string) => string;
|
|
3
|
+
bold: (text: string) => string;
|
|
4
|
+
};
|
|
5
|
+
/** Key in a tool's shared render-state bag (`context.state`) carrying the
|
|
6
|
+
* viewer's fold. `MinimizableToolComponent` sets it; the fold-aware renderCalls
|
|
7
|
+
* below read it and emit ONLY their one-line header while it is true, which is
|
|
8
|
+
* what makes a folded card exactly `<icon> <tool> <path>` with no body — the
|
|
9
|
+
* renderer owns its folded form, no post-hoc slicing of rendered rows. */
|
|
10
|
+
export declare const FOLDED_STATE_KEY = "crtrFolded";
|
|
11
|
+
export declare function isFoldedContext(context: {
|
|
12
|
+
state?: object;
|
|
13
|
+
}): boolean;
|
|
14
|
+
/** The one-line folded form of a file tool's call: `<icon> <label> <path>`. */
|
|
15
|
+
export declare function foldedFileCallLine(tool: 'read' | 'write' | 'edit', rawPath: string, theme: Theme, cwd: string): string;
|
|
16
|
+
/** Prefix a pi-built `Text` in place (its `text` field is TS-private but a plain
|
|
17
|
+
* runtime property). Used where the header line is baked into the same string as
|
|
18
|
+
* a body we want to keep verbatim — write's syntax-highlighted content preview,
|
|
19
|
+
* edit's header inside its own card. Silently no-ops if pi's Text ever stops
|
|
20
|
+
* looking like this, so the worst case is a missing glyph, never a crash. */
|
|
21
|
+
export declare function prefixToolText(component: unknown, prefix: string): void;
|
|
22
|
+
/** Attach-viewer `read`: an icon + the path (plus any `:offset-limit` range).
|
|
23
|
+
* The call is a single line by construction, so a folded read shows exactly
|
|
24
|
+
* what it read and nothing else. */
|
|
25
|
+
export declare function createCrtrReadToolDefinition(cwd: string): unknown;
|
|
26
|
+
/** Attach-viewer `write`: pi's builtin call renderer (it owns the
|
|
27
|
+
* syntax-highlighted content preview and its expand hint) with the icon
|
|
28
|
+
* prefixed onto its header line. Folded, only that header survives. */
|
|
29
|
+
export declare function createCrtrWriteToolDefinition(cwd: string): unknown;
|
|
30
|
+
/** Attach-viewer `bash` for anything that is not a `crtr` invocation. */
|
|
31
|
+
export declare function createCrtrPlainBashToolDefinition(cwd: string): unknown;
|
|
32
|
+
export {};
|