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 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,即可使用鼠标复制入口:
@@ -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 === descriptor.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
- while (index + count < children.length && same(children[index + count])) count++;
135
- return { hidden: index > 0 && Boolean(same(children[index - 1])), count,
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 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));
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 [...(run.compact ? [] : [""]), ...rows.map((line) => truncateToWidth(line, Math.max(1, width), ""))];
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) output.push(...renderPendingActivityUpdate(terminal, width, this));
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,
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.3",
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>",