overlayContext.tsx 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. import { c as _c } from "react/compiler-runtime";
  2. /**
  3. * Overlay tracking for Escape key coordination.
  4. *
  5. * This solves the problem of escape key handling when overlays (like Select with onCancel)
  6. * are open. The CancelRequestHandler needs to know when an overlay is active so it doesn't
  7. * cancel requests when the user just wants to dismiss the overlay.
  8. *
  9. * Usage:
  10. * 1. Call useRegisterOverlay() in any overlay component to automatically register it
  11. * 2. Call useIsOverlayActive() to check if any overlay is currently active
  12. *
  13. * The hook automatically registers on mount and unregisters on unmount,
  14. * so no manual cleanup or state management is needed.
  15. */
  16. import { useContext, useEffect, useLayoutEffect } from 'react';
  17. import instances from '../ink/instances.js';
  18. import { AppStoreContext, useAppState } from '../state/AppState.js';
  19. // Non-modal overlays that shouldn't disable TextInput focus
  20. const NON_MODAL_OVERLAYS = new Set(['autocomplete']);
  21. /**
  22. * Hook to register a component as an active overlay.
  23. * Automatically registers on mount and unregisters on unmount.
  24. *
  25. * @param id - Unique identifier for this overlay (e.g., 'select', 'multi-select')
  26. * @param enabled - Whether to register (default: true). Use this to conditionally register
  27. * based on component props, e.g., only register when onCancel is provided.
  28. *
  29. * @example
  30. * // Conditional registration based on whether cancel is supported
  31. * function useSelectInput({ state }) {
  32. * useRegisterOverlay('select', !!state.onCancel)
  33. * // ...
  34. * }
  35. */
  36. export function useRegisterOverlay(id, t0) {
  37. const $ = _c(8);
  38. const enabled = t0 === undefined ? true : t0;
  39. const store = useContext(AppStoreContext);
  40. const setAppState = store?.setState;
  41. let t1;
  42. let t2;
  43. if ($[0] !== enabled || $[1] !== id || $[2] !== setAppState) {
  44. t1 = () => {
  45. if (!enabled || !setAppState) {
  46. return;
  47. }
  48. setAppState(prev => {
  49. if (prev.activeOverlays.has(id)) {
  50. return prev;
  51. }
  52. const next = new Set(prev.activeOverlays);
  53. next.add(id);
  54. return {
  55. ...prev,
  56. activeOverlays: next
  57. };
  58. });
  59. return () => {
  60. setAppState(prev_0 => {
  61. if (!prev_0.activeOverlays.has(id)) {
  62. return prev_0;
  63. }
  64. const next_0 = new Set(prev_0.activeOverlays);
  65. next_0.delete(id);
  66. return {
  67. ...prev_0,
  68. activeOverlays: next_0
  69. };
  70. });
  71. };
  72. };
  73. t2 = [id, enabled, setAppState];
  74. $[0] = enabled;
  75. $[1] = id;
  76. $[2] = setAppState;
  77. $[3] = t1;
  78. $[4] = t2;
  79. } else {
  80. t1 = $[3];
  81. t2 = $[4];
  82. }
  83. useEffect(t1, t2);
  84. let t3;
  85. let t4;
  86. if ($[5] !== enabled) {
  87. t3 = () => {
  88. if (!enabled) {
  89. return;
  90. }
  91. return _temp;
  92. };
  93. t4 = [enabled];
  94. $[5] = enabled;
  95. $[6] = t3;
  96. $[7] = t4;
  97. } else {
  98. t3 = $[6];
  99. t4 = $[7];
  100. }
  101. useLayoutEffect(t3, t4);
  102. }
  103. /**
  104. * Hook to check if any overlay is currently active.
  105. * This is reactive - the component will re-render when the overlay state changes.
  106. *
  107. * @returns true if any overlay is currently active
  108. *
  109. * @example
  110. * function CancelRequestHandler() {
  111. * const isOverlayActive = useIsOverlayActive()
  112. * const isActive = !isOverlayActive && canCancelRunningTask
  113. * useKeybinding('chat:cancel', handleCancel, { isActive })
  114. * }
  115. */
  116. function _temp() {
  117. return instances.get(process.stdout)?.invalidatePrevFrame();
  118. }
  119. export function useIsOverlayActive() {
  120. return useAppState(_temp2);
  121. }
  122. /**
  123. * Hook to check if any modal overlay is currently active.
  124. * Modal overlays are overlays that should capture all input (like Select dialogs).
  125. * Non-modal overlays (like autocomplete) don't disable TextInput focus.
  126. *
  127. * @returns true if any modal overlay is currently active
  128. *
  129. * @example
  130. * // Use for TextInput focus - allows typing during autocomplete
  131. * focus: !isSearchingHistory && !isModalOverlayActive
  132. */
  133. function _temp2(s) {
  134. return s.activeOverlays.size > 0;
  135. }
  136. export function useIsModalOverlayActive() {
  137. return useAppState(_temp3);
  138. }
  139. function _temp3(s) {
  140. for (const id of s.activeOverlays) {
  141. if (!NON_MODAL_OVERLAYS.has(id)) {
  142. return true;
  143. }
  144. }
  145. return false;
  146. }