pi-btw 0.4.0 → 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.
Files changed (2) hide show
  1. package/extensions/btw.ts +59 -15
  2. package/package.json +4 -4
package/extensions/btw.ts CHANGED
@@ -1001,6 +1001,9 @@ function notify(ctx: ExtensionContext | ExtensionCommandContext, message: string
1001
1001
  }
1002
1002
  }
1003
1003
 
1004
+ /** Fixed overlay rows outside the transcript viewport (must match render() structure). */
1005
+ const BTW_OVERLAY_CHROME_LINES = 9;
1006
+
1004
1007
  function getOverlayTitle(mode: BtwThreadMode): string {
1005
1008
  return mode === "tangent" ? "BTW tangent" : "BTW";
1006
1009
  }
@@ -1085,6 +1088,9 @@ class BtwOverlayComponent extends Container implements Focusable {
1085
1088
 
1086
1089
  this.hintsText = new Text("", 1, 0);
1087
1090
 
1091
+ // Enable SGR mouse reporting so wheel/touchpad events reach handleInput().
1092
+ this.tui.terminal?.write?.("\x1b[?1000h\x1b[?1006h");
1093
+
1088
1094
  const originalHandleInput = this.input.handleInput.bind(this.input);
1089
1095
  this.input.handleInput = (data: string) => {
1090
1096
  if (keybindings.matches(data, "app.clear")) {
@@ -1111,17 +1117,17 @@ class BtwOverlayComponent extends Container implements Focusable {
1111
1117
  private frameLine(content: string, innerWidth: number): string {
1112
1118
  const truncated = truncateToWidth(content, innerWidth, "");
1113
1119
  const padding = Math.max(0, innerWidth - visibleWidth(truncated));
1114
- 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", "│")}`;
1115
1121
  }
1116
1122
 
1117
1123
  private ruleLine(innerWidth: number): string {
1118
- return this.theme.fg("borderMuted", `├${"─".repeat(innerWidth)}┤`);
1124
+ return this.theme.fg("border", `├${"─".repeat(innerWidth)}┤`);
1119
1125
  }
1120
1126
 
1121
1127
  private borderLine(innerWidth: number, edge: "top" | "bottom"): string {
1122
1128
  const left = edge === "top" ? "┌" : "└";
1123
1129
  const right = edge === "top" ? "┐" : "┘";
1124
- return this.theme.fg("borderMuted", `${left}${"─".repeat(innerWidth)}${right}`);
1130
+ return this.theme.fg("border", `${left}${"─".repeat(innerWidth)}${right}`);
1125
1131
  }
1126
1132
 
1127
1133
  private wrapTranscript(innerWidth: number): string[] {
@@ -1141,22 +1147,53 @@ class BtwOverlayComponent extends Container implements Focusable {
1141
1147
  return Math.max(18, Math.min(32, Math.floor(terminalRows * 0.78)));
1142
1148
  }
1143
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
+
1144
1176
  handleInput(data: string): void {
1145
1177
  if (matchesBtwFocusShortcut(data)) {
1146
1178
  this.onUnfocusCallback();
1147
1179
  return;
1148
1180
  }
1149
1181
 
1150
- if (matchesKey(data, Key.pageUp)) {
1151
- this.followTranscript = false;
1152
- this.transcriptScrollOffset = Math.max(0, this.transcriptScrollOffset - Math.max(1, this.transcriptViewportHeight - 1));
1153
- 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);
1154
1191
  return;
1155
1192
  }
1156
1193
 
1157
- if (matchesKey(data, Key.pageDown)) {
1158
- this.transcriptScrollOffset += Math.max(1, this.transcriptViewportHeight - 1);
1159
- 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);
1160
1197
  return;
1161
1198
  }
1162
1199
 
@@ -1172,19 +1209,25 @@ class BtwOverlayComponent extends Container implements Focusable {
1172
1209
  // the row stays geometrically stable while the overlay still owns keyboard input.
1173
1210
  this.input.focused = false;
1174
1211
  try {
1175
- const inputLine = this.input.render(targetWidth)[0] ?? "";
1176
- 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", "│")}`;
1177
1216
  } finally {
1178
1217
  this.input.focused = previousFocused;
1179
1218
  }
1180
1219
  }
1181
1220
 
1221
+ private fitRenderedLine(line: string, width: number): string {
1222
+ return visibleWidth(line) > width ? truncateToWidth(line, width, "") : line;
1223
+ }
1224
+
1182
1225
  override render(width: number): string[] {
1183
1226
  const dialogWidth = Math.max(24, width);
1184
1227
  const innerWidth = Math.max(22, dialogWidth - 2);
1185
1228
  const transcriptLines = this.wrapTranscript(innerWidth);
1186
1229
  const dialogHeight = this.getDialogHeight();
1187
- const chromeHeight = 8;
1230
+ const chromeHeight = BTW_OVERLAY_CHROME_LINES;
1188
1231
  const transcriptHeight = Math.max(6, dialogHeight - chromeHeight);
1189
1232
  this.transcriptViewportHeight = transcriptHeight;
1190
1233
 
@@ -1229,7 +1272,7 @@ class BtwOverlayComponent extends Container implements Focusable {
1229
1272
  lines.push(this.frameLine(this.theme.fg("dim", this.hintsTextValue.trim()), innerWidth));
1230
1273
  lines.push(this.borderLine(innerWidth, "bottom"));
1231
1274
 
1232
- return lines;
1275
+ return lines.map((line) => this.fitRenderedLine(line, width));
1233
1276
  }
1234
1277
 
1235
1278
  setDraft(value: string): void {
@@ -1263,7 +1306,7 @@ class BtwOverlayComponent extends Container implements Focusable {
1263
1306
  const status = this.getStatus() ?? "Ready. Enter submits; Escape dismisses without clearing.";
1264
1307
  this.statusTextValue = status;
1265
1308
  this.statusText.setText(this.statusTextValue);
1266
- this.hintsTextValue = "Enter submit · Alt+/ toggle focus · Escape dismiss · PgUp/PgDn scroll";
1309
+ this.hintsTextValue = "Scroll wheel ↑↓ PgUp/PgDn · Enter · Alt+/ focus · Esc";
1267
1310
  this.hintsText.setText(this.hintsTextValue);
1268
1311
  this.tui.requestRender();
1269
1312
  }
@@ -1655,6 +1698,7 @@ export default function (pi: ExtensionAPI) {
1655
1698
  };
1656
1699
  runtime.close = () => {
1657
1700
  overlayDraft = overlay.getDraft();
1701
+ overlay.dispose();
1658
1702
  closeRuntime();
1659
1703
  };
1660
1704
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-btw",
3
- "version": "0.4.0",
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
- "@earendil-works/pi-ai": "^0.74.0",
46
- "@earendil-works/pi-coding-agent": "^0.74.0",
47
- "@earendil-works/pi-tui": "^0.74.0"
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",