From a1395b2edaf41f890159fe954ca6262c4c294f79 Mon Sep 17 00:00:00 2001 From: Leonid Skorobogatyy Date: Mon, 22 Jun 2026 15:38:34 +1100 Subject: [PATCH] fix(git): materialize draft session for generate --- packages/ui/src/lib/gitApi.ts | 87 ++++++++++---- packages/ui/src/sync/session-ui-store.ts | 143 +++++++++++++++-------- 2 files changed, 159 insertions(+), 71 deletions(-) diff --git a/packages/ui/src/lib/gitApi.ts b/packages/ui/src/lib/gitApi.ts index e2c368954f..a177b7c3d3 100644 --- a/packages/ui/src/lib/gitApi.ts +++ b/packages/ui/src/lib/gitApi.ts @@ -2,8 +2,8 @@ import * as gitHttp from './gitApiHttp'; import { opencodeClient } from './opencode/client'; import { renderMagicPrompt } from './magicPrompts'; -import { useSessionUIStore } from '@/sync/session-ui-store'; -import { useContextStore } from '@/stores/contextStore'; +import { materializeOpenDraftSession, useSessionUIStore } from '@/sync/session-ui-store'; +import { useSelectionStore } from '@/sync/selection-store'; import { useConfigStore } from '@/stores/useConfigStore'; import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry'; @@ -217,11 +217,7 @@ export async function generateCommitMessage( ): Promise<{ message: import('./api/types').GeneratedCommitMessage }> { const startedAt = Date.now(); void options; - const generationSession = resolveSessionGenerationContext(); - - if (!generationSession) { - throw new Error('Select existing session for generation'); - } + const generationSession = await resolveGenerationSessionContext(); console.info('[git-generation][browser] request', { transport: 'session', @@ -283,10 +279,7 @@ export async function generatePullRequestDescription( payload: { base: string; head: string; context?: string; zenModel?: string; providerId?: string; modelId?: string } ): Promise { const startedAt = Date.now(); - const generationSession = resolveSessionGenerationContext(); - if (!generationSession) { - throw new Error('Select existing session for generation'); - } + const generationSession = await resolveGenerationSessionContext(); const commitLog = await getGitLog(directory, { from: payload.base, @@ -387,19 +380,65 @@ type SessionGenerationContext = { variant?: string; }; +const GENERATION_CONFIG_ERROR = 'No default provider or model configured. Please select a provider and model in settings first.'; + +async function resolveGenerationSessionContext(): Promise { + const activeSession = resolveSessionGenerationContext(); + if (activeSession) { + return activeSession; + } + + const draft = useSessionUIStore.getState().newSessionDraft; + if (!draft?.open) { + throw new Error('Select existing session for generation'); + } + + const config = useConfigStore.getState(); + if (!config.currentProviderId || !config.currentModelId) { + throw new Error(GENERATION_CONFIG_ERROR); + } + + const createdDraftSession = await materializeOpenDraftSession({ + providerID: config.currentProviderId, + modelID: config.currentModelId, + agent: config.currentAgentName || undefined, + variant: config.currentVariant || undefined, + }); + + if (!createdDraftSession) { + const retry = resolveSessionGenerationContext(); + if (retry) { + return retry; + } + throw new Error('Failed to create session for generation'); + } + + return { + sessionId: createdDraftSession.sessionId, + providerID: config.currentProviderId, + modelID: config.currentModelId, + agent: createdDraftSession.agent, + variant: config.currentVariant || undefined, + }; +} + const resolveSessionGenerationContext = (): SessionGenerationContext | null => { const sessionId = useSessionUIStore.getState().currentSessionId; if (!sessionId) { return null; } - const context = useContextStore.getState(); + const selection = useSelectionStore.getState(); const config = useConfigStore.getState(); - - const agent = context.getSessionAgentSelection(sessionId) || config.currentAgentName || undefined; - const sessionModel = context.getSessionModelSelection(sessionId); - const agentModel = agent ? context.getAgentModelForSession(sessionId, agent) : null; - const selectedModel = agentModel || sessionModel || (config.currentProviderId && config.currentModelId + const lastChoice = useSessionUIStore.getState().getLastUserChoice(sessionId); + + const agent = selection.getSessionAgentSelection(sessionId) || lastChoice?.agent || config.currentAgentName || undefined; + const sessionModel = selection.getSessionModelSelection(sessionId); + const agentModel = agent ? selection.getAgentModelForSession(sessionId, agent) : null; + const lastChoiceModel = lastChoice?.providerID && lastChoice.modelID + ? { providerId: lastChoice.providerID, modelId: lastChoice.modelID } + : null; + const selectedModel = agentModel || sessionModel || lastChoiceModel || (config.currentProviderId && config.currentModelId ? { providerId: config.currentProviderId, modelId: config.currentModelId } : null); @@ -407,10 +446,18 @@ const resolveSessionGenerationContext = (): SessionGenerationContext | null => { return null; } - const agentVariant = agent - ? context.getAgentModelVariantForSession(sessionId, agent, selectedModel.providerId, selectedModel.modelId) + const selectionVariant = agent + ? selection.getAgentModelVariantForSession(sessionId, agent, selectedModel.providerId, selectedModel.modelId) + : undefined; + const lastChoiceVariant = lastChoiceModel + && lastChoiceModel.providerId === selectedModel.providerId + && lastChoiceModel.modelId === selectedModel.modelId + ? lastChoice?.variant + : undefined; + const configVariant = config.currentProviderId === selectedModel.providerId && config.currentModelId === selectedModel.modelId + ? config.currentVariant : undefined; - const variant = agentVariant || config.currentVariant || undefined; + const variant = selectionVariant || lastChoiceVariant || configVariant || undefined; return { sessionId, diff --git a/packages/ui/src/sync/session-ui-store.ts b/packages/ui/src/sync/session-ui-store.ts index 374dc1a0fb..45dde68e27 100644 --- a/packages/ui/src/sync/session-ui-store.ts +++ b/packages/ui/src/sync/session-ui-store.ts @@ -29,6 +29,7 @@ import { markPendingUserSendAnimation } from "@/lib/userSendAnimation" import { flattenAssistantTextParts } from "@/lib/messages/messageText" import { composeForkSessionMessage } from "@/lib/messages/executionMeta" import { waitForPendingDraftWorktreeRequest } from "@/lib/worktrees/pendingDraftWorktree" +import { waitForWorktreeBootstrap } from "@/lib/worktrees/worktreeBootstrap" import { resolveProjectForSessionDirectory } from "@/lib/projectResolution" import { getSyncSessions, @@ -401,6 +402,84 @@ const writeRuntimeSessionMemory = (key: string, patch: Partial { + const store = useSessionUIStore.getState() + const draft = store.newSessionDraft + if (!draft?.open) return null + + const trimmedAgent = typeof selection.agent === "string" && selection.agent.trim().length > 0 + ? selection.agent.trim() + : undefined + const draftTargetFolderId = draft.targetFolderId + let draftDirectoryOverride = draft.bootstrapPendingDirectory ?? draft.directoryOverride ?? null + const draftProjectId = draft.selectedProjectId ?? null + + if (draft.pendingWorktreeRequestId) { + draftDirectoryOverride = await waitForPendingDraftWorktreeRequest(draft.pendingWorktreeRequestId) + store.resolvePendingDraftWorktreeTarget(draft.pendingWorktreeRequestId, draftDirectoryOverride) + } + + if (draftDirectoryOverride) { + await waitForWorktreeBootstrap(draftDirectoryOverride) + } + + const created = await store.createSession(draft.title, draftDirectoryOverride, draft.parentID ?? null) + if (!created?.id) throw new Error("Failed to create session") + + persistDraftTarget({ + projectId: draftProjectId, + directory: normalizePath(draftDirectoryOverride ?? created.directory ?? null), + }) + + const draftSyntheticParts = draft.syntheticParts + const createdDirectory = normalizePath(draftDirectoryOverride ?? created.directory ?? null) + const configState = useConfigStore.getState() + void activateConfigForDirectory(createdDirectory).catch((error) => { + console.warn("Failed to activate directory after creating session:", error) + }) + + const effectiveDraftAgent = trimmedAgent ?? configState.currentAgentName + + useSelectionStore.getState().saveSessionModelSelection(created.id, selection.providerID, selection.modelID) + + if (effectiveDraftAgent) { + useSelectionStore.getState().saveSessionAgentSelection(created.id, effectiveDraftAgent) + useSelectionStore.getState().saveAgentModelForSession(created.id, effectiveDraftAgent, selection.providerID, selection.modelID) + useSelectionStore.getState().saveAgentModelVariantForSession(created.id, effectiveDraftAgent, selection.providerID, selection.modelID, selection.variant) + } + + store.initializeNewOpenChamberSession(created.id, configState.agents ?? []) + + store.closeNewSessionDraft() + store.setCurrentSession(created.id, createdDirectory) + + if (draftTargetFolderId) { + const scopeKey = draftDirectoryOverride || created.directory || null + if (scopeKey) { + useSessionFoldersStore.getState().addSessionToFolder(scopeKey, draftTargetFolderId, created.id) + } + } + + return { + sessionId: created.id, + directory: createdDirectory, + agent: effectiveDraftAgent, + syntheticParts: draftSyntheticParts, + } +} + // --------------------------------------------------------------------------- // Store // --------------------------------------------------------------------------- @@ -913,59 +992,21 @@ export const useSessionUIStore = create()((set, get) => ({ // ---- New session from draft ---- if (!options?.sessionId && draft?.open) { - const draftTargetFolderId = draft.targetFolderId - let draftDirectoryOverride = draft.bootstrapPendingDirectory ?? draft.directoryOverride ?? null - const draftProjectId = draft.selectedProjectId ?? null - - if (draft.pendingWorktreeRequestId) { - draftDirectoryOverride = await waitForPendingDraftWorktreeRequest(draft.pendingWorktreeRequestId) - get().resolvePendingDraftWorktreeTarget(draft.pendingWorktreeRequestId, draftDirectoryOverride) - } - - const created = await get().createSession(draft.title, draftDirectoryOverride, draft.parentID ?? null) - if (!created?.id) throw new Error("Failed to create session") - - persistDraftTarget({ - projectId: draftProjectId, - directory: normalizePath(draftDirectoryOverride ?? created.directory ?? null), - }) - - const draftSyntheticParts = draft.syntheticParts - const createdDirectory = normalizePath(draftDirectoryOverride ?? created.directory ?? null) - const configState = useConfigStore.getState() - void activateConfigForDirectory(createdDirectory).catch((error) => { - console.warn("Failed to activate directory after creating session:", error) + const createdDraftSession = await materializeOpenDraftSession({ + providerID, + modelID, + agent: trimmedAgent, + variant, }) + if (!createdDraftSession) throw new Error("Failed to create session") - const effectiveDraftAgent = trimmedAgent ?? configState.currentAgentName - - useSelectionStore.getState().saveSessionModelSelection(created.id, providerID, modelID) - - if (effectiveDraftAgent) { - useSelectionStore.getState().saveSessionAgentSelection(created.id, effectiveDraftAgent) - useSelectionStore.getState().saveAgentModelForSession(created.id, effectiveDraftAgent, providerID, modelID) - useSelectionStore.getState().saveAgentModelVariantForSession(created.id, effectiveDraftAgent, providerID, modelID, variant) - } - - get().initializeNewOpenChamberSession(created.id, configState.agents ?? []) - - get().closeNewSessionDraft() - get().setCurrentSession(created.id, createdDirectory) - - if (draftTargetFolderId) { - const scopeKey = draftDirectoryOverride || created.directory || null - if (scopeKey) { - useSessionFoldersStore.getState().addSessionToFolder(scopeKey, draftTargetFolderId, created.id) - } - } - - const mergedAdditionalParts = draftSyntheticParts?.length - ? [...(additionalParts || []), ...draftSyntheticParts] + const mergedAdditionalParts = createdDraftSession.syntheticParts?.length + ? [...(additionalParts || []), ...createdDraftSession.syntheticParts] : additionalParts - notifyMessageSent(created.id) + notifyMessageSent(createdDraftSession.sessionId) - markPendingUserSendAnimation(created.id) + markPendingUserSendAnimation(createdDraftSession.sessionId) const files = attachments?.map((a) => ({ type: "file" as const, @@ -975,12 +1016,12 @@ export const useSessionUIStore = create()((set, get) => ({ })) await routeMessage({ - sessionId: created.id, - directory: createdDirectory, + sessionId: createdDraftSession.sessionId, + directory: createdDraftSession.directory, content, providerID, modelID, - agent: effectiveDraftAgent, + agent: createdDraftSession.agent, agentMentionName, variant, inputMode,