mixdog 0.9.31 → 0.9.33

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixdog",
3
- "version": "0.9.31",
3
+ "version": "0.9.33",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Standalone mixdog coding-agent CLI/TUI workspace.",
@@ -28,6 +28,15 @@ test('grep head_limit/offset/-C as numeric strings coerce', () => {
28
28
  assert.equal(a['-C'], 2);
29
29
  });
30
30
 
31
+ test('grep empty context strings are treated as omitted', () => {
32
+ const a = { pattern: 'x', output_mode: 'content_with_context', '-A': '', '-B': '', '-C': '', context: '' };
33
+ assert.equal(validateBuiltinArgs('grep', a), null);
34
+ assert.equal(Object.prototype.hasOwnProperty.call(a, '-A'), false);
35
+ assert.equal(Object.prototype.hasOwnProperty.call(a, '-B'), false);
36
+ assert.equal(Object.prototype.hasOwnProperty.call(a, '-C'), false);
37
+ assert.equal(Object.prototype.hasOwnProperty.call(a, 'context'), false);
38
+ });
39
+
31
40
  test('list/find/glob head_limit as numeric string coerces', () => {
32
41
  const l = { path: '.', head_limit: '5' };
33
42
  assert.equal(validateBuiltinArgs('list', l), null);
@@ -399,6 +399,10 @@ function guardGrep(a) {
399
399
  if (err) return err;
400
400
  }
401
401
  for (const k of ['-A', '-B', '-C', 'context']) {
402
+ if (hasOwn(a, k) && a[k] === '') {
403
+ delete a[k];
404
+ continue;
405
+ }
402
406
  const err = checkIntInRange(a, k, 0, GREP_CONTEXT_MAX, { clamp: true });
403
407
  if (err) return err;
404
408
  }
@@ -290,12 +290,15 @@ export async function startPg({ runtimeDir, pgdataDir, port: preferredPort = 554
290
290
  // so we return the instant pg_isready succeeds rather than waiting on pg_ctl.
291
291
  // Only the long-lived child handle is unref'd; poll timers stay ref'd.
292
292
  async function startAndWaitReady() {
293
- // detached on win32: give pg_ctl (and the postmaster it spawns) its own
294
- // process group / console so an ancestor `taskkill /F /T` cannot enumerate
295
- // it via the Node process tree. The orphaned postmaster survives and is
296
- // re-adopted next boot via supervisor tryReusePgInstance (postmaster.pid).
297
- const detached = process.platform === 'win32'
298
- const child = spawn(pgctl, startArgs, { env, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, detached })
293
+ // NEVER set `detached` on win32: DETACHED_PROCESS makes the OS ignore
294
+ // `windowsHide`, so pg_ctl + the postmaster it launches allocate a VISIBLE
295
+ // console (see shared/spawn-flags.mjs). Detachment gave no real isolation
296
+ // here anyway: `pg_ctl start` daemonizes the postmaster and exits at once,
297
+ // so the postmaster is already reparented OUT of the Node process tree
298
+ // detached only isolated the short-lived pg_ctl. Shutdown/reuse target
299
+ // pgdata/postmaster.pid (pg_ctl stop -D, tryReusePgInstance), not a
300
+ // Node-tree taskkill, so `windowsHide` alone is correct on all platforms.
301
+ const child = spawn(pgctl, startArgs, { env, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true })
299
302
  child.unref?.()
300
303
  let stdout = '', stderr = '', closed = false, exitCode = null
301
304
  child.stdout?.on('data', d => { stdout += d.toString() })
@@ -100,11 +100,11 @@ export function createMcpGlue({
100
100
  async function applyMcpServerConnection(name, enabled) {
101
101
  const target = clean(name);
102
102
  if (!target) return;
103
- const { servers, sources } = resolveEffectiveMcpServers();
104
- // A project-local `.mcp.json` entry WINS over config for this name, so the
105
- // config enable/disable flag doesn't change what's actually connected.
106
- // Only act when the effective entry is config-sourced (or absent).
107
- if (sources[target] && sources[target] !== 'config') return;
103
+ const { servers } = resolveEffectiveMcpServers();
104
+ // The toggle is applied to whichever source drives this name: project
105
+ // `.mcp.json` entries persist their `enabled` flag in that file (project >
106
+ // config precedence) and config entries in mixdog-config, so acting on the
107
+ // effective entry here keeps live state in sync with the durable flag.
108
108
  // Changing this server's state clears any stale failure record for it.
109
109
  if (Array.isArray(state.mcpFailures)) {
110
110
  state.mcpFailures = state.mcpFailures.filter((row) => row.name !== target);
@@ -1,5 +1,5 @@
1
1
  // Plugin/project MCP server discovery + normalization, and skill-file counting.
2
- import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
2
+ import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
3
3
  import { isAbsolute, join, relative, resolve } from 'node:path';
4
4
  import { clean } from './session-text.mjs';
5
5
  import { readJsonSafe } from './fs-utils.mjs';
@@ -45,6 +45,41 @@ export function readProjectMcpServers(cwd) {
45
45
  return out;
46
46
  }
47
47
 
48
+ // Persist an enable/disable flag for a project-local `.mcp.json` server, in
49
+ // place, preserving the file's shape. Accepts both the standard
50
+ // `{ mcpServers: {...} }` wrapper and a bare name->cfg map. Writes pretty JSON
51
+ // (2-space indent + trailing newline) so the file stays valid + diff-friendly.
52
+ // Throws if the file is missing/unparseable or the server isn't defined there.
53
+ export function setProjectMcpServerEnabled(cwd, name, enabled) {
54
+ const path = join(cwd || '.', '.mcp.json');
55
+ const key = clean(name);
56
+ if (!key) throw new Error('MCP server name is required');
57
+ let raw;
58
+ try {
59
+ raw = JSON.parse(readFileSync(path, 'utf8'));
60
+ } catch (error) {
61
+ throw new Error(`cannot update ${path}: ${error?.message || String(error)}`);
62
+ }
63
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
64
+ throw new Error(`unexpected .mcp.json shape at ${path}`);
65
+ }
66
+ const usesWrapper = raw.mcpServers && typeof raw.mcpServers === 'object' && !Array.isArray(raw.mcpServers);
67
+ const map = usesWrapper ? raw.mcpServers : raw;
68
+ const entryKey = Object.prototype.hasOwnProperty.call(map, key)
69
+ ? key
70
+ : Object.keys(map).reverse().find((candidate) => clean(candidate) === key);
71
+ if (!map || typeof map !== 'object' || Array.isArray(map) || !entryKey) {
72
+ throw new Error(`MCP server not defined in ${path}: ${key}`);
73
+ }
74
+ const entry = map[entryKey];
75
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
76
+ throw new Error(`MCP server is not an object in ${path}: ${key}`);
77
+ }
78
+ map[entryKey] = { ...entry, enabled: enabled !== false };
79
+ writeFileSync(path, `${JSON.stringify(raw, null, 2)}\n`, 'utf8');
80
+ return raw;
81
+ }
82
+
48
83
  function isPlainObject(value) {
49
84
  return value !== null && typeof value === 'object' && !Array.isArray(value);
50
85
  }
@@ -17,6 +17,7 @@ import {
17
17
  pluginRawMcpServers,
18
18
  pluginMcpEnableScript,
19
19
  resolveContainedPluginPath,
20
+ setProjectMcpServerEnabled,
20
21
  } from './plugin-mcp.mjs';
21
22
 
22
23
  // MCP servers, skills, plugins, hooks, and memory/recall surfaces. Extracted
@@ -129,6 +130,17 @@ export function createResourceApi(deps) {
129
130
  setMcpServerEnabled(name, enabled) {
130
131
  const serverName = clean(name);
131
132
  if (!serverName) throw new Error('MCP server name is required');
133
+ const want = enabled !== false;
134
+ // A project-local `.mcp.json` entry WINS over config for this name, so the
135
+ // durable toggle must land in whichever file actually drives the server.
136
+ // For project-sourced servers, persist the `enabled` flag into `.mcp.json`
137
+ // (the mtime bump invalidates the project cache), then run the same
138
+ // background connect/recreate chain used for config servers.
139
+ const shadowRow = mcpStatus().servers.find((s) => s.name === serverName);
140
+ if (shadowRow && shadowRow.source === 'project') {
141
+ setProjectMcpServerEnabled(getCurrentCwd(), serverName, want);
142
+ return scheduleMcpToggle(serverName, want);
143
+ }
132
144
  const nextConfig = { ...getConfig() };
133
145
  const current = nextConfig.mcpServers && typeof nextConfig.mcpServers === 'object'
134
146
  ? { ...nextConfig.mcpServers }
@@ -136,20 +148,13 @@ export function createResourceApi(deps) {
136
148
  if (!Object.prototype.hasOwnProperty.call(current, serverName)) {
137
149
  throw new Error(`MCP server not configured: ${serverName}`);
138
150
  }
139
- // A project-local `.mcp.json` entry WINS over config for this name, so a
140
- // config enable/disable flag would persist but never change live state.
141
- // Surface that to the caller instead of reporting a silent success.
142
- const shadowRow = mcpStatus().servers.find((s) => s.name === serverName);
143
- if (shadowRow && shadowRow.source === 'project') {
144
- throw new Error(`'${serverName}' is defined by project .mcp.json — config toggle has no effect`);
145
- }
146
151
  // Adopt + persist config synchronously (fast) so intent is durable, then
147
152
  // hand the heavy connect/close/recreate to the per-server background
148
153
  // chain. Return that chain's promise so callers can settle the picker on
149
154
  // completion, but the store no longer blocks on it.
150
- current[serverName] = { ...(current[serverName] || {}), enabled: enabled !== false };
155
+ current[serverName] = { ...(current[serverName] || {}), enabled: want };
151
156
  saveConfigAndAdopt({ ...nextConfig, mcpServers: current });
152
- return scheduleMcpToggle(serverName, enabled !== false);
157
+ return scheduleMcpToggle(serverName, want);
153
158
  },
154
159
  skillsStatus() {
155
160
  return skillsStatus();
@@ -64,7 +64,7 @@ export function createExtensionPickers({
64
64
  markerColor: enabled ? theme.success : theme.inactive,
65
65
  description: pending
66
66
  ? `${optimistic.enabled ? 'enabling' : 'disabling'}… · ${server.transport || 'unknown'}`
67
- : `${server.status || 'unknown'} · ${server.transport || 'unknown'} · ${server.toolCount || 0} tools${server.error ? ` · ${server.error}` : ''}`,
67
+ : `${server.source || 'config'} · ${server.status || 'unknown'} · ${server.transport || 'unknown'} · ${server.toolCount || 0} tools${server.error ? ` · ${server.error}` : ''}`,
68
68
  _action: 'server',
69
69
  _server: server,
70
70
  _enabled: enabled,
@@ -20,6 +20,12 @@ import {
20
20
  buildProviderModelItems,
21
21
  } from './model-options.mjs';
22
22
 
23
+ // Cached picker opens stay instant, but a catalog older than this is treated as
24
+ // stale: cached rows render immediately and a background force refresh updates
25
+ // the picker in place. Avoids the "stale /model & /agents catalog" without
26
+ // paying a remote provider-list round-trip on every open.
27
+ const MODEL_CACHE_TTL_MS = 5 * 60 * 1000;
28
+
23
29
  export function createModelPicker({
24
30
  store,
25
31
  getState,
@@ -35,6 +41,7 @@ export function createModelPicker({
35
41
  modelSwitchNotice,
36
42
  openProviderSetupPicker,
37
43
  }) {
44
+ let providerModelsTtlRefreshPromise = null;
38
45
  const openModelPicker = async (options = {}) => {
39
46
  const state = getState();
40
47
  setProviderPrompt(null);
@@ -61,6 +68,7 @@ export function createModelPicker({
61
68
  : [];
62
69
  let refreshModelsPromise = null;
63
70
  let renderedQuickModels = false;
71
+ let renderActiveProviderModels = null;
64
72
  if (!providerModels.length || options.refreshModels === true) {
65
73
  setPicker({
66
74
  title: options.title || 'Model',
@@ -88,6 +96,17 @@ export function createModelPicker({
88
96
  }
89
97
  }
90
98
 
99
+ // Served straight from a non-empty UI cache: if that cache is older than the
100
+ // TTL, render the cached rows now and quietly force a background refresh so
101
+ // the catalog can't drift stale. Never applies to the search cache (its own
102
+ // quick paths refresh differently) or explicit refreshModels opens.
103
+ const cacheAt = Number(cacheRef.current.at) || 0;
104
+ const cacheIsStale = providerModels.length > 0
105
+ && options.refreshModels !== true
106
+ && options.cacheRef !== 'search'
107
+ && !refreshModelsPromise
108
+ && (Date.now() - cacheAt) > MODEL_CACHE_TTL_MS;
109
+
91
110
  if (!providerModels || providerModels.length === 0) {
92
111
  store.pushNotice(options.emptyNotice || 'no provider models available; open /providers to sign in', 'warn');
93
112
  void openProviderSetupPicker({
@@ -112,9 +131,11 @@ export function createModelPicker({
112
131
  }
113
132
  const highlightProvider = renderOptions.highlightProvider || providerListHighlightProvider || null;
114
133
  activeModelProvider = null;
134
+ renderActiveProviderModels = null;
115
135
  const openProviderModelsPicker = (provider) => {
116
136
  if (!provider) return;
117
137
  activeModelProvider = provider;
138
+ renderActiveProviderModels = () => openProviderModelsPicker(provider);
118
139
  const providerModels = models.filter((model) => model.provider === provider);
119
140
  const preferredEffort = (values = []) => {
120
141
  const allowed = values.filter(Boolean);
@@ -345,19 +366,34 @@ export function createModelPicker({
345
366
  };
346
367
 
347
368
  renderModelPicker();
369
+ const applyFreshModels = (freshModels) => {
370
+ if (!isActiveModelPicker()) return;
371
+ if (!Array.isArray(freshModels) || freshModels.length === 0) return;
372
+ providerModels = freshModels;
373
+ models = normalizeModelOptions(providerModels);
374
+ cacheRef.current = { models: providerModels, at: Date.now() };
375
+ if (activeModelProvider === null) {
376
+ renderModelPicker();
377
+ } else if (typeof renderActiveProviderModels === 'function') {
378
+ renderActiveProviderModels();
379
+ }
380
+ };
348
381
  if (renderedQuickModels && refreshModelsPromise) {
349
- void refreshModelsPromise
350
- .then((freshModels) => {
351
- if (!isActiveModelPicker()) return;
352
- if (!Array.isArray(freshModels) || freshModels.length === 0) return;
353
- providerModels = freshModels;
354
- models = normalizeModelOptions(providerModels);
355
- cacheRef.current = { models: providerModels, at: Date.now() };
356
- if (activeModelProvider === null) {
357
- renderModelPicker();
358
- }
359
- })
360
- .catch(() => {});
382
+ void refreshModelsPromise.then(applyFreshModels).catch(() => {});
383
+ } else if (cacheIsStale) {
384
+ if (!providerModelsTtlRefreshPromise) {
385
+ providerModelsTtlRefreshPromise = Promise.resolve(loadModels({ force: true }))
386
+ .then((freshModels) => {
387
+ if (Array.isArray(freshModels) && freshModels.length > 0) {
388
+ cacheRef.current = { models: freshModels, at: Date.now() };
389
+ }
390
+ return freshModels;
391
+ })
392
+ .finally(() => {
393
+ providerModelsTtlRefreshPromise = null;
394
+ });
395
+ }
396
+ void providerModelsTtlRefreshPromise.then(applyFreshModels).catch(() => {});
361
397
  }
362
398
  };
363
399
 
@@ -74,6 +74,9 @@ export function createRoutePickers({
74
74
  store.pushNotice(`could not list agents: ${e?.message || e}`, 'error');
75
75
  return;
76
76
  }
77
+ // /agents refresh: force the nested model picker to reload the provider
78
+ // catalog on the next agent open (the agents list itself is always fresh).
79
+ const refreshModels = options.refreshModels === true;
77
80
  const routeOverrides = options.routeOverrides && typeof options.routeOverrides === 'object' ? options.routeOverrides : {};
78
81
  const initialAgentId = clean(options.initialAgentId || '');
79
82
  const items = agents.map((agent) => ({
@@ -104,6 +107,7 @@ export function createRoutePickers({
104
107
  void openModelPicker({
105
108
  title: `${agent.label} Model`,
106
109
  providerDescription: 'Choose a provider for this agent.',
110
+ refreshModels,
107
111
  currentRoute: agent.route || null,
108
112
  returnTo: () => openAgentsPicker(),
109
113
  onImmediateSelect: (routeInput) => {
@@ -71,6 +71,11 @@ export function createSlashDispatch({
71
71
  openModelPicker();
72
72
  return true;
73
73
  }
74
+ if (arg.trim().toLowerCase() === 'refresh') {
75
+ // Explicit catalog reload: force a fresh remote provider list.
76
+ openModelPicker({ refreshModels: true });
77
+ return true;
78
+ }
74
79
  void store.setModel(arg)
75
80
  .then(ok => store.pushNotice(ok ? modelSwitchNotice() : 'Model switch is already running.', ok ? 'info' : 'warn'))
76
81
  .catch((e) => store.pushNotice(`Couldn’t switch model: ${e?.message || e}`, 'error'));
@@ -92,7 +97,7 @@ export function createSlashDispatch({
92
97
  openSearchPicker();
93
98
  return true;
94
99
  case 'agents':
95
- openAgentsPicker();
100
+ openAgentsPicker(arg.trim().toLowerCase() === 'refresh' ? { refreshModels: true } : {});
96
101
  return true;
97
102
  case 'workflow':
98
103
  if (!arg) {
@@ -13122,7 +13122,7 @@ function createExtensionPickers({
13122
13122
  label: server.name,
13123
13123
  marker: enabled ? "\u25CF" : "\u25CB",
13124
13124
  markerColor: enabled ? theme2.success : theme2.inactive,
13125
- description: pending ? `${optimistic.enabled ? "enabling" : "disabling"}\u2026 \xB7 ${server.transport || "unknown"}` : `${server.status || "unknown"} \xB7 ${server.transport || "unknown"} \xB7 ${server.toolCount || 0} tools${server.error ? ` \xB7 ${server.error}` : ""}`,
13125
+ description: pending ? `${optimistic.enabled ? "enabling" : "disabling"}\u2026 \xB7 ${server.transport || "unknown"}` : `${server.source || "config"} \xB7 ${server.status || "unknown"} \xB7 ${server.transport || "unknown"} \xB7 ${server.toolCount || 0} tools${server.error ? ` \xB7 ${server.error}` : ""}`,
13126
13126
  _action: "server",
13127
13127
  _server: server,
13128
13128
  _enabled: enabled
@@ -15690,6 +15690,7 @@ function createChannelPickers({
15690
15690
  }
15691
15691
 
15692
15692
  // src/tui/app/model-picker.mjs
15693
+ var MODEL_CACHE_TTL_MS = 5 * 60 * 1e3;
15693
15694
  function createModelPicker({
15694
15695
  store,
15695
15696
  getState,
@@ -15705,6 +15706,7 @@ function createModelPicker({
15705
15706
  modelSwitchNotice: modelSwitchNotice2,
15706
15707
  openProviderSetupPicker
15707
15708
  }) {
15709
+ let providerModelsTtlRefreshPromise = null;
15708
15710
  const openModelPicker = async (options = {}) => {
15709
15711
  const state = getState();
15710
15712
  setProviderPrompt(null);
@@ -15729,6 +15731,7 @@ function createModelPicker({
15729
15731
  let providerModels = Array.isArray(cacheRef.current.models) ? cacheRef.current.models : [];
15730
15732
  let refreshModelsPromise = null;
15731
15733
  let renderedQuickModels = false;
15734
+ let renderActiveProviderModels = null;
15732
15735
  if (!providerModels.length || options.refreshModels === true) {
15733
15736
  setPicker({
15734
15737
  title: options.title || "Model",
@@ -15755,6 +15758,8 @@ function createModelPicker({
15755
15758
  return;
15756
15759
  }
15757
15760
  }
15761
+ const cacheAt = Number(cacheRef.current.at) || 0;
15762
+ const cacheIsStale = providerModels.length > 0 && options.refreshModels !== true && options.cacheRef !== "search" && !refreshModelsPromise && Date.now() - cacheAt > MODEL_CACHE_TTL_MS;
15758
15763
  if (!providerModels || providerModels.length === 0) {
15759
15764
  store.pushNotice(options.emptyNotice || "no provider models available; open /providers to sign in", "warn");
15760
15765
  void openProviderSetupPicker({
@@ -15778,9 +15783,11 @@ function createModelPicker({
15778
15783
  }
15779
15784
  const highlightProvider = renderOptions.highlightProvider || providerListHighlightProvider || null;
15780
15785
  activeModelProvider = null;
15786
+ renderActiveProviderModels = null;
15781
15787
  const openProviderModelsPicker = (provider) => {
15782
15788
  if (!provider) return;
15783
15789
  activeModelProvider = provider;
15790
+ renderActiveProviderModels = () => openProviderModelsPicker(provider);
15784
15791
  const providerModels2 = models.filter((model) => model.provider === provider);
15785
15792
  const preferredEffort = (values = []) => {
15786
15793
  const allowed = values.filter(Boolean);
@@ -15999,17 +16006,33 @@ ${model?.id || ""}`;
15999
16006
  });
16000
16007
  };
16001
16008
  renderModelPicker();
16009
+ const applyFreshModels = (freshModels) => {
16010
+ if (!isActiveModelPicker()) return;
16011
+ if (!Array.isArray(freshModels) || freshModels.length === 0) return;
16012
+ providerModels = freshModels;
16013
+ models = normalizeModelOptions(providerModels);
16014
+ cacheRef.current = { models: providerModels, at: Date.now() };
16015
+ if (activeModelProvider === null) {
16016
+ renderModelPicker();
16017
+ } else if (typeof renderActiveProviderModels === "function") {
16018
+ renderActiveProviderModels();
16019
+ }
16020
+ };
16002
16021
  if (renderedQuickModels && refreshModelsPromise) {
16003
- void refreshModelsPromise.then((freshModels) => {
16004
- if (!isActiveModelPicker()) return;
16005
- if (!Array.isArray(freshModels) || freshModels.length === 0) return;
16006
- providerModels = freshModels;
16007
- models = normalizeModelOptions(providerModels);
16008
- cacheRef.current = { models: providerModels, at: Date.now() };
16009
- if (activeModelProvider === null) {
16010
- renderModelPicker();
16011
- }
16012
- }).catch(() => {
16022
+ void refreshModelsPromise.then(applyFreshModels).catch(() => {
16023
+ });
16024
+ } else if (cacheIsStale) {
16025
+ if (!providerModelsTtlRefreshPromise) {
16026
+ providerModelsTtlRefreshPromise = Promise.resolve(loadModels({ force: true })).then((freshModels) => {
16027
+ if (Array.isArray(freshModels) && freshModels.length > 0) {
16028
+ cacheRef.current = { models: freshModels, at: Date.now() };
16029
+ }
16030
+ return freshModels;
16031
+ }).finally(() => {
16032
+ providerModelsTtlRefreshPromise = null;
16033
+ });
16034
+ }
16035
+ void providerModelsTtlRefreshPromise.then(applyFreshModels).catch(() => {
16013
16036
  });
16014
16037
  }
16015
16038
  };
@@ -16616,6 +16639,7 @@ function createRoutePickers({
16616
16639
  store.pushNotice(`could not list agents: ${e?.message || e}`, "error");
16617
16640
  return;
16618
16641
  }
16642
+ const refreshModels = options.refreshModels === true;
16619
16643
  const routeOverrides = options.routeOverrides && typeof options.routeOverrides === "object" ? options.routeOverrides : {};
16620
16644
  const initialAgentId = clean3(options.initialAgentId || "");
16621
16645
  const items = agents.map((agent) => ({
@@ -16646,6 +16670,7 @@ function createRoutePickers({
16646
16670
  void openModelPicker({
16647
16671
  title: `${agent.label} Model`,
16648
16672
  providerDescription: "Choose a provider for this agent.",
16673
+ refreshModels,
16649
16674
  currentRoute: agent.route || null,
16650
16675
  returnTo: () => openAgentsPicker(),
16651
16676
  onImmediateSelect: (routeInput) => {
@@ -17441,6 +17466,10 @@ function createSlashDispatch({
17441
17466
  openModelPicker();
17442
17467
  return true;
17443
17468
  }
17469
+ if (arg.trim().toLowerCase() === "refresh") {
17470
+ openModelPicker({ refreshModels: true });
17471
+ return true;
17472
+ }
17444
17473
  void store.setModel(arg).then((ok) => store.pushNotice(ok ? modelSwitchNotice2() : "Model switch is already running.", ok ? "info" : "warn")).catch((e) => store.pushNotice(`Couldn\u2019t switch model: ${e?.message || e}`, "error"));
17445
17474
  return true;
17446
17475
  case "remote": {
@@ -17453,7 +17482,7 @@ function createSlashDispatch({
17453
17482
  openSearchPicker();
17454
17483
  return true;
17455
17484
  case "agents":
17456
- openAgentsPicker();
17485
+ openAgentsPicker(arg.trim().toLowerCase() === "refresh" ? { refreshModels: true } : {});
17457
17486
  return true;
17458
17487
  case "workflow":
17459
17488
  if (!arg) {