mixdog 0.9.95 → 0.9.97

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 (282) hide show
  1. package/LICENSES/Apache-2.0.txt +201 -0
  2. package/LICENSES/MIT.txt +56 -0
  3. package/LICENSES/codex-NOTICE.txt +6 -0
  4. package/NOTICE.md +46 -9
  5. package/README.md +0 -17
  6. package/package.json +16 -12
  7. package/scripts/.tmp-coverage-report.mjs +96 -0
  8. package/scripts/build-tui.mjs +17 -1
  9. package/scripts/{channel-daemon-stub.mjs → daemon-stub.mjs} +18 -12
  10. package/scripts/fixtures/patch-replay-corpus.json +98 -0
  11. package/src/defaults/mixdog-config.template.json +5 -8
  12. package/src/defaults/skills/setup/SKILL.md +1 -1
  13. package/src/lib/keychain-cjs.cjs +1 -1
  14. package/src/lib/mixdog-debug.cjs +2 -3
  15. package/src/rules/agent/30-explorer.md +7 -4
  16. package/src/rules/lead/01-general.md +1 -2
  17. package/src/rules/lead/lead-tool.md +0 -3
  18. package/src/rules/shared/01-tool.md +21 -29
  19. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +18 -2
  20. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +31 -1
  21. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +6 -17
  22. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +12 -1
  23. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -13
  24. package/src/runtime/agent/orchestrator/config.mjs +246 -67
  25. package/src/runtime/agent/orchestrator/mcp/client.mjs +104 -41
  26. package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +23 -0
  27. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +113 -14
  28. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +5 -1
  29. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +32 -11
  30. package/src/runtime/agent/orchestrator/providers/gemini.mjs +89 -2
  31. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -1
  32. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +19 -1
  33. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +144 -39
  34. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -8
  35. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -1
  36. package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +1 -1
  37. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +3 -4
  38. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +3 -6
  39. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -2
  40. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +67 -7
  41. package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +291 -0
  42. package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +21 -0
  43. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +36 -10
  44. package/src/runtime/agent/orchestrator/session/compact.mjs +1 -1
  45. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +22 -33
  46. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +13 -0
  47. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +2 -2
  48. package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +0 -24
  49. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +21 -29
  50. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +7 -10
  51. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +45 -25
  52. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -3
  53. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +142 -84
  54. package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +12 -6
  55. package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +4 -0
  56. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +31 -1
  57. package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +38 -2
  58. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +29 -9
  59. package/src/runtime/agent/orchestrator/session/manager.mjs +2 -0
  60. package/src/runtime/agent/orchestrator/session/result-classification.mjs +65 -0
  61. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +86 -8
  62. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +10 -3
  63. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +4 -0
  64. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +71 -42
  65. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +4 -33
  66. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +4 -5
  67. package/src/runtime/agent/orchestrator/stall-policy.mjs +1 -1
  68. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +2 -2
  69. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +35 -1
  70. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +116 -26
  71. package/src/runtime/agent/orchestrator/tools/builtin/binary-file.mjs +92 -19
  72. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +8 -9
  73. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +2 -2
  74. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +2 -2
  75. package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +2 -2
  76. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +6 -15
  77. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +12 -26
  78. package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +5 -5
  79. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +83 -33
  80. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +42 -45
  81. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +27 -9
  82. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +7 -0
  83. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +17 -39
  84. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +73 -0
  85. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +1 -1
  86. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +0 -2
  87. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +3 -8
  88. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +30 -0
  89. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +5 -5
  90. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +142 -26
  91. package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +12 -1
  92. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +47 -19
  93. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +3 -2
  94. package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +1 -2
  95. package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +83 -13
  96. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +39 -1
  97. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +1 -1
  98. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +94 -20
  99. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +16 -0
  100. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +108 -12
  101. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +12 -11
  102. package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
  103. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +63 -9
  104. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +2 -2
  105. package/src/runtime/channels/lib/config.mjs +34 -34
  106. package/src/runtime/channels/lib/inbound-handler.mjs +8 -8
  107. package/src/runtime/channels/lib/interaction-handlers.mjs +6 -6
  108. package/src/runtime/channels/lib/memory-client.mjs +31 -157
  109. package/src/runtime/channels/lib/output-forwarder.mjs +16 -16
  110. package/src/runtime/channels/lib/owned-runtime.mjs +60 -60
  111. package/src/runtime/channels/lib/owner-heartbeat.mjs +1 -1
  112. package/src/runtime/channels/lib/{backend-dispatch.mjs → provider-dispatch.mjs} +6 -6
  113. package/src/runtime/channels/lib/runtime-paths.mjs +4 -30
  114. package/src/runtime/channels/lib/scheduler.mjs +2 -2
  115. package/src/runtime/channels/lib/status-snapshot.mjs +3 -3
  116. package/src/runtime/channels/lib/tool-dispatch.mjs +4 -4
  117. package/src/runtime/channels/lib/worker-bootstrap.mjs +1 -1
  118. package/src/runtime/channels/lib/worker-ipc.mjs +7 -7
  119. package/src/runtime/channels/lib/worker-main.mjs +38 -38
  120. package/src/runtime/channels/{backends → providers}/discord-access.mjs +1 -1
  121. package/src/runtime/channels/{backends → providers}/discord-gateway.mjs +1 -1
  122. package/src/runtime/channels/{backends → providers}/discord.mjs +3 -3
  123. package/src/runtime/channels/{backends → providers}/telegram.mjs +12 -12
  124. package/src/runtime/channels/tool-defs.mjs +1 -1
  125. package/src/runtime/memory/index.mjs +76 -248
  126. package/src/runtime/memory/lib/agent-ipc.mjs +146 -81
  127. package/src/runtime/memory/lib/compact-vector-cache.mjs +88 -0
  128. package/src/runtime/memory/lib/core-memory-store.mjs +18 -1
  129. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +13 -34
  130. package/src/runtime/memory/lib/embedding-provider.mjs +47 -26
  131. package/src/runtime/memory/lib/embedding-worker.mjs +30 -34
  132. package/src/runtime/memory/lib/http-router.mjs +9 -6
  133. package/src/runtime/memory/lib/ko-morph.mjs +3 -20
  134. package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -1
  135. package/src/runtime/memory/lib/memory-config-flags.mjs +1 -1
  136. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -0
  137. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +4 -2
  138. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +1 -0
  139. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +4 -1
  140. package/src/runtime/memory/lib/memory-cycle3.mjs +3 -1
  141. package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +5 -0
  142. package/src/runtime/memory/lib/memory-log.mjs +1 -1
  143. package/src/runtime/memory/lib/memory-process-lock.mjs +5 -60
  144. package/src/runtime/memory/lib/memory.mjs +1 -1
  145. package/src/runtime/memory/lib/pg/process.mjs +32 -9
  146. package/src/runtime/memory/lib/session-ingest.mjs +1 -1
  147. package/src/runtime/memory/tool-defs.mjs +1 -5
  148. package/src/runtime/search/index.mjs +2 -2
  149. package/src/runtime/search/tool-defs.mjs +2 -2
  150. package/src/runtime/shared/abort-race.mjs +70 -0
  151. package/src/runtime/shared/agent-route-config.mjs +119 -0
  152. package/src/runtime/shared/atomic-file.mjs +80 -4
  153. package/src/runtime/shared/channel-notification-routing.mjs +2 -2
  154. package/src/runtime/shared/child-guardian.mjs +204 -108
  155. package/src/runtime/shared/child-spawn-gate.mjs +139 -72
  156. package/src/runtime/shared/config.mjs +120 -47
  157. package/src/runtime/shared/llm/index.mjs +1 -1
  158. package/src/runtime/shared/owner-fair-gate.mjs +134 -0
  159. package/src/runtime/shared/resource-admission.mjs +150 -100
  160. package/src/runtime/shared/stream-progress.mjs +6 -0
  161. package/src/runtime/shared/tool-execution-owner.mjs +12 -0
  162. package/src/runtime/shared/tool-result-summary.mjs +1 -1
  163. package/src/runtime/shared/tool-surface.mjs +91 -27
  164. package/src/runtime/shared/tool-workload-gates.mjs +29 -0
  165. package/src/runtime/shared/turn-snapshot.mjs +47 -6
  166. package/src/runtime/shared/turn-worktree-snapshot.mjs +14 -2
  167. package/src/session-runtime/channel-config-api.mjs +5 -5
  168. package/src/session-runtime/config-helpers.mjs +39 -37
  169. package/src/session-runtime/config-lifecycle.mjs +27 -27
  170. package/src/session-runtime/lifecycle-api.mjs +22 -3
  171. package/src/session-runtime/memory-runtime-prewarm.mjs +15 -0
  172. package/src/session-runtime/model-route-api.mjs +7 -5
  173. package/src/session-runtime/prewarm.mjs +1 -1
  174. package/src/session-runtime/provider-auth-api.mjs +0 -7
  175. package/src/session-runtime/provider-models.mjs +1 -1
  176. package/src/session-runtime/remote-control.mjs +6 -7
  177. package/src/session-runtime/runtime-core.mjs +63 -39
  178. package/src/session-runtime/runtime-tunables.mjs +4 -4
  179. package/src/session-runtime/self-update.mjs +1 -1
  180. package/src/session-runtime/session-lifecycle.mjs +45 -17
  181. package/src/session-runtime/session-title.mjs +1 -1
  182. package/src/session-runtime/session-turn-api.mjs +96 -24
  183. package/src/session-runtime/settings-api.mjs +12 -24
  184. package/src/session-runtime/tool-defs.mjs +3 -3
  185. package/src/session-runtime/workflow-agents-api.mjs +55 -81
  186. package/src/session-runtime/workflow.mjs +14 -24
  187. package/src/standalone/agent-dispatch-broker.mjs +198 -0
  188. package/src/standalone/agent-tool/helpers.mjs +2 -2
  189. package/src/standalone/agent-tool/spawn-flow.mjs +2 -0
  190. package/src/standalone/agent-tool/spawn-preset.mjs +11 -21
  191. package/src/standalone/agent-tool/tool-def.mjs +0 -14
  192. package/src/standalone/channel-admin.mjs +53 -64
  193. package/src/standalone/{channel-daemon-client.mjs → channel-client.mjs} +33 -19
  194. package/src/standalone/{channel-daemon-transport.mjs → channel-transport.mjs} +127 -46
  195. package/src/standalone/channel-worker.mjs +124 -647
  196. package/src/standalone/{backend-daemon.mjs → daemon.mjs} +270 -154
  197. package/src/standalone/explore-tool.mjs +187 -70
  198. package/src/standalone/fair-call-scheduler.mjs +264 -0
  199. package/src/standalone/memory-runtime-proxy.mjs +58 -1
  200. package/src/standalone/session-client.mjs +1134 -0
  201. package/src/standalone/session-protocol.mjs +192 -0
  202. package/src/standalone/session-runtime-pool.mjs +393 -0
  203. package/src/standalone/session-runtime-record.mjs +20 -0
  204. package/src/standalone/session-runtime-worker.mjs +203 -0
  205. package/src/standalone/{engine-daemon-service.mjs → session-service.mjs} +481 -224
  206. package/src/standalone/{engine-daemon-transport.mjs → session-transport.mjs} +390 -121
  207. package/src/standalone/session-wire.mjs +55 -0
  208. package/src/tui/App.jsx +34 -21
  209. package/src/tui/app/app-format.mjs +1 -1
  210. package/src/tui/app/app-view.jsx +5 -2
  211. package/src/tui/app/channel-pickers.mjs +41 -41
  212. package/src/tui/app/core-memory-picker.mjs +19 -19
  213. package/src/tui/app/doctor.mjs +5 -11
  214. package/src/tui/app/input-parsers.mjs +2 -2
  215. package/src/tui/app/message-selector.mjs +103 -0
  216. package/src/tui/app/onboarding-steps.mjs +3 -4
  217. package/src/tui/app/project-picker.mjs +80 -56
  218. package/src/tui/app/prompt-submit.mjs +48 -43
  219. package/src/tui/app/resume-picker.mjs +2 -2
  220. package/src/tui/app/route-pickers.mjs +3 -1
  221. package/src/tui/app/settings-picker.mjs +31 -33
  222. package/src/tui/app/transcript-window.mjs +38 -0
  223. package/src/tui/app/use-prompt-draft-flow.mjs +5 -7
  224. package/src/tui/app/use-prompt-handlers.mjs +86 -21
  225. package/src/tui/app/use-prompt-hint.mjs +3 -2
  226. package/src/tui/app/use-prompt-queue-history.mjs +10 -3
  227. package/src/tui/app/use-transcript-scroll.mjs +10 -3
  228. package/src/tui/app/use-transcript-window.mjs +50 -17
  229. package/src/tui/components/PromptInput.jsx +71 -38
  230. package/src/tui/components/Spinner.jsx +1 -1
  231. package/src/tui/components/ToolExecution.jsx +1 -1
  232. package/src/tui/components/prompt-input/edit-helpers.mjs +9 -11
  233. package/src/tui/components/prompt-input/escape-policy.mjs +42 -0
  234. package/src/tui/components/prompt-input/immediate-render.mjs +0 -10
  235. package/src/tui/components/prompt-input/interrupt-policy.mjs +10 -0
  236. package/src/tui/components/prompt-input/restore-policy.mjs +10 -0
  237. package/src/tui/components/tool-execution/surface-detail.mjs +1 -1
  238. package/src/tui/dist/index.mjs +1031 -1676
  239. package/src/tui/hooks/useSession.mjs +16 -0
  240. package/src/tui/index.jsx +12 -13
  241. package/src/tui/{engine → session}/agent-envelope.mjs +3 -3
  242. package/src/tui/{engine → session}/agent-job-feed.mjs +4 -4
  243. package/src/tui/{engine → session}/boot-profile.mjs +2 -2
  244. package/src/tui/{engine → session}/context-state.mjs +3 -3
  245. package/src/tui/{engine → session}/labels.mjs +2 -2
  246. package/src/tui/{engine → session}/live-share.mjs +38 -4
  247. package/src/tui/{engine → session}/notice-text.mjs +2 -2
  248. package/src/tui/{engine → session}/notification-plan.mjs +3 -3
  249. package/src/tui/session/oauth-flows.mjs +140 -0
  250. package/src/tui/{engine → session}/prompt-history.mjs +3 -3
  251. package/src/tui/{engine → session}/queue-helpers.mjs +2 -2
  252. package/src/tui/{engine → session}/render-timing.mjs +2 -2
  253. package/src/tui/{engine → session}/session-api-ext.mjs +30 -20
  254. package/src/tui/{engine → session}/session-api.mjs +110 -93
  255. package/src/tui/{engine → session}/session-flow.mjs +8 -2
  256. package/src/tui/{engine → session}/session-stats.mjs +3 -3
  257. package/src/tui/{engine → session}/tool-approval.mjs +3 -3
  258. package/src/tui/{engine → session}/tool-call-fields.mjs +2 -2
  259. package/src/tui/{engine → session}/tool-card-results.mjs +8 -6
  260. package/src/tui/{engine → session}/tool-result-status.mjs +5 -5
  261. package/src/tui/{engine → session}/tool-result-text.mjs +13 -3
  262. package/src/tui/{engine → session}/transcript-spill.mjs +34 -34
  263. package/src/tui/{engine → session}/turn.mjs +38 -176
  264. package/src/tui/{engine-local-session.mjs → session-local.mjs} +103 -103
  265. package/src/tui/session.mjs +22 -0
  266. package/src/tui/spinner-meta.mjs +4 -4
  267. package/src/tui/spinner-verbs.mjs +51 -3
  268. package/src/ui/statusline-segments.mjs +1 -1
  269. package/src/ui/statusline.mjs +1 -1
  270. package/vendor/ink/build/components/App.js +10 -1
  271. package/src/session-runtime/memory-daemon-probe.mjs +0 -61
  272. package/src/standalone/engine-daemon-client.mjs +0 -894
  273. package/src/standalone/engine-daemon-local-bridge.mjs +0 -20
  274. package/src/standalone/engine-daemon-protocol.mjs +0 -33
  275. package/src/tui/engine/turn-watchdog.mjs +0 -92
  276. package/src/tui/engine.mjs +0 -22
  277. package/src/tui/hooks/useEngine.mjs +0 -16
  278. /package/src/runtime/agent/orchestrator/session/compact/{engine.mjs → runner.mjs} +0 -0
  279. /package/src/runtime/channels/{backends → providers}/discord-attachments.mjs +0 -0
  280. /package/src/tui/{engine → session}/agent-response-tail.mjs +0 -0
  281. /package/src/tui/{engine → session}/frame-batched-store.mjs +0 -0
  282. /package/src/tui/{engine → session}/tui-steering-persist.mjs +0 -0
