import { scopeProjectRef, scopedThreadKey, scopeThreadRef } from "@t3tools/client-runtime";import type { VcsStatusResult } from "@t3tools/contracts";import { CloudIcon, GitPullRequestIcon, TerminalIcon } from "lucide-react";import { useMemo } from "react";import { usePrimaryEnvironmentId } from "../environments/primary";import { useSavedEnvironmentRegistryStore, useSavedEnvironmentRuntimeStore,} from "../environments/runtime";import { useVcsStatus } from "../lib/vcsStatusState";import { type AppState, selectProjectByRef, useStore } from "../store";import { useThreadRunningTerminalIds } from "../terminalSessionState";import { useUiStateStore } from "../uiStateStore";import { resolveChangeRequestPresentation } from "../sourceControlPresentation";import { resolveThreadStatusPill, type ThreadStatusPill } from "./Sidebar.logic";import type { SidebarThreadSummary } from "../types";import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; export interface PrStatusIndicator { label: string; colorClass: string; tooltip: string; url: string;} export interface TerminalStatusIndicator { label: "Terminal process running"; colorClass: string; pulse: boolean;} export type ThreadPr = VcsStatusResult["pr"]; export function prStatusIndicator( pr: ThreadPr, provider: VcsStatusResult["sourceControlProvider"] | null | undefined,): PrStatusIndicator | null { if (!pr) return null; const presentation = resolveChangeRequestPresentation(provider); if (pr.state === "open") { return { label: `${presentation.shortName} open`, colorClass: "text-emerald-600 dark:text-emerald-300/90", tooltip: `#${pr.number} ${presentation.shortName} open: ${pr.title}`, url: pr.url, }; } if (pr.state === "closed") { return { label: `${presentation.shortName} closed`, colorClass: "text-zinc-500 dark:text-zinc-400/80", tooltip: `#${pr.number} ${presentation.shortName} closed: ${pr.title}`, url: pr.url, }; } if (pr.state === "merged") { return { label: `${presentation.shortName} merged`, colorClass: "text-violet-600 dark:text-violet-300/90", tooltip: `#${pr.number} ${presentation.shortName} merged: ${pr.title}`, url: pr.url, }; } return null;} export function ChangeRequestStatusIcon({ className }: { className?: string }) { return <GitPullRequestIcon className={className} />;} export function resolveThreadPr( threadBranch: string | null, gitStatus: VcsStatusResult | null,): ThreadPr | null { if (threadBranch === null || gitStatus === null || gitStatus.refName !== threadBranch) { return null; } return gitStatus.pr ?? null;} export function terminalStatusFromRunningIds( runningTerminalIds: ReadonlyArray<string>,): 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 ThreadStatusLabel({ status, compact = false,}: { status: ThreadStatusPill; compact?: boolean;}) { if (compact) { return ( <span title={status.label} className={`inline-flex size-3.5 shrink-0 items-center justify-center ${status.colorClass}`} > <span className={`size-[9px] rounded-full ${status.dotClass} ${ status.pulse ? "animate-pulse" : "" }`} /> <span className="sr-only">{status.label}</span> </span> ); } return ( <span title={status.label} className={`inline-flex items-center gap-1 text-[10px] ${status.colorClass}`} > <span className={`h-1.5 w-1.5 rounded-full ${status.dotClass} ${ status.pulse ? "animate-pulse" : "" }`} /> <span className="hidden md:inline">{status.label}</span> </span> );} /** * 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 threadProjectCwd = useStore( useMemo( () => (state: AppState) => selectProjectByRef(state, scopeProjectRef(thread.environmentId, thread.projectId))?.cwd ?? null, [thread.environmentId, thread.projectId], ), ); const gitCwd = thread.worktreePath ?? threadProjectCwd; const gitStatus = useVcsStatus({ environmentId: thread.environmentId, cwd: thread.branch != null ? gitCwd : null, }); const pr = resolveThreadPr(thread.branch, gitStatus.data); const prStatus = prStatusIndicator(pr, gitStatus.data?.sourceControlProvider); const threadStatus = resolveThreadStatusPill({ thread: { ...thread, lastVisitedAt, }, }); if (!prStatus && !threadStatus) { return null; } return ( <span className="inline-flex shrink-0 items-center gap-1.5"> {prStatus ? ( <Tooltip> <TooltipTrigger render={ <span aria-label={prStatus.tooltip} className={`inline-flex items-center justify-center ${prStatus.colorClass}`} /> } > <ChangeRequestStatusIcon className="size-3" /> </TooltipTrigger> <TooltipPopup side="top">{prStatus.tooltip}</TooltipPopup> </Tooltip> ) : null} {threadStatus ? <ThreadStatusLabel status={threadStatus} /> : null} </span> );} /** * 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 primaryEnvironmentId = usePrimaryEnvironmentId(); const isRemoteThread = primaryEnvironmentId !== null && thread.environmentId !== primaryEnvironmentId; const remoteEnvLabel = useSavedEnvironmentRuntimeStore( (state) => state.byId[thread.environmentId]?.descriptor?.label ?? null, ); const remoteEnvSavedLabel = useSavedEnvironmentRegistryStore( (state) => state.byId[thread.environmentId]?.label ?? null, ); const threadEnvironmentLabel = isRemoteThread ? (remoteEnvLabel ?? remoteEnvSavedLabel ?? "Remote") : null; const terminalStatus = terminalStatusFromRunningIds(runningTerminalIds); if (!terminalStatus && !isRemoteThread) { return null; } return ( <span className="inline-flex shrink-0 items-center gap-1.5"> {terminalStatus ? ( <span role="img" aria-label={terminalStatus.label} title={terminalStatus.label} className={`inline-flex items-center justify-center ${terminalStatus.colorClass}`} > <TerminalIcon className={`size-3 ${terminalStatus.pulse ? "animate-pulse" : ""}`} /> </span> ) : null} {isRemoteThread ? ( <Tooltip> <TooltipTrigger render={ <span aria-label={threadEnvironmentLabel ?? "Remote"} className="inline-flex items-center justify-center" /> } > <CloudIcon className="size-3 text-muted-foreground/60" /> </TooltipTrigger> <TooltipPopup side="top">{threadEnvironmentLabel}</TooltipPopup> </Tooltip> ) : null} </span> );}
import { scopeProjectRef, scopedThreadKey, scopeThreadRef } from "@t3tools/client-runtime";
import type { VcsStatusResult } from "@t3tools/contracts";
import { CloudIcon, GitPullRequestIcon, TerminalIcon } from "lucide-react";
import { useMemo } from "react";
import { usePrimaryEnvironmentId } from "../environments/primary";
import {
useSavedEnvironmentRegistryStore,
useSavedEnvironmentRuntimeStore,
} from "../environments/runtime";
import { useVcsStatus } from "../lib/vcsStatusState";
import { type AppState, selectProjectByRef, useStore } from "../store";
import { useThreadRunningTerminalIds } from "../terminalSessionState";
import { useUiStateStore } from "../uiStateStore";
import { resolveChangeRequestPresentation } from "../sourceControlPresentation";
import { resolveThreadStatusPill, type ThreadStatusPill } from "./Sidebar.logic";
import type { SidebarThreadSummary } from "../types";
import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";
export interface PrStatusIndicator {
label: string;
colorClass: string;
tooltip: string;
url: string;
}
export interface TerminalStatusIndicator {
label: "Terminal process running";
colorClass: string;
pulse: boolean;
}
export type ThreadPr = VcsStatusResult["pr"];
export function prStatusIndicator(
pr: ThreadPr,
provider: VcsStatusResult["sourceControlProvider"] | null | undefined,
): PrStatusIndicator | null {
if (!pr) return null;
const presentation = resolveChangeRequestPresentation(provider);
if (pr.state === "open") {
return {
label: `${presentation.shortName} open`,
colorClass: "text-emerald-600 dark:text-emerald-300/90",
tooltip: `#${pr.number} ${presentation.shortName} open: ${pr.title}`,
url: pr.url,
};
}
if (pr.state === "closed") {
return {
label: `${presentation.shortName} closed`,
colorClass: "text-zinc-500 dark:text-zinc-400/80",
tooltip: `#${pr.number} ${presentation.shortName} closed: ${pr.title}`,
url: pr.url,
};
}
if (pr.state === "merged") {
return {
label: `${presentation.shortName} merged`,
colorClass: "text-violet-600 dark:text-violet-300/90",
tooltip: `#${pr.number} ${presentation.shortName} merged: ${pr.title}`,
url: pr.url,
};
}
return null;
}
export function ChangeRequestStatusIcon({ className }: { className?: string }) {
return <GitPullRequestIcon className={className} />;
}
export function resolveThreadPr(
threadBranch: string | null,
gitStatus: VcsStatusResult | null,
): ThreadPr | null {
if (threadBranch === null || gitStatus === null || gitStatus.refName !== threadBranch) {
return null;
}
return gitStatus.pr ?? null;
}
export function terminalStatusFromRunningIds(
runningTerminalIds: ReadonlyArray<string>,
): 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 ThreadStatusLabel({
status,
compact = false,
}: {
status: ThreadStatusPill;
compact?: boolean;
}) {
if (compact) {
return (
<span
title={status.label}
className={`inline-flex size-3.5 shrink-0 items-center justify-center ${status.colorClass}`}
>
<span
className={`size-[9px] rounded-full ${status.dotClass} ${
status.pulse ? "animate-pulse" : ""
}`}
/>
<span className="sr-only">{status.label}</span>
</span>
);
}
return (
<span
title={status.label}
className={`inline-flex items-center gap-1 text-[10px] ${status.colorClass}`}
>
<span
className={`h-1.5 w-1.5 rounded-full ${status.dotClass} ${
status.pulse ? "animate-pulse" : ""
}`}
/>
<span className="hidden md:inline">{status.label}</span>
</span>
);
}
/**
* 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 threadProjectCwd = useStore(
useMemo(
() => (state: AppState) =>
selectProjectByRef(state, scopeProjectRef(thread.environmentId, thread.projectId))?.cwd ??
null,
[thread.environmentId, thread.projectId],
),
);
const gitCwd = thread.worktreePath ?? threadProjectCwd;
const gitStatus = useVcsStatus({
environmentId: thread.environmentId,
cwd: thread.branch != null ? gitCwd : null,
});
const pr = resolveThreadPr(thread.branch, gitStatus.data);
const prStatus = prStatusIndicator(pr, gitStatus.data?.sourceControlProvider);
const threadStatus = resolveThreadStatusPill({
thread: {
...thread,
lastVisitedAt,
},
});
if (!prStatus && !threadStatus) {
return null;
}
return (
<span className="inline-flex shrink-0 items-center gap-1.5">
{prStatus ? (
<Tooltip>
<TooltipTrigger
render={
<span
aria-label={prStatus.tooltip}
className={`inline-flex items-center justify-center ${prStatus.colorClass}`}
/>
}
>
<ChangeRequestStatusIcon className="size-3" />
</TooltipTrigger>
<TooltipPopup side="top">{prStatus.tooltip}</TooltipPopup>
</Tooltip>
) : null}
{threadStatus ? <ThreadStatusLabel status={threadStatus} /> : null}
</span>
);
}
/**
* 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 primaryEnvironmentId = usePrimaryEnvironmentId();
const isRemoteThread =
primaryEnvironmentId !== null && thread.environmentId !== primaryEnvironmentId;
const remoteEnvLabel = useSavedEnvironmentRuntimeStore(
(state) => state.byId[thread.environmentId]?.descriptor?.label ?? null,
);
const remoteEnvSavedLabel = useSavedEnvironmentRegistryStore(
(state) => state.byId[thread.environmentId]?.label ?? null,
);
const threadEnvironmentLabel = isRemoteThread
? (remoteEnvLabel ?? remoteEnvSavedLabel ?? "Remote")
: null;
const terminalStatus = terminalStatusFromRunningIds(runningTerminalIds);
if (!terminalStatus && !isRemoteThread) {
return null;
}
return (
<span className="inline-flex shrink-0 items-center gap-1.5">
{terminalStatus ? (
<span
role="img"
aria-label={terminalStatus.label}
title={terminalStatus.label}
className={`inline-flex items-center justify-center ${terminalStatus.colorClass}`}
>
<TerminalIcon className={`size-3 ${terminalStatus.pulse ? "animate-pulse" : ""}`} />
</span>
) : null}
{isRemoteThread ? (
<Tooltip>
<TooltipTrigger
render={
<span
aria-label={threadEnvironmentLabel ?? "Remote"}
className="inline-flex items-center justify-center"
/>
}
>
<CloudIcon className="size-3 text-muted-foreground/60" />
</TooltipTrigger>
<TooltipPopup side="top">{threadEnvironmentLabel}</TooltipPopup>
</Tooltip>
) : null}
</span>
);
}