import { scopeProjectRef } from "@t3tools/client-runtime/environment";import type { EnvironmentId, ProjectId, ScopedProjectRef } from "@t3tools/contracts";import type { DraftThreadEnvMode } from "../composerDraftStore";interface ThreadContextLike { environmentId: EnvironmentId; projectId: ProjectId;}interface NewThreadHandler { ( projectRef: ScopedProjectRef, options?: { branch?: string | null; worktreePath?: string | null; envMode?: DraftThreadEnvMode; startFromOrigin?: boolean; }, ): Promise<void>;}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 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;}// `defaultProjectRef` resolves to workspaces only by design, so a user whose// sole project is an agent has none — and "new thread" would silently no-op.// The fallback applies only when there is exactly one destination: with// several, an absent default means "no obvious target" and the user should// pick rather than have one chosen for them.export function resolveSoleNewThreadProjectRef( targets: ReadonlyArray<{ environmentId: EnvironmentId; id: ProjectId }>,): ScopedProjectRef | null { const sole = targets.length === 1 ? targets[0] : undefined; return sole ? scopeProjectRef(sole.environmentId, sole.id) : null;}// 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<boolean> { const projectRef = resolveThreadActionProjectRef(context); if (!projectRef) { return false; } await context.handleNewThread(projectRef); return true;}
import { scopeProjectRef } from "@t3tools/client-runtime/environment";
import type { EnvironmentId, ProjectId, ScopedProjectRef } from "@t3tools/contracts";
import type { DraftThreadEnvMode } from "../composerDraftStore";
interface ThreadContextLike {
environmentId: EnvironmentId;
projectId: ProjectId;
}
interface NewThreadHandler {
(
projectRef: ScopedProjectRef,
options?: {
branch?: string | null;
worktreePath?: string | null;
envMode?: DraftThreadEnvMode;
startFromOrigin?: boolean;
},
): Promise<void>;
}
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 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;
}
// `defaultProjectRef` resolves to workspaces only by design, so a user whose
// sole project is an agent has none — and "new thread" would silently no-op.
// The fallback applies only when there is exactly one destination: with
// several, an absent default means "no obvious target" and the user should
// pick rather than have one chosen for them.
export function resolveSoleNewThreadProjectRef(
targets: ReadonlyArray<{ environmentId: EnvironmentId; id: ProjectId }>,
): ScopedProjectRef | null {
const sole = targets.length === 1 ? targets[0] : undefined;
return sole ? scopeProjectRef(sole.environmentId, sole.id) : null;
}
// 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<boolean> {
const projectRef = resolveThreadActionProjectRef(context);
if (!projectRef) {
return false;
}
await context.handleNewThread(projectRef);
return true;
}