@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.
Files changed (70) hide show
  1. package/dist/api/dto/nodes.d.ts +3 -0
  2. package/dist/builtin-memory/internal/INDEX.md +1 -0
  3. package/dist/builtin-memory/internal/workflow-codification.md +82 -0
  4. package/dist/clients/attach/__tests__/bash-call-width.test.d.ts +1 -0
  5. package/dist/clients/attach/__tests__/bash-call-width.test.js +38 -0
  6. package/dist/clients/attach/__tests__/chat-view-snapshot-ordering.test.js +20 -0
  7. package/dist/clients/attach/__tests__/crtr-output-coverage.test.js +3 -1
  8. package/dist/clients/attach/chrome/bash-jobs.d.ts +5 -3
  9. package/dist/clients/attach/chrome/bash-jobs.js +15 -8
  10. package/dist/clients/attach/chrome/canvas-panels.js +10 -3
  11. package/dist/clients/attach/chrome/roster.d.ts +7 -2
  12. package/dist/clients/attach/chrome/roster.js +87 -3
  13. package/dist/clients/attach/input/controller.d.ts +3 -0
  14. package/dist/clients/attach/input/controller.js +1 -0
  15. package/dist/clients/attach/render/assistant-message.d.ts +13 -0
  16. package/dist/clients/attach/render/assistant-message.js +55 -0
  17. package/dist/clients/attach/render/chat-view.d.ts +29 -0
  18. package/dist/clients/attach/render/chat-view.js +233 -12
  19. package/dist/clients/attach/render/context-message.js +1 -1
  20. package/dist/clients/attach/render/edit-diff.js +20 -1
  21. package/dist/clients/attach/render/tool-calls.d.ts +32 -0
  22. package/dist/clients/attach/render/tool-calls.js +251 -0
  23. package/dist/clients/attach/session/input-wiring.d.ts +3 -0
  24. package/dist/clients/attach/session/input-wiring.js +3 -0
  25. package/dist/clients/attach/slash/dispatch.d.ts +4 -0
  26. package/dist/clients/attach/slash/dispatch.js +8 -0
  27. package/dist/clients/attach/viewer.js +512 -508
  28. package/dist/commands/cron.js +2 -2
  29. package/dist/commands/node.js +3 -3
  30. package/dist/commands/search.js +1 -1
  31. package/dist/commands/sys/config.js +10 -2
  32. package/dist/commands/sys/doctor.js +22 -1
  33. package/dist/commands/sys/setup-core.d.ts +5 -1
  34. package/dist/commands/sys/setup-core.js +16 -2
  35. package/dist/commands/sys/setup-wizard.d.ts +11 -0
  36. package/dist/commands/sys/setup-wizard.js +111 -6
  37. package/dist/core/__tests__/tmux-surface.test.js +5 -2
  38. package/dist/core/canvas/canvas.js +1 -1
  39. package/dist/core/canvas/labels.d.ts +1 -0
  40. package/dist/core/canvas/labels.js +3 -1
  41. package/dist/core/canvas/nav-render.d.ts +6 -0
  42. package/dist/core/canvas/nav-render.js +8 -0
  43. package/dist/core/canvas/types.d.ts +6 -0
  44. package/dist/core/config.js +3 -2
  45. package/dist/core/keybindings/__tests__/resolve.test.js +1 -1
  46. package/dist/core/keybindings/catalog.d.ts +2 -2
  47. package/dist/core/keybindings/catalog.js +2 -0
  48. package/dist/core/preview-registry.d.ts +5 -0
  49. package/dist/core/preview-registry.js +10 -0
  50. package/dist/core/runtime/canvas-extensions.d.ts +4 -0
  51. package/dist/core/runtime/canvas-extensions.js +4 -0
  52. package/dist/core/runtime/naming-persist.d.ts +4 -3
  53. package/dist/core/runtime/naming-persist.js +10 -9
  54. package/dist/core/runtime/naming.d.ts +28 -8
  55. package/dist/core/runtime/naming.js +168 -32
  56. package/dist/core/runtime/nerd-font.d.ts +28 -0
  57. package/dist/core/runtime/nerd-font.js +127 -0
  58. package/dist/core/runtime/tmux.js +16 -0
  59. package/dist/daemon/api/map.js +2 -0
  60. package/dist/pi-extensions/naming-tool.d.ts +24 -0
  61. package/dist/pi-extensions/naming-tool.js +67 -0
  62. package/dist/types.d.ts +5 -0
  63. package/dist/types.js +1 -0
  64. package/dist/web-client/assets/index-B76ZKfT_.js +79 -0
  65. package/dist/web-client/assets/{index-CpEl9LTS.css → index-CqLKj8Xu.css} +1 -1
  66. package/dist/web-client/index.html +2 -2
  67. package/dist/web-client/sw.js +1 -1
  68. package/package.json +1 -1
  69. package/runtime.lock.json +2 -2
  70. 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 { readConfig } from '../../../core/config.js';
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 AssistantMessageComponent(undefined, this.hideThinking, attachMarkdownTheme(), this.hiddenThinkingLabel);
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.addMessageToChat(message);
423
+ separator = this.appendAssistantMessage(message);
296
424
  }
297
- for (const content of this.assistantToolCalls(message)) {
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 AssistantMessageComponent(undefined, this.hideThinking, attachMarkdownTheme(), this.hiddenThinkingLabel);
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
- for (const content of this.assistantToolCalls(event.message)) {
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
- // Settled history message — render its supported diagram fences inline too.
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
- : undefined;
1022
+ : name === 'read'
1023
+ ? createCrtrReadToolDefinition(this.cwd)
1024
+ : name === 'write'
1025
+ ? createCrtrWriteToolDefinition(this.cwd)
1026
+ : undefined;
809
1027
  let component;
810
- component = new ToolExecutionComponent(name, id, args, { showImages: this.showImages, imageWidthCells: this.imageWidthCells }, toolDefinition, this.componentTui(() => component), this.cwd);
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-output toggle (ChatView.toggleToolsExpanded →
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 {};