@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/README.md +125 -88
- package/dist/index.ts +159 -50
- package/dist/index.ts.map +2 -2
- package/package.json +5 -8
- package/src/btw.ts +31 -26
- package/src/fullscreen-ui.ts +130 -22
- package/src/menu.ts +34 -2
- package/src/settings.ts +16 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@narumitw/pi-btw",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
38
|
-
"@earendil-works/pi-ai": "0.84.
|
|
39
|
-
"@earendil-works/pi-coding-agent": "0.84.
|
|
40
|
-
"@earendil-works/pi-tui": "0.84.
|
|
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
|
|
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(
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
state
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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) {
|
package/src/fullscreen-ui.ts
CHANGED
|
@@ -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
|
|
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
|
-
((
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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 =
|
|
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
|
|
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
|
|