pi-bro 0.13.0 → 0.13.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 CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  All notable changes to pi-bro are documented here.
4
4
 
5
+ ## [0.13.2] - 2026-09-19
6
+
7
+ ### Changed
8
+
9
+ - In the `/bro btw` modal, your questions now render as a quoted **You** block and answers carry an explicit **Bro** label, with a horizontal rule between turns. The old `## you` heading was indistinguishable from headings inside Bro's answers.
10
+
11
+ ## [0.13.1] - 2026-09-17
12
+
13
+ ### Changed
14
+
15
+ - Renamed `/bro btw` composer commands from `/send` and `/send all` to `/copy` and `/copy-all` (also accepts `/copy all`), with `/copy!` and `/copy-all!` force variants to replace existing main-editor drafts. Legacy `/send` commands remain supported as aliases.
16
+
5
17
  ## [0.13.0] - 2026-09-15
6
18
 
7
19
  ### Added
package/README.md CHANGED
@@ -108,7 +108,7 @@ your terminal mode; press **C** to copy the complete explanation reliably.
108
108
  `/bro btw` opens a separate multi-turn conversation in a modal, so you can ask
109
109
  a quick side question while the main agent keeps working. It runs through Agy,
110
110
  the same backend as the rest of Bro, and never adds anything to Pi's
111
- conversation unless you explicitly send it back.
111
+ conversation unless you explicitly copy it into the editor.
112
112
 
113
113
  - **Sandboxed by default**: the side conversation is read-only (no project
114
114
  access). Add `--full` to let it read and edit the workspace.
@@ -119,8 +119,8 @@ conversation unless you explicitly send it back.
119
119
  conversation text (40,000 characters max, with a truncation notice); the
120
120
  side agent can also read the repo itself when running in `--full` mode.
121
121
  - **In the modal**: type a question and press Enter (empty Enter re-asks the
122
- last question). `/send` copies the latest answer into the main editor
123
- without submitting (use `/send!` to replace an existing draft); `/send all`
122
+ last question). `/copy` copies the latest answer into the main editor
123
+ without submitting (use `/copy!` to replace an existing draft); `/copy-all`
124
124
  copies the full thread; `/retry` re-asks
125
125
  the last question; `/clear` resets the thread; Esc closes. A visible
126
126
  `full · edits repo` badge shows whenever `--full` mode is active.
