import { scopeProjectRef, scopedThreadKey, scopeThreadRef } from "@t3tools/client-runtime";
import type { GitStatusResult } 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 { useGitStatus } from "../lib/gitStatusState";
import { type AppState, selectProjectByRef, useStore } from "../store";
import { selectThreadTerminalState, useTerminalStateStore } from "../terminalStateStore";
import { useUiStateStore } from "../uiStateStore";
import { resolveThreadStatusPill, type ThreadStatusPill } from "./Sidebar.logic";
import type { SidebarThreadSummary } from "../types";
import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";
export interface PrStatusIndicator {
label: "PR open" | "PR closed" | "PR merged";
colorClass: string;
tooltip: string;
url: string;
}
export interface TerminalStatusIndicator {
label: "Terminal process running";
colorClass: string;
pulse: boolean;
}
export type ThreadPr = GitStatusResult["pr"];
export function prStatusIndicator(pr: ThreadPr): PrStatusIndicator | null {
if (!pr) return null;
if (pr.state === "open") {
return {
label: "PR open",
colorClass: "text-emerald-600 dark:text-emerald-300/90",
tooltip: `#${pr.number} PR open: ${pr.title}`,
url: pr.url,
};
}
if (pr.state === "closed") {
return {
label: "PR closed",
colorClass: "text-zinc-500 dark:text-zinc-400/80",
tooltip: `#${pr.number} PR closed: ${pr.title}`,
url: pr.url,
};
}
if (pr.state === "merged") {
return {
label: "PR merged",
colorClass: "text-violet-600 dark:text-violet-300/90",
tooltip: `#${pr.number} PR merged: ${pr.title}`,
url: pr.url,
};
}
return null;
}
export function resolveThreadPr(
threadBranch: string | null,
gitStatus: GitStatusResult | null,
): ThreadPr | null {
if (threadBranch === null || gitStatus === null || gitStatus.branch !== threadBranch) {
return null;
}
return gitStatus.pr ?? null;
}
export function terminalStatusFromRunningIds(
runningTerminalIds: 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 (
{status.label}
);
}
return (
{status.label}
);
}
/**
* Non-interactive leading status icons for a thread row in compact contexts
* like the command palette. Shows the PR 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 = useGitStatus({
environmentId: thread.environmentId,
cwd: thread.branch != null ? gitCwd : null,
});
const pr = resolveThreadPr(thread.branch, gitStatus.data);
const prStatus = prStatusIndicator(pr);
const threadStatus = resolveThreadStatusPill({
thread: {
...thread,
lastVisitedAt,
},
});
if (!prStatus && !threadStatus) {
return null;
}
return (
{prStatus ? (
}
>
{prStatus.tooltip}
) : 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 threadRef = scopeThreadRef(thread.environmentId, thread.id);
const runningTerminalIds = useTerminalStateStore(
(state) =>
selectThreadTerminalState(state.terminalStateByThreadKey, threadRef).runningTerminalIds,
);
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 (
{terminalStatus ? (
) : null}
{isRemoteThread ? (
}
>
{threadEnvironmentLabel}
) : null}
);
}