@maxiaochao/pi-toolkit 0.2.1 → 0.2.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
@@ -1,3 +1,7 @@
1
+ ## 2026-09-03 - v0.2.2
2
+
3
+ - let the focused `/btw` overlay scroll its full transcript with Up/Down and PageUp/PageDown
4
+
1
5
  ## 2026-09-02 - v0.2.1
2
6
 
3
7
  - update global agent instructions for harness-native file editing
package/README.md CHANGED
@@ -38,7 +38,7 @@ To synchronize the file directly from a checkout instead:
38
38
  bash scripts/install.sh
39
39
  ```
40
40
 
41
- The `/btw` side chat is a separate agent session. It can inspect the main conversation and repository, and its thread stays out of the main context until you close it and choose to inject a summary. Use `/btw` to open it or `/btw <question>` to ask directly.
41
+ The `/btw` side chat is a separate agent session. It can inspect the main conversation and repository, and its thread stays out of the main context until you close it and choose to inject a summary. Use `/btw` to open it or `/btw <question>` to ask directly. While the BTW overlay has focus, use `Up` / `Down` to scroll its transcript one line at a time and `PageUp` / `PageDown` to move by one transcript page.
42
42
 
43
43
  The bundled `nightowl` theme is available to Pi through the package manifest and is selected automatically on npm installation only when no theme is already configured.
44
44
 
@@ -0,0 +1,9 @@
1
+ export function getBtwPageScrollSize(viewportHeight: number): number {
2
+ return Math.max(1, Math.floor(viewportHeight));
3
+ }
4
+
5
+ export function clampBtwScrollTop(scrollTop: number, contentHeight: number, viewportHeight: number): number {
6
+ const maxScrollTop = Math.max(0, Math.floor(contentHeight) - Math.floor(viewportHeight));
7
+ const requested = Number.isFinite(scrollTop) ? Math.trunc(scrollTop) : 0;
8
+ return Math.max(0, Math.min(maxScrollTop, requested));
9
+ }
package/extensions/btw.ts CHANGED
@@ -23,6 +23,7 @@ import {
23
23
  type OverlayHandle,
24
24
  type TUI,
25
25
  } from "@earendil-works/pi-tui";
26
+ import { clampBtwScrollTop, getBtwPageScrollSize } from "./btw-scroll.ts";
26
27
 
27
28
  const BTW_ENTRY_TYPE = "btw-thread-entry";
28
29
  const BTW_RESET_TYPE = "btw-thread-reset";
@@ -59,6 +60,7 @@ type OverlayRuntime = {
59
60
  close?: () => void;
60
61
  finish?: () => void;
61
62
  setDraft?: (value: string) => void;
63
+ scrollToEnd?: () => void;
62
64
  closed?: boolean;
63
65
  };
64
66
 
@@ -201,6 +203,10 @@ class BtwOverlay extends Container implements Focusable {
201
203
  private readonly getStatus: () => string;
202
204
  private readonly onSubmitCallback: (value: string) => void;
203
205
  private readonly onDismissCallback: () => void;
206
+ private transcriptScrollTop = 0;
207
+ private transcriptFollowingEnd = true;
208
+ private transcriptContentHeight = 0;
209
+ private transcriptViewportHeight = 0;
204
210
  private _focused = false;
205
211
 
206
212
  get focused(): boolean {
@@ -245,9 +251,65 @@ class BtwOverlay extends Container implements Focusable {
245
251
  return;
246
252
  }
247
253
 
254
+ if (this.keybindings.matches(data, "tui.select.up")) {
255
+ this.scrollTranscriptBy(-1);
256
+ return;
257
+ }
258
+
259
+ if (this.keybindings.matches(data, "tui.select.down")) {
260
+ this.scrollTranscriptBy(1);
261
+ return;
262
+ }
263
+
264
+ const pageSize = getBtwPageScrollSize(this.transcriptViewportHeight);
265
+ if (this.keybindings.matches(data, "tui.select.pageUp")) {
266
+ this.scrollTranscriptBy(-pageSize);
267
+ return;
268
+ }
269
+
270
+ if (this.keybindings.matches(data, "tui.select.pageDown")) {
271
+ this.scrollTranscriptBy(pageSize);
272
+ return;
273
+ }
274
+
248
275
  this.input.handleInput(data);
249
276
  }
250
277
 
278
+ private scrollTranscriptBy(lines: number): void {
279
+ const maxScrollTop = Math.max(0, this.transcriptContentHeight - this.transcriptViewportHeight);
280
+ const currentScrollTop = this.transcriptFollowingEnd
281
+ ? maxScrollTop
282
+ : clampBtwScrollTop(this.transcriptScrollTop, this.transcriptContentHeight, this.transcriptViewportHeight);
283
+ const nextScrollTop = clampBtwScrollTop(
284
+ currentScrollTop + Math.trunc(lines),
285
+ this.transcriptContentHeight,
286
+ this.transcriptViewportHeight,
287
+ );
288
+ const nextFollowingEnd = nextScrollTop === maxScrollTop;
289
+ if (nextScrollTop === currentScrollTop && nextFollowingEnd === this.transcriptFollowingEnd) {
290
+ return;
291
+ }
292
+
293
+ this.transcriptScrollTop = nextScrollTop;
294
+ this.transcriptFollowingEnd = nextFollowingEnd;
295
+ this.tui.requestRender();
296
+ }
297
+
298
+ scrollTranscriptToEnd(): void {
299
+ const nextScrollTop = clampBtwScrollTop(
300
+ this.transcriptContentHeight,
301
+ this.transcriptContentHeight,
302
+ this.transcriptViewportHeight,
303
+ );
304
+ if (this.transcriptFollowingEnd && this.transcriptScrollTop === nextScrollTop) {
305
+ return;
306
+ }
307
+
308
+ this.transcriptScrollTop = nextScrollTop;
309
+ this.transcriptFollowingEnd = true;
310
+ this.tui.requestRender();
311
+ }
312
+
251
313
  setDraft(value: string): void {
252
314
  this.input.setValue(value);
253
315
  this.tui.requestRender();
@@ -279,7 +341,19 @@ class BtwOverlay extends Container implements Focusable {
279
341
 
280
342
  // Markdown renders to innerWidth already — no manual wrapping needed
281
343
  const transcript = this.getTranscript(innerWidth, this.theme);
282
- const visibleTranscript = transcript.slice(-transcriptHeight);
344
+ this.transcriptContentHeight = transcript.length;
345
+ this.transcriptViewportHeight = transcriptHeight;
346
+ const maxScrollTop = Math.max(0, this.transcriptContentHeight - this.transcriptViewportHeight);
347
+ if (this.transcriptFollowingEnd) {
348
+ this.transcriptScrollTop = maxScrollTop;
349
+ } else {
350
+ this.transcriptScrollTop = clampBtwScrollTop(
351
+ this.transcriptScrollTop,
352
+ this.transcriptContentHeight,
353
+ this.transcriptViewportHeight,
354
+ );
355
+ }
356
+ const visibleTranscript = transcript.slice(this.transcriptScrollTop, this.transcriptScrollTop + transcriptHeight);
283
357
  const transcriptPadding = Math.max(0, transcriptHeight - visibleTranscript.length);
284
358
 
285
359
  const status = this.getStatus();
@@ -308,7 +382,7 @@ class BtwOverlay extends Container implements Focusable {
308
382
  lines.push(
309
383
  `${this.theme.fg("borderMuted", "│")}${inputLine}${this.theme.fg("borderMuted", "│")}`,
310
384
  );
311
- lines.push(this.frameLine(this.theme.fg("dim", "Enter submit · Esc close"), innerWidth));
385
+ lines.push(this.frameLine(this.theme.fg("dim", "↑/↓ scroll · PgUp/PgDn page · Enter submit · Esc close"), innerWidth));
312
386
  lines.push(this.borderLine(innerWidth, "bottom"));
313
387
 
314
388
  return lines;
@@ -396,7 +470,7 @@ export default function (pi: ExtensionAPI) {
396
470
  }
397
471
 
398
472
  const lines: string[] = [];
399
- for (const item of thread.slice(-6)) {
473
+ for (const item of thread) {
400
474
  // User message
401
475
  const userText = item.question.trim().split("\n")[0];
402
476
  lines.push(theme.fg("accent", theme.bold("You: ")) + truncateToWidth(userText, width - 5, "…"));
@@ -509,6 +583,7 @@ export default function (pi: ExtensionAPI) {
509
583
  setOverlayDraft("");
510
584
  setOverlayStatus("Ready");
511
585
  await disposeSideSession();
586
+ overlayRuntime?.scrollToEnd?.();
512
587
  if (persist) {
513
588
  const details: BtwResetDetails = { timestamp: Date.now() };
514
589
  pi.appendEntry(BTW_RESET_TYPE, details);
@@ -546,6 +621,7 @@ export default function (pi: ExtensionAPI) {
546
621
  thread.push(details);
547
622
  }
548
623
 
624
+ overlayRuntime?.scrollToEnd?.();
549
625
  syncOverlay();
550
626
  }
551
627
 
@@ -691,6 +767,7 @@ export default function (pi: ExtensionAPI) {
691
767
  overlay.focused = true;
692
768
  overlay.setDraft(overlayDraft);
693
769
  runtime.setDraft = (value) => overlay.setDraft(value);
770
+ runtime.scrollToEnd = () => overlay.scrollTranscriptToEnd();
694
771
  runtime.refresh = () => {
695
772
  overlay.focused = runtime.handle?.isFocused() ?? false;
696
773
  tui.requestRender();
@@ -836,6 +913,7 @@ export default function (pi: ExtensionAPI) {
836
913
  return;
837
914
  }
838
915
 
916
+ overlayRuntime?.scrollToEnd?.();
839
917
  const side = await ensureSideSession(ctx);
840
918
  if (!side) {
841
919
  notify(ctx, "Unable to create BTW side session.", "error");
@@ -0,0 +1,14 @@
1
+ import assert from "node:assert/strict";
2
+ import { clampBtwScrollTop, getBtwPageScrollSize } from "../btw-scroll.ts";
3
+
4
+ assert.equal(getBtwPageScrollSize(18), 18, "page scrolling uses the transcript viewport height");
5
+ assert.equal(getBtwPageScrollSize(8.9), 8, "page scrolling uses a whole number of lines");
6
+ assert.equal(getBtwPageScrollSize(0), 1, "page scrolling always moves at least one line");
7
+
8
+ assert.equal(clampBtwScrollTop(-3, 20, 8), 0, "scroll top clamps at the start");
9
+ assert.equal(clampBtwScrollTop(4, 20, 8), 4, "scroll top remains inside the range");
10
+ assert.equal(clampBtwScrollTop(99, 20, 8), 12, "scroll top clamps at the end");
11
+ assert.equal(clampBtwScrollTop(5, 4, 8), 0, "short content cannot scroll");
12
+ assert.equal(clampBtwScrollTop(Number.NaN, 20, 8), 0, "invalid scroll input falls back to the start");
13
+
14
+ console.log("BTW history scrolling tests passed");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maxiaochao/pi-toolkit",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Personal pi coding agent toolkit: /todos, /btw, /cache_export extensions, browser automation, and bundled theme/skills.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -43,7 +43,7 @@
43
43
  "ws": "^8.19.0"
44
44
  },
45
45
  "scripts": {
46
- "test": "cd extensions/cache-export && node --experimental-strip-types tests/run-tests.mjs",
46
+ "test": "cd extensions/cache-export && node --experimental-strip-types tests/run-tests.mjs && node --experimental-strip-types ../tests/btw-history.mjs",
47
47
  "load-test": "pi -ne -e ./extensions/todo.ts && pi -ne -e ./extensions/cache-export/index.ts && pi -ne -e ./extensions/btw.ts",
48
48
  "eval:agents": "node evaluations/agents-md/run.mjs",
49
49
  "eval:agents:test": "node evaluations/agents-md/tests/run-tests.mjs",