package/bro.ts CHANGED
@@ -1098,7 +1098,7 @@ Press **R** to simplify the captured source again. Run a new \`/bro text\`, \`/b
1098
1098
 
1099
1099
  ## Side conversation
1100
1100
 
1101
- - \`/bro btw [--fresh] [--full] [question]\` — open a side conversation. Sandboxed (read-only) by default; add \`--full\` to let it read and edit the workspace, and \`--fresh\` to start without main-session context. Inside the side thread, type questions and press Enter (empty Enter re-asks); \`/send\` copies the latest answer to the main editor without submitting (use \`/send!\` to replace an existing draft), \`/send all\` the full thread, \`/retry\` re-asks the last question, and \`/clear\` resets the thread. Esc closes.
1101
+ - \`/bro btw [--fresh] [--full] [question]\` — open a side conversation. Sandboxed (read-only) by default; add \`--full\` to let it read and edit the workspace, and \`--fresh\` to start without main-session context. Inside the side thread, type questions and press Enter (empty Enter re-asks); \`/copy\` copies the latest answer to the main editor without submitting (use \`/copy!\` to replace an existing draft), \`/copy-all\` the full thread, \`/retry\` re-asks the last question, and \`/clear\` resets the thread. Esc closes.
1102
1102
 
1103
1103
  ## Current settings
1104
1104
 
@@ -1475,6 +1475,18 @@ export function resolveBtwThread(existing: BtwThread | undefined, parsed: { fres
1475
1475
  return !existing || startFresh ? { turns: [], full: targetFull } : existing;
1476
1476
  }
1477
1477
 
1478
+ export function formatBtwTranscript(turns: readonly BtwTurn[]): string {
1479
+ return turns
1480
+ .map((turn) => {
1481
+ const question = turn.question.split(/\r?\n/).map((line) => (line ? `> ${line}` : ">")).join("\n");
1482
+ // A turn aborted mid-stream can end inside an unclosed code fence, which would swallow the
1483
+ // separator and every later turn; close it so each turn renders as its own block.
1484
+ const answer = (turn.answer.match(/^```/gm)?.length ?? 0) % 2 === 1 ? `${turn.answer}\n\`\`\`` : turn.answer;
1485
+ return `> **You**\n>\n${question}\n\n**Bro**\n\n${answer}`;
1486
+ })
1487
+ .join("\n\n---\n\n");
1488
+ }
1489
+
1478
1490
  export function parseBtwAgyLine(line: string): { delta?: string; result?: string; conversationId?: string; error?: string } {
1479
1491
  let event: AgyEvent;
1480
1492
  try {
@@ -1496,6 +1508,37 @@ export function parseBtwAgyLine(line: string): { delta?: string; result?: string
1496
1508
  return { conversationId };
1497
1509
  }
1498
1510
 
1511
+ export type BtwComposerAction =
1512
+ | { kind: "clear" }
1513
+ | { kind: "retry" }
1514
+ | { kind: "copy"; all: boolean; force: boolean }
1515
+ | { kind: "question"; text: string };
1516
+
1517
+ export function parseBtwComposerCommand(value: string): BtwComposerAction {
1518
+ const command = value.trim();
1519
+ if (command === "/clear") return { kind: "clear" };
1520
+ if (command === "/retry" || command === "") return { kind: "retry" };
1521
+ if (
1522
+ command === "/copy" ||
1523
+ command === "/copy!" ||
1524
+ command === "/copy-all" ||
1525
+ command === "/copy-all!" ||
1526
+ command === "/copy all" ||
1527
+ command === "/copy all!" ||
1528
+ command === "/send" ||
1529
+ command === "/send!" ||
1530
+ command === "/send all" ||
1531
+ command === "/send all!"
1532
+ ) {
1533
+ const all =
1534
+ command.startsWith("/copy-all") ||
1535
+ command.startsWith("/copy all") ||
1536
+ command.startsWith("/send all");
1537
+ return { kind: "copy", all, force: command.endsWith("!") };
1538
+ }
1539
+ return { kind: "question", text: command };
1540
+ }
1541
+
1499
1542
  async function runBtwTurn(
1500
1543
  prompt: string,
1501
1544
  selection: ReturnType<typeof agySelection>,
@@ -1725,7 +1768,7 @@ class BtwModal implements Focusable {
1725
1768
 
1726
1769
  const controls = this.running
1727
1770
  ? this.theme.fg("dim", "Thinking… · Esc cancel")
1728
- : this.theme.fg("dim", "Enter ask · Esc close · /send · /send all · /clear · /retry");
1771
+ : this.theme.fg("dim", "Enter ask · Esc close · /copy · /copy-all · /clear · /retry");
1729
1772
 
1730
1773
  const lines = [
1731
1774
  this.borderLine(innerWidth, "top"),
@@ -1760,7 +1803,7 @@ async function openBtwModal(
1760
1803
  let closed = false;
1761
1804
  let controller: AbortController | undefined;
1762
1805
 
1763
- const transcript = () => thread.turns.map((turn) => `## you\n${turn.question}\n\n${turn.answer}`).join("\n\n");
1806
+ const transcript = () => formatBtwTranscript(thread.turns);
1764
1807
 
1765
1808
  const close = () => {
1766
1809
  if (closed) return;
@@ -1845,35 +1888,35 @@ async function openBtwModal(
1845
1888
  const handoff = (all: boolean, force: boolean) => {
1846
1889
  const text = all ? transcript() : (thread.turns.at(-1)?.answer ?? "");
1847
1890
  if (!text.trim()) {
1848
- modal.setNotice("Nothing to send yet.");
1891
+ modal.setNotice("Nothing to copy yet.");
1849
1892
  return;
1850
1893
  }
1851
1894
  if (ctx.ui.getEditorText().trim() && !force) {
1852
- modal.setNotice("Main editor has a draft. Use /send! (or /send all!) to replace it.");
1895
+ modal.setNotice("Main editor has a draft. Use /copy! (or /copy-all!) to replace it.");
1853
1896
  return;
1854
1897
  }
1855
1898
  ctx.ui.setEditorText(text);
1856
- modal.setNotice(all ? "Sent the full thread to the editor." : "Sent the latest answer to the editor.");
1899
+ modal.setNotice(all ? "Copied the full thread to the editor." : "Copied the latest answer to the editor.");
1857
1900
  };
1858
1901
 
1859
1902
  function submit(value: string): void {
1860
- const command = value.trim();
1861
- if (command === "/clear") {
1903
+ const action = parseBtwComposerCommand(value);
1904
+ if (action.kind === "clear") {
1862
1905
  modal.clearComposer();
1863
1906
  clear();
1864
1907
  return;
1865
1908
  }
1866
- if (command === "/send" || command === "/send all" || command === "/send!" || command === "/send all!") {
1909
+ if (action.kind === "copy") {
1867
1910
  modal.clearComposer();
1868
- handoff(command === "/send all" || command === "/send all!", command.endsWith("!"));
1911
+ handoff(action.all, action.force);
1869
1912
  return;
1870
1913
  }
1871
- if (command === "/retry" || command === "") {
1914
+ if (action.kind === "retry") {
1872
1915
  modal.clearComposer();
1873
1916
  retry();
1874
1917
  return;
1875
1918
  }
1876
- void runTurn(command);
1919
+ void runTurn(action.text);
1877
1920
  }
1878
1921
 
1879
1922
  modal.setFull(thread.full);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-bro",
3
- "version": "0.13.0",
3
+ "version": "0.13.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, and opens a sandboxed side conversation with /bro btw.",
5
5
  "type": "module",
6
6
  "license": "MIT",