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 +6 -0
- package/README.zh-CN.md +6 -0
- package/extensions/edit-diff.ts +103 -0
- package/extensions/index.ts +125 -61
- package/package.json +1 -1
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
|
+
}
|
package/extensions/index.ts
CHANGED
|
@@ -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 ===
|
|
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
|
-
|
|
135
|
-
|
|
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}
|
|
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
|
|
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
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
const
|
|
622
|
-
|
|
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
|
|
688
|
+
return rows;
|
|
625
689
|
};
|
|
626
690
|
|
|
627
691
|
const renderPendingActivityUpdate = (member: ActivityMember, width: number, component?: any): string[] => {
|
|
628
|
-
if (member.updateSeverity === "error"
|
|
629
|
-
return
|
|
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)
|
|
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
|
-
|
|
2748
|
-
|
|
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);
|