@narumitw/pi-btw 0.56.2 → 0.57.0

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
@@ -9,7 +9,7 @@ Only context you explicitly bring back is loaded into the main editor.
9
9
 
10
10
  - Starts a side thread immediately with `/btw <question>` or opens the manager with `/btw`.
11
11
  - Uses any persisted main-session branch as context without switching branches.
12
- - Supports scrollable answers, transcript search, follow-up questions, queued steering, and in-memory resume.
12
+ - Supports scrollable answers, transcript search, a clickable jump-to-latest control, follow-up questions, queued steering, and in-memory resume.
13
13
  - Keeps side questions and answers out of the main conversation by default.
14
14
  - Brings back the latest answer, a question suffix, an exact range, or the complete thread only when requested.
15
15
  - Uses Pi's current model and thinking level or saved pi-btw choices.
@@ -120,6 +120,9 @@ A failed response remains visible and does not discard later queued questions.
120
120
  Steering never appends to the main conversation or editor.
121
121
 
122
122
  Use the mouse wheel, trackpad, or `PgUp`/`PgDn` to scroll transcript history.
123
+ On Pi 0.85 or newer, scrolling away from a following transcript shows **Jump to latest message** over its final visible row.
124
+ Click the control or use Pi's effective `tui.altScreen.bottom` binding (`End` by default) to resume at the latest content.
125
+ The control disappears after the transcript returns to follow-end mode.
123
126
  The footer shows history keys only when scrolling is available.
124
127
  Ctrl+C cancels the active response and discards the current draft and steering queue.
125
128
  Completed questions, answers, and visible errors remain available through Resume until the extension instance ends.
@@ -216,6 +219,7 @@ The file is read for every `/btw` invocation, so edits apply without `/reload`.
216
219
  - Resume state is memory-only and lasts only for the current extension instance.
217
220
  - A side thread retains the latest 40,000 characters of main-conversation context and adds a truncation notice when earlier content is omitted.
218
221
  - Clipboard access depends on Pi's host helper, the operating system, and the terminal.
222
+ - Pi versions before 0.85 omit the clickable jump-to-latest control.
219
223
 
220
224
  ## 🗂️ Package layout
221
225
 
package/dist/index.ts CHANGED
@@ -482,8 +482,10 @@ import {
482
482
  } from "@earendil-works/pi-coding-agent";
483
483
  import {
484
484
  isKeyRelease,
485
+ isKittyProtocolActive,
485
486
  Key as Key2,
486
487
  matchesKey as matchesKey2,
488
+ parseKey,
487
489
  TuiAltScreen,
488
490
  truncateToWidth as truncateToWidth2
489
491
  } from "@earendil-works/pi-tui";
@@ -495,6 +497,18 @@ function sanitizeSingleLine(text) {
495
497
  return code > 31 && (code < 127 || code > 159);
496
498
  }).join("").replace(/ +/gu, " ").trim();
497
499
  }
500
+ function formatKeyLabel2(key) {
501
+ const sanitized = sanitizeSingleLine(key);
502
+ if (!sanitized) return "";
503
+ return sanitized.split("+").map((part) => {
504
+ const lower = part.toLowerCase();
505
+ if (lower === "shift") return "Shift";
506
+ if (lower === "ctrl") return "Ctrl";
507
+ if (lower === "alt") return "Alt";
508
+ if (lower === "super") return "Super";
509
+ return part.length === 1 ? part.toUpperCase() : `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`;
510
+ }).join("+");
511
+ }
498
512
 
499
513
  // src/fullscreen-ui.ts
