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