@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,336 @@
1
+ /**
2
+ * CJK-aware TextInput component for Ink.
3
+ *
4
+ * Replaces ink-text-input with proper wide character support:
5
+ * - Uses string-width for display width calculation
6
+ * - Cursor position based on character index (not display columns)
7
+ * - Renders CJK characters correctly (2 columns each)
8
+ * - Uses refs for value/cursor to avoid React state batching issues
9
+ * (IME sends multiple keystrokes synchronously, state updates are async)
10
+ *
11
+ * Drop-in replacement: same props as ink-text-input.
12
+ */
13
+
14
+ import chalk from 'chalk';
15
+ import { Box, usePaste } from 'ink';
16
+ import React, { useEffect, useMemo, useRef, useState } from 'react';
17
+
18
+ import {
19
+ applyCjkTextInput,
20
+ applyCjkTextPaste,
21
+ createCjkTextInputFlowState,
22
+ syncCjkTextInputFlowState,
23
+ type ICjkTextInputFlowState,
24
+ } from './flows/cjk-text-input-flow.js';
25
+ import {
26
+ cancelDeferredSubmit,
27
+ createDeferSubmitState,
28
+ scheduleDeferredSubmit,
29
+ type IDeferSubmitState,
30
+ } from './flows/defer-submit.js';
31
+ import { useRealCursorPosition } from './hooks/useRealCursorPosition.js';
32
+ import { useKeybindingActions } from './keybindings/keybindings-context.js';
33
+ import { RenderedText } from './SafeText.js';
34
+ import { sanitizeTerminalText } from './sanitize-terminal-text.js';
35
+ import { useScreenReaderPacing } from './screen-reader-pacing-context.js';
36
+ import { supportsImeCursorPositioning } from './terminal-capabilities.js';
37
+ import { useTerminalCapabilityOverrides } from './terminal-capabilities-context.js';
38
+ import { foreground, usePalette } from './theme/index.js';
39
+
40
+ import type { IKeyInput, TKeybindingContext } from './keybindings/keybinding-registry.js';
41
+ import type { DOMElement } from 'ink';
42
+
43
+ interface IProps {
44
+ value: string;
45
+ onChange: (value: string) => void;
46
+ onSubmit?: (value: string) => void;
47
+ onPaste?: (text: string, cursorPosition: number) => void;
48
+ placeholder?: string;
49
+ focus?: boolean;
50
+ showCursor?: boolean;
51
+ /** Available width in columns for visual line wrapping navigation */
52
+ availableWidth?: number;
53
+ /** Cursor position hint for external value changes. null = end (default). */
54
+ cursorHint?: number | null;
55
+ /** When false, parent flows own up/down arrow behavior. */
56
+ enableVerticalNavigation?: boolean;
57
+ /**
58
+ * CLI-2004: called with the text a word/line delete removed. A reader announces what is LEFT on
59
+ * the line, so the removed text is the one thing it cannot tell you.
60
+ */
61
+ onDeletedText?: (deleted: string) => void;
62
+ /** Semantic key context; autocomplete consumes its own actions without invoking text actions. */
63
+ keybindingContext?: 'chat-input' | 'text-input' | 'autocomplete-menu';
64
+ }
65
+
66
+ interface IInputHandlerOptions {
67
+ stateRef: React.MutableRefObject<ICjkTextInputFlowState>;
68
+ onChange: (value: string) => void;
69
+ onSubmit?: (value: string) => void;
70
+ onPaste?: (text: string, cursorPosition: number) => void;
71
+ availableWidth?: number;
72
+ focus: boolean;
73
+ enableVerticalNavigation: boolean;
74
+ onDeletedText?: (deleted: string) => void;
75
+ forceRender: React.Dispatch<React.SetStateAction<number>>;
76
+ /** CLI-061: deferred-submit state (timer + submit guard). The input pipeline stays live during the window. */
77
+ deferState: IDeferSubmitState;
78
+ keybindingContext: 'chat-input' | 'text-input' | 'autocomplete-menu';
79
+ }
80
+
81
+ export default function CjkTextInput({
82
+ value,
83
+ onChange,
84
+ onSubmit,
85
+ onPaste,
86
+ placeholder = '',
87
+ focus = true,
88
+ showCursor = true,
89
+ availableWidth,
90
+ cursorHint = null,
91
+ enableVerticalNavigation = true,
92
+ onDeletedText,
93
+ keybindingContext = 'text-input',
94
+ }: IProps): React.ReactElement {
95
+ const stateRef = useRef<ICjkTextInputFlowState>(createCjkTextInputFlowState(value));
96
+ const [, forceRender] = useState(0);
97
+ // CLI-061: deferred-submit state so a trailing IME character (a stdin event arriving just after Enter) is
98
+ // included in the submitted value. The timer is cancelled on unmount so no submit fires after teardown.
99
+ const deferRef = useRef<IDeferSubmitState>(createDeferSubmitState());
100
+ useEffect(() => () => cancelDeferredSubmit(deferRef.current), []);
101
+
102
+ // Sync ref when value changes from parent (e.g., setValue(''), tab completion, paste)
103
+ stateRef.current = syncCjkTextInputFlowState(stateRef.current, value, cursorHint);
104
+
105
+ useCjkTextInputHandlers({
106
+ stateRef,
107
+ onChange,
108
+ onSubmit,
109
+ onPaste,
110
+ availableWidth,
111
+ focus,
112
+ enableVerticalNavigation,
113
+ ...(onDeletedText !== undefined ? { onDeletedText } : {}),
114
+ forceRender,
115
+ deferState: deferRef.current,
116
+ keybindingContext,
117
+ });
118
+
119
+ // CLI-062: real terminal cursor positioning so the OS IME composition window appears AT the
120
+ // input position. The historical Terminal.app SIGSEGV came from a hardcoded `y: 0` (logo area);
121
+ // the hook only positions with a y measured from the live yoga layout and refuses the ink
122
+ // fullscreen geometry (invariants I1–I5 — see flows/real-cursor-flow.ts and the hook itself).
123
+ // SCREEN-2002: the placeholder's muted colour comes from the resolved theme, not a chalk literal.
124
+ const palette = usePalette();
125
+ const mutedPlaceholder = useMemo(() => foreground(palette.text.muted), [palette.text.muted]);
126
+ const boxRef = useRef<DOMElement | null>(null);
127
+ const terminalCapabilities = useTerminalCapabilityOverrides();
128
+ const { realCursorActive } = useRealCursorPosition({
129
+ boxRef,
130
+ enabled:
131
+ focus &&
132
+ showCursor &&
133
+ supportsImeCursorPositioning({ override: terminalCapabilities.imeCursorPositioning }),
134
+ value: stateRef.current.value,
135
+ cursor: stateRef.current.cursor,
136
+ ...(availableWidth !== undefined ? { availableWidth } : {}),
137
+ });
138
+
139
+ return (
140
+ <Box ref={boxRef}>
141
+ <RenderedText>
142
+ {renderWithCursor(
143
+ stateRef.current.value,
144
+ stateRef.current.cursor,
145
+ placeholder,
146
+ // I4: the drawn inverse cursor is suppressed ONLY while real positioning is active;
147
+ // any guard failure falls back to exactly today's rendering.
148
+ showCursor && focus && !realCursorActive,
149
+ mutedPlaceholder,
150
+ )}
151
+ </RenderedText>
152
+ </Box>
153
+ );
154
+ }
155
+
156
+ /**
157
+ * SCREEN-2670: which keys arm the echo exemption. Text insertion and deletion only — a keystroke
158
+ * whose commit repaints the composer. Submit and execute run through this same handler
159
+ * (`translateCjkInput` returns `key.return` for them) and their commit appends the user's prompt
160
+ * line to the transcript, which is exactly the content the park exists to pace; cursor moves and
161
+ * tab commit no text of the composer's own.
162
+ */
163
+ function mutatesComposerText(
164
+ action: string | undefined,
165
+ translated: { input: string; key: IKeyInput },
166
+ ): boolean {
167
+ if (action === 'delete-backward' || action === 'delete-word' || action === 'delete-line')
168
+ return true;
169
+ return (
170
+ translated.key.return !== true &&
171
+ translated.key.ctrl !== true &&
172
+ translated.key.tab !== true &&
173
+ translated.input.length > 0
174
+ );
175
+ }
176
+
177
+ function useCjkTextInputHandlers(options: IInputHandlerOptions): void {
178
+ const pacing = useScreenReaderPacing();
179
+ usePaste(
180
+ (text) => {
181
+ pacing.armEchoRelease();
182
+ applyCjkFlowSafely(options, () =>
183
+ applyCjkTextPaste(options.stateRef.current, text, createFlowOptions(options)),
184
+ );
185
+ },
186
+ { isActive: options.focus },
187
+ );
188
+
189
+ useKeybindingActions(
190
+ options.keybindingContext,
191
+ (actions, input, key, consumed, resolvedContext) => {
192
+ const translated = translateCjkInput(resolvedContext, actions, input, key, consumed);
193
+ if (translated === undefined) return;
194
+ if (mutatesComposerText(actions[0], translated)) pacing.armEchoRelease();
195
+ applyCjkFlowSafely(options, () =>
196
+ applyCjkTextInput(
197
+ options.stateRef.current,
198
+ translated.input,
199
+ translated.key,
200
+ createFlowOptions(options),
201
+ ),
202
+ );
203
+ },
204
+ {
205
+ isActive: options.focus,
206
+ // #3125: the menu owns only its own keys; editing keys keep the chat input's bindings.
207
+ ...(options.keybindingContext === 'autocomplete-menu'
208
+ ? { fallbackContext: 'chat-input' as const }
209
+ : {}),
210
+ },
211
+ );
212
+ }
213
+
214
+ function translateCjkInput(
215
+ context: TKeybindingContext,
216
+ actions: readonly string[],
217
+ input: string,
218
+ key: IKeyInput,
219
+ consumed: boolean,
220
+ ): { input: string; key: IKeyInput } | undefined {
221
+ const action = actions[0];
222
+ if (action === 'submit' || action === 'execute') return { input: '', key: { return: true } };
223
+ if (context === 'autocomplete-menu' && consumed) return undefined;
224
+ if (action === 'cursor-left') return { input: '', key: { leftArrow: true } };
225
+ if (action === 'cursor-right') return { input: '', key: { rightArrow: true } };
226
+ if (action === 'cursor-up') return { input: '', key: { upArrow: true } };
227
+ if (action === 'cursor-down') return { input: '', key: { downArrow: true } };
228
+ if (action === 'delete-backward') return { input: '', key: { backspace: true } };
229
+ if (action === 'delete-word') return { input: 'w', key: { ctrl: true } };
230
+ if (action === 'delete-line') return { input: 'u', key: { ctrl: true } };
231
+ if (consumed) return undefined;
232
+ const rawText =
233
+ key.ctrl !== true &&
234
+ key.meta !== true &&
235
+ !key.return &&
236
+ !key.escape &&
237
+ !key.tab &&
238
+ !key.upArrow &&
239
+ !key.downArrow &&
240
+ !key.leftArrow &&
241
+ !key.rightArrow &&
242
+ !key.backspace &&
243
+ !key.delete;
244
+ return rawText || (key.ctrl === true && input === 'c') || key.tab ? { input, key } : undefined;
245
+ }
246
+
247
+ function createFlowOptions(options: IInputHandlerOptions): {
248
+ availableWidth?: number;
249
+ canPaste: boolean;
250
+ enableVerticalNavigation: boolean;
251
+ } {
252
+ return {
253
+ availableWidth: options.availableWidth,
254
+ canPaste: options.onPaste !== undefined,
255
+ enableVerticalNavigation: options.enableVerticalNavigation,
256
+ };
257
+ }
258
+
259
+ function applyCjkFlowSafely(
260
+ options: IInputHandlerOptions,
261
+ run: () => ReturnType<typeof applyCjkTextInput>,
262
+ ): void {
263
+ try {
264
+ const result = run();
265
+ options.stateRef.current = result.state;
266
+ applyCjkTextInputEffect(options, result.effect);
267
+ } catch {
268
+ // allow-fallback: Korean IME in raw mode can produce unexpected byte sequences
269
+ }
270
+ }
271
+
272
+ function applyCjkTextInputEffect(
273
+ options: IInputHandlerOptions,
274
+ effect: ReturnType<typeof applyCjkTextInput>['effect'],
275
+ ): void {
276
+ if (effect.type === 'change') {
277
+ if (effect.deleted !== undefined) options.onDeletedText?.(effect.deleted);
278
+ options.onChange(effect.value);
279
+ } else if (effect.type === 'submit') {
280
+ // CLI-061: DEFER the submit and re-read the LIVE `stateRef.current.value` at fire time — never the stale
281
+ // `effect.value` captured at Enter. The input pipeline (this same handler) stays live during the window, so
282
+ // a trailing IME character's stdin event is applied to `stateRef` before the deferred read. The guard only
283
+ // blocks a SECOND submit — it must not gate the input pipeline (that would drop the trailing char).
284
+ const onSubmit = options.onSubmit;
285
+ if (onSubmit) {
286
+ scheduleDeferredSubmit(options.deferState, () => options.stateRef.current.value, onSubmit);
287
+ }
288
+ } else if (effect.type === 'paste') {
289
+ options.onPaste?.(effect.text, effect.cursor);
290
+ } else if (effect.type === 'render') {
291
+ options.forceRender((n) => n + 1);
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Render text with an inverse-style cursor at the correct position.
297
+ *
298
+ * #2222: the input is sanitized BEFORE the cursor is drawn, and the result renders through
299
+ * `RenderedText` — the SGR here (inverse cursor, muted placeholder) is this component's own styling,
300
+ * and `SafeText` would strip it along with everything else, which is exactly how the drawn cursor
301
+ * disappeared (CLI-062 byte-identical rendering).
302
+ */
303
+ function renderWithCursor(
304
+ rawValue: string,
305
+ cursorOffset: number,
306
+ rawPlaceholder: string,
307
+ showCursor: boolean,
308
+ mutedPlaceholder: (text: string) => string,
309
+ ): string {
310
+ const value = sanitizeTerminalText(rawValue);
311
+ const placeholder = sanitizeTerminalText(rawPlaceholder);
312
+ if (!showCursor) {
313
+ return value.length > 0 ? value : placeholder ? mutedPlaceholder(placeholder) : '';
314
+ }
315
+
316
+ if (value.length === 0) {
317
+ if (placeholder.length > 0) {
318
+ return chalk.inverse(placeholder[0]) + mutedPlaceholder(placeholder.slice(1));
319
+ }
320
+ return chalk.inverse(' ');
321
+ }
322
+
323
+ const chars = [...value];
324
+ let rendered = '';
325
+
326
+ for (let i = 0; i < chars.length; i++) {
327
+ const char = chars[i] ?? '';
328
+ rendered += i === cursorOffset ? chalk.inverse(char) : char;
329
+ }
330
+
331
+ if (cursorOffset >= chars.length) {
332
+ rendered += chalk.inverse(' ');
333
+ }
334
+
335
+ return rendered;
336
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Reusable confirmation prompt with arrow-key selection.
3
+ * Used by model change, permission prompts, and other yes/no confirmations.
4
+ */
5
+
6
+ import { Box, useInput } from 'ink';
7
+ import React, { useState, useRef, useCallback } from 'react';
8
+
9
+ import {
10
+ applyConfirmPromptInput,
11
+ applyTypedConfirmInput,
12
+ createTypedConfirmState,
13
+ type ITypedConfirmState,
14
+ type TConfirmPromptInputAction,
15
+ } from './flows/confirm-prompt-flow.js';
16
+ import { createSelectionFlowState, type ISelectionFlowState } from './flows/selection-flow.js';
17
+ import {
18
+ KeyHintFooter,
19
+ SELECTION_INDICATOR,
20
+ SELECTION_INDICATOR_NONE,
21
+ type IKeyHint,
22
+ } from './key-hint-footer.js';
23
+ import { useKeybindingActions, useKeybindingHints } from './keybindings/keybindings-context.js';
24
+ import { Text } from './SafeText.js';
25
+ import { useScreenReader } from './screen-reader-context.js';
26
+ import { usePalette } from './theme/index.js';
27
+
28
+ /** The typed-answer prompt. Authored here beside the reducer that accepts it. */
29
+ export const CONFIRM_PROMPT_TYPED_LITERAL = 'Answer y or n and press Enter';
30
+
31
+ /**
32
+ * Footer for the confirm prompt. Names the canonical keys for a horizontal row (←→; the reducer
33
+ * also accepts ↑↓ as aliases). Esc is deliberately suppressed by the flow (`escape: false` — the
34
+ * prompt must resolve explicitly), so per the affordance contract the footer omits it.
35
+ */
36
+ export const CONFIRM_PROMPT_FOOTER_HINTS: readonly IKeyHint[] = [
37
+ { keys: '←→', label: 'Navigate' },
38
+ { keys: 'Enter', label: 'Confirm' },
39
+ ];
40
+
41
+ interface IProps {
42
+ /** Message to display above the options */
43
+ message: string;
44
+ /** Options to select from (default: ['Yes', 'No']) */
45
+ options?: string[];
46
+ /** Callback with the selected index */
47
+ onSelect: (index: number) => void;
48
+ }
49
+
50
+ export default function ConfirmPrompt({
51
+ message,
52
+ options = ['Yes', 'No'],
53
+ onSelect,
54
+ }: IProps): React.ReactElement {
55
+ const palette = usePalette();
56
+ const [state, setState] = useState<ISelectionFlowState>(() => createSelectionFlowState());
57
+ const stateRef = useRef(state);
58
+ const applyAction = useCallback(
59
+ (action: TConfirmPromptInputAction): void => {
60
+ const result = applyConfirmPromptInput(stateRef.current, action, options.length);
61
+ stateRef.current = result.state;
62
+ setState(result.state);
63
+ if (result.effect.type === 'select') {
64
+ onSelect(result.effect.index);
65
+ }
66
+ },
67
+ [onSelect, options.length],
68
+ );
69
+
70
+ const screenReader = useScreenReader();
71
+ const [typed, setTyped] = useState<ITypedConfirmState>(createTypedConfirmState);
72
+ const typedRef = useRef(typed);
73
+
74
+ useInput(
75
+ (input, key) => {
76
+ const result = applyTypedConfirmInput(typedRef.current, input, key);
77
+ typedRef.current = result.state;
78
+ setTyped(result.state);
79
+ if (result.effect.type === 'select') onSelect(result.effect.index);
80
+ },
81
+ { isActive: screenReader },
82
+ );
83
+
84
+ useKeybindingActions(
85
+ 'confirm-prompt',
86
+ (actions) => {
87
+ for (const action of actions) {
88
+ if (action === 'choose-yes' && options.length === 2) {
89
+ applyAction({ type: 'shortcut', index: 0 });
90
+ } else if (action === 'choose-no' && options.length === 2) {
91
+ applyAction({ type: 'shortcut', index: 1 });
92
+ } else if (action === 'previous' || action === 'next' || action === 'confirm') {
93
+ applyAction(action === 'confirm' ? 'select' : action);
94
+ }
95
+ }
96
+ },
97
+ { isActive: !screenReader },
98
+ );
99
+ const footerHints = useKeybindingHints('confirm-prompt', [
100
+ [['previous', 'next'], 'Navigate'],
101
+ ['confirm', 'Confirm'],
102
+ ]);
103
+
104
+ if (screenReader) {
105
+ return (
106
+ <Box flexDirection="column">
107
+ <Text>{message}</Text>
108
+ <Text>
109
+ {CONFIRM_PROMPT_TYPED_LITERAL}
110
+ {typed.buffer.length > 0 ? ` ${typed.buffer}` : ''}
111
+ </Text>
112
+ </Box>
113
+ );
114
+ }
115
+
116
+ return (
117
+ <Box
118
+ flexDirection="column"
119
+ borderStyle="round"
120
+ borderColor={palette.border.attention}
121
+ paddingX={1}
122
+ >
123
+ <Text color={palette.text.warning}>{message}</Text>
124
+ <Box marginTop={1}>
125
+ {options.map((opt, i) => (
126
+ <Box key={opt} marginRight={2}>
127
+ <Text
128
+ color={i === state.selectedIndex ? palette.text.accent : undefined}
129
+ bold={i === state.selectedIndex}
130
+ >
131
+ {i === state.selectedIndex ? SELECTION_INDICATOR : SELECTION_INDICATOR_NONE}
132
+ {opt}
133
+ </Text>
134
+ </Box>
135
+ ))}
136
+ </Box>
137
+ <KeyHintFooter hints={footerHints} />
138
+ </Box>
139
+ );
140
+ }
@@ -0,0 +1,45 @@
1
+ import { Box } from 'ink';
2
+ import React from 'react';
3
+
4
+ import { Text } from './SafeText.js';
5
+ import { useScreenReader } from './screen-reader-context.js';
6
+ import { usePalette } from './theme/index.js';
7
+
8
+ interface IProps {
9
+ percentage: number;
10
+ }
11
+
12
+ const COMPACT_SUGGESTION_THRESHOLD = 70;
13
+ const CRITICAL_THRESHOLD = 90;
14
+
15
+ export function ContextWarningBanner({ percentage }: IProps): React.ReactElement | null {
16
+ const palette = usePalette();
17
+ const screenReader = useScreenReader();
18
+ if (percentage >= CRITICAL_THRESHOLD) {
19
+ return (
20
+ <Box
21
+ {...(screenReader
22
+ ? {}
23
+ : { borderStyle: 'single' as const, borderColor: palette.border.error })}
24
+ paddingX={1}
25
+ >
26
+ <Text color={palette.text.error} bold>
27
+ ⚠ Context at {Math.round(percentage)}% — window nearly full. Run /compact to summarize the
28
+ conversation.
29
+ </Text>
30
+ </Box>
31
+ );
32
+ }
33
+
34
+ if (percentage >= COMPACT_SUGGESTION_THRESHOLD) {
35
+ return (
36
+ <Box paddingX={1}>
37
+ <Text color={palette.text.warning}>
38
+ Context at {Math.round(percentage)}% — consider running /compact to free up space.
39
+ </Text>
40
+ </Box>
41
+ );
42
+ }
43
+
44
+ return null;
45
+ }
@@ -0,0 +1,85 @@
1
+ import { Box } from 'ink';
2
+ import React from 'react';
3
+
4
+ import {
5
+ formatExecutionDetailRecord,
6
+ formatExecutionWorkspaceEntryRow,
7
+ } from './execution-workspace-view-model.js';
8
+ import { Text } from './SafeText.js';
9
+ import { STATUS_SYMBOL, statusGlyphColor } from './status-glyph.js';
10
+ import { usePalette } from './theme/index.js';
11
+
12
+ import type { IThemeColors } from './theme/index.js';
13
+ import type {
14
+ IExecutionDetailPage,
15
+ IExecutionWorkspaceEntry,
16
+ TExecutionDetailRecordKind,
17
+ } from '@robota-sdk/agent-interface-execution';
18
+
19
+ const MAX_VISIBLE_DETAIL_RECORDS = 12;
20
+
21
+ interface IProps {
22
+ entry: IExecutionWorkspaceEntry;
23
+ page: IExecutionDetailPage | null;
24
+ loading?: boolean;
25
+ error?: string;
26
+ }
27
+
28
+ export default function ExecutionWorkspaceDetailPane({
29
+ entry,
30
+ page,
31
+ loading,
32
+ error,
33
+ }: IProps): React.ReactElement {
34
+ const palette = usePalette();
35
+ const row = formatExecutionWorkspaceEntryRow(entry, { selectedEntryId: entry.id });
36
+ const records = page?.records.slice(-MAX_VISIBLE_DETAIL_RECORDS) ?? [];
37
+
38
+ return (
39
+ <Box flexDirection="column" marginBottom={1}>
40
+ <Text color={palette.text.accent} bold>
41
+ {`Viewing ${row.title}`}
42
+ </Text>
43
+ <Text dimColor>
44
+ {row.statusLabel}
45
+ {row.subtitle ? ` · ${row.subtitle}` : ''}
46
+ {row.preview ? ` · ${row.preview}` : ''}
47
+ </Text>
48
+ {loading ? <Text dimColor>Loading workspace detail...</Text> : null}
49
+ {error ? <Text color={palette.text.error}>{error}</Text> : null}
50
+ {!loading && !error && records.length === 0 ? <Text dimColor>No detail yet</Text> : null}
51
+ {!loading &&
52
+ !error &&
53
+ records.map((record) => {
54
+ const { symbol, color } = getDetailRecordGlyph(record.kind, palette);
55
+ return (
56
+ <Text key={record.id} color={color}>
57
+ {symbol ? `${symbol} ` : ''}
58
+ {formatExecutionDetailRecord(record)}
59
+ </Text>
60
+ );
61
+ })}
62
+ {page?.nextCursor ? <Text dimColor>... more detail available</Text> : null}
63
+ </Box>
64
+ );
65
+ }
66
+
67
+ /**
68
+ * Symbol + color per detail-record kind. A symbol always accompanies the color
69
+ * so status is legible without color (SCREEN-005 — no color-only encoding).
70
+ */
71
+ function getDetailRecordGlyph(
72
+ kind: TExecutionDetailRecordKind,
73
+ palette: IThemeColors,
74
+ ): {
75
+ symbol: string;
76
+ color: string | undefined;
77
+ } {
78
+ if (kind === 'error')
79
+ return { symbol: STATUS_SYMBOL.error, color: statusGlyphColor(palette, 'error') };
80
+ if (kind === 'result')
81
+ return { symbol: STATUS_SYMBOL.success, color: statusGlyphColor(palette, 'success') };
82
+ if (kind === 'group_summary') return { symbol: '▸', color: palette.text.accent };
83
+ if (kind === 'process_output') return { symbol: '·', color: palette.text.emphasis };
84
+ return { symbol: '', color: undefined };
85
+ }