@robota-sdk/agent-ui-terminal 3.0.0-beta.81

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.
Files changed (328) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +27 -0
  3. package/dist/node/index.cjs +32 -0
  4. package/dist/node/index.d.cts +1066 -0
  5. package/dist/node/index.d.cts.map +1 -0
  6. package/dist/node/index.d.ts +1066 -0
  7. package/dist/node/index.d.ts.map +1 -0
  8. package/dist/node/index.js +33 -0
  9. package/dist/node/index.js.map +1 -0
  10. package/package.json +93 -0
  11. package/src/App.tsx +146 -0
  12. package/src/AppPresentation.tsx +190 -0
  13. package/src/AppView.tsx +26 -0
  14. package/src/BackgroundTaskPanel.tsx +73 -0
  15. package/src/CjkTextInput.tsx +336 -0
  16. package/src/ConfirmPrompt.tsx +140 -0
  17. package/src/ContextWarningBanner.tsx +45 -0
  18. package/src/ExecutionWorkspaceDetailPane.tsx +85 -0
  19. package/src/ExecutionWorkspaceSwitcher.tsx +213 -0
  20. package/src/HistorySearchOverlay.tsx +150 -0
  21. package/src/InputArea.tsx +338 -0
  22. package/src/ListPicker.tsx +158 -0
  23. package/src/MenuSelect.tsx +159 -0
  24. package/src/MessageList.tsx +308 -0
  25. package/src/MultiSelectList.tsx +294 -0
  26. package/src/PendingActionPrompt.tsx +121 -0
  27. package/src/PermissionPrompt.tsx +186 -0
  28. package/src/PluginTUI.tsx +258 -0
  29. package/src/RoleLabel.tsx +72 -0
  30. package/src/SafeText.tsx +66 -0
  31. package/src/SessionEventNotices.tsx +27 -0
  32. package/src/SessionPicker.tsx +72 -0
  33. package/src/SessionStatusBar.tsx +76 -0
  34. package/src/SlashAutocomplete.tsx +154 -0
  35. package/src/StatusBar.tsx +284 -0
  36. package/src/StreamingIndicator.tsx +145 -0
  37. package/src/SupervisedSessionView.tsx +455 -0
  38. package/src/TextPrompt.tsx +112 -0
  39. package/src/ThemePicker.tsx +366 -0
  40. package/src/ToolCommandOutput.tsx +48 -0
  41. package/src/ToolDiffBlock.tsx +44 -0
  42. package/src/TransportTUI.tsx +166 -0
  43. package/src/TuiInteractionChannel.ts +567 -0
  44. package/src/UpdateNotice.tsx +18 -0
  45. package/src/UsageSummaryEntry.tsx +49 -0
  46. package/src/WaveText.tsx +70 -0
  47. package/src/__tests__/ListPicker.test.tsx +188 -0
  48. package/src/__tests__/MenuSelect.test.tsx +127 -0
  49. package/src/__tests__/PendingActionPrompt.test.tsx +193 -0
  50. package/src/__tests__/PluginTUI.test.tsx +167 -0
  51. package/src/__tests__/SlashAutocomplete.test.tsx +162 -0
  52. package/src/__tests__/SupervisedSessionView-render.test.tsx +24 -0
  53. package/src/__tests__/SupervisedSessionView.test.tsx +629 -0
  54. package/src/__tests__/TextPrompt.test.tsx +112 -0
  55. package/src/__tests__/TransportTUI.test.tsx +124 -0
  56. package/src/__tests__/TuiInteractionChannel.askUser.test.ts +124 -0
  57. package/src/__tests__/TuiInteractionChannel.display-contract.test.ts +284 -0
  58. package/src/__tests__/TuiInteractionChannel.lifecycle.test.ts +642 -0
  59. package/src/__tests__/UpdateNotice.test.tsx +15 -0
  60. package/src/__tests__/abort-after-permission.test.tsx +160 -0
  61. package/src/__tests__/abort-streaming-e2e.test.tsx +183 -0
  62. package/src/__tests__/attention-bell-and-marks.test.ts +144 -0
  63. package/src/__tests__/autocomplete-editing-keys.test.tsx +127 -0
  64. package/src/__tests__/background-task-panel-countdown.test.tsx +75 -0
  65. package/src/__tests__/background-task-panel.test.tsx +98 -0
  66. package/src/__tests__/background-task-row-format.test.ts +152 -0
  67. package/src/__tests__/channel-factory-integration.test.ts +150 -0
  68. package/src/__tests__/cjk-defer-submit.test.tsx +67 -0
  69. package/src/__tests__/cjk-fallback-render.test.tsx +78 -0
  70. package/src/__tests__/cjk-text-input-flow.test.ts +186 -0
  71. package/src/__tests__/cjk-text-input.test.ts +191 -0
  72. package/src/__tests__/command-handoff-pty-e2e.test.ts +107 -0
  73. package/src/__tests__/command-output-summary.test.ts +95 -0
  74. package/src/__tests__/command-result-handler.test.ts +171 -0
  75. package/src/__tests__/compact-event-bridge.test.ts +20 -0
  76. package/src/__tests__/confirm-permission-flow.test.ts +149 -0
  77. package/src/__tests__/confirm-prompt.test.tsx +103 -0
  78. package/src/__tests__/contextual-keybindings-input.test.tsx +158 -0
  79. package/src/__tests__/default-tui-cli-adapter-host-settings.test.ts +45 -0
  80. package/src/__tests__/effort-reaches-the-session.test.ts +41 -0
  81. package/src/__tests__/execution-workspace-switcher.test.tsx +147 -0
  82. package/src/__tests__/execution-workspace-view-model.test.ts +94 -0
  83. package/src/__tests__/external-prompt-notice.test.tsx +100 -0
  84. package/src/__tests__/fixtures/command-handoff-driver.tsx +111 -0
  85. package/src/__tests__/fixtures/fake-editor.sh +7 -0
  86. package/src/__tests__/fixtures/terminal-handoff-driver.tsx +90 -0
  87. package/src/__tests__/fork-attach.test.tsx +235 -0
  88. package/src/__tests__/helpers/terminal-profiles.ts +152 -0
  89. package/src/__tests__/helpers/vt-cursor-interpreter.ts +175 -0
  90. package/src/__tests__/history-clear-broadcast.test.ts +66 -0
  91. package/src/__tests__/history-search-overlay.test.tsx +329 -0
  92. package/src/__tests__/humanize-tool-name.test.ts +30 -0
  93. package/src/__tests__/input-area-bottom-border.test.tsx +45 -0
  94. package/src/__tests__/input-area-flow.test.ts +199 -0
  95. package/src/__tests__/input-area-focus-handoff.test.tsx +77 -0
  96. package/src/__tests__/interaction-channel-charter.test.ts +13 -0
  97. package/src/__tests__/key-hint-consistency.test.tsx +249 -0
  98. package/src/__tests__/key-hint-footer.test.tsx +69 -0
  99. package/src/__tests__/live-trace-reaches-session.test.ts +14 -0
  100. package/src/__tests__/message-list-rendering.test.tsx +473 -0
  101. package/src/__tests__/org-policy-reaches-the-channel.test.ts +77 -0
  102. package/src/__tests__/output-style-reaches-session.test.ts +35 -0
  103. package/src/__tests__/palette-consistency.test.ts +330 -0
  104. package/src/__tests__/permission-prompt-availability.test.tsx +23 -0
  105. package/src/__tests__/permission-prompt-peer.test.tsx +34 -0
  106. package/src/__tests__/product-display-name.test.tsx +79 -0
  107. package/src/__tests__/prompt-queue.test.tsx +260 -0
  108. package/src/__tests__/provider-definitions-reach-the-session.test.ts +75 -0
  109. package/src/__tests__/pty/ask-user-question.ptytest.ts +72 -0
  110. package/src/__tests__/pty/background-work-switcher.ptytest.ts +53 -0
  111. package/src/__tests__/pty/behavior-2437-git.ptytest.ts +342 -0
  112. package/src/__tests__/pty/fixtures/ask-user-question.jsonl +3 -0
  113. package/src/__tests__/pty/fixtures/replay-conversation.jsonl +4 -0
  114. package/src/__tests__/pty/fixtures/screen-005-ask-shapes.jsonl +5 -0
  115. package/src/__tests__/pty/fixtures/screen-005-permission.jsonl +3 -0
  116. package/src/__tests__/pty/fixtures/screen-006-colors.jsonl +3 -0
  117. package/src/__tests__/pty/fixtures/screen-2002-themes.jsonl +5 -0
  118. package/src/__tests__/pty/flag-tui.ptytest.ts +56 -0
  119. package/src/__tests__/pty/flow-2006-deep-link.ptytest.ts +291 -0
  120. package/src/__tests__/pty/ime-cursor-tmux.ptytest.ts +165 -0
  121. package/src/__tests__/pty/ime-cursor.ptytest.ts +186 -0
  122. package/src/__tests__/pty/isolated-home.ts +64 -0
  123. package/src/__tests__/pty/org-policy.ptytest.ts +71 -0
  124. package/src/__tests__/pty/provider-setup.ptytest.ts +61 -0
  125. package/src/__tests__/pty/pty-driver.ts +153 -0
  126. package/src/__tests__/pty/replay-conversation.ptytest.ts +68 -0
  127. package/src/__tests__/pty/screen-005-prompt-footers.ptytest.ts +134 -0
  128. package/src/__tests__/pty/screen-006-no-color.ptytest.ts +138 -0
  129. package/src/__tests__/pty/screen-010-scrollback.ptytest.ts +123 -0
  130. package/src/__tests__/pty/screen-1993-scrollback.ptytest.ts +124 -0
  131. package/src/__tests__/pty/screen-2002-themes.ptytest.ts +292 -0
  132. package/src/__tests__/pty/screen-2670-prepark.ptytest.ts +228 -0
  133. package/src/__tests__/pty/screen-reader-mode.ptytest.ts +228 -0
  134. package/src/__tests__/pty/settings-screen.ptytest.ts +49 -0
  135. package/src/__tests__/pty/spawn-pty.test.ts +120 -0
  136. package/src/__tests__/pty/spawn-pty.ts +229 -0
  137. package/src/__tests__/pty/terminal-handoff.ptytest.ts +79 -0
  138. package/src/__tests__/pty/tui-pty.ptytest.ts +65 -0
  139. package/src/__tests__/pty-driver-dispose.test.ts +43 -0
  140. package/src/__tests__/real-cursor-positioning.test.tsx +263 -0
  141. package/src/__tests__/rename-broadcast-persistence.test.tsx +140 -0
  142. package/src/__tests__/render-channel-options.test.ts +201 -0
  143. package/src/__tests__/render-lifecycle.test.ts +36 -0
  144. package/src/__tests__/render-markdown.test.ts +164 -0
  145. package/src/__tests__/rendered-markdown-styling.test.tsx +98 -0
  146. package/src/__tests__/safe-text-boundary.test.tsx +75 -0
  147. package/src/__tests__/screen-reader-input.test.tsx +106 -0
  148. package/src/__tests__/screen-reader-labels.test.ts +91 -0
  149. package/src/__tests__/screen-reader-menus.test.tsx +200 -0
  150. package/src/__tests__/screen-reader-pacing.test.ts +230 -0
  151. package/src/__tests__/screen-reader-prepark-echo.test.tsx +200 -0
  152. package/src/__tests__/screen-reader-render-options.test.ts +262 -0
  153. package/src/__tests__/screen-reader-stdout.test.ts +322 -0
  154. package/src/__tests__/screen-reader-turn-marks-port.test.tsx +85 -0
  155. package/src/__tests__/screen-reader-turn-signals.test.tsx +125 -0
  156. package/src/__tests__/sec-019-terminal-sanitizer.test.ts +276 -0
  157. package/src/__tests__/sec-019-tool-label-render.test.tsx +207 -0
  158. package/src/__tests__/selection-flow.test.ts +61 -0
  159. package/src/__tests__/session-event-notices.test.tsx +31 -0
  160. package/src/__tests__/session-init-poller.test.ts +102 -0
  161. package/src/__tests__/session-switch-channel.test.tsx +470 -0
  162. package/src/__tests__/shutdown-signal.test.ts +34 -0
  163. package/src/__tests__/status-activity.test.ts +71 -0
  164. package/src/__tests__/status-bar.test.tsx +282 -0
  165. package/src/__tests__/status-glyph.test.ts +30 -0
  166. package/src/__tests__/streaming-indicator.test.tsx +177 -0
  167. package/src/__tests__/terminal-capabilities.test.ts +153 -0
  168. package/src/__tests__/terminal-handoff-controller.test.ts +173 -0
  169. package/src/__tests__/terminal-handoff-pty-e2e.test.ts +77 -0
  170. package/src/__tests__/text-prompt-flow.test.ts +77 -0
  171. package/src/__tests__/theme-picker.test.tsx +345 -0
  172. package/src/__tests__/transport-boundary.test.ts +11 -0
  173. package/src/__tests__/tui-app-channel-port.test.ts +31 -0
  174. package/src/__tests__/tui-channel-init-failure.test.ts +62 -0
  175. package/src/__tests__/tui-channel-lifecycle-coordinator.test.ts +258 -0
  176. package/src/__tests__/tui-interaction-queues.test.ts +139 -0
  177. package/src/__tests__/tui-preset-capabilities.test.ts +33 -0
  178. package/src/__tests__/tui-state-manager.error-stall.test.ts +169 -0
  179. package/src/__tests__/tui-state-manager.test.ts +451 -0
  180. package/src/__tests__/ui-intent-rendering.test.tsx +291 -0
  181. package/src/__tests__/wave-text.test.tsx +133 -0
  182. package/src/app-banner.tsx +42 -0
  183. package/src/app-static-items.ts +35 -0
  184. package/src/app-view-model.ts +136 -0
  185. package/src/attention/__tests__/attention-coordinator.test.ts +166 -0
  186. package/src/attention/__tests__/attention-tracker.test.ts +103 -0
  187. package/src/attention/__tests__/countdown.test.ts +15 -0
  188. package/src/attention/__tests__/focus-input-filter.test.ts +92 -0
  189. package/src/attention/__tests__/interval-recap.test.ts +49 -0
  190. package/src/attention/attention-coordinator.ts +71 -0
  191. package/src/attention/attention-tracker.ts +124 -0
  192. package/src/attention/countdown.ts +24 -0
  193. package/src/attention/focus-input-filter.ts +131 -0
  194. package/src/attention/interval-recap.ts +148 -0
  195. package/src/attention/time-units.ts +6 -0
  196. package/src/attention-bell.ts +79 -0
  197. package/src/attributed-user-echo.ts +30 -0
  198. package/src/background-task-row-format.ts +99 -0
  199. package/src/command-interaction.ts +8 -0
  200. package/src/command-output-summary.ts +129 -0
  201. package/src/create-default-tui-cli-adapter.ts +45 -0
  202. package/src/execution-workspace-switcher-selection.ts +118 -0
  203. package/src/execution-workspace-view-model.ts +139 -0
  204. package/src/external-prompt-notice.tsx +38 -0
  205. package/src/flows/__tests__/background-focus-flow.test.ts +48 -0
  206. package/src/flows/__tests__/defer-submit.test.ts +87 -0
  207. package/src/flows/__tests__/real-cursor-flow.test.ts +124 -0
  208. package/src/flows/background-focus-flow.ts +44 -0
  209. package/src/flows/cjk-cursor-motion.ts +75 -0
  210. package/src/flows/cjk-text-deletion.ts +53 -0
  211. package/src/flows/cjk-text-input-flow.ts +248 -0
  212. package/src/flows/cjk-text-input-types.ts +24 -0
  213. package/src/flows/confirm-prompt-flow.ts +90 -0
  214. package/src/flows/defer-submit.ts +67 -0
  215. package/src/flows/fork-attach-flow.ts +62 -0
  216. package/src/flows/input-area-flow.ts +188 -0
  217. package/src/flows/permission-prompt-flow.ts +108 -0
  218. package/src/flows/real-cursor-flow.ts +83 -0
  219. package/src/flows/selection-flow.ts +205 -0
  220. package/src/flows/session-init-poller.ts +76 -0
  221. package/src/flows/text-prompt-flow.ts +93 -0
  222. package/src/history-search/__tests__/history-search-flow.test.ts +86 -0
  223. package/src/history-search/history-search-flow.ts +94 -0
  224. package/src/history-search/useHistorySearch.ts +336 -0
  225. package/src/hooks/__tests__/use-app-loop-escape.test.tsx +74 -0
  226. package/src/hooks/__tests__/use-plugin-callbacks.test.ts +18 -0
  227. package/src/hooks/__tests__/use-tui-channel-stability.test.tsx +100 -0
  228. package/src/hooks/__tests__/useAppThemeState.test.tsx +130 -0
  229. package/src/hooks/command-result-handler.ts +44 -0
  230. package/src/hooks/side-effects-types.ts +45 -0
  231. package/src/hooks/useAppController.ts +267 -0
  232. package/src/hooks/useAppInputBindings.ts +145 -0
  233. package/src/hooks/useAppInteractionState.ts +81 -0
  234. package/src/hooks/useAppLifecycleState.ts +81 -0
  235. package/src/hooks/useAppOverlays.ts +74 -0
  236. package/src/hooks/useAppScreenState.ts +81 -0
  237. package/src/hooks/useAppSubmissionState.ts +45 -0
  238. package/src/hooks/useAppThemeState.ts +157 -0
  239. package/src/hooks/useAppWorkspaceState.ts +149 -0
  240. package/src/hooks/useAppearanceSettings.ts +30 -0
  241. package/src/hooks/useAutocomplete.ts +101 -0
  242. package/src/hooks/useBackgroundPanel.ts +58 -0
  243. package/src/hooks/useCountdownTick.ts +24 -0
  244. package/src/hooks/useExecutionDetailPage.ts +69 -0
  245. package/src/hooks/useForkAttach.ts +60 -0
  246. package/src/hooks/useInputAreaHistorySearch.ts +72 -0
  247. package/src/hooks/useInputAreaKeys.ts +114 -0
  248. package/src/hooks/useNumberedSelection.ts +76 -0
  249. package/src/hooks/usePluginCallbacks.ts +38 -0
  250. package/src/hooks/usePluginScreenData.ts +85 -0
  251. package/src/hooks/useRealCursorPosition.ts +125 -0
  252. package/src/hooks/useRenderMarkdown.ts +38 -0
  253. package/src/hooks/useScreenReaderTurnSignals.ts +113 -0
  254. package/src/hooks/useSideEffects.ts +144 -0
  255. package/src/hooks/useStatusLineSettings.ts +50 -0
  256. package/src/hooks/useTerminalHandoffSuspension.ts +30 -0
  257. package/src/hooks/useTuiChannel.ts +129 -0
  258. package/src/humanize-tool-name.ts +51 -0
  259. package/src/index.ts +56 -0
  260. package/src/input-area-rules.tsx +71 -0
  261. package/src/json-value.ts +18 -0
  262. package/src/key-hint-footer.tsx +54 -0
  263. package/src/keybindings/__tests__/keybinding-registry.test.ts +228 -0
  264. package/src/keybindings/__tests__/node-keybindings-source.test.ts +111 -0
  265. package/src/keybindings/keybinding-catalogue.ts +171 -0
  266. package/src/keybindings/keybinding-registry.ts +303 -0
  267. package/src/keybindings/keybinding-syntax.ts +155 -0
  268. package/src/keybindings/keybindings-context.tsx +124 -0
  269. package/src/keybindings/node-keybindings-source.ts +189 -0
  270. package/src/numbered-list.tsx +113 -0
  271. package/src/plugin-tui-handlers.ts +163 -0
  272. package/src/product-display-name-context.tsx +30 -0
  273. package/src/render-markdown.ts +209 -0
  274. package/src/render.tsx +544 -0
  275. package/src/sanitize-terminal-text.ts +171 -0
  276. package/src/screen-reader-announcement.ts +52 -0
  277. package/src/screen-reader-context.tsx +32 -0
  278. package/src/screen-reader-labels.ts +64 -0
  279. package/src/screen-reader-pacing-context.tsx +47 -0
  280. package/src/screen-reader-pacing.ts +178 -0
  281. package/src/screen-reader-stdout.ts +328 -0
  282. package/src/shutdown-signal.ts +34 -0
  283. package/src/status-activity.ts +70 -0
  284. package/src/status-glyph.ts +78 -0
  285. package/src/terminal-capabilities-context.tsx +27 -0
  286. package/src/terminal-capabilities.ts +77 -0
  287. package/src/terminal-focus-reporting.ts +50 -0
  288. package/src/terminal-handoff-controller.ts +98 -0
  289. package/src/terminal-marks.ts +63 -0
  290. package/src/theme/__tests__/built-in-themes.test.ts +117 -0
  291. package/src/theme/__tests__/color-vision.test.ts +82 -0
  292. package/src/theme/__tests__/theme-context.test.tsx +146 -0
  293. package/src/theme/__tests__/theme-document.test.ts +222 -0
  294. package/src/theme/__tests__/theme-registry.test.ts +138 -0
  295. package/src/theme/__tests__/theme-styles.test.ts +233 -0
  296. package/src/theme/built-in-themes.ts +342 -0
  297. package/src/theme/color-vision.ts +171 -0
  298. package/src/theme/index.ts +45 -0
  299. package/src/theme/theme-context.tsx +81 -0
  300. package/src/theme/theme-contracts.ts +139 -0
  301. package/src/theme/theme-document.ts +278 -0
  302. package/src/theme/theme-registry.ts +127 -0
  303. package/src/theme/theme-styles.ts +177 -0
  304. package/src/tool-summary-status.ts +59 -0
  305. package/src/tui-app-channel-port.ts +83 -0
  306. package/src/tui-channel-lifecycle-coordinator.ts +149 -0
  307. package/src/tui-channel-options.ts +172 -0
  308. package/src/tui-cli-adapter-context.tsx +13 -0
  309. package/src/tui-cli-adapter.ts +24 -0
  310. package/src/tui-interaction-queues.ts +163 -0
  311. package/src/tui-session-binding.ts +52 -0
  312. package/src/tui-session-event-projector.ts +140 -0
  313. package/src/tui-session-events.ts +82 -0
  314. package/src/tui-session-notice-store.ts +43 -0
  315. package/src/tui-session-options.ts +118 -0
  316. package/src/tui-state-manager.ts +327 -0
  317. package/src/types.ts +19 -0
  318. package/src/use-terminal-title.ts +29 -0
  319. package/src/utils/__tests__/edit-diff.test.ts +426 -0
  320. package/src/utils/__tests__/paste-detection.test.ts +116 -0
  321. package/src/utils/__tests__/paste-labels.test.ts +46 -0
  322. package/src/utils/__tests__/tool-call-extractor.test.ts +227 -0
  323. package/src/utils/__tests__/tool-diff-summary.test.ts +104 -0
  324. package/src/utils/edit-diff.ts +151 -0
  325. package/src/utils/input-top-border.ts +19 -0
  326. package/src/utils/paste-labels.ts +9 -0
  327. package/src/utils/tool-call-extractor.ts +92 -0
  328. package/src/utils/tool-diff-summary.ts +75 -0
