pi-ui-extension 1.1.10 → 1.1.11

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
@@ -100,7 +100,22 @@ For tool definitions, the extension prefers the observable provider-request tool
100
100
 
101
101
  Modes can be changed directly with mouse clicks.
102
102
 
103
- When Fabric takes ownership of tool rendering, the panel remains visible for usage statistics but the local mode selector is hidden and replaced with `View controlled by Fabric`. This avoids conflicting display control.
103
+ 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.
104
+
105
+ For **Codemode** specifically:
106
+
107
+ - **Normal** retains Pi's native script, nested-call, and output rendering (including its own expand/collapse behavior).
108
+ - **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.
109
+ - **Hidden** removes the Codemode transcript row, including expanded and failed results, without stopping execution or removing Tool/MCP usage accounting.
110
+
111
+ Example Compact output:
112
+
113
+ ```text
114
+ ▸ codemode 8 行代码
115
+ ✓ 已完成 │ 1 次调用 · 1 成功 │ 59 行输出 · 85ms
116
+ ```
117
+
118
+ Changing the Tool View mode also updates previously rendered tool rows in the current session. Other third-party tools are unaffected.
104
119
 
105
120
  ### 6. Tool / MCP usage statistics
106
121
 
@@ -165,7 +180,7 @@ Example configuration:
165
180
  To pin a specific version:
166
181
 
167
182
  ```bash
168
- pi install npm:pi-ui-extension@1.1.10
183
+ pi install npm:pi-ui-extension@1.1.11
169
184
  ```
170
185
 
171
186
  or:
@@ -173,7 +188,7 @@ or:
173
188
  ```json
174
189
  {
175
190
  "packages": [
176
- "npm:pi-ui-extension@1.1.10"
191
+ "npm:pi-ui-extension@1.1.11"
177
192
  ]
178
193
  }
179
194
  ```
@@ -201,7 +216,7 @@ Configuration form:
201
216
  Pin to a Git tag:
202
217
 
203
218
  ```bash
204
- pi install git:github.com/1993yihuan/pi-ui-extension@v1.1.10
219
+ pi install git:github.com/1993yihuan/pi-ui-extension@v1.1.11
205
220
  ```
206
221
 
207
222
  ### Temporary local-source testing
@@ -300,7 +315,7 @@ npm run typecheck
300
315
  npm test
301
316
  ```
302
317
 
303
- The test suite covers the main behavior of Sidebar, CTX, Session management, Tool View, MCP resources, Fabric detection, mouse scrolling, and resize handling.
318
+ The test suite covers Sidebar, CTX, Session management, official Tool View renderers, MCP resources, mouse scrolling, and resize handling.
304
319
 
305
320
  During development, Pi can load the local source directory directly:
306
321
 
@@ -325,7 +340,6 @@ pi-ui-extension/
325
340
  │ ├── session/ # Session list and actions
326
341
  │ ├── sidebar/ # Sidebar layout, scrolling, resize
327
342
  │ ├── tool-view/ # Tool View modes and usage statistics
328
- │ ├── fabric-detection.ts # Fabric ownership detection
329
343
  │ └── index.ts # Pi Extension entry point
330
344
  ├── .github/workflows/
331
345
  │ ├── ci.yml # Automatic checks on push / PR
@@ -386,14 +400,14 @@ npm publishing uses GitHub Actions OIDC Trusted Publishing, so no long-lived `NP
386
400
 
387
401
  - Does not modify Pi Core.
388
402
  - `@earendil-works/pi-coding-agent` and `@earendil-works/pi-tui` are host-provided peer dependencies to avoid duplicate runtime instances.
389
- - Fabric ownership is detected explicitly to avoid conflicting Tool View controls.
403
+ - Tool View changes only how built-in tool calls are rendered, using Pi's public renderer resolver rather than overriding their implementations.
390
404
  - Session and resource operations use Pi public APIs wherever possible.
391
405
  - Context composition is clearly presented as an estimate, not provider-exact token accounting.
392
406
  - Fullscreen sidebar behavior degrades safely when fullscreen layout is unavailable.
393
407
 
