mixdog 0.9.18 → 0.9.20

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 (214) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +93 -29
  3. package/package.json +5 -3
  4. package/scripts/build-runtime-windows.ps1 +242 -242
  5. package/scripts/build-tui.mjs +6 -0
  6. package/scripts/hook-bus-test.mjs +8 -0
  7. package/scripts/log-writer-guard-smoke.mjs +131 -0
  8. package/scripts/output-style-smoke.mjs +2 -2
  9. package/scripts/reactive-compact-persist-smoke.mjs +22 -6
  10. package/scripts/recall-bench-cases.json +10 -0
  11. package/scripts/recall-bench.mjs +91 -2
  12. package/scripts/recall-quality-cases.json +1 -2
  13. package/scripts/recall-usecase-cases.json +1 -1
  14. package/scripts/session-ingest-compaction-smoke.mjs +241 -0
  15. package/scripts/smoke-runtime-negative.ps1 +106 -106
  16. package/scripts/tool-efficiency-diag.mjs +5 -2
  17. package/scripts/tool-smoke.mjs +251 -72
  18. package/src/agents/debugger/AGENT.md +3 -3
  19. package/src/agents/heavy-worker/AGENT.md +7 -10
  20. package/src/agents/maintainer/AGENT.md +1 -2
  21. package/src/agents/reviewer/AGENT.md +1 -2
  22. package/src/agents/worker/AGENT.md +5 -8
  23. package/src/defaults/agents.json +3 -0
  24. package/src/help.mjs +2 -5
  25. package/src/lib/mixdog-debug.cjs +13 -0
  26. package/src/mixdog-session-runtime.mjs +7 -3311
  27. package/src/rules/agent/00-core.md +4 -3
  28. package/src/rules/agent/30-explorer.md +53 -22
  29. package/src/rules/lead/02-channels.md +3 -3
  30. package/src/rules/lead/lead-tool.md +3 -2
  31. package/src/rules/shared/01-tool.md +24 -29
  32. package/src/runtime/agent/orchestrator/context/collect.mjs +33 -9
  33. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +1 -1
  34. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -1
  35. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +24 -3
  36. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +3 -3
  37. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +663 -0
  38. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +6 -0
  39. package/src/runtime/agent/orchestrator/session/context-utils.mjs +2 -2
  40. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +153 -0
  41. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +49 -0
  42. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +250 -35
  43. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +9 -1
  44. package/src/runtime/agent/orchestrator/session/loop.mjs +8 -1860
  45. package/src/runtime/agent/orchestrator/session/manager/agent-runtime-singleton.mjs +29 -0
  46. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +686 -0
  47. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +60 -12
  48. package/src/runtime/agent/orchestrator/session/manager/env-utils.mjs +8 -0
  49. package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +159 -0
  50. package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +143 -0
  51. package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +268 -0
  52. package/src/runtime/agent/orchestrator/session/manager/provider-cache-key.mjs +22 -0
  53. package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +26 -0
  54. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +124 -0
  55. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +258 -0
  56. package/src/runtime/agent/orchestrator/session/manager/session-errors.mjs +20 -0
  57. package/src/runtime/agent/orchestrator/session/manager/session-id.mjs +9 -0
  58. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +475 -0
  59. package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +23 -0
  60. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +6 -4
  61. package/src/runtime/agent/orchestrator/session/manager.mjs +88 -2285
  62. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +440 -0
  63. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +153 -0
  64. package/src/runtime/agent/orchestrator/session/store.mjs +4 -1
  65. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +642 -0
  66. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +23 -3
  67. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +198 -6
  68. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -2
  69. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +13 -15
  70. package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +6 -1
  71. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +45 -3
  72. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +5 -2
  73. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +195 -3
  74. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +108 -2
  75. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +15 -3
  76. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +7 -0
  77. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +24 -2
  78. package/src/runtime/agent/orchestrator/tools/builtin.mjs +17 -1
  79. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +38 -0
  80. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +4 -4
  81. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +2 -2
  82. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +72 -8
  83. package/src/runtime/agent/orchestrator/tools/shell-policy-danger-target.mjs +5 -1
  84. package/src/runtime/agent/orchestrator/tools/shell-state.mjs +1 -1
  85. package/src/runtime/channels/backends/discord-gateway.mjs +14 -1
  86. package/src/runtime/channels/backends/discord.mjs +43 -1
  87. package/src/runtime/channels/index.mjs +6 -2096
  88. package/src/runtime/channels/lib/inbound-handler.mjs +328 -0
  89. package/src/runtime/channels/lib/inbound-routing.mjs +19 -12
  90. package/src/runtime/channels/lib/interaction-handlers.mjs +260 -0
  91. package/src/runtime/channels/lib/memory-client.mjs +32 -14
  92. package/src/runtime/channels/lib/network-retry.mjs +23 -0
  93. package/src/runtime/channels/lib/output-forwarder.mjs +38 -7
  94. package/src/runtime/channels/lib/owned-runtime.mjs +547 -0
  95. package/src/runtime/channels/lib/owner-heartbeat.mjs +13 -4
  96. package/src/runtime/channels/lib/runtime-paths.mjs +35 -1
  97. package/src/runtime/channels/lib/tool-dispatch.mjs +17 -7
  98. package/src/runtime/channels/lib/transcript-binding.mjs +336 -0
  99. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +39 -2
  100. package/src/runtime/channels/lib/worker-bootstrap.mjs +97 -0
  101. package/src/runtime/channels/lib/worker-ipc.mjs +201 -0
  102. package/src/runtime/channels/lib/worker-main.mjs +771 -0
  103. package/src/runtime/memory/index.mjs +73 -1725
  104. package/src/runtime/memory/lib/embedding-provider.mjs +4 -2
  105. package/src/runtime/memory/lib/embedding-worker.mjs +47 -6
  106. package/src/runtime/memory/lib/http-router.mjs +772 -0
  107. package/src/runtime/memory/lib/memory-action-handlers.mjs +901 -0
  108. package/src/runtime/memory/lib/memory-embed.mjs +28 -7
  109. package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +8 -2
  110. package/src/runtime/memory/lib/memory-recall-store.mjs +182 -9
  111. package/src/runtime/memory/lib/query-handlers.mjs +38 -6
  112. package/src/runtime/memory/lib/recall-format.mjs +106 -6
  113. package/src/runtime/memory/lib/session-ingest-runtime.mjs +401 -0
  114. package/src/runtime/memory/lib/session-ingest.mjs +1 -1
  115. package/src/runtime/shared/atomic-file.mjs +10 -4
  116. package/src/runtime/shared/background-tasks.mjs +4 -2
  117. package/src/runtime/shared/tool-execution-contract.mjs +1 -1
  118. package/src/runtime/shared/tool-result-summary.mjs +1 -1
  119. package/src/runtime/shared/tool-surface.mjs +30 -1
  120. package/src/session-runtime/boot-profile.mjs +36 -0
  121. package/src/session-runtime/channel-config-api.mjs +70 -0
  122. package/src/session-runtime/config-lifecycle.mjs +1 -1
  123. package/src/session-runtime/context-status.mjs +181 -0
  124. package/src/session-runtime/cwd-plugins.mjs +46 -3
  125. package/src/session-runtime/env.mjs +17 -0
  126. package/src/session-runtime/lifecycle-api.mjs +242 -0
  127. package/src/session-runtime/mcp-glue.mjs +24 -3
  128. package/src/session-runtime/model-route-api.mjs +198 -0
  129. package/src/session-runtime/output-styles.mjs +44 -10
  130. package/src/session-runtime/provider-auth-api.mjs +135 -0
  131. package/src/session-runtime/resource-api.mjs +282 -0
  132. package/src/session-runtime/runtime-core.mjs +2046 -0
  133. package/src/session-runtime/session-turn-api.mjs +274 -0
  134. package/src/session-runtime/tool-catalog.mjs +18 -264
  135. package/src/session-runtime/tool-defs.mjs +2 -2
  136. package/src/session-runtime/workflow-agents-api.mjs +238 -0
  137. package/src/session-runtime/workflow.mjs +16 -1
  138. package/src/standalone/agent-tool.mjs +2 -2
  139. package/src/standalone/channel-worker.mjs +78 -5
  140. package/src/standalone/explore-tool.mjs +1 -1
  141. package/src/standalone/memory-runtime-proxy.mjs +56 -6
  142. package/src/tui/App.jsx +88 -97
  143. package/src/tui/app/channel-pickers.mjs +45 -0
  144. package/src/tui/app/core-memory-picker.mjs +1 -1
  145. package/src/tui/app/slash-commands.mjs +0 -1
  146. package/src/tui/app/slash-dispatch.mjs +0 -16
  147. package/src/tui/app/transcript-window.mjs +44 -1
  148. package/src/tui/app/use-mouse-input.mjs +15 -2
  149. package/src/tui/app/use-prompt-handlers.mjs +7 -94
  150. package/src/tui/app/use-transcript-scroll.mjs +82 -4
  151. package/src/tui/app/use-transcript-window.mjs +65 -5
  152. package/src/tui/components/PromptInput.jsx +33 -64
  153. package/src/tui/components/ToolExecution.jsx +2 -2
  154. package/src/tui/dist/index.mjs +7908 -7558
  155. package/src/tui/engine/context-state.mjs +145 -0
  156. package/src/tui/engine/prompt-history.mjs +27 -0
  157. package/src/tui/engine/session-api-ext.mjs +478 -0
  158. package/src/tui/engine/session-api.mjs +545 -0
  159. package/src/tui/engine/session-flow.mjs +485 -0
  160. package/src/tui/engine/turn.mjs +1078 -0
  161. package/src/tui/engine.mjs +69 -2582
  162. package/src/tui/index.jsx +7 -0
  163. package/src/tui/lib/voice-setup.mjs +166 -0
  164. package/src/tui/paste-attachments.mjs +12 -5
  165. package/src/tui/prompt-history-store.mjs +125 -12
  166. package/vendor/ink/build/ink.js +16 -1
  167. package/vendor/ink/build/output.js +30 -4
  168. package/vendor/ink/build/render.js +5 -0
  169. package/scripts/bench/cache-probe-tasks.json +0 -8
  170. package/scripts/bench/lead-review-tasks-r3.json +0 -20
  171. package/scripts/bench/lead-review-tasks.json +0 -20
  172. package/scripts/bench/r4-mixed-tasks.json +0 -20
  173. package/scripts/bench/r5-orchestrated-task.json +0 -7
  174. package/scripts/bench/review-tasks.json +0 -20
  175. package/scripts/bench/round-codex.json +0 -114
  176. package/scripts/bench/round-mixdog-lead-r3.json +0 -269
  177. package/scripts/bench/round-mixdog-lead.json +0 -269
  178. package/scripts/bench/round-mixdog.json +0 -126
  179. package/scripts/bench/round-r10-bigsample.json +0 -679
  180. package/scripts/bench/round-r11-codexalign.json +0 -257
  181. package/scripts/bench/round-r13-clientmeta.json +0 -464
  182. package/scripts/bench/round-r14-betafeatures.json +0 -466
  183. package/scripts/bench/round-r15-fulldefault.json +0 -462
  184. package/scripts/bench/round-r16-sessionid.json +0 -466
  185. package/scripts/bench/round-r17-wirebytes.json +0 -456
  186. package/scripts/bench/round-r18-prewarm.json +0 -468
  187. package/scripts/bench/round-r19-clean.json +0 -472
  188. package/scripts/bench/round-r20-prewarm-clean.json +0 -475
  189. package/scripts/bench/round-r21-delta-retry.json +0 -473
  190. package/scripts/bench/round-r22-full-probe.json +0 -693
  191. package/scripts/bench/round-r23-itemprobe.json +0 -701
  192. package/scripts/bench/round-r24-shapefix.json +0 -677
  193. package/scripts/bench/round-r25-serial.json +0 -464
  194. package/scripts/bench/round-r26-parallel3.json +0 -671
  195. package/scripts/bench/round-r27-parallel10.json +0 -894
  196. package/scripts/bench/round-r28-parallel10-stagger.json +0 -882
  197. package/scripts/bench/round-r29-parallel10-stagger166.json +0 -886
  198. package/scripts/bench/round-r30-instid.json +0 -253
  199. package/scripts/bench/round-r31-upgradeprobe.json +0 -256
  200. package/scripts/bench/round-r32-vs-codex-lead.json +0 -254
  201. package/scripts/bench/round-r33-vs-codex-codex.json +0 -115
  202. package/scripts/bench/round-r34-orchestrated.json +0 -120
  203. package/scripts/bench/round-r35-orchestrated-codex.json +0 -61
  204. package/scripts/bench/round-r36-orchestrated-capped.json +0 -128
  205. package/scripts/bench/round-r4-codex.json +0 -114
  206. package/scripts/bench/round-r4-mixed.json +0 -225
  207. package/scripts/bench/round-r5-gpt-lead.json +0 -259
  208. package/scripts/bench/round-r6-codex.json +0 -114
  209. package/scripts/bench/round-r6-solo.json +0 -257
  210. package/scripts/bench/round-r7-full.json +0 -254
  211. package/scripts/bench/round-r8-fulldefault.json +0 -255
  212. package/src/tui/components/prompt-input/voice-indicator.mjs +0 -39
  213. package/src/tui/lib/voice-recorder.mjs +0 -469
  214. package/src/workflows/sequential/WORKFLOW.md +0 -51
