@narumitw/pi-btw 0.55.2 → 0.55.3

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,6 +481,9 @@ import {
481
481
  copyToClipboard as copyToHostClipboard
482
482
  } from "@earendil-works/pi-coding-agent";
483
483
  import {
484
+ isKeyRelease,
485
+ Key as Key2,
486
+ matchesKey as matchesKey2,
484
487
  TuiAltScreen,
485
488
  truncateToWidth as truncateToWidth2
486
489
  } from "@earendil-works/pi-tui";
@@ -509,23 +512,31 @@ async function runBtwFullscreen(ctx, run, dependencies = {}) {
509
512
  ));
510
513
  let liveEditorText = ctx.ui.getEditorText();
511
514
  let restoreEditor = false;
515
+ let host;
512
516
  const outcome = await ctx.ui.custom(
513
- (parent, theme, keybindings, done) => new BtwFullscreenHost(
514
- parent,
515
- theme,
516
- keybindings,
517
- ctx,
518
- run,
519
- (value) => {
520
- try {
521
- liveEditorText = ctx.ui.getEditorText();
522
- restoreEditor = true;
523
- } catch {
524
- }
525
- done(value);
526
- },
527
- createTui
528
- )
517
+ (parent, theme, keybindings, done) => {
518
+ host = new BtwFullscreenHost(
519
+ parent,
520
+ theme,
521
+ keybindings,
522
+ ctx,
523
+ run,
524
+ (value) => {
525
+ try {
526
+ liveEditorText = ctx.ui.getEditorText();
527
+ restoreEditor = true;
528
+ } catch {
529
+ }
530
+ done(value);
531
+ },
532
+ createTui
533
+ );
534
+ return host;
535
+ },
536
+ {
537
+ overlay: true,
538
+ onHandle: (handle) => host?.setParentOverlay(handle)
539
+ }
529
540
  );
