@runminton/pi-tidy-tools 0.1.2 → 0.2.0

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,55 @@
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).
20
+
21
+ ## Modes
22
+
23
+ | Mode | Behavior |
24
+ |---|---|
25
+ | compact (default) | all three tools collapsed |
26
+ | markdown (`/tidy-markdown` toggles) | `edit`/`write` on Markdown files (`.md`, `.mdx`, `.markdown`) expand to full content; everything else stays collapsed |
16
27
 
17
- Collapsed summaries are single-line (bash shows one preview line plus a count row).
28
+ Switching modes clears per-tool manual toggles and re-renders existing rows. The mode is not persisted across restarts.
18
29
 
19
30
  ## Shortcuts
20
31
 
21
- | Shortcut | Tool | Toggles between |
32
+ On Mac press `Command+Option+letter` (verified; maps to the registered `Ctrl+Alt+letter`. Other platforms press `Ctrl+Alt+letter`):
33
+
34
+ | Shortcut (Mac) | Tool | Toggles between |
22
35
  |---|---|---|
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 |
36
+ | `Command+Option+B` | `bash` | collapsed summary ↔ **one-key full output** |
37
+ | `Command+Option+E` | `edit` | official rendering ↔ collapsed summary |
38
+ | `Command+Option+W` | `write` | official rendering ↔ collapsed summary |
26
39
 
27
40
  A notification shows the current state on each toggle; press again to switch back.
28
41
 
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.
42
+ `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.
43
+
44
+ ## Commands
45
+
46
+ Use these commands when terminal shortcuts conflict or are not delivered:
47
+
48
+ | Command | Tool |
49
+ |---|---|
50
+ | `/tidy-bash` | Toggle `bash` output |
51
+ | `/tidy-edit` | Toggle `edit` output |
52
+ | `/tidy-write` | Toggle `write` output |
53
+ | `/tidy-markdown` | Toggle markdown mode (edit/write expand for Markdown files) |
30
54
 
31
55
  ## Usage
32
56
 
@@ -61,6 +85,22 @@ If a shortcut still does nothing on your machine, check:
61
85
  1. **Terminal or input method swallows the key.** Some terminals and IMEs (e.g. Chinese input methods, Windows Alt-menu accelerators) intercept `Ctrl+Alt+...` before Pi ever sees it. Try a different combination or a different terminal.
62
86
  2. **tmux / screen.** If Pi runs inside tmux with the default configuration, tmux's own key handling may interfere.
63
87
  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`.
88
+ 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.
89
+
90
+ ## Changelog
91
+
92
+ ### 0.2.0
93
+
94
+ - All three tools collapsed by default (edit/write used to expand)
95
+ - New `/tidy-markdown`: edit/write on Markdown files auto-expand to full content, the rest stays collapsed; switching modes clears manual toggles
96
+ - Write expansion skips the official 10-line preview and shows full content; global `Ctrl+O` no longer affects any of the three tools
97
+ - Bash collapsed output is 2 rows with the count suffix inline, including error output
98
+ - Fixed tab-width overflow wrapping collapsed bash output on narrow terminals; fixed doubled edit padding from a nested render shell
99
+
100
+ ### 0.1.3
101
+
102
+ - New `/tidy-bash`, `/tidy-edit`, `/tidy-write`, `/tidy-key-debug` commands
103
+ - Normalized the legacy `ESC` + control-character shortcut form
64
104
 
65
105
  ## Development
66
106
 
package/README.zh-CN.md CHANGED
@@ -2,31 +2,55 @@
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 行(计数后缀内联在输出行尾)。
20
+
21
+ ## 模式
22
+
23
+ | 模式 | 行为 |
24
+ |---|---|
25
+ | compact(默认) | 三工具全折叠 |
26
+ | markdown(`/tidy-markdown` 切换) | Markdown 文件(`.md`、`.mdx`、`.markdown`)的 `edit`/`write` 展开全文,其余照样折叠 |
16
27
 
