@runminton/pi-tidy-tools 0.1.3 → 0.2.1

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
@@ -2,31 +2,46 @@
2
2
 
3
3
  A tidy tool-rendering extension for [Pi](https://github.com/earendil-works/pi-mono) (the pi-coding-agent TUI). It overrides the rendering of `bash`, `edit` and `write`: compact summaries by default, full official rendering when you need it.
4
4
 
5
+ > Display layer only. Tool execution is fully delegated to Pi's official implementations — parameters, behavior and results are untouched; the extension only decides how many rows to draw and in which colors.
6
+
5
7
  [中文版](README.zh-CN.md)
6
8
 
7
9
  ## Default view
8
10
 
9
- Each tool starts in one of two states:
11
+ All three tools start **Collapsed**:
10
12
 
11
- | Tool | Default | What you see |
13
+ | Tool | Collapsed summary | Expanded |
12
14
  |---|---|---|
13
- | `bash` | **Collapsed** | `$ command`, one output line, and `... (N more lines)` if there is more output |
14
- | `edit` | **Expanded** | Pi's official renderer: path, block count, live diff preview, native layout |
15
- | `write` | **Expanded** | Pi's official renderer: path, line count, syntax-highlighted content |
15
+ | `bash` | `$ command` plus one output line, with an inline `... (N more lines)` suffix when truncated | complete output |
16
+ | `edit` | `edit path (N blocks)` + `applied +a -b` | Pi's official renderer: path, block count, live diff preview, native layout |
17
+ | `write` | `write path (N lines)` + `done` | Pi's official renderer: path, line count, syntax-highlighted content |
18
+
19
+ Collapsed summaries are single-line per slot: one command line plus one output line (the count suffix is inline at the end of the output line).
16
20
 
17
- Collapsed summaries are single-line (bash shows one preview line plus a count row).
21
+ In Pi's fullscreen mode, left-click any tool block to toggle that row between its compact summary and full rendering. Click it again to collapse it. Each tool has only these two states; `bash` and `write` skip Pi's intermediate previews.
22
+
23
+ ## Modes
24
+
25
+ | Mode | Behavior |
26
+ |---|---|
27
+ | compact (default) | all three tools collapsed |
28
+ | markdown (`/tidy-markdown` toggles) | `edit`/`write` on Markdown files (`.md`, `.mdx`, `.markdown`) expand to full content; everything else stays collapsed |
29
+
30
+ Switching modes clears shortcut state and row-local click state, then re-renders existing rows. The mode is not persisted across restarts.
18
31
 
19
32
  ## Shortcuts
20
33
 
21
- | Shortcut | Tool | Toggles between |
34
+ On Mac press `Command+Option+letter` (verified; maps to the registered `Ctrl+Alt+letter`. Other platforms press `Ctrl+Alt+letter`):
35
+
36
+ | Shortcut (Mac) | Tool | Toggles between |
22
37
  |---|---|---|
23
- | `Ctrl+Alt+B` | `bash` | collapsed summary ↔ **one-key full output** |
24
- | `Ctrl+Alt+E` | `edit` | official rendering ↔ collapsed summary |
25
- | `Ctrl+Alt+W` | `write` | official rendering ↔ collapsed summary |
38
+ | `Command+Option+B` | `bash` | collapsed summary ↔ **one-key full output** |
39
+ | `Command+Option+E` | `edit` | official rendering ↔ collapsed summary |
40
+ | `Command+Option+W` | `write` | official rendering ↔ collapsed summary |
26
41
 
27
- A notification shows the current state on each toggle; press again to switch back.
42
+ A shortcut toggles all existing rows of that tool type and clears their prior row-local click state. A notification shows the new state; press again to switch back.
28
43
 
29
- `bash` expands to the **complete output** in one keypress — it skips Pi's built-in 5-line tail preview. `edit` and `write` expand to Pi's **official default rendering**: they delegate to the official renderers, and whether the content itself is further expanded is still controlled by the global `Ctrl+O`, exactly as without this extension. Only `bash` ignores `Ctrl+O` in its expanded state.
44
+ `bash` and `write` expand to the **complete output** in one keypress — they skip Pi's built-in previews (bash's 5-line tail preview, write's 10-line preview). `edit` expands to Pi's **official rendering** (it has no preview state). The global `Ctrl+O` tool expansion does not affect these three tools.
30
45
 
31
46
  ## Commands
32
47
 
@@ -37,6 +52,7 @@ Use these commands when terminal shortcuts conflict or are not delivered:
37
52
  | `/tidy-bash` | Toggle `bash` output |
38
53
  | `/tidy-edit` | Toggle `edit` output |
39
54
  | `/tidy-write` | Toggle `write` output |
55
+ | `/tidy-markdown` | Toggle markdown mode (edit/write expand for Markdown files) |
40
56
 
41
57
  ## Usage
42
58
 
@@ -73,6 +89,26 @@ If a shortcut still does nothing on your machine, check:
73
89
  3. **Custom keys.** The bindings are registered in `tidy-tools.ts` via `pi.registerShortcut(...)` — edit the key strings there to rebind. Pi's built-in global expansion (`Ctrl+O`, `app.tools.expand`) is configured separately via `~/.pi/agent/keybindings.json`.
74
90
  4. **Inspect the input.** Run `/tidy-key-debug`, then press one target shortcut. Pi shows the received bytes and any rewrite. If no notification appears, the terminal or input method consumed the key before Pi received it.
75
91
 
92
+ ## Changelog
93
+
94
+ ### 0.2.1
95
+
96
+ - Added per-block fullscreen clicking for bash/edit/write, toggling between compact and full rendering
97
+ - Kept tool-wide shortcuts; a shortcut applies one state to all rows of that tool type
98
+
99
+ ### 0.2.0
100
+
101
+ - All three tools collapsed by default (edit/write used to expand)
102
+ - New `/tidy-markdown`: edit/write on Markdown files auto-expand to full content, the rest stays collapsed; switching modes clears manual toggles
103
+ - Write expansion skips the official 10-line preview and shows full content; global `Ctrl+O` no longer affects any of the three tools
104
+ - Bash collapsed output is 2 rows with the count suffix inline, including error output
105
+ - Fixed tab-width overflow wrapping collapsed bash output on narrow terminals; fixed doubled edit padding from a nested render shell
106
+
107
+ ### 0.1.3
108
+
109
+ - New `/tidy-bash`, `/tidy-edit`, `/tidy-write`, `/tidy-key-debug` commands
110
+ - Normalized the legacy `ESC` + control-character shortcut form
111
+
76
112
  ## Development
77
113
 
78
114
  ```bash
package/README.zh-CN.md CHANGED
@@ -2,31 +2,46 @@
2
2
 
3
3
  为 [Pi](https://github.com/earendil-works/pi-mono)(pi-coding-agent 的 TUI)打造的工具渲染精简扩展。它覆盖了 `bash`、`edit`、`write` 的渲染:默认呈现紧凑摘要,需要时切换为官方完整渲染。
4
4
 
5
+ > 只动显示层,不动执行层。三个工具的执行完全委托 Pi 官方实现,参数、行为、结果一字不差;插件只决定屏幕上画几行、用什么颜色。
6
+
5
7
  [English](README.md)
6
8
 
7
9
  ## 默认视图
8
10
 
9
- 每个工具启动时处于两种状态之一:
11
+ 三个工具默认**全部折叠**:
10
12
 
11
- | 工具 | 默认状态 | 显示内容 |
13
+ | 工具 | 折叠摘要 | 展开 |
12
14
  |---|---|---|
13
- | `bash` | **折叠** | `$ command`、一行输出预览、有更多输出时加 `... (N more lines)` 计数行 |
14
- | `edit` | **展开** | Pi 官方渲染:路径、修改块数、实时 diff 预览、原生布局 |
15
- | `write` | **展开** | Pi 官方渲染:路径、行数、语法高亮内容 |
15
+ | `bash` | `$ command` 加一行输出预览,计数后缀 `... (N more lines)` 内联在行尾 | 完整输出 |
16
+ | `edit` | `edit 路径(N blocks)` + `applied +a -b` | Pi 官方渲染:路径、修改块数、实时 diff 预览、原生布局 |
17
+ | `write` | `write 路径(N lines)` + `done` | Pi 官方渲染:路径、行数、语法高亮内容 |
18
+
19
+ 折叠摘要每槽单行:命令 1 行加输出 1 行(计数后缀内联在输出行尾)。
16
20
 
17
- 折叠摘要为单行样式(bash 额外保留一行输出预览 + 一行计数)。
21
+ 在 Pi 的 fullscreen 模式下,左键点击任意一个工具块,可单独在紧凑摘要和完整渲染之间切换。再次点击即收起。三个工具都只有这两种状态;`bash` `write` 不经过官方中间预览。
22
+
23
+ ## 模式
24
+
25
+ | 模式 | 行为 |
26
+ |---|---|
27
+ | compact(默认) | 三工具全折叠 |
28
+ | markdown(`/tidy-markdown` 切换) | Markdown 文件(`.md`、`.mdx`、`.markdown`)的 `edit`/`write` 展开全文,其余照样折叠 |
29
+
30
+ 切换模式会清空快捷键状态和各工具块的点击状态,并重渲染已有行。模式不持久化,重启后回到 compact。
18
31
 
19
32
  ## 快捷键
20
33
 
21
- | 快捷键 | 工具 | 切换内容 |
34
+ Mac 上按 `Command+Option+字母`(实测有效,对应插件注册的 `Ctrl+Alt+字母`;其他平台按 `Ctrl+Alt+字母`):
35
+
36
+ | 快捷键(Mac) | 工具 | 切换内容 |
22
37
  |---|---|---|
23
- | `Ctrl+Alt+B` | `bash` | 折叠摘要 ↔ **一键完整输出** |
24
- | `Ctrl+Alt+E` | `edit` | 官方渲染 ↔ 折叠摘要 |
25
- | `Ctrl+Alt+W` | `write` | 官方渲染 ↔ 折叠摘要 |
38
+ | `Command+Option+B` | `bash` | 折叠摘要 ↔ **一键完整输出** |
39
+ | `Command+Option+E` | `edit` | 官方渲染 ↔ 折叠摘要 |
40
+ | `Command+Option+W` | `write` | 官方渲染 ↔ 折叠摘要 |
26
41
 
27
- 每次切换会弹出通知提示当前状态;再按一次切回。
42
+ 快捷键按工具类型切换所有已有工具块,并清除该工具之前的逐块点击状态。每次切换会弹出通知;再按一次切回。
28
43
 
29
- `bash` 展开时**一次按键直接显示完整输出**——跳过 Pi 内置的 5 行尾部预览。`edit` `write` 展开时使用 Pi **官方默认渲染**:委托给官方渲染器,内容本身是否进一步展开仍由全局 `Ctrl+O` 控制,与不装本扩展时完全一致。只有 `bash` 在展开状态下不受 `Ctrl+O` 影响。
44
+ `bash` 和 `write` 展开时**一次按键直接显示完整输出**——跳过 Pi 内置预览(bash 5 行尾部预览、write 10 行预览)。`edit` 展开时使用 Pi **官方渲染**(它没有预览态)。全局 `Ctrl+O` 对这三个工具均无效。
30
45
 
31
46
  ## 命令
32
47
 
@@ -37,6 +52,7 @@
37
52
  | `/tidy-bash` | 切换 `bash` 输出 |
38
53
  | `/tidy-edit` | 切换 `edit` 输出 |
39
54
  | `/tidy-write` | 切换 `write` 输出 |
55
+ | `/tidy-markdown` | 切换 Markdown 模式(Markdown 文件的 edit/write 展开) |
40
56
 
41
57
  ## 使用方法
42
58
 
@@ -73,6 +89,26 @@ Pi 会在终端支持时协商 [Kitty 键盘协议](https://sw.kovidgoyal.net/ki
73
89
  3. **自定义按键。** 绑定在 `tidy-tools.ts` 里通过 `pi.registerShortcut(...)` 注册——改那里的按键字符串即可重新绑定。Pi 内置的全局展开(`Ctrl+O`,即 `app.tools.expand`)则单独通过 `~/.pi/agent/keybindings.json` 配置。
74
90
  4. **检查输入。** 执行 `/tidy-key-debug`,再按一次目标快捷键。Pi 会显示收到的字节和改写结果。若没有通知,终端或输入法在 Pi 收到按键前已拦截它。
75
91
 
92
+ ## 更新日志
93
+
94
+ ### 0.2.1
95
+
96
+ - 支持在 Pi fullscreen 模式下点击单个 bash/edit/write 工具块,在紧凑摘要和完整渲染之间切换
97
+ - 保留工具级快捷键;快捷键会统一设置同类工具块
98
+
99
+ ### 0.2.0 - 2026-09-05
100
+
101
+ - 默认三工具全折叠(edit/write 原默认展开)
102
+ - 新增 `/tidy-markdown`:Markdown 文件的 edit/write 自动展开全文,其余照样折叠;切模式清空手动钉选
103
+ - write 展开跳过官方 10 行预览,直接全文;全局 `Ctrl+O` 对三工具失效
104
+ - bash 折叠压成 2 行:计数后缀内联到输出行尾,失败输出(红色)同样单行
105
+ - 修 tab 宽度导致的窄终端折行;修 edit 双层外框多出的 padding
106
+
107
+ ### 0.1.3
108
+
109
+ - 新增 `/tidy-bash`、`/tidy-edit`、`/tidy-write`、`/tidy-key-debug` 命令
110
+ - 修传统 `ESC` + 控制字符形式的快捷键归一化
111
+
76
112
  ## 开发
77
113
 
78
114
  ```bash
package/package.json CHANGED
@@ -1,14 +1,13 @@
1
1
  {
2
2
  "name": "@runminton/pi-tidy-tools",
3
- "version": "0.1.3",
3
+ "version": "0.2.1",
4
4
  "description": "Collapse bash/edit/write into one-line summaries in the pi TUI; expand the official rendering per tool with Ctrl+Alt+B/W/E.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "files": [
8
8
  "tidy-tools.ts",
9
9
  "README.md",
10
- "README.zh-CN.md",
11
- "CHANGELOG.md"
10
+ "README.zh-CN.md"
12
11
  ],
13
12
  "pi": {
14
13
  "extensions": [
package/tidy-tools.ts CHANGED
@@ -4,9 +4,13 @@
4
4
  * Overrides the TUI rendering of bash, edit and write while delegating
5
5
  * execution to the official implementations:
6
6
  * - Collapsed: show a small summary with an ellipsis for long content.
7
- * - Expanded: Ctrl+Alt+E (edit), Ctrl+Alt+W (write), Ctrl+Alt+B (bash)
8
- * toggle each tool between collapsed and fully expanded rendering.
7
+ * - Expanded: click an individual tool block in fullscreen mode, or use
8
+ * Ctrl+Alt+E (edit), Ctrl+Alt+W (write), Ctrl+Alt+B (bash) for all rows
9
+ * of that tool, to toggle collapsed and fully expanded rendering.
9
10
  * - The global Ctrl+O tool expansion does not affect these three tools.
11
+ * - Modes: compact (default, all three collapsed) and markdown
12
+ * (`/tidy-markdown` toggles; edit/write on Markdown files expand,
13
+ * the rest stays collapsed). Not persisted across restarts.
10
14
  *
11
15
  * Usage:
12
16
  * pi -e ./tidy-tools.ts
@@ -23,6 +27,7 @@ import type { EditToolDetails } from "@earendil-works/pi-coding-agent";
23
27
  import {
24
28
  Box,
25
29
  Container,
30
+ MouseRegion,
26
31
  Text,
27
32
  type Component,
28
33
  sliceByColumn,
@@ -32,8 +37,8 @@ import {
32
37
 
33
38
  const MAX_COLLAPSED_COMMAND_LINES = 1;
34
39
  const MAX_COLLAPSED_CONTENT_LINES = 3;
35
- // bash 折叠输出:1 行预览 + 1 行计数提示(共 2 行)。
36
- const MAX_COLLAPSED_BASH_OUTPUT_LINES = 2;
40
+ // bash 折叠输出:预览与计数后缀合并为 1 行(命令 1 行 + 输出 1 行)。
41
+ const MAX_COLLAPSED_BASH_OUTPUT_LINES = 1;
37
42
 
38
43
  /**
39
44
  * Truncate rendered rows to at most maxLines, keeping the beginning.
@@ -65,6 +70,14 @@ function firstLine(text: string | undefined): string {
65
70
  return line.length > 80 ? `${line.slice(0, 77)}...` : line;
66
71
  }
67
72
 
73
+ // 终端 tab 按 8 的倍数对齐,pi-tui 按 3 列计算。不展开 tab 会导致
74
+ // 行宽估算小于终端实际渲染,窄终端下折成两行。与官方 replaceTabs
75
+ // 保持一致:tab 展开为 3 个空格。
76
+
77
+ function expandTabs(text: string): string {
78
+ return text.replace(/\t/g, " ");
79
+ }
80
+
68
81
  function errorText(result: { content: { type: string; text?: string }[] }): string {
69
82
  return result.content
70
83
  .filter((c) => c.type === "text")
@@ -119,6 +132,46 @@ class LimitedLinesText implements Component {
119
132
  }
120
133
  }
121
134
 
135
+ class CollapsedBashOutput implements Component {
136
+ // 首行预览 + 计数后缀合并为 1 行,后缀常显。预览放不下时截断并加省略标记。
137
+ constructor(
138
+ private readonly preview: string,
139
+ private readonly suffix: string,
140
+ private readonly truncatedMarker: string,
141
+ ) {}
142
+
143
+ render(width: number): string[] {
144
+ if (!this.suffix) {
145
+ return truncateRows(
146
+ wrapTextWithAnsi(this.preview, Math.max(1, width)),
147
+ MAX_COLLAPSED_BASH_OUTPUT_LINES,
148
+ this.truncatedMarker,
149
+ width,
150
+ );
151
+ }
152
+ const suffixWidth = visibleWidth(this.suffix);
153
+ if (suffixWidth >= width) {
154
+ return [sliceByColumn(this.suffix, 0, Math.max(1, width), true)];
155
+ }
156
+ const previewBudget = width - suffixWidth - 1;
157
+ if (visibleWidth(this.preview) <= previewBudget) {
158
+ return [`${this.preview} ${this.suffix}`];
159
+ }
160
+ const markerWidth = visibleWidth(this.truncatedMarker);
161
+ const preview = sliceByColumn(
162
+ this.preview,
163
+ 0,
164
+ Math.max(0, previewBudget - markerWidth),
165
+ true,
166
+ );
167
+ const row = `${preview}${this.truncatedMarker} ${this.suffix}`;
168
+ // 极窄宽度下省略标记本身都放不下时做最终钳制,保证恒为 1 行。
169
+ return [visibleWidth(row) > width ? sliceByColumn(row, 0, width, true) : row];
170
+ }
171
+
172
+ invalidate(): void {}
173
+ }
174
+
122
175
  class CollapsedToolShell extends Box {
123
176
  private callComponent?: Component;
124
177
  private resultComponent?: Component;
@@ -160,13 +213,66 @@ function getCollapsedBackground(isPartial: boolean, isError: boolean): string {
160
213
  return isError ? "toolErrorBg" : "toolSuccessBg";
161
214
  }
162
215
 
163
- // Per-tool expansion state, toggled by Ctrl+Alt+E/W/B. Independent of the
164
- // global tool-output expansion (Ctrl+O).
165
- const toolExpanded = new Map<string, boolean>([
166
- ["bash", false],
167
- ["edit", true],
168
- ["write", true],
169
- ]);
216
+ // 显示模式:compact 下三工具默认全折叠;markdown 模式下 edit/write 遇到
217
+ // Markdown 文件默认展开全文,其余照样折叠。默认 compact,不持久化。
218
+ let markdownMode = false;
219
+ // 手动钉选:Ctrl+Alt+? 或 /tidy-<tool> 设置,优先于模式默认;切换模式时清空。
220
+ const manualExpanded = new Map<string, boolean>();
221
+ // Per-tool revisions let a mode or tool-wide toggle replace row-local clicks
222
+ // without disturbing clicked rows belonging to the other tools.
223
+ const expansionRevisions = new Map<string, number>();
224
+
225
+ function isMarkdownPath(value: unknown): boolean {
226
+ if (typeof value !== "string") return false;
227
+ return /\.(md|mdx|markdown)$/i.test(value.trim());
228
+ }
229
+
230
+ function toolArgsMarkdown(args: any): boolean {
231
+ return isMarkdownPath(args?.path) || isMarkdownPath(args?.file_path);
232
+ }
233
+
234
+ // 不带文件上下文的工具级基准(用于翻转手动钉选):compact 下全 false;
235
+ // markdown 模式下 edit/write 为 true(具体到文件时再按后缀细化)。
236
+ function modeBaseExpanded(name: string): boolean {
237
+ if (!markdownMode) return false;
238
+ return name === "edit" || name === "write";
239
+ }
240
+
241
+ function modeDefaultExpanded(name: string, args: any): boolean {
242
+ if (!markdownMode) return false;
243
+ if (name === "edit" || name === "write") return toolArgsMarkdown(args);
244
+ return false;
245
+ }
246
+
247
+ interface RowExpansionState {
248
+ revision: number;
249
+ override?: boolean;
250
+ }
251
+
252
+ function getRowExpansionState(name: string, context: any): RowExpansionState {
253
+ const revision = expansionRevisions.get(name) ?? 0;
254
+ let state = context.state.tidyExpansion as RowExpansionState | undefined;
255
+ if (!state || state.revision !== revision) {
256
+ state = { revision };
257
+ context.state.tidyExpansion = state;
258
+ }
259
+ return state;
260
+ }
261
+
262
+ function isToolExpanded(name: string, args: any, context: any): boolean {
263
+ const rowState = getRowExpansionState(name, context);
264
+ return rowState.override ?? manualExpanded.get(name) ?? modeDefaultExpanded(name, args);
265
+ }
266
+
267
+ function makeToolClickable(component: Component, name: string, args: any, context: any): Component {
268
+ return new MouseRegion(component, (event) => {
269
+ if (event.type !== "click" || event.button !== "left") return undefined;
270
+ const rowState = getRowExpansionState(name, context);
271
+ rowState.override = !isToolExpanded(name, args, context);
272
+ context.invalidate();
273
+ return { handled: true };
274
+ });
275
+ }
170
276
 
171
277
  // Some terminals (Kitty keyboard protocol flag-1 mode) encode Ctrl+letter as
172
278
  // a CSI-u sequence using the control character code without a Ctrl modifier
@@ -201,14 +307,19 @@ function convertCtrlAltSequence(data: string): string | undefined {
201
307
  return `\x1b[${key.charCodeAt(0)};7${eventSuffix}u`; // 7 = Ctrl|Alt (1-indexed)
202
308
  }
203
309
 
204
- function toggleToolExpanded(ctx: ExtensionContext, name: string): void {
205
- toolExpanded.set(name, !(toolExpanded.get(name) ?? false));
310
+ function refreshToolRows(ctx: ExtensionContext): void {
206
311
  const globalExpanded = ctx.ui.getToolsExpanded();
207
312
  // Force every tool row to re-render. The global value is restored
208
313
  // immediately, so only the rows' renderers refresh, not the global state.
209
314
  ctx.ui.setToolsExpanded(!globalExpanded);
210
315
  ctx.ui.setToolsExpanded(globalExpanded);
211
- ctx.ui.notify(`${name}: ${toolExpanded.get(name) ? "expanded" : "collapsed"}`, "info");
316
+ }
317
+
318
+ function toggleToolExpanded(ctx: ExtensionContext, name: string): void {
319
+ manualExpanded.set(name, !(manualExpanded.get(name) ?? modeBaseExpanded(name)));
320
+ expansionRevisions.set(name, (expansionRevisions.get(name) ?? 0) + 1);
321
+ refreshToolRows(ctx);
322
+ ctx.ui.notify(`${name}: ${manualExpanded.get(name) ? "expanded" : "collapsed"}`, "info");
212
323
  }
213
324
 
214
325
  // Cache the official tool implementation by working directory, as pi may run
@@ -244,6 +355,9 @@ function registerCollapsibleTool(
244
355
  getDef: (cwd: string) => ToolDefinition<any, any, any>,
245
356
  collapsedCall: (args: any, theme: any) => Component,
246
357
  collapsedResult: (result: any, options: any, theme: any, context: any) => Component,
358
+ // 跳过官方预览态,展开即全文(bash 行为)。为 false 时保留官方默认视图,
359
+ // 其内部预览 / 全文仍由全局 Ctrl+O 决定。
360
+ forceFullExpansion = false,
247
361
  ) {
248
362
  const def = getDef(process.cwd());
249
363
  const usesSelfShell = def.renderShell === "self";
@@ -256,8 +370,11 @@ function registerCollapsibleTool(
256
370
  parameters: def.parameters,
257
371
  prepareArguments: def.prepareArguments,
258
372
  constrainedSampling: def.constrainedSampling,
259
- // Omit renderShell so Pi inherits each built-in tool's native shell.
260
- // In particular, edit uses "self" for its original expanded layout.
373
+ // Mirror the official renderShell. Omitting it falls back to
374
+ // "default", which double-wraps "self" tools like edit in an
375
+ // extra Box: one extra padding line on top and bottom when
376
+ // expanded, and a Box-in-Box when collapsed.
377
+ ...(usesSelfShell ? { renderShell: "self" as const } : {}),
261
378
 
262
379
  async execute(toolCallId, params, signal, onUpdate, ctx) {
263
380
  return getDef(ctx.cwd).execute(toolCallId, params, signal, onUpdate, ctx);
@@ -265,15 +382,22 @@ function registerCollapsibleTool(
265
382
 
266
383
  renderCall(args, theme, context) {
267
384
  const def = getDef(context.cwd);
268
- if (toolExpanded.get(def.name) && def.renderCall) {
269
- return def.renderCall(
385
+ if (isToolExpanded(def.name, args, context) && def.renderCall) {
386
+ const component = def.renderCall(
270
387
  args as never,
271
388
  theme as never,
272
- { ...context, lastComponent: undefined } as never,
389
+ // One-key full expansion: skip the official preview so
390
+ // the per-tool shortcut always shows complete output.
391
+ {
392
+ ...context,
393
+ ...(forceFullExpansion ? { expanded: true } : {}),
394
+ lastComponent: undefined,
395
+ } as never,
273
396
  );
397
+ return makeToolClickable(component, def.name, args, context);
274
398
  }
275
399
  const component = collapsedCall(args, theme);
276
- if (!usesSelfShell) return component;
400
+ if (!usesSelfShell) return makeToolClickable(component, def.name, args, context);
277
401
 
278
402
  const shell = getCollapsedToolShell(
279
403
  context.state,
@@ -281,21 +405,22 @@ function registerCollapsibleTool(
281
405
  getCollapsedBackground(context.isPartial, context.isError),
282
406
  );
283
407
  shell.setCall(component);
284
- return shell;
408
+ return makeToolClickable(shell, def.name, args, context);
285
409
  },
286
410
 
287
411
  renderResult(result, options, theme, context) {
288
412
  const def = getDef(context.cwd);
289
- if (toolExpanded.get(def.name) && def.renderResult) {
290
- return def.renderResult(
413
+ if (isToolExpanded(def.name, context.args, context) && def.renderResult) {
414
+ const component = def.renderResult(
291
415
  result as never,
292
- options as never,
416
+ { ...options, ...(forceFullExpansion ? { expanded: true } : {}) } as never,
293
417
  theme as never,
294
418
  { ...context, lastComponent: undefined } as never,
295
419
  );
420
+ return makeToolClickable(component, def.name, context.args, context);
296
421
  }
297
422
  const component = collapsedResult(result, options, theme, context);
298
- if (!usesSelfShell) return component;
423
+ if (!usesSelfShell) return makeToolClickable(component, def.name, context.args, context);
299
424
 
300
425
  const shell = getCollapsedToolShell(
301
426
  context.state,
@@ -345,36 +470,38 @@ export default function (pi: ExtensionAPI) {
345
470
  },
346
471
 
347
472
  renderCall(args, theme, context) {
348
- if (toolExpanded.get("bash")) {
473
+ if (isToolExpanded("bash", args, context)) {
349
474
  const def = getBashDef(context.cwd);
350
475
  if (def.renderCall) {
351
- return def.renderCall(
476
+ const component = def.renderCall(
352
477
  args as never,
353
478
  theme as never,
354
479
  // One-key full expansion for bash: skip the official
355
480
  // preview so Ctrl+Alt+B shows the complete output.
356
481
  { ...context, expanded: true, lastComponent: undefined } as never,
357
482
  );
483
+ return makeToolClickable(component, "bash", args, context);
358
484
  }
359
485
  }
360
486
 
361
- const command = args.command || "...";
487
+ const rawCommand = args.command || "...";
488
+ const command = typeof rawCommand === "string" ? expandTabs(rawCommand) : "...";
362
489
  const timeout = args.timeout as number | undefined;
363
490
  const timeoutSuffix = timeout ? theme.fg("muted", ` (timeout ${timeout}s)`) : "";
364
491
  const renderedCommand = theme.fg("toolTitle", theme.bold(`$ ${command}`)) + timeoutSuffix;
365
492
 
366
- return new LimitedLinesText(
493
+ return makeToolClickable(new LimitedLinesText(
367
494
  renderedCommand,
368
495
  MAX_COLLAPSED_COMMAND_LINES,
369
496
  theme.fg("muted", "..."),
370
- );
497
+ ), "bash", args, context);
371
498
  },
372
499
 
373
500
  renderResult(result, options, theme, context) {
374
- if (toolExpanded.get("bash")) {
501
+ if (isToolExpanded("bash", context.args, context)) {
375
502
  const def = getBashDef(context.cwd);
376
503
  if (def.renderResult) {
377
- return def.renderResult(
504
+ const component = def.renderResult(
378
505
  result as never,
379
506
  // One-key full expansion for bash: skip the official
380
507
  // preview so Ctrl+Alt+B shows the complete output.
@@ -382,31 +509,47 @@ export default function (pi: ExtensionAPI) {
382
509
  theme as never,
383
510
  { ...context, lastComponent: undefined } as never,
384
511
  );
512
+ return makeToolClickable(component, "bash", context.args, context);
385
513
  }
386
514
  }
387
515
 
388
- const output = errorText(result).trim().replace(/\r/g, "");
516
+ const clickable = (component: Component) => makeToolClickable(component, "bash", context.args, context);
517
+ const output = expandTabs(errorText(result).trim().replace(/\r/g, ""));
518
+ const allLines = output.split("\n");
519
+ const first = allLines[0] ?? "";
520
+ const moreSuffix =
521
+ allLines.length > 1
522
+ ? theme.fg("muted", `... (${allLines.length - 1} more lines)`)
523
+ : "";
524
+ // 失败输出同样压成 1 行:红色首行 + 内联计数,与成功态同形。
389
525
  if (context.isError) {
390
- return new LimitedLinesText(
391
- theme.fg("error", firstLine(output) || "failed"),
392
- MAX_COLLAPSED_CONTENT_LINES,
526
+ if (!first) {
527
+ return clickable(new CollapsedBashOutput(
528
+ theme.fg("error", "failed"),
529
+ "",
530
+ theme.fg("muted", "..."),
531
+ ));
532
+ }
533
+ return clickable(new CollapsedBashOutput(
534
+ theme.fg("error", first),
535
+ moreSuffix,
393
536
  theme.fg("muted", "..."),
394
- );
537
+ ));
395
538
  }
396
539
  // Preview only the first line. Wrapping the full output on every
397
540
  // frame is what made the TUI lag with large outputs.
398
- const allLines = output.split("\n");
399
- const previewLines = allLines.slice(0, 1).map((line) => theme.fg("toolOutput", line));
400
- if (allLines.length > 1) {
401
- previewLines.push(
402
- theme.fg("muted", `... (${allLines.length - 1} more lines)`),
403
- );
541
+ if (!first) {
542
+ return clickable(new CollapsedBashOutput(
543
+ theme.fg("muted", "(no output)"),
544
+ "",
545
+ theme.fg("muted", "..."),
546
+ ));
404
547
  }
405
- return new LimitedLinesText(
406
- previewLines.join("\n") || theme.fg("muted", "(no output)"),
407
- MAX_COLLAPSED_BASH_OUTPUT_LINES,
548
+ return clickable(new CollapsedBashOutput(
549
+ theme.fg("toolOutput", first),
550
+ moreSuffix,
408
551
  theme.fg("muted", "..."),
409
- );
552
+ ));
410
553
  },
411
554
  });
412
555
 
@@ -462,6 +605,8 @@ export default function (pi: ExtensionAPI) {
462
605
  }
463
606
  return new Text(theme.fg("success", "done"), 0, 0);
464
607
  },
608
+ // 展开即全文,跳过官方 10 行预览:三工具统一两档,Ctrl+O 彻底失效。
609
+ true,
465
610
  );
466
611
 
467
612
  pi.registerCommand("tidy-bash", {
@@ -476,6 +621,18 @@ export default function (pi: ExtensionAPI) {
476
621
  description: "Toggle write output expansion",
477
622
  handler: async (_args, ctx) => toggleToolExpanded(ctx, "write"),
478
623
  });
624
+ pi.registerCommand("tidy-markdown", {
625
+ description: "Toggle markdown mode (edit/write expand for Markdown files)",
626
+ handler: async (_args, ctx) => {
627
+ markdownMode = !markdownMode;
628
+ manualExpanded.clear();
629
+ for (const name of ["bash", "edit", "write"]) {
630
+ expansionRevisions.set(name, (expansionRevisions.get(name) ?? 0) + 1);
631
+ }
632
+ refreshToolRows(ctx);
633
+ ctx.ui.notify(`markdown mode: ${markdownMode ? "on" : "off"}`, "info");
634
+ },
635
+ });
479
636
  pi.registerCommand("tidy-key-debug", {
480
637
  description: "Show the next raw terminal key sequence",
481
638
  handler: async (_args, ctx) => {
package/CHANGELOG.md DELETED
@@ -1,16 +0,0 @@
1
- # Changelog
2
-
3
- ## [0.1.3] - 2026-08-27
4
-
5
- ### Added
6
-
7
- - Added `/tidy-bash`, `/tidy-edit`, and `/tidy-write` commands for toggling tool output.
8
- - Added `/tidy-key-debug` to inspect the next terminal key sequence.
9
-
10
- ### Fixed
11
-
12
- - Normalized the legacy `ESC` + control-character form of `Ctrl+Alt+letter` shortcuts.
13
-
14
- ### Documentation
15
-
16
- - Documented the new commands and keyboard input debugging steps.