pi-pretty-tui 0.3.3 → 0.3.5

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.
@@ -0,0 +1,106 @@
1
+ import { getLanguageFromPath, highlightCode } from "@earendil-works/pi-coding-agent";
2
+ import { mixColors, sliceByColumn, visibleWidth } from "@earendil-works/pi-tui";
3
+
4
+ type Range = [number, number];
5
+ type Row = { marker: string; number?: string; text: string; styled: string; changes: Range[] };
6
+ const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
7
+ const graphemes = (text: string) => Array.from(segmenter.segment(text), part => part.segment);
8
+
9
+ // Bounded grapheme LCS: long/unrelated lines fall back to highlighting their
10
+ // changed middle. Neither path splits emoji, combining marks or surrogate pairs.
11
+ function changedRanges(before: string, after: string): [Range[], Range[]] {
12
+ const a = graphemes(before), b = graphemes(after);
13
+ let start = 0;
14
+ while (start < a.length && start < b.length && a[start] === b[start]) start++;
15
+ let endA = a.length, endB = b.length;
16
+ while (endA > start && endB > start && a[endA - 1] === b[endB - 1]) { endA--; endB--; }
17
+ const removed: Range[] = [], added: Range[] = [];
18
+ const range = (parts: string[], from: number, to: number): Range =>
19
+ [visibleWidth(parts.slice(0, from).join("")), visibleWidth(parts.slice(0, to).join(""))];
20
+ const n = endA - start, m = endB - start;
21
+ if (!n || !m || n * m > 65536) {
22
+ if (n) removed.push(range(a, start, endA));
23
+ if (m) added.push(range(b, start, endB));
24
+ return [removed, added];
25
+ }
26
+ const matrix = Array.from({ length: n + 1 }, () => new Uint32Array(m + 1));
27
+ for (let i = n - 1; i >= 0; i--) for (let j = m - 1; j >= 0; j--)
28
+ matrix[i][j] = a[start + i] === b[start + j] ? matrix[i + 1][j + 1] + 1 : Math.max(matrix[i + 1][j], matrix[i][j + 1]);
29
+ let i = 0, j = 0;
30
+ while (i < n || j < m) {
31
+ if (i < n && j < m && a[start + i] === b[start + j]) { i++; j++; }
32
+ else if (i < n && (j === m || matrix[i + 1][j] >= matrix[i][j + 1])) {
33
+ removed.push(range(a, start + i, start + i + 1));
34
+ i++;
35
+ } else {
36
+ added.push(range(b, start + j, start + j + 1));
37
+ j++;
38
+ }
39
+ }
40
+ const merge = (ranges: Range[]) => ranges.reduce<Range[]>((result, next) => {
41
+ const previous = result[result.length - 1];
42
+ if (previous && previous[1] === next[0]) previous[1] = next[1];
43
+ else result.push(next);
44
+ return result;
45
+ }, []);
46
+ return [merge(removed), merge(added)];
47
+ }
48
+
49
+ export function editDiffLayout(theme: any, diff: string, path?: string) {
50
+ const rows: Row[] = diff.split("\n").map(line => {
51
+ const match = /^([ +\-]) *(\d+) (.*)$/.exec(line);
52
+ const marker = match?.[1] ?? (line.startsWith("+") && !line.startsWith("+++") ? "+" : line.startsWith("-") && !line.startsWith("---") ? "-" : " ");
53
+ const text = (match ? match[3] : /^\s+\.\.\.$/.test(line) ? "..." : line).replace(/\t/g, " ");
54
+ return { marker, number: match?.[2], text, styled: text, changes: [] };
55
+ });
56
+ // Highlight old/new snippets separately so removed code cannot corrupt the
57
+ // syntax state of the new side. Keep all highlighting in the host's theme.
58
+ const language = path ? getLanguageFromPath(path) : undefined;
59
+ for (const excluded of ["+", "-"]) {
60
+ const side = rows.filter(row => row.marker !== excluded);
61
+ let styled: string[];
62
+ try { styled = highlightCode(side.map(row => row.text).join("\n"), language); }
63
+ catch { styled = side.map(row => row.text); }
64
+ side.forEach((row, index) => {
65
+ if (row.marker !== " " || excluded === "+") row.styled = styled[index] ?? row.text;
66
+ });
67
+ }
68
+ for (let index = 0; index < rows.length;) {
69
+ if (rows[index].marker !== "-") { index++; continue; }
70
+ const removed: Row[] = [], added: Row[] = [];
71
+ while (rows[index]?.marker === "-") removed.push(rows[index++]);
72
+ while (rows[index]?.marker === "+") added.push(rows[index++]);
73
+ // Do not guess a line pairing when a replacement changes the line count.
74
+ if (removed.length === added.length) removed.forEach((row, i) => {
75
+ [row.changes, added[i].changes] = changedRanges(row.text, added[i].text);
76
+ });
77
+ }
78
+ const numberWidth = rows.reduce((width, row) => Math.max(width, row.number?.length ?? 0), 1);
79
+ let index = 0;
80
+ return (_line: string) => {
81
+ const row = rows[index++];
82
+ const color = row.marker === "+" ? "toolDiffAdded" : row.marker === "-" ? "toolDiffRemoved" : "toolDiffContext";
83
+ const background = row.marker === "+" ? "toolSuccessBg" : row.marker === "-" ? "toolErrorBg" : undefined;
84
+ const paint = (text: string) => background && theme.bg ? theme.bg(background, text) : text;
85
+ const changeBackground = background && theme.colors?.[background] && theme.colors?.[color]
86
+ ? mixColors(theme.colors[background], theme.colors[color], 0.3) : undefined;
87
+ const width = visibleWidth(row.text);
88
+ let content = "", cursor = 0;
89
+ for (const [start, end] of row.changes) {
90
+ content += sliceByColumn(row.styled, cursor, start - cursor);
91
+ const changed = sliceByColumn(row.styled, start, end - start);
92
+ // A stronger theme-derived background preserves syntax foregrounds and
93
+ // avoids adding font emphasis to the changed graphemes.
94
+ content += changeBackground && theme.style ? theme.style(changed, { bg: changeBackground }) : paint(changed);
95
+ cursor = end;
96
+ }
97
+ content += sliceByColumn(row.styled, cursor, Math.max(0, width - cursor));
98
+ const gutter = row.number ? `${row.number.padStart(numberWidth)} ${row.marker} ` : " ".repeat(numberWidth + 3);
99
+ return {
100
+ prefix: paint(theme.fg(color, gutter)),
101
+ continuation: paint(" ".repeat(numberWidth + 3)),
102
+ content,
103
+ background: background ? paint : undefined,
104
+ };
105
+ };
106
+ }
@@ -8,6 +8,7 @@ import {
8
8
  type InlineToken,
9
9
  } from "./markdown-emphasis.js";
