@esso0428/pi-subagents 0.15.14 → 0.15.15

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 (41) hide show
  1. package/CHANGELOG.md +6 -37
  2. package/dist/ui/agent-widget.d.ts +8 -0
  3. package/dist/ui/agent-widget.d.ts.map +1 -1
  4. package/dist/ui/agent-widget.js +40 -12
  5. package/dist/ui/agent-widget.js.map +1 -1
  6. package/dist/ui/ccstyle/diff/diff-renderer.d.ts.map +1 -1
  7. package/dist/ui/ccstyle/diff/diff-renderer.js +2 -4
  8. package/dist/ui/ccstyle/diff/diff-renderer.js.map +1 -1
  9. package/dist/ui/ccstyle/tool-renderer.d.ts.map +1 -1
  10. package/dist/ui/ccstyle/tool-renderer.js +4 -10
  11. package/dist/ui/ccstyle/tool-renderer.js.map +1 -1
  12. package/dist/ui/ccstyle/tool-result.d.ts +9 -16
  13. package/dist/ui/ccstyle/tool-result.d.ts.map +1 -1
  14. package/dist/ui/ccstyle/tool-result.js +71 -121
  15. package/dist/ui/ccstyle/tool-result.js.map +1 -1
  16. package/dist/ui/conversation-timeline.d.ts +14 -9
  17. package/dist/ui/conversation-timeline.d.ts.map +1 -1
  18. package/dist/ui/conversation-timeline.js +60 -72
  19. package/dist/ui/conversation-timeline.js.map +1 -1
  20. package/dist/ui/conversation-viewer.d.ts +4 -1
  21. package/dist/ui/conversation-viewer.d.ts.map +1 -1
  22. package/dist/ui/conversation-viewer.js +95 -94
  23. package/dist/ui/conversation-viewer.js.map +1 -1
  24. package/dist/write-execution.d.ts +0 -2
  25. package/dist/write-execution.d.ts.map +1 -1
  26. package/dist/write-execution.js +1 -5
  27. package/dist/write-execution.js.map +1 -1
  28. package/package.json +1 -1
  29. package/src/ui/agent-widget.ts +38 -12
  30. package/src/ui/ccstyle/diff/diff-renderer.ts +2 -7
  31. package/src/ui/ccstyle/tool-renderer.ts +9 -22
  32. package/src/ui/ccstyle/tool-result.ts +82 -186
  33. package/src/ui/conversation-timeline.ts +77 -73
  34. package/src/ui/conversation-viewer.ts +102 -108
  35. package/src/write-execution.ts +1 -7
  36. package/test/ui/agent-widget.test.ts +123 -0
  37. package/test/ui/conversation-diff.test.ts +1 -47
  38. package/test/ui/conversation-viewer.test.ts +181 -112
  39. package/test/write-execution.test.ts +0 -2
  40. package/.pi/tasks/tasks-01a09263-0ac6-77be-97f5-664b3ffea76b.json +0 -53
  41. package/.pi/workflows/index.json +0 -5
