@narumitw/pi-btw 0.55.4 → 0.56.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narumitw/pi-btw",
3
- "version": "0.55.4",
3
+ "version": "0.56.1",
4
4
  "description": "Pi extension that adds a /btw side-question command.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -23,9 +23,6 @@
23
23
  "./dist/index.ts"
24
24
  ]
25
25
  },
26
- "piExtension": {
27
- "lifecycle": "stable"
28
- },
29
26
  "scripts": {
30
27
  "build": "node scripts/build-runtime.mjs",
31
28
  "check": "npm run build && biome check . && npm run typecheck",
@@ -34,10 +31,10 @@
34
31
  "prepack": "npm run build"
35
32
  },
36
33
  "devDependencies": {
37
- "@biomejs/biome": "2.5.10",
38
- "@earendil-works/pi-ai": "0.84.3",
39
- "@earendil-works/pi-coding-agent": "0.84.3",
40
- "@earendil-works/pi-tui": "0.84.3",
34
+ "@biomejs/biome": "2.5.11",
35
+ "@earendil-works/pi-ai": "0.84.4",
36
+ "@earendil-works/pi-coding-agent": "0.84.4",
37
+ "@earendil-works/pi-tui": "0.84.4",
41
38
  "esbuild": "0.28.2",
42
39
  "typescript": "7.0.2"
43
40
  },
package/src/btw.ts CHANGED
@@ -33,6 +33,7 @@ import {
33
33
  } from "./menu.js";
