import { scopeProjectRef, scopedThreadKey, scopeThreadRef, } from "@t3tools/client-runtime/environment"; import type { VcsStatusResult } from "@t3tools/contracts"; import { CloudIcon, FolderGit2Icon, GitPullRequestIcon, TerminalIcon } from "lucide-react"; import { useMemo } from "react"; import { useEnvironment, usePrimaryEnvironmentId } from "../state/environments"; import { useProject } from "../state/entities"; import { useEnvironmentQuery } from "../state/query"; import { useThreadRunningTerminalIds } from "../state/terminalSessions"; import { vcsEnvironment } from "../state/vcs"; import { useUiStateStore } from "../uiStateStore"; import { resolveChangeRequestPresentation } from "../sourceControlPresentation"; import { resolveThreadStatusPill, type ThreadStatusPill } from "./Sidebar.logic"; import type { SidebarThreadSummary } from "../types"; import { formatWorktreePathForDisplay } from "../worktreeCleanup"; import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; export interface PrStatusIndicator { label: string; colorClass: string; tooltip: string; tooltipLead: string; tooltipTitle: string; url: string; } export interface TerminalStatusIndicator { label: "Terminal process running"; colorClass: string; pulse: boolean; } export type ThreadPr = VcsStatusResult["pr"]; export function settledPrHoverColorClass(state: NonNullable["state"]): string { switch (state) { case "open": return "group-hover/v2-row:text-emerald-600 dark:group-hover/v2-row:text-emerald-300/90"; case "merged": return "group-hover/v2-row:text-violet-600 dark:group-hover/v2-row:text-violet-300/90"; case "closed": return "group-hover/v2-row:text-red-600 dark:group-hover/v2-row:text-red-300/90"; } } export function prStatusIndicator( pr: ThreadPr, provider: VcsStatusResult["sourceControlProvider"] | null | undefined, ): PrStatusIndicator | null { function formatPrState(state: NonNullable["state"]): string { return state.charAt(0).toUpperCase() + state.slice(1); } function formatPrStatusLead(pr: NonNullable, changeRequestShortName: string): string { return `${changeRequestShortName} #${pr.number} - ${formatPrState(pr.state)}`; } if (!pr) return null; const presentation = resolveChangeRequestPresentation(provider); const tooltipLead = formatPrStatusLead(pr, presentation.shortName); const tooltip = `${tooltipLead}: ${pr.title}`; if (pr.state === "open") { return { label: `${presentation.shortName} open`, colorClass: "text-emerald-600 dark:text-emerald-300/90", tooltip, tooltipLead, tooltipTitle: pr.title, url: pr.url, }; } if (pr.state === "closed") { return { label: `${presentation.shortName} closed`, colorClass: "text-red-600 dark:text-red-300/90", tooltip, tooltipLead, tooltipTitle: pr.title, url: pr.url, }; } if (pr.state === "merged") { return { label: `${presentation.shortName} merged`, colorClass: "text-violet-600 dark:text-violet-300/90", tooltip, tooltipLead, tooltipTitle: pr.title, url: pr.url, }; } return null; } export function ChangeRequestStatusIcon({ className }: { className?: string }) { return ; } export function PrStatusTooltipContent({ status }: { status: PrStatusIndicator }) { return ( {status.tooltipLead} ); } export function resolveThreadPr(input: { threadBranch: string | null; gitStatus: VcsStatusResult | null; hasDedicatedWorktree: boolean; }): ThreadPr | null { const { threadBranch, gitStatus, hasDedicatedWorktree } = input; if (gitStatus === null) { return null; } if (hasDedicatedWorktree) { return gitStatus.pr ?? null; } if (threadBranch === null || gitStatus.refName !== threadBranch) { return null; } return gitStatus.pr ?? null; } export function terminalStatusFromRunningIds( runningTerminalIds: ReadonlyArray, ): TerminalStatusIndicator | null { if (runningTerminalIds.length === 0) { return null; } return { label: "Terminal process running", colorClass: "text-teal-600 dark:text-teal-300/90", pulse: true, }; } export function ThreadWorktreeIndicator({ thread, }: { thread: Pick; }) { const worktreePath = thread.worktreePath?.trim(); if (!worktreePath) { return null; } const displayPath = formatWorktreePathForDisplay(worktreePath); const tooltip = thread.branch ? `Worktree: ${displayPath} (${thread.branch})` : `Worktree: ${displayPath}`; return ( } > {tooltip} ); } export function ThreadStatusLabel({ status, compact = false, }: { status: ThreadStatusPill; compact?: boolean; }) { if (compact) { return ( } > {status.label} ); } return ( } > {status.label} {status.label} ); } /** * Non-interactive leading status icons for a thread row in compact contexts * like the command palette. Shows the change request state icon (if present) and the * thread status dot, matching the sidebar's leading indicators. */ export function ThreadRowLeadingStatus({ thread }: { thread: SidebarThreadSummary }) { const threadRef = scopeThreadRef(thread.environmentId, thread.id); const lastVisitedAt = useUiStateStore( (state) => state.threadLastVisitedAtById[scopedThreadKey(threadRef)], ); const threadProject = useProject( useMemo( () => scopeProjectRef(thread.environmentId, thread.projectId), [thread.environmentId, thread.projectId], ), ); const threadProjectCwd = threadProject?.workspaceRoot ?? null; const gitCwd = thread.worktreePath ?? threadProjectCwd; const gitStatus = useEnvironmentQuery( (thread.branch != null || thread.worktreePath !== null) && gitCwd !== null ? vcsEnvironment.status({ environmentId: thread.environmentId, input: { cwd: gitCwd }, }) : null, ); const pr = resolveThreadPr({ threadBranch: thread.branch, gitStatus: gitStatus.data, hasDedicatedWorktree: thread.worktreePath !== null, }); const prStatus = prStatusIndicator(pr, gitStatus.data?.sourceControlProvider); const threadStatus = resolveThreadStatusPill({ thread: { ...thread, lastVisitedAt, }, }); if (!prStatus && !threadStatus) { return null; } return ( {prStatus ? ( } > ) : null} {threadStatus ? : null} ); } /** * Non-interactive trailing status icons for a thread row in compact contexts * like the command palette. Shows a terminal-running indicator and a remote * environment indicator, matching the sidebar's trailing indicators. */ export function ThreadRowTrailingStatus({ thread }: { thread: SidebarThreadSummary }) { const runningTerminalIds = useThreadRunningTerminalIds({ environmentId: thread.environmentId, threadId: thread.id, }); const environment = useEnvironment(thread.environmentId); const primaryEnvironmentId = usePrimaryEnvironmentId(); const isRemoteThread = primaryEnvironmentId !== null && thread.environmentId !== primaryEnvironmentId; const remoteEnvLabel = environment?.label ?? null; const threadEnvironmentLabel = isRemoteThread ? (remoteEnvLabel ?? "Remote") : null; const terminalStatus = terminalStatusFromRunningIds(runningTerminalIds); if (!terminalStatus && !isRemoteThread) { return null; } return ( {terminalStatus ? ( } > {terminalStatus.label} ) : null} {isRemoteThread ? ( } > {threadEnvironmentLabel} ) : null} ); }