Rename Foundry handoffs to tasks (#239)

* Restore foundry onboarding stack

* Consolidate foundry rename

* Create foundry tasks without prompts

* Rename Foundry handoffs to tasks
This commit is contained in:
Nathan Flurry 2026-03-11 13:23:54 -07:00 committed by GitHub
parent d30cc0bcc8
commit d75e8c31d1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
281 changed files with 9242 additions and 4356 deletions

View file

@ -0,0 +1,152 @@
import type { FoundryAppSnapshot, FoundryBillingPlanId, UpdateFoundryOrganizationProfileInput } from "@sandbox-agent/foundry-shared";
import type { BackendClient } from "../backend-client.js";
import type { FoundryAppClient } from "../app-client.js";
export interface RemoteFoundryAppClientOptions {
backend: BackendClient;
}
class RemoteFoundryAppStore implements FoundryAppClient {
private readonly backend: BackendClient;
private snapshot: FoundryAppSnapshot = {
auth: { status: "signed_out", currentUserId: null },
activeOrganizationId: null,
onboarding: {
starterRepo: {
repoFullName: "rivet-dev/sandbox-agent",
repoUrl: "https://github.com/rivet-dev/sandbox-agent",
status: "pending",
starredAt: null,
skippedAt: null,
},
},
users: [],
organizations: [],
};
private readonly listeners = new Set<() => void>();
private refreshPromise: Promise<void> | null = null;
private syncPollTimeout: ReturnType<typeof setTimeout> | null = null;
constructor(options: RemoteFoundryAppClientOptions) {
this.backend = options.backend;
}
getSnapshot(): FoundryAppSnapshot {
return this.snapshot;
}
subscribe(listener: () => void): () => void {
this.listeners.add(listener);
void this.refresh();
return () => {
this.listeners.delete(listener);
};
}
async signInWithGithub(userId?: string): Promise<void> {
void userId;
await this.backend.signInWithGithub();
}
async signOut(): Promise<void> {
this.snapshot = await this.backend.signOutApp();
this.notify();
}
async skipStarterRepo(): Promise<void> {
this.snapshot = await this.backend.skipAppStarterRepo();
this.notify();
}
async starStarterRepo(organizationId: string): Promise<void> {
this.snapshot = await this.backend.starAppStarterRepo(organizationId);
this.notify();
}
async selectOrganization(organizationId: string): Promise<void> {
this.snapshot = await this.backend.selectAppOrganization(organizationId);
this.notify();
this.scheduleSyncPollingIfNeeded();
}
async updateOrganizationProfile(input: UpdateFoundryOrganizationProfileInput): Promise<void> {
this.snapshot = await this.backend.updateAppOrganizationProfile(input);
this.notify();
}
async triggerGithubSync(organizationId: string): Promise<void> {
this.snapshot = await this.backend.triggerAppRepoImport(organizationId);
this.notify();
this.scheduleSyncPollingIfNeeded();
}
async completeHostedCheckout(organizationId: string, planId: FoundryBillingPlanId): Promise<void> {
await this.backend.completeAppHostedCheckout(organizationId, planId);
}
async openBillingPortal(organizationId: string): Promise<void> {
await this.backend.openAppBillingPortal(organizationId);
}
async cancelScheduledRenewal(organizationId: string): Promise<void> {
this.snapshot = await this.backend.cancelAppScheduledRenewal(organizationId);
this.notify();
}
async resumeSubscription(organizationId: string): Promise<void> {
this.snapshot = await this.backend.resumeAppSubscription(organizationId);
this.notify();
}
async reconnectGithub(organizationId: string): Promise<void> {
await this.backend.reconnectAppGithub(organizationId);
}
async recordSeatUsage(workspaceId: string): Promise<void> {
this.snapshot = await this.backend.recordAppSeatUsage(workspaceId);
this.notify();
}
private scheduleSyncPollingIfNeeded(): void {
if (this.syncPollTimeout) {
clearTimeout(this.syncPollTimeout);
this.syncPollTimeout = null;
}
if (!this.snapshot.organizations.some((organization) => organization.github.syncStatus === "syncing")) {
return;
}
this.syncPollTimeout = setTimeout(() => {
this.syncPollTimeout = null;
void this.refresh();
}, 500);
}
private async refresh(): Promise<void> {
if (this.refreshPromise) {
await this.refreshPromise;
return;
}
this.refreshPromise = (async () => {
this.snapshot = await this.backend.getAppSnapshot();
this.notify();
this.scheduleSyncPollingIfNeeded();
})().finally(() => {
this.refreshPromise = null;
});
await this.refreshPromise;
}
private notify(): void {
for (const listener of [...this.listeners]) {
listener();
}
}
}
export function createRemoteFoundryAppClient(options: RemoteFoundryAppClientOptions): FoundryAppClient {
return new RemoteFoundryAppStore(options);
}

View file

@ -0,0 +1,197 @@
import type {
TaskWorkbenchAddTabResponse,
TaskWorkbenchChangeModelInput,
TaskWorkbenchCreateTaskInput,
TaskWorkbenchCreateTaskResponse,
TaskWorkbenchDiffInput,
TaskWorkbenchRenameInput,
TaskWorkbenchRenameSessionInput,
TaskWorkbenchSelectInput,
TaskWorkbenchSetSessionUnreadInput,
TaskWorkbenchSendMessageInput,
TaskWorkbenchSnapshot,
TaskWorkbenchTabInput,
TaskWorkbenchUpdateDraftInput,
} from "@sandbox-agent/foundry-shared";
import type { BackendClient } from "../backend-client.js";
import { groupWorkbenchProjects } from "../workbench-model.js";
import type { TaskWorkbenchClient } from "../workbench-client.js";
export interface RemoteWorkbenchClientOptions {
backend: BackendClient;
workspaceId: string;
}
class RemoteWorkbenchStore implements TaskWorkbenchClient {
private readonly backend: BackendClient;
private readonly workspaceId: 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.workspaceId = options.workspaceId;
this.snapshot = {
workspaceId: options.workspaceId,
repos: [],
projects: [],
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.workspaceId, input);
await this.refresh();
return created;
}
async markTaskUnread(input: TaskWorkbenchSelectInput): Promise<void> {
await this.backend.markWorkbenchUnread(this.workspaceId, input);
await this.refresh();
}
async renameTask(input: TaskWorkbenchRenameInput): Promise<void> {
await this.backend.renameWorkbenchTask(this.workspaceId, input);
await this.refresh();
}
async renameBranch(input: TaskWorkbenchRenameInput): Promise<void> {
await this.backend.renameWorkbenchBranch(this.workspaceId, input);
await this.refresh();
}
async archiveTask(input: TaskWorkbenchSelectInput): Promise<void> {
await this.backend.runAction(this.workspaceId, input.taskId, "archive");
await this.refresh();
}
async publishPr(input: TaskWorkbenchSelectInput): Promise<void> {
await this.backend.publishWorkbenchPr(this.workspaceId, input);
await this.refresh();
}
async revertFile(input: TaskWorkbenchDiffInput): Promise<void> {
await this.backend.revertWorkbenchFile(this.workspaceId, input);
await this.refresh();
}
async updateDraft(input: TaskWorkbenchUpdateDraftInput): Promise<void> {
await this.backend.updateWorkbenchDraft(this.workspaceId, input);
await this.refresh();
}
async sendMessage(input: TaskWorkbenchSendMessageInput): Promise<void> {
await this.backend.sendWorkbenchMessage(this.workspaceId, input);
await this.refresh();
}
async stopAgent(input: TaskWorkbenchTabInput): Promise<void> {
await this.backend.stopWorkbenchSession(this.workspaceId, input);
await this.refresh();
}
async setSessionUnread(input: TaskWorkbenchSetSessionUnreadInput): Promise<void> {
await this.backend.setWorkbenchSessionUnread(this.workspaceId, input);
await this.refresh();
}
async renameSession(input: TaskWorkbenchRenameSessionInput): Promise<void> {
await this.backend.renameWorkbenchSession(this.workspaceId, input);
await this.refresh();
}
async closeTab(input: TaskWorkbenchTabInput): Promise<void> {
await this.backend.closeWorkbenchSession(this.workspaceId, input);
await this.refresh();
}
async addTab(input: TaskWorkbenchSelectInput): Promise<TaskWorkbenchAddTabResponse> {
const created = await this.backend.createWorkbenchSession(this.workspaceId, input);
await this.refresh();
return created;
}
async changeModel(input: TaskWorkbenchChangeModelInput): Promise<void> {
await this.backend.changeWorkbenchModel(this.workspaceId, input);
await this.refresh();
}
private ensureStarted(): void {
if (!this.unsubscribeWorkbench) {
this.unsubscribeWorkbench = this.backend.subscribeWorkbench(this.workspaceId, () => {
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.workspaceId);
if (this.refreshRetryTimeout) {
clearTimeout(this.refreshRetryTimeout);
this.refreshRetryTimeout = null;
}
this.snapshot = {
...nextSnapshot,
projects: nextSnapshot.projects ?? groupWorkbenchProjects(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);
}