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 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("session_switch", (event, ctx) => {
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
- getEditorKeybindings,
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 = getEditorKeybindings();
941
+ const kb = getKeybindings();
942
942
  if (
943
- kb.matches(data, "selectUp") ||
944
- kb.matches(data, "selectDown") ||
945
- kb.matches(data, "selectConfirm") ||
946
- kb.matches(data, "selectCancel")
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, "selectCancel")) {
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("selectCancel", "cancel"), 1, 0));
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
- getEditorKeybindings,
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 = getEditorKeybindings();
401
- if (kb.matches(keyData, "selectUp")) {
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, "selectDown")) {
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, "selectConfirm")) {
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, "selectCancel")) {
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 = getEditorKeybindings();
577
- if (kb.matches(keyData, "selectCancel")) {
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, "selectConfirm")) {
581
+ if (kb.matches(keyData, "tui.select.confirm")) {
582
582
  this.onAction("work");
583
583
  return;
584
584
  }
585
- if (kb.matches(keyData, "selectUp")) {
585
+ if (kb.matches(keyData, "tui.select.up")) {
586
586
  this.scrollBy(-1);
587
587
  return;
588
588
  }
589
- if (kb.matches(keyData, "selectDown")) {
589
+ if (kb.matches(keyData, "tui.select.down")) {
590
590
  this.scrollBy(1);
591
591
  return;
592
592
  }
593
- if (kb.matches(keyData, "selectPageUp")) {
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, "selectPageDown")) {
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("expandTools", "to expand")})`)}`;
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",
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",