mixdog 0.9.84 → 0.9.86
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/README.md +7 -1
- package/package.json +3 -2
- package/scripts/channel-daemon-stub.mjs +2 -1
- package/src/defaults/agents.json +12 -0
- package/src/defaults/skills/setup/SKILL.md +44 -12
- package/src/help.mjs +40 -9
- package/src/lib/keychain-cjs.cjs +11 -1
- package/src/rules/agent/43-title-agent.md +22 -0
- package/src/rules/shared/01-tool.md +3 -3
- package/src/runtime/agent/orchestrator/mcp/client.mjs +24 -0
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +84 -3
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +30 -3
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +73 -45
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +15 -18
- package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +38 -4
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +183 -41
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +31 -12
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +12 -2
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +170 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +45 -6
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +26 -0
- package/src/runtime/agent/orchestrator/session/token-bpe.mjs +42 -0
- package/src/runtime/agent/orchestrator/session/token-native.mjs +186 -0
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +15 -5
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +20 -0
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +24 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +44 -73
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +286 -0
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +152 -18
- package/src/runtime/agent/orchestrator/tools/token-binary-fetcher.mjs +201 -0
- package/src/runtime/agent/orchestrator/tools/token-manifest.json +5 -0
- package/src/runtime/channels/lib/owned-runtime.mjs +6 -8
- package/src/runtime/channels/lib/parent-bridge.mjs +27 -15
- package/src/runtime/channels/lib/webhook/relay-tunnel.mjs +3 -3
- package/src/runtime/channels/lib/worker-ipc.mjs +17 -4
- package/src/runtime/media/adapters/gemini-video.mjs +1 -1
- package/src/runtime/media/renditions.mjs +1 -1
- package/src/runtime/media/store.mjs +1 -1
- package/src/runtime/memory/index.mjs +14 -5
- package/src/runtime/memory/lib/agent-ipc.mjs +11 -12
- package/src/runtime/shared/atomic-file.mjs +76 -38
- package/src/runtime/shared/automation-attachments.mjs +3 -3
- package/src/runtime/shared/child-guardian.mjs +15 -5
- package/src/runtime/shared/child-spawn-gate.mjs +6 -12
- package/src/runtime/shared/err-text.mjs +1 -0
- package/src/runtime/shared/resource-admission.mjs +7 -3
- package/src/runtime/shared/safe-ipc-send.mjs +34 -0
- package/src/runtime/shared/turn-snapshot.mjs +0 -3
- package/src/session-runtime/lifecycle-api.mjs +43 -12
- package/src/session-runtime/prewarm.mjs +35 -0
- package/src/session-runtime/remote-control.mjs +8 -47
- package/src/session-runtime/remote-transcript.mjs +3 -7
- package/src/session-runtime/route-preparation.mjs +24 -0
- package/src/session-runtime/runtime-core.mjs +42 -86
- package/src/session-runtime/session-turn-api.mjs +9 -2
- package/src/session-runtime/workflow-agents-api.mjs +3 -0
- package/src/standalone/agent-shard/shard-child.mjs +300 -0
- package/src/standalone/agent-shard/shard-pool.mjs +442 -0
- package/src/standalone/agent-tool.mjs +222 -0
- package/src/standalone/channel-daemon-transport.mjs +126 -185
- package/src/standalone/channel-daemon.mjs +11 -11
- package/src/standalone/channel-worker.mjs +21 -19
- package/src/standalone/memory-runtime-proxy.mjs +98 -23
- package/src/tui/App.jsx +16 -9
- package/src/tui/app/channel-pickers.mjs +3 -8
- package/src/tui/app/route-pickers.mjs +117 -152
- package/src/tui/app/settings-picker.mjs +90 -89
- package/src/tui/app/slash-commands.mjs +7 -7
- package/src/tui/app/slash-dispatch.mjs +23 -58
- package/src/tui/app/theme-effort-pickers.mjs +3 -1
- package/src/tui/components/StatusLine.jsx +1 -1
- package/src/tui/components/tool-execution/ResultBody.jsx +1 -1
- package/src/tui/dist/index.mjs +358 -380
- package/src/tui/engine/live-share.mjs +2 -2
- package/src/tui/engine/session-api-ext.mjs +24 -9
- package/src/tui/engine/session-flow.mjs +20 -10
- package/src/tui/engine/turn.mjs +28 -16
- package/src/tui/engine.mjs +24 -2
- package/src/tui/figures.mjs +0 -1
- package/src/ui/statusline-format.mjs +0 -1
- package/src/hooks/lib/permission-evaluator.cjs +0 -24
- package/src/lib/config-cjs.cjs +0 -61
- package/src/runtime/shared/launcher-control.mjs +0 -258
- package/src/runtime/shared/workspace-router.mjs +0 -259
|
@@ -103,10 +103,52 @@ function isMemoryReadOnlyToolCall(name, args = {}) {
|
|
|
103
103
|
return false;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
function
|
|
107
|
-
|
|
106
|
+
function memoryAbortError(reason) {
|
|
107
|
+
if (reason instanceof Error) return reason;
|
|
108
|
+
const error = new Error(typeof reason === 'string' && reason ? reason : 'memory proxy request aborted');
|
|
109
|
+
error.name = 'AbortError';
|
|
110
|
+
error.code = 'ABORT_ERR';
|
|
111
|
+
return error;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function requestJson({
|
|
115
|
+
port,
|
|
116
|
+
method = 'GET',
|
|
117
|
+
path = '/',
|
|
118
|
+
body = null,
|
|
119
|
+
timeoutMs = 10_000,
|
|
120
|
+
headers = {},
|
|
121
|
+
signal = null,
|
|
122
|
+
}) {
|
|
123
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
124
|
+
let settled = false;
|
|
125
|
+
let req = null;
|
|
126
|
+
const cleanup = () => {
|
|
127
|
+
try { signal?.removeEventListener?.('abort', onAbort); } catch {}
|
|
128
|
+
};
|
|
129
|
+
const resolve = (value) => {
|
|
130
|
+
if (settled) return;
|
|
131
|
+
settled = true;
|
|
132
|
+
cleanup();
|
|
133
|
+
resolvePromise(value);
|
|
134
|
+
};
|
|
135
|
+
const reject = (error) => {
|
|
136
|
+
if (settled) return;
|
|
137
|
+
settled = true;
|
|
138
|
+
cleanup();
|
|
139
|
+
rejectPromise(error);
|
|
140
|
+
};
|
|
141
|
+
const onAbort = () => {
|
|
142
|
+
const error = memoryAbortError(signal?.reason);
|
|
143
|
+
if (req) req.destroy(error);
|
|
144
|
+
else reject(error);
|
|
145
|
+
};
|
|
146
|
+
if (signal?.aborted) {
|
|
147
|
+
reject(memoryAbortError(signal.reason));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
108
150
|
const payload = body == null ? null : JSON.stringify(body);
|
|
109
|
-
|
|
151
|
+
req = http.request({
|
|
110
152
|
hostname: '127.0.0.1',
|
|
111
153
|
port,
|
|
112
154
|
path,
|
|
@@ -135,14 +177,19 @@ function requestJson({ port, method = 'GET', path = '/', body = null, timeoutMs
|
|
|
135
177
|
reject(error);
|
|
136
178
|
return;
|
|
137
179
|
}
|
|
138
|
-
|
|
180
|
+
resolve(parsed ?? { raw: data });
|
|
139
181
|
});
|
|
182
|
+
res.on('error', reject);
|
|
140
183
|
});
|
|
141
184
|
req.on('error', reject);
|
|
142
185
|
req.on('timeout', () => {
|
|
143
|
-
req.destroy();
|
|
144
|
-
reject(new Error(`memory proxy request timed out: ${method} ${path}`));
|
|
186
|
+
req.destroy(new Error(`memory proxy request timed out: ${method} ${path}`));
|
|
145
187
|
});
|
|
188
|
+
try { signal?.addEventListener?.('abort', onAbort, { once: true }); } catch {}
|
|
189
|
+
if (signal?.aborted) {
|
|
190
|
+
onAbort();
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
146
193
|
if (payload) req.write(payload);
|
|
147
194
|
req.end();
|
|
148
195
|
});
|
|
@@ -496,26 +543,54 @@ export function createStandaloneMemoryRuntime({
|
|
|
496
543
|
return { running: true, port, mode: 'http-proxy' };
|
|
497
544
|
}
|
|
498
545
|
|
|
499
|
-
async function handleToolCall(name, args = {}) {
|
|
546
|
+
async function handleToolCall(name, args = {}, signalOrOptions = null) {
|
|
547
|
+
const signal = signalOrOptions?.signal || signalOrOptions || null;
|
|
500
548
|
const readOnlyRpc = isMemoryReadOnlyToolCall(name, args);
|
|
501
549
|
const callId = `mem_${process.pid}_${nextCallId++}`;
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
if (
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
port,
|
|
550
|
+
let activePort = null;
|
|
551
|
+
let rpcStarted = false;
|
|
552
|
+
const throwIfAborted = () => {
|
|
553
|
+
if (signal?.aborted) throw memoryAbortError(signal.reason);
|
|
554
|
+
};
|
|
555
|
+
const cancelRemote = () => {
|
|
556
|
+
if (!activePort || !rpcStarted) return;
|
|
557
|
+
void requestJson({
|
|
558
|
+
port: activePort,
|
|
512
559
|
method: 'POST',
|
|
513
|
-
path: '/api/
|
|
514
|
-
body: {
|
|
515
|
-
timeoutMs:
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
560
|
+
path: '/api/cancel',
|
|
561
|
+
body: { callId },
|
|
562
|
+
timeoutMs: 1500,
|
|
563
|
+
}).catch(() => {});
|
|
564
|
+
};
|
|
565
|
+
throwIfAborted();
|
|
566
|
+
try { signal?.addEventListener?.('abort', cancelRemote, { once: true }); } catch {}
|
|
567
|
+
try {
|
|
568
|
+
return await withTransientMemoryRpcRetry(async () => {
|
|
569
|
+
throwIfAborted();
|
|
570
|
+
await start();
|
|
571
|
+
throwIfAborted();
|
|
572
|
+
let port = portCache || await findLivePort({ allowStarting: true });
|
|
573
|
+
if (!port) throw new Error('memory runtime is not available');
|
|
574
|
+
// ensureClientRegistered may respawn onto a fresh daemon/port; target the
|
|
575
|
+
// port it hands back so the RPC and registration always hit the same one.
|
|
576
|
+
port = await ensureClientRegistered(port);
|
|
577
|
+
if (!port) throw new Error('memory runtime is not available');
|
|
578
|
+
activePort = port;
|
|
579
|
+
throwIfAborted();
|
|
580
|
+
rpcStarted = true;
|
|
581
|
+
return await requestJson({
|
|
582
|
+
port,
|
|
583
|
+
method: 'POST',
|
|
584
|
+
path: '/api/tool',
|
|
585
|
+
body: { name, arguments: args || {} },
|
|
586
|
+
timeoutMs: Math.max(1000, Number(process.env.MIXDOG_MEMORY_TOOL_TIMEOUT_MS) || 180_000),
|
|
587
|
+
headers: { 'X-Mixdog-Call-Id': callId },
|
|
588
|
+
signal,
|
|
589
|
+
});
|
|
590
|
+
}, { readOnlyRpc });
|
|
591
|
+
} finally {
|
|
592
|
+
try { signal?.removeEventListener?.('abort', cancelRemote); } catch {}
|
|
593
|
+
}
|
|
519
594
|
}
|
|
520
595
|
|
|
521
596
|
async function buildSessionCoreMemoryPayload(sessionCwd) {
|
package/src/tui/App.jsx
CHANGED
|
@@ -686,8 +686,6 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
686
686
|
openAgentsPicker,
|
|
687
687
|
openWorkflowPicker,
|
|
688
688
|
openOutputStylePicker,
|
|
689
|
-
openBridgePicker,
|
|
690
|
-
openToolsPicker,
|
|
691
689
|
} = createRoutePickers({
|
|
692
690
|
store,
|
|
693
691
|
state,
|
|
@@ -699,7 +697,6 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
699
697
|
setContextPanel,
|
|
700
698
|
closeUsagePanel,
|
|
701
699
|
clean,
|
|
702
|
-
copyToClipboard,
|
|
703
700
|
routeLabel,
|
|
704
701
|
agentModelParts,
|
|
705
702
|
agentModelProfile,
|
|
@@ -726,8 +723,6 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
726
723
|
openAgentsPicker: (...a) => openAgentsPicker(...a),
|
|
727
724
|
openWorkflowPicker: (...a) => openWorkflowPicker(...a),
|
|
728
725
|
openOutputStylePicker: (...a) => openOutputStylePicker(...a),
|
|
729
|
-
openBridgePicker: (...a) => openBridgePicker(...a),
|
|
730
|
-
openToolsPicker: (...a) => openToolsPicker(...a),
|
|
731
726
|
openProviderSetupPicker: (...a) => openProviderSetupPicker(...a),
|
|
732
727
|
openThemePicker: (...a) => openThemePicker(...a),
|
|
733
728
|
openAutoClearPicker: (...a) => openAutoClearPicker(...a),
|
|
@@ -736,6 +731,7 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
736
731
|
openPluginsPicker: (...a) => openPluginsPicker(...a),
|
|
737
732
|
openHooksPicker: (...a) => openHooksPicker(...a),
|
|
738
733
|
openSkillsPicker: (...a) => openSkillsPicker(...a),
|
|
734
|
+
openMemoryCorePicker: (...a) => openMemoryCorePicker(...a),
|
|
739
735
|
openUpdatePicker: (...a) => openUpdatePicker(...a),
|
|
740
736
|
openChannelSettingTypePicker: (...a) => openChannelSettingTypePicker(...a),
|
|
741
737
|
});
|
|
@@ -756,10 +752,8 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
756
752
|
openThemePicker: (...a) => openThemePicker(...a),
|
|
757
753
|
themeNotice,
|
|
758
754
|
openEffortPicker: (...a) => openEffortPicker(...a),
|
|
759
|
-
projectNameFromPath,
|
|
760
755
|
enterProject: (...a) => enterProject(...a),
|
|
761
756
|
openProjectPicker: (...a) => openProjectPicker(...a),
|
|
762
|
-
openToolsPicker: (...a) => openToolsPicker(...a),
|
|
763
757
|
openMcpPicker: (...a) => openMcpPicker(...a),
|
|
764
758
|
openSkillsPicker: (...a) => openSkillsPicker(...a),
|
|
765
759
|
openPluginsPicker: (...a) => openPluginsPicker(...a),
|
|
@@ -2510,8 +2504,6 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
2510
2504
|
// Slash search floats above the normal prompt. Actual option panels own the
|
|
2511
2505
|
// prompt/status area, so they hide those rows and expand into that space.
|
|
2512
2506
|
const inputBoxHidden = expandedOptionPanel || bootSettling;
|
|
2513
|
-
const showWelcomeBanner = (state.items.length === 0 && !hasFloatingPanel) || projectSelectionActive || onboardingActive;
|
|
2514
|
-
const WELCOME_ROWS = showWelcomeBanner ? 11 : 0;
|
|
2515
2507
|
const liveSpinner = state.spinner?.active ? state.spinner : (state.commandStatus?.active ? state.commandStatus : null);
|
|
2516
2508
|
// Command-status spinner (auto-clear/compact/etc.) is NOT part of the
|
|
2517
2509
|
// spinner → TurnDone handoff: it typically starts while the transcript tail
|
|
@@ -2616,6 +2608,21 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
2616
2608
|
const queuedCompact = queuedFullRows > queuedRowBudget;
|
|
2617
2609
|
const queuedRows = queuedVisible ? (queuedCompact ? state.queued.length : queuedFullRows) : 0;
|
|
2618
2610
|
const INPUT_BOX_ROWS = promptBoxRows + promptMetaRows + overlayHintRows;
|
|
2611
|
+
// Welcome banner visibility (computed here, after the prompt/panel row math
|
|
2612
|
+
// it depends on). The slash palette must NOT unmount the banner on the empty
|
|
2613
|
+
// transcript: dropping the 11-row banner the moment '/' opened the palette
|
|
2614
|
+
// (and remounting it on Esc) read as the whole top of the screen jumping on
|
|
2615
|
+
// command-palette entry. Keep the banner mounted through a slash session
|
|
2616
|
+
// whenever the terminal is tall enough to hold banner + palette + prompt +
|
|
2617
|
+
// statusline at once; short terminals still yield the banner so the palette
|
|
2618
|
+
// chrome never clips (the floating container clips from the top).
|
|
2619
|
+
const WELCOME_BANNER_ROWS = 11;
|
|
2620
|
+
const SLASH_PALETTE_ROWS = PANEL_MAX_VISIBLE + PANEL_CHROME_ROWS;
|
|
2621
|
+
const slashKeepsWelcomeBanner = slashPaletteOpen
|
|
2622
|
+
&& resizeState.rows >= WELCOME_BANNER_ROWS + SLASH_PALETTE_ROWS + INPUT_BOX_ROWS + STATUSLINE_ROWS + 1;
|
|
2623
|
+
const showWelcomeBanner = (state.items.length === 0 && (!hasFloatingPanel || slashKeepsWelcomeBanner))
|
|
2624
|
+
|| projectSelectionActive || onboardingActive;
|
|
2625
|
+
const WELCOME_ROWS = showWelcomeBanner ? WELCOME_BANNER_ROWS : 0;
|
|
2619
2626
|
const baseReserve = WELCOME_ROWS + SCROLL_HINT_ROWS + LIVE_STATUS_ROWS + INPUT_BOX_ROWS + STATUSLINE_ROWS + queuedRows;
|
|
2620
2627
|
const maxFloatingPanelRows = Math.max(0, resizeState.rows - baseReserve - 1);
|
|
2621
2628
|
const desiredFloatingPanelRows = toolApproval
|
|
@@ -309,12 +309,13 @@ export function createChannelPickers({
|
|
|
309
309
|
value: 'webhook-endpoint',
|
|
310
310
|
label: 'Webhook endpoint',
|
|
311
311
|
// Relay tunnel: public exposure is automatic, so the endpoint is
|
|
312
|
-
// "On" whenever the webhook server itself is enabled.
|
|
312
|
+
// "On" whenever the webhook server itself is enabled. Info-only row:
|
|
313
|
+
// endpoint management lives in the desktop Webhooks page.
|
|
313
314
|
meta: setup.webhook?.enabled === false ? 'Off' : 'On',
|
|
314
315
|
description: setup.webhook?.publicUrl
|
|
315
316
|
? setup.webhook.publicUrl
|
|
316
317
|
: 'Mixdog relay tunnel — URL assigned on first run',
|
|
317
|
-
_action:
|
|
318
|
+
_action: null,
|
|
318
319
|
},
|
|
319
320
|
];
|
|
320
321
|
|
|
@@ -361,12 +362,6 @@ export function createChannelPickers({
|
|
|
361
362
|
});
|
|
362
363
|
return;
|
|
363
364
|
}
|
|
364
|
-
if (item._action === 'webhook-endpoint') {
|
|
365
|
-
void openChannelSetupPicker('webhook-endpoint', {
|
|
366
|
-
...(options.onboarding ? { onboarding: true, confirmBar: options.confirmBar || null } : {}),
|
|
367
|
-
returnTo: () => reopenRoot({ highlightValue: 'webhook-endpoint' }),
|
|
368
|
-
});
|
|
369
|
-
}
|
|
370
365
|
} catch (e) {
|
|
371
366
|
store.pushNotice(`channels update failed: ${e?.message || e}`, 'error');
|
|
372
367
|
}
|
|
@@ -27,7 +27,6 @@ export function createRoutePickers({
|
|
|
27
27
|
setContextPanel,
|
|
28
28
|
closeUsagePanel,
|
|
29
29
|
clean,
|
|
30
|
-
copyToClipboard,
|
|
31
30
|
routeLabel,
|
|
32
31
|
agentModelParts,
|
|
33
32
|
agentModelProfile,
|
|
@@ -158,28 +157,134 @@ export function createRoutePickers({
|
|
|
158
157
|
closeUsagePanel();
|
|
159
158
|
setPicker({
|
|
160
159
|
title: 'Workflow',
|
|
161
|
-
description: '
|
|
162
|
-
help: returnTo ? '↑/↓ Select · Enter
|
|
160
|
+
description: 'Active workflow and its agents.',
|
|
161
|
+
help: returnTo ? '↑/↓ Select · Enter Open · Esc Settings' : '↑/↓ Select · Enter Open · Esc Back',
|
|
163
162
|
labelWidth: 18,
|
|
163
|
+
initialIndex: Math.max(0, items.findIndex((item) => item.value === (options.initialWorkflowId || ''))),
|
|
164
164
|
items,
|
|
165
165
|
onSelect: (_value, item) => {
|
|
166
166
|
const workflow = item?._workflow;
|
|
167
167
|
if (!workflow) return;
|
|
168
|
+
// Depth mirrors the system model: a workflow pack owns the agent set
|
|
169
|
+
// it delegates to, so Enter opens the pack (activate + its agents)
|
|
170
|
+
// instead of switching immediately.
|
|
171
|
+
openWorkflowDetailPicker(workflow.id, { returnTo });
|
|
172
|
+
},
|
|
173
|
+
onCancel: () => {
|
|
168
174
|
setPicker(null);
|
|
169
|
-
|
|
170
|
-
|
|
175
|
+
if (returnTo) returnTo();
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// Workflow detail: activation row plus the agents this pack delegates to
|
|
181
|
+
// (route editing inline). Packs without an `agents:` key delegate to all
|
|
182
|
+
// fixed agents; a configured-but-empty list means Lead works alone.
|
|
183
|
+
const openWorkflowDetailPicker = (workflowId, options = {}) => {
|
|
184
|
+
const returnTo = typeof options.returnTo === 'function' ? options.returnTo : null;
|
|
185
|
+
let workflows = [];
|
|
186
|
+
try {
|
|
187
|
+
workflows = store.listWorkflows?.() || [];
|
|
188
|
+
} catch (e) {
|
|
189
|
+
store.pushNotice(`could not list workflows: ${e?.message || e}`, 'error');
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const workflow = workflows.find((item) => item.id === workflowId);
|
|
193
|
+
if (!workflow) {
|
|
194
|
+
store.pushNotice(`workflow "${workflowId}" not found`, 'warn');
|
|
195
|
+
openWorkflowPicker({ returnTo });
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
let agents = [];
|
|
199
|
+
try {
|
|
200
|
+
agents = store.listAgents?.() || [];
|
|
201
|
+
} catch (e) {
|
|
202
|
+
store.pushNotice(`could not list agents: ${e?.message || e}`, 'error');
|
|
203
|
+
agents = [];
|
|
204
|
+
}
|
|
205
|
+
const packAgents = workflow.agentsConfigured === true
|
|
206
|
+
? (workflow.agents || [])
|
|
207
|
+
.map((id) => agents.find((agent) => agent.id === id) || { id, label: id, route: null, description: '' })
|
|
208
|
+
: agents.filter((agent) => agent.custom !== true);
|
|
209
|
+
const routeOverrides = options.routeOverrides && typeof options.routeOverrides === 'object' ? options.routeOverrides : {};
|
|
210
|
+
const items = [
|
|
211
|
+
{
|
|
212
|
+
value: '__activate',
|
|
213
|
+
label: workflow.active ? 'Active' : 'Activate',
|
|
214
|
+
marker: workflow.active ? '✓' : '',
|
|
215
|
+
markerColor: theme.success,
|
|
216
|
+
description: workflow.active
|
|
217
|
+
? 'This workflow is active.'
|
|
218
|
+
: 'Make this the active workflow.',
|
|
219
|
+
},
|
|
220
|
+
...(packAgents.length
|
|
221
|
+
? packAgents.map((agent) => ({
|
|
222
|
+
value: agent.id,
|
|
223
|
+
label: agent.label,
|
|
224
|
+
metaParts: agentModelParts(routeOverrides[agent.id] || agent.route),
|
|
225
|
+
description: agent.description || agent.definition?.description || '',
|
|
226
|
+
_agent: agent,
|
|
227
|
+
}))
|
|
228
|
+
: [{
|
|
229
|
+
value: '__lead-only',
|
|
230
|
+
label: 'Lead only',
|
|
231
|
+
description: 'This workflow delegates to no agents.',
|
|
232
|
+
}]),
|
|
233
|
+
];
|
|
234
|
+
setPicker({
|
|
235
|
+
title: workflow.name,
|
|
236
|
+
description: workflow.description || 'Workflow activation and agents.',
|
|
237
|
+
help: '↑/↓ Select · Enter Choose · Esc Back',
|
|
238
|
+
indexMode: 'always',
|
|
239
|
+
labelWidth: 18,
|
|
240
|
+
metaWidth: 33,
|
|
241
|
+
initialIndex: Math.max(0, items.findIndex((item) => item.value === (options.initialValue || '__activate'))),
|
|
242
|
+
items,
|
|
243
|
+
onSelect: (_value, item) => {
|
|
244
|
+
if (item?.value === '__activate') {
|
|
245
|
+
if (workflow.active) {
|
|
246
|
+
store.pushNotice(`${workflow.name} is already the active workflow`, 'info');
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
setPicker(null);
|
|
250
|
+
void store.setWorkflow?.(workflow.id)
|
|
251
|
+
.then((result) => {
|
|
252
|
+
if (!result) {
|
|
253
|
+
store.pushNotice('Workflow switch is already running.', 'warn');
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
store.pushNotice(workflowSwitchNotice(result), 'info');
|
|
257
|
+
openWorkflowPicker({ returnTo, initialWorkflowId: workflow.id });
|
|
258
|
+
})
|
|
259
|
+
.catch((e) => store.pushNotice(`Couldn’t switch workflow: ${e?.message || e}`, 'error'));
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const agent = item?._agent;
|
|
263
|
+
if (!agent) return;
|
|
264
|
+
void openModelPicker({
|
|
265
|
+
title: `${agent.label} Model`,
|
|
266
|
+
providerDescription: 'Choose a provider for this agent.',
|
|
267
|
+
currentRoute: agent.route || null,
|
|
268
|
+
returnTo: () => openWorkflowDetailPicker(workflow.id, { returnTo, initialValue: agent.id }),
|
|
269
|
+
onImmediateSelect: (routeInput) => {
|
|
270
|
+
openWorkflowDetailPicker(workflow.id, {
|
|
271
|
+
returnTo,
|
|
272
|
+
routeOverrides: { [agent.id]: routeInput },
|
|
273
|
+
initialValue: agent.id,
|
|
274
|
+
});
|
|
275
|
+
},
|
|
276
|
+
onSelectRoute: async (routeInput) => {
|
|
277
|
+
const result = await store.setAgentRoute?.(agent.id, routeInput);
|
|
171
278
|
if (!result) {
|
|
172
|
-
store.pushNotice('
|
|
279
|
+
store.pushNotice('Agent model save is already running.', 'warn');
|
|
173
280
|
return;
|
|
174
281
|
}
|
|
175
|
-
store.pushNotice(
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
.catch((e) => store.pushNotice(`Couldn’t switch workflow: ${e?.message || e}`, 'error'));
|
|
282
|
+
store.pushNotice(`${agent.label} model set to ${agentModelProfile(result)}`, 'info');
|
|
283
|
+
},
|
|
284
|
+
});
|
|
179
285
|
},
|
|
180
286
|
onCancel: () => {
|
|
181
|
-
|
|
182
|
-
if (returnTo) returnTo();
|
|
287
|
+
openWorkflowPicker({ returnTo, initialWorkflowId: workflow.id });
|
|
183
288
|
},
|
|
184
289
|
});
|
|
185
290
|
};
|
|
@@ -274,150 +379,10 @@ export function createRoutePickers({
|
|
|
274
379
|
});
|
|
275
380
|
};
|
|
276
381
|
|
|
277
|
-
const openBridgePicker = () => {
|
|
278
|
-
setProviderPrompt(null);
|
|
279
|
-
setChannelPrompt(null);
|
|
280
|
-
setHookPrompt(null);
|
|
281
|
-
setSettingsPrompt(null);
|
|
282
|
-
setPicker({
|
|
283
|
-
title: 'Agent Tasks',
|
|
284
|
-
description: 'Inspect or clean up background agent tasks.',
|
|
285
|
-
items: [
|
|
286
|
-
{
|
|
287
|
-
value: 'list',
|
|
288
|
-
label: 'List agents/tasks',
|
|
289
|
-
description: 'show active agents and async tasks',
|
|
290
|
-
_action: 'control',
|
|
291
|
-
_args: { type: 'list' },
|
|
292
|
-
},
|
|
293
|
-
{
|
|
294
|
-
value: 'cleanup',
|
|
295
|
-
label: 'Cleanup finished tasks',
|
|
296
|
-
description: 'remove completed agent task records',
|
|
297
|
-
_action: 'control',
|
|
298
|
-
_args: { type: 'cleanup' },
|
|
299
|
-
},
|
|
300
|
-
],
|
|
301
|
-
onSelect: (_value, item) => {
|
|
302
|
-
setPicker(null);
|
|
303
|
-
if (item._action === 'control') {
|
|
304
|
-
void store.agentControl?.(item._args)
|
|
305
|
-
.catch((e) => store.pushNotice(`agent failed: ${e?.message || e}`, 'error'));
|
|
306
|
-
}
|
|
307
|
-
},
|
|
308
|
-
onCancel: () => {
|
|
309
|
-
setPicker(null);
|
|
310
|
-
},
|
|
311
|
-
});
|
|
312
|
-
};
|
|
313
|
-
|
|
314
|
-
const openToolsPicker = (query = '') => {
|
|
315
|
-
let status;
|
|
316
|
-
try {
|
|
317
|
-
status = store.toolsStatus?.(query) || { tools: [] };
|
|
318
|
-
} catch (e) {
|
|
319
|
-
store.pushNotice(`tools status failed: ${e?.message || e}`, 'error');
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
const tools = status.tools || [];
|
|
323
|
-
const items = [
|
|
324
|
-
{
|
|
325
|
-
value: 'summary',
|
|
326
|
-
label: 'Tool surface',
|
|
327
|
-
description: `${status.activeCount || 0}/${status.count || 0} active · mode ${status.mode || state.toolMode}`,
|
|
328
|
-
_action: 'summary',
|
|
329
|
-
},
|
|
330
|
-
...(tools.length ? tools.map((tool) => ({
|
|
331
|
-
value: tool.name,
|
|
332
|
-
label: tool.name,
|
|
333
|
-
marker: tool.active ? '●' : '○',
|
|
334
|
-
markerColor: tool.active ? theme.success : theme.inactive,
|
|
335
|
-
description: `${tool.kind || 'tool'} · usage ${tool.usage || 0}${tool.description ? ` · ${tool.description}` : ''}`,
|
|
336
|
-
_action: tool.active ? 'tool' : 'enable',
|
|
337
|
-
_tool: tool,
|
|
338
|
-
})) : [{
|
|
339
|
-
value: 'empty',
|
|
340
|
-
label: 'No tools',
|
|
341
|
-
description: query ? `no matches for "${query}"` : 'tool catalog is empty',
|
|
342
|
-
_action: 'noop',
|
|
343
|
-
}]),
|
|
344
|
-
];
|
|
345
|
-
setProviderPrompt(null);
|
|
346
|
-
setChannelPrompt(null);
|
|
347
|
-
setHookPrompt(null);
|
|
348
|
-
setSettingsPrompt(null);
|
|
349
|
-
setPicker({
|
|
350
|
-
title: query ? `Tools · ${query}` : 'Tools',
|
|
351
|
-
description: 'Browse active and deferred tools.',
|
|
352
|
-
items,
|
|
353
|
-
onSelect: (_value, item) => {
|
|
354
|
-
setPicker(null);
|
|
355
|
-
if (item._action === 'summary') {
|
|
356
|
-
store.pushNotice([
|
|
357
|
-
`mode: ${status.mode || state.toolMode}`,
|
|
358
|
-
`active: ${status.activeCount || 0}/${status.count || 0}`,
|
|
359
|
-
`active tools: ${(status.activeTools || []).join(', ') || '(none)'}`,
|
|
360
|
-
].join('\n'), 'info');
|
|
361
|
-
return;
|
|
362
|
-
}
|
|
363
|
-
if (item._action === 'enable') {
|
|
364
|
-
store.selectTools?.([item._tool.name]);
|
|
365
|
-
void openToolsPicker(query);
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
368
|
-
if (item._action === 'tool') {
|
|
369
|
-
const tool = item._tool;
|
|
370
|
-
setPicker({
|
|
371
|
-
title: `Tool · ${tool.name}`,
|
|
372
|
-
description: 'Tool details and quick actions.',
|
|
373
|
-
items: [
|
|
374
|
-
{
|
|
375
|
-
value: 'info',
|
|
376
|
-
label: 'Tool info',
|
|
377
|
-
description: `${tool.kind || 'tool'} · ${tool.active ? 'active' : 'deferred'}`,
|
|
378
|
-
_action: 'info',
|
|
379
|
-
},
|
|
380
|
-
{
|
|
381
|
-
value: 'copy-name',
|
|
382
|
-
label: 'Copy name',
|
|
383
|
-
description: tool.name,
|
|
384
|
-
_action: 'copy-name',
|
|
385
|
-
},
|
|
386
|
-
],
|
|
387
|
-
onSelect: (_detailValue, detail) => {
|
|
388
|
-
setPicker(null);
|
|
389
|
-
if (detail._action === 'info') {
|
|
390
|
-
store.pushNotice([
|
|
391
|
-
tool.name,
|
|
392
|
-
`kind: ${tool.kind || 'tool'}`,
|
|
393
|
-
`state: ${tool.active ? 'active' : 'deferred'}`,
|
|
394
|
-
`usage: ${tool.usage || 0}`,
|
|
395
|
-
tool.description || '',
|
|
396
|
-
].filter(Boolean).join('\n'), 'info');
|
|
397
|
-
return;
|
|
398
|
-
}
|
|
399
|
-
if (detail._action === 'copy-name') {
|
|
400
|
-
void copyToClipboard(tool.name)
|
|
401
|
-
.then(() => store.pushNotice(`copied tool name: ${tool.name}`, 'plain'))
|
|
402
|
-
.catch((e) => store.pushNotice(`copy failed: ${e?.message || e}`, 'error'));
|
|
403
|
-
}
|
|
404
|
-
},
|
|
405
|
-
onCancel: () => openToolsPicker(query),
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
},
|
|
409
|
-
onCancel: () => {
|
|
410
|
-
setPicker(null);
|
|
411
|
-
},
|
|
412
|
-
});
|
|
413
|
-
};
|
|
414
|
-
|
|
415
382
|
return {
|
|
416
383
|
openSearchPicker,
|
|
417
384
|
openAgentsPicker,
|
|
418
385
|
openWorkflowPicker,
|
|
419
386
|
openOutputStylePicker,
|
|
420
|
-
openBridgePicker,
|
|
421
|
-
openToolsPicker,
|
|
422
387
|
};
|
|
423
388
|
}
|