@oh-my-pi/pi-coding-agent 9.6.0 → 9.6.3

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 +17 -0
  2. package/docs/theme.md +9 -12
  3. package/package.json +7 -7
  4. package/src/exa/render.ts +12 -12
  5. package/src/ipy/gateway-coordinator.ts +1 -1
  6. package/src/ipy/kernel.ts +5 -0
  7. package/src/ipy/runtime.ts +2 -6
  8. package/src/lsp/render.ts +16 -30
  9. package/src/modes/components/bash-execution.ts +2 -4
  10. package/src/modes/components/custom-message.ts +1 -1
  11. package/src/modes/components/footer.ts +1 -1
  12. package/src/modes/components/history-search.ts +3 -2
  13. package/src/modes/components/hook-message.ts +1 -1
  14. package/src/modes/components/python-execution.ts +2 -4
  15. package/src/modes/components/read-tool-group.ts +1 -1
  16. package/src/modes/components/session-selector.ts +7 -11
  17. package/src/modes/components/status-line/segments.ts +1 -1
  18. package/src/modes/components/status-line.ts +1 -1
  19. package/src/modes/components/tool-execution.ts +3 -3
  20. package/src/modes/components/ttsr-notification.ts +1 -1
  21. package/src/modes/components/welcome.ts +2 -2
  22. package/src/modes/controllers/event-controller.ts +3 -3
  23. package/src/modes/interactive-mode.ts +1 -1
  24. package/src/modes/theme/theme.ts +0 -11
  25. package/src/patch/shared.ts +7 -10
  26. package/src/task/executor.ts +1 -1
  27. package/src/task/render.ts +31 -33
  28. package/src/tools/bash.ts +3 -3
  29. package/src/tools/calculator.ts +3 -3
  30. package/src/tools/fetch.ts +4 -6
  31. package/src/tools/python.ts +22 -31
  32. package/src/tools/read.ts +1 -1
  33. package/src/tools/render-utils.ts +12 -21
  34. package/src/tools/review.ts +1 -7
  35. package/src/tools/ssh.ts +6 -8
  36. package/src/tools/write.ts +5 -5
  37. package/src/tui/code-cell.ts +2 -2
  38. package/src/tui/output-block.ts +2 -2
  39. package/src/tui/tree-list.ts +1 -3
  40. package/src/tui/utils.ts +3 -5
  41. package/src/web/search/render.ts +14 -24
@@ -66,7 +66,7 @@ export class EventController {
66
66
  this.ctx.ui,
67
67
  spinner => theme.fg("accent", spinner),
68
68
  text => theme.fg("muted", text),
69
- `Working${theme.format.ellipsis} (esc to interrupt)`,
69
+ `Working (esc to interrupt)`,
70
70
  getSymbolTheme().spinnerFrames,
71
71
  );
72
72
  this.ctx.statusContainer.addChild(this.ctx.loadingAnimation);
@@ -286,7 +286,7 @@ export class EventController {
286
286
  this.ctx.ui,
287
287
  spinner => theme.fg("accent", spinner),
288
288
  text => theme.fg("muted", text),
289
- `${reasonText}Auto-compacting${theme.format.ellipsis} (esc to cancel)`,
289
+ `${reasonText}Auto-compacting (esc to cancel)`,
290
290
  getSymbolTheme().spinnerFrames,
291
291
  );
292
292
  this.ctx.statusContainer.addChild(this.ctx.autoCompactionLoader);
@@ -337,7 +337,7 @@ export class EventController {
337
337
  this.ctx.ui,
338
338
  spinner => theme.fg("warning", spinner),
339
339
  text => theme.fg("muted", text),
340
- `Retrying (${event.attempt}/${event.maxAttempts}) in ${delaySeconds}s${theme.format.ellipsis} (esc to cancel)`,
340
+ `Retrying (${event.attempt}/${event.maxAttempts}) in ${delaySeconds}s (esc to cancel)`,
341
341
  getSymbolTheme().spinnerFrames,
342
342
  );
343
343
  this.ctx.statusContainer.addChild(this.ctx.retryLoader);
