pi-bro 0.17.0 → 0.18.1
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/CHANGELOG.md +25 -0
- package/README.md +199 -243
- package/backend.ts +43 -22
- package/bro.ts +121 -373
- package/package.json +5 -4
- package/prompt.ts +17 -5
package/bro.ts
CHANGED
|
@@ -63,7 +63,9 @@ type BroSource = { text: string; label?: string };
|
|
|
63
63
|
type BroResult = { source: BroSource; text: string; model?: string };
|
|
64
64
|
type ModalResult = { source?: BroSource; text: string; htmlPath?: string; model?: string };
|
|
65
65
|
type BtwTurn = { question: string; answer: string };
|
|
66
|
-
|
|
66
|
+
// `context` keeps the main-session seed so a fresh native session can be reseeded with the whole
|
|
67
|
+
// thread; `sessionFull` is the access mode the native session last ran in.
|
|
68
|
+
type BtwThread = { turns: BtwTurn[]; conversationId?: string; full: boolean; backend?: BackendName; model?: string; context?: string; sessionFull?: boolean };
|
|
67
69
|
const EFFORTS = ["default", "low", "medium", "high"] as const;
|
|
68
70
|
const BACKENDS = ["agy", "claude", "grok"] as const;
|
|
69
71
|
type BackendName = (typeof BACKENDS)[number];
|
|
@@ -122,11 +124,9 @@ const COMMANDS = [
|
|
|
122
124
|
{ value: "url", label: "url", description: "Explain a public webpage" },
|
|
123
125
|
{ value: "open", label: "open", description: "Reopen the last explanation" },
|
|
124
126
|
{ value: "doctor", label: "doctor", description: "Check whether Bro is ready" },
|
|
125
|
-
{ value: "model", label: "model", description: "View or choose the shared default model" },
|
|
126
|
-
{ value: "effort", label: "effort", description: "View or choose the shared default reasoning effort" },
|
|
127
127
|
{ value: "show", label: "show", description: "Draw what happened in recent session turns as shapes" },
|
|
128
128
|
{ value: "mode", label: "mode", description: "View or choose explanation mode (brief, balanced, faithful)" },
|
|
129
|
-
{ value: "btw", label: "btw", description: "Open a side conversation (
|
|
129
|
+
{ value: "btw", label: "btw", description: "Open a side conversation (starts conversation-only; /mode toggles full permission)" },
|
|
130
130
|
{ value: "config", label: "config", description: "Configure shared defaults and per-capability model/effort overrides" },
|
|
131
131
|
{ value: "advisor", label: "advisor", description: "Check whether the executor's advisor tool is available right now" },
|
|
132
132
|
{ value: "advisor-steer", label: "advisor-steer", description: "View, edit, save, or clear the advisor's persistent steering brief" },
|
|
@@ -1092,7 +1092,7 @@ async function doctorReport(pi: ExtensionAPI, ctx: ExtensionCommandContext, sign
|
|
|
1092
1092
|
if (defaultBackend === "claude") {
|
|
1093
1093
|
pass("Selected model", `claude \`${settings.model}\``);
|
|
1094
1094
|
if (!isClaudeEffort(settings.effort)) {
|
|
1095
|
-
fail("Reasoning effort", `\`${settings.effort}\` is unsupported. Run \`/bro
|
|
1095
|
+
fail("Reasoning effort", `\`${settings.effort}\` is unsupported. Run \`/bro config\` to choose another.`);
|
|
1096
1096
|
} else if (settings.effort === "default") {
|
|
1097
1097
|
pass("Reasoning effort", "built into the selected model");
|
|
1098
1098
|
} else {
|
|
@@ -1101,7 +1101,7 @@ async function doctorReport(pi: ExtensionAPI, ctx: ExtensionCommandContext, sign
|
|
|
1101
1101
|
} else if (defaultBackend === "grok") {
|
|
1102
1102
|
pass("Selected model", `grok \`${settings.model}\``);
|
|
1103
1103
|
if (!isGrokEffort(settings.effort)) {
|
|
1104
|
-
fail("Reasoning effort", `\`${settings.effort}\` is unsupported. Run \`/bro
|
|
1104
|
+
fail("Reasoning effort", `\`${settings.effort}\` is unsupported. Run \`/bro config\` to choose another.`);
|
|
1105
1105
|
} else if (settings.effort === "default") {
|
|
1106
1106
|
pass("Reasoning effort", "built into the selected model");
|
|
1107
1107
|
} else {
|
|
@@ -1110,7 +1110,7 @@ async function doctorReport(pi: ExtensionAPI, ctx: ExtensionCommandContext, sign
|
|
|
1110
1110
|
} else if (models) {
|
|
1111
1111
|
const current = resolveCatalogSettings(settings, models);
|
|
1112
1112
|
if (!current.family) {
|
|
1113
|
-
fail("Selected model", `\`${settings.model}\` is unavailable. Run \`/bro
|
|
1113
|
+
fail("Selected model", `\`${settings.model}\` is unavailable. Run \`/bro config\` to choose another.`);
|
|
1114
1114
|
} else {
|
|
1115
1115
|
pass("Selected model", `agy \`${current.family.id}\``);
|
|
1116
1116
|
const effort = current.settings.effort;
|
|
@@ -1119,7 +1119,7 @@ async function doctorReport(pi: ExtensionAPI, ctx: ExtensionCommandContext, sign
|
|
|
1119
1119
|
} else if (effort !== "default" && current.family.efforts.includes(effort as AgyEffort)) {
|
|
1120
1120
|
pass("Reasoning effort", effort);
|
|
1121
1121
|
} else {
|
|
1122
|
-
fail("Reasoning effort", `\`${effort}\` is unsupported. Run \`/bro
|
|
1122
|
+
fail("Reasoning effort", `\`${effort}\` is unsupported. Run \`/bro config\` to choose another.`);
|
|
1123
1123
|
}
|
|
1124
1124
|
}
|
|
1125
1125
|
}
|
|
@@ -1130,10 +1130,6 @@ async function doctorReport(pi: ExtensionAPI, ctx: ExtensionCommandContext, sign
|
|
|
1130
1130
|
const backend = capabilityBackend(settings, capability);
|
|
1131
1131
|
const pair = capabilityPair(settings, capability);
|
|
1132
1132
|
if (backend === "claude") {
|
|
1133
|
-
if (capability === "btw") {
|
|
1134
|
-
fail(label, "`btw` is not supported on the Claude backend. Run `/bro config` to give it an Agy model.");
|
|
1135
|
-
continue;
|
|
1136
|
-
}
|
|
1137
1133
|
if (!isClaudeEffort(pair.effort)) {
|
|
1138
1134
|
fail(label, `\`${pair.effort}\` is unsupported for claude \`${pair.model}\`. Run \`/bro config\` to fix this.`);
|
|
1139
1135
|
continue;
|
|
@@ -2307,101 +2303,62 @@ export function helpText(settings?: BroSettings, settingsError?: string): string
|
|
|
2307
2303
|
const settingsSummary = settings
|
|
2308
2304
|
? `- **Backend:** ${settings.backend ?? "agy"}\n- **Model:** \`${settings.model}\`\n- **Reasoning effort:** ${settings.effort === "default" ? "built into the selected model" : settings.effort}\n- **Mode:** ${settings.mode}\n- **Show turns:** ${settings.showTurns}${overrideLines.length ? `\n${overrideLines.join("\n")}` : ""}`
|
|
2309
2305
|
: `Bro could not read its settings: ${settingsError}\n\nRun \`/bro doctor\` for setup help.`;
|
|
2310
|
-
const advisorBackend = settings ? capabilityBackend(settings, "advisor") : "agy";
|
|
2311
|
-
const advisorProcess = advisorBackend === "claude" ? "Claude process" : advisorBackend === "grok" ? "Grok process" : "Agy process";
|
|
2312
|
-
const advisorName = advisorBackend === "claude" ? "Claude" : advisorBackend === "grok" ? "Grok" : "Agy";
|
|
2313
2306
|
return `# Bro
|
|
2314
2307
|
|
|
2315
|
-
|
|
2308
|
+
Quick reference. The README is the full user guide: https://github.com/tranhoangnguyen03/pi-bro#readme
|
|
2316
2309
|
|
|
2317
|
-
## Explain
|
|
2310
|
+
## Explain and show
|
|
2318
2311
|
|
|
2319
2312
|
- \`/bro\` — explain the latest completed assistant reply
|
|
2320
2313
|
- \`/bro text [text]\` — explain pasted text, or the latest reply when text is omitted
|
|
2321
|
-
- \`/bro file <path>\` — explain a
|
|
2314
|
+
- \`/bro file <path>\` — explain a workspace \`.md\`, \`.markdown\`, \`.txt\`, \`.pdf\`, or \`.docx\` file
|
|
2322
2315
|
- \`/bro url <url>\` — explain one public webpage
|
|
2323
|
-
- \`/bro
|
|
2324
|
-
- \`/bro
|
|
2325
|
-
|
|
2326
|
-
Any other input is the source itself: a lone URL explains that webpage, an existing workspace file with a supported extension explains that file, and anything else is explained as pasted text. Quoted paths with spaces are routed too when the file exists.
|
|
2327
|
-
|
|
2328
|
-
Press **R** to simplify the captured source again. Run a new \`/bro text\`, \`/bro file\`, \`/bro url\`, or \`/bro show\` command — or give \`/bro\` the input directly — to capture a new source.
|
|
2329
|
-
|
|
2330
|
-
## Check and configure
|
|
2331
|
-
|
|
2332
|
-
- \`/bro doctor\` — check settings, backends, account, model, effort, and mode (per-feature backend/model/effort)
|
|
2333
|
-
- \`/bro model [id]\` — view or choose the shared default model (Agy catalog, sonnet/opus/explicit IDs on Claude, grok-4.7/grok-4.7-build-fast/explicit IDs on Grok)
|
|
2334
|
-
- \`/bro effort [low|medium|high|xhigh|max]\` — view or choose the shared default reasoning effort (xhigh on Claude/Grok, max on Claude only)
|
|
2335
|
-
- \`/bro mode [brief|balanced|faithful]\` — view or choose explanation mode
|
|
2336
|
-
- \`/bro config\` — open an interactive settings screen for the shared default backend/model/effort, explain mode, show turns, and per-capability (explain/show/btw/advisor) backend, model, and effort overrides. Changes save immediately; Esc on a picker cancels without changing anything, Esc on the screen closes it and keeps whatever was already saved.
|
|
2337
|
-
|
|
2338
|
-
\`/bro model\` and \`/bro effort\` always change the shared default that explain, show, btw, and advisor fall back to when they have no override. Use \`/bro config\` to give one of them its own backend, model, or effort. \`btw\` runs on Agy or Grok (Claude continuation is pending).
|
|
2316
|
+
- \`/bro <input>\` — a lone URL, an existing supported file, or anything else as pasted text
|
|
2317
|
+
- \`/bro open\` — reopen the latest explanation without a new request
|
|
2318
|
+
- \`/bro show [n-turns] [query]\` — draw recent turns' conversation text as shapes; an optional query steers the focus
|
|
2339
2319
|
|
|
2340
2320
|
## Side conversation
|
|
2341
2321
|
|
|
2342
|
-
- \`/bro btw [
|
|
2322
|
+
- \`/bro btw [question]\` — open a side conversation seeded with recent main-session context. It starts conversation-only; reopening keeps the thread and its mode.
|
|
2323
|
+
- Inside the modal: Enter asks (empty Enter re-asks); \`/mode\` toggles conversation-only / full permission (read and edit the workspace) and keeps the thread; \`/copy\` and \`/copy-all\` copy to the clipboard; \`/insert\` and \`/insert-all\` insert into an empty main editor without submitting; \`/retry\` re-asks; \`/clear\` resets. Any other text is sent as a question. Esc closes.
|
|
2343
2324
|
|
|
2344
2325
|
## Advisor
|
|
2345
2326
|
|
|
2346
|
-
- \`bro_advisor\` — a tool the executor agent
|
|
2347
|
-
- \`/bro advisor\` —
|
|
2348
|
-
- \`/bro advisor-steer\` —
|
|
2349
|
-
|
|
2327
|
+
- \`bro_advisor\` — a tool the executor agent may call for a second opinion from a fresh backend process with real, auto-approved workspace access; it is told to advise, not edit, but that is not enforced
|
|
2328
|
+
- \`/bro advisor\` — whether \`bro_advisor\` is available right now
|
|
2329
|
+
- \`/bro advisor-steer\` — edit the session's persistent steering brief (**Ctrl+S** save, **Ctrl+K** clear, **Ctrl+C** copy, **Esc** close)
|
|
2330
|
+
|
|
2331
|
+
## Configure and check
|
|
2350
2332
|
|
|
2351
|
-
|
|
2333
|
+
- \`/bro config\` — shared default and per-capability (explain/show/btw/advisor) backend, model, and effort; explain mode; show turns. Changes save immediately.
|
|
2334
|
+
- \`/bro mode [brief|balanced|faithful]\` — view or choose the explanation mode
|
|
2335
|
+
- \`/bro doctor\` — check settings, prompt, and every selected backend without running a model turn
|
|
2352
2336
|
|
|
2353
2337
|
## Current settings
|
|
2354
2338
|
|
|
2355
2339
|
${settingsSummary}
|
|
2356
2340
|
|
|
2357
|
-
Saved in \`${SETTINGS_FILE}\`.
|
|
2341
|
+
Saved in \`${SETTINGS_FILE}\`.
|
|
2358
2342
|
|
|
2359
2343
|
## Explanation modes
|
|
2360
2344
|
|
|
2361
2345
|
- brief — the main point and next action, with no fixed word target
|
|
2362
2346
|
- balanced — default; material detail with clearer structure
|
|
2363
2347
|
- faithful — closest to the source, with no fixed word limit
|
|
2364
|
-
|
|
2365
|
-
|
|
2348
|
+
|
|
2349
|
+
A valid \`${PROMPT_FILE}\` (with \`{{response}}\` exactly once) overrides the modes; the saved mode stays inactive until you remove or rename it. Show always uses its own prompt.
|
|
2366
2350
|
|
|
2367
2351
|
## Controls
|
|
2368
2352
|
|
|
2369
|
-
- **Mouse wheel / trackpad
|
|
2370
|
-
- **↑ / ↓** — scroll
|
|
2353
|
+
- **Mouse wheel / trackpad**, **↑ / ↓** — scroll
|
|
2371
2354
|
- **C** — copy the full explanation
|
|
2372
|
-
- **R** — repeat the current action
|
|
2355
|
+
- **R** — repeat the current action
|
|
2356
|
+
- **O** — open the HTML diagram when a show reply contains one
|
|
2373
2357
|
- **Esc** — close, or cancel while Bro is working
|
|
2374
2358
|
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
## Important limits
|
|
2378
|
-
|
|
2379
|
-
- Documents must be inside the current workspace, are limited to 10 MiB and 100,000 extracted characters, and must be \`.md\`, \`.markdown\`, \`.txt\`, \`.pdf\`, or \`.docx\`. Scanned PDFs need OCR first.
|
|
2380
|
-
- Web input is limited to one public HTML page. Bro cannot sign in, run page JavaScript, bypass paywalls or blocks, follow pagination, or understand images and video.
|
|
2381
|
-
- If a webpage fails, copy it into a text file or save it as a PDF, then use \`/bro file\`.
|
|
2382
|
-
- Show draws only what already happened in this session — the conversation text of the last few turns, with tool calls, tool results, reasoning, and images always omitted — and is requested not to investigate the repository; Grok retains normal tools, so this is not enforced isolation. On a remote or headless session with no display, pressing **O** reports a failure instead of opening the diagram.
|
|
2383
|
-
- Show reflects what was reported in the conversation, not independent verification against the actual code or system state.
|
|
2384
|
-
- Btw threads are memory-only and do not survive reloads or restarts. A turn is capped at 2 minutes in sandbox mode and 10 minutes in full mode; the side conversation resumes through Agy \`--conversation\` or Grok \`--resume\`.
|
|
2385
|
-
- Advisor consultations run with real tool access and auto-approved permissions (Grok: \`--sandbox off --permission-mode bypassPermissions\`; Agy/Claude: \`--dangerously-skip-permissions\`) — there is no enforced read-only isolation, only the advisor's own behavioral instructions to advise rather than implement. On invocation failure (not a completed answer), Bro retries with the identical snapshot, steering, and question: once after 5 seconds, once more after 10 seconds, then returns ${advisorName}'s own diagnostic as the failure.
|
|
2386
|
-
|
|
2387
|
-
## Privacy and safety
|
|
2388
|
-
|
|
2389
|
-
Bro sends the selected assistant reply, pasted text, locally extracted document or webpage text, or recent session conversation text (tool calls, tool results, reasoning, and images omitted) to the selected backend and its model provider. They may retain request data under their own policies.
|
|
2390
|
-
|
|
2391
|
-
Bro never adds the explanation to Pi's conversation, session file, or main-agent context. The captured source and latest explanation stay in process memory until you change sessions, reload extensions, or exit Pi.
|
|
2392
|
-
|
|
2393
|
-
Bro asks explain/show backends to use supplied context; Grok retains tool authority, so this is behavioral rather than enforced. \`/bro btw\` uses Agy sandbox controls or Grok conversation-only prompt instructions by default; with \`--full\` it can read and edit the workspace, so use \`--full\` only when you want the side conversation to touch your project.
|
|
2394
|
-
For webpages, it connects directly to the site without browser cookies; the site sees your IP address and Bro's user agent. Do not use private or signed URLs.
|
|
2359
|
+
## Privacy
|
|
2395
2360
|
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
Each advisor consultation sends the executor's system instructions, active tool list, ordered conversation (including tool calls and results, since the advisor needs to verify claims), your steering brief, and the executor's optional question to the selected backend and its model provider; the advisor process itself can read and edit the workspace with no permission prompts. The steering brief is stored as session-only extension data — never added to the main conversation Pi or the model sees; the advisor tool has no separate activation state.
|
|
2399
|
-
|
|
2400
|
-
## Custom prompt
|
|
2401
|
-
|
|
2402
|
-
Create or edit \`${PROMPT_FILE}\` and include \`{{response}}\` exactly once. Bro reads it on the next explanation and never modifies it. Existing valid custom prompts continue working unchanged.
|
|
2403
|
-
|
|
2404
|
-
A valid custom prompt fully overrides all built-in mode instructions. \`/bro mode\` still changes the saved mode, but that mode remains inactive until you remove or rename \`bro-prompt.md\`. An invalid custom prompt blocks explanations; run \`/bro doctor\` for the exact problem.`;
|
|
2361
|
+
Bro sends the captured source (or, for the advisor, the executor's instructions, tools, and conversation) to the selected backend and its model provider, which may retain it under their own policies. Nothing is added to Pi's conversation unless you insert it. Access controls differ by backend; see the README.`;
|
|
2405
2362
|
}
|
|
2406
2363
|
|
|
2407
2364
|
// The overlay framing pattern is adapted from pi-btw (MIT); see THIRD_PARTY_NOTICES.md.
|
|
@@ -2713,35 +2670,52 @@ async function showBroModal(ctx: ExtensionCommandContext, options: BroModalOptio
|
|
|
2713
2670
|
);
|
|
2714
2671
|
}
|
|
2715
2672
|
|
|
2716
|
-
export function parseBtwArguments(value: string): {
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
const token = space === -1 ? rest : rest.slice(0, space);
|
|
2723
|
-
if (token === "--fresh") fresh = true;
|
|
2724
|
-
else if (token === "--full") full = true;
|
|
2725
|
-
else if (token === "--sandbox") full = false;
|
|
2726
|
-
else return { fresh, full, question: "", invalid: `Unknown /bro btw flag: ${token}` };
|
|
2727
|
-
rest = space === -1 ? "" : rest.slice(space).replace(/^\s+/, "");
|
|
2673
|
+
export function parseBtwArguments(value: string): { question: string; invalid?: string } {
|
|
2674
|
+
const rest = value.trim();
|
|
2675
|
+
if (!rest.startsWith("--")) return { question: rest };
|
|
2676
|
+
const token = rest.split(/\s/, 1)[0]!;
|
|
2677
|
+
if (token === "--full" || token === "--sandbox") {
|
|
2678
|
+
return { question: "", invalid: `${token} was removed. Open /bro btw and type /mode to switch between conversation-only and full permission.` };
|
|
2728
2679
|
}
|
|
2729
|
-
|
|
2680
|
+
if (token === "--fresh") {
|
|
2681
|
+
return { question: "", invalid: "--fresh was removed. Every new thread starts with main-session context; type /clear inside /bro btw to start over." };
|
|
2682
|
+
}
|
|
2683
|
+
return { question: "", invalid: `Unknown /bro btw flag: ${token}` };
|
|
2730
2684
|
}
|
|
2731
2685
|
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
return !existing || startFresh ? { turns: [], full: targetFull } : existing;
|
|
2686
|
+
// A new thread starts conversation-only; reopening keeps the thread and its mode.
|
|
2687
|
+
export function resolveBtwThread(existing: BtwThread | undefined): BtwThread {
|
|
2688
|
+
return existing ?? { turns: [], full: false };
|
|
2736
2689
|
}
|
|
2737
2690
|
|
|
2738
2691
|
export function bindBtwBackend(thread: BtwThread, backend: BackendName): boolean {
|
|
2739
2692
|
const changed = thread.backend !== undefined && thread.backend !== backend;
|
|
2740
|
-
if (changed) { thread.turns = []; thread.conversationId = undefined; }
|
|
2693
|
+
if (changed) { thread.turns = []; thread.conversationId = undefined; thread.context = undefined; thread.sessionFull = undefined; }
|
|
2741
2694
|
thread.backend = backend;
|
|
2742
2695
|
return changed;
|
|
2743
2696
|
}
|
|
2744
2697
|
|
|
2698
|
+
export function btwModeLabel(full: boolean): string {
|
|
2699
|
+
return full ? "full permission" : "conversation-only";
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
// /mode keeps the transcript; whether the native session survives is decided by
|
|
2703
|
+
// nativeBtwContinuation on the next turn.
|
|
2704
|
+
export function toggleBtwMode(thread: BtwThread): void {
|
|
2705
|
+
thread.full = !thread.full;
|
|
2706
|
+
}
|
|
2707
|
+
|
|
2708
|
+
// Claude and Grok run btw in the workspace for both modes and resume one native session across
|
|
2709
|
+
// /mode switches. An Agy conversation stays bound to the workspace it started in (a sandbox scratch
|
|
2710
|
+
// dir vs. the repo), so an access change drops it; the next turn reseeds a fresh native session
|
|
2711
|
+
// with the main-session context and the whole thread.
|
|
2712
|
+
export function nativeBtwContinuation(thread: BtwThread, backend: BackendName): string | undefined {
|
|
2713
|
+
if (backend === "agy" && thread.conversationId && thread.sessionFull !== undefined && thread.sessionFull !== thread.full) {
|
|
2714
|
+
thread.conversationId = undefined;
|
|
2715
|
+
}
|
|
2716
|
+
return thread.conversationId;
|
|
2717
|
+
}
|
|
2718
|
+
|
|
2745
2719
|
export function formatBtwTranscript(turns: readonly BtwTurn[]): string {
|
|
2746
2720
|
return turns
|
|
2747
2721
|
.map((turn) => {
|
|
@@ -2756,26 +2730,23 @@ export function formatBtwTranscript(turns: readonly BtwTurn[]): string {
|
|
|
2756
2730
|
|
|
2757
2731
|
export type BtwComposerAction =
|
|
2758
2732
|
| { kind: "clear" }
|
|
2733
|
+
| { kind: "mode" }
|
|
2759
2734
|
| { kind: "retry" }
|
|
2760
2735
|
| { kind: "clipboard"; all: boolean }
|
|
2761
|
-
| { kind: "insert"; all: boolean
|
|
2736
|
+
| { kind: "insert"; all: boolean }
|
|
2737
|
+
| { kind: "removed"; command: string }
|
|
2762
2738
|
| { kind: "question"; text: string };
|
|
2763
2739
|
|
|
2764
2740
|
export function parseBtwComposerCommand(value: string): BtwComposerAction {
|
|
2765
2741
|
const command = value.trim();
|
|
2766
2742
|
if (command === "/clear") return { kind: "clear" };
|
|
2743
|
+
if (command === "/mode") return { kind: "mode" };
|
|
2767
2744
|
if (command === "/retry" || command === "") return { kind: "retry" };
|
|
2768
2745
|
if (command === "/copy" || command === "/copy-all") {
|
|
2769
2746
|
return { kind: "clipboard", all: command === "/copy-all" };
|
|
2770
2747
|
}
|
|
2771
|
-
if (
|
|
2772
|
-
|
|
2773
|
-
command === "/insert!" ||
|
|
2774
|
-
command === "/insert-all" ||
|
|
2775
|
-
command === "/insert-all!"
|
|
2776
|
-
) {
|
|
2777
|
-
return { kind: "insert", all: command.startsWith("/insert-all"), force: command.endsWith("!") };
|
|
2778
|
-
}
|
|
2748
|
+
if (command === "/insert" || command === "/insert-all") return { kind: "insert", all: command === "/insert-all" };
|
|
2749
|
+
if (command === "/insert!" || command === "/insert-all!") return { kind: "removed", command };
|
|
2779
2750
|
return { kind: "question", text: command };
|
|
2780
2751
|
}
|
|
2781
2752
|
|
|
@@ -2791,12 +2762,6 @@ async function runBtwTurn(
|
|
|
2791
2762
|
signal: AbortSignal,
|
|
2792
2763
|
onProgress?: (text: string) => void,
|
|
2793
2764
|
): Promise<{ text: string; conversationId?: string }> {
|
|
2794
|
-
// Claude continuation is not wired yet; Agy and Grok use native sessions.
|
|
2795
|
-
// Reject Claude explicitly instead of falling back to another backend.
|
|
2796
|
-
if (selection.backend === "claude") {
|
|
2797
|
-
const name = "Claude";
|
|
2798
|
-
throw new Error(`\`btw\` is not supported on the ${name} backend. Run \`/bro config\` to give it an Agy model.`);
|
|
2799
|
-
}
|
|
2800
2765
|
let updateTimer: ReturnType<typeof setTimeout> | undefined;
|
|
2801
2766
|
let latest: string | undefined;
|
|
2802
2767
|
const throttledProgress = onProgress
|
|
@@ -2950,14 +2915,14 @@ class BtwModal implements Focusable {
|
|
|
2950
2915
|
const scroll = this.maxOffset > 0 ? ` · ↑${this.offset} ↓${hiddenBelow}` : "";
|
|
2951
2916
|
|
|
2952
2917
|
const model = this.model ? this.theme.fg("dim", ` · ${this.model}`) : "";
|
|
2953
|
-
const mode = this.
|
|
2918
|
+
const mode = this.theme.fg("dim", " · ") + (this.full ? this.theme.fg("accent", this.theme.bold(btwModeLabel(true))) : this.theme.fg("dim", btwModeLabel(false)));
|
|
2954
2919
|
const header = this.theme.fg("accent", this.theme.bold("Bro · btw")) + model + mode + this.theme.fg("dim", scroll);
|
|
2955
2920
|
|
|
2956
2921
|
const composer = this.input.render(innerWidth)[0] ?? "";
|
|
2957
2922
|
|
|
2958
2923
|
const controls = this.running
|
|
2959
2924
|
? this.theme.fg("dim", "Thinking… · Esc cancel")
|
|
2960
|
-
: this.theme.fg("dim", "Enter ask · Esc close · /copy · /copy-all · /insert · /insert-all · /clear · /retry");
|
|
2925
|
+
: this.theme.fg("dim", "Enter ask · Esc close · /mode · /copy · /copy-all · /insert · /insert-all · /clear · /retry");
|
|
2961
2926
|
|
|
2962
2927
|
const lines = [
|
|
2963
2928
|
this.borderLine(innerWidth, "top"),
|
|
@@ -2983,7 +2948,7 @@ class BtwModal implements Focusable {
|
|
|
2983
2948
|
|
|
2984
2949
|
async function openBtwModal(
|
|
2985
2950
|
ctx: ExtensionCommandContext,
|
|
2986
|
-
options: { thread: BtwThread; initialQuestion?: string
|
|
2951
|
+
options: { thread: BtwThread; initialQuestion?: string },
|
|
2987
2952
|
): Promise<void> {
|
|
2988
2953
|
const thread = options.thread;
|
|
2989
2954
|
|
|
@@ -3021,15 +2986,20 @@ async function openBtwModal(
|
|
|
3021
2986
|
} catch (error) {
|
|
3022
2987
|
controller = undefined; modal.setRunning(false); modal.setNotice(errorMessage(error)); return;
|
|
3023
2988
|
}
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
const captured = captureShowTranscript(ctx, BTW_CONTEXT_TURNS);
|
|
3028
|
-
context = captured?.text;
|
|
2989
|
+
if (thread.turns.length === 0) {
|
|
2990
|
+
thread.conversationId = undefined;
|
|
2991
|
+
let context = captureShowTranscript(ctx, BTW_CONTEXT_TURNS)?.text;
|
|
3029
2992
|
if (context && context.length > BTW_CONTEXT_MAX) {
|
|
3030
2993
|
context = `${context.slice(0, BTW_CONTEXT_MAX)}\n[… context truncated …]`;
|
|
3031
2994
|
}
|
|
2995
|
+
thread.context = context;
|
|
3032
2996
|
}
|
|
2997
|
+
// Resume natively when possible; otherwise seed the fresh native session with the main-session
|
|
2998
|
+
// context plus every earlier turn so nothing is silently lost.
|
|
2999
|
+
const conversationId = nativeBtwContinuation(thread, capabilityBackend(settings, "btw"));
|
|
3000
|
+
const history = !conversationId && thread.turns.length ? formatBtwTranscript(thread.turns) : undefined;
|
|
3001
|
+
const context = conversationId ? undefined : thread.context;
|
|
3002
|
+
const full = thread.full;
|
|
3033
3003
|
|
|
3034
3004
|
thread.turns.push({ question, answer: "…" });
|
|
3035
3005
|
modal.setText(transcript());
|
|
@@ -3039,9 +3009,9 @@ async function openBtwModal(
|
|
|
3039
3009
|
thread.model = selectionLabel(selection);
|
|
3040
3010
|
modal.setModel(thread.model);
|
|
3041
3011
|
const result = await runBtwTurn(
|
|
3042
|
-
buildBtwPrompt(context, question),
|
|
3012
|
+
buildBtwPrompt(context, question, { full, history }),
|
|
3043
3013
|
selection,
|
|
3044
|
-
{ full
|
|
3014
|
+
{ full, cwd: ctx.cwd, conversationId },
|
|
3045
3015
|
turnController.signal,
|
|
3046
3016
|
(partial) => {
|
|
3047
3017
|
if (closed || turnController.signal.aborted) return;
|
|
@@ -3050,7 +3020,10 @@ async function openBtwModal(
|
|
|
3050
3020
|
},
|
|
3051
3021
|
);
|
|
3052
3022
|
if (turnController.signal.aborted) return;
|
|
3053
|
-
if (result.conversationId)
|
|
3023
|
+
if (result.conversationId) {
|
|
3024
|
+
thread.conversationId = result.conversationId;
|
|
3025
|
+
thread.sessionFull = full;
|
|
3026
|
+
}
|
|
3054
3027
|
thread.turns[thread.turns.length - 1]!.answer = result.text;
|
|
3055
3028
|
} catch (error) {
|
|
3056
3029
|
if (turnController.signal.aborted || closed) return;
|
|
@@ -3065,9 +3038,16 @@ async function openBtwModal(
|
|
|
3065
3038
|
}
|
|
3066
3039
|
};
|
|
3067
3040
|
|
|
3041
|
+
const toggleMode = () => {
|
|
3042
|
+
toggleBtwMode(thread);
|
|
3043
|
+
modal.setFull(thread.full);
|
|
3044
|
+
modal.setNotice(`Mode: ${btwModeLabel(thread.full)}`);
|
|
3045
|
+
};
|
|
3046
|
+
|
|
3068
3047
|
const clear = () => {
|
|
3069
3048
|
thread.turns = [];
|
|
3070
3049
|
thread.conversationId = undefined;
|
|
3050
|
+
thread.context = undefined;
|
|
3071
3051
|
modal.clearComposer();
|
|
3072
3052
|
modal.setNotice("");
|
|
3073
3053
|
modal.setText("");
|
|
@@ -3098,14 +3078,14 @@ async function openBtwModal(
|
|
|
3098
3078
|
}
|
|
3099
3079
|
};
|
|
3100
3080
|
|
|
3101
|
-
const insert = (all: boolean
|
|
3081
|
+
const insert = (all: boolean) => {
|
|
3102
3082
|
const text = all ? transcript() : (thread.turns.at(-1)?.answer ?? "");
|
|
3103
3083
|
if (!text.trim()) {
|
|
3104
3084
|
modal.setNotice("Nothing to insert yet.");
|
|
3105
3085
|
return;
|
|
3106
3086
|
}
|
|
3107
|
-
if (ctx.ui.getEditorText().trim()
|
|
3108
|
-
modal.setNotice("Main editor has a draft.
|
|
3087
|
+
if (ctx.ui.getEditorText().trim()) {
|
|
3088
|
+
modal.setNotice("Main editor has a draft. Edit or clear it first, then insert again.");
|
|
3109
3089
|
return;
|
|
3110
3090
|
}
|
|
3111
3091
|
ctx.ui.setEditorText(text);
|
|
@@ -3119,6 +3099,11 @@ async function openBtwModal(
|
|
|
3119
3099
|
clear();
|
|
3120
3100
|
return;
|
|
3121
3101
|
}
|
|
3102
|
+
if (action.kind === "mode") {
|
|
3103
|
+
modal.clearComposer();
|
|
3104
|
+
toggleMode();
|
|
3105
|
+
return;
|
|
3106
|
+
}
|
|
3122
3107
|
if (action.kind === "clipboard") {
|
|
3123
3108
|
modal.clearComposer();
|
|
3124
3109
|
void copyOut(action.all);
|
|
@@ -3126,7 +3111,12 @@ async function openBtwModal(
|
|
|
3126
3111
|
}
|
|
3127
3112
|
if (action.kind === "insert") {
|
|
3128
3113
|
modal.clearComposer();
|
|
3129
|
-
insert(action.all
|
|
3114
|
+
insert(action.all);
|
|
3115
|
+
return;
|
|
3116
|
+
}
|
|
3117
|
+
if (action.kind === "removed") {
|
|
3118
|
+
modal.clearComposer();
|
|
3119
|
+
modal.setNotice(`${action.command} was removed. Edit or clear the main editor draft, then use /insert or /insert-all.`);
|
|
3130
3120
|
return;
|
|
3131
3121
|
}
|
|
3132
3122
|
if (action.kind === "retry") {
|
|
@@ -3288,6 +3278,12 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
3288
3278
|
let action = parts[0] ?? "";
|
|
3289
3279
|
let value = raw.slice(raw.split(/\s+/, 1)[0]?.length ?? 0).trim();
|
|
3290
3280
|
|
|
3281
|
+
// Removed commands must never fall through to a paid text explanation.
|
|
3282
|
+
if (action === "model" || action === "effort") {
|
|
3283
|
+
ctx.ui.notify(`/bro ${action} was removed. Use /bro config to choose the shared default and per-capability ${action}; use /bro text <text> to explain text.`, "warning");
|
|
3284
|
+
return;
|
|
3285
|
+
}
|
|
3286
|
+
|
|
3291
3287
|
// An unknown first word means the whole input is the source: route it by shape.
|
|
3292
3288
|
if (action && !KNOWN_ACTIONS.has(action)) {
|
|
3293
3289
|
const candidate = unquote(raw);
|
|
@@ -3422,253 +3418,6 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
3422
3418
|
return;
|
|
3423
3419
|
}
|
|
3424
3420
|
|
|
3425
|
-
if (action === "model") {
|
|
3426
|
-
if (parts.length > 2) {
|
|
3427
|
-
ctx.ui.notify("Use /bro model or /bro model <id>.", "warning");
|
|
3428
|
-
return;
|
|
3429
|
-
}
|
|
3430
|
-
try {
|
|
3431
|
-
const settings = await readSettings();
|
|
3432
|
-
// /bro model operates on the effective shared backend: no Agy catalog
|
|
3433
|
-
// when the shared default is Claude/Grok.
|
|
3434
|
-
if ((settings.backend ?? "agy") === "grok") {
|
|
3435
|
-
const requested = value.trim();
|
|
3436
|
-
let model: string | undefined;
|
|
3437
|
-
if (requested) {
|
|
3438
|
-
try {
|
|
3439
|
-
model = resolveGrokModel(requested);
|
|
3440
|
-
} catch {
|
|
3441
|
-
ctx.ui.notify("Use /bro model or /bro model <grok-4.7|grok-4.7-build-fast|model-id>.", "warning");
|
|
3442
|
-
return;
|
|
3443
|
-
}
|
|
3444
|
-
} else {
|
|
3445
|
-
if (ctx.mode !== "tui") {
|
|
3446
|
-
ctx.ui.notify("Use /bro model <grok-4.7|grok-4.7-build-fast|model-id> outside Pi's interactive UI.", "warning");
|
|
3447
|
-
return;
|
|
3448
|
-
}
|
|
3449
|
-
const choices = [
|
|
3450
|
-
...GROK_MODELS.map(
|
|
3451
|
-
(entry) => `${entry.id} — ${entry.label}${entry.id === settings.model ? " (current)" : ""}`,
|
|
3452
|
-
),
|
|
3453
|
-
"Custom — enter a model ID…",
|
|
3454
|
-
];
|
|
3455
|
-
const choice = await ctx.ui.select(`Grok model (current: ${settings.model})`, choices);
|
|
3456
|
-
if (!choice) return;
|
|
3457
|
-
if (choice.startsWith("Custom")) {
|
|
3458
|
-
const input = await ctx.ui.input("Grok model", "grok-4.7, grok-4.7-build-fast, or an explicit model ID");
|
|
3459
|
-
if (!input?.trim()) return;
|
|
3460
|
-
model = resolveGrokModel(input);
|
|
3461
|
-
} else {
|
|
3462
|
-
model = GROK_MODELS[choices.indexOf(choice)]?.id;
|
|
3463
|
-
}
|
|
3464
|
-
}
|
|
3465
|
-
if (!model) return;
|
|
3466
|
-
const effort = settings.backend === "grok" && isGrokEffort(settings.effort) ? settings.effort : "default";
|
|
3467
|
-
await writeSettings({ ...settings, backend: "grok", model, effort });
|
|
3468
|
-
ctx.ui.notify(`Bro model: grok ${model}${effort === "default" ? "" : ` (${effort})`}`, "info");
|
|
3469
|
-
return;
|
|
3470
|
-
}
|
|
3471
|
-
if ((settings.backend ?? "agy") === "claude") {
|
|
3472
|
-
const requested = value.trim();
|
|
3473
|
-
let model: string | undefined;
|
|
3474
|
-
if (requested) {
|
|
3475
|
-
try {
|
|
3476
|
-
model = resolveClaudeModel(requested);
|
|
3477
|
-
} catch {
|
|
3478
|
-
ctx.ui.notify("Use /bro model or /bro model <sonnet|opus|model-id>.", "warning");
|
|
3479
|
-
return;
|
|
3480
|
-
}
|
|
3481
|
-
} else {
|
|
3482
|
-
if (ctx.mode !== "tui") {
|
|
3483
|
-
ctx.ui.notify("Use /bro model <sonnet|opus|model-id> outside Pi's interactive UI.", "warning");
|
|
3484
|
-
return;
|
|
3485
|
-
}
|
|
3486
|
-
const choices = [
|
|
3487
|
-
...CLAUDE_MODELS.map(
|
|
3488
|
-
(entry) => `${entry.id} — ${entry.label}${entry.id === settings.model ? " (current)" : ""}`,
|
|
3489
|
-
),
|
|
3490
|
-
"Custom — enter a model ID…",
|
|
3491
|
-
];
|
|
3492
|
-
const choice = await ctx.ui.select(`Claude model (current: ${settings.model})`, choices);
|
|
3493
|
-
if (!choice) return;
|
|
3494
|
-
if (choice.startsWith("Custom")) {
|
|
3495
|
-
const input = await ctx.ui.input("Claude model", "sonnet, opus, or an explicit model ID");
|
|
3496
|
-
if (!input?.trim()) return;
|
|
3497
|
-
model = resolveClaudeModel(input);
|
|
3498
|
-
} else {
|
|
3499
|
-
model = CLAUDE_MODELS[choices.indexOf(choice)]?.id;
|
|
3500
|
-
}
|
|
3501
|
-
}
|
|
3502
|
-
if (!model) return;
|
|
3503
|
-
const effort = settings.backend === "claude" && isClaudeEffort(settings.effort) ? settings.effort : "default";
|
|
3504
|
-
await writeSettings({ ...settings, backend: "claude", model, effort });
|
|
3505
|
-
ctx.ui.notify(`Bro model: claude ${model}${effort === "default" ? "" : ` (${effort})`}`, "info");
|
|
3506
|
-
return;
|
|
3507
|
-
}
|
|
3508
|
-
const models = await listAgyModels(pi);
|
|
3509
|
-
const current = resolveCatalogSettings(settings, models);
|
|
3510
|
-
const requested = parts[1];
|
|
3511
|
-
let selected: AgyModelFamily | undefined;
|
|
3512
|
-
let selectedEffort: BroEffort | undefined;
|
|
3513
|
-
if (requested) {
|
|
3514
|
-
selected = models.find((item) => item.id.toLowerCase() === requested);
|
|
3515
|
-
if (!selected) {
|
|
3516
|
-
for (const family of models) {
|
|
3517
|
-
const variant = family.variants.find((item) => item.id.toLowerCase() === requested);
|
|
3518
|
-
if (variant) {
|
|
3519
|
-
selected = family;
|
|
3520
|
-
selectedEffort = variant.effort ?? "default";
|
|
3521
|
-
break;
|
|
3522
|
-
}
|
|
3523
|
-
}
|
|
3524
|
-
}
|
|
3525
|
-
if (!selected) {
|
|
3526
|
-
ctx.ui.notify(`Unknown Agy model "${requested}". Run /bro model to see available choices.`, "warning");
|
|
3527
|
-
return;
|
|
3528
|
-
}
|
|
3529
|
-
} else {
|
|
3530
|
-
if (ctx.mode !== "tui") {
|
|
3531
|
-
ctx.ui.notify("Use /bro model <id> outside Pi's interactive UI.", "warning");
|
|
3532
|
-
return;
|
|
3533
|
-
}
|
|
3534
|
-
const ordered = [...models].sort((a, b) => Number(b.id === current.family?.id) - Number(a.id === current.family?.id));
|
|
3535
|
-
const choices = ordered.map(
|
|
3536
|
-
(item) =>
|
|
3537
|
-
`${item.id} — ${item.label} · ${item.efforts.length ? item.efforts.join("/") : "fixed effort"}${item.id === current.family?.id ? " (current)" : ""}`,
|
|
3538
|
-
);
|
|
3539
|
-
const choice = await ctx.ui.select(`Agy model (current: ${current.settings.model})`, choices);
|
|
3540
|
-
if (!choice) return;
|
|
3541
|
-
selected = ordered[choices.indexOf(choice)];
|
|
3542
|
-
}
|
|
3543
|
-
if (!selectedEffort) {
|
|
3544
|
-
const currentEffort = current.settings.effort;
|
|
3545
|
-
const canKeepCurrent =
|
|
3546
|
-
current.family?.id === selected.id &&
|
|
3547
|
-
(currentEffort === "default" ? !selected.efforts.length : selected.efforts.includes(currentEffort as AgyEffort));
|
|
3548
|
-
selectedEffort = canKeepCurrent ? currentEffort : preferredEffort(selected);
|
|
3549
|
-
}
|
|
3550
|
-
await writeSettings({ ...settings, model: selected.id, effort: selectedEffort });
|
|
3551
|
-
ctx.ui.notify(
|
|
3552
|
-
`Bro model: ${selected.id}${selectedEffort === "default" ? "" : ` (${selectedEffort})`}`,
|
|
3553
|
-
"info",
|
|
3554
|
-
);
|
|
3555
|
-
} catch (error) {
|
|
3556
|
-
ctx.ui.notify(withDoctor(error), "error");
|
|
3557
|
-
}
|
|
3558
|
-
return;
|
|
3559
|
-
}
|
|
3560
|
-
|
|
3561
|
-
if (action === "effort") {
|
|
3562
|
-
const requested = parts[1];
|
|
3563
|
-
// Claude/Grok levels pass this gate; each backend branch below validates strictly
|
|
3564
|
-
// (the Agy branch still rejects xhigh/max with its supports-list warning).
|
|
3565
|
-
if (parts.length > 2 || (requested && !isClaudeEffort(requested) && !EFFORTS.some((effort) => effort === requested))) {
|
|
3566
|
-
ctx.ui.notify("Use /bro effort, or choose low, medium, or high.", "warning");
|
|
3567
|
-
return;
|
|
3568
|
-
}
|
|
3569
|
-
try {
|
|
3570
|
-
const settings = await readSettings();
|
|
3571
|
-
// /bro effort operates on the effective shared backend: no Agy catalog
|
|
3572
|
-
// when the shared default is Claude/Grok.
|
|
3573
|
-
if ((settings.backend ?? "agy") === "grok") {
|
|
3574
|
-
const allowed = ["default", ...GROK_EFFORTS] as const;
|
|
3575
|
-
if (requested && !allowed.some((effort) => effort === requested)) {
|
|
3576
|
-
ctx.ui.notify("Use /bro effort, or choose default, low, medium, high, or xhigh.", "warning");
|
|
3577
|
-
return;
|
|
3578
|
-
}
|
|
3579
|
-
let selected = requested as BroEffort | undefined;
|
|
3580
|
-
if (!selected) {
|
|
3581
|
-
if (ctx.mode !== "tui") {
|
|
3582
|
-
ctx.ui.notify("Use /bro effort <default|low|medium|high|xhigh> outside Pi's interactive UI.", "warning");
|
|
3583
|
-
return;
|
|
3584
|
-
}
|
|
3585
|
-
const efforts = [...allowed].sort(
|
|
3586
|
-
(a, b) => Number(b === settings.effort) - Number(a === settings.effort),
|
|
3587
|
-
);
|
|
3588
|
-
const choices = efforts.map((effort) => `${effort}${effort === settings.effort ? " (current)" : ""}`);
|
|
3589
|
-
const choice = await ctx.ui.select(`Grok reasoning effort (current: ${settings.effort})`, choices);
|
|
3590
|
-
if (!choice) return;
|
|
3591
|
-
selected = efforts[choices.indexOf(choice)];
|
|
3592
|
-
}
|
|
3593
|
-
if (!selected || !isGrokEffort(selected)) return;
|
|
3594
|
-
await writeSettings({ ...settings, backend: "grok", model: settings.model, effort: selected });
|
|
3595
|
-
ctx.ui.notify(
|
|
3596
|
-
selected === "default" ? "Bro reasoning effort: built into the selected model" : `Bro reasoning effort: ${selected}`,
|
|
3597
|
-
"info",
|
|
3598
|
-
);
|
|
3599
|
-
return;
|
|
3600
|
-
}
|
|
3601
|
-
if ((settings.backend ?? "agy") === "claude") {
|
|
3602
|
-
const allowed = ["default", ...CLAUDE_EFFORTS] as const;
|
|
3603
|
-
if (requested && !allowed.some((effort) => effort === requested)) {
|
|
3604
|
-
ctx.ui.notify("Use /bro effort, or choose default, low, medium, high, xhigh, or max.", "warning");
|
|
3605
|
-
return;
|
|
3606
|
-
}
|
|
3607
|
-
let selected = requested as BroEffort | undefined;
|
|
3608
|
-
if (!selected) {
|
|
3609
|
-
if (ctx.mode !== "tui") {
|
|
3610
|
-
ctx.ui.notify("Use /bro effort <default|low|medium|high|xhigh|max> outside Pi's interactive UI.", "warning");
|
|
3611
|
-
return;
|
|
3612
|
-
}
|
|
3613
|
-
const efforts = [...CLAUDE_EFFORTS].sort(
|
|
3614
|
-
(a, b) => Number(b === settings.effort) - Number(a === settings.effort),
|
|
3615
|
-
);
|
|
3616
|
-
const choices = efforts.map((effort) => `${effort}${effort === settings.effort ? " (current)" : ""}`);
|
|
3617
|
-
const choice = await ctx.ui.select(`Claude reasoning effort (current: ${settings.effort})`, choices);
|
|
3618
|
-
if (!choice) return;
|
|
3619
|
-
selected = efforts[choices.indexOf(choice)];
|
|
3620
|
-
}
|
|
3621
|
-
if (!selected || !isClaudeEffort(selected)) return;
|
|
3622
|
-
await writeSettings({ ...settings, backend: "claude", model: settings.model, effort: selected });
|
|
3623
|
-
ctx.ui.notify(
|
|
3624
|
-
selected === "default" ? "Bro reasoning effort: built into the selected model" : `Bro reasoning effort: ${selected}`,
|
|
3625
|
-
"info",
|
|
3626
|
-
);
|
|
3627
|
-
return;
|
|
3628
|
-
}
|
|
3629
|
-
const current = resolveCatalogSettings(settings, await listAgyModels(pi));
|
|
3630
|
-
if (!current.family) {
|
|
3631
|
-
ctx.ui.notify(`Model "${settings.model}" is not in Agy's current model list. Run /bro model first.`, "warning");
|
|
3632
|
-
return;
|
|
3633
|
-
}
|
|
3634
|
-
if (!current.family.efforts.length) {
|
|
3635
|
-
if (requested && requested !== "default") {
|
|
3636
|
-
ctx.ui.notify(`${current.family.label} uses a fixed effort level.`, "warning");
|
|
3637
|
-
return;
|
|
3638
|
-
}
|
|
3639
|
-
await writeSettings({ ...current.settings, model: current.family.id, effort: "default" });
|
|
3640
|
-
ctx.ui.notify(`${current.family.label} uses its built-in effort level.`, "info");
|
|
3641
|
-
return;
|
|
3642
|
-
}
|
|
3643
|
-
if (requested === "default" || (requested && !current.family.efforts.includes(requested as AgyEffort))) {
|
|
3644
|
-
ctx.ui.notify(
|
|
3645
|
-
`${current.family.label} supports ${current.family.efforts.join(" or ")} effort.`,
|
|
3646
|
-
"warning",
|
|
3647
|
-
);
|
|
3648
|
-
return;
|
|
3649
|
-
}
|
|
3650
|
-
let selected = requested as AgyEffort | undefined;
|
|
3651
|
-
if (!selected) {
|
|
3652
|
-
if (ctx.mode !== "tui") {
|
|
3653
|
-
ctx.ui.notify("Use /bro effort <low|medium|high> outside Pi's interactive UI.", "warning");
|
|
3654
|
-
return;
|
|
3655
|
-
}
|
|
3656
|
-
const efforts = [...current.family.efforts].sort(
|
|
3657
|
-
(a, b) => Number(b === current.settings.effort) - Number(a === current.settings.effort),
|
|
3658
|
-
);
|
|
3659
|
-
const choices = efforts.map((effort) => `${effort}${effort === current.settings.effort ? " (current)" : ""}`);
|
|
3660
|
-
const choice = await ctx.ui.select(`Agy reasoning effort (current: ${current.settings.effort})`, choices);
|
|
3661
|
-
if (!choice) return;
|
|
3662
|
-
selected = efforts[choices.indexOf(choice)];
|
|
3663
|
-
}
|
|
3664
|
-
await writeSettings({ ...current.settings, model: current.family.id, effort: selected });
|
|
3665
|
-
ctx.ui.notify(`Bro reasoning effort: ${selected}`, "info");
|
|
3666
|
-
} catch (error) {
|
|
3667
|
-
ctx.ui.notify(withDoctor(error), "error");
|
|
3668
|
-
}
|
|
3669
|
-
return;
|
|
3670
|
-
}
|
|
3671
|
-
|
|
3672
3421
|
if (action === "btw") {
|
|
3673
3422
|
if (ctx.mode !== "tui") {
|
|
3674
3423
|
ctx.ui.notify("Use /bro btw in Pi's interactive UI.", "warning");
|
|
@@ -3681,11 +3430,10 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
3681
3430
|
}
|
|
3682
3431
|
try {
|
|
3683
3432
|
const btwBackend = capabilityBackend(await readSettings(), "btw");
|
|
3684
|
-
|
|
3685
|
-
const thread = resolveBtwThread(btwThread, parsed);
|
|
3433
|
+
const thread = resolveBtwThread(btwThread);
|
|
3686
3434
|
if (bindBtwBackend(thread, btwBackend)) ctx.ui.notify("Backend changed — started a fresh side thread.", "info");
|
|
3687
3435
|
btwThread = thread;
|
|
3688
|
-
await openBtwModal(ctx, { thread, initialQuestion: parsed.question
|
|
3436
|
+
await openBtwModal(ctx, { thread, initialQuestion: parsed.question });
|
|
3689
3437
|
} catch (error) {
|
|
3690
3438
|
ctx.ui.notify(withDoctor(error), "error");
|
|
3691
3439
|
}
|