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.
Files changed (91) hide show
  1. package/README.md +7 -1
  2. package/package.json +3 -2
  3. package/scripts/channel-daemon-stub.mjs +2 -1
  4. package/src/defaults/agents.json +12 -0
  5. package/src/defaults/skills/setup/SKILL.md +44 -12
  6. package/src/help.mjs +40 -9
  7. package/src/lib/keychain-cjs.cjs +11 -1
  8. package/src/rules/agent/43-title-agent.md +22 -0
  9. package/src/rules/shared/01-tool.md +3 -3
  10. package/src/runtime/agent/orchestrator/mcp/client.mjs +24 -0
  11. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +84 -3
  12. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +2 -2
  13. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +30 -3
  14. package/src/runtime/agent/orchestrator/session/context-utils.mjs +73 -45
  15. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +15 -18
  16. package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +1 -1
  17. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +38 -4
  18. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +13 -0
  19. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +183 -41
  20. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +31 -12
  21. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +12 -2
  22. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +14 -0
  23. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +170 -0
  24. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +45 -6
  25. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +26 -0
  26. package/src/runtime/agent/orchestrator/session/token-bpe.mjs +42 -0
  27. package/src/runtime/agent/orchestrator/session/token-native.mjs +186 -0
  28. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +15 -5
  29. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +20 -0
  30. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +1 -1
  31. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +24 -0
  32. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +44 -73
  33. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
  34. package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +286 -0
  35. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  36. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +152 -18
  37. package/src/runtime/agent/orchestrator/tools/token-binary-fetcher.mjs +201 -0
  38. package/src/runtime/agent/orchestrator/tools/token-manifest.json +5 -0
  39. package/src/runtime/channels/lib/owned-runtime.mjs +6 -8
  40. package/src/runtime/channels/lib/parent-bridge.mjs +27 -15
  41. package/src/runtime/channels/lib/webhook/relay-tunnel.mjs +3 -3
  42. package/src/runtime/channels/lib/worker-ipc.mjs +17 -4
  43. package/src/runtime/media/adapters/gemini-video.mjs +1 -1
  44. package/src/runtime/media/renditions.mjs +1 -1
  45. package/src/runtime/media/store.mjs +1 -1
  46. package/src/runtime/memory/index.mjs +14 -5
  47. package/src/runtime/memory/lib/agent-ipc.mjs +11 -12
  48. package/src/runtime/shared/atomic-file.mjs +76 -38
  49. package/src/runtime/shared/automation-attachments.mjs +3 -3
  50. package/src/runtime/shared/child-guardian.mjs +15 -5
  51. package/src/runtime/shared/child-spawn-gate.mjs +6 -12
  52. package/src/runtime/shared/err-text.mjs +1 -0
  53. package/src/runtime/shared/resource-admission.mjs +7 -3
  54. package/src/runtime/shared/safe-ipc-send.mjs +34 -0
  55. package/src/runtime/shared/turn-snapshot.mjs +0 -3
  56. package/src/session-runtime/lifecycle-api.mjs +43 -12
  57. package/src/session-runtime/prewarm.mjs +35 -0
  58. package/src/session-runtime/remote-control.mjs +8 -47
  59. package/src/session-runtime/remote-transcript.mjs +3 -7
  60. package/src/session-runtime/route-preparation.mjs +24 -0
  61. package/src/session-runtime/runtime-core.mjs +42 -86
  62. package/src/session-runtime/session-turn-api.mjs +9 -2
  63. package/src/session-runtime/workflow-agents-api.mjs +3 -0
  64. package/src/standalone/agent-shard/shard-child.mjs +300 -0
  65. package/src/standalone/agent-shard/shard-pool.mjs +442 -0
  66. package/src/standalone/agent-tool.mjs +222 -0
  67. package/src/standalone/channel-daemon-transport.mjs +126 -185
  68. package/src/standalone/channel-daemon.mjs +11 -11
  69. package/src/standalone/channel-worker.mjs +21 -19
  70. package/src/standalone/memory-runtime-proxy.mjs +98 -23
  71. package/src/tui/App.jsx +16 -9
  72. package/src/tui/app/channel-pickers.mjs +3 -8
  73. package/src/tui/app/route-pickers.mjs +117 -152
  74. package/src/tui/app/settings-picker.mjs +90 -89
  75. package/src/tui/app/slash-commands.mjs +7 -7
  76. package/src/tui/app/slash-dispatch.mjs +23 -58
  77. package/src/tui/app/theme-effort-pickers.mjs +3 -1
  78. package/src/tui/components/StatusLine.jsx +1 -1
  79. package/src/tui/components/tool-execution/ResultBody.jsx +1 -1
  80. package/src/tui/dist/index.mjs +358 -380
  81. package/src/tui/engine/live-share.mjs +2 -2
  82. package/src/tui/engine/session-api-ext.mjs +24 -9
  83. package/src/tui/engine/session-flow.mjs +20 -10
  84. package/src/tui/engine/turn.mjs +28 -16
  85. package/src/tui/engine.mjs +24 -2
  86. package/src/tui/figures.mjs +0 -1
  87. package/src/ui/statusline-format.mjs +0 -1
  88. package/src/hooks/lib/permission-evaluator.cjs +0 -24
  89. package/src/lib/config-cjs.cjs +0 -61
  90. package/src/runtime/shared/launcher-control.mjs +0 -258
  91. 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 requestJson({ port, method = 'GET', path = '/', body = null, timeoutMs = 10_000, headers = {} }) {
107
- return new Promise((resolvePromise, reject) => {
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
- const req = http.request({
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
- resolvePromise(parsed ?? { raw: data });
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
- return await withTransientMemoryRpcRetry(async () => {
503
- await start();
504
- let port = portCache || await findLivePort({ allowStarting: true });
505
- if (!port) throw new Error('memory runtime is not available');
506
- // ensureClientRegistered may respawn onto a fresh daemon/port; target the
507
- // port it hands back so the RPC and registration always hit the same one.
508
- port = await ensureClientRegistered(port);
509
- if (!port) throw new Error('memory runtime is not available');
510
- return await requestJson({
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/tool',
514
- body: { name, arguments: args || {} },
515
- timeoutMs: Math.max(1000, Number(process.env.MIXDOG_MEMORY_TOOL_TIMEOUT_MS) || 180_000),
516
- headers: { 'X-Mixdog-Call-Id': callId },
517
- });
518
- }, { readOnlyRpc });
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: 'webhook-endpoint',
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: 'Select active workflow.',
162
- help: returnTo ? '↑/↓ Select · Enter Choose · Esc Settings' : '↑/↓ Select · Enter Choose · Esc Back',
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
- void store.setWorkflow?.(workflow.id)
170
- .then((result) => {
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('Workflow switch is already running.', 'warn');
279
+ store.pushNotice('Agent model save is already running.', 'warn');
173
280
  return;
174
281
  }
175
- store.pushNotice(workflowSwitchNotice(result), 'info');
176
- if (returnTo) returnTo();
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
- setPicker(null);
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
  }