394
408
  ## Requirements
395
409
 
396
- - Pi Coding Agent
410
+ - Pi Coding Agent with the official `registerToolRenderer()` API (tested with `@earendil-works/pi-coding-agent@1.1.0`)
397
411
  - Node.js `>= 22.19.0` (use a Node version supported by your Pi installation)
398
412
  - CI validates Node.js 22.19.0 and 24; releases use Node.js 24
399
413
 
package/README.zh-CN.md CHANGED
@@ -100,7 +100,22 @@ Composition 会将当前可观测上下文拆分为:
100
100
 
101
101
  可以直接使用鼠标点击模式进行切换。
102
102
 
103
- 当 Fabric 接管工具展示行为时,扩展会保留 Tool View 面板和调用统计,但不再显示本地模式选择器,并提示 `View controlled by Fabric`,避免与 Fabric 的展示逻辑互相抢占。
103
+ Tool View 使用 Pi 官方的 `pi.registerToolRenderer()` 接口管理内置工具(`read`、`bash`、`edit`、`write`、`grep`、`find` 和 `ls`)**以及内置 `codemode` 工具**的展示,不再重注册工具定义或接管工具执行逻辑。其他 Tool 保留其自身的渲染方式。
104
+
105
+ 对于 **Codemode**:
106
+
107
+ - **Normal**:保留 Pi 原生的脚本、子工具调用、输出详情,以及原生展开 / 收起行为。
108
+ - **Compact**:采用带有状态颜色的双行摘要。第一行突出 Codemode 和代码行数;第二行将状态、子调用统计、输出规模与耗时分组展示,并统一使用中文单位。即使展开或执行失败,也不会展示完整代码、子调用参数或真实输出;终端宽度不足时优先保留状态、失败信息和调用次数,避免摘要自动换成多行。
109
+ - **Hidden**:隐藏 Codemode 工具记录(包括展开和失败结果),但不影响实际执行或 Tool / MCP 调用统计。
110
+
111
+ Compact 效果示例:
112
+
113
+ ```text
114
+ ▸ codemode 8 行代码
115
+ ✓ 已完成 │ 1 次调用 · 1 成功 │ 59 行输出 · 85ms
116
+ ```
117
+
118
+ 切换 Tool View 模式也会更新当前会话中已经展示的历史工具记录;其他第三方工具保持原有渲染方式。
104
119
 
105
120
  ### 6. Tool / MCP 调用统计
106
121
 
@@ -165,7 +180,7 @@ Pi 会安装 package,并将声明写入个人配置:
165
180
  如果希望固定版本:
166
181
 
167
182
  ```bash
168
- pi install npm:pi-ui-extension@1.1.10
183
+ pi install npm:pi-ui-extension@1.1.11
169
184
  ```
170
185
 
171
186
  或者:
@@ -173,7 +188,7 @@ pi install npm:pi-ui-extension@1.1.10
173
188
  ```json
174
189
  {
175
190
  "packages": [
176
- "npm:pi-ui-extension@1.1.10"
191
+ "npm:pi-ui-extension@1.1.11"
177
192
  ]
178
193
  }
179
194
  ```
@@ -201,7 +216,7 @@ pi install git:github.com/1993yihuan/pi-ui-extension
201
216
  固定到 Git tag:
202
217
 
203
218
  ```bash
204
- pi install git:github.com/1993yihuan/pi-ui-extension@v1.1.10
219
+ pi install git:github.com/1993yihuan/pi-ui-extension@v1.1.11
205
220
  ```
206
221
 
207
222
  ### 临时加载本地源码
@@ -300,7 +315,7 @@ npm run typecheck
300
315
  npm test
