@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-session-manager",
3
- "version": "0.3.3",
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/pi-ext-guards": "0.4.4",
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.3",
36
- "@zhushanwen/subagent-core": "1.5.1",
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
- ): Promise<{ content: Array<{ type: "text"; text: string }>; details: undefined }> {
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
+ }