1import { scopeProjectRef } from "@t3tools/client-runtime/environment";
2import type {
3 EnvironmentId,
4 ModelSelection,
5 ProjectId,
6 ScopedProjectRef,
7} from "@t3tools/contracts";
8import type { ComposerThreadDraftState, DraftThreadEnvMode } from "../composerDraftStore";
9
10type ComposerModelSelectionState = Pick<
11 ComposerThreadDraftState,
12 "activeProvider" | "modelSelectionByProvider" | "modelSelectionExplicit"
13>;
14
15interface ThreadContextLike {
16 environmentId: EnvironmentId;
17 projectId: ProjectId;
18}
19
20interface NewThreadHandler {
21 (
22 projectRef: ScopedProjectRef,
23 options?: {
24 branch?: string | null;
25 worktreePath?: string | null;
26 envMode?: DraftThreadEnvMode;
27 startFromOrigin?: boolean;
28 },
29 ): Promise<unknown>;
30}
31
32export interface ChatThreadActionContext {
33 readonly activeDraftThread: ThreadContextLike | null;
34 readonly activeThread: ThreadContextLike | undefined;
35 readonly defaultProjectRef: ScopedProjectRef | null;
36 readonly handleNewThread: NewThreadHandler;
37}
38
39export function resolveNewDraftStartFromOrigin(input: {
40 envMode: DraftThreadEnvMode;
41 newWorktreesStartFromOrigin: boolean;
42}): boolean {
43 return input.envMode === "worktree" && input.newWorktreesStartFromOrigin;
44}
45
46export function resolveNewThreadModelSelectionOverride(input: {
47 readonly projectDefaultSelection: ModelSelection | null;
48 readonly carrySelection: ModelSelection | null;
49 readonly carrySourceDraftId: string | null;
50 readonly destinationDraftId: string;
51}): ModelSelection | null {
52 return (
53 input.projectDefaultSelection ??
54 (input.carrySourceDraftId === input.destinationDraftId ? null : input.carrySelection)
55 );
56}
57
58export function hasExplicitComposerModelSelection(
59 draft: ComposerModelSelectionState | null | undefined,
60): boolean {
61 const activeProvider = draft?.activeProvider;
62 return (
63 draft?.modelSelectionExplicit === true &&
64 activeProvider !== null &&
65 activeProvider !== undefined &&
66 draft.modelSelectionByProvider[activeProvider] !== undefined
67 );
68}
69
70export function resolveThreadActionProjectRef(
71 context: ChatThreadActionContext,
72): ScopedProjectRef | null {
73 if (context.activeThread) {
74 return scopeProjectRef(context.activeThread.environmentId, context.activeThread.projectId);
75 }
76 if (context.activeDraftThread) {
77 return scopeProjectRef(
78 context.activeDraftThread.environmentId,
79 context.activeDraftThread.projectId,
80 );
81 }
82 return context.defaultProjectRef;
83}
84
85export async function startNewThreadFromContext(
86 context: ChatThreadActionContext,
87): Promise<boolean> {
88 const projectRef = resolveThreadActionProjectRef(context);
89 if (!projectRef) {
90 return false;
91 }
92
93 await context.handleNewThread(projectRef);
94 return true;
95}