mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-17 04:02:25 +00:00
Refactor Foundry GitHub and sandbox flows
This commit is contained in:
parent
4bccd5fc8d
commit
ec8e816d0d
112 changed files with 4026 additions and 2715 deletions
|
|
@ -28,6 +28,7 @@ import {
|
|||
type ModelId,
|
||||
} from "./mock-layout/view-model";
|
||||
import { activeMockOrganization, useMockAppSnapshot } from "../lib/mock-app";
|
||||
import { backendClient } from "../lib/backend";
|
||||
import { getTaskWorkbenchClient } from "../lib/workbench";
|
||||
|
||||
function firstAgentTabId(task: Task): string | null {
|
||||
|
|
@ -63,6 +64,39 @@ function sanitizeActiveTabId(task: Task, tabId: string | null | undefined, openD
|
|||
return openDiffs.length > 0 ? diffTabId(openDiffs[openDiffs.length - 1]!) : lastAgentTabId;
|
||||
}
|
||||
|
||||
function resolvedTaskLifecycle(task: Task) {
|
||||
return (
|
||||
task.lifecycle ?? {
|
||||
code: task.status === "running" ? "running" : task.status === "idle" ? "idle" : task.status === "archived" ? "archived" : "init_create_sandbox",
|
||||
state: task.status === "running" || task.status === "idle" ? "ready" : task.status === "archived" ? "archived" : "starting",
|
||||
label:
|
||||
task.status === "running" ? "Agent running" : task.status === "idle" ? "Task idle" : task.status === "archived" ? "Task archived" : "Creating sandbox",
|
||||
message: null,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function taskLifecycleAccent(task: Task): string {
|
||||
switch (resolvedTaskLifecycle(task).state) {
|
||||
case "error":
|
||||
return "#ef4444";
|
||||
case "starting":
|
||||
return "#f59e0b";
|
||||
case "ready":
|
||||
return "#10b981";
|
||||
case "archived":
|
||||
case "killed":
|
||||
return "#94a3b8";
|
||||
default:
|
||||
return "#94a3b8";
|
||||
}
|
||||
}
|
||||
|
||||
function shouldShowTaskLifecycle(task: Task): boolean {
|
||||
const lifecycle = resolvedTaskLifecycle(task);
|
||||
return lifecycle.state === "starting" || lifecycle.state === "error";
|
||||
}
|
||||
|
||||
const TranscriptPanel = memo(function TranscriptPanel({
|
||||
taskWorkbenchClient,
|
||||
task,
|
||||
|
|
@ -445,6 +479,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
|||
activeAgentTab?.status === "running" && activeAgentTab.thinkingSinceMs !== null
|
||||
? formatThinkingDuration(timerNowMs - activeAgentTab.thinkingSinceMs)
|
||||
: null;
|
||||
const lifecycle = resolvedTaskLifecycle(task);
|
||||
|
||||
return (
|
||||
<SPanel>
|
||||
|
|
@ -470,6 +505,37 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
|||
onToggleRightSidebar={onToggleRightSidebar}
|
||||
onNavigateToUsage={onNavigateToUsage}
|
||||
/>
|
||||
{shouldShowTaskLifecycle(task) ? (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "4px",
|
||||
padding: "10px 16px",
|
||||
borderLeft: `3px solid ${taskLifecycleAccent(task)}`,
|
||||
background: lifecycle.state === "error" ? "rgba(127, 29, 29, 0.35)" : "rgba(120, 53, 15, 0.28)",
|
||||
borderBottom: "1px solid rgba(255, 255, 255, 0.08)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "8px",
|
||||
fontSize: "12px",
|
||||
fontWeight: 700,
|
||||
letterSpacing: "0.04em",
|
||||
textTransform: "uppercase",
|
||||
}}
|
||||
>
|
||||
<span style={{ color: taskLifecycleAccent(task) }}>{lifecycle.label}</span>
|
||||
<span style={{ opacity: 0.6 }}>{lifecycle.code}</span>
|
||||
</div>
|
||||
<div style={{ fontSize: "13px", color: "#e4e4e7" }}>
|
||||
{lifecycle.message ?? (lifecycle.state === "starting" ? "Waiting for the sandbox and first session to come online." : "Task startup failed.")}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
|
|
@ -530,7 +596,12 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
|||
}}
|
||||
>
|
||||
<h2 style={{ margin: 0, fontSize: "20px", fontWeight: 600 }}>Create the first session</h2>
|
||||
<p style={{ margin: 0, opacity: 0.75 }}>Sessions are where you chat with the agent. Start one now to send the first prompt on this task.</p>
|
||||
<p style={{ margin: 0, opacity: 0.75 }}>
|
||||
{lifecycle.state === "starting"
|
||||
? `Task startup is still in progress: ${lifecycle.label}.`
|
||||
: "Sessions are where you chat with the agent. Start one now to send the first prompt on this task."}
|
||||
</p>
|
||||
{lifecycle.message ? <p style={{ margin: 0, fontSize: "13px", color: "#d4d4d8" }}>{lifecycle.message}</p> : null}
|
||||
<button
|
||||
type="button"
|
||||
onClick={addTab}
|
||||
|
|
@ -814,6 +885,72 @@ interface MockLayoutProps {
|
|||
selectedSessionId?: string | null;
|
||||
}
|
||||
|
||||
function githubStatusPill(organization: ReturnType<typeof activeMockOrganization>) {
|
||||
if (!organization) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const label =
|
||||
organization.github.installationStatus !== "connected"
|
||||
? "GitHub disconnected"
|
||||
: organization.github.syncStatus === "syncing"
|
||||
? "GitHub syncing"
|
||||
: organization.github.syncStatus === "error"
|
||||
? "GitHub error"
|
||||
: organization.github.syncStatus === "pending"
|
||||
? "GitHub pending"
|
||||
: "GitHub synced";
|
||||
|
||||
const colors =
|
||||
organization.github.installationStatus !== "connected"
|
||||
? { background: "rgba(255, 193, 7, 0.18)", color: "#ffe6a6" }
|
||||
: organization.github.syncStatus === "syncing"
|
||||
? { background: "rgba(24, 140, 255, 0.18)", color: "#b9d8ff" }
|
||||
: organization.github.syncStatus === "error"
|
||||
? { background: "rgba(255, 79, 0, 0.18)", color: "#ffd6c7" }
|
||||
: { background: "rgba(46, 160, 67, 0.16)", color: "#b7f0c3" };
|
||||
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
border: "1px solid rgba(255,255,255,0.08)",
|
||||
borderRadius: "999px",
|
||||
padding: "6px 10px",
|
||||
background: colors.background,
|
||||
color: colors.color,
|
||||
fontSize: "12px",
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function actorRuntimePill(organization: ReturnType<typeof activeMockOrganization>) {
|
||||
if (!organization || organization.runtime.status !== "error") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const label = organization.runtime.errorCount === 1 ? "1 actor error" : `${organization.runtime.errorCount} actor errors`;
|
||||
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
border: "1px solid rgba(255,255,255,0.08)",
|
||||
borderRadius: "999px",
|
||||
padding: "6px 10px",
|
||||
background: "rgba(255, 79, 0, 0.2)",
|
||||
color: "#ffd6c7",
|
||||
fontSize: "12px",
|
||||
fontWeight: 800,
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function MockWorkspaceOrgBar() {
|
||||
const navigate = useNavigate();
|
||||
const snapshot = useMockAppSnapshot();
|
||||
|
|
@ -834,6 +971,7 @@ function MockWorkspaceOrgBar() {
|
|||
fontSize: "13px",
|
||||
fontWeight: 600,
|
||||
} satisfies React.CSSProperties;
|
||||
const latestRuntimeIssue = organization.runtime.issues[0] ?? null;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -851,6 +989,15 @@ function MockWorkspaceOrgBar() {
|
|||
<strong style={{ fontSize: "14px", fontWeight: 600 }}>{organization.settings.displayName}</strong>
|
||||
<span style={{ fontSize: "12px", color: t.textMuted }}>{organization.settings.primaryDomain}</span>
|
||||
</div>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "10px", flexWrap: "wrap" }}>
|
||||
{actorRuntimePill(organization)}
|
||||
{githubStatusPill(organization)}
|
||||
<span style={{ fontSize: "12px", color: t.textMuted }}>
|
||||
{organization.runtime.status === "error" && latestRuntimeIssue
|
||||
? `${latestRuntimeIssue.scopeLabel}: ${latestRuntimeIssue.message}`
|
||||
: organization.github.lastSyncLabel}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: "8px", flexWrap: "wrap" }}>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -923,6 +1070,8 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
|||
const [lastAgentTabIdByTask, setLastAgentTabIdByTask] = useState<Record<string, string | null>>({});
|
||||
const [openDiffsByTask, setOpenDiffsByTask] = useState<Record<string, string[]>>({});
|
||||
const [selectedNewTaskRepoId, setSelectedNewTaskRepoId] = useState("");
|
||||
const [activeTaskDetail, setActiveTaskDetail] = useState<Task | null>(null);
|
||||
const [activeTaskDetailLoading, setActiveTaskDetailLoading] = useState(false);
|
||||
const [leftWidth, setLeftWidth] = useState(() => readStoredWidth(LEFT_WIDTH_STORAGE_KEY, LEFT_SIDEBAR_DEFAULT_WIDTH));
|
||||
const [rightWidth, setRightWidth] = useState(() => readStoredWidth(RIGHT_WIDTH_STORAGE_KEY, RIGHT_SIDEBAR_DEFAULT_WIDTH));
|
||||
const leftWidthRef = useRef(leftWidth);
|
||||
|
|
@ -995,7 +1144,43 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
|||
startRightRef.current = rightWidthRef.current;
|
||||
}, []);
|
||||
|
||||
const activeTask = useMemo(() => tasks.find((task) => task.id === selectedTaskId) ?? tasks[0] ?? null, [tasks, selectedTaskId]);
|
||||
const activeTaskSummary = useMemo(() => tasks.find((task) => task.id === selectedTaskId) ?? tasks[0] ?? null, [tasks, selectedTaskId]);
|
||||
const activeTask = useMemo(() => {
|
||||
if (activeTaskSummary && activeTaskDetail?.id === activeTaskSummary.id) {
|
||||
return activeTaskDetail;
|
||||
}
|
||||
return activeTaskSummary;
|
||||
}, [activeTaskDetail, activeTaskSummary]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeTaskSummary) {
|
||||
setActiveTaskDetail(null);
|
||||
setActiveTaskDetailLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
setActiveTaskDetailLoading(true);
|
||||
void backendClient
|
||||
.getWorkbenchTask(workspaceId, activeTaskSummary.id)
|
||||
.then((task) => {
|
||||
if (cancelled) return;
|
||||
setActiveTaskDetail(task as Task);
|
||||
})
|
||||
.catch((error) => {
|
||||
if (cancelled) return;
|
||||
console.error("failed to load active task detail", error);
|
||||
setActiveTaskDetail(null);
|
||||
})
|
||||
.finally(() => {
|
||||
if (cancelled) return;
|
||||
setActiveTaskDetailLoading(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [activeTaskSummary?.id, workspaceId, viewModel]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTask) {
|
||||
|
|
@ -1091,6 +1276,9 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
|||
if (!activeTask) {
|
||||
return;
|
||||
}
|
||||
if (activeTaskDetailLoading) {
|
||||
return;
|
||||
}
|
||||
if (activeTask.tabs.length > 0) {
|
||||
autoCreatingSessionForTaskRef.current.delete(activeTask.id);
|
||||
return;
|
||||
|
|
@ -1113,7 +1301,7 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
|||
autoCreatingSessionForTaskRef.current.delete(activeTask.id);
|
||||
}
|
||||
})();
|
||||
}, [activeTask, selectedSessionId, syncRouteSession, taskWorkbenchClient]);
|
||||
}, [activeTask, activeTaskDetailLoading, selectedSessionId, syncRouteSession, taskWorkbenchClient]);
|
||||
|
||||
const createTask = useCallback(() => {
|
||||
void (async () => {
|
||||
|
|
|
|||
|
|
@ -122,6 +122,24 @@ 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 pullRequestStatusLabel =
|
||||
task.pullRequest?.status === "merged"
|
||||
? "Merged"
|
||||
: task.pullRequest?.status === "closed"
|
||||
? "Closed"
|
||||
: task.pullRequest?.status === "draft"
|
||||
? "Draft"
|
||||
: task.pullRequest?.status === "ready"
|
||||
? "Open"
|
||||
: null;
|
||||
const pullRequestActionLabel =
|
||||
task.pullRequest?.status === "merged"
|
||||
? "Open merged PR"
|
||||
: task.pullRequest?.status === "closed"
|
||||
? "Open closed PR"
|
||||
: pullRequestUrl
|
||||
? "Open PR"
|
||||
: "Publish PR";
|
||||
|
||||
const copyFilePath = useCallback(async (path: string) => {
|
||||
try {
|
||||
|
|
@ -155,6 +173,38 @@ export const RightSidebar = memo(function RightSidebar({
|
|||
<div ref={headerRef} className={css({ display: "flex", alignItems: "center", flex: 1, minWidth: 0, justifyContent: "flex-end", gap: "2px" })}>
|
||||
{!isTerminal ? (
|
||||
<div className={css({ display: "flex", alignItems: "center", gap: "2px", flexShrink: 1, minWidth: 0 })}>
|
||||
{pullRequestStatusLabel ? (
|
||||
<span
|
||||
className={css({
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
padding: compact ? "3px 6px" : "4px 8px",
|
||||
borderRadius: "999px",
|
||||
fontSize: "10px",
|
||||
fontWeight: 600,
|
||||
color:
|
||||
task.pullRequest?.status === "merged"
|
||||
? "#86efac"
|
||||
: task.pullRequest?.status === "closed"
|
||||
? "#fca5a5"
|
||||
: task.pullRequest?.status === "draft"
|
||||
? t.accent
|
||||
: t.textSecondary,
|
||||
backgroundColor:
|
||||
task.pullRequest?.status === "merged"
|
||||
? "rgba(22, 101, 52, 0.35)"
|
||||
: task.pullRequest?.status === "closed"
|
||||
? "rgba(127, 29, 29, 0.35)"
|
||||
: task.pullRequest?.status === "draft"
|
||||
? "rgba(154, 52, 18, 0.28)"
|
||||
: t.interactiveHover,
|
||||
whiteSpace: "nowrap",
|
||||
flexShrink: 0,
|
||||
})}
|
||||
>
|
||||
{pullRequestStatusLabel}
|
||||
</span>
|
||||
) : null}
|
||||
<button
|
||||
onClick={() => {
|
||||
if (pullRequestUrl) {
|
||||
|
|
@ -188,7 +238,7 @@ export const RightSidebar = memo(function RightSidebar({
|
|||
})}
|
||||
>
|
||||
<GitPullRequest size={12} style={{ flexShrink: 0 }} />
|
||||
{!compact && <span>{pullRequestUrl ? "Open PR" : "Publish PR"}</span>}
|
||||
{!compact && <span>{pullRequestActionLabel}</span>}
|
||||
</button>
|
||||
<button
|
||||
className={css({
|
||||
|
|
|
|||
|
|
@ -593,6 +593,11 @@ export const Sidebar = memo(function Sidebar({
|
|||
#{task.pullRequest.number}
|
||||
</LabelXSmall>
|
||||
{task.pullRequest.status === "draft" ? <CloudUpload size={11} color={t.accent} /> : null}
|
||||
{task.pullRequest.status === "merged" ? <LabelXSmall color="#86efac">merged</LabelXSmall> : null}
|
||||
{task.pullRequest.status === "closed" ? <LabelXSmall color="#fca5a5">closed</LabelXSmall> : null}
|
||||
{task.pullRequest.status === "draft" ? <CloudUpload size={11} color={t.accent} /> : null}
|
||||
{task.pullRequest.status === "merged" ? <LabelXSmall color="#86efac">merged</LabelXSmall> : null}
|
||||
{task.pullRequest.status === "closed" ? <LabelXSmall color="#fca5a5">closed</LabelXSmall> : null}
|
||||
</span>
|
||||
) : (
|
||||
<GitPullRequestDraft size={11} color={t.textTertiary} />
|
||||
|
|
|
|||
|
|
@ -697,13 +697,44 @@ export function MockOrganizationSettingsPage({ organization }: { organization: F
|
|||
description={`Connected as ${organization.github.connectedAccount}. ${organization.github.importedRepoCount} repos imported.`}
|
||||
>
|
||||
<SettingsRow label="Installation status" description={`Last sync: ${organization.github.lastSyncLabel}`} action={githubBadge(t, organization)} />
|
||||
<div style={{ display: "flex", gap: "8px" }}>
|
||||
{organization.runtime.status === "error" ? (
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gap: "8px",
|
||||
padding: "12px",
|
||||
borderRadius: "8px",
|
||||
background: "rgba(255, 79, 0, 0.08)",
|
||||
border: "1px solid rgba(255, 79, 0, 0.18)",
|
||||
}}
|
||||
>
|
||||
<div style={{ color: t.textPrimary, fontSize: "12px", fontWeight: 600 }}>
|
||||
{organization.runtime.errorCount === 1
|
||||
? "1 actor is currently reporting a workflow error"
|
||||
: `${organization.runtime.errorCount} actors are currently reporting workflow errors`}
|
||||
</div>
|
||||
{organization.runtime.issues.slice(0, 4).map((issue) => (
|
||||
<div key={issue.actorId} style={{ color: t.textSecondary, fontSize: "11px", lineHeight: 1.5 }}>
|
||||
<strong>{issue.scopeLabel}</strong>: {issue.message}
|
||||
{issue.stepName ? ` · step ${issue.stepName}` : ""}
|
||||
{issue.attempt != null ? ` · attempt ${issue.attempt}` : ""}
|
||||
{issue.willRetry && issue.retryDelayMs != null ? ` · retrying in ${issue.retryDelayMs}ms` : ""}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
<div style={{ display: "flex", gap: "8px", flexWrap: "wrap" }}>
|
||||
<button type="button" onClick={() => void client.reconnectGithub(organization.id)} style={secondaryButtonStyle(t)}>
|
||||
Reconnect GitHub
|
||||
</button>
|
||||
<button type="button" onClick={() => void client.triggerGithubSync(organization.id)} style={subtleButtonStyle(t)}>
|
||||
Sync repos
|
||||
</button>
|
||||
{organization.runtime.errorCount > 0 ? (
|
||||
<button type="button" onClick={() => void client.clearOrganizationRuntimeIssues(organization.id)} style={subtleButtonStyle(t)}>
|
||||
Clear actor errors
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</SettingsContentSection>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue