Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 91 additions & 2 deletions apps/web/src/components/SidebarV2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
SearchIcon,
ServerIcon,
SquarePenIcon,
TerminalIcon,
Trash2Icon,
Undo2Icon,
} from "lucide-react";
Expand Down Expand Up @@ -124,6 +125,8 @@ import {
prStatusIndicator,
resolveThreadPr,
settledPrHoverColorClass,
terminalStatusFromRunningIds,
type TerminalStatusIndicator,
} from "./ThreadStatusIndicators";
import {
resolveSnoozePresets,
Expand All @@ -136,6 +139,7 @@ import { ProviderInstanceIcon } from "./chat/ProviderInstanceIcon";
import { getTriggerDisplayModelLabel } from "./chat/providerIconUtils";
import { deriveProviderInstanceEntries, type ProviderInstanceEntry } from "../providerInstances";
import { primaryServerProvidersAtom } from "../state/server";
import { useThreadRunningTerminalIds } from "../state/terminalSessions";
import { stackedThreadToast, toastManager } from "./ui/toast";
import { CommandDialogTrigger } from "./ui/command";
import { Button } from "./ui/button";
Expand Down Expand Up @@ -220,6 +224,10 @@ function WorkingDuration(props: { startedAt: string | null }) {
);
}

function terminalProcessLabel(count: number): string {
return `${count} terminal ${count === 1 ? "process" : "processes"} running`;
}

