pi-bro 0.19.0 → 0.19.2
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 +13 -0
- package/README.md +1 -0
- package/bro.ts +49 -20
- package/docs/README.md +8 -0
- package/docs/pig-compatibility.md +42 -0
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to pi-bro are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.19.2] - 2026-09-30
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Preserve Show's diagram controls, captured source, and steering when reopening and rerunning a result. Recreate missing temporary diagrams without a model call and retain the previous diagram after a failed rerun (#71).
|
|
10
|
+
|
|
11
|
+
## [0.19.1] - 2026-09-30
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Always enforce Bro's restrictive content policy on generated HTML, including when model output contains a permissive policy or CSP-looking comment (#69).
|
|
16
|
+
- Isolate smoke-test temporary artifacts and block real backend CLIs during offline tests. Order settings mutations by RPC acknowledgements instead of fixed sleeps, and support spaces in settings-test checkout paths (#70).
|
|
17
|
+
|
|
5
18
|
## [0.19.0] - 2026-09-25
|
|
6
19
|
|
|
7
20
|
### Added
|
package/README.md
CHANGED
|
@@ -5,6 +5,7 @@ plain-language explanation — or open a separate side conversation with
|
|
|
5
5
|
`/bro btw` — without adding anything to your main agent's context.
|
|
6
6
|
|
|
7
7
|
`pi-bro` is an extension for [Earendil Pi](https://github.com/earendil-works/pi).
|
|
8
|
+
Experimental [PiG 0.3.0 compatibility](docs/pig-compatibility.md) uses the same npm package and requires Node.js. See the compatibility notes for verified coverage and the PiG RPC tool-exclusion limitation.
|
|
8
9
|
It opens explanations in a separate modal and runs them through a CLI backend
|
|
9
10
|
you already have installed and signed in to. Explain, show, BTW, and the
|
|
10
11
|
advisor all work across five backends:
|
package/bro.ts
CHANGED
|
@@ -5,12 +5,12 @@ import { mkdir, mkdtemp, readdir, readFile, realpath, rm, stat, writeFile } from
|
|
|
5
5
|
import { request as httpRequest, type IncomingMessage } from "node:http";
|
|
6
6
|
import { request as httpsRequest } from "node:https";
|
|
7
7
|
import { BlockList, isIP } from "node:net";
|
|
8
|
-
import {
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
9
|
import { extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
10
10
|
import { stripVTControlCharacters } from "node:util";
|
|
11
11
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext, SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
12
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 { convertToLlm, copyToClipboard, getMarkdownTheme, getSelectListTheme, getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import { convertToLlm, copyToClipboard, getAgentDir, getMarkdownTheme, getSelectListTheme, getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
14
14
|
import { Defuddle } from "defuddle/node";
|
|
15
15
|
import { parseHTML } from "linkedom";
|
|
16
16
|
import mammoth from "mammoth";
|
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
|
|
36
36
|
export { agyFailureMessage, agySelection, advisorFlagErrorHint, parseBtwAgyLine };
|
|
37
37
|
|
|
38
|
-
const AGENT_DIR =
|
|
38
|
+
const AGENT_DIR = getAgentDir();
|
|
39
39
|
const ENV_MODEL = process.env.PI_BRO_MODEL?.trim();
|
|
40
40
|
const DEFAULT_MODEL = ENV_MODEL || "gemini-3.7-flash";
|
|
41
41
|
const PROMPT_FILE = join(AGENT_DIR, "bro-prompt.md");
|
|
@@ -57,7 +57,7 @@ const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
|
57
57
|
type Theme = ExtensionCommandContext["ui"]["theme"];
|
|
58
58
|
type TuiLike = {
|
|
59
59
|
readonly mode: "regular" | "fullscreen";
|
|
60
|
-
readonly terminal?: { write?: (data: string) => void };
|
|
60
|
+
readonly terminal?: { rows?: number; write?: (data: string) => void };
|
|
61
61
|
requestRender(): void;
|
|
62
62
|
};
|
|
63
63
|
type ModalKind = "loading" | "streaming" | "result" | "help" | "empty" | "error";
|
|
@@ -1561,10 +1561,12 @@ export function showHtmlDirectory(): string {
|
|
|
1561
1561
|
|
|
1562
1562
|
function withShowCsp(html: string): string {
|
|
1563
1563
|
// Defense in depth: the prompt forbids external resources and scripts;
|
|
1564
|
-
// a meta CSP blocks them anyway if the model slips.
|
|
1564
|
+
// a meta CSP blocks them anyway if the model slips. Always inserted: multiple
|
|
1565
|
+
// policies are enforced together, so a model-supplied CSP can only tighten it.
|
|
1566
|
+
// Skip only simple leading comments: browsers accept comment terminators that
|
|
1567
|
+
// a general regex misses. Ambiguous markup gets the policy prepended instead.
|
|
1565
1568
|
const meta = '<meta http-equiv="Content-Security-Policy" content="default-src \'none\'; style-src \'unsafe-inline\'; img-src data:;">';
|
|
1566
|
-
|
|
1567
|
-
return html.replace(/^(\s*(?:<!doctype[^>]*>\s*)?)/i, `$1\n${meta}\n`);
|
|
1569
|
+
return html.replace(/^(\s*(?:<!--[^<>]*-->\s*)*(?:<!doctype[^>]*>\s*)?)/i, `$1\n${meta}\n`);
|
|
1568
1570
|
}
|
|
1569
1571
|
|
|
1570
1572
|
export async function writeShowHtml(html: string): Promise<string> {
|
|
@@ -2798,7 +2800,7 @@ class BroModal implements Focusable {
|
|
|
2798
2800
|
render(width: number): string[] {
|
|
2799
2801
|
const dialogWidth = Math.max(24, width);
|
|
2800
2802
|
const innerWidth = Math.max(22, dialogWidth - 2);
|
|
2801
|
-
const terminalRows = process.stdout.rows ?? 30;
|
|
2803
|
+
const terminalRows = this.tui.terminal?.rows ?? process.stdout.rows ?? 30;
|
|
2802
2804
|
const dialogHeight = Math.min(32, Math.max(7, Math.floor(terminalRows * 0.78)));
|
|
2803
2805
|
this.bodyHeight = Math.max(1, dialogHeight - 6);
|
|
2804
2806
|
|
|
@@ -2940,6 +2942,12 @@ async function showBroModal(ctx: ExtensionCommandContext, options: BroModalOptio
|
|
|
2940
2942
|
options.retryLabel ?? "simplify again",
|
|
2941
2943
|
);
|
|
2942
2944
|
|
|
2945
|
+
const present = (result: ModalResult, notice = "") => {
|
|
2946
|
+
const display = result.htmlPath ? stripShowHtmlFence(result.text) : result.text;
|
|
2947
|
+
modal.setResult(display, options.retryable ?? Boolean(options.run), notice, result.source?.label, result.text, result.model);
|
|
2948
|
+
modal.setHtmlPath(result.htmlPath ?? "");
|
|
2949
|
+
};
|
|
2950
|
+
|
|
2943
2951
|
execute = (source?: BroSource) => {
|
|
2944
2952
|
if (!options.run || controller || closed) return;
|
|
2945
2953
|
const previous = current;
|
|
@@ -2956,16 +2964,14 @@ async function showBroModal(ctx: ExtensionCommandContext, options: BroModalOptio
|
|
|
2956
2964
|
if (closed || nextController.signal.aborted) return;
|
|
2957
2965
|
current = result;
|
|
2958
2966
|
options.onResult?.(result);
|
|
2959
|
-
|
|
2960
|
-
modal.setResult(display, options.retryable ?? true, "", result.source?.label, result.text, result.model);
|
|
2961
|
-
if (result.htmlPath) modal.setHtmlPath(result.htmlPath);
|
|
2967
|
+
present(result);
|
|
2962
2968
|
})
|
|
2963
2969
|
.catch((error) => {
|
|
2964
2970
|
if (closed || nextController.signal.aborted) return;
|
|
2965
2971
|
const message = error instanceof Error ? error.message : String(error);
|
|
2966
2972
|
if (previous) {
|
|
2967
2973
|
current = previous;
|
|
2968
|
-
|
|
2974
|
+
present(previous, `Retry failed: ${message}`);
|
|
2969
2975
|
} else {
|
|
2970
2976
|
modal.setError(message);
|
|
2971
2977
|
}
|
|
@@ -2978,7 +2984,7 @@ async function showBroModal(ctx: ExtensionCommandContext, options: BroModalOptio
|
|
|
2978
2984
|
if (options.text !== undefined) {
|
|
2979
2985
|
modal.setStatic(options.kind ?? "help", options.text, options.copyable ?? false);
|
|
2980
2986
|
} else if (current) {
|
|
2981
|
-
|
|
2987
|
+
present(current);
|
|
2982
2988
|
} else {
|
|
2983
2989
|
execute();
|
|
2984
2990
|
}
|
|
@@ -3231,7 +3237,7 @@ class BtwModal implements Focusable {
|
|
|
3231
3237
|
render(width: number): string[] {
|
|
3232
3238
|
const dialogWidth = Math.max(24, width);
|
|
3233
3239
|
const innerWidth = Math.max(22, dialogWidth - 2);
|
|
3234
|
-
const terminalRows = process.stdout.rows ?? 30;
|
|
3240
|
+
const terminalRows = this.tui.terminal?.rows ?? process.stdout.rows ?? 30;
|
|
3235
3241
|
const dialogHeight = Math.min(34, Math.max(8, Math.floor(terminalRows * 0.82)));
|
|
3236
3242
|
this.bodyHeight = Math.max(1, dialogHeight - 7);
|
|
3237
3243
|
|
|
@@ -3477,10 +3483,14 @@ async function openBtwModal(
|
|
|
3477
3483
|
}
|
|
3478
3484
|
|
|
3479
3485
|
export default async function bro(pi: ExtensionAPI) {
|
|
3480
|
-
let lastResult:
|
|
3486
|
+
let lastResult: ModalResult | undefined;
|
|
3487
|
+
let lastShowSteering: string | undefined;
|
|
3481
3488
|
let btwThread: BtwThread | undefined;
|
|
3482
|
-
const remember = (result: ModalResult) => {
|
|
3483
|
-
if (result.source)
|
|
3489
|
+
const remember = (result: ModalResult, showSteering?: string) => {
|
|
3490
|
+
if (result.source) {
|
|
3491
|
+
lastResult = result;
|
|
3492
|
+
lastShowSteering = showSteering;
|
|
3493
|
+
}
|
|
3484
3494
|
};
|
|
3485
3495
|
|
|
3486
3496
|
pi.on("session_start", async (_event, _ctx) => {
|
|
@@ -3657,7 +3667,7 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
3657
3667
|
loadingText: "Drawing what happened…",
|
|
3658
3668
|
retryLabel: "show again",
|
|
3659
3669
|
run: runShow,
|
|
3660
|
-
onResult: remember,
|
|
3670
|
+
onResult: (result) => remember(result, steering),
|
|
3661
3671
|
});
|
|
3662
3672
|
} catch (error) {
|
|
3663
3673
|
ctx.ui.notify(withDoctor(error), "error");
|
|
@@ -3858,10 +3868,29 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
3858
3868
|
return;
|
|
3859
3869
|
}
|
|
3860
3870
|
|
|
3871
|
+
const steering = lastShowSteering;
|
|
3872
|
+
if (steering !== undefined) {
|
|
3873
|
+
const html = extractShowHtml(lastResult.text);
|
|
3874
|
+
// Temp files may have been cleaned by the OS or another Bro session.
|
|
3875
|
+
if (html && ctx.mode === "tui") {
|
|
3876
|
+
try { lastResult.htmlPath = await writeShowHtml(html); }
|
|
3877
|
+
catch (error) {
|
|
3878
|
+
lastResult.htmlPath = undefined;
|
|
3879
|
+
ctx.ui.notify(`Diagram file unavailable: ${errorMessage(error)}`, "warning");
|
|
3880
|
+
}
|
|
3881
|
+
}
|
|
3882
|
+
}
|
|
3861
3883
|
await showBroModal(ctx, {
|
|
3862
3884
|
result: lastResult,
|
|
3863
|
-
|
|
3864
|
-
|
|
3885
|
+
loadingText: steering !== undefined ? "Drawing what happened…" : undefined,
|
|
3886
|
+
retryLabel: steering !== undefined ? "show again" : undefined,
|
|
3887
|
+
run: steering === undefined ? run : async (signal, source, onProgress) => {
|
|
3888
|
+
if (!source) throw new Error("No captured Show source.");
|
|
3889
|
+
const result = await runShowExplanation(source.text, steering, signal, await readSettings(), onProgress);
|
|
3890
|
+
const html = extractShowHtml(result.text);
|
|
3891
|
+
return { source, ...result, ...(html ? { htmlPath: await writeShowHtml(html) } : {}) };
|
|
3892
|
+
},
|
|
3893
|
+
onResult: (result) => remember(result, steering),
|
|
3865
3894
|
});
|
|
3866
3895
|
return;
|
|
3867
3896
|
}
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# pi-bro docs
|
|
2
|
+
|
|
3
|
+
- [README](../README.md) — the user guide (authoritative for behavior).
|
|
4
|
+
- [DEVELOPMENT.md](DEVELOPMENT.md) — code map, settings and BTW continuation model, invariants, test commands.
|
|
5
|
+
- [TESTING.md](TESTING.md) — the manual end-to-end checklist across backends.
|
|
6
|
+
- [plans/](plans/README.md) — historical design notes and plans; not the current spec.
|
|
7
|
+
- [images/](images/) — screenshots used by the README.
|
|
8
|
+
- [benchmark/README.md](../benchmark/README.md) — the manual, live prompt benchmark.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# PiG compatibility
|
|
2
|
+
|
|
3
|
+
Bro uses one TypeScript package on Pi and Pi-in-Go (PiG). PiG runs Bro in a Node subprocess; it is not a Go port or a fused native extension.
|
|
4
|
+
|
|
5
|
+
## Target and installation
|
|
6
|
+
|
|
7
|
+
PiG **0.3.0+0.87.1**, Node **>=22.19.0**. Compatibility is experimental; the checks below are not a guarantee for every PiG release or platform.
|
|
8
|
+
|
|
9
|
+
Install the PiG binary from https://github.com/MichaelKinsy/PiG/releases/tag/v0.3.0, then:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
pig install npm:pi-bro
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
That command installs the published Bro version; these compatibility changes take effect only after a Bro release containing them. To test this checkout now, install dependencies and use `pig -e ./bro.ts`.
|
|
16
|
+
|
|
17
|
+
At initial verification, `@pi-in-go/pig@0.3.0` was not available from npm. CI therefore downloads the pinned GitHub binary and verifies its published SHA-256 checksum.
|
|
18
|
+
|
|
19
|
+
Bro follows the host's `getAgentDir()`: normally `~/.pi/agent` on Pi and `~/.pig/agent` on PiG. PiG supports `PIG_CODING_AGENT_DIR`, `PIG_HOME`, XDG configuration and explicit `PIG_USE_PI_DIRS=1` sharing. No automatic migration or copying of existing Bro settings takes place. `PI_BRO_MODEL` remains the same Bro-specific override on both hosts.
|
|
20
|
+
|
|
21
|
+
## Known limitations
|
|
22
|
+
|
|
23
|
+
- **PiG 0.3.0 RPC does not enforce `--exclude-tools`.** Excluded tools may remain active and reach provider requests. This is not a sandbox boundary. Use an explicit `--tools` allowlist instead; do not rely on `/bro doctor` to prove exclusion. Bro does not mask this defect by falsely reporting an active tool as unavailable.
|
|
24
|
+
- The smoke suite retains Pi's strict exclusion assertion. The PiG lane has an explicit version-specific expected failure; a changed result fails the lane so the exception must be reviewed/removed.
|
|
25
|
+
- Mouse-wheel forwarding is not qualified; use keyboard scrolling.
|
|
26
|
+
- Remote/SSH OSC 52 clipboard forwarding is not qualified. Local clipboard behavior depends on the host and installed platform utilities.
|
|
27
|
+
- Windows support is not qualified by these checks.
|
|
28
|
+
|
|
29
|
+
## Verification and release gate
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
npm test
|
|
33
|
+
PIG_BIN=/absolute/path/to/pig sh pig-test.sh
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The PiG lane checks the exact binary version, real host settings isolation, advisor snapshot compaction/branch projection, and the existing fake-backend RPC smoke suite. Existing Node tests cover backend cancellation/process cleanup and modal geometry, but are not substitutes for real-host interactive tests.
|
|
37
|
+
|
|
38
|
+
CI and publishing call `.github/workflows/pig.yml`; publication requires both host lanes. A green PiG job includes the clearly printed RPC exclusion expected failure, not a claim of complete parity.
|
|
39
|
+
|
|
40
|
+
Manual macOS terminal checks confirmed help rendering/scrolling/resize, opening the BTW composer and switching `/mode`, and the advisor-steering editor displaying `Saved` after a correctly encoded Ctrl+S. Full end-to-end advisor model execution, interactive configuration persistence, clipboard, and host reload/shutdown with active backend processes still require qualification. No paid backend calls are part of CI.
|
|
41
|
+
|
|
42
|
+
Tracking: https://github.com/tranhoangnguyen03/pi-bro/issues/64 and the original loading fix https://github.com/MichaelKinsy/PiG/issues/88.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-bro",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.2",
|
|
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",
|
|
@@ -42,7 +42,8 @@
|
|
|
42
42
|
"README.md",
|
|
43
43
|
"CHANGELOG.md",
|
|
44
44
|
"LICENSE",
|
|
45
|
-
"THIRD_PARTY_NOTICES.md"
|
|
45
|
+
"THIRD_PARTY_NOTICES.md",
|
|
46
|
+
"docs/pig-compatibility.md"
|
|
46
47
|
],
|
|
47
48
|
"publishConfig": {
|
|
48
49
|
"access": "public"
|
|
@@ -52,7 +53,7 @@
|
|
|
52
53
|
},
|
|
53
54
|
"scripts": {
|
|
54
55
|
"typecheck": "tsc --noEmit",
|
|
55
|
-
"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 benchmark/*.test.ts && sh ./smoke-test.sh",
|
|
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",
|
|
56
57
|
"benchmark:dry-run": "node benchmark/run.ts dry-run",
|
|
57
58
|
"benchmark:run": "node benchmark/run.ts run",
|
|
58
59
|
"benchmark:report": "node benchmark/run.ts report",
|