pi-bro 0.18.1 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +22 -8
- package/backend.ts +485 -5
- package/bro.ts +371 -41
- package/docs/README.md +8 -0
- package/docs/pig-compatibility.md +42 -0
- package/package.json +7 -4
package/bro.ts
CHANGED
|
@@ -5,12 +5,12 @@ import { mkdir, mkdtemp, readdir, readFile, realpath, rm, stat, writeFile } from
|
|
|
5
5
|
import { request as httpRequest, type IncomingMessage } from "node:http";
|
|
6
6
|
import { request as httpsRequest } from "node:https";
|
|
7
7
|
import { BlockList, isIP } from "node:net";
|
|
8
|
-
import {
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
9
|
import { extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
10
10
|
import { stripVTControlCharacters } from "node:util";
|
|
11
11
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext, SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import { Container, Editor, Input, Markdown, SettingsList, SelectList, Text, matchesKey, truncateToWidth, visibleWidth, type Component, type EditorTheme, type Focusable, type SelectItem, type SettingItem, type TUI } from "@earendil-works/pi-tui";
|
|
13
|
-
import { convertToLlm, copyToClipboard, getMarkdownTheme, getSelectListTheme, getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import { convertToLlm, copyToClipboard, getAgentDir, getMarkdownTheme, getSelectListTheme, getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
14
14
|
import { Defuddle } from "defuddle/node";
|
|
15
15
|
import { parseHTML } from "linkedom";
|
|
16
16
|
import mammoth from "mammoth";
|
|
@@ -23,6 +23,8 @@ import {
|
|
|
23
23
|
advisorFlagErrorHint,
|
|
24
24
|
CLAUDE_EFFORTS,
|
|
25
25
|
GROK_EFFORTS,
|
|
26
|
+
CODEX_EFFORTS,
|
|
27
|
+
MUSE_EFFORTS,
|
|
26
28
|
backendSupports,
|
|
27
29
|
execute as executeBackend,
|
|
28
30
|
parseBtwAgyLine,
|
|
@@ -33,7 +35,7 @@ import {
|
|
|
33
35
|
|
|
34
36
|
export { agyFailureMessage, agySelection, advisorFlagErrorHint, parseBtwAgyLine };
|
|
35
37
|
|
|
36
|
-
const AGENT_DIR =
|
|
38
|
+
const AGENT_DIR = getAgentDir();
|
|
37
39
|
const ENV_MODEL = process.env.PI_BRO_MODEL?.trim();
|
|
38
40
|
const DEFAULT_MODEL = ENV_MODEL || "gemini-3.7-flash";
|
|
39
41
|
const PROMPT_FILE = join(AGENT_DIR, "bro-prompt.md");
|
|
@@ -55,7 +57,7 @@ const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
|
55
57
|
type Theme = ExtensionCommandContext["ui"]["theme"];
|
|
56
58
|
type TuiLike = {
|
|
57
59
|
readonly mode: "regular" | "fullscreen";
|
|
58
|
-
readonly terminal?: { write?: (data: string) => void };
|
|
60
|
+
readonly terminal?: { rows?: number; write?: (data: string) => void };
|
|
59
61
|
requestRender(): void;
|
|
60
62
|
};
|
|
61
63
|
type ModalKind = "loading" | "streaming" | "result" | "help" | "empty" | "error";
|
|
@@ -67,13 +69,15 @@ type BtwTurn = { question: string; answer: string };
|
|
|
67
69
|
// thread; `sessionFull` is the access mode the native session last ran in.
|
|
68
70
|
type BtwThread = { turns: BtwTurn[]; conversationId?: string; full: boolean; backend?: BackendName; model?: string; context?: string; sessionFull?: boolean };
|
|
69
71
|
const EFFORTS = ["default", "low", "medium", "high"] as const;
|
|
70
|
-
const BACKENDS = ["agy", "claude", "grok"] as const;
|
|
72
|
+
const BACKENDS = ["agy", "claude", "grok", "codex", "muse"] as const;
|
|
71
73
|
type BackendName = (typeof BACKENDS)[number];
|
|
72
|
-
type BroEffort = "default" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
73
|
-
type AgyEffort = Exclude<BroEffort, "default" | "xhigh" | "max">;
|
|
74
|
+
type BroEffort = "default" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
75
|
+
type AgyEffort = Exclude<BroEffort, "default" | "minimal" | "xhigh" | "max">;
|
|
74
76
|
type ClaudeEffort = (typeof CLAUDE_EFFORTS)[number];
|
|
75
|
-
export { GROK_EFFORTS };
|
|
77
|
+
export { GROK_EFFORTS, CODEX_EFFORTS, MUSE_EFFORTS };
|
|
76
78
|
type GrokEffort = (typeof GROK_EFFORTS)[number];
|
|
79
|
+
type CodexEffort = (typeof CODEX_EFFORTS)[number];
|
|
80
|
+
type MuseEffort = (typeof MUSE_EFFORTS)[number];
|
|
77
81
|
// "advisor" is a real capability (command, Agy invocation, Doctor check) like the other three;
|
|
78
82
|
// see docs/plans/2026-09-19-bro-advisor-design.md. All four share one model/effort resolution,
|
|
79
83
|
// override, and Doctor-check path via this single list -- there is no configuration-only tier.
|
|
@@ -99,6 +103,14 @@ export const GROK_MODELS = [
|
|
|
99
103
|
{ id: "grok-4.7", label: "Grok 4.7" },
|
|
100
104
|
{ id: "grok-4.7-build-fast", label: "Grok 4.7 Build Fast" },
|
|
101
105
|
] as const;
|
|
106
|
+
export const CODEX_MODELS = [
|
|
107
|
+
{ id: "gpt-5.5", label: "GPT-5.5" },
|
|
108
|
+
{ id: "gpt-5.4", label: "GPT-5.4" },
|
|
109
|
+
] as const;
|
|
110
|
+
export const MUSE_MODELS = [
|
|
111
|
+
{ id: "muse-spark-1.3-contributor", label: "Muse Spark 1.3 Contributor" },
|
|
112
|
+
{ id: "muse-spark-1.3", label: "Muse Spark 1.3" },
|
|
113
|
+
] as const;
|
|
102
114
|
export { backendSupports as supportsBackend };
|
|
103
115
|
type AgyModelFamily = {
|
|
104
116
|
id: string;
|
|
@@ -508,7 +520,7 @@ export async function extractWebPage(input: string, signal?: AbortSignal): Promi
|
|
|
508
520
|
|
|
509
521
|
function parseBackend(value: unknown, context: string): BackendName {
|
|
510
522
|
if (typeof value !== "string" || !BACKENDS.some((backend) => backend === value)) {
|
|
511
|
-
throw new Error(`${context} backend must be "agy", "claude", or "
|
|
523
|
+
throw new Error(`${context} backend must be "agy", "claude", "grok", "codex", or "muse".`);
|
|
512
524
|
}
|
|
513
525
|
return value as BackendName;
|
|
514
526
|
}
|
|
@@ -525,6 +537,14 @@ function isGrokEffort(effort: unknown): effort is GrokEffort | "default" {
|
|
|
525
537
|
return effort === "default" || GROK_EFFORTS.some((item) => item === effort);
|
|
526
538
|
}
|
|
527
539
|
|
|
540
|
+
function isCodexEffort(effort: unknown): effort is CodexEffort | "default" {
|
|
541
|
+
return effort === "default" || CODEX_EFFORTS.some((item) => item === effort);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function isMuseEffort(effort: unknown): effort is MuseEffort | "default" {
|
|
545
|
+
return effort === "default" || MUSE_EFFORTS.some((item) => item === effort);
|
|
546
|
+
}
|
|
547
|
+
|
|
528
548
|
function parseModelEffortPair(value: unknown, context: string): ModelEffortPair {
|
|
529
549
|
if (!isRecord(value) || typeof value.model !== "string" || !value.model.trim()) {
|
|
530
550
|
throw new Error(`${context} must contain a model and effort set to "default", "low", "medium", or "high".`);
|
|
@@ -542,6 +562,18 @@ function parseModelEffortPair(value: unknown, context: string): ModelEffortPair
|
|
|
542
562
|
}
|
|
543
563
|
return { backend, model: value.model.trim(), effort: value.effort };
|
|
544
564
|
}
|
|
565
|
+
if (backend === "codex") {
|
|
566
|
+
if (!isCodexEffort(value.effort)) {
|
|
567
|
+
throw new Error(`${context} must contain a model and effort set to "default", "low", "medium", "high", or "xhigh".`);
|
|
568
|
+
}
|
|
569
|
+
return { backend, model: value.model.trim(), effort: value.effort };
|
|
570
|
+
}
|
|
571
|
+
if (backend === "muse") {
|
|
572
|
+
if (!isMuseEffort(value.effort)) {
|
|
573
|
+
throw new Error(`${context} must contain a model and effort set to "default", "minimal", "low", "medium", "high", "xhigh", or "max".`);
|
|
574
|
+
}
|
|
575
|
+
return { backend, model: value.model.trim(), effort: value.effort };
|
|
576
|
+
}
|
|
545
577
|
if (!isAgyEffort(value.effort)) {
|
|
546
578
|
throw new Error(`${context} must contain a model and effort set to "default", "low", "medium", or "high".`);
|
|
547
579
|
}
|
|
@@ -590,14 +622,25 @@ export function parseBroSettings(value: unknown): BroSettings {
|
|
|
590
622
|
throw new Error('Settings must contain a model and effort set to "default", "low", "medium", or "high".');
|
|
591
623
|
}
|
|
592
624
|
const backend = value.backend === undefined ? undefined : parseBackend(value.backend, "Settings");
|
|
593
|
-
const effortOk =
|
|
625
|
+
const effortOk =
|
|
626
|
+
backend === "claude"
|
|
627
|
+
? isClaudeEffort(value.effort)
|
|
628
|
+
: backend === "grok"
|
|
629
|
+
? isGrokEffort(value.effort)
|
|
630
|
+
: backend === "codex"
|
|
631
|
+
? isCodexEffort(value.effort)
|
|
632
|
+
: backend === "muse"
|
|
633
|
+
? isMuseEffort(value.effort)
|
|
634
|
+
: isAgyEffort(value.effort);
|
|
594
635
|
if (!effortOk) {
|
|
595
636
|
throw new Error(
|
|
596
637
|
backend === "claude"
|
|
597
638
|
? 'Settings must contain a model and effort set to "default", "low", "medium", "high", "xhigh", or "max".'
|
|
598
|
-
: backend === "grok"
|
|
639
|
+
: backend === "grok" || backend === "codex"
|
|
599
640
|
? 'Settings must contain a model and effort set to "default", "low", "medium", "high", or "xhigh".'
|
|
600
|
-
:
|
|
641
|
+
: backend === "muse"
|
|
642
|
+
? 'Settings must contain a model and effort set to "default", "minimal", "low", "medium", "high", "xhigh", or "max".'
|
|
643
|
+
: 'Settings must contain a model and effort set to "default", "low", "medium", or "high".',
|
|
601
644
|
);
|
|
602
645
|
}
|
|
603
646
|
const mode = value.mode === undefined ? DEFAULT_BRO_MODE : parseBroMode(value.mode);
|
|
@@ -649,8 +692,8 @@ export function resolveModelEffort(
|
|
|
649
692
|
pair: ModelEffortPair,
|
|
650
693
|
families: AgyModelFamily[],
|
|
651
694
|
): { pair: ModelEffortPair; family?: AgyModelFamily } {
|
|
652
|
-
// Claude/Grok selections never resolve through the Agy catalog; they pass through untouched.
|
|
653
|
-
if (pair.backend === "claude" || pair.backend === "grok") return { pair };
|
|
695
|
+
// Claude/Grok/Codex/Muse selections never resolve through the Agy catalog; they pass through untouched.
|
|
696
|
+
if (pair.backend === "claude" || pair.backend === "grok" || pair.backend === "codex" || pair.backend === "muse") return { pair };
|
|
654
697
|
const family = families.find((item) => item.id === pair.model || item.variants.some((variant) => variant.id === pair.model));
|
|
655
698
|
if (!family) return { pair };
|
|
656
699
|
const variant = family.variants.find((item) => item.id === pair.model);
|
|
@@ -688,17 +731,41 @@ export function resolveGrokModel(input: string): string {
|
|
|
688
731
|
return trimmed;
|
|
689
732
|
}
|
|
690
733
|
|
|
734
|
+
// Codex seeds resolve to themselves; any other non-empty string passes through
|
|
735
|
+
// untouched as an explicit user-entered model ID.
|
|
736
|
+
export function resolveCodexModel(input: string): string {
|
|
737
|
+
const trimmed = input.trim();
|
|
738
|
+
if (!trimmed) throw new Error("Codex model must be a non-empty model ID (gpt-5.5, gpt-5.4, or an explicit model ID).");
|
|
739
|
+
return trimmed;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// Muse seeds resolve to themselves; any other non-empty string passes through
|
|
743
|
+
// untouched as an explicit user-entered model ID.
|
|
744
|
+
export function resolveMuseModel(input: string): string {
|
|
745
|
+
const trimmed = input.trim();
|
|
746
|
+
if (!trimmed) throw new Error("Muse model must be a non-empty model ID (muse-spark-1.3-contributor, muse-spark-1.3, or an explicit model ID).");
|
|
747
|
+
return trimmed;
|
|
748
|
+
}
|
|
749
|
+
|
|
691
750
|
// Routes one capability's whole pair to its backend execution selection. Agy keeps the
|
|
692
|
-
// existing model/effort split; Claude/Grok carry the model plus an optional effort
|
|
751
|
+
// existing model/effort split; Claude/Grok/Codex/Muse carry the model plus an optional effort
|
|
693
752
|
// ("default" means the CLI's own default and is omitted). Throws for an effort the
|
|
694
753
|
// resolved backend does not support instead of silently sending a mismatched pair.
|
|
695
754
|
export function selectionForCapability(settings: BroSettings, capability: Capability): BackendSelection {
|
|
696
755
|
const pair = capabilityPair(settings, capability);
|
|
697
756
|
const backend = capabilityBackend(settings, capability);
|
|
698
|
-
if (backend === "claude" || backend === "grok") {
|
|
699
|
-
const valid =
|
|
757
|
+
if (backend === "claude" || backend === "grok" || backend === "codex" || backend === "muse") {
|
|
758
|
+
const valid =
|
|
759
|
+
backend === "claude"
|
|
760
|
+
? isClaudeEffort(pair.effort)
|
|
761
|
+
: backend === "grok"
|
|
762
|
+
? isGrokEffort(pair.effort)
|
|
763
|
+
: backend === "codex"
|
|
764
|
+
? isCodexEffort(pair.effort)
|
|
765
|
+
: isMuseEffort(pair.effort);
|
|
700
766
|
if (!valid) {
|
|
701
|
-
|
|
767
|
+
const label = backend === "claude" ? "Claude" : backend === "grok" ? "Grok" : backend === "codex" ? "Codex" : "Muse";
|
|
768
|
+
throw new Error(`\`${pair.effort}\` is not supported on the ${label} backend. Run \`/bro config\` to fix this.`);
|
|
702
769
|
}
|
|
703
770
|
return (
|
|
704
771
|
pair.effort === "default"
|
|
@@ -970,6 +1037,67 @@ async function checkGrokVersion(pi: ExtensionAPI, signal: AbortSignal): Promise<
|
|
|
970
1037
|
}
|
|
971
1038
|
}
|
|
972
1039
|
|
|
1040
|
+
async function checkCodexVersion(pi: ExtensionAPI, signal: AbortSignal): Promise<string> {
|
|
1041
|
+
const runDirectory = await mkdtemp(join(tmpdir(), "pi-bro-"));
|
|
1042
|
+
try {
|
|
1043
|
+
const result = await pi.exec("codex", ["--version"], { cwd: runDirectory, signal, timeout: 10_000 });
|
|
1044
|
+
if (signal.aborted) throw new Error("Canceled.");
|
|
1045
|
+
if (result.killed || result.code !== 0) {
|
|
1046
|
+
const detail = result.stderr.trim() || result.stdout.trim();
|
|
1047
|
+
throw new Error(
|
|
1048
|
+
detail
|
|
1049
|
+
? `Codex could not start: ${detail}\n\nRun \`/bro doctor\` for setup help.`
|
|
1050
|
+
: "Codex could not start. Make sure Codex is installed and on PATH, then run `/bro doctor`.",
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
const version = result.stdout.trim() || result.stderr.trim();
|
|
1054
|
+
if (!version) throw new Error("Codex returned no version information. Update Codex, then run `/bro doctor` again.");
|
|
1055
|
+
return version;
|
|
1056
|
+
} finally {
|
|
1057
|
+
await rm(runDirectory, { recursive: true, force: true });
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// Auth status only: a version/auth answer without a model request. A passing answer here
|
|
1062
|
+
// says the CLI starts and reports signed-in state -- it does not imply connectivity.
|
|
1063
|
+
async function checkCodexAuth(pi: ExtensionAPI, signal: AbortSignal): Promise<string> {
|
|
1064
|
+
const runDirectory = await mkdtemp(join(tmpdir(), "pi-bro-"));
|
|
1065
|
+
try {
|
|
1066
|
+
const result = await pi.exec("codex", ["login", "status"], { cwd: runDirectory, signal, timeout: 15_000 });
|
|
1067
|
+
if (signal.aborted) throw new Error("Canceled.");
|
|
1068
|
+
if (result.killed || result.code !== 0) {
|
|
1069
|
+
const detail = result.stderr.trim() || result.stdout.trim();
|
|
1070
|
+
throw new Error(detail ? `Codex auth status: ${detail}` : "Codex is not logged in. Run `codex login`.");
|
|
1071
|
+
}
|
|
1072
|
+
const status = result.stdout.trim() || result.stderr.trim();
|
|
1073
|
+
if (!status || !/logged in/i.test(status)) throw new Error("Codex is not logged in. Run `codex login`.");
|
|
1074
|
+
return "authentication configured (not a connectivity test)";
|
|
1075
|
+
} finally {
|
|
1076
|
+
await rm(runDirectory, { recursive: true, force: true });
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
async function checkMuseVersion(pi: ExtensionAPI, signal: AbortSignal): Promise<string> {
|
|
1081
|
+
const runDirectory = await mkdtemp(join(tmpdir(), "pi-bro-"));
|
|
1082
|
+
try {
|
|
1083
|
+
const result = await pi.exec("muse", ["--version"], { cwd: runDirectory, signal, timeout: 10_000 });
|
|
1084
|
+
if (signal.aborted) throw new Error("Canceled.");
|
|
1085
|
+
if (result.killed || result.code !== 0) {
|
|
1086
|
+
const detail = result.stderr.trim() || result.stdout.trim();
|
|
1087
|
+
throw new Error(
|
|
1088
|
+
detail
|
|
1089
|
+
? `Muse could not start: ${detail}\n\nRun \`/bro doctor\` for setup help.`
|
|
1090
|
+
: "Muse could not start. Make sure Muse is installed and on PATH, then run `/bro doctor`.",
|
|
1091
|
+
);
|
|
1092
|
+
}
|
|
1093
|
+
const version = result.stdout.trim() || result.stderr.trim();
|
|
1094
|
+
if (!version) throw new Error("Muse returned no version information. Update Muse, then run `/bro doctor` again.");
|
|
1095
|
+
return version;
|
|
1096
|
+
} finally {
|
|
1097
|
+
await rm(runDirectory, { recursive: true, force: true });
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
973
1101
|
async function checkAgyUsage(pi: ExtensionAPI, signal: AbortSignal): Promise<string> {
|
|
974
1102
|
const runDirectory = await mkdtemp(join(tmpdir(), "pi-bro-"));
|
|
975
1103
|
try {
|
|
@@ -1016,11 +1144,13 @@ async function doctorReport(pi: ExtensionAPI, ctx: ExtensionCommandContext, sign
|
|
|
1016
1144
|
fail("Prompt", error);
|
|
1017
1145
|
}
|
|
1018
1146
|
|
|
1019
|
-
// Probe only the backends some feature actually selects: a Claude/Grok-only setup never
|
|
1147
|
+
// Probe only the backends some feature actually selects: a Claude/Grok/Codex/Muse-only setup never
|
|
1020
1148
|
// requires Agy to be installed, and vice versa.
|
|
1021
1149
|
const agyInUse = !settings || capabilityBackend(settings, "explain") === "agy" || capabilityBackend(settings, "show") === "agy" || capabilityBackend(settings, "btw") === "agy" || capabilityBackend(settings, "advisor") === "agy" || (settings.backend ?? "agy") === "agy";
|
|
1022
1150
|
const claudeInUse = !!settings && (capabilityBackend(settings, "explain") === "claude" || capabilityBackend(settings, "show") === "claude" || capabilityBackend(settings, "btw") === "claude" || capabilityBackend(settings, "advisor") === "claude" || settings.backend === "claude");
|
|
1023
1151
|
const grokInUse = !!settings && (capabilityBackend(settings, "explain") === "grok" || capabilityBackend(settings, "show") === "grok" || capabilityBackend(settings, "btw") === "grok" || capabilityBackend(settings, "advisor") === "grok" || settings.backend === "grok");
|
|
1152
|
+
const codexInUse = !!settings && (capabilityBackend(settings, "explain") === "codex" || capabilityBackend(settings, "show") === "codex" || capabilityBackend(settings, "btw") === "codex" || capabilityBackend(settings, "advisor") === "codex" || settings.backend === "codex");
|
|
1153
|
+
const museInUse = !!settings && (capabilityBackend(settings, "explain") === "muse" || capabilityBackend(settings, "show") === "muse" || capabilityBackend(settings, "btw") === "muse" || capabilityBackend(settings, "advisor") === "muse" || settings.backend === "muse");
|
|
1024
1154
|
|
|
1025
1155
|
let agyStarted = false;
|
|
1026
1156
|
if (agyInUse) {
|
|
@@ -1087,6 +1217,38 @@ async function doctorReport(pi: ExtensionAPI, ctx: ExtensionCommandContext, sign
|
|
|
1087
1217
|
pass("Grok", "not probed — no feature selects the Grok backend");
|
|
1088
1218
|
}
|
|
1089
1219
|
|
|
1220
|
+
// Version and auth status only, never a model request.
|
|
1221
|
+
if (codexInUse) {
|
|
1222
|
+
try {
|
|
1223
|
+
pass("Codex", await checkCodexVersion(pi, signal));
|
|
1224
|
+
} catch (error) {
|
|
1225
|
+
if (signal.aborted) throw error;
|
|
1226
|
+
fail("Codex", error);
|
|
1227
|
+
}
|
|
1228
|
+
try {
|
|
1229
|
+
pass("Codex auth", await checkCodexAuth(pi, signal));
|
|
1230
|
+
} catch (error) {
|
|
1231
|
+
if (signal.aborted) throw error;
|
|
1232
|
+
fail("Codex auth", error);
|
|
1233
|
+
}
|
|
1234
|
+
} else {
|
|
1235
|
+
pass("Codex", "not probed — no feature selects the Codex backend");
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
// Version only, never a model request. No Muse auth probe exists, so a passing
|
|
1239
|
+
// version says the CLI starts — it says nothing about auth or connectivity.
|
|
1240
|
+
if (museInUse) {
|
|
1241
|
+
try {
|
|
1242
|
+
pass("Muse", await checkMuseVersion(pi, signal));
|
|
1243
|
+
} catch (error) {
|
|
1244
|
+
if (signal.aborted) throw error;
|
|
1245
|
+
fail("Muse", error);
|
|
1246
|
+
}
|
|
1247
|
+
pass("Muse auth", "unverified — no auth probe exists (version does not imply auth or connectivity)");
|
|
1248
|
+
} else {
|
|
1249
|
+
pass("Muse", "not probed — no feature selects the Muse backend");
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1090
1252
|
if (settings) {
|
|
1091
1253
|
const defaultBackend = settings.backend ?? "agy";
|
|
1092
1254
|
if (defaultBackend === "claude") {
|
|
@@ -1107,6 +1269,24 @@ async function doctorReport(pi: ExtensionAPI, ctx: ExtensionCommandContext, sign
|
|
|
1107
1269
|
} else {
|
|
1108
1270
|
pass("Reasoning effort", settings.effort);
|
|
1109
1271
|
}
|
|
1272
|
+
} else if (defaultBackend === "codex") {
|
|
1273
|
+
pass("Selected model", `codex \`${settings.model}\``);
|
|
1274
|
+
if (!isCodexEffort(settings.effort)) {
|
|
1275
|
+
fail("Reasoning effort", `\`${settings.effort}\` is unsupported. Run \`/bro config\` to choose another.`);
|
|
1276
|
+
} else if (settings.effort === "default") {
|
|
1277
|
+
pass("Reasoning effort", "built into the selected model");
|
|
1278
|
+
} else {
|
|
1279
|
+
pass("Reasoning effort", settings.effort);
|
|
1280
|
+
}
|
|
1281
|
+
} else if (defaultBackend === "muse") {
|
|
1282
|
+
pass("Selected model", `muse \`${settings.model}\``);
|
|
1283
|
+
if (!isMuseEffort(settings.effort)) {
|
|
1284
|
+
fail("Reasoning effort", `\`${settings.effort}\` is unsupported. Run \`/bro config\` to choose another.`);
|
|
1285
|
+
} else if (settings.effort === "default") {
|
|
1286
|
+
pass("Reasoning effort", "built into the selected model");
|
|
1287
|
+
} else {
|
|
1288
|
+
pass("Reasoning effort", settings.effort);
|
|
1289
|
+
}
|
|
1110
1290
|
} else if (models) {
|
|
1111
1291
|
const current = resolveCatalogSettings(settings, models);
|
|
1112
1292
|
if (!current.family) {
|
|
@@ -1159,6 +1339,40 @@ async function doctorReport(pi: ExtensionAPI, ctx: ExtensionCommandContext, sign
|
|
|
1159
1339
|
);
|
|
1160
1340
|
continue;
|
|
1161
1341
|
}
|
|
1342
|
+
if (backend === "codex") {
|
|
1343
|
+
if (!backendSupports("codex", capability)) {
|
|
1344
|
+
fail(label, `\`${capability}\` is not supported on the Codex backend for this capability. Run \`/bro config\` to give it another backend.`);
|
|
1345
|
+
continue;
|
|
1346
|
+
}
|
|
1347
|
+
if (!isCodexEffort(pair.effort)) {
|
|
1348
|
+
fail(label, `\`${pair.effort}\` is unsupported for codex \`${pair.model}\`. Run \`/bro config\` to fix this.`);
|
|
1349
|
+
continue;
|
|
1350
|
+
}
|
|
1351
|
+
pass(
|
|
1352
|
+
label,
|
|
1353
|
+
override
|
|
1354
|
+
? `override codex \`${pair.model}\`${pair.effort === "default" ? "" : ` (${pair.effort})`}`
|
|
1355
|
+
: `codex \`${pair.model}\`${pair.effort === "default" ? "" : ` (${pair.effort})`} · using the shared default`,
|
|
1356
|
+
);
|
|
1357
|
+
continue;
|
|
1358
|
+
}
|
|
1359
|
+
if (backend === "muse") {
|
|
1360
|
+
if (!backendSupports("muse", capability)) {
|
|
1361
|
+
fail(label, `\`${capability}\` is not supported on the Muse backend for this capability. Run \`/bro config\` to give it another backend.`);
|
|
1362
|
+
continue;
|
|
1363
|
+
}
|
|
1364
|
+
if (!isMuseEffort(pair.effort)) {
|
|
1365
|
+
fail(label, `\`${pair.effort}\` is unsupported for muse \`${pair.model}\`. Run \`/bro config\` to fix this.`);
|
|
1366
|
+
continue;
|
|
1367
|
+
}
|
|
1368
|
+
pass(
|
|
1369
|
+
label,
|
|
1370
|
+
override
|
|
1371
|
+
? `override muse \`${pair.model}\`${pair.effort === "default" ? "" : ` (${pair.effort})`}`
|
|
1372
|
+
: `muse \`${pair.model}\`${pair.effort === "default" ? "" : ` (${pair.effort})`} · using the shared default`,
|
|
1373
|
+
);
|
|
1374
|
+
continue;
|
|
1375
|
+
}
|
|
1162
1376
|
if (!models) continue;
|
|
1163
1377
|
const resolved = resolveCapabilitySettings(settings, capability, models);
|
|
1164
1378
|
if (!resolved.family) {
|
|
@@ -1193,13 +1407,17 @@ async function doctorReport(pi: ExtensionAPI, ctx: ExtensionCommandContext, sign
|
|
|
1193
1407
|
pass("Advisor compatibility", "Claude backend — no Agy version floor applies");
|
|
1194
1408
|
} else if (settings && capabilityBackend(settings, "advisor") === "grok") {
|
|
1195
1409
|
pass("Advisor compatibility", "Grok backend — no Agy version floor applies; auth/connectivity unverified (no probe)");
|
|
1410
|
+
} else if (settings && capabilityBackend(settings, "advisor") === "codex") {
|
|
1411
|
+
pass("Advisor compatibility", "Codex backend — no Agy version floor applies");
|
|
1412
|
+
} else if (settings && capabilityBackend(settings, "advisor") === "muse") {
|
|
1413
|
+
pass("Advisor compatibility", "Muse backend — no Agy version floor applies; auth/connectivity unverified (no probe)");
|
|
1196
1414
|
} else if (agyVersion && advisorAgyCompatible(agyVersion)) pass("Advisor compatibility", ADVISOR_COMPATIBILITY);
|
|
1197
1415
|
else if (agyVersion) fail("Advisor compatibility", `installed \`${agyVersion}\`; requires Agy >=1.1.15. Run \`agy update\`.`);
|
|
1198
1416
|
|
|
1199
1417
|
return `# Bro doctor\n\n${lines.join("\n")}\n\n**${failed ? "Bro needs attention." : "Bro is ready."}**\n\n${
|
|
1200
1418
|
failed
|
|
1201
1419
|
? "Fix the failed items, then press **R** to check again."
|
|
1202
|
-
: "No assistant response was sent and no model turn was run. Version/auth checks do not imply connectivity (Grok auth
|
|
1420
|
+
: "No assistant response was sent and no model turn was run. Version/auth checks do not imply connectivity (Grok and Muse auth are unverified — no probe exists)."
|
|
1203
1421
|
}`;
|
|
1204
1422
|
}
|
|
1205
1423
|
|
|
@@ -1343,10 +1561,12 @@ export function showHtmlDirectory(): string {
|
|
|
1343
1561
|
|
|
1344
1562
|
function withShowCsp(html: string): string {
|
|
1345
1563
|
// Defense in depth: the prompt forbids external resources and scripts;
|
|
1346
|
-
// a meta CSP blocks them anyway if the model slips.
|
|
1564
|
+
// a meta CSP blocks them anyway if the model slips. Always inserted: multiple
|
|
1565
|
+
// policies are enforced together, so a model-supplied CSP can only tighten it.
|
|
1566
|
+
// Skip only simple leading comments: browsers accept comment terminators that
|
|
1567
|
+
// a general regex misses. Ambiguous markup gets the policy prepended instead.
|
|
1347
1568
|
const meta = '<meta http-equiv="Content-Security-Policy" content="default-src \'none\'; style-src \'unsafe-inline\'; img-src data:;">';
|
|
1348
|
-
|
|
1349
|
-
return html.replace(/^(\s*(?:<!doctype[^>]*>\s*)?)/i, `$1\n${meta}\n`);
|
|
1569
|
+
return html.replace(/^(\s*(?:<!--[^<>]*-->\s*)*(?:<!doctype[^>]*>\s*)?)/i, `$1\n${meta}\n`);
|
|
1350
1570
|
}
|
|
1351
1571
|
|
|
1352
1572
|
export async function writeShowHtml(html: string): Promise<string> {
|
|
@@ -1787,7 +2007,7 @@ export function advisorAttemptLabel(details: AdvisorToolDetails): string {
|
|
|
1787
2007
|
return `Bro advisor · ${details.model ?? "unknown model"} · ${details.attempt} attempt${details.attempt === 1 ? "" : "s"} · ${Math.ceil((details.durationMs ?? details.elapsedMs) / 1_000)}s`;
|
|
1788
2008
|
}
|
|
1789
2009
|
const latest = details.activity?.at(-1);
|
|
1790
|
-
const backendLabel = details.backend === "claude" ? "Claude" : details.backend === "grok" ? "Grok" : "Agy";
|
|
2010
|
+
const backendLabel = details.backend === "claude" ? "Claude" : details.backend === "grok" ? "Grok" : details.backend === "codex" ? "Codex" : details.backend === "muse" ? "Muse" : "Agy";
|
|
1791
2011
|
// "last reported" + freshness, never a claim about what the backend is doing right now and never
|
|
1792
2012
|
// "stalled" -- silence since lastActivityAt is not itself evidence of a stuck run.
|
|
1793
2013
|
const activityLabel = latest
|
|
@@ -1807,10 +2027,12 @@ function showTurnsValues(current: number): string[] {
|
|
|
1807
2027
|
// falsely healthy for an unavailable model), and flags a stored effort that isn't one of the
|
|
1808
2028
|
// resolved family's supported efforts instead of silently showing it as if it were valid.
|
|
1809
2029
|
function effortDisplay(resolved: { pair: ModelEffortPair; family?: AgyModelFamily }, backend: BackendName): string {
|
|
1810
|
-
// Claude/Grok selections never touch the Agy catalog: the stored effort is valid exactly
|
|
2030
|
+
// Claude/Grok/Codex/Muse selections never touch the Agy catalog: the stored effort is valid exactly
|
|
1811
2031
|
// when it is one of that backend's levels (or "default" for the CLI's own default).
|
|
1812
2032
|
if (backend === "claude") return isClaudeEffort(resolved.pair.effort) ? resolved.pair.effort : `${resolved.pair.effort} (unsupported)`;
|
|
1813
2033
|
if (backend === "grok") return isGrokEffort(resolved.pair.effort) ? resolved.pair.effort : `${resolved.pair.effort} (unsupported)`;
|
|
2034
|
+
if (backend === "codex") return isCodexEffort(resolved.pair.effort) ? resolved.pair.effort : `${resolved.pair.effort} (unsupported)`;
|
|
2035
|
+
if (backend === "muse") return isMuseEffort(resolved.pair.effort) ? resolved.pair.effort : `${resolved.pair.effort} (unsupported)`;
|
|
1814
2036
|
if (!resolved.family) return "unavailable";
|
|
1815
2037
|
const fixed = !resolved.family.efforts.length;
|
|
1816
2038
|
const valid = fixed ? resolved.pair.effort === "default" : resolved.family.efforts.includes(resolved.pair.effort as AgyEffort);
|
|
@@ -1818,7 +2040,7 @@ function effortDisplay(resolved: { pair: ModelEffortPair; family?: AgyModelFamil
|
|
|
1818
2040
|
return fixed ? "fixed" : resolved.pair.effort;
|
|
1819
2041
|
}
|
|
1820
2042
|
|
|
1821
|
-
// Model-picker values that switch backend carry a "claude:"/"grok:" prefix so one
|
|
2043
|
+
// Model-picker values that switch backend carry a "claude:"/"grok:"/"codex:"/"muse:" prefix so one
|
|
1822
2044
|
// atomic picker commit changes backend+model together -- cancelling the picker (Esc)
|
|
1823
2045
|
// leaves both unchanged via the existing submenu-cancel path.
|
|
1824
2046
|
const CLAUDE_OPTION_PREFIX = "claude:";
|
|
@@ -1833,11 +2055,27 @@ function parseGrokOption(value: string): string | undefined {
|
|
|
1833
2055
|
? value.slice(GROK_OPTION_PREFIX.length)
|
|
1834
2056
|
: undefined;
|
|
1835
2057
|
}
|
|
1836
|
-
|
|
2058
|
+
const CODEX_OPTION_PREFIX = "codex:";
|
|
2059
|
+
function parseCodexOption(value: string): string | undefined {
|
|
2060
|
+
return value.startsWith(CODEX_OPTION_PREFIX) && value.length > CODEX_OPTION_PREFIX.length
|
|
2061
|
+
? value.slice(CODEX_OPTION_PREFIX.length)
|
|
2062
|
+
: undefined;
|
|
2063
|
+
}
|
|
2064
|
+
const MUSE_OPTION_PREFIX = "muse:";
|
|
2065
|
+
function parseMuseOption(value: string): string | undefined {
|
|
2066
|
+
return value.startsWith(MUSE_OPTION_PREFIX) && value.length > MUSE_OPTION_PREFIX.length
|
|
2067
|
+
? value.slice(MUSE_OPTION_PREFIX.length)
|
|
2068
|
+
: undefined;
|
|
2069
|
+
}
|
|
2070
|
+
function parseBackendOption(value: string): { backend: "claude" | "grok" | "codex" | "muse"; id: string } | undefined {
|
|
1837
2071
|
const claudeId = parseClaudeOption(value);
|
|
1838
2072
|
if (claudeId !== undefined) return { backend: "claude", id: claudeId };
|
|
1839
2073
|
const grokId = parseGrokOption(value);
|
|
1840
2074
|
if (grokId !== undefined) return { backend: "grok", id: grokId };
|
|
2075
|
+
const codexId = parseCodexOption(value);
|
|
2076
|
+
if (codexId !== undefined) return { backend: "codex", id: codexId };
|
|
2077
|
+
const museId = parseMuseOption(value);
|
|
2078
|
+
if (museId !== undefined) return { backend: "muse", id: museId };
|
|
1841
2079
|
return undefined;
|
|
1842
2080
|
}
|
|
1843
2081
|
|
|
@@ -1877,7 +2115,7 @@ export function createConfigModal(
|
|
|
1877
2115
|
value: family.id,
|
|
1878
2116
|
label: `${family.label}${family.efforts.length ? "" : " · fixed effort"}`,
|
|
1879
2117
|
})),
|
|
1880
|
-
//
|
|
2118
|
+
// Alternative CLI entries stay last so existing Agy keyboard navigation is unaffected.
|
|
1881
2119
|
...CLAUDE_MODELS.map((model) => ({
|
|
1882
2120
|
value: `${CLAUDE_OPTION_PREFIX}${model.id}`,
|
|
1883
2121
|
label: `${model.label} · claude`,
|
|
@@ -1886,13 +2124,33 @@ export function createConfigModal(
|
|
|
1886
2124
|
value: `${GROK_OPTION_PREFIX}${model.id}`,
|
|
1887
2125
|
label: `${model.label} · grok`,
|
|
1888
2126
|
})),
|
|
2127
|
+
...CODEX_MODELS.map((model) => ({
|
|
2128
|
+
value: `${CODEX_OPTION_PREFIX}${model.id}`,
|
|
2129
|
+
label: `${model.label} · codex`,
|
|
2130
|
+
})),
|
|
2131
|
+
...MUSE_MODELS.map((model) => ({
|
|
2132
|
+
value: `${MUSE_OPTION_PREFIX}${model.id}`,
|
|
2133
|
+
label: `${model.label} · muse`,
|
|
2134
|
+
})),
|
|
1889
2135
|
];
|
|
1890
2136
|
options.push({ value: "__claude_custom__", label: "Claude · custom model ID…" });
|
|
1891
2137
|
options.push({ value: "__grok_custom__", label: "Grok · custom model ID…" });
|
|
2138
|
+
options.push({ value: "__codex_custom__", label: "Codex · custom model ID…" });
|
|
2139
|
+
options.push({ value: "__muse_custom__", label: "Muse · custom model ID…" });
|
|
1892
2140
|
const input = new Input();
|
|
1893
|
-
let enteringBackend: "claude" | "grok" | undefined;
|
|
2141
|
+
let enteringBackend: "claude" | "grok" | "codex" | "muse" | undefined;
|
|
1894
2142
|
input.onSubmit = (value) => {
|
|
1895
|
-
if (value.trim() && enteringBackend)
|
|
2143
|
+
if (value.trim() && enteringBackend) {
|
|
2144
|
+
const prefix =
|
|
2145
|
+
enteringBackend === "claude"
|
|
2146
|
+
? CLAUDE_OPTION_PREFIX
|
|
2147
|
+
: enteringBackend === "grok"
|
|
2148
|
+
? GROK_OPTION_PREFIX
|
|
2149
|
+
: enteringBackend === "codex"
|
|
2150
|
+
? CODEX_OPTION_PREFIX
|
|
2151
|
+
: MUSE_OPTION_PREFIX;
|
|
2152
|
+
pickerDone(`${prefix}${value.trim()}`);
|
|
2153
|
+
}
|
|
1896
2154
|
};
|
|
1897
2155
|
const picker = new SelectList(options, Math.min(options.length, 8), getSelectListTheme());
|
|
1898
2156
|
const selectedIndex = capability && current === "Default" ? 0 : options.findIndex((option) => option.value === current);
|
|
@@ -1900,11 +2158,14 @@ export function createConfigModal(
|
|
|
1900
2158
|
picker.onSelect = (item) => {
|
|
1901
2159
|
if (item.value === "__claude_custom__") { enteringBackend = "claude"; tui.requestRender(); }
|
|
1902
2160
|
else if (item.value === "__grok_custom__") { enteringBackend = "grok"; tui.requestRender(); }
|
|
2161
|
+
else if (item.value === "__codex_custom__") { enteringBackend = "codex"; tui.requestRender(); }
|
|
2162
|
+
else if (item.value === "__muse_custom__") { enteringBackend = "muse"; tui.requestRender(); }
|
|
1903
2163
|
else pickerDone(item.value);
|
|
1904
2164
|
};
|
|
1905
2165
|
picker.onCancel = () => pickerDone();
|
|
2166
|
+
const customLabel = () => enteringBackend === "claude" ? "Claude" : enteringBackend === "grok" ? "Grok" : enteringBackend === "codex" ? "Codex" : "Muse";
|
|
1906
2167
|
return {
|
|
1907
|
-
render: (width: number) => enteringBackend ? [`${
|
|
2168
|
+
render: (width: number) => enteringBackend ? [`${customLabel()} model ID (Enter saves, Esc cancels)`, ...input.render(width)] : picker.render(width),
|
|
1908
2169
|
invalidate: () => { picker.invalidate(); input.invalidate(); },
|
|
1909
2170
|
handleInput: (data: string) => {
|
|
1910
2171
|
if (enteringBackend && matchesKey(data, "escape")) pickerDone();
|
|
@@ -1945,9 +2206,29 @@ export function createConfigModal(
|
|
|
1945
2206
|
function refresh(): void {
|
|
1946
2207
|
const defaultBackend = settings.backend ?? "agy";
|
|
1947
2208
|
const def = resolveModelEffort({ backend: settings.backend, model: settings.model, effort: settings.effort }, families);
|
|
1948
|
-
modelItem.currentValue =
|
|
2209
|
+
modelItem.currentValue =
|
|
2210
|
+
defaultBackend === "claude"
|
|
2211
|
+
? `${CLAUDE_OPTION_PREFIX}${settings.model}`
|
|
2212
|
+
: defaultBackend === "grok"
|
|
2213
|
+
? `${GROK_OPTION_PREFIX}${settings.model}`
|
|
2214
|
+
: defaultBackend === "codex"
|
|
2215
|
+
? `${CODEX_OPTION_PREFIX}${settings.model}`
|
|
2216
|
+
: defaultBackend === "muse"
|
|
2217
|
+
? `${MUSE_OPTION_PREFIX}${settings.model}`
|
|
2218
|
+
: (def.family?.id ?? settings.model);
|
|
1949
2219
|
effortItem.currentValue = effortDisplay(def, defaultBackend);
|
|
1950
|
-
effortItem.values =
|
|
2220
|
+
effortItem.values =
|
|
2221
|
+
defaultBackend === "claude"
|
|
2222
|
+
? ["default", ...CLAUDE_EFFORTS]
|
|
2223
|
+
: defaultBackend === "grok"
|
|
2224
|
+
? ["default", ...GROK_EFFORTS]
|
|
2225
|
+
: defaultBackend === "codex"
|
|
2226
|
+
? ["default", ...CODEX_EFFORTS]
|
|
2227
|
+
: defaultBackend === "muse"
|
|
2228
|
+
? ["default", ...MUSE_EFFORTS]
|
|
2229
|
+
: def.family?.efforts.length
|
|
2230
|
+
? [...def.family.efforts]
|
|
2231
|
+
: undefined;
|
|
1951
2232
|
modeItem.currentValue = settings.mode;
|
|
1952
2233
|
showTurnsItem.currentValue = String(settings.showTurns);
|
|
1953
2234
|
showTurnsItem.values = showTurnsValues(settings.showTurns);
|
|
@@ -1963,9 +2244,24 @@ export function createConfigModal(
|
|
|
1963
2244
|
? `${CLAUDE_OPTION_PREFIX}${override.model}`
|
|
1964
2245
|
: backend === "grok"
|
|
1965
2246
|
? `${GROK_OPTION_PREFIX}${override.model}`
|
|
1966
|
-
:
|
|
2247
|
+
: backend === "codex"
|
|
2248
|
+
? `${CODEX_OPTION_PREFIX}${override.model}`
|
|
2249
|
+
: backend === "muse"
|
|
2250
|
+
? `${MUSE_OPTION_PREFIX}${override.model}`
|
|
2251
|
+
: (resolved.family?.id ?? override.model);
|
|
1967
2252
|
rows.effort.currentValue = backendSupports(backend, capability) ? effortDisplay(resolved, backend) : "unsupported backend";
|
|
1968
|
-
rows.effort.values =
|
|
2253
|
+
rows.effort.values =
|
|
2254
|
+
backend === "claude"
|
|
2255
|
+
? ["default", ...CLAUDE_EFFORTS]
|
|
2256
|
+
: backend === "grok"
|
|
2257
|
+
? ["default", ...GROK_EFFORTS]
|
|
2258
|
+
: backend === "codex"
|
|
2259
|
+
? ["default", ...CODEX_EFFORTS]
|
|
2260
|
+
: backend === "muse"
|
|
2261
|
+
? ["default", ...MUSE_EFFORTS]
|
|
2262
|
+
: resolved.family?.efforts.length
|
|
2263
|
+
? [...resolved.family.efforts]
|
|
2264
|
+
: undefined;
|
|
1969
2265
|
}
|
|
1970
2266
|
}
|
|
1971
2267
|
refresh();
|
|
@@ -2025,9 +2321,15 @@ export function createConfigModal(
|
|
|
2025
2321
|
if (switched.backend === "claude") {
|
|
2026
2322
|
const effort = settings.backend === "claude" && isClaudeEffort(settings.effort) ? settings.effort : "default";
|
|
2027
2323
|
settings = { ...settings, backend: "claude", model: resolveClaudeModel(switched.id), effort };
|
|
2028
|
-
} else {
|
|
2324
|
+
} else if (switched.backend === "grok") {
|
|
2029
2325
|
const effort = settings.backend === "grok" && isGrokEffort(settings.effort) ? settings.effort : "default";
|
|
2030
2326
|
settings = { ...settings, backend: "grok", model: resolveGrokModel(switched.id), effort };
|
|
2327
|
+
} else if (switched.backend === "codex") {
|
|
2328
|
+
const effort = settings.backend === "codex" && isCodexEffort(settings.effort) ? settings.effort : "default";
|
|
2329
|
+
settings = { ...settings, backend: "codex", model: resolveCodexModel(switched.id), effort };
|
|
2330
|
+
} else {
|
|
2331
|
+
const effort = settings.backend === "muse" && isMuseEffort(settings.effort) ? settings.effort : "default";
|
|
2332
|
+
settings = { ...settings, backend: "muse", model: resolveMuseModel(switched.id), effort };
|
|
2031
2333
|
}
|
|
2032
2334
|
} else {
|
|
2033
2335
|
const family = findFamily(newValue);
|
|
@@ -2044,6 +2346,12 @@ export function createConfigModal(
|
|
|
2044
2346
|
} else if ((settings.backend ?? "agy") === "grok") {
|
|
2045
2347
|
if (!isGrokEffort(newValue)) return;
|
|
2046
2348
|
settings = { ...settings, effort: newValue };
|
|
2349
|
+
} else if ((settings.backend ?? "agy") === "codex") {
|
|
2350
|
+
if (!isCodexEffort(newValue)) return;
|
|
2351
|
+
settings = { ...settings, effort: newValue };
|
|
2352
|
+
} else if ((settings.backend ?? "agy") === "muse") {
|
|
2353
|
+
if (!isMuseEffort(newValue)) return;
|
|
2354
|
+
settings = { ...settings, effort: newValue };
|
|
2047
2355
|
} else {
|
|
2048
2356
|
// Effort-only edit: pin the resolved family's canonical id, same reasoning as the
|
|
2049
2357
|
// capability-override effort-only edit below -- otherwise a shared default created
|
|
@@ -2076,12 +2384,24 @@ export function createConfigModal(
|
|
|
2076
2384
|
model: resolveClaudeModel(switched.id),
|
|
2077
2385
|
effort: capabilityBackend(settings, capability) === "claude" && isClaudeEffort(currentEffort) ? currentEffort : "default",
|
|
2078
2386
|
});
|
|
2079
|
-
} else {
|
|
2387
|
+
} else if (switched.backend === "grok") {
|
|
2080
2388
|
settings = withCapabilityOverride(settings, capability, {
|
|
2081
2389
|
backend: "grok",
|
|
2082
2390
|
model: resolveGrokModel(switched.id),
|
|
2083
2391
|
effort: capabilityBackend(settings, capability) === "grok" && isGrokEffort(currentEffort) ? currentEffort : "default",
|
|
2084
2392
|
});
|
|
2393
|
+
} else if (switched.backend === "codex") {
|
|
2394
|
+
settings = withCapabilityOverride(settings, capability, {
|
|
2395
|
+
backend: "codex",
|
|
2396
|
+
model: resolveCodexModel(switched.id),
|
|
2397
|
+
effort: capabilityBackend(settings, capability) === "codex" && isCodexEffort(currentEffort) ? currentEffort : "default",
|
|
2398
|
+
});
|
|
2399
|
+
} else {
|
|
2400
|
+
settings = withCapabilityOverride(settings, capability, {
|
|
2401
|
+
backend: "muse",
|
|
2402
|
+
model: resolveMuseModel(switched.id),
|
|
2403
|
+
effort: capabilityBackend(settings, capability) === "muse" && isMuseEffort(currentEffort) ? currentEffort : "default",
|
|
2404
|
+
});
|
|
2085
2405
|
}
|
|
2086
2406
|
} else {
|
|
2087
2407
|
const family = findFamily(newValue);
|
|
@@ -2104,6 +2424,16 @@ export function createConfigModal(
|
|
|
2104
2424
|
const existing = capabilityOverride(settings, capability);
|
|
2105
2425
|
const model = existing?.model ?? settings.model;
|
|
2106
2426
|
settings = withCapabilityOverride(settings, capability, { backend: "grok", model, effort: newValue });
|
|
2427
|
+
} else if (capabilityBackend(settings, capability) === "codex") {
|
|
2428
|
+
if (!isCodexEffort(newValue)) return;
|
|
2429
|
+
const existing = capabilityOverride(settings, capability);
|
|
2430
|
+
const model = existing?.model ?? settings.model;
|
|
2431
|
+
settings = withCapabilityOverride(settings, capability, { backend: "codex", model, effort: newValue });
|
|
2432
|
+
} else if (capabilityBackend(settings, capability) === "muse") {
|
|
2433
|
+
if (!isMuseEffort(newValue)) return;
|
|
2434
|
+
const existing = capabilityOverride(settings, capability);
|
|
2435
|
+
const model = existing?.model ?? settings.model;
|
|
2436
|
+
settings = withCapabilityOverride(settings, capability, { backend: "muse", model, effort: newValue });
|
|
2107
2437
|
} else {
|
|
2108
2438
|
// Effort-only edit: pin the resolved family's canonical id, never whatever raw
|
|
2109
2439
|
// string happens to sit in settings.model/override.model (which — for a shared
|
|
@@ -2173,7 +2503,7 @@ export async function showBroConfigModal(ctx: ExtensionCommandContext, pi: Exten
|
|
|
2173
2503
|
families = await listAgyModels(pi);
|
|
2174
2504
|
} catch (error) {
|
|
2175
2505
|
families = [];
|
|
2176
|
-
ctx.ui.notify("Agy model catalog unavailable — showing
|
|
2506
|
+
ctx.ui.notify("Agy model catalog unavailable — showing other backend settings without Agy choices.", "warning");
|
|
2177
2507
|
}
|
|
2178
2508
|
await ctx.ui.custom<void>(createConfigModal(settings, families, writeSettings), {
|
|
2179
2509
|
overlay: true,
|
|
@@ -2470,7 +2800,7 @@ class BroModal implements Focusable {
|
|
|
2470
2800
|
render(width: number): string[] {
|
|
2471
2801
|
const dialogWidth = Math.max(24, width);
|
|
2472
2802
|
const innerWidth = Math.max(22, dialogWidth - 2);
|
|
2473
|
-
const terminalRows = process.stdout.rows ?? 30;
|
|
2803
|
+
const terminalRows = this.tui.terminal?.rows ?? process.stdout.rows ?? 30;
|
|
2474
2804
|
const dialogHeight = Math.min(32, Math.max(7, Math.floor(terminalRows * 0.78)));
|
|
2475
2805
|
this.bodyHeight = Math.max(1, dialogHeight - 6);
|
|
2476
2806
|
|
|
@@ -2705,7 +3035,7 @@ export function toggleBtwMode(thread: BtwThread): void {
|
|
|
2705
3035
|
thread.full = !thread.full;
|
|
2706
3036
|
}
|
|
2707
3037
|
|
|
2708
|
-
// Claude and
|
|
3038
|
+
// Claude, Grok, Codex, and Muse run btw in the workspace for both modes and resume one native session across
|
|
2709
3039
|
// /mode switches. An Agy conversation stays bound to the workspace it started in (a sandbox scratch
|
|
2710
3040
|
// dir vs. the repo), so an access change drops it; the next turn reseeds a fresh native session
|
|
2711
3041
|
// with the main-session context and the whole thread.
|
|
@@ -2903,7 +3233,7 @@ class BtwModal implements Focusable {
|
|
|
2903
3233
|
render(width: number): string[] {
|
|
2904
3234
|
const dialogWidth = Math.max(24, width);
|
|
2905
3235
|
const innerWidth = Math.max(22, dialogWidth - 2);
|
|
2906
|
-
const terminalRows = process.stdout.rows ?? 30;
|
|
3236
|
+
const terminalRows = this.tui.terminal?.rows ?? process.stdout.rows ?? 30;
|
|
2907
3237
|
const dialogHeight = Math.min(34, Math.max(8, Math.floor(terminalRows * 0.82)));
|
|
2908
3238
|
this.bodyHeight = Math.max(1, dialogHeight - 7);
|
|
2909
3239
|
|