pi-open-tui 0.4.1 → 0.4.2

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/CHANGELOG.md ADDED
@@ -0,0 +1,29 @@
1
+ # Changelog
2
+
3
+ ## [0.4.2] - 2026-11-03
4
+
5
+ ### Changed
6
+
7
+ - Cache completed compact tool rows and Copy button labels so scrolling reuses rendered lines instead of rebuilding them.
8
+
9
+ ### Fixed
10
+
11
+ - Cancel an in-progress Copy click when rendering moves, removes, or changes its Copy target, so a stale press cannot copy different content.
12
+
13
+ ## [0.4.1] - 2026-11-02
14
+
15
+ ### New Features
16
+
17
+ - Add clickable Copy buttons to fenced code blocks and user messages in fullscreen mode, configurable through `/open-tui`.
18
+ - Add syntax-highlighted previews for successful edits and writes in compact tool view, limited to 16 rows with a hint to expand longer output.
19
+
20
+ ### Changed
21
+
22
+ - Preserve compatible editors installed by other extensions, including runtime replacements, while applying open-tui framing and retaining their input behavior.
23
+ - Refine compact tool status indicators with dimmed completed rows, animated running dots, and clearer error and truncation summaries.
24
+
25
+ ### Fixed
26
+
27
+ - Include tool-reported usage, compaction and branch summaries, and cache warming in footer token and cost totals; show the cache-hit percentage for the latest assistant response.
28
+ - Respect `PI_CODING_AGENT_DIR` for settings storage and warn when settings cannot be saved.
29
+ - Improve Scala project detection and Git status handling for renamed-then-deleted files.
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  **English** | [简体中文](./README.zh-CN.md)
6
6
 
