@ia-ccun/code-agent-cli 0.0.52 → 0.0.53-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/bin/cli.js +97 -69
  2. package/config/agent/bin/README.md +102 -0
  3. package/config/agent/extensions/aicode-advisor.ts +197 -61
  4. package/config/agent/extensions/aicode-banner.ts +101 -0
  5. package/config/agent/extensions/aicode-btw.ts +677 -183
  6. package/config/agent/extensions/aicode-cd.ts +1017 -0
  7. package/config/agent/extensions/aicode-grillMe.ts +198 -0
  8. package/config/agent/extensions/aicode-pets.ts +351 -0
  9. package/config/agent/extensions/aicode-plan.ts +2 -0
  10. package/config/agent/extensions/aicode-safety-guard.ts +555 -0
  11. package/config/agent/extensions/aicode-tips.ts +69 -30
  12. package/config/agent/extensions/aicode-todo-progress.ts +547 -0
  13. package/config/agent/extensions/context-monitor.ts +30 -15
  14. package/config/agent/extensions/context.ts +132 -60
  15. package/config/agent/extensions/custom-footer.ts +0 -5
  16. package/config/agent/extensions/custom.ts +119 -109
  17. package/config/agent/extensions/error-monitor.ts +38 -51
  18. package/config/agent/extensions/goal.ts +8 -6
  19. package/config/agent/extensions/msg-debug.ts +103 -112
  20. package/config/agent/extensions/ponytail.ts +441 -0
  21. package/config/agent/extensions/review.ts +365 -164
  22. package/config/agent/extensions/tps.ts +87 -0
  23. package/config/agent/extensions/working-msg.ts +164 -51
  24. package/config/agent/help.md +0 -7
  25. package/config/agent/models.json +7 -7
  26. package/config/agent/settings.json +2 -1
  27. package/config/agent/skills/chroma/LICENSE +21 -0
  28. package/config/agent/skills/chroma/chroma-local/SKILL.md +165 -0
  29. package/config/agent/skills/chroma/chroma-local/cli.md +48 -0
  30. package/config/agent/skills/chroma/chroma-local/data-model.md +103 -0
  31. package/config/agent/skills/chroma/chroma-local/error-handling/python.md +166 -0
  32. package/config/agent/skills/chroma/chroma-local/error-handling/typescript.md +231 -0
  33. package/config/agent/skills/chroma/chroma-local/local-chroma/python.md +95 -0
  34. package/config/agent/skills/chroma/chroma-local/local-chroma/typescript.md +101 -0
  35. package/config/agent/skills/chroma/chroma-local/metadata/python.md +154 -0
  36. package/config/agent/skills/chroma/chroma-local/metadata/typescript.md +153 -0
  37. package/config/agent/skills/chroma/chroma-local/querying/python.md +147 -0
  38. package/config/agent/skills/chroma/chroma-local/querying/typescript.md +157 -0
  39. package/config/agent/skills/chroma/chroma-local/regex/python.md +69 -0
  40. package/config/agent/skills/chroma/chroma-local/regex/typescript.md +71 -0
  41. package/config/agent/skills/chroma/chroma-local/understanding-a-codebase.md +80 -0
  42. package/config/agent/skills/chroma/chroma-local/updating-deleting/python.md +159 -0
  43. package/config/agent/skills/chroma/chroma-local/updating-deleting/typescript.md +228 -0
  44. package/config/agent/skills/milvus-skill/LICENSE +202 -0
  45. package/config/agent/skills/milvus-skill/README.md +37 -0
  46. package/config/agent/skills/milvus-skill/SKILL.md +259 -0
  47. package/config/agent/skills/milvus-skill/references/collection.md +86 -0
  48. package/config/agent/skills/milvus-skill/references/database.md +24 -0
  49. package/config/agent/skills/milvus-skill/references/index.md +69 -0
  50. package/config/agent/skills/milvus-skill/references/partition.md +29 -0
  51. package/config/agent/skills/milvus-skill/references/patterns.md +150 -0
  52. package/config/agent/skills/milvus-skill/references/user-role.md +90 -0
  53. package/config/agent/skills/milvus-skill/references/vector.md +247 -0
  54. package/config/agent/skills/ponytail/SKILL.md +100 -0
  55. package/config/agent/skills/ponytail-audit/SKILL.md +40 -0
  56. package/config/agent/skills/ponytail-debt/SKILL.md +44 -0
  57. package/config/agent/skills/ponytail-help/SKILL.md +68 -0
  58. package/config/agent/skills/ponytail-review/SKILL.md +56 -0
  59. package/config/agent/skills/zread/README.md +115 -0
  60. package/config/agent/skills/zread/SKILL.md +103 -0
  61. package/config/agent/skills/zread/references/stdio-protocol.md +312 -0
  62. package/dist/banner.d.ts.map +1 -1
  63. package/dist/banner.js +8 -1
  64. package/dist/banner.js.map +1 -1
  65. package/dist/config-loader.d.ts.map +1 -1
  66. package/dist/config-loader.js +9 -2
  67. package/dist/config-loader.js.map +1 -1
  68. package/package.json +7 -7
  69. package/scripts/postinstall.js +58 -1
  70. package/scripts/regression/README.md +224 -0
  71. package/scripts/regression/cases/01-version-and-deps.sh +94 -0
  72. package/scripts/regression/cases/02-extension-loading.sh +130 -0
  73. package/scripts/regression/cases/03-rpc-protocol.sh +259 -0
  74. package/scripts/regression/cases/04-prompt-stream.sh +130 -0
  75. package/scripts/regression/cases/05-slash-commands-and-skills.sh +119 -0
  76. package/scripts/regression/cases/06-tool-execution.sh +115 -0
  77. package/scripts/regression/cases/07-pi-new-features.sh +141 -0
  78. package/scripts/regression/lib/common.sh +287 -0
  79. package/scripts/regression/results/.last_pid +1 -0
  80. package/scripts/regression/results/.last_workdir +1 -0
  81. package/scripts/regression/results/02-extension-loading.log +18 -0
  82. package/scripts/regression/results/03-prompt-stream.log +16 -0
  83. package/scripts/regression/results/03-rpc-protocol.log +27 -0
  84. package/scripts/regression/results/04-slash-commands-and-skills.log +11 -0
  85. package/scripts/regression/results/05-tool-execution.log +8 -0
  86. package/scripts/regression/results/06-pi-new-features.log +29 -0
  87. package/scripts/regression/run-all.sh +187 -0
  88. package/config/agent/dangerous-patterns.json +0 -106
  89. package/config/agent/extensions/bash-check.ts +0 -227
@@ -5,12 +5,18 @@ import {
5
5
  SessionManager,
6
6
  type AgentSession,
7
7
  type AgentSessionEvent,
8
+ type CreateAgentSessionOptions,
8
9
  type ExtensionAPI,
9
10
  type ExtensionCommandContext,
10
11
  type ExtensionContext,
11
12
  type ResourceLoader,
12
13
  } from "@earendil-works/pi-coding-agent";
