pi-pretty-tui 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -38,6 +38,12 @@ Click activity rows, tool titles or thought entries to expand and collapse detai
38
38
 
39
39
  Settings persist across sessions. Disabling the extension restores Pi's native appearance.
40
40
 
41
+ ### Repeated errors
42
+
43
+ In clean and compact, adjacent standalone errors can share the first complete error and a `[×2]` / `[×3]` count even when trace, request or correlation IDs change. Source type, title and surrounding text must match exactly; only standalone hexadecimal IDs (16–128 characters containing a letter) or UUIDs may differ. Short codes, numeric quantities and prefixed IDs such as `msg_...` / `rs_...` remain exact.
44
+
45
+ Other transcript entries break the run; errors with thoughts, tool calls or visible answer text are not folded. In fullscreen, click the first error to expand or collapse the run. Expanded errors show every complete original error, aligned with the same prefix and without numbering; new retries preserve the expanded state. Session records remain unchanged. You can also use `/pretty-tui full` to see every occurrence and its original IDs.
46
+
41
47
  ## Copying
42
48
 
43
49
  Start Pi in **fullscreen mode** to use mouse controls:
package/README.zh-CN.md CHANGED
@@ -38,6 +38,12 @@ pi install npm:pi-pretty-tui
38
38
 
39
39
  设置会持久保存。禁用插件后恢复 Pi 原生外观。
40
40
 
41
+ ### 重复错误
42
+
43
+ clean 和 compact 中,相邻的独立错误即使 trace、request 或 correlation ID 不同,也可合并显示第一条完整错误及 `[×2]` / `[×3]` 次数。来源类型、标题和周围文本必须完全一致;仅独立的长十六进制 ID(16–128 位且含字母)或 UUID 可以变化。短错误码、纯数字值及 `msg_...` / `rs_...` 等带前缀的标识仍要求一致。
44
+
45
+ 其他会话条目会打断合并;带思考、工具调用或可见回答的错误不参与折叠。全屏模式下,点击第一条错误可展开或收起;展开后每条完整原始错误使用相同前缀、左侧对齐,不加序号,新追加的重复错误保留展开状态。会话记录保持不变,也可使用 `/pretty-tui full` 查看每次错误及原始 ID。
46
+
41
47
  ## 复制操作
42
48
 
43
49
  以 **fullscreen 全屏模式**启动 Pi,即可使用鼠标复制入口:
@@ -0,0 +1,103 @@
1
+ import { getLanguageFromPath, highlightCode } from "@earendil-works/pi-coding-agent";
2
+ import { 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 width = visibleWidth(row.text);
86
+ let content = "", cursor = 0;
87
+ for (const [start, end] of row.changes) {
88
+ content += sliceByColumn(row.styled, cursor, start - cursor);
89
+ const changed = sliceByColumn(row.styled, start, end - start);
90
+ // Underline + bold preserve syntax foreground colors, unlike inverse.
91
+ content += theme.underline ? theme.underline(theme.bold(changed)) : theme.bold(changed);
92
+ cursor = end;
93
+ }
94
+ content += sliceByColumn(row.styled, cursor, Math.max(0, width - cursor));
95
+ const gutter = row.number ? `${row.number.padStart(numberWidth)} ${row.marker} ` : " ".repeat(numberWidth + 3);
96
+ return {
97
+ prefix: paint(theme.fg(color, gutter)),
98
+ continuation: paint(" ".repeat(numberWidth + 3)),
99
+ content,
100
+ background: background ? paint : undefined,
101
+ };
102
+ };
103
+ }
@@ -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";
@@ -89,6 +90,24 @@ const loadConfig = (path: string): PrettyTuiConfig => {
89
90
  }
90
91
  };
91
92
 
