@narumitw/pi-btw 0.42.1 → 0.46.0
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 +45 -24
- package/package.json +1 -1
- package/src/btw.ts +196 -159
- package/src/menu.ts +256 -0
- package/src/settings.ts +237 -0
- package/src/text.ts +10 -0
- package/src/transcript-pager.ts +88 -11
package/src/btw.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
type Api,
|
|
3
|
+
clampThinkingLevel,
|
|
4
|
+
getSupportedThinkingLevels,
|
|
5
|
+
type Model,
|
|
6
|
+
} from "@earendil-works/pi-ai";
|
|
4
7
|
import {
|
|
5
8
|
BorderedLoader,
|
|
6
9
|
type ExtensionAPI,
|
|
7
10
|
type ExtensionCommandContext,
|
|
8
|
-
getAgentDir,
|
|
9
11
|
type KeybindingsManager,
|
|
10
12
|
type Theme,
|
|
11
13
|
} from "@earendil-works/pi-coding-agent";
|
|
@@ -22,6 +24,18 @@ import {
|
|
|
22
24
|
getAnsweredTurns,
|
|
23
25
|
summarizeBringToMain,
|
|
24
26
|
} from "./bring-to-main.js";
|
|
27
|
+
import {
|
|
28
|
+
type BtwCommandMenuResult,
|
|
29
|
+
runBtwMenuPreservingEditor,
|
|
30
|
+
showBtwCommandMenu,
|
|
31
|
+
} from "./menu.js";
|
|
32
|
+
import {
|
|
33
|
+
type BtwSettings,
|
|
34
|
+
effectiveRememberThinkingLevelChanges,
|
|
35
|
+
parseBtwModelReference,
|
|
36
|
+
readBtwSettings,
|
|
37
|
+
updateBtwSettings,
|
|
38
|
+
} from "./settings.js";
|
|
25
39
|
import {
|
|
26
40
|
BTW_THINKING_LEVELS,
|
|
27
41
|
type BtwThinkingLevel,
|
|
@@ -30,12 +44,22 @@ import {
|
|
|
30
44
|
type SideQuestionAuth,
|
|
31
45
|
type SideThread,
|
|
32
46
|
} from "./side-thread.js";
|
|
47
|
+
import { sanitizeSingleLine } from "./text.js";
|
|
33
48
|
import {
|
|
34
49
|
BtwAnsweringView,
|
|
50
|
+
type BtwThinkingControl,
|
|
35
51
|
BtwTranscriptPager,
|
|
36
52
|
type TranscriptPagerAction,
|
|
37
53
|
} from "./transcript-pager.js";
|
|
38
54
|
|
|
55
|
+
export {
|
|
56
|
+
BTW_SETTINGS_FILE,
|
|
57
|
+
type BtwSettings,
|
|
58
|
+
type BtwSettingsLoadResult,
|
|
59
|
+
normalizeBtwSettings,
|
|
60
|
+
parseBtwModelReference,
|
|
61
|
+
readBtwSettings,
|
|
62
|
+
} from "./settings.js";
|
|
39
63
|
export {
|
|
40
64
|
BTW_THINKING_LEVELS,
|
|
41
65
|
type BtwThinkingLevel,
|
|
@@ -43,19 +67,9 @@ export {
|
|
|
43
67
|
completeSideQuestion,
|
|
44
68
|
loadCompleteSimple,
|
|
45
69
|
} from "./side-thread.js";
|
|
70
|
+
export { sanitizeSingleLine } from "./text.js";
|
|
46
71
|
|
|
47
72
|
const MAX_CONTEXT_CHARS = 40_000;
|
|
48
|
-
export const BTW_SETTINGS_FILE = "pi-btw.json";
|
|
49
|
-
|
|
50
|
-
export interface BtwSettings {
|
|
51
|
-
model?: string;
|
|
52
|
-
thinkingLevel?: BtwThinkingLevel;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export type BtwSettingsLoadResult =
|
|
56
|
-
| { kind: "missing" }
|
|
57
|
-
| { kind: "invalid"; reason: string }
|
|
58
|
-
| { kind: "loaded"; settings: BtwSettings };
|
|
59
73
|
|
|
60
74
|
interface LoadBtwThinkingLevelOptions {
|
|
61
75
|
settingsPath?: string;
|
|
@@ -84,50 +98,25 @@ export interface ResolvedBtwModel {
|
|
|
84
98
|
auth: SideQuestionAuth;
|
|
85
99
|
}
|
|
86
100
|
|
|
87
|
-
export function normalizeBtwSettings(value: unknown): BtwSettings | undefined {
|
|
88
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
|
|
89
|
-
|
|
90
|
-
const settings: BtwSettings = {};
|
|
91
|
-
if (Object.hasOwn(value, "model")) {
|
|
92
|
-
const model = Reflect.get(value, "model");
|
|
93
|
-
if (typeof model !== "string" || !parseBtwModelReference(model)) return undefined;
|
|
94
|
-
settings.model = model;
|
|
95
|
-
}
|
|
96
|
-
if (Object.hasOwn(value, "thinkingLevel")) {
|
|
97
|
-
const thinkingLevel = Reflect.get(value, "thinkingLevel");
|
|
98
|
-
if (!isBtwThinkingLevel(thinkingLevel)) return undefined;
|
|
99
|
-
settings.thinkingLevel = thinkingLevel;
|
|
100
|
-
}
|
|
101
|
-
return settings;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export function parseBtwModelReference(
|
|
105
|
-
reference: string,
|
|
106
|
-
): { provider: string; modelId: string } | undefined {
|
|
107
|
-
if (/\s/.test(reference)) return undefined;
|
|
108
|
-
const separator = reference.indexOf("/");
|
|
109
|
-
if (separator <= 0 || separator === reference.length - 1) return undefined;
|
|
110
|
-
return { provider: reference.slice(0, separator), modelId: reference.slice(separator + 1) };
|
|
111
|
-
}
|
|
112
|
-
|
|
113
101
|
export async function resolveBtwModel({
|
|
114
102
|
settings,
|
|
115
103
|
currentModel,
|
|
116
104
|
modelRegistry,
|
|
117
105
|
warn,
|
|
118
106
|
}: ResolveBtwModelOptions): Promise<ResolvedBtwModel | undefined> {
|
|
107
|
+
const reportWarning = (message: string) => warn?.(sanitizeSingleLine(message));
|
|
119
108
|
if (settings.model) {
|
|
120
109
|
const fallback = currentModel
|
|
121
110
|
? `${currentModel.provider}/${currentModel.id}`
|
|
122
111
|
: "the current model";
|
|
123
112
|
const reference = parseBtwModelReference(settings.model);
|
|
124
113
|
if (!reference) {
|
|
125
|
-
|
|
126
|
-
return resolveBtwModel({ settings: {}, currentModel, modelRegistry, warn });
|
|
114
|
+
reportWarning(`pi-btw model ${settings.model} is invalid; falling back to ${fallback}.`);
|
|
115
|
+
return resolveBtwModel({ settings: {}, currentModel, modelRegistry, warn: reportWarning });
|
|
127
116
|
}
|
|
128
117
|
const configuredModel = modelRegistry.find(reference.provider, reference.modelId);
|
|
129
118
|
if (!configuredModel) {
|
|
130
|
-
|
|
119
|
+
reportWarning(`pi-btw model ${settings.model} was not found; falling back to ${fallback}.`);
|
|
131
120
|
} else {
|
|
132
121
|
const sameAsCurrent =
|
|
133
122
|
configuredModel === currentModel ||
|
|
@@ -140,9 +129,11 @@ export async function resolveBtwModel({
|
|
|
140
129
|
const auth = await modelRegistry.getApiKeyAndHeaders(configuredModel);
|
|
141
130
|
if (auth.ok && hasRequestAuth(auth)) return { model: configuredModel, auth };
|
|
142
131
|
const reason = auth.ok ? "has no request credentials" : auth.error;
|
|
143
|
-
|
|
132
|
+
reportWarning(
|
|
133
|
+
`pi-btw model ${settings.model} is unavailable (${reason}); ${fallbackAction}.`,
|
|
134
|
+
);
|
|
144
135
|
} catch (error: unknown) {
|
|
145
|
-
|
|
136
|
+
reportWarning(
|
|
146
137
|
`pi-btw model ${settings.model} credentials failed (${formatError(error)}); ${fallbackAction}.`,
|
|
147
138
|
);
|
|
148
139
|
}
|
|
@@ -168,26 +159,6 @@ function hasRequestAuth(auth: SideQuestionAuth): boolean {
|
|
|
168
159
|
);
|
|
169
160
|
}
|
|
170
161
|
|
|
171
|
-
export async function readBtwSettings(
|
|
172
|
-
settingsPath = join(getAgentDir(), BTW_SETTINGS_FILE),
|
|
173
|
-
): Promise<BtwSettingsLoadResult> {
|
|
174
|
-
let contents: string;
|
|
175
|
-
try {
|
|
176
|
-
contents = await readFile(settingsPath, "utf8");
|
|
177
|
-
} catch (error: unknown) {
|
|
178
|
-
if (isNodeError(error) && error.code === "ENOENT") return { kind: "missing" };
|
|
179
|
-
return { kind: "invalid", reason: `${settingsPath}: ${formatError(error)}` };
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
try {
|
|
183
|
-
const settings = normalizeBtwSettings(JSON.parse(contents) as unknown);
|
|
184
|
-
if (settings) return { kind: "loaded", settings };
|
|
185
|
-
return { kind: "invalid", reason: `${settingsPath}: invalid settings shape` };
|
|
186
|
-
} catch (error: unknown) {
|
|
187
|
-
return { kind: "invalid", reason: `${settingsPath}: ${formatError(error)}` };
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
162
|
export async function loadBtwThinkingLevel(
|
|
192
163
|
currentThinkingLevel: BtwThinkingLevel,
|
|
193
164
|
options: LoadBtwThinkingLevelOptions = {},
|
|
@@ -199,24 +170,44 @@ export async function loadBtwThinkingLevel(
|
|
|
199
170
|
}
|
|
200
171
|
|
|
201
172
|
options.warn?.(
|
|
202
|
-
|
|
173
|
+
sanitizeSingleLine(
|
|
174
|
+
`pi-btw settings ignored: ${settings.reason}; expected optional model "provider/model-id", thinkingLevel "${BTW_THINKING_LEVELS.join('" | "')}", and boolean rememberThinkingLevelChanges. Using current Pi thinking level.`,
|
|
175
|
+
),
|
|
203
176
|
);
|
|
204
177
|
return currentThinkingLevel;
|
|
205
178
|
}
|
|
206
179
|
|
|
207
|
-
function
|
|
208
|
-
return
|
|
180
|
+
function formatError(error: unknown): string {
|
|
181
|
+
return error instanceof Error ? error.message : String(error);
|
|
209
182
|
}
|
|
210
183
|
|
|
211
|
-
function
|
|
212
|
-
|
|
184
|
+
function notifySafely(
|
|
185
|
+
ctx: ExtensionCommandContext,
|
|
186
|
+
message: string,
|
|
187
|
+
level: Parameters<ExtensionCommandContext["ui"]["notify"]>[1],
|
|
188
|
+
): void {
|
|
189
|
+
try {
|
|
190
|
+
ctx.ui.notify(sanitizeSingleLine(message), level);
|
|
191
|
+
} catch {
|
|
192
|
+
// Async command continuations may finish after their ExtensionContext is replaced.
|
|
193
|
+
}
|
|
213
194
|
}
|
|
214
195
|
|
|
215
|
-
|
|
216
|
-
|
|
196
|
+
export interface BtwExtensionDependencies {
|
|
197
|
+
showCommandMenu?: (
|
|
198
|
+
pi: ExtensionAPI,
|
|
199
|
+
ctx: ExtensionCommandContext,
|
|
200
|
+
) => Promise<BtwCommandMenuResult>;
|
|
201
|
+
loadSettings?: typeof loadSettingsForCommand;
|
|
202
|
+
resolveModel?: typeof resolveBtwModelWithLoader;
|
|
203
|
+
runThread?: typeof runBtwThread;
|
|
217
204
|
}
|
|
218
205
|
|
|
219
|
-
export default function btw(pi: ExtensionAPI) {
|
|
206
|
+
export default function btw(pi: ExtensionAPI, dependencies: BtwExtensionDependencies = {}) {
|
|
207
|
+
const showCommandMenu = dependencies.showCommandMenu ?? showCommandMenuForBtw;
|
|
208
|
+
const loadSettings = dependencies.loadSettings ?? loadSettingsForCommand;
|
|
209
|
+
const resolveModel = dependencies.resolveModel ?? resolveBtwModelWithLoader;
|
|
210
|
+
const runThread = dependencies.runThread ?? runBtwThread;
|
|
220
211
|
pi.registerCommand("btw", {
|
|
221
212
|
description: "Ask a quick side question without adding it to the main conversation",
|
|
222
213
|
handler: async (args, ctx) => {
|
|
@@ -225,33 +216,57 @@ export default function btw(pi: ExtensionAPI) {
|
|
|
225
216
|
ctx.ui.notify("/btw requires interactive TUI mode", "error");
|
|
226
217
|
return;
|
|
227
218
|
}
|
|
219
|
+
if (!question && (await showCommandMenu(pi, ctx)) !== "start") return;
|
|
228
220
|
|
|
229
|
-
const settings = await
|
|
230
|
-
const resolution = await
|
|
221
|
+
const settings = await loadSettings(ctx);
|
|
222
|
+
const resolution = await resolveModel(settings, ctx);
|
|
231
223
|
if (resolution.kind === "cancelled") {
|
|
232
|
-
ctx
|
|
224
|
+
notifySafely(ctx, "Cancelled", "info");
|
|
233
225
|
return;
|
|
234
226
|
}
|
|
235
227
|
if (resolution.kind === "unavailable") {
|
|
236
|
-
ctx
|
|
228
|
+
notifySafely(ctx, "No available model for /btw", "error");
|
|
237
229
|
return;
|
|
238
230
|
}
|
|
239
231
|
|
|
240
|
-
await
|
|
232
|
+
await runThread({
|
|
241
233
|
initialQuestion: question || undefined,
|
|
242
234
|
selected: resolution.selected,
|
|
243
235
|
thinkingLevel: settings.thinkingLevel ?? pi.getThinkingLevel(),
|
|
236
|
+
rememberThinkingLevelChanges: effectiveRememberThinkingLevelChanges(settings),
|
|
244
237
|
ctx,
|
|
245
238
|
});
|
|
246
239
|
},
|
|
247
240
|
});
|
|
248
241
|
}
|
|
249
242
|
|
|
243
|
+
async function showCommandMenuForBtw(
|
|
244
|
+
pi: ExtensionAPI,
|
|
245
|
+
ctx: ExtensionCommandContext,
|
|
246
|
+
): Promise<BtwCommandMenuResult> {
|
|
247
|
+
const currentModel = ctx.model;
|
|
248
|
+
const availableModels = ctx.modelRegistry.getAll();
|
|
249
|
+
const currentThinkingLevel = pi.getThinkingLevel();
|
|
250
|
+
const loaded = await readBtwSettings();
|
|
251
|
+
const settings = loaded.kind === "loaded" ? loaded.settings : {};
|
|
252
|
+
const configured = settings.model ? parseBtwModelReference(settings.model) : undefined;
|
|
253
|
+
const configuredModel = configured
|
|
254
|
+
? availableModels.find(
|
|
255
|
+
(model) => model.provider === configured.provider && model.id === configured.modelId,
|
|
256
|
+
)
|
|
257
|
+
: undefined;
|
|
258
|
+
const model = configuredModel ?? currentModel;
|
|
259
|
+
return showBtwCommandMenu(ctx, {
|
|
260
|
+
currentThinkingLevel,
|
|
261
|
+
availableThinkingLevels: model ? getSupportedThinkingLevels(model) : BTW_THINKING_LEVELS,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
250
265
|
async function loadSettingsForCommand(ctx: ExtensionCommandContext): Promise<BtwSettings> {
|
|
251
266
|
const settingsResult = await readBtwSettings();
|
|
252
267
|
if (settingsResult.kind === "loaded") return settingsResult.settings;
|
|
253
268
|
if (settingsResult.kind === "invalid") {
|
|
254
|
-
ctx
|
|
269
|
+
notifySafely(ctx, `pi-btw settings ignored: ${settingsResult.reason}`, "warning");
|
|
255
270
|
}
|
|
256
271
|
return {};
|
|
257
272
|
}
|
|
@@ -279,7 +294,7 @@ async function resolveBtwModelWithLoader(
|
|
|
279
294
|
currentModel: ctx.model,
|
|
280
295
|
modelRegistry: ctx.modelRegistry,
|
|
281
296
|
warn: (message) => {
|
|
282
|
-
if (!settled) ctx
|
|
297
|
+
if (!settled) notifySafely(ctx, message, "warning");
|
|
283
298
|
},
|
|
284
299
|
})
|
|
285
300
|
.then((selected) => {
|
|
@@ -302,10 +317,13 @@ interface RunBtwThreadDependencies {
|
|
|
302
317
|
interact?: typeof showThreadComposer;
|
|
303
318
|
chooseBringToMain?: typeof chooseBringToMain;
|
|
304
319
|
deliverBringToMain?: typeof loadBringToMainDraft;
|
|
320
|
+
persistThinkingLevel?: (level: BtwThinkingLevel) => Promise<unknown>;
|
|
305
321
|
}
|
|
306
322
|
|
|
307
323
|
export type BtwThreadResult = { kind: "closed" };
|
|
308
324
|
|
|
325
|
+
type BtwThreadThinkingControl = Omit<BtwThinkingControl, "keybindings">;
|
|
326
|
+
|
|
309
327
|
type BtwBringToMainChoice =
|
|
310
328
|
| BtwThreadResult
|
|
311
329
|
| {
|
|
@@ -322,6 +340,8 @@ interface RunBtwThreadOptions {
|
|
|
322
340
|
initialQuestion?: string;
|
|
323
341
|
selected: ResolvedBtwModel;
|
|
324
342
|
thinkingLevel: BtwThinkingLevel;
|
|
343
|
+
rememberThinkingLevelChanges?: boolean;
|
|
344
|
+
settingsPath?: string;
|
|
325
345
|
ctx: ExtensionCommandContext;
|
|
326
346
|
dependencies?: RunBtwThreadDependencies;
|
|
327
347
|
}
|
|
@@ -330,6 +350,8 @@ export async function runBtwThread({
|
|
|
330
350
|
initialQuestion,
|
|
331
351
|
selected,
|
|
332
352
|
thinkingLevel,
|
|
353
|
+
rememberThinkingLevelChanges = false,
|
|
354
|
+
settingsPath,
|
|
333
355
|
ctx,
|
|
334
356
|
dependencies = {},
|
|
335
357
|
}: RunBtwThreadOptions): Promise<BtwThreadResult> {
|
|
@@ -337,44 +359,82 @@ export async function runBtwThread({
|
|
|
337
359
|
const interact = dependencies.interact ?? showThreadComposer;
|
|
338
360
|
const chooseBringToMainAction = dependencies.chooseBringToMain ?? chooseBringToMain;
|
|
339
361
|
const deliverBringToMainDraft = dependencies.deliverBringToMain ?? loadBringToMainDraft;
|
|
362
|
+
const persistThinkingLevel =
|
|
363
|
+
dependencies.persistThinkingLevel ??
|
|
364
|
+
((level: BtwThinkingLevel) => updateBtwSettings({ thinkingLevel: level }, { settingsPath }));
|
|
340
365
|
const thread = createSideThread(buildConversationContext(ctx.sessionManager.getBranch()));
|
|
366
|
+
const thinkingLevels = getSupportedThinkingLevels(selected.model);
|
|
367
|
+
const pendingWrites = new Set<Promise<void>>();
|
|
368
|
+
let activeThinkingLevel = clampThinkingLevel(selected.model, thinkingLevel);
|
|
341
369
|
let pendingQuestion = initialQuestion;
|
|
342
370
|
let composerDraft: string | undefined;
|
|
343
371
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
372
|
+
try {
|
|
373
|
+
while (true) {
|
|
374
|
+
if (!pendingQuestion) {
|
|
375
|
+
const thinking: BtwThreadThinkingControl = {
|
|
376
|
+
level: activeThinkingLevel,
|
|
377
|
+
levels: thinkingLevels,
|
|
378
|
+
onChange: (level) => {
|
|
379
|
+
if (!thinkingLevels.includes(level)) return;
|
|
380
|
+
activeThinkingLevel = level;
|
|
381
|
+
if (!rememberThinkingLevelChanges) return;
|
|
382
|
+
let write!: Promise<void>;
|
|
383
|
+
write = Promise.resolve()
|
|
384
|
+
.then(() => persistThinkingLevel(level))
|
|
385
|
+
.then(() => undefined)
|
|
386
|
+
.catch((error: unknown) => {
|
|
387
|
+
notifySafely(
|
|
388
|
+
ctx,
|
|
389
|
+
`Thinking level changed to ${level}, but could not be remembered in pi-btw.json: ${formatError(error)}`,
|
|
390
|
+
"warning",
|
|
391
|
+
);
|
|
392
|
+
})
|
|
393
|
+
.finally(() => pendingWrites.delete(write));
|
|
394
|
+
pendingWrites.add(write);
|
|
395
|
+
},
|
|
396
|
+
};
|
|
397
|
+
const action = await interact(
|
|
398
|
+
thread,
|
|
399
|
+
thread.turns.length > 0,
|
|
400
|
+
ctx,
|
|
401
|
+
composerDraft,
|
|
402
|
+
thinking,
|
|
403
|
+
);
|
|
404
|
+
if (action.kind === "close") return { kind: "closed" };
|
|
405
|
+
if (action.kind === "bringToMain") {
|
|
406
|
+
const choice = await chooseBringToMainAction(thread, ctx);
|
|
407
|
+
if (choice.kind === "closed") return choice;
|
|
408
|
+
if (choice.kind === "back") {
|
|
409
|
+
composerDraft = action.questionDraft;
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
const delivery = await deliverBringToMainDraft(choice.draft, ctx, choice.summary);
|
|
413
|
+
if (delivery === "loaded" || delivery === "closed") return { kind: "closed" };
|
|
352
414
|
composerDraft = action.questionDraft;
|
|
353
415
|
continue;
|
|
354
416
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
composerDraft = action.questionDraft;
|
|
358
|
-
continue;
|
|
417
|
+
composerDraft = undefined;
|
|
418
|
+
pendingQuestion = action.question;
|
|
359
419
|
}
|
|
360
|
-
composerDraft = undefined;
|
|
361
|
-
pendingQuestion = action.question;
|
|
362
|
-
}
|
|
363
420
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
421
|
+
const result = await ask(thread, pendingQuestion, selected, activeThinkingLevel, ctx);
|
|
422
|
+
if (result.kind === "aborted") {
|
|
423
|
+
notifySafely(ctx, "Cancelled", "info");
|
|
424
|
+
return { kind: "closed" };
|
|
425
|
+
}
|
|
426
|
+
if (result.kind === "error") {
|
|
427
|
+
thread.turns.push({
|
|
428
|
+
kind: "error",
|
|
429
|
+
question: pendingQuestion,
|
|
430
|
+
answer: result.message,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
376
433
|
|
|
377
|
-
|
|
434
|
+
pendingQuestion = undefined;
|
|
435
|
+
}
|
|
436
|
+
} finally {
|
|
437
|
+
await Promise.allSettled([...pendingWrites]);
|
|
378
438
|
}
|
|
379
439
|
}
|
|
380
440
|
|
|
@@ -393,13 +453,21 @@ async function showBtwCustomPreservingEditor<T>(
|
|
|
393
453
|
let completed = false;
|
|
394
454
|
const result = await ctx.ui.custom<T>((tui, theme, keybindings, done) =>
|
|
395
455
|
factory(tui, theme, keybindings, (value) => {
|
|
396
|
-
|
|
456
|
+
try {
|
|
457
|
+
liveEditorText = ctx.ui.getEditorText();
|
|
458
|
+
} catch {
|
|
459
|
+
// Keep completion finite if session replacement invalidates the editor context.
|
|
460
|
+
}
|
|
397
461
|
completed = true;
|
|
398
462
|
done(value);
|
|
399
463
|
}),
|
|
400
464
|
);
|
|
401
|
-
if (completed
|
|
402
|
-
|
|
465
|
+
if (completed) {
|
|
466
|
+
try {
|
|
467
|
+
if (ctx.ui.getEditorText() !== liveEditorText) ctx.ui.setEditorText(liveEditorText);
|
|
468
|
+
} catch {
|
|
469
|
+
// A replaced context owns a different editor and must not receive stale restoration.
|
|
470
|
+
}
|
|
403
471
|
}
|
|
404
472
|
return result;
|
|
405
473
|
}
|
|
@@ -602,35 +670,6 @@ async function showBtwMenu(
|
|
|
602
670
|
: terminalBtwMenuAction(result);
|
|
603
671
|
}
|
|
604
672
|
|
|
605
|
-
async function runBtwMenuPreservingEditor(
|
|
606
|
-
ctx: ExtensionCommandContext,
|
|
607
|
-
run: (menuContext: MenuContext) => Promise<RunMenuResult>,
|
|
608
|
-
): Promise<RunMenuResult> {
|
|
609
|
-
let liveEditorText = ctx.ui.getEditorText();
|
|
610
|
-
let completed = false;
|
|
611
|
-
const ui = new Proxy(ctx.ui, {
|
|
612
|
-
get(target, property) {
|
|
613
|
-
if (property === "custom") {
|
|
614
|
-
return <Value>(factory: BtwCustomFactory<Value>) =>
|
|
615
|
-
target.custom<Value>((tui, theme, keybindings, done) =>
|
|
616
|
-
factory(tui, theme, keybindings, (value) => {
|
|
617
|
-
liveEditorText = target.getEditorText();
|
|
618
|
-
completed = true;
|
|
619
|
-
done(value);
|
|
620
|
-
}),
|
|
621
|
-
);
|
|
622
|
-
}
|
|
623
|
-
const value = Reflect.get(target, property, target) as unknown;
|
|
624
|
-
return typeof value === "function" ? value.bind(target) : value;
|
|
625
|
-
},
|
|
626
|
-
});
|
|
627
|
-
const result = await run({ mode: ctx.mode, hasUI: ctx.hasUI, ui });
|
|
628
|
-
if (result.kind !== "stale" && completed && ctx.ui.getEditorText() !== liveEditorText) {
|
|
629
|
-
ctx.ui.setEditorText(liveEditorText);
|
|
630
|
-
}
|
|
631
|
-
return result;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
673
|
function terminalBtwMenuAction(result: RunMenuResult): { kind: "back" } | { kind: "close" } {
|
|
635
674
|
if (result.kind === "closed") return { kind: result.reason };
|
|
636
675
|
if (result.kind === "error") throw result.error;
|
|
@@ -715,11 +754,18 @@ async function askThreadQuestion(
|
|
|
715
754
|
return ctx.ui.custom<Awaited<ReturnType<typeof completeSideThreadTurn>>>(
|
|
716
755
|
(tui, theme, _keybindings, done) => {
|
|
717
756
|
let settled = false;
|
|
718
|
-
const view = new BtwAnsweringView(
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
757
|
+
const view = new BtwAnsweringView(
|
|
758
|
+
tui,
|
|
759
|
+
theme,
|
|
760
|
+
thread.turns,
|
|
761
|
+
question,
|
|
762
|
+
() => {
|
|
763
|
+
if (settled) return;
|
|
764
|
+
settled = true;
|
|
765
|
+
done({ kind: "aborted" });
|
|
766
|
+
},
|
|
767
|
+
thinkingLevel,
|
|
768
|
+
);
|
|
723
769
|
completeSideThreadTurn({
|
|
724
770
|
thread,
|
|
725
771
|
question,
|
|
@@ -742,28 +788,19 @@ async function showThreadComposer(
|
|
|
742
788
|
thread: SideThread,
|
|
743
789
|
startAtBottom: boolean,
|
|
744
790
|
ctx: ExtensionCommandContext,
|
|
745
|
-
initialQuestion
|
|
791
|
+
initialQuestion: string | undefined,
|
|
792
|
+
thinking: BtwThreadThinkingControl,
|
|
746
793
|
): Promise<TranscriptPagerAction> {
|
|
747
794
|
return ctx.ui.custom<TranscriptPagerAction>(
|
|
748
|
-
(tui, theme,
|
|
795
|
+
(tui, theme, keybindings, done) =>
|
|
749
796
|
new BtwTranscriptPager(tui, theme, thread.turns, done, {
|
|
750
797
|
startAtBottom,
|
|
751
798
|
initialQuestion,
|
|
799
|
+
thinking: { ...thinking, keybindings },
|
|
752
800
|
}),
|
|
753
801
|
);
|
|
754
802
|
}
|
|
755
803
|
|
|
756
|
-
export function sanitizeSingleLine(text: string) {
|
|
757
|
-
return [...text.replace(/[\r\n\t]/g, " ")]
|
|
758
|
-
.filter((character) => {
|
|
759
|
-
const code = character.charCodeAt(0);
|
|
760
|
-
return code > 31 && (code < 127 || code > 159);
|
|
761
|
-
})
|
|
762
|
-
.join("")
|
|
763
|
-
.replace(/ +/g, " ")
|
|
764
|
-
.trim();
|
|
765
|
-
}
|
|
766
|
-
|
|
767
804
|
type MessageContentBlock = {
|
|
768
805
|
type?: string;
|
|
769
806
|
text?: string;
|