17
- 折叠摘要为单行样式(bash 额外保留一行输出预览 + 一行计数)。
28
+ 切换模式会清空各工具的手动钉选并重渲染已有行。模式不持久化,重启后回到 compact。
18
29
 
19
30
  ## 快捷键
20
31
 
21
- | 快捷键 | 工具 | 切换内容 |
32
+ Mac 上按 `Command+Option+字母`(实测有效,对应插件注册的 `Ctrl+Alt+字母`;其他平台按 `Ctrl+Alt+字母`):
33
+
34
+ | 快捷键(Mac) | 工具 | 切换内容 |
22
35
  |---|---|---|
23
- | `Ctrl+Alt+B` | `bash` | 折叠摘要 ↔ **一键完整输出** |
24
- | `Ctrl+Alt+E` | `edit` | 官方渲染 ↔ 折叠摘要 |
25
- | `Ctrl+Alt+W` | `write` | 官方渲染 ↔ 折叠摘要 |
36
+ | `Command+Option+B` | `bash` | 折叠摘要 ↔ **一键完整输出** |
37
+ | `Command+Option+E` | `edit` | 官方渲染 ↔ 折叠摘要 |
38
+ | `Command+Option+W` | `write` | 官方渲染 ↔ 折叠摘要 |
26
39
 
27
40
  每次切换会弹出通知提示当前状态;再按一次切回。
28
41
 
29
- `bash` 展开时**一次按键直接显示完整输出**——跳过 Pi 内置的 5 行尾部预览。`edit` `write` 展开时使用 Pi **官方默认渲染**:委托给官方渲染器,内容本身是否进一步展开仍由全局 `Ctrl+O` 控制,与不装本扩展时完全一致。只有 `bash` 在展开状态下不受 `Ctrl+O` 影响。
42
+ `bash` 和 `write` 展开时**一次按键直接显示完整输出**——跳过 Pi 内置预览(bash 5 行尾部预览、write 10 行预览)。`edit` 展开时使用 Pi **官方渲染**(它没有预览态)。全局 `Ctrl+O` 对这三个工具均无效。
43
+
44
+ ## 命令
45
+
46
+ 终端快捷键冲突或无法传入 Pi 时,使用这些命令:
47
+
48
+ | 命令 | 工具 |
49
+ |---|---|
50
+ | `/tidy-bash` | 切换 `bash` 输出 |
51
+ | `/tidy-edit` | 切换 `edit` 输出 |
52
+ | `/tidy-write` | 切换 `write` 输出 |
53
+ | `/tidy-markdown` | 切换 Markdown 模式(Markdown 文件的 edit/write 展开) |
30
54
 
31
55
  ## 使用方法
32
56
 
