pi-btw 0.2.0 → 0.3.7
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 +31 -2
- package/extensions/btw.ts +422 -70
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -13,7 +13,9 @@ A small [pi](https://github.com/badlogic/pi-mono) extension that adds a `/btw` s
|
|
|
13
13
|
- keeps a continuous BTW thread by default
|
|
14
14
|
- supports `/btw:tangent` for a contextless side thread that does not inherit the current main-session conversation
|
|
15
15
|
- opens a focused BTW modal shell with its own composer and transcript
|
|
16
|
+
- keeps the BTW overlay open while you switch focus back to the main editor with `Alt+/`
|
|
16
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
|
|
17
19
|
- lets you inject the full thread, or a summary of it, back into the main agent
|
|
18
20
|
- optionally saves an individual BTW exchange as a visible session note with `--save`
|
|
19
21
|
|
|
@@ -51,6 +53,8 @@ pi install /absolute/path/to/pi-btw
|
|
|
51
53
|
/btw --save summarize the last error in one sentence
|
|
52
54
|
/btw:new let's start a fresh thread about auth
|
|
53
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
|
|
54
58
|
/btw:inject implement the plan we just discussed
|
|
55
59
|
/btw:summarize turn that side thread into a short handoff
|
|
56
60
|
/btw:clear
|
|
@@ -69,6 +73,13 @@ pi install /absolute/path/to/pi-btw
|
|
|
69
73
|
- persists the BTW exchange as hidden thread state
|
|
70
74
|
- with `--save`, also saves that single exchange as a visible session note
|
|
71
75
|
|
|
76
|
+
## Overlay controls
|
|
77
|
+
|
|
78
|
+
- `Alt+/` toggles focus between BTW and the main editor without closing the overlay
|
|
79
|
+
- `Ctrl+Alt+W` is a fallback focus toggle for terminals that do not deliver `Alt+/` as a usable shortcut
|
|
80
|
+
- `Esc` still dismisses BTW immediately while the overlay is focused
|
|
81
|
+
- BTW now opens top-centered so the main session remains visible underneath it
|
|
82
|
+
|
|
72
83
|
### `/btw:new [question]`
|
|
73
84
|
|
|
74
85
|
- clears the current BTW thread
|
|
@@ -97,11 +108,26 @@ pi install /absolute/path/to/pi-btw
|
|
|
97
108
|
|
|
98
109
|
### `/btw:summarize [instructions]`
|
|
99
110
|
|
|
100
|
-
- 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
|
|
101
113
|
- injects the summary into the main agent
|
|
102
114
|
- if pi is busy, queues it as a follow-up
|
|
103
115
|
- clears the BTW thread after sending
|
|
104
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
|
+
|
|
105
131
|
## Behavior
|
|
106
132
|
|
|
107
133
|
### Real sub-session model
|
|
@@ -110,6 +136,8 @@ BTW is implemented as an actual pi sub-session with its own in-memory session st
|
|
|
110
136
|
|
|
111
137
|
- contextual `/btw` threads seed that sub-session from the current main-session branch while filtering out BTW-visible notes from the parent context
|
|
112
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
|
|
113
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
|
|
114
142
|
- handoff commands (`/btw:inject` and `/btw:summarize`) read from the BTW sub-session thread rather than maintaining a separate manual transcript model
|
|
115
143
|
|
|
@@ -117,7 +145,7 @@ BTW is implemented as an actual pi sub-session with its own in-memory session st
|
|
|
117
145
|
|
|
118
146
|
Inside the BTW modal composer, slash handling is split at the BTW/session boundary:
|
|
119
147
|
|
|
120
|
-
- `/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
|
|
121
149
|
- any other slash-prefixed input is routed through the BTW sub-session's normal `prompt()` path
|
|
122
150
|
- this means ordinary pi slash commands like `/help` are handled by the sub-session instead of being rejected by a modal-only fallback
|
|
123
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
|
|
@@ -133,6 +161,7 @@ BTW exchanges are persisted in the session as hidden custom entries so they:
|
|
|
133
161
|
- survive reloads and restarts
|
|
134
162
|
- rehydrate the BTW modal shell for the current branch
|
|
135
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
|
|
136
165
|
- stay out of the main agent's LLM context
|
|
137
166
|
|
|
138
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,13 @@ 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";
|
|
36
|
+
const BTW_FOCUS_SHORTCUTS = [Key.alt("/"), Key.ctrlAlt("w")] as const;
|
|
37
|
+
|
|
38
|
+
function matchesBtwFocusShortcut(data: string): boolean {
|
|
39
|
+
return BTW_FOCUS_SHORTCUTS.some((shortcut) => matchesKey(data, shortcut));
|
|
40
|
+
}
|
|
34
41
|
|
|
35
42
|
const BTW_SYSTEM_PROMPT = [
|
|
36
43
|
"You are having an aside conversation with the user, separate from their main working session.",
|
|
@@ -48,6 +55,7 @@ const BTW_CONTINUE_THREAD_ASSISTANT_TEXT = "Understood, continuing our side conv
|
|
|
48
55
|
|
|
49
56
|
type SessionThinkingLevel = "off" | AiThinkingLevel;
|
|
50
57
|
type BtwThreadMode = "contextual" | "tangent";
|
|
58
|
+
type SessionModel = NonNullable<ExtensionCommandContext["model"]>;
|
|
51
59
|
|
|
52
60
|
type BtwDetails = {
|
|
53
61
|
question: string;
|
|
@@ -55,6 +63,7 @@ type BtwDetails = {
|
|
|
55
63
|
answer: string;
|
|
56
64
|
provider: string;
|
|
57
65
|
model: string;
|
|
66
|
+
api: string;
|
|
58
67
|
thinkingLevel: SessionThinkingLevel;
|
|
59
68
|
timestamp: number;
|
|
60
69
|
usage?: AssistantMessage["usage"];
|
|
@@ -72,6 +81,30 @@ type BtwResetDetails = {
|
|
|
72
81
|
mode?: BtwThreadMode;
|
|
73
82
|
};
|
|
74
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
|
+
|
|
75
108
|
type BtwTranscriptEntry =
|
|
76
109
|
| { id: number; turnId: number; type: "turn-boundary"; phase: "start" | "end" }
|
|
77
110
|
| { id: number; turnId: number; type: "user-message"; text: string }
|
|
@@ -147,7 +180,6 @@ function createBtwResourceLoader(
|
|
|
147
180
|
getAgentsFiles: () => ({ agentsFiles: [] }),
|
|
148
181
|
getSystemPrompt: () => systemPrompt,
|
|
149
182
|
getAppendSystemPrompt: () => appendSystemPrompt,
|
|
150
|
-
getPathMetadata: () => new Map(),
|
|
151
183
|
extendResources: () => {},
|
|
152
184
|
reload: async () => {},
|
|
153
185
|
};
|
|
@@ -181,17 +213,61 @@ function parseBtwArgs(args: string): ParsedBtwArgs {
|
|
|
181
213
|
return { question, save };
|
|
182
214
|
}
|
|
183
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
|
+
|
|
184
259
|
function buildBtwSeedState(
|
|
185
260
|
ctx: ExtensionCommandContext,
|
|
186
261
|
thread: BtwDetails[],
|
|
187
262
|
mode: BtwThreadMode,
|
|
263
|
+
sessionModel: SessionModel | null,
|
|
188
264
|
): { messages: Message[]; sideThreadStartIndex: number } {
|
|
189
265
|
const messages: Message[] = [];
|
|
190
266
|
|
|
191
267
|
if (mode === "contextual") {
|
|
192
268
|
try {
|
|
193
269
|
messages.push(
|
|
194
|
-
...buildSessionContext(ctx.sessionManager.getEntries(), ctx.sessionManager.getLeafId()).messages.filter(
|
|
270
|
+
...(buildSessionContext(ctx.sessionManager.getEntries(), ctx.sessionManager.getLeafId()).messages as Message[]).filter(
|
|
195
271
|
(message) => !isVisibleBtwMessage(message),
|
|
196
272
|
),
|
|
197
273
|
);
|
|
@@ -202,7 +278,7 @@ function buildBtwSeedState(
|
|
|
202
278
|
return [];
|
|
203
279
|
}
|
|
204
280
|
|
|
205
|
-
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 };
|
|
206
282
|
if (typeof message.role !== "string" || !Array.isArray(message.content)) {
|
|
207
283
|
return [];
|
|
208
284
|
}
|
|
@@ -225,9 +301,9 @@ function buildBtwSeedState(
|
|
|
225
301
|
{
|
|
226
302
|
role: "assistant",
|
|
227
303
|
content: [{ type: "text", text: BTW_CONTINUE_THREAD_ASSISTANT_TEXT }],
|
|
228
|
-
provider:
|
|
229
|
-
model:
|
|
230
|
-
api:
|
|
304
|
+
provider: sessionModel?.provider ?? "unknown",
|
|
305
|
+
model: sessionModel?.id ?? "unknown",
|
|
306
|
+
api: sessionModel?.api ?? "openai-responses",
|
|
231
307
|
usage: {
|
|
232
308
|
input: 0,
|
|
233
309
|
output: 0,
|
|
@@ -253,7 +329,7 @@ function buildBtwSeedState(
|
|
|
253
329
|
content: [{ type: "text", text: entry.answer }],
|
|
254
330
|
provider: entry.provider,
|
|
255
331
|
model: entry.model,
|
|
256
|
-
api: ctx.model?.api
|
|
332
|
+
api: entry.api || sessionModel?.api || ctx.model?.api || "openai-responses",
|
|
257
333
|
usage:
|
|
258
334
|
entry.usage ?? {
|
|
259
335
|
input: 0,
|
|
@@ -331,7 +407,7 @@ function ensureTranscriptTurn(state: BtwTranscriptState): number {
|
|
|
331
407
|
const turnId = state.nextTurnId++;
|
|
332
408
|
state.currentTurnId = turnId;
|
|
333
409
|
state.lastTurnId = turnId;
|
|
334
|
-
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">);
|
|
335
411
|
return turnId;
|
|
336
412
|
}
|
|
337
413
|
|
|
@@ -345,7 +421,7 @@ function finishTranscriptTurn(state: BtwTranscriptState, turnId?: number | null)
|
|
|
345
421
|
(entry) => entry.turnId === resolvedTurnId && entry.type === "turn-boundary" && entry.phase === "end",
|
|
346
422
|
);
|
|
347
423
|
if (!hasEndBoundary) {
|
|
348
|
-
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">);
|
|
349
425
|
}
|
|
350
426
|
|
|
351
427
|
for (const entry of state.entries) {
|
|
@@ -410,8 +486,18 @@ function ensureTranscriptTurnForUserMessage(state: BtwTranscriptState): number {
|
|
|
410
486
|
return ensureTranscriptTurn(state);
|
|
411
487
|
}
|
|
412
488
|
|
|
413
|
-
function extractMessageText(message: { content?: AssistantMessage["content"] }): string {
|
|
414
|
-
|
|
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();
|
|
415
501
|
}
|
|
416
502
|
|
|
417
503
|
function upsertUserMessageEntry(state: BtwTranscriptState, turnId: number, text: string): void {
|
|
@@ -425,7 +511,7 @@ function upsertUserMessageEntry(state: BtwTranscriptState, turnId: number, text:
|
|
|
425
511
|
return;
|
|
426
512
|
}
|
|
427
513
|
|
|
428
|
-
appendTranscriptEntry(state, { type: "user-message", turnId, text });
|
|
514
|
+
appendTranscriptEntry(state, { type: "user-message", turnId, text } as Omit<Extract<BtwTranscriptEntry, { type: "user-message" }>, "id">);
|
|
429
515
|
}
|
|
430
516
|
|
|
431
517
|
function upsertTranscriptTextEntry(
|
|
@@ -446,7 +532,7 @@ function upsertTranscriptTextEntry(
|
|
|
446
532
|
return;
|
|
447
533
|
}
|
|
448
534
|
|
|
449
|
-
appendTranscriptEntry(state, { type, turnId, text, streaming });
|
|
535
|
+
appendTranscriptEntry(state, { type, turnId, text, streaming } as Omit<Extract<BtwTranscriptEntry, { type: "thinking" | "assistant-text" }>, "id">);
|
|
450
536
|
}
|
|
451
537
|
|
|
452
538
|
function summarizeToolResult(value: unknown, maxLength = 400): { content: string; truncated: boolean } {
|
|
@@ -517,7 +603,7 @@ function ensureToolCallEntry(
|
|
|
517
603
|
toolCallId,
|
|
518
604
|
toolName,
|
|
519
605
|
args,
|
|
520
|
-
});
|
|
606
|
+
} as Omit<Extract<BtwTranscriptEntry, { type: "tool-call" }>, "id">);
|
|
521
607
|
const record = { turnId, callEntryId: callEntry.id };
|
|
522
608
|
state.toolCalls.set(toolCallId, record);
|
|
523
609
|
return record;
|
|
@@ -556,21 +642,17 @@ function upsertToolResultEntry(
|
|
|
556
642
|
truncated,
|
|
557
643
|
isError,
|
|
558
644
|
streaming,
|
|
559
|
-
});
|
|
645
|
+
} as Omit<Extract<BtwTranscriptEntry, { type: "tool-result" }>, "id">);
|
|
560
646
|
toolCall.resultEntryId = resultEntry.id;
|
|
561
647
|
}
|
|
562
648
|
|
|
563
649
|
function applyAssistantMessageToTranscript(
|
|
564
650
|
state: BtwTranscriptState,
|
|
565
651
|
turnId: number,
|
|
566
|
-
message:
|
|
652
|
+
message: AssistantMessage,
|
|
567
653
|
streaming: boolean,
|
|
568
654
|
): void {
|
|
569
|
-
|
|
570
|
-
return;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
const assistantMessage = message as AssistantMessage;
|
|
655
|
+
const assistantMessage = message;
|
|
574
656
|
const thinking = extractThinking(assistantMessage);
|
|
575
657
|
const answer = extractMessageText(assistantMessage);
|
|
576
658
|
|
|
@@ -842,7 +924,7 @@ function isThreadContinuationMarker(messages: Message[], index: number): boolean
|
|
|
842
924
|
|
|
843
925
|
function extractBtwHandoffThread(sessionRuntime: BtwSessionRuntime): BtwHandoffExchange[] {
|
|
844
926
|
const handoffMessages = sessionRuntime.session.state.messages.slice(sessionRuntime.sideThreadStartIndex);
|
|
845
|
-
const threadMessages = isThreadContinuationMarker(handoffMessages, 0) ? handoffMessages.slice(2) : handoffMessages;
|
|
927
|
+
const threadMessages = isThreadContinuationMarker(handoffMessages as Message[], 0) ? handoffMessages.slice(2) : handoffMessages;
|
|
846
928
|
const exchanges: BtwHandoffExchange[] = [];
|
|
847
929
|
let currentUser = "";
|
|
848
930
|
let currentAssistant = "";
|
|
@@ -922,8 +1004,8 @@ function getOverlayTitle(mode: BtwThreadMode): string {
|
|
|
922
1004
|
function buildTranscriptBadge(
|
|
923
1005
|
theme: ExtensionContext["ui"]["theme"],
|
|
924
1006
|
label: string,
|
|
925
|
-
background:
|
|
926
|
-
foreground:
|
|
1007
|
+
background: "userMessageBg" | "toolPendingBg" | "customMessageBg",
|
|
1008
|
+
foreground: "accent" | "warning" | "success",
|
|
927
1009
|
): string {
|
|
928
1010
|
return theme.bg(background, theme.fg(foreground, theme.bold(` ${label} `)));
|
|
929
1011
|
}
|
|
@@ -940,6 +1022,7 @@ class BtwOverlayComponent extends Container implements Focusable {
|
|
|
940
1022
|
private readonly getMode: () => BtwThreadMode;
|
|
941
1023
|
private readonly onSubmitCallback: (value: string) => void;
|
|
942
1024
|
private readonly onDismissCallback: () => void;
|
|
1025
|
+
private readonly onUnfocusCallback: () => void;
|
|
943
1026
|
private readonly tui: TUI;
|
|
944
1027
|
private readonly theme: ExtensionContext["ui"]["theme"];
|
|
945
1028
|
private transcriptLines: string[] = [];
|
|
@@ -947,6 +1030,10 @@ class BtwOverlayComponent extends Container implements Focusable {
|
|
|
947
1030
|
private transcriptViewportHeight = 8;
|
|
948
1031
|
private followTranscript = true;
|
|
949
1032
|
private _focused = false;
|
|
1033
|
+
private modeTextValue = "";
|
|
1034
|
+
private summaryTextValue = "";
|
|
1035
|
+
private statusTextValue = "";
|
|
1036
|
+
private hintsTextValue = "";
|
|
950
1037
|
|
|
951
1038
|
get focused(): boolean {
|
|
952
1039
|
return this._focused;
|
|
@@ -966,6 +1053,7 @@ class BtwOverlayComponent extends Container implements Focusable {
|
|
|
966
1053
|
getMode: () => BtwThreadMode,
|
|
967
1054
|
onSubmit: (value: string) => void,
|
|
968
1055
|
onDismiss: () => void,
|
|
1056
|
+
onUnfocus: () => void,
|
|
969
1057
|
) {
|
|
970
1058
|
super();
|
|
971
1059
|
this.tui = tui;
|
|
@@ -975,6 +1063,7 @@ class BtwOverlayComponent extends Container implements Focusable {
|
|
|
975
1063
|
this.getMode = getMode;
|
|
976
1064
|
this.onSubmitCallback = onSubmit;
|
|
977
1065
|
this.onDismissCallback = onDismiss;
|
|
1066
|
+
this.onUnfocusCallback = onUnfocus;
|
|
978
1067
|
|
|
979
1068
|
this.modeText = new Text("", 1, 0);
|
|
980
1069
|
this.summaryText = new Text("", 1, 0);
|
|
@@ -994,7 +1083,7 @@ class BtwOverlayComponent extends Container implements Focusable {
|
|
|
994
1083
|
|
|
995
1084
|
const originalHandleInput = this.input.handleInput.bind(this.input);
|
|
996
1085
|
this.input.handleInput = (data: string) => {
|
|
997
|
-
if (keybindings.matches(data, "
|
|
1086
|
+
if (keybindings.matches(data, "tui.select.cancel")) {
|
|
998
1087
|
this.onDismissCallback();
|
|
999
1088
|
return;
|
|
1000
1089
|
}
|
|
@@ -1034,10 +1123,15 @@ class BtwOverlayComponent extends Container implements Focusable {
|
|
|
1034
1123
|
|
|
1035
1124
|
private getDialogHeight(): number {
|
|
1036
1125
|
const terminalRows = process.stdout.rows ?? 30;
|
|
1037
|
-
return Math.max(
|
|
1126
|
+
return Math.max(18, Math.min(32, Math.floor(terminalRows * 0.78)));
|
|
1038
1127
|
}
|
|
1039
1128
|
|
|
1040
1129
|
handleInput(data: string): void {
|
|
1130
|
+
if (matchesBtwFocusShortcut(data)) {
|
|
1131
|
+
this.onUnfocusCallback();
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1041
1135
|
if (matchesKey(data, Key.pageUp)) {
|
|
1042
1136
|
this.followTranscript = false;
|
|
1043
1137
|
this.transcriptScrollOffset = Math.max(0, this.transcriptScrollOffset - Math.max(1, this.transcriptViewportHeight - 1));
|
|
@@ -1098,12 +1192,12 @@ class BtwOverlayComponent extends Container implements Focusable {
|
|
|
1098
1192
|
const hiddenBelow = Math.max(0, maxScroll - this.transcriptScrollOffset);
|
|
1099
1193
|
const summary =
|
|
1100
1194
|
hiddenAbove || hiddenBelow
|
|
1101
|
-
? `${this.
|
|
1102
|
-
: this.
|
|
1195
|
+
? `${this.summaryTextValue.trim()} · ↑${hiddenAbove} ↓${hiddenBelow}`
|
|
1196
|
+
: this.summaryTextValue.trim();
|
|
1103
1197
|
|
|
1104
1198
|
const lines = [this.borderLine(innerWidth, "top")];
|
|
1105
1199
|
|
|
1106
|
-
lines.push(this.frameLine(this.theme.fg("accent", this.theme.bold(this.
|
|
1200
|
+
lines.push(this.frameLine(this.theme.fg("accent", this.theme.bold(this.modeTextValue.trim())), innerWidth));
|
|
1107
1201
|
lines.push(this.frameLine(this.theme.fg("dim", summary), innerWidth));
|
|
1108
1202
|
lines.push(this.ruleLine(innerWidth));
|
|
1109
1203
|
|
|
@@ -1115,9 +1209,9 @@ class BtwOverlayComponent extends Container implements Focusable {
|
|
|
1115
1209
|
}
|
|
1116
1210
|
|
|
1117
1211
|
lines.push(this.ruleLine(innerWidth));
|
|
1118
|
-
lines.push(this.frameLine(this.theme.fg("warning", this.
|
|
1212
|
+
lines.push(this.frameLine(this.theme.fg("warning", this.statusTextValue.trim()), innerWidth));
|
|
1119
1213
|
lines.push(this.inputFrameLine(dialogWidth));
|
|
1120
|
-
lines.push(this.frameLine(this.theme.fg("dim", this.
|
|
1214
|
+
lines.push(this.frameLine(this.theme.fg("dim", this.hintsTextValue.trim()), innerWidth));
|
|
1121
1215
|
lines.push(this.borderLine(innerWidth, "bottom"));
|
|
1122
1216
|
|
|
1123
1217
|
return lines;
|
|
@@ -1137,11 +1231,13 @@ class BtwOverlayComponent extends Container implements Focusable {
|
|
|
1137
1231
|
}
|
|
1138
1232
|
|
|
1139
1233
|
refresh(): void {
|
|
1140
|
-
this.
|
|
1234
|
+
this.modeTextValue = `${getOverlayTitle(this.getMode())} · hidden thread preserved`;
|
|
1235
|
+
this.modeText.setText(this.modeTextValue);
|
|
1141
1236
|
const entries = this.readTranscriptEntries();
|
|
1142
1237
|
const exchanges = getCompletedExchangeCount(entries);
|
|
1143
1238
|
const active = hasStreamingTranscriptEntry(entries) ? " · streaming" : " · idle";
|
|
1144
|
-
this.
|
|
1239
|
+
this.summaryTextValue = `${exchanges} exchange${exchanges === 1 ? "" : "s"}${active}`;
|
|
1240
|
+
this.summaryText.setText(this.summaryTextValue);
|
|
1145
1241
|
|
|
1146
1242
|
this.transcriptLines = buildOverlayTranscript(entries, this.theme);
|
|
1147
1243
|
this.transcript.clear();
|
|
@@ -1150,8 +1246,10 @@ class BtwOverlayComponent extends Container implements Focusable {
|
|
|
1150
1246
|
}
|
|
1151
1247
|
|
|
1152
1248
|
const status = this.getStatus() ?? "Ready. Enter submits; Escape dismisses without clearing.";
|
|
1153
|
-
this.
|
|
1154
|
-
this.
|
|
1249
|
+
this.statusTextValue = status;
|
|
1250
|
+
this.statusText.setText(this.statusTextValue);
|
|
1251
|
+
this.hintsTextValue = "Enter submit · Alt+/ toggle focus · Escape dismiss · PgUp/PgDn scroll";
|
|
1252
|
+
this.hintsText.setText(this.hintsTextValue);
|
|
1155
1253
|
this.tui.requestRender();
|
|
1156
1254
|
}
|
|
1157
1255
|
}
|
|
@@ -1159,6 +1257,8 @@ class BtwOverlayComponent extends Container implements Focusable {
|
|
|
1159
1257
|
export default function (pi: ExtensionAPI) {
|
|
1160
1258
|
let pendingThread: BtwDetails[] = [];
|
|
1161
1259
|
let pendingMode: BtwThreadMode = "contextual";
|
|
1260
|
+
let btwModelOverride: SessionModel | null = null;
|
|
1261
|
+
let btwThinkingOverride: SessionThinkingLevel | null = null;
|
|
1162
1262
|
let transcriptState = createEmptyTranscriptState();
|
|
1163
1263
|
let overlayStatus: string | null = null;
|
|
1164
1264
|
let overlayDraft = "";
|
|
@@ -1189,6 +1289,32 @@ export default function (pi: ExtensionAPI) {
|
|
|
1189
1289
|
overlayRuntime = null;
|
|
1190
1290
|
}
|
|
1191
1291
|
|
|
1292
|
+
function toggleOverlayFocus(): void {
|
|
1293
|
+
const handle = overlayRuntime?.handle;
|
|
1294
|
+
if (!handle) {
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
handle.setHidden(false);
|
|
1299
|
+
if (handle.isFocused()) {
|
|
1300
|
+
handle.unfocus();
|
|
1301
|
+
} else {
|
|
1302
|
+
handle.focus();
|
|
1303
|
+
}
|
|
1304
|
+
overlayRuntime?.refresh?.();
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
function focusOverlay(): void {
|
|
1308
|
+
const handle = overlayRuntime?.handle;
|
|
1309
|
+
if (!handle) {
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
handle.setHidden(false);
|
|
1314
|
+
handle.focus();
|
|
1315
|
+
overlayRuntime?.refresh?.();
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1192
1318
|
function removeBtwSessionSubscription(sessionRuntime: BtwSessionRuntime, unsubscribe: () => void): void {
|
|
1193
1319
|
if (!sessionRuntime.subscriptions.delete(unsubscribe)) {
|
|
1194
1320
|
return;
|
|
@@ -1278,26 +1404,161 @@ export default function (pi: ExtensionAPI) {
|
|
|
1278
1404
|
await disposeBtwSession();
|
|
1279
1405
|
}
|
|
1280
1406
|
|
|
1407
|
+
async function resolveBtwModel(
|
|
1408
|
+
ctx: ExtensionCommandContext,
|
|
1409
|
+
notifyOnFallback = false,
|
|
1410
|
+
): Promise<ResolvedBtwModel> {
|
|
1411
|
+
if (btwModelOverride) {
|
|
1412
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(btwModelOverride);
|
|
1413
|
+
if (auth.ok && auth.apiKey) {
|
|
1414
|
+
return {
|
|
1415
|
+
model: btwModelOverride,
|
|
1416
|
+
source: "override",
|
|
1417
|
+
configuredOverride: btwModelOverride,
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
const fallbackReason = ctx.model
|
|
1422
|
+
? `Configured BTW model ${formatModelRef(btwModelOverride)} has no credentials. Falling back to main model ${formatModelRef(
|
|
1423
|
+
ctx.model,
|
|
1424
|
+
)}.`
|
|
1425
|
+
: `Configured BTW model ${formatModelRef(btwModelOverride)} has no credentials, and no main model is active.`;
|
|
1426
|
+
if (notifyOnFallback) {
|
|
1427
|
+
notify(ctx, fallbackReason, "warning");
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
if (ctx.model) {
|
|
1431
|
+
return {
|
|
1432
|
+
model: ctx.model,
|
|
1433
|
+
source: "main",
|
|
1434
|
+
configuredOverride: btwModelOverride,
|
|
1435
|
+
fallbackReason,
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
return {
|
|
1440
|
+
model: null,
|
|
1441
|
+
source: "none",
|
|
1442
|
+
configuredOverride: btwModelOverride,
|
|
1443
|
+
fallbackReason,
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
if (ctx.model) {
|
|
1448
|
+
return {
|
|
1449
|
+
model: ctx.model,
|
|
1450
|
+
source: "main",
|
|
1451
|
+
configuredOverride: null,
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
return {
|
|
1456
|
+
model: null,
|
|
1457
|
+
source: "none",
|
|
1458
|
+
configuredOverride: null,
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
async function resolveBtwSettings(
|
|
1463
|
+
ctx: ExtensionCommandContext,
|
|
1464
|
+
notifyOnFallback = false,
|
|
1465
|
+
): Promise<ResolvedBtwSettings> {
|
|
1466
|
+
const resolvedModel = await resolveBtwModel(ctx, notifyOnFallback);
|
|
1467
|
+
const thinkingLevel = btwThinkingOverride ?? (pi.getThinkingLevel() as SessionThinkingLevel);
|
|
1468
|
+
|
|
1469
|
+
return {
|
|
1470
|
+
model: resolvedModel.model,
|
|
1471
|
+
modelSource: resolvedModel.source,
|
|
1472
|
+
configuredModelOverride: resolvedModel.configuredOverride,
|
|
1473
|
+
thinkingLevel,
|
|
1474
|
+
thinkingSource: btwThinkingOverride ? "override" : "main",
|
|
1475
|
+
fallbackReason: resolvedModel.fallbackReason,
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
function describeResolvedModel(settings: ResolvedBtwSettings): string {
|
|
1480
|
+
if (!settings.model) {
|
|
1481
|
+
if (settings.configuredModelOverride && settings.fallbackReason) {
|
|
1482
|
+
return `BTW model unavailable. ${settings.fallbackReason}`;
|
|
1483
|
+
}
|
|
1484
|
+
return "BTW model unavailable. No active model selected.";
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
const source =
|
|
1488
|
+
settings.modelSource === "override"
|
|
1489
|
+
? "override"
|
|
1490
|
+
: settings.configuredModelOverride
|
|
1491
|
+
? "inherited fallback"
|
|
1492
|
+
: "inherits main thread";
|
|
1493
|
+
return `BTW model: ${formatModelRef(settings.model)} (${source}).${
|
|
1494
|
+
settings.fallbackReason ? ` ${settings.fallbackReason}` : ""
|
|
1495
|
+
}`;
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
function describeResolvedThinking(settings: ResolvedBtwSettings): string {
|
|
1499
|
+
const source = settings.thinkingSource === "override" ? "override" : "inherits main thread";
|
|
1500
|
+
return `BTW thinking: ${settings.thinkingLevel} (${source}).`;
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
async function setBtwModelOverride(ctx: ExtensionCommandContext, nextModel: SessionModel | null): Promise<void> {
|
|
1504
|
+
btwModelOverride = nextModel;
|
|
1505
|
+
const details: BtwModelOverrideDetails = nextModel
|
|
1506
|
+
? { action: "set", timestamp: Date.now(), provider: nextModel.provider, id: nextModel.id, api: nextModel.api }
|
|
1507
|
+
: { action: "clear", timestamp: Date.now() };
|
|
1508
|
+
pi.appendEntry(BTW_MODEL_OVERRIDE_TYPE, details);
|
|
1509
|
+
await disposeBtwSession();
|
|
1510
|
+
const settings = await resolveBtwSettings(ctx);
|
|
1511
|
+
const message = nextModel
|
|
1512
|
+
? `BTW model override set to ${formatModelRef(nextModel)}.`
|
|
1513
|
+
: "BTW model override cleared. BTW now inherits the main thread model.";
|
|
1514
|
+
setOverlayStatus(message, ctx);
|
|
1515
|
+
notify(ctx, `${message} ${describeResolvedModel(settings)}`, "info");
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
async function setBtwThinkingOverride(
|
|
1519
|
+
ctx: ExtensionCommandContext,
|
|
1520
|
+
nextThinkingLevel: SessionThinkingLevel | null,
|
|
1521
|
+
): Promise<void> {
|
|
1522
|
+
btwThinkingOverride = nextThinkingLevel;
|
|
1523
|
+
const details: BtwThinkingOverrideDetails = nextThinkingLevel
|
|
1524
|
+
? { action: "set", timestamp: Date.now(), thinkingLevel: nextThinkingLevel }
|
|
1525
|
+
: { action: "clear", timestamp: Date.now() };
|
|
1526
|
+
pi.appendEntry(BTW_THINKING_OVERRIDE_TYPE, details);
|
|
1527
|
+
await disposeBtwSession();
|
|
1528
|
+
const settings = await resolveBtwSettings(ctx);
|
|
1529
|
+
const message = nextThinkingLevel
|
|
1530
|
+
? `BTW thinking override set to ${nextThinkingLevel}.`
|
|
1531
|
+
: "BTW thinking override cleared. BTW now inherits the main thread thinking level.";
|
|
1532
|
+
setOverlayStatus(message, ctx);
|
|
1533
|
+
notify(ctx, `${message} ${describeResolvedThinking(settings)}`, "info");
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1281
1536
|
async function createBtwSubSession(ctx: ExtensionCommandContext, mode: BtwThreadMode): Promise<BtwSessionRuntime> {
|
|
1537
|
+
const settings = await resolveBtwSettings(ctx, true);
|
|
1538
|
+
if (!settings.model) {
|
|
1539
|
+
throw new Error(settings.fallbackReason || "No active model selected.");
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1282
1542
|
const { session } = await createAgentSession({
|
|
1283
1543
|
sessionManager: SessionManager.inMemory(),
|
|
1284
|
-
model:
|
|
1544
|
+
model: settings.model,
|
|
1285
1545
|
modelRegistry: ctx.modelRegistry as AgentSession["modelRegistry"],
|
|
1286
|
-
thinkingLevel:
|
|
1546
|
+
thinkingLevel: settings.thinkingLevel,
|
|
1287
1547
|
tools: codingTools,
|
|
1288
1548
|
resourceLoader: createBtwResourceLoader(ctx),
|
|
1289
1549
|
});
|
|
1290
1550
|
|
|
1291
|
-
const { messages: seedMessages, sideThreadStartIndex } = buildBtwSeedState(ctx, pendingThread, mode);
|
|
1551
|
+
const { messages: seedMessages, sideThreadStartIndex } = buildBtwSeedState(ctx, pendingThread, mode, settings.model);
|
|
1292
1552
|
if (seedMessages.length > 0) {
|
|
1293
|
-
session.agent.
|
|
1553
|
+
session.agent.state.messages = seedMessages as typeof session.state.messages;
|
|
1294
1554
|
}
|
|
1295
1555
|
|
|
1296
1556
|
return { session, mode, subscriptions: new Set(), sideThreadStartIndex };
|
|
1297
1557
|
}
|
|
1298
1558
|
|
|
1299
1559
|
async function ensureBtwSession(ctx: ExtensionCommandContext, mode: BtwThreadMode): Promise<BtwSessionRuntime | null> {
|
|
1300
|
-
|
|
1560
|
+
const settings = await resolveBtwSettings(ctx);
|
|
1561
|
+
if (!settings.model) {
|
|
1301
1562
|
return null;
|
|
1302
1563
|
}
|
|
1303
1564
|
|
|
@@ -1318,9 +1579,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1318
1579
|
|
|
1319
1580
|
if (overlayRuntime?.handle) {
|
|
1320
1581
|
subscribeOverlayToActiveBtwSession(ctx);
|
|
1321
|
-
|
|
1322
|
-
overlayRuntime.handle.focus();
|
|
1323
|
-
overlayRuntime.refresh?.();
|
|
1582
|
+
focusOverlay();
|
|
1324
1583
|
return;
|
|
1325
1584
|
}
|
|
1326
1585
|
|
|
@@ -1363,6 +1622,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
1363
1622
|
() => {
|
|
1364
1623
|
void dismissOverlaySession();
|
|
1365
1624
|
},
|
|
1625
|
+
() => {
|
|
1626
|
+
overlayRuntime?.handle?.unfocus();
|
|
1627
|
+
overlayRuntime?.refresh?.();
|
|
1628
|
+
},
|
|
1366
1629
|
);
|
|
1367
1630
|
|
|
1368
1631
|
overlay.focused = runtime.handle?.isFocused() ?? true;
|
|
@@ -1393,8 +1656,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
1393
1656
|
width: "78%",
|
|
1394
1657
|
minWidth: 72,
|
|
1395
1658
|
maxHeight: "78%",
|
|
1396
|
-
anchor: "center",
|
|
1397
|
-
margin: 1,
|
|
1659
|
+
anchor: "top-center",
|
|
1660
|
+
margin: { top: 1, left: 2, right: 2 },
|
|
1661
|
+
nonCapturing: true,
|
|
1398
1662
|
},
|
|
1399
1663
|
onHandle: (handle) => {
|
|
1400
1664
|
runtime.handle = handle;
|
|
@@ -1469,6 +1733,40 @@ export default function (pi: ExtensionAPI) {
|
|
|
1469
1733
|
return true;
|
|
1470
1734
|
}
|
|
1471
1735
|
|
|
1736
|
+
if (name === "btw:model") {
|
|
1737
|
+
const parsed = parseBtwModelArgs(trimmedArgs);
|
|
1738
|
+
if (parsed.action === "invalid") {
|
|
1739
|
+
setOverlayStatus(parsed.message, ctx);
|
|
1740
|
+
notify(ctx, parsed.message, "error");
|
|
1741
|
+
return true;
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
if (parsed.action === "show") {
|
|
1745
|
+
const settings = await resolveBtwSettings(ctx);
|
|
1746
|
+
const message = describeResolvedModel(settings);
|
|
1747
|
+
setOverlayStatus(message, ctx);
|
|
1748
|
+
notify(ctx, message, settings.model ? "info" : "warning");
|
|
1749
|
+
return true;
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
await setBtwModelOverride(ctx, parsed.action === "clear" ? null : parsed.model);
|
|
1753
|
+
return true;
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
if (name === "btw:thinking") {
|
|
1757
|
+
const parsed = parseBtwThinkingArgs(trimmedArgs);
|
|
1758
|
+
if (parsed.action === "show") {
|
|
1759
|
+
const settings = await resolveBtwSettings(ctx);
|
|
1760
|
+
const message = describeResolvedThinking(settings);
|
|
1761
|
+
setOverlayStatus(message, ctx);
|
|
1762
|
+
notify(ctx, message, "info");
|
|
1763
|
+
return true;
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
await setBtwThinkingOverride(ctx, parsed.action === "clear" ? null : parsed.thinkingLevel);
|
|
1767
|
+
return true;
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1472
1770
|
if (name === "btw:inject") {
|
|
1473
1771
|
if (pendingThread.length === 0) {
|
|
1474
1772
|
notify(ctx, "No BTW thread to inject.", "warning");
|
|
@@ -1531,7 +1829,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1531
1829
|
|
|
1532
1830
|
function parseOverlayBtwCommand(value: string): { name: string; args: string } | null {
|
|
1533
1831
|
const trimmed = value.trim();
|
|
1534
|
-
const match = trimmed.match(/^\/(btw:(?:new|tangent|clear|inject|summarize))(?:\s+(.*))?$/);
|
|
1832
|
+
const match = trimmed.match(/^\/(btw:(?:new|tangent|clear|inject|summarize|model|thinking))(?:\s+(.*))?$/);
|
|
1535
1833
|
if (!match) {
|
|
1536
1834
|
return null;
|
|
1537
1835
|
}
|
|
@@ -1554,17 +1852,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
1554
1852
|
return;
|
|
1555
1853
|
}
|
|
1556
1854
|
|
|
1855
|
+
const cmdCtx = ctx as ExtensionCommandContext;
|
|
1557
1856
|
const btwCommand = parseOverlayBtwCommand(question);
|
|
1558
1857
|
if (btwCommand) {
|
|
1559
1858
|
setOverlayDraft("");
|
|
1560
|
-
await dispatchBtwCommand(btwCommand.name, btwCommand.args,
|
|
1859
|
+
await dispatchBtwCommand(btwCommand.name, btwCommand.args, cmdCtx);
|
|
1561
1860
|
return;
|
|
1562
1861
|
}
|
|
1563
1862
|
|
|
1564
1863
|
setOverlayDraft("");
|
|
1565
1864
|
setOverlayStatus("⏳ streaming...", ctx);
|
|
1566
1865
|
syncUi(ctx);
|
|
1567
|
-
await runBtw(
|
|
1866
|
+
await runBtw(cmdCtx, question, false, pendingMode);
|
|
1568
1867
|
}
|
|
1569
1868
|
|
|
1570
1869
|
async function resetThread(
|
|
@@ -1589,6 +1888,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
1589
1888
|
await disposeBtwSession();
|
|
1590
1889
|
pendingThread = [];
|
|
1591
1890
|
pendingMode = "contextual";
|
|
1891
|
+
btwModelOverride = null;
|
|
1892
|
+
btwThinkingOverride = null;
|
|
1592
1893
|
transcriptState = createEmptyTranscriptState();
|
|
1593
1894
|
overlayDraft = "";
|
|
1594
1895
|
lastUiContext = ctx;
|
|
@@ -1598,9 +1899,29 @@ export default function (pi: ExtensionAPI) {
|
|
|
1598
1899
|
let lastResetIndex = -1;
|
|
1599
1900
|
|
|
1600
1901
|
for (let i = 0; i < branch.length; i++) {
|
|
1902
|
+
if (isCustomEntry(branch[i], BTW_MODEL_OVERRIDE_TYPE)) {
|
|
1903
|
+
const details = branch[i].data as BtwModelOverrideDetails | undefined;
|
|
1904
|
+
btwModelOverride =
|
|
1905
|
+
details?.action === "set"
|
|
1906
|
+
? { provider: details.provider, id: details.id, api: details.api }
|
|
1907
|
+
: details?.action === "clear"
|
|
1908
|
+
? null
|
|
1909
|
+
: btwModelOverride;
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
if (isCustomEntry(branch[i], BTW_THINKING_OVERRIDE_TYPE)) {
|
|
1913
|
+
const details = branch[i].data as BtwThinkingOverrideDetails | undefined;
|
|
1914
|
+
btwThinkingOverride =
|
|
1915
|
+
details?.action === "set"
|
|
1916
|
+
? details.thinkingLevel
|
|
1917
|
+
: details?.action === "clear"
|
|
1918
|
+
? null
|
|
1919
|
+
: btwThinkingOverride;
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1601
1922
|
if (isCustomEntry(branch[i], BTW_RESET_TYPE)) {
|
|
1602
1923
|
lastResetIndex = i;
|
|
1603
|
-
const details = branch[i]
|
|
1924
|
+
const details = (branch[i] as unknown as { data?: BtwResetDetails }).data;
|
|
1604
1925
|
pendingMode = details?.mode ?? "contextual";
|
|
1605
1926
|
}
|
|
1606
1927
|
}
|
|
@@ -1610,13 +1931,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
1610
1931
|
continue;
|
|
1611
1932
|
}
|
|
1612
1933
|
|
|
1613
|
-
const details = entry
|
|
1934
|
+
const details = (entry as unknown as { data?: BtwDetails }).data;
|
|
1614
1935
|
if (!details?.question || !details.answer) {
|
|
1615
1936
|
continue;
|
|
1616
1937
|
}
|
|
1617
1938
|
|
|
1618
|
-
|
|
1619
|
-
|
|
1939
|
+
const normalizedDetails: BtwDetails = {
|
|
1940
|
+
...details,
|
|
1941
|
+
api: details.api || ctx.model?.api || "openai-responses",
|
|
1942
|
+
};
|
|
1943
|
+
|
|
1944
|
+
pendingThread.push(normalizedDetails);
|
|
1945
|
+
appendPersistedTranscriptTurn(transcriptState, normalizedDetails);
|
|
1620
1946
|
}
|
|
1621
1947
|
|
|
1622
1948
|
syncUi(ctx);
|
|
@@ -1629,16 +1955,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
1629
1955
|
mode: BtwThreadMode,
|
|
1630
1956
|
): Promise<void> {
|
|
1631
1957
|
lastUiContext = ctx;
|
|
1632
|
-
const
|
|
1958
|
+
const settings = await resolveBtwSettings(ctx);
|
|
1959
|
+
const model = settings.model;
|
|
1633
1960
|
if (!model) {
|
|
1634
|
-
|
|
1635
|
-
|
|
1961
|
+
const message = settings.fallbackReason || "No active model selected.";
|
|
1962
|
+
setOverlayStatus(message, ctx);
|
|
1963
|
+
notify(ctx, message, "error");
|
|
1636
1964
|
return;
|
|
1637
1965
|
}
|
|
1638
1966
|
|
|
1639
|
-
const
|
|
1640
|
-
if (!apiKey) {
|
|
1641
|
-
const message = `No credentials available for ${model.provider}/${model.id}
|
|
1967
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
1968
|
+
if (!auth.ok || !auth.apiKey) {
|
|
1969
|
+
const message = auth.ok ? `No credentials available for ${model.provider}/${model.id}.` : auth.error;
|
|
1642
1970
|
setOverlayStatus(message, ctx);
|
|
1643
1971
|
notify(ctx, message, "error");
|
|
1644
1972
|
await ensureOverlay(ctx);
|
|
@@ -1655,7 +1983,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1655
1983
|
const session = sessionRuntime.session;
|
|
1656
1984
|
const wasBusy = !ctx.isIdle();
|
|
1657
1985
|
pendingMode = mode;
|
|
1658
|
-
const thinkingLevel =
|
|
1986
|
+
const thinkingLevel = settings.thinkingLevel;
|
|
1659
1987
|
|
|
1660
1988
|
setOverlayStatus("⏳ streaming...", ctx);
|
|
1661
1989
|
await ensureOverlay(ctx);
|
|
@@ -1688,6 +2016,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1688
2016
|
answer,
|
|
1689
2017
|
provider: model.provider,
|
|
1690
2018
|
model: model.id,
|
|
2019
|
+
api: model.api,
|
|
1691
2020
|
thinkingLevel,
|
|
1692
2021
|
timestamp: Date.now(),
|
|
1693
2022
|
usage: response.usage,
|
|
@@ -1736,14 +2065,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
1736
2065
|
}
|
|
1737
2066
|
|
|
1738
2067
|
async function summarizeThread(ctx: ExtensionCommandContext, thread: BtwHandoffExchange[]): Promise<string> {
|
|
1739
|
-
const
|
|
2068
|
+
const settings = await resolveBtwSettings(ctx, true);
|
|
2069
|
+
const model = settings.model;
|
|
1740
2070
|
if (!model) {
|
|
1741
|
-
throw new Error("No active model selected.");
|
|
2071
|
+
throw new Error(settings.fallbackReason || "No active model selected.");
|
|
1742
2072
|
}
|
|
1743
2073
|
|
|
1744
|
-
const
|
|
1745
|
-
if (!apiKey) {
|
|
1746
|
-
throw new Error(`No credentials available for ${model.provider}/${model.id}.`);
|
|
2074
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
2075
|
+
if (!auth.ok || !auth.apiKey) {
|
|
2076
|
+
throw new Error(auth.ok ? `No credentials available for ${model.provider}/${model.id}.` : auth.error);
|
|
1747
2077
|
}
|
|
1748
2078
|
|
|
1749
2079
|
const { session } = await createAgentSession({
|
|
@@ -1795,7 +2125,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
1795
2125
|
|
|
1796
2126
|
if (expanded && details) {
|
|
1797
2127
|
lines.push(
|
|
1798
|
-
theme.fg(
|
|
2128
|
+
theme.fg(
|
|
2129
|
+
"dim",
|
|
2130
|
+
`model: ${details.provider}/${details.model} (${details.api ?? "openai-responses"}) · thinking: ${details.thinkingLevel}`,
|
|
2131
|
+
),
|
|
1799
2132
|
);
|
|
1800
2133
|
|
|
1801
2134
|
if (details.usage) {
|
|
@@ -1823,10 +2156,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
1823
2156
|
await restoreThread(ctx);
|
|
1824
2157
|
});
|
|
1825
2158
|
|
|
1826
|
-
pi.on("session_switch", async (_event, ctx) => {
|
|
1827
|
-
await restoreThread(ctx);
|
|
1828
|
-
});
|
|
1829
|
-
|
|
1830
2159
|
pi.on("session_tree", async (_event, ctx) => {
|
|
1831
2160
|
await restoreThread(ctx);
|
|
1832
2161
|
});
|
|
@@ -1836,6 +2165,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
1836
2165
|
dismissOverlay();
|
|
1837
2166
|
});
|
|
1838
2167
|
|
|
2168
|
+
for (const shortcut of BTW_FOCUS_SHORTCUTS) {
|
|
2169
|
+
pi.registerShortcut(shortcut, {
|
|
2170
|
+
description: "Toggle BTW overlay focus while leaving it open.",
|
|
2171
|
+
handler: async (_ctx) => {
|
|
2172
|
+
toggleOverlayFocus();
|
|
2173
|
+
},
|
|
2174
|
+
});
|
|
2175
|
+
}
|
|
2176
|
+
|
|
1839
2177
|
pi.registerCommand("btw", {
|
|
1840
2178
|
description: "Continue a side conversation in a focused BTW modal. Add --save to also persist a visible note.",
|
|
1841
2179
|
handler: async (args, ctx) => {
|
|
@@ -1877,4 +2215,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
1877
2215
|
await dispatchBtwCommand("btw:summarize", args, ctx);
|
|
1878
2216
|
},
|
|
1879
2217
|
});
|
|
2218
|
+
|
|
2219
|
+
pi.registerCommand("btw:model", {
|
|
2220
|
+
description: "Show, set, or clear the BTW-only model override.",
|
|
2221
|
+
handler: async (args, ctx) => {
|
|
2222
|
+
await dispatchBtwCommand("btw:model", args, ctx);
|
|
2223
|
+
},
|
|
2224
|
+
});
|
|
2225
|
+
|
|
2226
|
+
pi.registerCommand("btw:thinking", {
|
|
2227
|
+
description: "Show, set, or clear the BTW-only thinking override.",
|
|
2228
|
+
handler: async (args, ctx) => {
|
|
2229
|
+
await dispatchBtwCommand("btw:thinking", args, ctx);
|
|
2230
|
+
},
|
|
2231
|
+
});
|
|
1880
2232
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-btw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.7",
|
|
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
|
}
|