pi-pretty-tui 0.3.2 → 0.3.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.
- package/README.md +6 -0
- package/README.zh-CN.md +6 -0
- package/extensions/index.ts +90 -18
- 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,即可使用鼠标复制入口:
|
package/extensions/index.ts
CHANGED
|
@@ -89,6 +89,24 @@ const loadConfig = (path: string): PrettyTuiConfig => {
|
|
|
89
89
|
}
|
|
90
90
|
};
|
|
91
91
|
|
|
92
|
+
/** Stable text affixes around opaque IDs, without fuzzy similarity chaining. */
|
|
93
|
+
const errorAffixKey = (text: string): string => {
|
|
94
|
+
const parts: (string | ["hex" | "uuid"])[] = [];
|
|
95
|
+
let cursor = 0;
|
|
96
|
+
// Whole tokens only: keep semantic identifiers such as msg_<hex> and
|
|
97
|
+
// rs_<hex>, short codes, ordinary words, and numeric quantities exact.
|
|
98
|
+
const opaqueId = /(?<![\w-])(?:[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}|[\da-f]{16,128})(?![\w-])/giu;
|
|
99
|
+
for (const match of text.matchAll(opaqueId)) {
|
|
100
|
+
const kind = match[0].includes("-") ? "uuid" : "hex";
|
|
101
|
+
if (kind === "hex" && !/[a-f]/iu.test(match[0])) continue;
|
|
102
|
+
parts.push(text.slice(cursor, match.index), [kind]);
|
|
103
|
+
cursor = match.index + match[0].length;
|
|
104
|
+
}
|
|
105
|
+
parts.push(text.slice(cursor));
|
|
106
|
+
// Typed slots cannot collide with a literal placeholder in an error.
|
|
107
|
+
return JSON.stringify(parts);
|
|
108
|
+
};
|
|
109
|
+
|
|
92
110
|
/** Polished user messages, tool calls, and list rendering for Pi's TUI. */
|
|
93
111
|
export default function prettyTui(pi: ExtensionAPI) {
|
|
94
112
|
const cwd = process.cwd();
|
|
@@ -111,6 +129,21 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
111
129
|
let currentPromptEditor: any;
|
|
112
130
|
let errorChatContainer: any;
|
|
113
131
|
const runtimeErrorDescriptors = new WeakMap<object, { source: string; title: string; content: string }>();
|
|
132
|
+
const expandedErrorRuns = new WeakSet<object>();
|
|
133
|
+
const errorDisclosureRows = new WeakMap<object, { start: number; end: number }>();
|
|
134
|
+
const errorDisclosureHit = (component: object, event: any) => {
|
|
135
|
+
const rows = errorDisclosureRows.get(component);
|
|
136
|
+
return renderMode !== "full" && Boolean(rows && event.y >= rows.start && event.y < rows.end);
|
|
137
|
+
};
|
|
138
|
+
const handleErrorDisclosureMouse = (component: object, event: any) => {
|
|
139
|
+
if (event.button !== "left" || !errorDisclosureHit(component, event)) return undefined;
|
|
140
|
+
if (event.type === "press") return { handled: true };
|
|
141
|
+
if (event.type !== "click") return undefined;
|
|
142
|
+
if (expandedErrorRuns.has(component)) expandedErrorRuns.delete(component);
|
|
143
|
+
else expandedErrorRuns.add(component);
|
|
144
|
+
currentTui?.requestRender?.();
|
|
145
|
+
return { handled: true };
|
|
146
|
+
};
|
|
114
147
|
const errorDescriptor = (component: any) => {
|
|
115
148
|
const runtime = runtimeErrorDescriptors.get(component);
|
|
116
149
|
if (runtime) return runtime;
|
|
@@ -124,15 +157,21 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
124
157
|
const children: any[] = errorChatContainer?.children ?? [];
|
|
125
158
|
const index = children.indexOf(component);
|
|
126
159
|
const descriptor = errorDescriptor(component);
|
|
160
|
+
const key = descriptor && errorAffixKey(descriptor.content);
|
|
127
161
|
const same = (other: any) => {
|
|
128
162
|
const value = errorDescriptor(other);
|
|
129
163
|
return descriptor && value && value.source === descriptor.source &&
|
|
130
|
-
value.title === descriptor.title && value.content ===
|
|
164
|
+
value.title === descriptor.title && errorAffixKey(value.content) === key;
|
|
131
165
|
};
|
|
132
166
|
if (index < 0 || !descriptor) return { hidden: false, count: 1, compact: false };
|
|
167
|
+
// Only the visible run head needs a count; hidden retries must not rescan
|
|
168
|
+
// the whole suffix and repeatedly fingerprint every later error.
|
|
169
|
+
const hidden = index > 0 && Boolean(same(children[index - 1]));
|
|
133
170
|
let count = 1;
|
|
134
|
-
|
|
135
|
-
|
|
171
|
+
if (!hidden) {
|
|
172
|
+
while (index + count < children.length && same(children[index + count])) count++;
|
|
173
|
+
}
|
|
174
|
+
return { hidden, count,
|
|
136
175
|
compact: index > 0 && Boolean(errorDescriptor(children[index - 1])) };
|
|
137
176
|
};
|
|
138
177
|
const runningColor = (theme: any, text: string): string =>
|
|
@@ -605,23 +644,45 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
605
644
|
|
|
606
645
|
const renderStandaloneError = (member: ActivityMember, width: number, component?: any): string[] => {
|
|
607
646
|
const run = component && renderMode !== "full" ? errorRun(component) : { hidden: false, count: 1, compact: false };
|
|
647
|
+
if (component) errorDisclosureRows.delete(component);
|
|
608
648
|
if (run.hidden) return [];
|
|
609
649
|
const theme = defaultActivityTheme();
|
|
610
650
|
const style = activityUpdateStyle(member, theme);
|
|
611
651
|
const original = component ? errorDescriptor(component) : undefined;
|
|
612
|
-
const
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
const
|
|
622
|
-
|
|
652
|
+
const renderError = (titleText: string, contentText: string, count = 1): string[] => {
|
|
653
|
+
const title = stripTerminalSequences(titleText);
|
|
654
|
+
const content = stripTerminalSequences(contentText);
|
|
655
|
+
const heading = style.icon + style.title(title);
|
|
656
|
+
const prefix = heading + (content ? theme.fg("dim", " · ") : "");
|
|
657
|
+
const text = prefix + theme.fg("muted", content) + (count > 1 ? theme.fg("dim", component && expandedErrorRuns.has(component) ? " [Collapse]" : ` [×${count}]`) : "");
|
|
658
|
+
// 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))));
|
|
660
|
+
const rows: string[] = [];
|
|
661
|
+
for (const line of text.split("\n")) {
|
|
662
|
+
const wrapped = wrapTextWithAnsi(line, Math.max(1, width - visibleWidth(continuation)));
|
|
663
|
+
rows.push(...wrapped.map((row, index) => (rows.length || index ? continuation : "") + row));
|
|
664
|
+
}
|
|
665
|
+
return rows.map((line) => truncateToWidth(line, Math.max(1, width), ""));
|
|
666
|
+
};
|
|
667
|
+
const rows = [
|
|
668
|
+
...(run.compact ? [] : [""]),
|
|
669
|
+
...renderError(original?.title ?? member.updateTitle ?? "Error", original?.content || member.updateContent || "", run.count),
|
|
670
|
+
];
|
|
671
|
+
if (component && run.count > 1) {
|
|
672
|
+
errorDisclosureRows.set(component, { start: run.compact ? 0 : 1, end: rows.length });
|
|
673
|
+
mouseClickHitTests.set(component, (event) => errorDisclosureHit(component, event));
|
|
674
|
+
if (expandedErrorRuns.has(component)) {
|
|
675
|
+
// The visible head owns the expanded projection. Original retry
|
|
676
|
+
// components stay untouched and hidden, and only the head counts them.
|
|
677
|
+
const children = errorChatContainer?.children ?? [];
|
|
678
|
+
const index = children.indexOf(component);
|
|
679
|
+
for (const retry of children.slice(index + 1, index + run.count)) {
|
|
680
|
+
const descriptor = errorDescriptor(retry);
|
|
681
|
+
if (descriptor) rows.push(...renderError(descriptor.title, descriptor.content));
|
|
682
|
+
}
|
|
683
|
+
}
|
|
623
684
|
}
|
|
624
|
-
return
|
|
685
|
+
return rows;
|
|
625
686
|
};
|
|
626
687
|
|
|
627
688
|
const renderPendingActivityUpdate = (member: ActivityMember, width: number, component?: any): string[] => {
|
|
@@ -1405,10 +1466,15 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1405
1466
|
}
|
|
1406
1467
|
if (renderMode === "compact") {
|
|
1407
1468
|
const terminal = assistantTerminalUpdate(this[originalMessageKey] ?? this.lastMessage);
|
|
1408
|
-
if (terminal)
|
|
1469
|
+
if (terminal) {
|
|
1470
|
+
const offset = output.length;
|
|
1471
|
+
output.push(...renderPendingActivityUpdate(terminal, width, this));
|
|
1472
|
+
const disclosure = errorDisclosureRows.get(this);
|
|
1473
|
+
if (disclosure) errorDisclosureRows.set(this, { start: disclosure.start + offset, end: disclosure.end + offset });
|
|
1474
|
+
}
|
|
1409
1475
|
}
|
|
1410
1476
|
panelAssistantViews.set(this, { rows, width: innerWidth, height: nativeLines.length });
|
|
1411
|
-
mouseClickHitTests.set(this, (event) => thoughtRows.has(event.y));
|
|
1477
|
+
mouseClickHitTests.set(this, (event) => thoughtRows.has(event.y) || errorDisclosureHit(this, event));
|
|
1412
1478
|
return renderMode === "compact" && width < 8
|
|
1413
1479
|
? output.map((line) => isNativeImageRow(line) ? line : truncateToWidth(line, Math.max(1, width), "")) : output;
|
|
1414
1480
|
};
|
|
@@ -1457,6 +1523,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1457
1523
|
};
|
|
1458
1524
|
|
|
1459
1525
|
const renderAssistantTranscript = function (this: any, width: number): string[] {
|
|
1526
|
+
errorDisclosureRows.delete(this);
|
|
1460
1527
|
const message = this[originalMessageKey] ?? this.lastMessage;
|
|
1461
1528
|
if (
|
|
1462
1529
|
message &&
|
|
@@ -1582,6 +1649,11 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1582
1649
|
};
|
|
1583
1650
|
|
|
1584
1651
|
const handleAssistantTranscriptMouse = function (this: any, event: any) {
|
|
1652
|
+
if (errorDescriptor(this)) {
|
|
1653
|
+
patchedRender.call(this, event.width);
|
|
1654
|
+
const disclosure = handleErrorDisclosureMouse(this, event);
|
|
1655
|
+
if (disclosure) return disclosure;
|
|
1656
|
+
}
|
|
1585
1657
|
const message = this[originalMessageKey] ?? this.lastMessage;
|
|
1586
1658
|
if (renderMode !== "clean") {
|
|
1587
1659
|
patchedRender.call(this, event.width);
|
|
@@ -1750,7 +1822,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1750
1822
|
const component: Component = {
|
|
1751
1823
|
render: (width: number) => renderActivityUpdateProjection(group, member, width, component),
|
|
1752
1824
|
invalidate() {},
|
|
1753
|
-
handleMouse: (event: any) => handleActivityUpdateMouse(group, member, event),
|
|
1825
|
+
handleMouse: (event: any) => handleErrorDisclosureMouse(component, event) ?? handleActivityUpdateMouse(group, member, event),
|
|
1754
1826
|
} as Component;
|
|
1755
1827
|
if (severity === "error") runtimeErrorDescriptors.set(component, {
|
|
1756
1828
|
source: "runtime:error", title, content,
|