34
34
  import {
35
35
  type BtwSettings,
36
+ effectiveFullscreenCopyOnSelect,
36
37
  effectiveRememberThinkingLevelChanges,
37
38
  parseBtwModelReference,
38
39
  readBtwSettings,
@@ -193,7 +194,7 @@ export async function loadBtwThinkingLevel(
193
194
 
194
195
  options.warn?.(
195
196
  sanitizeSingleLine(
196
- `pi-btw settings ignored: ${settings.reason}; expected optional model "provider/model-id", omitted thinkingLevel for Same as main thread or thinkingLevel "${BTW_THINKING_LEVELS.join('" | "')}", and boolean rememberThinkingLevelChanges. Using current Pi thinking level.`,
197
+ `pi-btw settings ignored: ${settings.reason}; expected optional model "provider/model-id", omitted thinkingLevel for Same as main thread or thinkingLevel "${BTW_THINKING_LEVELS.join('" | "')}", boolean rememberThinkingLevelChanges, and boolean fullscreenCopyOnSelect. Using current Pi thinking level.`,
197
198
  ),
198
199
  );
199
200
  return currentThinkingLevel;
@@ -314,31 +315,35 @@ export default function btw(pi: ExtensionAPI, dependencies: BtwExtensionDependen
314
315
  const startingTurnCount = state?.thread.turns.length ?? 0;
315
316
 
316
317
  try {
317
- await runFullscreen(ctx, (fullscreenCtx) => {
318
- if (!state) {
319
- const createdAt = Date.now();
320
- state = {
321
- id: `btw-${nextThreadNumber}`,
322
- thread: createSideThread(
323
- selectedConversationContext ??
324
- buildConversationContext(fullscreenCtx.sessionManager.getBranch()),
325
- ),
326
- thinkingLevel: settings.thinkingLevel ?? pi.getThinkingLevel(),
327
- createdAt,
328
- updatedAt: createdAt,
329
- };
330
- nextThreadNumber += 1;
331
- }
332
- return runThread({
333
- initialQuestion: question || undefined,
334
- selected: resolution.selected,
335
- thinkingLevel: state.thinkingLevel,
336
- rememberThinkingLevelChanges:
337
- !sameAsMainThinkingLevel && effectiveRememberThinkingLevelChanges(settings),
338
- state,
339
- ctx: fullscreenCtx,
340
- });
341
- });
318
+ await runFullscreen(
319
+ ctx,
320
+ (fullscreenCtx) => {
321
+ if (!state) {
322
+ const createdAt = Date.now();
323
+ state = {
324
+ id: `btw-${nextThreadNumber}`,
325
+ thread: createSideThread(
326
+ selectedConversationContext ??
327
+ buildConversationContext(fullscreenCtx.sessionManager.getBranch()),
328
+ ),
329
+ thinkingLevel: settings.thinkingLevel ?? pi.getThinkingLevel(),
330
+ createdAt,
331
+ updatedAt: createdAt,
332
+ };
333
+ nextThreadNumber += 1;
334
+ }
335
+ return runThread({
336
+ initialQuestion: question || undefined,
337
+ selected: resolution.selected,
338
+ thinkingLevel: state.thinkingLevel,
339
+ rememberThinkingLevelChanges:
340
+ !sameAsMainThinkingLevel && effectiveRememberThinkingLevelChanges(settings),
341
+ state,
342
+ ctx: fullscreenCtx,
343
+ });
344
+ },
345
+ { copyOnSelect: effectiveFullscreenCopyOnSelect(settings) },
346
+ );
342
347
  } finally {
343
348
  if (state?.title && state.thread.turns.length > 0) {
344
349
  if (state.thread.turns.length > startingTurnCount) {
@@ -30,6 +30,7 @@ type BtwCustomFactory<T> = (
30
30
  type BtwFullscreenTui = TUI & {
31
31
  flash?: (message: string, durationMs?: number) => void;
32
32
  setLayoutRoot(component: Component | undefined): void;
33
+ addInputListenerBeforeAll?(listener: TuiInputListener): () => void;
33
34
  addInputListenerBeforeViewport?(listener: TuiInputListener): () => void;
34
35
  };
35
36
 
@@ -37,17 +38,28 @@ export interface BtwFullscreenLayoutComponent extends Component {
37
38
  getFullscreenLayout(): Component;
38
39
  }
39
40
 
40
- export type BtwFullscreenTuiFactory = (parent: TUI, theme: Theme) => BtwFullscreenTui;
41
+ export interface BtwFullscreenOptions {
42
+ copyOnSelect?: boolean;
43
+ }
44
+
45
+ export type BtwFullscreenTuiFactory = (
46
+ parent: TUI,
47
+ theme: Theme,
48
+ keybindings: KeybindingsManager,
49
+ options: BtwFullscreenOptions,
50
+ ) => BtwFullscreenTui;
41
51
 
42
52
  export interface BtwFullscreenDependencies {
43
53
  createTui?: BtwFullscreenTuiFactory;
44
54
  openUrl?: (url: string) => void;
45
55
  copyToClipboard?: (text: string) => Promise<void>;
56
+ manualSelectionCopySupported?: boolean;
46
57
  }
47
58
 
48
59
  export type RunBtwFullscreen = <T>(
49
60
  ctx: ExtensionCommandContext,
50
61
  run: (ctx: ExtensionCommandContext) => Promise<T>,
62
+ options?: BtwFullscreenOptions,
51
63
  ) => Promise<T>;
52
64
 
53
65
  type FullscreenOutcome<T> = { kind: "completed"; value: T } | { kind: "failed"; error: unknown };
@@ -62,14 +74,23 @@ class FullscreenUiDisposedError extends Error {
62
74
  export async function runBtwFullscreen<T>(
63
75
  ctx: ExtensionCommandContext,
64
76
  run: (ctx: ExtensionCommandContext) => Promise<T>,
77
+ options: BtwFullscreenOptions = {},
65
78
  dependencies: BtwFullscreenDependencies = {},
66
79
  ): Promise<T> {
67
80
  const createTui =
68
81
  dependencies.createTui ??
69
- ((parent: TUI, theme: Theme) =>
82
+ ((
83
+ parent: TUI,
84
+ theme: Theme,
85
+ keybindings: KeybindingsManager,
86
+ fullscreenOptions: BtwFullscreenOptions,
87
+ ) =>
70
88
  createBtwFullscreenTui(
71
89
  parent,
72
90
  theme,
91
+ keybindings,
92
+ fullscreenOptions.copyOnSelect ?? true,
93
+ dependencies.manualSelectionCopySupported ?? hasManualSelectionCopyApi(),
73
94
  dependencies.openUrl ?? openUrlInBrowser,
74
95
  dependencies.copyToClipboard ?? copyToHostClipboard,
75
96
  ));
@@ -94,6 +115,7 @@ export async function runBtwFullscreen<T>(
94
115
  done(value);
95
116
  },
96
117
  createTui,
118
+ options,
97
119
  );
98
120
  return host;
99
121
  },
@@ -114,6 +136,7 @@ export async function runBtwFullscreen<T>(
114
136
  }
115
137
 
116
138
  type BtwInputListeners = {
139
+ beforeAll: Set<TuiInputListener>;
117
140
  beforeViewport: Set<TuiInputListener>;
118
141
  regular: Set<TuiInputListener>;
119
142
  };
@@ -122,7 +145,7 @@ const btwInputListeners = new WeakMap<BtwTuiAltScreen, BtwInputListeners>();
122
145
 
123
146
  function dispatchBtwInput(listeners: BtwInputListeners, data: string): TuiInputListenerResult {
124
147
  let current = data;
125
- for (const group of [listeners.beforeViewport, listeners.regular]) {
148
+ for (const group of [listeners.beforeAll, listeners.beforeViewport, listeners.regular]) {
126
149
  for (const listener of group) {
127
150
  const result = listener(current);
128
151
  if (result?.consume) return result;
@@ -133,10 +156,15 @@ function dispatchBtwInput(listeners: BtwInputListeners, data: string): TuiInputL
133
156
  }
134
157
 
135
158
  class BtwTuiAltScreen extends TuiAltScreen {
159
+ hasFocusedOverlay(): boolean {
160
+ return this.isOverlayFocused();
161
+ }
162
+
136
163
  override addInputListener(listener: TuiInputListener): () => void {
137
164
  let listeners = btwInputListeners.get(this);
138
165
  if (!listeners) {
139
166
  const registeredListeners: BtwInputListeners = {
167
+ beforeAll: new Set(),
140
168
  beforeViewport: new Set(),
141
169
  regular: new Set(),
142
170
  };
@@ -148,6 +176,13 @@ class BtwTuiAltScreen extends TuiAltScreen {
148
176
  return () => listeners.regular.delete(listener);
149
177
  }
150
178
 
179
+ addInputListenerBeforeAll(listener: TuiInputListener): () => void {
180
+ const listeners = btwInputListeners.get(this);
181
+ if (!listeners) return super.addInputListener(listener);
182
+ listeners.beforeAll.add(listener);
183
+ return () => listeners.beforeAll.delete(listener);
184
+ }
185
+
151
186
  addInputListenerBeforeViewport(listener: TuiInputListener): () => void {
152
187
  const listeners = btwInputListeners.get(this);
153
188
  if (!listeners) return super.addInputListener(listener);
@@ -161,33 +196,85 @@ class BtwTuiAltScreen extends TuiAltScreen {
161
196
  super.removeInputListener(listener);
162
197
  return;
163
198
  }
199
+ listeners.beforeAll.delete(listener);
164
200
  listeners.beforeViewport.delete(listener);
165
201
  listeners.regular.delete(listener);
166
202
  }
167
203
  }
168
204
 
205
+ const BRACKETED_PASTE_START = "\u001b[200~";
206
+ const BRACKETED_PASTE_END = "\u001b[201~";
207
+
208
+ function hasManualSelectionCopyApi(): boolean {
209
+ return (
210
+ typeof TuiAltScreen.prototype.hasActiveSelection === "function" &&
211
+ typeof TuiAltScreen.prototype.copyActiveSelectionToClipboard === "function"
212
+ );
213
+ }
214
+
169
215
  function createBtwFullscreenTui(
170
216
  parent: TUI,
171
217
  theme: Theme,
218
+ keybindings: KeybindingsManager,
219
+ copyOnSelect: boolean,
220
+ manualSelectionCopySupported: boolean,
172
221
  openUrl: (url: string) => void,
173
222
  copyToClipboard: (text: string) => Promise<void>,
174
223
  ): BtwFullscreenTui {
224
+ if (!copyOnSelect && !manualSelectionCopySupported) {
225
+ throw new Error(
226
+ "Manual fullscreen selection copying is unavailable in this Pi version; update Pi or enable automatic selection copying.",
227
+ );
228
+ }
175
229
  const styleSearchMatch = (text: string) =>
176
230
  theme.bg("searchMatchBg", theme.fg("searchMatchText", text));
177
- return new BtwTuiAltScreen(parent.terminal, parent.getShowHardwareCursor(), undefined, {
178
- mouse: true,
179
- searchMatchStyle: (text) => theme.underline(styleSearchMatch(text)),
180
- searchCurrentMatchStyle: (text) => theme.bold(theme.inverse(styleSearchMatch(text))),
181
- openUrl,
182
- copySelection: async (text) => {
183
- try {
184
- await copyToClipboard(text);
185
- return true;
186
- } catch {
187
- return false;
188
- }
231
+ const fullscreen = new BtwTuiAltScreen(
232
+ parent.terminal,
233
+ parent.getShowHardwareCursor(),
234
+ undefined,
235
+ {
236
+ mouse: true,
237
+ copyOnSelect,
238
+ searchMatchStyle: (text) => theme.underline(styleSearchMatch(text)),
239
+ searchCurrentMatchStyle: (text) => theme.bold(theme.inverse(styleSearchMatch(text))),
240
+ openUrl,
241
+ copySelection: async (text) => {
242
+ try {
243
+ await copyToClipboard(text);
244
+ return true;
245
+ } catch {
246
+ return false;
247
+ }
248
+ },
189
249
  },
190
- });
250
+ );
251
+ if (!copyOnSelect) {
252
+ let isInBracketedPaste = false;
253
+ fullscreen.addInputListenerBeforeViewport((data) => {
254
+ const wasInBracketedPaste = isInBracketedPaste;
255
+ const startsBracketedPaste = data.includes(BRACKETED_PASTE_START);
256
+ if (startsBracketedPaste) isInBracketedPaste = true;
257
+ if (isInBracketedPaste && data.includes(BRACKETED_PASTE_END)) {
258
+ isInBracketedPaste = false;
259
+ }
260
+ if (
261
+ wasInBracketedPaste ||
262
+ startsBracketedPaste ||
263
+ fullscreen.hasFocusedOverlay() ||
264
+ isKeyRelease(data) ||
265
+ !keybindings.matches(data, "app.message.copy")
266
+ ) {
267
+ return undefined;
268
+ }
269
+ if (!fullscreen.hasActiveSelection()) {
270
+ fullscreen.flash("No selection to copy");
271
+ return { consume: true };
272
+ }
273
+ void fullscreen.copyActiveSelectionToClipboard().catch(() => fullscreen.flash("Copy failed"));
274
+ return { consume: true };
275
+ });
276
+ }
277
+ return fullscreen;
191
278
  }
192
279
 
193
280
  // Pi does not export its browser opener, so mirror its shell-free launcher for this isolated TUI.
@@ -213,9 +300,10 @@ class BtwFullscreenHost<T> implements Component {
213
300
  private disposed = false;
214
301
  private finished = false;
215
302
  private parentStopped = false;
216
- private parentRestarted = false;
303
+ private parentRestoreAttempted = false;
217
304
  private fullscreenCreated = false;
218
305
  private fullscreenStopped = false;
306
+ private parentRestoreQueued = false;
219
307
  private cleanupError: unknown;
220
308
 
221
309
  constructor(
@@ -226,6 +314,7 @@ class BtwFullscreenHost<T> implements Component {
226
314
  private readonly run: (ctx: ExtensionCommandContext) => Promise<T>,
227
315
  private readonly done: (outcome: FullscreenOutcome<T>) => void,
228
316
  private readonly createTui: BtwFullscreenTuiFactory,
317
+ private readonly options: BtwFullscreenOptions,
229
318
  ) {
230
319
  queueMicrotask(() => void this.start());
231
320
  }
@@ -255,11 +344,12 @@ class BtwFullscreenHost<T> implements Component {
255
344
  this.parent.stop({ preserveScreen: true });
256
345
  this.parentStopped = true;
257
346
  if (this.disposed) throw new FullscreenUiDisposedError();
258
- this.fullscreen = this.createTui(this.parent, this.theme);
347
+ this.fullscreen = this.createTui(this.parent, this.theme, this.keybindings, this.options);
259
348
  this.fullscreenCreated = true;
260
349
  this.fullscreen.start();
261
350
  // Waiting for the custom promise would leave follow-up keys bound to the side TUI.
262
351
  const addHardCancelListener =
352
+ this.fullscreen.addInputListenerBeforeAll?.bind(this.fullscreen) ??
263
353
  this.fullscreen.addInputListenerBeforeViewport?.bind(this.fullscreen) ??
264
354
  this.fullscreen.addInputListener.bind(this.fullscreen);
265
355
  this.removeHardCancelListener = addHardCancelListener((data) => {
@@ -267,7 +357,11 @@ class BtwFullscreenHost<T> implements Component {
267
357
  try {
268
358
  this.hardCancelActiveCustom?.();
269
359
  } finally {
270
- this.restoreParent();
360
+ // ProcessTerminal.stop() destroys its active input buffer. Defer only the
361
+ // physical handoff so Windows can finish dispatching this Ctrl+C first.
362
+ // Pi has no public input injection, so do not replay bytes already coalesced
363
+ // behind the hard-cancel key.
364
+ this.queueParentRestore();
271
365
  }
272
366
  return { consume: true };
273
367
  });
@@ -287,9 +381,23 @@ class BtwFullscreenHost<T> implements Component {
287
381
  this.done(outcome);
288
382
  }
289
383
 
384
+ private queueParentRestore(): void {
385
+ if (this.parentRestoreQueued || this.parentRestoreAttempted) return;
386
+ this.parentRestoreQueued = true;
387
+ queueMicrotask(() => {
388
+ this.parentRestoreQueued = false;
389
+ this.restoreParent();
390
+ });
391
+ }
392
+
290
393
  private restoreParent(): void {
291
- this.removeHardCancelListener?.();
394
+ const removeHardCancelListener = this.removeHardCancelListener;
292
395
  this.removeHardCancelListener = undefined;
396
+ try {
397
+ removeHardCancelListener?.();
398
+ } catch (error) {
399
+ this.cleanupError ??= error;
400
+ }
293
401
  if (this.fullscreenCreated && !this.fullscreenStopped) {
294
402
  this.fullscreenStopped = true;
295
403
  try {
@@ -298,7 +406,7 @@ class BtwFullscreenHost<T> implements Component {
298
406
  this.cleanupError ??= error;
299
407
  }
300
408
  }
301
- if (!this.parentStopped || this.parentRestarted) return;
409
+ if (!this.parentStopped || this.parentRestoreAttempted) return;
302
410
  const parentOverlay = this.parentOverlay;
303
411
  this.parentOverlay = undefined;
304
412
  try {
@@ -307,8 +415,8 @@ class BtwFullscreenHost<T> implements Component {
307
415
  this.cleanupError ??= error;
308
416
  }
309
417
  try {
418
+ this.parentRestoreAttempted = true;
310
419
  this.parent.start();
311
- this.parentRestarted = true;
312
420
  this.parent.renderNow(false);
313
421
  } catch (error) {
314
422
  this.cleanupError ??= error;
package/src/menu.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  type BtwSettings,
10
10
  type BtwSettingsPatch,
11
11
  btwSettingsPath,
12
+ effectiveFullscreenCopyOnSelect,
12
13
  effectiveRememberThinkingLevelChanges,
13
14
  readBtwSettings,
14
15
  type UpdateBtwSettingsOptions,
@@ -48,7 +49,13 @@ export type BtwCommandMenuResult =
48
49
  | { kind: "resume"; threadId: string };
49
50
 
50
51
  type BtwMenuScreen = "main" | "resume" | "settings" | "invalid";
51
- type BtwMenuAction = "start" | "start-tree" | "resume" | "set-thinking" | "set-remember";
52
+ type BtwMenuAction =
53
+ | "start"
54
+ | "start-tree"
55
+ | "resume"
56
+ | "set-thinking"
57
+ | "set-remember"
58
+ | "set-fullscreen-copy";
52
59
  const SAME_AS_MAIN_THREAD = "Same as main thread";
53
60
  type BtwCustomOptions = Parameters<ExtensionCommandContext["ui"]["custom"]>[1];
54
61
 
@@ -111,6 +118,7 @@ export async function showBtwCommandMenu(
111
118
  title: "Pi BTW",
112
119
  lines: [
113
120
  `Thinking: ${displayThinkingSummary(state.settings)} · Remember changes: ${displayRememberSummary(state.settings)}`,
121
+ `Copy on select: ${effectiveFullscreenCopyOnSelect(state.settings) ? "On" : "Off"}`,
114
122
  ],
115
123
  items: [
116
124
  {
@@ -138,7 +146,7 @@ export async function showBtwCommandMenu(
138
146
  {
139
147
  id: "settings",
140
148
  label: "Settings",
141
- description: "Choose pi-btw thinking level and fixed-level shortcut memory",
149
+ description: "Choose thinking, shortcut memory, and selection copying",
142
150
  to: state.kind === "invalid" ? "invalid" : "settings",
143
151
  },
144
152
  ],
@@ -178,6 +186,15 @@ export async function showBtwCommandMenu(
178
186
  values: ["On", "Off"],
179
187
  action: "set-remember",
180
188
  },
189
+ {
190
+ id: "fullscreenCopyOnSelect",
191
+ label: "Copy selection automatically",
192
+ description:
193
+ "Copy mouse selections immediately instead of with the configured copy key.",
194
+ currentValue: effectiveFullscreenCopyOnSelect(state.settings) ? "On" : "Off",
195
+ values: ["On", "Off"],
196
+ action: "set-fullscreen-copy",
197
+ },
181
198
  ],
182
199
  }),
183
200
  invalid: ({ state }) => ({
@@ -240,6 +257,21 @@ export async function showBtwCommandMenu(
240
257
  return { kind: "rejected" };
241
258
  }
242
259
  },
260
+ "set-fullscreen-copy": async ({ value, signal }) => {
261
+ if (value !== "On" && value !== "Off") return { kind: "rejected" };
262
+ try {
263
+ await updateSettings(
264
+ { fullscreenCopyOnSelect: value === "On" },
265
+ { settingsPath, signal },
266
+ );
267
+ if (signal.aborted) return { kind: "rejected" };
268
+ notifySafely(ctx, `Copy selection automatically: ${value}.`, "info");
269
+ return { kind: "stay" };
270
+ } catch (error) {
271
+ if (!signal.aborted) notifySaveFailure(ctx, error);
272
+ return { kind: "rejected" };
273
+ }
274
+ },
243
275
  },
244
276
  });
245
277
 
package/src/settings.ts CHANGED
@@ -6,6 +6,7 @@ import { getAgentDir } from "@earendil-works/pi-coding-agent";
6
6
  import { BTW_THINKING_LEVELS, type BtwThinkingLevel } from "./side-thread.js";
7
7
 
8
8
  export const BTW_SETTINGS_FILE = "pi-btw.json";
9
+ export const DEFAULT_FULLSCREEN_COPY_ON_SELECT = true;
9
10
  export const DEFAULT_REMEMBER_THINKING_LEVEL_CHANGES = true;
10
11
  const MAX_SETTINGS_BYTES = 64 * 1024;
11
12
 
@@ -13,6 +14,7 @@ export interface BtwSettings {
13
14
  model?: string;
14
15
  thinkingLevel?: BtwThinkingLevel;
15
16
  rememberThinkingLevelChanges?: boolean;
17
+ fullscreenCopyOnSelect?: boolean;
16
18
  }
17
19
 
18
20
  export type BtwSettingsLoadResult =
@@ -23,6 +25,7 @@ export type BtwSettingsLoadResult =
23
25
  export interface BtwSettingsPatch {
24
26
  thinkingLevel?: BtwThinkingLevel;
25
27
  rememberThinkingLevelChanges?: boolean;
28
+ fullscreenCopyOnSelect?: boolean;
26
29
  }
27
30
 
28
31
  export interface UpdateBtwSettingsOptions {
@@ -58,6 +61,11 @@ export function normalizeBtwSettings(value: unknown): BtwSettings | undefined {
58
61
  if (typeof remember !== "boolean") return undefined;
59
62
  settings.rememberThinkingLevelChanges = remember;
60
63
  }
64
+ if (Object.hasOwn(value, "fullscreenCopyOnSelect")) {
65
+ const copyOnSelect = Reflect.get(value, "fullscreenCopyOnSelect");
66
+ if (typeof copyOnSelect !== "boolean") return undefined;
67
+ settings.fullscreenCopyOnSelect = copyOnSelect;
68
+ }
61
69
  return settings;
62
70
  }
63
71
 
@@ -70,6 +78,10 @@ export function parseBtwModelReference(
70
78
  return { provider: reference.slice(0, separator), modelId: reference.slice(separator + 1) };
71
79
  }
72
80
 
81
+ export function effectiveFullscreenCopyOnSelect(settings: BtwSettings): boolean {
82
+ return settings.fullscreenCopyOnSelect ?? DEFAULT_FULLSCREEN_COPY_ON_SELECT;
83
+ }
84
+
73
85
  export function effectiveRememberThinkingLevelChanges(settings: BtwSettings): boolean {
74
86
  return settings.rememberThinkingLevelChanges ?? DEFAULT_REMEMBER_THINKING_LEVEL_CHANGES;
75
87
  }
@@ -233,6 +245,10 @@ function applyBtwSettingsPatch(
233
245
  if (Object.hasOwn(patch, "rememberThinkingLevelChanges")) {
234
246
  updated.rememberThinkingLevelChanges = patch.rememberThinkingLevelChanges;
235
247
  }
248
+ if (Object.hasOwn(patch, "fullscreenCopyOnSelect")) {
249
+ if (patch.fullscreenCopyOnSelect === undefined) delete updated.fullscreenCopyOnSelect;
250
+ else updated.fullscreenCopyOnSelect = patch.fullscreenCopyOnSelect;
251
+ }
236
252
  return updated;
237
253
  }
238
254