pi-agent-extensions 0.3.3 → 0.3.5
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 +35 -0
- package/docs/mitsuhiko-integration-analysis.md +2 -2
- package/extensions/btw/btw.ts +70 -0
- package/extensions/btw/index.ts +400 -0
- package/extensions/files/index.ts +7 -7
- package/extensions/sessions/index.ts +1 -1
- package/extensions/todos/index.ts +14 -14
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -32,6 +32,7 @@ Original repository: https://github.com/mitsuhiko/agent-stuff
|
|
|
32
32
|
| **control** | RPC | Inter-session communication & control | ⚙️ Beta |
|
|
33
33
|
| **answer** | Tool | Structured Q&A for complex queries | ⚙️ Beta |
|
|
34
34
|
| **cwd_history** | Tracker | Tracks directory changes in context | ✅ Stable |
|
|
35
|
+
| **btw** | Command | Quick side questions without history | ✅ Stable |
|
|
35
36
|
| **nvidia-nim** | Command | Nvidia NIM auth & config | ✅ Stable |
|
|
36
37
|
|
|
37
38
|
## Install
|
|
@@ -98,6 +99,10 @@ pi
|
|
|
98
99
|
|
|
99
100
|
You'll see a loader while context is extracted, then an editor to review the handoff prompt.
|
|
100
101
|
|
|
102
|
+
## Changelog
|
|
103
|
+
|
|
104
|
+
See [CHANGELOG.md](CHANGELOG.md) for release history.
|
|
105
|
+
|
|
101
106
|
## Update
|
|
102
107
|
|
|
103
108
|
```bash
|
|
@@ -267,6 +272,36 @@ A personality engine for Pi that makes waiting fun.
|
|
|
267
272
|
|
|
268
273
|
See [extensions/whimsical/README.md](extensions/whimsical/README.md) for details.
|
|
269
274
|
|
|
275
|
+
### BTW
|
|
276
|
+
|
|
277
|
+
Ask quick "by the way" side questions without polluting your conversation history. Inspired by Claude Code's `/btw` command.
|
|
278
|
+
|
|
279
|
+
**Usage:**
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
/btw what's the syntax for useEffect cleanup?
|
|
283
|
+
/btw which files did we modify just now?
|
|
284
|
+
/btw why did you choose Zustand over Redux?
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
**Features:**
|
|
288
|
+
- ✅ Full conversation context visibility
|
|
289
|
+
- ✅ No tool access (lightweight, read-only)
|
|
290
|
+
- ✅ Ephemeral overlay — nothing enters session history
|
|
291
|
+
- ✅ Zero context cost — no tokens wasted
|
|
292
|
+
- ✅ Scrollable answer (↑↓/j/k, PgUp/PgDn)
|
|
293
|
+
- ✅ Prefers cheap/fast models (Codex mini → Haiku → current)
|
|
294
|
+
- ✅ Non-UI fallback (prints to stdout)
|
|
295
|
+
|
|
296
|
+
**When to use `/btw` vs normal prompts:**
|
|
297
|
+
|
|
298
|
+
| Use `/btw` | Use normal prompts |
|
|
299
|
+
|---|---|
|
|
300
|
+
| Quick syntax checks | Requests needing tool access |
|
|
301
|
+
| Confirming earlier decisions | Changes you want tracked |
|
|
302
|
+
| Recalling context details | Complex multi-step tasks |
|
|
303
|
+
| "One and done" lookups | Follow-up conversations |
|
|
304
|
+
|
|
270
305
|
### Productivity Tools
|
|
271
306
|
|
|
272
307
|
**Files (`/files`)**
|
|
@@ -458,8 +458,8 @@ pi.on("tool_result", (event, ctx) => {
|
|
|
458
458
|
```typescript
|
|
459
459
|
// Store extension state in session
|
|
460
460
|
pi.appendEntry<StateType>(CUSTOM_TYPE, stateData);
|
|
461
|
-
// Restore on session switch
|
|
462
|
-
pi.on("
|
|
461
|
+
// Restore on session start (covers startup, switch, fork, etc.)
|
|
462
|
+
pi.on("session_start", (event, ctx) => {
|
|
463
463
|
applyState(ctx);
|
|
464
464
|
});
|
|
465
465
|
```
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Testable logic for the /btw extension.
|
|
3
|
+
*
|
|
4
|
+
* Separated from index.ts so pure functions can be unit-tested
|
|
5
|
+
* without importing pi-coding-agent or pi-tui.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* System prompt for BTW side questions.
|
|
10
|
+
* Instructs the LLM to answer concisely from conversation context only.
|
|
11
|
+
*/
|
|
12
|
+
export const BTW_SYSTEM_PROMPT = `You are answering a quick "by the way" side question during a coding session.
|
|
13
|
+
|
|
14
|
+
Rules:
|
|
15
|
+
- Answer concisely and directly based on the conversation context provided.
|
|
16
|
+
- You have NO tool access — you cannot read files, run commands, or make changes.
|
|
17
|
+
- Only answer based on information already present in the conversation.
|
|
18
|
+
- Keep your response brief and to the point.
|
|
19
|
+
- Use markdown formatting where helpful (code blocks, lists, bold).
|
|
20
|
+
- If the conversation context doesn't contain enough information to answer, say so honestly.`;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Build the user message for the BTW LLM call.
|
|
24
|
+
* Includes the serialized conversation as context and the user's question.
|
|
25
|
+
*/
|
|
26
|
+
export function buildBtwUserMessage(
|
|
27
|
+
conversationText: string,
|
|
28
|
+
question: string,
|
|
29
|
+
): string {
|
|
30
|
+
return `<conversation_context>
|
|
31
|
+
${conversationText}
|
|
32
|
+
</conversation_context>
|
|
33
|
+
|
|
34
|
+
<side_question>
|
|
35
|
+
${question}
|
|
36
|
+
</side_question>
|
|
37
|
+
|
|
38
|
+
Answer the side question above based on the conversation context. Be concise.`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Validate the /btw command arguments.
|
|
43
|
+
* Returns the question text or an error message.
|
|
44
|
+
*/
|
|
45
|
+
export function validateBtwArgs(args: string | undefined): {
|
|
46
|
+
valid: boolean;
|
|
47
|
+
question?: string;
|
|
48
|
+
error?: string;
|
|
49
|
+
} {
|
|
50
|
+
const question = args?.trim();
|
|
51
|
+
if (!question || question.length === 0) {
|
|
52
|
+
return {
|
|
53
|
+
valid: false,
|
|
54
|
+
error: "Usage: /btw <question> — Ask a quick side question without polluting conversation history.",
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return { valid: true, question };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Extract text content from an LLM response content array.
|
|
62
|
+
*/
|
|
63
|
+
export function extractResponseText(
|
|
64
|
+
content: Array<{ type: string; text?: string }>,
|
|
65
|
+
): string {
|
|
66
|
+
return content
|
|
67
|
+
.filter((c): c is { type: "text"; text: string } => c.type === "text" && typeof c.text === "string")
|
|
68
|
+
.map((c) => c.text)
|
|
69
|
+
.join("\n");
|
|
70
|
+
}
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BTW Extension
|
|
3
|
+
*
|
|
4
|
+
* Provides a `/btw` command for quick side questions that don't pollute
|
|
5
|
+
* the conversation history. The answer appears in a temporary overlay
|
|
6
|
+
* and is fully ephemeral — nothing is persisted to the session.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* /btw what's the syntax for useEffect cleanup?
|
|
10
|
+
* /btw which files did we modify?
|
|
11
|
+
* /btw why did you choose that approach?
|
|
12
|
+
*
|
|
13
|
+
* Key behaviors:
|
|
14
|
+
* - Full visibility into current conversation context
|
|
15
|
+
* - No tool access (lightweight, read-only)
|
|
16
|
+
* - Answer displayed in dismissable overlay
|
|
17
|
+
* - Zero context cost — no tokens wasted on history
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { complete, type Model, type Api, type UserMessage } from "@mariozechner/pi-ai";
|
|
21
|
+
import type {
|
|
22
|
+
ExtensionAPI,
|
|
23
|
+
ExtensionCommandContext,
|
|
24
|
+
} from "@mariozechner/pi-coding-agent";
|
|
25
|
+
import {
|
|
26
|
+
BorderedLoader,
|
|
27
|
+
convertToLlm,
|
|
28
|
+
serializeConversation,
|
|
29
|
+
} from "@mariozechner/pi-coding-agent";
|
|
30
|
+
import {
|
|
31
|
+
type Component,
|
|
32
|
+
Container,
|
|
33
|
+
Key,
|
|
34
|
+
matchesKey,
|
|
35
|
+
Text,
|
|
36
|
+
wrapTextWithAnsi,
|
|
37
|
+
visibleWidth,
|
|
38
|
+
type TUI,
|
|
39
|
+
} from "@mariozechner/pi-tui";
|
|
40
|
+
|
|
41
|
+
import { getRequestAuth, hasRequestAuth } from "../shared/auth.js";
|
|
42
|
+
import {
|
|
43
|
+
BTW_SYSTEM_PROMPT,
|
|
44
|
+
buildBtwUserMessage,
|
|
45
|
+
validateBtwArgs,
|
|
46
|
+
extractResponseText,
|
|
47
|
+
} from "./btw.js";
|
|
48
|
+
|
|
49
|
+
const HAIKU_MODEL_ID = "claude-haiku-4-5";
|
|
50
|
+
const CODEX_MODEL_ID = "gpt-5.1-codex-mini";
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Select a cheap/fast model for BTW side questions.
|
|
54
|
+
* Prefers Codex mini → Haiku → current model.
|
|
55
|
+
*/
|
|
56
|
+
async function selectBtwModel(
|
|
57
|
+
currentModel: Model<Api>,
|
|
58
|
+
modelRegistry: {
|
|
59
|
+
find: (provider: string, modelId: string) => Model<Api> | undefined;
|
|
60
|
+
getApiKeyAndHeaders: (
|
|
61
|
+
model: Model<Api>,
|
|
62
|
+
) => Promise<
|
|
63
|
+
| { ok: true; apiKey?: string; headers?: Record<string, string> }
|
|
64
|
+
| { ok: false; error: string }
|
|
65
|
+
>;
|
|
66
|
+
},
|
|
67
|
+
): Promise<Model<Api>> {
|
|
68
|
+
// Try Codex mini first (cheapest)
|
|
69
|
+
const codexModel = modelRegistry.find("openai-codex", CODEX_MODEL_ID);
|
|
70
|
+
if (codexModel && (await hasRequestAuth(modelRegistry, codexModel))) {
|
|
71
|
+
return codexModel;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Try Haiku (fast & cheap)
|
|
75
|
+
const haikuModel = modelRegistry.find("anthropic", HAIKU_MODEL_ID);
|
|
76
|
+
if (haikuModel && (await hasRequestAuth(modelRegistry, haikuModel))) {
|
|
77
|
+
return haikuModel;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Fallback to current model
|
|
81
|
+
return currentModel;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Overlay component that displays the BTW answer.
|
|
86
|
+
* Supports scrolling for long answers.
|
|
87
|
+
*/
|
|
88
|
+
class BtwOverlay implements Component {
|
|
89
|
+
private tui: TUI;
|
|
90
|
+
private theme: any;
|
|
91
|
+
private question: string;
|
|
92
|
+
private answer: string;
|
|
93
|
+
private onDone: () => void;
|
|
94
|
+
private scrollOffset = 0;
|
|
95
|
+
private cachedWidth?: number;
|
|
96
|
+
private cachedLines?: string[];
|
|
97
|
+
|
|
98
|
+
constructor(
|
|
99
|
+
tui: TUI,
|
|
100
|
+
theme: any,
|
|
101
|
+
question: string,
|
|
102
|
+
answer: string,
|
|
103
|
+
onDone: () => void,
|
|
104
|
+
) {
|
|
105
|
+
this.tui = tui;
|
|
106
|
+
this.theme = theme;
|
|
107
|
+
this.question = question;
|
|
108
|
+
this.answer = answer;
|
|
109
|
+
this.onDone = onDone;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
handleInput(data: string): void {
|
|
113
|
+
// Dismiss overlay
|
|
114
|
+
if (
|
|
115
|
+
matchesKey(data, Key.escape) ||
|
|
116
|
+
matchesKey(data, Key.ctrl("c")) ||
|
|
117
|
+
data === " " ||
|
|
118
|
+
data.toLowerCase() === "q"
|
|
119
|
+
) {
|
|
120
|
+
this.onDone();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Scroll
|
|
125
|
+
if (matchesKey(data, Key.up) || data === "k") {
|
|
126
|
+
if (this.scrollOffset > 0) {
|
|
127
|
+
this.scrollOffset--;
|
|
128
|
+
this.invalidate();
|
|
129
|
+
this.tui.requestRender();
|
|
130
|
+
}
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (matchesKey(data, Key.down) || data === "j") {
|
|
134
|
+
this.scrollOffset++;
|
|
135
|
+
this.invalidate();
|
|
136
|
+
this.tui.requestRender();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Page up/down
|
|
141
|
+
if (matchesKey(data, Key.pageUp)) {
|
|
142
|
+
this.scrollOffset = Math.max(0, this.scrollOffset - 10);
|
|
143
|
+
this.invalidate();
|
|
144
|
+
this.tui.requestRender();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (matchesKey(data, Key.pageDown)) {
|
|
148
|
+
this.scrollOffset += 10;
|
|
149
|
+
this.invalidate();
|
|
150
|
+
this.tui.requestRender();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
invalidate(): void {
|
|
156
|
+
this.cachedWidth = undefined;
|
|
157
|
+
this.cachedLines = undefined;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
render(width: number): string[] {
|
|
161
|
+
if (this.cachedLines && this.cachedWidth === width) {
|
|
162
|
+
return this.cachedLines;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const theme = this.theme;
|
|
166
|
+
const boxWidth = Math.min(width - 2, 120);
|
|
167
|
+
const contentWidth = boxWidth - 6; // padding on each side
|
|
168
|
+
|
|
169
|
+
const horizontalLine = (count: number) => "─".repeat(count);
|
|
170
|
+
|
|
171
|
+
const boxLine = (content: string, leftPad: number = 2): string => {
|
|
172
|
+
const paddedContent = " ".repeat(leftPad) + content;
|
|
173
|
+
const contentLen = visibleWidth(paddedContent);
|
|
174
|
+
const rightPad = Math.max(0, boxWidth - contentLen - 2);
|
|
175
|
+
return theme.fg("border", "│") + paddedContent + " ".repeat(rightPad) + theme.fg("border", "│");
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const emptyBoxLine = (): string => {
|
|
179
|
+
return theme.fg("border", "│") + " ".repeat(boxWidth - 2) + theme.fg("border", "│");
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const padToWidth = (line: string): string => {
|
|
183
|
+
const len = visibleWidth(line);
|
|
184
|
+
return line + " ".repeat(Math.max(0, width - len));
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const lines: string[] = [];
|
|
188
|
+
|
|
189
|
+
// Top border
|
|
190
|
+
lines.push(padToWidth(theme.fg("accent", "╭" + horizontalLine(boxWidth - 2) + "╮")));
|
|
191
|
+
|
|
192
|
+
// Title
|
|
193
|
+
const title = theme.fg("accent", theme.bold("btw"));
|
|
194
|
+
lines.push(padToWidth(boxLine(title)));
|
|
195
|
+
|
|
196
|
+
// Separator
|
|
197
|
+
lines.push(padToWidth(theme.fg("accent", "├" + horizontalLine(boxWidth - 2) + "┤")));
|
|
198
|
+
|
|
199
|
+
// Question
|
|
200
|
+
const questionLabel = theme.fg("muted", "Q: ") + theme.fg("text", this.question);
|
|
201
|
+
const wrappedQuestion = wrapTextWithAnsi(questionLabel, contentWidth);
|
|
202
|
+
for (const line of wrappedQuestion) {
|
|
203
|
+
lines.push(padToWidth(boxLine(line)));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
lines.push(padToWidth(emptyBoxLine()));
|
|
207
|
+
|
|
208
|
+
// Separator between question and answer
|
|
209
|
+
lines.push(padToWidth(theme.fg("border", "├" + horizontalLine(boxWidth - 2) + "┤")));
|
|
210
|
+
lines.push(padToWidth(emptyBoxLine()));
|
|
211
|
+
|
|
212
|
+
// Answer — wrap and apply scroll
|
|
213
|
+
const answerLines: string[] = [];
|
|
214
|
+
for (const paragraph of this.answer.split("\n")) {
|
|
215
|
+
if (paragraph.trim() === "") {
|
|
216
|
+
answerLines.push("");
|
|
217
|
+
} else {
|
|
218
|
+
const wrapped = wrapTextWithAnsi(paragraph, contentWidth);
|
|
219
|
+
answerLines.push(...wrapped);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Clamp scroll offset
|
|
224
|
+
const termHeight = this.tui.height ?? 24;
|
|
225
|
+
const headerLines = lines.length;
|
|
226
|
+
const footerLines = 3; // separator + hint + bottom border
|
|
227
|
+
const maxVisibleAnswerLines = Math.max(1, termHeight - headerLines - footerLines - 2);
|
|
228
|
+
|
|
229
|
+
if (this.scrollOffset > Math.max(0, answerLines.length - maxVisibleAnswerLines)) {
|
|
230
|
+
this.scrollOffset = Math.max(0, answerLines.length - maxVisibleAnswerLines);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const visibleAnswerLines = answerLines.slice(
|
|
234
|
+
this.scrollOffset,
|
|
235
|
+
this.scrollOffset + maxVisibleAnswerLines,
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
for (const line of visibleAnswerLines) {
|
|
239
|
+
lines.push(padToWidth(boxLine(line)));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Scroll indicator
|
|
243
|
+
if (answerLines.length > maxVisibleAnswerLines) {
|
|
244
|
+
const scrollInfo = theme.fg("dim", `[${this.scrollOffset + 1}-${Math.min(this.scrollOffset + maxVisibleAnswerLines, answerLines.length)}/${answerLines.length}]`);
|
|
245
|
+
lines.push(padToWidth(boxLine(scrollInfo)));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
lines.push(padToWidth(emptyBoxLine()));
|
|
249
|
+
|
|
250
|
+
// Footer
|
|
251
|
+
lines.push(padToWidth(theme.fg("accent", "├" + horizontalLine(boxWidth - 2) + "┤")));
|
|
252
|
+
const hint = theme.fg("dim", "Esc/Space/q to dismiss · ↑↓/j/k scroll · PgUp/PgDn");
|
|
253
|
+
lines.push(padToWidth(boxLine(hint)));
|
|
254
|
+
lines.push(padToWidth(theme.fg("accent", "╰" + horizontalLine(boxWidth - 2) + "╯")));
|
|
255
|
+
|
|
256
|
+
this.cachedWidth = width;
|
|
257
|
+
this.cachedLines = lines;
|
|
258
|
+
return lines;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Main /btw command handler
|
|
264
|
+
*/
|
|
265
|
+
async function runBtwCommand(
|
|
266
|
+
args: string | undefined,
|
|
267
|
+
ctx: ExtensionCommandContext,
|
|
268
|
+
): Promise<void> {
|
|
269
|
+
// Validate args
|
|
270
|
+
const validation = validateBtwArgs(args);
|
|
271
|
+
if (!validation.valid) {
|
|
272
|
+
if (ctx.hasUI) {
|
|
273
|
+
ctx.ui.notify(validation.error!, "error");
|
|
274
|
+
} else {
|
|
275
|
+
console.error(validation.error);
|
|
276
|
+
}
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const question = validation.question!;
|
|
280
|
+
|
|
281
|
+
// Check for model
|
|
282
|
+
if (!ctx.model) {
|
|
283
|
+
const errorMsg = "No model selected. Use /model to select a model first.";
|
|
284
|
+
if (ctx.hasUI) {
|
|
285
|
+
ctx.ui.notify(errorMsg, "error");
|
|
286
|
+
} else {
|
|
287
|
+
console.error(errorMsg);
|
|
288
|
+
}
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Build conversation context
|
|
293
|
+
const sessionContext = ctx.sessionManager.buildSessionContext();
|
|
294
|
+
const messages = sessionContext.messages;
|
|
295
|
+
|
|
296
|
+
let conversationText = "";
|
|
297
|
+
if (messages.length > 0) {
|
|
298
|
+
const llmMessages = convertToLlm(messages);
|
|
299
|
+
conversationText = serializeConversation(llmMessages);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Select model (prefer cheap models)
|
|
303
|
+
const btwModel = await selectBtwModel(ctx.model, ctx.modelRegistry);
|
|
304
|
+
|
|
305
|
+
// Build LLM messages
|
|
306
|
+
const userMessage: UserMessage = {
|
|
307
|
+
role: "user",
|
|
308
|
+
content: [{ type: "text", text: buildBtwUserMessage(conversationText, question) }],
|
|
309
|
+
timestamp: Date.now(),
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
if (!ctx.hasUI) {
|
|
313
|
+
// Non-interactive mode: print answer to stdout
|
|
314
|
+
const requestAuth = await getRequestAuth(ctx.modelRegistry, btwModel);
|
|
315
|
+
const response = await complete(
|
|
316
|
+
btwModel,
|
|
317
|
+
{ systemPrompt: BTW_SYSTEM_PROMPT, messages: [userMessage] },
|
|
318
|
+
{ ...requestAuth },
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
if (response.stopReason === "error") {
|
|
322
|
+
console.error(response.errorMessage ?? "LLM error");
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const answerText = extractResponseText(response.content);
|
|
327
|
+
console.log(`\n> btw: ${question}\n`);
|
|
328
|
+
console.log(answerText);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Interactive mode: show loader, then overlay
|
|
333
|
+
|
|
334
|
+
// Step 1: Get the answer with a loading spinner
|
|
335
|
+
const answerResult = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
|
|
336
|
+
const loader = new BorderedLoader(
|
|
337
|
+
tui,
|
|
338
|
+
theme,
|
|
339
|
+
`Thinking (${btwModel.id})...`,
|
|
340
|
+
);
|
|
341
|
+
loader.onAbort = () => done(null);
|
|
342
|
+
|
|
343
|
+
const doQuery = async () => {
|
|
344
|
+
const requestAuth = await getRequestAuth(ctx.modelRegistry, btwModel);
|
|
345
|
+
const response = await complete(
|
|
346
|
+
btwModel,
|
|
347
|
+
{ systemPrompt: BTW_SYSTEM_PROMPT, messages: [userMessage] },
|
|
348
|
+
{ ...requestAuth, signal: loader.signal },
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
if (response.stopReason === "aborted") {
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (response.stopReason === "error") {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return extractResponseText(response.content);
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
doQuery()
|
|
363
|
+
.then(done)
|
|
364
|
+
.catch((err) => {
|
|
365
|
+
console.error("BTW query failed:", err);
|
|
366
|
+
done(null);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
return loader;
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
if (answerResult === null) {
|
|
373
|
+
ctx.ui.notify("Cancelled", "info");
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (answerResult.trim() === "") {
|
|
378
|
+
ctx.ui.notify("No answer received", "warning");
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Step 2: Show the answer in an overlay
|
|
383
|
+
await ctx.ui.custom<void>((tui, theme, _kb, done) => {
|
|
384
|
+
return new BtwOverlay(tui, theme, question, answerResult, done);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// Nothing persisted — fully ephemeral
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Main extension entry point
|
|
392
|
+
*/
|
|
393
|
+
export default function btwExtension(pi: ExtensionAPI) {
|
|
394
|
+
pi.registerCommand("btw", {
|
|
395
|
+
description: "Ask a quick side question without polluting conversation history",
|
|
396
|
+
handler: async (args, ctx) => {
|
|
397
|
+
await runBtwCommand(args, ctx);
|
|
398
|
+
},
|
|
399
|
+
});
|
|
400
|
+
}
|
|
@@ -25,7 +25,7 @@ import { DynamicBorder } from "@mariozechner/pi-coding-agent";
|
|
|
25
25
|
import {
|
|
26
26
|
Container,
|
|
27
27
|
fuzzyFilter,
|
|
28
|
-
|
|
28
|
+
getKeybindings,
|
|
29
29
|
Input,
|
|
30
30
|
matchesKey,
|
|
31
31
|
type SelectItem,
|
|
@@ -938,16 +938,16 @@ const showFileSelector = async (
|
|
|
938
938
|
}
|
|
939
939
|
}
|
|
940
940
|
|
|
941
|
-
const kb =
|
|
941
|
+
const kb = getKeybindings();
|
|
942
942
|
if (
|
|
943
|
-
kb.matches(data, "
|
|
944
|
-
kb.matches(data, "
|
|
945
|
-
kb.matches(data, "
|
|
946
|
-
kb.matches(data, "
|
|
943
|
+
kb.matches(data, "tui.select.up") ||
|
|
944
|
+
kb.matches(data, "tui.select.down") ||
|
|
945
|
+
kb.matches(data, "tui.select.confirm") ||
|
|
946
|
+
kb.matches(data, "tui.select.cancel")
|
|
947
947
|
) {
|
|
948
948
|
if (selectList) {
|
|
949
949
|
selectList.handleInput(data);
|
|
950
|
-
} else if (kb.matches(data, "
|
|
950
|
+
} else if (kb.matches(data, "tui.select.cancel")) {
|
|
951
951
|
done(null);
|
|
952
952
|
}
|
|
953
953
|
tui.requestRender();
|
|
@@ -67,7 +67,7 @@ async function listSessions(ctx: ExtensionCommandContext): Promise<SessionInfoLi
|
|
|
67
67
|
container.addChild(new DynamicBorder(borderColor));
|
|
68
68
|
container.addChild(loader);
|
|
69
69
|
container.addChild(new Spacer(1));
|
|
70
|
-
container.addChild(new Text(keyHint("
|
|
70
|
+
container.addChild(new Text(keyHint("tui.select.cancel", "cancel"), 1, 0));
|
|
71
71
|
container.addChild(new Spacer(1));
|
|
72
72
|
container.addChild(new DynamicBorder(borderColor));
|
|
73
73
|
|
|
@@ -48,7 +48,7 @@ import {
|
|
|
48
48
|
Text,
|
|
49
49
|
TUI,
|
|
50
50
|
fuzzyMatch,
|
|
51
|
-
|
|
51
|
+
getKeybindings,
|
|
52
52
|
matchesKey,
|
|
53
53
|
truncateToWidth,
|
|
54
54
|
visibleWidth,
|
|
@@ -397,25 +397,25 @@ class TodoSelectorComponent extends Container implements Focusable {
|
|
|
397
397
|
}
|
|
398
398
|
|
|
399
399
|
handleInput(keyData: string): void {
|
|
400
|
-
const kb =
|
|
401
|
-
if (kb.matches(keyData, "
|
|
400
|
+
const kb = getKeybindings();
|
|
401
|
+
if (kb.matches(keyData, "tui.select.up")) {
|
|
402
402
|
if (this.filteredTodos.length === 0) return;
|
|
403
403
|
this.selectedIndex = this.selectedIndex === 0 ? this.filteredTodos.length - 1 : this.selectedIndex - 1;
|
|
404
404
|
this.updateList();
|
|
405
405
|
return;
|
|
406
406
|
}
|
|
407
|
-
if (kb.matches(keyData, "
|
|
407
|
+
if (kb.matches(keyData, "tui.select.down")) {
|
|
408
408
|
if (this.filteredTodos.length === 0) return;
|
|
409
409
|
this.selectedIndex = this.selectedIndex === this.filteredTodos.length - 1 ? 0 : this.selectedIndex + 1;
|
|
410
410
|
this.updateList();
|
|
411
411
|
return;
|
|
412
412
|
}
|
|
413
|
-
if (kb.matches(keyData, "
|
|
413
|
+
if (kb.matches(keyData, "tui.select.confirm")) {
|
|
414
414
|
const selected = this.filteredTodos[this.selectedIndex];
|
|
415
415
|
if (selected) this.onSelectCallback(selected);
|
|
416
416
|
return;
|
|
417
417
|
}
|
|
418
|
-
if (kb.matches(keyData, "
|
|
418
|
+
if (kb.matches(keyData, "tui.select.cancel")) {
|
|
419
419
|
this.onCancelCallback();
|
|
420
420
|
return;
|
|
421
421
|
}
|
|
@@ -573,28 +573,28 @@ class TodoDetailOverlayComponent {
|
|
|
573
573
|
}
|
|
574
574
|
|
|
575
575
|
handleInput(keyData: string): void {
|
|
576
|
-
const kb =
|
|
577
|
-
if (kb.matches(keyData, "
|
|
576
|
+
const kb = getKeybindings();
|
|
577
|
+
if (kb.matches(keyData, "tui.select.cancel")) {
|
|
578
578
|
this.onAction("back");
|
|
579
579
|
return;
|
|
580
580
|
}
|
|
581
|
-
if (kb.matches(keyData, "
|
|
581
|
+
if (kb.matches(keyData, "tui.select.confirm")) {
|
|
582
582
|
this.onAction("work");
|
|
583
583
|
return;
|
|
584
584
|
}
|
|
585
|
-
if (kb.matches(keyData, "
|
|
585
|
+
if (kb.matches(keyData, "tui.select.up")) {
|
|
586
586
|
this.scrollBy(-1);
|
|
587
587
|
return;
|
|
588
588
|
}
|
|
589
|
-
if (kb.matches(keyData, "
|
|
589
|
+
if (kb.matches(keyData, "tui.select.down")) {
|
|
590
590
|
this.scrollBy(1);
|
|
591
591
|
return;
|
|
592
592
|
}
|
|
593
|
-
if (kb.matches(keyData, "
|
|
593
|
+
if (kb.matches(keyData, "tui.select.pageUp")) {
|
|
594
594
|
this.scrollBy(-this.viewHeight || -1);
|
|
595
595
|
return;
|
|
596
596
|
}
|
|
597
|
-
if (kb.matches(keyData, "
|
|
597
|
+
if (kb.matches(keyData, "tui.select.pageDown")) {
|
|
598
598
|
this.scrollBy(this.viewHeight || 1);
|
|
599
599
|
return;
|
|
600
600
|
}
|
|
@@ -1256,7 +1256,7 @@ function renderTodoDetail(theme: Theme, todo: TodoRecord, expanded: boolean): st
|
|
|
1256
1256
|
}
|
|
1257
1257
|
|
|
1258
1258
|
function appendExpandHint(theme: Theme, text: string): string {
|
|
1259
|
-
return `${text}\n${theme.fg("dim", `(${keyHint("
|
|
1259
|
+
return `${text}\n${theme.fg("dim", `(${keyHint("app.tools.expand", "to expand")})`)}`;
|
|
1260
1260
|
}
|
|
1261
1261
|
|
|
1262
1262
|
async function ensureTodoExists(filePath: string, id: string): Promise<TodoRecord | null> {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-agent-extensions",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"description": "Collection of extensions for pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -46,7 +46,8 @@
|
|
|
46
46
|
"./extensions/session-breakdown/index.ts",
|
|
47
47
|
"./extensions/todos/index.ts",
|
|
48
48
|
"./extensions/whimsical/index.ts",
|
|
49
|
-
"./extensions/nvidia-nim/index.ts"
|
|
49
|
+
"./extensions/nvidia-nim/index.ts",
|
|
50
|
+
"./extensions/btw/index.ts"
|
|
50
51
|
],
|
|
51
52
|
"themes": [
|
|
52
53
|
"./themes/nightowl.json",
|