mixdog 0.8.1 → 0.9.1

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 (299) hide show
  1. package/README.md +47 -23
  2. package/package.json +34 -28
  3. package/scripts/_test-folder-dialog.mjs +30 -0
  4. package/scripts/agent-parallel-smoke.mjs +388 -0
  5. package/scripts/agent-tag-reuse-smoke.mjs +183 -0
  6. package/scripts/background-task-meta-smoke.mjs +38 -0
  7. package/scripts/boot-smoke.mjs +52 -9
  8. package/scripts/build-runtime-linux.sh +348 -0
  9. package/scripts/build-runtime-macos.sh +217 -0
  10. package/scripts/build-runtime-windows.ps1 +242 -0
  11. package/scripts/compact-active-turn-test.mjs +68 -0
  12. package/scripts/compact-smoke.mjs +859 -129
  13. package/scripts/compact-trigger-migration-smoke.mjs +187 -0
  14. package/scripts/fix-brief-fn.mjs +35 -0
  15. package/scripts/fix-format-tool-surface.mjs +24 -0
  16. package/scripts/fix-tool-exec-visible.mjs +42 -0
  17. package/scripts/generate-runtime-manifest.mjs +166 -0
  18. package/scripts/hook-bus-test.mjs +330 -0
  19. package/scripts/lead-workflow-smoke.mjs +33 -39
  20. package/scripts/live-worker-smoke.mjs +43 -37
  21. package/scripts/llm-trace-summary.mjs +315 -0
  22. package/scripts/memory-meta-concurrency-test.mjs +20 -0
  23. package/scripts/output-style-smoke.mjs +56 -15
  24. package/scripts/parent-abort-link-test.mjs +44 -0
  25. package/scripts/patch-agent-brief.mjs +48 -0
  26. package/scripts/patch-app.mjs +21 -0
  27. package/scripts/patch-app2.mjs +18 -0
  28. package/scripts/patch-dist-brief.mjs +96 -0
  29. package/scripts/patch-tool-exec.mjs +70 -0
  30. package/scripts/pretool-ask-runtime-test.mjs +54 -0
  31. package/scripts/provider-toolcall-test.mjs +376 -0
  32. package/scripts/reactive-compact-persist-smoke.mjs +124 -0
  33. package/scripts/sanitize-tool-pairs-test.mjs +260 -0
  34. package/scripts/session-context-bench.mjs +205 -33
  35. package/scripts/session-ingest-smoke.mjs +177 -0
  36. package/scripts/set-effort-config-test.mjs +41 -0
  37. package/scripts/smoke-runtime-negative.ps1 +106 -0
  38. package/scripts/smoke-runtime-negative.sh +97 -0
  39. package/scripts/smoke.mjs +25 -0
  40. package/scripts/tool-result-hook-test.mjs +48 -0
  41. package/scripts/tool-smoke.mjs +1222 -90
  42. package/scripts/toolcall-args-test.mjs +150 -0
  43. package/scripts/tui-background-failure-smoke.mjs +73 -0
  44. package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
  45. package/src/agents/debugger/AGENT.md +8 -0
  46. package/src/agents/explore/AGENT.md +4 -0
  47. package/src/agents/heavy-worker/AGENT.md +9 -3
  48. package/src/agents/maintainer/AGENT.md +4 -0
  49. package/src/agents/reviewer/AGENT.md +8 -0
  50. package/src/agents/scheduler-task/AGENT.md +12 -0
  51. package/src/agents/scheduler-task/agent.json +6 -0
  52. package/src/agents/webhook-handler/AGENT.md +12 -0
  53. package/src/agents/webhook-handler/agent.json +6 -0
  54. package/src/agents/worker/AGENT.md +9 -3
  55. package/src/app.mjs +77 -3
  56. package/src/defaults/hidden-roles.json +17 -12
  57. package/src/headless-role.mjs +117 -0
  58. package/src/help.mjs +30 -0
  59. package/src/hooks/lib/permission-evaluator.cjs +11 -475
  60. package/src/lib/keychain-cjs.cjs +9 -1
  61. package/src/lib/mixdog-debug.cjs +0 -29
  62. package/src/lib/plugin-paths.cjs +1 -7
  63. package/src/lib/rules-builder.cjs +242 -98
  64. package/src/lib/text-utils.cjs +1 -1
  65. package/src/mixdog-session-runtime.mjs +2211 -446
  66. package/src/output-styles/default.md +12 -28
  67. package/src/output-styles/extreme-simple.md +9 -6
  68. package/src/output-styles/simple.md +22 -9
  69. package/src/repl.mjs +108 -59
  70. package/src/rules/agent/00-common.md +15 -0
  71. package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
  72. package/src/rules/agent/30-explorer.md +22 -0
  73. package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
  74. package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
  75. package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
  76. package/src/rules/lead/01-general.md +9 -5
  77. package/src/rules/lead/04-workflow.md +51 -12
  78. package/src/rules/lead/lead-tool.md +6 -0
  79. package/src/rules/shared/01-tool.md +12 -1
  80. package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
  81. package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
  82. package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
  83. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
  84. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
  85. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
  86. package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
  87. package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
  88. package/src/runtime/agent/orchestrator/config.mjs +174 -55
  89. package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
  90. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
  91. package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
  92. package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
  93. package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
  94. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
  95. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +380 -268
  96. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +131 -93
  97. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
  98. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
  99. package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
  100. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
  101. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
  102. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
  103. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
  104. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
  105. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
  106. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
  107. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +203 -71
  108. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +512 -312
  109. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -518
  110. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
  111. package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
  112. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +289 -11
  113. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
  114. package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
  115. package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
  116. package/src/runtime/agent/orchestrator/session/loop.mjs +840 -612
  117. package/src/runtime/agent/orchestrator/session/manager.mjs +1574 -459
  118. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
  119. package/src/runtime/agent/orchestrator/session/store.mjs +67 -29
  120. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
  121. package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
  122. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
  123. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +35 -8
  124. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -8
  125. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -53
  126. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
  127. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
  128. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
  129. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
  130. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
  131. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
  132. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
  133. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
  134. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
  135. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
  136. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
  137. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
  138. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
  139. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
  140. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
  141. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
  142. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
  143. package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
  144. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
  145. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
  146. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
  147. package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
  148. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
  149. package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
  150. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
  151. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
  152. package/src/runtime/channels/index.mjs +149 -51
  153. package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
  154. package/src/runtime/channels/lib/config.mjs +0 -1
  155. package/src/runtime/channels/lib/drop-trace.mjs +1 -1
  156. package/src/runtime/channels/lib/executor.mjs +0 -3
  157. package/src/runtime/channels/lib/memory-client.mjs +0 -38
  158. package/src/runtime/channels/lib/output-forwarder.mjs +1 -8
  159. package/src/runtime/channels/lib/runtime-paths.mjs +0 -6
  160. package/src/runtime/channels/lib/scheduler.mjs +18 -14
  161. package/src/runtime/channels/lib/session-discovery.mjs +3 -6
  162. package/src/runtime/channels/lib/tool-format.mjs +0 -1
  163. package/src/runtime/channels/lib/transcript-discovery.mjs +4 -12
  164. package/src/runtime/channels/lib/webhook.mjs +1 -1
  165. package/src/runtime/channels/tool-defs.mjs +29 -29
  166. package/src/runtime/lib/keychain-cjs.cjs +0 -1
  167. package/src/runtime/memory/data/runtime-manifest.json +6 -7
  168. package/src/runtime/memory/index.mjs +519 -118
  169. package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
  170. package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
  171. package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
  172. package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
  173. package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
  174. package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
  175. package/src/runtime/memory/lib/memory-cycle1.mjs +28 -7
  176. package/src/runtime/memory/lib/memory-cycle2.mjs +4 -5
  177. package/src/runtime/memory/lib/memory-cycle3.mjs +2 -2
  178. package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -2
  179. package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
  180. package/src/runtime/memory/lib/memory.mjs +88 -9
  181. package/src/runtime/memory/lib/model-profile.mjs +1 -1
  182. package/src/runtime/memory/lib/pg/adapter.mjs +1 -1
  183. package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
  184. package/src/runtime/memory/lib/runtime-fetcher.mjs +80 -21
  185. package/src/runtime/memory/lib/session-ingest.mjs +196 -0
  186. package/src/runtime/memory/lib/trace-store.mjs +96 -51
  187. package/src/runtime/memory/tool-defs.mjs +46 -37
  188. package/src/runtime/search/index.mjs +104 -473
  189. package/src/runtime/search/lib/config.mjs +0 -4
  190. package/src/runtime/search/lib/state.mjs +1 -15
  191. package/src/runtime/search/lib/web-tools.mjs +44 -25
  192. package/src/runtime/search/tool-defs.mjs +16 -23
  193. package/src/runtime/shared/abort-controller.mjs +1 -1
  194. package/src/runtime/shared/atomic-file.mjs +4 -3
  195. package/src/runtime/shared/background-tasks.mjs +122 -11
  196. package/src/runtime/shared/child-spawn-gate.mjs +139 -0
  197. package/src/runtime/shared/config.mjs +7 -4
  198. package/src/runtime/shared/err-text.mjs +131 -4
  199. package/src/runtime/shared/llm/cost.mjs +2 -2
  200. package/src/runtime/shared/llm/http-agent.mjs +23 -7
  201. package/src/runtime/shared/llm/index.mjs +34 -11
  202. package/src/runtime/shared/llm/usage-log.mjs +4 -4
  203. package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
  204. package/src/runtime/shared/singleton-owner.mjs +104 -0
  205. package/src/runtime/shared/tool-execution-contract.mjs +199 -20
  206. package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
  207. package/src/runtime/shared/tool-surface.mjs +624 -95
  208. package/src/runtime/shared/user-data-guard.mjs +0 -2
  209. package/src/standalone/agent-task-status.mjs +203 -0
  210. package/src/standalone/agent-task-status.test.mjs +76 -0
  211. package/src/standalone/agent-tool.mjs +1913 -0
  212. package/src/standalone/channel-worker.mjs +370 -14
  213. package/src/standalone/explore-tool.mjs +165 -70
  214. package/src/standalone/folder-dialog.mjs +314 -0
  215. package/src/standalone/hook-bus.mjs +898 -22
  216. package/src/standalone/memory-runtime-proxy.mjs +320 -0
  217. package/src/standalone/projects.mjs +226 -0
  218. package/src/standalone/provider-admin.mjs +41 -24
  219. package/src/standalone/seeds.mjs +1 -78
  220. package/src/standalone/usage-dashboard.mjs +96 -8
  221. package/src/tui/App.jsx +4800 -2140
  222. package/src/tui/components/AnsiText.jsx +39 -28
  223. package/src/tui/components/ContextPanel.jsx +14 -4
  224. package/src/tui/components/Markdown.jsx +43 -77
  225. package/src/tui/components/MarkdownTable.jsx +9 -184
  226. package/src/tui/components/Message.jsx +28 -11
  227. package/src/tui/components/Picker.jsx +95 -56
  228. package/src/tui/components/PromptInput.jsx +428 -239
  229. package/src/tui/components/QueuedCommands.jsx +1 -1
  230. package/src/tui/components/SlashCommandPalette.jsx +27 -21
  231. package/src/tui/components/Spinner.jsx +67 -38
  232. package/src/tui/components/StatusLine.jsx +606 -38
  233. package/src/tui/components/TextEntryPanel.jsx +128 -9
  234. package/src/tui/components/ToolExecution.jsx +597 -362
  235. package/src/tui/components/TurnDone.jsx +3 -3
  236. package/src/tui/components/UsagePanel.jsx +3 -5
  237. package/src/tui/components/tool-output-format.mjs +499 -0
  238. package/src/tui/components/tool-output-format.test.mjs +312 -0
  239. package/src/tui/dist/index.mjs +9192 -2399
  240. package/src/tui/engine-runtime-notification.test.mjs +115 -0
  241. package/src/tui/engine-tool-result-text.test.mjs +75 -0
  242. package/src/tui/engine.mjs +1455 -279
  243. package/src/tui/figures.mjs +21 -40
  244. package/src/tui/index.jsx +75 -31
  245. package/src/tui/input-editing.mjs +25 -0
  246. package/src/tui/markdown/format-token.mjs +671 -69
  247. package/src/tui/markdown/format-token.test.mjs +312 -0
  248. package/src/tui/markdown/render-ansi.mjs +94 -0
  249. package/src/tui/markdown/render-ansi.test.mjs +108 -0
  250. package/src/tui/markdown/stream-fence.mjs +34 -0
  251. package/src/tui/markdown/stream-fence.test.mjs +26 -0
  252. package/src/tui/markdown/table-layout.mjs +250 -0
  253. package/src/tui/paste-attachments.mjs +0 -7
  254. package/src/tui/spinner-verbs.mjs +1 -2
  255. package/src/tui/statusline-ansi-bridge.mjs +172 -0
  256. package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
  257. package/src/tui/theme.mjs +756 -24
  258. package/src/tui/time-format.mjs +1 -1
  259. package/src/tui/transcript-tool-failures.mjs +67 -0
  260. package/src/tui/transcript-tool-failures.test.mjs +111 -0
  261. package/src/ui/ansi.mjs +1 -2
  262. package/src/ui/markdown.mjs +85 -26
  263. package/src/ui/markdown.test.mjs +70 -0
  264. package/src/ui/model-display.mjs +121 -0
  265. package/src/ui/session-stats.mjs +44 -0
  266. package/src/ui/statusline-context-label.test.mjs +15 -0
  267. package/src/ui/statusline.mjs +386 -178
  268. package/src/ui/tool-card.mjs +2 -13
  269. package/src/vendor/statusline/bin/statusline-lib.mjs +1 -620
  270. package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
  271. package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
  272. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
  273. package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
  274. package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
  275. package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
  276. package/src/workflows/default/WORKFLOW.md +12 -5
  277. package/src/workflows/default/workflow.json +0 -1
  278. package/src/workflows/solo/WORKFLOW.md +15 -0
  279. package/src/workflows/solo/workflow.json +7 -0
  280. package/vendor/ink/build/ink.js +54 -8
  281. package/vendor/ink/build/output.js +6 -1
  282. package/src/agents/scheduler-task.md +0 -3
  283. package/src/agents/web-researcher/AGENT.md +0 -3
  284. package/src/agents/web-researcher/agent.json +0 -6
  285. package/src/agents/webhook-handler.md +0 -3
  286. package/src/hooks/lib/permission-rules.cjs +0 -170
  287. package/src/hooks/lib/settings-loader.cjs +0 -112
  288. package/src/lib/hook-pipe-path.cjs +0 -10
  289. package/src/rules/bridge/00-common.md +0 -5
  290. package/src/rules/bridge/30-explorer.md +0 -4
  291. package/src/rules/lead/00-tool-lead.md +0 -5
  292. package/src/rules/shared/00-language.md +0 -3
  293. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  294. package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
  295. package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
  296. package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
  297. package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
  298. package/src/standalone/bridge-tool.mjs +0 -1414
  299. package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