@@ -110,7 +110,7 @@ export class InteractiveMode implements InteractiveModeContext {
110
110
  public autoCompactionLoader: Loader | undefined = undefined;
111
111
  public retryLoader: Loader | undefined = undefined;
112
112
  private pendingWorkingMessage: string | undefined;
113
- private readonly defaultWorkingMessage = `Working${theme.format.ellipsis} (esc to interrupt)`;
113
+ private readonly defaultWorkingMessage = `Working (esc to interrupt)`;
114
114
  public autoCompactionEscapeHandler?: () => void;
115
115
  public retryEscapeHandler?: () => void;
116
116
  public unsubscribe?: () => void;
@@ -124,7 +124,6 @@ export type SymbolKey =
124
124
  | "checkbox.checked"
125
125
  | "checkbox.unchecked"
126
126
  // Text Formatting
127
- | "format.ellipsis"
128
127
  | "format.bullet"
129
128
  | "format.dash"
130
129
  | "format.bracketLeft"
@@ -357,9 +356,6 @@ const UNICODE_SYMBOLS: SymbolMap = {
357
356
  "checkbox.checked": "☑",
358
357
  // pick: ☐ | alt: □ ▢
359
358
  "checkbox.unchecked": "☐",
360
- // Text Formatting
361
- // pick: … | alt: ⋯ ...
362
- "format.ellipsis": "…",
363
359
  // pick: • | alt: · ▪ ◦
364
360
  "format.bullet": "•",
365
361
  // pick: – | alt: — ― -
@@ -598,9 +594,6 @@ const NERD_SYMBOLS: SymbolMap = {
598
594
  "checkbox.checked": "\uf14a",
599
595
  // pick:  | alt: 
600
596
  "checkbox.unchecked": "\uf096",
601
- // Text Formatting
602
- // pick: … | alt: ⋯ ...
603
- "format.ellipsis": "\u2026",
604
597
  // pick:  | alt:   •
605
598
  "format.bullet": "\uf111",
606
599
  // pick: – | alt: — ― -
@@ -752,8 +745,6 @@ const ASCII_SYMBOLS: SymbolMap = {
752
745
  // Checkboxes
753
746
  "checkbox.checked": "[x]",
754
747
  "checkbox.unchecked": "[ ]",
755
- // Text Formatting
756
- "format.ellipsis": "...",
757
748
  "format.bullet": "*",
758
749
  "format.dash": "-",
759
750
  "format.bracketLeft": "[",
@@ -1458,7 +1449,6 @@ export class Theme {
1458
1449
 
1459
1450
  get format() {
1460
1451
  return {
1461
- ellipsis: this.symbols["format.ellipsis"],
1462
1452
  bullet: this.symbols["format.bullet"],
1463
1453
  dash: this.symbols["format.dash"],
1464
1454
  bracketLeft: this.symbols["format.bracketLeft"],
@@ -2171,7 +2161,6 @@ export function getSymbolTheme(): SymbolTheme {
2171
2161
  return {
2172
2162
  cursor: theme.nav.cursor,
2173
2163
  inputCursor: preset === "ascii" ? "|" : "▏",
2174
- ellipsis: theme.format.ellipsis,
2175
2164
  boxRound: theme.boxRound,
2176
2165
  boxSharp: theme.boxSharp,
2177
2166
  table: theme.boxSharp,
@@ -103,10 +103,10 @@ function formatStreamingDiff(diff: string, rawPath: string, uiTheme: Theme): str
103
103
 
104
104
  let text = "\n\n";
105
105
  if (hidden > 0) {
106
- text += uiTheme.fg("dim", `${uiTheme.format.ellipsis} (${hidden} earlier lines)\n`);
106
+ text += uiTheme.fg("dim", `… (${hidden} earlier lines)\n`);
107
107
  }
108
108
  text += renderDiffColored(displayLines.join("\n"), { filePath: rawPath });
109
- text += uiTheme.fg("dim", `\n${uiTheme.format.ellipsis} (streaming)`);
109
+ text += uiTheme.fg("dim", `\n (streaming)`);
110
110
  return text;
111
111
  }
112
112
 
@@ -147,10 +147,7 @@ function renderDiffSection(
147
147
  const remainder: string[] = [];
148
148
  if (hiddenHunks > 0) remainder.push(`${hiddenHunks} more hunks`);
149
149
  if (hiddenLines > 0) remainder.push(`${hiddenLines} more lines`);
150
- text += uiTheme.fg(
151
- "toolOutput",
152
- `\n${uiTheme.format.ellipsis} (${remainder.join(", ")}) ${formatExpandHint(uiTheme)}`,
153
- );
150
+ text += uiTheme.fg("toolOutput", `\n… (${remainder.join(", ")}) ${formatExpandHint(uiTheme)}`);
154
151
  }
155
152
  return text;
156
153
  }
@@ -164,7 +161,7 @@ export const editToolRenderer = {
164
161
  const filePath = shortenPath(rawPath);
165
162
  const editLanguage = getLanguageFromPath(rawPath) ?? "text";
166
163
  const editIcon = uiTheme.fg("muted", uiTheme.getLangIcon(editLanguage));
167
- let pathDisplay = filePath ? uiTheme.fg("accent", filePath) : uiTheme.fg("toolOutput", uiTheme.format.ellipsis);
164
+ let pathDisplay = filePath ? uiTheme.fg("accent", filePath) : uiTheme.fg("toolOutput", "…");
168
165
 
169
166
  // Add arrow for move/rename operations
170
167
  if (args.rename) {
@@ -188,7 +185,7 @@ export const editToolRenderer = {
188
185
  text += `${uiTheme.fg("toolOutput", ui.truncate(line, 80))}\n`;
189
186
  }
190
187
  if (previewLines.length > maxLines) {
191
- text += uiTheme.fg("dim", `${uiTheme.format.ellipsis} ${previewLines.length - maxLines} more lines`);
188
+ text += uiTheme.fg("dim", `… ${previewLines.length - maxLines} more lines`);
192
189
  }
193
190
  } else if (args.newText || args.patch) {
194
191
  const previewLines = (args.newText ?? args.patch ?? "").split("\n");
@@ -198,7 +195,7 @@ export const editToolRenderer = {
198
195
  text += `${uiTheme.fg("toolOutput", ui.truncate(line, 80))}\n`;
199
196
  }
200
197
  if (previewLines.length > maxLines) {
201
- text += uiTheme.fg("dim", `${uiTheme.format.ellipsis} ${previewLines.length - maxLines} more lines`);
198
+ text += uiTheme.fg("dim", `… ${previewLines.length - maxLines} more lines`);
202
199
  }
203
200
  }
204
201
 
@@ -225,7 +222,7 @@ export const editToolRenderer = {
225
222
  const rename = args?.rename || result.details?.rename;
226
223
 
227
224
  // Build path display with line number if available
228
- let pathDisplay = filePath ? uiTheme.fg("accent", filePath) : uiTheme.fg("toolOutput", uiTheme.format.ellipsis);
225
+ let pathDisplay = filePath ? uiTheme.fg("accent", filePath) : uiTheme.fg("toolOutput", "…");
229
226
  const firstChangedLine =
230
227
  (editDiffPreview && "firstChangedLine" in editDiffPreview ? editDiffPreview.firstChangedLine : undefined) ||
231
228
  (result.details && !result.isError ? result.details.firstChangedLine : undefined);
@@ -135,7 +135,7 @@ function resolveModelOverride(
135
135
  ): { model?: Model<Api>; thinkingLevel?: ThinkingLevel } {
136
136
  if (modelPatterns.length === 0) return {};
137
137
  const matchPreferences = { usageOrder: settings?.getStorage()?.getModelUsageOrder() };
138
- const roles = settings?.serialize().modelRoles as Record<string, string> | undefined;
138
+ const roles = settings?.getGroup("modelRoles");
139
139
  for (const pattern of modelPatterns) {
140
140
  const normalized = pattern.trim().toLowerCase();
141
141
  if (!normalized || DEFAULT_MODEL_ALIASES.has(normalized)) {
@@ -15,7 +15,7 @@ import {
15
15
  formatMoreItems,
16
16
  formatStatusIcon,
17
17
  formatTokens,
18
- truncate,
18
+ truncateToWidth,
19
19
  } from "../tools/render-utils";
20
20
  import {
21
21
  type FindingPriority,
@@ -67,10 +67,10 @@ function formatFindingSummary(findings: ReportFindingDetails[], theme: Theme): s
67
67
  return `${theme.fg("dim", "Findings:")} ${parts.join(theme.sep.dot)}`;
68
68
  }
69
69
 
70
- function formatJsonScalar(value: unknown, theme: Theme): string {
70
+ function formatJsonScalar(value: unknown, _theme: Theme): string {
71
71
  if (value === null) return "null";
72
72
  if (typeof value === "string") {
73
- const trimmed = truncate(value, 70, theme.format.ellipsis);
73
+ const trimmed = truncateToWidth(value, 70);
74
74
  return `"${trimmed}"`;
75
75
  }
76
76
  if (typeof value === "number" || typeof value === "boolean") return String(value);
@@ -150,7 +150,7 @@ function renderJsonTreeLines(
150
150
  pushLine(
151
151
  `${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.last)} ${theme.fg(
152
152
  "dim",
153
- theme.format.ellipsis,
153
+ "…",
154
154
  )}`,
155
155
  );
156
156
  return;
@@ -183,7 +183,7 @@ function renderJsonTreeLines(
183
183
  pushLine(
184
184
  `${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.last)} ${theme.fg(
185
185
  "dim",
186
- theme.format.ellipsis,
186
+ "…",
187
187
  )}`,
188
188
  );
189
189
  return;
@@ -253,7 +253,7 @@ function renderOutputSection(
253
253
  lines.push(
254
254
  `${continuePrefix} ${theme.fg("warning", theme.status.warning)} ${theme.fg(
255
255
  "dim",
256
- truncate(warning, 80, theme.format.ellipsis),
256
+ truncateToWidth(warning, 80),
257
257
  )}`,
258
258
  );
259
259
 
@@ -276,7 +276,7 @@ function renderOutputSection(
276
276
  lines.push(`${continuePrefix} ${line}`);
277
277
  }
278
278
  if (tree.truncated) {
279
- lines.push(`${continuePrefix} ${theme.fg("dim", theme.format.ellipsis)}`);
279
+ lines.push(`${continuePrefix} ${theme.fg("dim", "…")}`);
280
280
  }
281
281
  return lines;
282
282
  }
@@ -288,12 +288,12 @@ function renderOutputSection(
288
288
  const outputLines = output.trimEnd().split("\n");
289
289
  const previewCount = expanded ? maxExpanded : maxCollapsed;
290
290
  for (const line of outputLines.slice(0, previewCount)) {
291
- lines.push(`${continuePrefix} ${theme.fg("dim", truncate(line, 70, theme.format.ellipsis))}`);
291
+ lines.push(`${continuePrefix} ${theme.fg("dim", truncateToWidth(line, 70))}`);
292
292
  }
293
293
 
294
294
  if (outputLines.length > previewCount) {
295
295
  lines.push(
296
- `${continuePrefix} ${theme.fg("dim", formatMoreItems(outputLines.length - previewCount, "line", theme))}`,
296
+ `${continuePrefix} ${theme.fg("dim", formatMoreItems(outputLines.length - previewCount, "line"))}`,
297
297
  );
298
298
  }
299
299
 
@@ -318,7 +318,7 @@ function renderOutputSection(
318
318
  lines.push(`${continuePrefix} ${line}`);
319
319
  }
320
320
  if (tree.truncated) {
321
- lines.push(`${continuePrefix} ${theme.fg("dim", theme.format.ellipsis)}`);
321
+ lines.push(`${continuePrefix} ${theme.fg("dim", "…")}`);
322
322
  }
323
323
  return lines;
324
324
  }
@@ -332,19 +332,17 @@ function renderOutputSection(
332
332
  const outputLines = output.trimEnd().split("\n");
333
333
  const previewCount = expanded ? maxExpanded : maxCollapsed;
334
334
  for (const line of outputLines.slice(0, previewCount)) {
335
- lines.push(`${continuePrefix} ${theme.fg("dim", truncate(line, 70, theme.format.ellipsis))}`);
335
+ lines.push(`${continuePrefix} ${theme.fg("dim", truncateToWidth(line, 70))}`);
336
336
  }
337
337
 
338
338
  if (outputLines.length > previewCount) {
339
- lines.push(
340
- `${continuePrefix} ${theme.fg("dim", formatMoreItems(outputLines.length - previewCount, "line", theme))}`,
341
- );
339
+ lines.push(`${continuePrefix} ${theme.fg("dim", formatMoreItems(outputLines.length - previewCount, "line"))}`);
342
340
  }
343
341
 
344
342
  return lines;
345
343
  }
346
344
 
347
- function formatArgsInline(args: Record<string, string>, theme: Theme): string {
345
+ function formatArgsInline(args: Record<string, string>, _theme: Theme): string {
348
346
  const entries = Object.entries(args);
349
347
  if (entries.length === 0) return "No arguments";
350
348
 
@@ -352,11 +350,11 @@ function formatArgsInline(args: Record<string, string>, theme: Theme): string {
352
350
  if (entries.length === 1) {
353
351
  const [key, value] = entries[0];
354
352
  const humanKey = humanizeKey(key);
355
- const displayValue = `"${truncate(value, 32, theme.format.ellipsis)}"`;
353
+ const displayValue = `"${truncateToWidth(value, 32)}"`;
356
354
  return `${humanKey}: ${displayValue}`;
357
355
  }
358
356
 
359
- const pairs = entries.map(([key, value]) => `${key}=${truncate(value, 24, theme.format.ellipsis)}`);
357
+ const pairs = entries.map(([key, value]) => `${key}=${truncateToWidth(value, 24)}`);
360
358
  return `Args: ${pairs.join(", ")}`;
361
359
  }
362
360
 
@@ -365,12 +363,12 @@ function humanizeKey(key: string): string {
365
363
  return key.replace(/[-_]/g, " ").replace(/\b\w/g, c => c.toUpperCase());
366
364
  }
367
365
 
368
- function formatScalarInline(value: unknown, maxLen: number, theme: Theme): string {
366
+ function formatScalarInline(value: unknown, maxLen: number, _theme: Theme): string {
369
367
  if (value === null) return "null";
370
368
  if (value === undefined) return "undefined";
371
369
  if (typeof value === "boolean") return String(value);
372
370
  if (typeof value === "number") return String(value);
373
- if (typeof value === "string") return `"${truncate(value, maxLen, theme.format.ellipsis)}"`;
371
+ if (typeof value === "string") return `"${truncateToWidth(value, maxLen)}"`;
374
372
  if (Array.isArray(value)) return `[${value.length} items]`;
375
373
  if (typeof value === "object") {
376
374
  const keys = Object.keys(value);
@@ -391,7 +389,7 @@ function formatOutputInline(data: unknown, theme: Theme, maxWidth = 80): string
391
389
  if (Array.isArray(data)) {
392
390
  if (data.length === 0) return "Output: []";
393
391
  const preview = formatScalarInline(data[0], 40, theme);
394
- return `Output: [${data.length} items] ${preview}${data.length > 1 ? theme.format.ellipsis : ""}`;
392
+ return `Output: [${data.length} items] ${preview}${data.length > 1 ? "…" : ""}`;
395
393
  }
396
394
 
397
395
  // For objects, show key=value pairs inline
@@ -407,7 +405,7 @@ function formatOutputInline(data: unknown, theme: Theme, maxWidth = 80): string
407
405
  const addLen = pairs.length > 0 ? pairStr.length + 2 : pairStr.length; // +2 for ", "
408
406
 
409
407
  if (totalLen + addLen > maxWidth && pairs.length > 0) {
410
- pairs.push(theme.format.ellipsis);
408
+ pairs.push("…");
411
409
  break;
412
410
  }
413
411
 
@@ -442,7 +440,7 @@ function renderArgsSection(
442
440
  if (entries.length === 1) {
443
441
  const [key, value] = entries[0];
444
442
  const humanKey = humanizeKey(key);
445
- const displayValue = `"${truncate(value, 60, theme.format.ellipsis)}"`;
443
+ const displayValue = `"${truncateToWidth(value, 60)}"`;
446
444
  lines.push(`${continuePrefix}${theme.fg("dim", `${humanKey}: ${displayValue}`)}`);
447
445
  return lines;
448
446
  }
@@ -453,7 +451,7 @@ function renderArgsSection(
453
451
  lines.push(`${continuePrefix} ${line}`);
454
452
  }
455
453
  if (tree.truncated) {
456
- lines.push(`${continuePrefix} ${theme.fg("dim", theme.format.ellipsis)}`);
454
+ lines.push(`${continuePrefix} ${theme.fg("dim", "…")}`);
457
455
  }
458
456
 
459
457
  return lines;
@@ -531,7 +529,7 @@ function renderAgentProgress(
531
529
 
532
530
  if (progress.status === "running") {
533
531
  if (!description) {
534
- const taskPreview = truncate(progress.task, 40, theme.format.ellipsis);
532
+ const taskPreview = truncateToWidth(progress.task, 40);
535
533
  statusLine += ` ${theme.fg("muted", taskPreview)}`;
536
534
  }
537
535
  statusLine += `${theme.sep.dot}${theme.fg("dim", `${progress.toolCount} tools`)}`;
@@ -552,7 +550,7 @@ function renderAgentProgress(
552
550
  if (progress.currentTool) {
553
551
  let toolLine = `${continuePrefix}${theme.tree.hook} ${theme.fg("muted", progress.currentTool)}`;
554
552
  if (progress.currentToolArgs) {
555
- toolLine += `: ${theme.fg("dim", truncate(progress.currentToolArgs, 40, theme.format.ellipsis))}`;
553
+ toolLine += `: ${theme.fg("dim", truncateToWidth(progress.currentToolArgs, 40))}`;
556
554
  }
557
555
  if (progress.currentToolStartMs) {
558
556
  const elapsed = Date.now() - progress.currentToolStartMs;
@@ -566,7 +564,7 @@ function renderAgentProgress(
566
564
  const recent = progress.recentTools[0];
567
565
  let toolLine = `${continuePrefix}${theme.tree.hook} ${theme.fg("dim", recent.tool)}`;
568
566
  if (recent.args) {
569
- toolLine += `: ${theme.fg("dim", truncate(recent.args, 40, theme.format.ellipsis))}`;
567
+ toolLine += `: ${theme.fg("dim", truncateToWidth(recent.args, 40))}`;
570
568
  }
571
569
  lines.push(toolLine);
572
570
  }
@@ -612,7 +610,7 @@ function renderAgentProgress(
612
610
  lines.push(
613
611
  `${continuePrefix}${theme.fg(
614
612
  "dim",
615
- formatMoreItems((dataArray as unknown[]).length - displayCount, "item", theme),
613
+ formatMoreItems((dataArray as unknown[]).length - displayCount, "item"),
616
614
  )}`,
617
615
  );
618
616
  }
@@ -661,7 +659,7 @@ function renderReviewResult(
661
659
  }
662
660
  } else {
663
661
  // Preview: first sentence or ~100 chars
664
- const preview = truncate(`${summary.explanation.split(/[.!?]/)[0]}.`, 100, theme.format.ellipsis);
662
+ const preview = truncateToWidth(`${summary.explanation.split(/[.!?]/)[0]}.`, 100);
665
663
  lines.push(`${continuePrefix}${theme.fg("dim", preview)}`);
666
664
  }
667
665
  }
@@ -718,7 +716,7 @@ function renderFindings(
718
716
  }
719
717
 
720
718
  if (!expanded && findings.length > 3) {
721
- lines.push(`${continuePrefix}${theme.fg("dim", formatMoreItems(findings.length - 3, "finding", theme))}`);
719
+ lines.push(`${continuePrefix}${theme.fg("dim", formatMoreItems(findings.length - 3, "finding"))}`);
722
720
  }
723
721
 
724
722
  return lines;
@@ -830,7 +828,7 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
830
828
  lines.push(
831
829
  `${continuePrefix}${theme.fg("warning", theme.status.warning)} ${theme.fg(
832
830
  "dim",
833
- truncate(missingCompleteWarning, 80, theme.format.ellipsis),
831
+ truncateToWidth(missingCompleteWarning, 80),
834
832
  )}`,
835
833
  );
836
834
  }
@@ -848,7 +846,7 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
848
846
 
849
847
  // Error message
850
848
  if (result.error && !success) {
851
- lines.push(`${continuePrefix}${theme.fg("error", truncate(result.error, 70, theme.format.ellipsis))}`);
849
+ lines.push(`${continuePrefix}${theme.fg("error", truncateToWidth(result.error, 70))}`);
852
850
  }
853
851
 
854
852
  return lines;
@@ -869,7 +867,7 @@ export function renderResult(
869
867
  if (!details) {
870
868
  // Fallback to simple text
871
869
  const text = result.content.find(c => c.type === "text")?.text || "";
872
- return new Text(theme.fg("dim", truncate(text, 100, theme.format.ellipsis)), 0, 0);
870
+ return new Text(theme.fg("dim", truncateToWidth(text, 100)), 0, 0);
873
871
  }
874
872
 
875
873
  const lines: string[] = [];
@@ -911,7 +909,7 @@ export function renderResult(
911
909
 
912
910
  if (lines.length === 0) {
913
911
  const text = fallbackText.trim() ? fallbackText : "No results";
914
- return new Text(theme.fg("dim", truncate(text, 140, theme.format.ellipsis)), 0, 0);
912
+ return new Text(theme.fg("dim", truncateToWidth(text, 140)), 0, 0);
915
913
  }
916
914
 
917
915
  if (fallbackText.trim()) {
package/src/tools/bash.ts CHANGED
@@ -182,8 +182,8 @@ interface BashRenderContext {
182
182
  timeout?: number;
183
183
  }
184
184
 
185
- function formatBashCommand(args: BashRenderArgs, uiTheme: Theme): string {
186
- const command = args.command || uiTheme.format.ellipsis;
185
+ function formatBashCommand(args: BashRenderArgs, _uiTheme: Theme): string {
186
+ const command = args.command || "…";
187
187
  const prompt = "$";
188
188
  const cwd = process.cwd();
189
189
  let displayWorkdir = args.cwd;
@@ -285,7 +285,7 @@ export const bashToolRenderer = {
285
285
  outputLines.push(
286
286
  uiTheme.fg(
287
287
  "dim",
288
- `${uiTheme.format.ellipsis} (${result.skippedCount} earlier lines, showing ${result.visualLines.length} of ${result.skippedCount + result.visualLines.length}) (ctrl+o to expand)`,
288
+ `… (${result.skippedCount} earlier lines, showing ${result.visualLines.length} of ${result.skippedCount + result.visualLines.length}) (ctrl+o to expand)`,
289
289
  ),
290
290
  );
291
291
  }
@@ -15,7 +15,7 @@ import {
15
15
  formatErrorMessage,
16
16
  PREVIEW_LIMITS,
17
17
  TRUNCATE_LENGTHS,
18
- truncate,
18
+ truncateToWidth,
19
19
  } from "./render-utils";
20
20
 
21
21
  // =============================================================================
@@ -454,7 +454,7 @@ export const calculatorToolRenderer = {
454
454
  renderCall(args: CalculatorRenderArgs, uiTheme: Theme): Component {
455
455
  const count = args.calculations?.length ?? 0;
456
456
  const firstExpression = args.calculations?.[0]?.expression;
457
- const description = firstExpression ? truncate(firstExpression, TRUNCATE_LENGTHS.TITLE, "...") : undefined;
457
+ const description = firstExpression ? truncateToWidth(firstExpression, TRUNCATE_LENGTHS.TITLE) : undefined;
458
458
  const meta = count > 0 ? [formatCount("calc", count)] : [];
459
459
  const text = renderStatusLine({ icon: "pending", title: "Calc", description, meta }, uiTheme);
460
460
  return new Text(text, 0, 0);
@@ -495,7 +495,7 @@ export const calculatorToolRenderer = {
495
495
  }
496
496
 
497
497
  const description = args?.calculations?.[0]?.expression
498
- ? truncate(args.calculations[0].expression, TRUNCATE_LENGTHS.TITLE, "...")
498
+ ? truncateToWidth(args.calculations[0].expression, TRUNCATE_LENGTHS.TITLE)
499
499
  : undefined;
500
500
  const header = renderStatusLine(
501
501
  { icon: "success", title: "Calc", description, meta: [formatCount("result", outputs.length)] },
@@ -967,7 +967,7 @@ export function renderFetchCall(
967
967
  uiTheme: Theme = theme,
968
968
  ): Component {
969
969
  const domain = getDomain(args.url);
970
- const path = truncate(args.url.replace(/^https?:\/\/[^/]+/, ""), 50, uiTheme.format.ellipsis);
970
+ const path = truncate(args.url.replace(/^https?:\/\/[^/]+/, ""), 50, "…");
971
971
  const description = `${domain}${path ? ` ${path}` : ""}`.trim();
972
972
  const meta: string[] = [];
973
973
  if (args.raw) meta.push("raw");
@@ -990,7 +990,7 @@ export function renderFetchResult(
990
990
  }
991
991
 
992
992
  const domain = getDomain(details.finalUrl);
993
- const path = truncate(details.finalUrl.replace(/^https?:\/\/[^/]+/, ""), 50, uiTheme.format.ellipsis);
993
+ const path = truncate(details.finalUrl.replace(/^https?:\/\/[^/]+/, ""), 50, "…");
994
994
  const hasRedirect = details.url !== details.finalUrl;
995
995
  const hasNotes = details.notes.length > 0;
996
996
  const truncation = details.meta?.truncation;
@@ -1035,15 +1035,13 @@ export function renderFetchResult(
1035
1035
 
1036
1036
  const previewLimit = expanded ? 12 : 3;
1037
1037
  const previewList = applyListLimit(contentLines, { headLimit: previewLimit });
1038
- const previewLines = previewList.items.map(line => truncate(line.trimEnd(), 120, uiTheme.format.ellipsis));
1038
+ const previewLines = previewList.items.map(line => truncate(line.trimEnd(), 120, "…"));
1039
1039
  const remaining = Math.max(0, contentLines.length - previewLines.length);
1040
1040
  const contentPreviewLines =
1041
1041
  previewLines.length > 0 ? previewLines.map(line => uiTheme.fg("dim", line)) : [uiTheme.fg("dim", "(no content)")];
1042
1042
  if (remaining > 0) {
1043
1043
  const hint = formatExpandHint(uiTheme, expanded, true);
1044
- contentPreviewLines.push(
1045
- uiTheme.fg("muted", `${uiTheme.format.ellipsis} ${remaining} more lines${hint ? ` ${hint}` : ""}`),
1046
- );
1044
+ contentPreviewLines.push(uiTheme.fg("muted", `… ${remaining} more lines${hint ? ` ${hint}` : ""}`));
1047
1045
  }
1048
1046
 
1049
1047
  return {
@@ -2,7 +2,7 @@ import * as path from "node:path";
2
2
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
3
3
  import type { ImageContent } from "@oh-my-pi/pi-ai";
4
4
  import type { Component } from "@oh-my-pi/pi-tui";
5
- import { Text, truncateToWidth } from "@oh-my-pi/pi-tui";
5
+ import { Text } from "@oh-my-pi/pi-tui";
6
6
  import { type Static, Type } from "@sinclair/typebox";
7
7
  import { renderPromptTemplate } from "../config/prompt-templates";
8
8
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
@@ -17,7 +17,7 @@ import type { ToolSession } from ".";
17
17
  import type { OutputMeta } from "./output-meta";
18
18
  import { allocateOutputArtifact, createTailBuffer } from "./output-utils";
19
19
  import { resolveToCwd } from "./path-utils";
20
- import { shortenPath, ToolUIKit, truncate } from "./render-utils";
20
+ import { shortenPath, ToolUIKit, truncateToWidth } from "./render-utils";
21
21
  import { ToolAbortError, ToolError } from "./tool-errors";
22
22
  import { toolResult } from "./tool-result";
23
23
  import { DEFAULT_MAX_BYTES } from "./truncate";
@@ -494,7 +494,7 @@ function formatStatusEvent(event: PythonStatusEvent, theme: Theme): string {
494
494
  case "find":
495
495
  case "glob":
496
496
  parts.push(`${data.count} match${(data.count as number) !== 1 ? "es" : ""}`);
497
- if (data.pattern) parts.push(`for "${truncate(String(data.pattern), 20, theme.format.ellipsis)}"`);
497
+ if (data.pattern) parts.push(`for "${truncateToWidth(String(data.pattern), 20)}"`);
498
498
  break;
499
499
  case "grep":
500
500
  parts.push(`${data.count} match${(data.count as number) !== 1 ? "es" : ""}`);
@@ -502,16 +502,16 @@ function formatStatusEvent(event: PythonStatusEvent, theme: Theme): string {
502
502
  break;
503
503
  case "rgrep":
504
504
  parts.push(`${data.count} match${(data.count as number) !== 1 ? "es" : ""}`);
505
- if (data.pattern) parts.push(`for "${truncate(String(data.pattern), 20, theme.format.ellipsis)}"`);
505
+ if (data.pattern) parts.push(`for "${truncateToWidth(String(data.pattern), 20)}"`);
506
506
  break;
507
507
  case "ls":
508
508
  parts.push(`${data.count} entr${(data.count as number) !== 1 ? "ies" : "y"}`);
509
509
  break;
510
510
  case "env":
511
511
  if (data.action === "set") {
512
- parts.push(`set ${data.key}=${truncate(String(data.value ?? ""), 30, theme.format.ellipsis)}`);
512
+ parts.push(`set ${data.key}=${truncateToWidth(String(data.value ?? ""), 30)}`);
513
513
  } else if (data.action === "get") {
514
- parts.push(`${data.key}=${truncate(String(data.value ?? ""), 30, theme.format.ellipsis)}`);
514
+ parts.push(`${data.key}=${truncateToWidth(String(data.value ?? ""), 30)}`);
515
515
  } else {
516
516
  parts.push(`${data.count} variable${(data.count as number) !== 1 ? "s" : ""}`);
517
517
  }
@@ -617,7 +617,7 @@ function formatStatusEventExpanded(event: PythonStatusEvent, theme: Theme): stri
617
617
  lines.push(` ${theme.fg("dim", formatter(arr[i]))}`);
618
618
  }
619
619
  if (arr.length > max) {
620
- lines.push(` ${theme.fg("dim", `${theme.format.ellipsis} ${arr.length - max} more`)}`);
620
+ lines.push(` ${theme.fg("dim", `… ${arr.length - max} more`)}`);
621
621
  }
622
622
  };
623
623
 
@@ -625,11 +625,11 @@ function formatStatusEventExpanded(event: PythonStatusEvent, theme: Theme): stri
625
625
  const addPreview = (preview: string, maxLines = 3) => {
626
626
  const previewLines = String(preview).split("\n").slice(0, maxLines);
627
627
  for (const line of previewLines) {
628
- lines.push(` ${theme.fg("toolOutput", truncate(line, 80, theme.format.ellipsis))}`);
628
+ lines.push(` ${theme.fg("toolOutput", truncateToWidth(line, 80))}`);
629
629
  }
630
630
  const totalLines = String(preview).split("\n").length;
631
631
  if (totalLines > maxLines) {
632
- lines.push(` ${theme.fg("dim", `${theme.format.ellipsis} ${totalLines - maxLines} more lines`)}`);
632
+ lines.push(` ${theme.fg("dim", `… ${totalLines - maxLines} more lines`)}`);
633
633
  }
634
634
  };
635
635
 
@@ -645,7 +645,7 @@ function formatStatusEventExpanded(event: PythonStatusEvent, theme: Theme): stri
645
645
  if (data.hits) {
646
646
  addItems(data.hits as unknown[], h => {
647
647
  const hit = h as { line: number; text: string };
648
- return `${hit.line}: ${truncate(hit.text, 60, theme.format.ellipsis)}`;
648
+ return `${hit.line}: ${truncateToWidth(hit.text, 60)}`;
649
649
  });
650
650
  }
651
651
  break;
@@ -653,7 +653,7 @@ function formatStatusEventExpanded(event: PythonStatusEvent, theme: Theme): stri
653
653
  if (data.hits) {
654
654
  addItems(data.hits as unknown[], h => {
655
655
  const hit = h as { file: string; line: number; text: string };
656
- return `${shortenPath(hit.file)}:${hit.line}: ${truncate(hit.text, 50, theme.format.ellipsis)}`;
656
+ return `${shortenPath(hit.file)}:${hit.line}: ${truncateToWidth(hit.text, 50)}`;
657
657
  });
658
658
  }
659
659
  break;
@@ -672,7 +672,7 @@ function formatStatusEventExpanded(event: PythonStatusEvent, theme: Theme): stri
672
672
  if (data.entries) {
673
673
  addItems(data.entries as unknown[], e => {
674
674
  const entry = e as { sha: string; subject: string };
675
- return `${entry.sha} ${truncate(entry.subject, 50, theme.format.ellipsis)}`;
675
+ return `${entry.sha} ${truncateToWidth(entry.subject, 50)}`;
676
676
  });
677
677
  }
678
678
  break;
@@ -725,13 +725,9 @@ function renderStatusEvents(events: PythonStatusEvent[], theme: Theme, expanded:
725
725
  }
726
726
 
727
727
  if (!expanded && events.length > maxCollapsed) {
728
- lines.push(
729
- `${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", `${theme.format.ellipsis} ${events.length - maxCollapsed} more`)}`,
730
- );
728
+ lines.push(`${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", `… ${events.length - maxCollapsed} more`)}`);
731
729
  } else if (expanded && events.length > maxExpanded) {
732
- lines.push(
733
- `${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", `${theme.format.ellipsis} ${events.length - maxExpanded} more`)}`,
734
- );
730
+ lines.push(`${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", `… ${events.length - maxExpanded} more`)}`);
735
731
  }
736
732
 
737
733
  return lines;
@@ -781,7 +777,7 @@ export const pythonToolRenderer = {
781
777
  if (cells.length === 0) {
782
778
  const prompt = uiTheme.fg("accent", ">>>");
783
779
  const prefix = workdirLabel ? `${uiTheme.fg("dim", `${workdirLabel} && `)}` : "";
784
- const text = ui.title(`${prompt} ${prefix}${uiTheme.format.ellipsis}`);
780
+ const text = ui.title(`${prompt} ${prefix}…`);
785
781
  return new Text(text, 0, 0);
786
782
  }
787
783
 
@@ -896,10 +892,7 @@ export const pythonToolRenderer = {
896
892
  const outputLines = [...outputContent.lines];
897
893
  if (!expanded && outputContent.hiddenCount > 0) {
898
894
  outputLines.push(
899
- uiTheme.fg(
900
- "dim",
901
- `${uiTheme.format.ellipsis} ${outputContent.hiddenCount} more lines (ctrl+o to expand)`,
902
- ),
895
+ uiTheme.fg("dim", `… ${outputContent.hiddenCount} more lines (ctrl+o to expand)`),
903
896
  );
904
897
  }
905
898
  if (statusLines.length > 0) {
@@ -1007,24 +1000,22 @@ export const pythonToolRenderer = {
1007
1000
  outputLines.push("");
1008
1001
  const skippedLine = uiTheme.fg(
1009
1002
  "dim",
1010
- `${uiTheme.format.ellipsis} (${cachedSkipped} earlier lines, showing ${cachedLines.length} of ${cachedSkipped + cachedLines.length}) (ctrl+o to expand)`,
1003
+ `… (${cachedSkipped} earlier lines, showing ${cachedLines.length} of ${cachedSkipped + cachedLines.length}) (ctrl+o to expand)`,
1011
1004
  );
1012
- outputLines.push(truncateToWidth(skippedLine, width, uiTheme.fg("dim", uiTheme.format.ellipsis)));
1005
+ outputLines.push(truncateToWidth(skippedLine, width));
1013
1006
  }
1014
1007
  outputLines.push(...cachedLines);
1015
1008
  if (statusLines.length > 0) {
1016
- outputLines.push(
1017
- truncateToWidth(uiTheme.fg("dim", "Status"), width, uiTheme.fg("dim", uiTheme.format.ellipsis)),
1018
- );
1009
+ outputLines.push(truncateToWidth(uiTheme.fg("dim", "Status"), width));
1019
1010
  for (const statusLine of statusLines) {
1020
- outputLines.push(truncateToWidth(statusLine, width, uiTheme.fg("dim", uiTheme.format.ellipsis)));
1011
+ outputLines.push(truncateToWidth(statusLine, width));
1021
1012
  }
1022
1013
  }
1023
1014
  if (timeoutLine) {
1024
- outputLines.push(truncateToWidth(timeoutLine, width, uiTheme.fg("dim", uiTheme.format.ellipsis)));
1015
+ outputLines.push(truncateToWidth(timeoutLine, width));
1025
1016
  }
1026
1017
  if (warningLine) {
1027
- outputLines.push(truncateToWidth(warningLine, width, uiTheme.fg("warning", uiTheme.format.ellipsis)));
1018
+ outputLines.push(truncateToWidth(warningLine, width));
1028
1019
  }
1029
1020
  return outputLines;
1030
1021
  },