Files
fusion/plugins/fusion-plugin-cursor-runtime/src/process-manager.ts
gsxdsm 3e7e4a865b FN-7700: enrich Cursor model picker with reasoning/contextWindow metadata
Threads optional reasoning and context-window metadata from Cursor CLI model discovery through to the dashboard's Cursor model picker, replacing hardcoded false/0 defaults with pass-through values when the CLI reports them.
- Extend cursorDiscoveryToModels/discoverCursorProviderModels to carry reasoning/contextWindow from structured JSON model entries
- Update runtime-provider-probes.ts to surface the new metadata fields
- Update cursor-agent process-manager and provider to parse and propagate reasoning/contextWindow from CLI output
- Add/extend tests covering the new metadata plumbing in cursor-model-cache, process-manager, and provider
- Add changeset documenting the patch-level dashboard feature

Files changed:
 .changeset/fn-7700-cursor-picker-reasoning-context-window.md      |  7 ++++
 packages/dashboard/src/__tests__/cursor-model-cache.test.ts       | 26 ++++++++++++
 packages/dashboard/src/cursor-model-cache.ts                      | 24 +++++++----
 packages/dashboard/src/runtime-provider-probes.ts                 | 11 ++++-
 plugins/fusion-plugin-cursor-runtime/src/__tests__/process-manager.test.ts | 33 +++++++++++++++
 plugins/fusion-plugin-cursor-runtime/src/__tests__/provider.test.ts       | 23 +++++++++++
 plugins/fusion-plugin-cursor-runtime/src/process-manager.ts               | 48 +++++++++++++++++++---
 plugins/fusion-plugin-cursor-runtime/src/provider.ts                      | 19 ++++++++-
 8 files changed, 176 insertions(+), 15 deletions(-)

Fusion-Task-Id: FN-7700

Fusion-Task-Lineage: 6b371a92-204a-4201-8c7d-df65e9210a1b

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-08 23:39:34 -07:00

109 lines
4.3 KiB
TypeScript

import { runCursorCommand } from "./cli-spawn.js";
const EMPTY_ACCOUNT_MESSAGE = "no models available for this account.";
/*
FNXC:CursorCli 2026-07-08-00:00:
`cursor-agent models` is plain text, NOT JSON — the real CLI rejects `--json`
(`error: unknown option '--json'`) and has no `model list` subcommand. Output
shape is: an `Available models` header line, a blank line, then one model per
line as `<id> - <Label>` (e.g. `auto - Auto (default)`), ending with a
`Tip: use --model <id> ...` line. An empty account prints the single line
`No models available for this account.`. We strip the header/tip/empty-state
lines and take the text before the first ` - ` as the bare model id.
*/
function parseModelLines(raw: string): string[] {
const ids = raw
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean)
.filter((line) => line.toLowerCase() !== "available models")
.filter((line) => line.toLowerCase() !== EMPTY_ACCOUNT_MESSAGE)
.filter((line) => !/^tip:/i.test(line))
.filter((line) => !line.toLowerCase().startsWith("usage"))
.map((line) => {
const separatorIndex = line.indexOf(" - ");
return separatorIndex === -1 ? line : line.slice(0, separatorIndex).trim();
})
.filter(Boolean);
return Array.from(new Set(ids));
}
/** Optional per-model metadata captured only from structured (JSON) discovery entries. */
export interface CursorModelMeta {
reasoning?: boolean;
contextWindow?: number;
}
export interface CursorModelDiscoveryResult {
models: string[];
source: string;
fallbackUsed: boolean;
reason?: string;
/**
* FNXC:CursorCli 2026-07-08-00:00:
* FN-7700: optional per-id `reasoning`/`contextWindow` metadata, populated
* ONLY when the defensive JSON-tolerant discovery path parses object
* entries carrying those fields. The real, plain-text `cursor-agent models`
* output (source `models-text`/`none`) never populates this map — metadata
* is structured-source pass-through only, never parsed from the free-text
* `<id> - <Label>` lines and never fabricated. Additive alongside the
* existing `models: string[]` bare-id contract; omitted entirely when no
* entry carried any metadata.
*/
modelMeta?: Record<string, CursorModelMeta>;
}
export async function discoverCursorModels(binary: string, timeoutMs = 5000): Promise<CursorModelDiscoveryResult> {
const res = await runCursorCommand(binary, ["models"], timeoutMs);
if (res.code !== 0) {
return { models: [], source: "none", fallbackUsed: true, reason: "model discovery command unavailable" };
}
const output = (res.stdout || "").trim();
if (!output) {
return { models: [], source: "none", fallbackUsed: true, reason: "model discovery command returned no output" };
}
if (output.toLowerCase() === EMPTY_ACCOUNT_MESSAGE) {
return { models: [], source: "models-text", fallbackUsed: false, reason: "no models available for this account" };
}
// Defensive fallback: tolerate output that happens to be JSON, even though
// the real CLI does not support --json today.
try {
const parsed = JSON.parse(output);
if (Array.isArray(parsed)) {
const ids: string[] = [];
const modelMeta: Record<string, CursorModelMeta> = {};
for (const entry of parsed) {
const id = typeof entry === "string" ? entry : typeof entry?.id === "string" ? entry.id : undefined;
if (!id) continue;
ids.push(id);
if (entry && typeof entry === "object") {
const meta: CursorModelMeta = {};
if (typeof entry.reasoning === "boolean") meta.reasoning = entry.reasoning;
if (typeof entry.contextWindow === "number") meta.contextWindow = entry.contextWindow;
if (Object.keys(meta).length > 0) modelMeta[id] = meta;
}
}
if (ids.length > 0) {
const result: CursorModelDiscoveryResult = { models: Array.from(new Set(ids)), source: "models-json", fallbackUsed: false };
if (Object.keys(modelMeta).length > 0) result.modelMeta = modelMeta;
return result;
}
}
} catch {
// output is not JSON; fall through to line-based parsing
}
const ids = parseModelLines(output);
if (ids.length > 0) {
return { models: ids, source: "models-text", fallbackUsed: false };
}
return { models: [], source: "none", fallbackUsed: true, reason: "model discovery command unavailable" };
}