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
@@ -0,0 +1,103 @@
1
+ /**
2
+ * app/message-selector.mjs — Claude Code's "jump back to a previous message"
3
+ * list, opened by a double Escape from an EMPTY prompt while idle.
4
+ *
5
+ * Selecting a row rewinds the conversation to just before that prompt and
6
+ * drops its text back into the input box for editing: the transcript rows and
7
+ * the model-side history after it are undone, so the resubmit continues from
8
+ * exactly that point (engine side: store.rewindToItem).
9
+ */
10
+ import { useCallback, useMemo } from 'react';
11
+
12
+ export const MAX_SELECTABLE_MESSAGES = 20;
13
+
14
+ /** Newest-last list of rewindable user prompts, capped for the picker. */
15
+ export function selectableUserItems(items, limit = MAX_SELECTABLE_MESSAGES) {
16
+ const source = Array.isArray(items) ? items : [];
17
+ const rows = [];
18
+ for (let index = source.length - 1; index >= 0 && rows.length < limit; index -= 1) {
19
+ const item = source[index];
20
+ if (item?.kind !== 'user') continue;
21
+ if (item?.id == null) continue;
22
+ const text = String(item.text || '').trim();
23
+ if (!text) continue;
24
+ rows.push({ id: String(item.id), text });
25
+ }
26
+ return rows.reverse();
27
+ }
28
+
29
+ export function messageSelectorLabel(text, width = 56) {
30
+ const firstLine = String(text || '').split('\n').map((line) => line.trim()).find(Boolean) || '';
31
+ if (firstLine.length <= width) return firstLine;
32
+ return `${firstLine.slice(0, Math.max(1, width - 1))}…`;
33
+ }
34
+
35
+ export function useMessageSelector({
36
+ store,
37
+ state,
38
+ setPicker,
39
+ setPromptDraftOverride,
40
+ syncPromptLayoutRows,
41
+ showPromptHint,
42
+ clearPromptHint,
43
+ }) {
44
+ // Cheap enough to recompute only when the transcript array identity changes;
45
+ // the prompt asks for this on every Escape, never per keystroke.
46
+ const hasUserMessages = useMemo(
47
+ () => (Array.isArray(state.items) ? state.items : []).some(
48
+ (item) => item?.kind === 'user' && String(item?.text || '').trim(),
49
+ ),
50
+ [state.items],
51
+ );
52
+
53
+ const applyRewind = useCallback((restored) => {
54
+ const text = String(restored?.text || '');
55
+ if (!text) {
56
+ showPromptHint('Could not restore that message.', 'error');
57
+ return false;
58
+ }
59
+ clearPromptHint();
60
+ syncPromptLayoutRows(text);
61
+ setPromptDraftOverride({ id: Date.now(), value: text });
62
+ return true;
63
+ }, [clearPromptHint, syncPromptLayoutRows, setPromptDraftOverride, showPromptHint]);
64
+
65
+ const openMessageSelector = useCallback(() => {
66
+ const items = store.getState?.().items ?? state.items;
67
+ const rows = selectableUserItems(items);
68
+ if (rows.length === 0) {
69
+ showPromptHint('No message to jump back to.', 'info');
70
+ return false;
71
+ }
72
+ setPicker({
73
+ _kind: 'message-selector',
74
+ title: 'Jump back to a message',
75
+ description: 'Rewind the conversation to a previous prompt and edit it.',
76
+ initialIndex: rows.length - 1,
77
+ items: rows.map((row) => ({ value: row.id, label: messageSelectorLabel(row.text) })),
78
+ onSelect: (value) => {
79
+ setPicker(null);
80
+ let restored;
81
+ try {
82
+ restored = store.rewindToItem?.(value);
83
+ } catch (error) {
84
+ showPromptHint(`rewind failed: ${error?.message || error}`, 'error');
85
+ return;
86
+ }
87
+ // Daemon-backed stores answer asynchronously; a local store returns the
88
+ // payload directly. Both land in the same apply path.
89
+ if (restored && typeof restored.then === 'function') {
90
+ void Promise.resolve(restored)
91
+ .then(applyRewind)
92
+ .catch((error) => showPromptHint(`rewind failed: ${error?.message || error}`, 'error'));
93
+ return;
94
+ }
95
+ applyRewind(restored);
96
+ },
97
+ onCancel: () => setPicker(null),
98
+ });
99
+ return true;
100
+ }, [store, state.items, setPicker, applyRewind, showPromptHint]);
101
+
102
+ return { hasUserMessages, openMessageSelector };
103
+ }
@@ -138,20 +138,19 @@ export function createOnboardingSteps({
138
138
  const done = () => store.pushNotice('First-run setup complete.', 'info');
139
139
  const failed = (e) => store.pushNotice(`Couldn’t save setup: ${e?.message || e}`, 'error');
140
140
  // Branch 1 — Main Model set: full persist. Agents without an explicit
141
- // override are sent; untouched agents are left out so the backend never
141
+ // override are sent; untouched agents are left out so the session never
142
142
  // overwrites them (they follow the Main Model dynamically at runtime).
143
143
  if (defaultRoute) {
144
144
  void store.completeOnboarding?.({
145
145
  defaultRoute,
146
- defaultProvider: defaultRoute.provider,
147
146
  ...(hasOverrides ? { agentRoutes: { ...overrides } } : {}),
148
147
  ...(searchRoute ? { searchRoute } : {}),
149
148
  }).then(done).catch(failed);
150
149
  return;
151
150
  }
152
151
  // Branch 2 — Main unset but some Search/agent picks exist: partial persist.
153
- // Only the explicit overrides are sent (no defaultRoute/defaultProvider); the
154
- // backend skips agents lacking a route and marks onboarding complete.
152
+ // Only the explicit overrides are sent (no defaultRoute); the
153
+ // session skips agents lacking a route and marks onboarding complete.
155
154
  if (hasOverrides || searchRoute) {
156
155
  void store.completeOnboarding?.({
157
156
  ...(hasOverrides ? { agentRoutes: { ...overrides } } : {}),
@@ -3,11 +3,9 @@
3
3
  *
4
4
  * Extracted from App.jsx behavior-preservingly. This cluster is ref/state
5
5
  * coupled (it drives setPicker + a fan of prompt setters), so it's delivered as
6
- * a dependency-injection factory rather than pure functions: App calls
7
- * createProjectPicker({...}) once and destructures the returned builders. Every
8
- * function body is the original App logic, verbatim, with closure identifiers
9
- * (state, store, setPicker, the prompt setters, and the projects.mjs helpers)
10
- * threaded in through the factory argument.
6
+ * a dependency-injection factory rather than pure functions. Project registry
7
+ * and path operations are daemon calls on `store`; the TUI retains only the
8
+ * OS-native folder chooser.
11
9
  */
12
10
  export function createProjectPicker({
13
11
  state,
@@ -19,44 +17,53 @@ export function createProjectPicker({
19
17
  setSettingsPrompt,
20
18
  setContextPanel,
21
19
  closeUsagePanel,
22
- listProjects,
23
- addProject,
24
- touchProjectSelected,
25
- resolveProjectPath,
26
20
  projectNameFromPath,
27
21
  pickFolder,
28
22
  }) {
29
- const buildProjectPickerState = ({ initialEntry = false } = {}) => {
30
- let projects = [];
31
- try {
32
- projects = listProjects() || [];
33
- } catch {
34
- projects = [];
23
+ const serviceCall = (name, ...args) => {
24
+ const target = store?.[name];
25
+ if (typeof target !== 'function') {
26
+ return Promise.reject(new TypeError(`project service method ${name} is unavailable`));
35
27
  }
28
+ return Promise.resolve(target.apply(store, args));
29
+ };
30
+
31
+ const buildProjectPickerState = ({
32
+ initialEntry = false,
33
+ projects = [],
34
+ loading = false,
35
+ requestId = null,
36
+ } = {}) => {
36
37
  const currentPath = String(state.cwd || process.cwd() || '');
37
38
  const items = [];
38
- // Registered projects (store order via listProjects).
39
- for (const project of projects) {
40
- if (!project?.path) continue;
39
+ if (!loading) {
40
+ for (const project of projects) {
41
+ if (!project?.path) continue;
42
+ items.push({
43
+ value: project.path,
44
+ label: project.name || project.path,
45
+ meta: project.path,
46
+ _project: project,
47
+ });
48
+ }
49
+ // Last row: implicit current-directory shortcut (not persisted).
41
50
  items.push({
42
- value: project.path,
43
- label: project.name || project.path,
44
- meta: project.path,
45
- _project: project,
51
+ value: '__use_current__',
52
+ label: 'Current Path',
53
+ meta: currentPath,
54
+ _action: 'current',
46
55
  });
47
56
  }
48
- // Last row: implicit current-directory shortcut (not persisted).
49
- items.push({
50
- value: '__use_current__',
51
- label: 'Current Path',
52
- meta: currentPath,
53
- _action: 'current',
54
- });
55
57
  return {
56
58
  kind: 'project',
59
+ _kind: 'project',
60
+ _projectRequestId: requestId,
61
+ _projectInitialPending: loading && initialEntry,
57
62
  title: 'Project',
58
- description: 'Choose a project.',
59
- help: initialEntry
63
+ description: loading ? 'Loading projects from the session…' : 'Choose a project.',
64
+ help: loading
65
+ ? 'Waiting for the project service…'
66
+ : initialEntry
60
67
  ? '↑/↓ Select · Enter Open · c Create · r Rename'
61
68
  : '↑/↓ Select · Enter Open · c Create · r Rename · Esc Back',
62
69
  indexMode: 'always',
@@ -69,20 +76,11 @@ export function createProjectPicker({
69
76
  return;
70
77
  }
71
78
  if (item?._action === 'current') {
72
- setPicker(null);
73
- try {
74
- store.setCwd?.(currentPath, {
75
- notice: !initialEntry,
76
- message: `Project set: ${projectNameFromPath(currentPath)}`,
77
- });
78
- } catch (e) {
79
- store.pushNotice(`project switch failed: ${e?.message || e}`, 'error');
80
- }
79
+ void enterProject(currentPath, { notice: !initialEntry, register: false });
81
80
  return;
82
81
  }
83
- setPicker(null);
84
82
  const project = item?._project;
85
- if (project?.path) enterProject(project.path, { notice: !initialEntry });
83
+ if (project?.path) void enterProject(project.path, { notice: !initialEntry });
86
84
  },
87
85
  onKey: (input, _key, item) => {
88
86
  if (input === 'c' || input === 'C') {
@@ -157,7 +155,7 @@ export function createProjectPicker({
157
155
  openProjectPicker();
158
156
  return;
159
157
  }
160
- registerProject(result.path);
158
+ void registerProject(result.path);
161
159
  })
162
160
  .catch(() => {
163
161
  beginNewProjectManual();
@@ -165,39 +163,43 @@ export function createProjectPicker({
165
163
  };
166
164
 
167
165
  // Register a project in the picker list without switching this session's cwd.
168
- const registerProject = (rawPath) => {
169
- const path = resolveProjectPath(rawPath);
166
+ const registerProject = async (rawPath) => {
167
+ const path = String(rawPath || '').trim();
170
168
  if (!path) {
171
169
  store.pushNotice('project path is required', 'warn');
172
- return;
170
+ return false;
173
171
  }
174
172
  try {
175
- const project = addProject(path);
173
+ const project = await serviceCall('addProject', path);
176
174
  if (project?.name) store.pushNotice(`project added: ${project.name}`, 'info');
177
- openProjectPicker();
175
+ await openProjectPicker();
176
+ return true;
178
177
  } catch (e) {
179
178
  store.pushNotice(`project add failed: ${e?.message || e}`, 'error');
179
+ return false;
180
180
  }
181
181
  };
182
182
 
183
183
  // Switch the active working directory to a registered/created project path.
184
- const enterProject = (rawPath, options = {}) => {
185
- const path = resolveProjectPath(rawPath);
184
+ const enterProject = async (rawPath, options = {}) => {
185
+ const path = String(rawPath || '').trim();
186
186
  if (!path) {
187
187
  store.pushNotice('project path is required', 'warn');
188
- return;
188
+ return false;
189
189
  }
190
+ setPicker(null);
190
191
  try {
191
192
  // Switch cwd first; only persist the project once the runtime accepts it,
192
193
  // so an invalid/missing path can never be written to projects.json.
193
- store.setCwd?.(path, {
194
+ const resolved = await serviceCall('setCwd', path, {
194
195
  notice: options?.notice !== false,
195
196
  message: `Project set: ${projectNameFromPath(path)}`,
196
197
  });
197
- addProject(path);
198
- touchProjectSelected(path);
198
+ if (options?.register !== false) await serviceCall('addProject', resolved || path);
199
+ return true;
199
200
  } catch (e) {
200
201
  store.pushNotice(`project switch failed: ${e?.message || e}`, 'error');
202
+ return false;
201
203
  }
202
204
  };
203
205
 
@@ -225,14 +227,36 @@ export function createProjectPicker({
225
227
  // a Name column + Path column. The list always opens (even when empty) and
226
228
  // lists registered projects first, then a trailing "Current Path" shortcut.
227
229
  // Creating a new project is available via the picker-level c shortcut.
228
- const openProjectPicker = () => {
230
+ const openProjectPicker = async ({ initialEntry = false } = {}) => {
229
231
  setProviderPrompt(null);
230
232
  setChannelPrompt(null);
231
233
  setHookPrompt(null);
232
234
  setSettingsPrompt(null);
233
235
  setContextPanel(null);
234
236
  closeUsagePanel();
235
- setPicker(buildProjectPickerState());
237
+ const requestId = Symbol('project-picker-request');
238
+ setPicker(buildProjectPickerState({
239
+ initialEntry,
240
+ loading: true,
241
+ requestId,
242
+ }));
243
+ try {
244
+ const projects = await serviceCall('listProjects');
245
+ setPicker((current) => current?._projectRequestId === requestId
246
+ ? buildProjectPickerState({
247
+ initialEntry,
248
+ projects: Array.isArray(projects) ? projects : [],
249
+ requestId,
250
+ })
251
+ : current);
252
+ return projects;
253
+ } catch (error) {
254
+ setPicker((current) => current?._projectRequestId === requestId
255
+ ? buildProjectPickerState({ initialEntry, projects: [], requestId })
256
+ : current);
257
+ store.pushNotice(`project list failed: ${error?.message || error}`, 'error');
258
+ return [];
259
+ }
236
260
  };
237
261
 
238
262
  return {
@@ -16,13 +16,6 @@ import {
16
16
  imageReferenceIds,
17
17
  pastedTextReferenceIds,
18
18
  } from '../paste-attachments.mjs';
19
- import {
20
- ensureDir,
21
- isDirectory,
22
- pathExists,
23
- renameProject,
24
- resolveProjectPath,
25
- } from '../../standalone/projects.mjs';
26
19
 
27
20
  export function createPromptSubmit({
28
21
  store,
@@ -56,10 +49,17 @@ export function createPromptSubmit({
56
49
  pastedImagesRef,
57
50
  pastedTextsRef,
58
51
  }) {
52
+ const serviceCall = (name, ...args) => {
53
+ const target = store?.[name];
54
+ if (typeof target !== 'function') {
55
+ return Promise.reject(new TypeError(`project service method ${name} is unavailable`));
56
+ }
57
+ return Promise.resolve(target.apply(store, args));
58
+ };
59
59
  const submitPrompt = (prompt, options) => {
60
60
  if (typeof store.submitAsync !== 'function') return store.submit(prompt, options);
61
61
  void Promise.resolve(store.submitAsync(prompt, options)).then((accepted) => {
62
- if (accepted === false) store.pushNotice('prompt was not accepted by the session backend', 'error');
62
+ if (accepted === false) store.pushNotice('prompt was not accepted by the session service', 'error');
63
63
  }).catch((error) => {
64
64
  store.pushNotice(`prompt submit failed: ${error?.message || error}`, 'error');
65
65
  });
@@ -192,7 +192,7 @@ export function createPromptSubmit({
192
192
  const channelId = isPipe ? parts[1] : parts[0];
193
193
  Promise.resolve(store.setChannel({
194
194
  channelId,
195
- backend: channelPrompt.backend,
195
+ provider: channelPrompt.provider,
196
196
  }))
197
197
  .then(() => resumeAfterChannelPrompt(channelPrompt))
198
198
  .catch((e) => {
@@ -240,9 +240,14 @@ export function createPromptSubmit({
240
240
  store.pushNotice('working directory path is required', 'warn');
241
241
  return false;
242
242
  }
243
- store.setCwd?.(commandText, { message: `Project set: ${projectNameFromPath(commandText)}` });
244
- setSettingsPrompt(null);
245
- void openSettingsPicker();
243
+ void serviceCall('setCwd', commandText, {
244
+ message: `Project set: ${projectNameFromPath(commandText)}`,
245
+ }).then(() => {
246
+ setSettingsPrompt(null);
247
+ void openSettingsPicker();
248
+ }).catch((error) => {
249
+ store.pushNotice(`project switch failed: ${error?.message || error}`, 'error');
250
+ });
246
251
  return true;
247
252
  }
248
253
  if (settingsPrompt.kind === 'project-new') {
@@ -250,23 +255,25 @@ export function createPromptSubmit({
250
255
  store.pushNotice('project path is required', 'warn');
251
256
  return false;
252
257
  }
253
- const path = resolveProjectPath(commandText);
254
- if (isDirectory(path)) {
255
- setSettingsPrompt(null);
256
- registerProject(path);
257
- return true;
258
- }
259
- // A path that exists but is a regular file is not a valid project dir.
260
- if (pathExists(path)) {
261
- store.pushNotice(`${path} is not a directory`, 'warn');
262
- return false;
263
- }
264
- // Missing folder: confirm creation before registering.
265
- setSettingsPrompt({
266
- kind: 'project-create-confirm',
267
- label: 'New project · Create folder?',
268
- hint: `${path} does not exist. Type "y" to create it, or anything else to cancel.`,
269
- pendingPath: path,
258
+ void serviceCall('inspectProjectPath', commandText).then((result) => {
259
+ const path = String(result?.path || commandText);
260
+ if (result?.directory === true) {
261
+ setSettingsPrompt(null);
262
+ void registerProject(path);
263
+ return;
264
+ }
265
+ if (result?.exists === true) {
266
+ store.pushNotice(`${path} is not a directory`, 'warn');
267
+ return;
268
+ }
269
+ setSettingsPrompt({
270
+ kind: 'project-create-confirm',
271
+ label: 'New project · Create folder?',
272
+ hint: `${path} does not exist. Type "y" to create it, or anything else to cancel.`,
273
+ pendingPath: path,
274
+ });
275
+ }).catch((error) => {
276
+ store.pushNotice(`project path check failed: ${error?.message || error}`, 'error');
270
277
  });
271
278
  return true;
272
279
  }
@@ -274,14 +281,13 @@ export function createPromptSubmit({
274
281
  const pendingPath = String(settingsPrompt.pendingPath || '');
275
282
  const answer = String(commandText || '').trim().toLowerCase();
276
283
  if (answer === 'y' || answer === 'yes') {
277
- const created = ensureDir(pendingPath);
278
- if (!created) {
279
- store.pushNotice(`could not create folder: ${pendingPath}`, 'error');
284
+ void serviceCall('ensureProjectDirectory', pendingPath).then((created) => {
280
285
  setSettingsPrompt(null);
281
- return true;
282
- }
283
- setSettingsPrompt(null);
284
- registerProject(pendingPath);
286
+ void registerProject(created || pendingPath);
287
+ }).catch((error) => {
288
+ store.pushNotice(`could not create folder: ${error?.message || error}`, 'error');
289
+ setSettingsPrompt(null);
290
+ });
285
291
  return true;
286
292
  }
287
293
  setSettingsPrompt(null);
@@ -290,16 +296,15 @@ export function createPromptSubmit({
290
296
  }
291
297
  if (settingsPrompt.kind === 'project-rename') {
292
298
  const targetPath = String(settingsPrompt.projectPath || '');
293
- try {
294
- const updated = renameProject(targetPath, commandText);
299
+ void serviceCall('renameProject', targetPath, commandText).then((updated) => {
295
300
  if (updated) {
296
301
  store.pushNotice(`project renamed to "${updated.name}"`, 'info');
297
302
  }
298
- } catch (e) {
299
- store.pushNotice(`rename failed: ${e?.message || e}`, 'error');
300
- }
301
- setSettingsPrompt(null);
302
- openProjectPicker();
303
+ setSettingsPrompt(null);
304
+ void openProjectPicker();
305
+ }).catch((error) => {
306
+ store.pushNotice(`rename failed: ${error?.message || error}`, 'error');
307
+ });
303
308
  return true;
304
309
  }
305
310
  if (settingsPrompt.kind === 'system-shell') {
@@ -13,14 +13,14 @@ export function createResumePicker({
13
13
  formatSessionUpdatedAt,
14
14
  formatSessionMessageCount,
15
15
  }) {
16
- const openResumePicker = () => {
16
+ const openResumePicker = async () => {
17
17
  let sessions;
18
18
  try {
19
19
  // Terminal ↔ desktop interop: the summary cache is per-process, so a
20
20
  // session created/updated by the desktop app (or another CLI) after this
21
21
  // process's first listing would be invisible without an authoritative
22
22
  // storage rescan on picker open.
23
- sessions = store.listSessions({ refreshFromStorage: true });
23
+ sessions = await store.listSessions({ refreshFromStorage: true });
24
24
  } catch (e) {
25
25
  store.pushNotice(`could not list saved chats: ${e?.message || e}`, 'error');
26
26
  return;
@@ -86,7 +86,9 @@ export function createRoutePickers({
86
86
  const items = agents.map((agent) => ({
87
87
  value: agent.id,
88
88
  label: agent.label,
89
- metaParts: agentModelParts(routeOverrides[agent.id] || agent.route),
89
+ metaParts: routeOverrides[agent.id] || agent.route
90
+ ? agentModelParts(routeOverrides[agent.id] || agent.route)
91
+ : [{ text: '(follows main)', width: 17 }, { text: '', width: 6 }, { text: '', width: 4 }],
90
92
  description: agent.description || agent.definition?.description || '',
91
93
  _agent: agent,
92
94
  }));
@@ -53,7 +53,7 @@ export function createSettingsPicker({
53
53
  const snapshot = (await store.getSettingsSnapshot?.({ heavy: !heavyCache })) || {};
54
54
  const autoClear = snapshot.autoClear || {};
55
55
  const compaction = snapshot.compaction || {};
56
- const memory = snapshot.memory || { enabled: true };
56
+ const recap = snapshot.recap || { enabled: true };
57
57
  const channels = snapshot.channels || { enabled: true };
58
58
  const systemShell = snapshot.systemShell || { source: 'auto', command: '', effective: '' };
59
59
  const outputStyle = snapshot.outputStyle || {};
@@ -63,19 +63,19 @@ export function createSettingsPicker({
63
63
  const plugins = heavyCache ? heavyCache.plugins : (snapshot.plugins || { count: 0 });
64
64
  const skills = heavyCache ? heavyCache.skills : (snapshot.skills || { count: 0 });
65
65
  const channelWorker = snapshot.channelWorker || null;
66
- const channelBackend = heavyCache
67
- ? (heavyCache.channelBackend || 'discord')
68
- : (snapshot.channelBackend || 'discord');
69
- const effectiveChannelBackend = overrides
70
- && Object.prototype.hasOwnProperty.call(overrides, 'channelBackend')
71
- ? overrides.channelBackend
72
- : channelBackend;
66
+ const channelProvider = heavyCache
67
+ ? (heavyCache.channelProvider || 'discord')
68
+ : (snapshot.channelProvider || 'discord');
69
+ const effectiveChannelProvider = overrides
70
+ && Object.prototype.hasOwnProperty.call(overrides, 'channelProvider')
71
+ ? overrides.channelProvider
72
+ : channelProvider;
73
73
  // Refresh the cache every build (light or full) so the next light
74
74
  // refresh reuses whatever was most recently known, and so an
75
- // optimistic override (e.g. channel backend cycle) sticks without
75
+ // optimistic override (e.g. channel provider cycle) sticks without
76
76
  // re-running the heavy getter it came from.
77
- settingsHeavyCacheRef.current = { mcp, hooks, plugins, skills, channelBackend: effectiveChannelBackend };
78
- const channelBackendLabel = effectiveChannelBackend === 'telegram' ? 'Telegram' : 'Discord';
77
+ settingsHeavyCacheRef.current = { mcp, hooks, plugins, skills, channelProvider: effectiveChannelProvider };
78
+ const channelProviderLabel = effectiveChannelProvider === 'telegram' ? 'Telegram' : 'Discord';
79
79
  const remoteEnabled = snapshot.remoteEnabled === true;
80
80
  const remoteRuntimeDescription = channelWorker?.running
81
81
  ? `runtime running · pid ${channelWorker.pid}`
@@ -84,9 +84,7 @@ export function createSettingsPicker({
84
84
  const outputStyleLabel = outputStyle?.current?.label || outputStyle?.current?.id || outputStyle?.configured || 'Default';
85
85
  const workflowLabel = workflowDisplayName(workflow);
86
86
  const boolLabel = (enabled) => enabled ? 'On' : 'Off';
87
- const compactTypeDescription = memory.enabled === false
88
- ? 'Injects raw transcript lines (memory off: no LLM chunking).'
89
- : 'Uses Memory recall to rebuild context faster on large histories.';
87
+ const compactTypeDescription = 'Uses Memory recall to rebuild context faster on large histories.';
90
88
  const applyAutoClear = (patch = {}) => {
91
89
  void Promise.resolve(store.setAutoClear?.(patch))
92
90
  .then((next) => {
@@ -200,25 +198,25 @@ export function createSettingsPicker({
200
198
  .catch((e) => store.pushNotice(`Remote toggle failed: ${e?.message || e}`, 'error'))
201
199
  .finally(() => { void openSettingsPicker({ light: true }); });
202
200
  };
203
- const cycleChannelBackend = (direction = 1) => {
204
- const backends = ['discord', 'telegram'];
205
- const currentIndex = Math.max(0, backends.indexOf(effectiveChannelBackend));
206
- const chosen = backends[(currentIndex + direction + backends.length) % backends.length];
207
- if (chosen === effectiveChannelBackend) {
208
- openSettingsPicker({ light: true, overrides: { channelBackend: effectiveChannelBackend } });
201
+ const cycleChannelProvider = (direction = 1) => {
202
+ const providers = ['discord', 'telegram'];
203
+ const currentIndex = Math.max(0, providers.indexOf(effectiveChannelProvider));
204
+ const chosen = providers[(currentIndex + direction + providers.length) % providers.length];
205
+ if (chosen === effectiveChannelProvider) {
206
+ openSettingsPicker({ light: true, overrides: { channelProvider: effectiveChannelProvider } });
209
207
  return;
210
208
  }
211
209
  try {
212
- store.setBackend(chosen);
210
+ store.setChannelProvider(chosen);
213
211
  const label = chosen === 'telegram' ? 'Telegram' : 'Discord';
214
212
  const restartHint = (remoteEnabled || channelWorker?.running)
215
213
  ? `Channel set to ${label}. Restart remote to apply.`
216
214
  : `Channel set to ${label}.`;
217
215
  store.pushNotice(restartHint, 'info');
218
216
  } catch (e) {
219
- store.pushNotice(`channel backend failed: ${e?.message || e}`, 'error');
217
+ store.pushNotice(`channel provider failed: ${e?.message || e}`, 'error');
220
218
  }
221
- openSettingsPicker({ light: true, overrides: { channelBackend: chosen } });
219
+ openSettingsPicker({ light: true, overrides: { channelProvider: chosen } });
222
220
  };
223
221
  // Row order groups by concern — routing/model first, then session
224
222
  // behavior, integrations, channels/remote, system — and must stay in
@@ -304,10 +302,10 @@ export function createSettingsPicker({
304
302
  {
305
303
  value: 'memory',
306
304
  label: 'Memory',
307
- meta: boolLabel(memory.enabled !== false),
308
- description: memory.enabled === false
309
- ? 'Memory off. Core memories stay editable.'
310
- : 'Session memory and core memories.',
305
+ meta: `Recap ${boolLabel(recap.enabled !== false)}`,
306
+ description: recap.enabled === false
307
+ ? 'Background recap off. Core memories and on-demand recall stay available.'
308
+ : 'Background recap and core memories.',
311
309
  _action: 'memory',
312
310
  },
313
311
  {
@@ -352,11 +350,11 @@ export function createSettingsPicker({
352
350
  _action: 'channels',
353
351
  },
354
352
  {
355
- value: 'channel-backend',
353
+ value: 'channel-provider',
356
354
  label: 'Channel',
357
- meta: channelBackendLabel,
355
+ meta: channelProviderLabel,
358
356
  description: 'Left/Right or Enter changes channel type (Discord or Telegram).',
359
- _action: 'channel-backend',
357
+ _action: 'channel-provider',
360
358
  },
361
359
  {
362
360
  value: 'channel-setting',
@@ -412,7 +410,7 @@ export function createSettingsPicker({
412
410
  else if (item?._action === 'autocompact') applyCompaction({ auto: !(compaction.auto !== false) });
413
411
  else if (item?._action === 'channels') applyChannels(!(channels.enabled !== false));
414
412
  else if (item?._action === 'remote-runtime') applyRemoteRuntime();
415
- else if (item?._action === 'channel-backend') cycleChannelBackend(-1);
413
+ else if (item?._action === 'channel-provider') cycleChannelProvider(-1);
416
414
  else if (item?._action === 'output-style') cycleOutputStyle(-1);
417
415
  else if (item?._action === 'theme') cycleTheme(-1);
418
416
  else if (item?._action === 'workflow') cycleWorkflow(-1);
@@ -422,7 +420,7 @@ export function createSettingsPicker({
422
420
  else if (item?._action === 'autocompact') applyCompaction({ auto: !(compaction.auto !== false) });
423
421
  else if (item?._action === 'channels') applyChannels(!(channels.enabled !== false));
424
422
  else if (item?._action === 'remote-runtime') applyRemoteRuntime();
425
- else if (item?._action === 'channel-backend') cycleChannelBackend(1);
423
+ else if (item?._action === 'channel-provider') cycleChannelProvider(1);
426
424
  else if (item?._action === 'output-style') cycleOutputStyle(1);
427
425
  else if (item?._action === 'theme') cycleTheme(1);
428
426
  else if (item?._action === 'workflow') cycleWorkflow(1);
@@ -433,7 +431,7 @@ export function createSettingsPicker({
433
431
  else if (item._action === 'autocompact') applyCompaction({ auto: !(compaction.auto !== false) });
434
432
  else if (item._action === 'channels') applyChannels(!(channels.enabled !== false));
435
433
  else if (item._action === 'remote-runtime') applyRemoteRuntime();
436
- else if (item._action === 'channel-backend') cycleChannelBackend(1);
434
+ else if (item._action === 'channel-provider') cycleChannelProvider(1);
437
435
  else if (item._action === 'channel-setting') openChannelSettingTypePicker({ returnTo: openSettingsPicker });
438
436
  else if (item._action === 'output-style') openOutputStylePicker({ returnTo: openSettingsPicker });
439
437
  else if (item._action === 'theme') openThemePicker({ returnTo: openSettingsPicker });