pi-open-tui 0.3.14 → 0.4.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.zh-CN.md CHANGED
@@ -1,170 +1,197 @@
1
- # pi-open-tui
2
-
3
- [English](./README.md) | **简体中文**
4
-
5
- 一个为 [Pi](https://pi.dev) 编程代理打造的终端界面扩展,将 pi-haiku、pi-claude-code-tui 与 pi-zentui 的优秀设计整合为统一、可配置的使用体验。
6
-
7
- ![pi-open-tui 预览](https://raw.githubusercontent.com/OldSuns/pi-open-tui/main/assets/preview_dashboard_1.png)
8
-
9
- ## 功能亮点
10
-
11
- - **Pi 顶栏**:显示模型、思考等级、当前目录和常用斜杠命令提示
12
- - **自适应底栏**:集中展示 Git 状态、运行环境、上下文用量、Token、费用和扩展状态
13
- - **带边框的编辑器**:支持块状、竖线和下划线三种光标样式
14
- - **项目环境感知**:识别 50 多种运行环境,并展示 ahead/behind、已暂存、已修改、未跟踪、stash 和 detached HEAD 等 Git 状态
15
- - **单轮遥测**:展示 TPS、首 Token 延迟(TTFT)、耗时、停顿、Token 数量和模型标价速率
16
- - **思考预览**:模型工作时,在 Pi 隐藏思考块的 `Thinking...` 位置显示实时字幕,展示推理内容的末尾片段
17
- - **交互式设置**:通过 `/open-tui` 配置,并支持英文和简体中文界面
18
-
19
- ## 环境要求
20
-
21
- - Pi 1.0 或更高版本
22
- - 支持 UTF-8 和彩色输出的终端
23
- - 使用完整图标集时需要 [Nerd Font](https://www.nerdfonts.com/font-downloads)(可选;内置可移植 Unicode 图标)
24
-
25
- ## 安装
26
-
27
- 安装扩展:
28
-
29
- ```bash
30
- pi install npm:pi-open-tui
31
- ```
32
-
33
- 也可以只在当前会话中试用:
34
-
35
- ```bash
36
- pi -e npm:pi-open-tui
37
- ```
38
-
39
- ## 字体与图标
40
-
41
- 可从 [Nerd Fonts 官方下载页](https://www.nerdfonts.com/font-downloads)或 [GitHub 最新版本](https://github.com/ryanoasis/nerd-fonts/releases/latest)下载任意已修补字体。安装后,请在终端配置中选择该字体,并重启终端。
42
-
43
- 默认的 `auto` 模式检查终端环境,而不是已安装的字体文件。在支持 UTF-8 的交互式 TTY 中,它会使用 Nerd Font 图标,即使 Pi 运行在 runner 或子 shell 中。如果图标显示为方框、乱码或错误符号,请打开 `/open-tui`,在**外观**页选择合适的模式:
44
-
45
- - `nerd`:终端已配置 Nerd Font 时,强制使用 Nerd Font 图标
46
- - `unicode`:可移植 Unicode 图标(文件夹、分支、笔记本电脑、灯泡、插头、沙漏等),无需修补字体;emoji 字形通过终端的 emoji 回退渲染
47
- - `ascii`:使用纯文本图标,无需安装修补字体
48
- - `auto`:在支持 UTF-8 的交互式 TTY 中使用 Nerd Font 图标;SSH 会话使用可移植 Unicode 图标(字体由客户端终端决定,通常没有 Nerd Font);非交互式输出、`TERM=dumb` 或明确配置为非 UTF-8 的 locale 使用 ASCII
49
-
50
- 如果已经安装字体,但 `auto` 仍选择 ASCII,请手动切换为 `nerd`。使用 VS Code、Windows Terminal 等应用时,只在操作系统中安装字体还不够,还需要在对应的终端配置中选中该字体。如果 SSH 客户端终端确实安装了 Nerd Font 并希望在 SSH 下使用完整图标,请将模式显式设为 `nerd`。
51
-
52
- 如果较宽的 Nerd Font 图标占用了图标与文字之间的间隔(例如在 Kitty 中),可开启 `footerSegments.nerdIconSpacing`(`/open-tui` 中的 **底栏 → Nerd 图标后额外留空**)。开启后底栏会在 Nerd Font 图标后额外预留一格空白,包括运行环境图标,常规底栏和内嵌底栏的宽度计算都包含这格留白;Unicode/ASCII 图标与紧凑的 Git 状态标记不受影响。Kitty 用户也可以在 `kitty.conf` 中设置 `narrow_symbols U+E000-U+F8FF 1`。
53
-
54
- ## 配置
55
-
56
- 运行 `/open-tui` 打开设置窗口,其中包含**常规**、**外观**、**底栏**和**遥测**四个页面。设置保存在 `~/.pi/agent/open-tui.json`:
57
-
58
- ```json
59
- {
60
- "enabled": true,
61
- "inlineFooter": false,
62
- "settingsLanguage": "zh",
63
- "cursorStyle": "block",
64
- "icons": {
65
- "mode": "auto"
66
- },
67
- "footerSegments": {
68
- "cwd": true,
69
- "hostname": false,
70
- "sessionName": false,
71
- "gitBranch": true,
72
- "gitStatus": true,
73
- "gitCommit": false,
74
- "runtime": true,
75
- "context": true,
76
- "tokens": true,
77
- "cost": true,
78
- "liveTps": true,
79
- "extensionStatuses": true,
80
- "capitalizeProviderName": true,
81
- "nerdIconSpacing": false
82
- },
83
- "telemetry": {
84
- "enabled": true,
85
- "tps": true,
86
- "ttft": true,
87
- "duration": true,
88
- "tokens": true,
89
- "stalls": true,
90
- "cost": true
91
- },
92
- "thinkingPeek": {
93
- "lines": 1
94
- }
95
- }
96
- ```
97
-
98
- 主要选项:
99
-
100
- | 选项 | 可选值 | 说明 |
101
- | --- | --- | --- |
102
- | `settingsLanguage` | `en`、`zh` | 切换 `/open-tui` 设置界面的语言 |
103
- | `inlineFooter` | `true`、`false` | 将两条主要 Footer 信息行移入编辑器上下边框以节省垂直空间,默认关闭;扩展状态行仍显示在编辑器外 |
104
- | `cursorStyle` | `block`、`bar`、`underline` | `bar` 和 `underline` 需要终端支持光标形状转义序列 |
105
- | `icons.mode` | `auto`、`nerd`、`unicode`、`ascii` | 控制底栏和遥测通知使用的图标 |
106
- | `footerSegments` | 布尔开关 | 分别控制底栏中的各项数据 |
107
- | `footerSegments.capitalizeProviderName` | 布尔开关 | 将底栏中提供商名称的首字母大写;设为 `false` 时保留原始大小写 |
108
- | `footerSegments.nerdIconSpacing` | 布尔开关 | 在底栏 Nerd Font 图标后额外留一格空白;默认 `false` |
109
- | `telemetry` | 布尔开关 | 控制遥测总开关和各项指标 |
110
- | `thinkingPeek.lines` | `0`、`1`、`2` | 关闭、单行或双行思考预览 |
111
-
112
- `sessionName` 仅在会话有名称时显示;`hostname` 会显示主机名的短名称(主机名的第一个标签,例如从 `mba.example.com` 显示为 `mba`),并使用服务器图标;处于分离 HEAD(detached HEAD)状态时,`gitBranch` 会显示 `HEAD` 和短提交哈希,`gitCommit` 会在其后追加指向该提交的标签,且仅在开启 `gitBranch` 时生效;关闭 `extensionStatuses` 会隐藏整行扩展状态,其中也包括 MCP 状态。每条状态会保留其扩展通过 `ctx.ui.theme.fg()` 设置的颜色;未设置颜色的状态使用 muted 主题色显示。
113
-
114
- 开启 `inlineFooter` 后,两条常规 Footer 信息行会移入编辑器边框,从而节省垂直空间。顶部边框左侧显示 Git 分支,右侧信息组以当前目录开头;开启 `sessionName` 时,会话标题也会显示在左侧。Header 和扩展状态行保持独立显示;终端较窄时优先截断低优先级 Footer 数据,保留右侧统计信息和边框角。
115
-
116
- ## 单轮遥测
117
-
118
- 每次 Agent 完整运行结束后,pi-open-tui 会显示一条临时结果,并将其中的多个工具调用轮次合并统计:
119
-
120
- ```text
121
- > TPS 42.5 tok/s | ~ TTFT 1.2s | + 29.7s | ↑ 567 | ↓ 1.2k | ! stall 1x / 4.3s | $ $3.60/M
122
- ```
123
-
124
- TPS 的计算方式是:将本次运行中服务商报告的全部 Assistant 输出 Token,除以各个生成轮次的总耗时。计时范围从 `turn_start` 到 Assistant 的 `message_end`,包含 TTFT、隐藏推理、缓冲和停顿,但不包含轮次之间的工具执行时间。没有输出 Token 或无法测得生成时间时,会显示 `TPS —`。
125
-
126
- 回复流式输出时,工作计时器还会显示当前的估算速度,例如 `working 12.3s · ~54 tok/s`。`~` 表示估算值:它是最近 2 秒内流式输出的正文、思考内容和工具调用参数的速率,按拉丁文本和代码约 4 个字符 1 个 Token、中日韩字符每字 1 个 Token 估算。停顿时数值会逐渐降向零;工具执行期间和消息之间以暗色保留上一个值。由**底栏 → 实时 TPS**(`footerSegments.liveTps`)控制,与遥测通知的开关互不影响。运行结束后不再显示,以上面的精确 TPS 为准。
127
-
128
- `$ / M` 表示根据 `usage.cost.total` 得到的模型标价速率,不是底栏中的会话累计费用。所有遥测字段都可以在**遥测**页单独开关。
129
-
130
- ## 思考预览
131
-
132
- 开启 Pi 的 **Hide thinking** 后,任务运行期间 pi-open-tui 会在原生隐藏思考块的 `Thinking...` 位置显示紧凑的实时字幕。`/open-tui` 中提供**关闭、单行、双行**三种模式:
133
-
134
- - 模型推理时,思考内容的末尾片段会随 spinner 滚动(`~ think ⠋ …`);
135
- - 开始输出正文时定格为对勾(`~ think ✓`);
136
- - 双行模式中,上一条和最新一条思考内容使用相同的文字缩进;如果最新一行超出宽度,则两行随新 Token 持续显示它的最新末尾片段,不再保留上一条;
137
- - 任务结束后恢复原生 `Thinking...` 标签。
138
-
139
- ```text
140
- ~ think ⠋ 上一条思考
141
- 最新一条思考
142
- ```
143
-
144
- 该字幕仅在模型真正输出推理内容后出现,非推理模型不会显示。可见性由 Pi 自身的 Hide thinking 开关控制,切换后立即生效。每一行都按*显示宽度*截断(全角字符计 2 列),因此包含中文的思考文本也不会溢出终端。可在 `/open-tui` 的**常规 → 思考预览**中切换,或直接修改 `open-tui.json` 中的 `thinkingPeek.lines`。
145
-
146
- ## 本地开发
147
-
148
- ```bash
149
- npm install
150
- npm test
151
- npm run typecheck
152
- pi -e .
153
- ```
154
-
155
- ## 致谢
156
-
157
- 本项目基于多个 Pi 社区包的工作:
158
-
159
- - **[pi-haiku](https://github.com/nnocte/pi-haiku)** — 双行底栏结构和工作计时器
160
- - **[pi-claude-code-tui](https://github.com/Phoobobo/pi-claude-code-tui)** — Pi Logo 帧与圆角编辑器边框技术
161
- - **[pi-zentui](https://github.com/lmilojevicc/pi-zentui)** — Starship 风格底栏、运行环境检测、会话生命周期和设置界面模式
162
- - **[pi-tps](https://github.com/monotykamary/pi-tps)** — 单轮计时、停顿检测和保守的 TPS 计算方式
163
-
164
- Logo 帧源自 Pi 官方安装脚本(`pi.dev/install.sh`)。运行环境检测和 Git porcelain 解析借鉴了 `pi-zentui` 的结构。
165
-
166
- 特别感谢 **[LINUX DO](https://linux.do)** 社区的支持。
167
-
168
- ## 许可证
169
-
170
- [MIT](./LICENSE)
1
+ # pi-open-tui
2
+
3
+ [![omp 状态](https://omppackages.com/badge/pi-open-tui.svg)](https://omppackages.com/packages/pi-open-tui)
4
+
5
+ [English](./README.md) | **简体中文**
6
+
7
+ 一个为 [Pi](https://pi.dev) 编程代理打造的终端界面扩展,将 pi-haiku、pi-claude-code-tui 与 pi-zentui 的优秀设计整合为统一、可配置的使用体验。
8
+
9
+ ![pi-open-tui 预览](https://raw.githubusercontent.com/OldSuns/pi-open-tui/main/assets/preview_dashboard_1.png)
10
+
11
+ ## 功能亮点
12
+
13
+ - **Pi 顶栏**:显示模型、思考等级、当前目录和常用斜杠命令提示
14
+ - **自适应底栏**:集中展示 Git 状态、运行环境、上下文用量、Token、费用和扩展状态
15
+ - **带边框的编辑器**:支持块状、竖线和下划线三种光标样式
16
+ - **项目环境感知**:识别 50 多种运行环境,并展示 ahead/behind、已暂存、已修改、未跟踪、stash 和 detached HEAD 等 Git 状态
17
+ - **单轮遥测**:展示 TPS、首 Token 延迟(TTFT)、耗时、停顿、Token 数量和模型标价速率
18
+ - **思考预览**:模型工作时,在 Pi 隐藏思考块的 `Thinking...` 位置显示实时字幕,展示推理内容的末尾片段
19
+ - **交互式设置**:通过 `/open-tui` 配置,并支持英文和简体中文界面
20
+
21
+ ## 环境要求
22
+
23
+ - Pi 1.2 或更高版本
24
+ - 支持 UTF-8 和彩色输出的终端
25
+ - 使用完整图标集时需要 [Nerd Font](https://www.nerdfonts.com/font-downloads)(可选;内置可移植 Unicode 图标)
26
+
27
+ ## 安装
28
+
29
+ 安装扩展:
30
+
31
+ ```bash
32
+ pi install npm:pi-open-tui
33
+ ```
34
+
35
+ 也可以只在当前会话中试用:
36
+
37
+ ```bash
38
+ pi -e npm:pi-open-tui
39
+ ```
40
+
41
+ ## 字体与图标
42
+
43
+ 可从 [Nerd Fonts 官方下载页](https://www.nerdfonts.com/font-downloads)或 [GitHub 最新版本](https://github.com/ryanoasis/nerd-fonts/releases/latest)下载任意已修补字体。安装后,请在终端配置中选择该字体,并重启终端。
44
+
45
+ 默认的 `auto` 模式检查终端环境,而不是已安装的字体文件。在支持 UTF-8 的交互式 TTY 中,它会使用 Nerd Font 图标,即使 Pi 运行在 runner 或子 shell 中。如果图标显示为方框、乱码或错误符号,请打开 `/open-tui`,在**外观**页选择合适的模式:
46
+
47
+ - `nerd`:终端已配置 Nerd Font 时,强制使用 Nerd Font 图标
48
+ - `unicode`:可移植 Unicode 图标(文件夹、分支、笔记本电脑、灯泡、插头、沙漏等),无需修补字体;emoji 字形通过终端的 emoji 回退渲染
49
+ - `ascii`:使用纯文本图标,无需安装修补字体
50
+ - `auto`:在支持 UTF-8 的交互式 TTY 中使用 Nerd Font 图标;SSH 会话使用可移植 Unicode 图标(字体由客户端终端决定,通常没有 Nerd Font);非交互式输出、`TERM=dumb` 或明确配置为非 UTF-8 的 locale 使用 ASCII
51
+
52
+ 如果已经安装字体,但 `auto` 仍选择 ASCII,请手动切换为 `nerd`。使用 VS Code、Windows Terminal 等应用时,只在操作系统中安装字体还不够,还需要在对应的终端配置中选中该字体。如果 SSH 客户端终端确实安装了 Nerd Font 并希望在 SSH 下使用完整图标,请将模式显式设为 `nerd`。
53
+
54
+ 如果较宽的 Nerd Font 图标占用了图标与文字之间的间隔(例如在 Kitty 中),可开启 `footerSegments.nerdIconSpacing`(`/open-tui` 中的 **底栏 → Nerd 图标后额外留空**)。开启后底栏会在 Nerd Font 图标后额外预留一格空白,包括运行环境图标,常规底栏和内嵌底栏的宽度计算都包含这格留白;Unicode/ASCII 图标与紧凑的 Git 状态标记不受影响。Kitty 用户也可以在 `kitty.conf` 中设置 `narrow_symbols U+E000-U+F8FF 1`。
55
+
56
+ ## 配置
57
+
58
+ 运行 `/open-tui` 打开设置窗口,其中包含**常规**、**外观**、**底栏**和**遥测**四个页面。设置保存在 `~/.pi/agent/open-tui.json`:
59
+
60
+ ```json
61
+ {
62
+ "enabled": true,
63
+ "header": true,
64
+ "inlineFooter": false,
65
+ "settingsLanguage": "zh",
66
+ "cursorStyle": "block",
67
+ "icons": {
68
+ "mode": "auto"
69
+ },
70
+ "footerSegments": {
71
+ "cwd": true,
72
+ "hostname": false,
73
+ "sessionName": false,
74
+ "gitBranch": true,
75
+ "gitStatus": true,
76
+ "gitCommit": false,
77
+ "runtime": true,
78
+ "context": true,
79
+ "tokens": true,
80
+ "cost": true,
81
+ "liveTps": true,
82
+ "extensionStatuses": true,
83
+ "capitalizeProviderName": true,
84
+ "nerdIconSpacing": false
85
+ },
86
+ "telemetry": {
87
+ "enabled": true,
88
+ "tps": true,
89
+ "ttft": true,
90
+ "duration": true,
91
+ "tokens": true,
92
+ "stalls": true,
93
+ "cost": true
94
+ },
95
+ "thinkingPeek": {
96
+ "lines": 1
97
+ },
98
+ "toolView": {
99
+ "mode": "native"
100
+ }
101
+ }
102
+ ```
103
+
104
+ 主要选项:
105
+
106
+ | 选项 | 可选值 | 说明 |
107
+ | --- | --- | --- |
108
+ | `settingsLanguage` | `en`、`zh` | 切换 `/open-tui` 设置界面的语言 |
109
+ | `header` | `true`、`false` | 决定是否用 open-tui 顶部信息栏覆盖 Pi 的 Header;设为 `false` 时把顶部栏位置留给 Pi 或其他扩展 |
110
+ | `inlineFooter` | `true`、`false` | 将两条主要 Footer 信息行移入编辑器上下边框以节省垂直空间,默认关闭;扩展状态行仍显示在编辑器外 |
111
+ | `cursorStyle` | `block`、`bar`、`underline` | `bar` 和 `underline` 需要终端支持光标形状转义序列 |
112
+ | `icons.mode` | `auto`、`nerd`、`unicode`、`ascii` | 控制底栏和遥测通知使用的图标 |
113
+ | `footerSegments` | 布尔开关 | 分别控制底栏中的各项数据 |
114
+ | `footerSegments.capitalizeProviderName` | 布尔开关 | 将底栏中提供商名称的首字母大写;设为 `false` 时保留原始大小写 |
115
+ | `footerSegments.nerdIconSpacing` | 布尔开关 | 在底栏 Nerd Font 图标后额外留一格空白;默认 `false` |
116
+ | `telemetry` | 布尔开关 | 控制遥测总开关和各项指标 |
117
+ | `thinkingPeek.lines` | `0`、`1`、`2` | 关闭、单行或双行思考预览 |
118
+ | `toolView.mode` | `native`、`compact` | 内置工具调用显示为 Pi 原生卡片,或紧凑的两行摘要;默认 `native`。只对切换后新建的行生效 |
119
+
120
+ `sessionName` 仅在会话有名称时显示;`hostname` 会显示主机名的短名称(主机名的第一个标签,例如从 `mba.example.com` 显示为 `mba`),并使用服务器图标;处于分离 HEAD(detached HEAD)状态时,`gitBranch` 会显示 `HEAD` 和短提交哈希,`gitCommit` 会在其后追加指向该提交的标签,且仅在开启 `gitBranch` 时生效;关闭 `extensionStatuses` 会隐藏整行扩展状态,其中也包括 MCP 状态。每条状态会保留其扩展通过 `ctx.ui.theme.fg()` 设置的颜色;未设置颜色的状态使用 muted 主题色显示。
121
+
122
+ 开启 `inlineFooter` 后,两条常规 Footer 信息行会移入编辑器边框,从而节省垂直空间。顶部边框左侧显示 Git 分支,右侧信息组以当前目录开头;开启 `sessionName` 时,会话标题也会显示在左侧。Header 和扩展状态行保持独立显示;终端较窄时优先截断低优先级 Footer 数据,保留右侧统计信息和边框角。
123
+
124
+ ## 单轮遥测
125
+
126
+ 每次 Agent 完整运行结束后,pi-open-tui 会显示一条临时结果,并将其中的多个工具调用轮次合并统计:
127
+
128
+ ```text
129
+ > TPS 42.5 tok/s | ~ TTFT 1.2s | + 29.7s | ↑ 567 | ↓ 1.2k | ! stall 1x / 4.3s | $ $3.60/M
130
+ ```
131
+
132
+ TPS 的计算方式是:将本次运行中服务商报告的全部 Assistant 输出 Token,除以各个生成轮次的总耗时。计时范围从 `turn_start` 到 Assistant 的 `message_end`,包含 TTFT、隐藏推理、缓冲和停顿,但不包含轮次之间的工具执行时间。没有输出 Token 或无法测得生成时间时,会显示 `TPS —`。
133
+
134
+ 回复流式输出时,工作计时器还会显示当前的估算速度,例如 `working 12.3s · ~54 tok/s`。`~` 表示估算值:它是最近 2 秒内流式输出的正文、思考内容和工具调用参数的速率,按拉丁文本和代码约 4 个字符 1 个 Token、中日韩字符每字 1 个 Token 估算。停顿时数值会逐渐降向零;工具执行期间和消息之间以暗色保留上一个值。由**底栏 → 实时 TPS**(`footerSegments.liveTps`)控制,与遥测通知的开关互不影响。运行结束后不再显示,以上面的精确 TPS 为准。
135
+
136
+ `$ / M` 表示根据 `usage.cost.total` 得到的模型标价速率,不是底栏中的会话累计费用。所有遥测字段都可以在**遥测**页单独开关。
137
+
138
+ ## 思考预览
139
+
140
+ 开启 Pi 的 **Hide thinking** 后,任务运行期间 pi-open-tui 会在原生隐藏思考块的 `Thinking...` 位置显示紧凑的实时字幕。`/open-tui` 中提供**关闭、单行、双行**三种模式:
141
+
142
+ - 模型推理时,思考内容的末尾片段会随 spinner 滚动(`~ think ⠋ …`);
143
+ - 开始输出正文时定格为对勾(`~ think ✓`);
144
+ - 双行模式中,上一条和最新一条思考内容使用相同的文字缩进;如果最新一行超出宽度,则两行随新 Token 持续显示它的最新末尾片段,不再保留上一条;
145
+ - 任务结束后恢复原生 `Thinking...` 标签。
146
+
147
+ ```text
148
+ ~ think ⠋ 上一条思考
149
+ 最新一条思考
150
+ ```
151
+
152
+ 该字幕仅在模型真正输出推理内容后出现,非推理模型不会显示。可见性由 Pi 自身的 Hide thinking 开关控制,切换后立即生效。每一行都按*显示宽度*截断(全角字符计 2 列),因此包含中文的思考文本也不会溢出终端。可在 `/open-tui` 的**常规 → 思考预览**中切换,或直接修改 `open-tui.json` 中的 `thinkingPeek.lines`。
153
+
154
+ ## 工具视图
155
+
156
+ 将**常规 → 工具视图**设为**紧凑**(`toolView.mode: "compact"`)后,Pi 内置工具(`read`、`bash`、`powershell`、`edit`、`write`、`grep`、`find`、`ls`)显示为一行调用加一行摘要:
157
+
158
+ ```text
159
+ ● Read(extensions/open-tui/footer.ts:1-120)
160
+ └ 120 lines
161
+ ● Edit(extensions/open-tui/footer.ts)
162
+ └ +2 −1
163
+ ● Bash(npm test)
164
+ └ Done · 131 lines · 6.4s
165
+ ```
166
+
167
+ - Shell 命令运行中会显示最新 3 行输出和已用时间。
168
+ - 圆点运行中为黄色、完成为绿色、失败为红色;失败的调用同样保持紧凑行,并显示红色摘要;Shell 命令的摘要末尾带上最后一行输出(如 `Exited with code 1 · 12 lines · 1.2s · FAIL tests/a.test.ts`),按 Ctrl+O 查看完整输出。
169
+ - 点击某行只展开这一行为 Pi 原生卡片,`Ctrl+O` 展开全部行;完整输出、diff 和截断提示保持不变。
170
+ - 只改变显示:工具、参数、结果以及模型看到的内容都不受影响,第三方和 MCP 工具保持各自的渲染。
171
+ - 模式在 Pi 创建某行时确定,因此切换只影响新建的行;执行 `/reload` 或新开会话后整个记录都会应用。
172
+
173
+ ## 本地开发
174
+
175
+ ```bash
176
+ npm install
177
+ npm test
178
+ npm run typecheck
179
+ pi -e .
180
+ ```
181
+
182
+ ## 致谢
183
+
184
+ 本项目基于多个 Pi 社区包的工作:
185
+
186
+ - **[pi-haiku](https://github.com/nnocte/pi-haiku)** — 双行底栏结构和工作计时器
187
+ - **[pi-claude-code-tui](https://github.com/Phoobobo/pi-claude-code-tui)** — Pi Logo 帧与圆角编辑器边框技术
188
+ - **[pi-zentui](https://github.com/lmilojevicc/pi-zentui)** — Starship 风格底栏、运行环境检测、会话生命周期和设置界面模式
189
+ - **[pi-tps](https://github.com/monotykamary/pi-tps)** — 单轮计时、停顿检测和保守的 TPS 计算方式
190
+
191
+ Logo 帧源自 Pi 官方安装脚本(`pi.dev/install.sh`)。运行环境检测和 Git porcelain 解析借鉴了 `pi-zentui` 的结构。
192
+
193
+ 特别感谢 **[LINUX DO](https://linux.do)** 社区的支持。
194
+
195
+ ## 许可证
196
+
197
+ [MIT](./LICENSE)
@@ -6,6 +6,7 @@ import type { IconMode } from "./icons.ts";
6
6
  export type SettingsLanguage = "en" | "zh";
7
7
  export type CursorStyle = "block" | "bar" | "underline";
8
8
  export type ThinkingPeekLines = 0 | 1 | 2;
9
+ export type ToolViewMode = "native" | "compact";
9
10
 
10
11
  export type { IconMode } from "./icons.ts";
11
12
 
@@ -42,6 +43,8 @@ export interface ThinkingPeekConfig {
42
43
 
43
44
  export interface OpenTuiConfig {
44
45
  enabled: boolean;
46
+ /** Override Pi's header with the open-tui header. When false, the header slot is left to Pi or another extension. */
47
+ header: boolean;
45
48
  inlineFooter: boolean;
46
49
  settingsLanguage: SettingsLanguage;
47
50
  cursorStyle: CursorStyle;
@@ -51,10 +54,14 @@ export interface OpenTuiConfig {
51
54
  footerSegments: FooterSegments;
52
55
  telemetry: TelemetryConfig;
53
56
  thinkingPeek: ThinkingPeekConfig;
57
+ toolView: {
58
+ mode: ToolViewMode;
59
+ };
54
60
  }
55
61
 
56
62
  export const DEFAULT_CONFIG: OpenTuiConfig = {
57
63
  enabled: true,
64
+ header: true,
58
65
  inlineFooter: false,
59
66
  settingsLanguage: "en",
60
67
  cursorStyle: "block",
@@ -89,6 +96,9 @@ export const DEFAULT_CONFIG: OpenTuiConfig = {
89
96
  thinkingPeek: {
90
97
  lines: 1,
91
98
  },
99
+ toolView: {
100
+ mode: "native",
101
+ },
92
102
  };
93
103
 
94
104
  export function getConfigPath(): string {
@@ -160,6 +170,9 @@ export function loadConfig(notify?: (msg: string, level: "warning" | "info") =>
160
170
  if (typeof config.enabled !== "boolean") {
161
171
  config.enabled = DEFAULT_CONFIG.enabled;
162
172
  }
173
+ if (typeof config.header !== "boolean") {
174
+ config.header = DEFAULT_CONFIG.header;
175
+ }
163
176
  if (typeof config.inlineFooter !== "boolean") {
164
177
  config.inlineFooter = DEFAULT_CONFIG.inlineFooter;
165
178
  }
@@ -179,6 +192,10 @@ export function loadConfig(notify?: (msg: string, level: "warning" | "info") =>
179
192
  } else {
180
193
  config.thinkingPeek.lines = normalizeThinkingPeekLines(config.thinkingPeek.lines);
181
194
  }
195
+ if (typeof config.toolView !== "object" || config.toolView === null
196
+ || (config.toolView.mode !== "native" && config.toolView.mode !== "compact")) {
197
+ config.toolView = structuredClone(DEFAULT_CONFIG.toolView);
198
+ }
182
199
  return config;
183
200
  } catch (err) {
184
201
  notify?.(`open-tui config parse error: ${err instanceof Error ? err.message : String(err)}`, "warning");
@@ -81,8 +81,11 @@ function hiddenThinkingTextWidth(width: number, outputPad: unknown): number {
81
81
  return Math.max(1, width - paddingX * 2);
82
82
  }
83
83
 
84
+ // pi-tui's renderFakeCursor() markers (FAKE_CURSOR_START/END, not exported). Truncation can drop the end marker.
85
+ const FAKE_CURSOR = /\x1b_pi:fc\x07([\s\S]*?)(?:\x1b_pi:\/fc\x07|$)/g;
86
+
84
87
  function removeSoftwareCursor(line: string, cursorMarker = ""): string {
85
- return line.replace(/\x1b\[7m([\s\S]*?)\x1b\[0m/g, (_match, cursor: string) => {
88
+ return line.replace(FAKE_CURSOR, (_match, cursor: string) => {
86
89
  const replacement = `${cursorMarker}${cursor}`;
87
90
  cursorMarker = "";
88
91
  return replacement;
@@ -383,7 +386,9 @@ export function installEditor(
383
386
  const originalRender = target.render;
384
387
  let lastWidth: number | undefined;
385
388
  let lastBuild: ((width: number) => string) | undefined;
386
- target.render = function (this: HiddenThinkingLabelComponent, width: number): string[] {
389
+ let active = true;
390
+ const patched = function (this: HiddenThinkingLabelComponent, width: number): string[] {
391
+ if (!active) return originalRender.call(this, width);
387
392
  const build = buildHiddenThinkingLabel;
388
393
  const textWidth = hiddenThinkingTextWidth(width, this.outputPad);
389
394
  if (build && (build !== lastBuild || textWidth !== lastWidth)) {
@@ -393,8 +398,11 @@ export function installEditor(
393
398
  }
394
399
  return originalRender.call(this, width);
395
400
  };
401
+ target.render = patched;
396
402
  restoreHiddenThinkingRender = () => {
397
- target.render = originalRender;
403
+ active = false;
404
+ // Another copy of this extension may have wrapped us; unwinding then would drop its wrapper.
405
+ if (target.render === patched) target.render = originalRender;
398
406
  };
399
407
  };
400
408
  // Only the fullscreen renderer clears and rewrites the whole screen on a forced render.
@@ -28,6 +28,9 @@ export interface IconGlyphs {
28
28
  untracked: string;
29
29
  renamed: string;
30
30
  deleted: string;
31
+ /** Compact tool view: call marker and result gutter. */
32
+ toolCall: string;
33
+ toolResult: string;
31
34
  }
32
35
 
33
36
  const NERD_GLYPHS: IconGlyphs = {
@@ -59,6 +62,8 @@ const NERD_GLYPHS: IconGlyphs = {
59
62
  untracked: "?",
60
63
  renamed: "»",
61
64
  deleted: "✘",
65
+ toolCall: "●",
66
+ toolResult: "⎿",
62
67
  };
63
68
 
64
69
  // ponytail: ASCII fallback uses compact symbols (not English words) to keep
@@ -92,6 +97,8 @@ const ASCII_GLYPHS: IconGlyphs = {
92
97
  untracked: "?",
93
98
  renamed: "r",
94
99
  deleted: "x",
100
+ toolCall: "*",
101
+ toolResult: "`-",
95
102
  };
96
103
 
97
104
  // Portable Unicode icon set for terminals without a Nerd Font. Keeps the
@@ -129,6 +136,8 @@ const UNICODE_GLYPHS: IconGlyphs = {
129
136
  untracked: "?",
130
137
  renamed: "→",
131
138
  deleted: "✗",
139
+ toolCall: "●",
140
+ toolResult: "└",
132
141
  };
133
142
 
134
143
  function isSshSession(): boolean {
@@ -8,6 +8,7 @@ import { readRuntimeInfo } from "./runtime.ts";
8
8
  import { SessionLifecycle } from "./session-lifecycle.ts";
9
9
  import { registerSettingsCommand } from "./settings-command.ts";
10
10
  import { formatTurnTelemetry, TurnTelemetryTracker } from "./telemetry.ts";
11
+ import { registerToolView } from "./tool-view.ts";
11
12
  import {
12
13
  createInitialState,
13
14
  getModelMeta,
@@ -45,6 +46,7 @@ export default function (pi: ExtensionAPI) {
45
46
  const turnTelemetry = new TurnTelemetryTracker();
46
47
 
47
48
  let config: OpenTuiConfig = structuredClone(DEFAULT_CONFIG);
49
+ let configLoaded = false;
48
50
  let active = false;
49
51
  let lastCtx: ExtensionContext | undefined;
50
52
  let requestFooterRender: (() => void) | undefined;
@@ -114,7 +116,8 @@ export default function (pi: ExtensionAPI) {
114
116
  return;
115
117
  }
116
118
  if (!active) {
117
- cleanupHeader = installHeader(pi, ctx);
119
+ // Leave the header slot alone when disabled so another extension's header survives.
120
+ if (config.header) cleanupHeader = installHeader(pi, ctx);
118
121
  const footer = installFooter(
119
122
  ctx,
120
123
  () => state,
@@ -228,6 +231,7 @@ export default function (pi: ExtensionAPI) {
228
231
 
229
232
  ensureConfigExists();
230
233
  config = loadConfig((msg, level) => ctx.ui.notify(msg, level));
234
+ configLoaded = true;
231
235
  clearPeekLabel(ctx);
232
236
 
233
237
  applyUi(ctx);
@@ -366,10 +370,26 @@ export default function (pi: ExtensionAPI) {
366
370
  refreshInteractiveState(ctx);
367
371
  });
368
372
 
373
+ if (typeof pi.registerToolRenderer === "function") {
374
+ registerToolView(
375
+ pi,
376
+ () => {
377
+ // Resumed sessions rebuild their rows before session_start fires, so read the file once here.
378
+ if (!configLoaded) {
379
+ config = loadConfig();
380
+ configLoaded = true;
381
+ }
382
+ return config.enabled ? config.toolView.mode : "native";
383
+ },
384
+ () => resolveGlyphs(config.icons.mode),
385
+ );
386
+ }
387
+
369
388
  registerSettingsCommand(pi, {
370
389
  getConfig: () => config,
371
390
  onConfigChanged: (newConfig) => {
372
391
  const cursorStyleChanged = config.cursorStyle !== newConfig.cursorStyle;
392
+ const headerChanged = config.header !== newConfig.header;
373
393
  const thinkingPeekLinesChanged = config.thinkingPeek.lines !== newConfig.thinkingPeek.lines;
374
394
  saveConfig(newConfig);
375
395
  config = newConfig;
@@ -382,6 +402,10 @@ export default function (pi: ExtensionAPI) {
382
402
  if (cursorStyleChanged && active && editor) {
383
403
  editor.setCursorStyle(newConfig.cursorStyle);
384
404
  }
405
+ if (headerChanged && active && lastCtx) {
406
+ cleanupHeader?.();
407
+ cleanupHeader = newConfig.header ? installHeader(pi, lastCtx) : undefined;
408
+ }
385
409
  if (lastCtx) {
386
410
  pendingUiChange = getPendingUiChange(newConfig.enabled, active);
387
411
  }
@@ -28,8 +28,10 @@ const COPY = {
28
28
  hint: "Tab/Shift+Tab/←/→: tabs · ↑/↓: move · Enter/Space: change · Esc/q: close",
29
29
  labels: {
30
30
  enabled: "Enabled",
31
+ header: "Override header",
31
32
  inlineFooter: "Footer in editor border",
32
33
  thinkingPeek: "Thinking peek",
34
+ toolView: "Tool view",
33
35
  language: "Language",
34
36
  cursorStyle: "Cursor style",
35
37
  iconMode: "Icon mode",
@@ -56,6 +58,7 @@ const COPY = {
56
58
  on: "On",
57
59
  off: "Off",
58
60
  thinkingPeek: { off: "Off", one: "1 line", two: "2 lines" },
61
+ toolView: { native: "Native", compact: "Compact" },
59
62
  languages: { en: "English", zh: "简体中文" },
60
63
  cursorStyles: { block: "Block", bar: "Bar", underline: "Underline" },
61
64
  icons: { auto: "Auto", nerd: "Nerd", unicode: "Unicode", ascii: "ASCII" },
@@ -67,8 +70,10 @@ const COPY = {
67
70
  hint: "Tab/Shift+Tab/←/→:切页 · ↑/↓:移动 · Enter/Space:更改 · Esc/q:关闭",
68
71
  labels: {
69
72
  enabled: "启用",
73
+ header: "覆盖顶部信息栏",
70
74
  inlineFooter: "底栏嵌入输入框",
71
75
  thinkingPeek: "思考预览",
76
+ toolView: "工具视图",
72
77
  language: "语言",
73
78
  cursorStyle: "光标样式",
74
79
  iconMode: "图标模式",
@@ -95,6 +100,7 @@ const COPY = {
95
100
  on: "开启",
96
101
  off: "关闭",
97
102
  thinkingPeek: { off: "关闭", one: "单行", two: "双行" },
103
+ toolView: { native: "原生", compact: "紧凑" },
98
104
  languages: { en: "English", zh: "简体中文" },
99
105
  cursorStyles: { block: "块", bar: "竖线", underline: "下划线" },
100
106
  icons: { auto: "自动", nerd: "Nerd", unicode: "Unicode", ascii: "ASCII" },
@@ -160,9 +166,11 @@ function buildFeaturesItems(config: OpenTuiConfig, copy: SettingsCopy): SettingI
160
166
  const flag = (value: boolean) => value ? copy.values.on : copy.values.off;
161
167
  return [
162
168
  { id: "enabled", label: copy.labels.enabled, currentValue: flag(config.enabled) },
169
+ { id: "header", label: copy.labels.header, currentValue: flag(config.header) },
163
170
  { id: "settingsLanguage", label: copy.labels.language, currentValue: copy.values.languages[config.settingsLanguage] },
164
171
  { id: "thinkingPeek", label: copy.labels.thinkingPeek, currentValue: formatThinkingPeekLines(config.thinkingPeek.lines, copy) },
165
172
  { id: "inlineFooter", label: copy.labels.inlineFooter, currentValue: flag(config.inlineFooter) },
173
+ { id: "toolView", label: copy.labels.toolView, currentValue: copy.values.toolView[config.toolView.mode] },
166
174
  ];
167
175
  }
168
176
 
@@ -225,9 +233,13 @@ function handleSettingChange(
225
233
  ): OpenTuiConfig {
226
234
  if (tab === "features") {
227
235
  if (itemId === "enabled") return toggleEnabled(config);
236
+ if (itemId === "header") return { ...config, header: !config.header };
228
237
  if (itemId === "inlineFooter") return { ...config, inlineFooter: !config.inlineFooter };
229
238
  if (itemId === "settingsLanguage") return toggleLanguage(config);
230
239
  if (itemId === "thinkingPeek") return cycleThinkingPeek(config);
240
+ if (itemId === "toolView") {
241
+ return { ...config, toolView: { mode: config.toolView.mode === "native" ? "compact" : "native" } };
242
+ }
231
243
  }
232
244
  if (tab === "icons") {
233
245
  if (itemId === "mode") return cycleIconMode(config);