mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-16 13:04:11 +00:00
chore(foundry): migrate to actions (#262)
* feat(foundry): checkpoint actor and workspace refactor
* docs(foundry): add agent handoff context
* wip(foundry): continue actor refactor
* wip(foundry): capture remaining local changes
* Complete Foundry refactor checklist
* Fix Foundry validation fallout
* wip
* wip: convert all actors from workflow to plain run handlers
Workaround for RivetKit bug where c.queue.iter() never yields messages
for actors created via getOrCreate from another actor's context. The
queue accepts messages (visible in inspector) but the iterator hangs.
Sleep/wake fixes it, but actors with active connections never sleep.
Converted organization, github-data, task, and user actors from
run: workflow(...) to plain run: async (c) => { for await ... }.
Also fixes:
- Missing auth tables in org migration (auth_verification etc)
- default_model NOT NULL constraint on org profile upsert
- Nested workflow step in github-data (HistoryDivergedError)
- Removed --force from frontend Dockerfile pnpm install
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Convert all actors from queues/workflows to direct actions, lazy task creation
Major refactor replacing all queue-based workflow communication with direct
RivetKit action calls across all actors. This works around a RivetKit bug
where c.queue.iter() deadlocks for actors created from another actor's context.
Key changes:
- All actors (organization, task, user, audit-log, github-data) converted
from run: workflow(...) to actions-only (no run handler, no queues)
- PR sync creates virtual task entries in org local DB instead of spawning
task actors — prevents OOM from 200+ actors created simultaneously
- Task actors created lazily on first user interaction via getOrCreate,
self-initialize from org's getTaskIndexEntry data
- Removed requireRepoExists cross-actor call (caused 500s), replaced with
local resolveTaskRepoId from org's taskIndex table
- Fixed getOrganizationContext to thread overrides through all sync phases
- Fixed sandbox repo path (/home/user/repo for E2B compatibility)
- Fixed buildSessionDetail to skip transcript fetch for pending sessions
- Added process crash protection (uncaughtException/unhandledRejection)
- Fixed React infinite render loop in mock-layout useEffect dependencies
- Added sandbox listProcesses error handling for expired E2B sandboxes
- Set E2B sandbox timeout to 1 hour (was 5 min default)
- Updated CLAUDE.md with lazy task creation rules, no-silent-catch policy,
React hook dependency safety rules
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix E2B sandbox timeout comment, frontend stability, and create-flow improvements
- Add TEMPORARY comment on E2B timeoutMs with pointer to rivetkit sandbox
resilience proposal for when autoPause lands
- Fix React useEffect dependency stability in mock-layout and
organization-dashboard to prevent infinite re-render loops
- Fix terminal-pane ref handling
- Improve create-flow service and tests
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
32f3c6c3bc
commit
f45a467484
139 changed files with 9768 additions and 7204 deletions
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -7,28 +7,29 @@ import type {
|
|||
CreateTaskInput,
|
||||
AppEvent,
|
||||
SessionEvent,
|
||||
SandboxProcessSnapshot,
|
||||
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,8 +38,10 @@ import type {
|
|||
StarSandboxAgentRepoResult,
|
||||
SwitchResult,
|
||||
UpdateFoundryOrganizationProfileInput,
|
||||
WorkspaceModelGroup,
|
||||
WorkspaceModelId,
|
||||
} from "@sandbox-agent/foundry-shared";
|
||||
import type { ProcessCreateRequest, ProcessInfo, ProcessLogFollowQuery, ProcessLogsResponse, ProcessSignalQuery } from "sandbox-agent";
|
||||
import type { ProcessCreateRequest, ProcessLogFollowQuery, ProcessLogsResponse, ProcessSignalQuery } from "sandbox-agent";
|
||||
import { createMockBackendClient } from "./mock/backend-client.js";
|
||||
import { taskKey, taskSandboxKey, organizationKey } from "./keys.js";
|
||||
|
||||
|
|
@ -64,7 +67,7 @@ export interface SandboxSessionEventRecord {
|
|||
payload: unknown;
|
||||
}
|
||||
|
||||
export type SandboxProcessRecord = ProcessInfo;
|
||||
export type SandboxProcessRecord = SandboxProcessSnapshot;
|
||||
|
||||
export interface ActorConn {
|
||||
on(event: string, listener: (payload: any) => void): () => void;
|
||||
|
|
@ -72,45 +75,44 @@ export interface ActorConn {
|
|||
dispose(): Promise<void>;
|
||||
}
|
||||
|
||||
interface AuthSessionScopedInput {
|
||||
authSessionId?: string;
|
||||
}
|
||||
|
||||
interface OrganizationHandle {
|
||||
connect(): ActorConn;
|
||||
listRepos(input: { organizationId: string }): Promise<RepoRecord[]>;
|
||||
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>;
|
||||
createWorkspaceTask(input: TaskWorkspaceCreateTaskInput & AuthSessionScopedInput): Promise<TaskWorkspaceCreateTaskResponse>;
|
||||
markWorkspaceUnread(input: TaskWorkspaceSelectInput & AuthSessionScopedInput): Promise<void>;
|
||||
renameWorkspaceTask(input: TaskWorkspaceRenameInput & AuthSessionScopedInput): Promise<void>;
|
||||
createWorkspaceSession(input: TaskWorkspaceSelectInput & { model?: string } & AuthSessionScopedInput): Promise<{ sessionId: string }>;
|
||||
renameWorkspaceSession(input: TaskWorkspaceRenameSessionInput & AuthSessionScopedInput): Promise<void>;
|
||||
selectWorkspaceSession(input: TaskWorkspaceSessionInput & AuthSessionScopedInput): Promise<void>;
|
||||
setWorkspaceSessionUnread(input: TaskWorkspaceSetSessionUnreadInput & AuthSessionScopedInput): Promise<void>;
|
||||
updateWorkspaceDraft(input: TaskWorkspaceUpdateDraftInput & AuthSessionScopedInput): Promise<void>;
|
||||
changeWorkspaceModel(input: TaskWorkspaceChangeModelInput & AuthSessionScopedInput): Promise<void>;
|
||||
sendWorkspaceMessage(input: TaskWorkspaceSendMessageInput & AuthSessionScopedInput): Promise<void>;
|
||||
stopWorkspaceSession(input: TaskWorkspaceSessionInput & AuthSessionScopedInput): Promise<void>;
|
||||
closeWorkspaceSession(input: TaskWorkspaceSessionInput & AuthSessionScopedInput): Promise<void>;
|
||||
publishWorkspacePr(input: TaskWorkspaceSelectInput & AuthSessionScopedInput): Promise<void>;
|
||||
revertWorkspaceFile(input: TaskWorkspaceDiffInput & AuthSessionScopedInput): Promise<void>;
|
||||
adminReloadGithubOrganization(): Promise<void>;
|
||||
adminReloadGithubRepository(input: { repoId: string }): Promise<void>;
|
||||
}
|
||||
|
||||
interface AppOrganizationHandle {
|
||||
|
|
@ -119,6 +121,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 +133,9 @@ interface AppOrganizationHandle {
|
|||
}
|
||||
|
||||
interface TaskHandle {
|
||||
getTaskSummary(): Promise<WorkbenchTaskSummary>;
|
||||
getTaskDetail(): Promise<WorkbenchTaskDetail>;
|
||||
getSessionDetail(input: { sessionId: string }): Promise<WorkbenchSessionDetail>;
|
||||
getTaskSummary(): Promise<WorkspaceTaskSummary>;
|
||||
getTaskDetail(input?: AuthSessionScopedInput): Promise<WorkspaceTaskDetail>;
|
||||
getSessionDetail(input: { sessionId: string } & AuthSessionScopedInput): Promise<WorkspaceSessionDetail>;
|
||||
connect(): ActorConn;
|
||||
}
|
||||
|
||||
|
|
@ -157,6 +160,7 @@ interface TaskSandboxHandle {
|
|||
rawSendSessionMethod(sessionId: string, method: string, params: Record<string, unknown>): Promise<unknown>;
|
||||
destroySession(sessionId: string): Promise<void>;
|
||||
sandboxAgentConnection(): Promise<{ endpoint: string; token?: string }>;
|
||||
listWorkspaceModelGroups(): Promise<WorkspaceModelGroup[]>;
|
||||
providerState(): Promise<{ sandboxProviderId: SandboxProviderId; sandboxId: string; state: string; at: number }>;
|
||||
}
|
||||
|
||||
|
|
@ -179,6 +183,7 @@ export interface BackendClientOptions {
|
|||
endpoint: string;
|
||||
defaultOrganizationId?: string;
|
||||
mode?: "remote" | "mock";
|
||||
encoding?: "json" | "cbor" | "bare";
|
||||
}
|
||||
|
||||
export interface BackendClient {
|
||||
|
|
@ -192,6 +197,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 +210,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;
|
||||
|
|
@ -279,29 +285,28 @@ export interface BackendClient {
|
|||
sandboxId: string,
|
||||
): Promise<{ sandboxProviderId: SandboxProviderId; sandboxId: string; state: string; at: number }>;
|
||||
getSandboxAgentConnection(organizationId: string, sandboxProviderId: SandboxProviderId, sandboxId: string): Promise<{ endpoint: string; token?: string }>;
|
||||
getSandboxWorkspaceModelGroups(organizationId: string, sandboxProviderId: SandboxProviderId, sandboxId: string): Promise<WorkspaceModelGroup[]>;
|
||||
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>;
|
||||
selectWorkspaceSession(organizationId: string, input: TaskWorkspaceSessionInput): 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>;
|
||||
adminReloadGithubRepository(organizationId: string, repoId: string): Promise<void>;
|
||||
health(): Promise<{ ok: true }>;
|
||||
useOrganization(organizationId: string): Promise<{ organizationId: string }>;
|
||||
starSandboxAgentRepo(organizationId: string): Promise<StarSandboxAgentRepoResult>;
|
||||
|
|
@ -409,8 +414,8 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
const endpoints = deriveBackendEndpoints(options.endpoint);
|
||||
const rivetApiEndpoint = endpoints.rivetEndpoint;
|
||||
const appApiEndpoint = endpoints.appEndpoint;
|
||||
const client = createClient({ endpoint: rivetApiEndpoint }) as unknown as RivetClient;
|
||||
const workbenchSubscriptions = new Map<
|
||||
const client = createClient({ endpoint: rivetApiEndpoint, encoding: options.encoding }) as unknown as RivetClient;
|
||||
const workspaceSubscriptions = new Map<
|
||||
string,
|
||||
{
|
||||
listeners: Set<() => void>;
|
||||
|
|
@ -461,6 +466,16 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
return typeof sessionId === "string" && sessionId.length > 0 ? sessionId : null;
|
||||
};
|
||||
|
||||
const getAuthSessionInput = async (): Promise<AuthSessionScopedInput | undefined> => {
|
||||
const authSessionId = await getSessionId();
|
||||
return authSessionId ? { authSessionId } : undefined;
|
||||
};
|
||||
|
||||
const withAuthSessionInput = async <TInput extends object>(input: TInput): Promise<TInput & AuthSessionScopedInput> => {
|
||||
const authSessionInput = await getAuthSessionInput();
|
||||
return authSessionInput ? { ...input, ...authSessionInput } : input;
|
||||
};
|
||||
|
||||
const organization = async (organizationId: string): Promise<OrganizationHandle> =>
|
||||
client.organization.getOrCreate(organizationKey(organizationId), {
|
||||
createWithInput: organizationId,
|
||||
|
|
@ -471,7 +486,15 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
createWithInput: "app",
|
||||
}) as unknown as AppOrganizationHandle;
|
||||
|
||||
const task = async (organizationId: string, repoId: string, taskId: string): Promise<TaskHandle> => client.task.get(taskKey(organizationId, repoId, taskId));
|
||||
// getOrCreate is intentional here — this is the ONLY lazy creation point for
|
||||
// virtual tasks (PR-driven entries that exist in the org's local tables but
|
||||
// have no task actor yet). The task actor self-initializes from org data in
|
||||
// getCurrentRecord(). Backend code must NEVER use getOrCreateTask except in
|
||||
// createTaskMutation. See backend/CLAUDE.md "Lazy Task Actor Creation".
|
||||
const task = async (organizationId: string, repoId: string, taskId: string): Promise<TaskHandle> =>
|
||||
client.task.getOrCreate(taskKey(organizationId, repoId, taskId), {
|
||||
createWithInput: { organizationId, repoId, taskId },
|
||||
});
|
||||
|
||||
const sandboxByKey = async (organizationId: string, _providerId: SandboxProviderId, sandboxId: string): Promise<TaskSandboxHandle> => {
|
||||
return (client as any).taskSandbox.get(taskSandboxKey(organizationId, sandboxId));
|
||||
|
|
@ -493,17 +516,15 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
|
||||
for (const row of candidates) {
|
||||
try {
|
||||
const detail = await ws.getTask({ organizationId, taskId: row.taskId });
|
||||
const detail = await ws.getTask({ organizationId, repoId: row.repoId, taskId: row.taskId });
|
||||
if (detail.sandboxProviderId !== sandboxProviderId) {
|
||||
continue;
|
||||
}
|
||||
const sandbox = detail.sandboxes.find(
|
||||
const sandboxes = detail.sandboxes as Array<(typeof detail.sandboxes)[number] & { sandboxActorId?: string }>;
|
||||
const sandbox = sandboxes.find(
|
||||
(sb) =>
|
||||
sb.sandboxId === sandboxId &&
|
||||
sb.sandboxProviderId === sandboxProviderId &&
|
||||
typeof (sb as any).sandboxActorId === "string" &&
|
||||
(sb as any).sandboxActorId.length > 0,
|
||||
) as { sandboxActorId?: string } | undefined;
|
||||
sb.sandboxId === sandboxId && sb.sandboxProviderId === sandboxProviderId && typeof sb.sandboxActorId === "string" && sb.sandboxActorId.length > 0,
|
||||
);
|
||||
if (sandbox?.sandboxActorId) {
|
||||
return (client as any).taskSandbox.getForId(sandbox.sandboxActorId);
|
||||
}
|
||||
|
|
@ -563,67 +584,81 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
}
|
||||
};
|
||||
|
||||
const getWorkbenchCompat = async (organizationId: string): Promise<TaskWorkbenchSnapshot> => {
|
||||
const getTaskDetailWithAuth = async (organizationId: string, repoId: string, taskIdValue: string): Promise<WorkspaceTaskDetail> => {
|
||||
return (await task(organizationId, repoId, taskIdValue)).getTaskDetail(await getAuthSessionInput());
|
||||
};
|
||||
|
||||
const getSessionDetailWithAuth = async (organizationId: string, repoId: string, taskIdValue: string, sessionId: string): Promise<WorkspaceSessionDetail> => {
|
||||
return (await task(organizationId, repoId, taskIdValue)).getSessionDetail(await withAuthSessionInput({ sessionId }));
|
||||
};
|
||||
|
||||
const getWorkspaceCompat = async (organizationId: string): Promise<TaskWorkspaceSnapshot> => {
|
||||
const authSessionInput = await getAuthSessionInput();
|
||||
const summary = await (await organization(organizationId)).getOrganizationSummary({ organizationId });
|
||||
const tasks = (
|
||||
await Promise.all(
|
||||
summary.taskSummaries.map(async (taskSummary) => {
|
||||
let detail;
|
||||
try {
|
||||
detail = await (await task(organizationId, taskSummary.repoId, taskSummary.id)).getTaskDetail();
|
||||
} catch (error) {
|
||||
if (isActorNotFoundError(error)) {
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
const resolvedTasks = await Promise.all(
|
||||
summary.taskSummaries.map(async (taskSummary) => {
|
||||
let detail;
|
||||
try {
|
||||
const taskHandle = await task(organizationId, taskSummary.repoId, taskSummary.id);
|
||||
detail = await taskHandle.getTaskDetail(authSessionInput);
|
||||
} catch (error) {
|
||||
if (isActorNotFoundError(error)) {
|
||||
return null;
|
||||
}
|
||||
const sessionDetails = await Promise.all(
|
||||
detail.sessionsSummary.map(async (session) => {
|
||||
try {
|
||||
const full = await (await task(organizationId, detail.repoId, detail.id)).getSessionDetail({ sessionId: session.id });
|
||||
return [session.id, full] as const;
|
||||
} catch (error) {
|
||||
if (isActorNotFoundError(error)) {
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
throw error;
|
||||
}
|
||||
const sessionDetails = await Promise.all(
|
||||
detail.sessionsSummary.map(async (session) => {
|
||||
try {
|
||||
const full = await (await task(organizationId, detail.repoId, detail.id)).getSessionDetail({
|
||||
sessionId: session.id,
|
||||
...(authSessionInput ?? {}),
|
||||
});
|
||||
return [session.id, full] as const;
|
||||
} catch (error) {
|
||||
if (isActorNotFoundError(error)) {
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
);
|
||||
const sessionDetailsById = new Map(sessionDetails.filter((entry): entry is readonly [string, WorkbenchSessionDetail] => entry !== null));
|
||||
return {
|
||||
id: detail.id,
|
||||
repoId: detail.repoId,
|
||||
title: detail.title,
|
||||
status: detail.status,
|
||||
repoName: detail.repoName,
|
||||
updatedAtMs: detail.updatedAtMs,
|
||||
branch: detail.branch,
|
||||
pullRequest: detail.pullRequest,
|
||||
sessions: detail.sessionsSummary.map((session) => {
|
||||
const full = sessionDetailsById.get(session.id);
|
||||
return {
|
||||
id: session.id,
|
||||
sessionId: session.sessionId,
|
||||
sessionName: session.sessionName,
|
||||
agent: session.agent,
|
||||
model: session.model,
|
||||
status: session.status,
|
||||
thinkingSinceMs: session.thinkingSinceMs,
|
||||
unread: session.unread,
|
||||
created: session.created,
|
||||
draft: full?.draft ?? { text: "", attachments: [], updatedAtMs: null },
|
||||
transcript: full?.transcript ?? [],
|
||||
};
|
||||
}),
|
||||
fileChanges: detail.fileChanges,
|
||||
diffs: detail.diffs,
|
||||
fileTree: detail.fileTree,
|
||||
minutesUsed: detail.minutesUsed,
|
||||
};
|
||||
}),
|
||||
)
|
||||
).filter((task): task is TaskWorkbenchSnapshot["tasks"][number] => task !== null);
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
);
|
||||
const sessionDetailsById = new Map(sessionDetails.filter((entry): entry is readonly [string, WorkspaceSessionDetail] => entry !== null));
|
||||
return {
|
||||
id: detail.id,
|
||||
repoId: detail.repoId,
|
||||
title: detail.title,
|
||||
status: detail.status,
|
||||
repoName: detail.repoName,
|
||||
updatedAtMs: detail.updatedAtMs,
|
||||
branch: detail.branch,
|
||||
pullRequest: detail.pullRequest,
|
||||
activeSessionId: detail.activeSessionId ?? null,
|
||||
sessions: detail.sessionsSummary.map((session) => {
|
||||
const full = sessionDetailsById.get(session.id);
|
||||
return {
|
||||
id: session.id,
|
||||
sessionId: session.sessionId,
|
||||
sessionName: session.sessionName,
|
||||
agent: session.agent,
|
||||
model: session.model,
|
||||
status: session.status,
|
||||
thinkingSinceMs: session.thinkingSinceMs,
|
||||
unread: session.unread,
|
||||
created: session.created,
|
||||
draft: full?.draft ?? { text: "", attachments: [], updatedAtMs: null },
|
||||
transcript: full?.transcript ?? [],
|
||||
};
|
||||
}),
|
||||
fileChanges: detail.fileChanges,
|
||||
diffs: detail.diffs,
|
||||
fileTree: detail.fileTree,
|
||||
minutesUsed: detail.minutesUsed,
|
||||
activeSandboxId: detail.activeSandboxId ?? null,
|
||||
};
|
||||
}),
|
||||
);
|
||||
const tasks = resolvedTasks.filter((task): task is Exclude<(typeof resolvedTasks)[number], null> => task !== null);
|
||||
|
||||
const repositories = summary.repos
|
||||
.map((repo) => ({
|
||||
|
|
@ -642,14 +677,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 +693,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 +712,7 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
}
|
||||
|
||||
return () => {
|
||||
const current = workbenchSubscriptions.get(organizationId);
|
||||
const current = workspaceSubscriptions.get(organizationId);
|
||||
if (!current) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -686,7 +721,7 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
return;
|
||||
}
|
||||
|
||||
workbenchSubscriptions.delete(organizationId);
|
||||
workspaceSubscriptions.delete(organizationId);
|
||||
void current.disposeConnPromise?.then(async (disposeConn) => {
|
||||
await disposeConn?.();
|
||||
});
|
||||
|
|
@ -849,6 +884,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 +991,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 +1029,7 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
if (action === "sync") {
|
||||
await (await organization(organizationId)).syncTask({
|
||||
organizationId,
|
||||
repoId,
|
||||
taskId,
|
||||
reason: "cli.sync",
|
||||
});
|
||||
|
|
@ -991,6 +1038,7 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
if (action === "merge") {
|
||||
await (await organization(organizationId)).mergeTask({
|
||||
organizationId,
|
||||
repoId,
|
||||
taskId,
|
||||
reason: "cli.merge",
|
||||
});
|
||||
|
|
@ -999,6 +1047,7 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
if (action === "archive") {
|
||||
await (await organization(organizationId)).archiveTask({
|
||||
organizationId,
|
||||
repoId,
|
||||
taskId,
|
||||
reason: "cli.archive",
|
||||
});
|
||||
|
|
@ -1006,6 +1055,7 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
}
|
||||
await (await organization(organizationId)).killTask({
|
||||
organizationId,
|
||||
repoId,
|
||||
taskId,
|
||||
reason: "cli.kill",
|
||||
});
|
||||
|
|
@ -1156,96 +1206,92 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
return await withSandboxHandle(organizationId, sandboxProviderId, sandboxId, async (handle) => handle.sandboxAgentConnection());
|
||||
},
|
||||
|
||||
async getSandboxWorkspaceModelGroups(organizationId: string, sandboxProviderId: SandboxProviderId, sandboxId: string): Promise<WorkspaceModelGroup[]> {
|
||||
return await withSandboxHandle(organizationId, sandboxProviderId, sandboxId, async (handle) => handle.listWorkspaceModelGroups());
|
||||
},
|
||||
|
||||
async getOrganizationSummary(organizationId: string): Promise<OrganizationSummarySnapshot> {
|
||||
return (await organization(organizationId)).getOrganizationSummary({ organizationId });
|
||||
},
|
||||
|
||||
async getTaskDetail(organizationId: string, repoId: string, taskIdValue: string): Promise<WorkbenchTaskDetail> {
|
||||
return (await task(organizationId, repoId, taskIdValue)).getTaskDetail();
|
||||
async getTaskDetail(organizationId: string, repoId: string, taskIdValue: string): Promise<WorkspaceTaskDetail> {
|
||||
return await getTaskDetailWithAuth(organizationId, repoId, taskIdValue);
|
||||
},
|
||||
|
||||
async getSessionDetail(organizationId: string, repoId: string, taskIdValue: string, sessionId: string): Promise<WorkbenchSessionDetail> {
|
||||
return (await task(organizationId, repoId, taskIdValue)).getSessionDetail({ sessionId });
|
||||
async getSessionDetail(organizationId: string, repoId: string, taskIdValue: string, sessionId: string): Promise<WorkspaceSessionDetail> {
|
||||
return await getSessionDetailWithAuth(organizationId, repoId, taskIdValue, 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(await withAuthSessionInput(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(await withAuthSessionInput(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(await withAuthSessionInput(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(await withAuthSessionInput(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(await withAuthSessionInput(input));
|
||||
},
|
||||
|
||||
async renameWorkbenchSession(organizationId: string, input: TaskWorkbenchRenameSessionInput): Promise<void> {
|
||||
await (await organization(organizationId)).renameWorkbenchSession(input);
|
||||
async selectWorkspaceSession(organizationId: string, input: TaskWorkspaceSessionInput): Promise<void> {
|
||||
await (await organization(organizationId)).selectWorkspaceSession(await withAuthSessionInput(input));
|
||||
},
|
||||
|
||||
async setWorkbenchSessionUnread(organizationId: string, input: TaskWorkbenchSetSessionUnreadInput): Promise<void> {
|
||||
await (await organization(organizationId)).setWorkbenchSessionUnread(input);
|
||||
async setWorkspaceSessionUnread(organizationId: string, input: TaskWorkspaceSetSessionUnreadInput): Promise<void> {
|
||||
await (await organization(organizationId)).setWorkspaceSessionUnread(await withAuthSessionInput(input));
|
||||
},
|
||||
|
||||
async updateWorkbenchDraft(organizationId: string, input: TaskWorkbenchUpdateDraftInput): Promise<void> {
|
||||
await (await organization(organizationId)).updateWorkbenchDraft(input);
|
||||
async updateWorkspaceDraft(organizationId: string, input: TaskWorkspaceUpdateDraftInput): Promise<void> {
|
||||
await (await organization(organizationId)).updateWorkspaceDraft(await withAuthSessionInput(input));
|
||||
},
|
||||
|
||||
async changeWorkbenchModel(organizationId: string, input: TaskWorkbenchChangeModelInput): Promise<void> {
|
||||
await (await organization(organizationId)).changeWorkbenchModel(input);
|
||||
async changeWorkspaceModel(organizationId: string, input: TaskWorkspaceChangeModelInput): Promise<void> {
|
||||
await (await organization(organizationId)).changeWorkspaceModel(await withAuthSessionInput(input));
|
||||
},
|
||||
|
||||
async sendWorkbenchMessage(organizationId: string, input: TaskWorkbenchSendMessageInput): Promise<void> {
|
||||
await (await organization(organizationId)).sendWorkbenchMessage(input);
|
||||
async sendWorkspaceMessage(organizationId: string, input: TaskWorkspaceSendMessageInput): Promise<void> {
|
||||
await (await organization(organizationId)).sendWorkspaceMessage(await withAuthSessionInput(input));
|
||||
},
|
||||
|
||||
async stopWorkbenchSession(organizationId: string, input: TaskWorkbenchSessionInput): Promise<void> {
|
||||
await (await organization(organizationId)).stopWorkbenchSession(input);
|
||||
async stopWorkspaceSession(organizationId: string, input: TaskWorkspaceSessionInput): Promise<void> {
|
||||
await (await organization(organizationId)).stopWorkspaceSession(await withAuthSessionInput(input));
|
||||
},
|
||||
|
||||
async closeWorkbenchSession(organizationId: string, input: TaskWorkbenchSessionInput): Promise<void> {
|
||||
await (await organization(organizationId)).closeWorkbenchSession(input);
|
||||
async closeWorkspaceSession(organizationId: string, input: TaskWorkspaceSessionInput): Promise<void> {
|
||||
await (await organization(organizationId)).closeWorkspaceSession(await withAuthSessionInput(input));
|
||||
},
|
||||
|
||||
async publishWorkbenchPr(organizationId: string, input: TaskWorkbenchSelectInput): Promise<void> {
|
||||
await (await organization(organizationId)).publishWorkbenchPr(input);
|
||||
async publishWorkspacePr(organizationId: string, input: TaskWorkspaceSelectInput): Promise<void> {
|
||||
await (await organization(organizationId)).publishWorkspacePr(await withAuthSessionInput(input));
|
||||
},
|
||||
|
||||
async revertWorkbenchFile(organizationId: string, input: TaskWorkbenchDiffInput): Promise<void> {
|
||||
await (await organization(organizationId)).revertWorkbenchFile(input);
|
||||
async revertWorkspaceFile(organizationId: string, input: TaskWorkspaceDiffInput): Promise<void> {
|
||||
await (await organization(organizationId)).revertWorkspaceFile(await withAuthSessionInput(input));
|
||||
},
|
||||
|
||||
async reloadGithubOrganization(organizationId: string): Promise<void> {
|
||||
await (await organization(organizationId)).reloadGithubOrganization();
|
||||
async adminReloadGithubOrganization(organizationId: string): Promise<void> {
|
||||
await (await organization(organizationId)).adminReloadGithubOrganization();
|
||||
},
|
||||
|
||||
async reloadGithubPullRequests(organizationId: string): Promise<void> {
|
||||
await (await organization(organizationId)).reloadGithubPullRequests();
|
||||
},
|
||||
|
||||
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 adminReloadGithubRepository(organizationId: string, repoId: string): Promise<void> {
|
||||
await (await organization(organizationId)).adminReloadGithubRepository({ repoId });
|
||||
},
|
||||
|
||||
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";
|
||||
|
|
|
|||
|
|
@ -4,18 +4,14 @@ export function organizationKey(organizationId: string): ActorKey {
|
|||
return ["org", organizationId];
|
||||
}
|
||||
|
||||
export function repositoryKey(organizationId: string, repoId: string): ActorKey {
|
||||
return ["org", organizationId, "repository", repoId];
|
||||
}
|
||||
|
||||
export function taskKey(organizationId: string, repoId: string, taskId: string): ActorKey {
|
||||
return ["org", organizationId, "repository", repoId, "task", taskId];
|
||||
return ["org", organizationId, "task", repoId, taskId];
|
||||
}
|
||||
|
||||
export function taskSandboxKey(organizationId: string, sandboxId: string): ActorKey {
|
||||
return ["org", organizationId, "sandbox", sandboxId];
|
||||
}
|
||||
|
||||
export function historyKey(organizationId: string, repoId: string): ActorKey {
|
||||
return ["org", organizationId, "repository", repoId, "history"];
|
||||
export function auditLogKey(organizationId: string): ActorKey {
|
||||
return ["org", organizationId, "audit-log"];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
import type { WorkbenchModelId } from "@sandbox-agent/foundry-shared";
|
||||
import { DEFAULT_WORKSPACE_MODEL_GROUPS, DEFAULT_WORKSPACE_MODEL_ID, type WorkspaceModelId } from "@sandbox-agent/foundry-shared";
|
||||
|
||||
const claudeModels = DEFAULT_WORKSPACE_MODEL_GROUPS.find((group) => group.agentKind === "Claude")?.models ?? [];
|
||||
const CLAUDE_SECONDARY_MODEL_ID = claudeModels[1]?.id ?? claudeModels[0]?.id ?? DEFAULT_WORKSPACE_MODEL_ID;
|
||||
const CLAUDE_TERTIARY_MODEL_ID = claudeModels[2]?.id ?? CLAUDE_SECONDARY_MODEL_ID;
|
||||
import { injectMockLatency } from "./mock/latency.js";
|
||||
import rivetDevFixture from "../../../scripts/data/rivet-dev.json" with { type: "json" };
|
||||
|
||||
|
|
@ -16,6 +20,7 @@ export interface MockFoundryUser {
|
|||
githubLogin: string;
|
||||
roleLabel: string;
|
||||
eligibleOrganizationIds: string[];
|
||||
defaultModel: WorkspaceModelId;
|
||||
}
|
||||
|
||||
export interface MockFoundryOrganizationMember {
|
||||
|
|
@ -61,7 +66,6 @@ export interface MockFoundryOrganizationSettings {
|
|||
slug: string;
|
||||
primaryDomain: string;
|
||||
seatAccrualMode: "first_prompt";
|
||||
defaultModel: WorkbenchModelId;
|
||||
autoImportRepos: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -111,6 +115,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 +185,6 @@ function buildRivetOrganization(): MockFoundryOrganization {
|
|||
slug: "rivet",
|
||||
primaryDomain: "rivet.dev",
|
||||
seatAccrualMode: "first_prompt",
|
||||
defaultModel: "gpt-5.3-codex",
|
||||
autoImportRepos: true,
|
||||
},
|
||||
github: {
|
||||
|
|
@ -233,6 +237,7 @@ function buildDefaultSnapshot(): MockFoundryAppSnapshot {
|
|||
githubLogin: "nathan",
|
||||
roleLabel: "Founder",
|
||||
eligibleOrganizationIds: ["personal-nathan", "acme", "rivet"],
|
||||
defaultModel: DEFAULT_WORKSPACE_MODEL_ID,
|
||||
},
|
||||
{
|
||||
id: "user-maya",
|
||||
|
|
@ -241,6 +246,7 @@ function buildDefaultSnapshot(): MockFoundryAppSnapshot {
|
|||
githubLogin: "maya",
|
||||
roleLabel: "Staff Engineer",
|
||||
eligibleOrganizationIds: ["acme"],
|
||||
defaultModel: CLAUDE_SECONDARY_MODEL_ID,
|
||||
},
|
||||
{
|
||||
id: "user-jamie",
|
||||
|
|
@ -249,6 +255,7 @@ function buildDefaultSnapshot(): MockFoundryAppSnapshot {
|
|||
githubLogin: "jamie",
|
||||
roleLabel: "Platform Lead",
|
||||
eligibleOrganizationIds: ["personal-jamie", "rivet"],
|
||||
defaultModel: CLAUDE_TERTIARY_MODEL_ID,
|
||||
},
|
||||
],
|
||||
organizations: [
|
||||
|
|
@ -261,7 +268,6 @@ function buildDefaultSnapshot(): MockFoundryAppSnapshot {
|
|||
slug: "nathan",
|
||||
primaryDomain: "personal",
|
||||
seatAccrualMode: "first_prompt",
|
||||
defaultModel: "claude-sonnet-4",
|
||||
autoImportRepos: true,
|
||||
},
|
||||
github: {
|
||||
|
|
@ -297,7 +303,6 @@ function buildDefaultSnapshot(): MockFoundryAppSnapshot {
|
|||
slug: "acme",
|
||||
primaryDomain: "acme.dev",
|
||||
seatAccrualMode: "first_prompt",
|
||||
defaultModel: "claude-sonnet-4",
|
||||
autoImportRepos: true,
|
||||
},
|
||||
github: {
|
||||
|
|
@ -342,7 +347,6 @@ function buildDefaultSnapshot(): MockFoundryAppSnapshot {
|
|||
slug: "jamie",
|
||||
primaryDomain: "personal",
|
||||
seatAccrualMode: "first_prompt",
|
||||
defaultModel: "claude-opus-4",
|
||||
autoImportRepos: true,
|
||||
},
|
||||
github: {
|
||||
|
|
@ -538,6 +542,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,26 @@ 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,
|
||||
WorkspaceModelGroup,
|
||||
WorkspaceTaskDetail,
|
||||
WorkspaceTaskSummary,
|
||||
OrganizationEvent,
|
||||
OrganizationSummarySnapshot,
|
||||
HistoryEvent,
|
||||
AuditLogEvent as HistoryEvent,
|
||||
HistoryQueryInput,
|
||||
SandboxProviderId,
|
||||
RepoOverview,
|
||||
|
|
@ -32,9 +33,10 @@ import type {
|
|||
StarSandboxAgentRepoResult,
|
||||
SwitchResult,
|
||||
} from "@sandbox-agent/foundry-shared";
|
||||
import { DEFAULT_WORKSPACE_MODEL_GROUPS } 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 +91,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 +99,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 +166,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,
|
||||
|
|
@ -173,6 +175,7 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
|||
updatedAtMs: task.updatedAtMs,
|
||||
branch: task.branch,
|
||||
pullRequest: task.pullRequest,
|
||||
activeSessionId: task.activeSessionId ?? task.sessions[0]?.id ?? null,
|
||||
sessionsSummary: task.sessions.map((tab) => ({
|
||||
id: tab.id,
|
||||
sessionId: tab.sessionId,
|
||||
|
|
@ -187,16 +190,9 @@ 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",
|
||||
runtimeStatus: toTaskStatus(task.status === "archived" ? "archived" : "running", task.status === "archived"),
|
||||
statusMessage: task.status === "archived" ? "archived" : "mock sandbox ready",
|
||||
activeSessionId: task.sessions[0]?.sessionId ?? null,
|
||||
diffStat: task.fileChanges.length > 0 ? `+${task.fileChanges.length}/-${task.fileChanges.length}` : "+0/-0",
|
||||
prUrl: task.pullRequest ? `https://example.test/pr/${task.pullRequest.number}` : null,
|
||||
reviewStatus: null,
|
||||
fileChanges: task.fileChanges,
|
||||
diffs: task.diffs,
|
||||
fileTree: task.fileTree,
|
||||
|
|
@ -211,7 +207,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,10 +228,24 @@ 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,
|
||||
github: {
|
||||
connectedAccount: "mock",
|
||||
installationStatus: "connected",
|
||||
syncStatus: "synced",
|
||||
importedRepoCount: snapshot.repos.length,
|
||||
lastSyncLabel: "Synced just now",
|
||||
lastSyncAt: nowMs(),
|
||||
lastWebhookAt: null,
|
||||
lastWebhookEvent: "",
|
||||
syncGeneration: 1,
|
||||
syncPhase: null,
|
||||
processedRepositoryCount: snapshot.repos.length,
|
||||
totalRepositoryCount: snapshot.repos.length,
|
||||
},
|
||||
repos: snapshot.repos.map((repo) => {
|
||||
const repoTasks = taskSummaries.filter((task) => task.repoId === repo.id);
|
||||
return {
|
||||
|
|
@ -246,7 +256,6 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
|||
};
|
||||
}),
|
||||
taskSummaries,
|
||||
openPullRequests: [],
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -256,20 +265,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);
|
||||
};
|
||||
|
|
@ -303,9 +308,8 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
|||
task: task.title,
|
||||
sandboxProviderId: "local",
|
||||
status: toTaskStatus(archived ? "archived" : "running", archived),
|
||||
statusMessage: archived ? "archived" : "mock sandbox ready",
|
||||
pullRequest: null,
|
||||
activeSandboxId: task.id,
|
||||
activeSessionId: task.sessions[0]?.sessionId ?? null,
|
||||
sandboxes: [
|
||||
{
|
||||
sandboxId: task.id,
|
||||
|
|
@ -317,17 +321,6 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
|||
updatedAt: task.updatedAtMs,
|
||||
},
|
||||
],
|
||||
agentType: task.sessions[0]?.agent === "Codex" ? "codex" : "claude",
|
||||
prSubmitted: Boolean(task.pullRequest),
|
||||
diffStat: task.fileChanges.length > 0 ? `+${task.fileChanges.length}/-${task.fileChanges.length}` : "+0/-0",
|
||||
prUrl: task.pullRequest ? `https://example.test/pr/${task.pullRequest.number}` : null,
|
||||
prAuthor: task.pullRequest ? "mock" : null,
|
||||
ciStatus: null,
|
||||
reviewStatus: null,
|
||||
reviewer: null,
|
||||
conflictsWithMain: "0",
|
||||
hasUnpushed: task.fileChanges.length > 0 ? "1" : "0",
|
||||
parentBranch: null,
|
||||
createdAt: task.updatedAtMs,
|
||||
updatedAt: task.updatedAtMs,
|
||||
};
|
||||
|
|
@ -400,6 +393,10 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
|||
return unsupportedAppSnapshot();
|
||||
},
|
||||
|
||||
async setAppDefaultModel(): Promise<FoundryAppSnapshot> {
|
||||
return unsupportedAppSnapshot();
|
||||
},
|
||||
|
||||
async updateAppOrganizationProfile(): Promise<FoundryAppSnapshot> {
|
||||
return unsupportedAppSnapshot();
|
||||
},
|
||||
|
|
@ -433,7 +430,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 +444,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) => ({
|
||||
|
|
@ -457,6 +454,7 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
|||
branchName: task.branch,
|
||||
title: task.title,
|
||||
status: task.status === "archived" ? "archived" : "running",
|
||||
pullRequest: null,
|
||||
updatedAt: task.updatedAtMs,
|
||||
}));
|
||||
},
|
||||
|
|
@ -464,7 +462,7 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
|||
async getRepoOverview(_organizationId: string, _repoId: string): Promise<RepoOverview> {
|
||||
notSupported("getRepoOverview");
|
||||
},
|
||||
async getTask(_organizationId: string, taskId: string): Promise<TaskRecord> {
|
||||
async getTask(_organizationId: string, _repoId: string, taskId: string): Promise<TaskRecord> {
|
||||
return buildTaskRecord(taskId);
|
||||
},
|
||||
|
||||
|
|
@ -472,7 +470,7 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
|||
return [];
|
||||
},
|
||||
|
||||
async switchTask(_organizationId: string, taskId: string): Promise<SwitchResult> {
|
||||
async switchTask(_organizationId: string, _repoId: string, taskId: string): Promise<SwitchResult> {
|
||||
return {
|
||||
organizationId: defaultOrganizationId,
|
||||
taskId,
|
||||
|
|
@ -481,14 +479,14 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
|||
};
|
||||
},
|
||||
|
||||
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 {
|
||||
target: `mock://${taskId}`,
|
||||
sessionId: requireTask(taskId).sessions[0]?.sessionId ?? null,
|
||||
};
|
||||
},
|
||||
|
||||
async runAction(_organizationId: string, _taskId: string): Promise<void> {
|
||||
async runAction(_organizationId: string, _repoId: string, _taskId: string): Promise<void> {
|
||||
notSupported("runAction");
|
||||
},
|
||||
|
||||
|
|
@ -637,28 +635,32 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
|||
return { endpoint: "mock://terminal-unavailable" };
|
||||
},
|
||||
|
||||
async getSandboxWorkspaceModelGroups(_organizationId: string, _sandboxProviderId: SandboxProviderId, _sandboxId: string): Promise<WorkspaceModelGroup[]> {
|
||||
return DEFAULT_WORKSPACE_MODEL_GROUPS;
|
||||
},
|
||||
|
||||
async getOrganizationSummary(): Promise<OrganizationSummarySnapshot> {
|
||||
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 +669,95 @@ 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 selectWorkspaceSession(_organizationId: string, input: TaskWorkspaceSessionInput): Promise<void> {
|
||||
await workspace.selectSession(input);
|
||||
emitOrganizationSnapshot();
|
||||
emitTaskUpdate(input.taskId);
|
||||
emitSessionUpdate(input.taskId, input.sessionId);
|
||||
},
|
||||
|
||||
async updateWorkbenchDraft(_organizationId: string, input: TaskWorkbenchUpdateDraftInput): Promise<void> {
|
||||
await workbench.updateDraft(input);
|
||||
async setWorkspaceSessionUnread(_organizationId: string, input: TaskWorkspaceSetSessionUnreadInput): Promise<void> {
|
||||
await workspace.setSessionUnread(input);
|
||||
emitOrganizationSnapshot();
|
||||
emitTaskUpdate(input.taskId);
|
||||
emitSessionUpdate(input.taskId, input.sessionId);
|
||||
},
|
||||
|
||||
async changeWorkbenchModel(_organizationId: string, input: TaskWorkbenchChangeModelInput): Promise<void> {
|
||||
await workbench.changeModel(input);
|
||||
async updateWorkspaceDraft(_organizationId: string, input: TaskWorkspaceUpdateDraftInput): Promise<void> {
|
||||
await workspace.updateDraft(input);
|
||||
emitOrganizationSnapshot();
|
||||
emitTaskUpdate(input.taskId);
|
||||
emitSessionUpdate(input.taskId, input.sessionId);
|
||||
},
|
||||
|
||||
async sendWorkbenchMessage(_organizationId: string, input: TaskWorkbenchSendMessageInput): Promise<void> {
|
||||
await workbench.sendMessage(input);
|
||||
async changeWorkspaceModel(_organizationId: string, input: TaskWorkspaceChangeModelInput): Promise<void> {
|
||||
await workspace.changeModel(input);
|
||||
emitOrganizationSnapshot();
|
||||
emitTaskUpdate(input.taskId);
|
||||
emitSessionUpdate(input.taskId, input.sessionId);
|
||||
},
|
||||
|
||||
async stopWorkbenchSession(_organizationId: string, input: TaskWorkbenchSessionInput): Promise<void> {
|
||||
await workbench.stopAgent(input);
|
||||
async sendWorkspaceMessage(_organizationId: string, input: TaskWorkspaceSendMessageInput): Promise<void> {
|
||||
await workspace.sendMessage(input);
|
||||
emitOrganizationSnapshot();
|
||||
emitTaskUpdate(input.taskId);
|
||||
emitSessionUpdate(input.taskId, input.sessionId);
|
||||
},
|
||||
|
||||
async closeWorkbenchSession(_organizationId: string, input: TaskWorkbenchSessionInput): Promise<void> {
|
||||
await workbench.closeSession(input);
|
||||
async stopWorkspaceSession(_organizationId: string, input: TaskWorkspaceSessionInput): Promise<void> {
|
||||
await workspace.stopAgent(input);
|
||||
emitOrganizationSnapshot();
|
||||
emitTaskUpdate(input.taskId);
|
||||
emitSessionUpdate(input.taskId, input.sessionId);
|
||||
},
|
||||
|
||||
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 reloadGithubPullRequests(): Promise<void> {},
|
||||
|
||||
async reloadGithubRepository(): Promise<void> {},
|
||||
|
||||
async reloadGithubPullRequest(): Promise<void> {},
|
||||
async adminReloadGithubOrganization(): Promise<void> {},
|
||||
async adminReloadGithubRepository(): Promise<void> {},
|
||||
|
||||
async health(): Promise<{ ok: true }> {
|
||||
return { ok: true };
|
||||
|
|
|
|||
|
|
@ -1,33 +1,34 @@
|
|||
import {
|
||||
MODEL_GROUPS,
|
||||
buildInitialMockLayoutViewModel,
|
||||
groupWorkbenchRepositories,
|
||||
groupWorkspaceRepositories,
|
||||
nowMs,
|
||||
providerAgent,
|
||||
randomReply,
|
||||
removeFileTreePath,
|
||||
slugify,
|
||||
uid,
|
||||
} from "../workbench-model.js";
|
||||
} from "../workspace-model.js";
|
||||
import { DEFAULT_WORKSPACE_MODEL_ID, workspaceAgentForModel } from "@sandbox-agent/foundry-shared";
|
||||
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 +48,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 +64,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);
|
||||
|
|
@ -74,20 +75,19 @@ class MockWorkbenchStore implements TaskWorkbenchClient {
|
|||
id,
|
||||
repoId: repo.id,
|
||||
title: input.title?.trim() || "New Task",
|
||||
status: "new",
|
||||
status: "init_enqueue_provision",
|
||||
repoName: repo.label,
|
||||
updatedAtMs: nowMs(),
|
||||
branch: input.branch?.trim() || null,
|
||||
pullRequest: null,
|
||||
activeSessionId: sessionId,
|
||||
sessions: [
|
||||
{
|
||||
id: sessionId,
|
||||
sessionId: sessionId,
|
||||
sessionName: "Session 1",
|
||||
agent: providerAgent(
|
||||
MODEL_GROUPS.find((group) => group.models.some((model) => model.id === (input.model ?? "claude-sonnet-4")))?.provider ?? "Claude",
|
||||
),
|
||||
model: input.model ?? "claude-sonnet-4",
|
||||
agent: workspaceAgentForModel(input.model ?? DEFAULT_WORKSPACE_MODEL_ID, MODEL_GROUPS),
|
||||
model: input.model ?? DEFAULT_WORKSPACE_MODEL_ID,
|
||||
status: "idle",
|
||||
thinkingSinceMs: null,
|
||||
unread: false,
|
||||
|
|
@ -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,28 +131,32 @@ 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,
|
||||
updatedAtMs: nowMs(),
|
||||
pullRequest: { number: nextPrNumber, status: "ready" },
|
||||
pullRequest: {
|
||||
number: nextPrNumber,
|
||||
status: "ready",
|
||||
title: task.title,
|
||||
state: "open",
|
||||
url: `https://example.test/pr/${nextPrNumber}`,
|
||||
headRefName: task.branch ?? `task/${task.id}`,
|
||||
baseRefName: "main",
|
||||
repoFullName: task.repoName,
|
||||
authorLogin: "mock",
|
||||
isDraft: false,
|
||||
updatedAtMs: nowMs(),
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
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 +171,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 +191,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}`);
|
||||
|
|
@ -197,7 +201,7 @@ class MockWorkbenchStore implements TaskWorkbenchClient {
|
|||
const startedAtMs = nowMs();
|
||||
|
||||
this.updateTask(input.taskId, (currentTask) => {
|
||||
const isFirstOnTask = currentTask.status === "new";
|
||||
const isFirstOnTask = String(currentTask.status).startsWith("init_");
|
||||
const newTitle = isFirstOnTask ? (text.length > 50 ? `${text.slice(0, 47)}...` : text) : currentTask.title;
|
||||
const newBranch = isFirstOnTask ? `feat/${slugify(newTitle)}` : currentTask.branch;
|
||||
const userMessageLines = [text, ...input.attachments.map((attachment) => `@ ${attachment.filePath}:${attachment.lineNumber}`)];
|
||||
|
|
@ -288,7 +292,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 +315,22 @@ class MockWorkbenchStore implements TaskWorkbenchClient {
|
|||
});
|
||||
}
|
||||
|
||||
async setSessionUnread(input: TaskWorkbenchSetSessionUnreadInput): Promise<void> {
|
||||
async selectSession(input: TaskWorkspaceSessionInput): Promise<void> {
|
||||
this.assertSession(input.taskId, input.sessionId);
|
||||
this.updateTask(input.taskId, (currentTask) => ({
|
||||
...currentTask,
|
||||
activeSessionId: input.sessionId,
|
||||
}));
|
||||
}
|
||||
|
||||
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 +341,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;
|
||||
|
|
@ -337,12 +349,13 @@ class MockWorkbenchStore implements TaskWorkbenchClient {
|
|||
|
||||
return {
|
||||
...currentTask,
|
||||
activeSessionId: currentTask.activeSessionId === input.sessionId ? (currentTask.sessions.find((candidate) => candidate.id !== input.sessionId)?.id ?? null) : currentTask.activeSessionId,
|
||||
sessions: currentTask.sessions.filter((candidate) => candidate.id !== input.sessionId),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async addSession(input: TaskWorkbenchSelectInput): Promise<TaskWorkbenchAddSessionResponse> {
|
||||
async addSession(input: TaskWorkspaceSelectInput): Promise<TaskWorkspaceAddSessionResponse> {
|
||||
this.assertTask(input.taskId);
|
||||
const nextSessionId = uid();
|
||||
const nextSession: AgentSession = {
|
||||
|
|
@ -350,8 +363,8 @@ class MockWorkbenchStore implements TaskWorkbenchClient {
|
|||
sessionId: nextSessionId,
|
||||
sandboxSessionId: null,
|
||||
sessionName: `Session ${this.requireTask(input.taskId).sessions.length + 1}`,
|
||||
agent: "Claude",
|
||||
model: "claude-sonnet-4",
|
||||
agent: workspaceAgentForModel(DEFAULT_WORKSPACE_MODEL_ID, MODEL_GROUPS),
|
||||
model: DEFAULT_WORKSPACE_MODEL_ID,
|
||||
status: "idle",
|
||||
thinkingSinceMs: null,
|
||||
unread: false,
|
||||
|
|
@ -363,12 +376,13 @@ class MockWorkbenchStore implements TaskWorkbenchClient {
|
|||
this.updateTask(input.taskId, (currentTask) => ({
|
||||
...currentTask,
|
||||
updatedAtMs: nowMs(),
|
||||
activeSessionId: nextSession.id,
|
||||
sessions: [...currentTask.sessions, nextSession],
|
||||
}));
|
||||
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}`);
|
||||
|
|
@ -377,16 +391,16 @@ class MockWorkbenchStore implements TaskWorkbenchClient {
|
|||
this.updateTask(input.taskId, (currentTask) => ({
|
||||
...currentTask,
|
||||
sessions: currentTask.sessions.map((candidate) =>
|
||||
candidate.id === input.sessionId ? { ...candidate, model: input.model, agent: providerAgent(group.provider) } : candidate,
|
||||
candidate.id === input.sessionId ? { ...candidate, model: input.model, agent: workspaceAgentForModel(input.model, MODEL_GROUPS) } : candidate,
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
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 +450,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);
|
||||
}
|
||||
198
foundry/packages/client/src/remote/workspace-client.ts
Normal file
198
foundry/packages/client/src/remote/workspace-client.ts
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
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 selectSession(input: TaskWorkspaceSessionInput): Promise<void> {
|
||||
await this.backend.selectWorkspaceSession(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);
|
||||
}
|
||||
|
|
@ -81,6 +81,7 @@ class TopicEntry<TData, TParams, TEvent> {
|
|||
private unsubscribeError: (() => void) | null = null;
|
||||
private teardownTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private startPromise: Promise<void> | null = null;
|
||||
private eventPromise: Promise<void> = Promise.resolve();
|
||||
private started = false;
|
||||
|
||||
constructor(
|
||||
|
|
@ -157,12 +158,7 @@ class TopicEntry<TData, TParams, TEvent> {
|
|||
try {
|
||||
this.conn = await this.definition.connect(this.backend, this.params);
|
||||
this.unsubscribeEvent = this.conn.on(this.definition.event, (event: TEvent) => {
|
||||
if (this.data === undefined) {
|
||||
return;
|
||||
}
|
||||
this.data = this.definition.applyEvent(this.data, event);
|
||||
this.lastRefreshAt = Date.now();
|
||||
this.notify();
|
||||
void this.applyEvent(event);
|
||||
});
|
||||
this.unsubscribeError = this.conn.onError((error: unknown) => {
|
||||
this.status = "error";
|
||||
|
|
@ -182,6 +178,33 @@ class TopicEntry<TData, TParams, TEvent> {
|
|||
}
|
||||
}
|
||||
|
||||
private applyEvent(event: TEvent): Promise<void> {
|
||||
this.eventPromise = this.eventPromise
|
||||
.then(async () => {
|
||||
if (!this.started || this.data === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextData = await this.definition.applyEvent(this.backend, this.params, this.data, event);
|
||||
if (!this.started) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.data = nextData;
|
||||
this.status = "connected";
|
||||
this.error = null;
|
||||
this.lastRefreshAt = Date.now();
|
||||
this.notify();
|
||||
})
|
||||
.catch((error) => {
|
||||
this.status = "error";
|
||||
this.error = error instanceof Error ? error : new Error(String(error));
|
||||
this.notify();
|
||||
});
|
||||
|
||||
return this.eventPromise;
|
||||
}
|
||||
|
||||
private notify(): void {
|
||||
for (const listener of [...this.listeners]) {
|
||||
listener();
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import type {
|
|||
SandboxProcessesEvent,
|
||||
SessionEvent,
|
||||
TaskEvent,
|
||||
WorkbenchSessionDetail,
|
||||
WorkbenchTaskDetail,
|
||||
WorkspaceSessionDetail,
|
||||
WorkspaceTaskDetail,
|
||||
OrganizationEvent,
|
||||
OrganizationSummarySnapshot,
|
||||
} from "@sandbox-agent/foundry-shared";
|
||||
|
|
@ -16,15 +16,15 @@ import type { ActorConn, BackendClient, SandboxProcessRecord } from "../backend-
|
|||
* Topic definitions for the subscription manager.
|
||||
*
|
||||
* Each topic describes one actor connection plus one materialized read model.
|
||||
* Events always carry full replacement payloads for the changed entity so the
|
||||
* client can replace cached state directly instead of reconstructing patches.
|
||||
* Some topics can apply broadcast payloads directly, while others refetch
|
||||
* through BackendClient so auth-scoped state stays user-specific.
|
||||
*/
|
||||
export interface TopicDefinition<TData, TParams, TEvent> {
|
||||
key: (params: TParams) => string;
|
||||
event: string;
|
||||
connect: (backend: BackendClient, params: TParams) => Promise<ActorConn>;
|
||||
fetchInitial: (backend: BackendClient, params: TParams) => Promise<TData>;
|
||||
applyEvent: (current: TData, event: TEvent) => TData;
|
||||
applyEvent: (backend: BackendClient, params: TParams, current: TData, event: TEvent) => Promise<TData> | TData;
|
||||
}
|
||||
|
||||
export interface AppTopicParams {}
|
||||
|
|
@ -48,23 +48,13 @@ 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",
|
||||
event: "appUpdated",
|
||||
connect: (backend: BackendClient, _params: AppTopicParams) => backend.connectOrganization("app"),
|
||||
fetchInitial: (backend: BackendClient, _params: AppTopicParams) => backend.getAppSnapshot(),
|
||||
applyEvent: (_current: FoundryAppSnapshot, event: AppEvent) => event.snapshot,
|
||||
applyEvent: (_backend: BackendClient, _params: AppTopicParams, _current: FoundryAppSnapshot, event: AppEvent) => event.snapshot,
|
||||
} satisfies TopicDefinition<FoundryAppSnapshot, AppTopicParams, AppEvent>,
|
||||
|
||||
organization: {
|
||||
|
|
@ -72,41 +62,8 @@ 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: (_backend: BackendClient, _params: OrganizationTopicParams, _current: OrganizationSummarySnapshot, event: OrganizationEvent) =>
|
||||
event.snapshot,
|
||||
} satisfies TopicDefinition<OrganizationSummarySnapshot, OrganizationTopicParams, OrganizationEvent>,
|
||||
|
||||
task: {
|
||||
|
|
@ -114,8 +71,9 @@ 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: (backend: BackendClient, params: TaskTopicParams, _current: WorkspaceTaskDetail, _event: TaskEvent) =>
|
||||
backend.getTaskDetail(params.organizationId, params.repoId, params.taskId),
|
||||
} satisfies TopicDefinition<WorkspaceTaskDetail, TaskTopicParams, TaskEvent>,
|
||||
|
||||
session: {
|
||||
key: (params: SessionTopicParams) => `session:${params.organizationId}:${params.taskId}:${params.sessionId}`,
|
||||
|
|
@ -123,13 +81,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) => {
|
||||
if (event.session.sessionId !== current.sessionId) {
|
||||
applyEvent: async (backend: BackendClient, params: SessionTopicParams, current: WorkspaceSessionDetail, event: SessionEvent) => {
|
||||
if (event.session.sessionId !== params.sessionId) {
|
||||
return current;
|
||||
}
|
||||
return event.session;
|
||||
return await backend.getSessionDetail(params.organizationId, params.repoId, params.taskId, params.sessionId);
|
||||
},
|
||||
} satisfies TopicDefinition<WorkbenchSessionDetail, SessionTopicParams, SessionEvent>,
|
||||
} satisfies TopicDefinition<WorkspaceSessionDetail, SessionTopicParams, SessionEvent>,
|
||||
|
||||
sandboxProcesses: {
|
||||
key: (params: SandboxProcessesTopicParams) => `sandbox:${params.organizationId}:${params.sandboxProviderId}:${params.sandboxId}`,
|
||||
|
|
@ -138,7 +96,8 @@ export const topicDefinitions = {
|
|||
backend.connectSandbox(params.organizationId, params.sandboxProviderId, params.sandboxId),
|
||||
fetchInitial: async (backend: BackendClient, params: SandboxProcessesTopicParams) =>
|
||||
(await backend.listSandboxProcesses(params.organizationId, params.sandboxProviderId, params.sandboxId)).processes,
|
||||
applyEvent: (_current: SandboxProcessRecord[], event: SandboxProcessesEvent) => event.processes,
|
||||
applyEvent: (_backend: BackendClient, _params: SandboxProcessesTopicParams, _current: SandboxProcessRecord[], event: SandboxProcessesEvent) =>
|
||||
event.processes,
|
||||
} satisfies TopicDefinition<SandboxProcessRecord[], SandboxProcessesTopicParams, SandboxProcessesEvent>,
|
||||
} as const;
|
||||
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ export function filterTasks(rows: TaskRecord[], query: string): TaskRecord[] {
|
|||
}
|
||||
|
||||
return rows.filter((row) => {
|
||||
const fields = [row.branchName ?? "", row.title ?? "", row.taskId, row.task, row.prAuthor ?? "", row.reviewer ?? ""];
|
||||
const fields = [row.branchName ?? "", row.title ?? "", row.taskId, row.task];
|
||||
return fields.some((field) => fuzzyMatch(field, q));
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
64
foundry/packages/client/src/workspace-client.ts
Normal file
64
foundry/packages/client/src/workspace-client.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
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>;
|
||||
selectSession(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,40 +1,28 @@
|
|||
import {
|
||||
DEFAULT_WORKSPACE_MODEL_ID,
|
||||
DEFAULT_WORKSPACE_MODEL_GROUPS as SharedModelGroups,
|
||||
workspaceModelLabel as sharedWorkspaceModelLabel,
|
||||
workspaceProviderAgent as sharedWorkspaceProviderAgent,
|
||||
} from "@sandbox-agent/foundry-shared";
|
||||
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" };
|
||||
|
||||
export const MODEL_GROUPS: ModelGroup[] = [
|
||||
{
|
||||
provider: "Claude",
|
||||
models: [
|
||||
{ id: "claude-sonnet-4", label: "Sonnet 4" },
|
||||
{ id: "claude-opus-4", label: "Opus 4" },
|
||||
],
|
||||
},
|
||||
{
|
||||
provider: "OpenAI",
|
||||
models: [
|
||||
{ id: "gpt-5.3-codex", label: "GPT-5.3 Codex" },
|
||||
{ id: "gpt-5.4", label: "GPT-5.4" },
|
||||
{ id: "gpt-5.2-codex", label: "GPT-5.2 Codex" },
|
||||
{ id: "gpt-5.1-codex-max", label: "GPT-5.1 Codex Max" },
|
||||
{ id: "gpt-5.2", label: "GPT-5.2" },
|
||||
{ id: "gpt-5.1-codex-mini", label: "GPT-5.1 Codex Mini" },
|
||||
],
|
||||
},
|
||||
];
|
||||
export const MODEL_GROUPS: ModelGroup[] = SharedModelGroups;
|
||||
export const DEFAULT_MODEL_ID: ModelId = DEFAULT_WORKSPACE_MODEL_ID;
|
||||
|
||||
const MOCK_REPLIES = [
|
||||
"Got it. I'll work on that now. Let me start by examining the relevant files...",
|
||||
|
|
@ -73,15 +61,11 @@ export function formatMessageDuration(durationMs: number): string {
|
|||
}
|
||||
|
||||
export function modelLabel(id: ModelId): string {
|
||||
const group = MODEL_GROUPS.find((candidate) => candidate.models.some((model) => model.id === id));
|
||||
const model = group?.models.find((candidate) => candidate.id === id);
|
||||
return model && group ? `${group.provider} ${model.label}` : id;
|
||||
return sharedWorkspaceModelLabel(id, MODEL_GROUPS);
|
||||
}
|
||||
|
||||
export function providerAgent(provider: string): AgentKind {
|
||||
if (provider === "Claude") return "Claude";
|
||||
if (provider === "OpenAI") return "Codex";
|
||||
return "Cursor";
|
||||
return sharedWorkspaceProviderAgent(provider);
|
||||
}
|
||||
|
||||
export function slugify(text: string): string {
|
||||
|
|
@ -204,6 +188,29 @@ export function buildHistoryEvents(sessions: AgentSession[]): HistoryEvent[] {
|
|||
.sort((left, right) => messageOrder(left.messageId) - messageOrder(right.messageId));
|
||||
}
|
||||
|
||||
function buildPullRequestSummary(params: {
|
||||
number: number;
|
||||
title: string;
|
||||
branch: string;
|
||||
repoName: string;
|
||||
updatedAtMs: number;
|
||||
status: "ready" | "draft";
|
||||
}) {
|
||||
return {
|
||||
number: params.number,
|
||||
status: params.status,
|
||||
title: params.title,
|
||||
state: "open",
|
||||
url: `https://github.com/${params.repoName}/pull/${params.number}`,
|
||||
headRefName: params.branch,
|
||||
baseRefName: "main",
|
||||
repoFullName: params.repoName,
|
||||
authorLogin: "mock",
|
||||
isDraft: params.status === "draft",
|
||||
updatedAtMs: params.updatedAtMs,
|
||||
};
|
||||
}
|
||||
|
||||
function transcriptFromLegacyMessages(sessionId: string, messages: LegacyMessage[]): TranscriptEvent[] {
|
||||
return messages.map((message, index) => ({
|
||||
id: message.id,
|
||||
|
|
@ -315,14 +322,21 @@ export function buildInitialTasks(): Task[] {
|
|||
repoName: "rivet-dev/sandbox-agent",
|
||||
updatedAtMs: minutesAgo(8),
|
||||
branch: "NathanFlurry/pi-bootstrap-fix",
|
||||
pullRequest: { number: 227, status: "ready" },
|
||||
pullRequest: buildPullRequestSummary({
|
||||
number: 227,
|
||||
title: "Normalize Pi ACP bootstrap payloads",
|
||||
branch: "NathanFlurry/pi-bootstrap-fix",
|
||||
repoName: "rivet-dev/sandbox-agent",
|
||||
updatedAtMs: minutesAgo(8),
|
||||
status: "ready",
|
||||
}),
|
||||
sessions: [
|
||||
{
|
||||
id: "t1",
|
||||
sessionId: "t1",
|
||||
sessionName: "Pi payload fix",
|
||||
agent: "Claude",
|
||||
model: "claude-sonnet-4",
|
||||
model: "sonnet",
|
||||
status: "idle",
|
||||
thinkingSinceMs: null,
|
||||
unread: false,
|
||||
|
|
@ -484,14 +498,21 @@ export function buildInitialTasks(): Task[] {
|
|||
repoName: "rivet-dev/sandbox-agent",
|
||||
updatedAtMs: minutesAgo(3),
|
||||
branch: "feat/builtin-agent-skills",
|
||||
pullRequest: { number: 223, status: "draft" },
|
||||
pullRequest: buildPullRequestSummary({
|
||||
number: 223,
|
||||
title: "Auto-inject builtin agent skills at startup",
|
||||
branch: "feat/builtin-agent-skills",
|
||||
repoName: "rivet-dev/sandbox-agent",
|
||||
updatedAtMs: minutesAgo(3),
|
||||
status: "draft",
|
||||
}),
|
||||
sessions: [
|
||||
{
|
||||
id: "t3",
|
||||
sessionId: "t3",
|
||||
sessionName: "Skills injection",
|
||||
agent: "Claude",
|
||||
model: "claude-opus-4",
|
||||
model: "opus",
|
||||
status: "running",
|
||||
thinkingSinceMs: NOW_MS - 45_000,
|
||||
unread: false,
|
||||
|
|
@ -584,14 +605,21 @@ export function buildInitialTasks(): Task[] {
|
|||
repoName: "rivet-dev/sandbox-agent",
|
||||
updatedAtMs: minutesAgo(45),
|
||||
branch: "hooks-example",
|
||||
pullRequest: { number: 225, status: "ready" },
|
||||
pullRequest: buildPullRequestSummary({
|
||||
number: 225,
|
||||
title: "Add hooks example for Claude, Codex, and OpenCode",
|
||||
branch: "hooks-example",
|
||||
repoName: "rivet-dev/sandbox-agent",
|
||||
updatedAtMs: minutesAgo(45),
|
||||
status: "ready",
|
||||
}),
|
||||
sessions: [
|
||||
{
|
||||
id: "t4",
|
||||
sessionId: "t4",
|
||||
sessionName: "Example docs",
|
||||
agent: "Claude",
|
||||
model: "claude-sonnet-4",
|
||||
model: "sonnet",
|
||||
status: "idle",
|
||||
thinkingSinceMs: null,
|
||||
unread: false,
|
||||
|
|
@ -659,14 +687,21 @@ export function buildInitialTasks(): Task[] {
|
|||
repoName: "rivet-dev/rivet",
|
||||
updatedAtMs: minutesAgo(15),
|
||||
branch: "actor-reschedule-endpoint",
|
||||
pullRequest: { number: 4400, status: "ready" },
|
||||
pullRequest: buildPullRequestSummary({
|
||||
number: 4400,
|
||||
title: "Add actor reschedule endpoint",
|
||||
branch: "actor-reschedule-endpoint",
|
||||
repoName: "rivet-dev/rivet",
|
||||
updatedAtMs: minutesAgo(15),
|
||||
status: "ready",
|
||||
}),
|
||||
sessions: [
|
||||
{
|
||||
id: "t5",
|
||||
sessionId: "t5",
|
||||
sessionName: "Reschedule API",
|
||||
agent: "Claude",
|
||||
model: "claude-sonnet-4",
|
||||
model: "sonnet",
|
||||
status: "idle",
|
||||
thinkingSinceMs: null,
|
||||
unread: false,
|
||||
|
|
@ -793,14 +828,21 @@ export function buildInitialTasks(): Task[] {
|
|||
repoName: "rivet-dev/rivet",
|
||||
updatedAtMs: minutesAgo(35),
|
||||
branch: "feat/dynamic-actors",
|
||||
pullRequest: { number: 4395, status: "draft" },
|
||||
pullRequest: buildPullRequestSummary({
|
||||
number: 4395,
|
||||
title: "Dynamic actors",
|
||||
branch: "feat/dynamic-actors",
|
||||
repoName: "rivet-dev/rivet",
|
||||
updatedAtMs: minutesAgo(35),
|
||||
status: "draft",
|
||||
}),
|
||||
sessions: [
|
||||
{
|
||||
id: "t6",
|
||||
sessionId: "t6",
|
||||
sessionName: "Dynamic actors impl",
|
||||
agent: "Claude",
|
||||
model: "claude-opus-4",
|
||||
model: "opus",
|
||||
status: "idle",
|
||||
thinkingSinceMs: null,
|
||||
unread: true,
|
||||
|
|
@ -850,14 +892,21 @@ export function buildInitialTasks(): Task[] {
|
|||
repoName: "rivet-dev/vbare",
|
||||
updatedAtMs: minutesAgo(25),
|
||||
branch: "fix-use-full-cloud-run-pool-name",
|
||||
pullRequest: { number: 235, status: "ready" },
|
||||
pullRequest: buildPullRequestSummary({
|
||||
number: 235,
|
||||
title: "Use full cloud run pool name for routing",
|
||||
branch: "fix-use-full-cloud-run-pool-name",
|
||||
repoName: "rivet-dev/vbare",
|
||||
updatedAtMs: minutesAgo(25),
|
||||
status: "ready",
|
||||
}),
|
||||
sessions: [
|
||||
{
|
||||
id: "t7",
|
||||
sessionId: "t7",
|
||||
sessionName: "Pool routing fix",
|
||||
agent: "Claude",
|
||||
model: "claude-sonnet-4",
|
||||
model: "sonnet",
|
||||
status: "idle",
|
||||
thinkingSinceMs: null,
|
||||
unread: false,
|
||||
|
|
@ -959,14 +1008,21 @@ export function buildInitialTasks(): Task[] {
|
|||
repoName: "rivet-dev/skills",
|
||||
updatedAtMs: minutesAgo(50),
|
||||
branch: "fix-guard-support-https-targets",
|
||||
pullRequest: { number: 125, status: "ready" },
|
||||
pullRequest: buildPullRequestSummary({
|
||||
number: 125,
|
||||
title: "Route compute gateway path correctly",
|
||||
branch: "fix-guard-support-https-targets",
|
||||
repoName: "rivet-dev/skills",
|
||||
updatedAtMs: minutesAgo(50),
|
||||
status: "ready",
|
||||
}),
|
||||
sessions: [
|
||||
{
|
||||
id: "t8",
|
||||
sessionId: "t8",
|
||||
sessionName: "Guard routing",
|
||||
agent: "Claude",
|
||||
model: "claude-sonnet-4",
|
||||
model: "sonnet",
|
||||
status: "idle",
|
||||
thinkingSinceMs: null,
|
||||
unread: false,
|
||||
|
|
@ -1073,14 +1129,21 @@ export function buildInitialTasks(): Task[] {
|
|||
repoName: "rivet-dev/skills",
|
||||
updatedAtMs: minutesAgo(2 * 24 * 60),
|
||||
branch: "chore-move-compute-gateway-to",
|
||||
pullRequest: { number: 123, status: "ready" },
|
||||
pullRequest: buildPullRequestSummary({
|
||||
number: 123,
|
||||
title: "Move compute gateway to guard",
|
||||
branch: "chore-move-compute-gateway-to",
|
||||
repoName: "rivet-dev/skills",
|
||||
updatedAtMs: minutesAgo(2 * 24 * 60),
|
||||
status: "ready",
|
||||
}),
|
||||
sessions: [
|
||||
{
|
||||
id: "t9",
|
||||
sessionId: "t9",
|
||||
sessionName: "Gateway migration",
|
||||
agent: "Claude",
|
||||
model: "claude-sonnet-4",
|
||||
model: "sonnet",
|
||||
status: "idle",
|
||||
thinkingSinceMs: null,
|
||||
unread: false,
|
||||
|
|
@ -1166,8 +1229,6 @@ export function buildInitialTasks(): Task[] {
|
|||
repoId: "sandbox-agent",
|
||||
title: "Fix broken auth middleware (error demo)",
|
||||
status: "error",
|
||||
runtimeStatus: "error",
|
||||
statusMessage: "session:error",
|
||||
repoName: "rivet-dev/sandbox-agent",
|
||||
updatedAtMs: minutesAgo(2),
|
||||
branch: "fix/auth-middleware",
|
||||
|
|
@ -1178,7 +1239,7 @@ export function buildInitialTasks(): Task[] {
|
|||
sessionId: "status-error-session",
|
||||
sessionName: "Auth fix",
|
||||
agent: "Claude",
|
||||
model: "claude-sonnet-4",
|
||||
model: "sonnet",
|
||||
status: "error",
|
||||
thinkingSinceMs: null,
|
||||
unread: false,
|
||||
|
|
@ -1197,9 +1258,7 @@ export function buildInitialTasks(): Task[] {
|
|||
id: "status-provisioning",
|
||||
repoId: "sandbox-agent",
|
||||
title: "Add rate limiting to API gateway (provisioning demo)",
|
||||
status: "new",
|
||||
runtimeStatus: "init_enqueue_provision",
|
||||
statusMessage: "Queueing sandbox provisioning.",
|
||||
status: "init_enqueue_provision",
|
||||
repoName: "rivet-dev/sandbox-agent",
|
||||
updatedAtMs: minutesAgo(0),
|
||||
branch: null,
|
||||
|
|
@ -1211,7 +1270,7 @@ export function buildInitialTasks(): Task[] {
|
|||
sandboxSessionId: null,
|
||||
sessionName: "Session 1",
|
||||
agent: "Claude",
|
||||
model: "claude-sonnet-4",
|
||||
model: "sonnet",
|
||||
status: "pending_provision",
|
||||
thinkingSinceMs: null,
|
||||
unread: false,
|
||||
|
|
@ -1259,7 +1318,6 @@ export function buildInitialTasks(): Task[] {
|
|||
repoId: "sandbox-agent",
|
||||
title: "Refactor WebSocket handler (running demo)",
|
||||
status: "running",
|
||||
runtimeStatus: "running",
|
||||
repoName: "rivet-dev/sandbox-agent",
|
||||
updatedAtMs: minutesAgo(1),
|
||||
branch: "refactor/ws-handler",
|
||||
|
|
@ -1300,7 +1358,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,
|
||||
|
|
@ -1313,55 +1371,19 @@ function repoIdFromFullName(fullName: string): string {
|
|||
return parts[parts.length - 1] ?? fullName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build task entries from open PR fixture data.
|
||||
* Maps to the backend's PR sync behavior (RepositoryPrSyncActor) where PRs
|
||||
* appear as first-class sidebar items even without an associated task.
|
||||
* Each open PR gets a lightweight task entry so it shows in the sidebar.
|
||||
*/
|
||||
function buildPrTasks(): Task[] {
|
||||
// Collect branch names already claimed by hand-written tasks so we don't duplicate
|
||||
const existingBranches = new Set(
|
||||
buildInitialTasks()
|
||||
.map((t) => t.branch)
|
||||
.filter(Boolean),
|
||||
);
|
||||
|
||||
return rivetDevFixture.openPullRequests
|
||||
.filter((pr) => !existingBranches.has(pr.headRefName))
|
||||
.map((pr) => {
|
||||
const repoId = repoIdFromFullName(pr.repoFullName);
|
||||
return {
|
||||
id: `pr-${repoId}-${pr.number}`,
|
||||
repoId,
|
||||
title: pr.title,
|
||||
status: "idle" as const,
|
||||
repoName: pr.repoFullName,
|
||||
updatedAtMs: new Date(pr.updatedAt).getTime(),
|
||||
branch: pr.headRefName,
|
||||
pullRequest: { number: pr.number, status: pr.draft ? ("draft" as const) : ("ready" as const) },
|
||||
sessions: [],
|
||||
fileChanges: [],
|
||||
diffs: {},
|
||||
fileTree: [],
|
||||
minutesUsed: 0,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function buildInitialMockLayoutViewModel(): TaskWorkbenchSnapshot {
|
||||
export function buildInitialMockLayoutViewModel(): TaskWorkspaceSnapshot {
|
||||
const repos = buildMockRepos();
|
||||
const tasks = [...buildInitialTasks(), ...buildPrTasks()];
|
||||
const tasks = buildInitialTasks();
|
||||
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, {
|
||||
Loading…
Add table
Add a link
Reference in a new issue