import type { ThreadId } from "@t3tools/contracts";import { FolderIcon, GitForkIcon } from "lucide-react";import { useCallback } from "react"; import { newCommandId } from "../lib/utils";import { readNativeApi } from "../nativeApi";import { useComposerDraftStore } from "../composerDraftStore";import { useStore } from "../store";import { EnvMode, resolveDraftEnvModeAfterBranchChange, resolveEffectiveEnvMode,} from "./BranchToolbar.logic";import { BranchToolbarBranchSelector } from "./BranchToolbarBranchSelector";import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "./ui/select"; const envModeItems = [ { value: "local", label: "Local" }, { value: "worktree", label: "New worktree" },] as const; interface BranchToolbarProps { threadId: ThreadId; onEnvModeChange: (mode: EnvMode) => void; envLocked: boolean; onCheckoutPullRequestRequest?: (reference: string) => void; onComposerFocusRequest?: () => void;} export default function BranchToolbar({ threadId, onEnvModeChange, envLocked, onCheckoutPullRequestRequest, onComposerFocusRequest,}: BranchToolbarProps) { const threads = useStore((store) => store.threads); const projects = useStore((store) => store.projects); const setThreadBranchAction = useStore((store) => store.setThreadBranch); const draftThread = useComposerDraftStore((store) => store.getDraftThread(threadId)); const setDraftThreadContext = useComposerDraftStore((store) => store.setDraftThreadContext); const serverThread = threads.find((thread) => thread.id === threadId); const activeProjectId = serverThread?.projectId ?? draftThread?.projectId ?? null; const activeProject = projects.find((project) => project.id === activeProjectId); const activeThreadId = serverThread?.id ?? (draftThread ? threadId : undefined); const activeThreadBranch = serverThread?.branch ?? draftThread?.branch ?? null; const activeWorktreePath = serverThread?.worktreePath ?? draftThread?.worktreePath ?? null; const branchCwd = activeWorktreePath ?? activeProject?.cwd ?? null; const hasServerThread = serverThread !== undefined; const effectiveEnvMode = resolveEffectiveEnvMode({ activeWorktreePath, hasServerThread, draftThreadEnvMode: draftThread?.envMode, }); const setThreadBranch = useCallback( (branch: string | null, worktreePath: string | null) => { if (!activeThreadId) return; const api = readNativeApi(); // If the effective cwd is about to change, stop the running session so the // next message creates a new one with the correct cwd. if (serverThread?.session && worktreePath !== activeWorktreePath && api) { void api.orchestration .dispatchCommand({ type: "thread.session.stop", commandId: newCommandId(), threadId: activeThreadId, createdAt: new Date().toISOString(), }) .catch(() => undefined); } if (api && hasServerThread) { void api.orchestration.dispatchCommand({ type: "thread.meta.update", commandId: newCommandId(), threadId: activeThreadId, branch, worktreePath, }); } if (hasServerThread) { setThreadBranchAction(activeThreadId, branch, worktreePath); return; } const nextDraftEnvMode = resolveDraftEnvModeAfterBranchChange({ nextWorktreePath: worktreePath, currentWorktreePath: activeWorktreePath, effectiveEnvMode, }); setDraftThreadContext(threadId, { branch, worktreePath, envMode: nextDraftEnvMode, }); }, [ activeThreadId, serverThread?.session, activeWorktreePath, hasServerThread, setThreadBranchAction, setDraftThreadContext, threadId, effectiveEnvMode, ], ); if (!activeThreadId || !activeProject) return null; return ( <div className="mx-auto flex w-full max-w-3xl items-center justify-between px-5 pb-3 pt-1"> {envLocked || activeWorktreePath ? ( <span className="inline-flex items-center gap-1 border border-transparent px-[calc(--spacing(3)-1px)] text-sm font-medium text-muted-foreground/70 sm:text-xs"> {activeWorktreePath ? ( <> <GitForkIcon className="size-3" /> Worktree </> ) : ( <> <FolderIcon className="size-3" /> Local </> )} </span> ) : ( <Select value={effectiveEnvMode} onValueChange={(value) => onEnvModeChange(value as EnvMode)} items={envModeItems} > <SelectTrigger variant="ghost" size="xs" className="font-medium"> {effectiveEnvMode === "worktree" ? ( <GitForkIcon className="size-3" /> ) : ( <FolderIcon className="size-3" /> )} <SelectValue /> </SelectTrigger> <SelectPopup> <SelectItem value="local"> <span className="inline-flex items-center gap-1.5"> <FolderIcon className="size-3" /> Local </span> </SelectItem> <SelectItem value="worktree"> <span className="inline-flex items-center gap-1.5"> <GitForkIcon className="size-3" /> New worktree </span> </SelectItem> </SelectPopup> </Select> )} <BranchToolbarBranchSelector activeProjectCwd={activeProject.cwd} activeThreadBranch={activeThreadBranch} activeWorktreePath={activeWorktreePath} branchCwd={branchCwd} effectiveEnvMode={effectiveEnvMode} envLocked={envLocked} onSetThreadBranch={setThreadBranch} {...(onCheckoutPullRequestRequest ? { onCheckoutPullRequestRequest } : {})} {...(onComposerFocusRequest ? { onComposerFocusRequest } : {})} /> </div> );}
import type { ThreadId } from "@t3tools/contracts";
import { FolderIcon, GitForkIcon } from "lucide-react";
import { useCallback } from "react";
import { newCommandId } from "../lib/utils";
import { readNativeApi } from "../nativeApi";
import { useComposerDraftStore } from "../composerDraftStore";
import { useStore } from "../store";
import {
EnvMode,
resolveDraftEnvModeAfterBranchChange,
resolveEffectiveEnvMode,
} from "./BranchToolbar.logic";
import { BranchToolbarBranchSelector } from "./BranchToolbarBranchSelector";
import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "./ui/select";
const envModeItems = [
{ value: "local", label: "Local" },
{ value: "worktree", label: "New worktree" },
] as const;
interface BranchToolbarProps {
threadId: ThreadId;
onEnvModeChange: (mode: EnvMode) => void;
envLocked: boolean;
onCheckoutPullRequestRequest?: (reference: string) => void;
onComposerFocusRequest?: () => void;
}
export default function BranchToolbar({
threadId,
onEnvModeChange,
envLocked,
onCheckoutPullRequestRequest,
onComposerFocusRequest,
}: BranchToolbarProps) {
const threads = useStore((store) => store.threads);
const projects = useStore((store) => store.projects);
const setThreadBranchAction = useStore((store) => store.setThreadBranch);
const draftThread = useComposerDraftStore((store) => store.getDraftThread(threadId));
const setDraftThreadContext = useComposerDraftStore((store) => store.setDraftThreadContext);
const serverThread = threads.find((thread) => thread.id === threadId);
const activeProjectId = serverThread?.projectId ?? draftThread?.projectId ?? null;
const activeProject = projects.find((project) => project.id === activeProjectId);
const activeThreadId = serverThread?.id ?? (draftThread ? threadId : undefined);
const activeThreadBranch = serverThread?.branch ?? draftThread?.branch ?? null;
const activeWorktreePath = serverThread?.worktreePath ?? draftThread?.worktreePath ?? null;
const branchCwd = activeWorktreePath ?? activeProject?.cwd ?? null;
const hasServerThread = serverThread !== undefined;
const effectiveEnvMode = resolveEffectiveEnvMode({
activeWorktreePath,
hasServerThread,
draftThreadEnvMode: draftThread?.envMode,
});
const setThreadBranch = useCallback(
(branch: string | null, worktreePath: string | null) => {
if (!activeThreadId) return;
const api = readNativeApi();
// If the effective cwd is about to change, stop the running session so the
// next message creates a new one with the correct cwd.
if (serverThread?.session && worktreePath !== activeWorktreePath && api) {
void api.orchestration
.dispatchCommand({
type: "thread.session.stop",
commandId: newCommandId(),
threadId: activeThreadId,
createdAt: new Date().toISOString(),
})
.catch(() => undefined);
}
if (api && hasServerThread) {
void api.orchestration.dispatchCommand({
type: "thread.meta.update",
commandId: newCommandId(),
threadId: activeThreadId,
branch,
worktreePath,
});
}
if (hasServerThread) {
setThreadBranchAction(activeThreadId, branch, worktreePath);
return;
}
const nextDraftEnvMode = resolveDraftEnvModeAfterBranchChange({
nextWorktreePath: worktreePath,
currentWorktreePath: activeWorktreePath,
effectiveEnvMode,
});
setDraftThreadContext(threadId, {
branch,
worktreePath,
envMode: nextDraftEnvMode,
});
},
[
activeThreadId,
serverThread?.session,
activeWorktreePath,
hasServerThread,
setThreadBranchAction,
setDraftThreadContext,
threadId,
effectiveEnvMode,
],
);
if (!activeThreadId || !activeProject) return null;
return (
<div className="mx-auto flex w-full max-w-3xl items-center justify-between px-5 pb-3 pt-1">
{envLocked || activeWorktreePath ? (
<span className="inline-flex items-center gap-1 border border-transparent px-[calc(--spacing(3)-1px)] text-sm font-medium text-muted-foreground/70 sm:text-xs">
{activeWorktreePath ? (
<>
<GitForkIcon className="size-3" />
Worktree
</>
) : (
<>
<FolderIcon className="size-3" />
Local
</>
)}
</span>
) : (
<Select
value={effectiveEnvMode}
onValueChange={(value) => onEnvModeChange(value as EnvMode)}
items={envModeItems}
>
<SelectTrigger variant="ghost" size="xs" className="font-medium">
{effectiveEnvMode === "worktree" ? (
<GitForkIcon className="size-3" />
) : (
<FolderIcon className="size-3" />
)}
<SelectValue />
</SelectTrigger>
<SelectPopup>
<SelectItem value="local">
<span className="inline-flex items-center gap-1.5">
<FolderIcon className="size-3" />
Local
</span>
</SelectItem>
<SelectItem value="worktree">
<span className="inline-flex items-center gap-1.5">
<GitForkIcon className="size-3" />
New worktree
</span>
</SelectItem>
</SelectPopup>
</Select>
)}
<BranchToolbarBranchSelector
activeProjectCwd={activeProject.cwd}
activeThreadBranch={activeThreadBranch}
activeWorktreePath={activeWorktreePath}
branchCwd={branchCwd}
effectiveEnvMode={effectiveEnvMode}
envLocked={envLocked}
onSetThreadBranch={setThreadBranch}
{...(onCheckoutPullRequestRequest ? { onCheckoutPullRequestRequest } : {})}
{...(onComposerFocusRequest ? { onComposerFocusRequest } : {})}
/>
</div>
);
}