pi-one-ui 0.2.0 → 0.2.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/README.md
CHANGED
|
@@ -1,57 +1,105 @@
|
|
|
1
1
|
# pi-one-ui
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/pi-one-ui)
|
|
4
|
+
[](https://nodejs.org/)
|
|
5
|
+
[](./LICENSE)
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
`pi-one-ui` 是一个面向 [Pi](https://pi.dev) 的统一 TUI 扩展包。项目的初衷是希望打造一个简单美观的 TUI 界面,最初通过源码级融合,将:
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
pi
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
```
|
|
9
|
+
- [pi-zentui](https://github.com/lmilojevicc/pi-zentui)`pi-zentui` 的终端外壳能力
|
|
10
|
+
- [pi-cc-extensions](https://github.com/minuque/pi-cc-extensions) 的对话内容渲染及生产力功能
|
|
11
|
+
|
|
12
|
+
整合到同一个可安装、可配置的 Pi package 中,并在此基础上持续进行模块重构、职责收敛和独立优化。
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
## 特性
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
### 统一的界面布局
|
|
17
|
+
|
|
18
|
+
`pi-one-ui` 将 Pi 的交互界面划分为以下布局:
|
|
16
19
|
|
|
17
20
|
```text
|
|
18
|
-
|
|
21
|
+
Header → Context → WorkingLine → Editor → Footer
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- **Header**:启动信息、Logo 和快捷键提示。
|
|
25
|
+
- **Context**:对话内容区,包含用户消息、Assistant 消息、Thinking、Tool、Diff、Markdown 和 Summary。
|
|
26
|
+
- **WorkingLine**:工作状态、spinner、token/thought/elapsed 信息和回合摘要。
|
|
27
|
+
- **Editor**:输入编辑器、completion、metadata、Accent Rail 和 Minimalist 样式。
|
|
28
|
+
- **Footer**:目录、Git、runtime、token、cost 和扩展状态等信息。
|
|
29
|
+
- **Overlay**:设置面板、Context Inspector 等临时浮层由统一的 OverlayManager 管理。
|
|
30
|
+
|
|
31
|
+
### 内置功能
|
|
32
|
+
|
|
33
|
+
| 功能 | 说明 | 入口 |
|
|
34
|
+
| --------------------- | ---------------------------------------------------------------------- | ----------------- |
|
|
35
|
+
| 统一设置面板 | 按 Header、Context、WorkingLine、Editor、Footer 和 Features 组织设置 | `/oneui` |
|
|
36
|
+
| Context Inspector | 查看上下文占用,并预览 System prompt、Memory、Skills、Tools 和消息内容 | `/context` |
|
|
37
|
+
| Session reference | 搜索并注入历史 Pi session 或 SubAgent 的有效上下文 | `@` 补全 |
|
|
38
|
+
| Subagent autocomplete | 提供 SubAgent 名称补全和委派提示 | `@` 补全 |
|
|
39
|
+
| Tool / Diff renderer | 工具调用、结果、折叠内容和 Edit/Write diff 的统一展示 | 自动生效 |
|
|
40
|
+
| Markdown enhancement | 支持 Mermaid、提示框和 URL 链接化等增强渲染 | 自动生效 |
|
|
41
|
+
| Built-in themes | 提供 CC Dark 和 CC Light 主题 | `/theme` |
|
|
42
|
+
| Compatibility aliases | 可选提供常用命令别名 | `/clear`、`/exit` |
|
|
43
|
+
|
|
44
|
+
## 快速开始
|
|
45
|
+
|
|
46
|
+
### 环境要求
|
|
47
|
+
|
|
48
|
+
- Node.js `>=22.19.0`
|
|
49
|
+
- Pi 及其相关运行时包 `>=0.84.0`
|
|
50
|
+
|
|
51
|
+
### 从 npm 安装
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pi install npm:pi-one-ui
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 从 GitHub 安装
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pi install git:github.com/kerolt/pi-one-ui
|
|
19
61
|
```
|
|
20
62
|
|
|
21
|
-
|
|
63
|
+
安装完成后,在 Pi 中重新加载扩展:
|
|
22
64
|
|
|
23
65
|
```text
|
|
24
|
-
|
|
66
|
+
/reload
|
|
25
67
|
```
|
|
26
68
|
|
|
27
|
-
|
|
69
|
+
然后使用统一入口打开设置:
|
|
28
70
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
- **Editor**:输入编辑器、completion、metadata、Accent Rail 和 Minimalist 样式。
|
|
33
|
-
- **Footer**:目录、Git、runtime、token、cost、extension status 等信息。
|
|
34
|
-
- **Features**:Context Inspector、Session Reference、Subagent Autocomplete、Aliases 等行为能力。
|
|
35
|
-
- **Overlay**:设置面板和 Context Inspector 等临时浮层由统一 OverlayManager 跟踪。
|
|
71
|
+
```text
|
|
72
|
+
/oneui
|
|
73
|
+
```
|
|
36
74
|
|
|
37
75
|
## 配置
|
|
38
76
|
|
|
39
|
-
|
|
77
|
+
配置文件位于:
|
|
40
78
|
|
|
41
79
|
```text
|
|
42
80
|
~/.pi/agent/pi-one-ui.json
|
|
43
81
|
```
|
|
44
82
|
|
|
45
|
-
|
|
83
|
+
推荐通过 `/oneui` 设置面板修改配置。当前配置仍使用 v1 结构,例如:
|
|
46
84
|
|
|
47
85
|
```json
|
|
48
86
|
{
|
|
49
87
|
"version": 1,
|
|
50
88
|
"components": {
|
|
51
|
-
"editor": {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
89
|
+
"editor": {
|
|
90
|
+
"enabled": true,
|
|
91
|
+
"style": "opencode"
|
|
92
|
+
},
|
|
93
|
+
"userMessages": {
|
|
94
|
+
"enabled": true,
|
|
95
|
+
"style": "framed"
|
|
96
|
+
},
|
|
97
|
+
"workingLine": {
|
|
98
|
+
"enabled": true
|
|
99
|
+
},
|
|
100
|
+
"footer": {
|
|
101
|
+
"style": "starship"
|
|
102
|
+
}
|
|
55
103
|
},
|
|
56
104
|
"renderer": {
|
|
57
105
|
"mode": "on",
|
|
@@ -61,73 +109,147 @@ Header → Context → WorkingLine → Editor → Footer → Features → Preset
|
|
|
61
109
|
}
|
|
62
110
|
```
|
|
63
111
|
|
|
64
|
-
|
|
112
|
+
不同布局和渲染器的可用选项会随版本变化,建议优先使用 `/oneui` 面板进行配置。
|
|
113
|
+
|
|
114
|
+
### 配置迁移与兼容
|
|
65
115
|
|
|
66
|
-
|
|
116
|
+
`pi-one-ui` 通过统一的 `ConfigStore` 读取和写入配置,并兼容读取以下历史配置文件:
|
|
67
117
|
|
|
68
118
|
```text
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
runtime/ # TuiRuntime、state、事件和调度
|
|
73
|
-
host/ # Pi API 的窄化 host ports
|
|
74
|
-
config/ # ConfigStore 与配置领域解析
|
|
75
|
-
ownership/ # Layout ownership
|
|
76
|
-
overlay/ # OverlayManager、InputRouter、selector
|
|
77
|
-
commands/ # /oneui and settings panel previews
|
|
78
|
-
layouts/
|
|
79
|
-
header/ # Header layout
|
|
80
|
-
context/ # 原 Transcript:对话内容区
|
|
81
|
-
message/ # User Message
|
|
82
|
-
thinking/ # Thinking
|
|
83
|
-
renderer/ # Tool、Diff、Markdown、Mouse
|
|
84
|
-
summary/ # Agent summary
|
|
85
|
-
working-line/ # WorkingLine 与 interaction summary
|
|
86
|
-
editor/ # Editor 及其样式实现
|
|
87
|
-
footer/ # Footer 及其布局/format/status
|
|
88
|
-
features/
|
|
89
|
-
aliases.ts
|
|
90
|
-
context-inspector/ # /context,不是 Context 对话区
|
|
91
|
-
flush-docked-bash.ts
|
|
92
|
-
legacy/ # standalone compatibility implementation
|
|
93
|
-
session-reference/
|
|
94
|
-
subagent-autocomplete.ts
|
|
95
|
-
services/ # Git、runtime、package、project、session 数据
|
|
96
|
-
shared/ # ANSI、format、style、icons 等共享实现
|
|
97
|
-
tools/ # 底层组件树、patch 和 terminal helpers
|
|
98
|
-
vendor/ # 只读上游参考快照
|
|
119
|
+
~/.pi/agent/zentui.json
|
|
120
|
+
~/.pi/agent/claude-code-style.json
|
|
121
|
+
~/.pi/agent/pi-mine-ui.json
|
|
99
122
|
```
|
|
100
123
|
|
|
101
|
-
`
|
|
124
|
+
历史配置只作为兼容输入;新的配置写入统一使用 `pi-one-ui.json`。`enableWorkingMessage` 仍可以从旧配置中读取,但统一运行时不会注册第二套 Working Message 实现,WorkingLine 是该 UI seam 的唯一 owner。
|
|
125
|
+
|
|
126
|
+
## 上游来源与项目演进
|
|
127
|
+
|
|
128
|
+
`pi-one-ui` 以以下两个开源项目的源码为初始基础,并对它们进行源码级融合。在此再次感谢两个上游项目及其贡献者,他们为 `pi-one-ui` 提供了最初的实现基础。
|
|
102
129
|
|
|
103
|
-
|
|
130
|
+
| 上游项目 | 融入 `pi-one-ui` 的主要能力 | 参照baseline |
|
|
131
|
+
| --------------------------------------------------------------- | -------------------------------------------------------------------------- | ------------------------- |
|
|
132
|
+
| [pi-zentui](https://github.com/lmilojevicc/pi-zentui) | Starship 风格 Footer、Opencode 风格 Editor、布局和 shell 能力 | v0.21.0,commit `5341b38` |
|
|
133
|
+
| [pi-cc-extensions](https://github.com/minuque/pi-cc-extensions) | Claude Code 风格 Context renderer、Tool/Diff、Context Inspector 和引用功能 | v0.8.67,commit `dba37e5` |
|
|
134
|
+
|
|
135
|
+
融合后的生产代码位于 `extensions/`。项目已经在原始实现之上统一入口、配置存储、生命周期、Layout ownership、Overlay 和输入路由,并会继续独立演进;当前实现不再等同于任一上游项目,也不会自动跟随上游同步。
|
|
136
|
+
|
|
137
|
+
感谢两个上游项目及其贡献者。它们为 `pi-one-ui` 提供了最初的实现基础,而本项目后续的工作重点是将这些能力收敛为一个边界清晰、可持续维护的统一产品。
|
|
138
|
+
|
|
139
|
+
更详细的模块边界、事件流和 ownership 约定参见 [ARCHITECTURE.md](./ARCHITECTURE.md)。
|
|
140
|
+
|
|
141
|
+
## 本地开发
|
|
142
|
+
|
|
143
|
+
### 获取源码
|
|
104
144
|
|
|
105
145
|
```bash
|
|
146
|
+
git clone https://github.com/kerolt/pi-one-ui.git
|
|
147
|
+
cd pi-one-ui
|
|
106
148
|
npm install
|
|
107
|
-
npm run typecheck
|
|
108
|
-
npm test
|
|
109
|
-
npm run pack:check
|
|
110
149
|
```
|
|
111
150
|
|
|
112
|
-
|
|
151
|
+
项目要求 Node.js `>=22.19.0`。如果本机安装了多个 Node.js 版本,请先切换到满足要求的版本。
|
|
152
|
+
|
|
153
|
+
### 开发模式运行
|
|
154
|
+
|
|
155
|
+
直接使用当前源码启动 Pi:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
npm run pi:dev
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
该命令等价于:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
pi --no-extensions -e ./extensions/index.ts
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
也可以将当前 package 以本地链接方式安装:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
npm run pi:install-local
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
修改代码后,在 Pi 中执行:
|
|
174
|
+
|
|
175
|
+
```text
|
|
176
|
+
/reload
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### 常用开发命令
|
|
180
|
+
|
|
181
|
+
| 命令 | 用途 |
|
|
182
|
+
| ---------------------- | ---------------------------------- |
|
|
183
|
+
| `npm install` | 安装依赖 |
|
|
184
|
+
| `npm run format` | 格式化源码、测试和配置文件 |
|
|
185
|
+
| `npm run format:check` | 检查格式但不修改文件 |
|
|
186
|
+
| `npm run lint` | 运行 Biome 检查 |
|
|
187
|
+
| `npm run typecheck` | 执行 TypeScript 类型检查 |
|
|
188
|
+
| `npm test` | 运行全部测试 |
|
|
189
|
+
| `npm run test:node` | 运行 Node.js test runner 测试 |
|
|
190
|
+
| `npm run test:vitest` | 运行 Vitest 测试 |
|
|
191
|
+
| `npm run pack:check` | 预览 npm 实际打包内容 |
|
|
192
|
+
| `npm run verify` | 执行格式、lint、类型检查和全部测试 |
|
|
193
|
+
|
|
194
|
+
提交修改前建议至少运行:
|
|
113
195
|
|
|
114
196
|
```bash
|
|
115
197
|
npm run verify
|
|
198
|
+
npm run pack:check
|
|
116
199
|
```
|
|
117
200
|
|
|
118
|
-
|
|
201
|
+
### 测试组织
|
|
202
|
+
|
|
203
|
+
测试按照功能领域划分:
|
|
119
204
|
|
|
120
|
-
- `tests/context-*`:Context 内容区、Tool、Diff、Thinking
|
|
205
|
+
- `tests/context-*`:Context 内容区、Tool、Diff、Thinking 和鼠标交互。
|
|
121
206
|
- `tests/working-line-*`:WorkingLine 和 turn summary。
|
|
122
207
|
- `tests/editor-*`:Editor、completion、metadata、transfer 和 Accent Rail。
|
|
123
208
|
- `tests/footer-*`:Footer、Footer format/layout/status。
|
|
124
209
|
- `tests/services-*`:Git、runtime、project、session 和 telemetry。
|
|
125
|
-
- `tests/shell
|
|
210
|
+
- `tests/shell-*`:剩余布局生命周期 glue 和 standalone compatibility。
|
|
211
|
+
|
|
212
|
+
涉及 TUI 生命周期的修改,应特别覆盖 reload、session tree rebuild、compact、regular/fullscreen TUI、headless mode、overlay 和第三方 patch ownership 等场景。
|
|
213
|
+
|
|
214
|
+
## 发布
|
|
215
|
+
|
|
216
|
+
项目使用 GitHub Actions 进行持续集成和 npm 发布:
|
|
126
217
|
|
|
127
|
-
|
|
218
|
+
- [`.github/workflows/ci.yml`](./.github/workflows/ci.yml):在 Pull Request 和 `main` 分支提交时执行完整验证。
|
|
219
|
+
- [`.github/workflows/publish.yml`](./.github/workflows/publish.yml):推送符合 `v*.*.*` 格式的 tag 时发布 npm 包。
|
|
220
|
+
|
|
221
|
+
发布新版本时:
|
|
128
222
|
|
|
129
223
|
```bash
|
|
130
|
-
|
|
224
|
+
# 确认位于 main,并同步远程代码
|
|
225
|
+
git switch main
|
|
226
|
+
git pull --ff-only origin main
|
|
227
|
+
|
|
228
|
+
# 发布前检查
|
|
229
|
+
npm ci
|
|
230
|
+
npm run verify
|
|
231
|
+
npm run pack:check
|
|
232
|
+
|
|
233
|
+
# 按 SemVer 升级版本
|
|
234
|
+
npm version patch # Bug 修复,例如 0.2.1 -> 0.2.2
|
|
235
|
+
# npm version minor # 新增兼容功能
|
|
236
|
+
# npm version major # 破坏性变更
|
|
237
|
+
|
|
238
|
+
# 推送版本 commit 和 tag
|
|
239
|
+
git push origin main --follow-tags
|
|
131
240
|
```
|
|
132
241
|
|
|
133
|
-
|
|
242
|
+
推送 tag 后,发布 workflow 会校验 tag 版本与 `package.json` 版本一致,重新执行验证,并通过 npm Trusted Publishing 发布带 provenance 的公开包。已发布的 npm 版本不可覆盖,因此不要重复使用已经发布过的版本号或 tag。
|
|
243
|
+
|
|
244
|
+
## 贡献
|
|
245
|
+
|
|
246
|
+
欢迎通过 GitHub Issues 报告问题或提出改进建议。提交代码时建议:
|
|
247
|
+
|
|
248
|
+
1. 保持每个 commit 只包含一个主要目的。
|
|
249
|
+
2. 使用简洁的 Conventional Commit message,例如 `fix: prevent settings panel freeze after editor toggle`。
|
|
250
|
+
3. 为行为修改补充或更新测试。
|
|
251
|
+
4. 提交前运行 `npm run verify` 和 `npm run pack:check`。
|
|
252
|
+
|
|
253
|
+
## 许可证
|
|
254
|
+
|
|
255
|
+
本项目基于 [MIT License](./LICENSE) 发布。
|
package/extensions/app/panel.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
type ExtensionCommandContext,
|
|
2
3
|
type ExtensionContext,
|
|
3
4
|
getSettingsListTheme,
|
|
4
5
|
} from "@earendil-works/pi-coding-agent";
|
|
@@ -84,6 +85,26 @@ function renderTabs(theme: any, active: number, width: number): string {
|
|
|
84
85
|
return truncateToWidth(text, Math.max(0, width));
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
/** Adds a stable chrome frame around the settings content. */
|
|
89
|
+
function renderPanelFrame(theme: any, width: number, rows: string[]): string[] {
|
|
90
|
+
const safeWidth = Math.max(1, width);
|
|
91
|
+
const border = (text: string) => theme.fg("border", text);
|
|
92
|
+
if (safeWidth === 1)
|
|
93
|
+
return [border("│"), ...rows.map(() => border("│")), border("│")];
|
|
94
|
+
|
|
95
|
+
const innerWidth = safeWidth - 2;
|
|
96
|
+
const content = rows.map(
|
|
97
|
+
(row) =>
|
|
98
|
+
`${border("│")}${truncateToWidth(row, innerWidth, "", true)}${border("│")}`,
|
|
99
|
+
);
|
|
100
|
+
if (safeWidth === 2) return [border("╭╮"), ...content, border("╰╯")];
|
|
101
|
+
return [
|
|
102
|
+
border(`╭${"─".repeat(innerWidth)}╮`),
|
|
103
|
+
...content,
|
|
104
|
+
border(`╰${"─".repeat(innerWidth)}╯`),
|
|
105
|
+
];
|
|
106
|
+
}
|
|
107
|
+
|
|
87
108
|
function sectionDescription(section: SectionId): string {
|
|
88
109
|
switch (section) {
|
|
89
110
|
case "header":
|
|
@@ -554,7 +575,7 @@ let unifiedPanelOpen = false;
|
|
|
554
575
|
* @param deps Runtime controllers used for live updates.
|
|
555
576
|
*/
|
|
556
577
|
export async function showOneUiPanel(
|
|
557
|
-
ctx:
|
|
578
|
+
ctx: ExtensionCommandContext,
|
|
558
579
|
deps: UnifiedPanelDeps = {},
|
|
559
580
|
): Promise<void> {
|
|
560
581
|
if (
|
|
@@ -572,9 +593,10 @@ export async function showOneUiPanel(
|
|
|
572
593
|
|
|
573
594
|
unifiedPanelOpen = true;
|
|
574
595
|
try {
|
|
596
|
+
let panelHandle: { focus: () => void } | undefined;
|
|
575
597
|
await overlayManager.run(() =>
|
|
576
598
|
ctx.ui.custom(
|
|
577
|
-
(tui
|
|
599
|
+
(tui, theme, _keybindings, done) => {
|
|
578
600
|
let activeIndex = 0;
|
|
579
601
|
let list: SettingsList;
|
|
580
602
|
const createList = () => {
|
|
@@ -583,11 +605,29 @@ export async function showOneUiPanel(
|
|
|
583
605
|
10,
|
|
584
606
|
getSettingsListTheme(),
|
|
585
607
|
(id, value) => {
|
|
608
|
+
// Replacing Pi's editor synchronously moves focus away from
|
|
609
|
+
// this overlay. Restore the overlay handle after the editor
|
|
610
|
+
// has been reconciled so the panel remains interactive.
|
|
611
|
+
if (id === "editorEnabled") {
|
|
612
|
+
try {
|
|
613
|
+
updateSetting(id, value, ctx, deps);
|
|
614
|
+
list.updateValue(id, value);
|
|
615
|
+
} catch (error) {
|
|
616
|
+
ctx.ui?.notify?.(
|
|
617
|
+
`Could not update Editor setting: ${error instanceof Error ? error.message : String(error)}`,
|
|
618
|
+
"error",
|
|
619
|
+
);
|
|
620
|
+
} finally {
|
|
621
|
+
panelHandle?.focus();
|
|
622
|
+
tui.requestRender();
|
|
623
|
+
}
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
586
626
|
updateSetting(id, value, ctx, deps);
|
|
587
627
|
list.updateValue(id, value);
|
|
588
628
|
tui.requestRender();
|
|
589
629
|
},
|
|
590
|
-
done,
|
|
630
|
+
() => done(undefined),
|
|
591
631
|
);
|
|
592
632
|
};
|
|
593
633
|
createList();
|
|
@@ -600,29 +640,32 @@ export async function showOneUiPanel(
|
|
|
600
640
|
};
|
|
601
641
|
|
|
602
642
|
return {
|
|
603
|
-
render: (width: number) =>
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
"
|
|
608
|
-
` ${sectionDescription(SECTIONS[activeIndex].id)}`,
|
|
609
|
-
),
|
|
610
|
-
"",
|
|
611
|
-
...list.render(width),
|
|
612
|
-
...(SECTIONS[activeIndex].id === "editor"
|
|
613
|
-
? renderEditorPreview(theme, width)
|
|
614
|
-
: SECTIONS[activeIndex].id === "context"
|
|
615
|
-
? renderContextPreview(theme, width)
|
|
616
|
-
: []),
|
|
617
|
-
"",
|
|
618
|
-
truncateToWidth(
|
|
643
|
+
render: (width: number) => {
|
|
644
|
+
const contentWidth = Math.max(0, width - 2);
|
|
645
|
+
return renderPanelFrame(theme, width, [
|
|
646
|
+
renderTabs(theme, activeIndex, contentWidth),
|
|
647
|
+
theme.fg("dim", "─".repeat(contentWidth)),
|
|
619
648
|
theme.fg(
|
|
620
|
-
"
|
|
621
|
-
|
|
649
|
+
"muted",
|
|
650
|
+
` ${sectionDescription(SECTIONS[activeIndex].id)}`,
|
|
651
|
+
),
|
|
652
|
+
"",
|
|
653
|
+
...list.render(contentWidth),
|
|
654
|
+
...(SECTIONS[activeIndex].id === "editor"
|
|
655
|
+
? renderEditorPreview(theme, contentWidth)
|
|
656
|
+
: SECTIONS[activeIndex].id === "context"
|
|
657
|
+
? renderContextPreview(theme, contentWidth)
|
|
658
|
+
: []),
|
|
659
|
+
"",
|
|
660
|
+
truncateToWidth(
|
|
661
|
+
theme.fg(
|
|
662
|
+
"dim",
|
|
663
|
+
" Tab/Shift+Tab switch sections · Enter/Space change · Esc close",
|
|
664
|
+
),
|
|
665
|
+
contentWidth,
|
|
622
666
|
),
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
],
|
|
667
|
+
]);
|
|
668
|
+
},
|
|
626
669
|
invalidate: () => list.invalidate(),
|
|
627
670
|
handleInput: (data: string) => {
|
|
628
671
|
if (data === "\x1b[Z" || matchesKey(data, "shift+tab")) {
|
|
@@ -640,10 +683,18 @@ export async function showOneUiPanel(
|
|
|
640
683
|
{
|
|
641
684
|
overlay: true,
|
|
642
685
|
overlayOptions: {
|
|
643
|
-
anchor: "center",
|
|
686
|
+
anchor: "top-center",
|
|
644
687
|
width: "85%",
|
|
645
688
|
maxHeight: "90%",
|
|
646
|
-
margin:
|
|
689
|
+
margin: {
|
|
690
|
+
top: 10,
|
|
691
|
+
right: 1,
|
|
692
|
+
bottom: 1,
|
|
693
|
+
left: 1,
|
|
694
|
+
},
|
|
695
|
+
},
|
|
696
|
+
onHandle: (handle: { focus: () => void }) => {
|
|
697
|
+
panelHandle = handle;
|
|
647
698
|
},
|
|
648
699
|
},
|
|
649
700
|
),
|
|
@@ -9,10 +9,7 @@ import {
|
|
|
9
9
|
visibleWidth,
|
|
10
10
|
type Component,
|
|
11
11
|
} from "@earendil-works/pi-tui";
|
|
12
|
-
import {
|
|
13
|
-
TOOL_LOADING_INTERVAL_MS,
|
|
14
|
-
toolLoadingIcon,
|
|
15
|
-
} from "../../../../tools/tool-loading-icon.ts";
|
|
12
|
+
import { toolLoadingIcon } from "../../../../tools/tool-loading-icon.ts";
|
|
16
13
|
import { isToolTuiFullscreen, showMoreHintText } from "./show-more-hint.ts";
|
|
17
14
|
import {
|
|
18
15
|
stripAnsi,
|
|
@@ -21,6 +18,15 @@ import {
|
|
|
21
18
|
} from "../../../../tools/ansi-text.ts";
|
|
22
19
|
import { walkComponentTree } from "../../../../tools/component-tree.ts";
|
|
23
20
|
import { humanizeToolLabel, toolCallSummary } from "./names.ts";
|
|
21
|
+
import {
|
|
22
|
+
captureIoViewMarkers,
|
|
23
|
+
getActiveIoViewFrame,
|
|
24
|
+
isExpandedToolIoView,
|
|
25
|
+
replayIoViewMarkers,
|
|
26
|
+
type CapturedIoViewMarker,
|
|
27
|
+
type ExpandedToolIoView,
|
|
28
|
+
type ToolIoSection,
|
|
29
|
+
} from "./result.ts";
|
|
24
30
|
import {
|
|
25
31
|
patchRegistry,
|
|
26
32
|
TOOL_GROUPING_GENERATION_KEY as GENERATION_KEY,
|
|
@@ -41,7 +47,6 @@ type Patch = {
|
|
|
41
47
|
generation: number;
|
|
42
48
|
lastEnabled: boolean;
|
|
43
49
|
theme?: any;
|
|
44
|
-
animationTimer: ReturnType<typeof setTimeout> | null;
|
|
45
50
|
};
|
|
46
51
|
|
|
47
52
|
function toolName(tool: any): string {
|
|
@@ -93,23 +98,6 @@ function statusIcon(value: ToolStatus): string {
|
|
|
93
98
|
return toolLoadingIcon();
|
|
94
99
|
}
|
|
95
100
|
|
|
96
|
-
function scheduleGroupAnimation(patch: Patch): void {
|
|
97
|
-
if (patch.animationTimer || !patch.active) return;
|
|
98
|
-
patch.animationTimer = setTimeout(() => {
|
|
99
|
-
patch.animationTimer = null;
|
|
100
|
-
if (!patch.active) return;
|
|
101
|
-
for (const group of patch.groups) {
|
|
102
|
-
if (
|
|
103
|
-
(group.children as any[]).some(
|
|
104
|
-
(tool) => tool?.executionStarted && status(tool) === "pending",
|
|
105
|
-
)
|
|
106
|
-
)
|
|
107
|
-
group.invalidate();
|
|
108
|
-
}
|
|
109
|
-
}, TOOL_LOADING_INTERVAL_MS);
|
|
110
|
-
patch.animationTimer.unref?.();
|
|
111
|
-
}
|
|
112
|
-
|
|
113
101
|
function visibleLines(lines: string[]): string[] {
|
|
114
102
|
return lines.filter((line) => stripAnsi(line).trim());
|
|
115
103
|
}
|
|
@@ -180,15 +168,74 @@ let nextGroupId = 1;
|
|
|
180
168
|
|
|
181
169
|
type SettledGroupCache = {
|
|
182
170
|
width: number;
|
|
171
|
+
expanded: boolean;
|
|
183
172
|
hover: boolean;
|
|
184
173
|
theme: unknown;
|
|
185
174
|
fullscreen: boolean;
|
|
186
175
|
children: readonly unknown[];
|
|
187
176
|
args: unknown[];
|
|
188
177
|
results: unknown[];
|
|
178
|
+
statuses: ToolStatus[];
|
|
179
|
+
callComponents: unknown[];
|
|
180
|
+
resultComponents: unknown[];
|
|
181
|
+
ioViews: Array<ExpandedToolIoView | undefined>;
|
|
182
|
+
ioHoveredSections: Array<ToolIoSection | null | undefined>;
|
|
183
|
+
ioRevisions: Array<number | undefined>;
|
|
184
|
+
capturedIoFrame: boolean;
|
|
185
|
+
lines: string[];
|
|
186
|
+
ioMarkers: CapturedIoViewMarker[];
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
type SettledGroupCacheSlots = {
|
|
190
|
+
collapsed?: SettledGroupCache;
|
|
191
|
+
expanded?: SettledGroupCache;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
type SettledExpandedChildCache = {
|
|
195
|
+
width: number;
|
|
196
|
+
theme: unknown;
|
|
197
|
+
fullscreen: boolean;
|
|
198
|
+
index: number;
|
|
199
|
+
total: number;
|
|
200
|
+
args: unknown;
|
|
201
|
+
result: unknown;
|
|
202
|
+
status: ToolStatus;
|
|
203
|
+
expanded: boolean;
|
|
204
|
+
callComponent: unknown;
|
|
205
|
+
resultComponent: unknown;
|
|
206
|
+
ioView: ExpandedToolIoView | undefined;
|
|
207
|
+
ioHoveredSection: ToolIoSection | null | undefined;
|
|
208
|
+
ioRevision: number | undefined;
|
|
209
|
+
capturedIoFrame: boolean;
|
|
189
210
|
lines: string[];
|
|
211
|
+
ioMarkers: CapturedIoViewMarker[];
|
|
190
212
|
};
|
|
191
213
|
|
|
214
|
+
function toolIoView(tool: any): ExpandedToolIoView | undefined {
|
|
215
|
+
for (const candidate of [
|
|
216
|
+
tool?.rendererState?.ccstyleIoView,
|
|
217
|
+
tool?.state?.ccstyleIoView,
|
|
218
|
+
tool?.resultRendererComponent,
|
|
219
|
+
]) {
|
|
220
|
+
if (isExpandedToolIoView(candidate)) {
|
|
221
|
+
return candidate;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function ioHoveredSection(
|
|
228
|
+
view: ExpandedToolIoView | undefined,
|
|
229
|
+
): ToolIoSection | null | undefined {
|
|
230
|
+
return view?.getHoveredSection();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function ioRenderRevision(
|
|
234
|
+
view: ExpandedToolIoView | undefined,
|
|
235
|
+
): number | undefined {
|
|
236
|
+
return view?.getRenderRevision();
|
|
237
|
+
}
|
|
238
|
+
|
|
192
239
|
export class ToolGroupComponent extends Container {
|
|
193
240
|
readonly toolCallId = `ccstyle-tool-group-${nextGroupId++}`;
|
|
194
241
|
readonly toolName = "Tool group";
|
|
@@ -199,8 +246,12 @@ export class ToolGroupComponent extends Container {
|
|
|
199
246
|
}
|
|
200
247
|
private hintHovered = false;
|
|
201
248
|
private readonly patch: Patch;
|
|
202
|
-
/**
|
|
203
|
-
private
|
|
249
|
+
/** 已完成分组按折叠/展开槽位跨帧缓存;pending 走子工具级缓存。 */
|
|
250
|
+
private settledCaches: SettledGroupCacheSlots = {};
|
|
251
|
+
private settledExpandedChildCaches = new WeakMap<
|
|
252
|
+
object,
|
|
253
|
+
SettledExpandedChildCache
|
|
254
|
+
>();
|
|
204
255
|
|
|
205
256
|
constructor(patch: Patch) {
|
|
206
257
|
super();
|
|
@@ -209,13 +260,13 @@ export class ToolGroupComponent extends Container {
|
|
|
209
260
|
}
|
|
210
261
|
|
|
211
262
|
addTool(tool: any): void {
|
|
212
|
-
this.
|
|
263
|
+
this.clearAllCaches();
|
|
213
264
|
this.children.push(tool);
|
|
214
265
|
tool[PARENT_KEY] = this;
|
|
215
266
|
}
|
|
216
267
|
|
|
217
268
|
releaseTools(): any[] {
|
|
218
|
-
this.
|
|
269
|
+
this.clearAllCaches();
|
|
219
270
|
const tools = [...this.children];
|
|
220
271
|
this.children.length = 0;
|
|
221
272
|
this.patch.groups.delete(this);
|
|
@@ -223,14 +274,13 @@ export class ToolGroupComponent extends Container {
|
|
|
223
274
|
}
|
|
224
275
|
|
|
225
276
|
removeTool(tool: any): void {
|
|
226
|
-
this.
|
|
277
|
+
this.clearAllCaches();
|
|
227
278
|
const index = this.children.indexOf(tool);
|
|
228
279
|
if (index >= 0) this.children.splice(index, 1);
|
|
229
280
|
if (tool?.[PARENT_KEY] === this) delete tool[PARENT_KEY];
|
|
230
281
|
}
|
|
231
282
|
|
|
232
283
|
setExpanded(expanded: boolean): void {
|
|
233
|
-
if (this._expanded !== expanded) this.settledCache = undefined;
|
|
234
284
|
this._expanded = expanded;
|
|
235
285
|
for (const tool of this.children)
|
|
236
286
|
(
|
|
@@ -239,7 +289,9 @@ export class ToolGroupComponent extends Container {
|
|
|
239
289
|
}
|
|
240
290
|
|
|
241
291
|
setHintHovered(hovered: boolean): void {
|
|
242
|
-
if (this.hintHovered !== hovered)
|
|
292
|
+
if (this.hintHovered !== hovered) {
|
|
293
|
+
this.clearSettledCaches();
|
|
294
|
+
}
|
|
243
295
|
this.hintHovered = hovered;
|
|
244
296
|
}
|
|
245
297
|
|
|
@@ -272,53 +324,205 @@ export class ToolGroupComponent extends Container {
|
|
|
272
324
|
}
|
|
273
325
|
|
|
274
326
|
invalidate(): void {
|
|
275
|
-
this.
|
|
327
|
+
this.clearAllCaches();
|
|
276
328
|
for (const tool of this.children) tool.invalidate?.();
|
|
277
329
|
}
|
|
278
330
|
|
|
331
|
+
private clearSettledCaches(): void {
|
|
332
|
+
this.settledCaches = {};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private clearAllCaches(): void {
|
|
336
|
+
this.clearSettledCaches();
|
|
337
|
+
this.settledExpandedChildCaches = new WeakMap();
|
|
338
|
+
}
|
|
339
|
+
|
|
279
340
|
private settledCacheHit(width: number): string[] | undefined {
|
|
280
|
-
const
|
|
281
|
-
|
|
341
|
+
const slot = this._expanded ? "expanded" : "collapsed";
|
|
342
|
+
const cache = this.settledCaches[slot];
|
|
343
|
+
if (!cache) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
282
346
|
if (
|
|
283
347
|
cache.width !== width ||
|
|
348
|
+
cache.expanded !== this._expanded ||
|
|
284
349
|
cache.hover !== this.hintHovered ||
|
|
285
350
|
cache.theme !== this.patch.theme ||
|
|
286
|
-
cache.fullscreen !== isToolTuiFullscreen()
|
|
351
|
+
cache.fullscreen !== isToolTuiFullscreen() ||
|
|
352
|
+
(getActiveIoViewFrame() !== null &&
|
|
353
|
+
!cache.capturedIoFrame &&
|
|
354
|
+
cache.ioViews.some(Boolean))
|
|
287
355
|
) {
|
|
288
356
|
return;
|
|
289
357
|
}
|
|
290
358
|
const tools = this.children as any[];
|
|
291
|
-
if (cache.children.length !== tools.length)
|
|
359
|
+
if (cache.children.length !== tools.length) {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
292
362
|
for (let i = 0; i < tools.length; i++) {
|
|
293
363
|
const tool = tools[i];
|
|
364
|
+
const ioView = toolIoView(tool);
|
|
365
|
+
const toolStatus = status(tool);
|
|
294
366
|
if (
|
|
295
367
|
cache.children[i] !== tool ||
|
|
296
368
|
cache.args[i] !== tool?.args ||
|
|
297
369
|
cache.results[i] !== tool?.result ||
|
|
298
|
-
|
|
370
|
+
cache.statuses[i] !== toolStatus ||
|
|
371
|
+
cache.callComponents[i] !== tool?.callRendererComponent ||
|
|
372
|
+
cache.resultComponents[i] !== tool?.resultRendererComponent ||
|
|
373
|
+
cache.ioViews[i] !== ioView ||
|
|
374
|
+
cache.ioHoveredSections[i] !== ioHoveredSection(ioView) ||
|
|
375
|
+
cache.ioRevisions[i] !== ioRenderRevision(ioView) ||
|
|
376
|
+
toolStatus === "pending"
|
|
299
377
|
) {
|
|
300
378
|
return;
|
|
301
379
|
}
|
|
302
380
|
}
|
|
303
|
-
return cache.lines;
|
|
381
|
+
return replayIoViewMarkers(cache.lines, cache.ioMarkers);
|
|
304
382
|
}
|
|
305
383
|
|
|
306
384
|
private storeSettledCache(width: number, lines: string[]): void {
|
|
307
|
-
this.
|
|
385
|
+
const tools = this.children as any[];
|
|
386
|
+
const ioViews = tools.map(toolIoView);
|
|
387
|
+
const captured = captureIoViewMarkers(lines);
|
|
388
|
+
const cache: SettledGroupCache = {
|
|
308
389
|
width,
|
|
390
|
+
expanded: this._expanded,
|
|
309
391
|
hover: this.hintHovered,
|
|
310
392
|
theme: this.patch.theme,
|
|
311
393
|
fullscreen: isToolTuiFullscreen(),
|
|
312
|
-
children: [...
|
|
313
|
-
args:
|
|
314
|
-
results:
|
|
315
|
-
|
|
394
|
+
children: [...tools],
|
|
395
|
+
args: tools.map((tool) => tool?.args),
|
|
396
|
+
results: tools.map((tool) => tool?.result),
|
|
397
|
+
statuses: tools.map(status),
|
|
398
|
+
callComponents: tools.map((tool) => tool?.callRendererComponent),
|
|
399
|
+
resultComponents: tools.map((tool) => tool?.resultRendererComponent),
|
|
400
|
+
ioViews,
|
|
401
|
+
ioHoveredSections: ioViews.map(ioHoveredSection),
|
|
402
|
+
ioRevisions: ioViews.map(ioRenderRevision),
|
|
403
|
+
capturedIoFrame: getActiveIoViewFrame() !== null,
|
|
404
|
+
lines: captured.lines,
|
|
405
|
+
ioMarkers: captured.markers,
|
|
316
406
|
};
|
|
407
|
+
this.settledCaches[this._expanded ? "expanded" : "collapsed"] = cache;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
private settledExpandedChildCacheHit(
|
|
411
|
+
tool: any,
|
|
412
|
+
index: number,
|
|
413
|
+
total: number,
|
|
414
|
+
width: number,
|
|
415
|
+
): string[] | undefined {
|
|
416
|
+
const cache = this.settledExpandedChildCaches.get(tool);
|
|
417
|
+
if (!cache) {
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
const toolStatus = status(tool);
|
|
421
|
+
const ioView = toolIoView(tool);
|
|
422
|
+
if (
|
|
423
|
+
toolStatus === "pending" ||
|
|
424
|
+
cache.width !== width ||
|
|
425
|
+
cache.theme !== this.patch.theme ||
|
|
426
|
+
cache.fullscreen !== isToolTuiFullscreen() ||
|
|
427
|
+
cache.index !== index ||
|
|
428
|
+
cache.total !== total ||
|
|
429
|
+
cache.args !== tool?.args ||
|
|
430
|
+
cache.result !== tool?.result ||
|
|
431
|
+
cache.status !== toolStatus ||
|
|
432
|
+
cache.expanded !== (tool?.expanded === true) ||
|
|
433
|
+
cache.callComponent !== tool?.callRendererComponent ||
|
|
434
|
+
cache.resultComponent !== tool?.resultRendererComponent ||
|
|
435
|
+
cache.ioView !== ioView ||
|
|
436
|
+
cache.ioHoveredSection !== ioHoveredSection(ioView) ||
|
|
437
|
+
cache.ioRevision !== ioRenderRevision(ioView) ||
|
|
438
|
+
(getActiveIoViewFrame() !== null &&
|
|
439
|
+
!cache.capturedIoFrame &&
|
|
440
|
+
ioView !== undefined)
|
|
441
|
+
) {
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
return replayIoViewMarkers(cache.lines, cache.ioMarkers);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
private storeSettledExpandedChildCache(
|
|
448
|
+
tool: any,
|
|
449
|
+
index: number,
|
|
450
|
+
total: number,
|
|
451
|
+
width: number,
|
|
452
|
+
lines: string[],
|
|
453
|
+
): void {
|
|
454
|
+
const ioView = toolIoView(tool);
|
|
455
|
+
const captured = captureIoViewMarkers(lines);
|
|
456
|
+
this.settledExpandedChildCaches.set(tool, {
|
|
457
|
+
width,
|
|
458
|
+
theme: this.patch.theme,
|
|
459
|
+
fullscreen: isToolTuiFullscreen(),
|
|
460
|
+
index,
|
|
461
|
+
total,
|
|
462
|
+
args: tool?.args,
|
|
463
|
+
result: tool?.result,
|
|
464
|
+
status: status(tool),
|
|
465
|
+
expanded: tool?.expanded === true,
|
|
466
|
+
callComponent: tool?.callRendererComponent,
|
|
467
|
+
resultComponent: tool?.resultRendererComponent,
|
|
468
|
+
ioView,
|
|
469
|
+
ioHoveredSection: ioHoveredSection(ioView),
|
|
470
|
+
ioRevision: ioRenderRevision(ioView),
|
|
471
|
+
capturedIoFrame: getActiveIoViewFrame() !== null,
|
|
472
|
+
lines: captured.lines,
|
|
473
|
+
ioMarkers: captured.markers,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
private renderExpandedChildBlock(
|
|
478
|
+
tool: any,
|
|
479
|
+
index: number,
|
|
480
|
+
total: number,
|
|
481
|
+
width: number,
|
|
482
|
+
theme: any,
|
|
483
|
+
fg: (color: string, text: string) => string,
|
|
484
|
+
): string[] {
|
|
485
|
+
const cached = this.settledExpandedChildCacheHit(tool, index, total, width);
|
|
486
|
+
if (cached) {
|
|
487
|
+
return cached;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const toolStatus = status(tool);
|
|
491
|
+
const color = toolStatus === "pending" ? "accent" : toolStatus;
|
|
492
|
+
const branch = index === total - 1 ? "└" : "├";
|
|
493
|
+
const continuation = index === total - 1 ? " " : "│ ";
|
|
494
|
+
const rendered = visibleLines(tool.render(Math.max(1, width - 2)));
|
|
495
|
+
if (rendered.length) {
|
|
496
|
+
rendered[0] = stripLeadingStatusIcon(rendered[0])
|
|
497
|
+
.replace(/^ +/, "")
|
|
498
|
+
.replace(/^((?:\x1b\[[0-?]*[ -/]*[@-~])*) +/, "$1");
|
|
499
|
+
}
|
|
500
|
+
const childLines = rendered.length ? rendered : [toolSummary(tool).main];
|
|
501
|
+
const backgroundSlot = "userMessageBg";
|
|
502
|
+
const lines = childLines.map((line, lineIndex) => {
|
|
503
|
+
const content = lineIndex === 0 ? line : stripLeadingSpaces(line, 1);
|
|
504
|
+
const prefix =
|
|
505
|
+
lineIndex === 0
|
|
506
|
+
? `${fg("dim", branch)} ${fg(color, statusIcon(toolStatus))} `
|
|
507
|
+
: fg("dim", continuation);
|
|
508
|
+
return paddedBackgroundRow(
|
|
509
|
+
theme,
|
|
510
|
+
backgroundSlot,
|
|
511
|
+
prefix + content,
|
|
512
|
+
width,
|
|
513
|
+
);
|
|
514
|
+
});
|
|
515
|
+
if (toolStatus !== "pending") {
|
|
516
|
+
this.storeSettledExpandedChildCache(tool, index, total, width, lines);
|
|
517
|
+
}
|
|
518
|
+
return lines;
|
|
317
519
|
}
|
|
318
520
|
|
|
319
521
|
render(width: number): string[] {
|
|
320
522
|
const cached = this.settledCacheHit(width);
|
|
321
|
-
if (cached)
|
|
523
|
+
if (cached) {
|
|
524
|
+
return cached;
|
|
525
|
+
}
|
|
322
526
|
const theme = this.patch.theme;
|
|
323
527
|
const fg = (color: string, text: string) =>
|
|
324
528
|
theme?.fg?.(color, text) ?? text;
|
|
@@ -343,12 +547,6 @@ export class ToolGroupComponent extends Container {
|
|
|
343
547
|
: counts.pending
|
|
344
548
|
? "pending"
|
|
345
549
|
: "success";
|
|
346
|
-
if (
|
|
347
|
-
(this.children as any[]).some(
|
|
348
|
-
(tool) => tool?.executionStarted && status(tool) === "pending",
|
|
349
|
-
)
|
|
350
|
-
)
|
|
351
|
-
scheduleGroupAnimation(this.patch);
|
|
352
550
|
const overallColor = overall === "pending" ? "accent" : overall;
|
|
353
551
|
const nameList =
|
|
354
552
|
names.size > 1 ? ` ${fg("dim", `• ${toolNameList(this.children)}`)}` : "";
|
|
@@ -363,52 +561,37 @@ export class ToolGroupComponent extends Container {
|
|
|
363
561
|
),
|
|
364
562
|
];
|
|
365
563
|
const total = this.children.length;
|
|
366
|
-
const expandedLines: string[] = [];
|
|
367
564
|
for (let index = 0; index < total; index++) {
|
|
368
565
|
const tool = this.children[index];
|
|
369
|
-
|
|
370
|
-
const color = toolStatus === "pending" ? "accent" : toolStatus;
|
|
371
|
-
const branch = index === total - 1 ? "└" : "├";
|
|
372
|
-
const continuation = index === total - 1 ? " " : "│ ";
|
|
373
|
-
if (!this._expanded) {
|
|
374
|
-
const summary = toolSummary(tool);
|
|
566
|
+
if (this._expanded) {
|
|
375
567
|
lines.push(
|
|
376
|
-
|
|
377
|
-
|
|
568
|
+
...this.renderExpandedChildBlock(
|
|
569
|
+
tool,
|
|
570
|
+
index,
|
|
571
|
+
total,
|
|
378
572
|
width,
|
|
379
|
-
|
|
573
|
+
theme,
|
|
574
|
+
fg,
|
|
380
575
|
),
|
|
381
576
|
);
|
|
382
577
|
continue;
|
|
383
578
|
}
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
? childLines[lineIndex]
|
|
396
|
-
: stripLeadingSpaces(childLines[lineIndex], 1);
|
|
397
|
-
const prefix =
|
|
398
|
-
lineIndex === 0
|
|
399
|
-
? `${fg("dim", branch)} ${fg(color, statusIcon(toolStatus))} `
|
|
400
|
-
: fg("dim", continuation);
|
|
401
|
-
expandedLines.push(prefix + content);
|
|
402
|
-
}
|
|
579
|
+
const toolStatus = status(tool);
|
|
580
|
+
const color = toolStatus === "pending" ? "accent" : toolStatus;
|
|
581
|
+
const branch = index === total - 1 ? "└" : "├";
|
|
582
|
+
const summary = toolSummary(tool);
|
|
583
|
+
lines.push(
|
|
584
|
+
truncateToWidth(
|
|
585
|
+
` ${fg("dim", branch)} ${fg(color, statusIcon(toolStatus))} ${fg("toolTitle", summary.main)}${fg("dim", summary.detail)}`,
|
|
586
|
+
width,
|
|
587
|
+
"…",
|
|
588
|
+
),
|
|
589
|
+
);
|
|
403
590
|
}
|
|
404
591
|
if (this._expanded) {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
lines.push(paddedBackgroundRow(theme, backgroundSlot, line, width));
|
|
409
|
-
}
|
|
410
|
-
lines.push(paddedBackgroundRow(theme, backgroundSlot, "", width));
|
|
411
|
-
} else if (counts.pending === 0) {
|
|
592
|
+
lines.push(paddedBackgroundRow(theme, "userMessageBg", "", width));
|
|
593
|
+
}
|
|
594
|
+
if (counts.pending === 0) {
|
|
412
595
|
this.storeSettledCache(width, lines);
|
|
413
596
|
}
|
|
414
597
|
return lines;
|
|
@@ -517,8 +700,6 @@ export function installToolGrouping(
|
|
|
517
700
|
if (previous) {
|
|
518
701
|
previous.active = false;
|
|
519
702
|
previous.enabled = () => false;
|
|
520
|
-
if (previous.animationTimer) clearTimeout(previous.animationTimer);
|
|
521
|
-
previous.animationTimer = null;
|
|
522
703
|
ungroup(previous);
|
|
523
704
|
}
|
|
524
705
|
const original = {
|
|
@@ -545,7 +726,6 @@ export function installToolGrouping(
|
|
|
545
726
|
enabled: getEnabled,
|
|
546
727
|
generation: 0,
|
|
547
728
|
lastEnabled: getEnabled(),
|
|
548
|
-
animationTimer: null,
|
|
549
729
|
};
|
|
550
730
|
patch.installed = {
|
|
551
731
|
addChild: function (this: any, component: any) {
|
|
@@ -604,8 +784,6 @@ export function installToolGrouping(
|
|
|
604
784
|
shutdown() {
|
|
605
785
|
if (!patch.active) return;
|
|
606
786
|
patch.active = false;
|
|
607
|
-
if (patch.animationTimer) clearTimeout(patch.animationTimer);
|
|
608
|
-
patch.animationTimer = null;
|
|
609
787
|
patch.enabled = () => false;
|
|
610
788
|
ungroup(patch);
|
|
611
789
|
if (prototype.addChild === patch.installed.addChild)
|
|
@@ -270,6 +270,7 @@ export class ExpandedToolIoView {
|
|
|
270
270
|
private maxInputLines: number;
|
|
271
271
|
private cachedWidth: number | undefined;
|
|
272
272
|
private cachedLines: string[] | undefined;
|
|
273
|
+
private renderRevision = 0;
|
|
273
274
|
private hoveredSection: ToolIoSection | null = null;
|
|
274
275
|
/** Which sections currently show the show-more affordance (after last render). */
|
|
275
276
|
private truncated: { input: boolean; output: boolean } = {
|
|
@@ -344,6 +345,14 @@ export class ExpandedToolIoView {
|
|
|
344
345
|
return this.outputBody.trim() ? this.outputBody : "Done";
|
|
345
346
|
}
|
|
346
347
|
|
|
348
|
+
getHoveredSection(): ToolIoSection | null {
|
|
349
|
+
return this.hoveredSection;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
getRenderRevision(): number {
|
|
353
|
+
return this.renderRevision;
|
|
354
|
+
}
|
|
355
|
+
|
|
347
356
|
setHoveredSection(section: ToolIoSection | null): void {
|
|
348
357
|
if (this.hoveredSection === section) return;
|
|
349
358
|
this.hoveredSection = section;
|
|
@@ -534,6 +543,7 @@ export class ExpandedToolIoView {
|
|
|
534
543
|
}
|
|
535
544
|
|
|
536
545
|
invalidate(): void {
|
|
546
|
+
this.renderRevision++;
|
|
537
547
|
this.cachedWidth = undefined;
|
|
538
548
|
this.cachedLines = undefined;
|
|
539
549
|
}
|
|
@@ -817,6 +827,72 @@ function withIoViewMarkers(
|
|
|
817
827
|
}
|
|
818
828
|
return marked;
|
|
819
829
|
}
|
|
830
|
+
|
|
831
|
+
const TOOL_VIEW_MARKER_RE = /\x1b_cc:v(\d+):([io])\x07/g;
|
|
832
|
+
|
|
833
|
+
export type CapturedIoViewMarker = {
|
|
834
|
+
line: number;
|
|
835
|
+
view: ExpandedToolIoView;
|
|
836
|
+
section: ToolIoSection;
|
|
837
|
+
};
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* Removes frame-local IO markers before rendered lines enter a cross-frame cache.
|
|
841
|
+
* The captured view identities let a later frame register fresh marker ids.
|
|
842
|
+
*/
|
|
843
|
+
export function captureIoViewMarkers(lines: string[]): {
|
|
844
|
+
lines: string[];
|
|
845
|
+
markers: CapturedIoViewMarker[];
|
|
846
|
+
} {
|
|
847
|
+
const frame = activeIoViewFrame;
|
|
848
|
+
if (!frame) {
|
|
849
|
+
return { lines, markers: [] };
|
|
850
|
+
}
|
|
851
|
+
const markers: CapturedIoViewMarker[] = [];
|
|
852
|
+
let changed = false;
|
|
853
|
+
const clean = lines.map((line, lineIndex) =>
|
|
854
|
+
line.replace(TOOL_VIEW_MARKER_RE, (marker, rawId, rawSection) => {
|
|
855
|
+
const view = frame.idToView.get(Number(rawId));
|
|
856
|
+
if (!view) {
|
|
857
|
+
return marker;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
changed = true;
|
|
861
|
+
markers.push({
|
|
862
|
+
line: lineIndex,
|
|
863
|
+
view,
|
|
864
|
+
section: rawSection === "i" ? "input" : "output",
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
return "";
|
|
868
|
+
}),
|
|
869
|
+
);
|
|
870
|
+
return { lines: changed ? clean : lines, markers };
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/** Re-registers cached IO views in the active frame and attaches fresh marker ids. */
|
|
874
|
+
export function replayIoViewMarkers(
|
|
875
|
+
lines: string[],
|
|
876
|
+
markers: readonly CapturedIoViewMarker[],
|
|
877
|
+
): string[] {
|
|
878
|
+
if (!activeIoViewFrame || markers.length === 0) {
|
|
879
|
+
return lines;
|
|
880
|
+
}
|
|
881
|
+
const replayed = lines.slice();
|
|
882
|
+
for (const marker of markers) {
|
|
883
|
+
if (marker.line < 0 || marker.line >= replayed.length) {
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
const id = frameViewId(marker.view);
|
|
887
|
+
if (id === null) {
|
|
888
|
+
continue;
|
|
889
|
+
}
|
|
890
|
+
replayed[marker.line] =
|
|
891
|
+
`${replayed[marker.line]}${toolViewMarker(id, marker.section)}`;
|
|
892
|
+
}
|
|
893
|
+
return replayed;
|
|
894
|
+
}
|
|
895
|
+
|
|
820
896
|
const ioViewInvalidators = new WeakMap<ExpandedToolIoView, () => void>();
|
|
821
897
|
function rememberIoView(context: any, view: ExpandedToolIoView): void {
|
|
822
898
|
if (!context || typeof context !== "object") return;
|
package/package.json
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-one-ui",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "A single, customizable Pi UI package with layout-oriented Context and Shell rendering for Pi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/kerolt/pi-one-ui"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/kerolt/pi-one-ui/issues"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/kerolt/pi-one-ui#readme",
|
|
7
15
|
"files": [
|
|
8
16
|
"extensions",
|
|
9
17
|
"themes",
|