mixdog 0.9.1 → 0.9.3

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 (301) hide show
  1. package/package.json +9 -1
  2. package/scripts/_bench-cwc.json +20 -0
  3. package/scripts/agent-loop-policy-test.mjs +37 -0
  4. package/scripts/agent-parallel-smoke.mjs +54 -10
  5. package/scripts/anthropic-maxtokens-test.mjs +119 -0
  6. package/scripts/background-task-meta-smoke.mjs +1 -1
  7. package/scripts/bench-run.mjs +262 -0
  8. package/scripts/build-tui.mjs +13 -1
  9. package/scripts/compact-smoke.mjs +12 -0
  10. package/scripts/compact-trigger-migration-smoke.mjs +67 -1
  11. package/scripts/explore-bench.mjs +124 -0
  12. package/scripts/hook-bus-test.mjs +191 -0
  13. package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
  14. package/scripts/internal-comms-bench.mjs +727 -0
  15. package/scripts/internal-comms-smoke.mjs +75 -0
  16. package/scripts/lead-workflow-smoke.mjs +4 -4
  17. package/scripts/live-worker-smoke.mjs +9 -9
  18. package/scripts/output-style-bench.mjs +285 -0
  19. package/scripts/output-style-smoke.mjs +13 -10
  20. package/scripts/patch-replay.mjs +90 -0
  21. package/scripts/path-suffix-test.mjs +57 -0
  22. package/scripts/provider-stream-stall-test.mjs +276 -0
  23. package/scripts/provider-toolcall-test.mjs +599 -1
  24. package/scripts/recall-bench.mjs +207 -0
  25. package/scripts/routing-corpus.mjs +281 -0
  26. package/scripts/session-bench.mjs +1526 -0
  27. package/scripts/session-diag.mjs +595 -0
  28. package/scripts/task-bench.mjs +207 -0
  29. package/scripts/tool-failures.mjs +6 -6
  30. package/scripts/tool-smoke.mjs +310 -67
  31. package/scripts/toolcall-args-test.mjs +81 -0
  32. package/src/agents/debugger/AGENT.md +4 -4
  33. package/src/agents/heavy-worker/AGENT.md +20 -9
  34. package/src/agents/reviewer/AGENT.md +4 -4
  35. package/src/agents/worker/AGENT.md +17 -9
  36. package/src/app.mjs +10 -6
  37. package/src/defaults/{hidden-roles.json → agents.json} +7 -7
  38. package/src/examples/schedules/SCHEDULE.example.md +32 -0
  39. package/src/examples/webhooks/WEBHOOK.example.md +40 -0
  40. package/src/headless-role.mjs +14 -14
  41. package/src/help.mjs +1 -0
  42. package/src/lib/rules-builder.cjs +32 -54
  43. package/src/mixdog-session-runtime.mjs +1040 -2036
  44. package/src/output-styles/default.md +12 -7
  45. package/src/output-styles/minimal.md +25 -0
  46. package/src/output-styles/oneline.md +21 -0
  47. package/src/output-styles/simple.md +10 -9
  48. package/src/repl.mjs +17 -7
  49. package/src/rules/agent/00-common.md +7 -5
  50. package/src/rules/agent/30-explorer.md +8 -12
  51. package/src/rules/lead/01-general.md +3 -1
  52. package/src/rules/lead/lead-tool.md +13 -2
  53. package/src/rules/shared/01-tool.md +23 -12
  54. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
  55. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
  56. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
  57. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
  58. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
  59. package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
  60. package/src/runtime/agent/orchestrator/config.mjs +3 -0
  61. package/src/runtime/agent/orchestrator/context/collect.mjs +182 -67
  62. package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
  63. package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
  64. package/src/runtime/agent/orchestrator/mcp/client.mjs +100 -18
  65. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
  66. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
  67. package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
  68. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +93 -0
  69. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +313 -84
  70. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +104 -38
  71. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +28 -33
  72. package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
  73. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
  74. package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +32 -0
  75. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
  76. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
  77. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +227 -31
  78. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +83 -6
  79. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +229 -115
  80. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +213 -33
  81. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
  82. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +49 -17
  83. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
  84. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +22 -17
  85. package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
  86. package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
  87. package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +28 -0
  88. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +262 -0
  89. package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +38 -0
  90. package/src/runtime/agent/orchestrator/session/loop/env.mjs +14 -0
  91. package/src/runtime/agent/orchestrator/session/loop/hidden-agents.mjs +21 -0
  92. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +49 -0
  93. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +63 -0
  94. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +100 -0
  95. package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +52 -0
  96. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +218 -0
  97. package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +101 -0
  98. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +35 -0
  99. package/src/runtime/agent/orchestrator/session/loop.mjs +513 -1000
  100. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +227 -0
  101. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +235 -0
  102. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +137 -0
  103. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +155 -0
  104. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +303 -0
  105. package/src/runtime/agent/orchestrator/session/manager.mjs +232 -1152
  106. package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
  107. package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
  108. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
  109. package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
  110. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -24
  111. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
  112. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
  113. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +241 -0
  114. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +162 -0
  115. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
  116. package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
  117. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
  118. package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +42 -2
  119. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
  120. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
  121. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
  122. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +14 -5
  123. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
  124. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
  125. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +23 -2
  126. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +50 -39
  127. package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
  128. package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
  129. package/src/runtime/agent/orchestrator/tools/builtin.mjs +70 -1
  130. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +303 -0
  131. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +43 -0
  132. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +382 -0
  133. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +497 -0
  134. package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +295 -0
  135. package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +158 -0
  136. package/src/runtime/agent/orchestrator/tools/code-graph/lang-predicates.mjs +128 -0
  137. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +66 -0
  138. package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +44 -0
  139. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1192 -0
  140. package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +81 -0
  141. package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +19 -0
  142. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +280 -0
  143. package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +347 -0
  144. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
  145. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +39 -4189
  146. package/src/runtime/agent/orchestrator/tools/patch.mjs +119 -5
  147. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -2
  148. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -2
  149. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +2 -4
  150. package/src/runtime/channels/backends/discord.mjs +99 -9
  151. package/src/runtime/channels/backends/telegram.mjs +501 -0
  152. package/src/runtime/channels/index.mjs +437 -1439
  153. package/src/runtime/channels/lib/boot-profile.mjs +23 -0
  154. package/src/runtime/channels/lib/config.mjs +54 -2
  155. package/src/runtime/channels/lib/crash-log.mjs +106 -0
  156. package/src/runtime/channels/lib/format.mjs +4 -2
  157. package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
  158. package/src/runtime/channels/lib/output-forwarder.mjs +88 -67
  159. package/src/runtime/channels/lib/runtime-paths.mjs +29 -0
  160. package/src/runtime/channels/lib/scheduler.mjs +1 -1
  161. package/src/runtime/channels/lib/telegram-format.mjs +280 -0
  162. package/src/runtime/channels/lib/tool-format.mjs +1 -1
  163. package/src/runtime/channels/lib/transcript-discovery.mjs +19 -1
  164. package/src/runtime/channels/lib/webhook.mjs +59 -31
  165. package/src/runtime/channels/lib/whisper-language.mjs +42 -0
  166. package/src/runtime/channels/tool-defs.mjs +1 -1
  167. package/src/runtime/memory/index.mjs +465 -345
  168. package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
  169. package/src/runtime/memory/lib/core-memory-store.mjs +352 -2
  170. package/src/runtime/memory/lib/cycle-signatures.mjs +34 -0
  171. package/src/runtime/memory/lib/http-wire.mjs +57 -0
  172. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
  173. package/src/runtime/memory/lib/memory-cycle2.mjs +65 -9
  174. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
  175. package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +24 -0
  176. package/src/runtime/memory/lib/memory-retrievers.mjs +8 -0
  177. package/src/runtime/memory/lib/memory.mjs +121 -4
  178. package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
  179. package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
  180. package/src/runtime/memory/lib/recall-format.mjs +183 -0
  181. package/src/runtime/memory/lib/session-ingest.mjs +107 -0
  182. package/src/runtime/memory/lib/trace-store.mjs +69 -22
  183. package/src/runtime/memory/tool-defs.mjs +10 -7
  184. package/src/runtime/shared/abort-controller.mjs +1 -1
  185. package/src/runtime/shared/background-tasks.mjs +2 -3
  186. package/src/runtime/shared/buffered-appender.mjs +149 -0
  187. package/src/runtime/shared/channel-notification-routing.mjs +12 -0
  188. package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
  189. package/src/runtime/shared/config.mjs +9 -0
  190. package/src/runtime/shared/llm/http-agent.mjs +12 -5
  191. package/src/runtime/shared/schedules-store.mjs +21 -19
  192. package/src/runtime/shared/task-notification-envelope.mjs +98 -0
  193. package/src/runtime/shared/task-notification-envelope.test.mjs +107 -0
  194. package/src/runtime/shared/tool-execution-contract.mjs +2 -2
  195. package/src/runtime/shared/tool-surface.mjs +98 -13
  196. package/src/runtime/shared/transcript-writer.mjs +156 -0
  197. package/src/runtime/shared/update-checker.mjs +214 -0
  198. package/src/session-runtime/config-helpers.mjs +209 -0
  199. package/src/session-runtime/effort.mjs +128 -0
  200. package/src/session-runtime/fs-utils.mjs +10 -0
  201. package/src/session-runtime/model-capabilities.mjs +130 -0
  202. package/src/session-runtime/output-styles.mjs +124 -0
  203. package/src/session-runtime/plugin-mcp.mjs +114 -0
  204. package/src/session-runtime/session-text.mjs +100 -0
  205. package/src/session-runtime/statusline-route.mjs +35 -0
  206. package/src/session-runtime/tool-catalog.mjs +720 -0
  207. package/src/session-runtime/workflow.mjs +358 -0
  208. package/src/standalone/agent-tool.mjs +302 -117
  209. package/src/standalone/channel-admin.mjs +133 -40
  210. package/src/standalone/channel-worker.mjs +10 -292
  211. package/src/standalone/explore-tool.mjs +12 -5
  212. package/src/standalone/hook-bus.mjs +165 -8
  213. package/src/standalone/memory-runtime-proxy.mjs +3 -1
  214. package/src/standalone/opencode-go-login.mjs +121 -0
  215. package/src/standalone/provider-admin.mjs +25 -3
  216. package/src/standalone/usage-dashboard.mjs +1 -1
  217. package/src/tui/App.jsx +2883 -778
  218. package/src/tui/components/ConfirmBar.jsx +47 -0
  219. package/src/tui/components/ContextPanel.jsx +5 -3
  220. package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
  221. package/src/tui/components/Markdown.jsx +22 -98
  222. package/src/tui/components/Message.jsx +14 -35
  223. package/src/tui/components/Picker.jsx +87 -12
  224. package/src/tui/components/PromptInput.jsx +355 -22
  225. package/src/tui/components/QueuedCommands.jsx +1 -1
  226. package/src/tui/components/SlashCommandPalette.jsx +8 -5
  227. package/src/tui/components/Spinner.jsx +7 -7
  228. package/src/tui/components/StatusLine.jsx +40 -21
  229. package/src/tui/components/TextEntryPanel.jsx +51 -7
  230. package/src/tui/components/ToolExecution.jsx +183 -101
  231. package/src/tui/components/TurnDone.jsx +4 -4
  232. package/src/tui/components/UsagePanel.jsx +1 -1
  233. package/src/tui/components/tool-output-format.mjs +161 -23
  234. package/src/tui/components/tool-output-format.test.mjs +87 -0
  235. package/src/tui/display-width.mjs +69 -0
  236. package/src/tui/display-width.test.mjs +35 -0
  237. package/src/tui/dist/index.mjs +6731 -2333
  238. package/src/tui/engine/agent-envelope.mjs +296 -0
  239. package/src/tui/engine/boot-profile.mjs +21 -0
  240. package/src/tui/engine/labels.mjs +67 -0
  241. package/src/tui/engine/notice-text.mjs +112 -0
  242. package/src/tui/engine/queue-helpers.mjs +161 -0
  243. package/src/tui/engine/session-stats.mjs +46 -0
  244. package/src/tui/engine/tool-call-fields.mjs +23 -0
  245. package/src/tui/engine/tool-result-text.mjs +126 -0
  246. package/src/tui/engine.mjs +569 -948
  247. package/src/tui/index.jsx +117 -7
  248. package/src/tui/input-editing.mjs +58 -8
  249. package/src/tui/input-editing.selection.test.mjs +75 -0
  250. package/src/tui/keyboard-protocol.mjs +42 -0
  251. package/src/tui/lib/voice-recorder.mjs +469 -0
  252. package/src/tui/markdown/format-token.mjs +128 -76
  253. package/src/tui/markdown/format-token.test.mjs +61 -19
  254. package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
  255. package/src/tui/markdown/render-ansi.test.mjs +1 -1
  256. package/src/tui/markdown/streaming-markdown.mjs +167 -0
  257. package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
  258. package/src/tui/markdown/table-layout.mjs +9 -9
  259. package/src/tui/paste-attachments.mjs +36 -9
  260. package/src/tui/paste-fix.test.mjs +119 -0
  261. package/src/tui/prompt-history-store.mjs +129 -0
  262. package/src/tui/prompt-history-store.test.mjs +52 -0
  263. package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
  264. package/src/tui/theme.mjs +41 -657
  265. package/src/tui/themes/base.mjs +86 -0
  266. package/src/tui/themes/basic.mjs +85 -0
  267. package/src/tui/themes/catppuccin.mjs +72 -0
  268. package/src/tui/themes/dracula.mjs +70 -0
  269. package/src/tui/themes/everforest.mjs +71 -0
  270. package/src/tui/themes/gruvbox.mjs +71 -0
  271. package/src/tui/themes/index.mjs +71 -0
  272. package/src/tui/themes/indigo.mjs +78 -0
  273. package/src/tui/themes/kanagawa.mjs +80 -0
  274. package/src/tui/themes/light.mjs +81 -0
  275. package/src/tui/themes/nord.mjs +72 -0
  276. package/src/tui/themes/onedark.mjs +16 -0
  277. package/src/tui/themes/rosepine.mjs +70 -0
  278. package/src/tui/themes/teal.mjs +80 -0
  279. package/src/tui/themes/tokyonight.mjs +79 -0
  280. package/src/tui/themes/utils.mjs +106 -0
  281. package/src/tui/themes/warm.mjs +79 -0
  282. package/src/tui/transcript-tool-failures.mjs +13 -2
  283. package/src/ui/markdown.mjs +1 -1
  284. package/src/ui/model-display.mjs +2 -2
  285. package/src/ui/statusline.mjs +75 -27
  286. package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
  287. package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
  288. package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
  289. package/src/workflows/default/WORKFLOW.md +46 -12
  290. package/src/workflows/sequential/WORKFLOW.md +51 -0
  291. package/src/workflows/solo/WORKFLOW.md +12 -1
  292. package/vendor/ink/build/display-width.js +62 -0
  293. package/vendor/ink/build/ink.js +100 -12
  294. package/vendor/ink/build/measure-text.js +4 -1
  295. package/vendor/ink/build/output.js +115 -9
  296. package/vendor/ink/build/render-node-to-output.js +4 -1
  297. package/vendor/ink/build/render.js +4 -0
  298. package/src/output-styles/extreme-simple.md +0 -20
  299. package/src/rules/lead/04-workflow.md +0 -51
  300. package/src/workflows/default/workflow.json +0 -13
  301. package/src/workflows/solo/workflow.json +0 -7