@@ -61,6 +85,22 @@ Pi 会在终端支持时协商 [Kitty 键盘协议](https://sw.kovidgoyal.net/ki
61
85
  1. **终端或输入法吞掉了按键。** 有些终端和输入法(例如中文输入法、Windows 的 Alt 菜单加速键)会在 Pi 收到按键之前把它拦截。换一个组合键或换一个终端试试。
62
86
  2. **tmux / screen。** 如果 Pi 运行在 tmux 默认配置里,tmux 自己的按键处理可能干扰。
63
87
  3. **自定义按键。** 绑定在 `tidy-tools.ts` 里通过 `pi.registerShortcut(...)` 注册——改那里的按键字符串即可重新绑定。Pi 内置的全局展开(`Ctrl+O`,即 `app.tools.expand`)则单独通过 `~/.pi/agent/keybindings.json` 配置。
88
+ 4. **检查输入。** 执行 `/tidy-key-debug`,再按一次目标快捷键。Pi 会显示收到的字节和改写结果。若没有通知,终端或输入法在 Pi 收到按键前已拦截它。
89
+
90
+ ## 更新日志
91
+
92
+ ### 0.2.0 - 2026-09-05
93
+
94
+ - 默认三工具全折叠(edit/write 原默认展开)
95
+ - 新增 `/tidy-markdown`:Markdown 文件的 edit/write 自动展开全文,其余照样折叠;切模式清空手动钉选
96
+ - write 展开跳过官方 10 行预览,直接全文;全局 `Ctrl+O` 对三工具失效
97
+ - bash 折叠压成 2 行:计数后缀内联到输出行尾,失败输出(红色)同样单行
98
+ - 修 tab 宽度导致的窄终端折行;修 edit 双层外框多出的 padding
99
+
100
+ ### 0.1.3
101
+
102
+ - 新增 `/tidy-bash`、`/tidy-edit`、`/tidy-write`、`/tidy-key-debug` 命令
103
+ - 修传统 `ESC` + 控制字符形式的快捷键归一化
64
104
 
65
105
  ## 开发
66
106
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runminton/pi-tidy-tools",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
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",
package/tidy-tools.ts CHANGED
@@ -7,6 +7,9 @@
7
7
  * - Expanded: Ctrl+Alt+E (edit), Ctrl+Alt+W (write), Ctrl+Alt+B (bash)
8
8
  * toggle each tool between collapsed and fully expanded rendering.
9
9
  * - The global Ctrl+O tool expansion does not affect these three tools.
10
+ * - Modes: compact (default, all three collapsed) and markdown
11
+ * (`/tidy-markdown` toggles; edit/write on Markdown files expand,
12
+ * the rest stays collapsed). Not persisted across restarts.
10
13
  *
11
14
  * Usage:
12
15
  * pi -e ./tidy-tools.ts
@@ -32,8 +35,8 @@ import {
32
35
 
33
36
  const MAX_COLLAPSED_COMMAND_LINES = 1;
34
37
  const MAX_COLLAPSED_CONTENT_LINES = 3;
35
- // bash 折叠输出:1 行预览 + 1 行计数提示(共 2 行)。
36
- const MAX_COLLAPSED_BASH_OUTPUT_LINES = 2;
38
+ // bash 折叠输出:预览与计数后缀合并为 1 行(命令 1 行 + 输出 1 行)。
39
+ const MAX_COLLAPSED_BASH_OUTPUT_LINES = 1;
37
40
 
38
41
  /**
39
42
  * Truncate rendered rows to at most maxLines, keeping the beginning.
@@ -65,6 +68,14 @@ function firstLine(text: string | undefined): string {
65
68
  return line.length > 80 ? `${line.slice(0, 77)}...` : line;
66
69
  }
67
70
 
71
+ // 终端 tab 按 8 的倍数对齐,pi-tui 按 3 列计算。不展开 tab 会导致
72
+ // 行宽估算小于终端实际渲染,窄终端下折成两行。与官方 replaceTabs
73
+ // 保持一致:tab 展开为 3 个空格。
74
+
75
+ function expandTabs(text: string): string {
76
+ return text.replace(/\t/g, " ");
77
+ }
78
+
68
79
  function errorText(result: { content: { type: string; text?: string }[] }): string {
69
80
  return result.content
70
81
  .filter((c) => c.type === "text")
@@ -119,6 +130,46 @@ class LimitedLinesText implements Component {
119
130
  }
120
131
  }
121
132
 
133
+ class CollapsedBashOutput implements Component {
134
+ // 首行预览 + 计数后缀合并为 1 行,后缀常显。预览放不下时截断并加省略标记。
135
+ constructor(
136
+ private readonly preview: string,
137
+ private readonly suffix: string,
138
+ private readonly truncatedMarker: string,
139
+ ) {}
140
+
141
+ render(width: number): string[] {
142
+ if (!this.suffix) {
143
+ return truncateRows(
144
+ wrapTextWithAnsi(this.preview, Math.max(1, width)),
145
+ MAX_COLLAPSED_BASH_OUTPUT_LINES,
146
+ this.truncatedMarker,
147
+ width,
148
+ );
149
+ }
150
+ const suffixWidth = visibleWidth(this.suffix);
151
+ if (suffixWidth >= width) {
152
+ return [sliceByColumn(this.suffix, 0, Math.max(1, width), true)];
153
+ }
154
+ const previewBudget = width - suffixWidth - 1;
155
+ if (visibleWidth(this.preview) <= previewBudget) {
156
+ return [`${this.preview} ${this.suffix}`];
157
+ }
158
+ const markerWidth = visibleWidth(this.truncatedMarker);
159
+ const preview = sliceByColumn(
160
+ this.preview,
161
+ 0,
162
+ Math.max(0, previewBudget - markerWidth),
163
+ true,
164
+ );
165
+ const row = `${preview}${this.truncatedMarker} ${this.suffix}`;
166
+ // 极窄宽度下省略标记本身都放不下时做最终钳制,保证恒为 1 行。
167
+ return [visibleWidth(row) > width ? sliceByColumn(row, 0, width, true) : row];
168
+ }
169
+
170
+ invalidate(): void {}
171
+ }
172
+
122
173
  class CollapsedToolShell extends Box {
123
174
  private callComponent?: Component;
124
175
  private resultComponent?: Component;
@@ -160,13 +211,37 @@ function getCollapsedBackground(isPartial: boolean, isError: boolean): string {
160
211
  return isError ? "toolErrorBg" : "toolSuccessBg";
161
212
  }
162
213
 
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
- ]);
214
+ // 显示模式:compact 下三工具默认全折叠;markdown 模式下 edit/write 遇到
215
+ // Markdown 文件默认展开全文,其余照样折叠。默认 compact,不持久化。
216
+ let markdownMode = false;
217
+ // 手动钉选:Ctrl+Alt+? 或 /tidy-<tool> 设置,优先于模式默认;切换模式时清空。
218
+ const manualExpanded = new Map<string, boolean>();
219
+
220
+ function isMarkdownPath(value: unknown): boolean {
221
+ if (typeof value !== "string") return false;
222
+ return /\.(md|mdx|markdown)$/i.test(value.trim());
223
+ }
224
+
225
+ function toolArgsMarkdown(args: any): boolean {
226
+ return isMarkdownPath(args?.path) || isMarkdownPath(args?.file_path);
227
+ }
228
+
229
+ // 不带文件上下文的工具级基准(用于翻转手动钉选):compact 下全 false;
230
+ // markdown 模式下 edit/write 为 true(具体到文件时再按后缀细化)。
231
+ function modeBaseExpanded(name: string): boolean {
232
+ if (!markdownMode) return false;
233
+ return name === "edit" || name === "write";
234
+ }
235
+
236
+ function modeDefaultExpanded(name: string, args: any): boolean {
237
+ if (!markdownMode) return false;
238
+ if (name === "edit" || name === "write") return toolArgsMarkdown(args);
239
+ return false;
240
+ }
241
+
242
+ function isToolExpanded(name: string, args: any): boolean {
243
+ return manualExpanded.get(name) ?? modeDefaultExpanded(name, args);
244
+ }
170
245
 
171
246
  // Some terminals (Kitty keyboard protocol flag-1 mode) encode Ctrl+letter as
172
247
  // a CSI-u sequence using the control character code without a Ctrl modifier
@@ -180,8 +255,17 @@ const CTRL_CODE_TO_KEY: Record<number, string> = {
180
255
  2: "b", // Ctrl+B
181
256
  };
182
257
  const ALT_MODIFIER = 2;
258
+ let debugNextTerminalInput = false;
183
259
 
184
260
  function convertCtrlAltSequence(data: string): string | undefined {
261
+ // Legacy terminals send Ctrl+Alt+letter as ESC followed by the Ctrl
262
+ // character. Pi deliberately ignores this form after Kitty negotiation,
263
+ // so normalize it before Pi's key matcher sees it.
264
+ if (data.length === 2 && data[0] === "\x1b") {
265
+ const key = CTRL_CODE_TO_KEY[data.charCodeAt(1)];
266
+ if (key) return `\x1b[${key.charCodeAt(0)};7u`; // 7 = Ctrl|Alt (1-indexed)
267
+ }
268
+
185
269
  const match = data.match(/^\x1b\[(\d+);(\d+)(?::(\d+))?u$/);
186
270
  if (!match) return undefined;
187
271
  const code = Number(match[1]);
@@ -192,14 +276,18 @@ function convertCtrlAltSequence(data: string): string | undefined {
192
276
  return `\x1b[${key.charCodeAt(0)};7${eventSuffix}u`; // 7 = Ctrl|Alt (1-indexed)
193
277
  }
194
278
 
195
- function toggleToolExpanded(ctx: ExtensionContext, name: string): void {
196
- toolExpanded.set(name, !(toolExpanded.get(name) ?? false));
279
+ function refreshToolRows(ctx: ExtensionContext): void {
197
280
  const globalExpanded = ctx.ui.getToolsExpanded();
198
281
  // Force every tool row to re-render. The global value is restored
199
282
  // immediately, so only the rows' renderers refresh, not the global state.
200
283
  ctx.ui.setToolsExpanded(!globalExpanded);
201
284
  ctx.ui.setToolsExpanded(globalExpanded);
202
- ctx.ui.notify(`${name}: ${toolExpanded.get(name) ? "expanded" : "collapsed"}`, "info");
285
+ }
286
+
287
+ function toggleToolExpanded(ctx: ExtensionContext, name: string): void {
288
+ manualExpanded.set(name, !(manualExpanded.get(name) ?? modeBaseExpanded(name)));
289
+ refreshToolRows(ctx);
290
+ ctx.ui.notify(`${name}: ${manualExpanded.get(name) ? "expanded" : "collapsed"}`, "info");
203
291
  }
204
292
 
205
293
  // Cache the official tool implementation by working directory, as pi may run
@@ -235,6 +323,9 @@ function registerCollapsibleTool(
235
323
  getDef: (cwd: string) => ToolDefinition<any, any, any>,
236
324
  collapsedCall: (args: any, theme: any) => Component,
237
325
  collapsedResult: (result: any, options: any, theme: any, context: any) => Component,
326
+ // 跳过官方预览态,展开即全文(bash 行为)。为 false 时保留官方默认视图,
327
+ // 其内部预览 / 全文仍由全局 Ctrl+O 决定。
328
+ forceFullExpansion = false,
238
329
  ) {
239
330
  const def = getDef(process.cwd());
240
331
  const usesSelfShell = def.renderShell === "self";
@@ -247,8 +338,11 @@ function registerCollapsibleTool(
247
338
  parameters: def.parameters,
248
339
  prepareArguments: def.prepareArguments,
249
340
  constrainedSampling: def.constrainedSampling,
250
- // Omit renderShell so Pi inherits each built-in tool's native shell.
251
- // In particular, edit uses "self" for its original expanded layout.
341
+ // Mirror the official renderShell. Omitting it falls back to
342
+ // "default", which double-wraps "self" tools like edit in an
343
+ // extra Box: one extra padding line on top and bottom when
344
+ // expanded, and a Box-in-Box when collapsed.
345
+ ...(usesSelfShell ? { renderShell: "self" as const } : {}),
252
346
 
253
347
  async execute(toolCallId, params, signal, onUpdate, ctx) {
254
348
  return getDef(ctx.cwd).execute(toolCallId, params, signal, onUpdate, ctx);
@@ -256,11 +350,17 @@ function registerCollapsibleTool(
256
350
 
257
351
  renderCall(args, theme, context) {
258
352
  const def = getDef(context.cwd);
259
- if (toolExpanded.get(def.name) && def.renderCall) {
353
+ if (isToolExpanded(def.name, args) && def.renderCall) {
260
354
  return def.renderCall(
261
355
  args as never,
262
356
  theme as never,
263
- { ...context, lastComponent: undefined } as never,
357
+ // One-key full expansion: skip the official preview so
358
+ // the per-tool shortcut always shows complete output.
359
+ {
360
+ ...context,
361
+ ...(forceFullExpansion ? { expanded: true } : {}),
362
+ lastComponent: undefined,
363
+ } as never,
264
364
  );
265
365
  }
266
366
  const component = collapsedCall(args, theme);
@@ -277,10 +377,10 @@ function registerCollapsibleTool(
277
377
 
278
378
  renderResult(result, options, theme, context) {
279
379
  const def = getDef(context.cwd);
280
- if (toolExpanded.get(def.name) && def.renderResult) {
380
+ if (isToolExpanded(def.name, context.args) && def.renderResult) {
281
381
  return def.renderResult(
282
382
  result as never,
283
- options as never,
383
+ { ...options, ...(forceFullExpansion ? { expanded: true } : {}) } as never,
284
384
  theme as never,
285
385
  { ...context, lastComponent: undefined } as never,
286
386
  );
@@ -336,7 +436,7 @@ export default function (pi: ExtensionAPI) {
336
436
  },
337
437
 
338
438
  renderCall(args, theme, context) {
339
- if (toolExpanded.get("bash")) {
439
+ if (isToolExpanded("bash", args)) {
340
440
  const def = getBashDef(context.cwd);
341
441
  if (def.renderCall) {
342
442
  return def.renderCall(
@@ -349,7 +449,8 @@ export default function (pi: ExtensionAPI) {
349
449
  }
350
450
  }
351
451
 
352
- const command = args.command || "...";
452
+ const rawCommand = args.command || "...";
453
+ const command = typeof rawCommand === "string" ? expandTabs(rawCommand) : "...";
353
454
  const timeout = args.timeout as number | undefined;
354
455
  const timeoutSuffix = timeout ? theme.fg("muted", ` (timeout ${timeout}s)`) : "";
355
456
  const renderedCommand = theme.fg("toolTitle", theme.bold(`$ ${command}`)) + timeoutSuffix;
@@ -362,7 +463,7 @@ export default function (pi: ExtensionAPI) {
362
463
  },
363
464
 
364
465
  renderResult(result, options, theme, context) {
365
- if (toolExpanded.get("bash")) {
466
+ if (isToolExpanded("bash", context.args)) {
366
467
  const def = getBashDef(context.cwd);
367
468
  if (def.renderResult) {
368
469
  return def.renderResult(
@@ -376,26 +477,40 @@ export default function (pi: ExtensionAPI) {
376
477
  }
377
478
  }
378
479
 
379
- const output = errorText(result).trim().replace(/\r/g, "");
480
+ const output = expandTabs(errorText(result).trim().replace(/\r/g, ""));
481
+ const allLines = output.split("\n");
482
+ const first = allLines[0] ?? "";
483
+ const moreSuffix =
484
+ allLines.length > 1
485
+ ? theme.fg("muted", `... (${allLines.length - 1} more lines)`)
486
+ : "";
487
+ // 失败输出同样压成 1 行:红色首行 + 内联计数,与成功态同形。
380
488
  if (context.isError) {
381
- return new LimitedLinesText(
382
- theme.fg("error", firstLine(output) || "failed"),
383
- MAX_COLLAPSED_CONTENT_LINES,
489
+ if (!first) {
490
+ return new CollapsedBashOutput(
491
+ theme.fg("error", "failed"),
492
+ "",
493
+ theme.fg("muted", "..."),
494
+ );
495
+ }
496
+ return new CollapsedBashOutput(
497
+ theme.fg("error", first),
498
+ moreSuffix,
384
499
  theme.fg("muted", "..."),
385
500
  );
386
501
  }
387
502
  // Preview only the first line. Wrapping the full output on every
388
503
  // frame is what made the TUI lag with large outputs.
389
- const allLines = output.split("\n");
390
- const previewLines = allLines.slice(0, 1).map((line) => theme.fg("toolOutput", line));
391
- if (allLines.length > 1) {
392
- previewLines.push(
393
- theme.fg("muted", `... (${allLines.length - 1} more lines)`),
504
+ if (!first) {
505
+ return new CollapsedBashOutput(
506
+ theme.fg("muted", "(no output)"),
507
+ "",
508
+ theme.fg("muted", "..."),
394
509
  );
395
510
  }
396
- return new LimitedLinesText(
397
- previewLines.join("\n") || theme.fg("muted", "(no output)"),
398
- MAX_COLLAPSED_BASH_OUTPUT_LINES,
511
+ return new CollapsedBashOutput(
512
+ theme.fg("toolOutput", first),
513
+ moreSuffix,
399
514
  theme.fg("muted", "..."),
400
515
  );
401
516
  },
@@ -453,8 +568,39 @@ export default function (pi: ExtensionAPI) {
453
568
  }
454
569
  return new Text(theme.fg("success", "done"), 0, 0);
455
570
  },
571
+ // 展开即全文,跳过官方 10 行预览:三工具统一两档,Ctrl+O 彻底失效。
572
+ true,
456
573
  );
457
574
 
575
+ pi.registerCommand("tidy-bash", {
576
+ description: "Toggle bash output expansion",
577
+ handler: async (_args, ctx) => toggleToolExpanded(ctx, "bash"),
578
+ });
579
+ pi.registerCommand("tidy-edit", {
580
+ description: "Toggle edit output expansion",
581
+ handler: async (_args, ctx) => toggleToolExpanded(ctx, "edit"),
582
+ });
583
+ pi.registerCommand("tidy-write", {
584
+ description: "Toggle write output expansion",
585
+ handler: async (_args, ctx) => toggleToolExpanded(ctx, "write"),
586
+ });
587
+ pi.registerCommand("tidy-markdown", {
588
+ description: "Toggle markdown mode (edit/write expand for Markdown files)",
589
+ handler: async (_args, ctx) => {
590
+ markdownMode = !markdownMode;
591
+ manualExpanded.clear();
592
+ refreshToolRows(ctx);
593
+ ctx.ui.notify(`markdown mode: ${markdownMode ? "on" : "off"}`, "info");
594
+ },
595
+ });
596
+ pi.registerCommand("tidy-key-debug", {
597
+ description: "Show the next raw terminal key sequence",
598
+ handler: async (_args, ctx) => {
599
+ debugNextTerminalInput = true;
600
+ ctx.ui.notify("Press one key to inspect its terminal sequence.", "info");
601
+ },
602
+ });
603
+
458
604
  // --- Per-tool expansion shortcuts, independent of the global Ctrl+O ---
459
605
  pi.registerShortcut("ctrl+alt+e", {
460
606
  description: "Toggle edit expansion",
@@ -472,8 +618,16 @@ export default function (pi: ExtensionAPI) {
472
618
  // Some terminals encode Ctrl+Alt+letter as a control-character CSI-u
473
619
  // sequence. Rewrite those three sequences before they reach the editor.
474
620
  pi.on("session_start", (_event, ctx) => {
475
- ctx.ui.onTerminalInput?.((data) => {
621
+ ctx.ui.onTerminalInput((data) => {
476
622
  const converted = convertCtrlAltSequence(data);
623
+ if (debugNextTerminalInput) {
624
+ debugNextTerminalInput = false;
625
+ const bytes = [...data].map((char) => char.charCodeAt(0)).join(", ");
626
+ ctx.ui.notify(
627
+ `raw: ${JSON.stringify(data)} [${bytes}]${converted ? ` → ${JSON.stringify(converted)}` : ""}`,
628
+ "info",
629
+ );
630
+ }
477
631
  return converted ? { data: converted } : undefined;
478
632
  });
479
633
  });