pi-pretty-tui 0.3.5 → 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)。
@@ -74,6 +74,8 @@ const RESPONSE_FOOTER_ENTRY_TYPE = "pretty-tui-response-footer";
74
74
  type PrettyTuiConfig = {
75
75
  enabled?: boolean;
76
76
  mode?: PrettyTuiMode;
77
+ messageCopy?: boolean;
78
+ codeBlockCopy?: boolean;
77
79
  /** Exclude plugin decoration from fullscreen selections. Opt-in. */
78
80
  selectionCopy?: boolean;
79
81
  /** "native" keeps Pi's flags-only strong and emphasis. Read when the extension loads. */
@@ -117,6 +119,8 @@ export default function prettyTui(pi: ExtensionAPI) {
117
119
  const configPath = join(getAgentDir(), "pretty-tui.json");
118
120
  let config = loadConfig(configPath);
119
121
  const activeForSession = config.enabled !== false;
122
+ const messageCopyEnabled = config.messageCopy !== false;
123
+ const codeBlockCopyEnabled = config.codeBlockCopy !== false;
120
124
  const configuredMode = config.mode ?? config.bash?.mode;
121
125
  let renderMode: PrettyTuiMode = configuredMode === "full" || configuredMode === "compact" || configuredMode === "clean"
122
126
  ? configuredMode
@@ -766,6 +770,10 @@ export default function prettyTui(pi: ExtensionAPI) {
766
770
  { value: "full", label: "full", description: "Full tool details and output" },
767
771
  { value: "compact", label: "compact", description: "Concise summaries for all built-in tools" },
768
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" },
769
777
  { value: "selection-copy on", label: "selection-copy on", description: "Copy selected text without UI borders in fullscreen mode; enable and reload" },
770
778
  { value: "selection-copy off", label: "selection-copy off", description: "Restore native selection copying and reload" },
771
779
  { value: "status", label: "status", description: "Show the current settings" },
@@ -775,25 +783,32 @@ export default function prettyTui(pi: ExtensionAPI) {
775
783
  },
776
784
  handler: async (args, ctx) => {
777
785
  let requested = args.trim().toLowerCase();
786
+ if (!requested && !ctx.hasUI) requested = "status";
778
787
 
779
- if (!requested) {
780
- if (!ctx.hasUI) {
781
- ctx.ui.notify(
782
- `pi-pretty-tui: ${config.enabled !== false ? "enabled" : "disabled"} · mode: ${renderMode} · selection copy: ${config.selectionCopy === true ? "on" : "off"}`,
783
- "info",
784
- );
785
- return;
786
- }
788
+ while (!requested) {
787
789
  const enabled = `Enabled — ${config.enabled !== false ? "on" : "off"}`;
788
790
  const full = `Full — full tool details and output${renderMode === "full" ? " (current)" : ""}`;
789
791
  const compact = `Compact — concise tool summaries and nested calls${renderMode === "compact" ? " (current)" : ""}`;
790
792
  const clean = `Clean — group supported tools into Running/Done status${renderMode === "clean" ? " (current)" : ""}`;
791
793
  const selectionCopy = `Selection copy — ${config.selectionCopy === true ? "on" : "off"} · copy text without UI borders (fullscreen, experimental)`;
792
- 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]);
793
798
  if (!selected) return;
794
- requested = selected === selectionCopy
795
- ? config.selectionCopy === true ? "selection-copy off" : "selection-copy on"
796
- : 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
797
812
  ? config.enabled !== false ? "disable" : "enable"
798
813
  : selected === full
799
814
  ? "full"
@@ -804,21 +819,26 @@ export default function prettyTui(pi: ExtensionAPI) {
804
819
 
805
820
  if (requested === "status") {
806
821
  ctx.ui.notify(
807
- `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"}`,
808
823
  "info",
809
824
  );
810
825
  return;
811
826
  }
812
- if (requested === "selection-copy on" || requested === "selection-copy off") {
813
- 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";
814
832
  try {
815
- saveConfig({ ...config, selectionCopy });
833
+ saveConfig({ ...config, [key]: enabled });
816
834
  } catch (error) {
817
835
  const message = error instanceof Error ? error.message : String(error);
818
- ctx.ui.notify(`Could not save selection-copy setting: ${message}`, "error");
836
+ ctx.ui.notify(`Could not save ${copySetting[1]} setting: ${message}`, "error");
819
837
  return;
820
838
  }
821
- 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");
822
842
  try { await ctx.reload(); }
823
843
  catch (error) {
824
844
  const message = error instanceof Error ? error.message : String(error);
@@ -849,7 +869,7 @@ export default function prettyTui(pi: ExtensionAPI) {
849
869
  }
850
870
  }
851
871
  if (requested !== "full" && requested !== "compact" && requested !== "clean") {
852
- 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");
853
873
  return;
854
874
  }
855
875
 
@@ -1125,7 +1145,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1125
1145
  }));
1126
1146
  if (result?.handled) return result;
1127
1147
  }
1128
- if (fullscreenTui && event.type === "click" && event.button === "left" && userText.trim()) {
1148
+ if (messageCopyEnabled && fullscreenTui && event.type === "click" && event.button === "left" && userText.trim()) {
1129
1149
  copyTranscriptText(userText);
1130
1150
  return { handled: true };
1131
1151
  }
@@ -1137,8 +1157,17 @@ export default function prettyTui(pi: ExtensionAPI) {
1137
1157
  };
1138
1158
 
1139
1159
  const bubbleClickHitTest = (event: any) => {
1140
- if (!fullscreenTui || !userText.trim()) return false;
1160
+ if (!fullscreenTui) return false;
1141
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
+ }
1142
1171
  return event.x >= measured.leftPad && event.x < measured.leftPad + measured.frameWidth &&
1143
1172
  event.y >= 0 && event.y < measured.body.length + (measured.framed ? 2 : 0);
1144
1173
  };
@@ -2423,7 +2452,7 @@ export default function prettyTui(pi: ExtensionAPI) {
2423
2452
 
2424
2453
  const patchedRender = function (this: any, width: number): string[] {
2425
2454
  const scoped = transcriptMarkdownActive();
2426
- mouseClickHitTests.set(this, (event) => fullscreenTui &&
2455
+ mouseClickHitTests.set(this, (event) => codeBlockCopyEnabled && fullscreenTui &&
2427
2456
  this[transcriptMarkdownStateKey] === true &&
2428
2457
  (this[codeBlockRegionsKey] ?? []).some((region: any) =>
2429
2458
  event.y === region.y && event.x >= region.xStart && event.x < region.xEnd));
@@ -2579,7 +2608,7 @@ export default function prettyTui(pi: ExtensionAPI) {
2579
2608
  }
2580
2609
 
2581
2610
  const copyLabel = "[Copy]";
2582
- const showCopyButton = fullscreenTui && maxWidth >= 18;
2611
+ const showCopyButton = codeBlockCopyEnabled && fullscreenTui && maxWidth >= 18;
2583
2612
  const topSuffix = showCopyButton ? ` ${copyLabel} ╮` : "╮";
2584
2613
  const rawLanguage = typeof token.lang === "string" ? token.lang.trim() : "";
2585
2614
  const language = rawLanguage.split(/\s+/, 1)[0] || "code";
@@ -2651,7 +2680,7 @@ export default function prettyTui(pi: ExtensionAPI) {
2651
2680
  delete this[codeBlockRegionsKey];
2652
2681
  return originalHandleMouse?.call(this, event);
2653
2682
  }
2654
- if (event.button === "left" && (event.type === "press" || event.type === "click")) {
2683
+ if (codeBlockCopyEnabled && fullscreenTui && event.button === "left" && (event.type === "press" || event.type === "click")) {
2655
2684
  const region = (this[codeBlockRegionsKey] ?? []).find(
2656
2685
  (candidate: any) =>
2657
2686
  event.y === candidate.y && event.x >= candidate.xStart && event.x < candidate.xEnd,
@@ -3805,14 +3834,14 @@ export default function prettyTui(pi: ExtensionAPI) {
3805
3834
  return {
3806
3835
  render(width: number): string[] {
3807
3836
  const data = entry.data;
3808
- mouseClickHitTests.set(this, (event) => fullscreenTui && copyStart >= 0 &&
3837
+ mouseClickHitTests.set(this, (event) => messageCopyEnabled && fullscreenTui && copyStart >= 0 &&
3809
3838
  event.y === 0 && event.x >= copyStart && event.x < copyStart + copyWidth);
3810
3839
  if (!data || width <= 0) return [];
3811
3840
  copyStart = -1;
3812
3841
  copyWidth = 0;
3813
3842
  const answer = data.answerEntryId ? responseAnswerTexts.get(data.answerEntryId) ?? "" : "";
3814
3843
  const copyLabel = "[Copy]";
3815
- const showCopy = fullscreenTui && Boolean(answer.trim()) && width >= 30;
3844
+ const showCopy = messageCopyEnabled && fullscreenTui && Boolean(answer.trim()) && width >= 30;
3816
3845
  const duration = formatResponseDuration(data.durationMs);
3817
3846
  const statusText = data.outcome === "stopped"
3818
3847
  ? `⚠ Stopped after ${duration}`
@@ -3838,7 +3867,7 @@ export default function prettyTui(pi: ExtensionAPI) {
3838
3867
  },
3839
3868
  handleMouse(event: TuiMouseEvent) {
3840
3869
  if (
3841
- copyStart < 0 ||
3870
+ !messageCopyEnabled || !fullscreenTui || copyStart < 0 ||
3842
3871
  event.button !== "left" ||
3843
3872
  (event.type !== "press" && event.type !== "click") ||
3844
3873
  event.y !== 0 ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-pretty-tui",
3
- "version": "0.3.5",
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"