From 0a34ce366d849586ddd7d1aad44c40919e53a335 Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Wed, 1 Apr 2026 19:02:52 -0700 Subject: [PATCH 1/8] Fix model selection lost during task creation --- apps/code/src/main/services/agent/schemas.ts | 1 + apps/code/src/main/services/agent/service.ts | 6 ++++ .../sessions/hooks/useSessionConnection.ts | 18 +++++------- .../features/sessions/service/service.ts | 28 ++----------------- .../agent/src/adapters/claude/claude-agent.ts | 11 ++++++-- packages/agent/src/adapters/claude/types.ts | 2 ++ 6 files changed, 26 insertions(+), 40 deletions(-) diff --git a/apps/code/src/main/services/agent/schemas.ts b/apps/code/src/main/services/agent/schemas.ts index dafcaf169..97d7648ec 100644 --- a/apps/code/src/main/services/agent/schemas.ts +++ b/apps/code/src/main/services/agent/schemas.ts @@ -49,6 +49,7 @@ export const startSessionInput = z.object({ additionalDirectories: z.array(z.string()).optional(), customInstructions: z.string().max(2000).optional(), effort: effortLevelSchema.optional(), + model: z.string().optional(), }); export type StartSessionInput = z.infer; diff --git a/apps/code/src/main/services/agent/service.ts b/apps/code/src/main/services/agent/service.ts index 9d64c2ac9..c3a2e3f8b 100644 --- a/apps/code/src/main/services/agent/service.ts +++ b/apps/code/src/main/services/agent/service.ts @@ -189,6 +189,8 @@ interface SessionConfig { customInstructions?: string; /** Effort level for Claude sessions */ effort?: EffortLevel; + /** Model to use for the session (e.g. "claude-sonnet-4-6") */ + model?: string; } interface ManagedSession { @@ -465,6 +467,7 @@ export class AgentService extends TypedEventEmitter { permissionMode, customInstructions, effort, + model, } = config; // Preview sessions don't need a real repo — use a temp directory @@ -638,6 +641,7 @@ export class AgentService extends TypedEventEmitter { sessionId: existingSessionId, systemPrompt, ...(permissionMode && { permissionMode }), + ...(model && { model }), claudeCode: { options: { ...(additionalDirectories?.length && { @@ -669,6 +673,7 @@ export class AgentService extends TypedEventEmitter { taskRunId, systemPrompt, ...(permissionMode && { permissionMode }), + ...(model && { model }), claudeCode: { options: { ...(additionalDirectories?.length && { additionalDirectories }), @@ -1362,6 +1367,7 @@ For git operations while detached: customInstructions: "customInstructions" in params ? params.customInstructions : undefined, effort: "effort" in params ? params.effort : undefined, + model: "model" in params ? params.model : undefined, }; } diff --git a/apps/code/src/renderer/features/sessions/hooks/useSessionConnection.ts b/apps/code/src/renderer/features/sessions/hooks/useSessionConnection.ts index 1c8eab208..cb7e20789 100644 --- a/apps/code/src/renderer/features/sessions/hooks/useSessionConnection.ts +++ b/apps/code/src/renderer/features/sessions/hooks/useSessionConnection.ts @@ -86,28 +86,24 @@ export function useSessionConnection({ return; } - connectingTasks.add(taskId); - - const isNewSession = !task.latest_run?.id; - const hasInitialPrompt = isNewSession && task.description; + // New sessions (no latest_run) are handled by the task creation saga, + // which passes model/adapter/executionMode. Only reconnect existing ones here. + if (!task.latest_run?.id) return; - if (hasInitialPrompt) { - markActivity(task.id); - } + connectingTasks.add(taskId); - log.info("Connecting to task session", { + log.info("Reconnecting to existing task session", { taskId: task.id, hasLatestRun: !!task.latest_run, sessionStatus: session?.status ?? "none", }); + markActivity(task.id); + getSessionService() .connectToTask({ task, repoPath, - initialPrompt: hasInitialPrompt - ? [{ type: "text", text: task.description }] - : undefined, }) .finally(() => { connectingTasks.delete(taskId); diff --git a/apps/code/src/renderer/features/sessions/service/service.ts b/apps/code/src/renderer/features/sessions/service/service.ts index 5eb919f47..b4a9c16eb 100644 --- a/apps/code/src/renderer/features/sessions/service/service.ts +++ b/apps/code/src/renderer/features/sessions/service/service.ts @@ -536,6 +536,7 @@ export class SessionService { const { customInstructions: startCustomInstructions } = useSettingsStore.getState(); + const preferredModel = model ?? DEFAULT_GATEWAY_MODEL; const result = await trpcClient.agent.start.mutate({ taskId, taskRunId: taskRun.id, @@ -548,6 +549,7 @@ export class SessionService { effort: effortLevelSchema.safeParse(reasoningLevel).success ? (reasoningLevel as EffortLevel) : undefined, + model: preferredModel || undefined, }); const session = this.createBaseSession(taskRun.id, taskId, taskTitle); @@ -586,32 +588,6 @@ export class SessionService { adapter, }); - const preferredModel = model ?? DEFAULT_GATEWAY_MODEL; - const configPromises: Promise[] = []; - if (preferredModel) { - configPromises.push( - this.setSessionConfigOptionByCategory( - taskId, - "model", - preferredModel, - ).catch((err) => log.warn("Failed to set model", { taskId, err })), - ); - } - if (reasoningLevel) { - configPromises.push( - this.setSessionConfigOptionByCategory( - taskId, - "thought_level", - reasoningLevel, - ).catch((err) => - log.warn("Failed to set reasoning level", { taskId, err }), - ), - ); - } - if (configPromises.length > 0) { - await Promise.all(configPromises); - } - if (initialPrompt?.length) { await this.sendPrompt(taskId, initialPrompt); } diff --git a/packages/agent/src/adapters/claude/claude-agent.ts b/packages/agent/src/adapters/claude/claude-agent.ts index f4a036857..59b832ea7 100644 --- a/packages/agent/src/adapters/claude/claude-agent.ts +++ b/packages/agent/src/adapters/claude/claude-agent.ts @@ -919,15 +919,20 @@ export class ClaudeAcpAgent extends BaseAcpAgent { }); } - // Resolve model: settings model takes priority, then gateway + // Resolve model: settings model takes priority, then meta model, then gateway default const settingsModel = settingsManager.getSettings().model; - const modelOptions = await this.getModelConfigOptions(); - const resolvedModelId = settingsModel || modelOptions.currentModelId; + const metaModel = meta?.model; + const modelOptions = await this.getModelConfigOptions( + settingsModel || metaModel || undefined, + ); + const resolvedModelId = + settingsModel || metaModel || modelOptions.currentModelId; session.modelId = resolvedModelId; session.lastContextWindowSize = this.getContextWindowForModel(resolvedModelId); const resolvedSdkModel = toSdkModelId(resolvedModelId); + if (!isResume && resolvedSdkModel !== DEFAULT_MODEL) { await this.session.query.setModel(resolvedSdkModel); } diff --git a/packages/agent/src/adapters/claude/types.ts b/packages/agent/src/adapters/claude/types.ts index 02ea340d9..8bc8cce35 100644 --- a/packages/agent/src/adapters/claude/types.ts +++ b/packages/agent/src/adapters/claude/types.ts @@ -108,6 +108,8 @@ export type NewSessionMeta = { persistence?: { taskId?: string; runId?: string; logUrl?: string }; additionalRoots?: string[]; allowedDomains?: string[]; + /** Model ID to use for this session (e.g. "claude-sonnet-4-6") */ + model?: string; claudeCode?: { options?: Options; }; From 88fa2ac3f1bcb3b6c857a1905f28a16221b011a3 Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Wed, 1 Apr 2026 19:11:55 -0700 Subject: [PATCH 2/8] Parallelize SDK initialization with model config fetch --- .../agent/src/adapters/claude/claude-agent.ts | 114 +++++++++++------- 1 file changed, 72 insertions(+), 42 deletions(-) diff --git a/packages/agent/src/adapters/claude/claude-agent.ts b/packages/agent/src/adapters/claude/claude-agent.ts index 59b832ea7..0a2527b57 100644 --- a/packages/agent/src/adapters/claude/claude-agent.ts +++ b/packages/agent/src/adapters/claude/claude-agent.ts @@ -876,55 +876,85 @@ export class ClaudeAcpAgent extends BaseAcpAgent { { sessionId, taskId, taskRunId: meta?.taskRunId }, ); - try { - const result = await withTimeout( - q.initializationResult(), - SESSION_VALIDATION_TIMEOUT_MS, - ); - if (result.result === "timeout") { - throw new Error( - `Session ${isResume ? (forkSession ? "fork" : "resumption") : "initialization"} timed out for sessionId=${sessionId}`, + if (isResume) { + // Resume must block on initialization to validate the session is still alive. + // For stale sessions this throws (e.g. "No conversation found"). + try { + const result = await withTimeout( + q.initializationResult(), + SESSION_VALIDATION_TIMEOUT_MS, ); + if (result.result === "timeout") { + throw new Error( + `Session ${forkSession ? "fork" : "resumption"} timed out for sessionId=${sessionId}`, + ); + } + } catch (err) { + settingsManager.dispose(); + if ( + err instanceof Error && + err.message === "Query closed before response received" + ) { + throw RequestError.resourceNotFound(sessionId); + } + this.logger.error( + forkSession ? "Session fork failed" : "Session resumption failed", + { + sessionId, + taskId, + taskRunId: meta?.taskRunId, + error: err instanceof Error ? err.message : String(err), + }, + ); + throw err; } - } catch (err) { - settingsManager.dispose(); - if ( - isResume && - err instanceof Error && - err.message === "Query closed before response received" - ) { - throw RequestError.resourceNotFound(sessionId); - } - this.logger.error( - isResume - ? forkSession - ? "Session fork failed" - : "Session resumption failed" - : "Session initialization failed", - { - sessionId, - taskId, - taskRunId: meta?.taskRunId, - error: err instanceof Error ? err.message : String(err), - }, - ); - throw err; } - if (meta?.taskRunId) { - await this.client.extNotification("_posthog/sdk_session", { - taskRunId: meta.taskRunId, - sessionId, - adapter: "claude", - }); - } + // Fetch model config in parallel with SDK initialization for new sessions. + // The gateway REST call doesn't depend on the SDK being ready. + const [modelOptions] = await Promise.all([ + this.getModelConfigOptions( + settingsManager.getSettings().model || meta?.model || undefined, + ), + // For new sessions, await initialization concurrently with model fetch. + // SDK starts in the background via query() — we just need it ready + // before the first prompt, not before returning configOptions. + ...(!isResume + ? [ + withTimeout(q.initializationResult(), SESSION_VALIDATION_TIMEOUT_MS) + .then((result) => { + if (result.result === "timeout") { + this.logger.error("Session initialization timed out", { + sessionId, + taskId, + taskRunId: meta?.taskRunId, + }); + } + }) + .catch((err) => { + this.logger.error("Session initialization failed", { + sessionId, + taskId, + taskRunId: meta?.taskRunId, + error: err instanceof Error ? err.message : String(err), + }); + }), + ] + : []), + // Fire notification in parallel too + ...(meta?.taskRunId + ? [ + this.client.extNotification("_posthog/sdk_session", { + taskRunId: meta.taskRunId, + sessionId, + adapter: "claude", + }), + ] + : []), + ]); - // Resolve model: settings model takes priority, then meta model, then gateway default const settingsModel = settingsManager.getSettings().model; const metaModel = meta?.model; - const modelOptions = await this.getModelConfigOptions( - settingsModel || metaModel || undefined, - ); const resolvedModelId = settingsModel || metaModel || modelOptions.currentModelId; session.modelId = resolvedModelId; From ff2787dc258ec849ce257fc00cfc0c418f7fcf26 Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Wed, 1 Apr 2026 21:42:31 -0700 Subject: [PATCH 3/8] Throw on new session init failure instead of swallowing --- apps/code/src/main/services/agent/service.ts | 4 +- .../features/sessions/service/service.ts | 2 +- .../agent/src/adapters/claude/claude-agent.ts | 55 +++++++++---------- 3 files changed, 30 insertions(+), 31 deletions(-) diff --git a/apps/code/src/main/services/agent/service.ts b/apps/code/src/main/services/agent/service.ts index c3a2e3f8b..409632d66 100644 --- a/apps/code/src/main/services/agent/service.ts +++ b/apps/code/src/main/services/agent/service.ts @@ -641,7 +641,7 @@ export class AgentService extends TypedEventEmitter { sessionId: existingSessionId, systemPrompt, ...(permissionMode && { permissionMode }), - ...(model && { model }), + ...(model != null && { model }), claudeCode: { options: { ...(additionalDirectories?.length && { @@ -673,7 +673,7 @@ export class AgentService extends TypedEventEmitter { taskRunId, systemPrompt, ...(permissionMode && { permissionMode }), - ...(model && { model }), + ...(model != null && { model }), claudeCode: { options: { ...(additionalDirectories?.length && { additionalDirectories }), diff --git a/apps/code/src/renderer/features/sessions/service/service.ts b/apps/code/src/renderer/features/sessions/service/service.ts index b4a9c16eb..c8843f34f 100644 --- a/apps/code/src/renderer/features/sessions/service/service.ts +++ b/apps/code/src/renderer/features/sessions/service/service.ts @@ -549,7 +549,7 @@ export class SessionService { effort: effortLevelSchema.safeParse(reasoningLevel).success ? (reasoningLevel as EffortLevel) : undefined, - model: preferredModel || undefined, + model: preferredModel, }); const session = this.createBaseSession(taskRun.id, taskId, taskTitle); diff --git a/packages/agent/src/adapters/claude/claude-agent.ts b/packages/agent/src/adapters/claude/claude-agent.ts index 0a2527b57..78719875f 100644 --- a/packages/agent/src/adapters/claude/claude-agent.ts +++ b/packages/agent/src/adapters/claude/claude-agent.ts @@ -910,38 +910,16 @@ export class ClaudeAcpAgent extends BaseAcpAgent { } } - // Fetch model config in parallel with SDK initialization for new sessions. - // The gateway REST call doesn't depend on the SDK being ready. + // Kick off SDK initialization for new sessions so it runs concurrently + // with the model config fetch below (the gateway REST call is independent). + const initPromise = !isResume + ? withTimeout(q.initializationResult(), SESSION_VALIDATION_TIMEOUT_MS) + : undefined; + const [modelOptions] = await Promise.all([ this.getModelConfigOptions( settingsManager.getSettings().model || meta?.model || undefined, ), - // For new sessions, await initialization concurrently with model fetch. - // SDK starts in the background via query() — we just need it ready - // before the first prompt, not before returning configOptions. - ...(!isResume - ? [ - withTimeout(q.initializationResult(), SESSION_VALIDATION_TIMEOUT_MS) - .then((result) => { - if (result.result === "timeout") { - this.logger.error("Session initialization timed out", { - sessionId, - taskId, - taskRunId: meta?.taskRunId, - }); - } - }) - .catch((err) => { - this.logger.error("Session initialization failed", { - sessionId, - taskId, - taskRunId: meta?.taskRunId, - error: err instanceof Error ? err.message : String(err), - }); - }), - ] - : []), - // Fire notification in parallel too ...(meta?.taskRunId ? [ this.client.extNotification("_posthog/sdk_session", { @@ -953,6 +931,27 @@ export class ClaudeAcpAgent extends BaseAcpAgent { : []), ]); + if (initPromise) { + try { + const initResult = await initPromise; + if (initResult.result === "timeout") { + settingsManager.dispose(); + throw new Error( + `Session initialization timed out for sessionId=${sessionId}`, + ); + } + } catch (err) { + settingsManager.dispose(); + this.logger.error("Session initialization failed", { + sessionId, + taskId, + taskRunId: meta?.taskRunId, + error: err instanceof Error ? err.message : String(err), + }); + throw err; + } + } + const settingsModel = settingsManager.getSettings().model; const metaModel = meta?.model; const resolvedModelId = From 14d6d25ee6febb7d44b500d80a45f4c41deb36a2 Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Wed, 1 Apr 2026 19:21:21 -0700 Subject: [PATCH 4/8] Replace preview session process spawn with lightweight config query --- apps/code/src/main/services/agent/schemas.ts | 6 + apps/code/src/main/services/agent/service.ts | 76 ++++++++++++ apps/code/src/main/trpc/routers/agent.ts | 7 ++ .../features/sessions/service/service.ts | 117 +++++++++++------- packages/agent/package.json | 8 ++ packages/agent/tsup.config.ts | 2 + 6 files changed, 169 insertions(+), 47 deletions(-) diff --git a/apps/code/src/main/services/agent/schemas.ts b/apps/code/src/main/services/agent/schemas.ts index 97d7648ec..af96173c7 100644 --- a/apps/code/src/main/services/agent/schemas.ts +++ b/apps/code/src/main/services/agent/schemas.ts @@ -293,3 +293,9 @@ export const getGatewayModelsInput = z.object({ }); export const getGatewayModelsOutput = z.array(modelOptionSchema); + +export const getPreviewConfigOptionsInput = z.object({ + apiHost: z.string(), +}); + +export const getPreviewConfigOptionsOutput = z.array(sessionConfigOptionSchema); diff --git a/apps/code/src/main/services/agent/service.ts b/apps/code/src/main/services/agent/service.ts index 409632d66..fb0a2f949 100644 --- a/apps/code/src/main/services/agent/service.ts +++ b/apps/code/src/main/services/agent/service.ts @@ -14,11 +14,15 @@ import { } from "@agentclientprotocol/sdk"; import { isMcpToolReadOnly } from "@posthog/agent"; import { hydrateSessionJsonl } from "@posthog/agent/adapters/claude/session/jsonl-hydration"; +import { getEffortOptions } from "@posthog/agent/adapters/claude/session/models"; import { Agent } from "@posthog/agent/agent"; +import { getAvailableModes } from "@posthog/agent/execution-mode"; import { + DEFAULT_GATEWAY_MODEL, fetchGatewayModels, formatGatewayModelName, getProviderName, + isAnthropicModel, } from "@posthog/agent/gateway-models"; import { getLlmGatewayUrl } from "@posthog/agent/posthog-api"; import type { OnLogCallback } from "@posthog/agent/types"; @@ -1519,4 +1523,76 @@ For git operations while detached: return getModelTier(a.modelId) - getModelTier(b.modelId); }); } + + async getPreviewConfigOptions( + apiHost: string, + ): Promise { + const gatewayUrl = getLlmGatewayUrl(apiHost); + const gatewayModels = await fetchGatewayModels({ gatewayUrl }); + + const modelOptions = gatewayModels + .filter((model) => isAnthropicModel(model)) + .map((model) => ({ + value: model.id, + name: formatGatewayModelName(model), + description: `Context: ${model.context_window.toLocaleString()} tokens`, + })); + + const resolvedModelId = modelOptions.some( + (o) => o.value === DEFAULT_GATEWAY_MODEL, + ) + ? DEFAULT_GATEWAY_MODEL + : (modelOptions[0]?.value ?? DEFAULT_GATEWAY_MODEL); + + if (!modelOptions.some((o) => o.value === resolvedModelId)) { + modelOptions.unshift({ + value: resolvedModelId, + name: resolvedModelId, + description: "Custom model", + }); + } + + const modeOptions = getAvailableModes().map((mode) => ({ + value: mode.id, + name: mode.name, + description: mode.description ?? undefined, + })); + + const configOptions: SessionConfigOption[] = [ + { + id: "mode", + name: "Approval Preset", + type: "select" as const, + currentValue: "plan", + options: modeOptions, + category: "mode", + description: + "Choose an approval and sandboxing preset for your session", + }, + { + id: "model", + name: "Model", + type: "select" as const, + currentValue: resolvedModelId, + options: modelOptions, + category: "model", + description: "Choose which model Claude should use", + }, + ]; + + const effortOpts = getEffortOptions(resolvedModelId); + if (effortOpts) { + configOptions.push({ + id: "effort", + name: "Effort", + type: "select" as const, + currentValue: "high", + options: effortOpts, + category: "thought_level", + description: "Controls how much effort Claude puts into its response", + }); + } + + return configOptions; + } } diff --git a/apps/code/src/main/trpc/routers/agent.ts b/apps/code/src/main/trpc/routers/agent.ts index f6907f61a..a8afb49ea 100644 --- a/apps/code/src/main/trpc/routers/agent.ts +++ b/apps/code/src/main/trpc/routers/agent.ts @@ -8,6 +8,8 @@ import { cancelSessionInput, getGatewayModelsInput, getGatewayModelsOutput, + getPreviewConfigOptionsInput, + getPreviewConfigOptionsOutput, listSessionsInput, listSessionsOutput, notifySessionContextInput, @@ -193,4 +195,9 @@ export const agentRouter = router({ .input(getGatewayModelsInput) .output(getGatewayModelsOutput) .query(({ input }) => getService().getGatewayModels(input.apiHost)), + + getPreviewConfigOptions: publicProcedure + .input(getPreviewConfigOptionsInput) + .output(getPreviewConfigOptionsOutput) + .query(({ input }) => getService().getPreviewConfigOptions(input.apiHost)), }); diff --git a/apps/code/src/renderer/features/sessions/service/service.ts b/apps/code/src/renderer/features/sessions/service/service.ts index c8843f34f..4624920b0 100644 --- a/apps/code/src/renderer/features/sessions/service/service.ts +++ b/apps/code/src/renderer/features/sessions/service/service.ts @@ -26,6 +26,7 @@ import { } from "@features/sessions/stores/sessionStore"; import { useSettingsStore } from "@features/settings/stores/settingsStore"; import { taskViewedApi } from "@features/sidebar/hooks/useTaskViewed"; +import { getEffortOptions } from "@posthog/agent/adapters/claude/session/models"; import { DEFAULT_GATEWAY_MODEL } from "@posthog/agent/gateway-models"; import { getIsOnline } from "@renderer/stores/connectivityStore"; import { trpcClient } from "@renderer/trpc/client"; @@ -628,10 +629,10 @@ export class SessionService { const abort = new AbortController(); this.previewAbort = abort; - await this.cleanupPreviewSession(); - if (abort.signal.aborted) return; + this.cleanupPreviewSession(); const auth = await this.getAuthCredentials(); + if (abort.signal.aborted) return; if (!auth) { log.info("Skipping preview session - not authenticated"); return; @@ -647,55 +648,39 @@ export class SessionService { sessionStoreSetters.setSession(session); try { - const { - customInstructions: previewCustomInstructions, - defaultInitialTaskMode, - lastUsedInitialTaskMode, - } = useSettingsStore.getState(); + const { defaultInitialTaskMode, lastUsedInitialTaskMode } = + useSettingsStore.getState(); const initialMode = defaultInitialTaskMode === "last_used" ? lastUsedInitialTaskMode : "plan"; - const result = await trpcClient.agent.start.mutate({ - taskId: PREVIEW_TASK_ID, - taskRunId, - repoPath: "__preview__", - apiHost: auth.apiHost, - projectId: auth.projectId, - adapter: params.adapter, - permissionMode: initialMode, - customInstructions: previewCustomInstructions || undefined, - }); + + const configOptions = + await trpcClient.agent.getPreviewConfigOptions.query({ + apiHost: auth.apiHost, + }); if (abort.signal.aborted) { - trpcClient.agent.cancel - .mutate({ sessionId: taskRunId }) - .catch((err) => { - log.warn("Failed to cancel stale preview session", { - taskRunId, - error: err, - }); - }); sessionStoreSetters.removeSession(taskRunId); return; } - const configOptions = result.configOptions as - | SessionConfigOption[] - | undefined; + // Apply the user's preferred initial mode + const updatedOptions = configOptions.map((opt) => + opt.id === "mode" + ? ({ ...opt, currentValue: initialMode } as SessionConfigOption) + : opt, + ); sessionStoreSetters.updateSession(taskRunId, { status: "connected", - channel: result.channel, - configOptions, + configOptions: updatedOptions, }); - this.subscribeToChannel(taskRunId); - - log.info("Preview session started", { + log.info("Preview session started (fast path)", { taskRunId, adapter: params.adapter, - configOptionsCount: configOptions?.length ?? 0, + configOptionsCount: updatedOptions.length, }); } catch (error) { if (abort.signal.aborted) return; @@ -707,23 +692,13 @@ export class SessionService { async cancelPreviewSession(): Promise { this.previewAbort?.abort(); this.previewAbort = null; - await this.cleanupPreviewSession(); + this.cleanupPreviewSession(); } - private async cleanupPreviewSession(): Promise { + private cleanupPreviewSession(): void { const session = sessionStoreSetters.getSessionByTaskId(PREVIEW_TASK_ID); if (!session) return; - - const { taskRunId } = session; - - this.unsubscribeFromChannel(taskRunId); - sessionStoreSetters.removeSession(taskRunId); - - try { - await trpcClient.agent.cancel.mutate({ sessionId: taskRunId }); - } catch (error) { - log.warn("Failed to cancel preview session", { taskRunId, error }); - } + sessionStoreSetters.removeSession(session.taskRunId); } // --- Subscription Management --- @@ -1620,6 +1595,54 @@ export class SessionService { return; } + // Preview sessions have no backing agent process — update store directly + if (taskId === PREVIEW_TASK_ID) { + let updatedOptions = configOptions.map((opt) => + opt.id === configId + ? ({ ...opt, currentValue: value } as SessionConfigOption) + : opt, + ); + + // Recompute effort options when model changes + if (configId === "model") { + const effortOpts = getEffortOptions(value); + const existingIdx = updatedOptions.findIndex((o) => o.id === "effort"); + + if (effortOpts && existingIdx >= 0) { + const currentEffort = updatedOptions[existingIdx].currentValue; + const validEffort = effortOpts.some((e) => e.value === currentEffort) + ? currentEffort + : "high"; + updatedOptions[existingIdx] = { + ...updatedOptions[existingIdx], + currentValue: validEffort, + options: effortOpts, + } as SessionConfigOption; + } else if (effortOpts && existingIdx === -1) { + updatedOptions = [ + ...updatedOptions, + { + id: "effort", + name: "Effort", + type: "select" as const, + currentValue: "high", + options: effortOpts, + category: "thought_level", + description: + "Controls how much effort Claude puts into its response", + } as SessionConfigOption, + ]; + } else if (!effortOpts && existingIdx >= 0) { + updatedOptions = updatedOptions.filter((o) => o.id !== "effort"); + } + } + + sessionStoreSetters.updateSession(session.taskRunId, { + configOptions: updatedOptions, + }); + return; + } + // Optimistic update const updatedOptions = configOptions.map((opt) => opt.id === configId diff --git a/packages/agent/package.json b/packages/agent/package.json index 56f15c2fb..e112079e8 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -44,6 +44,14 @@ "types": "./dist/adapters/claude/session/jsonl-hydration.d.ts", "import": "./dist/adapters/claude/session/jsonl-hydration.js" }, + "./adapters/claude/session/models": { + "types": "./dist/adapters/claude/session/models.d.ts", + "import": "./dist/adapters/claude/session/models.js" + }, + "./execution-mode": { + "types": "./dist/execution-mode.d.ts", + "import": "./dist/execution-mode.js" + }, "./server": { "types": "./dist/server/agent-server.d.ts", "import": "./dist/server/agent-server.js" diff --git a/packages/agent/tsup.config.ts b/packages/agent/tsup.config.ts index 9298935d7..9af0bc8ff 100644 --- a/packages/agent/tsup.config.ts +++ b/packages/agent/tsup.config.ts @@ -81,6 +81,8 @@ export default defineConfig([ "src/adapters/claude/tools.ts", "src/adapters/claude/conversion/tool-use-to-acp.ts", "src/adapters/claude/session/jsonl-hydration.ts", + "src/adapters/claude/session/models.ts", + "src/execution-mode.ts", "src/server/agent-server.ts", ], format: ["esm"], From 213948550cc6d5cf7ec39133d873ed75066664ee Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Wed, 1 Apr 2026 20:29:21 -0700 Subject: [PATCH 5/8] rename preview session to preview config --- apps/code/src/main/services/agent/service.ts | 2 +- .../onboarding/components/TutorialStep.tsx | 14 +++--- .../features/sessions/service/service.ts | 44 +++++++++---------- .../task-detail/components/TaskInput.tsx | 17 ++++--- .../components/TaskInputEditor.tsx | 10 ++--- ...ePreviewSession.ts => usePreviewConfig.ts} | 33 +++++++------- 6 files changed, 59 insertions(+), 61 deletions(-) rename apps/code/src/renderer/features/task-detail/hooks/{usePreviewSession.ts => usePreviewConfig.ts} (60%) diff --git a/apps/code/src/main/services/agent/service.ts b/apps/code/src/main/services/agent/service.ts index fb0a2f949..871dd7a1f 100644 --- a/apps/code/src/main/services/agent/service.ts +++ b/apps/code/src/main/services/agent/service.ts @@ -474,7 +474,7 @@ export class AgentService extends TypedEventEmitter { model, } = config; - // Preview sessions don't need a real repo — use a temp directory + // Preview config doesn't need a real repo — use a temp directory const repoPath = taskId === "__preview__" ? tmpdir() : rawRepoPath; if (!isRetry) { diff --git a/apps/code/src/renderer/features/onboarding/components/TutorialStep.tsx b/apps/code/src/renderer/features/onboarding/components/TutorialStep.tsx index 8f4e5cb13..52423eb4e 100644 --- a/apps/code/src/renderer/features/onboarding/components/TutorialStep.tsx +++ b/apps/code/src/renderer/features/onboarding/components/TutorialStep.tsx @@ -14,7 +14,7 @@ import { import { useSettingsStore } from "@features/settings/stores/settingsStore"; import { TaskInputEditor } from "@features/task-detail/components/TaskInputEditor"; import { WorkspaceModeSelect } from "@features/task-detail/components/WorkspaceModeSelect"; -import { usePreviewSession } from "@features/task-detail/hooks/usePreviewSession"; +import { usePreviewConfig } from "@features/task-detail/hooks/usePreviewConfig"; import { useTaskCreation } from "@features/task-detail/hooks/useTaskCreation"; import { useGithubBranches, @@ -104,9 +104,9 @@ export function TutorialStep({ onComplete, onBack }: TutorialStepProps) { const cloudBranches = cloudBranchData?.branches; const cloudDefaultBranch = cloudBranchData?.defaultBranch ?? null; - // Preview session for config options — always claude - const { modeOption, thoughtOption, previewTaskId, isConnecting } = - usePreviewSession("claude"); + // Preview config options — always claude + const { modeOption, thoughtOption, previewConfigId, isConnecting } = + usePreviewConfig("claude"); const currentExecutionMode = getCurrentModeFromConfigOptions(modeOption ? [modeOption] : undefined) ?? @@ -194,12 +194,12 @@ export function TutorialStep({ onComplete, onBack }: TutorialStepProps) { const nextValue = cycleModeOption(modeOption, allowBypassPermissions); if (nextValue && modeOption) { getSessionService().setSessionConfigOption( - previewTaskId, + previewConfigId, modeOption.id, nextValue, ); } - }, [modeOption, allowBypassPermissions, previewTaskId]); + }, [modeOption, allowBypassPermissions, previewConfigId]); useHotkeys( "shift+tab", @@ -388,7 +388,7 @@ export function TutorialStep({ onComplete, onBack }: TutorialStepProps) { directoryTooltip="Select a repository first" onEmptyChange={setEditorIsEmpty} adapter="claude" - previewTaskId={previewTaskId} + previewConfigId={previewConfigId} onAdapterChange={() => {}} isPreviewConnecting={isConnecting} autoFocus={false} diff --git a/apps/code/src/renderer/features/sessions/service/service.ts b/apps/code/src/renderer/features/sessions/service/service.ts index 4624920b0..790448bc2 100644 --- a/apps/code/src/renderer/features/sessions/service/service.ts +++ b/apps/code/src/renderer/features/sessions/service/service.ts @@ -62,7 +62,7 @@ import { const log = logger.scope("session-service"); -export const PREVIEW_TASK_ID = "__preview__"; +export const PREVIEW_CONFIG_ID = "__preview__"; interface AuthCredentials { apiHost: string; @@ -113,8 +113,8 @@ export class SessionService { permission?: { unsubscribe: () => void }; } >(); - /** AbortController for the current in-flight preview session start */ - private previewAbort: AbortController | null = null; + /** AbortController for the current in-flight preview config fetch */ + private previewConfigAbort: AbortController | null = null; /** Active cloud task watchers, keyed by taskId */ private cloudTaskWatchers = new Map< string, @@ -620,28 +620,28 @@ export class SessionService { await this.teardownSession(session.taskRunId); } - // --- Preview Session Management --- + // --- Preview Config Management --- - async startPreviewSession(params: { + async fetchPreviewConfig(params: { adapter: "claude" | "codex"; }): Promise { - this.previewAbort?.abort(); + this.previewConfigAbort?.abort(); const abort = new AbortController(); - this.previewAbort = abort; + this.previewConfigAbort = abort; - this.cleanupPreviewSession(); + this.clearPreviewConfig(); const auth = await this.getAuthCredentials(); if (abort.signal.aborted) return; if (!auth) { - log.info("Skipping preview session - not authenticated"); + log.info("Skipping preview config - not authenticated"); return; } const taskRunId = `preview-${crypto.randomUUID()}`; const session = this.createBaseSession( taskRunId, - PREVIEW_TASK_ID, + PREVIEW_CONFIG_ID, "Preview", ); session.adapter = params.adapter; @@ -677,26 +677,26 @@ export class SessionService { configOptions: updatedOptions, }); - log.info("Preview session started (fast path)", { + log.info("Preview config loaded", { taskRunId, adapter: params.adapter, configOptionsCount: updatedOptions.length, }); } catch (error) { if (abort.signal.aborted) return; - log.error("Failed to start preview session", { error }); + log.error("Failed to fetch preview config", { error }); sessionStoreSetters.removeSession(taskRunId); } } - async cancelPreviewSession(): Promise { - this.previewAbort?.abort(); - this.previewAbort = null; - this.cleanupPreviewSession(); + async cancelPreviewConfig(): Promise { + this.previewConfigAbort?.abort(); + this.previewConfigAbort = null; + this.clearPreviewConfig(); } - private cleanupPreviewSession(): void { - const session = sessionStoreSetters.getSessionByTaskId(PREVIEW_TASK_ID); + private clearPreviewConfig(): void { + const session = sessionStoreSetters.getSessionByTaskId(PREVIEW_CONFIG_ID); if (!session) return; sessionStoreSetters.removeSession(session.taskRunId); } @@ -776,8 +776,8 @@ export class SessionService { } this.connectingTasks.clear(); - this.previewAbort?.abort(); - this.previewAbort = null; + this.previewConfigAbort?.abort(); + this.previewConfigAbort = null; this.idleKilledSubscription?.unsubscribe(); this.idleKilledSubscription = null; } @@ -1595,8 +1595,8 @@ export class SessionService { return; } - // Preview sessions have no backing agent process — update store directly - if (taskId === PREVIEW_TASK_ID) { + // Preview config has no backing agent process — update store directly + if (taskId === PREVIEW_CONFIG_ID) { let updatedOptions = configOptions.map((opt) => opt.id === configId ? ({ ...opt, currentValue: value } as SessionConfigOption) diff --git a/apps/code/src/renderer/features/task-detail/components/TaskInput.tsx b/apps/code/src/renderer/features/task-detail/components/TaskInput.tsx index bcfdc822d..337eaffd2 100644 --- a/apps/code/src/renderer/features/task-detail/components/TaskInput.tsx +++ b/apps/code/src/renderer/features/task-detail/components/TaskInput.tsx @@ -32,7 +32,7 @@ import { useNavigationStore } from "@stores/navigationStore"; import { useQuery } from "@tanstack/react-query"; import { useCallback, useEffect, useRef, useState } from "react"; import { useHotkeys } from "react-hotkeys-hook"; -import { usePreviewSession } from "../hooks/usePreviewSession"; +import { usePreviewConfig } from "../hooks/usePreviewConfig"; import { useTaskCreation } from "../hooks/useTaskCreation"; import { TaskInputEditor } from "./TaskInputEditor"; import { type WorkspaceMode, WorkspaceModeSelect } from "./WorkspaceModeSelect"; @@ -150,14 +150,13 @@ export function TaskInput({ } }, [selectedDirectory, newBranchName, gitActions]); - // Preview session provides adapter-specific config options const { modeOption, modelOption, thoughtOption, - previewTaskId, + previewConfigId, isConnecting, - } = usePreviewSession(adapter); + } = usePreviewConfig(adapter); const { folders } = useFolders(); @@ -194,8 +193,8 @@ export function TaskInput({ const effectiveWorkspaceMode = workspaceMode; - // Get current values from preview session config options for task creation. - // Defaults ensure values are always passed even before the preview session loads. + // Get current values from preview config options for task creation. + // Defaults ensure values are always passed even before the preview config loads. const currentModel = modelOption?.type === "select" ? modelOption.currentValue : undefined; const modeFallback = @@ -235,12 +234,12 @@ export function TaskInput({ const nextValue = cycleModeOption(modeOption, allowBypassPermissions); if (nextValue && modeOption) { getSessionService().setSessionConfigOption( - previewTaskId, + previewConfigId, modeOption.id, nextValue, ); } - }, [modeOption, allowBypassPermissions, previewTaskId]); + }, [modeOption, allowBypassPermissions, previewConfigId]); // Global shift+tab to cycle mode regardless of focus useHotkeys( @@ -462,7 +461,7 @@ export function TaskInput({ } onEmptyChange={setEditorIsEmpty} adapter={adapter} - previewTaskId={previewTaskId} + previewConfigId={previewConfigId} onAdapterChange={setAdapter} isPreviewConnecting={isConnecting} /> diff --git a/apps/code/src/renderer/features/task-detail/components/TaskInputEditor.tsx b/apps/code/src/renderer/features/task-detail/components/TaskInputEditor.tsx index f77f2f346..13593a9c1 100644 --- a/apps/code/src/renderer/features/task-detail/components/TaskInputEditor.tsx +++ b/apps/code/src/renderer/features/task-detail/components/TaskInputEditor.tsx @@ -25,7 +25,7 @@ interface TaskInputEditorProps { directoryTooltip?: string; onEmptyChange?: (isEmpty: boolean) => void; adapter?: "claude" | "codex"; - previewTaskId?: string; + previewConfigId?: string; onAdapterChange?: (adapter: AgentAdapter) => void; isPreviewConnecting?: boolean; autoFocus?: boolean; @@ -48,7 +48,7 @@ export const TaskInputEditor = forwardRef< directoryTooltip = "Select a folder first", onEmptyChange, adapter, - previewTaskId, + previewConfigId, onAdapterChange, isPreviewConnecting, autoFocus = true, @@ -86,7 +86,7 @@ export const TaskInputEditor = forwardRef< submitDisabled: !isOnline, isLoading: isCreatingTask, autoFocus, - context: { repoPath, taskId: previewTaskId }, + context: { repoPath, taskId: previewConfigId }, capabilities: { commands: true, bashMode: false }, clearOnSubmit: false, getPromptHistory, @@ -218,7 +218,7 @@ export const TaskInputEditor = forwardRef< /> {})} disabled={isCreatingTask} @@ -228,7 +228,7 @@ export const TaskInputEditor = forwardRef< {!isPreviewConnecting && ( )} diff --git a/apps/code/src/renderer/features/task-detail/hooks/usePreviewSession.ts b/apps/code/src/renderer/features/task-detail/hooks/usePreviewConfig.ts similarity index 60% rename from apps/code/src/renderer/features/task-detail/hooks/usePreviewSession.ts rename to apps/code/src/renderer/features/task-detail/hooks/usePreviewConfig.ts index 90f07764c..06c3d9ecd 100644 --- a/apps/code/src/renderer/features/task-detail/hooks/usePreviewSession.ts +++ b/apps/code/src/renderer/features/task-detail/hooks/usePreviewConfig.ts @@ -2,7 +2,7 @@ import type { SessionConfigOption } from "@agentclientprotocol/sdk"; import { useAuthStateValue } from "@features/auth/hooks/authQueries"; import { getSessionService, - PREVIEW_TASK_ID, + PREVIEW_CONFIG_ID, } from "@features/sessions/service/service"; import { useModeConfigOptionForTask, @@ -12,42 +12,41 @@ import { } from "@features/sessions/stores/sessionStore"; import { useEffect } from "react"; -interface PreviewSessionResult { +interface PreviewConfigResult { modeOption: SessionConfigOption | undefined; modelOption: SessionConfigOption | undefined; thoughtOption: SessionConfigOption | undefined; - previewTaskId: string; - /** True while the preview session is connecting (no config options yet) */ + previewConfigId: string; + /** True while the preview config is loading (no config options yet) */ isConnecting: boolean; } /** - * Manages a lightweight preview session that provides adapter-specific - * config options (models, modes, reasoning levels) for the task input page. + * Fetches adapter-specific config options (models, modes, reasoning levels) + * for the task input page without spawning a full agent session. * - * Starts a new preview session when adapter changes, - * and cleans up on unmount or when inputs change. + * Refetches when the adapter changes and cleans up on unmount. */ -export function usePreviewSession( +export function usePreviewConfig( adapter: "claude" | "codex", -): PreviewSessionResult { +): PreviewConfigResult { const projectId = useAuthStateValue((state) => state.projectId); useEffect(() => { if (!projectId) return; const service = getSessionService(); - service.startPreviewSession({ adapter }); + service.fetchPreviewConfig({ adapter }); return () => { - service.cancelPreviewSession(); + service.cancelPreviewConfig(); }; }, [adapter, projectId]); - const session = useSessionForTask(PREVIEW_TASK_ID); - const modeOption = useModeConfigOptionForTask(PREVIEW_TASK_ID); - const modelOption = useModelConfigOptionForTask(PREVIEW_TASK_ID); - const thoughtOption = useThoughtLevelConfigOptionForTask(PREVIEW_TASK_ID); + const session = useSessionForTask(PREVIEW_CONFIG_ID); + const modeOption = useModeConfigOptionForTask(PREVIEW_CONFIG_ID); + const modelOption = useModelConfigOptionForTask(PREVIEW_CONFIG_ID); + const thoughtOption = useThoughtLevelConfigOptionForTask(PREVIEW_CONFIG_ID); // Connecting if we have a session but it's not connected yet, // or if we don't have a session at all (start hasn't created one yet) @@ -57,7 +56,7 @@ export function usePreviewSession( modeOption, modelOption, thoughtOption, - previewTaskId: PREVIEW_TASK_ID, + previewConfigId: PREVIEW_CONFIG_ID, isConnecting, }; } From 4797bda6315196bd9660c6d44817c45d46b60452 Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Wed, 1 Apr 2026 20:53:47 -0700 Subject: [PATCH 6/8] remove fake preview session infrastructure --- apps/code/src/main/services/agent/schemas.ts | 1 + apps/code/src/main/services/agent/service.ts | 47 ++++-- apps/code/src/main/trpc/routers/agent.ts | 4 +- .../onboarding/components/TutorialStep.tsx | 30 ++-- .../components/ReasoningLevelSelector.tsx | 52 +++---- .../components/UnifiedModelSelector.tsx | 28 ++-- .../features/sessions/service/service.ts | 136 ---------------- .../task-detail/components/TaskInput.tsx | 19 +-- .../components/TaskInputEditor.tsx | 37 +++-- .../task-detail/hooks/usePreviewConfig.ts | 145 ++++++++++++++---- packages/agent/src/gateway-models.ts | 9 ++ 11 files changed, 245 insertions(+), 263 deletions(-) diff --git a/apps/code/src/main/services/agent/schemas.ts b/apps/code/src/main/services/agent/schemas.ts index af96173c7..702c8de93 100644 --- a/apps/code/src/main/services/agent/schemas.ts +++ b/apps/code/src/main/services/agent/schemas.ts @@ -296,6 +296,7 @@ export const getGatewayModelsOutput = z.array(modelOptionSchema); export const getPreviewConfigOptionsInput = z.object({ apiHost: z.string(), + adapter: z.enum(["claude", "codex"]), }); export const getPreviewConfigOptionsOutput = z.array(sessionConfigOptionSchema); diff --git a/apps/code/src/main/services/agent/service.ts b/apps/code/src/main/services/agent/service.ts index 871dd7a1f..e26466915 100644 --- a/apps/code/src/main/services/agent/service.ts +++ b/apps/code/src/main/services/agent/service.ts @@ -23,6 +23,7 @@ import { formatGatewayModelName, getProviderName, isAnthropicModel, + isOpenAIModel, } from "@posthog/agent/gateway-models"; import { getLlmGatewayUrl } from "@posthog/agent/posthog-api"; import type { OnLogCallback } from "@posthog/agent/types"; @@ -1526,23 +1527,29 @@ For git operations while detached: async getPreviewConfigOptions( apiHost: string, + adapter: "claude" | "codex" = "claude", ): Promise { const gatewayUrl = getLlmGatewayUrl(apiHost); const gatewayModels = await fetchGatewayModels({ gatewayUrl }); + const modelFilter = adapter === "codex" ? isOpenAIModel : isAnthropicModel; + const modelOptions = gatewayModels - .filter((model) => isAnthropicModel(model)) + .filter((model) => modelFilter(model)) .map((model) => ({ value: model.id, name: formatGatewayModelName(model), description: `Context: ${model.context_window.toLocaleString()} tokens`, })); - const resolvedModelId = modelOptions.some( - (o) => o.value === DEFAULT_GATEWAY_MODEL, - ) - ? DEFAULT_GATEWAY_MODEL - : (modelOptions[0]?.value ?? DEFAULT_GATEWAY_MODEL); + const defaultModel = + adapter === "codex" + ? (modelOptions[0]?.value ?? "") + : DEFAULT_GATEWAY_MODEL; + + const resolvedModelId = modelOptions.some((o) => o.value === defaultModel) + ? defaultModel + : (modelOptions[0]?.value ?? defaultModel); if (!modelOptions.some((o) => o.value === resolvedModelId)) { modelOptions.unshift({ @@ -1580,17 +1587,33 @@ For git operations while detached: }, ]; - const effortOpts = getEffortOptions(resolvedModelId); - if (effortOpts) { + if (adapter === "codex") { configOptions.push({ - id: "effort", - name: "Effort", + id: "reasoning_effort", + name: "Reasoning Level", type: "select" as const, currentValue: "high", - options: effortOpts, + options: [ + { value: "low", name: "Low" }, + { value: "medium", name: "Medium" }, + { value: "high", name: "High" }, + ], category: "thought_level", - description: "Controls how much effort Claude puts into its response", + description: "Controls how much reasoning effort the model uses", }); + } else { + const effortOpts = getEffortOptions(resolvedModelId); + if (effortOpts) { + configOptions.push({ + id: "effort", + name: "Effort", + type: "select" as const, + currentValue: "high", + options: effortOpts, + category: "thought_level", + description: "Controls how much effort Claude puts into its response", + }); + } } return configOptions; diff --git a/apps/code/src/main/trpc/routers/agent.ts b/apps/code/src/main/trpc/routers/agent.ts index a8afb49ea..07cd63025 100644 --- a/apps/code/src/main/trpc/routers/agent.ts +++ b/apps/code/src/main/trpc/routers/agent.ts @@ -199,5 +199,7 @@ export const agentRouter = router({ getPreviewConfigOptions: publicProcedure .input(getPreviewConfigOptionsInput) .output(getPreviewConfigOptionsOutput) - .query(({ input }) => getService().getPreviewConfigOptions(input.apiHost)), + .query(({ input }) => + getService().getPreviewConfigOptions(input.apiHost, input.adapter), + ), }); diff --git a/apps/code/src/renderer/features/onboarding/components/TutorialStep.tsx b/apps/code/src/renderer/features/onboarding/components/TutorialStep.tsx index 52423eb4e..77495339d 100644 --- a/apps/code/src/renderer/features/onboarding/components/TutorialStep.tsx +++ b/apps/code/src/renderer/features/onboarding/components/TutorialStep.tsx @@ -6,7 +6,6 @@ import type { MessageEditorHandle } from "@features/message-editor/components/Me import { ModeIndicatorInput } from "@features/message-editor/components/ModeIndicatorInput"; import { useDraftStore } from "@features/message-editor/stores/draftStore"; import { useOnboardingStore } from "@features/onboarding/stores/onboardingStore"; -import { getSessionService } from "@features/sessions/service/service"; import { cycleModeOption, getCurrentModeFromConfigOptions, @@ -105,8 +104,13 @@ export function TutorialStep({ onComplete, onBack }: TutorialStepProps) { const cloudDefaultBranch = cloudBranchData?.defaultBranch ?? null; // Preview config options — always claude - const { modeOption, thoughtOption, previewConfigId, isConnecting } = - usePreviewConfig("claude"); + const { + modeOption, + modelOption, + thoughtOption, + isLoading: isPreviewLoading, + setConfigOption, + } = usePreviewConfig("claude"); const currentExecutionMode = getCurrentModeFromConfigOptions(modeOption ? [modeOption] : undefined) ?? @@ -193,13 +197,9 @@ export function TutorialStep({ onComplete, onBack }: TutorialStepProps) { const handleCycleMode = useCallback(() => { const nextValue = cycleModeOption(modeOption, allowBypassPermissions); if (nextValue && modeOption) { - getSessionService().setSessionConfigOption( - previewConfigId, - modeOption.id, - nextValue, - ); + setConfigOption(modeOption.id, nextValue); } - }, [modeOption, allowBypassPermissions, previewConfigId]); + }, [modeOption, allowBypassPermissions, setConfigOption]); useHotkeys( "shift+tab", @@ -388,9 +388,16 @@ export function TutorialStep({ onComplete, onBack }: TutorialStepProps) { directoryTooltip="Select a repository first" onEmptyChange={setEditorIsEmpty} adapter="claude" - previewConfigId={previewConfigId} + modelOption={modelOption} + thoughtOption={thoughtOption} + onConfigOptionChange={(configId, value) => { + setConfigOption(configId, value); + if (configId === modelOption?.id) { + handleModelChange(value); + } + }} onAdapterChange={() => {}} - isPreviewConnecting={isConnecting} + isLoading={isPreviewLoading} autoFocus={false} tourHighlight={ isHighlighted("model-selector") @@ -399,7 +406,6 @@ export function TutorialStep({ onComplete, onBack }: TutorialStepProps) { ? "submit-button" : null } - onModelChange={handleModelChange} /> diff --git a/apps/code/src/renderer/features/sessions/components/ReasoningLevelSelector.tsx b/apps/code/src/renderer/features/sessions/components/ReasoningLevelSelector.tsx index e9e8083bc..df4b15c77 100644 --- a/apps/code/src/renderer/features/sessions/components/ReasoningLevelSelector.tsx +++ b/apps/code/src/renderer/features/sessions/components/ReasoningLevelSelector.tsx @@ -1,25 +1,21 @@ -import { Select, Text } from "@radix-ui/themes"; -import { getSessionService } from "../service/service"; -import { - flattenSelectOptions, - useAdapterForTask, - useSessionForTask, - useThoughtLevelConfigOptionForTask, -} from "../stores/sessionStore"; +import type { SessionConfigOption } from "@agentclientprotocol/sdk"; +import { Brain } from "@phosphor-icons/react"; +import { Flex, Select, Text } from "@radix-ui/themes"; +import { flattenSelectOptions } from "../stores/sessionStore"; interface ReasoningLevelSelectorProps { - taskId?: string; + thoughtOption?: SessionConfigOption; + adapter?: "claude" | "codex"; + onChange?: (value: string) => void; disabled?: boolean; } export function ReasoningLevelSelector({ - taskId, + thoughtOption, + adapter, + onChange, disabled, }: ReasoningLevelSelectorProps) { - const session = useSessionForTask(taskId); - const thoughtOption = useThoughtLevelConfigOptionForTask(taskId); - const adapter = useAdapterForTask(taskId); - if (!thoughtOption || thoughtOption.type !== "select") { return null; } @@ -30,37 +26,35 @@ export function ReasoningLevelSelector({ const activeLabel = options.find((opt) => opt.value === activeLevel)?.name ?? activeLevel; - const handleChange = (value: string) => { - if (taskId && session?.status === "connected") { - getSessionService().setSessionConfigOption( - taskId, - thoughtOption.id, - value, - ); - } - }; - return ( onChange?.(value)} disabled={disabled} size="1" > - - {adapter === "codex" ? "Reasoning" : "Effort"}: {activeLabel} - + + + + {adapter === "codex" ? "Reasoning" : "Effort"}: {activeLabel} + + {options.map((level) => ( diff --git a/apps/code/src/renderer/features/sessions/components/UnifiedModelSelector.tsx b/apps/code/src/renderer/features/sessions/components/UnifiedModelSelector.tsx index efcd848f2..b18359cd4 100644 --- a/apps/code/src/renderer/features/sessions/components/UnifiedModelSelector.tsx +++ b/apps/code/src/renderer/features/sessions/components/UnifiedModelSelector.tsx @@ -1,4 +1,7 @@ -import type { SessionConfigSelectGroup } from "@agentclientprotocol/sdk"; +import type { + SessionConfigOption, + SessionConfigSelectGroup, +} from "@agentclientprotocol/sdk"; import type { AgentAdapter } from "@features/settings/stores/settingsStore"; import { ArrowsClockwise, @@ -9,12 +12,7 @@ import { } from "@phosphor-icons/react"; import { Button, DropdownMenu, Flex, Text } from "@radix-ui/themes"; import { Fragment, useMemo } from "react"; -import { getSessionService } from "../service/service"; -import { - flattenSelectOptions, - useModelConfigOptionForTask, - useSessionForTask, -} from "../stores/sessionStore"; +import { flattenSelectOptions } from "../stores/sessionStore"; const ADAPTER_ICONS: Record = { claude: , @@ -31,25 +29,22 @@ function getOtherAdapter(adapter: AgentAdapter): AgentAdapter { } interface UnifiedModelSelectorProps { - taskId?: string; + modelOption?: SessionConfigOption; adapter: AgentAdapter; onAdapterChange: (adapter: AgentAdapter) => void; + onModelChange?: (model: string) => void; disabled?: boolean; isConnecting?: boolean; - onModelChange?: (model: string) => void; } export function UnifiedModelSelector({ - taskId, + modelOption, adapter, onAdapterChange, + onModelChange, disabled, isConnecting, - onModelChange, }: UnifiedModelSelectorProps) { - const session = useSessionForTask(taskId); - const modelOption = useModelConfigOptionForTask(taskId); - const selectOption = modelOption?.type === "select" ? modelOption : undefined; const options = selectOption ? flattenSelectOptions(selectOption.options) @@ -69,10 +64,7 @@ export function UnifiedModelSelector({ const otherAdapter = getOtherAdapter(adapter); const handleModelSelect = (value: string) => { - if (taskId && session?.status === "connected" && modelOption) { - getSessionService().setSessionConfigOption(taskId, modelOption.id, value); - onModelChange?.(value); - } + onModelChange?.(value); }; const triggerStyle = { diff --git a/apps/code/src/renderer/features/sessions/service/service.ts b/apps/code/src/renderer/features/sessions/service/service.ts index 790448bc2..5de0ce60e 100644 --- a/apps/code/src/renderer/features/sessions/service/service.ts +++ b/apps/code/src/renderer/features/sessions/service/service.ts @@ -26,7 +26,6 @@ import { } from "@features/sessions/stores/sessionStore"; import { useSettingsStore } from "@features/settings/stores/settingsStore"; import { taskViewedApi } from "@features/sidebar/hooks/useTaskViewed"; -import { getEffortOptions } from "@posthog/agent/adapters/claude/session/models"; import { DEFAULT_GATEWAY_MODEL } from "@posthog/agent/gateway-models"; import { getIsOnline } from "@renderer/stores/connectivityStore"; import { trpcClient } from "@renderer/trpc/client"; @@ -62,8 +61,6 @@ import { const log = logger.scope("session-service"); -export const PREVIEW_CONFIG_ID = "__preview__"; - interface AuthCredentials { apiHost: string; projectId: number; @@ -113,8 +110,6 @@ export class SessionService { permission?: { unsubscribe: () => void }; } >(); - /** AbortController for the current in-flight preview config fetch */ - private previewConfigAbort: AbortController | null = null; /** Active cloud task watchers, keyed by taskId */ private cloudTaskWatchers = new Map< string, @@ -620,87 +615,6 @@ export class SessionService { await this.teardownSession(session.taskRunId); } - // --- Preview Config Management --- - - async fetchPreviewConfig(params: { - adapter: "claude" | "codex"; - }): Promise { - this.previewConfigAbort?.abort(); - const abort = new AbortController(); - this.previewConfigAbort = abort; - - this.clearPreviewConfig(); - - const auth = await this.getAuthCredentials(); - if (abort.signal.aborted) return; - if (!auth) { - log.info("Skipping preview config - not authenticated"); - return; - } - - const taskRunId = `preview-${crypto.randomUUID()}`; - const session = this.createBaseSession( - taskRunId, - PREVIEW_CONFIG_ID, - "Preview", - ); - session.adapter = params.adapter; - sessionStoreSetters.setSession(session); - - try { - const { defaultInitialTaskMode, lastUsedInitialTaskMode } = - useSettingsStore.getState(); - const initialMode = - defaultInitialTaskMode === "last_used" - ? lastUsedInitialTaskMode - : "plan"; - - const configOptions = - await trpcClient.agent.getPreviewConfigOptions.query({ - apiHost: auth.apiHost, - }); - - if (abort.signal.aborted) { - sessionStoreSetters.removeSession(taskRunId); - return; - } - - // Apply the user's preferred initial mode - const updatedOptions = configOptions.map((opt) => - opt.id === "mode" - ? ({ ...opt, currentValue: initialMode } as SessionConfigOption) - : opt, - ); - - sessionStoreSetters.updateSession(taskRunId, { - status: "connected", - configOptions: updatedOptions, - }); - - log.info("Preview config loaded", { - taskRunId, - adapter: params.adapter, - configOptionsCount: updatedOptions.length, - }); - } catch (error) { - if (abort.signal.aborted) return; - log.error("Failed to fetch preview config", { error }); - sessionStoreSetters.removeSession(taskRunId); - } - } - - async cancelPreviewConfig(): Promise { - this.previewConfigAbort?.abort(); - this.previewConfigAbort = null; - this.clearPreviewConfig(); - } - - private clearPreviewConfig(): void { - const session = sessionStoreSetters.getSessionByTaskId(PREVIEW_CONFIG_ID); - if (!session) return; - sessionStoreSetters.removeSession(session.taskRunId); - } - // --- Subscription Management --- private subscribeToChannel(taskRunId: string): void { @@ -776,8 +690,6 @@ export class SessionService { } this.connectingTasks.clear(); - this.previewConfigAbort?.abort(); - this.previewConfigAbort = null; this.idleKilledSubscription?.unsubscribe(); this.idleKilledSubscription = null; } @@ -1595,54 +1507,6 @@ export class SessionService { return; } - // Preview config has no backing agent process — update store directly - if (taskId === PREVIEW_CONFIG_ID) { - let updatedOptions = configOptions.map((opt) => - opt.id === configId - ? ({ ...opt, currentValue: value } as SessionConfigOption) - : opt, - ); - - // Recompute effort options when model changes - if (configId === "model") { - const effortOpts = getEffortOptions(value); - const existingIdx = updatedOptions.findIndex((o) => o.id === "effort"); - - if (effortOpts && existingIdx >= 0) { - const currentEffort = updatedOptions[existingIdx].currentValue; - const validEffort = effortOpts.some((e) => e.value === currentEffort) - ? currentEffort - : "high"; - updatedOptions[existingIdx] = { - ...updatedOptions[existingIdx], - currentValue: validEffort, - options: effortOpts, - } as SessionConfigOption; - } else if (effortOpts && existingIdx === -1) { - updatedOptions = [ - ...updatedOptions, - { - id: "effort", - name: "Effort", - type: "select" as const, - currentValue: "high", - options: effortOpts, - category: "thought_level", - description: - "Controls how much effort Claude puts into its response", - } as SessionConfigOption, - ]; - } else if (!effortOpts && existingIdx >= 0) { - updatedOptions = updatedOptions.filter((o) => o.id !== "effort"); - } - } - - sessionStoreSetters.updateSession(session.taskRunId, { - configOptions: updatedOptions, - }); - return; - } - // Optimistic update const updatedOptions = configOptions.map((opt) => opt.id === configId diff --git a/apps/code/src/renderer/features/task-detail/components/TaskInput.tsx b/apps/code/src/renderer/features/task-detail/components/TaskInput.tsx index 337eaffd2..0c055e51f 100644 --- a/apps/code/src/renderer/features/task-detail/components/TaskInput.tsx +++ b/apps/code/src/renderer/features/task-detail/components/TaskInput.tsx @@ -13,7 +13,6 @@ import { import type { MessageEditorHandle } from "@features/message-editor/components/MessageEditor"; import { ModeIndicatorInput } from "@features/message-editor/components/ModeIndicatorInput"; import { DropZoneOverlay } from "@features/sessions/components/DropZoneOverlay"; -import { getSessionService } from "@features/sessions/service/service"; import { cycleModeOption, getCurrentModeFromConfigOptions, @@ -154,8 +153,8 @@ export function TaskInput({ modeOption, modelOption, thoughtOption, - previewConfigId, - isConnecting, + isLoading: isPreviewLoading, + setConfigOption, } = usePreviewConfig(adapter); const { folders } = useFolders(); @@ -233,13 +232,9 @@ export function TaskInput({ const handleCycleMode = useCallback(() => { const nextValue = cycleModeOption(modeOption, allowBypassPermissions); if (nextValue && modeOption) { - getSessionService().setSessionConfigOption( - previewConfigId, - modeOption.id, - nextValue, - ); + setConfigOption(modeOption.id, nextValue); } - }, [modeOption, allowBypassPermissions, previewConfigId]); + }, [modeOption, allowBypassPermissions, setConfigOption]); // Global shift+tab to cycle mode regardless of focus useHotkeys( @@ -461,9 +456,11 @@ export function TaskInput({ } onEmptyChange={setEditorIsEmpty} adapter={adapter} - previewConfigId={previewConfigId} + modelOption={modelOption} + thoughtOption={thoughtOption} + onConfigOptionChange={setConfigOption} onAdapterChange={setAdapter} - isPreviewConnecting={isConnecting} + isLoading={isPreviewLoading} /> void; adapter?: "claude" | "codex"; - previewConfigId?: string; + modelOption?: SessionConfigOption; + thoughtOption?: SessionConfigOption; + onConfigOptionChange?: (configId: string, value: string) => void; onAdapterChange?: (adapter: AgentAdapter) => void; - isPreviewConnecting?: boolean; + isLoading?: boolean; autoFocus?: boolean; tourHighlight?: "model-selector" | "submit-button" | null; - onModelChange?: (model: string) => void; } export const TaskInputEditor = forwardRef< @@ -48,12 +50,13 @@ export const TaskInputEditor = forwardRef< directoryTooltip = "Select a folder first", onEmptyChange, adapter, - previewConfigId, + modelOption, + thoughtOption, + onConfigOptionChange, onAdapterChange, - isPreviewConnecting, + isLoading, autoFocus = true, tourHighlight, - onModelChange, }, ref, ) => { @@ -86,7 +89,7 @@ export const TaskInputEditor = forwardRef< submitDisabled: !isOnline, isLoading: isCreatingTask, autoFocus, - context: { repoPath, taskId: previewConfigId }, + context: { repoPath }, capabilities: { commands: true, bashMode: false }, clearOnSubmit: false, getPromptHistory, @@ -135,6 +138,14 @@ export const TaskInputEditor = forwardRef< return "Create task"; }; + const handleModelChange = (value: string) => { + onConfigOptionChange?.(modelOption?.id ?? "model", value); + }; + + const handleThoughtChange = (value: string) => { + onConfigOptionChange?.(thoughtOption?.id ?? "effort", value); + }; + return ( {})} disabled={isCreatingTask} - isConnecting={isPreviewConnecting} - onModelChange={onModelChange} + isConnecting={isLoading} + onModelChange={handleModelChange} /> - {!isPreviewConnecting && ( + {!isLoading && ( )} diff --git a/apps/code/src/renderer/features/task-detail/hooks/usePreviewConfig.ts b/apps/code/src/renderer/features/task-detail/hooks/usePreviewConfig.ts index 06c3d9ecd..e70027b4d 100644 --- a/apps/code/src/renderer/features/task-detail/hooks/usePreviewConfig.ts +++ b/apps/code/src/renderer/features/task-detail/hooks/usePreviewConfig.ts @@ -1,62 +1,143 @@ import type { SessionConfigOption } from "@agentclientprotocol/sdk"; import { useAuthStateValue } from "@features/auth/hooks/authQueries"; -import { - getSessionService, - PREVIEW_CONFIG_ID, -} from "@features/sessions/service/service"; -import { - useModeConfigOptionForTask, - useModelConfigOptionForTask, - useSessionForTask, - useThoughtLevelConfigOptionForTask, -} from "@features/sessions/stores/sessionStore"; -import { useEffect } from "react"; +import { useSettingsStore } from "@features/settings/stores/settingsStore"; +import { getEffortOptions } from "@posthog/agent/adapters/claude/session/models"; +import { trpcClient } from "@renderer/trpc/client"; +import { getCloudUrlFromRegion } from "@shared/constants/oauth"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; interface PreviewConfigResult { + configOptions: SessionConfigOption[]; modeOption: SessionConfigOption | undefined; modelOption: SessionConfigOption | undefined; thoughtOption: SessionConfigOption | undefined; - previewConfigId: string; - /** True while the preview config is loading (no config options yet) */ - isConnecting: boolean; + isLoading: boolean; + setConfigOption: (configId: string, value: string) => void; +} + +function getOptionByCategory( + options: SessionConfigOption[], + category: string, +): SessionConfigOption | undefined { + return options.find( + (opt) => opt.category === category || opt.id === category, + ); } /** - * Fetches adapter-specific config options (models, modes, reasoning levels) - * for the task input page without spawning a full agent session. + * Fetches config options (models, modes, effort levels) for the task input + * page via a lightweight tRPC query. No agent session is created. * - * Refetches when the adapter changes and cleans up on unmount. + * Returns config options as local state with a setter for local updates. */ export function usePreviewConfig( adapter: "claude" | "codex", ): PreviewConfigResult { - const projectId = useAuthStateValue((state) => state.projectId); + const cloudRegion = useAuthStateValue((state) => state.cloudRegion); + const apiHost = useMemo( + () => (cloudRegion ? getCloudUrlFromRegion(cloudRegion) : null), + [cloudRegion], + ); + const [configOptions, setConfigOptions] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const abortRef = useRef(null); useEffect(() => { - if (!projectId) return; + if (!apiHost) return; + + abortRef.current?.abort(); + const abort = new AbortController(); + abortRef.current = abort; + + setIsLoading(true); + + trpcClient.agent.getPreviewConfigOptions + .query({ apiHost, adapter }) + .then((options) => { + if (abort.signal.aborted) return; - const service = getSessionService(); - service.fetchPreviewConfig({ adapter }); + const { defaultInitialTaskMode, lastUsedInitialTaskMode } = + useSettingsStore.getState(); + const initialMode = + defaultInitialTaskMode === "last_used" + ? lastUsedInitialTaskMode + : "plan"; + + const withMode = options.map((opt) => + opt.id === "mode" + ? ({ ...opt, currentValue: initialMode } as SessionConfigOption) + : opt, + ); + + setConfigOptions(withMode); + setIsLoading(false); + }) + .catch((error) => { + if (abort.signal.aborted) return; + setIsLoading(false); + throw error; + }); return () => { - service.cancelPreviewConfig(); + abort.abort(); }; - }, [adapter, projectId]); + }, [adapter, apiHost]); + + const setConfigOption = useCallback((configId: string, value: string) => { + setConfigOptions((prev) => { + let updated = prev.map((opt) => + opt.id === configId + ? ({ ...opt, currentValue: value } as SessionConfigOption) + : opt, + ); + + if (configId === "model") { + const effortOpts = getEffortOptions(value); + const existingIdx = updated.findIndex((o) => o.id === "effort"); + + if (effortOpts && existingIdx >= 0) { + const currentEffort = updated[existingIdx].currentValue; + const validEffort = effortOpts.some((e) => e.value === currentEffort) + ? currentEffort + : "high"; + updated[existingIdx] = { + ...updated[existingIdx], + currentValue: validEffort, + options: effortOpts, + } as SessionConfigOption; + } else if (effortOpts && existingIdx === -1) { + updated = [ + ...updated, + { + id: "effort", + name: "Effort", + type: "select" as const, + currentValue: "high", + options: effortOpts, + category: "thought_level", + description: + "Controls how much effort Claude puts into its response", + } as SessionConfigOption, + ]; + } else if (!effortOpts && existingIdx >= 0) { + updated = updated.filter((o) => o.id !== "effort"); + } + } - const session = useSessionForTask(PREVIEW_CONFIG_ID); - const modeOption = useModeConfigOptionForTask(PREVIEW_CONFIG_ID); - const modelOption = useModelConfigOptionForTask(PREVIEW_CONFIG_ID); - const thoughtOption = useThoughtLevelConfigOptionForTask(PREVIEW_CONFIG_ID); + return updated; + }); + }, []); - // Connecting if we have a session but it's not connected yet, - // or if we don't have a session at all (start hasn't created one yet) - const isConnecting = !session || session.status === "connecting"; + const modeOption = getOptionByCategory(configOptions, "mode"); + const modelOption = getOptionByCategory(configOptions, "model"); + const thoughtOption = getOptionByCategory(configOptions, "thought_level"); return { + configOptions, modeOption, modelOption, thoughtOption, - previewConfigId: PREVIEW_CONFIG_ID, - isConnecting, + isLoading, + setConfigOption, }; } diff --git a/packages/agent/src/gateway-models.ts b/packages/agent/src/gateway-models.ts index a64b90d1a..16b142862 100644 --- a/packages/agent/src/gateway-models.ts +++ b/packages/agent/src/gateway-models.ts @@ -79,6 +79,15 @@ export function isAnthropicModel(model: GatewayModel): boolean { return model.id.startsWith("claude-") || model.id.startsWith("anthropic/"); } +export function isOpenAIModel( + model: GatewayModel | { id: string; owned_by?: string }, +): boolean { + if (model.owned_by) { + return model.owned_by === "openai"; + } + return model.id.startsWith("gpt-") || model.id.startsWith("openai/"); +} + export interface ModelInfo { id: string; owned_by?: string; From 421c644887801104be85318f076b79e616511b61 Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Wed, 1 Apr 2026 21:53:11 -0700 Subject: [PATCH 7/8] fix review feedback on preview config --- apps/code/src/main/services/agent/service.ts | 8 ++++---- .../features/task-detail/components/TaskInputEditor.tsx | 4 +++- .../features/task-detail/hooks/usePreviewConfig.ts | 7 +++++-- packages/agent/src/gateway-models.ts | 4 +--- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/apps/code/src/main/services/agent/service.ts b/apps/code/src/main/services/agent/service.ts index e26466915..a21ecc5e9 100644 --- a/apps/code/src/main/services/agent/service.ts +++ b/apps/code/src/main/services/agent/service.ts @@ -1569,7 +1569,7 @@ For git operations while detached: { id: "mode", name: "Approval Preset", - type: "select" as const, + type: "select", currentValue: "plan", options: modeOptions, category: "mode", @@ -1579,7 +1579,7 @@ For git operations while detached: { id: "model", name: "Model", - type: "select" as const, + type: "select", currentValue: resolvedModelId, options: modelOptions, category: "model", @@ -1591,7 +1591,7 @@ For git operations while detached: configOptions.push({ id: "reasoning_effort", name: "Reasoning Level", - type: "select" as const, + type: "select", currentValue: "high", options: [ { value: "low", name: "Low" }, @@ -1607,7 +1607,7 @@ For git operations while detached: configOptions.push({ id: "effort", name: "Effort", - type: "select" as const, + type: "select", currentValue: "high", options: effortOpts, category: "thought_level", diff --git a/apps/code/src/renderer/features/task-detail/components/TaskInputEditor.tsx b/apps/code/src/renderer/features/task-detail/components/TaskInputEditor.tsx index d892ed274..22bf2a48d 100644 --- a/apps/code/src/renderer/features/task-detail/components/TaskInputEditor.tsx +++ b/apps/code/src/renderer/features/task-detail/components/TaskInputEditor.tsx @@ -143,7 +143,9 @@ export const TaskInputEditor = forwardRef< }; const handleThoughtChange = (value: string) => { - onConfigOptionChange?.(thoughtOption?.id ?? "effort", value); + if (thoughtOption) { + onConfigOptionChange?.(thoughtOption.id, value); + } }; return ( diff --git a/apps/code/src/renderer/features/task-detail/hooks/usePreviewConfig.ts b/apps/code/src/renderer/features/task-detail/hooks/usePreviewConfig.ts index e70027b4d..4687b214a 100644 --- a/apps/code/src/renderer/features/task-detail/hooks/usePreviewConfig.ts +++ b/apps/code/src/renderer/features/task-detail/hooks/usePreviewConfig.ts @@ -4,8 +4,11 @@ import { useSettingsStore } from "@features/settings/stores/settingsStore"; import { getEffortOptions } from "@posthog/agent/adapters/claude/session/models"; import { trpcClient } from "@renderer/trpc/client"; import { getCloudUrlFromRegion } from "@shared/constants/oauth"; +import { logger } from "@utils/logger"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +const log = logger.scope("preview-config"); + interface PreviewConfigResult { configOptions: SessionConfigOption[]; modeOption: SessionConfigOption | undefined; @@ -74,8 +77,8 @@ export function usePreviewConfig( }) .catch((error) => { if (abort.signal.aborted) return; + log.error("Failed to fetch preview config options", { error }); setIsLoading(false); - throw error; }); return () => { @@ -111,7 +114,7 @@ export function usePreviewConfig( { id: "effort", name: "Effort", - type: "select" as const, + type: "select", currentValue: "high", options: effortOpts, category: "thought_level", diff --git a/packages/agent/src/gateway-models.ts b/packages/agent/src/gateway-models.ts index 16b142862..82e6bd956 100644 --- a/packages/agent/src/gateway-models.ts +++ b/packages/agent/src/gateway-models.ts @@ -79,9 +79,7 @@ export function isAnthropicModel(model: GatewayModel): boolean { return model.id.startsWith("claude-") || model.id.startsWith("anthropic/"); } -export function isOpenAIModel( - model: GatewayModel | { id: string; owned_by?: string }, -): boolean { +export function isOpenAIModel(model: GatewayModel): boolean { if (model.owned_by) { return model.owned_by === "openai"; } From e2a1a1702ee6b25532a11c787d909ebc8bb45d2e Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Wed, 1 Apr 2026 21:56:22 -0700 Subject: [PATCH 8/8] guard handleModelChange on modelOption being defined --- .../features/task-detail/components/TaskInputEditor.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/code/src/renderer/features/task-detail/components/TaskInputEditor.tsx b/apps/code/src/renderer/features/task-detail/components/TaskInputEditor.tsx index 22bf2a48d..8335e39bf 100644 --- a/apps/code/src/renderer/features/task-detail/components/TaskInputEditor.tsx +++ b/apps/code/src/renderer/features/task-detail/components/TaskInputEditor.tsx @@ -139,7 +139,9 @@ export const TaskInputEditor = forwardRef< }; const handleModelChange = (value: string) => { - onConfigOptionChange?.(modelOption?.id ?? "model", value); + if (modelOption) { + onConfigOptionChange?.(modelOption.id, value); + } }; const handleThoughtChange = (value: string) => {