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 +29 -0
- package/README.md +2 -1
- package/README.zh-CN.md +2 -1
- package/extensions/open-tui/copy-buttons.ts +34 -6
- package/extensions/open-tui/tool-view.ts +31 -16
- package/package.json +2 -1
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.
|
|
7
|
+
A polished terminal interface for the [Pi](https://pi.dev) coding agent.
|
|
8
8
|
|
|
9
9
|

|
|
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)
|
|
7
|
+
一个为 [Pi](https://pi.dev) 编程代理打造的终端界面扩展。
|
|
8
8
|
|
|
9
9
|

|
|
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")
|
|
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
|
-
|
|
336
|
-
|
|
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
|
-
/**
|
|
262
|
-
function lines(
|
|
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
|
|
265
|
-
|
|
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,
|
|
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
|
-
|
|
375
|
-
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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.
|
|
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"
|