mixdog 0.9.147 → 0.9.149

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 (265) hide show
  1. package/package.json +23 -3
  2. package/scripts/build-voice-runtime-unix.sh +101 -0
  3. package/scripts/embedding-quality-probe.mjs +96 -0
  4. package/scripts/generate-voice-runtime-manifest.mjs +300 -0
  5. package/scripts/generate-voice-runtime-manifest.test.mjs +135 -0
  6. package/scripts/native-binary-arch.mjs +30 -13
  7. package/scripts/native-binary-arch.test.mjs +40 -4
  8. package/scripts/plan-voice-runtime-release.mjs +73 -0
  9. package/scripts/prepare-native-assets.mjs +50 -9
  10. package/scripts/prune-embedding-runtime.mjs +3 -3
  11. package/scripts/sync-voice-runtime-manifest.mjs +46 -0
  12. package/scripts/verify-release-assets.mjs +106 -6
  13. package/scripts/verify-voice-runtime-release.mjs +89 -0
  14. package/scripts/voice-runtime-config.json +52 -0
  15. package/src/app.mjs +42 -5
  16. package/src/headless-exec.mjs +58 -32
  17. package/src/headless-exec.test.mjs +74 -13
  18. package/src/rules/lead/01-general.md +2 -0
  19. package/src/rules/shared/10-tool-workflow.md +7 -4
  20. package/src/rules/shared/30-exploration.md +5 -1
  21. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +28 -9
  22. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +6 -1
  23. package/src/runtime/agent/orchestrator/config.mjs +53 -18
  24. package/src/runtime/agent/orchestrator/mcp/cancellation.test.mjs +139 -0
  25. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +34 -5
  26. package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -10
  27. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +5 -0
  28. package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +98 -0
  29. package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.test.mjs +90 -0
  30. package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +67 -16
  31. package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +4 -82
  32. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +0 -5
  33. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +62 -121
  34. package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.mjs +48 -0
  35. package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.test.mjs +215 -0
  36. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +44 -48
  37. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +54 -31
  38. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +56 -2
  39. package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +34 -2
  40. package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +61 -1
  41. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +6 -1
  42. package/src/runtime/agent/orchestrator/providers/gemini.mjs +61 -15
  43. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +120 -1
  44. package/src/runtime/agent/orchestrator/providers/lib/env-utils.mjs +9 -0
  45. package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +0 -3
  46. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +187 -84
  47. package/src/runtime/agent/orchestrator/providers/openai-codex-identity.test.mjs +32 -9
  48. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +78 -33
  49. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +21 -0
  50. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +36 -36
  51. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +16 -6
  52. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +50 -213
  53. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +135 -126
  54. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +20 -11
  55. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +112 -36
  56. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +220 -50
  57. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +112 -9
  58. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +11 -0
  59. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +130 -60
  60. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -2
  61. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +28 -10
  62. package/src/runtime/agent/orchestrator/providers/registry.mjs +122 -95
  63. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +69 -3
  64. package/src/runtime/agent/orchestrator/providers/tool-stream-state.mjs +4 -22
  65. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +7 -14
  66. package/src/runtime/agent/orchestrator/session/compact-policy.test.mjs +128 -0
  67. package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +5 -0
  68. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +27 -48
  69. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +19 -14
  70. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +9 -43
  71. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +5 -1
  72. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +43 -5
  73. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +15 -2
  74. package/src/runtime/agent/orchestrator/session/manager/pending-messages-persistence.test.mjs +325 -0
  75. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +599 -195
  76. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +22 -0
  77. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +9 -2
  78. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +2 -0
  79. package/src/runtime/agent/orchestrator/session/manager.mjs +3 -0
  80. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +26 -16
  81. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +23 -11
  82. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +118 -3
  83. package/src/runtime/agent/orchestrator/session/store-summary-reader.test.mjs +332 -0
  84. package/src/runtime/agent/orchestrator/session/store.mjs +38 -1
  85. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +27 -6
  86. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -20
  87. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +291 -128
  88. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +46 -9
  89. package/src/runtime/agent/orchestrator/tools/builtin/edit-encoding-eol.test.mjs +224 -0
  90. package/src/runtime/agent/orchestrator/tools/builtin/edit-sequential-occupation.test.mjs +53 -3
  91. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +255 -25
  92. package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +87 -0
  93. package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +1 -1
  94. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +132 -9
  95. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +10 -0
  96. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-pattern-fanout.mjs +29 -3
  97. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +1 -1
  98. package/src/runtime/agent/orchestrator/tools/builtin/list-tool-integrity.test.mjs +14 -0
  99. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +180 -31
  100. package/src/runtime/agent/orchestrator/tools/builtin/local-search-telemetry.mjs +4 -1
  101. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +32 -5
  102. package/src/runtime/agent/orchestrator/tools/builtin/native-search-health.test.mjs +5 -2
  103. package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +10 -1
  104. package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +38 -10
  105. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +12 -0
  106. package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +31 -1
  107. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +11 -1
  108. package/src/runtime/agent/orchestrator/tools/builtin/read-windows.mjs +22 -4
  109. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +6 -8
  110. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +25 -24
  111. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +152 -133
  112. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +114 -42
  113. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +129 -15
  114. package/src/runtime/agent/orchestrator/tools/code-graph/loose-file-target.test.mjs +72 -0
  115. package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +7 -8
  116. package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +4 -4
  117. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +201 -19
  118. package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +26 -3
  119. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +10 -1
  120. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-search-scope.test.mjs +55 -0
  121. package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +115 -25
  122. package/src/runtime/agent/orchestrator/tools/code-graph/text-span-mask.test.mjs +268 -0
  123. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +3 -3
  124. package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +296 -46
  125. package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.test.mjs +104 -0
  126. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +136 -0
  127. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +127 -38
  128. package/src/runtime/agent/orchestrator/tools/patch/engine-contract-gate.test.mjs +661 -0
  129. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +350 -9
  130. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +418 -44
  131. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +27 -3
  132. package/src/runtime/agent/orchestrator/tools/patch/patch-codec-eol.test.mjs +552 -0
  133. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +65 -15
  134. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  135. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +284 -109
  136. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +51 -21
  137. package/src/runtime/agent/orchestrator/tools/shell-exec-policy.mjs +44 -27
  138. package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +6 -1
  139. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
  140. package/src/runtime/attachments/store.mjs +11 -3
  141. package/src/runtime/channels/data/voice-runtime-manifest.json +46 -70
  142. package/src/runtime/channels/lib/event-queue.mjs +51 -41
  143. package/src/runtime/channels/lib/scheduler.mjs +23 -28
  144. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -17
  145. package/src/runtime/channels/lib/voice-runtime-fetcher.test.mjs +17 -0
  146. package/src/runtime/channels/lib/webhook.mjs +39 -20
  147. package/src/runtime/channels/lib/whisper-server.mjs +46 -1
  148. package/src/runtime/channels/lib/worker-main.mjs +3 -1
  149. package/src/runtime/media/adapters/gemini-video.mjs +20 -4
  150. package/src/runtime/media/jobs.mjs +13 -0
  151. package/src/runtime/media/renditions.mjs +7 -1
  152. package/src/runtime/media/store.mjs +42 -22
  153. package/src/runtime/memory/index.mjs +4 -9
  154. package/src/runtime/memory/lib/agent-ipc.mjs +1 -6
  155. package/src/runtime/memory/lib/archive-security.test.mjs +21 -1
  156. package/src/runtime/memory/lib/core-memory-store.mjs +28 -50
  157. package/src/runtime/memory/lib/embedding-model-config.mjs +37 -1
  158. package/src/runtime/memory/lib/embedding-model-config.test.mjs +33 -0
  159. package/src/runtime/memory/lib/embedding-provider.mjs +34 -12
  160. package/src/runtime/memory/lib/embedding-worker.mjs +37 -5
  161. package/src/runtime/memory/lib/ko-morph.mjs +46 -15
  162. package/src/runtime/memory/lib/memory-action-handlers.mjs +18 -1
  163. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -18
  164. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +1 -18
  165. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +98 -45
  166. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +71 -0
  167. package/src/runtime/memory/lib/memory-cycle2-shared.test.mjs +78 -0
  168. package/src/runtime/memory/lib/memory-cycle2.mjs +37 -3
  169. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
  170. package/src/runtime/memory/lib/memory-embedding-migration.test.mjs +50 -0
  171. package/src/runtime/memory/lib/memory-recall-store.mjs +9 -24
  172. package/src/runtime/memory/lib/memory.mjs +50 -12
  173. package/src/runtime/memory/lib/pg/process.mjs +1 -10
  174. package/src/runtime/memory/lib/pg/supervisor.mjs +1 -9
  175. package/src/runtime/memory/lib/query-handlers.mjs +2 -4
  176. package/src/runtime/memory/lib/session-ingest-runtime.mjs +2 -2
  177. package/src/runtime/shared/background-tasks.mjs +15 -0
  178. package/src/runtime/shared/background-tasks.test.mjs +78 -0
  179. package/src/runtime/shared/child-guardian.mjs +2 -10
  180. package/src/runtime/shared/pid-liveness.mjs +27 -0
  181. package/src/runtime/shared/pristine-execution-contract.json +0 -1
  182. package/src/runtime/shared/runtime-root.mjs +1 -11
  183. package/src/runtime/shared/service-discovery.mjs +1 -17
  184. package/src/runtime/shared/singleton-owner.mjs +1 -11
  185. package/src/runtime/shared/staged-update.mjs +2 -12
  186. package/src/runtime/shared/tool-status.mjs +5 -1
  187. package/src/runtime/shared/tool-surface.mjs +15 -7
  188. package/src/runtime/shared/turn-snapshot-store.mjs +168 -0
  189. package/src/runtime/shared/turn-snapshot.mjs +137 -13
  190. package/src/runtime/shared/turn-worktree-snapshot.mjs +110 -18
  191. package/src/runtime/shared/webhook-session-run.mjs +16 -2
  192. package/src/runtime/web-search/lib/http-fetch.mjs +34 -4
  193. package/src/runtime/web-search/lib/state.mjs +15 -15
  194. package/src/runtime/web-search/lib/web-tools.mjs +46 -5
  195. package/src/session-runtime/context-status.mjs +4 -12
  196. package/src/session-runtime/runtime-core.mjs +20 -1
  197. package/src/session-runtime/session-lifecycle.mjs +9 -0
  198. package/src/standalone/agent-dispatch-broker.mjs +33 -2
  199. package/src/standalone/agent-tool/job-task-reconcile.mjs +78 -0
  200. package/src/standalone/agent-tool/notify.mjs +3 -1
  201. package/src/standalone/agent-tool/spawn-flow.mjs +52 -70
  202. package/src/standalone/agent-tool/tag-registry.mjs +116 -33
  203. package/src/standalone/agent-tool/tool-def.mjs +2 -2
  204. package/src/standalone/agent-tool/worker-index.mjs +8 -11
  205. package/src/standalone/agent-tool/worker-rows.mjs +4 -1
  206. package/src/standalone/agent-tool.mjs +20 -55
  207. package/src/standalone/agent-watchdog-registry.mjs +28 -4
  208. package/src/standalone/channel-client.mjs +47 -15
  209. package/src/standalone/channel-transport.mjs +44 -25
  210. package/src/standalone/daemon.mjs +31 -2
  211. package/src/standalone/hook-bus/config.mjs +25 -9
  212. package/src/standalone/hook-bus/handlers.mjs +16 -52
  213. package/src/standalone/hook-bus.mjs +29 -23
  214. package/src/standalone/memory-runtime-proxy.mjs +1 -16
  215. package/src/standalone/session-client.mjs +86 -23
  216. package/src/standalone/session-protocol.mjs +0 -2
  217. package/src/standalone/session-runtime-host-health.test.mjs +5 -4
  218. package/src/standalone/session-runtime-host.mjs +101 -6
  219. package/src/standalone/session-runtime-record.mjs +1 -5
  220. package/src/standalone/session-runtime-worker.mjs +52 -57
  221. package/src/standalone/session-service.mjs +4 -1
  222. package/src/standalone/session-transport.mjs +56 -25
  223. package/src/tui/App.jsx +40 -49
  224. package/src/tui/app/app-view.jsx +14 -21
  225. package/src/tui/app/core-memory-picker.mjs +29 -16
  226. package/src/tui/app/create-app-pickers.mjs +15 -36
  227. package/src/tui/app/extension-pickers.mjs +60 -49
  228. package/src/tui/app/maintenance-pickers.mjs +100 -57
  229. package/src/tui/app/message-selector.mjs +6 -5
  230. package/src/tui/app/model-picker.mjs +30 -13
  231. package/src/tui/app/onboarding-steps.mjs +21 -12
  232. package/src/tui/app/panel-epoch.mjs +56 -0
  233. package/src/tui/app/panel-epoch.test.mjs +1009 -0
  234. package/src/tui/app/panel-surface.mjs +98 -0
  235. package/src/tui/app/project-picker.mjs +30 -29
  236. package/src/tui/app/prompt-submit.mjs +192 -97
  237. package/src/tui/app/prompt-submit.test.mjs +206 -0
  238. package/src/tui/app/provider-setup-picker.mjs +74 -43
  239. package/src/tui/app/resume-picker.mjs +11 -8
  240. package/src/tui/app/route-pickers.mjs +45 -28
  241. package/src/tui/app/settings-picker.mjs +69 -18
  242. package/src/tui/app/shell-layout.mjs +1 -3
  243. package/src/tui/app/slash-commands.mjs +0 -2
  244. package/src/tui/app/slash-dispatch.mjs +4 -2
  245. package/src/tui/app/text-entry-policy.mjs +25 -0
  246. package/src/tui/app/theme-effort-pickers.mjs +11 -16
  247. package/src/tui/app/usage-context-panels.mjs +21 -25
  248. package/src/tui/app/use-global-key-input.mjs +3 -2
  249. package/src/tui/app/use-prompt-draft-flow.mjs +6 -18
  250. package/src/tui/app/use-prompt-handlers.mjs +3 -2
  251. package/src/tui/app/use-welcome-prompt-hint.mjs +17 -3
  252. package/src/tui/components/Picker.jsx +1 -1
  253. package/src/tui/components/TextEntryPanel.jsx +7 -2
  254. package/src/tui/dist/index.mjs +652 -561
  255. package/src/tui/lib/voice-setup.mjs +1 -2
  256. package/src/tui/session/agent-job-feed.mjs +73 -11
  257. package/src/tui/session/session-api-ext.mjs +5 -6
  258. package/src/tui/session/session-api.mjs +1 -0
  259. package/src/tui/session/tool-approval.mjs +15 -5
  260. package/src/tui/session/tui-steering-persist.mjs +33 -19
  261. package/src/tui/session/tui-steering-persist.test.mjs +104 -0
  262. package/src/tui/session-local.mjs +0 -22
  263. package/scripts/.tmp-coverage-report.mjs +0 -96
  264. package/src/runtime/agent/orchestrator/tools/builtin/shell-monitor.mjs +0 -57
  265. package/src/runtime/agent/orchestrator/tools/builtin/shell-monitor.test.mjs +0 -57
