diff --git a/apps/web/src/hooks/useHandleNewThread.ts b/apps/web/src/hooks/useHandleNewThread.ts index 8d50e39972d1..32e16832b5f4 100644 --- a/apps/web/src/hooks/useHandleNewThread.ts +++ b/apps/web/src/hooks/useHandleNewThread.ts @@ -5,7 +5,6 @@ import { } from "@t3tools/client-runtime/environment"; import { DEFAULT_SERVER_SETTINGS, type ScopedProjectRef, type ThreadId } from "@t3tools/contracts"; import { useParams, useRouter } from "@tanstack/react-router"; -import { useCallback, useMemo } from "react"; import { composerDraftHasUserContent, markPromotedDraftThreadByRef, @@ -61,335 +60,342 @@ function pickExplicitWorkspaceOptions(options: NewThreadWorkspaceOptions | undef export function useNewThreadHandler() { const projectGroupingSettings = useClientSettings(selectProjectGroupingSettings); const router = useRouter(); - const getCurrentRouteTarget = useCallback(() => { + const getCurrentRouteTarget = () => { const currentRouteParams = router.state.matches[router.state.matches.length - 1]?.params ?? {}; return resolveThreadRouteTarget(currentRouteParams); - }, [router]); + }; - return useCallback( - ( - projectRef: ScopedProjectRef, - options?: { - branch?: string | null; - worktreePath?: string | null; - envMode?: DraftThreadEnvMode; - startFromOrigin?: boolean; - replace?: boolean; - }, - ): Promise<{ draftId: DraftId; threadId: ThreadId } | null> => { - const projects = readProjects(); - // Read at call time: every provider status push replaces the config map, and - // subscribing would hand every sidebar row a new handler. - const environmentServerConfigs = appAtomRegistry.get(environmentServerConfigsAtom); - const targetServerSettings = - environmentServerConfigs.get(projectRef.environmentId)?.settings ?? DEFAULT_SERVER_SETTINGS; - const { - getComposerDraft, - getDraftSessionByLogicalProjectKey, - getDraftSession, - getDraftThread, - applyStickyState, - setDraftThreadContext, - setLogicalProjectDraftThreadId, - setModelSelection, - } = useComposerDraftStore.getState(); - const requestingRouteHref = router.state.location.href; - const routeChangedSinceRequest = () => router.state.location.href !== requestingRouteHref; - const currentRouteTarget = getCurrentRouteTarget(); - const carrySourceShell = - currentRouteTarget?.kind === "server" - ? readThreadShell(currentRouteTarget.threadRef) - : null; - const carrySourceDraft = - currentRouteTarget?.kind === "draft" ? getDraftSession(currentRouteTarget.draftId) : null; - const carrySourceComposer = currentRouteTarget - ? getComposerDraft( - currentRouteTarget.kind === "server" - ? currentRouteTarget.threadRef - : currentRouteTarget.draftId, - ) - : null; - const composerActiveProvider = carrySourceComposer?.activeProvider ?? null; - const composerModelSelection = composerActiveProvider - ? (carrySourceComposer?.modelSelectionByProvider[composerActiveProvider] ?? null) + return ( + projectRef: ScopedProjectRef, + options?: { + branch?: string | null; + worktreePath?: string | null; + envMode?: DraftThreadEnvMode; + startFromOrigin?: boolean; + replace?: boolean; + }, + ): Promise<{ draftId: DraftId; threadId: ThreadId } | null> => { + const projects = readProjects(); + // Read at call time: every provider status push replaces the config map, and + // subscribing would hand every sidebar row a new handler. + const environmentServerConfigs = appAtomRegistry.get(environmentServerConfigsAtom); + const targetServerSettings = + environmentServerConfigs.get(projectRef.environmentId)?.settings ?? DEFAULT_SERVER_SETTINGS; + const { + getComposerDraft, + getDraftSessionByLogicalProjectKey, + getDraftSession, + getDraftThread, + applyStickyState, + setDraftThreadContext, + setLogicalProjectDraftThreadId, + setModelSelection, + } = useComposerDraftStore.getState(); + const requestingRouteHref = router.state.location.href; + const routeChangedSinceRequest = () => router.state.location.href !== requestingRouteHref; + const currentRouteTarget = getCurrentRouteTarget(); + const carrySourceShell = + currentRouteTarget?.kind === "server" ? readThreadShell(currentRouteTarget.threadRef) : null; + const carrySourceDraft = + currentRouteTarget?.kind === "draft" ? getDraftSession(currentRouteTarget.draftId) : null; + const carrySourceComposer = currentRouteTarget + ? getComposerDraft( + currentRouteTarget.kind === "server" + ? currentRouteTarget.threadRef + : currentRouteTarget.draftId, + ) + : null; + const composerActiveProvider = carrySourceComposer?.activeProvider ?? null; + const composerModelSelection = composerActiveProvider + ? (carrySourceComposer?.modelSelectionByProvider[composerActiveProvider] ?? null) + : null; + const carryModelSelection = composerModelSelection ?? carrySourceShell?.modelSelection ?? null; + const carryInteractionMode = + carrySourceComposer?.interactionMode ?? + carrySourceShell?.interactionMode ?? + carrySourceDraft?.interactionMode ?? + null; + const project = projects.find( + (candidate) => + candidate.id === projectRef.projectId && + candidate.environmentId === projectRef.environmentId, + ); + const abandonedSinceRequest = () => + routeChangedSinceRequest() || (project !== undefined && readProject(projectRef) === null); + const projectSettings = resolveProjectSettings( + targetServerSettings, + project?.id ?? null, + project, + ); + const projectDefaultModelSelection = projectSettings.settings.defaultModelSelection; + const defaultRuntimeMode = projectSettings.settings.defaultRuntimeMode; + const resolveModelSelectionOverride = (destinationDraftId: DraftId) => + resolveNewThreadModelSelectionOverride({ + projectDefaultSelection: projectDefaultModelSelection ?? null, + carrySelection: carryModelSelection, + carrySourceDraftId: + currentRouteTarget?.kind === "draft" ? currentRouteTarget.draftId : null, + destinationDraftId, + }); + const resolveDefaultEnvMode = async (): Promise => { + const consultProjectFile = + project !== undefined && projectSettings.settings.defaultThreadEnvMode === null; + const projectFile = consultProjectFile + ? await readT3ProjectFile(project.environmentId, project.workspaceRoot) : null; - const carryModelSelection = - composerModelSelection ?? carrySourceShell?.modelSelection ?? null; - const carryInteractionMode = - carrySourceComposer?.interactionMode ?? - carrySourceShell?.interactionMode ?? - carrySourceDraft?.interactionMode ?? - null; - const project = projects.find( - (candidate) => - candidate.id === projectRef.projectId && - candidate.environmentId === projectRef.environmentId, - ); - const abandonedSinceRequest = () => - routeChangedSinceRequest() || (project !== undefined && readProject(projectRef) === null); - const projectSettings = resolveProjectSettings( - targetServerSettings, - project?.id ?? null, - project, - ); - const projectDefaultModelSelection = projectSettings.settings.defaultModelSelection; - const defaultRuntimeMode = projectSettings.settings.defaultRuntimeMode; - const resolveModelSelectionOverride = (destinationDraftId: DraftId) => - resolveNewThreadModelSelectionOverride({ - projectDefaultSelection: projectDefaultModelSelection ?? null, - carrySelection: carryModelSelection, - carrySourceDraftId: - currentRouteTarget?.kind === "draft" ? currentRouteTarget.draftId : null, - destinationDraftId, - }); - const resolveDefaultEnvMode = async (): Promise => { - const consultProjectFile = - project !== undefined && projectSettings.settings.defaultThreadEnvMode === null; - const projectFile = consultProjectFile - ? await readT3ProjectFile(project.environmentId, project.workspaceRoot) - : null; - return resolveNewThreadEnvMode({ - projectSettings: resolveProjectSettings( - targetServerSettings, - project?.id ?? null, - project, - projectFile, - ), - workspaceRoot: project?.workspaceRoot ?? null, - newProjectsRoot: environmentServerConfigs.get(projectRef.environmentId)?.newProjectsRoot, - projectHasThreads: readThreadShells().some( - (thread) => - thread.environmentId === projectRef.environmentId && - thread.projectId === projectRef.projectId, - ), - }); - }; - const logicalProjectKey = project - ? deriveLogicalProjectKeyFromSettings(project, projectGroupingSettings) - : scopedProjectKey(projectRef); - const hasBranchOption = options?.branch !== undefined; - const hasWorktreePathOption = options?.worktreePath !== undefined; - const hasEnvModeOption = options?.envMode !== undefined; - const hasStartFromOriginOption = options?.startFromOrigin !== undefined; - const storedDraftThread = getDraftSessionByLogicalProjectKey(logicalProjectKey); - const storedDraftThreadRef = storedDraftThread - ? scopeThreadRef(storedDraftThread.environmentId, storedDraftThread.threadId) + return resolveNewThreadEnvMode({ + projectSettings: resolveProjectSettings( + targetServerSettings, + project?.id ?? null, + project, + projectFile, + ), + workspaceRoot: project?.workspaceRoot ?? null, + newProjectsRoot: environmentServerConfigs.get(projectRef.environmentId)?.newProjectsRoot, + projectHasThreads: readThreadShells().some( + (thread) => + thread.environmentId === projectRef.environmentId && + thread.projectId === projectRef.projectId, + ), + }); + }; + const logicalProjectKey = project + ? deriveLogicalProjectKeyFromSettings(project, projectGroupingSettings) + : scopedProjectKey(projectRef); + const hasBranchOption = options?.branch !== undefined; + const hasWorktreePathOption = options?.worktreePath !== undefined; + const hasEnvModeOption = options?.envMode !== undefined; + const hasStartFromOriginOption = options?.startFromOrigin !== undefined; + const storedDraftThread = getDraftSessionByLogicalProjectKey(logicalProjectKey); + const storedDraftThreadRef = storedDraftThread + ? scopeThreadRef(storedDraftThread.environmentId, storedDraftThread.threadId) + : null; + const reusableStoredDraftThread = + storedDraftThread !== null && + storedDraftThread.promotedTo == null && + storedDraftThreadRef !== null && + readThreadShell(storedDraftThreadRef) === null + ? storedDraftThread : null; - const reusableStoredDraftThread = - storedDraftThread !== null && - storedDraftThread.promotedTo == null && - storedDraftThreadRef !== null && - readThreadShell(storedDraftThreadRef) === null - ? storedDraftThread - : null; - if (storedDraftThreadRef && reusableStoredDraftThread === null) { - markPromotedDraftThreadByRef(storedDraftThreadRef); - } - const emptyStoredDraftThread = - reusableStoredDraftThread && - !composerDraftHasUserContent(getComposerDraft(reusableStoredDraftThread.draftId)) - ? reusableStoredDraftThread - : null; - const latestActiveDraftThread: DraftThreadState | null = currentRouteTarget - ? currentRouteTarget.kind === "server" - ? getDraftThread(currentRouteTarget.threadRef) - : getDraftSession(currentRouteTarget.draftId) + if (storedDraftThreadRef && reusableStoredDraftThread === null) { + markPromotedDraftThreadByRef(storedDraftThreadRef); + } + const emptyStoredDraftThread = + reusableStoredDraftThread && + !composerDraftHasUserContent(getComposerDraft(reusableStoredDraftThread.draftId)) + ? reusableStoredDraftThread : null; - if (emptyStoredDraftThread) { - return (async () => { - const isDraftAlreadyOpen = - currentRouteTarget?.kind === "draft" && - currentRouteTarget.draftId === emptyStoredDraftThread.draftId; - const hasExplicitWorkspaceOption = - hasBranchOption || - hasWorktreePathOption || - hasEnvModeOption || - hasStartFromOriginOption; - let workspaceContext: NewThreadWorkspaceOptions | null = null; - if (hasExplicitWorkspaceOption) { - workspaceContext = pickExplicitWorkspaceOptions(options); - } else if (!isDraftAlreadyOpen) { - const defaultEnvMode = await resolveDefaultEnvMode(); - if (abandonedSinceRequest()) { - return null; - } - // If the draft was opened (a concurrent invocation's navigation - // landed), promoted to a real thread, remapped away (a concurrent - // invocation registered a fresh draft — remapping back would evict - // the winner and let the store GC it), or gained content (no longer - // a reusable empty draft) in the meantime, this invocation is a - // stale loser: resetting context, remapping, or navigating would - // all clobber state written after the snapshot above. - const routeTargetNow = getCurrentRouteTarget(); - const openedMeanwhile = - routeTargetNow?.kind === "draft" && - routeTargetNow.draftId === emptyStoredDraftThread.draftId; - const promotedMeanwhile = - storedDraftThreadRef !== null && readThreadShell(storedDraftThreadRef) !== null; - const remappedMeanwhile = - getDraftSessionByLogicalProjectKey(logicalProjectKey)?.draftId !== - emptyStoredDraftThread.draftId; - const investedMeanwhile = composerDraftHasUserContent( - getComposerDraft(emptyStoredDraftThread.draftId), - ); - if (openedMeanwhile || promotedMeanwhile || remappedMeanwhile || investedMeanwhile) { - return null; - } - workspaceContext = { - branch: null, - worktreePath: null, - envMode: defaultEnvMode, - startFromOrigin: resolveNewDraftStartFromOrigin({ - envMode: defaultEnvMode, - newWorktreesStartFromOrigin: projectSettings.settings.newWorktreesStartFromOrigin, - }), - }; - } - if (workspaceContext) { - setDraftThreadContext(emptyStoredDraftThread.draftId, { - ...workspaceContext, - ...(!isDraftAlreadyOpen ? { runtimeMode: defaultRuntimeMode } : {}), - ...(carryInteractionMode ? { interactionMode: carryInteractionMode } : {}), - }); + const latestActiveDraftThread: DraftThreadState | null = currentRouteTarget + ? currentRouteTarget.kind === "server" + ? getDraftThread(currentRouteTarget.threadRef) + : getDraftSession(currentRouteTarget.draftId) + : null; + if (emptyStoredDraftThread) { + return (async () => { + const isDraftAlreadyOpen = + currentRouteTarget?.kind === "draft" && + currentRouteTarget.draftId === emptyStoredDraftThread.draftId; + const hasExplicitWorkspaceOption = + hasBranchOption || hasWorktreePathOption || hasEnvModeOption || hasStartFromOriginOption; + let workspaceContext: NewThreadWorkspaceOptions | null = null; + if (hasExplicitWorkspaceOption) { + workspaceContext = pickExplicitWorkspaceOptions(options); + } else if (!isDraftAlreadyOpen) { + const defaultEnvMode = await resolveDefaultEnvMode(); + if (abandonedSinceRequest()) { + return null; } - const storedDraft = getComposerDraft(emptyStoredDraftThread.draftId); - const storedDraftHasExplicitModelPick = hasExplicitComposerModelSelection(storedDraft); - if (!storedDraftHasExplicitModelPick) { - applyStickyState(emptyStoredDraftThread.draftId); - const modelSelectionOverride = resolveModelSelectionOverride( - emptyStoredDraftThread.draftId, - ); - if (modelSelectionOverride) { - setModelSelection(emptyStoredDraftThread.draftId, modelSelectionOverride, { - replaceOptions: true, - }); - } + // If the draft was opened (a concurrent invocation's navigation + // landed), promoted to a real thread, remapped away (a concurrent + // invocation registered a fresh draft — remapping back would evict + // the winner and let the store GC it), or gained content (no longer + // a reusable empty draft) in the meantime, this invocation is a + // stale loser: resetting context, remapping, or navigating would + // all clobber state written after the snapshot above. + const routeTargetNow = getCurrentRouteTarget(); + const openedMeanwhile = + routeTargetNow?.kind === "draft" && + routeTargetNow.draftId === emptyStoredDraftThread.draftId; + const promotedMeanwhile = + storedDraftThreadRef !== null && readThreadShell(storedDraftThreadRef) !== null; + const remappedMeanwhile = + getDraftSessionByLogicalProjectKey(logicalProjectKey)?.draftId !== + emptyStoredDraftThread.draftId; + const investedMeanwhile = composerDraftHasUserContent( + getComposerDraft(emptyStoredDraftThread.draftId), + ); + if (openedMeanwhile || promotedMeanwhile || remappedMeanwhile || investedMeanwhile) { + return null; } - // The workspace context must also ride along here: when projectRef - // targets a different physical member of the logical project, - // createDraftThreadState treats the remap as a project change and - // would otherwise wipe branch/worktree, undoing the write above. - setLogicalProjectDraftThreadId( - logicalProjectKey, - projectRef, + workspaceContext = { + branch: null, + worktreePath: null, + envMode: defaultEnvMode, + startFromOrigin: resolveNewDraftStartFromOrigin({ + envMode: defaultEnvMode, + newWorktreesStartFromOrigin: projectSettings.settings.newWorktreesStartFromOrigin, + }), + }; + } + if (workspaceContext) { + setDraftThreadContext(emptyStoredDraftThread.draftId, { + ...workspaceContext, + ...(!isDraftAlreadyOpen ? { runtimeMode: defaultRuntimeMode } : {}), + ...(carryInteractionMode ? { interactionMode: carryInteractionMode } : {}), + }); + } + const storedDraft = getComposerDraft(emptyStoredDraftThread.draftId); + const storedDraftHasExplicitModelPick = hasExplicitComposerModelSelection(storedDraft); + if (!storedDraftHasExplicitModelPick) { + applyStickyState(emptyStoredDraftThread.draftId); + const modelSelectionOverride = resolveModelSelectionOverride( emptyStoredDraftThread.draftId, - { - threadId: emptyStoredDraftThread.threadId, - ...workspaceContext, - ...(!isDraftAlreadyOpen ? { runtimeMode: defaultRuntimeMode } : {}), - ...(carryInteractionMode ? { interactionMode: carryInteractionMode } : {}), - }, ); - const opened = { - draftId: emptyStoredDraftThread.draftId, - threadId: emptyStoredDraftThread.threadId, - }; - const routeTargetAfterWrites = getCurrentRouteTarget(); - if ( - routeTargetAfterWrites?.kind === "draft" && - routeTargetAfterWrites.draftId === emptyStoredDraftThread.draftId - ) { - return opened; + if (modelSelectionOverride) { + setModelSelection(emptyStoredDraftThread.draftId, modelSelectionOverride, { + replaceOptions: true, + }); } - await router.navigate({ - to: "/draft/$draftId", - params: { draftId: emptyStoredDraftThread.draftId }, - replace: options?.replace ?? false, - }); - return opened; - })(); - } - - if ( - latestActiveDraftThread && - currentRouteTarget?.kind === "draft" && - latestActiveDraftThread.logicalProjectKey === logicalProjectKey && - latestActiveDraftThread.promotedTo == null && - !composerDraftHasUserContent(getComposerDraft(currentRouteTarget.draftId)) - ) { + } + // The workspace context must also ride along here: when projectRef + // targets a different physical member of the logical project, + // createDraftThreadState treats the remap as a project change and + // would otherwise wipe branch/worktree, undoing the write above. + setLogicalProjectDraftThreadId( + logicalProjectKey, + projectRef, + emptyStoredDraftThread.draftId, + { + threadId: emptyStoredDraftThread.threadId, + ...workspaceContext, + ...(!isDraftAlreadyOpen ? { runtimeMode: defaultRuntimeMode } : {}), + ...(carryInteractionMode ? { interactionMode: carryInteractionMode } : {}), + }, + ); + const opened = { + draftId: emptyStoredDraftThread.draftId, + threadId: emptyStoredDraftThread.threadId, + }; + const routeTargetAfterWrites = getCurrentRouteTarget(); if ( - hasBranchOption || - hasWorktreePathOption || - hasEnvModeOption || - hasStartFromOriginOption + routeTargetAfterWrites?.kind === "draft" && + routeTargetAfterWrites.draftId === emptyStoredDraftThread.draftId ) { - setDraftThreadContext(currentRouteTarget.draftId, pickExplicitWorkspaceOptions(options)); + return opened; } - setLogicalProjectDraftThreadId(logicalProjectKey, projectRef, currentRouteTarget.draftId, { - threadId: latestActiveDraftThread.threadId, - createdAt: latestActiveDraftThread.createdAt, - runtimeMode: latestActiveDraftThread.runtimeMode, - interactionMode: latestActiveDraftThread.interactionMode, - ...pickExplicitWorkspaceOptions(options), - }); - return Promise.resolve({ - draftId: currentRouteTarget.draftId, - threadId: latestActiveDraftThread.threadId, + await router.navigate({ + to: "/draft/$draftId", + params: { draftId: emptyStoredDraftThread.draftId }, + replace: options?.replace ?? false, }); + return opened; + })(); + } + + if ( + latestActiveDraftThread && + currentRouteTarget?.kind === "draft" && + latestActiveDraftThread.logicalProjectKey === logicalProjectKey && + latestActiveDraftThread.promotedTo == null && + !composerDraftHasUserContent(getComposerDraft(currentRouteTarget.draftId)) + ) { + if ( + hasBranchOption || + hasWorktreePathOption || + hasEnvModeOption || + hasStartFromOriginOption + ) { + setDraftThreadContext(currentRouteTarget.draftId, pickExplicitWorkspaceOptions(options)); } + setLogicalProjectDraftThreadId(logicalProjectKey, projectRef, currentRouteTarget.draftId, { + threadId: latestActiveDraftThread.threadId, + createdAt: latestActiveDraftThread.createdAt, + runtimeMode: latestActiveDraftThread.runtimeMode, + interactionMode: latestActiveDraftThread.interactionMode, + ...pickExplicitWorkspaceOptions(options), + }); + return Promise.resolve({ + draftId: currentRouteTarget.draftId, + threadId: latestActiveDraftThread.threadId, + }); + } - const draftId = newDraftId(); - const threadId = newThreadId(); - const createdAt = new Date().toISOString(); - return (async () => { - const initialEnvMode = options?.envMode ?? (await resolveDefaultEnvMode()); - if (abandonedSinceRequest()) { - return null; - } - // The await yields, so a concurrent invocation may have registered a - // draft for this logical project in the meantime. Registering ours - // too would evict that draft while its navigation is in flight — - // reuse the winner instead, like the synchronous path above does. - const racedDraft = getDraftSessionByLogicalProjectKey(logicalProjectKey); - if ( - racedDraft && - racedDraft.draftId !== storedDraftThread?.draftId && - readThreadShell(scopeThreadRef(racedDraft.environmentId, racedDraft.threadId)) === null - ) { - setLogicalProjectDraftThreadId(logicalProjectKey, projectRef, racedDraft.draftId, { - threadId: racedDraft.threadId, - createdAt: racedDraft.createdAt, - runtimeMode: racedDraft.runtimeMode, - interactionMode: racedDraft.interactionMode, - ...pickExplicitWorkspaceOptions(options), - }); - await router.navigate({ - to: "/draft/$draftId", - params: { draftId: racedDraft.draftId }, - replace: options?.replace ?? false, - }); - return { draftId: racedDraft.draftId, threadId: racedDraft.threadId }; - } - setLogicalProjectDraftThreadId(logicalProjectKey, projectRef, draftId, { - threadId, - createdAt, - branch: options?.branch ?? null, - worktreePath: options?.worktreePath ?? null, - envMode: initialEnvMode, - startFromOrigin: - options?.startFromOrigin ?? - resolveNewDraftStartFromOrigin({ - envMode: initialEnvMode, - newWorktreesStartFromOrigin: projectSettings.settings.newWorktreesStartFromOrigin, - }), - runtimeMode: defaultRuntimeMode, - ...(carryInteractionMode ? { interactionMode: carryInteractionMode } : {}), + const draftId = newDraftId(); + const threadId = newThreadId(); + const createdAt = new Date().toISOString(); + return (async () => { + const initialEnvMode = options?.envMode ?? (await resolveDefaultEnvMode()); + if (abandonedSinceRequest()) { + return null; + } + // The await yields, so a concurrent invocation may have registered a + // draft for this logical project in the meantime. Registering ours + // too would evict that draft while its navigation is in flight — + // reuse the winner instead, like the synchronous path above does. + const racedDraft = getDraftSessionByLogicalProjectKey(logicalProjectKey); + if ( + racedDraft && + racedDraft.draftId !== storedDraftThread?.draftId && + readThreadShell(scopeThreadRef(racedDraft.environmentId, racedDraft.threadId)) === null + ) { + setLogicalProjectDraftThreadId(logicalProjectKey, projectRef, racedDraft.draftId, { + threadId: racedDraft.threadId, + createdAt: racedDraft.createdAt, + runtimeMode: racedDraft.runtimeMode, + interactionMode: racedDraft.interactionMode, + ...pickExplicitWorkspaceOptions(options), }); - applyStickyState(draftId); - const modelSelectionOverride = resolveModelSelectionOverride(draftId); - if (modelSelectionOverride) { - setModelSelection(draftId, modelSelectionOverride, { replaceOptions: true }); - } await router.navigate({ to: "/draft/$draftId", - params: { draftId }, + params: { draftId: racedDraft.draftId }, replace: options?.replace ?? false, }); - return { draftId, threadId }; - })(); - }, - [getCurrentRouteTarget, projectGroupingSettings, router], - ); + return { draftId: racedDraft.draftId, threadId: racedDraft.threadId }; + } + setLogicalProjectDraftThreadId(logicalProjectKey, projectRef, draftId, { + threadId, + createdAt, + branch: options?.branch ?? null, + worktreePath: options?.worktreePath ?? null, + envMode: initialEnvMode, + startFromOrigin: + options?.startFromOrigin ?? + resolveNewDraftStartFromOrigin({ + envMode: initialEnvMode, + newWorktreesStartFromOrigin: projectSettings.settings.newWorktreesStartFromOrigin, + }), + runtimeMode: defaultRuntimeMode, + ...(carryInteractionMode ? { interactionMode: carryInteractionMode } : {}), + }); + applyStickyState(draftId); + const modelSelectionOverride = resolveModelSelectionOverride(draftId); + if (modelSelectionOverride) { + setModelSelection(draftId, modelSelectionOverride, { replaceOptions: true }); + } + await router.navigate({ + to: "/draft/$draftId", + params: { draftId }, + replace: options?.replace ?? false, + }); + return { draftId, threadId }; + })(); + }; +} + +function useOrderedProjects( + projects: ReturnType, + projectOrder: readonly string[], +) { + "use memo"; + return orderItemsByPreferredIds({ + items: projects, + preferredIds: projectOrder, + getId: getProjectOrderKey, + getPreferenceIds: (project) => [ + getProjectOrderKey(project), + legacyProjectCwdPreferenceKey(project.workspaceRoot), + ], + }); } export function useHandleNewThread() { @@ -410,17 +416,7 @@ export function useHandleNewThread() { : null, ); const projects = useProjects(); - const orderedProjects = useMemo(() => { - return orderItemsByPreferredIds({ - items: projects, - preferredIds: projectOrder, - getId: getProjectOrderKey, - getPreferenceIds: (project) => [ - getProjectOrderKey(project), - legacyProjectCwdPreferenceKey(project.workspaceRoot), - ], - }); - }, [projectOrder, projects]); + const orderedProjects = useOrderedProjects(projects, projectOrder); const handleNewThread = useNewThreadHandler(); return {