fix(git): materialize draft session for generate (#1761)

* fix(git): materialize draft session for generate

* fix(sync): remove redundant draft session side effects

---------

Co-authored-by: Leonid Skorobogatyy <bash@opencode.itc.local>
This commit is contained in:
bashrusakh
2026-06-26 11:52:39 +03:00
committed by GitHub
co-authored by Leonid Skorobogatyy
parent b0e476ab7c
commit 03e6f789a4
2 changed files with 149 additions and 70 deletions
+66 -19
View File
@@ -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<import('./api/types').GeneratedPullRequestDescription> {
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<SessionGenerationContext> {
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 lastChoice = useSessionUIStore.getState().getLastUserChoice(sessionId);
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 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 variant = agentVariant || config.currentVariant || 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 = selectionVariant || lastChoiceVariant || configVariant || undefined;
return {
sessionId,
+83 -51
View File
@@ -30,6 +30,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,
@@ -408,6 +409,75 @@ const writeRuntimeSessionMemory = (key: string, patch: Partial<RuntimeSessionMem
})
}
type MaterializedDraftSession = {
sessionId: string
directory: string | null
agent?: string
syntheticParts?: SyntheticContextPart[]
}
export async function materializeOpenDraftSession(selection: {
providerID: string
modelID: string
agent?: string
variant?: string
}): Promise<MaterializedDraftSession | null> {
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
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.setCurrentSession(created.id, createdDirectory)
return {
sessionId: created.id,
directory: createdDirectory,
agent: effectiveDraftAgent,
syntheticParts: draftSyntheticParts,
}
}
// ---------------------------------------------------------------------------
// Store
// ---------------------------------------------------------------------------
@@ -919,59 +989,21 @@ export const useSessionUIStore = create<SessionUIState>()((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 createdDraftSession = await materializeOpenDraftSession({
providerID,
modelID,
agent: trimmedAgent,
variant,
})
if (!createdDraftSession) throw new Error("Failed to create session")
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, 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,
@@ -981,12 +1013,12 @@ export const useSessionUIStore = create<SessionUIState>()((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,