@@ -0,0 +1,149 @@
1
+ import { useCallback, useEffect, useMemo, useState } from 'react';
2
+
3
+ import {
4
+ countActiveBackgroundWorkspaceEntries,
5
+ getDefaultBackgroundWorkspaceEntries,
6
+ } from '../execution-workspace-view-model.js';
7
+ import { useBackgroundPanel } from './useBackgroundPanel.js';
8
+ import { resolveBackgroundFocusKey } from '../flows/background-focus-flow.js';
9
+ import { useKeybindingActions } from '../keybindings/keybindings-context.js';
10
+
11
+ import type { IAppBackgroundViewModel } from '../app-view-model.js';
12
+ import type { IHistoryEntry } from '@robota-sdk/agent-core';
13
+ import type {
14
+ IExecutionDetailPage,
15
+ IExecutionWorkspaceSnapshot,
16
+ } from '@robota-sdk/agent-interface-execution';
17
+ import type { IInteractiveSessionStore } from '@robota-sdk/agent-interface-session';
18
+
19
+ export interface IAppWorkspaceState {
20
+ readonly background: IAppBackgroundViewModel;
21
+ readonly activeBackgroundTaskCount: number;
22
+ readonly activeAgentLabel: string | undefined;
23
+ readonly mainThreadEntryId: string | undefined;
24
+ readonly isSelectedEntryInteractive: boolean;
25
+ readonly isBackgroundListFocused: boolean;
26
+ readonly openSwitcher: () => void;
27
+ readonly toggleSwitcher: () => void;
28
+ readonly focusBackgroundList: () => void;
29
+ }
30
+
31
+ interface IOptions {
32
+ readonly snapshot: IExecutionWorkspaceSnapshot | null;
33
+ readonly selectedEntryId: string | undefined;
34
+ readonly select: (entryId: string) => void;
35
+ readonly read: (entryId: string) => Promise<IExecutionDetailPage>;
36
+ readonly sessionStore: IInteractiveSessionStore | undefined;
37
+ readonly onSessionSwitch: (sessionId: string) => Promise<void>;
38
+ readonly addEntry: (entry: IHistoryEntry) => void;
39
+ readonly navigationEnabled: boolean;
40
+ readonly switcherVisible: boolean;
41
+ readonly setSwitcherVisible: (visible: boolean | ((current: boolean) => boolean)) => void;
42
+ }
43
+
44
+ interface ISwitcherActions {
45
+ readonly open: () => void;
46
+ readonly toggle: () => void;
47
+ readonly close: () => void;
48
+ }
49
+
50
+ function useBackgroundListKeys(
51
+ focusedIndex: number | null,
52
+ entryIds: readonly string[],
53
+ setFocusedIndex: (index: number | null) => void,
54
+ select: (entryId: string) => void,
55
+ enabled: boolean,
56
+ ): void {
57
+ useKeybindingActions(
58
+ 'background-list',
59
+ (actions) => {
60
+ if (focusedIndex === null) return;
61
+ const action = resolveBackgroundFocusKey(focusedIndex, entryIds.length, {
62
+ upArrow: actions.includes('previous'),
63
+ downArrow: actions.includes('next'),
64
+ return: actions.includes('open'),
65
+ escape: actions.includes('close'),
66
+ });
67
+ if (action.type === 'move') setFocusedIndex(action.index);
68
+ if (action.type === 'open') {
69
+ const entryId = entryIds[action.index];
70
+ if (entryId) select(entryId);
71
+ setFocusedIndex(null);
72
+ }
73
+ if (action.type === 'exit') setFocusedIndex(null);
74
+ },
75
+ { isActive: focusedIndex !== null && enabled },
76
+ );
77
+ }
78
+
79
+ function useBackgroundFocus(
80
+ entryIds: readonly string[],
81
+ select: (entryId: string) => void,
82
+ enabled: boolean,
83
+ ): { focusedIndex: number | null; focusList: () => void } {
84
+ const [focusedIndex, setFocusedIndex] = useState<number | null>(null);
85
+ useEffect(() => {
86
+ setFocusedIndex((index) => {
87
+ if (index === null || entryIds.length === 0) return null;
88
+ return Math.min(index, entryIds.length - 1);
89
+ });
90
+ }, [entryIds.length]);
91
+ useBackgroundListKeys(focusedIndex, entryIds, setFocusedIndex, select, enabled);
92
+ const focusList = useCallback(() => {
93
+ if (entryIds.length > 0) setFocusedIndex(0);
94
+ }, [entryIds.length]);
95
+ return { focusedIndex, focusList };
96
+ }
97
+
98
+ function useSwitcherActions(setVisible: IOptions['setSwitcherVisible']): ISwitcherActions {
99
+ const open = useCallback(() => setVisible(true), [setVisible]);
100
+ const toggle = useCallback(() => setVisible((visible) => !visible), [setVisible]);
101
+ const close = useCallback(() => setVisible(false), [setVisible]);
102
+ return { open, toggle, close };
103
+ }
104
+
105
+ export function useAppWorkspaceState(options: IOptions): IAppWorkspaceState {
106
+ const entries = useMemo(
107
+ () => getDefaultBackgroundWorkspaceEntries(options.snapshot),
108
+ [options.snapshot],
109
+ );
110
+ const { focusedIndex, focusList } = useBackgroundFocus(
111
+ entries.map((entry) => entry.id),
112
+ options.select,
113
+ options.navigationEnabled,
114
+ );
115
+
116
+ const panel = useBackgroundPanel({
117
+ selectedEntryId: options.selectedEntryId,
118
+ snapshot: options.snapshot,
119
+ read: options.read,
120
+ sessionStore: options.sessionStore,
121
+ onSessionSwitch: options.onSessionSwitch,
122
+ addEntry: options.addEntry,
123
+ });
124
+ const switcher = useSwitcherActions(options.setSwitcherVisible);
125
+ const selected = panel.entry;
126
+ return {
127
+ background: {
128
+ entries,
129
+ focusedIndex,
130
+ selectedEntry: selected,
131
+ detail: panel.detail,
132
+ switcherVisible: options.switcherVisible,
133
+ snapshot: options.snapshot,
134
+ selectedEntryId: options.selectedEntryId,
135
+ select: options.select,
136
+ closeSwitcher: switcher.close,
137
+ attachToFork: panel.attachToFork,
138
+ },
139
+ activeBackgroundTaskCount: countActiveBackgroundWorkspaceEntries(options.snapshot),
140
+ activeAgentLabel: selected?.kind === 'main_thread' ? undefined : selected?.title,
141
+ mainThreadEntryId: options.snapshot?.entries.find((entry) => entry.kind === 'main_thread')?.id,
142
+ isSelectedEntryInteractive:
143
+ !selected || selected.kind === 'main_thread' || selected.controls.includes('send'),
144
+ isBackgroundListFocused: focusedIndex !== null,
145
+ openSwitcher: switcher.open,
146
+ toggleSwitcher: switcher.toggle,
147
+ focusBackgroundList: focusList,
148
+ };
149
+ }
@@ -0,0 +1,30 @@
1
+ import { readAppearanceSettings } from '@robota-sdk/agent-framework';
2
+ import { useCallback, useState } from 'react';
3
+
4
+ import { useTuiCliAdapter } from '../tui-cli-adapter-context.js';
5
+
6
+ import type { ITuiCliAdapter } from '../tui-cli-adapter.js';
7
+ import type { IAppearanceSettings } from '@robota-sdk/agent-interface-command';
8
+
9
+ function readFromAdapter(cliAdapter: ITuiCliAdapter): IAppearanceSettings {
10
+ return readAppearanceSettings(cliAdapter.readSettings(cliAdapter.getUserSettingsPath()));
11
+ }
12
+
13
+ /**
14
+ * SCREEN-2002 — the persisted appearance, plus a from-disk `refresh()`.
15
+ *
16
+ * Refresh-on-result, the shape `useStatusLineSettings` already uses: the HOST applies
17
+ * `appearance-settings-patch` through the settings adapter, so the renderer learns about a change by
18
+ * RE-READING the document when a command result arrives. The TUI never writes these keys itself,
19
+ * which is what keeps one writer for one file.
20
+ */
21
+ export function useAppearanceSettings(): [IAppearanceSettings, () => void] {
22
+ const cliAdapter = useTuiCliAdapter();
23
+ const [appearance, setAppearance] = useState<IAppearanceSettings>(() =>
24
+ readFromAdapter(cliAdapter),
25
+ );
26
+ const refresh = useCallback((): void => {
27
+ setAppearance(readFromAdapter(cliAdapter));
28
+ }, [cliAdapter]);
29
+ return [appearance, refresh];
30
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Slash command autocomplete hook.
3
+ * Extracted from InputArea.tsx for single-responsibility.
4
+ */
5
+
6
+ import React, { useState, useMemo, useEffect } from 'react';
7
+
8
+ import type { ITuiCommandQueryPort } from '../tui-app-channel-port.js';
9
+ import type { ICommand } from '@robota-sdk/agent-interface-command';
10
+
11
+ /** Parse input to determine autocomplete state */
12
+ function parseSlashInput(value: string): {
13
+ isSlash: boolean;
14
+ parentCommand: string;
15
+ filter: string;
16
+ } {
17
+ if (!value.startsWith('/')) return { isSlash: false, parentCommand: '', filter: '' };
18
+ const afterSlash = value.slice(1);
19
+ const spaceIndex = afterSlash.indexOf(' ');
20
+ if (spaceIndex === -1) return { isSlash: true, parentCommand: '', filter: afterSlash };
21
+ const parent = afterSlash.slice(0, spaceIndex);
22
+ const rest = afterSlash.slice(spaceIndex + 1);
23
+ return { isSlash: true, parentCommand: parent, filter: rest };
24
+ }
25
+
26
+ /** Hook: manage autocomplete state */
27
+ export function useAutocomplete(
28
+ value: string,
29
+ commandQueryPort: ITuiCommandQueryPort | undefined,
30
+ ): {
31
+ showPopup: boolean;
32
+ filteredCommands: ICommand[];
33
+ selectedIndex: number;
34
+ setSelectedIndex: React.Dispatch<React.SetStateAction<number>>;
35
+ isSubcommandMode: boolean;
36
+ setShowPopup: React.Dispatch<React.SetStateAction<boolean>>;
37
+ } {
38
+ const [selectedIndex, setSelectedIndex] = useState(0);
39
+ const [dismissed, setDismissed] = useState(false);
40
+
41
+ // Reset dismissed when input changes
42
+ const prevValueRef = React.useRef(value);
43
+ if (prevValueRef.current !== value) {
44
+ prevValueRef.current = value;
45
+ if (dismissed) setDismissed(false);
46
+ }
47
+
48
+ const parsed = parseSlashInput(value);
49
+ const isSubcommandMode = parsed.isSlash && parsed.parentCommand.length > 0;
50
+
51
+ const previousParentCommandRef = React.useRef(parsed.parentCommand);
52
+ useEffect(() => {
53
+ if (previousParentCommandRef.current === parsed.parentCommand) return;
54
+ previousParentCommandRef.current = parsed.parentCommand;
55
+ setSelectedIndex(0);
56
+ }, [parsed.parentCommand]);
57
+
58
+ const filteredCommands = useMemo(() => {
59
+ if (!commandQueryPort || !parsed.isSlash || dismissed) return [];
60
+ if (isSubcommandMode) {
61
+ const subs = commandQueryPort.getSubcommands(parsed.parentCommand);
62
+ if (subs.length === 0) return [];
63
+ if (!parsed.filter) return subs;
64
+ const lower = parsed.filter.toLowerCase();
65
+ return subs.filter((c) => c.name.toLowerCase().startsWith(lower));
66
+ }
67
+ return commandQueryPort.getCommands(parsed.filter);
68
+ }, [
69
+ commandQueryPort,
70
+ parsed.isSlash,
71
+ parsed.parentCommand,
72
+ parsed.filter,
73
+ dismissed,
74
+ isSubcommandMode,
75
+ ]);
76
+
77
+ const showPopup = parsed.isSlash && filteredCommands.length > 0 && !dismissed;
78
+
79
+ // Clamp selectedIndex
80
+ if (selectedIndex >= filteredCommands.length && filteredCommands.length > 0) {
81
+ setSelectedIndex(filteredCommands.length - 1);
82
+ }
83
+
84
+ return {
85
+ showPopup,
86
+ filteredCommands,
87
+ selectedIndex,
88
+ setSelectedIndex,
89
+ isSubcommandMode,
90
+ setShowPopup: (val) => {
91
+ if (typeof val === 'function') {
92
+ setDismissed((prev) => {
93
+ const nextVal = (val as (prev: boolean) => boolean)(!prev);
94
+ return !nextVal;
95
+ });
96
+ } else {
97
+ setDismissed(!val);
98
+ }
99
+ },
100
+ };
101
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The background panel's two data concerns as one call: what the selected entry's detail page says,
3
+ * and what happens when the operator attaches to a forked one.
4
+ *
5
+ * Both belong to the same surface and both are read by the same component, so `App` asks for them
6
+ * once rather than wiring two hooks whose inputs it has to keep in step. Split out because CLI-2004
7
+ * had already brought `App.tsx` to its size budget by extracting the detail read, and CLI-1994 adds
8
+ * the attach handler beside it — growing the file was the alternative the size rule forbids.
9
+ */
10
+
11
+ import { useMemo } from 'react';
12
+
13
+ import { useExecutionDetailPage } from './useExecutionDetailPage.js';
14
+ import { useForkAttach } from './useForkAttach.js';
15
+
16
+ import type { IExecutionDetailState } from './useExecutionDetailPage.js';
17
+ import type { IHistoryEntry } from '@robota-sdk/agent-core';
18
+ import type {
19
+ IExecutionDetailPage,
20
+ IExecutionWorkspaceEntry,
21
+ IExecutionWorkspaceSnapshot,
22
+ } from '@robota-sdk/agent-interface-execution';
23
+ import type { IInteractiveSessionStore } from '@robota-sdk/agent-interface-session';
24
+
25
+ export interface IUseBackgroundPanelOptions {
26
+ /** Which entry the switcher has selected; resolved against the snapshot below. */
27
+ readonly selectedEntryId: string | undefined;
28
+ /** The panel's data, and the re-read trigger — the detail follows a running task's output. */
29
+ readonly snapshot: IExecutionWorkspaceSnapshot | null;
30
+ readonly read: (entryId: string) => Promise<IExecutionDetailPage>;
31
+ /** Absent ⇒ this surface cannot open another session's record, and attach says so. */
32
+ readonly sessionStore: IInteractiveSessionStore | undefined;
33
+ readonly onSessionSwitch: (sessionId: string) => void;
34
+ readonly addEntry: (entry: IHistoryEntry) => void;
35
+ }
36
+
37
+ export interface IBackgroundPanel {
38
+ /** The selected entry itself, which the switcher and the detail view both render. */
39
+ readonly entry: IExecutionWorkspaceEntry | undefined;
40
+ readonly detail: IExecutionDetailState;
41
+ readonly attachToFork: (entry: IExecutionWorkspaceEntry) => void;
42
+ }
43
+
44
+ /** Wire the background panel's detail read and its attach handler together. */
45
+ export function useBackgroundPanel(inputs: IUseBackgroundPanelOptions): IBackgroundPanel {
46
+ const { snapshot, selectedEntryId } = inputs;
47
+ const entry = useMemo(
48
+ () => snapshot?.entries.find((candidate) => candidate.id === selectedEntryId),
49
+ [snapshot, selectedEntryId],
50
+ );
51
+ const detail = useExecutionDetailPage({ entry, snapshot, read: inputs.read });
52
+ const attachToFork = useForkAttach({
53
+ sessionStore: inputs.sessionStore,
54
+ onSessionSwitch: inputs.onSessionSwitch,
55
+ addEntry: inputs.addEntry,
56
+ });
57
+ return { entry, detail, attachToFork };
58
+ }
@@ -0,0 +1,24 @@
1
+ import { useEffect, useState } from 'react';
2
+
3
+ import { MS_PER_SECOND } from '../attention/time-units.js';
4
+
5
+ /** SCREEN-1992: one second, the resolution a `in 59s` countdown is read at. */
6
+ const COUNTDOWN_TICK_MS = MS_PER_SECOND;
7
+
8
+ /**
9
+ * The clock a sleeping schedule's countdown is rendered against. Re-renders once a second only while
10
+ * `active` (an entry carries `nextFireAt`) and not in screen-reader mode — a reader would otherwise
11
+ * hear the row change every second. When not ticking the clock is still the CURRENT one on every
12
+ * render (never the mount-time seed): a reader gets a correct number, read once.
13
+ */
14
+ export function useCountdownTick(active: boolean, screenReader: boolean): Date {
15
+ const ticking = active && !screenReader;
16
+ const [tickedAt, setTickedAt] = useState(() => new Date());
17
+ useEffect(() => {
18
+ if (!ticking) return undefined;
19
+ setTickedAt(new Date());
20
+ const timer = setInterval(() => setTickedAt(new Date()), COUNTDOWN_TICK_MS);
21
+ return () => clearInterval(timer);
22
+ }, [ticking]);
23
+ return ticking ? tickedAt : new Date();
24
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Loads the inline detail page for the selected background-execution entry.
3
+ *
4
+ * Extracted from `App.tsx` (CLI-2004): three pieces of state and one async effect that exist only
5
+ * for the detail pane, sitting in the middle of the component's input handlers. One responsibility,
6
+ * one module — and the App keeps room for the concerns that actually belong to it.
7
+ */
8
+
9
+ import { useEffect, useState } from 'react';
10
+
11
+ import type {
12
+ IExecutionDetailPage,
13
+ IExecutionWorkspaceEntry,
14
+ IExecutionWorkspaceSnapshot,
15
+ } from '@robota-sdk/agent-interface-execution';
16
+
17
+ export interface IExecutionDetailState {
18
+ page: IExecutionDetailPage | null;
19
+ error: string | undefined;
20
+ loading: boolean;
21
+ }
22
+
23
+ export interface IUseExecutionDetailPageInputs {
24
+ /** The selected entry, or undefined / the main thread when no detail is shown. */
25
+ entry: IExecutionWorkspaceEntry | undefined;
26
+ /** Re-read trigger: the snapshot the entry came from. */
27
+ snapshot: IExecutionWorkspaceSnapshot | null;
28
+ read: (entryId: string) => Promise<IExecutionDetailPage>;
29
+ }
30
+
31
+ /** Load (and cancel-on-change) the detail page for the selected entry. */
32
+ export function useExecutionDetailPage(
33
+ inputs: IUseExecutionDetailPageInputs,
34
+ ): IExecutionDetailState {
35
+ const [page, setPage] = useState<IExecutionDetailPage | null>(null);
36
+ const [error, setError] = useState<string | undefined>();
37
+ const [loading, setLoading] = useState(false);
38
+ const { entry, snapshot, read } = inputs;
39
+
40
+ useEffect(() => {
41
+ if (!entry || entry.kind === 'main_thread') {
42
+ setPage(null);
43
+ setError(undefined);
44
+ setLoading(false);
45
+ return;
46
+ }
47
+
48
+ let isCurrent = true;
49
+ setLoading(true);
50
+ setError(undefined);
51
+ read(entry.id)
52
+ .then((loaded) => {
53
+ if (!isCurrent) return;
54
+ setPage(loaded);
55
+ setLoading(false);
56
+ })
57
+ .catch((cause: Error) => {
58
+ if (!isCurrent) return;
59
+ setError(cause.message);
60
+ setLoading(false);
61
+ });
62
+
63
+ return () => {
64
+ isCurrent = false;
65
+ };
66
+ }, [snapshot, read, entry]);
67
+
68
+ return { page, error, loading };
69
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * CLI-1994: the App's `attach` handler, as a hook rather than another closure inside `App.tsx`.
3
+ *
4
+ * `App.tsx` is a file the size floor has already frozen as debt, where the rule is "split instead of
5
+ * extending" — so the handler lives beside the other App hooks. The decision itself is neither here
6
+ * nor there: `attachToForkedSession` asks the framework, which owns when `attach` is offered and
7
+ * when it is honoured.
8
+ */
9
+
10
+ import { createSystemMessage, messageToHistoryEntry } from '@robota-sdk/agent-core';
11
+ import { useCallback } from 'react';
12
+
13
+ import { attachToForkedSession } from '../flows/fork-attach-flow.js';
14
+ import { useTuiCliAdapter } from '../tui-cli-adapter-context.js';
15
+
16
+ import type { IHistoryEntry } from '@robota-sdk/agent-core';
17
+ import type { IExecutionWorkspaceEntry } from '@robota-sdk/agent-interface-execution';
18
+ import type { IInteractiveSessionStore } from '@robota-sdk/agent-interface-session';
19
+
20
+ export interface IUseForkAttachOptions {
21
+ /** Absent when the surface persists nothing; attach then refuses, saying so. */
22
+ readonly sessionStore: IInteractiveSessionStore | undefined;
23
+ /** The surface's session-switch path — the same one the session picker takes. */
24
+ readonly onSessionSwitch: (sessionId: string) => void;
25
+ /** Where a refusal is written, so it reaches the operator instead of disappearing. */
26
+ readonly addEntry: (entry: IHistoryEntry) => void;
27
+ }
28
+
29
+ /**
30
+ * Attach to a forked conversation: point this terminal at the fork's own session record. A VIEW
31
+ * switch, never a merge — the parent's record is not read, written or joined either way.
32
+ */
33
+ export function useForkAttach({
34
+ sessionStore,
35
+ onSessionSwitch,
36
+ addEntry,
37
+ }: IUseForkAttachOptions): (entry: IExecutionWorkspaceEntry) => void {
38
+ const cliAdapter = useTuiCliAdapter();
39
+ return useCallback(
40
+ (entry: IExecutionWorkspaceEntry): void => {
41
+ const notify = (message: string): void =>
42
+ addEntry(messageToHistoryEntry(createSystemMessage(message)));
43
+ // A surface composed without a session store cannot open another session's record. Said
44
+ // plainly rather than reported as "the record is missing", which is a different fact.
45
+ if (sessionStore === undefined) {
46
+ notify('This surface has no session store, so it cannot attach to a forked session.');
47
+ return;
48
+ }
49
+ attachToForkedSession(entry, {
50
+ hasSessionRecord: (sessionId) => sessionStore.load(sessionId).status === 'valid',
51
+ switchSession: onSessionSwitch,
52
+ notify,
53
+ ...(cliAdapter.formatResumeCommand === undefined
54
+ ? {}
55
+ : { formatResumeCommand: cliAdapter.formatResumeCommand }),
56
+ });
57
+ },
58
+ [addEntry, cliAdapter, onSessionSwitch, sessionStore],
59
+ );
60
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * SCREEN-1993 — the input area's side of prompt-history search: the `chat-input.history-search`
3
+ * open binding, and what insert and execute do to the composer.
4
+ *
5
+ * Insert replaces the composer text with the match (cursor at the end); execute submits the match
6
+ * through the same path Enter takes, so it is recorded in the live prompt list like any prompt. While
7
+ * the overlay is open the composer is untouched, which is what makes cancel restore the draft exactly.
8
+ */
9
+ import { useCallback, useEffect } from 'react';
10
+
11
+ import {
12
+ useHistorySearch,
13
+ type IHistorySearchSurface,
14
+ } from '../history-search/useHistorySearch.js';
15
+ import { useKeybindingActions } from '../keybindings/keybindings-context.js';
16
+
17
+ import type { IHistorySearchView } from '../history-search/useHistorySearch.js';
18
+
19
+ /** What the input area receives from the app; absent ⇒ the feature is off and `ctrl+r` is inert. */
20
+ export interface IInputAreaHistorySearch extends IHistorySearchSurface {
21
+ /** Lets the app block its own bindings (Esc abort, the switcher) while the overlay is open. */
22
+ readonly onOpenChange?: ((open: boolean) => void) | undefined;
23
+ }
24
+
25
+ export interface IUseInputAreaHistorySearchOptions {
26
+ readonly historySearch: IInputAreaHistorySearch | undefined;
27
+ readonly sessionPrompts: readonly string[];
28
+ /** The composer can take keys: no popup, not disabled, nothing queued. */
29
+ readonly composerActive: boolean;
30
+ readonly setValue: (value: string) => void;
31
+ readonly setCursorHint: (hint: number | null) => void;
32
+ readonly submitPrompt: (prompt: string) => void;
33
+ }
34
+
35
+ export function useInputAreaHistorySearch(
36
+ options: IUseInputAreaHistorySearchOptions,
37
+ ): IHistorySearchView {
38
+ const { historySearch, setValue, setCursorHint, submitPrompt, composerActive } = options;
39
+ const onInsert = useCallback(
40
+ (text: string) => {
41
+ setValue(text);
42
+ setCursorHint(null);
43
+ },
44
+ [setCursorHint, setValue],
45
+ );
46
+ const onExecute = useCallback(
47
+ (text: string) => {
48
+ setValue('');
49
+ submitPrompt(text);
50
+ },
51
+ [setValue, submitPrompt],
52
+ );
53
+ const { view, openSearch, closeSearch } = useHistorySearch({
54
+ surface: historySearch,
55
+ sessionPrompts: options.sessionPrompts,
56
+ onInsert,
57
+ onExecute,
58
+ onOpenChange: historySearch?.onOpenChange,
59
+ });
60
+ useKeybindingActions(
61
+ 'chat-input',
62
+ (actions) => {
63
+ if (actions.includes('history-search')) openSearch();
64
+ },
65
+ { isActive: composerActive && !view.open && historySearch !== undefined },
66
+ );
67
+ // Another overlay or a disabled composer takes the keys: the search cannot stay open under it.
68
+ useEffect(() => {
69
+ if (view.open && !composerActive) closeSearch();
70
+ }, [closeSearch, composerActive, view.open]);
71
+ return view;
72
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * The input area's three key bindings, in one place.
3
+ *
4
+ * Extracted from `InputArea.tsx` (CLI-2004) by responsibility: the component owns the input's state
5
+ * and its rendering; these three `useInput` registrations are the routing between the two, and each
6
+ * is gated on a different combination of the same flags — which is the shape that gets copied wrong
7
+ * when it is inline among the render code.
8
+ */
9
+
10
+ import {
11
+ moveAutocompleteSelection,
12
+ navigatePromptHistory,
13
+ type IPromptHistoryNavigationState,
14
+ } from '../flows/input-area-flow.js';
15
+ import { useKeybindingActions } from '../keybindings/keybindings-context.js';
16
+
17
+ import type { ICommand } from '@robota-sdk/agent-interface-command';
18
+
19
+ export interface IUseInputAreaKeysInputs {
20
+ value: string;
21
+ isDisabled: boolean;
22
+ /** SCREEN-1993: the history-search overlay owns the keys while it is open. */
23
+ searchOpen: boolean;
24
+ isQueueCancellationDisabled: boolean;
25
+ pendingPrompt?: string | null;
26
+ showPopup: boolean;
27
+ setShowPopup: (shown: boolean) => void;
28
+ filteredCommands: ICommand[];
29
+ selectedIndex: number;
30
+ setSelectedIndex: (update: (previous: number) => number) => void;
31
+ tabCompleteCommand: (command: ICommand) => void;
32
+ promptHistory: string[];
33
+ historyState: IPromptHistoryNavigationState;
34
+ setValue: (value: string) => void;
35
+ setCursorHint: (hint: number | null) => void;
36
+ setHistoryState: (state: IPromptHistoryNavigationState) => void;
37
+ onRequestFocusBackgroundList?: (() => void) | undefined;
38
+ onCancelQueue?: (() => void) | undefined;
39
+ }
40
+
41
+ /** The autocomplete popup's own keys: move the selection, close it, or take the completion. */
42
+ function useAutocompletePopupKeys(inputs: IUseInputAreaKeysInputs): void {
43
+ const { showPopup, isDisabled, filteredCommands, selectedIndex } = inputs;
44
+ useKeybindingActions(
45
+ 'autocomplete-menu',
46
+ (actions) => {
47
+ if (!showPopup) return;
48
+ for (const action of actions) {
49
+ if (action === 'previous' || action === 'next') {
50
+ inputs.setSelectedIndex((previous) =>
51
+ moveAutocompleteSelection(previous, filteredCommands.length, action),
52
+ );
53
+ } else if (action === 'close') {
54
+ inputs.setShowPopup(false);
55
+ } else if (action === 'accept') {
56
+ const command = filteredCommands[selectedIndex];
57
+ if (command) inputs.tabCompleteCommand(command);
58
+ }
59
+ }
60
+ },
61
+ { isActive: showPopup && !isDisabled },
62
+ );
63
+ }
64
+
65
+ /** Register the autocomplete-popup, prompt-history and queued-prompt bindings. */
66
+ export function useInputAreaKeys(inputs: IUseInputAreaKeysInputs): void {
67
+ const { showPopup, isDisabled, isQueueCancellationDisabled, pendingPrompt, searchOpen } = inputs;
68
+
69
+ useAutocompletePopupKeys(inputs);
70
+
71
+ useKeybindingActions(
72
+ 'chat-input',
73
+ (actions) => {
74
+ const action = actions.find(
75
+ (candidate): candidate is 'history-previous' | 'history-next' =>
76
+ candidate === 'history-previous' || candidate === 'history-next',
77
+ );
78
+ if (action === undefined) return;
79
+ const direction = action === 'history-previous' ? 'previous' : 'next';
80
+ // SCREEN-014: ↓ on an empty input that is not browsing history falls through into the
81
+ // background-work list (where it is a no-op for the input today). The parent decides whether
82
+ // there is a list to focus.
83
+ if (
84
+ direction === 'next' &&
85
+ inputs.historyState.selectedIndex === null &&
86
+ inputs.value.length === 0
87
+ ) {
88
+ inputs.onRequestFocusBackgroundList?.();
89
+ return;
90
+ }
91
+ const result = navigatePromptHistory(
92
+ inputs.value,
93
+ inputs.promptHistory,
94
+ inputs.historyState,
95
+ direction,
96
+ );
97
+ inputs.setValue(result.value);
98
+ inputs.setCursorHint(result.cursorHint);
99
+ inputs.setHistoryState(result.state);
100
+ },
101
+ { isActive: !showPopup && !isDisabled && !pendingPrompt && !searchOpen },
102
+ );
103
+
104
+ // Backspace cancels queued prompt
105
+ useKeybindingActions(
106
+ 'queued-prompt',
107
+ (actions) => {
108
+ if (actions.includes('cancel') && pendingPrompt) {
109
+ inputs.onCancelQueue?.();
110
+ }
111
+ },
112
+ { isActive: !!pendingPrompt && !isQueueCancellationDisabled },
113
+ );
114
+ }