@@ -1,20 +1,20 @@
1
- /*
2
- * components/PromptInput.jsx — the prompt input line.
3
- *
4
- * A self-contained editor built on ink's useInput. The text renders as plain
5
- * text and a thin NATIVE hardware cursor sits at the insertion point (also where
6
- * the terminal echoes typed/IME characters).
7
- *
8
- * CURSOR — via a small ink fork (vendor/ink, mirrored into node_modules): we
9
- * tag the text-box node with `internal_cursorAnchor = { col, row }` (the caret's
10
- * position WITHIN the box). Forked ink, during renderNodeToOutput, parks the
11
- * hardware cursor at that node's REAL laid-out absolute cell + (col,row) — every
12
- * frame, from the actual yoga layout. This replaces ink's stock useCursor, whose
13
- * externally-supplied absolute coordinate drifted/vanished whenever the layout
14
- * above the input changed (spinner/thinking growth, Enter, fullscreen relayout)
15
- * because it was computed a beat before the final layout. The fork computes it
16
- * at the exact moment of drawing, so it can never be stale.
17
- */
1
+ /*
2
+ * components/PromptInput.jsx — the prompt input line.
3
+ *
4
+ * A self-contained editor built on ink's useInput. The text renders as plain
5
+ * text and a thin NATIVE hardware cursor sits at the insertion point (also where
6
+ * the terminal echoes typed/IME characters).
7
+ *
8
+ * CURSOR — via a small ink fork (vendor/ink, mirrored into node_modules): we
9
+ * tag the text-box node with `internal_cursorAnchor = { col, row }` (the caret's
10
+ * position WITHIN the box). Forked ink, during renderNodeToOutput, parks the
11
+ * hardware cursor at that node's REAL laid-out absolute cell + (col,row) — every
12
+ * frame, from the actual yoga layout. This replaces ink's stock useCursor, whose
13
+ * externally-supplied absolute coordinate drifted/vanished whenever the layout
14
+ * above the input changed (spinner/thinking growth, Enter, fullscreen relayout)
15
+ * because it was computed a beat before the final layout. The fork computes it
16
+ * at the exact moment of drawing, so it can never be stale.
17
+ */
18
18
  import React, { useEffect, useLayoutEffect, useState, useRef } from 'react';
