import { type EnvironmentId, type EditorId, type ProjectScript, type ResolvedKeybindingsConfig, type ThreadId,} from "@t3tools/contracts";import { scopeThreadRef } from "@t3tools/client-runtime/environment";import { memo } from "react";import GitActionsControl from "../GitActionsControl";import { type DraftId } from "~/composerDraftStore";import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";import ProjectScriptsControl, { type NewProjectScriptInput, type ProjectScriptActionResult,} from "../ProjectScriptsControl";import { OpenInPicker } from "./OpenInPicker";import { usePrimaryEnvironmentId } from "../../state/environments";import { useT3ProjectFileScripts } from "~/hooks/useT3ProjectFileScripts";import { ProjectFavicon } from "../ProjectFavicon";import { cn } from "~/lib/utils"; interface ChatHeaderProps { activeThreadEnvironmentId: EnvironmentId; activeThreadId: ThreadId; draftId?: DraftId; activeThreadTitle: string; activeProjectName: string | undefined; activeProjectCwd: string | null; openInCwd: string | null; activeProjectScripts: ReadonlyArray<ProjectScript> | undefined; preferredScriptId: string | null; keybindings: ResolvedKeybindingsConfig; availableEditors: ReadonlyArray<EditorId>; rightPanelOpen: boolean; gitCwd: string | null; onRunProjectScript: (script: ProjectScript) => void; onAddProjectScript: (input: NewProjectScriptInput) => Promise<ProjectScriptActionResult>; onUpdateProjectScript: ( scriptId: string, input: NewProjectScriptInput, ) => Promise<ProjectScriptActionResult>; onDeleteProjectScript: (scriptId: string) => Promise<ProjectScriptActionResult>;} export function shouldShowOpenInPicker(input: { readonly activeProjectName: string | undefined; readonly activeThreadEnvironmentId: EnvironmentId; readonly primaryEnvironmentId: EnvironmentId | null;}): boolean { return ( Boolean(input.activeProjectName) && input.primaryEnvironmentId !== null && input.activeThreadEnvironmentId === input.primaryEnvironmentId );} export const ChatHeader = memo(function ChatHeader({ activeThreadEnvironmentId, activeThreadId, draftId, activeThreadTitle, activeProjectName, activeProjectCwd, openInCwd, activeProjectScripts, preferredScriptId, keybindings, availableEditors, rightPanelOpen, gitCwd, onRunProjectScript, onAddProjectScript, onUpdateProjectScript, onDeleteProjectScript,}: ChatHeaderProps) { const primaryEnvironmentId = usePrimaryEnvironmentId(); const fileScripts = useT3ProjectFileScripts( activeThreadEnvironmentId, activeProjectScripts ? activeProjectCwd : null, ); const showOpenInPicker = shouldShowOpenInPicker({ activeProjectName, activeThreadEnvironmentId, primaryEnvironmentId, }); return ( <div className="@container/header-actions flex min-w-0 flex-1 items-center gap-2 sm:gap-3"> <div className="flex min-w-0 flex-1 items-center gap-2 overflow-hidden sm:gap-3"> {/* The project always leads the header: knowing which project a thread lives in is priority zero, and the thread title alone doesn't answer it. */} {activeProjectName ? ( <span className="inline-flex shrink-0 items-center gap-2"> <span className="inline-flex min-w-0 items-center gap-1.5"> <ProjectFavicon environmentId={activeThreadEnvironmentId} cwd={activeProjectCwd ?? ""} className="size-3.5" /> <span className="max-w-40 truncate text-sm font-medium text-muted-foreground"> {activeProjectName} </span> </span> <span aria-hidden className="text-muted-foreground/40"> / </span> </span> ) : null} <Tooltip> <TooltipTrigger render={ <h2 aria-label={activeThreadTitle} className="min-w-0 flex-1 truncate text-sm font-medium text-foreground" > {activeThreadTitle} </h2> } /> <TooltipPopup side="top">{activeThreadTitle}</TooltipPopup> </Tooltip> </div> <div data-chat-header-actions className={cn( "flex shrink-0 items-center justify-end gap-2 @3xl/header-actions:gap-3", rightPanelOpen ? "pr-0" : "pr-16", )} > {activeProjectScripts && ( <ProjectScriptsControl scripts={activeProjectScripts} fileScripts={fileScripts} keybindings={keybindings} preferredScriptId={preferredScriptId} onRunScript={onRunProjectScript} onAddScript={onAddProjectScript} onUpdateScript={onUpdateProjectScript} onDeleteScript={onDeleteProjectScript} /> )} {showOpenInPicker && ( <OpenInPicker environmentId={activeThreadEnvironmentId} keybindings={keybindings} availableEditors={availableEditors} openInCwd={openInCwd} /> )} {activeProjectName && ( <GitActionsControl gitCwd={gitCwd} activeThreadRef={scopeThreadRef(activeThreadEnvironmentId, activeThreadId)} {...(draftId ? { draftId } : {})} /> )} </div> </div> );});
import {
type EnvironmentId,
type EditorId,
type ProjectScript,
type ResolvedKeybindingsConfig,
type ThreadId,
} from "@t3tools/contracts";
import { scopeThreadRef } from "@t3tools/client-runtime/environment";
import { memo } from "react";
import GitActionsControl from "../GitActionsControl";
import { type DraftId } from "~/composerDraftStore";
import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
import ProjectScriptsControl, {
type NewProjectScriptInput,
type ProjectScriptActionResult,
} from "../ProjectScriptsControl";
import { OpenInPicker } from "./OpenInPicker";
import { usePrimaryEnvironmentId } from "../../state/environments";
import { useT3ProjectFileScripts } from "~/hooks/useT3ProjectFileScripts";
import { ProjectFavicon } from "../ProjectFavicon";
import { cn } from "~/lib/utils";
interface ChatHeaderProps {
activeThreadEnvironmentId: EnvironmentId;
activeThreadId: ThreadId;
draftId?: DraftId;
activeThreadTitle: string;
activeProjectName: string | undefined;
activeProjectCwd: string | null;
openInCwd: string | null;
activeProjectScripts: ReadonlyArray<ProjectScript> | undefined;
preferredScriptId: string | null;
keybindings: ResolvedKeybindingsConfig;
availableEditors: ReadonlyArray<EditorId>;
rightPanelOpen: boolean;
gitCwd: string | null;
onRunProjectScript: (script: ProjectScript) => void;
onAddProjectScript: (input: NewProjectScriptInput) => Promise<ProjectScriptActionResult>;
onUpdateProjectScript: (
scriptId: string,
input: NewProjectScriptInput,
) => Promise<ProjectScriptActionResult>;
onDeleteProjectScript: (scriptId: string) => Promise<ProjectScriptActionResult>;
}
export function shouldShowOpenInPicker(input: {
readonly activeProjectName: string | undefined;
readonly activeThreadEnvironmentId: EnvironmentId;
readonly primaryEnvironmentId: EnvironmentId | null;
}): boolean {
return (
Boolean(input.activeProjectName) &&
input.primaryEnvironmentId !== null &&
input.activeThreadEnvironmentId === input.primaryEnvironmentId
);
}
export const ChatHeader = memo(function ChatHeader({
activeThreadEnvironmentId,
activeThreadId,
draftId,
activeThreadTitle,
activeProjectName,
activeProjectCwd,
openInCwd,
activeProjectScripts,
preferredScriptId,
keybindings,
availableEditors,
rightPanelOpen,
gitCwd,
onRunProjectScript,
onAddProjectScript,
onUpdateProjectScript,
onDeleteProjectScript,
}: ChatHeaderProps) {
const primaryEnvironmentId = usePrimaryEnvironmentId();
const fileScripts = useT3ProjectFileScripts(
activeThreadEnvironmentId,
activeProjectScripts ? activeProjectCwd : null,
);
const showOpenInPicker = shouldShowOpenInPicker({
activeProjectName,
activeThreadEnvironmentId,
primaryEnvironmentId,
});
return (
<div className="@container/header-actions flex min-w-0 flex-1 items-center gap-2 sm:gap-3">
<div className="flex min-w-0 flex-1 items-center gap-2 overflow-hidden sm:gap-3">
{/* The project always leads the header: knowing which project a
thread lives in is priority zero, and the thread title alone
doesn't answer it. */}
{activeProjectName ? (
<span className="inline-flex shrink-0 items-center gap-2">
<span className="inline-flex min-w-0 items-center gap-1.5">
<ProjectFavicon
environmentId={activeThreadEnvironmentId}
cwd={activeProjectCwd ?? ""}
className="size-3.5"
/>
<span className="max-w-40 truncate text-sm font-medium text-muted-foreground">
{activeProjectName}
</span>
</span>
<span aria-hidden className="text-muted-foreground/40">
/
</span>
</span>
) : null}
<Tooltip>
<TooltipTrigger
render={
<h2
aria-label={activeThreadTitle}
className="min-w-0 flex-1 truncate text-sm font-medium text-foreground"
>
{activeThreadTitle}
</h2>
}
/>
<TooltipPopup side="top">{activeThreadTitle}</TooltipPopup>
</Tooltip>
</div>
<div
data-chat-header-actions
className={cn(
"flex shrink-0 items-center justify-end gap-2 @3xl/header-actions:gap-3",
rightPanelOpen ? "pr-0" : "pr-16",
)}
>
{activeProjectScripts && (
<ProjectScriptsControl
scripts={activeProjectScripts}
fileScripts={fileScripts}
keybindings={keybindings}
preferredScriptId={preferredScriptId}
onRunScript={onRunProjectScript}
onAddScript={onAddProjectScript}
onUpdateScript={onUpdateProjectScript}
onDeleteScript={onDeleteProjectScript}
/>
)}
{showOpenInPicker && (
<OpenInPicker
environmentId={activeThreadEnvironmentId}
keybindings={keybindings}
availableEditors={availableEditors}
openInCwd={openInCwd}
/>
)}
{activeProjectName && (
<GitActionsControl
gitCwd={gitCwd}
activeThreadRef={scopeThreadRef(activeThreadEnvironmentId, activeThreadId)}
{...(draftId ? { draftId } : {})}
/>
)}
</div>
</div>
);
});