pi-bro 0.27.0 → 0.29.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 +18 -0
- package/docs/reference/commands.md +13 -4
- package/docs/reference/settings.md +34 -3
- package/docs/user-guide.md +8 -0
- package/package.json +6 -2
- package/src/bro.ts +35 -42
- package/src/config-navigation-ui.ts +110 -0
- package/src/config-navigation.ts +74 -0
- package/src/config-ui.ts +25 -15
- package/src/doctor.ts +53 -26
- package/src/explain-modal.ts +4 -3
- package/src/simplify-engine.ts +37 -0
- package/src/simplify-ui.ts +156 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to pi-bro are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.29.0] - 2026-11-05
|
|
6
|
+
|
|
7
|
+
### Highlights
|
|
8
|
+
|
|
9
|
+
- The default Bro completion menu shows eight capability-level actions while preserving all explicit command routes.
|
|
10
|
+
- `/bro config` now offers Global (default, Preferences and on-demand Doctor) and Features navigation.
|
|
11
|
+
- Feature pages reuse the existing model pickers and save logic; Elevate, Grill Me and Remember Me display inherited BTW settings without new overrides.
|
|
12
|
+
|
|
13
|
+
## [0.28.0] - 2026-11-05
|
|
14
|
+
|
|
15
|
+
### Highlights
|
|
16
|
+
|
|
17
|
+
- Simplify gains a source-focused modal with internal /simplify, /text, /file, /url, /copy, /clear and /help commands.
|
|
18
|
+
- Bare /bro captures a fresh latest assistant response; /bro simplify reopens the last Simplify view without a new model call.
|
|
19
|
+
- M changes explanation mode for that source without rewriting the saved default; R retries the same source.
|
|
20
|
+
- Existing explicit top-level source forms and backend selection remain compatible.
|
|
21
|
+
|
|
22
|
+
|
|
5
23
|
## [0.27.0] - 2026-10-30
|
|
6
24
|
|
|
7
25
|
### Highlights
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
| Source | Command | What Bro does |
|
|
8
8
|
| --- | --- | --- |
|
|
9
|
-
| Latest assistant reply | `/bro` |
|
|
9
|
+
| Latest assistant reply | `/bro` | Opens Simplify and explains the latest completed reply without inserting the result into Pi's conversation. |
|
|
10
10
|
| Pasted text | `/bro text <text>` | Explains text supplied directly in the command. |
|
|
11
11
|
| Local document | `/bro file <path>` | Extracts text from a workspace-local Markdown, text, PDF, or DOCX file. |
|
|
12
12
|
| Public webpage | `/bro url <url>` | Fetches one public HTML page and extracts its main readable content. |
|
|
@@ -18,21 +18,26 @@ Pressing **R** simplifies the captured source again in the mode shown, and
|
|
|
18
18
|
new source: `/bro text`, `/bro file`, `/bro url`, and `/bro show`. Giving `/bro` a URL, path, or
|
|
19
19
|
text directly captures a new source the same way.
|
|
20
20
|
|
|
21
|
+
## Default command suggestions
|
|
22
|
+
|
|
23
|
+
Typing `/bro ` displays eight task-level suggestions: `simplify`, `show`, `btw`, `guided-review`, `elevate`, `grill-me`, `remember-me`, and `config`. Every other documented command remains callable and discoverable by typing its prefix (e.g., `/bro doc`).
|
|
24
|
+
|
|
21
25
|
## Commands
|
|
22
26
|
|
|
23
27
|
| Command | Description |
|
|
24
28
|
| --- | --- |
|
|
25
|
-
| `/bro
|
|
29
|
+
| `/bro` | Open Simplify and explain the latest completed assistant reply. |
|
|
30
|
+
| `/bro simplify` | Reopen the last Simplify screen within this Pi session, without a new model call. |
|
|
26
31
|
| `/bro text <text>` | Explain pasted text. |
|
|
27
32
|
| `/bro <input>` | Explain it directly: a lone URL runs the webpage reader, an existing workspace file with a supported extension runs the document reader, and anything else is pasted text. A quoted path with spaces is routed too when the file exists. |
|
|
28
33
|
| `/bro file <path>` | Explain a workspace-local `.md`, `.markdown`, `.txt`, `.pdf`, or `.docx` file. |
|
|
29
34
|
| `/bro url <url>` | Explain one public, text-based webpage. |
|
|
30
|
-
| `/bro open` |
|
|
35
|
+
| `/bro open` | Legacy compatibility shortcut to the last result. No `/open` command inside Simplify. |
|
|
31
36
|
| `/bro show [n-turns] [query]` | Draw recent session turns (default last 1) as shapes instead of prose, from user and assistant conversation text only — tool calls, tool results, reasoning, and images are omitted. An optional query steers what the shapes focus on, with or without a leading turn count. |
|
|
32
37
|
| `/bro doctor` | Check Bro's settings, preferences, and each selected backend, with the effective backend/model/effort per feature. |
|
|
33
38
|
| `/bro mode [brief\|balanced\|faithful]` | View or choose the explanation mode. |
|
|
34
39
|
| `/bro preferences` | View or edit what Bro knows about you and how you like answers; see [Preferences](settings.md#preferences). |
|
|
35
|
-
| `/bro config` | Open
|
|
40
|
+
| `/bro config` | Open Global (defaults, preferences and Doctor) or Features (per-feature overrides, BTW inheritance and Advisor steering). |
|
|
36
41
|
| `/bro guided-review <PR number or URL>` | Automatically explain and assess a captured PR in local Pi, with source evidence and optional private questions. Opening the same PR restores its saved review. |
|
|
37
42
|
| `/bro guided-review` | List saved reviews; `resume` is an alias. |
|
|
38
43
|
| `/bro remember-me [thought]` | Start a private memory-writing conversation, browse and expand memories, and approve saves explicitly. [Guide](../remember-me.md). |
|
|
@@ -50,3 +55,7 @@ Giving `/bro` the input directly works the same way:
|
|
|
50
55
|
/bro docs/report.pdf
|
|
51
56
|
/bro any other text is explained as pasted text
|
|
52
57
|
```
|
|
58
|
+
|
|
59
|
+
## Simplify modal (single-source explanation)
|
|
60
|
+
|
|
61
|
+
Once inside Simplify, use `/simplify` for the latest completed reply; `/text <text>`, `/file <path>`, `/url <url>` for a new source; `/copy` to copy the current explanation; `/clear` to return to the empty screen; and `/help` for controls. **M** changes the one-off Brief/Balanced/Faithful mode and re-explains the captured source; **R** retries the same source. This modal is not a conversation and does not persist history across restarts. Legacy `/bro text|file|url` shortcuts keep working. The global menu consolidation is tracked separately.
|
|
@@ -4,9 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
## Settings
|
|
6
6
|
|
|
7
|
-
Use `/bro config`
|
|
8
|
-
and any per-capability (explain/show/btw/advisor/review) overrides, the explanation
|
|
9
|
-
mode, and the default show turn count. Changes save immediately. Esc inside a
|
|
7
|
+
Use `/bro config` for the layered Global and Features menu. Its editable pages retain the existing backend/model/effort pickers, save checks and per-capability overrides. Changes save immediately. Esc inside a
|
|
10
8
|
picker cancels that pick; Esc on the settings screen closes it, keeping
|
|
11
9
|
whatever was already saved. If the terminal becomes too small, config blocks
|
|
12
10
|
hidden actions; Esc closes config even if a picker was open, after pending saves
|
|
@@ -15,6 +13,39 @@ alone makes config too small, any other key clears it so you can try again. `/br
|
|
|
15
13
|
tries another mode without saving it), and `/bro help` shows the active settings
|
|
16
14
|
and file path.
|
|
17
15
|
|
|
16
|
+
### Config navigation
|
|
17
|
+
|
|
18
|
+
```text
|
|
19
|
+
/bro config
|
|
20
|
+
├── Global
|
|
21
|
+
│ ├── Default backend, model and effort
|
|
22
|
+
│ ├── Preferences (opens the existing shared editor)
|
|
23
|
+
│ └── Doctor (runs only when selected)
|
|
24
|
+
└── Features
|
|
25
|
+
├── Simplify — Explain override and default brief/balanced/faithful mode
|
|
26
|
+
├── Show — Show override and default turn count
|
|
27
|
+
├── BTW — backend/model/effort override
|
|
28
|
+
├── Guided Review — Review override
|
|
29
|
+
├── Advisor — override and persistent steering editor
|
|
30
|
+
├── Elevate — inherits BTW (informational)
|
|
31
|
+
├── Grill Me — inherits BTW (informational)
|
|
32
|
+
└── Remember Me — inherits BTW (informational)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The BTW page shows what the selected backend can do in conversation-only mode,
|
|
36
|
+
and reminds users that `/mode` **inside BTW** switches to full workspace access.
|
|
37
|
+
Guided Review shows its selected backend's real inspection limits: strict file
|
|
38
|
+
tools on some backends, sandboxed shell on others, and Agy's ability to write
|
|
39
|
+
within the captured checkout. These are explanations of existing behavior,
|
|
40
|
+
not a new permission switch or an extra compatibility restriction. See
|
|
41
|
+
[backend access and retention](data-and-permissions.md#backends-access-and-retention).
|
|
42
|
+
|
|
43
|
+
Enter drills down. Esc backs out or closes at root; editors use their existing
|
|
44
|
+
save/cancel behavior. Opening Config does **not** run Doctor, touch a model, or
|
|
45
|
+
modify settings. Current selected models and overrides remain visible in the
|
|
46
|
+
existing editing pages. The old `/bro doctor`, `/bro preferences` and
|
|
47
|
+
`/bro advisor-steer` shortcuts remain callable.
|
|
48
|
+
|
|
18
49
|
Elevate inherits the **BTW** backend/model/effort selection. There is no separate Elevate setting in this release. See the [Elevate guide](../elevate.md).
|
|
19
50
|
|
|
20
51
|
Settings live in this user-editable file, created when the extension loads
|
package/docs/user-guide.md
CHANGED
|
@@ -163,3 +163,11 @@ See [Privacy and safety](reference/data-and-permissions.md#privacy-and-safety).
|
|
|
163
163
|
## Troubleshooting and current limits
|
|
164
164
|
|
|
165
165
|
See [Troubleshooting and current limits](reference/troubleshooting.md#troubleshooting-and-current-limits).
|
|
166
|
+
|
|
167
|
+
## Simplify modal
|
|
168
|
+
|
|
169
|
+
`/bro` opens Simplify and explains the latest completed assistant reply. `/bro simplify` returns to the most recent Simplify screen without another model request. Inside, `/simplify`, `/text <text>`, `/file <path>` and `/url <url>` produce one explanation at a time; `/copy` copies the answer, `/clear` clears it, and `/help` shows controls. Press **M** with an empty composer to change the explanation mode and rerun; **R** retries. The saved default mode remains unchanged. The current screen is kept only within the Pi session. Old `/bro text|file|url|open` forms remain callable for compatibility.
|
|
170
|
+
|
|
171
|
+
## Bro command discovery and Config navigation
|
|
172
|
+
|
|
173
|
+
The default `/bro ` completion menu highlights eight tasks: `simplify`, `show`, `btw`, `guided-review`, `elevate`, `grill-me`, `remember-me`, `config`. Existing explicit commands, including `/bro doctor`, `/bro preferences`, `/bro advisor-steer`, `/bro file`, `/bro url`, `/bro text`, `/bro open` and `/bro help`, remain valid and discoverable by prefix. `/bro config` starts with **Global** (default, Preferences editor, Doctor on demand) and **Features** (five configurable capabilities and three workflows inheriting BTW). Esc goes back; existing save behavior is unchanged.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-bro",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"description": "A sidekick for you and your Pi agent: plain-language explanations, side questions, diagrams, PR review and second opinions, without cluttering the agent's context.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -54,6 +54,8 @@
|
|
|
54
54
|
"src/remember-engine.ts",
|
|
55
55
|
"src/remember-tool.ts",
|
|
56
56
|
"src/remember.ts",
|
|
57
|
+
"src/simplify-engine.ts",
|
|
58
|
+
"src/simplify-ui.ts",
|
|
57
59
|
"src/explain-modal.ts",
|
|
58
60
|
"src/review.ts",
|
|
59
61
|
"src/review-source.ts",
|
|
@@ -63,6 +65,8 @@
|
|
|
63
65
|
"src/settings.ts",
|
|
64
66
|
"src/sources.ts",
|
|
65
67
|
"src/config-ui.ts",
|
|
68
|
+
"src/config-navigation-ui.ts",
|
|
69
|
+
"src/config-navigation.ts",
|
|
66
70
|
"src/ui-capabilities.ts",
|
|
67
71
|
"src/modal-kit.ts",
|
|
68
72
|
"src/backend.ts",
|
|
@@ -93,7 +97,7 @@
|
|
|
93
97
|
"scripts": {
|
|
94
98
|
"docs:check": "node scripts/check-docs.mjs && node --test test/docs-quality.test.mjs",
|
|
95
99
|
"typecheck": "tsc --noEmit",
|
|
96
|
-
"test": "npm run typecheck && npm run docs:check && node .github/scripts/release-utils.mjs selftest && node --test test/elevate-docs.test.mjs test/elevate.test.ts test/grill.test.ts test/remember.test.ts test/remember-modal.test.mjs test/review-ux.test.mjs test/review-doctor.test.mjs test/review-versions.test.mjs test/review-regeneration.test.mjs test/review-modal.test.mjs test/review.test.ts test/review-ui.test.mjs test/review-session.test.mjs test/helpers.test.mjs test/prompt.test.ts test/backend.test.ts test/claude.test.ts test/grok.test.ts test/codex.test.ts test/muse.test.ts test/settings.test.ts test/sources.test.ts test/release.test.ts test/show-html.test.ts test/smoke-rpc.test.ts test/ui-capabilities.test.ts test/modal-contract.test.mjs benchmark/*.test.ts && sh ./test/smoke-test.sh",
|
|
100
|
+
"test": "npm run typecheck && npm run docs:check && node .github/scripts/release-utils.mjs selftest && node --test test/elevate-docs.test.mjs test/elevate.test.ts test/simplify.test.ts test/simplify-modal.test.mjs test/grill.test.ts test/config-navigation.test.ts test/config-navigation-modal.test.mjs test/remember.test.ts test/remember-modal.test.mjs test/review-ux.test.mjs test/review-doctor.test.mjs test/review-versions.test.mjs test/review-regeneration.test.mjs test/review-modal.test.mjs test/review.test.ts test/review-ui.test.mjs test/review-session.test.mjs test/helpers.test.mjs test/prompt.test.ts test/backend.test.ts test/claude.test.ts test/grok.test.ts test/codex.test.ts test/muse.test.ts test/settings.test.ts test/sources.test.ts test/release.test.ts test/show-html.test.ts test/smoke-rpc.test.ts test/ui-capabilities.test.ts test/modal-contract.test.mjs benchmark/*.test.ts && sh ./test/smoke-test.sh",
|
|
97
101
|
"benchmark:dry-run": "node benchmark/run.ts dry-run",
|
|
98
102
|
"benchmark:run": "node benchmark/run.ts run",
|
|
99
103
|
"benchmark:report": "node benchmark/run.ts report",
|
package/src/bro.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { setPiHostContext } from "./backend.ts";
|
|
|
12
12
|
import { openElevateModal } from "./elevate.ts";
|
|
13
13
|
import { openGrillModal } from "./grill.ts";
|
|
14
14
|
import { openRememberModal } from "./remember.ts";
|
|
15
|
+
import { openSimplifyWorkflow, type SimplifyState } from "./simplify-ui.ts";
|
|
15
16
|
import { registerAboutUserTool } from "./remember-tool.ts";
|
|
16
17
|
import { parseGrillArguments, type GrillThread } from "./grill-engine.ts";
|
|
17
18
|
import {
|
|
@@ -42,8 +43,6 @@ import {
|
|
|
42
43
|
} from "./show.ts";
|
|
43
44
|
import {
|
|
44
45
|
type BroSource,
|
|
45
|
-
extractDocumentText,
|
|
46
|
-
extractWebPage,
|
|
47
46
|
isWorkspaceFile,
|
|
48
47
|
looksLikeWebUrl,
|
|
49
48
|
} from "./sources.ts";
|
|
@@ -66,9 +65,11 @@ export * from "./grill-engine.ts";
|
|
|
66
65
|
export * from "./remember-store.ts";
|
|
67
66
|
export * from "./remember-engine.ts";
|
|
68
67
|
export * from "./explain-modal.ts";
|
|
68
|
+
export * from "./simplify-engine.ts";
|
|
69
|
+
export * from "./simplify-ui.ts";
|
|
69
70
|
|
|
70
71
|
const COMMANDS = [
|
|
71
|
-
{ value: "simplify", label: "simplify", description: "
|
|
72
|
+
{ value: "simplify", label: "simplify", description: "Reopen the last Simplify screen without a new model call" },
|
|
72
73
|
{ value: "text", label: "text", description: "Explain pasted text, or the latest reply when text is omitted" },
|
|
73
74
|
{ value: "file", label: "file", description: "Explain a local document" },
|
|
74
75
|
{ value: "url", label: "url", description: "Explain a public webpage" },
|
|
@@ -82,15 +83,21 @@ const COMMANDS = [
|
|
|
82
83
|
{ value: "elevate", label: "elevate", description: "Discover missing expert considerations, then improve a request" },
|
|
83
84
|
{ value: "remember-me", label: "remember-me", description: "Help write and review personal memories; agents can unfold them on demand" },
|
|
84
85
|
{ value: "grill-me", label: "grill-me", description: "A private, one-question-at-a-time decision interview" },
|
|
85
|
-
{ value: "config", label: "config", description: "
|
|
86
|
+
{ value: "config", label: "config", description: "Global defaults, diagnostics and feature settings" },
|
|
86
87
|
{ value: "advisor", label: "advisor", description: "Check whether the executor's advisor tool is available right now" },
|
|
87
88
|
{ value: "advisor-steer", label: "advisor-steer", description: "View, edit, save, or clear the advisor's persistent steering brief" },
|
|
88
89
|
{ value: "help", label: "help", description: "Learn what Bro does and what it can access" },
|
|
89
90
|
];
|
|
90
91
|
const KNOWN_ACTIONS = new Set(COMMANDS.map((command) => command.value));
|
|
92
|
+
/** Only capability-level commands appear by default; all existing commands remain discoverable by prefix. */
|
|
93
|
+
export const FEATURED_BRO_COMMANDS = [
|
|
94
|
+
"simplify","show","btw","guided-review","elevate","grill-me","remember-me","config",
|
|
95
|
+
] as const;
|
|
96
|
+
const FEATURED_COMMANDS = new Set<string>(FEATURED_BRO_COMMANDS);
|
|
91
97
|
|
|
92
98
|
export default async function bro(pi: ExtensionAPI) {
|
|
93
99
|
let lastResult: ModalResult | undefined;
|
|
100
|
+
let simplifyState: SimplifyState = {};
|
|
94
101
|
let lastShowSteering: string | undefined;
|
|
95
102
|
let btwThread: BtwThread | undefined;
|
|
96
103
|
let grillThread: GrillThread | undefined;
|
|
@@ -105,6 +112,7 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
105
112
|
|
|
106
113
|
pi.on("session_start", async (_event, _ctx) => {
|
|
107
114
|
lastResult = undefined;
|
|
115
|
+
simplifyState = {};
|
|
108
116
|
btwThread = undefined;
|
|
109
117
|
grillThread = undefined;
|
|
110
118
|
});
|
|
@@ -116,7 +124,8 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
116
124
|
description: "Explain text, documents, and webpages; draw session turns; open a side conversation; configure Bro and the executor's advisor tool",
|
|
117
125
|
getArgumentCompletions: (prefix) => {
|
|
118
126
|
const normalized = prefix.trim().toLowerCase();
|
|
119
|
-
const matches = COMMANDS.filter((command) =>
|
|
127
|
+
const matches = COMMANDS.filter((command) =>
|
|
128
|
+
normalized ? command.value.startsWith(normalized) : FEATURED_COMMANDS.has(command.value));
|
|
120
129
|
return matches.length ? matches : null;
|
|
121
130
|
},
|
|
122
131
|
handler: async (args, ctx) => {
|
|
@@ -129,8 +138,10 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
129
138
|
let value = raw.slice(raw.split(/\s+/, 1)[0]?.length ?? 0).trim();
|
|
130
139
|
|
|
131
140
|
if (action === "simplify") {
|
|
132
|
-
if (value) { ctx.ui.notify("Use /bro simplify
|
|
133
|
-
|
|
141
|
+
if (value) { ctx.ui.notify("Use /bro simplify to reopen the last Simplify view; use /text, /file or /url inside it.", "warning"); return; }
|
|
142
|
+
try { await openSimplifyWorkflow(ctx, simplifyState, {kind:"resume"}); if (simplifyState.result) remember(simplifyState.result); }
|
|
143
|
+
catch (error) { ctx.ui.notify(withDoctor(error), "error"); }
|
|
144
|
+
return;
|
|
134
145
|
}
|
|
135
146
|
|
|
136
147
|
// Removed commands must never fall through to a paid text explanation.
|
|
@@ -153,6 +164,23 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
153
164
|
value = raw;
|
|
154
165
|
}
|
|
155
166
|
|
|
167
|
+
// Source operations open the same single-purpose Simplify modal, not separate
|
|
168
|
+
// top-level workflows. Old /bro text|file|url commands remain valid aliases.
|
|
169
|
+
if (!action || action === "text" || action === "file" || action === "url") {
|
|
170
|
+
if ((action === "file" || action === "url") && !value) {
|
|
171
|
+
ctx.ui.notify(`Use /bro ${action} <${action === "file" ? "path" : "url"}>.`, "warning");
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const initial = !action || (action === "text" && !value)
|
|
175
|
+
? {kind:"latest" as const}
|
|
176
|
+
: {kind:action as "text" | "file" | "url", input:value};
|
|
177
|
+
try {
|
|
178
|
+
await openSimplifyWorkflow(ctx, simplifyState, initial);
|
|
179
|
+
if (simplifyState.result) remember(simplifyState.result);
|
|
180
|
+
} catch (error) { ctx.ui.notify(withDoctor(error), "error"); }
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
156
184
|
if (action === "show") {
|
|
157
185
|
const { requested, steering, invalid } = parseShowArguments(value);
|
|
158
186
|
if (invalid) {
|
|
@@ -191,41 +219,6 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
191
219
|
return;
|
|
192
220
|
}
|
|
193
221
|
|
|
194
|
-
if (action === "file" || action === "url") {
|
|
195
|
-
if (!value) {
|
|
196
|
-
ctx.ui.notify(`Use /bro ${action} <${action === "file" ? "path" : "url"}>.`, "warning");
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
const runInput = async (
|
|
200
|
-
signal: AbortSignal,
|
|
201
|
-
source?: BroSource,
|
|
202
|
-
onProgress?: (text: string) => void,
|
|
203
|
-
mode?: BroMode,
|
|
204
|
-
): Promise<BroResult> => {
|
|
205
|
-
const target = source ?? (action === "url"
|
|
206
|
-
? await extractWebPage(value, signal)
|
|
207
|
-
: { text: await extractDocumentText(value, ctx.cwd, signal), label: unquote(value) });
|
|
208
|
-
try {
|
|
209
|
-
return {
|
|
210
|
-
source: target,
|
|
211
|
-
...(await simplify(target.text, signal, await readSettings(), onProgress, mode)),
|
|
212
|
-
};
|
|
213
|
-
} catch (error) {
|
|
214
|
-
throw new Error(withDoctor(error));
|
|
215
|
-
}
|
|
216
|
-
};
|
|
217
|
-
try {
|
|
218
|
-
await showBroModal(ctx, {
|
|
219
|
-
loadingText: action === "url" ? "Fetching and simplifying webpage…" : "Reading and simplifying document…",
|
|
220
|
-
run: runInput,
|
|
221
|
-
onResult: remember,
|
|
222
|
-
});
|
|
223
|
-
} catch (error) {
|
|
224
|
-
ctx.ui.notify(errorMessage(error), "error");
|
|
225
|
-
}
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
222
|
if (action === "doctor") {
|
|
230
223
|
if (parts.length !== 1) {
|
|
231
224
|
ctx.ui.notify("Use /bro doctor.", "warning");
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/** Compact navigation over existing settings/editors; no separate configuration state. */
|
|
2
|
+
import { getSelectListTheme } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { SelectList, matchesKey, wrapTextWithAnsi, type SelectItem, type Component } from "@earendil-works/pi-tui";
|
|
4
|
+
import { delegateMouse, modalHeight, renderFrame, type Theme, type TuiLike, type Area } from "./modal-kit.ts";
|
|
5
|
+
import type { BroMouseEvent } from "./ui-capabilities.ts";
|
|
6
|
+
import type { BroSettings } from "./settings.ts";
|
|
7
|
+
import {
|
|
8
|
+
CONFIG_ROOT, CONFIG_GLOBAL, CONFIG_FEATURES,
|
|
9
|
+
inheritedConfigDescription,
|
|
10
|
+
type ConfigNavAction, type ConfigNavState, type ConfigNavScreen,
|
|
11
|
+
} from "./config-navigation.ts";
|
|
12
|
+
|
|
13
|
+
type MenuItem = {value:string;label:string};
|
|
14
|
+
const rowsFor=(screen:ConfigNavScreen):readonly MenuItem[]=>{
|
|
15
|
+
if(screen==="root")return CONFIG_ROOT;
|
|
16
|
+
if(screen==="global")return CONFIG_GLOBAL;
|
|
17
|
+
if(screen==="features")return CONFIG_FEATURES;
|
|
18
|
+
if(screen==="advisor")return [
|
|
19
|
+
{value:"advisor-settings",label:"Backend, model and effort"},
|
|
20
|
+
{value:"advisor-steer",label:"Persistent advisor steering brief"},
|
|
21
|
+
];
|
|
22
|
+
return [{value:"btw-settings",label:"Open BTW's inherited backend settings"}];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function createConfigNavigationModal(
|
|
26
|
+
state:ConfigNavState,
|
|
27
|
+
settings:BroSettings,
|
|
28
|
+
): (tui:TuiLike,theme:Theme,keys:unknown,done:(result:ConfigNavAction)=>void)=>Component {
|
|
29
|
+
return (tui,theme,_keys,done)=>{
|
|
30
|
+
let closed=false;
|
|
31
|
+
let list:SelectList;
|
|
32
|
+
let listArea:Area={top:0,left:0,width:0,height:0};
|
|
33
|
+
const finish=(result:ConfigNavAction)=>{if(closed)return;closed=true;done(result);};
|
|
34
|
+
const go=(screen:ConfigNavScreen)=>{
|
|
35
|
+
state.screen=screen;
|
|
36
|
+
const items=rowsFor(screen) as SelectItem[];
|
|
37
|
+
list=new SelectList(items,Math.min(items.length,Math.max(1,modalHeight(tui)-10)),getSelectListTheme());
|
|
38
|
+
list.onSelect=item=>choose(item.value);
|
|
39
|
+
list.onCancel=back;
|
|
40
|
+
tui.requestRender();
|
|
41
|
+
};
|
|
42
|
+
const back=()=>{
|
|
43
|
+
if(state.screen==="root")return finish({kind:"close"});
|
|
44
|
+
if(state.screen==="global"||state.screen==="features"){go("root");return;}
|
|
45
|
+
go("features");
|
|
46
|
+
};
|
|
47
|
+
const choose=(value:string)=>{
|
|
48
|
+
if(state.screen==="root"){go(value==="global"?"global":"features");return;}
|
|
49
|
+
if(state.screen==="global"){
|
|
50
|
+
if(value==="defaults")return finish({kind:"settings",page:"defaults"});
|
|
51
|
+
if(value==="preferences")return finish({kind:"preferences"});
|
|
52
|
+
if(value==="doctor")return finish({kind:"doctor"});
|
|
53
|
+
}
|
|
54
|
+
if(state.screen==="features"){
|
|
55
|
+
if(value==="advisor"){go("advisor");return;}
|
|
56
|
+
if(value==="elevate"||value==="grill-me"||value==="remember-me"){
|
|
57
|
+
state.inherited=value;go("inherited");return;
|
|
58
|
+
}
|
|
59
|
+
if(value==="explain"||value==="show"||value==="btw"||value==="review")return finish({kind:"settings",page:value});
|
|
60
|
+
}
|
|
61
|
+
if(state.screen==="advisor"){
|
|
62
|
+
if(value==="advisor-steer")return finish({kind:"advisor-steer"});
|
|
63
|
+
if(value==="advisor-settings")return finish({kind:"settings",page:"advisor"});
|
|
64
|
+
}
|
|
65
|
+
if(state.screen==="inherited"&&value==="btw-settings"){
|
|
66
|
+
state.screen="features";return finish({kind:"settings",page:"btw"});
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
go(state.screen);
|
|
70
|
+
const title=()=>{
|
|
71
|
+
const s=state.screen;
|
|
72
|
+
return s==="root"?"Bro · config":
|
|
73
|
+
s==="global"?"Bro · config / Global":
|
|
74
|
+
s==="features"?"Bro · config / Features":
|
|
75
|
+
s==="advisor"?"Bro · config / Features / Advisor":
|
|
76
|
+
"Bro · config / Features / "+(state.inherited??"Inherited");
|
|
77
|
+
};
|
|
78
|
+
const subtitle=()=>{
|
|
79
|
+
if(state.screen==="root")return "Choose Global or Features. Enter drill down · Esc close.";
|
|
80
|
+
if(state.screen==="global")return "Defaults are shared. Doctor runs only when selected.";
|
|
81
|
+
if(state.screen==="features")return "Feature model overrides are optional; inherited features use BTW.";
|
|
82
|
+
if(state.screen==="advisor")return "Steering keeps its own file; changing models does not change steering.";
|
|
83
|
+
if(state.inherited)return inheritedConfigDescription(settings,state.inherited);
|
|
84
|
+
return "Inherits BTW";
|
|
85
|
+
};
|
|
86
|
+
return {
|
|
87
|
+
render:(width:number)=>{
|
|
88
|
+
const h=modalHeight(tui),w=Math.max(1,width-2),lines=list.render(w);
|
|
89
|
+
// Derive mouse hit area from the same subtitle lines we render, including long
|
|
90
|
+
// inherited-backend descriptions and narrow-terminal layouts.
|
|
91
|
+
const subtitleLines=wrapTextWithAnsi(theme.fg("dim",subtitle()),w).slice(0,Math.max(0,h-5-lines.length));
|
|
92
|
+
listArea={top:3+subtitleLines.length,left:1,width:w,height:Math.max(0,Math.min(lines.length,h-5-subtitleLines.length))};
|
|
93
|
+
return renderFrame(theme,width,h,{
|
|
94
|
+
header:()=>theme.fg("accent",theme.bold(title())),
|
|
95
|
+
body:()=>[...subtitleLines,...lines],
|
|
96
|
+
hints:"↑/↓ select · Enter open · Esc back",
|
|
97
|
+
fill:false,
|
|
98
|
+
minBody:Math.max(1,lines.length),
|
|
99
|
+
});
|
|
100
|
+
},
|
|
101
|
+
invalidate:()=>list.invalidate(),
|
|
102
|
+
handleMouse:(event:BroMouseEvent)=>delegateMouse(list,event,listArea),
|
|
103
|
+
handleInput:(data:string)=>{
|
|
104
|
+
if(matchesKey(data,"escape")){back();return;}
|
|
105
|
+
list.handleInput(data);
|
|
106
|
+
tui.requestRender();
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/** Navigation taxonomy only. Existing config storage remains in settings.ts. */
|
|
2
|
+
import type { BroSettings, Capability } from "./settings.ts";
|
|
3
|
+
import { capabilityBackend, capabilityOverride, capabilityPair } from "./settings.ts";
|
|
4
|
+
import { reviewBackendNotice } from "./backend.ts";
|
|
5
|
+
|
|
6
|
+
export type ConfigSettingsPage = "defaults" | Capability;
|
|
7
|
+
export type ConfigNavScreen = "root" | "global" | "features" | "advisor" | "inherited";
|
|
8
|
+
export type ConfigNavState = { screen: ConfigNavScreen; inherited?: "elevate"|"grill-me"|"remember-me" };
|
|
9
|
+
export type ConfigNavAction =
|
|
10
|
+
| {kind:"settings";page:ConfigSettingsPage}
|
|
11
|
+
| {kind:"preferences"|"doctor"|"advisor-steer"|"close"};
|
|
12
|
+
|
|
13
|
+
export const CONFIG_ROOT = [
|
|
14
|
+
{value:"global",label:"Global — shared defaults, preferences and Doctor"},
|
|
15
|
+
{value:"features",label:"Features — specific settings and inherited behavior"},
|
|
16
|
+
] as const;
|
|
17
|
+
export const CONFIG_GLOBAL = [
|
|
18
|
+
{value:"defaults",label:"Default backend, model and effort"},
|
|
19
|
+
{value:"preferences",label:"Preferences — shared response style"},
|
|
20
|
+
{value:"doctor",label:"Doctor — run setup and model diagnostics"},
|
|
21
|
+
] as const;
|
|
22
|
+
export const CONFIG_FEATURES = [
|
|
23
|
+
{value:"explain",label:"Simplify — Explain override and default mode"},
|
|
24
|
+
{value:"show",label:"Show — backend override and default turn count"},
|
|
25
|
+
{value:"btw",label:"BTW — backend override; conversation/full access"},
|
|
26
|
+
{value:"review",label:"Guided Review — override and inspection limits"},
|
|
27
|
+
{value:"advisor",label:"Advisor — backend override and steering"},
|
|
28
|
+
{value:"elevate",label:"Elevate — inherits BTW"},
|
|
29
|
+
{value:"grill-me",label:"Grill Me — inherits BTW"},
|
|
30
|
+
{value:"remember-me",label:"Remember Me — inherits BTW"},
|
|
31
|
+
] as const;
|
|
32
|
+
|
|
33
|
+
export function effectiveConfigLabel(settings:BroSettings, capability:Capability):string {
|
|
34
|
+
const p=capabilityPair(settings,capability);
|
|
35
|
+
const source=capabilityOverride(settings,capability)?"override":"global default";
|
|
36
|
+
return (capabilityBackend(settings,capability))+" / "+p.model+" / "+p.effort+" ("+source+")";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function inheritedConfigDescription(settings:BroSettings, feature:"elevate"|"grill-me"|"remember-me"):string {
|
|
40
|
+
const purpose=feature==="elevate"?"Improve a request":feature==="grill-me"?"Private decision interview":"Personal memory writing";
|
|
41
|
+
return purpose+". This workflow inherits BTW's configured backend, model and effort: "+
|
|
42
|
+
effectiveConfigLabel(settings,"btw")+
|
|
43
|
+
". No separate override exists. Use Features > BTW to change the inherited selection.";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Show the selected backend's actual access boundary at the point of configuration.
|
|
49
|
+
* Informational only: does not alter backend invocation, tools or permissions.
|
|
50
|
+
* Matched to the existing backend adapter flags and reference/data-and-permissions.md.
|
|
51
|
+
*/
|
|
52
|
+
export function configFeatureAccessNote(settings: BroSettings, feature: "btw" | "review"): string {
|
|
53
|
+
const backend = capabilityBackend(settings,feature);
|
|
54
|
+
if(feature==="btw") {
|
|
55
|
+
const notes: Record<typeof backend,string> = {
|
|
56
|
+
agy:"Agy's conversation-only path uses a temporary directory and sandboxed shell; the sandbox is not a no-write guarantee.",
|
|
57
|
+
claude:"Claude disables tools in conversation-only mode.",
|
|
58
|
+
grok:"Grok's conversation-only limit is prompt guidance only: its sandbox stays off and permissions are bypassed.",
|
|
59
|
+
codex:"Codex allows workspace reads and sandboxed commands under its read-only sandbox; it is not tool-free.",
|
|
60
|
+
muse:"Muse disables shell, write and approvals in conversation-only mode, but other tools can remain available.",
|
|
61
|
+
pi:"Pi's conversation-only session has no tools, extensions or context files.",
|
|
62
|
+
};
|
|
63
|
+
return "BTW: /mode in the BTW modal toggles conversation-only and full workspace access; full mode bypasses backend permission checks. "+notes[backend];
|
|
64
|
+
}
|
|
65
|
+
const existing=reviewBackendNotice(backend);
|
|
66
|
+
if(existing)return existing;
|
|
67
|
+
const strict: Record<"claude"|"muse"|"pi",string>={
|
|
68
|
+
claude:"Claude uses Read/Grep/Glob file-inspection tools only in the captured checkout; no shell.",
|
|
69
|
+
muse:"Muse disables shell, writes and web access in the captured checkout; reads remain available.",
|
|
70
|
+
pi:"Pi permits only read/grep/find/ls in the captured checkout; no shell or workspace writes.",
|
|
71
|
+
};
|
|
72
|
+
return "Guided Review: "+strict[backend as keyof typeof strict]+
|
|
73
|
+
" PR content remains untrusted evidence; backend restrictions do not prove the review's conclusions.";
|
|
74
|
+
}
|
package/src/config-ui.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
SelectList,
|
|
7
7
|
SettingsList,
|
|
8
8
|
matchesKey,
|
|
9
|
+
wrapTextWithAnsi,
|
|
9
10
|
truncateToWidth,
|
|
10
11
|
} from "@earendil-works/pi-tui";
|
|
11
12
|
import { getSelectListTheme, getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
@@ -31,6 +32,7 @@ import {
|
|
|
31
32
|
withCapabilityOverride,
|
|
32
33
|
} from "./settings.ts";
|
|
33
34
|
import { BRO_MODES, parseBroMode } from "./prompt.ts";
|
|
35
|
+
import { type ConfigSettingsPage, effectiveConfigLabel, configFeatureAccessNote } from "./config-navigation.ts";
|
|
34
36
|
import { delegateMouse, formatNotice, modalHeight, renderFrame, type Area, type Theme, type TuiLike } from "./modal-kit.ts";
|
|
35
37
|
import type { BroMouseEvent } from "./ui-capabilities.ts";
|
|
36
38
|
import { errorMessage } from "./util.ts";
|
|
@@ -64,6 +66,7 @@ export function createConfigModal(
|
|
|
64
66
|
families: AgyModelFamily[],
|
|
65
67
|
persistSettings: (settings: BroSettings) => Promise<void>,
|
|
66
68
|
piModels: PiModelChoice[] = [],
|
|
69
|
+
page: ConfigSettingsPage | "all" = "all",
|
|
67
70
|
): (tui: TuiLike, theme: Theme, keybindings: unknown, done: (value?: void) => void) => Component & { dispose?(): void } {
|
|
68
71
|
return (tui, theme, _keybindings, done) => {
|
|
69
72
|
// Size the host list once, then stop interaction if a later resize cannot show the whole
|
|
@@ -294,13 +297,15 @@ export function createConfigModal(
|
|
|
294
297
|
}
|
|
295
298
|
refresh();
|
|
296
299
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
effortItem,
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
300
|
+
// The existing pickers and save queue remain unchanged. Pages differ only in rows.
|
|
301
|
+
const items: SettingItem[] = page === "all"
|
|
302
|
+
? [modelItem, effortItem, modeItem, showTurnsItem, ...CAPABILITIES.flatMap((capability) => [capabilityItems[capability].model, capabilityItems[capability].effort])]
|
|
303
|
+
: page === "defaults"
|
|
304
|
+
? [modelItem, effortItem]
|
|
305
|
+
: [capabilityItems[page].model, capabilityItems[page].effort,
|
|
306
|
+
...(page === "explain" ? [modeItem] : []),
|
|
307
|
+
...(page === "show" ? [showTurnsItem] : []),
|
|
308
|
+
];
|
|
304
309
|
|
|
305
310
|
let notice = "";
|
|
306
311
|
let interactive = true;
|
|
@@ -414,27 +419,32 @@ export function createConfigModal(
|
|
|
414
419
|
const height = modalHeight(tui);
|
|
415
420
|
const innerWidth = Math.max(1, width - 2);
|
|
416
421
|
const list = settingsList.render(innerWidth);
|
|
422
|
+
// An access boundary must be visible whenever BTW/Review permissions are editable.
|
|
423
|
+
// If the terminal cannot show it alongside the controls, refuse blind edits.
|
|
424
|
+
const accessNote = page === "btw" || page === "review"
|
|
425
|
+
? wrapTextWithAnsi(theme.fg("warning", configFeatureAccessNote(settings, page)), innerWidth)
|
|
426
|
+
: [];
|
|
417
427
|
// renderFrame shrinks a notice to one line before taking minBody rows.
|
|
418
|
-
interactive = list.length <= Math.max(0, height - 5 - (notice ? 1 : 0));
|
|
428
|
+
interactive = list.length + accessNote.length <= Math.max(0, height - 5 - (notice ? 1 : 0));
|
|
419
429
|
return renderFrame(theme, width, height, {
|
|
420
430
|
header: () => theme.fg(
|
|
421
431
|
interactive ? "accent" : "warning",
|
|
422
|
-
theme.bold(interactive ? "Bro · config" : "Bro · config · enlarge terminal"),
|
|
432
|
+
theme.bold(interactive ? "Bro · config · " + (page === "all" ? "All" : page === "defaults" ? "Global defaults" : CAPABILITY_LABELS[page]) : "Bro · config · enlarge terminal"),
|
|
423
433
|
),
|
|
424
434
|
body: (rows) => {
|
|
425
435
|
if (!interactive) return [theme.fg("warning", "Enlarge the terminal to configure settings safely.")];
|
|
426
436
|
// The subtitle gives way before SettingsList's selection, scroll state and key hints.
|
|
427
|
-
const subtitle = rows > list.length
|
|
428
|
-
? [truncateToWidth(theme.fg("dim", "Shared defaults, with optional overrides per capability"), innerWidth)]
|
|
437
|
+
const subtitle = rows > list.length + accessNote.length
|
|
438
|
+
? [truncateToWidth(theme.fg("dim", page === "all" ? "Shared defaults, with optional overrides per capability" : page === "defaults" ? "Shared default selection" : "Effective: " + effectiveConfigLabel(settings, page)), innerWidth)]
|
|
429
439
|
: [];
|
|
430
|
-
//
|
|
431
|
-
listArea = { top: 3 + subtitle.length, left: 1, width: innerWidth, height: Math.min(list.length, rows - subtitle.length) };
|
|
432
|
-
return [...subtitle, ...list];
|
|
440
|
+
// Access notes always remain above the editable list, including after backend changes.
|
|
441
|
+
listArea = { top: 3 + subtitle.length + accessNote.length, left: 1, width: innerWidth, height: Math.min(list.length, rows - subtitle.length - accessNote.length) };
|
|
442
|
+
return [...subtitle, ...accessNote, ...list];
|
|
433
443
|
},
|
|
434
444
|
notice,
|
|
435
445
|
hints: interactive ? undefined : "Esc close",
|
|
436
446
|
fill: false,
|
|
437
|
-
minBody: interactive ? list.length : 1,
|
|
447
|
+
minBody: interactive ? list.length + accessNote.length : 1,
|
|
438
448
|
});
|
|
439
449
|
},
|
|
440
450
|
invalidate: () => settingsList.invalidate(),
|
package/src/doctor.ts
CHANGED
|
@@ -18,6 +18,9 @@ import {
|
|
|
18
18
|
} from "./advisor.ts";
|
|
19
19
|
import { agyFailureMessage, bridgePiRuntimeForCatalog, piEffectiveEffort, reviewBackendNotice, splitPiModel } from "./backend.ts";
|
|
20
20
|
import { createConfigModal } from "./config-ui.ts";
|
|
21
|
+
import { createConfigNavigationModal } from "./config-navigation-ui.ts";
|
|
22
|
+
import { type ConfigNavAction, type ConfigNavState } from "./config-navigation.ts";
|
|
23
|
+
import { showBroModal, showPreferencesModal, showAdvisorSteerModal } from "./explain-modal.ts";
|
|
21
24
|
import { openBroOverlay } from "./modal-kit.ts";
|
|
22
25
|
import {
|
|
23
26
|
type AgyEffort,
|
|
@@ -642,36 +645,60 @@ export async function doctorReport(pi: ExtensionAPI, ctx: ExtensionCommandContex
|
|
|
642
645
|
}`;
|
|
643
646
|
}
|
|
644
647
|
|
|
648
|
+
/** Global/Features navigation over the existing settings modal and editor contracts. */
|
|
645
649
|
export async function showBroConfigModal(ctx: ExtensionCommandContext, pi: ExtensionAPI): Promise<void> {
|
|
646
650
|
if (!hasBroCustomUi(ctx)) {
|
|
647
651
|
ctx.ui.notify("Use /bro config in Pi's interactive UI.", "warning");
|
|
648
652
|
return;
|
|
649
653
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
settings
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
654
|
+
const navigation: ConfigNavState = {screen:"root"};
|
|
655
|
+
for (;;) {
|
|
656
|
+
// Even invalid settings must not prevent access to Doctor. The placeholder is navigation-only:
|
|
657
|
+
// every editable page re-reads the actual settings and refuses to edit on failure.
|
|
658
|
+
let current: BroSettings;
|
|
659
|
+
try { current = await readSettings(); }
|
|
660
|
+
catch (error) {
|
|
661
|
+
ctx.ui.notify("Settings unavailable: "+errorMessage(error)+". Global > Doctor can diagnose.", "warning");
|
|
662
|
+
current = { backend:"agy",model:"(unavailable)",effort:"default",mode:"balanced",showTurns:1,overrides:{} };
|
|
663
|
+
}
|
|
664
|
+
const selected = await openBroOverlay<ConfigNavAction>(
|
|
665
|
+
ctx, createConfigNavigationModal(navigation,current), {mouse:true}
|
|
666
|
+
);
|
|
667
|
+
if (!selected || selected.kind === "close") return;
|
|
668
|
+
if (selected.kind === "preferences") {
|
|
669
|
+
await showPreferencesModal(ctx);
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
if (selected.kind === "advisor-steer") {
|
|
673
|
+
await showAdvisorSteerModal(ctx,pi);
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
if (selected.kind === "doctor") {
|
|
677
|
+
await showBroModal(ctx,{
|
|
678
|
+
loadingText:"Checking Bro setup…",
|
|
679
|
+
retryable:true,
|
|
680
|
+
retryLabel:"check again",
|
|
681
|
+
run:async(signal)=>({text:await doctorReport(pi,ctx,signal)}),
|
|
682
|
+
});
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
if (selected.kind !== "settings") continue;
|
|
686
|
+
// Catalog discovery is needed only when an editable settings page is opened, not on
|
|
687
|
+
// every navigation or diagnostic visit. Keep independent Agy/Pi failure handling.
|
|
688
|
+
let settings:BroSettings;
|
|
689
|
+
try { settings = await readSettings(); }
|
|
690
|
+
catch (error) { ctx.ui.notify(withDoctor(error),"error"); continue; }
|
|
691
|
+
let families:AgyModelFamily[]=[];
|
|
692
|
+
try { families=await listAgyModels(pi); }
|
|
693
|
+
catch { ctx.ui.notify("Agy catalog unavailable — other backends remain configurable.","warning"); }
|
|
694
|
+
let piModels:PiModelChoice[]=[];
|
|
695
|
+
try {
|
|
696
|
+
const catalog=await listPiModels(ctx);
|
|
697
|
+
piModels=catalog.models;
|
|
698
|
+
for(const diagnostic of catalog.diagnostics) ctx.ui.notify("Pi scoped models: "+diagnostic,"warning");
|
|
699
|
+
} catch(error) { ctx.ui.notify("Pi catalog unavailable: "+errorMessage(error),"warning"); }
|
|
700
|
+
await openBroOverlay<void>(
|
|
701
|
+
ctx,createConfigModal(settings,families,writeSettings,piModels,selected.page),{mouse:true}
|
|
702
|
+
);
|
|
675
703
|
}
|
|
676
|
-
await openBroOverlay<void>(ctx, createConfigModal(settings, families, writeSettings, piModels));
|
|
677
704
|
}
|
package/src/explain-modal.ts
CHANGED
|
@@ -384,12 +384,13 @@ Quick reference. Full user guide: https://github.com/tranhoangnguyen03/pi-bro/bl
|
|
|
384
384
|
|
|
385
385
|
## Explain and show
|
|
386
386
|
|
|
387
|
-
- \`/bro\`
|
|
387
|
+
- \`/bro\` — open Simplify and explain the latest completed assistant reply
|
|
388
|
+
- \`/bro simplify\` — reopen the current Simplify screen without another model call
|
|
388
389
|
- \`/bro text [text]\` — explain pasted text, or the latest reply when text is omitted
|
|
389
390
|
- \`/bro file <path>\` — explain a workspace \`.md\`, \`.markdown\`, \`.txt\`, \`.pdf\`, or \`.docx\` file
|
|
390
391
|
- \`/bro url <url>\` — explain one public webpage
|
|
391
392
|
- \`/bro <input>\` — a lone URL, an existing supported file, or anything else as pasted text
|
|
392
|
-
- \`/bro open\` —
|
|
393
|
+
- Inside Simplify: \`/simplify\`, \`/text <text>\`, \`/file <path>\`, \`/url <url>\`, \`/copy\`, \`/clear\`, \`/help\`. **M** changes the mode and re-explains; **R** retries. One source at a time, no chat thread.\n- \`/bro open\` — legacy last-result view (retained for compatibility)
|
|
393
394
|
- \`/bro show [n-turns] [query]\` — draw recent turns' conversation text as shapes; an optional query steers the focus
|
|
394
395
|
|
|
395
396
|
## Guided Review
|
|
@@ -424,7 +425,7 @@ Quick reference. Full user guide: https://github.com/tranhoangnguyen03/pi-bro/bl
|
|
|
424
425
|
## Configure and check
|
|
425
426
|
|
|
426
427
|
- \`/bro preferences\` — tell Bro about yourself and how you like answers; added to explain, show, btw, Elevate and Guided Review prompts, never the advisor (**Ctrl+S** save, **Ctrl+K** delete, **Ctrl+C** copy, **Esc** close)
|
|
427
|
-
- \`/bro config\` —
|
|
428
|
+
- \`/bro config\` — Global (shared defaults, preferences, Doctor) and Features (Simplify, Show, BTW, Review, Advisor and BTW-inherited workflows). The same model pickers, safe saves and error handling remain; Esc backs out.
|
|
428
429
|
- \`/bro mode [brief|balanced|faithful]\` — view or choose the explanation mode
|
|
429
430
|
- \`/bro doctor\` — check settings, preferences, and every selected backend without running a model turn
|
|
430
431
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Pure command parser for the one-source-at-a-time Simplify modal. */
|
|
2
|
+
export type SimplifyAction =
|
|
3
|
+
| { kind: "latest" | "copy" | "clear" | "help" | "retry" }
|
|
4
|
+
| { kind: "url" | "text" | "file"; input: string }
|
|
5
|
+
| { kind: "invalid"; message: string };
|
|
6
|
+
|
|
7
|
+
export function parseSimplifyAction(input: string): SimplifyAction {
|
|
8
|
+
const trimmed = input.trim();
|
|
9
|
+
if (!trimmed) return { kind:"invalid", message:"Enter /simplify, /url, /text, /file, /copy, /clear or /help." };
|
|
10
|
+
const i=trimmed.indexOf(" ");
|
|
11
|
+
const cmd=(i<0?trimmed:trimmed.slice(0,i)).toLowerCase();
|
|
12
|
+
const value=i<0?"":trimmed.slice(i+1).trim();
|
|
13
|
+
if (["/simplify","/copy","/clear","/help","/retry"].includes(cmd)) {
|
|
14
|
+
if(value) return {kind:"invalid",message:cmd+" takes no arguments."};
|
|
15
|
+
return {kind:({"/simplify":"latest","/copy":"copy","/clear":"clear","/help":"help","/retry":"retry"} as Record<string,"latest"|"copy"|"clear"|"help"|"retry">)[cmd]!};
|
|
16
|
+
}
|
|
17
|
+
for (const kind of ["url","text","file"] as const) {
|
|
18
|
+
if(cmd==="/"+kind) return value ? {kind,input:value} : {kind:"invalid",message:"Use /"+kind+" <"+(kind==="url"?"url":kind==="file"?"path":"text")+">."};
|
|
19
|
+
}
|
|
20
|
+
return {kind:"invalid",message:"Unknown Simplify command. Use /help. Ordinary chat belongs to /bro btw."};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const SIMPLIFY_MODAL_HELP = [
|
|
24
|
+
"## Simplify · Commands",
|
|
25
|
+
"",
|
|
26
|
+
"Bro explains one source in plain language. Every new explanation replaces the last one. This is not a chat.",
|
|
27
|
+
"",
|
|
28
|
+
"- \`/simplify\` — explain the latest completed assistant reply",
|
|
29
|
+
"- \`/url <url>\` — explain a public webpage",
|
|
30
|
+
"- \`/text <text>\` — explain pasted text",
|
|
31
|
+
"- \`/file <path>\` — explain a workspace document",
|
|
32
|
+
"- \`/copy\` — copy the current explanation",
|
|
33
|
+
"- \`/clear\` — clear the explanation and source",
|
|
34
|
+
"- \`/help\` — show these commands",
|
|
35
|
+
"",
|
|
36
|
+
"With an empty composer, **M** changes explanation mode and re-explains the captured source; **R** retries. Both keep the backend configured for Explain. **Esc** closes; /bro simplify reopens this view in the current Pi session.",
|
|
37
|
+
].join("\n");
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/** Source-focused Simplify modal. No chat thread, durable draft or new backend boundary. */
|
|
2
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { stripVTControlCharacters } from "node:util";
|
|
4
|
+
import { copyToClipboard } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { matchesKey } from "@earendil-works/pi-tui";
|
|
6
|
+
import { BtwModal } from "./btw.ts";
|
|
7
|
+
import { latestAssistant, simplify, type BroResult } from "./explain-modal.ts";
|
|
8
|
+
import { openBroOverlay } from "./modal-kit.ts";
|
|
9
|
+
import { nextBroMode, type BroMode } from "./prompt.ts";
|
|
10
|
+
import { readSettings } from "./settings.ts";
|
|
11
|
+
import { extractDocumentText, extractWebPage, type BroSource } from "./sources.ts";
|
|
12
|
+
import { hasBroCustomUi } from "./ui-capabilities.ts";
|
|
13
|
+
import { errorMessage, unquote } from "./util.ts";
|
|
14
|
+
import { parseSimplifyAction, SIMPLIFY_MODAL_HELP, type SimplifyAction } from "./simplify-engine.ts";
|
|
15
|
+
|
|
16
|
+
export type SimplifyState = {
|
|
17
|
+
result?: BroResult;
|
|
18
|
+
view?: "result" | "help" | "empty";
|
|
19
|
+
draft?: string;
|
|
20
|
+
error?: string;
|
|
21
|
+
};
|
|
22
|
+
export type SimplifyStart =
|
|
23
|
+
| { kind:"resume" }
|
|
24
|
+
| { kind:"latest" }
|
|
25
|
+
| { kind:"text"|"file"|"url"; input:string };
|
|
26
|
+
|
|
27
|
+
async function resolveSource(ctx: ExtensionCommandContext, request: SimplifyStart | SimplifyAction,
|
|
28
|
+
signal: AbortSignal, retained?: BroSource): Promise<BroSource> {
|
|
29
|
+
if (retained) return retained;
|
|
30
|
+
switch (request.kind) {
|
|
31
|
+
case "latest": {
|
|
32
|
+
await ctx.waitForIdle();
|
|
33
|
+
if (signal.aborted) throw new Error("Cancelled.");
|
|
34
|
+
const source=latestAssistant(ctx);
|
|
35
|
+
if (!source) throw new Error("No completed assistant reply to simplify.");
|
|
36
|
+
return {text:source.text,label:"Latest assistant reply"};
|
|
37
|
+
}
|
|
38
|
+
case "text": return {text:request.input,label:"Pasted text"};
|
|
39
|
+
case "file": return {text:await extractDocumentText(request.input,ctx.cwd,signal),label:unquote(request.input)};
|
|
40
|
+
case "url": return await extractWebPage(request.input,signal);
|
|
41
|
+
default: throw new Error("Choose a source with /simplify, /text, /file or /url.");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The state belongs to the parent /bro extension instance and resets on session_start. */
|
|
46
|
+
export async function openSimplifyWorkflow(
|
|
47
|
+
ctx: ExtensionCommandContext,
|
|
48
|
+
state: SimplifyState,
|
|
49
|
+
initial: SimplifyStart,
|
|
50
|
+
): Promise<void> {
|
|
51
|
+
if (!hasBroCustomUi(ctx)) {
|
|
52
|
+
if (initial.kind==="resume") { ctx.ui.notify("Simplify's interactive view requires Pi TUI.", "warning"); return; }
|
|
53
|
+
const controller=new AbortController();
|
|
54
|
+
const source=await resolveSource(ctx,initial,controller.signal);
|
|
55
|
+
const result=await simplify(source.text,controller.signal,await readSettings());
|
|
56
|
+
state.result={source,...result};
|
|
57
|
+
state.view="result"; state.error=undefined;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
await openBroOverlay<void>(ctx,(tui,theme,_keys,done)=>{
|
|
62
|
+
let closed=false;
|
|
63
|
+
let controller:AbortController|undefined;
|
|
64
|
+
let running=false;
|
|
65
|
+
const close=()=>{if(closed)return; closed=true;state.draft=modal.getComposerText();controller?.abort();done(undefined);};
|
|
66
|
+
const modal=new BtwModal(tui,theme,close,submit,()=>{
|
|
67
|
+
closed=true;state.draft=modal.getComposerText();controller?.abort();
|
|
68
|
+
},{
|
|
69
|
+
title:"Bro · simplify",
|
|
70
|
+
commands:"",
|
|
71
|
+
inputLabel:"Simplify command",
|
|
72
|
+
idleHint:"Enter run · M change mode · R retry · /copy · /help · Esc close",
|
|
73
|
+
});
|
|
74
|
+
const display=()=>{
|
|
75
|
+
modal.setCustomMode(state.result?.mode ? state.result.mode+(state.result.preferences ? " · prefs" : "") : "one source · one explanation");
|
|
76
|
+
modal.setModel(state.result?.model ?? "");
|
|
77
|
+
if(state.view==="help")modal.setText(SIMPLIFY_MODAL_HELP);
|
|
78
|
+
else if(state.view==="result"&&state.result)modal.setText((state.result.source.label ? "**Source:** "+stripVTControlCharacters(state.result.source.label).replace(/[\r\n]/g," ")+"\n\n---\n\n" : "")+state.result.text);
|
|
79
|
+
else modal.setText("## Simplify\n\nUse /simplify for the latest reply, /text, /file or /url for another source. Type /help for all commands.");
|
|
80
|
+
// Notices describe the currently shown state, not an earlier request.
|
|
81
|
+
modal.setNotice(state.error ?? "",state.error ? "error" : "info");
|
|
82
|
+
modal.scrollToEnd();
|
|
83
|
+
};
|
|
84
|
+
const run=async (request:SimplifyStart|SimplifyAction, retained?:BroSource, mode?:BroMode, submitted?:string)=>{
|
|
85
|
+
if(running||closed)return;
|
|
86
|
+
const attempt=new AbortController();
|
|
87
|
+
controller=attempt; running=true;
|
|
88
|
+
modal.setRunning(true);modal.setNotice("Simplifying…");
|
|
89
|
+
const previous=state.result,previousView=state.view;
|
|
90
|
+
try {
|
|
91
|
+
const source=await resolveSource(ctx,request,attempt.signal,retained);
|
|
92
|
+
if(closed||attempt.signal.aborted)return;
|
|
93
|
+
const currentSettings=await readSettings();
|
|
94
|
+
const result=await simplify(source.text,attempt.signal,currentSettings,text=>{
|
|
95
|
+
if(!closed&&!attempt.signal.aborted)modal.setText(text);
|
|
96
|
+
},mode);
|
|
97
|
+
if(closed||attempt.signal.aborted)return;
|
|
98
|
+
state.result={source,...result};
|
|
99
|
+
state.view="result";state.error=undefined;
|
|
100
|
+
display();
|
|
101
|
+
if(mode && mode !== currentSettings.mode)modal.setNotice("Mode: "+mode+" (not saved)");
|
|
102
|
+
} catch(error){
|
|
103
|
+
if(closed||attempt.signal.aborted)return;
|
|
104
|
+
state.result=previous;state.view=previousView;
|
|
105
|
+
state.error=errorMessage(error);
|
|
106
|
+
display();
|
|
107
|
+
// Failed replacement is editable and retryable. Do not overwrite text the user
|
|
108
|
+
// entered while this request was running. Preserve it on modal close as well.
|
|
109
|
+
if(submitted && !modal.getComposerText().trim()) {
|
|
110
|
+
modal.setComposerText(submitted);
|
|
111
|
+
state.draft=submitted;
|
|
112
|
+
}
|
|
113
|
+
} finally {
|
|
114
|
+
if(controller===attempt)controller=undefined;
|
|
115
|
+
running=false;
|
|
116
|
+
if(!closed)modal.setRunning(false);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
const retry=(switchMode=false)=>{
|
|
120
|
+
if(running||!state.result)return;
|
|
121
|
+
const next=switchMode?nextBroMode(state.result.mode??"balanced"):state.result.mode;
|
|
122
|
+
void run({kind:"latest"},state.result.source,next);
|
|
123
|
+
};
|
|
124
|
+
const copy=()=>{
|
|
125
|
+
if(!state.result?.text){modal.setNotice("Nothing to copy yet.","warning");return;}
|
|
126
|
+
void copyToClipboard(state.result.text)
|
|
127
|
+
.then(()=>{if(!closed)modal.setNotice("Explanation copied.","success");})
|
|
128
|
+
.catch(error=>{if(!closed)modal.setNotice("Copy failed: "+errorMessage(error),"error");});
|
|
129
|
+
};
|
|
130
|
+
function submit(raw:string):void {
|
|
131
|
+
if(running||closed)return;
|
|
132
|
+
const action=parseSimplifyAction(raw);
|
|
133
|
+
if(action.kind==="invalid"){modal.setNotice(action.message,"warning");return;}
|
|
134
|
+
modal.clearComposer();state.draft=undefined;
|
|
135
|
+
if(action.kind==="copy"){copy();return;}
|
|
136
|
+
if(action.kind==="clear"){
|
|
137
|
+
state.result=undefined;state.view="empty";state.error=undefined;display();return;
|
|
138
|
+
}
|
|
139
|
+
if(action.kind==="help"){state.view="help";state.error=undefined;display();return;}
|
|
140
|
+
if(action.kind==="retry"){retry();return;}
|
|
141
|
+
void run(action,undefined,undefined,raw);
|
|
142
|
+
}
|
|
143
|
+
modal.onKey=(data:string)=>{
|
|
144
|
+
if(running||modal.getComposerText().trim())return false;
|
|
145
|
+
if(matchesKey(data,"m")||matchesKey(data,"shift+m")){retry(true);return Boolean(state.result);}
|
|
146
|
+
if(matchesKey(data,"r")||matchesKey(data,"shift+r")){retry();return Boolean(state.result);}
|
|
147
|
+
if(matchesKey(data,"c")||matchesKey(data,"shift+c")){copy();return true;}
|
|
148
|
+
return false;
|
|
149
|
+
};
|
|
150
|
+
display();
|
|
151
|
+
if(state.draft)modal.setComposerText(state.draft);
|
|
152
|
+
if(initial.kind!=="resume")queueMicrotask(()=>{if(!closed)void run(initial,undefined,undefined,
|
|
153
|
+
initial.kind==="latest" ? "/simplify" : "/"+initial.kind+" "+initial.input);});
|
|
154
|
+
return modal;
|
|
155
|
+
},{mouse:true});
|
|
156
|
+
}
|