package/src/tui/index.jsx CHANGED
@@ -6,19 +6,24 @@
6
6
  */
7
7
  import React from 'react';
8
8
  import { render } from 'ink';
9
- import { createWriteStream, mkdirSync } from 'node:fs';
9
+ import { closeSync, constants as fsConstants, createWriteStream, mkdirSync, openSync, readSync } from 'node:fs';
10
10
  import { tmpdir } from 'node:os';
11
11
  import { dirname, join } from 'node:path';
12
12
  import { performance } from 'node:perf_hooks';
13
13
  import { App } from './App.jsx';
14
14
  import { createEngineSession } from './engine.mjs';
15
15
  import { installProcessSignalCleanup } from '../runtime/shared/process-shutdown.mjs';
16
+ import { touchUiHeartbeat } from '../runtime/channels/lib/runtime-paths.mjs';
16
17
  import { emitTerminalBackground, loadThemeSettingFromConfig, theme } from './theme.mjs';
18
+ import { POP_KITTY, DISABLE_MODIFY_OTHER_KEYS } from './keyboard-protocol.mjs';
17
19
 
18
20
  const TERMINAL_MODE_RESET = '\x1b[?1006l\x1b[?1005l\x1b[?1015l\x1b[?1003l\x1b[?1002l\x1b[?1000l\x1b[?2004l\x1b[?25h';
