pi-bro 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +55 -27
- package/backend.ts +277 -7
- package/bro.ts +342 -115
- package/package.json +2 -2
package/bro.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
agySelection,
|
|
23
23
|
advisorFlagErrorHint,
|
|
24
24
|
CLAUDE_EFFORTS,
|
|
25
|
+
GROK_EFFORTS,
|
|
25
26
|
backendSupports,
|
|
26
27
|
execute as executeBackend,
|
|
27
28
|
parseBtwAgyLine,
|
|
@@ -59,16 +60,18 @@ type TuiLike = {
|
|
|
59
60
|
};
|
|
60
61
|
type ModalKind = "loading" | "streaming" | "result" | "help" | "empty" | "error";
|
|
61
62
|
type BroSource = { text: string; label?: string };
|
|
62
|
-
type BroResult = { source: BroSource; text: string };
|
|
63
|
-
type ModalResult = { source?: BroSource; text: string; htmlPath?: string };
|
|
63
|
+
type BroResult = { source: BroSource; text: string; model?: string };
|
|
64
|
+
type ModalResult = { source?: BroSource; text: string; htmlPath?: string; model?: string };
|
|
64
65
|
type BtwTurn = { question: string; answer: string };
|
|
65
|
-
type BtwThread = { turns: BtwTurn[]; conversationId?: string; full: boolean };
|
|
66
|
+
type BtwThread = { turns: BtwTurn[]; conversationId?: string; full: boolean; backend?: BackendName; model?: string };
|
|
66
67
|
const EFFORTS = ["default", "low", "medium", "high"] as const;
|
|
67
|
-
const BACKENDS = ["agy", "claude"] as const;
|
|
68
|
+
const BACKENDS = ["agy", "claude", "grok"] as const;
|
|
68
69
|
type BackendName = (typeof BACKENDS)[number];
|
|
69
70
|
type BroEffort = "default" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
70
71
|
type AgyEffort = Exclude<BroEffort, "default" | "xhigh" | "max">;
|
|
71
72
|
type ClaudeEffort = (typeof CLAUDE_EFFORTS)[number];
|
|
73
|
+
export { GROK_EFFORTS };
|
|
74
|
+
type GrokEffort = (typeof GROK_EFFORTS)[number];
|
|
72
75
|
// "advisor" is a real capability (command, Agy invocation, Doctor check) like the other three;
|
|
73
76
|
// see docs/plans/2026-09-19-bro-advisor-design.md. All four share one model/effort resolution,
|
|
74
77
|
// override, and Doctor-check path via this single list -- there is no configuration-only tier.
|
|
@@ -90,6 +93,11 @@ export const CLAUDE_MODELS = [
|
|
|
90
93
|
{ id: "sonnet", label: "Claude Sonnet" },
|
|
91
94
|
{ id: "opus", label: "Claude Opus" },
|
|
92
95
|
] as const;
|
|
96
|
+
export const GROK_MODELS = [
|
|
97
|
+
{ id: "grok-4.7", label: "Grok 4.7" },
|
|
98
|
+
{ id: "grok-4.7-build-fast", label: "Grok 4.7 Build Fast" },
|
|
99
|
+
] as const;
|
|
100
|
+
export { backendSupports as supportsBackend };
|
|
93
101
|
type AgyModelFamily = {
|
|
94
102
|
id: string;
|
|
95
103
|
label: string;
|
|
@@ -114,12 +122,11 @@ const COMMANDS = [
|
|
|
114
122
|
{ value: "url", label: "url", description: "Explain a public webpage" },
|
|
115
123
|
{ value: "open", label: "open", description: "Reopen the last explanation" },
|
|
116
124
|
{ value: "doctor", label: "doctor", description: "Check whether Bro is ready" },
|
|
117
|
-
{ value: "
|
|
118
|
-
{ value: "model", label: "model", description: "View or choose the shared default Agy model" },
|
|
125
|
+
{ value: "model", label: "model", description: "View or choose the shared default model" },
|
|
119
126
|
{ value: "effort", label: "effort", description: "View or choose the shared default reasoning effort" },
|
|
120
127
|
{ value: "show", label: "show", description: "Draw what happened in recent session turns as shapes" },
|
|
121
128
|
{ value: "mode", label: "mode", description: "View or choose explanation mode (brief, balanced, faithful)" },
|
|
122
|
-
{ value: "btw", label: "btw", description: "Open a side conversation (
|
|
129
|
+
{ value: "btw", label: "btw", description: "Open a side conversation (context-only intent; --full invites workspace access)" },
|
|
123
130
|
{ value: "config", label: "config", description: "Configure shared defaults and per-capability model/effort overrides" },
|
|
124
131
|
{ value: "advisor", label: "advisor", description: "Check whether the executor's advisor tool is available right now" },
|
|
125
132
|
{ value: "advisor-steer", label: "advisor-steer", description: "View, edit, save, or clear the advisor's persistent steering brief" },
|
|
@@ -501,7 +508,7 @@ export async function extractWebPage(input: string, signal?: AbortSignal): Promi
|
|
|
501
508
|
|
|
502
509
|
function parseBackend(value: unknown, context: string): BackendName {
|
|
503
510
|
if (typeof value !== "string" || !BACKENDS.some((backend) => backend === value)) {
|
|
504
|
-
throw new Error(`${context} backend must be "agy" or "
|
|
511
|
+
throw new Error(`${context} backend must be "agy", "claude", or "grok".`);
|
|
505
512
|
}
|
|
506
513
|
return value as BackendName;
|
|
507
514
|
}
|
|
@@ -514,6 +521,10 @@ function isClaudeEffort(effort: unknown): effort is ClaudeEffort | "default" {
|
|
|
514
521
|
return effort === "default" || CLAUDE_EFFORTS.some((item) => item === effort);
|
|
515
522
|
}
|
|
516
523
|
|
|
524
|
+
function isGrokEffort(effort: unknown): effort is GrokEffort | "default" {
|
|
525
|
+
return effort === "default" || GROK_EFFORTS.some((item) => item === effort);
|
|
526
|
+
}
|
|
527
|
+
|
|
517
528
|
function parseModelEffortPair(value: unknown, context: string): ModelEffortPair {
|
|
518
529
|
if (!isRecord(value) || typeof value.model !== "string" || !value.model.trim()) {
|
|
519
530
|
throw new Error(`${context} must contain a model and effort set to "default", "low", "medium", or "high".`);
|
|
@@ -525,6 +536,12 @@ function parseModelEffortPair(value: unknown, context: string): ModelEffortPair
|
|
|
525
536
|
}
|
|
526
537
|
return { backend, model: value.model.trim(), effort: value.effort };
|
|
527
538
|
}
|
|
539
|
+
if (backend === "grok") {
|
|
540
|
+
if (!isGrokEffort(value.effort)) {
|
|
541
|
+
throw new Error(`${context} must contain a model and effort set to "default", "low", "medium", "high", or "xhigh".`);
|
|
542
|
+
}
|
|
543
|
+
return { backend, model: value.model.trim(), effort: value.effort };
|
|
544
|
+
}
|
|
528
545
|
if (!isAgyEffort(value.effort)) {
|
|
529
546
|
throw new Error(`${context} must contain a model and effort set to "default", "low", "medium", or "high".`);
|
|
530
547
|
}
|
|
@@ -573,12 +590,14 @@ export function parseBroSettings(value: unknown): BroSettings {
|
|
|
573
590
|
throw new Error('Settings must contain a model and effort set to "default", "low", "medium", or "high".');
|
|
574
591
|
}
|
|
575
592
|
const backend = value.backend === undefined ? undefined : parseBackend(value.backend, "Settings");
|
|
576
|
-
const effortOk = backend === "claude" ? isClaudeEffort(value.effort) : isAgyEffort(value.effort);
|
|
593
|
+
const effortOk = backend === "claude" ? isClaudeEffort(value.effort) : backend === "grok" ? isGrokEffort(value.effort) : isAgyEffort(value.effort);
|
|
577
594
|
if (!effortOk) {
|
|
578
595
|
throw new Error(
|
|
579
596
|
backend === "claude"
|
|
580
597
|
? 'Settings must contain a model and effort set to "default", "low", "medium", "high", "xhigh", or "max".'
|
|
581
|
-
:
|
|
598
|
+
: backend === "grok"
|
|
599
|
+
? 'Settings must contain a model and effort set to "default", "low", "medium", "high", or "xhigh".'
|
|
600
|
+
: 'Settings must contain a model and effort set to "default", "low", "medium", or "high".',
|
|
582
601
|
);
|
|
583
602
|
}
|
|
584
603
|
const mode = value.mode === undefined ? DEFAULT_BRO_MODE : parseBroMode(value.mode);
|
|
@@ -630,8 +649,8 @@ export function resolveModelEffort(
|
|
|
630
649
|
pair: ModelEffortPair,
|
|
631
650
|
families: AgyModelFamily[],
|
|
632
651
|
): { pair: ModelEffortPair; family?: AgyModelFamily } {
|
|
633
|
-
// Claude selections never resolve through the Agy catalog; they pass through untouched.
|
|
634
|
-
if (pair.backend === "claude") return { pair };
|
|
652
|
+
// Claude/Grok selections never resolve through the Agy catalog; they pass through untouched.
|
|
653
|
+
if (pair.backend === "claude" || pair.backend === "grok") return { pair };
|
|
635
654
|
const family = families.find((item) => item.id === pair.model || item.variants.some((variant) => variant.id === pair.model));
|
|
636
655
|
if (!family) return { pair };
|
|
637
656
|
const variant = family.variants.find((item) => item.id === pair.model);
|
|
@@ -661,19 +680,31 @@ export function resolveClaudeModel(input: string): string {
|
|
|
661
680
|
return alias ? alias.id : trimmed;
|
|
662
681
|
}
|
|
663
682
|
|
|
683
|
+
// Grok seeds resolve to themselves; any other non-empty string passes through
|
|
684
|
+
// untouched as an explicit user-entered model ID (installed `grok models` confirms).
|
|
685
|
+
export function resolveGrokModel(input: string): string {
|
|
686
|
+
const trimmed = input.trim();
|
|
687
|
+
if (!trimmed) throw new Error("Grok model must be a non-empty model ID (grok-4.7, grok-4.7-build-fast, or an explicit model ID).");
|
|
688
|
+
return trimmed;
|
|
689
|
+
}
|
|
690
|
+
|
|
664
691
|
// Routes one capability's whole pair to its backend execution selection. Agy keeps the
|
|
665
|
-
// existing model/effort split; Claude
|
|
692
|
+
// existing model/effort split; Claude/Grok carry the model plus an optional effort
|
|
666
693
|
// ("default" means the CLI's own default and is omitted). Throws for an effort the
|
|
667
694
|
// resolved backend does not support instead of silently sending a mismatched pair.
|
|
668
695
|
export function selectionForCapability(settings: BroSettings, capability: Capability): BackendSelection {
|
|
669
696
|
const pair = capabilityPair(settings, capability);
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
697
|
+
const backend = capabilityBackend(settings, capability);
|
|
698
|
+
if (backend === "claude" || backend === "grok") {
|
|
699
|
+
const valid = backend === "claude" ? isClaudeEffort(pair.effort) : isGrokEffort(pair.effort);
|
|
700
|
+
if (!valid) {
|
|
701
|
+
throw new Error(`\`${pair.effort}\` is not supported on the ${backend === "claude" ? "Claude" : "Grok"} backend. Run \`/bro config\` to fix this.`);
|
|
673
702
|
}
|
|
674
|
-
return
|
|
675
|
-
|
|
676
|
-
|
|
703
|
+
return (
|
|
704
|
+
pair.effort === "default"
|
|
705
|
+
? { backend, model: pair.model }
|
|
706
|
+
: { backend, model: pair.model, effort: pair.effort }
|
|
707
|
+
) as BackendSelection;
|
|
677
708
|
}
|
|
678
709
|
if (!isAgyEffort(pair.effort)) {
|
|
679
710
|
throw new Error(`\`${pair.effort}\` is not supported on the Agy backend. Run \`/bro config\` to fix this.`);
|
|
@@ -681,6 +712,12 @@ export function selectionForCapability(settings: BroSettings, capability: Capabi
|
|
|
681
712
|
return agySelection({ model: pair.model, effort: pair.effort });
|
|
682
713
|
}
|
|
683
714
|
|
|
715
|
+
// The modal label for the exact selection a request runs with: model plus effort, where an
|
|
716
|
+
// omitted effort (the model's own default) reads simply "default".
|
|
717
|
+
export function selectionLabel(selection: BackendSelection): string {
|
|
718
|
+
return `${selection.model} · ${selection.effort ?? "default"}`;
|
|
719
|
+
}
|
|
720
|
+
|
|
684
721
|
function resolveCapabilitySettings(
|
|
685
722
|
settings: BroSettings,
|
|
686
723
|
capability: Capability,
|
|
@@ -912,6 +949,27 @@ async function checkClaudeAuth(pi: ExtensionAPI, signal: AbortSignal): Promise<s
|
|
|
912
949
|
}
|
|
913
950
|
}
|
|
914
951
|
|
|
952
|
+
async function checkGrokVersion(pi: ExtensionAPI, signal: AbortSignal): Promise<string> {
|
|
953
|
+
const runDirectory = await mkdtemp(join(tmpdir(), "pi-bro-"));
|
|
954
|
+
try {
|
|
955
|
+
const result = await pi.exec("grok", ["--version"], { cwd: runDirectory, signal, timeout: 10_000 });
|
|
956
|
+
if (signal.aborted) throw new Error("Canceled.");
|
|
957
|
+
if (result.killed || result.code !== 0) {
|
|
958
|
+
const detail = result.stderr.trim() || result.stdout.trim();
|
|
959
|
+
throw new Error(
|
|
960
|
+
detail
|
|
961
|
+
? `Grok could not start: ${detail}\n\nRun \`/bro doctor\` for setup help.`
|
|
962
|
+
: "Grok could not start. Make sure Grok is installed and on PATH, then run `/bro doctor`.",
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
const version = result.stdout.trim() || result.stderr.trim();
|
|
966
|
+
if (!version) throw new Error("Grok returned no version information. Update Grok, then run `/bro doctor` again.");
|
|
967
|
+
return version;
|
|
968
|
+
} finally {
|
|
969
|
+
await rm(runDirectory, { recursive: true, force: true });
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
915
973
|
async function checkAgyUsage(pi: ExtensionAPI, signal: AbortSignal): Promise<string> {
|
|
916
974
|
const runDirectory = await mkdtemp(join(tmpdir(), "pi-bro-"));
|
|
917
975
|
try {
|
|
@@ -958,10 +1016,11 @@ async function doctorReport(pi: ExtensionAPI, ctx: ExtensionCommandContext, sign
|
|
|
958
1016
|
fail("Prompt", error);
|
|
959
1017
|
}
|
|
960
1018
|
|
|
961
|
-
// Probe only the backends some feature actually selects: a Claude-only setup never
|
|
1019
|
+
// Probe only the backends some feature actually selects: a Claude/Grok-only setup never
|
|
962
1020
|
// requires Agy to be installed, and vice versa.
|
|
963
1021
|
const agyInUse = !settings || capabilityBackend(settings, "explain") === "agy" || capabilityBackend(settings, "show") === "agy" || capabilityBackend(settings, "btw") === "agy" || capabilityBackend(settings, "advisor") === "agy" || (settings.backend ?? "agy") === "agy";
|
|
964
1022
|
const claudeInUse = !!settings && (capabilityBackend(settings, "explain") === "claude" || capabilityBackend(settings, "show") === "claude" || capabilityBackend(settings, "btw") === "claude" || capabilityBackend(settings, "advisor") === "claude" || settings.backend === "claude");
|
|
1023
|
+
const grokInUse = !!settings && (capabilityBackend(settings, "explain") === "grok" || capabilityBackend(settings, "show") === "grok" || capabilityBackend(settings, "btw") === "grok" || capabilityBackend(settings, "advisor") === "grok" || settings.backend === "grok");
|
|
965
1024
|
|
|
966
1025
|
let agyStarted = false;
|
|
967
1026
|
if (agyInUse) {
|
|
@@ -1014,6 +1073,20 @@ async function doctorReport(pi: ExtensionAPI, ctx: ExtensionCommandContext, sign
|
|
|
1014
1073
|
pass("Claude", "not probed — no feature selects the Claude backend");
|
|
1015
1074
|
}
|
|
1016
1075
|
|
|
1076
|
+
// Version only, never a model request. No Grok auth probe exists, so a passing
|
|
1077
|
+
// version says the CLI starts — it says nothing about auth or connectivity.
|
|
1078
|
+
if (grokInUse) {
|
|
1079
|
+
try {
|
|
1080
|
+
pass("Grok", await checkGrokVersion(pi, signal));
|
|
1081
|
+
} catch (error) {
|
|
1082
|
+
if (signal.aborted) throw error;
|
|
1083
|
+
fail("Grok", error);
|
|
1084
|
+
}
|
|
1085
|
+
pass("Grok auth", "unverified — no auth probe exists (version does not imply auth or connectivity)");
|
|
1086
|
+
} else {
|
|
1087
|
+
pass("Grok", "not probed — no feature selects the Grok backend");
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1017
1090
|
if (settings) {
|
|
1018
1091
|
const defaultBackend = settings.backend ?? "agy";
|
|
1019
1092
|
if (defaultBackend === "claude") {
|
|
@@ -1025,6 +1098,15 @@ async function doctorReport(pi: ExtensionAPI, ctx: ExtensionCommandContext, sign
|
|
|
1025
1098
|
} else {
|
|
1026
1099
|
pass("Reasoning effort", settings.effort);
|
|
1027
1100
|
}
|
|
1101
|
+
} else if (defaultBackend === "grok") {
|
|
1102
|
+
pass("Selected model", `grok \`${settings.model}\``);
|
|
1103
|
+
if (!isGrokEffort(settings.effort)) {
|
|
1104
|
+
fail("Reasoning effort", `\`${settings.effort}\` is unsupported. Run \`/bro effort\` to choose another.`);
|
|
1105
|
+
} else if (settings.effort === "default") {
|
|
1106
|
+
pass("Reasoning effort", "built into the selected model");
|
|
1107
|
+
} else {
|
|
1108
|
+
pass("Reasoning effort", settings.effort);
|
|
1109
|
+
}
|
|
1028
1110
|
} else if (models) {
|
|
1029
1111
|
const current = resolveCatalogSettings(settings, models);
|
|
1030
1112
|
if (!current.family) {
|
|
@@ -1064,6 +1146,23 @@ async function doctorReport(pi: ExtensionAPI, ctx: ExtensionCommandContext, sign
|
|
|
1064
1146
|
);
|
|
1065
1147
|
continue;
|
|
1066
1148
|
}
|
|
1149
|
+
if (backend === "grok") {
|
|
1150
|
+
if (!backendSupports("grok", capability)) {
|
|
1151
|
+
fail(label, `\`${capability}\` is not supported on the Grok backend for this capability. Run \`/bro config\` to give it another backend.`);
|
|
1152
|
+
continue;
|
|
1153
|
+
}
|
|
1154
|
+
if (!isGrokEffort(pair.effort)) {
|
|
1155
|
+
fail(label, `\`${pair.effort}\` is unsupported for grok \`${pair.model}\`. Run \`/bro config\` to fix this.`);
|
|
1156
|
+
continue;
|
|
1157
|
+
}
|
|
1158
|
+
pass(
|
|
1159
|
+
label,
|
|
1160
|
+
override
|
|
1161
|
+
? `override grok \`${pair.model}\`${pair.effort === "default" ? "" : ` (${pair.effort})`}`
|
|
1162
|
+
: `grok \`${pair.model}\`${pair.effort === "default" ? "" : ` (${pair.effort})`} · using the shared default`,
|
|
1163
|
+
);
|
|
1164
|
+
continue;
|
|
1165
|
+
}
|
|
1067
1166
|
if (!models) continue;
|
|
1068
1167
|
const resolved = resolveCapabilitySettings(settings, capability, models);
|
|
1069
1168
|
if (!resolved.family) {
|
|
@@ -1096,13 +1195,15 @@ async function doctorReport(pi: ExtensionAPI, ctx: ExtensionCommandContext, sign
|
|
|
1096
1195
|
pass("Advisor steering", resolveAdvisorState(ctx.sessionManager.getBranch()).steering.trim() ? "present" : "none");
|
|
1097
1196
|
if (settings && capabilityBackend(settings, "advisor") === "claude") {
|
|
1098
1197
|
pass("Advisor compatibility", "Claude backend — no Agy version floor applies");
|
|
1198
|
+
} else if (settings && capabilityBackend(settings, "advisor") === "grok") {
|
|
1199
|
+
pass("Advisor compatibility", "Grok backend — no Agy version floor applies; auth/connectivity unverified (no probe)");
|
|
1099
1200
|
} else if (agyVersion && advisorAgyCompatible(agyVersion)) pass("Advisor compatibility", ADVISOR_COMPATIBILITY);
|
|
1100
1201
|
else if (agyVersion) fail("Advisor compatibility", `installed \`${agyVersion}\`; requires Agy >=1.1.15. Run \`agy update\`.`);
|
|
1101
1202
|
|
|
1102
1203
|
return `# Bro doctor\n\n${lines.join("\n")}\n\n**${failed ? "Bro needs attention." : "Bro is ready."}**\n\n${
|
|
1103
1204
|
failed
|
|
1104
1205
|
? "Fix the failed items, then press **R** to check again."
|
|
1105
|
-
: "No assistant response was sent and no model turn was run.
|
|
1206
|
+
: "No assistant response was sent and no model turn was run. Version/auth checks do not imply connectivity (Grok auth is unverified — no probe exists)."
|
|
1106
1207
|
}`;
|
|
1107
1208
|
}
|
|
1108
1209
|
|
|
@@ -1295,8 +1396,9 @@ async function simplify(
|
|
|
1295
1396
|
signal: AbortSignal,
|
|
1296
1397
|
settings: BroSettings,
|
|
1297
1398
|
onProgress?: (text: string) => void,
|
|
1298
|
-
): Promise<string> {
|
|
1299
|
-
|
|
1399
|
+
): Promise<{ text: string; model: string }> {
|
|
1400
|
+
const selection = selectionForCapability(settings, "explain");
|
|
1401
|
+
return { text: await runAgyText((await promptFor(response, settings.mode)).text, selection, signal, onProgress), model: selectionLabel(selection) };
|
|
1300
1402
|
}
|
|
1301
1403
|
|
|
1302
1404
|
async function runShowExplanation(
|
|
@@ -1305,8 +1407,9 @@ async function runShowExplanation(
|
|
|
1305
1407
|
signal: AbortSignal,
|
|
1306
1408
|
settings: BroSettings,
|
|
1307
1409
|
onProgress?: (text: string) => void,
|
|
1308
|
-
): Promise<string> {
|
|
1309
|
-
|
|
1410
|
+
): Promise<{ text: string; model: string }> {
|
|
1411
|
+
const selection = selectionForCapability(settings, "show");
|
|
1412
|
+
return { text: await runAgyText(buildShowPrompt(transcript, steering), selection, signal, onProgress, "show"), model: selectionLabel(selection) };
|
|
1310
1413
|
}
|
|
1311
1414
|
|
|
1312
1415
|
// Thin presentation-boundary wrapper around the shared backend: coalesces raw text progress to the
|
|
@@ -1688,7 +1791,7 @@ export function advisorAttemptLabel(details: AdvisorToolDetails): string {
|
|
|
1688
1791
|
return `Bro advisor · ${details.model ?? "unknown model"} · ${details.attempt} attempt${details.attempt === 1 ? "" : "s"} · ${Math.ceil((details.durationMs ?? details.elapsedMs) / 1_000)}s`;
|
|
1689
1792
|
}
|
|
1690
1793
|
const latest = details.activity?.at(-1);
|
|
1691
|
-
const backendLabel = details.backend === "claude" ? "Claude" : "Agy";
|
|
1794
|
+
const backendLabel = details.backend === "claude" ? "Claude" : details.backend === "grok" ? "Grok" : "Agy";
|
|
1692
1795
|
// "last reported" + freshness, never a claim about what the backend is doing right now and never
|
|
1693
1796
|
// "stalled" -- silence since lastActivityAt is not itself evidence of a stuck run.
|
|
1694
1797
|
const activityLabel = latest
|
|
@@ -1708,9 +1811,10 @@ function showTurnsValues(current: number): string[] {
|
|
|
1708
1811
|
// falsely healthy for an unavailable model), and flags a stored effort that isn't one of the
|
|
1709
1812
|
// resolved family's supported efforts instead of silently showing it as if it were valid.
|
|
1710
1813
|
function effortDisplay(resolved: { pair: ModelEffortPair; family?: AgyModelFamily }, backend: BackendName): string {
|
|
1711
|
-
// Claude selections never touch the Agy catalog: the stored effort is valid exactly
|
|
1712
|
-
// when it is one of
|
|
1814
|
+
// Claude/Grok selections never touch the Agy catalog: the stored effort is valid exactly
|
|
1815
|
+
// when it is one of that backend's levels (or "default" for the CLI's own default).
|
|
1713
1816
|
if (backend === "claude") return isClaudeEffort(resolved.pair.effort) ? resolved.pair.effort : `${resolved.pair.effort} (unsupported)`;
|
|
1817
|
+
if (backend === "grok") return isGrokEffort(resolved.pair.effort) ? resolved.pair.effort : `${resolved.pair.effort} (unsupported)`;
|
|
1714
1818
|
if (!resolved.family) return "unavailable";
|
|
1715
1819
|
const fixed = !resolved.family.efforts.length;
|
|
1716
1820
|
const valid = fixed ? resolved.pair.effort === "default" : resolved.family.efforts.includes(resolved.pair.effort as AgyEffort);
|
|
@@ -1718,7 +1822,7 @@ function effortDisplay(resolved: { pair: ModelEffortPair; family?: AgyModelFamil
|
|
|
1718
1822
|
return fixed ? "fixed" : resolved.pair.effort;
|
|
1719
1823
|
}
|
|
1720
1824
|
|
|
1721
|
-
// Model-picker values that switch
|
|
1825
|
+
// Model-picker values that switch backend carry a "claude:"/"grok:" prefix so one
|
|
1722
1826
|
// atomic picker commit changes backend+model together -- cancelling the picker (Esc)
|
|
1723
1827
|
// leaves both unchanged via the existing submenu-cancel path.
|
|
1724
1828
|
const CLAUDE_OPTION_PREFIX = "claude:";
|
|
@@ -1727,6 +1831,19 @@ function parseClaudeOption(value: string): string | undefined {
|
|
|
1727
1831
|
? value.slice(CLAUDE_OPTION_PREFIX.length)
|
|
1728
1832
|
: undefined;
|
|
1729
1833
|
}
|
|
1834
|
+
const GROK_OPTION_PREFIX = "grok:";
|
|
1835
|
+
function parseGrokOption(value: string): string | undefined {
|
|
1836
|
+
return value.startsWith(GROK_OPTION_PREFIX) && value.length > GROK_OPTION_PREFIX.length
|
|
1837
|
+
? value.slice(GROK_OPTION_PREFIX.length)
|
|
1838
|
+
: undefined;
|
|
1839
|
+
}
|
|
1840
|
+
function parseBackendOption(value: string): { backend: "claude" | "grok"; id: string } | undefined {
|
|
1841
|
+
const claudeId = parseClaudeOption(value);
|
|
1842
|
+
if (claudeId !== undefined) return { backend: "claude", id: claudeId };
|
|
1843
|
+
const grokId = parseGrokOption(value);
|
|
1844
|
+
if (grokId !== undefined) return { backend: "grok", id: grokId };
|
|
1845
|
+
return undefined;
|
|
1846
|
+
}
|
|
1730
1847
|
|
|
1731
1848
|
// Testable core: takes settings/catalog/persist as plain arguments so smoke tests can drive
|
|
1732
1849
|
// the exact interaction (submenus, cancel, cycling, save failure) without a real Agy process
|
|
@@ -1764,30 +1881,38 @@ export function createConfigModal(
|
|
|
1764
1881
|
value: family.id,
|
|
1765
1882
|
label: `${family.label}${family.efforts.length ? "" : " · fixed effort"}`,
|
|
1766
1883
|
})),
|
|
1767
|
-
// Claude entries stay last so existing Agy keyboard navigation is unaffected.
|
|
1884
|
+
// Claude/Grok entries stay last so existing Agy keyboard navigation is unaffected.
|
|
1768
1885
|
...CLAUDE_MODELS.map((model) => ({
|
|
1769
1886
|
value: `${CLAUDE_OPTION_PREFIX}${model.id}`,
|
|
1770
1887
|
label: `${model.label} · claude`,
|
|
1771
1888
|
})),
|
|
1889
|
+
...GROK_MODELS.map((model) => ({
|
|
1890
|
+
value: `${GROK_OPTION_PREFIX}${model.id}`,
|
|
1891
|
+
label: `${model.label} · grok`,
|
|
1892
|
+
})),
|
|
1772
1893
|
];
|
|
1773
1894
|
options.push({ value: "__claude_custom__", label: "Claude · custom model ID…" });
|
|
1895
|
+
options.push({ value: "__grok_custom__", label: "Grok · custom model ID…" });
|
|
1774
1896
|
const input = new Input();
|
|
1775
|
-
let
|
|
1776
|
-
input.onSubmit = (value) => {
|
|
1897
|
+
let enteringBackend: "claude" | "grok" | undefined;
|
|
1898
|
+
input.onSubmit = (value) => {
|
|
1899
|
+
if (value.trim() && enteringBackend) pickerDone(`${enteringBackend === "claude" ? CLAUDE_OPTION_PREFIX : GROK_OPTION_PREFIX}${value.trim()}`);
|
|
1900
|
+
};
|
|
1777
1901
|
const picker = new SelectList(options, Math.min(options.length, 8), getSelectListTheme());
|
|
1778
1902
|
const selectedIndex = capability && current === "Default" ? 0 : options.findIndex((option) => option.value === current);
|
|
1779
1903
|
picker.setSelectedIndex(Math.max(0, selectedIndex));
|
|
1780
1904
|
picker.onSelect = (item) => {
|
|
1781
|
-
if (item.value === "__claude_custom__") {
|
|
1905
|
+
if (item.value === "__claude_custom__") { enteringBackend = "claude"; tui.requestRender(); }
|
|
1906
|
+
else if (item.value === "__grok_custom__") { enteringBackend = "grok"; tui.requestRender(); }
|
|
1782
1907
|
else pickerDone(item.value);
|
|
1783
1908
|
};
|
|
1784
1909
|
picker.onCancel = () => pickerDone();
|
|
1785
1910
|
return {
|
|
1786
|
-
render: (width: number) =>
|
|
1911
|
+
render: (width: number) => enteringBackend ? [`${enteringBackend === "claude" ? "Claude" : "Grok"} model ID (Enter saves, Esc cancels)`, ...input.render(width)] : picker.render(width),
|
|
1787
1912
|
invalidate: () => { picker.invalidate(); input.invalidate(); },
|
|
1788
1913
|
handleInput: (data: string) => {
|
|
1789
|
-
if (
|
|
1790
|
-
else if (
|
|
1914
|
+
if (enteringBackend && matchesKey(data, "escape")) pickerDone();
|
|
1915
|
+
else if (enteringBackend) input.handleInput(data);
|
|
1791
1916
|
else picker.handleInput(data);
|
|
1792
1917
|
},
|
|
1793
1918
|
};
|
|
@@ -1823,10 +1948,10 @@ export function createConfigModal(
|
|
|
1823
1948
|
|
|
1824
1949
|
function refresh(): void {
|
|
1825
1950
|
const defaultBackend = settings.backend ?? "agy";
|
|
1826
|
-
const def = resolveModelEffort({ model: settings.model, effort: settings.effort }, families);
|
|
1827
|
-
modelItem.currentValue = defaultBackend === "claude" ? `${CLAUDE_OPTION_PREFIX}${settings.model}` : (def.family?.id ?? settings.model);
|
|
1951
|
+
const def = resolveModelEffort({ backend: settings.backend, model: settings.model, effort: settings.effort }, families);
|
|
1952
|
+
modelItem.currentValue = defaultBackend === "claude" ? `${CLAUDE_OPTION_PREFIX}${settings.model}` : defaultBackend === "grok" ? `${GROK_OPTION_PREFIX}${settings.model}` : (def.family?.id ?? settings.model);
|
|
1828
1953
|
effortItem.currentValue = effortDisplay(def, defaultBackend);
|
|
1829
|
-
effortItem.values = defaultBackend === "claude" ? ["default", ...CLAUDE_EFFORTS] : def.family?.efforts.length ? [...def.family.efforts] : undefined;
|
|
1954
|
+
effortItem.values = defaultBackend === "claude" ? ["default", ...CLAUDE_EFFORTS] : defaultBackend === "grok" ? ["default", ...GROK_EFFORTS] : def.family?.efforts.length ? [...def.family.efforts] : undefined;
|
|
1830
1955
|
modeItem.currentValue = settings.mode;
|
|
1831
1956
|
showTurnsItem.currentValue = String(settings.showTurns);
|
|
1832
1957
|
showTurnsItem.values = showTurnsValues(settings.showTurns);
|
|
@@ -1840,9 +1965,11 @@ export function createConfigModal(
|
|
|
1840
1965
|
? "Default"
|
|
1841
1966
|
: backend === "claude"
|
|
1842
1967
|
? `${CLAUDE_OPTION_PREFIX}${override.model}`
|
|
1843
|
-
:
|
|
1968
|
+
: backend === "grok"
|
|
1969
|
+
? `${GROK_OPTION_PREFIX}${override.model}`
|
|
1970
|
+
: (resolved.family?.id ?? override.model);
|
|
1844
1971
|
rows.effort.currentValue = backendSupports(backend, capability) ? effortDisplay(resolved, backend) : "unsupported backend";
|
|
1845
|
-
rows.effort.values = backend === "claude" ? ["default", ...CLAUDE_EFFORTS] : resolved.family?.efforts.length ? [...resolved.family.efforts] : undefined;
|
|
1972
|
+
rows.effort.values = backend === "claude" ? ["default", ...CLAUDE_EFFORTS] : backend === "grok" ? ["default", ...GROK_EFFORTS] : resolved.family?.efforts.length ? [...resolved.family.efforts] : undefined;
|
|
1846
1973
|
}
|
|
1847
1974
|
}
|
|
1848
1975
|
refresh();
|
|
@@ -1897,14 +2024,19 @@ export function createConfigModal(
|
|
|
1897
2024
|
|
|
1898
2025
|
const onChange = (id: string, newValue: string) => {
|
|
1899
2026
|
if (id === "model") {
|
|
1900
|
-
const
|
|
1901
|
-
if (
|
|
1902
|
-
|
|
1903
|
-
|
|
2027
|
+
const switched = parseBackendOption(newValue);
|
|
2028
|
+
if (switched !== undefined) {
|
|
2029
|
+
if (switched.backend === "claude") {
|
|
2030
|
+
const effort = settings.backend === "claude" && isClaudeEffort(settings.effort) ? settings.effort : "default";
|
|
2031
|
+
settings = { ...settings, backend: "claude", model: resolveClaudeModel(switched.id), effort };
|
|
2032
|
+
} else {
|
|
2033
|
+
const effort = settings.backend === "grok" && isGrokEffort(settings.effort) ? settings.effort : "default";
|
|
2034
|
+
settings = { ...settings, backend: "grok", model: resolveGrokModel(switched.id), effort };
|
|
2035
|
+
}
|
|
1904
2036
|
} else {
|
|
1905
2037
|
const family = findFamily(newValue);
|
|
1906
2038
|
if (!family) return;
|
|
1907
|
-
const keepCurrent = settings.backend
|
|
2039
|
+
const keepCurrent = (settings.backend ?? "agy") === "agy" && (settings.effort === "default" ? !family.efforts.length : family.efforts.includes(settings.effort as AgyEffort));
|
|
1908
2040
|
// Backend-less internal pairs mean Agy; disk saves still tag them explicitly.
|
|
1909
2041
|
const { backend: _dropped, ...rest } = settings;
|
|
1910
2042
|
settings = { ...rest, model: family.id, effort: keepCurrent ? settings.effort : preferredEffort(family) };
|
|
@@ -1913,6 +2045,9 @@ export function createConfigModal(
|
|
|
1913
2045
|
if ((settings.backend ?? "agy") === "claude") {
|
|
1914
2046
|
if (!isClaudeEffort(newValue)) return;
|
|
1915
2047
|
settings = { ...settings, effort: newValue };
|
|
2048
|
+
} else if ((settings.backend ?? "agy") === "grok") {
|
|
2049
|
+
if (!isGrokEffort(newValue)) return;
|
|
2050
|
+
settings = { ...settings, effort: newValue };
|
|
1916
2051
|
} else {
|
|
1917
2052
|
// Effort-only edit: pin the resolved family's canonical id, same reasoning as the
|
|
1918
2053
|
// capability-override effort-only edit below -- otherwise a shared default created
|
|
@@ -1936,14 +2071,22 @@ export function createConfigModal(
|
|
|
1936
2071
|
if (newValue === "__default__") {
|
|
1937
2072
|
settings = withCapabilityOverride(settings, capability, undefined);
|
|
1938
2073
|
} else {
|
|
1939
|
-
const
|
|
1940
|
-
if (
|
|
2074
|
+
const switched = parseBackendOption(newValue);
|
|
2075
|
+
if (switched !== undefined) {
|
|
1941
2076
|
const currentEffort = capabilityPair(settings, capability).effort;
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
2077
|
+
if (switched.backend === "claude") {
|
|
2078
|
+
settings = withCapabilityOverride(settings, capability, {
|
|
2079
|
+
backend: "claude",
|
|
2080
|
+
model: resolveClaudeModel(switched.id),
|
|
2081
|
+
effort: capabilityBackend(settings, capability) === "claude" && isClaudeEffort(currentEffort) ? currentEffort : "default",
|
|
2082
|
+
});
|
|
2083
|
+
} else {
|
|
2084
|
+
settings = withCapabilityOverride(settings, capability, {
|
|
2085
|
+
backend: "grok",
|
|
2086
|
+
model: resolveGrokModel(switched.id),
|
|
2087
|
+
effort: capabilityBackend(settings, capability) === "grok" && isGrokEffort(currentEffort) ? currentEffort : "default",
|
|
2088
|
+
});
|
|
2089
|
+
}
|
|
1947
2090
|
} else {
|
|
1948
2091
|
const family = findFamily(newValue);
|
|
1949
2092
|
if (!family) return;
|
|
@@ -1960,6 +2103,11 @@ export function createConfigModal(
|
|
|
1960
2103
|
const existing = capabilityOverride(settings, capability);
|
|
1961
2104
|
const model = existing?.model ?? settings.model;
|
|
1962
2105
|
settings = withCapabilityOverride(settings, capability, { backend: "claude", model, effort: newValue });
|
|
2106
|
+
} else if (capabilityBackend(settings, capability) === "grok") {
|
|
2107
|
+
if (!isGrokEffort(newValue)) return;
|
|
2108
|
+
const existing = capabilityOverride(settings, capability);
|
|
2109
|
+
const model = existing?.model ?? settings.model;
|
|
2110
|
+
settings = withCapabilityOverride(settings, capability, { backend: "grok", model, effort: newValue });
|
|
1963
2111
|
} else {
|
|
1964
2112
|
// Effort-only edit: pin the resolved family's canonical id, never whatever raw
|
|
1965
2113
|
// string happens to sit in settings.model/override.model (which — for a shared
|
|
@@ -2023,14 +2171,13 @@ export async function showBroConfigModal(ctx: ExtensionCommandContext, pi: Exten
|
|
|
2023
2171
|
ctx.ui.notify(withDoctor(error), "error");
|
|
2024
2172
|
return;
|
|
2025
2173
|
}
|
|
2026
|
-
//
|
|
2027
|
-
// Agy catalog; anything still on Agy keeps the previous hard requirement.
|
|
2174
|
+
// Keep configuration repair accessible even when the Agy catalog is unavailable.
|
|
2028
2175
|
let families: AgyModelFamily[];
|
|
2029
2176
|
try {
|
|
2030
2177
|
families = await listAgyModels(pi);
|
|
2031
2178
|
} catch (error) {
|
|
2032
2179
|
families = [];
|
|
2033
|
-
ctx.ui.notify("Agy model catalog unavailable — showing Claude settings without Agy choices.", "warning");
|
|
2180
|
+
ctx.ui.notify("Agy model catalog unavailable — showing Claude/Grok settings without Agy choices.", "warning");
|
|
2034
2181
|
}
|
|
2035
2182
|
await ctx.ui.custom<void>(createConfigModal(settings, families, writeSettings), {
|
|
2036
2183
|
overlay: true,
|
|
@@ -2161,10 +2308,11 @@ export function helpText(settings?: BroSettings, settingsError?: string): string
|
|
|
2161
2308
|
? `- **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")}` : ""}`
|
|
2162
2309
|
: `Bro could not read its settings: ${settingsError}\n\nRun \`/bro doctor\` for setup help.`;
|
|
2163
2310
|
const advisorBackend = settings ? capabilityBackend(settings, "advisor") : "agy";
|
|
2164
|
-
const advisorProcess = advisorBackend === "claude" ? "Claude process" : "Agy process";
|
|
2311
|
+
const advisorProcess = advisorBackend === "claude" ? "Claude process" : advisorBackend === "grok" ? "Grok process" : "Agy process";
|
|
2312
|
+
const advisorName = advisorBackend === "claude" ? "Claude" : advisorBackend === "grok" ? "Grok" : "Agy";
|
|
2165
2313
|
return `# Bro
|
|
2166
2314
|
|
|
2167
|
-
Bro explains a dense assistant reply, pasted text, local document, or public webpage in plain language, draws recent session turns as shapes, or opens a
|
|
2315
|
+
Bro explains a dense assistant reply, pasted text, local document, or public webpage in plain language, draws recent session turns as shapes, or opens a separate side conversation with \`/bro btw\` — without adding anything to Pi's conversation.
|
|
2168
2316
|
|
|
2169
2317
|
## Explain
|
|
2170
2318
|
|
|
@@ -2182,24 +2330,23 @@ Press **R** to simplify the captured source again. Run a new \`/bro text\`, \`/b
|
|
|
2182
2330
|
## Check and configure
|
|
2183
2331
|
|
|
2184
2332
|
- \`/bro doctor\` — check settings, backends, account, model, effort, and mode (per-feature backend/model/effort)
|
|
2185
|
-
- \`/bro
|
|
2186
|
-
- \`/bro
|
|
2187
|
-
- \`/bro effort [low|medium|high|xhigh|max]\` — view or choose the shared default reasoning effort (xhigh/max are Claude-only)
|
|
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)
|
|
2188
2335
|
- \`/bro mode [brief|balanced|faithful]\` — view or choose explanation mode
|
|
2189
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.
|
|
2190
2337
|
|
|
2191
|
-
\`/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\`
|
|
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).
|
|
2192
2339
|
|
|
2193
2340
|
## Side conversation
|
|
2194
2341
|
|
|
2195
|
-
- \`/bro btw [--fresh] [--full] [question]\` — open a side conversation.
|
|
2342
|
+
- \`/bro btw [--fresh] [--full] [question]\` — open a side conversation. Conversation-only intent by default (Agy sandbox controls; Grok prompt request, not enforced); add \`--full\` to let it read and edit the workspace, and \`--fresh\` to start without main-session context. Reopening preserves the thread's access mode (even with \`--fresh\`); use \`--sandbox\` to return to sandbox mode. Changing access mode starts a new thread. Inside the side thread, type questions and press Enter (empty Enter re-asks); exact commands \`/copy\` and \`/copy-all\` copy the latest answer or full thread to the system clipboard; exact commands \`/insert\` and \`/insert-all\` insert into the main editor without submitting (use \`/insert!\` or \`/insert-all!\` to replace an existing draft); \`/retry\` re-asks the last question; \`/clear\` resets the thread. Any other input is sent as a question. Esc closes.
|
|
2196
2343
|
|
|
2197
2344
|
## Advisor
|
|
2198
2345
|
|
|
2199
2346
|
- \`bro_advisor\` — a tool the executor agent can voluntarily call mid-task for a second opinion from a fresh ${advisorProcess} before or after a non-trivial decision. It is registered like any other tool and has no on/off switch of its own; whether the executor can actually call it depends entirely on this host's own tool restrictions
|
|
2200
2347
|
- \`/bro advisor\` — a quick notice of whether \`bro_advisor\` is available right now, pointing at \`/bro config\`, \`/bro advisor-steer\`, and \`/bro doctor\`
|
|
2201
2348
|
- \`/bro advisor-steer\` — open an editor for one persistent steering brief the advisor always sees. **Ctrl+S** saves, **Enter**/**Shift+Enter** insert newlines, **Ctrl+K** clears the saved brief and draft, **Ctrl+C** copies the full draft, and **Esc** closes without saving unsaved edits
|
|
2202
|
-
- \`/bro doctor\` — the full advisor diagnostic: whether this host exposes and activates \`bro_advisor\`, its resolved model/effort, steering presence, and
|
|
2349
|
+
- \`/bro doctor\` — the full advisor diagnostic: whether this host exposes and activates \`bro_advisor\`, its resolved model/effort, steering presence, and backend compatibility
|
|
2203
2350
|
|
|
2204
2351
|
Each consultation is a fresh, standalone ${advisorProcess} — never resumed, never looping, never automatically triggered. Bro captures the context snapshot (system instructions, active tools, and the conversation so far including tool calls and results) automatically; the executor never has to assemble one. The advisor has real tool access in the workspace, running with permissions auto-approved, so it can verify claims itself; it is instructed to only return advice and leave edits to the executor, but that instruction is behavioral rather than an enforced sandbox constraint. The steering brief persists in the session (not sent to the model) and is restored on resume or reload; forking a session inherits it, and edits after the fork are independent of the original branch.
|
|
2205
2352
|
|
|
@@ -2232,10 +2379,10 @@ Bro temporarily captures mouse input while the modal is open. Native mouse selec
|
|
|
2232
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.
|
|
2233
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.
|
|
2234
2381
|
- If a webpage fails, copy it into a text file or save it as a PDF, then use \`/bro file\`.
|
|
2235
|
-
- 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
|
|
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.
|
|
2236
2383
|
- Show reflects what was reported in the conversation, not independent verification against the actual code or system state.
|
|
2237
|
-
- 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
|
|
2238
|
-
- Advisor consultations run with real tool access and auto-approved permissions (\`--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 ${
|
|
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.
|
|
2239
2386
|
|
|
2240
2387
|
## Privacy and safety
|
|
2241
2388
|
|
|
@@ -2243,10 +2390,10 @@ Bro sends the selected assistant reply, pasted text, locally extracted document
|
|
|
2243
2390
|
|
|
2244
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.
|
|
2245
2392
|
|
|
2246
|
-
Bro
|
|
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.
|
|
2247
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.
|
|
2248
2395
|
|
|
2249
|
-
|
|
2396
|
+
Doctor checks contact only the selected backends, but never send source text or run a model turn. Pressing **C** sends the explanation to your system clipboard.
|
|
2250
2397
|
|
|
2251
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.
|
|
2252
2399
|
|
|
@@ -2264,6 +2411,7 @@ class BroModal implements Focusable {
|
|
|
2264
2411
|
private kind: ModalKind = "loading";
|
|
2265
2412
|
private rawText = "";
|
|
2266
2413
|
private sourceLabel = "";
|
|
2414
|
+
private modelLabel = "";
|
|
2267
2415
|
private notice = "";
|
|
2268
2416
|
private offset = 0;
|
|
2269
2417
|
private maxOffset = 0;
|
|
@@ -2292,8 +2440,8 @@ class BroModal implements Focusable {
|
|
|
2292
2440
|
this.setContent("streaming", text, "", false, false);
|
|
2293
2441
|
}
|
|
2294
2442
|
|
|
2295
|
-
setResult(text: string, retryable: boolean, notice = "", sourceLabel = "", rawText = text): void {
|
|
2296
|
-
this.setContent("result", text, rawText, true, retryable, notice, sourceLabel);
|
|
2443
|
+
setResult(text: string, retryable: boolean, notice = "", sourceLabel = "", rawText = text, modelLabel = ""): void {
|
|
2444
|
+
this.setContent("result", text, rawText, true, retryable, notice, sourceLabel, modelLabel);
|
|
2297
2445
|
}
|
|
2298
2446
|
|
|
2299
2447
|
setHtmlPath(path: string): void {
|
|
@@ -2317,6 +2465,7 @@ class BroModal implements Focusable {
|
|
|
2317
2465
|
retryable: boolean,
|
|
2318
2466
|
notice = "",
|
|
2319
2467
|
sourceLabel = "",
|
|
2468
|
+
modelLabel = "",
|
|
2320
2469
|
): void {
|
|
2321
2470
|
this.kind = kind;
|
|
2322
2471
|
if (kind !== "result") this.htmlPath = "";
|
|
@@ -2328,6 +2477,7 @@ class BroModal implements Focusable {
|
|
|
2328
2477
|
.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ")
|
|
2329
2478
|
.replace(/\s+/g, " ")
|
|
2330
2479
|
.trim();
|
|
2480
|
+
this.modelLabel = modelLabel;
|
|
2331
2481
|
if (kind !== "streaming") this.offset = 0;
|
|
2332
2482
|
this.markdown.setText(text);
|
|
2333
2483
|
this.tui.requestRender();
|
|
@@ -2377,7 +2527,11 @@ class BroModal implements Focusable {
|
|
|
2377
2527
|
|
|
2378
2528
|
const lines = [
|
|
2379
2529
|
this.borderLine(innerWidth, "top"),
|
|
2380
|
-
this.frameLine(
|
|
2530
|
+
this.frameLine(
|
|
2531
|
+
this.theme.fg("accent", this.theme.bold(`Bro${this.sourceLabel ? ` · ${this.sourceLabel}` : ""}`)) +
|
|
2532
|
+
this.theme.fg("dim", `${this.modelLabel ? ` · ${this.modelLabel}` : ""}${scroll}`),
|
|
2533
|
+
innerWidth,
|
|
2534
|
+
),
|
|
2381
2535
|
this.ruleLine(innerWidth),
|
|
2382
2536
|
];
|
|
2383
2537
|
|
|
@@ -2518,7 +2672,7 @@ async function showBroModal(ctx: ExtensionCommandContext, options: BroModalOptio
|
|
|
2518
2672
|
current = result;
|
|
2519
2673
|
options.onResult?.(result);
|
|
2520
2674
|
const display = result.htmlPath ? stripShowHtmlFence(result.text) : result.text;
|
|
2521
|
-
modal.setResult(display, options.retryable ?? true, "", result.source?.label, result.text);
|
|
2675
|
+
modal.setResult(display, options.retryable ?? true, "", result.source?.label, result.text, result.model);
|
|
2522
2676
|
if (result.htmlPath) modal.setHtmlPath(result.htmlPath);
|
|
2523
2677
|
})
|
|
2524
2678
|
.catch((error) => {
|
|
@@ -2526,7 +2680,7 @@ async function showBroModal(ctx: ExtensionCommandContext, options: BroModalOptio
|
|
|
2526
2680
|
const message = error instanceof Error ? error.message : String(error);
|
|
2527
2681
|
if (previous) {
|
|
2528
2682
|
current = previous;
|
|
2529
|
-
modal.setResult(previous.text, options.retryable ?? true, `Retry failed: ${message}`, previous.source?.label);
|
|
2683
|
+
modal.setResult(previous.text, options.retryable ?? true, `Retry failed: ${message}`, previous.source?.label, previous.text, previous.model);
|
|
2530
2684
|
} else {
|
|
2531
2685
|
modal.setError(message);
|
|
2532
2686
|
}
|
|
@@ -2539,7 +2693,7 @@ async function showBroModal(ctx: ExtensionCommandContext, options: BroModalOptio
|
|
|
2539
2693
|
if (options.text !== undefined) {
|
|
2540
2694
|
modal.setStatic(options.kind ?? "help", options.text, options.copyable ?? false);
|
|
2541
2695
|
} else if (current) {
|
|
2542
|
-
modal.setResult(current.text, options.retryable ?? Boolean(options.run), "", current.source?.label);
|
|
2696
|
+
modal.setResult(current.text, options.retryable ?? Boolean(options.run), "", current.source?.label, current.text, current.model);
|
|
2543
2697
|
} else {
|
|
2544
2698
|
execute();
|
|
2545
2699
|
}
|
|
@@ -2581,6 +2735,13 @@ export function resolveBtwThread(existing: BtwThread | undefined, parsed: { fres
|
|
|
2581
2735
|
return !existing || startFresh ? { turns: [], full: targetFull } : existing;
|
|
2582
2736
|
}
|
|
2583
2737
|
|
|
2738
|
+
export function bindBtwBackend(thread: BtwThread, backend: BackendName): boolean {
|
|
2739
|
+
const changed = thread.backend !== undefined && thread.backend !== backend;
|
|
2740
|
+
if (changed) { thread.turns = []; thread.conversationId = undefined; }
|
|
2741
|
+
thread.backend = backend;
|
|
2742
|
+
return changed;
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2584
2745
|
export function formatBtwTranscript(turns: readonly BtwTurn[]): string {
|
|
2585
2746
|
return turns
|
|
2586
2747
|
.map((turn) => {
|
|
@@ -2630,11 +2791,11 @@ async function runBtwTurn(
|
|
|
2630
2791
|
signal: AbortSignal,
|
|
2631
2792
|
onProgress?: (text: string) => void,
|
|
2632
2793
|
): Promise<{ text: string; conversationId?: string }> {
|
|
2633
|
-
// Claude
|
|
2634
|
-
//
|
|
2635
|
-
// reach the Agy CLI.
|
|
2794
|
+
// Claude continuation is not wired yet; Agy and Grok use native sessions.
|
|
2795
|
+
// Reject Claude explicitly instead of falling back to another backend.
|
|
2636
2796
|
if (selection.backend === "claude") {
|
|
2637
|
-
|
|
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.`);
|
|
2638
2799
|
}
|
|
2639
2800
|
let updateTimer: ReturnType<typeof setTimeout> | undefined;
|
|
2640
2801
|
let latest: string | undefined;
|
|
@@ -2657,7 +2818,7 @@ async function runBtwTurn(
|
|
|
2657
2818
|
feature: "btw",
|
|
2658
2819
|
prompt,
|
|
2659
2820
|
access: options.full ? "workspace-full" : "restricted",
|
|
2660
|
-
cwd: options.
|
|
2821
|
+
cwd: options.cwd,
|
|
2661
2822
|
continuation: options.conversationId ? { id: options.conversationId } : undefined,
|
|
2662
2823
|
},
|
|
2663
2824
|
selection,
|
|
@@ -2681,6 +2842,7 @@ class BtwModal implements Focusable {
|
|
|
2681
2842
|
private bodyHeight = 1;
|
|
2682
2843
|
private running = false;
|
|
2683
2844
|
private full = false;
|
|
2845
|
+
private model = "";
|
|
2684
2846
|
private disposed = false;
|
|
2685
2847
|
|
|
2686
2848
|
get focused(): boolean {
|
|
@@ -2721,6 +2883,11 @@ class BtwModal implements Focusable {
|
|
|
2721
2883
|
this.tui.requestRender();
|
|
2722
2884
|
}
|
|
2723
2885
|
|
|
2886
|
+
setModel(model: string): void {
|
|
2887
|
+
this.model = model;
|
|
2888
|
+
this.tui.requestRender();
|
|
2889
|
+
}
|
|
2890
|
+
|
|
2724
2891
|
setFull(full: boolean): void {
|
|
2725
2892
|
this.full = full;
|
|
2726
2893
|
this.tui.requestRender();
|
|
@@ -2782,10 +2949,9 @@ class BtwModal implements Focusable {
|
|
|
2782
2949
|
const hiddenBelow = Math.max(0, this.maxOffset - this.offset);
|
|
2783
2950
|
const scroll = this.maxOffset > 0 ? ` · ↑${this.offset} ↓${hiddenBelow}` : "";
|
|
2784
2951
|
|
|
2785
|
-
const
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
const header = this.theme.fg("accent", this.theme.bold("Bro · btw")) + this.theme.fg("dim", ` · ${mode}${scroll}`);
|
|
2952
|
+
const model = this.model ? this.theme.fg("dim", ` · ${this.model}`) : "";
|
|
2953
|
+
const mode = this.full ? this.theme.fg("dim", " · ") + this.theme.fg("accent", this.theme.bold("full · edits repo")) : "";
|
|
2954
|
+
const header = this.theme.fg("accent", this.theme.bold("Bro · btw")) + model + mode + this.theme.fg("dim", scroll);
|
|
2789
2955
|
|
|
2790
2956
|
const composer = this.input.render(innerWidth)[0] ?? "";
|
|
2791
2957
|
|
|
@@ -2847,6 +3013,14 @@ async function openBtwModal(
|
|
|
2847
3013
|
modal.setRunning(true);
|
|
2848
3014
|
modal.clearComposer();
|
|
2849
3015
|
|
|
3016
|
+
let settings: BroSettings;
|
|
3017
|
+
try {
|
|
3018
|
+
settings = await readSettings();
|
|
3019
|
+
const backend = capabilityBackend(settings, "btw");
|
|
3020
|
+
if (bindBtwBackend(thread, backend)) modal.setNotice("Backend changed — started a fresh side thread.");
|
|
3021
|
+
} catch (error) {
|
|
3022
|
+
controller = undefined; modal.setRunning(false); modal.setNotice(errorMessage(error)); return;
|
|
3023
|
+
}
|
|
2850
3024
|
const first = thread.turns.length === 0;
|
|
2851
3025
|
let context: string | undefined;
|
|
2852
3026
|
if (first && options.seed) {
|
|
@@ -2861,10 +3035,12 @@ async function openBtwModal(
|
|
|
2861
3035
|
modal.setText(transcript());
|
|
2862
3036
|
|
|
2863
3037
|
try {
|
|
2864
|
-
const
|
|
3038
|
+
const selection = selectionForCapability(settings, "btw");
|
|
3039
|
+
thread.model = selectionLabel(selection);
|
|
3040
|
+
modal.setModel(thread.model);
|
|
2865
3041
|
const result = await runBtwTurn(
|
|
2866
3042
|
buildBtwPrompt(context, question),
|
|
2867
|
-
|
|
3043
|
+
selection,
|
|
2868
3044
|
{ full: thread.full, cwd: ctx.cwd, conversationId: thread.conversationId },
|
|
2869
3045
|
turnController.signal,
|
|
2870
3046
|
(partial) => {
|
|
@@ -2962,6 +3138,7 @@ async function openBtwModal(
|
|
|
2962
3138
|
}
|
|
2963
3139
|
|
|
2964
3140
|
modal.setFull(thread.full);
|
|
3141
|
+
modal.setModel(thread.model ?? "");
|
|
2965
3142
|
modal.setText(transcript());
|
|
2966
3143
|
|
|
2967
3144
|
if (options.initialQuestion) void runTurn(options.initialQuestion);
|
|
@@ -2985,7 +3162,7 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
2985
3162
|
let lastResult: BroResult | undefined;
|
|
2986
3163
|
let btwThread: BtwThread | undefined;
|
|
2987
3164
|
const remember = (result: ModalResult) => {
|
|
2988
|
-
if (result.source) lastResult = { source: result.source, text: result.text };
|
|
3165
|
+
if (result.source) lastResult = { source: result.source, text: result.text, model: result.model };
|
|
2989
3166
|
};
|
|
2990
3167
|
|
|
2991
3168
|
pi.on("session_start", async (_event, _ctx) => {
|
|
@@ -3061,8 +3238,8 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
3061
3238
|
// the activity throttle window.
|
|
3062
3239
|
activityCount: run.activityCount,
|
|
3063
3240
|
};
|
|
3064
|
-
// The backend qualifier appears only
|
|
3065
|
-
const header = `Bro advisor · ${backend === "
|
|
3241
|
+
// The backend qualifier appears only off Agy, keeping the Agy header byte-identical.
|
|
3242
|
+
const header = `Bro advisor · ${backend === "agy" ? "" : `backend: ${backend} · `}model: ${selection.model} · effort: ${effort} · ${run.attempts} attempt${run.attempts === 1 ? "" : "s"} · ${Math.ceil(run.durationMs / 1_000)}s`;
|
|
3066
3243
|
const context = `Context · cwd: ${JSON.stringify(ctx.cwd)} · steering: ${details.steeringIncluded ? "included" : "none"} · snapshot: ${snapshot.text.length} chars · Bro truncation: none · omissions: ${omissions}`;
|
|
3067
3244
|
return { content: [{ type: "text", text: `${header}\n${context}\n\n${run.advice}` }], details };
|
|
3068
3245
|
} finally {
|
|
@@ -3142,13 +3319,14 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
3142
3319
|
return { text: "**Nothing to show yet**\n\nThis session has no conversation turns to draw. Run something first, then press **R**." };
|
|
3143
3320
|
}
|
|
3144
3321
|
let text: string;
|
|
3322
|
+
let model: string;
|
|
3145
3323
|
try {
|
|
3146
|
-
text = await runShowExplanation(captured.text, steering, signal, await readSettings(), onProgress);
|
|
3324
|
+
({ text, model } = await runShowExplanation(captured.text, steering, signal, await readSettings(), onProgress));
|
|
3147
3325
|
} catch (error) {
|
|
3148
3326
|
throw new Error(withDoctor(error));
|
|
3149
3327
|
}
|
|
3150
3328
|
const html = extractShowHtml(text);
|
|
3151
|
-
return { source: captured, text, ...(html ? { htmlPath: await writeShowHtml(html) } : {}) };
|
|
3329
|
+
return { source: captured, text, model, ...(html ? { htmlPath: await writeShowHtml(html) } : {}) };
|
|
3152
3330
|
};
|
|
3153
3331
|
try {
|
|
3154
3332
|
await showBroModal(ctx, {
|
|
@@ -3179,7 +3357,7 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
3179
3357
|
try {
|
|
3180
3358
|
return {
|
|
3181
3359
|
source: target,
|
|
3182
|
-
|
|
3360
|
+
...(await simplify(target.text, signal, await readSettings(), onProgress)),
|
|
3183
3361
|
};
|
|
3184
3362
|
} catch (error) {
|
|
3185
3363
|
throw new Error(withDoctor(error));
|
|
@@ -3215,24 +3393,6 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
3215
3393
|
return;
|
|
3216
3394
|
}
|
|
3217
3395
|
|
|
3218
|
-
if (action === "usage") {
|
|
3219
|
-
const valid = parts.length === 1 || (parts.length === 3 && parts[1] === "--provider" && parts[2] === "agy");
|
|
3220
|
-
if (!valid) {
|
|
3221
|
-
ctx.ui.notify("Use /bro usage or /bro usage --provider agy.", "warning");
|
|
3222
|
-
return;
|
|
3223
|
-
}
|
|
3224
|
-
try {
|
|
3225
|
-
await showBroModal(ctx, {
|
|
3226
|
-
loadingText: "Checking Agy usage…",
|
|
3227
|
-
retryable: false,
|
|
3228
|
-
run: async (signal) => ({ text: await checkAgyUsage(pi, signal) }),
|
|
3229
|
-
});
|
|
3230
|
-
} catch (error) {
|
|
3231
|
-
ctx.ui.notify(withDoctor(error), "error");
|
|
3232
|
-
}
|
|
3233
|
-
return;
|
|
3234
|
-
}
|
|
3235
|
-
|
|
3236
3396
|
if (action === "mode") {
|
|
3237
3397
|
const requested = parts[1];
|
|
3238
3398
|
if (parts.length > 2 || (requested && !parseBroMode(requested))) {
|
|
@@ -3270,7 +3430,44 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
3270
3430
|
try {
|
|
3271
3431
|
const settings = await readSettings();
|
|
3272
3432
|
// /bro model operates on the effective shared backend: no Agy catalog
|
|
3273
|
-
// when the shared default is Claude.
|
|
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
|
+
}
|
|
3274
3471
|
if ((settings.backend ?? "agy") === "claude") {
|
|
3275
3472
|
const requested = value.trim();
|
|
3276
3473
|
let model: string | undefined;
|
|
@@ -3363,7 +3560,7 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
3363
3560
|
|
|
3364
3561
|
if (action === "effort") {
|
|
3365
3562
|
const requested = parts[1];
|
|
3366
|
-
// Claude levels pass this gate; each backend branch below validates strictly
|
|
3563
|
+
// Claude/Grok levels pass this gate; each backend branch below validates strictly
|
|
3367
3564
|
// (the Agy branch still rejects xhigh/max with its supports-list warning).
|
|
3368
3565
|
if (parts.length > 2 || (requested && !isClaudeEffort(requested) && !EFFORTS.some((effort) => effort === requested))) {
|
|
3369
3566
|
ctx.ui.notify("Use /bro effort, or choose low, medium, or high.", "warning");
|
|
@@ -3372,7 +3569,35 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
3372
3569
|
try {
|
|
3373
3570
|
const settings = await readSettings();
|
|
3374
3571
|
// /bro effort operates on the effective shared backend: no Agy catalog
|
|
3375
|
-
// when the shared default is Claude.
|
|
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
|
+
}
|
|
3376
3601
|
if ((settings.backend ?? "agy") === "claude") {
|
|
3377
3602
|
const allowed = ["default", ...CLAUDE_EFFORTS] as const;
|
|
3378
3603
|
if (requested && !allowed.some((effort) => effort === requested)) {
|
|
@@ -3455,8 +3680,10 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
3455
3680
|
return;
|
|
3456
3681
|
}
|
|
3457
3682
|
try {
|
|
3458
|
-
|
|
3683
|
+
const btwBackend = capabilityBackend(await readSettings(), "btw");
|
|
3684
|
+
if (btwBackend === "claude") throw new Error(`${btwBackend === "claude" ? "Claude" : "Grok"} does not support /bro btw yet; choose an Agy or Grok override in /bro config.`);
|
|
3459
3685
|
const thread = resolveBtwThread(btwThread, parsed);
|
|
3686
|
+
if (bindBtwBackend(thread, btwBackend)) ctx.ui.notify("Backend changed — started a fresh side thread.", "info");
|
|
3460
3687
|
btwThread = thread;
|
|
3461
3688
|
await openBtwModal(ctx, { thread, initialQuestion: parsed.question, seed: !parsed.fresh });
|
|
3462
3689
|
} catch (error) {
|
|
@@ -3535,7 +3762,7 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
3535
3762
|
const settings = await readSettings();
|
|
3536
3763
|
return {
|
|
3537
3764
|
source: target,
|
|
3538
|
-
|
|
3765
|
+
...(await simplify(target.text, signal, settings, onProgress)),
|
|
3539
3766
|
};
|
|
3540
3767
|
} catch (error) {
|
|
3541
3768
|
throw new Error(withDoctor(error));
|