Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 67 additions & 20 deletions packages/ui/src/lib/gitApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -387,30 +380,84 @@ 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 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;
Comment on lines +438 to +440

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For better type safety and consistency, use optional chaining on lastChoice.modelID as well. This prevents potential static analysis or TypeScript compiler warnings under strict null-checking configurations.

Suggested change
const lastChoiceModel = lastChoice?.providerID && lastChoice.modelID
? { providerId: lastChoice.providerID, modelId: lastChoice.modelID }
: 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);

if (!selectedModel?.providerId || !selectedModel?.modelId) {
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,
Expand Down
143 changes: 92 additions & 51 deletions packages/ui/src/sync/session-ui-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -401,6 +402,84 @@ 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
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)
Comment on lines +441 to +447

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Instead of accessing created.directory directly (which is not type-safe on the Session object and doesn't handle fallback fields like project.worktree), we should leverage the existing resolveDirectoryKey helper. We can define createdDirectory first and reuse it for both persistDraftTarget and subsequent operations to keep the code DRY and robust.

Suggested change
persistDraftTarget({
projectId: draftProjectId,
directory: normalizePath(draftDirectoryOverride ?? created.directory ?? null),
})
const draftSyntheticParts = draft.syntheticParts
const createdDirectory = normalizePath(draftDirectoryOverride ?? created.directory ?? null)
const createdDirectory = normalizePath(draftDirectoryOverride) ?? resolveDirectoryKey(created)
persistDraftTarget({
projectId: draftProjectId,
directory: createdDirectory,
})
const draftSyntheticParts = draft.syntheticParts

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)
}
}
Comment on lines +468 to +473

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

We can simplify this block by reusing the already resolved and normalized createdDirectory variable instead of duplicating the fallback logic with draftDirectoryOverride || created.directory || null.

  if (draftTargetFolderId && createdDirectory) {
    useSessionFoldersStore.getState().addSessionToFolder(createdDirectory, draftTargetFolderId, created.id)
  }


return {
sessionId: created.id,
directory: createdDirectory,
agent: effectiveDraftAgent,
syntheticParts: draftSyntheticParts,
}
}

// ---------------------------------------------------------------------------
// Store
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -913,59 +992,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 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,
Expand All @@ -975,12 +1016,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,
Expand Down
Loading