pi-pretty-tui 0.3.4 → 0.3.6

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
@@ -58,9 +58,25 @@ pi --tui-mode fullscreen
58
58
 
59
59
  Dragging selects text rather than copying a whole message. Links keep their normal behavior; image attachments are not included in text copies.
60
60
 
61
+ **Message copy** controls user-message click-to-copy and the final-answer `[Copy]` button.
62
+ **Code block copy** controls snippet `[Copy]` buttons independently.
63
+ Both are on by default. Open `/pretty-tui` → **Copy settings** to configure message, code-block and selection copying, or use:
64
+
65
+ ```text
66
+ /pretty-tui message-copy off
67
+ /pretty-tui message-copy on
68
+ /pretty-tui code-block-copy off
69
+ /pretty-tui code-block-copy on
70
+ ```
71
+
72
+ Toggling either option saves the setting and reloads Pi.
73
+ The `messageCopy` and `codeBlockCopy` booleans are stored in `pretty-tui.json` in Pi's agent directory, alongside `selectionCopy`.
74
+ Disabling message copying keeps the response duration and status; disabling code-block copying keeps code frames, highlighting and content.
75
+ Neither option changes selection copying.
76
+
61
77
  ### Optional border-free selection
62
78
 
63
- Enable **Selection copy** in `/pretty-tui`, or run:
79
+ Enable **Selection copy** in `/pretty-tui` → **Copy settings**, or run:
64
80
 
65
81
  ```text
66
82
  /pretty-tui selection-copy on
@@ -73,7 +89,7 @@ In regular mode, the terminal handles selection and copying, so this option has
73
89
 
74
90
  ## Notes
75
91
 
76
- - Tested with **Pi 1.5.0**. Future Pi updates may require compatibility fixes.
92
+ - Tested with **Pi 1.5.1**. Future Pi updates may require compatibility fixes.
77
93
  - Keeps Pi's native input behavior and tool execution, including third-party tool implementations.
78
94
  - Nested-tool text details are saved with sessions for restoration and may be truncated. Session files can contain sensitive tool output.
79
95
  - See [Releases](https://github.com/ykn0309/pi-pretty-tui/releases) for changes and contributor acknowledgments.
package/README.zh-CN.md CHANGED
@@ -58,9 +58,25 @@ pi --tui-mode fullscreen
58
58
 
59
59
  拖动鼠标按选区处理,不触发整条消息复制。链接保留正常行为;文本复制不包含图片附件。
60
60
 
61
+ **Message copy** 控制用户消息点击复制和最终回答的 `[Copy]` 按钮。
62
+ **Code block copy** 独立控制代码块的 `[Copy]` 按钮。
63
+ 两项均默认开启。在 `/pretty-tui` → **Copy settings** 子菜单中统一配置消息、代码块和选区复制,也可运行:
64
+
65
+ ```text
66
+ /pretty-tui message-copy off
67
+ /pretty-tui message-copy on
68
+ /pretty-tui code-block-copy off
69
+ /pretty-tui code-block-copy on
70
+ ```
71
+
72
+ 切换任一选项会保存设置并自动重载 Pi。
73
+ 布尔值 `messageCopy` 和 `codeBlockCopy` 与 `selectionCopy` 一起保存在 Pi agent 目录下的 `pretty-tui.json` 中。
74
+ 关闭消息复制仍保留回答耗时和状态;关闭代码块复制仍保留边框、语法高亮和内容。
75
+ 两项均不影响选区复制。
76
+
61
77
  ### 可选的无边框选区复制
62
78
 
63
- 在 `/pretty-tui` 中切换 **Selection copy**,或运行:
79
+ 在 `/pretty-tui` → **Copy settings** 中切换 **Selection copy**,或运行:
64
80
 
65
81
  ```text
66
82
  /pretty-tui selection-copy on
@@ -73,7 +89,7 @@ regular 非全屏模式由终端处理选择与复制,开启此选项也不会
73
89
 
