@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.
- package/bin/cli.js +92 -68
- package/config/agent/extensions/aicode-advisor.ts +187 -64
- package/config/agent/extensions/aicode-banner.ts +101 -0
- package/config/agent/extensions/aicode-btw.ts +622 -180
- package/config/agent/extensions/aicode-cd.ts +246 -63
- package/config/agent/extensions/aicode-pets.ts +0 -11
- package/config/agent/extensions/aicode-plan.ts +2 -0
- package/config/agent/extensions/aicode-safety-guard.ts +3 -0
- package/config/agent/extensions/aicode-tips.ts +1 -16
- package/config/agent/extensions/context-monitor.ts +30 -15
- package/config/agent/extensions/context.ts +132 -60
- package/config/agent/extensions/custom-footer.ts +0 -5
- package/config/agent/extensions/custom.ts +41 -29
- package/config/agent/extensions/error-monitor.ts +37 -33
- package/config/agent/extensions/goal.ts +8 -6
- package/config/agent/extensions/msg-debug.ts +103 -112
- package/config/agent/extensions/review.ts +365 -164
- package/config/agent/extensions/working-msg.ts +2 -4
- package/dist/banner.d.ts.map +1 -1
- package/dist/banner.js +8 -1
- package/dist/banner.js.map +1 -1
- package/dist/config-loader.d.ts.map +1 -1
- package/dist/config-loader.js +9 -2
- package/dist/config-loader.js.map +1 -1
- package/package.json +6 -7
- package/scripts/regression/results/.last_pid +1 -1
- package/scripts/regression/results/.last_workdir +1 -1
- package/scripts/regression/results/02-extension-loading.log +16 -27
- package/scripts/regression/results/03-prompt-stream.log +14 -23
- package/scripts/regression/results/03-rpc-protocol.log +20 -31
- package/scripts/regression/results/04-slash-commands-and-skills.log +11 -10
- package/scripts/regression/results/05-tool-execution.log +6 -4
- 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 {
|
|
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> & {
|
|
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 {
|
|
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 =
|
|
67
|
-
|
|
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<
|
|
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
|
-
| {
|
|
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
|
-
| {
|
|
130
|
-
|
|
131
|
-
|
|
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<
|
|
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: {
|
|
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(
|
|
176
|
-
|
|
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(
|
|
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 = {
|
|
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(
|
|
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(
|
|
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 {
|
|
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(
|
|
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(
|
|
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
|
-
...(
|
|
289
|
-
(
|
|
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(
|
|
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
|
-
|
|
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:
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
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(
|
|
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, {
|
|
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(
|
|
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) =>
|
|
564
|
+
(entry) =>
|
|
565
|
+
entry.turnId === resolvedTurnId &&
|
|
566
|
+
entry.type === "turn-boundary" &&
|
|
567
|
+
entry.phase === "end",
|
|
462
568
|
);
|
|
463
569
|
if (!hasEndBoundary) {
|
|
464
|
-
appendTranscriptEntry(state, {
|
|
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 (
|
|
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(
|
|
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(
|
|
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: {
|
|
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(
|
|
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(
|
|
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<
|
|
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<
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
858
|
+
upsertTranscriptTextEntry(
|
|
859
|
+
state,
|
|
860
|
+
turnId,
|
|
861
|
+
"assistant-text",
|
|
862
|
+
answer,
|
|
863
|
+
streaming,
|
|
864
|
+
);
|
|
714
865
|
}
|
|
715
866
|
}
|
|
716
867
|
|
|
717
|
-
function applyTranscriptEvent(
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
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(
|
|
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(
|
|
981
|
+
upsertTranscriptTextEntry(
|
|
982
|
+
state,
|
|
983
|
+
turnId,
|
|
984
|
+
"thinking",
|
|
985
|
+
details.thinking,
|
|
986
|
+
false,
|
|
987
|
+
);
|
|
807
988
|
}
|
|
808
|
-
upsertTranscriptTextEntry(
|
|
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(
|
|
813
|
-
|
|
814
|
-
|
|
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" ||
|
|
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(
|
|
1026
|
+
return entries.filter(
|
|
1027
|
+
(entry) => entry.type === "assistant-text" && !entry.streaming,
|
|
1028
|
+
).length;
|
|
828
1029
|
}
|
|
829
1030
|
|
|
830
|
-
function buildOverlayTranscript(
|
|
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(
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
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: {
|
|
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
|
|
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
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
975
|
-
|
|
976
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
1695
|
+
console.warn("[btw] Unsubscribe failed during session removal:", error);
|
|
1389
1696
|
}
|
|
1390
1697
|
}
|
|
1391
1698
|
|
|
1392
|
-
function clearBtwSessionSubscriptions(
|
|
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 (
|
|
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(
|
|
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(
|
|
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(
|
|
1441
|
-
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
-
? {
|
|
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
|
-
? {
|
|
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(
|
|
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
|
-
|
|
1615
|
-
|
|
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(
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
2285
|
+
function parseOverlayBtwCommand(
|
|
2286
|
+
value: string,
|
|
2287
|
+
): { name: string; args: string } | null {
|
|
1913
2288
|
const trimmed = value.trim();
|
|
1914
|
-
const match = trimmed.match(
|
|
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(
|
|
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(
|
|
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 = (
|
|
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 = (
|
|
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 })
|
|
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
|
|
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(
|
|
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 =
|
|
2492
|
+
const completedTurnId =
|
|
2493
|
+
transcriptState.lastTurnId ?? transcriptState.currentTurnId;
|
|
2100
2494
|
const streamedThinking =
|
|
2101
|
-
completedTurnId !== null
|
|
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(
|
|
2528
|
+
setOverlayStatus(
|
|
2529
|
+
"Ready for a follow-up. Hidden BTW thread updated.",
|
|
2530
|
+
ctx,
|
|
2531
|
+
);
|
|
2129
2532
|
}
|
|
2130
2533
|
} catch (error) {
|
|
2131
|
-
const errorMessage =
|
|
2534
|
+
const errorMessage =
|
|
2535
|
+
error instanceof Error ? error.message : String(error);
|
|
2132
2536
|
setTranscriptFailure(transcriptState, errorMessage);
|
|
2133
|
-
setOverlayStatus(
|
|
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) => ({
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
2175
|
-
|
|
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, [
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
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
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
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(
|
|
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:
|
|
2712
|
+
description:
|
|
2713
|
+
"侧边对话。默认带主会话上下文;用 /btw:tangent 开启无上下文模式。加 --save 保存到主对话。",
|
|
2272
2714
|
handler: async (args, ctx) => {
|
|
2273
2715
|
await dispatchBtwCommand("btw", args, ctx);
|
|
2274
2716
|
},
|