500
514
  var FullscreenUiDisposedError = class extends Error {
@@ -607,6 +621,88 @@ var BtwTuiAltScreen = class extends TuiAltScreen {
607
621
  };
608
622
  var BRACKETED_PASTE_START = "\x1B[200~";
609
623
  var BRACKETED_PASTE_END = "\x1B[201~";
624
+ var ALT_SCREEN_ACTIONS_BEFORE_BOTTOM = [
625
+ "tui.altScreen.search",
626
+ "tui.altScreen.searchNext",
627
+ "tui.altScreen.searchPrevious",
628
+ "tui.altScreen.searchClose",
629
+ "tui.altScreen.pageUp",
630
+ "tui.altScreen.pageDown",
631
+ "tui.altScreen.halfPageUp",
632
+ "tui.altScreen.halfPageDown",
633
+ "tui.altScreen.lineUp",
634
+ "tui.altScreen.lineDown",
635
+ "tui.altScreen.previousPrompt",
636
+ "tui.altScreen.nextPrompt",
637
+ "tui.altScreen.top"
638
+ ];
639
+ var KEY_MODIFIER_ORDER = ["shift", "ctrl", "alt", "super"];
640
+ var MATCHABLE_SPECIAL_KEYS = /* @__PURE__ */ new Set([
641
+ "space",
642
+ "tab",
643
+ "enter",
644
+ "backspace",
645
+ "delete",
646
+ "insert",
647
+ "home",
648
+ "end",
649
+ "pageup",
650
+ "pagedown",
651
+ "up",
652
+ "down",
653
+ "left",
654
+ "right"
655
+ ]);
656
+ var MATCHABLE_SYMBOL_KEYS = new Set("`-=[]\\;',./!@#$%^&*()_+|~{}:<>?");
657
+ function normalizedKeyId(key) {
658
+ const parts = key.toLowerCase().split("+");
659
+ const base = parts.at(-1);
660
+ if (!base) return "";
661
+ const normalizedBase = base === "esc" ? "escape" : base === "return" ? "enter" : base;
662
+ const modifiers = KEY_MODIFIER_ORDER.filter((modifier) => parts.includes(modifier));
663
+ return [...modifiers, normalizedBase].join("+");
664
+ }
665
+ function formatEffectiveKeyLabel(key) {
666
+ const parts = key.split("+");
667
+ const base = parts.at(-1);
668
+ if (base === "pageup") parts[parts.length - 1] = "pageUp";
669
+ if (base === "pagedown") parts[parts.length - 1] = "pageDown";
670
+ return formatKeyLabel2(parts.join("+"));
671
+ }
672
+ function canMatchKeyInput(key) {
673
+ const parts = key.split("+");
674
+ const base = parts.at(-1) ?? "";
675
+ const modifiers = parts.slice(0, -1);
676
+ if (base === "escape") return modifiers.length === 0;
677
+ if (base === "clear") {
678
+ return modifiers.length === 0 || modifiers.length === 1 && (modifiers[0] === "shift" || modifiers[0] === "ctrl");
679
+ }
680
+ if (/^f(?:[1-9]|1[0-2])$/u.test(base)) return modifiers.length === 0;
681
+ return MATCHABLE_SPECIAL_KEYS.has(base) || base.length === 1 && (/^[a-z0-9]$/u.test(base) || MATCHABLE_SYMBOL_KEYS.has(base));
682
+ }
683
+ function rawCtrlInput(base) {
684
+ if (base.length !== 1) return void 0;
685
+ const rawBase = base === "-" ? "_" : base;
686
+ if (!"abcdefghijklmnopqrstuvwxyz[\\]_".includes(rawBase)) return void 0;
687
+ return String.fromCharCode(rawBase.charCodeAt(0) & 31);
688
+ }
689
+ function legacyRawInput(key) {
690
+ const parts = key.split("+");
691
+ const base = parts.at(-1) ?? "";
692
+ if (parts.length === 2 && parts[0] === "ctrl") return rawCtrlInput(base);
693
+ if (isKittyProtocolActive()) return void 0;
694
+ if (parts.length === 2 && parts[0] === "alt" && base.length === 1) return `\x1B${base}`;
695
+ if (parts.length === 3 && parts[0] === "ctrl" && parts[1] === "alt") {
696
+ const input = rawCtrlInput(base);
697
+ return input ? `\x1B${input}` : void 0;
698
+ }
699
+ return void 0;
700
+ }
701
+ function keyInputIdentity(key) {
702
+ const identity = normalizedKeyId(key);
703
+ const input = legacyRawInput(identity);
704
+ return input ? normalizedKeyId(parseKey(input) ?? identity) : identity;
705
+ }
610
706
  function hasManualSelectionCopyApi() {
611
707
  return typeof TuiAltScreen.prototype.hasActiveSelection === "function" && typeof TuiAltScreen.prototype.copyActiveSelectionToClipboard === "function";
612
708
  }
@@ -625,6 +721,25 @@ function createBtwFullscreenTui(parent, theme, keybindings, copyOnSelect, manual
625
721
  mouse: true,
626
722
  copyOnSelect,
627
723
  searchMatchStyle: (text) => theme.underline(styleSearchMatch(text)),
724
+ scrollToEndIndicator: () => {
725
+ const unavailableKeyIdentities = /* @__PURE__ */ new Set([keyInputIdentity(Key2.ctrl("c"))]);
726
+ for (const action of ALT_SCREEN_ACTIONS_BEFORE_BOTTOM) {
727
+ for (const actionKey of keybindings.getKeys(action)) {
728
+ unavailableKeyIdentities.add(keyInputIdentity(String(actionKey)));
729
+ }
730
+ }
731
+ if (!copyOnSelect) {
732
+ for (const copyKey of keybindings.getKeys("app.message.copy")) {
733
+ unavailableKeyIdentities.add(keyInputIdentity(String(copyKey)));
734
+ }
735
+ }
736
+ const key = keybindings.getKeys("tui.altScreen.bottom").map((candidate) => keyInputIdentity(String(candidate))).find(
737
+ (identity) => identity && canMatchKeyInput(identity) && !unavailableKeyIdentities.has(identity) && formatEffectiveKeyLabel(identity)
738
+ );
739
+ const label = theme.fg("text", " \u2193 Jump to latest message");
740
+ const shortcut = key ? theme.fg("muted", ` \xB7 ${formatEffectiveKeyLabel(key)}`) : "";
741
+ return theme.bg("selectedBg", `${label}${shortcut} `);
742
+ },
628
743
  searchCurrentMatchStyle: (text) => theme.bold(theme.inverse(styleSearchMatch(text))),
629
744
  openUrl,
630
745
  copySelection: async (text) => {
@@ -2376,14 +2491,7 @@ function renderSideThreadHeader(width, theme, thinkingLevel) {
2376
2491
  return theme.fg("muted", `${title}${"\u2500".repeat(ruleWidth)}`);
2377
2492
  }
2378
2493
  function thinkingKeyLabel(keybindings) {
2379
- const key = sanitizeSingleLine(String(keybindings.getKeys("app.thinking.cycle")[0] ?? "shift+tab")) || "Shift+Tab";
2380
- return key.split("+").map((part) => {
2381
- const lower = part.toLowerCase();
2382
- if (lower === "shift") return "Shift";
2383
- if (lower === "ctrl") return "Ctrl";
2384
- if (lower === "alt") return "Alt";
2385
- return part.length === 1 ? part.toUpperCase() : `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`;
2386
- }).join("+");
2494
+ return formatKeyLabel2(String(keybindings.getKeys("app.thinking.cycle")[0] ?? "shift+tab")) || "Shift+Tab";
2387
2495
  }
2388
2496
  function fitComposerLayout(header, contentLines, footer, editorLines, availableRows, statusLines = []) {
2389
2497
  const lines = [header, ...contentLines, ...statusLines, footer, ...editorLines];