@narumitw/pi-btw 0.58.1 → 0.60.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 +8 -1
- package/dist/index.ts +490 -385
- package/dist/index.ts.map +4 -4
- package/package.json +52 -53
- package/src/bring-to-main.ts +494 -547
- package/src/btw.ts +803 -856
- package/src/fullscreen-ui.ts +673 -674
- package/src/keybindings.ts +228 -270
- package/src/main-tree-picker.ts +309 -318
- package/src/menu.ts +645 -454
- package/src/settings.ts +208 -219
- package/src/side-thread.ts +173 -201
- package/src/text.ts +21 -23
- package/src/transcript-markdown.ts +65 -0
- package/src/transcript-pager.ts +611 -662
package/src/btw.ts
CHANGED
|
@@ -1,466 +1,425 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
type Api,
|
|
3
|
+
clampThinkingLevel,
|
|
4
|
+
getSupportedThinkingLevels,
|
|
5
|
+
type Model,
|
|
6
|
+
type ProviderHeaders,
|
|
7
7
|
} from "@earendil-works/pi-ai";
|
|
8
|
-
import {
|
|
9
|
-
BorderedLoader,
|
|
10
|
-
type ExtensionAPI,
|
|
11
|
-
type ExtensionCommandContext,
|
|
12
|
-
} from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { BorderedLoader, type ExtensionAPI, type ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
13
9
|
import type { MenuContext, RunMenuResult } from "@narumitw/pi-tui-kit";
|
|
14
10
|
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
11
|
+
type BtwBringToMainSegment,
|
|
12
|
+
type BtwBringToMainSummary,
|
|
13
|
+
BtwTextRangeSelector,
|
|
14
|
+
type BtwTextRangeSelectorState,
|
|
15
|
+
buildQuickBringToMainSegments,
|
|
16
|
+
estimateBringToMainTokens,
|
|
17
|
+
formatBtwBringToMain,
|
|
18
|
+
getAnsweredTurns,
|
|
19
|
+
summarizeBringToMain,
|
|
24
20
|
} from "./bring-to-main.js";
|
|
25
21
|
import { type RunBtwFullscreen, runBtwFullscreen } from "./fullscreen-ui.js";
|
|
26
22
|
import { pickMainEntry } from "./main-tree-picker.js";
|
|
27
23
|
import {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
24
|
+
type BtwCommandMenuResult,
|
|
25
|
+
type BtwResumeThreadSummary,
|
|
26
|
+
runBtwMenuPreservingEditor,
|
|
27
|
+
showBtwCommandMenu,
|
|
28
|
+
showBtwCustomPreservingEditor,
|
|
33
29
|
} from "./menu.js";
|
|
34
30
|
import {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
31
|
+
type BtwSettings,
|
|
32
|
+
effectiveFullscreenCopyOnSelect,
|
|
33
|
+
effectiveRememberThinkingLevelChanges,
|
|
34
|
+
parseBtwModelReference,
|
|
35
|
+
readBtwSettings,
|
|
36
|
+
updateBtwSettings,
|
|
41
37
|
} from "./settings.js";
|
|
42
38
|
import {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
39
|
+
BTW_THINKING_LEVELS,
|
|
40
|
+
type BtwThinkingLevel,
|
|
41
|
+
type CompleteSimpleFunction,
|
|
42
|
+
completeSideThreadTurn,
|
|
43
|
+
createSideThread,
|
|
44
|
+
type SideQuestionAuth,
|
|
45
|
+
type SideThread,
|
|
50
46
|
} from "./side-thread.js";
|
|
51
47
|
import { sanitizeSingleLine } from "./text.js";
|
|
48
|
+
import { type BtwMarkdownTransformers, prepareBtwTranscriptMarkdown } from "./transcript-markdown.js";
|
|
52
49
|
import {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
50
|
+
BtwAnsweringView,
|
|
51
|
+
type BtwThinkingControl,
|
|
52
|
+
BtwTranscriptPager,
|
|
53
|
+
type TranscriptPagerAction,
|
|
57
54
|
} from "./transcript-pager.js";
|
|
58
55
|
|
|
59
56
|
export {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
57
|
+
BTW_SETTINGS_FILE,
|
|
58
|
+
type BtwSettings,
|
|
59
|
+
type BtwSettingsLoadResult,
|
|
60
|
+
normalizeBtwSettings,
|
|
61
|
+
parseBtwModelReference,
|
|
62
|
+
readBtwSettings,
|
|
66
63
|
} from "./settings.js";
|
|
67
64
|
export {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
65
|
+
BTW_THINKING_LEVELS,
|
|
66
|
+
type BtwThinkingLevel,
|
|
67
|
+
buildUserPrompt,
|
|
68
|
+
completeSideQuestion,
|
|
72
69
|
} from "./side-thread.js";
|
|
73
70
|
export { sanitizeSingleLine } from "./text.js";
|
|
74
71
|
|
|
75
72
|
const MAX_CONTEXT_CHARS = 40_000;
|
|
76
73
|
|
|
77
74
|
interface LoadBtwThinkingLevelOptions {
|
|
78
|
-
|
|
79
|
-
|
|
75
|
+
settingsPath?: string;
|
|
76
|
+
warn?: (message: string) => void;
|
|
80
77
|
}
|
|
81
78
|
|
|
82
|
-
type BtwModelRegistry = Pick<
|
|
83
|
-
ExtensionCommandContext["modelRegistry"],
|
|
84
|
-
"find" | "getApiKeyAndHeaders"
|
|
85
|
-
>;
|
|
79
|
+
type BtwModelRegistry = Pick<ExtensionCommandContext["modelRegistry"], "find" | "getApiKeyAndHeaders">;
|
|
86
80
|
|
|
87
81
|
type BtwProviderRegistry = Pick<ExtensionCommandContext["modelRegistry"], "getProvider">;
|
|
88
82
|
|
|
89
|
-
export function createModelRegistryCompleteSimple(
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
return provider.streamSimple(model, context, options).result();
|
|
96
|
-
};
|
|
83
|
+
export function createModelRegistryCompleteSimple(modelRegistry: BtwProviderRegistry): CompleteSimpleFunction {
|
|
84
|
+
return async (model, context, options) => {
|
|
85
|
+
const provider = modelRegistry.getProvider(model.provider);
|
|
86
|
+
if (!provider) throw new Error(`No provider registered for model provider: ${model.provider}`);
|
|
87
|
+
return provider.streamSimple(model, context, options).result();
|
|
88
|
+
};
|
|
97
89
|
}
|
|
98
90
|
|
|
99
91
|
interface ResolveBtwModelOptions {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
92
|
+
settings: BtwSettings;
|
|
93
|
+
currentModel: Model<Api> | undefined;
|
|
94
|
+
modelRegistry: BtwModelRegistry;
|
|
95
|
+
warn?: (message: string) => void;
|
|
104
96
|
}
|
|
105
97
|
|
|
106
98
|
export interface ResolvedBtwModel {
|
|
107
|
-
|
|
108
|
-
|
|
99
|
+
model: Model<Api>;
|
|
100
|
+
auth: SideQuestionAuth;
|
|
109
101
|
}
|
|
110
102
|
|
|
111
103
|
export interface BtwThreadState {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
104
|
+
id: string;
|
|
105
|
+
title?: string;
|
|
106
|
+
thread: SideThread;
|
|
107
|
+
thinkingLevel: BtwThinkingLevel;
|
|
108
|
+
createdAt: number;
|
|
109
|
+
updatedAt: number;
|
|
118
110
|
}
|
|
119
111
|
|
|
120
112
|
export async function resolveBtwModel({
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
113
|
+
settings,
|
|
114
|
+
currentModel,
|
|
115
|
+
modelRegistry,
|
|
116
|
+
warn,
|
|
125
117
|
}: ResolveBtwModelOptions): Promise<ResolvedBtwModel | undefined> {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
return {
|
|
174
|
-
model: auth.baseUrl ? { ...currentModel, baseUrl: auth.baseUrl } : currentModel,
|
|
175
|
-
auth,
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
} catch {
|
|
179
|
-
// The caller reports the final lack of an available model.
|
|
180
|
-
}
|
|
181
|
-
return undefined;
|
|
118
|
+
const reportWarning = (message: string) => warn?.(sanitizeSingleLine(message));
|
|
119
|
+
if (settings.model) {
|
|
120
|
+
const fallback = currentModel ? `${currentModel.provider}/${currentModel.id}` : "the current model";
|
|
121
|
+
const reference = parseBtwModelReference(settings.model);
|
|
122
|
+
if (!reference) {
|
|
123
|
+
reportWarning(`pi-btw model ${settings.model} is invalid; falling back to ${fallback}.`);
|
|
124
|
+
return resolveBtwModel({ settings: {}, currentModel, modelRegistry, warn: reportWarning });
|
|
125
|
+
}
|
|
126
|
+
const configuredModel = modelRegistry.find(reference.provider, reference.modelId);
|
|
127
|
+
if (!configuredModel) {
|
|
128
|
+
reportWarning(`pi-btw model ${settings.model} was not found; falling back to ${fallback}.`);
|
|
129
|
+
} else {
|
|
130
|
+
const sameAsCurrent =
|
|
131
|
+
configuredModel === currentModel ||
|
|
132
|
+
(configuredModel.provider === currentModel?.provider && configuredModel.id === currentModel.id);
|
|
133
|
+
const fallbackAction = sameAsCurrent ? "no distinct current model is available" : `falling back to ${fallback}`;
|
|
134
|
+
try {
|
|
135
|
+
const auth = await modelRegistry.getApiKeyAndHeaders(configuredModel);
|
|
136
|
+
if (auth.ok && hasRequestAuth(auth)) {
|
|
137
|
+
return {
|
|
138
|
+
model: auth.baseUrl ? { ...configuredModel, baseUrl: auth.baseUrl } : configuredModel,
|
|
139
|
+
auth,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const reason = auth.ok ? "has no request credentials" : auth.error;
|
|
143
|
+
reportWarning(`pi-btw model ${settings.model} is unavailable (${reason}); ${fallbackAction}.`);
|
|
144
|
+
} catch (error: unknown) {
|
|
145
|
+
reportWarning(`pi-btw model ${settings.model} credentials failed (${formatError(error)}); ${fallbackAction}.`);
|
|
146
|
+
}
|
|
147
|
+
if (sameAsCurrent) return undefined;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!currentModel) return undefined;
|
|
152
|
+
try {
|
|
153
|
+
const auth = await modelRegistry.getApiKeyAndHeaders(currentModel);
|
|
154
|
+
if (auth.ok && hasRequestAuth(auth)) {
|
|
155
|
+
// Direct provider streams bypass Pi's request preparation, including OAuth routing.
|
|
156
|
+
return {
|
|
157
|
+
model: auth.baseUrl ? { ...currentModel, baseUrl: auth.baseUrl } : currentModel,
|
|
158
|
+
auth,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
} catch {
|
|
162
|
+
// The caller reports the final lack of an available model.
|
|
163
|
+
}
|
|
164
|
+
return undefined;
|
|
182
165
|
}
|
|
183
166
|
|
|
184
167
|
function hasRequestAuth(auth: SideQuestionAuth): boolean {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
(auth.env && Object.keys(auth.env).length > 0),
|
|
189
|
-
);
|
|
168
|
+
return Boolean(
|
|
169
|
+
auth.apiKey || providerHeadersHaveValue(auth.headers) || (auth.env && Object.keys(auth.env).length > 0),
|
|
170
|
+
);
|
|
190
171
|
}
|
|
191
172
|
|
|
192
173
|
function providerHeadersHaveValue(headers: ProviderHeaders | undefined): boolean {
|
|
193
|
-
|
|
174
|
+
return headers !== undefined && Object.values(headers).some((value) => value !== null);
|
|
194
175
|
}
|
|
195
176
|
|
|
196
177
|
export async function loadBtwThinkingLevel(
|
|
197
|
-
|
|
198
|
-
|
|
178
|
+
currentThinkingLevel: BtwThinkingLevel,
|
|
179
|
+
options: LoadBtwThinkingLevelOptions = {},
|
|
199
180
|
): Promise<BtwThinkingLevel> {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
181
|
+
const settings = await readBtwSettings(options.settingsPath);
|
|
182
|
+
if (settings.kind === "missing") return currentThinkingLevel;
|
|
183
|
+
if (settings.kind === "loaded") {
|
|
184
|
+
return settings.settings.thinkingLevel ?? currentThinkingLevel;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
options.warn?.(
|
|
188
|
+
sanitizeSingleLine(
|
|
189
|
+
`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.`,
|
|
190
|
+
),
|
|
191
|
+
);
|
|
192
|
+
return currentThinkingLevel;
|
|
212
193
|
}
|
|
213
194
|
|
|
214
195
|
function formatError(error: unknown): string {
|
|
215
|
-
|
|
196
|
+
return error instanceof Error ? error.message : String(error);
|
|
216
197
|
}
|
|
217
198
|
|
|
218
199
|
function readBtwSessionId(ctx: ExtensionCommandContext): string | undefined {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
200
|
+
const getSessionId = ctx.sessionManager.getSessionId;
|
|
201
|
+
if (typeof getSessionId !== "function") return undefined;
|
|
202
|
+
const sessionId = getSessionId.call(ctx.sessionManager);
|
|
203
|
+
return sessionId.length > 0 ? sessionId : undefined;
|
|
223
204
|
}
|
|
224
205
|
|
|
225
206
|
function notifySafely(
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
207
|
+
ctx: ExtensionCommandContext,
|
|
208
|
+
message: string,
|
|
209
|
+
level: Parameters<ExtensionCommandContext["ui"]["notify"]>[1],
|
|
229
210
|
): void {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
211
|
+
try {
|
|
212
|
+
ctx.ui.notify(sanitizeSingleLine(message), level);
|
|
213
|
+
} catch {
|
|
214
|
+
// Async command continuations may finish after their ExtensionContext is replaced.
|
|
215
|
+
}
|
|
235
216
|
}
|
|
236
217
|
|
|
237
218
|
// Keep this slightly-over-1,000-line command coordinator intact: its injectable menu,
|
|
238
219
|
// request, resume, and delivery flows share the same thread-state and test seams;
|
|
239
220
|
// settings, keybinding policy, terminal ownership, and rendering live in separate modules.
|
|
240
221
|
export interface BtwExtensionDependencies {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
222
|
+
showCommandMenu?: (
|
|
223
|
+
pi: ExtensionAPI,
|
|
224
|
+
ctx: ExtensionCommandContext,
|
|
225
|
+
resumeThreads: readonly BtwResumeThreadSummary[],
|
|
226
|
+
) => Promise<BtwCommandMenuResult>;
|
|
227
|
+
pickMainEntry?: typeof pickMainEntry;
|
|
228
|
+
loadSettings?: typeof loadSettingsForCommand;
|
|
229
|
+
resolveModel?: typeof resolveBtwModelWithLoader;
|
|
230
|
+
runThread?: typeof runBtwThread;
|
|
231
|
+
runFullscreen?: RunBtwFullscreen;
|
|
251
232
|
}
|
|
252
233
|
|
|
253
234
|
export default function btw(pi: ExtensionAPI, dependencies: BtwExtensionDependencies = {}) {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
if (state?.title && state.thread.turns.length > 0) {
|
|
373
|
-
if (state.thread.turns.length > startingTurnCount) {
|
|
374
|
-
resumableThreads.delete(state.id);
|
|
375
|
-
}
|
|
376
|
-
resumableThreads.set(state.id, state);
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
},
|
|
380
|
-
});
|
|
235
|
+
const showCommandMenu = dependencies.showCommandMenu ?? showCommandMenuForBtw;
|
|
236
|
+
const pickEntry = dependencies.pickMainEntry ?? pickMainEntry;
|
|
237
|
+
const loadSettings = dependencies.loadSettings ?? loadSettingsForCommand;
|
|
238
|
+
const resolveModel = dependencies.resolveModel ?? resolveBtwModelWithLoader;
|
|
239
|
+
const runThread = dependencies.runThread ?? runBtwThread;
|
|
240
|
+
const runFullscreen = dependencies.runFullscreen ?? runBtwFullscreen;
|
|
241
|
+
// Pi creates a fresh extension instance after session replacement or reload.
|
|
242
|
+
const resumableThreads = new Map<string, BtwThreadState>();
|
|
243
|
+
let nextThreadNumber = 1;
|
|
244
|
+
const listResumeThreads = (): BtwResumeThreadSummary[] =>
|
|
245
|
+
[...resumableThreads.values()]
|
|
246
|
+
.reverse()
|
|
247
|
+
.filter((state) => state.thread.turns.length > 0 && state.title)
|
|
248
|
+
.sort((first, second) => second.updatedAt - first.updatedAt || second.createdAt - first.createdAt)
|
|
249
|
+
.map((state) => ({
|
|
250
|
+
id: state.id,
|
|
251
|
+
title: state.title ?? "Untitled side thread",
|
|
252
|
+
questionCount: state.thread.turns.length,
|
|
253
|
+
}));
|
|
254
|
+
pi.registerCommand("btw", {
|
|
255
|
+
description: "Ask a quick side question without adding it to the main conversation",
|
|
256
|
+
handler: async (args, ctx) => {
|
|
257
|
+
const question = args.trim();
|
|
258
|
+
if (ctx.mode !== "tui") {
|
|
259
|
+
ctx.ui.notify("/btw requires interactive TUI mode", "error");
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
let menuResult: BtwCommandMenuResult = "start";
|
|
264
|
+
let selectedConversationContext: string | undefined;
|
|
265
|
+
if (!question) {
|
|
266
|
+
while (true) {
|
|
267
|
+
menuResult = await showCommandMenu(pi, ctx, listResumeThreads());
|
|
268
|
+
if (menuResult === "closed") return;
|
|
269
|
+
if (menuResult !== "tree") break;
|
|
270
|
+
|
|
271
|
+
const treeResult = await pickEntry(pi, ctx);
|
|
272
|
+
if (treeResult.kind === "closed") return;
|
|
273
|
+
if (treeResult.kind === "back") continue;
|
|
274
|
+
try {
|
|
275
|
+
if (!ctx.sessionManager.getEntry(treeResult.entryId)) {
|
|
276
|
+
notifySafely(ctx, "The selected main-thread entry is no longer available", "warning");
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
const branch = ctx.sessionManager.getBranch(treeResult.entryId);
|
|
280
|
+
if (branch.at(-1)?.id !== treeResult.entryId) {
|
|
281
|
+
notifySafely(ctx, "The selected main-thread branch is no longer available", "warning");
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
selectedConversationContext = buildConversationContext(branch);
|
|
285
|
+
menuResult = "start";
|
|
286
|
+
break;
|
|
287
|
+
} catch {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const settings = await loadSettings(ctx);
|
|
294
|
+
const sameAsMainThinkingLevel = settings.thinkingLevel === undefined;
|
|
295
|
+
const resolution = await resolveModel(settings, ctx);
|
|
296
|
+
if (resolution.kind === "cancelled") {
|
|
297
|
+
notifySafely(ctx, "Cancelled", "info");
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (resolution.kind === "unavailable") {
|
|
301
|
+
notifySafely(ctx, "No available model for /btw", "error");
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
let state = typeof menuResult === "object" ? resumableThreads.get(menuResult.threadId) : undefined;
|
|
306
|
+
if (typeof menuResult === "object" && !state) {
|
|
307
|
+
notifySafely(ctx, "The selected /btw side thread is no longer available", "warning");
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const startingTurnCount = state?.thread.turns.length ?? 0;
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
await runFullscreen(
|
|
314
|
+
ctx,
|
|
315
|
+
(fullscreenCtx) => {
|
|
316
|
+
if (!state) {
|
|
317
|
+
const createdAt = Date.now();
|
|
318
|
+
state = {
|
|
319
|
+
id: `btw-${nextThreadNumber}`,
|
|
320
|
+
thread: createSideThread(
|
|
321
|
+
selectedConversationContext ?? buildConversationContext(fullscreenCtx.sessionManager.getBranch()),
|
|
322
|
+
),
|
|
323
|
+
thinkingLevel: settings.thinkingLevel ?? pi.getThinkingLevel(),
|
|
324
|
+
createdAt,
|
|
325
|
+
updatedAt: createdAt,
|
|
326
|
+
};
|
|
327
|
+
nextThreadNumber += 1;
|
|
328
|
+
}
|
|
329
|
+
return runThread({
|
|
330
|
+
initialQuestion: question || undefined,
|
|
331
|
+
selected: resolution.selected,
|
|
332
|
+
thinkingLevel: state.thinkingLevel,
|
|
333
|
+
rememberThinkingLevelChanges: !sameAsMainThinkingLevel && effectiveRememberThinkingLevelChanges(settings),
|
|
334
|
+
state,
|
|
335
|
+
ctx: fullscreenCtx,
|
|
336
|
+
});
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
copyOnSelect: effectiveFullscreenCopyOnSelect(settings),
|
|
340
|
+
...(settings.keybindings ? { keybindings: settings.keybindings } : {}),
|
|
341
|
+
},
|
|
342
|
+
);
|
|
343
|
+
} finally {
|
|
344
|
+
if (state?.title && state.thread.turns.length > 0) {
|
|
345
|
+
if (state.thread.turns.length > startingTurnCount) {
|
|
346
|
+
resumableThreads.delete(state.id);
|
|
347
|
+
}
|
|
348
|
+
resumableThreads.set(state.id, state);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
});
|
|
381
353
|
}
|
|
382
354
|
|
|
383
355
|
async function showCommandMenuForBtw(
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
356
|
+
pi: ExtensionAPI,
|
|
357
|
+
ctx: ExtensionCommandContext,
|
|
358
|
+
resumeThreads: readonly BtwResumeThreadSummary[],
|
|
387
359
|
): Promise<BtwCommandMenuResult> {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
const settings = loaded.kind === "loaded" ? loaded.settings : {};
|
|
393
|
-
const configured = settings.model ? parseBtwModelReference(settings.model) : undefined;
|
|
394
|
-
const configuredModel = configured
|
|
395
|
-
? availableModels.find(
|
|
396
|
-
(model) => model.provider === configured.provider && model.id === configured.modelId,
|
|
397
|
-
)
|
|
398
|
-
: undefined;
|
|
399
|
-
const model = configuredModel ?? currentModel;
|
|
400
|
-
return showBtwCommandMenu(ctx, {
|
|
401
|
-
currentThinkingLevel,
|
|
402
|
-
availableThinkingLevels: model ? getSupportedThinkingLevels(model) : BTW_THINKING_LEVELS,
|
|
403
|
-
resumeThreads,
|
|
404
|
-
});
|
|
360
|
+
return showBtwCommandMenu(ctx, {
|
|
361
|
+
currentThinkingLevel: pi.getThinkingLevel(),
|
|
362
|
+
resumeThreads,
|
|
363
|
+
});
|
|
405
364
|
}
|
|
406
365
|
|
|
407
366
|
async function loadSettingsForCommand(ctx: ExtensionCommandContext): Promise<BtwSettings> {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
367
|
+
const settingsResult = await readBtwSettings();
|
|
368
|
+
if (settingsResult.kind === "loaded") return settingsResult.settings;
|
|
369
|
+
if (settingsResult.kind === "invalid") {
|
|
370
|
+
notifySafely(ctx, `pi-btw settings ignored: ${settingsResult.reason}`, "warning");
|
|
371
|
+
}
|
|
372
|
+
return {};
|
|
414
373
|
}
|
|
415
374
|
|
|
416
375
|
type ModelResolutionOutcome =
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
376
|
+
| { kind: "cancelled" }
|
|
377
|
+
| { kind: "unavailable" }
|
|
378
|
+
| { kind: "selected"; selected: ResolvedBtwModel };
|
|
420
379
|
|
|
421
380
|
async function resolveBtwModelWithLoader(
|
|
422
|
-
|
|
423
|
-
|
|
381
|
+
settings: BtwSettings,
|
|
382
|
+
ctx: ExtensionCommandContext,
|
|
424
383
|
): Promise<ModelResolutionOutcome> {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
384
|
+
return ctx.ui.custom<ModelResolutionOutcome>((tui, theme, _keybindings, done) => {
|
|
385
|
+
const loader = new BorderedLoader(tui, theme, "Resolving /btw model credentials...");
|
|
386
|
+
let settled = false;
|
|
387
|
+
loader.onAbort = () => {
|
|
388
|
+
if (settled) return;
|
|
389
|
+
settled = true;
|
|
390
|
+
done({ kind: "cancelled" });
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
resolveBtwModel({
|
|
394
|
+
settings,
|
|
395
|
+
currentModel: ctx.model,
|
|
396
|
+
modelRegistry: ctx.modelRegistry,
|
|
397
|
+
warn: (message) => {
|
|
398
|
+
if (!settled) notifySafely(ctx, message, "warning");
|
|
399
|
+
},
|
|
400
|
+
})
|
|
401
|
+
.then((selected) => {
|
|
402
|
+
if (settled) return;
|
|
403
|
+
settled = true;
|
|
404
|
+
done(selected ? { kind: "selected", selected } : { kind: "unavailable" });
|
|
405
|
+
})
|
|
406
|
+
.catch(() => {
|
|
407
|
+
if (settled) return;
|
|
408
|
+
settled = true;
|
|
409
|
+
done({ kind: "unavailable" });
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
return loader;
|
|
413
|
+
});
|
|
455
414
|
}
|
|
456
415
|
|
|
457
416
|
interface RunBtwThreadDependencies {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
417
|
+
ask?: typeof askThreadQuestion;
|
|
418
|
+
interact?: typeof showThreadComposer;
|
|
419
|
+
chooseBringToMain?: typeof chooseBringToMain;
|
|
420
|
+
deliverBringToMain?: typeof loadBringToMainDraft;
|
|
421
|
+
persistThinkingLevel?: (level: BtwThinkingLevel) => Promise<unknown>;
|
|
422
|
+
now?: () => number;
|
|
464
423
|
}
|
|
465
424
|
|
|
466
425
|
export type BtwThreadResult = { kind: "closed" };
|
|
@@ -468,557 +427,545 @@ export type BtwThreadResult = { kind: "closed" };
|
|
|
468
427
|
type BtwThreadThinkingControl = Omit<BtwThinkingControl, "keybindings">;
|
|
469
428
|
|
|
470
429
|
interface BtwThreadSteeringControl {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
430
|
+
questions: readonly string[];
|
|
431
|
+
submit: (question: string) => void;
|
|
432
|
+
thinking: BtwThreadThinkingControl;
|
|
474
433
|
}
|
|
475
434
|
|
|
476
435
|
type BtwBringToMainChoice =
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
436
|
+
| BtwThreadResult
|
|
437
|
+
| {
|
|
438
|
+
kind: "bringToMain";
|
|
439
|
+
draft: string;
|
|
440
|
+
summary: BtwBringToMainSummary;
|
|
441
|
+
selectionState?: BtwTextRangeSelectorState;
|
|
442
|
+
}
|
|
443
|
+
| { kind: "back" };
|
|
485
444
|
|
|
486
445
|
type BtwBringToMainDelivery = "loaded" | "back" | "closed";
|
|
487
446
|
|
|
488
447
|
interface RunBtwThreadOptions {
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
448
|
+
initialQuestion?: string;
|
|
449
|
+
selected: ResolvedBtwModel;
|
|
450
|
+
thinkingLevel: BtwThinkingLevel;
|
|
451
|
+
rememberThinkingLevelChanges?: boolean;
|
|
452
|
+
settingsPath?: string;
|
|
453
|
+
state?: BtwThreadState;
|
|
454
|
+
ctx: ExtensionCommandContext;
|
|
455
|
+
dependencies?: RunBtwThreadDependencies;
|
|
497
456
|
}
|
|
498
457
|
|
|
499
458
|
export async function runBtwThread({
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
459
|
+
initialQuestion,
|
|
460
|
+
selected,
|
|
461
|
+
thinkingLevel,
|
|
462
|
+
rememberThinkingLevelChanges = false,
|
|
463
|
+
settingsPath,
|
|
464
|
+
state,
|
|
465
|
+
ctx,
|
|
466
|
+
dependencies = {},
|
|
508
467
|
}: RunBtwThreadOptions): Promise<BtwThreadResult> {
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
if (state) {
|
|
597
|
-
state.title ||= sanitizeSingleLine(pendingQuestion) || "Untitled side thread";
|
|
598
|
-
state.updatedAt = now();
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
pendingQuestion = steeringQuestions.shift();
|
|
602
|
-
}
|
|
603
|
-
} finally {
|
|
604
|
-
await Promise.allSettled([...pendingWrites]);
|
|
605
|
-
}
|
|
468
|
+
const ask = dependencies.ask ?? askThreadQuestion;
|
|
469
|
+
const interact = dependencies.interact ?? showThreadComposer;
|
|
470
|
+
const chooseBringToMainAction = dependencies.chooseBringToMain ?? chooseBringToMain;
|
|
471
|
+
const deliverBringToMainDraft = dependencies.deliverBringToMain ?? loadBringToMainDraft;
|
|
472
|
+
const persistThinkingLevel =
|
|
473
|
+
dependencies.persistThinkingLevel ??
|
|
474
|
+
((level: BtwThinkingLevel) => updateBtwSettings({ thinkingLevel: level }, { settingsPath }));
|
|
475
|
+
const now = dependencies.now ?? Date.now;
|
|
476
|
+
const thread = state?.thread ?? createSideThread(buildConversationContext(ctx.sessionManager.getBranch()));
|
|
477
|
+
const thinkingLevels = getSupportedThinkingLevels(selected.model);
|
|
478
|
+
const pendingWrites = new Set<Promise<void>>();
|
|
479
|
+
const steeringQuestions: string[] = [];
|
|
480
|
+
let activeThinkingLevel = clampThinkingLevel(selected.model, state?.thinkingLevel ?? thinkingLevel);
|
|
481
|
+
if (state) state.thinkingLevel = activeThinkingLevel;
|
|
482
|
+
let pendingQuestion = initialQuestion;
|
|
483
|
+
let composerDraft: string | undefined;
|
|
484
|
+
const createThinkingControl = (): BtwThreadThinkingControl => ({
|
|
485
|
+
level: activeThinkingLevel,
|
|
486
|
+
levels: thinkingLevels,
|
|
487
|
+
onChange: (level) => {
|
|
488
|
+
if (!thinkingLevels.includes(level)) return;
|
|
489
|
+
activeThinkingLevel = level;
|
|
490
|
+
if (state) state.thinkingLevel = level;
|
|
491
|
+
if (!rememberThinkingLevelChanges) return;
|
|
492
|
+
let write!: Promise<void>;
|
|
493
|
+
write = Promise.resolve()
|
|
494
|
+
.then(() => persistThinkingLevel(level))
|
|
495
|
+
.then(() => undefined)
|
|
496
|
+
.catch((error: unknown) => {
|
|
497
|
+
notifySafely(
|
|
498
|
+
ctx,
|
|
499
|
+
`Thinking level changed to ${level}, but could not be remembered in pi-btw.json: ${formatError(error)}`,
|
|
500
|
+
"warning",
|
|
501
|
+
);
|
|
502
|
+
})
|
|
503
|
+
.finally(() => pendingWrites.delete(write));
|
|
504
|
+
pendingWrites.add(write);
|
|
505
|
+
},
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
try {
|
|
509
|
+
while (true) {
|
|
510
|
+
if (!pendingQuestion) {
|
|
511
|
+
const action = await interact(thread, thread.turns.length > 0, ctx, composerDraft, createThinkingControl());
|
|
512
|
+
if (action.kind === "close") return { kind: "closed" };
|
|
513
|
+
if (action.kind === "bringToMain") {
|
|
514
|
+
const choice = await chooseBringToMainAction(thread, ctx);
|
|
515
|
+
if (choice.kind === "closed") return choice;
|
|
516
|
+
if (choice.kind === "back") {
|
|
517
|
+
composerDraft = action.questionDraft;
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
const delivery = await deliverBringToMainDraft(choice.draft, ctx, choice.summary);
|
|
521
|
+
if (delivery === "loaded" || delivery === "closed") return { kind: "closed" };
|
|
522
|
+
composerDraft = action.questionDraft;
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
composerDraft = undefined;
|
|
526
|
+
pendingQuestion = action.question;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const result = await ask(thread, pendingQuestion, selected, activeThinkingLevel, ctx, {
|
|
530
|
+
questions: steeringQuestions,
|
|
531
|
+
submit: (question) => steeringQuestions.push(question),
|
|
532
|
+
thinking: createThinkingControl(),
|
|
533
|
+
});
|
|
534
|
+
if (result.kind === "aborted") {
|
|
535
|
+
notifySafely(ctx, "Cancelled", "info");
|
|
536
|
+
return { kind: "closed" };
|
|
537
|
+
}
|
|
538
|
+
if (result.kind === "error") {
|
|
539
|
+
thread.turns.push({
|
|
540
|
+
kind: "error",
|
|
541
|
+
question: pendingQuestion,
|
|
542
|
+
answer: result.message,
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
if (state) {
|
|
546
|
+
state.title ||= sanitizeSingleLine(pendingQuestion) || "Untitled side thread";
|
|
547
|
+
state.updatedAt = now();
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
pendingQuestion = steeringQuestions.shift();
|
|
551
|
+
}
|
|
552
|
+
} finally {
|
|
553
|
+
await Promise.allSettled([...pendingWrites]);
|
|
554
|
+
}
|
|
606
555
|
}
|
|
607
556
|
|
|
608
557
|
interface ChooseBringToMainDependencies {
|
|
609
|
-
|
|
610
|
-
|
|
558
|
+
showMenu?: typeof showBtwMenu;
|
|
559
|
+
showPreview?: typeof showBringToMainPreview;
|
|
611
560
|
}
|
|
612
561
|
|
|
613
562
|
export async function chooseBringToMain(
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
563
|
+
thread: SideThread,
|
|
564
|
+
ctx: ExtensionCommandContext,
|
|
565
|
+
dependencies: ChooseBringToMainDependencies = {},
|
|
617
566
|
): Promise<BtwBringToMainChoice> {
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
summary: selectedRange.summary,
|
|
719
|
-
};
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
type BtwMenuSelectorAction =
|
|
725
|
-
| { kind: "select"; value: string }
|
|
726
|
-
| { kind: "back" }
|
|
727
|
-
| { kind: "close" };
|
|
567
|
+
const answered = getAnsweredTurns(thread.turns);
|
|
568
|
+
if (answered.length === 0) return { kind: "back" };
|
|
569
|
+
const showMenu = dependencies.showMenu ?? showBtwMenu;
|
|
570
|
+
const showPreview = dependencies.showPreview ?? showBringToMainPreview;
|
|
571
|
+
const makeChoice = (segments: readonly BtwBringToMainSegment[]) => ({
|
|
572
|
+
kind: "bringToMain" as const,
|
|
573
|
+
draft: formatBtwBringToMain(segments),
|
|
574
|
+
summary: summarizeBringToMain(segments),
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
const latestSegments = buildQuickBringToMainSegments(thread.turns, { kind: "latest" });
|
|
578
|
+
const entireSegments = buildQuickBringToMainSegments(thread.turns, { kind: "entire" });
|
|
579
|
+
const latestOption = `Latest question and answer 1 Q&A · ~${estimateBringToMainTokens(latestSegments)} tokens`;
|
|
580
|
+
const fromOption = "From a question onward… Choose a starting question";
|
|
581
|
+
const exactOption = "Select exact text… Lines or characters";
|
|
582
|
+
const entireOption = `Entire side thread ${answered.length} Q&A · ~${estimateBringToMainTokens(entireSegments)} tokens`;
|
|
583
|
+
const cancelOption = "Cancel Return to the side thread";
|
|
584
|
+
let selectedScope: string | undefined;
|
|
585
|
+
|
|
586
|
+
while (true) {
|
|
587
|
+
const scopeResult = await showMenu(
|
|
588
|
+
ctx,
|
|
589
|
+
"Bring what back to the main thread?",
|
|
590
|
+
[latestOption, fromOption, exactOption, entireOption, cancelOption],
|
|
591
|
+
selectedScope,
|
|
592
|
+
);
|
|
593
|
+
if (scopeResult.kind === "close") return { kind: "closed" };
|
|
594
|
+
if (scopeResult.kind === "back" || scopeResult.value === cancelOption) return { kind: "back" };
|
|
595
|
+
const scope = scopeResult.value;
|
|
596
|
+
selectedScope = scope;
|
|
597
|
+
if (scope === latestOption) return makeChoice(latestSegments);
|
|
598
|
+
if (scope === entireOption) {
|
|
599
|
+
const choice = makeChoice(entireSegments);
|
|
600
|
+
const preview = await showPreview(ctx, choice.draft, choice.summary);
|
|
601
|
+
if (preview.kind === "close") return { kind: "closed" };
|
|
602
|
+
if (preview.kind === "back") continue;
|
|
603
|
+
return choice;
|
|
604
|
+
}
|
|
605
|
+
if (scope === fromOption) {
|
|
606
|
+
const questions = answered.map(
|
|
607
|
+
(turn, index) => `${index + 1}. ${truncatePreview(sanitizeSingleLine(turn.question))}`,
|
|
608
|
+
);
|
|
609
|
+
let selectedQuestion: string | undefined;
|
|
610
|
+
while (true) {
|
|
611
|
+
const questionResult = await showMenu(ctx, "Start from which question?", questions, selectedQuestion);
|
|
612
|
+
if (questionResult.kind === "close") return { kind: "closed" };
|
|
613
|
+
if (questionResult.kind === "back") break;
|
|
614
|
+
const answeredTurnIndex = questions.indexOf(questionResult.value);
|
|
615
|
+
if (answeredTurnIndex < 0) continue;
|
|
616
|
+
selectedQuestion = questionResult.value;
|
|
617
|
+
const choice = makeChoice(buildQuickBringToMainSegments(thread.turns, { kind: "from", answeredTurnIndex }));
|
|
618
|
+
const preview = await showPreview(ctx, choice.draft, choice.summary);
|
|
619
|
+
if (preview.kind === "close") return { kind: "closed" };
|
|
620
|
+
if (preview.kind === "back") continue;
|
|
621
|
+
return choice;
|
|
622
|
+
}
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (scope !== exactOption) continue;
|
|
627
|
+
let selectionState: BtwTextRangeSelectorState | undefined;
|
|
628
|
+
while (true) {
|
|
629
|
+
const selectedRange = await showBtwCustomPreservingEditor<BtwBringToMainChoice>(
|
|
630
|
+
ctx,
|
|
631
|
+
(tui, theme, keybindings, done) => {
|
|
632
|
+
let selector: BtwTextRangeSelector;
|
|
633
|
+
selector = new BtwTextRangeSelector(
|
|
634
|
+
tui,
|
|
635
|
+
theme,
|
|
636
|
+
keybindings,
|
|
637
|
+
thread.turns,
|
|
638
|
+
(action) => {
|
|
639
|
+
if (action.kind === "back") done({ kind: "back" });
|
|
640
|
+
else if (action.kind === "close") done({ kind: "closed" });
|
|
641
|
+
else done({ ...makeChoice(action.segments), selectionState: selector.getState() });
|
|
642
|
+
},
|
|
643
|
+
selectionState,
|
|
644
|
+
);
|
|
645
|
+
return selector;
|
|
646
|
+
},
|
|
647
|
+
);
|
|
648
|
+
if (!selectedRange) return { kind: "closed" };
|
|
649
|
+
if (selectedRange.kind === "closed") return selectedRange;
|
|
650
|
+
if (selectedRange.kind === "back") break;
|
|
651
|
+
const preview = await showPreview(ctx, selectedRange.draft, selectedRange.summary);
|
|
652
|
+
if (preview.kind === "close") return { kind: "closed" };
|
|
653
|
+
if (preview.kind === "back") {
|
|
654
|
+
selectionState = selectedRange.selectionState;
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
return {
|
|
658
|
+
kind: "bringToMain",
|
|
659
|
+
draft: selectedRange.draft,
|
|
660
|
+
summary: selectedRange.summary,
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
type BtwMenuSelectorAction = { kind: "select"; value: string } | { kind: "back" } | { kind: "close" };
|
|
728
667
|
|
|
729
668
|
type BtwBringToMainPreviewAction = { kind: "bring" } | { kind: "back" } | { kind: "close" };
|
|
730
669
|
|
|
731
670
|
async function showBringToMainPreview(
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
671
|
+
ctx: ExtensionCommandContext,
|
|
672
|
+
draft: string,
|
|
673
|
+
summary: BtwBringToMainSummary,
|
|
735
674
|
): Promise<BtwBringToMainPreviewAction> {
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
675
|
+
const { defineMenu, runMenu } = await import("@narumitw/pi-tui-kit");
|
|
676
|
+
if (ctx.signal?.aborted) return { kind: "close" };
|
|
677
|
+
let confirmed = false;
|
|
678
|
+
const count = summary.messages === 1 ? "1 message" : `${summary.messages} messages`;
|
|
679
|
+
const lineCount = summary.lines === 1 ? "1 line" : `${summary.lines} lines`;
|
|
680
|
+
const menu = defineMenu<void, "preview", "bring", MenuContext>({
|
|
681
|
+
start: "preview",
|
|
682
|
+
screens: {
|
|
683
|
+
preview: () => ({
|
|
684
|
+
kind: "review",
|
|
685
|
+
title: `Preview · ${count} · ${lineCount} · ~${summary.tokens} tokens`,
|
|
686
|
+
content: draft,
|
|
687
|
+
viewportSize: "adaptive",
|
|
688
|
+
hint: "back",
|
|
689
|
+
confirm: { id: "bring", label: "Bring", action: "bring" },
|
|
690
|
+
}),
|
|
691
|
+
},
|
|
692
|
+
actions: {
|
|
693
|
+
bring: async () => {
|
|
694
|
+
confirmed = true;
|
|
695
|
+
return { kind: "close" } as const;
|
|
696
|
+
},
|
|
697
|
+
},
|
|
698
|
+
});
|
|
699
|
+
const result = await runBtwMenuPreservingEditor(ctx, (menuContext) =>
|
|
700
|
+
runMenu(menuContext, menu, { getState: () => undefined }),
|
|
701
|
+
);
|
|
702
|
+
if (confirmed && result.kind === "closed" && result.reason === "close") {
|
|
703
|
+
return { kind: "bring" };
|
|
704
|
+
}
|
|
705
|
+
return terminalBtwMenuAction(result);
|
|
767
706
|
}
|
|
768
707
|
|
|
769
708
|
async function showBtwMenu(
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
709
|
+
ctx: ExtensionCommandContext,
|
|
710
|
+
title: string,
|
|
711
|
+
options: readonly string[],
|
|
712
|
+
initialValue?: string,
|
|
774
713
|
): Promise<BtwMenuSelectorAction> {
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
? { kind: "select", value: selectedValue }
|
|
807
|
-
: terminalBtwMenuAction(result);
|
|
714
|
+
const { defineMenu, runMenu } = await import("@narumitw/pi-tui-kit");
|
|
715
|
+
if (ctx.signal?.aborted) return { kind: "close" };
|
|
716
|
+
const items = options.map((label, index) => ({ id: `option-${index}`, label }));
|
|
717
|
+
const initialIndex = initialValue === undefined ? -1 : options.indexOf(initialValue);
|
|
718
|
+
let selectedValue: string | undefined;
|
|
719
|
+
const menu = defineMenu<void, "choices", "select", MenuContext>({
|
|
720
|
+
start: "choices",
|
|
721
|
+
screens: {
|
|
722
|
+
choices: () => ({
|
|
723
|
+
kind: "choice",
|
|
724
|
+
title,
|
|
725
|
+
items,
|
|
726
|
+
action: "select",
|
|
727
|
+
initialItemId: initialIndex >= 0 ? `option-${initialIndex}` : undefined,
|
|
728
|
+
hint: "back",
|
|
729
|
+
}),
|
|
730
|
+
},
|
|
731
|
+
actions: {
|
|
732
|
+
select: async ({ itemId }: { itemId: string }) => {
|
|
733
|
+
const index = Number.parseInt(itemId.slice("option-".length), 10);
|
|
734
|
+
selectedValue = options[index];
|
|
735
|
+
return selectedValue === undefined ? ({ kind: "stay" } as const) : ({ kind: "close" } as const);
|
|
736
|
+
},
|
|
737
|
+
},
|
|
738
|
+
});
|
|
739
|
+
const result = await runBtwMenuPreservingEditor(ctx, (menuContext) =>
|
|
740
|
+
runMenu(menuContext, menu, { getState: () => undefined }),
|
|
741
|
+
);
|
|
742
|
+
return selectedValue !== undefined && result.kind === "closed" && result.reason === "close"
|
|
743
|
+
? { kind: "select", value: selectedValue }
|
|
744
|
+
: terminalBtwMenuAction(result);
|
|
808
745
|
}
|
|
809
746
|
|
|
810
747
|
function terminalBtwMenuAction(result: RunMenuResult): { kind: "back" } | { kind: "close" } {
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
748
|
+
if (result.kind === "closed") return { kind: result.reason };
|
|
749
|
+
if (result.kind === "error") throw result.error;
|
|
750
|
+
return { kind: "close" };
|
|
814
751
|
}
|
|
815
752
|
|
|
816
753
|
export async function loadBringToMainDraft(
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
754
|
+
draft: string,
|
|
755
|
+
ctx: ExtensionCommandContext,
|
|
756
|
+
summary: BtwBringToMainSummary,
|
|
820
757
|
): Promise<BtwBringToMainDelivery> {
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
ctx.ui.setEditorText(draft);
|
|
872
|
-
ctx.ui.notify(
|
|
873
|
-
`Replaced the main-editor draft with ${describeContent()}. Review and submit when ready.`,
|
|
874
|
-
"info",
|
|
875
|
-
);
|
|
876
|
-
return "loaded";
|
|
877
|
-
}
|
|
758
|
+
const describeContent = () =>
|
|
759
|
+
`${summary.messages} ${summary.messages === 1 ? "message" : "messages"} (~${summary.tokens} ${summary.tokens === 1 ? "token" : "tokens"})`;
|
|
760
|
+
const existing = ctx.ui.getEditorText();
|
|
761
|
+
if (!existing.trim()) {
|
|
762
|
+
ctx.ui.setEditorText(draft);
|
|
763
|
+
ctx.ui.notify(`Brought ${describeContent()} to the main editor. Review and submit when ready.`, "info");
|
|
764
|
+
return "loaded";
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
const appendOption = "Append after current draft Recommended";
|
|
768
|
+
const replaceOption = "⚠ Replace current draft Discards current editor text";
|
|
769
|
+
const cancelOption = "Cancel Return to the side thread";
|
|
770
|
+
while (true) {
|
|
771
|
+
const action = await showBtwMenu(ctx, "The main editor already has a draft", [
|
|
772
|
+
appendOption,
|
|
773
|
+
replaceOption,
|
|
774
|
+
cancelOption,
|
|
775
|
+
]);
|
|
776
|
+
if (action.kind === "close") return "closed";
|
|
777
|
+
if (action.kind === "back" || action.value === cancelOption) return "back";
|
|
778
|
+
if (action.value === appendOption) {
|
|
779
|
+
ctx.ui.setEditorText(`${ctx.ui.getEditorText()}\n\n${draft}`);
|
|
780
|
+
ctx.ui.notify(
|
|
781
|
+
`Appended ${describeContent()} to the existing main-editor draft. Review and submit when ready.`,
|
|
782
|
+
"info",
|
|
783
|
+
);
|
|
784
|
+
return "loaded";
|
|
785
|
+
}
|
|
786
|
+
if (action.value !== replaceOption) continue;
|
|
787
|
+
|
|
788
|
+
const current = ctx.ui.getEditorText();
|
|
789
|
+
const characters = [...current].length;
|
|
790
|
+
const confirmed = await showBtwMenu(ctx, `Replace the current ${characters}-character editor draft?`, [
|
|
791
|
+
"Back Keep current editor text",
|
|
792
|
+
"⚠ Replace current draft Cannot be undone",
|
|
793
|
+
]);
|
|
794
|
+
if (confirmed.kind === "close") return "closed";
|
|
795
|
+
if (confirmed.kind === "back" || confirmed.value === "Back Keep current editor text") continue;
|
|
796
|
+
if (confirmed.value !== "⚠ Replace current draft Cannot be undone") continue;
|
|
797
|
+
if (ctx.ui.getEditorText() !== current) {
|
|
798
|
+
ctx.ui.notify(
|
|
799
|
+
"The main editor changed during confirmation. Review the updated draft and choose again.",
|
|
800
|
+
"warning",
|
|
801
|
+
);
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
ctx.ui.setEditorText(draft);
|
|
805
|
+
ctx.ui.notify(`Replaced the main-editor draft with ${describeContent()}. Review and submit when ready.`, "info");
|
|
806
|
+
return "loaded";
|
|
807
|
+
}
|
|
878
808
|
}
|
|
879
809
|
|
|
880
810
|
function truncatePreview(text: string): string {
|
|
881
|
-
|
|
811
|
+
return text.length <= 72 ? text : `${text.slice(0, 69)}…`;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
async function prepareCurrentTranscriptMarkdown(
|
|
815
|
+
thread: SideThread,
|
|
816
|
+
pendingQuestion: string | undefined,
|
|
817
|
+
ctx: ExtensionCommandContext,
|
|
818
|
+
): Promise<BtwMarkdownTransformers | undefined> {
|
|
819
|
+
while (true) {
|
|
820
|
+
const turnCount = thread.turns.length;
|
|
821
|
+
const createMarkdownTransformers = ctx.signal
|
|
822
|
+
? await prepareBtwTranscriptMarkdown(thread.turns, pendingQuestion, ctx.signal)
|
|
823
|
+
: await prepareBtwTranscriptMarkdown(thread.turns, pendingQuestion);
|
|
824
|
+
if (!createMarkdownTransformers || ctx.signal?.aborted) return undefined;
|
|
825
|
+
if (thread.turns.length === turnCount) return createMarkdownTransformers;
|
|
826
|
+
}
|
|
882
827
|
}
|
|
883
828
|
|
|
884
829
|
async function askThreadQuestion(
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
830
|
+
thread: SideThread,
|
|
831
|
+
question: string,
|
|
832
|
+
selected: ResolvedBtwModel,
|
|
833
|
+
thinkingLevel: BtwThinkingLevel,
|
|
834
|
+
ctx: ExtensionCommandContext,
|
|
835
|
+
steering: BtwThreadSteeringControl,
|
|
891
836
|
) {
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
837
|
+
const createMarkdownTransformers = await prepareCurrentTranscriptMarkdown(thread, question, ctx);
|
|
838
|
+
if (!createMarkdownTransformers) return { kind: "aborted" as const };
|
|
839
|
+
return ctx.ui.custom<Awaited<ReturnType<typeof completeSideThreadTurn>>>((tui, theme, keybindings, done) => {
|
|
840
|
+
let settled = false;
|
|
841
|
+
const view = new BtwAnsweringView(
|
|
842
|
+
tui,
|
|
843
|
+
theme,
|
|
844
|
+
thread.turns,
|
|
845
|
+
question,
|
|
846
|
+
() => {
|
|
847
|
+
if (settled) return;
|
|
848
|
+
settled = true;
|
|
849
|
+
done({ kind: "aborted" });
|
|
850
|
+
},
|
|
851
|
+
thinkingLevel,
|
|
852
|
+
{
|
|
853
|
+
markdownTransformers: createMarkdownTransformers(theme),
|
|
854
|
+
steering: {
|
|
855
|
+
questions: steering.questions,
|
|
856
|
+
onSubmit: steering.submit,
|
|
857
|
+
thinking: { ...steering.thinking, keybindings },
|
|
858
|
+
},
|
|
859
|
+
},
|
|
860
|
+
);
|
|
861
|
+
completeSideThreadTurn({
|
|
862
|
+
thread,
|
|
863
|
+
question,
|
|
864
|
+
model: selected.model,
|
|
865
|
+
thinkingLevel,
|
|
866
|
+
auth: selected.auth,
|
|
867
|
+
signal: view.signal,
|
|
868
|
+
completeSimple: createModelRegistryCompleteSimple(ctx.modelRegistry),
|
|
869
|
+
sessionId: readBtwSessionId(ctx),
|
|
870
|
+
}).then((result) => {
|
|
871
|
+
if (settled) return;
|
|
872
|
+
settled = true;
|
|
873
|
+
view.finish();
|
|
874
|
+
done(result);
|
|
875
|
+
});
|
|
876
|
+
return view;
|
|
877
|
+
});
|
|
932
878
|
}
|
|
933
879
|
|
|
934
880
|
async function showThreadComposer(
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
881
|
+
thread: SideThread,
|
|
882
|
+
startAtBottom: boolean,
|
|
883
|
+
ctx: ExtensionCommandContext,
|
|
884
|
+
initialQuestion: string | undefined,
|
|
885
|
+
thinking: BtwThreadThinkingControl,
|
|
940
886
|
): Promise<TranscriptPagerAction> {
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
887
|
+
const createMarkdownTransformers = await prepareCurrentTranscriptMarkdown(thread, initialQuestion, ctx);
|
|
888
|
+
if (!createMarkdownTransformers) return { kind: "close" };
|
|
889
|
+
return ctx.ui.custom<TranscriptPagerAction>(
|
|
890
|
+
(tui, theme, keybindings, done) =>
|
|
891
|
+
new BtwTranscriptPager(tui, theme, thread.turns, done, {
|
|
892
|
+
startAtBottom,
|
|
893
|
+
initialQuestion,
|
|
894
|
+
markdownTransformers: createMarkdownTransformers(theme),
|
|
895
|
+
thinking: { ...thinking, keybindings },
|
|
896
|
+
}),
|
|
897
|
+
);
|
|
949
898
|
}
|
|
950
899
|
|
|
951
900
|
type MessageContentBlock = {
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
901
|
+
type?: string;
|
|
902
|
+
text?: string;
|
|
903
|
+
name?: string;
|
|
904
|
+
arguments?: unknown;
|
|
905
|
+
result?: unknown;
|
|
957
906
|
};
|
|
958
907
|
|
|
959
908
|
type SessionMessage = {
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
909
|
+
role?: string;
|
|
910
|
+
content?: unknown;
|
|
911
|
+
stopReason?: string;
|
|
963
912
|
};
|
|
964
913
|
|
|
965
914
|
type SessionEntry = {
|
|
966
|
-
|
|
967
|
-
|
|
915
|
+
type: string;
|
|
916
|
+
message?: SessionMessage;
|
|
968
917
|
};
|
|
969
918
|
|
|
970
919
|
export function buildConversationContext(entries: readonly SessionEntry[]) {
|
|
971
|
-
|
|
920
|
+
const sections: string[] = [];
|
|
972
921
|
|
|
973
|
-
|
|
974
|
-
|
|
922
|
+
for (const entry of entries) {
|
|
923
|
+
if (entry.type !== "message" || !entry.message?.role) continue;
|
|
975
924
|
|
|
976
|
-
|
|
977
|
-
|
|
925
|
+
const role = entry.message.role;
|
|
926
|
+
if (role !== "user" && role !== "assistant") continue;
|
|
978
927
|
|
|
979
|
-
|
|
980
|
-
|
|
928
|
+
const contentLines = extractContentLines(entry.message.content);
|
|
929
|
+
if (contentLines.length === 0) continue;
|
|
981
930
|
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
sections.push(`${label}${status}: ${contentLines.join("\n")}`);
|
|
988
|
-
}
|
|
931
|
+
const label = role === "user" ? "User" : "Assistant";
|
|
932
|
+
const status =
|
|
933
|
+
entry.message.stopReason && entry.message.stopReason !== "stop" ? ` (${entry.message.stopReason})` : "";
|
|
934
|
+
sections.push(`${label}${status}: ${contentLines.join("\n")}`);
|
|
935
|
+
}
|
|
989
936
|
|
|
990
|
-
|
|
937
|
+
return truncateFromStart(sections.join("\n\n"), MAX_CONTEXT_CHARS);
|
|
991
938
|
}
|
|
992
939
|
|
|
993
940
|
function extractContentLines(content: unknown): string[] {
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
941
|
+
if (typeof content === "string") return [content.trim()].filter(Boolean);
|
|
942
|
+
if (!Array.isArray(content)) return [];
|
|
943
|
+
|
|
944
|
+
const lines: string[] = [];
|
|
945
|
+
for (const part of content) {
|
|
946
|
+
if (!part || typeof part !== "object") continue;
|
|
947
|
+
const block = part as MessageContentBlock;
|
|
948
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
949
|
+
lines.push(block.text.trim());
|
|
950
|
+
} else if (block.type === "toolCall" && typeof block.name === "string") {
|
|
951
|
+
lines.push(`Tool call: ${block.name}(${formatJson(block.arguments)})`);
|
|
952
|
+
} else if (block.type === "toolResult" && typeof block.name === "string") {
|
|
953
|
+
lines.push(`Tool result from ${block.name}: ${formatJson(block.result)}`);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
return lines.filter(Boolean);
|
|
1010
957
|
}
|
|
1011
958
|
|
|
1012
959
|
function formatJson(value: unknown) {
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
960
|
+
if (value === undefined) return "";
|
|
961
|
+
try {
|
|
962
|
+
return JSON.stringify(value);
|
|
963
|
+
} catch {
|
|
964
|
+
return String(value);
|
|
965
|
+
}
|
|
1019
966
|
}
|
|
1020
967
|
|
|
1021
968
|
function truncateFromStart(text: string, maxChars: number) {
|
|
1022
|
-
|
|
1023
|
-
|
|
969
|
+
if (text.length <= maxChars) return text;
|
|
970
|
+
return `[Earlier context omitted; showing the last ${maxChars} characters.]\n${text.slice(-maxChars)}`;
|
|
1024
971
|
}
|