Skip to content
Merged
Show file tree
Hide file tree
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
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,10 @@ export function useEffectiveDiffSource(taskId: string): EffectiveDiffSource {
const hasLocalChanges = diffStats.filesChanged > 0;
const branchSourceAvailable = !!linkedBranch && aheadOfDefault > 0;

const prUrl = useLinkedBranchPrUrl(taskId);
const prUrl = useLinkedBranchPrUrl({
linkedBranch,
folderPath: workspace?.folderPath ?? null,
});
const prSourceAvailable = !!prUrl;

const repoSlug = repoInfo
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { PRBadgeLink } from "@features/git-interaction/components/PRBadgeLink";
import { usePrDetails } from "@features/git-interaction/hooks/usePrDetails";
import { useTaskPrUrl } from "@features/git-interaction/hooks/useTaskPrUrl";
import type { WorkspaceMode } from "@main/services/workspace/schemas";

interface CommandCenterPRButtonProps {
taskId: string;
workspaceMode: WorkspaceMode | null;
}

/**
* PR badge for a task cell in the command center. Same resolution rules as
* `TaskActionsMenu` via `useTaskPrUrl`, gated by `usePrDetails` returning a
* real PR state.
*/
export function CommandCenterPRButton({
taskId,
workspaceMode,
}: CommandCenterPRButtonProps) {
const isCloud = workspaceMode === "cloud";
const prUrl = useTaskPrUrl(taskId, isCloud);

const {
meta: { state, merged, draft },
} = usePrDetails(prUrl);

if (!prUrl || state === null) return null;

return (
<PRBadgeLink
prUrl={prUrl}
prState={state}
merged={merged}
draft={draft}
compact
/>
);
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { useCloudPrUrl } from "@features/git-interaction/hooks/useCloudPrUrl";
import { useDraftStore } from "@features/message-editor/stores/draftStore";
import { TaskIcon } from "@features/sidebar/components/items/TaskIcon";
import { useTaskPrStatus } from "@features/sidebar/hooks/useTaskPrStatus";
import { TaskInput } from "@features/task-detail/components/TaskInput";
import type { WorkspaceMode } from "@main/services/workspace/schemas";
import {
ArrowsOut,
Cloud,
Desktop,
Folder,
GitFork,
Plus,
X,
Expand All @@ -13,13 +17,16 @@ import { Flex, Text } from "@radix-ui/themes";
import type { Task } from "@shared/types";
import { useNavigationStore } from "@stores/navigationStore";
import { useCallback, useEffect, useRef, useState } from "react";
import type { CommandCenterCellData } from "../hooks/useCommandCenterData";
import type {
CellStatus,
CommandCenterCellData,
} from "../hooks/useCommandCenterData";
import {
getCellSessionId,
useCommandCenterStore,
} from "../stores/commandCenterStore";
import { CommandCenterPRButton } from "./CommandCenterPRButton";
import { CommandCenterSessionView } from "./CommandCenterSessionView";
import { StatusBadge } from "./StatusBadge";
import { TaskSelector } from "./TaskSelector";

interface CommandCenterPanelProps {
Expand All @@ -36,12 +43,57 @@ const environmentConfig: Record<
cloud: { label: "Cloud", icon: Cloud },
};

const STATUS_LABEL: Record<CellStatus, string | null> = {
running: "Running",
waiting: "Waiting",
idle: "Idle",
completed: "Completed",
error: null,
};

function CellStatusBadge({
cell,
}: {
cell: CommandCenterCellData & { task: Task };
}) {
const { task, session, workspaceMode, status } = cell;
const isCloud = workspaceMode === "cloud";
const cloudPrUrl = useCloudPrUrl(task.id);
const { prState, hasDiff } = useTaskPrStatus({
id: task.id,
cloudPrUrl,
taskRunEnvironment: task.latest_run?.environment,
});

const label = STATUS_LABEL[status];
if (label === null) return null;

const taskRunStatus = isCloud
? (session?.cloudStatus ?? task.latest_run?.status ?? undefined)
: undefined;

return (
<span className="inline-flex items-center gap-0.5 rounded bg-gray-3 px-1 py-0.5 text-[10px] text-gray-11">
<TaskIcon
workspaceMode={workspaceMode ?? undefined}
isGenerating={session?.isPromptPending}
needsPermission={(session?.pendingPermissions?.size ?? 0) > 0}
taskRunStatus={taskRunStatus}
prState={prState}
hasDiff={hasDiff}
size={10}
/>
{label}
</span>
);
}

function EnvironmentBadge({ mode }: { mode: WorkspaceMode | null }) {
if (!mode) return null;
const config = environmentConfig[mode];
const Icon = config.icon;
return (
<span className="inline-flex items-center gap-0.5 rounded bg-gray-3 px-1 py-0.5 text-[9px] text-gray-10">
<span className="inline-flex items-center gap-0.5 rounded bg-gray-3 px-1 py-0.5 text-[10px] text-gray-10">
<Icon size={10} />
{config.label}
</span>
Expand Down Expand Up @@ -170,13 +222,18 @@ function PopulatedCell({
{cell.task.title}
</Text>
<Flex align="center" gap="1" className="shrink-0">
<StatusBadge status={cell.status} />
<CellStatusBadge cell={cell} />
<EnvironmentBadge mode={cell.workspaceMode} />
{cell.repoName && (
<span className="rounded bg-gray-3 px-1 py-0.5 text-[9px] text-gray-10">
<span className="inline-flex items-center gap-0.5 rounded bg-gray-3 px-1 py-0.5 text-[10px] text-gray-10">
<Folder size={10} />
{cell.repoName}
</span>
)}
<CommandCenterPRButton
taskId={cell.task.id}
workspaceMode={cell.workspaceMode}
/>
<button
type="button"
onClick={handleExpand}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import {
getPrVisualConfig,
type PrVisualConfig,
parsePrNumber,
} from "@features/git-interaction/utils/prStatus";
import { Button, Flex, Spinner, Text } from "@radix-ui/themes";

interface PRBadgeLinkProps {
prUrl: string;
prState: string;
merged: boolean;
draft: boolean;
isPrPending?: boolean;
/**
* When true, flatten the right edge so a dropdown trigger button can sit
* flush against this badge (used by TaskActionsMenu's combined control).
*/
attachedRight?: boolean;
/**
* Compact pill matching the other badges in the command-center cell header
* (text-[10px], small padding). Renders as a plain anchor instead of a
* Radix Button.
*/
compact?: boolean;
}

const COMPACT_COLOR_CLASSES: Record<PrVisualConfig["color"], string> = {
gray: "bg-(--gray-3) text-(--gray-11) hover:bg-(--gray-4)",
green: "bg-(--green-3) text-(--green-11) hover:bg-(--green-4)",
red: "bg-(--red-3) text-(--red-11) hover:bg-(--red-4)",
purple: "bg-(--purple-3) text-(--purple-11) hover:bg-(--purple-4)",
};

/**
* The colored "open this PR on GitHub" badge — styled by the PR's lifecycle
* state (open / draft / closed / merged) and rendered as an external anchor.
* Shared between the task header (TaskActionsMenu) and the command center
* cell header.
*/
export function PRBadgeLink({
prUrl,
prState,
merged,
draft,
isPrPending = false,
attachedRight = false,
compact = false,
}: PRBadgeLinkProps) {
const config = getPrVisualConfig(prState, merged, draft);
const prNumber = parsePrNumber(prUrl);

if (compact) {
return (
<a
href={prUrl}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
className={`inline-flex items-center gap-0.5 rounded px-1 py-0.5 text-[10px] no-underline ${COMPACT_COLOR_CLASSES[config.color]}`}
>
{isPrPending ? (
<Spinner size="1" />
) : (
<config.Icon size={10} weight="bold" />
)}
<span>
{config.label}
{prNumber && ` #${prNumber}`}
</span>
</a>
);
}

return (
<Button
size="1"
variant="soft"
color={config.color}
asChild
className={attachedRight ? "rounded-r-none" : undefined}
>
<a
href={prUrl}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
>
<Flex align="center" gap="2">
{isPrPending ? (
<Spinner size="1" />
) : (
<config.Icon size={12} weight="bold" />
)}
<Text size="1">
{config.label}
{prNumber && ` #${prNumber}`}
</Text>
</Flex>
</a>
</Button>
);
}
Loading
Loading