mixdog 0.8.1 → 0.9.0

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