@@ -0,0 +1,545 @@
1
+ /**
2
+ * src/tui/engine/session-api.mjs - part of the public engine session object.
3
+ */
4
+ import { compactEventDetail, projectNameFromPath } from './labels.mjs';
5
+ import { toolErrorDisplay } from './tool-result-text.mjs';
6
+ import { isQueuedEntryEditable, promptDisplayText } from './queue-helpers.mjs';
7
+ import { createEngineApiB } from './session-api-ext.mjs';
8
+
9
+ export function createEngineApi(bag) {
10
+ return { ...createEngineApiA(bag), ...createEngineApiB(bag) };
11
+ }
12
+
13
+ export function createEngineApiA(bag) {
14
+ const {
15
+ runtime, nextId, flags, pending, listeners, getState, set, pushItem, patchItem, replaceItems, pushNotice, autoClearState, agentStatusState, routeState, syncContextStats, denyAllToolApprovals, updateAgentJobCard, requeueEntriesFront, enqueue, autoClearBeforeSubmit, restoreQueued, resetStatsAndSyncContext,
16
+ } = bag;
17
+ return {
18
+ getState: () => getState(),
19
+ patchItem,
20
+ subscribe: (listener) => {
21
+ listeners.add(listener);
22
+ return () => listeners.delete(listener);
23
+ },
24
+ submit: (text, options = {}) => {
25
+ const t = promptDisplayText(text, options).trim();
26
+ if (!t) return false;
27
+ const mode = options.mode || 'prompt';
28
+ // Prompt input queued while a turn is active keeps the
29
+ // default `next` priority, so it is injected at the next tool/model
30
+ // boundary. Explicit options.priority still wins.
31
+ const priority = options.priority;
32
+ const queueOptions = {
33
+ ...options,
34
+ mode,
35
+ displayText: promptDisplayText(text, options),
36
+ priority,
37
+ };
38
+ // A running clear (idle auto-clear or session_manage) sets commandBusy;
39
+ // queue the prompt instead of dropping it — it drains after the clear.
40
+ if (flags.autoClearRunning) {
41
+ enqueue(text, queueOptions);
42
+ return true;
43
+ }
44
+ if (getState().commandBusy) return false;
45
+ if (getState().busy) {
46
+ enqueue(text, queueOptions);
47
+ return true;
48
+ }
49
+ void autoClearBeforeSubmit().then(() => enqueue(text, queueOptions));
50
+ return true;
51
+ },
52
+ restoreQueued,
53
+ setModel: async (m) => {
54
+ if (getState().commandBusy) return false;
55
+ set({ commandBusy: true });
56
+ try {
57
+ // Model changes apply to the NEXT session only (default setRoute
58
+ // behavior) — never rewrite the live session's provider/model, which
59
+ // would force a full prompt-cache rewrite mid-conversation.
60
+ await runtime.setRoute({ model: m });
61
+ set({ ...routeState(), stats: { ...getState().stats } });
62
+ return true;
63
+ } finally {
64
+ set({ commandBusy: false });
65
+ }
66
+ },
67
+ setEffort: async (value) => {
68
+ if (getState().commandBusy) return false;
69
+ set({ commandBusy: true });
70
+ try {
71
+ await runtime.setEffort(value);
72
+ set({ ...routeState() });
73
+ return runtime.effort || 'auto';
74
+ } finally {
75
+ set({ commandBusy: false });
76
+ }
77
+ },
78
+ setFast: async (value) => {
79
+ if (getState().commandBusy) return null;
80
+ set({ commandBusy: true });
81
+ try {
82
+ const enabled = await runtime.setFast(value);
83
+ set({ ...routeState() });
84
+ return enabled;
85
+ } finally {
86
+ set({ commandBusy: false });
87
+ }
88
+ },
89
+ toggleFast: async () => {
90
+ if (getState().commandBusy) return null;
91
+ set({ commandBusy: true });
92
+ try {
93
+ const enabled = await runtime.toggleFast();
94
+ set({ ...routeState() });
95
+ return enabled;
96
+ } finally {
97
+ set({ commandBusy: false });
98
+ }
99
+ },
100
+ setToolMode: (m) => {
101
+ void runtime.setToolMode(m)
102
+ .then(() => {
103
+ resetStatsAndSyncContext();
104
+ set({ ...routeState(), toolMode: runtime.toolMode, stats: { ...getState().stats } });
105
+ })
106
+ .catch((error) => pushNotice(toolErrorDisplay(error, 'tool'), 'error'));
107
+ },
108
+ getAutoClear: () => autoClearState(),
109
+ setAutoClear: (input = {}) => {
110
+ const next = runtime.setAutoClear?.(input) || autoClearState();
111
+ set({ autoClear: next });
112
+ return next;
113
+ },
114
+ getUpdateSettings: () => runtime.getUpdateSettings?.() || null,
115
+ setAutoUpdate: (enabled) => runtime.setAutoUpdate?.(enabled),
116
+ checkForUpdate: (input = {}) => runtime.checkForUpdate?.(input),
117
+ runUpdateNow: () => runtime.runUpdateNow?.(),
118
+ getUpdateStatus: () => runtime.getUpdateStatus?.() || { phase: 'idle' },
119
+ getProfile: () => runtime.getProfile?.() || { title: '', language: 'system', languages: [] },
120
+ setProfile: (input = {}) => {
121
+ const next = runtime.setProfile?.(input) || runtime.getProfile?.() || null;
122
+ return next;
123
+ },
124
+ getCompactionSettings: () => {
125
+ return runtime.getCompactionSettings?.() || {};
126
+ },
127
+ setCompactionSettings: async (input = {}) => {
128
+ if (getState().commandBusy) return null;
129
+ set({ commandBusy: true });
130
+ try {
131
+ const next = runtime.setCompactionSettings?.(input) || {};
132
+ set({ ...routeState(), stats: { ...getState().stats } });
133
+ // Context-stats recompute (transcript scan + per-message JSON
134
+ // stringify) is the secondary hitch source on this toggle; defer it
135
+ // off the key-handler tick so Ink repaints the setting change first.
136
+ // Stats become eventually consistent on the next tick/repaint.
137
+ setTimeout(() => {
138
+ syncContextStats({ allowEstimated: true });
139
+ set({ stats: { ...getState().stats } });
140
+ }, 0);
141
+ return next;
142
+ } finally {
143
+ set({ commandBusy: false });
144
+ }
145
+ },
146
+ getMemorySettings: () => {
147
+ return runtime.getMemorySettings?.() || { enabled: true };
148
+ },
149
+ setMemoryEnabled: async (enabled) => {
150
+ if (getState().commandBusy) return null;
151
+ set({ commandBusy: true });
152
+ try {
153
+ const next = await runtime.setMemoryEnabled?.(enabled);
154
+ set({ ...routeState(), stats: { ...getState().stats } });
155
+ // Deferred for the same reason as setCompactionSettings above: keep
156
+ // the recompute off the key-handler tick so the toggle repaints
157
+ // immediately; stats catch up right after.
158
+ setTimeout(() => {
159
+ syncContextStats({ allowEstimated: true });
160
+ set({ stats: { ...getState().stats } });
161
+ }, 0);
162
+ return next;
163
+ } finally {
164
+ set({ commandBusy: false });
165
+ }
166
+ },
167
+ getChannelSettings: (options = {}) => {
168
+ return runtime.getChannelSettings?.(options) || {
169
+ enabled: true,
170
+ ...(options?.includeStatus === false ? {} : { status: runtime.getChannelWorkerStatus?.() }),
171
+ };
172
+ },
173
+ setChannelsEnabled: async (enabled) => {
174
+ if (getState().commandBusy) return null;
175
+ set({ commandBusy: true });
176
+ try {
177
+ const next = await runtime.setChannelsEnabled?.(enabled);
178
+ set({ ...routeState(), stats: { ...getState().stats } });
179
+ return next;
180
+ } finally {
181
+ set({ commandBusy: false });
182
+ }
183
+ },
184
+ agentControl: async (args = {}) => {
185
+ if (getState().commandBusy) return null;
186
+ set({ commandBusy: true });
187
+ try {
188
+ const result = await runtime.agentControl(args);
189
+ const text = String(result ?? '').trim();
190
+ const itemId = nextId();
191
+ pushItem({
192
+ kind: 'tool',
193
+ id: itemId,
194
+ name: 'agent',
195
+ args,
196
+ result: null,
197
+ isError: false,
198
+ expanded: false,
199
+ count: 1,
200
+ completedCount: 0,
201
+ startedAt: Date.now(),
202
+ });
203
+ updateAgentJobCard(itemId, text, /^error:/i.test(text));
204
+ set(agentStatusState({ force: true }));
205
+ return result;
206
+ } finally {
207
+ set({ commandBusy: false });
208
+ }
209
+ },
210
+ toolsStatus: (query = '') => {
211
+ return runtime.toolsStatus?.(query) || { mode: getState().toolMode, count: 0, activeCount: 0, tools: [] };
212
+ },
213
+ selectTools: (names) => {
214
+ const result = runtime.selectTools?.(names) || { added: [], already: [], blocked: [], missing: [] };
215
+ const added = result.added?.length ? `added ${result.added.join(', ')}` : '';
216
+ const already = result.already?.length ? `already ${result.already.join(', ')}` : '';
217
+ const blocked = result.blocked?.length ? `blocked ${result.blocked.map((row) => row.name).join(', ')}` : '';
218
+ const missing = result.missing?.length ? `missing ${result.missing.join(', ')}` : '';
219
+ pushNotice(
220
+ [added, already, blocked, missing].filter(Boolean).join(' - ') || 'no tool changes',
221
+ result.blocked?.length || result.missing?.length ? 'warn' : 'info',
222
+ );
223
+ return result;
224
+ },
225
+ setCwd: (path, options = {}) => {
226
+ const next = runtime.setCwd(path);
227
+ set({ cwd: next });
228
+ if (options?.notice !== false) {
229
+ pushNotice(options?.message || `Project set: ${projectNameFromPath(next)}`, 'info');
230
+ }
231
+ return next;
232
+ },
233
+ getSystemShell: () => {
234
+ return runtime.getSystemShell?.() || runtime.systemShell || { source: 'auto', command: '', effective: '' };
235
+ },
236
+ setSystemShell: (command) => {
237
+ const next = runtime.setSystemShell?.(command) || { source: 'auto', command: '', effective: '' };
238
+ set({ ...routeState(), systemShell: next });
239
+ pushNotice(`system shell -> ${next.effective || 'auto'}`, 'info');
240
+ return next;
241
+ },
242
+ mcpStatus: () => {
243
+ return runtime.mcpStatus?.() || { servers: [], configuredCount: 0, connectedCount: 0, failedCount: 0 };
244
+ },
245
+ reconnectMcp: async () => {
246
+ if (getState().commandBusy) return null;
247
+ set({ commandBusy: true });
248
+ try {
249
+ const status = await runtime.reconnectMcp?.();
250
+ resetStatsAndSyncContext();
251
+ set({ ...routeState(), stats: { ...getState().stats } });
252
+ pushNotice(
253
+ `mcp reconnect: ${status?.connectedCount || 0}/${status?.configuredCount || 0} connected${status?.failedCount ? ` - ${status.failedCount} failed` : ''}`,
254
+ status?.failedCount ? 'warn' : 'info',
255
+ );
256
+ return status;
257
+ } finally {
258
+ set({ commandBusy: false });
259
+ }
260
+ },
261
+ addMcpServer: async (input) => {
262
+ if (getState().commandBusy) return null;
263
+ set({ commandBusy: true });
264
+ try {
265
+ const result = await runtime.addMcpServer?.(input);
266
+ resetStatsAndSyncContext();
267
+ set({ ...routeState(), stats: { ...getState().stats } });
268
+ pushNotice(`mcp added: ${result?.name || input?.name || 'server'}`, 'info');
269
+ return result;
270
+ } finally {
271
+ set({ commandBusy: false });
272
+ }
273
+ },
274
+ removeMcpServer: async (name) => {
275
+ if (getState().commandBusy) return null;
276
+ set({ commandBusy: true });
277
+ try {
278
+ const status = await runtime.removeMcpServer?.(name);
279
+ resetStatsAndSyncContext();
280
+ set({ ...routeState(), stats: { ...getState().stats } });
281
+ pushNotice(`mcp removed: ${name}`, 'info');
282
+ return status;
283
+ } finally {
284
+ set({ commandBusy: false });
285
+ }
286
+ },
287
+ setMcpServerEnabled: async (name, enabled) => {
288
+ if (getState().commandBusy) return null;
289
+ set({ commandBusy: true });
290
+ try {
291
+ const status = await runtime.setMcpServerEnabled?.(name, enabled);
292
+ resetStatsAndSyncContext();
293
+ set({ ...routeState(), stats: { ...getState().stats } });
294
+ pushNotice(`mcp ${enabled ? 'enabled' : 'disabled'}: ${name}`, 'info');
295
+ return status;
296
+ } finally {
297
+ set({ commandBusy: false });
298
+ }
299
+ },
300
+ getDisabledSkills: () => runtime.getDisabledSkills?.() || { disabled: [] },
301
+ setDisabledSkills: (disabled) => runtime.setDisabledSkills?.(disabled) || { disabled: [] },
302
+ skillsStatus: () => {
303
+ return runtime.skillsStatus?.() || { cwd: getState().cwd, count: 0, skills: [] };
304
+ },
305
+ skillContent: (name) => {
306
+ return runtime.skillContent?.(name);
307
+ },
308
+ addSkill: async (input) => {
309
+ if (getState().commandBusy) return null;
310
+ set({ commandBusy: true });
311
+ try {
312
+ const result = await runtime.addSkill?.(input);
313
+ resetStatsAndSyncContext();
314
+ set({ ...routeState(), stats: { ...getState().stats } });
315
+ pushNotice(`skill added: ${result?.skill?.name || input?.name || 'skill'}`, 'info');
316
+ return result;
317
+ } finally {
318
+ set({ commandBusy: false });
319
+ }
320
+ },
321
+ reloadSkills: async () => {
322
+ if (getState().commandBusy) return null;
323
+ set({ commandBusy: true });
324
+ try {
325
+ const status = await runtime.reloadSkills?.();
326
+ resetStatsAndSyncContext();
327
+ set({ ...routeState(), stats: { ...getState().stats } });
328
+ pushNotice(`skills reload: ${status?.count || 0} available`, 'info');
329
+ return status;
330
+ } finally {
331
+ set({ commandBusy: false });
332
+ }
333
+ },
334
+ pluginsStatus: () => {
335
+ return runtime.pluginsStatus?.() || { count: 0, plugins: [] };
336
+ },
337
+ reloadPlugins: async () => {
338
+ if (getState().commandBusy) return null;
339
+ set({ commandBusy: true });
340
+ try {
341
+ const status = await runtime.reloadPlugins?.();
342
+ resetStatsAndSyncContext();
343
+ set({ ...routeState(), stats: { ...getState().stats } });
344
+ pushNotice(`plugins reload: ${status?.count || 0} detected`, 'info');
345
+ return status;
346
+ } finally {
347
+ set({ commandBusy: false });
348
+ }
349
+ },
350
+ addPlugin: async (source) => {
351
+ if (getState().commandBusy) return null;
352
+ set({ commandBusy: true });
353
+ try {
354
+ const result = await runtime.addPlugin?.(source);
355
+ resetStatsAndSyncContext();
356
+ set({ ...routeState(), stats: { ...getState().stats } });
357
+ pushNotice(`plugin added: ${result?.plugin?.title || result?.plugin?.name || source}`, 'info');
358
+ return result;
359
+ } finally {
360
+ set({ commandBusy: false });
361
+ }
362
+ },
363
+ updatePlugin: async (plugin) => {
364
+ if (getState().commandBusy) return null;
365
+ set({ commandBusy: true });
366
+ try {
367
+ const result = await runtime.updatePlugin?.(plugin);
368
+ resetStatsAndSyncContext();
369
+ set({ ...routeState(), stats: { ...getState().stats } });
370
+ pushNotice(`plugin updated: ${result?.plugin?.title || result?.plugin?.name || plugin?.name || plugin}`, 'info');
371
+ return result;
372
+ } finally {
373
+ set({ commandBusy: false });
374
+ }
375
+ },
376
+ removePlugin: async (plugin) => {
377
+ if (getState().commandBusy) return null;
378
+ set({ commandBusy: true });
379
+ try {
380
+ const result = await runtime.removePlugin?.(plugin);
381
+ resetStatsAndSyncContext();
382
+ set({ ...routeState(), stats: { ...getState().stats } });
383
+ pushNotice(`plugin uninstalled: ${result?.plugin?.title || result?.plugin?.name || plugin?.name || plugin}`, 'info');
384
+ return result;
385
+ } finally {
386
+ set({ commandBusy: false });
387
+ }
388
+ },
389
+ enablePluginMcp: async (plugin) => {
390
+ if (getState().commandBusy) return null;
391
+ set({ commandBusy: true });
392
+ try {
393
+ const result = await runtime.enablePluginMcp?.(plugin);
394
+ resetStatsAndSyncContext();
395
+ set({ ...routeState(), stats: { ...getState().stats } });
396
+ pushNotice(`plugin MCP enabled: ${result?.serverName || plugin?.name || 'plugin'}`, 'info');
397
+ return result;
398
+ } finally {
399
+ set({ commandBusy: false });
400
+ }
401
+ },
402
+ hooksStatus: () => {
403
+ return runtime.hooksStatus?.() || { enabled: false, events: [], recent: [] };
404
+ },
405
+ contextStatus: () => {
406
+ return runtime.contextStatus?.() || null;
407
+ },
408
+ addHookRule: (rule) => {
409
+ const rules = runtime.addHookRule?.(rule) || [];
410
+ pushNotice(`hook rule added (${rules.length} total)`, 'info');
411
+ return rules;
412
+ },
413
+ setHookRuleEnabled: (index, enabled) => {
414
+ const rules = runtime.setHookRuleEnabled?.(index, enabled) || [];
415
+ pushNotice(`hook rule ${index + 1} ${enabled ? 'enabled' : 'disabled'}`, 'info');
416
+ return rules;
417
+ },
418
+ deleteHookRule: (index) => {
419
+ const rules = runtime.deleteHookRule?.(index) || [];
420
+ pushNotice(`hook rule ${index + 1} deleted`, 'info');
421
+ return rules;
422
+ },
423
+ memoryControl: async (args = {}, options = {}) => {
424
+ if (getState().commandBusy) return null;
425
+ set({ commandBusy: true });
426
+ try {
427
+ const result = await runtime.memoryControl(args);
428
+ const text = String(result || '').trim() || '(empty memory result)';
429
+ if (!options.silent) pushNotice(text, 'info');
430
+ return result;
431
+ } finally {
432
+ set({ commandBusy: false });
433
+ }
434
+ },
435
+ recall: async (query, args = {}) => {
436
+ if (getState().commandBusy) return null;
437
+ const startedAt = Date.now();
438
+ set({ commandBusy: true, commandStatus: { active: true, verb: 'Recalling memory', startedAt, mode: 'recalling' } });
439
+ try {
440
+ const result = await runtime.recall(query, args);
441
+ pushNotice(String(result || '').trim() || '(empty recall result)', 'info');
442
+ return result;
443
+ } finally {
444
+ set({ commandBusy: false, commandStatus: null });
445
+ }
446
+ },
447
+ compact: async () => {
448
+ if (getState().commandBusy) return null;
449
+ if (getState().busy) {
450
+ pushNotice('Compact skipped: turn in progress', 'info');
451
+ return { changed: false, reason: 'compact skipped: turn in progress' };
452
+ }
453
+ const startedAt = Date.now();
454
+ set({ commandBusy: true, commandStatus: { active: true, verb: 'Compacting conversation', startedAt, mode: 'compacting' } });
455
+ try {
456
+ // Give Ink one event-loop turn to paint the compacting spinner before
457
+ // runtime.compact() starts synchronous session/transcript work (same
458
+ // yield as the auto-clear path; without it /compact looks frozen with
459
+ // no spinner until the compact already finished).
460
+ await new Promise((resolve) => setTimeout(resolve, 0));
461
+ const result = await runtime.compact({ recoverAgent: true });
462
+ syncContextStats({ allowEstimated: true });
463
+ set({ ...routeState(), stats: { ...getState().stats } });
464
+ if (result) {
465
+ pushItem({
466
+ kind: 'statusdone',
467
+ id: nextId(),
468
+ label: result.error ? 'Compact failed' : (result.changed === false ? 'Compact checked' : 'Compact complete'),
469
+ detail: compactEventDetail({
470
+ stage: 'manual',
471
+ trigger: 'manual',
472
+ status: result.error ? 'failed' : (result.changed === false ? 'no_change' : 'compacted'),
473
+ compactType: result.compactType,
474
+ beforeTokens: result.beforeTokens,
475
+ afterTokens: result.afterTokens,
476
+ beforeMessages: result.beforeMessages,
477
+ afterMessages: result.afterMessages,
478
+ semantic: result.semanticCompact,
479
+ recallFastTrack: result.recallFastTrack,
480
+ durationMs: Date.now() - startedAt,
481
+ error: result.error,
482
+ }),
483
+ });
484
+ } else {
485
+ // null = session missing/closed: still surface a done row so
486
+ // /compact never ends silently without a completion marker.
487
+ pushItem({
488
+ kind: 'statusdone',
489
+ id: nextId(),
490
+ label: 'Compact failed',
491
+ detail: 'no active session',
492
+ });
493
+ }
494
+ return result;
495
+ } finally {
496
+ set({ commandBusy: false, commandStatus: null });
497
+ }
498
+ },
499
+ abort: () => {
500
+ if (!getState().busy) return false;
501
+ denyAllToolApprovals('interrupted by user');
502
+ const restoreState = flags.activePromptRestore;
503
+ // A queued steering prompt means the user already redirected the turn:
504
+ // interrupting should just cancel the running turn and let the steering
505
+ // prompt run next, NOT resurrect the in-flight prompt back into the draft.
506
+ const hasPendingSteering = pending.some((entry) => isQueuedEntryEditable(entry));
507
+ const canRestore = restoreState?.restorable && !hasPendingSteering;
508
+ const restoreText = canRestore ? restoreState.text : '';
509
+ const restorePastedImages = canRestore && restoreState?.pastedImages ? restoreState.pastedImages : null;
510
+ const restorePastedTexts = canRestore && restoreState?.pastedTexts ? restoreState.pastedTexts : null;
511
+ // When steering suppresses the restore, the interrupted prompt's pasted
512
+ // images never get committed (onCommitted won't fire) nor re-installed into
513
+ // the draft, so hand them back for cleanup to avoid a stale `[Image #id]`
514
+ // lingering in the paste snapshot.
515
+ const discardPastedImages = restoreState?.restorable && hasPendingSteering && restoreState?.pastedImages
516
+ ? restoreState.pastedImages
517
+ : null;
518
+ const discardPastedTexts = restoreState?.restorable && hasPendingSteering && restoreState?.pastedTexts
519
+ ? restoreState.pastedTexts
520
+ : null;
521
+ const requeueEntries = restoreState && !restoreState.committed && Array.isArray(restoreState.requeueEntries)
522
+ ? restoreState.requeueEntries.slice()
523
+ : [];
524
+ const aborted = runtime.abort('cli-react-abort');
525
+ if (restoreState) {
526
+ if ((restoreText || requeueEntries.length > 0) && aborted !== false) {
527
+ restoreState.reclaimed = true;
528
+ const idSet = new Set((restoreState.submittedIds || []).filter((id) => id != null));
529
+ const patch = { spinner: null, thinking: null, lastTurn: null };
530
+ if (idSet.size > 0) {
531
+ const items = getState().items.filter((item) => !idSet.has(item?.id));
532
+ if (items.length !== getState().items.length) {
533
+ patch.items = replaceItems(items);
534
+ }
535
+ }
536
+ set(patch);
537
+ if (requeueEntries.length > 0) requeueEntriesFront(requeueEntries);
538
+ }
539
+ restoreState.restorable = false;
540
+ restoreState.requeueEntries = [];
541
+ }
542
+ return { aborted, restoreText, pastedImages: restorePastedImages, discardPastedImages, pastedTexts: restorePastedTexts, discardPastedTexts };
543
+ },
544
+ };
545
+ }