7
- A polished terminal interface for the [Pi](https://pi.dev) coding agent. It brings the strongest ideas from pi-haiku, pi-claude-code-tui, and pi-zentui into one configurable extension.
7
+ A polished terminal interface for the [Pi](https://pi.dev) coding agent.
8
8
 
9
9
  ![pi-open-tui preview](https://raw.githubusercontent.com/OldSuns/pi-open-tui/main/assets/preview_dashboard_1.png)
10
10
 
@@ -227,6 +227,7 @@ This project builds on several Pi community packages:
227
227
  - **[pi-claude-code-tui](https://github.com/Phoobobo/pi-claude-code-tui)** — Pi logo artwork and rounded editor border technique
228
228
  - **[pi-zentui](https://github.com/lmilojevicc/pi-zentui)** — Starship-style footer segments, runtime detection, session lifecycle, and settings UI pattern
229
229
  - **[pi-tps](https://github.com/monotykamary/pi-tps)** — turn timing, stall detection, and conservative TPS measurement
230
+ - **[pi-polish](https://github.com/Pringled/pi-extensions/blob/main/packages/pi-polish)** — compact tool-call rows and syntax-highlighted edit diffs
230
231
 
231
232
  The logo artwork is derived from Pi's official install script (`pi.dev/install.sh`). Runtime detection and Git porcelain parsing borrow structure from `pi-zentui`.
232
233
 
package/README.zh-CN.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  [English](./README.md) | **简体中文**
6
6
 
7
- 一个为 [Pi](https://pi.dev) 编程代理打造的终端界面扩展,将 pi-haiku、pi-claude-code-tui 与 pi-zentui 的优秀设计整合为统一、可配置的使用体验。
7
+ 一个为 [Pi](https://pi.dev) 编程代理打造的终端界面扩展。
8
8
 
9
9
  ![pi-open-tui 预览](https://raw.githubusercontent.com/OldSuns/pi-open-tui/main/assets/preview_dashboard_1.png)
10
10
 
@@ -227,6 +227,7 @@ CI 在 Linux、Windows、macOS 上,使用 Node.js 22.19.0 和 24,检查 Pi 1
227
227
  - **[pi-claude-code-tui](https://github.com/Phoobobo/pi-claude-code-tui)** — Pi Logo 图案与圆角编辑器边框技术
228
228
  - **[pi-zentui](https://github.com/lmilojevicc/pi-zentui)** — Starship 风格底栏、运行环境检测、会话生命周期和设置界面模式
229
229
  - **[pi-tps](https://github.com/monotykamary/pi-tps)** — 单轮计时、停顿检测和保守的 TPS 计算方式
230
+ - **[pi-polish](https://github.com/Pringled/pi-extensions/blob/main/packages/pi-polish)** — 紧凑的工具调用行和带语法高亮的编辑 diff
230
231
 
231
232
  Logo 图案源自 Pi 官方安装脚本(`pi.dev/install.sh`)。运行环境检测和 Git porcelain 解析借鉴了 `pi-zentui` 的结构。
232
233
 
@@ -73,7 +73,7 @@ interface UserMessageInternals {
73
73
  let renderScope: { frame: boolean; label: boolean; ordinal: number; blocks: { text: string; ordinal: number }[] } | undefined;
74
74
  let press: Press | undefined;
75
75
  const markdownRegions = new WeakMap<object, { lines: readonly string[]; regions: CopyRegion[] }>();
76
- const userRegions = new WeakMap<object, CopyRegion>();
76
+ const userRegions = new WeakMap<object, { region: CopyRegion; source: string; label: string; width: number; line: string }>();
77
77
 
78
78
  /**
79
79
  * Markdown transformer that only records the message kind; Pi calls it during Markdown.render.
@@ -145,6 +145,16 @@ export function handleCopyMouse(
145
145
  return { handled: true, render: false };
146
146
  }
147
147
 
148
+ /** Cancel a held click when rendering moves, removes, or changes its copy target. */
149
+ function cancelStalePress(owner: object, regions: readonly CopyRegion[]): void {
150
+ if (press?.owner !== owner) return;
151
+ const previous = press.region;
152
+ if (!regions.some((region) => region.row === previous.row && region.col === previous.col
153
+ && region.width === previous.width && region.text === previous.text)) {
154
+ press = undefined;
155
+ }
156
+ }
157
+
148
158
  /**
149
159
  * Markdown.render normalizes tabs to three spaces before lexing, so token text loses them.
150
160
  * Match the normalized tokens back to source lines: lexing raw lists would expand their tabs first.
@@ -314,6 +324,7 @@ export function installCopyButtons(options: {
314
324
  text: restoreTabs(source, block.ordinal, block.text),
315
325
  });
316
326
  }
327
+ cancelStalePress(this, regions);
317
328
  markdownRegions.set(this, { lines, regions });
318
329
  return lines;
319
330
  }),
@@ -325,20 +336,37 @@ export function installCopyButtons(options: {
325
336
  patch(userProto, "render", function (this: UserMessageInternals, width) {
326
337
  if (!installed) return originalUserRender.call(this, width);
327
338
  const lines = originalUserRender.call(this, width);
328
- userRegions.delete(this);
329
339
  const pad = typeof this.outputPad === "number" ? this.outputPad : 1;
330
340
  const col = width - pad - LABEL.length;
331
- if (!active() || lines.length < 2 || col < 1 || typeof this.text !== "string") return lines;
341
+ if (!active() || lines.length < 2 || col < 1 || typeof this.text !== "string") {
342
+ cancelStalePress(this, []);
343
+ userRegions.delete(this);
344
+ return lines;
345
+ }
332
346
  // The last row is the bubble's bottom padding, so the label never shares a row with a code-block label.
333
347
  const row = lines.length - 1;
334
348
  const theme = options.getTheme();
335
- lines[row] = compositeTuiLine(lines[row]!, theme.bg("userMessageBg", label()), col, LABEL.length, width);
336
- userRegions.set(this, { row, col, width: LABEL.length, text: this.text });
349
+ const button = theme.bg("userMessageBg", label());
350
+ const cached = userRegions.get(this);
351
+ // UserMessage.render returns a fresh array, so compare its undecorated bottom row instead.
352
+ if (cached && cached.source === lines[row] && cached.label === button && cached.width === width
353
+ && cached.region.row === row && cached.region.col === col && cached.region.text === this.text) {
354
+ lines[row] = cached.line;
355
+ return lines;
356
+ }
357
+ const source = lines[row]!;
358
+ lines[row] = compositeTuiLine(source, button, col, LABEL.length, width);
359
+ const region = { row, col, width: LABEL.length, text: this.text };
360
+ cancelStalePress(this, [region]);
361
+ userRegions.set(this, {
362
+ region,
363
+ source, label: button, width, line: lines[row]!,
364
+ });
337
365
  return lines;
338
366
  }),
339
367
  patch(userProto, "handleMouse", function (this: UserMessageInternals, event) {
340
368
  if (!active()) return originalUserMouse?.call(this, event);
341
- const region = userRegions.get(this);
369
+ const region = userRegions.get(this)?.region;
342
370
  return handleCopyMouse(this, region ? [region] : [], event, copy) ?? originalUserMouse?.call(this, event);
343
371
  }),
344
372
  ];
@@ -258,11 +258,25 @@ function formatElapsed(ms: number): string {
258
258
  return ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(1)}s`;
259
259
  }
260
260
 
261
- /** Lines truncated to the render width; rebuilt on every render call. */
262
- function lines(build: (width: number) => string[]): Component {
261
+ /** Stable rows reuse their final lines until width, theme, icons, or invalidation changes them. */
262
+ function lines(
263
+ build: (width: number, icons: IconGlyphs) => string[],
264
+ theme: Theme,
265
+ glyphs: () => IconGlyphs,
266
+ cacheable = true,
267
+ ): Component {
268
+ let cache: { width: number; colors: Theme["colors"]; toolCall: string; toolResult: string; lines: string[] } | undefined;
263
269
  return {
264
- render: (width) => build(width).map((line) => truncateToWidth(line, Math.max(0, width), "…")),
265
- invalidate() {},
270
+ render(width) {
271
+ const icons = glyphs();
272
+ const colors = theme.colors;
273
+ if (cacheable && cache && cache.width === width && cache.colors === colors
274
+ && cache.toolCall === icons.toolCall && cache.toolResult === icons.toolResult) return cache.lines;
275
+ const rendered = build(width, icons).map((line) => truncateToWidth(line, Math.max(0, width), "…"));
276
+ if (cacheable) cache = { width, colors, toolCall: icons.toolCall, toolResult: icons.toolResult, lines: rendered };
277
+ return rendered;
278
+ },
279
+ invalidate() { cache = undefined; },
266
280
  };
267
281
  }
268
282
 
@@ -340,10 +354,10 @@ export function createCompactRenderers(
340
354
  const titleColor = context.isPartial || context.isError ? "toolTitle" : "dim";
341
355
  const detail = sanitizeStatus(callDetail(toolName, (args ?? {}) as Args, context.cwd));
342
356
  const pad = " ".repeat(context.outputPad);
343
- return lines(() => [
344
- pad + theme.fg(color, glyphs().toolCall) + " " + theme.fg(titleColor, titleColor === "dim" ? label : theme.bold(label))
357
+ return lines((_width, icons) => [
358
+ pad + theme.fg(color, icons.toolCall) + " " + theme.fg(titleColor, titleColor === "dim" ? label : theme.bold(label))
345
359
  + (detail ? theme.fg(context.isPartial || context.isError ? "muted" : "dim", `(${detail})`) : ""),
346
- ]);
360
+ ], theme, glyphs, !context.isPartial);
347
361
  },
348
362
 
349
363
  renderResult(result, options, theme, context) {
@@ -371,22 +385,23 @@ export function createCompactRenderers(
371
385
  row.preview = { source, path, colors: theme.colors, render: prepareToolPreview(toolName, source, path, theme) };
372
386
  }
373
387
  }
374
- return lines((width) => {
375
- const { toolResult } = glyphs();
388
+ const duration = isShell && context.durationMs !== undefined ? ` · ${formatElapsed(context.durationMs)}` : "";
389
+ const summary = options.isPartial ? "" : context.isError
390
+ ? summarizeError(toolName, result, duration)
391
+ : summarizeResult(toolName, result, args) + duration;
392
+ const tail = options.isPartial && isShell ? tailLines(textOutput(result), LIVE_TAIL_LINES) : [];
393
+ return lines((width, { toolResult }) => {
376
394
  const gutter = `${pad} ${theme.fg("dim", toolResult)} `;
377
395
  const indent = `${pad} ${" ".repeat(visibleWidth(toolResult))} `;
378
396
  if (options.isPartial) {
379
397
  if (!isShell) return [gutter + theme.fg("warning", "Running…")];
380
- const tail = tailLines(textOutput(result), LIVE_TAIL_LINES).map((line) => theme.fg("toolOutput", line));
381
398
  const elapsed = row.startedAt === undefined ? "" : ` ${formatElapsed(Date.now() - row.startedAt)}`;
382
- return [...tail, theme.fg("warning", `Running…${elapsed}`)]
399
+ return [...tail.map((line) => theme.fg("toolOutput", line)), theme.fg("warning", `Running…${elapsed}`)]
383
400
  .map((line, index) => (index === 0 ? gutter : indent) + line);
384
401
  }
385
- const duration = isShell && context.durationMs !== undefined ? ` · ${formatElapsed(context.durationMs)}` : "";
386
- if (context.isError) return [gutter + theme.fg("error", summarizeError(toolName, result, duration))];
387
- const summary = summarizeResult(toolName, result, args);
388
- return [gutter + theme.fg("dim", summary + duration), ...(row.preview?.render(width, indent) ?? [])];
389
- });
402
+ if (context.isError) return [gutter + theme.fg("error", summary)];
403
+ return [gutter + theme.fg("dim", summary), ...(row.preview?.render(width, indent) ?? [])];
404
+ }, theme, glyphs, !options.isPartial);
390
405
  },
391
406
  };
392
407
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-open-tui",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "A polished TUI for Pi coding agent: branded header, responsive footer, framed editor, thinking preview, and compact tool views.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -20,6 +20,7 @@
20
20
  "files": [
21
21
  "extensions/",
22
22
  "assets/",
23
+ "CHANGELOG.md",
23
24
  "README.md",
24
25
  "README.zh-CN.md",
25
26
  "LICENSE"