530
541
  if (restoreEditor) {
531
542
  try {
@@ -536,9 +547,52 @@ async function runBtwFullscreen(ctx, run, dependencies = {}) {
536
547
  if (outcome.kind === "failed") throw outcome.error;
537
548
  return outcome.value;
538
549
  }
550
+ var btwInputListeners = /* @__PURE__ */ new WeakMap();
551
+ function dispatchBtwInput(listeners, data) {
552
+ let current = data;
553
+ for (const group of [listeners.beforeViewport, listeners.regular]) {
554
+ for (const listener of group) {
555
+ const result = listener(current);
556
+ if (result?.consume) return result;
557
+ if (result?.data !== void 0) current = result.data;
558
+ }
559
+ }
560
+ return current === data ? void 0 : { data: current };
561
+ }
562
+ var BtwTuiAltScreen = class extends TuiAltScreen {
563
+ addInputListener(listener) {
564
+ let listeners = btwInputListeners.get(this);
565
+ if (!listeners) {
566
+ const registeredListeners = {
567
+ beforeViewport: /* @__PURE__ */ new Set(),
568
+ regular: /* @__PURE__ */ new Set()
569
+ };
570
+ btwInputListeners.set(this, registeredListeners);
571
+ super.addInputListener((data) => dispatchBtwInput(registeredListeners, data));
572
+ listeners = registeredListeners;
573
+ }
574
+ listeners.regular.add(listener);
575
+ return () => listeners.regular.delete(listener);
576
+ }
577
+ addInputListenerBeforeViewport(listener) {
578
+ const listeners = btwInputListeners.get(this);
579
+ if (!listeners) return super.addInputListener(listener);
580
+ listeners.beforeViewport.add(listener);
581
+ return () => listeners.beforeViewport.delete(listener);
582
+ }
583
+ removeInputListener(listener) {
584
+ const listeners = btwInputListeners.get(this);
585
+ if (!listeners) {
586
+ super.removeInputListener(listener);
587
+ return;
588
+ }
589
+ listeners.beforeViewport.delete(listener);
590
+ listeners.regular.delete(listener);
591
+ }
592
+ };
539
593
  function createBtwFullscreenTui(parent, theme, openUrl, copyToClipboard2) {
540
594
  const styleSearchMatch = (text) => theme.bg("searchMatchBg", theme.fg("searchMatchText", text));
541
- return new TuiAltScreen(parent.terminal, parent.getShowHardwareCursor(), void 0, {
595
+ return new BtwTuiAltScreen(parent.terminal, parent.getShowHardwareCursor(), void 0, {
542
596
  mouse: true,
543
597
  searchMatchStyle: (text) => theme.underline(styleSearchMatch(text)),
544
598
  searchCurrentMatchStyle: (text) => theme.bold(theme.inverse(styleSearchMatch(text))),
@@ -577,10 +631,21 @@ var BtwFullscreenHost = class {
577
631
  done;
578
632
  createTui;
579
633
  fullscreen;
634
+ parentOverlay;
580
635
  cancelActiveCustom;
636
+ hardCancelActiveCustom;
637
+ removeHardCancelListener;
581
638
  started = false;
582
639
  disposed = false;
583
640
  finished = false;
641
+ parentStopped = false;
642
+ parentRestarted = false;
643
+ fullscreenCreated = false;
644
+ fullscreenStopped = false;
645
+ cleanupError;
646
+ setParentOverlay(overlay) {
647
+ this.parentOverlay = overlay;
648
+ }
584
649
  render(width) {
585
650
  return [truncateToWidth2(this.theme.fg("muted", "Opening btw side thread\u2026"), width)];
586
651
  }
@@ -595,44 +660,64 @@ var BtwFullscreenHost = class {
595
660
  if (this.started || this.finished) return;
596
661
  this.started = true;
597
662
  let outcome;
598
- let parentStopped = false;
599
- let fullscreenCreated = false;
600
663
  try {
601
664
  if (this.disposed) throw new FullscreenUiDisposedError();
602
665
  this.parent.stop({ preserveScreen: true });
603
- parentStopped = true;
666
+ this.parentStopped = true;
604
667
  if (this.disposed) throw new FullscreenUiDisposedError();
605
668
  this.fullscreen = this.createTui(this.parent, this.theme);
606
- fullscreenCreated = true;
669
+ this.fullscreenCreated = true;
607
670
  this.fullscreen.start();
671
+ const addHardCancelListener = this.fullscreen.addInputListenerBeforeViewport?.bind(this.fullscreen) ?? this.fullscreen.addInputListener.bind(this.fullscreen);
672
+ this.removeHardCancelListener = addHardCancelListener((data) => {
673
+ if (isKeyRelease(data) || !matchesKey2(data, Key2.ctrl("c"))) return void 0;
674
+ try {
675
+ this.hardCancelActiveCustom?.();
676
+ } finally {
677
+ this.restoreParent();
678
+ }
679
+ return { consume: true };
680
+ });
608
681
  outcome = { kind: "completed", value: await this.run(this.createContext()) };
609
682
  } catch (error) {
610
683
  outcome = { kind: "failed", error };
611
684
  }
612
- let cleanupError;
613
685
  try {
614
686
  this.cancelActiveCustom?.();
615
687
  } catch (error) {
616
- cleanupError = error;
688
+ this.cleanupError ??= error;
617
689
  }
618
- if (fullscreenCreated) {
690
+ this.restoreParent();
691
+ if (this.cleanupError !== void 0) outcome = { kind: "failed", error: this.cleanupError };
692
+ this.finished = true;
693
+ this.done(outcome);
694
+ }
695
+ restoreParent() {
696
+ this.removeHardCancelListener?.();
697
+ this.removeHardCancelListener = void 0;
698
+ if (this.fullscreenCreated && !this.fullscreenStopped) {
699
+ this.fullscreenStopped = true;
619
700
  try {
620
701
  this.fullscreen?.stop({ preserveScreen: true });
621
702
  } catch (error) {
622
- cleanupError ??= error;
703
+ this.cleanupError ??= error;
623
704
  }
624
705
  }
625
- if (parentStopped) {
626
- try {
627
- this.parent.start();
628
- this.parent.renderNow(false);
629
- } catch (error) {
630
- cleanupError ??= error;
631
- }
706
+ if (!this.parentStopped || this.parentRestarted) return;
707
+ const parentOverlay = this.parentOverlay;
708
+ this.parentOverlay = void 0;
709
+ try {
710
+ parentOverlay?.setHidden(true);
711
+ } catch (error) {
712
+ this.cleanupError ??= error;
713
+ }
714
+ try {
715
+ this.parent.start();
716
+ this.parentRestarted = true;
717
+ this.parent.renderNow(false);
718
+ } catch (error) {
719
+ this.cleanupError ??= error;
632
720
  }
633
- if (cleanupError !== void 0) outcome = { kind: "failed", error: cleanupError };
634
- this.finished = true;
635
- this.done(outcome);
636
721
  }
637
722
  createContext() {
638
723
  const ui = new Proxy(this.ctx.ui, {
@@ -706,6 +791,7 @@ var BtwFullscreenHost = class {
706
791
  if (promiseSettled || !hasPendingValue) return;
707
792
  promiseSettled = true;
708
793
  this.cancelActiveCustom = void 0;
794
+ this.hardCancelActiveCustom = void 0;
709
795
  if (!factorySettled) {
710
796
  resolve(pendingValue);
711
797
  return;
@@ -729,6 +815,7 @@ var BtwFullscreenHost = class {
729
815
  closed = true;
730
816
  promiseSettled = true;
731
817
  this.cancelActiveCustom = void 0;
818
+ this.hardCancelActiveCustom = void 0;
732
819
  try {
733
820
  unmount();
734
821
  reject(error);
@@ -741,6 +828,16 @@ var BtwFullscreenHost = class {
741
828
  disposeComponent();
742
829
  if (!promiseSettled) fail(new FullscreenUiDisposedError());
743
830
  };
831
+ this.hardCancelActiveCustom = () => {
832
+ if (promiseSettled) return;
833
+ try {
834
+ component?.handleInput?.("");
835
+ } catch (error) {
836
+ fail(error);
837
+ return;
838
+ }
839
+ this.cancelActiveCustom?.();
840
+ };
744
841
  let created;
745
842
  try {
746
843
  created = factory(fullscreen, this.theme, this.keybindings, close);
@@ -789,7 +886,7 @@ import {
789
886
  copyToClipboard,
790
887
  TreeSelectorComponent
791
888
  } from "@earendil-works/pi-coding-agent";
792
- import { Key as Key2, matchesKey as matchesKey2 } from "@earendil-works/pi-tui";
889
+ import { Key as Key3, matchesKey as matchesKey3 } from "@earendil-works/pi-tui";
793
890
 
794
891
  // src/settings.ts
795
892
  import { randomUUID } from "node:crypto";
@@ -1386,7 +1483,7 @@ var MainThreadTreePickerComponent = class {
1386
1483
  return this.selector.render(width);
1387
1484
  }
1388
1485
  handleInput(data) {
1389
- if (matchesKey2(data, Key2.ctrl("c"))) {
1486
+ if (matchesKey3(data, Key3.ctrl("c"))) {
1390
1487
  this.onClose();
1391
1488
  return;
1392
1489
  }
@@ -1678,10 +1775,10 @@ import {
1678
1775
  import {
1679
1776
  CURSOR_MARKER,
1680
1777
  Editor,
1681
- Key as Key3,
1778
+ Key as Key4,
1682
1779
  Loader,
1683
1780
  Markdown,
1684
- matchesKey as matchesKey3,
1781
+ matchesKey as matchesKey4,
1685
1782
  ScrollView,
1686
1783
  truncateToWidth as truncateToWidth3,
1687
1784
  VStack,
@@ -1794,12 +1891,12 @@ var BtwTranscriptPager = class {
1794
1891
  }
1795
1892
  handleInput(data) {
1796
1893
  if (this.finished) return;
1797
- if (matchesKey3(data, Key3.ctrl("c"))) {
1894
+ if (matchesKey4(data, Key4.ctrl("c"))) {
1798
1895
  this.finished = true;
1799
1896
  this.onAction({ kind: "close" });
1800
1897
  return;
1801
1898
  }
1802
- if (this.canBringToMain && matchesKey3(data, Key3.ctrl("r"))) {
1899
+ if (this.canBringToMain && matchesKey4(data, Key4.ctrl("r"))) {
1803
1900
  this.finished = true;
1804
1901
  this.onAction({ kind: "bringToMain", questionDraft: this.editor.getExpandedText() });
1805
1902
  return;
@@ -1816,12 +1913,12 @@ var BtwTranscriptPager = class {
1816
1913
  }
1817
1914
  return;
1818
1915
  }
1819
- if (matchesKey3(data, Key3.pageUp)) {
1916
+ if (matchesKey4(data, Key4.pageUp)) {
1820
1917
  this.scrollView.scrollBy(-Math.max(1, this.scrollView.viewportHeight));
1821
1918
  this.tui.requestRender();
1822
1919
  return;
1823
1920
  }
1824
- if (matchesKey3(data, Key3.pageDown)) {
1921
+ if (matchesKey4(data, Key4.pageDown)) {
1825
1922
  this.scrollView.scrollBy(Math.max(1, this.scrollView.viewportHeight));
1826
1923
  this.tui.requestRender();
1827
1924
  return;
@@ -2018,7 +2115,7 @@ var BtwAnsweringView = class {
2018
2115
  }
2019
2116
  handleInput(data) {
2020
2117
  if (this.finished) return;
2021
- if (matchesKey3(data, Key3.ctrl("c"))) {
2118
+ if (matchesKey4(data, Key4.ctrl("c"))) {
2022
2119
  this.finished = true;
2023
2120
  this.loader.stop();
2024
2121
  this.controller.abort();
@@ -2037,12 +2134,12 @@ var BtwAnsweringView = class {
2037
2134
  }
2038
2135
  return;
2039
2136
  }
2040
- if (matchesKey3(data, Key3.pageUp)) {
2137
+ if (matchesKey4(data, Key4.pageUp)) {
2041
2138
  this.scrollView.scrollBy(-Math.max(1, this.scrollView.viewportHeight));
2042
2139
  this.tui.requestRender();
2043
2140
  return;
2044
2141
  }
2045
- if (matchesKey3(data, Key3.pageDown)) {
2142
+ if (matchesKey4(data, Key4.pageDown)) {
2046
2143
  this.scrollView.scrollBy(Math.max(1, this.scrollView.viewportHeight));
2047
2144
  this.tui.requestRender();
2048
2145
  return;