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