19
21
  const TERMINAL_OSC_RESET_BG = '\x1b]111\x07';
20
22
  const TERMINAL_MODE_RESET_HIDDEN_CURSOR = TERMINAL_MODE_RESET.replace('\x1b[?25h', '\x1b[?25l');
21
23
  const MOUSE_TRACKING_ON = '\x1b[?1000h\x1b[?1002h\x1b[?1006h';
24
+ // Keyboard-protocol teardown. App.jsx enables kitty + modifyOtherKeys
25
+ // synchronously at raw-mode-on (no query); here we just pop/disable them on
26
+ // exit. POP_KITTY / DISABLE_MODIFY_OTHER_KEYS come from keyboard-protocol.mjs.
22
27
  const BOOT_PROFILE_ENABLED = /^(1|true|yes|on)$/i.test(String(process.env.MIXDOG_BOOT_PROFILE || ''));
23
28
  const BOOT_PROFILE_START = globalThis.__mixdogBootProfileStart || (globalThis.__mixdogBootProfileStart = performance.now());
24
29
  const EXIT_WAIT_TIMEOUT_MS = positiveIntEnv('MIXDOG_TUI_EXIT_WAIT_MS', 2500);
@@ -136,6 +141,47 @@ function resolveTuiStderrLogPath() {
136
141
  || join(process.env.MIXDOG_RUNTIME_ROOT || join(tmpdir(), 'mixdog'), 'mixdog-tui.stderr.log');
137
142
  }
