@narumitw/pi-btw 0.42.0 → 0.43.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 +9 -1
- package/src/bring-to-main.ts +0 -238
- package/src/btw.ts +273 -142
- 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,22 +1,21 @@
|
|
|
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";
|
|
12
14
|
import type { Component, TUI } from "@earendil-works/pi-tui";
|
|
15
|
+
import { defineMenu, type MenuContext, type RunMenuResult, runMenu } from "@narumitw/pi-tui-kit";
|
|
13
16
|
import {
|
|
14
|
-
BtwBringToMainPreview,
|
|
15
|
-
type BtwBringToMainPreviewAction,
|
|
16
17
|
type BtwBringToMainSegment,
|
|
17
18
|
type BtwBringToMainSummary,
|
|
18
|
-
BtwMenuSelector,
|
|
19
|
-
type BtwMenuSelectorAction,
|
|
20
19
|
BtwTextRangeSelector,
|
|
21
20
|
type BtwTextRangeSelectorState,
|
|
22
21
|
buildQuickBringToMainSegments,
|
|
@@ -25,6 +24,18 @@ import {
|
|
|
25
24
|
getAnsweredTurns,
|
|
26
25
|
summarizeBringToMain,
|
|
27
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";
|
|
28
39
|
import {
|
|
29
40
|
BTW_THINKING_LEVELS,
|
|
30
41
|
type BtwThinkingLevel,
|
|
@@ -33,12 +44,22 @@ import {
|
|
|
33
44
|
type SideQuestionAuth,
|
|
34
45
|
type SideThread,
|
|
35
46
|
} from "./side-thread.js";
|
|
47
|
+
import { sanitizeSingleLine } from "./text.js";
|
|
36
48
|
import {
|
|
37
49
|
BtwAnsweringView,
|
|
50
|
+
type BtwThinkingControl,
|
|
38
51
|
BtwTranscriptPager,
|
|
39
52
|
type TranscriptPagerAction,
|
|
40
53
|
} from "./transcript-pager.js";
|
|
41
54
|
|
|
55
|
+
export {
|
|
56
|
+
BTW_SETTINGS_FILE,
|
|
57
|
+
type BtwSettings,
|
|
58
|
+
type BtwSettingsLoadResult,
|
|
59
|
+
normalizeBtwSettings,
|
|
60
|
+
parseBtwModelReference,
|
|
61
|
+
readBtwSettings,
|
|
62
|
+
} from "./settings.js";
|
|
42
63
|
export {
|
|
43
64
|
BTW_THINKING_LEVELS,
|
|
44
65
|
type BtwThinkingLevel,
|
|
@@ -46,19 +67,9 @@ export {
|
|
|
46
67
|
completeSideQuestion,
|
|
47
68
|
loadCompleteSimple,
|
|
48
69
|
} from "./side-thread.js";
|
|
70
|
+
export { sanitizeSingleLine } from "./text.js";
|
|
49
71
|
|
|
50
72
|
const MAX_CONTEXT_CHARS = 40_000;
|
|
51
|
-
export const BTW_SETTINGS_FILE = "pi-btw.json";
|
|
52
|
-
|
|
53
|
-
export interface BtwSettings {
|
|
54
|
-
model?: string;
|
|
55
|
-
thinkingLevel?: BtwThinkingLevel;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export type BtwSettingsLoadResult =
|
|
59
|
-
| { kind: "missing" }
|
|
60
|
-
| { kind: "invalid"; reason: string }
|
|
61
|
-
| { kind: "loaded"; settings: BtwSettings };
|
|
62
73
|
|
|
63
74
|
interface LoadBtwThinkingLevelOptions {
|
|
64
75
|
settingsPath?: string;
|
|
@@ -87,50 +98,25 @@ export interface ResolvedBtwModel {
|
|
|
87
98
|
auth: SideQuestionAuth;
|
|
88
99
|
}
|
|
89
100
|
|
|
90
|
-
export function normalizeBtwSettings(value: unknown): BtwSettings | undefined {
|
|
91
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
|
|
92
|
-
|
|
93
|
-
const settings: BtwSettings = {};
|
|
94
|
-
if (Object.hasOwn(value, "model")) {
|
|
95
|
-
const model = Reflect.get(value, "model");
|
|
96
|
-
if (typeof model !== "string" || !parseBtwModelReference(model)) return undefined;
|
|
97
|
-
settings.model = model;
|
|
98
|
-
}
|
|
99
|
-
if (Object.hasOwn(value, "thinkingLevel")) {
|
|
100
|
-
const thinkingLevel = Reflect.get(value, "thinkingLevel");
|
|
101
|
-
if (!isBtwThinkingLevel(thinkingLevel)) return undefined;
|
|
102
|
-
settings.thinkingLevel = thinkingLevel;
|
|
103
|
-
}
|
|
104
|
-
return settings;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export function parseBtwModelReference(
|
|
108
|
-
reference: string,
|
|
109
|
-
): { provider: string; modelId: string } | undefined {
|
|
110
|
-
if (/\s/.test(reference)) return undefined;
|
|
111
|
-
const separator = reference.indexOf("/");
|
|
112
|
-
if (separator <= 0 || separator === reference.length - 1) return undefined;
|
|
113
|
-
return { provider: reference.slice(0, separator), modelId: reference.slice(separator + 1) };
|
|
114
|
-
}
|
|
115
|
-
|
|
116
101
|
export async function resolveBtwModel({
|
|
117
102
|
settings,
|
|
118
103
|
currentModel,
|
|
119
104
|
modelRegistry,
|
|
120
105
|
warn,
|
|
121
106
|
}: ResolveBtwModelOptions): Promise<ResolvedBtwModel | undefined> {
|
|
107
|
+
const reportWarning = (message: string) => warn?.(sanitizeSingleLine(message));
|
|
122
108
|
if (settings.model) {
|
|
123
109
|
const fallback = currentModel
|
|
124
110
|
? `${currentModel.provider}/${currentModel.id}`
|
|
125
111
|
: "the current model";
|
|
126
112
|
const reference = parseBtwModelReference(settings.model);
|
|
127
113
|
if (!reference) {
|
|
128
|
-
|
|
129
|
-
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 });
|
|
130
116
|
}
|
|
131
117
|
const configuredModel = modelRegistry.find(reference.provider, reference.modelId);
|
|
132
118
|
if (!configuredModel) {
|
|
133
|
-
|
|
119
|
+
reportWarning(`pi-btw model ${settings.model} was not found; falling back to ${fallback}.`);
|
|
134
120
|
} else {
|
|
135
121
|
const sameAsCurrent =
|
|
136
122
|
configuredModel === currentModel ||
|
|
@@ -143,9 +129,11 @@ export async function resolveBtwModel({
|
|
|
143
129
|
const auth = await modelRegistry.getApiKeyAndHeaders(configuredModel);
|
|
144
130
|
if (auth.ok && hasRequestAuth(auth)) return { model: configuredModel, auth };
|
|
145
131
|
const reason = auth.ok ? "has no request credentials" : auth.error;
|
|
146
|
-
|
|
132
|
+
reportWarning(
|
|
133
|
+
`pi-btw model ${settings.model} is unavailable (${reason}); ${fallbackAction}.`,
|
|
134
|
+
);
|
|
147
135
|
} catch (error: unknown) {
|
|
148
|
-
|
|
136
|
+
reportWarning(
|
|
149
137
|
`pi-btw model ${settings.model} credentials failed (${formatError(error)}); ${fallbackAction}.`,
|
|
150
138
|
);
|
|
151
139
|
}
|
|
@@ -171,26 +159,6 @@ function hasRequestAuth(auth: SideQuestionAuth): boolean {
|
|
|
171
159
|
);
|
|
172
160
|
}
|
|
173
161
|
|
|
174
|
-
export async function readBtwSettings(
|
|
175
|
-
settingsPath = join(getAgentDir(), BTW_SETTINGS_FILE),
|
|
176
|
-
): Promise<BtwSettingsLoadResult> {
|
|
177
|
-
let contents: string;
|
|
178
|
-
try {
|
|
179
|
-
contents = await readFile(settingsPath, "utf8");
|
|
180
|
-
} catch (error: unknown) {
|
|
181
|
-
if (isNodeError(error) && error.code === "ENOENT") return { kind: "missing" };
|
|
182
|
-
return { kind: "invalid", reason: `${settingsPath}: ${formatError(error)}` };
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
try {
|
|
186
|
-
const settings = normalizeBtwSettings(JSON.parse(contents) as unknown);
|
|
187
|
-
if (settings) return { kind: "loaded", settings };
|
|
188
|
-
return { kind: "invalid", reason: `${settingsPath}: invalid settings shape` };
|
|
189
|
-
} catch (error: unknown) {
|
|
190
|
-
return { kind: "invalid", reason: `${settingsPath}: ${formatError(error)}` };
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
162
|
export async function loadBtwThinkingLevel(
|
|
195
163
|
currentThinkingLevel: BtwThinkingLevel,
|
|
196
164
|
options: LoadBtwThinkingLevelOptions = {},
|
|
@@ -202,24 +170,44 @@ export async function loadBtwThinkingLevel(
|
|
|
202
170
|
}
|
|
203
171
|
|
|
204
172
|
options.warn?.(
|
|
205
|
-
|
|
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
|
+
),
|
|
206
176
|
);
|
|
207
177
|
return currentThinkingLevel;
|
|
208
178
|
}
|
|
209
179
|
|
|
210
|
-
function
|
|
211
|
-
return
|
|
180
|
+
function formatError(error: unknown): string {
|
|
181
|
+
return error instanceof Error ? error.message : String(error);
|
|
212
182
|
}
|
|
213
183
|
|
|
214
|
-
function
|
|
215
|
-
|
|
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
|
+
}
|
|
216
194
|
}
|
|
217
195
|
|
|
218
|
-
|
|
219
|
-
|
|
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;
|
|
220
204
|
}
|
|
221
205
|
|
|
222
|
-
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;
|
|
223
211
|
pi.registerCommand("btw", {
|
|
224
212
|
description: "Ask a quick side question without adding it to the main conversation",
|
|
225
213
|
handler: async (args, ctx) => {
|
|
@@ -228,33 +216,57 @@ export default function btw(pi: ExtensionAPI) {
|
|
|
228
216
|
ctx.ui.notify("/btw requires interactive TUI mode", "error");
|
|
229
217
|
return;
|
|
230
218
|
}
|
|
219
|
+
if (!question && (await showCommandMenu(pi, ctx)) !== "start") return;
|
|
231
220
|
|
|
232
|
-
const settings = await
|
|
233
|
-
const resolution = await
|
|
221
|
+
const settings = await loadSettings(ctx);
|
|
222
|
+
const resolution = await resolveModel(settings, ctx);
|
|
234
223
|
if (resolution.kind === "cancelled") {
|
|
235
|
-
ctx
|
|
224
|
+
notifySafely(ctx, "Cancelled", "info");
|
|
236
225
|
return;
|
|
237
226
|
}
|
|
238
227
|
if (resolution.kind === "unavailable") {
|
|
239
|
-
ctx
|
|
228
|
+
notifySafely(ctx, "No available model for /btw", "error");
|
|
240
229
|
return;
|
|
241
230
|
}
|
|
242
231
|
|
|
243
|
-
await
|
|
232
|
+
await runThread({
|
|
244
233
|
initialQuestion: question || undefined,
|
|
245
234
|
selected: resolution.selected,
|
|
246
235
|
thinkingLevel: settings.thinkingLevel ?? pi.getThinkingLevel(),
|
|
236
|
+
rememberThinkingLevelChanges: effectiveRememberThinkingLevelChanges(settings),
|
|
247
237
|
ctx,
|
|
248
238
|
});
|
|
249
239
|
},
|
|
250
240
|
});
|
|
251
241
|
}
|
|
252
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
|
+
|
|
253
265
|
async function loadSettingsForCommand(ctx: ExtensionCommandContext): Promise<BtwSettings> {
|
|
254
266
|
const settingsResult = await readBtwSettings();
|
|
255
267
|
if (settingsResult.kind === "loaded") return settingsResult.settings;
|
|
256
268
|
if (settingsResult.kind === "invalid") {
|
|
257
|
-
ctx
|
|
269
|
+
notifySafely(ctx, `pi-btw settings ignored: ${settingsResult.reason}`, "warning");
|
|
258
270
|
}
|
|
259
271
|
return {};
|
|
260
272
|
}
|
|
@@ -282,7 +294,7 @@ async function resolveBtwModelWithLoader(
|
|
|
282
294
|
currentModel: ctx.model,
|
|
283
295
|
modelRegistry: ctx.modelRegistry,
|
|
284
296
|
warn: (message) => {
|
|
285
|
-
if (!settled) ctx
|
|
297
|
+
if (!settled) notifySafely(ctx, message, "warning");
|
|
286
298
|
},
|
|
287
299
|
})
|
|
288
300
|
.then((selected) => {
|
|
@@ -305,10 +317,13 @@ interface RunBtwThreadDependencies {
|
|
|
305
317
|
interact?: typeof showThreadComposer;
|
|
306
318
|
chooseBringToMain?: typeof chooseBringToMain;
|
|
307
319
|
deliverBringToMain?: typeof loadBringToMainDraft;
|
|
320
|
+
persistThinkingLevel?: (level: BtwThinkingLevel) => Promise<unknown>;
|
|
308
321
|
}
|
|
309
322
|
|
|
310
323
|
export type BtwThreadResult = { kind: "closed" };
|
|
311
324
|
|
|
325
|
+
type BtwThreadThinkingControl = Omit<BtwThinkingControl, "keybindings">;
|
|
326
|
+
|
|
312
327
|
type BtwBringToMainChoice =
|
|
313
328
|
| BtwThreadResult
|
|
314
329
|
| {
|
|
@@ -325,6 +340,8 @@ interface RunBtwThreadOptions {
|
|
|
325
340
|
initialQuestion?: string;
|
|
326
341
|
selected: ResolvedBtwModel;
|
|
327
342
|
thinkingLevel: BtwThinkingLevel;
|
|
343
|
+
rememberThinkingLevelChanges?: boolean;
|
|
344
|
+
settingsPath?: string;
|
|
328
345
|
ctx: ExtensionCommandContext;
|
|
329
346
|
dependencies?: RunBtwThreadDependencies;
|
|
330
347
|
}
|
|
@@ -333,6 +350,8 @@ export async function runBtwThread({
|
|
|
333
350
|
initialQuestion,
|
|
334
351
|
selected,
|
|
335
352
|
thinkingLevel,
|
|
353
|
+
rememberThinkingLevelChanges = false,
|
|
354
|
+
settingsPath,
|
|
336
355
|
ctx,
|
|
337
356
|
dependencies = {},
|
|
338
357
|
}: RunBtwThreadOptions): Promise<BtwThreadResult> {
|
|
@@ -340,44 +359,82 @@ export async function runBtwThread({
|
|
|
340
359
|
const interact = dependencies.interact ?? showThreadComposer;
|
|
341
360
|
const chooseBringToMainAction = dependencies.chooseBringToMain ?? chooseBringToMain;
|
|
342
361
|
const deliverBringToMainDraft = dependencies.deliverBringToMain ?? loadBringToMainDraft;
|
|
362
|
+
const persistThinkingLevel =
|
|
363
|
+
dependencies.persistThinkingLevel ??
|
|
364
|
+
((level: BtwThinkingLevel) => updateBtwSettings({ thinkingLevel: level }, { settingsPath }));
|
|
343
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);
|
|
344
369
|
let pendingQuestion = initialQuestion;
|
|
345
370
|
let composerDraft: string | undefined;
|
|
346
371
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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" };
|
|
355
414
|
composerDraft = action.questionDraft;
|
|
356
415
|
continue;
|
|
357
416
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
composerDraft = action.questionDraft;
|
|
361
|
-
continue;
|
|
417
|
+
composerDraft = undefined;
|
|
418
|
+
pendingQuestion = action.question;
|
|
362
419
|
}
|
|
363
|
-
composerDraft = undefined;
|
|
364
|
-
pendingQuestion = action.question;
|
|
365
|
-
}
|
|
366
420
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
+
}
|
|
379
433
|
|
|
380
|
-
|
|
434
|
+
pendingQuestion = undefined;
|
|
435
|
+
}
|
|
436
|
+
} finally {
|
|
437
|
+
await Promise.allSettled([...pendingWrites]);
|
|
381
438
|
}
|
|
382
439
|
}
|
|
383
440
|
|
|
@@ -391,15 +448,27 @@ type BtwCustomFactory<T> = (
|
|
|
391
448
|
async function showBtwCustomPreservingEditor<T>(
|
|
392
449
|
ctx: ExtensionCommandContext,
|
|
393
450
|
factory: BtwCustomFactory<T>,
|
|
394
|
-
): Promise<T> {
|
|
451
|
+
): Promise<T | undefined> {
|
|
395
452
|
let liveEditorText = ctx.ui.getEditorText();
|
|
453
|
+
let completed = false;
|
|
396
454
|
const result = await ctx.ui.custom<T>((tui, theme, keybindings, done) =>
|
|
397
455
|
factory(tui, theme, keybindings, (value) => {
|
|
398
|
-
|
|
456
|
+
try {
|
|
457
|
+
liveEditorText = ctx.ui.getEditorText();
|
|
458
|
+
} catch {
|
|
459
|
+
// Keep completion finite if session replacement invalidates the editor context.
|
|
460
|
+
}
|
|
461
|
+
completed = true;
|
|
399
462
|
done(value);
|
|
400
463
|
}),
|
|
401
464
|
);
|
|
402
|
-
if (
|
|
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
|
+
}
|
|
471
|
+
}
|
|
403
472
|
return result;
|
|
404
473
|
}
|
|
405
474
|
|
|
@@ -501,6 +570,7 @@ export async function chooseBringToMain(
|
|
|
501
570
|
return selector;
|
|
502
571
|
},
|
|
503
572
|
);
|
|
573
|
+
if (!selectedRange) return { kind: "closed" };
|
|
504
574
|
if (selectedRange.kind === "closed") return selectedRange;
|
|
505
575
|
if (selectedRange.kind === "back") break;
|
|
506
576
|
const preview = await showPreview(ctx, selectedRange.draft, selectedRange.summary);
|
|
@@ -518,16 +588,47 @@ export async function chooseBringToMain(
|
|
|
518
588
|
}
|
|
519
589
|
}
|
|
520
590
|
|
|
591
|
+
type BtwMenuSelectorAction =
|
|
592
|
+
| { kind: "select"; value: string }
|
|
593
|
+
| { kind: "back" }
|
|
594
|
+
| { kind: "close" };
|
|
595
|
+
|
|
596
|
+
type BtwBringToMainPreviewAction = { kind: "bring" } | { kind: "back" } | { kind: "close" };
|
|
597
|
+
|
|
521
598
|
async function showBringToMainPreview(
|
|
522
599
|
ctx: ExtensionCommandContext,
|
|
523
600
|
draft: string,
|
|
524
601
|
summary: BtwBringToMainSummary,
|
|
525
602
|
): Promise<BtwBringToMainPreviewAction> {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
603
|
+
let confirmed = false;
|
|
604
|
+
const count = summary.messages === 1 ? "1 message" : `${summary.messages} messages`;
|
|
605
|
+
const lineCount = summary.lines === 1 ? "1 line" : `${summary.lines} lines`;
|
|
606
|
+
const menu = defineMenu<void, "preview", "bring", MenuContext>({
|
|
607
|
+
start: "preview",
|
|
608
|
+
screens: {
|
|
609
|
+
preview: () => ({
|
|
610
|
+
kind: "review",
|
|
611
|
+
title: `Preview · ${count} · ${lineCount} · ~${summary.tokens} tokens`,
|
|
612
|
+
content: draft,
|
|
613
|
+
viewportSize: "adaptive",
|
|
614
|
+
hint: "back",
|
|
615
|
+
confirm: { id: "bring", label: "Bring", action: "bring" },
|
|
616
|
+
}),
|
|
617
|
+
},
|
|
618
|
+
actions: {
|
|
619
|
+
bring: async () => {
|
|
620
|
+
confirmed = true;
|
|
621
|
+
return { kind: "close" } as const;
|
|
622
|
+
},
|
|
623
|
+
},
|
|
624
|
+
});
|
|
625
|
+
const result = await runBtwMenuPreservingEditor(ctx, (menuContext) =>
|
|
626
|
+
runMenu(menuContext, menu, { getState: () => undefined }),
|
|
530
627
|
);
|
|
628
|
+
if (confirmed && result.kind === "closed" && result.reason === "close") {
|
|
629
|
+
return { kind: "bring" };
|
|
630
|
+
}
|
|
631
|
+
return terminalBtwMenuAction(result);
|
|
531
632
|
}
|
|
532
633
|
|
|
533
634
|
async function showBtwMenu(
|
|
@@ -536,11 +637,43 @@ async function showBtwMenu(
|
|
|
536
637
|
options: readonly string[],
|
|
537
638
|
initialValue?: string,
|
|
538
639
|
): Promise<BtwMenuSelectorAction> {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
640
|
+
const items = options.map((label, index) => ({ id: `option-${index}`, label }));
|
|
641
|
+
const initialIndex = initialValue === undefined ? -1 : options.indexOf(initialValue);
|
|
642
|
+
let selectedValue: string | undefined;
|
|
643
|
+
const menu = defineMenu<void, "choices", "select", MenuContext>({
|
|
644
|
+
start: "choices",
|
|
645
|
+
screens: {
|
|
646
|
+
choices: () => ({
|
|
647
|
+
kind: "choice",
|
|
648
|
+
title,
|
|
649
|
+
items,
|
|
650
|
+
action: "select",
|
|
651
|
+
initialItemId: initialIndex >= 0 ? `option-${initialIndex}` : undefined,
|
|
652
|
+
hint: "back",
|
|
653
|
+
}),
|
|
654
|
+
},
|
|
655
|
+
actions: {
|
|
656
|
+
select: async ({ itemId }: { itemId: string }) => {
|
|
657
|
+
const index = Number.parseInt(itemId.slice("option-".length), 10);
|
|
658
|
+
selectedValue = options[index];
|
|
659
|
+
return selectedValue === undefined
|
|
660
|
+
? ({ kind: "stay" } as const)
|
|
661
|
+
: ({ kind: "close" } as const);
|
|
662
|
+
},
|
|
663
|
+
},
|
|
664
|
+
});
|
|
665
|
+
const result = await runBtwMenuPreservingEditor(ctx, (menuContext) =>
|
|
666
|
+
runMenu(menuContext, menu, { getState: () => undefined }),
|
|
543
667
|
);
|
|
668
|
+
return selectedValue !== undefined && result.kind === "closed" && result.reason === "close"
|
|
669
|
+
? { kind: "select", value: selectedValue }
|
|
670
|
+
: terminalBtwMenuAction(result);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function terminalBtwMenuAction(result: RunMenuResult): { kind: "back" } | { kind: "close" } {
|
|
674
|
+
if (result.kind === "closed") return { kind: result.reason };
|
|
675
|
+
if (result.kind === "error") throw result.error;
|
|
676
|
+
return { kind: "close" };
|
|
544
677
|
}
|
|
545
678
|
|
|
546
679
|
export async function loadBringToMainDraft(
|
|
@@ -621,11 +754,18 @@ async function askThreadQuestion(
|
|
|
621
754
|
return ctx.ui.custom<Awaited<ReturnType<typeof completeSideThreadTurn>>>(
|
|
622
755
|
(tui, theme, _keybindings, done) => {
|
|
623
756
|
let settled = false;
|
|
624
|
-
const view = new BtwAnsweringView(
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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
|
+
);
|
|
629
769
|
completeSideThreadTurn({
|
|
630
770
|
thread,
|
|
631
771
|
question,
|
|
@@ -648,28 +788,19 @@ async function showThreadComposer(
|
|
|
648
788
|
thread: SideThread,
|
|
649
789
|
startAtBottom: boolean,
|
|
650
790
|
ctx: ExtensionCommandContext,
|
|
651
|
-
initialQuestion
|
|
791
|
+
initialQuestion: string | undefined,
|
|
792
|
+
thinking: BtwThreadThinkingControl,
|
|
652
793
|
): Promise<TranscriptPagerAction> {
|
|
653
794
|
return ctx.ui.custom<TranscriptPagerAction>(
|
|
654
|
-
(tui, theme,
|
|
795
|
+
(tui, theme, keybindings, done) =>
|
|
655
796
|
new BtwTranscriptPager(tui, theme, thread.turns, done, {
|
|
656
797
|
startAtBottom,
|
|
657
798
|
initialQuestion,
|
|
799
|
+
thinking: { ...thinking, keybindings },
|
|
658
800
|
}),
|
|
659
801
|
);
|
|
660
802
|
}
|
|
661
803
|
|
|
662
|
-
export function sanitizeSingleLine(text: string) {
|
|
663
|
-
return [...text.replace(/[\r\n\t]/g, " ")]
|
|
664
|
-
.filter((character) => {
|
|
665
|
-
const code = character.charCodeAt(0);
|
|
666
|
-
return code > 31 && (code < 127 || code > 159);
|
|
667
|
-
})
|
|
668
|
-
.join("")
|
|
669
|
-
.replace(/ +/g, " ")
|
|
670
|
-
.trim();
|
|
671
|
-
}
|
|
672
|
-
|
|
673
804
|
type MessageContentBlock = {
|
|
674
805
|
type?: string;
|
|
675
806
|
text?: string;
|