pi-bro 0.19.5 → 0.21.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 +28 -0
- package/README.md +81 -38
- package/backend.ts +103 -190
- package/bro.ts +308 -81
- package/package.json +2 -2
- package/prompt.ts +54 -8
package/bro.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { parseHTML } from "linkedom";
|
|
|
17
17
|
import mammoth from "mammoth";
|
|
18
18
|
import { Type } from "typebox";
|
|
19
19
|
import { extractText } from "unpdf";
|
|
20
|
-
import { BRO_MODES, DEFAULT_BRO_MODE, buildAdvisorPrompt, buildBtwPrompt, buildDefaultPrompt, buildShowPrompt, parseBroMode, type BroMode } from "./prompt.ts";
|
|
20
|
+
import { BRO_MODES, DEFAULT_BRO_MODE, MAX_PREFERENCES_CHARS, STARTER_PREFERENCES, buildAdvisorPrompt, buildBtwPrompt, buildDefaultPrompt, buildShowPrompt, nextBroMode, parseBroMode, type BroMode } from "./prompt.ts";
|
|
21
21
|
import {
|
|
22
22
|
agyFailureMessage,
|
|
23
23
|
agySelection,
|
|
@@ -38,7 +38,9 @@ export { agyFailureMessage, agySelection, advisorFlagErrorHint, parseBtwAgyLine
|
|
|
38
38
|
const AGENT_DIR = getAgentDir();
|
|
39
39
|
const ENV_MODEL = process.env.PI_BRO_MODEL?.trim();
|
|
40
40
|
const DEFAULT_MODEL = ENV_MODEL || "gemini-3.7-flash";
|
|
41
|
-
const
|
|
41
|
+
const PREFERENCES_FILE = join(AGENT_DIR, "bro-preferences.md");
|
|
42
|
+
// Larger files are rejected before reading, even by the editor; the prompt limit is MAX_PREFERENCES_CHARS.
|
|
43
|
+
const MAX_PREFERENCES_FILE_BYTES = 64 * 1024;
|
|
42
44
|
const SETTINGS_FILE = join(AGENT_DIR, "bro-settings.json");
|
|
43
45
|
const LOADING_TEXT = "Simplifying for my bro…";
|
|
44
46
|
const MAX_FILE_BYTES = 10 * 1024 * 1024;
|
|
@@ -62,12 +64,15 @@ type TuiLike = {
|
|
|
62
64
|
};
|
|
63
65
|
type ModalKind = "loading" | "streaming" | "result" | "help" | "empty" | "error";
|
|
64
66
|
type BroSource = { text: string; label?: string };
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
// `mode` is the built-in explain mode that produced the text; absent for Show and Doctor.
|
|
68
|
+
// `preferences` records whether bro-preferences.md shaped this result; like `mode`, it belongs to the result.
|
|
69
|
+
type BroResult = { source: BroSource; text: string; model?: string; mode?: BroMode; preferences?: boolean };
|
|
70
|
+
type ModalResult = { source?: BroSource; text: string; htmlPath?: string; model?: string; mode?: BroMode; preferences?: boolean };
|
|
67
71
|
type BtwTurn = { question: string; answer: string };
|
|
68
72
|
// `context` keeps the main-session seed so a fresh native session can be reseeded with the whole
|
|
69
|
-
// thread; `sessionFull`
|
|
70
|
-
|
|
73
|
+
// thread; `sessionFull` and `sessionPreferences` are the access mode and preferences the native
|
|
74
|
+
// session last ran with. `preferences` records whether the latest turn used them (header tag).
|
|
75
|
+
type BtwThread = { turns: BtwTurn[]; conversationId?: string; full: boolean; backend?: BackendName; model?: string; preferences?: boolean; context?: string; sessionFull?: boolean; sessionPreferences?: string };
|
|
71
76
|
const EFFORTS = ["default", "low", "medium", "high"] as const;
|
|
72
77
|
const BACKENDS = ["agy", "claude", "grok", "codex", "muse"] as const;
|
|
73
78
|
type BackendName = (typeof BACKENDS)[number];
|
|
@@ -138,6 +143,7 @@ const COMMANDS = [
|
|
|
138
143
|
{ value: "doctor", label: "doctor", description: "Check whether Bro is ready" },
|
|
139
144
|
{ value: "show", label: "show", description: "Draw what happened in recent session turns as shapes" },
|
|
140
145
|
{ value: "mode", label: "mode", description: "View or choose explanation mode (brief, balanced, faithful)" },
|
|
146
|
+
{ value: "preferences", label: "preferences", description: "View or edit what Bro knows about you and how you like answers" },
|
|
141
147
|
{ value: "btw", label: "btw", description: "Open a side conversation (starts conversation-only; /mode toggles full permission)" },
|
|
142
148
|
{ value: "config", label: "config", description: "Configure shared defaults and per-capability model/effort overrides" },
|
|
143
149
|
{ value: "advisor", label: "advisor", description: "Check whether the executor's advisor tool is available right now" },
|
|
@@ -1138,10 +1144,10 @@ async function doctorReport(pi: ExtensionAPI, ctx: ExtensionCommandContext, sign
|
|
|
1138
1144
|
}
|
|
1139
1145
|
|
|
1140
1146
|
try {
|
|
1141
|
-
const
|
|
1142
|
-
pass("
|
|
1147
|
+
const preferences = await readPreferences();
|
|
1148
|
+
pass("Preferences", preferences ? `${preferences.length.toLocaleString("en-US")} characters · used by explain, show, btw` : "none — /bro preferences to add some");
|
|
1143
1149
|
} catch (error) {
|
|
1144
|
-
fail("
|
|
1150
|
+
fail("Preferences", error);
|
|
1145
1151
|
}
|
|
1146
1152
|
|
|
1147
1153
|
// Probe only the backends some feature actually selects: a Claude/Grok/Codex/Muse-only setup never
|
|
@@ -1595,20 +1601,32 @@ function openShowHtml(path: string): boolean {
|
|
|
1595
1601
|
return result.status === 0;
|
|
1596
1602
|
}
|
|
1597
1603
|
|
|
1598
|
-
|
|
1599
|
-
|
|
1604
|
+
// Raw file text for the editor, undefined when there is no file: never rejects for length, so an
|
|
1605
|
+
// oversize file can be opened and trimmed.
|
|
1606
|
+
export async function readPreferencesRaw(): Promise<string | undefined> {
|
|
1607
|
+
let size: number;
|
|
1600
1608
|
try {
|
|
1601
|
-
|
|
1609
|
+
size = (await stat(PREFERENCES_FILE)).size;
|
|
1602
1610
|
} catch (error) {
|
|
1603
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT")
|
|
1604
|
-
|
|
1605
|
-
}
|
|
1606
|
-
throw error;
|
|
1611
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
1612
|
+
throw new Error(`Cannot read ${PREFERENCES_FILE}: ${errorMessage(error)}`);
|
|
1607
1613
|
}
|
|
1614
|
+
if (size > MAX_PREFERENCES_FILE_BYTES) throw new Error(`${PREFERENCES_FILE} is larger than 64 KB. Edit or delete it directly.`);
|
|
1615
|
+
try {
|
|
1616
|
+
return (await readFile(PREFERENCES_FILE, "utf8")).replace(/^\uFEFF/, "");
|
|
1617
|
+
} catch (error) {
|
|
1618
|
+
throw new Error(`Cannot read ${PREFERENCES_FILE}: ${errorMessage(error)}`);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1608
1621
|
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1622
|
+
// Preferences for prompts: "" when absent or blank. Re-read on every request. An oversize file stops
|
|
1623
|
+
// the request before any backend call instead of being silently truncated.
|
|
1624
|
+
export async function readPreferences(): Promise<string> {
|
|
1625
|
+
const text = ((await readPreferencesRaw()) ?? "").trim();
|
|
1626
|
+
if (text.length > MAX_PREFERENCES_CHARS) {
|
|
1627
|
+
throw new Error(`${PREFERENCES_FILE} is ${text.length.toLocaleString("en-US")} characters; keep it under ${MAX_PREFERENCES_CHARS.toLocaleString("en-US")} (it's sent with every request). Use /bro preferences to trim it.`);
|
|
1628
|
+
}
|
|
1629
|
+
return text;
|
|
1612
1630
|
}
|
|
1613
1631
|
|
|
1614
1632
|
async function simplify(
|
|
@@ -1616,9 +1634,12 @@ async function simplify(
|
|
|
1616
1634
|
signal: AbortSignal,
|
|
1617
1635
|
settings: BroSettings,
|
|
1618
1636
|
onProgress?: (text: string) => void,
|
|
1619
|
-
|
|
1637
|
+
mode = settings.mode,
|
|
1638
|
+
): Promise<{ text: string; model: string; mode: BroMode; preferences: boolean }> {
|
|
1620
1639
|
const selection = selectionForCapability(settings, "explain");
|
|
1621
|
-
|
|
1640
|
+
const preferences = await readPreferences();
|
|
1641
|
+
const text = await runAgyText(buildDefaultPrompt(response, mode, preferences), selection, signal, onProgress);
|
|
1642
|
+
return { text, model: selectionLabel(selection), mode, preferences: Boolean(preferences) };
|
|
1622
1643
|
}
|
|
1623
1644
|
|
|
1624
1645
|
async function runShowExplanation(
|
|
@@ -1627,9 +1648,11 @@ async function runShowExplanation(
|
|
|
1627
1648
|
signal: AbortSignal,
|
|
1628
1649
|
settings: BroSettings,
|
|
1629
1650
|
onProgress?: (text: string) => void,
|
|
1630
|
-
): Promise<{ text: string; model: string }> {
|
|
1651
|
+
): Promise<{ text: string; model: string; preferences: boolean }> {
|
|
1631
1652
|
const selection = selectionForCapability(settings, "show");
|
|
1632
|
-
|
|
1653
|
+
const preferences = await readPreferences();
|
|
1654
|
+
const text = await runAgyText(buildShowPrompt(transcript, steering, preferences), selection, signal, onProgress, "show");
|
|
1655
|
+
return { text, model: selectionLabel(selection), preferences: Boolean(preferences) };
|
|
1633
1656
|
}
|
|
1634
1657
|
|
|
1635
1658
|
// Thin presentation-boundary wrapper around the shared backend: coalesces raw text progress to the
|
|
@@ -2531,31 +2554,71 @@ export async function showBroConfigModal(ctx: ExtensionCommandContext, pi: Exten
|
|
|
2531
2554
|
});
|
|
2532
2555
|
}
|
|
2533
2556
|
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
initialText: string
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2557
|
+
type TextEditorModalOptions = {
|
|
2558
|
+
title: string;
|
|
2559
|
+
subtitle: string;
|
|
2560
|
+
initialText: string;
|
|
2561
|
+
initialNotice?: string;
|
|
2562
|
+
// Returns an error message to refuse the save without calling onSave.
|
|
2563
|
+
validate?: (text: string) => string | undefined;
|
|
2564
|
+
// Either may be async; the modal then waits, ignores further edits, and reports success only once it settles.
|
|
2565
|
+
onSave: (text: string) => void | Promise<void>;
|
|
2566
|
+
onClear: () => void | Promise<void>;
|
|
2567
|
+
copy?: (text: string) => Promise<void>;
|
|
2568
|
+
};
|
|
2569
|
+
|
|
2570
|
+
// Testable core shared by /bro advisor-steer and /bro preferences: persistence only happens on
|
|
2571
|
+
// Ctrl+S/Ctrl+K. Esc leaves the stored text untouched; only the in-memory draft is discarded.
|
|
2572
|
+
export function createTextEditorModal(
|
|
2573
|
+
options: TextEditorModalOptions,
|
|
2541
2574
|
): (tui: TUI, theme: Theme, keybindings: unknown, done: (value?: void) => void) => Component & { dispose?(): void } {
|
|
2575
|
+
const { onSave, onClear, validate, copy = copyToClipboard } = options;
|
|
2542
2576
|
return (tui, theme, _keybindings, done) => {
|
|
2543
2577
|
const editorTheme: EditorTheme = { borderColor: (s: string) => theme.fg("border", s), selectList: getSelectListTheme() };
|
|
2544
2578
|
const editor = new Editor(tui, editorTheme);
|
|
2545
2579
|
editor.focused = true;
|
|
2546
|
-
editor.setText(initialText);
|
|
2580
|
+
editor.setText(options.initialText);
|
|
2547
2581
|
let disposed = false;
|
|
2548
|
-
|
|
2582
|
+
let pending = false;
|
|
2583
|
+
const notice = new Text(options.initialNotice ? theme.fg("accent", options.initialNotice) : "");
|
|
2549
2584
|
const showNotice = (message: string, color: "success" | "error") => {
|
|
2550
2585
|
if (disposed) return;
|
|
2551
2586
|
notice.setText(theme.fg(color, message));
|
|
2552
2587
|
tui.requestRender();
|
|
2553
2588
|
};
|
|
2554
2589
|
editor.onChange = () => notice.setText("");
|
|
2590
|
+
const persist = (action: () => void | Promise<void>, success: string, failure: string, after?: () => void) => {
|
|
2591
|
+
const fail = (error: unknown) => showNotice(`${failure}: ${errorMessage(error)}`, "error");
|
|
2592
|
+
let result: void | Promise<void>;
|
|
2593
|
+
try {
|
|
2594
|
+
result = action();
|
|
2595
|
+
} catch (error) {
|
|
2596
|
+
fail(error);
|
|
2597
|
+
return;
|
|
2598
|
+
}
|
|
2599
|
+
if (!(result instanceof Promise)) {
|
|
2600
|
+
after?.();
|
|
2601
|
+
showNotice(success, "success");
|
|
2602
|
+
return;
|
|
2603
|
+
}
|
|
2604
|
+
pending = true;
|
|
2605
|
+
result.then(
|
|
2606
|
+
() => {
|
|
2607
|
+
pending = false;
|
|
2608
|
+
if (disposed) return;
|
|
2609
|
+
after?.();
|
|
2610
|
+
showNotice(success, "success");
|
|
2611
|
+
},
|
|
2612
|
+
(error) => {
|
|
2613
|
+
pending = false;
|
|
2614
|
+
fail(error);
|
|
2615
|
+
},
|
|
2616
|
+
);
|
|
2617
|
+
};
|
|
2555
2618
|
|
|
2556
2619
|
const container = new Container();
|
|
2557
|
-
container.addChild(new Text(theme.fg("accent", theme.bold(
|
|
2558
|
-
container.addChild(new Text(theme.fg("dim",
|
|
2620
|
+
container.addChild(new Text(theme.fg("accent", theme.bold(options.title))));
|
|
2621
|
+
container.addChild(new Text(theme.fg("dim", options.subtitle)));
|
|
2559
2622
|
container.addChild(editor);
|
|
2560
2623
|
container.addChild(notice);
|
|
2561
2624
|
container.addChild(new Text(theme.fg("dim", "Ctrl+S save · Enter newline · Ctrl+K clear · Ctrl+C copy · Esc close")));
|
|
@@ -2576,23 +2639,19 @@ export function createAdvisorSteerModal(
|
|
|
2576
2639
|
done(undefined);
|
|
2577
2640
|
return;
|
|
2578
2641
|
}
|
|
2642
|
+
if (pending) return;
|
|
2579
2643
|
if (matchesKey(data, "ctrl+s")) {
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2644
|
+
const text = editor.getExpandedText();
|
|
2645
|
+
const invalid = validate?.(text);
|
|
2646
|
+
if (invalid) {
|
|
2647
|
+
showNotice(invalid, "error");
|
|
2648
|
+
return;
|
|
2585
2649
|
}
|
|
2650
|
+
persist(() => onSave(text), "Saved", "Save failed");
|
|
2586
2651
|
return;
|
|
2587
2652
|
}
|
|
2588
2653
|
if (matchesKey(data, "ctrl+k")) {
|
|
2589
|
-
|
|
2590
|
-
onClear();
|
|
2591
|
-
editor.setText("");
|
|
2592
|
-
showNotice("Cleared", "success");
|
|
2593
|
-
} catch (error) {
|
|
2594
|
-
showNotice(`Clear failed: ${errorMessage(error)}`, "error");
|
|
2595
|
-
}
|
|
2654
|
+
persist(onClear, "Cleared", "Clear failed", () => editor.setText(""));
|
|
2596
2655
|
return;
|
|
2597
2656
|
}
|
|
2598
2657
|
if (matchesKey(data, "ctrl+c")) {
|
|
@@ -2616,6 +2675,83 @@ export function createAdvisorSteerModal(
|
|
|
2616
2675
|
};
|
|
2617
2676
|
}
|
|
2618
2677
|
|
|
2678
|
+
export function createAdvisorSteerModal(
|
|
2679
|
+
initialText: string,
|
|
2680
|
+
onSave: (text: string) => void,
|
|
2681
|
+
onClear: () => void,
|
|
2682
|
+
copy: (text: string) => Promise<void> = copyToClipboard,
|
|
2683
|
+
): ReturnType<typeof createTextEditorModal> {
|
|
2684
|
+
return createTextEditorModal({
|
|
2685
|
+
title: "Bro · advisor steer",
|
|
2686
|
+
subtitle: "One persistent steering brief the advisor always sees — never sent to the main model.",
|
|
2687
|
+
initialText,
|
|
2688
|
+
onSave,
|
|
2689
|
+
onClear,
|
|
2690
|
+
copy,
|
|
2691
|
+
});
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
function preferencesTooLong(text: string): string | undefined {
|
|
2695
|
+
const length = text.replace(/^\uFEFF/, "").trim().length;
|
|
2696
|
+
return length > MAX_PREFERENCES_CHARS
|
|
2697
|
+
? `${length.toLocaleString("en-US")}/${MAX_PREFERENCES_CHARS.toLocaleString("en-US")} characters — trim before saving`
|
|
2698
|
+
: undefined;
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2701
|
+
// Testable core for /bro preferences. A missing file opens with the unsaved starter text; an
|
|
2702
|
+
// unreadable or oversize-on-disk file opens empty with the error, and Clear still works.
|
|
2703
|
+
export function createPreferencesModal(
|
|
2704
|
+
loaded: { text?: string; error?: string },
|
|
2705
|
+
persist: { save: (text: string) => Promise<void>; clear: () => Promise<void> },
|
|
2706
|
+
copy?: (text: string) => Promise<void>,
|
|
2707
|
+
): ReturnType<typeof createTextEditorModal> {
|
|
2708
|
+
const starter = loaded.text === undefined && !loaded.error;
|
|
2709
|
+
return createTextEditorModal({
|
|
2710
|
+
title: "Bro · preferences",
|
|
2711
|
+
subtitle: "About you and how you like answers. Sent to the selected backend with every explain, show, and btw request — never to the advisor or Pi's main model.",
|
|
2712
|
+
initialText: starter ? STARTER_PREFERENCES : (loaded.text ?? ""),
|
|
2713
|
+
initialNotice: loaded.error ?? (starter ? "Starter text — not saved. Ctrl+S saves it; Esc leaves no file." : undefined),
|
|
2714
|
+
validate: preferencesTooLong,
|
|
2715
|
+
onSave: persist.save,
|
|
2716
|
+
onClear: persist.clear,
|
|
2717
|
+
copy,
|
|
2718
|
+
});
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
// Saves and deletes run one at a time, even across a closed and reopened editor, so a slow delete
|
|
2722
|
+
// from an earlier editor can never remove a file saved by a later one.
|
|
2723
|
+
let preferencesWrites: Promise<void> = Promise.resolve();
|
|
2724
|
+
export function queuePreferencesWrite(write: () => Promise<void>): Promise<void> {
|
|
2725
|
+
const next = preferencesWrites.then(write);
|
|
2726
|
+
preferencesWrites = next.catch(() => {});
|
|
2727
|
+
return next;
|
|
2728
|
+
}
|
|
2729
|
+
|
|
2730
|
+
export async function showPreferencesModal(ctx: ExtensionCommandContext): Promise<void> {
|
|
2731
|
+
if (!hasBroCustomUi(ctx)) {
|
|
2732
|
+
ctx.ui.notify(`Edit ${PREFERENCES_FILE} directly.`, "warning");
|
|
2733
|
+
return;
|
|
2734
|
+
}
|
|
2735
|
+
let loaded: { text?: string; error?: string };
|
|
2736
|
+
try {
|
|
2737
|
+
const text = await readPreferencesRaw();
|
|
2738
|
+
loaded = text === undefined ? {} : { text };
|
|
2739
|
+
} catch (error) {
|
|
2740
|
+
loaded = { error: `${errorMessage(error)} Ctrl+K deletes it.` };
|
|
2741
|
+
}
|
|
2742
|
+
await ctx.ui.custom<void>(
|
|
2743
|
+
createPreferencesModal(loaded, {
|
|
2744
|
+
// ponytail: last writer wins against external edits while the editor is open, like writeSettings.
|
|
2745
|
+
save: (text) => queuePreferencesWrite(() => writeFile(PREFERENCES_FILE, `${text.trim()}\n`, "utf8")),
|
|
2746
|
+
clear: () => queuePreferencesWrite(() => rm(PREFERENCES_FILE, { force: true })),
|
|
2747
|
+
}),
|
|
2748
|
+
{
|
|
2749
|
+
overlay: true,
|
|
2750
|
+
overlayOptions: { width: "78%", minWidth: 48, maxHeight: "60%", anchor: "top-center", margin: { top: 1, left: 2, right: 2 } },
|
|
2751
|
+
},
|
|
2752
|
+
);
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2619
2755
|
export async function showAdvisorSteerModal(ctx: ExtensionCommandContext, pi: ExtensionAPI): Promise<void> {
|
|
2620
2756
|
if (!hasBroCustomUi(ctx)) {
|
|
2621
2757
|
ctx.ui.notify("Use /bro advisor-steer in Pi's interactive UI.", "warning");
|
|
@@ -2635,7 +2771,9 @@ export async function showAdvisorSteerModal(ctx: ExtensionCommandContext, pi: Ex
|
|
|
2635
2771
|
);
|
|
2636
2772
|
}
|
|
2637
2773
|
|
|
2638
|
-
|
|
2774
|
+
// `preferences` is a ready status ("off", "on (312 characters)", or "error: …") so a preferences
|
|
2775
|
+
// problem never hides Help or mixes with settings errors.
|
|
2776
|
+
export function helpText(settings?: BroSettings, settingsError?: string, preferences = "off"): string {
|
|
2639
2777
|
const overrideLines = settings
|
|
2640
2778
|
? CAPABILITIES.map((capability) => {
|
|
2641
2779
|
const override = capabilityOverride(settings, capability);
|
|
@@ -2674,15 +2812,17 @@ Quick reference. The README is the full user guide: https://github.com/tranhoang
|
|
|
2674
2812
|
|
|
2675
2813
|
## Configure and check
|
|
2676
2814
|
|
|
2815
|
+
- \`/bro preferences\` — tell Bro about yourself and how you like answers; added to explain, show, and btw prompts, never the advisor (**Ctrl+S** save, **Ctrl+K** delete, **Ctrl+C** copy, **Esc** close)
|
|
2677
2816
|
- \`/bro config\` — shared default and per-capability (explain/show/btw/advisor) backend, model, and effort; explain mode; show turns. Changes save immediately.
|
|
2678
2817
|
- \`/bro mode [brief|balanced|faithful]\` — view or choose the explanation mode
|
|
2679
|
-
- \`/bro doctor\` — check settings,
|
|
2818
|
+
- \`/bro doctor\` — check settings, preferences, and every selected backend without running a model turn
|
|
2680
2819
|
|
|
2681
2820
|
## Current settings
|
|
2682
2821
|
|
|
2683
2822
|
${settingsSummary}
|
|
2823
|
+
- **Preferences:** ${preferences} — \`/bro preferences\`
|
|
2684
2824
|
|
|
2685
|
-
Saved in \`${SETTINGS_FILE}\`.
|
|
2825
|
+
Saved in \`${SETTINGS_FILE}\` and \`${PREFERENCES_FILE}\`.
|
|
2686
2826
|
|
|
2687
2827
|
## Explanation modes
|
|
2688
2828
|
|
|
@@ -2690,19 +2830,22 @@ Saved in \`${SETTINGS_FILE}\`.
|
|
|
2690
2830
|
- balanced — default; material detail with clearer structure
|
|
2691
2831
|
- faithful — closest to the source, with no fixed word limit
|
|
2692
2832
|
|
|
2693
|
-
|
|
2833
|
+
Press **M** in an explanation to re-simplify it in the next mode without changing the saved one.
|
|
2834
|
+
|
|
2835
|
+
The mode decides how much of the source to keep; your preferences decide who it's written for, its tone, and its language. Neither overrides Bro's source rules.
|
|
2694
2836
|
|
|
2695
2837
|
## Controls
|
|
2696
2838
|
|
|
2697
2839
|
- **Mouse wheel / trackpad**, **↑ / ↓** — scroll
|
|
2698
2840
|
- **C** — copy the full explanation
|
|
2699
|
-
- **R** — repeat the current action
|
|
2841
|
+
- **R** — repeat the current action (an explanation keeps its mode)
|
|
2842
|
+
- **M** — re-simplify in the next mode (brief → balanced → faithful); not saved
|
|
2700
2843
|
- **O** — open the HTML diagram when a show reply contains one
|
|
2701
2844
|
- **Esc** — close, or cancel while Bro is working
|
|
2702
2845
|
|
|
2703
2846
|
## Privacy
|
|
2704
2847
|
|
|
2705
|
-
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.`;
|
|
2848
|
+
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. Your preferences go with every explain, show, and btw request. Nothing is added to Pi's conversation unless you insert it. Access controls differ by backend; see the README.`;
|
|
2706
2849
|
}
|
|
2707
2850
|
|
|
2708
2851
|
// The overlay framing pattern is adapted from pi-btw (MIT); see THIRD_PARTY_NOTICES.md.
|
|
@@ -2721,6 +2864,10 @@ class BroModal implements Focusable {
|
|
|
2721
2864
|
private retryable = false;
|
|
2722
2865
|
private disposed = false;
|
|
2723
2866
|
private htmlPath = "";
|
|
2867
|
+
// Survives loading/streaming so the header names the mode being produced; empty disables M.
|
|
2868
|
+
private modeLabel = "";
|
|
2869
|
+
// Set only with a result, so the tag never claims preferences for work still in flight.
|
|
2870
|
+
private preferencesUsed = false;
|
|
2724
2871
|
|
|
2725
2872
|
constructor(
|
|
2726
2873
|
private readonly tui: TuiLike,
|
|
@@ -2729,6 +2876,7 @@ class BroModal implements Focusable {
|
|
|
2729
2876
|
private readonly onRetry: () => void,
|
|
2730
2877
|
private readonly onDispose: () => void,
|
|
2731
2878
|
private readonly retryLabel: string,
|
|
2879
|
+
private readonly onSwitchMode: () => void = () => {},
|
|
2732
2880
|
) {
|
|
2733
2881
|
setRegularMouseReporting(this.tui, true);
|
|
2734
2882
|
}
|
|
@@ -2750,6 +2898,16 @@ class BroModal implements Focusable {
|
|
|
2750
2898
|
this.tui.requestRender();
|
|
2751
2899
|
}
|
|
2752
2900
|
|
|
2901
|
+
setMode(mode: BroMode | undefined): void {
|
|
2902
|
+
this.modeLabel = mode ?? "";
|
|
2903
|
+
this.tui.requestRender();
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2906
|
+
setPreferences(used: boolean): void {
|
|
2907
|
+
this.preferencesUsed = used;
|
|
2908
|
+
this.tui.requestRender();
|
|
2909
|
+
}
|
|
2910
|
+
|
|
2753
2911
|
setStatic(kind: "help" | "empty", text: string, copyable: boolean): void {
|
|
2754
2912
|
this.setContent(kind, text, text, copyable, false);
|
|
2755
2913
|
}
|
|
@@ -2800,11 +2958,16 @@ class BroModal implements Focusable {
|
|
|
2800
2958
|
return this.theme.fg("border", `├${"─".repeat(innerWidth)}┤`);
|
|
2801
2959
|
}
|
|
2802
2960
|
|
|
2961
|
+
private canSwitchMode(): boolean {
|
|
2962
|
+
return Boolean(this.modeLabel) && (this.kind === "loading" || this.kind === "streaming" || this.kind === "result");
|
|
2963
|
+
}
|
|
2964
|
+
|
|
2803
2965
|
private controls(): string {
|
|
2804
|
-
|
|
2805
|
-
if (this.kind === "
|
|
2966
|
+
const mode = this.canSwitchMode() ? " · M mode" : "";
|
|
2967
|
+
if (this.kind === "loading") return mode ? "M mode · Esc cancel" : "Esc cancel";
|
|
2968
|
+
if (this.kind === "streaming") return `Simplifying… · ↑/↓ scroll${mode} · Esc cancel`;
|
|
2806
2969
|
if (this.kind === "result") {
|
|
2807
|
-
return `↑/↓ scroll · C copy${this.htmlPath ? " · O open diagram" : ""}${this.retryable ? ` · R ${this.retryLabel}` : ""} · Esc close`;
|
|
2970
|
+
return `↑/↓ scroll · C copy${this.htmlPath ? " · O open diagram" : ""}${mode}${this.retryable ? ` · R ${this.retryLabel}` : ""} · Esc close`;
|
|
2808
2971
|
}
|
|
2809
2972
|
if (this.kind === "help") return "↑/↓ scroll · C copy · Esc close";
|
|
2810
2973
|
if (this.kind === "error") return "R try again · Esc close";
|
|
@@ -2830,7 +2993,7 @@ class BroModal implements Focusable {
|
|
|
2830
2993
|
this.borderLine(innerWidth, "top"),
|
|
2831
2994
|
this.frameLine(
|
|
2832
2995
|
this.theme.fg("accent", this.theme.bold(`Bro${this.sourceLabel ? ` · ${this.sourceLabel}` : ""}`)) +
|
|
2833
|
-
this.theme.fg("dim", `${this.modelLabel ? ` · ${this.modelLabel}` : ""}${scroll}`),
|
|
2996
|
+
this.theme.fg("dim", `${this.modelLabel ? ` · ${this.modelLabel}` : ""}${this.modeLabel ? ` · ${this.modeLabel}` : ""}${this.preferencesUsed ? " · prefs" : ""}${scroll}`),
|
|
2834
2997
|
innerWidth,
|
|
2835
2998
|
),
|
|
2836
2999
|
this.ruleLine(innerWidth),
|
|
@@ -2889,6 +3052,11 @@ class BroModal implements Focusable {
|
|
|
2889
3052
|
return;
|
|
2890
3053
|
}
|
|
2891
3054
|
|
|
3055
|
+
if ((matchesKey(data, "m") || matchesKey(data, "shift+m")) && this.canSwitchMode()) {
|
|
3056
|
+
this.onSwitchMode();
|
|
3057
|
+
return;
|
|
3058
|
+
}
|
|
3059
|
+
|
|
2892
3060
|
if ((matchesKey(data, "o") || matchesKey(data, "shift+o")) && this.htmlPath && this.kind === "result") {
|
|
2893
3061
|
this.notice = openShowHtml(this.htmlPath)
|
|
2894
3062
|
? "Opening diagram"
|
|
@@ -2914,6 +3082,7 @@ interface BroModalOptions {
|
|
|
2914
3082
|
signal: AbortSignal,
|
|
2915
3083
|
source?: BroSource,
|
|
2916
3084
|
onProgress?: (text: string) => void,
|
|
3085
|
+
mode?: BroMode,
|
|
2917
3086
|
) => Promise<ModalResult>;
|
|
2918
3087
|
onResult?: (result: ModalResult) => void;
|
|
2919
3088
|
loadingText?: string;
|
|
@@ -2935,7 +3104,8 @@ async function showBroModal(ctx: ExtensionCommandContext, options: BroModalOptio
|
|
|
2935
3104
|
let closed = false;
|
|
2936
3105
|
let controller: AbortController | undefined;
|
|
2937
3106
|
let current = options.result;
|
|
2938
|
-
let
|
|
3107
|
+
let inFlightMode: BroMode | undefined;
|
|
3108
|
+
let execute: (source?: BroSource, mode?: BroMode, notice?: string) => void = () => {};
|
|
2939
3109
|
|
|
2940
3110
|
const close = () => {
|
|
2941
3111
|
if (closed) return;
|
|
@@ -2948,37 +3118,51 @@ async function showBroModal(ctx: ExtensionCommandContext, options: BroModalOptio
|
|
|
2948
3118
|
tui,
|
|
2949
3119
|
theme,
|
|
2950
3120
|
close,
|
|
2951
|
-
() => execute(current?.source),
|
|
3121
|
+
() => execute(current?.source, current?.mode),
|
|
2952
3122
|
() => {
|
|
2953
3123
|
closed = true;
|
|
2954
3124
|
controller?.abort();
|
|
2955
3125
|
},
|
|
2956
3126
|
options.retryLabel ?? "simplify again",
|
|
3127
|
+
// M advances from the mode in flight, so repeated presses cancel and skip ahead; the saved default is untouched.
|
|
3128
|
+
() => {
|
|
3129
|
+
const from = inFlightMode ?? current?.mode;
|
|
3130
|
+
if (!from || !current?.source || closed) return;
|
|
3131
|
+
const next = nextBroMode(from);
|
|
3132
|
+
controller?.abort();
|
|
3133
|
+
controller = undefined;
|
|
3134
|
+
execute(current.source, next, `Mode: ${next} (not saved)`);
|
|
3135
|
+
},
|
|
2957
3136
|
);
|
|
2958
3137
|
|
|
2959
3138
|
const present = (result: ModalResult, notice = "") => {
|
|
2960
3139
|
const display = result.htmlPath ? stripShowHtmlFence(result.text) : result.text;
|
|
2961
3140
|
modal.setResult(display, options.retryable ?? Boolean(options.run), notice, result.source?.label, result.text, result.model);
|
|
2962
3141
|
modal.setHtmlPath(result.htmlPath ?? "");
|
|
3142
|
+
modal.setMode(result.mode);
|
|
3143
|
+
modal.setPreferences(Boolean(result.preferences));
|
|
2963
3144
|
};
|
|
2964
3145
|
|
|
2965
|
-
execute = (source?: BroSource) => {
|
|
3146
|
+
execute = (source?: BroSource, mode?: BroMode, notice = "") => {
|
|
2966
3147
|
if (!options.run || controller || closed) return;
|
|
2967
3148
|
const previous = current;
|
|
2968
3149
|
const nextController = new AbortController();
|
|
2969
3150
|
controller = nextController;
|
|
3151
|
+
inFlightMode = mode;
|
|
2970
3152
|
modal.setLoading(options.loadingText);
|
|
3153
|
+
modal.setMode(mode);
|
|
3154
|
+
modal.setPreferences(false);
|
|
2971
3155
|
|
|
2972
3156
|
void options
|
|
2973
3157
|
.run(nextController.signal, source, (text) => {
|
|
2974
3158
|
if (closed || nextController.signal.aborted || controller !== nextController) return;
|
|
2975
3159
|
modal.setStreaming(text);
|
|
2976
|
-
})
|
|
3160
|
+
}, mode)
|
|
2977
3161
|
.then((result) => {
|
|
2978
3162
|
if (closed || nextController.signal.aborted) return;
|
|
2979
3163
|
current = result;
|
|
2980
3164
|
options.onResult?.(result);
|
|
2981
|
-
present(result);
|
|
3165
|
+
present(result, notice);
|
|
2982
3166
|
})
|
|
2983
3167
|
.catch((error) => {
|
|
2984
3168
|
if (closed || nextController.signal.aborted) return;
|
|
@@ -2987,11 +3171,15 @@ async function showBroModal(ctx: ExtensionCommandContext, options: BroModalOptio
|
|
|
2987
3171
|
current = previous;
|
|
2988
3172
|
present(previous, `Retry failed: ${message}`);
|
|
2989
3173
|
} else {
|
|
3174
|
+
modal.setMode(undefined);
|
|
2990
3175
|
modal.setError(message);
|
|
2991
3176
|
}
|
|
2992
3177
|
})
|
|
2993
3178
|
.finally(() => {
|
|
2994
|
-
if (controller === nextController)
|
|
3179
|
+
if (controller === nextController) {
|
|
3180
|
+
controller = undefined;
|
|
3181
|
+
inFlightMode = undefined;
|
|
3182
|
+
}
|
|
2995
3183
|
});
|
|
2996
3184
|
};
|
|
2997
3185
|
|
|
@@ -3038,7 +3226,7 @@ export function resolveBtwThread(existing: BtwThread | undefined): BtwThread {
|
|
|
3038
3226
|
|
|
3039
3227
|
export function bindBtwBackend(thread: BtwThread, backend: BackendName): boolean {
|
|
3040
3228
|
const changed = thread.backend !== undefined && thread.backend !== backend;
|
|
3041
|
-
if (changed) { thread.turns = []; thread.conversationId = undefined; thread.context = undefined; thread.sessionFull = undefined; }
|
|
3229
|
+
if (changed) { thread.turns = []; thread.conversationId = undefined; thread.context = undefined; thread.sessionFull = undefined; thread.sessionPreferences = undefined; }
|
|
3042
3230
|
thread.backend = backend;
|
|
3043
3231
|
return changed;
|
|
3044
3232
|
}
|
|
@@ -3057,13 +3245,20 @@ export function toggleBtwMode(thread: BtwThread): void {
|
|
|
3057
3245
|
// /mode switches. An Agy conversation stays bound to the workspace it started in (a sandbox scratch
|
|
3058
3246
|
// dir vs. the repo), so an access change drops it; the next turn reseeds a fresh native session
|
|
3059
3247
|
// with the main-session context and the whole thread.
|
|
3060
|
-
|
|
3248
|
+
// A native session remembers every earlier prompt, including old preferences, so a preferences
|
|
3249
|
+
// change (or deletion) on any backend starts a fresh session reseeded with the quoted thread.
|
|
3250
|
+
export function nativeBtwContinuation(thread: BtwThread, backend: BackendName, preferences = ""): string | undefined {
|
|
3061
3251
|
if (backend === "agy" && thread.conversationId && thread.sessionFull !== undefined && thread.sessionFull !== thread.full) {
|
|
3062
3252
|
thread.conversationId = undefined;
|
|
3063
3253
|
}
|
|
3254
|
+
if (thread.conversationId && (thread.sessionPreferences ?? "") !== preferences) thread.conversationId = undefined;
|
|
3064
3255
|
return thread.conversationId;
|
|
3065
3256
|
}
|
|
3066
3257
|
|
|
3258
|
+
export function btwHeaderLabel(thread: Pick<BtwThread, "model" | "preferences">): string {
|
|
3259
|
+
return thread.model ? `${thread.model}${thread.preferences ? " · prefs" : ""}` : "";
|
|
3260
|
+
}
|
|
3261
|
+
|
|
3067
3262
|
export function formatBtwTranscript(turns: readonly BtwTurn[]): string {
|
|
3068
3263
|
return turns
|
|
3069
3264
|
.map((turn) => {
|
|
@@ -3320,7 +3515,8 @@ async function openBtwModal(
|
|
|
3320
3515
|
controller?.abort();
|
|
3321
3516
|
});
|
|
3322
3517
|
|
|
3323
|
-
|
|
3518
|
+
// `replaced` is the turn /retry removed; it is put back if the turn never starts.
|
|
3519
|
+
const runTurn = async (question: string, replaced?: BtwTurn) => {
|
|
3324
3520
|
if (controller) return;
|
|
3325
3521
|
const turnController = new AbortController();
|
|
3326
3522
|
controller = turnController;
|
|
@@ -3328,13 +3524,23 @@ async function openBtwModal(
|
|
|
3328
3524
|
modal.clearComposer();
|
|
3329
3525
|
|
|
3330
3526
|
let settings: BroSettings;
|
|
3527
|
+
let preferences: string;
|
|
3331
3528
|
try {
|
|
3332
3529
|
settings = await readSettings();
|
|
3333
|
-
|
|
3334
|
-
if (bindBtwBackend(thread, backend)) modal.setNotice("Backend changed — started a fresh side thread.");
|
|
3530
|
+
preferences = await readPreferences();
|
|
3335
3531
|
} catch (error) {
|
|
3336
|
-
|
|
3532
|
+
if (replaced) thread.turns.push(replaced);
|
|
3533
|
+
if (controller === turnController) controller = undefined;
|
|
3534
|
+
if (!closed) { modal.setRunning(false); modal.setNotice(errorMessage(error)); }
|
|
3535
|
+
return;
|
|
3337
3536
|
}
|
|
3537
|
+
// Closing (or reopening) during the reads above must not touch the shared thread.
|
|
3538
|
+
if (closed || turnController.signal.aborted) {
|
|
3539
|
+
if (replaced) thread.turns.push(replaced);
|
|
3540
|
+
if (controller === turnController) controller = undefined;
|
|
3541
|
+
return;
|
|
3542
|
+
}
|
|
3543
|
+
if (bindBtwBackend(thread, capabilityBackend(settings, "btw"))) modal.setNotice("Backend changed — started a fresh side thread.");
|
|
3338
3544
|
if (thread.turns.length === 0) {
|
|
3339
3545
|
thread.conversationId = undefined;
|
|
3340
3546
|
const context = captureShowTranscript(ctx, BTW_CONTEXT_TURNS, BTW_CONTEXT_MAX)?.text;
|
|
@@ -3342,7 +3548,7 @@ async function openBtwModal(
|
|
|
3342
3548
|
}
|
|
3343
3549
|
// Resume natively when possible; otherwise seed the fresh native session with the main-session
|
|
3344
3550
|
// context plus every earlier turn so nothing is silently lost.
|
|
3345
|
-
const conversationId = nativeBtwContinuation(thread, capabilityBackend(settings, "btw"));
|
|
3551
|
+
const conversationId = nativeBtwContinuation(thread, capabilityBackend(settings, "btw"), preferences);
|
|
3346
3552
|
const history = !conversationId && thread.turns.length ? formatBtwTranscript(thread.turns) : undefined;
|
|
3347
3553
|
const context = conversationId ? undefined : thread.context;
|
|
3348
3554
|
const full = thread.full;
|
|
@@ -3353,9 +3559,10 @@ async function openBtwModal(
|
|
|
3353
3559
|
try {
|
|
3354
3560
|
const selection = selectionForCapability(settings, "btw");
|
|
3355
3561
|
thread.model = selectionLabel(selection);
|
|
3356
|
-
|
|
3562
|
+
thread.preferences = Boolean(preferences);
|
|
3563
|
+
modal.setModel(btwHeaderLabel(thread));
|
|
3357
3564
|
const result = await runBtwTurn(
|
|
3358
|
-
buildBtwPrompt(context, question, { full, history }),
|
|
3565
|
+
buildBtwPrompt(context, question, { full, history, preferences }),
|
|
3359
3566
|
selection,
|
|
3360
3567
|
{ full, cwd: ctx.cwd, conversationId },
|
|
3361
3568
|
turnController.signal,
|
|
@@ -3369,6 +3576,7 @@ async function openBtwModal(
|
|
|
3369
3576
|
if (result.conversationId) {
|
|
3370
3577
|
thread.conversationId = result.conversationId;
|
|
3371
3578
|
thread.sessionFull = full;
|
|
3579
|
+
thread.sessionPreferences = preferences;
|
|
3372
3580
|
}
|
|
3373
3581
|
thread.turns[thread.turns.length - 1]!.answer = result.text;
|
|
3374
3582
|
} catch (error) {
|
|
@@ -3394,6 +3602,8 @@ async function openBtwModal(
|
|
|
3394
3602
|
thread.turns = [];
|
|
3395
3603
|
thread.conversationId = undefined;
|
|
3396
3604
|
thread.context = undefined;
|
|
3605
|
+
thread.sessionFull = undefined;
|
|
3606
|
+
thread.sessionPreferences = undefined;
|
|
3397
3607
|
modal.clearComposer();
|
|
3398
3608
|
modal.setNotice("");
|
|
3399
3609
|
modal.setText("");
|
|
@@ -3407,7 +3617,7 @@ async function openBtwModal(
|
|
|
3407
3617
|
return;
|
|
3408
3618
|
}
|
|
3409
3619
|
thread.turns.pop();
|
|
3410
|
-
void runTurn(last.question);
|
|
3620
|
+
void runTurn(last.question, last);
|
|
3411
3621
|
};
|
|
3412
3622
|
|
|
3413
3623
|
const copyOut = async (all: boolean) => {
|
|
@@ -3479,7 +3689,7 @@ async function openBtwModal(
|
|
|
3479
3689
|
}
|
|
3480
3690
|
|
|
3481
3691
|
modal.setFull(thread.full);
|
|
3482
|
-
modal.setModel(thread
|
|
3692
|
+
modal.setModel(btwHeaderLabel(thread));
|
|
3483
3693
|
modal.setText(transcript());
|
|
3484
3694
|
|
|
3485
3695
|
if (options.initialQuestion) void runTurn(options.initialQuestion);
|
|
@@ -3674,15 +3884,14 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
3674
3884
|
if (!captured) {
|
|
3675
3885
|
return { text: "**Nothing to show yet**\n\nThis session has no conversation turns to draw. Run something first, then press **R**." };
|
|
3676
3886
|
}
|
|
3677
|
-
let
|
|
3678
|
-
let model: string;
|
|
3887
|
+
let result: Awaited<ReturnType<typeof runShowExplanation>>;
|
|
3679
3888
|
try {
|
|
3680
|
-
|
|
3889
|
+
result = await runShowExplanation(captured.text, steering, signal, await readSettings(), onProgress);
|
|
3681
3890
|
} catch (error) {
|
|
3682
3891
|
throw new Error(withDoctor(error));
|
|
3683
3892
|
}
|
|
3684
|
-
const html = extractShowHtml(text);
|
|
3685
|
-
return { source: captured,
|
|
3893
|
+
const html = extractShowHtml(result.text);
|
|
3894
|
+
return { source: captured, ...result, ...(html ? { htmlPath: await writeShowHtml(html) } : {}) };
|
|
3686
3895
|
};
|
|
3687
3896
|
try {
|
|
3688
3897
|
await showBroModal(ctx, {
|
|
@@ -3706,6 +3915,7 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
3706
3915
|
signal: AbortSignal,
|
|
3707
3916
|
source?: BroSource,
|
|
3708
3917
|
onProgress?: (text: string) => void,
|
|
3918
|
+
mode?: BroMode,
|
|
3709
3919
|
): Promise<BroResult> => {
|
|
3710
3920
|
const target = source ?? (action === "url"
|
|
3711
3921
|
? await extractWebPage(value, signal)
|
|
@@ -3713,7 +3923,7 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
3713
3923
|
try {
|
|
3714
3924
|
return {
|
|
3715
3925
|
source: target,
|
|
3716
|
-
...(await simplify(target.text, signal, await readSettings(), onProgress)),
|
|
3926
|
+
...(await simplify(target.text, signal, await readSettings(), onProgress, mode)),
|
|
3717
3927
|
};
|
|
3718
3928
|
} catch (error) {
|
|
3719
3929
|
throw new Error(withDoctor(error));
|
|
@@ -3839,6 +4049,15 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
3839
4049
|
return;
|
|
3840
4050
|
}
|
|
3841
4051
|
|
|
4052
|
+
if (action === "preferences") {
|
|
4053
|
+
if (parts.length !== 1) {
|
|
4054
|
+
ctx.ui.notify("Use /bro preferences.", "warning");
|
|
4055
|
+
return;
|
|
4056
|
+
}
|
|
4057
|
+
await showPreferencesModal(ctx);
|
|
4058
|
+
return;
|
|
4059
|
+
}
|
|
4060
|
+
|
|
3842
4061
|
if (action === "help") {
|
|
3843
4062
|
if (parts.length !== 1) {
|
|
3844
4063
|
ctx.ui.notify("Use /bro help.", "warning");
|
|
@@ -3851,7 +4070,14 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
3851
4070
|
} catch (error) {
|
|
3852
4071
|
settingsError = errorMessage(error);
|
|
3853
4072
|
}
|
|
3854
|
-
|
|
4073
|
+
let preferences: string;
|
|
4074
|
+
try {
|
|
4075
|
+
const text = await readPreferences();
|
|
4076
|
+
preferences = text ? `on (${text.length.toLocaleString("en-US")} characters)` : "off";
|
|
4077
|
+
} catch (error) {
|
|
4078
|
+
preferences = `error: ${errorMessage(error)}`;
|
|
4079
|
+
}
|
|
4080
|
+
await showBroModal(ctx, { text: helpText(settings, settingsError, preferences), kind: "help", copyable: true });
|
|
3855
4081
|
return;
|
|
3856
4082
|
}
|
|
3857
4083
|
|
|
@@ -3859,6 +4085,7 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
3859
4085
|
signal: AbortSignal,
|
|
3860
4086
|
source?: BroSource,
|
|
3861
4087
|
onProgress?: (text: string) => void,
|
|
4088
|
+
mode?: BroMode,
|
|
3862
4089
|
): Promise<BroResult> => {
|
|
3863
4090
|
let target = source ?? (action === "text" && value ? { text: value } : undefined);
|
|
3864
4091
|
if (!target) {
|
|
@@ -3870,7 +4097,7 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
3870
4097
|
const settings = await readSettings();
|
|
3871
4098
|
return {
|
|
3872
4099
|
source: target,
|
|
3873
|
-
...(await simplify(target.text, signal, settings, onProgress)),
|
|
4100
|
+
...(await simplify(target.text, signal, settings, onProgress, mode)),
|
|
3874
4101
|
};
|
|
3875
4102
|
} catch (error) {
|
|
3876
4103
|
throw new Error(withDoctor(error));
|