function SidebarV2ThreadTooltip({
thread,
projectTitle,
Expand All @@ -229,6 +237,8 @@ function SidebarV2ThreadTooltip({
modelInstanceId,
modelLabel,
branchMismatch,
terminalStatus,
terminalProcessCount,
}: {
thread: SidebarThreadSummary;
projectTitle: string | null;
Expand All @@ -241,6 +251,8 @@ function SidebarV2ThreadTooltip({
threadBranch: string;
currentBranch: string;
} | null;
terminalStatus: TerminalStatusIndicator | null;
terminalProcessCount: number;
}) {
return (
<TooltipPopup
Expand Down Expand Up @@ -295,6 +307,17 @@ function SidebarV2ThreadTooltip({
<div className="min-w-0 truncate text-foreground/75">{modelLabel}</div>
</div>
) : null}
{terminalStatus ? (
<div className="flex min-w-0 items-center gap-2">
<TerminalIcon
aria-hidden
className={cn("size-3 shrink-0", terminalStatus.colorClass)}
/>
<div className="min-w-0 truncate text-foreground/75">
{terminalProcessLabel(terminalProcessCount)}
</div>
</div>
) : null}
{thread.session?.lastError ? (
<div className="flex min-w-0 items-center gap-2 text-red-600 dark:text-red-400">
<CircleAlertIcon className="size-3 shrink-0 stroke-current" />
Expand Down Expand Up @@ -424,6 +447,12 @@ const SidebarV2Row = memo(function SidebarV2Row(props: {
const lastVisitedAt = useUiStateStore((state) => state.threadLastVisitedAtById[threadKey]);
const isSelected = useThreadSelectionStore((state) => state.selectedThreadKeys.has(threadKey));
const openPrLink = useOpenPrLink();
const runningTerminalIds = useThreadRunningTerminalIds({
environmentId: thread.environmentId,
threadId: thread.id,
});
const terminalStatus = terminalStatusFromRunningIds(runningTerminalIds);
const terminalProcessCount = runningTerminalIds.length;

// Same semantics as v1 (never-visited counts as read): flipping the beta
// flag must not light up every historical thread as unread.
Expand Down Expand Up @@ -542,6 +571,8 @@ const SidebarV2Row = memo(function SidebarV2Row(props: {
modelInstanceId={modelInstanceId}
modelLabel={modelLabel}
branchMismatch={branchMismatch}
terminalStatus={terminalStatus}
terminalProcessCount={terminalProcessCount}
/>
);

Expand Down Expand Up @@ -735,6 +766,16 @@ const SidebarV2Row = memo(function SidebarV2Row(props: {
#{pr.number}
</button>
) : null;
const terminalStatusIcon = terminalStatus ? (
<span
role="img"
aria-label={terminalProcessLabel(terminalProcessCount)}
data-testid={`sidebar-v2-terminal-status-${thread.id}`}
className={cn("inline-flex shrink-0 items-center justify-center", terminalStatus.colorClass)}
>
<TerminalIcon className={cn("size-3.5", terminalStatus.pulse && "animate-status-pulse")} />
</span>
) : null;

if (variant === "slim") {
return (
Expand Down Expand Up @@ -774,6 +815,7 @@ const SidebarV2Row = memo(function SidebarV2Row(props: {
/>
</span>
{title}
{terminalStatusIcon}
{/* The PR badge stays outside the hover-fading slot: it must
remain visible AND clickable while the row is hovered. Only
the time/jump label yields to the settle affordance. */}
Expand Down Expand Up @@ -960,6 +1002,7 @@ const SidebarV2Row = memo(function SidebarV2Row(props: {
) : (
<span className="flex-1" />
)}
{terminalStatusIcon}
{prBadge}
{diff ? (
<span className="shrink-0 font-mono">
Expand Down Expand Up @@ -1028,7 +1071,7 @@ export default function SidebarV2() {
reportFailure: false,
});
const updateSettings = useUpdateClientSettings();
const { copyToClipboard: copyProjectPath } = useCopyToClipboard<{ path: string }>({
const { copyToClipboard: copyPathToClipboard } = useCopyToClipboard<{ path: string }>({
onCopy: ({ path }) => {
toastManager.add({
type: "success",
Expand All @@ -1046,6 +1089,25 @@ export default function SidebarV2() {
);
},
});
const { copyToClipboard: copyBranchToClipboard } = useCopyToClipboard<{ branch: string }>({
target: "branch name",
onCopy: ({ branch }) => {
toastManager.add({
type: "success",
title: "Branch copied",
description: branch,
});
},
onError: (error) => {
toastManager.add(
stackedThreadToast({
type: "error",
title: "Failed to copy branch",
description: error instanceof Error ? error.message : "An error occurred.",
}),
);
},
});
const [projectActionsTarget, setProjectActionsTarget] = useState<SidebarProjectSnapshot | null>(
null,
);
Expand Down Expand Up @@ -1985,6 +2047,10 @@ export default function SidebarV2() {
}
const thread = threadByKeyRef.current.get(threadKey);
if (!thread) return;
const threadWorkspacePath =
thread.worktreePath ??
projectCwdByKey.get(`${thread.environmentId}:${thread.projectId}`) ??
null;
// Un-settle works on every settled row: for explicit settles it
// clears the override, for auto-settled rows it pins the thread
// active until real activity clears the pin. Environments without
Expand Down Expand Up @@ -2033,6 +2099,8 @@ export default function SidebarV2() {
: []),
{ id: "rename", label: "Rename thread" },
{ id: "mark-unread", label: "Mark unread" },
{ id: "copy-path", label: "Copy path", icon: "copy" },
...(thread.branch ? [{ id: "copy-branch", label: "Copy branch", icon: "copy" }] : []),
{ id: "delete", label: "Delete", destructive: true, icon: "trash" },
],
position,
Expand Down Expand Up @@ -2085,6 +2153,24 @@ export default function SidebarV2() {
case "mark-unread":
markThreadUnread(threadKey, thread.latestTurn?.completedAt);
return;
case "copy-path":
if (!threadWorkspacePath) {
toastManager.add(
stackedThreadToast({
type: "error",
title: "Path unavailable",
description: "This thread does not have a workspace path to copy.",
}),
);
return;
}
copyPathToClipboard(threadWorkspacePath, { path: threadWorkspacePath });
return;
case "copy-branch":
if (thread.branch) {
copyBranchToClipboard(thread.branch, { branch: thread.branch });
}
return;
case "delete": {
if (confirmThreadDelete) {
const confirmed = await settlePromise(() =>
Expand Down Expand Up @@ -2122,9 +2208,12 @@ export default function SidebarV2() {
attemptUnsettle,
attemptUnsnooze,
confirmThreadDelete,
copyBranchToClipboard,
copyPathToClipboard,
deleteThread,
handleMultiSelectContextMenu,
markThreadUnread,
projectCwdByKey,
serverConfigs,
startThreadRename,
],
Expand Down Expand Up @@ -2600,7 +2689,7 @@ export default function SidebarV2() {
aria-label="Copy project path"
title="Copy project path"
onClick={() =>
copyProjectPath(member.workspaceRoot, { path: member.workspaceRoot })
copyPathToClipboard(member.workspaceRoot, { path: member.workspaceRoot })
}
>
<CopyIcon className="size-3.5" />
Expand Down
Loading