138
143
 
144
+ /** Drain stdin so queued key/mouse bytes do not leak into the shell after exit. */
145
+ function drainStdin(stdin = process.stdin) {
146
+ if (!stdin.isTTY) return;
147
+ try {
148
+ while (stdin.read() !== null) {
149
+ /* discard */
150
+ }
151
+ } catch {
152
+ /* stream may be destroyed */
153
+ }
154
+ if (process.platform === 'win32') return;
155
+ const tty = stdin;
156
+ const wasRaw = tty.isRaw === true;
157
+ let fd = -1;
158
+ try {
159
+ if (!wasRaw) tty.setRawMode?.(true);
160
+ fd = openSync('/dev/tty', fsConstants.O_RDONLY | fsConstants.O_NONBLOCK);
161
+ const buf = Buffer.alloc(1024);
162
+ for (let i = 0; i < 64; i++) {
163
+ if (readSync(fd, buf, 0, buf.length, null) <= 0) break;
164
+ }
165
+ } catch {
166
+ /* EAGAIN, ENXIO, ENOENT, EBADF, EIO */
167
+ } finally {
168
+ if (fd >= 0) {
169
+ try {
170
+ closeSync(fd);
171
+ } catch {
172
+ /* ignore */
173
+ }
174
+ }
175
+ if (!wasRaw) {
176
+ try {
177
+ tty.setRawMode?.(false);
178
+ } catch {
179
+ /* TTY may be gone */
180
+ }
181
+ }
182
+ }
183
+ }
184
+
139
185
  function installTuiStderrGuard() {
140
186
  if (process.env.MIXDOG_TUI_ALLOW_STDERR === '1') return () => {};
141
187
  const originalWrite = process.stderr.write.bind(process.stderr);
@@ -177,9 +223,9 @@ function installTuiStderrGuard() {
177
223
  };
178
224
  }
179
225
 