301
316
  ```
302
317
 
303
- 当前自动化测试覆盖 Sidebar、CTX、Session、Tool View、MCP resource、Fabric detection、鼠标滚轮 / resize 等主要行为。
318
+ 当前自动化测试覆盖 Sidebar、CTX、Session、官方 Tool Renderer、MCP resource、鼠标滚轮 / resize 等主要行为。
304
319
 
305
320
  开发时可以直接让 Pi 加载本地目录:
306
321
 
@@ -325,7 +340,6 @@ pi-ui-extension/
325
340
  │ ├── session/ # Session 列表与操作
326
341
  │ ├── sidebar/ # Sidebar 布局、滚动和 resize
327
342
  │ ├── tool-view/ # Tool View 模式与调用统计
328
- │ ├── fabric-detection.ts # Fabric 工具接管检测
329
343
  │ └── index.ts # Pi Extension 入口
330
344
  ├── .github/workflows/
331
345
  │ ├── ci.yml # push / PR 自动检查
@@ -386,14 +400,14 @@ npm 发布使用 GitHub Actions OIDC Trusted Publishing,不需要在 GitHub Re
386
400
 
387
401
  - 不修改 Pi Core。
388
402
  - Pi host 提供的 `@earendil-works/pi-coding-agent` 和 `@earendil-works/pi-tui` 使用 `peerDependencies`,避免产生重复运行时实例。
389
- - 对 Fabric 做显式检测,避免 Tool View 模式控制发生冲突。
403
+ - Tool View 只通过 Pi 官方 Renderer Resolver 控制内置工具的显示,不覆写工具定义或执行逻辑。
390
404
  - Session 与资源操作尽量通过 Pi 已公开 API 完成。
391
405
  - Context composition 明确标记为估算值,不冒充 provider 精确 token 统计。
392
406
  - fullscreen Sidebar 不可用时安全降级,不影响 Pi 主流程。
393
407
 
394
408
  ## Requirements
395
409
 
396
- - Pi Coding Agent
410
+ - 支持官方 `registerToolRenderer()` API 的 Pi Coding Agent(已使用 `@earendil-works/pi-coding-agent@1.1.0` 验证)
397
411
  - Node.js `>= 22.19.0`(建议使用当前 Pi 支持的 Node 版本)
398
412
  - CI 覆盖 Node.js 22.19.0 和 24;发布工作流使用 Node.js 24
399
413
 
@@ -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;Fabric 捕获但未直接启用的工具不重复计入。无启用工具为 0;API 不可用、启用工具缺少定义或 schema 无法序列化时保持 unknown,避免显示不完整合计。工具定义在每次现有快照事件刷新时重新读取(包括动态启用/禁用),不在 render 中读取;空闲时注册工具将在下一次快照事件反映。
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 按当前快照工具结果文本估算。Fabric 外层 content 只计一次,不遍历内部操作;不读 details、全量 JSONL、沙箱、截断前原始输出或被压缩结果。已返回 content 内的截断提示本身仍属于可发送文本。
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
- 未修改 pi core、Fabric 或全局配置。全屏鼠标/极短终端还应在用户真实终端手工确认。
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: "fabric_exec", toolCallId: String(n), content: [{ type: "text", text: "x".repeat(n * 4) }], details: { fullOutput: "x".repeat(100000), operations: [{ toolName: "read" }] } });
7
- const tool = { name: "fabric_exec", description: "Execute TypeScript", parameters: createReadTool(process.cwd()).parameters };
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 = ["fabric_exec"];
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: "fabric_exec", description: "Execute TypeScript", parameters: createReadTool(process.cwd()).parameters };
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 = ["fabric_exec", "fabric_exec"];
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 = ["fabric_exec"];
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));
@@ -12,7 +12,6 @@ import {
12
12
  import { installContextEstimates } from "./ctx/data.ts";
13
13
  import { createActivityTracker, type ActivityTracker } from "./ctx/activity.ts";
14
14
  import { installFooter, type FooterController } from "./footer/index.ts";
15
- import { isFabricFullCodeMode } from "./fabric-detection.ts";
16
15
  import { adapterMcpServers, buildMcpStatusSnapshot, describeMcpStatus, type McpStatusSnapshot } from "./mcp/status-center.ts";
17
16
  import {
18
17
  createDefaultSidebarPanels,
@@ -524,7 +523,6 @@ export default async function piUiExtension(pi: ExtensionAPI): Promise<void> {
524
523
  toolUsage = createToolUsageTracker([], getMcpServerNames);
525
524
  toolUsage.resetSession(branch);
526
525
  if (ctx.mode !== "tui") return;
527
- const showToolViewModeSelector = (): boolean => !isFabricFullCodeMode(pi.getActiveTools());
528
526
 
529
527
  activity = createActivityTracker(ctx.cwd, refreshContextUi);
530
528
  footer = installFooter(
@@ -543,7 +541,6 @@ export default async function piUiExtension(pi: ExtensionAPI): Promise<void> {
543
541
  onSelectToolViewMode: (mode): void => {
544
542
  void toolView.setMode(mode, ctx);
545
543
  },
546
- showToolViewModeSelector,
547
544
  loadSessions: async (): Promise<readonly SessionListItem[]> => {
548
545
  const currentSessionId = ctx.sessionManager.getSessionId();
549
546
  const sessions = await SessionManager.list(ctx.cwd, ctx.sessionManager.getSessionDir());
@@ -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]); },
@@ -4,7 +4,7 @@ import { createDefaultSidebarPanels, createToolViewScrollView, SESSION_PANEL_ID,
4
4
  import { renderLayoutFrame } from "@earendil-works/pi-tui/dist/layout.js";
5
5
  import { ToolViewPanel } from "../tool-view/panel.ts";
6
6
 
7
- function options(showToolViewModeSelector?: boolean): Parameters<typeof createDefaultSidebarPanels>[0] {
7
+ function options(): Parameters<typeof createDefaultSidebarPanels>[0] {
8
8
  return {
9
9
  getContextUsage: () => undefined,
10
10
  getModel: () => "test-model",
@@ -12,7 +12,6 @@ function options(showToolViewModeSelector?: boolean): Parameters<typeof createDe
12
12
  getToolViewMode: () => "compact",
13
13
  getToolUsage: () => undefined,
14
14
  onSelectToolViewMode() {},
15
- showToolViewModeSelector: () => showToolViewModeSelector ?? true,
16
15
  loadSessions: async () => [],
17
16
  onSelectSession() {},
18
17
  onRenameSession() {},
@@ -23,12 +22,12 @@ function options(showToolViewModeSelector?: boolean): Parameters<typeof createDe
23
22
  }
24
23
 
25
24
  test("registers sessions without removed standalone panels", () => {
26
- const ids = createDefaultSidebarPanels(options(true)).map((panel) => panel.id);
25
+ const ids = createDefaultSidebarPanels(options()).map((panel) => panel.id);
27
26
  assert.deepEqual(ids, [SESSION_PANEL_ID, "context", TOOL_VIEW_PANEL_ID]);
28
27
  assert.ok(!ids.includes("activity"));
29
28
  assert.ok(ids.includes("context"));
30
- const context = createDefaultSidebarPanels(options(true)).find(panel => panel.id === "context")!;
31
- const sessions = createDefaultSidebarPanels(options(true)).find(panel => panel.id === SESSION_PANEL_ID)!;
29
+ const context = createDefaultSidebarPanels(options()).find(panel => panel.id === "context")!;
30
+ const sessions = createDefaultSidebarPanels(options()).find(panel => panel.id === SESSION_PANEL_ID)!;
32
31
  assert.equal(sessions.layout?.maxSize, 9);
33
32
  assert.equal(context.layout?.minSize, 10);
34
33
  assert.ok(!ids.includes("resources"));
@@ -36,15 +35,11 @@ test("registers sessions without removed standalone panels", () => {
36
35
  });
37
36
 
38
37
 
39
- test("always registers Tool View without Fabric", () => {
40
- const ids = createDefaultSidebarPanels(options(true)).map((panel) => panel.id);
38
+ test("always registers Tool View", () => {
39
+ const ids = createDefaultSidebarPanels(options()).map((panel) => panel.id);
41
40
  assert.ok(ids.includes(TOOL_VIEW_PANEL_ID));
42
41
  });
43
42
 
44
- test("keeps Tool View registered when Fabric hides only its selector", () => {
45
- const ids = createDefaultSidebarPanels(options(false)).map((panel) => panel.id);
46
- assert.ok(ids.includes(TOOL_VIEW_PANEL_ID));
47
- });
48
43
 
49
44
  test("keeps Tool View anchored at the top when usage rows overflow", () => {
50
45
  const tui = { addInputListener: () => () => {}, requestRender() {} } as any;
@@ -65,7 +65,6 @@ export type DefaultSidebarPanelOptions = {
65
65
  readonly getToolViewMode: () => ToolViewMode | undefined;
66
66
  readonly getToolUsage: () => ToolUsageSnapshot | undefined;
67
67
  readonly onSelectToolViewMode: (mode: ToolViewMode) => void;
68
- readonly showToolViewModeSelector?: () => boolean;
69
68
  readonly loadSessions: () => Promise<readonly SessionListItem[]>;
70
69
  readonly onSelectSession: (session: SessionListItem) => void;
71
70
  readonly onRenameSession: (session: SessionListItem) => void;
@@ -142,7 +141,6 @@ export function createDefaultSidebarPanels(
142
141
  getMode: options.getToolViewMode,
143
142
  getUsage: options.getToolUsage,
144
143
  onSelect: options.onSelectToolViewMode,
145
- showModeSelector: options.showToolViewModeSelector,
146
144
  showBottomBorder: false,
147
145
  });
148
146
  const scroll = createToolViewScrollView(panel, theme);
@@ -0,0 +1,135 @@
1
+ import type {
2
+ AgentToolResult,
3
+ Theme,
4
+ ToolRenderers,
5
+ ToolRenderResultOptions,
6
+ } from "@earendil-works/pi-coding-agent";
7
+ import { truncateToWidth, visibleWidth, type Component } from "@earendil-works/pi-tui";
8
+
9
+ type RenderContext = Parameters<NonNullable<ToolRenderers["renderCall"]>>[2];
10
+ type MetricColor = "text" | "success" | "error" | "warning" | "muted";
11
+ type Metric = {
12
+ label: string;
13
+ color: MetricColor;
14
+ group: "calls" | "output";
15
+ /** Lower priority number survives narrower terminals first. */
16
+ priority: number;
17
+ };
18
+
19
+ const CALL_SEPARATOR = " │ ";
20
+ const METRIC_SEPARATOR = " · ";
21
+
22
+ /** One terminal row regardless of viewport width; no wrapping or padding. */
23
+ function singleLine(line: string): Component {
24
+ return {
25
+ render(width: number): string[] {
26
+ return width > 0 ? [truncateToWidth(line, width, "…")] : [];
27
+ },
28
+ invalidate() {},
29
+ };
30
+ }
31
+
32
+ function formatDuration(durationMs: unknown): string | undefined {
33
+ if (typeof durationMs !== "number" || !Number.isFinite(durationMs) || durationMs < 0) return undefined;
34
+ return durationMs < 1000 ? `${Math.round(durationMs)}ms` : `${(durationMs / 1000).toFixed(1)}s`;
35
+ }
36
+
37
+ function countContent(result: AgentToolResult<any>): { lines: number; images: number } {
38
+ let lines = 0;
39
+ let images = 0;
40
+ for (const block of result.content) {
41
+ if (block.type === "image") images++;
42
+ else if (block.type === "text" &&
43
+ !/^Script (completed|failed)\nWall time [\d.]+ seconds\nOutput:\n$/.test(block.text)) {
44
+ lines += block.text.split("\n").filter((line) => line.trim().length > 0).length;
45
+ }
46
+ }
47
+ return { lines, images };
48
+ }
49
+
50
+ function codeLineCount(args: unknown): string {
51
+ const code = args !== null && typeof args === "object" && "code" in args
52
+ ? (args as { code?: unknown }).code : undefined;
53
+ if (typeof code !== "string") return "代码行数未知";
54
+ return `${code ? code.split("\n").length : 0} 行代码`;
55
+ }
56
+
57
+ export function renderCodemodeCompactCall(args: unknown, theme: Theme): Component {
58
+ const label = theme.fg("accent", "▸ ") +
59
+ theme.fg("toolTitle", theme.bold("codemode")) +
60
+ theme.fg("dim", ` ${codeLineCount(args)}`);
61
+ return singleLine(label);
62
+ }
63
+
64
+ function formatSummary(
65
+ theme: Theme,
66
+ state: string,
67
+ stateColor: "success" | "error" | "warning",
68
+ metrics: readonly Metric[],
69
+ width: number,
70
+ ): string {
71
+ const heading = theme.fg(stateColor, state);
72
+ if (width <= visibleWidth(heading)) return truncateToWidth(heading, width, "…");
73
+
74
+ // Work out which complete metrics can be shown, then restore display order.
75
+ // Never show half a metric or let Text wrap the row across multiple lines.
76
+ const selected = new Set<Metric>();
77
+ const ranked = metrics.slice().sort((a, b) => a.priority - b.priority);
78
+ const compose = (): string => {
79
+ let row = heading;
80
+ let lastGroup: Metric["group"] | undefined;
81
+ for (const metric of metrics) {
82
+ if (!selected.has(metric)) continue;
83
+ const separator = lastGroup && lastGroup === metric.group ? METRIC_SEPARATOR : CALL_SEPARATOR;
84
+ row += theme.fg("dim", separator) + theme.fg(metric.color, metric.label);
85
+ lastGroup = metric.group;
86
+ }
87
+ return row;
88
+ };
89
+ for (const metric of ranked) {
90
+ selected.add(metric);
91
+ if (visibleWidth(compose()) > width) selected.delete(metric);
92
+ }
93
+ return compose();
94
+ }
95
+
96
+ export function renderCodemodeCompactResult(
97
+ result: AgentToolResult<any>,
98
+ theme: Theme,
99
+ context: RenderContext,
100
+ options: ToolRenderResultOptions,
101
+ ): Component {
102
+ const status = options.isPartial ? "… 运行中" : context.isError ? "✗ 失败" : "✓ 已完成";
103
+ const stateColor = options.isPartial ? "warning" : context.isError ? "error" : "success";
104
+ const rawCalls: unknown = result.details?.calls;
105
+ const calls: readonly unknown[] = Array.isArray(rawCalls) ? rawCalls : [];
106
+ const statuses = { ok: 0, error: 0, cancelled: 0, running: 0 };
107
+ for (const call of calls) {
108
+ if (!call || typeof call !== "object") continue;
109
+ const state: unknown = (call as { status?: unknown }).status;
110
+ if (state === "ok" || state === "error" || state === "cancelled" || state === "running") {
111
+ statuses[state]++;
112
+ }
113
+ }
114
+ const metrics: Metric[] = [];
115
+ if (calls.length > 0) metrics.push({ label: `${calls.length} 次调用`, color: "text", group: "calls", priority: 1 });
116
+ if (statuses.ok) metrics.push({ label: `${statuses.ok} 成功`, color: "success", group: "calls", priority: 5 });
117
+ if (statuses.error) metrics.push({ label: `${statuses.error} 失败`, color: "error", group: "calls", priority: 2 });
118
+ if (statuses.cancelled) metrics.push({ label: `${statuses.cancelled} 取消`, color: "muted", group: "calls", priority: 3 });
119
+ if (statuses.running) metrics.push({ label: `${statuses.running} 进行中`, color: "warning", group: "calls", priority: 3 });
120
+ if (!options.isPartial) {
121
+ const { lines, images } = countContent(result);
122
+ if (lines) metrics.push({ label: `${lines} 行输出`, color: "muted", group: "output", priority: 6 });
123
+ if (images) metrics.push({ label: `${images} 张图片`, color: "muted", group: "output", priority: 7 });
124
+ }
125
+ const duration = formatDuration(context.durationMs);
126
+ if (duration) metrics.push({ label: duration, color: "muted", group: "output", priority: 4 });
127
+
128
+ // All necessary data is captured by the component, with only width varying on render.
129
+ return {
130
+ render(width: number): string[] {
131
+ return width > 0 ? [formatSummary(theme, status, stateColor, metrics, width)] : [];
132
+ },
133
+ invalidate() {},
134
+ };
135
+ }