pi-ui-extension 1.1.10 → 1.2.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.md +35 -10
- package/README.zh-CN.md +36 -10
- package/extensions/ctx/README.md +3 -3
- package/extensions/ctx/data.test.ts +3 -3
- package/extensions/ctx/tools.test.ts +3 -3
- package/extensions/index.ts +87 -49
- package/extensions/mcp/integration.test.ts +1 -1
- package/extensions/session/manager.test.ts +141 -0
- package/extensions/session/manager.ts +183 -0
- package/extensions/session/panel.test.ts +195 -1
- package/extensions/session/panel.ts +74 -13
- package/extensions/sidebar/default-panels.test.ts +6 -11
- package/extensions/sidebar/default-panels.ts +4 -2
- package/extensions/tool-view/codemode-compact.ts +135 -0
- package/extensions/tool-view/codemode.test.ts +209 -0
- package/extensions/tool-view/controller.test.ts +2 -1
- package/extensions/tool-view/controller.ts +48 -81
- package/extensions/tool-view/panel.test.ts +8 -36
- package/extensions/tool-view/panel.ts +1 -7
- package/extensions/tool-view/renderer.test.ts +149 -0
- package/extensions/tool-view/usage.test.ts +0 -37
- package/extensions/tool-view/usage.ts +4 -70
- package/package.json +1 -1
- package/extensions/fabric-detection.test.ts +0 -15
- package/extensions/fabric-detection.ts +0 -7
- package/extensions/tool-view/trust.test.ts +0 -41
package/README.md
CHANGED
|
@@ -34,9 +34,18 @@ The `SESSIONS` panel provides quick access to recent sessions:
|
|
|
34
34
|
- Click a session to switch to it.
|
|
35
35
|
- Rename a non-current session.
|
|
36
36
|
- Delete a non-current session.
|
|
37
|
-
- Use
|
|
37
|
+
- Use `⊗` to move other unpinned sessions to trash while keeping the current and pinned sessions.
|
|
38
38
|
- Click `SESSIONS ↻` to refresh the session list.
|
|
39
39
|
|
|
40
|
+
**Advanced Sessions management (v1.2.0+)**: click `≡` on the Sessions header or run `/sessions-manager`.
|
|
41
|
+
|
|
42
|
+
- Browse all saved sessions for the current project in **8-item pages**, with previous/next navigation and refresh.
|
|
43
|
+
- Search session titles or IDs case-insensitively, and sort by latest update, oldest update, message count, or title.
|
|
44
|
+
- Switch, rename, pin/unpin, or move an individual session to trash. Deletions require confirmation and the active session cannot be deleted.
|
|
45
|
+
- Pinned sessions (`★`) appear first, including in the five-session quick sidebar. Pins persist as IDs (no session content) in `pi-ui-extension/session-pins.json` under Pi's agent directory.
|
|
46
|
+
- Each session row has a quick pin button: `☆` means unpinned and `★` means pinned; clicking it toggles pinning without opening the manager. The existing `✎` rename and `×` delete controls are unchanged. On narrow sidebars, the pin action hides to preserve the existing controls.
|
|
47
|
+
- The `⊗` bulk cleanup now **protects both the active session and all pinned sessions**. Explicit single-session deletion still requires confirmation.
|
|
48
|
+
|
|
40
49
|
To avoid corrupting active agent state, switching, renaming, deleting, and bulk cleanup are only allowed while Pi is idle. The current session cannot be deleted.
|
|
41
50
|
|
|
42
51
|
### 3. Resource shortcuts
|
|
@@ -100,7 +109,22 @@ For tool definitions, the extension prefers the observable provider-request tool
|
|
|
100
109
|
|
|
101
110
|
Modes can be changed directly with mouse clicks.
|
|
102
111
|
|
|
103
|
-
|
|
112
|
+
Tool View uses Pi's official `pi.registerToolRenderer()` hook for the built-in tools (`read`, `bash`, `edit`, `write`, `grep`, `find`, and `ls`) **and the built-in `codemode` tool**. It never replaces tool definitions or execution functions. Other tools retain their own renderers.
|
|
113
|
+
|
|
114
|
+
For **Codemode** specifically:
|
|
115
|
+
|
|
116
|
+
- **Normal** retains Pi's native script, nested-call, and output rendering (including its own expand/collapse behavior).
|
|
117
|
+
- **Compact** uses a two-line, status-colored summary with Chinese labels: a highlighted Codemode title and script line count, followed by grouped execution status, nested-call counts, output size, and duration. It omits script source, nested-call arguments, and actual output, even if the row is expanded or execution fails. On narrow terminals it keeps each part to one row and prioritizes status and failures over less important metrics.
|
|
118
|
+
- **Hidden** removes the Codemode transcript row, including expanded and failed results, without stopping execution or removing Tool/MCP usage accounting.
|
|
119
|
+
|
|
120
|
+
Example Compact output:
|
|
121
|
+
|
|
122
|
+
```text
|
|
123
|
+
▸ codemode 8 行代码
|
|
124
|
+
✓ 已完成 │ 1 次调用 · 1 成功 │ 59 行输出 · 85ms
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Changing the Tool View mode also updates previously rendered tool rows in the current session. Other third-party tools are unaffected.
|
|
104
128
|
|
|
105
129
|
### 6. Tool / MCP usage statistics
|
|
106
130
|
|
|
@@ -165,7 +189,7 @@ Example configuration:
|
|
|
165
189
|
To pin a specific version:
|
|
166
190
|
|
|
167
191
|
```bash
|
|
168
|
-
pi install npm:pi-ui-extension@1.
|
|
192
|
+
pi install npm:pi-ui-extension@1.2.0
|
|
169
193
|
```
|
|
170
194
|
|
|
171
195
|
or:
|
|
@@ -173,7 +197,7 @@ or:
|
|
|
173
197
|
```json
|
|
174
198
|
{
|
|
175
199
|
"packages": [
|
|
176
|
-
"npm:pi-ui-extension@1.
|
|
200
|
+
"npm:pi-ui-extension@1.2.0"
|
|
177
201
|
]
|
|
178
202
|
}
|
|
179
203
|
```
|
|
@@ -201,7 +225,7 @@ Configuration form:
|
|
|
201
225
|
Pin to a Git tag:
|
|
202
226
|
|
|
203
227
|
```bash
|
|
204
|
-
pi install git:github.com/1993yihuan/pi-ui-extension@v1.
|
|
228
|
+
pi install git:github.com/1993yihuan/pi-ui-extension@v1.2.0
|
|
205
229
|
```
|
|
206
230
|
|
|
207
231
|
### Temporary local-source testing
|
|
@@ -300,7 +324,7 @@ npm run typecheck
|
|
|
300
324
|
npm test
|
|
301
325
|
```
|
|
302
326
|
|
|
303
|
-
The test suite covers
|
|
327
|
+
The test suite covers Sidebar, CTX, Session management, official Tool View renderers, MCP resources, mouse scrolling, and resize handling.
|
|
304
328
|
|
|
305
329
|
During development, Pi can load the local source directory directly:
|
|
306
330
|
|
|
@@ -325,7 +349,6 @@ pi-ui-extension/
|
|
|
325
349
|
│ ├── session/ # Session list and actions
|
|
326
350
|
│ ├── sidebar/ # Sidebar layout, scrolling, resize
|
|
327
351
|
│ ├── tool-view/ # Tool View modes and usage statistics
|
|
328
|
-
│ ├── fabric-detection.ts # Fabric ownership detection
|
|
329
352
|
│ └── index.ts # Pi Extension entry point
|
|
330
353
|
├── .github/workflows/
|
|
331
354
|
│ ├── ci.yml # Automatic checks on push / PR
|
|
@@ -376,24 +399,26 @@ Checkout
|
|
|
376
399
|
→ npm pack --dry-run
|
|
377
400
|
→ npm Trusted Publishing (OIDC)
|
|
378
401
|
→ npm publish
|
|
379
|
-
→ verify npm Registry visibility (
|
|
402
|
+
→ verify npm Registry visibility (up to ~10 minutes of retries)
|
|
380
403
|
→ GitHub Release
|
|
381
404
|
```
|
|
382
405
|
|
|
383
406
|
npm publishing uses GitHub Actions OIDC Trusted Publishing, so no long-lived `NPM_TOKEN` is required in the repository.
|
|
384
407
|
|
|
408
|
+
Because npm can accept a publication before the new version becomes searchable, the release workflow waits up to approximately 10 minutes for registry indexing. If that verification times out, check the package on npm before retrying: a successful publish cannot be repeated for the same version.
|
|
409
|
+
|
|
385
410
|
## Compatibility and design principles
|
|
386
411
|
|
|
387
412
|
- Does not modify Pi Core.
|
|
388
413
|
- `@earendil-works/pi-coding-agent` and `@earendil-works/pi-tui` are host-provided peer dependencies to avoid duplicate runtime instances.
|
|
389
|
-
-
|
|
414
|
+
- Tool View changes only how built-in tool calls are rendered, using Pi's public renderer resolver rather than overriding their implementations.
|
|
390
415
|
- Session and resource operations use Pi public APIs wherever possible.
|
|
391
416
|
- Context composition is clearly presented as an estimate, not provider-exact token accounting.
|
|
392
417
|
- Fullscreen sidebar behavior degrades safely when fullscreen layout is unavailable.
|
|
393
418
|
|
|
394
419
|
## Requirements
|
|
395
420
|
|
|
396
|
-
- Pi Coding Agent
|
|
421
|
+
- Pi Coding Agent with the official `registerToolRenderer()` API (tested with `@earendil-works/pi-coding-agent@1.1.0`)
|
|
397
422
|
- Node.js `>= 22.19.0` (use a Node version supported by your Pi installation)
|
|
398
423
|
- CI validates Node.js 22.19.0 and 24; releases use Node.js 24
|
|
399
424
|
|
package/README.zh-CN.md
CHANGED
|
@@ -34,9 +34,19 @@ Sidebar 支持鼠标交互、滚轮滚动和终端尺寸变化。终端高度不
|
|
|
34
34
|
- 点击会话切换到对应 Session。
|
|
35
35
|
- 重命名非当前 Session。
|
|
36
36
|
- 删除非当前 Session。
|
|
37
|
-
- 使用右上角 `⊗`
|
|
37
|
+
- 使用右上角 `⊗` 清理其他未置顶会话(保留当前和置顶 Session)。
|
|
38
38
|
- 点击 `SESSIONS ↻` 刷新会话列表。
|
|
39
39
|
|
|
40
|
+
**Sessions 高级管理(v1.2.0 起)**:点击标题右侧的 `≡`,或输入 `/sessions-manager` 打开完整会话管理器。
|
|
41
|
+
|
|
42
|
+
- 全量浏览当前项目的历史 Session,**每页 8 条**,可向前 / 向后翻页和手动刷新。
|
|
43
|
+
- 按会话标题或 Session ID 搜索(不区分大小写),支持最近更新、最早更新、消息数量和标题排序。
|
|
44
|
+
- 单独查看会话操作:切换、重命名、置顶 / 取消置顶、移到回收站。删除前有确认,当前 Session 不允许删除。
|
|
45
|
+
- 置顶会话始终优先排序,并使用 `★` 标记,优先显示在 Sidebar 的最近 5 条会话中。
|
|
46
|
+
- Sidebar 每条会话右侧新增快捷置顶按钮:`☆` 表示未置顶,`★` 表示已置顶,点击立即切换;与 `✎` 重命名、`×` 删除互不影响。Sidebar 较窄时优先保留原有操作按钮。
|
|
47
|
+
- 置顶 ID 持久保存在 Pi 的 agent 目录下 `pi-ui-extension/session-pins.json`,不保存会话内容。
|
|
48
|
+
- 使用 `⊗` 批量清理时,**保留当前会话及所有置顶会话**;显式删除单条置顶会话仍需确认。
|
|
49
|
+
|
|
40
50
|
为避免正在执行中的 Agent 状态被破坏,会话切换、重命名、删除和批量清理只会在 Pi 空闲时执行;当前正在使用的 Session 不允许删除。
|
|
41
51
|
|
|
42
52
|
### 3. 资源快速查看
|
|
@@ -100,7 +110,22 @@ Composition 会将当前可观测上下文拆分为:
|
|
|
100
110
|
|
|
101
111
|
可以直接使用鼠标点击模式进行切换。
|
|
102
112
|
|
|
103
|
-
|
|
113
|
+
Tool View 使用 Pi 官方的 `pi.registerToolRenderer()` 接口管理内置工具(`read`、`bash`、`edit`、`write`、`grep`、`find` 和 `ls`)**以及内置 `codemode` 工具**的展示,不再重注册工具定义或接管工具执行逻辑。其他 Tool 保留其自身的渲染方式。
|
|
114
|
+
|
|
115
|
+
对于 **Codemode**:
|
|
116
|
+
|
|
117
|
+
- **Normal**:保留 Pi 原生的脚本、子工具调用、输出详情,以及原生展开 / 收起行为。
|
|
118
|
+
- **Compact**:采用带有状态颜色的双行摘要。第一行突出 Codemode 和代码行数;第二行将状态、子调用统计、输出规模与耗时分组展示,并统一使用中文单位。即使展开或执行失败,也不会展示完整代码、子调用参数或真实输出;终端宽度不足时优先保留状态、失败信息和调用次数,避免摘要自动换成多行。
|
|
119
|
+
- **Hidden**:隐藏 Codemode 工具记录(包括展开和失败结果),但不影响实际执行或 Tool / MCP 调用统计。
|
|
120
|
+
|
|
121
|
+
Compact 效果示例:
|
|
122
|
+
|
|
123
|
+
```text
|
|
124
|
+
▸ codemode 8 行代码
|
|
125
|
+
✓ 已完成 │ 1 次调用 · 1 成功 │ 59 行输出 · 85ms
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
切换 Tool View 模式也会更新当前会话中已经展示的历史工具记录;其他第三方工具保持原有渲染方式。
|
|
104
129
|
|
|
105
130
|
### 6. Tool / MCP 调用统计
|
|
106
131
|
|
|
@@ -165,7 +190,7 @@ Pi 会安装 package,并将声明写入个人配置:
|
|
|
165
190
|
如果希望固定版本:
|
|
166
191
|
|
|
167
192
|
```bash
|
|
168
|
-
pi install npm:pi-ui-extension@1.
|
|
193
|
+
pi install npm:pi-ui-extension@1.2.0
|
|
169
194
|
```
|
|
170
195
|
|
|
171
196
|
或者:
|
|
@@ -173,7 +198,7 @@ pi install npm:pi-ui-extension@1.1.10
|
|
|
173
198
|
```json
|
|
174
199
|
{
|
|
175
200
|
"packages": [
|
|
176
|
-
"npm:pi-ui-extension@1.
|
|
201
|
+
"npm:pi-ui-extension@1.2.0"
|
|
177
202
|
]
|
|
178
203
|
}
|
|
179
204
|
```
|
|
@@ -201,7 +226,7 @@ pi install git:github.com/1993yihuan/pi-ui-extension
|
|
|
201
226
|
固定到 Git tag:
|
|
202
227
|
|
|
203
228
|
```bash
|
|
204
|
-
pi install git:github.com/1993yihuan/pi-ui-extension@v1.
|
|
229
|
+
pi install git:github.com/1993yihuan/pi-ui-extension@v1.2.0
|
|
205
230
|
```
|
|
206
231
|
|
|
207
232
|
### 临时加载本地源码
|
|
@@ -300,7 +325,7 @@ npm run typecheck
|
|
|
300
325
|
npm test
|
|
301
326
|
```
|
|
302
327
|
|
|
303
|
-
当前自动化测试覆盖 Sidebar、CTX、Session
|
|
328
|
+
当前自动化测试覆盖 Sidebar、CTX、Session、官方 Tool Renderer、MCP resource、鼠标滚轮 / resize 等主要行为。
|
|
304
329
|
|
|
305
330
|
开发时可以直接让 Pi 加载本地目录:
|
|
306
331
|
|
|
@@ -325,7 +350,6 @@ pi-ui-extension/
|
|
|
325
350
|
│ ├── session/ # Session 列表与操作
|
|
326
351
|
│ ├── sidebar/ # Sidebar 布局、滚动和 resize
|
|
327
352
|
│ ├── tool-view/ # Tool View 模式与调用统计
|
|
328
|
-
│ ├── fabric-detection.ts # Fabric 工具接管检测
|
|
329
353
|
│ └── index.ts # Pi Extension 入口
|
|
330
354
|
├── .github/workflows/
|
|
331
355
|
│ ├── ci.yml # push / PR 自动检查
|
|
@@ -376,24 +400,26 @@ Checkout
|
|
|
376
400
|
→ npm pack --dry-run
|
|
377
401
|
→ npm Trusted Publishing (OIDC)
|
|
378
402
|
→ npm publish
|
|
379
|
-
→ 重试检查 npm Registry
|
|
403
|
+
→ 重试检查 npm Registry 已可查询新版本(最多约 10 分钟)
|
|
380
404
|
→ GitHub Release
|
|
381
405
|
```
|
|
382
406
|
|
|
383
407
|
npm 发布使用 GitHub Actions OIDC Trusted Publishing,不需要在 GitHub Repository 中保存长期 `NPM_TOKEN`。
|
|
384
408
|
|
|
409
|
+
npm 可能先接受发布请求,再经过一段时间才能查询到新版本。Release 工作流会等待最多约 10 分钟让 Registry 完成索引。如果检查超时,请先核对 npm 上的实际版本再决定如何处理,避免对已发布的同一个版本重复执行 `npm publish`。
|
|
410
|
+
|
|
385
411
|
## 兼容性与设计原则
|
|
386
412
|
|
|
387
413
|
- 不修改 Pi Core。
|
|
388
414
|
- Pi host 提供的 `@earendil-works/pi-coding-agent` 和 `@earendil-works/pi-tui` 使用 `peerDependencies`,避免产生重复运行时实例。
|
|
389
|
-
-
|
|
415
|
+
- Tool View 只通过 Pi 官方 Renderer Resolver 控制内置工具的显示,不覆写工具定义或执行逻辑。
|
|
390
416
|
- Session 与资源操作尽量通过 Pi 已公开 API 完成。
|
|
391
417
|
- Context composition 明确标记为估算值,不冒充 provider 精确 token 统计。
|
|
392
418
|
- fullscreen Sidebar 不可用时安全降级,不影响 Pi 主流程。
|
|
393
419
|
|
|
394
420
|
## Requirements
|
|
395
421
|
|
|
396
|
-
- Pi Coding Agent
|
|
422
|
+
- 支持官方 `registerToolRenderer()` API 的 Pi Coding Agent(已使用 `@earendil-works/pi-coding-agent@1.1.0` 验证)
|
|
397
423
|
- Node.js `>= 22.19.0`(建议使用当前 Pi 支持的 Node 版本)
|
|
398
424
|
- CI 覆盖 Node.js 22.19.0 和 24;发布工作流使用 Node.js 24
|
|
399
425
|
|
package/extensions/ctx/README.md
CHANGED
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
|
|
7
7
|
- approx = 本地可观测文本 UTF-16 长度 / 4,逐消息向上取整;不是模型 tokenizer。已知消息先经公开 SDK `convertToLlm` 转换,再提取文本,因此包含摘要包装、bash 无输出/退出/取消/截断提示;仍按原始角色分类。未知角色安全降级为外层 content 文本,`excludeFromContext` 的 bash 不计入。
|
|
8
8
|
- 六类为 System、Tools、Messages、Tool Results、Summary、Other。百分比分母是已覆盖分类的估算 token 合计,不是模型窗口;pi usage 单独展示,不校准分类。
|
|
9
|
-
- System 使用 ctx.getSystemPrompt() 的可观测字符串;无法取得时 unknown。Tools 通过 pi.getActiveTools()/getAllTools() 仅选取当前启用的工具,逐工具序列化 `{name, description, parameters}`,按 UTF-16 长度 / 4 向上取整后合计,并纳入分类百分比分母。不计 sourceInfo、执行函数及已归入 System 的 promptGuidelines
|
|
9
|
+
- System 使用 ctx.getSystemPrompt() 的可观测字符串;无法取得时 unknown。Tools 通过 pi.getActiveTools()/getAllTools() 仅选取当前启用的工具,逐工具序列化 `{name, description, parameters}`,按 UTF-16 长度 / 4 向上取整后合计,并纳入分类百分比分母。不计 sourceInfo、执行函数及已归入 System 的 promptGuidelines;未直接启用的工具不重复计入。无启用工具为 0;API 不可用、启用工具缺少定义或 schema 无法序列化时保持 unknown,避免显示不完整合计。工具定义在每次现有快照事件刷新时重新读取(包括动态启用/禁用),不在 render 中读取;空闲时注册工具将在下一次快照事件反映。
|
|
10
10
|
- Tools 在 `before_provider_request` 事件后优先统计 provider-specific payload 的 `tools` 字段序列化文本(仍按 UTF-16 长度 / 4 估算);请求发出前未观察到 payload 时,回退到当前启用工具 schema 的本地估算。它仍不是模型 tokenizer、HTTP 字节数或计费 token,也不包含 headers;如果后续扩展继续改写 payload,面板值可能是本扩展处理时观察到的版本。Images 仅统计图片块数量,不序列化请求或计算未展示的字节数;在 `before_provider_request` 可取得最终 provider payload 时优先统计该 payload,否则统计当前 context 消息;未能观测时显示 `not observed`。
|
|
11
11
|
- context 事件提供调用前的消息快照;session_start/tree/compact、turn_end、agent_settled 使用 buildContextEntries() 重建 compaction-applied active branch。后者显示 active branch,前者显示 context hook。事件之外只读取缓存,不在 render 中扫描历史。
|
|
12
12
|
- Messages 包括 user/assistant 文本、thinking 和 tool call 名称/参数;Tool Results 仅计 content;Summary 包括 compaction/branch summary;Other 包括可发送 custom message 和非排除的用户 bash。
|
|
13
|
-
- Tool Results
|
|
13
|
+
- Tool Results 按当前快照工具结果文本估算。每条 Tool Result 的 content 只计一次,不遍历内部操作;不读 details、全量 JSONL、沙箱、截断前原始输出或被压缩结果。已返回 content 内的截断提示本身仍属于可发送文本。
|
|
14
14
|
|
|
15
15
|
Composition 的 token 估算使用 `~` 前缀及 K/M 紧凑缩写(四舍五入),百分比保留一位小数。宽度不足时标签与数值拆行;支持的最小侧栏宽 18 可完整显示如 `~100K 100.0%`。
|
|
16
16
|
|
|
@@ -32,4 +32,4 @@ Composition 的 token 估算使用 `~` 前缀及 K/M 紧凑缩写(四舍五入
|
|
|
32
32
|
|
|
33
33
|
这是明确标记的本地估算,不宣称最终请求精确统计。后加载扩展仍可能改写 context 或 provider payload;不同 provider 的转换、剔除、包装开销未知。恢复/回合结束时 active branch 可能不同于上次请求经过 context 扩展裁剪后的快照;下一次 context 事件会替换它。模型生成过程中不逐 token 更新分类,最终回合/settled 时更新。只含图片的消息可见文本为零,不代表图片成本为零。
|
|
34
34
|
|
|
35
|
-
未修改
|
|
35
|
+
未修改 Pi Core 或全局配置。全屏鼠标/极短终端还应在用户真实终端手工确认。
|
|
@@ -3,8 +3,8 @@ import test from "node:test";
|
|
|
3
3
|
import { createReadTool, SessionManager } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { estimateContext, installContextEstimates, messagesFromContextEntries } from "./data.ts";
|
|
5
5
|
|
|
6
|
-
const result = (n: number) => ({ role: "toolResult", toolName: "
|
|
7
|
-
const tool = { name: "
|
|
6
|
+
const result = (n: number) => ({ role: "toolResult", toolName: "sample_runner", toolCallId: String(n), content: [{ type: "text", text: "x".repeat(n * 4) }], details: { fullOutput: "x".repeat(100000), operations: [{ toolName: "read" }] } });
|
|
7
|
+
const tool = { name: "sample_runner", description: "Execute TypeScript", parameters: createReadTool(process.cwd()).parameters };
|
|
8
8
|
|
|
9
9
|
test("context estimate keeps tool schemas, summaries, images and active branch semantics", () => {
|
|
10
10
|
const snapshot = estimateContext([{role:"user",content:"abcd"}, result(4), {role:"compactionSummary",summary:"abcd"}], "abcd", "context hook", [tool]);
|
|
@@ -16,7 +16,7 @@ test("context estimate keeps tool schemas, summaries, images and active branch s
|
|
|
16
16
|
|
|
17
17
|
test("tracker filters active tools, refreshes schemas, caches snapshots and resets", () => {
|
|
18
18
|
const handlers = new Map<string, Function>();
|
|
19
|
-
let active = ["
|
|
19
|
+
let active = ["sample_runner"];
|
|
20
20
|
let all = [tool];
|
|
21
21
|
const tracker = installContextEstimates({
|
|
22
22
|
on: (name: string, fn: Function) => handlers.set(name, fn),
|
|
@@ -3,7 +3,7 @@ import test from "node:test";
|
|
|
3
3
|
import { createReadTool } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { estimateContext, estimateProviderToolsTokens, installContextEstimates } from "./data.ts";
|
|
5
5
|
|
|
6
|
-
const tool = { name: "
|
|
6
|
+
const tool = { name: "sample_runner", description: "Execute TypeScript", parameters: createReadTool(process.cwd()).parameters };
|
|
7
7
|
const expected = Math.ceil(JSON.stringify(tool).length / 4);
|
|
8
8
|
const toolsCategory = (snapshot: ReturnType<typeof estimateContext>) => snapshot.categories.find(c => c.name === "Tools")!;
|
|
9
9
|
|
|
@@ -25,7 +25,7 @@ test("Tools counts schema JSON only and contributes to totals and percentages",
|
|
|
25
25
|
|
|
26
26
|
test("tracker filters active tools, refreshes dynamic schemas, caches reads and resets", () => {
|
|
27
27
|
const handlers = new Map<string, Function>();
|
|
28
|
-
let active = ["
|
|
28
|
+
let active = ["sample_runner", "sample_runner"];
|
|
29
29
|
let all = [tool, {...tool,name:"captured_read",description:"not exposed".repeat(100)}];
|
|
30
30
|
let reads = 0;
|
|
31
31
|
let fail = false;
|
|
@@ -44,7 +44,7 @@ test("tracker filters active tools, refreshes dynamic schemas, caches reads and
|
|
|
44
44
|
assert.ok(toolsCategory(tracker.getSnapshot()).tokens! > expected);
|
|
45
45
|
handlers.get("before_provider_request")!({payload:{tools:[tool]}},ctx);
|
|
46
46
|
assert.equal(toolsCategory(tracker.getSnapshot()).tokens, Math.ceil(JSON.stringify([tool]).length/4));
|
|
47
|
-
all = [{...tool,description:"changed"}]; active = ["
|
|
47
|
+
all = [{...tool,description:"changed"}]; active = ["sample_runner"];
|
|
48
48
|
handlers.get("turn_end")!({},ctx);
|
|
49
49
|
// The last provider payload remains authoritative through active-branch refresh.
|
|
50
50
|
assert.equal(toolsCategory(tracker.getSnapshot()).tokens, Math.ceil(JSON.stringify([tool]).length/4));
|
package/extensions/index.ts
CHANGED
|
@@ -8,11 +8,11 @@ import {
|
|
|
8
8
|
SettingsManager,
|
|
9
9
|
SessionManager,
|
|
10
10
|
type ExtensionAPI,
|
|
11
|
+
type ExtensionContext,
|
|
11
12
|
} from "@earendil-works/pi-coding-agent";
|
|
12
13
|
import { installContextEstimates } from "./ctx/data.ts";
|
|
13
14
|
import { createActivityTracker, type ActivityTracker } from "./ctx/activity.ts";
|
|
14
15
|
import { installFooter, type FooterController } from "./footer/index.ts";
|
|
15
|
-
import { isFabricFullCodeMode } from "./fabric-detection.ts";
|
|
16
16
|
import { adapterMcpServers, buildMcpStatusSnapshot, describeMcpStatus, type McpStatusSnapshot } from "./mcp/status-center.ts";
|
|
17
17
|
import {
|
|
18
18
|
createDefaultSidebarPanels,
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
import type { ResourceButtonId } from "./session/panel.ts";
|
|
22
22
|
import { installSidebar, type SidebarController } from "./sidebar/index.ts";
|
|
23
23
|
import { deleteSessionFile } from "./session/mutations.ts";
|
|
24
|
+
import { openSessionManager, readSessionPins, sessionsEligibleForClear, sortedSessions, toggleSessionPin } from "./session/manager.ts";
|
|
24
25
|
import type { SessionListItem } from "./session/panel.ts";
|
|
25
26
|
import { installToolViewController } from "./tool-view/controller.ts";
|
|
26
27
|
import type { ToolViewMode } from "./tool-view/types.ts";
|
|
@@ -33,6 +34,8 @@ const SESSION_SWITCH_COMMAND = "sidebar-session-switch";
|
|
|
33
34
|
const SESSION_RENAME_COMMAND = "sidebar-session-rename";
|
|
34
35
|
const SESSION_DELETE_COMMAND = "sidebar-session-delete";
|
|
35
36
|
const SESSION_CLEAR_COMMAND = "sidebar-session-clear-others";
|
|
37
|
+
const SESSION_MANAGER_COMMAND = "sessions-manager";
|
|
38
|
+
const SESSION_PINS_FILE = join(getAgentDir(), "pi-ui-extension", "session-pins.json");
|
|
36
39
|
|
|
37
40
|
export type ResourceEntry = { name: string; description: string; content?: string };
|
|
38
41
|
|
|
@@ -209,12 +212,8 @@ export async function loadResourceEntries(
|
|
|
209
212
|
*/
|
|
210
213
|
async function clearOtherSessions(
|
|
211
214
|
pi: ExtensionAPI,
|
|
212
|
-
|
|
213
|
-
sessionDir: string,
|
|
214
|
-
keepId: string,
|
|
215
|
+
targets: readonly { readonly path: string }[],
|
|
215
216
|
): Promise<{ readonly deleted: number; readonly failed: number; readonly firstError?: string }> {
|
|
216
|
-
const sessions = await SessionManager.list(cwd, sessionDir);
|
|
217
|
-
const targets = sessions.filter((session) => session.id !== keepId);
|
|
218
217
|
let deleted = 0;
|
|
219
218
|
let firstError: string | undefined;
|
|
220
219
|
for (const target of targets) {
|
|
@@ -265,6 +264,65 @@ export default async function piUiExtension(pi: ExtensionAPI): Promise<void> {
|
|
|
265
264
|
}
|
|
266
265
|
};
|
|
267
266
|
let switchRequested = false;
|
|
267
|
+
let sessionManagerOpen = false;
|
|
268
|
+
const safeReadPins = (): ReadonlySet<string> => {
|
|
269
|
+
try { return readSessionPins(SESSION_PINS_FILE); }
|
|
270
|
+
catch { return new Set(); } // Broken preferences must not hide saved sessions.
|
|
271
|
+
};
|
|
272
|
+
const loadSessionRows = async (ctx: ExtensionContext): Promise<readonly SessionListItem[]> => {
|
|
273
|
+
const currentId = ctx.sessionManager.getSessionId();
|
|
274
|
+
const pinned = safeReadPins();
|
|
275
|
+
const sessions = await SessionManager.list(ctx.cwd, ctx.sessionManager.getSessionDir());
|
|
276
|
+
return sortedSessions(sessions.map((session) => ({
|
|
277
|
+
id: session.id,
|
|
278
|
+
title: session.name ?? session.firstMessage,
|
|
279
|
+
modified: session.modified,
|
|
280
|
+
messageCount: session.messageCount,
|
|
281
|
+
current: session.id === currentId,
|
|
282
|
+
pinned: pinned.has(session.id),
|
|
283
|
+
})), "recent");
|
|
284
|
+
};
|
|
285
|
+
const canManageSessions = (ctx: ExtensionContext): boolean => {
|
|
286
|
+
if (ctx.isIdle()) return true;
|
|
287
|
+
ctx.ui.notify("Wait for the current run to finish before managing sessions", "warning");
|
|
288
|
+
return false;
|
|
289
|
+
};
|
|
290
|
+
const selectSession = (ctx: ExtensionContext, session: SessionListItem): void => {
|
|
291
|
+
if (session.current || switchRequested || !canManageSessions(ctx)) return;
|
|
292
|
+
switchRequested = true;
|
|
293
|
+
pi.sendUserMessage(`/${SESSION_SWITCH_COMMAND} ${session.id}`, { expandPromptTemplates: true });
|
|
294
|
+
};
|
|
295
|
+
const renameSession = (ctx: ExtensionContext, session: SessionListItem): void => {
|
|
296
|
+
if (!canManageSessions(ctx)) return;
|
|
297
|
+
pi.sendUserMessage(`/${SESSION_RENAME_COMMAND} ${session.id}`, { expandPromptTemplates: true });
|
|
298
|
+
};
|
|
299
|
+
const deleteSession = (ctx: ExtensionContext, session: SessionListItem): void => {
|
|
300
|
+
if (session.current) {
|
|
301
|
+
ctx.ui.notify("Cannot delete the currently active session", "warning");
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (!canManageSessions(ctx)) return;
|
|
305
|
+
pi.sendUserMessage(`/${SESSION_DELETE_COMMAND} ${session.id}`, { expandPromptTemplates: true });
|
|
306
|
+
};
|
|
307
|
+
const launchSessionManager = async (ctx: ExtensionContext): Promise<void> => {
|
|
308
|
+
if (sessionManagerOpen || !canManageSessions(ctx)) return;
|
|
309
|
+
sessionManagerOpen = true;
|
|
310
|
+
try {
|
|
311
|
+
await openSessionManager({
|
|
312
|
+
ui: ctx.ui,
|
|
313
|
+
loadSessions: () => loadSessionRows(ctx),
|
|
314
|
+
togglePin: (id) => toggleSessionPin(SESSION_PINS_FILE, id),
|
|
315
|
+
onPinChanged: () => refreshSessions(),
|
|
316
|
+
onSwitch: (session) => selectSession(ctx, session),
|
|
317
|
+
onRename: (session) => renameSession(ctx, session),
|
|
318
|
+
onDelete: (session) => deleteSession(ctx, session),
|
|
319
|
+
});
|
|
320
|
+
} catch (error) {
|
|
321
|
+
ctx.ui.notify(`Failed to open Sessions manager: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
322
|
+
} finally {
|
|
323
|
+
sessionManagerOpen = false;
|
|
324
|
+
}
|
|
325
|
+
};
|
|
268
326
|
const getMcpServerNames = () => mcpResources.map((server) => server.name);
|
|
269
327
|
const updateMcpStatus = (): void => {
|
|
270
328
|
mcpStatus = buildMcpStatusSnapshot(getAgentDir(), mcpCwd, mcpTrusted, registeredServers, liveMcpServers);
|
|
@@ -286,6 +344,11 @@ export default async function piUiExtension(pi: ExtensionAPI): Promise<void> {
|
|
|
286
344
|
updateMcpStatus();
|
|
287
345
|
});
|
|
288
346
|
|
|
347
|
+
pi.registerCommand(SESSION_MANAGER_COMMAND, {
|
|
348
|
+
description: "Search, sort, page, pin, switch, rename and delete saved sessions",
|
|
349
|
+
handler: async (_args, ctx) => launchSessionManager(ctx),
|
|
350
|
+
});
|
|
351
|
+
|
|
289
352
|
pi.registerCommand(SESSION_RENAME_COMMAND, {
|
|
290
353
|
description: "Rename a saved session selected from the sidebar",
|
|
291
354
|
handler: async (args, ctx) => {
|
|
@@ -358,23 +421,24 @@ export default async function piUiExtension(pi: ExtensionAPI): Promise<void> {
|
|
|
358
421
|
});
|
|
359
422
|
|
|
360
423
|
pi.registerCommand(SESSION_CLEAR_COMMAND, {
|
|
361
|
-
description: "Move
|
|
424
|
+
description: "Move unpinned saved sessions except the active one to trash",
|
|
362
425
|
handler: async (_args, ctx) => {
|
|
363
426
|
try {
|
|
364
427
|
const currentSessionId = ctx.sessionManager.getSessionId();
|
|
428
|
+
const pinnedIds = readSessionPins(SESSION_PINS_FILE);
|
|
365
429
|
const sessions = await SessionManager.list(ctx.cwd, ctx.sessionManager.getSessionDir());
|
|
366
|
-
const others = sessions
|
|
430
|
+
const others = sessionsEligibleForClear(sessions, currentSessionId, pinnedIds);
|
|
367
431
|
if (others.length === 0) {
|
|
368
|
-
ctx.ui.notify("No
|
|
432
|
+
ctx.ui.notify("No unpinned sessions to clear", "info");
|
|
369
433
|
return;
|
|
370
434
|
}
|
|
371
435
|
const confirmed = await ctx.ui.confirm(
|
|
372
436
|
"Clear other sessions",
|
|
373
|
-
`Move ${others.length}
|
|
437
|
+
`Move ${others.length} unpinned session(s) to trash? The active and pinned sessions are kept.`,
|
|
374
438
|
);
|
|
375
439
|
if (!confirmed) return;
|
|
376
440
|
|
|
377
|
-
const result = await clearOtherSessions(pi,
|
|
441
|
+
const result = await clearOtherSessions(pi, others);
|
|
378
442
|
refreshSessions();
|
|
379
443
|
if (result.failed > 0) {
|
|
380
444
|
ctx.ui.notify(
|
|
@@ -524,7 +588,6 @@ export default async function piUiExtension(pi: ExtensionAPI): Promise<void> {
|
|
|
524
588
|
toolUsage = createToolUsageTracker([], getMcpServerNames);
|
|
525
589
|
toolUsage.resetSession(branch);
|
|
526
590
|
if (ctx.mode !== "tui") return;
|
|
527
|
-
const showToolViewModeSelector = (): boolean => !isFabricFullCodeMode(pi.getActiveTools());
|
|
528
591
|
|
|
529
592
|
activity = createActivityTracker(ctx.cwd, refreshContextUi);
|
|
530
593
|
footer = installFooter(
|
|
@@ -543,45 +606,20 @@ export default async function piUiExtension(pi: ExtensionAPI): Promise<void> {
|
|
|
543
606
|
onSelectToolViewMode: (mode): void => {
|
|
544
607
|
void toolView.setMode(mode, ctx);
|
|
545
608
|
},
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
},
|
|
558
|
-
onSelectSession: (session): void => {
|
|
559
|
-
if (session.current || switchRequested) return;
|
|
560
|
-
if (!ctx.isIdle()) {
|
|
561
|
-
ctx.ui.notify("Wait for the current run to finish before switching sessions", "warning");
|
|
562
|
-
return;
|
|
563
|
-
}
|
|
564
|
-
switchRequested = true;
|
|
565
|
-
pi.sendUserMessage(`/${SESSION_SWITCH_COMMAND} ${session.id}`, { expandPromptTemplates: true });
|
|
566
|
-
},
|
|
567
|
-
onRenameSession: (session): void => {
|
|
568
|
-
if (!ctx.isIdle()) {
|
|
569
|
-
ctx.ui.notify("Wait for the current run to finish before renaming sessions", "warning");
|
|
570
|
-
return;
|
|
571
|
-
}
|
|
572
|
-
pi.sendUserMessage(`/${SESSION_RENAME_COMMAND} ${session.id}`, { expandPromptTemplates: true });
|
|
573
|
-
},
|
|
574
|
-
onDeleteSession: (session): void => {
|
|
575
|
-
if (session.current) {
|
|
576
|
-
ctx.ui.notify("Cannot delete the currently active session", "warning");
|
|
577
|
-
return;
|
|
578
|
-
}
|
|
579
|
-
if (!ctx.isIdle()) {
|
|
580
|
-
ctx.ui.notify("Wait for the current run to finish before deleting sessions", "warning");
|
|
581
|
-
return;
|
|
609
|
+
loadSessions: () => loadSessionRows(ctx),
|
|
610
|
+
onSelectSession: (session): void => selectSession(ctx, session),
|
|
611
|
+
onRenameSession: (session): void => renameSession(ctx, session),
|
|
612
|
+
onDeleteSession: (session): void => deleteSession(ctx, session),
|
|
613
|
+
onTogglePinSession: (session): void => {
|
|
614
|
+
try {
|
|
615
|
+
const pinned = toggleSessionPin(SESSION_PINS_FILE, session.id);
|
|
616
|
+
refreshSessions();
|
|
617
|
+
ctx.ui.notify(pinned ? "会话已置顶" : "已取消置顶", "info");
|
|
618
|
+
} catch (error) {
|
|
619
|
+
ctx.ui.notify(`保存置顶失败:${error instanceof Error ? error.message : String(error)}`, "error");
|
|
582
620
|
}
|
|
583
|
-
pi.sendUserMessage(`/${SESSION_DELETE_COMMAND} ${session.id}`, { expandPromptTemplates: true });
|
|
584
621
|
},
|
|
622
|
+
onOpenSessionManager: (): void => { void launchSessionManager(ctx); },
|
|
585
623
|
onDeleteOthers: (): void => {
|
|
586
624
|
if (!ctx.isIdle()) {
|
|
587
625
|
ctx.ui.notify("Wait for the current run to finish before clearing sessions", "warning");
|
|
@@ -20,7 +20,7 @@ test("entry wires early/late MCP status into restored and live usage without dou
|
|
|
20
20
|
};
|
|
21
21
|
const theme: any = { fg: (_: string, text: string) => text, bold: (text: string) => text };
|
|
22
22
|
const pi: any = {
|
|
23
|
-
registerTool() {}, registerCommand() {}, registerShortcut() {},
|
|
23
|
+
registerTool() { throw new Error("Tool View must not register tools"); }, registerToolRenderer() {}, registerCommand() {}, registerShortcut() {},
|
|
24
24
|
getCommands: () => [], getActiveTools: () => [], getAllTools: () => [], getThinkingLevel: () => "off",
|
|
25
25
|
getMcpServers: () => registrations,
|
|
26
26
|
on(name: string, fn: any) { handlers.set(name, [...(handlers.get(name) ?? []), fn]); },
|