import { scopeProjectRef } from "@t3tools/client-runtime/environment"; import type { EnvironmentId, ModelSelection, ProjectId, ScopedProjectRef, } from "@t3tools/contracts"; import type { ComposerThreadDraftState, DraftThreadEnvMode } from "../composerDraftStore"; type ComposerModelSelectionState = Pick< ComposerThreadDraftState, "activeProvider" | "modelSelectionByProvider" | "modelSelectionExplicit" >; interface ThreadContextLike { environmentId: EnvironmentId; projectId: ProjectId; } interface NewThreadHandler { ( projectRef: ScopedProjectRef, options?: { branch?: string | null; worktreePath?: string | null; envMode?: DraftThreadEnvMode; startFromOrigin?: boolean; }, // The opened draft's identity, which most callers have no use for. ): Promise; } export interface ChatThreadActionContext { readonly activeDraftThread: ThreadContextLike | null; readonly activeThread: ThreadContextLike | undefined; readonly defaultProjectRef: ScopedProjectRef | null; readonly handleNewThread: NewThreadHandler; } export function resolveNewDraftStartFromOrigin(input: { envMode: DraftThreadEnvMode; newWorktreesStartFromOrigin: boolean; }): boolean { return input.envMode === "worktree" && input.newWorktreesStartFromOrigin; } export function resolveNewThreadModelSelectionOverride(input: { readonly projectDefaultSelection: ModelSelection | null; readonly carrySelection: ModelSelection | null; readonly carrySourceDraftId: string | null; readonly destinationDraftId: string; }): ModelSelection | null { return ( input.projectDefaultSelection ?? (input.carrySourceDraftId === input.destinationDraftId ? null : input.carrySelection) ); } export function hasExplicitComposerModelSelection( draft: ComposerModelSelectionState | null | undefined, ): boolean { const activeProvider = draft?.activeProvider; return ( draft?.modelSelectionExplicit === true && activeProvider !== null && activeProvider !== undefined && draft.modelSelectionByProvider[activeProvider] !== undefined ); } export function resolveThreadActionProjectRef( context: ChatThreadActionContext, ): ScopedProjectRef | null { if (context.activeThread) { return scopeProjectRef(context.activeThread.environmentId, context.activeThread.projectId); } if (context.activeDraftThread) { return scopeProjectRef( context.activeDraftThread.environmentId, context.activeDraftThread.projectId, ); } return context.defaultProjectRef; } // New threads inherit only the *project* from the current context. Branch, // worktree, and env mode always come from the user's configured defaults — // carrying them over from the viewed thread meant "new thread" silently // reused checkouts and branches. Explicit affordances (branch toolbar's // "new thread in this worktree") pass those options to handleNewThread // directly instead. export async function startNewThreadFromContext( context: ChatThreadActionContext, ): Promise { const projectRef = resolveThreadActionProjectRef(context); if (!projectRef) { return false; } await context.handleNewThread(projectRef); return true; }