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

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 (348) hide show
  1. package/CHANGELOG.md +1204 -0
  2. package/LICENSE +661 -0
  3. package/README.md +50 -0
  4. package/dist/node/index.d.ts +1224 -0
  5. package/dist/node/index.d.ts.map +1 -0
  6. package/dist/node/index.js +33 -0
  7. package/dist/node/index.js.map +1 -0
  8. package/package.json +88 -0
  9. package/src/App.tsx +157 -0
  10. package/src/AppPresentation.tsx +190 -0
  11. package/src/AppView.tsx +26 -0
  12. package/src/BackgroundTaskPanel.tsx +73 -0
  13. package/src/CjkTextInput.tsx +336 -0
  14. package/src/ConfirmPrompt.tsx +140 -0
  15. package/src/ContextWarningBanner.tsx +45 -0
  16. package/src/ExecutionWorkspaceDetailPane.tsx +85 -0
  17. package/src/ExecutionWorkspaceSwitcher.tsx +213 -0
  18. package/src/HistorySearchOverlay.tsx +150 -0
  19. package/src/InputArea.tsx +338 -0
  20. package/src/ListPicker.tsx +158 -0
  21. package/src/MenuSelect.tsx +159 -0
  22. package/src/MessageList.tsx +308 -0
  23. package/src/MultiSelectList.tsx +294 -0
  24. package/src/PendingActionPrompt.tsx +121 -0
  25. package/src/PermissionPrompt.tsx +236 -0
  26. package/src/PluginTUI.tsx +258 -0
  27. package/src/RoleLabel.tsx +72 -0
  28. package/src/SafeText.tsx +66 -0
  29. package/src/SessionEventNotices.tsx +27 -0
  30. package/src/SessionPicker.tsx +92 -0
  31. package/src/SessionStatusBar.tsx +80 -0
  32. package/src/SlashAutocomplete.tsx +154 -0
  33. package/src/StatusBar.tsx +308 -0
  34. package/src/StreamingIndicator.tsx +145 -0
  35. package/src/SupervisedSessionView.tsx +872 -0
  36. package/src/TextPrompt.tsx +112 -0
  37. package/src/ThemePicker.tsx +366 -0
  38. package/src/ToolCommandOutput.tsx +48 -0
  39. package/src/ToolDiffBlock.tsx +44 -0
  40. package/src/TransportTUI.tsx +166 -0
  41. package/src/TuiInteractionChannel.ts +530 -0
  42. package/src/UpdateNotice.tsx +18 -0
  43. package/src/UsageSummaryEntry.tsx +49 -0
  44. package/src/WaveText.tsx +70 -0
  45. package/src/__tests__/ListPicker.test.tsx +188 -0
  46. package/src/__tests__/MenuSelect.test.tsx +127 -0
  47. package/src/__tests__/PendingActionPrompt.test.tsx +193 -0
  48. package/src/__tests__/PluginTUI.test.tsx +167 -0
  49. package/src/__tests__/SlashAutocomplete.test.tsx +162 -0
  50. package/src/__tests__/SupervisedSessionView-render.test.tsx +52 -0
  51. package/src/__tests__/SupervisedSessionView.test.tsx +1112 -0
  52. package/src/__tests__/TextPrompt.test.tsx +112 -0
  53. package/src/__tests__/TransportTUI.test.tsx +124 -0
  54. package/src/__tests__/TuiInteractionChannel.askUser.test.ts +124 -0
  55. package/src/__tests__/TuiInteractionChannel.display-contract.test.ts +294 -0
  56. package/src/__tests__/TuiInteractionChannel.lifecycle.test.ts +650 -0
  57. package/src/__tests__/UpdateNotice.test.tsx +15 -0
  58. package/src/__tests__/__fixtures__/korean-text.json +17 -0
  59. package/src/__tests__/abort-after-permission.test.tsx +160 -0
  60. package/src/__tests__/abort-streaming-e2e.test.tsx +183 -0
  61. package/src/__tests__/attached-app-session-switch.test.tsx +317 -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__/checkpoint-store-per-channel.test.ts +81 -0
  69. package/src/__tests__/cjk-defer-submit.test.tsx +68 -0
  70. package/src/__tests__/cjk-fallback-render.test.tsx +89 -0
  71. package/src/__tests__/cjk-text-input-flow.test.ts +187 -0
  72. package/src/__tests__/cjk-text-input.test.ts +196 -0
  73. package/src/__tests__/command-handoff-pty-e2e.test.ts +107 -0
  74. package/src/__tests__/command-output-summary.test.ts +95 -0
  75. package/src/__tests__/command-result-handler.test.ts +171 -0
  76. package/src/__tests__/compact-event-bridge.test.ts +20 -0
  77. package/src/__tests__/confirm-permission-flow.test.ts +149 -0
  78. package/src/__tests__/confirm-prompt.test.tsx +103 -0
  79. package/src/__tests__/contextual-keybindings-input.test.tsx +158 -0
  80. package/src/__tests__/default-tui-cli-adapter-host-settings.test.ts +47 -0
  81. package/src/__tests__/effort-reaches-the-session.test.ts +41 -0
  82. package/src/__tests__/execution-workspace-switcher.test.tsx +147 -0
  83. package/src/__tests__/execution-workspace-view-model.test.ts +94 -0
  84. package/src/__tests__/external-prompt-notice.test.tsx +100 -0
  85. package/src/__tests__/fixtures/command-handoff-driver.tsx +114 -0
  86. package/src/__tests__/fixtures/fake-editor.sh +7 -0
  87. package/src/__tests__/fixtures/terminal-handoff-driver.tsx +90 -0
  88. package/src/__tests__/fork-attach.test.tsx +235 -0
  89. package/src/__tests__/grant-history-reaches-the-session.test.ts +31 -0
  90. package/src/__tests__/helpers/terminal-profiles.ts +152 -0
  91. package/src/__tests__/helpers/vt-cursor-interpreter.ts +175 -0
  92. package/src/__tests__/history-clear-broadcast.test.ts +66 -0
  93. package/src/__tests__/history-search-overlay.test.tsx +329 -0
  94. package/src/__tests__/humanize-tool-name.test.ts +30 -0
  95. package/src/__tests__/input-area-bottom-border.test.tsx +45 -0
  96. package/src/__tests__/input-area-flow.test.ts +199 -0
  97. package/src/__tests__/input-area-focus-handoff.test.tsx +77 -0
  98. package/src/__tests__/interaction-channel-charter.test.ts +13 -0
  99. package/src/__tests__/key-hint-consistency.test.tsx +249 -0
  100. package/src/__tests__/key-hint-footer.test.tsx +69 -0
  101. package/src/__tests__/live-trace-reaches-session.test.ts +14 -0
  102. package/src/__tests__/message-list-rendering.test.tsx +474 -0
  103. package/src/__tests__/numbered-selection-chunk.test.tsx +41 -0
  104. package/src/__tests__/org-policy-reaches-the-channel.test.ts +77 -0
  105. package/src/__tests__/output-style-reaches-session.test.ts +35 -0
  106. package/src/__tests__/palette-consistency.test.ts +329 -0
  107. package/src/__tests__/permission-prompt-arming.test.tsx +153 -0
  108. package/src/__tests__/permission-prompt-availability.test.tsx +23 -0
  109. package/src/__tests__/permission-prompt-peer.test.tsx +34 -0
  110. package/src/__tests__/permission-prompt-requester.test.tsx +34 -0
  111. package/src/__tests__/product-display-name.test.tsx +79 -0
  112. package/src/__tests__/prompt-queue.test.tsx +260 -0
  113. package/src/__tests__/provider-definitions-reach-the-session.test.ts +75 -0
  114. package/src/__tests__/pty/ask-user-question.ptytest.ts +72 -0
  115. package/src/__tests__/pty/background-work-switcher.ptytest.ts +53 -0
  116. package/src/__tests__/pty/behavior-2437-git.ptytest.ts +350 -0
  117. package/src/__tests__/pty/fixtures/ask-user-question.jsonl +3 -0
  118. package/src/__tests__/pty/fixtures/replay-conversation.jsonl +4 -0
  119. package/src/__tests__/pty/fixtures/screen-005-ask-shapes.jsonl +5 -0
  120. package/src/__tests__/pty/fixtures/screen-005-permission.jsonl +3 -0
  121. package/src/__tests__/pty/fixtures/screen-006-colors.jsonl +3 -0
  122. package/src/__tests__/pty/fixtures/screen-2002-themes.jsonl +5 -0
  123. package/src/__tests__/pty/flag-tui.ptytest.ts +56 -0
  124. package/src/__tests__/pty/flow-2006-deep-link.ptytest.ts +320 -0
  125. package/src/__tests__/pty/ime-cursor-tmux.ptytest.ts +211 -0
  126. package/src/__tests__/pty/ime-cursor.ptytest.ts +187 -0
  127. package/src/__tests__/pty/isolated-home.ts +97 -0
  128. package/src/__tests__/pty/org-policy.ptytest.ts +71 -0
  129. package/src/__tests__/pty/provider-setup.ptytest.ts +61 -0
  130. package/src/__tests__/pty/pty-driver.ts +155 -0
  131. package/src/__tests__/pty/replay-conversation.ptytest.ts +68 -0
  132. package/src/__tests__/pty/screen-005-prompt-footers.ptytest.ts +135 -0
  133. package/src/__tests__/pty/screen-006-no-color.ptytest.ts +140 -0
  134. package/src/__tests__/pty/screen-010-scrollback.ptytest.ts +123 -0
  135. package/src/__tests__/pty/screen-1993-scrollback.ptytest.ts +124 -0
  136. package/src/__tests__/pty/screen-2002-themes.ptytest.ts +292 -0
  137. package/src/__tests__/pty/screen-2670-prepark.ptytest.ts +228 -0
  138. package/src/__tests__/pty/screen-reader-mode.ptytest.ts +228 -0
  139. package/src/__tests__/pty/session-attach.ptytest.ts +182 -0
  140. package/src/__tests__/pty/settings-screen.ptytest.ts +49 -0
  141. package/src/__tests__/pty/spawn-pty.test.ts +120 -0
  142. package/src/__tests__/pty/spawn-pty.ts +229 -0
  143. package/src/__tests__/pty/terminal-handoff.ptytest.ts +79 -0
  144. package/src/__tests__/pty/trust-prompt.ptytest.ts +134 -0
  145. package/src/__tests__/pty/tui-pty.ptytest.ts +65 -0
  146. package/src/__tests__/pty-driver-dispose.test.ts +43 -0
  147. package/src/__tests__/real-cursor-positioning.test.tsx +264 -0
  148. package/src/__tests__/rename-broadcast-persistence.test.tsx +140 -0
  149. package/src/__tests__/render-channel-options.test.ts +201 -0
  150. package/src/__tests__/render-lifecycle.test.ts +36 -0
  151. package/src/__tests__/render-markdown.test.ts +164 -0
  152. package/src/__tests__/rendered-markdown-styling.test.tsx +98 -0
  153. package/src/__tests__/safe-text-boundary.test.tsx +75 -0
  154. package/src/__tests__/sandbox-reaches-the-session.test.ts +37 -0
  155. package/src/__tests__/screen-reader-input.test.tsx +106 -0
  156. package/src/__tests__/screen-reader-labels.test.ts +91 -0
  157. package/src/__tests__/screen-reader-menus.test.tsx +200 -0
  158. package/src/__tests__/screen-reader-pacing.test.ts +230 -0
  159. package/src/__tests__/screen-reader-prepark-echo.test.tsx +200 -0
  160. package/src/__tests__/screen-reader-render-options.test.ts +262 -0
  161. package/src/__tests__/screen-reader-stdout.test.ts +322 -0
  162. package/src/__tests__/screen-reader-turn-marks-port.test.tsx +85 -0
  163. package/src/__tests__/screen-reader-turn-signals.test.tsx +125 -0
  164. package/src/__tests__/sec-019-terminal-sanitizer.test.ts +276 -0
  165. package/src/__tests__/sec-019-tool-label-render.test.tsx +207 -0
  166. package/src/__tests__/selection-flow.test.ts +61 -0
  167. package/src/__tests__/session-event-notices.test.tsx +31 -0
  168. package/src/__tests__/session-init-poller.test.ts +102 -0
  169. package/src/__tests__/session-picker.test.tsx +93 -0
  170. package/src/__tests__/session-switch-channel.test.tsx +470 -0
  171. package/src/__tests__/shutdown-signal.test.ts +34 -0
  172. package/src/__tests__/status-activity.test.ts +71 -0
  173. package/src/__tests__/status-bar.test.tsx +290 -0
  174. package/src/__tests__/status-glyph.test.ts +30 -0
  175. package/src/__tests__/streaming-indicator.test.tsx +178 -0
  176. package/src/__tests__/terminal-capabilities.test.ts +153 -0
  177. package/src/__tests__/terminal-handoff-controller.test.ts +173 -0
  178. package/src/__tests__/terminal-handoff-pty-e2e.test.ts +77 -0
  179. package/src/__tests__/text-prompt-flow.test.ts +77 -0
  180. package/src/__tests__/theme-picker.test.tsx +345 -0
  181. package/src/__tests__/transport-boundary.test.ts +11 -0
  182. package/src/__tests__/tui-app-channel-port.test.ts +31 -0
  183. package/src/__tests__/tui-channel-init-failure.test.ts +70 -0
  184. package/src/__tests__/tui-channel-lifecycle-coordinator.test.ts +258 -0
  185. package/src/__tests__/tui-interaction-queues.test.ts +149 -0
  186. package/src/__tests__/tui-preset-capabilities.test.ts +33 -0
  187. package/src/__tests__/tui-state-manager.error-stall.test.ts +169 -0
  188. package/src/__tests__/tui-state-manager.test.ts +482 -0
  189. package/src/__tests__/ui-intent-rendering.test.tsx +291 -0
  190. package/src/__tests__/wave-text.test.tsx +133 -0
  191. package/src/__tests__/wire-tui-channel.test.ts +1457 -0
  192. package/src/__tests__/wire-tui-client-commands.test.ts +243 -0
  193. package/src/app-banner.tsx +42 -0
  194. package/src/app-static-items.ts +35 -0
  195. package/src/app-view-model.ts +140 -0
  196. package/src/attached-session-connection.ts +14 -0
  197. package/src/attention/__tests__/attention-coordinator.test.ts +166 -0
  198. package/src/attention/__tests__/attention-tracker.test.ts +103 -0
  199. package/src/attention/__tests__/countdown.test.ts +15 -0
  200. package/src/attention/__tests__/focus-input-filter.test.ts +93 -0
  201. package/src/attention/__tests__/interval-recap.test.ts +49 -0
  202. package/src/attention/attention-coordinator.ts +71 -0
  203. package/src/attention/attention-tracker.ts +124 -0
  204. package/src/attention/countdown.ts +24 -0
  205. package/src/attention/focus-input-filter.ts +131 -0
  206. package/src/attention/interval-recap.ts +148 -0
  207. package/src/attention/time-units.ts +6 -0
  208. package/src/attention-bell.ts +79 -0
  209. package/src/attributed-user-echo.ts +30 -0
  210. package/src/background-task-row-format.ts +99 -0
  211. package/src/command-interaction.ts +8 -0
  212. package/src/command-output-summary.ts +129 -0
  213. package/src/create-default-tui-cli-adapter.ts +45 -0
  214. package/src/execution-workspace-switcher-selection.ts +118 -0
  215. package/src/execution-workspace-view-model.ts +139 -0
  216. package/src/external-prompt-notice.tsx +38 -0
  217. package/src/flows/__tests__/background-focus-flow.test.ts +48 -0
  218. package/src/flows/__tests__/defer-submit.test.ts +88 -0
  219. package/src/flows/__tests__/real-cursor-flow.test.ts +139 -0
  220. package/src/flows/background-focus-flow.ts +44 -0
  221. package/src/flows/cjk-cursor-motion.ts +75 -0
  222. package/src/flows/cjk-text-deletion.ts +53 -0
  223. package/src/flows/cjk-text-input-flow.ts +248 -0
  224. package/src/flows/cjk-text-input-types.ts +24 -0
  225. package/src/flows/confirm-prompt-flow.ts +90 -0
  226. package/src/flows/defer-submit.ts +67 -0
  227. package/src/flows/fork-attach-flow.ts +62 -0
  228. package/src/flows/input-area-flow.ts +188 -0
  229. package/src/flows/permission-prompt-flow.ts +108 -0
  230. package/src/flows/real-cursor-flow.ts +83 -0
  231. package/src/flows/selection-flow.ts +205 -0
  232. package/src/flows/session-init-poller.ts +76 -0
  233. package/src/flows/text-prompt-flow.ts +93 -0
  234. package/src/history-search/__tests__/history-search-flow.test.ts +86 -0
  235. package/src/history-search/history-search-flow.ts +94 -0
  236. package/src/history-search/useHistorySearch.ts +336 -0
  237. package/src/hooks/__tests__/use-app-detach-key.test.tsx +72 -0
  238. package/src/hooks/__tests__/use-app-loop-escape.test.tsx +87 -0
  239. package/src/hooks/__tests__/use-plugin-callbacks.test.ts +18 -0
  240. package/src/hooks/__tests__/use-tui-channel-stability.test.tsx +100 -0
  241. package/src/hooks/__tests__/useAppThemeState.test.tsx +130 -0
  242. package/src/hooks/command-result-handler.ts +44 -0
  243. package/src/hooks/side-effects-types.ts +45 -0
  244. package/src/hooks/useAppController.ts +271 -0
  245. package/src/hooks/useAppInputBindings.ts +163 -0
  246. package/src/hooks/useAppInteractionState.ts +84 -0
  247. package/src/hooks/useAppLifecycleState.ts +81 -0
  248. package/src/hooks/useAppOverlays.ts +90 -0
  249. package/src/hooks/useAppScreenState.ts +81 -0
  250. package/src/hooks/useAppSubmissionState.ts +45 -0
  251. package/src/hooks/useAppThemeState.ts +157 -0
  252. package/src/hooks/useAppWorkspaceState.ts +149 -0
  253. package/src/hooks/useAppearanceSettings.ts +30 -0
  254. package/src/hooks/useAutocomplete.ts +101 -0
  255. package/src/hooks/useBackgroundPanel.ts +58 -0
  256. package/src/hooks/useCountdownTick.ts +24 -0
  257. package/src/hooks/useExecutionDetailPage.ts +69 -0
  258. package/src/hooks/useForkAttach.ts +60 -0
  259. package/src/hooks/useInputAreaHistorySearch.ts +72 -0
  260. package/src/hooks/useInputAreaKeys.ts +114 -0
  261. package/src/hooks/useNumberedSelection.ts +93 -0
  262. package/src/hooks/usePluginCallbacks.ts +38 -0
  263. package/src/hooks/usePluginScreenData.ts +85 -0
  264. package/src/hooks/useRealCursorPosition.ts +125 -0
  265. package/src/hooks/useRenderMarkdown.ts +38 -0
  266. package/src/hooks/useScreenReaderTurnSignals.ts +113 -0
  267. package/src/hooks/useSideEffects.ts +144 -0
  268. package/src/hooks/useStatusLineSettings.ts +50 -0
  269. package/src/hooks/useTerminalHandoffSuspension.ts +30 -0
  270. package/src/hooks/useTuiChannel.ts +141 -0
  271. package/src/humanize-tool-name.ts +51 -0
  272. package/src/index.ts +72 -0
  273. package/src/input-area-rules.tsx +71 -0
  274. package/src/json-value.ts +18 -0
  275. package/src/key-hint-footer.tsx +54 -0
  276. package/src/keybindings/__tests__/keybinding-registry.test.ts +228 -0
  277. package/src/keybindings/__tests__/node-keybindings-source.test.ts +118 -0
  278. package/src/keybindings/keybinding-catalogue.ts +171 -0
  279. package/src/keybindings/keybinding-registry.ts +303 -0
  280. package/src/keybindings/keybinding-syntax.ts +155 -0
  281. package/src/keybindings/keybindings-context.tsx +124 -0
  282. package/src/keybindings/node-keybindings-source.ts +189 -0
  283. package/src/numbered-list.tsx +113 -0
  284. package/src/plugin-tui-handlers.ts +163 -0
  285. package/src/product-display-name-context.tsx +30 -0
  286. package/src/render-markdown.ts +209 -0
  287. package/src/render.tsx +746 -0
  288. package/src/sanitize-terminal-text.ts +171 -0
  289. package/src/screen-reader-announcement.ts +52 -0
  290. package/src/screen-reader-context.tsx +32 -0
  291. package/src/screen-reader-labels.ts +64 -0
  292. package/src/screen-reader-pacing-context.tsx +47 -0
  293. package/src/screen-reader-pacing.ts +178 -0
  294. package/src/screen-reader-stdout.ts +328 -0
  295. package/src/short-session-id.ts +9 -0
  296. package/src/shutdown-signal.ts +34 -0
  297. package/src/slash-command-input.ts +11 -0
  298. package/src/status-activity.ts +70 -0
  299. package/src/status-glyph.ts +78 -0
  300. package/src/terminal-capabilities-context.tsx +27 -0
  301. package/src/terminal-capabilities.ts +77 -0
  302. package/src/terminal-focus-reporting.ts +50 -0
  303. package/src/terminal-handoff-controller.ts +98 -0
  304. package/src/terminal-marks.ts +63 -0
  305. package/src/theme/__tests__/built-in-themes.test.ts +117 -0
  306. package/src/theme/__tests__/color-vision.test.ts +82 -0
  307. package/src/theme/__tests__/theme-context.test.tsx +146 -0
  308. package/src/theme/__tests__/theme-document.test.ts +222 -0
  309. package/src/theme/__tests__/theme-registry.test.ts +138 -0
  310. package/src/theme/__tests__/theme-styles.test.ts +233 -0
  311. package/src/theme/built-in-themes.ts +342 -0
  312. package/src/theme/color-vision.ts +171 -0
  313. package/src/theme/index.ts +45 -0
  314. package/src/theme/theme-context.tsx +81 -0
  315. package/src/theme/theme-contracts.ts +139 -0
  316. package/src/theme/theme-document.ts +278 -0
  317. package/src/theme/theme-registry.ts +127 -0
  318. package/src/theme/theme-styles.ts +177 -0
  319. package/src/tool-summary-status.ts +59 -0
  320. package/src/tui-app-channel-port.ts +111 -0
  321. package/src/tui-channel-lifecycle-coordinator.ts +149 -0
  322. package/src/tui-channel-options.ts +184 -0
  323. package/src/tui-cli-adapter-context.tsx +13 -0
  324. package/src/tui-cli-adapter.ts +24 -0
  325. package/src/tui-interaction-queues.ts +171 -0
  326. package/src/tui-session-binding.ts +52 -0
  327. package/src/tui-session-event-projector.ts +149 -0
  328. package/src/tui-session-events.ts +89 -0
  329. package/src/tui-session-notice-store.ts +43 -0
  330. package/src/tui-session-options.ts +134 -0
  331. package/src/tui-state-manager.ts +347 -0
  332. package/src/types.ts +21 -0
  333. package/src/use-terminal-title.ts +29 -0
  334. package/src/utils/__tests__/edit-diff.test.ts +426 -0
  335. package/src/utils/__tests__/paste-detection.test.ts +116 -0
  336. package/src/utils/__tests__/paste-labels.test.ts +46 -0
  337. package/src/utils/__tests__/tool-call-extractor.test.ts +227 -0
  338. package/src/utils/__tests__/tool-diff-summary.test.ts +104 -0
  339. package/src/utils/edit-diff.ts +151 -0
  340. package/src/utils/input-top-border.ts +19 -0
  341. package/src/utils/paste-labels.ts +9 -0
  342. package/src/utils/tool-call-extractor.ts +92 -0
  343. package/src/utils/tool-diff-summary.ts +75 -0
  344. package/src/waiting-loop-stop-notice.ts +23 -0
  345. package/src/wire-history-sync.ts +159 -0
  346. package/src/wire-tui-channel.ts +963 -0
  347. package/src/wire-tui-client-commands.ts +142 -0
  348. package/src/wire-tui-projection.ts +136 -0
