@herbertgao/pi-extensions 2026.9.8 → 2026.9.9
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 +8 -4
- package/THIRD_PARTY_NOTICES.md +52 -0
- package/node_modules/@herbertgao/pi-bark/package.json +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +4 -4
- package/node_modules/@herbertgao/resume-from/package.json +2 -2
- package/node_modules/@narumitw/pi-btw/README.md +6 -1
- package/node_modules/@narumitw/pi-btw/dist/index.ts +209 -23
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +4 -4
- package/node_modules/@narumitw/pi-btw/package.json +1 -1
- package/node_modules/@narumitw/pi-btw/src/btw.ts +1 -12
- package/node_modules/@narumitw/pi-btw/src/menu.ts +244 -15
- package/node_modules/@narumitw/pi-btw/src/settings.ts +5 -0
- package/node_modules/pi-jev-auto-mode/CHANGELOG.md +110 -0
- package/node_modules/pi-jev-auto-mode/LICENSE +21 -0
- package/node_modules/pi-jev-auto-mode/README.md +285 -0
- package/node_modules/pi-jev-auto-mode/SECURITY.md +32 -0
- package/node_modules/pi-jev-auto-mode/docs/calibration.md +154 -0
- package/node_modules/pi-jev-auto-mode/docs/design.md +164 -0
- package/node_modules/pi-jev-auto-mode/docs/security.md +124 -0
- package/node_modules/pi-jev-auto-mode/index.ts +1 -0
- package/node_modules/pi-jev-auto-mode/package.json +72 -0
- package/node_modules/pi-jev-auto-mode/src/call.ts +180 -0
- package/node_modules/pi-jev-auto-mode/src/decide.ts +86 -0
- package/node_modules/pi-jev-auto-mode/src/extension.ts +862 -0
- package/node_modules/pi-jev-auto-mode/src/intent.ts +71 -0
- package/node_modules/pi-jev-auto-mode/src/jev/availability.ts +53 -0
- package/node_modules/pi-jev-auto-mode/src/jev/criteria.ts +19 -0
- package/node_modules/pi-jev-auto-mode/src/jev/decide.ts +187 -0
- package/node_modules/pi-jev-auto-mode/src/jev/engine.ts +165 -0
- package/node_modules/pi-jev-auto-mode/src/jev/index.ts +20 -0
- package/node_modules/pi-jev-auto-mode/src/jev/questions.ts +273 -0
- package/node_modules/pi-jev-auto-mode/src/jev/response.ts +64 -0
- package/node_modules/pi-jev-auto-mode/src/jev/state.ts +20 -0
- package/node_modules/pi-jev-auto-mode/src/jev/transport.ts +117 -0
- package/node_modules/pi-jev-auto-mode/src/jev/types.ts +46 -0
- package/node_modules/pi-jev-auto-mode/src/policy.ts +558 -0
- package/node_modules/pi-jev-auto-mode/src/records.ts +118 -0
- package/node_modules/pi-jev-auto-mode/src/settings.ts +327 -0
- package/node_modules/pi-jev-auto-mode/src/ui.ts +233 -0
- package/node_modules/pi-lens/CHANGELOG.md +45 -0
- package/node_modules/pi-lens/dist/clients/actionable-warnings.js +21 -9
- package/node_modules/pi-lens/dist/clients/cascade-format.js +24 -0
- package/node_modules/pi-lens/dist/clients/extension-log.js +48 -1
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +11 -12
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +3 -0
- package/node_modules/pi-lens/dist/clients/lsp/index.js +41 -7
- package/node_modules/pi-lens/dist/clients/mcp/analyze.js +9 -2
- package/node_modules/pi-lens/dist/clients/path-utils.js +51 -9
- package/node_modules/pi-lens/dist/clients/persistent-reverify.js +271 -0
- package/node_modules/pi-lens/dist/clients/pipeline.js +15 -1
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +124 -7
- package/node_modules/pi-lens/dist/clients/skills-resolver.js +38 -2
- package/node_modules/pi-lens/dist/clients/widget-state.js +152 -6
- package/node_modules/pi-lens/dist/index.js +729 -344
- package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +14 -2
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +84 -17
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +32 -5
- package/node_modules/pi-lens/package.json +1 -1
- package/node_modules/pi-typesafe/LICENSE +21 -0
- package/node_modules/pi-typesafe/README.md +153 -0
- package/node_modules/pi-typesafe/dist/ask.d.ts +32 -0
- package/node_modules/pi-typesafe/dist/ask.js +25 -0
- package/node_modules/pi-typesafe/dist/auth.d.ts +56 -0
- package/node_modules/pi-typesafe/dist/auth.js +114 -0
- package/node_modules/pi-typesafe/dist/batch.d.ts +74 -0
- package/node_modules/pi-typesafe/dist/batch.js +116 -0
- package/node_modules/pi-typesafe/dist/calibrate.d.ts +106 -0
- package/node_modules/pi-typesafe/dist/calibrate.js +157 -0
- package/node_modules/pi-typesafe/dist/client.d.ts +69 -0
- package/node_modules/pi-typesafe/dist/client.js +188 -0
- package/node_modules/pi-typesafe/dist/credentials.d.ts +46 -0
- package/node_modules/pi-typesafe/dist/credentials.js +112 -0
- package/node_modules/pi-typesafe/dist/errors.d.ts +9 -0
- package/node_modules/pi-typesafe/dist/errors.js +31 -0
- package/node_modules/pi-typesafe/dist/extension.d.ts +3 -0
- package/node_modules/pi-typesafe/dist/extension.js +222 -0
- package/node_modules/pi-typesafe/dist/index.d.ts +18 -0
- package/node_modules/pi-typesafe/dist/index.js +9 -0
- package/node_modules/pi-typesafe/dist/key-prompt.d.ts +3 -0
- package/node_modules/pi-typesafe/dist/key-prompt.js +43 -0
- package/node_modules/pi-typesafe/dist/login.d.ts +27 -0
- package/node_modules/pi-typesafe/dist/login.js +38 -0
- package/node_modules/pi-typesafe/dist/schema.d.ts +43 -0
- package/node_modules/pi-typesafe/dist/schema.js +152 -0
- package/node_modules/pi-typesafe/dist/ui.d.ts +3 -0
- package/node_modules/pi-typesafe/dist/ui.js +3 -0
- package/node_modules/pi-typesafe/dist/usage.d.ts +73 -0
- package/node_modules/pi-typesafe/dist/usage.js +182 -0
- package/node_modules/pi-typesafe/examples/decision-extension.ts +41 -0
- package/node_modules/pi-typesafe/extensions/index.js +2 -0
- package/node_modules/pi-typesafe/package.json +89 -0
- package/package.json +15 -8
|
@@ -357,19 +357,8 @@ async function showCommandMenuForBtw(
|
|
|
357
357
|
ctx: ExtensionCommandContext,
|
|
358
358
|
resumeThreads: readonly BtwResumeThreadSummary[],
|
|
359
359
|
): Promise<BtwCommandMenuResult> {
|
|
360
|
-
const currentModel = ctx.model;
|
|
361
|
-
const availableModels = ctx.modelRegistry.getAll();
|
|
362
|
-
const currentThinkingLevel = pi.getThinkingLevel();
|
|
363
|
-
const loaded = await readBtwSettings();
|
|
364
|
-
const settings = loaded.kind === "loaded" ? loaded.settings : {};
|
|
365
|
-
const configured = settings.model ? parseBtwModelReference(settings.model) : undefined;
|
|
366
|
-
const configuredModel = configured
|
|
367
|
-
? availableModels.find((model) => model.provider === configured.provider && model.id === configured.modelId)
|
|
368
|
-
: undefined;
|
|
369
|
-
const model = configuredModel ?? currentModel;
|
|
370
360
|
return showBtwCommandMenu(ctx, {
|
|
371
|
-
currentThinkingLevel,
|
|
372
|
-
availableThinkingLevels: model ? getSupportedThinkingLevels(model) : BTW_THINKING_LEVELS,
|
|
361
|
+
currentThinkingLevel: pi.getThinkingLevel(),
|
|
373
362
|
resumeThreads,
|
|
374
363
|
});
|
|
375
364
|
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Api, getSupportedThinkingLevels, type Model } from "@earendil-works/pi-ai";
|
|
2
|
+
import {
|
|
3
|
+
BorderedLoader,
|
|
4
|
+
type ExtensionCommandContext,
|
|
5
|
+
type KeybindingsManager,
|
|
6
|
+
type Theme,
|
|
7
|
+
} from "@earendil-works/pi-coding-agent";
|
|
2
8
|
import type { Component, TUI } from "@earendil-works/pi-tui";
|
|
3
9
|
import type { MenuContext, RunMenuResult } from "@narumitw/pi-tui-kit";
|
|
4
10
|
import {
|
|
@@ -14,6 +20,7 @@ import {
|
|
|
14
20
|
btwSettingsPath,
|
|
15
21
|
effectiveFullscreenCopyOnSelect,
|
|
16
22
|
effectiveRememberThinkingLevelChanges,
|
|
23
|
+
parseBtwModelReference,
|
|
17
24
|
readBtwSettings,
|
|
18
25
|
type UpdateBtwSettingsOptions,
|
|
19
26
|
updateBtwSettings,
|
|
@@ -35,7 +42,11 @@ export interface BtwResumeThreadSummary {
|
|
|
35
42
|
|
|
36
43
|
export interface ShowBtwCommandMenuOptions {
|
|
37
44
|
currentThinkingLevel: BtwThinkingLevel;
|
|
38
|
-
|
|
45
|
+
/** Deterministic test override; production derives levels from each selected model. */
|
|
46
|
+
availableThinkingLevels?: readonly BtwThinkingLevel[];
|
|
47
|
+
availableModels?: readonly Model<Api>[];
|
|
48
|
+
currentModel?: Model<Api>;
|
|
49
|
+
scopedModels?: ExtensionCommandContext["scopedModels"];
|
|
39
50
|
resumeThreads?: readonly BtwResumeThreadSummary[];
|
|
40
51
|
settingsPath?: string;
|
|
41
52
|
readSettings?: typeof readBtwSettings;
|
|
@@ -44,11 +55,13 @@ export interface ShowBtwCommandMenuOptions {
|
|
|
44
55
|
|
|
45
56
|
export type BtwCommandMenuResult = "start" | "tree" | "closed" | { kind: "resume"; threadId: string };
|
|
46
57
|
|
|
47
|
-
type BtwMenuScreen = "main" | "resume" | "settings" | "invalid" | "shortcut" | "shortcut-input";
|
|
58
|
+
type BtwMenuScreen = "main" | "resume" | "settings" | "model" | "invalid" | "shortcut" | "shortcut-input";
|
|
48
59
|
type BtwMenuAction =
|
|
49
60
|
| "start"
|
|
50
61
|
| "start-tree"
|
|
51
62
|
| "resume"
|
|
63
|
+
| "open-model"
|
|
64
|
+
| "set-model"
|
|
52
65
|
| "set-thinking"
|
|
53
66
|
| "set-remember"
|
|
54
67
|
| "set-fullscreen-copy"
|
|
@@ -70,15 +83,32 @@ export async function showBtwCommandMenu(
|
|
|
70
83
|
options: ShowBtwCommandMenuOptions,
|
|
71
84
|
): Promise<BtwCommandMenuResult> {
|
|
72
85
|
if (ctx.mode !== "tui") return "closed";
|
|
73
|
-
const { defineMenu, runMenu } = await import("@narumitw/pi-tui-kit");
|
|
86
|
+
const { defineMenu, runMenu, sanitizeTerminalText } = await import("@narumitw/pi-tui-kit");
|
|
74
87
|
if (ctx.signal?.aborted) return "closed";
|
|
75
88
|
const settingsPath = options.settingsPath ?? btwSettingsPath();
|
|
76
89
|
const readSettings = options.readSettings ?? readBtwSettings;
|
|
77
90
|
const updateSettings = options.updateSettings ?? updateBtwSettings;
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
91
|
+
const getAvailable = ctx.modelRegistry.getAvailable;
|
|
92
|
+
const allAvailableModels = deduplicateModels(
|
|
93
|
+
options.availableModels ??
|
|
94
|
+
(typeof getAvailable === "function" ? getAvailable.call(ctx.modelRegistry) : ctx.modelRegistry.getAll()),
|
|
95
|
+
);
|
|
96
|
+
const currentModel = options.currentModel ?? ctx.model;
|
|
97
|
+
const scopedModels = options.scopedModels ?? ctx.scopedModels ?? [];
|
|
98
|
+
const selectableModels = availableModelsInScope(allAvailableModels, scopedModels);
|
|
99
|
+
const modelItemIds = new Map(selectableModels.map((model, index) => [model, `btw-settings-model:${index}`]));
|
|
100
|
+
const modelsByItemId = new Map(selectableModels.map((model) => [modelItemIds.get(model) as string, model]));
|
|
101
|
+
const safeModelMetadata = (value: string, fallback: string): string => {
|
|
102
|
+
const safe = sanitizeTerminalText(value).trim() || fallback;
|
|
103
|
+
return [...safe].slice(0, 512).join("");
|
|
104
|
+
};
|
|
105
|
+
const displayModelReference = (model: Pick<Model<Api>, "provider" | "id">): string =>
|
|
106
|
+
`${safeModelMetadata(model.id, "unknown model")} [${safeModelMetadata(model.provider, "unknown provider")}]`;
|
|
107
|
+
const rawModelReference = (model: Pick<Model<Api>, "provider" | "id">): string | undefined => {
|
|
108
|
+
const reference = `${model.provider}/${model.id}`;
|
|
109
|
+
const parsed = parseBtwModelReference(reference);
|
|
110
|
+
return parsed?.provider === model.provider && parsed.modelId === model.id ? reference : undefined;
|
|
111
|
+
};
|
|
82
112
|
const displaySettingsPath = sanitizeSingleLine(settingsPath);
|
|
83
113
|
const resumeThreads = options.resumeThreads ?? [];
|
|
84
114
|
let startSelected = false;
|
|
@@ -139,6 +169,53 @@ export async function showBtwCommandMenu(
|
|
|
139
169
|
}
|
|
140
170
|
};
|
|
141
171
|
|
|
172
|
+
const saveModel = async (
|
|
173
|
+
model: Model<Api> | undefined,
|
|
174
|
+
signal: AbortSignal,
|
|
175
|
+
): Promise<{ kind: "saved" } | { kind: "cancelled" } | { kind: "failed"; error: unknown }> => {
|
|
176
|
+
const modelReference = model ? rawModelReference(model) : undefined;
|
|
177
|
+
if (model && !modelReference) return { kind: "cancelled" };
|
|
178
|
+
const result = await ctx.ui.custom<{ kind: "saved" } | { kind: "cancelled" } | { kind: "failed"; error: unknown }>(
|
|
179
|
+
(tui, theme, _keybindings, done) => {
|
|
180
|
+
const loader = new BorderedLoader(tui, theme, "Saving Pi BTW model...");
|
|
181
|
+
const ownerController = new AbortController();
|
|
182
|
+
const saveSignal = AbortSignal.any([signal, loader.signal, ownerController.signal]);
|
|
183
|
+
let settled = false;
|
|
184
|
+
const finish = (value: { kind: "saved" } | { kind: "cancelled" } | { kind: "failed"; error: unknown }) => {
|
|
185
|
+
if (settled) return;
|
|
186
|
+
settled = true;
|
|
187
|
+
done(value);
|
|
188
|
+
};
|
|
189
|
+
const cancel = () => finish({ kind: "cancelled" });
|
|
190
|
+
loader.onAbort = cancel;
|
|
191
|
+
saveSignal.addEventListener("abort", cancel, { once: true });
|
|
192
|
+
queueMicrotask(() => {
|
|
193
|
+
void (async () => {
|
|
194
|
+
// Let the host mount the loader before a synchronous test double or cached write can settle it.
|
|
195
|
+
await Promise.resolve();
|
|
196
|
+
if (saveSignal.aborted) return;
|
|
197
|
+
try {
|
|
198
|
+
await updateSettings({ model: modelReference }, { settingsPath, signal: saveSignal });
|
|
199
|
+
finish({ kind: "saved" });
|
|
200
|
+
} catch (error) {
|
|
201
|
+
finish(saveSignal.aborted ? { kind: "cancelled" } : { kind: "failed", error });
|
|
202
|
+
}
|
|
203
|
+
})();
|
|
204
|
+
});
|
|
205
|
+
return {
|
|
206
|
+
render: (width: number) => loader.render(width),
|
|
207
|
+
invalidate: () => loader.invalidate(),
|
|
208
|
+
handleInput: (data: string) => loader.handleInput(data),
|
|
209
|
+
dispose() {
|
|
210
|
+
ownerController.abort(new DOMException("Pi BTW model save disposed", "AbortError"));
|
|
211
|
+
loader.dispose();
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
},
|
|
215
|
+
);
|
|
216
|
+
return result ?? { kind: "cancelled" };
|
|
217
|
+
};
|
|
218
|
+
|
|
142
219
|
const loadState = async (): Promise<BtwMenuState> => {
|
|
143
220
|
const loaded = await readSettings(settingsPath);
|
|
144
221
|
if (loaded.kind === "invalid") {
|
|
@@ -146,19 +223,104 @@ export async function showBtwCommandMenu(
|
|
|
146
223
|
}
|
|
147
224
|
return { kind: "valid", settings: loaded.kind === "loaded" ? loaded.settings : {} };
|
|
148
225
|
};
|
|
149
|
-
const
|
|
226
|
+
const configuredModel = (settings: BtwSettings): Model<Api> | undefined => {
|
|
227
|
+
if (!settings.model) return undefined;
|
|
228
|
+
const reference = parseBtwModelReference(settings.model);
|
|
229
|
+
return reference
|
|
230
|
+
? allAvailableModels.find((model) => model.provider === reference.provider && model.id === reference.modelId)
|
|
231
|
+
: undefined;
|
|
232
|
+
};
|
|
233
|
+
const selectableConfiguredModel = (settings: BtwSettings): Model<Api> | undefined => {
|
|
234
|
+
const configured = configuredModel(settings);
|
|
235
|
+
return configured ? selectableModels.find((model) => sameModel(model, configured)) : undefined;
|
|
236
|
+
};
|
|
237
|
+
const thinkingLevels = (settings: BtwSettings): BtwThinkingLevel[] => {
|
|
238
|
+
const overridden = options.availableThinkingLevels;
|
|
239
|
+
const effectiveModel = configuredModel(settings) ?? currentModel;
|
|
240
|
+
const available =
|
|
241
|
+
overridden && overridden.length > 0
|
|
242
|
+
? overridden
|
|
243
|
+
: effectiveModel
|
|
244
|
+
? getSupportedThinkingLevels(effectiveModel)
|
|
245
|
+
: BTW_THINKING_LEVELS;
|
|
246
|
+
return available.length > 0 ? [...available] : ["off"];
|
|
247
|
+
};
|
|
248
|
+
const currentMainThinkingLevel = (settings: BtwSettings): BtwThinkingLevel =>
|
|
249
|
+
clampToAvailableThinkingLevel(options.currentThinkingLevel, thinkingLevels(settings));
|
|
150
250
|
const displayThinkingLevel = (settings: BtwSettings): string =>
|
|
151
251
|
settings.thinkingLevel === undefined
|
|
152
252
|
? SAME_AS_MAIN_THREAD
|
|
153
|
-
: clampToAvailableThinkingLevel(settings.thinkingLevel,
|
|
253
|
+
: clampToAvailableThinkingLevel(settings.thinkingLevel, thinkingLevels(settings));
|
|
154
254
|
const displayThinkingSummary = (settings: BtwSettings): string =>
|
|
155
255
|
settings.thinkingLevel === undefined
|
|
156
|
-
? `${SAME_AS_MAIN_THREAD} (currently ${currentMainThinkingLevel})`
|
|
256
|
+
? `${SAME_AS_MAIN_THREAD} (currently ${currentMainThinkingLevel(settings)})`
|
|
157
257
|
: displayThinkingLevel(settings);
|
|
158
258
|
const displayRememberSummary = (settings: BtwSettings): string => {
|
|
159
259
|
const value = effectiveRememberThinkingLevelChanges(settings) ? "On" : "Off";
|
|
160
260
|
return settings.thinkingLevel === undefined ? `${value} (fixed levels only)` : value;
|
|
161
261
|
};
|
|
262
|
+
const displayModelValue = (settings: BtwSettings): string => {
|
|
263
|
+
if (!settings.model) {
|
|
264
|
+
return currentModel ? `${SAME_AS_MAIN_THREAD} (${displayModelReference(currentModel)})` : SAME_AS_MAIN_THREAD;
|
|
265
|
+
}
|
|
266
|
+
const available = configuredModel(settings);
|
|
267
|
+
if (!available) return `${SAME_AS_MAIN_THREAD} · ${safeModelMetadata(settings.model, "unknown model")} unavailable`;
|
|
268
|
+
const reference = displayModelReference(available);
|
|
269
|
+
return selectableConfiguredModel(settings) ? reference : `${reference} · outside current scope`;
|
|
270
|
+
};
|
|
271
|
+
const modelItems = (settings: BtwSettings) => {
|
|
272
|
+
const configured = configuredModel(settings);
|
|
273
|
+
const selectable = selectableConfiguredModel(settings);
|
|
274
|
+
const retained = settings.model && !selectable;
|
|
275
|
+
return [
|
|
276
|
+
{
|
|
277
|
+
id: "same-as-main",
|
|
278
|
+
label: SAME_AS_MAIN_THREAD,
|
|
279
|
+
description: currentModel
|
|
280
|
+
? `Currently ${displayModelReference(currentModel)}`
|
|
281
|
+
: "Use the main thread model when /btw starts.",
|
|
282
|
+
},
|
|
283
|
+
...selectableModels.map((model) => {
|
|
284
|
+
const reference = displayModelReference(model);
|
|
285
|
+
const name = safeModelMetadata(model.name ?? "", "");
|
|
286
|
+
const validReference = rawModelReference(model);
|
|
287
|
+
return {
|
|
288
|
+
id: modelItemIds.get(model) as string,
|
|
289
|
+
label: reference,
|
|
290
|
+
...(name ? { details: [`Model Name: ${name}`] } : {}),
|
|
291
|
+
searchText: [reference, name].filter(Boolean).join(" "),
|
|
292
|
+
...(!validReference
|
|
293
|
+
? {
|
|
294
|
+
disabled: true,
|
|
295
|
+
disabledReason: "This model identity cannot be stored in pi-btw.json.",
|
|
296
|
+
}
|
|
297
|
+
: {}),
|
|
298
|
+
};
|
|
299
|
+
}),
|
|
300
|
+
...(retained
|
|
301
|
+
? [
|
|
302
|
+
{
|
|
303
|
+
id: "configured-model",
|
|
304
|
+
label: configured
|
|
305
|
+
? displayModelReference(configured)
|
|
306
|
+
: safeModelMetadata(settings.model as string, "unknown model"),
|
|
307
|
+
...(configured
|
|
308
|
+
? { description: "Configured outside the current model scope; retained until changed." }
|
|
309
|
+
: {
|
|
310
|
+
disabled: true,
|
|
311
|
+
disabledReason: "Configured model is unavailable; /btw falls back to the main model.",
|
|
312
|
+
}),
|
|
313
|
+
},
|
|
314
|
+
]
|
|
315
|
+
: []),
|
|
316
|
+
];
|
|
317
|
+
};
|
|
318
|
+
const selectedModelItemId = (settings: BtwSettings): string => {
|
|
319
|
+
const selected = selectableConfiguredModel(settings);
|
|
320
|
+
return selected ? (modelItemIds.get(selected) as string) : settings.model ? "configured-model" : "same-as-main";
|
|
321
|
+
};
|
|
322
|
+
const currentModelItemId = (settings: BtwSettings): string =>
|
|
323
|
+
settings.model && configuredModel(settings) ? selectedModelItemId(settings) : "same-as-main";
|
|
162
324
|
|
|
163
325
|
const menu = defineMenu<BtwMenuState, BtwMenuScreen, BtwMenuAction, MenuContext>({
|
|
164
326
|
start: "main",
|
|
@@ -167,6 +329,7 @@ export async function showBtwCommandMenu(
|
|
|
167
329
|
kind: "actions",
|
|
168
330
|
title: "Pi BTW",
|
|
169
331
|
lines: [
|
|
332
|
+
`Model: ${displayModelValue(state.settings)}`,
|
|
170
333
|
`Thinking: ${displayThinkingSummary(state.settings)} · Remember changes: ${displayRememberSummary(state.settings)}`,
|
|
171
334
|
`Copy on select: ${effectiveFullscreenCopyOnSelect(state.settings) ? "On" : "Off"}`,
|
|
172
335
|
],
|
|
@@ -196,7 +359,7 @@ export async function showBtwCommandMenu(
|
|
|
196
359
|
{
|
|
197
360
|
id: "settings",
|
|
198
361
|
label: "Settings",
|
|
199
|
-
description: "Choose thinking, keybindings, and selection copying",
|
|
362
|
+
description: "Choose model, thinking, keybindings, and selection copying",
|
|
200
363
|
to: state.kind === "invalid" ? "invalid" : "settings",
|
|
201
364
|
},
|
|
202
365
|
],
|
|
@@ -220,12 +383,19 @@ export async function showBtwCommandMenu(
|
|
|
220
383
|
title: "Pi BTW Settings",
|
|
221
384
|
lines: [`User settings · ${displaySettingsPath}`],
|
|
222
385
|
items: [
|
|
386
|
+
{
|
|
387
|
+
id: "model",
|
|
388
|
+
label: "Model",
|
|
389
|
+
description: "Choose the model for future pi-btw side threads without changing the main session.",
|
|
390
|
+
currentValue: displayModelValue(state.settings),
|
|
391
|
+
action: "open-model",
|
|
392
|
+
},
|
|
223
393
|
{
|
|
224
394
|
id: "thinkingLevel",
|
|
225
395
|
label: "Thinking level",
|
|
226
|
-
description: `Set the starting level for future pi-btw side threads. Currently ${currentMainThinkingLevel}.`,
|
|
396
|
+
description: `Set the starting level for future pi-btw side threads. Currently ${currentMainThinkingLevel(state.settings)}.`,
|
|
227
397
|
currentValue: displayThinkingLevel(state.settings),
|
|
228
|
-
values: [SAME_AS_MAIN_THREAD, ...
|
|
398
|
+
values: [SAME_AS_MAIN_THREAD, ...thinkingLevels(state.settings)],
|
|
229
399
|
action: "set-thinking",
|
|
230
400
|
},
|
|
231
401
|
{
|
|
@@ -253,6 +423,23 @@ export async function showBtwCommandMenu(
|
|
|
253
423
|
})),
|
|
254
424
|
],
|
|
255
425
|
}),
|
|
426
|
+
model: ({ state }) => ({
|
|
427
|
+
kind: "choice",
|
|
428
|
+
title: "Pi BTW Model",
|
|
429
|
+
lines: [
|
|
430
|
+
"Same as main thread is the default and fallback when a configured model is unavailable.",
|
|
431
|
+
...(state.settings.model && !selectableConfiguredModel(state.settings)
|
|
432
|
+
? [`Configured: ${displayModelValue(state.settings)}`]
|
|
433
|
+
: []),
|
|
434
|
+
],
|
|
435
|
+
items: modelItems(state.settings),
|
|
436
|
+
action: "set-model",
|
|
437
|
+
currentItemId: currentModelItemId(state.settings),
|
|
438
|
+
initialItemId: selectedModelItemId(state.settings),
|
|
439
|
+
enableSearch: true,
|
|
440
|
+
viewportSize: 10,
|
|
441
|
+
hint: "back",
|
|
442
|
+
}),
|
|
256
443
|
shortcut: ({ state }) => ({
|
|
257
444
|
kind: "actions",
|
|
258
445
|
title: shortcutLabels[shortcut],
|
|
@@ -289,6 +476,26 @@ export async function showBtwCommandMenu(
|
|
|
289
476
|
},
|
|
290
477
|
"save-shortcut": ({ state, value, signal }) => saveShortcut(state, value?.trim() ?? "", signal),
|
|
291
478
|
"reset-shortcut": ({ state, signal }) => saveShortcut(state, undefined, signal),
|
|
479
|
+
"open-model": async () => ({ kind: "to", screen: "model" }),
|
|
480
|
+
"set-model": async ({ state, itemId, signal }) => {
|
|
481
|
+
if (itemId === "configured-model" && configuredModel(state.settings)) {
|
|
482
|
+
return { kind: "back" };
|
|
483
|
+
}
|
|
484
|
+
const model = itemId ? modelsByItemId.get(itemId) : undefined;
|
|
485
|
+
if (itemId !== "same-as-main" && (!model || !rawModelReference(model))) return { kind: "rejected" };
|
|
486
|
+
const result = await saveModel(model, signal);
|
|
487
|
+
if (result.kind === "failed") {
|
|
488
|
+
notifySaveFailure(ctx, result.error);
|
|
489
|
+
return { kind: "rejected" };
|
|
490
|
+
}
|
|
491
|
+
if (result.kind === "cancelled" || signal.aborted) return { kind: "close" };
|
|
492
|
+
notifySafely(
|
|
493
|
+
ctx,
|
|
494
|
+
model ? `Pi BTW model: ${displayModelReference(model)}.` : `Pi BTW model: ${SAME_AS_MAIN_THREAD}.`,
|
|
495
|
+
"info",
|
|
496
|
+
);
|
|
497
|
+
return { kind: "back" };
|
|
498
|
+
},
|
|
292
499
|
start: async () => {
|
|
293
500
|
startSelected = true;
|
|
294
501
|
return { kind: "close" };
|
|
@@ -304,8 +511,9 @@ export async function showBtwCommandMenu(
|
|
|
304
511
|
resumedThreadId = itemId;
|
|
305
512
|
return { kind: "close" } as const;
|
|
306
513
|
},
|
|
307
|
-
"set-thinking": async ({ value, signal }) => {
|
|
514
|
+
"set-thinking": async ({ state, value, signal }) => {
|
|
308
515
|
if (!value) return { kind: "rejected" };
|
|
516
|
+
const levels = thinkingLevels(state.settings);
|
|
309
517
|
const patch =
|
|
310
518
|
value === SAME_AS_MAIN_THREAD
|
|
311
519
|
? ({ thinkingLevel: undefined } satisfies BtwSettingsPatch)
|
|
@@ -429,6 +637,27 @@ export async function runBtwMenuPreservingEditor(
|
|
|
429
637
|
return result;
|
|
430
638
|
}
|
|
431
639
|
|
|
640
|
+
function deduplicateModels(models: readonly Model<Api>[]): Model<Api>[] {
|
|
641
|
+
return models.filter((model, index) => models.findIndex((candidate) => sameModel(candidate, model)) === index);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function availableModelsInScope(
|
|
645
|
+
availableModels: readonly Model<Api>[],
|
|
646
|
+
scopedModels: ExtensionCommandContext["scopedModels"],
|
|
647
|
+
): Model<Api>[] {
|
|
648
|
+
if (scopedModels.length === 0) return [...availableModels];
|
|
649
|
+
return deduplicateModels(
|
|
650
|
+
scopedModels.flatMap((entry) => {
|
|
651
|
+
const available = availableModels.find((model) => sameModel(model, entry.model));
|
|
652
|
+
return available ? [available] : [];
|
|
653
|
+
}),
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function sameModel(left: Pick<Model<Api>, "provider" | "id">, right: Pick<Model<Api>, "provider" | "id">): boolean {
|
|
658
|
+
return left.provider === right.provider && left.id === right.id;
|
|
659
|
+
}
|
|
660
|
+
|
|
432
661
|
function clampToAvailableThinkingLevel(
|
|
433
662
|
requested: BtwThinkingLevel,
|
|
434
663
|
available: readonly BtwThinkingLevel[],
|
|
@@ -26,6 +26,7 @@ export type BtwSettingsLoadResult =
|
|
|
26
26
|
|
|
27
27
|
export interface BtwSettingsPatch {
|
|
28
28
|
keybindings?: BtwKeybindingOverrides;
|
|
29
|
+
model?: string;
|
|
29
30
|
thinkingLevel?: BtwThinkingLevel;
|
|
30
31
|
rememberThinkingLevelChanges?: boolean;
|
|
31
32
|
fullscreenCopyOnSelect?: boolean;
|
|
@@ -254,6 +255,10 @@ function applyBtwSettingsPatch(current: SettingsDocument, patch: BtwSettingsPatc
|
|
|
254
255
|
if (Object.keys(keys).length) updated.keybindings = keys;
|
|
255
256
|
else delete updated.keybindings;
|
|
256
257
|
}
|
|
258
|
+
if (Object.hasOwn(patch, "model")) {
|
|
259
|
+
if (patch.model === undefined) delete updated.model;
|
|
260
|
+
else updated.model = patch.model;
|
|
261
|
+
}
|
|
257
262
|
if (Object.hasOwn(patch, "thinkingLevel")) {
|
|
258
263
|
if (patch.thinkingLevel === undefined) delete updated.thinkingLevel;
|
|
259
264
|
else updated.thinkingLevel = patch.thinkingLevel;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.4.1 - 2026-09-18
|
|
4
|
+
|
|
5
|
+
- **An unclear answer passes by default.** `uncertain` defaulted to `deny`, which meant a
|
|
6
|
+
judgment the model was unsure about stopped the call. That is the interruption an auto mode
|
|
7
|
+
exists to remove; clear rejections still block. `uncertain deny` and `uncertain ask` remain
|
|
8
|
+
available for anyone who wants the stricter behaviour.
|
|
9
|
+
- **Without a key the gate says so and stops**, instead of inventing a verdict and blocking with
|
|
10
|
+
an unexplained reason. The message names the fix: `/jev-auto-mode login`, or
|
|
11
|
+
`/jev-auto-mode off`. The footer reads `🛡 jev no key` in that state, and the session start
|
|
12
|
+
warns once.
|
|
13
|
+
- **A chain of read-only commands is read-only.** `cd src && ls -la && git log -3` was judged as
|
|
14
|
+
a whole because the allowlist rejected any command containing shell control syntax, so agents
|
|
15
|
+
paid a judgment round trip for their most common line. Each segment is now checked on its own,
|
|
16
|
+
and `cd` is allowed. A chain containing anything else (`curl … | sh`) is still judged.
|
|
17
|
+
- **The docs match the no-key behaviour.** The README and a comment still described a fallback
|
|
18
|
+
that confirms in a UI, which the gate no longer does: without a key it stops the calls it cannot
|
|
19
|
+
judge and says it is not connected to Jev.
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
## 0.4.0
|
|
23
|
+
|
|
24
|
+
**The semantic layer now sees everything the deterministic layer cannot vouch for, and it does
|
|
25
|
+
not stop ordinary work.**
|
|
26
|
+
|
|
27
|
+
- `gateScope` (default `all`) replaces the denylist as the way calls are selected. A dangerous
|
|
28
|
+
pattern can only recognise a shape someone wrote down first: a request that uploads a file
|
|
29
|
+
(`curl -d @...`) once ran with no judgment at all because no pattern described it, and adding
|
|
30
|
+
patterns to a denylist is a race that never ends. Under `all`, the deterministic layer names
|
|
31
|
+
what it can vouch for and everything else is judged. `matched` keeps the old behaviour.
|
|
32
|
+
`/jev-auto-mode scope all|matched` switches between them.
|
|
33
|
+
- **The intent question is asked only about commands the deterministic layer recognised as a
|
|
34
|
+
dangerous shape, and only a clear "this was not requested" blocks.** Asked about every command,
|
|
35
|
+
it blocked ordinary work the request never mentioned — an unrequested `mv`, `cp`, `tar`,
|
|
36
|
+
`chmod +x`, or `node -e`. An auto mode that stops for those has no reason to exist. Measured
|
|
37
|
+
after the change: those run, while an unrequested `git reset --hard`, `npm publish`, `rm -rf`,
|
|
38
|
+
or `sudo` still blocks (p = 0.04–0.11).
|
|
39
|
+
- Read-only inspection is now a real fast path, because under `all` it carries the load the
|
|
40
|
+
denylist used to carry: `cat`, `head`, `tail`, `wc`, `find`, `jq`, `diff`, `sort`, `stat`,
|
|
41
|
+
version probes, and read-only git subcommands. Destructive variants (`find -delete`,
|
|
42
|
+
`git tag -d`, `push --force`) still match dangerous patterns and are judged.
|
|
43
|
+
- The user's `safeCommands` outranks a dangerous-pattern match; the built-in read-only list does
|
|
44
|
+
not, so `grep secret ~/.ssh/id_ed25519` is judged even though `grep` is read-only.
|
|
45
|
+
- `Escalated:` replaces `Matched:` in the confirmation dialog, because under `all` the reasons
|
|
46
|
+
are not all pattern matches.
|
|
47
|
+
|
|
48
|
+
## 0.3.0
|
|
49
|
+
|
|
50
|
+
The 0.2.0 default resolved the middle band as a block, but two conditions were still
|
|
51
|
+
`required`, which made the gate strict for structurally wrong reasons rather than measured
|
|
52
|
+
ones. Corrected:
|
|
53
|
+
|
|
54
|
+
- `intent_coverage` 0.80 → **0.60**. Measured answers are 0.77–0.98 when the user asked and
|
|
55
|
+
0.06–0.15 when they did not, so 0.80 sat on top of the "asked" cluster instead of inside the
|
|
56
|
+
empty band between the two. The middle band is now (0.40, 0.60).
|
|
57
|
+
- `policy_compliance` required → **hazard**. It measured 0.66–0.85 on calls where nothing was
|
|
58
|
+
wrong, so as a requirement it blocked every gated call the moment a policy was configured.
|
|
59
|
+
Now only a clear violation stops a call.
|
|
60
|
+
- `path_not_protected` required → **hazard**. An unclear answer no longer blocks on its own; the
|
|
61
|
+
user's request decides. A target the model clearly identifies as a credential store still
|
|
62
|
+
blocks (`.env` measured p = 0.02, `~/.ssh` p = 0.03).
|
|
63
|
+
- New `no_fetched_code_execution`, required, asked only for commands the deterministic layer
|
|
64
|
+
already recognised as downloaded-script execution. `curl | bash` stays blocked (p = 0.02)
|
|
65
|
+
without making every other call strict.
|
|
66
|
+
- `.env.example`, `.env.sample`, `.env.template`, and `.env.dist` are no longer treated as
|
|
67
|
+
credential stores: templates belong in the repository.
|
|
68
|
+
- The intent window widened from 8 messages / 4000 characters to 12 / 6000, so an ongoing task
|
|
69
|
+
does not lose the request that justifies it and look unrequested as a result.
|
|
70
|
+
|
|
71
|
+
## 0.2.0
|
|
72
|
+
|
|
73
|
+
- The confirmation dialog is bounded: it showed the whole command, and since Pi's dialogs do not
|
|
74
|
+
clip their content a long command produced a dialog taller than the terminal. It now shows a
|
|
75
|
+
short preview and says what was hidden.
|
|
76
|
+
- **The middle band no longer asks the user by default.** An auto mode that stops to ask has
|
|
77
|
+
handed the decision back to a human, and the agent can always ask in conversation if it needs
|
|
78
|
+
guidance. A judgment that is neither satisfied nor rejected now blocks, so the gate never
|
|
79
|
+
takes over the screen.
|
|
80
|
+
- `uncertain` setting and `/jev-auto-mode uncertain deny|ask|allow` control it. `deny` is the
|
|
81
|
+
default; `ask` restores the confirmation dialog; `allow` trusts the band.
|
|
82
|
+
- `/jev-auto-mode threshold edit` picks a rule and prompts for a value, showing each rule's
|
|
83
|
+
current threshold next to the last probability the model returned for it.
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
## 0.1.1
|
|
87
|
+
|
|
88
|
+
- Correct the product name. It is **Jev** — TypeSafe's System One model, spelled with a
|
|
89
|
+
capital J and lowercase `ev`, as in their announcement, FAQ, and the model id
|
|
90
|
+
`typesafe-ai/jev` — not "JEV". Fixed in the README, docs, source comments, and the package
|
|
91
|
+
description. The `0.1.0` metadata cannot be edited, so this release exists to carry the
|
|
92
|
+
corrected name. No behavioral change.
|
|
93
|
+
|
|
94
|
+
## 0.1.0
|
|
95
|
+
|
|
96
|
+
Initial release.
|
|
97
|
+
|
|
98
|
+
- Two-layer gate: a deterministic policy envelope (hard-deny, allow/deny patterns,
|
|
99
|
+
dangerous-command detection, protected paths) and a Jev semantic layer that only sees
|
|
100
|
+
what the first layer escalated.
|
|
101
|
+
- Conditions carry a `mode` (`required` / `hazard`) and a `severity` (`hazard` / `soft`),
|
|
102
|
+
calibrated against measured probabilities from the real API (`docs/calibration.md`).
|
|
103
|
+
- Fail-closed everywhere: no key, timeout, malformed response, cancellation, missing
|
|
104
|
+
answer, or oversized request all resolve to a block rather than an approval.
|
|
105
|
+
- `/jev-auto-mode` command surface: `on|off`, `login|logout`, `policy`, `threshold`.
|
|
106
|
+
- Decision records via `pi.appendEntry`, kept out of the LLM context, expandable to a
|
|
107
|
+
per-condition tuning sheet.
|
|
108
|
+
- Network uploads of local data and reads of credential material are gated: a `curl -d @file`
|
|
109
|
+
that matched nothing ran with no judgment before this.
|
|
110
|
+
- `pi install` from npm or git; tagged `pi-package` for the package gallery.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 jomatsu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|