@@ -14,7 +14,19 @@ import { getLifetimeTotal, getSessionContextPercent, type LifetimeUsage, type Se
14
14
  // ---- Constants ----
15
15
 
16
16
  /** Maximum number of rendered lines before overflow collapse kicks in. */
17
- const MAX_WIDGET_LINES = 12;
17
+ export const MAX_WIDGET_LINES = 12;
18
+ /** Keep a small editor/input area visible below the above-editor widget. */
19
+ const MIN_EDITOR_LINES = 4;
20
+
21
+ /**
22
+ * Derive the widget ceiling from the terminal height while retaining the
23
+ * historical MAX_WIDGET_LINES ceiling. Very short terminals may have no room
24
+ * for the widget; that is preferable to consuming the editor/input area.
25
+ */
26
+ export function getWidgetLineBudget(rows: number): number {
27
+ if (!Number.isFinite(rows)) return MAX_WIDGET_LINES;
28
+ return Math.min(MAX_WIDGET_LINES, Math.max(0, Math.floor(rows) - MIN_EDITOR_LINES));
29
+ }
18
30
 
19
31
  /** Braille spinner frames for animated running indicator. */
20
32
  export const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
@@ -176,9 +188,9 @@ export function buildInvocationTags(
176
188
  return { modelName: invocation.modelName, tags };
177
189
  }
178
190
 
179
- /** Truncate text to a single line, max `len` chars. */
191
+ /** Normalize and truncate text so it can never add physical widget rows. */
180
192
  function truncateLine(text: string, len = 60): string {
181
- const line = text.split("\n").find(l => l.trim())?.trim() ?? "";
193
+ const line = text.replace(/[\r\n]+/g, " ").replace(/\s+/g, " ").trim();
182
194
  if (line.length <= len) return line;
183
195
  return line.slice(0, len) + "…";
184
196
  }
@@ -325,7 +337,7 @@ export class AgentWidget {
325
337
  statusText = theme.fg("dim", " stopped");
326
338
  } else if (a.status === "error") {
327
339
  icon = theme.fg("error", "✗");
328
- const errMsg = a.error ? `: ${a.error.slice(0, 60)}` : "";
340
+ const errMsg = a.error ? `: ${truncateLine(a.error)}` : "";
329
341
  statusText = theme.fg("error", ` error${errMsg}`);
330
342
  } else {
331
343
  // aborted
@@ -340,7 +352,7 @@ export class AgentWidget {
340
352
  parts.push(duration);
341
353
 
342
354
  const modeTag = modeLabel ? ` ${theme.fg("dim", `(${modeLabel})`)}` : "";
343
- return `${icon} ${theme.fg("dim", name)}${modeTag} ${theme.fg("dim", a.description)} ${theme.fg("dim", "·")} ${theme.fg("dim", parts.join(" · "))}${statusText}`;
355
+ return `${icon} ${theme.fg("dim", name)}${modeTag} ${theme.fg("dim", truncateLine(a.description))} ${theme.fg("dim", "·")} ${theme.fg("dim", parts.join(" · "))}${statusText}`;
344
356
  }
345
357
 
346
358
  /**
@@ -363,6 +375,8 @@ export class AgentWidget {
363
375
  if (!hasActive && !hasFinished) return [];
364
376
 
365
377
  const w = tui.terminal.columns;
378
+ const maxLines = getWidgetLineBudget(tui.terminal.rows);
379
+ if (maxLines === 0) return [];
366
380
  const truncate = (line: string) => truncateToWidth(line, w);
367
381
  const headingColor = hasActive ? "accent" : "dim";
368
382
  const headingIcon = hasActive ? "●" : "○";
@@ -399,7 +413,7 @@ export class AgentWidget {
399
413
  const activity = bg ? describeActivity(bg.activeTools, bg.responseText) : "thinking…";
400
414
 
401
415
  runningLines.push([
402
- truncate(theme.fg("dim", "├─") + ` ${theme.fg("accent", frame)} ${theme.bold(name)}${modeTag} ${theme.fg("muted", a.description)} ${theme.fg("dim", "·")} ${fgPreservingNestedStyles(theme, "dim", statsText)}`),
416
+ truncate(theme.fg("dim", "├─") + ` ${theme.fg("accent", frame)} ${theme.bold(name)}${modeTag} ${theme.fg("muted", truncateLine(a.description))} ${theme.fg("dim", "·")} ${fgPreservingNestedStyles(theme, "dim", statsText)}`),
403
417
  truncate(theme.fg("dim", "│ ") + theme.fg("dim", ` ⎿ ${activity}`)),
404
418
  ]);
405
419
  }
@@ -408,12 +422,18 @@ export class AgentWidget {
408
422
  ? truncate(theme.fg("dim", "├─") + ` ${theme.fg("muted", "◦")} ${theme.fg("dim", `${queued.length} queued`)}`)
409
423
  : undefined;
410
424
 
411
- // Assemble with overflow cap (heading + overflow indicator = 2 reserved lines).
412
- const maxBody = MAX_WIDGET_LINES - 1; // heading takes 1 line
425
+ // Assemble with a responsive cap (heading + overflow indicator = 2
426
+ // reserved lines when content exceeds the available body budget).
427
+ const maxBody = maxLines - 1; // heading takes 1 line
413
428
  const totalBody = finishedLines.length + runningLines.length * 2 + (queuedLine ? 1 : 0);
414
429
 
415
430
  const lines: string[] = [truncate(theme.fg(headingColor, headingIcon) + " " + theme.fg(headingColor, "Agents"))];
416
431
 
432
+ if (maxLines === 1) {
433
+ // There is room only for the heading; do not consume the editor/input row.
434
+ return lines;
435
+ }
436
+
417
437
  if (totalBody <= maxBody) {
418
438
  // Everything fits — add all lines and fix up connectors for the last item.
419
439
  lines.push(...finishedLines);
@@ -439,6 +459,7 @@ export class AgentWidget {
439
459
  // Reserve 1 line for overflow indicator.
440
460
  let budget = maxBody - 1;
441
461
  let hiddenRunning = 0;
462
+ let hiddenQueued = 0;
442
463
  let hiddenFinished = 0;
443
464
 
444
465
  // 1. Running agents (2 lines each)
@@ -452,9 +473,13 @@ export class AgentWidget {
452
473
  }
453
474
 
454
475
  // 2. Queued line
455
- if (queuedLine && budget >= 1) {
456
- lines.push(queuedLine);
457
- budget--;
476
+ if (queuedLine) {
477
+ if (budget >= 1) {
478
+ lines.push(queuedLine);
479
+ budget--;
480
+ } else {
481
+ hiddenQueued = 1;
482
+ }
458
483
  }
459
484
 
460
485
  // 3. Finished agents
@@ -470,9 +495,10 @@ export class AgentWidget {
470
495
  // Overflow summary
471
496
  const overflowParts: string[] = [];
472
497
  if (hiddenRunning > 0) overflowParts.push(`${hiddenRunning} running`);
498
+ if (hiddenQueued > 0) overflowParts.push(`${hiddenQueued} queued`);
473
499
  if (hiddenFinished > 0) overflowParts.push(`${hiddenFinished} finished`);
474
500
  const overflowText = overflowParts.join(", ");
475
- lines.push(truncate(theme.fg("dim", "└─") + ` ${theme.fg("dim", `+${hiddenRunning + hiddenFinished} more (${overflowText})`)}`)
501
+ lines.push(truncate(theme.fg("dim", "└─") + ` ${theme.fg("dim", `+${hiddenRunning + hiddenQueued + hiddenFinished} more (${overflowText})`)}`)
476
502
  );
477
503
  }
478
504
 
@@ -2951,11 +2951,6 @@ export function renderWriteDiffResult(
2951
2951
  return new Text(theme.fg("warning", "↳ diff unavailable: durable diff has no displayable lines"), 0, 0);
2952
2952
  }
2953
2953
  }
2954
- const persistedWasOverwrite = persistedParsed?.entries.some(
2955
- (entry) => entry.kind === "line" && entry.lineKind === "remove",
2956
- ) ?? false;
2957
- const wasOverwrite = options.fileExistedBeforeWrite === true || persistedWasOverwrite;
2958
-
2959
2954
  if (typeof content !== "string" && !persistedParsed) {
2960
2955
  if (!safeFallbackText.trim()) {
2961
2956
  return new Text(theme.fg("muted", "↳ write completed"), 0, 0);
@@ -3036,7 +3031,7 @@ export function renderWriteDiffResult(
3036
3031
  hovered,
3037
3032
  clampDiffLinesToWidth(
3038
3033
  renderWriteCollapsedHintLine(
3039
- wasOverwrite,
3034
+ options.fileExistedBeforeWrite === true,
3040
3035
  safeWidth,
3041
3036
  theme,
3042
3037
  hovered,
@@ -3048,7 +3043,7 @@ export function renderWriteDiffResult(
3048
3043
  }
3049
3044
 
3050
3045
  const header = renderWriteHeader(
3051
- wasOverwrite,
3046
+ options.fileExistedBeforeWrite === true,
3052
3047
  safeWidth,
3053
3048
  theme,
3054
3049
  options.headerLabel,
@@ -31,11 +31,8 @@ function summaryForCall(toolName: string, args: any): { main: string; detail: st
31
31
  detail: details.length ? ` (${details.join(", ")})` : "",
32
32
  };
33
33
  }
34
- const value =
35
- args.path ?? args.file_path ?? args.command ?? args.query ?? args.pattern ?? args.url ?? args.name;
36
- return value === undefined
37
- ? { main: title, detail: "" }
38
- : { main: `${title} ${oneLine(value)}`, detail: "" };
34
+ const value = args.path ?? args.file_path ?? args.command ?? args.query ?? args.pattern ?? args.url ?? args.name;
35
+ return value === undefined ? { main: title, detail: "" } : { main: `${title} ${oneLine(value)}`, detail: "" };
39
36
  }
40
37
 
41
38
  function renderCallComponent(toolName: string, args: any, theme: any, context: any): Component {
@@ -45,11 +42,9 @@ function renderCallComponent(toolName: string, args: any, theme: any, context: a
45
42
  return {
46
43
  render(width: number): string[] {
47
44
  const safeWidth = Math.max(1, Math.floor(Number.isFinite(width) ? width : 1));
48
- const summary = summaryForCall(toolName, args);
49
45
  const prefix = ` ${theme.fg(color, icon)} `;
50
- const title = theme.fg("toolTitle", summary.main);
51
- const detail = summary.detail ? theme.fg("dim", summary.detail) : "";
52
- return [truncateToWidth(prefix + title + detail, safeWidth, "")];
46
+ const summary = summaryForCall(toolName, args);
47
+ return [truncateToWidth(prefix + theme.fg("toolTitle", summary.main) + theme.fg("dim", summary.detail), safeWidth, "")];
53
48
  },
54
49
  invalidate() {},
55
50
  };
@@ -78,19 +73,11 @@ export function createViewerCcstyleTool(toolName: string, isHovered: () => boole
78
73
  },
79
74
  renderResult(result, options, theme, context) {
80
75
  settled = true;
81
- return createViewerCcstyleResult(
82
- name,
83
- result,
84
- { ...options, isHovered },
85
- theme,
86
- {
87
- ...context,
88
- isHovered,
89
- diffConfig: {
90
- ...DEFAULT_TOOL_DISPLAY_CONFIG,
91
- },
92
- },
93
- );
76
+ return createViewerCcstyleResult(name, result, { ...options, isHovered }, theme, {
77
+ ...context,
78
+ isHovered,
79
+ diffConfig: { ...DEFAULT_TOOL_DISPLAY_CONFIG },
80
+ });
94
81
  },
95
82
  };
96
83
  }
@@ -51,21 +51,16 @@ export function outputLineCount(result: any): number {
51
51
  }
52
52
 
53
53
  export function countLines(text: string): number {
54
- return text.trim() ? text.trim().split("\n").filter((line) => line.trim().length > 0).length : 0;
54
+ return text.trim() ? text.trim().split("\n").filter((line) => line.trim()).length : 0;
55
55
  }
56
56
 
57
57
  /** Format arguments as stable, readable `key: value` rows for the Input rail. */
58
58
  export function formatToolInputArgs(args: unknown, maxChars = 8_000): string {
59
59
  if (args === undefined || args === null) return "";
60
- if (typeof args !== "object") {
61
- const text = sanitizeToolResultText(String(args));
62
- return text.length > maxChars ? `${text.slice(0, maxChars)}…` : text;
60
+ if (typeof args !== "object" || Array.isArray(args)) {
61
+ const rendered = typeof args === "string" ? sanitizeToolResultText(args) : safeJson(args);
62
+ return rendered.length > maxChars ? `${rendered.slice(0, maxChars)}…` : rendered;
63
63
  }
64
- if (Array.isArray(args)) {
65
- const text = safeJson(args);
66
- return text.length > maxChars ? `${text.slice(0, maxChars)}…` : text;
67
- }
68
-
69
64
  const preferred = [
70
65
  "path", "file_path", "command", "query", "pattern", "url", "name",
71
66
  "message", "content", "old_string", "new_string",
@@ -81,32 +76,24 @@ export function formatToolInputArgs(args: unknown, maxChars = 8_000): string {
81
76
  return li - ri;
82
77
  });
83
78
  const lines: string[] = [];
84
- for (const [rawKey, value] of entries) {
85
- const key = sanitizeToolResultText(rawKey);
79
+ for (const [key, value] of entries) {
80
+ const safeKey = sanitizeToolResultText(key);
86
81
  if (typeof value === "string") {
87
82
  const safeValue = sanitizeToolResultText(value);
88
83
  if (safeValue.includes("\n")) {
89
- lines.push(`${key}:`);
90
- for (const line of safeValue.split("\n")) lines.push(` ${line}`);
91
- } else {
92
- lines.push(`${key}: ${safeValue}`);
93
- }
94
- continue;
95
- }
96
- if (typeof value === "number" || typeof value === "boolean" || value === null) {
97
- lines.push(`${key}: ${String(value)}`);
98
- continue;
99
- }
100
- const json = safeJson(value);
101
- if (json.includes("\n")) {
102
- lines.push(`${key}:`);
103
- for (const line of json.split("\n")) lines.push(` ${line}`);
84
+ lines.push(`${safeKey}:`);
85
+ lines.push(...safeValue.split("\n").map((line) => ` ${line}`));
86
+ } else lines.push(`${safeKey}: ${safeValue}`);
104
87
  } else {
105
- lines.push(`${key}: ${json}`);
88
+ const rendered = safeJson(value);
89
+ if (rendered.includes("\n")) {
90
+ lines.push(`${safeKey}:`);
91
+ lines.push(...rendered.split("\n").map((line) => ` ${line}`));
92
+ } else lines.push(`${safeKey}: ${rendered}`);
106
93
  }
107
94
  }
108
- const text = lines.join("\n");
109
- return text.length > maxChars ? `${text.slice(0, maxChars)}…` : text;
95
+ const rendered = lines.join("\n");
96
+ return rendered.length > maxChars ? `${rendered.slice(0, maxChars)}…` : rendered;
110
97
  }
111
98
 
112
99
  function hasExpandableDetail(text: string, args: unknown): boolean {
@@ -115,18 +102,8 @@ function hasExpandableDetail(text: string, args: unknown): boolean {
115
102
 
116
103
  export type ToolIoSection = "input" | "output";
117
104
 
118
- /**
119
- * Viewer-local Input/Output card. The rail and show-more rows intentionally
120
- * mirror ccstyle, but state is kept on this component so history remains
121
- * deterministic and no global terminal mouse handler is needed.
122
- */
123
- class ExpandedToolIoView implements Component {
124
- private readonly isError: boolean;
125
- private input: string;
126
- private output: string;
127
- private maxOutputLines: number;
128
- private maxInputLines: number;
129
- private theme: any;
105
+ /** Viewer-local adaptation of pi-cc's structured Input/Output tool card. */
106
+ export class ExpandedToolIoView implements Component {
130
107
  private cachedWidth: number | undefined;
131
108
  private cachedLines: string[] | undefined;
132
109
  private lineSections: Array<ToolIoSection | null> = [];
@@ -134,28 +111,16 @@ class ExpandedToolIoView implements Component {
134
111
  private showMoreRows: Partial<Record<ToolIoSection, number>> = {};
135
112
 
136
113
  constructor(
137
- input: string,
138
- output: string,
139
- isError: boolean,
140
- maxOutputLines = DEFAULT_TOOL_DISPLAY_CONFIG.expandedPreviewMaxLines,
141
- maxInputLines = DEFAULT_TOOL_DISPLAY_CONFIG.expandedPreviewMaxLines,
142
- theme?: any,
143
- ) {
144
- this.input = input;
145
- this.output = output;
146
- this.isError = isError;
147
- this.maxOutputLines = Math.max(1, maxOutputLines);
148
- this.maxInputLines = Math.max(1, maxInputLines);
149
- this.theme = theme;
150
- }
114
+ private input: string,
115
+ private output: string,
116
+ private readonly isError: boolean,
117
+ private readonly maxOutputLines = DEFAULT_TOOL_DISPLAY_CONFIG.expandedPreviewMaxLines,
118
+ private readonly maxInputLines = DEFAULT_TOOL_DISPLAY_CONFIG.expandedPreviewMaxLines,
119
+ private readonly theme?: any,
120
+ ) {}
151
121
 
152
- getInputBody(): string {
153
- return this.input;
154
- }
155
-
156
- getOutputBody(): string {
157
- return this.output.trim() ? this.output : "Done";
158
- }
122
+ getInputBody(): string { return this.input; }
123
+ getOutputBody(): string { return this.output.trim() ? this.output : "Done"; }
159
124
 
160
125
  setHoveredSection(section: ToolIoSection | null): void {
161
126
  if (this.hoveredSection === section) return;
@@ -163,47 +128,30 @@ class ExpandedToolIoView implements Component {
163
128
  this.invalidate();
164
129
  }
165
130
 
166
- sectionAtLine(line: number): ToolIoSection | null {
167
- return this.lineSections[line] ?? null;
168
- }
169
-
170
- showMoreLine(section: ToolIoSection): number | undefined {
171
- return this.showMoreRows[section];
172
- }
131
+ sectionAtLine(line: number): ToolIoSection | null { return this.lineSections[line] ?? null; }
132
+ showMoreLine(section: ToolIoSection): number | undefined { return this.showMoreRows[section]; }
173
133
 
174
134
  render(width: number): string[] {
175
135
  const safeWidth = toolViewportWidth(width);
176
136
  if (this.cachedLines && this.cachedWidth === safeWidth) return this.cachedLines;
177
-
178
- const bodyColor = this.isError ? "error" : "toolOutput";
137
+ const theme = this.theme;
138
+ const color = (name: string, value: string): string => typeof theme?.fg === "function" ? theme.fg(name, value) : value;
139
+ const bold = (value: string): string => typeof theme?.bold === "function" ? theme.bold(value) : value;
179
140
  const rail = " │ ";
180
141
  const contentWidth = Math.max(1, safeWidth - visibleWidth(rail));
142
+ const bodyColor = this.isError ? "error" : "toolOutput";
181
143
  const rows: string[] = [];
182
144
  this.lineSections = [];
183
145
  this.showMoreRows = {};
184
-
185
146
  const push = (line: string, section: ToolIoSection | null) => {
186
147
  rows.push(truncateToWidth(line, safeWidth, ""));
187
148
  this.lineSections.push(section);
188
149
  };
189
- // The theme is optional so the component remains safe in small/test hosts.
190
- const theme = this.theme;
191
- const color = (name: string, value: string): string => typeof theme?.fg === "function" ? theme.fg(name, value) : value;
192
- const bold = (value: string): string => typeof theme?.bold === "function" ? theme.bold(value) : value;
193
- const pushHeader = (corner: "├" | "└", title: string) => push(
194
- color("dim", ` ${corner} `) + color("accent", bold(title)),
195
- null,
196
- );
197
- const pushRail = (value: string, section: ToolIoSection, continued: boolean) => push(
198
- color("dim", continued ? rail : " ") + value,
199
- section,
200
- );
201
- const pushBlank = () => push(color("dim", " │"), null);
202
- const styleInput = (line: string): string => {
203
- const match = line.match(/^([A-Za-z_][\w.-]*)(:\s*)(.*)$/);
204
- return match
205
- ? color("dim", `${match[1]}${match[2]}`) + color("muted", match[3] ?? "")
206
- : color("muted", line);
150
+ const pushHeader = (corner: "├" | "└", title: string) => {
151
+ push(color("dim", ` ${corner} `) + color("accent", bold(title)), null);
152
+ };
153
+ const pushRail = (line: string, section: ToolIoSection, continued: boolean) => {
154
+ push(color("dim", continued ? rail : " ") + line, section);
207
155
  };
208
156
  const pushBody = (body: string, section: ToolIoSection, limit: number, continued: boolean, input: boolean) => {
209
157
  const raw = body.replace(/\t/g, " ").replace(/\n+$/, "");
@@ -213,20 +161,25 @@ class ExpandedToolIoView implements Component {
213
161
  }
214
162
  const sources = raw.split("\n");
215
163
  const wrapped: string[] = [];
164
+ const styleInput = (line: string): string => {
165
+ const match = line.match(/^([A-Za-z_][\w.-]*)(:\s*)(.*)$/);
166
+ return match
167
+ ? color("dim", `${match[1]}${match[2]}`) + color("muted", match[3] ?? "")
168
+ : color("muted", line);
169
+ };
216
170
  for (const source of sources) {
217
171
  const styled = input ? styleInput(source) : color(bodyColor, source);
218
- const parts = wrapTextWithAnsi(styled, contentWidth);
219
- wrapped.push(...(parts.length ? parts : [styled]));
172
+ const lines = wrapTextWithAnsi(styled, contentWidth);
173
+ wrapped.push(...(lines.length ? lines : [styled]));
220
174
  }
221
175
  const truncated = sources.length > limit || wrapped.length > limit;
222
176
  const visible = truncated ? wrapped.slice(0, limit) : wrapped;
223
177
  for (const line of visible) pushRail(line, section, continued);
224
- if (truncated) {
225
- const hidden = Math.max(1, wrapped.length - visible.length);
178
+ if (truncated && wrapped.length > visible.length) {
179
+ const hidden = wrapped.length - visible.length;
226
180
  const hintColor = this.hoveredSection === section ? "text" : "dim";
227
181
  pushRail(
228
- color("dim", `… +${hidden} more lines`) +
229
- color("dim", " •") + color(hintColor, ` ${showMoreHintText()}`),
182
+ color("dim", `… +${hidden} more lines`) + color("dim", " •") + color(hintColor, ` ${showMoreHintText()}`),
230
183
  section,
231
184
  continued,
232
185
  );
@@ -235,15 +188,13 @@ class ExpandedToolIoView implements Component {
235
188
  };
236
189
 
237
190
  const hasInput = this.input.trim().length > 0;
238
- const output = this.getOutputBody();
239
191
  if (hasInput) {
240
192
  pushHeader("├", "Input");
241
- pushBody(this.input, "input", this.maxInputLines, true, true);
242
- pushBlank();
193
+ pushBody(this.input, "input", Math.max(1, this.maxInputLines), true, true);
194
+ push(color("dim", " │"), null);
243
195
  }
244
196
  pushHeader("└", "Output");
245
- pushBody(output, "output", this.maxOutputLines, false, false);
246
-
197
+ pushBody(this.getOutputBody(), "output", Math.max(1, this.maxOutputLines), false, false);
247
198
  this.cachedWidth = safeWidth;
248
199
  this.cachedLines = rows;
249
200
  return rows;
@@ -253,7 +204,6 @@ class ExpandedToolIoView implements Component {
253
204
  this.cachedWidth = undefined;
254
205
  this.cachedLines = undefined;
255
206
  }
256
-
257
207
  }
258
208
 
259
209
  function unavailableComponent(reason: string, theme: any): Component {
@@ -265,34 +215,24 @@ function unavailableComponent(reason: string, theme: any): Component {
265
215
  };
266
216
  }
267
217
 
268
- function resultText(result: any): string {
269
- return rawTextFromResult(result);
270
- }
218
+ function resultText(result: any): string { return rawTextFromResult(result); }
271
219
 
272
220
  function persistedFromDetails(details: any): PersistedDiff | undefined {
273
221
  if (!details || typeof details !== "object") return undefined;
274
222
  const persisted: PersistedDiff = {};
275
223
  if (typeof details.diff === "string") persisted.diff = details.diff;
276
224
  if (typeof details.patch === "string") persisted.patch = details.patch;
277
- if (typeof details.diffUnavailableReason === "string") {
278
- persisted.diffUnavailableReason = details.diffUnavailableReason;
279
- }
225
+ if (typeof details.diffUnavailableReason === "string") persisted.diffUnavailableReason = details.diffUnavailableReason;
280
226
  return Object.keys(persisted).length ? persisted : undefined;
281
227
  }
282
228
 
283
229
  function viewerConfig(context: any): ViewerDiffConfig {
284
230
  const candidate = context?.diffConfig;
285
- if (!candidate || typeof candidate !== "object") return DEFAULT_TOOL_DISPLAY_CONFIG;
286
- return {
287
- ...DEFAULT_TOOL_DISPLAY_CONFIG,
288
- ...candidate,
289
- };
231
+ return candidate && typeof candidate === "object"
232
+ ? { ...DEFAULT_TOOL_DISPLAY_CONFIG, ...candidate }
233
+ : DEFAULT_TOOL_DISPLAY_CONFIG;
290
234
  }
291
235
 
292
- /**
293
- * Rich edit/write rendering for the conversation viewer. Durable details are
294
- * passed through before any live/ephemeral context so history is deterministic.
295
- */
296
236
  export function renderRichToolResult(
297
237
  toolName: string,
298
238
  result: any,
@@ -303,61 +243,35 @@ export function renderRichToolResult(
303
243
  if (options?.isPartial || options?.isError || context?.isError) return undefined;
304
244
  const expanded = options?.expanded === true || context?.expanded === true;
305
245
  const args = context?.args ?? options?.args;
306
- const filePath = args?.file_path ?? args?.path;
307
246
  const details = result?.details;
308
247
  const persistedDiff = context?.persistedDiff ?? persistedFromDetails(details);
309
248
  const fileExistedBeforeWrite = typeof context?.fileExistedBeforeWrite === "boolean"
310
249
  ? context.fileExistedBeforeWrite
311
- : typeof details?.fileExistedBeforeWrite === "boolean"
312
- ? details.fileExistedBeforeWrite
313
- : undefined;
250
+ : typeof details?.fileExistedBeforeWrite === "boolean" ? details.fileExistedBeforeWrite : undefined;
314
251
 
315
252
  if (toolName === "edit") {
316
- return renderEditDiffResult(
317
- details,
318
- { expanded, filePath, persistedDiff, isHovered: options?.isHovered, invalidate: context?.invalidate },
319
- viewerConfig(context),
320
- theme,
321
- resultText(result),
322
- );
253
+ return renderEditDiffResult(details, {
254
+ expanded, filePath: args?.file_path ?? args?.path, persistedDiff,
255
+ isHovered: options?.isHovered, invalidate: context?.invalidate,
256
+ }, viewerConfig(context), theme, resultText(result));
323
257
  }
324
258
  if (toolName !== "write") return undefined;
325
259
  if (persistedDiff?.diffUnavailableReason && !persistedDiff.diff && !persistedDiff.patch) {
326
260
  return unavailableComponent(persistedDiff.diffUnavailableReason, theme);
327
261
  }
328
-
329
262
  const content = typeof args?.content === "string" ? args.content : undefined;
330
- if (!content && !persistedDiff?.diff && !persistedDiff?.patch) {
263
+ if (typeof content !== "string" && !persistedDiff?.diff && !persistedDiff?.patch) {
331
264
  return unavailableComponent("execution metadata is unavailable", theme);
332
265
  }
333
- return renderWriteDiffResult(
334
- content,
335
- {
336
- expanded,
337
- filePath,
338
- persistedDiff,
339
- previousContent: context?.previousContent,
340
- fileExistedBeforeWrite,
341
- isHovered: options?.isHovered,
342
- invalidate: context?.invalidate,
343
- },
344
- viewerConfig(context),
345
- theme,
346
- resultText(result),
347
- );
348
- }
349
-
350
- function renderCallSummary(toolName: string, args: any, theme: any): string {
351
- const title = toolName.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ");
352
- const value = args?.path ?? args?.file_path ?? args?.command ?? args?.query ?? args?.name;
353
- const summary = value === undefined ? title : `${title} ${oneLine(value)}`;
354
- return theme.fg("toolTitle", summary);
355
- }
356
-
357
- function settledIcon(context: any): string {
358
- if (context?.isError) return "✗";
359
- if (context?.isPartial || context?.executionStarted) return "●";
360
- return "✓";
266
+ return renderWriteDiffResult(content, {
267
+ expanded,
268
+ filePath: args?.file_path ?? args?.path,
269
+ persistedDiff,
270
+ previousContent: context?.previousContent,
271
+ fileExistedBeforeWrite,
272
+ isHovered: options?.isHovered,
273
+ invalidate: context?.invalidate,
274
+ }, viewerConfig(context), theme, resultText(result));
361
275
  }
362
276
 
363
277
  function renderFallbackResult(result: any, options: any, theme: any, context: any): Component {
@@ -365,49 +279,31 @@ function renderFallbackResult(result: any, options: any, theme: any, context: an
365
279
  const expanded = options?.expanded === true || context?.expanded === true;
366
280
  const maxChars = Number.isFinite(context?.maxChars) ? Math.max(0, Math.floor(context.maxChars)) : 16_384;
367
281
  const text = textFromResult(result, expanded, maxChars);
368
- if (expanded && (text.trim() || formatToolInputArgs(context?.args, maxChars).trim())) {
282
+ const input = formatToolInputArgs(context?.args, maxChars);
283
+ if (expanded && (text.trim() || input.trim())) {
369
284
  const maxLines = Number.isFinite(context?.diffConfig?.expandedPreviewMaxLines)
370
285
  ? Math.max(1, Math.floor(context.diffConfig.expandedPreviewMaxLines))
371
286
  : DEFAULT_TOOL_DISPLAY_CONFIG.expandedPreviewMaxLines;
372
- return new ExpandedToolIoView(
373
- formatToolInputArgs(context?.args, maxChars),
374
- text,
375
- Boolean(error),
376
- maxLines,
377
- maxLines,
378
- theme,
379
- );
287
+ return new ExpandedToolIoView(input, text, Boolean(error), maxLines, maxLines, theme);
380
288
  }
381
- const lines = text ? text.split("\n") : [];
382
- const count = lines.length;
289
+ const count = text ? text.split("\n").length : 0;
383
290
  const summary = error ? oneLine(text) || "Failed" : count ? `${count} ${count === 1 ? "line" : "lines"} returned` : "Done";
384
291
  const expandable = !expanded && hasExpandableDetail(text, context?.args);
385
292
  return {
386
293
  render(width: number): string[] {
387
294
  const hovered = options?.isHovered?.() ?? context?.isHovered?.() ?? false;
388
- const clickLabel = showMoreHintText();
389
- const hint = expandable ? ` • ${clickLabel}` : "";
390
- const color = error ? "error" : "muted";
295
+ const hint = expandable ? ` • ${showMoreHintText()}` : "";
391
296
  const line = expandable && hovered
392
- ? theme.fg(color, ` ↳ ${summary} • `) + theme.fg("text", clickLabel)
393
- : theme.fg(color, ` ↳ ${summary}${hint}`);
297
+ ? theme.fg(error ? "error" : "muted", ` ↳ ${summary} • `) + theme.fg("text", showMoreHintText())
298
+ : theme.fg(error ? "error" : "muted", ` ↳ ${summary}${hint}`);
394
299
  return [truncateToWidth(line, toolViewportWidth(width), "")];
395
300
  },
396
301
  invalidate() {},
397
302
  };
398
303
  }
399
304
 
400
- export function createViewerCcstyleResult(
401
- toolName: string,
402
- result: any,
403
- options: any,
404
- theme: any,
405
- context: any,
406
- ): Component {
407
- return (
408
- renderRichToolResult(toolName, result, options, theme, context) ??
409
- renderFallbackResult(result, options, theme, context)
410
- );
305
+ export function createViewerCcstyleResult(toolName: string, result: any, options: any, theme: any, context: any): Component {
306
+ return renderRichToolResult(toolName, result, options, theme, context) ?? renderFallbackResult(result, options, theme, context);
411
307
  }
412
308
 
413
- export { ExpandedToolIoView };
309
+ export type { DisplayConfigInput };