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 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: ctx.model?.provider ?? "unknown",
229
- model: ctx.model?.id ?? "unknown",
230
- api: ctx.model?.api ?? "openai-responses",
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 ?? "openai-responses",
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
- return Array.isArray(message.content) ? extractText(message.content, "text") : "";
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: AgentSessionEvent extends { message: infer T } ? T : never,
652
+ message: AssistantMessage,
567
653
  streaming: boolean,
568
654
  ): void {
569
- if (!message || typeof message !== "object" || (message as { role?: string }).role !== "assistant") {
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: string,
926
- foreground: string,
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, "selectCancel")) {
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(16, Math.min(24, Math.floor(terminalRows * 0.7)));
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.summaryText.text.trim()} · ↑${hiddenAbove} ↓${hiddenBelow}`
1102
- : this.summaryText.text.trim();
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.modeText.text.trim())), innerWidth));
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.statusText.text.trim()), innerWidth));
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.hintsText.text.trim()), innerWidth));
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.modeText.setText(`${getOverlayTitle(this.getMode())} · hidden thread preserved`);
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.summaryText.setText(`${exchanges} exchange${exchanges === 1 ? "" : "s"}${active}`);
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.statusText.setText(status);
1154
- this.hintsText.setText("Enter submit · Escape dismiss · PgUp/PgDn scroll · /btw:clear resets thread");
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: ctx.model,
1544
+ model: settings.model,
1285
1545
  modelRegistry: ctx.modelRegistry as AgentSession["modelRegistry"],
1286
- thinkingLevel: pi.getThinkingLevel() as SessionThinkingLevel,
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.replaceMessages(seedMessages as typeof session.state.messages);
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
- if (!ctx.model) {
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
- overlayRuntime.handle.setHidden(false);
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, ctx);
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(ctx, question, false, pendingMode);
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].data as BtwResetDetails | undefined;
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.data as BtwDetails | undefined;
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
- pendingThread.push(details);
1619
- appendPersistedTranscriptTurn(transcriptState, details);
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 model = ctx.model;
1958
+ const settings = await resolveBtwSettings(ctx);
1959
+ const model = settings.model;
1633
1960
  if (!model) {
1634
- setOverlayStatus("No active model selected.", ctx);
1635
- notify(ctx, "No active model selected.", "error");
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 apiKey = await ctx.modelRegistry.getApiKey(model);
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 = pi.getThinkingLevel() as SessionThinkingLevel;
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 model = ctx.model;
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 apiKey = await ctx.modelRegistry.getApiKey(model);
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("dim", `model: ${details.provider}/${details.model} · thinking: ${details.thinkingLevel}`),
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.2.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
  }