@narumitw/pi-btw 0.57.1 → 0.58.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 +39 -0
- package/dist/index.ts +432 -36
- package/dist/index.ts.map +4 -4
- package/docs/workflows.md +4 -3
- package/package.json +1 -1
- package/src/btw.ts +7 -1
- package/src/fullscreen-ui.ts +32 -2
- package/src/keybindings.ts +330 -0
- package/src/menu.ts +136 -19
- package/src/settings.ts +32 -0
- package/src/transcript-pager.ts +52 -30
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 (
|
|
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() ||
|
|
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
|
-
|
|
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,
|
|
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
|
-
(
|
|
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
|
-
|
|
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 (
|
|
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 &&
|
|
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 &&
|
|
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 ?
|
|
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 ${
|
|
2194
|
-
const base =
|
|
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 =
|
|
2197
|
-
const compactBase =
|
|
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 =
|
|
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 (
|
|
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 &&
|
|
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 ?
|
|
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 ?
|
|
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 ${
|
|
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 ?
|
|
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;
|
|
@@ -2748,7 +3141,10 @@ function btw(pi, dependencies = {}) {
|
|
|
2748
3141
|
ctx: fullscreenCtx
|
|
2749
3142
|
});
|
|
2750
3143
|
},
|
|
2751
|
-
{
|
|
3144
|
+
{
|
|
3145
|
+
copyOnSelect: effectiveFullscreenCopyOnSelect(settings),
|
|
3146
|
+
...settings.keybindings ? { keybindings: settings.keybindings } : {}
|
|
3147
|
+
}
|
|
2752
3148
|
);
|
|
2753
3149
|
} finally {
|
|
2754
3150
|
if (state?.title && state.thread.turns.length > 0) {
|