@@ -2,25 +2,33 @@
2
2
  * maintenance-pickers.mjs — Update / Auto-clear / Profile picker cluster.
3
3
  *
4
4
  * Extracted from App.jsx behavior-preservingly as a dependency-injection
5
- * factory. These openers drive setPicker + setSettingsPrompt and read live
6
- * store state, so they can't be pure. Every function body is the original App
7
- * logic verbatim, with closure identifiers threaded through the factory
5
+ * factory. These openers drive the panel surface + setSettingsPrompt and read
6
+ * live store state, so they can't be pure. Every function body is the original
7
+ * App logic verbatim, with closure identifiers threaded through the factory
8
8
  * argument.
9
9
  */
10
10
  export function createMaintenancePickers({
11
11
  store,
12
12
  theme,
13
13
  formatDuration,
14
- setPicker,
14
+ surface,
15
15
  setProviderPrompt,
16
- setChannelPrompt,
17
- setHookPrompt,
18
16
  setSettingsPrompt,
19
- setContextPanel,
20
17
  closeUsagePanel,
21
18
  }) {
22
19
  const openUpdatePicker = (options = {}) => {
23
20
  const returnTo = typeof options.returnTo === 'function' ? options.returnTo : null;
21
+ // Surface claim for this panel, taken at the user's open action — BEFORE
22
+ // the first daemon read. Every paint through it re-validates and re-arms,
23
+ // so a re-check or install settling after Esc cannot paint over whatever
24
+ // the user is looking at now.
25
+ const own = surface.claim();
26
+ const paintPanel = (panel) => {
27
+ if (!own.owns()) return false;
28
+ setProviderPrompt(null);
29
+ setSettingsPrompt(null);
30
+ return own.paint(panel);
31
+ };
24
32
  // Async: both reads are remote calls on a daemon-backed store, so the sync
25
33
  // versions rendered every row from an unresolved promise.
26
34
  const readSettings = async () => {
@@ -64,11 +72,7 @@ export function createMaintenancePickers({
64
72
  _action: 'auto-update',
65
73
  },
66
74
  ];
67
- setProviderPrompt(null);
68
- setChannelPrompt(null);
69
- setHookPrompt(null);
70
- setSettingsPrompt(null);
71
- setPicker({
75
+ return paintPanel({
72
76
  title: 'Update',
73
77
  description: 'Check version and update mixdog.',
74
78
  help: '↑/↓ Select · Enter Open/Toggle · Esc Close',
@@ -101,31 +105,40 @@ export function createMaintenancePickers({
101
105
  }
102
106
  },
103
107
  onCancel: () => {
104
- setPicker(null);
108
+ own.close();
105
109
  if (returnTo) returnTo();
106
110
  },
107
111
  });
108
112
  };
113
+ // Every render() is an async daemon read; a detached call would surface a
114
+ // failed read as an unhandled rejection (fatal for the TUI process).
115
+ const rerender = (opts = {}) => {
116
+ void Promise.resolve(render(opts))
117
+ .catch((e) => store.pushNotice(`update panel failed: ${e?.message || e}`, 'error'));
118
+ };
119
+ // Deferred repaint bound to the claim AT ACTION TIME: a check/install that
120
+ // settles after Esc must not re-open the Update panel.
121
+ const deferredRerender = (opts = {}) => own.defer(() => rerender(opts));
109
122
  const toggleAutoUpdate = (enabled) => {
110
- try {
111
- void Promise.resolve(store.setAutoUpdate?.(enabled)).finally(() => render());
112
- store.pushNotice(`Auto-update ${enabled ? 'on' : 'off'}`, 'info');
113
- } catch (e) {
114
- store.pushNotice(`auto-update failed: ${e?.message || e}`, 'error');
115
- }
116
- render();
123
+ // Persisted by the daemon: only claim the new value once it is written.
124
+ void Promise.resolve(store.setAutoUpdate?.(enabled))
125
+ .then(() => store.pushNotice(`Auto-update ${enabled ? 'on' : 'off'}`, 'info'))
126
+ .catch((e) => store.pushNotice(`auto-update failed: ${e?.message || e}`, 'error'))
127
+ .finally(deferredRerender());
117
128
  };
118
129
  const recheck = () => {
119
- render({ checking: true });
130
+ rerender({ checking: true });
131
+ const settled = deferredRerender();
120
132
  void Promise.resolve(store.checkForUpdate?.({ force: true }))
121
- .then(() => render())
133
+ .then(() => settled())
122
134
  .catch((e) => {
123
135
  store.pushNotice(`update check failed: ${e?.message || e}`, 'error');
124
- render();
136
+ settled();
125
137
  });
126
138
  };
127
139
  const runUpdate = () => {
128
140
  store.pushNotice('Updating…', 'info');
141
+ const settled = deferredRerender();
129
142
  void Promise.resolve(store.runUpdateNow?.())
130
143
  .then((result) => {
131
144
  if (result?.ok) {
@@ -133,21 +146,42 @@ export function createMaintenancePickers({
133
146
  } else {
134
147
  store.pushNotice(`Update failed: ${result?.error || 'unknown error'}`, 'error');
135
148
  }
136
- render();
149
+ settled();
137
150
  })
138
151
  .catch((e) => {
139
152
  store.pushNotice(`Update failed: ${e?.message || e}`, 'error');
140
- render();
153
+ settled();
141
154
  });
142
155
  };
143
- render({ checking: true });
144
- void Promise.resolve(store.checkForUpdate?.({}))
145
- .then(() => render())
146
- .catch(() => render());
156
+ // First paint, THEN the initial check. The repaint epoch is captured after
157
+ // this panel has taken the surface: capturing it before the first paint
158
+ // binds it to the previous owner's epoch, which the open transition (a
159
+ // panel identity change) supersedes — leaving "Latest version" stuck on
160
+ // "checking…". Esc after the paint still closes the panel for good.
161
+ void Promise.resolve(render({ checking: true }))
162
+ .catch((e) => {
163
+ store.pushNotice(`update panel failed: ${e?.message || e}`, 'error');
164
+ return false;
165
+ })
166
+ .then((painted) => {
167
+ // Open abandoned (Esc while the first read was pending): the panel
168
+ // never took the surface, so the check result must not paint it either.
169
+ if (!painted) return;
170
+ const initialChecked = deferredRerender();
171
+ void Promise.resolve(store.checkForUpdate?.({}))
172
+ .then(() => initialChecked())
173
+ .catch(() => initialChecked());
174
+ });
147
175
  };
148
176
 
149
177
  const openAutoClearPicker = (options = {}) => {
150
178
  const returnTo = typeof options.returnTo === 'function' ? options.returnTo : null;
179
+ // Surface claim, same rule as openUpdatePicker: every paint (render() and
180
+ // renderAdvanced()) re-validates and re-arms, so a readCurrent() settling
181
+ // after Esc — on open or on any later toggle — can never paint over the
182
+ // user's surface.
183
+ const own = surface.claim();
184
+ const paintPanel = (panel) => own.paint(panel);
151
185
  // Lead BP4 messages cache TTL follows autoClear (cache-strategy.mjs
152
186
  // resolveLeadMessagesTtl): off or idle>=1h -> 1h, shorter idle -> 5m.
153
187
  const HOUR_MS = 60 * 60 * 1000;
@@ -162,6 +196,12 @@ export function createMaintenancePickers({
162
196
  try { return (await store.getAutoClear?.()) || null; } catch { return null; }
163
197
  };
164
198
  const applyAutoClear = (patch = {}) => {
199
+ // Bound to the claim on this keypress: a write acking after Esc must not
200
+ // re-open the Auto-clear panel.
201
+ const settled = own.defer(() => {
202
+ void Promise.resolve(render())
203
+ .catch((e) => store.pushNotice(`auto-clear panel failed: ${e?.message || e}`, 'error'));
204
+ });
165
205
  void Promise.resolve(store.setAutoClear?.(patch))
166
206
  .then((next) => {
167
207
  if (!next) {
@@ -171,11 +211,11 @@ export function createMaintenancePickers({
171
211
  store.pushNotice(next.enabled ? `autoclear on · idle ${formatDuration(next.idleMs)}` : 'autoclear off', 'info');
172
212
  })
173
213
  .catch((e) => store.pushNotice(`autoclear failed: ${e?.message || e}`, 'error'))
174
- .finally(() => { void render(); });
214
+ .finally(settled);
175
215
  };
176
216
  const openProviderDurationEditor = (entry) => {
177
217
  if (!entry?.provider) return;
178
- setPicker(null);
218
+ own.close();
179
219
  setSettingsPrompt({
180
220
  kind: 'autoclear-provider',
181
221
  label: `Auto-clear · ${entry.provider}`,
@@ -200,7 +240,7 @@ export function createMaintenancePickers({
200
240
  _action: 'provider-default',
201
241
  _entry: entry,
202
242
  }));
203
- setPicker({
243
+ paintPanel({
204
244
  title: 'Auto-clear · Advanced',
205
245
  description: 'Provider default idle windows. Enter edits the duration text.',
206
246
  help: '↑/↓ Select · Enter Edit · Esc Back',
@@ -238,7 +278,7 @@ export function createMaintenancePickers({
238
278
  _action: 'advanced',
239
279
  },
240
280
  ];
241
- setPicker({
281
+ paintPanel({
242
282
  title: 'Auto-clear',
243
283
  description: `Clear idle context after ${enabled ? formatDuration(idleMs) : 'never'} · lead cache TTL ${cacheTtlLabel}.`,
244
284
  help: '↑/↓ Select · ←/→ Toggle On/Off · Enter Open/Toggle · Esc Close',
@@ -259,16 +299,14 @@ export function createMaintenancePickers({
259
299
  }
260
300
  },
261
301
  onCancel: () => {
262
- setPicker(null);
302
+ own.close();
263
303
  if (returnTo) returnTo();
264
304
  },
265
305
  });
266
306
  };
267
307
  setProviderPrompt(null);
268
- setChannelPrompt(null);
269
- setHookPrompt(null);
270
308
  setSettingsPrompt(null);
271
- setContextPanel(null);
309
+ own.context(null);
272
310
  closeUsagePanel();
273
311
  if (options.advanced === true) renderAdvanced();
274
312
  else render();
@@ -276,6 +314,9 @@ export function createMaintenancePickers({
276
314
 
277
315
  const openProfilePicker = async (options = {}) => {
278
316
  const returnTo = typeof options.returnTo === 'function' ? options.returnTo : null;
317
+ // Surface claim (panel-surface.mjs): getProfile() is a daemon read, so Esc
318
+ // can land before the first paint.
319
+ const own = surface.claim();
279
320
  let profile = null;
280
321
  try {
281
322
  profile = (await store.getProfile?.()) || null;
@@ -298,16 +339,22 @@ export function createMaintenancePickers({
298
339
  const currentExperienceLevelId = profile?.experienceLevel || '';
299
340
  const currentExperienceLevel = experienceLevels.find((level) => level.id === currentExperienceLevelId) || null;
300
341
  const titleValue = String(profile?.title || '').trim();
342
+ // setProfile is a daemon write: rebuild the panel only after it settles so
343
+ // the row cannot show a value the daemon rejected (and so a rejected write
344
+ // cannot escape as an unhandled rejection).
345
+ // Bound to the claim when the cycle keypress builds its chain: a setProfile
346
+ // that acks after Esc must not re-open the Profile panel.
347
+ const reopenProfile = () => own.defer(() => {
348
+ void Promise.resolve(openProfilePicker({ returnTo }))
349
+ .catch((e) => store.pushNotice(`profile panel failed: ${e?.message || e}`, 'error'));
350
+ });
301
351
  const cycleLanguage = (direction = 1) => {
302
352
  const idx = Math.max(0, languages.findIndex((lang) => lang.id === currentLangId));
303
353
  const next = languages[(idx + direction + languages.length) % languages.length];
304
- try {
305
- store.setProfile?.({ language: next.id });
306
- store.pushNotice(`Language set to ${next.label}`, 'info');
307
- } catch (e) {
308
- store.pushNotice(`profile update failed: ${e?.message || e}`, 'error');
309
- }
310
- openProfilePicker({ returnTo });
354
+ void Promise.resolve(store.setProfile?.({ language: next.id }))
355
+ .then(() => store.pushNotice(`Language set to ${next.label}`, 'info'))
356
+ .catch((e) => store.pushNotice(`profile update failed: ${e?.message || e}`, 'error'))
357
+ .finally(reopenProfile());
311
358
  };
312
359
  const cycleExperienceLevel = (direction = 1) => {
313
360
  const idx = experienceLevels.findIndex((level) => level.id === currentExperienceLevelId);
@@ -315,21 +362,17 @@ export function createMaintenancePickers({
315
362
  ? (direction < 0 ? experienceLevels.length - 1 : 0)
316
363
  : (idx + direction + experienceLevels.length) % experienceLevels.length;
317
364
  const next = experienceLevels[nextIdx];
318
- try {
319
- store.setProfile?.({ experienceLevel: next.id });
320
- store.pushNotice(`Experience level set to ${next.label}`, 'info');
321
- } catch (e) {
322
- store.pushNotice(`profile update failed: ${e?.message || e}`, 'error');
323
- }
324
- openProfilePicker({ returnTo });
365
+ void Promise.resolve(store.setProfile?.({ experienceLevel: next.id }))
366
+ .then(() => store.pushNotice(`Experience level set to ${next.label}`, 'info'))
367
+ .catch((e) => store.pushNotice(`profile update failed: ${e?.message || e}`, 'error'))
368
+ .finally(reopenProfile());
325
369
  };
370
+ if (!own.owns()) return;
326
371
  setProviderPrompt(null);
327
- setChannelPrompt(null);
328
- setHookPrompt(null);
329
372
  setSettingsPrompt(null);
330
- setContextPanel(null);
373
+ own.context(null);
331
374
  closeUsagePanel();
332
- setPicker({
375
+ own.paint({
333
376
  title: 'Profile',
334
377
  description: 'How the assistant addresses you, adapts terminology, and chooses its response language.',
335
378
  help: '↑/↓ Select · ←/→ Change · Enter Edit · Esc Close',
@@ -369,7 +412,7 @@ export function createMaintenancePickers({
369
412
  },
370
413
  onSelect: (_value, item) => {
371
414
  if (item?._action === 'title') {
372
- setPicker(null);
415
+ own.close();
373
416
  setSettingsPrompt({
374
417
  kind: 'profile-title',
375
418
  label: 'Profile · Title',
@@ -382,7 +425,7 @@ export function createMaintenancePickers({
382
425
  }
383
426
  },
384
427
  onCancel: () => {
385
- setPicker(null);
428
+ own.close();
386
429
  if (returnTo) returnTo();
387
430
  },
388
431
  });
@@ -35,7 +35,7 @@ export function messageSelectorLabel(text, width = 56) {
35
35
  export function useMessageSelector({
36
36
  store,
37
37
  state,
38
- setPicker,
38
+ surface,
39
39
  setPromptDraftOverride,
40
40
  syncPromptLayoutRows,
41
41
  showPromptHint,
@@ -69,14 +69,15 @@ export function useMessageSelector({
69
69
  showPromptHint('No message to jump back to.', 'info');
70
70
  return false;
71
71
  }
72
- setPicker({
72
+ const own = surface.claim();
73
+ own.paint({
73
74
  _kind: 'message-selector',
74
75
  title: 'Jump back to a message',
75
76
  description: 'Rewind the conversation to a previous prompt and edit it.',
76
77
  initialIndex: rows.length - 1,
77
78
  items: rows.map((row) => ({ value: row.id, label: messageSelectorLabel(row.text) })),
78
79
  onSelect: (value) => {
79
- setPicker(null);
80
+ own.close();
80
81
  let restored;
81
82
  try {
82
83
  restored = store.rewindToItem?.(value);
@@ -94,10 +95,10 @@ export function useMessageSelector({
94
95
  }
95
96
  applyRewind(restored);
96
97
  },
97
- onCancel: () => setPicker(null),
98
+ onCancel: () => own.close(),
98
99
  });
99
100
  return true;
100
- }, [store, state.items, setPicker, applyRewind, showPromptHint]);
101
+ }, [store, state.items, surface, applyRewind, showPromptHint]);
101
102
 
102
103
  return { hasUserMessages, openMessageSelector };
103
104
  }
@@ -31,10 +31,8 @@ const MODEL_CACHE_TTL_MS = 5 * 60 * 1000;
31
31
  export function createModelPicker({
32
32
  store,
33
33
  getState,
34
- setPicker,
34
+ surface,
35
35
  setProviderPrompt,
36
- setChannelPrompt,
37
- setHookPrompt,
38
36
  setSettingsPrompt,
39
37
  providerModelsCacheRef,
40
38
  webSearchModelsCacheRef,
@@ -46,9 +44,14 @@ export function createModelPicker({
46
44
  let providerModelsTtlRefreshPromise = null;
47
45
  const openModelPicker = async (options = {}) => {
48
46
  const state = getState();
47
+ // Surface claim for this picker (panel-surface.mjs): every paint — the
48
+ // loading frame, the provider list, the per-provider model list — proves
49
+ // ownership through it, and each paint re-arms it because a paint can
50
+ // itself be the handover. modelPickerRequestRef only orders concurrent
51
+ // OPENS; it cannot see an Esc that closed this one, so a model load
52
+ // settling afterwards used to repaint the dismissed picker.
53
+ const own = surface.claim();
49
54
  setProviderPrompt(null);
50
- setChannelPrompt(null);
51
- setHookPrompt(null);
52
55
  setSettingsPrompt(null);
53
56
  modelPickerRequestRef.current += 1;
54
57
  let modelPickerClosed = false;
@@ -59,8 +62,9 @@ export function createModelPicker({
59
62
  const cancelModelPicker = () => {
60
63
  modelPickerClosed = true;
61
64
  if (returnTo) returnTo();
62
- else setPicker(null);
65
+ else own.close();
63
66
  };
67
+ const paintModelPicker = (panel) => own.paint(panel);
64
68
  const cacheRef = options.cacheRef === 'webSearch' ? webSearchModelsCacheRef : providerModelsCacheRef;
65
69
  const loadModels = typeof options.loadModels === 'function' ? options.loadModels : store.listProviderModels;
66
70
  let providerModels = Array.isArray(cacheRef.current.models)
@@ -69,7 +73,7 @@ export function createModelPicker({
69
73
  let refreshModelsPromise = null;
70
74
  let renderedQuickModels = false;
71
75
  if (!providerModels.length || options.refreshModels === true) {
72
- setPicker({
76
+ paintModelPicker({
73
77
  title: options.title || 'Model',
74
78
  description: options.loadingDescription || 'Loading models...',
75
79
  help: returnTo ? `↑/↓ Select · Enter Open · Esc ${returnLabel}` : '↑/↓ Select · Enter Open · Esc Back',
@@ -108,6 +112,9 @@ export function createModelPicker({
108
112
 
109
113
  if (!providerModels || providerModels.length === 0) {
110
114
  store.pushNotice(options.emptyNotice || 'no provider models available; open /providers to sign in', 'warn');
115
+ // Delegation is a paint by proxy: the empty-catalog fallback opens
116
+ // Providers, so it must prove ownership exactly like paintModelPicker.
117
+ if (!own.owns()) return;
111
118
  void openProviderSetupPicker({
112
119
  title: 'Providers',
113
120
  continueLabel: 'Back to model setup',
@@ -375,18 +382,28 @@ export function createModelPicker({
375
382
  if (typeof options.onImmediateSelect === 'function') {
376
383
  options.onImmediateSelect(routeInput, selected, effort);
377
384
  } else {
378
- setPicker(null);
385
+ own.paint(null);
379
386
  }
387
+ // Post-ack delegation (return to the caller's panel), bound to the
388
+ // claim AFTER this keypress's own navigation: an Esc between the
389
+ // save and its ack must cancel the hop, not paint over the surface
390
+ // the user moved to.
391
+ const afterSelect = own.defer(() => {
392
+ if (typeof options.onAfterSelect === 'function') options.onAfterSelect();
393
+ });
380
394
  void savePromise
381
395
  .then((result) => {
382
396
  if (result) clearModelCaches('all');
383
- if (typeof options.onAfterSelect === 'function') options.onAfterSelect();
397
+ afterSelect();
384
398
  return result;
385
399
  })
386
400
  .catch((e) => store.pushNotice(`Couldn’t save model: ${e?.message || e}`, 'error'));
387
401
  return;
388
402
  }
389
- setPicker(null);
403
+ own.paint(null);
404
+ const afterSelect = own.defer(() => {
405
+ if (typeof options.onAfterSelect === 'function') options.onAfterSelect();
406
+ });
390
407
  void store.setRoute(routeInput)
391
408
  .then((ok) => {
392
409
  if (ok) clearModelCaches('provider');
@@ -396,7 +413,7 @@ export function createModelPicker({
396
413
  : 'Model switch is already running',
397
414
  ok ? 'info' : 'warn',
398
415
  );
399
- if (ok && typeof options.onAfterSelect === 'function') options.onAfterSelect();
416
+ if (ok) afterSelect();
400
417
  })
401
418
  .catch((e) => store.pushNotice(`Couldn’t switch model: ${e?.message || e}`, 'error'));
402
419
  };
@@ -408,7 +425,7 @@ export function createModelPicker({
408
425
  (item) => item._provider === activeRoute?.provider && item._modelId === activeRoute?.model,
409
426
  ),
410
427
  );
411
- setPicker({
428
+ paintModelPicker({
412
429
  title: providerDisplayName(provider),
413
430
  description: options.modelDescription || 'Select a model. Adjust Effort with ←/→.',
414
431
  footer: (item) => modelFooter(item?._model),
@@ -460,7 +477,7 @@ export function createModelPicker({
460
477
  0,
461
478
  providerItems.findIndex((item) => item._provider === providerHighlight),
462
479
  );
463
- setPicker({
480
+ paintModelPicker({
464
481
  title: options.title || 'Model',
465
482
  description: options.providerDescription || 'Choose a provider.',
466
483
  help: returnTo ? `↑/↓ Select · Enter Open · Esc ${returnLabel}` : '↑/↓ Select · Enter Open · Esc Back',
@@ -19,10 +19,8 @@ import {
19
19
 
20
20
  export function createOnboardingSteps({
21
21
  store,
22
- setPicker,
22
+ surface,
23
23
  setProviderPrompt,
24
- setChannelPrompt,
25
- setHookPrompt,
26
24
  setSettingsPrompt,
27
25
  setOnboardingActive,
28
26
  onboardingRef,
@@ -85,6 +83,11 @@ export function createOnboardingSteps({
85
83
 
86
84
  const openOnboardingAuthStep = async () => {
87
85
  prefetchOnboardingStep2();
86
+ // Surface claim (panel-surface.mjs): the provider-setup read below runs
87
+ // before anything paints, and the wizard is reachable while a normal
88
+ // surface is on screen (/onboarding, Step-1 re-entry), so Esc during the
89
+ // wait must leave that surface alone instead of dropping Step 1 on it.
90
+ const own = surface.claim();
88
91
  // Load the provider setup BEFORE opening the picker so Step 1 renders the
89
92
  // real list in one frame instead of flashing the "Checking Providers"
90
93
  // placeholder (that swap is what looked like a jump on entry). On failure,
@@ -93,6 +96,7 @@ export function createOnboardingSteps({
93
96
  try {
94
97
  preloadedSetup = await store.getProviderSetup?.();
95
98
  } catch { /* openProviderSetupPicker will show its loading frame + error. */ }
99
+ if (!own.owns()) return;
96
100
  void openProviderSetupPicker({
97
101
  title: 'First Run · Step 1/4 · Provider Auth',
98
102
  returnTo: () => openOnboardingAuthStep(),
@@ -133,7 +137,7 @@ export function createOnboardingSteps({
133
137
  const webSearchRoute = onboardingRef.current.webSearchRoute || null;
134
138
  const overrides = onboardingRef.current.agentRoutes || {};
135
139
  const hasOverrides = Object.keys(overrides).length > 0;
136
- setPicker(null);
140
+ surface.claim().close();
137
141
  setOnboardingActive(false);
138
142
  const done = () => store.pushNotice('First-run setup complete.', 'info');
139
143
  const failed = (e) => store.pushNotice(`Couldn’t save setup: ${e?.message || e}`, 'error');
@@ -172,6 +176,7 @@ export function createOnboardingSteps({
172
176
  // the plain model list is shown. Selecting Main Model updates defaultRoute;
173
177
  // agents that have no explicit override keep inheriting it.
174
178
  const openOnboardingRoleModelPicker = async (target) => {
179
+ const own = surface.claim();
175
180
  const isLead = target === 'lead';
176
181
  const isWebSearch = target === 'webSearch';
177
182
  // Web Search uses the web-search-capable model list; lead/agent use provider models.
@@ -186,14 +191,15 @@ export function createOnboardingSteps({
186
191
  models = normalizeModelOptions(webSearchModels || []);
187
192
  if (models.length === 0) {
188
193
  store.pushNotice('no native web-search models available; connect OpenAI, Grok, Gemini, or Anthropic', 'warn');
189
- void openOnboardingWorkflowStep();
194
+ // Fallback delegation is a paint by proxy: prove ownership first.
195
+ if (own.owns()) void openOnboardingWorkflowStep();
190
196
  return;
191
197
  }
192
198
  } else {
193
199
  models = normalizeModelOptions(onboardingRef.current.providerModels || []);
194
200
  if (models.length === 0) {
195
201
  store.pushNotice('no provider models available; open /providers to sign in', 'warn');
196
- openOnboardingAuthStep();
202
+ if (own.owns()) openOnboardingAuthStep();
197
203
  return;
198
204
  }
199
205
  }
@@ -245,7 +251,8 @@ export function createOnboardingSteps({
245
251
  : isWebSearch
246
252
  ? 'Web Search'
247
253
  : (onboardingRef.current.agents || []).find((a) => a.id === target)?.label || target;
248
- setPicker({
254
+ if (!own.owns()) return;
255
+ own.paint({
249
256
  title: `First Run · ${label}`,
250
257
  description: isLead
251
258
  ? 'Pick the main model. Agents inherit this unless individually changed.'
@@ -294,6 +301,7 @@ export function createOnboardingSteps({
294
301
  };
295
302
 
296
303
  const openOnboardingWorkflowStep = async () => {
304
+ const own = surface.claim();
297
305
  if (!Array.isArray(onboardingRef.current.providerModels) || onboardingRef.current.providerModels.length === 0) {
298
306
  try {
299
307
  onboardingRef.current.providerModels = await store.listProviderModels();
@@ -308,7 +316,9 @@ export function createOnboardingSteps({
308
316
  onboardingRef.current.defaultRoute = null;
309
317
  onboardingRef.current.agentRoutes = {};
310
318
  store.pushNotice('no provider models available; open /providers to sign in', 'warn');
311
- openOnboardingAuthStep();
319
+ // Step 2's empty-model fallback hands the surface to Step 1 after an
320
+ // await: same ownership rule as its own paint below.
321
+ if (own.owns()) openOnboardingAuthStep();
312
322
  return;
313
323
  }
314
324
  // Main Model stays unset until the user picks one; no auto-recommendation.
@@ -326,11 +336,10 @@ export function createOnboardingSteps({
326
336
  const webSearchRoute = onboardingRef.current.webSearchRoute || null;
327
337
  const overrides = onboardingRef.current.agentRoutes || {};
328
338
  const agents = onboardingRef.current.agents || [];
339
+ if (!own.owns()) return;
329
340
  setProviderPrompt(null);
330
- setChannelPrompt(null);
331
- setHookPrompt(null);
332
341
  setSettingsPrompt(null);
333
- setPicker({
342
+ own.paint({
334
343
  title: 'First Run · Step 2/4 · Models',
335
344
  description: 'Set the Main Model; each agent inherits it unless changed.',
336
345
  indexMode: 'always',
@@ -384,7 +393,7 @@ export function createOnboardingSteps({
384
393
  }
385
394
  },
386
395
  onCancel: () => {
387
- setPicker(null);
396
+ own.close();
388
397
  onboardingWarnReopen();
389
398
  },
390
399
  });
@@ -0,0 +1,56 @@
1
+ // Panel/prompt supersession epoch.
2
+ //
3
+ // Module-global on purpose: the TUI runs ONE App tree and every picker/prompt
4
+ // factory is re-created on each render, so this ownership marker cannot live in
5
+ // a factory closure.
6
+ //
7
+ // Anything that hands surface ownership back to the user — closing a picker
8
+ // (Esc/cancel), cancelling a text-entry prompt, or issuing a NEWER submit of
9
+ // the same prompt — supersedes every deferred paint issued before it. Async
10
+ // work captures currentPanelEpoch() when the USER ACTION starts and re-checks
11
+ // isPanelEpochCurrent(token) immediately before it opens, refreshes, or closes
12
+ // a surface. That single check kills both failure modes:
13
+ // - a daemon write settling after Esc re-opening the dismissed panel, and
14
+ // - an older ACK closing (or claiming success for) a panel that meanwhile
15
+ // holds newer user input.
16
+ let panelEpoch = 0;
17
+
18
+ export function currentPanelEpoch() {
19
+ return panelEpoch;
20
+ }
21
+
22
+ // Marks every in-flight deferred paint as superseded and returns the new token
23
+ // for the action performing the supersession.
24
+ export function supersedePanelEpoch() {
25
+ panelEpoch += 1;
26
+ return panelEpoch;
27
+ }
28
+
29
+ export function isPanelEpochCurrent(token) {
30
+ return token === panelEpoch;
31
+ }
32
+
33
+ // Identity of the SURFACE a panel object stands for. A rebuild of the same
34
+ // surface (light Settings refresh, toggle-driven MCP re-render) keeps this
35
+ // value stable; a different panel reports a different identity.
36
+ export function panelIdentity(panel) {
37
+ if (!panel || typeof panel !== 'object') return null;
38
+ return panel._kind || panel.title || null;
39
+ }
40
+
41
+ // True when `next` takes the surface away from a DIFFERENT panel that was open:
42
+ // a close (open → null) or a replacement by another panel. Both hand the
43
+ // surface to something the user is looking at now, so deferred paints issued
44
+ // before it are stale. Not a handover:
45
+ // - null → anything: nothing owned the surface, and the in-flight write of a
46
+ // text-entry prompt currently on screen must stay valid, and
47
+ // - a rebuild of the same panel: its own deferred refresh must still land.
48
+ export function shouldSupersedePanelEpoch(previous, next) {
49
+ if (!previous || typeof previous !== 'object') return false;
50
+ if (!next || typeof next !== 'object') return true;
51
+ if (previous === next) return false;
52
+ return panelIdentity(previous) !== panelIdentity(next);
53
+ }
54
+
55
+ // Ownership is CLAIMED, never asserted by hand: panel-surface.mjs owns the
56
+ // sinks and hands out claims that carry these tokens (see that module's header).