180
- export async function runTui({ provider, model, toolMode } = {}) {
226
+ export async function runTui({ provider, model, toolMode, remote, forceOnboarding } = {}) {
181
227
  const startedAt = performance.now();
182
- bootProfile('run:start', { provider, model, toolMode });
228
+ bootProfile('run:start', { provider, model, toolMode, remote });
183
229
  // The React/ink TUI needs a raw-mode-capable TTY (interactive input). In a
184
230
  // pipe/redirect/CI, ink's input hooks throw — bail with a clear hint instead.
185
231
  if (!process.stdin.isTTY) {
@@ -191,13 +237,19 @@ export async function runTui({ provider, model, toolMode } = {}) {
191
237
  }
192
238
 
193
239
  const restoreStderr = installTuiStderrGuard();
194
- const restorePrimedInput = () => {}; // stdin raw mode is owned by Ink's useInput effects
240
+ const restorePrimedInput = () => drainStdin(process.stdin);
195
241
  let restored = false;
196
242
  const restoreTerminal = () => {
197
243
  if (restored) return;
198
244
  restored = true;
199
245
  restorePrimedInput();
200
- try { process.stdout.write(`${TERMINAL_MODE_RESET}\x1b[0 q\x1b[?1049l${TERMINAL_MODE_RESET}${TERMINAL_OSC_RESET_BG}`); } catch { /* ignore */ }
246
+ try {
247
+ process.stdout.write(
248
+ // Pop kitty + disable modifyOtherKeys BEFORE leaving the alt screen.
249
+ // Both are no-ops if nothing was enabled, so this is always safe.
250
+ `${TERMINAL_MODE_RESET}\x1b[0 q${POP_KITTY}${DISABLE_MODIFY_OTHER_KEYS}\x1b[?1049l${TERMINAL_MODE_RESET}${TERMINAL_OSC_RESET_BG}`,
251
+ );
252
+ } catch { /* ignore */ }
201
253
  };
202
254
 
203
255
  // Enter the alternate screen buffer before session/runtime boot so no stale
@@ -211,6 +263,10 @@ export async function runTui({ provider, model, toolMode } = {}) {
211
263
  // fat block. PromptInput parks this hardware cursor at the insertion point via
212
264
  // useCursor; the terminal also anchors IME composition to it.
213
265
  process.stdout.write('\x1b[5 q'); // blinking bar
266
+ // NOTE: extended-keys enabling (kitty + modifyOtherKeys) is done by App.jsx's
267
+ // mount effect, SYNCHRONOUSLY at ink's raw-mode-on — no query, no round-trip —
268
+ // so the first Ctrl+Enter is already covered. Only teardown lives here (see
269
+ // restoreTerminal above).
214
270
 
215
271
  process.on('exit', restoreTerminal);
216
272
 
@@ -223,7 +279,7 @@ export async function runTui({ provider, model, toolMode } = {}) {
223
279
 
224
280
  let store;
225
281
  try {
226
- store = await createEngineSession({ provider, model, toolMode });
282
+ store = await createEngineSession({ provider, model, toolMode, remote });
227
283
  bootProfile('store:ready', { ms: (performance.now() - startedAt).toFixed(1) });
228
284
  } catch (error) {
229
285
  restoreTerminal();
@@ -260,11 +316,58 @@ export async function runTui({ provider, model, toolMode } = {}) {
260
316
  },
261
317
  });
262
318
 
319
+ // Zombie-Lead repro (2026-07-02): the render loop can wedge (all owning
320
+ // PIDs still alive) leaving active-instance.json's pid-only staleness
321
+ // check unable to detect it. Heartbeat every 30s so runtime-paths.mjs can
322
+ // fall back to a ui_heartbeat_at staleness check when pids look fine.
323
+ // Best-effort: a failed/late write just means the next tick catches up,
324
+ // and the timer is unref'd so it never keeps the process alive on its own.
325
+ const uiHeartbeatTimer = setInterval(() => {
326
+ try { touchUiHeartbeat(); } catch { /* ignore */ }
327
+ }, 30_000);
328
+ if (typeof uiHeartbeatTimer.unref === 'function') uiHeartbeatTimer.unref();
329
+ try { touchUiHeartbeat(); } catch { /* ignore */ }
330
+
331
+ // Zombie-Lead repro (2026-07-02): stdio can die (TTY hangup, EPIPE on a
332
+ // detached/piped stdout) without the process ever receiving SIGHUP/SIGTERM,
333
+ // leaving a zombie renderer looping forever with nothing left to draw to.
334
+ // Treat a dead stdio surface as fatal and route it through the same
335
+ // signalCleanup teardown used for signals.
336
+ // 'error' whitelist: only codes that mean the stream is truly gone.
337
+ // Transient EAGAIN (backpressure) or terminal-resize noise must NOT be
338
+ // treated as fatal here — only EPIPE / ERR_STREAM_DESTROYED / EIO.
339
+ const STDIO_DEATH_FATAL_CODES = new Set(['EPIPE', 'ERR_STREAM_DESTROYED', 'EIO']);
340
+ const stdioDeathListeners = [];
341
+ const registerStdioDeath = (stream, event, { requireCode = false } = {}) => {
342
+ if (!stream || typeof stream.on !== 'function') return;
343
+ const handler = (err) => {
344
+ if (requireCode && !(err && STDIO_DEATH_FATAL_CODES.has(err.code))) return;
345
+ void signalCleanup.run('stdio-dead', {
346
+ code: 1,
347
+ shouldExit: true,
348
+ error: err || new Error(`stdio ${event} (source: ${stream === process.stdin ? 'stdin' : stream === process.stdout ? 'stdout' : 'stderr'})`),
349
+ });
350
+ };
351
+ stream.on(event, handler);
352
+ stdioDeathListeners.push([stream, event, handler]);
353
+ };
354
+ // stdin end/close/error: TTY/pipe on the input side is gone — nothing left
355
+ // to read from, no point keeping the renderer alive.
356
+ registerStdioDeath(process.stdin, 'end');
357
+ registerStdioDeath(process.stdin, 'close');
358
+ registerStdioDeath(process.stdin, 'error', { requireCode: true });
359
+ // stdout/stderr: 'close' always means the fd is gone (fatal); 'error' is
360
+ // filtered to the whitelist above so resize/EAGAIN noise doesn't kill us.
361
+ registerStdioDeath(process.stdout, 'error', { requireCode: true });
362
+ registerStdioDeath(process.stdout, 'close');
363
+ registerStdioDeath(process.stderr, 'error', { requireCode: true });
364
+ registerStdioDeath(process.stderr, 'close');
365
+
263
366
  // exitOnCtrlC:false — App handles Ctrl+C as an interrupt/line-clear so Ink
264
367
  // does not exit abruptly. Explicit exits go through /exit or /quit so teardown
265
368
  // still restores the cursor, mouse mode, and alternate screen cleanly.
266
369
  try {
267
- const instance = render(<App store={store} />, { exitOnCtrlC: false, maxFps: 120, onRender: makeRenderProfiler() });
370
+ const instance = render(<App store={store} forceOnboarding={forceOnboarding === true} />, { exitOnCtrlC: false, maxFps: 120, onRender: makeRenderProfiler() });
268
371
  bootProfile('render:mounted', { ms: (performance.now() - startedAt).toFixed(1) });
269
372
  const { waitUntilExit } = instance;
270
373
  // [mixdog fork] Hand the ink renderer's drag-selection setter to the store so
@@ -281,8 +384,15 @@ export async function runTui({ provider, model, toolMode } = {}) {
281
384
  if (mouseTracking && typeof instance.getWordRectAt === 'function') {
282
385
  store.getWordRectAt = instance.getWordRectAt;
283
386
  }
387
+ if (mouseTracking && typeof instance.getLineRectAt === 'function') {
388
+ store.getLineRectAt = instance.getLineRectAt;
389
+ }
284
390
  await waitUntilExit();
285
391
  } finally {
392
+ clearInterval(uiHeartbeatTimer);
393
+ for (const [stream, event, handler] of stdioDeathListeners.splice(0)) {
394
+ try { stream.off(event, handler); } catch { /* ignore */ }
395
+ }
286
396
  signalCleanup.uninstall();
287
397
  try {
288
398
  await disposeStoreOnce('cli-react-exit');
@@ -122,14 +122,52 @@ export function lineEnd(text, offset) {
122
122
  return end === -1 ? value.length : end;
123
123
  }
124
124
 
125
- export function caretPosition(text, offset, width) {
125
+ // Return the [start,end) run of same-class characters (word-like vs
126
+ // non-word-like) surrounding `offset`, used by double-click word selection.
127
+ // Mirrors typical desktop double-click behavior: clicking inside a run of
128
+ // letters/digits selects that whole run; clicking on a run of punctuation or
129
+ // whitespace selects that run instead (still deterministic, never empty).
130
+ export function wordRangeAt(text, offset) {
131
+ const value = String(text || '');
132
+ const units = graphemeUnits(value);
133
+ if (units.length === 0) return { start: 0, end: 0 };
134
+ const off = clampOffset(value, offset);
135
+ let idx = units.findIndex((u) => off >= u.start && off < u.end);
136
+ if (idx === -1) {
137
+ idx = units.length - 1;
138
+ for (let i = 0; i < units.length; i += 1) {
139
+ if (units[i].start >= off) { idx = i > 0 ? i - 1 : 0; break; }
140
+ }
141
+ }
142
+ const targetIsWord = wordLike(units[idx].segment);
143
+ let start = idx;
144
+ while (start > 0 && wordLike(units[start - 1].segment) === targetIsWord) start -= 1;
145
+ let end = idx;
146
+ while (end < units.length - 1 && wordLike(units[end + 1].segment) === targetIsWord) end += 1;
147
+ return { start: units[start].start, end: units[end].end };
148
+ }
149
+
150
+ // `hasTrailingContent` (default derived from the full text) decides whether a
151
+ // caret that lands FLUSH on the last column (col === w) rolls to the next row.
152
+ // It must reflect whether ANY rendered cell follows this offset in the FULL
153
+ // text — not just the sliced prefix — otherwise a caret at a mid-text soft-wrap
154
+ // boundary reports {row N, col w} while ink already rendered the next glyph on
155
+ // row N+1, dropping the caret outside the box. PromptInput also appends a
156
+ // trailing space cell when the cursor sits at end-of-input, so "end of text"
157
+ // still has a following cell there; callers pass hasTrailingContent=true for
158
+ // that case.
159
+ export function caretPosition(text, offset, width, hasTrailingContent = undefined) {
126
160
  const value = String(text || '');
127
161
  const before = value.slice(0, offset);
128
162
  const w = safeWidth(width);
163
+ const followsInFullText = hasTrailingContent === undefined
164
+ ? offset < value.length
165
+ : hasTrailingContent === true;
129
166
  let row = 0;
130
167
  let col = 0;
131
-
132
- for (const { segment } of graphemeSegmenter.segment(before)) {
168
+ const segments = [...graphemeSegmenter.segment(before)];
169
+ for (let i = 0; i < segments.length; i += 1) {
170
+ const { segment } = segments[i];
133
171
  if (segment === '\n') {
134
172
  row += 1;
135
173
  col = 0;
@@ -139,15 +177,24 @@ export function caretPosition(text, offset, width) {
139
177
  const segmentWidth = stringWidth(segment);
140
178
  if (segmentWidth === 0) continue;
141
179
 
180
+ // Mirror ink's wrap-ansi wrapWord (wrap="hard", wordWrap:false): a glyph
181
+ // that would overflow the row is pushed WHOLE to the next row (col never
182
+ // exceeds w for wide chars — no half-column left behind), and a glyph that
183
+ // lands flush on the last column starts a new row ONLY when more rendered
184
+ // content follows. Whether content follows is decided from the FULL text
185
+ // (followsInFullText), not the sliced prefix, so a caret at a mid-text wrap
186
+ // boundary rolls to row N+1 exactly as ink renders it. The old
187
+ // `col >= w → row += floor(col/w)` over-counted a row for width-2 glyphs at
188
+ // odd widths (col could reach w+1), landing the cursor one row too low.
142
189
  if (col > 0 && col + segmentWidth > w) {
143
190
  row += 1;
144
191
  col = 0;
145
192
  }
146
-
147
193
  col += segmentWidth;
148
- if (col >= w) {
149
- row += Math.floor(col / w);
150
- col %= w;
194
+ const moreFollows = i < segments.length - 1 || followsInFullText;
195
+ if (col === w && moreFollows) {
196
+ row += 1;
197
+ col = 0;
151
198
  }
152
199
  }
153
200
 
@@ -185,13 +232,16 @@ function boundaryPositions(text, width) {
185
232
  for (const unit of graphemeUnits(value)) offsets.push(unit.end);
186
233
 
187
234
  const seen = new Set();
188
- return offsets
235
+ const positions = offsets
189
236
  .filter((offset) => {
190
237
  if (seen.has(offset)) return false;
191
238
  seen.add(offset);
192
239
  return true;
193
240
  })
241
+ // Decide the flush-boundary advance from the FULL text so a soft-wrap
242
+ // offset reports {row N+1, col 0} (matching ink) instead of {row N, col w}.
194
243
  .map((offset) => ({ offset, ...caretPosition(value, offset, width) }));
244
+ return positions;
195
245
  }
196
246
 
197
247
  export function verticalOffset(text, offset, width, direction, preferredColumn = null) {
@@ -0,0 +1,75 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {
4
+ moveCursor,
5
+ previousWordOffset,
6
+ nextWordOffset,
7
+ selectionRange,
8
+ verticalOffset,
9
+ wordRangeAt,
10
+ lineStart,
11
+ lineEnd,
12
+ } from './input-editing.mjs';
13
+
14
+ // Whole-word extend (Ctrl+Shift+Left/Right) relies on moveCursor(extend:true)
15
+ // starting/keeping an anchor while jumping by word.
16
+ test('ctrl+shift+right extends selection by whole word', () => {
17
+ const draft = { value: 'hello world foo', cursor: 0, selectionAnchor: null };
18
+ const next = moveCursor(draft, nextWordOffset(draft.value, draft.cursor), { extend: true });
19
+ assert.equal(next.cursor, 'hello'.length);
20
+ assert.equal(next.selectionAnchor, 0);
21
+ assert.deepEqual(selectionRange(next), { start: 0, end: 5 });
22
+ });
23
+
24
+ test('ctrl+shift+left extends selection backward by whole word', () => {
25
+ const value = 'hello world';
26
+ const draft = { value, cursor: value.length, selectionAnchor: null };
27
+ const next = moveCursor(draft, previousWordOffset(value, value.length), { extend: true });
28
+ assert.equal(next.selectionAnchor, value.length);
29
+ assert.deepEqual(selectionRange(next), { start: 'hello '.length, end: value.length });
30
+ });
31
+
32
+ // Shift+Up on the first line yields no vertical move (targetRow < 0) → callers
33
+ // then extend to document start (offset 0).
34
+ test('verticalOffset returns same cursor on first line (up)', () => {
35
+ const value = 'line1\nline2';
36
+ const moved = verticalOffset(value, 2, 80, -1, null);
37
+ assert.equal(moved.cursor, 2, 'no vertical move available above first line');
38
+ });
39
+
40
+ test('shift+up to document start extends selection to offset 0', () => {
41
+ const value = 'abc';
42
+ const draft = { value, cursor: 2, selectionAnchor: null };
43
+ const next = moveCursor(draft, 0, { extend: true });
44
+ assert.deepEqual(selectionRange(next), { start: 0, end: 2 });
45
+ });
46
+
47
+ test('shift+down to document end extends selection to value.length', () => {
48
+ const value = 'abc';
49
+ const draft = { value, cursor: 1, selectionAnchor: null };
50
+ const next = moveCursor(draft, value.length, { extend: true });
51
+ assert.deepEqual(selectionRange(next), { start: 1, end: 3 });
52
+ });
53
+
54
+ // Double-click word select: clicking anywhere inside a word run selects the
55
+ // whole run, and a click on punctuation/whitespace selects that run instead.
56
+ test('wordRangeAt selects the word run under the offset', () => {
57
+ const value = 'hello world foo';
58
+ assert.deepEqual(wordRangeAt(value, 2), { start: 0, end: 5 });
59
+ assert.deepEqual(wordRangeAt(value, 6), { start: 6, end: 11 });
60
+ assert.deepEqual(wordRangeAt(value, 0), { start: 0, end: 5 });
61
+ assert.deepEqual(wordRangeAt(value, value.length), { start: 12, end: 15 });
62
+ });
63
+
64
+ test('wordRangeAt selects a punctuation/whitespace run distinctly from words', () => {
65
+ const value = 'foo---bar';
66
+ assert.deepEqual(wordRangeAt(value, 4), { start: 3, end: 6 });
67
+ });
68
+
69
+ // Triple-click line select uses lineStart/lineEnd directly.
70
+ test('lineStart/lineEnd bound the current line for triple-click select', () => {
71
+ const value = 'line1\nline2\nline3';
72
+ const offset = value.indexOf('line2') + 2;
73
+ assert.equal(lineStart(value, offset), 6);
74
+ assert.equal(lineEnd(value, offset), 11);
75
+ });
@@ -0,0 +1,42 @@
1
+ /**
2
+ * src/tui/keyboard-protocol.mjs — terminal extended-keys capability gate.
3
+ *
4
+ * We do NOT query the terminal. Instead, we enable the
5
+ * kitty keyboard protocol AND xterm modifyOtherKeys SYNCHRONOUSLY at the moment
6
+ * raw mode turns on (see App.jsx's mount effect), gated by the allowlist below.
7
+ * Writing both enables unconditionally is safe — a terminal honors whichever it
8
+ * implements (Windows Terminal 1.24 has no kitty support but DOES honor
9
+ * modifyOtherKeys), and doing it before the first keypair is read removes the
10
+ * round-trip race that made the first Ctrl+Enter submit instead of inserting a
11
+ * newline.
12
+ *
13
+ * Enable / disable byte sequences (used by App.jsx enable + index.jsx teardown):
14
+ * ENABLE_KITTY_KEYBOARD \x1b[>1u push kitty flags=1 (disambiguate)
15
+ * ENABLE_MODIFY_OTHER_KEYS \x1b[>4;2m xterm modifyOtherKeys level 2
16
+ * POP_KITTY \x1b[<u pop the kitty stack entry
17
+ * DISABLE_MODIFY_OTHER_KEYS \x1b[>4;0m modifyOtherKeys off
18
+ */
19
+ export const ENABLE_KITTY_KEYBOARD = '\x1b[>1u';
20
+ export const ENABLE_MODIFY_OTHER_KEYS = '\x1b[>4;2m';
21
+ export const POP_KITTY = '\x1b[<u';
22
+ export const DISABLE_MODIFY_OTHER_KEYS = '\x1b[>4;0m';
23
+
24
+ // Allowlist of terminals known to honor kitty and/or xterm modifyOtherKeys.
25
+ // VS Code's xterm.js
26
+ // integrated terminal mishandles these sequences, so it is excluded first.
27
+ // Honors the MIXDOG_TUI_EXTENDED_KEYS opt-out (=0) / override (=1).
28
+ export function supportsExtendedKeys() {
29
+ const raw = String(process.env.MIXDOG_TUI_EXTENDED_KEYS ?? '').trim();
30
+ if (/^(0|false|no|off)$/i.test(raw)) return false;
31
+ if (/^(1|true|yes|on)$/i.test(raw)) return true;
32
+ if (process.env.TERM_PROGRAM === 'vscode') return false;
33
+ if (process.env.WT_SESSION) return true; // Windows Terminal
34
+ if (process.env.TERM?.includes('kitty')) return true;
35
+ if (process.env.KITTY_WINDOW_ID) return true;
36
+ if (process.env.TERM_PROGRAM === 'WezTerm') return true;
37
+ if (process.env.TERM_PROGRAM === 'ghostty') return true;
38
+ if (process.env.TERM === 'xterm-ghostty') return true;
39
+ if (process.env.TMUX) return true;
40
+ if (process.env.TERM_PROGRAM === 'iTerm.app') return true;
41
+ return false;
42
+ }