10
10
  import { gapRowsAfterBlock, renderSpacedList } from "./markdown-spacing.js";
11
+ import { editDiffLayout } from "./edit-diff.js";
11
12
  import { ThoughtPresentation } from "./thought-presentation.js";
12
13
  import { isThirdPartyTool, renderToolProjection, THIRD_PARTY_TOOL, toolLabel, type ToolProjection } from "./tool-presentation.js";
13
14
  import { NestedTools, NESTED_SNAPSHOT_ENTRY } from "./nested-tools.js";
@@ -36,12 +37,15 @@ import {
36
37
  copyToClipboard,
37
38
  getAgentDir,
38
39
  getMarkdownTheme,
40
+ getLanguageFromPath,
41
+ highlightCode,
39
42
  type ExtensionAPI,
40
43
  } from "@earendil-works/pi-coding-agent";
41
44
  import {
42
45
  type Component,
43
46
  Markdown,
44
47
  stripTerminalSequences,
48
+ sliceByColumn,
45
49
  truncateToWidth,
46
50
  TuiAltScreen,
47
51
  type TuiMouseEvent,
@@ -196,14 +200,14 @@ export default function prettyTui(pi: ExtensionAPI) {
196
200
  let latestActivityStatusGeneration = 0;
197
201
  const renderLatestActivityStatus = (status: LatestActivityStatus, width: number, theme: any): string => {
198
202
  const severity = status.severity ?? "status";
199
- const icon = severity === "error" ? "✕" : severity === "success" ? "✓" : "◇";
203
+ const icon = severity === "error" ? "✕ " : severity === "success" ? "✓ " : "";
200
204
  const color = severity === "error"
201
205
  ? "error"
202
206
  : severity === "success"
203
207
  ? "success"
204
208
  : "dim";
205
209
  const detail = status.detail?.trim() ? ` — ${status.detail.trim()}` : "";
206
- return truncateToWidth(` ${theme.fg(color, `${icon} ${status.title}`)}${theme.fg("dim", detail)}`, Math.max(1, width), "…");
210
+ return truncateToWidth(` ${theme.fg(color, `${icon}${status.title}`)}${theme.fg("dim", detail)}`, Math.max(1, width), "…");
207
211
  };
208
212
  const refreshLatestActivityWidget = () => {
209
213
  if (!currentExtensionUi?.setWidget) return;
@@ -642,7 +646,7 @@ export default function prettyTui(pi: ExtensionAPI) {
642
646
  );
643
647
  };
644
648
 
645
- const renderStandaloneError = (member: ActivityMember, width: number, component?: any): string[] => {
649
+ const renderStandaloneAlert = (member: ActivityMember, width: number, component?: any): string[] => {
646
650
  const run = component && renderMode !== "full" ? errorRun(component) : { hidden: false, count: 1, compact: false };
647
651
  if (component) errorDisclosureRows.delete(component);
648
652
  if (run.hidden) return [];
@@ -656,7 +660,9 @@ export default function prettyTui(pi: ExtensionAPI) {
656
660
  const prefix = heading + (content ? theme.fg("dim", " · ") : "");
657
661
  const text = prefix + theme.fg("muted", content) + (count > 1 ? theme.fg("dim", component && expandedErrorRuns.has(component) ? " [Collapse]" : ` [×${count}]`) : "");
658
662
  // Wrap rather than truncate; all original detail lines remain accessible.
659
- const continuation = " ".repeat(Math.min(visibleWidth(prefix), Math.max(0, Math.floor(width / 3))));
663
+ // A single-line notification can be entirely its title. Indenting by
664
+ // that title's length wastes most of the viewport on every continuation.
665
+ const continuation = " ".repeat(Math.min(2, Math.max(0, width - 1)));
660
666
  const rows: string[] = [];
661
667
  for (const line of text.split("\n")) {
662
668
  const wrapped = wrapTextWithAnsi(line, Math.max(1, width - visibleWidth(continuation)));
@@ -686,8 +692,8 @@ export default function prettyTui(pi: ExtensionAPI) {
686
692
  };
687
693
 
688
694
  const renderPendingActivityUpdate = (member: ActivityMember, width: number, component?: any): string[] => {
689
- if (member.updateSeverity === "error" && member.updateTitle !== "Response truncated") {
690
- return renderStandaloneError(member, width, component);
695
+ if (member.updateSeverity === "error" || member.updateSeverity === "warning") {
696
+ return renderStandaloneAlert(member, width, component);
691
697
  }
692
698
  const theme = defaultActivityTheme();
693
699
  const title = truncateToWidth(member.updateTitle ?? "Update", Math.max(1, width - 2), "…");
@@ -2797,6 +2803,8 @@ export default function prettyTui(pi: ExtensionAPI) {
2797
2803
  prefix: DisplayValue;
2798
2804
  continuation?: DisplayValue;
2799
2805
  content: DisplayValue;
2806
+ singleLine?: boolean;
2807
+ background?: (text: string) => string;
2800
2808
  };
2801
2809
 
2802
2810
  /**
@@ -2810,14 +2818,35 @@ export default function prettyTui(pi: ExtensionAPI) {
2810
2818
  for (const row of rows) {
2811
2819
  const prefix = typeof row.prefix === "function" ? row.prefix() : row.prefix;
2812
2820
  const content = typeof row.content === "function" ? row.content() : row.content;
2821
+ if (row.singleLine) {
2822
+ const inline = content.replace(/\r\n|\r|\n/g, " ");
2823
+ rendered.push(truncateToWidth(prefix + inline, Math.max(1, width), "…"));
2824
+ continue;
2825
+ }
2813
2826
  const rawContinuation = row.continuation ?? " ".repeat(visibleWidth(prefix));
2814
2827
  const continuation = typeof rawContinuation === "function" ? rawContinuation() : rawContinuation;
2815
2828
  const prefixWidth = Math.max(visibleWidth(prefix), visibleWidth(continuation));
2816
2829
  const contentWidth = Math.max(1, width - prefixWidth);
2817
2830
  const wrapped = wrapTextWithAnsi(content || " ", contentWidth);
2818
2831
 
2819
- rendered.push(prefix + (wrapped[0] ?? ""));
2820
- for (const line of wrapped.slice(1)) rendered.push(continuation + line);
2832
+ const paint = (line: string) => row.background
2833
+ ? row.background(line + " ".repeat(Math.max(0, contentWidth - visibleWidth(line)))) : line;
2834
+ const corner = renderMode === "clean" ? stripTerminalSequences(prefix).indexOf("└") : -1;
2835
+ if (corner >= 0 && wrapped.length > 1) {
2836
+ // Close the branch on the final physical row, not the first row of
2837
+ // a logical source line that may wrap or contain explicit newlines.
2838
+ const column = visibleWidth(stripTerminalSequences(prefix).slice(0, corner));
2839
+ const guide = (symbol: string) => sliceByColumn(continuation, 0, column) +
2840
+ defaultActivityTheme().fg("dim", symbol) +
2841
+ sliceByColumn(continuation, column + 1, Math.max(0, visibleWidth(continuation) - column - 1));
2842
+ rendered.push(prefix.replace("└", "│") + paint(wrapped[0] ?? ""));
2843
+ for (let index = 1; index < wrapped.length; index++) {
2844
+ rendered.push(guide(index === wrapped.length - 1 ? "└" : "│") + paint(wrapped[index]));
2845
+ }
2846
+ } else {
2847
+ rendered.push(prefix + paint(wrapped[0] ?? ""));
2848
+ for (const line of wrapped.slice(1)) rendered.push(continuation + paint(line));
2849
+ }
2821
2850
  }
2822
2851
 
2823
2852
  return rendered;
@@ -2830,23 +2859,25 @@ export default function prettyTui(pi: ExtensionAPI) {
2830
2859
  context.state.compactToolStatus = status;
2831
2860
  };
2832
2861
 
2833
- const callRow = (theme: any, name: string, detail: string, state: any): DisplayRow => ({
2862
+ const callRow = (theme: any, name: string, detail: string, state: any, expanded = false): DisplayRow => ({
2863
+ singleLine: renderMode === "clean" && !expanded,
2834
2864
  prefix: () => {
2835
2865
  const status = (state.compactToolStatus ?? "running") as ToolStatus;
2836
2866
  const color = status === "success" ? "success" : status === "error" ? "error" : "dim";
2837
2867
  if (renderMode === "compact") return name === "bash" ? theme.fg("toolTitle", "$ ") : "";
2838
2868
  return theme.fg(color, "● ");
2839
2869
  },
2840
- continuation: renderMode === "compact" && name !== "bash" ? "" : " ",
2870
+ continuation: renderMode === "clean" && expanded ? theme.fg("dim", "│ ") :
2871
+ renderMode === "compact" && name !== "bash" ? "" : " ",
2841
2872
  content: renderMode === "compact" ? theme.fg("muted", detail) :
2842
2873
  theme.fg("text", theme.bold(name)) +
2843
2874
  theme.fg("dim", "(") +
2844
- theme.fg("muted", detail) +
2875
+ theme.fg(renderMode === "clean" && expanded ? "text" : "muted", detail) +
2845
2876
  theme.fg("dim", ")"),
2846
2877
  });
2847
2878
 
2848
- const call = (theme: any, name: string, detail: string, state: any) =>
2849
- block([callRow(theme, name, detail, state)]);
2879
+ const call = (theme: any, name: string, detail: string, state: any, expanded = false) =>
2880
+ block([callRow(theme, name, detail, state, expanded)]);
2850
2881
 
2851
2882
  const writeCall = (theme: any, name: string, path: string, content: string, expanded: boolean, state: any) => {
2852
2883
  const lines = content.replace(/\t/g, " ").split("\n");
@@ -2856,7 +2887,7 @@ export default function prettyTui(pi: ExtensionAPI) {
2856
2887
  const shown = lines.slice(0, expanded ? total : 10);
2857
2888
  const remaining = total - shown.length;
2858
2889
  const rows: DisplayRow[] = [
2859
- callRow(theme, name, `${path} · ${total} ${total === 1 ? "line" : "lines"}`, state),
2890
+ callRow(theme, name, `${path} · ${total} ${total === 1 ? "line" : "lines"}`, state, expanded),
2860
2891
  ];
2861
2892
 
2862
2893
  for (let index = 0; index < shown.length; index++) {
@@ -2879,28 +2910,6 @@ export default function prettyTui(pi: ExtensionAPI) {
2879
2910
  return block(rows);
2880
2911
  };
2881
2912
 
2882
- // Pi diffs encode the marker before the padded line number. Move it into
2883
- // its own column and keep the whole gutter out of content word wrapping.
2884
- const editDiffLayout = (theme: any, diff: string): ((line: string) => DisplayRow) => {
2885
- const parse = (line: string) => /^([ +\-]) *(\d+) (.*)$/.exec(line);
2886
- const numberWidth = diff.split("\n").reduce((width, line) => Math.max(width, parse(line)?.[2].length ?? 0), 1);
2887
- const gutterWidth = numberWidth + 3;
2888
- return (line) => {
2889
- const match = parse(line);
2890
- const marker = match?.[1] ?? (
2891
- line.startsWith("+") && !line.startsWith("+++") ? "+" :
2892
- line.startsWith("-") && !line.startsWith("---") ? "-" : " "
2893
- );
2894
- const color = marker === "+" ? "toolDiffAdded" : marker === "-" ? "toolDiffRemoved" : "toolDiffContext";
2895
- const gutter = match ? `${match[2].padStart(numberWidth)} ${marker} ` : " ".repeat(gutterWidth);
2896
- return {
2897
- prefix: theme.fg(color, gutter),
2898
- continuation: " ".repeat(gutterWidth),
2899
- content: theme.fg(color, match ? match[3] : /^\s+\.\.\.$/.test(line) ? "..." : line),
2900
- };
2901
- };
2902
- };
2903
-
2904
2913
  const result = (
2905
2914
  theme: any,
2906
2915
  summary: string,
@@ -2929,6 +2938,7 @@ export default function prettyTui(pi: ExtensionAPI) {
2929
2938
  prefix: theme.fg("dim", isLast ? " └ " : " │ ") + (layout?.prefix ?? ""),
2930
2939
  continuation: (isLast ? " " : theme.fg("dim", " │ ")) + (layout?.continuation ?? ""),
2931
2940
  content: layout?.content ?? outputStyle(shown[index] || " "),
2941
+ background: layout?.background,
2932
2942
  });
2933
2943
  }
2934
2944
  if (omitted > 0) {
@@ -3577,7 +3587,7 @@ export default function prettyTui(pi: ExtensionAPI) {
3577
3587
  const dimPhase = responseRunStartedAt !== undefined &&
3578
3588
  Math.floor(Math.max(0, Date.now() - responseRunStartedAt) / RUNNING_DOT_INTERVAL_MS) % 2 === 1;
3579
3589
  const color = currentActivity === "done"
3580
- ? collapsedDone ? "dim" : "success"
3590
+ ? collapsedDone ? "dim" : "text"
3581
3591
  : dimPhase ? "dim" : "accent";
3582
3592
  return currentActivity !== "done" && !dimPhase
3583
3593
  ? runningColor(theme, "● ")
@@ -3588,7 +3598,7 @@ export default function prettyTui(pi: ExtensionAPI) {
3588
3598
  const currentActivity = typeof activity === "function" ? activity() : activity;
3589
3599
  const label = currentActivity === "done" ? "Done" : "Running";
3590
3600
  const color = label === "Done"
3591
- ? collapsedDone ? "dim" : "success"
3601
+ ? collapsedDone ? "dim" : "text"
3592
3602
  : "accent";
3593
3603
  const detailColor = label === "Done" && collapsedDone ? "dim" : "text";
3594
3604
  const title = label === "Running"
@@ -4536,7 +4546,7 @@ export default function prettyTui(pi: ExtensionAPI) {
4536
4546
  renderCall(args: any, theme: any, context: any) {
4537
4547
  if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, readLabel, context.toolCallId);
4538
4548
  const range = readLineRange(args.offset, args.limit);
4539
- return call(theme, readLabel, `${args.path}${range}`, context.state);
4549
+ return call(theme, readLabel, `${args.path}${range}`, context.state, context.expanded);
4540
4550
  },
4541
4551
  renderResult(toolResult: any, options: any, theme: any, context: any) {
4542
4552
  const output = textContent(toolResult);
@@ -4572,9 +4582,9 @@ export default function prettyTui(pi: ExtensionAPI) {
4572
4582
  const detail = omitted > 0
4573
4583
  ? `${firstLine} … (${omitted} more ${omitted === 1 ? "line" : "lines"})`
4574
4584
  : firstLine;
4575
- return call(theme, bashLabel, detail, context.state);
4585
+ return call(theme, bashLabel, detail, context.state, context.expanded);
4576
4586
  }
4577
- return call(theme, bashLabel, command, context.state);
4587
+ return call(theme, bashLabel, command, context.state, context.expanded);
4578
4588
  },
4579
4589
  renderResult(toolResult: any, options: any, theme: any, context: any) {
4580
4590
  const output = textContent(toolResult);
@@ -4616,7 +4626,7 @@ export default function prettyTui(pi: ExtensionAPI) {
4616
4626
  renderCall(args: any, theme: any, context: any) {
4617
4627
  if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, editLabel, context.toolCallId);
4618
4628
  const count = Array.isArray(args.edits) ? ` · ${args.edits.length} changes` : "";
4619
- return call(theme, editLabel, `${args.path}${count}`, context.state);
4629
+ return call(theme, editLabel, `${args.path}${count}`, context.state, context.expanded);
4620
4630
  },
4621
4631
  renderResult(toolResult: any, options: any, theme: any, context: any) {
4622
4632
  const output = textContent(toolResult);
@@ -4646,7 +4656,7 @@ export default function prettyTui(pi: ExtensionAPI) {
4646
4656
  if (line.startsWith("-") && !line.startsWith("---")) return theme.fg("toolDiffRemoved", line);
4647
4657
  return theme.fg("toolDiffContext", line);
4648
4658
  },
4649
- !failed && diff ? editDiffLayout(theme, diff) : undefined,
4659
+ !failed && diff ? editDiffLayout(theme, diff, context.args?.path) : undefined,
4650
4660
  );
4651
4661
  },
4652
4662
  } as any);
@@ -4660,11 +4670,11 @@ export default function prettyTui(pi: ExtensionAPI) {
4660
4670
  if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, writeLabel, context.toolCallId);
4661
4671
  const content = typeof args.content === "string" ? args.content : "";
4662
4672
  const path = String(args.path ?? "");
4663
- if (useCompactToolView(context.toolCallId, context.expanded)) {
4673
+ if (renderMode === "clean" || useCompactToolView(context.toolCallId, context.expanded)) {
4664
4674
  const lines = content.replace(/\t/g, " ").split("\n");
4665
4675
  while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
4666
4676
  const total = lines.length;
4667
- return call(theme, writeLabel, `${path} · ${total} ${total === 1 ? "line" : "lines"}`, context.state);
4677
+ return call(theme, writeLabel, `${path} · ${total} ${total === 1 ? "line" : "lines"}`, context.state, context.expanded);
4668
4678
  }
4669
4679
  return writeCall(theme, writeLabel, path, content, context.expanded, context.state);
4670
4680
  },
@@ -4673,6 +4683,33 @@ export default function prettyTui(pi: ExtensionAPI) {
4673
4683
  if (hideCleanTool(context.toolCallId, options.expanded, context.executionStarted)) return hiddenToolResult(context, options, output);
4674
4684
  if (options.isPartial) return partialResult(context, theme, "Writing…");
4675
4685
  const failed = completeStatus(context);
4686
+ if (renderMode === "clean") {
4687
+ if (failed) return result(theme, "Write failed", output, options.expanded, true);
4688
+ const status = output ? `Written · ${output}` : "Written";
4689
+ const rows: DisplayRow[] = [{
4690
+ prefix: theme.fg("dim", "└ "), continuation: " ",
4691
+ content: theme.fg("toolOutput", status),
4692
+ }];
4693
+ if (options.expanded) {
4694
+ const content = typeof context.args?.content === "string" ? context.args.content : "";
4695
+ const source = content.replace(/\r\n|\r/g, "\n").replace(/\t/g, " ").split("\n");
4696
+ // A final newline terminates the last line; additional blank lines
4697
+ // are actual file content and retain their own line numbers.
4698
+ if (source.at(-1) === "") source.pop();
4699
+ let highlighted: string[];
4700
+ try { highlighted = highlightCode(source.join("\n"), getLanguageFromPath(String(context.args?.path ?? ""))); }
4701
+ catch { highlighted = source; }
4702
+ const digits = String(source.length).length;
4703
+ for (let index = 0; index < source.length; index++) {
4704
+ rows.push({
4705
+ prefix: theme.fg("dim", index === source.length - 1 ? " └ " : " │ ") + theme.fg("dim", `${String(index + 1).padStart(digits)} `),
4706
+ continuation: (index === source.length - 1 ? " " : theme.fg("dim", " │ ")) + " ".repeat(digits + 1),
4707
+ content: highlighted[index] ?? source[index],
4708
+ });
4709
+ }
4710
+ }
4711
+ return block(rows);
4712
+ }
4676
4713
  return result(
4677
4714
  theme,
4678
4715
  failed ? output.split("\n")[0] || "Write failed" : "Written",
@@ -4692,7 +4729,7 @@ export default function prettyTui(pi: ExtensionAPI) {
4692
4729
  if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, grepLabel, context.toolCallId);
4693
4730
  const where = args.path ? ` · ${args.path}` : "";
4694
4731
  const glob = args.glob ? ` · ${args.glob}` : "";
4695
- return call(theme, grepLabel, `${args.pattern}${where}${glob}`, context.state);
4732
+ return call(theme, grepLabel, `${args.pattern}${where}${glob}`, context.state, context.expanded);
4696
4733
  },
4697
4734
  renderResult(toolResult: any, options: any, theme: any, context: any) {
4698
4735
  const output = textContent(toolResult);
@@ -4711,7 +4748,7 @@ export default function prettyTui(pi: ExtensionAPI) {
4711
4748
  renderShell: "self",
4712
4749
  renderCall(args: any, theme: any, context: any) {
4713
4750
  if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, findLabel, context.toolCallId);
4714
- return call(theme, findLabel, `${args.pattern}${args.path ? ` · ${args.path}` : ""}`, context.state);
4751
+ return call(theme, findLabel, `${args.pattern}${args.path ? ` · ${args.path}` : ""}`, context.state, context.expanded);
4715
4752
  },
4716
4753
  renderResult(toolResult: any, options: any, theme: any, context: any) {
4717
4754
  const output = textContent(toolResult);
@@ -4730,7 +4767,7 @@ export default function prettyTui(pi: ExtensionAPI) {
4730
4767
  renderShell: "self",
4731
4768
  renderCall(args: any, theme: any, context: any) {
4732
4769
  if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, lsLabel, context.toolCallId);
4733
- return call(theme, lsLabel, args.path ?? ".", context.state);
4770
+ return call(theme, lsLabel, args.path ?? ".", context.state, context.expanded);
4734
4771
  },
4735
4772
  renderResult(toolResult: any, options: any, theme: any, context: any) {
4736
4773
  const output = textContent(toolResult);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-pretty-tui",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "A beautiful, polished TUI for the Pi coding agent",
5
5
  "license": "MIT",
6
6
  "author": "Kainan Yang <ykn0309@whu.edu.cn>",