import { useEffect, useMemo, useState, type ReactNode } from "react";
import type { AgentType, RepoBranchRecord, RepoOverview, RepoStackAction, TaskWorkbenchSnapshot, WorkbenchTaskStatus } from "@sandbox-agent/foundry-shared";
import { useInterest } from "@sandbox-agent/foundry-client";
import { useMutation, useQuery } from "@tanstack/react-query";
import { Link, useNavigate } from "@tanstack/react-router";
import { Button } from "baseui/button";
import { Input } from "baseui/input";
import { Modal, ModalBody, ModalFooter, ModalHeader } from "baseui/modal";
import { Select, type OnChangeParams, type Option, type Value } from "baseui/select";
import { Skeleton } from "baseui/skeleton";
import { Tag } from "baseui/tag";
import { Textarea } from "baseui/textarea";
import { StyledDivider } from "baseui/divider";
import { styled, useStyletron } from "baseui";
import { HeadingSmall, HeadingXSmall, LabelSmall, LabelXSmall, MonoLabelSmall, ParagraphSmall } from "baseui/typography";
import { Bot, CircleAlert, FolderGit2, GitBranch, MessageSquareText, SendHorizontal, Shuffle } from "lucide-react";
import { formatDiffStat } from "../features/tasks/model";
import { deriveHeaderStatus, describeTaskState } from "../features/tasks/status";
import { HeaderStatusPill } from "./mock-layout/ui";
import { buildTranscript, resolveSessionSelection } from "../features/sessions/model";
import { backendClient } from "../lib/backend";
import { interestManager } from "../lib/interest";
import { DevPanel, useDevPanel } from "./dev-panel";
interface WorkspaceDashboardProps {
workspaceId: string;
selectedTaskId?: string;
selectedRepoId?: string;
}
type RepoOverviewFilter = "active" | "archived" | "unmapped" | "all";
type StatusTagKind = "neutral" | "positive" | "warning" | "negative";
type SelectItem = Readonly<{ id: string; label: string; disabled?: boolean }>;
const AppShell = styled("main", ({ $theme }) => ({
minHeight: "100dvh",
backgroundColor: $theme.colors.backgroundPrimary,
}));
const DashboardGrid = styled("div", ({ $theme }) => ({
display: "grid",
gap: "1px",
minHeight: "100dvh",
backgroundColor: $theme.colors.borderOpaque,
gridTemplateColumns: "minmax(0, 1fr)",
"@media screen and (min-width: 960px)": {
gridTemplateColumns: "260px minmax(0, 1fr)",
},
"@media screen and (min-width: 1480px)": {
gridTemplateColumns: "260px minmax(0, 1fr) 280px",
},
}));
const Panel = styled("section", ({ $theme }) => ({
minHeight: 0,
display: "flex",
flexDirection: "column",
backgroundColor: $theme.colors.backgroundSecondary,
overflow: "hidden",
}));
const PanelHeader = styled("div", ({ $theme }) => ({
padding: "10px 12px",
borderBottom: `1px solid ${$theme.colors.borderOpaque}`,
display: "flex",
flexDirection: "column",
gap: "8px",
}));
const ScrollBody = styled("div", ({ $theme }) => ({
minHeight: 0,
flex: 1,
overflowY: "auto",
padding: "10px 12px",
display: "flex",
flexDirection: "column",
gap: "8px",
}));
const DetailRail = styled("aside", ({ $theme }) => ({
minHeight: 0,
display: "none",
backgroundColor: $theme.colors.backgroundSecondary,
overflow: "hidden",
"@media screen and (min-width: 1480px)": {
display: "flex",
flexDirection: "column",
},
}));
const FILTER_OPTIONS: SelectItem[] = [
{ id: "active", label: "Active + Unmapped" },
{ id: "archived", label: "Archived Tasks" },
{ id: "unmapped", label: "Unmapped Only" },
{ id: "all", label: "All Branches" },
];
const AGENT_OPTIONS: SelectItem[] = [
{ id: "codex", label: "codex" },
{ id: "claude", label: "claude" },
];
function statusKind(status: WorkbenchTaskStatus): StatusTagKind {
if (status === "running") return "positive";
if (status === "error") return "negative";
if (status === "new" || String(status).startsWith("init_")) return "warning";
return "neutral";
}
function normalizeAgent(agent: string | null): AgentType | undefined {
if (agent === "claude" || agent === "codex") {
return agent;
}
return undefined;
}
function formatTime(value: number): string {
return new Date(value).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
}
function formatRelativeAge(value: number): string {
const deltaSeconds = Math.max(0, Math.floor((Date.now() - value) / 1000));
if (deltaSeconds < 60) return `${deltaSeconds}s`;
const minutes = Math.floor(deltaSeconds / 60);
if (minutes < 60) return `${minutes}m`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h`;
const days = Math.floor(hours / 24);
return `${days}d`;
}
function branchTestIdToken(value: string): string {
const token = value
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
return token || "branch";
}
function repoSummary(overview: RepoOverview | undefined): {
total: number;
mapped: number;
unmapped: number;
conflicts: number;
needsRestack: number;
openPrs: number;
} {
if (!overview) {
return {
total: 0,
mapped: 0,
unmapped: 0,
conflicts: 0,
needsRestack: 0,
openPrs: 0,
};
}
let mapped = 0;
let conflicts = 0;
let needsRestack = 0;
let openPrs = 0;
for (const row of overview.branches) {
if (row.taskId) {
mapped += 1;
}
if (row.conflictsWithMain) {
conflicts += 1;
}
if (row.trackedInStack && row.parentBranch && row.hasUnpushed) {
needsRestack += 1;
}
if (row.prNumber && row.prState !== "MERGED" && row.prState !== "CLOSED") {
openPrs += 1;
}
}
return {
total: overview.branches.length,
mapped,
unmapped: Math.max(0, overview.branches.length - mapped),
conflicts,
needsRestack,
openPrs,
};
}
function branchKind(row: RepoBranchRecord): StatusTagKind {
if (row.conflictsWithMain) {
return "negative";
}
if (row.prState === "OPEN" || row.prState === "DRAFT") {
return "warning";
}
if (row.prState === "MERGED") {
return "positive";
}
return "neutral";
}
function matchesOverviewFilter(branch: RepoBranchRecord, filter: RepoOverviewFilter): boolean {
if (filter === "archived") {
return branch.taskStatus === "archived";
}
if (filter === "unmapped") {
return branch.taskId === null;
}
if (filter === "active") {
return branch.taskStatus !== "archived";
}
return true;
}
function selectValue(option: Option | null | undefined): Value {
return option ? [option] : [];
}
function optionId(value: Value): string | null {
const id = value[0]?.id;
if (typeof id === "string") return id;
if (typeof id === "number") return String(id);
return null;
}
function createOption(item: SelectItem): Option {
return {
id: item.id,
label: item.label,
disabled: item.disabled,
};
}
function inputTestIdOverrides(testId?: string) {
return testId
? {
Input: {
props: {
"data-testid": testId,
},
},
}
: undefined;
}
function textareaTestIdOverrides(testId?: string) {
return testId
? {
Input: {
props: {
"data-testid": testId,
},
},
}
: undefined;
}
function selectTestIdOverrides(testId?: string) {
return testId
? {
ControlContainer: {
props: {
"data-testid": testId,
},
},
}
: undefined;
}
function EmptyState({ children, testId }: { children: string; testId?: string }) {
return (
);
}
function StatusPill({ children, kind }: { children: ReactNode; kind: StatusTagKind }) {
return (
{children}
);
}
function MetaRow({ label, value, mono = false }: { label: string; value: string; mono?: boolean }) {
return (
{label}
{mono ? (
{value}
) : (
{value}
)}
);
}
export function WorkspaceDashboard({ workspaceId, selectedTaskId, selectedRepoId }: WorkspaceDashboardProps) {
const [css, theme] = useStyletron();
const navigate = useNavigate();
const showDevPanel = useDevPanel();
const repoOverviewMode = typeof selectedRepoId === "string" && selectedRepoId.length > 0;
const [draft, setDraft] = useState("");
const [activeSessionId, setActiveSessionId] = useState(null);
const [createRepoId, setCreateRepoId] = useState("");
const [newTask, setNewTask] = useState("");
const [newTitle, setNewTitle] = useState("");
const [newBranchName, setNewBranchName] = useState("");
const [createOnBranch, setCreateOnBranch] = useState(null);
const [addRepoOpen, setAddRepoOpen] = useState(false);
const [createTaskOpen, setCreateTaskOpen] = useState(false);
const [addRepoRemote, setAddRepoRemote] = useState("");
const [addRepoError, setAddRepoError] = useState(null);
const [stackActionError, setStackActionError] = useState(null);
const [stackActionMessage, setStackActionMessage] = useState(null);
const [selectedOverviewBranch, setSelectedOverviewBranch] = useState(null);
const [overviewFilter, setOverviewFilter] = useState("active");
const [reparentBranchName, setReparentBranchName] = useState(null);
const [reparentParentBranch, setReparentParentBranch] = useState("");
const [newAgentType, setNewAgentType] = useState(() => {
try {
const raw = globalThis.localStorage?.getItem("hf.settings.agentType");
return raw === "claude" || raw === "codex" ? raw : "codex";
} catch {
return "codex";
}
});
const [createError, setCreateError] = useState(null);
const workspaceState = useInterest(interestManager, "workspace", { workspaceId });
const repos = workspaceState.data?.repos ?? [];
const rows = workspaceState.data?.taskSummaries ?? [];
const selectedSummary = useMemo(() => rows.find((row) => row.id === selectedTaskId) ?? rows[0] ?? null, [rows, selectedTaskId]);
const taskState = useInterest(
interestManager,
"task",
!repoOverviewMode && selectedSummary
? {
workspaceId,
repoId: selectedSummary.repoId,
taskId: selectedSummary.id,
}
: null,
);
const activeRepoId = selectedRepoId ?? createRepoId;
const repoOverviewQuery = useQuery({
queryKey: ["workspace", workspaceId, "repo-overview", activeRepoId],
enabled: Boolean(repoOverviewMode && activeRepoId),
queryFn: async () => {
if (!activeRepoId) {
throw new Error("No repo selected");
}
return backendClient.getRepoOverview(workspaceId, activeRepoId);
},
});
useEffect(() => {
if (repoOverviewMode && selectedRepoId) {
setCreateRepoId(selectedRepoId);
return;
}
if (!createRepoId && repos.length > 0) {
setCreateRepoId(repos[0]!.id);
}
}, [createRepoId, repoOverviewMode, repos, selectedRepoId]);
useEffect(() => {
try {
globalThis.localStorage?.setItem("hf.settings.agentType", newAgentType);
} catch {
// ignore storage failures
}
}, [newAgentType]);
const repoGroups = useMemo(() => {
const byRepo = new Map();
for (const row of rows) {
const bucket = byRepo.get(row.repoId);
if (bucket) {
bucket.push(row);
} else {
byRepo.set(row.repoId, [row]);
}
}
return repos
.map((repo) => {
const tasks = [...(byRepo.get(repo.id) ?? [])].sort((a, b) => b.updatedAtMs - a.updatedAtMs);
const latestTaskAt = tasks[0]?.updatedAtMs ?? 0;
return {
repoId: repo.id,
repoLabel: repo.label,
latestActivityAt: Math.max(repo.latestActivityMs, latestTaskAt),
tasks,
};
})
.sort((a, b) => {
if (a.latestActivityAt !== b.latestActivityAt) {
return b.latestActivityAt - a.latestActivityAt;
}
return a.repoLabel.localeCompare(b.repoLabel);
});
}, [repos, rows]);
const selectedForSession = repoOverviewMode ? null : (taskState.data ?? null);
const activeSandbox = useMemo(() => {
if (!selectedForSession) return null;
const byActive = selectedForSession.activeSandboxId
? (selectedForSession.sandboxes.find((sandbox) => sandbox.sandboxId === selectedForSession.activeSandboxId) ?? null)
: null;
return byActive ?? selectedForSession.sandboxes[0] ?? null;
}, [selectedForSession]);
useEffect(() => {
if (!repoOverviewMode && !selectedTaskId && rows.length > 0) {
void navigate({
to: "/workspaces/$workspaceId/tasks/$taskId",
params: {
workspaceId,
taskId: rows[0]!.id,
},
search: { sessionId: undefined },
replace: true,
});
}
}, [navigate, repoOverviewMode, rows, selectedTaskId, workspaceId]);
useEffect(() => {
setActiveSessionId(null);
setDraft("");
}, [selectedForSession?.id]);
const sessionRows = selectedForSession?.sessionsSummary ?? [];
const taskRuntimeStatus = selectedForSession?.runtimeStatus ?? selectedForSession?.status ?? null;
const taskStatusState = describeTaskState(taskRuntimeStatus, selectedForSession?.statusMessage ?? null);
const taskStateSummary = `${taskStatusState.title}. ${taskStatusState.detail}`;
const shouldUseTaskStateEmptyState = Boolean(selectedForSession && taskRuntimeStatus && taskRuntimeStatus !== "running" && taskRuntimeStatus !== "idle");
const sessionSelection = useMemo(
() =>
resolveSessionSelection({
explicitSessionId: activeSessionId,
taskSessionId: selectedForSession?.activeSessionId ?? null,
sessions: sessionRows.map((session) => ({
id: session.id,
agent: session.agent,
agentSessionId: session.sessionId ?? "",
lastConnectionId: "",
createdAt: 0,
status: session.status,
})),
}),
[activeSessionId, selectedForSession?.activeSessionId, sessionRows],
);
const resolvedSessionId = sessionSelection.sessionId;
const staleSessionId = sessionSelection.staleSessionId;
const sessionState = useInterest(
interestManager,
"session",
selectedForSession && resolvedSessionId
? {
workspaceId,
repoId: selectedForSession.repoId,
taskId: selectedForSession.id,
sessionId: resolvedSessionId,
}
: null,
);
const selectedSessionSummary = useMemo(() => sessionRows.find((session) => session.id === resolvedSessionId) ?? null, [resolvedSessionId, sessionRows]);
const isPendingProvision = selectedSessionSummary?.status === "pending_provision";
const isPendingSessionCreate = selectedSessionSummary?.status === "pending_session_create";
const isSessionError = selectedSessionSummary?.status === "error";
const canStartSession = Boolean(selectedForSession && activeSandbox?.sandboxId);
const devPanelFocusedTask = useMemo(() => {
if (repoOverviewMode) {
return null;
}
const task = selectedForSession ?? selectedSummary;
if (!task) {
return null;
}
return {
id: task.id,
repoId: task.repoId,
title: task.title,
status: task.status,
runtimeStatus: selectedForSession?.runtimeStatus ?? null,
statusMessage: selectedForSession?.statusMessage ?? null,
branch: task.branch ?? null,
activeSandboxId: selectedForSession?.activeSandboxId ?? null,
activeSessionId: selectedForSession?.activeSessionId ?? null,
sandboxes: selectedForSession?.sandboxes ?? [],
sessions: selectedForSession?.sessionsSummary ?? [],
};
}, [repoOverviewMode, selectedForSession, selectedSummary]);
const devPanelSnapshot = useMemo(
(): TaskWorkbenchSnapshot => ({
workspaceId,
repos: repos.map((repo) => ({ id: repo.id, label: repo.label })),
projects: [],
tasks: rows.map((task) => ({
id: task.id,
repoId: task.repoId,
title: task.title,
status: task.status,
runtimeStatus: selectedForSession?.id === task.id ? selectedForSession.runtimeStatus : undefined,
statusMessage: selectedForSession?.id === task.id ? selectedForSession.statusMessage : null,
repoName: task.repoName,
updatedAtMs: task.updatedAtMs,
branch: task.branch ?? null,
pullRequest: task.pullRequest,
tabs: task.sessionsSummary.map((session) => ({
...session,
draft: {
text: "",
attachments: [],
updatedAtMs: null,
},
transcript: [],
})),
fileChanges: [],
diffs: {},
fileTree: [],
minutesUsed: 0,
activeSandboxId: selectedForSession?.id === task.id ? selectedForSession.activeSandboxId : null,
})),
}),
[repos, rows, selectedForSession, workspaceId],
);
const startSessionFromTask = async (): Promise<{ id: string; status: "running" | "idle" | "error" }> => {
if (!selectedForSession || !activeSandbox?.sandboxId) {
throw new Error("No sandbox is available for this task");
}
return backendClient.createSandboxSession({
workspaceId,
providerId: activeSandbox.providerId,
sandboxId: activeSandbox.sandboxId,
prompt: selectedForSession.task,
cwd: activeSandbox.cwd ?? undefined,
agent: normalizeAgent(selectedForSession.agentType),
});
};
const createSession = useMutation({
mutationFn: async () => startSessionFromTask(),
onSuccess: (session) => {
setActiveSessionId(session.id);
},
});
const ensureSessionForPrompt = async (): Promise => {
if (resolvedSessionId) {
return resolvedSessionId;
}
const created = await startSessionFromTask();
setActiveSessionId(created.id);
return created.id;
};
const sendPrompt = useMutation({
mutationFn: async (prompt: string) => {
if (!selectedForSession || !activeSandbox?.sandboxId) {
throw new Error("No sandbox is available for this task");
}
const sessionId = await ensureSessionForPrompt();
await backendClient.sendSandboxPrompt({
workspaceId,
providerId: activeSandbox.providerId,
sandboxId: activeSandbox.sandboxId,
sessionId,
prompt,
});
},
onSuccess: () => {
setDraft("");
},
});
const transcript = buildTranscript(sessionState.data?.transcript ?? []);
const canCreateTask = createRepoId.trim().length > 0 && newTask.trim().length > 0;
const createTask = useMutation({
mutationFn: async () => {
const repoId = createRepoId.trim();
const task = newTask.trim();
if (!repoId || !task) {
throw new Error("Repository and task are required");
}
const draftTitle = newTitle.trim();
const draftBranchName = newBranchName.trim();
return backendClient.createTask({
workspaceId,
repoId,
task,
agentType: newAgentType,
explicitTitle: draftTitle || undefined,
explicitBranchName: createOnBranch ? undefined : draftBranchName || undefined,
onBranch: createOnBranch ?? undefined,
});
},
onSuccess: async (task) => {
setCreateError(null);
setNewTask("");
setNewTitle("");
setNewBranchName("");
setCreateOnBranch(null);
setCreateTaskOpen(false);
await navigate({
to: "/workspaces/$workspaceId/tasks/$taskId",
params: {
workspaceId,
taskId: task.taskId,
},
search: { sessionId: undefined },
});
},
onError: (error) => {
setCreateError(error instanceof Error ? error.message : String(error));
},
});
const addRepo = useMutation({
mutationFn: async (remoteUrl: string) => {
const trimmed = remoteUrl.trim();
if (!trimmed) {
throw new Error("Remote URL is required");
}
return backendClient.addRepo(workspaceId, trimmed);
},
onSuccess: async (created) => {
setAddRepoError(null);
setAddRepoRemote("");
setAddRepoOpen(false);
setCreateRepoId(created.repoId);
if (repoOverviewMode) {
await navigate({
to: "/workspaces/$workspaceId/repos/$repoId",
params: {
workspaceId,
repoId: created.repoId,
},
});
}
},
onError: (error) => {
setAddRepoError(error instanceof Error ? error.message : String(error));
},
});
const runStackAction = useMutation({
mutationFn: async (input: { action: RepoStackAction; branchName?: string; parentBranch?: string }) => {
if (!activeRepoId) {
throw new Error("No repository selected");
}
return backendClient.runRepoStackAction({
workspaceId,
repoId: activeRepoId,
action: input.action,
branchName: input.branchName,
parentBranch: input.parentBranch,
});
},
onSuccess: async (result) => {
if (result.executed) {
setStackActionError(null);
setStackActionMessage(result.message);
} else {
setStackActionMessage(null);
setStackActionError(result.message);
}
await repoOverviewQuery.refetch();
},
onError: (error) => {
setStackActionMessage(null);
setStackActionError(error instanceof Error ? error.message : String(error));
},
});
const openCreateFromBranch = (repoId: string, branchName: string): void => {
setCreateRepoId(repoId);
setCreateOnBranch(branchName);
setNewBranchName("");
setCreateError(null);
if (!newTask.trim()) {
setNewTask(`Continue work on ${branchName}`);
}
setCreateTaskOpen(true);
};
const repoOptions = useMemo(() => repos.map((repo) => createOption({ id: repo.id, label: repo.label })), [repos]);
const selectedRepoOption = repoOptions.find((option) => option.id === createRepoId) ?? null;
const selectedAgentOption = useMemo(() => createOption(AGENT_OPTIONS.find((option) => option.id === newAgentType) ?? AGENT_OPTIONS[0]!), [newAgentType]);
const selectedFilterOption = useMemo(
() => createOption(FILTER_OPTIONS.find((option) => option.id === overviewFilter) ?? FILTER_OPTIONS[0]!),
[overviewFilter],
);
const sessionOptions = useMemo(
() => sessionRows.map((session) => createOption({ id: session.id, label: `${session.sessionName} (${session.status})` })),
[sessionRows],
);
const selectedSessionOption = sessionOptions.find((option) => option.id === resolvedSessionId) ?? null;
const overview = repoOverviewQuery.data;
const overviewStats = repoSummary(overview);
const stackActionsEnabled = Boolean(overview?.stackAvailable) && !runStackAction.isPending;
const filteredOverviewBranches = useMemo(() => {
if (!overview?.branches?.length) {
return [];
}
return overview.branches.filter((branch) => matchesOverviewFilter(branch, overviewFilter));
}, [overview, overviewFilter]);
const selectedBranchOverview = useMemo(() => {
if (!filteredOverviewBranches.length) {
return null;
}
if (!selectedOverviewBranch) {
return filteredOverviewBranches[0] ?? null;
}
return filteredOverviewBranches.find((row) => row.branchName === selectedOverviewBranch) ?? filteredOverviewBranches[0] ?? null;
}, [filteredOverviewBranches, selectedOverviewBranch]);
useEffect(() => {
if (!filteredOverviewBranches.length) {
setSelectedOverviewBranch(null);
return;
}
if (!selectedOverviewBranch || !filteredOverviewBranches.some((row) => row.branchName === selectedOverviewBranch)) {
setSelectedOverviewBranch(filteredOverviewBranches[0]?.branchName ?? null);
}
}, [filteredOverviewBranches, selectedOverviewBranch]);
const handleReparentSubmit = (): void => {
if (!reparentBranchName || !reparentParentBranch.trim()) {
return;
}
setStackActionError(null);
void runStackAction
.mutateAsync({
action: "reparent_branch",
branchName: reparentBranchName,
parentBranch: reparentParentBranch.trim(),
})
.then(() => {
setReparentBranchName(null);
setReparentParentBranch("");
})
.catch(() => {
// mutation state is surfaced above
});
};
const modalOverrides = useMemo(
() => ({
Dialog: {
style: {
borderRadius: "0",
backgroundColor: theme.colors.backgroundSecondary,
border: `1px solid ${theme.colors.borderOpaque}`,
boxShadow: "0 18px 40px rgba(0, 0, 0, 0.45)",
},
},
Close: {
style: {
borderRadius: "0",
},
},
}),
[theme.colors.backgroundSecondary, theme.colors.borderOpaque],
);
return (
{
setAddRepoError(null);
setAddRepoOpen(true);
}}
data-testid="repo-add-open"
>
Add Repo
Tasks
{workspaceState.status === "loading" ? (
<>
>
) : null}
{workspaceState.status !== "loading" && repoGroups.length === 0 ? (
No repos or tasks yet. Add a repo to start a workspace.
) : null}
{repoGroups.map((group) => (
{group.repoLabel}
{group.tasks
.filter((task) => task.status !== "archived" || task.id === selectedSummary?.id)
.map((task) => {
const isActive = !repoOverviewMode && task.id === selectedSummary?.id;
return (
{task.title ?? "Determining title..."}
{task.branch ?? "Determining branch..."}
{task.status}
);
})}
{
setCreateRepoId(group.repoId);
setCreateOnBranch(null);
setCreateError(null);
setCreateTaskOpen(true);
}}
data-testid={group.repoId === createRepoId ? "task-create-open" : `task-create-open-${group.repoId}`}
>
Create Task
))}
{repoOverviewMode ? (
<>
Repo Overview
{
const next = optionId(params.value) as RepoOverviewFilter | null;
if (next) {
setOverviewFilter(next);
}
}}
aria-label="Filter branches"
overrides={selectTestIdOverrides("repo-overview-filter")}
/>
{
setStackActionError(null);
void runStackAction.mutateAsync({ action: "sync_repo" });
}}
data-testid="repo-stack-sync"
>
Sync Stack
{
setStackActionError(null);
void runStackAction.mutateAsync({ action: "restack_repo" });
}}
data-testid="repo-stack-restack-all"
>
Restack All
Branches {overviewStats.total}
Mapped {overviewStats.mapped}
Unmapped {overviewStats.unmapped}
Conflicts {overviewStats.conflicts}
Open PRs {overviewStats.openPrs}
Needs restack {overviewStats.needsRestack}
{overview && !overview.stackAvailable ? (
git-spice is unavailable for this repo. Stack actions are disabled.
) : null}
{stackActionError ? (
{stackActionError}
) : null}
{stackActionMessage ? (
{stackActionMessage}
) : null}
{repoOverviewQuery.isLoading ? : null}
{!repoOverviewQuery.isLoading && !overview ? No repo overview is available yet. : null}
{overview ? (
{["Branch", "Parent", "Ahead", "PR", "CI/Review", "Actions"].map((label) => (
{label}
))}
{filteredOverviewBranches.length === 0 ? (
No branches match the selected filter.
) : (
filteredOverviewBranches.map((branch) => {
const selectedRow = selectedBranchOverview?.branchName === branch.branchName;
const branchToken = branchTestIdToken(branch.branchName);
const rowClass = css({
display: "contents",
});
const cellClass = css({
padding: `${theme.sizing.scale400} ${theme.sizing.scale400}`,
borderBottom: `1px solid ${theme.colors.borderOpaque}`,
backgroundColor: selectedRow ? "rgba(29, 111, 95, 0.08)" : theme.colors.backgroundSecondary,
fontSize: theme.typography.ParagraphSmall.fontSize,
cursor: "pointer",
});
return (
setSelectedOverviewBranch(branch.branchName)}
data-testid={`repo-overview-row-${branchToken}`}
>
{branch.branchName}
{formatRelativeAge(branch.updatedAt)}
{branch.taskId ? "task" : "unmapped"}
{branch.trackedInStack ?
stack : null}
{branch.parentBranch ?? "-"}
{branch.hasUnpushed ? "yes" : "-"}
{branch.ciStatus ?? "-"} / {branch.reviewStatus ?? "-"}
{
event.stopPropagation();
setStackActionError(null);
void runStackAction.mutateAsync({
action: "restack_subtree",
branchName: branch.branchName,
});
}}
data-testid={`repo-overview-restack-${branchToken}`}
>
Restack
{
event.stopPropagation();
setStackActionError(null);
void runStackAction.mutateAsync({
action: "rebase_branch",
branchName: branch.branchName,
});
}}
data-testid={`repo-overview-rebase-${branchToken}`}
>
Rebase
{
event.stopPropagation();
setReparentBranchName(branch.branchName);
setReparentParentBranch(branch.parentBranch ?? "main");
setStackActionError(null);
}}
data-testid={`repo-overview-reparent-${branchToken}`}
>
Reparent
{!branch.taskId ? (
{
event.stopPropagation();
openCreateFromBranch(activeRepoId, branch.branchName);
}}
data-testid={`repo-overview-create-${branchToken}`}
>
Create Task
) : null}
{branch.conflictsWithMain ? "conflict" : "ok"}
);
})
)}
) : null}
>
) : (
<>
{selectedForSession ? (selectedForSession.title ?? "Determining title...") : "No task selected"}
{selectedForSession ? (
) : null}
{selectedForSession && !resolvedSessionId ? (
{
void createSession.mutateAsync();
}}
disabled={createSession.isPending || !canStartSession}
>
{staleSessionId ? "Start New Session" : "Start Session"}
) : null}
{selectedForSession ? (
{taskStateSummary}
) : null}
{!selectedForSession ? (
Select a task from the left sidebar.
) : (
<>
Session {resolvedSessionId ?? staleSessionId ?? "(none)"}
{sessionRows.length > 0 ? (
{
const next = optionId(params.value);
if (next) {
setActiveSessionId(next);
}
}}
overrides={selectTestIdOverrides("task-session-select")}
/>
) : null}
{resolvedSessionId && sessionState.status === "loading" ?
: null}
{selectedSessionSummary && (isPendingProvision || isPendingSessionCreate) ? (
{shouldUseTaskStateEmptyState ? taskStatusState.title : isPendingProvision ? "Provisioning sandbox..." : "Creating session..."}
{shouldUseTaskStateEmptyState
? taskStateSummary
: (selectedForSession?.statusMessage ??
(isPendingProvision ? "The task is still provisioning." : "The session is being created."))}
) : null}
{transcript.length === 0 && !(resolvedSessionId && sessionState.status === "loading") ? (
{shouldUseTaskStateEmptyState
? taskStateSummary
: isPendingProvision
? (selectedForSession.statusMessage ?? "Provisioning sandbox...")
: isPendingSessionCreate
? "Creating session..."
: isSessionError
? (selectedSessionSummary?.errorMessage ?? "Session failed to start.")
: !activeSandbox?.sandboxId
? selectedForSession.statusMessage
? `Sandbox unavailable: ${selectedForSession.statusMessage}`
: "This task is still provisioning its sandbox."
: staleSessionId
? `Session ${staleSessionId} is unavailable. Start a new session to continue.`
: resolvedSessionId
? "No transcript events yet. Send a prompt to start this session."
: "No active session for this task."}
) : null}
{transcript.map((entry) => (
{entry.sender}
{formatTime(entry.createdAt)}
{entry.text}
))}
>
)}
>
)}
{repoOverviewMode ? "Repo Details" : "Task Details"}
{repoOverviewMode ? (
!overview ? (
No repo overview available.
) : (
<>
Selected Branch
{!selectedBranchOverview ? (
Select a branch in the center panel.
) : (
)}
>
)
) : !selectedForSession ? (
No task selected.
) : (
<>
Task
{selectedForSession.task}
{taskRuntimeStatus === "error" ? (
Task reported an error state
{taskStatusState.detail}
) : null}
>
)}
setAddRepoOpen(false)} overrides={modalOverrides}>
Add Repo
setAddRepoOpen(false)}>
Cancel
{
setAddRepoError(null);
void addRepo.mutateAsync(addRepoRemote);
}}
disabled={addRepo.isPending || addRepoRemote.trim().length === 0}
data-testid="repo-add-submit"
>
Add Repo
{
setCreateTaskOpen(false);
setCreateOnBranch(null);
}}
overrides={modalOverrides}
>
Create Task
{
setCreateTaskOpen(false);
setCreateOnBranch(null);
}}
>
Cancel
{
setCreateError(null);
void createTask.mutateAsync();
}}
data-testid="task-create-submit"
>
{createTask.isPending ? "Creating..." : "Create Task"}
{
setReparentBranchName(null);
setReparentParentBranch("");
}}
overrides={modalOverrides}
>
Reparent Branch
{
setReparentBranchName(null);
setReparentParentBranch("");
}}
>
Cancel
Reparent
{showDevPanel ? : null}
);
}