@narumitw/pi-btw 0.13.1 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -12
- package/package.json +2 -2
- package/src/btw.ts +157 -19
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ Use it when you want to ask a temporary question, inspect context, or get a shor
|
|
|
11
11
|
- Adds a `/btw <question>` command to Pi.
|
|
12
12
|
- Answers side questions in a temporary, scrollable UI.
|
|
13
13
|
- Uses the current session branch as context.
|
|
14
|
+
- Uses Pi's current model or an independent model selected in `pi-btw.json`.
|
|
14
15
|
- Inherits Pi's current thinking level or uses a fixed level from `pi-btw.json`.
|
|
15
16
|
- Does not append the side question or answer to the main conversation.
|
|
16
17
|
- Works as an independently installable npm Pi extension package.
|
|
@@ -52,13 +53,10 @@ Long answers open in a pager-style view. Use `↑`/`↓` or `k`/`j` to scroll by
|
|
|
52
53
|
`Ctrl+U`/`Ctrl+D` to scroll by half page, and `Home`/`End` to jump. Close with
|
|
53
54
|
`q`, `Esc`, `Enter`, or `Ctrl+C`.
|
|
54
55
|
|
|
55
|
-
## ⚙️
|
|
56
|
+
## ⚙️ Model and thinking level
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
not read or change `defaultThinkingLevel` directly.
|
|
60
|
-
|
|
61
|
-
To request a fixed level for side questions, create:
|
|
58
|
+
By default, `/btw` uses the current session model. To use an independent model for side
|
|
59
|
+
questions, create:
|
|
62
60
|
|
|
63
61
|
```text
|
|
64
62
|
$PI_CODING_AGENT_DIR/pi-btw.json
|
|
@@ -69,18 +67,29 @@ setting; pi-btw does not add any environment variables.
|
|
|
69
67
|
|
|
70
68
|
```json
|
|
71
69
|
{
|
|
70
|
+
"model": "anthropic/claude-sonnet-4-5",
|
|
72
71
|
"thinkingLevel": "low"
|
|
73
72
|
}
|
|
74
73
|
```
|
|
75
74
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
75
|
+
The `model` value uses `provider/model-id` format. Only the first `/` is the separator, so
|
|
76
|
+
model IDs may contain additional slashes, such as `openrouter/anthropic/claude-sonnet`.
|
|
77
|
+
The configured model must exist in Pi's model registry and have usable credentials. If it
|
|
78
|
+
cannot be found or authenticated, pi-btw warns and falls back to the current session model.
|
|
79
|
+
If neither model is available, `/btw` reports an error and stops. This selection affects only
|
|
80
|
+
`/btw`; it does not change the main session model.
|
|
81
|
+
|
|
82
|
+
Pi calls its reasoning setting the **thinking level**. By default, `/btw` inherits the
|
|
83
|
+
current runtime level, including changes made through `/settings` or `Shift+Tab`. It does
|
|
84
|
+
not read or change `defaultThinkingLevel` directly. Supported fixed values are `off`,
|
|
85
|
+
`minimal`, `low`, `medium`, `high`, and `xhigh`. The selected value applies to the model
|
|
86
|
+
actually used by `/btw` and does not change the main session. Pi's provider layer may clamp
|
|
87
|
+
a requested level when that model does not support it.
|
|
79
88
|
|
|
80
|
-
The settings file is optional and is never created automatically. A missing file, `{}`,
|
|
81
|
-
|
|
89
|
+
The settings file is optional and is never created automatically. A missing file, `{}`, or
|
|
90
|
+
omitted fields silently inherit the current Pi model and thinking level. The file is read for
|
|
82
91
|
each `/btw` invocation, so edits apply to the next side question without `/reload`. Invalid
|
|
83
|
-
or unreadable settings produce a warning and fall back to the current Pi
|
|
92
|
+
or unreadable settings produce a warning and fall back to the current Pi defaults.
|
|
84
93
|
|
|
85
94
|
## 🧠 Why use pi-btw?
|
|
86
95
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@narumitw/pi-btw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Pi extension that adds a /btw side-question command.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"typecheck": "tsc --noEmit"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
|
-
"@biomejs/biome": "2.5.
|
|
31
|
+
"@biomejs/biome": "2.5.3",
|
|
32
32
|
"@earendil-works/pi-ai": "0.80.3",
|
|
33
33
|
"@earendil-works/pi-coding-agent": "0.80.3",
|
|
34
34
|
"@earendil-works/pi-tui": "0.80.3",
|
package/src/btw.ts
CHANGED
|
@@ -80,6 +80,7 @@ export const BTW_THINKING_LEVELS = [
|
|
|
80
80
|
export type BtwThinkingLevel = (typeof BTW_THINKING_LEVELS)[number];
|
|
81
81
|
|
|
82
82
|
export interface BtwSettings {
|
|
83
|
+
model?: string;
|
|
83
84
|
thinkingLevel?: BtwThinkingLevel;
|
|
84
85
|
}
|
|
85
86
|
|
|
@@ -94,11 +95,31 @@ interface LoadBtwThinkingLevelOptions {
|
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
interface SideQuestionAuth {
|
|
97
|
-
apiKey
|
|
98
|
+
apiKey?: string;
|
|
98
99
|
headers?: Record<string, string>;
|
|
99
100
|
env?: Record<string, string>;
|
|
100
101
|
}
|
|
101
102
|
|
|
103
|
+
interface BtwModelRegistry {
|
|
104
|
+
find(provider: string, modelId: string): Model<Api> | undefined;
|
|
105
|
+
getApiKeyAndHeaders(model: Model<Api>): Promise<
|
|
106
|
+
| { ok: true; apiKey?: string; headers?: Record<string, string>; env?: Record<string, string> }
|
|
107
|
+
| { ok: false; error: string }
|
|
108
|
+
>;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
interface ResolveBtwModelOptions {
|
|
112
|
+
settings: BtwSettings;
|
|
113
|
+
currentModel: Model<Api> | undefined;
|
|
114
|
+
modelRegistry: BtwModelRegistry;
|
|
115
|
+
warn?: (message: string) => void;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
interface ResolvedBtwModel {
|
|
119
|
+
model: Model<Api>;
|
|
120
|
+
auth: SideQuestionAuth;
|
|
121
|
+
}
|
|
122
|
+
|
|
102
123
|
interface CompleteSideQuestionOptions {
|
|
103
124
|
model: Model<Api>;
|
|
104
125
|
question: string;
|
|
@@ -111,10 +132,83 @@ interface CompleteSideQuestionOptions {
|
|
|
111
132
|
|
|
112
133
|
export function normalizeBtwSettings(value: unknown): BtwSettings | undefined {
|
|
113
134
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
|
|
114
|
-
if (!Object.hasOwn(value, "thinkingLevel")) return {};
|
|
115
135
|
|
|
116
|
-
const
|
|
117
|
-
|
|
136
|
+
const settings: BtwSettings = {};
|
|
137
|
+
if (Object.hasOwn(value, "model")) {
|
|
138
|
+
const model = Reflect.get(value, "model");
|
|
139
|
+
if (typeof model !== "string" || !parseBtwModelReference(model)) return undefined;
|
|
140
|
+
settings.model = model;
|
|
141
|
+
}
|
|
142
|
+
if (Object.hasOwn(value, "thinkingLevel")) {
|
|
143
|
+
const thinkingLevel = Reflect.get(value, "thinkingLevel");
|
|
144
|
+
if (!isBtwThinkingLevel(thinkingLevel)) return undefined;
|
|
145
|
+
settings.thinkingLevel = thinkingLevel;
|
|
146
|
+
}
|
|
147
|
+
return settings;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function parseBtwModelReference(
|
|
151
|
+
reference: string,
|
|
152
|
+
): { provider: string; modelId: string } | undefined {
|
|
153
|
+
if (/\s/.test(reference)) return undefined;
|
|
154
|
+
const separator = reference.indexOf("/");
|
|
155
|
+
if (separator <= 0 || separator === reference.length - 1) return undefined;
|
|
156
|
+
return { provider: reference.slice(0, separator), modelId: reference.slice(separator + 1) };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function resolveBtwModel({
|
|
160
|
+
settings,
|
|
161
|
+
currentModel,
|
|
162
|
+
modelRegistry,
|
|
163
|
+
warn,
|
|
164
|
+
}: ResolveBtwModelOptions): Promise<ResolvedBtwModel | undefined> {
|
|
165
|
+
if (settings.model) {
|
|
166
|
+
const fallback = currentModel
|
|
167
|
+
? `${currentModel.provider}/${currentModel.id}`
|
|
168
|
+
: "the current model";
|
|
169
|
+
const reference = parseBtwModelReference(settings.model)!;
|
|
170
|
+
const configuredModel = modelRegistry.find(reference.provider, reference.modelId);
|
|
171
|
+
if (!configuredModel) {
|
|
172
|
+
warn?.(`pi-btw model ${settings.model} was not found; falling back to ${fallback}.`);
|
|
173
|
+
} else {
|
|
174
|
+
const sameAsCurrent =
|
|
175
|
+
configuredModel === currentModel ||
|
|
176
|
+
(configuredModel.provider === currentModel?.provider && configuredModel.id === currentModel.id);
|
|
177
|
+
const fallbackAction = sameAsCurrent
|
|
178
|
+
? "no distinct current model is available"
|
|
179
|
+
: `falling back to ${fallback}`;
|
|
180
|
+
try {
|
|
181
|
+
const auth = await modelRegistry.getApiKeyAndHeaders(configuredModel);
|
|
182
|
+
if (auth.ok && hasRequestAuth(auth)) return { model: configuredModel, auth };
|
|
183
|
+
const reason = auth.ok ? "has no request credentials" : auth.error;
|
|
184
|
+
warn?.(
|
|
185
|
+
`pi-btw model ${settings.model} is unavailable (${reason}); ${fallbackAction}.`,
|
|
186
|
+
);
|
|
187
|
+
} catch (error: unknown) {
|
|
188
|
+
warn?.(
|
|
189
|
+
`pi-btw model ${settings.model} credentials failed (${formatError(error)}); ${fallbackAction}.`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
if (sameAsCurrent) return undefined;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (!currentModel) return undefined;
|
|
197
|
+
try {
|
|
198
|
+
const auth = await modelRegistry.getApiKeyAndHeaders(currentModel);
|
|
199
|
+
if (auth.ok && hasRequestAuth(auth)) return { model: currentModel, auth };
|
|
200
|
+
} catch {
|
|
201
|
+
// The caller reports the final lack of an available model.
|
|
202
|
+
}
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function hasRequestAuth(auth: SideQuestionAuth): boolean {
|
|
207
|
+
return Boolean(
|
|
208
|
+
auth.apiKey ||
|
|
209
|
+
(auth.headers && Object.keys(auth.headers).length > 0) ||
|
|
210
|
+
(auth.env && Object.keys(auth.env).length > 0),
|
|
211
|
+
);
|
|
118
212
|
}
|
|
119
213
|
|
|
120
214
|
export async function readBtwSettings(
|
|
@@ -148,7 +242,7 @@ export async function loadBtwThinkingLevel(
|
|
|
148
242
|
}
|
|
149
243
|
|
|
150
244
|
options.warn?.(
|
|
151
|
-
`pi-btw settings ignored: ${settings.reason}; expected
|
|
245
|
+
`pi-btw settings ignored: ${settings.reason}; expected optional model "provider/model-id" and thinkingLevel "${BTW_THINKING_LEVELS.join('" | "')}". Using current Pi thinking level.`,
|
|
152
246
|
);
|
|
153
247
|
return currentThinkingLevel;
|
|
154
248
|
}
|
|
@@ -233,15 +327,26 @@ export default function btw(pi: ExtensionAPI) {
|
|
|
233
327
|
return;
|
|
234
328
|
}
|
|
235
329
|
|
|
236
|
-
|
|
237
|
-
|
|
330
|
+
const settingsResult = await readBtwSettings();
|
|
331
|
+
let settings: BtwSettings = {};
|
|
332
|
+
if (settingsResult.kind === "loaded") {
|
|
333
|
+
settings = settingsResult.settings;
|
|
334
|
+
} else if (settingsResult.kind === "invalid") {
|
|
335
|
+
ctx.ui.notify(`pi-btw settings ignored: ${settingsResult.reason}`, "warning");
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const resolution = await resolveBtwModelWithLoader(settings, ctx);
|
|
339
|
+
if (resolution.kind === "cancelled") {
|
|
340
|
+
ctx.ui.notify("Cancelled", "info");
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
if (resolution.kind === "unavailable") {
|
|
344
|
+
ctx.ui.notify("No available model for /btw", "error");
|
|
238
345
|
return;
|
|
239
346
|
}
|
|
240
347
|
|
|
241
|
-
const thinkingLevel =
|
|
242
|
-
|
|
243
|
-
});
|
|
244
|
-
const answer = await askSideQuestion(question, thinkingLevel, ctx);
|
|
348
|
+
const thinkingLevel = settings.thinkingLevel ?? pi.getThinkingLevel();
|
|
349
|
+
const answer = await askSideQuestion(question, resolution.selected, thinkingLevel, ctx);
|
|
245
350
|
if (answer === undefined) {
|
|
246
351
|
ctx.ui.notify("Cancelled", "info");
|
|
247
352
|
return;
|
|
@@ -252,28 +357,61 @@ export default function btw(pi: ExtensionAPI) {
|
|
|
252
357
|
});
|
|
253
358
|
}
|
|
254
359
|
|
|
360
|
+
type ModelResolutionOutcome =
|
|
361
|
+
| { kind: "cancelled" }
|
|
362
|
+
| { kind: "unavailable" }
|
|
363
|
+
| { kind: "selected"; selected: ResolvedBtwModel };
|
|
364
|
+
|
|
365
|
+
async function resolveBtwModelWithLoader(
|
|
366
|
+
settings: BtwSettings,
|
|
367
|
+
ctx: ExtensionCommandContext,
|
|
368
|
+
): Promise<ModelResolutionOutcome> {
|
|
369
|
+
return ctx.ui.custom<ModelResolutionOutcome>((tui, theme, _keybindings, done) => {
|
|
370
|
+
const loader = new BorderedLoader(tui, theme, "Resolving /btw model credentials...");
|
|
371
|
+
let cancelled = false;
|
|
372
|
+
loader.onAbort = () => {
|
|
373
|
+
cancelled = true;
|
|
374
|
+
done({ kind: "cancelled" });
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
resolveBtwModel({
|
|
378
|
+
settings,
|
|
379
|
+
currentModel: ctx.model,
|
|
380
|
+
modelRegistry: ctx.modelRegistry,
|
|
381
|
+
warn: (message) => {
|
|
382
|
+
if (!cancelled) ctx.ui.notify(message, "warning");
|
|
383
|
+
},
|
|
384
|
+
}).then((selected) => {
|
|
385
|
+
if (cancelled) return;
|
|
386
|
+
done(selected ? { kind: "selected", selected } : { kind: "unavailable" });
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
return loader;
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
255
393
|
async function askSideQuestion(
|
|
256
394
|
question: string,
|
|
395
|
+
selected: ResolvedBtwModel,
|
|
257
396
|
thinkingLevel: BtwThinkingLevel,
|
|
258
397
|
ctx: ExtensionCommandContext,
|
|
259
398
|
): Promise<string | undefined> {
|
|
260
399
|
return ctx.ui.custom<string | undefined>((tui, theme, _keybindings, done) => {
|
|
261
|
-
const loader = new BorderedLoader(
|
|
400
|
+
const loader = new BorderedLoader(
|
|
401
|
+
tui,
|
|
402
|
+
theme,
|
|
403
|
+
`Answering /btw with ${selected.model.provider}/${selected.model.id}...`,
|
|
404
|
+
);
|
|
262
405
|
loader.onAbort = () => done(undefined);
|
|
263
406
|
|
|
264
407
|
const ask = async () => {
|
|
265
|
-
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(ctx.model!);
|
|
266
|
-
if (!auth.ok || !auth.apiKey) {
|
|
267
|
-
throw new Error(auth.ok ? `No API key for ${ctx.model!.provider}` : auth.error);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
408
|
const conversationContext = buildConversationContext(ctx.sessionManager.getBranch());
|
|
271
409
|
const response = await completeSideQuestion({
|
|
272
|
-
model:
|
|
410
|
+
model: selected.model,
|
|
273
411
|
question,
|
|
274
412
|
conversationContext,
|
|
275
413
|
thinkingLevel,
|
|
276
|
-
auth:
|
|
414
|
+
auth: selected.auth,
|
|
277
415
|
signal: loader.signal,
|
|
278
416
|
});
|
|
279
417
|
|