pi-bro 0.27.0 → 0.28.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 CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  All notable changes to pi-bro are documented here.
4
4
 
5
+ ## [0.28.0] - 2026-11-05
6
+
7
+ ### Highlights
8
+
9
+ - Simplify gains a source-focused modal with internal /simplify, /text, /file, /url, /copy, /clear and /help commands.
10
+ - Bare /bro captures a fresh latest assistant response; /bro simplify reopens the last Simplify view without a new model call.
11
+ - M changes explanation mode for that source without rewriting the saved default; R retries the same source.
12
+ - Existing explicit top-level source forms and backend selection remain compatible.
13
+
14
+
5
15
  ## [0.27.0] - 2026-10-30
6
16
 
7
17
  ### Highlights
@@ -6,7 +6,7 @@
6
6
 
7
7
  | Source | Command | What Bro does |
8
8
  | --- | --- | --- |
9
- | Latest assistant reply | `/bro` | Explains the latest completed reply without adding the result to the conversation. |
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. |
@@ -22,12 +22,13 @@ text directly captures a new source the same way.
22
22
 
23
23
  | Command | Description |
24
24
  | --- | --- |
25
- | `/bro`, `/bro simplify`, or `/bro text` | Explain the latest completed assistant response. |
25
+ | `/bro` | Open Simplify and explain the latest completed assistant reply. |
26
+ | `/bro simplify` | Reopen the last Simplify screen within this Pi session, without a new model call. |
26
27
  | `/bro text <text>` | Explain pasted text. |
27
28
  | `/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
29
  | `/bro file <path>` | Explain a workspace-local `.md`, `.markdown`, `.txt`, `.pdf`, or `.docx` file. |
29
30
  | `/bro url <url>` | Explain one public, text-based webpage. |
30
- | `/bro open` | Reopen the latest explanation without calling the simplifier again. |
31
+ | `/bro open` | Legacy compatibility shortcut to the last result. No `/open` command inside Simplify. |
31
32
  | `/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
33
  | `/bro doctor` | Check Bro's settings, preferences, and each selected backend, with the effective backend/model/effort per feature. |
33
34
  | `/bro mode [brief\|balanced\|faithful]` | View or choose the explanation mode. |
@@ -50,3 +51,7 @@ Giving `/bro` the input directly works the same way:
50
51
  /bro docs/report.pdf
51
52
  /bro any other text is explained as pasted text
52
53
  ```
54
+
55
+ ## Simplify modal (single-source explanation)
56
+
57
+ 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.
@@ -163,3 +163,7 @@ 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-bro",
3
- "version": "0.27.0",
3
+ "version": "0.28.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",
@@ -93,7 +95,7 @@
93
95
  "scripts": {
94
96
  "docs:check": "node scripts/check-docs.mjs && node --test test/docs-quality.test.mjs",
95
97
  "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",
98
+ "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/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
99
  "benchmark:dry-run": "node benchmark/run.ts dry-run",
98
100
  "benchmark:run": "node benchmark/run.ts run",
99
101
  "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: "Explain the latest completed assistant reply (same as /bro)" },
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" },
@@ -91,6 +92,7 @@ const KNOWN_ACTIONS = new Set(COMMANDS.map((command) => command.value));
91
92
 
92
93
  export default async function bro(pi: ExtensionAPI) {
93
94
  let lastResult: ModalResult | undefined;
95
+ let simplifyState: SimplifyState = {};
94
96
  let lastShowSteering: string | undefined;
95
97
  let btwThread: BtwThread | undefined;
96
98
  let grillThread: GrillThread | undefined;
@@ -105,6 +107,7 @@ export default async function bro(pi: ExtensionAPI) {
105
107
 
106
108
  pi.on("session_start", async (_event, _ctx) => {
107
109
  lastResult = undefined;
110
+ simplifyState = {};
108
111
  btwThread = undefined;
109
112
  grillThread = undefined;
110
113
  });
@@ -129,8 +132,10 @@ export default async function bro(pi: ExtensionAPI) {
129
132
  let value = raw.slice(raw.split(/\s+/, 1)[0]?.length ?? 0).trim();
130
133
 
131
134
  if (action === "simplify") {
132
- if (value) { ctx.ui.notify("Use /bro simplify for the latest reply, or /bro text <text> for pasted text.", "warning"); return; }
133
- action = "";
135
+ if (value) { ctx.ui.notify("Use /bro simplify to reopen the last Simplify view; use /text, /file or /url inside it.", "warning"); return; }
136
+ try { await openSimplifyWorkflow(ctx, simplifyState, {kind:"resume"}); if (simplifyState.result) remember(simplifyState.result); }
137
+ catch (error) { ctx.ui.notify(withDoctor(error), "error"); }
138
+ return;
134
139
  }
135
140
 
136
141
  // Removed commands must never fall through to a paid text explanation.
@@ -153,6 +158,23 @@ export default async function bro(pi: ExtensionAPI) {
153
158
  value = raw;
154
159
  }
155
160
 
161
+ // Source operations open the same single-purpose Simplify modal, not separate
162
+ // top-level workflows. Old /bro text|file|url commands remain valid aliases.
163
+ if (!action || action === "text" || action === "file" || action === "url") {
164
+ if ((action === "file" || action === "url") && !value) {
165
+ ctx.ui.notify(`Use /bro ${action} <${action === "file" ? "path" : "url"}>.`, "warning");
166
+ return;
167
+ }
168
+ const initial = !action || (action === "text" && !value)
169
+ ? {kind:"latest" as const}
170
+ : {kind:action as "text" | "file" | "url", input:value};
171
+ try {
172
+ await openSimplifyWorkflow(ctx, simplifyState, initial);
173
+ if (simplifyState.result) remember(simplifyState.result);
174
+ } catch (error) { ctx.ui.notify(withDoctor(error), "error"); }
175
+ return;
176
+ }
177
+
156
178
  if (action === "show") {
157
179
  const { requested, steering, invalid } = parseShowArguments(value);
158
180
  if (invalid) {
@@ -191,41 +213,6 @@ export default async function bro(pi: ExtensionAPI) {
191
213
  return;
192
214
  }
193
215
 
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
216
  if (action === "doctor") {
230
217
  if (parts.length !== 1) {
231
218
  ctx.ui.notify("Use /bro doctor.", "warning");
@@ -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\` or \`/bro simplify\` — explain the latest completed assistant reply
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\` — reopen the latest explanation without a new request
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
@@ -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
+ }