@ia-ccun/code-agent-cli 0.0.53-beta.1 → 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 (33) hide show
  1. package/bin/cli.js +92 -68
  2. package/config/agent/extensions/aicode-advisor.ts +187 -64
  3. package/config/agent/extensions/aicode-banner.ts +101 -0
  4. package/config/agent/extensions/aicode-btw.ts +622 -180
  5. package/config/agent/extensions/aicode-cd.ts +246 -63
  6. package/config/agent/extensions/aicode-pets.ts +0 -11
  7. package/config/agent/extensions/aicode-plan.ts +2 -0
  8. package/config/agent/extensions/aicode-safety-guard.ts +3 -0
  9. package/config/agent/extensions/aicode-tips.ts +1 -16
  10. package/config/agent/extensions/context-monitor.ts +30 -15
  11. package/config/agent/extensions/context.ts +132 -60
  12. package/config/agent/extensions/custom-footer.ts +0 -5
  13. package/config/agent/extensions/custom.ts +41 -29
  14. package/config/agent/extensions/error-monitor.ts +37 -33
  15. package/config/agent/extensions/goal.ts +8 -6
  16. package/config/agent/extensions/msg-debug.ts +103 -112
  17. package/config/agent/extensions/review.ts +365 -164
  18. package/config/agent/extensions/working-msg.ts +2 -4
  19. package/dist/banner.d.ts.map +1 -1
  20. package/dist/banner.js +8 -1
  21. package/dist/banner.js.map +1 -1
  22. package/dist/config-loader.d.ts.map +1 -1
  23. package/dist/config-loader.js +9 -2
  24. package/dist/config-loader.js.map +1 -1
  25. package/package.json +6 -7
  26. package/scripts/regression/results/.last_pid +1 -1
  27. package/scripts/regression/results/.last_workdir +1 -1
  28. package/scripts/regression/results/02-extension-loading.log +16 -27
  29. package/scripts/regression/results/03-prompt-stream.log +14 -23
  30. package/scripts/regression/results/03-rpc-protocol.log +20 -31
  31. package/scripts/regression/results/04-slash-commands-and-skills.log +11 -10
  32. package/scripts/regression/results/05-tool-execution.log +6 -4
  33. package/scripts/regression/results/06-pi-new-features.log +22 -26
