pi-btw 0.3.8 → 0.4.1

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # pi-btw
2
2
 
3
- A small [pi](https://github.com/badlogic/pi-mono) extension that adds a `/btw` side conversation channel.
3
+ A small [pi](https://github.com/earendil-works/pi-mono) extension that adds a `/btw` side conversation channel.
4
4
 
5
5
  `/btw` opens a real pi sub-session with coding-tool access, and it runs immediately even while the main agent is still busy.
6
6
 
package/extensions/btw.ts CHANGED
@@ -2,7 +2,6 @@ import {
2
2
  buildSessionContext,
3
3
  createAgentSession,
4
4
  createExtensionRuntime,
5
- codingTools,
6
5
  SessionManager,
7
6
  type AgentSession,
8
7
  type AgentSessionEvent,
@@ -10,8 +9,8 @@ import {
10
9
  type ExtensionCommandContext,
11
10
  type ExtensionContext,
12
11
  type ResourceLoader,
13
- } from "@mariozechner/pi-coding-agent";
14
- import { type AssistantMessage, type Message, type ThinkingLevel as AiThinkingLevel, type UserMessage } from "@mariozechner/pi-ai";
12
+ } from "@earendil-works/pi-coding-agent";
13
+ import { type AssistantMessage, type Message, type ThinkingLevel as AiThinkingLevel, type UserMessage } from "@earendil-works/pi-ai";
15
14
  import {
16
15
  Box,
17
16
  Container,
@@ -26,7 +25,7 @@ import {
26
25
  type KeybindingsManager,
27
26
  type OverlayHandle,
28
27
  type TUI,
29
- } from "@mariozechner/pi-tui";
28
+ } from "@earendil-works/pi-tui";
30
29
 
31
30
  const BTW_MESSAGE_TYPE = "btw-note";
32
31
  const BTW_ENTRY_TYPE = "btw-thread-entry";
@@ -56,6 +55,11 @@ const BTW_CONTINUE_THREAD_ASSISTANT_TEXT = "Understood, continuing our side conv
56
55
  type SessionThinkingLevel = "off" | AiThinkingLevel;
57
56
  type BtwThreadMode = "contextual" | "tangent";
58
57
  type SessionModel = NonNullable<ExtensionCommandContext["model"]>;
58
+ /**
59
+ * Loose model reference parsed from `/btw:model <provider> <id> <api>` and persisted to
60
+ * session entries. Resolved to a full SessionModel via ctx.modelRegistry.find(...).
61
+ */
62
+ type BtwModelRef = Pick<SessionModel, "provider" | "id" | "api">;
59
63
 
60
64
  type BtwDetails = {
61
65
  question: string;
@@ -216,7 +220,7 @@ function parseBtwArgs(args: string): ParsedBtwArgs {
216
220
  function parseBtwModelArgs(args: string):
217
221
  | { action: "show" }
218
222
  | { action: "clear" }
219
- | { action: "set"; model: SessionModel }
223
+ | { action: "set"; model: BtwModelRef }
220
224
  | { action: "invalid"; message: string } {
221
225
  const trimmed = args.trim();
222
226
  if (!trimmed) {
@@ -233,7 +237,7 @@ function parseBtwModelArgs(args: string):
233
237
  }
234
238
 
235
239
  const [provider, id, api] = parts;
236
- return { action: "set", model: { provider, id, api } };
240
+ return { action: "set", model: { provider, id, api } as BtwModelRef };
237
241
  }
238
242
 
239
243
  function parseBtwThinkingArgs(args: string):
@@ -997,6 +1001,9 @@ function notify(ctx: ExtensionContext | ExtensionCommandContext, message: string
997
1001
  }
998
1002
  }
999
1003
 
1004
+ /** Fixed overlay rows outside the transcript viewport (must match render() structure). */
1005
+ const BTW_OVERLAY_CHROME_LINES = 9;
1006
+
1000
1007
  function getOverlayTitle(mode: BtwThreadMode): string {
1001
1008
  return mode === "tangent" ? "BTW tangent" : "BTW";
1002
1009
  }
@@ -1081,6 +1088,9 @@ class BtwOverlayComponent extends Container implements Focusable {
1081
1088
 
1082
1089
  this.hintsText = new Text("", 1, 0);
1083
1090
 
1091
+ // Enable SGR mouse reporting so wheel/touchpad events reach handleInput().
1092
+ this.tui.terminal?.write?.("\x1b[?1000h\x1b[?1006h");
1093
+
1084
1094
  const originalHandleInput = this.input.handleInput.bind(this.input);
1085
1095
  this.input.handleInput = (data: string) => {
1086
1096
  if (keybindings.matches(data, "app.clear")) {
@@ -1107,17 +1117,17 @@ class BtwOverlayComponent extends Container implements Focusable {
1107
1117
  private frameLine(content: string, innerWidth: number): string {
1108
1118
  const truncated = truncateToWidth(content, innerWidth, "");
1109
1119
  const padding = Math.max(0, innerWidth - visibleWidth(truncated));
1110
- return `${this.theme.fg("borderMuted", "│")}${truncated}${" ".repeat(padding)}${this.theme.fg("borderMuted", "│")}`;
1120
+ return `${this.theme.fg("border", "│")}${truncated}${" ".repeat(padding)}${this.theme.fg("border", "│")}`;
1111
1121
  }
1112
1122
 
1113
1123
  private ruleLine(innerWidth: number): string {
1114
- return this.theme.fg("borderMuted", `├${"─".repeat(innerWidth)}┤`);
1124
+ return this.theme.fg("border", `├${"─".repeat(innerWidth)}┤`);
1115
1125
  }
1116
1126
 
1117
1127
  private borderLine(innerWidth: number, edge: "top" | "bottom"): string {
1118
1128
  const left = edge === "top" ? "┌" : "└";
1119
1129
  const right = edge === "top" ? "┐" : "┘";
1120
- return this.theme.fg("borderMuted", `${left}${"─".repeat(innerWidth)}${right}`);
1130
+ return this.theme.fg("border", `${left}${"─".repeat(innerWidth)}${right}`);
1121
1131
  }
1122
1132
 
1123
1133
  private wrapTranscript(innerWidth: number): string[] {
@@ -1137,22 +1147,53 @@ class BtwOverlayComponent extends Container implements Focusable {
1137
1147
  return Math.max(18, Math.min(32, Math.floor(terminalRows * 0.78)));
1138
1148
  }
1139
1149
 
1150
+ private scrollTranscript(delta: number): void {
1151
+ if (delta < 0) {
1152
+ this.followTranscript = false;
1153
+ }
1154
+ this.transcriptScrollOffset = Math.max(0, this.transcriptScrollOffset + delta);
1155
+ this.tui.requestRender();
1156
+ }
1157
+
1158
+ dispose(): void {
1159
+ this.tui.terminal?.write?.("\x1b[?1000l\x1b[?1006l");
1160
+ }
1161
+
1162
+ private getMouseScrollDelta(data: string): number | null {
1163
+ const match = data.match(/^\x1b\[<(\d+);\d+;\d+[Mm]$/);
1164
+ if (!match) {
1165
+ return null;
1166
+ }
1167
+
1168
+ const button = Number(match[1]);
1169
+ if ((button & 64) !== 64) {
1170
+ return null;
1171
+ }
1172
+
1173
+ return (button & 1) === 0 ? -3 : 3;
1174
+ }
1175
+
1140
1176
  handleInput(data: string): void {
1141
1177
  if (matchesBtwFocusShortcut(data)) {
1142
1178
  this.onUnfocusCallback();
1143
1179
  return;
1144
1180
  }
1145
1181
 
1146
- if (matchesKey(data, Key.pageUp)) {
1147
- this.followTranscript = false;
1148
- this.transcriptScrollOffset = Math.max(0, this.transcriptScrollOffset - Math.max(1, this.transcriptViewportHeight - 1));
1149
- this.tui.requestRender();
1182
+ const mouseScrollDelta = this.getMouseScrollDelta(data);
1183
+ if (mouseScrollDelta !== null) {
1184
+ this.scrollTranscript(mouseScrollDelta);
1185
+ return;
1186
+ }
1187
+
1188
+ if (matchesKey(data, Key.pageUp) || matchesKey(data, Key.up)) {
1189
+ const step = matchesKey(data, Key.pageUp) ? Math.max(1, this.transcriptViewportHeight - 1) : 1;
1190
+ this.scrollTranscript(-step);
1150
1191
  return;
1151
1192
  }
1152
1193
 
1153
- if (matchesKey(data, Key.pageDown)) {
1154
- this.transcriptScrollOffset += Math.max(1, this.transcriptViewportHeight - 1);
1155
- this.tui.requestRender();
1194
+ if (matchesKey(data, Key.pageDown) || matchesKey(data, Key.down)) {
1195
+ const step = matchesKey(data, Key.pageDown) ? Math.max(1, this.transcriptViewportHeight - 1) : 1;
1196
+ this.scrollTranscript(step);
1156
1197
  return;
1157
1198
  }
1158
1199
 
@@ -1168,19 +1209,25 @@ class BtwOverlayComponent extends Container implements Focusable {
1168
1209
  // the row stays geometrically stable while the overlay still owns keyboard input.
1169
1210
  this.input.focused = false;
1170
1211
  try {
1171
- const inputLine = this.input.render(targetWidth)[0] ?? "";
1172
- return `${this.theme.fg("borderMuted", "│")}${inputLine}${this.theme.fg("borderMuted", "")}`;
1212
+ const renderedInputLine = this.input.render(targetWidth)[0] ?? "";
1213
+ const inputLine = truncateToWidth(renderedInputLine, targetWidth, "");
1214
+ const padding = Math.max(0, targetWidth - visibleWidth(inputLine));
1215
+ return `${this.theme.fg("border", "│")}${inputLine}${" ".repeat(padding)}${this.theme.fg("border", "│")}`;
1173
1216
  } finally {
1174
1217
  this.input.focused = previousFocused;
1175
1218
  }
1176
1219
  }
1177
1220
 
1221
+ private fitRenderedLine(line: string, width: number): string {
1222
+ return visibleWidth(line) > width ? truncateToWidth(line, width, "") : line;
1223
+ }
1224
+
1178
1225
  override render(width: number): string[] {
1179
1226
  const dialogWidth = Math.max(24, width);
1180
1227
  const innerWidth = Math.max(22, dialogWidth - 2);
1181
1228
  const transcriptLines = this.wrapTranscript(innerWidth);
1182
1229
  const dialogHeight = this.getDialogHeight();
1183
- const chromeHeight = 8;
1230
+ const chromeHeight = BTW_OVERLAY_CHROME_LINES;
1184
1231
  const transcriptHeight = Math.max(6, dialogHeight - chromeHeight);
1185
1232
  this.transcriptViewportHeight = transcriptHeight;
1186
1233
 
@@ -1225,7 +1272,7 @@ class BtwOverlayComponent extends Container implements Focusable {
1225
1272
  lines.push(this.frameLine(this.theme.fg("dim", this.hintsTextValue.trim()), innerWidth));
1226
1273
  lines.push(this.borderLine(innerWidth, "bottom"));
1227
1274
 
1228
- return lines;
1275
+ return lines.map((line) => this.fitRenderedLine(line, width));
1229
1276
  }
1230
1277
 
1231
1278
  setDraft(value: string): void {
@@ -1259,7 +1306,7 @@ class BtwOverlayComponent extends Container implements Focusable {
1259
1306
  const status = this.getStatus() ?? "Ready. Enter submits; Escape dismisses without clearing.";
1260
1307
  this.statusTextValue = status;
1261
1308
  this.statusText.setText(this.statusTextValue);
1262
- this.hintsTextValue = "Enter submit · Alt+/ toggle focus · Escape dismiss · PgUp/PgDn scroll";
1309
+ this.hintsTextValue = "Scroll wheel ↑↓ PgUp/PgDn · Enter · Alt+/ focus · Esc";
1263
1310
  this.hintsText.setText(this.hintsTextValue);
1264
1311
  this.tui.requestRender();
1265
1312
  }
@@ -1555,7 +1602,8 @@ export default function (pi: ExtensionAPI) {
1555
1602
  model: settings.model,
1556
1603
  modelRegistry: ctx.modelRegistry as AgentSession["modelRegistry"],
1557
1604
  thinkingLevel: settings.thinkingLevel,
1558
- tools: codingTools,
1605
+ // Match pi's default coding-agent toolset (read/bash/edit/write).
1606
+ tools: ["read", "bash", "edit", "write"],
1559
1607
  resourceLoader: createBtwResourceLoader(ctx),
1560
1608
  });
1561
1609
 
@@ -1650,6 +1698,7 @@ export default function (pi: ExtensionAPI) {
1650
1698
  };
1651
1699
  runtime.close = () => {
1652
1700
  overlayDraft = overlay.getDraft();
1701
+ overlay.dispose();
1653
1702
  closeRuntime();
1654
1703
  };
1655
1704
 
@@ -1760,7 +1809,19 @@ export default function (pi: ExtensionAPI) {
1760
1809
  return true;
1761
1810
  }
1762
1811
 
1763
- await setBtwModelOverride(ctx, parsed.action === "clear" ? null : parsed.model);
1812
+ if (parsed.action === "clear") {
1813
+ await setBtwModelOverride(ctx, null);
1814
+ return true;
1815
+ }
1816
+ const ref = parsed.model;
1817
+ const resolved = ctx.modelRegistry.find(ref.provider, ref.id);
1818
+ if (!resolved) {
1819
+ const message = `Unknown model ${ref.provider}/${ref.id}. Use /login or /models to add it before setting it as the BTW override.`;
1820
+ setOverlayStatus(message, ctx);
1821
+ notify(ctx, message, "error");
1822
+ return true;
1823
+ }
1824
+ await setBtwModelOverride(ctx, resolved);
1764
1825
  return true;
1765
1826
  }
1766
1827
 
@@ -1911,17 +1972,22 @@ export default function (pi: ExtensionAPI) {
1911
1972
 
1912
1973
  for (let i = 0; i < branch.length; i++) {
1913
1974
  if (isCustomEntry(branch[i], BTW_MODEL_OVERRIDE_TYPE)) {
1914
- const details = branch[i].data as BtwModelOverrideDetails | undefined;
1915
- btwModelOverride =
1916
- details?.action === "set"
1917
- ? { provider: details.provider, id: details.id, api: details.api }
1918
- : details?.action === "clear"
1919
- ? null
1920
- : btwModelOverride;
1975
+ const details = (branch[i] as unknown as { data?: BtwModelOverrideDetails }).data;
1976
+ if (details?.action === "set") {
1977
+ const resolved = ctx.modelRegistry.find(details.provider, details.id);
1978
+ if (resolved) {
1979
+ btwModelOverride = resolved;
1980
+ } else {
1981
+ // Configured override is no longer in the registry; drop it on restore.
1982
+ btwModelOverride = null;
1983
+ }
1984
+ } else if (details?.action === "clear") {
1985
+ btwModelOverride = null;
1986
+ }
1921
1987
  }
1922
1988
 
1923
1989
  if (isCustomEntry(branch[i], BTW_THINKING_OVERRIDE_TYPE)) {
1924
- const details = branch[i].data as BtwThinkingOverrideDetails | undefined;
1990
+ const details = (branch[i] as unknown as { data?: BtwThinkingOverrideDetails }).data;
1925
1991
  btwThinkingOverride =
1926
1992
  details?.action === "set"
1927
1993
  ? details.thinkingLevel
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-btw",
3
- "version": "0.3.8",
3
+ "version": "0.4.1",
4
4
  "description": "A pi extension for parallel side conversations with /btw",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -42,9 +42,9 @@
42
42
  "image": "https://raw.githubusercontent.com/dbachelder/pi-btw/main/docs/btw-overlay.png"
43
43
  },
44
44
  "peerDependencies": {
45
- "@mariozechner/pi-ai": "^0.66.1",
46
- "@mariozechner/pi-coding-agent": "^0.66.1",
47
- "@mariozechner/pi-tui": "^0.66.1"
45
+ "@earendil-works/pi-ai": ">=0.74.0 <1",
46
+ "@earendil-works/pi-coding-agent": ">=0.74.0 <1",
47
+ "@earendil-works/pi-tui": ">=0.74.0 <1"
48
48
  },
49
49
  "devDependencies": {
50
50
  "typescript": "^6.0.2",