19
19
  import { Box, Text, useInput, usePaste, useStdin } from 'ink';
20
20
  import stringWidth from 'string-width';
@@ -32,6 +32,7 @@ import {
32
32
  moveCursor,
33
33
  nextOffset,
34
34
  nextWordOffset,
35
+ offsetAtCell,
35
36
  previousOffset,
36
37
  previousWordOffset,
37
38
  replaceSelection,
@@ -45,11 +46,20 @@ function hintStyle(tone) {
45
46
  if (tone === 'plain') return { textColor: theme.subtle };
46
47
  return { textColor: theme.inactive };
47
48
  }
48
-
49
+
50
+ // Windows Terminal IME composition can clip a glyph that starts exactly at the
51
+ // left edge of the editable text node. The rounded prompt box already adds a
52
+ // paddingX of 1, so the typing start can sit directly against that padding
53
+ // without an extra guard column.
54
+ const IME_LEFT_GUARD_COLUMNS = 0;
55
+ // Coalesce prompt mouse-drag extend commits (SGR motion can fire faster than ink
56
+ // needs to immediate-render). Matches transcript selection paint cadence.
57
+ const MOUSE_EXTEND_COALESCE_MS = 24;
58
+
49
59
  function insertText(draft, input) {
50
- if (!input) return draft;
60
+ if (!input) return draft;
51
61
  return replaceSelection(draft, input);
52
- }
62
+ }
53
63
 
54
64
  function renderSelectedText(displayValue, range, trailingSpace = false) {
55
65
  if (!range) return trailingSpace ? `${displayValue} ` : displayValue;
@@ -59,14 +69,14 @@ function renderSelectedText(displayValue, range, trailingSpace = false) {
59
69
  <>
60
70
  {start > 0 ? displayValue.slice(0, start) : null}
61
71
  {end > start ? (
62
- <Text color={theme.inverseText} backgroundColor="rgb(245,245,245)">{displayValue.slice(start, end)}</Text>
72
+ <Text color={theme.selectionText} backgroundColor={theme.selectionBackground}>{displayValue.slice(start, end)}</Text>
63
73
  ) : null}
64
74
  {displayValue.slice(end)}
65
75
  {trailingSpace ? ' ' : ''}
66
76
  </>
67
77
  );
68
78
  }
69
-
79
+
70
80
  function normalizePastedText(text) {
71
81
  return String(text ?? '').replace(/\r\n?/g, '\n');
72
82
  }
@@ -78,10 +88,28 @@ function singleTrailingLineBreakPrefix(text) {
78
88
  return prefix.includes('\n') ? null : prefix;
79
89
  }
80
90
 
