mixdog 0.9.32 → 0.9.34
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 +4 -2
- package/scripts/compact-trigger-migration-smoke.mjs +37 -31
- package/scripts/provider-toolcall-test.mjs +535 -36
- package/src/agents/heavy-worker/AGENT.md +3 -4
- package/src/agents/worker/AGENT.md +2 -3
- package/src/repl.mjs +9 -1
- package/src/rules/shared/01-tool.md +4 -2
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +22 -0
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +21 -2
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +15 -9
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +35 -4
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +130 -18
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +22 -12
- package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +131 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +102 -26
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +194 -9
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +13 -2
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -1
- package/src/runtime/agent/orchestrator/session/compact/constants.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/compact.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +39 -6
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +15 -23
- package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +25 -0
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +16 -8
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +50 -4
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +53 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +12 -2
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +270 -47
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +4 -2
- package/src/runtime/channels/lib/output-forwarder.mjs +7 -1
- package/src/runtime/channels/lib/owned-runtime.mjs +5 -2
- package/src/runtime/memory/lib/embedding-worker.mjs +18 -3
- package/src/runtime/memory/lib/memory-embed.mjs +89 -8
- package/src/session-runtime/context-status.mjs +7 -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/session-runtime/runtime-core.mjs +0 -10
- 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/app/use-transcript-window.mjs +13 -1
- package/src/tui/components/StatusLine.jsx +5 -5
- package/src/tui/dist/index.mjs +46 -15
- package/src/ui/statusline.mjs +4 -10
- package/src/vendor/statusline/bin/statusline-route.mjs +4 -5
- package/src/vendor/statusline/src/gateway/route-meta.mjs +3 -2
|
@@ -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();
|
|
@@ -81,16 +81,6 @@ import {
|
|
|
81
81
|
removePlugin as registryRemovePlugin,
|
|
82
82
|
updatePlugin as registryUpdatePlugin,
|
|
83
83
|
} from '../standalone/plugin-admin.mjs';
|
|
84
|
-
import {
|
|
85
|
-
estimateMessagesTokens,
|
|
86
|
-
estimateRequestReserveTokens,
|
|
87
|
-
estimateTranscriptContextUsage,
|
|
88
|
-
estimateToolSchemaTokens,
|
|
89
|
-
resolveCompactBufferTokens,
|
|
90
|
-
resolveCompactTriggerTokens,
|
|
91
|
-
summarizeContextMessages,
|
|
92
|
-
} from '../runtime/agent/orchestrator/session/context-utils.mjs';
|
|
93
|
-
|
|
94
84
|
import {
|
|
95
85
|
sessionMessageText,
|
|
96
86
|
messageContextText,
|
|
@@ -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) {
|
|
@@ -595,7 +595,19 @@ export function useTranscriptWindow({
|
|
|
595
595
|
toolExpanded: toolExpandedFlag,
|
|
596
596
|
variantKey,
|
|
597
597
|
});
|
|
598
|
-
if (!isStreamingAssistant)
|
|
598
|
+
if (!isStreamingAssistant) {
|
|
599
|
+
// First mount (no prior entry): this frame's row index already used the
|
|
600
|
+
// ESTIMATE for this item (measuredTranscriptRows returned null). If Yoga
|
|
601
|
+
// measured exactly what the estimate predicted, the geometry is already
|
|
602
|
+
// correct — bumping measuredRowsVersion would re-run the row-index/window
|
|
603
|
+
// memos for a no-op and repaint the card one frame later (the "settle"
|
|
604
|
+
// jump). Only bump when the measurement actually corrects the estimate.
|
|
605
|
+
// A CHANGED height (prev existed but differs) always bumps.
|
|
606
|
+
const estimateRows = prev
|
|
607
|
+
? -1
|
|
608
|
+
: estimateTranscriptItemRowsCached(item, frameColumns, toolOutputExpanded);
|
|
609
|
+
if (prev || measured !== estimateRows) changed = true;
|
|
610
|
+
}
|
|
599
611
|
}
|
|
600
612
|
if (changed) {
|
|
601
613
|
// `changed` only flips true when a height actually differs from the
|
|
@@ -158,11 +158,11 @@ function localContextPct({
|
|
|
158
158
|
: (localNum(contextWindow) > 0
|
|
159
159
|
? localNum(contextWindow)
|
|
160
160
|
: (localNum(rawContextWindow) > 0 ? localNum(rawContextWindow) : 200_000)));
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
const window =
|
|
161
|
+
// Boundary-only denominator (mirrors ui/statusline.mjs resolveContextUsedPct):
|
|
162
|
+
// context % is always measured against the single compaction boundary value,
|
|
163
|
+
// never a separate auto-compact trigger — statusline and gateway stay on the
|
|
164
|
+
// same denominator with no before/after trigger semantics.
|
|
165
|
+
const window = baseWindow;
|
|
166
166
|
const s = stats && typeof stats === 'object' ? stats : {};
|
|
167
167
|
const source = String(s.currentContextSource || '').toLowerCase();
|
|
168
168
|
const estimated = localNum(s.currentEstimatedContextTokens);
|
package/src/tui/dist/index.mjs
CHANGED
|
@@ -3910,8 +3910,7 @@ function localContextPct({
|
|
|
3910
3910
|
autoCompactTokenLimit = 0
|
|
3911
3911
|
} = {}) {
|
|
3912
3912
|
const baseWindow = localNum(compactBoundaryTokens) > 0 ? localNum(compactBoundaryTokens) : localNum(displayContextWindow) > 0 ? localNum(displayContextWindow) : localNum(contextWindow) > 0 ? localNum(contextWindow) : localNum(rawContextWindow) > 0 ? localNum(rawContextWindow) : 2e5;
|
|
3913
|
-
const
|
|
3914
|
-
const window = trigger > 0 && trigger < baseWindow ? trigger : baseWindow;
|
|
3913
|
+
const window = baseWindow;
|
|
3915
3914
|
const s = stats && typeof stats === "object" ? stats : {};
|
|
3916
3915
|
const source = String(s.currentContextSource || "").toLowerCase();
|
|
3917
3916
|
const estimated = localNum(s.currentEstimatedContextTokens);
|
|
@@ -11944,7 +11943,10 @@ function useTranscriptWindow({
|
|
|
11944
11943
|
toolExpanded: toolExpandedFlag,
|
|
11945
11944
|
variantKey
|
|
11946
11945
|
});
|
|
11947
|
-
if (!isStreamingAssistant)
|
|
11946
|
+
if (!isStreamingAssistant) {
|
|
11947
|
+
const estimateRows = prev ? -1 : estimateTranscriptItemRowsCached(item, frameColumns, toolOutputExpanded);
|
|
11948
|
+
if (prev || measured !== estimateRows) changed = true;
|
|
11949
|
+
}
|
|
11948
11950
|
}
|
|
11949
11951
|
if (changed) {
|
|
11950
11952
|
setMeasuredRowsVersion((v) => (v + 1) % 1e6);
|
|
@@ -13122,7 +13124,7 @@ function createExtensionPickers({
|
|
|
13122
13124
|
label: server.name,
|
|
13123
13125
|
marker: enabled ? "\u25CF" : "\u25CB",
|
|
13124
13126
|
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}` : ""}`,
|
|
13127
|
+
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
13128
|
_action: "server",
|
|
13127
13129
|
_server: server,
|
|
13128
13130
|
_enabled: enabled
|
|
@@ -15690,6 +15692,7 @@ function createChannelPickers({
|
|
|
15690
15692
|
}
|
|
15691
15693
|
|
|
15692
15694
|
// src/tui/app/model-picker.mjs
|
|
15695
|
+
var MODEL_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
15693
15696
|
function createModelPicker({
|
|
15694
15697
|
store,
|
|
15695
15698
|
getState,
|
|
@@ -15705,6 +15708,7 @@ function createModelPicker({
|
|
|
15705
15708
|
modelSwitchNotice: modelSwitchNotice2,
|
|
15706
15709
|
openProviderSetupPicker
|
|
15707
15710
|
}) {
|
|
15711
|
+
let providerModelsTtlRefreshPromise = null;
|
|
15708
15712
|
const openModelPicker = async (options = {}) => {
|
|
15709
15713
|
const state = getState();
|
|
15710
15714
|
setProviderPrompt(null);
|
|
@@ -15729,6 +15733,7 @@ function createModelPicker({
|
|
|
15729
15733
|
let providerModels = Array.isArray(cacheRef.current.models) ? cacheRef.current.models : [];
|
|
15730
15734
|
let refreshModelsPromise = null;
|
|
15731
15735
|
let renderedQuickModels = false;
|
|
15736
|
+
let renderActiveProviderModels = null;
|
|
15732
15737
|
if (!providerModels.length || options.refreshModels === true) {
|
|
15733
15738
|
setPicker({
|
|
15734
15739
|
title: options.title || "Model",
|
|
@@ -15755,6 +15760,8 @@ function createModelPicker({
|
|
|
15755
15760
|
return;
|
|
15756
15761
|
}
|
|
15757
15762
|
}
|
|
15763
|
+
const cacheAt = Number(cacheRef.current.at) || 0;
|
|
15764
|
+
const cacheIsStale = providerModels.length > 0 && options.refreshModels !== true && options.cacheRef !== "search" && !refreshModelsPromise && Date.now() - cacheAt > MODEL_CACHE_TTL_MS;
|
|
15758
15765
|
if (!providerModels || providerModels.length === 0) {
|
|
15759
15766
|
store.pushNotice(options.emptyNotice || "no provider models available; open /providers to sign in", "warn");
|
|
15760
15767
|
void openProviderSetupPicker({
|
|
@@ -15778,9 +15785,11 @@ function createModelPicker({
|
|
|
15778
15785
|
}
|
|
15779
15786
|
const highlightProvider = renderOptions.highlightProvider || providerListHighlightProvider || null;
|
|
15780
15787
|
activeModelProvider = null;
|
|
15788
|
+
renderActiveProviderModels = null;
|
|
15781
15789
|
const openProviderModelsPicker = (provider) => {
|
|
15782
15790
|
if (!provider) return;
|
|
15783
15791
|
activeModelProvider = provider;
|
|
15792
|
+
renderActiveProviderModels = () => openProviderModelsPicker(provider);
|
|
15784
15793
|
const providerModels2 = models.filter((model) => model.provider === provider);
|
|
15785
15794
|
const preferredEffort = (values = []) => {
|
|
15786
15795
|
const allowed = values.filter(Boolean);
|
|
@@ -15999,17 +16008,33 @@ ${model?.id || ""}`;
|
|
|
15999
16008
|
});
|
|
16000
16009
|
};
|
|
16001
16010
|
renderModelPicker();
|
|
16011
|
+
const applyFreshModels = (freshModels) => {
|
|
16012
|
+
if (!isActiveModelPicker()) return;
|
|
16013
|
+
if (!Array.isArray(freshModels) || freshModels.length === 0) return;
|
|
16014
|
+
providerModels = freshModels;
|
|
16015
|
+
models = normalizeModelOptions(providerModels);
|
|
16016
|
+
cacheRef.current = { models: providerModels, at: Date.now() };
|
|
16017
|
+
if (activeModelProvider === null) {
|
|
16018
|
+
renderModelPicker();
|
|
16019
|
+
} else if (typeof renderActiveProviderModels === "function") {
|
|
16020
|
+
renderActiveProviderModels();
|
|
16021
|
+
}
|
|
16022
|
+
};
|
|
16002
16023
|
if (renderedQuickModels && refreshModelsPromise) {
|
|
16003
|
-
void refreshModelsPromise.then((
|
|
16004
|
-
|
|
16005
|
-
|
|
16006
|
-
|
|
16007
|
-
|
|
16008
|
-
|
|
16009
|
-
|
|
16010
|
-
|
|
16011
|
-
|
|
16012
|
-
|
|
16024
|
+
void refreshModelsPromise.then(applyFreshModels).catch(() => {
|
|
16025
|
+
});
|
|
16026
|
+
} else if (cacheIsStale) {
|
|
16027
|
+
if (!providerModelsTtlRefreshPromise) {
|
|
16028
|
+
providerModelsTtlRefreshPromise = Promise.resolve(loadModels({ force: true })).then((freshModels) => {
|
|
16029
|
+
if (Array.isArray(freshModels) && freshModels.length > 0) {
|
|
16030
|
+
cacheRef.current = { models: freshModels, at: Date.now() };
|
|
16031
|
+
}
|
|
16032
|
+
return freshModels;
|
|
16033
|
+
}).finally(() => {
|
|
16034
|
+
providerModelsTtlRefreshPromise = null;
|
|
16035
|
+
});
|
|
16036
|
+
}
|
|
16037
|
+
void providerModelsTtlRefreshPromise.then(applyFreshModels).catch(() => {
|
|
16013
16038
|
});
|
|
16014
16039
|
}
|
|
16015
16040
|
};
|
|
@@ -16616,6 +16641,7 @@ function createRoutePickers({
|
|
|
16616
16641
|
store.pushNotice(`could not list agents: ${e?.message || e}`, "error");
|
|
16617
16642
|
return;
|
|
16618
16643
|
}
|
|
16644
|
+
const refreshModels = options.refreshModels === true;
|
|
16619
16645
|
const routeOverrides = options.routeOverrides && typeof options.routeOverrides === "object" ? options.routeOverrides : {};
|
|
16620
16646
|
const initialAgentId = clean3(options.initialAgentId || "");
|
|
16621
16647
|
const items = agents.map((agent) => ({
|
|
@@ -16646,6 +16672,7 @@ function createRoutePickers({
|
|
|
16646
16672
|
void openModelPicker({
|
|
16647
16673
|
title: `${agent.label} Model`,
|
|
16648
16674
|
providerDescription: "Choose a provider for this agent.",
|
|
16675
|
+
refreshModels,
|
|
16649
16676
|
currentRoute: agent.route || null,
|
|
16650
16677
|
returnTo: () => openAgentsPicker(),
|
|
16651
16678
|
onImmediateSelect: (routeInput) => {
|
|
@@ -17441,6 +17468,10 @@ function createSlashDispatch({
|
|
|
17441
17468
|
openModelPicker();
|
|
17442
17469
|
return true;
|
|
17443
17470
|
}
|
|
17471
|
+
if (arg.trim().toLowerCase() === "refresh") {
|
|
17472
|
+
openModelPicker({ refreshModels: true });
|
|
17473
|
+
return true;
|
|
17474
|
+
}
|
|
17444
17475
|
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
17476
|
return true;
|
|
17446
17477
|
case "remote": {
|
|
@@ -17453,7 +17484,7 @@ function createSlashDispatch({
|
|
|
17453
17484
|
openSearchPicker();
|
|
17454
17485
|
return true;
|
|
17455
17486
|
case "agents":
|
|
17456
|
-
openAgentsPicker();
|
|
17487
|
+
openAgentsPicker(arg.trim().toLowerCase() === "refresh" ? { refreshModels: true } : {});
|
|
17457
17488
|
return true;
|
|
17458
17489
|
case "workflow":
|
|
17459
17490
|
if (!arg) {
|
package/src/ui/statusline.mjs
CHANGED
|
@@ -210,16 +210,10 @@ function resolveContextUsedPct({
|
|
|
210
210
|
autoCompactTokenLimit,
|
|
211
211
|
compact,
|
|
212
212
|
});
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
// the boundary denominator, so it is bypassed in that case.
|
|
218
|
-
const trigger = num(autoCompactTokenLimit);
|
|
219
|
-
const triggerDenominator = trigger > 0 && (!(boundary > 0) || trigger < boundary);
|
|
220
|
-
if (triggerDenominator && numerator > 0) {
|
|
221
|
-
return (numerator / trigger) * 100;
|
|
222
|
-
}
|
|
213
|
+
// Boundary-only denominator: context % is always measured against the single
|
|
214
|
+
// compaction boundary value (shared with the gateway), never against a
|
|
215
|
+
// separate auto-compact trigger. This keeps the statusline and gateway
|
|
216
|
+
// display on the same denominator with no before/after trigger semantics.
|
|
223
217
|
const gatewayRawPct = gatewayStatus?.contextUsedPct;
|
|
224
218
|
if (
|
|
225
219
|
gatewayStatus
|
|
@@ -11,7 +11,10 @@ import {
|
|
|
11
11
|
readLatestGatewayHostRoute,
|
|
12
12
|
readGatewaySessionRoute,
|
|
13
13
|
} from '../src/gateway/session-routes.mjs';
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
compactBoundaryDenominator,
|
|
16
|
+
defaultEffectiveContextWindowPercent,
|
|
17
|
+
} from '../src/gateway/route-meta.mjs';
|
|
15
18
|
|
|
16
19
|
function positiveInt(value) {
|
|
17
20
|
const n = parseInt(String(value || ''), 10);
|
|
@@ -189,10 +192,6 @@ function boundedPercent(value, fallback = null) {
|
|
|
189
192
|
return fallback;
|
|
190
193
|
}
|
|
191
194
|
|
|
192
|
-
function defaultEffectiveContextWindowPercent(_provider) {
|
|
193
|
-
return 90;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
195
|
function routeContextMeta(provider, info = {}, inherited = {}) {
|
|
197
196
|
const rawContextWindow = firstPositiveWindow(
|
|
198
197
|
inherited.rawContextWindow,
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
estimateMessagesTokens,
|
|
10
10
|
estimateRequestReserveTokens,
|
|
11
11
|
} from '../../../../runtime/agent/orchestrator/session/context-utils.mjs';
|
|
12
|
+
import { DEFAULT_EFFECTIVE_CONTEXT_WINDOW_PERCENT } from '../../../../runtime/agent/orchestrator/session/compact/constants.mjs';
|
|
12
13
|
import { CLAUDE_CURRENT_MODE } from './claude-current.mjs';
|
|
13
14
|
|
|
14
15
|
const GATEWAY_USAGE_FILE = 'gateway-usage.local.json';
|
|
@@ -98,12 +99,12 @@ function boundedPercent(value, fallback = null) {
|
|
|
98
99
|
return fallback;
|
|
99
100
|
}
|
|
100
101
|
|
|
101
|
-
function defaultEffectiveContextWindowPercent(_provider) {
|
|
102
|
+
export function defaultEffectiveContextWindowPercent(_provider) {
|
|
102
103
|
// Gateway-routed models use catalog/provider context metadata (LiteLLM,
|
|
103
104
|
// models.dev, or native provider catalogs). Reserve a small universal
|
|
104
105
|
// headroom for output/tool/system tokens while keeping the raw model window
|
|
105
106
|
// visible separately.
|
|
106
|
-
return
|
|
107
|
+
return DEFAULT_EFFECTIVE_CONTEXT_WINDOW_PERCENT;
|
|
107
108
|
}
|
|
108
109
|
|
|
109
110
|
function effectiveContextWindowPercent(provider, info = {}, seed = {}) {
|