mixdog 0.9.151 → 0.9.152

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 (175) hide show
  1. package/README.md +110 -68
  2. package/package.json +3 -2
  3. package/scripts/tool-stress.mjs +0 -2
  4. package/src/defaults/skills/setup/SKILL.md +18 -27
  5. package/src/headless-exec.mjs +3 -10
  6. package/src/headless-exec.test.mjs +2 -7
  7. package/src/runtime/agent/orchestrator/config.mjs +6 -66
  8. package/src/runtime/agent/orchestrator/context/collect-skills.test.mjs +153 -0
  9. package/src/runtime/agent/orchestrator/context/collect.mjs +49 -45
  10. package/src/runtime/agent/orchestrator/mcp/client.mjs +242 -15
  11. package/src/runtime/agent/orchestrator/mcp/features.test.mjs +66 -0
  12. package/src/runtime/agent/orchestrator/mcp/security.test.mjs +36 -20
  13. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +54 -33
  14. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -0
  15. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +7 -0
  16. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +7 -0
  17. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +3 -8
  18. package/src/runtime/agent/orchestrator/providers/gemini.mjs +7 -0
  19. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +10 -2
  20. package/src/runtime/agent/orchestrator/providers/lib/provider-replay.mjs +50 -0
  21. package/src/runtime/agent/orchestrator/providers/lib/sse-framing.mjs +93 -0
  22. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +3 -3
  23. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +8 -0
  24. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +7 -0
  25. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +25 -2
  26. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +35 -4
  27. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -0
  28. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +0 -2
  29. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +13 -0
  30. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +4 -7
  31. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +9 -0
  32. package/src/runtime/agent/orchestrator/providers/provider-replay.test.mjs +235 -0
  33. package/src/runtime/agent/orchestrator/providers/sse-framing.test.mjs +719 -0
  34. package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +399 -39
  35. package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +26 -0
  36. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +14 -4
  37. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +13 -0
  38. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +24 -1
  39. package/src/runtime/agent/orchestrator/session/compaction-read-reset.test.mjs +161 -0
  40. package/src/runtime/agent/orchestrator/session/context-utils.mjs +14 -4
  41. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +2 -2
  42. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +12 -1
  43. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +31 -18
  44. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -0
  45. package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +3 -0
  46. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +6 -1
  47. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +1 -3
  48. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +556 -0
  49. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.test.mjs +457 -0
  50. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +105 -186
  51. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +115 -0
  52. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +2 -0
  53. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +7 -0
  54. package/src/runtime/agent/orchestrator/session/store.mjs +23 -1
  55. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool-cwd-env.test.mjs +14 -0
  56. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +19 -10
  57. package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +30 -14
  58. package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +15 -4
  59. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +73 -15
  60. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +76 -2
  61. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +24 -683
  62. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +64 -72
  63. package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +2 -5
  64. package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.mjs +75 -0
  65. package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.test.mjs +24 -0
  66. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +12 -0
  67. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +4 -1
  68. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +24 -5
  69. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.test.mjs +44 -0
  70. package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +12 -2
  71. package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +35 -21
  72. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +28 -13
  73. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.test.mjs +54 -0
  74. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
  75. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  76. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +8 -0
  77. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
  78. package/src/runtime/browser-bridge/client.mjs +103 -0
  79. package/src/runtime/browser-bridge/tool-defs.mjs +42 -0
  80. package/src/runtime/computer-bridge/client.mjs +101 -0
  81. package/src/runtime/computer-bridge/tool-defs.mjs +53 -0
  82. package/src/runtime/media/store.mjs +18 -8
  83. package/src/runtime/media/store.test.mjs +14 -0
  84. package/src/runtime/memory/index.mjs +1 -1
  85. package/src/runtime/memory/lib/transcript-ingest.mjs +8 -6
  86. package/src/runtime/memory/lib/transcript-ingest.test.mjs +52 -0
  87. package/src/runtime/shared/child-spawn-gate.mjs +17 -0
  88. package/src/runtime/shared/child-spawn-remote.mjs +69 -7
  89. package/src/runtime/shared/child-spawn-remote.test.mjs +124 -0
  90. package/src/runtime/shared/pristine-execution-contract.json +2 -1
  91. package/src/runtime/shared/provider-api-key.mjs +1 -0
  92. package/src/runtime/shared/session-runtime-health.mjs +91 -0
  93. package/src/runtime/shared/session-runtime-health.test.mjs +53 -0
  94. package/src/runtime/shared/skill-document.mjs +86 -0
  95. package/src/runtime/shared/skill-document.test.mjs +81 -0
  96. package/src/runtime/shared/tool-surface.mjs +18 -0
  97. package/src/runtime/shared/turn-snapshot-store.mjs +1 -0
  98. package/src/runtime/shared/turn-snapshot.mjs +32 -9
  99. package/src/runtime/shared/user-cwd.mjs +33 -14
  100. package/src/runtime/shared/user-cwd.test.mjs +43 -0
  101. package/src/session-runtime/cwd-plugins.mjs +26 -26
  102. package/src/session-runtime/cwd-plugins.test.mjs +75 -0
  103. package/src/session-runtime/cwd-tool-routing.test.mjs +5 -0
  104. package/src/session-runtime/global-extensions.mjs +26 -0
  105. package/src/session-runtime/global-extensions.test.mjs +28 -0
  106. package/src/session-runtime/goal-runtime.mjs +721 -0
  107. package/src/session-runtime/goal-runtime.test.mjs +108 -0
  108. package/src/session-runtime/lifecycle-api.mjs +18 -4
  109. package/src/session-runtime/lifecycle-api.test.mjs +21 -0
  110. package/src/session-runtime/mcp-glue.mjs +49 -47
  111. package/src/session-runtime/plugin-mcp-standard.test.mjs +177 -0
  112. package/src/session-runtime/plugin-mcp.mjs +96 -22
  113. package/src/session-runtime/prewarm.mjs +1 -10
  114. package/src/session-runtime/resource-api-global.test.mjs +69 -0
  115. package/src/session-runtime/resource-api.mjs +173 -58
  116. package/src/session-runtime/runtime-core.mjs +148 -20
  117. package/src/session-runtime/runtime-tunables.mjs +6 -8
  118. package/src/session-runtime/session-turn-api.mjs +22 -11
  119. package/src/session-runtime/skills-api.mjs +79 -25
  120. package/src/session-runtime/skills-api.test.mjs +99 -0
  121. package/src/session-runtime/tool-catalog-schema.mjs +0 -5
  122. package/src/session-runtime/tool-catalog.mjs +4 -13
  123. package/src/session-runtime/tool-policy-surface.test.mjs +33 -11
  124. package/src/session-runtime/tool-surface.mjs +12 -0
  125. package/src/standalone/agent-dispatch-broker.mjs +5 -9
  126. package/src/standalone/agent-tool/notify.mjs +6 -8
  127. package/src/standalone/agent-tool/notify.test.mjs +37 -0
  128. package/src/standalone/daemon.mjs +31 -15
  129. package/src/standalone/hook-bus/config.mjs +1 -0
  130. package/src/standalone/plugin-admin.mjs +17 -0
  131. package/src/standalone/plugin-admin.test.mjs +7 -0
  132. package/src/standalone/provider-admin.mjs +1 -0
  133. package/src/standalone/session-protocol.mjs +5 -0
  134. package/src/standalone/session-runtime-agent-control-client.mjs +133 -0
  135. package/src/standalone/session-runtime-agent-control-client.test.mjs +71 -0
  136. package/src/standalone/session-runtime-dispatch-cancel.test.mjs +139 -0
  137. package/src/standalone/session-runtime-host-factory.mjs +17 -0
  138. package/src/standalone/session-runtime-host-factory.test.mjs +31 -0
  139. package/src/standalone/session-runtime-host-health.test.mjs +5 -0
  140. package/src/standalone/session-runtime-host.mjs +696 -58
  141. package/src/standalone/session-runtime-inline-host.mjs +351 -0
  142. package/src/standalone/session-runtime-inline-host.test.mjs +83 -0
  143. package/src/standalone/session-runtime-provider-cooldown.mjs +78 -0
  144. package/src/standalone/session-runtime-provider-cooldown.test.mjs +80 -0
  145. package/src/standalone/session-runtime-shard-host.test.mjs +569 -0
  146. package/src/standalone/session-runtime-shard-router.mjs +149 -0
  147. package/src/standalone/session-runtime-shard-router.test.mjs +101 -0
  148. package/src/standalone/session-runtime-worker.mjs +476 -18
  149. package/src/standalone/session-service.mjs +81 -6
  150. package/src/tui/app/app-view.jsx +1 -0
  151. package/src/tui/app/core-memory-picker.mjs +2 -0
  152. package/src/tui/app/extension-pickers.mjs +1 -1
  153. package/src/tui/app/maintenance-pickers.mjs +3 -3
  154. package/src/tui/app/model-options.mjs +2 -0
  155. package/src/tui/app/model-picker.mjs +14 -4
  156. package/src/tui/app/panel-handoff.test.mjs +311 -0
  157. package/src/tui/app/project-picker.mjs +1 -0
  158. package/src/tui/app/route-pickers.mjs +21 -6
  159. package/src/tui/app/settings-picker.mjs +26 -3
  160. package/src/tui/app/slash-commands.mjs +1 -0
  161. package/src/tui/app/slash-dispatch.mjs +73 -22
  162. package/src/tui/app/theme-effort-pickers.mjs +7 -2
  163. package/src/tui/components/Picker.jsx +10 -1
  164. package/src/tui/dist/index.mjs +163 -44
  165. package/src/tui/session/context-state.mjs +1 -0
  166. package/src/tui/session/goal-continuation.mjs +110 -0
  167. package/src/tui/session/goal-continuation.test.mjs +84 -0
  168. package/src/tui/session/queue-helpers.mjs +5 -2
  169. package/src/tui/session/session-action-surface.test.mjs +108 -0
  170. package/src/tui/session/session-api-ext.mjs +39 -8
  171. package/src/tui/session/session-api.mjs +58 -0
  172. package/src/tui/session/session-flow.mjs +6 -0
  173. package/src/tui/session/turn.mjs +1 -0
  174. package/src/tui/session-local.mjs +15 -1
  175. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +0 -301
