diff --git a/apps/desktop/src/main/ipc-methods.ts b/apps/desktop/src/main/ipc-methods.ts index fb40519b..4b49703c 100644 --- a/apps/desktop/src/main/ipc-methods.ts +++ b/apps/desktop/src/main/ipc-methods.ts @@ -40,6 +40,7 @@ export const IPC_METHODS = [ 'attachments.read', 'providers.detect', 'providers.models', + 'providers.efforts', 'providers.plan', 'providers.install', 'providers.cancelInstall', diff --git a/apps/desktop/src/main/rpc.ts b/apps/desktop/src/main/rpc.ts index b3d51877..5c888103 100644 --- a/apps/desktop/src/main/rpc.ts +++ b/apps/desktop/src/main/rpc.ts @@ -207,6 +207,66 @@ async function probeAcpModels( } } +/** + * Thought/reasoning levels for one kind resolved against a specific model. + * Effort is per-model on some agents (OpenCode's `effort` option appears + * only for reasoning-capable models), so the picker asks with the selected + * model rather than trusting the default-model catalog probe. Throws on + * transport failure so the caller keeps its catalog fallback. + */ +async function probeEffortsForModel( + kind: DriverKind, + modelId: string | null | undefined, +): Promise<{ id: string; label: string; description?: string; current?: boolean }[]> { + const { detectDriver } = await import('@ari/providers/detector') + const environment = await resolveDetectionEnvironment() + const detection = await detectDriver(kind, environment) + if (!detection.binaryPath) return [] + if (process.env['ARI_ACP'] === '0') return [] + const launch = resolveAcpLaunch(kind, { cliBinaryPath: detection.binaryPath }, environment) + if (launch === null) return [] + const { AcpConnection } = await import('@ari/providers/acp/connection') + const probeEnv = processEnvWithPath(environment.pathEnv) + const probe = probeLaunch(launch) + const connection = await AcpConnection.connect({ + launch: probe, + cwd: homedir(), + initializeTimeoutMs: 20_000, + runtimeEnv: { ...probeEnv, ...probe.env }, + }) + try { + const created = await connection.newSession(homedir()) + let configOptions = created.configOptions ?? [] + if (typeof modelId === 'string' && modelId.length > 0) { + const modelOption = configOptions.find((o) => o.category === 'model' && o.type === 'select') + const offered = modelOption?.options?.some((v) => v.value === modelId) === true + if (modelOption?.id !== undefined && offered) { + const refreshed = await connection.setConfigOption( + created.sessionId as string, + modelOption.id, + modelId, + ) + if (refreshed !== null && refreshed !== undefined && refreshed.length > 0) { + configOptions = refreshed + } + } + } + const catalog = mergeEffortCatalogs( + thoughtEffortsFromSession({ ...created, configOptions }), + thoughtEffortsFromMeta(connection.initialize._meta), + ) + if (catalog.options.length > 0) setDynamicEfforts(kind, catalog) + return catalog.options.map((option) => ({ + id: option.id, + label: option.label, + ...(option.description !== undefined ? { description: option.description } : {}), + ...(catalog.currentId === option.id ? { current: true as const } : {}), + })) + } finally { + await connection.shutdown() + } +} + /** * Every CLI kind probed by `providers.detect`, regardless of hydration state, * so the providers grid is complete even while drivers are still registering. @@ -1073,6 +1133,22 @@ export function registerRpc(contents: WebContents, options: RegisterRpcOptions = }) }) + // Model-aware effort lookup for the picker: a throwaway ACP session has + // the selected model applied, then reports that model's own thought + // levels. Empty on any failure — the caller keeps the catalog fallback. + r.register('providers.efforts', async (params) => { + try { + const efforts = await probeEffortsForModel(params.kind, params.modelId ?? null) + return { efforts } + } catch (error) { + log.debug('model-specific effort probe failed; keeping catalog fallback', { + kind: params.kind, + error: String(error), + }) + return { efforts: [] } + } + }) + r.register('window.minimize', () => { BrowserWindow.fromWebContents(contents)?.minimize() return { done: true } diff --git a/apps/desktop/src/renderer/src/features/composer/ModelSelector.tsx b/apps/desktop/src/renderer/src/features/composer/ModelSelector.tsx index 4f7a32a4..388255df 100644 --- a/apps/desktop/src/renderer/src/features/composer/ModelSelector.tsx +++ b/apps/desktop/src/renderer/src/features/composer/ModelSelector.tsx @@ -580,8 +580,8 @@ export function ModelSelector({ {!searching && fallbackLabel !== null && activeKind !== null ? (
- {driverLabel(activeKind)} has not reported its own models — this is{' '} - {fallbackLabel}. It may not accept every entry. + {driverLabel(activeKind)} has not reported its own models — this is {fallbackLabel}. + It may not accept every entry.