@narumitw/pi-btw 0.58.0 → 0.59.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 +2 -0
- package/dist/index.ts +296 -367
- package/dist/index.ts.map +3 -3
- package/package.json +52 -53
- package/src/bring-to-main.ts +494 -547
- package/src/btw.ts +813 -844
- 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 +416 -454
- package/src/settings.ts +203 -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,455 +1,436 @@
|
|
|
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
|
-
|
|
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;
|
|
171
165
|
}
|
|
172
166
|
|
|
173
167
|
function hasRequestAuth(auth: SideQuestionAuth): boolean {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
(auth.env && Object.keys(auth.env).length > 0),
|
|
178
|
-
);
|
|
168
|
+
return Boolean(
|
|
169
|
+
auth.apiKey || providerHeadersHaveValue(auth.headers) || (auth.env && Object.keys(auth.env).length > 0),
|
|
170
|
+
);
|
|
179
171
|
}
|
|
180
172
|
|
|
181
173
|
function providerHeadersHaveValue(headers: ProviderHeaders | undefined): boolean {
|
|
182
|
-
|
|
174
|
+
return headers !== undefined && Object.values(headers).some((value) => value !== null);
|
|
183
175
|
}
|
|
184
176
|
|
|
185
177
|
export async function loadBtwThinkingLevel(
|
|
186
|
-
|
|
187
|
-
|
|
178
|
+
currentThinkingLevel: BtwThinkingLevel,
|
|
179
|
+
options: LoadBtwThinkingLevelOptions = {},
|
|
188
180
|
): Promise<BtwThinkingLevel> {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
+
}
|
|
194
186
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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;
|
|
201
193
|
}
|
|
202
194
|
|
|
203
195
|
function formatError(error: unknown): string {
|
|
204
|
-
|
|
196
|
+
return error instanceof Error ? error.message : String(error);
|
|
205
197
|
}
|
|
206
198
|
|
|
207
199
|
function readBtwSessionId(ctx: ExtensionCommandContext): string | undefined {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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;
|
|
212
204
|
}
|
|
213
205
|
|
|
214
206
|
function notifySafely(
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
207
|
+
ctx: ExtensionCommandContext,
|
|
208
|
+
message: string,
|
|
209
|
+
level: Parameters<ExtensionCommandContext["ui"]["notify"]>[1],
|
|
218
210
|
): void {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
211
|
+
try {
|
|
212
|
+
ctx.ui.notify(sanitizeSingleLine(message), level);
|
|
213
|
+
} catch {
|
|
214
|
+
// Async command continuations may finish after their ExtensionContext is replaced.
|
|
215
|
+
}
|
|
224
216
|
}
|
|
225
217
|
|
|
226
218
|
// Keep this slightly-over-1,000-line command coordinator intact: its injectable menu,
|
|
227
219
|
// request, resume, and delivery flows share the same thread-state and test seams;
|
|
228
220
|
// settings, keybinding policy, terminal ownership, and rendering live in separate modules.
|
|
229
221
|
export interface BtwExtensionDependencies {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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;
|
|
240
232
|
}
|
|
241
233
|
|
|
242
234
|
export default function btw(pi: ExtensionAPI, dependencies: BtwExtensionDependencies = {}) {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
if (state?.title && state.thread.turns.length > 0) {
|
|
362
|
-
if (state.thread.turns.length > startingTurnCount) {
|
|
363
|
-
resumableThreads.delete(state.id);
|
|
364
|
-
}
|
|
365
|
-
resumableThreads.set(state.id, state);
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
},
|
|
369
|
-
});
|
|
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
|
+
});
|
|
370
353
|
}
|
|
371
354
|
|
|
372
355
|
async function showCommandMenuForBtw(
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
356
|
+
pi: ExtensionAPI,
|
|
357
|
+
ctx: ExtensionCommandContext,
|
|
358
|
+
resumeThreads: readonly BtwResumeThreadSummary[],
|
|
376
359
|
): Promise<BtwCommandMenuResult> {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
resumeThreads,
|
|
393
|
-
});
|
|
360
|
+
const currentModel = ctx.model;
|
|
361
|
+
const availableModels = ctx.modelRegistry.getAll();
|
|
362
|
+
const currentThinkingLevel = pi.getThinkingLevel();
|
|
363
|
+
const loaded = await readBtwSettings();
|
|
364
|
+
const settings = loaded.kind === "loaded" ? loaded.settings : {};
|
|
365
|
+
const configured = settings.model ? parseBtwModelReference(settings.model) : undefined;
|
|
366
|
+
const configuredModel = configured
|
|
367
|
+
? availableModels.find((model) => model.provider === configured.provider && model.id === configured.modelId)
|
|
368
|
+
: undefined;
|
|
369
|
+
const model = configuredModel ?? currentModel;
|
|
370
|
+
return showBtwCommandMenu(ctx, {
|
|
371
|
+
currentThinkingLevel,
|
|
372
|
+
availableThinkingLevels: model ? getSupportedThinkingLevels(model) : BTW_THINKING_LEVELS,
|
|
373
|
+
resumeThreads,
|
|
374
|
+
});
|
|
394
375
|
}
|
|
395
376
|
|
|
396
377
|
async function loadSettingsForCommand(ctx: ExtensionCommandContext): Promise<BtwSettings> {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
378
|
+
const settingsResult = await readBtwSettings();
|
|
379
|
+
if (settingsResult.kind === "loaded") return settingsResult.settings;
|
|
380
|
+
if (settingsResult.kind === "invalid") {
|
|
381
|
+
notifySafely(ctx, `pi-btw settings ignored: ${settingsResult.reason}`, "warning");
|
|
382
|
+
}
|
|
383
|
+
return {};
|
|
403
384
|
}
|
|
404
385
|
|
|
405
386
|
type ModelResolutionOutcome =
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
387
|
+
| { kind: "cancelled" }
|
|
388
|
+
| { kind: "unavailable" }
|
|
389
|
+
| { kind: "selected"; selected: ResolvedBtwModel };
|
|
409
390
|
|
|
410
391
|
async function resolveBtwModelWithLoader(
|
|
411
|
-
|
|
412
|
-
|
|
392
|
+
settings: BtwSettings,
|
|
393
|
+
ctx: ExtensionCommandContext,
|
|
413
394
|
): Promise<ModelResolutionOutcome> {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
395
|
+
return ctx.ui.custom<ModelResolutionOutcome>((tui, theme, _keybindings, done) => {
|
|
396
|
+
const loader = new BorderedLoader(tui, theme, "Resolving /btw model credentials...");
|
|
397
|
+
let settled = false;
|
|
398
|
+
loader.onAbort = () => {
|
|
399
|
+
if (settled) return;
|
|
400
|
+
settled = true;
|
|
401
|
+
done({ kind: "cancelled" });
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
resolveBtwModel({
|
|
405
|
+
settings,
|
|
406
|
+
currentModel: ctx.model,
|
|
407
|
+
modelRegistry: ctx.modelRegistry,
|
|
408
|
+
warn: (message) => {
|
|
409
|
+
if (!settled) notifySafely(ctx, message, "warning");
|
|
410
|
+
},
|
|
411
|
+
})
|
|
412
|
+
.then((selected) => {
|
|
413
|
+
if (settled) return;
|
|
414
|
+
settled = true;
|
|
415
|
+
done(selected ? { kind: "selected", selected } : { kind: "unavailable" });
|
|
416
|
+
})
|
|
417
|
+
.catch(() => {
|
|
418
|
+
if (settled) return;
|
|
419
|
+
settled = true;
|
|
420
|
+
done({ kind: "unavailable" });
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
return loader;
|
|
424
|
+
});
|
|
444
425
|
}
|
|
445
426
|
|
|
446
427
|
interface RunBtwThreadDependencies {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
428
|
+
ask?: typeof askThreadQuestion;
|
|
429
|
+
interact?: typeof showThreadComposer;
|
|
430
|
+
chooseBringToMain?: typeof chooseBringToMain;
|
|
431
|
+
deliverBringToMain?: typeof loadBringToMainDraft;
|
|
432
|
+
persistThinkingLevel?: (level: BtwThinkingLevel) => Promise<unknown>;
|
|
433
|
+
now?: () => number;
|
|
453
434
|
}
|
|
454
435
|
|
|
455
436
|
export type BtwThreadResult = { kind: "closed" };
|
|
@@ -457,557 +438,545 @@ export type BtwThreadResult = { kind: "closed" };
|
|
|
457
438
|
type BtwThreadThinkingControl = Omit<BtwThinkingControl, "keybindings">;
|
|
458
439
|
|
|
459
440
|
interface BtwThreadSteeringControl {
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
441
|
+
questions: readonly string[];
|
|
442
|
+
submit: (question: string) => void;
|
|
443
|
+
thinking: BtwThreadThinkingControl;
|
|
463
444
|
}
|
|
464
445
|
|
|
465
446
|
type BtwBringToMainChoice =
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
447
|
+
| BtwThreadResult
|
|
448
|
+
| {
|
|
449
|
+
kind: "bringToMain";
|
|
450
|
+
draft: string;
|
|
451
|
+
summary: BtwBringToMainSummary;
|
|
452
|
+
selectionState?: BtwTextRangeSelectorState;
|
|
453
|
+
}
|
|
454
|
+
| { kind: "back" };
|
|
474
455
|
|
|
475
456
|
type BtwBringToMainDelivery = "loaded" | "back" | "closed";
|
|
476
457
|
|
|
477
458
|
interface RunBtwThreadOptions {
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
459
|
+
initialQuestion?: string;
|
|
460
|
+
selected: ResolvedBtwModel;
|
|
461
|
+
thinkingLevel: BtwThinkingLevel;
|
|
462
|
+
rememberThinkingLevelChanges?: boolean;
|
|
463
|
+
settingsPath?: string;
|
|
464
|
+
state?: BtwThreadState;
|
|
465
|
+
ctx: ExtensionCommandContext;
|
|
466
|
+
dependencies?: RunBtwThreadDependencies;
|
|
486
467
|
}
|
|
487
468
|
|
|
488
469
|
export async function runBtwThread({
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
470
|
+
initialQuestion,
|
|
471
|
+
selected,
|
|
472
|
+
thinkingLevel,
|
|
473
|
+
rememberThinkingLevelChanges = false,
|
|
474
|
+
settingsPath,
|
|
475
|
+
state,
|
|
476
|
+
ctx,
|
|
477
|
+
dependencies = {},
|
|
497
478
|
}: RunBtwThreadOptions): Promise<BtwThreadResult> {
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
-
if (state) {
|
|
586
|
-
state.title ||= sanitizeSingleLine(pendingQuestion) || "Untitled side thread";
|
|
587
|
-
state.updatedAt = now();
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
pendingQuestion = steeringQuestions.shift();
|
|
591
|
-
}
|
|
592
|
-
} finally {
|
|
593
|
-
await Promise.allSettled([...pendingWrites]);
|
|
594
|
-
}
|
|
479
|
+
const ask = dependencies.ask ?? askThreadQuestion;
|
|
480
|
+
const interact = dependencies.interact ?? showThreadComposer;
|
|
481
|
+
const chooseBringToMainAction = dependencies.chooseBringToMain ?? chooseBringToMain;
|
|
482
|
+
const deliverBringToMainDraft = dependencies.deliverBringToMain ?? loadBringToMainDraft;
|
|
483
|
+
const persistThinkingLevel =
|
|
484
|
+
dependencies.persistThinkingLevel ??
|
|
485
|
+
((level: BtwThinkingLevel) => updateBtwSettings({ thinkingLevel: level }, { settingsPath }));
|
|
486
|
+
const now = dependencies.now ?? Date.now;
|
|
487
|
+
const thread = state?.thread ?? createSideThread(buildConversationContext(ctx.sessionManager.getBranch()));
|
|
488
|
+
const thinkingLevels = getSupportedThinkingLevels(selected.model);
|
|
489
|
+
const pendingWrites = new Set<Promise<void>>();
|
|
490
|
+
const steeringQuestions: string[] = [];
|
|
491
|
+
let activeThinkingLevel = clampThinkingLevel(selected.model, state?.thinkingLevel ?? thinkingLevel);
|
|
492
|
+
if (state) state.thinkingLevel = activeThinkingLevel;
|
|
493
|
+
let pendingQuestion = initialQuestion;
|
|
494
|
+
let composerDraft: string | undefined;
|
|
495
|
+
const createThinkingControl = (): BtwThreadThinkingControl => ({
|
|
496
|
+
level: activeThinkingLevel,
|
|
497
|
+
levels: thinkingLevels,
|
|
498
|
+
onChange: (level) => {
|
|
499
|
+
if (!thinkingLevels.includes(level)) return;
|
|
500
|
+
activeThinkingLevel = level;
|
|
501
|
+
if (state) state.thinkingLevel = level;
|
|
502
|
+
if (!rememberThinkingLevelChanges) return;
|
|
503
|
+
let write!: Promise<void>;
|
|
504
|
+
write = Promise.resolve()
|
|
505
|
+
.then(() => persistThinkingLevel(level))
|
|
506
|
+
.then(() => undefined)
|
|
507
|
+
.catch((error: unknown) => {
|
|
508
|
+
notifySafely(
|
|
509
|
+
ctx,
|
|
510
|
+
`Thinking level changed to ${level}, but could not be remembered in pi-btw.json: ${formatError(error)}`,
|
|
511
|
+
"warning",
|
|
512
|
+
);
|
|
513
|
+
})
|
|
514
|
+
.finally(() => pendingWrites.delete(write));
|
|
515
|
+
pendingWrites.add(write);
|
|
516
|
+
},
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
try {
|
|
520
|
+
while (true) {
|
|
521
|
+
if (!pendingQuestion) {
|
|
522
|
+
const action = await interact(thread, thread.turns.length > 0, ctx, composerDraft, createThinkingControl());
|
|
523
|
+
if (action.kind === "close") return { kind: "closed" };
|
|
524
|
+
if (action.kind === "bringToMain") {
|
|
525
|
+
const choice = await chooseBringToMainAction(thread, ctx);
|
|
526
|
+
if (choice.kind === "closed") return choice;
|
|
527
|
+
if (choice.kind === "back") {
|
|
528
|
+
composerDraft = action.questionDraft;
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
const delivery = await deliverBringToMainDraft(choice.draft, ctx, choice.summary);
|
|
532
|
+
if (delivery === "loaded" || delivery === "closed") return { kind: "closed" };
|
|
533
|
+
composerDraft = action.questionDraft;
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
composerDraft = undefined;
|
|
537
|
+
pendingQuestion = action.question;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const result = await ask(thread, pendingQuestion, selected, activeThinkingLevel, ctx, {
|
|
541
|
+
questions: steeringQuestions,
|
|
542
|
+
submit: (question) => steeringQuestions.push(question),
|
|
543
|
+
thinking: createThinkingControl(),
|
|
544
|
+
});
|
|
545
|
+
if (result.kind === "aborted") {
|
|
546
|
+
notifySafely(ctx, "Cancelled", "info");
|
|
547
|
+
return { kind: "closed" };
|
|
548
|
+
}
|
|
549
|
+
if (result.kind === "error") {
|
|
550
|
+
thread.turns.push({
|
|
551
|
+
kind: "error",
|
|
552
|
+
question: pendingQuestion,
|
|
553
|
+
answer: result.message,
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
if (state) {
|
|
557
|
+
state.title ||= sanitizeSingleLine(pendingQuestion) || "Untitled side thread";
|
|
558
|
+
state.updatedAt = now();
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
pendingQuestion = steeringQuestions.shift();
|
|
562
|
+
}
|
|
563
|
+
} finally {
|
|
564
|
+
await Promise.allSettled([...pendingWrites]);
|
|
565
|
+
}
|
|
595
566
|
}
|
|
596
567
|
|
|
597
568
|
interface ChooseBringToMainDependencies {
|
|
598
|
-
|
|
599
|
-
|
|
569
|
+
showMenu?: typeof showBtwMenu;
|
|
570
|
+
showPreview?: typeof showBringToMainPreview;
|
|
600
571
|
}
|
|
601
572
|
|
|
602
573
|
export async function chooseBringToMain(
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
574
|
+
thread: SideThread,
|
|
575
|
+
ctx: ExtensionCommandContext,
|
|
576
|
+
dependencies: ChooseBringToMainDependencies = {},
|
|
606
577
|
): Promise<BtwBringToMainChoice> {
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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
|
-
summary: selectedRange.summary,
|
|
708
|
-
};
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
type BtwMenuSelectorAction =
|
|
714
|
-
| { kind: "select"; value: string }
|
|
715
|
-
| { kind: "back" }
|
|
716
|
-
| { kind: "close" };
|
|
578
|
+
const answered = getAnsweredTurns(thread.turns);
|
|
579
|
+
if (answered.length === 0) return { kind: "back" };
|
|
580
|
+
const showMenu = dependencies.showMenu ?? showBtwMenu;
|
|
581
|
+
const showPreview = dependencies.showPreview ?? showBringToMainPreview;
|
|
582
|
+
const makeChoice = (segments: readonly BtwBringToMainSegment[]) => ({
|
|
583
|
+
kind: "bringToMain" as const,
|
|
584
|
+
draft: formatBtwBringToMain(segments),
|
|
585
|
+
summary: summarizeBringToMain(segments),
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
const latestSegments = buildQuickBringToMainSegments(thread.turns, { kind: "latest" });
|
|
589
|
+
const entireSegments = buildQuickBringToMainSegments(thread.turns, { kind: "entire" });
|
|
590
|
+
const latestOption = `Latest question and answer 1 Q&A · ~${estimateBringToMainTokens(latestSegments)} tokens`;
|
|
591
|
+
const fromOption = "From a question onward… Choose a starting question";
|
|
592
|
+
const exactOption = "Select exact text… Lines or characters";
|
|
593
|
+
const entireOption = `Entire side thread ${answered.length} Q&A · ~${estimateBringToMainTokens(entireSegments)} tokens`;
|
|
594
|
+
const cancelOption = "Cancel Return to the side thread";
|
|
595
|
+
let selectedScope: string | undefined;
|
|
596
|
+
|
|
597
|
+
while (true) {
|
|
598
|
+
const scopeResult = await showMenu(
|
|
599
|
+
ctx,
|
|
600
|
+
"Bring what back to the main thread?",
|
|
601
|
+
[latestOption, fromOption, exactOption, entireOption, cancelOption],
|
|
602
|
+
selectedScope,
|
|
603
|
+
);
|
|
604
|
+
if (scopeResult.kind === "close") return { kind: "closed" };
|
|
605
|
+
if (scopeResult.kind === "back" || scopeResult.value === cancelOption) return { kind: "back" };
|
|
606
|
+
const scope = scopeResult.value;
|
|
607
|
+
selectedScope = scope;
|
|
608
|
+
if (scope === latestOption) return makeChoice(latestSegments);
|
|
609
|
+
if (scope === entireOption) {
|
|
610
|
+
const choice = makeChoice(entireSegments);
|
|
611
|
+
const preview = await showPreview(ctx, choice.draft, choice.summary);
|
|
612
|
+
if (preview.kind === "close") return { kind: "closed" };
|
|
613
|
+
if (preview.kind === "back") continue;
|
|
614
|
+
return choice;
|
|
615
|
+
}
|
|
616
|
+
if (scope === fromOption) {
|
|
617
|
+
const questions = answered.map(
|
|
618
|
+
(turn, index) => `${index + 1}. ${truncatePreview(sanitizeSingleLine(turn.question))}`,
|
|
619
|
+
);
|
|
620
|
+
let selectedQuestion: string | undefined;
|
|
621
|
+
while (true) {
|
|
622
|
+
const questionResult = await showMenu(ctx, "Start from which question?", questions, selectedQuestion);
|
|
623
|
+
if (questionResult.kind === "close") return { kind: "closed" };
|
|
624
|
+
if (questionResult.kind === "back") break;
|
|
625
|
+
const answeredTurnIndex = questions.indexOf(questionResult.value);
|
|
626
|
+
if (answeredTurnIndex < 0) continue;
|
|
627
|
+
selectedQuestion = questionResult.value;
|
|
628
|
+
const choice = makeChoice(buildQuickBringToMainSegments(thread.turns, { kind: "from", answeredTurnIndex }));
|
|
629
|
+
const preview = await showPreview(ctx, choice.draft, choice.summary);
|
|
630
|
+
if (preview.kind === "close") return { kind: "closed" };
|
|
631
|
+
if (preview.kind === "back") continue;
|
|
632
|
+
return choice;
|
|
633
|
+
}
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (scope !== exactOption) continue;
|
|
638
|
+
let selectionState: BtwTextRangeSelectorState | undefined;
|
|
639
|
+
while (true) {
|
|
640
|
+
const selectedRange = await showBtwCustomPreservingEditor<BtwBringToMainChoice>(
|
|
641
|
+
ctx,
|
|
642
|
+
(tui, theme, keybindings, done) => {
|
|
643
|
+
let selector: BtwTextRangeSelector;
|
|
644
|
+
selector = new BtwTextRangeSelector(
|
|
645
|
+
tui,
|
|
646
|
+
theme,
|
|
647
|
+
keybindings,
|
|
648
|
+
thread.turns,
|
|
649
|
+
(action) => {
|
|
650
|
+
if (action.kind === "back") done({ kind: "back" });
|
|
651
|
+
else if (action.kind === "close") done({ kind: "closed" });
|
|
652
|
+
else done({ ...makeChoice(action.segments), selectionState: selector.getState() });
|
|
653
|
+
},
|
|
654
|
+
selectionState,
|
|
655
|
+
);
|
|
656
|
+
return selector;
|
|
657
|
+
},
|
|
658
|
+
);
|
|
659
|
+
if (!selectedRange) return { kind: "closed" };
|
|
660
|
+
if (selectedRange.kind === "closed") return selectedRange;
|
|
661
|
+
if (selectedRange.kind === "back") break;
|
|
662
|
+
const preview = await showPreview(ctx, selectedRange.draft, selectedRange.summary);
|
|
663
|
+
if (preview.kind === "close") return { kind: "closed" };
|
|
664
|
+
if (preview.kind === "back") {
|
|
665
|
+
selectionState = selectedRange.selectionState;
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
return {
|
|
669
|
+
kind: "bringToMain",
|
|
670
|
+
draft: selectedRange.draft,
|
|
671
|
+
summary: selectedRange.summary,
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
type BtwMenuSelectorAction = { kind: "select"; value: string } | { kind: "back" } | { kind: "close" };
|
|
717
678
|
|
|
718
679
|
type BtwBringToMainPreviewAction = { kind: "bring" } | { kind: "back" } | { kind: "close" };
|
|
719
680
|
|
|
720
681
|
async function showBringToMainPreview(
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
682
|
+
ctx: ExtensionCommandContext,
|
|
683
|
+
draft: string,
|
|
684
|
+
summary: BtwBringToMainSummary,
|
|
724
685
|
): Promise<BtwBringToMainPreviewAction> {
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
686
|
+
const { defineMenu, runMenu } = await import("@narumitw/pi-tui-kit");
|
|
687
|
+
if (ctx.signal?.aborted) return { kind: "close" };
|
|
688
|
+
let confirmed = false;
|
|
689
|
+
const count = summary.messages === 1 ? "1 message" : `${summary.messages} messages`;
|
|
690
|
+
const lineCount = summary.lines === 1 ? "1 line" : `${summary.lines} lines`;
|
|
691
|
+
const menu = defineMenu<void, "preview", "bring", MenuContext>({
|
|
692
|
+
start: "preview",
|
|
693
|
+
screens: {
|
|
694
|
+
preview: () => ({
|
|
695
|
+
kind: "review",
|
|
696
|
+
title: `Preview · ${count} · ${lineCount} · ~${summary.tokens} tokens`,
|
|
697
|
+
content: draft,
|
|
698
|
+
viewportSize: "adaptive",
|
|
699
|
+
hint: "back",
|
|
700
|
+
confirm: { id: "bring", label: "Bring", action: "bring" },
|
|
701
|
+
}),
|
|
702
|
+
},
|
|
703
|
+
actions: {
|
|
704
|
+
bring: async () => {
|
|
705
|
+
confirmed = true;
|
|
706
|
+
return { kind: "close" } as const;
|
|
707
|
+
},
|
|
708
|
+
},
|
|
709
|
+
});
|
|
710
|
+
const result = await runBtwMenuPreservingEditor(ctx, (menuContext) =>
|
|
711
|
+
runMenu(menuContext, menu, { getState: () => undefined }),
|
|
712
|
+
);
|
|
713
|
+
if (confirmed && result.kind === "closed" && result.reason === "close") {
|
|
714
|
+
return { kind: "bring" };
|
|
715
|
+
}
|
|
716
|
+
return terminalBtwMenuAction(result);
|
|
756
717
|
}
|
|
757
718
|
|
|
758
719
|
async function showBtwMenu(
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
720
|
+
ctx: ExtensionCommandContext,
|
|
721
|
+
title: string,
|
|
722
|
+
options: readonly string[],
|
|
723
|
+
initialValue?: string,
|
|
763
724
|
): Promise<BtwMenuSelectorAction> {
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
? { kind: "select", value: selectedValue }
|
|
796
|
-
: terminalBtwMenuAction(result);
|
|
725
|
+
const { defineMenu, runMenu } = await import("@narumitw/pi-tui-kit");
|
|
726
|
+
if (ctx.signal?.aborted) return { kind: "close" };
|
|
727
|
+
const items = options.map((label, index) => ({ id: `option-${index}`, label }));
|
|
728
|
+
const initialIndex = initialValue === undefined ? -1 : options.indexOf(initialValue);
|
|
729
|
+
let selectedValue: string | undefined;
|
|
730
|
+
const menu = defineMenu<void, "choices", "select", MenuContext>({
|
|
731
|
+
start: "choices",
|
|
732
|
+
screens: {
|
|
733
|
+
choices: () => ({
|
|
734
|
+
kind: "choice",
|
|
735
|
+
title,
|
|
736
|
+
items,
|
|
737
|
+
action: "select",
|
|
738
|
+
initialItemId: initialIndex >= 0 ? `option-${initialIndex}` : undefined,
|
|
739
|
+
hint: "back",
|
|
740
|
+
}),
|
|
741
|
+
},
|
|
742
|
+
actions: {
|
|
743
|
+
select: async ({ itemId }: { itemId: string }) => {
|
|
744
|
+
const index = Number.parseInt(itemId.slice("option-".length), 10);
|
|
745
|
+
selectedValue = options[index];
|
|
746
|
+
return selectedValue === undefined ? ({ kind: "stay" } as const) : ({ kind: "close" } as const);
|
|
747
|
+
},
|
|
748
|
+
},
|
|
749
|
+
});
|
|
750
|
+
const result = await runBtwMenuPreservingEditor(ctx, (menuContext) =>
|
|
751
|
+
runMenu(menuContext, menu, { getState: () => undefined }),
|
|
752
|
+
);
|
|
753
|
+
return selectedValue !== undefined && result.kind === "closed" && result.reason === "close"
|
|
754
|
+
? { kind: "select", value: selectedValue }
|
|
755
|
+
: terminalBtwMenuAction(result);
|
|
797
756
|
}
|
|
798
757
|
|
|
799
758
|
function terminalBtwMenuAction(result: RunMenuResult): { kind: "back" } | { kind: "close" } {
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
759
|
+
if (result.kind === "closed") return { kind: result.reason };
|
|
760
|
+
if (result.kind === "error") throw result.error;
|
|
761
|
+
return { kind: "close" };
|
|
803
762
|
}
|
|
804
763
|
|
|
805
764
|
export async function loadBringToMainDraft(
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
765
|
+
draft: string,
|
|
766
|
+
ctx: ExtensionCommandContext,
|
|
767
|
+
summary: BtwBringToMainSummary,
|
|
809
768
|
): Promise<BtwBringToMainDelivery> {
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
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
|
-
ctx.ui.setEditorText(draft);
|
|
861
|
-
ctx.ui.notify(
|
|
862
|
-
`Replaced the main-editor draft with ${describeContent()}. Review and submit when ready.`,
|
|
863
|
-
"info",
|
|
864
|
-
);
|
|
865
|
-
return "loaded";
|
|
866
|
-
}
|
|
769
|
+
const describeContent = () =>
|
|
770
|
+
`${summary.messages} ${summary.messages === 1 ? "message" : "messages"} (~${summary.tokens} ${summary.tokens === 1 ? "token" : "tokens"})`;
|
|
771
|
+
const existing = ctx.ui.getEditorText();
|
|
772
|
+
if (!existing.trim()) {
|
|
773
|
+
ctx.ui.setEditorText(draft);
|
|
774
|
+
ctx.ui.notify(`Brought ${describeContent()} to the main editor. Review and submit when ready.`, "info");
|
|
775
|
+
return "loaded";
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const appendOption = "Append after current draft Recommended";
|
|
779
|
+
const replaceOption = "⚠ Replace current draft Discards current editor text";
|
|
780
|
+
const cancelOption = "Cancel Return to the side thread";
|
|
781
|
+
while (true) {
|
|
782
|
+
const action = await showBtwMenu(ctx, "The main editor already has a draft", [
|
|
783
|
+
appendOption,
|
|
784
|
+
replaceOption,
|
|
785
|
+
cancelOption,
|
|
786
|
+
]);
|
|
787
|
+
if (action.kind === "close") return "closed";
|
|
788
|
+
if (action.kind === "back" || action.value === cancelOption) return "back";
|
|
789
|
+
if (action.value === appendOption) {
|
|
790
|
+
ctx.ui.setEditorText(`${ctx.ui.getEditorText()}\n\n${draft}`);
|
|
791
|
+
ctx.ui.notify(
|
|
792
|
+
`Appended ${describeContent()} to the existing main-editor draft. Review and submit when ready.`,
|
|
793
|
+
"info",
|
|
794
|
+
);
|
|
795
|
+
return "loaded";
|
|
796
|
+
}
|
|
797
|
+
if (action.value !== replaceOption) continue;
|
|
798
|
+
|
|
799
|
+
const current = ctx.ui.getEditorText();
|
|
800
|
+
const characters = [...current].length;
|
|
801
|
+
const confirmed = await showBtwMenu(ctx, `Replace the current ${characters}-character editor draft?`, [
|
|
802
|
+
"Back Keep current editor text",
|
|
803
|
+
"⚠ Replace current draft Cannot be undone",
|
|
804
|
+
]);
|
|
805
|
+
if (confirmed.kind === "close") return "closed";
|
|
806
|
+
if (confirmed.kind === "back" || confirmed.value === "Back Keep current editor text") continue;
|
|
807
|
+
if (confirmed.value !== "⚠ Replace current draft Cannot be undone") continue;
|
|
808
|
+
if (ctx.ui.getEditorText() !== current) {
|
|
809
|
+
ctx.ui.notify(
|
|
810
|
+
"The main editor changed during confirmation. Review the updated draft and choose again.",
|
|
811
|
+
"warning",
|
|
812
|
+
);
|
|
813
|
+
continue;
|
|
814
|
+
}
|
|
815
|
+
ctx.ui.setEditorText(draft);
|
|
816
|
+
ctx.ui.notify(`Replaced the main-editor draft with ${describeContent()}. Review and submit when ready.`, "info");
|
|
817
|
+
return "loaded";
|
|
818
|
+
}
|
|
867
819
|
}
|
|
868
820
|
|
|
869
821
|
function truncatePreview(text: string): string {
|
|
870
|
-
|
|
822
|
+
return text.length <= 72 ? text : `${text.slice(0, 69)}…`;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
async function prepareCurrentTranscriptMarkdown(
|
|
826
|
+
thread: SideThread,
|
|
827
|
+
pendingQuestion: string | undefined,
|
|
828
|
+
ctx: ExtensionCommandContext,
|
|
829
|
+
): Promise<BtwMarkdownTransformers | undefined> {
|
|
830
|
+
while (true) {
|
|
831
|
+
const turnCount = thread.turns.length;
|
|
832
|
+
const createMarkdownTransformers = ctx.signal
|
|
833
|
+
? await prepareBtwTranscriptMarkdown(thread.turns, pendingQuestion, ctx.signal)
|
|
834
|
+
: await prepareBtwTranscriptMarkdown(thread.turns, pendingQuestion);
|
|
835
|
+
if (!createMarkdownTransformers || ctx.signal?.aborted) return undefined;
|
|
836
|
+
if (thread.turns.length === turnCount) return createMarkdownTransformers;
|
|
837
|
+
}
|
|
871
838
|
}
|
|
872
839
|
|
|
873
840
|
async function askThreadQuestion(
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
841
|
+
thread: SideThread,
|
|
842
|
+
question: string,
|
|
843
|
+
selected: ResolvedBtwModel,
|
|
844
|
+
thinkingLevel: BtwThinkingLevel,
|
|
845
|
+
ctx: ExtensionCommandContext,
|
|
846
|
+
steering: BtwThreadSteeringControl,
|
|
880
847
|
) {
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
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
|
-
|
|
848
|
+
const createMarkdownTransformers = await prepareCurrentTranscriptMarkdown(thread, question, ctx);
|
|
849
|
+
if (!createMarkdownTransformers) return { kind: "aborted" as const };
|
|
850
|
+
return ctx.ui.custom<Awaited<ReturnType<typeof completeSideThreadTurn>>>((tui, theme, keybindings, done) => {
|
|
851
|
+
let settled = false;
|
|
852
|
+
const view = new BtwAnsweringView(
|
|
853
|
+
tui,
|
|
854
|
+
theme,
|
|
855
|
+
thread.turns,
|
|
856
|
+
question,
|
|
857
|
+
() => {
|
|
858
|
+
if (settled) return;
|
|
859
|
+
settled = true;
|
|
860
|
+
done({ kind: "aborted" });
|
|
861
|
+
},
|
|
862
|
+
thinkingLevel,
|
|
863
|
+
{
|
|
864
|
+
markdownTransformers: createMarkdownTransformers(theme),
|
|
865
|
+
steering: {
|
|
866
|
+
questions: steering.questions,
|
|
867
|
+
onSubmit: steering.submit,
|
|
868
|
+
thinking: { ...steering.thinking, keybindings },
|
|
869
|
+
},
|
|
870
|
+
},
|
|
871
|
+
);
|
|
872
|
+
completeSideThreadTurn({
|
|
873
|
+
thread,
|
|
874
|
+
question,
|
|
875
|
+
model: selected.model,
|
|
876
|
+
thinkingLevel,
|
|
877
|
+
auth: selected.auth,
|
|
878
|
+
signal: view.signal,
|
|
879
|
+
completeSimple: createModelRegistryCompleteSimple(ctx.modelRegistry),
|
|
880
|
+
sessionId: readBtwSessionId(ctx),
|
|
881
|
+
}).then((result) => {
|
|
882
|
+
if (settled) return;
|
|
883
|
+
settled = true;
|
|
884
|
+
view.finish();
|
|
885
|
+
done(result);
|
|
886
|
+
});
|
|
887
|
+
return view;
|
|
888
|
+
});
|
|
921
889
|
}
|
|
922
890
|
|
|
923
891
|
async function showThreadComposer(
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
892
|
+
thread: SideThread,
|
|
893
|
+
startAtBottom: boolean,
|
|
894
|
+
ctx: ExtensionCommandContext,
|
|
895
|
+
initialQuestion: string | undefined,
|
|
896
|
+
thinking: BtwThreadThinkingControl,
|
|
929
897
|
): Promise<TranscriptPagerAction> {
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
898
|
+
const createMarkdownTransformers = await prepareCurrentTranscriptMarkdown(thread, initialQuestion, ctx);
|
|
899
|
+
if (!createMarkdownTransformers) return { kind: "close" };
|
|
900
|
+
return ctx.ui.custom<TranscriptPagerAction>(
|
|
901
|
+
(tui, theme, keybindings, done) =>
|
|
902
|
+
new BtwTranscriptPager(tui, theme, thread.turns, done, {
|
|
903
|
+
startAtBottom,
|
|
904
|
+
initialQuestion,
|
|
905
|
+
markdownTransformers: createMarkdownTransformers(theme),
|
|
906
|
+
thinking: { ...thinking, keybindings },
|
|
907
|
+
}),
|
|
908
|
+
);
|
|
938
909
|
}
|
|
939
910
|
|
|
940
911
|
type MessageContentBlock = {
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
912
|
+
type?: string;
|
|
913
|
+
text?: string;
|
|
914
|
+
name?: string;
|
|
915
|
+
arguments?: unknown;
|
|
916
|
+
result?: unknown;
|
|
946
917
|
};
|
|
947
918
|
|
|
948
919
|
type SessionMessage = {
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
920
|
+
role?: string;
|
|
921
|
+
content?: unknown;
|
|
922
|
+
stopReason?: string;
|
|
952
923
|
};
|
|
953
924
|
|
|
954
925
|
type SessionEntry = {
|
|
955
|
-
|
|
956
|
-
|
|
926
|
+
type: string;
|
|
927
|
+
message?: SessionMessage;
|
|
957
928
|
};
|
|
958
929
|
|
|
959
930
|
export function buildConversationContext(entries: readonly SessionEntry[]) {
|
|
960
|
-
|
|
931
|
+
const sections: string[] = [];
|
|
961
932
|
|
|
962
|
-
|
|
963
|
-
|
|
933
|
+
for (const entry of entries) {
|
|
934
|
+
if (entry.type !== "message" || !entry.message?.role) continue;
|
|
964
935
|
|
|
965
|
-
|
|
966
|
-
|
|
936
|
+
const role = entry.message.role;
|
|
937
|
+
if (role !== "user" && role !== "assistant") continue;
|
|
967
938
|
|
|
968
|
-
|
|
969
|
-
|
|
939
|
+
const contentLines = extractContentLines(entry.message.content);
|
|
940
|
+
if (contentLines.length === 0) continue;
|
|
970
941
|
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
sections.push(`${label}${status}: ${contentLines.join("\n")}`);
|
|
977
|
-
}
|
|
942
|
+
const label = role === "user" ? "User" : "Assistant";
|
|
943
|
+
const status =
|
|
944
|
+
entry.message.stopReason && entry.message.stopReason !== "stop" ? ` (${entry.message.stopReason})` : "";
|
|
945
|
+
sections.push(`${label}${status}: ${contentLines.join("\n")}`);
|
|
946
|
+
}
|
|
978
947
|
|
|
979
|
-
|
|
948
|
+
return truncateFromStart(sections.join("\n\n"), MAX_CONTEXT_CHARS);
|
|
980
949
|
}
|
|
981
950
|
|
|
982
951
|
function extractContentLines(content: unknown): string[] {
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
952
|
+
if (typeof content === "string") return [content.trim()].filter(Boolean);
|
|
953
|
+
if (!Array.isArray(content)) return [];
|
|
954
|
+
|
|
955
|
+
const lines: string[] = [];
|
|
956
|
+
for (const part of content) {
|
|
957
|
+
if (!part || typeof part !== "object") continue;
|
|
958
|
+
const block = part as MessageContentBlock;
|
|
959
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
960
|
+
lines.push(block.text.trim());
|
|
961
|
+
} else if (block.type === "toolCall" && typeof block.name === "string") {
|
|
962
|
+
lines.push(`Tool call: ${block.name}(${formatJson(block.arguments)})`);
|
|
963
|
+
} else if (block.type === "toolResult" && typeof block.name === "string") {
|
|
964
|
+
lines.push(`Tool result from ${block.name}: ${formatJson(block.result)}`);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
return lines.filter(Boolean);
|
|
999
968
|
}
|
|
1000
969
|
|
|
1001
970
|
function formatJson(value: unknown) {
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
971
|
+
if (value === undefined) return "";
|
|
972
|
+
try {
|
|
973
|
+
return JSON.stringify(value);
|
|
974
|
+
} catch {
|
|
975
|
+
return String(value);
|
|
976
|
+
}
|
|
1008
977
|
}
|
|
1009
978
|
|
|
1010
979
|
function truncateFromStart(text: string, maxChars: number) {
|
|
1011
|
-
|
|
1012
|
-
|
|
980
|
+
if (text.length <= maxChars) return text;
|
|
981
|
+
return `[Earlier context omitted; showing the last ${maxChars} characters.]\n${text.slice(-maxChars)}`;
|
|
1013
982
|
}
|