@@ -188,6 +188,9 @@ export function createRoutePickers({
188
188
 
189
189
  const openWorkflowPicker = async (options = {}) => {
190
190
  const returnTo = typeof options.returnTo === 'function' ? options.returnTo : null;
191
+ const handoffPanel = options.handoffPanel && typeof options.handoffPanel === 'object'
192
+ ? options.handoffPanel
193
+ : null;
191
194
  const own = surface.claim();
192
195
  let workflows = [];
193
196
  try {
@@ -225,21 +228,26 @@ export function createRoutePickers({
225
228
  // Clear-and-continue: the flow keeps the surface it just emptied, so
226
229
  // the post-ack hop below is bound to THIS keypress. An Esc before the
227
230
  // switch acks cancels the hop instead of re-opening Settings.
228
- own.paint(null);
231
+ own.paint(handoffPanel);
229
232
  const returnAfterSwitch = own.defer(() => { if (returnTo) returnTo(); });
230
233
  void store.setWorkflow?.(workflow.id)
231
234
  .then((result) => {
232
235
  if (!result) {
233
236
  store.pushNotice('Workflow switch is already running.', 'warn');
237
+ if (handoffPanel) returnAfterSwitch();
234
238
  return;
235
239
  }
236
240
  store.pushNotice(workflowSwitchNotice(result), 'info');
237
241
  returnAfterSwitch();
238
242
  })
239
- .catch((e) => store.pushNotice(`Couldn’t switch workflow: ${e?.message || e}`, 'error'));
243
+ .catch((e) => {
244
+ store.pushNotice(`Couldn’t switch workflow: ${e?.message || e}`, 'error');
245
+ if (handoffPanel) returnAfterSwitch();
246
+ });
240
247
  },
241
248
  onCancel: () => {
242
- own.close();
249
+ if (handoffPanel) own.paint(handoffPanel);
250
+ else own.close();
243
251
  if (returnTo) returnTo();
244
252
  },
245
253
  });
@@ -247,6 +255,9 @@ export function createRoutePickers({
247
255
 
248
256
  const openOutputStylePicker = async (options = {}) => {
249
257
  const returnTo = typeof options.returnTo === 'function' ? options.returnTo : null;
258
+ const handoffPanel = options.handoffPanel && typeof options.handoffPanel === 'object'
259
+ ? options.handoffPanel
260
+ : null;
250
261
  // Onboarding mode: Enter (row select) and ConfirmBar Next must both persist
251
262
  // the chosen style, then advance. `onboarding.onAdvance/onBack` drive the
252
263
  // wizard; the confirm bar is built here so both paths share `saveStyle`.
@@ -284,7 +295,7 @@ export function createRoutePickers({
284
295
  // Onboarding advance: keep the current picker visible during the async
285
296
  // style switch so the screen never flashes empty between steps; the next
286
297
  // step (or finishOnboarding) replaces/clears the picker itself.
287
- if (!(advance && onboarding)) own.paint(null);
298
+ if (!(advance && onboarding)) own.paint(handoffPanel);
288
299
  // Post-ack delegation (next onboarding step, or back to the caller):
289
300
  // bound after this keypress's own navigation so an Esc while the switch
290
301
  // is in flight cannot paint a panel over the new surface.
@@ -301,7 +312,10 @@ export function createRoutePickers({
301
312
  }
302
313
  advanceAfterSave();
303
314
  })
304
- .catch((e) => store.pushNotice(`Couldn’t switch output style: ${e?.message || e}`, 'error'));
315
+ .catch((e) => {
316
+ store.pushNotice(`Couldn’t switch output style: ${e?.message || e}`, 'error');
317
+ if (handoffPanel) advanceAfterSave();
318
+ });
305
319
  };
306
320
  own.paint({
307
321
  title: 'Output Style',
@@ -334,7 +348,8 @@ export function createRoutePickers({
334
348
  saveStyle(style.id, { advance: Boolean(onboarding) });
335
349
  },
336
350
  onCancel: () => {
337
- own.close();
351
+ if (handoffPanel) own.paint(handoffPanel);
352
+ else own.close();
338
353
  if (onboarding) onboarding.onCancel?.();
339
354
  else if (returnTo) returnTo();
340
355
  },
@@ -41,6 +41,15 @@ export function createSettingsPicker({
41
41
  openMemoryCorePicker,
42
42
  openUpdatePicker,
43
43
  }) {
44
+ const settingsHandoffPanel = (description = 'Loading settings...') => ({
45
+ title: 'Settings',
46
+ description,
47
+ help: 'Esc Close',
48
+ indexMode: 'never',
49
+ pickerKey: 'settings-handoff',
50
+ loading: true,
51
+ items: [],
52
+ });
44
53
  // Build generation guard. Every open/refresh takes a ticket and Esc bumps it,
45
54
  // so a slow snapshot from a superseded (or already closed) build can never
46
55
  // re-open Settings. The ref is owned by App so it survives this per-render
@@ -60,6 +69,10 @@ export function createSettingsPicker({
60
69
  // caught here instead.
61
70
  const own = surface.claim();
62
71
  const light = opts.light === true;
72
+ // A full Settings open usually follows another option panel. Paint the
73
+ // destination synchronously before the daemon snapshot so the empty-chat
74
+ // welcome logo cannot appear between the old panel and Settings.
75
+ if (!light && !own.paint(settingsHandoffPanel())) return;
63
76
  const overrides = opts.overrides || null;
64
77
  const heavyCache = light ? settingsHeavyCacheRef.current : null;
65
78
  // ONE round-trip for the whole panel: on a daemon-backed store each getter
@@ -448,14 +461,24 @@ export function createSettingsPicker({
448
461
  else if (item._action === 'memory-enabled') toggleMemory();
449
462
  else if (item._action === 'memory-cycles') toggleMemoryCycles();
450
463
  else if (item._action === 'voice') applyVoice();
451
- else if (item._action === 'output-style') openOutputStylePicker({ returnTo: openSettingsPicker });
452
- else if (item._action === 'theme') openThemePicker({ returnTo: openSettingsPicker });
453
- else if (item._action === 'workflow') openWorkflowPicker({ returnTo: openSettingsPicker });
464
+ else if (item._action === 'output-style') openOutputStylePicker({
465
+ returnTo: openSettingsPicker,
466
+ handoffPanel: settingsHandoffPanel('Applying output style...'),
467
+ });
468
+ else if (item._action === 'theme') openThemePicker({
469
+ returnTo: openSettingsPicker,
470
+ handoffPanel: settingsHandoffPanel('Applying theme...'),
471
+ });
472
+ else if (item._action === 'workflow') openWorkflowPicker({
473
+ returnTo: openSettingsPicker,
474
+ handoffPanel: settingsHandoffPanel('Switching workflow...'),
475
+ });
454
476
  else if (item._action === 'model') openModelPicker({
455
477
  returnTo: openSettingsPicker,
456
478
  returnLabel: 'Settings',
457
479
  returnOnNestedCancel: true,
458
480
  onAfterSelect: openSettingsPicker,
481
+ handoffPanel: settingsHandoffPanel('Switching model...'),
459
482
  });
460
483
  else if (item._action === 'websearch') openWebSearchPicker({
461
484
  returnTo: openSettingsPicker,
@@ -7,6 +7,7 @@ export const SLASH_COMMANDS = [
7
7
  { name: 'clear', usage: '/clear', aliases: ['new'], aliasUsage: ['new'], description: 'Start a fresh chat' },
8
8
  { name: 'project', usage: '/project', aliases: ['projects'], aliasUsage: ['projects'], showAliasUsage: false, params: '[path]', description: 'Switch working directory (project)' },
9
9
  { name: 'compact', usage: '/compact', description: 'Compact older conversation context' },
10
+ { name: 'goal', usage: '/goal', params: '[objective --time 1h|status|pause|resume|edit|complete|clear]', description: 'Run and manage a durable session Goal' },
10
11
  { name: 'autoclear', usage: '/autoclear', params: '[on|off|duration]', description: 'Reduce cache-miss cost after long idle gaps' },
11
12
  { name: 'resume', usage: '/resume', params: '[id]', description: 'Resume a saved chat' },
12
13
  { name: 'context', usage: '/context', description: 'Show current context surface' },
@@ -55,6 +55,44 @@ export function createSlashDispatch({
55
55
  runDoctor,
56
56
  requestExit,
57
57
  }) {
58
+ const openSlashPanel = (command, title, open) => {
59
+ const own = surface.claim();
60
+ if (!own.paint({
61
+ _kind: `slash-loading:${command}`,
62
+ title,
63
+ description: `Loading ${title.toLowerCase()}...`,
64
+ help: 'Esc Close',
65
+ indexMode: 'never',
66
+ pickerKey: `slash-loading:${command}`,
67
+ loading: true,
68
+ items: [],
69
+ onSelect: () => {},
70
+ onCancel: () => own.close(),
71
+ })) return;
72
+ const finishUnclaimedLoading = () => {
73
+ // A real picker/context/usage surface supersedes this loading identity.
74
+ // If the opener completed without painting (empty result or failure),
75
+ // remove only the still-owned placeholder and restore the normal prompt.
76
+ if (own.owns()) own.close();
77
+ };
78
+ try {
79
+ const opening = open();
80
+ if (opening && typeof opening.then === 'function') {
81
+ void Promise.resolve(opening).then(
82
+ finishUnclaimedLoading,
83
+ (error) => {
84
+ finishUnclaimedLoading();
85
+ store.pushNotice(`${title} panel failed: ${error?.message || error}`, 'error');
86
+ },
87
+ );
88
+ } else {
89
+ finishUnclaimedLoading();
90
+ }
91
+ } catch (error) {
92
+ finishUnclaimedLoading();
93
+ store.pushNotice(`${title} panel failed: ${error?.message || error}`, 'error');
94
+ }
95
+ };
58
96
  const runSlashCommand = (cmd, arg = '') => {
59
97
  const rawName = String(cmd || '').toLowerCase();
60
98
  cmd = normalizeSlashCommandName(cmd);
@@ -86,12 +124,12 @@ export function createSlashDispatch({
86
124
  return true;
87
125
  case 'model':
88
126
  if (!arg) {
89
- openModelPicker();
127
+ openSlashPanel('model', 'Model', () => openModelPicker());
90
128
  return true;
91
129
  }
92
130
  if (arg.trim().toLowerCase() === 'refresh') {
93
131
  // Explicit catalog reload: force a fresh remote provider list.
94
- openModelPicker({ refreshModels: true });
132
+ openSlashPanel('model', 'Model', () => openModelPicker({ refreshModels: true }));
95
133
  return true;
96
134
  }
97
135
  void store.setModel(arg)
@@ -104,14 +142,16 @@ export function createSlashDispatch({
104
142
  // in-flight turn, and the same picker is already reachable mid-turn via
105
143
  // /settings, so blocking it here was inconsistent.
106
144
  if (arg) store.pushNotice('/websearch sets the web-search provider/model; the web_search tool uses that model when called.', 'warn');
107
- openWebSearchPicker();
145
+ openSlashPanel('websearch', 'Web Search Model', () => openWebSearchPicker());
108
146
  return true;
109
147
  case 'agents':
110
- openAgentsPicker(arg.trim().toLowerCase() === 'refresh' ? { refreshModels: true } : {});
148
+ openSlashPanel('agents', 'Agents', () => openAgentsPicker(
149
+ arg.trim().toLowerCase() === 'refresh' ? { refreshModels: true } : {},
150
+ ));
111
151
  return true;
112
152
  case 'workflow':
113
153
  if (!arg) {
114
- openWorkflowPicker();
154
+ openSlashPanel('workflow', 'Workflow', () => openWorkflowPicker());
115
155
  return true;
116
156
  }
117
157
  void store.setWorkflow?.(arg.trim())
@@ -132,7 +172,7 @@ export function createSlashDispatch({
132
172
  const value = arg.trim();
133
173
  const lower = value.toLowerCase();
134
174
  if (!value) {
135
- openOutputStylePicker();
175
+ openSlashPanel('outputstyle', 'Output Style', () => openOutputStylePicker());
136
176
  return true;
137
177
  }
138
178
  if (lower === 'status' || lower === 'current' || lower === 'show') {
@@ -159,7 +199,7 @@ export function createSlashDispatch({
159
199
  const value = arg.trim();
160
200
  const lower = value.toLowerCase();
161
201
  if (!value) {
162
- openThemePicker();
202
+ openSlashPanel('theme', 'Theme', () => openThemePicker());
163
203
  return true;
164
204
  }
165
205
  let themes = [];
@@ -194,7 +234,7 @@ export function createSlashDispatch({
194
234
  // turn. Same for /fast below.
195
235
  const pendingTurn = state.busy ? ' (applies from the next turn)' : '';
196
236
  if (!arg) {
197
- openEffortPicker();
237
+ openSlashPanel('effort', 'Effort', () => openEffortPicker());
198
238
  return true;
199
239
  }
200
240
  void store.setEffort(arg)
@@ -237,23 +277,23 @@ export function createSlashDispatch({
237
277
  enterProject(target);
238
278
  return true;
239
279
  }
240
- openProjectPicker();
280
+ openSlashPanel('project', 'Projects', () => openProjectPicker());
241
281
  return true;
242
282
  }
243
283
  case 'mcp':
244
- openMcpPicker();
284
+ openSlashPanel('mcp', 'MCP Servers', () => openMcpPicker());
245
285
  return true;
246
286
  case 'skills':
247
- openSkillsPicker();
287
+ openSlashPanel('skills', 'Skills', () => openSkillsPicker());
248
288
  return true;
249
289
  case 'plugins':
250
- openPluginsPicker();
290
+ openSlashPanel('plugins', 'Plugins', () => openPluginsPicker());
251
291
  return true;
252
292
  case 'hooks':
253
- openHooksPicker();
293
+ openSlashPanel('hooks', 'Hooks', () => openHooksPicker());
254
294
  return true;
255
295
  case 'providers':
256
- void openProviderSetupPicker();
296
+ openSlashPanel('providers', 'Providers', () => openProviderSetupPicker());
257
297
  return true;
258
298
  case 'schedules':
259
299
  case 'webhooks':
@@ -263,7 +303,7 @@ export function createSlashDispatch({
263
303
  return true;
264
304
  case 'memory': {
265
305
  if (!arg.trim()) {
266
- openMemoryCorePicker({ returnTo: null });
306
+ openSlashPanel('memory', 'Memory', () => openMemoryCorePicker({ returnTo: null }));
267
307
  return true;
268
308
  }
269
309
  void store.memoryControl?.(parseMemoryCommand(arg))
@@ -273,7 +313,7 @@ export function createSlashDispatch({
273
313
  case 'autoclear': {
274
314
  const value = arg.trim().toLowerCase();
275
315
  if (!value) {
276
- openAutoClearPicker();
316
+ openSlashPanel('autoclear', 'Auto-clear', () => openAutoClearPicker());
277
317
  return true;
278
318
  }
279
319
  // Promise-shaped on a daemon-backed store, so the verdict is reported
@@ -324,6 +364,17 @@ export function createSlashDispatch({
324
364
  })
325
365
  .catch((error) => store.pushNotice(compactFailureNotice(error), 'error'));
326
366
  return true;
367
+ case 'goal':
368
+ void Promise.resolve(store.goalControl?.({ command: arg }))
369
+ .then((result) => {
370
+ if (!result) {
371
+ store.pushNotice('Goal is unavailable.', 'warn');
372
+ return;
373
+ }
374
+ store.pushNotice(result.message || 'Goal updated.', 'info');
375
+ })
376
+ .catch((error) => store.pushNotice(`Goal failed: ${error?.message || error}`, 'error'));
377
+ return true;
327
378
  case 'resume':
328
379
  if (state.busy) {
329
380
  store.pushNotice('wait for the current turn to finish before /resume', 'warn');
@@ -334,14 +385,14 @@ export function createSlashDispatch({
334
385
  .then(ok => store.pushNotice(ok ? `Resumed ${arg}` : 'Couldn’t resume chat.', ok ? 'info' : 'warn'))
335
386
  .catch((e) => store.pushNotice(`Couldn’t resume chat: ${e?.message || e}`, 'error'));
336
387
  } else {
337
- openResumePicker();
388
+ openSlashPanel('resume', 'Resume', () => openResumePicker());
338
389
  }
339
390
  return true;
340
391
  case 'usage':
341
- openUsagePanel(arg);
392
+ openSlashPanel('usage', 'Provider Quotas', () => openUsagePanel(arg));
342
393
  return true;
343
394
  case 'context':
344
- openContextPicker();
395
+ openSlashPanel('context', 'Context Usage', () => openContextPicker());
345
396
  return true;
346
397
  case 'inherit':
347
398
  if (state.busy) {
@@ -363,13 +414,13 @@ export function createSlashDispatch({
363
414
  return true;
364
415
  case 'settings':
365
416
  case 'config':
366
- openSettingsPicker();
417
+ openSlashPanel('settings', 'Settings', () => openSettingsPicker());
367
418
  return true;
368
419
  case 'profile':
369
- openProfilePicker();
420
+ openSlashPanel('profile', 'Profile', () => openProfilePicker());
370
421
  return true;
371
422
  case 'update':
372
- openUpdatePicker();
423
+ openSlashPanel('update', 'Update', () => openUpdatePicker());
373
424
  return true;
374
425
  case 'doctor':
375
426
  if (state.commandBusy) {
@@ -24,6 +24,9 @@ export function createThemeEffortPickers({
24
24
  const openThemePicker = (options = {}) => {
25
25
  const returnTo = typeof options.returnTo === 'function' ? options.returnTo : null;
26
26
  const onboarding = options.onboarding || null;
27
+ const handoffPanel = options.handoffPanel && typeof options.handoffPanel === 'object'
28
+ ? options.handoffPanel
29
+ : null;
27
30
  const own = surface.claim();
28
31
  let themes = [];
29
32
  try {
@@ -60,7 +63,8 @@ export function createThemeEffortPickers({
60
63
  // Onboarding: Enter (row) and ConfirmBar Next both persist the highlighted
61
64
  // theme, then advance; Back restores the original palette then steps back.
62
65
  const saveTheme = (id, { advance = false } = {}) => {
63
- own.close();
66
+ if (handoffPanel) own.paint(handoffPanel);
67
+ else own.close();
64
68
  const applied = applyTheme(id, { persist: true });
65
69
  store.pushNotice(themeNotice(applied || { id }), 'info');
66
70
  if (advance && onboarding) onboarding.onAdvance?.();
@@ -102,7 +106,8 @@ export function createThemeEffortPickers({
102
106
  saveTheme(entry.id, { advance: Boolean(onboarding) });
103
107
  },
104
108
  onCancel: () => {
105
- own.close();
109
+ if (handoffPanel) own.paint(handoffPanel);
110
+ else own.close();
106
111
  // Restore the theme that was active before the picker opened.
107
112
  if (currentId) applyTheme(currentId, { persist: false });
108
113
  if (onboarding) onboarding.onCancel?.();
@@ -94,6 +94,10 @@ export function Picker({
94
94
  indexMode = 'auto',
95
95
  fillHeight = false,
96
96
  visibleCount = MAX_VISIBLE,
97
+ // Loading panels reserve the same content-row geometry as the real list.
98
+ // This prevents an `(empty)` row from being replaced by a multi-row menu in
99
+ // one commit, which reads as the option items jumping into place.
100
+ loading = false,
97
101
  // Onboarding confirm bar: { buttons:[{value,label}], onConfirm(button,index) }.
98
102
  // When present, ←/→ and Tab drive button focus (mutually exclusive with
99
103
  // onLeft/onRight), and Enter fires onConfirm while a button is focused.
@@ -284,6 +288,11 @@ export function Picker({
284
288
  // Clamp selected index when items change length.
285
289
  if (items.length === 0) {
286
290
  const emptyLine = truncateText(String(description || '').replace(/\s+/g, ' ').trim(), Math.max(0, columns - 4));
291
+ const loadingRows = loading
292
+ ? Array.from({ length: visibleLimit }, (_, index) => (
293
+ <Text key={`loading-${index}`} color={theme.inactive}> </Text>
294
+ ))
295
+ : null;
287
296
  return (
288
297
  <Box flexDirection="column" flexShrink={0} height={fillHeight ? '100%' : undefined}>
289
298
  <Box
@@ -303,7 +312,7 @@ export function Picker({
303
312
  <Text> </Text>
304
313
  <Text color={theme.text}>{emptyLine || ' '}</Text>
305
314
  <Text> </Text>
306
- <Text color={theme.inactive}>(empty)</Text>
315
+ {loading ? loadingRows : <Text color={theme.inactive}>(empty)</Text>}
307
316
  {hasConfirm ? (
308
317
  <>
309
318
  <Box flexGrow={1} />