pi-agent-toolkit 0.1.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/dist/dotfiles/AGENTS.md +197 -0
- package/dist/dotfiles/APPEND_SYSTEM.md +78 -0
- package/dist/dotfiles/agent-modes.json +12 -0
- package/dist/dotfiles/agent-skills/exa-search/.env.example +4 -0
- package/dist/dotfiles/agent-skills/exa-search/SKILL.md +234 -0
- package/dist/dotfiles/agent-skills/exa-search/scripts/exa-api.cjs +197 -0
- package/dist/dotfiles/auth.json.template +5 -0
- package/dist/dotfiles/damage-control-rules.yaml +318 -0
- package/dist/dotfiles/extensions/btw.ts +1031 -0
- package/dist/dotfiles/extensions/commit-approval.ts +590 -0
- package/dist/dotfiles/extensions/context.ts +578 -0
- package/dist/dotfiles/extensions/control.ts +1748 -0
- package/dist/dotfiles/extensions/damage-control/index.ts +543 -0
- package/dist/dotfiles/extensions/damage-control/node_modules/.package-lock.json +22 -0
- package/dist/dotfiles/extensions/damage-control/package-lock.json +28 -0
- package/dist/dotfiles/extensions/damage-control/package.json +7 -0
- package/dist/dotfiles/extensions/dirty-repo-guard.ts +56 -0
- package/dist/dotfiles/extensions/exa-enforce.ts +51 -0
- package/dist/dotfiles/extensions/exa-search-tool.ts +384 -0
- package/dist/dotfiles/extensions/execute-command/index.ts +82 -0
- package/dist/dotfiles/extensions/files.ts +1112 -0
- package/dist/dotfiles/extensions/loop.ts +446 -0
- package/dist/dotfiles/extensions/pr-approval.ts +730 -0
- package/dist/dotfiles/extensions/qna-interactive.ts +532 -0
- package/dist/dotfiles/extensions/question-mode.ts +242 -0
- package/dist/dotfiles/extensions/require-session-name-on-exit.ts +141 -0
- package/dist/dotfiles/extensions/review.ts +2091 -0
- package/dist/dotfiles/extensions/session-breakdown.ts +1629 -0
- package/dist/dotfiles/extensions/term-notify.ts +150 -0
- package/dist/dotfiles/extensions/tilldone.ts +527 -0
- package/dist/dotfiles/extensions/todos.ts +2082 -0
- package/dist/dotfiles/extensions/tools.ts +146 -0
- package/dist/dotfiles/extensions/uv.ts +123 -0
- package/dist/dotfiles/global-skills/brainstorm/SKILL.md +10 -0
- package/dist/dotfiles/global-skills/cli-detector/SKILL.md +192 -0
- package/dist/dotfiles/global-skills/gh-issue-creator/SKILL.md +173 -0
- package/dist/dotfiles/global-skills/google-chat-cards-v2/SKILL.md +237 -0
- package/dist/dotfiles/global-skills/google-chat-cards-v2/references/bridge_tap_implementation.md +466 -0
- package/dist/dotfiles/global-skills/technical-docs/SKILL.md +204 -0
- package/dist/dotfiles/global-skills/technical-docs/references/diagrams.md +168 -0
- package/dist/dotfiles/global-skills/technical-docs/references/examples.md +449 -0
- package/dist/dotfiles/global-skills/technical-docs/scripts/validate_docs.py +352 -0
- package/dist/dotfiles/global-skills/whats-new/SKILL.md +159 -0
- package/dist/dotfiles/intercepted-commands/pip +7 -0
- package/dist/dotfiles/intercepted-commands/pip3 +7 -0
- package/dist/dotfiles/intercepted-commands/poetry +10 -0
- package/dist/dotfiles/intercepted-commands/python +104 -0
- package/dist/dotfiles/intercepted-commands/python3 +104 -0
- package/dist/dotfiles/mcp.json.template +32 -0
- package/dist/dotfiles/models.json +27 -0
- package/dist/dotfiles/settings.json +25 -0
- package/dist/index.js +1344 -0
- package/package.json +34 -0
|
@@ -0,0 +1,1031 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildSessionContext,
|
|
3
|
+
codingTools,
|
|
4
|
+
createAgentSession,
|
|
5
|
+
createExtensionRuntime,
|
|
6
|
+
getMarkdownTheme,
|
|
7
|
+
SessionManager,
|
|
8
|
+
type AgentSession,
|
|
9
|
+
type AgentSessionEvent,
|
|
10
|
+
type ExtensionAPI,
|
|
11
|
+
type ExtensionCommandContext,
|
|
12
|
+
type ExtensionContext,
|
|
13
|
+
type ResourceLoader,
|
|
14
|
+
} from "@mariozechner/pi-coding-agent";
|
|
15
|
+
import { type AssistantMessage, type Message, type ThinkingLevel as AiThinkingLevel } from "@mariozechner/pi-ai";
|
|
16
|
+
import {
|
|
17
|
+
Container,
|
|
18
|
+
Input,
|
|
19
|
+
Markdown,
|
|
20
|
+
matchesKey,
|
|
21
|
+
truncateToWidth,
|
|
22
|
+
visibleWidth,
|
|
23
|
+
type Focusable,
|
|
24
|
+
type KeybindingsManager,
|
|
25
|
+
type OverlayHandle,
|
|
26
|
+
type TUI,
|
|
27
|
+
} from "@mariozechner/pi-tui";
|
|
28
|
+
|
|
29
|
+
const BTW_ENTRY_TYPE = "btw-thread-entry";
|
|
30
|
+
const BTW_RESET_TYPE = "btw-thread-reset";
|
|
31
|
+
|
|
32
|
+
const BTW_SYSTEM_PROMPT = [
|
|
33
|
+
"You are BTW, a side-channel assistant embedded in the user's coding agent.",
|
|
34
|
+
"You have access to the main conversation context — use it to give informed answers.",
|
|
35
|
+
"Help with focused questions, planning, and quick explorations.",
|
|
36
|
+
"Be direct and practical.",
|
|
37
|
+
].join(" ");
|
|
38
|
+
|
|
39
|
+
const BTW_SUMMARY_PROMPT =
|
|
40
|
+
"Summarize this side conversation for handoff into the main conversation. Keep key decisions, findings, risks, and next actions. Output only the summary.";
|
|
41
|
+
|
|
42
|
+
type SessionThinkingLevel = "off" | AiThinkingLevel;
|
|
43
|
+
|
|
44
|
+
type BtwDetails = {
|
|
45
|
+
question: string;
|
|
46
|
+
answer: string;
|
|
47
|
+
timestamp: number;
|
|
48
|
+
provider: string;
|
|
49
|
+
model: string;
|
|
50
|
+
thinkingLevel: SessionThinkingLevel;
|
|
51
|
+
usage?: AssistantMessage["usage"];
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
type BtwResetDetails = {
|
|
55
|
+
timestamp: number;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
type OverlayRuntime = {
|
|
59
|
+
handle?: OverlayHandle;
|
|
60
|
+
refresh?: () => void;
|
|
61
|
+
close?: () => void;
|
|
62
|
+
finish?: () => void;
|
|
63
|
+
setDraft?: (value: string) => void;
|
|
64
|
+
snapToBottom?: () => void;
|
|
65
|
+
closed?: boolean;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
type SideSessionRuntime = {
|
|
69
|
+
session: AgentSession;
|
|
70
|
+
modelKey: string;
|
|
71
|
+
unsubscribe: () => void;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
type ToolCallInfo = {
|
|
75
|
+
toolCallId: string;
|
|
76
|
+
toolName: string;
|
|
77
|
+
args: string;
|
|
78
|
+
status: "running" | "done" | "error";
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
function stripDynamicSystemPromptFooter(systemPrompt: string): string {
|
|
82
|
+
return systemPrompt
|
|
83
|
+
.replace(/\nCurrent date and time:[^\n]*(?:\nCurrent working directory:[^\n]*)?$/u, "")
|
|
84
|
+
.replace(/\nCurrent working directory:[^\n]*$/u, "")
|
|
85
|
+
.trim();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function createBtwResourceLoader(ctx: ExtensionContext, appendSystemPrompt: string[] = [BTW_SYSTEM_PROMPT]): ResourceLoader {
|
|
89
|
+
const extensionsResult = { extensions: [], errors: [], runtime: createExtensionRuntime() };
|
|
90
|
+
const systemPrompt = stripDynamicSystemPromptFooter(ctx.getSystemPrompt());
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
getExtensions: () => extensionsResult,
|
|
94
|
+
getSkills: () => ({ skills: [], diagnostics: [] }),
|
|
95
|
+
getPrompts: () => ({ prompts: [], diagnostics: [] }),
|
|
96
|
+
getThemes: () => ({ themes: [], diagnostics: [] }),
|
|
97
|
+
getAgentsFiles: () => ({ agentsFiles: [] }),
|
|
98
|
+
getSystemPrompt: () => systemPrompt,
|
|
99
|
+
getAppendSystemPrompt: () => appendSystemPrompt,
|
|
100
|
+
getPathMetadata: () => new Map(),
|
|
101
|
+
extendResources: () => {},
|
|
102
|
+
reload: async () => {},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function extractText(parts: AssistantMessage["content"]): string {
|
|
107
|
+
return parts
|
|
108
|
+
.filter((part) => part.type === "text")
|
|
109
|
+
.map((part) => part.text)
|
|
110
|
+
.join("\n")
|
|
111
|
+
.trim();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function extractEventAssistantText(message: unknown): string {
|
|
115
|
+
if (!message || typeof message !== "object") {
|
|
116
|
+
return "";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const maybeMessage = message as { role?: unknown; content?: unknown };
|
|
120
|
+
if (maybeMessage.role !== "assistant" || !Array.isArray(maybeMessage.content)) {
|
|
121
|
+
return "";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return maybeMessage.content
|
|
125
|
+
.filter((part): part is { type: "text"; text: string } => {
|
|
126
|
+
return !!part && typeof part === "object" && (part as { type?: unknown }).type === "text";
|
|
127
|
+
})
|
|
128
|
+
.map((part) => part.text)
|
|
129
|
+
.join("\n")
|
|
130
|
+
.trim();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function getLastAssistantMessage(session: AgentSession): AssistantMessage | null {
|
|
134
|
+
for (let i = session.state.messages.length - 1; i >= 0; i--) {
|
|
135
|
+
const message = session.state.messages[i];
|
|
136
|
+
if (message.role === "assistant") {
|
|
137
|
+
return message as AssistantMessage;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function buildSeedMessages(ctx: ExtensionContext, thread: BtwDetails[]): Message[] {
|
|
145
|
+
const seed: Message[] = [];
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const contextMessages = buildSessionContext(ctx.sessionManager.getEntries(), ctx.sessionManager.getLeafId()).messages;
|
|
149
|
+
seed.push(...contextMessages);
|
|
150
|
+
} catch {
|
|
151
|
+
// Ignore context seed failures and continue with an empty side thread.
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
for (const item of thread) {
|
|
155
|
+
seed.push(
|
|
156
|
+
{
|
|
157
|
+
role: "user",
|
|
158
|
+
content: [{ type: "text", text: item.question }],
|
|
159
|
+
timestamp: item.timestamp,
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
role: "assistant",
|
|
163
|
+
content: [{ type: "text", text: item.answer }],
|
|
164
|
+
provider: item.provider,
|
|
165
|
+
model: item.model,
|
|
166
|
+
api: ctx.model?.api ?? "openai-responses",
|
|
167
|
+
usage:
|
|
168
|
+
item.usage ??
|
|
169
|
+
{
|
|
170
|
+
input: 0,
|
|
171
|
+
output: 0,
|
|
172
|
+
cacheRead: 0,
|
|
173
|
+
cacheWrite: 0,
|
|
174
|
+
totalTokens: 0,
|
|
175
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
176
|
+
},
|
|
177
|
+
stopReason: "stop",
|
|
178
|
+
timestamp: item.timestamp,
|
|
179
|
+
},
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return seed;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function formatThread(thread: BtwDetails[]): string {
|
|
187
|
+
return thread
|
|
188
|
+
.map((item) => `User: ${item.question.trim()}\nAssistant: ${item.answer.trim()}`)
|
|
189
|
+
.join("\n\n---\n\n");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function notify(ctx: ExtensionContext | ExtensionCommandContext, message: string, level: "info" | "warning" | "error"): void {
|
|
193
|
+
if (ctx.hasUI) {
|
|
194
|
+
ctx.ui.notify(message, level);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class BtwOverlay extends Container implements Focusable {
|
|
200
|
+
private readonly input: Input;
|
|
201
|
+
private readonly tui: TUI;
|
|
202
|
+
private readonly theme: ExtensionContext["ui"]["theme"];
|
|
203
|
+
private readonly keybindings: KeybindingsManager;
|
|
204
|
+
private readonly getTranscript: (width: number, theme: ExtensionContext["ui"]["theme"]) => string[];
|
|
205
|
+
private readonly getStatus: () => string;
|
|
206
|
+
private readonly onSubmitCallback: (value: string) => void;
|
|
207
|
+
private readonly onDismissCallback: () => void;
|
|
208
|
+
private _focused = false;
|
|
209
|
+
private scrollOffset = 0; // 0 = pinned to bottom, >0 = lines scrolled up
|
|
210
|
+
private lastTranscriptLength = 0;
|
|
211
|
+
|
|
212
|
+
get focused(): boolean {
|
|
213
|
+
return this._focused;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
set focused(value: boolean) {
|
|
217
|
+
this._focused = value;
|
|
218
|
+
this.input.focused = value;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
constructor(
|
|
222
|
+
tui: TUI,
|
|
223
|
+
theme: ExtensionContext["ui"]["theme"],
|
|
224
|
+
keybindings: KeybindingsManager,
|
|
225
|
+
getTranscript: (width: number, theme: ExtensionContext["ui"]["theme"]) => string[],
|
|
226
|
+
getStatus: () => string,
|
|
227
|
+
onSubmit: (value: string) => void,
|
|
228
|
+
onDismiss: () => void,
|
|
229
|
+
) {
|
|
230
|
+
super();
|
|
231
|
+
this.tui = tui;
|
|
232
|
+
this.theme = theme;
|
|
233
|
+
this.keybindings = keybindings;
|
|
234
|
+
this.getTranscript = getTranscript;
|
|
235
|
+
this.getStatus = getStatus;
|
|
236
|
+
this.onSubmitCallback = onSubmit;
|
|
237
|
+
this.onDismissCallback = onDismiss;
|
|
238
|
+
|
|
239
|
+
this.input = new Input();
|
|
240
|
+
this.input.onSubmit = (value) => {
|
|
241
|
+
this.onSubmitCallback(value);
|
|
242
|
+
};
|
|
243
|
+
this.input.onEscape = () => {
|
|
244
|
+
this.onDismissCallback();
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
handleInput(data: string): void {
|
|
249
|
+
if (this.keybindings.matches(data, "selectCancel")) {
|
|
250
|
+
this.onDismissCallback();
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Scroll: Shift+Up/Down for single lines, PageUp/PageDown for pages
|
|
255
|
+
if (matchesKey(data, "shift+up")) {
|
|
256
|
+
this.scrollUp(1);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (matchesKey(data, "shift+down")) {
|
|
260
|
+
this.scrollDown(1);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (matchesKey(data, "pageUp")) {
|
|
264
|
+
this.scrollUp(10);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (matchesKey(data, "pageDown")) {
|
|
268
|
+
this.scrollDown(10);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
this.input.handleInput(data);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private scrollUp(lines: number): void {
|
|
276
|
+
const maxOffset = Math.max(0, this.lastTranscriptLength - 1);
|
|
277
|
+
this.scrollOffset = Math.min(this.scrollOffset + lines, maxOffset);
|
|
278
|
+
this.tui.requestRender();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private scrollDown(lines: number): void {
|
|
282
|
+
this.scrollOffset = Math.max(0, this.scrollOffset - lines);
|
|
283
|
+
this.tui.requestRender();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Call when new content arrives to snap back to bottom. */
|
|
287
|
+
snapToBottom(): void {
|
|
288
|
+
this.scrollOffset = 0;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
setDraft(value: string): void {
|
|
292
|
+
this.input.setValue(value);
|
|
293
|
+
this.tui.requestRender();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
getDraft(): string {
|
|
297
|
+
return this.input.getValue();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private frameLine(content: string, innerWidth: number): string {
|
|
301
|
+
const truncated = truncateToWidth(content, innerWidth, "");
|
|
302
|
+
const padding = Math.max(0, innerWidth - visibleWidth(truncated));
|
|
303
|
+
return `${this.theme.fg("borderMuted", "│")}${truncated}${" ".repeat(padding)}${this.theme.fg("borderMuted", "│")}`;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private borderLine(innerWidth: number, edge: "top" | "bottom"): string {
|
|
307
|
+
const left = edge === "top" ? "┌" : "└";
|
|
308
|
+
const right = edge === "top" ? "┐" : "┘";
|
|
309
|
+
return this.theme.fg("borderMuted", `${left}${"─".repeat(innerWidth)}${right}`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
override render(width: number): string[] {
|
|
313
|
+
const dialogWidth = Math.max(56, Math.min(width, Math.floor(width * 0.9)));
|
|
314
|
+
const innerWidth = Math.max(40, dialogWidth - 2);
|
|
315
|
+
const terminalRows = process.stdout.rows ?? 30;
|
|
316
|
+
const dialogHeight = Math.max(16, Math.min(30, Math.floor(terminalRows * 0.75)));
|
|
317
|
+
const chromeHeight = 7;
|
|
318
|
+
const transcriptHeight = Math.max(6, dialogHeight - chromeHeight);
|
|
319
|
+
|
|
320
|
+
// Markdown renders to innerWidth already — no manual wrapping needed
|
|
321
|
+
const transcript = this.getTranscript(innerWidth, this.theme);
|
|
322
|
+
this.lastTranscriptLength = transcript.length;
|
|
323
|
+
|
|
324
|
+
// Clamp scrollOffset to valid range
|
|
325
|
+
const maxOffset = Math.max(0, transcript.length - transcriptHeight);
|
|
326
|
+
this.scrollOffset = Math.min(this.scrollOffset, maxOffset);
|
|
327
|
+
|
|
328
|
+
// Calculate viewport: scrollOffset=0 means pinned to bottom
|
|
329
|
+
const endIndex = transcript.length - this.scrollOffset;
|
|
330
|
+
const startIndex = Math.max(0, endIndex - transcriptHeight);
|
|
331
|
+
const visibleTranscript = transcript.slice(startIndex, endIndex);
|
|
332
|
+
const transcriptPadding = Math.max(0, transcriptHeight - visibleTranscript.length);
|
|
333
|
+
|
|
334
|
+
const linesAbove = startIndex;
|
|
335
|
+
const linesBelow = transcript.length - endIndex;
|
|
336
|
+
|
|
337
|
+
const status = this.getStatus();
|
|
338
|
+
|
|
339
|
+
const previousFocused = this.input.focused;
|
|
340
|
+
this.input.focused = false;
|
|
341
|
+
const inputLine = this.input.render(innerWidth)[0] ?? "";
|
|
342
|
+
this.input.focused = previousFocused;
|
|
343
|
+
|
|
344
|
+
const lines = [
|
|
345
|
+
this.borderLine(innerWidth, "top"),
|
|
346
|
+
this.frameLine(this.theme.fg("accent", this.theme.bold(" BTW side chat ")), innerWidth),
|
|
347
|
+
this.frameLine(this.theme.fg("dim", "Separate side conversation. Esc closes."), innerWidth),
|
|
348
|
+
this.theme.fg("borderMuted", `├${"─".repeat(innerWidth)}┤`),
|
|
349
|
+
];
|
|
350
|
+
|
|
351
|
+
// Scroll-up indicator
|
|
352
|
+
if (linesAbove > 0) {
|
|
353
|
+
const indicator = this.theme.fg("dim", `--- ${linesAbove} more above (Shift+Up / PgUp) `);
|
|
354
|
+
lines.push(this.frameLine(indicator, innerWidth));
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
for (const line of visibleTranscript) {
|
|
358
|
+
lines.push(this.frameLine(line, innerWidth));
|
|
359
|
+
}
|
|
360
|
+
// Adjust padding to account for indicator lines
|
|
361
|
+
const indicatorLines = (linesAbove > 0 ? 1 : 0) + (linesBelow > 0 ? 1 : 0);
|
|
362
|
+
const adjustedPadding = Math.max(0, transcriptPadding - indicatorLines);
|
|
363
|
+
for (let i = 0; i < adjustedPadding; i++) {
|
|
364
|
+
lines.push(this.frameLine("", innerWidth));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Scroll-down indicator
|
|
368
|
+
if (linesBelow > 0) {
|
|
369
|
+
const indicator = this.theme.fg("dim", `--- ${linesBelow} more below (Shift+Down / PgDn) `);
|
|
370
|
+
lines.push(this.frameLine(indicator, innerWidth));
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
lines.push(this.theme.fg("borderMuted", `├${"─".repeat(innerWidth)}┤`));
|
|
374
|
+
lines.push(this.frameLine(this.theme.fg("warning", status), innerWidth));
|
|
375
|
+
lines.push(
|
|
376
|
+
`${this.theme.fg("borderMuted", "│")}${inputLine}${this.theme.fg("borderMuted", "│")}`,
|
|
377
|
+
);
|
|
378
|
+
lines.push(this.frameLine(this.theme.fg("dim", "Enter submit · Esc close · Shift+Up/Down scroll"), innerWidth));
|
|
379
|
+
lines.push(this.borderLine(innerWidth, "bottom"));
|
|
380
|
+
|
|
381
|
+
return lines;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export default function (pi: ExtensionAPI) {
|
|
386
|
+
let thread: BtwDetails[] = [];
|
|
387
|
+
let pendingQuestion: string | null = null;
|
|
388
|
+
let pendingAnswer = "";
|
|
389
|
+
let pendingError: string | null = null;
|
|
390
|
+
let pendingToolCalls: ToolCallInfo[] = [];
|
|
391
|
+
let sideBusy = false;
|
|
392
|
+
let overlayStatus = "Ready";
|
|
393
|
+
let overlayDraft = "";
|
|
394
|
+
let overlayRuntime: OverlayRuntime | null = null;
|
|
395
|
+
let activeSideSession: SideSessionRuntime | null = null;
|
|
396
|
+
let overlayRefreshTimer: ReturnType<typeof setTimeout> | null = null;
|
|
397
|
+
|
|
398
|
+
const mdTheme = getMarkdownTheme();
|
|
399
|
+
|
|
400
|
+
function getModelKey(ctx: ExtensionContext): string {
|
|
401
|
+
const model = ctx.model;
|
|
402
|
+
return model ? `${model.provider}/${model.id}` : "none";
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function renderMarkdownLines(text: string, width: number): string[] {
|
|
406
|
+
if (!text) return [];
|
|
407
|
+
try {
|
|
408
|
+
const md = new Markdown(text, 0, 0, mdTheme);
|
|
409
|
+
return md.render(width);
|
|
410
|
+
} catch {
|
|
411
|
+
// Fall back to plain text wrapping if Markdown rendering fails
|
|
412
|
+
return text.split("\n").flatMap((line) => {
|
|
413
|
+
if (!line) return [""];
|
|
414
|
+
const wrapped: string[] = [];
|
|
415
|
+
for (let i = 0; i < line.length; i += width) {
|
|
416
|
+
wrapped.push(line.slice(i, i + width));
|
|
417
|
+
}
|
|
418
|
+
return wrapped.length > 0 ? wrapped : [""];
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function formatToolArgs(toolName: string, args: unknown): string {
|
|
424
|
+
if (!args || typeof args !== "object") return "";
|
|
425
|
+
const a = args as Record<string, unknown>;
|
|
426
|
+
switch (toolName) {
|
|
427
|
+
case "bash":
|
|
428
|
+
return typeof a.command === "string" ? truncateToWidth(a.command.split("\n")[0], 50, "…") : "";
|
|
429
|
+
case "read":
|
|
430
|
+
case "write":
|
|
431
|
+
case "edit":
|
|
432
|
+
return typeof a.path === "string" ? a.path : "";
|
|
433
|
+
default: {
|
|
434
|
+
const first = Object.values(a)[0];
|
|
435
|
+
return typeof first === "string" ? truncateToWidth(first.split("\n")[0], 40, "…") : "";
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function renderToolCallLines(toolCalls: ToolCallInfo[], theme: ExtensionContext["ui"]["theme"], width: number): string[] {
|
|
441
|
+
const lines: string[] = [];
|
|
442
|
+
for (const tc of toolCalls) {
|
|
443
|
+
const icon = tc.status === "running" ? "⚙" : tc.status === "error" ? "✗" : "✓";
|
|
444
|
+
const color = tc.status === "error" ? "error" : tc.status === "done" ? "success" : "dim";
|
|
445
|
+
const label = theme.fg(color, `${icon} `) + theme.fg("toolTitle", tc.toolName);
|
|
446
|
+
const argsText = tc.args ? theme.fg("dim", ` ${tc.args}`) : "";
|
|
447
|
+
lines.push(truncateToWidth(` ${label}${argsText}`, width, ""));
|
|
448
|
+
}
|
|
449
|
+
return lines;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function getTranscriptLines(width: number, theme: ExtensionContext["ui"]["theme"]): string[] {
|
|
453
|
+
try {
|
|
454
|
+
return getTranscriptLinesInner(width, theme);
|
|
455
|
+
} catch (error) {
|
|
456
|
+
return [theme.fg("error", `Render error: ${error instanceof Error ? error.message : String(error)}`)];
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function getTranscriptLinesInner(width: number, theme: ExtensionContext["ui"]["theme"]): string[] {
|
|
461
|
+
if (thread.length === 0 && !pendingQuestion && !pendingAnswer && !pendingError) {
|
|
462
|
+
return [theme.fg("dim", "No BTW messages yet. Type a question below.")];
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const lines: string[] = [];
|
|
466
|
+
for (const item of thread.slice(-6)) {
|
|
467
|
+
// User message
|
|
468
|
+
const userText = item.question.trim().split("\n")[0];
|
|
469
|
+
lines.push(theme.fg("accent", theme.bold("You: ")) + truncateToWidth(userText, width - 5, "…"));
|
|
470
|
+
lines.push("");
|
|
471
|
+
|
|
472
|
+
// Assistant message rendered as markdown
|
|
473
|
+
const mdLines = renderMarkdownLines(item.answer, width);
|
|
474
|
+
lines.push(...mdLines);
|
|
475
|
+
lines.push("");
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (pendingQuestion) {
|
|
479
|
+
const userText = pendingQuestion.trim().split("\n")[0];
|
|
480
|
+
lines.push(theme.fg("accent", theme.bold("You: ")) + truncateToWidth(userText, width - 5, "…"));
|
|
481
|
+
|
|
482
|
+
// Show tool calls inline
|
|
483
|
+
if (pendingToolCalls.length > 0) {
|
|
484
|
+
lines.push(...renderToolCallLines(pendingToolCalls, theme, width));
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (pendingError) {
|
|
488
|
+
lines.push(theme.fg("error", `❌ ${pendingError}`));
|
|
489
|
+
} else if (pendingAnswer) {
|
|
490
|
+
lines.push("");
|
|
491
|
+
const mdLines = renderMarkdownLines(pendingAnswer, width);
|
|
492
|
+
lines.push(...mdLines);
|
|
493
|
+
} else if (pendingToolCalls.length === 0) {
|
|
494
|
+
lines.push(theme.fg("dim", "…"));
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Trim trailing empty line
|
|
499
|
+
while (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
500
|
+
lines.pop();
|
|
501
|
+
}
|
|
502
|
+
return lines;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function syncOverlay(): void {
|
|
506
|
+
overlayRuntime?.refresh?.();
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function scheduleOverlayRefresh(): void {
|
|
510
|
+
if (overlayRefreshTimer) {
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
overlayRefreshTimer = setTimeout(() => {
|
|
515
|
+
overlayRefreshTimer = null;
|
|
516
|
+
syncOverlay();
|
|
517
|
+
}, 16);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function setOverlayStatus(status: string, throttled = false): void {
|
|
521
|
+
overlayStatus = status;
|
|
522
|
+
if (throttled) {
|
|
523
|
+
scheduleOverlayRefresh();
|
|
524
|
+
} else {
|
|
525
|
+
syncOverlay();
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function dismissOverlay(): void {
|
|
530
|
+
overlayRuntime?.close?.();
|
|
531
|
+
overlayRuntime = null;
|
|
532
|
+
if (overlayRefreshTimer) {
|
|
533
|
+
clearTimeout(overlayRefreshTimer);
|
|
534
|
+
overlayRefreshTimer = null;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function setOverlayDraft(value: string): void {
|
|
539
|
+
overlayDraft = value;
|
|
540
|
+
overlayRuntime?.setDraft?.(value);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
async function disposeSideSession(): Promise<void> {
|
|
544
|
+
const current = activeSideSession;
|
|
545
|
+
activeSideSession = null;
|
|
546
|
+
if (!current) {
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
try {
|
|
551
|
+
current.unsubscribe();
|
|
552
|
+
} catch {
|
|
553
|
+
// Ignore unsubscribe errors during cleanup.
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
try {
|
|
557
|
+
await current.session.abort();
|
|
558
|
+
} catch {
|
|
559
|
+
// Ignore abort errors during cleanup.
|
|
560
|
+
}
|
|
561
|
+
current.session.dispose();
|
|
562
|
+
|
|
563
|
+
if (overlayRefreshTimer) {
|
|
564
|
+
clearTimeout(overlayRefreshTimer);
|
|
565
|
+
overlayRefreshTimer = null;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
async function resetThread(ctx: ExtensionContext | ExtensionCommandContext, persist = true): Promise<void> {
|
|
570
|
+
thread = [];
|
|
571
|
+
pendingQuestion = null;
|
|
572
|
+
pendingAnswer = "";
|
|
573
|
+
pendingError = null;
|
|
574
|
+
pendingToolCalls = [];
|
|
575
|
+
sideBusy = false;
|
|
576
|
+
setOverlayDraft("");
|
|
577
|
+
setOverlayStatus("Ready");
|
|
578
|
+
await disposeSideSession();
|
|
579
|
+
if (persist) {
|
|
580
|
+
const details: BtwResetDetails = { timestamp: Date.now() };
|
|
581
|
+
pi.appendEntry(BTW_RESET_TYPE, details);
|
|
582
|
+
}
|
|
583
|
+
syncOverlay();
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
async function restoreThread(ctx: ExtensionContext): Promise<void> {
|
|
587
|
+
await disposeSideSession();
|
|
588
|
+
thread = [];
|
|
589
|
+
pendingQuestion = null;
|
|
590
|
+
pendingAnswer = "";
|
|
591
|
+
pendingError = null;
|
|
592
|
+
pendingToolCalls = [];
|
|
593
|
+
sideBusy = false;
|
|
594
|
+
overlayStatus = "Ready";
|
|
595
|
+
overlayDraft = "";
|
|
596
|
+
const branch = ctx.sessionManager.getBranch();
|
|
597
|
+
let lastResetIndex = -1;
|
|
598
|
+
for (let i = 0; i < branch.length; i++) {
|
|
599
|
+
const entry = branch[i];
|
|
600
|
+
if (entry.type === "custom" && entry.customType === BTW_RESET_TYPE) {
|
|
601
|
+
lastResetIndex = i;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
for (const entry of branch.slice(lastResetIndex + 1)) {
|
|
606
|
+
if (entry.type !== "custom" || entry.customType !== BTW_ENTRY_TYPE) {
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
const details = entry.data as BtwDetails | undefined;
|
|
610
|
+
if (!details?.question || !details.answer) {
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
thread.push(details);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
syncOverlay();
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
async function createSideSession(ctx: ExtensionCommandContext): Promise<SideSessionRuntime | null> {
|
|
620
|
+
if (!ctx.model) {
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const { session } = await createAgentSession({
|
|
625
|
+
sessionManager: SessionManager.inMemory(),
|
|
626
|
+
model: ctx.model,
|
|
627
|
+
modelRegistry: ctx.modelRegistry as AgentSession["modelRegistry"],
|
|
628
|
+
thinkingLevel: pi.getThinkingLevel() as SessionThinkingLevel,
|
|
629
|
+
tools: codingTools,
|
|
630
|
+
resourceLoader: createBtwResourceLoader(ctx),
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
const seedMessages = buildSeedMessages(ctx, thread);
|
|
634
|
+
if (seedMessages.length > 0) {
|
|
635
|
+
session.agent.replaceMessages(seedMessages as typeof session.state.messages);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const unsubscribe = session.subscribe((event: AgentSessionEvent) => {
|
|
639
|
+
if (!sideBusy || !pendingQuestion) {
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
switch (event.type) {
|
|
644
|
+
case "message_start":
|
|
645
|
+
case "message_update":
|
|
646
|
+
case "message_end": {
|
|
647
|
+
const streamed = extractEventAssistantText(event.message);
|
|
648
|
+
if (streamed) {
|
|
649
|
+
pendingAnswer = streamed;
|
|
650
|
+
pendingError = null;
|
|
651
|
+
}
|
|
652
|
+
setOverlayStatus(event.type === "message_end" ? "Finalizing side response..." : "Streaming side response...", true);
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
case "tool_execution_start": {
|
|
656
|
+
const toolName = (event as { toolName?: string }).toolName ?? "unknown";
|
|
657
|
+
try {
|
|
658
|
+
pendingToolCalls.push({
|
|
659
|
+
toolCallId: (event as { toolCallId?: string }).toolCallId ?? "",
|
|
660
|
+
toolName,
|
|
661
|
+
args: formatToolArgs(toolName, (event as { args?: unknown }).args),
|
|
662
|
+
status: "running",
|
|
663
|
+
});
|
|
664
|
+
} catch {
|
|
665
|
+
// Ignore tool tracking failures
|
|
666
|
+
}
|
|
667
|
+
setOverlayStatus(`Running tool: ${toolName}...`, true);
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
case "tool_execution_end": {
|
|
671
|
+
const endToolName = (event as { toolName?: string }).toolName ?? "unknown";
|
|
672
|
+
const tc = pendingToolCalls.find(
|
|
673
|
+
(t) => t.toolName === endToolName && t.status === "running",
|
|
674
|
+
);
|
|
675
|
+
if (tc) {
|
|
676
|
+
tc.status = (event as { isError?: boolean }).isError ? "error" : "done";
|
|
677
|
+
}
|
|
678
|
+
setOverlayStatus("Streaming side response...", true);
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
case "turn_end": {
|
|
682
|
+
setOverlayStatus("Finalizing side response...", true);
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
default:
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
return {
|
|
691
|
+
session,
|
|
692
|
+
modelKey: getModelKey(ctx),
|
|
693
|
+
unsubscribe,
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
async function ensureSideSession(ctx: ExtensionCommandContext): Promise<SideSessionRuntime | null> {
|
|
698
|
+
if (!ctx.model) {
|
|
699
|
+
return null;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const expectedModelKey = getModelKey(ctx);
|
|
703
|
+
if (activeSideSession && activeSideSession.modelKey === expectedModelKey) {
|
|
704
|
+
return activeSideSession;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
await disposeSideSession();
|
|
708
|
+
activeSideSession = await createSideSession(ctx);
|
|
709
|
+
return activeSideSession;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
async function ensureOverlay(ctx: ExtensionCommandContext | ExtensionContext): Promise<void> {
|
|
713
|
+
if (!ctx.hasUI) {
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
if (overlayRuntime?.handle) {
|
|
718
|
+
overlayRuntime.handle.setHidden(false);
|
|
719
|
+
overlayRuntime.handle.focus();
|
|
720
|
+
overlayRuntime.refresh?.();
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const runtime: OverlayRuntime = {};
|
|
725
|
+
const closeRuntime = () => {
|
|
726
|
+
if (runtime.closed) {
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
runtime.closed = true;
|
|
730
|
+
runtime.handle?.hide();
|
|
731
|
+
if (overlayRuntime === runtime) {
|
|
732
|
+
overlayRuntime = null;
|
|
733
|
+
}
|
|
734
|
+
runtime.finish?.();
|
|
735
|
+
};
|
|
736
|
+
runtime.close = closeRuntime;
|
|
737
|
+
overlayRuntime = runtime;
|
|
738
|
+
|
|
739
|
+
void ctx.ui
|
|
740
|
+
.custom<void>(
|
|
741
|
+
async (tui, theme, keybindings, done) => {
|
|
742
|
+
runtime.finish = () => done();
|
|
743
|
+
|
|
744
|
+
const overlay = new BtwOverlay(
|
|
745
|
+
tui,
|
|
746
|
+
theme,
|
|
747
|
+
keybindings,
|
|
748
|
+
(width, t) => getTranscriptLines(width, t),
|
|
749
|
+
() => overlayStatus,
|
|
750
|
+
(value) => {
|
|
751
|
+
void submitFromOverlay(ctx, value);
|
|
752
|
+
},
|
|
753
|
+
() => {
|
|
754
|
+
void closeOverlayFlow(ctx);
|
|
755
|
+
},
|
|
756
|
+
);
|
|
757
|
+
|
|
758
|
+
overlay.focused = true;
|
|
759
|
+
overlay.setDraft(overlayDraft);
|
|
760
|
+
runtime.setDraft = (value) => overlay.setDraft(value);
|
|
761
|
+
runtime.snapToBottom = () => overlay.snapToBottom();
|
|
762
|
+
runtime.refresh = () => {
|
|
763
|
+
overlay.focused = runtime.handle?.isFocused() ?? false;
|
|
764
|
+
tui.requestRender();
|
|
765
|
+
};
|
|
766
|
+
runtime.close = () => {
|
|
767
|
+
overlayDraft = overlay.getDraft();
|
|
768
|
+
closeRuntime();
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
if (runtime.closed) {
|
|
772
|
+
done();
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
return overlay;
|
|
776
|
+
},
|
|
777
|
+
{
|
|
778
|
+
overlay: true,
|
|
779
|
+
overlayOptions: {
|
|
780
|
+
width: "80%",
|
|
781
|
+
minWidth: 72,
|
|
782
|
+
maxHeight: "78%",
|
|
783
|
+
anchor: "top-center",
|
|
784
|
+
margin: { top: 1, left: 2, right: 2 },
|
|
785
|
+
},
|
|
786
|
+
onHandle: (handle) => {
|
|
787
|
+
runtime.handle = handle;
|
|
788
|
+
handle.focus();
|
|
789
|
+
if (runtime.closed) {
|
|
790
|
+
closeRuntime();
|
|
791
|
+
}
|
|
792
|
+
},
|
|
793
|
+
},
|
|
794
|
+
)
|
|
795
|
+
.catch((error) => {
|
|
796
|
+
if (overlayRuntime === runtime) {
|
|
797
|
+
overlayRuntime = null;
|
|
798
|
+
}
|
|
799
|
+
notify(ctx, error instanceof Error ? error.message : String(error), "error");
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
async function summarizeThread(ctx: ExtensionContext, items: BtwDetails[]): Promise<string> {
|
|
804
|
+
const model = ctx.model;
|
|
805
|
+
if (!model) {
|
|
806
|
+
throw new Error("No active model selected.");
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
810
|
+
if (!auth.ok) {
|
|
811
|
+
throw new Error(auth.error || `No credentials available for ${model.provider}/${model.id}.`);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
const { session } = await createAgentSession({
|
|
815
|
+
sessionManager: SessionManager.inMemory(),
|
|
816
|
+
model,
|
|
817
|
+
modelRegistry: ctx.modelRegistry as AgentSession["modelRegistry"],
|
|
818
|
+
thinkingLevel: "off",
|
|
819
|
+
tools: [],
|
|
820
|
+
resourceLoader: createBtwResourceLoader(ctx, [BTW_SUMMARY_PROMPT]),
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
try {
|
|
824
|
+
await session.prompt(formatThread(items), { source: "extension" });
|
|
825
|
+
const response = getLastAssistantMessage(session);
|
|
826
|
+
if (!response) {
|
|
827
|
+
throw new Error("Summary finished without a response.");
|
|
828
|
+
}
|
|
829
|
+
if (response.stopReason === "aborted") {
|
|
830
|
+
throw new Error("Summary request was aborted.");
|
|
831
|
+
}
|
|
832
|
+
if (response.stopReason === "error") {
|
|
833
|
+
throw new Error(response.errorMessage || "Summary request failed.");
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
return extractText(response.content) || "(No summary generated)";
|
|
837
|
+
} finally {
|
|
838
|
+
try {
|
|
839
|
+
await session.abort();
|
|
840
|
+
} catch {
|
|
841
|
+
// Ignore abort errors during temporary session teardown.
|
|
842
|
+
}
|
|
843
|
+
session.dispose();
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
async function injectSummaryIntoMain(ctx: ExtensionContext | ExtensionCommandContext): Promise<void> {
|
|
848
|
+
if (thread.length === 0) {
|
|
849
|
+
notify(ctx, "No BTW thread to summarize.", "warning");
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
setOverlayStatus("Summarizing BTW thread for injection...");
|
|
854
|
+
try {
|
|
855
|
+
const summary = await summarizeThread(ctx, thread);
|
|
856
|
+
const message = `Summary of my BTW side conversation:\n\n${summary}`;
|
|
857
|
+
if (ctx.isIdle()) {
|
|
858
|
+
pi.sendUserMessage(message);
|
|
859
|
+
} else {
|
|
860
|
+
pi.sendUserMessage(message, { deliverAs: "followUp" });
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
await resetThread(ctx);
|
|
864
|
+
notify(ctx, "Injected BTW summary into main chat.", "info");
|
|
865
|
+
} catch (error) {
|
|
866
|
+
notify(ctx, error instanceof Error ? error.message : String(error), "error");
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
async function closeOverlayFlow(ctx: ExtensionContext | ExtensionCommandContext): Promise<void> {
|
|
871
|
+
dismissOverlay();
|
|
872
|
+
if (!ctx.hasUI) {
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
if (thread.length === 0) {
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
const choice = await ctx.ui.select("Close BTW:", ["Keep side thread", "Inject summary into main chat"]);
|
|
881
|
+
if (choice === "Inject summary into main chat") {
|
|
882
|
+
await injectSummaryIntoMain(ctx);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
async function runBtwPrompt(ctx: ExtensionCommandContext, question: string): Promise<void> {
|
|
887
|
+
const model = ctx.model;
|
|
888
|
+
if (!model) {
|
|
889
|
+
setOverlayStatus("No active model selected.");
|
|
890
|
+
notify(ctx, "No active model selected.", "error");
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
895
|
+
if (!auth.ok) {
|
|
896
|
+
const message = auth.error || `No credentials available for ${model.provider}/${model.id}.`;
|
|
897
|
+
setOverlayStatus(message);
|
|
898
|
+
notify(ctx, message, "error");
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
if (sideBusy) {
|
|
903
|
+
notify(ctx, "BTW is still processing the previous message.", "warning");
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
const side = await ensureSideSession(ctx);
|
|
908
|
+
if (!side) {
|
|
909
|
+
notify(ctx, "Unable to create BTW side session.", "error");
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
sideBusy = true;
|
|
914
|
+
pendingQuestion = question;
|
|
915
|
+
pendingAnswer = "";
|
|
916
|
+
pendingError = null;
|
|
917
|
+
pendingToolCalls = [];
|
|
918
|
+
overlayRuntime?.snapToBottom?.();
|
|
919
|
+
setOverlayStatus("Streaming side response...");
|
|
920
|
+
syncOverlay();
|
|
921
|
+
|
|
922
|
+
try {
|
|
923
|
+
await side.session.prompt(question, { source: "extension" });
|
|
924
|
+
const response = getLastAssistantMessage(side.session);
|
|
925
|
+
if (!response) {
|
|
926
|
+
throw new Error("BTW request finished without a response.");
|
|
927
|
+
}
|
|
928
|
+
if (response.stopReason === "aborted") {
|
|
929
|
+
throw new Error("BTW request aborted.");
|
|
930
|
+
}
|
|
931
|
+
if (response.stopReason === "error") {
|
|
932
|
+
throw new Error(response.errorMessage || "BTW request failed.");
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const answer = extractText(response.content) || "(No text response)";
|
|
936
|
+
pendingAnswer = answer;
|
|
937
|
+
const details: BtwDetails = {
|
|
938
|
+
question,
|
|
939
|
+
answer,
|
|
940
|
+
timestamp: Date.now(),
|
|
941
|
+
provider: model.provider,
|
|
942
|
+
model: model.id,
|
|
943
|
+
thinkingLevel: pi.getThinkingLevel() as SessionThinkingLevel,
|
|
944
|
+
usage: response.usage,
|
|
945
|
+
};
|
|
946
|
+
thread.push(details);
|
|
947
|
+
pi.appendEntry(BTW_ENTRY_TYPE, details);
|
|
948
|
+
|
|
949
|
+
pendingQuestion = null;
|
|
950
|
+
pendingAnswer = "";
|
|
951
|
+
pendingToolCalls = [];
|
|
952
|
+
setOverlayStatus("Ready for the next side question.");
|
|
953
|
+
} catch (error) {
|
|
954
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
955
|
+
pendingError = message;
|
|
956
|
+
setOverlayStatus("BTW request failed.");
|
|
957
|
+
notify(ctx, message, "error");
|
|
958
|
+
} finally {
|
|
959
|
+
sideBusy = false;
|
|
960
|
+
syncOverlay();
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
async function submitFromOverlay(ctx: ExtensionContext | ExtensionCommandContext, rawValue: string): Promise<void> {
|
|
965
|
+
const question = rawValue.trim();
|
|
966
|
+
if (!question) {
|
|
967
|
+
setOverlayStatus("Enter a question first.");
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
setOverlayDraft("");
|
|
972
|
+
if (!("waitForIdle" in ctx)) {
|
|
973
|
+
setOverlayStatus("BTW submit requires command context. Re-open with /btw.");
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
await runBtwPrompt(ctx, question);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
pi.registerCommand("btw", {
|
|
981
|
+
description: "Open a simple BTW side-chat popover. `/btw <text>` asks immediately, `/btw` opens the side thread.",
|
|
982
|
+
handler: async (args, ctx) => {
|
|
983
|
+
const question = args.trim();
|
|
984
|
+
|
|
985
|
+
if (!question) {
|
|
986
|
+
if (thread.length > 0 && ctx.hasUI) {
|
|
987
|
+
const choice = await ctx.ui.select("BTW side chat:", [
|
|
988
|
+
"Continue previous conversation",
|
|
989
|
+
"Start fresh",
|
|
990
|
+
]);
|
|
991
|
+
if (choice === "Continue previous conversation") {
|
|
992
|
+
// Dispose session so it's recreated with fresh main context on next submit
|
|
993
|
+
await disposeSideSession();
|
|
994
|
+
setOverlayStatus("Continuing BTW thread.");
|
|
995
|
+
await ensureOverlay(ctx);
|
|
996
|
+
} else if (choice === "Start fresh") {
|
|
997
|
+
await resetThread(ctx, true);
|
|
998
|
+
setOverlayStatus("Ready");
|
|
999
|
+
await ensureOverlay(ctx);
|
|
1000
|
+
}
|
|
1001
|
+
// null = user cancelled (Esc), do nothing
|
|
1002
|
+
} else {
|
|
1003
|
+
await resetThread(ctx, true);
|
|
1004
|
+
setOverlayStatus("Ready");
|
|
1005
|
+
await ensureOverlay(ctx);
|
|
1006
|
+
}
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
await ensureOverlay(ctx);
|
|
1011
|
+
await runBtwPrompt(ctx, question);
|
|
1012
|
+
},
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
1016
|
+
await restoreThread(ctx);
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
pi.on("session_switch", async (_event, ctx) => {
|
|
1020
|
+
await restoreThread(ctx);
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
pi.on("session_tree", async (_event, ctx) => {
|
|
1024
|
+
await restoreThread(ctx);
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
pi.on("session_shutdown", async () => {
|
|
1028
|
+
await disposeSideSession();
|
|
1029
|
+
dismissOverlay();
|
|
1030
|
+
});
|
|
1031
|
+
}
|