feat(foundry): checkpoint actor and workspace refactor

This commit is contained in:
Nathan Flurry 2026-03-15 10:20:27 -07:00
parent 32f3c6c3bc
commit dbe57d45b9
81 changed files with 3441 additions and 2332 deletions

View file

@ -7,10 +7,10 @@ import type {
FoundryAppSnapshot,
FoundryOrganization,
TaskStatus,
TaskWorkbenchSnapshot,
WorkbenchSandboxSummary,
WorkbenchSessionSummary,
WorkbenchTaskStatus,
TaskWorkspaceSnapshot,
WorkspaceSandboxSummary,
WorkspaceSessionSummary,
WorkspaceTaskStatus,
} from "@sandbox-agent/foundry-shared";
import { useSubscription } from "@sandbox-agent/foundry-client";
import type { DebugSubscriptionTopic } from "@sandbox-agent/foundry-client";
@ -18,7 +18,7 @@ import { describeTaskState } from "../features/tasks/status";
interface DevPanelProps {
organizationId: string;
snapshot: TaskWorkbenchSnapshot;
snapshot: TaskWorkspaceSnapshot;
organization?: FoundryOrganization | null;
focusedTask?: DevPanelFocusedTask | null;
}
@ -27,14 +27,14 @@ export interface DevPanelFocusedTask {
id: string;
repoId: string;
title: string | null;
status: WorkbenchTaskStatus;
status: WorkspaceTaskStatus;
runtimeStatus?: TaskStatus | null;
statusMessage?: string | null;
branch?: string | null;
activeSandboxId?: string | null;
activeSessionId?: string | null;
sandboxes?: WorkbenchSandboxSummary[];
sessions?: WorkbenchSessionSummary[];
sandboxes?: WorkspaceSandboxSummary[];
sessions?: WorkspaceSessionSummary[];
}
interface TopicInfo {

View file

@ -4,11 +4,11 @@ import { useStyletron } from "baseui";
import {
createErrorContext,
type FoundryOrganization,
type TaskWorkbenchSnapshot,
type WorkbenchOpenPrSummary,
type WorkbenchSessionSummary,
type WorkbenchTaskDetail,
type WorkbenchTaskSummary,
type TaskWorkspaceSnapshot,
type WorkspaceOpenPrSummary,
type WorkspaceSessionSummary,
type WorkspaceTaskDetail,
type WorkspaceTaskSummary,
} from "@sandbox-agent/foundry-shared";
import { useSubscription } from "@sandbox-agent/foundry-client";
@ -39,7 +39,7 @@ import {
type Message,
type ModelId,
} from "./mock-layout/view-model";
import { activeMockOrganization, useMockAppSnapshot } from "../lib/mock-app";
import { activeMockOrganization, activeMockUser, useMockAppClient, useMockAppSnapshot } from "../lib/mock-app";
import { backendClient } from "../lib/backend";
import { subscriptionManager } from "../lib/subscription";
import { describeTaskState, isProvisioningTaskStatus } from "../features/tasks/status";
@ -131,7 +131,7 @@ function GithubInstallationWarning({
}
function toSessionModel(
summary: WorkbenchSessionSummary,
summary: WorkspaceSessionSummary,
sessionDetail?: { draft: Task["sessions"][number]["draft"]; transcript: Task["sessions"][number]["transcript"] },
): Task["sessions"][number] {
return {
@ -155,8 +155,8 @@ function toSessionModel(
}
function toTaskModel(
summary: WorkbenchTaskSummary,
detail?: WorkbenchTaskDetail,
summary: WorkspaceTaskSummary,
detail?: WorkspaceTaskDetail,
sessionCache?: Map<string, { draft: Task["sessions"][number]["draft"]; transcript: Task["sessions"][number]["transcript"] }>,
): Task {
const sessions = detail?.sessionsSummary ?? summary.sessionsSummary;
@ -190,7 +190,7 @@ function isOpenPrTaskId(taskId: string): boolean {
return taskId.startsWith(OPEN_PR_TASK_PREFIX);
}
function toOpenPrTaskModel(pullRequest: WorkbenchOpenPrSummary): Task {
function toOpenPrTaskModel(pullRequest: WorkspaceOpenPrSummary): Task {
return {
id: openPrTaskId(pullRequest.prId),
repoId: pullRequest.repoId,
@ -241,7 +241,7 @@ function groupRepositories(repos: Array<{ id: string; label: string }>, tasks: T
.filter((repo) => repo.tasks.length > 0);
}
interface WorkbenchActions {
interface WorkspaceActions {
createTask(input: {
repoId: string;
task: string;
@ -252,7 +252,6 @@ interface WorkbenchActions {
}): Promise<{ taskId: string; sessionId?: string }>;
markTaskUnread(input: { taskId: string }): Promise<void>;
renameTask(input: { taskId: string; value: string }): Promise<void>;
renameBranch(input: { taskId: string; value: string }): Promise<void>;
archiveTask(input: { taskId: string }): Promise<void>;
publishPr(input: { taskId: string }): Promise<void>;
revertFile(input: { taskId: string; path: string }): Promise<void>;
@ -264,14 +263,14 @@ interface WorkbenchActions {
closeSession(input: { taskId: string; sessionId: string }): Promise<void>;
addSession(input: { taskId: string; model?: string }): Promise<{ sessionId: string }>;
changeModel(input: { taskId: string; sessionId: string; model: ModelId }): Promise<void>;
reloadGithubOrganization(): Promise<void>;
reloadGithubPullRequests(): Promise<void>;
reloadGithubRepository(repoId: string): Promise<void>;
reloadGithubPullRequest(repoId: string, prNumber: number): 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({
taskWorkbenchClient,
taskWorkspaceClient,
task,
hasSandbox,
activeSessionId,
@ -290,7 +289,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
selectedSessionHydrating = false,
onNavigateToUsage,
}: {
taskWorkbenchClient: WorkbenchActions;
taskWorkspaceClient: WorkspaceActions;
task: Task;
hasSandbox: boolean;
activeSessionId: string | null;
@ -310,8 +309,11 @@ const TranscriptPanel = memo(function TranscriptPanel({
onNavigateToUsage?: () => void;
}) {
const t = useFoundryTokens();
const [defaultModel, setDefaultModel] = useState<ModelId>("claude-sonnet-4");
const [editingField, setEditingField] = useState<"title" | "branch" | null>(null);
const appSnapshot = useMockAppSnapshot();
const appClient = useMockAppClient();
const currentUser = activeMockUser(appSnapshot);
const defaultModel = currentUser?.defaultModel ?? "claude-sonnet-4";
const [editingField, setEditingField] = useState<"title" | null>(null);
const [editValue, setEditValue] = useState("");
const [editingSessionId, setEditingSessionId] = useState<string | null>(null);
const [editingSessionName, setEditingSessionName] = useState("");
@ -436,14 +438,14 @@ const TranscriptPanel = memo(function TranscriptPanel({
return;
}
void taskWorkbenchClient.setSessionUnread({
void taskWorkspaceClient.setSessionUnread({
taskId: task.id,
sessionId: activeAgentSession.id,
unread: false,
});
}, [activeAgentSession?.id, activeAgentSession?.unread, task.id]);
const startEditingField = useCallback((field: "title" | "branch", value: string) => {
const startEditingField = useCallback((field: "title", value: string) => {
setEditingField(field);
setEditValue(value);
}, []);
@ -453,18 +455,14 @@ const TranscriptPanel = memo(function TranscriptPanel({
}, []);
const commitEditingField = useCallback(
(field: "title" | "branch") => {
(field: "title") => {
const value = editValue.trim();
if (!value) {
setEditingField(null);
return;
}
if (field === "title") {
void taskWorkbenchClient.renameTask({ taskId: task.id, value });
} else {
void taskWorkbenchClient.renameBranch({ taskId: task.id, value });
}
void taskWorkspaceClient.renameTask({ taskId: task.id, value });
setEditingField(null);
},
[editValue, task.id],
@ -474,7 +472,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
const flushDraft = useCallback(
(text: string, nextAttachments: LineAttachment[], sessionId: string) => {
void taskWorkbenchClient.updateDraft({
void taskWorkspaceClient.updateDraft({
taskId: task.id,
sessionId,
text,
@ -535,7 +533,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
onSetActiveSessionId(promptSession.id);
onSetLastAgentSessionId(promptSession.id);
void taskWorkbenchClient.sendMessage({
void taskWorkspaceClient.sendMessage({
taskId: task.id,
sessionId: promptSession.id,
text,
@ -548,7 +546,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
return;
}
void taskWorkbenchClient.stopAgent({
void taskWorkspaceClient.stopAgent({
taskId: task.id,
sessionId: promptSession.id,
});
@ -562,7 +560,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
onSetLastAgentSessionId(sessionId);
const session = task.sessions.find((candidate) => candidate.id === sessionId);
if (session?.unread) {
void taskWorkbenchClient.setSessionUnread({
void taskWorkspaceClient.setSessionUnread({
taskId: task.id,
sessionId,
unread: false,
@ -576,7 +574,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
const setSessionUnread = useCallback(
(sessionId: string, unread: boolean) => {
void taskWorkbenchClient.setSessionUnread({ taskId: task.id, sessionId, unread });
void taskWorkspaceClient.setSessionUnread({ taskId: task.id, sessionId, unread });
},
[task.id],
);
@ -610,7 +608,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
return;
}
void taskWorkbenchClient.renameSession({
void taskWorkspaceClient.renameSession({
taskId: task.id,
sessionId: editingSessionId,
title: trimmedName,
@ -631,7 +629,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
}
onSyncRouteSession(task.id, nextSessionId);
void taskWorkbenchClient.closeSession({ taskId: task.id, sessionId });
void taskWorkspaceClient.closeSession({ taskId: task.id, sessionId });
},
[activeSessionId, task.id, task.sessions, lastAgentSessionId, onSetActiveSessionId, onSetLastAgentSessionId, onSyncRouteSession],
);
@ -651,7 +649,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
const addSession = useCallback(() => {
void (async () => {
const { sessionId } = await taskWorkbenchClient.addSession({ taskId: task.id });
const { sessionId } = await taskWorkspaceClient.addSession({ taskId: task.id });
onSetLastAgentSessionId(sessionId);
onSetActiveSessionId(sessionId);
onSyncRouteSession(task.id, sessionId);
@ -664,7 +662,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
throw new Error(`Unable to change model for task ${task.id} without an active prompt session`);
}
void taskWorkbenchClient.changeModel({
void taskWorkspaceClient.changeModel({
taskId: task.id,
sessionId: promptSession.id,
model,
@ -939,7 +937,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
messageRefs={messageRefs}
historyEvents={historyEvents}
onSelectHistoryEvent={jumpToHistoryEvent}
targetMessageId={pendingHistoryTarget && activeSessionId === pendingHistoryTarget.sessionId ? pendingHistoryTarget.messageId : null}
targetMessageId={pendingHistoryTarget && activeAgentSession?.id === pendingHistoryTarget.sessionId ? pendingHistoryTarget.messageId : null}
onTargetMessageResolved={() => setPendingHistoryTarget(null)}
copiedMessageId={copiedMessageId}
onCopyMessage={(message) => {
@ -966,7 +964,9 @@ const TranscriptPanel = memo(function TranscriptPanel({
onStop={stopAgent}
onRemoveAttachment={removeAttachment}
onChangeModel={changeModel}
onSetDefaultModel={setDefaultModel}
onSetDefaultModel={(model) => {
void appClient.setDefaultModel(model);
}}
/>
) : null}
</div>
@ -1280,27 +1280,26 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
const [css] = useStyletron();
const t = useFoundryTokens();
const navigate = useNavigate();
const taskWorkbenchClient = useMemo<WorkbenchActions>(
const taskWorkspaceClient = useMemo<WorkspaceActions>(
() => ({
createTask: (input) => backendClient.createWorkbenchTask(organizationId, input),
markTaskUnread: (input) => backendClient.markWorkbenchUnread(organizationId, input),
renameTask: (input) => backendClient.renameWorkbenchTask(organizationId, input),
renameBranch: (input) => backendClient.renameWorkbenchBranch(organizationId, input),
archiveTask: async (input) => backendClient.runAction(organizationId, input.taskId, "archive"),
publishPr: (input) => backendClient.publishWorkbenchPr(organizationId, input),
revertFile: (input) => backendClient.revertWorkbenchFile(organizationId, input),
updateDraft: (input) => backendClient.updateWorkbenchDraft(organizationId, input),
sendMessage: (input) => backendClient.sendWorkbenchMessage(organizationId, input),
stopAgent: (input) => backendClient.stopWorkbenchSession(organizationId, input),
setSessionUnread: (input) => backendClient.setWorkbenchSessionUnread(organizationId, input),
renameSession: (input) => backendClient.renameWorkbenchSession(organizationId, input),
closeSession: (input) => backendClient.closeWorkbenchSession(organizationId, input),
addSession: (input) => backendClient.createWorkbenchSession(organizationId, input),
changeModel: (input) => backendClient.changeWorkbenchModel(organizationId, input),
reloadGithubOrganization: () => backendClient.reloadGithubOrganization(organizationId),
reloadGithubPullRequests: () => backendClient.reloadGithubPullRequests(organizationId),
reloadGithubRepository: (repoId) => backendClient.reloadGithubRepository(organizationId, repoId),
reloadGithubPullRequest: (repoId, prNumber) => backendClient.reloadGithubPullRequest(organizationId, repoId, prNumber),
createTask: (input) => backendClient.createWorkspaceTask(organizationId, input),
markTaskUnread: (input) => backendClient.markWorkspaceUnread(organizationId, input),
renameTask: (input) => backendClient.renameWorkspaceTask(organizationId, input),
archiveTask: async (input) => backendClient.runAction(organizationId, input.repoId, input.taskId, "archive"),
publishPr: (input) => backendClient.publishWorkspacePr(organizationId, input),
revertFile: (input) => backendClient.revertWorkspaceFile(organizationId, input),
updateDraft: (input) => backendClient.updateWorkspaceDraft(organizationId, input),
sendMessage: (input) => backendClient.sendWorkspaceMessage(organizationId, input),
stopAgent: (input) => backendClient.stopWorkspaceSession(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],
);
@ -1495,7 +1494,7 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
}, [selectedOpenPullRequest, selectedTaskId, tasks]);
const materializeOpenPullRequest = useCallback(
async (pullRequest: WorkbenchOpenPrSummary) => {
async (pullRequest: WorkspaceOpenPrSummary) => {
if (resolvingOpenPullRequestsRef.current.has(pullRequest.prId)) {
return;
}
@ -1504,7 +1503,7 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
setMaterializingOpenPrId(pullRequest.prId);
try {
const { taskId, sessionId } = await taskWorkbenchClient.createTask({
const { taskId, sessionId } = await taskWorkspaceClient.createTask({
repoId: pullRequest.repoId,
task: `Continue work on GitHub PR #${pullRequest.number}: ${pullRequest.title}`,
model: "gpt-5.3-codex",
@ -1534,7 +1533,7 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
);
}
},
[navigate, taskWorkbenchClient, organizationId],
[navigate, taskWorkspaceClient, organizationId],
);
useEffect(() => {
@ -1664,7 +1663,7 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
autoCreatingSessionForTaskRef.current.add(activeTask.id);
void (async () => {
try {
const { sessionId } = await taskWorkbenchClient.addSession({ taskId: activeTask.id });
const { sessionId } = await taskWorkspaceClient.addSession({ taskId: activeTask.id });
syncRouteSession(activeTask.id, sessionId, true);
} catch (error) {
logger.error(
@ -1672,13 +1671,13 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
taskId: activeTask.id,
...createErrorContext(error),
},
"failed_to_auto_create_workbench_session",
"failed_to_auto_create_workspace_session",
);
// Keep the guard in the set on error to prevent retry storms.
// The guard is cleared when sessions appear (line above) or the task changes.
}
})();
}, [activeTask, selectedSessionId, syncRouteSession, taskWorkbenchClient]);
}, [activeTask, selectedSessionId, syncRouteSession, taskWorkspaceClient]);
const createTask = useCallback(
(overrideRepoId?: string, options?: { title?: string; task?: string; branch?: string; onBranch?: string }) => {
@ -1688,7 +1687,7 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
throw new Error("Cannot create a task without an available repo");
}
const { taskId, sessionId } = await taskWorkbenchClient.createTask({
const { taskId, sessionId } = await taskWorkspaceClient.createTask({
repoId,
task: options?.task ?? "New task",
model: "gpt-5.3-codex",
@ -1706,7 +1705,7 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
});
})();
},
[navigate, selectedNewTaskRepoId, taskWorkbenchClient, organizationId],
[navigate, selectedNewTaskRepoId, taskWorkspaceClient, organizationId],
);
const openDiffTab = useCallback(
@ -1757,7 +1756,7 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
);
const markTaskUnread = useCallback((id: string) => {
void taskWorkbenchClient.markTaskUnread({ taskId: id });
void taskWorkspaceClient.markTaskUnread({ taskId: id });
}, []);
const renameTask = useCallback(
@ -1777,29 +1776,7 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
return;
}
void taskWorkbenchClient.renameTask({ taskId: id, value: trimmedTitle });
},
[tasks],
);
const renameBranch = useCallback(
(id: string) => {
const currentTask = tasks.find((task) => task.id === id);
if (!currentTask) {
throw new Error(`Unable to rename missing task ${id}`);
}
const nextBranch = window.prompt("Rename branch", currentTask.branch ?? "");
if (nextBranch === null) {
return;
}
const trimmedBranch = nextBranch.trim();
if (!trimmedBranch) {
return;
}
void taskWorkbenchClient.renameBranch({ taskId: id, value: trimmedBranch });
void taskWorkspaceClient.renameTask({ taskId: id, value: trimmedTitle });
},
[tasks],
);
@ -1808,14 +1785,14 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
if (!activeTask) {
throw new Error("Cannot archive without an active task");
}
void taskWorkbenchClient.archiveTask({ taskId: activeTask.id });
void taskWorkspaceClient.archiveTask({ taskId: activeTask.id });
}, [activeTask]);
const publishPr = useCallback(() => {
if (!activeTask) {
throw new Error("Cannot publish PR without an active task");
}
void taskWorkbenchClient.publishPr({ taskId: activeTask.id });
void taskWorkspaceClient.publishPr({ taskId: activeTask.id });
}, [activeTask]);
const revertFile = useCallback(
@ -1835,7 +1812,7 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
: (current[activeTask.id] ?? null),
}));
void taskWorkbenchClient.revertFile({
void taskWorkspaceClient.revertFile({
taskId: activeTask.id,
path,
});
@ -1939,14 +1916,13 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
onSelectNewTaskRepo={setSelectedNewTaskRepoId}
onMarkUnread={markTaskUnread}
onRenameTask={renameTask}
onRenameBranch={renameBranch}
onReorderRepositories={reorderRepositories}
taskOrderByRepository={taskOrderByRepository}
onReorderTasks={reorderTasks}
onReloadOrganization={() => void taskWorkbenchClient.reloadGithubOrganization()}
onReloadPullRequests={() => void taskWorkbenchClient.reloadGithubPullRequests()}
onReloadRepository={(repoId) => void taskWorkbenchClient.reloadGithubRepository(repoId)}
onReloadPullRequest={(repoId, prNumber) => void taskWorkbenchClient.reloadGithubPullRequest(repoId, prNumber)}
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>
@ -2079,7 +2055,7 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
{showDevPanel && (
<DevPanel
organizationId={organizationId}
snapshot={{ organizationId, repos: organizationRepos, repositories: rawRepositories, tasks } as TaskWorkbenchSnapshot}
snapshot={{ organizationId, repos: organizationRepos, repositories: rawRepositories, tasks } as TaskWorkspaceSnapshot}
organization={activeOrg}
focusedTask={null}
/>
@ -2114,14 +2090,13 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
onSelectNewTaskRepo={setSelectedNewTaskRepoId}
onMarkUnread={markTaskUnread}
onRenameTask={renameTask}
onRenameBranch={renameBranch}
onReorderRepositories={reorderRepositories}
taskOrderByRepository={taskOrderByRepository}
onReorderTasks={reorderTasks}
onReloadOrganization={() => void taskWorkbenchClient.reloadGithubOrganization()}
onReloadPullRequests={() => void taskWorkbenchClient.reloadGithubPullRequests()}
onReloadRepository={(repoId) => void taskWorkbenchClient.reloadGithubRepository(repoId)}
onReloadPullRequest={(repoId, prNumber) => void taskWorkbenchClient.reloadGithubPullRequest(repoId, prNumber)}
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>
@ -2169,14 +2144,13 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
onSelectNewTaskRepo={setSelectedNewTaskRepoId}
onMarkUnread={markTaskUnread}
onRenameTask={renameTask}
onRenameBranch={renameBranch}
onReorderRepositories={reorderRepositories}
taskOrderByRepository={taskOrderByRepository}
onReorderTasks={reorderTasks}
onReloadOrganization={() => void taskWorkbenchClient.reloadGithubOrganization()}
onReloadPullRequests={() => void taskWorkbenchClient.reloadGithubPullRequests()}
onReloadRepository={(repoId) => void taskWorkbenchClient.reloadGithubRepository(repoId)}
onReloadPullRequest={(repoId, prNumber) => void taskWorkbenchClient.reloadGithubPullRequest(repoId, prNumber)}
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);
@ -2189,7 +2163,7 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
{leftSidebarOpen ? <PanelResizeHandle onResizeStart={onLeftResizeStart} onResize={onLeftResize} /> : null}
<div style={{ flex: 1, minWidth: 0, display: "flex", flexDirection: "column" }}>
<TranscriptPanel
taskWorkbenchClient={taskWorkbenchClient}
taskWorkspaceClient={taskWorkspaceClient}
task={activeTask}
hasSandbox={hasSandbox}
activeSessionId={activeSessionId}
@ -2248,7 +2222,7 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
{showDevPanel && (
<DevPanel
organizationId={organizationId}
snapshot={{ organizationId, repos: organizationRepos, repositories: rawRepositories, tasks } as TaskWorkbenchSnapshot}
snapshot={{ organizationId, repos: organizationRepos, repositories: rawRepositories, tasks } as TaskWorkspaceSnapshot}
organization={activeOrg}
focusedTask={{
id: activeTask.id,

View file

@ -68,7 +68,6 @@ export const Sidebar = memo(function Sidebar({
onSelectNewTaskRepo,
onMarkUnread,
onRenameTask,
onRenameBranch,
onReorderRepositories,
taskOrderByRepository,
onReorderTasks,
@ -87,7 +86,6 @@ export const Sidebar = memo(function Sidebar({
onSelectNewTaskRepo: (repoId: string) => void;
onMarkUnread: (id: string) => void;
onRenameTask: (id: string) => void;
onRenameBranch: (id: string) => void;
onReorderRepositories: (fromIndex: number, toIndex: number) => void;
taskOrderByRepository: Record<string, string[]>;
onReorderTasks: (repositoryId: string, fromIndex: number, toIndex: number) => void;
@ -729,7 +727,6 @@ export const Sidebar = memo(function Sidebar({
}
contextMenu.open(event, [
{ label: "Rename task", onClick: () => onRenameTask(task.id) },
{ label: "Rename branch", onClick: () => onRenameBranch(task.id) },
{ label: "Mark as unread", onClick: () => onMarkUnread(task.id) },
]);
}}

View file

@ -30,11 +30,11 @@ export const TranscriptHeader = memo(function TranscriptHeader({
task: Task;
hasSandbox: boolean;
activeSession: AgentSession | null | undefined;
editingField: "title" | "branch" | null;
editingField: "title" | null;
editValue: string;
onEditValueChange: (value: string) => void;
onStartEditingField: (field: "title" | "branch", value: string) => void;
onCommitEditingField: (field: "title" | "branch") => void;
onStartEditingField: (field: "title", value: string) => void;
onCommitEditingField: (field: "title") => void;
onCancelEditingField: () => void;
onSetActiveSessionUnread: (unread: boolean) => void;
sidebarCollapsed?: boolean;
@ -118,55 +118,20 @@ export const TranscriptHeader = memo(function TranscriptHeader({
</LabelSmall>
)}
{task.branch ? (
editingField === "branch" ? (
<input
autoFocus
value={editValue}
onChange={(event) => onEditValueChange(event.target.value)}
onBlur={() => onCommitEditingField("branch")}
onKeyDown={(event) => {
if (event.key === "Enter") {
onCommitEditingField("branch");
} else if (event.key === "Escape") {
onCancelEditingField();
}
}}
className={css({
appearance: "none",
WebkitAppearance: "none",
margin: "0",
outline: "none",
padding: "2px 8px",
borderRadius: "999px",
border: `1px solid ${t.borderFocus}`,
backgroundColor: t.interactiveSubtle,
color: t.textPrimary,
fontSize: "11px",
whiteSpace: "nowrap",
fontFamily: '"IBM Plex Mono", monospace',
minWidth: "60px",
})}
/>
) : (
<span
title="Rename"
onClick={() => onStartEditingField("branch", task.branch ?? "")}
className={css({
padding: "2px 8px",
borderRadius: "999px",
border: `1px solid ${t.borderMedium}`,
backgroundColor: t.interactiveSubtle,
color: t.textPrimary,
fontSize: "11px",
whiteSpace: "nowrap",
fontFamily: '"IBM Plex Mono", monospace',
cursor: "pointer",
":hover": { borderColor: t.borderFocus },
})}
>
{task.branch}
</span>
)
<span
className={css({
padding: "2px 8px",
borderRadius: "999px",
border: `1px solid ${t.borderMedium}`,
backgroundColor: t.interactiveSubtle,
color: t.textPrimary,
fontSize: "11px",
whiteSpace: "nowrap",
fontFamily: '"IBM Plex Mono", monospace',
})}
>
{task.branch}
</span>
) : null}
<HeaderStatusPill status={headerStatus} />
<div className={css({ flex: 1 })} />

View file

@ -1,8 +1,8 @@
import { describe, expect, it } from "vitest";
import type { WorkbenchSession } from "@sandbox-agent/foundry-shared";
import type { WorkspaceSession } from "@sandbox-agent/foundry-shared";
import { buildDisplayMessages } from "./view-model";
function makeSession(transcript: WorkbenchSession["transcript"]): WorkbenchSession {
function makeSession(transcript: WorkspaceSession["transcript"]): WorkspaceSession {
return {
id: "session-1",
sessionId: "session-1",

View file

@ -1,17 +1,17 @@
import type {
WorkbenchAgentKind as AgentKind,
WorkbenchSession as AgentSession,
WorkbenchDiffLineKind as DiffLineKind,
WorkbenchFileChange as FileChange,
WorkbenchFileTreeNode as FileTreeNode,
WorkbenchTask as Task,
WorkbenchHistoryEvent as HistoryEvent,
WorkbenchLineAttachment as LineAttachment,
WorkbenchModelGroup as ModelGroup,
WorkbenchModelId as ModelId,
WorkbenchParsedDiffLine as ParsedDiffLine,
WorkbenchRepositorySection as RepositorySection,
WorkbenchTranscriptEvent as TranscriptEvent,
WorkspaceAgentKind as AgentKind,
WorkspaceSession as AgentSession,
WorkspaceDiffLineKind as DiffLineKind,
WorkspaceFileChange as FileChange,
WorkspaceFileTreeNode as FileTreeNode,
WorkspaceTask as Task,
WorkspaceHistoryEvent as HistoryEvent,
WorkspaceLineAttachment as LineAttachment,
WorkspaceModelGroup as ModelGroup,
WorkspaceModelId as ModelId,
WorkspaceParsedDiffLine as ParsedDiffLine,
WorkspaceRepositorySection as RepositorySection,
WorkspaceTranscriptEvent as TranscriptEvent,
} from "@sandbox-agent/foundry-shared";
import { extractEventText } from "../../features/sessions/model";

View file

@ -1,5 +1,5 @@
import { useEffect, useMemo, useState, type ReactNode } from "react";
import type { AgentType, RepoBranchRecord, RepoOverview, TaskWorkbenchSnapshot, WorkbenchTaskStatus } from "@sandbox-agent/foundry-shared";
import type { AgentType, 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";
@ -100,7 +100,7 @@ const AGENT_OPTIONS: SelectItem[] = [
{ id: "claude", label: "claude" },
];
function statusKind(status: WorkbenchTaskStatus): StatusTagKind {
function statusKind(status: WorkspaceTaskStatus): StatusTagKind {
if (status === "running") return "positive";
if (status === "error") return "negative";
if (status === "new" || String(status).startsWith("init_")) return "warning";
@ -515,7 +515,7 @@ export function OrganizationDashboard({ organizationId, selectedTaskId, selected
};
}, [repoOverviewMode, selectedForSession, selectedSummary]);
const devPanelSnapshot = useMemo(
(): TaskWorkbenchSnapshot => ({
(): TaskWorkspaceSnapshot => ({
organizationId,
repos: repos.map((repo) => ({ id: repo.id, label: repo.label })),
repositories: [],