import type { Thread } from "../types";import { cn } from "../lib/utils";import { findLatestProposedPlan, hasActionableProposedPlan, isLatestTurnSettled,} from "../session-logic"; export const THREAD_SELECTION_SAFE_SELECTOR = "[data-thread-item], [data-thread-selection-safe]";export type SidebarNewThreadEnvMode = "local" | "worktree"; export interface ThreadStatusPill { label: | "Working" | "Connecting" | "Completed" | "Pending Approval" | "Awaiting Input" | "Plan Ready"; colorClass: string; dotClass: string; pulse: boolean;} const THREAD_STATUS_PRIORITY: Record<ThreadStatusPill["label"], number> = { "Pending Approval": 5, "Awaiting Input": 4, Working: 3, Connecting: 3, "Plan Ready": 2, Completed: 1,}; type ThreadStatusInput = Pick< Thread, "interactionMode" | "latestTurn" | "lastVisitedAt" | "proposedPlans" | "session">; export function hasUnseenCompletion(thread: ThreadStatusInput): boolean { if (!thread.latestTurn?.completedAt) return false; const completedAt = Date.parse(thread.latestTurn.completedAt); if (Number.isNaN(completedAt)) return false; if (!thread.lastVisitedAt) return true; const lastVisitedAt = Date.parse(thread.lastVisitedAt); if (Number.isNaN(lastVisitedAt)) return true; return completedAt > lastVisitedAt;} export function shouldClearThreadSelectionOnMouseDown(target: HTMLElement | null): boolean { if (target === null) return true; return !target.closest(THREAD_SELECTION_SAFE_SELECTOR);} export function resolveSidebarNewThreadEnvMode(input: { requestedEnvMode?: SidebarNewThreadEnvMode; defaultEnvMode: SidebarNewThreadEnvMode;}): SidebarNewThreadEnvMode { return input.requestedEnvMode ?? input.defaultEnvMode;} export function resolveThreadRowClassName(input: { isActive: boolean; isSelected: boolean;}): string { const baseClassName = "h-7 w-full translate-x-0 cursor-pointer justify-start px-2 text-left select-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring"; if (input.isSelected && input.isActive) { return cn( baseClassName, "bg-primary/22 text-foreground font-medium hover:bg-primary/26 hover:text-foreground dark:bg-primary/30 dark:hover:bg-primary/36", ); } if (input.isSelected) { return cn( baseClassName, "bg-primary/15 text-foreground hover:bg-primary/19 hover:text-foreground dark:bg-primary/22 dark:hover:bg-primary/28", ); } if (input.isActive) { return cn( baseClassName, "bg-accent/85 text-foreground font-medium hover:bg-accent hover:text-foreground dark:bg-accent/55 dark:hover:bg-accent/70", ); } return cn(baseClassName, "text-muted-foreground hover:bg-accent hover:text-foreground");} export function resolveThreadStatusPill(input: { thread: ThreadStatusInput; hasPendingApprovals: boolean; hasPendingUserInput: boolean;}): ThreadStatusPill | null { const { hasPendingApprovals, hasPendingUserInput, thread } = input; if (hasPendingApprovals) { return { label: "Pending Approval", colorClass: "text-amber-600 dark:text-amber-300/90", dotClass: "bg-amber-500 dark:bg-amber-300/90", pulse: false, }; } if (hasPendingUserInput) { return { label: "Awaiting Input", colorClass: "text-indigo-600 dark:text-indigo-300/90", dotClass: "bg-indigo-500 dark:bg-indigo-300/90", pulse: false, }; } if (thread.session?.status === "running") { return { label: "Working", colorClass: "text-sky-600 dark:text-sky-300/80", dotClass: "bg-sky-500 dark:bg-sky-300/80", pulse: true, }; } if (thread.session?.status === "connecting") { return { label: "Connecting", colorClass: "text-sky-600 dark:text-sky-300/80", dotClass: "bg-sky-500 dark:bg-sky-300/80", pulse: true, }; } const hasPlanReadyPrompt = !hasPendingUserInput && thread.interactionMode === "plan" && isLatestTurnSettled(thread.latestTurn, thread.session) && hasActionableProposedPlan( findLatestProposedPlan(thread.proposedPlans, thread.latestTurn?.turnId ?? null), ); if (hasPlanReadyPrompt) { return { label: "Plan Ready", colorClass: "text-violet-600 dark:text-violet-300/90", dotClass: "bg-violet-500 dark:bg-violet-300/90", pulse: false, }; } if (hasUnseenCompletion(thread)) { return { label: "Completed", colorClass: "text-emerald-600 dark:text-emerald-300/90", dotClass: "bg-emerald-500 dark:bg-emerald-300/90", pulse: false, }; } return null;} export function resolveProjectStatusIndicator( statuses: ReadonlyArray<ThreadStatusPill | null>,): ThreadStatusPill | null { let highestPriorityStatus: ThreadStatusPill | null = null; for (const status of statuses) { if (status === null) continue; if ( highestPriorityStatus === null || THREAD_STATUS_PRIORITY[status.label] > THREAD_STATUS_PRIORITY[highestPriorityStatus.label] ) { highestPriorityStatus = status; } } return highestPriorityStatus;}
import type { Thread } from "../types";
import { cn } from "../lib/utils";
import {
findLatestProposedPlan,
hasActionableProposedPlan,
isLatestTurnSettled,
} from "../session-logic";
export const THREAD_SELECTION_SAFE_SELECTOR = "[data-thread-item], [data-thread-selection-safe]";
export type SidebarNewThreadEnvMode = "local" | "worktree";
export interface ThreadStatusPill {
label:
| "Working"
| "Connecting"
| "Completed"
| "Pending Approval"
| "Awaiting Input"
| "Plan Ready";
colorClass: string;
dotClass: string;
pulse: boolean;
}
const THREAD_STATUS_PRIORITY: Record<ThreadStatusPill["label"], number> = {
"Pending Approval": 5,
"Awaiting Input": 4,
Working: 3,
Connecting: 3,
"Plan Ready": 2,
Completed: 1,
};
type ThreadStatusInput = Pick<
Thread,
"interactionMode" | "latestTurn" | "lastVisitedAt" | "proposedPlans" | "session"
>;
export function hasUnseenCompletion(thread: ThreadStatusInput): boolean {
if (!thread.latestTurn?.completedAt) return false;
const completedAt = Date.parse(thread.latestTurn.completedAt);
if (Number.isNaN(completedAt)) return false;
if (!thread.lastVisitedAt) return true;
const lastVisitedAt = Date.parse(thread.lastVisitedAt);
if (Number.isNaN(lastVisitedAt)) return true;
return completedAt > lastVisitedAt;
}
export function shouldClearThreadSelectionOnMouseDown(target: HTMLElement | null): boolean {
if (target === null) return true;
return !target.closest(THREAD_SELECTION_SAFE_SELECTOR);
}
export function resolveSidebarNewThreadEnvMode(input: {
requestedEnvMode?: SidebarNewThreadEnvMode;
defaultEnvMode: SidebarNewThreadEnvMode;
}): SidebarNewThreadEnvMode {
return input.requestedEnvMode ?? input.defaultEnvMode;
}
export function resolveThreadRowClassName(input: {
isActive: boolean;
isSelected: boolean;
}): string {
const baseClassName =
"h-7 w-full translate-x-0 cursor-pointer justify-start px-2 text-left select-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring";
if (input.isSelected && input.isActive) {
return cn(
baseClassName,
"bg-primary/22 text-foreground font-medium hover:bg-primary/26 hover:text-foreground dark:bg-primary/30 dark:hover:bg-primary/36",
);
}
if (input.isSelected) {
return cn(
baseClassName,
"bg-primary/15 text-foreground hover:bg-primary/19 hover:text-foreground dark:bg-primary/22 dark:hover:bg-primary/28",
);
}
if (input.isActive) {
return cn(
baseClassName,
"bg-accent/85 text-foreground font-medium hover:bg-accent hover:text-foreground dark:bg-accent/55 dark:hover:bg-accent/70",
);
}
return cn(baseClassName, "text-muted-foreground hover:bg-accent hover:text-foreground");
}
export function resolveThreadStatusPill(input: {
thread: ThreadStatusInput;
hasPendingApprovals: boolean;
hasPendingUserInput: boolean;
}): ThreadStatusPill | null {
const { hasPendingApprovals, hasPendingUserInput, thread } = input;
if (hasPendingApprovals) {
return {
label: "Pending Approval",
colorClass: "text-amber-600 dark:text-amber-300/90",
dotClass: "bg-amber-500 dark:bg-amber-300/90",
pulse: false,
};
}
if (hasPendingUserInput) {
return {
label: "Awaiting Input",
colorClass: "text-indigo-600 dark:text-indigo-300/90",
dotClass: "bg-indigo-500 dark:bg-indigo-300/90",
pulse: false,
};
}
if (thread.session?.status === "running") {
return {
label: "Working",
colorClass: "text-sky-600 dark:text-sky-300/80",
dotClass: "bg-sky-500 dark:bg-sky-300/80",
pulse: true,
};
}
if (thread.session?.status === "connecting") {
return {
label: "Connecting",
colorClass: "text-sky-600 dark:text-sky-300/80",
dotClass: "bg-sky-500 dark:bg-sky-300/80",
pulse: true,
};
}
const hasPlanReadyPrompt =
!hasPendingUserInput &&
thread.interactionMode === "plan" &&
isLatestTurnSettled(thread.latestTurn, thread.session) &&
hasActionableProposedPlan(
findLatestProposedPlan(thread.proposedPlans, thread.latestTurn?.turnId ?? null),
);
if (hasPlanReadyPrompt) {
return {
label: "Plan Ready",
colorClass: "text-violet-600 dark:text-violet-300/90",
dotClass: "bg-violet-500 dark:bg-violet-300/90",
pulse: false,
};
}
if (hasUnseenCompletion(thread)) {
return {
label: "Completed",
colorClass: "text-emerald-600 dark:text-emerald-300/90",
dotClass: "bg-emerald-500 dark:bg-emerald-300/90",
pulse: false,
};
}
return null;
}
export function resolveProjectStatusIndicator(
statuses: ReadonlyArray<ThreadStatusPill | null>,
): ThreadStatusPill | null {
let highestPriorityStatus: ThreadStatusPill | null = null;
for (const status of statuses) {
if (status === null) continue;
if (
highestPriorityStatus === null ||
THREAD_STATUS_PRIORITY[status.label] > THREAD_STATUS_PRIORITY[highestPriorityStatus.label]
) {
highestPriorityStatus = status;
}
}
return highestPriorityStatus;
}