91
+ function draftStateEqual(a, b) {
92
+ return (
93
+ a.value === b.value
94
+ && a.cursor === b.cursor
95
+ && a.selectionAnchor === b.selectionAnchor
96
+ );
97
+ }
98
+
99
+ function isCtrlEnterSequence(input) {
100
+ const text = String(input ?? '');
101
+ const body = text.startsWith('\x1b[') ? text.slice(2) : text.startsWith('[') ? text.slice(1) : '';
102
+ if (!body) return false;
103
+ const kitty = /^13;(\d+)(?::\d+)?(?:;[\d:]+)?u$/.exec(body);
104
+ if (kitty) return ((Number(kitty[1]) - 1) & 4) !== 0;
105
+ const modifyOtherKeys = /^27;(\d+);13~$/.exec(body);
106
+ return Boolean(modifyOtherKeys && (((Number(modifyOtherKeys[1]) - 1) & 4) !== 0));
107
+ }
108
+
81
109
  export function PromptInput({
82
- onSubmit,
83
- disabled = false,
84
- onDraftChange,
110
+ onSubmit,
111
+ disabled = false,
112
+ onDraftChange,
85
113
  interruptActive = false,
86
114
  onInterrupt,
87
115
  commandPaletteActive = false,
@@ -91,14 +119,18 @@ export function PromptInput({
91
119
  initialValue = '',
92
120
  draftOverride,
93
121
  onEscape,
122
+ onTab,
94
123
  onCommandPaletteNavigate,
95
124
  onCommandPaletteAccept,
96
125
  onCommandPaletteCancel,
97
126
  onCommandPaletteComplete,
98
127
  onRestoreQueued,
128
+ onHistoryNavigate,
99
129
  onPasteText,
100
130
  selectionRef,
101
131
  valueRef,
132
+ boxRectRef,
133
+ mouseSelectionRef,
102
134
  }) {
103
135
  const [draft, setDraft] = useState(() => {
104
136
  const value = String(initialValue || '');
@@ -109,43 +141,46 @@ export function PromptInput({
109
141
  const lastReportedValueRef = useRef(draft.value);
110
142
  if (valueRef) valueRef.current = draftRef.current.value;
111
143
  const { isRawModeSupported } = useStdin();
112
- // The text box's ink DOM node. We mark it as the cursor anchor (forked ink
113
- // reads internal_cursorAnchor during render and parks the hardware cursor at
114
- // that node's REAL laid-out position + our caret col/row — no external
115
- // absolute-coordinate guessing, so it never drifts).
116
- const boxRef = useRef(null);
117
- const cursorEnabledRef = useRef(false); // latest enabled state, read by the anchor fn at render time
118
- const contentWidthRef = useRef(80);
119
- const preferredColumnRef = useRef(null);
120
- const { value, cursor } = draft;
121
- draftRef.current = draft;
122
- if (selectionRef) {
123
- const range = selectionRange(draft);
124
- selectionRef.current = range
125
- ? { range, text: mask ? '' : draft.value.slice(range.start, range.end) }
126
- : null;
127
- }
128
-
129
- // Bypass ink's render throttle for keystroke echo. ink coalesces renders to
130
- // maxFps (leading+trailing throttle), so when typing faster than one frame the
131
- // last chars land on the trailing timer felt as input lag ("a beat behind").
132
- // ink exposes an UNthrottled `onImmediateRender` on the ink-root node; walking
133
- // the parent chain from our box and firing it flushes the new draft in the same
134
- // tick. Guarded so a structure change in the ink fork degrades to throttled
135
- // (slower) rendering, never a crash.
144
+ // The text box's ink DOM node. We mark it as the cursor anchor (forked ink
145
+ // reads internal_cursorAnchor during render and parks the hardware cursor at
146
+ // that node's REAL laid-out position + our caret col/row — no external
147
+ // absolute-coordinate guessing, so it never drifts).
148
+ const boxRef = useRef(null);
149
+ const cursorEnabledRef = useRef(false); // latest enabled state, read by the anchor fn at render time
150
+ const contentWidthRef = useRef(80);
151
+ const preferredColumnRef = useRef(null);
152
+ const mouseExtendCoalesceRef = useRef({ pendingNext: null, timer: null, t: 0 });
153
+ const { value, cursor } = draft;
154
+ draftRef.current = draft;
155
+ if (selectionRef) {
156
+ const range = selectionRange(draft);
157
+ selectionRef.current = range
158
+ ? { range, text: mask ? '' : draft.value.slice(range.start, range.end) }
159
+ : null;
160
+ }
161
+
162
+ // Bypass ink's render throttle for keystroke echo. ink coalesces renders to
163
+ // maxFps (leading+trailing throttle), so when typing faster than one frame the
164
+ // last chars land on the trailing timer — felt as input lag ("a beat behind").
165
+ // ink exposes an UNthrottled `onImmediateRender` on the ink-root node; walking
166
+ // the parent chain from our box and firing it flushes the new draft in the same
167
+ // tick. Guarded so a structure change in the ink fork degrades to throttled
168
+ // (slower) rendering, never a crash.
136
169
  const flushImmediate = () => {
137
- let node = boxRef.current;
138
- for (let i = 0; node && i < 64; i++) {
139
- if (node.nodeName === 'ink-root') {
140
- if (typeof node.onImmediateRender === 'function') node.onImmediateRender();
141
- return;
142
- }
143
- node = node.parentNode;
144
- }
145
- };
146
-
170
+ let node = boxRef.current;
171
+ for (let i = 0; node && i < 64; i++) {
172
+ if (node.nodeName === 'ink-root') {
173
+ if (typeof node.onImmediateRender === 'function') node.onImmediateRender();
174
+ return;
175
+ }
176
+ node = node.parentNode;
177
+ }
178
+ };
179
+
147
180
  const commitDraft = (next, options = {}) => {
181
+ const sameDraft = draftStateEqual(draftRef.current, next);
148
182
  if (!options.keepPreferredColumn) preferredColumnRef.current = null;
183
+ if (sameDraft) return;
149
184
  draftRef.current = next;
150
185
  setDraft(next);
151
186
  queueMicrotask(flushImmediate);
@@ -159,21 +194,135 @@ export function PromptInput({
159
194
  const installCursorAnchor = () => {
160
195
  if (!boxRef.current || boxRef.current.internal_cursorAnchor) return false;
161
196
  boxRef.current.internal_cursorAnchor = (yogaNode) => {
197
+ // [mixdog] Report the editable content box's REAL absolute rect up to App
198
+ // every frame so the mouse handler can map a click/drag cell to an edit
199
+ // offset. Walk the parent chain summing yoga computed offsets (same math
200
+ // render-node-to-output uses) — boxRef is the flex-row that holds the text
201
+ // node, so its absolute x/y is the first content cell (col 0,row 0).
202
+ if (boxRectRef) {
203
+ let absLeft = 0;
204
+ let absTop = 0;
205
+ let node = boxRef.current;
206
+ for (let i = 0; node && i < 64; i++) {
207
+ const yn = node.yogaNode;
208
+ if (yn?.getComputedLeft) {
209
+ absLeft += yn.getComputedLeft() || 0;
210
+ absTop += yn.getComputedTop() || 0;
211
+ }
212
+ if (node.nodeName === 'ink-root') break;
213
+ node = node.parentNode;
214
+ }
215
+ const wNow = yogaNode?.getComputedWidth?.() ?? 0;
216
+ const hNow = yogaNode?.getComputedHeight?.() ?? 1;
217
+ boxRectRef.current = {
218
+ top: absTop,
219
+ left: absLeft,
220
+ height: Math.max(1, hNow || 1),
221
+ contentWidth: contentWidthRef.current,
222
+ };
223
+ }
162
224
  if (!cursorEnabledRef.current) return null;
163
225
  const d = draftRef.current;
164
226
  const w = yogaNode?.getComputedWidth?.() ?? 0;
165
- contentWidthRef.current = Math.max(1, w || contentWidthRef.current || 80);
166
- return w > 0
167
- ? caretPosition(d.value, d.cursor, w)
227
+ const guardColumns = w > IME_LEFT_GUARD_COLUMNS ? IME_LEFT_GUARD_COLUMNS : 0;
228
+ const contentWidth = Math.max(1, (w ? w - guardColumns : contentWidthRef.current) || 80);
229
+ contentWidthRef.current = contentWidth;
230
+ const caret = w > 0
231
+ ? caretPosition(d.value, d.cursor, contentWidth)
168
232
  : { row: 0, col: stringWidth(d.value.slice(0, d.cursor)) };
233
+ return w > 0
234
+ ? { ...caret, col: caret.col + guardColumns }
235
+ : caret;
169
236
  };
170
237
  return true;
171
238
  };
172
-
239
+
173
240
  const updateDraft = (fn, options = {}) => {
174
241
  commitDraft(fn(draftRef.current), options);
175
242
  };
176
243
 
244
+ const cancelMouseExtendCoalesce = () => {
245
+ const state = mouseExtendCoalesceRef.current;
246
+ if (state.timer) {
247
+ clearTimeout(state.timer);
248
+ state.timer = null;
249
+ }
250
+ state.pendingNext = null;
251
+ };
252
+
253
+ const queueMouseExtendCommit = (next, immediate = false) => {
254
+ if (immediate) {
255
+ cancelMouseExtendCoalesce();
256
+ commitDraft(next);
257
+ mouseExtendCoalesceRef.current.t = Date.now();
258
+ return;
259
+ }
260
+ if (draftStateEqual(draftRef.current, next)) {
261
+ cancelMouseExtendCoalesce();
262
+ commitDraft(next);
263
+ return;
264
+ }
265
+ const state = mouseExtendCoalesceRef.current;
266
+ state.pendingNext = next;
267
+ const now = Date.now();
268
+ const elapsed = now - state.t;
269
+ if (elapsed >= MOUSE_EXTEND_COALESCE_MS) {
270
+ cancelMouseExtendCoalesce();
271
+ state.t = now;
272
+ commitDraft(next);
273
+ return;
274
+ }
275
+ if (state.timer) return;
276
+ state.timer = setTimeout(() => {
277
+ const current = mouseExtendCoalesceRef.current;
278
+ const pending = current.pendingNext;
279
+ current.timer = null;
280
+ current.pendingNext = null;
281
+ current.t = Date.now();
282
+ if (pending) commitDraft(pending);
283
+ }, Math.max(1, MOUSE_EXTEND_COALESCE_MS - elapsed));
284
+ state.timer.unref?.();
285
+ };
286
+
287
+ // [mixdog] Mouse drag-selection driver. App's single mouse handler maps a
288
+ // click/drag cell over the prompt box to an edit offset and calls these so the
289
+ // SAME selectionAnchor/cursor engine that keyboard Shift-selection uses paints
290
+ // the highlight. Anchor on press, extend on drag/release; clear on a plain
291
+ // click. Reuses contentWidthRef (the real measured content width).
292
+ if (mouseSelectionRef) {
293
+ mouseSelectionRef.current = {
294
+ offsetAtCell: (row, col) => offsetAtCell(draftRef.current.value, row, col, contentWidthRef.current),
295
+ anchorAt: (offset) => {
296
+ cancelMouseExtendCoalesce();
297
+ const value = draftRef.current.value;
298
+ const off = Math.max(0, Math.min(value.length, Math.floor(Number(offset) || 0)));
299
+ commitDraft({ ...draftRef.current, cursor: off, selectionAnchor: off });
300
+ mouseExtendCoalesceRef.current.t = Date.now();
301
+ },
302
+ extendTo: (offset, immediate = false) => {
303
+ if (offset == null) {
304
+ if (immediate) cancelMouseExtendCoalesce();
305
+ return;
306
+ }
307
+ const d = draftRef.current;
308
+ const off = Math.max(0, Math.min(d.value.length, Math.floor(Number(offset) || 0)));
309
+ const anchor = Number.isFinite(d.selectionAnchor) ? d.selectionAnchor : d.cursor;
310
+ queueMouseExtendCommit({ ...d, cursor: off, selectionAnchor: anchor }, immediate);
311
+ },
312
+ clear: () => {
313
+ cancelMouseExtendCoalesce();
314
+ if (selectionRange(draftRef.current)) commitDraft(clearSelection(draftRef.current));
315
+ },
316
+ };
317
+ }
318
+
319
+ useEffect(() => () => {
320
+ const state = mouseExtendCoalesceRef.current;
321
+ if (state.timer) clearTimeout(state.timer);
322
+ state.timer = null;
323
+ state.pendingNext = null;
324
+ }, []);
325
+
177
326
  const moveDraftVertically = (direction, { extend = false } = {}) => {
178
327
  const current = draftRef.current;
179
328
  const moved = verticalOffset(
@@ -193,6 +342,13 @@ export function PromptInput({
193
342
  return onRestoreQueued?.(draftRef.current.value) === true;
194
343
  };
195
344
 
345
+ const applyHistoryNavigation = (direction, meta = {}) => {
346
+ const nextValue = onHistoryNavigate?.(direction, draftRef.current.value, meta);
347
+ if (typeof nextValue !== 'string') return false;
348
+ commitDraft({ value: nextValue, cursor: nextValue.length, selectionAnchor: null });
349
+ return true;
350
+ };
351
+
196
352
  const insertAtDraft = (text) => {
197
353
  const value = String(text ?? '');
198
354
  if (!value) return;
@@ -234,12 +390,12 @@ export function PromptInput({
234
390
  }, [selectionRef]);
235
391
 
236
392
  const submitDraft = (next) => {
237
- const text = next.value;
238
- const accepted = onSubmit?.(text) !== false;
239
- if (!accepted) {
240
- commitDraft(next);
241
- return;
242
- }
393
+ const text = next.value;
394
+ const accepted = onSubmit?.(text) !== false;
395
+ if (!accepted) {
396
+ commitDraft(next);
397
+ return;
398
+ }
243
399
  commitDraft({ value: '', cursor: 0, selectionAnchor: null });
244
400
  };
245
401
 
@@ -257,34 +413,47 @@ export function PromptInput({
257
413
  }
258
414
  submitDraft(next);
259
415
  };
260
-
261
- // Input capture is only active on a real TTY (raw mode). In pipes/CI the input
262
- // is inert — useInput with isActive:false won't throw.
416
+
417
+ // Input capture is only active on a real TTY (raw mode). In pipes/CI the input
418
+ // is inert — useInput with isActive:false won't throw.
263
419
  usePaste((text) => {
264
420
  if (disabled) return;
265
421
  handleExternalPaste(text, { source: 'paste' });
266
422
  }, { isActive: isRawModeSupported && !disabled });
267
-
268
- useInput((input, key) => {
269
- if (disabled) return;
270
-
271
- const rawInput = String(input ?? '');
272
- const inputKey = rawInput.toLowerCase();
273
-
274
- // Drop SGR mouse-tracking sequences (wheel/click). When app mouse tracking
275
- // is explicitly enabled, App parses these off raw stdin itself;
276
- // ink still forwards the bytes here as "input", which would otherwise type
277
- // garbage like `[<64;55;22M` into the prompt. Match with or without the
278
- // leading ESC (terminals/ink may strip it): CSI '<' … final 'M'/'m'.
423
+
424
+ useInput((input, key) => {
425
+ if (disabled) return;
426
+
427
+ const rawInput = String(input ?? '');
428
+ const inputKey = rawInput.toLowerCase();
429
+
430
+ // Drop SGR mouse-tracking sequences (wheel/click). When app mouse tracking
431
+ // is explicitly enabled, App parses these off raw stdin itself;
432
+ // ink still forwards the bytes here as "input", which would otherwise type
433
+ // garbage like `[<64;55;22M` into the prompt. Match with or without the
434
+ // leading ESC (terminals/ink may strip it): CSI '<' … final 'M'/'m'.
279
435
  if (/(?:\x1b)?\[<\d+;\d+;\d+[Mm]/.test(rawInput) || /^\[?<\d+;\d+;\d+[Mm]?$/.test(rawInput)) {
280
436
  return;
281
437
  }
282
438
 
283
439
  const rawUpArrow = rawInput === '\x1b[A' || rawInput === '\x1bOA' || rawInput === '[A' || rawInput === 'OA';
284
440
  const rawDownArrow = rawInput === '\x1b[B' || rawInput === '\x1bOB' || rawInput === '[B' || rawInput === 'OB';
441
+ // Shift+Arrow modifier sequences (xterm `\x1b[1;2<dir>`, rxvt `\x1b[<dir>`
442
+ // lowercase). Ink's useInput does not decode the `;2` (shift) modifier into
443
+ // key.shift for arrows, so the bytes arrive as raw input and the plain-arrow
444
+ // matchers above miss them — selection-extend never fires. Detect them here
445
+ // and fold into a single `shiftHeld` signal used by every arrow/home/end
446
+ // branch below (alongside ink's key.shift for terminals that DO decode it).
447
+ const rawShiftUp = rawInput === '\x1b[1;2A' || rawInput === '\x1b[a' || rawInput === '[1;2A';
448
+ const rawShiftDown = rawInput === '\x1b[1;2B' || rawInput === '\x1b[b' || rawInput === '[1;2B';
449
+ const rawShiftRight = rawInput === '\x1b[1;2C' || rawInput === '\x1b[c' || rawInput === '[1;2C';
450
+ const rawShiftLeft = rawInput === '\x1b[1;2D' || rawInput === '\x1b[d' || rawInput === '[1;2D';
451
+ const shiftHeld = key.shift || rawShiftUp || rawShiftDown || rawShiftLeft || rawShiftRight;
285
452
  const lineBreakIndex = rawInput.search(/[\r\n]/);
286
453
  const rawEnter = rawInput === '\r' || rawInput === '\n' || rawInput === '\r\n';
287
454
  const trailingEnterPrefix = singleTrailingLineBreakPrefix(rawInput);
455
+ const rawCtrlEnter = isCtrlEnterSequence(rawInput);
456
+ const modifiedLineBreak = key.shift || key.meta || key.ctrl || rawCtrlEnter;
288
457
 
289
458
  const pasteFallback = lineBreakIndex !== -1 && trailingEnterPrefix === null && !rawEnter && (rawInput.length > 1 || !key.return);
290
459
  if (pasteFallback) {
@@ -293,7 +462,7 @@ export function PromptInput({
293
462
  }
294
463
 
295
464
  if (trailingEnterPrefix !== null) {
296
- if (key.shift || key.meta) {
465
+ if (modifiedLineBreak) {
297
466
  updateDraft((d) => insertText(d, `${trailingEnterPrefix}\n`));
298
467
  return;
299
468
  }
@@ -301,53 +470,65 @@ export function PromptInput({
301
470
  return;
302
471
  }
303
472
 
473
+ if (rawCtrlEnter) {
474
+ updateDraft((d) => replaceSelection(d, '\n'));
475
+ return;
476
+ }
477
+
304
478
  if (!commandPaletteActive && ((key.ctrl && inputKey === 'v') || (key.meta && inputKey === 'v'))) {
305
479
  handleExternalPaste('', { source: 'clipboard-image-shortcut' });
306
480
  return;
307
481
  }
308
482
 
309
- if (key.return) {
310
- if (key.shift || key.meta) {
311
- updateDraft((d) => replaceSelection(d, '\n'));
312
- return;
313
- }
314
-
315
- if (commandPaletteActive) {
316
- const accepted = onCommandPaletteAccept?.(draftRef.current.value);
317
- if (accepted !== false) {
318
- commitDraft({ value: '', cursor: 0, selectionAnchor: null });
483
+ if (key.return) {
484
+ if (modifiedLineBreak) {
485
+ updateDraft((d) => replaceSelection(d, '\n'));
486
+ return;
487
+ }
488
+
489
+ if (commandPaletteActive) {
490
+ const accepted = onCommandPaletteAccept?.(draftRef.current.value);
491
+ if (accepted !== false) {
492
+ commitDraft({ value: '', cursor: 0, selectionAnchor: null });
319
493
  }
320
- return;
321
- }
322
-
323
- const current = draftRef.current;
324
- if (current.value[current.cursor - 1] === '\\') {
325
- updateDraft((d) => ({
326
- value: `${d.value.slice(0, d.cursor - 1)}\n${d.value.slice(d.cursor)}`,
327
- cursor: d.cursor,
328
- selectionAnchor: null,
329
- }));
330
- return;
331
- }
332
-
333
- submitDraft(current);
334
- return;
335
- }
336
-
337
- if (key.upArrow || rawUpArrow) {
494
+ return;
495
+ }
496
+
497
+ const current = draftRef.current;
498
+ if (current.value[current.cursor - 1] === '\\') {
499
+ updateDraft((d) => ({
500
+ value: `${d.value.slice(0, d.cursor - 1)}\n${d.value.slice(d.cursor)}`,
501
+ cursor: d.cursor,
502
+ selectionAnchor: null,
503
+ }));
504
+ return;
505
+ }
506
+
507
+ submitDraft(current);
508
+ return;
509
+ }
510
+
511
+ if (key.upArrow || rawUpArrow || rawShiftUp) {
338
512
  if (commandPaletteActive) {
339
513
  onCommandPaletteNavigate?.(-1);
340
514
  } else {
341
- if (!restoreQueuedToDraft()) moveDraftVertically(-1, { extend: key.shift });
515
+ const hasDraftText = String(draftRef.current.value || '').trim().length > 0;
516
+ if (!hasDraftText) {
517
+ if (!restoreQueuedToDraft()) applyHistoryNavigation('up', { emptyDraft: true });
518
+ } else if (!moveDraftVertically(-1, { extend: shiftHeld })) {
519
+ applyHistoryNavigation('up', { emptyDraft: false });
520
+ }
342
521
  }
343
522
  return;
344
523
  }
345
524
 
346
- if (key.downArrow || rawDownArrow) {
525
+ if (key.downArrow || rawDownArrow || rawShiftDown) {
347
526
  if (commandPaletteActive) {
348
527
  onCommandPaletteNavigate?.(1);
349
528
  } else {
350
- moveDraftVertically(1, { extend: key.shift });
529
+ if (!moveDraftVertically(1, { extend: shiftHeld })) {
530
+ applyHistoryNavigation('down', { emptyDraft: String(draftRef.current.value || '').trim().length === 0 });
531
+ }
351
532
  }
352
533
  return;
353
534
  }
@@ -372,14 +553,17 @@ export function PromptInput({
372
553
  return;
373
554
  }
374
555
 
375
- if (key.tab && commandPaletteActive) {
376
- const completed = onCommandPaletteComplete?.(draftRef.current.value);
377
- if (typeof completed === 'string') {
378
- commitDraft({ value: completed, cursor: completed.length, selectionAnchor: null });
379
- }
380
- return;
381
- }
382
-
556
+ if (key.tab) {
557
+ if (commandPaletteActive) {
558
+ const completed = onCommandPaletteComplete?.(draftRef.current.value);
559
+ if (typeof completed === 'string') {
560
+ commitDraft({ value: completed, cursor: completed.length, selectionAnchor: null });
561
+ }
562
+ return;
563
+ }
564
+ if (onTab?.(draftRef.current.value) === true) return;
565
+ }
566
+
383
567
  if (key.escape) {
384
568
  if (commandPaletteActive) {
385
569
  onCommandPaletteCancel?.(draftRef.current.value);
@@ -399,6 +583,10 @@ export function PromptInput({
399
583
  commitDraft({ value: '', cursor: 0, selectionAnchor: null });
400
584
  return;
401
585
  }
586
+ // Active turn takes precedence over queue restore (matches claude-code
587
+ // useCancelRequest priority): Esc during a running turn interrupts the
588
+ // turn and leaves queued steering prompts intact to run afterward. Queued
589
+ // messages only pop back into the draft when the turn is idle.
402
590
  if (interruptActive) {
403
591
  const restoredText = onInterrupt?.('');
404
592
  if (typeof restoredText === 'string') {
@@ -413,145 +601,145 @@ export function PromptInput({
413
601
  return;
414
602
  }
415
603
 
416
- if (key.leftArrow) {
604
+ if (key.leftArrow || rawShiftLeft) {
417
605
  if (commandPaletteActive) {
418
606
  onCommandPaletteNavigate?.('left');
419
607
  return;
420
608
  }
421
609
  updateDraft((d) => {
422
- const range = !key.shift && !key.ctrl && !key.meta ? selectionRange(d) : null;
610
+ const range = !shiftHeld && !key.ctrl && !key.meta ? selectionRange(d) : null;
423
611
  const cursor = range
424
612
  ? range.start
425
613
  : key.ctrl || key.meta
426
614
  ? previousWordOffset(d.value, d.cursor)
427
615
  : previousOffset(d.value, d.cursor);
428
- return moveCursor(d, cursor, { extend: key.shift });
616
+ return moveCursor(d, cursor, { extend: shiftHeld });
429
617
  });
430
618
  return;
431
619
  }
432
- if (key.rightArrow) {
620
+ if (key.rightArrow || rawShiftRight) {
433
621
  if (commandPaletteActive) {
434
622
  onCommandPaletteNavigate?.('right');
435
623
  return;
436
624
  }
437
625
  updateDraft((d) => {
438
- const range = !key.shift && !key.ctrl && !key.meta ? selectionRange(d) : null;
626
+ const range = !shiftHeld && !key.ctrl && !key.meta ? selectionRange(d) : null;
439
627
  const cursor = range
440
628
  ? range.end
441
629
  : key.ctrl || key.meta
442
630
  ? nextWordOffset(d.value, d.cursor)
443
631
  : nextOffset(d.value, d.cursor);
444
- return moveCursor(d, cursor, { extend: key.shift });
632
+ return moveCursor(d, cursor, { extend: shiftHeld });
445
633
  });
446
634
  return;
447
635
  }
448
- if (key.home) {
449
- updateDraft((d) => moveCursor(d, lineStart(d.value, d.cursor), { extend: key.shift }));
450
- return;
451
- }
452
- if (key.end) {
636
+ if (key.home) {
637
+ updateDraft((d) => moveCursor(d, lineStart(d.value, d.cursor), { extend: shiftHeld }));
638
+ return;
639
+ }
640
+ if (key.end) {
641
+ updateDraft((d) => moveCursor(d, lineEnd(d.value, d.cursor), { extend: shiftHeld }));
642
+ return;
643
+ }
644
+
645
+ const editingKey = String(input || '').toLowerCase();
646
+
647
+ // ctrl+a selects all like a normal text box; ctrl+e keeps readline line-end.
648
+ if (key.ctrl && editingKey === 'a') {
649
+ updateDraft((d) => (d.value ? { ...d, cursor: d.value.length, selectionAnchor: 0 } : clearSelection(d)));
650
+ return;
651
+ }
652
+ if (key.ctrl && editingKey === 'e') {
453
653
  updateDraft((d) => moveCursor(d, lineEnd(d.value, d.cursor), { extend: key.shift }));
454
- return;
455
- }
456
-
457
- const editingKey = String(input || '').toLowerCase();
458
-
459
- // ctrl+a selects all like a normal text box; ctrl+e keeps readline line-end.
460
- if (key.ctrl && editingKey === 'a') {
461
- updateDraft((d) => (d.value ? { ...d, cursor: d.value.length, selectionAnchor: 0 } : clearSelection(d)));
462
- return;
463
- }
464
- if (key.ctrl && editingKey === 'e') {
465
- updateDraft((d) => moveCursor(d, lineEnd(d.value, d.cursor), { extend: key.shift }));
466
- return;
467
- }
468
- // ctrl+b / ctrl+f — character left / right.
469
- if (key.ctrl && editingKey === 'b') {
470
- updateDraft((d) => moveCursor(d, previousOffset(d.value, d.cursor), { extend: key.shift }));
471
- return;
472
- }
473
- if (key.ctrl && editingKey === 'f') {
474
- updateDraft((d) => moveCursor(d, nextOffset(d.value, d.cursor), { extend: key.shift }));
475
- return;
476
- }
477
- // alt/option+b / alt/option+f — word left / right.
478
- if (key.meta && editingKey === 'b') {
479
- updateDraft((d) => moveCursor(d, previousWordOffset(d.value, d.cursor), { extend: key.shift }));
480
- return;
481
- }
482
- if (key.meta && editingKey === 'f') {
483
- updateDraft((d) => moveCursor(d, nextWordOffset(d.value, d.cursor), { extend: key.shift }));
484
- return;
485
- }
486
- // ctrl+u / ctrl+k — delete to line start / end.
487
- if (key.ctrl && editingKey === 'u') {
488
- updateDraft(deleteToLineStart);
489
- return;
490
- }
491
- if (key.ctrl && editingKey === 'k') {
492
- updateDraft(deleteToLineEnd);
493
- return;
494
- }
495
- // ctrl+w / alt+backspace — delete previous word.
496
- if ((key.ctrl && editingKey === 'w') || ((key.ctrl || key.meta) && key.backspace)) {
497
- updateDraft(deleteBackwardWord);
498
- return;
499
- }
500
- // alt+d / ctrl+delete — delete next word.
501
- if ((key.meta && editingKey === 'd') || (key.ctrl && key.delete)) {
502
- updateDraft(deleteForwardWord);
503
- return;
504
- }
505
-
506
- if (key.backspace) {
507
- updateDraft((d) => {
508
- if (selectionRange(d)) return deleteSelectedText(d);
509
- if (d.cursor <= 0) return d;
510
- const start = previousOffset(d.value, d.cursor);
511
- return {
512
- value: d.value.slice(0, start) + d.value.slice(d.cursor),
513
- cursor: start,
514
- selectionAnchor: null,
515
- };
516
- });
517
- return;
518
- }
519
-
520
- if (key.delete) {
521
- updateDraft((d) => {
522
- if (selectionRange(d)) return deleteSelectedText(d);
523
- if (d.cursor >= d.value.length) return d;
524
- const end = nextOffset(d.value, d.cursor);
525
- return {
526
- value: d.value.slice(0, d.cursor) + d.value.slice(end),
527
- cursor: d.cursor,
528
- selectionAnchor: null,
529
- };
530
- });
531
- return;
532
- }
533
-
534
- // Printable input (ignore other control keys). Strip any embedded SGR mouse
535
- // sequences as a belt-and-suspenders guard (the early return above catches
536
- // whole-sequence inputs; this removes partials that rode in with real text).
537
- const printable = rawInput
538
- .replace(/(?:\x1b)?\[<\d+;\d+;\d+[Mm]/g, '')
539
- .replace(/[\r\n]/g, '');
540
- if (printable && !key.ctrl && !key.meta) {
541
- updateDraft((d) => insertText(d, printable));
542
- }
543
- }, { isActive: isRawModeSupported && !disabled });
544
-
545
- // Mark the text-box node with a cursor-anchor FUNCTION. Forked ink calls it
546
- // during renderNodeToOutput — AFTER yoga layout is final and (crucially)
547
- // during the same onRender that paints the new text. The function reads the
548
- // latest caret from refs (synced every render below), so the cursor can never
549
- // be stale: an earlier object-anchor set in a layout effect was always one
550
- // keystroke behind, because ink's reconciler runs onRender inside
551
- // resetAfterCommit BEFORE React layout effects — that lag made the 2nd+ char
552
- // appear to land behind the caret (the observed scramble). Computing inside
553
- // the fork, from the real layout + current refs, fixes that by construction.
554
- cursorEnabledRef.current = !disabled && isRawModeSupported;
654
+ return;
655
+ }
656
+ // ctrl+b / ctrl+f — character left / right.
657
+ if (key.ctrl && editingKey === 'b') {
658
+ updateDraft((d) => moveCursor(d, previousOffset(d.value, d.cursor), { extend: key.shift }));
659
+ return;
660
+ }
661
+ if (key.ctrl && editingKey === 'f') {
662
+ updateDraft((d) => moveCursor(d, nextOffset(d.value, d.cursor), { extend: key.shift }));
663
+ return;
664
+ }
665
+ // alt/option+b / alt/option+f word left / right.
666
+ if (key.meta && editingKey === 'b') {
667
+ updateDraft((d) => moveCursor(d, previousWordOffset(d.value, d.cursor), { extend: key.shift }));
668
+ return;
669
+ }
670
+ if (key.meta && editingKey === 'f') {
671
+ updateDraft((d) => moveCursor(d, nextWordOffset(d.value, d.cursor), { extend: key.shift }));
672
+ return;
673
+ }
674
+ // ctrl+u / ctrl+k delete to line start / end.
675
+ if (key.ctrl && editingKey === 'u') {
676
+ updateDraft(deleteToLineStart);
677
+ return;
678
+ }
679
+ if (key.ctrl && editingKey === 'k') {
680
+ updateDraft(deleteToLineEnd);
681
+ return;
682
+ }
683
+ // ctrl+w / alt+backspace delete previous word.
684
+ if ((key.ctrl && editingKey === 'w') || ((key.ctrl || key.meta) && key.backspace)) {
685
+ updateDraft(deleteBackwardWord);
686
+ return;
687
+ }
688
+ // alt+d / ctrl+delete — delete next word.
689
+ if ((key.meta && editingKey === 'd') || (key.ctrl && key.delete)) {
690
+ updateDraft(deleteForwardWord);
691
+ return;
692
+ }
693
+
694
+ if (key.backspace) {
695
+ updateDraft((d) => {
696
+ if (selectionRange(d)) return deleteSelectedText(d);
697
+ if (d.cursor <= 0) return d;
698
+ const start = previousOffset(d.value, d.cursor);
699
+ return {
700
+ value: d.value.slice(0, start) + d.value.slice(d.cursor),
701
+ cursor: start,
702
+ selectionAnchor: null,
703
+ };
704
+ });
705
+ return;
706
+ }
707
+
708
+ if (key.delete) {
709
+ updateDraft((d) => {
710
+ if (selectionRange(d)) return deleteSelectedText(d);
711
+ if (d.cursor >= d.value.length) return d;
712
+ const end = nextOffset(d.value, d.cursor);
713
+ return {
714
+ value: d.value.slice(0, d.cursor) + d.value.slice(end),
715
+ cursor: d.cursor,
716
+ selectionAnchor: null,
717
+ };
718
+ });
719
+ return;
720
+ }
721
+
722
+ // Printable input (ignore other control keys). Strip any embedded SGR mouse
723
+ // sequences as a belt-and-suspenders guard (the early return above catches
724
+ // whole-sequence inputs; this removes partials that rode in with real text).
725
+ const printable = rawInput
726
+ .replace(/(?:\x1b)?\[<\d+;\d+;\d+[Mm]/g, '')
727
+ .replace(/[\r\n]/g, '');
728
+ if (printable && !key.ctrl && !key.meta) {
729
+ updateDraft((d) => insertText(d, printable));
730
+ }
731
+ }, { isActive: isRawModeSupported && !disabled });
732
+
733
+ // Mark the text-box node with a cursor-anchor FUNCTION. Forked ink calls it
734
+ // during renderNodeToOutput AFTER yoga layout is final and (crucially)
735
+ // during the same onRender that paints the new text. The function reads the
736
+ // latest caret from refs (synced every render below), so the cursor can never
737
+ // be stale: an earlier object-anchor set in a layout effect was always one
738
+ // keystroke behind, because ink's reconciler runs onRender inside
739
+ // resetAfterCommit BEFORE React layout effects — that lag made the 2nd+ char
740
+ // appear to land behind the caret (the observed scramble). Computing inside
741
+ // the fork, from the real layout + current refs, fixes that by construction.
742
+ cursorEnabledRef.current = !disabled && isRawModeSupported;
555
743
  installCursorAnchor();
556
744
 
557
745
  useLayoutEffect(() => {
@@ -564,15 +752,16 @@ export function PromptInput({
564
752
  if (disabled || !isRawModeSupported) return;
565
753
  queueMicrotask(flushImmediate);
566
754
  }, [disabled, isRawModeSupported]);
567
-
568
- // Trailing space cell so the caret at end-of-input has a rendered cell to sit
569
- // on (kept visually blank — no synthetic underline).
755
+
756
+ // Trailing space cell so the caret at end-of-input has a rendered cell to sit
757
+ // on (kept visually blank — no synthetic underline).
570
758
  const displayValue = mask ? value.replace(/[^\n]/g, '*') : value;
571
759
  const renderedValue = renderSelectedText(displayValue, selectionRange(draft), cursor === value.length);
572
760
  const hintMeta = hintStyle(hintTone);
573
761
 
574
762
  return (
575
763
  <Box ref={boxRef} flexDirection="row" flexGrow={1} flexShrink={1} backgroundColor={theme.background}>
764
+ <Box width={IME_LEFT_GUARD_COLUMNS} flexShrink={0} backgroundColor={theme.background} />
576
765
  <Text color={theme.text} wrap="hard">{renderedValue}</Text>
577
766
  {!value && hint ? (
578
767
  <Box marginLeft={-1}>