@@ -53,6 +53,7 @@ import {
53
53
  cancelPromptImmediateFlush,
54
54
  schedulePromptImmediateFlush,
55
55
  } from './prompt-input/immediate-render.mjs';
56
+ import { classifyPromptEscape } from './prompt-input/escape-policy.mjs';
56
57
 
57
58
  // Windows Terminal IME composition can clip a glyph that starts exactly at the
58
59
  // left edge of the editable text node. The rounded prompt box already adds a
@@ -86,6 +87,7 @@ export function PromptInput({
86
87
  interruptActive = false,
87
88
  onInterrupt,
88
89
  commandPaletteActive = false,
90
+ commandPaletteOpen = commandPaletteActive,
89
91
  mask = false,
90
92
  hint = '',
91
93
  hintTone = 'info',
@@ -98,6 +100,8 @@ export function PromptInput({
98
100
  onCommandPaletteCancel,
99
101
  onCommandPaletteComplete,
100
102
  onRestoreQueued,
103
+ hasQueuedMessages = false,
104
+ hasMessages = false,
101
105
  onHistoryNavigate,
102
106
  onPasteText,
103
107
  selectionRef,
@@ -112,7 +116,6 @@ export function PromptInput({
112
116
  });
113
117
  const [, bumpCursorAnchorEpoch] = useState(0);
114
118
  const draftRef = useRef(draft);
115
- const interruptActiveRef = useRef(interruptActive);
116
119
  const lastReportedValueRef = useRef(draft.value);
117
120
  // Bumped on every submit/draftOverride replace/unmount so an async paste
118
121
  // (clipboard read or onPasteText promise) that resolves after the draft
@@ -122,6 +125,7 @@ export function PromptInput({
122
125
  // One physical Enter must not enqueue twice when the terminal delivers the
123
126
  // chord as multiple input events before the draft clears (Windows CR/LF).
124
127
  const submitGateRef = useRef(false);
128
+ const escapeClearAtRef = useRef(0);
125
129
  if (valueRef) valueRef.current = draftRef.current.value;
126
130
  const { isRawModeSupported } = useStdin();
127
131
  // The text box's ink DOM node. We mark it as the cursor anchor (forked ink
@@ -141,7 +145,6 @@ export function PromptInput({
141
145
  const undoRef = useRef({ past: [], future: [], lastPushAt: 0, lastValue: null });
142
146
  const { value, cursor } = draft;
143
147
  draftRef.current = draft;
144
- interruptActiveRef.current = interruptActive;
145
148
  if (selectionRef) {
146
149
  const range = selectionRange(draft);
147
150
  selectionRef.current = range
@@ -174,14 +177,12 @@ export function PromptInput({
174
177
  }
175
178
  };
176
179
 
177
- // During an active turn, ink is already painting streaming updates at its
178
- // 60fps budget. A second unthrottled input render competes with those frames
179
- // and makes key echo slower, so busy input stays on ink's normal render path.
180
- // Idle input keeps the leading+trailing immediate flush for crisp caret echo.
180
+ // Keep prompt echo on the same leading+trailing immediate cadence while a
181
+ // turn is active. Claude Code keeps mid-turn typing responsive; suppressing
182
+ // this path made the draft visibly trail the keyboard under streaming load.
181
183
  const scheduleImmediateFlush = () => {
182
184
  schedulePromptImmediateFlush({
183
185
  throttle: flushThrottleRef.current,
184
- isSuppressed: () => interruptActiveRef.current,
185
186
  flush: flushImmediate,
186
187
  });
187
188
  };
@@ -191,6 +192,7 @@ export function PromptInput({
191
192
  }, []);
192
193
 
193
194
  const commitDraft = (next, options = {}) => {
195
+ escapeClearAtRef.current = 0;
194
196
  const sameDraft = draftStateEqual(draftRef.current, next);
195
197
  if (!options.keepPreferredColumn) preferredColumnRef.current = null;
196
198
  if (sameDraft) {
@@ -449,8 +451,12 @@ export function PromptInput({
449
451
  return true;
450
452
  };
451
453
 
452
- const restoreQueuedToDraft = () => {
453
- return onRestoreQueued?.(draftRef.current.value) === true;
454
+ const restoreQueuedToDraft = ({ showHint = false } = {}) => {
455
+ return onRestoreQueued?.({
456
+ restoreDraft: true,
457
+ showHint,
458
+ currentText: draftRef.current.value,
459
+ }) === true;
454
460
  };
455
461
 
456
462
  const applyHistoryNavigation = (direction, meta = {}) => {
@@ -552,6 +558,7 @@ export function PromptInput({
552
558
 
553
559
  const rawInput = String(input ?? '');
554
560
  const inputKey = rawInput.toLowerCase();
561
+ if (!key.escape) escapeClearAtRef.current = 0;
555
562
  const rawShiftArrowForGrid =
556
563
  rawInput === '\x1b[1;2A' || rawInput === '\x1b[a' || rawInput === '[1;2A'
557
564
  || rawInput === '\x1b[1;2B' || rawInput === '\x1b[b' || rawInput === '[1;2B'
@@ -628,8 +635,8 @@ export function PromptInput({
628
635
  const lineBreakIndex = rawInput.search(/[\r\n]/);
629
636
  const rawEnter = rawInput === '\r' || rawInput === '\n' || rawInput === '\r\n';
630
637
  const trailingEnterPrefix = singleTrailingLineBreakPrefix(rawInput);
631
- const rawCtrlEnter = isModifiedEnterSequence(rawInput);
632
- const modifiedLineBreak = key.shift || key.meta || key.ctrl || rawCtrlEnter;
638
+ const rawModifiedEnter = isModifiedEnterSequence(rawInput);
639
+ const modifiedLineBreak = key.shift || key.meta || key.ctrl || rawModifiedEnter;
633
640
 
634
641
  // Ctrl+J is the protocol-INDEPENDENT newline that works on every terminal.
635
642
  // • Legacy / modifyOtherKeys terminals: Ctrl+J is a lone '\n' (0x0A). A real
@@ -646,12 +653,9 @@ export function PromptInput({
646
653
  return;
647
654
  }
648
655
 
649
- // A modified Enter that is NOT a newline chord (e.g. Alt+Enter \x1b[13;3u or
650
- // \x1b[27;3;13~). isModifiedEnterSequence already handled shift/ctrl above
651
- // (→ newline); here we CONSUME any other modified Enter so its raw CSI bytes
652
- // never type into the prompt under modifyOtherKeys. Plain Enter (mod=1) is
653
- // not matched and still submits below.
654
- if (!rawCtrlEnter && isAnyModifiedEnterSequence(rawInput)) {
656
+ // Consume uncommon modified-Enter combinations outside the normal
657
+ // Shift/Alt/Ctrl newline set so raw CSI bytes never enter the prompt.
658
+ if (!rawModifiedEnter && isAnyModifiedEnterSequence(rawInput)) {
655
659
  return;
656
660
  }
657
661
 
@@ -676,7 +680,7 @@ export function PromptInput({
676
680
  return;
677
681
  }
678
682
 
679
- if (rawCtrlEnter) {
683
+ if (rawModifiedEnter) {
680
684
  updateDraft((d) => replaceSelection(d, '\n'));
681
685
  return;
682
686
  }
@@ -745,12 +749,10 @@ export function PromptInput({
745
749
  if (!moveDraftVertically(-1, { extend: true })) {
746
750
  updateDraft((d) => moveCursor(d, 0, { extend: true }));
747
751
  }
748
- } else {
749
- const hasDraftText = String(draftRef.current.value || '').trim().length > 0;
750
- if (!hasDraftText) {
751
- if (!restoreQueuedToDraft()) applyHistoryNavigation('up', { emptyDraft: true });
752
- } else if (!moveDraftVertically(-1, { extend: false })) {
753
- applyHistoryNavigation('up', { emptyDraft: false });
752
+ } else if (!moveDraftVertically(-1, { extend: false })) {
753
+ const emptyDraft = String(draftRef.current.value || '').length === 0;
754
+ if (!hasQueuedMessages || !restoreQueuedToDraft()) {
755
+ applyHistoryNavigation('up', { emptyDraft });
754
756
  }
755
757
  }
756
758
  }
@@ -768,7 +770,7 @@ export function PromptInput({
768
770
  updateDraft((d) => moveCursor(d, d.value.length, { extend: true }));
769
771
  }
770
772
  } else if (!moveDraftVertically(1, { extend: false })) {
771
- applyHistoryNavigation('down', { emptyDraft: String(draftRef.current.value || '').trim().length === 0 });
773
+ applyHistoryNavigation('down', { emptyDraft: String(draftRef.current.value || '').length === 0 });
772
774
  }
773
775
  }
774
776
  return;
@@ -806,9 +808,8 @@ export function PromptInput({
806
808
  }
807
809
 
808
810
  if (key.escape) {
809
- if (commandPaletteActive) {
811
+ if (commandPaletteOpen) {
810
812
  onCommandPaletteCancel?.(draftRef.current.value);
811
- commitDraft({ value: '', cursor: 0, selectionAnchor: null });
812
813
  return;
813
814
  }
814
815
  if (selectionRange(draftRef.current)) {
@@ -819,23 +820,55 @@ export function PromptInput({
819
820
  if (onEscape?.(currentValue, { phase: 'before' }) === true) {
820
821
  return;
821
822
  }
822
- if (currentValue) {
823
+ let escape = classifyPromptEscape({
824
+ interruptActive,
825
+ hasQueuedMessages,
826
+ hasMessages,
827
+ value: currentValue,
828
+ lastClearPressAt: escapeClearAtRef.current,
829
+ });
830
+ if (escape.action === 'restore-queue') {
831
+ if (restoreQueuedToDraft()) {
832
+ escapeClearAtRef.current = 0;
833
+ return;
834
+ }
835
+ // A stale projected queue can empty between render and key handling.
836
+ // Fall through to the normal draft/idle action in that case.
837
+ escape = classifyPromptEscape({
838
+ interruptActive,
839
+ hasMessages,
840
+ value: currentValue,
841
+ lastClearPressAt: escapeClearAtRef.current,
842
+ });
843
+ }
844
+ escapeClearAtRef.current = escape.nextClearPressAt;
845
+ // Active work always wins, even if the user has already typed a steering
846
+ // draft. The draft is preserved; the old submitted prompt is restored only
847
+ // when this box is still empty after cancellation.
848
+ if (escape.action === 'interrupt') {
849
+ const restoredText = onInterrupt?.(currentValue);
850
+ if (!currentValue && typeof restoredText === 'string') {
851
+ commitDraft({ value: restoredText, cursor: restoredText.length, selectionAnchor: null });
852
+ }
853
+ return;
854
+ }
855
+ if (escape.action === 'arm-clear') {
856
+ onEscape?.(currentValue, { phase: 'clear-arm' });
857
+ return;
858
+ }
859
+ if (escape.action === 'clear') {
823
860
  onEscape?.(currentValue, { phase: 'clear' });
824
861
  commitDraft({ value: '', cursor: 0, selectionAnchor: null });
825
862
  return;
826
863
  }
827
- // Active turn takes precedence over queue restore: Esc during a
828
- // running turn interrupts the
829
- // turn and leaves queued steering prompts intact to run afterward. Queued
830
- // messages only pop back into the draft when the turn is idle.
831
- if (interruptActive) {
832
- const restoredText = onInterrupt?.('');
833
- if (typeof restoredText === 'string') {
834
- commitDraft({ value: restoredText, cursor: restoredText.length, selectionAnchor: null });
835
- }
864
+ // Empty draft + conversation history: first press arms, the second
865
+ // opens the message selector (Claude Code's jump-back list).
866
+ if (escape.action === 'arm-select') {
867
+ onEscape?.('', { phase: 'select-arm' });
836
868
  return;
837
869
  }
838
- if (restoreQueuedToDraft()) {
870
+ if (escape.action === 'message-selector') {
871
+ onEscape?.('', { phase: 'select' });
839
872
  return;
840
873
  }
841
874
  onEscape?.('', { phase: 'empty' });
@@ -22,7 +22,7 @@ import { Box, Text } from 'ink';
22
22
  import { useSharedTick } from '../hooks/useSharedTick.mjs';
23
23
  import { theme } from '../theme.mjs';
24
24
  import { SPINNER_FRAMES, SPINNER_MODE_OVERRIDE_VERBS, spinnerVerbFor } from '../spinner-verbs.mjs';
25
- import { SHOW_TOKENS_AFTER_MS, buildSpinnerMeta, isReducedMotion } from '../spinner-meta.mjs';
25
+ import { buildSpinnerMeta, isReducedMotion } from '../spinner-meta.mjs';
26
26
  import { DOWN_ARROW, UP_ARROW } from '../figures.mjs';
27
27
  import { formatDuration } from '../time-format.mjs';
28
28
 
@@ -96,7 +96,7 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
96
96
  const elapsedMs = startedAtMs ? Math.max(0, (pending ? nowMs : (completedAtMs || nowMs)) - startedAtMs) : 0;
97
97
  const elapsed = elapsedMs >= 1000 ? formatElapsed(elapsedMs) : '';
98
98
  const failedCount = clampFailureCount(errorCount, groupCount, isError);
99
- // Real tool-call failures only (backend isError / error toolKind). Drives the
99
+ // Real tool-call failures only (provider isError / error toolKind). Drives the
100
100
  // ● dot color; command/result failures (shell exit, failed status) are counted
101
101
  // in `failedCount`/L2 detail but never in `callFailedCount`, so they never
102
102
  // paint the dot red. Fall back to 0 (never `isError`) when the engine did not
@@ -41,26 +41,24 @@ export function draftStateEqual(a, b) {
41
41
  // Recognize a MODIFIED Enter delivered via the kitty keyboard protocol
42
42
  // (\x1b[13;<mod>u) or modifyOtherKeys (\x1b[27;<mod>;13~). The xterm modifier
43
43
  // param is (1 + bitmask) where the bitmask bits are shift=1, alt=2, ctrl=4. We
44
- // treat Ctrl+Enter AND Shift+Enter (the two common "insert newline" chords) as a
45
- // newline, so we match when the shift OR ctrl bit is set. Ctrl+J is handled
46
- // separately as the protocol-independent fallback.
47
- const MODIFIED_ENTER_SHIFT_OR_CTRL = 1 | 4;
44
+ // treat Shift+Enter, Alt/Meta+Enter, and Ctrl+Enter as newline chords. Claude
45
+ // Code maps Shift/Meta+Enter to newline; Ctrl+Enter remains a compatible Mixdog
46
+ // extension. Ctrl+J is handled separately as the protocol-independent fallback.
47
+ const MODIFIED_ENTER_NEWLINE = 1 | 2 | 4;
48
48
 
49
49
  export function isModifiedEnterSequence(input) {
50
50
  const text = String(input ?? '');
51
51
  const body = text.startsWith('\x1b[') ? text.slice(2) : text.startsWith('[') ? text.slice(1) : '';
52
52
  if (!body) return false;
53
53
  const kitty = /^13;(\d+)(?::\d+)?(?:;[\d:]+)?u$/.exec(body);
54
- if (kitty) return ((Number(kitty[1]) - 1) & MODIFIED_ENTER_SHIFT_OR_CTRL) !== 0;
54
+ if (kitty) return ((Number(kitty[1]) - 1) & MODIFIED_ENTER_NEWLINE) !== 0;
55
55
  const modifyOtherKeys = /^27;(\d+);13~$/.exec(body);
56
- return Boolean(modifyOtherKeys && (((Number(modifyOtherKeys[1]) - 1) & MODIFIED_ENTER_SHIFT_OR_CTRL) !== 0));
56
+ return Boolean(modifyOtherKeys && (((Number(modifyOtherKeys[1]) - 1) & MODIFIED_ENTER_NEWLINE) !== 0));
57
57
  }
58
58
 
59
- // Recognize ANY modified Enter (any modifier bitmask, e.g. Alt+Enter \x1b[13;3u
60
- // / \x1b[27;3;13~). Used to CONSUME modified-Enter sequences we don't map to a
61
- // newline (Alt-only, etc.) so they aren't typed into the prompt as raw CSI text
62
- // under modifyOtherKeys. Plain Enter (mod param = 1, bitmask 0) is intentionally
63
- // NOT matched, so it still submits.
59
+ // Recognize ANY modified Enter. Used to consume uncommon modifier combinations
60
+ // outside the Shift/Alt/Ctrl newline set so raw CSI bytes never reach the draft.
61
+ // Plain Enter (mod param = 1, bitmask 0) intentionally remains a submit.
64
62
  export function isAnyModifiedEnterSequence(input) {
65
63
  const text = String(input ?? '');
66
64
  const body = text.startsWith('\x1b[') ? text.slice(2) : text.startsWith('[') ? text.slice(1) : '';
@@ -0,0 +1,42 @@
1
+ export const PROMPT_ESCAPE_CLEAR_WINDOW_MS = 800;
2
+
3
+ /**
4
+ * Claude Code-compatible chat Escape priority after overlays/selections have
5
+ * already had a chance to consume the key:
6
+ * active turn -> interrupt, idle queue -> restore for editing,
7
+ * idle non-empty draft -> double-press clear, idle empty draft with history ->
8
+ * double-press message selector, otherwise fallback.
9
+ */
10
+ export function classifyPromptEscape({
11
+ interruptActive = false,
12
+ hasQueuedMessages = false,
13
+ hasMessages = false,
14
+ value = '',
15
+ lastClearPressAt = 0,
16
+ now = Date.now(),
17
+ } = {}) {
18
+ if (interruptActive) return { action: 'interrupt', nextClearPressAt: 0 };
19
+ if (hasQueuedMessages) return { action: 'restore-queue', nextClearPressAt: 0 };
20
+
21
+ const current = Number(now);
22
+ const previous = Number(lastClearPressAt);
23
+ const repeated = Number.isFinite(current)
24
+ && Number.isFinite(previous)
25
+ && previous > 0
26
+ && current >= previous
27
+ && current - previous <= PROMPT_ESCAPE_CLEAR_WINDOW_MS;
28
+ const armed = Number.isFinite(current) && current > 0 ? current : 1;
29
+
30
+ // Empty draft: Claude Code's double-press opens the message selector so a
31
+ // previous prompt can be pulled back into the box (nothing to clear here).
32
+ if (String(value ?? '') === '') {
33
+ if (!hasMessages) return { action: 'idle', nextClearPressAt: 0 };
34
+ return repeated
35
+ ? { action: 'message-selector', nextClearPressAt: 0 }
36
+ : { action: 'arm-select', nextClearPressAt: armed };
37
+ }
38
+
39
+ return repeated
40
+ ? { action: 'clear', nextClearPressAt: 0 }
41
+ : { action: 'arm-clear', nextClearPressAt: armed };
42
+ }
@@ -1,7 +1,3 @@
1
- function suppressed(value) {
2
- return typeof value === 'function' ? value() === true : value === true;
3
- }
4
-
5
1
  export function cancelPromptImmediateFlush(throttle, clearTimer = clearTimeout) {
6
2
  if (!throttle || throttle.timer === null) return false;
7
3
  clearTimer(throttle.timer);
@@ -11,7 +7,6 @@ export function cancelPromptImmediateFlush(throttle, clearTimer = clearTimeout)
11
7
 
12
8
  export function schedulePromptImmediateFlush({
13
9
  throttle,
14
- isSuppressed = false,
15
10
  flush,
16
11
  intervalMs = 16,
17
12
  now = Date.now,
@@ -22,10 +17,6 @@ export function schedulePromptImmediateFlush({
22
17
  if (!throttle || typeof flush !== 'function') {
23
18
  throw new TypeError('schedulePromptImmediateFlush requires throttle state and a flush function');
24
19
  }
25
- if (suppressed(isSuppressed)) {
26
- cancelPromptImmediateFlush(throttle, clearTimer);
27
- return false;
28
- }
29
20
 
30
21
  const current = now();
31
22
  const elapsed = current - throttle.lastAt;
@@ -39,7 +30,6 @@ export function schedulePromptImmediateFlush({
39
30
 
40
31
  throttle.timer = setTimer(() => {
41
32
  throttle.timer = null;
42
- if (suppressed(isSuppressed)) return;
43
33
  throttle.lastAt = now();
44
34
  flush();
45
35
  }, intervalMs - elapsed);
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Claude Code restores the submitted prompt only when Esc cancelled before a
3
+ * meaningful response and the user has not typed a replacement meanwhile.
4
+ * The engine owns the response-progress guard; the prompt owns the draft guard.
5
+ */
6
+ export function promptInterruptRestoreText(result, currentText = '') {
7
+ if (!result || result?.aborted === false) return '';
8
+ if (String(currentText ?? '') !== '') return '';
9
+ return String(result?.restoreText || '').trim();
10
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Queue restore RPCs may settle after the user has continued editing. The
3
+ * session service is asked for queued text only; prepend it to the latest local draft
4
+ * so a delayed response can never replace newer typing.
5
+ */
6
+ export function mergeQueuedRestoreText(queuedText = '', currentText = '') {
7
+ return [String(queuedText ?? ''), String(currentText ?? '')]
8
+ .filter((text) => text.trim())
9
+ .join('\n');
10
+ }
@@ -44,7 +44,7 @@ export {
44
44
  // all failed -> theme.error
45
45
  // cancelled -> theme.warning
46
46
  // The RED/orange failure color is driven ONLY by real tool-call errors
47
- // (`callFailedCount` — backend isError / error toolKind), NOT by command/result
47
+ // (`callFailedCount` — provider isError / error toolKind), NOT by command/result
48
48
  // failures like a `[status: failed]` result. A shell command-exit
49
49
  // (`exitFailedCount`) is its own distinct neutral state: warning color, never
50
50
  // red. `terminalStatus` is still consulted so a cancelled card stays warning.