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 +1 -1
- package/scripts/arg-guard-test.mjs +9 -0
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +4 -0
- package/src/runtime/memory/lib/pg/process.mjs +9 -6
- package/src/session-runtime/mcp-glue.mjs +5 -5
- package/src/session-runtime/plugin-mcp.mjs +36 -1
- package/src/session-runtime/resource-api.mjs +14 -9
- package/src/tui/app/extension-pickers.mjs +1 -1
- package/src/tui/app/model-picker.mjs +48 -12
- package/src/tui/app/route-pickers.mjs +4 -0
- package/src/tui/app/slash-dispatch.mjs +6 -1
- package/src/tui/dist/index.mjs +41 -12
package/package.json
CHANGED
|
@@ -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:
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
|
|
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
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
|
|
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:
|
|
155
|
+
current[serverName] = { ...(current[serverName] || {}), enabled: want };
|
|
151
156
|
saveConfigAndAdopt({ ...nextConfig, mcpServers: current });
|
|
152
|
-
return scheduleMcpToggle(serverName,
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
|
|
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) {
|
package/src/tui/dist/index.mjs
CHANGED
|
@@ -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((
|
|
16004
|
-
|
|
16005
|
-
|
|
16006
|
-
|
|
16007
|
-
|
|
16008
|
-
|
|
16009
|
-
|
|
16010
|
-
|
|
16011
|
-
|
|
16012
|
-
|
|
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) {
|