@@ -0,0 +1,228 @@
1
+ import { readFileSync } from 'node:fs';
2
+
3
+ import { describe, expect, it } from 'vitest';
4
+
5
+ import { KEYBINDING_ACTIONS } from '../keybinding-catalogue.js';
6
+ import {
7
+ createEmptyChordState,
8
+ DEFAULT_KEYBINDINGS,
9
+ keybindingHints,
10
+ parseKeybindingsDocument,
11
+ resolveKeybindingInput,
12
+ } from '../keybinding-registry.js';
13
+
14
+ const SOURCE = '/tmp/agent-test-keybindings.json';
15
+
16
+ function parse(document: unknown) {
17
+ return parseKeybindingsDocument(JSON.stringify(document), SOURCE);
18
+ }
19
+
20
+ describe('contextual keybinding registry', () => {
21
+ it('normalizes aliases and uppercase, reuses bindings by context, and applies null unbinding', () => {
22
+ const result = parse({
23
+ version: 1,
24
+ bindings: {
25
+ 'chat-input': { submit: 'control+j', 'history-previous': null },
26
+ 'autocomplete-menu': { accept: 'ctrl+j' },
27
+ 'workspace-switcher': { attach: 'A' },
28
+ },
29
+ });
30
+
31
+ expect(result.ok).toBe(true);
32
+ if (!result.ok) return;
33
+ expect(result.snapshot.bindings['chat-input'].submit).toEqual(['ctrl+j']);
34
+ expect(result.snapshot.bindings['chat-input']['history-previous']).toEqual([]);
35
+ expect(result.snapshot.bindings['autocomplete-menu'].accept).toEqual(['ctrl+j']);
36
+ expect(result.snapshot.bindings['workspace-switcher'].attach).toEqual(['shift+a']);
37
+ expect(result.snapshot.bindings['chat-input']['cursor-left']).toEqual(
38
+ DEFAULT_KEYBINDINGS['chat-input']['cursor-left'],
39
+ );
40
+ expect(Object.isFrozen(result.snapshot)).toBe(true);
41
+ });
42
+
43
+ it.each([
44
+ [{ version: 1, bindings: { missing: { submit: 'enter' } } }, '$.bindings.missing'],
45
+ [
46
+ { version: 1, bindings: { 'chat-input': { missing: 'enter' } } },
47
+ '$.bindings.chat-input.missing',
48
+ ],
49
+ [
50
+ { version: 1, bindings: { 'chat-input': { submit: 'ctrl+c' } } },
51
+ '$.bindings.chat-input.submit',
52
+ ],
53
+ // SCREEN-1993: Ctrl+C stays reserved in the search overlay too.
54
+ [
55
+ { version: 1, bindings: { 'history-search': { execute: 'ctrl+c' } } },
56
+ '$.bindings.history-search.execute',
57
+ ],
58
+ [
59
+ {
60
+ version: 1,
61
+ bindings: { 'chat-input': { submit: 'ctrl+j', 'history-previous': 'control+j' } },
62
+ },
63
+ '$.bindings.chat-input.history-previous',
64
+ ],
65
+ [
66
+ {
67
+ version: 1,
68
+ bindings: { 'chat-input': { submit: 'ctrl+m', 'history-previous': 'enter' } },
69
+ },
70
+ '$.bindings.chat-input.history-previous',
71
+ ],
72
+ [
73
+ {
74
+ version: 1,
75
+ bindings: { 'chat-input': { submit: 'ctrl+x', 'history-previous': 'ctrl+x ctrl+p' } },
76
+ },
77
+ '$.bindings.chat-input.history-previous',
78
+ ],
79
+ [{ version: 1, bindings: { 'chat-input': { submit: 'g g' } } }, '$.bindings.chat-input.submit'],
80
+ ])('rejects an invalid replacement atomically with an exact path', (document, path) => {
81
+ const result = parse(document);
82
+ expect(result.ok).toBe(false);
83
+ if (result.ok) return;
84
+ expect(result.diagnostic.path).toBe(path);
85
+ expect(result.diagnostic.file).toBe(SOURCE);
86
+ });
87
+
88
+ it('keeps multiplexer and undeliverable modifier conflicts as warnings', () => {
89
+ const result = parse({
90
+ version: 1,
91
+ bindings: {
92
+ app: { 'open-workspace-switcher': 'ctrl+b' },
93
+ 'workspace-switcher': { attach: 'ctrl+shift+a' },
94
+ },
95
+ });
96
+ expect(result.ok).toBe(true);
97
+ if (!result.ok) return;
98
+ expect(result.snapshot.warnings.map((warning) => warning.code)).toEqual([
99
+ 'multiplexer-conflict',
100
+ 'modifier-delivery',
101
+ ]);
102
+ });
103
+
104
+ it('resolves bounded chords and re-evaluates one mismatching stroke', () => {
105
+ const parsed = parse({
106
+ version: 1,
107
+ bindings: {
108
+ 'workspace-switcher': { attach: 'g g', close: 'escape' },
109
+ },
110
+ });
111
+ expect(parsed.ok).toBe(true);
112
+ if (!parsed.ok) return;
113
+
114
+ const first = resolveKeybindingInput({
115
+ snapshot: parsed.snapshot,
116
+ state: createEmptyChordState(),
117
+ context: 'workspace-switcher',
118
+ input: 'g',
119
+ key: {},
120
+ now: 100,
121
+ });
122
+ expect(first).toMatchObject({ actions: [], consumed: true });
123
+
124
+ const mismatch = resolveKeybindingInput({
125
+ snapshot: parsed.snapshot,
126
+ state: first.state,
127
+ context: 'workspace-switcher',
128
+ input: '',
129
+ key: { escape: true },
130
+ now: 200,
131
+ });
132
+ expect(mismatch.actions).toEqual(['close']);
133
+ expect(mismatch.state.strokes).toEqual([]);
134
+ });
135
+
136
+ it('resolves terminal control-code aliases without changing the configured label', () => {
137
+ const parsed = parse({
138
+ version: 1,
139
+ bindings: { 'chat-input': { submit: 'ctrl+m' } },
140
+ });
141
+ expect(parsed.ok).toBe(true);
142
+ if (!parsed.ok) return;
143
+
144
+ const result = resolveKeybindingInput({
145
+ snapshot: parsed.snapshot,
146
+ state: createEmptyChordState(),
147
+ context: 'chat-input',
148
+ input: '',
149
+ key: { return: true },
150
+ now: 0,
151
+ });
152
+
153
+ expect(result).toMatchObject({ actions: ['submit'], consumed: true });
154
+ expect(keybindingHints(parsed.snapshot, 'chat-input', [['submit', 'Submit']])).toEqual([
155
+ { keys: 'Ctrl+M', label: 'Submit' },
156
+ ]);
157
+ });
158
+
159
+ it('resets pending chords on timeout, context change, and snapshot replacement', () => {
160
+ const parsed = parse({
161
+ version: 1,
162
+ bindings: { 'workspace-switcher': { attach: 'g g' } },
163
+ });
164
+ expect(parsed.ok).toBe(true);
165
+ if (!parsed.ok) return;
166
+ const pending = resolveKeybindingInput({
167
+ snapshot: parsed.snapshot,
168
+ state: createEmptyChordState(),
169
+ context: 'workspace-switcher',
170
+ input: 'g',
171
+ key: {},
172
+ now: 0,
173
+ }).state;
174
+
175
+ for (const input of [
176
+ { context: 'workspace-switcher' as const, now: 1_001, generation: 0, strokes: ['g'] },
177
+ { context: 'list-picker' as const, now: 100, generation: 0, strokes: [] },
178
+ { context: 'workspace-switcher' as const, now: 100, generation: 1, strokes: ['g'] },
179
+ ]) {
180
+ const snapshot = { ...parsed.snapshot, generation: input.generation };
181
+ const result = resolveKeybindingInput({
182
+ snapshot,
183
+ state: pending,
184
+ context: input.context,
185
+ input: 'g',
186
+ key: {},
187
+ now: input.now,
188
+ });
189
+ expect(result.actions).toEqual([]);
190
+ expect(result.state.strokes).toEqual(input.strokes);
191
+ }
192
+ });
193
+
194
+ it('derives user-facing hints only from effective active bindings', () => {
195
+ const parsed = parse({
196
+ version: 1,
197
+ bindings: { 'chat-input': { submit: 'ctrl+j', 'history-previous': null } },
198
+ });
199
+ expect(parsed.ok).toBe(true);
200
+ if (!parsed.ok) return;
201
+ expect(
202
+ keybindingHints(parsed.snapshot, 'chat-input', [
203
+ ['history-previous', 'Previous'],
204
+ ['submit', 'Submit'],
205
+ ]),
206
+ ).toEqual([{ keys: 'Ctrl+J', label: 'Submit' }]);
207
+ });
208
+
209
+ it('keeps the published JSON Schema aligned with every runtime context and action', () => {
210
+ const schemaPath = new URL(
211
+ '../../../../../apps/docs/public/schemas/keybindings.schema.json',
212
+ import.meta.url,
213
+ );
214
+ const schema = JSON.parse(readFileSync(schemaPath, 'utf8')) as {
215
+ $id: string;
216
+ properties: { bindings: { properties: Record<string, { $ref: string }> } };
217
+ $defs: Record<string, { properties?: Record<string, unknown> }>;
218
+ };
219
+
220
+ expect(schema.$id).toBe('https://docs.robota.io/schemas/keybindings.schema.json');
221
+ expect(Object.keys(schema.properties.bindings.properties)).toEqual(
222
+ Object.keys(KEYBINDING_ACTIONS),
223
+ );
224
+ for (const [context, actions] of Object.entries(KEYBINDING_ACTIONS)) {
225
+ expect(Object.keys(schema.$defs[context]?.properties ?? {})).toEqual(actions);
226
+ }
227
+ });
228
+ });
@@ -0,0 +1,118 @@
1
+ import { mkdtemp, readFile, rename, rm, writeFile } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+
5
+ import { afterEach, describe, expect, it, vi } from 'vitest';
6
+
7
+ import {
8
+ createNodeKeybindingsSource,
9
+ } from '../node-keybindings-source.js';
10
+
11
+ const roots: string[] = [];
12
+
13
+ async function temporaryRoot(): Promise<string> {
14
+ const root = await mkdtemp(join(tmpdir(), 'agent-test-keybindings-'));
15
+ roots.push(root);
16
+ return root;
17
+ }
18
+
19
+ async function eventually(assertion: () => void, timeout = 2_000): Promise<void> {
20
+ const deadline = Date.now() + timeout;
21
+ while (Date.now() < deadline) {
22
+ try {
23
+ assertion();
24
+ return;
25
+ } catch {
26
+ await new Promise((resolve) => setTimeout(resolve, 10));
27
+ }
28
+ }
29
+ assertion();
30
+ }
31
+
32
+ afterEach(async () => {
33
+ await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
34
+ });
35
+
36
+ describe('Node keybindings source', () => {
37
+ it('omits the schema when the host has no documentation URL', async () => {
38
+ const root = await temporaryRoot();
39
+ const source = createNodeKeybindingsSource({ filePath: join(root, 'keys.json') });
40
+ expect(JSON.parse(await readFile(await source.ensureFile(), 'utf8'))).toEqual({ version: 1, bindings: {} });
41
+ source.dispose();
42
+ });
43
+
44
+ it('uses only the host-selected file and schema when creating the sparse document', async () => {
45
+ const root = await temporaryRoot();
46
+ const filePath = join(root, 'host-config', 'keys.json');
47
+ const schemaUrl = 'https://example.invalid/schemas/keys.json';
48
+ const source = createNodeKeybindingsSource({ filePath, schemaUrl });
49
+ const firstPath = await source.ensureFile();
50
+ expect(firstPath).toBe(filePath);
51
+ expect(JSON.parse(await readFile(firstPath, 'utf8'))).toEqual({
52
+ $schema: schemaUrl,
53
+ version: 1,
54
+ bindings: {},
55
+ });
56
+
57
+ await writeFile(firstPath, '{"owned":true}\n', 'utf8');
58
+ expect(await source.ensureFile()).toBe(firstPath);
59
+ expect(await readFile(firstPath, 'utf8')).toBe('{"owned":true}\n');
60
+ source.dispose();
61
+ });
62
+
63
+ it('loads atomic replacements, retains last-valid bindings on invalid input, and disposes', async () => {
64
+ const root = await temporaryRoot();
65
+ const path = join(root, 'custom', 'keybindings.json');
66
+ const log = vi.fn();
67
+ const source = createNodeKeybindingsSource({
68
+ filePath: path,
69
+ schemaUrl: 'https://example.invalid/keys.json',
70
+ onDiagnostic: log,
71
+ });
72
+ await source.start();
73
+ expect(await source.ensureFile()).toBe(path);
74
+ const snapshots = [source.getSnapshot()];
75
+ const unsubscribe = source.subscribe((snapshot) => snapshots.push(snapshot));
76
+
77
+ const replacement = join(root, 'custom', 'keybindings.next.json');
78
+ await writeFile(
79
+ replacement,
80
+ JSON.stringify({ version: 1, bindings: { 'chat-input': { submit: 'ctrl+k' } } }),
81
+ 'utf8',
82
+ );
83
+ await rename(replacement, path);
84
+ await eventually(() => {
85
+ expect(source.getSnapshot().bindings['chat-input'].submit).toEqual(['ctrl+k']);
86
+ });
87
+ const valid = source.getSnapshot();
88
+
89
+ await writeFile(replacement, '{"version":1,"bindings":{"missing":{}}}', 'utf8');
90
+ await rename(replacement, path);
91
+ await eventually(() => {
92
+ expect(source.getSnapshot().diagnostic?.path).toBe('$.bindings.missing');
93
+ });
94
+ expect(source.getSnapshot().bindings).toBe(valid.bindings);
95
+ expect(source.getSnapshot().generation).toBeGreaterThan(valid.generation);
96
+ expect(log).toHaveBeenCalledWith(expect.objectContaining({ file: path }));
97
+
98
+ unsubscribe();
99
+ source.dispose();
100
+ const count = snapshots.length;
101
+ await writeFile(path, JSON.stringify({ version: 1, bindings: {} }), 'utf8');
102
+ await new Promise((resolve) => setTimeout(resolve, 40));
103
+ expect(snapshots).toHaveLength(count);
104
+ });
105
+
106
+ it('shares one start, and a failed start leaves no half-started source', async () => {
107
+ const root = await temporaryRoot();
108
+ const source = createNodeKeybindingsSource({
109
+ filePath: join(root, 'custom', 'keybindings.json'),
110
+ schemaUrl: 'https://example.invalid/keys.json',
111
+ });
112
+ await Promise.all([source.start(), source.start()]);
113
+ expect(source.isStarted()).toBe(true);
114
+ source.dispose();
115
+ expect(source.isStarted()).toBe(false);
116
+ await expect(source.start()).rejects.toThrow(/disposed/i);
117
+ });
118
+ });
@@ -0,0 +1,171 @@
1
+ export const KEYBINDING_ACTIONS = {
2
+ app: ['open-workspace-switcher'],
3
+ thinking: ['abort'],
4
+ 'background-detail': ['return-to-main'],
5
+ recovery: ['retry'],
6
+ 'chat-input': [
7
+ 'submit',
8
+ 'history-previous',
9
+ 'history-next',
10
+ 'history-search',
11
+ 'cursor-left',
12
+ 'cursor-right',
13
+ 'delete-backward',
14
+ 'delete-word',
15
+ 'delete-line',
16
+ ],
17
+ 'text-input': [
18
+ 'submit',
19
+ 'cursor-left',
20
+ 'cursor-right',
21
+ 'cursor-up',
22
+ 'cursor-down',
23
+ 'delete-backward',
24
+ 'delete-word',
25
+ 'delete-line',
26
+ ],
27
+ 'autocomplete-menu': ['previous', 'next', 'close', 'accept', 'execute'],
28
+ 'queued-prompt': ['cancel'],
29
+ 'confirm-prompt': ['previous', 'next', 'confirm', 'choose-yes', 'choose-no'],
30
+ 'permission-prompt': [
31
+ 'previous',
32
+ 'next',
33
+ 'confirm',
34
+ 'allow-once',
35
+ 'allow-session',
36
+ 'allow-project',
37
+ 'deny',
38
+ ],
39
+ 'text-prompt': ['submit', 'cancel', 'delete-backward'],
40
+ 'list-picker': ['previous', 'next', 'select', 'cancel'],
41
+ 'menu-select': ['previous', 'next', 'select', 'back'],
42
+ 'multi-select': ['previous', 'next', 'toggle', 'confirm', 'cancel'],
43
+ 'workspace-switcher': ['previous', 'next', 'select', 'close', 'attach'],
44
+ 'background-list': ['previous', 'next', 'open', 'close'],
45
+ 'transport-settings': ['previous', 'next', 'toggle', 'close'],
46
+ /** SCREEN-1993: the reverse prompt-history search overlay. */
47
+ 'history-search': ['previous', 'next', 'cycle-scope', 'insert', 'execute', 'cancel'],
48
+ /**
49
+ * SCREEN-2002: the theme picker. Navigation PREVIEWS; only `select` applies. Printable keys are
50
+ * fine here because the picker takes no text.
51
+ */
52
+ 'theme-picker': ['previous', 'next', 'select', 'toggle-syntax', 'toggle-motion', 'cancel'],
53
+ } as const;
54
+
55
+ export type TKeybindingContext = keyof typeof KEYBINDING_ACTIONS;
56
+ export type TKeybindingAction<TContext extends TKeybindingContext = TKeybindingContext> =
57
+ (typeof KEYBINDING_ACTIONS)[TContext][number];
58
+
59
+ type TDefaultKeybindings = {
60
+ readonly [TContext in TKeybindingContext]: Readonly<
61
+ Record<TKeybindingAction<TContext>, readonly string[]>
62
+ >;
63
+ };
64
+
65
+ export const DEFAULT_KEYBINDINGS = {
66
+ app: { 'open-workspace-switcher': ['ctrl+b'] },
67
+ thinking: { abort: ['escape'] },
68
+ 'background-detail': { 'return-to-main': ['escape'] },
69
+ recovery: { retry: ['enter'] },
70
+ 'chat-input': {
71
+ submit: ['enter'],
72
+ 'history-previous': ['up'],
73
+ 'history-next': ['down'],
74
+ 'history-search': ['ctrl+r'],
75
+ 'cursor-left': ['left'],
76
+ 'cursor-right': ['right'],
77
+ 'delete-backward': ['backspace', 'delete'],
78
+ 'delete-word': ['ctrl+w'],
79
+ 'delete-line': ['ctrl+u'],
80
+ },
81
+ 'text-input': {
82
+ submit: ['enter'],
83
+ 'cursor-left': ['left'],
84
+ 'cursor-right': ['right'],
85
+ 'cursor-up': ['up'],
86
+ 'cursor-down': ['down'],
87
+ 'delete-backward': ['backspace', 'delete'],
88
+ 'delete-word': ['ctrl+w'],
89
+ 'delete-line': ['ctrl+u'],
90
+ },
91
+ 'autocomplete-menu': {
92
+ previous: ['up'],
93
+ next: ['down'],
94
+ close: ['escape'],
95
+ accept: ['tab'],
96
+ execute: ['enter'],
97
+ },
98
+ 'queued-prompt': { cancel: ['backspace', 'delete'] },
99
+ 'confirm-prompt': {
100
+ previous: ['left', 'up'],
101
+ next: ['right', 'down'],
102
+ confirm: ['enter'],
103
+ 'choose-yes': ['y'],
104
+ 'choose-no': ['n'],
105
+ },
106
+ 'permission-prompt': {
107
+ previous: ['left', 'up'],
108
+ next: ['right', 'down'],
109
+ confirm: ['enter'],
110
+ 'allow-once': ['y'],
111
+ 'allow-session': ['s', 'a'],
112
+ 'allow-project': ['p'],
113
+ deny: ['n', 'd'],
114
+ },
115
+ 'text-prompt': {
116
+ submit: ['enter'],
117
+ cancel: ['escape'],
118
+ 'delete-backward': ['backspace', 'delete'],
119
+ },
120
+ 'list-picker': { previous: ['up'], next: ['down'], select: ['enter'], cancel: ['escape'] },
121
+ 'menu-select': { previous: ['up'], next: ['down'], select: ['enter'], back: ['escape'] },
122
+ 'multi-select': {
123
+ previous: ['up'],
124
+ next: ['down'],
125
+ toggle: ['space'],
126
+ confirm: ['enter'],
127
+ cancel: ['escape'],
128
+ },
129
+ 'workspace-switcher': {
130
+ previous: ['up'],
131
+ next: ['down'],
132
+ select: ['enter'],
133
+ close: ['ctrl+b', 'escape'],
134
+ attach: ['a'],
135
+ },
136
+ 'background-list': {
137
+ previous: ['up'],
138
+ next: ['down'],
139
+ open: ['enter'],
140
+ close: ['escape'],
141
+ },
142
+ 'transport-settings': {
143
+ previous: ['up'],
144
+ next: ['down'],
145
+ toggle: ['space'],
146
+ close: ['enter', 'escape'],
147
+ },
148
+ 'history-search': {
149
+ previous: ['up'],
150
+ next: ['down', 'ctrl+r'],
151
+ 'cycle-scope': ['ctrl+s'],
152
+ insert: ['enter', 'tab'],
153
+ execute: ['ctrl+e'],
154
+ cancel: ['escape'],
155
+ },
156
+ 'theme-picker': {
157
+ previous: ['up'],
158
+ next: ['down'],
159
+ select: ['enter'],
160
+ 'toggle-syntax': ['s'],
161
+ 'toggle-motion': ['m'],
162
+ cancel: ['escape'],
163
+ },
164
+ } as const satisfies TDefaultKeybindings;
165
+
166
+ export const TEXT_ENTRY_CONTEXTS: ReadonlySet<TKeybindingContext> = new Set([
167
+ 'chat-input',
168
+ 'text-input',
169
+ 'text-prompt',
170
+ 'history-search',
171
+ ]);