74
90
  ## 必要说明
75
91
 
76
- - 当前已在 **Pi 1.5.0** 上测试;未来 Pi 更新可能需要适配。
92
+ - 当前已在 **Pi 1.5.1** 上测试;未来 Pi 更新可能需要适配。
77
93
  - 保留 Pi 原生输入行为和工具执行逻辑,包括第三方工具实现。
78
94
  - 嵌套工具的文本详情会随会话保存以便恢复,内容可能截断;会话文件可能包含工具输出中的敏感信息。
79
95
  - 版本变更与贡献者致谢见 [Releases](https://github.com/ykn0309/pi-pretty-tui/releases)。
@@ -1,5 +1,5 @@
1
1
  import { getLanguageFromPath, highlightCode } from "@earendil-works/pi-coding-agent";
2
- import { sliceByColumn, visibleWidth } from "@earendil-works/pi-tui";
2
+ import { mixColors, sliceByColumn, visibleWidth } from "@earendil-works/pi-tui";
3
3
 
4
4
  type Range = [number, number];
5
5
  type Row = { marker: string; number?: string; text: string; styled: string; changes: Range[] };
@@ -82,13 +82,16 @@ export function editDiffLayout(theme: any, diff: string, path?: string) {
82
82
  const color = row.marker === "+" ? "toolDiffAdded" : row.marker === "-" ? "toolDiffRemoved" : "toolDiffContext";
83
83
  const background = row.marker === "+" ? "toolSuccessBg" : row.marker === "-" ? "toolErrorBg" : undefined;
84
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;
85
87
  const width = visibleWidth(row.text);
86
88
  let content = "", cursor = 0;
87
89
  for (const [start, end] of row.changes) {
88
90
  content += sliceByColumn(row.styled, cursor, start - cursor);
89
91
  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
+ // 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);
92
95
  cursor = end;
93
96
  }
94
97
  content += sliceByColumn(row.styled, cursor, Math.max(0, width - cursor));
@@ -37,12 +37,15 @@ import {
37
37
  copyToClipboard,
38
38
  getAgentDir,
39
39
  getMarkdownTheme,
40
+ getLanguageFromPath,
41
+ highlightCode,
40
42
  type ExtensionAPI,
41
43
  } from "@earendil-works/pi-coding-agent";
42
44
  import {
43
45
  type Component,
44
46
  Markdown,
45
47
  stripTerminalSequences,
48
+ sliceByColumn,
46
49
  truncateToWidth,
47
50
  TuiAltScreen,
48
51
  type TuiMouseEvent,
@@ -71,6 +74,8 @@ const RESPONSE_FOOTER_ENTRY_TYPE = "pretty-tui-response-footer";
71
74
  type PrettyTuiConfig = {
72
75
  enabled?: boolean;
73
76
  mode?: PrettyTuiMode;
77
+ messageCopy?: boolean;
78
+ codeBlockCopy?: boolean;
74
79
  /** Exclude plugin decoration from fullscreen selections. Opt-in. */
75
80
  selectionCopy?: boolean;
76
81
  /** "native" keeps Pi's flags-only strong and emphasis. Read when the extension loads. */
@@ -114,6 +119,8 @@ export default function prettyTui(pi: ExtensionAPI) {
114
119
  const configPath = join(getAgentDir(), "pretty-tui.json");
115
120
  let config = loadConfig(configPath);
116
121
  const activeForSession = config.enabled !== false;
122
+ const messageCopyEnabled = config.messageCopy !== false;
123
+ const codeBlockCopyEnabled = config.codeBlockCopy !== false;
117
124
  const configuredMode = config.mode ?? config.bash?.mode;
118
125
  let renderMode: PrettyTuiMode = configuredMode === "full" || configuredMode === "compact" || configuredMode === "clean"
119
126
  ? configuredMode
@@ -763,6 +770,10 @@ export default function prettyTui(pi: ExtensionAPI) {
763
770
  { value: "full", label: "full", description: "Full tool details and output" },
764
771
  { value: "compact", label: "compact", description: "Concise summaries for all built-in tools" },
765
772
  { value: "clean", label: "clean", description: "Group supported tools into Running/Done status" },
773
+ { value: "message-copy on", label: "message-copy on", description: "Enable user-message and final-answer copying and reload" },
774
+ { value: "message-copy off", label: "message-copy off", description: "Disable user-message and final-answer copying and reload" },
775
+ { value: "code-block-copy on", label: "code-block-copy on", description: "Show code-block Copy controls and reload" },
776
+ { value: "code-block-copy off", label: "code-block-copy off", description: "Hide code-block Copy controls and reload" },
766
777
  { value: "selection-copy on", label: "selection-copy on", description: "Copy selected text without UI borders in fullscreen mode; enable and reload" },
767
778
  { value: "selection-copy off", label: "selection-copy off", description: "Restore native selection copying and reload" },
768
779
  { value: "status", label: "status", description: "Show the current settings" },
@@ -772,25 +783,32 @@ export default function prettyTui(pi: ExtensionAPI) {
772
783
  },
773
784
  handler: async (args, ctx) => {
774
785
  let requested = args.trim().toLowerCase();
786
+ if (!requested && !ctx.hasUI) requested = "status";
775
787
 
776
- if (!requested) {
777
- if (!ctx.hasUI) {
778
- ctx.ui.notify(
779
- `pi-pretty-tui: ${config.enabled !== false ? "enabled" : "disabled"} · mode: ${renderMode} · selection copy: ${config.selectionCopy === true ? "on" : "off"}`,
780
- "info",
781
- );
782
- return;
783
- }
788
+ while (!requested) {
784
789
  const enabled = `Enabled — ${config.enabled !== false ? "on" : "off"}`;
785
790
  const full = `Full — full tool details and output${renderMode === "full" ? " (current)" : ""}`;
786
791
  const compact = `Compact — concise tool summaries and nested calls${renderMode === "compact" ? " (current)" : ""}`;
787
792
  const clean = `Clean — group supported tools into Running/Done status${renderMode === "clean" ? " (current)" : ""}`;
788
793
  const selectionCopy = `Selection copy — ${config.selectionCopy === true ? "on" : "off"} · copy text without UI borders (fullscreen, experimental)`;
789
- const selected = await ctx.ui.select("pi-pretty-tui settings", [enabled, full, compact, clean, selectionCopy]);
794
+ const messageCopy = `Message copy — ${config.messageCopy !== false ? "on" : "off"} · user messages and final answers`;
795
+ const codeBlockCopy = `Code block copy — ${config.codeBlockCopy !== false ? "on" : "off"} · code-block Copy controls`;
796
+ const copySettings = "Copy settings — message, code block and selection copying";
797
+ const selected = await ctx.ui.select("pi-pretty-tui settings", [enabled, full, compact, clean, copySettings]);
790
798
  if (!selected) return;
791
- requested = selected === selectionCopy
792
- ? config.selectionCopy === true ? "selection-copy off" : "selection-copy on"
793
- : selected === enabled
799
+ if (selected === copySettings) {
800
+ const back = "Back";
801
+ const copy = await ctx.ui.select("Copy settings", [messageCopy, codeBlockCopy, selectionCopy, back]);
802
+ if (!copy) return;
803
+ if (copy === back) continue;
804
+ requested = copy === messageCopy
805
+ ? config.messageCopy !== false ? "message-copy off" : "message-copy on"
806
+ : copy === codeBlockCopy
807
+ ? config.codeBlockCopy !== false ? "code-block-copy off" : "code-block-copy on"
808
+ : config.selectionCopy === true ? "selection-copy off" : "selection-copy on";
809
+ continue;
810
+ }
811
+ requested = selected === enabled
794
812
  ? config.enabled !== false ? "disable" : "enable"
795
813
  : selected === full
796
814
  ? "full"
@@ -801,21 +819,26 @@ export default function prettyTui(pi: ExtensionAPI) {
801
819
 
802
820
  if (requested === "status") {
803
821
  ctx.ui.notify(
804
- `pi-pretty-tui: ${config.enabled !== false ? "enabled" : "disabled"} · mode: ${renderMode} · selection copy: ${config.selectionCopy === true ? "on" : "off"}`,
822
+ `pi-pretty-tui: ${config.enabled !== false ? "enabled" : "disabled"} · mode: ${renderMode} · message copy: ${config.messageCopy !== false ? "on" : "off"} · code block copy: ${config.codeBlockCopy !== false ? "on" : "off"} · selection copy: ${config.selectionCopy === true ? "on" : "off"}`,
805
823
  "info",
806
824
  );
807
825
  return;
808
826
  }
809
- if (requested === "selection-copy on" || requested === "selection-copy off") {
810
- const selectionCopy = requested.endsWith(" on");
827
+ const copySetting = /^(message-copy|code-block-copy|selection-copy) (on|off)$/.exec(requested);
828
+ if (copySetting) {
829
+ const key = copySetting[1] === "message-copy" ? "messageCopy"
830
+ : copySetting[1] === "code-block-copy" ? "codeBlockCopy" : "selectionCopy";
831
+ const enabled = copySetting[2] === "on";
811
832
  try {
812
- saveConfig({ ...config, selectionCopy });
833
+ saveConfig({ ...config, [key]: enabled });
813
834
  } catch (error) {
814
835
  const message = error instanceof Error ? error.message : String(error);
815
- ctx.ui.notify(`Could not save selection-copy setting: ${message}`, "error");
836
+ ctx.ui.notify(`Could not save ${copySetting[1]} setting: ${message}`, "error");
816
837
  return;
817
838
  }
818
- ctx.ui.notify(`Border-free selection copying ${selectionCopy ? "enabled" : "disabled"}; reloading…`, "info");
839
+ const label = key === "messageCopy" ? "Message copying"
840
+ : key === "codeBlockCopy" ? "Code-block copying" : "Border-free selection copying";
841
+ ctx.ui.notify(`${label} ${enabled ? "enabled" : "disabled"}; reloading…`, "info");
819
842
  try { await ctx.reload(); }
820
843
  catch (error) {
821
844
  const message = error instanceof Error ? error.message : String(error);
@@ -846,7 +869,7 @@ export default function prettyTui(pi: ExtensionAPI) {
846
869
  }
847
870
  }
848
871
  if (requested !== "full" && requested !== "compact" && requested !== "clean") {
849
- ctx.ui.notify("Usage: /pretty-tui [enable|disable|full|compact|clean|status|selection-copy on|selection-copy off]", "error");
872
+ ctx.ui.notify("Usage: /pretty-tui [enable|disable|full|compact|clean|status|message-copy on|message-copy off|code-block-copy on|code-block-copy off|selection-copy on|selection-copy off]", "error");
850
873
  return;
851
874
  }
852
875
 
@@ -1122,7 +1145,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1122
1145
  }));
1123
1146
  if (result?.handled) return result;
1124
1147
  }
1125
- if (fullscreenTui && event.type === "click" && event.button === "left" && userText.trim()) {
1148
+ if (messageCopyEnabled && fullscreenTui && event.type === "click" && event.button === "left" && userText.trim()) {
1126
1149
  copyTranscriptText(userText);
1127
1150
  return { handled: true };
1128
1151
  }
@@ -1134,8 +1157,17 @@ export default function prettyTui(pi: ExtensionAPI) {
1134
1157
  };
1135
1158
 
1136
1159
  const bubbleClickHitTest = (event: any) => {
1137
- if (!fullscreenTui || !userText.trim()) return false;
1160
+ if (!fullscreenTui) return false;
1138
1161
  const measured = layout(event.width);
1162
+ if (!messageCopyEnabled || !userText.trim()) {
1163
+ return mouseClickHitTests.get(markdown)?.({
1164
+ ...event,
1165
+ x: event.x - measured.leftPad - (measured.framed ? 2 : 0),
1166
+ y: event.y - (measured.framed ? 1 : 0),
1167
+ width: measured.contentWidth,
1168
+ height: measured.body.length,
1169
+ }) ?? false;
1170
+ }
1139
1171
  return event.x >= measured.leftPad && event.x < measured.leftPad + measured.frameWidth &&
1140
1172
  event.y >= 0 && event.y < measured.body.length + (measured.framed ? 2 : 0);
1141
1173
  };
@@ -2420,7 +2452,7 @@ export default function prettyTui(pi: ExtensionAPI) {
2420
2452
 
2421
2453
  const patchedRender = function (this: any, width: number): string[] {
2422
2454
  const scoped = transcriptMarkdownActive();
2423
- mouseClickHitTests.set(this, (event) => fullscreenTui &&
2455
+ mouseClickHitTests.set(this, (event) => codeBlockCopyEnabled && fullscreenTui &&
2424
2456
  this[transcriptMarkdownStateKey] === true &&
2425
2457
  (this[codeBlockRegionsKey] ?? []).some((region: any) =>
2426
2458
  event.y === region.y && event.x >= region.xStart && event.x < region.xEnd));
@@ -2576,7 +2608,7 @@ export default function prettyTui(pi: ExtensionAPI) {
2576
2608
  }
2577
2609
 
2578
2610
  const copyLabel = "[Copy]";
2579
- const showCopyButton = fullscreenTui && maxWidth >= 18;
2611
+ const showCopyButton = codeBlockCopyEnabled && fullscreenTui && maxWidth >= 18;
2580
2612
  const topSuffix = showCopyButton ? ` ${copyLabel} ╮` : "╮";
2581
2613
  const rawLanguage = typeof token.lang === "string" ? token.lang.trim() : "";
2582
2614
  const language = rawLanguage.split(/\s+/, 1)[0] || "code";
@@ -2648,7 +2680,7 @@ export default function prettyTui(pi: ExtensionAPI) {
2648
2680
  delete this[codeBlockRegionsKey];
2649
2681
  return originalHandleMouse?.call(this, event);
2650
2682
  }
2651
- if (event.button === "left" && (event.type === "press" || event.type === "click")) {
2683
+ if (codeBlockCopyEnabled && fullscreenTui && event.button === "left" && (event.type === "press" || event.type === "click")) {
2652
2684
  const region = (this[codeBlockRegionsKey] ?? []).find(
2653
2685
  (candidate: any) =>
2654
2686
  event.y === candidate.y && event.x >= candidate.xStart && event.x < candidate.xEnd,
@@ -2828,8 +2860,22 @@ export default function prettyTui(pi: ExtensionAPI) {
2828
2860
 
2829
2861
  const paint = (line: string) => row.background
2830
2862
  ? 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));
2863
+ const corner = renderMode === "clean" ? stripTerminalSequences(prefix).indexOf("└") : -1;
2864
+ if (corner >= 0 && wrapped.length > 1) {
2865
+ // Close the branch on the final physical row, not the first row of
2866
+ // a logical source line that may wrap or contain explicit newlines.
2867
+ const column = visibleWidth(stripTerminalSequences(prefix).slice(0, corner));
2868
+ const guide = (symbol: string) => sliceByColumn(continuation, 0, column) +
2869
+ defaultActivityTheme().fg("dim", symbol) +
2870
+ sliceByColumn(continuation, column + 1, Math.max(0, visibleWidth(continuation) - column - 1));
2871
+ rendered.push(prefix.replace("└", "│") + paint(wrapped[0] ?? ""));
2872
+ for (let index = 1; index < wrapped.length; index++) {
2873
+ rendered.push(guide(index === wrapped.length - 1 ? "└" : "│") + paint(wrapped[index]));
2874
+ }
2875
+ } else {
2876
+ rendered.push(prefix + paint(wrapped[0] ?? ""));
2877
+ for (const line of wrapped.slice(1)) rendered.push(continuation + paint(line));
2878
+ }
2833
2879
  }
2834
2880
 
2835
2881
  return rendered;
@@ -2850,7 +2896,8 @@ export default function prettyTui(pi: ExtensionAPI) {
2850
2896
  if (renderMode === "compact") return name === "bash" ? theme.fg("toolTitle", "$ ") : "";
2851
2897
  return theme.fg(color, "● ");
2852
2898
  },
2853
- continuation: renderMode === "compact" && name !== "bash" ? "" : " ",
2899
+ continuation: renderMode === "clean" && expanded ? theme.fg("dim", "│ ") :
2900
+ renderMode === "compact" && name !== "bash" ? "" : " ",
2854
2901
  content: renderMode === "compact" ? theme.fg("muted", detail) :
2855
2902
  theme.fg("text", theme.bold(name)) +
2856
2903
  theme.fg("dim", "(") +
@@ -3569,7 +3616,7 @@ export default function prettyTui(pi: ExtensionAPI) {
3569
3616
  const dimPhase = responseRunStartedAt !== undefined &&
3570
3617
  Math.floor(Math.max(0, Date.now() - responseRunStartedAt) / RUNNING_DOT_INTERVAL_MS) % 2 === 1;
3571
3618
  const color = currentActivity === "done"
3572
- ? collapsedDone ? "dim" : "success"
3619
+ ? collapsedDone ? "dim" : "text"
3573
3620
  : dimPhase ? "dim" : "accent";
3574
3621
  return currentActivity !== "done" && !dimPhase
3575
3622
  ? runningColor(theme, "● ")
@@ -3580,7 +3627,7 @@ export default function prettyTui(pi: ExtensionAPI) {
3580
3627
  const currentActivity = typeof activity === "function" ? activity() : activity;
3581
3628
  const label = currentActivity === "done" ? "Done" : "Running";
3582
3629
  const color = label === "Done"
3583
- ? collapsedDone ? "dim" : "success"
3630
+ ? collapsedDone ? "dim" : "text"
3584
3631
  : "accent";
3585
3632
  const detailColor = label === "Done" && collapsedDone ? "dim" : "text";
3586
3633
  const title = label === "Running"
@@ -3787,14 +3834,14 @@ export default function prettyTui(pi: ExtensionAPI) {
3787
3834
  return {
3788
3835
  render(width: number): string[] {
3789
3836
  const data = entry.data;
3790
- mouseClickHitTests.set(this, (event) => fullscreenTui && copyStart >= 0 &&
3837
+ mouseClickHitTests.set(this, (event) => messageCopyEnabled && fullscreenTui && copyStart >= 0 &&
3791
3838
  event.y === 0 && event.x >= copyStart && event.x < copyStart + copyWidth);
3792
3839
  if (!data || width <= 0) return [];
3793
3840
  copyStart = -1;
3794
3841
  copyWidth = 0;
3795
3842
  const answer = data.answerEntryId ? responseAnswerTexts.get(data.answerEntryId) ?? "" : "";
3796
3843
  const copyLabel = "[Copy]";
3797
- const showCopy = fullscreenTui && Boolean(answer.trim()) && width >= 30;
3844
+ const showCopy = messageCopyEnabled && fullscreenTui && Boolean(answer.trim()) && width >= 30;
3798
3845
  const duration = formatResponseDuration(data.durationMs);
3799
3846
  const statusText = data.outcome === "stopped"
3800
3847
  ? `⚠ Stopped after ${duration}`
@@ -3820,7 +3867,7 @@ export default function prettyTui(pi: ExtensionAPI) {
3820
3867
  },
3821
3868
  handleMouse(event: TuiMouseEvent) {
3822
3869
  if (
3823
- copyStart < 0 ||
3870
+ !messageCopyEnabled || !fullscreenTui || copyStart < 0 ||
3824
3871
  event.button !== "left" ||
3825
3872
  (event.type !== "press" && event.type !== "click") ||
3826
3873
  event.y !== 0 ||
@@ -4652,7 +4699,7 @@ export default function prettyTui(pi: ExtensionAPI) {
4652
4699
  if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, writeLabel, context.toolCallId);
4653
4700
  const content = typeof args.content === "string" ? args.content : "";
4654
4701
  const path = String(args.path ?? "");
4655
- if (useCompactToolView(context.toolCallId, context.expanded)) {
4702
+ if (renderMode === "clean" || useCompactToolView(context.toolCallId, context.expanded)) {
4656
4703
  const lines = content.replace(/\t/g, " ").split("\n");
4657
4704
  while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
4658
4705
  const total = lines.length;
@@ -4665,6 +4712,33 @@ export default function prettyTui(pi: ExtensionAPI) {
4665
4712
  if (hideCleanTool(context.toolCallId, options.expanded, context.executionStarted)) return hiddenToolResult(context, options, output);
4666
4713
  if (options.isPartial) return partialResult(context, theme, "Writing…");
4667
4714
  const failed = completeStatus(context);
4715
+ if (renderMode === "clean") {
4716
+ if (failed) return result(theme, "Write failed", output, options.expanded, true);
4717
+ const status = output ? `Written · ${output}` : "Written";
4718
+ const rows: DisplayRow[] = [{
4719
+ prefix: theme.fg("dim", "└ "), continuation: " ",
4720
+ content: theme.fg("toolOutput", status),
4721
+ }];
4722
+ if (options.expanded) {
4723
+ const content = typeof context.args?.content === "string" ? context.args.content : "";
4724
+ const source = content.replace(/\r\n|\r/g, "\n").replace(/\t/g, " ").split("\n");
4725
+ // A final newline terminates the last line; additional blank lines
4726
+ // are actual file content and retain their own line numbers.
4727
+ if (source.at(-1) === "") source.pop();
4728
+ let highlighted: string[];
4729
+ try { highlighted = highlightCode(source.join("\n"), getLanguageFromPath(String(context.args?.path ?? ""))); }
4730
+ catch { highlighted = source; }
4731
+ const digits = String(source.length).length;
4732
+ for (let index = 0; index < source.length; index++) {
4733
+ rows.push({
4734
+ prefix: theme.fg("dim", index === source.length - 1 ? " └ " : " │ ") + theme.fg("dim", `${String(index + 1).padStart(digits)} `),
4735
+ continuation: (index === source.length - 1 ? " " : theme.fg("dim", " │ ")) + " ".repeat(digits + 1),
4736
+ content: highlighted[index] ?? source[index],
4737
+ });
4738
+ }
4739
+ }
4740
+ return block(rows);
4741
+ }
4668
4742
  return result(
4669
4743
  theme,
4670
4744
  failed ? output.split("\n")[0] || "Write failed" : "Written",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-pretty-tui",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
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>",
@@ -45,8 +45,8 @@
45
45
  "@earendil-works/pi-tui": "*"
46
46
  },
47
47
  "devDependencies": {
48
- "@earendil-works/pi-coding-agent": "1.5.0",
49
- "@earendil-works/pi-tui": "1.5.0",
48
+ "@earendil-works/pi-coding-agent": "1.5.1",
49
+ "@earendil-works/pi-tui": "1.5.1",
50
50
  "@types/node": "^24.0.0",
51
51
  "jiti": "^2.7.0",
52
52
  "typescript": "^5.9.0"