pi-pretty-tui 0.2.6 → 0.2.8
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 +10 -4
- package/README.zh-CN.md +10 -4
- package/extensions/index.ts +118 -20
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -6,8 +6,8 @@ A beautiful, polished TUI for the [Pi coding agent](https://pi.dev/).
|
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
|
-
- **Collapsible activity timeline:** instead of filling the conversation with individual calls, thoughts, tools, and ordinary `info` notifications share one chronological activity stream. Work appears beneath a single `Running(...)` row and settles into a quiet `Done(...)` row. Parent rows
|
|
10
|
-
- **Live working duration:** in clean mode, elapsed time appears beside `Working` in the prompt editor's top border, using the border's own color. It shares the agent-run clock and duration format with the completion footer, includes automatic retries, and disappears when the run settles. Narrow editors omit the timer when it cannot fit. The clean-mode `Running` parent dot alternates between
|
|
9
|
+
- **Collapsible activity timeline:** instead of filling the conversation with individual calls, thoughts, tools, and ordinary `info` notifications share one chronological activity stream. Work appears beneath a single `Running(...)` row and settles into a quiet `Done(...)` row. Parent rows show a combined total of top-level and nested tool calls, plus thoughts; expanding one reveals every ordered member with summaries retained above nested details. Thoughts and tools are peers: a turn that calls no tool still gets its own `Running(1 thought)` / `Done(1 thought)` parent row with the Thought as its child, so thinking is never dropped.
|
|
10
|
+
- **Live working duration:** in clean mode, elapsed time appears beside `Working` in the prompt editor's top border, using the border's own color. It shares the agent-run clock and duration format with the completion footer, includes automatic retries, and disappears when the run settles. Narrow editors omit the timer when it cannot fit. The clean-mode `Running` parent dot alternates between the prompt editor’s current border color and `dim` about every 800 ms; its label uses that same border color, and tool dots stay stable, and the pulse stops when the run settles. Expanded `Done` titles and markers use the theme's `success` color; collapsed summary dots, titles, and details use `dim`.
|
|
11
11
|
- **Transient UI status:** one line above the editor shows direct Pi UI feedback such as Thinking/tool-output visibility changes. A newer UI status replaces it, and it disappears automatically after ten seconds without entering the transcript or session.
|
|
12
12
|
- **Durable response footers:** once the complete agent run settles—including tools, retries, and compaction—the final answer ends with one quiet gray divider containing the elapsed-work time and, in fullscreen, a `[Copy]` control. It copies only the final visible answer, excluding thoughts, tools, statuses, and the footer itself; the footer survives reloads and session-tree navigation.
|
|
13
13
|
- **Useful details on demand:** tool-aware summaries keep paths, commands, results, diffs, file previews, and live shell output concise without removing access to the original content.
|
|
@@ -37,11 +37,15 @@ Displayed custom messages without their own renderer use one generic accent-colo
|
|
|
37
37
|
|
|
38
38
|
Pi's direct internal `showStatus()` feedback is transient: it appears only in the status line above the editor, a newer status replaces it, and a ten-second timer removes it. It never enters the transcript or activity timeline and is cleared on reload. In clean mode, `Ctrl+T` and `Ctrl+O` only show short hints to click the corresponding disclosure; neither changes visibility or Pi's Thinking setting. In full and compact modes, both shortcuts keep Pi's native behaviour.
|
|
39
39
|
|
|
40
|
+
## Error presentation
|
|
41
|
+
|
|
42
|
+
In clean mode, standalone errors keep their original title and complete text in a compact `✕ Error · reason` layout. Consecutive errors with exactly the same source type, title, and full text share one visible row with a dim `×2` / `×3` count. Other transcript components break the run; errors carrying thought/tool/visible-response content are not folded. This is UI-only: session records and original message components remain unchanged. Long errors wrap without an added preview truncation; full/compact modes keep native presentation.
|
|
43
|
+
|
|
40
44
|
## Codemode and nested tools
|
|
41
45
|
|
|
42
46
|
In clean mode, tools that call other tools through Pi's nested execution API keep their own parent node. Expand the activity group to see each internal call, its status, compact arguments, duration, and result summary. Click an internal call to inspect its arguments and text result independently. Click its expanded body to collapse just that call. Script and script-output bodies also collapse their own sections when clicked. Tool calls use a `●` marker: dim while running, success-colored when successful, and error-colored when failed. Tool names use bold theme text. `[Script]` and `[Script output]` are separate disclosures; the nested parent title is informational and clicking it does not expand or collapse details. Use each section or child to expand and collapse its own details; `Ctrl+O` only shows a hint in clean mode. Extensions can still use Pi's `setToolsExpanded()` API for global control; local disclosures remain usable afterward, and another global API call resets the local overrides. Calls retain their start order even when they finish in parallel. A handled child failure remains marked as failed even if the script succeeds.
|
|
43
47
|
|
|
44
|
-
The group
|
|
48
|
+
The activity-group summary combines top-level and nested calls: one parent call plus three internal calls displays as `4 tool calls`. The parent call itself counts once, while its node retains the nested-call count and tree structure. Internal calls never become phantom transcript members or displace the end-of-tree collapse action. Ownership uses Pi's `parentToolCallId`, not tool names or JavaScript parsing; wrappers other than codemode benefit too. This requires a Pi version with nested execution events (0.99.0 or later); older Pi keeps its existing display. Full and compact modes retain the native tool presentation.
|
|
45
49
|
|
|
46
50
|
Child text results and edit diffs are saved in hidden, UI-only session entries for reload and branch navigation. Each root retains at most 256 calls, 4,096 characters of arguments per call, 8,192 characters of text per result, and 131,072 characters of arguments/results in total; truncation is indicated. Detail previews also have a 200-line limit, and nested trees display up to eight levels below the root. Old sessions without a snapshot explicitly show that only call metadata was retained. Nested results currently use a generic text presentation, not each third-party renderer, and images are not stored in these snapshots. Root media remains available in native full/compact mode.
|
|
47
51
|
|
|
@@ -61,6 +65,8 @@ In fullscreen mode, left-click a user bubble's body or border to copy its comple
|
|
|
61
65
|
|
|
62
66
|
## Copy code blocks
|
|
63
67
|
|
|
68
|
+
Rounded code-block lines and corners use `borderMuted`; language labels, `[Copy]`, and code text retain their existing colors.
|
|
69
|
+
|
|
64
70
|
In fullscreen TUI mode, fenced Markdown code blocks in user and assistant transcript messages include a clickable `[Copy]` control in the top rule:
|
|
65
71
|
|
|
66
72
|
```sh
|
|
@@ -82,7 +88,7 @@ The suite covers live and restored activity timelines, retries and terminal stat
|
|
|
82
88
|
|
|
83
89
|
## Compatibility notice
|
|
84
90
|
|
|
85
|
-
The current development and regression-test baseline is **Pi 1.0
|
|
91
|
+
The current development and regression-test baseline is **Pi 1.1.0**. User chat bubbles support its direct Markdown layout, keeping their transparent background and content-sized width. Nested execution requires Pi 0.99.0 or later; this is a feature requirement, not a promise that every older Pi release is tested.
|
|
86
92
|
|
|
87
93
|
Tool rendering uses Pi's documented extension APIs. Active-tool state follows Pi's built-in execution transitions through its exported interactive components. Clean-mode thought details use the activity hierarchy's own disclosure state; full and compact modes retain Pi's built-in Thinking visibility control. Rendering native user messages as adaptive chat bubbles, rounding the native prompt editor, changing unordered-list markers, and enhancing fenced code blocks require reload-safe runtime patches because Pi does not currently expose public renderer hooks for those presentation details. Markdown enhancements are activated only while rendering user and assistant messages in the main transcript; Markdown created by extension overlays keeps Pi's native layout. Markdown parsing, syntax highlighting, and terminal semantic zones remain handled by Pi.
|
|
88
94
|
|
package/README.zh-CN.md
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
## 功能特性
|
|
8
8
|
|
|
9
|
-
- **可折叠的活动时间线:** 不再让大量独立调用填满对话。思考、工具和普通 `info` 通知会进入同一条按真实时间排序的活动流;工作进行时显示为 `Running(...)`,结束后变为低调的 `Done(...)
|
|
9
|
+
- **可折叠的活动时间线:** 不再让大量独立调用填满对话。思考、工具和普通 `info` 通知会进入同一条按真实时间排序的活动流;工作进行时显示为 `Running(...)`,结束后变为低调的 `Done(...)`。父级合并统计顶层与嵌套工具调用总数,并单独显示思考数量;展开后可查看所有有序成员,成员摘要在展开嵌套详情后仍然保留。思考与工具地位相同:没有调用任何工具的那一轮同样会有自己的 `Running(1 thought)` / `Done(1 thought)` 父行,思考作为其子级,内容不会被丢弃。
|
|
10
10
|
- **可持久化的回答 footer:** 整轮 Agent 工作真正 settled 后(包括工具、自动重试和 compaction),最终回答底部会显示一条低调的灰色分隔线,工作时长与 fullscreen 模式下的 `[Copy]` 控件都嵌在线中。`[Copy]` 只复制最终可见回答,不包含 thought、工具、状态或 footer;reload 和 session tree 切换后仍会保留。
|
|
11
|
-
- **实时工作计时:** clean 模式下,耗时显示在输入框顶部边框的 `Working` 右侧,颜色与边框一致。它与完成 footer 共用 Agent 整轮工作的计时起点和时长格式,包含自动重试,工作真正结束后消失;窄输入框放不下时会隐藏计时。clean 模式下,`Running` 父节点圆点约每 800 ms
|
|
11
|
+
- **实时工作计时:** clean 模式下,耗时显示在输入框顶部边框的 `Working` 右侧,颜色与边框一致。它与完成 footer 共用 Agent 整轮工作的计时起点和时长格式,包含自动重试,工作真正结束后消失;窄输入框放不下时会隐藏计时。clean 模式下,`Running` 父节点圆点约每 800 ms 在输入框当前边框色和 `dim` 间切换;`Running` 文字使用相同边框色,工具圆点保持稳定,整轮结束后停止闪烁。展开后的 `Done` 标题和圆点使用主题的 `success` 色,折叠摘要的圆点、标题与内容统一使用 `dim`。
|
|
12
12
|
- **临时 UI 状态:** 编辑器上方的一行只显示 Pi 直接产生的 UI 操作反馈,例如 Thinking 或工具输出可见性变化。新状态会替换旧状态,并在十秒后自动消失,不进入 transcript 或 session。
|
|
13
13
|
- **按需查看有效信息:** 针对不同工具设计的摘要,会简洁呈现路径、命令、结果、diff、文件预览和实时 shell 输出,同时保留查看原始完整内容的能力。
|
|
14
14
|
- **统一、精致的视觉体验:** 透明、右对齐的用户聊天气泡会根据短消息自适应宽度,长消息则在终端宽度的 75% 处换行;气泡使用主题链接色(`mdLink`)的圆角边框,并移除了多余的 `User` 标注。配合圆角输入框、主题感知配色、层级清晰的 Markdown 标题、更清爽的列表,以及带语法高亮的圆角代码块,让整个 TUI 更协调。在 fullscreen 模式下,每个围栏代码块还带有符合原生体验的 `[Copy]` 控件。
|
|
@@ -37,11 +37,15 @@ Pi 可能会提示内置工具被覆盖。这是正常现象:pi-pretty-tui 会
|
|
|
37
37
|
|
|
38
38
|
Pi 内部直接调用 `showStatus()` 产生的反馈属于临时 UI 状态:它只显示在编辑器上方,新状态替换旧状态,并由十秒 timer 自动清除;它不会进入 transcript 或活动时间线,reload 时也会清除。在 clean 模式下,`Ctrl+T` 和 `Ctrl+O` 都只显示点击对应节点的简短提示,不改变内容可见性或 Pi 的 Thinking 设置。在 full 和 compact 模式下,两者保留 Pi 的原生行为。
|
|
39
39
|
|
|
40
|
+
## 错误展示
|
|
41
|
+
|
|
42
|
+
clean 模式下,独立错误保留原始标题和完整内容,以紧凑的 `✕ Error · 原因` 展示。来源类型、标题及完整内容完全一致的相邻错误合为一条可见行,并使用暗色 `×2` / `×3` 标识次数。其他 transcript 组件会打断计数;包含思考、工具或可见回答的错误不参与合并。合并仅影响 UI,不删除或修改会话记录及原始消息组件。长错误完整换行,不增加预览截断;full/compact 模式保留原生展示。
|
|
43
|
+
|
|
40
44
|
## Codemode 与嵌套工具
|
|
41
45
|
|
|
42
46
|
clean 模式下,通过 Pi 嵌套执行 API 调用其他工具的工具保留自己的父节点。展开活动组后,会显示各内部调用的状态、紧凑参数、耗时和结果摘要;点击某个内部调用,可以独立展开其参数和文本结果;点击展开后的正文可收起该调用,脚本和脚本输出正文也可点击收起各自区块。工具统一使用 `●` 圆点:运行中为主题暗色,成功与失败分别为主题成功色与错误色;工具名使用正文色加粗。`[Script]` 和 `[Script output]` 分别控制脚本与汇总输出;嵌套父工具标题仅用于展示,点击不会展开或折叠详情。各区块或子调用通过自己的入口独立展开/收起,`Ctrl+O` 在 clean 模式下只显示提示。扩展仍可通过 Pi 的 `setToolsExpanded()` API 全局控制,之后也可局部展开/收起;再次调用全局 API 会重置局部状态。并行调用按照启动顺序排列,不会随完成顺序跳动。即使脚本捕获错误后成功,失败的子调用仍会明确标记。
|
|
43
47
|
|
|
44
|
-
|
|
48
|
+
活动组摘要合并统计顶层与嵌套调用:一个父调用加三个内部调用显示为 `4 tool calls`,父调用本身也计一次;父工具节点仍保留子调用数量和树结构。内部调用不会变成不存在的顶层 transcript 成员,也不会挤掉树尾折叠按钮。归属依据 Pi 的 `parentToolCallId`,不依赖工具名或 JavaScript 源码解析,因此其他嵌套调用工具也适用。这需要支持嵌套执行事件的 Pi(0.99.0 及以后);旧版本保持原有显示。full 和 compact 模式保留原生工具展示。
|
|
45
49
|
|
|
46
50
|
子工具的文本结果和 edit diff 会存入隐藏的、仅用于 UI 的 session 条目,支持 reload 和会话分支恢复。每个父调用最多保留 256 个子调用,每项参数最多 4,096 字符,每项文本结果最多 8,192 字符,参数和结果合计最多 131,072 字符;截断会明确提示。每项展开预览还设有 200 行上限,嵌套树最多展示父调用以下八层。没有快照的旧会话会明确显示仅保留调用记录。目前子结果使用通用文本视图,不调用各第三方工具的 renderer,也不在快照中保存图片;父工具自身的媒体结果仍可在原生 full/compact 模式查看。
|
|
47
51
|
|
|
@@ -61,6 +65,8 @@ fullscreen 模式下,左键点击用户气泡的正文或边框,可复制完
|
|
|
61
65
|
|
|
62
66
|
## 复制代码块
|
|
63
67
|
|
|
68
|
+
代码块的圆角与线条使用 `borderMuted`,语言标签、`[Copy]` 和代码正文保持原有配色。
|
|
69
|
+
|
|
64
70
|
在 fullscreen TUI 模式下,用户消息与助手回复中的 Markdown 围栏代码块会在顶部边框显示可点击的 `[Copy]` 控件:
|
|
65
71
|
|
|
66
72
|
```sh
|
|
@@ -82,7 +88,7 @@ npm test
|
|
|
82
88
|
|
|
83
89
|
## 兼容性说明
|
|
84
90
|
|
|
85
|
-
当前开发与回归测试基线为 **Pi 1.0
|
|
91
|
+
当前开发与回归测试基线为 **Pi 1.1.0**。用户气泡已适配该版本直接使用 Markdown 的布局,保留透明背景与按内容自适应的宽度。嵌套执行需要 Pi 0.99.0 及以后;这是功能要求,不代表每个旧版都经过测试。
|
|
86
92
|
|
|
87
93
|
工具渲染使用 Pi 的公开扩展 API。活动工具状态通过 Pi 导出的交互组件跟随其内置执行状态变化;clean 模式的 thought 详情使用活动时间线自身的展开状态,full 和 compact 模式则保留 Pi 内置的 Thinking 可见性控制。由于 Pi 目前没有为这些展示细节提供公开渲染钩子,自适应的原生用户消息气泡、原生输入编辑器圆角、无序列表标记和围栏代码块增强需要使用可安全重载的运行时补丁。Markdown 增强仅在渲染主 transcript 中的用户消息和助手回复时启用;扩展 overlay 创建的 Markdown 保持 Pi 原生布局。Markdown 解析、语法高亮和终端语义区域仍由 Pi 处理。
|
|
88
94
|
|
package/extensions/index.ts
CHANGED
|
@@ -99,6 +99,37 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
99
99
|
let fullscreenTui = false;
|
|
100
100
|
const mouseClickHitTests = new WeakMap<object, (event: any) => boolean>();
|
|
101
101
|
let currentTui: any;
|
|
102
|
+
let currentPromptEditor: any;
|
|
103
|
+
let errorChatContainer: any;
|
|
104
|
+
const runtimeErrorDescriptors = new WeakMap<object, { source: string; title: string; content: string }>();
|
|
105
|
+
const errorDescriptor = (component: any) => {
|
|
106
|
+
const runtime = runtimeErrorDescriptors.get(component);
|
|
107
|
+
if (runtime) return runtime;
|
|
108
|
+
if (!(component instanceof AssistantMessageComponent)) return undefined;
|
|
109
|
+
const message = (component as any).lastMessage;
|
|
110
|
+
if (message?.stopReason !== "error" || (message.content ?? []).some((item: any) =>
|
|
111
|
+
item.type === "toolCall" || item.type === "thinking" || item.type === "text" && item.text?.length)) return undefined;
|
|
112
|
+
return { source: "assistant:error", title: "Error", content: String(message.errorMessage ?? "") };
|
|
113
|
+
};
|
|
114
|
+
const errorRun = (component: any) => {
|
|
115
|
+
const children: any[] = errorChatContainer?.children ?? [];
|
|
116
|
+
const index = children.indexOf(component);
|
|
117
|
+
const descriptor = errorDescriptor(component);
|
|
118
|
+
const same = (other: any) => {
|
|
119
|
+
const value = errorDescriptor(other);
|
|
120
|
+
return descriptor && value && value.source === descriptor.source &&
|
|
121
|
+
value.title === descriptor.title && value.content === descriptor.content;
|
|
122
|
+
};
|
|
123
|
+
if (index < 0 || !descriptor) return { hidden: false, count: 1, compact: false };
|
|
124
|
+
let count = 1;
|
|
125
|
+
while (index + count < children.length && same(children[index + count])) count++;
|
|
126
|
+
return { hidden: index > 0 && Boolean(same(children[index - 1])), count,
|
|
127
|
+
compact: index > 0 && Boolean(errorDescriptor(children[index - 1])) };
|
|
128
|
+
};
|
|
129
|
+
const runningColor = (theme: any, text: string): string =>
|
|
130
|
+
typeof currentPromptEditor?.borderColor === "function"
|
|
131
|
+
? currentPromptEditor.borderColor(text)
|
|
132
|
+
: theme.fg("border", text);
|
|
102
133
|
let currentExtensionUi: any;
|
|
103
134
|
let changingAllToolsExpansion = false;
|
|
104
135
|
const latestActivityWidgetKey = "pretty-tui-latest-activity";
|
|
@@ -420,6 +451,10 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
420
451
|
}
|
|
421
452
|
}
|
|
422
453
|
};
|
|
454
|
+
const requestActivityRender = (group?: ActivityGroup): void => {
|
|
455
|
+
if (group) refreshActivityGroup(group);
|
|
456
|
+
currentTui?.requestRender?.();
|
|
457
|
+
};
|
|
423
458
|
const revealActivityGroup = (group: ActivityGroup, ui?: any): void => {
|
|
424
459
|
collapsedActivityGroups.delete(group.id);
|
|
425
460
|
revealedActivityGroups.add(group.id);
|
|
@@ -544,7 +579,31 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
544
579
|
);
|
|
545
580
|
};
|
|
546
581
|
|
|
547
|
-
const
|
|
582
|
+
const renderStandaloneError = (member: ActivityMember, width: number, component?: any): string[] => {
|
|
583
|
+
const run = component ? errorRun(component) : { hidden: false, count: 1, compact: false };
|
|
584
|
+
if (run.hidden) return [];
|
|
585
|
+
const theme = defaultActivityTheme();
|
|
586
|
+
const style = activityUpdateStyle(member, theme);
|
|
587
|
+
const original = component ? errorDescriptor(component) : undefined;
|
|
588
|
+
const title = stripTerminalSequences(original?.title ?? member.updateTitle ?? "Error");
|
|
589
|
+
const content = stripTerminalSequences(original?.content || member.updateContent || "");
|
|
590
|
+
const heading = style.icon + style.title(title);
|
|
591
|
+
const prefix = heading + (content ? theme.fg("dim", " · ") : "");
|
|
592
|
+
const text = prefix + theme.fg("muted", content) + (run.count > 1 ? theme.fg("dim", ` ×${run.count}`) : "");
|
|
593
|
+
// Wrap rather than truncate; all original detail lines remain accessible.
|
|
594
|
+
const continuation = " ".repeat(Math.min(visibleWidth(prefix), Math.max(0, Math.floor(width / 3))));
|
|
595
|
+
const rows: string[] = [];
|
|
596
|
+
for (const line of text.split("\n")) {
|
|
597
|
+
const wrapped = wrapTextWithAnsi(line, Math.max(1, width - visibleWidth(continuation)));
|
|
598
|
+
rows.push(...wrapped.map((row, index) => (rows.length || index ? continuation : "") + row));
|
|
599
|
+
}
|
|
600
|
+
return [...(run.compact ? [] : [""]), ...rows.map((line) => truncateToWidth(line, Math.max(1, width), ""))];
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
const renderPendingActivityUpdate = (member: ActivityMember, width: number, component?: any): string[] => {
|
|
604
|
+
if (member.updateSeverity === "error" && member.updateTitle !== "Response truncated") {
|
|
605
|
+
return renderStandaloneError(member, width, component);
|
|
606
|
+
}
|
|
548
607
|
const theme = defaultActivityTheme();
|
|
549
608
|
const title = truncateToWidth(member.updateTitle ?? "Update", Math.max(1, width - 2), "…");
|
|
550
609
|
const style = activityUpdateStyle(member, theme);
|
|
@@ -563,8 +622,9 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
563
622
|
group: ActivityGroup,
|
|
564
623
|
member: ActivityMember,
|
|
565
624
|
width: number,
|
|
625
|
+
component?: any,
|
|
566
626
|
): string[] => {
|
|
567
|
-
if (!activityGroupCollapsible(group)) return renderPendingActivityUpdate(member, width);
|
|
627
|
+
if (!activityGroupCollapsible(group)) return renderPendingActivityUpdate(member, width, component);
|
|
568
628
|
const position = activityMemberPosition(group, member);
|
|
569
629
|
if (!activityGroupRevealed(group)) {
|
|
570
630
|
return position.first
|
|
@@ -722,6 +782,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
722
782
|
const originalEditorHandleMouse = customEditorPrototype.handleMouse;
|
|
723
783
|
const patchedEditorTopBorder = function (this: any, width: number, hiddenLineCount: number): string {
|
|
724
784
|
if (this.tui) currentTui = this.tui;
|
|
785
|
+
currentPromptEditor = this;
|
|
725
786
|
const indicator = this.workingStatusIndicator;
|
|
726
787
|
if (renderMode !== "clean" || indicator?.kind !== "working" || responseRunStartedAt === undefined) {
|
|
727
788
|
return originalEditorTopBorder.call(this, width, hiddenLineCount);
|
|
@@ -743,6 +804,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
743
804
|
}
|
|
744
805
|
};
|
|
745
806
|
const patchedEditorRender = function (this: any, width: number): string[] {
|
|
807
|
+
currentPromptEditor = this;
|
|
746
808
|
if (width < 3) return originalEditorRender.call(this, width);
|
|
747
809
|
|
|
748
810
|
const innerWidth = width - 2;
|
|
@@ -786,6 +848,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
786
848
|
customEditorPrototype.handleMouse = patchedEditorHandleMouse;
|
|
787
849
|
|
|
788
850
|
pi.on("session_shutdown", () => {
|
|
851
|
+
currentPromptEditor = undefined;
|
|
789
852
|
const patch = customEditorPrototype[editorFramePatchKey];
|
|
790
853
|
if (!patch) return;
|
|
791
854
|
if (patch.patchedRender === customEditorPrototype.render) {
|
|
@@ -1230,7 +1293,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1230
1293
|
const terminalGroup = activityTimeline.groupForMember(terminalMember.id);
|
|
1231
1294
|
if (!terminalGroup) return originalRender.call(this, width);
|
|
1232
1295
|
activityUpdateComponents.set(terminalMember.id, this);
|
|
1233
|
-
return renderActivityUpdateProjection(terminalGroup, terminalMember, width);
|
|
1296
|
+
return renderActivityUpdateProjection(terminalGroup, terminalMember, width, this);
|
|
1234
1297
|
}
|
|
1235
1298
|
|
|
1236
1299
|
const key = assistantMessageKey(message);
|
|
@@ -1495,10 +1558,14 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1495
1558
|
const group = activityTimeline.groupForMember(member.id);
|
|
1496
1559
|
if (!group || !mode.chatContainer?.addChild) return false;
|
|
1497
1560
|
const component: Component = {
|
|
1498
|
-
render: (width: number) => renderActivityUpdateProjection(group, member, width),
|
|
1561
|
+
render: (width: number) => renderActivityUpdateProjection(group, member, width, component),
|
|
1499
1562
|
invalidate() {},
|
|
1500
1563
|
handleMouse: (event: any) => handleActivityUpdateMouse(group, member, event),
|
|
1501
1564
|
} as Component;
|
|
1565
|
+
if (severity === "error") runtimeErrorDescriptors.set(component, {
|
|
1566
|
+
source: "runtime:error", title, content,
|
|
1567
|
+
});
|
|
1568
|
+
errorChatContainer = mode.chatContainer;
|
|
1502
1569
|
activityUpdateComponents.set(member.id, component);
|
|
1503
1570
|
mode.chatContainer.addChild(component);
|
|
1504
1571
|
mode.ui?.requestRender?.();
|
|
@@ -1557,6 +1624,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1557
1624
|
|
|
1558
1625
|
const patchedRenderSessionEntries = function (this: any, entries: any[], options?: any) {
|
|
1559
1626
|
currentTui = this.ui;
|
|
1627
|
+
errorChatContainer = this.chatContainer;
|
|
1560
1628
|
fullscreenTui = currentTui?.mode === "fullscreen";
|
|
1561
1629
|
// buildContextEntries() prepends the latest compaction for model context,
|
|
1562
1630
|
// while Pi's live compaction UI appends it chronologically. Keep reloads
|
|
@@ -1662,6 +1730,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1662
1730
|
// calling sites, rather than depend on private chat-container children or
|
|
1663
1731
|
// instanceof checks (these can be wrapped or come from another module).
|
|
1664
1732
|
const patchedAddMessageToChat = function (this: any, message: any, ...args: any[]) {
|
|
1733
|
+
errorChatContainer = this.chatContainer;
|
|
1665
1734
|
if (message?.role === "assistant") cacheMessageContexts.set(this, message);
|
|
1666
1735
|
else cacheMessageContexts.delete(this);
|
|
1667
1736
|
return originalAddMessageToChat.call(this, message, ...args);
|
|
@@ -1862,6 +1931,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1862
1931
|
|
|
1863
1932
|
pi.on("session_shutdown", () => {
|
|
1864
1933
|
fullscreenTui = false;
|
|
1934
|
+
errorChatContainer = undefined;
|
|
1865
1935
|
currentTui = undefined;
|
|
1866
1936
|
clearLatestActivityStatus();
|
|
1867
1937
|
currentExtensionUi = undefined;
|
|
@@ -2254,17 +2324,23 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2254
2324
|
);
|
|
2255
2325
|
const buttonStart = visibleWidth(topPrefix) + fillWidth + (showCopyButton ? 1 : 0);
|
|
2256
2326
|
const topRule = topPrefix + "─".repeat(fillWidth) + topSuffix;
|
|
2257
|
-
const
|
|
2327
|
+
const frameBorder = (text: string) => currentExtensionUi?.theme?.fg
|
|
2328
|
+
? currentExtensionUi.theme.fg("borderMuted", text)
|
|
2329
|
+
: this.theme.codeBlockBorder(text);
|
|
2330
|
+
// Keep labels and Copy at their original contrast; soften only the frame.
|
|
2331
|
+
const styledTopRule = frameBorder("╭─ ") + this.theme.codeBlockBorder(label + " ") +
|
|
2332
|
+
frameBorder("─".repeat(fillWidth)) +
|
|
2333
|
+
(showCopyButton ? this.theme.codeBlockBorder(` ${copyLabel} `) : "") + frameBorder("╮");
|
|
2258
2334
|
const framedCodeWidth = Math.max(1, frameWidth - 4);
|
|
2259
2335
|
const lines = [
|
|
2260
2336
|
styledTopRule,
|
|
2261
2337
|
...codeLines.map((line) =>
|
|
2262
|
-
|
|
2338
|
+
frameBorder("│ ") +
|
|
2263
2339
|
line +
|
|
2264
2340
|
" ".repeat(Math.max(0, framedCodeWidth - visibleWidth(line))) +
|
|
2265
|
-
|
|
2341
|
+
frameBorder(" │")
|
|
2266
2342
|
),
|
|
2267
|
-
|
|
2343
|
+
frameBorder(`╰${"─".repeat(Math.max(0, frameWidth - 2))}╯`),
|
|
2268
2344
|
];
|
|
2269
2345
|
|
|
2270
2346
|
if (showCopyButton && Array.isArray(this[codeBlockCollectionKey])) {
|
|
@@ -3103,6 +3179,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
3103
3179
|
}
|
|
3104
3180
|
|
|
3105
3181
|
const finishCleanGroup = (activity = cleanRun.activity ?? "done") => {
|
|
3182
|
+
const activeGroup = activityTimeline.currentGroup();
|
|
3106
3183
|
if (cleanRun.lastCompletedToolCallId && cleanRun.count > 0) {
|
|
3107
3184
|
const group: ToolSummaryGroup = {
|
|
3108
3185
|
count: cleanRun.count,
|
|
@@ -3129,6 +3206,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
3129
3206
|
cleanRun.activeToolCallId = undefined;
|
|
3130
3207
|
cleanRun.activeToolName = undefined;
|
|
3131
3208
|
activityTimeline.boundary();
|
|
3209
|
+
requestActivityRender(activeGroup);
|
|
3132
3210
|
};
|
|
3133
3211
|
|
|
3134
3212
|
closeCleanAtNativeCustomBoundary = () => {
|
|
@@ -3164,7 +3242,8 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
3164
3242
|
const activityText = /^thinking(?:\.\.\.)?$/iu.test(normalizedActivityText)
|
|
3165
3243
|
? "thinking"
|
|
3166
3244
|
: rawActivityText;
|
|
3167
|
-
const
|
|
3245
|
+
const totalCalls = count + nestedCount;
|
|
3246
|
+
const countLabel = totalCalls > 0 ? `${totalCalls} tool ${totalCalls === 1 ? "call" : "calls"}` : "";
|
|
3168
3247
|
const thoughtLabel = thoughtCount > 0
|
|
3169
3248
|
? `${thoughtCount} ${thoughtCount === 1 ? "thought" : "thoughts"}`
|
|
3170
3249
|
: "";
|
|
@@ -3173,7 +3252,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
3173
3252
|
const activityLabel = count > 0 && activityText !== "done" && activityText.trim()
|
|
3174
3253
|
? activityText
|
|
3175
3254
|
: "";
|
|
3176
|
-
return [countLabel,
|
|
3255
|
+
return [countLabel, thoughtLabel, activityLabel].filter(Boolean).join(" · ");
|
|
3177
3256
|
};
|
|
3178
3257
|
|
|
3179
3258
|
const summaryRow = (
|
|
@@ -3190,19 +3269,24 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
3190
3269
|
const dimPhase = responseRunStartedAt !== undefined &&
|
|
3191
3270
|
Math.floor(Math.max(0, Date.now() - responseRunStartedAt) / RUNNING_DOT_INTERVAL_MS) % 2 === 1;
|
|
3192
3271
|
const color = currentActivity === "done"
|
|
3193
|
-
? collapsedDone ? "
|
|
3272
|
+
? collapsedDone ? "dim" : "success"
|
|
3194
3273
|
: dimPhase ? "dim" : "accent";
|
|
3195
|
-
return
|
|
3274
|
+
return currentActivity !== "done" && !dimPhase
|
|
3275
|
+
? runningColor(theme, "● ")
|
|
3276
|
+
: theme.fg(color, "● ");
|
|
3196
3277
|
},
|
|
3197
3278
|
continuation: " ",
|
|
3198
3279
|
content: () => {
|
|
3199
3280
|
const currentActivity = typeof activity === "function" ? activity() : activity;
|
|
3200
3281
|
const label = currentActivity === "done" ? "Done" : "Running";
|
|
3201
3282
|
const color = label === "Done"
|
|
3202
|
-
? collapsedDone ? "
|
|
3283
|
+
? collapsedDone ? "dim" : "success"
|
|
3203
3284
|
: "accent";
|
|
3204
|
-
const detailColor = label === "Done" && collapsedDone ? "
|
|
3205
|
-
|
|
3285
|
+
const detailColor = label === "Done" && collapsedDone ? "dim" : "text";
|
|
3286
|
+
const title = label === "Running"
|
|
3287
|
+
? runningColor(theme, theme.bold(label))
|
|
3288
|
+
: theme.fg(color, theme.bold(label));
|
|
3289
|
+
return title +
|
|
3206
3290
|
theme.fg("dim", "(") +
|
|
3207
3291
|
theme.fg(detailColor, summaryText(count, failed, thoughtCount, currentActivity, nestedCount)) +
|
|
3208
3292
|
theme.fg("dim", ")");
|
|
@@ -3256,6 +3340,10 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
3256
3340
|
const toolCount = group.toolCallIds.length;
|
|
3257
3341
|
const nestedCount = group.toolCallIds.reduce((count, id) => count + nestedTools.count(id), 0);
|
|
3258
3342
|
const live = () => cleanRun.activeToolName ?? currentCleanActivity();
|
|
3343
|
+
const currentGroup = activityTimeline.currentGroup();
|
|
3344
|
+
const isLiveGroup = currentGroup?.id === group.id &&
|
|
3345
|
+
!activityTimeline.isSettled(group.id) &&
|
|
3346
|
+
!cleanRun.settled;
|
|
3259
3347
|
if (settled) {
|
|
3260
3348
|
return fit(block([summaryRow(
|
|
3261
3349
|
summaryTheme,
|
|
@@ -3267,8 +3355,8 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
3267
3355
|
nestedCount,
|
|
3268
3356
|
)]).render(width));
|
|
3269
3357
|
}
|
|
3270
|
-
if (owner &&
|
|
3271
|
-
//
|
|
3358
|
+
if (owner && isLiveGroup) {
|
|
3359
|
+
// Only the current, unsettled group may consume live run state.
|
|
3272
3360
|
return fit(block([summaryRow(
|
|
3273
3361
|
summaryTheme,
|
|
3274
3362
|
Math.max(toolCount, liveCleanToolCount()),
|
|
@@ -3279,8 +3367,8 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
3279
3367
|
nestedCount,
|
|
3280
3368
|
)]).render(width));
|
|
3281
3369
|
}
|
|
3282
|
-
if (!owner &&
|
|
3283
|
-
// Thought-only group while the model is still working.
|
|
3370
|
+
if (!owner && isLiveGroup) {
|
|
3371
|
+
// Thought-only current group while the model is still working.
|
|
3284
3372
|
return fit(block([summaryRow(
|
|
3285
3373
|
summaryTheme,
|
|
3286
3374
|
toolCount,
|
|
@@ -3314,6 +3402,11 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
3314
3402
|
cleanToolThemes.set(toolCallId, theme);
|
|
3315
3403
|
const activityGroup = activityTimeline.groupForTool(toolCallId);
|
|
3316
3404
|
if (activityGroup) activityFallbackThemes.set(activityGroup.id, theme);
|
|
3405
|
+
const isLiveGroup = !activityGroup || (
|
|
3406
|
+
activityTimeline.currentGroup()?.id === activityGroup.id &&
|
|
3407
|
+
!activityTimeline.isSettled(activityGroup.id) &&
|
|
3408
|
+
!cleanRun.settled
|
|
3409
|
+
);
|
|
3317
3410
|
return {
|
|
3318
3411
|
render(width: number): string[] {
|
|
3319
3412
|
if (renderMode !== "clean") return [];
|
|
@@ -3331,7 +3424,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
3331
3424
|
}
|
|
3332
3425
|
if (cleanRun.settled) return [];
|
|
3333
3426
|
|
|
3334
|
-
if (cleanRun.activeToolCallId === toolCallId) {
|
|
3427
|
+
if (isLiveGroup && cleanRun.activeToolCallId === toolCallId) {
|
|
3335
3428
|
return block([summaryRow(
|
|
3336
3429
|
theme,
|
|
3337
3430
|
liveCleanToolCount(),
|
|
@@ -3344,6 +3437,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
3344
3437
|
// The active tool owns the live Running row. Keep the latest completed
|
|
3345
3438
|
// portion hidden while that tool's minimum display time is running.
|
|
3346
3439
|
if (
|
|
3440
|
+
isLiveGroup &&
|
|
3347
3441
|
cleanRun.lastCompletedToolCallId === toolCallId &&
|
|
3348
3442
|
cleanRun.count > 0 &&
|
|
3349
3443
|
!cleanRun.activeToolCallId
|
|
@@ -3949,6 +4043,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
3949
4043
|
cleanRun.active = true;
|
|
3950
4044
|
cleanRun.activeToolCallId = event.toolCallId;
|
|
3951
4045
|
beginToolActivity(event.toolCallId, displayName, true);
|
|
4046
|
+
requestActivityRender(activityTimeline.groupForTool(event.toolCallId));
|
|
3952
4047
|
});
|
|
3953
4048
|
pi.on("tool_execution_update", (event) => {
|
|
3954
4049
|
if ((event as any).parentToolCallId) {
|
|
@@ -3990,6 +4085,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
3990
4085
|
}
|
|
3991
4086
|
const visibleSummaryToolCallId = cleanRun.activeToolCallId ?? cleanRun.lastCompletedToolCallId;
|
|
3992
4087
|
setCleanGroupMembers(visibleSummaryToolCallId, cleanRun.currentToolCallIds.slice());
|
|
4088
|
+
requestActivityRender(activityTimeline.groupForTool(event.toolCallId));
|
|
3993
4089
|
// Tool success and failure remain owned by the activity group. The bottom
|
|
3994
4090
|
// line is reserved for transient UI status messages.
|
|
3995
4091
|
});
|
|
@@ -4029,6 +4125,8 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
4029
4125
|
cleanRun.activeToolName = undefined;
|
|
4030
4126
|
cleanRun.activity = "done";
|
|
4031
4127
|
cleanRun.settled = true;
|
|
4128
|
+
for (const group of activityTimeline.groups()) refreshActivityGroup(group);
|
|
4129
|
+
currentTui?.requestRender?.();
|
|
4032
4130
|
|
|
4033
4131
|
if (responseRunStartedAt !== undefined) {
|
|
4034
4132
|
// message_end runs before later extensions can replace the message and
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-pretty-tui",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
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>",
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
"@earendil-works/pi-tui": "*"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@earendil-works/pi-coding-agent": "1.0
|
|
43
|
-
"@earendil-works/pi-tui": "1.0
|
|
42
|
+
"@earendil-works/pi-coding-agent": "1.1.0",
|
|
43
|
+
"@earendil-works/pi-tui": "1.1.0",
|
|
44
44
|
"@types/node": "^24.0.0",
|
|
45
45
|
"jiti": "^2.7.0",
|
|
46
46
|
"typescript": "^5.9.0"
|