pi-bro 0.19.3 → 0.19.5
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 +27 -0
- package/README.md +7 -4
- package/backend.ts +2 -7
- package/bro.ts +46 -37
- package/package.json +3 -2
- package/ui-capabilities.ts +35 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to pi-bro are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.19.5] - 2026-10-01
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Remove the always-true backend-support predicate and unreachable doctor/config branches; retain backend-specific effort and permission validation (#74).
|
|
10
|
+
- Move helper, modal, advisor, and Pi SDK regression checks into named Node tests with shared isolated compilation, leaving shell smoke tests focused on host RPC integration and avoiding duplicate helper execution in PiG (#74).
|
|
11
|
+
- Consolidate redundant support, prompt, and BTW assertions; replace prose greps with package-documentation checks and document surviving coverage (#74).
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Make the advisor throttle cancellation test actually queue an emission and advance controlled time beyond its deadline; verified by removing cleanup and observing failure (#74).
|
|
16
|
+
|
|
17
|
+
### Documentation
|
|
18
|
+
|
|
19
|
+
- Explicitly archive the seven pre-conversation-only decomposition captures as historical semantic examples and record before/after test organization and timings (#74).
|
|
20
|
+
|
|
21
|
+
## [0.19.4] - 2026-10-01
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- Preserve advisor timeout and cancellation status through the consultation wrapper. Host deadlines and cancellation stop after one attempt without backoff; ordinary invocation failures retain the existing three-attempt policy (#72).
|
|
26
|
+
- Bound benchmark call and usage-preflight cleanup when descendants retain output pipes, reusing backend process-group termination and escalation. Ignore late output after a stop without adding benchmark retries (#72).
|
|
27
|
+
|
|
28
|
+
### Documentation
|
|
29
|
+
|
|
30
|
+
- Distinguish host deadlines from CLI-reported failures and document unverified shutdown/reload cancellation, POSIX process-group cleanup, and Windows direct-child limits (#72).
|
|
31
|
+
|
|
5
32
|
## [0.19.3] - 2026-09-30
|
|
6
33
|
|
|
7
34
|
### Fixed
|
package/README.md
CHANGED
|
@@ -64,7 +64,7 @@ text directly captures a new source the same way.
|
|
|
64
64
|
|
|
65
65
|
| Command | Description |
|
|
66
66
|
| --- | --- |
|
|
67
|
-
| `/bro
|
|
67
|
+
| `/bro`, `/bro simplify`, or `/bro text` | Explain the latest completed assistant response. |
|
|
68
68
|
| `/bro text <text>` | Explain pasted text. |
|
|
69
69
|
| `/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. |
|
|
70
70
|
| `/bro file <path>` | Explain a workspace-local `.md`, `.markdown`, `.txt`, `.pdf`, or `.docx` file. |
|
|
@@ -205,9 +205,12 @@ presence, and backend compatibility (for Agy, a minimum CLI version with an
|
|
|
205
205
|
more evidence" is a normal result, not a failure), Bro retries with the
|
|
206
206
|
identical snapshot, steering, and question: once after 5 seconds, once more
|
|
207
207
|
after 10 seconds, then returns the backend's own diagnostic — including a
|
|
208
|
-
context-length error, verbatim — as the failure.
|
|
209
|
-
|
|
210
|
-
|
|
208
|
+
context-length error, verbatim — as the failure. Host-imposed deadlines and
|
|
209
|
+
cancellation are terminal: neither starts another attempt or a backoff wait.
|
|
210
|
+
The advisor CLI is asked to stop at 10 minutes; Bro's host deadline is 610
|
|
211
|
+
seconds plus bounded process cleanup. A CLI-reported error before that host
|
|
212
|
+
deadline remains an invocation failure and can be retried.
|
|
213
|
+
Cancelling the tool call skips any pending retry wait.
|
|
211
214
|
- **Progress and provenance**: while an attempt is running, Bro shows the last
|
|
212
215
|
thing the advisor actually reported — a tool name or a user-facing response
|
|
213
216
|
line, never hidden reasoning — and how long ago it arrived, e.g. `last
|
package/backend.ts
CHANGED
|
@@ -123,7 +123,7 @@ const DEFAULT_KILL_ESCALATION_MS = 5_000;
|
|
|
123
123
|
// protocol failure it signals the whole POSIX process group (SIGTERM, then SIGKILL after
|
|
124
124
|
// `killEscalationMs` if the child or a misbehaving grandchild ignores it). Windows only ever
|
|
125
125
|
// reaches the immediate child directly -- there is no process-tree guarantee there.
|
|
126
|
-
function beginAttempt(child: ChildProcess, signal: AbortSignal, deadlineMs: number, killEscalationMs: number): Attempt {
|
|
126
|
+
export function beginAttempt(child: ChildProcess, signal: AbortSignal, deadlineMs: number, killEscalationMs: number): Attempt {
|
|
127
127
|
let cause: StopCause | undefined;
|
|
128
128
|
let killTimer: ReturnType<typeof setTimeout> | undefined;
|
|
129
129
|
let finishClose: (value: { code: number | null; exitSignal: NodeJS.Signals | null }) => void;
|
|
@@ -286,6 +286,7 @@ async function executeArgvPrint(
|
|
|
286
286
|
}
|
|
287
287
|
|
|
288
288
|
const lines = createInterface({ input: child.stdout!, crlfDelay: Infinity });
|
|
289
|
+
void attempt.closed.then(() => lines.close());
|
|
289
290
|
try {
|
|
290
291
|
for await (const line of lines) {
|
|
291
292
|
if (!line.trim() || attempt.causeOf()) continue;
|
|
@@ -510,12 +511,6 @@ async function executeAdvisorStdin(
|
|
|
510
511
|
}
|
|
511
512
|
|
|
512
513
|
|
|
513
|
-
// Every backend supports every feature. Grok's "restricted" access is a prompt instruction, not
|
|
514
|
-
// enforcement (see GROK_RESTRICTED_PREFIX); Claude's restricted btw runs tool-less.
|
|
515
|
-
export function backendSupports(_backend: "agy" | "claude" | "grok" | "codex" | "muse", _feature: BackendFeature): boolean {
|
|
516
|
-
return true;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
514
|
type ClaudeEvent = {
|
|
520
515
|
type?: unknown;
|
|
521
516
|
subtype?: unknown;
|
package/bro.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { hasBroCustomUi, broModalRows, canBroInsertIntoEditor, insertBroDesktopText } from "./ui-capabilities.ts";
|
|
2
3
|
import { createHash } from "node:crypto";
|
|
3
4
|
import { lookup } from "node:dns/promises";
|
|
4
5
|
import { mkdir, mkdtemp, readdir, readFile, realpath, rm, stat, writeFile } from "node:fs/promises";
|
|
@@ -9,7 +10,7 @@ import { tmpdir } from "node:os";
|
|
|
9
10
|
import { extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
10
11
|
import { stripVTControlCharacters } from "node:util";
|
|
11
12
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext, SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
12
|
-
import { Container, Editor, Input, Markdown, SettingsList, SelectList, Text, matchesKey, truncateToWidth, visibleWidth, type Component, type EditorTheme, type Focusable, type SelectItem, type SettingItem, type TUI } from "@earendil-works/pi-tui";
|
|
13
|
+
import { Container, Editor, Input, Markdown, SettingsList, SelectList, Text, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi, type Component, type EditorTheme, type Focusable, type SelectItem, type SettingItem, type TUI } from "@earendil-works/pi-tui";
|
|
13
14
|
import { convertToLlm, copyToClipboard, getAgentDir, getMarkdownTheme, getSelectListTheme, getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
14
15
|
import { Defuddle } from "defuddle/node";
|
|
15
16
|
import { parseHTML } from "linkedom";
|
|
@@ -25,7 +26,6 @@ import {
|
|
|
25
26
|
GROK_EFFORTS,
|
|
26
27
|
CODEX_EFFORTS,
|
|
27
28
|
MUSE_EFFORTS,
|
|
28
|
-
backendSupports,
|
|
29
29
|
execute as executeBackend,
|
|
30
30
|
parseBtwAgyLine,
|
|
31
31
|
type AgySelection,
|
|
@@ -111,7 +111,6 @@ export const MUSE_MODELS = [
|
|
|
111
111
|
{ id: "muse-spark-1.3-contributor", label: "Muse Spark 1.3 Contributor" },
|
|
112
112
|
{ id: "muse-spark-1.3", label: "Muse Spark 1.3" },
|
|
113
113
|
] as const;
|
|
114
|
-
export { backendSupports as supportsBackend };
|
|
115
114
|
type AgyModelFamily = {
|
|
116
115
|
id: string;
|
|
117
116
|
label: string;
|
|
@@ -131,6 +130,7 @@ export function setRegularMouseReporting(tui: Pick<TuiLike, "mode" | "terminal">
|
|
|
131
130
|
}
|
|
132
131
|
|
|
133
132
|
const COMMANDS = [
|
|
133
|
+
{ value: "simplify", label: "simplify", description: "Explain the latest completed assistant reply (same as /bro)" },
|
|
134
134
|
{ value: "text", label: "text", description: "Explain pasted text, or the latest reply when text is omitted" },
|
|
135
135
|
{ value: "file", label: "file", description: "Explain a local document" },
|
|
136
136
|
{ value: "url", label: "url", description: "Explain a public webpage" },
|
|
@@ -1323,10 +1323,6 @@ async function doctorReport(pi: ExtensionAPI, ctx: ExtensionCommandContext, sign
|
|
|
1323
1323
|
continue;
|
|
1324
1324
|
}
|
|
1325
1325
|
if (backend === "grok") {
|
|
1326
|
-
if (!backendSupports("grok", capability)) {
|
|
1327
|
-
fail(label, `\`${capability}\` is not supported on the Grok backend for this capability. Run \`/bro config\` to give it another backend.`);
|
|
1328
|
-
continue;
|
|
1329
|
-
}
|
|
1330
1326
|
if (!isGrokEffort(pair.effort)) {
|
|
1331
1327
|
fail(label, `\`${pair.effort}\` is unsupported for grok \`${pair.model}\`. Run \`/bro config\` to fix this.`);
|
|
1332
1328
|
continue;
|
|
@@ -1340,10 +1336,6 @@ async function doctorReport(pi: ExtensionAPI, ctx: ExtensionCommandContext, sign
|
|
|
1340
1336
|
continue;
|
|
1341
1337
|
}
|
|
1342
1338
|
if (backend === "codex") {
|
|
1343
|
-
if (!backendSupports("codex", capability)) {
|
|
1344
|
-
fail(label, `\`${capability}\` is not supported on the Codex backend for this capability. Run \`/bro config\` to give it another backend.`);
|
|
1345
|
-
continue;
|
|
1346
|
-
}
|
|
1347
1339
|
if (!isCodexEffort(pair.effort)) {
|
|
1348
1340
|
fail(label, `\`${pair.effort}\` is unsupported for codex \`${pair.model}\`. Run \`/bro config\` to fix this.`);
|
|
1349
1341
|
continue;
|
|
@@ -1357,10 +1349,6 @@ async function doctorReport(pi: ExtensionAPI, ctx: ExtensionCommandContext, sign
|
|
|
1357
1349
|
continue;
|
|
1358
1350
|
}
|
|
1359
1351
|
if (backend === "muse") {
|
|
1360
|
-
if (!backendSupports("muse", capability)) {
|
|
1361
|
-
fail(label, `\`${capability}\` is not supported on the Muse backend for this capability. Run \`/bro config\` to give it another backend.`);
|
|
1362
|
-
continue;
|
|
1363
|
-
}
|
|
1364
1352
|
if (!isMuseEffort(pair.effort)) {
|
|
1365
1353
|
fail(label, `\`${pair.effort}\` is unsupported for muse \`${pair.model}\`. Run \`/bro config\` to fix this.`);
|
|
1366
1354
|
continue;
|
|
@@ -1818,6 +1806,14 @@ export type AdvisorActivityCallback = (label: string, timestamp: number) => void
|
|
|
1818
1806
|
// "Transport" for why stdin rather than --print). This function keeps its exact existing
|
|
1819
1807
|
// signature/throw contract -- it is called directly by tests and by runAdvisorWithRetries below,
|
|
1820
1808
|
// which owns the 3-attempt retry/backoff policy the backend itself never performs.
|
|
1809
|
+
export class TerminalConsultError extends Error {
|
|
1810
|
+
readonly status: "failure" | "cancelled" | "timeout";
|
|
1811
|
+
constructor(status: "failure" | "cancelled" | "timeout", message: string) {
|
|
1812
|
+
super(message);
|
|
1813
|
+
this.status = status;
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1821
1817
|
export async function runAdvisorConsultation(
|
|
1822
1818
|
prompt: string,
|
|
1823
1819
|
selection: BackendSelection,
|
|
@@ -1825,6 +1821,7 @@ export async function runAdvisorConsultation(
|
|
|
1825
1821
|
signal: AbortSignal,
|
|
1826
1822
|
killEscalationMs = 5_000,
|
|
1827
1823
|
onActivity?: AdvisorActivityCallback,
|
|
1824
|
+
deadlineMs?: number,
|
|
1828
1825
|
): Promise<string> {
|
|
1829
1826
|
const outcome = await executeBackend(
|
|
1830
1827
|
{ feature: "advisor", prompt, access: "workspace-full", cwd },
|
|
@@ -1835,10 +1832,10 @@ export async function runAdvisorConsultation(
|
|
|
1835
1832
|
if (progress.kind === "activity") onActivity(progress.label, progress.timestamp);
|
|
1836
1833
|
}
|
|
1837
1834
|
: undefined,
|
|
1838
|
-
{ killEscalationMs },
|
|
1835
|
+
{ killEscalationMs, deadlineMs },
|
|
1839
1836
|
);
|
|
1840
1837
|
if (outcome.status === "success") return outcome.text;
|
|
1841
|
-
throw new
|
|
1838
|
+
throw new TerminalConsultError(outcome.status, outcome.message);
|
|
1842
1839
|
}
|
|
1843
1840
|
|
|
1844
1841
|
function advisorDelay(ms: number, signal: AbortSignal, onTick?: (remainingMs: number) => void): Promise<void> {
|
|
@@ -1992,7 +1989,8 @@ export async function runAdvisorWithRetries(
|
|
|
1992
1989
|
} catch (error) {
|
|
1993
1990
|
if (progressTimer) clearInterval(progressTimer);
|
|
1994
1991
|
stopActivityThrottle();
|
|
1995
|
-
if (signal.aborted || errorMessage(error) === "Canceled."
|
|
1992
|
+
if (signal.aborted || errorMessage(error) === "Canceled." ||
|
|
1993
|
+
(error instanceof TerminalConsultError && error.status !== "failure")) throw error;
|
|
1996
1994
|
lastError = error;
|
|
1997
1995
|
if (attempt === delays.length) break;
|
|
1998
1996
|
const delay = delays[attempt]!;
|
|
@@ -2265,7 +2263,7 @@ export function createConfigModal(
|
|
|
2265
2263
|
: backend === "muse"
|
|
2266
2264
|
? `${MUSE_OPTION_PREFIX}${override.model}`
|
|
2267
2265
|
: (resolved.family?.id ?? override.model);
|
|
2268
|
-
rows.effort.currentValue =
|
|
2266
|
+
rows.effort.currentValue = effortDisplay(resolved, backend);
|
|
2269
2267
|
rows.effort.values =
|
|
2270
2268
|
backend === "claude"
|
|
2271
2269
|
? ["default", ...CLAUDE_EFFORTS]
|
|
@@ -2502,7 +2500,7 @@ export function createConfigModal(
|
|
|
2502
2500
|
}
|
|
2503
2501
|
|
|
2504
2502
|
export async function showBroConfigModal(ctx: ExtensionCommandContext, pi: ExtensionAPI): Promise<void> {
|
|
2505
|
-
if (ctx
|
|
2503
|
+
if (!hasBroCustomUi(ctx)) {
|
|
2506
2504
|
ctx.ui.notify("Use /bro config in Pi's interactive UI.", "warning");
|
|
2507
2505
|
return;
|
|
2508
2506
|
}
|
|
@@ -2619,7 +2617,7 @@ export function createAdvisorSteerModal(
|
|
|
2619
2617
|
}
|
|
2620
2618
|
|
|
2621
2619
|
export async function showAdvisorSteerModal(ctx: ExtensionCommandContext, pi: ExtensionAPI): Promise<void> {
|
|
2622
|
-
if (ctx
|
|
2620
|
+
if (!hasBroCustomUi(ctx)) {
|
|
2623
2621
|
ctx.ui.notify("Use /bro advisor-steer in Pi's interactive UI.", "warning");
|
|
2624
2622
|
return;
|
|
2625
2623
|
}
|
|
@@ -2655,7 +2653,7 @@ Quick reference. The README is the full user guide: https://github.com/tranhoang
|
|
|
2655
2653
|
|
|
2656
2654
|
## Explain and show
|
|
2657
2655
|
|
|
2658
|
-
- \`/bro\` — explain the latest completed assistant reply
|
|
2656
|
+
- \`/bro\` or \`/bro simplify\` — explain the latest completed assistant reply
|
|
2659
2657
|
- \`/bro text [text]\` — explain pasted text, or the latest reply when text is omitted
|
|
2660
2658
|
- \`/bro file <path>\` — explain a workspace \`.md\`, \`.markdown\`, \`.txt\`, \`.pdf\`, or \`.docx\` file
|
|
2661
2659
|
- \`/bro url <url>\` — explain one public webpage
|
|
@@ -2816,7 +2814,7 @@ class BroModal implements Focusable {
|
|
|
2816
2814
|
render(width: number): string[] {
|
|
2817
2815
|
const dialogWidth = Math.max(24, width);
|
|
2818
2816
|
const innerWidth = Math.max(22, dialogWidth - 2);
|
|
2819
|
-
const terminalRows = this.tui
|
|
2817
|
+
const terminalRows = broModalRows(this.tui, process.stdout.rows);
|
|
2820
2818
|
const dialogHeight = Math.min(32, Math.max(7, Math.floor(terminalRows * 0.78)));
|
|
2821
2819
|
this.bodyHeight = Math.max(1, dialogHeight - 6);
|
|
2822
2820
|
|
|
@@ -2924,7 +2922,7 @@ interface BroModalOptions {
|
|
|
2924
2922
|
}
|
|
2925
2923
|
|
|
2926
2924
|
async function showBroModal(ctx: ExtensionCommandContext, options: BroModalOptions): Promise<void> {
|
|
2927
|
-
if (ctx
|
|
2925
|
+
if (!hasBroCustomUi(ctx)) {
|
|
2928
2926
|
if (options.run && !options.result && options.text === undefined) {
|
|
2929
2927
|
const result = await options.run(new AbortController().signal);
|
|
2930
2928
|
options.onResult?.(result);
|
|
@@ -3189,6 +3187,7 @@ class BtwModal implements Focusable {
|
|
|
3189
3187
|
}
|
|
3190
3188
|
|
|
3191
3189
|
setNotice(notice: string): void {
|
|
3190
|
+
if (this.disposed) return;
|
|
3192
3191
|
this.notice = notice;
|
|
3193
3192
|
this.tui.requestRender();
|
|
3194
3193
|
}
|
|
@@ -3253,9 +3252,16 @@ class BtwModal implements Focusable {
|
|
|
3253
3252
|
render(width: number): string[] {
|
|
3254
3253
|
const dialogWidth = Math.max(24, width);
|
|
3255
3254
|
const innerWidth = Math.max(22, dialogWidth - 2);
|
|
3256
|
-
const terminalRows = this.tui
|
|
3255
|
+
const terminalRows = broModalRows(this.tui, process.stdout.rows);
|
|
3257
3256
|
const dialogHeight = Math.min(34, Math.max(8, Math.floor(terminalRows * 0.82)));
|
|
3258
|
-
|
|
3257
|
+
const footer = [
|
|
3258
|
+
...wrapTextWithAnsi(this.theme.fg("accent", this.theme.bold("Your message to Bro")), innerWidth),
|
|
3259
|
+
this.input.render(innerWidth)[0] ?? "",
|
|
3260
|
+
...wrapTextWithAnsi(this.running ? "Bro is thinking… · Esc: cancel response" : "Enter: send message · Esc: close panel", innerWidth),
|
|
3261
|
+
...wrapTextWithAnsi("Commands: /mode · /copy · /copy-all · /insert · /insert-all · /clear · /retry", innerWidth),
|
|
3262
|
+
...(this.notice ? wrapTextWithAnsi(this.theme.fg("accent", this.notice), innerWidth) : []),
|
|
3263
|
+
];
|
|
3264
|
+
this.bodyHeight = Math.max(1, dialogHeight - 5 - footer.length);
|
|
3259
3265
|
|
|
3260
3266
|
const rendered = this.markdown.render(innerWidth);
|
|
3261
3267
|
this.maxOffset = Math.max(0, rendered.length - this.bodyHeight);
|
|
@@ -3268,12 +3274,6 @@ class BtwModal implements Focusable {
|
|
|
3268
3274
|
const mode = this.theme.fg("dim", " · ") + (this.full ? this.theme.fg("accent", this.theme.bold(btwModeLabel(true))) : this.theme.fg("dim", btwModeLabel(false)));
|
|
3269
3275
|
const header = this.theme.fg("accent", this.theme.bold("Bro · btw")) + model + mode + this.theme.fg("dim", scroll);
|
|
3270
3276
|
|
|
3271
|
-
const composer = this.input.render(innerWidth)[0] ?? "";
|
|
3272
|
-
|
|
3273
|
-
const controls = this.running
|
|
3274
|
-
? this.theme.fg("dim", "Thinking… · Esc cancel")
|
|
3275
|
-
: this.theme.fg("dim", "Enter ask · Esc close · /mode · /copy · /copy-all · /insert · /insert-all · /clear · /retry");
|
|
3276
|
-
|
|
3277
3277
|
const lines = [
|
|
3278
3278
|
this.borderLine(innerWidth, "top"),
|
|
3279
3279
|
this.frameLine(header, innerWidth),
|
|
@@ -3282,8 +3282,7 @@ class BtwModal implements Focusable {
|
|
|
3282
3282
|
for (const line of visible) lines.push(this.frameLine(line, innerWidth));
|
|
3283
3283
|
for (let i = visible.length; i < this.bodyHeight; i++) lines.push(this.frameLine("", innerWidth));
|
|
3284
3284
|
lines.push(this.ruleLine(innerWidth));
|
|
3285
|
-
lines.push(this.frameLine(
|
|
3286
|
-
lines.push(this.frameLine(this.notice ? this.theme.fg("accent", this.notice) : controls, innerWidth));
|
|
3285
|
+
for (const line of footer) lines.push(this.frameLine(line, innerWidth));
|
|
3287
3286
|
lines.push(this.borderLine(innerWidth, "bottom"));
|
|
3288
3287
|
return lines;
|
|
3289
3288
|
}
|
|
@@ -3425,12 +3424,17 @@ async function openBtwModal(
|
|
|
3425
3424
|
}
|
|
3426
3425
|
};
|
|
3427
3426
|
|
|
3428
|
-
const insert = (all: boolean) => {
|
|
3427
|
+
const insert = async (all: boolean) => {
|
|
3429
3428
|
const text = all ? transcript() : (thread.turns.at(-1)?.answer ?? "");
|
|
3430
3429
|
if (!text.trim()) {
|
|
3431
3430
|
modal.setNotice("Nothing to insert yet.");
|
|
3432
3431
|
return;
|
|
3433
3432
|
}
|
|
3433
|
+
if (!canBroInsertIntoEditor(ctx)) {
|
|
3434
|
+
const result = await insertBroDesktopText(ctx, text);
|
|
3435
|
+
modal.setNotice(result === "inserted" ? "Inserted into the main editor. Review it before sending." : result === "not_empty" ? "Main editor has a draft or attachment. Clear it first, then insert again." : "Could not confirm insertion. Check the main editor before trying again.");
|
|
3436
|
+
return;
|
|
3437
|
+
}
|
|
3434
3438
|
if (ctx.ui.getEditorText().trim()) {
|
|
3435
3439
|
modal.setNotice("Main editor has a draft. Edit or clear it first, then insert again.");
|
|
3436
3440
|
return;
|
|
@@ -3629,6 +3633,11 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
3629
3633
|
let action = parts[0] ?? "";
|
|
3630
3634
|
let value = raw.slice(raw.split(/\s+/, 1)[0]?.length ?? 0).trim();
|
|
3631
3635
|
|
|
3636
|
+
if (action === "simplify") {
|
|
3637
|
+
if (value) { ctx.ui.notify("Use /bro simplify for the latest reply, or /bro text <text> for pasted text.", "warning"); return; }
|
|
3638
|
+
action = "";
|
|
3639
|
+
}
|
|
3640
|
+
|
|
3632
3641
|
// Removed commands must never fall through to a paid text explanation.
|
|
3633
3642
|
if (action === "model" || action === "effort") {
|
|
3634
3643
|
ctx.ui.notify(`/bro ${action} was removed. Use /bro config to choose the shared default and per-capability ${action}; use /bro text <text> to explain text.`, "warning");
|
|
@@ -3750,7 +3759,7 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
3750
3759
|
const settings = await readSettings();
|
|
3751
3760
|
let selected = parseBroMode(requested);
|
|
3752
3761
|
if (!selected) {
|
|
3753
|
-
if (ctx
|
|
3762
|
+
if (!hasBroCustomUi(ctx)) {
|
|
3754
3763
|
ctx.ui.notify("Use /bro mode <brief|balanced|faithful> outside Pi's interactive UI.", "warning");
|
|
3755
3764
|
return;
|
|
3756
3765
|
}
|
|
@@ -3770,7 +3779,7 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
3770
3779
|
}
|
|
3771
3780
|
|
|
3772
3781
|
if (action === "btw") {
|
|
3773
|
-
if (ctx
|
|
3782
|
+
if (!hasBroCustomUi(ctx)) {
|
|
3774
3783
|
ctx.ui.notify("Use /bro btw in Pi's interactive UI.", "warning");
|
|
3775
3784
|
return;
|
|
3776
3785
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-bro",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.5",
|
|
4
4
|
"description": "An Earendil Pi extension that explains pasted text, assistant responses, local documents, public webpages, and recent session turns (as shapes) in a context-isolated window, opens a sandboxed side conversation with /bro btw, and provides a second-opinion advisor tool for executor agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
],
|
|
38
38
|
"files": [
|
|
39
39
|
"bro.ts",
|
|
40
|
+
"ui-capabilities.ts",
|
|
40
41
|
"backend.ts",
|
|
41
42
|
"prompt.ts",
|
|
42
43
|
"README.md",
|
|
@@ -53,7 +54,7 @@
|
|
|
53
54
|
},
|
|
54
55
|
"scripts": {
|
|
55
56
|
"typecheck": "tsc --noEmit",
|
|
56
|
-
"test": "npm run typecheck && node --test prompt.test.ts backend.test.ts claude.test.ts grok.test.ts codex.test.ts muse.test.ts settings.test.ts show-html.test.ts smoke-rpc.test.ts benchmark/*.test.ts && sh ./smoke-test.sh",
|
|
57
|
+
"test": "npm run typecheck && node --test helpers.test.mjs prompt.test.ts backend.test.ts claude.test.ts grok.test.ts codex.test.ts muse.test.ts settings.test.ts show-html.test.ts smoke-rpc.test.ts ui-capabilities.test.ts benchmark/*.test.ts && sh ./smoke-test.sh",
|
|
57
58
|
"benchmark:dry-run": "node benchmark/run.ts dry-run",
|
|
58
59
|
"benchmark:run": "node benchmark/run.ts run",
|
|
59
60
|
"benchmark:report": "node benchmark/run.ts report",
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Local structural contract. This does not extend, patch, or impersonate the upstream Pi API. */
|
|
2
|
+
type UiContext = { mode: string; ui: unknown };
|
|
3
|
+
type DesktopUi = { getDesktopUiCapabilities?: () => unknown };
|
|
4
|
+
|
|
5
|
+
export function hasBroCustomUi(ctx: UiContext): boolean {
|
|
6
|
+
if (ctx.mode === "tui") return true;
|
|
7
|
+
if (ctx.mode !== "rpc" || !ctx.ui || typeof ctx.ui !== "object") return false;
|
|
8
|
+
const query = (ctx.ui as DesktopUi).getDesktopUiCapabilities;
|
|
9
|
+
if (typeof query !== "function") return false;
|
|
10
|
+
try {
|
|
11
|
+
const value = query.call(ctx.ui);
|
|
12
|
+
if (!value || typeof value !== "object") return false;
|
|
13
|
+
const capability = value as { version?: unknown; customTui?: unknown; viewport?: unknown };
|
|
14
|
+
return capability.version === 1 && capability.customTui === true && capability.viewport === true;
|
|
15
|
+
} catch { return false; }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Read a virtual terminal's actual viewport in desktop mode; preserve terminal behavior elsewhere. */
|
|
19
|
+
export function broModalRows(tui: unknown, terminalRows: number | undefined): number {
|
|
20
|
+
const rows = tui && typeof tui === "object" ? (tui as { terminal?: { rows?: unknown } }).terminal?.rows : undefined;
|
|
21
|
+
const candidate = typeof rows === "number" && Number.isFinite(rows) && rows > 0 ? rows : terminalRows;
|
|
22
|
+
return typeof candidate === "number" && Number.isFinite(candidate) && candidate > 0 ? Math.floor(candidate) : 30;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function insertBroDesktopText(ctx: UiContext, text: string): Promise<"inserted" | "not_empty" | "unavailable"> {
|
|
26
|
+
if (!hasBroCustomUi(ctx) || ctx.mode !== "rpc") return "unavailable";
|
|
27
|
+
const ui = ctx.ui as DesktopUi & { insertEditorTextIfEmpty?: (text: string) => Promise<"inserted" | "not_empty" | "unavailable"> };
|
|
28
|
+
try {
|
|
29
|
+
const caps = ui.getDesktopUiCapabilities?.() as { atomicEditorInsert?: boolean } | undefined;
|
|
30
|
+
return caps?.atomicEditorInsert === true && ui.insertEditorTextIfEmpty ? await ui.insertEditorTextIfEmpty(text) : "unavailable";
|
|
31
|
+
} catch { return "unavailable"; }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Only terminal mode uses synchronous insertion; Desktop uses insertBroDesktopText instead. */
|
|
35
|
+
export function canBroInsertIntoEditor(ctx: UiContext): boolean { return ctx.mode === "tui"; }
|