@zhushanwen/pi-session-manager 0.3.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -5
- package/src/__tests__/tool-summary.test.ts +169 -0
- package/src/index.ts +62 -3
- package/src/render.ts +22 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-session-manager",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Pi extension for managing agent-managed sessions — create, send, read history, list, status, abort via ctx.ui.select channel.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -30,11 +30,11 @@
|
|
|
30
30
|
"README.md"
|
|
31
31
|
],
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@zhushanwen/
|
|
33
|
+
"@zhushanwen/extension-protocol": "0.21.0",
|
|
34
|
+
"@zhushanwen/pi-ext-guards": "0.5.0",
|
|
34
35
|
"@zhushanwen/pi-extension-logger": "0.6.2",
|
|
35
|
-
"@zhushanwen/pi-notify-ledger-host": "0.3.
|
|
36
|
-
"@zhushanwen/subagent-core": "1.5.
|
|
37
|
-
"@zhushanwen/extension-protocol": "0.20.0"
|
|
36
|
+
"@zhushanwen/pi-notify-ledger-host": "0.3.5",
|
|
37
|
+
"@zhushanwen/subagent-core": "1.5.3"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@types/node": "^24.0.0",
|
|
@@ -43,12 +43,16 @@
|
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"@earendil-works/pi-coding-agent": "*",
|
|
46
|
+
"@earendil-works/pi-tui": "^1.0.0",
|
|
46
47
|
"typebox": "1.3.27"
|
|
47
48
|
},
|
|
48
49
|
"peerDependenciesMeta": {
|
|
49
50
|
"@earendil-works/pi-coding-agent": {
|
|
50
51
|
"optional": true
|
|
51
52
|
},
|
|
53
|
+
"@earendil-works/pi-tui": {
|
|
54
|
+
"optional": true
|
|
55
|
+
},
|
|
52
56
|
"typebox": {
|
|
53
57
|
"optional": true
|
|
54
58
|
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// tool-summary.test.ts — details.summary 产出(六工具文案断言,走 execute 骨架)+
|
|
2
|
+
// renderSessionManagerResult 两态(折叠态一行摘要 / 展开态原始 content 文本)。
|
|
3
|
+
|
|
4
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
6
|
+
import registerExtension from "../index.ts";
|
|
7
|
+
import { renderSessionManagerResult } from "../render.ts";
|
|
8
|
+
|
|
9
|
+
/** Capture registered tools and provide a mock ctx.ui.select. */
|
|
10
|
+
function createHarness() {
|
|
11
|
+
const registered: Array<{ name: string; execute: Function; renderResult?: Function }> = [];
|
|
12
|
+
const selectMock = vi.fn().mockResolvedValue('{"ok":true}');
|
|
13
|
+
const pi = {
|
|
14
|
+
registerTool: (tool: { name: string; execute: Function; renderResult?: Function }) => registered.push(tool),
|
|
15
|
+
on: vi.fn(),
|
|
16
|
+
getAllTools: vi.fn(() => []),
|
|
17
|
+
setActiveTools: vi.fn(),
|
|
18
|
+
};
|
|
19
|
+
const ctx = {
|
|
20
|
+
mode: "rpc" as const,
|
|
21
|
+
hasUI: true,
|
|
22
|
+
ui: { select: selectMock },
|
|
23
|
+
};
|
|
24
|
+
registerExtension(pi as never);
|
|
25
|
+
return { registered, selectMock, ctx };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// mock theme(fg 直通,便于断言纯文本;todo render.test.ts 同款)
|
|
29
|
+
const mockTheme = {
|
|
30
|
+
fg: (_color: string, text: string) => text,
|
|
31
|
+
bg: (_color: string, text: string) => text,
|
|
32
|
+
bold: (text: string) => text,
|
|
33
|
+
} as unknown as Theme;
|
|
34
|
+
|
|
35
|
+
function renderedText(result: unknown, expanded: boolean): string {
|
|
36
|
+
return renderSessionManagerResult(result, { expanded }, mockTheme).render(400).join("\n");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("execute 组装 details.summary(六工具文案)", () => {
|
|
40
|
+
let harness: ReturnType<typeof createHarness>;
|
|
41
|
+
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
harness = createHarness();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
async function summaryOf(toolName: string, params: Record<string, unknown>, reply: unknown): Promise<string> {
|
|
47
|
+
harness.selectMock.mockResolvedValue(JSON.stringify(reply));
|
|
48
|
+
const tool = harness.registered.find((t) => t.name === toolName)!;
|
|
49
|
+
const result = (await tool.execute("call-1", params, undefined, undefined, harness.ctx)) as {
|
|
50
|
+
details: { summary: string };
|
|
51
|
+
};
|
|
52
|
+
return result.details.summary;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
it("create_managed_session:带 label → 引号 label;无 label → 回退 sessionId", async () => {
|
|
56
|
+
expect(
|
|
57
|
+
await summaryOf("create_managed_session", { cwd: "/work", label: "worker" }, { sessionId: "s1", status: "created" }),
|
|
58
|
+
).toBe('Created session "worker"');
|
|
59
|
+
expect(
|
|
60
|
+
await summaryOf("create_managed_session", { cwd: "/work" }, { sessionId: "s1", status: "created" }),
|
|
61
|
+
).toBe("Created session s1");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("send_to_session:Sent message to <sessionId>", async () => {
|
|
65
|
+
expect(await summaryOf("send_to_session", { sessionId: "s1", prompt: "hi" }, { queued: true, willNotify: true })).toBe(
|
|
66
|
+
"Sent message to s1",
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("read_session_history:messages 计数后缀", async () => {
|
|
71
|
+
expect(
|
|
72
|
+
await summaryOf("read_session_history", { sessionId: "s1" }, { messages: [{}, {}, {}], truncated: false }),
|
|
73
|
+
).toBe("Read history of s1 · 3 entries");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("list_my_sessions:sessions 计数", async () => {
|
|
77
|
+
expect(await summaryOf("list_my_sessions", {}, { sessions: [{ id: "s1" }, { id: "s2" }] })).toBe(
|
|
78
|
+
"Listed 2 managed sessions",
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("get_session_status:<sessionId> · <status>", async () => {
|
|
83
|
+
expect(await summaryOf("get_session_status", { sessionId: "s1" }, { status: "idle", undeliveredResults: 0 })).toBe(
|
|
84
|
+
"s1 · idle",
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("abort_session:Aborted <sessionId>", async () => {
|
|
89
|
+
expect(await summaryOf("abort_session", { sessionId: "s1" }, { success: true })).toBe("Aborted s1");
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("toSummary 数据缺失分支(形状漂移退化为短文案,不抛错)", () => {
|
|
94
|
+
let harness: ReturnType<typeof createHarness>;
|
|
95
|
+
|
|
96
|
+
beforeEach(() => {
|
|
97
|
+
harness = createHarness();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
async function summaryOf(toolName: string, params: Record<string, unknown>, reply: unknown): Promise<string> {
|
|
101
|
+
harness.selectMock.mockResolvedValue(JSON.stringify(reply));
|
|
102
|
+
const tool = harness.registered.find((t) => t.name === toolName)!;
|
|
103
|
+
const result = (await tool.execute("call-1", params, undefined, undefined, harness.ctx)) as {
|
|
104
|
+
details: { summary: string };
|
|
105
|
+
};
|
|
106
|
+
return result.details.summary;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
it("create_managed_session:label 与 sessionId 均缺 → 裸文案", async () => {
|
|
110
|
+
expect(await summaryOf("create_managed_session", { cwd: "/work" }, {})).toBe("Created session");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("read_session_history:messages 缺失/非数组 → 无计数后缀", async () => {
|
|
114
|
+
expect(await summaryOf("read_session_history", { sessionId: "s1" }, { truncated: false })).toBe("Read history of s1");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("list_my_sessions:sessions 非数组 → 无计数", async () => {
|
|
118
|
+
expect(await summaryOf("list_my_sessions", {}, { sessions: "bad" })).toBe("Listed managed sessions");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("get_session_status:status 缺失 → Status of <sessionId>", async () => {
|
|
122
|
+
expect(await summaryOf("get_session_status", { sessionId: "s1" }, {})).toBe("Status of s1");
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("renderSessionManagerResult 两态", () => {
|
|
127
|
+
it("折叠态(expanded=false):渲染 details.summary 一行摘要,不含原始 JSON", () => {
|
|
128
|
+
const result = {
|
|
129
|
+
content: [{ type: "text", text: '{"messages":[{}]}' }],
|
|
130
|
+
details: { summary: "Read history of s1 · 1 entries" },
|
|
131
|
+
};
|
|
132
|
+
const out = renderedText(result, false);
|
|
133
|
+
expect(out).toContain("Read history of s1 · 1 entries");
|
|
134
|
+
expect(out).not.toContain("messages");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("展开态(expanded=true):渲染 content[0] 原始文本,不含摘要", () => {
|
|
138
|
+
const result = {
|
|
139
|
+
content: [{ type: "text", text: '{"messages":[{}]}' }],
|
|
140
|
+
details: { summary: "Read history of s1 · 1 entries" },
|
|
141
|
+
};
|
|
142
|
+
const out = renderedText(result, true);
|
|
143
|
+
expect(out).toContain('{"messages":[{}]}');
|
|
144
|
+
expect(out).not.toContain("Read history of s1");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("折叠态但 details 缺失 / summary 非字符串 → 回退 content 文本", () => {
|
|
148
|
+
expect(renderedText({ content: [{ type: "text", text: "raw" }] }, false)).toContain("raw");
|
|
149
|
+
expect(renderedText({ content: [{ type: "text", text: "raw" }], details: { summary: 7 } }, false)).toContain("raw");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("六工具注册的 renderResult 均存在且折叠态行为一致(共用渲染内核)", () => {
|
|
153
|
+
const harness = createHarness();
|
|
154
|
+
const renderFns = harness.registered.map((t) => t.renderResult);
|
|
155
|
+
expect(renderFns).toHaveLength(6);
|
|
156
|
+
for (const fn of renderFns) expect(typeof fn).toBe("function");
|
|
157
|
+
// 抽查一个 handler:折叠态出摘要、不出原始 JSON(handler 是各 registerTool
|
|
158
|
+
// 的方法包装,内核同为 renderSessionManagerResult)
|
|
159
|
+
const out = (renderFns[0] as (r: unknown, o: { expanded: boolean }, t: unknown) => { render: (w: number) => string[] })(
|
|
160
|
+
{ content: [{ type: "text", text: "raw" }], details: { summary: "Aborted s1" } },
|
|
161
|
+
{ expanded: false },
|
|
162
|
+
mockTheme,
|
|
163
|
+
)
|
|
164
|
+
.render(400)
|
|
165
|
+
.join("\n");
|
|
166
|
+
expect(out).toContain("Aborted s1");
|
|
167
|
+
expect(out).not.toContain("raw");
|
|
168
|
+
});
|
|
169
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
ExtensionToolContext,
|
|
8
8
|
SessionCompactEvent,
|
|
9
9
|
SessionStartEvent,
|
|
10
|
+
Theme,
|
|
10
11
|
} from "@earendil-works/pi-coding-agent";
|
|
11
12
|
import {
|
|
12
13
|
SESSION_MANAGER_MARKER,
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
import { getLogger, setPiHandle } from "@zhushanwen/pi-extension-logger";
|
|
20
21
|
import { Type, type Static, type TObject } from "typebox";
|
|
21
22
|
|
|
23
|
+
import { renderSessionManagerResult } from "./render.ts";
|
|
22
24
|
import { ensureLedgerBound, runLedgerCompactionCheck } from "./notify-ledger.ts";
|
|
23
25
|
import { createWatchCoordinator } from "./watch-coordinator.ts";
|
|
24
26
|
|
|
@@ -106,6 +108,18 @@ function asResultRecord(v: unknown): Record<string, unknown> | undefined {
|
|
|
106
108
|
: undefined;
|
|
107
109
|
}
|
|
108
110
|
|
|
111
|
+
/** toSummary 的字段守卫:非空 string 才采信(缺字段 = 摘要省略对应段,不抛错——
|
|
112
|
+
* 摘要只是展示辅助,不得反过来影响工具成功路径)。 */
|
|
113
|
+
function stringField(rec: Record<string, unknown> | undefined, key: string): string | undefined {
|
|
114
|
+
const v = rec?.[key];
|
|
115
|
+
return typeof v === "string" && v !== "" ? v : undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function arrayField(rec: Record<string, unknown> | undefined, key: string): unknown[] | undefined {
|
|
119
|
+
const v = rec?.[key];
|
|
120
|
+
return Array.isArray(v) ? v : undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
109
123
|
/**
|
|
110
124
|
* 统一的 execute 包装:调用 select 通道并解析结果。
|
|
111
125
|
* 返回标准 AgentToolResult 形状;select 取消/超时/异常/非 JSON 回包是错误路径,
|
|
@@ -122,7 +136,8 @@ async function executeTool(
|
|
|
122
136
|
action: SessionManagerAction,
|
|
123
137
|
params: Record<string, unknown>,
|
|
124
138
|
onResult?: (ctx: ExtensionContext, params: Record<string, unknown>, result: unknown) => void,
|
|
125
|
-
|
|
139
|
+
toSummary?: (params: Record<string, unknown>, result: unknown) => string,
|
|
140
|
+
): Promise<{ content: Array<{ type: "text"; text: string }>; details: { summary: string } | undefined }> {
|
|
126
141
|
const result = await callSessionManager(ctx, action, params);
|
|
127
142
|
if (!result.ok) {
|
|
128
143
|
// 行为微变①(D8,有意):非 JSON 回包从「catch 后
|
|
@@ -147,9 +162,11 @@ async function executeTool(
|
|
|
147
162
|
// 成功路径副作用编排(notify-once U4):send/create 的 willNotify arm(register + 开表)、
|
|
148
163
|
// label 缓存回填(list)——错误路径已 throw,天然不触发。
|
|
149
164
|
onResult?.(ctx, params, parsed);
|
|
165
|
+
// details.summary 一行摘要(工具收起态展示):六工具共用本骨架统一组装,
|
|
166
|
+
// 文案由各工具 cfg.toSummary 从协议 params + 回包产出(未配置 = details 缺省)。
|
|
150
167
|
return {
|
|
151
168
|
content: [{ type: "text" as const, text: raw }],
|
|
152
|
-
details: undefined,
|
|
169
|
+
details: toSummary ? { summary: toSummary(params, parsed) } : undefined,
|
|
153
170
|
};
|
|
154
171
|
}
|
|
155
172
|
|
|
@@ -166,6 +183,13 @@ interface SessionToolConfig<S extends TObject> {
|
|
|
166
183
|
toParams: (params: Static<S>) => Record<string, unknown>
|
|
167
184
|
/** 成功结果钩子(仅非错误通道结果触发)——notify-once 的 arm/label 缓存编排 */
|
|
168
185
|
onResult?: (ctx: ExtensionContext, params: Record<string, unknown>, result: unknown) => void
|
|
186
|
+
/**
|
|
187
|
+
* 一行收起态摘要(details.summary 的产出函数,六工具共用 executeTool 骨架统一
|
|
188
|
+
* 组装):输入 = 协议 params(toParams 产物)+ 成功回包;纯函数、缺数据省略
|
|
189
|
+
* 对应段不抛错。消费方 = taiji GUI 对话流(details.summary)与本包 renderResult
|
|
190
|
+
* 折叠态(pi TUI)。
|
|
191
|
+
*/
|
|
192
|
+
toSummary?: (params: Record<string, unknown>, result: unknown) => string
|
|
169
193
|
}
|
|
170
194
|
|
|
171
195
|
/**
|
|
@@ -182,6 +206,9 @@ function registerSessionTool<S extends TObject>(pi: ExtensionAPI, cfg: SessionTo
|
|
|
182
206
|
exposure: "deferred",
|
|
183
207
|
description: cfg.description,
|
|
184
208
|
parameters: cfg.parameters,
|
|
209
|
+
renderResult(result: unknown, options: { expanded: boolean }, theme: Theme) {
|
|
210
|
+
return renderSessionManagerResult(result, options, theme);
|
|
211
|
+
},
|
|
185
212
|
async execute(
|
|
186
213
|
_toolCallId: string,
|
|
187
214
|
params: Static<S>,
|
|
@@ -190,7 +217,7 @@ function registerSessionTool<S extends TObject>(pi: ExtensionAPI, cfg: SessionTo
|
|
|
190
217
|
// pi 1.0.0:ToolDefinition.execute 第 5 参 ctx 收窄为 ExtensionToolContext
|
|
191
218
|
ctx: ExtensionToolContext,
|
|
192
219
|
) {
|
|
193
|
-
return executeTool(ctx, cfg.action, cfg.toParams(params), cfg.onResult);
|
|
220
|
+
return executeTool(ctx, cfg.action, cfg.toParams(params), cfg.onResult, cfg.toSummary);
|
|
194
221
|
},
|
|
195
222
|
});
|
|
196
223
|
}
|
|
@@ -250,6 +277,12 @@ export default function sessionManagerExtension(pi: ExtensionAPI): void {
|
|
|
250
277
|
coordinator.armSessionNotify(ctx, r.lifetimeNotifyId, name);
|
|
251
278
|
}
|
|
252
279
|
},
|
|
280
|
+
toSummary: (params, result) => {
|
|
281
|
+
const label = stringField(params, "label");
|
|
282
|
+
if (label) return `Created session "${label}"`;
|
|
283
|
+
const sessionId = stringField(asResultRecord(result), "sessionId");
|
|
284
|
+
return sessionId ? `Created session ${sessionId}` : "Created session";
|
|
285
|
+
},
|
|
253
286
|
});
|
|
254
287
|
|
|
255
288
|
registerSessionTool(pi, {
|
|
@@ -268,6 +301,10 @@ export default function sessionManagerExtension(pi: ExtensionAPI): void {
|
|
|
268
301
|
if (typeof notifyId !== "string" || typeof sessionId !== "string") return;
|
|
269
302
|
coordinator.armSessionNotify(ctx, notifyId, coordinator.labelFor(sessionId));
|
|
270
303
|
},
|
|
304
|
+
toSummary: (params) => {
|
|
305
|
+
const sessionId = stringField(params, "sessionId");
|
|
306
|
+
return sessionId ? `Sent message to ${sessionId}` : "Sent message";
|
|
307
|
+
},
|
|
271
308
|
});
|
|
272
309
|
|
|
273
310
|
registerSessionTool(pi, {
|
|
@@ -277,6 +314,13 @@ export default function sessionManagerExtension(pi: ExtensionAPI): void {
|
|
|
277
314
|
parameters: ReadSessionHistoryParams,
|
|
278
315
|
action: "history",
|
|
279
316
|
toParams: (p) => ({ sessionId: p.sessionId, tailTurns: p.tailTurns }),
|
|
317
|
+
toSummary: (params, result) => {
|
|
318
|
+
const sessionId = stringField(params, "sessionId");
|
|
319
|
+
const messages = arrayField(asResultRecord(result), "messages");
|
|
320
|
+
if (sessionId && messages) return `Read history of ${sessionId} · ${messages.length} entries`;
|
|
321
|
+
if (sessionId) return `Read history of ${sessionId}`;
|
|
322
|
+
return "Read history";
|
|
323
|
+
},
|
|
280
324
|
});
|
|
281
325
|
|
|
282
326
|
registerSessionTool(pi, {
|
|
@@ -300,6 +344,10 @@ export default function sessionManagerExtension(pi: ExtensionAPI): void {
|
|
|
300
344
|
);
|
|
301
345
|
}
|
|
302
346
|
},
|
|
347
|
+
toSummary: (_params, result) => {
|
|
348
|
+
const sessions = arrayField(asResultRecord(result), "sessions");
|
|
349
|
+
return sessions ? `Listed ${sessions.length} managed sessions` : "Listed managed sessions";
|
|
350
|
+
},
|
|
303
351
|
});
|
|
304
352
|
|
|
305
353
|
registerSessionTool(pi, {
|
|
@@ -309,6 +357,13 @@ export default function sessionManagerExtension(pi: ExtensionAPI): void {
|
|
|
309
357
|
parameters: GetSessionStatusParams,
|
|
310
358
|
action: "status",
|
|
311
359
|
toParams: (p) => ({ sessionId: p.sessionId }),
|
|
360
|
+
toSummary: (params, result) => {
|
|
361
|
+
const sessionId = stringField(params, "sessionId");
|
|
362
|
+
const status = stringField(asResultRecord(result), "status");
|
|
363
|
+
if (sessionId && status) return `${sessionId} · ${status}`;
|
|
364
|
+
if (sessionId) return `Status of ${sessionId}`;
|
|
365
|
+
return "Session status";
|
|
366
|
+
},
|
|
312
367
|
});
|
|
313
368
|
|
|
314
369
|
registerSessionTool(pi, {
|
|
@@ -318,5 +373,9 @@ export default function sessionManagerExtension(pi: ExtensionAPI): void {
|
|
|
318
373
|
parameters: AbortSessionParams,
|
|
319
374
|
action: "abort",
|
|
320
375
|
toParams: (p) => ({ sessionId: p.sessionId }),
|
|
376
|
+
toSummary: (params) => {
|
|
377
|
+
const sessionId = stringField(params, "sessionId");
|
|
378
|
+
return sessionId ? `Aborted ${sessionId}` : "Aborted";
|
|
379
|
+
},
|
|
321
380
|
});
|
|
322
381
|
}
|
package/src/render.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// render.ts — session-manager 六工具共用 renderResult(pi TUI 侧;taiji GUI 对话流
|
|
2
|
+
// 不走本文件——GUI 直读 details.summary)。形态对齐 todo / canvas 扩展 render 先例:
|
|
3
|
+
// 折叠态一行摘要(dim 染色),展开态 / 无摘要(异常形态)回退原始 content 文本。
|
|
4
|
+
|
|
5
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
7
|
+
import { firstContentText } from "@zhushanwen/extension-protocol";
|
|
8
|
+
|
|
9
|
+
/** pi toolResult 通用形状(AgentToolResult 的结构最小面,renderResult 入参收窄用)。 */
|
|
10
|
+
interface ToolResultShape { // oe-exempt:20261017:framework:details 数据形状窄 view(单实现消费,非抽象接口)
|
|
11
|
+
content: Array<{ type: string; text?: string }>;
|
|
12
|
+
details?: { summary?: unknown };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function renderSessionManagerResult(result: unknown, options: { expanded: boolean }, theme: Theme): Text {
|
|
16
|
+
const r = result as ToolResultShape;
|
|
17
|
+
const summary = r.details?.summary;
|
|
18
|
+
if (options.expanded === false && typeof summary === "string" && summary !== "") {
|
|
19
|
+
return new Text(theme.fg("dim", summary), 0, 0);
|
|
20
|
+
}
|
|
21
|
+
return new Text(firstContentText(r), 0, 0);
|
|
22
|
+
}
|