13
- import { type AssistantMessage, type Message, type ThinkingLevel as AiThinkingLevel, type UserMessage } from "@earendil-works/pi-ai";
14
+ import {
15
+ type AssistantMessage,
16
+ type Message,
17
+ type ThinkingLevel as AiThinkingLevel,
18
+ type UserMessage,
19
+ } from "@earendil-works/pi-ai";
14
20
  import {
15
21
  Box,
16
22
  Container,
@@ -28,7 +34,6 @@ import {
28
34
  } from "@earendil-works/pi-tui";
29
35
 
30
36
  // ============== 常量 ==============
31
- const STATUS_KEY = "aicode-btw";
32
37
  const BTW_MESSAGE_TYPE = "aicode-btw-message";
33
38
  const BTW_ENTRY_TYPE = "aicode-btw-entry";
34
39
  const BTW_RESET_TYPE = "aicode-btw-reset";
@@ -40,6 +45,26 @@ function matchesBtwFocusShortcut(data: string): boolean {
40
45
  return BTW_FOCUS_SHORTCUTS.some((shortcut) => matchesKey(data, shortcut));
41
46
  }
42
47
 
48
+ /**
49
+ * 类型守卫:检查 entry 是否是有效的 Message 对象
50
+ * ✅ 用于安全转换,避免使用危险的 as unknown as
51
+ */
52
+ function isMessageLike(entry: unknown): entry is Partial<Message> & {
53
+ role?: string;
54
+ customType?: string;
55
+ content?: unknown;
56
+ } {
57
+ if (!entry || typeof entry !== "object") {
58
+ return false;
59
+ }
60
+ const candidate = entry as {
61
+ role?: unknown;
62
+ customType?: string;
63
+ content?: unknown;
64
+ };
65
+ return typeof candidate.role === "string" && Array.isArray(candidate.content);
66
+ }
67
+
43
68
  const BTW_SYSTEM_PROMPT = [
44
69
  "You are having an aside conversation with the user, separate from their main working session.",
45
70
  "If main session messages are provided, they are for context only — that work is being handled by another agent.",
@@ -51,8 +76,10 @@ const BTW_SYSTEM_PROMPT = [
51
76
  const BTW_SUMMARIZE_SYSTEM_PROMPT =
52
77
  "Summarize the side conversation concisely. Preserve key decisions, plans, insights, risks, and action items. Output only the summary.";
53
78
 
54
- const BTW_CONTINUE_THREAD_USER_TEXT = "[The following is a separate side conversation. Continue this thread.]";
55
- const BTW_CONTINUE_THREAD_ASSISTANT_TEXT = "Understood, continuing our side conversation.";
79
+ const BTW_CONTINUE_THREAD_USER_TEXT =
80
+ "[The following is a separate side conversation. Continue this thread.]";
81
+ const BTW_CONTINUE_THREAD_ASSISTANT_TEXT =
82
+ "Understood, continuing our side conversation.";
56
83
 
57
84
  type SessionThinkingLevel = "off" | AiThinkingLevel;
58
85
  type BtwThreadMode = "contextual" | "tangent";
@@ -88,7 +115,10 @@ type BtwResetDetails = {
88
115
  };
89
116
 
90
117
  type BtwModelOverrideDetails =
91
- | ({ timestamp: number; action: "set" } & Pick<SessionModel, "provider" | "id" | "api">)
118
+ | ({ timestamp: number; action: "set" } & Pick<
119
+ SessionModel,
120
+ "provider" | "id" | "api"
121
+ >)
92
122
  | { timestamp: number; action: "clear" };
93
123
 
94
124
  type BtwThinkingOverrideDetails =
@@ -112,11 +142,35 @@ type ResolvedBtwSettings = {
112
142
  };
113
143
 
114
144
  type BtwTranscriptEntry =
115
- | { id: number; turnId: number; type: "turn-boundary"; phase: "start" | "end" }
145
+ | {
146
+ id: number;
147
+ turnId: number;
148
+ type: "turn-boundary";
149
+ phase: "start" | "end";
150
+ }
116
151
  | { id: number; turnId: number; type: "user-message"; text: string }
117
- | { id: number; turnId: number; type: "thinking"; text: string; streaming: boolean }
118
- | { id: number; turnId: number; type: "assistant-text"; text: string; streaming: boolean }
119
- | { id: number; turnId: number; type: "tool-call"; toolCallId: string; toolName: string; args: string }
152
+ | {
153
+ id: number;
154
+ turnId: number;
155
+ type: "thinking";
156
+ text: string;
157
+ streaming: boolean;
158
+ }
159
+ | {
160
+ id: number;
161
+ turnId: number;
162
+ type: "assistant-text";
163
+ text: string;
164
+ streaming: boolean;
165
+ }
166
+ | {
167
+ id: number;
168
+ turnId: number;
169
+ type: "tool-call";
170
+ toolCallId: string;
171
+ toolName: string;
172
+ args: string;
173
+ }
120
174
  | {
121
175
  id: number;
122
176
  turnId: number;
@@ -137,7 +191,10 @@ type BtwTranscriptState = {
137
191
  nextTurnId: number;
138
192
  currentTurnId: number | null;
139
193
  lastTurnId: number | null;
140
- toolCalls: Map<string, { turnId: number; callEntryId: number; resultEntryId?: number }>;
194
+ toolCalls: Map<
195
+ string,
196
+ { turnId: number; callEntryId: number; resultEntryId?: number }
197
+ >;
141
198
  };
142
199
 
143
200
  type BtwSessionRuntime = {
@@ -156,17 +213,31 @@ type OverlayRuntime = {
156
213
  closed?: boolean;
157
214
  };
158
215
 
159
- function isVisibleBtwMessage(message: { role: string; customType?: string }): boolean {
216
+ function isVisibleBtwMessage(message: {
217
+ role: string;
218
+ customType?: string;
219
+ }): boolean {
160
220
  return message.role === "custom" && message.customType === BTW_MESSAGE_TYPE;
161
221
  }
162
222
 
163
- function isCustomEntry(entry: unknown, customType: string): entry is { type: "custom"; customType: string; data?: unknown } {
164
- return !!entry && typeof entry === "object" && (entry as { type?: string }).type === "custom" && (entry as { customType?: string }).customType === customType;
223
+ function isCustomEntry(
224
+ entry: unknown,
225
+ customType: string,
226
+ ): entry is { type: "custom"; customType: string; data?: unknown } {
227
+ return (
228
+ !!entry &&
229
+ typeof entry === "object" &&
230
+ (entry as { type?: string }).type === "custom" &&
231
+ (entry as { customType?: string }).customType === customType
232
+ );
165
233
  }
166
234
 
167
235
  function stripDynamicSystemPromptFooter(systemPrompt: string): string {
168
236
  return systemPrompt
169
- .replace(/\nCurrent date and time:[^\n]*(?:\nCurrent working directory:[^\n]*)?$/u, "")
237
+ .replace(
238
+ /\nCurrent date and time:[^\n]*(?:\nCurrent working directory:[^\n]*)?$/u,
239
+ "",
240
+ )
170
241
  .replace(/\nCurrent working directory:[^\n]*$/u, "")
171
242
  .trim();
172
243
  }
@@ -175,7 +246,11 @@ function createBtwResourceLoader(
175
246
  ctx: ExtensionCommandContext,
176
247
  appendSystemPrompt: string[] = [BTW_SYSTEM_PROMPT],
177
248
  ): ResourceLoader {
178
- const extensionsResult = { extensions: [], errors: [], runtime: createExtensionRuntime() };
249
+ const extensionsResult = {
250
+ extensions: [],
251
+ errors: [],
252
+ runtime: createExtensionRuntime(),
253
+ };
179
254
  const systemPrompt = stripDynamicSystemPromptFooter(ctx.getSystemPrompt());
180
255
 
181
256
  return {
@@ -185,13 +260,18 @@ function createBtwResourceLoader(
185
260
  getThemes: () => ({ themes: [], diagnostics: [] }),
186
261
  getAgentsFiles: () => ({ agentsFiles: [] }),
187
262
  getSystemPrompt: () => systemPrompt,
263
+ getSystemPromptSource: () => undefined,
188
264
  getAppendSystemPrompt: () => appendSystemPrompt,
265
+ getAppendSystemPromptSources: () => [],
189
266
  extendResources: () => {},
190
267
  reload: async () => {},
191
268
  };
192
269
  }
193
270
 
194
- function extractText(parts: AssistantMessage["content"], type: "text" | "thinking"): string {
271
+ function extractText(
272
+ parts: AssistantMessage["content"],
273
+ type: "text" | "thinking",
274
+ ): string {
195
275
  const chunks: string[] = [];
196
276
 
197
277
  for (const part of parts) {
@@ -219,7 +299,9 @@ function parseBtwArgs(args: string): ParsedBtwArgs {
219
299
  return { question, save };
220
300
  }
221
301
 
222
- function parseBtwModelArgs(args: string):
302
+ function parseBtwModelArgs(
303
+ args: string,
304
+ ):
223
305
  | { action: "show" }
224
306
  | { action: "clear" }
225
307
  | { action: "set"; model: BtwModelRef }
@@ -235,14 +317,19 @@ function parseBtwModelArgs(args: string):
235
317
 
236
318
  const parts = trimmed.split(/\s+/);
237
319
  if (parts.length !== 3) {
238
- return { action: "invalid", message: "Usage: /btw:model <provider> <model> <api> | clear" };
320
+ return {
321
+ action: "invalid",
322
+ message: "Usage: /btw:model <provider> <model> <api> | clear",
323
+ };
239
324
  }
240
325
 
241
326
  const [provider, id, api] = parts;
242
327
  return { action: "set", model: { provider, id, api } as BtwModelRef };
243
328
  }
244
329
 
245
- function parseBtwThinkingArgs(args: string):
330
+ function parseBtwThinkingArgs(
331
+ args: string,
332
+ ):
246
333
  | { action: "show" }
247
334
  | { action: "clear" }
248
335
  | { action: "set"; thinkingLevel: SessionThinkingLevel } {
@@ -258,7 +345,9 @@ function parseBtwThinkingArgs(args: string):
258
345
  return { action: "set", thinkingLevel: trimmed as SessionThinkingLevel };
259
346
  }
260
347
 
261
- function formatModelRef(model: Pick<SessionModel, "provider" | "id" | "api">): string {
348
+ function formatModelRef(
349
+ model: Pick<SessionModel, "provider" | "id" | "api">,
350
+ ): string {
262
351
  return `${model.provider}/${model.id} (${model.api})`;
263
352
  }
264
353
 
@@ -273,23 +362,33 @@ function buildBtwSeedState(
273
362
  if (mode === "contextual") {
274
363
  try {
275
364
  messages.push(
276
- ...(buildSessionContext(ctx.sessionManager.getEntries(), ctx.sessionManager.getLeafId()).messages as Message[]).filter(
277
- (message) => !isVisibleBtwMessage(message),
278
- ),
365
+ ...(
366
+ buildSessionContext(
367
+ ctx.sessionManager.getEntries(),
368
+ ctx.sessionManager.getLeafId(),
369
+ ).messages as Message[]
370
+ ).filter((message) => !isVisibleBtwMessage(message)),
279
371
  );
280
- } catch {
372
+ } catch (error) {
373
+ // ✅ 改进错误处理:记录错误而不忽略
374
+ console.warn("[btw] buildSessionContext failed, using fallback:", error);
281
375
  messages.push(
282
376
  ...ctx.sessionManager.getEntries().flatMap((entry) => {
283
- if (!entry || typeof entry !== "object") {
377
+ // ✅ 使用类型守卫进行安全转换,避免 as unknown as
378
+ if (!isMessageLike(entry)) {
284
379
  return [];
285
380
  }
286
381
 
287
- const message = entry as unknown as Partial<Message> & { role?: string; customType?: string; content?: unknown };
288
- if (typeof message.role !== "string" || !Array.isArray(message.content)) {
289
- return [];
382
+ const message = entry; // ✅ 类型安全
383
+ if (
384
+ !isVisibleBtwMessage({
385
+ role: message.role ?? "",
386
+ customType: message.customType,
387
+ })
388
+ ) {
389
+ return [message as Message];
290
390
  }
291
-
292
- return isVisibleBtwMessage({ role: message.role, customType: message.customType }) ? [] : [message as Message];
391
+ return [];
293
392
  }),
294
393
  );
295
394
  }
@@ -335,16 +434,25 @@ function buildBtwSeedState(
335
434
  content: [{ type: "text", text: entry.answer }],
336
435
  provider: entry.provider,
337
436
  model: entry.model,
338
- api: entry.api || sessionModel?.api || ctx.model?.api || "openai-responses",
339
- usage:
340
- entry.usage ?? {
437
+ api:
438
+ entry.api ||
439
+ sessionModel?.api ||
440
+ ctx.model?.api ||
441
+ "openai-responses",
442
+ usage: entry.usage ?? {
443
+ input: 0,
444
+ output: 0,
445
+ cacheRead: 0,
446
+ cacheWrite: 0,
447
+ totalTokens: 0,
448
+ cost: {
341
449
  input: 0,
342
450
  output: 0,
343
451
  cacheRead: 0,
344
452
  cacheWrite: 0,
345
- totalTokens: 0,
346
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
453
+ total: 0,
347
454
  },
455
+ },
348
456
  stopReason: "stop",
349
457
  timestamp: entry.timestamp,
350
458
  },
@@ -380,7 +488,9 @@ function formatToolPreview(value: unknown): string {
380
488
  return "";
381
489
  }
382
490
  return preview.length > 120 ? `${preview.slice(0, 117)}...` : preview;
383
- } catch {
491
+ } catch (error) {
492
+ // ✅ 改进错误处理:记录错误而不忽略
493
+ console.warn("[btw] JSON.stringify failed in formatToolPreview:", error);
384
494
  return "";
385
495
  }
386
496
  }
@@ -396,6 +506,26 @@ function createEmptyTranscriptState(): BtwTranscriptState {
396
506
  };
397
507
  }
398
508
 
509
+ /**
510
+ * clearTranscriptState - 清理 transcriptState 的所有资源
511
+ * ✅ 清理 entries 数组
512
+ * ✅ 清理 toolCalls Map(防止内存泄漏)
513
+ * ✅ 重置所有状态
514
+ */
515
+ function clearTranscriptState(state: BtwTranscriptState): void {
516
+ // ✅ 清理 toolCalls Map(防止内存泄漏)
517
+ state.toolCalls.clear();
518
+
519
+ // 清理 entries 数组
520
+ state.entries = [];
521
+
522
+ // 重置状态
523
+ state.nextEntryId = 1;
524
+ state.nextTurnId = 1;
525
+ state.currentTurnId = null;
526
+ state.lastTurnId = null;
527
+ }
528
+
399
529
  function appendTranscriptEntry<T extends BtwTranscriptEntry>(
400
530
  state: BtwTranscriptState,
401
531
  entry: Omit<T, "id">,
@@ -413,21 +543,35 @@ function ensureTranscriptTurn(state: BtwTranscriptState): number {
413
543
  const turnId = state.nextTurnId++;
414
544
  state.currentTurnId = turnId;
415
545
  state.lastTurnId = turnId;
416
- appendTranscriptEntry(state, { type: "turn-boundary", turnId, phase: "start" } as Omit<Extract<BtwTranscriptEntry, { type: "turn-boundary" }>, "id">);
546
+ appendTranscriptEntry(state, {
547
+ type: "turn-boundary",
548
+ turnId,
549
+ phase: "start",
550
+ } as Omit<Extract<BtwTranscriptEntry, { type: "turn-boundary" }>, "id">);
417
551
  return turnId;
418
552
  }
419
553
 
420
- function finishTranscriptTurn(state: BtwTranscriptState, turnId?: number | null): void {
554
+ function finishTranscriptTurn(
555
+ state: BtwTranscriptState,
556
+ turnId?: number | null,
557
+ ): void {
421
558
  const resolvedTurnId = turnId ?? state.currentTurnId;
422
559
  if (resolvedTurnId === null || resolvedTurnId === undefined) {
423
560
  return;
424
561
  }
425
562
 
426
563
  const hasEndBoundary = state.entries.some(
427
- (entry) => entry.turnId === resolvedTurnId && entry.type === "turn-boundary" && entry.phase === "end",
564
+ (entry) =>
565
+ entry.turnId === resolvedTurnId &&
566
+ entry.type === "turn-boundary" &&
567
+ entry.phase === "end",
428
568
  );
429
569
  if (!hasEndBoundary) {
430
- appendTranscriptEntry(state, { type: "turn-boundary", turnId: resolvedTurnId, phase: "end" } as Omit<Extract<BtwTranscriptEntry, { type: "turn-boundary" }>, "id">);
570
+ appendTranscriptEntry(state, {
571
+ type: "turn-boundary",
572
+ turnId: resolvedTurnId,
573
+ phase: "end",
574
+ } as Omit<Extract<BtwTranscriptEntry, { type: "turn-boundary" }>, "id">);
431
575
  }
432
576
 
433
577
  for (const entry of state.entries) {
@@ -435,7 +579,11 @@ function finishTranscriptTurn(state: BtwTranscriptState, turnId?: number | null)
435
579
  continue;
436
580
  }
437
581
 
438
- if (entry.type === "thinking" || entry.type === "assistant-text" || entry.type === "tool-result") {
582
+ if (
583
+ entry.type === "thinking" ||
584
+ entry.type === "assistant-text" ||
585
+ entry.type === "tool-result"
586
+ ) {
439
587
  entry.streaming = false;
440
588
  }
441
589
  }
@@ -444,9 +592,19 @@ function finishTranscriptTurn(state: BtwTranscriptState, turnId?: number | null)
444
592
  if (state.currentTurnId === resolvedTurnId) {
445
593
  state.currentTurnId = null;
446
594
  }
595
+
596
+ // ✅ 清理当前 turn 的 toolCalls(防止内存泄漏)
597
+ for (const [toolCallId, toolCall] of state.toolCalls.entries()) {
598
+ if (toolCall.turnId === resolvedTurnId) {
599
+ state.toolCalls.delete(toolCallId);
600
+ }
601
+ }
447
602
  }
448
603
 
449
- function removeTranscriptTurn(state: BtwTranscriptState, turnId: number | null): void {
604
+ function removeTranscriptTurn(
605
+ state: BtwTranscriptState,
606
+ turnId: number | null,
607
+ ): void {
450
608
  if (turnId === null) {
451
609
  return;
452
610
  }
@@ -483,7 +641,11 @@ function findLatestTranscriptEntry<TType extends BtwTranscriptEntry["type"]>(
483
641
 
484
642
  function ensureTranscriptTurnForUserMessage(state: BtwTranscriptState): number {
485
643
  if (state.currentTurnId !== null) {
486
- const currentAssistant = findLatestTranscriptEntry(state, state.currentTurnId, "assistant-text");
644
+ const currentAssistant = findLatestTranscriptEntry(
645
+ state,
646
+ state.currentTurnId,
647
+ "assistant-text",
648
+ );
487
649
  if (currentAssistant && !currentAssistant.streaming) {
488
650
  finishTranscriptTurn(state, state.currentTurnId);
489
651
  }
@@ -492,7 +654,9 @@ function ensureTranscriptTurnForUserMessage(state: BtwTranscriptState): number {
492
654
  return ensureTranscriptTurn(state);
493
655
  }
494
656
 
495
- function extractMessageText(message: { content?: string | AssistantMessage["content"] | UserMessage["content"] }): string {
657
+ function extractMessageText(message: {
658
+ content?: string | AssistantMessage["content"] | UserMessage["content"];
659
+ }): string {
496
660
  if (typeof message.content === "string") {
497
661
  return message.content;
498
662
  }
@@ -500,13 +664,20 @@ function extractMessageText(message: { content?: string | AssistantMessage["cont
500
664
  return "";
501
665
  }
502
666
  return message.content
503
- .filter((part): part is { type: "text"; text: string } => part.type === "text" && typeof part.text === "string")
667
+ .filter(
668
+ (part): part is { type: "text"; text: string } =>
669
+ part.type === "text" && typeof part.text === "string",
670
+ )
504
671
  .map((part) => part.text)
505
672
  .join("\n")
506
673
  .trim();
507
674
  }
508
675
 
509
- function upsertUserMessageEntry(state: BtwTranscriptState, turnId: number, text: string): void {
676
+ function upsertUserMessageEntry(
677
+ state: BtwTranscriptState,
678
+ turnId: number,
679
+ text: string,
680
+ ): void {
510
681
  if (!text) {
511
682
  return;
512
683
  }
@@ -517,7 +688,10 @@ function upsertUserMessageEntry(state: BtwTranscriptState, turnId: number, text:
517
688
  return;
518
689
  }
519
690
 
520
- appendTranscriptEntry(state, { type: "user-message", turnId, text } as Omit<Extract<BtwTranscriptEntry, { type: "user-message" }>, "id">);
691
+ appendTranscriptEntry(state, { type: "user-message", turnId, text } as Omit<
692
+ Extract<BtwTranscriptEntry, { type: "user-message" }>,
693
+ "id"
694
+ >);
521
695
  }
522
696
 
523
697
  function upsertTranscriptTextEntry(
@@ -538,10 +712,16 @@ function upsertTranscriptTextEntry(
538
712
  return;
539
713
  }
540
714
 
541
- appendTranscriptEntry(state, { type, turnId, text, streaming } as Omit<Extract<BtwTranscriptEntry, { type: "thinking" | "assistant-text" }>, "id">);
715
+ appendTranscriptEntry(state, { type, turnId, text, streaming } as Omit<
716
+ Extract<BtwTranscriptEntry, { type: "thinking" | "assistant-text" }>,
717
+ "id"
718
+ >);
542
719
  }
543
720
 
544
- function summarizeToolResult(value: unknown, maxLength = 400): { content: string; truncated: boolean } {
721
+ function summarizeToolResult(
722
+ value: unknown,
723
+ maxLength = 400,
724
+ ): { content: string; truncated: boolean } {
545
725
  let content = "";
546
726
 
547
727
  if (value && typeof value === "object") {
@@ -574,7 +754,12 @@ function summarizeToolResult(value: unknown, maxLength = 400): { content: string
574
754
  } else if (value !== undefined) {
575
755
  try {
576
756
  content = JSON.stringify(value, null, 2);
577
- } catch {
757
+ } catch (error) {
758
+ // ✅ 改进错误处理:记录错误而不忽略
759
+ console.warn(
760
+ "[btw] JSON.stringify failed in summarizeToolResult:",
761
+ error,
762
+ );
578
763
  content = String(value);
579
764
  }
580
765
  }
@@ -628,7 +813,10 @@ function upsertToolResultEntry(
628
813
  const toolCall = ensureToolCallEntry(state, turnId, toolCallId, toolName, "");
629
814
  const existing =
630
815
  toolCall.resultEntryId !== undefined
631
- ? state.entries.find((entry) => entry.id === toolCall.resultEntryId && entry.type === "tool-result")
816
+ ? state.entries.find(
817
+ (entry) =>
818
+ entry.id === toolCall.resultEntryId && entry.type === "tool-result",
819
+ )
632
820
  : undefined;
633
821
 
634
822
  if (existing && existing.type === "tool-result") {
@@ -667,11 +855,20 @@ function applyAssistantMessageToTranscript(
667
855
  }
668
856
 
669
857
  if (answer) {
670
- upsertTranscriptTextEntry(state, turnId, "assistant-text", answer, streaming);
858
+ upsertTranscriptTextEntry(
859
+ state,
860
+ turnId,
861
+ "assistant-text",
862
+ answer,
863
+ streaming,
864
+ );
671
865
  }
672
866
  }
673
867
 
674
- function applyTranscriptEvent(state: BtwTranscriptState, event: AgentSessionEvent): void {
868
+ function applyTranscriptEvent(
869
+ state: BtwTranscriptState,
870
+ event: AgentSessionEvent,
871
+ ): void {
675
872
  switch (event.type) {
676
873
  case "turn_start": {
677
874
  ensureTranscriptTurn(state);
@@ -680,7 +877,11 @@ function applyTranscriptEvent(state: BtwTranscriptState, event: AgentSessionEven
680
877
  case "message_start": {
681
878
  if (event.message.role === "user") {
682
879
  const turnId = ensureTranscriptTurnForUserMessage(state);
683
- upsertUserMessageEntry(state, turnId, extractMessageText(event.message));
880
+ upsertUserMessageEntry(
881
+ state,
882
+ turnId,
883
+ extractMessageText(event.message),
884
+ );
684
885
  return;
685
886
  }
686
887
 
@@ -702,7 +903,11 @@ function applyTranscriptEvent(state: BtwTranscriptState, event: AgentSessionEven
702
903
  case "message_end": {
703
904
  if (event.message.role === "user") {
704
905
  const turnId = ensureTranscriptTurnForUserMessage(state);
705
- upsertUserMessageEntry(state, turnId, extractMessageText(event.message));
906
+ upsertUserMessageEntry(
907
+ state,
908
+ turnId,
909
+ extractMessageText(event.message),
910
+ );
706
911
  return;
707
912
  }
708
913
 
@@ -714,11 +919,19 @@ function applyTranscriptEvent(state: BtwTranscriptState, event: AgentSessionEven
714
919
  }
715
920
  case "tool_execution_start": {
716
921
  const turnId = ensureTranscriptTurn(state);
717
- ensureToolCallEntry(state, turnId, event.toolCallId, event.toolName, formatToolPreview(event.args));
922
+ ensureToolCallEntry(
923
+ state,
924
+ turnId,
925
+ event.toolCallId,
926
+ event.toolName,
927
+ formatToolPreview(event.args),
928
+ );
718
929
  return;
719
930
  }
720
931
  case "tool_execution_update": {
721
- const turnId = state.toolCalls.get(event.toolCallId)?.turnId ?? ensureTranscriptTurn(state);
932
+ const turnId =
933
+ state.toolCalls.get(event.toolCallId)?.turnId ??
934
+ ensureTranscriptTurn(state);
722
935
  const result = summarizeToolResult(event.partialResult);
723
936
  upsertToolResultEntry(
724
937
  state,
@@ -733,7 +946,9 @@ function applyTranscriptEvent(state: BtwTranscriptState, event: AgentSessionEven
733
946
  return;
734
947
  }
735
948
  case "tool_execution_end": {
736
- const turnId = state.toolCalls.get(event.toolCallId)?.turnId ?? ensureTranscriptTurn(state);
949
+ const turnId =
950
+ state.toolCalls.get(event.toolCallId)?.turnId ??
951
+ ensureTranscriptTurn(state);
737
952
  const result = summarizeToolResult(event.result);
738
953
  upsertToolResultEntry(
739
954
  state,
@@ -756,45 +971,100 @@ function applyTranscriptEvent(state: BtwTranscriptState, event: AgentSessionEven
756
971
  }
757
972
  }
758
973
 
759
- function appendPersistedTranscriptTurn(state: BtwTranscriptState, details: BtwDetails): void {
974
+ function appendPersistedTranscriptTurn(
975
+ state: BtwTranscriptState,
976
+ details: BtwDetails,
977
+ ): void {
760
978
  const turnId = ensureTranscriptTurn(state);
761
979
  upsertUserMessageEntry(state, turnId, details.question);
762
980
  if (details.thinking) {
763
- upsertTranscriptTextEntry(state, turnId, "thinking", details.thinking, false);
981
+ upsertTranscriptTextEntry(
982
+ state,
983
+ turnId,
984
+ "thinking",
985
+ details.thinking,
986
+ false,
987
+ );
764
988
  }
765
- upsertTranscriptTextEntry(state, turnId, "assistant-text", details.answer, false);
989
+ upsertTranscriptTextEntry(
990
+ state,
991
+ turnId,
992
+ "assistant-text",
993
+ details.answer,
994
+ false,
995
+ );
766
996
  finishTranscriptTurn(state, turnId);
767
997
  }
768
998
 
769
- function setTranscriptFailure(state: BtwTranscriptState, message: string): void {
770
- const turnId = state.currentTurnId ?? state.lastTurnId ?? ensureTranscriptTurn(state);
771
- upsertTranscriptTextEntry(state, turnId, "assistant-text", `❌ ${message}`, false);
999
+ function setTranscriptFailure(
1000
+ state: BtwTranscriptState,
1001
+ message: string,
1002
+ ): void {
1003
+ const turnId =
1004
+ state.currentTurnId ?? state.lastTurnId ?? ensureTranscriptTurn(state);
1005
+ upsertTranscriptTextEntry(
1006
+ state,
1007
+ turnId,
1008
+ "assistant-text",
1009
+ `❌ ${message}`,
1010
+ false,
1011
+ );
772
1012
  finishTranscriptTurn(state, turnId);
773
1013
  }
774
1014
 
775
1015
  function hasStreamingTranscriptEntry(entries: BtwTranscript): boolean {
776
1016
  return entries.some(
777
1017
  (entry) =>
778
- (entry.type === "thinking" || entry.type === "assistant-text" || entry.type === "tool-result") &&
1018
+ (entry.type === "thinking" ||
1019
+ entry.type === "assistant-text" ||
1020
+ entry.type === "tool-result") &&
779
1021
  entry.streaming,
780
1022
  );
781
1023
  }
782
1024
 
783
1025
  function getCompletedExchangeCount(entries: BtwTranscript): number {
784
- return entries.filter((entry) => entry.type === "assistant-text" && !entry.streaming).length;
1026
+ return entries.filter(
1027
+ (entry) => entry.type === "assistant-text" && !entry.streaming,
1028
+ ).length;
785
1029
  }
786
1030
 
787
- function buildOverlayTranscript(entries: BtwTranscript, theme: ExtensionContext["ui"]["theme"]): string[] {
1031
+ function buildOverlayTranscript(
1032
+ entries: BtwTranscript,
1033
+ theme: ExtensionContext["ui"]["theme"],
1034
+ ): string[] {
788
1035
  if (entries.length === 0) {
789
1036
  return [theme.fg("dim", "暂无 BTW 对话。输入侧边问题开始。")];
790
1037
  }
791
1038
 
792
1039
  const lines: string[] = [];
793
- const userBadge = buildTranscriptBadge(theme, "You", "userMessageBg", "accent");
794
- const thinkingBadge = buildTranscriptBadge(theme, "Thinking", "toolPendingBg", "warning");
795
- const toolBadge = buildTranscriptBadge(theme, "Tool", "toolPendingBg", "warning");
796
- const assistantBadge = buildTranscriptBadge(theme, "Assistant", "customMessageBg", "success");
797
- const separator = theme.fg("borderMuted", "────────────────────────────────────────");
1040
+ const userBadge = buildTranscriptBadge(
1041
+ theme,
1042
+ "You",
1043
+ "userMessageBg",
1044
+ "accent",
1045
+ );
1046
+ const thinkingBadge = buildTranscriptBadge(
1047
+ theme,
1048
+ "Thinking",
1049
+ "toolPendingBg",
1050
+ "warning",
1051
+ );
1052
+ const toolBadge = buildTranscriptBadge(
1053
+ theme,
1054
+ "Tool",
1055
+ "toolPendingBg",
1056
+ "warning",
1057
+ );
1058
+ const assistantBadge = buildTranscriptBadge(
1059
+ theme,
1060
+ "Assistant",
1061
+ "customMessageBg",
1062
+ "success",
1063
+ );
1064
+ const separator = theme.fg(
1065
+ "borderMuted",
1066
+ "────────────────────────────────────────",
1067
+ );
798
1068
  const blockIndent = " ";
799
1069
  const resultIndent = blockIndent;
800
1070
 
@@ -825,7 +1095,11 @@ function buildOverlayTranscript(entries: BtwTranscript, theme: ExtensionContext[
825
1095
  const pushStackedBlock = (
826
1096
  header: string,
827
1097
  text: string,
828
- options: { blankBefore?: boolean; indent?: string; style?: (value: string) => string } = {},
1098
+ options: {
1099
+ blankBefore?: boolean;
1100
+ indent?: string;
1101
+ style?: (value: string) => string;
1102
+ } = {},
829
1103
  ) => {
830
1104
  const bodyLines = text.split("\n");
831
1105
  const indent = options.indent ?? blockIndent;
@@ -855,7 +1129,9 @@ function buildOverlayTranscript(entries: BtwTranscript, theme: ExtensionContext[
855
1129
  }
856
1130
 
857
1131
  if (entry.type === "thinking") {
858
- const thinkingHeader = entry.streaming ? `${thinkingBadge} ${theme.fg("warning", "▍")}` : thinkingBadge;
1132
+ const thinkingHeader = entry.streaming
1133
+ ? `${thinkingBadge} ${theme.fg("warning", "▍")}`
1134
+ : thinkingBadge;
859
1135
  pushStackedBlock(thinkingHeader, entry.text, {
860
1136
  style: (line) => theme.fg("warning", theme.italic(line)),
861
1137
  });
@@ -875,17 +1151,26 @@ function buildOverlayTranscript(entries: BtwTranscript, theme: ExtensionContext[
875
1151
  : entry.streaming
876
1152
  ? theme.fg("warning", "↳ 流式输出中")
877
1153
  : theme.fg("dim", "↳ result");
878
- const truncationLabel = entry.truncated ? theme.fg("dim", " (truncated)") : "";
879
- pushStackedBlock(`${resultHeaderLabel}${truncationLabel}`, entry.content, {
880
- blankBefore: false,
881
- indent: resultIndent,
882
- style: (line) => (entry.isError ? theme.fg("error", line) : theme.fg("dim", line)),
883
- });
1154
+ const truncationLabel = entry.truncated
1155
+ ? theme.fg("dim", " (truncated)")
1156
+ : "";
1157
+ pushStackedBlock(
1158
+ `${resultHeaderLabel}${truncationLabel}`,
1159
+ entry.content,
1160
+ {
1161
+ blankBefore: false,
1162
+ indent: resultIndent,
1163
+ style: (line) =>
1164
+ entry.isError ? theme.fg("error", line) : theme.fg("dim", line),
1165
+ },
1166
+ );
884
1167
  continue;
885
1168
  }
886
1169
 
887
1170
  if (entry.type === "assistant-text") {
888
- const assistantHeader = entry.streaming ? `${assistantBadge} ${theme.fg("warning", "▍")}` : assistantBadge;
1171
+ const assistantHeader = entry.streaming
1172
+ ? `${assistantBadge} ${theme.fg("warning", "▍")}`
1173
+ : assistantBadge;
889
1174
  pushStackedBlock(assistantHeader, entry.text);
890
1175
  }
891
1176
  }
@@ -893,7 +1178,9 @@ function buildOverlayTranscript(entries: BtwTranscript, theme: ExtensionContext[
893
1178
  return lines;
894
1179
  }
895
1180
 
896
- function getLastAssistantMessage(session: AgentSession): AssistantMessage | null {
1181
+ function getLastAssistantMessage(
1182
+ session: AgentSession,
1183
+ ): AssistantMessage | null {
897
1184
  for (let i = session.state.messages.length - 1; i >= 0; i--) {
898
1185
  const message = session.state.messages[i];
899
1186
  if (message.role === "assistant") {
@@ -914,10 +1201,18 @@ function buildBtwMessageContent(question: string, answer: string): string {
914
1201
  }
915
1202
 
916
1203
  function formatThread(thread: BtwHandoffExchange[]): string {
917
- return thread.map((entry) => `User: ${entry.user.trim()}\nAssistant: ${entry.assistant.trim()}`).join("\n\n---\n\n");
1204
+ return thread
1205
+ .map(
1206
+ (entry) =>
1207
+ `User: ${entry.user.trim()}\nAssistant: ${entry.assistant.trim()}`,
1208
+ )
1209
+ .join("\n\n---\n\n");
918
1210
  }
919
1211
 
920
- function isThreadContinuationMarker(messages: Message[], index: number): boolean {
1212
+ function isThreadContinuationMarker(
1213
+ messages: Message[],
1214
+ index: number,
1215
+ ): boolean {
921
1216
  const userMessage = messages[index];
922
1217
  const assistantMessage = messages[index + 1];
923
1218
  return (
@@ -928,9 +1223,18 @@ function isThreadContinuationMarker(messages: Message[], index: number): boolean
928
1223
  );
929
1224
  }
930
1225
 
931
- function extractBtwHandoffThread(sessionRuntime: BtwSessionRuntime): BtwHandoffExchange[] {
932
- const handoffMessages = sessionRuntime.session.state.messages.slice(sessionRuntime.sideThreadStartIndex);
933
- const threadMessages = isThreadContinuationMarker(handoffMessages as Message[], 0) ? handoffMessages.slice(2) : handoffMessages;
1226
+ function extractBtwHandoffThread(
1227
+ sessionRuntime: BtwSessionRuntime,
1228
+ ): BtwHandoffExchange[] {
1229
+ const handoffMessages = sessionRuntime.session.state.messages.slice(
1230
+ sessionRuntime.sideThreadStartIndex,
1231
+ );
1232
+ const threadMessages = isThreadContinuationMarker(
1233
+ handoffMessages as Message[],
1234
+ 0,
1235
+ )
1236
+ ? handoffMessages.slice(2)
1237
+ : handoffMessages;
934
1238
  const exchanges: BtwHandoffExchange[] = [];
935
1239
  let currentUser = "";
936
1240
  let currentAssistant = "";
@@ -964,7 +1268,9 @@ function extractBtwHandoffThread(sessionRuntime: BtwSessionRuntime): BtwHandoffE
964
1268
  continue;
965
1269
  }
966
1270
 
967
- currentAssistant = currentAssistant ? `${currentAssistant}\n\n${text}` : text;
1271
+ currentAssistant = currentAssistant
1272
+ ? `${currentAssistant}\n\n${text}`
1273
+ : text;
968
1274
  }
969
1275
 
970
1276
  pushCurrent();
@@ -997,7 +1303,11 @@ function saveVisibleBtwNote(
997
1303
  return "saved";
998
1304
  }
999
1305
 
1000
- function notify(ctx: ExtensionContext | ExtensionCommandContext, message: string, level: "info" | "warning" | "error"): void {
1306
+ function notify(
1307
+ ctx: ExtensionContext | ExtensionCommandContext,
1308
+ message: string,
1309
+ level: "info" | "warning" | "error",
1310
+ ): void {
1001
1311
  if (ctx.hasUI) {
1002
1312
  ctx.ui.notify(message, level);
1003
1313
  }
@@ -1123,7 +1433,10 @@ class BtwOverlayComponent extends Container implements Focusable {
1123
1433
  private borderLine(innerWidth: number, edge: "top" | "bottom"): string {
1124
1434
  const left = edge === "top" ? "┌" : "└";
1125
1435
  const right = edge === "top" ? "┐" : "┘";
1126
- return this.theme.fg("borderMuted", `${left}${"─".repeat(innerWidth)}${right}`);
1436
+ return this.theme.fg(
1437
+ "borderMuted",
1438
+ `${left}${"─".repeat(innerWidth)}${right}`,
1439
+ );
1127
1440
  }
1128
1441
 
1129
1442
  private wrapTranscript(innerWidth: number): string[] {
@@ -1151,13 +1464,20 @@ class BtwOverlayComponent extends Container implements Focusable {
1151
1464
 
1152
1465
  if (matchesKey(data, Key.pageUp)) {
1153
1466
  this.followTranscript = false;
1154
- this.transcriptScrollOffset = Math.max(0, this.transcriptScrollOffset - Math.max(1, this.transcriptViewportHeight - 1));
1467
+ this.transcriptScrollOffset = Math.max(
1468
+ 0,
1469
+ this.transcriptScrollOffset -
1470
+ Math.max(1, this.transcriptViewportHeight - 1),
1471
+ );
1155
1472
  this.tui.requestRender();
1156
1473
  return;
1157
1474
  }
1158
1475
 
1159
1476
  if (matchesKey(data, Key.pageDown)) {
1160
- this.transcriptScrollOffset += Math.max(1, this.transcriptViewportHeight - 1);
1477
+ this.transcriptScrollOffset += Math.max(
1478
+ 1,
1479
+ this.transcriptViewportHeight - 1,
1480
+ );
1161
1481
  this.tui.requestRender();
1162
1482
  return;
1163
1483
  }
@@ -1194,7 +1514,10 @@ class BtwOverlayComponent extends Container implements Focusable {
1194
1514
  if (this.followTranscript) {
1195
1515
  this.transcriptScrollOffset = maxScroll;
1196
1516
  } else {
1197
- this.transcriptScrollOffset = Math.max(0, Math.min(this.transcriptScrollOffset, maxScroll));
1517
+ this.transcriptScrollOffset = Math.max(
1518
+ 0,
1519
+ Math.min(this.transcriptScrollOffset, maxScroll),
1520
+ );
1198
1521
  if (this.transcriptScrollOffset >= maxScroll) {
1199
1522
  this.followTranscript = true;
1200
1523
  }
@@ -1204,7 +1527,10 @@ class BtwOverlayComponent extends Container implements Focusable {
1204
1527
  this.transcriptScrollOffset,
1205
1528
  this.transcriptScrollOffset + transcriptHeight,
1206
1529
  );
1207
- const transcriptPadCount = Math.max(0, transcriptHeight - visibleTranscript.length);
1530
+ const transcriptPadCount = Math.max(
1531
+ 0,
1532
+ transcriptHeight - visibleTranscript.length,
1533
+ );
1208
1534
  const hiddenAbove = this.transcriptScrollOffset;
1209
1535
  const hiddenBelow = Math.max(0, maxScroll - this.transcriptScrollOffset);
1210
1536
  const summary =
@@ -1214,7 +1540,12 @@ class BtwOverlayComponent extends Container implements Focusable {
1214
1540
 
1215
1541
  const lines = [this.borderLine(innerWidth, "top")];
1216
1542
 
1217
- lines.push(this.frameLine(this.theme.fg("accent", this.theme.bold(this.modeTextValue.trim())), innerWidth));
1543
+ lines.push(
1544
+ this.frameLine(
1545
+ this.theme.fg("accent", this.theme.bold(this.modeTextValue.trim())),
1546
+ innerWidth,
1547
+ ),
1548
+ );
1218
1549
  lines.push(this.frameLine(this.theme.fg("dim", summary), innerWidth));
1219
1550
  lines.push(this.ruleLine(innerWidth));
1220
1551
 
@@ -1226,9 +1557,19 @@ class BtwOverlayComponent extends Container implements Focusable {
1226
1557
  }
1227
1558
 
1228
1559
  lines.push(this.ruleLine(innerWidth));
1229
- lines.push(this.frameLine(this.theme.fg("warning", this.statusTextValue.trim()), innerWidth));
1560
+ lines.push(
1561
+ this.frameLine(
1562
+ this.theme.fg("warning", this.statusTextValue.trim()),
1563
+ innerWidth,
1564
+ ),
1565
+ );
1230
1566
  lines.push(this.inputFrameLine(dialogWidth));
1231
- lines.push(this.frameLine(this.theme.fg("dim", this.hintsTextValue.trim()), innerWidth));
1567
+ lines.push(
1568
+ this.frameLine(
1569
+ this.theme.fg("dim", this.hintsTextValue.trim()),
1570
+ innerWidth,
1571
+ ),
1572
+ );
1232
1573
  lines.push(this.borderLine(innerWidth, "bottom"));
1233
1574
 
1234
1575
  return lines;
@@ -1252,7 +1593,9 @@ class BtwOverlayComponent extends Container implements Focusable {
1252
1593
  this.modeText.setText(this.modeTextValue);
1253
1594
  const entries = this.readTranscriptEntries();
1254
1595
  const exchanges = getCompletedExchangeCount(entries);
1255
- const active = hasStreamingTranscriptEntry(entries) ? " · 流式输出中" : " · 空闲";
1596
+ const active = hasStreamingTranscriptEntry(entries)
1597
+ ? " · 流式输出中"
1598
+ : " · 空闲";
1256
1599
  this.summaryTextValue = `${exchanges} 轮对话${active}`;
1257
1600
  this.summaryText.setText(this.summaryTextValue);
1258
1601
 
@@ -1265,7 +1608,8 @@ class BtwOverlayComponent extends Container implements Focusable {
1265
1608
  const status = this.getStatus() ?? "就绪。Enter 提交,Escape 取消。";
1266
1609
  this.statusTextValue = status;
1267
1610
  this.statusText.setText(this.statusTextValue);
1268
- this.hintsTextValue = "Enter 提交 · Alt+/ 切换焦点 · Escape 取消 · PgUp/PgDn 滚动";
1611
+ this.hintsTextValue =
1612
+ "Enter 提交 · Alt+/ 切换焦点 · Escape 取消 · PgUp/PgDn 滚动";
1269
1613
  this.hintsText.setText(this.hintsTextValue);
1270
1614
  this.tui.requestRender();
1271
1615
  }
@@ -1292,7 +1636,10 @@ export default function (pi: ExtensionAPI) {
1292
1636
  }
1293
1637
  }
1294
1638
 
1295
- function setOverlayStatus(status: string | null, ctx?: ExtensionContext | ExtensionCommandContext): void {
1639
+ function setOverlayStatus(
1640
+ status: string | null,
1641
+ ctx?: ExtensionContext | ExtensionCommandContext,
1642
+ ): void {
1296
1643
  overlayStatus = status;
1297
1644
  syncUi(ctx);
1298
1645
  }
@@ -1333,19 +1680,25 @@ export default function (pi: ExtensionAPI) {
1333
1680
  overlayRuntime?.refresh?.();
1334
1681
  }
1335
1682
 
1336
- function removeBtwSessionSubscription(sessionRuntime: BtwSessionRuntime, unsubscribe: () => void): void {
1683
+ function removeBtwSessionSubscription(
1684
+ sessionRuntime: BtwSessionRuntime,
1685
+ unsubscribe: () => void,
1686
+ ): void {
1337
1687
  if (!sessionRuntime.subscriptions.delete(unsubscribe)) {
1338
1688
  return;
1339
1689
  }
1340
1690
 
1341
1691
  try {
1342
1692
  unsubscribe();
1343
- } catch {
1344
- // Ignore unsubscribe errors during BTW session replacement/shutdown.
1693
+ } catch (error) {
1694
+ // ✅ 改进错误处理:记录错误而不忽略
1695
+ console.warn("[btw] Unsubscribe failed during session removal:", error);
1345
1696
  }
1346
1697
  }
1347
1698
 
1348
- function clearBtwSessionSubscriptions(sessionRuntime: BtwSessionRuntime): void {
1699
+ function clearBtwSessionSubscriptions(
1700
+ sessionRuntime: BtwSessionRuntime,
1701
+ ): void {
1349
1702
  for (const unsubscribe of [...sessionRuntime.subscriptions]) {
1350
1703
  removeBtwSessionSubscription(sessionRuntime, unsubscribe);
1351
1704
  }
@@ -1356,7 +1709,10 @@ export default function (pi: ExtensionAPI) {
1356
1709
  event: AgentSessionEvent,
1357
1710
  ctx?: ExtensionContext | ExtensionCommandContext,
1358
1711
  ): void {
1359
- if (activeBtwSession?.session !== sessionRuntime.session || !overlayRuntime) {
1712
+ if (
1713
+ activeBtwSession?.session !== sessionRuntime.session ||
1714
+ !overlayRuntime
1715
+ ) {
1360
1716
  return;
1361
1717
  }
1362
1718
 
@@ -1368,7 +1724,12 @@ export default function (pi: ExtensionAPI) {
1368
1724
  }
1369
1725
 
1370
1726
  if (event.type === "tool_execution_end") {
1371
- setOverlayStatus(sessionRuntime.session.isStreaming ? `⏳ 运行工具: ${event.toolName}` : "⏳ 流式输出中...", ctx);
1727
+ setOverlayStatus(
1728
+ sessionRuntime.session.isStreaming
1729
+ ? `⏳ 运行工具: ${event.toolName}`
1730
+ : "⏳ 流式输出中...",
1731
+ ctx,
1732
+ );
1372
1733
  return;
1373
1734
  }
1374
1735
 
@@ -1387,21 +1748,31 @@ export default function (pi: ExtensionAPI) {
1387
1748
  }
1388
1749
  }
1389
1750
 
1390
- function subscribeOverlayToActiveBtwSession(ctx?: ExtensionContext | ExtensionCommandContext): void {
1751
+ function subscribeOverlayToActiveBtwSession(
1752
+ ctx?: ExtensionContext | ExtensionCommandContext,
1753
+ ): void {
1391
1754
  const sessionRuntime = activeBtwSession;
1392
1755
  if (!sessionRuntime || sessionRuntime.subscriptions.size > 0) {
1393
1756
  return;
1394
1757
  }
1395
1758
 
1396
- const unsubscribe = sessionRuntime.session.subscribe((event: AgentSessionEvent) => {
1397
- handleBtwSessionEvent(sessionRuntime, event, ctx);
1398
- });
1759
+ const unsubscribe = sessionRuntime.session.subscribe(
1760
+ (event: AgentSessionEvent) => {
1761
+ handleBtwSessionEvent(sessionRuntime, event, ctx);
1762
+ },
1763
+ );
1399
1764
  sessionRuntime.subscriptions.add(unsubscribe);
1400
1765
  }
1401
1766
 
1402
1767
  async function disposeBtwSession(): Promise<void> {
1403
1768
  const current = activeBtwSession;
1404
1769
  activeBtwSession = null;
1770
+
1771
+ // ✅ 清理 transcriptState(防止内存泄漏)
1772
+ if (transcriptState) {
1773
+ clearTranscriptState(transcriptState);
1774
+ }
1775
+
1405
1776
  if (!current) {
1406
1777
  return;
1407
1778
  }
@@ -1410,8 +1781,9 @@ export default function (pi: ExtensionAPI) {
1410
1781
 
1411
1782
  try {
1412
1783
  await current.session.abort();
1413
- } catch {
1414
- // Ignore abort errors during BTW session replacement/shutdown.
1784
+ } catch (error) {
1785
+ // ✅ 改进错误处理:记录错误而不忽略
1786
+ console.warn("[btw] Session abort failed during disposal:", error);
1415
1787
  }
1416
1788
 
1417
1789
  current.session.dispose();
@@ -1427,7 +1799,8 @@ export default function (pi: ExtensionAPI) {
1427
1799
  notifyOnFallback = false,
1428
1800
  ): Promise<ResolvedBtwModel> {
1429
1801
  if (btwModelOverride) {
1430
- const auth = await ctx.modelRegistry.getApiKeyAndHeaders(btwModelOverride);
1802
+ const auth =
1803
+ await ctx.modelRegistry.getApiKeyAndHeaders(btwModelOverride);
1431
1804
  if (auth.ok && auth.apiKey) {
1432
1805
  return {
1433
1806
  model: btwModelOverride,
@@ -1482,7 +1855,8 @@ export default function (pi: ExtensionAPI) {
1482
1855
  notifyOnFallback = false,
1483
1856
  ): Promise<ResolvedBtwSettings> {
1484
1857
  const resolvedModel = await resolveBtwModel(ctx, notifyOnFallback);
1485
- const thinkingLevel = btwThinkingOverride ?? (pi.getThinkingLevel() as SessionThinkingLevel);
1858
+ const thinkingLevel =
1859
+ btwThinkingOverride ?? (pi.getThinkingLevel() as SessionThinkingLevel);
1486
1860
 
1487
1861
  return {
1488
1862
  model: resolvedModel.model,
@@ -1514,14 +1888,26 @@ export default function (pi: ExtensionAPI) {
1514
1888
  }
1515
1889
 
1516
1890
  function describeResolvedThinking(settings: ResolvedBtwSettings): string {
1517
- const source = settings.thinkingSource === "override" ? "override" : "inherits main thread";
1891
+ const source =
1892
+ settings.thinkingSource === "override"
1893
+ ? "override"
1894
+ : "inherits main thread";
1518
1895
  return `BTW thinking: ${settings.thinkingLevel} (${source}).`;
1519
1896
  }
1520
1897
 
1521
- async function setBtwModelOverride(ctx: ExtensionCommandContext, nextModel: SessionModel | null): Promise<void> {
1898
+ async function setBtwModelOverride(
1899
+ ctx: ExtensionCommandContext,
1900
+ nextModel: SessionModel | null,
1901
+ ): Promise<void> {
1522
1902
  btwModelOverride = nextModel;
1523
1903
  const details: BtwModelOverrideDetails = nextModel
1524
- ? { action: "set", timestamp: Date.now(), provider: nextModel.provider, id: nextModel.id, api: nextModel.api }
1904
+ ? {
1905
+ action: "set",
1906
+ timestamp: Date.now(),
1907
+ provider: nextModel.provider,
1908
+ id: nextModel.id,
1909
+ api: nextModel.api,
1910
+ }
1525
1911
  : { action: "clear", timestamp: Date.now() };
1526
1912
  pi.appendEntry(BTW_MODEL_OVERRIDE_TYPE, details);
1527
1913
  await disposeBtwSession();
@@ -1539,7 +1925,11 @@ export default function (pi: ExtensionAPI) {
1539
1925
  ): Promise<void> {
1540
1926
  btwThinkingOverride = nextThinkingLevel;
1541
1927
  const details: BtwThinkingOverrideDetails = nextThinkingLevel
1542
- ? { action: "set", timestamp: Date.now(), thinkingLevel: nextThinkingLevel }
1928
+ ? {
1929
+ action: "set",
1930
+ timestamp: Date.now(),
1931
+ thinkingLevel: nextThinkingLevel,
1932
+ }
1543
1933
  : { action: "clear", timestamp: Date.now() };
1544
1934
  pi.appendEntry(BTW_THINKING_OVERRIDE_TYPE, details);
1545
1935
  await disposeBtwSession();
@@ -1551,7 +1941,10 @@ export default function (pi: ExtensionAPI) {
1551
1941
  notify(ctx, `${message} ${describeResolvedThinking(settings)}`, "info");
1552
1942
  }
1553
1943
 
1554
- async function createBtwSubSession(ctx: ExtensionCommandContext, mode: BtwThreadMode): Promise<BtwSessionRuntime> {
1944
+ async function createBtwSubSession(
1945
+ ctx: ExtensionCommandContext,
1946
+ mode: BtwThreadMode,
1947
+ ): Promise<BtwSessionRuntime> {
1555
1948
  const settings = await resolveBtwSettings(ctx, true);
1556
1949
  if (!settings.model) {
1557
1950
  throw new Error(settings.fallbackReason || "No active model selected.");
@@ -1560,22 +1953,32 @@ export default function (pi: ExtensionAPI) {
1560
1953
  const { session } = await createAgentSession({
1561
1954
  sessionManager: SessionManager.inMemory(),
1562
1955
  model: settings.model,
1563
- modelRegistry: ctx.modelRegistry as AgentSession["modelRegistry"],
1564
- thinkingLevel: settings.thinkingLevel,
1956
+ // pi 1.1.0: AgentSession 改用 ModelRuntime, 但 ModelRegistry 实现了 Models 接口
1957
+ modelRuntime:
1958
+ ctx.modelRegistry as unknown as CreateAgentSessionOptions["modelRuntime"],
1565
1959
  // Match pi's default coding-agent toolset (read/bash/edit/write).
1566
1960
  tools: ["read", "bash", "edit", "write"],
1567
1961
  resourceLoader: createBtwResourceLoader(ctx),
1568
1962
  });
1569
1963
 
1570
- const { messages: seedMessages, sideThreadStartIndex } = buildBtwSeedState(ctx, pendingThread, mode, settings.model);
1964
+ const { messages: seedMessages, sideThreadStartIndex } = buildBtwSeedState(
1965
+ ctx,
1966
+ pendingThread,
1967
+ mode,
1968
+ settings.model,
1969
+ );
1571
1970
  if (seedMessages.length > 0) {
1572
- session.agent.state.messages = seedMessages as typeof session.state.messages;
1971
+ session.agent.state.messages =
1972
+ seedMessages as typeof session.state.messages;
1573
1973
  }
1574
1974
 
1575
1975
  return { session, mode, subscriptions: new Set(), sideThreadStartIndex };
1576
1976
  }
1577
1977
 
1578
- async function ensureBtwSession(ctx: ExtensionCommandContext, mode: BtwThreadMode): Promise<BtwSessionRuntime | null> {
1978
+ async function ensureBtwSession(
1979
+ ctx: ExtensionCommandContext,
1980
+ mode: BtwThreadMode,
1981
+ ): Promise<BtwSessionRuntime | null> {
1579
1982
  const settings = await resolveBtwSettings(ctx);
1580
1983
  if (!settings.model) {
1581
1984
  return null;
@@ -1590,7 +1993,9 @@ export default function (pi: ExtensionAPI) {
1590
1993
  return activeBtwSession;
1591
1994
  }
1592
1995
 
1593
- async function ensureOverlay(ctx: ExtensionCommandContext | ExtensionContext): Promise<void> {
1996
+ async function ensureOverlay(
1997
+ ctx: ExtensionCommandContext | ExtensionContext,
1998
+ ): Promise<void> {
1594
1999
  if (!ctx.hasUI) {
1595
2000
  return;
1596
2001
  }
@@ -1692,11 +2097,19 @@ export default function (pi: ExtensionAPI) {
1692
2097
  if (overlayRuntime === runtime) {
1693
2098
  overlayRuntime = null;
1694
2099
  }
1695
- notify(ctx, error instanceof Error ? error.message : String(error), "error");
2100
+ notify(
2101
+ ctx,
2102
+ error instanceof Error ? error.message : String(error),
2103
+ "error",
2104
+ );
1696
2105
  });
1697
2106
  }
1698
2107
 
1699
- async function dispatchBtwCommand(name: string, args: string, ctx: ExtensionCommandContext): Promise<boolean> {
2108
+ async function dispatchBtwCommand(
2109
+ name: string,
2110
+ args: string,
2111
+ ctx: ExtensionCommandContext,
2112
+ ): Promise<boolean> {
1700
2113
  const trimmedArgs = args.trim();
1701
2114
 
1702
2115
  if (name === "btw") {
@@ -1794,7 +2207,10 @@ export default function (pi: ExtensionAPI) {
1794
2207
  return true;
1795
2208
  }
1796
2209
 
1797
- await setBtwThinkingOverride(ctx, parsed.action === "clear" ? null : parsed.thinkingLevel);
2210
+ await setBtwThinkingOverride(
2211
+ ctx,
2212
+ parsed.action === "clear" ? null : parsed.thinkingLevel,
2213
+ );
1798
2214
  return true;
1799
2215
  }
1800
2216
 
@@ -1821,7 +2237,11 @@ export default function (pi: ExtensionAPI) {
1821
2237
  notify(ctx, `已注入 BTW 线程(${count} 轮对话)。`, "info");
1822
2238
  } catch (error) {
1823
2239
  setOverlayStatus("注入失败。线程已保留,可重试或总结。", ctx);
1824
- notify(ctx, error instanceof Error ? error.message : String(error), "error");
2240
+ notify(
2241
+ ctx,
2242
+ error instanceof Error ? error.message : String(error),
2243
+ "error",
2244
+ );
1825
2245
  }
1826
2246
  return true;
1827
2247
  }
@@ -1850,7 +2270,11 @@ export default function (pi: ExtensionAPI) {
1850
2270
  notify(ctx, `已注入 BTW 总结(${count} 轮对话)。`, "info");
1851
2271
  } catch (error) {
1852
2272
  setOverlayStatus("总结失败。线程已保留,可重试或注入。", ctx);
1853
- notify(ctx, error instanceof Error ? error.message : String(error), "error");
2273
+ notify(
2274
+ ctx,
2275
+ error instanceof Error ? error.message : String(error),
2276
+ "error",
2277
+ );
1854
2278
  }
1855
2279
  return true;
1856
2280
  }
@@ -1858,9 +2282,13 @@ export default function (pi: ExtensionAPI) {
1858
2282
  return false;
1859
2283
  }
1860
2284
 
1861
- function parseOverlayBtwCommand(value: string): { name: string; args: string } | null {
2285
+ function parseOverlayBtwCommand(
2286
+ value: string,
2287
+ ): { name: string; args: string } | null {
1862
2288
  const trimmed = value.trim();
1863
- const match = trimmed.match(/^\/(btw:(?:new|tangent|clear|inject|summarize|model|thinking))(?:\s+(.*))?$/);
2289
+ const match = trimmed.match(
2290
+ /^\/(btw:(?:new|tangent|clear|inject|summarize|model|thinking))(?:\s+(.*))?$/,
2291
+ );
1864
2292
  if (!match) {
1865
2293
  return null;
1866
2294
  }
@@ -1871,7 +2299,10 @@ export default function (pi: ExtensionAPI) {
1871
2299
  };
1872
2300
  }
1873
2301
 
1874
- async function submitFromOverlay(ctx: ExtensionCommandContext | ExtensionContext, value: string): Promise<void> {
2302
+ async function submitFromOverlay(
2303
+ ctx: ExtensionCommandContext | ExtensionContext,
2304
+ value: string,
2305
+ ): Promise<void> {
1875
2306
  const question = value.trim();
1876
2307
  if (!question) {
1877
2308
  setOverlayStatus("Enter a BTW prompt before submitting.", ctx);
@@ -1879,7 +2310,10 @@ export default function (pi: ExtensionAPI) {
1879
2310
  }
1880
2311
 
1881
2312
  if (!("getSystemPrompt" in ctx)) {
1882
- setOverlayStatus("BTW overlay submit requires a command context. Reopen BTW from a command.", ctx);
2313
+ setOverlayStatus(
2314
+ "BTW overlay submit requires a command context. Reopen BTW from a command.",
2315
+ ctx,
2316
+ );
1883
2317
  return;
1884
2318
  }
1885
2319
 
@@ -1933,7 +2367,9 @@ export default function (pi: ExtensionAPI) {
1933
2367
 
1934
2368
  for (let i = 0; i < branch.length; i++) {
1935
2369
  if (isCustomEntry(branch[i], BTW_MODEL_OVERRIDE_TYPE)) {
1936
- const details = (branch[i] as unknown as { data?: BtwModelOverrideDetails }).data;
2370
+ const details = (
2371
+ branch[i] as unknown as { data?: BtwModelOverrideDetails }
2372
+ ).data;
1937
2373
  if (details?.action === "set") {
1938
2374
  const resolved = ctx.modelRegistry.find(details.provider, details.id);
1939
2375
  if (resolved) {
@@ -1948,7 +2384,9 @@ export default function (pi: ExtensionAPI) {
1948
2384
  }
1949
2385
 
1950
2386
  if (isCustomEntry(branch[i], BTW_THINKING_OVERRIDE_TYPE)) {
1951
- const details = (branch[i] as unknown as { data?: BtwThinkingOverrideDetails }).data;
2387
+ const details = (
2388
+ branch[i] as unknown as { data?: BtwThinkingOverrideDetails }
2389
+ ).data;
1952
2390
  btwThinkingOverride =
1953
2391
  details?.action === "set"
1954
2392
  ? details.thinkingLevel
@@ -1959,7 +2397,8 @@ export default function (pi: ExtensionAPI) {
1959
2397
 
1960
2398
  if (isCustomEntry(branch[i], BTW_RESET_TYPE)) {
1961
2399
  lastResetIndex = i;
1962
- const details = (branch[i] as unknown as { data?: BtwResetDetails }).data;
2400
+ const details = (branch[i] as unknown as { data?: BtwResetDetails })
2401
+ .data;
1963
2402
  pendingMode = details?.mode ?? "contextual";
1964
2403
  }
1965
2404
  }
@@ -2004,7 +2443,9 @@ export default function (pi: ExtensionAPI) {
2004
2443
 
2005
2444
  const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
2006
2445
  if (!auth.ok || !auth.apiKey) {
2007
- const message = auth.ok ? `No credentials available for ${model.provider}/${model.id}.` : auth.error;
2446
+ const message = auth.ok
2447
+ ? `No credentials available for ${model.provider}/${model.id}.`
2448
+ : auth.error;
2008
2449
  setOverlayStatus(message, ctx);
2009
2450
  notify(ctx, message, "error");
2010
2451
  await ensureOverlay(ctx);
@@ -2037,7 +2478,10 @@ export default function (pi: ExtensionAPI) {
2037
2478
  throw new Error("BTW request finished without a response.");
2038
2479
  }
2039
2480
  if (response.stopReason === "aborted") {
2040
- removeTranscriptTurn(transcriptState, transcriptState.lastTurnId ?? transcriptState.currentTurnId);
2481
+ removeTranscriptTurn(
2482
+ transcriptState,
2483
+ transcriptState.lastTurnId ?? transcriptState.currentTurnId,
2484
+ );
2041
2485
  setOverlayStatus("Request aborted.", ctx);
2042
2486
  return;
2043
2487
  }
@@ -2045,9 +2489,16 @@ export default function (pi: ExtensionAPI) {
2045
2489
  throw new Error(response.errorMessage || "BTW request failed.");
2046
2490
  }
2047
2491
 
2048
- const completedTurnId = transcriptState.lastTurnId ?? transcriptState.currentTurnId;
2492
+ const completedTurnId =
2493
+ transcriptState.lastTurnId ?? transcriptState.currentTurnId;
2049
2494
  const streamedThinking =
2050
- completedTurnId !== null ? findLatestTranscriptEntry(transcriptState, completedTurnId, "thinking")?.text : "";
2495
+ completedTurnId !== null
2496
+ ? findLatestTranscriptEntry(
2497
+ transcriptState,
2498
+ completedTurnId,
2499
+ "thinking",
2500
+ )?.text
2501
+ : "";
2051
2502
  const answer = extractAnswer(response);
2052
2503
  const thinking = extractThinking(response) || streamedThinking || "";
2053
2504
 
@@ -2074,12 +2525,19 @@ export default function (pi: ExtensionAPI) {
2074
2525
  notify(ctx, "已排队,将在当前轮次结束后保存到主对话。", "info");
2075
2526
  setOverlayStatus("已排队,将在当前轮次结束后保存到主对话。", ctx);
2076
2527
  } else {
2077
- setOverlayStatus("Ready for a follow-up. Hidden BTW thread updated.", ctx);
2528
+ setOverlayStatus(
2529
+ "Ready for a follow-up. Hidden BTW thread updated.",
2530
+ ctx,
2531
+ );
2078
2532
  }
2079
2533
  } catch (error) {
2080
- const errorMessage = error instanceof Error ? error.message : String(error);
2534
+ const errorMessage =
2535
+ error instanceof Error ? error.message : String(error);
2081
2536
  setTranscriptFailure(transcriptState, errorMessage);
2082
- setOverlayStatus("Request failed. Thread preserved for retry or follow-up.", ctx);
2537
+ setOverlayStatus(
2538
+ "Request failed. Thread preserved for retry or follow-up.",
2539
+ ctx,
2540
+ );
2083
2541
  notify(ctx, errorMessage, "error");
2084
2542
  await disposeBtwSession();
2085
2543
  } finally {
@@ -2088,15 +2546,23 @@ export default function (pi: ExtensionAPI) {
2088
2546
  }
2089
2547
 
2090
2548
  function getPendingThreadForHandoff(): BtwHandoffExchange[] {
2091
- return pendingThread.map((entry) => ({ user: entry.question, assistant: entry.answer }));
2092
- }
2093
-
2094
- async function getBtwHandoffThread(
2095
- ctx: ExtensionCommandContext,
2096
- ): Promise<{ sessionRuntime: BtwSessionRuntime | null; thread: BtwHandoffExchange[] }> {
2097
- const sessionRuntime = activeBtwSession ?? (await ensureBtwSession(ctx, pendingMode));
2098
- const thread = sessionRuntime ? extractBtwHandoffThread(sessionRuntime) : [];
2099
- const resolvedThread = thread.length > 0 ? thread : getPendingThreadForHandoff();
2549
+ return pendingThread.map((entry) => ({
2550
+ user: entry.question,
2551
+ assistant: entry.answer,
2552
+ }));
2553
+ }
2554
+
2555
+ async function getBtwHandoffThread(ctx: ExtensionCommandContext): Promise<{
2556
+ sessionRuntime: BtwSessionRuntime | null;
2557
+ thread: BtwHandoffExchange[];
2558
+ }> {
2559
+ const sessionRuntime =
2560
+ activeBtwSession ?? (await ensureBtwSession(ctx, pendingMode));
2561
+ const thread = sessionRuntime
2562
+ ? extractBtwHandoffThread(sessionRuntime)
2563
+ : [];
2564
+ const resolvedThread =
2565
+ thread.length > 0 ? thread : getPendingThreadForHandoff();
2100
2566
 
2101
2567
  if (resolvedThread.length === 0) {
2102
2568
  throw new Error("无可用 BTW 对话用于交接。");
@@ -2105,7 +2571,10 @@ export default function (pi: ExtensionAPI) {
2105
2571
  return { sessionRuntime, thread: resolvedThread };
2106
2572
  }
2107
2573
 
2108
- async function summarizeThread(ctx: ExtensionCommandContext, thread: BtwHandoffExchange[]): Promise<string> {
2574
+ async function summarizeThread(
2575
+ ctx: ExtensionCommandContext,
2576
+ thread: BtwHandoffExchange[],
2577
+ ): Promise<string> {
2109
2578
  const settings = await resolveBtwSettings(ctx, true);
2110
2579
  const model = settings.model;
2111
2580
  if (!model) {
@@ -2114,16 +2583,23 @@ export default function (pi: ExtensionAPI) {
2114
2583
 
2115
2584
  const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
2116
2585
  if (!auth.ok || !auth.apiKey) {
2117
- throw new Error(auth.ok ? `No credentials available for ${model.provider}/${model.id}.` : auth.error);
2586
+ throw new Error(
2587
+ auth.ok
2588
+ ? `No credentials available for ${model.provider}/${model.id}.`
2589
+ : auth.error,
2590
+ );
2118
2591
  }
2119
2592
 
2120
2593
  const { session } = await createAgentSession({
2121
2594
  sessionManager: SessionManager.inMemory(),
2122
2595
  model,
2123
- modelRegistry: ctx.modelRegistry as AgentSession["modelRegistry"],
2124
- thinkingLevel: "off",
2596
+ // pi 1.1.0: AgentSession 改用 ModelRuntime, 但 ModelRegistry 实现了 Models 接口
2597
+ modelRuntime:
2598
+ ctx.modelRegistry as unknown as CreateAgentSessionOptions["modelRuntime"],
2125
2599
  tools: [],
2126
- resourceLoader: createBtwResourceLoader(ctx, [BTW_SUMMARIZE_SYSTEM_PROMPT]),
2600
+ resourceLoader: createBtwResourceLoader(ctx, [
2601
+ BTW_SUMMARIZE_SYSTEM_PROMPT,
2602
+ ]),
2127
2603
  });
2128
2604
 
2129
2605
  try {
@@ -2134,7 +2610,9 @@ export default function (pi: ExtensionAPI) {
2134
2610
  throw new Error("BTW summarize finished without a response.");
2135
2611
  }
2136
2612
  if (response.stopReason === "error") {
2137
- throw new Error(response.errorMessage || "Failed to summarize BTW thread.");
2613
+ throw new Error(
2614
+ response.errorMessage || "Failed to summarize BTW thread.",
2615
+ );
2138
2616
  }
2139
2617
  if (response.stopReason === "aborted") {
2140
2618
  throw new Error("BTW summarize aborted.");
@@ -2144,14 +2622,21 @@ export default function (pi: ExtensionAPI) {
2144
2622
  } finally {
2145
2623
  try {
2146
2624
  await session.abort();
2147
- } catch {
2148
- // Ignore abort errors during summarize session shutdown.
2625
+ } catch (error) {
2626
+ // ✅ 改进错误处理:记录错误而不忽略
2627
+ console.warn(
2628
+ "[btw] Session abort failed during summarize shutdown:",
2629
+ error,
2630
+ );
2149
2631
  }
2150
2632
  session.dispose();
2151
2633
  }
2152
2634
  }
2153
2635
 
2154
- function sendThreadToMain(ctx: ExtensionCommandContext, content: string): void {
2636
+ function sendThreadToMain(
2637
+ ctx: ExtensionCommandContext,
2638
+ content: string,
2639
+ ): void {
2155
2640
  if (ctx.isIdle()) {
2156
2641
  pi.sendUserMessage(content);
2157
2642
  } else {
@@ -2159,37 +2644,45 @@ export default function (pi: ExtensionAPI) {
2159
2644
  }
2160
2645
  }
2161
2646
 
2162
- pi.registerMessageRenderer(BTW_MESSAGE_TYPE, (message, { expanded }, theme) => {
2163
- const details = message.details as BtwDetails | undefined;
2164
- const content = typeof message.content === "string" ? message.content : "[non-text btw message]";
2165
- const lines = [theme.fg("accent", theme.bold("[BTW]")), content];
2166
-
2167
- if (expanded && details) {
2168
- lines.push(
2169
- theme.fg(
2170
- "dim",
2171
- `model: ${details.provider}/${details.model} (${details.api ?? "openai-responses"}) · thinking: ${details.thinkingLevel}`,
2172
- ),
2173
- );
2647
+ pi.registerMessageRenderer(
2648
+ BTW_MESSAGE_TYPE,
2649
+ (message, { expanded }, theme) => {
2650
+ const details = message.details as BtwDetails | undefined;
2651
+ const content =
2652
+ typeof message.content === "string"
2653
+ ? message.content
2654
+ : "[non-text btw message]";
2655
+ const lines = [theme.fg("accent", theme.bold("[BTW]")), content];
2174
2656
 
2175
- if (details.usage) {
2657
+ if (expanded && details) {
2176
2658
  lines.push(
2177
2659
  theme.fg(
2178
2660
  "dim",
2179
- `tokens: in ${details.usage.input} · out ${details.usage.output} · total ${details.usage.totalTokens}`,
2661
+ `model: ${details.provider}/${details.model} (${details.api ?? "openai-responses"}) · thinking: ${details.thinkingLevel}`,
2180
2662
  ),
2181
2663
  );
2664
+
2665
+ if (details.usage) {
2666
+ lines.push(
2667
+ theme.fg(
2668
+ "dim",
2669
+ `tokens: in ${details.usage.input} · out ${details.usage.output} · total ${details.usage.totalTokens}`,
2670
+ ),
2671
+ );
2672
+ }
2182
2673
  }
2183
- }
2184
2674
 
2185
- const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
2186
- box.addChild(new Text(lines.join("\n"), 0, 0));
2187
- return box;
2188
- });
2675
+ const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
2676
+ box.addChild(new Text(lines.join("\n"), 0, 0));
2677
+ return box;
2678
+ },
2679
+ );
2189
2680
 
2190
2681
  pi.on("context", async (event) => {
2191
2682
  return {
2192
- messages: event.messages.filter((message) => !isVisibleBtwMessage(message)),
2683
+ messages: event.messages.filter(
2684
+ (message) => !isVisibleBtwMessage(message),
2685
+ ),
2193
2686
  };
2194
2687
  });
2195
2688
 
@@ -2216,7 +2709,8 @@ export default function (pi: ExtensionAPI) {
2216
2709
  }
2217
2710
 
2218
2711
  pi.registerCommand("btw", {
2219
- description: "侧边对话。默认带主会话上下文;用 /btw:tangent 开启无上下文模式。加 --save 保存到主对话。",
2712
+ description:
2713
+ "侧边对话。默认带主会话上下文;用 /btw:tangent 开启无上下文模式。加 --save 保存到主对话。",
2220
2714
  handler: async (args, ctx) => {
2221
2715
  await dispatchBtwCommand("btw", args, ctx);
2222
2716
  },