@narumitw/pi-btw 0.57.1 → 0.58.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/dist/index.ts CHANGED
@@ -481,15 +481,23 @@ import {
481
481
  copyToClipboard as copyToHostClipboard
482
482
  } from "@earendil-works/pi-coding-agent";
483
483
  import {
484
- isKeyRelease,
485
- isKittyProtocolActive,
484
+ isKeyRelease as isKeyRelease2,
485
+ isKittyProtocolActive as isKittyProtocolActive2,
486
486
  Key as Key2,
487
- matchesKey as matchesKey2,
488
487
  parseKey,
489
488
  TuiAltScreen,
490
489
  truncateToWidth as truncateToWidth2
491
490
  } from "@earendil-works/pi-tui";
492
491
 
492
+ // src/keybindings.ts
493
+ import {
494
+ isKeyRelease,
495
+ isKittyProtocolActive,
496
+ KeybindingsManager,
497
+ matchesKey as matchesKey2,
498
+ TUI_KEYBINDINGS
499
+ } from "@earendil-works/pi-tui";
500
+
493
501
  // src/text.ts
494
502
  function sanitizeSingleLine(text) {
495
503
  return [...text.replace(/[\r\n\t]/gu, " ")].filter((character) => {
@@ -510,6 +518,234 @@ function formatKeyLabel2(key) {
510
518
  }).join("+");
511
519
  }
512
520
 
521
+ // src/keybindings.ts
522
+ var BTW_SHORTCUT_ACTIONS = ["exit", "cycleThinkingLevel", "bringToMain"];
523
+ var MODIFIERS = ["shift", "alt", "ctrl", "super"];
524
+ var SYMBOLS = "`-=[]\\;',./!@#$%^&*()_|~{}:<>?";
525
+ var SPECIAL = {
526
+ escape: 27,
527
+ tab: 9,
528
+ enter: 13,
529
+ space: 32,
530
+ backspace: 127,
531
+ insert: 57425,
532
+ delete: 57426,
533
+ home: 57423,
534
+ end: 57424,
535
+ pageup: 57421,
536
+ pagedown: 57422,
537
+ left: 57417,
538
+ right: 57418,
539
+ up: 57419,
540
+ down: 57420
541
+ };
542
+ var FUNCTION_INPUTS = [
543
+ "OP",
544
+ "OQ",
545
+ "OR",
546
+ "OS",
547
+ "[15~",
548
+ "[17~",
549
+ "[18~",
550
+ "[19~",
551
+ "[20~",
552
+ "[21~",
553
+ "[23~",
554
+ "[24~"
555
+ ];
556
+ var LEGACY_INPUTS = [
557
+ ...Array.from({ length: 128 }, (_, code) => String.fromCharCode(code)),
558
+ ...Array.from({ length: 128 }, (_, code) => `\x1B${String.fromCharCode(code)}`),
559
+ "\x1B[Z",
560
+ "\x1BOM",
561
+ "\x1B[E",
562
+ "\x1B[e",
563
+ "\x1BOe",
564
+ ...FUNCTION_INPUTS.map((suffix) => `\x1B${suffix}`)
565
+ ];
566
+ function normalizeBtwKey(value) {
567
+ if (typeof value !== "string" || value.length > 80 || /[\s\p{Cc}]/u.test(value)) return void 0;
568
+ const parts = value.toLowerCase().split("+");
569
+ let base = parts.pop();
570
+ if (!base) return void 0;
571
+ if (base === "esc") base = "escape";
572
+ if (base === "return") base = "enter";
573
+ if (new Set(parts).size !== parts.length || parts.some((part) => !MODIFIERS.includes(part)))
574
+ return void 0;
575
+ if (!Object.hasOwn(SPECIAL, base) && base !== "clear" && !/^f(?:[1-9]|1[0-2])$/u.test(base) && !(base.length === 1 && (/^[a-z0-9]$/u.test(base) || SYMBOLS.includes(base))))
576
+ return void 0;
577
+ if ((base === "escape" || base.startsWith("f") && base.length > 1) && parts.length)
578
+ return void 0;
579
+ if (base === "clear" && (parts.length > 1 || parts.length === 1 && !["ctrl", "shift"].includes(parts[0] ?? "")))
580
+ return void 0;
581
+ return [...MODIFIERS.filter((part) => parts.includes(part)), base].join("+");
582
+ }
583
+ function inputsFor(key) {
584
+ const parts = key.split("+");
585
+ const base = parts.pop() ?? "";
586
+ const modifier = MODIFIERS.reduce(
587
+ (mask, part, bit) => mask | (parts.includes(part) ? 1 << bit : 0),
588
+ 0
589
+ );
590
+ const code = SPECIAL[base] ?? (base.length === 1 ? base.charCodeAt(0) : void 0);
591
+ const inputs = code === void 0 ? LEGACY_INPUTS : [...LEGACY_INPUTS, `\x1B[${code};${modifier + 1}u`];
592
+ return inputs.filter((input) => matchesKey2(input, key));
593
+ }
594
+ function btwKeysOverlap(first, second) {
595
+ const normalized = normalizeBtwKey(first);
596
+ return normalized !== void 0 && inputsFor(normalized).some((input) => matchesKey2(input, second));
597
+ }
598
+ function reservedKeys(keybindings, copyOnSelect) {
599
+ return [
600
+ "ctrl+c",
601
+ "shift+backspace",
602
+ "shift+delete",
603
+ "shift+space",
604
+ // Exact-range review uses these fixed selection actions even if Editor is remapped.
605
+ "shift+left",
606
+ "shift+right",
607
+ "shift+up",
608
+ "shift+down",
609
+ "left",
610
+ "right",
611
+ "enter",
612
+ "alt+enter",
613
+ "ctrl+j",
614
+ "pageUp",
615
+ "pageDown",
616
+ ...Object.keys(TUI_KEYBINDINGS).flatMap(
617
+ (id) => keybindings.getKeys(id)
618
+ ),
619
+ ...!copyOnSelect ? keybindings.getKeys("app.message.copy") : []
620
+ ];
621
+ }
622
+ function isTextKey(key) {
623
+ const parts = key.split("+");
624
+ const base = parts.at(-1) ?? "";
625
+ return (base.length === 1 || base === "space") && !parts.some((part) => ["ctrl", "alt", "super"].includes(part));
626
+ }
627
+ function resolveBtwShortcuts(overrides = {}, keybindings, copyOnSelect = true) {
628
+ let mode = isKittyProtocolActive();
629
+ let snapshot = resolveShortcutSnapshot(overrides, keybindings, copyOnSelect);
630
+ const current = () => {
631
+ if (mode !== isKittyProtocolActive()) {
632
+ mode = isKittyProtocolActive();
633
+ snapshot = resolveShortcutSnapshot(overrides, keybindings, copyOnSelect);
634
+ }
635
+ return snapshot;
636
+ };
637
+ return {
638
+ get keys() {
639
+ return current().keys;
640
+ },
641
+ get warnings() {
642
+ return current().warnings;
643
+ },
644
+ matches: (data, action) => current().matches(data, action),
645
+ label: (action) => current().label(action)
646
+ };
647
+ }
648
+ function resolveShortcutSnapshot(overrides = {}, keybindings, copyOnSelect = true) {
649
+ const reserved = reservedKeys(keybindings, copyOnSelect);
650
+ const keys = { exit: ["ctrl+c"], cycleThinkingLevel: [], bringToMain: [] };
651
+ const warnings = [];
652
+ const defaults = {
653
+ exit: ["ctrl+c"],
654
+ cycleThinkingLevel: keybindings.getKeys("app.thinking.cycle"),
655
+ bringToMain: ["ctrl+r"]
656
+ };
657
+ const usable = (action, candidate, inherited = false) => {
658
+ const key = normalizeBtwKey(candidate);
659
+ if (!key || !inherited && isTextKey(key)) return void 0;
660
+ const inputs = inputsFor(key);
661
+ if (!inputs.length) return void 0;
662
+ if (action === "exit" && key === "ctrl+c") return key;
663
+ if (reserved.some(
664
+ (other) => typeof other === "string" && inputs.some((input) => matchesKey2(input, other))
665
+ ))
666
+ return void 0;
667
+ return key;
668
+ };
669
+ const candidates = {};
670
+ const availableDefaults = { ...defaults };
671
+ for (const action of BTW_SHORTCUT_ACTIONS) {
672
+ availableDefaults[action] = defaults[action].map((key) => usable(action, key, action === "cycleThinkingLevel")).filter((key) => key !== void 0);
673
+ const override = overrides[action];
674
+ if (override !== void 0) candidates[action] = usable(action, override);
675
+ }
676
+ for (; ; ) {
677
+ const rejected = BTW_SHORTCUT_ACTIONS.filter((action) => {
678
+ const candidate = candidates[action];
679
+ return candidate !== void 0 && BTW_SHORTCUT_ACTIONS.some(
680
+ (other) => other !== action && (candidates[other] ? [candidates[other]] : availableDefaults[other]).some(
681
+ (key) => btwKeysOverlap(candidate, key)
682
+ )
683
+ );
684
+ });
685
+ if (!rejected.length) break;
686
+ for (const action of rejected) delete candidates[action];
687
+ }
688
+ for (const action of BTW_SHORTCUT_ACTIONS) {
689
+ const override = overrides[action];
690
+ const selected = candidates[action];
691
+ if (override !== void 0 && !selected)
692
+ warnings.push(
693
+ `${action}: configured shortcut is invalid or conflicts with a reserved action; using an available default.`
694
+ );
695
+ const effective = selected ? [selected] : availableDefaults[action].filter(
696
+ (key) => !BTW_SHORTCUT_ACTIONS.some(
697
+ (other) => other !== action && keys[other].some((used) => btwKeysOverlap(key, used))
698
+ )
699
+ );
700
+ keys[action] = action === "exit" ? [.../* @__PURE__ */ new Set([...effective, "ctrl+c"])] : effective;
701
+ if (!keys[action].length && (override !== void 0 || defaults[action].length > 0))
702
+ warnings.push(`${action}: no usable shortcut; change Pi BTW Settings or Pi keybindings.`);
703
+ }
704
+ return {
705
+ keys,
706
+ warnings,
707
+ matches: (data, action) => !isKeyRelease(data) && keys[action].some((key) => matchesKey2(data, key)),
708
+ label: (action) => keys[action].length ? formatKeyLabel2(keys[action][0] ?? "") : "Unavailable"
709
+ };
710
+ }
711
+ function validateBtwShortcutEdit(action, value, overrides, keybindings, copyOnSelect) {
712
+ if (value === void 0) return void 0;
713
+ if (!normalizeBtwKey(value))
714
+ return "Invalid key combination. Use a Pi key name such as ctrl+q or f6.";
715
+ const next = { ...overrides, [action]: value };
716
+ const resolved = resolveBtwShortcuts(next, keybindings, copyOnSelect);
717
+ const previous = resolveBtwShortcuts(overrides, keybindings, copyOnSelect);
718
+ for (const item of BTW_SHORTCUT_ACTIONS) {
719
+ if (item === action && !resolved.keys[item].includes(normalizeBtwKey(value) ?? "") || item !== action && previous.keys[item].some((key) => !resolved.keys[item].includes(key))) {
720
+ return `${item} conflicts with another BTW shortcut, editing, selection, search, scrolling, or copying. Choose a different key.`;
721
+ }
722
+ }
723
+ return void 0;
724
+ }
725
+ var bindingsByTui = /* @__PURE__ */ new WeakMap();
726
+ function setBtwShortcuts(tui, shortcuts) {
727
+ bindingsByTui.set(tui, shortcuts);
728
+ }
729
+ function getBtwShortcuts(tui, keybindings) {
730
+ return bindingsByTui.get(tui) ?? resolveBtwShortcuts(
731
+ {},
732
+ keybindings ?? new KeybindingsManager({
733
+ ...TUI_KEYBINDINGS,
734
+ "app.thinking.cycle": { defaultKeys: "shift+tab" }
735
+ })
736
+ );
737
+ }
738
+ var BtwPasteGuard = class {
739
+ active = false;
740
+ consume(data) {
741
+ const wasActive = this.active;
742
+ const starts = data.includes("\x1B[200~");
743
+ if (starts) this.active = true;
744
+ if (this.active && data.includes("\x1B[201~")) this.active = false;
745
+ return wasActive || starts;
746
+ }
747
+ };
748
+
513
749
  // src/fullscreen-ui.ts
514
750
  var FullscreenUiDisposedError = class extends Error {
515
751
  constructor() {
@@ -690,7 +926,7 @@ function legacyRawInput(key) {
690
926
  const parts = key.split("+");
691
927
  const base = parts.at(-1) ?? "";
692
928
  if (parts.length === 2 && parts[0] === "ctrl") return rawCtrlInput(base);
693
- if (isKittyProtocolActive()) return void 0;
929
+ if (isKittyProtocolActive2()) return void 0;
694
930
  if (parts.length === 2 && parts[0] === "alt" && base.length === 1) return `\x1B${base}`;
695
931
  if (parts.length === 3 && parts[0] === "ctrl" && parts[1] === "alt") {
696
932
  const input = rawCtrlInput(base);
@@ -761,7 +997,7 @@ function createBtwFullscreenTui(parent, theme, keybindings, copyOnSelect, manual
761
997
  if (isInBracketedPaste && data.includes(BRACKETED_PASTE_END)) {
762
998
  isInBracketedPaste = false;
763
999
  }
764
- if (wasInBracketedPaste || startsBracketedPaste || fullscreen.hasFocusedOverlay() || isKeyRelease(data) || !keybindings.matches(data, "app.message.copy")) {
1000
+ if (wasInBracketedPaste || startsBracketedPaste || fullscreen.hasFocusedOverlay() || isKeyRelease2(data) || !keybindings.matches(data, "app.message.copy")) {
765
1001
  return void 0;
766
1002
  }
767
1003
  if (!fullscreen.hasActiveSelection()) {
@@ -839,9 +1075,30 @@ var BtwFullscreenHost = class {
839
1075
  this.fullscreen = this.createTui(this.parent, this.theme, this.keybindings, this.options);
840
1076
  this.fullscreenCreated = true;
841
1077
  this.fullscreen.start();
1078
+ const shortcuts = resolveBtwShortcuts(
1079
+ this.options.keybindings,
1080
+ this.keybindings,
1081
+ this.options.copyOnSelect ?? true
1082
+ );
1083
+ setBtwShortcuts(this.fullscreen, shortcuts);
1084
+ let previousWarnings = [];
1085
+ const reportWarnings = () => {
1086
+ const warnings = shortcuts.warnings;
1087
+ for (const warning of warnings) {
1088
+ if (previousWarnings.includes(warning)) continue;
1089
+ try {
1090
+ this.ctx.ui.notify(`Pi BTW: ${warning}`, "warning");
1091
+ } catch {
1092
+ }
1093
+ }
1094
+ previousWarnings = warnings;
1095
+ };
1096
+ const pasteGuard = new BtwPasteGuard();
842
1097
  const addHardCancelListener = this.fullscreen.addInputListenerBeforeAll?.bind(this.fullscreen) ?? this.fullscreen.addInputListenerBeforeViewport?.bind(this.fullscreen) ?? this.fullscreen.addInputListener.bind(this.fullscreen);
843
1098
  this.removeHardCancelListener = addHardCancelListener((data) => {
844
- if (isKeyRelease(data) || !matchesKey2(data, Key2.ctrl("c"))) return void 0;
1099
+ reportWarnings();
1100
+ if (pasteGuard.consume(data) || !shortcuts.matches(data, "exit")) return void 0;
1101
+ this.disposed = true;
845
1102
  try {
846
1103
  this.hardCancelActiveCustom?.();
847
1104
  } finally {
@@ -1242,6 +1499,17 @@ function btwSettingsPath() {
1242
1499
  function normalizeBtwSettings(value) {
1243
1500
  if (!isSettingsDocument(value)) return void 0;
1244
1501
  const settings = {};
1502
+ if (Object.hasOwn(value, "keybindings")) {
1503
+ const keys = value.keybindings;
1504
+ if (!isSettingsDocument(keys)) return void 0;
1505
+ settings.keybindings = {};
1506
+ for (const action of BTW_SHORTCUT_ACTIONS) {
1507
+ if (!Object.hasOwn(keys, action)) continue;
1508
+ const key = normalizeBtwKey(keys[action]);
1509
+ if (!key) return void 0;
1510
+ settings.keybindings[action] = key;
1511
+ }
1512
+ }
1245
1513
  if (Object.hasOwn(value, "model")) {
1246
1514
  const model = Reflect.get(value, "model");
1247
1515
  if (typeof model !== "string" || !parseBtwModelReference(model)) return void 0;
@@ -1285,6 +1553,8 @@ function updateBtwSettings(patch, options = {}) {
1285
1553
  return enqueueMutation(settingsPath, async () => {
1286
1554
  options.signal?.throwIfAborted();
1287
1555
  const current = await readSettingsDocumentForUpdate(settingsPath);
1556
+ options.signal?.throwIfAborted();
1557
+ options.validateCurrent?.(normalizeBtwSettings(current) ?? {});
1288
1558
  const updated = applyBtwSettingsPatch(current, patch);
1289
1559
  const settings = normalizeBtwSettings(updated);
1290
1560
  if (!settings) throw invalidSettingsError(settingsPath, "invalid settings shape");
@@ -1403,6 +1673,16 @@ async function publishSettings(settingsPath, document, signal, beforeRename) {
1403
1673
  }
1404
1674
  function applyBtwSettingsPatch(current, patch) {
1405
1675
  const updated = { ...current };
1676
+ if (patch.keybindings) {
1677
+ const keys = isSettingsDocument(current.keybindings) ? { ...current.keybindings } : {};
1678
+ for (const action of BTW_SHORTCUT_ACTIONS) {
1679
+ if (!Object.hasOwn(patch.keybindings, action)) continue;
1680
+ if (patch.keybindings[action] === void 0) delete keys[action];
1681
+ else keys[action] = patch.keybindings[action];
1682
+ }
1683
+ if (Object.keys(keys).length) updated.keybindings = keys;
1684
+ else delete updated.keybindings;
1685
+ }
1406
1686
  if (Object.hasOwn(patch, "thinkingLevel")) {
1407
1687
  if (patch.thinkingLevel === void 0) delete updated.thinkingLevel;
1408
1688
  else updated.thinkingLevel = patch.thinkingLevel;
@@ -1447,6 +1727,64 @@ async function showBtwCommandMenu(ctx, options) {
1447
1727
  let startSelected = false;
1448
1728
  let treeSelected = false;
1449
1729
  let resumedThreadId;
1730
+ let keybindings;
1731
+ let shortcut = "exit";
1732
+ const shortcutLabels = {
1733
+ exit: "Exit shortcut",
1734
+ cycleThinkingLevel: "Cycle thinking level shortcut",
1735
+ bringToMain: "Bring to main shortcut"
1736
+ };
1737
+ const shortcutValue = (settings, action) => {
1738
+ if (!keybindings) return "Default";
1739
+ const effective = resolveBtwShortcuts(
1740
+ settings.keybindings,
1741
+ keybindings,
1742
+ effectiveFullscreenCopyOnSelect(settings)
1743
+ );
1744
+ const configured = settings.keybindings?.[action];
1745
+ if (configured !== void 0 && !effective.keys[action].includes(configured)) {
1746
+ return `Fallback (${effective.label(action)}; saved ${formatKeyLabel2(configured)})`;
1747
+ }
1748
+ const source = configured === void 0 ? action === "cycleThinkingLevel" ? "Inherit Pi" : "Default" : "Custom";
1749
+ return `${source} (${effective.label(action)})`;
1750
+ };
1751
+ const saveShortcut = async (state, value, signal) => {
1752
+ if (!keybindings || state.kind !== "valid" || signal.aborted)
1753
+ return { kind: "rejected" };
1754
+ const action = shortcut;
1755
+ const manager = keybindings;
1756
+ const validate = (settings) => validateBtwShortcutEdit(
1757
+ action,
1758
+ value,
1759
+ settings.keybindings ?? {},
1760
+ manager,
1761
+ effectiveFullscreenCopyOnSelect(settings)
1762
+ );
1763
+ const error = validate(state.settings);
1764
+ if (error) {
1765
+ notifySafely(ctx, error, "error");
1766
+ return { kind: "rejected" };
1767
+ }
1768
+ try {
1769
+ await updateSettings(
1770
+ { keybindings: { [action]: value === void 0 ? void 0 : normalizeBtwKey(value) } },
1771
+ {
1772
+ settingsPath,
1773
+ signal,
1774
+ validateCurrent: (settings) => {
1775
+ const conflict = validate(settings);
1776
+ if (conflict) throw new Error(conflict);
1777
+ }
1778
+ }
1779
+ );
1780
+ if (signal.aborted) return { kind: "rejected" };
1781
+ notifySafely(ctx, "Pi BTW shortcut saved; applies when opening or resuming BTW.", "info");
1782
+ return { kind: "back" };
1783
+ } catch (error2) {
1784
+ if (!signal.aborted) notifySaveFailure(ctx, error2);
1785
+ return { kind: "rejected" };
1786
+ }
1787
+ };
1450
1788
  const loadState = async () => {
1451
1789
  const loaded = await readSettings(settingsPath);
1452
1790
  if (loaded.kind === "invalid") {
@@ -1498,7 +1836,7 @@ async function showBtwCommandMenu(ctx, options) {
1498
1836
  {
1499
1837
  id: "settings",
1500
1838
  label: "Settings",
1501
- description: "Choose thinking, shortcut memory, and selection copying",
1839
+ description: "Choose thinking, keybindings, and selection copying",
1502
1840
  to: state.kind === "invalid" ? "invalid" : "settings"
1503
1841
  }
1504
1842
  ],
@@ -1545,9 +1883,34 @@ async function showBtwCommandMenu(ctx, options) {
1545
1883
  currentValue: effectiveFullscreenCopyOnSelect(state.settings) ? "On" : "Off",
1546
1884
  values: ["On", "Off"],
1547
1885
  action: "set-fullscreen-copy"
1548
- }
1886
+ },
1887
+ ...BTW_SHORTCUT_ACTIONS.map((action) => ({
1888
+ id: action,
1889
+ label: shortcutLabels[action],
1890
+ description: "Edit a BTW-only key combination or restore its default. Ctrl+C always hard-cancels.",
1891
+ currentValue: shortcutValue(state.settings, action),
1892
+ action: "edit-shortcut"
1893
+ }))
1549
1894
  ]
1550
1895
  }),
1896
+ shortcut: ({ state }) => ({
1897
+ kind: "actions",
1898
+ title: shortcutLabels[shortcut],
1899
+ lines: [shortcutValue(state.settings, shortcut), "Ctrl+C always hard-cancels BTW."],
1900
+ items: [
1901
+ { id: "edit", label: "Edit key combination\u2026", to: "shortcut-input" },
1902
+ { id: "reset", label: "Restore default", action: "reset-shortcut" }
1903
+ ],
1904
+ hint: "back"
1905
+ }),
1906
+ "shortcut-input": () => ({
1907
+ kind: "input",
1908
+ title: shortcutLabels[shortcut],
1909
+ lines: ["Type a key name, not the shortcut itself. For example: ctrl+q or f6."],
1910
+ placeholder: "Key combination",
1911
+ action: "save-shortcut",
1912
+ hint: "back"
1913
+ }),
1551
1914
  invalid: ({ state }) => ({
1552
1915
  kind: "detail",
1553
1916
  title: "Pi BTW Settings \xB7 Read only",
@@ -1559,6 +1922,14 @@ async function showBtwCommandMenu(ctx, options) {
1559
1922
  })
1560
1923
  },
1561
1924
  actions: {
1925
+ "edit-shortcut": ({ itemId }) => {
1926
+ if (!BTW_SHORTCUT_ACTIONS.includes(itemId))
1927
+ return { kind: "rejected" };
1928
+ shortcut = itemId;
1929
+ return { kind: "to", screen: "shortcut" };
1930
+ },
1931
+ "save-shortcut": ({ state, value, signal }) => saveShortcut(state, value?.trim() ?? "", signal),
1932
+ "reset-shortcut": ({ state, signal }) => saveShortcut(state, void 0, signal),
1562
1933
  start: async () => {
1563
1934
  startSelected = true;
1564
1935
  return { kind: "close" };
@@ -1622,7 +1993,10 @@ async function showBtwCommandMenu(ctx, options) {
1622
1993
  });
1623
1994
  const result = await runBtwMenuPreservingEditor(
1624
1995
  ctx,
1625
- (menuContext) => runMenu(menuContext, menu, { getState: loadState })
1996
+ (menuContext) => runMenu(menuContext, menu, { getState: loadState }),
1997
+ (manager) => {
1998
+ keybindings = manager;
1999
+ }
1626
2000
  );
1627
2001
  if (result.kind !== "closed" || result.reason !== "close") return "closed";
1628
2002
  if (resumedThreadId) return { kind: "resume", threadId: resumedThreadId };
@@ -1650,23 +2024,23 @@ async function showBtwCustomPreservingEditor(ctx, factory) {
1650
2024
  }
1651
2025
  return result;
1652
2026
  }
1653
- async function runBtwMenuPreservingEditor(ctx, run) {
2027
+ async function runBtwMenuPreservingEditor(ctx, run, onKeybindings) {
1654
2028
  let liveEditorText = ctx.ui.getEditorText();
1655
2029
  let completed = false;
1656
2030
  const ui = new Proxy(ctx.ui, {
1657
2031
  get(target, property) {
1658
2032
  if (property === "custom") {
1659
- return (factory, customOptions) => target.custom(
1660
- (tui, theme, keybindings, done) => factory(tui, theme, keybindings, (value2) => {
2033
+ return (factory, customOptions) => target.custom((tui, theme, keybindings, done) => {
2034
+ onKeybindings?.(keybindings);
2035
+ return factory(tui, theme, keybindings, (value2) => {
1661
2036
  try {
1662
2037
  liveEditorText = target.getEditorText();
1663
2038
  } catch {
1664
2039
  }
1665
2040
  completed = true;
1666
2041
  done(value2);
1667
- }),
1668
- customOptions
1669
- );
2042
+ });
2043
+ }, customOptions);
1670
2044
  }
1671
2045
  const value = Reflect.get(target, property, target);
1672
2046
  return typeof value === "function" ? value.bind(target) : value;
@@ -2052,6 +2426,7 @@ var BtwTranscriptPager = class {
2052
2426
  this.theme = theme;
2053
2427
  this.onAction = onAction;
2054
2428
  this.options = options;
2429
+ this.shortcuts = getBtwShortcuts(tui, options.thinking?.keybindings);
2055
2430
  this.transcriptComponents = buildTranscriptComponents(turns, this.theme);
2056
2431
  this.canBringToMain = turns.some((turn) => turn.kind === "answered");
2057
2432
  this.thinkingLevel = options.thinking?.level;
@@ -2095,6 +2470,8 @@ var BtwTranscriptPager = class {
2095
2470
  theme;
2096
2471
  onAction;
2097
2472
  options;
2473
+ shortcuts;
2474
+ pasteGuard = new BtwPasteGuard();
2098
2475
  transcriptComponents;
2099
2476
  editor;
2100
2477
  canBringToMain;
@@ -2116,6 +2493,7 @@ var BtwTranscriptPager = class {
2116
2493
  return this.layoutRoot;
2117
2494
  }
2118
2495
  render(width) {
2496
+ if (width <= 0) return [];
2119
2497
  const safeWidth = Math.max(1, width);
2120
2498
  const editorLines = this.editor.render(safeWidth);
2121
2499
  const availableRows = Math.max(1, this.tui.terminal.rows - RESERVED_APP_LINES);
@@ -2136,22 +2514,27 @@ var BtwTranscriptPager = class {
2136
2514
  this.renderFooter(safeWidth),
2137
2515
  editorLines,
2138
2516
  availableRows
2139
- );
2517
+ ).map((line) => truncateToWidth3(line, safeWidth));
2140
2518
  }
2141
2519
  handleInput(data) {
2142
2520
  if (this.finished) return;
2143
- if (matchesKey4(data, Key4.ctrl("c"))) {
2521
+ if (this.pasteGuard.consume(data)) {
2522
+ this.editor.handleInput(data);
2523
+ this.tui.requestRender();
2524
+ return;
2525
+ }
2526
+ if (this.shortcuts.matches(data, "exit")) {
2144
2527
  this.finished = true;
2145
2528
  this.onAction({ kind: "close" });
2146
2529
  return;
2147
2530
  }
2148
- if (this.canBringToMain && matchesKey4(data, Key4.ctrl("r"))) {
2531
+ if (this.canBringToMain && this.shortcuts.matches(data, "bringToMain")) {
2149
2532
  this.finished = true;
2150
2533
  this.onAction({ kind: "bringToMain", questionDraft: this.editor.getExpandedText() });
2151
2534
  return;
2152
2535
  }
2153
2536
  const thinking = this.options.thinking;
2154
- if (thinking && thinking.levels.length > 1 && thinking.keybindings.matches(data, "app.thinking.cycle")) {
2537
+ if (thinking && thinking.levels.length > 1 && this.shortcuts.matches(data, "cycleThinkingLevel")) {
2155
2538
  const currentIndex = thinking.levels.indexOf(this.thinkingLevel ?? thinking.level);
2156
2539
  const nextLevel = thinking.levels[(currentIndex + 1) % thinking.levels.length];
2157
2540
  if (nextLevel) {
@@ -2184,23 +2567,26 @@ var BtwTranscriptPager = class {
2184
2567
  this.onAction({ kind: "close" });
2185
2568
  }
2186
2569
  renderFooter(width) {
2570
+ const exit = this.shortcuts.label("exit");
2571
+ const bring = this.canBringToMain && this.shortcuts.keys.bringToMain.length > 0;
2572
+ const bringKey = this.shortcuts.label("bringToMain");
2187
2573
  if (this.warning) {
2188
- const warning = width < 32 ? "Empty \u2022 Ctrl+C" : `${this.warning} \u2022 Ctrl+C exit`;
2574
+ const warning = width < 32 ? `Empty \u2022 ${exit}` : `${this.warning} \u2022 ${exit} exit`;
2189
2575
  return truncateToWidth3(this.theme.fg("warning", warning), width);
2190
2576
  }
2191
2577
  const scrollable = this.getMaxScrollOffset() > 0;
2192
2578
  const thinking = this.options.thinking;
2193
- const cycleHint = thinking && thinking.levels.length > 1 && this.thinkingLevel ? ` \u2022 thinking ${this.thinkingLevel} \u2022 ${thinkingKeyLabel(thinking.keybindings)} cycle` : "";
2194
- const base = this.canBringToMain ? "btw \u2022 Enter send \u2022 Ctrl+R bring to main \u2022 Ctrl+C exit" : "btw \u2022 Enter send \u2022 Ctrl+C exit";
2579
+ const cycleHint = thinking && thinking.levels.length > 1 && this.thinkingLevel && this.shortcuts.keys.cycleThinkingLevel.length ? ` \u2022 thinking ${this.thinkingLevel} \u2022 ${this.shortcuts.label("cycleThinkingLevel")} cycle` : "";
2580
+ const base = bring ? `btw \u2022 Enter send \u2022 ${bringKey} bring to main \u2022 ${exit} exit` : `btw \u2022 Enter send \u2022 ${exit} exit`;
2195
2581
  const fullBase = `${base}${cycleHint}`;
2196
- const fallbackBase = "btw \u2022 Enter \u2022 Ctrl+C";
2197
- const compactBase = this.canBringToMain ? "btw \u2022 Enter \u2022 Ctrl+R \u2022 Ctrl+C" : fallbackBase;
2582
+ const fallbackBase = `btw \u2022 Enter \u2022 ${exit}`;
2583
+ const compactBase = bring ? `btw \u2022 Enter \u2022 ${bringKey} \u2022 ${exit}` : fallbackBase;
2198
2584
  const compactWithThinking = `${compactBase}${cycleHint}`;
2199
2585
  let hints = visibleWidth2(fullBase) <= width ? fullBase : visibleWidth2(compactWithThinking) <= width ? compactWithThinking : visibleWidth2(compactBase) <= width ? compactBase : fallbackBase;
2200
2586
  if (scrollable) {
2201
2587
  const history = ` \u2022 ${this.scrollView.scrollTop > 0 ? "\u2191 older" : "\u2193 newer"} \u2022 PgUp/PgDn history`;
2202
2588
  const compactHistory = " \u2022 PgUp/PgDn";
2203
- const compactScrollable = this.canBringToMain ? "Enter \u2022 Ctrl+R \u2022 Ctrl+C \u2022 PgUp/PgDn" : `${fallbackBase}${compactHistory}`;
2589
+ const compactScrollable = bring ? `Enter \u2022 ${bringKey} \u2022 ${exit} \u2022 PgUp/PgDn` : `${fallbackBase}${compactHistory}`;
2204
2590
  if (visibleWidth2(`${hints}${history}`) <= width) {
2205
2591
  hints += history;
2206
2592
  } else if (visibleWidth2(`${compactBase}${history}`) <= width) {
@@ -2251,6 +2637,7 @@ var BtwAnsweringView = class {
2251
2637
  this.theme = theme;
2252
2638
  this.onCancel = onCancel;
2253
2639
  this.options = options;
2640
+ this.shortcuts = getBtwShortcuts(tui, options.steering?.thinking?.keybindings);
2254
2641
  this.transcriptComponents = buildTranscriptComponents(turns, this.theme, pendingQuestion);
2255
2642
  this.thinkingLevel = options.steering?.thinking?.level ?? thinkingLevel;
2256
2643
  this.loader = new Loader(
@@ -2304,6 +2691,8 @@ var BtwAnsweringView = class {
2304
2691
  theme;
2305
2692
  onCancel;
2306
2693
  options;
2694
+ shortcuts;
2695
+ pasteGuard = new BtwPasteGuard();
2307
2696
  transcriptComponents;
2308
2697
  loader;
2309
2698
  editor;
@@ -2329,6 +2718,7 @@ var BtwAnsweringView = class {
2329
2718
  return this.layoutRoot;
2330
2719
  }
2331
2720
  render(width) {
2721
+ if (width <= 0) return [];
2332
2722
  const safeWidth = Math.max(1, width);
2333
2723
  const availableRows = Math.max(1, this.tui.terminal.rows - RESERVED_APP_LINES);
2334
2724
  const editorLines = this.editor?.render(safeWidth) ?? [];
@@ -2360,11 +2750,16 @@ var BtwAnsweringView = class {
2360
2750
  editorLines,
2361
2751
  availableRows,
2362
2752
  steeringLines
2363
- );
2753
+ ).map((line) => truncateToWidth3(line, safeWidth));
2364
2754
  }
2365
2755
  handleInput(data) {
2366
2756
  if (this.finished) return;
2367
- if (matchesKey4(data, Key4.ctrl("c"))) {
2757
+ if (this.pasteGuard.consume(data)) {
2758
+ this.editor?.handleInput(data);
2759
+ this.tui.requestRender();
2760
+ return;
2761
+ }
2762
+ if (this.shortcuts.matches(data, "exit")) {
2368
2763
  this.finished = true;
2369
2764
  this.loader.stop();
2370
2765
  this.controller.abort();
@@ -2372,7 +2767,7 @@ var BtwAnsweringView = class {
2372
2767
  return;
2373
2768
  }
2374
2769
  const thinking = this.options.steering?.thinking;
2375
- if (thinking && thinking.levels.length > 1 && thinking.keybindings.matches(data, "app.thinking.cycle")) {
2770
+ if (thinking && thinking.levels.length > 1 && this.shortcuts.matches(data, "cycleThinkingLevel")) {
2376
2771
  const currentIndex = thinking.levels.indexOf(this.thinkingLevel ?? thinking.level);
2377
2772
  const nextLevel = thinking.levels[(currentIndex + 1) % thinking.levels.length];
2378
2773
  if (nextLevel) {
@@ -2415,16 +2810,17 @@ var BtwAnsweringView = class {
2415
2810
  this.onCancel();
2416
2811
  }
2417
2812
  renderFooter(width) {
2813
+ const exit = this.shortcuts.label("exit");
2418
2814
  if (this.warning) {
2419
- const warning = width < 32 ? "Empty \u2022 Ctrl+C" : `${this.warning} \u2022 Ctrl+C cancel`;
2815
+ const warning = width < 32 ? `Empty \u2022 ${exit}` : `${this.warning} \u2022 ${exit} cancel`;
2420
2816
  return truncateToWidth3(this.theme.fg("warning", warning), width);
2421
2817
  }
2422
- const baseHint = this.editor ? "Enter steer \u2022 Ctrl+C cancel" : "Ctrl+C cancel";
2818
+ const baseHint = this.editor ? `Enter steer \u2022 ${exit} cancel` : `${exit} cancel`;
2423
2819
  const thinking = this.options.steering?.thinking;
2424
- const cycleHint = thinking && thinking.levels.length > 1 && this.thinkingLevel ? ` \u2022 thinking ${this.thinkingLevel} \u2022 ${thinkingKeyLabel(thinking.keybindings)} cycle` : "";
2820
+ const cycleHint = thinking && thinking.levels.length > 1 && this.thinkingLevel && this.shortcuts.keys.cycleThinkingLevel.length ? ` \u2022 thinking ${this.thinkingLevel} \u2022 ${this.shortcuts.label("cycleThinkingLevel")} cycle` : "";
2425
2821
  const scrollHint = this.getMaxScrollOffset() > 0 ? " \u2022 PgUp/PgDn history" : "";
2426
2822
  const hints = `${baseHint}${cycleHint}${scrollHint}`;
2427
- const compactHints = this.editor ? "Enter \u2022 Ctrl+C" : "Ctrl+C";
2823
+ const compactHints = this.editor ? `Enter \u2022 ${exit}` : exit;
2428
2824
  const selectedHints = visibleWidth2(hints) <= width ? hints : compactHints;
2429
2825
  const loaderWidth = Math.max(1, width - visibleWidth2(selectedHints) - 3);
2430
2826
  const loaderLine = this.loader.render(loaderWidth).at(-1) ?? "Answering\u2026";
@@ -2512,9 +2908,6 @@ function renderSideThreadHeader(width, theme, thinkingLevel) {
2512
2908
  const ruleWidth = Math.max(0, width - visibleWidth2(title));
2513
2909
  return theme.fg("muted", `${title}${"\u2500".repeat(ruleWidth)}`);
2514
2910
  }
2515
- function thinkingKeyLabel(keybindings) {
2516
- return formatKeyLabel2(String(keybindings.getKeys("app.thinking.cycle")[0] ?? "shift+tab")) || "Shift+Tab";
2517
- }
2518
2911
  function fitComposerLayout(header, contentLines, footer, editorLines, availableRows, statusLines = []) {
2519
2912
  const lines = [header, ...contentLines, ...statusLines, footer, ...editorLines];
2520
2913
  if (lines.length <= availableRows) return lines;
@@ -2605,7 +2998,12 @@ async function resolveBtwModel({
2605
2998
  const fallbackAction = sameAsCurrent ? "no distinct current model is available" : `falling back to ${fallback}`;
2606
2999
  try {
2607
3000
  const auth = await modelRegistry.getApiKeyAndHeaders(configuredModel);
2608
- if (auth.ok && hasRequestAuth(auth)) return { model: configuredModel, auth };
3001
+ if (auth.ok && hasRequestAuth(auth)) {
3002
+ return {
3003
+ model: auth.baseUrl ? { ...configuredModel, baseUrl: auth.baseUrl } : configuredModel,
3004
+ auth
3005
+ };
3006
+ }
2609
3007
  const reason = auth.ok ? "has no request credentials" : auth.error;
2610
3008
  reportWarning(
2611
3009
  `pi-btw model ${settings.model} is unavailable (${reason}); ${fallbackAction}.`
@@ -2621,7 +3019,12 @@ async function resolveBtwModel({
2621
3019
  if (!currentModel) return void 0;
2622
3020
  try {
2623
3021
  const auth = await modelRegistry.getApiKeyAndHeaders(currentModel);
2624
- if (auth.ok && hasRequestAuth(auth)) return { model: currentModel, auth };
3022
+ if (auth.ok && hasRequestAuth(auth)) {
3023
+ return {
3024
+ model: auth.baseUrl ? { ...currentModel, baseUrl: auth.baseUrl } : currentModel,
3025
+ auth
3026
+ };
3027
+ }
2625
3028
  } catch {
2626
3029
  }
2627
3030
  return void 0;
@@ -2748,7 +3151,10 @@ function btw(pi, dependencies = {}) {
2748
3151
  ctx: fullscreenCtx
2749
3152
  });
2750
3153
  },
2751
- { copyOnSelect: effectiveFullscreenCopyOnSelect(settings) }
3154
+ {
3155
+ copyOnSelect: effectiveFullscreenCopyOnSelect(settings),
3156
+ ...settings.keybindings ? { keybindings: settings.keybindings } : {}
3157
+ }
2752
3158
  );
2753
3159
  } finally {
2754
3160
  if (state?.title && state.thread.turns.length > 0) {