mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-19 06:01:21 +00:00
Complete Foundry refactor checklist
This commit is contained in:
parent
40bed3b0a1
commit
13fc9cb318
91 changed files with 5091 additions and 4108 deletions
|
|
@ -6,7 +6,6 @@ import { subscriptionManager } from "../lib/subscription";
|
|||
import type {
|
||||
FoundryAppSnapshot,
|
||||
FoundryOrganization,
|
||||
TaskStatus,
|
||||
TaskWorkspaceSnapshot,
|
||||
WorkspaceSandboxSummary,
|
||||
WorkspaceSessionSummary,
|
||||
|
|
@ -28,8 +27,6 @@ export interface DevPanelFocusedTask {
|
|||
repoId: string;
|
||||
title: string | null;
|
||||
status: WorkspaceTaskStatus;
|
||||
runtimeStatus?: TaskStatus | null;
|
||||
statusMessage?: string | null;
|
||||
branch?: string | null;
|
||||
activeSandboxId?: string | null;
|
||||
activeSessionId?: string | null;
|
||||
|
|
@ -80,7 +77,7 @@ function timeAgo(ts: number | null): string {
|
|||
}
|
||||
|
||||
function statusColor(status: string, t: ReturnType<typeof useFoundryTokens>): string {
|
||||
if (status === "new" || status.startsWith("init_") || status.startsWith("archive_") || status.startsWith("kill_") || status.startsWith("pending_")) {
|
||||
if (status.startsWith("init_") || status.startsWith("archive_") || status.startsWith("kill_") || status.startsWith("pending_")) {
|
||||
return t.statusWarning;
|
||||
}
|
||||
switch (status) {
|
||||
|
|
@ -159,14 +156,16 @@ export const DevPanel = memo(function DevPanel({ organizationId, snapshot, organ
|
|||
}, [now]);
|
||||
|
||||
const appState = useSubscription(subscriptionManager, "app", {});
|
||||
const organizationState = useSubscription(subscriptionManager, "organization", { organizationId });
|
||||
const appSnapshot: FoundryAppSnapshot | null = appState.data ?? null;
|
||||
const liveGithub = organizationState.data?.github ?? organization?.github ?? null;
|
||||
|
||||
const repos = snapshot.repos ?? [];
|
||||
const tasks = snapshot.tasks ?? [];
|
||||
const prCount = tasks.filter((task) => task.pullRequest != null).length;
|
||||
const focusedTaskStatus = focusedTask?.runtimeStatus ?? focusedTask?.status ?? null;
|
||||
const focusedTaskState = describeTaskState(focusedTaskStatus, focusedTask?.statusMessage ?? null);
|
||||
const lastWebhookAt = organization?.github.lastWebhookAt ?? null;
|
||||
const focusedTaskStatus = focusedTask?.status ?? null;
|
||||
const focusedTaskState = describeTaskState(focusedTaskStatus);
|
||||
const lastWebhookAt = liveGithub?.lastWebhookAt ?? null;
|
||||
const hasRecentWebhook = lastWebhookAt != null && now - lastWebhookAt < 5 * 60_000;
|
||||
const totalOrgs = appSnapshot?.organizations.length ?? 0;
|
||||
const authStatus = appSnapshot?.auth.status ?? "unknown";
|
||||
|
|
@ -442,7 +441,7 @@ export const DevPanel = memo(function DevPanel({ organizationId, snapshot, organ
|
|||
|
||||
{/* GitHub */}
|
||||
<Section label="GitHub" t={t} css={css}>
|
||||
{organization ? (
|
||||
{liveGithub ? (
|
||||
<div className={css({ display: "flex", flexDirection: "column", gap: "3px", fontSize: "10px" })}>
|
||||
<div className={css({ display: "flex", alignItems: "center", gap: "6px" })}>
|
||||
<span
|
||||
|
|
@ -450,13 +449,13 @@ export const DevPanel = memo(function DevPanel({ organizationId, snapshot, organ
|
|||
width: "5px",
|
||||
height: "5px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: installStatusColor(organization.github.installationStatus, t),
|
||||
backgroundColor: installStatusColor(liveGithub.installationStatus, t),
|
||||
flexShrink: 0,
|
||||
})}
|
||||
/>
|
||||
<span className={css({ color: t.textPrimary, flex: 1 })}>App Install</span>
|
||||
<span className={`${mono} ${css({ color: installStatusColor(organization.github.installationStatus, t) })}`}>
|
||||
{organization.github.installationStatus.replace(/_/g, " ")}
|
||||
<span className={`${mono} ${css({ color: installStatusColor(liveGithub.installationStatus, t) })}`}>
|
||||
{liveGithub.installationStatus.replace(/_/g, " ")}
|
||||
</span>
|
||||
</div>
|
||||
<div className={css({ display: "flex", alignItems: "center", gap: "6px" })}>
|
||||
|
|
@ -465,14 +464,14 @@ export const DevPanel = memo(function DevPanel({ organizationId, snapshot, organ
|
|||
width: "5px",
|
||||
height: "5px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: syncStatusColor(organization.github.syncStatus, t),
|
||||
backgroundColor: syncStatusColor(liveGithub.syncStatus, t),
|
||||
flexShrink: 0,
|
||||
})}
|
||||
/>
|
||||
<span className={css({ color: t.textPrimary, flex: 1 })}>Sync</span>
|
||||
<span className={`${mono} ${css({ color: syncStatusColor(organization.github.syncStatus, t) })}`}>{organization.github.syncStatus}</span>
|
||||
{organization.github.lastSyncAt != null && (
|
||||
<span className={`${mono} ${css({ color: t.textTertiary })}`}>{timeAgo(organization.github.lastSyncAt)}</span>
|
||||
<span className={`${mono} ${css({ color: syncStatusColor(liveGithub.syncStatus, t) })}`}>{liveGithub.syncStatus}</span>
|
||||
{liveGithub.lastSyncAt != null && (
|
||||
<span className={`${mono} ${css({ color: t.textTertiary })}`}>{timeAgo(liveGithub.lastSyncAt)}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={css({ display: "flex", alignItems: "center", gap: "6px" })}>
|
||||
|
|
@ -488,21 +487,27 @@ export const DevPanel = memo(function DevPanel({ organizationId, snapshot, organ
|
|||
<span className={css({ color: t.textPrimary, flex: 1 })}>Webhook</span>
|
||||
{lastWebhookAt != null ? (
|
||||
<span className={`${mono} ${css({ color: hasRecentWebhook ? t.textPrimary : t.textMuted })}`}>
|
||||
{organization.github.lastWebhookEvent} · {timeAgo(lastWebhookAt)}
|
||||
{liveGithub.lastWebhookEvent} · {timeAgo(lastWebhookAt)}
|
||||
</span>
|
||||
) : (
|
||||
<span className={`${mono} ${css({ color: t.statusWarning })}`}>never received</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={css({ display: "flex", gap: "10px", marginTop: "2px" })}>
|
||||
<Stat label="imported" value={organization.github.importedRepoCount} t={t} css={css} />
|
||||
<Stat label="catalog" value={organization.repoCatalog.length} t={t} css={css} />
|
||||
<Stat label="imported" value={liveGithub.importedRepoCount} t={t} css={css} />
|
||||
<Stat label="catalog" value={organization?.repoCatalog.length ?? repos.length} t={t} css={css} />
|
||||
<Stat label="target" value={liveGithub.totalRepositoryCount} t={t} css={css} />
|
||||
</div>
|
||||
{organization.github.connectedAccount && (
|
||||
<div className={`${mono} ${css({ color: t.textMuted, marginTop: "1px" })}`}>@{organization.github.connectedAccount}</div>
|
||||
{liveGithub.connectedAccount && (
|
||||
<div className={`${mono} ${css({ color: t.textMuted, marginTop: "1px" })}`}>@{liveGithub.connectedAccount}</div>
|
||||
)}
|
||||
{organization.github.lastSyncLabel && (
|
||||
<div className={`${mono} ${css({ color: t.textMuted })}`}>last sync: {organization.github.lastSyncLabel}</div>
|
||||
{liveGithub.lastSyncLabel && (
|
||||
<div className={`${mono} ${css({ color: t.textMuted })}`}>last sync: {liveGithub.lastSyncLabel}</div>
|
||||
)}
|
||||
{liveGithub.syncPhase && (
|
||||
<div className={`${mono} ${css({ color: t.textTertiary })}`}>
|
||||
phase: {liveGithub.syncPhase.replace(/^syncing_/, "").replace(/_/g, " ")} ({liveGithub.processedRepositoryCount}/{liveGithub.totalRepositoryCount})
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type PointerEvent as ReactPointerEvent } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { useStyletron } from "baseui";
|
||||
import {
|
||||
DEFAULT_WORKSPACE_MODEL_GROUPS,
|
||||
DEFAULT_WORKSPACE_MODEL_ID,
|
||||
createErrorContext,
|
||||
type FoundryOrganization,
|
||||
type TaskWorkspaceSnapshot,
|
||||
type WorkspaceOpenPrSummary,
|
||||
type WorkspaceModelGroup,
|
||||
type WorkspaceSessionSummary,
|
||||
type WorkspaceTaskDetail,
|
||||
type WorkspaceTaskSummary,
|
||||
|
|
@ -77,29 +80,35 @@ function sanitizeActiveSessionId(task: Task, sessionId: string | null | undefine
|
|||
return openDiffs.length > 0 ? diffTabId(openDiffs[openDiffs.length - 1]!) : lastAgentSessionId;
|
||||
}
|
||||
|
||||
function githubInstallationWarningTitle(organization: FoundryOrganization): string {
|
||||
return organization.github.installationStatus === "install_required" ? "GitHub App not installed" : "GitHub App needs reconnection";
|
||||
type GithubStatusView = Pick<FoundryOrganization["github"], "connectedAccount" | "installationStatus" | "syncStatus" | "importedRepoCount" | "lastSyncLabel"> & {
|
||||
syncPhase?: string | null;
|
||||
processedRepositoryCount?: number;
|
||||
totalRepositoryCount?: number;
|
||||
};
|
||||
|
||||
function githubInstallationWarningTitle(github: GithubStatusView): string {
|
||||
return github.installationStatus === "install_required" ? "GitHub App not installed" : "GitHub App needs reconnection";
|
||||
}
|
||||
|
||||
function githubInstallationWarningDetail(organization: FoundryOrganization): string {
|
||||
const statusDetail = organization.github.lastSyncLabel.trim();
|
||||
function githubInstallationWarningDetail(github: GithubStatusView): string {
|
||||
const statusDetail = github.lastSyncLabel.trim();
|
||||
const requirementDetail =
|
||||
organization.github.installationStatus === "install_required"
|
||||
github.installationStatus === "install_required"
|
||||
? "Webhooks are required for Foundry to function. Repo sync and PR updates will not work until the GitHub App is installed for this organization."
|
||||
: "Webhook delivery is unavailable. Repo sync and PR updates will not work until the GitHub App is reconnected.";
|
||||
return statusDetail ? `${requirementDetail} ${statusDetail}.` : requirementDetail;
|
||||
}
|
||||
|
||||
function GithubInstallationWarning({
|
||||
organization,
|
||||
github,
|
||||
css,
|
||||
t,
|
||||
}: {
|
||||
organization: FoundryOrganization;
|
||||
github: GithubStatusView;
|
||||
css: ReturnType<typeof useStyletron>[0];
|
||||
t: ReturnType<typeof useFoundryTokens>;
|
||||
}) {
|
||||
if (organization.github.installationStatus === "connected") {
|
||||
if (github.installationStatus === "connected") {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -123,8 +132,8 @@ function GithubInstallationWarning({
|
|||
>
|
||||
<CircleAlert size={15} color={t.statusError} />
|
||||
<div className={css({ display: "flex", flexDirection: "column", gap: "3px" })}>
|
||||
<div className={css({ fontSize: "11px", fontWeight: 600, color: t.textPrimary })}>{githubInstallationWarningTitle(organization)}</div>
|
||||
<div className={css({ fontSize: "11px", lineHeight: 1.45, color: t.textMuted })}>{githubInstallationWarningDetail(organization)}</div>
|
||||
<div className={css({ fontSize: "11px", fontWeight: 600, color: t.textPrimary })}>{githubInstallationWarningTitle(github)}</div>
|
||||
<div className={css({ fontSize: "11px", lineHeight: 1.45, color: t.textMuted })}>{githubInstallationWarningDetail(github)}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -164,13 +173,12 @@ function toTaskModel(
|
|||
id: summary.id,
|
||||
repoId: summary.repoId,
|
||||
title: detail?.title ?? summary.title,
|
||||
status: detail?.runtimeStatus ?? detail?.status ?? summary.status,
|
||||
runtimeStatus: detail?.runtimeStatus,
|
||||
statusMessage: detail?.statusMessage ?? null,
|
||||
status: detail?.status ?? summary.status,
|
||||
repoName: detail?.repoName ?? summary.repoName,
|
||||
updatedAtMs: detail?.updatedAtMs ?? summary.updatedAtMs,
|
||||
branch: detail?.branch ?? summary.branch,
|
||||
pullRequest: detail?.pullRequest ?? summary.pullRequest,
|
||||
activeSessionId: detail?.activeSessionId ?? summary.activeSessionId ?? null,
|
||||
sessions: sessions.map((session) => toSessionModel(session, sessionCache?.get(session.id))),
|
||||
fileChanges: detail?.fileChanges ?? [],
|
||||
diffs: detail?.diffs ?? {},
|
||||
|
|
@ -180,40 +188,6 @@ function toTaskModel(
|
|||
};
|
||||
}
|
||||
|
||||
const OPEN_PR_TASK_PREFIX = "pr:";
|
||||
|
||||
function openPrTaskId(prId: string): string {
|
||||
return `${OPEN_PR_TASK_PREFIX}${prId}`;
|
||||
}
|
||||
|
||||
function isOpenPrTaskId(taskId: string): boolean {
|
||||
return taskId.startsWith(OPEN_PR_TASK_PREFIX);
|
||||
}
|
||||
|
||||
function toOpenPrTaskModel(pullRequest: WorkspaceOpenPrSummary): Task {
|
||||
return {
|
||||
id: openPrTaskId(pullRequest.prId),
|
||||
repoId: pullRequest.repoId,
|
||||
title: pullRequest.title,
|
||||
status: "new",
|
||||
runtimeStatus: undefined,
|
||||
statusMessage: pullRequest.authorLogin ? `@${pullRequest.authorLogin}` : null,
|
||||
repoName: pullRequest.repoFullName,
|
||||
updatedAtMs: pullRequest.updatedAtMs,
|
||||
branch: pullRequest.headRefName,
|
||||
pullRequest: {
|
||||
number: pullRequest.number,
|
||||
status: pullRequest.isDraft ? "draft" : "ready",
|
||||
},
|
||||
sessions: [],
|
||||
fileChanges: [],
|
||||
diffs: {},
|
||||
fileTree: [],
|
||||
minutesUsed: 0,
|
||||
activeSandboxId: null,
|
||||
};
|
||||
}
|
||||
|
||||
function sessionStateMessage(tab: Task["sessions"][number] | null | undefined): string | null {
|
||||
if (!tab) {
|
||||
return null;
|
||||
|
|
@ -258,15 +232,14 @@ interface WorkspaceActions {
|
|||
updateDraft(input: { repoId: string; taskId: string; sessionId: string; text: string; attachments: LineAttachment[] }): Promise<void>;
|
||||
sendMessage(input: { repoId: string; taskId: string; sessionId: string; text: string; attachments: LineAttachment[] }): Promise<void>;
|
||||
stopAgent(input: { repoId: string; taskId: string; sessionId: string }): Promise<void>;
|
||||
selectSession(input: { repoId: string; taskId: string; sessionId: string }): Promise<void>;
|
||||
setSessionUnread(input: { repoId: string; taskId: string; sessionId: string; unread: boolean }): Promise<void>;
|
||||
renameSession(input: { repoId: string; taskId: string; sessionId: string; title: string }): Promise<void>;
|
||||
closeSession(input: { repoId: string; taskId: string; sessionId: string }): Promise<void>;
|
||||
addSession(input: { repoId: string; taskId: string; model?: string }): Promise<{ sessionId: string }>;
|
||||
changeModel(input: { repoId: string; taskId: string; sessionId: string; model: ModelId }): Promise<void>;
|
||||
adminReloadGithubOrganization(): Promise<void>;
|
||||
adminReloadGithubPullRequests(): Promise<void>;
|
||||
adminReloadGithubRepository(repoId: string): Promise<void>;
|
||||
adminReloadGithubPullRequest(repoId: string, prNumber: number): Promise<void>;
|
||||
}
|
||||
|
||||
const TranscriptPanel = memo(function TranscriptPanel({
|
||||
|
|
@ -287,6 +260,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
|||
rightSidebarCollapsed,
|
||||
onToggleRightSidebar,
|
||||
selectedSessionHydrating = false,
|
||||
modelGroups,
|
||||
onNavigateToUsage,
|
||||
}: {
|
||||
taskWorkspaceClient: WorkspaceActions;
|
||||
|
|
@ -306,13 +280,14 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
|||
rightSidebarCollapsed?: boolean;
|
||||
onToggleRightSidebar?: () => void;
|
||||
selectedSessionHydrating?: boolean;
|
||||
modelGroups: WorkspaceModelGroup[];
|
||||
onNavigateToUsage?: () => void;
|
||||
}) {
|
||||
const t = useFoundryTokens();
|
||||
const appSnapshot = useMockAppSnapshot();
|
||||
const appClient = useMockAppClient();
|
||||
const currentUser = activeMockUser(appSnapshot);
|
||||
const defaultModel = currentUser?.defaultModel ?? "claude-sonnet-4";
|
||||
const defaultModel = currentUser?.defaultModel ?? DEFAULT_WORKSPACE_MODEL_ID;
|
||||
const [editingField, setEditingField] = useState<"title" | null>(null);
|
||||
const [editValue, setEditValue] = useState("");
|
||||
const [editingSessionId, setEditingSessionId] = useState<string | null>(null);
|
||||
|
|
@ -335,9 +310,8 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
|||
const isTerminal = task.status === "archived";
|
||||
const historyEvents = useMemo(() => buildHistoryEvents(task.sessions), [task.sessions]);
|
||||
const activeMessages = useMemo(() => buildDisplayMessages(activeAgentSession), [activeAgentSession]);
|
||||
const taskRuntimeStatus = task.runtimeStatus ?? task.status;
|
||||
const taskState = describeTaskState(taskRuntimeStatus, task.statusMessage ?? null);
|
||||
const taskProvisioning = isProvisioningTaskStatus(taskRuntimeStatus);
|
||||
const taskState = describeTaskState(task.status);
|
||||
const taskProvisioning = isProvisioningTaskStatus(task.status);
|
||||
const taskProvisioningMessage = taskState.detail;
|
||||
const activeSessionMessage = sessionStateMessage(activeAgentSession);
|
||||
const showPendingSessionState =
|
||||
|
|
@ -562,6 +536,11 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
|||
|
||||
if (!isDiffTab(sessionId)) {
|
||||
onSetLastAgentSessionId(sessionId);
|
||||
void taskWorkspaceClient.selectSession({
|
||||
repoId: task.repoId,
|
||||
taskId: task.id,
|
||||
sessionId,
|
||||
});
|
||||
const session = task.sessions.find((candidate) => candidate.id === sessionId);
|
||||
if (session?.unread) {
|
||||
void taskWorkspaceClient.setSessionUnread({
|
||||
|
|
@ -574,7 +553,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
|||
onSyncRouteSession(task.id, sessionId);
|
||||
}
|
||||
},
|
||||
[task.id, task.sessions, onSetActiveSessionId, onSetLastAgentSessionId, onSyncRouteSession],
|
||||
[task.id, task.repoId, task.sessions, onSetActiveSessionId, onSetLastAgentSessionId, onSyncRouteSession],
|
||||
);
|
||||
|
||||
const setSessionUnread = useCallback(
|
||||
|
|
@ -963,6 +942,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
|||
textareaRef={textareaRef}
|
||||
placeholder={!promptSession.created ? "Describe your task..." : "Send a message..."}
|
||||
attachments={attachments}
|
||||
modelGroups={modelGroups}
|
||||
defaultModel={defaultModel}
|
||||
model={promptSession.model}
|
||||
isRunning={promptSession.status === "running"}
|
||||
|
|
@ -1298,30 +1278,20 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
|||
updateDraft: (input) => backendClient.updateWorkspaceDraft(organizationId, input),
|
||||
sendMessage: (input) => backendClient.sendWorkspaceMessage(organizationId, input),
|
||||
stopAgent: (input) => backendClient.stopWorkspaceSession(organizationId, input),
|
||||
selectSession: (input) => backendClient.selectWorkspaceSession(organizationId, input),
|
||||
setSessionUnread: (input) => backendClient.setWorkspaceSessionUnread(organizationId, input),
|
||||
renameSession: (input) => backendClient.renameWorkspaceSession(organizationId, input),
|
||||
closeSession: (input) => backendClient.closeWorkspaceSession(organizationId, input),
|
||||
addSession: (input) => backendClient.createWorkspaceSession(organizationId, input),
|
||||
changeModel: (input) => backendClient.changeWorkspaceModel(organizationId, input),
|
||||
adminReloadGithubOrganization: () => backendClient.adminReloadGithubOrganization(organizationId),
|
||||
adminReloadGithubPullRequests: () => backendClient.adminReloadGithubPullRequests(organizationId),
|
||||
adminReloadGithubRepository: (repoId) => backendClient.adminReloadGithubRepository(organizationId, repoId),
|
||||
adminReloadGithubPullRequest: (repoId, prNumber) => backendClient.adminReloadGithubPullRequest(organizationId, repoId, prNumber),
|
||||
}),
|
||||
[organizationId],
|
||||
);
|
||||
const organizationState = useSubscription(subscriptionManager, "organization", { organizationId });
|
||||
const organizationRepos = organizationState.data?.repos ?? [];
|
||||
const taskSummaries = organizationState.data?.taskSummaries ?? [];
|
||||
const openPullRequests = organizationState.data?.openPullRequests ?? [];
|
||||
const openPullRequestsByTaskId = useMemo(
|
||||
() => new Map(openPullRequests.map((pullRequest) => [openPrTaskId(pullRequest.prId), pullRequest])),
|
||||
[openPullRequests],
|
||||
);
|
||||
const selectedOpenPullRequest = useMemo(
|
||||
() => (selectedTaskId ? (openPullRequestsByTaskId.get(selectedTaskId) ?? null) : null),
|
||||
[openPullRequestsByTaskId, selectedTaskId],
|
||||
);
|
||||
const selectedTaskSummary = useMemo(
|
||||
() => taskSummaries.find((task) => task.id === selectedTaskId) ?? taskSummaries[0] ?? null,
|
||||
[selectedTaskId, taskSummaries],
|
||||
|
|
@ -1365,6 +1335,20 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
|||
: null,
|
||||
);
|
||||
const hasSandbox = Boolean(activeSandbox) && sandboxState.status !== "error";
|
||||
const modelGroupsQuery = useQuery({
|
||||
queryKey: ["mock-layout", "workspace-model-groups", organizationId, activeSandbox?.sandboxProviderId ?? "", activeSandbox?.sandboxId ?? ""],
|
||||
enabled: Boolean(activeSandbox?.sandboxId),
|
||||
staleTime: 30_000,
|
||||
refetchOnWindowFocus: false,
|
||||
queryFn: async () => {
|
||||
if (!activeSandbox) {
|
||||
throw new Error("Cannot load workspace model groups without an active sandbox.");
|
||||
}
|
||||
|
||||
return await backendClient.getSandboxWorkspaceModelGroups(organizationId, activeSandbox.sandboxProviderId, activeSandbox.sandboxId);
|
||||
},
|
||||
});
|
||||
const modelGroups = modelGroupsQuery.data && modelGroupsQuery.data.length > 0 ? modelGroupsQuery.data : DEFAULT_WORKSPACE_MODEL_GROUPS;
|
||||
const tasks = useMemo(() => {
|
||||
const sessionCache = new Map<string, { draft: Task["sessions"][number]["draft"]; transcript: Task["sessions"][number]["transcript"] }>();
|
||||
if (selectedTaskSummary && taskState.data) {
|
||||
|
|
@ -1389,12 +1373,13 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
|||
const hydratedTasks = taskSummaries.map((summary) =>
|
||||
summary.id === selectedTaskSummary?.id ? toTaskModel(summary, taskState.data, sessionCache) : toTaskModel(summary),
|
||||
);
|
||||
const openPrTasks = openPullRequests.map((pullRequest) => toOpenPrTaskModel(pullRequest));
|
||||
return [...hydratedTasks, ...openPrTasks].sort((left, right) => right.updatedAtMs - left.updatedAtMs);
|
||||
}, [openPullRequests, selectedTaskSummary, selectedSessionId, sessionState.data, taskState.data, taskSummaries, organizationId]);
|
||||
return hydratedTasks.sort((left, right) => right.updatedAtMs - left.updatedAtMs);
|
||||
}, [selectedTaskSummary, selectedSessionId, sessionState.data, taskState.data, taskSummaries, organizationId]);
|
||||
const rawRepositories = useMemo(() => groupRepositories(organizationRepos, tasks), [tasks, organizationRepos]);
|
||||
const appSnapshot = useMockAppSnapshot();
|
||||
const currentUser = activeMockUser(appSnapshot);
|
||||
const activeOrg = activeMockOrganization(appSnapshot);
|
||||
const liveGithub = organizationState.data?.github ?? activeOrg?.github ?? null;
|
||||
const navigateToUsage = useCallback(() => {
|
||||
if (activeOrg) {
|
||||
void navigate({ to: "/organizations/$organizationId/billing" as never, params: { organizationId: activeOrg.id } as never });
|
||||
|
|
@ -1419,11 +1404,9 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
|||
const leftWidthRef = useRef(leftWidth);
|
||||
const rightWidthRef = useRef(rightWidth);
|
||||
const autoCreatingSessionForTaskRef = useRef<Set<string>>(new Set());
|
||||
const resolvingOpenPullRequestsRef = useRef<Set<string>>(new Set());
|
||||
const [leftSidebarOpen, setLeftSidebarOpen] = useState(true);
|
||||
const [rightSidebarOpen, setRightSidebarOpen] = useState(true);
|
||||
const [leftSidebarPeeking, setLeftSidebarPeeking] = useState(false);
|
||||
const [materializingOpenPrId, setMaterializingOpenPrId] = useState<string | null>(null);
|
||||
const showDevPanel = useDevPanel();
|
||||
const peekTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
|
|
@ -1490,80 +1473,17 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
|||
}, []);
|
||||
|
||||
const activeTask = useMemo(() => {
|
||||
const realTasks = tasks.filter((task) => !isOpenPrTaskId(task.id));
|
||||
if (selectedOpenPullRequest) {
|
||||
return null;
|
||||
}
|
||||
if (selectedTaskId) {
|
||||
return realTasks.find((task) => task.id === selectedTaskId) ?? realTasks[0] ?? null;
|
||||
return tasks.find((task) => task.id === selectedTaskId) ?? tasks[0] ?? null;
|
||||
}
|
||||
return realTasks[0] ?? null;
|
||||
}, [selectedOpenPullRequest, selectedTaskId, tasks]);
|
||||
|
||||
const materializeOpenPullRequest = useCallback(
|
||||
async (pullRequest: WorkspaceOpenPrSummary) => {
|
||||
if (resolvingOpenPullRequestsRef.current.has(pullRequest.prId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
resolvingOpenPullRequestsRef.current.add(pullRequest.prId);
|
||||
setMaterializingOpenPrId(pullRequest.prId);
|
||||
|
||||
try {
|
||||
const { taskId, sessionId } = await taskWorkspaceClient.createTask({
|
||||
repoId: pullRequest.repoId,
|
||||
task: `Continue work on GitHub PR #${pullRequest.number}: ${pullRequest.title}`,
|
||||
model: "gpt-5.3-codex",
|
||||
title: pullRequest.title,
|
||||
onBranch: pullRequest.headRefName,
|
||||
});
|
||||
await navigate({
|
||||
to: "/organizations/$organizationId/tasks/$taskId",
|
||||
params: {
|
||||
organizationId,
|
||||
taskId,
|
||||
},
|
||||
search: { sessionId: sessionId ?? undefined },
|
||||
replace: true,
|
||||
});
|
||||
} catch (error) {
|
||||
setMaterializingOpenPrId((current) => (current === pullRequest.prId ? null : current));
|
||||
resolvingOpenPullRequestsRef.current.delete(pullRequest.prId);
|
||||
logger.error(
|
||||
{
|
||||
prId: pullRequest.prId,
|
||||
repoId: pullRequest.repoId,
|
||||
branchName: pullRequest.headRefName,
|
||||
...createErrorContext(error),
|
||||
},
|
||||
"failed_to_materialize_open_pull_request_task",
|
||||
);
|
||||
}
|
||||
},
|
||||
[navigate, taskWorkspaceClient, organizationId],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedOpenPullRequest) {
|
||||
if (materializingOpenPrId) {
|
||||
resolvingOpenPullRequestsRef.current.delete(materializingOpenPrId);
|
||||
}
|
||||
setMaterializingOpenPrId(null);
|
||||
return;
|
||||
}
|
||||
|
||||
void materializeOpenPullRequest(selectedOpenPullRequest);
|
||||
}, [materializeOpenPullRequest, materializingOpenPrId, selectedOpenPullRequest]);
|
||||
return tasks[0] ?? null;
|
||||
}, [selectedTaskId, tasks]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTask) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedOpenPullRequest || materializingOpenPrId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fallbackTaskId = tasks[0]?.id;
|
||||
if (!fallbackTaskId) {
|
||||
return;
|
||||
|
|
@ -1580,11 +1500,13 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
|||
search: { sessionId: fallbackTask?.sessions[0]?.id ?? undefined },
|
||||
replace: true,
|
||||
});
|
||||
}, [activeTask, materializingOpenPrId, navigate, selectedOpenPullRequest, tasks, organizationId]);
|
||||
}, [activeTask, navigate, tasks, organizationId]);
|
||||
|
||||
const openDiffs = activeTask ? sanitizeOpenDiffs(activeTask, openDiffsByTask[activeTask.id]) : [];
|
||||
const lastAgentSessionId = activeTask ? sanitizeLastAgentSessionId(activeTask, lastAgentSessionIdByTask[activeTask.id]) : null;
|
||||
const activeSessionId = activeTask ? sanitizeActiveSessionId(activeTask, activeSessionIdByTask[activeTask.id], openDiffs, lastAgentSessionId) : null;
|
||||
const activeSessionId = activeTask
|
||||
? sanitizeActiveSessionId(activeTask, activeSessionIdByTask[activeTask.id] ?? activeTask.activeSessionId ?? null, openDiffs, lastAgentSessionId)
|
||||
: null;
|
||||
const selectedSessionHydrating = Boolean(
|
||||
selectedSessionId && activeSessionId === selectedSessionId && sessionState.status === "loading" && !sessionState.data,
|
||||
);
|
||||
|
|
@ -1697,7 +1619,7 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
|||
const { taskId, sessionId } = await taskWorkspaceClient.createTask({
|
||||
repoId,
|
||||
task: options?.task ?? "New task",
|
||||
model: "gpt-5.3-codex",
|
||||
model: currentUser?.defaultModel ?? DEFAULT_WORKSPACE_MODEL_ID,
|
||||
title: options?.title ?? "New task",
|
||||
...(options?.branch ? { branch: options.branch } : {}),
|
||||
...(options?.onBranch ? { onBranch: options.onBranch } : {}),
|
||||
|
|
@ -1712,7 +1634,7 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
|||
});
|
||||
})();
|
||||
},
|
||||
[navigate, selectedNewTaskRepoId, taskWorkspaceClient, organizationId],
|
||||
[currentUser?.defaultModel, navigate, selectedNewTaskRepoId, taskWorkspaceClient, organizationId],
|
||||
);
|
||||
|
||||
const openDiffTab = useCallback(
|
||||
|
|
@ -1741,14 +1663,6 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
|||
|
||||
const selectTask = useCallback(
|
||||
(id: string) => {
|
||||
if (isOpenPrTaskId(id)) {
|
||||
const pullRequest = openPullRequestsByTaskId.get(id);
|
||||
if (!pullRequest) {
|
||||
return;
|
||||
}
|
||||
void materializeOpenPullRequest(pullRequest);
|
||||
return;
|
||||
}
|
||||
const task = tasks.find((candidate) => candidate.id === id) ?? null;
|
||||
void navigate({
|
||||
to: "/organizations/$organizationId/tasks/$taskId",
|
||||
|
|
@ -1759,7 +1673,7 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
|||
search: { sessionId: task?.sessions[0]?.id ?? undefined },
|
||||
});
|
||||
},
|
||||
[materializeOpenPullRequest, navigate, openPullRequestsByTaskId, tasks, organizationId],
|
||||
[navigate, tasks, organizationId],
|
||||
);
|
||||
|
||||
const markTaskUnread = useCallback(
|
||||
|
|
@ -1904,7 +1818,6 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
|||
};
|
||||
|
||||
if (!activeTask) {
|
||||
const isMaterializingSelectedOpenPr = Boolean(selectedOpenPullRequest) || materializingOpenPrId != null;
|
||||
return (
|
||||
<>
|
||||
{dragRegion}
|
||||
|
|
@ -1935,9 +1848,7 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
|||
taskOrderByRepository={taskOrderByRepository}
|
||||
onReorderTasks={reorderTasks}
|
||||
onReloadOrganization={() => void taskWorkspaceClient.adminReloadGithubOrganization()}
|
||||
onReloadPullRequests={() => void taskWorkspaceClient.adminReloadGithubPullRequests()}
|
||||
onReloadRepository={(repoId) => void taskWorkspaceClient.adminReloadGithubRepository(repoId)}
|
||||
onReloadPullRequest={(repoId, prNumber) => void taskWorkspaceClient.adminReloadGithubPullRequest(repoId, prNumber)}
|
||||
onToggleSidebar={() => setLeftSidebarOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -1979,7 +1890,7 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
|||
gap: "12px",
|
||||
}}
|
||||
>
|
||||
{activeOrg?.github.syncStatus === "syncing" || activeOrg?.github.syncStatus === "pending" ? (
|
||||
{liveGithub?.syncStatus === "syncing" || liveGithub?.syncStatus === "pending" ? (
|
||||
<>
|
||||
<div
|
||||
className={css({
|
||||
|
|
@ -2000,19 +1911,18 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
|||
/>
|
||||
<h2 style={{ margin: 0, fontSize: "20px", fontWeight: 600 }}>Syncing with GitHub</h2>
|
||||
<p style={{ margin: 0, opacity: 0.75 }}>
|
||||
Importing repos from @{activeOrg.github.connectedAccount || "GitHub"}...
|
||||
{activeOrg.github.importedRepoCount > 0 && <> {activeOrg.github.importedRepoCount} repos imported so far.</>}
|
||||
{liveGithub.lastSyncLabel || `Importing repos from @${liveGithub.connectedAccount || "GitHub"}...`}
|
||||
{liveGithub.totalRepositoryCount > 0 && (
|
||||
<>
|
||||
{" "}
|
||||
{liveGithub.syncPhase === "syncing_repositories"
|
||||
? `${liveGithub.importedRepoCount} of ${liveGithub.totalRepositoryCount} repos imported so far.`
|
||||
: `${liveGithub.processedRepositoryCount} of ${liveGithub.totalRepositoryCount} repos processed in ${liveGithub.syncPhase?.replace(/^syncing_/, "").replace(/_/g, " ") ?? "sync"}.`}
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
) : isMaterializingSelectedOpenPr && selectedOpenPullRequest ? (
|
||||
<>
|
||||
<SpinnerDot />
|
||||
<h2 style={{ margin: 0, fontSize: "20px", fontWeight: 600 }}>Creating task from pull request</h2>
|
||||
<p style={{ margin: 0, opacity: 0.75 }}>
|
||||
Preparing a task for <strong>{selectedOpenPullRequest.title}</strong> on <strong>{selectedOpenPullRequest.headRefName}</strong>.
|
||||
</p>
|
||||
</>
|
||||
) : activeOrg?.github.syncStatus === "error" ? (
|
||||
) : liveGithub?.syncStatus === "error" ? (
|
||||
<>
|
||||
<h2 style={{ margin: 0, fontSize: "20px", fontWeight: 600, color: t.statusError }}>GitHub sync failed</h2>
|
||||
<p style={{ margin: 0, opacity: 0.75 }}>There was a problem syncing repos from GitHub. Check the dev panel for details.</p>
|
||||
|
|
@ -2066,7 +1976,7 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
|||
</div>
|
||||
</div>
|
||||
</Shell>
|
||||
{activeOrg && <GithubInstallationWarning organization={activeOrg} css={css} t={t} />}
|
||||
{liveGithub && <GithubInstallationWarning github={liveGithub} css={css} t={t} />}
|
||||
{showDevPanel && (
|
||||
<DevPanel
|
||||
organizationId={organizationId}
|
||||
|
|
@ -2109,9 +2019,7 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
|||
taskOrderByRepository={taskOrderByRepository}
|
||||
onReorderTasks={reorderTasks}
|
||||
onReloadOrganization={() => void taskWorkspaceClient.adminReloadGithubOrganization()}
|
||||
onReloadPullRequests={() => void taskWorkspaceClient.adminReloadGithubPullRequests()}
|
||||
onReloadRepository={(repoId) => void taskWorkspaceClient.adminReloadGithubRepository(repoId)}
|
||||
onReloadPullRequest={(repoId, prNumber) => void taskWorkspaceClient.adminReloadGithubPullRequest(repoId, prNumber)}
|
||||
onToggleSidebar={() => setLeftSidebarOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -2163,9 +2071,7 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
|||
taskOrderByRepository={taskOrderByRepository}
|
||||
onReorderTasks={reorderTasks}
|
||||
onReloadOrganization={() => void taskWorkspaceClient.adminReloadGithubOrganization()}
|
||||
onReloadPullRequests={() => void taskWorkspaceClient.adminReloadGithubPullRequests()}
|
||||
onReloadRepository={(repoId) => void taskWorkspaceClient.adminReloadGithubRepository(repoId)}
|
||||
onReloadPullRequest={(repoId, prNumber) => void taskWorkspaceClient.adminReloadGithubPullRequest(repoId, prNumber)}
|
||||
onToggleSidebar={() => {
|
||||
setLeftSidebarPeeking(false);
|
||||
setLeftSidebarOpen(true);
|
||||
|
|
@ -2181,6 +2087,7 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
|||
taskWorkspaceClient={taskWorkspaceClient}
|
||||
task={activeTask}
|
||||
hasSandbox={hasSandbox}
|
||||
modelGroups={modelGroups}
|
||||
activeSessionId={activeSessionId}
|
||||
lastAgentSessionId={lastAgentSessionId}
|
||||
openDiffs={openDiffs}
|
||||
|
|
@ -2233,7 +2140,7 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{activeOrg && <GithubInstallationWarning organization={activeOrg} css={css} t={t} />}
|
||||
{liveGithub && <GithubInstallationWarning github={liveGithub} css={css} t={t} />}
|
||||
{showDevPanel && (
|
||||
<DevPanel
|
||||
organizationId={organizationId}
|
||||
|
|
@ -2244,11 +2151,9 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
|||
repoId: activeTask.repoId,
|
||||
title: activeTask.title,
|
||||
status: activeTask.status,
|
||||
runtimeStatus: activeTask.runtimeStatus ?? null,
|
||||
statusMessage: activeTask.statusMessage ?? null,
|
||||
branch: activeTask.branch ?? null,
|
||||
activeSandboxId: activeTask.activeSandboxId ?? null,
|
||||
activeSessionId: selectedSessionId ?? activeTask.sessions[0]?.id ?? null,
|
||||
activeSessionId: activeTask.activeSessionId ?? selectedSessionId ?? activeTask.sessions[0]?.id ?? null,
|
||||
sandboxes: [],
|
||||
sessions:
|
||||
activeTask.sessions?.map((tab) => ({
|
||||
|
|
|
|||
|
|
@ -2,18 +2,21 @@ import { memo, useState } from "react";
|
|||
import { useStyletron } from "baseui";
|
||||
import { StatefulPopover, PLACEMENT } from "baseui/popover";
|
||||
import { ChevronUp, Star } from "lucide-react";
|
||||
import { workspaceModelLabel, type WorkspaceModelGroup } from "@sandbox-agent/foundry-shared";
|
||||
|
||||
import { useFoundryTokens } from "../../app/theme";
|
||||
import { AgentIcon } from "./ui";
|
||||
import { MODEL_GROUPS, modelLabel, providerAgent, type ModelId } from "./view-model";
|
||||
import { type ModelId } from "./view-model";
|
||||
|
||||
const ModelPickerContent = memo(function ModelPickerContent({
|
||||
groups,
|
||||
value,
|
||||
defaultModel,
|
||||
onChange,
|
||||
onSetDefault,
|
||||
close,
|
||||
}: {
|
||||
groups: WorkspaceModelGroup[];
|
||||
value: ModelId;
|
||||
defaultModel: ModelId;
|
||||
onChange: (id: ModelId) => void;
|
||||
|
|
@ -26,7 +29,7 @@ const ModelPickerContent = memo(function ModelPickerContent({
|
|||
|
||||
return (
|
||||
<div className={css({ minWidth: "220px", padding: "6px 0" })}>
|
||||
{MODEL_GROUPS.map((group) => (
|
||||
{groups.map((group) => (
|
||||
<div key={group.provider}>
|
||||
<div
|
||||
className={css({
|
||||
|
|
@ -44,7 +47,7 @@ const ModelPickerContent = memo(function ModelPickerContent({
|
|||
const isActive = model.id === value;
|
||||
const isDefault = model.id === defaultModel;
|
||||
const isHovered = model.id === hoveredId;
|
||||
const agent = providerAgent(group.provider);
|
||||
const agent = group.agentKind;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -94,11 +97,13 @@ const ModelPickerContent = memo(function ModelPickerContent({
|
|||
});
|
||||
|
||||
export const ModelPicker = memo(function ModelPicker({
|
||||
groups,
|
||||
value,
|
||||
defaultModel,
|
||||
onChange,
|
||||
onSetDefault,
|
||||
}: {
|
||||
groups: WorkspaceModelGroup[];
|
||||
value: ModelId;
|
||||
defaultModel: ModelId;
|
||||
onChange: (id: ModelId) => void;
|
||||
|
|
@ -137,7 +142,9 @@ export const ModelPicker = memo(function ModelPicker({
|
|||
},
|
||||
},
|
||||
}}
|
||||
content={({ close }) => <ModelPickerContent value={value} defaultModel={defaultModel} onChange={onChange} onSetDefault={onSetDefault} close={close} />}
|
||||
content={({ close }) => (
|
||||
<ModelPickerContent groups={groups} value={value} defaultModel={defaultModel} onChange={onChange} onSetDefault={onSetDefault} close={close} />
|
||||
)}
|
||||
>
|
||||
<div className={css({ display: "inline-flex" })}>
|
||||
<button
|
||||
|
|
@ -162,7 +169,7 @@ export const ModelPicker = memo(function ModelPicker({
|
|||
":hover": { color: t.textSecondary, backgroundColor: t.interactiveHover },
|
||||
})}
|
||||
>
|
||||
{modelLabel(value)}
|
||||
{workspaceModelLabel(value, groups)}
|
||||
{(isHovered || isOpen) && <ChevronUp size={11} />}
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { memo, type Ref } from "react";
|
|||
import { useStyletron } from "baseui";
|
||||
import { ChatComposer, type ChatComposerClassNames } from "@sandbox-agent/react";
|
||||
import { FileCode, SendHorizonal, Square, X } from "lucide-react";
|
||||
import { type WorkspaceModelGroup } from "@sandbox-agent/foundry-shared";
|
||||
|
||||
import { useFoundryTokens } from "../../app/theme";
|
||||
import { ModelPicker } from "./model-picker";
|
||||
|
|
@ -13,6 +14,7 @@ export const PromptComposer = memo(function PromptComposer({
|
|||
textareaRef,
|
||||
placeholder,
|
||||
attachments,
|
||||
modelGroups,
|
||||
defaultModel,
|
||||
model,
|
||||
isRunning,
|
||||
|
|
@ -27,6 +29,7 @@ export const PromptComposer = memo(function PromptComposer({
|
|||
textareaRef: Ref<HTMLTextAreaElement>;
|
||||
placeholder: string;
|
||||
attachments: LineAttachment[];
|
||||
modelGroups: WorkspaceModelGroup[];
|
||||
defaultModel: ModelId;
|
||||
model: ModelId;
|
||||
isRunning: boolean;
|
||||
|
|
@ -172,7 +175,7 @@ export const PromptComposer = memo(function PromptComposer({
|
|||
renderSubmitContent={() => (isRunning ? <Square size={16} style={{ display: "block" }} /> : <SendHorizonal size={16} style={{ display: "block" }} />)}
|
||||
renderFooter={() => (
|
||||
<div className={css({ padding: "0 10px 8px" })}>
|
||||
<ModelPicker value={model} defaultModel={defaultModel} onChange={onChangeModel} onSetDefault={onSetDefaultModel} />
|
||||
<ModelPicker groups={modelGroups} value={model} defaultModel={defaultModel} onChange={onChangeModel} onSetDefault={onSetDefaultModel} />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ export const RightSidebar = memo(function RightSidebar({
|
|||
});
|
||||
observer.observe(node);
|
||||
}, []);
|
||||
const pullRequestUrl = task.pullRequest != null ? `https://github.com/${task.repoName}/pull/${task.pullRequest.number}` : null;
|
||||
const pullRequestUrl = task.pullRequest?.url ?? null;
|
||||
|
||||
const copyFilePath = useCallback(async (path: string) => {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -54,10 +54,6 @@ function repositoryIconColor(label: string): string {
|
|||
return REPOSITORY_COLORS[Math.abs(hash) % REPOSITORY_COLORS.length]!;
|
||||
}
|
||||
|
||||
function isPullRequestSidebarItem(task: Task): boolean {
|
||||
return task.id.startsWith("pr:");
|
||||
}
|
||||
|
||||
export const Sidebar = memo(function Sidebar({
|
||||
repositories,
|
||||
newTaskRepos,
|
||||
|
|
@ -72,9 +68,7 @@ export const Sidebar = memo(function Sidebar({
|
|||
taskOrderByRepository,
|
||||
onReorderTasks,
|
||||
onReloadOrganization,
|
||||
onReloadPullRequests,
|
||||
onReloadRepository,
|
||||
onReloadPullRequest,
|
||||
onToggleSidebar,
|
||||
}: {
|
||||
repositories: RepositorySection[];
|
||||
|
|
@ -90,9 +84,7 @@ export const Sidebar = memo(function Sidebar({
|
|||
taskOrderByRepository: Record<string, string[]>;
|
||||
onReorderTasks: (repositoryId: string, fromIndex: number, toIndex: number) => void;
|
||||
onReloadOrganization: () => void;
|
||||
onReloadPullRequests: () => void;
|
||||
onReloadRepository: (repoId: string) => void;
|
||||
onReloadPullRequest: (repoId: string, prNumber: number) => void;
|
||||
onToggleSidebar?: () => void;
|
||||
}) {
|
||||
const [css] = useStyletron();
|
||||
|
|
@ -444,16 +436,6 @@ export const Sidebar = memo(function Sidebar({
|
|||
>
|
||||
Reload organization
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setHeaderMenuOpen(false);
|
||||
onReloadPullRequests();
|
||||
}}
|
||||
className={css(menuButtonStyle(false, t))}
|
||||
>
|
||||
Reload all PRs
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
<div
|
||||
|
|
@ -665,15 +647,12 @@ export const Sidebar = memo(function Sidebar({
|
|||
if (item.type === "task") {
|
||||
const { repository, task, taskIndex } = item;
|
||||
const isActive = task.id === activeId;
|
||||
const isPullRequestItem = isPullRequestSidebarItem(task);
|
||||
const isRunning = task.sessions.some((s) => s.status === "running");
|
||||
const isProvisioning =
|
||||
!isPullRequestItem &&
|
||||
((String(task.status).startsWith("init_") && task.status !== "init_complete") ||
|
||||
task.status === "new" ||
|
||||
task.sessions.some((s) => s.status === "pending_provision" || s.status === "pending_session_create"));
|
||||
(String(task.status).startsWith("init_") && task.status !== "init_complete") ||
|
||||
task.sessions.some((s) => s.status === "pending_provision" || s.status === "pending_session_create");
|
||||
const hasUnread = task.sessions.some((s) => s.unread);
|
||||
const isDraft = task.pullRequest == null || task.pullRequest.status === "draft";
|
||||
const isDraft = task.pullRequest?.isDraft ?? true;
|
||||
const totalAdded = task.fileChanges.reduce((sum, file) => sum + file.added, 0);
|
||||
const totalRemoved = task.fileChanges.reduce((sum, file) => sum + file.removed, 0);
|
||||
const hasDiffs = totalAdded > 0 || totalRemoved > 0;
|
||||
|
|
@ -718,17 +697,11 @@ export const Sidebar = memo(function Sidebar({
|
|||
<div
|
||||
onClick={() => onSelect(task.id)}
|
||||
onContextMenu={(event) => {
|
||||
if (isPullRequestItem && task.pullRequest) {
|
||||
contextMenu.open(event, [
|
||||
{ label: "Reload pull request", onClick: () => onReloadPullRequest(task.repoId, task.pullRequest!.number) },
|
||||
{ label: "Create task", onClick: () => onSelect(task.id) },
|
||||
]);
|
||||
return;
|
||||
}
|
||||
contextMenu.open(event, [
|
||||
const items = [
|
||||
{ label: "Rename task", onClick: () => onRenameTask(task.id) },
|
||||
{ label: "Mark as unread", onClick: () => onMarkUnread(task.id) },
|
||||
]);
|
||||
];
|
||||
contextMenu.open(event, items);
|
||||
}}
|
||||
className={css({
|
||||
padding: "8px 12px",
|
||||
|
|
@ -753,11 +726,7 @@ export const Sidebar = memo(function Sidebar({
|
|||
flexShrink: 0,
|
||||
})}
|
||||
>
|
||||
{isPullRequestItem ? (
|
||||
<GitPullRequestDraft size={13} color={isDraft ? t.accent : t.textSecondary} />
|
||||
) : (
|
||||
<TaskIndicator isRunning={isRunning} isProvisioning={isProvisioning} hasUnread={hasUnread} isDraft={isDraft} />
|
||||
)}
|
||||
<TaskIndicator isRunning={isRunning} isProvisioning={isProvisioning} hasUnread={hasUnread} isDraft={isDraft} />
|
||||
</div>
|
||||
<div className={css({ minWidth: 0, flex: 1, display: "flex", flexDirection: "column", gap: "1px" })}>
|
||||
<LabelSmall
|
||||
|
|
@ -773,18 +742,13 @@ export const Sidebar = memo(function Sidebar({
|
|||
>
|
||||
{task.title}
|
||||
</LabelSmall>
|
||||
{isPullRequestItem && task.statusMessage ? (
|
||||
<LabelXSmall color={t.textTertiary} $style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
{task.statusMessage}
|
||||
</LabelXSmall>
|
||||
) : null}
|
||||
</div>
|
||||
{task.pullRequest != null ? (
|
||||
<span className={css({ display: "inline-flex", alignItems: "center", gap: "4px", flexShrink: 0 })}>
|
||||
<LabelXSmall color={t.textSecondary} $style={{ fontWeight: 600 }}>
|
||||
#{task.pullRequest.number}
|
||||
</LabelXSmall>
|
||||
{task.pullRequest.status === "draft" ? <CloudUpload size={11} color={t.accent} /> : null}
|
||||
{task.pullRequest.isDraft ? <CloudUpload size={11} color={t.accent} /> : null}
|
||||
</span>
|
||||
) : (
|
||||
<GitPullRequestDraft size={11} color={t.textTertiary} />
|
||||
|
|
|
|||
|
|
@ -49,10 +49,9 @@ export const TranscriptHeader = memo(function TranscriptHeader({
|
|||
const t = useFoundryTokens();
|
||||
const isDesktop = !!import.meta.env.VITE_DESKTOP;
|
||||
const needsTrafficLightInset = isDesktop && sidebarCollapsed;
|
||||
const taskStatus = task.runtimeStatus ?? task.status;
|
||||
const headerStatus = useMemo(
|
||||
() => deriveHeaderStatus(taskStatus, task.statusMessage ?? null, activeSession?.status ?? null, activeSession?.errorMessage ?? null, hasSandbox),
|
||||
[taskStatus, task.statusMessage, activeSession?.status, activeSession?.errorMessage, hasSandbox],
|
||||
() => deriveHeaderStatus(task.status, activeSession?.status ?? null, activeSession?.errorMessage ?? null, hasSandbox),
|
||||
[task.status, activeSession?.status, activeSession?.errorMessage, hasSandbox],
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -181,6 +181,8 @@ export const AgentIcon = memo(function AgentIcon({ agent, size = 14 }: { agent:
|
|||
return <OpenAIIcon size={size} />;
|
||||
case "Cursor":
|
||||
return <CursorIcon size={size} />;
|
||||
default:
|
||||
return <CursorIcon size={size} />;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
import {
|
||||
DEFAULT_WORKSPACE_MODEL_GROUPS as SharedModelGroups,
|
||||
workspaceModelLabel as sharedWorkspaceModelLabel,
|
||||
workspaceProviderAgent as sharedWorkspaceProviderAgent,
|
||||
} from "@sandbox-agent/foundry-shared";
|
||||
import type {
|
||||
WorkspaceAgentKind as AgentKind,
|
||||
WorkspaceSession as AgentSession,
|
||||
|
|
@ -17,26 +22,7 @@ import { extractEventText } from "../../features/sessions/model";
|
|||
|
||||
export type { RepositorySection };
|
||||
|
||||
export const MODEL_GROUPS: ModelGroup[] = [
|
||||
{
|
||||
provider: "Claude",
|
||||
models: [
|
||||
{ id: "claude-sonnet-4", label: "Sonnet 4" },
|
||||
{ id: "claude-opus-4", label: "Opus 4" },
|
||||
],
|
||||
},
|
||||
{
|
||||
provider: "OpenAI",
|
||||
models: [
|
||||
{ id: "gpt-5.3-codex", label: "GPT-5.3 Codex" },
|
||||
{ id: "gpt-5.4", label: "GPT-5.4" },
|
||||
{ id: "gpt-5.2-codex", label: "GPT-5.2 Codex" },
|
||||
{ id: "gpt-5.1-codex-max", label: "GPT-5.1 Codex Max" },
|
||||
{ id: "gpt-5.2", label: "GPT-5.2" },
|
||||
{ id: "gpt-5.1-codex-mini", label: "GPT-5.1 Codex Mini" },
|
||||
],
|
||||
},
|
||||
];
|
||||
export const MODEL_GROUPS: ModelGroup[] = SharedModelGroups;
|
||||
|
||||
export function formatRelativeAge(updatedAtMs: number, nowMs = Date.now()): string {
|
||||
const deltaSeconds = Math.max(0, Math.floor((nowMs - updatedAtMs) / 1000));
|
||||
|
|
@ -94,15 +80,11 @@ export function formatMessageDuration(durationMs: number): string {
|
|||
}
|
||||
|
||||
export function modelLabel(id: ModelId): string {
|
||||
const group = MODEL_GROUPS.find((candidate) => candidate.models.some((model) => model.id === id));
|
||||
const model = group?.models.find((candidate) => candidate.id === id);
|
||||
return model && group ? `${group.provider} ${model.label}` : id;
|
||||
return sharedWorkspaceModelLabel(id, MODEL_GROUPS);
|
||||
}
|
||||
|
||||
export function providerAgent(provider: string): AgentKind {
|
||||
if (provider === "Claude") return "Claude";
|
||||
if (provider === "OpenAI") return "Codex";
|
||||
return "Cursor";
|
||||
return sharedWorkspaceProviderAgent(provider);
|
||||
}
|
||||
|
||||
const DIFF_PREFIX = "diff:";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
||||
import type { AgentType, RepoBranchRecord, RepoOverview, TaskWorkspaceSnapshot, WorkspaceTaskStatus } from "@sandbox-agent/foundry-shared";
|
||||
import type { RepoBranchRecord, RepoOverview, TaskWorkspaceSnapshot, WorkspaceTaskStatus } from "@sandbox-agent/foundry-shared";
|
||||
import { currentFoundryOrganization, useSubscription } from "@sandbox-agent/foundry-client";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { Link, useNavigate } from "@tanstack/react-router";
|
||||
|
|
@ -14,7 +14,6 @@ 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 } 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";
|
||||
|
|
@ -95,25 +94,13 @@ const FILTER_OPTIONS: SelectItem[] = [
|
|||
{ id: "all", label: "All Branches" },
|
||||
];
|
||||
|
||||
const AGENT_OPTIONS: SelectItem[] = [
|
||||
{ id: "codex", label: "codex" },
|
||||
{ id: "claude", label: "claude" },
|
||||
];
|
||||
|
||||
function statusKind(status: WorkspaceTaskStatus): StatusTagKind {
|
||||
if (status === "running") return "positive";
|
||||
if (status === "error") return "negative";
|
||||
if (status === "new" || String(status).startsWith("init_")) return "warning";
|
||||
if (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" });
|
||||
}
|
||||
|
|
@ -160,7 +147,7 @@ function repoSummary(overview: RepoOverview | undefined): {
|
|||
if (row.taskId) {
|
||||
mapped += 1;
|
||||
}
|
||||
if (row.prNumber && row.prState !== "MERGED" && row.prState !== "CLOSED") {
|
||||
if (row.pullRequest && row.pullRequest.state !== "MERGED" && row.pullRequest.state !== "CLOSED") {
|
||||
openPrs += 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -174,15 +161,25 @@ function repoSummary(overview: RepoOverview | undefined): {
|
|||
}
|
||||
|
||||
function branchKind(row: RepoBranchRecord): StatusTagKind {
|
||||
if (row.prState === "OPEN" || row.prState === "DRAFT") {
|
||||
if (row.pullRequest?.isDraft || row.pullRequest?.state === "OPEN") {
|
||||
return "warning";
|
||||
}
|
||||
if (row.prState === "MERGED") {
|
||||
if (row.pullRequest?.state === "MERGED") {
|
||||
return "positive";
|
||||
}
|
||||
return "neutral";
|
||||
}
|
||||
|
||||
function branchPullRequestLabel(branch: RepoBranchRecord): string {
|
||||
if (!branch.pullRequest) {
|
||||
return "no pr";
|
||||
}
|
||||
if (branch.pullRequest.isDraft) {
|
||||
return "draft";
|
||||
}
|
||||
return branch.pullRequest.state.toLowerCase();
|
||||
}
|
||||
|
||||
function matchesOverviewFilter(branch: RepoBranchRecord, filter: RepoOverviewFilter): boolean {
|
||||
if (filter === "archived") {
|
||||
return branch.taskStatus === "archived";
|
||||
|
|
@ -332,14 +329,6 @@ export function OrganizationDashboard({ organizationId, selectedTaskId, selected
|
|||
const [createTaskOpen, setCreateTaskOpen] = useState(false);
|
||||
const [selectedOverviewBranch, setSelectedOverviewBranch] = useState<string | null>(null);
|
||||
const [overviewFilter, setOverviewFilter] = useState<RepoOverviewFilter>("active");
|
||||
const [newAgentType, setNewAgentType] = useState<AgentType>(() => {
|
||||
try {
|
||||
const raw = globalThis.localStorage?.getItem("hf.settings.agentType");
|
||||
return raw === "claude" || raw === "codex" ? raw : "codex";
|
||||
} catch {
|
||||
return "codex";
|
||||
}
|
||||
});
|
||||
const [createError, setCreateError] = useState<string | null>(null);
|
||||
|
||||
const appState = useSubscription(subscriptionManager, "app", {});
|
||||
|
|
@ -383,14 +372,6 @@ export function OrganizationDashboard({ organizationId, selectedTaskId, selected
|
|||
}
|
||||
}, [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<string, typeof rows>();
|
||||
for (const row of rows) {
|
||||
|
|
@ -451,10 +432,10 @@ export function OrganizationDashboard({ organizationId, selectedTaskId, selected
|
|||
}, [selectedForSession?.id]);
|
||||
|
||||
const sessionRows = selectedForSession?.sessionsSummary ?? [];
|
||||
const taskRuntimeStatus = selectedForSession?.runtimeStatus ?? selectedForSession?.status ?? null;
|
||||
const taskStatusState = describeTaskState(taskRuntimeStatus, selectedForSession?.statusMessage ?? null);
|
||||
const taskStatus = selectedForSession?.status ?? null;
|
||||
const taskStatusState = describeTaskState(taskStatus);
|
||||
const taskStateSummary = `${taskStatusState.title}. ${taskStatusState.detail}`;
|
||||
const shouldUseTaskStateEmptyState = Boolean(selectedForSession && taskRuntimeStatus && taskRuntimeStatus !== "running" && taskRuntimeStatus !== "idle");
|
||||
const shouldUseTaskStateEmptyState = Boolean(selectedForSession && taskStatus && taskStatus !== "running" && taskStatus !== "idle");
|
||||
const sessionSelection = useMemo(
|
||||
() =>
|
||||
resolveSessionSelection({
|
||||
|
|
@ -505,8 +486,6 @@ export function OrganizationDashboard({ organizationId, selectedTaskId, selected
|
|||
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,
|
||||
|
|
@ -524,8 +503,6 @@ export function OrganizationDashboard({ organizationId, selectedTaskId, selected
|
|||
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,
|
||||
|
|
@ -553,13 +530,15 @@ export function OrganizationDashboard({ organizationId, selectedTaskId, selected
|
|||
if (!selectedForSession || !activeSandbox?.sandboxId) {
|
||||
throw new Error("No sandbox is available for this task");
|
||||
}
|
||||
const preferredAgent =
|
||||
selectedSessionSummary?.agent === "Claude" ? "claude" : selectedSessionSummary?.agent === "Codex" ? "codex" : undefined;
|
||||
return backendClient.createSandboxSession({
|
||||
organizationId,
|
||||
sandboxProviderId: activeSandbox.sandboxProviderId,
|
||||
sandboxId: activeSandbox.sandboxId,
|
||||
prompt: selectedForSession.task,
|
||||
cwd: activeSandbox.cwd ?? undefined,
|
||||
agent: normalizeAgent(selectedForSession.agentType),
|
||||
agent: preferredAgent,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -616,7 +595,6 @@ export function OrganizationDashboard({ organizationId, selectedTaskId, selected
|
|||
organizationId,
|
||||
repoId,
|
||||
task,
|
||||
agentType: newAgentType,
|
||||
explicitTitle: draftTitle || undefined,
|
||||
explicitBranchName: createOnBranch ? undefined : draftBranchName || undefined,
|
||||
onBranch: createOnBranch ?? undefined,
|
||||
|
|
@ -656,7 +634,6 @@ export function OrganizationDashboard({ organizationId, selectedTaskId, selected
|
|||
|
||||
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],
|
||||
|
|
@ -1057,23 +1034,23 @@ export function OrganizationDashboard({ organizationId, selectedTaskId, selected
|
|||
</div>
|
||||
<div className={cellClass}>{branch.taskTitle ?? branch.taskId ?? "-"}</div>
|
||||
<div className={cellClass}>
|
||||
{branch.prNumber ? (
|
||||
{branch.pullRequest ? (
|
||||
<a
|
||||
href={branch.prUrl ?? undefined}
|
||||
href={branch.pullRequest.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={css({
|
||||
color: theme.colors.contentPrimary,
|
||||
})}
|
||||
>
|
||||
#{branch.prNumber} {branch.prState ?? "open"}
|
||||
#{branch.pullRequest.number} {branchPullRequestLabel(branch)}
|
||||
</a>
|
||||
) : (
|
||||
<span className={css({ color: theme.colors.contentSecondary })}>-</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={cellClass}>
|
||||
{branch.ciStatus ?? "-"} / {branch.reviewStatus ?? "-"}
|
||||
{branch.ciStatus ?? "-"} / {branch.pullRequest ? (branch.pullRequest.isDraft ? "draft" : "ready") : "-"}
|
||||
</div>
|
||||
<div className={cellClass}>{formatRelativeAge(branch.updatedAt)}</div>
|
||||
<div className={cellClass}>
|
||||
|
|
@ -1098,7 +1075,7 @@ export function OrganizationDashboard({ organizationId, selectedTaskId, selected
|
|||
</Button>
|
||||
) : null}
|
||||
|
||||
<StatusPill kind={branchKind(branch)}>{branch.prState?.toLowerCase() ?? "no pr"}</StatusPill>
|
||||
<StatusPill kind={branchKind(branch)}>{branchPullRequestLabel(branch)}</StatusPill>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1138,7 +1115,6 @@ export function OrganizationDashboard({ organizationId, selectedTaskId, selected
|
|||
<HeaderStatusPill
|
||||
status={deriveHeaderStatus(
|
||||
taskRuntimeStatus ?? selectedForSession.status,
|
||||
selectedForSession.statusMessage ?? null,
|
||||
selectedSessionSummary?.status ?? null,
|
||||
selectedSessionSummary?.errorMessage ?? null,
|
||||
Boolean(activeSandbox?.sandboxId),
|
||||
|
|
@ -1266,8 +1242,7 @@ export function OrganizationDashboard({ organizationId, selectedTaskId, selected
|
|||
<ParagraphSmall marginTop="0" marginBottom="0" color="contentSecondary">
|
||||
{shouldUseTaskStateEmptyState
|
||||
? taskStateSummary
|
||||
: (selectedForSession?.statusMessage ??
|
||||
(isPendingProvision ? "The task is still provisioning." : "The session is being created."))}
|
||||
: (isPendingProvision ? "The task is still provisioning." : "The session is being created.")}
|
||||
</ParagraphSmall>
|
||||
</div>
|
||||
) : null}
|
||||
|
|
@ -1277,15 +1252,13 @@ export function OrganizationDashboard({ organizationId, selectedTaskId, selected
|
|||
{shouldUseTaskStateEmptyState
|
||||
? taskStateSummary
|
||||
: isPendingProvision
|
||||
? (selectedForSession.statusMessage ?? "Provisioning sandbox...")
|
||||
? "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."
|
||||
? "This task is still provisioning its sandbox."
|
||||
: staleSessionId
|
||||
? `Session ${staleSessionId} is unavailable. Start a new session to continue.`
|
||||
: resolvedSessionId
|
||||
|
|
@ -1458,7 +1431,7 @@ export function OrganizationDashboard({ organizationId, selectedTaskId, selected
|
|||
<MetaRow label="Branch" value={selectedBranchOverview.branchName} mono />
|
||||
<MetaRow label="Commit" value={selectedBranchOverview.commitSha.slice(0, 10)} mono />
|
||||
<MetaRow label="Task" value={selectedBranchOverview.taskTitle ?? selectedBranchOverview.taskId ?? "-"} />
|
||||
<MetaRow label="PR" value={selectedBranchOverview.prUrl ?? "-"} />
|
||||
<MetaRow label="PR" value={selectedBranchOverview.pullRequest?.url ?? "-"} />
|
||||
<MetaRow label="Updated" value={new Date(selectedBranchOverview.updatedAt).toLocaleTimeString()} />
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -1504,9 +1477,8 @@ export function OrganizationDashboard({ organizationId, selectedTaskId, selected
|
|||
})}
|
||||
>
|
||||
<MetaRow label="Branch" value={selectedForSession.branch ?? "-"} mono />
|
||||
<MetaRow label="Diff" value={formatDiffStat(selectedForSession.diffStat)} />
|
||||
<MetaRow label="PR" value={selectedForSession.prUrl ?? "-"} />
|
||||
<MetaRow label="Review" value={selectedForSession.reviewStatus ?? "-"} />
|
||||
<MetaRow label="PR" value={selectedForSession.pullRequest?.url ?? "-"} />
|
||||
<MetaRow label="Review" value={selectedForSession.pullRequest ? (selectedForSession.pullRequest.isDraft ? "draft" : "ready") : "-"} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
|
@ -1607,25 +1579,6 @@ export function OrganizationDashboard({ organizationId, selectedTaskId, selected
|
|||
) : null}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<LabelXSmall color="contentSecondary" marginBottom="scale200">
|
||||
Agent
|
||||
</LabelXSmall>
|
||||
<Select
|
||||
options={AGENT_OPTIONS.map(createOption)}
|
||||
value={selectValue(selectedAgentOption)}
|
||||
clearable={false}
|
||||
searchable={false}
|
||||
onChange={(params: OnChangeParams) => {
|
||||
const next = optionId(params.value);
|
||||
if (next === "claude" || next === "codex") {
|
||||
setNewAgentType(next);
|
||||
}
|
||||
}}
|
||||
overrides={selectTestIdOverrides("task-create-agent")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<LabelXSmall color="contentSecondary" marginBottom="scale200">
|
||||
Task
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue