mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-26 03:03:04 +00:00
Prevent duplicate catalog choices from crashing agent drafts (#4839)
* fix(providers): keep duplicate catalogs from breaking agent drafts Normalize model identities before publication and when materializing older catalogs, preserving the first definition and configured overrides. Keep explicit default variants separate from the base model selection. * fix(providers): preserve saved variant identities across resumes Persist the selection encoding so legacy names remain literal. Refresh provider persistence when a selection changes, before another prompt is sent. * test(app): let fixtures own catalog journey cleanup * fix(providers): preserve the existing default variant contract Keep one Default catalog choice and let the provider resolve an omitted variant. Remove the encoding and persistence machinery; other variant IDs retain their literal meaning. * test(creation): expose failed completion after conflicting requests Creation returns a failed snapshot instead of rejecting. Assert the result before its side effects so platform failures report the underlying error. * test(app): wait for the seed client after daemon restart The browser and seed client reconnect independently. Wait for the seed connection before issuing post-restart RPCs instead of assuming browser readiness covers both clients. * test(app): isolate visible prompts and checkout diff fixtures Inactive tabs retain message DOM, so prompt assertions must inspect visible rows. Ignore the local fixture remote before observation rather than deleting it after the daemon has started tracking the checkout. * fix(file-observer): preserve final deletions when batching events A duplicate creation notification followed by deletion was collapsed into an update, hiding final absence from consumers that distinguish deletes. Keep final deletes while retaining update semantics for atomic replacement. * fix(desktop): wait for Metro HTTP readiness before launching Electron Expo briefly listens while probing port availability. A successful TCP connection can therefore launch Electron before Metro starts and leave it exiting on connection refusal. Require the configured renderer origin to report the running packager status. * test(ci): consolidate fixture and readiness corrections Await the connection and UI states each journey owns, isolate Git fixture contents before observation, and hold real Markdown frames at asserted checkpoints. Keep creation completion diagnostics explicit without claiming the original Windows failure is fixed.
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
import { test } from "../support/fixtures";
|
||||
import path from "node:path";
|
||||
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
|
||||
import { test, expect, type Page } from "../support/fixtures";
|
||||
import { connectDaemonClient } from "../support/helpers/daemon-client-loader";
|
||||
import {
|
||||
closeModelPicker,
|
||||
expectModelSearchEmptyState,
|
||||
@@ -15,7 +18,7 @@ import {
|
||||
expectSearchResultsVirtualized,
|
||||
} from "../support/helpers/agent-profiles";
|
||||
import { expectComposerVisible } from "../support/helpers/composer";
|
||||
import { clickNewChat, gotoWorkspace } from "../support/helpers/launcher";
|
||||
import { clickNewChat, clickNewTerminal, gotoWorkspace } from "../support/helpers/launcher";
|
||||
import { seedWorkspace } from "../support/helpers/seed-client";
|
||||
|
||||
const MOCK_PROVIDER_LABEL = "Mock Load Test";
|
||||
@@ -158,3 +161,110 @@ test.describe("Cross-provider model search", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
async function setRuntimeCatalog(client: DaemonClient, count: number, cwd: string) {
|
||||
await client.patchDaemonConfig({
|
||||
providers: {
|
||||
gemini: {
|
||||
extends: "acp",
|
||||
label: "Catalog provider",
|
||||
enabled: true,
|
||||
command: [
|
||||
process.execPath,
|
||||
path.resolve(__dirname, "../support/fixtures/catalog-acp.cjs"),
|
||||
String(count),
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
await expect
|
||||
.poll(
|
||||
async () =>
|
||||
(await client.getProvidersSnapshot({ cwd })).entries.find(
|
||||
(entry) => entry.provider === "gemini",
|
||||
)?.status,
|
||||
{ timeout: 30_000 },
|
||||
)
|
||||
.toBe("ready");
|
||||
}
|
||||
async function reloadSavedDraft(page: Page) {
|
||||
await page.evaluate(() =>
|
||||
localStorage.setItem(
|
||||
"@paseo:e2e-disable-default-seed-once",
|
||||
localStorage.getItem("@paseo:e2e-seed-nonce")!,
|
||||
),
|
||||
);
|
||||
await page.reload();
|
||||
await expectComposerVisible(page);
|
||||
}
|
||||
async function expectOneCatalogChoice(page: Page, label: string) {
|
||||
await openModelPicker(page);
|
||||
await page.getByRole("button", { name: "Back", exact: true }).click();
|
||||
await searchAllModels(page, label);
|
||||
await expect(page.getByTestId("model-row-gemini-gemini-3.5-flash")).toHaveCount(1);
|
||||
await closeModelPicker(page);
|
||||
}
|
||||
|
||||
async function configureModelOverride(client: DaemonClient, cwd: string) {
|
||||
await client.patchDaemonConfig({
|
||||
providers: {
|
||||
gemini: {
|
||||
additionalModels: [{ id: "gemini-3.5-flash", label: "Configured model", isDefault: true }],
|
||||
},
|
||||
},
|
||||
});
|
||||
await expect
|
||||
.poll(
|
||||
async () =>
|
||||
(await client.getProvidersSnapshot({ cwd: cwd })).entries.find(
|
||||
(entry) => entry.provider === "gemini",
|
||||
)?.models,
|
||||
{ timeout: 30_000 },
|
||||
)
|
||||
.toMatchObject([{ id: "gemini-3.5-flash", label: "Configured model", isDefault: true }]);
|
||||
}
|
||||
|
||||
const catalogTest = test.extend<{ catalogClient: DaemonClient }>({
|
||||
catalogClient: async ({ e2eWorker }, provide) => {
|
||||
void e2eWorker;
|
||||
const client = await connectDaemonClient<DaemonClient>({ clientIdPrefix: "catalog-models" });
|
||||
try {
|
||||
await provide(client);
|
||||
} finally {
|
||||
try {
|
||||
await client.patchDaemonConfig({ removeProviders: ["gemini"] });
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
catalogTest(
|
||||
"New Agent and saved drafts stay usable with repeated runtime model rows",
|
||||
async ({ page, withWorkspace, catalogClient: client }, testInfo) => {
|
||||
const workspace = await withWorkspace({ prefix: "catalog-models-" });
|
||||
await test.step("open a draft with a one-row runtime catalog", async () => {
|
||||
await setRuntimeCatalog(client, 1, workspace.repoPath);
|
||||
await gotoWorkspace(page, workspace.workspaceId);
|
||||
await expectComposerVisible(page);
|
||||
});
|
||||
await test.step("open New Agent after the provider publishes repeated model rows", async () => {
|
||||
await clickNewTerminal(page);
|
||||
await setRuntimeCatalog(client, 2, workspace.repoPath);
|
||||
await clickNewChat(page);
|
||||
await expectComposerVisible(page);
|
||||
await expectOneCatalogChoice(page, "Gemini 3.5 Flash");
|
||||
await page.screenshot({ path: testInfo.outputPath("duplicate-catalog-draft.png") });
|
||||
});
|
||||
await test.step("reopen the saved draft with the same catalog", async () => {
|
||||
await reloadSavedDraft(page);
|
||||
await expectOneCatalogChoice(page, "Gemini 3.5 Flash");
|
||||
});
|
||||
await test.step("retain configured model overrides when runtime IDs repeat", async () => {
|
||||
await configureModelOverride(client, workspace.repoPath);
|
||||
await reloadSavedDraft(page);
|
||||
await expectOneCatalogChoice(page, "Configured model");
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
const readline = require("node:readline");
|
||||
const model = { modelId: "gemini-3.5-flash", name: "Gemini 3.5 Flash" };
|
||||
readline.createInterface({ input: process.stdin }).on("line", (line) => {
|
||||
const request = JSON.parse(line);
|
||||
if (request.id === undefined) return;
|
||||
let result = {};
|
||||
if (request.method === "initialize")
|
||||
result = { protocolVersion: 1, agentCapabilities: {}, authMethods: [] };
|
||||
if (request.method === "session/new")
|
||||
result = {
|
||||
sessionId: "catalog-diagnostic",
|
||||
models: {
|
||||
currentModelId: model.modelId,
|
||||
availableModels: Array.from({ length: Number(process.argv[2]) }, () => model),
|
||||
},
|
||||
};
|
||||
process.stdout.write(JSON.stringify({ jsonrpc: "2.0", id: request.id, result }) + "\n");
|
||||
});
|
||||
@@ -130,6 +130,43 @@ function writeSnapshot(
|
||||
}
|
||||
|
||||
describe("provider snapshot cache", () => {
|
||||
it("normalizes duplicate model identities in live, legacy and persisted catalogs", async () => {
|
||||
const storage = createStorage();
|
||||
const entries = snapshotEntries("shared");
|
||||
const first = entries[0].models![0];
|
||||
entries[0].models!.push({ ...first, label: "Duplicate" });
|
||||
const compactSnapshot = compactProviderSnapshot(entries);
|
||||
const payload = { requestId: "catalog", generatedAt: "2026-09-01T00:00:00.000Z", entries };
|
||||
const cache = createProviderSnapshotCache(storage);
|
||||
const legacy = await cache.materialize("server-1", payload);
|
||||
expect(legacy.entries[0].models).toEqual([first]);
|
||||
const live = await cache.materialize("server-1", {
|
||||
...payload,
|
||||
snapshotHash: "old-hash",
|
||||
compactSnapshot,
|
||||
});
|
||||
expect(live.entries[0].models).toEqual([first]);
|
||||
expect(live.compactSnapshot).toEqual(compactSnapshot);
|
||||
await cache.write({
|
||||
serverId: "server-1",
|
||||
cwd: "/repo",
|
||||
hash: "old-hash",
|
||||
generatedAt: payload.generatedAt,
|
||||
compactSnapshot,
|
||||
});
|
||||
const restarted = createProviderSnapshotCache(storage);
|
||||
const saved = await restarted.read("server-1", "/repo");
|
||||
expect(saved?.entries[0].models).toEqual([first]);
|
||||
const unchanged = await restarted.materialize("server-1", {
|
||||
...payload,
|
||||
entries: [],
|
||||
snapshotHash: "old-hash",
|
||||
notModified: true,
|
||||
});
|
||||
expect(unchanged.entries[0].models).toEqual([first]);
|
||||
expect(unchanged.snapshotHash).toBe("old-hash");
|
||||
});
|
||||
|
||||
it("removes externally lost body references and retains independent orphan bodies on reconciliation", async () => {
|
||||
const storage = createStorage();
|
||||
const cache = createProviderSnapshotCache(storage);
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import { Buffer } from "buffer";
|
||||
import type { ProviderSnapshotEntry } from "@getpaseo/protocol/agent-types";
|
||||
import {
|
||||
normalizeAgentModelCatalog,
|
||||
type ProviderSnapshotEntry,
|
||||
} from "@getpaseo/protocol/agent-types";
|
||||
import {
|
||||
expandProviderSnapshot,
|
||||
type CompactProviderSnapshot,
|
||||
@@ -77,6 +80,17 @@ export class ProviderSnapshotCacheMissError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
// COMPAT(duplicateModelCatalog): added in v0.8.1, remove after 2027-03-14 once
|
||||
// the daemon floor is v0.8.1 and pre-fix cached catalogs are no longer supported.
|
||||
// Keep the original compact body/hash: normalization belongs to its decoded view.
|
||||
function normalizeCatalogs(entries: ProviderSnapshotEntry[]): ProviderSnapshotEntry[] {
|
||||
return entries.map((entry) => {
|
||||
if (!entry.models) return entry;
|
||||
const models = normalizeAgentModelCatalog(entry.models);
|
||||
return models === entry.models ? entry : { ...entry, models };
|
||||
});
|
||||
}
|
||||
|
||||
function cacheKey(serverId: string, cwd: string | null): string {
|
||||
return `${CACHE_KEY_PREFIX}:${JSON.stringify([serverId, "cwd", cwd])}`;
|
||||
}
|
||||
@@ -303,7 +317,7 @@ export function createProviderSnapshotCache(
|
||||
const body = {
|
||||
...stored,
|
||||
compactSnapshot: stored.compactSnapshot,
|
||||
entries: expandProviderSnapshot(stored.compactSnapshot),
|
||||
entries: normalizeCatalogs(expandProviderSnapshot(stored.compactSnapshot)),
|
||||
};
|
||||
bodies.set(key, body);
|
||||
return body;
|
||||
@@ -328,7 +342,7 @@ export function createProviderSnapshotCache(
|
||||
return {
|
||||
async materialize(serverId, snapshot) {
|
||||
const { snapshotHash: hash, compactSnapshot } = snapshot;
|
||||
if (!hash) return snapshot;
|
||||
if (!hash) return { ...snapshot, entries: normalizeCatalogs(snapshot.entries) };
|
||||
let body = await safely(async () => {
|
||||
const cached = await readBody(serverId, hash);
|
||||
if (cached) return cached;
|
||||
@@ -348,7 +362,7 @@ export function createProviderSnapshotCache(
|
||||
hash,
|
||||
generatedAt: snapshot.generatedAt,
|
||||
compactSnapshot,
|
||||
entries: expandProviderSnapshot(compactSnapshot),
|
||||
entries: normalizeCatalogs(expandProviderSnapshot(compactSnapshot)),
|
||||
};
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -102,6 +102,17 @@ export function normalizeAgentModelDefinition(model: AgentModelDefinition): Agen
|
||||
return { ...model, defaultThinkingOptionId };
|
||||
}
|
||||
|
||||
/** A provider addresses models by ID; repeated rows retain the first definition. */
|
||||
export function normalizeAgentModelCatalog(models: AgentModelDefinition[]): AgentModelDefinition[] {
|
||||
const ids = new Set<string>();
|
||||
const unique = models.filter((model) => {
|
||||
if (ids.has(model.id)) return false;
|
||||
ids.add(model.id);
|
||||
return true;
|
||||
});
|
||||
return unique.length === models.length ? models : unique;
|
||||
}
|
||||
|
||||
export interface ProviderSnapshotEntry {
|
||||
provider: AgentProvider;
|
||||
status: ProviderStatus;
|
||||
|
||||
@@ -138,6 +138,79 @@ async function runTestCatalogActivities(
|
||||
}
|
||||
|
||||
describe("ProviderSnapshotManager public surface", () => {
|
||||
test("publishes one model per provider identity and retains the first definition", async () => {
|
||||
const first = { provider: "codex", id: "shared", label: "First", isDefault: true };
|
||||
const manager = new ProviderSnapshotManager({
|
||||
logger: createTestLogger(),
|
||||
extraClients: {
|
||||
codex: createExtraClient("codex", {
|
||||
isAvailable: async () => true,
|
||||
fetchCatalog: async () => ({
|
||||
models: [first, { ...first, label: "Duplicate" }],
|
||||
modes: [],
|
||||
}),
|
||||
}),
|
||||
claude: createExtraClient("claude", {
|
||||
isAvailable: async () => true,
|
||||
fetchCatalog: async () => ({ models: [{ ...first, provider: "claude" }], modes: [] }),
|
||||
}),
|
||||
},
|
||||
});
|
||||
try {
|
||||
const codex = await manager.getProvider({ provider: "codex", wait: true });
|
||||
const claude = await manager.getProvider({ provider: "claude", wait: true });
|
||||
expect(codex.models).toEqual([first]);
|
||||
expect(claude.models).toEqual([{ ...first, provider: "claude" }]);
|
||||
} finally {
|
||||
manager.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
test("normalizes plugin model catalogs at the same publication boundary", async () => {
|
||||
const manager = new ProviderSnapshotManager({ logger: createTestLogger() });
|
||||
manager.replacePluginProviders([
|
||||
{
|
||||
id: "catalog-plugin",
|
||||
label: "Catalog plugin",
|
||||
async connect() {
|
||||
let listener: ((event: ProviderEvent) => void) | undefined;
|
||||
return {
|
||||
version: 1,
|
||||
capabilities: [],
|
||||
async send(input) {
|
||||
if (input.type === "catalog")
|
||||
listener?.({
|
||||
type: "catalog",
|
||||
requestId: input.requestId,
|
||||
catalog: {
|
||||
models: [
|
||||
{ id: "same", label: "First" },
|
||||
{ id: "same", label: "Second" },
|
||||
],
|
||||
modes: [],
|
||||
},
|
||||
});
|
||||
},
|
||||
onEvent(next) {
|
||||
listener = next;
|
||||
return () => {
|
||||
listener = undefined;
|
||||
};
|
||||
},
|
||||
async close() {},
|
||||
};
|
||||
},
|
||||
},
|
||||
]);
|
||||
try {
|
||||
const entry = await manager.getProvider({ provider: "catalog-plugin", wait: true });
|
||||
expect(entry.models).toHaveLength(1);
|
||||
expect(entry.models?.[0]).toMatchObject({ id: "same", label: "First" });
|
||||
} finally {
|
||||
manager.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
test("carries a plugin provider icon in snapshot metadata", () => {
|
||||
const iconSvg = '<svg viewBox="0 0 24 24"><path d="M4 4h16v16H4z" /></svg>';
|
||||
const registration: ProviderRegistration = {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { normalizeAgentModelCatalog } from "@getpaseo/protocol/agent-types";
|
||||
import { createHash } from "node:crypto";
|
||||
import { EventEmitter } from "node:events";
|
||||
import { homedir } from "node:os";
|
||||
@@ -1021,13 +1022,20 @@ export class ProviderSnapshotManager {
|
||||
return;
|
||||
}
|
||||
|
||||
const models = normalizeAgentModelCatalog(catalog.models);
|
||||
if (models.length !== catalog.models.length) {
|
||||
this.logger.warn(
|
||||
{ provider, discardedRows: catalog.models.length - models.length },
|
||||
"Provider catalog contains repeated model IDs; retaining the first definition",
|
||||
);
|
||||
}
|
||||
setEntry({
|
||||
...base,
|
||||
defaultModeId:
|
||||
catalog.defaultModeId === undefined ? base.defaultModeId : catalog.defaultModeId,
|
||||
status: "ready",
|
||||
enabled: true,
|
||||
models: catalog.models,
|
||||
models,
|
||||
modes: catalog.modes,
|
||||
fetchedAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
@@ -612,6 +612,115 @@ describe("OpenCodeAgentClient adapter smoke tests", () => {
|
||||
rmSync(cwd, { recursive: true, force: true });
|
||||
}, 120_000);
|
||||
|
||||
test.each([
|
||||
{ name: "absent", variants: {} },
|
||||
{ name: "empty", variants: { default: {} } },
|
||||
{ name: "configured", variants: { default: { reasoningEffort: "low" } } },
|
||||
])("catalog exposes one Default when upstream default is $name", async ({ variants }) => {
|
||||
const runtime = new TestOpenCodeHarness();
|
||||
const openCodeClient = new TestOpenCodeClient();
|
||||
openCodeClient.providerListResponse = {
|
||||
data: {
|
||||
connected: ["catalog-provider"],
|
||||
all: [
|
||||
{
|
||||
id: "catalog-provider",
|
||||
name: "Catalog provider",
|
||||
source: "api",
|
||||
models: {
|
||||
model: {
|
||||
name: "Variant model",
|
||||
variants: { ...variants, high: {}, "variant:default": {} },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
runtime.enqueueClient(openCodeClient);
|
||||
const cwd = tmpCwd();
|
||||
try {
|
||||
const client = new OpenCodeAgentClient(logger, undefined, {
|
||||
serverManager: runtime,
|
||||
createClient: runtime.createClient,
|
||||
resolveHomeDir: () => cwd,
|
||||
});
|
||||
const catalog = await client.fetchCatalog({ scope: "global", force: false });
|
||||
const options = catalog.models[0].thinkingOptions ?? [];
|
||||
expect(options).toEqual([
|
||||
{ id: "default", label: "Default", isDefault: true },
|
||||
{ id: "high", label: "high" },
|
||||
{ id: "variant:default", label: "variant:default" },
|
||||
]);
|
||||
expect(catalog.models[0].defaultThinkingOptionId).toBe("default");
|
||||
const execution = new TestOpenCodeClient();
|
||||
execution.sessionCreateResponse = { data: { id: "ses_catalog_variants" } };
|
||||
execution.sessionPromptAsyncEvents = [
|
||||
{ type: "session.idle", properties: { sessionID: "ses_catalog_variants" } },
|
||||
];
|
||||
runtime.enqueueClient(execution);
|
||||
const session = await client.createSession({
|
||||
provider: "opencode",
|
||||
cwd,
|
||||
model: "catalog-provider/model",
|
||||
thinkingOptionId: "default",
|
||||
});
|
||||
try {
|
||||
await collectTurnEvents(streamSession(session, "Use OpenCode's default"));
|
||||
await session.setThinkingOption!("high");
|
||||
await collectTurnEvents(streamSession(session, "Use high"));
|
||||
await session.setThinkingOption!("variant:default");
|
||||
await collectTurnEvents(streamSession(session, "Use the literal variant name"));
|
||||
await session.setThinkingOption!("default");
|
||||
await collectTurnEvents(streamSession(session, "Return to OpenCode's default"));
|
||||
expect(execution.calls.sessionPromptAsync).toEqual([
|
||||
expect.not.objectContaining({ variant: expect.anything() }),
|
||||
expect.objectContaining({ variant: "high" }),
|
||||
expect.objectContaining({ variant: "variant:default" }),
|
||||
expect.not.objectContaining({ variant: expect.anything() }),
|
||||
]);
|
||||
} finally {
|
||||
await session.close();
|
||||
}
|
||||
} finally {
|
||||
rmSync(cwd, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test.each([
|
||||
{ saved: "default", variant: undefined },
|
||||
{ saved: "high", variant: "high" },
|
||||
{ saved: "variant:default", variant: "variant:default" },
|
||||
])("resume preserves saved thinking choice $saved", async ({ saved, variant }) => {
|
||||
const runtime = new TestOpenCodeHarness();
|
||||
const execution = new TestOpenCodeClient();
|
||||
execution.sessionPromptAsyncEvents = [
|
||||
{ type: "session.idle", properties: { sessionID: "ses_saved_variant" } },
|
||||
];
|
||||
runtime.enqueueClient(execution);
|
||||
const client = new OpenCodeAgentClient(logger, undefined, {
|
||||
serverManager: runtime,
|
||||
createClient: runtime.createClient,
|
||||
});
|
||||
const cwd = tmpCwd();
|
||||
try {
|
||||
const session = await client.resumeSession(
|
||||
{ provider: "opencode", sessionId: "ses_saved_variant", metadata: { cwd } },
|
||||
{ thinkingOptionId: saved },
|
||||
);
|
||||
try {
|
||||
await collectTurnEvents(streamSession(session, "Keep my saved choice"));
|
||||
expect(execution.calls.sessionPromptAsync.map((request) => request.variant)).toEqual([
|
||||
variant,
|
||||
]);
|
||||
} finally {
|
||||
await session.close();
|
||||
}
|
||||
} finally {
|
||||
rmSync(cwd, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("fetchCatalog returns models with required fields", async () => {
|
||||
const runtime = new TestOpenCodeHarness();
|
||||
const openCodeClient = new TestOpenCodeClient();
|
||||
|
||||
@@ -806,11 +806,14 @@ function buildOpenCodeModelDefinition(
|
||||
},
|
||||
): AgentModelDefinition {
|
||||
const rawVariants = model.variants ? Object.keys(model.variants) : [];
|
||||
// OpenCode lists only overrides; its base model behavior is selected by omitting `variant`.
|
||||
// Like OpenCode's web UI, Default omits `variant` and lets OpenCode resolve it.
|
||||
// Reserve that choice instead of exposing a second upstream `default` entry.
|
||||
const thinkingOptions = rawVariants.length
|
||||
? [
|
||||
{ id: OPENCODE_DEFAULT_VARIANT_ID, label: "Default", isDefault: true },
|
||||
...rawVariants.map((id) => ({ id, label: id })),
|
||||
...rawVariants
|
||||
.filter((id) => id !== OPENCODE_DEFAULT_VARIANT_ID)
|
||||
.map((id) => ({ id, label: id })),
|
||||
]
|
||||
: [];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user