@@ -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";
@@ -44,11 +49,19 @@ function matchesBtwFocusShortcut(data: string): boolean {
44
49
  * 类型守卫:检查 entry 是否是有效的 Message 对象
45
50
  * ✅ 用于安全转换,避免使用危险的 as unknown as
46
51
  */
47
- function isMessageLike(entry: unknown): entry is Partial<Message> & { role?: string; customType?: string; content?: unknown } {
52
+ function isMessageLike(entry: unknown): entry is Partial<Message> & {
53
+ role?: string;
54
+ customType?: string;
55
+ content?: unknown;
56
+ } {
48
57
  if (!entry || typeof entry !== "object") {
49
58
  return false;
50
59
  }
51
- const candidate = entry as { role?: unknown; customType?: string; content?: unknown };
60
+ const candidate = entry as {
61
+ role?: unknown;
62
+ customType?: string;
63
+ content?: unknown;
64
+ };
52
65
  return typeof candidate.role === "string" && Array.isArray(candidate.content);
53
66
  }
54
67
 
@@ -63,8 +76,10 @@ const BTW_SYSTEM_PROMPT = [
63
76
  const BTW_SUMMARIZE_SYSTEM_PROMPT =
64
77
  "Summarize the side conversation concisely. Preserve key decisions, plans, insights, risks, and action items. Output only the summary.";
65
78
 
66
- const BTW_CONTINUE_THREAD_USER_TEXT = "[The following is a separate side conversation. Continue this thread.]";
67
- 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.";
68
83
 
69
84
  type SessionThinkingLevel = "off" | AiThinkingLevel;
70
85
  type BtwThreadMode = "contextual" | "tangent";
@@ -100,7 +115,10 @@ type BtwResetDetails = {
100
115
  };
101
116
 
102
117
  type BtwModelOverrideDetails =
103
- | ({ timestamp: number; action: "set" } & Pick<SessionModel, "provider" | "id" | "api">)
118
+ | ({ timestamp: number; action: "set" } & Pick<
119
+ SessionModel,
120
+ "provider" | "id" | "api"
121
+ >)
104
122
  | { timestamp: number; action: "clear" };
105
123
 
106
124
  type BtwThinkingOverrideDetails =
@@ -124,11 +142,35 @@ type ResolvedBtwSettings = {
124
142
  };
125
143
 
126
144
  type BtwTranscriptEntry =
127
- | { 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
+ }
128
151
  | { id: number; turnId: number; type: "user-message"; text: string }
129
- | { id: number; turnId: number; type: "thinking"; text: string; streaming: boolean }
130
- | { id: number; turnId: number; type: "assistant-text"; text: string; streaming: boolean }
131
- | { 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
+ }
132
174
  | {
133
175
  id: number;
134
176
  turnId: number;
@@ -149,7 +191,10 @@ type BtwTranscriptState = {
149
191
  nextTurnId: number;
150
192
  currentTurnId: number | null;
151
193
  lastTurnId: number | null;
152
- toolCalls: Map<string, { turnId: number; callEntryId: number; resultEntryId?: number }>;
194
+ toolCalls: Map<
195
+ string,
196
+ { turnId: number; callEntryId: number; resultEntryId?: number }
197
+ >;
153
198
  };
154
199
 
155
200
  type BtwSessionRuntime = {
@@ -168,17 +213,31 @@ type OverlayRuntime = {
168
213
  closed?: boolean;
169
214
  };
170
215
 
171
- function isVisibleBtwMessage(message: { role: string; customType?: string }): boolean {
216
+ function isVisibleBtwMessage(message: {
217
+ role: string;
218
+ customType?: string;
219
+ }): boolean {
172
220
  return message.role === "custom" && message.customType === BTW_MESSAGE_TYPE;
173
221
  }
174
222
 
175
- function isCustomEntry(entry: unknown, customType: string): entry is { type: "custom"; customType: string; data?: unknown } {
176
- 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
+ );
177
233
  }
178
234
 
179
235
  function stripDynamicSystemPromptFooter(systemPrompt: string): string {
180
236
  return systemPrompt
181
- .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
+ )
182
241
  .replace(/\nCurrent working directory:[^\n]*$/u, "")
183
242
  .trim();
184
243
  }
@@ -187,7 +246,11 @@ function createBtwResourceLoader(
187
246
  ctx: ExtensionCommandContext,
188
247
  appendSystemPrompt: string[] = [BTW_SYSTEM_PROMPT],
189
248
  ): ResourceLoader {
190
- const extensionsResult = { extensions: [], errors: [], runtime: createExtensionRuntime() };
249
+ const extensionsResult = {
250
+ extensions: [],
251
+ errors: [],
252
+ runtime: createExtensionRuntime(),
253
+ };
191
254
  const systemPrompt = stripDynamicSystemPromptFooter(ctx.getSystemPrompt());
192
255
 
193
256
  return {
@@ -197,13 +260,18 @@ function createBtwResourceLoader(
197
260
  getThemes: () => ({ themes: [], diagnostics: [] }),
198
261
  getAgentsFiles: () => ({ agentsFiles: [] }),
199
262
  getSystemPrompt: () => systemPrompt,
263
+ getSystemPromptSource: () => undefined,
200
264
  getAppendSystemPrompt: () => appendSystemPrompt,
265
+ getAppendSystemPromptSources: () => [],
201
266
  extendResources: () => {},
202
267
  reload: async () => {},
203
268
  };
204
269
  }
205
270
 
206
- function extractText(parts: AssistantMessage["content"], type: "text" | "thinking"): string {
271
+ function extractText(
272
+ parts: AssistantMessage["content"],
273
+ type: "text" | "thinking",
274
+ ): string {
207
275
  const chunks: string[] = [];
208
276
 
209
277
  for (const part of parts) {
@@ -231,7 +299,9 @@ function parseBtwArgs(args: string): ParsedBtwArgs {
231
299
  return { question, save };
232
300
  }
233
301
 
234
- function parseBtwModelArgs(args: string):
302
+ function parseBtwModelArgs(
303
+ args: string,
304
+ ):
235
305
  | { action: "show" }
236
306
  | { action: "clear" }
237
307
  | { action: "set"; model: BtwModelRef }
@@ -247,14 +317,19 @@ function parseBtwModelArgs(args: string):
247
317
 
248
318
  const parts = trimmed.split(/\s+/);
249
319
  if (parts.length !== 3) {
250
- 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
+ };
251
324
  }
252
325
 
253
326
  const [provider, id, api] = parts;
254
327
  return { action: "set", model: { provider, id, api } as BtwModelRef };
255
328
  }
256
329
 
257
- function parseBtwThinkingArgs(args: string):
330
+ function parseBtwThinkingArgs(
331
+ args: string,
332
+ ):
258
333
  | { action: "show" }
259
334
  | { action: "clear" }
260
335
  | { action: "set"; thinkingLevel: SessionThinkingLevel } {
@@ -270,7 +345,9 @@ function parseBtwThinkingArgs(args: string):
270
345
  return { action: "set", thinkingLevel: trimmed as SessionThinkingLevel };
271
346
  }
272
347
 
273
- function formatModelRef(model: Pick<SessionModel, "provider" | "id" | "api">): string {
348
+ function formatModelRef(
349
+ model: Pick<SessionModel, "provider" | "id" | "api">,
350
+ ): string {
274
351
  return `${model.provider}/${model.id} (${model.api})`;
275
352
  }
276
353
 
@@ -285,13 +362,16 @@ function buildBtwSeedState(
285
362
  if (mode === "contextual") {
286
363
  try {
287
364
  messages.push(
288
- ...(buildSessionContext(ctx.sessionManager.getEntries(), ctx.sessionManager.getLeafId()).messages as Message[]).filter(
289
- (message) => !isVisibleBtwMessage(message),
290
- ),
365
+ ...(
366
+ buildSessionContext(
367
+ ctx.sessionManager.getEntries(),
368
+ ctx.sessionManager.getLeafId(),
369
+ ).messages as Message[]
370
+ ).filter((message) => !isVisibleBtwMessage(message)),
291
371
  );
292
372
  } catch (error) {
293
373
  // ✅ 改进错误处理:记录错误而不忽略
294
- console.warn('[btw] buildSessionContext failed, using fallback:', error);
374
+ console.warn("[btw] buildSessionContext failed, using fallback:", error);
295
375
  messages.push(
296
376
  ...ctx.sessionManager.getEntries().flatMap((entry) => {
297
377
  // ✅ 使用类型守卫进行安全转换,避免 as unknown as
@@ -300,8 +380,15 @@ function buildBtwSeedState(
300
380
  }
301
381
 
302
382
  const message = entry; // ✅ 类型安全
303
-
304
- return isVisibleBtwMessage({ role: message.role, customType: message.customType }) ? [] : [message as Message];
383
+ if (
384
+ !isVisibleBtwMessage({
385
+ role: message.role ?? "",
386
+ customType: message.customType,
387
+ })
388
+ ) {
389
+ return [message as Message];
390
+ }
391
+ return [];
305
392
  }),
306
393
  );
307
394
  }
@@ -347,16 +434,25 @@ function buildBtwSeedState(
347
434
  content: [{ type: "text", text: entry.answer }],
348
435
  provider: entry.provider,
349
436
  model: entry.model,
350
- api: entry.api || sessionModel?.api || ctx.model?.api || "openai-responses",
351
- usage:
352
- 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: {
353
449
  input: 0,
354
450
  output: 0,
355
451
  cacheRead: 0,
356
452
  cacheWrite: 0,
357
- totalTokens: 0,
358
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
453
+ total: 0,
359
454
  },
455
+ },
360
456
  stopReason: "stop",
361
457
  timestamp: entry.timestamp,
362
458
  },
@@ -394,7 +490,7 @@ function formatToolPreview(value: unknown): string {
394
490
  return preview.length > 120 ? `${preview.slice(0, 117)}...` : preview;
395
491
  } catch (error) {
396
492
  // ✅ 改进错误处理:记录错误而不忽略
397
- console.warn('[btw] JSON.stringify failed in formatToolPreview:', error);
493
+ console.warn("[btw] JSON.stringify failed in formatToolPreview:", error);
398
494
  return "";
399
495
  }
400
496
  }
@@ -419,10 +515,10 @@ function createEmptyTranscriptState(): BtwTranscriptState {
419
515
  function clearTranscriptState(state: BtwTranscriptState): void {
420
516
  // ✅ 清理 toolCalls Map(防止内存泄漏)
421
517
  state.toolCalls.clear();
422
-
518
+
423
519
  // 清理 entries 数组
424
520
  state.entries = [];
425
-
521
+
426
522
  // 重置状态
427
523
  state.nextEntryId = 1;
428
524
  state.nextTurnId = 1;
@@ -447,21 +543,35 @@ function ensureTranscriptTurn(state: BtwTranscriptState): number {
447
543
  const turnId = state.nextTurnId++;
448
544
  state.currentTurnId = turnId;
449
545
  state.lastTurnId = turnId;
450
- 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">);
451
551
  return turnId;
452
552
  }
453
553
 
454
- function finishTranscriptTurn(state: BtwTranscriptState, turnId?: number | null): void {
554
+ function finishTranscriptTurn(
555
+ state: BtwTranscriptState,
556
+ turnId?: number | null,
557
+ ): void {
455
558
  const resolvedTurnId = turnId ?? state.currentTurnId;
456
559
  if (resolvedTurnId === null || resolvedTurnId === undefined) {
457
560
  return;
458
561
  }
459
562
 
460
563
  const hasEndBoundary = state.entries.some(
461
- (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",
462
568
  );
463
569
  if (!hasEndBoundary) {
464
- 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">);
465
575
  }
466
576
 
467
577
  for (const entry of state.entries) {
@@ -469,7 +579,11 @@ function finishTranscriptTurn(state: BtwTranscriptState, turnId?: number | null)
469
579
  continue;
470
580
  }
471
581
 
472
- 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
+ ) {
473
587
  entry.streaming = false;
474
588
  }
475
589
  }
@@ -487,7 +601,10 @@ function finishTranscriptTurn(state: BtwTranscriptState, turnId?: number | null)
487
601
  }
488
602
  }
489
603
 
490
- function removeTranscriptTurn(state: BtwTranscriptState, turnId: number | null): void {
604
+ function removeTranscriptTurn(
605
+ state: BtwTranscriptState,
606
+ turnId: number | null,
607
+ ): void {
491
608
  if (turnId === null) {
492
609
  return;
493
610
  }
@@ -524,7 +641,11 @@ function findLatestTranscriptEntry<TType extends BtwTranscriptEntry["type"]>(
524
641
 
525
642
  function ensureTranscriptTurnForUserMessage(state: BtwTranscriptState): number {
526
643
  if (state.currentTurnId !== null) {
527
- const currentAssistant = findLatestTranscriptEntry(state, state.currentTurnId, "assistant-text");
644
+ const currentAssistant = findLatestTranscriptEntry(
645
+ state,
646
+ state.currentTurnId,
647
+ "assistant-text",
648
+ );
528
649
  if (currentAssistant && !currentAssistant.streaming) {
529
650
  finishTranscriptTurn(state, state.currentTurnId);
530
651
  }
@@ -533,7 +654,9 @@ function ensureTranscriptTurnForUserMessage(state: BtwTranscriptState): number {
533
654
  return ensureTranscriptTurn(state);
534
655
  }
535
656
 
536
- function extractMessageText(message: { content?: string | AssistantMessage["content"] | UserMessage["content"] }): string {
657
+ function extractMessageText(message: {
658
+ content?: string | AssistantMessage["content"] | UserMessage["content"];
659
+ }): string {
537
660
  if (typeof message.content === "string") {
538
661
  return message.content;
539
662
  }
@@ -541,13 +664,20 @@ function extractMessageText(message: { content?: string | AssistantMessage["cont
541
664
  return "";
542
665
  }
543
666
  return message.content
544
- .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
+ )
545
671
  .map((part) => part.text)
546
672
  .join("\n")
547
673
  .trim();
548
674
  }
549
675
 
550
- function upsertUserMessageEntry(state: BtwTranscriptState, turnId: number, text: string): void {
676
+ function upsertUserMessageEntry(
677
+ state: BtwTranscriptState,
678
+ turnId: number,
679
+ text: string,
680
+ ): void {
551
681
  if (!text) {
552
682
  return;
553
683
  }
@@ -558,7 +688,10 @@ function upsertUserMessageEntry(state: BtwTranscriptState, turnId: number, text:
558
688
  return;
559
689
  }
560
690
 
561
- 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
+ >);
562
695
  }
563
696
 
564
697
  function upsertTranscriptTextEntry(
@@ -579,10 +712,16 @@ function upsertTranscriptTextEntry(
579
712
  return;
580
713
  }
581
714
 
582
- 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
+ >);
583
719
  }
584
720
 
585
- function summarizeToolResult(value: unknown, maxLength = 400): { content: string; truncated: boolean } {
721
+ function summarizeToolResult(
722
+ value: unknown,
723
+ maxLength = 400,
724
+ ): { content: string; truncated: boolean } {
586
725
  let content = "";
587
726
 
588
727
  if (value && typeof value === "object") {
@@ -617,7 +756,10 @@ function summarizeToolResult(value: unknown, maxLength = 400): { content: string
617
756
  content = JSON.stringify(value, null, 2);
618
757
  } catch (error) {
619
758
  // ✅ 改进错误处理:记录错误而不忽略
620
- console.warn('[btw] JSON.stringify failed in summarizeToolResult:', error);
759
+ console.warn(
760
+ "[btw] JSON.stringify failed in summarizeToolResult:",
761
+ error,
762
+ );
621
763
  content = String(value);
622
764
  }
623
765
  }
@@ -671,7 +813,10 @@ function upsertToolResultEntry(
671
813
  const toolCall = ensureToolCallEntry(state, turnId, toolCallId, toolName, "");
672
814
  const existing =
673
815
  toolCall.resultEntryId !== undefined
674
- ? 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
+ )
675
820
  : undefined;
676
821
 
677
822
  if (existing && existing.type === "tool-result") {
@@ -710,11 +855,20 @@ function applyAssistantMessageToTranscript(
710
855
  }
711
856
 
712
857
  if (answer) {
713
- upsertTranscriptTextEntry(state, turnId, "assistant-text", answer, streaming);
858
+ upsertTranscriptTextEntry(
859
+ state,
860
+ turnId,
861
+ "assistant-text",
862
+ answer,
863
+ streaming,
864
+ );
714
865
  }
715
866
  }
716
867
 
717
- function applyTranscriptEvent(state: BtwTranscriptState, event: AgentSessionEvent): void {
868
+ function applyTranscriptEvent(
869
+ state: BtwTranscriptState,
870
+ event: AgentSessionEvent,
871
+ ): void {
718
872
  switch (event.type) {
719
873
  case "turn_start": {
720
874
  ensureTranscriptTurn(state);
@@ -723,7 +877,11 @@ function applyTranscriptEvent(state: BtwTranscriptState, event: AgentSessionEven
723
877
  case "message_start": {
724
878
  if (event.message.role === "user") {
725
879
  const turnId = ensureTranscriptTurnForUserMessage(state);
726
- upsertUserMessageEntry(state, turnId, extractMessageText(event.message));
880
+ upsertUserMessageEntry(
881
+ state,
882
+ turnId,
883
+ extractMessageText(event.message),
884
+ );
727
885
  return;
728
886
  }
729
887
 
@@ -745,7 +903,11 @@ function applyTranscriptEvent(state: BtwTranscriptState, event: AgentSessionEven
745
903
  case "message_end": {
746
904
  if (event.message.role === "user") {
747
905
  const turnId = ensureTranscriptTurnForUserMessage(state);
748
- upsertUserMessageEntry(state, turnId, extractMessageText(event.message));
906
+ upsertUserMessageEntry(
907
+ state,
908
+ turnId,
909
+ extractMessageText(event.message),
910
+ );
749
911
  return;
750
912
  }
751
913
 
@@ -757,11 +919,19 @@ function applyTranscriptEvent(state: BtwTranscriptState, event: AgentSessionEven
757
919
  }
758
920
  case "tool_execution_start": {
759
921
  const turnId = ensureTranscriptTurn(state);
760
- 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
+ );
761
929
  return;
762
930
  }
763
931
  case "tool_execution_update": {
764
- const turnId = state.toolCalls.get(event.toolCallId)?.turnId ?? ensureTranscriptTurn(state);
932
+ const turnId =
933
+ state.toolCalls.get(event.toolCallId)?.turnId ??
934
+ ensureTranscriptTurn(state);
765
935
  const result = summarizeToolResult(event.partialResult);
766
936
  upsertToolResultEntry(
767
937
  state,
@@ -776,7 +946,9 @@ function applyTranscriptEvent(state: BtwTranscriptState, event: AgentSessionEven
776
946
  return;
777
947
  }
778
948
  case "tool_execution_end": {
779
- const turnId = state.toolCalls.get(event.toolCallId)?.turnId ?? ensureTranscriptTurn(state);
949
+ const turnId =
950
+ state.toolCalls.get(event.toolCallId)?.turnId ??
951
+ ensureTranscriptTurn(state);
780
952
  const result = summarizeToolResult(event.result);
781
953
  upsertToolResultEntry(
782
954
  state,
@@ -799,45 +971,100 @@ function applyTranscriptEvent(state: BtwTranscriptState, event: AgentSessionEven
799
971
  }
800
972
  }
801
973
 
802
- function appendPersistedTranscriptTurn(state: BtwTranscriptState, details: BtwDetails): void {
974
+ function appendPersistedTranscriptTurn(
975
+ state: BtwTranscriptState,
976
+ details: BtwDetails,
977
+ ): void {
803
978
  const turnId = ensureTranscriptTurn(state);
804
979
  upsertUserMessageEntry(state, turnId, details.question);
805
980
  if (details.thinking) {
806
- upsertTranscriptTextEntry(state, turnId, "thinking", details.thinking, false);
981
+ upsertTranscriptTextEntry(
982
+ state,
983
+ turnId,
984
+ "thinking",
985
+ details.thinking,
986
+ false,
987
+ );
807
988
  }
808
- upsertTranscriptTextEntry(state, turnId, "assistant-text", details.answer, false);
989
+ upsertTranscriptTextEntry(
990
+ state,
991
+ turnId,
992
+ "assistant-text",
993
+ details.answer,
994
+ false,
995
+ );
809
996
  finishTranscriptTurn(state, turnId);
810
997
  }
811
998
 
812
- function setTranscriptFailure(state: BtwTranscriptState, message: string): void {
813
- const turnId = state.currentTurnId ?? state.lastTurnId ?? ensureTranscriptTurn(state);
814
- 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
+ );
815
1012
  finishTranscriptTurn(state, turnId);
816
1013
  }
817
1014
 
818
1015
  function hasStreamingTranscriptEntry(entries: BtwTranscript): boolean {
819
1016
  return entries.some(
820
1017
  (entry) =>
821
- (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") &&
822
1021
  entry.streaming,
823
1022
  );
824
1023
  }
825
1024
 
826
1025
  function getCompletedExchangeCount(entries: BtwTranscript): number {
827
- 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;
828
1029
  }
829
1030
 
830
- function buildOverlayTranscript(entries: BtwTranscript, theme: ExtensionContext["ui"]["theme"]): string[] {
1031
+ function buildOverlayTranscript(
1032
+ entries: BtwTranscript,
1033
+ theme: ExtensionContext["ui"]["theme"],
1034
+ ): string[] {
831
1035
  if (entries.length === 0) {
832
1036
  return [theme.fg("dim", "暂无 BTW 对话。输入侧边问题开始。")];
833
1037
  }
834
1038
 
835
1039
  const lines: string[] = [];
836
- const userBadge = buildTranscriptBadge(theme, "You", "userMessageBg", "accent");
837
- const thinkingBadge = buildTranscriptBadge(theme, "Thinking", "toolPendingBg", "warning");
838
- const toolBadge = buildTranscriptBadge(theme, "Tool", "toolPendingBg", "warning");
839
- const assistantBadge = buildTranscriptBadge(theme, "Assistant", "customMessageBg", "success");
840
- 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
+ );
841
1068
  const blockIndent = " ";
842
1069
  const resultIndent = blockIndent;
843
1070
 
@@ -868,7 +1095,11 @@ function buildOverlayTranscript(entries: BtwTranscript, theme: ExtensionContext[
868
1095
  const pushStackedBlock = (
869
1096
  header: string,
870
1097
  text: string,
871
- options: { blankBefore?: boolean; indent?: string; style?: (value: string) => string } = {},
1098
+ options: {
1099
+ blankBefore?: boolean;
1100
+ indent?: string;
1101
+ style?: (value: string) => string;
1102
+ } = {},
872
1103
  ) => {
873
1104
  const bodyLines = text.split("\n");
874
1105
  const indent = options.indent ?? blockIndent;
@@ -898,7 +1129,9 @@ function buildOverlayTranscript(entries: BtwTranscript, theme: ExtensionContext[
898
1129
  }
899
1130
 
900
1131
  if (entry.type === "thinking") {
901
- const thinkingHeader = entry.streaming ? `${thinkingBadge} ${theme.fg("warning", "▍")}` : thinkingBadge;
1132
+ const thinkingHeader = entry.streaming
1133
+ ? `${thinkingBadge} ${theme.fg("warning", "▍")}`
1134
+ : thinkingBadge;
902
1135
  pushStackedBlock(thinkingHeader, entry.text, {
903
1136
  style: (line) => theme.fg("warning", theme.italic(line)),
904
1137
  });
@@ -918,17 +1151,26 @@ function buildOverlayTranscript(entries: BtwTranscript, theme: ExtensionContext[
918
1151
  : entry.streaming
919
1152
  ? theme.fg("warning", "↳ 流式输出中")
920
1153
  : theme.fg("dim", "↳ result");
921
- const truncationLabel = entry.truncated ? theme.fg("dim", " (truncated)") : "";
922
- pushStackedBlock(`${resultHeaderLabel}${truncationLabel}`, entry.content, {
923
- blankBefore: false,
924
- indent: resultIndent,
925
- style: (line) => (entry.isError ? theme.fg("error", line) : theme.fg("dim", line)),
926
- });
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
+ );
927
1167
  continue;
928
1168
  }
929
1169
 
930
1170
  if (entry.type === "assistant-text") {
931
- const assistantHeader = entry.streaming ? `${assistantBadge} ${theme.fg("warning", "▍")}` : assistantBadge;
1171
+ const assistantHeader = entry.streaming
1172
+ ? `${assistantBadge} ${theme.fg("warning", "▍")}`
1173
+ : assistantBadge;
932
1174
  pushStackedBlock(assistantHeader, entry.text);
933
1175
  }
934
1176
  }
@@ -936,7 +1178,9 @@ function buildOverlayTranscript(entries: BtwTranscript, theme: ExtensionContext[
936
1178
  return lines;
937
1179
  }
938
1180
 
939
- function getLastAssistantMessage(session: AgentSession): AssistantMessage | null {
1181
+ function getLastAssistantMessage(
1182
+ session: AgentSession,
1183
+ ): AssistantMessage | null {
940
1184
  for (let i = session.state.messages.length - 1; i >= 0; i--) {
941
1185
  const message = session.state.messages[i];
942
1186
  if (message.role === "assistant") {
@@ -957,10 +1201,18 @@ function buildBtwMessageContent(question: string, answer: string): string {
957
1201
  }
958
1202
 
959
1203
  function formatThread(thread: BtwHandoffExchange[]): string {
960
- 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");
961
1210
  }
962
1211
 
963
- function isThreadContinuationMarker(messages: Message[], index: number): boolean {
1212
+ function isThreadContinuationMarker(
1213
+ messages: Message[],
1214
+ index: number,
1215
+ ): boolean {
964
1216
  const userMessage = messages[index];
965
1217
  const assistantMessage = messages[index + 1];
966
1218
  return (
@@ -971,9 +1223,18 @@ function isThreadContinuationMarker(messages: Message[], index: number): boolean
971
1223
  );
972
1224
  }
973
1225
 
974
- function extractBtwHandoffThread(sessionRuntime: BtwSessionRuntime): BtwHandoffExchange[] {
975
- const handoffMessages = sessionRuntime.session.state.messages.slice(sessionRuntime.sideThreadStartIndex);
976
- 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;
977
1238
  const exchanges: BtwHandoffExchange[] = [];
978
1239
  let currentUser = "";
979
1240
  let currentAssistant = "";
@@ -1007,7 +1268,9 @@ function extractBtwHandoffThread(sessionRuntime: BtwSessionRuntime): BtwHandoffE
1007
1268
  continue;
1008
1269
  }
1009
1270
 
1010
- currentAssistant = currentAssistant ? `${currentAssistant}\n\n${text}` : text;
1271
+ currentAssistant = currentAssistant
1272
+ ? `${currentAssistant}\n\n${text}`
1273
+ : text;
1011
1274
  }
1012
1275
 
1013
1276
  pushCurrent();
@@ -1040,7 +1303,11 @@ function saveVisibleBtwNote(
1040
1303
  return "saved";
1041
1304
  }
1042
1305
 
1043
- 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 {
1044
1311
  if (ctx.hasUI) {
1045
1312
  ctx.ui.notify(message, level);
1046
1313
  }
@@ -1166,7 +1433,10 @@ class BtwOverlayComponent extends Container implements Focusable {
1166
1433
  private borderLine(innerWidth: number, edge: "top" | "bottom"): string {
1167
1434
  const left = edge === "top" ? "┌" : "└";
1168
1435
  const right = edge === "top" ? "┐" : "┘";
1169
- return this.theme.fg("borderMuted", `${left}${"─".repeat(innerWidth)}${right}`);
1436
+ return this.theme.fg(
1437
+ "borderMuted",
1438
+ `${left}${"─".repeat(innerWidth)}${right}`,
1439
+ );
1170
1440
  }
1171
1441
 
1172
1442
  private wrapTranscript(innerWidth: number): string[] {
@@ -1194,13 +1464,20 @@ class BtwOverlayComponent extends Container implements Focusable {
1194
1464
 
1195
1465
  if (matchesKey(data, Key.pageUp)) {
1196
1466
  this.followTranscript = false;
1197
- 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
+ );
1198
1472
  this.tui.requestRender();
1199
1473
  return;
1200
1474
  }
1201
1475
 
1202
1476
  if (matchesKey(data, Key.pageDown)) {
1203
- this.transcriptScrollOffset += Math.max(1, this.transcriptViewportHeight - 1);
1477
+ this.transcriptScrollOffset += Math.max(
1478
+ 1,
1479
+ this.transcriptViewportHeight - 1,
1480
+ );
1204
1481
  this.tui.requestRender();
1205
1482
  return;
1206
1483
  }
@@ -1237,7 +1514,10 @@ class BtwOverlayComponent extends Container implements Focusable {
1237
1514
  if (this.followTranscript) {
1238
1515
  this.transcriptScrollOffset = maxScroll;
1239
1516
  } else {
1240
- this.transcriptScrollOffset = Math.max(0, Math.min(this.transcriptScrollOffset, maxScroll));
1517
+ this.transcriptScrollOffset = Math.max(
1518
+ 0,
1519
+ Math.min(this.transcriptScrollOffset, maxScroll),
1520
+ );
1241
1521
  if (this.transcriptScrollOffset >= maxScroll) {
1242
1522
  this.followTranscript = true;
1243
1523
  }
@@ -1247,7 +1527,10 @@ class BtwOverlayComponent extends Container implements Focusable {
1247
1527
  this.transcriptScrollOffset,
1248
1528
  this.transcriptScrollOffset + transcriptHeight,
1249
1529
  );
1250
- const transcriptPadCount = Math.max(0, transcriptHeight - visibleTranscript.length);
1530
+ const transcriptPadCount = Math.max(
1531
+ 0,
1532
+ transcriptHeight - visibleTranscript.length,
1533
+ );
1251
1534
  const hiddenAbove = this.transcriptScrollOffset;
1252
1535
  const hiddenBelow = Math.max(0, maxScroll - this.transcriptScrollOffset);
1253
1536
  const summary =
@@ -1257,7 +1540,12 @@ class BtwOverlayComponent extends Container implements Focusable {
1257
1540
 
1258
1541
  const lines = [this.borderLine(innerWidth, "top")];
1259
1542
 
1260
- 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
+ );
1261
1549
  lines.push(this.frameLine(this.theme.fg("dim", summary), innerWidth));
1262
1550
  lines.push(this.ruleLine(innerWidth));
1263
1551
 
@@ -1269,9 +1557,19 @@ class BtwOverlayComponent extends Container implements Focusable {
1269
1557
  }
1270
1558
 
1271
1559
  lines.push(this.ruleLine(innerWidth));
1272
- 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
+ );
1273
1566
  lines.push(this.inputFrameLine(dialogWidth));
1274
- 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
+ );
1275
1573
  lines.push(this.borderLine(innerWidth, "bottom"));
1276
1574
 
1277
1575
  return lines;
@@ -1295,7 +1593,9 @@ class BtwOverlayComponent extends Container implements Focusable {
1295
1593
  this.modeText.setText(this.modeTextValue);
1296
1594
  const entries = this.readTranscriptEntries();
1297
1595
  const exchanges = getCompletedExchangeCount(entries);
1298
- const active = hasStreamingTranscriptEntry(entries) ? " · 流式输出中" : " · 空闲";
1596
+ const active = hasStreamingTranscriptEntry(entries)
1597
+ ? " · 流式输出中"
1598
+ : " · 空闲";
1299
1599
  this.summaryTextValue = `${exchanges} 轮对话${active}`;
1300
1600
  this.summaryText.setText(this.summaryTextValue);
1301
1601
 
@@ -1308,7 +1608,8 @@ class BtwOverlayComponent extends Container implements Focusable {
1308
1608
  const status = this.getStatus() ?? "就绪。Enter 提交,Escape 取消。";
1309
1609
  this.statusTextValue = status;
1310
1610
  this.statusText.setText(this.statusTextValue);
1311
- this.hintsTextValue = "Enter 提交 · Alt+/ 切换焦点 · Escape 取消 · PgUp/PgDn 滚动";
1611
+ this.hintsTextValue =
1612
+ "Enter 提交 · Alt+/ 切换焦点 · Escape 取消 · PgUp/PgDn 滚动";
1312
1613
  this.hintsText.setText(this.hintsTextValue);
1313
1614
  this.tui.requestRender();
1314
1615
  }
@@ -1335,7 +1636,10 @@ export default function (pi: ExtensionAPI) {
1335
1636
  }
1336
1637
  }
1337
1638
 
1338
- function setOverlayStatus(status: string | null, ctx?: ExtensionContext | ExtensionCommandContext): void {
1639
+ function setOverlayStatus(
1640
+ status: string | null,
1641
+ ctx?: ExtensionContext | ExtensionCommandContext,
1642
+ ): void {
1339
1643
  overlayStatus = status;
1340
1644
  syncUi(ctx);
1341
1645
  }
@@ -1376,7 +1680,10 @@ export default function (pi: ExtensionAPI) {
1376
1680
  overlayRuntime?.refresh?.();
1377
1681
  }
1378
1682
 
1379
- function removeBtwSessionSubscription(sessionRuntime: BtwSessionRuntime, unsubscribe: () => void): void {
1683
+ function removeBtwSessionSubscription(
1684
+ sessionRuntime: BtwSessionRuntime,
1685
+ unsubscribe: () => void,
1686
+ ): void {
1380
1687
  if (!sessionRuntime.subscriptions.delete(unsubscribe)) {
1381
1688
  return;
1382
1689
  }
@@ -1385,11 +1692,13 @@ export default function (pi: ExtensionAPI) {
1385
1692
  unsubscribe();
1386
1693
  } catch (error) {
1387
1694
  // ✅ 改进错误处理:记录错误而不忽略
1388
- console.warn('[btw] Unsubscribe failed during session removal:', error);
1695
+ console.warn("[btw] Unsubscribe failed during session removal:", error);
1389
1696
  }
1390
1697
  }
1391
1698
 
1392
- function clearBtwSessionSubscriptions(sessionRuntime: BtwSessionRuntime): void {
1699
+ function clearBtwSessionSubscriptions(
1700
+ sessionRuntime: BtwSessionRuntime,
1701
+ ): void {
1393
1702
  for (const unsubscribe of [...sessionRuntime.subscriptions]) {
1394
1703
  removeBtwSessionSubscription(sessionRuntime, unsubscribe);
1395
1704
  }
@@ -1400,7 +1709,10 @@ export default function (pi: ExtensionAPI) {
1400
1709
  event: AgentSessionEvent,
1401
1710
  ctx?: ExtensionContext | ExtensionCommandContext,
1402
1711
  ): void {
1403
- if (activeBtwSession?.session !== sessionRuntime.session || !overlayRuntime) {
1712
+ if (
1713
+ activeBtwSession?.session !== sessionRuntime.session ||
1714
+ !overlayRuntime
1715
+ ) {
1404
1716
  return;
1405
1717
  }
1406
1718
 
@@ -1412,7 +1724,12 @@ export default function (pi: ExtensionAPI) {
1412
1724
  }
1413
1725
 
1414
1726
  if (event.type === "tool_execution_end") {
1415
- setOverlayStatus(sessionRuntime.session.isStreaming ? `⏳ 运行工具: ${event.toolName}` : "⏳ 流式输出中...", ctx);
1727
+ setOverlayStatus(
1728
+ sessionRuntime.session.isStreaming
1729
+ ? `⏳ 运行工具: ${event.toolName}`
1730
+ : "⏳ 流式输出中...",
1731
+ ctx,
1732
+ );
1416
1733
  return;
1417
1734
  }
1418
1735
 
@@ -1431,15 +1748,19 @@ export default function (pi: ExtensionAPI) {
1431
1748
  }
1432
1749
  }
1433
1750
 
1434
- function subscribeOverlayToActiveBtwSession(ctx?: ExtensionContext | ExtensionCommandContext): void {
1751
+ function subscribeOverlayToActiveBtwSession(
1752
+ ctx?: ExtensionContext | ExtensionCommandContext,
1753
+ ): void {
1435
1754
  const sessionRuntime = activeBtwSession;
1436
1755
  if (!sessionRuntime || sessionRuntime.subscriptions.size > 0) {
1437
1756
  return;
1438
1757
  }
1439
1758
 
1440
- const unsubscribe = sessionRuntime.session.subscribe((event: AgentSessionEvent) => {
1441
- handleBtwSessionEvent(sessionRuntime, event, ctx);
1442
- });
1759
+ const unsubscribe = sessionRuntime.session.subscribe(
1760
+ (event: AgentSessionEvent) => {
1761
+ handleBtwSessionEvent(sessionRuntime, event, ctx);
1762
+ },
1763
+ );
1443
1764
  sessionRuntime.subscriptions.add(unsubscribe);
1444
1765
  }
1445
1766
 
@@ -1462,7 +1783,7 @@ export default function (pi: ExtensionAPI) {
1462
1783
  await current.session.abort();
1463
1784
  } catch (error) {
1464
1785
  // ✅ 改进错误处理:记录错误而不忽略
1465
- console.warn('[btw] Session abort failed during disposal:', error);
1786
+ console.warn("[btw] Session abort failed during disposal:", error);
1466
1787
  }
1467
1788
 
1468
1789
  current.session.dispose();
@@ -1478,7 +1799,8 @@ export default function (pi: ExtensionAPI) {
1478
1799
  notifyOnFallback = false,
1479
1800
  ): Promise<ResolvedBtwModel> {
1480
1801
  if (btwModelOverride) {
1481
- const auth = await ctx.modelRegistry.getApiKeyAndHeaders(btwModelOverride);
1802
+ const auth =
1803
+ await ctx.modelRegistry.getApiKeyAndHeaders(btwModelOverride);
1482
1804
  if (auth.ok && auth.apiKey) {
1483
1805
  return {
1484
1806
  model: btwModelOverride,
@@ -1533,7 +1855,8 @@ export default function (pi: ExtensionAPI) {
1533
1855
  notifyOnFallback = false,
1534
1856
  ): Promise<ResolvedBtwSettings> {
1535
1857
  const resolvedModel = await resolveBtwModel(ctx, notifyOnFallback);
1536
- const thinkingLevel = btwThinkingOverride ?? (pi.getThinkingLevel() as SessionThinkingLevel);
1858
+ const thinkingLevel =
1859
+ btwThinkingOverride ?? (pi.getThinkingLevel() as SessionThinkingLevel);
1537
1860
 
1538
1861
  return {
1539
1862
  model: resolvedModel.model,
@@ -1565,14 +1888,26 @@ export default function (pi: ExtensionAPI) {
1565
1888
  }
1566
1889
 
1567
1890
  function describeResolvedThinking(settings: ResolvedBtwSettings): string {
1568
- const source = settings.thinkingSource === "override" ? "override" : "inherits main thread";
1891
+ const source =
1892
+ settings.thinkingSource === "override"
1893
+ ? "override"
1894
+ : "inherits main thread";
1569
1895
  return `BTW thinking: ${settings.thinkingLevel} (${source}).`;
1570
1896
  }
1571
1897
 
1572
- async function setBtwModelOverride(ctx: ExtensionCommandContext, nextModel: SessionModel | null): Promise<void> {
1898
+ async function setBtwModelOverride(
1899
+ ctx: ExtensionCommandContext,
1900
+ nextModel: SessionModel | null,
1901
+ ): Promise<void> {
1573
1902
  btwModelOverride = nextModel;
1574
1903
  const details: BtwModelOverrideDetails = nextModel
1575
- ? { 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
+ }
1576
1911
  : { action: "clear", timestamp: Date.now() };
1577
1912
  pi.appendEntry(BTW_MODEL_OVERRIDE_TYPE, details);
1578
1913
  await disposeBtwSession();
@@ -1590,7 +1925,11 @@ export default function (pi: ExtensionAPI) {
1590
1925
  ): Promise<void> {
1591
1926
  btwThinkingOverride = nextThinkingLevel;
1592
1927
  const details: BtwThinkingOverrideDetails = nextThinkingLevel
1593
- ? { action: "set", timestamp: Date.now(), thinkingLevel: nextThinkingLevel }
1928
+ ? {
1929
+ action: "set",
1930
+ timestamp: Date.now(),
1931
+ thinkingLevel: nextThinkingLevel,
1932
+ }
1594
1933
  : { action: "clear", timestamp: Date.now() };
1595
1934
  pi.appendEntry(BTW_THINKING_OVERRIDE_TYPE, details);
1596
1935
  await disposeBtwSession();
@@ -1602,7 +1941,10 @@ export default function (pi: ExtensionAPI) {
1602
1941
  notify(ctx, `${message} ${describeResolvedThinking(settings)}`, "info");
1603
1942
  }
1604
1943
 
1605
- async function createBtwSubSession(ctx: ExtensionCommandContext, mode: BtwThreadMode): Promise<BtwSessionRuntime> {
1944
+ async function createBtwSubSession(
1945
+ ctx: ExtensionCommandContext,
1946
+ mode: BtwThreadMode,
1947
+ ): Promise<BtwSessionRuntime> {
1606
1948
  const settings = await resolveBtwSettings(ctx, true);
1607
1949
  if (!settings.model) {
1608
1950
  throw new Error(settings.fallbackReason || "No active model selected.");
@@ -1611,22 +1953,32 @@ export default function (pi: ExtensionAPI) {
1611
1953
  const { session } = await createAgentSession({
1612
1954
  sessionManager: SessionManager.inMemory(),
1613
1955
  model: settings.model,
1614
- modelRegistry: ctx.modelRegistry as AgentSession["modelRegistry"],
1615
- thinkingLevel: settings.thinkingLevel,
1956
+ // pi 1.1.0: AgentSession 改用 ModelRuntime, 但 ModelRegistry 实现了 Models 接口
1957
+ modelRuntime:
1958
+ ctx.modelRegistry as unknown as CreateAgentSessionOptions["modelRuntime"],
1616
1959
  // Match pi's default coding-agent toolset (read/bash/edit/write).
1617
1960
  tools: ["read", "bash", "edit", "write"],
1618
1961
  resourceLoader: createBtwResourceLoader(ctx),
1619
1962
  });
1620
1963
 
1621
- 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
+ );
1622
1970
  if (seedMessages.length > 0) {
1623
- session.agent.state.messages = seedMessages as typeof session.state.messages;
1971
+ session.agent.state.messages =
1972
+ seedMessages as typeof session.state.messages;
1624
1973
  }
1625
1974
 
1626
1975
  return { session, mode, subscriptions: new Set(), sideThreadStartIndex };
1627
1976
  }
1628
1977
 
1629
- async function ensureBtwSession(ctx: ExtensionCommandContext, mode: BtwThreadMode): Promise<BtwSessionRuntime | null> {
1978
+ async function ensureBtwSession(
1979
+ ctx: ExtensionCommandContext,
1980
+ mode: BtwThreadMode,
1981
+ ): Promise<BtwSessionRuntime | null> {
1630
1982
  const settings = await resolveBtwSettings(ctx);
1631
1983
  if (!settings.model) {
1632
1984
  return null;
@@ -1641,7 +1993,9 @@ export default function (pi: ExtensionAPI) {
1641
1993
  return activeBtwSession;
1642
1994
  }
1643
1995
 
1644
- async function ensureOverlay(ctx: ExtensionCommandContext | ExtensionContext): Promise<void> {
1996
+ async function ensureOverlay(
1997
+ ctx: ExtensionCommandContext | ExtensionContext,
1998
+ ): Promise<void> {
1645
1999
  if (!ctx.hasUI) {
1646
2000
  return;
1647
2001
  }
@@ -1743,11 +2097,19 @@ export default function (pi: ExtensionAPI) {
1743
2097
  if (overlayRuntime === runtime) {
1744
2098
  overlayRuntime = null;
1745
2099
  }
1746
- 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
+ );
1747
2105
  });
1748
2106
  }
1749
2107
 
1750
- 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> {
1751
2113
  const trimmedArgs = args.trim();
1752
2114
 
1753
2115
  if (name === "btw") {
@@ -1845,7 +2207,10 @@ export default function (pi: ExtensionAPI) {
1845
2207
  return true;
1846
2208
  }
1847
2209
 
1848
- await setBtwThinkingOverride(ctx, parsed.action === "clear" ? null : parsed.thinkingLevel);
2210
+ await setBtwThinkingOverride(
2211
+ ctx,
2212
+ parsed.action === "clear" ? null : parsed.thinkingLevel,
2213
+ );
1849
2214
  return true;
1850
2215
  }
1851
2216
 
@@ -1872,7 +2237,11 @@ export default function (pi: ExtensionAPI) {
1872
2237
  notify(ctx, `已注入 BTW 线程(${count} 轮对话)。`, "info");
1873
2238
  } catch (error) {
1874
2239
  setOverlayStatus("注入失败。线程已保留,可重试或总结。", ctx);
1875
- 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
+ );
1876
2245
  }
1877
2246
  return true;
1878
2247
  }
@@ -1901,7 +2270,11 @@ export default function (pi: ExtensionAPI) {
1901
2270
  notify(ctx, `已注入 BTW 总结(${count} 轮对话)。`, "info");
1902
2271
  } catch (error) {
1903
2272
  setOverlayStatus("总结失败。线程已保留,可重试或注入。", ctx);
1904
- 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
+ );
1905
2278
  }
1906
2279
  return true;
1907
2280
  }
@@ -1909,9 +2282,13 @@ export default function (pi: ExtensionAPI) {
1909
2282
  return false;
1910
2283
  }
1911
2284
 
1912
- function parseOverlayBtwCommand(value: string): { name: string; args: string } | null {
2285
+ function parseOverlayBtwCommand(
2286
+ value: string,
2287
+ ): { name: string; args: string } | null {
1913
2288
  const trimmed = value.trim();
1914
- 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
+ );
1915
2292
  if (!match) {
1916
2293
  return null;
1917
2294
  }
@@ -1922,7 +2299,10 @@ export default function (pi: ExtensionAPI) {
1922
2299
  };
1923
2300
  }
1924
2301
 
1925
- async function submitFromOverlay(ctx: ExtensionCommandContext | ExtensionContext, value: string): Promise<void> {
2302
+ async function submitFromOverlay(
2303
+ ctx: ExtensionCommandContext | ExtensionContext,
2304
+ value: string,
2305
+ ): Promise<void> {
1926
2306
  const question = value.trim();
1927
2307
  if (!question) {
1928
2308
  setOverlayStatus("Enter a BTW prompt before submitting.", ctx);
@@ -1930,7 +2310,10 @@ export default function (pi: ExtensionAPI) {
1930
2310
  }
1931
2311
 
1932
2312
  if (!("getSystemPrompt" in ctx)) {
1933
- 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
+ );
1934
2317
  return;
1935
2318
  }
1936
2319
 
@@ -1984,7 +2367,9 @@ export default function (pi: ExtensionAPI) {
1984
2367
 
1985
2368
  for (let i = 0; i < branch.length; i++) {
1986
2369
  if (isCustomEntry(branch[i], BTW_MODEL_OVERRIDE_TYPE)) {
1987
- const details = (branch[i] as unknown as { data?: BtwModelOverrideDetails }).data;
2370
+ const details = (
2371
+ branch[i] as unknown as { data?: BtwModelOverrideDetails }
2372
+ ).data;
1988
2373
  if (details?.action === "set") {
1989
2374
  const resolved = ctx.modelRegistry.find(details.provider, details.id);
1990
2375
  if (resolved) {
@@ -1999,7 +2384,9 @@ export default function (pi: ExtensionAPI) {
1999
2384
  }
2000
2385
 
2001
2386
  if (isCustomEntry(branch[i], BTW_THINKING_OVERRIDE_TYPE)) {
2002
- const details = (branch[i] as unknown as { data?: BtwThinkingOverrideDetails }).data;
2387
+ const details = (
2388
+ branch[i] as unknown as { data?: BtwThinkingOverrideDetails }
2389
+ ).data;
2003
2390
  btwThinkingOverride =
2004
2391
  details?.action === "set"
2005
2392
  ? details.thinkingLevel
@@ -2010,7 +2397,8 @@ export default function (pi: ExtensionAPI) {
2010
2397
 
2011
2398
  if (isCustomEntry(branch[i], BTW_RESET_TYPE)) {
2012
2399
  lastResetIndex = i;
2013
- const details = (branch[i] as unknown as { data?: BtwResetDetails }).data;
2400
+ const details = (branch[i] as unknown as { data?: BtwResetDetails })
2401
+ .data;
2014
2402
  pendingMode = details?.mode ?? "contextual";
2015
2403
  }
2016
2404
  }
@@ -2055,7 +2443,9 @@ export default function (pi: ExtensionAPI) {
2055
2443
 
2056
2444
  const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
2057
2445
  if (!auth.ok || !auth.apiKey) {
2058
- 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;
2059
2449
  setOverlayStatus(message, ctx);
2060
2450
  notify(ctx, message, "error");
2061
2451
  await ensureOverlay(ctx);
@@ -2088,7 +2478,10 @@ export default function (pi: ExtensionAPI) {
2088
2478
  throw new Error("BTW request finished without a response.");
2089
2479
  }
2090
2480
  if (response.stopReason === "aborted") {
2091
- removeTranscriptTurn(transcriptState, transcriptState.lastTurnId ?? transcriptState.currentTurnId);
2481
+ removeTranscriptTurn(
2482
+ transcriptState,
2483
+ transcriptState.lastTurnId ?? transcriptState.currentTurnId,
2484
+ );
2092
2485
  setOverlayStatus("Request aborted.", ctx);
2093
2486
  return;
2094
2487
  }
@@ -2096,9 +2489,16 @@ export default function (pi: ExtensionAPI) {
2096
2489
  throw new Error(response.errorMessage || "BTW request failed.");
2097
2490
  }
2098
2491
 
2099
- const completedTurnId = transcriptState.lastTurnId ?? transcriptState.currentTurnId;
2492
+ const completedTurnId =
2493
+ transcriptState.lastTurnId ?? transcriptState.currentTurnId;
2100
2494
  const streamedThinking =
2101
- completedTurnId !== null ? findLatestTranscriptEntry(transcriptState, completedTurnId, "thinking")?.text : "";
2495
+ completedTurnId !== null
2496
+ ? findLatestTranscriptEntry(
2497
+ transcriptState,
2498
+ completedTurnId,
2499
+ "thinking",
2500
+ )?.text
2501
+ : "";
2102
2502
  const answer = extractAnswer(response);
2103
2503
  const thinking = extractThinking(response) || streamedThinking || "";
2104
2504
 
@@ -2125,12 +2525,19 @@ export default function (pi: ExtensionAPI) {
2125
2525
  notify(ctx, "已排队,将在当前轮次结束后保存到主对话。", "info");
2126
2526
  setOverlayStatus("已排队,将在当前轮次结束后保存到主对话。", ctx);
2127
2527
  } else {
2128
- 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
+ );
2129
2532
  }
2130
2533
  } catch (error) {
2131
- const errorMessage = error instanceof Error ? error.message : String(error);
2534
+ const errorMessage =
2535
+ error instanceof Error ? error.message : String(error);
2132
2536
  setTranscriptFailure(transcriptState, errorMessage);
2133
- 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
+ );
2134
2541
  notify(ctx, errorMessage, "error");
2135
2542
  await disposeBtwSession();
2136
2543
  } finally {
@@ -2139,15 +2546,23 @@ export default function (pi: ExtensionAPI) {
2139
2546
  }
2140
2547
 
2141
2548
  function getPendingThreadForHandoff(): BtwHandoffExchange[] {
2142
- return pendingThread.map((entry) => ({ user: entry.question, assistant: entry.answer }));
2143
- }
2144
-
2145
- async function getBtwHandoffThread(
2146
- ctx: ExtensionCommandContext,
2147
- ): Promise<{ sessionRuntime: BtwSessionRuntime | null; thread: BtwHandoffExchange[] }> {
2148
- const sessionRuntime = activeBtwSession ?? (await ensureBtwSession(ctx, pendingMode));
2149
- const thread = sessionRuntime ? extractBtwHandoffThread(sessionRuntime) : [];
2150
- 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();
2151
2566
 
2152
2567
  if (resolvedThread.length === 0) {
2153
2568
  throw new Error("无可用 BTW 对话用于交接。");
@@ -2156,7 +2571,10 @@ export default function (pi: ExtensionAPI) {
2156
2571
  return { sessionRuntime, thread: resolvedThread };
2157
2572
  }
2158
2573
 
2159
- async function summarizeThread(ctx: ExtensionCommandContext, thread: BtwHandoffExchange[]): Promise<string> {
2574
+ async function summarizeThread(
2575
+ ctx: ExtensionCommandContext,
2576
+ thread: BtwHandoffExchange[],
2577
+ ): Promise<string> {
2160
2578
  const settings = await resolveBtwSettings(ctx, true);
2161
2579
  const model = settings.model;
2162
2580
  if (!model) {
@@ -2165,16 +2583,23 @@ export default function (pi: ExtensionAPI) {
2165
2583
 
2166
2584
  const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
2167
2585
  if (!auth.ok || !auth.apiKey) {
2168
- 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
+ );
2169
2591
  }
2170
2592
 
2171
2593
  const { session } = await createAgentSession({
2172
2594
  sessionManager: SessionManager.inMemory(),
2173
2595
  model,
2174
- modelRegistry: ctx.modelRegistry as AgentSession["modelRegistry"],
2175
- thinkingLevel: "off",
2596
+ // pi 1.1.0: AgentSession 改用 ModelRuntime, 但 ModelRegistry 实现了 Models 接口
2597
+ modelRuntime:
2598
+ ctx.modelRegistry as unknown as CreateAgentSessionOptions["modelRuntime"],
2176
2599
  tools: [],
2177
- resourceLoader: createBtwResourceLoader(ctx, [BTW_SUMMARIZE_SYSTEM_PROMPT]),
2600
+ resourceLoader: createBtwResourceLoader(ctx, [
2601
+ BTW_SUMMARIZE_SYSTEM_PROMPT,
2602
+ ]),
2178
2603
  });
2179
2604
 
2180
2605
  try {
@@ -2185,7 +2610,9 @@ export default function (pi: ExtensionAPI) {
2185
2610
  throw new Error("BTW summarize finished without a response.");
2186
2611
  }
2187
2612
  if (response.stopReason === "error") {
2188
- throw new Error(response.errorMessage || "Failed to summarize BTW thread.");
2613
+ throw new Error(
2614
+ response.errorMessage || "Failed to summarize BTW thread.",
2615
+ );
2189
2616
  }
2190
2617
  if (response.stopReason === "aborted") {
2191
2618
  throw new Error("BTW summarize aborted.");
@@ -2197,13 +2624,19 @@ export default function (pi: ExtensionAPI) {
2197
2624
  await session.abort();
2198
2625
  } catch (error) {
2199
2626
  // ✅ 改进错误处理:记录错误而不忽略
2200
- console.warn('[btw] Session abort failed during summarize shutdown:', error);
2627
+ console.warn(
2628
+ "[btw] Session abort failed during summarize shutdown:",
2629
+ error,
2630
+ );
2201
2631
  }
2202
2632
  session.dispose();
2203
2633
  }
2204
2634
  }
2205
2635
 
2206
- function sendThreadToMain(ctx: ExtensionCommandContext, content: string): void {
2636
+ function sendThreadToMain(
2637
+ ctx: ExtensionCommandContext,
2638
+ content: string,
2639
+ ): void {
2207
2640
  if (ctx.isIdle()) {
2208
2641
  pi.sendUserMessage(content);
2209
2642
  } else {
@@ -2211,37 +2644,45 @@ export default function (pi: ExtensionAPI) {
2211
2644
  }
2212
2645
  }
2213
2646
 
2214
- pi.registerMessageRenderer(BTW_MESSAGE_TYPE, (message, { expanded }, theme) => {
2215
- const details = message.details as BtwDetails | undefined;
2216
- const content = typeof message.content === "string" ? message.content : "[non-text btw message]";
2217
- const lines = [theme.fg("accent", theme.bold("[BTW]")), content];
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];
2218
2656
 
2219
- if (expanded && details) {
2220
- lines.push(
2221
- theme.fg(
2222
- "dim",
2223
- `model: ${details.provider}/${details.model} (${details.api ?? "openai-responses"}) · thinking: ${details.thinkingLevel}`,
2224
- ),
2225
- );
2226
-
2227
- if (details.usage) {
2657
+ if (expanded && details) {
2228
2658
  lines.push(
2229
2659
  theme.fg(
2230
2660
  "dim",
2231
- `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}`,
2232
2662
  ),
2233
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
+ }
2234
2673
  }
2235
- }
2236
2674
 
2237
- const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
2238
- box.addChild(new Text(lines.join("\n"), 0, 0));
2239
- return box;
2240
- });
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
+ );
2241
2680
 
2242
2681
  pi.on("context", async (event) => {
2243
2682
  return {
2244
- messages: event.messages.filter((message) => !isVisibleBtwMessage(message)),
2683
+ messages: event.messages.filter(
2684
+ (message) => !isVisibleBtwMessage(message),
2685
+ ),
2245
2686
  };
2246
2687
  });
2247
2688
 
@@ -2268,7 +2709,8 @@ export default function (pi: ExtensionAPI) {
2268
2709
  }
2269
2710
 
2270
2711
  pi.registerCommand("btw", {
2271
- description: "侧边对话。默认带主会话上下文;用 /btw:tangent 开启无上下文模式。加 --save 保存到主对话。",
2712
+ description:
2713
+ "侧边对话。默认带主会话上下文;用 /btw:tangent 开启无上下文模式。加 --save 保存到主对话。",
2272
2714
  handler: async (args, ctx) => {
2273
2715
  await dispatchBtwCommand("btw", args, ctx);
2274
2716
  },