mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-16 17:01:06 +00:00
feat(foundry): checkpoint actor and workspace refactor
This commit is contained in:
parent
32f3c6c3bc
commit
dbe57d45b9
81 changed files with 3441 additions and 2332 deletions
|
|
@ -10,8 +10,8 @@
|
|||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"test:e2e:full": "HF_ENABLE_DAEMON_FULL_E2E=1 vitest run test/e2e/full-integration-e2e.test.ts",
|
||||
"test:e2e:workbench": "HF_ENABLE_DAEMON_WORKBENCH_E2E=1 vitest run test/e2e/workbench-e2e.test.ts",
|
||||
"test:e2e:workbench-load": "HF_ENABLE_DAEMON_WORKBENCH_LOAD_E2E=1 vitest run test/e2e/workbench-load-e2e.test.ts"
|
||||
"test:e2e:workspace": "HF_ENABLE_DAEMON_WORKBENCH_E2E=1 vitest run test/e2e/workspace-e2e.test.ts",
|
||||
"test:e2e:workspace-load": "HF_ENABLE_DAEMON_WORKBENCH_LOAD_E2E=1 vitest run test/e2e/workspace-load-e2e.test.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sandbox-agent/foundry-shared": "workspace:*",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import type {
|
|||
FoundryOrganization,
|
||||
FoundryUser,
|
||||
UpdateFoundryOrganizationProfileInput,
|
||||
WorkspaceModelId,
|
||||
} from "@sandbox-agent/foundry-shared";
|
||||
import type { BackendClient } from "./backend-client.js";
|
||||
import { getMockFoundryAppClient } from "./mock-app.js";
|
||||
|
|
@ -17,6 +18,7 @@ export interface FoundryAppClient {
|
|||
skipStarterRepo(): Promise<void>;
|
||||
starStarterRepo(organizationId: string): Promise<void>;
|
||||
selectOrganization(organizationId: string): Promise<void>;
|
||||
setDefaultModel(model: WorkspaceModelId): Promise<void>;
|
||||
updateOrganizationProfile(input: UpdateFoundryOrganizationProfileInput): Promise<void>;
|
||||
triggerGithubSync(organizationId: string): Promise<void>;
|
||||
completeHostedCheckout(organizationId: string, planId: FoundryBillingPlanId): Promise<void>;
|
||||
|
|
|
|||
|
|
@ -10,25 +10,25 @@ import type {
|
|||
SandboxProcessesEvent,
|
||||
TaskRecord,
|
||||
TaskSummary,
|
||||
TaskWorkbenchChangeModelInput,
|
||||
TaskWorkbenchCreateTaskInput,
|
||||
TaskWorkbenchCreateTaskResponse,
|
||||
TaskWorkbenchDiffInput,
|
||||
TaskWorkbenchRenameInput,
|
||||
TaskWorkbenchRenameSessionInput,
|
||||
TaskWorkbenchSelectInput,
|
||||
TaskWorkbenchSetSessionUnreadInput,
|
||||
TaskWorkbenchSendMessageInput,
|
||||
TaskWorkbenchSnapshot,
|
||||
TaskWorkbenchSessionInput,
|
||||
TaskWorkbenchUpdateDraftInput,
|
||||
TaskWorkspaceChangeModelInput,
|
||||
TaskWorkspaceCreateTaskInput,
|
||||
TaskWorkspaceCreateTaskResponse,
|
||||
TaskWorkspaceDiffInput,
|
||||
TaskWorkspaceRenameInput,
|
||||
TaskWorkspaceRenameSessionInput,
|
||||
TaskWorkspaceSelectInput,
|
||||
TaskWorkspaceSetSessionUnreadInput,
|
||||
TaskWorkspaceSendMessageInput,
|
||||
TaskWorkspaceSnapshot,
|
||||
TaskWorkspaceSessionInput,
|
||||
TaskWorkspaceUpdateDraftInput,
|
||||
TaskEvent,
|
||||
WorkbenchTaskDetail,
|
||||
WorkbenchTaskSummary,
|
||||
WorkbenchSessionDetail,
|
||||
WorkspaceTaskDetail,
|
||||
WorkspaceTaskSummary,
|
||||
WorkspaceSessionDetail,
|
||||
OrganizationEvent,
|
||||
OrganizationSummarySnapshot,
|
||||
HistoryEvent,
|
||||
AuditLogEvent as HistoryEvent,
|
||||
HistoryQueryInput,
|
||||
SandboxProviderId,
|
||||
RepoOverview,
|
||||
|
|
@ -37,6 +37,7 @@ import type {
|
|||
StarSandboxAgentRepoResult,
|
||||
SwitchResult,
|
||||
UpdateFoundryOrganizationProfileInput,
|
||||
WorkspaceModelId,
|
||||
} from "@sandbox-agent/foundry-shared";
|
||||
import type { ProcessCreateRequest, ProcessInfo, ProcessLogFollowQuery, ProcessLogsResponse, ProcessSignalQuery } from "sandbox-agent";
|
||||
import { createMockBackendClient } from "./mock/backend-client.js";
|
||||
|
|
@ -78,39 +79,36 @@ interface OrganizationHandle {
|
|||
createTask(input: CreateTaskInput): Promise<TaskRecord>;
|
||||
listTasks(input: { organizationId: string; repoId?: string }): Promise<TaskSummary[]>;
|
||||
getRepoOverview(input: { organizationId: string; repoId: string }): Promise<RepoOverview>;
|
||||
history(input: HistoryQueryInput): Promise<HistoryEvent[]>;
|
||||
switchTask(taskId: string): Promise<SwitchResult>;
|
||||
getTask(input: { organizationId: string; taskId: string }): Promise<TaskRecord>;
|
||||
attachTask(input: { organizationId: string; taskId: string; reason?: string }): Promise<{ target: string; sessionId: string | null }>;
|
||||
pushTask(input: { organizationId: string; taskId: string; reason?: string }): Promise<void>;
|
||||
syncTask(input: { organizationId: string; taskId: string; reason?: string }): Promise<void>;
|
||||
mergeTask(input: { organizationId: string; taskId: string; reason?: string }): Promise<void>;
|
||||
archiveTask(input: { organizationId: string; taskId: string; reason?: string }): Promise<void>;
|
||||
killTask(input: { organizationId: string; taskId: string; reason?: string }): Promise<void>;
|
||||
auditLog(input: HistoryQueryInput): Promise<HistoryEvent[]>;
|
||||
switchTask(input: { repoId: string; taskId: string }): Promise<SwitchResult>;
|
||||
getTask(input: { organizationId: string; repoId: string; taskId: string }): Promise<TaskRecord>;
|
||||
attachTask(input: { organizationId: string; repoId: string; taskId: string; reason?: string }): Promise<{ target: string; sessionId: string | null }>;
|
||||
pushTask(input: { organizationId: string; repoId: string; taskId: string; reason?: string }): Promise<void>;
|
||||
syncTask(input: { organizationId: string; repoId: string; taskId: string; reason?: string }): Promise<void>;
|
||||
mergeTask(input: { organizationId: string; repoId: string; taskId: string; reason?: string }): Promise<void>;
|
||||
archiveTask(input: { organizationId: string; repoId: string; taskId: string; reason?: string }): Promise<void>;
|
||||
killTask(input: { organizationId: string; repoId: string; taskId: string; reason?: string }): Promise<void>;
|
||||
useOrganization(input: { organizationId: string }): Promise<{ organizationId: string }>;
|
||||
starSandboxAgentRepo(input: StarSandboxAgentRepoInput): Promise<StarSandboxAgentRepoResult>;
|
||||
getOrganizationSummary(input: { organizationId: string }): Promise<OrganizationSummarySnapshot>;
|
||||
applyTaskSummaryUpdate(input: { taskSummary: WorkbenchTaskSummary }): Promise<void>;
|
||||
removeTaskSummary(input: { taskId: string }): Promise<void>;
|
||||
reconcileWorkbenchState(input: { organizationId: string }): Promise<OrganizationSummarySnapshot>;
|
||||
createWorkbenchTask(input: TaskWorkbenchCreateTaskInput): Promise<TaskWorkbenchCreateTaskResponse>;
|
||||
markWorkbenchUnread(input: TaskWorkbenchSelectInput): Promise<void>;
|
||||
renameWorkbenchTask(input: TaskWorkbenchRenameInput): Promise<void>;
|
||||
renameWorkbenchBranch(input: TaskWorkbenchRenameInput): Promise<void>;
|
||||
createWorkbenchSession(input: TaskWorkbenchSelectInput & { model?: string }): Promise<{ sessionId: string }>;
|
||||
renameWorkbenchSession(input: TaskWorkbenchRenameSessionInput): Promise<void>;
|
||||
setWorkbenchSessionUnread(input: TaskWorkbenchSetSessionUnreadInput): Promise<void>;
|
||||
updateWorkbenchDraft(input: TaskWorkbenchUpdateDraftInput): Promise<void>;
|
||||
changeWorkbenchModel(input: TaskWorkbenchChangeModelInput): Promise<void>;
|
||||
sendWorkbenchMessage(input: TaskWorkbenchSendMessageInput): Promise<void>;
|
||||
stopWorkbenchSession(input: TaskWorkbenchSessionInput): Promise<void>;
|
||||
closeWorkbenchSession(input: TaskWorkbenchSessionInput): Promise<void>;
|
||||
publishWorkbenchPr(input: TaskWorkbenchSelectInput): Promise<void>;
|
||||
revertWorkbenchFile(input: TaskWorkbenchDiffInput): Promise<void>;
|
||||
reloadGithubOrganization(): Promise<void>;
|
||||
reloadGithubPullRequests(): Promise<void>;
|
||||
reloadGithubRepository(input: { repoId: string }): Promise<void>;
|
||||
reloadGithubPullRequest(input: { repoId: string; prNumber: number }): Promise<void>;
|
||||
adminReconcileWorkspaceState(input: { organizationId: string }): Promise<OrganizationSummarySnapshot>;
|
||||
createWorkspaceTask(input: TaskWorkspaceCreateTaskInput): Promise<TaskWorkspaceCreateTaskResponse>;
|
||||
markWorkspaceUnread(input: TaskWorkspaceSelectInput): Promise<void>;
|
||||
renameWorkspaceTask(input: TaskWorkspaceRenameInput): Promise<void>;
|
||||
createWorkspaceSession(input: TaskWorkspaceSelectInput & { model?: string }): Promise<{ sessionId: string }>;
|
||||
renameWorkspaceSession(input: TaskWorkspaceRenameSessionInput): Promise<void>;
|
||||
setWorkspaceSessionUnread(input: TaskWorkspaceSetSessionUnreadInput): Promise<void>;
|
||||
updateWorkspaceDraft(input: TaskWorkspaceUpdateDraftInput): Promise<void>;
|
||||
changeWorkspaceModel(input: TaskWorkspaceChangeModelInput): Promise<void>;
|
||||
sendWorkspaceMessage(input: TaskWorkspaceSendMessageInput): Promise<void>;
|
||||
stopWorkspaceSession(input: TaskWorkspaceSessionInput): Promise<void>;
|
||||
closeWorkspaceSession(input: TaskWorkspaceSessionInput): Promise<void>;
|
||||
publishWorkspacePr(input: TaskWorkspaceSelectInput): Promise<void>;
|
||||
revertWorkspaceFile(input: TaskWorkspaceDiffInput): Promise<void>;
|
||||
adminReloadGithubOrganization(): Promise<void>;
|
||||
adminReloadGithubPullRequests(): Promise<void>;
|
||||
adminReloadGithubRepository(input: { repoId: string }): Promise<void>;
|
||||
adminReloadGithubPullRequest(input: { repoId: string; prNumber: number }): Promise<void>;
|
||||
}
|
||||
|
||||
interface AppOrganizationHandle {
|
||||
|
|
@ -119,6 +117,7 @@ interface AppOrganizationHandle {
|
|||
skipAppStarterRepo(input: { sessionId: string }): Promise<FoundryAppSnapshot>;
|
||||
starAppStarterRepo(input: { sessionId: string; organizationId: string }): Promise<FoundryAppSnapshot>;
|
||||
selectAppOrganization(input: { sessionId: string; organizationId: string }): Promise<FoundryAppSnapshot>;
|
||||
setAppDefaultModel(input: { sessionId: string; defaultModel: WorkspaceModelId }): Promise<FoundryAppSnapshot>;
|
||||
updateAppOrganizationProfile(input: UpdateFoundryOrganizationProfileInput & { sessionId: string }): Promise<FoundryAppSnapshot>;
|
||||
triggerAppRepoImport(input: { sessionId: string; organizationId: string }): Promise<FoundryAppSnapshot>;
|
||||
beginAppGithubInstall(input: { sessionId: string; organizationId: string }): Promise<{ url: string }>;
|
||||
|
|
@ -130,9 +129,9 @@ interface AppOrganizationHandle {
|
|||
}
|
||||
|
||||
interface TaskHandle {
|
||||
getTaskSummary(): Promise<WorkbenchTaskSummary>;
|
||||
getTaskDetail(): Promise<WorkbenchTaskDetail>;
|
||||
getSessionDetail(input: { sessionId: string }): Promise<WorkbenchSessionDetail>;
|
||||
getTaskSummary(): Promise<WorkspaceTaskSummary>;
|
||||
getTaskDetail(): Promise<WorkspaceTaskDetail>;
|
||||
getSessionDetail(input: { sessionId: string }): Promise<WorkspaceSessionDetail>;
|
||||
connect(): ActorConn;
|
||||
}
|
||||
|
||||
|
|
@ -192,6 +191,7 @@ export interface BackendClient {
|
|||
skipAppStarterRepo(): Promise<FoundryAppSnapshot>;
|
||||
starAppStarterRepo(organizationId: string): Promise<FoundryAppSnapshot>;
|
||||
selectAppOrganization(organizationId: string): Promise<FoundryAppSnapshot>;
|
||||
setAppDefaultModel(defaultModel: WorkspaceModelId): Promise<FoundryAppSnapshot>;
|
||||
updateAppOrganizationProfile(input: UpdateFoundryOrganizationProfileInput): Promise<FoundryAppSnapshot>;
|
||||
triggerAppRepoImport(organizationId: string): Promise<FoundryAppSnapshot>;
|
||||
reconnectAppGithub(organizationId: string): Promise<void>;
|
||||
|
|
@ -204,11 +204,11 @@ export interface BackendClient {
|
|||
createTask(input: CreateTaskInput): Promise<TaskRecord>;
|
||||
listTasks(organizationId: string, repoId?: string): Promise<TaskSummary[]>;
|
||||
getRepoOverview(organizationId: string, repoId: string): Promise<RepoOverview>;
|
||||
getTask(organizationId: string, taskId: string): Promise<TaskRecord>;
|
||||
getTask(organizationId: string, repoId: string, taskId: string): Promise<TaskRecord>;
|
||||
listHistory(input: HistoryQueryInput): Promise<HistoryEvent[]>;
|
||||
switchTask(organizationId: string, taskId: string): Promise<SwitchResult>;
|
||||
attachTask(organizationId: string, taskId: string): Promise<{ target: string; sessionId: string | null }>;
|
||||
runAction(organizationId: string, taskId: string, action: TaskAction): Promise<void>;
|
||||
switchTask(organizationId: string, repoId: string, taskId: string): Promise<SwitchResult>;
|
||||
attachTask(organizationId: string, repoId: string, taskId: string): Promise<{ target: string; sessionId: string | null }>;
|
||||
runAction(organizationId: string, repoId: string, taskId: string, action: TaskAction): Promise<void>;
|
||||
createSandboxSession(input: {
|
||||
organizationId: string;
|
||||
sandboxProviderId: SandboxProviderId;
|
||||
|
|
@ -280,28 +280,27 @@ export interface BackendClient {
|
|||
): Promise<{ sandboxProviderId: SandboxProviderId; sandboxId: string; state: string; at: number }>;
|
||||
getSandboxAgentConnection(organizationId: string, sandboxProviderId: SandboxProviderId, sandboxId: string): Promise<{ endpoint: string; token?: string }>;
|
||||
getOrganizationSummary(organizationId: string): Promise<OrganizationSummarySnapshot>;
|
||||
getTaskDetail(organizationId: string, repoId: string, taskId: string): Promise<WorkbenchTaskDetail>;
|
||||
getSessionDetail(organizationId: string, repoId: string, taskId: string, sessionId: string): Promise<WorkbenchSessionDetail>;
|
||||
getWorkbench(organizationId: string): Promise<TaskWorkbenchSnapshot>;
|
||||
subscribeWorkbench(organizationId: string, listener: () => void): () => void;
|
||||
createWorkbenchTask(organizationId: string, input: TaskWorkbenchCreateTaskInput): Promise<TaskWorkbenchCreateTaskResponse>;
|
||||
markWorkbenchUnread(organizationId: string, input: TaskWorkbenchSelectInput): Promise<void>;
|
||||
renameWorkbenchTask(organizationId: string, input: TaskWorkbenchRenameInput): Promise<void>;
|
||||
renameWorkbenchBranch(organizationId: string, input: TaskWorkbenchRenameInput): Promise<void>;
|
||||
createWorkbenchSession(organizationId: string, input: TaskWorkbenchSelectInput & { model?: string }): Promise<{ sessionId: string }>;
|
||||
renameWorkbenchSession(organizationId: string, input: TaskWorkbenchRenameSessionInput): Promise<void>;
|
||||
setWorkbenchSessionUnread(organizationId: string, input: TaskWorkbenchSetSessionUnreadInput): Promise<void>;
|
||||
updateWorkbenchDraft(organizationId: string, input: TaskWorkbenchUpdateDraftInput): Promise<void>;
|
||||
changeWorkbenchModel(organizationId: string, input: TaskWorkbenchChangeModelInput): Promise<void>;
|
||||
sendWorkbenchMessage(organizationId: string, input: TaskWorkbenchSendMessageInput): Promise<void>;
|
||||
stopWorkbenchSession(organizationId: string, input: TaskWorkbenchSessionInput): Promise<void>;
|
||||
closeWorkbenchSession(organizationId: string, input: TaskWorkbenchSessionInput): Promise<void>;
|
||||
publishWorkbenchPr(organizationId: string, input: TaskWorkbenchSelectInput): Promise<void>;
|
||||
revertWorkbenchFile(organizationId: string, input: TaskWorkbenchDiffInput): Promise<void>;
|
||||
reloadGithubOrganization(organizationId: string): Promise<void>;
|
||||
reloadGithubPullRequests(organizationId: string): Promise<void>;
|
||||
reloadGithubRepository(organizationId: string, repoId: string): Promise<void>;
|
||||
reloadGithubPullRequest(organizationId: string, repoId: string, prNumber: number): Promise<void>;
|
||||
getTaskDetail(organizationId: string, repoId: string, taskId: string): Promise<WorkspaceTaskDetail>;
|
||||
getSessionDetail(organizationId: string, repoId: string, taskId: string, sessionId: string): Promise<WorkspaceSessionDetail>;
|
||||
getWorkspace(organizationId: string): Promise<TaskWorkspaceSnapshot>;
|
||||
subscribeWorkspace(organizationId: string, listener: () => void): () => void;
|
||||
createWorkspaceTask(organizationId: string, input: TaskWorkspaceCreateTaskInput): Promise<TaskWorkspaceCreateTaskResponse>;
|
||||
markWorkspaceUnread(organizationId: string, input: TaskWorkspaceSelectInput): Promise<void>;
|
||||
renameWorkspaceTask(organizationId: string, input: TaskWorkspaceRenameInput): Promise<void>;
|
||||
createWorkspaceSession(organizationId: string, input: TaskWorkspaceSelectInput & { model?: string }): Promise<{ sessionId: string }>;
|
||||
renameWorkspaceSession(organizationId: string, input: TaskWorkspaceRenameSessionInput): Promise<void>;
|
||||
setWorkspaceSessionUnread(organizationId: string, input: TaskWorkspaceSetSessionUnreadInput): Promise<void>;
|
||||
updateWorkspaceDraft(organizationId: string, input: TaskWorkspaceUpdateDraftInput): Promise<void>;
|
||||
changeWorkspaceModel(organizationId: string, input: TaskWorkspaceChangeModelInput): Promise<void>;
|
||||
sendWorkspaceMessage(organizationId: string, input: TaskWorkspaceSendMessageInput): Promise<void>;
|
||||
stopWorkspaceSession(organizationId: string, input: TaskWorkspaceSessionInput): Promise<void>;
|
||||
closeWorkspaceSession(organizationId: string, input: TaskWorkspaceSessionInput): Promise<void>;
|
||||
publishWorkspacePr(organizationId: string, input: TaskWorkspaceSelectInput): Promise<void>;
|
||||
revertWorkspaceFile(organizationId: string, input: TaskWorkspaceDiffInput): Promise<void>;
|
||||
adminReloadGithubOrganization(organizationId: string): Promise<void>;
|
||||
adminReloadGithubPullRequests(organizationId: string): Promise<void>;
|
||||
adminReloadGithubRepository(organizationId: string, repoId: string): Promise<void>;
|
||||
adminReloadGithubPullRequest(organizationId: string, repoId: string, prNumber: number): Promise<void>;
|
||||
health(): Promise<{ ok: true }>;
|
||||
useOrganization(organizationId: string): Promise<{ organizationId: string }>;
|
||||
starSandboxAgentRepo(organizationId: string): Promise<StarSandboxAgentRepoResult>;
|
||||
|
|
@ -410,7 +409,7 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
const rivetApiEndpoint = endpoints.rivetEndpoint;
|
||||
const appApiEndpoint = endpoints.appEndpoint;
|
||||
const client = createClient({ endpoint: rivetApiEndpoint }) as unknown as RivetClient;
|
||||
const workbenchSubscriptions = new Map<
|
||||
const workspaceSubscriptions = new Map<
|
||||
string,
|
||||
{
|
||||
listeners: Set<() => void>;
|
||||
|
|
@ -563,7 +562,7 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
}
|
||||
};
|
||||
|
||||
const getWorkbenchCompat = async (organizationId: string): Promise<TaskWorkbenchSnapshot> => {
|
||||
const getWorkspaceCompat = async (organizationId: string): Promise<TaskWorkspaceSnapshot> => {
|
||||
const summary = await (await organization(organizationId)).getOrganizationSummary({ organizationId });
|
||||
const tasks = (
|
||||
await Promise.all(
|
||||
|
|
@ -590,7 +589,7 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
}
|
||||
}),
|
||||
);
|
||||
const sessionDetailsById = new Map(sessionDetails.filter((entry): entry is readonly [string, WorkbenchSessionDetail] => entry !== null));
|
||||
const sessionDetailsById = new Map(sessionDetails.filter((entry): entry is readonly [string, WorkspaceSessionDetail] => entry !== null));
|
||||
return {
|
||||
id: detail.id,
|
||||
repoId: detail.repoId,
|
||||
|
|
@ -623,7 +622,7 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
};
|
||||
}),
|
||||
)
|
||||
).filter((task): task is TaskWorkbenchSnapshot["tasks"][number] => task !== null);
|
||||
).filter((task): task is TaskWorkspaceSnapshot["tasks"][number] => task !== null);
|
||||
|
||||
const repositories = summary.repos
|
||||
.map((repo) => ({
|
||||
|
|
@ -642,14 +641,14 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
};
|
||||
};
|
||||
|
||||
const subscribeWorkbench = (organizationId: string, listener: () => void): (() => void) => {
|
||||
let entry = workbenchSubscriptions.get(organizationId);
|
||||
const subscribeWorkspace = (organizationId: string, listener: () => void): (() => void) => {
|
||||
let entry = workspaceSubscriptions.get(organizationId);
|
||||
if (!entry) {
|
||||
entry = {
|
||||
listeners: new Set(),
|
||||
disposeConnPromise: null,
|
||||
};
|
||||
workbenchSubscriptions.set(organizationId, entry);
|
||||
workspaceSubscriptions.set(organizationId, entry);
|
||||
}
|
||||
|
||||
entry.listeners.add(listener);
|
||||
|
|
@ -658,8 +657,8 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
entry.disposeConnPromise = (async () => {
|
||||
const handle = await organization(organizationId);
|
||||
const conn = (handle as any).connect();
|
||||
const unsubscribeEvent = conn.on("workbenchUpdated", () => {
|
||||
const current = workbenchSubscriptions.get(organizationId);
|
||||
const unsubscribeEvent = conn.on("organizationUpdated", () => {
|
||||
const current = workspaceSubscriptions.get(organizationId);
|
||||
if (!current) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -677,7 +676,7 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
}
|
||||
|
||||
return () => {
|
||||
const current = workbenchSubscriptions.get(organizationId);
|
||||
const current = workspaceSubscriptions.get(organizationId);
|
||||
if (!current) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -686,7 +685,7 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
return;
|
||||
}
|
||||
|
||||
workbenchSubscriptions.delete(organizationId);
|
||||
workspaceSubscriptions.delete(organizationId);
|
||||
void current.disposeConnPromise?.then(async (disposeConn) => {
|
||||
await disposeConn?.();
|
||||
});
|
||||
|
|
@ -849,6 +848,14 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
return await (await appOrganization()).selectAppOrganization({ sessionId, organizationId });
|
||||
},
|
||||
|
||||
async setAppDefaultModel(defaultModel: WorkspaceModelId): Promise<FoundryAppSnapshot> {
|
||||
const sessionId = await getSessionId();
|
||||
if (!sessionId) {
|
||||
throw new Error("No active auth session");
|
||||
}
|
||||
return await (await appOrganization()).setAppDefaultModel({ sessionId, defaultModel });
|
||||
},
|
||||
|
||||
async updateAppOrganizationProfile(input: UpdateFoundryOrganizationProfileInput): Promise<FoundryAppSnapshot> {
|
||||
const sessionId = await getSessionId();
|
||||
if (!sessionId) {
|
||||
|
|
@ -948,33 +955,36 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
return (await organization(organizationId)).getRepoOverview({ organizationId, repoId });
|
||||
},
|
||||
|
||||
async getTask(organizationId: string, taskId: string): Promise<TaskRecord> {
|
||||
async getTask(organizationId: string, repoId: string, taskId: string): Promise<TaskRecord> {
|
||||
return (await organization(organizationId)).getTask({
|
||||
organizationId,
|
||||
repoId,
|
||||
taskId,
|
||||
});
|
||||
},
|
||||
|
||||
async listHistory(input: HistoryQueryInput): Promise<HistoryEvent[]> {
|
||||
return (await organization(input.organizationId)).history(input);
|
||||
return (await organization(input.organizationId)).auditLog(input);
|
||||
},
|
||||
|
||||
async switchTask(organizationId: string, taskId: string): Promise<SwitchResult> {
|
||||
return (await organization(organizationId)).switchTask(taskId);
|
||||
async switchTask(organizationId: string, repoId: string, taskId: string): Promise<SwitchResult> {
|
||||
return (await organization(organizationId)).switchTask({ repoId, taskId });
|
||||
},
|
||||
|
||||
async attachTask(organizationId: string, taskId: string): Promise<{ target: string; sessionId: string | null }> {
|
||||
async attachTask(organizationId: string, repoId: string, taskId: string): Promise<{ target: string; sessionId: string | null }> {
|
||||
return (await organization(organizationId)).attachTask({
|
||||
organizationId,
|
||||
repoId,
|
||||
taskId,
|
||||
reason: "cli.attach",
|
||||
});
|
||||
},
|
||||
|
||||
async runAction(organizationId: string, taskId: string, action: TaskAction): Promise<void> {
|
||||
async runAction(organizationId: string, repoId: string, taskId: string, action: TaskAction): Promise<void> {
|
||||
if (action === "push") {
|
||||
await (await organization(organizationId)).pushTask({
|
||||
organizationId,
|
||||
repoId,
|
||||
taskId,
|
||||
reason: "cli.push",
|
||||
});
|
||||
|
|
@ -983,6 +993,7 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
if (action === "sync") {
|
||||
await (await organization(organizationId)).syncTask({
|
||||
organizationId,
|
||||
repoId,
|
||||
taskId,
|
||||
reason: "cli.sync",
|
||||
});
|
||||
|
|
@ -991,6 +1002,7 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
if (action === "merge") {
|
||||
await (await organization(organizationId)).mergeTask({
|
||||
organizationId,
|
||||
repoId,
|
||||
taskId,
|
||||
reason: "cli.merge",
|
||||
});
|
||||
|
|
@ -999,6 +1011,7 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
if (action === "archive") {
|
||||
await (await organization(organizationId)).archiveTask({
|
||||
organizationId,
|
||||
repoId,
|
||||
taskId,
|
||||
reason: "cli.archive",
|
||||
});
|
||||
|
|
@ -1006,6 +1019,7 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
}
|
||||
await (await organization(organizationId)).killTask({
|
||||
organizationId,
|
||||
repoId,
|
||||
taskId,
|
||||
reason: "cli.kill",
|
||||
});
|
||||
|
|
@ -1160,92 +1174,88 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
return (await organization(organizationId)).getOrganizationSummary({ organizationId });
|
||||
},
|
||||
|
||||
async getTaskDetail(organizationId: string, repoId: string, taskIdValue: string): Promise<WorkbenchTaskDetail> {
|
||||
async getTaskDetail(organizationId: string, repoId: string, taskIdValue: string): Promise<WorkspaceTaskDetail> {
|
||||
return (await task(organizationId, repoId, taskIdValue)).getTaskDetail();
|
||||
},
|
||||
|
||||
async getSessionDetail(organizationId: string, repoId: string, taskIdValue: string, sessionId: string): Promise<WorkbenchSessionDetail> {
|
||||
async getSessionDetail(organizationId: string, repoId: string, taskIdValue: string, sessionId: string): Promise<WorkspaceSessionDetail> {
|
||||
return (await task(organizationId, repoId, taskIdValue)).getSessionDetail({ sessionId });
|
||||
},
|
||||
|
||||
async getWorkbench(organizationId: string): Promise<TaskWorkbenchSnapshot> {
|
||||
return await getWorkbenchCompat(organizationId);
|
||||
async getWorkspace(organizationId: string): Promise<TaskWorkspaceSnapshot> {
|
||||
return await getWorkspaceCompat(organizationId);
|
||||
},
|
||||
|
||||
subscribeWorkbench(organizationId: string, listener: () => void): () => void {
|
||||
return subscribeWorkbench(organizationId, listener);
|
||||
subscribeWorkspace(organizationId: string, listener: () => void): () => void {
|
||||
return subscribeWorkspace(organizationId, listener);
|
||||
},
|
||||
|
||||
async createWorkbenchTask(organizationId: string, input: TaskWorkbenchCreateTaskInput): Promise<TaskWorkbenchCreateTaskResponse> {
|
||||
return (await organization(organizationId)).createWorkbenchTask(input);
|
||||
async createWorkspaceTask(organizationId: string, input: TaskWorkspaceCreateTaskInput): Promise<TaskWorkspaceCreateTaskResponse> {
|
||||
return (await organization(organizationId)).createWorkspaceTask(input);
|
||||
},
|
||||
|
||||
async markWorkbenchUnread(organizationId: string, input: TaskWorkbenchSelectInput): Promise<void> {
|
||||
await (await organization(organizationId)).markWorkbenchUnread(input);
|
||||
async markWorkspaceUnread(organizationId: string, input: TaskWorkspaceSelectInput): Promise<void> {
|
||||
await (await organization(organizationId)).markWorkspaceUnread(input);
|
||||
},
|
||||
|
||||
async renameWorkbenchTask(organizationId: string, input: TaskWorkbenchRenameInput): Promise<void> {
|
||||
await (await organization(organizationId)).renameWorkbenchTask(input);
|
||||
async renameWorkspaceTask(organizationId: string, input: TaskWorkspaceRenameInput): Promise<void> {
|
||||
await (await organization(organizationId)).renameWorkspaceTask(input);
|
||||
},
|
||||
|
||||
async renameWorkbenchBranch(organizationId: string, input: TaskWorkbenchRenameInput): Promise<void> {
|
||||
await (await organization(organizationId)).renameWorkbenchBranch(input);
|
||||
async createWorkspaceSession(organizationId: string, input: TaskWorkspaceSelectInput & { model?: string }): Promise<{ sessionId: string }> {
|
||||
return await (await organization(organizationId)).createWorkspaceSession(input);
|
||||
},
|
||||
|
||||
async createWorkbenchSession(organizationId: string, input: TaskWorkbenchSelectInput & { model?: string }): Promise<{ sessionId: string }> {
|
||||
return await (await organization(organizationId)).createWorkbenchSession(input);
|
||||
async renameWorkspaceSession(organizationId: string, input: TaskWorkspaceRenameSessionInput): Promise<void> {
|
||||
await (await organization(organizationId)).renameWorkspaceSession(input);
|
||||
},
|
||||
|
||||
async renameWorkbenchSession(organizationId: string, input: TaskWorkbenchRenameSessionInput): Promise<void> {
|
||||
await (await organization(organizationId)).renameWorkbenchSession(input);
|
||||
async setWorkspaceSessionUnread(organizationId: string, input: TaskWorkspaceSetSessionUnreadInput): Promise<void> {
|
||||
await (await organization(organizationId)).setWorkspaceSessionUnread(input);
|
||||
},
|
||||
|
||||
async setWorkbenchSessionUnread(organizationId: string, input: TaskWorkbenchSetSessionUnreadInput): Promise<void> {
|
||||
await (await organization(organizationId)).setWorkbenchSessionUnread(input);
|
||||
async updateWorkspaceDraft(organizationId: string, input: TaskWorkspaceUpdateDraftInput): Promise<void> {
|
||||
await (await organization(organizationId)).updateWorkspaceDraft(input);
|
||||
},
|
||||
|
||||
async updateWorkbenchDraft(organizationId: string, input: TaskWorkbenchUpdateDraftInput): Promise<void> {
|
||||
await (await organization(organizationId)).updateWorkbenchDraft(input);
|
||||
async changeWorkspaceModel(organizationId: string, input: TaskWorkspaceChangeModelInput): Promise<void> {
|
||||
await (await organization(organizationId)).changeWorkspaceModel(input);
|
||||
},
|
||||
|
||||
async changeWorkbenchModel(organizationId: string, input: TaskWorkbenchChangeModelInput): Promise<void> {
|
||||
await (await organization(organizationId)).changeWorkbenchModel(input);
|
||||
async sendWorkspaceMessage(organizationId: string, input: TaskWorkspaceSendMessageInput): Promise<void> {
|
||||
await (await organization(organizationId)).sendWorkspaceMessage(input);
|
||||
},
|
||||
|
||||
async sendWorkbenchMessage(organizationId: string, input: TaskWorkbenchSendMessageInput): Promise<void> {
|
||||
await (await organization(organizationId)).sendWorkbenchMessage(input);
|
||||
async stopWorkspaceSession(organizationId: string, input: TaskWorkspaceSessionInput): Promise<void> {
|
||||
await (await organization(organizationId)).stopWorkspaceSession(input);
|
||||
},
|
||||
|
||||
async stopWorkbenchSession(organizationId: string, input: TaskWorkbenchSessionInput): Promise<void> {
|
||||
await (await organization(organizationId)).stopWorkbenchSession(input);
|
||||
async closeWorkspaceSession(organizationId: string, input: TaskWorkspaceSessionInput): Promise<void> {
|
||||
await (await organization(organizationId)).closeWorkspaceSession(input);
|
||||
},
|
||||
|
||||
async closeWorkbenchSession(organizationId: string, input: TaskWorkbenchSessionInput): Promise<void> {
|
||||
await (await organization(organizationId)).closeWorkbenchSession(input);
|
||||
async publishWorkspacePr(organizationId: string, input: TaskWorkspaceSelectInput): Promise<void> {
|
||||
await (await organization(organizationId)).publishWorkspacePr(input);
|
||||
},
|
||||
|
||||
async publishWorkbenchPr(organizationId: string, input: TaskWorkbenchSelectInput): Promise<void> {
|
||||
await (await organization(organizationId)).publishWorkbenchPr(input);
|
||||
async revertWorkspaceFile(organizationId: string, input: TaskWorkspaceDiffInput): Promise<void> {
|
||||
await (await organization(organizationId)).revertWorkspaceFile(input);
|
||||
},
|
||||
|
||||
async revertWorkbenchFile(organizationId: string, input: TaskWorkbenchDiffInput): Promise<void> {
|
||||
await (await organization(organizationId)).revertWorkbenchFile(input);
|
||||
async adminReloadGithubOrganization(organizationId: string): Promise<void> {
|
||||
await (await organization(organizationId)).adminReloadGithubOrganization();
|
||||
},
|
||||
|
||||
async reloadGithubOrganization(organizationId: string): Promise<void> {
|
||||
await (await organization(organizationId)).reloadGithubOrganization();
|
||||
async adminReloadGithubPullRequests(organizationId: string): Promise<void> {
|
||||
await (await organization(organizationId)).adminReloadGithubPullRequests();
|
||||
},
|
||||
|
||||
async reloadGithubPullRequests(organizationId: string): Promise<void> {
|
||||
await (await organization(organizationId)).reloadGithubPullRequests();
|
||||
async adminReloadGithubRepository(organizationId: string, repoId: string): Promise<void> {
|
||||
await (await organization(organizationId)).adminReloadGithubRepository({ repoId });
|
||||
},
|
||||
|
||||
async reloadGithubRepository(organizationId: string, repoId: string): Promise<void> {
|
||||
await (await organization(organizationId)).reloadGithubRepository({ repoId });
|
||||
},
|
||||
|
||||
async reloadGithubPullRequest(organizationId: string, repoId: string, prNumber: number): Promise<void> {
|
||||
await (await organization(organizationId)).reloadGithubPullRequest({ repoId, prNumber });
|
||||
async adminReloadGithubPullRequest(organizationId: string, repoId: string, prNumber: number): Promise<void> {
|
||||
await (await organization(organizationId)).adminReloadGithubPullRequest({ repoId, prNumber });
|
||||
},
|
||||
|
||||
async health(): Promise<{ ok: true }> {
|
||||
|
|
|
|||
|
|
@ -8,4 +8,4 @@ export * from "./subscription/use-subscription.js";
|
|||
export * from "./keys.js";
|
||||
export * from "./mock-app.js";
|
||||
export * from "./view-model.js";
|
||||
export * from "./workbench-client.js";
|
||||
export * from "./workspace-client.js";
|
||||
|
|
|
|||
|
|
@ -16,6 +16,6 @@ export function taskSandboxKey(organizationId: string, sandboxId: string): Actor
|
|||
return ["org", organizationId, "sandbox", sandboxId];
|
||||
}
|
||||
|
||||
export function historyKey(organizationId: string, repoId: string): ActorKey {
|
||||
return ["org", organizationId, "repository", repoId, "history"];
|
||||
export function auditLogKey(organizationId: string, repoId: string): ActorKey {
|
||||
return ["org", organizationId, "repository", repoId, "audit-log"];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { WorkbenchModelId } from "@sandbox-agent/foundry-shared";
|
||||
import type { WorkspaceModelId } from "@sandbox-agent/foundry-shared";
|
||||
import { injectMockLatency } from "./mock/latency.js";
|
||||
import rivetDevFixture from "../../../scripts/data/rivet-dev.json" with { type: "json" };
|
||||
|
||||
|
|
@ -16,6 +16,7 @@ export interface MockFoundryUser {
|
|||
githubLogin: string;
|
||||
roleLabel: string;
|
||||
eligibleOrganizationIds: string[];
|
||||
defaultModel: WorkspaceModelId;
|
||||
}
|
||||
|
||||
export interface MockFoundryOrganizationMember {
|
||||
|
|
@ -61,7 +62,6 @@ export interface MockFoundryOrganizationSettings {
|
|||
slug: string;
|
||||
primaryDomain: string;
|
||||
seatAccrualMode: "first_prompt";
|
||||
defaultModel: WorkbenchModelId;
|
||||
autoImportRepos: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -111,6 +111,7 @@ export interface MockFoundryAppClient {
|
|||
skipStarterRepo(): Promise<void>;
|
||||
starStarterRepo(organizationId: string): Promise<void>;
|
||||
selectOrganization(organizationId: string): Promise<void>;
|
||||
setDefaultModel(model: WorkspaceModelId): Promise<void>;
|
||||
updateOrganizationProfile(input: UpdateMockOrganizationProfileInput): Promise<void>;
|
||||
triggerGithubSync(organizationId: string): Promise<void>;
|
||||
completeHostedCheckout(organizationId: string, planId: MockBillingPlanId): Promise<void>;
|
||||
|
|
@ -180,7 +181,6 @@ function buildRivetOrganization(): MockFoundryOrganization {
|
|||
slug: "rivet",
|
||||
primaryDomain: "rivet.dev",
|
||||
seatAccrualMode: "first_prompt",
|
||||
defaultModel: "gpt-5.3-codex",
|
||||
autoImportRepos: true,
|
||||
},
|
||||
github: {
|
||||
|
|
@ -233,6 +233,7 @@ function buildDefaultSnapshot(): MockFoundryAppSnapshot {
|
|||
githubLogin: "nathan",
|
||||
roleLabel: "Founder",
|
||||
eligibleOrganizationIds: ["personal-nathan", "acme", "rivet"],
|
||||
defaultModel: "gpt-5.3-codex",
|
||||
},
|
||||
{
|
||||
id: "user-maya",
|
||||
|
|
@ -241,6 +242,7 @@ function buildDefaultSnapshot(): MockFoundryAppSnapshot {
|
|||
githubLogin: "maya",
|
||||
roleLabel: "Staff Engineer",
|
||||
eligibleOrganizationIds: ["acme"],
|
||||
defaultModel: "claude-sonnet-4",
|
||||
},
|
||||
{
|
||||
id: "user-jamie",
|
||||
|
|
@ -249,6 +251,7 @@ function buildDefaultSnapshot(): MockFoundryAppSnapshot {
|
|||
githubLogin: "jamie",
|
||||
roleLabel: "Platform Lead",
|
||||
eligibleOrganizationIds: ["personal-jamie", "rivet"],
|
||||
defaultModel: "claude-opus-4",
|
||||
},
|
||||
],
|
||||
organizations: [
|
||||
|
|
@ -261,7 +264,6 @@ function buildDefaultSnapshot(): MockFoundryAppSnapshot {
|
|||
slug: "nathan",
|
||||
primaryDomain: "personal",
|
||||
seatAccrualMode: "first_prompt",
|
||||
defaultModel: "claude-sonnet-4",
|
||||
autoImportRepos: true,
|
||||
},
|
||||
github: {
|
||||
|
|
@ -297,7 +299,6 @@ function buildDefaultSnapshot(): MockFoundryAppSnapshot {
|
|||
slug: "acme",
|
||||
primaryDomain: "acme.dev",
|
||||
seatAccrualMode: "first_prompt",
|
||||
defaultModel: "claude-sonnet-4",
|
||||
autoImportRepos: true,
|
||||
},
|
||||
github: {
|
||||
|
|
@ -342,7 +343,6 @@ function buildDefaultSnapshot(): MockFoundryAppSnapshot {
|
|||
slug: "jamie",
|
||||
primaryDomain: "personal",
|
||||
seatAccrualMode: "first_prompt",
|
||||
defaultModel: "claude-opus-4",
|
||||
autoImportRepos: true,
|
||||
},
|
||||
github: {
|
||||
|
|
@ -538,6 +538,18 @@ class MockFoundryAppStore implements MockFoundryAppClient {
|
|||
}
|
||||
}
|
||||
|
||||
async setDefaultModel(model: WorkspaceModelId): Promise<void> {
|
||||
await this.injectAsyncLatency();
|
||||
const currentUserId = this.snapshot.auth.currentUserId;
|
||||
if (!currentUserId) {
|
||||
throw new Error("No signed-in mock user");
|
||||
}
|
||||
this.updateSnapshot((current) => ({
|
||||
...current,
|
||||
users: current.users.map((user) => (user.id === currentUserId ? { ...user, defaultModel: model } : user)),
|
||||
}));
|
||||
}
|
||||
|
||||
async updateOrganizationProfile(input: UpdateMockOrganizationProfileInput): Promise<void> {
|
||||
await this.injectAsyncLatency();
|
||||
this.requireOrganization(input.organizationId);
|
||||
|
|
|
|||
|
|
@ -6,25 +6,25 @@ import type {
|
|||
SessionEvent,
|
||||
TaskRecord,
|
||||
TaskSummary,
|
||||
TaskWorkbenchChangeModelInput,
|
||||
TaskWorkbenchCreateTaskInput,
|
||||
TaskWorkbenchCreateTaskResponse,
|
||||
TaskWorkbenchDiffInput,
|
||||
TaskWorkbenchRenameInput,
|
||||
TaskWorkbenchRenameSessionInput,
|
||||
TaskWorkbenchSelectInput,
|
||||
TaskWorkbenchSetSessionUnreadInput,
|
||||
TaskWorkbenchSendMessageInput,
|
||||
TaskWorkbenchSnapshot,
|
||||
TaskWorkbenchSessionInput,
|
||||
TaskWorkbenchUpdateDraftInput,
|
||||
TaskWorkspaceChangeModelInput,
|
||||
TaskWorkspaceCreateTaskInput,
|
||||
TaskWorkspaceCreateTaskResponse,
|
||||
TaskWorkspaceDiffInput,
|
||||
TaskWorkspaceRenameInput,
|
||||
TaskWorkspaceRenameSessionInput,
|
||||
TaskWorkspaceSelectInput,
|
||||
TaskWorkspaceSetSessionUnreadInput,
|
||||
TaskWorkspaceSendMessageInput,
|
||||
TaskWorkspaceSnapshot,
|
||||
TaskWorkspaceSessionInput,
|
||||
TaskWorkspaceUpdateDraftInput,
|
||||
TaskEvent,
|
||||
WorkbenchSessionDetail,
|
||||
WorkbenchTaskDetail,
|
||||
WorkbenchTaskSummary,
|
||||
WorkspaceSessionDetail,
|
||||
WorkspaceTaskDetail,
|
||||
WorkspaceTaskSummary,
|
||||
OrganizationEvent,
|
||||
OrganizationSummarySnapshot,
|
||||
HistoryEvent,
|
||||
AuditLogEvent as HistoryEvent,
|
||||
HistoryQueryInput,
|
||||
SandboxProviderId,
|
||||
RepoOverview,
|
||||
|
|
@ -34,7 +34,7 @@ import type {
|
|||
} from "@sandbox-agent/foundry-shared";
|
||||
import type { ProcessCreateRequest, ProcessLogFollowQuery, ProcessLogsResponse, ProcessSignalQuery } from "sandbox-agent";
|
||||
import type { ActorConn, BackendClient, SandboxProcessRecord, SandboxSessionEventRecord, SandboxSessionRecord } from "../backend-client.js";
|
||||
import { getSharedMockWorkbenchClient } from "./workbench-client.js";
|
||||
import { getSharedMockWorkspaceClient } from "./workspace-client.js";
|
||||
|
||||
interface MockProcessRecord extends SandboxProcessRecord {
|
||||
logText: string;
|
||||
|
|
@ -89,7 +89,7 @@ function toTaskStatus(status: TaskRecord["status"], archived: boolean): TaskReco
|
|||
}
|
||||
|
||||
export function createMockBackendClient(defaultOrganizationId = "default"): BackendClient {
|
||||
const workbench = getSharedMockWorkbenchClient();
|
||||
const workspace = getSharedMockWorkspaceClient();
|
||||
const listenersBySandboxId = new Map<string, Set<() => void>>();
|
||||
const processesBySandboxId = new Map<string, MockProcessRecord[]>();
|
||||
const connectionListeners = new Map<string, Set<(payload: any) => void>>();
|
||||
|
|
@ -97,7 +97,7 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
|||
let nextProcessId = 1;
|
||||
|
||||
const requireTask = (taskId: string) => {
|
||||
const task = workbench.getSnapshot().tasks.find((candidate) => candidate.id === taskId);
|
||||
const task = workspace.getSnapshot().tasks.find((candidate) => candidate.id === taskId);
|
||||
if (!task) {
|
||||
throw new Error(`Unknown mock task ${taskId}`);
|
||||
}
|
||||
|
|
@ -164,7 +164,7 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
|||
async dispose(): Promise<void> {},
|
||||
});
|
||||
|
||||
const buildTaskSummary = (task: TaskWorkbenchSnapshot["tasks"][number]): WorkbenchTaskSummary => ({
|
||||
const buildTaskSummary = (task: TaskWorkspaceSnapshot["tasks"][number]): WorkspaceTaskSummary => ({
|
||||
id: task.id,
|
||||
repoId: task.repoId,
|
||||
title: task.title,
|
||||
|
|
@ -187,7 +187,7 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
|||
})),
|
||||
});
|
||||
|
||||
const buildTaskDetail = (task: TaskWorkbenchSnapshot["tasks"][number]): WorkbenchTaskDetail => ({
|
||||
const buildTaskDetail = (task: TaskWorkspaceSnapshot["tasks"][number]): WorkspaceTaskDetail => ({
|
||||
...buildTaskSummary(task),
|
||||
task: task.title,
|
||||
agentType: task.sessions[0]?.agent === "Codex" ? "codex" : "claude",
|
||||
|
|
@ -211,7 +211,7 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
|||
activeSandboxId: task.id,
|
||||
});
|
||||
|
||||
const buildSessionDetail = (task: TaskWorkbenchSnapshot["tasks"][number], sessionId: string): WorkbenchSessionDetail => {
|
||||
const buildSessionDetail = (task: TaskWorkspaceSnapshot["tasks"][number], sessionId: string): WorkspaceSessionDetail => {
|
||||
const tab = task.sessions.find((candidate) => candidate.id === sessionId);
|
||||
if (!tab) {
|
||||
throw new Error(`Unknown mock session ${sessionId} for task ${task.id}`);
|
||||
|
|
@ -232,7 +232,7 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
|||
};
|
||||
|
||||
const buildOrganizationSummary = (): OrganizationSummarySnapshot => {
|
||||
const snapshot = workbench.getSnapshot();
|
||||
const snapshot = workspace.getSnapshot();
|
||||
const taskSummaries = snapshot.tasks.map(buildTaskSummary);
|
||||
return {
|
||||
organizationId: defaultOrganizationId,
|
||||
|
|
@ -256,20 +256,16 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
|||
`sandbox:${organizationId}:${sandboxProviderId}:${sandboxId}`;
|
||||
|
||||
const emitOrganizationSnapshot = (): void => {
|
||||
const summary = buildOrganizationSummary();
|
||||
const latestTask = [...summary.taskSummaries].sort((left, right) => right.updatedAtMs - left.updatedAtMs)[0] ?? null;
|
||||
if (latestTask) {
|
||||
emitConnectionEvent(organizationScope(defaultOrganizationId), "organizationUpdated", {
|
||||
type: "taskSummaryUpdated",
|
||||
taskSummary: latestTask,
|
||||
} satisfies OrganizationEvent);
|
||||
}
|
||||
emitConnectionEvent(organizationScope(defaultOrganizationId), "organizationUpdated", {
|
||||
type: "organizationUpdated",
|
||||
snapshot: buildOrganizationSummary(),
|
||||
} satisfies OrganizationEvent);
|
||||
};
|
||||
|
||||
const emitTaskUpdate = (taskId: string): void => {
|
||||
const task = requireTask(taskId);
|
||||
emitConnectionEvent(taskScope(defaultOrganizationId, task.repoId, task.id), "taskUpdated", {
|
||||
type: "taskDetailUpdated",
|
||||
type: "taskUpdated",
|
||||
detail: buildTaskDetail(task),
|
||||
} satisfies TaskEvent);
|
||||
};
|
||||
|
|
@ -400,6 +396,10 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
|||
return unsupportedAppSnapshot();
|
||||
},
|
||||
|
||||
async setAppDefaultModel(): Promise<FoundryAppSnapshot> {
|
||||
return unsupportedAppSnapshot();
|
||||
},
|
||||
|
||||
async updateAppOrganizationProfile(): Promise<FoundryAppSnapshot> {
|
||||
return unsupportedAppSnapshot();
|
||||
},
|
||||
|
|
@ -433,7 +433,7 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
|||
},
|
||||
|
||||
async listRepos(_organizationId: string): Promise<RepoRecord[]> {
|
||||
return workbench.getSnapshot().repos.map((repo) => ({
|
||||
return workspace.getSnapshot().repos.map((repo) => ({
|
||||
organizationId: defaultOrganizationId,
|
||||
repoId: repo.id,
|
||||
remoteUrl: mockRepoRemote(repo.label),
|
||||
|
|
@ -447,7 +447,7 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
|||
},
|
||||
|
||||
async listTasks(_organizationId: string, repoId?: string): Promise<TaskSummary[]> {
|
||||
return workbench
|
||||
return workspace
|
||||
.getSnapshot()
|
||||
.tasks.filter((task) => !repoId || task.repoId === repoId)
|
||||
.map((task) => ({
|
||||
|
|
@ -641,24 +641,24 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
|||
return buildOrganizationSummary();
|
||||
},
|
||||
|
||||
async getTaskDetail(_organizationId: string, _repoId: string, taskId: string): Promise<WorkbenchTaskDetail> {
|
||||
async getTaskDetail(_organizationId: string, _repoId: string, taskId: string): Promise<WorkspaceTaskDetail> {
|
||||
return buildTaskDetail(requireTask(taskId));
|
||||
},
|
||||
|
||||
async getSessionDetail(_organizationId: string, _repoId: string, taskId: string, sessionId: string): Promise<WorkbenchSessionDetail> {
|
||||
async getSessionDetail(_organizationId: string, _repoId: string, taskId: string, sessionId: string): Promise<WorkspaceSessionDetail> {
|
||||
return buildSessionDetail(requireTask(taskId), sessionId);
|
||||
},
|
||||
|
||||
async getWorkbench(): Promise<TaskWorkbenchSnapshot> {
|
||||
return workbench.getSnapshot();
|
||||
async getWorkspace(): Promise<TaskWorkspaceSnapshot> {
|
||||
return workspace.getSnapshot();
|
||||
},
|
||||
|
||||
subscribeWorkbench(_organizationId: string, listener: () => void): () => void {
|
||||
return workbench.subscribe(listener);
|
||||
subscribeWorkspace(_organizationId: string, listener: () => void): () => void {
|
||||
return workspace.subscribe(listener);
|
||||
},
|
||||
|
||||
async createWorkbenchTask(_organizationId: string, input: TaskWorkbenchCreateTaskInput): Promise<TaskWorkbenchCreateTaskResponse> {
|
||||
const created = await workbench.createTask(input);
|
||||
async createWorkspaceTask(_organizationId: string, input: TaskWorkspaceCreateTaskInput): Promise<TaskWorkspaceCreateTaskResponse> {
|
||||
const created = await workspace.createTask(input);
|
||||
emitOrganizationSnapshot();
|
||||
emitTaskUpdate(created.taskId);
|
||||
if (created.sessionId) {
|
||||
|
|
@ -667,99 +667,93 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
|||
return created;
|
||||
},
|
||||
|
||||
async markWorkbenchUnread(_organizationId: string, input: TaskWorkbenchSelectInput): Promise<void> {
|
||||
await workbench.markTaskUnread(input);
|
||||
async markWorkspaceUnread(_organizationId: string, input: TaskWorkspaceSelectInput): Promise<void> {
|
||||
await workspace.markTaskUnread(input);
|
||||
emitOrganizationSnapshot();
|
||||
emitTaskUpdate(input.taskId);
|
||||
},
|
||||
|
||||
async renameWorkbenchTask(_organizationId: string, input: TaskWorkbenchRenameInput): Promise<void> {
|
||||
await workbench.renameTask(input);
|
||||
async renameWorkspaceTask(_organizationId: string, input: TaskWorkspaceRenameInput): Promise<void> {
|
||||
await workspace.renameTask(input);
|
||||
emitOrganizationSnapshot();
|
||||
emitTaskUpdate(input.taskId);
|
||||
},
|
||||
|
||||
async renameWorkbenchBranch(_organizationId: string, input: TaskWorkbenchRenameInput): Promise<void> {
|
||||
await workbench.renameBranch(input);
|
||||
emitOrganizationSnapshot();
|
||||
emitTaskUpdate(input.taskId);
|
||||
},
|
||||
|
||||
async createWorkbenchSession(_organizationId: string, input: TaskWorkbenchSelectInput & { model?: string }): Promise<{ sessionId: string }> {
|
||||
const created = await workbench.addSession(input);
|
||||
async createWorkspaceSession(_organizationId: string, input: TaskWorkspaceSelectInput & { model?: string }): Promise<{ sessionId: string }> {
|
||||
const created = await workspace.addSession(input);
|
||||
emitOrganizationSnapshot();
|
||||
emitTaskUpdate(input.taskId);
|
||||
emitSessionUpdate(input.taskId, created.sessionId);
|
||||
return created;
|
||||
},
|
||||
|
||||
async renameWorkbenchSession(_organizationId: string, input: TaskWorkbenchRenameSessionInput): Promise<void> {
|
||||
await workbench.renameSession(input);
|
||||
async renameWorkspaceSession(_organizationId: string, input: TaskWorkspaceRenameSessionInput): Promise<void> {
|
||||
await workspace.renameSession(input);
|
||||
emitOrganizationSnapshot();
|
||||
emitTaskUpdate(input.taskId);
|
||||
emitSessionUpdate(input.taskId, input.sessionId);
|
||||
},
|
||||
|
||||
async setWorkbenchSessionUnread(_organizationId: string, input: TaskWorkbenchSetSessionUnreadInput): Promise<void> {
|
||||
await workbench.setSessionUnread(input);
|
||||
async setWorkspaceSessionUnread(_organizationId: string, input: TaskWorkspaceSetSessionUnreadInput): Promise<void> {
|
||||
await workspace.setSessionUnread(input);
|
||||
emitOrganizationSnapshot();
|
||||
emitTaskUpdate(input.taskId);
|
||||
emitSessionUpdate(input.taskId, input.sessionId);
|
||||
},
|
||||
|
||||
async updateWorkbenchDraft(_organizationId: string, input: TaskWorkbenchUpdateDraftInput): Promise<void> {
|
||||
await workbench.updateDraft(input);
|
||||
async updateWorkspaceDraft(_organizationId: string, input: TaskWorkspaceUpdateDraftInput): Promise<void> {
|
||||
await workspace.updateDraft(input);
|
||||
emitOrganizationSnapshot();
|
||||
emitTaskUpdate(input.taskId);
|
||||
emitSessionUpdate(input.taskId, input.sessionId);
|
||||
},
|
||||
|
||||
async changeWorkbenchModel(_organizationId: string, input: TaskWorkbenchChangeModelInput): Promise<void> {
|
||||
await workbench.changeModel(input);
|
||||
async changeWorkspaceModel(_organizationId: string, input: TaskWorkspaceChangeModelInput): Promise<void> {
|
||||
await workspace.changeModel(input);
|
||||
emitOrganizationSnapshot();
|
||||
emitTaskUpdate(input.taskId);
|
||||
emitSessionUpdate(input.taskId, input.sessionId);
|
||||
},
|
||||
|
||||
async sendWorkbenchMessage(_organizationId: string, input: TaskWorkbenchSendMessageInput): Promise<void> {
|
||||
await workbench.sendMessage(input);
|
||||
async sendWorkspaceMessage(_organizationId: string, input: TaskWorkspaceSendMessageInput): Promise<void> {
|
||||
await workspace.sendMessage(input);
|
||||
emitOrganizationSnapshot();
|
||||
emitTaskUpdate(input.taskId);
|
||||
emitSessionUpdate(input.taskId, input.sessionId);
|
||||
},
|
||||
|
||||
async stopWorkbenchSession(_organizationId: string, input: TaskWorkbenchSessionInput): Promise<void> {
|
||||
await workbench.stopAgent(input);
|
||||
async stopWorkspaceSession(_organizationId: string, input: TaskWorkspaceSessionInput): Promise<void> {
|
||||
await workspace.stopAgent(input);
|
||||
emitOrganizationSnapshot();
|
||||
emitTaskUpdate(input.taskId);
|
||||
emitSessionUpdate(input.taskId, input.sessionId);
|
||||
},
|
||||
|
||||
async closeWorkbenchSession(_organizationId: string, input: TaskWorkbenchSessionInput): Promise<void> {
|
||||
await workbench.closeSession(input);
|
||||
async closeWorkspaceSession(_organizationId: string, input: TaskWorkspaceSessionInput): Promise<void> {
|
||||
await workspace.closeSession(input);
|
||||
emitOrganizationSnapshot();
|
||||
emitTaskUpdate(input.taskId);
|
||||
},
|
||||
|
||||
async publishWorkbenchPr(_organizationId: string, input: TaskWorkbenchSelectInput): Promise<void> {
|
||||
await workbench.publishPr(input);
|
||||
async publishWorkspacePr(_organizationId: string, input: TaskWorkspaceSelectInput): Promise<void> {
|
||||
await workspace.publishPr(input);
|
||||
emitOrganizationSnapshot();
|
||||
emitTaskUpdate(input.taskId);
|
||||
},
|
||||
|
||||
async revertWorkbenchFile(_organizationId: string, input: TaskWorkbenchDiffInput): Promise<void> {
|
||||
await workbench.revertFile(input);
|
||||
async revertWorkspaceFile(_organizationId: string, input: TaskWorkspaceDiffInput): Promise<void> {
|
||||
await workspace.revertFile(input);
|
||||
emitOrganizationSnapshot();
|
||||
emitTaskUpdate(input.taskId);
|
||||
},
|
||||
|
||||
async reloadGithubOrganization(): Promise<void> {},
|
||||
async adminReloadGithubOrganization(): Promise<void> {},
|
||||
|
||||
async reloadGithubPullRequests(): Promise<void> {},
|
||||
async adminReloadGithubPullRequests(): Promise<void> {},
|
||||
|
||||
async reloadGithubRepository(): Promise<void> {},
|
||||
async adminReloadGithubRepository(): Promise<void> {},
|
||||
|
||||
async reloadGithubPullRequest(): Promise<void> {},
|
||||
async adminReloadGithubPullRequest(): Promise<void> {},
|
||||
|
||||
async health(): Promise<{ ok: true }> {
|
||||
return { ok: true };
|
||||
|
|
|
|||
|
|
@ -1,33 +1,33 @@
|
|||
import {
|
||||
MODEL_GROUPS,
|
||||
buildInitialMockLayoutViewModel,
|
||||
groupWorkbenchRepositories,
|
||||
groupWorkspaceRepositories,
|
||||
nowMs,
|
||||
providerAgent,
|
||||
randomReply,
|
||||
removeFileTreePath,
|
||||
slugify,
|
||||
uid,
|
||||
} from "../workbench-model.js";
|
||||
} from "../workspace-model.js";
|
||||
import type {
|
||||
TaskWorkbenchAddSessionResponse,
|
||||
TaskWorkbenchChangeModelInput,
|
||||
TaskWorkbenchCreateTaskInput,
|
||||
TaskWorkbenchCreateTaskResponse,
|
||||
TaskWorkbenchDiffInput,
|
||||
TaskWorkbenchRenameInput,
|
||||
TaskWorkbenchRenameSessionInput,
|
||||
TaskWorkbenchSelectInput,
|
||||
TaskWorkbenchSetSessionUnreadInput,
|
||||
TaskWorkbenchSendMessageInput,
|
||||
TaskWorkbenchSnapshot,
|
||||
TaskWorkbenchSessionInput,
|
||||
TaskWorkbenchUpdateDraftInput,
|
||||
WorkbenchSession as AgentSession,
|
||||
WorkbenchTask as Task,
|
||||
WorkbenchTranscriptEvent as TranscriptEvent,
|
||||
TaskWorkspaceAddSessionResponse,
|
||||
TaskWorkspaceChangeModelInput,
|
||||
TaskWorkspaceCreateTaskInput,
|
||||
TaskWorkspaceCreateTaskResponse,
|
||||
TaskWorkspaceDiffInput,
|
||||
TaskWorkspaceRenameInput,
|
||||
TaskWorkspaceRenameSessionInput,
|
||||
TaskWorkspaceSelectInput,
|
||||
TaskWorkspaceSetSessionUnreadInput,
|
||||
TaskWorkspaceSendMessageInput,
|
||||
TaskWorkspaceSnapshot,
|
||||
TaskWorkspaceSessionInput,
|
||||
TaskWorkspaceUpdateDraftInput,
|
||||
WorkspaceSession as AgentSession,
|
||||
WorkspaceTask as Task,
|
||||
WorkspaceTranscriptEvent as TranscriptEvent,
|
||||
} from "@sandbox-agent/foundry-shared";
|
||||
import type { TaskWorkbenchClient } from "../workbench-client.js";
|
||||
import type { TaskWorkspaceClient } from "../workspace-client.js";
|
||||
|
||||
function buildTranscriptEvent(params: {
|
||||
sessionId: string;
|
||||
|
|
@ -47,12 +47,12 @@ function buildTranscriptEvent(params: {
|
|||
};
|
||||
}
|
||||
|
||||
class MockWorkbenchStore implements TaskWorkbenchClient {
|
||||
class MockWorkspaceStore implements TaskWorkspaceClient {
|
||||
private snapshot = buildInitialMockLayoutViewModel();
|
||||
private listeners = new Set<() => void>();
|
||||
private pendingTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
|
||||
getSnapshot(): TaskWorkbenchSnapshot {
|
||||
getSnapshot(): TaskWorkspaceSnapshot {
|
||||
return this.snapshot;
|
||||
}
|
||||
|
||||
|
|
@ -63,7 +63,7 @@ class MockWorkbenchStore implements TaskWorkbenchClient {
|
|||
};
|
||||
}
|
||||
|
||||
async createTask(input: TaskWorkbenchCreateTaskInput): Promise<TaskWorkbenchCreateTaskResponse> {
|
||||
async createTask(input: TaskWorkspaceCreateTaskInput): Promise<TaskWorkspaceCreateTaskResponse> {
|
||||
const id = uid();
|
||||
const sessionId = `session-${id}`;
|
||||
const repo = this.snapshot.repos.find((candidate) => candidate.id === input.repoId);
|
||||
|
|
@ -109,7 +109,7 @@ class MockWorkbenchStore implements TaskWorkbenchClient {
|
|||
return { taskId: id, sessionId };
|
||||
}
|
||||
|
||||
async markTaskUnread(input: TaskWorkbenchSelectInput): Promise<void> {
|
||||
async markTaskUnread(input: TaskWorkspaceSelectInput): Promise<void> {
|
||||
this.updateTask(input.taskId, (task) => {
|
||||
const targetSession = task.sessions[task.sessions.length - 1] ?? null;
|
||||
if (!targetSession) {
|
||||
|
|
@ -123,7 +123,7 @@ class MockWorkbenchStore implements TaskWorkbenchClient {
|
|||
});
|
||||
}
|
||||
|
||||
async renameTask(input: TaskWorkbenchRenameInput): Promise<void> {
|
||||
async renameTask(input: TaskWorkspaceRenameInput): Promise<void> {
|
||||
const value = input.value.trim();
|
||||
if (!value) {
|
||||
throw new Error(`Cannot rename task ${input.taskId} to an empty title`);
|
||||
|
|
@ -131,19 +131,11 @@ class MockWorkbenchStore implements TaskWorkbenchClient {
|
|||
this.updateTask(input.taskId, (task) => ({ ...task, title: value, updatedAtMs: nowMs() }));
|
||||
}
|
||||
|
||||
async renameBranch(input: TaskWorkbenchRenameInput): Promise<void> {
|
||||
const value = input.value.trim();
|
||||
if (!value) {
|
||||
throw new Error(`Cannot rename branch for task ${input.taskId} to an empty value`);
|
||||
}
|
||||
this.updateTask(input.taskId, (task) => ({ ...task, branch: value, updatedAtMs: nowMs() }));
|
||||
}
|
||||
|
||||
async archiveTask(input: TaskWorkbenchSelectInput): Promise<void> {
|
||||
async archiveTask(input: TaskWorkspaceSelectInput): Promise<void> {
|
||||
this.updateTask(input.taskId, (task) => ({ ...task, status: "archived", updatedAtMs: nowMs() }));
|
||||
}
|
||||
|
||||
async publishPr(input: TaskWorkbenchSelectInput): Promise<void> {
|
||||
async publishPr(input: TaskWorkspaceSelectInput): Promise<void> {
|
||||
const nextPrNumber = Math.max(0, ...this.snapshot.tasks.map((task) => task.pullRequest?.number ?? 0)) + 1;
|
||||
this.updateTask(input.taskId, (task) => ({
|
||||
...task,
|
||||
|
|
@ -152,7 +144,7 @@ class MockWorkbenchStore implements TaskWorkbenchClient {
|
|||
}));
|
||||
}
|
||||
|
||||
async revertFile(input: TaskWorkbenchDiffInput): Promise<void> {
|
||||
async revertFile(input: TaskWorkspaceDiffInput): Promise<void> {
|
||||
this.updateTask(input.taskId, (task) => {
|
||||
const file = task.fileChanges.find((entry) => entry.path === input.path);
|
||||
const nextDiffs = { ...task.diffs };
|
||||
|
|
@ -167,7 +159,7 @@ class MockWorkbenchStore implements TaskWorkbenchClient {
|
|||
});
|
||||
}
|
||||
|
||||
async updateDraft(input: TaskWorkbenchUpdateDraftInput): Promise<void> {
|
||||
async updateDraft(input: TaskWorkspaceUpdateDraftInput): Promise<void> {
|
||||
this.assertSession(input.taskId, input.sessionId);
|
||||
this.updateTask(input.taskId, (task) => ({
|
||||
...task,
|
||||
|
|
@ -187,7 +179,7 @@ class MockWorkbenchStore implements TaskWorkbenchClient {
|
|||
}));
|
||||
}
|
||||
|
||||
async sendMessage(input: TaskWorkbenchSendMessageInput): Promise<void> {
|
||||
async sendMessage(input: TaskWorkspaceSendMessageInput): Promise<void> {
|
||||
const text = input.text.trim();
|
||||
if (!text) {
|
||||
throw new Error(`Cannot send an empty mock prompt for task ${input.taskId}`);
|
||||
|
|
@ -288,7 +280,7 @@ class MockWorkbenchStore implements TaskWorkbenchClient {
|
|||
this.pendingTimers.set(input.sessionId, timer);
|
||||
}
|
||||
|
||||
async stopAgent(input: TaskWorkbenchSessionInput): Promise<void> {
|
||||
async stopAgent(input: TaskWorkspaceSessionInput): Promise<void> {
|
||||
this.assertSession(input.taskId, input.sessionId);
|
||||
const existing = this.pendingTimers.get(input.sessionId);
|
||||
if (existing) {
|
||||
|
|
@ -311,14 +303,14 @@ class MockWorkbenchStore implements TaskWorkbenchClient {
|
|||
});
|
||||
}
|
||||
|
||||
async setSessionUnread(input: TaskWorkbenchSetSessionUnreadInput): Promise<void> {
|
||||
async setSessionUnread(input: TaskWorkspaceSetSessionUnreadInput): Promise<void> {
|
||||
this.updateTask(input.taskId, (currentTask) => ({
|
||||
...currentTask,
|
||||
sessions: currentTask.sessions.map((candidate) => (candidate.id === input.sessionId ? { ...candidate, unread: input.unread } : candidate)),
|
||||
}));
|
||||
}
|
||||
|
||||
async renameSession(input: TaskWorkbenchRenameSessionInput): Promise<void> {
|
||||
async renameSession(input: TaskWorkspaceRenameSessionInput): Promise<void> {
|
||||
const title = input.title.trim();
|
||||
if (!title) {
|
||||
throw new Error(`Cannot rename session ${input.sessionId} to an empty title`);
|
||||
|
|
@ -329,7 +321,7 @@ class MockWorkbenchStore implements TaskWorkbenchClient {
|
|||
}));
|
||||
}
|
||||
|
||||
async closeSession(input: TaskWorkbenchSessionInput): Promise<void> {
|
||||
async closeSession(input: TaskWorkspaceSessionInput): Promise<void> {
|
||||
this.updateTask(input.taskId, (currentTask) => {
|
||||
if (currentTask.sessions.length <= 1) {
|
||||
return currentTask;
|
||||
|
|
@ -342,7 +334,7 @@ class MockWorkbenchStore implements TaskWorkbenchClient {
|
|||
});
|
||||
}
|
||||
|
||||
async addSession(input: TaskWorkbenchSelectInput): Promise<TaskWorkbenchAddSessionResponse> {
|
||||
async addSession(input: TaskWorkspaceSelectInput): Promise<TaskWorkspaceAddSessionResponse> {
|
||||
this.assertTask(input.taskId);
|
||||
const nextSessionId = uid();
|
||||
const nextSession: AgentSession = {
|
||||
|
|
@ -368,7 +360,7 @@ class MockWorkbenchStore implements TaskWorkbenchClient {
|
|||
return { sessionId: nextSession.id };
|
||||
}
|
||||
|
||||
async changeModel(input: TaskWorkbenchChangeModelInput): Promise<void> {
|
||||
async changeModel(input: TaskWorkspaceChangeModelInput): Promise<void> {
|
||||
const group = MODEL_GROUPS.find((candidate) => candidate.models.some((entry) => entry.id === input.model));
|
||||
if (!group) {
|
||||
throw new Error(`Unable to resolve model provider for ${input.model}`);
|
||||
|
|
@ -382,11 +374,11 @@ class MockWorkbenchStore implements TaskWorkbenchClient {
|
|||
}));
|
||||
}
|
||||
|
||||
private updateState(updater: (current: TaskWorkbenchSnapshot) => TaskWorkbenchSnapshot): void {
|
||||
private updateState(updater: (current: TaskWorkspaceSnapshot) => TaskWorkspaceSnapshot): void {
|
||||
const nextSnapshot = updater(this.snapshot);
|
||||
this.snapshot = {
|
||||
...nextSnapshot,
|
||||
repositories: groupWorkbenchRepositories(nextSnapshot.repos, nextSnapshot.tasks),
|
||||
repositories: groupWorkspaceRepositories(nextSnapshot.repos, nextSnapshot.tasks),
|
||||
};
|
||||
this.notify();
|
||||
}
|
||||
|
|
@ -436,11 +428,11 @@ function candidateEventIndex(task: Task, sessionId: string): number {
|
|||
return (session?.transcript.length ?? 0) + 1;
|
||||
}
|
||||
|
||||
let sharedMockWorkbenchClient: TaskWorkbenchClient | null = null;
|
||||
let sharedMockWorkspaceClient: TaskWorkspaceClient | null = null;
|
||||
|
||||
export function getSharedMockWorkbenchClient(): TaskWorkbenchClient {
|
||||
if (!sharedMockWorkbenchClient) {
|
||||
sharedMockWorkbenchClient = new MockWorkbenchStore();
|
||||
export function getSharedMockWorkspaceClient(): TaskWorkspaceClient {
|
||||
if (!sharedMockWorkspaceClient) {
|
||||
sharedMockWorkspaceClient = new MockWorkspaceStore();
|
||||
}
|
||||
return sharedMockWorkbenchClient;
|
||||
return sharedMockWorkspaceClient;
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import type { FoundryAppSnapshot, FoundryBillingPlanId, UpdateFoundryOrganizationProfileInput } from "@sandbox-agent/foundry-shared";
|
||||
import type { FoundryAppSnapshot, FoundryBillingPlanId, UpdateFoundryOrganizationProfileInput, WorkspaceModelId } from "@sandbox-agent/foundry-shared";
|
||||
import type { BackendClient } from "../backend-client.js";
|
||||
import type { FoundryAppClient } from "../app-client.js";
|
||||
|
||||
|
|
@ -72,6 +72,11 @@ class RemoteFoundryAppStore implements FoundryAppClient {
|
|||
this.notify();
|
||||
}
|
||||
|
||||
async setDefaultModel(model: WorkspaceModelId): Promise<void> {
|
||||
this.snapshot = await this.backend.setAppDefaultModel(model);
|
||||
this.notify();
|
||||
}
|
||||
|
||||
async updateOrganizationProfile(input: UpdateFoundryOrganizationProfileInput): Promise<void> {
|
||||
this.snapshot = await this.backend.updateAppOrganizationProfile(input);
|
||||
this.notify();
|
||||
|
|
|
|||
|
|
@ -1,198 +0,0 @@
|
|||
import type {
|
||||
TaskWorkbenchAddSessionResponse,
|
||||
TaskWorkbenchChangeModelInput,
|
||||
TaskWorkbenchCreateTaskInput,
|
||||
TaskWorkbenchCreateTaskResponse,
|
||||
TaskWorkbenchDiffInput,
|
||||
TaskWorkbenchRenameInput,
|
||||
TaskWorkbenchRenameSessionInput,
|
||||
TaskWorkbenchSelectInput,
|
||||
TaskWorkbenchSetSessionUnreadInput,
|
||||
TaskWorkbenchSendMessageInput,
|
||||
TaskWorkbenchSnapshot,
|
||||
TaskWorkbenchSessionInput,
|
||||
TaskWorkbenchUpdateDraftInput,
|
||||
} from "@sandbox-agent/foundry-shared";
|
||||
import type { BackendClient } from "../backend-client.js";
|
||||
import { groupWorkbenchRepositories } from "../workbench-model.js";
|
||||
import type { TaskWorkbenchClient } from "../workbench-client.js";
|
||||
|
||||
export interface RemoteWorkbenchClientOptions {
|
||||
backend: BackendClient;
|
||||
organizationId: string;
|
||||
}
|
||||
|
||||
class RemoteWorkbenchStore implements TaskWorkbenchClient {
|
||||
private readonly backend: BackendClient;
|
||||
private readonly organizationId: string;
|
||||
private snapshot: TaskWorkbenchSnapshot;
|
||||
private readonly listeners = new Set<() => void>();
|
||||
private unsubscribeWorkbench: (() => void) | null = null;
|
||||
private refreshPromise: Promise<void> | null = null;
|
||||
private refreshRetryTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
constructor(options: RemoteWorkbenchClientOptions) {
|
||||
this.backend = options.backend;
|
||||
this.organizationId = options.organizationId;
|
||||
this.snapshot = {
|
||||
organizationId: options.organizationId,
|
||||
repos: [],
|
||||
repositories: [],
|
||||
tasks: [],
|
||||
};
|
||||
}
|
||||
|
||||
getSnapshot(): TaskWorkbenchSnapshot {
|
||||
return this.snapshot;
|
||||
}
|
||||
|
||||
subscribe(listener: () => void): () => void {
|
||||
this.listeners.add(listener);
|
||||
this.ensureStarted();
|
||||
return () => {
|
||||
this.listeners.delete(listener);
|
||||
if (this.listeners.size === 0 && this.refreshRetryTimeout) {
|
||||
clearTimeout(this.refreshRetryTimeout);
|
||||
this.refreshRetryTimeout = null;
|
||||
}
|
||||
if (this.listeners.size === 0 && this.unsubscribeWorkbench) {
|
||||
this.unsubscribeWorkbench();
|
||||
this.unsubscribeWorkbench = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async createTask(input: TaskWorkbenchCreateTaskInput): Promise<TaskWorkbenchCreateTaskResponse> {
|
||||
const created = await this.backend.createWorkbenchTask(this.organizationId, input);
|
||||
await this.refresh();
|
||||
return created;
|
||||
}
|
||||
|
||||
async markTaskUnread(input: TaskWorkbenchSelectInput): Promise<void> {
|
||||
await this.backend.markWorkbenchUnread(this.organizationId, input);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async renameTask(input: TaskWorkbenchRenameInput): Promise<void> {
|
||||
await this.backend.renameWorkbenchTask(this.organizationId, input);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async renameBranch(input: TaskWorkbenchRenameInput): Promise<void> {
|
||||
await this.backend.renameWorkbenchBranch(this.organizationId, input);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async archiveTask(input: TaskWorkbenchSelectInput): Promise<void> {
|
||||
await this.backend.runAction(this.organizationId, input.taskId, "archive");
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async publishPr(input: TaskWorkbenchSelectInput): Promise<void> {
|
||||
await this.backend.publishWorkbenchPr(this.organizationId, input);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async revertFile(input: TaskWorkbenchDiffInput): Promise<void> {
|
||||
await this.backend.revertWorkbenchFile(this.organizationId, input);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async updateDraft(input: TaskWorkbenchUpdateDraftInput): Promise<void> {
|
||||
await this.backend.updateWorkbenchDraft(this.organizationId, input);
|
||||
// Skip refresh — the server broadcast will trigger it, and the frontend
|
||||
// holds local draft state to avoid the round-trip overwriting user input.
|
||||
}
|
||||
|
||||
async sendMessage(input: TaskWorkbenchSendMessageInput): Promise<void> {
|
||||
await this.backend.sendWorkbenchMessage(this.organizationId, input);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async stopAgent(input: TaskWorkbenchSessionInput): Promise<void> {
|
||||
await this.backend.stopWorkbenchSession(this.organizationId, input);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async setSessionUnread(input: TaskWorkbenchSetSessionUnreadInput): Promise<void> {
|
||||
await this.backend.setWorkbenchSessionUnread(this.organizationId, input);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async renameSession(input: TaskWorkbenchRenameSessionInput): Promise<void> {
|
||||
await this.backend.renameWorkbenchSession(this.organizationId, input);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async closeSession(input: TaskWorkbenchSessionInput): Promise<void> {
|
||||
await this.backend.closeWorkbenchSession(this.organizationId, input);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async addSession(input: TaskWorkbenchSelectInput): Promise<TaskWorkbenchAddSessionResponse> {
|
||||
const created = await this.backend.createWorkbenchSession(this.organizationId, input);
|
||||
await this.refresh();
|
||||
return created;
|
||||
}
|
||||
|
||||
async changeModel(input: TaskWorkbenchChangeModelInput): Promise<void> {
|
||||
await this.backend.changeWorkbenchModel(this.organizationId, input);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
private ensureStarted(): void {
|
||||
if (!this.unsubscribeWorkbench) {
|
||||
this.unsubscribeWorkbench = this.backend.subscribeWorkbench(this.organizationId, () => {
|
||||
void this.refresh().catch(() => {
|
||||
this.scheduleRefreshRetry();
|
||||
});
|
||||
});
|
||||
}
|
||||
void this.refresh().catch(() => {
|
||||
this.scheduleRefreshRetry();
|
||||
});
|
||||
}
|
||||
|
||||
private scheduleRefreshRetry(): void {
|
||||
if (this.refreshRetryTimeout || this.listeners.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.refreshRetryTimeout = setTimeout(() => {
|
||||
this.refreshRetryTimeout = null;
|
||||
void this.refresh().catch(() => {
|
||||
this.scheduleRefreshRetry();
|
||||
});
|
||||
}, 1_000);
|
||||
}
|
||||
|
||||
private async refresh(): Promise<void> {
|
||||
if (this.refreshPromise) {
|
||||
await this.refreshPromise;
|
||||
return;
|
||||
}
|
||||
|
||||
this.refreshPromise = (async () => {
|
||||
const nextSnapshot = await this.backend.getWorkbench(this.organizationId);
|
||||
if (this.refreshRetryTimeout) {
|
||||
clearTimeout(this.refreshRetryTimeout);
|
||||
this.refreshRetryTimeout = null;
|
||||
}
|
||||
this.snapshot = {
|
||||
...nextSnapshot,
|
||||
repositories: nextSnapshot.repositories ?? groupWorkbenchRepositories(nextSnapshot.repos, nextSnapshot.tasks),
|
||||
};
|
||||
for (const listener of [...this.listeners]) {
|
||||
listener();
|
||||
}
|
||||
})().finally(() => {
|
||||
this.refreshPromise = null;
|
||||
});
|
||||
|
||||
await this.refreshPromise;
|
||||
}
|
||||
}
|
||||
|
||||
export function createRemoteWorkbenchClient(options: RemoteWorkbenchClientOptions): TaskWorkbenchClient {
|
||||
return new RemoteWorkbenchStore(options);
|
||||
}
|
||||
193
foundry/packages/client/src/remote/workspace-client.ts
Normal file
193
foundry/packages/client/src/remote/workspace-client.ts
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
import type {
|
||||
TaskWorkspaceAddSessionResponse,
|
||||
TaskWorkspaceChangeModelInput,
|
||||
TaskWorkspaceCreateTaskInput,
|
||||
TaskWorkspaceCreateTaskResponse,
|
||||
TaskWorkspaceDiffInput,
|
||||
TaskWorkspaceRenameInput,
|
||||
TaskWorkspaceRenameSessionInput,
|
||||
TaskWorkspaceSelectInput,
|
||||
TaskWorkspaceSetSessionUnreadInput,
|
||||
TaskWorkspaceSendMessageInput,
|
||||
TaskWorkspaceSnapshot,
|
||||
TaskWorkspaceSessionInput,
|
||||
TaskWorkspaceUpdateDraftInput,
|
||||
} from "@sandbox-agent/foundry-shared";
|
||||
import type { BackendClient } from "../backend-client.js";
|
||||
import { groupWorkspaceRepositories } from "../workspace-model.js";
|
||||
import type { TaskWorkspaceClient } from "../workspace-client.js";
|
||||
|
||||
export interface RemoteWorkspaceClientOptions {
|
||||
backend: BackendClient;
|
||||
organizationId: string;
|
||||
}
|
||||
|
||||
class RemoteWorkspaceStore implements TaskWorkspaceClient {
|
||||
private readonly backend: BackendClient;
|
||||
private readonly organizationId: string;
|
||||
private snapshot: TaskWorkspaceSnapshot;
|
||||
private readonly listeners = new Set<() => void>();
|
||||
private unsubscribeWorkspace: (() => void) | null = null;
|
||||
private refreshPromise: Promise<void> | null = null;
|
||||
private refreshRetryTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
constructor(options: RemoteWorkspaceClientOptions) {
|
||||
this.backend = options.backend;
|
||||
this.organizationId = options.organizationId;
|
||||
this.snapshot = {
|
||||
organizationId: options.organizationId,
|
||||
repos: [],
|
||||
repositories: [],
|
||||
tasks: [],
|
||||
};
|
||||
}
|
||||
|
||||
getSnapshot(): TaskWorkspaceSnapshot {
|
||||
return this.snapshot;
|
||||
}
|
||||
|
||||
subscribe(listener: () => void): () => void {
|
||||
this.listeners.add(listener);
|
||||
this.ensureStarted();
|
||||
return () => {
|
||||
this.listeners.delete(listener);
|
||||
if (this.listeners.size === 0 && this.refreshRetryTimeout) {
|
||||
clearTimeout(this.refreshRetryTimeout);
|
||||
this.refreshRetryTimeout = null;
|
||||
}
|
||||
if (this.listeners.size === 0 && this.unsubscribeWorkspace) {
|
||||
this.unsubscribeWorkspace();
|
||||
this.unsubscribeWorkspace = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async createTask(input: TaskWorkspaceCreateTaskInput): Promise<TaskWorkspaceCreateTaskResponse> {
|
||||
const created = await this.backend.createWorkspaceTask(this.organizationId, input);
|
||||
await this.refresh();
|
||||
return created;
|
||||
}
|
||||
|
||||
async markTaskUnread(input: TaskWorkspaceSelectInput): Promise<void> {
|
||||
await this.backend.markWorkspaceUnread(this.organizationId, input);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async renameTask(input: TaskWorkspaceRenameInput): Promise<void> {
|
||||
await this.backend.renameWorkspaceTask(this.organizationId, input);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async archiveTask(input: TaskWorkspaceSelectInput): Promise<void> {
|
||||
await this.backend.runAction(this.organizationId, input.repoId, input.taskId, "archive");
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async publishPr(input: TaskWorkspaceSelectInput): Promise<void> {
|
||||
await this.backend.publishWorkspacePr(this.organizationId, input);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async revertFile(input: TaskWorkspaceDiffInput): Promise<void> {
|
||||
await this.backend.revertWorkspaceFile(this.organizationId, input);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async updateDraft(input: TaskWorkspaceUpdateDraftInput): Promise<void> {
|
||||
await this.backend.updateWorkspaceDraft(this.organizationId, input);
|
||||
// Skip refresh — the server broadcast will trigger it, and the frontend
|
||||
// holds local draft state to avoid the round-trip overwriting user input.
|
||||
}
|
||||
|
||||
async sendMessage(input: TaskWorkspaceSendMessageInput): Promise<void> {
|
||||
await this.backend.sendWorkspaceMessage(this.organizationId, input);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async stopAgent(input: TaskWorkspaceSessionInput): Promise<void> {
|
||||
await this.backend.stopWorkspaceSession(this.organizationId, input);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async setSessionUnread(input: TaskWorkspaceSetSessionUnreadInput): Promise<void> {
|
||||
await this.backend.setWorkspaceSessionUnread(this.organizationId, input);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async renameSession(input: TaskWorkspaceRenameSessionInput): Promise<void> {
|
||||
await this.backend.renameWorkspaceSession(this.organizationId, input);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async closeSession(input: TaskWorkspaceSessionInput): Promise<void> {
|
||||
await this.backend.closeWorkspaceSession(this.organizationId, input);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async addSession(input: TaskWorkspaceSelectInput): Promise<TaskWorkspaceAddSessionResponse> {
|
||||
const created = await this.backend.createWorkspaceSession(this.organizationId, input);
|
||||
await this.refresh();
|
||||
return created;
|
||||
}
|
||||
|
||||
async changeModel(input: TaskWorkspaceChangeModelInput): Promise<void> {
|
||||
await this.backend.changeWorkspaceModel(this.organizationId, input);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
private ensureStarted(): void {
|
||||
if (!this.unsubscribeWorkspace) {
|
||||
this.unsubscribeWorkspace = this.backend.subscribeWorkspace(this.organizationId, () => {
|
||||
void this.refresh().catch(() => {
|
||||
this.scheduleRefreshRetry();
|
||||
});
|
||||
});
|
||||
}
|
||||
void this.refresh().catch(() => {
|
||||
this.scheduleRefreshRetry();
|
||||
});
|
||||
}
|
||||
|
||||
private scheduleRefreshRetry(): void {
|
||||
if (this.refreshRetryTimeout || this.listeners.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.refreshRetryTimeout = setTimeout(() => {
|
||||
this.refreshRetryTimeout = null;
|
||||
void this.refresh().catch(() => {
|
||||
this.scheduleRefreshRetry();
|
||||
});
|
||||
}, 1_000);
|
||||
}
|
||||
|
||||
private async refresh(): Promise<void> {
|
||||
if (this.refreshPromise) {
|
||||
await this.refreshPromise;
|
||||
return;
|
||||
}
|
||||
|
||||
this.refreshPromise = (async () => {
|
||||
const nextSnapshot = await this.backend.getWorkspace(this.organizationId);
|
||||
if (this.refreshRetryTimeout) {
|
||||
clearTimeout(this.refreshRetryTimeout);
|
||||
this.refreshRetryTimeout = null;
|
||||
}
|
||||
this.snapshot = {
|
||||
...nextSnapshot,
|
||||
repositories: nextSnapshot.repositories ?? groupWorkspaceRepositories(nextSnapshot.repos, nextSnapshot.tasks),
|
||||
};
|
||||
for (const listener of [...this.listeners]) {
|
||||
listener();
|
||||
}
|
||||
})().finally(() => {
|
||||
this.refreshPromise = null;
|
||||
});
|
||||
|
||||
await this.refreshPromise;
|
||||
}
|
||||
}
|
||||
|
||||
export function createRemoteWorkspaceClient(options: RemoteWorkspaceClientOptions): TaskWorkspaceClient {
|
||||
return new RemoteWorkspaceStore(options);
|
||||
}
|
||||
|
|
@ -5,8 +5,8 @@ import type {
|
|||
SandboxProcessesEvent,
|
||||
SessionEvent,
|
||||
TaskEvent,
|
||||
WorkbenchSessionDetail,
|
||||
WorkbenchTaskDetail,
|
||||
WorkspaceSessionDetail,
|
||||
WorkspaceTaskDetail,
|
||||
OrganizationEvent,
|
||||
OrganizationSummarySnapshot,
|
||||
} from "@sandbox-agent/foundry-shared";
|
||||
|
|
@ -48,16 +48,6 @@ export interface SandboxProcessesTopicParams {
|
|||
sandboxId: string;
|
||||
}
|
||||
|
||||
function upsertById<T extends { id: string }>(items: T[], nextItem: T, sort: (left: T, right: T) => number): T[] {
|
||||
const filtered = items.filter((item) => item.id !== nextItem.id);
|
||||
return [...filtered, nextItem].sort(sort);
|
||||
}
|
||||
|
||||
function upsertByPrId<T extends { prId: string }>(items: T[], nextItem: T, sort: (left: T, right: T) => number): T[] {
|
||||
const filtered = items.filter((item) => item.prId !== nextItem.prId);
|
||||
return [...filtered, nextItem].sort(sort);
|
||||
}
|
||||
|
||||
export const topicDefinitions = {
|
||||
app: {
|
||||
key: () => "app",
|
||||
|
|
@ -72,41 +62,7 @@ export const topicDefinitions = {
|
|||
event: "organizationUpdated",
|
||||
connect: (backend: BackendClient, params: OrganizationTopicParams) => backend.connectOrganization(params.organizationId),
|
||||
fetchInitial: (backend: BackendClient, params: OrganizationTopicParams) => backend.getOrganizationSummary(params.organizationId),
|
||||
applyEvent: (current: OrganizationSummarySnapshot, event: OrganizationEvent) => {
|
||||
switch (event.type) {
|
||||
case "taskSummaryUpdated":
|
||||
return {
|
||||
...current,
|
||||
taskSummaries: upsertById(current.taskSummaries, event.taskSummary, (left, right) => right.updatedAtMs - left.updatedAtMs),
|
||||
};
|
||||
case "taskRemoved":
|
||||
return {
|
||||
...current,
|
||||
taskSummaries: current.taskSummaries.filter((task) => task.id !== event.taskId),
|
||||
};
|
||||
case "repoAdded":
|
||||
case "repoUpdated":
|
||||
return {
|
||||
...current,
|
||||
repos: upsertById(current.repos, event.repo, (left, right) => right.latestActivityMs - left.latestActivityMs),
|
||||
};
|
||||
case "repoRemoved":
|
||||
return {
|
||||
...current,
|
||||
repos: current.repos.filter((repo) => repo.id !== event.repoId),
|
||||
};
|
||||
case "pullRequestUpdated":
|
||||
return {
|
||||
...current,
|
||||
openPullRequests: upsertByPrId(current.openPullRequests, event.pullRequest, (left, right) => right.updatedAtMs - left.updatedAtMs),
|
||||
};
|
||||
case "pullRequestRemoved":
|
||||
return {
|
||||
...current,
|
||||
openPullRequests: current.openPullRequests.filter((pullRequest) => pullRequest.prId !== event.prId),
|
||||
};
|
||||
}
|
||||
},
|
||||
applyEvent: (_current: OrganizationSummarySnapshot, event: OrganizationEvent) => event.snapshot,
|
||||
} satisfies TopicDefinition<OrganizationSummarySnapshot, OrganizationTopicParams, OrganizationEvent>,
|
||||
|
||||
task: {
|
||||
|
|
@ -114,8 +70,8 @@ export const topicDefinitions = {
|
|||
event: "taskUpdated",
|
||||
connect: (backend: BackendClient, params: TaskTopicParams) => backend.connectTask(params.organizationId, params.repoId, params.taskId),
|
||||
fetchInitial: (backend: BackendClient, params: TaskTopicParams) => backend.getTaskDetail(params.organizationId, params.repoId, params.taskId),
|
||||
applyEvent: (_current: WorkbenchTaskDetail, event: TaskEvent) => event.detail,
|
||||
} satisfies TopicDefinition<WorkbenchTaskDetail, TaskTopicParams, TaskEvent>,
|
||||
applyEvent: (_current: WorkspaceTaskDetail, event: TaskEvent) => event.detail,
|
||||
} satisfies TopicDefinition<WorkspaceTaskDetail, TaskTopicParams, TaskEvent>,
|
||||
|
||||
session: {
|
||||
key: (params: SessionTopicParams) => `session:${params.organizationId}:${params.taskId}:${params.sessionId}`,
|
||||
|
|
@ -123,13 +79,13 @@ export const topicDefinitions = {
|
|||
connect: (backend: BackendClient, params: SessionTopicParams) => backend.connectTask(params.organizationId, params.repoId, params.taskId),
|
||||
fetchInitial: (backend: BackendClient, params: SessionTopicParams) =>
|
||||
backend.getSessionDetail(params.organizationId, params.repoId, params.taskId, params.sessionId),
|
||||
applyEvent: (current: WorkbenchSessionDetail, event: SessionEvent) => {
|
||||
applyEvent: (current: WorkspaceSessionDetail, event: SessionEvent) => {
|
||||
if (event.session.sessionId !== current.sessionId) {
|
||||
return current;
|
||||
}
|
||||
return event.session;
|
||||
},
|
||||
} satisfies TopicDefinition<WorkbenchSessionDetail, SessionTopicParams, SessionEvent>,
|
||||
} satisfies TopicDefinition<WorkspaceSessionDetail, SessionTopicParams, SessionEvent>,
|
||||
|
||||
sandboxProcesses: {
|
||||
key: (params: SandboxProcessesTopicParams) => `sandbox:${params.organizationId}:${params.sandboxProviderId}:${params.sandboxId}`,
|
||||
|
|
|
|||
|
|
@ -1,64 +0,0 @@
|
|||
import type {
|
||||
TaskWorkbenchAddSessionResponse,
|
||||
TaskWorkbenchChangeModelInput,
|
||||
TaskWorkbenchCreateTaskInput,
|
||||
TaskWorkbenchCreateTaskResponse,
|
||||
TaskWorkbenchDiffInput,
|
||||
TaskWorkbenchRenameInput,
|
||||
TaskWorkbenchRenameSessionInput,
|
||||
TaskWorkbenchSelectInput,
|
||||
TaskWorkbenchSetSessionUnreadInput,
|
||||
TaskWorkbenchSendMessageInput,
|
||||
TaskWorkbenchSnapshot,
|
||||
TaskWorkbenchSessionInput,
|
||||
TaskWorkbenchUpdateDraftInput,
|
||||
} from "@sandbox-agent/foundry-shared";
|
||||
import type { BackendClient } from "./backend-client.js";
|
||||
import { getSharedMockWorkbenchClient } from "./mock/workbench-client.js";
|
||||
import { createRemoteWorkbenchClient } from "./remote/workbench-client.js";
|
||||
|
||||
export type TaskWorkbenchClientMode = "mock" | "remote";
|
||||
|
||||
export interface CreateTaskWorkbenchClientOptions {
|
||||
mode: TaskWorkbenchClientMode;
|
||||
backend?: BackendClient;
|
||||
organizationId?: string;
|
||||
}
|
||||
|
||||
export interface TaskWorkbenchClient {
|
||||
getSnapshot(): TaskWorkbenchSnapshot;
|
||||
subscribe(listener: () => void): () => void;
|
||||
createTask(input: TaskWorkbenchCreateTaskInput): Promise<TaskWorkbenchCreateTaskResponse>;
|
||||
markTaskUnread(input: TaskWorkbenchSelectInput): Promise<void>;
|
||||
renameTask(input: TaskWorkbenchRenameInput): Promise<void>;
|
||||
renameBranch(input: TaskWorkbenchRenameInput): Promise<void>;
|
||||
archiveTask(input: TaskWorkbenchSelectInput): Promise<void>;
|
||||
publishPr(input: TaskWorkbenchSelectInput): Promise<void>;
|
||||
revertFile(input: TaskWorkbenchDiffInput): Promise<void>;
|
||||
updateDraft(input: TaskWorkbenchUpdateDraftInput): Promise<void>;
|
||||
sendMessage(input: TaskWorkbenchSendMessageInput): Promise<void>;
|
||||
stopAgent(input: TaskWorkbenchSessionInput): Promise<void>;
|
||||
setSessionUnread(input: TaskWorkbenchSetSessionUnreadInput): Promise<void>;
|
||||
renameSession(input: TaskWorkbenchRenameSessionInput): Promise<void>;
|
||||
closeSession(input: TaskWorkbenchSessionInput): Promise<void>;
|
||||
addSession(input: TaskWorkbenchSelectInput): Promise<TaskWorkbenchAddSessionResponse>;
|
||||
changeModel(input: TaskWorkbenchChangeModelInput): Promise<void>;
|
||||
}
|
||||
|
||||
export function createTaskWorkbenchClient(options: CreateTaskWorkbenchClientOptions): TaskWorkbenchClient {
|
||||
if (options.mode === "mock") {
|
||||
return getSharedMockWorkbenchClient();
|
||||
}
|
||||
|
||||
if (!options.backend) {
|
||||
throw new Error("Remote task workbench client requires a backend client");
|
||||
}
|
||||
if (!options.organizationId) {
|
||||
throw new Error("Remote task workbench client requires a organization id");
|
||||
}
|
||||
|
||||
return createRemoteWorkbenchClient({
|
||||
backend: options.backend,
|
||||
organizationId: options.organizationId,
|
||||
});
|
||||
}
|
||||
63
foundry/packages/client/src/workspace-client.ts
Normal file
63
foundry/packages/client/src/workspace-client.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import type {
|
||||
TaskWorkspaceAddSessionResponse,
|
||||
TaskWorkspaceChangeModelInput,
|
||||
TaskWorkspaceCreateTaskInput,
|
||||
TaskWorkspaceCreateTaskResponse,
|
||||
TaskWorkspaceDiffInput,
|
||||
TaskWorkspaceRenameInput,
|
||||
TaskWorkspaceRenameSessionInput,
|
||||
TaskWorkspaceSelectInput,
|
||||
TaskWorkspaceSetSessionUnreadInput,
|
||||
TaskWorkspaceSendMessageInput,
|
||||
TaskWorkspaceSnapshot,
|
||||
TaskWorkspaceSessionInput,
|
||||
TaskWorkspaceUpdateDraftInput,
|
||||
} from "@sandbox-agent/foundry-shared";
|
||||
import type { BackendClient } from "./backend-client.js";
|
||||
import { getSharedMockWorkspaceClient } from "./mock/workspace-client.js";
|
||||
import { createRemoteWorkspaceClient } from "./remote/workspace-client.js";
|
||||
|
||||
export type TaskWorkspaceClientMode = "mock" | "remote";
|
||||
|
||||
export interface CreateTaskWorkspaceClientOptions {
|
||||
mode: TaskWorkspaceClientMode;
|
||||
backend?: BackendClient;
|
||||
organizationId?: string;
|
||||
}
|
||||
|
||||
export interface TaskWorkspaceClient {
|
||||
getSnapshot(): TaskWorkspaceSnapshot;
|
||||
subscribe(listener: () => void): () => void;
|
||||
createTask(input: TaskWorkspaceCreateTaskInput): Promise<TaskWorkspaceCreateTaskResponse>;
|
||||
markTaskUnread(input: TaskWorkspaceSelectInput): Promise<void>;
|
||||
renameTask(input: TaskWorkspaceRenameInput): Promise<void>;
|
||||
archiveTask(input: TaskWorkspaceSelectInput): Promise<void>;
|
||||
publishPr(input: TaskWorkspaceSelectInput): Promise<void>;
|
||||
revertFile(input: TaskWorkspaceDiffInput): Promise<void>;
|
||||
updateDraft(input: TaskWorkspaceUpdateDraftInput): Promise<void>;
|
||||
sendMessage(input: TaskWorkspaceSendMessageInput): Promise<void>;
|
||||
stopAgent(input: TaskWorkspaceSessionInput): Promise<void>;
|
||||
setSessionUnread(input: TaskWorkspaceSetSessionUnreadInput): Promise<void>;
|
||||
renameSession(input: TaskWorkspaceRenameSessionInput): Promise<void>;
|
||||
closeSession(input: TaskWorkspaceSessionInput): Promise<void>;
|
||||
addSession(input: TaskWorkspaceSelectInput): Promise<TaskWorkspaceAddSessionResponse>;
|
||||
changeModel(input: TaskWorkspaceChangeModelInput): Promise<void>;
|
||||
}
|
||||
|
||||
export function createTaskWorkspaceClient(options: CreateTaskWorkspaceClientOptions): TaskWorkspaceClient {
|
||||
if (options.mode === "mock") {
|
||||
return getSharedMockWorkspaceClient();
|
||||
}
|
||||
|
||||
if (!options.backend) {
|
||||
throw new Error("Remote task workspace client requires a backend client");
|
||||
}
|
||||
if (!options.organizationId) {
|
||||
throw new Error("Remote task workspace client requires a organization id");
|
||||
}
|
||||
|
||||
return createRemoteWorkspaceClient({
|
||||
backend: options.backend,
|
||||
organizationId: options.organizationId,
|
||||
});
|
||||
}
|
||||
|
|
@ -1,17 +1,17 @@
|
|||
import type {
|
||||
WorkbenchAgentKind as AgentKind,
|
||||
WorkbenchSession as AgentSession,
|
||||
WorkbenchDiffLineKind as DiffLineKind,
|
||||
WorkbenchFileTreeNode as FileTreeNode,
|
||||
WorkbenchTask as Task,
|
||||
TaskWorkbenchSnapshot,
|
||||
WorkbenchHistoryEvent as HistoryEvent,
|
||||
WorkbenchModelGroup as ModelGroup,
|
||||
WorkbenchModelId as ModelId,
|
||||
WorkbenchParsedDiffLine as ParsedDiffLine,
|
||||
WorkbenchRepositorySection,
|
||||
WorkbenchRepo,
|
||||
WorkbenchTranscriptEvent as TranscriptEvent,
|
||||
WorkspaceAgentKind as AgentKind,
|
||||
WorkspaceSession as AgentSession,
|
||||
WorkspaceDiffLineKind as DiffLineKind,
|
||||
WorkspaceFileTreeNode as FileTreeNode,
|
||||
WorkspaceTask as Task,
|
||||
TaskWorkspaceSnapshot,
|
||||
WorkspaceHistoryEvent as HistoryEvent,
|
||||
WorkspaceModelGroup as ModelGroup,
|
||||
WorkspaceModelId as ModelId,
|
||||
WorkspaceParsedDiffLine as ParsedDiffLine,
|
||||
WorkspaceRepositorySection,
|
||||
WorkspaceRepo,
|
||||
WorkspaceTranscriptEvent as TranscriptEvent,
|
||||
} from "@sandbox-agent/foundry-shared";
|
||||
import rivetDevFixture from "../../../scripts/data/rivet-dev.json" with { type: "json" };
|
||||
|
||||
|
|
@ -1300,7 +1300,7 @@ export function buildInitialTasks(): Task[] {
|
|||
* Uses real public repos so the mock sidebar matches what an actual rivet-dev
|
||||
* organization would show after a GitHub sync.
|
||||
*/
|
||||
function buildMockRepos(): WorkbenchRepo[] {
|
||||
function buildMockRepos(): WorkspaceRepo[] {
|
||||
return rivetDevFixture.repos.map((r) => ({
|
||||
id: repoIdFromFullName(r.fullName),
|
||||
label: r.fullName,
|
||||
|
|
@ -1349,19 +1349,19 @@ function buildPrTasks(): Task[] {
|
|||
});
|
||||
}
|
||||
|
||||
export function buildInitialMockLayoutViewModel(): TaskWorkbenchSnapshot {
|
||||
export function buildInitialMockLayoutViewModel(): TaskWorkspaceSnapshot {
|
||||
const repos = buildMockRepos();
|
||||
const tasks = [...buildInitialTasks(), ...buildPrTasks()];
|
||||
return {
|
||||
organizationId: "default",
|
||||
repos,
|
||||
repositories: groupWorkbenchRepositories(repos, tasks),
|
||||
repositories: groupWorkspaceRepositories(repos, tasks),
|
||||
tasks,
|
||||
};
|
||||
}
|
||||
|
||||
export function groupWorkbenchRepositories(repos: WorkbenchRepo[], tasks: Task[]): WorkbenchRepositorySection[] {
|
||||
const grouped = new Map<string, WorkbenchRepositorySection>();
|
||||
export function groupWorkspaceRepositories(repos: WorkspaceRepo[], tasks: Task[]): WorkspaceRepositorySection[] {
|
||||
const grouped = new Map<string, WorkspaceRepositorySection>();
|
||||
|
||||
for (const repo of repos) {
|
||||
grouped.set(repo.id, {
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { randomUUID } from "node:crypto";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { HistoryEvent, RepoOverview } from "@sandbox-agent/foundry-shared";
|
||||
import type { AuditLogEvent as HistoryEvent, RepoOverview } from "@sandbox-agent/foundry-shared";
|
||||
import { createBackendClient } from "../../src/backend-client.js";
|
||||
import { requireImportedRepo } from "./helpers.js";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import type { TaskRecord, HistoryEvent } from "@sandbox-agent/foundry-shared";
|
||||
import type { AuditLogEvent as HistoryEvent, TaskRecord } from "@sandbox-agent/foundry-shared";
|
||||
import { createBackendClient } from "../../src/backend-client.js";
|
||||
import { requireImportedRepo } from "./helpers.js";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import type { TaskWorkbenchSnapshot, WorkbenchSession, WorkbenchTask, WorkbenchModelId, WorkbenchTranscriptEvent } from "@sandbox-agent/foundry-shared";
|
||||
import type { TaskWorkspaceSnapshot, WorkspaceSession, WorkspaceTask, WorkspaceModelId, WorkspaceTranscriptEvent } from "@sandbox-agent/foundry-shared";
|
||||
import { createBackendClient } from "../../src/backend-client.js";
|
||||
import { requireImportedRepo } from "./helpers.js";
|
||||
|
||||
|
|
@ -13,7 +13,7 @@ function requiredEnv(name: string): string {
|
|||
return value;
|
||||
}
|
||||
|
||||
function workbenchModelEnv(name: string, fallback: WorkbenchModelId): WorkbenchModelId {
|
||||
function workspaceModelEnv(name: string, fallback: WorkspaceModelId): WorkspaceModelId {
|
||||
const value = process.env[name]?.trim();
|
||||
switch (value) {
|
||||
case "claude-sonnet-4":
|
||||
|
|
@ -50,7 +50,7 @@ async function poll<T>(label: string, timeoutMs: number, intervalMs: number, fn:
|
|||
}
|
||||
}
|
||||
|
||||
function findTask(snapshot: TaskWorkbenchSnapshot, taskId: string): WorkbenchTask {
|
||||
function findTask(snapshot: TaskWorkspaceSnapshot, taskId: string): WorkspaceTask {
|
||||
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
||||
if (!task) {
|
||||
throw new Error(`task ${taskId} missing from snapshot`);
|
||||
|
|
@ -58,7 +58,7 @@ function findTask(snapshot: TaskWorkbenchSnapshot, taskId: string): WorkbenchTas
|
|||
return task;
|
||||
}
|
||||
|
||||
function findTab(task: WorkbenchTask, sessionId: string): WorkbenchSession {
|
||||
function findTab(task: WorkspaceTask, sessionId: string): WorkspaceSession {
|
||||
const tab = task.sessions.find((candidate) => candidate.id === sessionId);
|
||||
if (!tab) {
|
||||
throw new Error(`tab ${sessionId} missing from task ${task.id}`);
|
||||
|
|
@ -66,7 +66,7 @@ function findTab(task: WorkbenchTask, sessionId: string): WorkbenchSession {
|
|||
return tab;
|
||||
}
|
||||
|
||||
function extractEventText(event: WorkbenchTranscriptEvent): string {
|
||||
function extractEventText(event: WorkspaceTranscriptEvent): string {
|
||||
const payload = event.payload;
|
||||
if (!payload || typeof payload !== "object") {
|
||||
return String(payload ?? "");
|
||||
|
|
@ -127,7 +127,7 @@ function extractEventText(event: WorkbenchTranscriptEvent): string {
|
|||
return JSON.stringify(payload);
|
||||
}
|
||||
|
||||
function transcriptIncludesAgentText(transcript: WorkbenchTranscriptEvent[], expectedText: string): boolean {
|
||||
function transcriptIncludesAgentText(transcript: WorkspaceTranscriptEvent[], expectedText: string): boolean {
|
||||
return transcript
|
||||
.filter((event) => event.sender === "agent")
|
||||
.map((event) => extractEventText(event))
|
||||
|
|
@ -135,15 +135,15 @@ function transcriptIncludesAgentText(transcript: WorkbenchTranscriptEvent[], exp
|
|||
.includes(expectedText);
|
||||
}
|
||||
|
||||
describe("e2e(client): workbench flows", () => {
|
||||
describe("e2e(client): workspace flows", () => {
|
||||
it.skipIf(!RUN_WORKBENCH_E2E)(
|
||||
"creates a task from an imported repo, adds sessions, exchanges messages, and manages workbench state",
|
||||
"creates a task from an imported repo, adds sessions, exchanges messages, and manages workspace state",
|
||||
{ timeout: 20 * 60_000 },
|
||||
async () => {
|
||||
const endpoint = process.env.HF_E2E_BACKEND_ENDPOINT?.trim() || "http://127.0.0.1:7741/v1/rivet";
|
||||
const organizationId = process.env.HF_E2E_WORKSPACE?.trim() || "default";
|
||||
const repoRemote = requiredEnv("HF_E2E_GITHUB_REPO");
|
||||
const model = workbenchModelEnv("HF_E2E_MODEL", "gpt-5.3-codex");
|
||||
const model = workspaceModelEnv("HF_E2E_MODEL", "gpt-5.3-codex");
|
||||
const runId = `wb-${Date.now().toString(36)}`;
|
||||
const expectedFile = `${runId}.txt`;
|
||||
const expectedInitialReply = `WORKBENCH_READY_${runId}`;
|
||||
|
|
@ -155,9 +155,9 @@ describe("e2e(client): workbench flows", () => {
|
|||
});
|
||||
|
||||
const repo = await requireImportedRepo(client, organizationId, repoRemote);
|
||||
const created = await client.createWorkbenchTask(organizationId, {
|
||||
const created = await client.createWorkspaceTask(organizationId, {
|
||||
repoId: repo.repoId,
|
||||
title: `Workbench E2E ${runId}`,
|
||||
title: `Workspace E2E ${runId}`,
|
||||
branch: `e2e/${runId}`,
|
||||
model,
|
||||
task: `Reply with exactly: ${expectedInitialReply}`,
|
||||
|
|
@ -167,7 +167,7 @@ describe("e2e(client): workbench flows", () => {
|
|||
"task provisioning",
|
||||
12 * 60_000,
|
||||
2_000,
|
||||
async () => findTask(await client.getWorkbench(organizationId), created.taskId),
|
||||
async () => findTask(await client.getWorkspace(organizationId), created.taskId),
|
||||
(task) => task.branch === `e2e/${runId}` && task.sessions.length > 0,
|
||||
);
|
||||
|
||||
|
|
@ -177,7 +177,7 @@ describe("e2e(client): workbench flows", () => {
|
|||
"initial agent response",
|
||||
12 * 60_000,
|
||||
2_000,
|
||||
async () => findTask(await client.getWorkbench(organizationId), created.taskId),
|
||||
async () => findTask(await client.getWorkspace(organizationId), created.taskId),
|
||||
(task) => {
|
||||
const tab = findTab(task, primaryTab.id);
|
||||
return task.status === "idle" && tab.status === "idle" && transcriptIncludesAgentText(tab.transcript, expectedInitialReply);
|
||||
|
|
@ -187,28 +187,28 @@ describe("e2e(client): workbench flows", () => {
|
|||
expect(findTab(initialCompleted, primaryTab.id).sessionId).toBeTruthy();
|
||||
expect(transcriptIncludesAgentText(findTab(initialCompleted, primaryTab.id).transcript, expectedInitialReply)).toBe(true);
|
||||
|
||||
await client.renameWorkbenchTask(organizationId, {
|
||||
await client.renameWorkspaceTask(organizationId, {
|
||||
taskId: created.taskId,
|
||||
value: `Workbench E2E ${runId} Renamed`,
|
||||
value: `Workspace E2E ${runId} Renamed`,
|
||||
});
|
||||
await client.renameWorkbenchSession(organizationId, {
|
||||
await client.renameWorkspaceSession(organizationId, {
|
||||
taskId: created.taskId,
|
||||
sessionId: primaryTab.id,
|
||||
title: "Primary Session",
|
||||
});
|
||||
|
||||
const secondTab = await client.createWorkbenchSession(organizationId, {
|
||||
const secondTab = await client.createWorkspaceSession(organizationId, {
|
||||
taskId: created.taskId,
|
||||
model,
|
||||
});
|
||||
|
||||
await client.renameWorkbenchSession(organizationId, {
|
||||
await client.renameWorkspaceSession(organizationId, {
|
||||
taskId: created.taskId,
|
||||
sessionId: secondTab.sessionId,
|
||||
title: "Follow-up Session",
|
||||
});
|
||||
|
||||
await client.updateWorkbenchDraft(organizationId, {
|
||||
await client.updateWorkspaceDraft(organizationId, {
|
||||
taskId: created.taskId,
|
||||
sessionId: secondTab.sessionId,
|
||||
text: [
|
||||
|
|
@ -226,11 +226,11 @@ describe("e2e(client): workbench flows", () => {
|
|||
],
|
||||
});
|
||||
|
||||
const drafted = findTask(await client.getWorkbench(organizationId), created.taskId);
|
||||
const drafted = findTask(await client.getWorkspace(organizationId), created.taskId);
|
||||
expect(findTab(drafted, secondTab.sessionId).draft.text).toContain(expectedReply);
|
||||
expect(findTab(drafted, secondTab.sessionId).draft.attachments).toHaveLength(1);
|
||||
|
||||
await client.sendWorkbenchMessage(organizationId, {
|
||||
await client.sendWorkspaceMessage(organizationId, {
|
||||
taskId: created.taskId,
|
||||
sessionId: secondTab.sessionId,
|
||||
text: [
|
||||
|
|
@ -252,7 +252,7 @@ describe("e2e(client): workbench flows", () => {
|
|||
"follow-up session response",
|
||||
10 * 60_000,
|
||||
2_000,
|
||||
async () => findTask(await client.getWorkbench(organizationId), created.taskId),
|
||||
async () => findTask(await client.getWorkspace(organizationId), created.taskId),
|
||||
(task) => {
|
||||
const tab = findTab(task, secondTab.sessionId);
|
||||
return (
|
||||
|
|
@ -265,17 +265,17 @@ describe("e2e(client): workbench flows", () => {
|
|||
expect(transcriptIncludesAgentText(secondTranscript, expectedReply)).toBe(true);
|
||||
expect(withSecondReply.fileChanges.some((file) => file.path === expectedFile)).toBe(true);
|
||||
|
||||
await client.setWorkbenchSessionUnread(organizationId, {
|
||||
await client.setWorkspaceSessionUnread(organizationId, {
|
||||
taskId: created.taskId,
|
||||
sessionId: secondTab.sessionId,
|
||||
unread: false,
|
||||
});
|
||||
await client.markWorkbenchUnread(organizationId, { taskId: created.taskId });
|
||||
await client.markWorkspaceUnread(organizationId, { taskId: created.taskId });
|
||||
|
||||
const unreadSnapshot = findTask(await client.getWorkbench(organizationId), created.taskId);
|
||||
const unreadSnapshot = findTask(await client.getWorkspace(organizationId), created.taskId);
|
||||
expect(unreadSnapshot.sessions.some((tab) => tab.unread)).toBe(true);
|
||||
|
||||
await client.closeWorkbenchSession(organizationId, {
|
||||
await client.closeWorkspaceSession(organizationId, {
|
||||
taskId: created.taskId,
|
||||
sessionId: secondTab.sessionId,
|
||||
});
|
||||
|
|
@ -284,26 +284,26 @@ describe("e2e(client): workbench flows", () => {
|
|||
"secondary session closed",
|
||||
30_000,
|
||||
1_000,
|
||||
async () => findTask(await client.getWorkbench(organizationId), created.taskId),
|
||||
async () => findTask(await client.getWorkspace(organizationId), created.taskId),
|
||||
(task) => !task.sessions.some((tab) => tab.id === secondTab.sessionId),
|
||||
);
|
||||
expect(closedSnapshot.sessions).toHaveLength(1);
|
||||
|
||||
await client.revertWorkbenchFile(organizationId, {
|
||||
await client.revertWorkspaceFile(organizationId, {
|
||||
taskId: created.taskId,
|
||||
path: expectedFile,
|
||||
});
|
||||
|
||||
const revertedSnapshot = await poll(
|
||||
"file revert reflected in workbench",
|
||||
"file revert reflected in workspace",
|
||||
30_000,
|
||||
1_000,
|
||||
async () => findTask(await client.getWorkbench(organizationId), created.taskId),
|
||||
async () => findTask(await client.getWorkspace(organizationId), created.taskId),
|
||||
(task) => !task.fileChanges.some((file) => file.path === expectedFile),
|
||||
);
|
||||
|
||||
expect(revertedSnapshot.fileChanges.some((file) => file.path === expectedFile)).toBe(false);
|
||||
expect(revertedSnapshot.title).toBe(`Workbench E2E ${runId} Renamed`);
|
||||
expect(revertedSnapshot.title).toBe(`Workspace E2E ${runId} Renamed`);
|
||||
expect(findTab(revertedSnapshot, primaryTab.id).sessionName).toBe("Primary Session");
|
||||
},
|
||||
);
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
createFoundryLogger,
|
||||
type TaskWorkbenchSnapshot,
|
||||
type WorkbenchSession,
|
||||
type WorkbenchTask,
|
||||
type WorkbenchModelId,
|
||||
type WorkbenchTranscriptEvent,
|
||||
type TaskWorkspaceSnapshot,
|
||||
type WorkspaceSession,
|
||||
type WorkspaceTask,
|
||||
type WorkspaceModelId,
|
||||
type WorkspaceTranscriptEvent,
|
||||
} from "@sandbox-agent/foundry-shared";
|
||||
import { createBackendClient } from "../../src/backend-client.js";
|
||||
import { requireImportedRepo } from "./helpers.js";
|
||||
|
|
@ -14,7 +14,7 @@ const RUN_WORKBENCH_LOAD_E2E = process.env.HF_ENABLE_DAEMON_WORKBENCH_LOAD_E2E =
|
|||
const logger = createFoundryLogger({
|
||||
service: "foundry-client-e2e",
|
||||
bindings: {
|
||||
suite: "workbench-load",
|
||||
suite: "workspace-load",
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -26,7 +26,7 @@ function requiredEnv(name: string): string {
|
|||
return value;
|
||||
}
|
||||
|
||||
function workbenchModelEnv(name: string, fallback: WorkbenchModelId): WorkbenchModelId {
|
||||
function workspaceModelEnv(name: string, fallback: WorkspaceModelId): WorkspaceModelId {
|
||||
const value = process.env[name]?.trim();
|
||||
switch (value) {
|
||||
case "claude-sonnet-4":
|
||||
|
|
@ -72,7 +72,7 @@ async function poll<T>(label: string, timeoutMs: number, intervalMs: number, fn:
|
|||
}
|
||||
}
|
||||
|
||||
function findTask(snapshot: TaskWorkbenchSnapshot, taskId: string): WorkbenchTask {
|
||||
function findTask(snapshot: TaskWorkspaceSnapshot, taskId: string): WorkspaceTask {
|
||||
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
||||
if (!task) {
|
||||
throw new Error(`task ${taskId} missing from snapshot`);
|
||||
|
|
@ -80,7 +80,7 @@ function findTask(snapshot: TaskWorkbenchSnapshot, taskId: string): WorkbenchTas
|
|||
return task;
|
||||
}
|
||||
|
||||
function findTab(task: WorkbenchTask, sessionId: string): WorkbenchSession {
|
||||
function findTab(task: WorkspaceTask, sessionId: string): WorkspaceSession {
|
||||
const tab = task.sessions.find((candidate) => candidate.id === sessionId);
|
||||
if (!tab) {
|
||||
throw new Error(`tab ${sessionId} missing from task ${task.id}`);
|
||||
|
|
@ -88,7 +88,7 @@ function findTab(task: WorkbenchTask, sessionId: string): WorkbenchSession {
|
|||
return tab;
|
||||
}
|
||||
|
||||
function extractEventText(event: WorkbenchTranscriptEvent): string {
|
||||
function extractEventText(event: WorkspaceTranscriptEvent): string {
|
||||
const payload = event.payload;
|
||||
if (!payload || typeof payload !== "object") {
|
||||
return String(payload ?? "");
|
||||
|
|
@ -138,7 +138,7 @@ function extractEventText(event: WorkbenchTranscriptEvent): string {
|
|||
return typeof envelope.method === "string" ? envelope.method : JSON.stringify(payload);
|
||||
}
|
||||
|
||||
function transcriptIncludesAgentText(transcript: WorkbenchTranscriptEvent[], expectedText: string): boolean {
|
||||
function transcriptIncludesAgentText(transcript: WorkspaceTranscriptEvent[], expectedText: string): boolean {
|
||||
return transcript
|
||||
.filter((event) => event.sender === "agent")
|
||||
.map((event) => extractEventText(event))
|
||||
|
|
@ -150,7 +150,7 @@ function average(values: number[]): number {
|
|||
return values.reduce((sum, value) => sum + value, 0) / Math.max(values.length, 1);
|
||||
}
|
||||
|
||||
async function measureWorkbenchSnapshot(
|
||||
async function measureWorkspaceSnapshot(
|
||||
client: ReturnType<typeof createBackendClient>,
|
||||
organizationId: string,
|
||||
iterations: number,
|
||||
|
|
@ -163,11 +163,11 @@ async function measureWorkbenchSnapshot(
|
|||
transcriptEventCount: number;
|
||||
}> {
|
||||
const durations: number[] = [];
|
||||
let snapshot: TaskWorkbenchSnapshot | null = null;
|
||||
let snapshot: TaskWorkspaceSnapshot | null = null;
|
||||
|
||||
for (let index = 0; index < iterations; index += 1) {
|
||||
const startedAt = performance.now();
|
||||
snapshot = await client.getWorkbench(organizationId);
|
||||
snapshot = await client.getWorkspace(organizationId);
|
||||
durations.push(performance.now() - startedAt);
|
||||
}
|
||||
|
||||
|
|
@ -191,12 +191,12 @@ async function measureWorkbenchSnapshot(
|
|||
};
|
||||
}
|
||||
|
||||
describe("e2e(client): workbench load", () => {
|
||||
describe("e2e(client): workspace load", () => {
|
||||
it.skipIf(!RUN_WORKBENCH_LOAD_E2E)("runs a simple sequential load profile against the real backend", { timeout: 30 * 60_000 }, async () => {
|
||||
const endpoint = process.env.HF_E2E_BACKEND_ENDPOINT?.trim() || "http://127.0.0.1:7741/v1/rivet";
|
||||
const organizationId = process.env.HF_E2E_WORKSPACE?.trim() || "default";
|
||||
const repoRemote = requiredEnv("HF_E2E_GITHUB_REPO");
|
||||
const model = workbenchModelEnv("HF_E2E_MODEL", "gpt-5.3-codex");
|
||||
const model = workspaceModelEnv("HF_E2E_MODEL", "gpt-5.3-codex");
|
||||
const taskCount = intEnv("HF_LOAD_TASK_COUNT", 3);
|
||||
const extraSessionCount = intEnv("HF_LOAD_EXTRA_SESSION_COUNT", 2);
|
||||
const pollIntervalMs = intEnv("HF_LOAD_POLL_INTERVAL_MS", 2_000);
|
||||
|
|
@ -220,16 +220,16 @@ describe("e2e(client): workbench load", () => {
|
|||
transcriptEventCount: number;
|
||||
}> = [];
|
||||
|
||||
snapshotSeries.push(await measureWorkbenchSnapshot(client, organizationId, 2));
|
||||
snapshotSeries.push(await measureWorkspaceSnapshot(client, organizationId, 2));
|
||||
|
||||
for (let taskIndex = 0; taskIndex < taskCount; taskIndex += 1) {
|
||||
const runId = `load-${taskIndex}-${Date.now().toString(36)}`;
|
||||
const initialReply = `LOAD_INIT_${runId}`;
|
||||
|
||||
const createStartedAt = performance.now();
|
||||
const created = await client.createWorkbenchTask(organizationId, {
|
||||
const created = await client.createWorkspaceTask(organizationId, {
|
||||
repoId: repo.repoId,
|
||||
title: `Workbench Load ${runId}`,
|
||||
title: `Workspace Load ${runId}`,
|
||||
branch: `load/${runId}`,
|
||||
model,
|
||||
task: `Reply with exactly: ${initialReply}`,
|
||||
|
|
@ -241,7 +241,7 @@ describe("e2e(client): workbench load", () => {
|
|||
`task ${runId} provisioning`,
|
||||
12 * 60_000,
|
||||
pollIntervalMs,
|
||||
async () => findTask(await client.getWorkbench(organizationId), created.taskId),
|
||||
async () => findTask(await client.getWorkspace(organizationId), created.taskId),
|
||||
(task) => {
|
||||
const tab = task.sessions[0];
|
||||
return Boolean(tab && task.status === "idle" && tab.status === "idle" && transcriptIncludesAgentText(tab.transcript, initialReply));
|
||||
|
|
@ -256,13 +256,13 @@ describe("e2e(client): workbench load", () => {
|
|||
for (let sessionIndex = 0; sessionIndex < extraSessionCount; sessionIndex += 1) {
|
||||
const expectedReply = `LOAD_REPLY_${runId}_${sessionIndex}`;
|
||||
const createSessionStartedAt = performance.now();
|
||||
const createdSession = await client.createWorkbenchSession(organizationId, {
|
||||
const createdSession = await client.createWorkspaceSession(organizationId, {
|
||||
taskId: created.taskId,
|
||||
model,
|
||||
});
|
||||
createSessionLatencies.push(performance.now() - createSessionStartedAt);
|
||||
|
||||
await client.sendWorkbenchMessage(organizationId, {
|
||||
await client.sendWorkspaceMessage(organizationId, {
|
||||
taskId: created.taskId,
|
||||
sessionId: createdSession.sessionId,
|
||||
text: `Run pwd in the repo, then reply with exactly: ${expectedReply}`,
|
||||
|
|
@ -274,7 +274,7 @@ describe("e2e(client): workbench load", () => {
|
|||
`task ${runId} session ${sessionIndex} reply`,
|
||||
10 * 60_000,
|
||||
pollIntervalMs,
|
||||
async () => findTask(await client.getWorkbench(organizationId), created.taskId),
|
||||
async () => findTask(await client.getWorkspace(organizationId), created.taskId),
|
||||
(task) => {
|
||||
const tab = findTab(task, createdSession.sessionId);
|
||||
return tab.status === "idle" && transcriptIncludesAgentText(tab.transcript, expectedReply);
|
||||
|
|
@ -285,14 +285,14 @@ describe("e2e(client): workbench load", () => {
|
|||
expect(transcriptIncludesAgentText(findTab(withReply, createdSession.sessionId).transcript, expectedReply)).toBe(true);
|
||||
}
|
||||
|
||||
const snapshotMetrics = await measureWorkbenchSnapshot(client, organizationId, 3);
|
||||
const snapshotMetrics = await measureWorkspaceSnapshot(client, organizationId, 3);
|
||||
snapshotSeries.push(snapshotMetrics);
|
||||
logger.info(
|
||||
{
|
||||
taskIndex: taskIndex + 1,
|
||||
...snapshotMetrics,
|
||||
},
|
||||
"workbench_load_snapshot",
|
||||
"workspace_load_snapshot",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -314,7 +314,7 @@ describe("e2e(client): workbench load", () => {
|
|||
snapshotTranscriptFinalCount: lastSnapshot.transcriptEventCount,
|
||||
};
|
||||
|
||||
logger.info(summary, "workbench_load_summary");
|
||||
logger.info(summary, "workspace_load_summary");
|
||||
|
||||
expect(createTaskLatencies.length).toBe(taskCount);
|
||||
expect(provisionLatencies.length).toBe(taskCount);
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { historyKey, organizationKey, repositoryKey, taskKey, taskSandboxKey } from "../src/keys.js";
|
||||
import { auditLogKey, organizationKey, repositoryKey, taskKey, taskSandboxKey } from "../src/keys.js";
|
||||
|
||||
describe("actor keys", () => {
|
||||
it("prefixes every key with organization namespace", () => {
|
||||
|
|
@ -8,7 +8,7 @@ describe("actor keys", () => {
|
|||
repositoryKey("default", "repo"),
|
||||
taskKey("default", "repo", "task"),
|
||||
taskSandboxKey("default", "sbx"),
|
||||
historyKey("default", "repo"),
|
||||
auditLogKey("default", "repo"),
|
||||
];
|
||||
|
||||
for (const key of keys) {
|
||||
|
|
|
|||
|
|
@ -115,17 +115,24 @@ describe("RemoteSubscriptionManager", () => {
|
|||
]);
|
||||
|
||||
conn.emit("organizationUpdated", {
|
||||
type: "taskSummaryUpdated",
|
||||
taskSummary: {
|
||||
id: "task-1",
|
||||
repoId: "repo-1",
|
||||
title: "Updated task",
|
||||
status: "running",
|
||||
repoName: "repo-1",
|
||||
updatedAtMs: 20,
|
||||
branch: "feature/live",
|
||||
pullRequest: null,
|
||||
sessionsSummary: [],
|
||||
type: "organizationUpdated",
|
||||
snapshot: {
|
||||
organizationId: "org-1",
|
||||
repos: [],
|
||||
taskSummaries: [
|
||||
{
|
||||
id: "task-1",
|
||||
repoId: "repo-1",
|
||||
title: "Updated task",
|
||||
status: "running",
|
||||
repoName: "repo-1",
|
||||
updatedAtMs: 20,
|
||||
branch: "feature/live",
|
||||
pullRequest: null,
|
||||
sessionsSummary: [],
|
||||
},
|
||||
],
|
||||
openPullRequests: [],
|
||||
},
|
||||
} satisfies OrganizationEvent);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue