pi-bro 0.16.0 → 0.18.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 +36 -0
- package/README.md +206 -222
- package/backend.ts +314 -23
- package/bro.ts +371 -396
- package/package.json +2 -2
- package/prompt.ts +17 -5
package/bro.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
agySelection,
|
|
23
23
|
advisorFlagErrorHint,
|
|
24
24
|
CLAUDE_EFFORTS,
|
|
25
|
+
GROK_EFFORTS,
|
|
25
26
|
backendSupports,
|
|
26
27
|
execute as executeBackend,
|
|
27
28
|
parseBtwAgyLine,
|
|
@@ -59,16 +60,20 @@ type TuiLike = {
|
|
|
59
60
|
};
|
|
60
61
|
type ModalKind = "loading" | "streaming" | "result" | "help" | "empty" | "error";
|
|
61
62
|
type BroSource = { text: string; label?: string };
|
|
62
|
-
type BroResult = { source: BroSource; text: string };
|
|
63
|
-
type ModalResult = { source?: BroSource; text: string; htmlPath?: string };
|
|
63
|
+
type BroResult = { source: BroSource; text: string; model?: string };
|
|
64
|
+
type ModalResult = { source?: BroSource; text: string; htmlPath?: string; model?: string };
|
|
64
65
|
type BtwTurn = { question: string; answer: string };
|
|
65
|
-
|
|
66
|
+
// `context` keeps the main-session seed so a fresh native session can be reseeded with the whole
|
|
67
|
+
// thread; `sessionFull` is the access mode the native session last ran in.
|
|
68
|
+
type BtwThread = { turns: BtwTurn[]; conversationId?: string; full: boolean; backend?: BackendName; model?: string; context?: string; sessionFull?: boolean };
|
|
66
69
|
const EFFORTS = ["default", "low", "medium", "high"] as const;
|
|
67
|
-
const BACKENDS = ["agy", "claude"] as const;
|
|
70
|
+
const BACKENDS = ["agy", "claude", "grok"] as const;
|
|
68
71
|
type BackendName = (typeof BACKENDS)[number];
|
|
69
72
|
type BroEffort = "default" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
70
73
|
type AgyEffort = Exclude<BroEffort, "default" | "xhigh" | "max">;
|
|
71
74
|
type ClaudeEffort = (typeof CLAUDE_EFFORTS)[number];
|
|
75
|
+
export { GROK_EFFORTS };
|
|
76
|
+
type GrokEffort = (typeof GROK_EFFORTS)[number];
|
|
72
77
|
// "advisor" is a real capability (command, Agy invocation, Doctor check) like the other three;
|
|
73
78
|
// see docs/plans/2026-09-19-bro-advisor-design.md. All four share one model/effort resolution,
|
|
74
79
|
// override, and Doctor-check path via this single list -- there is no configuration-only tier.
|
|
@@ -90,6 +95,11 @@ export const CLAUDE_MODELS = [
|
|
|
90
95
|
{ id: "sonnet", label: "Claude Sonnet" },
|
|
91
96
|
{ id: "opus", label: "Claude Opus" },
|
|
92
97
|
] as const;
|
|
98
|
+
export const GROK_MODELS = [
|
|
99
|
+
{ id: "grok-4.7", label: "Grok 4.7" },
|
|
100
|
+
{ id: "grok-4.7-build-fast", label: "Grok 4.7 Build Fast" },
|
|
101
|
+
] as const;
|
|
102
|
+
export { backendSupports as supportsBackend };
|
|
93
103
|
type AgyModelFamily = {
|
|
94
104
|
id: string;
|
|
95
105
|
label: string;
|
|
@@ -114,12 +124,9 @@ const COMMANDS = [
|
|
|
114
124
|
{ value: "url", label: "url", description: "Explain a public webpage" },
|
|
115
125
|
{ value: "open", label: "open", description: "Reopen the last explanation" },
|
|
116
126
|
{ value: "doctor", label: "doctor", description: "Check whether Bro is ready" },
|
|
117
|
-
{ value: "usage", label: "usage", description: "Show current Agy usage" },
|
|
118
|
-
{ value: "model", label: "model", description: "View or choose the shared default Agy model" },
|
|
119
|
-
{ value: "effort", label: "effort", description: "View or choose the shared default reasoning effort" },
|
|
120
127
|
{ value: "show", label: "show", description: "Draw what happened in recent session turns as shapes" },
|
|
121
128
|
{ value: "mode", label: "mode", description: "View or choose explanation mode (brief, balanced, faithful)" },
|
|
122
|
-
{ value: "btw", label: "btw", description: "Open a side conversation (
|
|
129
|
+
{ value: "btw", label: "btw", description: "Open a side conversation (starts conversation-only; /mode toggles full permission)" },
|
|
123
130
|
{ value: "config", label: "config", description: "Configure shared defaults and per-capability model/effort overrides" },
|
|
124
131
|
{ value: "advisor", label: "advisor", description: "Check whether the executor's advisor tool is available right now" },
|
|
125
132
|
{ value: "advisor-steer", label: "advisor-steer", description: "View, edit, save, or clear the advisor's persistent steering brief" },
|
|
@@ -501,7 +508,7 @@ export async function extractWebPage(input: string, signal?: AbortSignal): Promi
|
|
|
501
508
|
|
|
502
509
|
function parseBackend(value: unknown, context: string): BackendName {
|
|
503
510
|
if (typeof value !== "string" || !BACKENDS.some((backend) => backend === value)) {
|
|
504
|
-
throw new Error(`${context} backend must be "agy" or "
|
|
511
|
+
throw new Error(`${context} backend must be "agy", "claude", or "grok".`);
|
|
505
512
|
}
|
|
506
513
|
return value as BackendName;
|
|
507
514
|
}
|
|
@@ -514,6 +521,10 @@ function isClaudeEffort(effort: unknown): effort is ClaudeEffort | "default" {
|
|
|
514
521
|
return effort === "default" || CLAUDE_EFFORTS.some((item) => item === effort);
|
|
515
522
|
}
|
|
516
523
|
|
|
524
|
+
function isGrokEffort(effort: unknown): effort is GrokEffort | "default" {
|
|
525
|
+
return effort === "default" || GROK_EFFORTS.some((item) => item === effort);
|
|
526
|
+
}
|
|
527
|
+
|
|
517
528
|
function parseModelEffortPair(value: unknown, context: string): ModelEffortPair {
|
|
518
529
|
if (!isRecord(value) || typeof value.model !== "string" || !value.model.trim()) {
|
|
519
530
|
throw new Error(`${context} must contain a model and effort set to "default", "low", "medium", or "high".`);
|
|
@@ -525,6 +536,12 @@ function parseModelEffortPair(value: unknown, context: string): ModelEffortPair
|
|
|
525
536
|
}
|
|
526
537
|
return { backend, model: value.model.trim(), effort: value.effort };
|
|
527
538
|
}
|
|
539
|
+
if (backend === "grok") {
|
|
540
|
+
if (!isGrokEffort(value.effort)) {
|
|
541
|
+
throw new Error(`${context} must contain a model and effort set to "default", "low", "medium", "high", or "xhigh".`);
|
|
542
|
+
}
|
|
543
|
+
return { backend, model: value.model.trim(), effort: value.effort };
|
|
544
|
+
}
|
|
528
545
|
if (!isAgyEffort(value.effort)) {
|
|
529
546
|
throw new Error(`${context} must contain a model and effort set to "default", "low", "medium", or "high".`);
|
|
530
547
|
}
|
|
@@ -573,12 +590,14 @@ export function parseBroSettings(value: unknown): BroSettings {
|
|
|
573
590
|
throw new Error('Settings must contain a model and effort set to "default", "low", "medium", or "high".');
|
|
574
591
|
}
|
|
575
592
|
const backend = value.backend === undefined ? undefined : parseBackend(value.backend, "Settings");
|
|
576
|
-
const effortOk = backend === "claude" ? isClaudeEffort(value.effort) : isAgyEffort(value.effort);
|
|
593
|
+
const effortOk = backend === "claude" ? isClaudeEffort(value.effort) : backend === "grok" ? isGrokEffort(value.effort) : isAgyEffort(value.effort);
|
|
577
594
|
if (!effortOk) {
|
|
578
595
|
throw new Error(
|
|
579
596
|
backend === "claude"
|
|
580
597
|
? 'Settings must contain a model and effort set to "default", "low", "medium", "high", "xhigh", or "max".'
|
|
581
|
-
:
|
|
598
|
+
: backend === "grok"
|
|
599
|
+
? 'Settings must contain a model and effort set to "default", "low", "medium", "high", or "xhigh".'
|
|
600
|
+
: 'Settings must contain a model and effort set to "default", "low", "medium", or "high".',
|
|
582
601
|
);
|
|
583
602
|
}
|
|
584
603
|
const mode = value.mode === undefined ? DEFAULT_BRO_MODE : parseBroMode(value.mode);
|
|
@@ -630,8 +649,8 @@ export function resolveModelEffort(
|
|
|
630
649
|
pair: ModelEffortPair,
|
|
631
650
|
families: AgyModelFamily[],
|
|
632
651
|
): { pair: ModelEffortPair; family?: AgyModelFamily } {
|
|
633
|
-
// Claude selections never resolve through the Agy catalog; they pass through untouched.
|
|
634
|
-
if (pair.backend === "claude") return { pair };
|
|
652
|
+
// Claude/Grok selections never resolve through the Agy catalog; they pass through untouched.
|
|
653
|
+
if (pair.backend === "claude" || pair.backend === "grok") return { pair };
|
|
635
654
|
const family = families.find((item) => item.id === pair.model || item.variants.some((variant) => variant.id === pair.model));
|
|
636
655
|
if (!family) return { pair };
|
|
637
656
|
const variant = family.variants.find((item) => item.id === pair.model);
|
|
@@ -661,19 +680,31 @@ export function resolveClaudeModel(input: string): string {
|
|
|
661
680
|
return alias ? alias.id : trimmed;
|
|
662
681
|
}
|
|
663
682
|
|
|
683
|
+
// Grok seeds resolve to themselves; any other non-empty string passes through
|
|
684
|
+
// untouched as an explicit user-entered model ID (installed `grok models` confirms).
|
|
685
|
+
export function resolveGrokModel(input: string): string {
|
|
686
|
+
const trimmed = input.trim();
|
|
687
|
+
if (!trimmed) throw new Error("Grok model must be a non-empty model ID (grok-4.7, grok-4.7-build-fast, or an explicit model ID).");
|
|
688
|
+
return trimmed;
|
|
689
|
+
}
|
|
690
|
+
|
|
664
691
|
// Routes one capability's whole pair to its backend execution selection. Agy keeps the
|
|
665
|
-
// existing model/effort split; Claude
|
|
692
|
+
// existing model/effort split; Claude/Grok carry the model plus an optional effort
|
|
666
693
|
// ("default" means the CLI's own default and is omitted). Throws for an effort the
|
|
667
694
|
// resolved backend does not support instead of silently sending a mismatched pair.
|
|
668
695
|
export function selectionForCapability(settings: BroSettings, capability: Capability): BackendSelection {
|
|
669
696
|
const pair = capabilityPair(settings, capability);
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
697
|
+
const backend = capabilityBackend(settings, capability);
|
|
698
|
+
if (backend === "claude" || backend === "grok") {
|
|
699
|
+
const valid = backend === "claude" ? isClaudeEffort(pair.effort) : isGrokEffort(pair.effort);
|
|
700
|
+
if (!valid) {
|
|
701
|
+
throw new Error(`\`${pair.effort}\` is not supported on the ${backend === "claude" ? "Claude" : "Grok"} backend. Run \`/bro config\` to fix this.`);
|
|
673
702
|
}
|
|
674
|
-
return
|
|
675
|
-
|
|
676
|
-
|
|
703
|
+
return (
|
|
704
|
+
pair.effort === "default"
|
|
705
|
+
? { backend, model: pair.model }
|
|
706
|
+
: { backend, model: pair.model, effort: pair.effort }
|
|
707
|
+
) as BackendSelection;
|
|
677
708
|
}
|
|
678
709
|
if (!isAgyEffort(pair.effort)) {
|
|
679
710
|
throw new Error(`\`${pair.effort}\` is not supported on the Agy backend. Run \`/bro config\` to fix this.`);
|
|
@@ -681,6 +712,12 @@ export function selectionForCapability(settings: BroSettings, capability: Capabi
|
|
|
681
712
|
return agySelection({ model: pair.model, effort: pair.effort });
|
|
682
713
|
}
|
|
683
714
|
|
|
715
|
+
// The modal label for the exact selection a request runs with: model plus effort, where an
|
|
716
|
+
// omitted effort (the model's own default) reads simply "default".
|
|
717
|
+
export function selectionLabel(selection: BackendSelection): string {
|
|
718
|
+
return `${selection.model} · ${selection.effort ?? "default"}`;
|
|
719
|
+
}
|
|
720
|
+
|
|
684
721
|
function resolveCapabilitySettings(
|
|
685
722
|
settings: BroSettings,
|
|
686
723
|
capability: Capability,
|
|
@@ -912,6 +949,27 @@ async function checkClaudeAuth(pi: ExtensionAPI, signal: AbortSignal): Promise<s
|
|
|
912
949
|
}
|
|
913
950
|
}
|
|
914
951
|
|
|
952
|
+
async function checkGrokVersion(pi: ExtensionAPI, signal: AbortSignal): Promise<string> {
|
|
953
|
+
const runDirectory = await mkdtemp(join(tmpdir(), "pi-bro-"));
|
|
954
|
+
try {
|
|
955
|
+
const result = await pi.exec("grok", ["--version"], { cwd: runDirectory, signal, timeout: 10_000 });
|
|
956
|
+
if (signal.aborted) throw new Error("Canceled.");
|
|
957
|
+
if (result.killed || result.code !== 0) {
|
|
958
|
+
const detail = result.stderr.trim() || result.stdout.trim();
|
|
959
|
+
throw new Error(
|
|
960
|
+
detail
|
|
961
|
+
? `Grok could not start: ${detail}\n\nRun \`/bro doctor\` for setup help.`
|
|
962
|
+
: "Grok could not start. Make sure Grok is installed and on PATH, then run `/bro doctor`.",
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
const version = result.stdout.trim() || result.stderr.trim();
|
|
966
|
+
if (!version) throw new Error("Grok returned no version information. Update Grok, then run `/bro doctor` again.");
|
|
967
|
+
return version;
|
|
968
|
+
} finally {
|
|
969
|
+
await rm(runDirectory, { recursive: true, force: true });
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
915
973
|
async function checkAgyUsage(pi: ExtensionAPI, signal: AbortSignal): Promise<string> {
|
|
916
974
|
const runDirectory = await mkdtemp(join(tmpdir(), "pi-bro-"));
|
|
917
975
|
try {
|
|
@@ -958,10 +1016,11 @@ async function doctorReport(pi: ExtensionAPI, ctx: ExtensionCommandContext, sign
|
|
|
958
1016
|
fail("Prompt", error);
|
|
959
1017
|
}
|
|
960
1018
|
|
|
961
|
-
// Probe only the backends some feature actually selects: a Claude-only setup never
|
|
1019
|
+
// Probe only the backends some feature actually selects: a Claude/Grok-only setup never
|
|
962
1020
|
// requires Agy to be installed, and vice versa.
|
|
963
1021
|
const agyInUse = !settings || capabilityBackend(settings, "explain") === "agy" || capabilityBackend(settings, "show") === "agy" || capabilityBackend(settings, "btw") === "agy" || capabilityBackend(settings, "advisor") === "agy" || (settings.backend ?? "agy") === "agy";
|
|
964
1022
|
const claudeInUse = !!settings && (capabilityBackend(settings, "explain") === "claude" || capabilityBackend(settings, "show") === "claude" || capabilityBackend(settings, "btw") === "claude" || capabilityBackend(settings, "advisor") === "claude" || settings.backend === "claude");
|
|
1023
|
+
const grokInUse = !!settings && (capabilityBackend(settings, "explain") === "grok" || capabilityBackend(settings, "show") === "grok" || capabilityBackend(settings, "btw") === "grok" || capabilityBackend(settings, "advisor") === "grok" || settings.backend === "grok");
|
|
965
1024
|
|
|
966
1025
|
let agyStarted = false;
|
|
967
1026
|
if (agyInUse) {
|
|
@@ -1014,12 +1073,35 @@ async function doctorReport(pi: ExtensionAPI, ctx: ExtensionCommandContext, sign
|
|
|
1014
1073
|
pass("Claude", "not probed — no feature selects the Claude backend");
|
|
1015
1074
|
}
|
|
1016
1075
|
|
|
1076
|
+
// Version only, never a model request. No Grok auth probe exists, so a passing
|
|
1077
|
+
// version says the CLI starts — it says nothing about auth or connectivity.
|
|
1078
|
+
if (grokInUse) {
|
|
1079
|
+
try {
|
|
1080
|
+
pass("Grok", await checkGrokVersion(pi, signal));
|
|
1081
|
+
} catch (error) {
|
|
1082
|
+
if (signal.aborted) throw error;
|
|
1083
|
+
fail("Grok", error);
|
|
1084
|
+
}
|
|
1085
|
+
pass("Grok auth", "unverified — no auth probe exists (version does not imply auth or connectivity)");
|
|
1086
|
+
} else {
|
|
1087
|
+
pass("Grok", "not probed — no feature selects the Grok backend");
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1017
1090
|
if (settings) {
|
|
1018
1091
|
const defaultBackend = settings.backend ?? "agy";
|
|
1019
1092
|
if (defaultBackend === "claude") {
|
|
1020
1093
|
pass("Selected model", `claude \`${settings.model}\``);
|
|
1021
1094
|
if (!isClaudeEffort(settings.effort)) {
|
|
1022
|
-
fail("Reasoning effort", `\`${settings.effort}\` is unsupported. Run \`/bro
|
|
1095
|
+
fail("Reasoning effort", `\`${settings.effort}\` is unsupported. Run \`/bro config\` to choose another.`);
|
|
1096
|
+
} else if (settings.effort === "default") {
|
|
1097
|
+
pass("Reasoning effort", "built into the selected model");
|
|
1098
|
+
} else {
|
|
1099
|
+
pass("Reasoning effort", settings.effort);
|
|
1100
|
+
}
|
|
1101
|
+
} else if (defaultBackend === "grok") {
|
|
1102
|
+
pass("Selected model", `grok \`${settings.model}\``);
|
|
1103
|
+
if (!isGrokEffort(settings.effort)) {
|
|
1104
|
+
fail("Reasoning effort", `\`${settings.effort}\` is unsupported. Run \`/bro config\` to choose another.`);
|
|
1023
1105
|
} else if (settings.effort === "default") {
|
|
1024
1106
|
pass("Reasoning effort", "built into the selected model");
|
|
1025
1107
|
} else {
|
|
@@ -1028,7 +1110,7 @@ async function doctorReport(pi: ExtensionAPI, ctx: ExtensionCommandContext, sign
|
|
|
1028
1110
|
} else if (models) {
|
|
1029
1111
|
const current = resolveCatalogSettings(settings, models);
|
|
1030
1112
|
if (!current.family) {
|
|
1031
|
-
fail("Selected model", `\`${settings.model}\` is unavailable. Run \`/bro
|
|
1113
|
+
fail("Selected model", `\`${settings.model}\` is unavailable. Run \`/bro config\` to choose another.`);
|
|
1032
1114
|
} else {
|
|
1033
1115
|
pass("Selected model", `agy \`${current.family.id}\``);
|
|
1034
1116
|
const effort = current.settings.effort;
|
|
@@ -1037,7 +1119,7 @@ async function doctorReport(pi: ExtensionAPI, ctx: ExtensionCommandContext, sign
|
|
|
1037
1119
|
} else if (effort !== "default" && current.family.efforts.includes(effort as AgyEffort)) {
|
|
1038
1120
|
pass("Reasoning effort", effort);
|
|
1039
1121
|
} else {
|
|
1040
|
-
fail("Reasoning effort", `\`${effort}\` is unsupported. Run \`/bro
|
|
1122
|
+
fail("Reasoning effort", `\`${effort}\` is unsupported. Run \`/bro config\` to choose another.`);
|
|
1041
1123
|
}
|
|
1042
1124
|
}
|
|
1043
1125
|
}
|
|
@@ -1048,10 +1130,6 @@ async function doctorReport(pi: ExtensionAPI, ctx: ExtensionCommandContext, sign
|
|
|
1048
1130
|
const backend = capabilityBackend(settings, capability);
|
|
1049
1131
|
const pair = capabilityPair(settings, capability);
|
|
1050
1132
|
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
1133
|
if (!isClaudeEffort(pair.effort)) {
|
|
1056
1134
|
fail(label, `\`${pair.effort}\` is unsupported for claude \`${pair.model}\`. Run \`/bro config\` to fix this.`);
|
|
1057
1135
|
continue;
|
|
@@ -1064,6 +1142,23 @@ async function doctorReport(pi: ExtensionAPI, ctx: ExtensionCommandContext, sign
|
|
|
1064
1142
|
);
|
|
1065
1143
|
continue;
|
|
1066
1144
|
}
|
|
1145
|
+
if (backend === "grok") {
|
|
1146
|
+
if (!backendSupports("grok", capability)) {
|
|
1147
|
+
fail(label, `\`${capability}\` is not supported on the Grok backend for this capability. Run \`/bro config\` to give it another backend.`);
|
|
1148
|
+
continue;
|
|
1149
|
+
}
|
|
1150
|
+
if (!isGrokEffort(pair.effort)) {
|
|
1151
|
+
fail(label, `\`${pair.effort}\` is unsupported for grok \`${pair.model}\`. Run \`/bro config\` to fix this.`);
|
|
1152
|
+
continue;
|
|
1153
|
+
}
|
|
1154
|
+
pass(
|
|
1155
|
+
label,
|
|
1156
|
+
override
|
|
1157
|
+
? `override grok \`${pair.model}\`${pair.effort === "default" ? "" : ` (${pair.effort})`}`
|
|
1158
|
+
: `grok \`${pair.model}\`${pair.effort === "default" ? "" : ` (${pair.effort})`} · using the shared default`,
|
|
1159
|
+
);
|
|
1160
|
+
continue;
|
|
1161
|
+
}
|
|
1067
1162
|
if (!models) continue;
|
|
1068
1163
|
const resolved = resolveCapabilitySettings(settings, capability, models);
|
|
1069
1164
|
if (!resolved.family) {
|
|
@@ -1096,13 +1191,15 @@ async function doctorReport(pi: ExtensionAPI, ctx: ExtensionCommandContext, sign
|
|
|
1096
1191
|
pass("Advisor steering", resolveAdvisorState(ctx.sessionManager.getBranch()).steering.trim() ? "present" : "none");
|
|
1097
1192
|
if (settings && capabilityBackend(settings, "advisor") === "claude") {
|
|
1098
1193
|
pass("Advisor compatibility", "Claude backend — no Agy version floor applies");
|
|
1194
|
+
} else if (settings && capabilityBackend(settings, "advisor") === "grok") {
|
|
1195
|
+
pass("Advisor compatibility", "Grok backend — no Agy version floor applies; auth/connectivity unverified (no probe)");
|
|
1099
1196
|
} else if (agyVersion && advisorAgyCompatible(agyVersion)) pass("Advisor compatibility", ADVISOR_COMPATIBILITY);
|
|
1100
1197
|
else if (agyVersion) fail("Advisor compatibility", `installed \`${agyVersion}\`; requires Agy >=1.1.15. Run \`agy update\`.`);
|
|
1101
1198
|
|
|
1102
1199
|
return `# Bro doctor\n\n${lines.join("\n")}\n\n**${failed ? "Bro needs attention." : "Bro is ready."}**\n\n${
|
|
1103
1200
|
failed
|
|
1104
1201
|
? "Fix the failed items, then press **R** to check again."
|
|
1105
|
-
: "No assistant response was sent and no model turn was run.
|
|
1202
|
+
: "No assistant response was sent and no model turn was run. Version/auth checks do not imply connectivity (Grok auth is unverified — no probe exists)."
|
|
1106
1203
|
}`;
|
|
1107
1204
|
}
|
|
1108
1205
|
|
|
@@ -1295,8 +1392,9 @@ async function simplify(
|
|
|
1295
1392
|
signal: AbortSignal,
|
|
1296
1393
|
settings: BroSettings,
|
|
1297
1394
|
onProgress?: (text: string) => void,
|
|
1298
|
-
): Promise<string> {
|
|
1299
|
-
|
|
1395
|
+
): Promise<{ text: string; model: string }> {
|
|
1396
|
+
const selection = selectionForCapability(settings, "explain");
|
|
1397
|
+
return { text: await runAgyText((await promptFor(response, settings.mode)).text, selection, signal, onProgress), model: selectionLabel(selection) };
|
|
1300
1398
|
}
|
|
1301
1399
|
|
|
1302
1400
|
async function runShowExplanation(
|
|
@@ -1305,8 +1403,9 @@ async function runShowExplanation(
|
|
|
1305
1403
|
signal: AbortSignal,
|
|
1306
1404
|
settings: BroSettings,
|
|
1307
1405
|
onProgress?: (text: string) => void,
|
|
1308
|
-
): Promise<string> {
|
|
1309
|
-
|
|
1406
|
+
): Promise<{ text: string; model: string }> {
|
|
1407
|
+
const selection = selectionForCapability(settings, "show");
|
|
1408
|
+
return { text: await runAgyText(buildShowPrompt(transcript, steering), selection, signal, onProgress, "show"), model: selectionLabel(selection) };
|
|
1310
1409
|
}
|
|
1311
1410
|
|
|
1312
1411
|
// Thin presentation-boundary wrapper around the shared backend: coalesces raw text progress to the
|
|
@@ -1688,7 +1787,7 @@ export function advisorAttemptLabel(details: AdvisorToolDetails): string {
|
|
|
1688
1787
|
return `Bro advisor · ${details.model ?? "unknown model"} · ${details.attempt} attempt${details.attempt === 1 ? "" : "s"} · ${Math.ceil((details.durationMs ?? details.elapsedMs) / 1_000)}s`;
|
|
1689
1788
|
}
|
|
1690
1789
|
const latest = details.activity?.at(-1);
|
|
1691
|
-
const backendLabel = details.backend === "claude" ? "Claude" : "Agy";
|
|
1790
|
+
const backendLabel = details.backend === "claude" ? "Claude" : details.backend === "grok" ? "Grok" : "Agy";
|
|
1692
1791
|
// "last reported" + freshness, never a claim about what the backend is doing right now and never
|
|
1693
1792
|
// "stalled" -- silence since lastActivityAt is not itself evidence of a stuck run.
|
|
1694
1793
|
const activityLabel = latest
|
|
@@ -1708,9 +1807,10 @@ function showTurnsValues(current: number): string[] {
|
|
|
1708
1807
|
// falsely healthy for an unavailable model), and flags a stored effort that isn't one of the
|
|
1709
1808
|
// resolved family's supported efforts instead of silently showing it as if it were valid.
|
|
1710
1809
|
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
|
|
1810
|
+
// Claude/Grok selections never touch the Agy catalog: the stored effort is valid exactly
|
|
1811
|
+
// when it is one of that backend's levels (or "default" for the CLI's own default).
|
|
1713
1812
|
if (backend === "claude") return isClaudeEffort(resolved.pair.effort) ? resolved.pair.effort : `${resolved.pair.effort} (unsupported)`;
|
|
1813
|
+
if (backend === "grok") return isGrokEffort(resolved.pair.effort) ? resolved.pair.effort : `${resolved.pair.effort} (unsupported)`;
|
|
1714
1814
|
if (!resolved.family) return "unavailable";
|
|
1715
1815
|
const fixed = !resolved.family.efforts.length;
|
|
1716
1816
|
const valid = fixed ? resolved.pair.effort === "default" : resolved.family.efforts.includes(resolved.pair.effort as AgyEffort);
|
|
@@ -1718,7 +1818,7 @@ function effortDisplay(resolved: { pair: ModelEffortPair; family?: AgyModelFamil
|
|
|
1718
1818
|
return fixed ? "fixed" : resolved.pair.effort;
|
|
1719
1819
|
}
|
|
1720
1820
|
|
|
1721
|
-
// Model-picker values that switch
|
|
1821
|
+
// Model-picker values that switch backend carry a "claude:"/"grok:" prefix so one
|
|
1722
1822
|
// atomic picker commit changes backend+model together -- cancelling the picker (Esc)
|
|
1723
1823
|
// leaves both unchanged via the existing submenu-cancel path.
|
|
1724
1824
|
const CLAUDE_OPTION_PREFIX = "claude:";
|
|
@@ -1727,6 +1827,19 @@ function parseClaudeOption(value: string): string | undefined {
|
|
|
1727
1827
|
? value.slice(CLAUDE_OPTION_PREFIX.length)
|
|
1728
1828
|
: undefined;
|
|
1729
1829
|
}
|
|
1830
|
+
const GROK_OPTION_PREFIX = "grok:";
|
|
1831
|
+
function parseGrokOption(value: string): string | undefined {
|
|
1832
|
+
return value.startsWith(GROK_OPTION_PREFIX) && value.length > GROK_OPTION_PREFIX.length
|
|
1833
|
+
? value.slice(GROK_OPTION_PREFIX.length)
|
|
1834
|
+
: undefined;
|
|
1835
|
+
}
|
|
1836
|
+
function parseBackendOption(value: string): { backend: "claude" | "grok"; id: string } | undefined {
|
|
1837
|
+
const claudeId = parseClaudeOption(value);
|
|
1838
|
+
if (claudeId !== undefined) return { backend: "claude", id: claudeId };
|
|
1839
|
+
const grokId = parseGrokOption(value);
|
|
1840
|
+
if (grokId !== undefined) return { backend: "grok", id: grokId };
|
|
1841
|
+
return undefined;
|
|
1842
|
+
}
|
|
1730
1843
|
|
|
1731
1844
|
// Testable core: takes settings/catalog/persist as plain arguments so smoke tests can drive
|
|
1732
1845
|
// the exact interaction (submenus, cancel, cycling, save failure) without a real Agy process
|
|
@@ -1764,30 +1877,38 @@ export function createConfigModal(
|
|
|
1764
1877
|
value: family.id,
|
|
1765
1878
|
label: `${family.label}${family.efforts.length ? "" : " · fixed effort"}`,
|
|
1766
1879
|
})),
|
|
1767
|
-
// Claude entries stay last so existing Agy keyboard navigation is unaffected.
|
|
1880
|
+
// Claude/Grok entries stay last so existing Agy keyboard navigation is unaffected.
|
|
1768
1881
|
...CLAUDE_MODELS.map((model) => ({
|
|
1769
1882
|
value: `${CLAUDE_OPTION_PREFIX}${model.id}`,
|
|
1770
1883
|
label: `${model.label} · claude`,
|
|
1771
1884
|
})),
|
|
1885
|
+
...GROK_MODELS.map((model) => ({
|
|
1886
|
+
value: `${GROK_OPTION_PREFIX}${model.id}`,
|
|
1887
|
+
label: `${model.label} · grok`,
|
|
1888
|
+
})),
|
|
1772
1889
|
];
|
|
1773
1890
|
options.push({ value: "__claude_custom__", label: "Claude · custom model ID…" });
|
|
1891
|
+
options.push({ value: "__grok_custom__", label: "Grok · custom model ID…" });
|
|
1774
1892
|
const input = new Input();
|
|
1775
|
-
let
|
|
1776
|
-
input.onSubmit = (value) => {
|
|
1893
|
+
let enteringBackend: "claude" | "grok" | undefined;
|
|
1894
|
+
input.onSubmit = (value) => {
|
|
1895
|
+
if (value.trim() && enteringBackend) pickerDone(`${enteringBackend === "claude" ? CLAUDE_OPTION_PREFIX : GROK_OPTION_PREFIX}${value.trim()}`);
|
|
1896
|
+
};
|
|
1777
1897
|
const picker = new SelectList(options, Math.min(options.length, 8), getSelectListTheme());
|
|
1778
1898
|
const selectedIndex = capability && current === "Default" ? 0 : options.findIndex((option) => option.value === current);
|
|
1779
1899
|
picker.setSelectedIndex(Math.max(0, selectedIndex));
|
|
1780
1900
|
picker.onSelect = (item) => {
|
|
1781
|
-
if (item.value === "__claude_custom__") {
|
|
1901
|
+
if (item.value === "__claude_custom__") { enteringBackend = "claude"; tui.requestRender(); }
|
|
1902
|
+
else if (item.value === "__grok_custom__") { enteringBackend = "grok"; tui.requestRender(); }
|
|
1782
1903
|
else pickerDone(item.value);
|
|
1783
1904
|
};
|
|
1784
1905
|
picker.onCancel = () => pickerDone();
|
|
1785
1906
|
return {
|
|
1786
|
-
render: (width: number) =>
|
|
1907
|
+
render: (width: number) => enteringBackend ? [`${enteringBackend === "claude" ? "Claude" : "Grok"} model ID (Enter saves, Esc cancels)`, ...input.render(width)] : picker.render(width),
|
|
1787
1908
|
invalidate: () => { picker.invalidate(); input.invalidate(); },
|
|
1788
1909
|
handleInput: (data: string) => {
|
|
1789
|
-
if (
|
|
1790
|
-
else if (
|
|
1910
|
+
if (enteringBackend && matchesKey(data, "escape")) pickerDone();
|
|
1911
|
+
else if (enteringBackend) input.handleInput(data);
|
|
1791
1912
|
else picker.handleInput(data);
|
|
1792
1913
|
},
|
|
1793
1914
|
};
|
|
@@ -1823,10 +1944,10 @@ export function createConfigModal(
|
|
|
1823
1944
|
|
|
1824
1945
|
function refresh(): void {
|
|
1825
1946
|
const defaultBackend = settings.backend ?? "agy";
|
|
1826
|
-
const def = resolveModelEffort({ model: settings.model, effort: settings.effort }, families);
|
|
1827
|
-
modelItem.currentValue = defaultBackend === "claude" ? `${CLAUDE_OPTION_PREFIX}${settings.model}` : (def.family?.id ?? settings.model);
|
|
1947
|
+
const def = resolveModelEffort({ backend: settings.backend, model: settings.model, effort: settings.effort }, families);
|
|
1948
|
+
modelItem.currentValue = defaultBackend === "claude" ? `${CLAUDE_OPTION_PREFIX}${settings.model}` : defaultBackend === "grok" ? `${GROK_OPTION_PREFIX}${settings.model}` : (def.family?.id ?? settings.model);
|
|
1828
1949
|
effortItem.currentValue = effortDisplay(def, defaultBackend);
|
|
1829
|
-
effortItem.values = defaultBackend === "claude" ? ["default", ...CLAUDE_EFFORTS] : def.family?.efforts.length ? [...def.family.efforts] : undefined;
|
|
1950
|
+
effortItem.values = defaultBackend === "claude" ? ["default", ...CLAUDE_EFFORTS] : defaultBackend === "grok" ? ["default", ...GROK_EFFORTS] : def.family?.efforts.length ? [...def.family.efforts] : undefined;
|
|
1830
1951
|
modeItem.currentValue = settings.mode;
|
|
1831
1952
|
showTurnsItem.currentValue = String(settings.showTurns);
|
|
1832
1953
|
showTurnsItem.values = showTurnsValues(settings.showTurns);
|
|
@@ -1840,9 +1961,11 @@ export function createConfigModal(
|
|
|
1840
1961
|
? "Default"
|
|
1841
1962
|
: backend === "claude"
|
|
1842
1963
|
? `${CLAUDE_OPTION_PREFIX}${override.model}`
|
|
1843
|
-
:
|
|
1964
|
+
: backend === "grok"
|
|
1965
|
+
? `${GROK_OPTION_PREFIX}${override.model}`
|
|
1966
|
+
: (resolved.family?.id ?? override.model);
|
|
1844
1967
|
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;
|
|
1968
|
+
rows.effort.values = backend === "claude" ? ["default", ...CLAUDE_EFFORTS] : backend === "grok" ? ["default", ...GROK_EFFORTS] : resolved.family?.efforts.length ? [...resolved.family.efforts] : undefined;
|
|
1846
1969
|
}
|
|
1847
1970
|
}
|
|
1848
1971
|
refresh();
|
|
@@ -1897,14 +2020,19 @@ export function createConfigModal(
|
|
|
1897
2020
|
|
|
1898
2021
|
const onChange = (id: string, newValue: string) => {
|
|
1899
2022
|
if (id === "model") {
|
|
1900
|
-
const
|
|
1901
|
-
if (
|
|
1902
|
-
|
|
1903
|
-
|
|
2023
|
+
const switched = parseBackendOption(newValue);
|
|
2024
|
+
if (switched !== undefined) {
|
|
2025
|
+
if (switched.backend === "claude") {
|
|
2026
|
+
const effort = settings.backend === "claude" && isClaudeEffort(settings.effort) ? settings.effort : "default";
|
|
2027
|
+
settings = { ...settings, backend: "claude", model: resolveClaudeModel(switched.id), effort };
|
|
2028
|
+
} else {
|
|
2029
|
+
const effort = settings.backend === "grok" && isGrokEffort(settings.effort) ? settings.effort : "default";
|
|
2030
|
+
settings = { ...settings, backend: "grok", model: resolveGrokModel(switched.id), effort };
|
|
2031
|
+
}
|
|
1904
2032
|
} else {
|
|
1905
2033
|
const family = findFamily(newValue);
|
|
1906
2034
|
if (!family) return;
|
|
1907
|
-
const keepCurrent = settings.backend
|
|
2035
|
+
const keepCurrent = (settings.backend ?? "agy") === "agy" && (settings.effort === "default" ? !family.efforts.length : family.efforts.includes(settings.effort as AgyEffort));
|
|
1908
2036
|
// Backend-less internal pairs mean Agy; disk saves still tag them explicitly.
|
|
1909
2037
|
const { backend: _dropped, ...rest } = settings;
|
|
1910
2038
|
settings = { ...rest, model: family.id, effort: keepCurrent ? settings.effort : preferredEffort(family) };
|
|
@@ -1913,6 +2041,9 @@ export function createConfigModal(
|
|
|
1913
2041
|
if ((settings.backend ?? "agy") === "claude") {
|
|
1914
2042
|
if (!isClaudeEffort(newValue)) return;
|
|
1915
2043
|
settings = { ...settings, effort: newValue };
|
|
2044
|
+
} else if ((settings.backend ?? "agy") === "grok") {
|
|
2045
|
+
if (!isGrokEffort(newValue)) return;
|
|
2046
|
+
settings = { ...settings, effort: newValue };
|
|
1916
2047
|
} else {
|
|
1917
2048
|
// Effort-only edit: pin the resolved family's canonical id, same reasoning as the
|
|
1918
2049
|
// capability-override effort-only edit below -- otherwise a shared default created
|
|
@@ -1936,14 +2067,22 @@ export function createConfigModal(
|
|
|
1936
2067
|
if (newValue === "__default__") {
|
|
1937
2068
|
settings = withCapabilityOverride(settings, capability, undefined);
|
|
1938
2069
|
} else {
|
|
1939
|
-
const
|
|
1940
|
-
if (
|
|
2070
|
+
const switched = parseBackendOption(newValue);
|
|
2071
|
+
if (switched !== undefined) {
|
|
1941
2072
|
const currentEffort = capabilityPair(settings, capability).effort;
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
2073
|
+
if (switched.backend === "claude") {
|
|
2074
|
+
settings = withCapabilityOverride(settings, capability, {
|
|
2075
|
+
backend: "claude",
|
|
2076
|
+
model: resolveClaudeModel(switched.id),
|
|
2077
|
+
effort: capabilityBackend(settings, capability) === "claude" && isClaudeEffort(currentEffort) ? currentEffort : "default",
|
|
2078
|
+
});
|
|
2079
|
+
} else {
|
|
2080
|
+
settings = withCapabilityOverride(settings, capability, {
|
|
2081
|
+
backend: "grok",
|
|
2082
|
+
model: resolveGrokModel(switched.id),
|
|
2083
|
+
effort: capabilityBackend(settings, capability) === "grok" && isGrokEffort(currentEffort) ? currentEffort : "default",
|
|
2084
|
+
});
|
|
2085
|
+
}
|
|
1947
2086
|
} else {
|
|
1948
2087
|
const family = findFamily(newValue);
|
|
1949
2088
|
if (!family) return;
|
|
@@ -1960,6 +2099,11 @@ export function createConfigModal(
|
|
|
1960
2099
|
const existing = capabilityOverride(settings, capability);
|
|
1961
2100
|
const model = existing?.model ?? settings.model;
|
|
1962
2101
|
settings = withCapabilityOverride(settings, capability, { backend: "claude", model, effort: newValue });
|
|
2102
|
+
} else if (capabilityBackend(settings, capability) === "grok") {
|
|
2103
|
+
if (!isGrokEffort(newValue)) return;
|
|
2104
|
+
const existing = capabilityOverride(settings, capability);
|
|
2105
|
+
const model = existing?.model ?? settings.model;
|
|
2106
|
+
settings = withCapabilityOverride(settings, capability, { backend: "grok", model, effort: newValue });
|
|
1963
2107
|
} else {
|
|
1964
2108
|
// Effort-only edit: pin the resolved family's canonical id, never whatever raw
|
|
1965
2109
|
// string happens to sit in settings.model/override.model (which — for a shared
|
|
@@ -2023,14 +2167,13 @@ export async function showBroConfigModal(ctx: ExtensionCommandContext, pi: Exten
|
|
|
2023
2167
|
ctx.ui.notify(withDoctor(error), "error");
|
|
2024
2168
|
return;
|
|
2025
2169
|
}
|
|
2026
|
-
//
|
|
2027
|
-
// Agy catalog; anything still on Agy keeps the previous hard requirement.
|
|
2170
|
+
// Keep configuration repair accessible even when the Agy catalog is unavailable.
|
|
2028
2171
|
let families: AgyModelFamily[];
|
|
2029
2172
|
try {
|
|
2030
2173
|
families = await listAgyModels(pi);
|
|
2031
2174
|
} catch (error) {
|
|
2032
2175
|
families = [];
|
|
2033
|
-
ctx.ui.notify("Agy model catalog unavailable — showing Claude settings without Agy choices.", "warning");
|
|
2176
|
+
ctx.ui.notify("Agy model catalog unavailable — showing Claude/Grok settings without Agy choices.", "warning");
|
|
2034
2177
|
}
|
|
2035
2178
|
await ctx.ui.custom<void>(createConfigModal(settings, families, writeSettings), {
|
|
2036
2179
|
overlay: true,
|
|
@@ -2160,101 +2303,62 @@ export function helpText(settings?: BroSettings, settingsError?: string): string
|
|
|
2160
2303
|
const settingsSummary = settings
|
|
2161
2304
|
? `- **Backend:** ${settings.backend ?? "agy"}\n- **Model:** \`${settings.model}\`\n- **Reasoning effort:** ${settings.effort === "default" ? "built into the selected model" : settings.effort}\n- **Mode:** ${settings.mode}\n- **Show turns:** ${settings.showTurns}${overrideLines.length ? `\n${overrideLines.join("\n")}` : ""}`
|
|
2162
2305
|
: `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";
|
|
2165
2306
|
return `# Bro
|
|
2166
2307
|
|
|
2167
|
-
|
|
2308
|
+
Quick reference. The README is the full user guide: https://github.com/tranhoangnguyen03/pi-bro#readme
|
|
2168
2309
|
|
|
2169
|
-
## Explain
|
|
2310
|
+
## Explain and show
|
|
2170
2311
|
|
|
2171
2312
|
- \`/bro\` — explain the latest completed assistant reply
|
|
2172
2313
|
- \`/bro text [text]\` — explain pasted text, or the latest reply when text is omitted
|
|
2173
|
-
- \`/bro file <path>\` — explain a
|
|
2314
|
+
- \`/bro file <path>\` — explain a workspace \`.md\`, \`.markdown\`, \`.txt\`, \`.pdf\`, or \`.docx\` file
|
|
2174
2315
|
- \`/bro url <url>\` — explain one public webpage
|
|
2175
|
-
- \`/bro
|
|
2176
|
-
- \`/bro
|
|
2177
|
-
|
|
2178
|
-
Any other input is the source itself: a lone URL explains that webpage, an existing workspace file with a supported extension explains that file, and anything else is explained as pasted text. Quoted paths with spaces are routed too when the file exists.
|
|
2179
|
-
|
|
2180
|
-
Press **R** to simplify the captured source again. Run a new \`/bro text\`, \`/bro file\`, \`/bro url\`, or \`/bro show\` command — or give \`/bro\` the input directly — to capture a new source.
|
|
2181
|
-
|
|
2182
|
-
## Check and configure
|
|
2183
|
-
|
|
2184
|
-
- \`/bro doctor\` — check settings, backends, account, model, effort, and mode (per-feature backend/model/effort)
|
|
2185
|
-
- \`/bro usage [--provider agy]\` — show current Agy limits
|
|
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)
|
|
2188
|
-
- \`/bro mode [brief|balanced|faithful]\` — view or choose explanation mode
|
|
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.
|
|
2190
|
-
|
|
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.
|
|
2316
|
+
- \`/bro <input>\` — a lone URL, an existing supported file, or anything else as pasted text
|
|
2317
|
+
- \`/bro open\` — reopen the latest explanation without a new request
|
|
2318
|
+
- \`/bro show [n-turns] [query]\` — draw recent turns' conversation text as shapes; an optional query steers the focus
|
|
2192
2319
|
|
|
2193
2320
|
## Side conversation
|
|
2194
2321
|
|
|
2195
|
-
- \`/bro btw [
|
|
2322
|
+
- \`/bro btw [question]\` — open a side conversation seeded with recent main-session context. It starts conversation-only; reopening keeps the thread and its mode.
|
|
2323
|
+
- Inside the modal: Enter asks (empty Enter re-asks); \`/mode\` toggles conversation-only / full permission (read and edit the workspace) and keeps the thread; \`/copy\` and \`/copy-all\` copy to the clipboard; \`/insert\` and \`/insert-all\` insert into an empty main editor without submitting; \`/retry\` re-asks; \`/clear\` resets. Any other text is sent as a question. Esc closes.
|
|
2196
2324
|
|
|
2197
2325
|
## Advisor
|
|
2198
2326
|
|
|
2199
|
-
- \`bro_advisor\` — a tool the executor agent
|
|
2200
|
-
- \`/bro advisor\` —
|
|
2201
|
-
- \`/bro advisor-steer\` —
|
|
2202
|
-
|
|
2327
|
+
- \`bro_advisor\` — a tool the executor agent may call for a second opinion from a fresh backend process with real, auto-approved workspace access; it is told to advise, not edit, but that is not enforced
|
|
2328
|
+
- \`/bro advisor\` — whether \`bro_advisor\` is available right now
|
|
2329
|
+
- \`/bro advisor-steer\` — edit the session's persistent steering brief (**Ctrl+S** save, **Ctrl+K** clear, **Ctrl+C** copy, **Esc** close)
|
|
2330
|
+
|
|
2331
|
+
## Configure and check
|
|
2203
2332
|
|
|
2204
|
-
|
|
2333
|
+
- \`/bro config\` — shared default and per-capability (explain/show/btw/advisor) backend, model, and effort; explain mode; show turns. Changes save immediately.
|
|
2334
|
+
- \`/bro mode [brief|balanced|faithful]\` — view or choose the explanation mode
|
|
2335
|
+
- \`/bro doctor\` — check settings, prompt, and every selected backend without running a model turn
|
|
2205
2336
|
|
|
2206
2337
|
## Current settings
|
|
2207
2338
|
|
|
2208
2339
|
${settingsSummary}
|
|
2209
2340
|
|
|
2210
|
-
Saved in \`${SETTINGS_FILE}\`.
|
|
2341
|
+
Saved in \`${SETTINGS_FILE}\`.
|
|
2211
2342
|
|
|
2212
2343
|
## Explanation modes
|
|
2213
2344
|
|
|
2214
2345
|
- brief — the main point and next action, with no fixed word target
|
|
2215
2346
|
- balanced — default; material detail with clearer structure
|
|
2216
2347
|
- faithful — closest to the source, with no fixed word limit
|
|
2217
|
-
|
|
2218
|
-
|
|
2348
|
+
|
|
2349
|
+
A valid \`${PROMPT_FILE}\` (with \`{{response}}\` exactly once) overrides the modes; the saved mode stays inactive until you remove or rename it. Show always uses its own prompt.
|
|
2219
2350
|
|
|
2220
2351
|
## Controls
|
|
2221
2352
|
|
|
2222
|
-
- **Mouse wheel / trackpad
|
|
2223
|
-
- **↑ / ↓** — scroll
|
|
2353
|
+
- **Mouse wheel / trackpad**, **↑ / ↓** — scroll
|
|
2224
2354
|
- **C** — copy the full explanation
|
|
2225
|
-
- **R** — repeat the current action
|
|
2355
|
+
- **R** — repeat the current action
|
|
2356
|
+
- **O** — open the HTML diagram when a show reply contains one
|
|
2226
2357
|
- **Esc** — close, or cancel while Bro is working
|
|
2227
2358
|
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
## Important limits
|
|
2231
|
-
|
|
2232
|
-
- Documents must be inside the current workspace, are limited to 10 MiB and 100,000 extracted characters, and must be \`.md\`, \`.markdown\`, \`.txt\`, \`.pdf\`, or \`.docx\`. Scanned PDFs need OCR first.
|
|
2233
|
-
- Web input is limited to one public HTML page. Bro cannot sign in, run page JavaScript, bypass paywalls or blocks, follow pagination, or understand images and video.
|
|
2234
|
-
- If a webpage fails, copy it into a text file or save it as a PDF, then use \`/bro file\`.
|
|
2235
|
-
- Show draws only what already happened in this session — the conversation text of the last few turns, with tool calls, tool results, reasoning, and images always omitted — and 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.
|
|
2236
|
-
- Show reflects what was reported in the conversation, not independent verification against the actual code or system state.
|
|
2237
|
-
- Btw threads are memory-only and do not survive reloads or restarts. A turn is capped at 2 minutes in sandbox mode and 10 minutes in full mode; the side conversation resumes through Agy's \`--conversation\` support.
|
|
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.
|
|
2239
|
-
|
|
2240
|
-
## Privacy and safety
|
|
2241
|
-
|
|
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.
|
|
2243
|
-
|
|
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.
|
|
2245
|
-
|
|
2246
|
-
Bro's explain, show, file, and url commands never modify project files. \`/bro btw\` runs sandboxed (read-only) by default; with \`--full\` it can read and edit the workspace, so use \`--full\` only when you want the side conversation to touch your project.
|
|
2247
|
-
For webpages, it connects directly to the site without browser cookies; the site sees your IP address and Bro's user agent. Do not use private or signed URLs.
|
|
2248
|
-
|
|
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.
|
|
2250
|
-
|
|
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.
|
|
2359
|
+
## Privacy
|
|
2252
2360
|
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
Create or edit \`${PROMPT_FILE}\` and include \`{{response}}\` exactly once. Bro reads it on the next explanation and never modifies it. Existing valid custom prompts continue working unchanged.
|
|
2256
|
-
|
|
2257
|
-
A valid custom prompt fully overrides all built-in mode instructions. \`/bro mode\` still changes the saved mode, but that mode remains inactive until you remove or rename \`bro-prompt.md\`. An invalid custom prompt blocks explanations; run \`/bro doctor\` for the exact problem.`;
|
|
2361
|
+
Bro sends the captured source (or, for the advisor, the executor's instructions, tools, and conversation) to the selected backend and its model provider, which may retain it under their own policies. Nothing is added to Pi's conversation unless you insert it. Access controls differ by backend; see the README.`;
|
|
2258
2362
|
}
|
|
2259
2363
|
|
|
2260
2364
|
// The overlay framing pattern is adapted from pi-btw (MIT); see THIRD_PARTY_NOTICES.md.
|
|
@@ -2264,6 +2368,7 @@ class BroModal implements Focusable {
|
|
|
2264
2368
|
private kind: ModalKind = "loading";
|
|
2265
2369
|
private rawText = "";
|
|
2266
2370
|
private sourceLabel = "";
|
|
2371
|
+
private modelLabel = "";
|
|
2267
2372
|
private notice = "";
|
|
2268
2373
|
private offset = 0;
|
|
2269
2374
|
private maxOffset = 0;
|
|
@@ -2292,8 +2397,8 @@ class BroModal implements Focusable {
|
|
|
2292
2397
|
this.setContent("streaming", text, "", false, false);
|
|
2293
2398
|
}
|
|
2294
2399
|
|
|
2295
|
-
setResult(text: string, retryable: boolean, notice = "", sourceLabel = "", rawText = text): void {
|
|
2296
|
-
this.setContent("result", text, rawText, true, retryable, notice, sourceLabel);
|
|
2400
|
+
setResult(text: string, retryable: boolean, notice = "", sourceLabel = "", rawText = text, modelLabel = ""): void {
|
|
2401
|
+
this.setContent("result", text, rawText, true, retryable, notice, sourceLabel, modelLabel);
|
|
2297
2402
|
}
|
|
2298
2403
|
|
|
2299
2404
|
setHtmlPath(path: string): void {
|
|
@@ -2317,6 +2422,7 @@ class BroModal implements Focusable {
|
|
|
2317
2422
|
retryable: boolean,
|
|
2318
2423
|
notice = "",
|
|
2319
2424
|
sourceLabel = "",
|
|
2425
|
+
modelLabel = "",
|
|
2320
2426
|
): void {
|
|
2321
2427
|
this.kind = kind;
|
|
2322
2428
|
if (kind !== "result") this.htmlPath = "";
|
|
@@ -2328,6 +2434,7 @@ class BroModal implements Focusable {
|
|
|
2328
2434
|
.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ")
|
|
2329
2435
|
.replace(/\s+/g, " ")
|
|
2330
2436
|
.trim();
|
|
2437
|
+
this.modelLabel = modelLabel;
|
|
2331
2438
|
if (kind !== "streaming") this.offset = 0;
|
|
2332
2439
|
this.markdown.setText(text);
|
|
2333
2440
|
this.tui.requestRender();
|
|
@@ -2377,7 +2484,11 @@ class BroModal implements Focusable {
|
|
|
2377
2484
|
|
|
2378
2485
|
const lines = [
|
|
2379
2486
|
this.borderLine(innerWidth, "top"),
|
|
2380
|
-
this.frameLine(
|
|
2487
|
+
this.frameLine(
|
|
2488
|
+
this.theme.fg("accent", this.theme.bold(`Bro${this.sourceLabel ? ` · ${this.sourceLabel}` : ""}`)) +
|
|
2489
|
+
this.theme.fg("dim", `${this.modelLabel ? ` · ${this.modelLabel}` : ""}${scroll}`),
|
|
2490
|
+
innerWidth,
|
|
2491
|
+
),
|
|
2381
2492
|
this.ruleLine(innerWidth),
|
|
2382
2493
|
];
|
|
2383
2494
|
|
|
@@ -2518,7 +2629,7 @@ async function showBroModal(ctx: ExtensionCommandContext, options: BroModalOptio
|
|
|
2518
2629
|
current = result;
|
|
2519
2630
|
options.onResult?.(result);
|
|
2520
2631
|
const display = result.htmlPath ? stripShowHtmlFence(result.text) : result.text;
|
|
2521
|
-
modal.setResult(display, options.retryable ?? true, "", result.source?.label, result.text);
|
|
2632
|
+
modal.setResult(display, options.retryable ?? true, "", result.source?.label, result.text, result.model);
|
|
2522
2633
|
if (result.htmlPath) modal.setHtmlPath(result.htmlPath);
|
|
2523
2634
|
})
|
|
2524
2635
|
.catch((error) => {
|
|
@@ -2526,7 +2637,7 @@ async function showBroModal(ctx: ExtensionCommandContext, options: BroModalOptio
|
|
|
2526
2637
|
const message = error instanceof Error ? error.message : String(error);
|
|
2527
2638
|
if (previous) {
|
|
2528
2639
|
current = previous;
|
|
2529
|
-
modal.setResult(previous.text, options.retryable ?? true, `Retry failed: ${message}`, previous.source?.label);
|
|
2640
|
+
modal.setResult(previous.text, options.retryable ?? true, `Retry failed: ${message}`, previous.source?.label, previous.text, previous.model);
|
|
2530
2641
|
} else {
|
|
2531
2642
|
modal.setError(message);
|
|
2532
2643
|
}
|
|
@@ -2539,7 +2650,7 @@ async function showBroModal(ctx: ExtensionCommandContext, options: BroModalOptio
|
|
|
2539
2650
|
if (options.text !== undefined) {
|
|
2540
2651
|
modal.setStatic(options.kind ?? "help", options.text, options.copyable ?? false);
|
|
2541
2652
|
} else if (current) {
|
|
2542
|
-
modal.setResult(current.text, options.retryable ?? Boolean(options.run), "", current.source?.label);
|
|
2653
|
+
modal.setResult(current.text, options.retryable ?? Boolean(options.run), "", current.source?.label, current.text, current.model);
|
|
2543
2654
|
} else {
|
|
2544
2655
|
execute();
|
|
2545
2656
|
}
|
|
@@ -2559,26 +2670,50 @@ async function showBroModal(ctx: ExtensionCommandContext, options: BroModalOptio
|
|
|
2559
2670
|
);
|
|
2560
2671
|
}
|
|
2561
2672
|
|
|
2562
|
-
export function parseBtwArguments(value: string): {
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
const token = space === -1 ? rest : rest.slice(0, space);
|
|
2569
|
-
if (token === "--fresh") fresh = true;
|
|
2570
|
-
else if (token === "--full") full = true;
|
|
2571
|
-
else if (token === "--sandbox") full = false;
|
|
2572
|
-
else return { fresh, full, question: "", invalid: `Unknown /bro btw flag: ${token}` };
|
|
2573
|
-
rest = space === -1 ? "" : rest.slice(space).replace(/^\s+/, "");
|
|
2673
|
+
export function parseBtwArguments(value: string): { question: string; invalid?: string } {
|
|
2674
|
+
const rest = value.trim();
|
|
2675
|
+
if (!rest.startsWith("--")) return { question: rest };
|
|
2676
|
+
const token = rest.split(/\s/, 1)[0]!;
|
|
2677
|
+
if (token === "--full" || token === "--sandbox") {
|
|
2678
|
+
return { question: "", invalid: `${token} was removed. Open /bro btw and type /mode to switch between conversation-only and full permission.` };
|
|
2574
2679
|
}
|
|
2575
|
-
|
|
2680
|
+
if (token === "--fresh") {
|
|
2681
|
+
return { question: "", invalid: "--fresh was removed. Every new thread starts with main-session context; type /clear inside /bro btw to start over." };
|
|
2682
|
+
}
|
|
2683
|
+
return { question: "", invalid: `Unknown /bro btw flag: ${token}` };
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
// A new thread starts conversation-only; reopening keeps the thread and its mode.
|
|
2687
|
+
export function resolveBtwThread(existing: BtwThread | undefined): BtwThread {
|
|
2688
|
+
return existing ?? { turns: [], full: false };
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
export function bindBtwBackend(thread: BtwThread, backend: BackendName): boolean {
|
|
2692
|
+
const changed = thread.backend !== undefined && thread.backend !== backend;
|
|
2693
|
+
if (changed) { thread.turns = []; thread.conversationId = undefined; thread.context = undefined; thread.sessionFull = undefined; }
|
|
2694
|
+
thread.backend = backend;
|
|
2695
|
+
return changed;
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2698
|
+
export function btwModeLabel(full: boolean): string {
|
|
2699
|
+
return full ? "full permission" : "conversation-only";
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
// /mode keeps the transcript; whether the native session survives is decided by
|
|
2703
|
+
// nativeBtwContinuation on the next turn.
|
|
2704
|
+
export function toggleBtwMode(thread: BtwThread): void {
|
|
2705
|
+
thread.full = !thread.full;
|
|
2576
2706
|
}
|
|
2577
2707
|
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2708
|
+
// Claude and Grok run btw in the workspace for both modes and resume one native session across
|
|
2709
|
+
// /mode switches. An Agy conversation stays bound to the workspace it started in (a sandbox scratch
|
|
2710
|
+
// dir vs. the repo), so an access change drops it; the next turn reseeds a fresh native session
|
|
2711
|
+
// with the main-session context and the whole thread.
|
|
2712
|
+
export function nativeBtwContinuation(thread: BtwThread, backend: BackendName): string | undefined {
|
|
2713
|
+
if (backend === "agy" && thread.conversationId && thread.sessionFull !== undefined && thread.sessionFull !== thread.full) {
|
|
2714
|
+
thread.conversationId = undefined;
|
|
2715
|
+
}
|
|
2716
|
+
return thread.conversationId;
|
|
2582
2717
|
}
|
|
2583
2718
|
|
|
2584
2719
|
export function formatBtwTranscript(turns: readonly BtwTurn[]): string {
|
|
@@ -2595,26 +2730,23 @@ export function formatBtwTranscript(turns: readonly BtwTurn[]): string {
|
|
|
2595
2730
|
|
|
2596
2731
|
export type BtwComposerAction =
|
|
2597
2732
|
| { kind: "clear" }
|
|
2733
|
+
| { kind: "mode" }
|
|
2598
2734
|
| { kind: "retry" }
|
|
2599
2735
|
| { kind: "clipboard"; all: boolean }
|
|
2600
|
-
| { kind: "insert"; all: boolean
|
|
2736
|
+
| { kind: "insert"; all: boolean }
|
|
2737
|
+
| { kind: "removed"; command: string }
|
|
2601
2738
|
| { kind: "question"; text: string };
|
|
2602
2739
|
|
|
2603
2740
|
export function parseBtwComposerCommand(value: string): BtwComposerAction {
|
|
2604
2741
|
const command = value.trim();
|
|
2605
2742
|
if (command === "/clear") return { kind: "clear" };
|
|
2743
|
+
if (command === "/mode") return { kind: "mode" };
|
|
2606
2744
|
if (command === "/retry" || command === "") return { kind: "retry" };
|
|
2607
2745
|
if (command === "/copy" || command === "/copy-all") {
|
|
2608
2746
|
return { kind: "clipboard", all: command === "/copy-all" };
|
|
2609
2747
|
}
|
|
2610
|
-
if (
|
|
2611
|
-
|
|
2612
|
-
command === "/insert!" ||
|
|
2613
|
-
command === "/insert-all" ||
|
|
2614
|
-
command === "/insert-all!"
|
|
2615
|
-
) {
|
|
2616
|
-
return { kind: "insert", all: command.startsWith("/insert-all"), force: command.endsWith("!") };
|
|
2617
|
-
}
|
|
2748
|
+
if (command === "/insert" || command === "/insert-all") return { kind: "insert", all: command === "/insert-all" };
|
|
2749
|
+
if (command === "/insert!" || command === "/insert-all!") return { kind: "removed", command };
|
|
2618
2750
|
return { kind: "question", text: command };
|
|
2619
2751
|
}
|
|
2620
2752
|
|
|
@@ -2630,12 +2762,6 @@ async function runBtwTurn(
|
|
|
2630
2762
|
signal: AbortSignal,
|
|
2631
2763
|
onProgress?: (text: string) => void,
|
|
2632
2764
|
): 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
|
-
}
|
|
2639
2765
|
let updateTimer: ReturnType<typeof setTimeout> | undefined;
|
|
2640
2766
|
let latest: string | undefined;
|
|
2641
2767
|
const throttledProgress = onProgress
|
|
@@ -2657,7 +2783,7 @@ async function runBtwTurn(
|
|
|
2657
2783
|
feature: "btw",
|
|
2658
2784
|
prompt,
|
|
2659
2785
|
access: options.full ? "workspace-full" : "restricted",
|
|
2660
|
-
cwd: options.
|
|
2786
|
+
cwd: options.cwd,
|
|
2661
2787
|
continuation: options.conversationId ? { id: options.conversationId } : undefined,
|
|
2662
2788
|
},
|
|
2663
2789
|
selection,
|
|
@@ -2681,6 +2807,7 @@ class BtwModal implements Focusable {
|
|
|
2681
2807
|
private bodyHeight = 1;
|
|
2682
2808
|
private running = false;
|
|
2683
2809
|
private full = false;
|
|
2810
|
+
private model = "";
|
|
2684
2811
|
private disposed = false;
|
|
2685
2812
|
|
|
2686
2813
|
get focused(): boolean {
|
|
@@ -2721,6 +2848,11 @@ class BtwModal implements Focusable {
|
|
|
2721
2848
|
this.tui.requestRender();
|
|
2722
2849
|
}
|
|
2723
2850
|
|
|
2851
|
+
setModel(model: string): void {
|
|
2852
|
+
this.model = model;
|
|
2853
|
+
this.tui.requestRender();
|
|
2854
|
+
}
|
|
2855
|
+
|
|
2724
2856
|
setFull(full: boolean): void {
|
|
2725
2857
|
this.full = full;
|
|
2726
2858
|
this.tui.requestRender();
|
|
@@ -2782,16 +2914,15 @@ class BtwModal implements Focusable {
|
|
|
2782
2914
|
const hiddenBelow = Math.max(0, this.maxOffset - this.offset);
|
|
2783
2915
|
const scroll = this.maxOffset > 0 ? ` · ↑${this.offset} ↓${hiddenBelow}` : "";
|
|
2784
2916
|
|
|
2785
|
-
const
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
const header = this.theme.fg("accent", this.theme.bold("Bro · btw")) + this.theme.fg("dim", ` · ${mode}${scroll}`);
|
|
2917
|
+
const model = this.model ? this.theme.fg("dim", ` · ${this.model}`) : "";
|
|
2918
|
+
const mode = this.theme.fg("dim", " · ") + (this.full ? this.theme.fg("accent", this.theme.bold(btwModeLabel(true))) : this.theme.fg("dim", btwModeLabel(false)));
|
|
2919
|
+
const header = this.theme.fg("accent", this.theme.bold("Bro · btw")) + model + mode + this.theme.fg("dim", scroll);
|
|
2789
2920
|
|
|
2790
2921
|
const composer = this.input.render(innerWidth)[0] ?? "";
|
|
2791
2922
|
|
|
2792
2923
|
const controls = this.running
|
|
2793
2924
|
? this.theme.fg("dim", "Thinking… · Esc cancel")
|
|
2794
|
-
: this.theme.fg("dim", "Enter ask · Esc close · /copy · /copy-all · /insert · /insert-all · /clear · /retry");
|
|
2925
|
+
: this.theme.fg("dim", "Enter ask · Esc close · /mode · /copy · /copy-all · /insert · /insert-all · /clear · /retry");
|
|
2795
2926
|
|
|
2796
2927
|
const lines = [
|
|
2797
2928
|
this.borderLine(innerWidth, "top"),
|
|
@@ -2817,7 +2948,7 @@ class BtwModal implements Focusable {
|
|
|
2817
2948
|
|
|
2818
2949
|
async function openBtwModal(
|
|
2819
2950
|
ctx: ExtensionCommandContext,
|
|
2820
|
-
options: { thread: BtwThread; initialQuestion?: string
|
|
2951
|
+
options: { thread: BtwThread; initialQuestion?: string },
|
|
2821
2952
|
): Promise<void> {
|
|
2822
2953
|
const thread = options.thread;
|
|
2823
2954
|
|
|
@@ -2847,25 +2978,40 @@ async function openBtwModal(
|
|
|
2847
2978
|
modal.setRunning(true);
|
|
2848
2979
|
modal.clearComposer();
|
|
2849
2980
|
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
const
|
|
2854
|
-
|
|
2981
|
+
let settings: BroSettings;
|
|
2982
|
+
try {
|
|
2983
|
+
settings = await readSettings();
|
|
2984
|
+
const backend = capabilityBackend(settings, "btw");
|
|
2985
|
+
if (bindBtwBackend(thread, backend)) modal.setNotice("Backend changed — started a fresh side thread.");
|
|
2986
|
+
} catch (error) {
|
|
2987
|
+
controller = undefined; modal.setRunning(false); modal.setNotice(errorMessage(error)); return;
|
|
2988
|
+
}
|
|
2989
|
+
if (thread.turns.length === 0) {
|
|
2990
|
+
thread.conversationId = undefined;
|
|
2991
|
+
let context = captureShowTranscript(ctx, BTW_CONTEXT_TURNS)?.text;
|
|
2855
2992
|
if (context && context.length > BTW_CONTEXT_MAX) {
|
|
2856
2993
|
context = `${context.slice(0, BTW_CONTEXT_MAX)}\n[… context truncated …]`;
|
|
2857
2994
|
}
|
|
2995
|
+
thread.context = context;
|
|
2858
2996
|
}
|
|
2997
|
+
// Resume natively when possible; otherwise seed the fresh native session with the main-session
|
|
2998
|
+
// context plus every earlier turn so nothing is silently lost.
|
|
2999
|
+
const conversationId = nativeBtwContinuation(thread, capabilityBackend(settings, "btw"));
|
|
3000
|
+
const history = !conversationId && thread.turns.length ? formatBtwTranscript(thread.turns) : undefined;
|
|
3001
|
+
const context = conversationId ? undefined : thread.context;
|
|
3002
|
+
const full = thread.full;
|
|
2859
3003
|
|
|
2860
3004
|
thread.turns.push({ question, answer: "…" });
|
|
2861
3005
|
modal.setText(transcript());
|
|
2862
3006
|
|
|
2863
3007
|
try {
|
|
2864
|
-
const
|
|
3008
|
+
const selection = selectionForCapability(settings, "btw");
|
|
3009
|
+
thread.model = selectionLabel(selection);
|
|
3010
|
+
modal.setModel(thread.model);
|
|
2865
3011
|
const result = await runBtwTurn(
|
|
2866
|
-
buildBtwPrompt(context, question),
|
|
2867
|
-
|
|
2868
|
-
{ full
|
|
3012
|
+
buildBtwPrompt(context, question, { full, history }),
|
|
3013
|
+
selection,
|
|
3014
|
+
{ full, cwd: ctx.cwd, conversationId },
|
|
2869
3015
|
turnController.signal,
|
|
2870
3016
|
(partial) => {
|
|
2871
3017
|
if (closed || turnController.signal.aborted) return;
|
|
@@ -2874,7 +3020,10 @@ async function openBtwModal(
|
|
|
2874
3020
|
},
|
|
2875
3021
|
);
|
|
2876
3022
|
if (turnController.signal.aborted) return;
|
|
2877
|
-
if (result.conversationId)
|
|
3023
|
+
if (result.conversationId) {
|
|
3024
|
+
thread.conversationId = result.conversationId;
|
|
3025
|
+
thread.sessionFull = full;
|
|
3026
|
+
}
|
|
2878
3027
|
thread.turns[thread.turns.length - 1]!.answer = result.text;
|
|
2879
3028
|
} catch (error) {
|
|
2880
3029
|
if (turnController.signal.aborted || closed) return;
|
|
@@ -2889,9 +3038,16 @@ async function openBtwModal(
|
|
|
2889
3038
|
}
|
|
2890
3039
|
};
|
|
2891
3040
|
|
|
3041
|
+
const toggleMode = () => {
|
|
3042
|
+
toggleBtwMode(thread);
|
|
3043
|
+
modal.setFull(thread.full);
|
|
3044
|
+
modal.setNotice(`Mode: ${btwModeLabel(thread.full)}`);
|
|
3045
|
+
};
|
|
3046
|
+
|
|
2892
3047
|
const clear = () => {
|
|
2893
3048
|
thread.turns = [];
|
|
2894
3049
|
thread.conversationId = undefined;
|
|
3050
|
+
thread.context = undefined;
|
|
2895
3051
|
modal.clearComposer();
|
|
2896
3052
|
modal.setNotice("");
|
|
2897
3053
|
modal.setText("");
|
|
@@ -2922,14 +3078,14 @@ async function openBtwModal(
|
|
|
2922
3078
|
}
|
|
2923
3079
|
};
|
|
2924
3080
|
|
|
2925
|
-
const insert = (all: boolean
|
|
3081
|
+
const insert = (all: boolean) => {
|
|
2926
3082
|
const text = all ? transcript() : (thread.turns.at(-1)?.answer ?? "");
|
|
2927
3083
|
if (!text.trim()) {
|
|
2928
3084
|
modal.setNotice("Nothing to insert yet.");
|
|
2929
3085
|
return;
|
|
2930
3086
|
}
|
|
2931
|
-
if (ctx.ui.getEditorText().trim()
|
|
2932
|
-
modal.setNotice("Main editor has a draft.
|
|
3087
|
+
if (ctx.ui.getEditorText().trim()) {
|
|
3088
|
+
modal.setNotice("Main editor has a draft. Edit or clear it first, then insert again.");
|
|
2933
3089
|
return;
|
|
2934
3090
|
}
|
|
2935
3091
|
ctx.ui.setEditorText(text);
|
|
@@ -2943,6 +3099,11 @@ async function openBtwModal(
|
|
|
2943
3099
|
clear();
|
|
2944
3100
|
return;
|
|
2945
3101
|
}
|
|
3102
|
+
if (action.kind === "mode") {
|
|
3103
|
+
modal.clearComposer();
|
|
3104
|
+
toggleMode();
|
|
3105
|
+
return;
|
|
3106
|
+
}
|
|
2946
3107
|
if (action.kind === "clipboard") {
|
|
2947
3108
|
modal.clearComposer();
|
|
2948
3109
|
void copyOut(action.all);
|
|
@@ -2950,7 +3111,12 @@ async function openBtwModal(
|
|
|
2950
3111
|
}
|
|
2951
3112
|
if (action.kind === "insert") {
|
|
2952
3113
|
modal.clearComposer();
|
|
2953
|
-
insert(action.all
|
|
3114
|
+
insert(action.all);
|
|
3115
|
+
return;
|
|
3116
|
+
}
|
|
3117
|
+
if (action.kind === "removed") {
|
|
3118
|
+
modal.clearComposer();
|
|
3119
|
+
modal.setNotice(`${action.command} was removed. Edit or clear the main editor draft, then use /insert or /insert-all.`);
|
|
2954
3120
|
return;
|
|
2955
3121
|
}
|
|
2956
3122
|
if (action.kind === "retry") {
|
|
@@ -2962,6 +3128,7 @@ async function openBtwModal(
|
|
|
2962
3128
|
}
|
|
2963
3129
|
|
|
2964
3130
|
modal.setFull(thread.full);
|
|
3131
|
+
modal.setModel(thread.model ?? "");
|
|
2965
3132
|
modal.setText(transcript());
|
|
2966
3133
|
|
|
2967
3134
|
if (options.initialQuestion) void runTurn(options.initialQuestion);
|
|
@@ -2985,7 +3152,7 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
2985
3152
|
let lastResult: BroResult | undefined;
|
|
2986
3153
|
let btwThread: BtwThread | undefined;
|
|
2987
3154
|
const remember = (result: ModalResult) => {
|
|
2988
|
-
if (result.source) lastResult = { source: result.source, text: result.text };
|
|
3155
|
+
if (result.source) lastResult = { source: result.source, text: result.text, model: result.model };
|
|
2989
3156
|
};
|
|
2990
3157
|
|
|
2991
3158
|
pi.on("session_start", async (_event, _ctx) => {
|
|
@@ -3061,8 +3228,8 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
3061
3228
|
// the activity throttle window.
|
|
3062
3229
|
activityCount: run.activityCount,
|
|
3063
3230
|
};
|
|
3064
|
-
// The backend qualifier appears only
|
|
3065
|
-
const header = `Bro advisor · ${backend === "
|
|
3231
|
+
// The backend qualifier appears only off Agy, keeping the Agy header byte-identical.
|
|
3232
|
+
const header = `Bro advisor · ${backend === "agy" ? "" : `backend: ${backend} · `}model: ${selection.model} · effort: ${effort} · ${run.attempts} attempt${run.attempts === 1 ? "" : "s"} · ${Math.ceil(run.durationMs / 1_000)}s`;
|
|
3066
3233
|
const context = `Context · cwd: ${JSON.stringify(ctx.cwd)} · steering: ${details.steeringIncluded ? "included" : "none"} · snapshot: ${snapshot.text.length} chars · Bro truncation: none · omissions: ${omissions}`;
|
|
3067
3234
|
return { content: [{ type: "text", text: `${header}\n${context}\n\n${run.advice}` }], details };
|
|
3068
3235
|
} finally {
|
|
@@ -3111,6 +3278,12 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
3111
3278
|
let action = parts[0] ?? "";
|
|
3112
3279
|
let value = raw.slice(raw.split(/\s+/, 1)[0]?.length ?? 0).trim();
|
|
3113
3280
|
|
|
3281
|
+
// Removed commands must never fall through to a paid text explanation.
|
|
3282
|
+
if (action === "model" || action === "effort") {
|
|
3283
|
+
ctx.ui.notify(`/bro ${action} was removed. Use /bro config to choose the shared default and per-capability ${action}; use /bro text <text> to explain text.`, "warning");
|
|
3284
|
+
return;
|
|
3285
|
+
}
|
|
3286
|
+
|
|
3114
3287
|
// An unknown first word means the whole input is the source: route it by shape.
|
|
3115
3288
|
if (action && !KNOWN_ACTIONS.has(action)) {
|
|
3116
3289
|
const candidate = unquote(raw);
|
|
@@ -3142,13 +3315,14 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
3142
3315
|
return { text: "**Nothing to show yet**\n\nThis session has no conversation turns to draw. Run something first, then press **R**." };
|
|
3143
3316
|
}
|
|
3144
3317
|
let text: string;
|
|
3318
|
+
let model: string;
|
|
3145
3319
|
try {
|
|
3146
|
-
text = await runShowExplanation(captured.text, steering, signal, await readSettings(), onProgress);
|
|
3320
|
+
({ text, model } = await runShowExplanation(captured.text, steering, signal, await readSettings(), onProgress));
|
|
3147
3321
|
} catch (error) {
|
|
3148
3322
|
throw new Error(withDoctor(error));
|
|
3149
3323
|
}
|
|
3150
3324
|
const html = extractShowHtml(text);
|
|
3151
|
-
return { source: captured, text, ...(html ? { htmlPath: await writeShowHtml(html) } : {}) };
|
|
3325
|
+
return { source: captured, text, model, ...(html ? { htmlPath: await writeShowHtml(html) } : {}) };
|
|
3152
3326
|
};
|
|
3153
3327
|
try {
|
|
3154
3328
|
await showBroModal(ctx, {
|
|
@@ -3179,7 +3353,7 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
3179
3353
|
try {
|
|
3180
3354
|
return {
|
|
3181
3355
|
source: target,
|
|
3182
|
-
|
|
3356
|
+
...(await simplify(target.text, signal, await readSettings(), onProgress)),
|
|
3183
3357
|
};
|
|
3184
3358
|
} catch (error) {
|
|
3185
3359
|
throw new Error(withDoctor(error));
|
|
@@ -3215,24 +3389,6 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
3215
3389
|
return;
|
|
3216
3390
|
}
|
|
3217
3391
|
|
|
3218
|
-
if (action === "usage") {
|
|
3219
|
-
const valid = parts.length === 1 || (parts.length === 3 && parts[1] === "--provider" && parts[2] === "agy");
|
|
3220
|
-
if (!valid) {
|
|
3221
|
-
ctx.ui.notify("Use /bro usage or /bro usage --provider agy.", "warning");
|
|
3222
|
-
return;
|
|
3223
|
-
}
|
|
3224
|
-
try {
|
|
3225
|
-
await showBroModal(ctx, {
|
|
3226
|
-
loadingText: "Checking Agy usage…",
|
|
3227
|
-
retryable: false,
|
|
3228
|
-
run: async (signal) => ({ text: await checkAgyUsage(pi, signal) }),
|
|
3229
|
-
});
|
|
3230
|
-
} catch (error) {
|
|
3231
|
-
ctx.ui.notify(withDoctor(error), "error");
|
|
3232
|
-
}
|
|
3233
|
-
return;
|
|
3234
|
-
}
|
|
3235
|
-
|
|
3236
3392
|
if (action === "mode") {
|
|
3237
3393
|
const requested = parts[1];
|
|
3238
3394
|
if (parts.length > 2 || (requested && !parseBroMode(requested))) {
|
|
@@ -3262,188 +3418,6 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
3262
3418
|
return;
|
|
3263
3419
|
}
|
|
3264
3420
|
|
|
3265
|
-
if (action === "model") {
|
|
3266
|
-
if (parts.length > 2) {
|
|
3267
|
-
ctx.ui.notify("Use /bro model or /bro model <id>.", "warning");
|
|
3268
|
-
return;
|
|
3269
|
-
}
|
|
3270
|
-
try {
|
|
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
|
-
}
|
|
3311
|
-
const models = await listAgyModels(pi);
|
|
3312
|
-
const current = resolveCatalogSettings(settings, models);
|
|
3313
|
-
const requested = parts[1];
|
|
3314
|
-
let selected: AgyModelFamily | undefined;
|
|
3315
|
-
let selectedEffort: BroEffort | undefined;
|
|
3316
|
-
if (requested) {
|
|
3317
|
-
selected = models.find((item) => item.id.toLowerCase() === requested);
|
|
3318
|
-
if (!selected) {
|
|
3319
|
-
for (const family of models) {
|
|
3320
|
-
const variant = family.variants.find((item) => item.id.toLowerCase() === requested);
|
|
3321
|
-
if (variant) {
|
|
3322
|
-
selected = family;
|
|
3323
|
-
selectedEffort = variant.effort ?? "default";
|
|
3324
|
-
break;
|
|
3325
|
-
}
|
|
3326
|
-
}
|
|
3327
|
-
}
|
|
3328
|
-
if (!selected) {
|
|
3329
|
-
ctx.ui.notify(`Unknown Agy model "${requested}". Run /bro model to see available choices.`, "warning");
|
|
3330
|
-
return;
|
|
3331
|
-
}
|
|
3332
|
-
} else {
|
|
3333
|
-
if (ctx.mode !== "tui") {
|
|
3334
|
-
ctx.ui.notify("Use /bro model <id> outside Pi's interactive UI.", "warning");
|
|
3335
|
-
return;
|
|
3336
|
-
}
|
|
3337
|
-
const ordered = [...models].sort((a, b) => Number(b.id === current.family?.id) - Number(a.id === current.family?.id));
|
|
3338
|
-
const choices = ordered.map(
|
|
3339
|
-
(item) =>
|
|
3340
|
-
`${item.id} — ${item.label} · ${item.efforts.length ? item.efforts.join("/") : "fixed effort"}${item.id === current.family?.id ? " (current)" : ""}`,
|
|
3341
|
-
);
|
|
3342
|
-
const choice = await ctx.ui.select(`Agy model (current: ${current.settings.model})`, choices);
|
|
3343
|
-
if (!choice) return;
|
|
3344
|
-
selected = ordered[choices.indexOf(choice)];
|
|
3345
|
-
}
|
|
3346
|
-
if (!selectedEffort) {
|
|
3347
|
-
const currentEffort = current.settings.effort;
|
|
3348
|
-
const canKeepCurrent =
|
|
3349
|
-
current.family?.id === selected.id &&
|
|
3350
|
-
(currentEffort === "default" ? !selected.efforts.length : selected.efforts.includes(currentEffort as AgyEffort));
|
|
3351
|
-
selectedEffort = canKeepCurrent ? currentEffort : preferredEffort(selected);
|
|
3352
|
-
}
|
|
3353
|
-
await writeSettings({ ...settings, model: selected.id, effort: selectedEffort });
|
|
3354
|
-
ctx.ui.notify(
|
|
3355
|
-
`Bro model: ${selected.id}${selectedEffort === "default" ? "" : ` (${selectedEffort})`}`,
|
|
3356
|
-
"info",
|
|
3357
|
-
);
|
|
3358
|
-
} catch (error) {
|
|
3359
|
-
ctx.ui.notify(withDoctor(error), "error");
|
|
3360
|
-
}
|
|
3361
|
-
return;
|
|
3362
|
-
}
|
|
3363
|
-
|
|
3364
|
-
if (action === "effort") {
|
|
3365
|
-
const requested = parts[1];
|
|
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))) {
|
|
3369
|
-
ctx.ui.notify("Use /bro effort, or choose low, medium, or high.", "warning");
|
|
3370
|
-
return;
|
|
3371
|
-
}
|
|
3372
|
-
try {
|
|
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
|
-
}
|
|
3404
|
-
const current = resolveCatalogSettings(settings, await listAgyModels(pi));
|
|
3405
|
-
if (!current.family) {
|
|
3406
|
-
ctx.ui.notify(`Model "${settings.model}" is not in Agy's current model list. Run /bro model first.`, "warning");
|
|
3407
|
-
return;
|
|
3408
|
-
}
|
|
3409
|
-
if (!current.family.efforts.length) {
|
|
3410
|
-
if (requested && requested !== "default") {
|
|
3411
|
-
ctx.ui.notify(`${current.family.label} uses a fixed effort level.`, "warning");
|
|
3412
|
-
return;
|
|
3413
|
-
}
|
|
3414
|
-
await writeSettings({ ...current.settings, model: current.family.id, effort: "default" });
|
|
3415
|
-
ctx.ui.notify(`${current.family.label} uses its built-in effort level.`, "info");
|
|
3416
|
-
return;
|
|
3417
|
-
}
|
|
3418
|
-
if (requested === "default" || (requested && !current.family.efforts.includes(requested as AgyEffort))) {
|
|
3419
|
-
ctx.ui.notify(
|
|
3420
|
-
`${current.family.label} supports ${current.family.efforts.join(" or ")} effort.`,
|
|
3421
|
-
"warning",
|
|
3422
|
-
);
|
|
3423
|
-
return;
|
|
3424
|
-
}
|
|
3425
|
-
let selected = requested as AgyEffort | undefined;
|
|
3426
|
-
if (!selected) {
|
|
3427
|
-
if (ctx.mode !== "tui") {
|
|
3428
|
-
ctx.ui.notify("Use /bro effort <low|medium|high> outside Pi's interactive UI.", "warning");
|
|
3429
|
-
return;
|
|
3430
|
-
}
|
|
3431
|
-
const efforts = [...current.family.efforts].sort(
|
|
3432
|
-
(a, b) => Number(b === current.settings.effort) - Number(a === current.settings.effort),
|
|
3433
|
-
);
|
|
3434
|
-
const choices = efforts.map((effort) => `${effort}${effort === current.settings.effort ? " (current)" : ""}`);
|
|
3435
|
-
const choice = await ctx.ui.select(`Agy reasoning effort (current: ${current.settings.effort})`, choices);
|
|
3436
|
-
if (!choice) return;
|
|
3437
|
-
selected = efforts[choices.indexOf(choice)];
|
|
3438
|
-
}
|
|
3439
|
-
await writeSettings({ ...current.settings, model: current.family.id, effort: selected });
|
|
3440
|
-
ctx.ui.notify(`Bro reasoning effort: ${selected}`, "info");
|
|
3441
|
-
} catch (error) {
|
|
3442
|
-
ctx.ui.notify(withDoctor(error), "error");
|
|
3443
|
-
}
|
|
3444
|
-
return;
|
|
3445
|
-
}
|
|
3446
|
-
|
|
3447
3421
|
if (action === "btw") {
|
|
3448
3422
|
if (ctx.mode !== "tui") {
|
|
3449
3423
|
ctx.ui.notify("Use /bro btw in Pi's interactive UI.", "warning");
|
|
@@ -3455,10 +3429,11 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
3455
3429
|
return;
|
|
3456
3430
|
}
|
|
3457
3431
|
try {
|
|
3458
|
-
|
|
3459
|
-
const thread = resolveBtwThread(btwThread
|
|
3432
|
+
const btwBackend = capabilityBackend(await readSettings(), "btw");
|
|
3433
|
+
const thread = resolveBtwThread(btwThread);
|
|
3434
|
+
if (bindBtwBackend(thread, btwBackend)) ctx.ui.notify("Backend changed — started a fresh side thread.", "info");
|
|
3460
3435
|
btwThread = thread;
|
|
3461
|
-
await openBtwModal(ctx, { thread, initialQuestion: parsed.question
|
|
3436
|
+
await openBtwModal(ctx, { thread, initialQuestion: parsed.question });
|
|
3462
3437
|
} catch (error) {
|
|
3463
3438
|
ctx.ui.notify(withDoctor(error), "error");
|
|
3464
3439
|
}
|
|
@@ -3535,7 +3510,7 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
3535
3510
|
const settings = await readSettings();
|
|
3536
3511
|
return {
|
|
3537
3512
|
source: target,
|
|
3538
|
-
|
|
3513
|
+
...(await simplify(target.text, signal, settings, onProgress)),
|
|
3539
3514
|
};
|
|
3540
3515
|
} catch (error) {
|
|
3541
3516
|
throw new Error(withDoctor(error));
|