pi-bro 0.15.1 → 0.16.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 +7 -0
- package/README.md +44 -29
- package/backend.ts +226 -9
- package/bro.ts +499 -108
- package/package.json +2 -2
package/bro.ts
CHANGED
|
@@ -21,10 +21,13 @@ import {
|
|
|
21
21
|
agyFailureMessage,
|
|
22
22
|
agySelection,
|
|
23
23
|
advisorFlagErrorHint,
|
|
24
|
+
CLAUDE_EFFORTS,
|
|
25
|
+
backendSupports,
|
|
24
26
|
execute as executeBackend,
|
|
25
27
|
parseBtwAgyLine,
|
|
26
28
|
type AgySelection,
|
|
27
29
|
type BackendProgress,
|
|
30
|
+
type BackendSelection,
|
|
28
31
|
} from "./backend.ts";
|
|
29
32
|
|
|
30
33
|
export { agyFailureMessage, agySelection, advisorFlagErrorHint, parseBtwAgyLine };
|
|
@@ -61,22 +64,32 @@ type ModalResult = { source?: BroSource; text: string; htmlPath?: string };
|
|
|
61
64
|
type BtwTurn = { question: string; answer: string };
|
|
62
65
|
type BtwThread = { turns: BtwTurn[]; conversationId?: string; full: boolean };
|
|
63
66
|
const EFFORTS = ["default", "low", "medium", "high"] as const;
|
|
64
|
-
|
|
65
|
-
type
|
|
67
|
+
const BACKENDS = ["agy", "claude"] as const;
|
|
68
|
+
type BackendName = (typeof BACKENDS)[number];
|
|
69
|
+
type BroEffort = "default" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
70
|
+
type AgyEffort = Exclude<BroEffort, "default" | "xhigh" | "max">;
|
|
71
|
+
type ClaudeEffort = (typeof CLAUDE_EFFORTS)[number];
|
|
66
72
|
// "advisor" is a real capability (command, Agy invocation, Doctor check) like the other three;
|
|
67
73
|
// see docs/plans/2026-09-19-bro-advisor-design.md. All four share one model/effort resolution,
|
|
68
74
|
// override, and Doctor-check path via this single list -- there is no configuration-only tier.
|
|
69
75
|
const CAPABILITIES = ["explain", "show", "btw", "advisor"] as const;
|
|
70
76
|
type Capability = (typeof CAPABILITIES)[number];
|
|
71
77
|
const CAPABILITY_LABELS: Record<Capability, string> = { explain: "Explain", show: "Show", btw: "Btw", advisor: "Advisor" };
|
|
72
|
-
type ModelEffortPair = { model: string; effort: BroEffort };
|
|
78
|
+
type ModelEffortPair = { backend?: BackendName; model: string; effort: BroEffort };
|
|
73
79
|
type BroSettings = {
|
|
80
|
+
backend?: BackendName;
|
|
74
81
|
model: string;
|
|
75
82
|
effort: BroEffort;
|
|
76
83
|
mode: BroMode;
|
|
77
84
|
showTurns: number;
|
|
78
85
|
overrides: Partial<Record<Capability, ModelEffortPair>>;
|
|
79
86
|
};
|
|
87
|
+
// BackendSelection (shared execution routing selection) lives in backend.ts; the backend
|
|
88
|
+
// field stays optional there so every legacy backend-less pair keeps meaning Agy.
|
|
89
|
+
export const CLAUDE_MODELS = [
|
|
90
|
+
{ id: "sonnet", label: "Claude Sonnet" },
|
|
91
|
+
{ id: "opus", label: "Claude Opus" },
|
|
92
|
+
] as const;
|
|
80
93
|
type AgyModelFamily = {
|
|
81
94
|
id: string;
|
|
82
95
|
label: string;
|
|
@@ -486,11 +499,38 @@ export async function extractWebPage(input: string, signal?: AbortSignal): Promi
|
|
|
486
499
|
}
|
|
487
500
|
}
|
|
488
501
|
|
|
502
|
+
function parseBackend(value: unknown, context: string): BackendName {
|
|
503
|
+
if (typeof value !== "string" || !BACKENDS.some((backend) => backend === value)) {
|
|
504
|
+
throw new Error(`${context} backend must be "agy" or "claude".`);
|
|
505
|
+
}
|
|
506
|
+
return value as BackendName;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function isAgyEffort(effort: unknown): effort is AgyEffort | "default" {
|
|
510
|
+
return EFFORTS.some((item) => item === effort);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function isClaudeEffort(effort: unknown): effort is ClaudeEffort | "default" {
|
|
514
|
+
return effort === "default" || CLAUDE_EFFORTS.some((item) => item === effort);
|
|
515
|
+
}
|
|
516
|
+
|
|
489
517
|
function parseModelEffortPair(value: unknown, context: string): ModelEffortPair {
|
|
490
|
-
if (!isRecord(value) || typeof value.model !== "string" || !value.model.trim()
|
|
518
|
+
if (!isRecord(value) || typeof value.model !== "string" || !value.model.trim()) {
|
|
491
519
|
throw new Error(`${context} must contain a model and effort set to "default", "low", "medium", or "high".`);
|
|
492
520
|
}
|
|
493
|
-
|
|
521
|
+
const backend = value.backend === undefined ? undefined : parseBackend(value.backend, context);
|
|
522
|
+
if (backend === "claude") {
|
|
523
|
+
if (!isClaudeEffort(value.effort)) {
|
|
524
|
+
throw new Error(`${context} must contain a model and effort set to "default", "low", "medium", "high", "xhigh", or "max".`);
|
|
525
|
+
}
|
|
526
|
+
return { backend, model: value.model.trim(), effort: value.effort };
|
|
527
|
+
}
|
|
528
|
+
if (!isAgyEffort(value.effort)) {
|
|
529
|
+
throw new Error(`${context} must contain a model and effort set to "default", "low", "medium", or "high".`);
|
|
530
|
+
}
|
|
531
|
+
const pair: ModelEffortPair = { model: value.model.trim(), effort: value.effort };
|
|
532
|
+
if (backend !== undefined) pair.backend = backend;
|
|
533
|
+
return pair;
|
|
494
534
|
}
|
|
495
535
|
|
|
496
536
|
function parseOverrides(value: unknown): Partial<Record<Capability, ModelEffortPair>> {
|
|
@@ -505,14 +545,42 @@ function parseOverrides(value: unknown): Partial<Record<Capability, ModelEffortP
|
|
|
505
545
|
}
|
|
506
546
|
|
|
507
547
|
export function parseBroSettings(value: unknown): BroSettings {
|
|
508
|
-
if (
|
|
509
|
-
!isRecord(value) ||
|
|
510
|
-
typeof value.model !== "string" ||
|
|
511
|
-
!value.model.trim() ||
|
|
512
|
-
!EFFORTS.some((effort) => effort === value.effort)
|
|
513
|
-
) {
|
|
548
|
+
if (!isRecord(value)) {
|
|
514
549
|
throw new Error('Settings must contain a model and effort set to "default", "low", "medium", or "high".');
|
|
515
550
|
}
|
|
551
|
+
// v2 disk shape: { version: 2, default: { backend, model, effort }, mode, showTurns, overrides }.
|
|
552
|
+
// Reads never rewrite: a legacy flat file keeps parsing into backend-less (Agy) settings.
|
|
553
|
+
if (value.version !== undefined && value.version !== 2) throw new Error(`Unsupported settings version ${JSON.stringify(value.version)}.`);
|
|
554
|
+
if (value.version === 2 && value.default === undefined) throw new Error("Settings version 2 requires default.");
|
|
555
|
+
if (value.default !== undefined) {
|
|
556
|
+
if (!isRecord(value.default)) throw new Error("Settings default must contain a model and effort.");
|
|
557
|
+
if (value.version !== undefined && value.version !== 2) {
|
|
558
|
+
throw new Error(`Unsupported settings version ${JSON.stringify(value.version)}.`);
|
|
559
|
+
}
|
|
560
|
+
const pair = parseModelEffortPair(value.default, "Settings default");
|
|
561
|
+
const mode = value.mode === undefined ? DEFAULT_BRO_MODE : parseBroMode(value.mode);
|
|
562
|
+
if (!mode) throw new Error('Settings mode must be "brief", "balanced", or "faithful".');
|
|
563
|
+
const showTurns = value.showTurns === undefined ? DEFAULT_SHOW_TURNS : value.showTurns;
|
|
564
|
+
if (typeof showTurns !== "number" || !Number.isInteger(showTurns) || showTurns < 1) {
|
|
565
|
+
throw new Error("Settings showTurns must be a positive whole number of turns.");
|
|
566
|
+
}
|
|
567
|
+
const overrides = parseOverrides(value.overrides);
|
|
568
|
+
const settings: BroSettings = { model: pair.model, effort: pair.effort, mode, showTurns, overrides };
|
|
569
|
+
if (pair.backend !== undefined) settings.backend = pair.backend;
|
|
570
|
+
return settings;
|
|
571
|
+
}
|
|
572
|
+
if (typeof value.model !== "string" || !value.model.trim()) {
|
|
573
|
+
throw new Error('Settings must contain a model and effort set to "default", "low", "medium", or "high".');
|
|
574
|
+
}
|
|
575
|
+
const backend = value.backend === undefined ? undefined : parseBackend(value.backend, "Settings");
|
|
576
|
+
const effortOk = backend === "claude" ? isClaudeEffort(value.effort) : isAgyEffort(value.effort);
|
|
577
|
+
if (!effortOk) {
|
|
578
|
+
throw new Error(
|
|
579
|
+
backend === "claude"
|
|
580
|
+
? 'Settings must contain a model and effort set to "default", "low", "medium", "high", "xhigh", or "max".'
|
|
581
|
+
: 'Settings must contain a model and effort set to "default", "low", "medium", or "high".',
|
|
582
|
+
);
|
|
583
|
+
}
|
|
516
584
|
const mode = value.mode === undefined ? DEFAULT_BRO_MODE : parseBroMode(value.mode);
|
|
517
585
|
if (!mode) throw new Error('Settings mode must be "brief", "balanced", or "faithful".');
|
|
518
586
|
const showTurns = value.showTurns === undefined ? DEFAULT_SHOW_TURNS : value.showTurns;
|
|
@@ -520,7 +588,15 @@ export function parseBroSettings(value: unknown): BroSettings {
|
|
|
520
588
|
throw new Error("Settings showTurns must be a positive whole number of turns.");
|
|
521
589
|
}
|
|
522
590
|
const overrides = parseOverrides(value.overrides);
|
|
523
|
-
|
|
591
|
+
const settings: BroSettings = {
|
|
592
|
+
model: value.model.trim(),
|
|
593
|
+
effort: value.effort as BroSettings["effort"],
|
|
594
|
+
mode,
|
|
595
|
+
showTurns,
|
|
596
|
+
overrides,
|
|
597
|
+
};
|
|
598
|
+
if (backend !== undefined) settings.backend = backend;
|
|
599
|
+
return settings;
|
|
524
600
|
}
|
|
525
601
|
|
|
526
602
|
function capabilityOverride(settings: BroSettings, capability: Capability): ModelEffortPair | undefined {
|
|
@@ -528,7 +604,7 @@ function capabilityOverride(settings: BroSettings, capability: Capability): Mode
|
|
|
528
604
|
}
|
|
529
605
|
|
|
530
606
|
function capabilityPair(settings: BroSettings, capability: Capability): ModelEffortPair {
|
|
531
|
-
return capabilityOverride(settings, capability) ?? { model: settings.model, effort: settings.effort };
|
|
607
|
+
return capabilityOverride(settings, capability) ?? { ...(settings.backend ? { backend: settings.backend } : {}), model: settings.model, effort: settings.effort };
|
|
532
608
|
}
|
|
533
609
|
|
|
534
610
|
// An override always pins both model and effort together (never just one), so a capability's
|
|
@@ -554,13 +630,55 @@ export function resolveModelEffort(
|
|
|
554
630
|
pair: ModelEffortPair,
|
|
555
631
|
families: AgyModelFamily[],
|
|
556
632
|
): { pair: ModelEffortPair; family?: AgyModelFamily } {
|
|
633
|
+
// Claude selections never resolve through the Agy catalog; they pass through untouched.
|
|
634
|
+
if (pair.backend === "claude") return { pair };
|
|
557
635
|
const family = families.find((item) => item.id === pair.model || item.variants.some((variant) => variant.id === pair.model));
|
|
558
636
|
if (!family) return { pair };
|
|
559
637
|
const variant = family.variants.find((item) => item.id === pair.model);
|
|
560
|
-
|
|
561
|
-
family,
|
|
562
|
-
|
|
638
|
+
const resolved: ModelEffortPair = {
|
|
639
|
+
model: family.id,
|
|
640
|
+
effort: pair.effort === "default" && variant?.effort ? variant.effort : pair.effort,
|
|
563
641
|
};
|
|
642
|
+
if (pair.backend !== undefined) resolved.backend = pair.backend;
|
|
643
|
+
return { family, pair: resolved };
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// A capability's effective backend: an explicit override is a complete selection, so a
|
|
647
|
+
// backend-less override still means Agy (all legacy stays Agy) and never inherits the
|
|
648
|
+
// shared default's backend. Only a capability without an override inherits the default.
|
|
649
|
+
export function capabilityBackend(settings: BroSettings, capability: Capability): BackendName {
|
|
650
|
+
const override = settings.overrides[capability];
|
|
651
|
+
if (override) return override.backend ?? "agy";
|
|
652
|
+
return settings.backend ?? "agy";
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Sonnet/opus aliases resolve case-insensitively; any other non-empty string passes
|
|
656
|
+
// through untouched as an explicit user-entered model ID.
|
|
657
|
+
export function resolveClaudeModel(input: string): string {
|
|
658
|
+
const trimmed = input.trim();
|
|
659
|
+
if (!trimmed) throw new Error("Claude model must be a non-empty model ID (sonnet, opus, or an explicit model ID).");
|
|
660
|
+
const alias = CLAUDE_MODELS.find((model) => model.id === trimmed.toLowerCase());
|
|
661
|
+
return alias ? alias.id : trimmed;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// Routes one capability's whole pair to its backend execution selection. Agy keeps the
|
|
665
|
+
// existing model/effort split; Claude carries the model plus an optional effort
|
|
666
|
+
// ("default" means the CLI's own default and is omitted). Throws for an effort the
|
|
667
|
+
// resolved backend does not support instead of silently sending a mismatched pair.
|
|
668
|
+
export function selectionForCapability(settings: BroSettings, capability: Capability): BackendSelection {
|
|
669
|
+
const pair = capabilityPair(settings, capability);
|
|
670
|
+
if (capabilityBackend(settings, capability) === "claude") {
|
|
671
|
+
if (!isClaudeEffort(pair.effort)) {
|
|
672
|
+
throw new Error(`\`${pair.effort}\` is not supported on the Claude backend. Run \`/bro config\` to fix this.`);
|
|
673
|
+
}
|
|
674
|
+
return pair.effort === "default"
|
|
675
|
+
? { backend: "claude", model: pair.model }
|
|
676
|
+
: { backend: "claude", model: pair.model, effort: pair.effort };
|
|
677
|
+
}
|
|
678
|
+
if (!isAgyEffort(pair.effort)) {
|
|
679
|
+
throw new Error(`\`${pair.effort}\` is not supported on the Agy backend. Run \`/bro config\` to fix this.`);
|
|
680
|
+
}
|
|
681
|
+
return agySelection({ model: pair.model, effort: pair.effort });
|
|
564
682
|
}
|
|
565
683
|
|
|
566
684
|
function resolveCapabilitySettings(
|
|
@@ -631,8 +749,17 @@ async function readSettings(): Promise<BroSettings> {
|
|
|
631
749
|
// any individual override that happens to match the shared default; see withCapabilityOverride
|
|
632
750
|
// for why an explicit override is always kept until the user clears it back to "Default".
|
|
633
751
|
export function settingsPayload(settings: BroSettings): Record<string, unknown> {
|
|
634
|
-
const
|
|
635
|
-
|
|
752
|
+
const overrides: Record<string, unknown> = {};
|
|
753
|
+
for (const [capability, pair] of Object.entries(settings.overrides)) {
|
|
754
|
+
if (pair) overrides[capability] = { backend: pair.backend ?? "agy", model: pair.model, effort: pair.effort };
|
|
755
|
+
}
|
|
756
|
+
return {
|
|
757
|
+
version: 2,
|
|
758
|
+
default: { backend: settings.backend ?? "agy", model: settings.model, effort: settings.effort },
|
|
759
|
+
mode: settings.mode,
|
|
760
|
+
showTurns: settings.showTurns,
|
|
761
|
+
...(Object.keys(overrides).length ? { overrides } : {}),
|
|
762
|
+
};
|
|
636
763
|
}
|
|
637
764
|
|
|
638
765
|
async function writeSettings(settings: BroSettings): Promise<void> {
|
|
@@ -744,6 +871,47 @@ function preferredEffort(family: AgyModelFamily): BroEffort {
|
|
|
744
871
|
return family.efforts.includes("low") ? "low" : (family.efforts[0] ?? "default");
|
|
745
872
|
}
|
|
746
873
|
|
|
874
|
+
async function checkClaudeVersion(pi: ExtensionAPI, signal: AbortSignal): Promise<string> {
|
|
875
|
+
const runDirectory = await mkdtemp(join(tmpdir(), "pi-bro-"));
|
|
876
|
+
try {
|
|
877
|
+
const result = await pi.exec("claude", ["--version"], { cwd: runDirectory, signal, timeout: 10_000 });
|
|
878
|
+
if (signal.aborted) throw new Error("Canceled.");
|
|
879
|
+
if (result.killed || result.code !== 0) {
|
|
880
|
+
const detail = result.stderr.trim() || result.stdout.trim();
|
|
881
|
+
throw new Error(
|
|
882
|
+
detail
|
|
883
|
+
? `Claude could not start: ${detail}\n\nRun \`/bro doctor\` for setup help.`
|
|
884
|
+
: "Claude could not start. Make sure Claude is installed and on PATH, then run `/bro doctor`.",
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
const version = result.stdout.trim() || result.stderr.trim();
|
|
888
|
+
if (!version) throw new Error("Claude returned no version information. Update Claude, then run `/bro doctor` again.");
|
|
889
|
+
return version;
|
|
890
|
+
} finally {
|
|
891
|
+
await rm(runDirectory, { recursive: true, force: true });
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// Auth status only: a version/auth answer without a model request. A passing answer here
|
|
896
|
+
// says the CLI starts and reports signed-in state -- it does not imply connectivity.
|
|
897
|
+
async function checkClaudeAuth(pi: ExtensionAPI, signal: AbortSignal): Promise<string> {
|
|
898
|
+
const runDirectory = await mkdtemp(join(tmpdir(), "pi-bro-"));
|
|
899
|
+
try {
|
|
900
|
+
const result = await pi.exec("claude", ["auth", "status"], { cwd: runDirectory, signal, timeout: 15_000 });
|
|
901
|
+
if (signal.aborted) throw new Error("Canceled.");
|
|
902
|
+
if (result.killed || result.code !== 0) {
|
|
903
|
+
const detail = result.stderr.trim() || result.stdout.trim();
|
|
904
|
+
throw new Error(detail ? `Claude auth status: ${detail}` : "Claude auth status is unknown. Sign in, then run `/bro doctor`.");
|
|
905
|
+
}
|
|
906
|
+
let status: unknown;
|
|
907
|
+
try { status = JSON.parse(result.stdout); } catch { throw new Error("Claude returned unreadable auth status; sign in and retry /bro doctor."); }
|
|
908
|
+
if (!isRecord(status) || status.loggedIn !== true) throw new Error("Claude is not signed in. Run `claude auth login`.");
|
|
909
|
+
return "authentication configured (not a connectivity test)";
|
|
910
|
+
} finally {
|
|
911
|
+
await rm(runDirectory, { recursive: true, force: true });
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
747
915
|
async function checkAgyUsage(pi: ExtensionAPI, signal: AbortSignal): Promise<string> {
|
|
748
916
|
const runDirectory = await mkdtemp(join(tmpdir(), "pi-bro-"));
|
|
749
917
|
try {
|
|
@@ -790,53 +958,113 @@ async function doctorReport(pi: ExtensionAPI, ctx: ExtensionCommandContext, sign
|
|
|
790
958
|
fail("Prompt", error);
|
|
791
959
|
}
|
|
792
960
|
|
|
961
|
+
// Probe only the backends some feature actually selects: a Claude-only setup never
|
|
962
|
+
// requires Agy to be installed, and vice versa.
|
|
963
|
+
const agyInUse = !settings || capabilityBackend(settings, "explain") === "agy" || capabilityBackend(settings, "show") === "agy" || capabilityBackend(settings, "btw") === "agy" || capabilityBackend(settings, "advisor") === "agy" || (settings.backend ?? "agy") === "agy";
|
|
964
|
+
const claudeInUse = !!settings && (capabilityBackend(settings, "explain") === "claude" || capabilityBackend(settings, "show") === "claude" || capabilityBackend(settings, "btw") === "claude" || capabilityBackend(settings, "advisor") === "claude" || settings.backend === "claude");
|
|
965
|
+
|
|
793
966
|
let agyStarted = false;
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
967
|
+
if (agyInUse) {
|
|
968
|
+
try {
|
|
969
|
+
agyVersion = await checkAgyVersion(pi, signal);
|
|
970
|
+
pass("Agy", agyVersion);
|
|
971
|
+
agyStarted = true;
|
|
972
|
+
} catch (error) {
|
|
973
|
+
if (signal.aborted) throw error;
|
|
974
|
+
fail("Agy", error);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
if (agyStarted) {
|
|
978
|
+
try {
|
|
979
|
+
models = await listAgyModels(pi, signal);
|
|
980
|
+
pass("Model catalog", `${models.length} model${models.length === 1 ? "" : "s"} available`);
|
|
981
|
+
} catch (error) {
|
|
982
|
+
if (signal.aborted) throw error;
|
|
983
|
+
fail("Model catalog", error);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
try {
|
|
987
|
+
await checkAgyUsage(pi, signal);
|
|
988
|
+
pass("Account", "connected");
|
|
989
|
+
} catch (error) {
|
|
990
|
+
if (signal.aborted) throw error;
|
|
991
|
+
fail("Account", error);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
} else {
|
|
995
|
+
pass("Agy", "not probed — no feature selects the Agy backend");
|
|
801
996
|
}
|
|
802
997
|
|
|
803
|
-
|
|
998
|
+
// Version and auth status only, never a model request. A passing answer says the CLI
|
|
999
|
+
// starts and reports signed-in state -- it does not imply connectivity.
|
|
1000
|
+
if (claudeInUse) {
|
|
804
1001
|
try {
|
|
805
|
-
|
|
806
|
-
pass("Model catalog", `${models.length} model${models.length === 1 ? "" : "s"} available`);
|
|
1002
|
+
pass("Claude", await checkClaudeVersion(pi, signal));
|
|
807
1003
|
} catch (error) {
|
|
808
1004
|
if (signal.aborted) throw error;
|
|
809
|
-
fail("
|
|
1005
|
+
fail("Claude", error);
|
|
810
1006
|
}
|
|
811
|
-
|
|
812
1007
|
try {
|
|
813
|
-
await
|
|
814
|
-
pass("Account", "connected");
|
|
1008
|
+
pass("Claude auth", await checkClaudeAuth(pi, signal));
|
|
815
1009
|
} catch (error) {
|
|
816
1010
|
if (signal.aborted) throw error;
|
|
817
|
-
fail("
|
|
1011
|
+
fail("Claude auth", error);
|
|
818
1012
|
}
|
|
1013
|
+
} else {
|
|
1014
|
+
pass("Claude", "not probed — no feature selects the Claude backend");
|
|
819
1015
|
}
|
|
820
1016
|
|
|
821
|
-
if (settings
|
|
822
|
-
const
|
|
823
|
-
if (
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
if (!current.family.efforts.length && effort === "default") {
|
|
1017
|
+
if (settings) {
|
|
1018
|
+
const defaultBackend = settings.backend ?? "agy";
|
|
1019
|
+
if (defaultBackend === "claude") {
|
|
1020
|
+
pass("Selected model", `claude \`${settings.model}\``);
|
|
1021
|
+
if (!isClaudeEffort(settings.effort)) {
|
|
1022
|
+
fail("Reasoning effort", `\`${settings.effort}\` is unsupported. Run \`/bro effort\` to choose another.`);
|
|
1023
|
+
} else if (settings.effort === "default") {
|
|
829
1024
|
pass("Reasoning effort", "built into the selected model");
|
|
830
|
-
} else if (effort !== "default" && current.family.efforts.includes(effort)) {
|
|
831
|
-
pass("Reasoning effort", effort);
|
|
832
1025
|
} else {
|
|
833
|
-
|
|
1026
|
+
pass("Reasoning effort", settings.effort);
|
|
1027
|
+
}
|
|
1028
|
+
} else if (models) {
|
|
1029
|
+
const current = resolveCatalogSettings(settings, models);
|
|
1030
|
+
if (!current.family) {
|
|
1031
|
+
fail("Selected model", `\`${settings.model}\` is unavailable. Run \`/bro model\` to choose another.`);
|
|
1032
|
+
} else {
|
|
1033
|
+
pass("Selected model", `agy \`${current.family.id}\``);
|
|
1034
|
+
const effort = current.settings.effort;
|
|
1035
|
+
if (!current.family.efforts.length && effort === "default") {
|
|
1036
|
+
pass("Reasoning effort", "built into the selected model");
|
|
1037
|
+
} else if (effort !== "default" && current.family.efforts.includes(effort as AgyEffort)) {
|
|
1038
|
+
pass("Reasoning effort", effort);
|
|
1039
|
+
} else {
|
|
1040
|
+
fail("Reasoning effort", `\`${effort}\` is unsupported. Run \`/bro effort\` to choose another.`);
|
|
1041
|
+
}
|
|
834
1042
|
}
|
|
835
1043
|
}
|
|
836
1044
|
|
|
837
1045
|
for (const capability of CAPABILITIES) {
|
|
838
1046
|
const label = CAPABILITY_LABELS[capability];
|
|
839
1047
|
const override = capabilityOverride(settings, capability);
|
|
1048
|
+
const backend = capabilityBackend(settings, capability);
|
|
1049
|
+
const pair = capabilityPair(settings, capability);
|
|
1050
|
+
if (backend === "claude") {
|
|
1051
|
+
if (capability === "btw") {
|
|
1052
|
+
fail(label, "`btw` is not supported on the Claude backend. Run `/bro config` to give it an Agy model.");
|
|
1053
|
+
continue;
|
|
1054
|
+
}
|
|
1055
|
+
if (!isClaudeEffort(pair.effort)) {
|
|
1056
|
+
fail(label, `\`${pair.effort}\` is unsupported for claude \`${pair.model}\`. Run \`/bro config\` to fix this.`);
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
1059
|
+
pass(
|
|
1060
|
+
label,
|
|
1061
|
+
override
|
|
1062
|
+
? `override claude \`${pair.model}\`${pair.effort === "default" ? "" : ` (${pair.effort})`}`
|
|
1063
|
+
: `claude \`${pair.model}\`${pair.effort === "default" ? "" : ` (${pair.effort})`} · using the shared default`,
|
|
1064
|
+
);
|
|
1065
|
+
continue;
|
|
1066
|
+
}
|
|
1067
|
+
if (!models) continue;
|
|
840
1068
|
const resolved = resolveCapabilitySettings(settings, capability, models);
|
|
841
1069
|
if (!resolved.family) {
|
|
842
1070
|
fail(label, `\`${resolved.pair.model}\` is unavailable. Run \`/bro config\` to fix this override.`);
|
|
@@ -844,7 +1072,7 @@ async function doctorReport(pi: ExtensionAPI, ctx: ExtensionCommandContext, sign
|
|
|
844
1072
|
}
|
|
845
1073
|
const effortOk = !resolved.family.efforts.length
|
|
846
1074
|
? resolved.pair.effort === "default"
|
|
847
|
-
: resolved.pair.effort !== "default" && resolved.family.efforts.includes(resolved.pair.effort);
|
|
1075
|
+
: resolved.pair.effort !== "default" && resolved.family.efforts.includes(resolved.pair.effort as AgyEffort);
|
|
848
1076
|
if (!effortOk) {
|
|
849
1077
|
fail(label, `\`${resolved.pair.effort}\` is unsupported for \`${resolved.family.id}\`. Run \`/bro config\` to fix this.`);
|
|
850
1078
|
continue;
|
|
@@ -852,7 +1080,7 @@ async function doctorReport(pi: ExtensionAPI, ctx: ExtensionCommandContext, sign
|
|
|
852
1080
|
pass(
|
|
853
1081
|
label,
|
|
854
1082
|
override
|
|
855
|
-
? `override \`${resolved.family.id}\`${resolved.pair.effort === "default" ? "" : ` (${resolved.pair.effort})`}`
|
|
1083
|
+
? `override agy \`${resolved.family.id}\`${resolved.pair.effort === "default" ? "" : ` (${resolved.pair.effort})`}`
|
|
856
1084
|
: "using the shared default",
|
|
857
1085
|
);
|
|
858
1086
|
}
|
|
@@ -866,11 +1094,15 @@ async function doctorReport(pi: ExtensionAPI, ctx: ExtensionCommandContext, sign
|
|
|
866
1094
|
if (advisorExposed && !advisorActive) fail("Advisor tool", "bro_advisor is exposed but not active in this session. Run /reload.");
|
|
867
1095
|
else pass("Advisor tool", advisorExposed ? "bro_advisor is exposed and active" : "bro_advisor is not exposed by this host (tool restriction, or the extension has not finished loading)");
|
|
868
1096
|
pass("Advisor steering", resolveAdvisorState(ctx.sessionManager.getBranch()).steering.trim() ? "present" : "none");
|
|
869
|
-
if (
|
|
1097
|
+
if (settings && capabilityBackend(settings, "advisor") === "claude") {
|
|
1098
|
+
pass("Advisor compatibility", "Claude backend — no Agy version floor applies");
|
|
1099
|
+
} else if (agyVersion && advisorAgyCompatible(agyVersion)) pass("Advisor compatibility", ADVISOR_COMPATIBILITY);
|
|
870
1100
|
else if (agyVersion) fail("Advisor compatibility", `installed \`${agyVersion}\`; requires Agy >=1.1.15. Run \`agy update\`.`);
|
|
871
1101
|
|
|
872
1102
|
return `# Bro doctor\n\n${lines.join("\n")}\n\n**${failed ? "Bro needs attention." : "Bro is ready."}**\n\n${
|
|
873
|
-
failed
|
|
1103
|
+
failed
|
|
1104
|
+
? "Fix the failed items, then press **R** to check again."
|
|
1105
|
+
: "No assistant response was sent and no model turn was run. Claude version/auth status does not imply connectivity."
|
|
874
1106
|
}`;
|
|
875
1107
|
}
|
|
876
1108
|
|
|
@@ -1064,7 +1296,7 @@ async function simplify(
|
|
|
1064
1296
|
settings: BroSettings,
|
|
1065
1297
|
onProgress?: (text: string) => void,
|
|
1066
1298
|
): Promise<string> {
|
|
1067
|
-
return runAgyText((await promptFor(response, settings.mode)).text,
|
|
1299
|
+
return runAgyText((await promptFor(response, settings.mode)).text, selectionForCapability(settings, "explain"), signal, onProgress);
|
|
1068
1300
|
}
|
|
1069
1301
|
|
|
1070
1302
|
async function runShowExplanation(
|
|
@@ -1074,7 +1306,7 @@ async function runShowExplanation(
|
|
|
1074
1306
|
settings: BroSettings,
|
|
1075
1307
|
onProgress?: (text: string) => void,
|
|
1076
1308
|
): Promise<string> {
|
|
1077
|
-
return runAgyText(buildShowPrompt(transcript, steering),
|
|
1309
|
+
return runAgyText(buildShowPrompt(transcript, steering), selectionForCapability(settings, "show"), signal, onProgress, "show");
|
|
1078
1310
|
}
|
|
1079
1311
|
|
|
1080
1312
|
// Thin presentation-boundary wrapper around the shared backend: coalesces raw text progress to the
|
|
@@ -1082,7 +1314,7 @@ async function runShowExplanation(
|
|
|
1082
1314
|
// tagged outcome back into this function's existing throw-on-failure contract.
|
|
1083
1315
|
async function runAgyText(
|
|
1084
1316
|
prompt: string,
|
|
1085
|
-
selection:
|
|
1317
|
+
selection: BackendSelection,
|
|
1086
1318
|
signal: AbortSignal,
|
|
1087
1319
|
onProgress?: (text: string) => void,
|
|
1088
1320
|
feature: "explain" | "show" = "explain",
|
|
@@ -1253,7 +1485,7 @@ export type AdvisorActivityCallback = (label: string, timestamp: number) => void
|
|
|
1253
1485
|
// which owns the 3-attempt retry/backoff policy the backend itself never performs.
|
|
1254
1486
|
export async function runAdvisorConsultation(
|
|
1255
1487
|
prompt: string,
|
|
1256
|
-
selection:
|
|
1488
|
+
selection: BackendSelection,
|
|
1257
1489
|
cwd: string,
|
|
1258
1490
|
signal: AbortSignal,
|
|
1259
1491
|
killEscalationMs = 5_000,
|
|
@@ -1302,7 +1534,7 @@ function advisorDelay(ms: number, signal: AbortSignal, onTick?: (remainingMs: nu
|
|
|
1302
1534
|
|
|
1303
1535
|
export type AdvisorConsult = (
|
|
1304
1536
|
prompt: string,
|
|
1305
|
-
selection:
|
|
1537
|
+
selection: BackendSelection,
|
|
1306
1538
|
cwd: string,
|
|
1307
1539
|
signal: AbortSignal,
|
|
1308
1540
|
killEscalationMs?: number,
|
|
@@ -1316,6 +1548,7 @@ export type AdvisorToolDetails = {
|
|
|
1316
1548
|
elapsedMs: number;
|
|
1317
1549
|
error?: string;
|
|
1318
1550
|
retryInMs?: number;
|
|
1551
|
+
backend?: BackendName;
|
|
1319
1552
|
model?: string;
|
|
1320
1553
|
effort?: string;
|
|
1321
1554
|
durationMs?: number;
|
|
@@ -1348,7 +1581,7 @@ const ADVISOR_ACTIVITY_THROTTLE_MS = 250;
|
|
|
1348
1581
|
// --conversation, even across retries.
|
|
1349
1582
|
export async function runAdvisorWithRetries(
|
|
1350
1583
|
prompt: string,
|
|
1351
|
-
selection:
|
|
1584
|
+
selection: BackendSelection,
|
|
1352
1585
|
cwd: string,
|
|
1353
1586
|
signal: AbortSignal,
|
|
1354
1587
|
consult: AdvisorConsult = runAdvisorConsultation,
|
|
@@ -1455,11 +1688,12 @@ export function advisorAttemptLabel(details: AdvisorToolDetails): string {
|
|
|
1455
1688
|
return `Bro advisor · ${details.model ?? "unknown model"} · ${details.attempt} attempt${details.attempt === 1 ? "" : "s"} · ${Math.ceil((details.durationMs ?? details.elapsedMs) / 1_000)}s`;
|
|
1456
1689
|
}
|
|
1457
1690
|
const latest = details.activity?.at(-1);
|
|
1458
|
-
|
|
1691
|
+
const backendLabel = details.backend === "claude" ? "Claude" : "Agy";
|
|
1692
|
+
// "last reported" + freshness, never a claim about what the backend is doing right now and never
|
|
1459
1693
|
// "stalled" -- silence since lastActivityAt is not itself evidence of a stuck run.
|
|
1460
1694
|
const activityLabel = latest
|
|
1461
1695
|
? `last reported: ${advisorActivityPreview(latest)} (${Math.max(0, Math.floor((Date.now() - (details.lastActivityAt ?? Date.now())) / 1_000))}s ago)`
|
|
1462
|
-
:
|
|
1696
|
+
: `awaiting first activity from ${backendLabel}`;
|
|
1463
1697
|
return `Bro advisor · running · ${Math.floor(details.elapsedMs / 1_000)}s · attempt ${details.attempt}/${details.of} · ${activityLabel}`;
|
|
1464
1698
|
}
|
|
1465
1699
|
|
|
@@ -1473,7 +1707,10 @@ function showTurnsValues(current: number): string[] {
|
|
|
1473
1707
|
// one that simply isn't in the current catalog (both used to render as "fixed", which read as
|
|
1474
1708
|
// falsely healthy for an unavailable model), and flags a stored effort that isn't one of the
|
|
1475
1709
|
// resolved family's supported efforts instead of silently showing it as if it were valid.
|
|
1476
|
-
function effortDisplay(resolved: { pair: ModelEffortPair; family?: AgyModelFamily }): string {
|
|
1710
|
+
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 the Claude levels (or "default" for the CLI's own default).
|
|
1713
|
+
if (backend === "claude") return isClaudeEffort(resolved.pair.effort) ? resolved.pair.effort : `${resolved.pair.effort} (unsupported)`;
|
|
1477
1714
|
if (!resolved.family) return "unavailable";
|
|
1478
1715
|
const fixed = !resolved.family.efforts.length;
|
|
1479
1716
|
const valid = fixed ? resolved.pair.effort === "default" : resolved.family.efforts.includes(resolved.pair.effort as AgyEffort);
|
|
@@ -1481,6 +1718,16 @@ function effortDisplay(resolved: { pair: ModelEffortPair; family?: AgyModelFamil
|
|
|
1481
1718
|
return fixed ? "fixed" : resolved.pair.effort;
|
|
1482
1719
|
}
|
|
1483
1720
|
|
|
1721
|
+
// Model-picker values that switch to the Claude backend carry a "claude:" prefix so one
|
|
1722
|
+
// atomic picker commit changes backend+model together -- cancelling the picker (Esc)
|
|
1723
|
+
// leaves both unchanged via the existing submenu-cancel path.
|
|
1724
|
+
const CLAUDE_OPTION_PREFIX = "claude:";
|
|
1725
|
+
function parseClaudeOption(value: string): string | undefined {
|
|
1726
|
+
return value.startsWith(CLAUDE_OPTION_PREFIX) && value.length > CLAUDE_OPTION_PREFIX.length
|
|
1727
|
+
? value.slice(CLAUDE_OPTION_PREFIX.length)
|
|
1728
|
+
: undefined;
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1484
1731
|
// Testable core: takes settings/catalog/persist as plain arguments so smoke tests can drive
|
|
1485
1732
|
// the exact interaction (submenus, cancel, cycling, save failure) without a real Agy process
|
|
1486
1733
|
// or settings file. showBroConfigModal below wires this to the real ctx/pi/filesystem.
|
|
@@ -1517,13 +1764,33 @@ export function createConfigModal(
|
|
|
1517
1764
|
value: family.id,
|
|
1518
1765
|
label: `${family.label}${family.efforts.length ? "" : " · fixed effort"}`,
|
|
1519
1766
|
})),
|
|
1767
|
+
// Claude entries stay last so existing Agy keyboard navigation is unaffected.
|
|
1768
|
+
...CLAUDE_MODELS.map((model) => ({
|
|
1769
|
+
value: `${CLAUDE_OPTION_PREFIX}${model.id}`,
|
|
1770
|
+
label: `${model.label} · claude`,
|
|
1771
|
+
})),
|
|
1520
1772
|
];
|
|
1773
|
+
options.push({ value: "__claude_custom__", label: "Claude · custom model ID…" });
|
|
1774
|
+
const input = new Input();
|
|
1775
|
+
let enteringModel = false;
|
|
1776
|
+
input.onSubmit = (value) => { if (value.trim()) pickerDone(`${CLAUDE_OPTION_PREFIX}${value.trim()}`); };
|
|
1521
1777
|
const picker = new SelectList(options, Math.min(options.length, 8), getSelectListTheme());
|
|
1522
1778
|
const selectedIndex = capability && current === "Default" ? 0 : options.findIndex((option) => option.value === current);
|
|
1523
1779
|
picker.setSelectedIndex(Math.max(0, selectedIndex));
|
|
1524
|
-
picker.onSelect = (item) =>
|
|
1780
|
+
picker.onSelect = (item) => {
|
|
1781
|
+
if (item.value === "__claude_custom__") { enteringModel = true; tui.requestRender(); }
|
|
1782
|
+
else pickerDone(item.value);
|
|
1783
|
+
};
|
|
1525
1784
|
picker.onCancel = () => pickerDone();
|
|
1526
|
-
return
|
|
1785
|
+
return {
|
|
1786
|
+
render: (width: number) => enteringModel ? ["Claude model ID (Enter saves, Esc cancels)", ...input.render(width)] : picker.render(width),
|
|
1787
|
+
invalidate: () => { picker.invalidate(); input.invalidate(); },
|
|
1788
|
+
handleInput: (data: string) => {
|
|
1789
|
+
if (enteringModel && matchesKey(data, "escape")) pickerDone();
|
|
1790
|
+
else if (enteringModel) input.handleInput(data);
|
|
1791
|
+
else picker.handleInput(data);
|
|
1792
|
+
},
|
|
1793
|
+
};
|
|
1527
1794
|
};
|
|
1528
1795
|
|
|
1529
1796
|
const modelItem: SettingItem = { id: "model", label: "Default model", currentValue: settings.model, submenu: modelPicker };
|
|
@@ -1555,21 +1822,27 @@ export function createConfigModal(
|
|
|
1555
1822
|
) as Record<Capability, { model: SettingItem; effort: SettingItem }>;
|
|
1556
1823
|
|
|
1557
1824
|
function refresh(): void {
|
|
1825
|
+
const defaultBackend = settings.backend ?? "agy";
|
|
1558
1826
|
const def = resolveModelEffort({ model: settings.model, effort: settings.effort }, families);
|
|
1559
|
-
modelItem.currentValue = def.family?.id ?? settings.model;
|
|
1560
|
-
effortItem.currentValue = effortDisplay(def);
|
|
1561
|
-
effortItem.values = def.family?.efforts.length ? [...def.family.efforts] : undefined;
|
|
1827
|
+
modelItem.currentValue = defaultBackend === "claude" ? `${CLAUDE_OPTION_PREFIX}${settings.model}` : (def.family?.id ?? settings.model);
|
|
1828
|
+
effortItem.currentValue = effortDisplay(def, defaultBackend);
|
|
1829
|
+
effortItem.values = defaultBackend === "claude" ? ["default", ...CLAUDE_EFFORTS] : def.family?.efforts.length ? [...def.family.efforts] : undefined;
|
|
1562
1830
|
modeItem.currentValue = settings.mode;
|
|
1563
1831
|
showTurnsItem.currentValue = String(settings.showTurns);
|
|
1564
1832
|
showTurnsItem.values = showTurnsValues(settings.showTurns);
|
|
1565
1833
|
|
|
1566
1834
|
for (const capability of CAPABILITIES) {
|
|
1567
1835
|
const override = capabilityOverride(settings, capability);
|
|
1836
|
+
const backend = capabilityBackend(settings, capability);
|
|
1568
1837
|
const resolved = resolveModelEffort(capabilityPair(settings, capability), families);
|
|
1569
1838
|
const rows = capabilityItems[capability];
|
|
1570
|
-
rows.model.currentValue = override
|
|
1571
|
-
|
|
1572
|
-
|
|
1839
|
+
rows.model.currentValue = !override
|
|
1840
|
+
? "Default"
|
|
1841
|
+
: backend === "claude"
|
|
1842
|
+
? `${CLAUDE_OPTION_PREFIX}${override.model}`
|
|
1843
|
+
: (resolved.family?.id ?? override.model);
|
|
1844
|
+
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;
|
|
1573
1846
|
}
|
|
1574
1847
|
}
|
|
1575
1848
|
refresh();
|
|
@@ -1624,17 +1897,30 @@ export function createConfigModal(
|
|
|
1624
1897
|
|
|
1625
1898
|
const onChange = (id: string, newValue: string) => {
|
|
1626
1899
|
if (id === "model") {
|
|
1627
|
-
const
|
|
1628
|
-
if (
|
|
1629
|
-
|
|
1630
|
-
|
|
1900
|
+
const claudeId = parseClaudeOption(newValue);
|
|
1901
|
+
if (claudeId !== undefined) {
|
|
1902
|
+
const effort = settings.backend === "claude" && isClaudeEffort(settings.effort) ? settings.effort : "default";
|
|
1903
|
+
settings = { ...settings, backend: "claude", model: resolveClaudeModel(claudeId), effort };
|
|
1904
|
+
} else {
|
|
1905
|
+
const family = findFamily(newValue);
|
|
1906
|
+
if (!family) return;
|
|
1907
|
+
const keepCurrent = settings.backend !== "claude" && (settings.effort === "default" ? !family.efforts.length : family.efforts.includes(settings.effort as AgyEffort));
|
|
1908
|
+
// Backend-less internal pairs mean Agy; disk saves still tag them explicitly.
|
|
1909
|
+
const { backend: _dropped, ...rest } = settings;
|
|
1910
|
+
settings = { ...rest, model: family.id, effort: keepCurrent ? settings.effort : preferredEffort(family) };
|
|
1911
|
+
}
|
|
1631
1912
|
} else if (id === "effort") {
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1913
|
+
if ((settings.backend ?? "agy") === "claude") {
|
|
1914
|
+
if (!isClaudeEffort(newValue)) return;
|
|
1915
|
+
settings = { ...settings, effort: newValue };
|
|
1916
|
+
} else {
|
|
1917
|
+
// Effort-only edit: pin the resolved family's canonical id, same reasoning as the
|
|
1918
|
+
// capability-override effort-only edit below -- otherwise a shared default created
|
|
1919
|
+
// from a suffixed variant id (e.g. "gemini-x-low") would end up paired with an
|
|
1920
|
+
// unrelated effort instead of its actual family id.
|
|
1921
|
+
const resolved = resolveModelEffort({ model: settings.model, effort: settings.effort }, families);
|
|
1922
|
+
settings = { ...settings, model: resolved.family?.id ?? settings.model, effort: newValue as BroEffort };
|
|
1923
|
+
}
|
|
1638
1924
|
} else if (id === "mode") {
|
|
1639
1925
|
const mode = parseBroMode(newValue);
|
|
1640
1926
|
if (!mode) return;
|
|
@@ -1650,15 +1936,30 @@ export function createConfigModal(
|
|
|
1650
1936
|
if (newValue === "__default__") {
|
|
1651
1937
|
settings = withCapabilityOverride(settings, capability, undefined);
|
|
1652
1938
|
} else {
|
|
1653
|
-
const
|
|
1654
|
-
if (
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1939
|
+
const claudeId = parseClaudeOption(newValue);
|
|
1940
|
+
if (claudeId !== undefined) {
|
|
1941
|
+
const currentEffort = capabilityPair(settings, capability).effort;
|
|
1942
|
+
settings = withCapabilityOverride(settings, capability, {
|
|
1943
|
+
backend: "claude",
|
|
1944
|
+
model: resolveClaudeModel(claudeId),
|
|
1945
|
+
effort: capabilityBackend(settings, capability) === "claude" && isClaudeEffort(currentEffort) ? currentEffort : "default",
|
|
1946
|
+
});
|
|
1947
|
+
} else {
|
|
1948
|
+
const family = findFamily(newValue);
|
|
1949
|
+
if (!family) return;
|
|
1950
|
+
const currentEffort = capabilityPair(settings, capability).effort;
|
|
1951
|
+
const keepCurrent = capabilityBackend(settings, capability) === "agy" && (currentEffort === "default" ? !family.efforts.length : family.efforts.includes(currentEffort as AgyEffort));
|
|
1952
|
+
settings = withCapabilityOverride(settings, capability, {
|
|
1953
|
+
model: family.id,
|
|
1954
|
+
effort: keepCurrent ? currentEffort : preferredEffort(family),
|
|
1955
|
+
});
|
|
1956
|
+
}
|
|
1661
1957
|
}
|
|
1958
|
+
} else if (capabilityBackend(settings, capability) === "claude") {
|
|
1959
|
+
if (!isClaudeEffort(newValue)) return;
|
|
1960
|
+
const existing = capabilityOverride(settings, capability);
|
|
1961
|
+
const model = existing?.model ?? settings.model;
|
|
1962
|
+
settings = withCapabilityOverride(settings, capability, { backend: "claude", model, effort: newValue });
|
|
1662
1963
|
} else {
|
|
1663
1964
|
// Effort-only edit: pin the resolved family's canonical id, never whatever raw
|
|
1664
1965
|
// string happens to sit in settings.model/override.model (which — for a shared
|
|
@@ -1716,14 +2017,21 @@ export async function showBroConfigModal(ctx: ExtensionCommandContext, pi: Exten
|
|
|
1716
2017
|
return;
|
|
1717
2018
|
}
|
|
1718
2019
|
let settings: BroSettings;
|
|
1719
|
-
let families: AgyModelFamily[];
|
|
1720
2020
|
try {
|
|
1721
2021
|
settings = await readSettings();
|
|
1722
|
-
families = await listAgyModels(pi);
|
|
1723
2022
|
} catch (error) {
|
|
1724
2023
|
ctx.ui.notify(withDoctor(error), "error");
|
|
1725
2024
|
return;
|
|
1726
2025
|
}
|
|
2026
|
+
// A Claude-only setup (shared default and every override on Claude) opens without the
|
|
2027
|
+
// Agy catalog; anything still on Agy keeps the previous hard requirement.
|
|
2028
|
+
let families: AgyModelFamily[];
|
|
2029
|
+
try {
|
|
2030
|
+
families = await listAgyModels(pi);
|
|
2031
|
+
} catch (error) {
|
|
2032
|
+
families = [];
|
|
2033
|
+
ctx.ui.notify("Agy model catalog unavailable — showing Claude settings without Agy choices.", "warning");
|
|
2034
|
+
}
|
|
1727
2035
|
await ctx.ui.custom<void>(createConfigModal(settings, families, writeSettings), {
|
|
1728
2036
|
overlay: true,
|
|
1729
2037
|
overlayOptions: {
|
|
@@ -1845,13 +2153,15 @@ export function helpText(settings?: BroSettings, settingsError?: string): string
|
|
|
1845
2153
|
? CAPABILITIES.map((capability) => {
|
|
1846
2154
|
const override = capabilityOverride(settings, capability);
|
|
1847
2155
|
return override
|
|
1848
|
-
? `- **${CAPABILITY_LABELS[capability]} override:** \`${override.model}\`${override.effort === "default" ? "" : ` (${override.effort})`}`
|
|
2156
|
+
? `- **${CAPABILITY_LABELS[capability]} override:** ${override.backend ?? "agy"} \`${override.model}\`${override.effort === "default" ? "" : ` (${override.effort})`}`
|
|
1849
2157
|
: undefined;
|
|
1850
2158
|
}).filter((line): line is string => line !== undefined)
|
|
1851
2159
|
: [];
|
|
1852
2160
|
const settingsSummary = settings
|
|
1853
|
-
? `- **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")}` : ""}`
|
|
2161
|
+
? `- **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")}` : ""}`
|
|
1854
2162
|
: `Bro could not read its settings: ${settingsError}\n\nRun \`/bro doctor\` for setup help.`;
|
|
2163
|
+
const advisorBackend = settings ? capabilityBackend(settings, "advisor") : "agy";
|
|
2164
|
+
const advisorProcess = advisorBackend === "claude" ? "Claude process" : "Agy process";
|
|
1855
2165
|
return `# Bro
|
|
1856
2166
|
|
|
1857
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 sandboxed side conversation with \`/bro btw\` — without adding anything to Pi's conversation.
|
|
@@ -1871,14 +2181,14 @@ Press **R** to simplify the captured source again. Run a new \`/bro text\`, \`/b
|
|
|
1871
2181
|
|
|
1872
2182
|
## Check and configure
|
|
1873
2183
|
|
|
1874
|
-
- \`/bro doctor\` — check settings,
|
|
2184
|
+
- \`/bro doctor\` — check settings, backends, account, model, effort, and mode (per-feature backend/model/effort)
|
|
1875
2185
|
- \`/bro usage [--provider agy]\` — show current Agy limits
|
|
1876
|
-
- \`/bro model [id]\` — view or choose the shared default Agy
|
|
1877
|
-
- \`/bro effort [low|medium|high]\` — view or choose the shared default reasoning effort
|
|
2186
|
+
- \`/bro model [id]\` — view or choose the shared default model (Agy catalog, or sonnet/opus/explicit IDs on the Claude backend)
|
|
2187
|
+
- \`/bro effort [low|medium|high|xhigh|max]\` — view or choose the shared default reasoning effort (xhigh/max are Claude-only)
|
|
1878
2188
|
- \`/bro mode [brief|balanced|faithful]\` — view or choose explanation mode
|
|
1879
|
-
- \`/bro config\` — open an interactive settings screen for the shared default model/effort, explain mode, show turns, and per-capability (explain/show/btw/advisor) 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.
|
|
2189
|
+
- \`/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.
|
|
1880
2190
|
|
|
1881
|
-
\`/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 model or effort.
|
|
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\` is not supported on the Claude backend.
|
|
1882
2192
|
|
|
1883
2193
|
## Side conversation
|
|
1884
2194
|
|
|
@@ -1886,12 +2196,12 @@ Press **R** to simplify the captured source again. Run a new \`/bro text\`, \`/b
|
|
|
1886
2196
|
|
|
1887
2197
|
## Advisor
|
|
1888
2198
|
|
|
1889
|
-
- \`bro_advisor\` — a tool the executor agent can voluntarily call mid-task for a second opinion from a fresh
|
|
2199
|
+
- \`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
|
|
1890
2200
|
- \`/bro advisor\` — a quick notice of whether \`bro_advisor\` is available right now, pointing at \`/bro config\`, \`/bro advisor-steer\`, and \`/bro doctor\`
|
|
1891
2201
|
- \`/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
|
|
1892
2202
|
- \`/bro doctor\` — the full advisor diagnostic: whether this host exposes and activates \`bro_advisor\`, its resolved model/effort, steering presence, and the Agy compatibility floor
|
|
1893
2203
|
|
|
1894
|
-
Each consultation is a fresh, standalone
|
|
2204
|
+
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.
|
|
1895
2205
|
|
|
1896
2206
|
## Current settings
|
|
1897
2207
|
|
|
@@ -1925,11 +2235,11 @@ Bro temporarily captures mouse input while the modal is open. Native mouse selec
|
|
|
1925
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 cannot read the repository or other files on its own. On a remote or headless session with no display, pressing **O** reports a failure instead of opening the diagram.
|
|
1926
2236
|
- Show reflects what was reported in the conversation, not independent verification against the actual code or system state.
|
|
1927
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's \`--conversation\` support.
|
|
1928
|
-
- 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 Agy's own diagnostic as the failure.
|
|
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 ${advisorBackend === "claude" ? "Claude" : "Agy"}'s own diagnostic as the failure.
|
|
1929
2239
|
|
|
1930
2240
|
## Privacy and safety
|
|
1931
2241
|
|
|
1932
|
-
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
|
|
2242
|
+
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.
|
|
1933
2243
|
|
|
1934
2244
|
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.
|
|
1935
2245
|
|
|
@@ -1938,7 +2248,7 @@ For webpages, it connects directly to the site without browser cookies; the site
|
|
|
1938
2248
|
|
|
1939
2249
|
Usage and Doctor checks contact Agy but do not send source text or run a model turn. Pressing **C** sends the explanation to your system clipboard.
|
|
1940
2250
|
|
|
1941
|
-
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
|
|
2251
|
+
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.
|
|
1942
2252
|
|
|
1943
2253
|
## Custom prompt
|
|
1944
2254
|
|
|
@@ -2315,11 +2625,17 @@ export function parseBtwComposerCommand(value: string): BtwComposerAction {
|
|
|
2315
2625
|
// failed turns.
|
|
2316
2626
|
async function runBtwTurn(
|
|
2317
2627
|
prompt: string,
|
|
2318
|
-
selection:
|
|
2628
|
+
selection: BackendSelection,
|
|
2319
2629
|
options: { full: boolean; cwd: string; conversationId?: string },
|
|
2320
2630
|
signal: AbortSignal,
|
|
2321
2631
|
onProgress?: (text: string) => void,
|
|
2322
2632
|
): Promise<{ text: string; conversationId?: string }> {
|
|
2633
|
+
// Claude supports explain/show (restricted, fresh) and advisor (workspace-full, fresh);
|
|
2634
|
+
// btw is rejected. Fail before spawning anything so a Claude btw selection can never
|
|
2635
|
+
// reach the Agy CLI.
|
|
2636
|
+
if (selection.backend === "claude") {
|
|
2637
|
+
throw new Error("`btw` is not supported on the Claude backend. Run `/bro config` to give it an Agy model.");
|
|
2638
|
+
}
|
|
2323
2639
|
let updateTimer: ReturnType<typeof setTimeout> | undefined;
|
|
2324
2640
|
let latest: string | undefined;
|
|
2325
2641
|
const throttledProgress = onProgress
|
|
@@ -2548,7 +2864,7 @@ async function openBtwModal(
|
|
|
2548
2864
|
const settings = await readSettings();
|
|
2549
2865
|
const result = await runBtwTurn(
|
|
2550
2866
|
buildBtwPrompt(context, question),
|
|
2551
|
-
|
|
2867
|
+
selectionForCapability(settings, "btw"),
|
|
2552
2868
|
{ full: thread.full, cwd: ctx.cwd, conversationId: thread.conversationId },
|
|
2553
2869
|
turnController.signal,
|
|
2554
2870
|
(partial) => {
|
|
@@ -2681,10 +2997,10 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
2681
2997
|
name: ADVISOR_TOOL_NAME,
|
|
2682
2998
|
label: "Bro advisor",
|
|
2683
2999
|
description:
|
|
2684
|
-
"Consult a fresh, independent
|
|
2685
|
-
promptSnippet: "Consult a fresh
|
|
3000
|
+
"Consult a fresh, independent process of the configured advisor backend for a second opinion mid-task. It has real, unsandboxed tool access in the current workspace (read files, search, run commands) with permissions auto-approved, and is instructed to investigate before advising and to leave edits to you — that is a behavioral instruction to the advisor, not an enforced restriction, so treat its findings as advice rather than a delegated implementation. You never need to prepare a summary or evidence first: Bro automatically captures your system instructions, active tools, and the conversation so far, plus any human-set steering priorities, and sends them to the advisor.",
|
|
3001
|
+
promptSnippet: "Consult a fresh configured-backend process for a second opinion mid-task; it investigates the workspace itself and returns advice",
|
|
2686
3002
|
promptGuidelines: [
|
|
2687
|
-
"Call bro_advisor before or after a non-trivial design or scope decision, or when genuinely uncertain, for a second opinion from a fresh, independent
|
|
3003
|
+
"Call bro_advisor before or after a non-trivial design or scope decision, or when genuinely uncertain, for a second opinion from a fresh, independent process of the configured advisor backend.",
|
|
2688
3004
|
"bro_advisor's question parameter is optional — never delay a call to first prepare a summary or evidence; Bro captures context automatically.",
|
|
2689
3005
|
"Any human-set steering priorities are applied automatically by bro_advisor; you don't need to relay or repeat them.",
|
|
2690
3006
|
"bro_advisor is instructed to only return advice and leave edits to you — that instruction is not enforced, so verify its findings yourself rather than treating them as a completed implementation.",
|
|
@@ -2697,10 +3013,11 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
2697
3013
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
2698
3014
|
const state = resolveAdvisorState(ctx.sessionManager.getBranch());
|
|
2699
3015
|
const settings = await readSettings();
|
|
2700
|
-
const selection =
|
|
3016
|
+
const selection = selectionForCapability(settings, "advisor");
|
|
3017
|
+
const backend = selection.backend ?? "agy";
|
|
2701
3018
|
const snapshot = buildAdvisorSnapshot(ctx, pi);
|
|
2702
3019
|
const prompt = buildAdvisorPrompt(state.steering, snapshot.text, params.question);
|
|
2703
|
-
let lastAttempt: AdvisorToolDetails = { status: "investigating", attempt: 1, of: 3, elapsedMs: 0 };
|
|
3020
|
+
let lastAttempt: AdvisorToolDetails = { status: "investigating", attempt: 1, of: 3, elapsedMs: 0, backend };
|
|
2704
3021
|
try {
|
|
2705
3022
|
const run = await runAdvisorWithRetries(
|
|
2706
3023
|
prompt,
|
|
@@ -2710,7 +3027,7 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
2710
3027
|
undefined,
|
|
2711
3028
|
undefined,
|
|
2712
3029
|
(details) => {
|
|
2713
|
-
lastAttempt = details;
|
|
3030
|
+
lastAttempt = { ...details, backend };
|
|
2714
3031
|
const label = advisorAttemptLabel(lastAttempt);
|
|
2715
3032
|
onUpdate?.({
|
|
2716
3033
|
content: [{ type: "text", text: label }],
|
|
@@ -2731,6 +3048,7 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
2731
3048
|
...lastAttempt,
|
|
2732
3049
|
status: "done",
|
|
2733
3050
|
attempt: run.attempts,
|
|
3051
|
+
backend,
|
|
2734
3052
|
model: selection.model,
|
|
2735
3053
|
effort,
|
|
2736
3054
|
durationMs: run.durationMs,
|
|
@@ -2743,7 +3061,8 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
2743
3061
|
// the activity throttle window.
|
|
2744
3062
|
activityCount: run.activityCount,
|
|
2745
3063
|
};
|
|
2746
|
-
|
|
3064
|
+
// The backend qualifier appears only for Claude, keeping the Agy header byte-identical.
|
|
3065
|
+
const header = `Bro advisor · ${backend === "claude" ? "backend: claude · " : ""}model: ${selection.model} · effort: ${effort} · ${run.attempts} attempt${run.attempts === 1 ? "" : "s"} · ${Math.ceil(run.durationMs / 1_000)}s`;
|
|
2747
3066
|
const context = `Context · cwd: ${JSON.stringify(ctx.cwd)} · steering: ${details.steeringIncluded ? "included" : "none"} · snapshot: ${snapshot.text.length} chars · Bro truncation: none · omissions: ${omissions}`;
|
|
2748
3067
|
return { content: [{ type: "text", text: `${header}\n${context}\n\n${run.advice}` }], details };
|
|
2749
3068
|
} finally {
|
|
@@ -2950,6 +3269,45 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
2950
3269
|
}
|
|
2951
3270
|
try {
|
|
2952
3271
|
const settings = await readSettings();
|
|
3272
|
+
// /bro model operates on the effective shared backend: no Agy catalog
|
|
3273
|
+
// when the shared default is Claude.
|
|
3274
|
+
if ((settings.backend ?? "agy") === "claude") {
|
|
3275
|
+
const requested = value.trim();
|
|
3276
|
+
let model: string | undefined;
|
|
3277
|
+
if (requested) {
|
|
3278
|
+
try {
|
|
3279
|
+
model = resolveClaudeModel(requested);
|
|
3280
|
+
} catch {
|
|
3281
|
+
ctx.ui.notify("Use /bro model or /bro model <sonnet|opus|model-id>.", "warning");
|
|
3282
|
+
return;
|
|
3283
|
+
}
|
|
3284
|
+
} else {
|
|
3285
|
+
if (ctx.mode !== "tui") {
|
|
3286
|
+
ctx.ui.notify("Use /bro model <sonnet|opus|model-id> outside Pi's interactive UI.", "warning");
|
|
3287
|
+
return;
|
|
3288
|
+
}
|
|
3289
|
+
const choices = [
|
|
3290
|
+
...CLAUDE_MODELS.map(
|
|
3291
|
+
(entry) => `${entry.id} — ${entry.label}${entry.id === settings.model ? " (current)" : ""}`,
|
|
3292
|
+
),
|
|
3293
|
+
"Custom — enter a model ID…",
|
|
3294
|
+
];
|
|
3295
|
+
const choice = await ctx.ui.select(`Claude model (current: ${settings.model})`, choices);
|
|
3296
|
+
if (!choice) return;
|
|
3297
|
+
if (choice.startsWith("Custom")) {
|
|
3298
|
+
const input = await ctx.ui.input("Claude model", "sonnet, opus, or an explicit model ID");
|
|
3299
|
+
if (!input?.trim()) return;
|
|
3300
|
+
model = resolveClaudeModel(input);
|
|
3301
|
+
} else {
|
|
3302
|
+
model = CLAUDE_MODELS[choices.indexOf(choice)]?.id;
|
|
3303
|
+
}
|
|
3304
|
+
}
|
|
3305
|
+
if (!model) return;
|
|
3306
|
+
const effort = settings.backend === "claude" && isClaudeEffort(settings.effort) ? settings.effort : "default";
|
|
3307
|
+
await writeSettings({ ...settings, backend: "claude", model, effort });
|
|
3308
|
+
ctx.ui.notify(`Bro model: claude ${model}${effort === "default" ? "" : ` (${effort})`}`, "info");
|
|
3309
|
+
return;
|
|
3310
|
+
}
|
|
2953
3311
|
const models = await listAgyModels(pi);
|
|
2954
3312
|
const current = resolveCatalogSettings(settings, models);
|
|
2955
3313
|
const requested = parts[1];
|
|
@@ -2989,7 +3347,7 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
2989
3347
|
const currentEffort = current.settings.effort;
|
|
2990
3348
|
const canKeepCurrent =
|
|
2991
3349
|
current.family?.id === selected.id &&
|
|
2992
|
-
(currentEffort === "default" ? !selected.efforts.length : selected.efforts.includes(currentEffort));
|
|
3350
|
+
(currentEffort === "default" ? !selected.efforts.length : selected.efforts.includes(currentEffort as AgyEffort));
|
|
2993
3351
|
selectedEffort = canKeepCurrent ? currentEffort : preferredEffort(selected);
|
|
2994
3352
|
}
|
|
2995
3353
|
await writeSettings({ ...settings, model: selected.id, effort: selectedEffort });
|
|
@@ -3005,12 +3363,44 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
3005
3363
|
|
|
3006
3364
|
if (action === "effort") {
|
|
3007
3365
|
const requested = parts[1];
|
|
3008
|
-
|
|
3366
|
+
// Claude levels pass this gate; each backend branch below validates strictly
|
|
3367
|
+
// (the Agy branch still rejects xhigh/max with its supports-list warning).
|
|
3368
|
+
if (parts.length > 2 || (requested && !isClaudeEffort(requested) && !EFFORTS.some((effort) => effort === requested))) {
|
|
3009
3369
|
ctx.ui.notify("Use /bro effort, or choose low, medium, or high.", "warning");
|
|
3010
3370
|
return;
|
|
3011
3371
|
}
|
|
3012
3372
|
try {
|
|
3013
3373
|
const settings = await readSettings();
|
|
3374
|
+
// /bro effort operates on the effective shared backend: no Agy catalog
|
|
3375
|
+
// when the shared default is Claude.
|
|
3376
|
+
if ((settings.backend ?? "agy") === "claude") {
|
|
3377
|
+
const allowed = ["default", ...CLAUDE_EFFORTS] as const;
|
|
3378
|
+
if (requested && !allowed.some((effort) => effort === requested)) {
|
|
3379
|
+
ctx.ui.notify("Use /bro effort, or choose default, low, medium, high, xhigh, or max.", "warning");
|
|
3380
|
+
return;
|
|
3381
|
+
}
|
|
3382
|
+
let selected = requested as BroEffort | undefined;
|
|
3383
|
+
if (!selected) {
|
|
3384
|
+
if (ctx.mode !== "tui") {
|
|
3385
|
+
ctx.ui.notify("Use /bro effort <default|low|medium|high|xhigh|max> outside Pi's interactive UI.", "warning");
|
|
3386
|
+
return;
|
|
3387
|
+
}
|
|
3388
|
+
const efforts = [...CLAUDE_EFFORTS].sort(
|
|
3389
|
+
(a, b) => Number(b === settings.effort) - Number(a === settings.effort),
|
|
3390
|
+
);
|
|
3391
|
+
const choices = efforts.map((effort) => `${effort}${effort === settings.effort ? " (current)" : ""}`);
|
|
3392
|
+
const choice = await ctx.ui.select(`Claude reasoning effort (current: ${settings.effort})`, choices);
|
|
3393
|
+
if (!choice) return;
|
|
3394
|
+
selected = efforts[choices.indexOf(choice)];
|
|
3395
|
+
}
|
|
3396
|
+
if (!selected || !isClaudeEffort(selected)) return;
|
|
3397
|
+
await writeSettings({ ...settings, backend: "claude", model: settings.model, effort: selected });
|
|
3398
|
+
ctx.ui.notify(
|
|
3399
|
+
selected === "default" ? "Bro reasoning effort: built into the selected model" : `Bro reasoning effort: ${selected}`,
|
|
3400
|
+
"info",
|
|
3401
|
+
);
|
|
3402
|
+
return;
|
|
3403
|
+
}
|
|
3014
3404
|
const current = resolveCatalogSettings(settings, await listAgyModels(pi));
|
|
3015
3405
|
if (!current.family) {
|
|
3016
3406
|
ctx.ui.notify(`Model "${settings.model}" is not in Agy's current model list. Run /bro model first.`, "warning");
|
|
@@ -3065,6 +3455,7 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
3065
3455
|
return;
|
|
3066
3456
|
}
|
|
3067
3457
|
try {
|
|
3458
|
+
if (capabilityBackend(await readSettings(), "btw") === "claude") throw new Error("Claude does not support /bro btw yet; choose an Agy override in /bro config.");
|
|
3068
3459
|
const thread = resolveBtwThread(btwThread, parsed);
|
|
3069
3460
|
btwThread = thread;
|
|
3070
3461
|
await openBtwModal(ctx, { thread, initialQuestion: parsed.question, seed: !parsed.fresh });
|