pi-btw 0.2.1 → 0.3.8
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/README.md +23 -2
- package/extensions/btw.ts +377 -65
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -15,6 +15,7 @@ A small [pi](https://github.com/badlogic/pi-mono) extension that adds a `/btw` s
|
|
|
15
15
|
- opens a focused BTW modal shell with its own composer and transcript
|
|
16
16
|
- keeps the BTW overlay open while you switch focus back to the main editor with `Alt+/`
|
|
17
17
|
- keeps BTW thread entries out of the main agent's future context
|
|
18
|
+
- supports BTW-only model and thinking overrides without changing the main thread settings
|
|
18
19
|
- lets you inject the full thread, or a summary of it, back into the main agent
|
|
19
20
|
- optionally saves an individual BTW exchange as a visible session note with `--save`
|
|
20
21
|
|
|
@@ -52,6 +53,8 @@ pi install /absolute/path/to/pi-btw
|
|
|
52
53
|
/btw --save summarize the last error in one sentence
|
|
53
54
|
/btw:new let's start a fresh thread about auth
|
|
54
55
|
/btw:tangent brainstorm from first principles without using the current chat context
|
|
56
|
+
/btw:model openai gpt-5-mini openai-responses
|
|
57
|
+
/btw:thinking low
|
|
55
58
|
/btw:inject implement the plan we just discussed
|
|
56
59
|
/btw:summarize turn that side thread into a short handoff
|
|
57
60
|
/btw:clear
|
|
@@ -105,11 +108,26 @@ pi install /absolute/path/to/pi-btw
|
|
|
105
108
|
|
|
106
109
|
### `/btw:summarize [instructions]`
|
|
107
110
|
|
|
108
|
-
- summarizes the BTW thread with the current model
|
|
111
|
+
- summarizes the BTW thread with the current effective BTW model
|
|
112
|
+
- always runs summarize with thinking off, even if BTW chat is using a thinking override
|
|
109
113
|
- injects the summary into the main agent
|
|
110
114
|
- if pi is busy, queues it as a follow-up
|
|
111
115
|
- clears the BTW thread after sending
|
|
112
116
|
|
|
117
|
+
### `/btw:model [<provider> <model> <api> | clear]`
|
|
118
|
+
|
|
119
|
+
- with no args, shows the current effective BTW model and whether it is inherited or overridden
|
|
120
|
+
- with values, sets a BTW-only model override
|
|
121
|
+
- `clear` removes the override and returns BTW to inheriting the main thread model
|
|
122
|
+
- if the configured BTW model has no credentials, BTW warns and falls back to the main thread model
|
|
123
|
+
|
|
124
|
+
### `/btw:thinking [<level> | clear]`
|
|
125
|
+
|
|
126
|
+
- with no args, shows the current effective BTW thinking level and whether it is inherited or overridden
|
|
127
|
+
- with a value, sets a BTW-only thinking override for normal BTW chat
|
|
128
|
+
- `clear` removes the override and returns BTW to inheriting the main thread thinking level
|
|
129
|
+
- changing `/btw:model` or `/btw:thinking` disposes the current BTW sub-session and applies the new settings on the next BTW prompt while preserving the hidden thread
|
|
130
|
+
|
|
113
131
|
## Behavior
|
|
114
132
|
|
|
115
133
|
### Real sub-session model
|
|
@@ -118,6 +136,8 @@ BTW is implemented as an actual pi sub-session with its own in-memory session st
|
|
|
118
136
|
|
|
119
137
|
- contextual `/btw` threads seed that sub-session from the current main-session branch while filtering out BTW-visible notes from the parent context
|
|
120
138
|
- `/btw:tangent` starts the same BTW UI in a contextless mode with no inherited main-session conversation
|
|
139
|
+
- BTW can inherit the main thread model/thinking settings or use BTW-only overrides via `/btw:model` and `/btw:thinking`
|
|
140
|
+
- `/btw:summarize` uses the current effective BTW model but keeps thinking off
|
|
121
141
|
- the overlay transcript/status line is driven from sub-session events, so tool activity, streaming deltas, failures, and recovery are all visible without scraping rendered output
|
|
122
142
|
- handoff commands (`/btw:inject` and `/btw:summarize`) read from the BTW sub-session thread rather than maintaining a separate manual transcript model
|
|
123
143
|
|
|
@@ -125,7 +145,7 @@ BTW is implemented as an actual pi sub-session with its own in-memory session st
|
|
|
125
145
|
|
|
126
146
|
Inside the BTW modal composer, slash handling is split at the BTW/session boundary:
|
|
127
147
|
|
|
128
|
-
- `/btw:new`, `/btw:tangent`, `/btw:clear`, `/btw:inject`, and `/btw:summarize` stay owned by BTW because they control BTW lifecycle or handoff behavior
|
|
148
|
+
- `/btw:new`, `/btw:tangent`, `/btw:clear`, `/btw:model`, `/btw:thinking`, `/btw:inject`, and `/btw:summarize` stay owned by BTW because they control BTW lifecycle, configuration, or handoff behavior
|
|
129
149
|
- any other slash-prefixed input is routed through the BTW sub-session's normal `prompt()` path
|
|
130
150
|
- this means ordinary pi slash commands like `/help` are handled by the sub-session instead of being rejected by a modal-only fallback
|
|
131
151
|
- if the sub-session cannot handle a slash command, BTW surfaces the real sub-session failure through the transcript/status state instead of inventing an "unsupported slash input" warning
|
|
@@ -141,6 +161,7 @@ BTW exchanges are persisted in the session as hidden custom entries so they:
|
|
|
141
161
|
- survive reloads and restarts
|
|
142
162
|
- rehydrate the BTW modal shell for the current branch
|
|
143
163
|
- preserve whether the current side thread is a normal `/btw` thread or a contextless `/btw:tangent`
|
|
164
|
+
- preserve the current BTW-only model and thinking overrides for that session history
|
|
144
165
|
- stay out of the main agent's LLM context
|
|
145
166
|
|
|
146
167
|
### Visible saved notes
|
package/extensions/btw.ts
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
type ExtensionContext,
|
|
12
12
|
type ResourceLoader,
|
|
13
13
|
} from "@mariozechner/pi-coding-agent";
|
|
14
|
-
import { type AssistantMessage, type Message, type ThinkingLevel as AiThinkingLevel } from "@mariozechner/pi-ai";
|
|
14
|
+
import { type AssistantMessage, type Message, type ThinkingLevel as AiThinkingLevel, type UserMessage } from "@mariozechner/pi-ai";
|
|
15
15
|
import {
|
|
16
16
|
Box,
|
|
17
17
|
Container,
|
|
@@ -31,6 +31,8 @@ import {
|
|
|
31
31
|
const BTW_MESSAGE_TYPE = "btw-note";
|
|
32
32
|
const BTW_ENTRY_TYPE = "btw-thread-entry";
|
|
33
33
|
const BTW_RESET_TYPE = "btw-thread-reset";
|
|
34
|
+
const BTW_MODEL_OVERRIDE_TYPE = "btw-model-override";
|
|
35
|
+
const BTW_THINKING_OVERRIDE_TYPE = "btw-thinking-override";
|
|
34
36
|
const BTW_FOCUS_SHORTCUTS = [Key.alt("/"), Key.ctrlAlt("w")] as const;
|
|
35
37
|
|
|
36
38
|
function matchesBtwFocusShortcut(data: string): boolean {
|
|
@@ -53,6 +55,7 @@ const BTW_CONTINUE_THREAD_ASSISTANT_TEXT = "Understood, continuing our side conv
|
|
|
53
55
|
|
|
54
56
|
type SessionThinkingLevel = "off" | AiThinkingLevel;
|
|
55
57
|
type BtwThreadMode = "contextual" | "tangent";
|
|
58
|
+
type SessionModel = NonNullable<ExtensionCommandContext["model"]>;
|
|
56
59
|
|
|
57
60
|
type BtwDetails = {
|
|
58
61
|
question: string;
|
|
@@ -60,6 +63,7 @@ type BtwDetails = {
|
|
|
60
63
|
answer: string;
|
|
61
64
|
provider: string;
|
|
62
65
|
model: string;
|
|
66
|
+
api: string;
|
|
63
67
|
thinkingLevel: SessionThinkingLevel;
|
|
64
68
|
timestamp: number;
|
|
65
69
|
usage?: AssistantMessage["usage"];
|
|
@@ -77,6 +81,30 @@ type BtwResetDetails = {
|
|
|
77
81
|
mode?: BtwThreadMode;
|
|
78
82
|
};
|
|
79
83
|
|
|
84
|
+
type BtwModelOverrideDetails =
|
|
85
|
+
| ({ timestamp: number; action: "set" } & Pick<SessionModel, "provider" | "id" | "api">)
|
|
86
|
+
| { timestamp: number; action: "clear" };
|
|
87
|
+
|
|
88
|
+
type BtwThinkingOverrideDetails =
|
|
89
|
+
| { timestamp: number; action: "set"; thinkingLevel: SessionThinkingLevel }
|
|
90
|
+
| { timestamp: number; action: "clear" };
|
|
91
|
+
|
|
92
|
+
type ResolvedBtwModel = {
|
|
93
|
+
model: SessionModel | null;
|
|
94
|
+
source: "override" | "main" | "none";
|
|
95
|
+
configuredOverride: SessionModel | null;
|
|
96
|
+
fallbackReason?: string;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
type ResolvedBtwSettings = {
|
|
100
|
+
model: SessionModel | null;
|
|
101
|
+
modelSource: "override" | "main" | "none";
|
|
102
|
+
configuredModelOverride: SessionModel | null;
|
|
103
|
+
thinkingLevel: SessionThinkingLevel;
|
|
104
|
+
thinkingSource: "override" | "main";
|
|
105
|
+
fallbackReason?: string;
|
|
106
|
+
};
|
|
107
|
+
|
|
80
108
|
type BtwTranscriptEntry =
|
|
81
109
|
| { id: number; turnId: number; type: "turn-boundary"; phase: "start" | "end" }
|
|
82
110
|
| { id: number; turnId: number; type: "user-message"; text: string }
|
|
@@ -152,7 +180,6 @@ function createBtwResourceLoader(
|
|
|
152
180
|
getAgentsFiles: () => ({ agentsFiles: [] }),
|
|
153
181
|
getSystemPrompt: () => systemPrompt,
|
|
154
182
|
getAppendSystemPrompt: () => appendSystemPrompt,
|
|
155
|
-
getPathMetadata: () => new Map(),
|
|
156
183
|
extendResources: () => {},
|
|
157
184
|
reload: async () => {},
|
|
158
185
|
};
|
|
@@ -186,17 +213,61 @@ function parseBtwArgs(args: string): ParsedBtwArgs {
|
|
|
186
213
|
return { question, save };
|
|
187
214
|
}
|
|
188
215
|
|
|
216
|
+
function parseBtwModelArgs(args: string):
|
|
217
|
+
| { action: "show" }
|
|
218
|
+
| { action: "clear" }
|
|
219
|
+
| { action: "set"; model: SessionModel }
|
|
220
|
+
| { action: "invalid"; message: string } {
|
|
221
|
+
const trimmed = args.trim();
|
|
222
|
+
if (!trimmed) {
|
|
223
|
+
return { action: "show" };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (trimmed === "clear") {
|
|
227
|
+
return { action: "clear" };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const parts = trimmed.split(/\s+/);
|
|
231
|
+
if (parts.length !== 3) {
|
|
232
|
+
return { action: "invalid", message: "Usage: /btw:model <provider> <model> <api> | clear" };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const [provider, id, api] = parts;
|
|
236
|
+
return { action: "set", model: { provider, id, api } };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function parseBtwThinkingArgs(args: string):
|
|
240
|
+
| { action: "show" }
|
|
241
|
+
| { action: "clear" }
|
|
242
|
+
| { action: "set"; thinkingLevel: SessionThinkingLevel } {
|
|
243
|
+
const trimmed = args.trim();
|
|
244
|
+
if (!trimmed) {
|
|
245
|
+
return { action: "show" };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (trimmed === "clear") {
|
|
249
|
+
return { action: "clear" };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return { action: "set", thinkingLevel: trimmed as SessionThinkingLevel };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function formatModelRef(model: Pick<SessionModel, "provider" | "id" | "api">): string {
|
|
256
|
+
return `${model.provider}/${model.id} (${model.api})`;
|
|
257
|
+
}
|
|
258
|
+
|
|
189
259
|
function buildBtwSeedState(
|
|
190
260
|
ctx: ExtensionCommandContext,
|
|
191
261
|
thread: BtwDetails[],
|
|
192
262
|
mode: BtwThreadMode,
|
|
263
|
+
sessionModel: SessionModel | null,
|
|
193
264
|
): { messages: Message[]; sideThreadStartIndex: number } {
|
|
194
265
|
const messages: Message[] = [];
|
|
195
266
|
|
|
196
267
|
if (mode === "contextual") {
|
|
197
268
|
try {
|
|
198
269
|
messages.push(
|
|
199
|
-
...buildSessionContext(ctx.sessionManager.getEntries(), ctx.sessionManager.getLeafId()).messages.filter(
|
|
270
|
+
...(buildSessionContext(ctx.sessionManager.getEntries(), ctx.sessionManager.getLeafId()).messages as Message[]).filter(
|
|
200
271
|
(message) => !isVisibleBtwMessage(message),
|
|
201
272
|
),
|
|
202
273
|
);
|
|
@@ -207,7 +278,7 @@ function buildBtwSeedState(
|
|
|
207
278
|
return [];
|
|
208
279
|
}
|
|
209
280
|
|
|
210
|
-
const message = entry as Partial<Message> & { role?: string; customType?: string; content?: unknown };
|
|
281
|
+
const message = entry as unknown as Partial<Message> & { role?: string; customType?: string; content?: unknown };
|
|
211
282
|
if (typeof message.role !== "string" || !Array.isArray(message.content)) {
|
|
212
283
|
return [];
|
|
213
284
|
}
|
|
@@ -230,9 +301,9 @@ function buildBtwSeedState(
|
|
|
230
301
|
{
|
|
231
302
|
role: "assistant",
|
|
232
303
|
content: [{ type: "text", text: BTW_CONTINUE_THREAD_ASSISTANT_TEXT }],
|
|
233
|
-
provider:
|
|
234
|
-
model:
|
|
235
|
-
api:
|
|
304
|
+
provider: sessionModel?.provider ?? "unknown",
|
|
305
|
+
model: sessionModel?.id ?? "unknown",
|
|
306
|
+
api: sessionModel?.api ?? "openai-responses",
|
|
236
307
|
usage: {
|
|
237
308
|
input: 0,
|
|
238
309
|
output: 0,
|
|
@@ -258,7 +329,7 @@ function buildBtwSeedState(
|
|
|
258
329
|
content: [{ type: "text", text: entry.answer }],
|
|
259
330
|
provider: entry.provider,
|
|
260
331
|
model: entry.model,
|
|
261
|
-
api: ctx.model?.api
|
|
332
|
+
api: entry.api || sessionModel?.api || ctx.model?.api || "openai-responses",
|
|
262
333
|
usage:
|
|
263
334
|
entry.usage ?? {
|
|
264
335
|
input: 0,
|
|
@@ -336,7 +407,7 @@ function ensureTranscriptTurn(state: BtwTranscriptState): number {
|
|
|
336
407
|
const turnId = state.nextTurnId++;
|
|
337
408
|
state.currentTurnId = turnId;
|
|
338
409
|
state.lastTurnId = turnId;
|
|
339
|
-
appendTranscriptEntry(state, { type: "turn-boundary", turnId, phase: "start" });
|
|
410
|
+
appendTranscriptEntry(state, { type: "turn-boundary", turnId, phase: "start" } as Omit<Extract<BtwTranscriptEntry, { type: "turn-boundary" }>, "id">);
|
|
340
411
|
return turnId;
|
|
341
412
|
}
|
|
342
413
|
|
|
@@ -350,7 +421,7 @@ function finishTranscriptTurn(state: BtwTranscriptState, turnId?: number | null)
|
|
|
350
421
|
(entry) => entry.turnId === resolvedTurnId && entry.type === "turn-boundary" && entry.phase === "end",
|
|
351
422
|
);
|
|
352
423
|
if (!hasEndBoundary) {
|
|
353
|
-
appendTranscriptEntry(state, { type: "turn-boundary", turnId: resolvedTurnId, phase: "end" });
|
|
424
|
+
appendTranscriptEntry(state, { type: "turn-boundary", turnId: resolvedTurnId, phase: "end" } as Omit<Extract<BtwTranscriptEntry, { type: "turn-boundary" }>, "id">);
|
|
354
425
|
}
|
|
355
426
|
|
|
356
427
|
for (const entry of state.entries) {
|
|
@@ -415,8 +486,18 @@ function ensureTranscriptTurnForUserMessage(state: BtwTranscriptState): number {
|
|
|
415
486
|
return ensureTranscriptTurn(state);
|
|
416
487
|
}
|
|
417
488
|
|
|
418
|
-
function extractMessageText(message: { content?: AssistantMessage["content"] }): string {
|
|
419
|
-
|
|
489
|
+
function extractMessageText(message: { content?: string | AssistantMessage["content"] | UserMessage["content"] }): string {
|
|
490
|
+
if (typeof message.content === "string") {
|
|
491
|
+
return message.content;
|
|
492
|
+
}
|
|
493
|
+
if (!Array.isArray(message.content)) {
|
|
494
|
+
return "";
|
|
495
|
+
}
|
|
496
|
+
return message.content
|
|
497
|
+
.filter((part): part is { type: "text"; text: string } => part.type === "text" && typeof part.text === "string")
|
|
498
|
+
.map((part) => part.text)
|
|
499
|
+
.join("\n")
|
|
500
|
+
.trim();
|
|
420
501
|
}
|
|
421
502
|
|
|
422
503
|
function upsertUserMessageEntry(state: BtwTranscriptState, turnId: number, text: string): void {
|
|
@@ -430,7 +511,7 @@ function upsertUserMessageEntry(state: BtwTranscriptState, turnId: number, text:
|
|
|
430
511
|
return;
|
|
431
512
|
}
|
|
432
513
|
|
|
433
|
-
appendTranscriptEntry(state, { type: "user-message", turnId, text });
|
|
514
|
+
appendTranscriptEntry(state, { type: "user-message", turnId, text } as Omit<Extract<BtwTranscriptEntry, { type: "user-message" }>, "id">);
|
|
434
515
|
}
|
|
435
516
|
|
|
436
517
|
function upsertTranscriptTextEntry(
|
|
@@ -451,7 +532,7 @@ function upsertTranscriptTextEntry(
|
|
|
451
532
|
return;
|
|
452
533
|
}
|
|
453
534
|
|
|
454
|
-
appendTranscriptEntry(state, { type, turnId, text, streaming });
|
|
535
|
+
appendTranscriptEntry(state, { type, turnId, text, streaming } as Omit<Extract<BtwTranscriptEntry, { type: "thinking" | "assistant-text" }>, "id">);
|
|
455
536
|
}
|
|
456
537
|
|
|
457
538
|
function summarizeToolResult(value: unknown, maxLength = 400): { content: string; truncated: boolean } {
|
|
@@ -522,7 +603,7 @@ function ensureToolCallEntry(
|
|
|
522
603
|
toolCallId,
|
|
523
604
|
toolName,
|
|
524
605
|
args,
|
|
525
|
-
});
|
|
606
|
+
} as Omit<Extract<BtwTranscriptEntry, { type: "tool-call" }>, "id">);
|
|
526
607
|
const record = { turnId, callEntryId: callEntry.id };
|
|
527
608
|
state.toolCalls.set(toolCallId, record);
|
|
528
609
|
return record;
|
|
@@ -561,21 +642,17 @@ function upsertToolResultEntry(
|
|
|
561
642
|
truncated,
|
|
562
643
|
isError,
|
|
563
644
|
streaming,
|
|
564
|
-
});
|
|
645
|
+
} as Omit<Extract<BtwTranscriptEntry, { type: "tool-result" }>, "id">);
|
|
565
646
|
toolCall.resultEntryId = resultEntry.id;
|
|
566
647
|
}
|
|
567
648
|
|
|
568
649
|
function applyAssistantMessageToTranscript(
|
|
569
650
|
state: BtwTranscriptState,
|
|
570
651
|
turnId: number,
|
|
571
|
-
message:
|
|
652
|
+
message: AssistantMessage,
|
|
572
653
|
streaming: boolean,
|
|
573
654
|
): void {
|
|
574
|
-
|
|
575
|
-
return;
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
const assistantMessage = message as AssistantMessage;
|
|
655
|
+
const assistantMessage = message;
|
|
579
656
|
const thinking = extractThinking(assistantMessage);
|
|
580
657
|
const answer = extractMessageText(assistantMessage);
|
|
581
658
|
|
|
@@ -847,7 +924,7 @@ function isThreadContinuationMarker(messages: Message[], index: number): boolean
|
|
|
847
924
|
|
|
848
925
|
function extractBtwHandoffThread(sessionRuntime: BtwSessionRuntime): BtwHandoffExchange[] {
|
|
849
926
|
const handoffMessages = sessionRuntime.session.state.messages.slice(sessionRuntime.sideThreadStartIndex);
|
|
850
|
-
const threadMessages = isThreadContinuationMarker(handoffMessages, 0) ? handoffMessages.slice(2) : handoffMessages;
|
|
927
|
+
const threadMessages = isThreadContinuationMarker(handoffMessages as Message[], 0) ? handoffMessages.slice(2) : handoffMessages;
|
|
851
928
|
const exchanges: BtwHandoffExchange[] = [];
|
|
852
929
|
let currentUser = "";
|
|
853
930
|
let currentAssistant = "";
|
|
@@ -927,8 +1004,8 @@ function getOverlayTitle(mode: BtwThreadMode): string {
|
|
|
927
1004
|
function buildTranscriptBadge(
|
|
928
1005
|
theme: ExtensionContext["ui"]["theme"],
|
|
929
1006
|
label: string,
|
|
930
|
-
background:
|
|
931
|
-
foreground:
|
|
1007
|
+
background: "userMessageBg" | "toolPendingBg" | "customMessageBg",
|
|
1008
|
+
foreground: "accent" | "warning" | "success",
|
|
932
1009
|
): string {
|
|
933
1010
|
return theme.bg(background, theme.fg(foreground, theme.bold(` ${label} `)));
|
|
934
1011
|
}
|
|
@@ -953,6 +1030,10 @@ class BtwOverlayComponent extends Container implements Focusable {
|
|
|
953
1030
|
private transcriptViewportHeight = 8;
|
|
954
1031
|
private followTranscript = true;
|
|
955
1032
|
private _focused = false;
|
|
1033
|
+
private modeTextValue = "";
|
|
1034
|
+
private summaryTextValue = "";
|
|
1035
|
+
private statusTextValue = "";
|
|
1036
|
+
private hintsTextValue = "";
|
|
956
1037
|
|
|
957
1038
|
get focused(): boolean {
|
|
958
1039
|
return this._focused;
|
|
@@ -1002,7 +1083,18 @@ class BtwOverlayComponent extends Container implements Focusable {
|
|
|
1002
1083
|
|
|
1003
1084
|
const originalHandleInput = this.input.handleInput.bind(this.input);
|
|
1004
1085
|
this.input.handleInput = (data: string) => {
|
|
1005
|
-
if (keybindings.matches(data, "
|
|
1086
|
+
if (keybindings.matches(data, "app.clear")) {
|
|
1087
|
+
if (this.input.getValue().length > 0) {
|
|
1088
|
+
this.input.setValue("");
|
|
1089
|
+
this.tui.requestRender();
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
this.onDismissCallback();
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
if (keybindings.matches(data, "tui.select.cancel")) {
|
|
1006
1098
|
this.onDismissCallback();
|
|
1007
1099
|
return;
|
|
1008
1100
|
}
|
|
@@ -1111,12 +1203,12 @@ class BtwOverlayComponent extends Container implements Focusable {
|
|
|
1111
1203
|
const hiddenBelow = Math.max(0, maxScroll - this.transcriptScrollOffset);
|
|
1112
1204
|
const summary =
|
|
1113
1205
|
hiddenAbove || hiddenBelow
|
|
1114
|
-
? `${this.
|
|
1115
|
-
: this.
|
|
1206
|
+
? `${this.summaryTextValue.trim()} · ↑${hiddenAbove} ↓${hiddenBelow}`
|
|
1207
|
+
: this.summaryTextValue.trim();
|
|
1116
1208
|
|
|
1117
1209
|
const lines = [this.borderLine(innerWidth, "top")];
|
|
1118
1210
|
|
|
1119
|
-
lines.push(this.frameLine(this.theme.fg("accent", this.theme.bold(this.
|
|
1211
|
+
lines.push(this.frameLine(this.theme.fg("accent", this.theme.bold(this.modeTextValue.trim())), innerWidth));
|
|
1120
1212
|
lines.push(this.frameLine(this.theme.fg("dim", summary), innerWidth));
|
|
1121
1213
|
lines.push(this.ruleLine(innerWidth));
|
|
1122
1214
|
|
|
@@ -1128,9 +1220,9 @@ class BtwOverlayComponent extends Container implements Focusable {
|
|
|
1128
1220
|
}
|
|
1129
1221
|
|
|
1130
1222
|
lines.push(this.ruleLine(innerWidth));
|
|
1131
|
-
lines.push(this.frameLine(this.theme.fg("warning", this.
|
|
1223
|
+
lines.push(this.frameLine(this.theme.fg("warning", this.statusTextValue.trim()), innerWidth));
|
|
1132
1224
|
lines.push(this.inputFrameLine(dialogWidth));
|
|
1133
|
-
lines.push(this.frameLine(this.theme.fg("dim", this.
|
|
1225
|
+
lines.push(this.frameLine(this.theme.fg("dim", this.hintsTextValue.trim()), innerWidth));
|
|
1134
1226
|
lines.push(this.borderLine(innerWidth, "bottom"));
|
|
1135
1227
|
|
|
1136
1228
|
return lines;
|
|
@@ -1150,11 +1242,13 @@ class BtwOverlayComponent extends Container implements Focusable {
|
|
|
1150
1242
|
}
|
|
1151
1243
|
|
|
1152
1244
|
refresh(): void {
|
|
1153
|
-
this.
|
|
1245
|
+
this.modeTextValue = `${getOverlayTitle(this.getMode())} · hidden thread preserved`;
|
|
1246
|
+
this.modeText.setText(this.modeTextValue);
|
|
1154
1247
|
const entries = this.readTranscriptEntries();
|
|
1155
1248
|
const exchanges = getCompletedExchangeCount(entries);
|
|
1156
1249
|
const active = hasStreamingTranscriptEntry(entries) ? " · streaming" : " · idle";
|
|
1157
|
-
this.
|
|
1250
|
+
this.summaryTextValue = `${exchanges} exchange${exchanges === 1 ? "" : "s"}${active}`;
|
|
1251
|
+
this.summaryText.setText(this.summaryTextValue);
|
|
1158
1252
|
|
|
1159
1253
|
this.transcriptLines = buildOverlayTranscript(entries, this.theme);
|
|
1160
1254
|
this.transcript.clear();
|
|
@@ -1163,8 +1257,10 @@ class BtwOverlayComponent extends Container implements Focusable {
|
|
|
1163
1257
|
}
|
|
1164
1258
|
|
|
1165
1259
|
const status = this.getStatus() ?? "Ready. Enter submits; Escape dismisses without clearing.";
|
|
1166
|
-
this.
|
|
1167
|
-
this.
|
|
1260
|
+
this.statusTextValue = status;
|
|
1261
|
+
this.statusText.setText(this.statusTextValue);
|
|
1262
|
+
this.hintsTextValue = "Enter submit · Alt+/ toggle focus · Escape dismiss · PgUp/PgDn scroll";
|
|
1263
|
+
this.hintsText.setText(this.hintsTextValue);
|
|
1168
1264
|
this.tui.requestRender();
|
|
1169
1265
|
}
|
|
1170
1266
|
}
|
|
@@ -1172,6 +1268,8 @@ class BtwOverlayComponent extends Container implements Focusable {
|
|
|
1172
1268
|
export default function (pi: ExtensionAPI) {
|
|
1173
1269
|
let pendingThread: BtwDetails[] = [];
|
|
1174
1270
|
let pendingMode: BtwThreadMode = "contextual";
|
|
1271
|
+
let btwModelOverride: SessionModel | null = null;
|
|
1272
|
+
let btwThinkingOverride: SessionThinkingLevel | null = null;
|
|
1175
1273
|
let transcriptState = createEmptyTranscriptState();
|
|
1176
1274
|
let overlayStatus: string | null = null;
|
|
1177
1275
|
let overlayDraft = "";
|
|
@@ -1317,26 +1415,161 @@ export default function (pi: ExtensionAPI) {
|
|
|
1317
1415
|
await disposeBtwSession();
|
|
1318
1416
|
}
|
|
1319
1417
|
|
|
1418
|
+
async function resolveBtwModel(
|
|
1419
|
+
ctx: ExtensionCommandContext,
|
|
1420
|
+
notifyOnFallback = false,
|
|
1421
|
+
): Promise<ResolvedBtwModel> {
|
|
1422
|
+
if (btwModelOverride) {
|
|
1423
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(btwModelOverride);
|
|
1424
|
+
if (auth.ok && auth.apiKey) {
|
|
1425
|
+
return {
|
|
1426
|
+
model: btwModelOverride,
|
|
1427
|
+
source: "override",
|
|
1428
|
+
configuredOverride: btwModelOverride,
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
const fallbackReason = ctx.model
|
|
1433
|
+
? `Configured BTW model ${formatModelRef(btwModelOverride)} has no credentials. Falling back to main model ${formatModelRef(
|
|
1434
|
+
ctx.model,
|
|
1435
|
+
)}.`
|
|
1436
|
+
: `Configured BTW model ${formatModelRef(btwModelOverride)} has no credentials, and no main model is active.`;
|
|
1437
|
+
if (notifyOnFallback) {
|
|
1438
|
+
notify(ctx, fallbackReason, "warning");
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
if (ctx.model) {
|
|
1442
|
+
return {
|
|
1443
|
+
model: ctx.model,
|
|
1444
|
+
source: "main",
|
|
1445
|
+
configuredOverride: btwModelOverride,
|
|
1446
|
+
fallbackReason,
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
return {
|
|
1451
|
+
model: null,
|
|
1452
|
+
source: "none",
|
|
1453
|
+
configuredOverride: btwModelOverride,
|
|
1454
|
+
fallbackReason,
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
if (ctx.model) {
|
|
1459
|
+
return {
|
|
1460
|
+
model: ctx.model,
|
|
1461
|
+
source: "main",
|
|
1462
|
+
configuredOverride: null,
|
|
1463
|
+
};
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
return {
|
|
1467
|
+
model: null,
|
|
1468
|
+
source: "none",
|
|
1469
|
+
configuredOverride: null,
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
async function resolveBtwSettings(
|
|
1474
|
+
ctx: ExtensionCommandContext,
|
|
1475
|
+
notifyOnFallback = false,
|
|
1476
|
+
): Promise<ResolvedBtwSettings> {
|
|
1477
|
+
const resolvedModel = await resolveBtwModel(ctx, notifyOnFallback);
|
|
1478
|
+
const thinkingLevel = btwThinkingOverride ?? (pi.getThinkingLevel() as SessionThinkingLevel);
|
|
1479
|
+
|
|
1480
|
+
return {
|
|
1481
|
+
model: resolvedModel.model,
|
|
1482
|
+
modelSource: resolvedModel.source,
|
|
1483
|
+
configuredModelOverride: resolvedModel.configuredOverride,
|
|
1484
|
+
thinkingLevel,
|
|
1485
|
+
thinkingSource: btwThinkingOverride ? "override" : "main",
|
|
1486
|
+
fallbackReason: resolvedModel.fallbackReason,
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
function describeResolvedModel(settings: ResolvedBtwSettings): string {
|
|
1491
|
+
if (!settings.model) {
|
|
1492
|
+
if (settings.configuredModelOverride && settings.fallbackReason) {
|
|
1493
|
+
return `BTW model unavailable. ${settings.fallbackReason}`;
|
|
1494
|
+
}
|
|
1495
|
+
return "BTW model unavailable. No active model selected.";
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
const source =
|
|
1499
|
+
settings.modelSource === "override"
|
|
1500
|
+
? "override"
|
|
1501
|
+
: settings.configuredModelOverride
|
|
1502
|
+
? "inherited fallback"
|
|
1503
|
+
: "inherits main thread";
|
|
1504
|
+
return `BTW model: ${formatModelRef(settings.model)} (${source}).${
|
|
1505
|
+
settings.fallbackReason ? ` ${settings.fallbackReason}` : ""
|
|
1506
|
+
}`;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
function describeResolvedThinking(settings: ResolvedBtwSettings): string {
|
|
1510
|
+
const source = settings.thinkingSource === "override" ? "override" : "inherits main thread";
|
|
1511
|
+
return `BTW thinking: ${settings.thinkingLevel} (${source}).`;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
async function setBtwModelOverride(ctx: ExtensionCommandContext, nextModel: SessionModel | null): Promise<void> {
|
|
1515
|
+
btwModelOverride = nextModel;
|
|
1516
|
+
const details: BtwModelOverrideDetails = nextModel
|
|
1517
|
+
? { action: "set", timestamp: Date.now(), provider: nextModel.provider, id: nextModel.id, api: nextModel.api }
|
|
1518
|
+
: { action: "clear", timestamp: Date.now() };
|
|
1519
|
+
pi.appendEntry(BTW_MODEL_OVERRIDE_TYPE, details);
|
|
1520
|
+
await disposeBtwSession();
|
|
1521
|
+
const settings = await resolveBtwSettings(ctx);
|
|
1522
|
+
const message = nextModel
|
|
1523
|
+
? `BTW model override set to ${formatModelRef(nextModel)}.`
|
|
1524
|
+
: "BTW model override cleared. BTW now inherits the main thread model.";
|
|
1525
|
+
setOverlayStatus(message, ctx);
|
|
1526
|
+
notify(ctx, `${message} ${describeResolvedModel(settings)}`, "info");
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
async function setBtwThinkingOverride(
|
|
1530
|
+
ctx: ExtensionCommandContext,
|
|
1531
|
+
nextThinkingLevel: SessionThinkingLevel | null,
|
|
1532
|
+
): Promise<void> {
|
|
1533
|
+
btwThinkingOverride = nextThinkingLevel;
|
|
1534
|
+
const details: BtwThinkingOverrideDetails = nextThinkingLevel
|
|
1535
|
+
? { action: "set", timestamp: Date.now(), thinkingLevel: nextThinkingLevel }
|
|
1536
|
+
: { action: "clear", timestamp: Date.now() };
|
|
1537
|
+
pi.appendEntry(BTW_THINKING_OVERRIDE_TYPE, details);
|
|
1538
|
+
await disposeBtwSession();
|
|
1539
|
+
const settings = await resolveBtwSettings(ctx);
|
|
1540
|
+
const message = nextThinkingLevel
|
|
1541
|
+
? `BTW thinking override set to ${nextThinkingLevel}.`
|
|
1542
|
+
: "BTW thinking override cleared. BTW now inherits the main thread thinking level.";
|
|
1543
|
+
setOverlayStatus(message, ctx);
|
|
1544
|
+
notify(ctx, `${message} ${describeResolvedThinking(settings)}`, "info");
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1320
1547
|
async function createBtwSubSession(ctx: ExtensionCommandContext, mode: BtwThreadMode): Promise<BtwSessionRuntime> {
|
|
1548
|
+
const settings = await resolveBtwSettings(ctx, true);
|
|
1549
|
+
if (!settings.model) {
|
|
1550
|
+
throw new Error(settings.fallbackReason || "No active model selected.");
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1321
1553
|
const { session } = await createAgentSession({
|
|
1322
1554
|
sessionManager: SessionManager.inMemory(),
|
|
1323
|
-
model:
|
|
1555
|
+
model: settings.model,
|
|
1324
1556
|
modelRegistry: ctx.modelRegistry as AgentSession["modelRegistry"],
|
|
1325
|
-
thinkingLevel:
|
|
1557
|
+
thinkingLevel: settings.thinkingLevel,
|
|
1326
1558
|
tools: codingTools,
|
|
1327
1559
|
resourceLoader: createBtwResourceLoader(ctx),
|
|
1328
1560
|
});
|
|
1329
1561
|
|
|
1330
|
-
const { messages: seedMessages, sideThreadStartIndex } = buildBtwSeedState(ctx, pendingThread, mode);
|
|
1562
|
+
const { messages: seedMessages, sideThreadStartIndex } = buildBtwSeedState(ctx, pendingThread, mode, settings.model);
|
|
1331
1563
|
if (seedMessages.length > 0) {
|
|
1332
|
-
session.agent.
|
|
1564
|
+
session.agent.state.messages = seedMessages as typeof session.state.messages;
|
|
1333
1565
|
}
|
|
1334
1566
|
|
|
1335
1567
|
return { session, mode, subscriptions: new Set(), sideThreadStartIndex };
|
|
1336
1568
|
}
|
|
1337
1569
|
|
|
1338
1570
|
async function ensureBtwSession(ctx: ExtensionCommandContext, mode: BtwThreadMode): Promise<BtwSessionRuntime | null> {
|
|
1339
|
-
|
|
1571
|
+
const settings = await resolveBtwSettings(ctx);
|
|
1572
|
+
if (!settings.model) {
|
|
1340
1573
|
return null;
|
|
1341
1574
|
}
|
|
1342
1575
|
|
|
@@ -1511,6 +1744,40 @@ export default function (pi: ExtensionAPI) {
|
|
|
1511
1744
|
return true;
|
|
1512
1745
|
}
|
|
1513
1746
|
|
|
1747
|
+
if (name === "btw:model") {
|
|
1748
|
+
const parsed = parseBtwModelArgs(trimmedArgs);
|
|
1749
|
+
if (parsed.action === "invalid") {
|
|
1750
|
+
setOverlayStatus(parsed.message, ctx);
|
|
1751
|
+
notify(ctx, parsed.message, "error");
|
|
1752
|
+
return true;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
if (parsed.action === "show") {
|
|
1756
|
+
const settings = await resolveBtwSettings(ctx);
|
|
1757
|
+
const message = describeResolvedModel(settings);
|
|
1758
|
+
setOverlayStatus(message, ctx);
|
|
1759
|
+
notify(ctx, message, settings.model ? "info" : "warning");
|
|
1760
|
+
return true;
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
await setBtwModelOverride(ctx, parsed.action === "clear" ? null : parsed.model);
|
|
1764
|
+
return true;
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
if (name === "btw:thinking") {
|
|
1768
|
+
const parsed = parseBtwThinkingArgs(trimmedArgs);
|
|
1769
|
+
if (parsed.action === "show") {
|
|
1770
|
+
const settings = await resolveBtwSettings(ctx);
|
|
1771
|
+
const message = describeResolvedThinking(settings);
|
|
1772
|
+
setOverlayStatus(message, ctx);
|
|
1773
|
+
notify(ctx, message, "info");
|
|
1774
|
+
return true;
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
await setBtwThinkingOverride(ctx, parsed.action === "clear" ? null : parsed.thinkingLevel);
|
|
1778
|
+
return true;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1514
1781
|
if (name === "btw:inject") {
|
|
1515
1782
|
if (pendingThread.length === 0) {
|
|
1516
1783
|
notify(ctx, "No BTW thread to inject.", "warning");
|
|
@@ -1573,7 +1840,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1573
1840
|
|
|
1574
1841
|
function parseOverlayBtwCommand(value: string): { name: string; args: string } | null {
|
|
1575
1842
|
const trimmed = value.trim();
|
|
1576
|
-
const match = trimmed.match(/^\/(btw:(?:new|tangent|clear|inject|summarize))(?:\s+(.*))?$/);
|
|
1843
|
+
const match = trimmed.match(/^\/(btw:(?:new|tangent|clear|inject|summarize|model|thinking))(?:\s+(.*))?$/);
|
|
1577
1844
|
if (!match) {
|
|
1578
1845
|
return null;
|
|
1579
1846
|
}
|
|
@@ -1596,17 +1863,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
1596
1863
|
return;
|
|
1597
1864
|
}
|
|
1598
1865
|
|
|
1866
|
+
const cmdCtx = ctx as ExtensionCommandContext;
|
|
1599
1867
|
const btwCommand = parseOverlayBtwCommand(question);
|
|
1600
1868
|
if (btwCommand) {
|
|
1601
1869
|
setOverlayDraft("");
|
|
1602
|
-
await dispatchBtwCommand(btwCommand.name, btwCommand.args,
|
|
1870
|
+
await dispatchBtwCommand(btwCommand.name, btwCommand.args, cmdCtx);
|
|
1603
1871
|
return;
|
|
1604
1872
|
}
|
|
1605
1873
|
|
|
1606
1874
|
setOverlayDraft("");
|
|
1607
1875
|
setOverlayStatus("⏳ streaming...", ctx);
|
|
1608
1876
|
syncUi(ctx);
|
|
1609
|
-
await runBtw(
|
|
1877
|
+
await runBtw(cmdCtx, question, false, pendingMode);
|
|
1610
1878
|
}
|
|
1611
1879
|
|
|
1612
1880
|
async function resetThread(
|
|
@@ -1631,6 +1899,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
1631
1899
|
await disposeBtwSession();
|
|
1632
1900
|
pendingThread = [];
|
|
1633
1901
|
pendingMode = "contextual";
|
|
1902
|
+
btwModelOverride = null;
|
|
1903
|
+
btwThinkingOverride = null;
|
|
1634
1904
|
transcriptState = createEmptyTranscriptState();
|
|
1635
1905
|
overlayDraft = "";
|
|
1636
1906
|
lastUiContext = ctx;
|
|
@@ -1640,9 +1910,29 @@ export default function (pi: ExtensionAPI) {
|
|
|
1640
1910
|
let lastResetIndex = -1;
|
|
1641
1911
|
|
|
1642
1912
|
for (let i = 0; i < branch.length; i++) {
|
|
1913
|
+
if (isCustomEntry(branch[i], BTW_MODEL_OVERRIDE_TYPE)) {
|
|
1914
|
+
const details = branch[i].data as BtwModelOverrideDetails | undefined;
|
|
1915
|
+
btwModelOverride =
|
|
1916
|
+
details?.action === "set"
|
|
1917
|
+
? { provider: details.provider, id: details.id, api: details.api }
|
|
1918
|
+
: details?.action === "clear"
|
|
1919
|
+
? null
|
|
1920
|
+
: btwModelOverride;
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
if (isCustomEntry(branch[i], BTW_THINKING_OVERRIDE_TYPE)) {
|
|
1924
|
+
const details = branch[i].data as BtwThinkingOverrideDetails | undefined;
|
|
1925
|
+
btwThinkingOverride =
|
|
1926
|
+
details?.action === "set"
|
|
1927
|
+
? details.thinkingLevel
|
|
1928
|
+
: details?.action === "clear"
|
|
1929
|
+
? null
|
|
1930
|
+
: btwThinkingOverride;
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1643
1933
|
if (isCustomEntry(branch[i], BTW_RESET_TYPE)) {
|
|
1644
1934
|
lastResetIndex = i;
|
|
1645
|
-
const details = branch[i]
|
|
1935
|
+
const details = (branch[i] as unknown as { data?: BtwResetDetails }).data;
|
|
1646
1936
|
pendingMode = details?.mode ?? "contextual";
|
|
1647
1937
|
}
|
|
1648
1938
|
}
|
|
@@ -1652,13 +1942,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
1652
1942
|
continue;
|
|
1653
1943
|
}
|
|
1654
1944
|
|
|
1655
|
-
const details = entry
|
|
1945
|
+
const details = (entry as unknown as { data?: BtwDetails }).data;
|
|
1656
1946
|
if (!details?.question || !details.answer) {
|
|
1657
1947
|
continue;
|
|
1658
1948
|
}
|
|
1659
1949
|
|
|
1660
|
-
|
|
1661
|
-
|
|
1950
|
+
const normalizedDetails: BtwDetails = {
|
|
1951
|
+
...details,
|
|
1952
|
+
api: details.api || ctx.model?.api || "openai-responses",
|
|
1953
|
+
};
|
|
1954
|
+
|
|
1955
|
+
pendingThread.push(normalizedDetails);
|
|
1956
|
+
appendPersistedTranscriptTurn(transcriptState, normalizedDetails);
|
|
1662
1957
|
}
|
|
1663
1958
|
|
|
1664
1959
|
syncUi(ctx);
|
|
@@ -1671,16 +1966,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
1671
1966
|
mode: BtwThreadMode,
|
|
1672
1967
|
): Promise<void> {
|
|
1673
1968
|
lastUiContext = ctx;
|
|
1674
|
-
const
|
|
1969
|
+
const settings = await resolveBtwSettings(ctx);
|
|
1970
|
+
const model = settings.model;
|
|
1675
1971
|
if (!model) {
|
|
1676
|
-
|
|
1677
|
-
|
|
1972
|
+
const message = settings.fallbackReason || "No active model selected.";
|
|
1973
|
+
setOverlayStatus(message, ctx);
|
|
1974
|
+
notify(ctx, message, "error");
|
|
1678
1975
|
return;
|
|
1679
1976
|
}
|
|
1680
1977
|
|
|
1681
|
-
const
|
|
1682
|
-
if (!apiKey) {
|
|
1683
|
-
const message = `No credentials available for ${model.provider}/${model.id}
|
|
1978
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
1979
|
+
if (!auth.ok || !auth.apiKey) {
|
|
1980
|
+
const message = auth.ok ? `No credentials available for ${model.provider}/${model.id}.` : auth.error;
|
|
1684
1981
|
setOverlayStatus(message, ctx);
|
|
1685
1982
|
notify(ctx, message, "error");
|
|
1686
1983
|
await ensureOverlay(ctx);
|
|
@@ -1697,7 +1994,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1697
1994
|
const session = sessionRuntime.session;
|
|
1698
1995
|
const wasBusy = !ctx.isIdle();
|
|
1699
1996
|
pendingMode = mode;
|
|
1700
|
-
const thinkingLevel =
|
|
1997
|
+
const thinkingLevel = settings.thinkingLevel;
|
|
1701
1998
|
|
|
1702
1999
|
setOverlayStatus("⏳ streaming...", ctx);
|
|
1703
2000
|
await ensureOverlay(ctx);
|
|
@@ -1730,6 +2027,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1730
2027
|
answer,
|
|
1731
2028
|
provider: model.provider,
|
|
1732
2029
|
model: model.id,
|
|
2030
|
+
api: model.api,
|
|
1733
2031
|
thinkingLevel,
|
|
1734
2032
|
timestamp: Date.now(),
|
|
1735
2033
|
usage: response.usage,
|
|
@@ -1778,14 +2076,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
1778
2076
|
}
|
|
1779
2077
|
|
|
1780
2078
|
async function summarizeThread(ctx: ExtensionCommandContext, thread: BtwHandoffExchange[]): Promise<string> {
|
|
1781
|
-
const
|
|
2079
|
+
const settings = await resolveBtwSettings(ctx, true);
|
|
2080
|
+
const model = settings.model;
|
|
1782
2081
|
if (!model) {
|
|
1783
|
-
throw new Error("No active model selected.");
|
|
2082
|
+
throw new Error(settings.fallbackReason || "No active model selected.");
|
|
1784
2083
|
}
|
|
1785
2084
|
|
|
1786
|
-
const
|
|
1787
|
-
if (!apiKey) {
|
|
1788
|
-
throw new Error(`No credentials available for ${model.provider}/${model.id}.`);
|
|
2085
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
2086
|
+
if (!auth.ok || !auth.apiKey) {
|
|
2087
|
+
throw new Error(auth.ok ? `No credentials available for ${model.provider}/${model.id}.` : auth.error);
|
|
1789
2088
|
}
|
|
1790
2089
|
|
|
1791
2090
|
const { session } = await createAgentSession({
|
|
@@ -1837,7 +2136,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
1837
2136
|
|
|
1838
2137
|
if (expanded && details) {
|
|
1839
2138
|
lines.push(
|
|
1840
|
-
theme.fg(
|
|
2139
|
+
theme.fg(
|
|
2140
|
+
"dim",
|
|
2141
|
+
`model: ${details.provider}/${details.model} (${details.api ?? "openai-responses"}) · thinking: ${details.thinkingLevel}`,
|
|
2142
|
+
),
|
|
1841
2143
|
);
|
|
1842
2144
|
|
|
1843
2145
|
if (details.usage) {
|
|
@@ -1865,10 +2167,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
1865
2167
|
await restoreThread(ctx);
|
|
1866
2168
|
});
|
|
1867
2169
|
|
|
1868
|
-
pi.on("session_switch", async (_event, ctx) => {
|
|
1869
|
-
await restoreThread(ctx);
|
|
1870
|
-
});
|
|
1871
|
-
|
|
1872
2170
|
pi.on("session_tree", async (_event, ctx) => {
|
|
1873
2171
|
await restoreThread(ctx);
|
|
1874
2172
|
});
|
|
@@ -1881,7 +2179,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1881
2179
|
for (const shortcut of BTW_FOCUS_SHORTCUTS) {
|
|
1882
2180
|
pi.registerShortcut(shortcut, {
|
|
1883
2181
|
description: "Toggle BTW overlay focus while leaving it open.",
|
|
1884
|
-
handler: async (
|
|
2182
|
+
handler: async (_ctx) => {
|
|
1885
2183
|
toggleOverlayFocus();
|
|
1886
2184
|
},
|
|
1887
2185
|
});
|
|
@@ -1928,4 +2226,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
1928
2226
|
await dispatchBtwCommand("btw:summarize", args, ctx);
|
|
1929
2227
|
},
|
|
1930
2228
|
});
|
|
2229
|
+
|
|
2230
|
+
pi.registerCommand("btw:model", {
|
|
2231
|
+
description: "Show, set, or clear the BTW-only model override.",
|
|
2232
|
+
handler: async (args, ctx) => {
|
|
2233
|
+
await dispatchBtwCommand("btw:model", args, ctx);
|
|
2234
|
+
},
|
|
2235
|
+
});
|
|
2236
|
+
|
|
2237
|
+
pi.registerCommand("btw:thinking", {
|
|
2238
|
+
description: "Show, set, or clear the BTW-only thinking override.",
|
|
2239
|
+
handler: async (args, ctx) => {
|
|
2240
|
+
await dispatchBtwCommand("btw:thinking", args, ctx);
|
|
2241
|
+
},
|
|
2242
|
+
});
|
|
1931
2243
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-btw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.8",
|
|
4
4
|
"description": "A pi extension for parallel side conversations with /btw",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,11 +42,12 @@
|
|
|
42
42
|
"image": "https://raw.githubusercontent.com/dbachelder/pi-btw/main/docs/btw-overlay.png"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
|
-
"@mariozechner/pi-ai": "
|
|
46
|
-
"@mariozechner/pi-coding-agent": "
|
|
47
|
-
"@mariozechner/pi-tui": "
|
|
45
|
+
"@mariozechner/pi-ai": "^0.66.1",
|
|
46
|
+
"@mariozechner/pi-coding-agent": "^0.66.1",
|
|
47
|
+
"@mariozechner/pi-tui": "^0.66.1"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
+
"typescript": "^6.0.2",
|
|
50
51
|
"vitest": "^4.1.0"
|
|
51
52
|
}
|
|
52
53
|
}
|