93
+ /** Stable text affixes around opaque IDs, without fuzzy similarity chaining. */
94
+ const errorAffixKey = (text: string): string => {
95
+ const parts: (string | ["hex" | "uuid"])[] = [];
96
+ let cursor = 0;
97
+ // Whole tokens only: keep semantic identifiers such as msg_<hex> and
98
+ // rs_<hex>, short codes, ordinary words, and numeric quantities exact.
99
+ const opaqueId = /(?<![\w-])(?:[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}|[\da-f]{16,128})(?![\w-])/giu;
100
+ for (const match of text.matchAll(opaqueId)) {
101
+ const kind = match[0].includes("-") ? "uuid" : "hex";
102
+ if (kind === "hex" && !/[a-f]/iu.test(match[0])) continue;
103
+ parts.push(text.slice(cursor, match.index), [kind]);
104
+ cursor = match.index + match[0].length;
105
+ }
106
+ parts.push(text.slice(cursor));
107
+ // Typed slots cannot collide with a literal placeholder in an error.
108
+ return JSON.stringify(parts);
109
+ };
110
+
92
111
  /** Polished user messages, tool calls, and list rendering for Pi's TUI. */
93
112
  export default function prettyTui(pi: ExtensionAPI) {
94
113
  const cwd = process.cwd();
@@ -111,6 +130,21 @@ export default function prettyTui(pi: ExtensionAPI) {
111
130
  let currentPromptEditor: any;
112
131
  let errorChatContainer: any;
113
132
  const runtimeErrorDescriptors = new WeakMap<object, { source: string; title: string; content: string }>();
133
+ const expandedErrorRuns = new WeakSet<object>();
134
+ const errorDisclosureRows = new WeakMap<object, { start: number; end: number }>();
135
+ const errorDisclosureHit = (component: object, event: any) => {
136
+ const rows = errorDisclosureRows.get(component);
137
+ return renderMode !== "full" && Boolean(rows && event.y >= rows.start && event.y < rows.end);
138
+ };
139
+ const handleErrorDisclosureMouse = (component: object, event: any) => {
140
+ if (event.button !== "left" || !errorDisclosureHit(component, event)) return undefined;
141
+ if (event.type === "press") return { handled: true };
142
+ if (event.type !== "click") return undefined;
143
+ if (expandedErrorRuns.has(component)) expandedErrorRuns.delete(component);
144
+ else expandedErrorRuns.add(component);
145
+ currentTui?.requestRender?.();
146
+ return { handled: true };
147
+ };
114
148
  const errorDescriptor = (component: any) => {
115
149
  const runtime = runtimeErrorDescriptors.get(component);
116
150
  if (runtime) return runtime;
@@ -124,15 +158,21 @@ export default function prettyTui(pi: ExtensionAPI) {
124
158
  const children: any[] = errorChatContainer?.children ?? [];
125
159
  const index = children.indexOf(component);
126
160
  const descriptor = errorDescriptor(component);
161
+ const key = descriptor && errorAffixKey(descriptor.content);
127
162
  const same = (other: any) => {
128
163
  const value = errorDescriptor(other);
129
164
  return descriptor && value && value.source === descriptor.source &&
130
- value.title === descriptor.title && value.content === descriptor.content;
165
+ value.title === descriptor.title && errorAffixKey(value.content) === key;
131
166
  };
132
167
  if (index < 0 || !descriptor) return { hidden: false, count: 1, compact: false };
168
+ // Only the visible run head needs a count; hidden retries must not rescan
169
+ // the whole suffix and repeatedly fingerprint every later error.
170
+ const hidden = index > 0 && Boolean(same(children[index - 1]));
133
171
  let count = 1;
134
- while (index + count < children.length && same(children[index + count])) count++;
135
- return { hidden: index > 0 && Boolean(same(children[index - 1])), count,
172
+ if (!hidden) {
173
+ while (index + count < children.length && same(children[index + count])) count++;
174
+ }
175
+ return { hidden, count,
136
176
  compact: index > 0 && Boolean(errorDescriptor(children[index - 1])) };
137
177
  };
138
178
  const runningColor = (theme: any, text: string): string =>
@@ -157,14 +197,14 @@ export default function prettyTui(pi: ExtensionAPI) {
157
197
  let latestActivityStatusGeneration = 0;
158
198
  const renderLatestActivityStatus = (status: LatestActivityStatus, width: number, theme: any): string => {
159
199
  const severity = status.severity ?? "status";
160
- const icon = severity === "error" ? "✕" : severity === "success" ? "✓" : "◇";
200
+ const icon = severity === "error" ? "✕ " : severity === "success" ? "✓ " : "";
161
201
  const color = severity === "error"
162
202
  ? "error"
163
203
  : severity === "success"
164
204
  ? "success"
165
205
  : "dim";
166
206
  const detail = status.detail?.trim() ? ` — ${status.detail.trim()}` : "";
167
- return truncateToWidth(` ${theme.fg(color, `${icon} ${status.title}`)}${theme.fg("dim", detail)}`, Math.max(1, width), "…");
207
+ return truncateToWidth(` ${theme.fg(color, `${icon}${status.title}`)}${theme.fg("dim", detail)}`, Math.max(1, width), "…");
168
208
  };
169
209
  const refreshLatestActivityWidget = () => {
170
210
  if (!currentExtensionUi?.setWidget) return;
@@ -603,30 +643,54 @@ export default function prettyTui(pi: ExtensionAPI) {
603
643
  );
604
644
  };
605
645
 
606
- const renderStandaloneError = (member: ActivityMember, width: number, component?: any): string[] => {
646
+ const renderStandaloneAlert = (member: ActivityMember, width: number, component?: any): string[] => {
607
647
  const run = component && renderMode !== "full" ? errorRun(component) : { hidden: false, count: 1, compact: false };
648
+ if (component) errorDisclosureRows.delete(component);
608
649
  if (run.hidden) return [];
609
650
  const theme = defaultActivityTheme();
610
651
  const style = activityUpdateStyle(member, theme);
611
652
  const original = component ? errorDescriptor(component) : undefined;
612
- const title = stripTerminalSequences(original?.title ?? member.updateTitle ?? "Error");
613
- const content = stripTerminalSequences(original?.content || member.updateContent || "");
614
- const heading = style.icon + style.title(title);
615
- const prefix = heading + (content ? theme.fg("dim", " · ") : "");
616
- const text = prefix + theme.fg("muted", content) + (run.count > 1 ? theme.fg("dim", ` ×${run.count}`) : "");
617
- // Wrap rather than truncate; all original detail lines remain accessible.
618
- const continuation = " ".repeat(Math.min(visibleWidth(prefix), Math.max(0, Math.floor(width / 3))));
619
- const rows: string[] = [];
620
- for (const line of text.split("\n")) {
621
- const wrapped = wrapTextWithAnsi(line, Math.max(1, width - visibleWidth(continuation)));
622
- rows.push(...wrapped.map((row, index) => (rows.length || index ? continuation : "") + row));
653
+ const renderError = (titleText: string, contentText: string, count = 1): string[] => {
654
+ const title = stripTerminalSequences(titleText);
655
+ const content = stripTerminalSequences(contentText);
656
+ const heading = style.icon + style.title(title);
657
+ const prefix = heading + (content ? theme.fg("dim", " · ") : "");
658
+ const text = prefix + theme.fg("muted", content) + (count > 1 ? theme.fg("dim", component && expandedErrorRuns.has(component) ? " [Collapse]" : ` [×${count}]`) : "");
659
+ // Wrap rather than truncate; all original detail lines remain accessible.
660
+ // A single-line notification can be entirely its title. Indenting by
661
+ // that title's length wastes most of the viewport on every continuation.
662
+ const continuation = " ".repeat(Math.min(2, Math.max(0, width - 1)));
663
+ const rows: string[] = [];
664
+ for (const line of text.split("\n")) {
665
+ const wrapped = wrapTextWithAnsi(line, Math.max(1, width - visibleWidth(continuation)));
666
+ rows.push(...wrapped.map((row, index) => (rows.length || index ? continuation : "") + row));
667
+ }
668
+ return rows.map((line) => truncateToWidth(line, Math.max(1, width), ""));
669
+ };
670
+ const rows = [
671
+ ...(run.compact ? [] : [""]),
672
+ ...renderError(original?.title ?? member.updateTitle ?? "Error", original?.content || member.updateContent || "", run.count),
673
+ ];
674
+ if (component && run.count > 1) {
675
+ errorDisclosureRows.set(component, { start: run.compact ? 0 : 1, end: rows.length });
676
+ mouseClickHitTests.set(component, (event) => errorDisclosureHit(component, event));
677
+ if (expandedErrorRuns.has(component)) {
678
+ // The visible head owns the expanded projection. Original retry
679
+ // components stay untouched and hidden, and only the head counts them.
680
+ const children = errorChatContainer?.children ?? [];
681
+ const index = children.indexOf(component);
682
+ for (const retry of children.slice(index + 1, index + run.count)) {
683
+ const descriptor = errorDescriptor(retry);
684
+ if (descriptor) rows.push(...renderError(descriptor.title, descriptor.content));
685
+ }
686
+ }
623
687
  }
624
- return [...(run.compact ? [] : [""]), ...rows.map((line) => truncateToWidth(line, Math.max(1, width), ""))];
688
+ return rows;
625
689
  };
626
690
 
627
691
  const renderPendingActivityUpdate = (member: ActivityMember, width: number, component?: any): string[] => {
628
- if (member.updateSeverity === "error" && member.updateTitle !== "Response truncated") {
629
- return renderStandaloneError(member, width, component);
692
+ if (member.updateSeverity === "error" || member.updateSeverity === "warning") {
693
+ return renderStandaloneAlert(member, width, component);
630
694
  }
631
695
  const theme = defaultActivityTheme();
632
696
  const title = truncateToWidth(member.updateTitle ?? "Update", Math.max(1, width - 2), "…");
@@ -1405,10 +1469,15 @@ export default function prettyTui(pi: ExtensionAPI) {
1405
1469
  }
1406
1470
  if (renderMode === "compact") {
1407
1471
  const terminal = assistantTerminalUpdate(this[originalMessageKey] ?? this.lastMessage);
1408
- if (terminal) output.push(...renderPendingActivityUpdate(terminal, width, this));
1472
+ if (terminal) {
1473
+ const offset = output.length;
1474
+ output.push(...renderPendingActivityUpdate(terminal, width, this));
1475
+ const disclosure = errorDisclosureRows.get(this);
1476
+ if (disclosure) errorDisclosureRows.set(this, { start: disclosure.start + offset, end: disclosure.end + offset });
1477
+ }
1409
1478
  }
1410
1479
  panelAssistantViews.set(this, { rows, width: innerWidth, height: nativeLines.length });
1411
- mouseClickHitTests.set(this, (event) => thoughtRows.has(event.y));
1480
+ mouseClickHitTests.set(this, (event) => thoughtRows.has(event.y) || errorDisclosureHit(this, event));
1412
1481
  return renderMode === "compact" && width < 8
1413
1482
  ? output.map((line) => isNativeImageRow(line) ? line : truncateToWidth(line, Math.max(1, width), "")) : output;
1414
1483
  };
@@ -1457,6 +1526,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1457
1526
  };
1458
1527
 
1459
1528
  const renderAssistantTranscript = function (this: any, width: number): string[] {
1529
+ errorDisclosureRows.delete(this);
1460
1530
  const message = this[originalMessageKey] ?? this.lastMessage;
1461
1531
  if (
1462
1532
  message &&
@@ -1582,6 +1652,11 @@ export default function prettyTui(pi: ExtensionAPI) {
1582
1652
  };
1583
1653
 
1584
1654
  const handleAssistantTranscriptMouse = function (this: any, event: any) {
1655
+ if (errorDescriptor(this)) {
1656
+ patchedRender.call(this, event.width);
1657
+ const disclosure = handleErrorDisclosureMouse(this, event);
1658
+ if (disclosure) return disclosure;
1659
+ }
1585
1660
  const message = this[originalMessageKey] ?? this.lastMessage;
1586
1661
  if (renderMode !== "clean") {
1587
1662
  patchedRender.call(this, event.width);
@@ -1750,7 +1825,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1750
1825
  const component: Component = {
1751
1826
  render: (width: number) => renderActivityUpdateProjection(group, member, width, component),
1752
1827
  invalidate() {},
1753
- handleMouse: (event: any) => handleActivityUpdateMouse(group, member, event),
1828
+ handleMouse: (event: any) => handleErrorDisclosureMouse(component, event) ?? handleActivityUpdateMouse(group, member, event),
1754
1829
  } as Component;
1755
1830
  if (severity === "error") runtimeErrorDescriptors.set(component, {
1756
1831
  source: "runtime:error", title, content,
@@ -2725,6 +2800,8 @@ export default function prettyTui(pi: ExtensionAPI) {
2725
2800
  prefix: DisplayValue;
2726
2801
  continuation?: DisplayValue;
2727
2802
  content: DisplayValue;
2803
+ singleLine?: boolean;
2804
+ background?: (text: string) => string;
2728
2805
  };
2729
2806
 
2730
2807
  /**
@@ -2738,14 +2815,21 @@ export default function prettyTui(pi: ExtensionAPI) {
2738
2815
  for (const row of rows) {
2739
2816
  const prefix = typeof row.prefix === "function" ? row.prefix() : row.prefix;
2740
2817
  const content = typeof row.content === "function" ? row.content() : row.content;
2818
+ if (row.singleLine) {
2819
+ const inline = content.replace(/\r\n|\r|\n/g, " ");
2820
+ rendered.push(truncateToWidth(prefix + inline, Math.max(1, width), "…"));
2821
+ continue;
2822
+ }
2741
2823
  const rawContinuation = row.continuation ?? " ".repeat(visibleWidth(prefix));
2742
2824
  const continuation = typeof rawContinuation === "function" ? rawContinuation() : rawContinuation;
2743
2825
  const prefixWidth = Math.max(visibleWidth(prefix), visibleWidth(continuation));
2744
2826
  const contentWidth = Math.max(1, width - prefixWidth);
2745
2827
  const wrapped = wrapTextWithAnsi(content || " ", contentWidth);
2746
2828
 
2747
- rendered.push(prefix + (wrapped[0] ?? ""));
2748
- for (const line of wrapped.slice(1)) rendered.push(continuation + line);
2829
+ const paint = (line: string) => row.background
2830
+ ? row.background(line + " ".repeat(Math.max(0, contentWidth - visibleWidth(line)))) : line;
2831
+ rendered.push(prefix + paint(wrapped[0] ?? ""));
2832
+ for (const line of wrapped.slice(1)) rendered.push(continuation + paint(line));
2749
2833
  }
2750
2834
 
2751
2835
  return rendered;
@@ -2758,7 +2842,8 @@ export default function prettyTui(pi: ExtensionAPI) {
2758
2842
  context.state.compactToolStatus = status;
2759
2843
  };
2760
2844
 
2761
- const callRow = (theme: any, name: string, detail: string, state: any): DisplayRow => ({
2845
+ const callRow = (theme: any, name: string, detail: string, state: any, expanded = false): DisplayRow => ({
2846
+ singleLine: renderMode === "clean" && !expanded,
2762
2847
  prefix: () => {
2763
2848
  const status = (state.compactToolStatus ?? "running") as ToolStatus;
2764
2849
  const color = status === "success" ? "success" : status === "error" ? "error" : "dim";
@@ -2769,12 +2854,12 @@ export default function prettyTui(pi: ExtensionAPI) {
2769
2854
  content: renderMode === "compact" ? theme.fg("muted", detail) :
2770
2855
  theme.fg("text", theme.bold(name)) +
2771
2856
  theme.fg("dim", "(") +
2772
- theme.fg("muted", detail) +
2857
+ theme.fg(renderMode === "clean" && expanded ? "text" : "muted", detail) +
2773
2858
  theme.fg("dim", ")"),
2774
2859
  });
2775
2860
 
2776
- const call = (theme: any, name: string, detail: string, state: any) =>
2777
- block([callRow(theme, name, detail, state)]);
2861
+ const call = (theme: any, name: string, detail: string, state: any, expanded = false) =>
2862
+ block([callRow(theme, name, detail, state, expanded)]);
2778
2863
 
2779
2864
  const writeCall = (theme: any, name: string, path: string, content: string, expanded: boolean, state: any) => {
2780
2865
  const lines = content.replace(/\t/g, " ").split("\n");
@@ -2784,7 +2869,7 @@ export default function prettyTui(pi: ExtensionAPI) {
2784
2869
  const shown = lines.slice(0, expanded ? total : 10);
2785
2870
  const remaining = total - shown.length;
2786
2871
  const rows: DisplayRow[] = [
2787
- callRow(theme, name, `${path} · ${total} ${total === 1 ? "line" : "lines"}`, state),
2872
+ callRow(theme, name, `${path} · ${total} ${total === 1 ? "line" : "lines"}`, state, expanded),
2788
2873
  ];
2789
2874
 
2790
2875
  for (let index = 0; index < shown.length; index++) {
@@ -2807,28 +2892,6 @@ export default function prettyTui(pi: ExtensionAPI) {
2807
2892
  return block(rows);
2808
2893
  };
2809
2894
 
2810
- // Pi diffs encode the marker before the padded line number. Move it into
2811
- // its own column and keep the whole gutter out of content word wrapping.
2812
- const editDiffLayout = (theme: any, diff: string): ((line: string) => DisplayRow) => {
2813
- const parse = (line: string) => /^([ +\-]) *(\d+) (.*)$/.exec(line);
2814
- const numberWidth = diff.split("\n").reduce((width, line) => Math.max(width, parse(line)?.[2].length ?? 0), 1);
2815
- const gutterWidth = numberWidth + 3;
2816
- return (line) => {
2817
- const match = parse(line);
2818
- const marker = match?.[1] ?? (
2819
- line.startsWith("+") && !line.startsWith("+++") ? "+" :
2820
- line.startsWith("-") && !line.startsWith("---") ? "-" : " "
2821
- );
2822
- const color = marker === "+" ? "toolDiffAdded" : marker === "-" ? "toolDiffRemoved" : "toolDiffContext";
2823
- const gutter = match ? `${match[2].padStart(numberWidth)} ${marker} ` : " ".repeat(gutterWidth);
2824
- return {
2825
- prefix: theme.fg(color, gutter),
2826
- continuation: " ".repeat(gutterWidth),
2827
- content: theme.fg(color, match ? match[3] : /^\s+\.\.\.$/.test(line) ? "..." : line),
2828
- };
2829
- };
2830
- };
2831
-
2832
2895
  const result = (
2833
2896
  theme: any,
2834
2897
  summary: string,
@@ -2857,6 +2920,7 @@ export default function prettyTui(pi: ExtensionAPI) {
2857
2920
  prefix: theme.fg("dim", isLast ? " └ " : " │ ") + (layout?.prefix ?? ""),
2858
2921
  continuation: (isLast ? " " : theme.fg("dim", " │ ")) + (layout?.continuation ?? ""),
2859
2922
  content: layout?.content ?? outputStyle(shown[index] || " "),
2923
+ background: layout?.background,
2860
2924
  });
2861
2925
  }
2862
2926
  if (omitted > 0) {
@@ -4464,7 +4528,7 @@ export default function prettyTui(pi: ExtensionAPI) {
4464
4528
  renderCall(args: any, theme: any, context: any) {
4465
4529
  if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, readLabel, context.toolCallId);
4466
4530
  const range = readLineRange(args.offset, args.limit);
4467
- return call(theme, readLabel, `${args.path}${range}`, context.state);
4531
+ return call(theme, readLabel, `${args.path}${range}`, context.state, context.expanded);
4468
4532
  },
4469
4533
  renderResult(toolResult: any, options: any, theme: any, context: any) {
4470
4534
  const output = textContent(toolResult);
@@ -4500,9 +4564,9 @@ export default function prettyTui(pi: ExtensionAPI) {
4500
4564
  const detail = omitted > 0
4501
4565
  ? `${firstLine} … (${omitted} more ${omitted === 1 ? "line" : "lines"})`
4502
4566
  : firstLine;
4503
- return call(theme, bashLabel, detail, context.state);
4567
+ return call(theme, bashLabel, detail, context.state, context.expanded);
4504
4568
  }
4505
- return call(theme, bashLabel, command, context.state);
4569
+ return call(theme, bashLabel, command, context.state, context.expanded);
4506
4570
  },
4507
4571
  renderResult(toolResult: any, options: any, theme: any, context: any) {
4508
4572
  const output = textContent(toolResult);
@@ -4544,7 +4608,7 @@ export default function prettyTui(pi: ExtensionAPI) {
4544
4608
  renderCall(args: any, theme: any, context: any) {
4545
4609
  if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, editLabel, context.toolCallId);
4546
4610
  const count = Array.isArray(args.edits) ? ` · ${args.edits.length} changes` : "";
4547
- return call(theme, editLabel, `${args.path}${count}`, context.state);
4611
+ return call(theme, editLabel, `${args.path}${count}`, context.state, context.expanded);
4548
4612
  },
4549
4613
  renderResult(toolResult: any, options: any, theme: any, context: any) {
4550
4614
  const output = textContent(toolResult);
@@ -4574,7 +4638,7 @@ export default function prettyTui(pi: ExtensionAPI) {
4574
4638
  if (line.startsWith("-") && !line.startsWith("---")) return theme.fg("toolDiffRemoved", line);
4575
4639
  return theme.fg("toolDiffContext", line);
4576
4640
  },
4577
- !failed && diff ? editDiffLayout(theme, diff) : undefined,
4641
+ !failed && diff ? editDiffLayout(theme, diff, context.args?.path) : undefined,
4578
4642
  );
4579
4643
  },
4580
4644
  } as any);
@@ -4592,7 +4656,7 @@ export default function prettyTui(pi: ExtensionAPI) {
4592
4656
  const lines = content.replace(/\t/g, " ").split("\n");
4593
4657
  while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
4594
4658
  const total = lines.length;
4595
- return call(theme, writeLabel, `${path} · ${total} ${total === 1 ? "line" : "lines"}`, context.state);
4659
+ return call(theme, writeLabel, `${path} · ${total} ${total === 1 ? "line" : "lines"}`, context.state, context.expanded);
4596
4660
  }
4597
4661
  return writeCall(theme, writeLabel, path, content, context.expanded, context.state);
4598
4662
  },
@@ -4620,7 +4684,7 @@ export default function prettyTui(pi: ExtensionAPI) {
4620
4684
  if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, grepLabel, context.toolCallId);
4621
4685
  const where = args.path ? ` · ${args.path}` : "";
4622
4686
  const glob = args.glob ? ` · ${args.glob}` : "";
4623
- return call(theme, grepLabel, `${args.pattern}${where}${glob}`, context.state);
4687
+ return call(theme, grepLabel, `${args.pattern}${where}${glob}`, context.state, context.expanded);
4624
4688
  },
4625
4689
  renderResult(toolResult: any, options: any, theme: any, context: any) {
4626
4690
  const output = textContent(toolResult);
@@ -4639,7 +4703,7 @@ export default function prettyTui(pi: ExtensionAPI) {
4639
4703
  renderShell: "self",
4640
4704
  renderCall(args: any, theme: any, context: any) {
4641
4705
  if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, findLabel, context.toolCallId);
4642
- return call(theme, findLabel, `${args.pattern}${args.path ? ` · ${args.path}` : ""}`, context.state);
4706
+ return call(theme, findLabel, `${args.pattern}${args.path ? ` · ${args.path}` : ""}`, context.state, context.expanded);
4643
4707
  },
4644
4708
  renderResult(toolResult: any, options: any, theme: any, context: any) {
4645
4709
  const output = textContent(toolResult);
@@ -4658,7 +4722,7 @@ export default function prettyTui(pi: ExtensionAPI) {
4658
4722
  renderShell: "self",
4659
4723
  renderCall(args: any, theme: any, context: any) {
4660
4724
  if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, lsLabel, context.toolCallId);
4661
- return call(theme, lsLabel, args.path ?? ".", context.state);
4725
+ return call(theme, lsLabel, args.path ?? ".", context.state, context.expanded);
4662
4726
  },
4663
4727
  renderResult(toolResult: any, options: any, theme: any, context: any) {
4664
4728
  const output = textContent(toolResult);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-pretty-tui",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
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>",