@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.
Files changed (94) hide show
  1. package/README.md +8 -4
  2. package/THIRD_PARTY_NOTICES.md +52 -0
  3. package/node_modules/@herbertgao/pi-bark/package.json +2 -2
  4. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +1 -1
  5. package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -1
  6. package/node_modules/@herbertgao/pi-cc-extensions/package.json +4 -4
  7. package/node_modules/@herbertgao/resume-from/package.json +2 -2
  8. package/node_modules/@narumitw/pi-btw/README.md +6 -1
  9. package/node_modules/@narumitw/pi-btw/dist/index.ts +209 -23
  10. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +4 -4
  11. package/node_modules/@narumitw/pi-btw/package.json +1 -1
  12. package/node_modules/@narumitw/pi-btw/src/btw.ts +1 -12
  13. package/node_modules/@narumitw/pi-btw/src/menu.ts +244 -15
  14. package/node_modules/@narumitw/pi-btw/src/settings.ts +5 -0
  15. package/node_modules/pi-jev-auto-mode/CHANGELOG.md +110 -0
  16. package/node_modules/pi-jev-auto-mode/LICENSE +21 -0
  17. package/node_modules/pi-jev-auto-mode/README.md +285 -0
  18. package/node_modules/pi-jev-auto-mode/SECURITY.md +32 -0
  19. package/node_modules/pi-jev-auto-mode/docs/calibration.md +154 -0
  20. package/node_modules/pi-jev-auto-mode/docs/design.md +164 -0
  21. package/node_modules/pi-jev-auto-mode/docs/security.md +124 -0
  22. package/node_modules/pi-jev-auto-mode/index.ts +1 -0
  23. package/node_modules/pi-jev-auto-mode/package.json +72 -0
  24. package/node_modules/pi-jev-auto-mode/src/call.ts +180 -0
  25. package/node_modules/pi-jev-auto-mode/src/decide.ts +86 -0
  26. package/node_modules/pi-jev-auto-mode/src/extension.ts +862 -0
  27. package/node_modules/pi-jev-auto-mode/src/intent.ts +71 -0
  28. package/node_modules/pi-jev-auto-mode/src/jev/availability.ts +53 -0
  29. package/node_modules/pi-jev-auto-mode/src/jev/criteria.ts +19 -0
  30. package/node_modules/pi-jev-auto-mode/src/jev/decide.ts +187 -0
  31. package/node_modules/pi-jev-auto-mode/src/jev/engine.ts +165 -0
  32. package/node_modules/pi-jev-auto-mode/src/jev/index.ts +20 -0
  33. package/node_modules/pi-jev-auto-mode/src/jev/questions.ts +273 -0
  34. package/node_modules/pi-jev-auto-mode/src/jev/response.ts +64 -0
  35. package/node_modules/pi-jev-auto-mode/src/jev/state.ts +20 -0
  36. package/node_modules/pi-jev-auto-mode/src/jev/transport.ts +117 -0
  37. package/node_modules/pi-jev-auto-mode/src/jev/types.ts +46 -0
  38. package/node_modules/pi-jev-auto-mode/src/policy.ts +558 -0
  39. package/node_modules/pi-jev-auto-mode/src/records.ts +118 -0
  40. package/node_modules/pi-jev-auto-mode/src/settings.ts +327 -0
  41. package/node_modules/pi-jev-auto-mode/src/ui.ts +233 -0
  42. package/node_modules/pi-lens/CHANGELOG.md +45 -0
  43. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +21 -9
  44. package/node_modules/pi-lens/dist/clients/cascade-format.js +24 -0
  45. package/node_modules/pi-lens/dist/clients/extension-log.js +48 -1
  46. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +11 -12
  47. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +3 -0
  48. package/node_modules/pi-lens/dist/clients/lsp/index.js +41 -7
  49. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +9 -2
  50. package/node_modules/pi-lens/dist/clients/path-utils.js +51 -9
  51. package/node_modules/pi-lens/dist/clients/persistent-reverify.js +271 -0
  52. package/node_modules/pi-lens/dist/clients/pipeline.js +15 -1
  53. package/node_modules/pi-lens/dist/clients/runtime-turn.js +124 -7
  54. package/node_modules/pi-lens/dist/clients/skills-resolver.js +38 -2
  55. package/node_modules/pi-lens/dist/clients/widget-state.js +152 -6
  56. package/node_modules/pi-lens/dist/index.js +729 -344
  57. package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +14 -2
  58. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +84 -17
  59. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +32 -5
  60. package/node_modules/pi-lens/package.json +1 -1
  61. package/node_modules/pi-typesafe/LICENSE +21 -0
  62. package/node_modules/pi-typesafe/README.md +153 -0
  63. package/node_modules/pi-typesafe/dist/ask.d.ts +32 -0
  64. package/node_modules/pi-typesafe/dist/ask.js +25 -0
  65. package/node_modules/pi-typesafe/dist/auth.d.ts +56 -0
  66. package/node_modules/pi-typesafe/dist/auth.js +114 -0
  67. package/node_modules/pi-typesafe/dist/batch.d.ts +74 -0
  68. package/node_modules/pi-typesafe/dist/batch.js +116 -0
  69. package/node_modules/pi-typesafe/dist/calibrate.d.ts +106 -0
  70. package/node_modules/pi-typesafe/dist/calibrate.js +157 -0
  71. package/node_modules/pi-typesafe/dist/client.d.ts +69 -0
  72. package/node_modules/pi-typesafe/dist/client.js +188 -0
  73. package/node_modules/pi-typesafe/dist/credentials.d.ts +46 -0
  74. package/node_modules/pi-typesafe/dist/credentials.js +112 -0
  75. package/node_modules/pi-typesafe/dist/errors.d.ts +9 -0
  76. package/node_modules/pi-typesafe/dist/errors.js +31 -0
  77. package/node_modules/pi-typesafe/dist/extension.d.ts +3 -0
  78. package/node_modules/pi-typesafe/dist/extension.js +222 -0
  79. package/node_modules/pi-typesafe/dist/index.d.ts +18 -0
  80. package/node_modules/pi-typesafe/dist/index.js +9 -0
  81. package/node_modules/pi-typesafe/dist/key-prompt.d.ts +3 -0
  82. package/node_modules/pi-typesafe/dist/key-prompt.js +43 -0
  83. package/node_modules/pi-typesafe/dist/login.d.ts +27 -0
  84. package/node_modules/pi-typesafe/dist/login.js +38 -0
  85. package/node_modules/pi-typesafe/dist/schema.d.ts +43 -0
  86. package/node_modules/pi-typesafe/dist/schema.js +152 -0
  87. package/node_modules/pi-typesafe/dist/ui.d.ts +3 -0
  88. package/node_modules/pi-typesafe/dist/ui.js +3 -0
  89. package/node_modules/pi-typesafe/dist/usage.d.ts +73 -0
  90. package/node_modules/pi-typesafe/dist/usage.js +182 -0
  91. package/node_modules/pi-typesafe/examples/decision-extension.ts +41 -0
  92. package/node_modules/pi-typesafe/extensions/index.js +2 -0
  93. package/node_modules/pi-typesafe/package.json +89 -0
  94. package/package.json +15 -8
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narumitw/pi-btw",
3
- "version": "0.59.0",
3
+ "version": "0.60.0",
4
4
  "description": "Pi extension that adds a /btw side-question command.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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 { ExtensionCommandContext, KeybindingsManager, Theme } from "@earendil-works/pi-coding-agent";
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
- availableThinkingLevels: readonly BtwThinkingLevel[];
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 levels =
79
- options.availableThinkingLevels.length > 0
80
- ? [...options.availableThinkingLevels]
81
- : (["off"] satisfies BtwThinkingLevel[]);
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 currentMainThinkingLevel = clampToAvailableThinkingLevel(options.currentThinkingLevel, levels);
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, levels);
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, ...levels],
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.