mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 12:03:53 +00:00
feat(foundry): checkpoint actor and workspace refactor
This commit is contained in:
parent
32f3c6c3bc
commit
dbe57d45b9
81 changed files with 3441 additions and 2332 deletions
|
|
@ -73,7 +73,7 @@ Use `pnpm` workspaces and Turborepo.
|
|||
- All backend interaction (actor calls, metadata/health checks, backend HTTP endpoint access) must go through the dedicated client library in `packages/client`.
|
||||
- Outside `packages/client`, do not call backend endpoints directly (for example `fetch(.../v1/rivet...)`), except in black-box E2E tests that intentionally exercise raw transport behavior.
|
||||
- GUI state should update in realtime (no manual refresh buttons). Prefer RivetKit push reactivity and actor-driven events; do not add polling/refetch for normal product flows.
|
||||
- Keep the mock workbench types and mock client in `packages/shared` + `packages/client` up to date with the frontend contract. The mock is the UI testing reference implementation while backend functionality catches up.
|
||||
- Keep the mock workspace types and mock client in `packages/shared` + `packages/client` up to date with the frontend contract. The mock is the UI testing reference implementation while backend functionality catches up.
|
||||
- Keep frontend route/state coverage current in code and tests; there is no separate page-inventory doc to maintain.
|
||||
- If Foundry uses a shared component from `@sandbox-agent/react`, make changes in `sdks/react` instead of copying or forking that component into Foundry.
|
||||
- When changing shared React components in `sdks/react` for Foundry, verify they still work in the Sandbox Agent Inspector before finishing.
|
||||
|
|
@ -227,8 +227,8 @@ Action handlers must return fast. The pattern:
|
|||
|
||||
Examples:
|
||||
- `createTask` → `wait: true` (returns `{ taskId }`), then enqueue provisioning with `wait: false`. Client sees task appear immediately with pending status, observes `ready` via organization events.
|
||||
- `sendWorkbenchMessage` → validate session is `ready` (throw if not), enqueue with `wait: false`. Client observes session transition to `running` → `idle` via session events.
|
||||
- `createWorkbenchSession` → `wait: true` (returns `{ tabId }`), enqueue sandbox provisioning with `wait: false`. Client observes `pending_provision` → `ready` via task events.
|
||||
- `sendWorkspaceMessage` → validate session is `ready` (throw if not), enqueue with `wait: false`. Client observes session transition to `running` → `idle` via session events.
|
||||
- `createWorkspaceSession` → `wait: true` (returns `{ sessionId }`), enqueue sandbox provisioning with `wait: false`. Client observes `pending_provision` → `ready` via task events.
|
||||
|
||||
Never use `wait: true` for operations that depend on external readiness, sandbox I/O, agent responses, git network operations, polling loops, or long-running queue drains. Never hold an action open while waiting for an external system to become ready — that is a polling/retry loop in disguise.
|
||||
|
||||
|
|
@ -320,9 +320,9 @@ Each entry must include:
|
|||
- Friction/issue
|
||||
- Attempted fix/workaround and outcome
|
||||
|
||||
## History Events
|
||||
## Audit Log Events
|
||||
|
||||
Log notable workflow changes to `events` so `hf history` remains complete:
|
||||
Log notable workflow changes to `events` so the audit log remains complete:
|
||||
|
||||
- create
|
||||
- attach
|
||||
|
|
@ -331,6 +331,8 @@ Log notable workflow changes to `events` so `hf history` remains complete:
|
|||
- status transitions
|
||||
- PR state transitions
|
||||
|
||||
When adding new task/workspace commands, always add a corresponding audit log event.
|
||||
|
||||
## Validation After Changes
|
||||
|
||||
Always run and fix failures:
|
||||
|
|
|
|||
1343
foundry/FOUNDRY-CHANGES.md
Normal file
1343
foundry/FOUNDRY-CHANGES.md
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -6,13 +6,13 @@ Keep the backend actor tree aligned with this shape unless we explicitly decide
|
|||
|
||||
```text
|
||||
OrganizationActor
|
||||
├─ HistoryActor(organization-scoped global feed)
|
||||
├─ AuditLogActor(organization-scoped global feed)
|
||||
├─ GithubDataActor
|
||||
├─ RepositoryActor(repo)
|
||||
│ └─ TaskActor(task)
|
||||
│ ├─ TaskSessionActor(session) × N
|
||||
│ │ └─ SessionStatusSyncActor(session) × 0..1
|
||||
│ └─ Task-local workbench state
|
||||
│ └─ Task-local workspace state
|
||||
└─ SandboxInstanceActor(sandboxProviderId, sandboxId) × N
|
||||
```
|
||||
|
||||
|
|
@ -46,12 +46,12 @@ OrganizationActor (coordinator for repos + auth users)
|
|||
│ └─ TaskActor (coordinator for sessions + sandboxes)
|
||||
│ │
|
||||
│ │ Index tables:
|
||||
│ │ ├─ taskWorkbenchSessions → Session index (session metadata, transcript, draft)
|
||||
│ │ ├─ taskWorkspaceSessions → Session index (session metadata, transcript, draft)
|
||||
│ │ └─ taskSandboxes → SandboxInstanceActor index (sandbox history)
|
||||
│ │
|
||||
│ └─ SandboxInstanceActor (leaf)
|
||||
│
|
||||
├─ HistoryActor (organization-scoped audit log, not a coordinator)
|
||||
├─ AuditLogActor (organization-scoped audit log, not a coordinator)
|
||||
└─ GithubDataActor (GitHub API cache, not a coordinator)
|
||||
```
|
||||
|
||||
|
|
@ -60,13 +60,13 @@ When adding a new index table, annotate it in the schema file with a doc comment
|
|||
## Ownership Rules
|
||||
|
||||
- `OrganizationActor` is the organization coordinator and lookup/index owner.
|
||||
- `HistoryActor` is organization-scoped. There is one organization-level history feed.
|
||||
- `AuditLogActor` is organization-scoped. There is one organization-level audit log feed.
|
||||
- `RepositoryActor` is the repo coordinator and owns repo-local caches/indexes.
|
||||
- `TaskActor` is one branch. Treat `1 task = 1 branch` once branch assignment is finalized.
|
||||
- `TaskActor` can have many sessions.
|
||||
- `TaskActor` can reference many sandbox instances historically, but should have only one active sandbox/session at a time.
|
||||
- Session unread state and draft prompts are backend-owned workbench state, not frontend-local state.
|
||||
- Branch rename is a real git operation, not just metadata.
|
||||
- Session unread state and draft prompts are backend-owned workspace state, not frontend-local state.
|
||||
- Branch names are immutable after task creation. Do not implement branch-rename flows.
|
||||
- `SandboxInstanceActor` stays separate from `TaskActor`; tasks/sessions reference it by identity.
|
||||
- The backend stores no local git state. No clones, no refs, no working trees, and no git-spice. Repository metadata comes from GitHub API data and webhook events. Any working-tree git operation runs inside a sandbox via `executeInSandbox()`.
|
||||
- When a backend request path must aggregate multiple independent actor calls or reads, prefer bounded parallelism over sequential fan-out when correctness permits. Do not serialize independent work by default.
|
||||
|
|
@ -75,6 +75,11 @@ When adding a new index table, annotate it in the schema file with a doc comment
|
|||
- Read paths must use the coordinator's local index tables. Do not fan out to child actors on the hot read path.
|
||||
- Never build "enriched" read actions that chain through multiple actors (e.g., coordinator → child actor → sibling actor). If data from multiple actors is needed for a read, it should already be materialized in the coordinator's index tables via push updates. If it's not there, fix the write path to push it — do not add a fan-out read path.
|
||||
|
||||
## SQLite Constraints
|
||||
|
||||
- Single-row tables must use an integer primary key with `CHECK (id = 1)` to enforce the singleton invariant at the database level.
|
||||
- Follow the task actor pattern for metadata/profile rows and keep the fixed row id in code as `1`, not a string sentinel.
|
||||
|
||||
## Multiplayer Correctness
|
||||
|
||||
Per-user UI state must live on the user actor, not on shared task/session actors. This is critical for multiplayer — multiple users may view the same task simultaneously with different active sessions, unread states, and in-progress drafts.
|
||||
|
|
@ -85,6 +90,10 @@ Per-user UI state must live on the user actor, not on shared task/session actors
|
|||
|
||||
Do not store per-user preferences, selections, or ephemeral UI state on shared actors. If a field's value should differ between two users looking at the same task, it belongs on the user actor.
|
||||
|
||||
## Audit Log Maintenance
|
||||
|
||||
Every new action or command handler that represents a user-visible or workflow-significant event must append to the audit log actor. The audit log must remain a comprehensive record of significant operations.
|
||||
|
||||
## Maintenance
|
||||
|
||||
- Keep this file up to date whenever actor ownership, hierarchy, or lifecycle responsibilities change.
|
||||
|
|
|
|||
|
|
@ -2,4 +2,4 @@ import { db } from "rivetkit/db/drizzle";
|
|||
import * as schema from "./schema.js";
|
||||
import migrations from "./migrations.js";
|
||||
|
||||
export const authUserDb = db({ schema, migrations });
|
||||
export const auditLogDb = db({ schema, migrations });
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import { defineConfig } from "rivetkit/db/drizzle";
|
||||
|
||||
export default defineConfig({
|
||||
out: "./src/actors/audit-log/db/drizzle",
|
||||
schema: "./src/actors/audit-log/db/schema.ts",
|
||||
});
|
||||
|
|
@ -5,7 +5,7 @@ export const events = sqliteTable("events", {
|
|||
taskId: text("task_id"),
|
||||
branchName: text("branch_name"),
|
||||
kind: text("kind").notNull(),
|
||||
// Structured by the history event kind definitions in application code.
|
||||
// Structured by the audit-log event kind definitions in application code.
|
||||
payloadJson: text("payload_json").notNull(),
|
||||
createdAt: integer("created_at").notNull(),
|
||||
});
|
||||
|
|
@ -2,32 +2,31 @@
|
|||
import { and, desc, eq } from "drizzle-orm";
|
||||
import { actor, queue } from "rivetkit";
|
||||
import { Loop, workflow } from "rivetkit/workflow";
|
||||
import type { HistoryEvent } from "@sandbox-agent/foundry-shared";
|
||||
import { selfHistory } from "../handles.js";
|
||||
import { historyDb } from "./db/db.js";
|
||||
import type { AuditLogEvent } from "@sandbox-agent/foundry-shared";
|
||||
import { auditLogDb } from "./db/db.js";
|
||||
import { events } from "./db/schema.js";
|
||||
|
||||
export interface HistoryInput {
|
||||
export interface AuditLogInput {
|
||||
organizationId: string;
|
||||
repoId: string;
|
||||
}
|
||||
|
||||
export interface AppendHistoryCommand {
|
||||
export interface AppendAuditLogCommand {
|
||||
kind: string;
|
||||
taskId?: string;
|
||||
branchName?: string;
|
||||
payload: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface ListHistoryParams {
|
||||
export interface ListAuditLogParams {
|
||||
branch?: string;
|
||||
taskId?: string;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
const HISTORY_QUEUE_NAMES = ["history.command.append"] as const;
|
||||
export const AUDIT_LOG_QUEUE_NAMES = ["auditLog.command.append"] as const;
|
||||
|
||||
async function appendHistoryRow(loopCtx: any, body: AppendHistoryCommand): Promise<void> {
|
||||
async function appendAuditLogRow(loopCtx: any, body: AppendAuditLogCommand): Promise<void> {
|
||||
const now = Date.now();
|
||||
await loopCtx.db
|
||||
.insert(events)
|
||||
|
|
@ -41,18 +40,18 @@ async function appendHistoryRow(loopCtx: any, body: AppendHistoryCommand): Promi
|
|||
.run();
|
||||
}
|
||||
|
||||
async function runHistoryWorkflow(ctx: any): Promise<void> {
|
||||
await ctx.loop("history-command-loop", async (loopCtx: any) => {
|
||||
const msg = await loopCtx.queue.next("next-history-command", {
|
||||
names: [...HISTORY_QUEUE_NAMES],
|
||||
async function runAuditLogWorkflow(ctx: any): Promise<void> {
|
||||
await ctx.loop("audit-log-command-loop", async (loopCtx: any) => {
|
||||
const msg = await loopCtx.queue.next("next-audit-log-command", {
|
||||
names: [...AUDIT_LOG_QUEUE_NAMES],
|
||||
completable: true,
|
||||
});
|
||||
if (!msg) {
|
||||
return Loop.continue(undefined);
|
||||
}
|
||||
|
||||
if (msg.name === "history.command.append") {
|
||||
await loopCtx.step("append-history-row", async () => appendHistoryRow(loopCtx, msg.body as AppendHistoryCommand));
|
||||
if (msg.name === "auditLog.command.append") {
|
||||
await loopCtx.step("append-audit-log-row", async () => appendAuditLogRow(loopCtx, msg.body as AppendAuditLogCommand));
|
||||
await msg.complete({ ok: true });
|
||||
}
|
||||
|
||||
|
|
@ -60,26 +59,21 @@ async function runHistoryWorkflow(ctx: any): Promise<void> {
|
|||
});
|
||||
}
|
||||
|
||||
export const history = actor({
|
||||
db: historyDb,
|
||||
export const auditLog = actor({
|
||||
db: auditLogDb,
|
||||
queues: {
|
||||
"history.command.append": queue(),
|
||||
"auditLog.command.append": queue(),
|
||||
},
|
||||
options: {
|
||||
name: "History",
|
||||
name: "Audit Log",
|
||||
icon: "database",
|
||||
},
|
||||
createState: (_c, input: HistoryInput) => ({
|
||||
createState: (_c, input: AuditLogInput) => ({
|
||||
organizationId: input.organizationId,
|
||||
repoId: input.repoId,
|
||||
}),
|
||||
actions: {
|
||||
async append(c, command: AppendHistoryCommand): Promise<void> {
|
||||
const self = selfHistory(c);
|
||||
await self.send("history.command.append", command, { wait: true, timeout: 15_000 });
|
||||
},
|
||||
|
||||
async list(c, params?: ListHistoryParams): Promise<HistoryEvent[]> {
|
||||
async list(c, params?: ListAuditLogParams): Promise<AuditLogEvent[]> {
|
||||
const whereParts = [];
|
||||
if (params?.taskId) {
|
||||
whereParts.push(eq(events.taskId, params.taskId));
|
||||
|
|
@ -111,5 +105,5 @@ export const history = actor({
|
|||
}));
|
||||
},
|
||||
},
|
||||
run: workflow(runHistoryWorkflow),
|
||||
run: workflow(runAuditLogWorkflow),
|
||||
});
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
import { integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
|
||||
|
||||
export const authUsers = sqliteTable("user", {
|
||||
id: text("id").notNull().primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
email: text("email").notNull(),
|
||||
emailVerified: integer("email_verified").notNull(),
|
||||
image: text("image"),
|
||||
createdAt: integer("created_at").notNull(),
|
||||
updatedAt: integer("updated_at").notNull(),
|
||||
});
|
||||
|
||||
export const authSessions = sqliteTable(
|
||||
"session",
|
||||
{
|
||||
id: text("id").notNull().primaryKey(),
|
||||
token: text("token").notNull(),
|
||||
userId: text("user_id").notNull(),
|
||||
expiresAt: integer("expires_at").notNull(),
|
||||
ipAddress: text("ip_address"),
|
||||
userAgent: text("user_agent"),
|
||||
createdAt: integer("created_at").notNull(),
|
||||
updatedAt: integer("updated_at").notNull(),
|
||||
},
|
||||
(table) => ({
|
||||
tokenIdx: uniqueIndex("session_token_idx").on(table.token),
|
||||
}),
|
||||
);
|
||||
|
||||
export const authAccounts = sqliteTable(
|
||||
"account",
|
||||
{
|
||||
id: text("id").notNull().primaryKey(),
|
||||
accountId: text("account_id").notNull(),
|
||||
providerId: text("provider_id").notNull(),
|
||||
userId: text("user_id").notNull(),
|
||||
accessToken: text("access_token"),
|
||||
refreshToken: text("refresh_token"),
|
||||
idToken: text("id_token"),
|
||||
accessTokenExpiresAt: integer("access_token_expires_at"),
|
||||
refreshTokenExpiresAt: integer("refresh_token_expires_at"),
|
||||
scope: text("scope"),
|
||||
password: text("password"),
|
||||
createdAt: integer("created_at").notNull(),
|
||||
updatedAt: integer("updated_at").notNull(),
|
||||
},
|
||||
(table) => ({
|
||||
providerAccountIdx: uniqueIndex("account_provider_account_idx").on(table.providerId, table.accountId),
|
||||
}),
|
||||
);
|
||||
|
||||
export const userProfiles = sqliteTable("user_profiles", {
|
||||
userId: text("user_id").notNull().primaryKey(),
|
||||
githubAccountId: text("github_account_id"),
|
||||
githubLogin: text("github_login"),
|
||||
roleLabel: text("role_label").notNull(),
|
||||
eligibleOrganizationIdsJson: text("eligible_organization_ids_json").notNull(),
|
||||
starterRepoStatus: text("starter_repo_status").notNull(),
|
||||
starterRepoStarredAt: integer("starter_repo_starred_at"),
|
||||
starterRepoSkippedAt: integer("starter_repo_skipped_at"),
|
||||
createdAt: integer("created_at").notNull(),
|
||||
updatedAt: integer("updated_at").notNull(),
|
||||
});
|
||||
|
||||
export const sessionState = sqliteTable("session_state", {
|
||||
sessionId: text("session_id").notNull().primaryKey(),
|
||||
activeOrganizationId: text("active_organization_id"),
|
||||
createdAt: integer("created_at").notNull(),
|
||||
updatedAt: integer("updated_at").notNull(),
|
||||
});
|
||||
|
|
@ -32,7 +32,8 @@ export default {
|
|||
\`installation_id\` integer,
|
||||
\`last_sync_label\` text NOT NULL,
|
||||
\`last_sync_at\` integer,
|
||||
\`updated_at\` integer NOT NULL
|
||||
\`updated_at\` integer NOT NULL,
|
||||
CONSTRAINT \`github_meta_singleton_id_check\` CHECK(\`id\` = 1)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE \`github_repositories\` (
|
||||
|
|
|
|||
|
|
@ -1,15 +1,20 @@
|
|||
import { integer, sqliteTable, text } from "rivetkit/db/drizzle";
|
||||
import { check, integer, sqliteTable, text } from "rivetkit/db/drizzle";
|
||||
import { sql } from "drizzle-orm";
|
||||
|
||||
export const githubMeta = sqliteTable("github_meta", {
|
||||
id: integer("id").primaryKey(),
|
||||
connectedAccount: text("connected_account").notNull(),
|
||||
installationStatus: text("installation_status").notNull(),
|
||||
syncStatus: text("sync_status").notNull(),
|
||||
installationId: integer("installation_id"),
|
||||
lastSyncLabel: text("last_sync_label").notNull(),
|
||||
lastSyncAt: integer("last_sync_at"),
|
||||
updatedAt: integer("updated_at").notNull(),
|
||||
});
|
||||
export const githubMeta = sqliteTable(
|
||||
"github_meta",
|
||||
{
|
||||
id: integer("id").primaryKey(),
|
||||
connectedAccount: text("connected_account").notNull(),
|
||||
installationStatus: text("installation_status").notNull(),
|
||||
syncStatus: text("sync_status").notNull(),
|
||||
installationId: integer("installation_id"),
|
||||
lastSyncLabel: text("last_sync_label").notNull(),
|
||||
lastSyncAt: integer("last_sync_at"),
|
||||
updatedAt: integer("updated_at").notNull(),
|
||||
},
|
||||
(table) => [check("github_meta_singleton_id_check", sql`${table.id} = 1`)],
|
||||
);
|
||||
|
||||
export const githubRepositories = sqliteTable("github_repositories", {
|
||||
repoId: text("repo_id").notNull().primaryKey(),
|
||||
|
|
|
|||
|
|
@ -681,15 +681,15 @@ export const githubData = actor({
|
|||
};
|
||||
},
|
||||
|
||||
async fullSync(c, input: FullSyncInput = {}) {
|
||||
async adminFullSync(c, input: FullSyncInput = {}) {
|
||||
return await runFullSync(c, input);
|
||||
},
|
||||
|
||||
async reloadOrganization(c) {
|
||||
async adminReloadOrganization(c) {
|
||||
return await runFullSync(c, { label: "Reloading GitHub organization..." });
|
||||
},
|
||||
|
||||
async reloadAllPullRequests(c) {
|
||||
async adminReloadAllPullRequests(c) {
|
||||
return await runFullSync(c, { label: "Reloading GitHub pull requests..." });
|
||||
},
|
||||
|
||||
|
|
@ -846,7 +846,7 @@ export const githubData = actor({
|
|||
);
|
||||
},
|
||||
|
||||
async clearState(c, input: ClearStateInput) {
|
||||
async adminClearState(c, input: ClearStateInput) {
|
||||
const beforeRows = await readAllPullRequestRows(c);
|
||||
await c.db.delete(githubPullRequests).run();
|
||||
await c.db.delete(githubBranches).run();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { authUserKey, githubDataKey, historyKey, organizationKey, repositoryKey, taskKey, taskSandboxKey } from "./keys.js";
|
||||
import { auditLogKey, githubDataKey, organizationKey, repositoryKey, taskKey, taskSandboxKey, userKey } from "./keys.js";
|
||||
|
||||
export function actorClient(c: any) {
|
||||
return c.client();
|
||||
|
|
@ -10,14 +10,14 @@ export async function getOrCreateOrganization(c: any, organizationId: string) {
|
|||
});
|
||||
}
|
||||
|
||||
export async function getOrCreateAuthUser(c: any, userId: string) {
|
||||
return await actorClient(c).authUser.getOrCreate(authUserKey(userId), {
|
||||
export async function getOrCreateUser(c: any, userId: string) {
|
||||
return await actorClient(c).user.getOrCreate(userKey(userId), {
|
||||
createWithInput: { userId },
|
||||
});
|
||||
}
|
||||
|
||||
export function getAuthUser(c: any, userId: string) {
|
||||
return actorClient(c).authUser.get(authUserKey(userId));
|
||||
export function getUser(c: any, userId: string) {
|
||||
return actorClient(c).user.get(userKey(userId));
|
||||
}
|
||||
|
||||
export async function getOrCreateRepository(c: any, organizationId: string, repoId: string, remoteUrl: string) {
|
||||
|
|
@ -44,8 +44,8 @@ export async function getOrCreateTask(c: any, organizationId: string, repoId: st
|
|||
});
|
||||
}
|
||||
|
||||
export async function getOrCreateHistory(c: any, organizationId: string, repoId: string) {
|
||||
return await actorClient(c).history.getOrCreate(historyKey(organizationId, repoId), {
|
||||
export async function getOrCreateAuditLog(c: any, organizationId: string, repoId: string) {
|
||||
return await actorClient(c).auditLog.getOrCreate(auditLogKey(organizationId, repoId), {
|
||||
createWithInput: {
|
||||
organizationId,
|
||||
repoId,
|
||||
|
|
@ -75,8 +75,8 @@ export async function getOrCreateTaskSandbox(c: any, organizationId: string, san
|
|||
});
|
||||
}
|
||||
|
||||
export function selfHistory(c: any) {
|
||||
return actorClient(c).history.getForId(c.actorId);
|
||||
export function selfAuditLog(c: any) {
|
||||
return actorClient(c).auditLog.getForId(c.actorId);
|
||||
}
|
||||
|
||||
export function selfTask(c: any) {
|
||||
|
|
@ -91,8 +91,8 @@ export function selfRepository(c: any) {
|
|||
return actorClient(c).repository.getForId(c.actorId);
|
||||
}
|
||||
|
||||
export function selfAuthUser(c: any) {
|
||||
return actorClient(c).authUser.getForId(c.actorId);
|
||||
export function selfUser(c: any) {
|
||||
return actorClient(c).user.getForId(c.actorId);
|
||||
}
|
||||
|
||||
export function selfGithubData(c: any) {
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
import { defineConfig } from "rivetkit/db/drizzle";
|
||||
|
||||
export default defineConfig({
|
||||
out: "./src/actors/history/db/drizzle",
|
||||
schema: "./src/actors/history/db/schema.ts",
|
||||
});
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import { authUser } from "./auth-user/index.js";
|
||||
import { user } from "./user/index.js";
|
||||
import { setup } from "rivetkit";
|
||||
import { githubData } from "./github-data/index.js";
|
||||
import { task } from "./task/index.js";
|
||||
import { history } from "./history/index.js";
|
||||
import { auditLog } from "./audit-log/index.js";
|
||||
import { repository } from "./repository/index.js";
|
||||
import { taskSandbox } from "./sandbox/index.js";
|
||||
import { organization } from "./organization/index.js";
|
||||
|
|
@ -21,22 +21,22 @@ export const registry = setup({
|
|||
baseLogger: logger,
|
||||
},
|
||||
use: {
|
||||
authUser,
|
||||
user,
|
||||
organization,
|
||||
repository,
|
||||
task,
|
||||
taskSandbox,
|
||||
history,
|
||||
auditLog,
|
||||
githubData,
|
||||
},
|
||||
});
|
||||
|
||||
export * from "./context.js";
|
||||
export * from "./events.js";
|
||||
export * from "./auth-user/index.js";
|
||||
export * from "./audit-log/index.js";
|
||||
export * from "./user/index.js";
|
||||
export * from "./github-data/index.js";
|
||||
export * from "./task/index.js";
|
||||
export * from "./history/index.js";
|
||||
export * from "./keys.js";
|
||||
export * from "./repository/index.js";
|
||||
export * from "./sandbox/index.js";
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ export function organizationKey(organizationId: string): ActorKey {
|
|||
return ["org", organizationId];
|
||||
}
|
||||
|
||||
export function authUserKey(userId: string): ActorKey {
|
||||
export function userKey(userId: string): ActorKey {
|
||||
return ["org", "app", "user", userId];
|
||||
}
|
||||
|
||||
|
|
@ -20,8 +20,8 @@ export function taskSandboxKey(organizationId: string, sandboxId: string): Actor
|
|||
return ["org", organizationId, "sandbox", sandboxId];
|
||||
}
|
||||
|
||||
export function historyKey(organizationId: string, repoId: string): ActorKey {
|
||||
return ["org", organizationId, "repository", repoId, "history"];
|
||||
export function auditLogKey(organizationId: string, repoId: string): ActorKey {
|
||||
return ["org", organizationId, "repository", repoId, "audit-log"];
|
||||
}
|
||||
|
||||
export function githubDataKey(organizationId: string): ActorKey {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { desc, eq } from "drizzle-orm";
|
|||
import { Loop } from "rivetkit/workflow";
|
||||
import type {
|
||||
CreateTaskInput,
|
||||
HistoryEvent,
|
||||
AuditLogEvent,
|
||||
HistoryQueryInput,
|
||||
ListTasksInput,
|
||||
SandboxProviderId,
|
||||
|
|
@ -14,32 +14,30 @@ import type {
|
|||
SwitchResult,
|
||||
TaskRecord,
|
||||
TaskSummary,
|
||||
TaskWorkbenchChangeModelInput,
|
||||
TaskWorkbenchCreateTaskInput,
|
||||
TaskWorkbenchDiffInput,
|
||||
TaskWorkbenchRenameInput,
|
||||
TaskWorkbenchRenameSessionInput,
|
||||
TaskWorkbenchSelectInput,
|
||||
TaskWorkbenchSetSessionUnreadInput,
|
||||
TaskWorkbenchSendMessageInput,
|
||||
TaskWorkbenchSessionInput,
|
||||
TaskWorkbenchUpdateDraftInput,
|
||||
WorkbenchOpenPrSummary,
|
||||
WorkbenchRepositorySummary,
|
||||
WorkbenchSessionSummary,
|
||||
WorkbenchTaskSummary,
|
||||
TaskWorkspaceChangeModelInput,
|
||||
TaskWorkspaceCreateTaskInput,
|
||||
TaskWorkspaceDiffInput,
|
||||
TaskWorkspaceRenameInput,
|
||||
TaskWorkspaceRenameSessionInput,
|
||||
TaskWorkspaceSelectInput,
|
||||
TaskWorkspaceSetSessionUnreadInput,
|
||||
TaskWorkspaceSendMessageInput,
|
||||
TaskWorkspaceSessionInput,
|
||||
TaskWorkspaceUpdateDraftInput,
|
||||
WorkspaceRepositorySummary,
|
||||
WorkspaceTaskSummary,
|
||||
OrganizationEvent,
|
||||
OrganizationSummarySnapshot,
|
||||
OrganizationUseInput,
|
||||
} from "@sandbox-agent/foundry-shared";
|
||||
import { getActorRuntimeContext } from "../context.js";
|
||||
import { getGithubData, getOrCreateGithubData, getTask, getOrCreateHistory, getOrCreateRepository, selfOrganization } from "../handles.js";
|
||||
import { getGithubData, getOrCreateAuditLog, getOrCreateGithubData, getTask as getTaskHandle, getOrCreateRepository, selfOrganization } from "../handles.js";
|
||||
import { logActorWarning, resolveErrorMessage } from "../logging.js";
|
||||
import { defaultSandboxProviderId } from "../../sandbox-config.js";
|
||||
import { repoIdFromRemote } from "../../services/repo.js";
|
||||
import { resolveOrganizationGithubAuth } from "../../services/github-auth.js";
|
||||
import { organizationProfile, taskLookup, repos, taskSummaries } from "./db/schema.js";
|
||||
import { agentTypeForModel } from "../task/workbench.js";
|
||||
import { organizationProfile, repos } from "./db/schema.js";
|
||||
import { agentTypeForModel } from "../task/workspace.js";
|
||||
import { expectQueueResponse } from "../../services/queue.js";
|
||||
import { organizationAppActions } from "./app-shell.js";
|
||||
|
||||
|
|
@ -49,6 +47,7 @@ interface OrganizationState {
|
|||
|
||||
interface GetTaskInput {
|
||||
organizationId: string;
|
||||
repoId?: string;
|
||||
taskId: string;
|
||||
}
|
||||
|
||||
|
|
@ -72,7 +71,7 @@ export function organizationWorkflowQueueName(name: OrganizationQueueName): Orga
|
|||
return name;
|
||||
}
|
||||
|
||||
const ORGANIZATION_PROFILE_ROW_ID = "profile";
|
||||
const ORGANIZATION_PROFILE_ROW_ID = 1;
|
||||
|
||||
function assertOrganization(c: { state: OrganizationState }, organizationId: string): void {
|
||||
if (organizationId !== c.state.organizationId) {
|
||||
|
|
@ -80,42 +79,6 @@ function assertOrganization(c: { state: OrganizationState }, organizationId: str
|
|||
}
|
||||
}
|
||||
|
||||
async function resolveRepoId(c: any, taskId: string): Promise<string> {
|
||||
const row = await c.db.select({ repoId: taskLookup.repoId }).from(taskLookup).where(eq(taskLookup.taskId, taskId)).get();
|
||||
|
||||
if (!row) {
|
||||
throw new Error(`Unknown task: ${taskId} (not in lookup)`);
|
||||
}
|
||||
|
||||
return row.repoId;
|
||||
}
|
||||
|
||||
async function upsertTaskLookupRow(c: any, taskId: string, repoId: string): Promise<void> {
|
||||
await c.db
|
||||
.insert(taskLookup)
|
||||
.values({
|
||||
taskId,
|
||||
repoId,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: taskLookup.taskId,
|
||||
set: { repoId },
|
||||
})
|
||||
.run();
|
||||
}
|
||||
|
||||
function parseJsonValue<T>(value: string | null | undefined, fallback: T): T {
|
||||
if (!value) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(value) as T;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
async function collectAllTaskSummaries(c: any): Promise<TaskSummary[]> {
|
||||
const repoRows = await c.db.select({ repoId: repos.repoId, remoteUrl: repos.remoteUrl }).from(repos).orderBy(desc(repos.updatedAt)).all();
|
||||
|
||||
|
|
@ -152,7 +115,7 @@ function repoLabelFromRemote(remoteUrl: string): string {
|
|||
return remoteUrl;
|
||||
}
|
||||
|
||||
function buildRepoSummary(repoRow: { repoId: string; remoteUrl: string; updatedAt: number }, taskRows: WorkbenchTaskSummary[]): WorkbenchRepositorySummary {
|
||||
function buildRepoSummary(repoRow: { repoId: string; remoteUrl: string; updatedAt: number }, taskRows: WorkspaceTaskSummary[]): WorkspaceRepositorySummary {
|
||||
const repoTasks = taskRows.filter((task) => task.repoId === repoRow.repoId);
|
||||
const latestActivityMs = repoTasks.reduce((latest, task) => Math.max(latest, task.updatedAtMs), repoRow.updatedAt);
|
||||
|
||||
|
|
@ -164,79 +127,42 @@ function buildRepoSummary(repoRow: { repoId: string; remoteUrl: string; updatedA
|
|||
};
|
||||
}
|
||||
|
||||
function taskSummaryRowFromSummary(taskSummary: WorkbenchTaskSummary) {
|
||||
return {
|
||||
taskId: taskSummary.id,
|
||||
repoId: taskSummary.repoId,
|
||||
title: taskSummary.title,
|
||||
status: taskSummary.status,
|
||||
repoName: taskSummary.repoName,
|
||||
updatedAtMs: taskSummary.updatedAtMs,
|
||||
branch: taskSummary.branch,
|
||||
pullRequestJson: JSON.stringify(taskSummary.pullRequest),
|
||||
sessionsSummaryJson: JSON.stringify(taskSummary.sessionsSummary),
|
||||
};
|
||||
async function resolveRepositoryForTask(c: any, taskId: string, repoId?: string | null) {
|
||||
if (repoId) {
|
||||
const repoRow = await c.db.select({ remoteUrl: repos.remoteUrl }).from(repos).where(eq(repos.repoId, repoId)).get();
|
||||
if (!repoRow) {
|
||||
throw new Error(`Unknown repo: ${repoId}`);
|
||||
}
|
||||
const repository = await getOrCreateRepository(c, c.state.organizationId, repoId, repoRow.remoteUrl);
|
||||
return { repoId, repository };
|
||||
}
|
||||
|
||||
const repoRows = await c.db.select({ repoId: repos.repoId, remoteUrl: repos.remoteUrl }).from(repos).orderBy(desc(repos.updatedAt)).all();
|
||||
for (const row of repoRows) {
|
||||
const repository = await getOrCreateRepository(c, c.state.organizationId, row.repoId, row.remoteUrl);
|
||||
const summaries = await repository.listTaskSummaries({ includeArchived: true });
|
||||
if (summaries.some((summary: TaskSummary) => summary.taskId === taskId)) {
|
||||
return { repoId: row.repoId, repository };
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Unknown task: ${taskId}`);
|
||||
}
|
||||
|
||||
function taskSummaryFromRow(row: any): WorkbenchTaskSummary {
|
||||
return {
|
||||
id: row.taskId,
|
||||
repoId: row.repoId,
|
||||
title: row.title,
|
||||
status: row.status,
|
||||
repoName: row.repoName,
|
||||
updatedAtMs: row.updatedAtMs,
|
||||
branch: row.branch ?? null,
|
||||
pullRequest: parseJsonValue(row.pullRequestJson, null),
|
||||
sessionsSummary: parseJsonValue<WorkbenchSessionSummary[]>(row.sessionsSummaryJson, []),
|
||||
};
|
||||
}
|
||||
|
||||
async function listOpenPullRequestsSnapshot(c: any, taskRows: WorkbenchTaskSummary[]): Promise<WorkbenchOpenPrSummary[]> {
|
||||
const githubData = getGithubData(c, c.state.organizationId);
|
||||
const openPullRequests = await githubData.listOpenPullRequests({}).catch(() => []);
|
||||
const claimedBranches = new Set(taskRows.filter((task) => task.branch).map((task) => `${task.repoId}:${task.branch}`));
|
||||
|
||||
return openPullRequests.filter((pullRequest: WorkbenchOpenPrSummary) => !claimedBranches.has(`${pullRequest.repoId}:${pullRequest.headRefName}`));
|
||||
}
|
||||
|
||||
async function reconcileWorkbenchProjection(c: any): Promise<OrganizationSummarySnapshot> {
|
||||
async function reconcileWorkspaceProjection(c: any): Promise<OrganizationSummarySnapshot> {
|
||||
const repoRows = await c.db
|
||||
.select({ repoId: repos.repoId, remoteUrl: repos.remoteUrl, updatedAt: repos.updatedAt })
|
||||
.from(repos)
|
||||
.orderBy(desc(repos.updatedAt))
|
||||
.all();
|
||||
|
||||
const taskRows: WorkbenchTaskSummary[] = [];
|
||||
const taskRows: WorkspaceTaskSummary[] = [];
|
||||
for (const row of repoRows) {
|
||||
try {
|
||||
const repository = await getOrCreateRepository(c, c.state.organizationId, row.repoId, row.remoteUrl);
|
||||
const summaries = await repository.listTaskSummaries({ includeArchived: true });
|
||||
for (const summary of summaries) {
|
||||
try {
|
||||
await upsertTaskLookupRow(c, summary.taskId, row.repoId);
|
||||
const task = getTask(c, c.state.organizationId, row.repoId, summary.taskId);
|
||||
const taskSummary = await task.getTaskSummary({});
|
||||
taskRows.push(taskSummary);
|
||||
await c.db
|
||||
.insert(taskSummaries)
|
||||
.values(taskSummaryRowFromSummary(taskSummary))
|
||||
.onConflictDoUpdate({
|
||||
target: taskSummaries.taskId,
|
||||
set: taskSummaryRowFromSummary(taskSummary),
|
||||
})
|
||||
.run();
|
||||
} catch (error) {
|
||||
logActorWarning("organization", "failed collecting task summary during reconciliation", {
|
||||
organizationId: c.state.organizationId,
|
||||
repoId: row.repoId,
|
||||
taskId: summary.taskId,
|
||||
error: resolveErrorMessage(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
taskRows.push(...(await repository.listWorkspaceTaskSummaries({})));
|
||||
} catch (error) {
|
||||
logActorWarning("organization", "failed collecting repo during workbench reconciliation", {
|
||||
logActorWarning("organization", "failed collecting repo during workspace reconciliation", {
|
||||
organizationId: c.state.organizationId,
|
||||
repoId: row.repoId,
|
||||
error: resolveErrorMessage(error),
|
||||
|
|
@ -249,19 +175,17 @@ async function reconcileWorkbenchProjection(c: any): Promise<OrganizationSummary
|
|||
organizationId: c.state.organizationId,
|
||||
repos: repoRows.map((row) => buildRepoSummary(row, taskRows)).sort((left, right) => right.latestActivityMs - left.latestActivityMs),
|
||||
taskSummaries: taskRows,
|
||||
openPullRequests: await listOpenPullRequestsSnapshot(c, taskRows),
|
||||
};
|
||||
}
|
||||
|
||||
async function requireWorkbenchTask(c: any, taskId: string) {
|
||||
const repoId = await resolveRepoId(c, taskId);
|
||||
return getTask(c, c.state.organizationId, repoId, taskId);
|
||||
async function requireWorkspaceTask(c: any, repoId: string, taskId: string) {
|
||||
return getTaskHandle(c, c.state.organizationId, repoId, taskId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the organization sidebar snapshot from the organization actor's local SQLite
|
||||
* plus the org-scoped GitHub actor for open PRs. Task actors still push
|
||||
* summary updates into `task_summaries`, so the hot read path stays bounded.
|
||||
* Reads the organization sidebar snapshot by fanning out one level to the
|
||||
* repository coordinators. Task summaries are repository-owned; organization
|
||||
* only aggregates them.
|
||||
*/
|
||||
async function getOrganizationSummarySnapshot(c: any): Promise<OrganizationSummarySnapshot> {
|
||||
const repoRows = await c.db
|
||||
|
|
@ -273,25 +197,33 @@ async function getOrganizationSummarySnapshot(c: any): Promise<OrganizationSumma
|
|||
.from(repos)
|
||||
.orderBy(desc(repos.updatedAt))
|
||||
.all();
|
||||
const taskRows = await c.db.select().from(taskSummaries).orderBy(desc(taskSummaries.updatedAtMs)).all();
|
||||
const summaries = taskRows.map(taskSummaryFromRow);
|
||||
const summaries: WorkspaceTaskSummary[] = [];
|
||||
for (const row of repoRows) {
|
||||
try {
|
||||
const repository = await getOrCreateRepository(c, c.state.organizationId, row.repoId, row.remoteUrl);
|
||||
summaries.push(...(await repository.listWorkspaceTaskSummaries({})));
|
||||
} catch (error) {
|
||||
logActorWarning("organization", "failed reading repository task projection", {
|
||||
organizationId: c.state.organizationId,
|
||||
repoId: row.repoId,
|
||||
error: resolveErrorMessage(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
summaries.sort((left, right) => right.updatedAtMs - left.updatedAtMs);
|
||||
|
||||
return {
|
||||
organizationId: c.state.organizationId,
|
||||
repos: repoRows.map((row) => buildRepoSummary(row, summaries)).sort((left, right) => right.latestActivityMs - left.latestActivityMs),
|
||||
taskSummaries: summaries,
|
||||
openPullRequests: await listOpenPullRequestsSnapshot(c, summaries),
|
||||
};
|
||||
}
|
||||
|
||||
async function broadcastRepoSummary(
|
||||
c: any,
|
||||
type: "repoAdded" | "repoUpdated",
|
||||
repoRow: { repoId: string; remoteUrl: string; updatedAt: number },
|
||||
): Promise<void> {
|
||||
const matchingTaskRows = await c.db.select().from(taskSummaries).where(eq(taskSummaries.repoId, repoRow.repoId)).all();
|
||||
const repo = buildRepoSummary(repoRow, matchingTaskRows.map(taskSummaryFromRow));
|
||||
c.broadcast("organizationUpdated", { type, repo } satisfies OrganizationEvent);
|
||||
async function broadcastOrganizationSnapshot(c: any): Promise<void> {
|
||||
c.broadcast("organizationUpdated", {
|
||||
type: "organizationUpdated",
|
||||
snapshot: await getOrganizationSummarySnapshot(c),
|
||||
} satisfies OrganizationEvent);
|
||||
}
|
||||
|
||||
async function createTaskMutation(c: any, input: CreateTaskInput): Promise<TaskRecord> {
|
||||
|
|
@ -318,32 +250,6 @@ async function createTaskMutation(c: any, input: CreateTaskInput): Promise<TaskR
|
|||
onBranch: input.onBranch ?? null,
|
||||
});
|
||||
|
||||
await c.db
|
||||
.insert(taskLookup)
|
||||
.values({
|
||||
taskId: created.taskId,
|
||||
repoId,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: taskLookup.taskId,
|
||||
set: { repoId },
|
||||
})
|
||||
.run();
|
||||
|
||||
try {
|
||||
const task = getTask(c, c.state.organizationId, repoId, created.taskId);
|
||||
await organizationActions.applyTaskSummaryUpdate(c, {
|
||||
taskSummary: await task.getTaskSummary({}),
|
||||
});
|
||||
} catch (error) {
|
||||
logActorWarning("organization", "failed seeding task summary after task creation", {
|
||||
organizationId: c.state.organizationId,
|
||||
repoId,
|
||||
taskId: created.taskId,
|
||||
error: resolveErrorMessage(error),
|
||||
});
|
||||
}
|
||||
|
||||
return created;
|
||||
}
|
||||
|
||||
|
|
@ -451,67 +357,8 @@ export const organizationActions = {
|
|||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Called by task actors when their summary-level state changes.
|
||||
* This is the write path for the local materialized projection; clients read
|
||||
* the projection via `getOrganizationSummary`, but only task actors should push
|
||||
* rows into it.
|
||||
*/
|
||||
async applyTaskSummaryUpdate(c: any, input: { taskSummary: WorkbenchTaskSummary }): Promise<void> {
|
||||
await c.db
|
||||
.insert(taskSummaries)
|
||||
.values(taskSummaryRowFromSummary(input.taskSummary))
|
||||
.onConflictDoUpdate({
|
||||
target: taskSummaries.taskId,
|
||||
set: taskSummaryRowFromSummary(input.taskSummary),
|
||||
})
|
||||
.run();
|
||||
c.broadcast("organizationUpdated", { type: "taskSummaryUpdated", taskSummary: input.taskSummary } satisfies OrganizationEvent);
|
||||
},
|
||||
|
||||
async removeTaskSummary(c: any, input: { taskId: string }): Promise<void> {
|
||||
await c.db.delete(taskSummaries).where(eq(taskSummaries.taskId, input.taskId)).run();
|
||||
c.broadcast("organizationUpdated", { type: "taskRemoved", taskId: input.taskId } satisfies OrganizationEvent);
|
||||
},
|
||||
|
||||
async findTaskForGithubBranch(c: any, input: { repoId: string; branchName: string }): Promise<{ taskId: string | null }> {
|
||||
const summaries = await c.db.select().from(taskSummaries).where(eq(taskSummaries.repoId, input.repoId)).all();
|
||||
const existing = summaries.find((summary) => summary.branch === input.branchName);
|
||||
return { taskId: existing?.taskId ?? null };
|
||||
},
|
||||
|
||||
async refreshTaskSummaryForGithubBranch(c: any, input: { repoId: string; branchName: string }): Promise<void> {
|
||||
const summaries = await c.db.select().from(taskSummaries).where(eq(taskSummaries.repoId, input.repoId)).all();
|
||||
const matches = summaries.filter((summary) => summary.branch === input.branchName);
|
||||
|
||||
for (const summary of matches) {
|
||||
try {
|
||||
const task = getTask(c, c.state.organizationId, input.repoId, summary.taskId);
|
||||
await organizationActions.applyTaskSummaryUpdate(c, {
|
||||
taskSummary: await task.getTaskSummary({}),
|
||||
});
|
||||
} catch (error) {
|
||||
logActorWarning("organization", "failed refreshing task summary for GitHub branch", {
|
||||
organizationId: c.state.organizationId,
|
||||
repoId: input.repoId,
|
||||
branchName: input.branchName,
|
||||
taskId: summary.taskId,
|
||||
error: resolveErrorMessage(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async applyOpenPullRequestUpdate(c: any, input: { pullRequest: WorkbenchOpenPrSummary }): Promise<void> {
|
||||
const summaries = await c.db.select().from(taskSummaries).where(eq(taskSummaries.repoId, input.pullRequest.repoId)).all();
|
||||
if (summaries.some((summary) => summary.branch === input.pullRequest.headRefName)) {
|
||||
return;
|
||||
}
|
||||
c.broadcast("organizationUpdated", { type: "pullRequestUpdated", pullRequest: input.pullRequest } satisfies OrganizationEvent);
|
||||
},
|
||||
|
||||
async removeOpenPullRequest(c: any, input: { prId: string }): Promise<void> {
|
||||
c.broadcast("organizationUpdated", { type: "pullRequestRemoved", prId: input.prId } satisfies OrganizationEvent);
|
||||
async refreshOrganizationSnapshot(c: any): Promise<void> {
|
||||
await broadcastOrganizationSnapshot(c);
|
||||
},
|
||||
|
||||
async applyGithubRepositoryProjection(c: any, input: { repoId: string; remoteUrl: string }): Promise<void> {
|
||||
|
|
@ -533,11 +380,7 @@ export const organizationActions = {
|
|||
},
|
||||
})
|
||||
.run();
|
||||
await broadcastRepoSummary(c, existing ? "repoUpdated" : "repoAdded", {
|
||||
repoId: input.repoId,
|
||||
remoteUrl: input.remoteUrl,
|
||||
updatedAt: now,
|
||||
});
|
||||
await broadcastOrganizationSnapshot(c);
|
||||
},
|
||||
|
||||
async applyGithubDataProjection(
|
||||
|
|
@ -576,11 +419,7 @@ export const organizationActions = {
|
|||
},
|
||||
})
|
||||
.run();
|
||||
await broadcastRepoSummary(c, existingById.has(repoId) ? "repoUpdated" : "repoAdded", {
|
||||
repoId,
|
||||
remoteUrl: repository.cloneUrl,
|
||||
updatedAt: now,
|
||||
});
|
||||
await broadcastOrganizationSnapshot(c);
|
||||
}
|
||||
|
||||
for (const repo of existingRepos) {
|
||||
|
|
@ -588,7 +427,7 @@ export const organizationActions = {
|
|||
continue;
|
||||
}
|
||||
await c.db.delete(repos).where(eq(repos.repoId, repo.repoId)).run();
|
||||
c.broadcast("organizationUpdated", { type: "repoRemoved", repoId: repo.repoId } satisfies OrganizationEvent);
|
||||
await broadcastOrganizationSnapshot(c);
|
||||
}
|
||||
|
||||
const profile = await c.db
|
||||
|
|
@ -648,12 +487,12 @@ export const organizationActions = {
|
|||
return await getOrganizationSummarySnapshot(c);
|
||||
},
|
||||
|
||||
async reconcileWorkbenchState(c: any, input: OrganizationUseInput): Promise<OrganizationSummarySnapshot> {
|
||||
async adminReconcileWorkspaceState(c: any, input: OrganizationUseInput): Promise<OrganizationSummarySnapshot> {
|
||||
assertOrganization(c, input.organizationId);
|
||||
return await reconcileWorkbenchProjection(c);
|
||||
return await reconcileWorkspaceProjection(c);
|
||||
},
|
||||
|
||||
async createWorkbenchTask(c: any, input: TaskWorkbenchCreateTaskInput): Promise<{ taskId: string; sessionId?: string }> {
|
||||
async createWorkspaceTask(c: any, input: TaskWorkspaceCreateTaskInput): Promise<{ taskId: string; sessionId?: string }> {
|
||||
// Step 1: Create the task record (wait: true — local state mutations only).
|
||||
const created = await organizationActions.createTask(c, {
|
||||
organizationId: c.state.organizationId,
|
||||
|
|
@ -668,8 +507,8 @@ export const organizationActions = {
|
|||
// The task workflow creates the session record and sends the message in
|
||||
// the background. The client observes progress via push events on the
|
||||
// task subscription topic.
|
||||
const task = await requireWorkbenchTask(c, created.taskId);
|
||||
await task.createWorkbenchSessionAndSend({
|
||||
const task = await requireWorkspaceTask(c, input.repoId, created.taskId);
|
||||
await task.createWorkspaceSessionAndSend({
|
||||
model: input.model,
|
||||
text: input.task,
|
||||
});
|
||||
|
|
@ -677,84 +516,79 @@ export const organizationActions = {
|
|||
return { taskId: created.taskId };
|
||||
},
|
||||
|
||||
async markWorkbenchUnread(c: any, input: TaskWorkbenchSelectInput): Promise<void> {
|
||||
const task = await requireWorkbenchTask(c, input.taskId);
|
||||
await task.markWorkbenchUnread({});
|
||||
async markWorkspaceUnread(c: any, input: TaskWorkspaceSelectInput): Promise<void> {
|
||||
const task = await requireWorkspaceTask(c, input.repoId, input.taskId);
|
||||
await task.markWorkspaceUnread({});
|
||||
},
|
||||
|
||||
async renameWorkbenchTask(c: any, input: TaskWorkbenchRenameInput): Promise<void> {
|
||||
const task = await requireWorkbenchTask(c, input.taskId);
|
||||
await task.renameWorkbenchTask(input);
|
||||
async renameWorkspaceTask(c: any, input: TaskWorkspaceRenameInput): Promise<void> {
|
||||
const task = await requireWorkspaceTask(c, input.repoId, input.taskId);
|
||||
await task.renameWorkspaceTask(input);
|
||||
},
|
||||
|
||||
async renameWorkbenchBranch(c: any, input: TaskWorkbenchRenameInput): Promise<void> {
|
||||
const task = await requireWorkbenchTask(c, input.taskId);
|
||||
await task.renameWorkbenchBranch(input);
|
||||
async createWorkspaceSession(c: any, input: TaskWorkspaceSelectInput & { model?: string }): Promise<{ sessionId: string }> {
|
||||
const task = await requireWorkspaceTask(c, input.repoId, input.taskId);
|
||||
return await task.createWorkspaceSession({ ...(input.model ? { model: input.model } : {}) });
|
||||
},
|
||||
|
||||
async createWorkbenchSession(c: any, input: TaskWorkbenchSelectInput & { model?: string }): Promise<{ sessionId: string }> {
|
||||
const task = await requireWorkbenchTask(c, input.taskId);
|
||||
return await task.createWorkbenchSession({ ...(input.model ? { model: input.model } : {}) });
|
||||
async renameWorkspaceSession(c: any, input: TaskWorkspaceRenameSessionInput): Promise<void> {
|
||||
const task = await requireWorkspaceTask(c, input.repoId, input.taskId);
|
||||
await task.renameWorkspaceSession(input);
|
||||
},
|
||||
|
||||
async renameWorkbenchSession(c: any, input: TaskWorkbenchRenameSessionInput): Promise<void> {
|
||||
const task = await requireWorkbenchTask(c, input.taskId);
|
||||
await task.renameWorkbenchSession(input);
|
||||
async setWorkspaceSessionUnread(c: any, input: TaskWorkspaceSetSessionUnreadInput): Promise<void> {
|
||||
const task = await requireWorkspaceTask(c, input.repoId, input.taskId);
|
||||
await task.setWorkspaceSessionUnread(input);
|
||||
},
|
||||
|
||||
async setWorkbenchSessionUnread(c: any, input: TaskWorkbenchSetSessionUnreadInput): Promise<void> {
|
||||
const task = await requireWorkbenchTask(c, input.taskId);
|
||||
await task.setWorkbenchSessionUnread(input);
|
||||
async updateWorkspaceDraft(c: any, input: TaskWorkspaceUpdateDraftInput): Promise<void> {
|
||||
const task = await requireWorkspaceTask(c, input.repoId, input.taskId);
|
||||
await task.updateWorkspaceDraft(input);
|
||||
},
|
||||
|
||||
async updateWorkbenchDraft(c: any, input: TaskWorkbenchUpdateDraftInput): Promise<void> {
|
||||
const task = await requireWorkbenchTask(c, input.taskId);
|
||||
await task.updateWorkbenchDraft(input);
|
||||
async changeWorkspaceModel(c: any, input: TaskWorkspaceChangeModelInput): Promise<void> {
|
||||
const task = await requireWorkspaceTask(c, input.repoId, input.taskId);
|
||||
await task.changeWorkspaceModel(input);
|
||||
},
|
||||
|
||||
async changeWorkbenchModel(c: any, input: TaskWorkbenchChangeModelInput): Promise<void> {
|
||||
const task = await requireWorkbenchTask(c, input.taskId);
|
||||
await task.changeWorkbenchModel(input);
|
||||
async sendWorkspaceMessage(c: any, input: TaskWorkspaceSendMessageInput): Promise<void> {
|
||||
const task = await requireWorkspaceTask(c, input.repoId, input.taskId);
|
||||
await task.sendWorkspaceMessage(input);
|
||||
},
|
||||
|
||||
async sendWorkbenchMessage(c: any, input: TaskWorkbenchSendMessageInput): Promise<void> {
|
||||
const task = await requireWorkbenchTask(c, input.taskId);
|
||||
await task.sendWorkbenchMessage(input);
|
||||
async stopWorkspaceSession(c: any, input: TaskWorkspaceSessionInput): Promise<void> {
|
||||
const task = await requireWorkspaceTask(c, input.repoId, input.taskId);
|
||||
await task.stopWorkspaceSession(input);
|
||||
},
|
||||
|
||||
async stopWorkbenchSession(c: any, input: TaskWorkbenchSessionInput): Promise<void> {
|
||||
const task = await requireWorkbenchTask(c, input.taskId);
|
||||
await task.stopWorkbenchSession(input);
|
||||
async closeWorkspaceSession(c: any, input: TaskWorkspaceSessionInput): Promise<void> {
|
||||
const task = await requireWorkspaceTask(c, input.repoId, input.taskId);
|
||||
await task.closeWorkspaceSession(input);
|
||||
},
|
||||
|
||||
async closeWorkbenchSession(c: any, input: TaskWorkbenchSessionInput): Promise<void> {
|
||||
const task = await requireWorkbenchTask(c, input.taskId);
|
||||
await task.closeWorkbenchSession(input);
|
||||
async publishWorkspacePr(c: any, input: TaskWorkspaceSelectInput): Promise<void> {
|
||||
const task = await requireWorkspaceTask(c, input.repoId, input.taskId);
|
||||
await task.publishWorkspacePr({});
|
||||
},
|
||||
|
||||
async publishWorkbenchPr(c: any, input: TaskWorkbenchSelectInput): Promise<void> {
|
||||
const task = await requireWorkbenchTask(c, input.taskId);
|
||||
await task.publishWorkbenchPr({});
|
||||
async revertWorkspaceFile(c: any, input: TaskWorkspaceDiffInput): Promise<void> {
|
||||
const task = await requireWorkspaceTask(c, input.repoId, input.taskId);
|
||||
await task.revertWorkspaceFile(input);
|
||||
},
|
||||
|
||||
async revertWorkbenchFile(c: any, input: TaskWorkbenchDiffInput): Promise<void> {
|
||||
const task = await requireWorkbenchTask(c, input.taskId);
|
||||
await task.revertWorkbenchFile(input);
|
||||
async adminReloadGithubOrganization(c: any): Promise<void> {
|
||||
await getOrCreateGithubData(c, c.state.organizationId).adminReloadOrganization({});
|
||||
},
|
||||
|
||||
async reloadGithubOrganization(c: any): Promise<void> {
|
||||
await getOrCreateGithubData(c, c.state.organizationId).reloadOrganization({});
|
||||
async adminReloadGithubPullRequests(c: any): Promise<void> {
|
||||
await getOrCreateGithubData(c, c.state.organizationId).adminReloadAllPullRequests({});
|
||||
},
|
||||
|
||||
async reloadGithubPullRequests(c: any): Promise<void> {
|
||||
await getOrCreateGithubData(c, c.state.organizationId).reloadAllPullRequests({});
|
||||
},
|
||||
|
||||
async reloadGithubRepository(c: any, input: { repoId: string }): Promise<void> {
|
||||
async adminReloadGithubRepository(c: any, input: { repoId: string }): Promise<void> {
|
||||
await getOrCreateGithubData(c, c.state.organizationId).reloadRepository(input);
|
||||
},
|
||||
|
||||
async reloadGithubPullRequest(c: any, input: { repoId: string; prNumber: number }): Promise<void> {
|
||||
async adminReloadGithubPullRequest(c: any, input: { repoId: string; prNumber: number }): Promise<void> {
|
||||
await getOrCreateGithubData(c, c.state.organizationId).reloadPullRequest(input);
|
||||
},
|
||||
|
||||
|
|
@ -786,39 +620,39 @@ export const organizationActions = {
|
|||
return await repository.getRepoOverview({});
|
||||
},
|
||||
|
||||
async switchTask(c: any, taskId: string): Promise<SwitchResult> {
|
||||
const repoId = await resolveRepoId(c, taskId);
|
||||
const h = getTask(c, c.state.organizationId, repoId, taskId);
|
||||
async switchTask(c: any, input: { repoId?: string; taskId: string }): Promise<SwitchResult> {
|
||||
const { repoId } = await resolveRepositoryForTask(c, input.taskId, input.repoId);
|
||||
const h = getTaskHandle(c, c.state.organizationId, repoId, input.taskId);
|
||||
const record = await h.get();
|
||||
const switched = await h.switch();
|
||||
|
||||
return {
|
||||
organizationId: c.state.organizationId,
|
||||
taskId,
|
||||
taskId: input.taskId,
|
||||
sandboxProviderId: record.sandboxProviderId,
|
||||
switchTarget: switched.switchTarget,
|
||||
};
|
||||
},
|
||||
|
||||
async history(c: any, input: HistoryQueryInput): Promise<HistoryEvent[]> {
|
||||
async auditLog(c: any, input: HistoryQueryInput): Promise<AuditLogEvent[]> {
|
||||
assertOrganization(c, input.organizationId);
|
||||
|
||||
const limit = input.limit ?? 20;
|
||||
const repoRows = await c.db.select({ repoId: repos.repoId }).from(repos).all();
|
||||
|
||||
const allEvents: HistoryEvent[] = [];
|
||||
const allEvents: AuditLogEvent[] = [];
|
||||
|
||||
for (const row of repoRows) {
|
||||
try {
|
||||
const hist = await getOrCreateHistory(c, c.state.organizationId, row.repoId);
|
||||
const items = await hist.list({
|
||||
const auditLog = await getOrCreateAuditLog(c, c.state.organizationId, row.repoId);
|
||||
const items = await auditLog.list({
|
||||
branch: input.branch,
|
||||
taskId: input.taskId,
|
||||
limit,
|
||||
});
|
||||
allEvents.push(...items);
|
||||
} catch (error) {
|
||||
logActorWarning("organization", "history lookup failed for repo", {
|
||||
logActorWarning("organization", "audit log lookup failed for repo", {
|
||||
organizationId: c.state.organizationId,
|
||||
repoId: row.repoId,
|
||||
error: resolveErrorMessage(error),
|
||||
|
|
@ -832,57 +666,49 @@ export const organizationActions = {
|
|||
|
||||
async getTask(c: any, input: GetTaskInput): Promise<TaskRecord> {
|
||||
assertOrganization(c, input.organizationId);
|
||||
|
||||
const repoId = await resolveRepoId(c, input.taskId);
|
||||
|
||||
const repoRow = await c.db.select({ remoteUrl: repos.remoteUrl }).from(repos).where(eq(repos.repoId, repoId)).get();
|
||||
if (!repoRow) {
|
||||
throw new Error(`Unknown repo: ${repoId}`);
|
||||
}
|
||||
|
||||
const repository = await getOrCreateRepository(c, c.state.organizationId, repoId, repoRow.remoteUrl);
|
||||
return await repository.getTaskEnriched({ taskId: input.taskId });
|
||||
const { repoId } = await resolveRepositoryForTask(c, input.taskId, input.repoId);
|
||||
return await getTaskHandle(c, c.state.organizationId, repoId, input.taskId).get();
|
||||
},
|
||||
|
||||
async attachTask(c: any, input: TaskProxyActionInput): Promise<{ target: string; sessionId: string | null }> {
|
||||
assertOrganization(c, input.organizationId);
|
||||
const repoId = await resolveRepoId(c, input.taskId);
|
||||
const h = getTask(c, c.state.organizationId, repoId, input.taskId);
|
||||
const { repoId } = await resolveRepositoryForTask(c, input.taskId, input.repoId);
|
||||
const h = getTaskHandle(c, c.state.organizationId, repoId, input.taskId);
|
||||
return await h.attach({ reason: input.reason });
|
||||
},
|
||||
|
||||
async pushTask(c: any, input: TaskProxyActionInput): Promise<void> {
|
||||
assertOrganization(c, input.organizationId);
|
||||
const repoId = await resolveRepoId(c, input.taskId);
|
||||
const h = getTask(c, c.state.organizationId, repoId, input.taskId);
|
||||
const { repoId } = await resolveRepositoryForTask(c, input.taskId, input.repoId);
|
||||
const h = getTaskHandle(c, c.state.organizationId, repoId, input.taskId);
|
||||
await h.push({ reason: input.reason });
|
||||
},
|
||||
|
||||
async syncTask(c: any, input: TaskProxyActionInput): Promise<void> {
|
||||
assertOrganization(c, input.organizationId);
|
||||
const repoId = await resolveRepoId(c, input.taskId);
|
||||
const h = getTask(c, c.state.organizationId, repoId, input.taskId);
|
||||
const { repoId } = await resolveRepositoryForTask(c, input.taskId, input.repoId);
|
||||
const h = getTaskHandle(c, c.state.organizationId, repoId, input.taskId);
|
||||
await h.sync({ reason: input.reason });
|
||||
},
|
||||
|
||||
async mergeTask(c: any, input: TaskProxyActionInput): Promise<void> {
|
||||
assertOrganization(c, input.organizationId);
|
||||
const repoId = await resolveRepoId(c, input.taskId);
|
||||
const h = getTask(c, c.state.organizationId, repoId, input.taskId);
|
||||
const { repoId } = await resolveRepositoryForTask(c, input.taskId, input.repoId);
|
||||
const h = getTaskHandle(c, c.state.organizationId, repoId, input.taskId);
|
||||
await h.merge({ reason: input.reason });
|
||||
},
|
||||
|
||||
async archiveTask(c: any, input: TaskProxyActionInput): Promise<void> {
|
||||
assertOrganization(c, input.organizationId);
|
||||
const repoId = await resolveRepoId(c, input.taskId);
|
||||
const h = getTask(c, c.state.organizationId, repoId, input.taskId);
|
||||
const { repoId } = await resolveRepositoryForTask(c, input.taskId, input.repoId);
|
||||
const h = getTaskHandle(c, c.state.organizationId, repoId, input.taskId);
|
||||
await h.archive({ reason: input.reason });
|
||||
},
|
||||
|
||||
async killTask(c: any, input: TaskProxyActionInput): Promise<void> {
|
||||
assertOrganization(c, input.organizationId);
|
||||
const repoId = await resolveRepoId(c, input.taskId);
|
||||
const h = getTask(c, c.state.organizationId, repoId, input.taskId);
|
||||
const { repoId } = await resolveRepositoryForTask(c, input.taskId, input.repoId);
|
||||
const h = getTaskHandle(c, c.state.organizationId, repoId, input.taskId);
|
||||
await h.kill({ reason: input.reason });
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import type {
|
|||
FoundryOrganizationMember,
|
||||
FoundryUser,
|
||||
UpdateFoundryOrganizationProfileInput,
|
||||
WorkspaceModelId,
|
||||
} from "@sandbox-agent/foundry-shared";
|
||||
import { getActorRuntimeContext } from "../context.js";
|
||||
import { getOrCreateGithubData, getOrCreateOrganization, selfOrganization } from "../handles.js";
|
||||
|
|
@ -98,7 +99,7 @@ const githubWebhookLogger = logger.child({
|
|||
scope: "github-webhook",
|
||||
});
|
||||
|
||||
const PROFILE_ROW_ID = "profile";
|
||||
const PROFILE_ROW_ID = 1;
|
||||
|
||||
function roundDurationMs(start: number): number {
|
||||
return Math.round((performance.now() - start) * 100) / 100;
|
||||
|
|
@ -359,6 +360,7 @@ async function buildAppSnapshot(c: any, sessionId: string, allowOrganizationRepa
|
|||
githubLogin: profile?.githubLogin ?? "",
|
||||
roleLabel: profile?.roleLabel ?? "GitHub user",
|
||||
eligibleOrganizationIds,
|
||||
defaultModel: profile?.defaultModel ?? "claude-sonnet-4",
|
||||
}
|
||||
: null;
|
||||
|
||||
|
|
@ -685,7 +687,6 @@ async function buildOrganizationStateFromRow(c: any, row: any, startedAt: number
|
|||
slug: row.slug,
|
||||
primaryDomain: row.primaryDomain,
|
||||
seatAccrualMode: "first_prompt",
|
||||
defaultModel: row.defaultModel,
|
||||
autoImportRepos: row.autoImportRepos === 1,
|
||||
},
|
||||
github: {
|
||||
|
|
@ -1078,6 +1079,15 @@ export const organizationAppActions = {
|
|||
return await buildAppSnapshot(c, input.sessionId);
|
||||
},
|
||||
|
||||
async setAppDefaultModel(c: any, input: { sessionId: string; defaultModel: WorkspaceModelId }): Promise<FoundryAppSnapshot> {
|
||||
assertAppOrganization(c);
|
||||
const session = await requireSignedInSession(c, input.sessionId);
|
||||
await getBetterAuthService().upsertUserProfile(session.authUserId, {
|
||||
defaultModel: input.defaultModel,
|
||||
});
|
||||
return await buildAppSnapshot(c, input.sessionId);
|
||||
},
|
||||
|
||||
async updateAppOrganizationProfile(
|
||||
c: any,
|
||||
input: { sessionId: string; organizationId: string } & UpdateFoundryOrganizationProfileInput,
|
||||
|
|
@ -1393,14 +1403,14 @@ export const organizationAppActions = {
|
|||
"installation_event",
|
||||
);
|
||||
if (body.action === "deleted") {
|
||||
await githubData.clearState({
|
||||
await githubData.adminClearState({
|
||||
connectedAccount: accountLogin,
|
||||
installationStatus: "install_required",
|
||||
installationId: null,
|
||||
label: "GitHub App installation removed",
|
||||
});
|
||||
} else if (body.action === "created") {
|
||||
await githubData.fullSync({
|
||||
await githubData.adminFullSync({
|
||||
connectedAccount: accountLogin,
|
||||
installationStatus: "connected",
|
||||
installationId: body.installation?.id ?? null,
|
||||
|
|
@ -1409,14 +1419,14 @@ export const organizationAppActions = {
|
|||
label: "Syncing GitHub data from installation webhook...",
|
||||
});
|
||||
} else if (body.action === "suspend") {
|
||||
await githubData.clearState({
|
||||
await githubData.adminClearState({
|
||||
connectedAccount: accountLogin,
|
||||
installationStatus: "reconnect_required",
|
||||
installationId: body.installation?.id ?? null,
|
||||
label: "GitHub App installation suspended",
|
||||
});
|
||||
} else if (body.action === "unsuspend") {
|
||||
await githubData.fullSync({
|
||||
await githubData.adminFullSync({
|
||||
connectedAccount: accountLogin,
|
||||
installationStatus: "connected",
|
||||
installationId: body.installation?.id ?? null,
|
||||
|
|
@ -1440,7 +1450,7 @@ export const organizationAppActions = {
|
|||
},
|
||||
"repository_membership_changed",
|
||||
);
|
||||
await githubData.fullSync({
|
||||
await githubData.adminFullSync({
|
||||
connectedAccount: accountLogin,
|
||||
installationStatus: "connected",
|
||||
installationId: body.installation?.id ?? null,
|
||||
|
|
@ -1578,7 +1588,6 @@ export const organizationAppActions = {
|
|||
displayName: input.displayName,
|
||||
slug,
|
||||
primaryDomain: existing?.primaryDomain ?? (input.kind === "personal" ? "personal" : `${slug}.github`),
|
||||
defaultModel: existing?.defaultModel ?? "claude-sonnet-4",
|
||||
autoImportRepos: existing?.autoImportRepos ?? 1,
|
||||
repoImportStatus: existing?.repoImportStatus ?? "not_started",
|
||||
githubConnectedAccount: input.githubLogin,
|
||||
|
|
|
|||
|
|
@ -10,24 +10,6 @@ const journal = {
|
|||
tag: "0000_melted_viper",
|
||||
breakpoints: true,
|
||||
},
|
||||
{
|
||||
idx: 1,
|
||||
when: 1773638400000,
|
||||
tag: "0001_auth_index_tables",
|
||||
breakpoints: true,
|
||||
},
|
||||
{
|
||||
idx: 2,
|
||||
when: 1773720000000,
|
||||
tag: "0002_task_summaries",
|
||||
breakpoints: true,
|
||||
},
|
||||
{
|
||||
idx: 3,
|
||||
when: 1773810001000,
|
||||
tag: "0003_drop_provider_profiles",
|
||||
breakpoints: true,
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
|
||||
|
|
@ -73,7 +55,7 @@ CREATE TABLE \`organization_members\` (
|
|||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE \`organization_profile\` (
|
||||
\`id\` text PRIMARY KEY NOT NULL,
|
||||
\`id\` integer PRIMARY KEY NOT NULL,
|
||||
\`kind\` text NOT NULL,
|
||||
\`github_account_id\` text NOT NULL,
|
||||
\`github_login\` text NOT NULL,
|
||||
|
|
@ -81,7 +63,6 @@ CREATE TABLE \`organization_profile\` (
|
|||
\`display_name\` text NOT NULL,
|
||||
\`slug\` text NOT NULL,
|
||||
\`primary_domain\` text NOT NULL,
|
||||
\`default_model\` text NOT NULL,
|
||||
\`auto_import_repos\` integer NOT NULL,
|
||||
\`repo_import_status\` text NOT NULL,
|
||||
\`github_connected_account\` text NOT NULL,
|
||||
|
|
@ -102,7 +83,8 @@ CREATE TABLE \`organization_profile\` (
|
|||
\`billing_renewal_at\` text,
|
||||
\`billing_payment_method_label\` text NOT NULL,
|
||||
\`created_at\` integer NOT NULL,
|
||||
\`updated_at\` integer NOT NULL
|
||||
\`updated_at\` integer NOT NULL,
|
||||
CONSTRAINT \`organization_profile_singleton_id_check\` CHECK(\`id\` = 1)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE \`repos\` (
|
||||
|
|
@ -122,56 +104,6 @@ CREATE TABLE \`stripe_lookup\` (
|
|||
\`organization_id\` text NOT NULL,
|
||||
\`updated_at\` integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE \`task_lookup\` (
|
||||
\`task_id\` text PRIMARY KEY NOT NULL,
|
||||
\`repo_id\` text NOT NULL
|
||||
);
|
||||
`,
|
||||
m0001: `CREATE TABLE IF NOT EXISTS \`auth_session_index\` (
|
||||
\`session_id\` text PRIMARY KEY NOT NULL,
|
||||
\`session_token\` text NOT NULL,
|
||||
\`user_id\` text NOT NULL,
|
||||
\`created_at\` integer NOT NULL,
|
||||
\`updated_at\` integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS \`auth_email_index\` (
|
||||
\`email\` text PRIMARY KEY NOT NULL,
|
||||
\`user_id\` text NOT NULL,
|
||||
\`updated_at\` integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS \`auth_account_index\` (
|
||||
\`id\` text PRIMARY KEY NOT NULL,
|
||||
\`provider_id\` text NOT NULL,
|
||||
\`account_id\` text NOT NULL,
|
||||
\`user_id\` text NOT NULL,
|
||||
\`updated_at\` integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS \`auth_verification\` (
|
||||
\`id\` text PRIMARY KEY NOT NULL,
|
||||
\`identifier\` text NOT NULL,
|
||||
\`value\` text NOT NULL,
|
||||
\`expires_at\` integer NOT NULL,
|
||||
\`created_at\` integer NOT NULL,
|
||||
\`updated_at\` integer NOT NULL
|
||||
);
|
||||
`,
|
||||
m0002: `CREATE TABLE IF NOT EXISTS \`task_summaries\` (
|
||||
\`task_id\` text PRIMARY KEY NOT NULL,
|
||||
\`repo_id\` text NOT NULL,
|
||||
\`title\` text NOT NULL,
|
||||
\`status\` text NOT NULL,
|
||||
\`repo_name\` text NOT NULL,
|
||||
\`updated_at_ms\` integer NOT NULL,
|
||||
\`branch\` text,
|
||||
\`pull_request_json\` text,
|
||||
\`sessions_summary_json\` text DEFAULT '[]' NOT NULL
|
||||
);
|
||||
`,
|
||||
m0003: `DROP TABLE IF EXISTS \`provider_profiles\`;
|
||||
`,
|
||||
} as const,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { integer, sqliteTable, text } from "rivetkit/db/drizzle";
|
||||
import { check, integer, sqliteTable, text } from "rivetkit/db/drizzle";
|
||||
import { sql } from "drizzle-orm";
|
||||
|
||||
// SQLite is per organization actor instance, so no organizationId column needed.
|
||||
|
||||
|
|
@ -14,66 +15,41 @@ export const repos = sqliteTable("repos", {
|
|||
updatedAt: integer("updated_at").notNull(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Coordinator index of TaskActor instances.
|
||||
* Fast taskId → repoId lookup so the organization can route requests
|
||||
* to the correct RepositoryActor without scanning all repos.
|
||||
*/
|
||||
export const taskLookup = sqliteTable("task_lookup", {
|
||||
taskId: text("task_id").notNull().primaryKey(),
|
||||
repoId: text("repo_id").notNull(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Coordinator index of TaskActor instances — materialized sidebar projection.
|
||||
* Task actors push summary updates to the organization actor via
|
||||
* applyTaskSummaryUpdate(). Source of truth lives on each TaskActor;
|
||||
* this table exists so organization reads stay local without fan-out.
|
||||
*/
|
||||
export const taskSummaries = sqliteTable("task_summaries", {
|
||||
taskId: text("task_id").notNull().primaryKey(),
|
||||
repoId: text("repo_id").notNull(),
|
||||
title: text("title").notNull(),
|
||||
status: text("status").notNull(),
|
||||
repoName: text("repo_name").notNull(),
|
||||
updatedAtMs: integer("updated_at_ms").notNull(),
|
||||
branch: text("branch"),
|
||||
pullRequestJson: text("pull_request_json"),
|
||||
sessionsSummaryJson: text("sessions_summary_json").notNull().default("[]"),
|
||||
});
|
||||
|
||||
export const organizationProfile = sqliteTable("organization_profile", {
|
||||
id: text("id").notNull().primaryKey(),
|
||||
kind: text("kind").notNull(),
|
||||
githubAccountId: text("github_account_id").notNull(),
|
||||
githubLogin: text("github_login").notNull(),
|
||||
githubAccountType: text("github_account_type").notNull(),
|
||||
displayName: text("display_name").notNull(),
|
||||
slug: text("slug").notNull(),
|
||||
primaryDomain: text("primary_domain").notNull(),
|
||||
defaultModel: text("default_model").notNull(),
|
||||
autoImportRepos: integer("auto_import_repos").notNull(),
|
||||
repoImportStatus: text("repo_import_status").notNull(),
|
||||
githubConnectedAccount: text("github_connected_account").notNull(),
|
||||
githubInstallationStatus: text("github_installation_status").notNull(),
|
||||
githubSyncStatus: text("github_sync_status").notNull(),
|
||||
githubInstallationId: integer("github_installation_id"),
|
||||
githubLastSyncLabel: text("github_last_sync_label").notNull(),
|
||||
githubLastSyncAt: integer("github_last_sync_at"),
|
||||
githubLastWebhookAt: integer("github_last_webhook_at"),
|
||||
githubLastWebhookEvent: text("github_last_webhook_event"),
|
||||
stripeCustomerId: text("stripe_customer_id"),
|
||||
stripeSubscriptionId: text("stripe_subscription_id"),
|
||||
stripePriceId: text("stripe_price_id"),
|
||||
billingPlanId: text("billing_plan_id").notNull(),
|
||||
billingStatus: text("billing_status").notNull(),
|
||||
billingSeatsIncluded: integer("billing_seats_included").notNull(),
|
||||
billingTrialEndsAt: text("billing_trial_ends_at"),
|
||||
billingRenewalAt: text("billing_renewal_at"),
|
||||
billingPaymentMethodLabel: text("billing_payment_method_label").notNull(),
|
||||
createdAt: integer("created_at").notNull(),
|
||||
updatedAt: integer("updated_at").notNull(),
|
||||
});
|
||||
export const organizationProfile = sqliteTable(
|
||||
"organization_profile",
|
||||
{
|
||||
id: integer("id").primaryKey(),
|
||||
kind: text("kind").notNull(),
|
||||
githubAccountId: text("github_account_id").notNull(),
|
||||
githubLogin: text("github_login").notNull(),
|
||||
githubAccountType: text("github_account_type").notNull(),
|
||||
displayName: text("display_name").notNull(),
|
||||
slug: text("slug").notNull(),
|
||||
primaryDomain: text("primary_domain").notNull(),
|
||||
autoImportRepos: integer("auto_import_repos").notNull(),
|
||||
repoImportStatus: text("repo_import_status").notNull(),
|
||||
githubConnectedAccount: text("github_connected_account").notNull(),
|
||||
githubInstallationStatus: text("github_installation_status").notNull(),
|
||||
githubSyncStatus: text("github_sync_status").notNull(),
|
||||
githubInstallationId: integer("github_installation_id"),
|
||||
githubLastSyncLabel: text("github_last_sync_label").notNull(),
|
||||
githubLastSyncAt: integer("github_last_sync_at"),
|
||||
githubLastWebhookAt: integer("github_last_webhook_at"),
|
||||
githubLastWebhookEvent: text("github_last_webhook_event"),
|
||||
stripeCustomerId: text("stripe_customer_id"),
|
||||
stripeSubscriptionId: text("stripe_subscription_id"),
|
||||
stripePriceId: text("stripe_price_id"),
|
||||
billingPlanId: text("billing_plan_id").notNull(),
|
||||
billingStatus: text("billing_status").notNull(),
|
||||
billingSeatsIncluded: integer("billing_seats_included").notNull(),
|
||||
billingTrialEndsAt: text("billing_trial_ends_at"),
|
||||
billingRenewalAt: text("billing_renewal_at"),
|
||||
billingPaymentMethodLabel: text("billing_payment_method_label").notNull(),
|
||||
createdAt: integer("created_at").notNull(),
|
||||
updatedAt: integer("updated_at").notNull(),
|
||||
},
|
||||
(table) => [check("organization_profile_singleton_id_check", sql`${table.id} = 1`)],
|
||||
);
|
||||
|
||||
export const organizationMembers = sqliteTable("organization_members", {
|
||||
id: text("id").notNull().primaryKey(),
|
||||
|
|
@ -133,6 +109,7 @@ export const authAccountIndex = sqliteTable("auth_account_index", {
|
|||
updatedAt: integer("updated_at").notNull(),
|
||||
});
|
||||
|
||||
/** Better Auth core model — schema defined at https://better-auth.com/docs/concepts/database */
|
||||
export const authVerification = sqliteTable("auth_verification", {
|
||||
id: text("id").notNull().primaryKey(),
|
||||
identifier: text("identifier").notNull(),
|
||||
|
|
|
|||
|
|
@ -2,12 +2,21 @@
|
|||
import { randomUUID } from "node:crypto";
|
||||
import { and, desc, eq, isNotNull, ne } from "drizzle-orm";
|
||||
import { Loop } from "rivetkit/workflow";
|
||||
import type { AgentType, RepoOverview, SandboxProviderId, TaskRecord, TaskSummary } from "@sandbox-agent/foundry-shared";
|
||||
import { getGithubData, getOrCreateHistory, getOrCreateTask, getTask, selfRepository } from "../handles.js";
|
||||
import type {
|
||||
AgentType,
|
||||
RepoOverview,
|
||||
SandboxProviderId,
|
||||
TaskRecord,
|
||||
TaskSummary,
|
||||
WorkspacePullRequestSummary,
|
||||
WorkspaceSessionSummary,
|
||||
WorkspaceTaskSummary,
|
||||
} from "@sandbox-agent/foundry-shared";
|
||||
import { getGithubData, getOrCreateAuditLog, getOrCreateOrganization, getOrCreateTask, getTask, selfRepository } from "../handles.js";
|
||||
import { deriveFallbackTitle, resolveCreateFlowDecision } from "../../services/create-flow.js";
|
||||
import { expectQueueResponse } from "../../services/queue.js";
|
||||
import { isActorNotFoundError, logActorWarning, resolveErrorMessage } from "../logging.js";
|
||||
import { repoMeta, taskIndex } from "./db/schema.js";
|
||||
import { repoMeta, taskIndex, tasks } from "./db/schema.js";
|
||||
|
||||
interface CreateTaskCommand {
|
||||
task: string;
|
||||
|
|
@ -29,10 +38,6 @@ interface ListTaskSummariesCommand {
|
|||
includeArchived?: boolean;
|
||||
}
|
||||
|
||||
interface GetTaskEnrichedCommand {
|
||||
taskId: string;
|
||||
}
|
||||
|
||||
interface GetPullRequestForBranchCommand {
|
||||
branchName: string;
|
||||
}
|
||||
|
|
@ -52,6 +57,61 @@ function isStaleTaskReferenceError(error: unknown): boolean {
|
|||
return isActorNotFoundError(error) || message.startsWith("Task not found:");
|
||||
}
|
||||
|
||||
function parseJsonValue<T>(value: string | null | undefined, fallback: T): T {
|
||||
if (!value) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(value) as T;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function taskSummaryRowFromSummary(taskSummary: WorkspaceTaskSummary) {
|
||||
return {
|
||||
taskId: taskSummary.id,
|
||||
title: taskSummary.title,
|
||||
status: taskSummary.status,
|
||||
repoName: taskSummary.repoName,
|
||||
updatedAtMs: taskSummary.updatedAtMs,
|
||||
branch: taskSummary.branch,
|
||||
pullRequestJson: JSON.stringify(taskSummary.pullRequest),
|
||||
sessionsSummaryJson: JSON.stringify(taskSummary.sessionsSummary),
|
||||
};
|
||||
}
|
||||
|
||||
function taskSummaryFromRow(c: any, row: any): WorkspaceTaskSummary {
|
||||
return {
|
||||
id: row.taskId,
|
||||
repoId: c.state.repoId,
|
||||
title: row.title,
|
||||
status: row.status,
|
||||
repoName: row.repoName,
|
||||
updatedAtMs: row.updatedAtMs,
|
||||
branch: row.branch ?? null,
|
||||
pullRequest: parseJsonValue<WorkspacePullRequestSummary | null>(row.pullRequestJson, null),
|
||||
sessionsSummary: parseJsonValue<WorkspaceSessionSummary[]>(row.sessionsSummaryJson, []),
|
||||
};
|
||||
}
|
||||
|
||||
async function upsertTaskSummary(c: any, taskSummary: WorkspaceTaskSummary): Promise<void> {
|
||||
await c.db
|
||||
.insert(tasks)
|
||||
.values(taskSummaryRowFromSummary(taskSummary))
|
||||
.onConflictDoUpdate({
|
||||
target: tasks.taskId,
|
||||
set: taskSummaryRowFromSummary(taskSummary),
|
||||
})
|
||||
.run();
|
||||
}
|
||||
|
||||
async function notifyOrganizationSnapshotChanged(c: any): Promise<void> {
|
||||
const organization = await getOrCreateOrganization(c, c.state.organizationId);
|
||||
await organization.refreshOrganizationSnapshot({});
|
||||
}
|
||||
|
||||
async function persistRemoteUrl(c: any, remoteUrl: string): Promise<void> {
|
||||
c.state.remoteUrl = remoteUrl;
|
||||
await c.db
|
||||
|
|
@ -104,6 +164,46 @@ async function listKnownTaskBranches(c: any): Promise<string[]> {
|
|||
return rows.map((row) => row.branchName).filter((value): value is string => typeof value === "string" && value.trim().length > 0);
|
||||
}
|
||||
|
||||
function parseJsonValue<T>(value: string | null | undefined, fallback: T): T {
|
||||
if (!value) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(value) as T;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function taskSummaryRowFromSummary(taskSummary: WorkspaceTaskSummary) {
|
||||
return {
|
||||
taskId: taskSummary.id,
|
||||
repoId: taskSummary.repoId,
|
||||
title: taskSummary.title,
|
||||
status: taskSummary.status,
|
||||
repoName: taskSummary.repoName,
|
||||
updatedAtMs: taskSummary.updatedAtMs,
|
||||
branch: taskSummary.branch,
|
||||
pullRequestJson: JSON.stringify(taskSummary.pullRequest),
|
||||
sessionsSummaryJson: JSON.stringify(taskSummary.sessionsSummary),
|
||||
};
|
||||
}
|
||||
|
||||
function workspaceTaskSummaryFromRow(row: any): WorkspaceTaskSummary {
|
||||
return {
|
||||
id: row.taskId,
|
||||
repoId: row.repoId,
|
||||
title: row.title,
|
||||
status: row.status,
|
||||
repoName: row.repoName,
|
||||
updatedAtMs: row.updatedAtMs,
|
||||
branch: row.branch ?? null,
|
||||
pullRequest: parseJsonValue(row.pullRequestJson, null),
|
||||
sessionsSummary: parseJsonValue<WorkspaceSessionSummary[]>(row.sessionsSummaryJson, []),
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveGitHubRepository(c: any) {
|
||||
const githubData = getGithubData(c, c.state.organizationId);
|
||||
return await githubData.getRepository({ repoId: c.state.repoId }).catch(() => null);
|
||||
|
|
@ -114,34 +214,6 @@ async function listGitHubBranches(c: any): Promise<Array<{ branchName: string; c
|
|||
return await githubData.listBranchesForRepository({ repoId: c.state.repoId }).catch(() => []);
|
||||
}
|
||||
|
||||
async function enrichTaskRecord(c: any, record: TaskRecord): Promise<TaskRecord> {
|
||||
const branchName = record.branchName?.trim() || null;
|
||||
if (!branchName) {
|
||||
return record;
|
||||
}
|
||||
|
||||
const pr =
|
||||
branchName != null
|
||||
? await getGithubData(c, c.state.organizationId)
|
||||
.listPullRequestsForRepository({ repoId: c.state.repoId })
|
||||
.then((rows: any[]) => rows.find((row) => row.headRefName === branchName) ?? null)
|
||||
.catch(() => null)
|
||||
: null;
|
||||
|
||||
return {
|
||||
...record,
|
||||
prUrl: pr?.url ?? null,
|
||||
prAuthor: pr?.authorLogin ?? null,
|
||||
ciStatus: null,
|
||||
reviewStatus: null,
|
||||
reviewer: pr?.authorLogin ?? null,
|
||||
diffStat: record.diffStat ?? null,
|
||||
hasUnpushed: record.hasUnpushed ?? null,
|
||||
conflictsWithMain: record.conflictsWithMain ?? null,
|
||||
parentBranch: record.parentBranch ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
async function createTaskMutation(c: any, cmd: CreateTaskCommand): Promise<TaskRecord> {
|
||||
const organizationId = c.state.organizationId;
|
||||
const repoId = c.state.repoId;
|
||||
|
|
@ -213,19 +285,60 @@ async function createTaskMutation(c: any, cmd: CreateTaskCommand): Promise<TaskR
|
|||
|
||||
const created = await taskHandle.initialize({ sandboxProviderId: cmd.sandboxProviderId });
|
||||
|
||||
const history = await getOrCreateHistory(c, organizationId, repoId);
|
||||
await history.append({
|
||||
kind: "task.created",
|
||||
taskId,
|
||||
payload: {
|
||||
try {
|
||||
await upsertTaskSummary(c, await taskHandle.getTaskSummary({}));
|
||||
await notifyOrganizationSnapshotChanged(c);
|
||||
} catch (error) {
|
||||
logActorWarning("repository", "failed seeding task summary after task creation", {
|
||||
organizationId,
|
||||
repoId,
|
||||
sandboxProviderId: cmd.sandboxProviderId,
|
||||
taskId,
|
||||
error: resolveErrorMessage(error),
|
||||
});
|
||||
}
|
||||
|
||||
const auditLog = await getOrCreateAuditLog(c, organizationId, repoId);
|
||||
await auditLog.send(
|
||||
"auditLog.command.append",
|
||||
{
|
||||
kind: "task.created",
|
||||
taskId,
|
||||
payload: {
|
||||
repoId,
|
||||
sandboxProviderId: cmd.sandboxProviderId,
|
||||
},
|
||||
},
|
||||
});
|
||||
{
|
||||
wait: false,
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
const taskSummary = await taskHandle.getTaskSummary({});
|
||||
await upsertTaskSummary(c, taskSummary);
|
||||
} catch (error) {
|
||||
logActorWarning("repository", "failed seeding repository task projection", {
|
||||
organizationId,
|
||||
repoId,
|
||||
taskId,
|
||||
error: resolveErrorMessage(error),
|
||||
});
|
||||
}
|
||||
|
||||
return created;
|
||||
}
|
||||
|
||||
async function upsertTaskSummary(c: any, taskSummary: WorkspaceTaskSummary): Promise<void> {
|
||||
await c.db
|
||||
.insert(tasks)
|
||||
.values(taskSummaryRowFromSummary(taskSummary))
|
||||
.onConflictDoUpdate({
|
||||
target: tasks.taskId,
|
||||
set: taskSummaryRowFromSummary(taskSummary),
|
||||
})
|
||||
.run();
|
||||
}
|
||||
|
||||
async function registerTaskBranchMutation(c: any, cmd: RegisterTaskBranchCommand): Promise<{ branchName: string; headSha: string }> {
|
||||
const branchName = cmd.branchName.trim();
|
||||
if (!branchName) {
|
||||
|
|
@ -289,40 +402,23 @@ async function registerTaskBranchMutation(c: any, cmd: RegisterTaskBranchCommand
|
|||
}
|
||||
|
||||
async function listTaskSummaries(c: any, includeArchived = false): Promise<TaskSummary[]> {
|
||||
const taskRows = await c.db.select({ taskId: taskIndex.taskId }).from(taskIndex).orderBy(desc(taskIndex.updatedAt)).all();
|
||||
const records: TaskSummary[] = [];
|
||||
const rows = await c.db.select().from(tasks).orderBy(desc(tasks.updatedAtMs)).all();
|
||||
return rows
|
||||
.map((row) => ({
|
||||
organizationId: c.state.organizationId,
|
||||
repoId: c.state.repoId,
|
||||
taskId: row.taskId,
|
||||
branchName: row.branch ?? null,
|
||||
title: row.title,
|
||||
status: row.status,
|
||||
updatedAt: row.updatedAtMs,
|
||||
}))
|
||||
.filter((row) => includeArchived || row.status !== "archived");
|
||||
}
|
||||
|
||||
for (const row of taskRows) {
|
||||
try {
|
||||
const record = await getTask(c, c.state.organizationId, c.state.repoId, row.taskId).get();
|
||||
if (!includeArchived && record.status === "archived") {
|
||||
continue;
|
||||
}
|
||||
records.push({
|
||||
organizationId: record.organizationId,
|
||||
repoId: record.repoId,
|
||||
taskId: record.taskId,
|
||||
branchName: record.branchName,
|
||||
title: record.title,
|
||||
status: record.status,
|
||||
updatedAt: record.updatedAt,
|
||||
});
|
||||
} catch (error) {
|
||||
if (isStaleTaskReferenceError(error)) {
|
||||
await deleteStaleTaskIndexRow(c, row.taskId);
|
||||
continue;
|
||||
}
|
||||
logActorWarning("repository", "failed loading task summary row", {
|
||||
organizationId: c.state.organizationId,
|
||||
repoId: c.state.repoId,
|
||||
taskId: row.taskId,
|
||||
error: resolveErrorMessage(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
records.sort((a, b) => b.updatedAt - a.updatedAt);
|
||||
return records;
|
||||
async function listWorkspaceTaskSummaries(c: any): Promise<WorkspaceTaskSummary[]> {
|
||||
const rows = await c.db.select().from(tasks).orderBy(desc(tasks.updatedAtMs)).all();
|
||||
return rows.map(workspaceTaskSummaryFromRow);
|
||||
}
|
||||
|
||||
function sortOverviewBranches(
|
||||
|
|
@ -415,38 +511,12 @@ export const repositoryActions = {
|
|||
return await listKnownTaskBranches(c);
|
||||
},
|
||||
|
||||
async registerTaskBranch(c: any, cmd: RegisterTaskBranchCommand): Promise<{ branchName: string; headSha: string }> {
|
||||
const self = selfRepository(c);
|
||||
return expectQueueResponse<{ branchName: string; headSha: string }>(
|
||||
await self.send(repositoryWorkflowQueueName("repository.command.registerTaskBranch"), cmd, {
|
||||
wait: true,
|
||||
timeout: 10_000,
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
||||
async listTaskSummaries(c: any, cmd?: ListTaskSummariesCommand): Promise<TaskSummary[]> {
|
||||
return await listTaskSummaries(c, cmd?.includeArchived === true);
|
||||
},
|
||||
|
||||
async getTaskEnriched(c: any, cmd: GetTaskEnrichedCommand): Promise<TaskRecord> {
|
||||
const row = await c.db.select({ taskId: taskIndex.taskId }).from(taskIndex).where(eq(taskIndex.taskId, cmd.taskId)).get();
|
||||
if (!row) {
|
||||
const record = await getTask(c, c.state.organizationId, c.state.repoId, cmd.taskId).get();
|
||||
await reinsertTaskIndexRow(c, cmd.taskId, record.branchName ?? null, record.updatedAt ?? Date.now());
|
||||
return await enrichTaskRecord(c, record);
|
||||
}
|
||||
|
||||
try {
|
||||
const record = await getTask(c, c.state.organizationId, c.state.repoId, cmd.taskId).get();
|
||||
return await enrichTaskRecord(c, record);
|
||||
} catch (error) {
|
||||
if (isStaleTaskReferenceError(error)) {
|
||||
await deleteStaleTaskIndexRow(c, cmd.taskId);
|
||||
throw new Error(`Unknown task in repo ${c.state.repoId}: ${cmd.taskId}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
async listWorkspaceTaskSummaries(c: any): Promise<WorkspaceTaskSummary[]> {
|
||||
return await listWorkspaceTaskSummaries(c);
|
||||
},
|
||||
|
||||
async getRepositoryMetadata(c: any): Promise<{ defaultBranch: string | null; fullName: string | null; remoteUrl: string }> {
|
||||
|
|
@ -468,34 +538,23 @@ export const repositoryActions = {
|
|||
const prRows = await githubData.listPullRequestsForRepository({ repoId: c.state.repoId }).catch(() => []);
|
||||
const prByBranch = new Map(prRows.map((row) => [row.headRefName, row]));
|
||||
|
||||
const taskRows = await c.db
|
||||
.select({
|
||||
taskId: taskIndex.taskId,
|
||||
branchName: taskIndex.branchName,
|
||||
updatedAt: taskIndex.updatedAt,
|
||||
})
|
||||
.from(taskIndex)
|
||||
.all();
|
||||
const taskRows = await c.db.select().from(tasks).all();
|
||||
|
||||
const taskMetaByBranch = new Map<string, { taskId: string; title: string | null; status: TaskRecord["status"] | null; updatedAt: number }>();
|
||||
const taskMetaByBranch = new Map<
|
||||
string,
|
||||
{ taskId: string; title: string | null; status: TaskRecord["status"] | null; updatedAt: number; pullRequest: WorkspacePullRequestSummary | null }
|
||||
>();
|
||||
for (const row of taskRows) {
|
||||
if (!row.branchName) {
|
||||
if (!row.branch) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const record = await getTask(c, c.state.organizationId, c.state.repoId, row.taskId).get();
|
||||
taskMetaByBranch.set(row.branchName, {
|
||||
taskId: row.taskId,
|
||||
title: record.title ?? null,
|
||||
status: record.status,
|
||||
updatedAt: record.updatedAt,
|
||||
});
|
||||
} catch (error) {
|
||||
if (isStaleTaskReferenceError(error)) {
|
||||
await deleteStaleTaskIndexRow(c, row.taskId);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
taskMetaByBranch.set(row.branch, {
|
||||
taskId: row.taskId,
|
||||
title: row.title ?? null,
|
||||
status: row.status,
|
||||
updatedAt: row.updatedAtMs,
|
||||
pullRequest: parseJsonValue<WorkspacePullRequestSummary | null>(row.pullRequestJson, null),
|
||||
});
|
||||
}
|
||||
|
||||
const branchMap = new Map<string, { branchName: string; commitSha: string }>();
|
||||
|
|
@ -514,7 +573,7 @@ export const repositoryActions = {
|
|||
const branches = sortOverviewBranches(
|
||||
[...branchMap.values()].map((branch) => {
|
||||
const taskMeta = taskMetaByBranch.get(branch.branchName);
|
||||
const pr = prByBranch.get(branch.branchName);
|
||||
const pr = taskMeta?.pullRequest ?? prByBranch.get(branch.branchName) ?? null;
|
||||
return {
|
||||
branchName: branch.branchName,
|
||||
commitSha: branch.commitSha,
|
||||
|
|
@ -522,10 +581,10 @@ export const repositoryActions = {
|
|||
taskTitle: taskMeta?.title ?? null,
|
||||
taskStatus: taskMeta?.status ?? null,
|
||||
prNumber: pr?.number ?? null,
|
||||
prState: pr?.state ?? null,
|
||||
prUrl: pr?.url ?? null,
|
||||
prState: "state" in (pr ?? {}) ? pr.state : null,
|
||||
prUrl: "url" in (pr ?? {}) ? pr.url : null,
|
||||
ciStatus: null,
|
||||
reviewStatus: null,
|
||||
reviewStatus: pr && "isDraft" in pr ? (pr.isDraft ? "draft" : "ready") : null,
|
||||
reviewer: pr?.authorLogin ?? null,
|
||||
updatedAt: Math.max(taskMeta?.updatedAt ?? 0, pr?.updatedAtMs ?? 0, now),
|
||||
};
|
||||
|
|
@ -543,15 +602,51 @@ export const repositoryActions = {
|
|||
};
|
||||
},
|
||||
|
||||
async getPullRequestForBranch(c: any, cmd: GetPullRequestForBranchCommand): Promise<{ number: number; status: "draft" | "ready" } | null> {
|
||||
async applyTaskSummaryUpdate(c: any, input: { taskSummary: WorkspaceTaskSummary }): Promise<void> {
|
||||
await upsertTaskSummary(c, input.taskSummary);
|
||||
await notifyOrganizationSnapshotChanged(c);
|
||||
},
|
||||
|
||||
async removeTaskSummary(c: any, input: { taskId: string }): Promise<void> {
|
||||
await c.db.delete(tasks).where(eq(tasks.taskId, input.taskId)).run();
|
||||
await notifyOrganizationSnapshotChanged(c);
|
||||
},
|
||||
|
||||
async findTaskForGithubBranch(c: any, input: { branchName: string }): Promise<{ taskId: string | null }> {
|
||||
const row = await c.db.select({ taskId: tasks.taskId }).from(tasks).where(eq(tasks.branch, input.branchName)).get();
|
||||
return { taskId: row?.taskId ?? null };
|
||||
},
|
||||
|
||||
async refreshTaskSummaryForGithubBranch(c: any, input: { branchName: string }): Promise<void> {
|
||||
const rows = await c.db.select({ taskId: tasks.taskId }).from(tasks).where(eq(tasks.branch, input.branchName)).all();
|
||||
|
||||
for (const row of rows) {
|
||||
try {
|
||||
const task = getTask(c, c.state.organizationId, c.state.repoId, row.taskId);
|
||||
await upsertTaskSummary(c, await task.getTaskSummary({}));
|
||||
} catch (error) {
|
||||
logActorWarning("repository", "failed refreshing task summary for branch", {
|
||||
organizationId: c.state.organizationId,
|
||||
repoId: c.state.repoId,
|
||||
branchName: input.branchName,
|
||||
taskId: row.taskId,
|
||||
error: resolveErrorMessage(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await notifyOrganizationSnapshotChanged(c);
|
||||
},
|
||||
|
||||
async getPullRequestForBranch(c: any, cmd: GetPullRequestForBranchCommand): Promise<WorkspacePullRequestSummary | null> {
|
||||
const branchName = cmd.branchName?.trim();
|
||||
if (!branchName) {
|
||||
return null;
|
||||
}
|
||||
const githubData = getGithubData(c, c.state.organizationId);
|
||||
return await githubData.getPullRequestForBranch({
|
||||
const rows = await githubData.listPullRequestsForRepository({
|
||||
repoId: c.state.repoId,
|
||||
branchName,
|
||||
});
|
||||
return rows.find((candidate: WorkspacePullRequestSummary) => candidate.headRefName === branchName) ?? null;
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -10,12 +10,6 @@ const journal = {
|
|||
tag: "0000_useful_la_nuit",
|
||||
breakpoints: true,
|
||||
},
|
||||
{
|
||||
idx: 1,
|
||||
when: 1778900000000,
|
||||
tag: "0001_remove_local_git_state",
|
||||
breakpoints: true,
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
|
||||
|
|
@ -23,21 +17,30 @@ export default {
|
|||
journal,
|
||||
migrations: {
|
||||
m0000: `CREATE TABLE \`repo_meta\` (
|
||||
\t\`id\` integer PRIMARY KEY NOT NULL,
|
||||
\t\`remote_url\` text NOT NULL,
|
||||
\t\`updated_at\` integer NOT NULL
|
||||
\`id\` integer PRIMARY KEY NOT NULL,
|
||||
\`remote_url\` text NOT NULL,
|
||||
\`updated_at\` integer NOT NULL,
|
||||
CONSTRAINT \`repo_meta_singleton_id_check\` CHECK(\`id\` = 1)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE \`task_index\` (
|
||||
\t\`task_id\` text PRIMARY KEY NOT NULL,
|
||||
\t\`branch_name\` text,
|
||||
\t\`created_at\` integer NOT NULL,
|
||||
\t\`updated_at\` integer NOT NULL
|
||||
\`task_id\` text PRIMARY KEY NOT NULL,
|
||||
\`branch_name\` text,
|
||||
\`created_at\` integer NOT NULL,
|
||||
\`updated_at\` integer NOT NULL
|
||||
);
|
||||
`,
|
||||
m0001: `DROP TABLE IF EXISTS \`branches\`;
|
||||
--> statement-breakpoint
|
||||
DROP TABLE IF EXISTS \`repo_action_jobs\`;
|
||||
CREATE TABLE \`tasks\` (
|
||||
\`task_id\` text PRIMARY KEY NOT NULL,
|
||||
\`repo_id\` text NOT NULL,
|
||||
\`title\` text NOT NULL,
|
||||
\`status\` text NOT NULL,
|
||||
\`repo_name\` text NOT NULL,
|
||||
\`updated_at_ms\` integer NOT NULL,
|
||||
\`branch\` text,
|
||||
\`pull_request_json\` text,
|
||||
\`sessions_summary_json\` text DEFAULT '[]' NOT NULL
|
||||
);
|
||||
`,
|
||||
} as const,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,19 +1,23 @@
|
|||
import { integer, sqliteTable, text } from "rivetkit/db/drizzle";
|
||||
import { check, integer, sqliteTable, text } from "rivetkit/db/drizzle";
|
||||
import { sql } from "drizzle-orm";
|
||||
|
||||
// SQLite is per repository actor instance (organizationId+repoId).
|
||||
|
||||
export const repoMeta = sqliteTable("repo_meta", {
|
||||
id: integer("id").primaryKey(),
|
||||
remoteUrl: text("remote_url").notNull(),
|
||||
updatedAt: integer("updated_at").notNull(),
|
||||
});
|
||||
export const repoMeta = sqliteTable(
|
||||
"repo_meta",
|
||||
{
|
||||
id: integer("id").primaryKey(),
|
||||
remoteUrl: text("remote_url").notNull(),
|
||||
updatedAt: integer("updated_at").notNull(),
|
||||
},
|
||||
(table) => [check("repo_meta_singleton_id_check", sql`${table.id} = 1`)],
|
||||
);
|
||||
|
||||
/**
|
||||
* Coordinator index of TaskActor instances.
|
||||
* The repository actor is the coordinator for tasks. Each row maps a
|
||||
* taskId to its branch name. Used for branch conflict checking and
|
||||
* task-by-branch lookups. Rows are inserted at task creation and
|
||||
* updated on branch rename.
|
||||
* taskId to its immutable branch name. Used for branch conflict checking
|
||||
* and task-by-branch lookups. Rows are inserted at task creation.
|
||||
*/
|
||||
export const taskIndex = sqliteTable("task_index", {
|
||||
taskId: text("task_id").notNull().primaryKey(),
|
||||
|
|
@ -21,3 +25,35 @@ export const taskIndex = sqliteTable("task_index", {
|
|||
createdAt: integer("created_at").notNull(),
|
||||
updatedAt: integer("updated_at").notNull(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Repository-owned materialized task summary projection.
|
||||
* Task actors push summary updates to their direct repository coordinator,
|
||||
* which keeps this table local for fast list/lookups without fan-out.
|
||||
*/
|
||||
export const tasks = sqliteTable("tasks", {
|
||||
taskId: text("task_id").notNull().primaryKey(),
|
||||
title: text("title").notNull(),
|
||||
status: text("status").notNull(),
|
||||
repoName: text("repo_name").notNull(),
|
||||
updatedAtMs: integer("updated_at_ms").notNull(),
|
||||
branch: text("branch"),
|
||||
pullRequestJson: text("pull_request_json"),
|
||||
sessionsSummaryJson: text("sessions_summary_json").notNull().default("[]"),
|
||||
});
|
||||
|
||||
/**
|
||||
* Materialized task summary projection owned by the repository coordinator.
|
||||
* Task actors push updates here; organization reads fan in through repositories.
|
||||
*/
|
||||
export const tasks = sqliteTable("tasks", {
|
||||
taskId: text("task_id").notNull().primaryKey(),
|
||||
repoId: text("repo_id").notNull(),
|
||||
title: text("title").notNull(),
|
||||
status: text("status").notNull(),
|
||||
repoName: text("repo_name").notNull(),
|
||||
updatedAtMs: integer("updated_at_ms").notNull(),
|
||||
branch: text("branch"),
|
||||
pullRequestJson: text("pull_request_json"),
|
||||
sessionsSummaryJson: text("sessions_summary_json").notNull().default("[]"),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ CREATE TABLE `task` (
|
|||
`branch_name` text,
|
||||
`title` text,
|
||||
`task` text NOT NULL,
|
||||
`provider_id` text NOT NULL,
|
||||
`sandbox_provider_id` text NOT NULL,
|
||||
`status` text NOT NULL,
|
||||
`agent_type` text DEFAULT 'claude',
|
||||
`pr_submitted` integer DEFAULT 0,
|
||||
|
|
@ -19,13 +19,17 @@ CREATE TABLE `task_runtime` (
|
|||
`active_switch_target` text,
|
||||
`active_cwd` text,
|
||||
`status_message` text,
|
||||
`git_state_json` text,
|
||||
`git_state_updated_at` integer,
|
||||
`provision_stage` text,
|
||||
`provision_stage_updated_at` integer,
|
||||
`updated_at` integer NOT NULL,
|
||||
CONSTRAINT "task_runtime_singleton_id_check" CHECK("task_runtime"."id" = 1)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `task_sandboxes` (
|
||||
`sandbox_id` text PRIMARY KEY NOT NULL,
|
||||
`provider_id` text NOT NULL,
|
||||
`sandbox_provider_id` text NOT NULL,
|
||||
`sandbox_actor_id` text,
|
||||
`switch_target` text NOT NULL,
|
||||
`cwd` text,
|
||||
|
|
@ -34,10 +38,15 @@ CREATE TABLE `task_sandboxes` (
|
|||
`updated_at` integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `task_workbench_sessions` (
|
||||
CREATE TABLE `task_workspace_sessions` (
|
||||
`session_id` text PRIMARY KEY NOT NULL,
|
||||
`sandbox_session_id` text,
|
||||
`session_name` text NOT NULL,
|
||||
`model` text NOT NULL,
|
||||
`status` text DEFAULT 'ready' NOT NULL,
|
||||
`error_message` text,
|
||||
`transcript_json` text DEFAULT '[]' NOT NULL,
|
||||
`transcript_updated_at` integer,
|
||||
`unread` integer DEFAULT 0 NOT NULL,
|
||||
`draft_text` text DEFAULT '' NOT NULL,
|
||||
`draft_attachments_json` text DEFAULT '[]' NOT NULL,
|
||||
|
|
|
|||
|
|
@ -221,8 +221,8 @@
|
|||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"task_workbench_sessions": {
|
||||
"name": "task_workbench_sessions",
|
||||
"task_workspace_sessions": {
|
||||
"name": "task_workspace_sessions",
|
||||
"columns": {
|
||||
"session_id": {
|
||||
"name": "session_id",
|
||||
|
|
|
|||
|
|
@ -10,12 +10,6 @@ const journal = {
|
|||
tag: "0000_charming_maestro",
|
||||
breakpoints: true,
|
||||
},
|
||||
{
|
||||
idx: 1,
|
||||
when: 1773810000000,
|
||||
tag: "0001_sandbox_provider_columns",
|
||||
breakpoints: true,
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
|
||||
|
|
@ -27,10 +21,8 @@ export default {
|
|||
\`branch_name\` text,
|
||||
\`title\` text,
|
||||
\`task\` text NOT NULL,
|
||||
\`provider_id\` text NOT NULL,
|
||||
\`sandbox_provider_id\` text NOT NULL,
|
||||
\`status\` text NOT NULL,
|
||||
\`agent_type\` text DEFAULT 'claude',
|
||||
\`pr_submitted\` integer DEFAULT 0,
|
||||
\`created_at\` integer NOT NULL,
|
||||
\`updated_at\` integer NOT NULL,
|
||||
CONSTRAINT "task_singleton_id_check" CHECK("task"."id" = 1)
|
||||
|
|
@ -39,17 +31,17 @@ export default {
|
|||
CREATE TABLE \`task_runtime\` (
|
||||
\`id\` integer PRIMARY KEY NOT NULL,
|
||||
\`active_sandbox_id\` text,
|
||||
\`active_session_id\` text,
|
||||
\`active_switch_target\` text,
|
||||
\`active_cwd\` text,
|
||||
\`status_message\` text,
|
||||
\`git_state_json\` text,
|
||||
\`git_state_updated_at\` integer,
|
||||
\`updated_at\` integer NOT NULL,
|
||||
CONSTRAINT "task_runtime_singleton_id_check" CHECK("task_runtime"."id" = 1)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE \`task_sandboxes\` (
|
||||
\`sandbox_id\` text PRIMARY KEY NOT NULL,
|
||||
\`provider_id\` text NOT NULL,
|
||||
\`sandbox_provider_id\` text NOT NULL,
|
||||
\`sandbox_actor_id\` text,
|
||||
\`switch_target\` text NOT NULL,
|
||||
\`cwd\` text,
|
||||
|
|
@ -58,24 +50,21 @@ CREATE TABLE \`task_sandboxes\` (
|
|||
\`updated_at\` integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE \`task_workbench_sessions\` (
|
||||
CREATE TABLE \`task_workspace_sessions\` (
|
||||
\`session_id\` text PRIMARY KEY NOT NULL,
|
||||
\`sandbox_session_id\` text,
|
||||
\`session_name\` text NOT NULL,
|
||||
\`model\` text NOT NULL,
|
||||
\`unread\` integer DEFAULT 0 NOT NULL,
|
||||
\`draft_text\` text DEFAULT '' NOT NULL,
|
||||
\`draft_attachments_json\` text DEFAULT '[]' NOT NULL,
|
||||
\`draft_updated_at\` integer,
|
||||
\`status\` text DEFAULT 'ready' NOT NULL,
|
||||
\`error_message\` text,
|
||||
\`transcript_json\` text DEFAULT '[]' NOT NULL,
|
||||
\`transcript_updated_at\` integer,
|
||||
\`created\` integer DEFAULT 1 NOT NULL,
|
||||
\`closed\` integer DEFAULT 0 NOT NULL,
|
||||
\`thinking_since_ms\` integer,
|
||||
\`created_at\` integer NOT NULL,
|
||||
\`created_at\` integer NOT NULL,
|
||||
\`updated_at\` integer NOT NULL
|
||||
);
|
||||
`,
|
||||
m0001: `ALTER TABLE \`task\` RENAME COLUMN \`provider_id\` TO \`sandbox_provider_id\`;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE \`task_sandboxes\` RENAME COLUMN \`provider_id\` TO \`sandbox_provider_id\`;
|
||||
`,
|
||||
} as const,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,8 +11,6 @@ export const task = sqliteTable(
|
|||
task: text("task").notNull(),
|
||||
sandboxProviderId: text("sandbox_provider_id").notNull(),
|
||||
status: text("status").notNull(),
|
||||
agentType: text("agent_type").default("claude"),
|
||||
prSubmitted: integer("pr_submitted").default(0),
|
||||
createdAt: integer("created_at").notNull(),
|
||||
updatedAt: integer("updated_at").notNull(),
|
||||
},
|
||||
|
|
@ -24,14 +22,10 @@ export const taskRuntime = sqliteTable(
|
|||
{
|
||||
id: integer("id").primaryKey(),
|
||||
activeSandboxId: text("active_sandbox_id"),
|
||||
activeSessionId: text("active_session_id"),
|
||||
activeSwitchTarget: text("active_switch_target"),
|
||||
activeCwd: text("active_cwd"),
|
||||
statusMessage: text("status_message"),
|
||||
gitStateJson: text("git_state_json"),
|
||||
gitStateUpdatedAt: integer("git_state_updated_at"),
|
||||
provisionStage: text("provision_stage"),
|
||||
provisionStageUpdatedAt: integer("provision_stage_updated_at"),
|
||||
updatedAt: integer("updated_at").notNull(),
|
||||
},
|
||||
(table) => [check("task_runtime_singleton_id_check", sql`${table.id} = 1`)],
|
||||
|
|
@ -54,12 +48,12 @@ export const taskSandboxes = sqliteTable("task_sandboxes", {
|
|||
});
|
||||
|
||||
/**
|
||||
* Coordinator index of workbench sessions within this task.
|
||||
* Coordinator index of workspace sessions within this task.
|
||||
* The task actor is the coordinator for sessions. Each row holds session
|
||||
* metadata, model, status, transcript, and draft state. Sessions are
|
||||
* sub-entities of the task — no separate session actor in the DB.
|
||||
*/
|
||||
export const taskWorkbenchSessions = sqliteTable("task_workbench_sessions", {
|
||||
export const taskWorkspaceSessions = sqliteTable("task_workspace_sessions", {
|
||||
sessionId: text("session_id").notNull().primaryKey(),
|
||||
sandboxSessionId: text("sandbox_session_id"),
|
||||
sessionName: text("session_name").notNull(),
|
||||
|
|
@ -68,11 +62,6 @@ export const taskWorkbenchSessions = sqliteTable("task_workbench_sessions", {
|
|||
errorMessage: text("error_message"),
|
||||
transcriptJson: text("transcript_json").notNull().default("[]"),
|
||||
transcriptUpdatedAt: integer("transcript_updated_at"),
|
||||
unread: integer("unread").notNull().default(0),
|
||||
draftText: text("draft_text").notNull().default(""),
|
||||
// Structured by the workbench composer attachment payload format.
|
||||
draftAttachmentsJson: text("draft_attachments_json").notNull().default("[]"),
|
||||
draftUpdatedAt: integer("draft_updated_at"),
|
||||
created: integer("created").notNull().default(1),
|
||||
closed: integer("closed").notNull().default(0),
|
||||
thinkingSinceMs: integer("thinking_since_ms"),
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
import { actor, queue } from "rivetkit";
|
||||
import { workflow } from "rivetkit/workflow";
|
||||
import type {
|
||||
AgentType,
|
||||
TaskRecord,
|
||||
TaskWorkbenchChangeModelInput,
|
||||
TaskWorkbenchRenameInput,
|
||||
TaskWorkbenchRenameSessionInput,
|
||||
TaskWorkbenchSetSessionUnreadInput,
|
||||
TaskWorkbenchSendMessageInput,
|
||||
TaskWorkbenchUpdateDraftInput,
|
||||
TaskWorkspaceChangeModelInput,
|
||||
TaskWorkspaceRenameInput,
|
||||
TaskWorkspaceRenameSessionInput,
|
||||
TaskWorkspaceSetSessionUnreadInput,
|
||||
TaskWorkspaceSendMessageInput,
|
||||
TaskWorkspaceUpdateDraftInput,
|
||||
SandboxProviderId,
|
||||
} from "@sandbox-agent/foundry-shared";
|
||||
import { expectQueueResponse } from "../../services/queue.js";
|
||||
|
|
@ -16,24 +15,23 @@ import { selfTask } from "../handles.js";
|
|||
import { taskDb } from "./db/db.js";
|
||||
import { getCurrentRecord } from "./workflow/common.js";
|
||||
import {
|
||||
changeWorkbenchModel,
|
||||
closeWorkbenchSession,
|
||||
createWorkbenchSession,
|
||||
changeWorkspaceModel,
|
||||
closeWorkspaceSession,
|
||||
createWorkspaceSession,
|
||||
getSessionDetail,
|
||||
getTaskDetail,
|
||||
getTaskSummary,
|
||||
markWorkbenchUnread,
|
||||
publishWorkbenchPr,
|
||||
renameWorkbenchBranch,
|
||||
renameWorkbenchTask,
|
||||
renameWorkbenchSession,
|
||||
revertWorkbenchFile,
|
||||
sendWorkbenchMessage,
|
||||
syncWorkbenchSessionStatus,
|
||||
setWorkbenchSessionUnread,
|
||||
stopWorkbenchSession,
|
||||
updateWorkbenchDraft,
|
||||
} from "./workbench.js";
|
||||
markWorkspaceUnread,
|
||||
publishWorkspacePr,
|
||||
renameWorkspaceTask,
|
||||
renameWorkspaceSession,
|
||||
revertWorkspaceFile,
|
||||
sendWorkspaceMessage,
|
||||
syncWorkspaceSessionStatus,
|
||||
setWorkspaceSessionUnread,
|
||||
stopWorkspaceSession,
|
||||
updateWorkspaceDraft,
|
||||
} from "./workspace.js";
|
||||
import { TASK_QUEUE_NAMES, taskWorkflowQueueName, runTaskWorkflow } from "./workflow/index.js";
|
||||
|
||||
export interface TaskInput {
|
||||
|
|
@ -45,10 +43,8 @@ export interface TaskInput {
|
|||
title: string | null;
|
||||
task: string;
|
||||
sandboxProviderId: SandboxProviderId;
|
||||
agentType: AgentType | null;
|
||||
explicitTitle: string | null;
|
||||
explicitBranchName: string | null;
|
||||
initialPrompt: string | null;
|
||||
}
|
||||
|
||||
interface InitializeCommand {
|
||||
|
|
@ -69,48 +65,57 @@ interface TaskStatusSyncCommand {
|
|||
at: number;
|
||||
}
|
||||
|
||||
interface TaskWorkbenchValueCommand {
|
||||
interface TaskWorkspaceValueCommand {
|
||||
value: string;
|
||||
authSessionId?: string;
|
||||
}
|
||||
|
||||
interface TaskWorkbenchSessionTitleCommand {
|
||||
interface TaskWorkspaceSessionTitleCommand {
|
||||
sessionId: string;
|
||||
title: string;
|
||||
authSessionId?: string;
|
||||
}
|
||||
|
||||
interface TaskWorkbenchSessionUnreadCommand {
|
||||
interface TaskWorkspaceSessionUnreadCommand {
|
||||
sessionId: string;
|
||||
unread: boolean;
|
||||
authSessionId?: string;
|
||||
}
|
||||
|
||||
interface TaskWorkbenchUpdateDraftCommand {
|
||||
interface TaskWorkspaceUpdateDraftCommand {
|
||||
sessionId: string;
|
||||
text: string;
|
||||
attachments: Array<any>;
|
||||
authSessionId?: string;
|
||||
}
|
||||
|
||||
interface TaskWorkbenchChangeModelCommand {
|
||||
interface TaskWorkspaceChangeModelCommand {
|
||||
sessionId: string;
|
||||
model: string;
|
||||
authSessionId?: string;
|
||||
}
|
||||
|
||||
interface TaskWorkbenchSendMessageCommand {
|
||||
interface TaskWorkspaceSendMessageCommand {
|
||||
sessionId: string;
|
||||
text: string;
|
||||
attachments: Array<any>;
|
||||
authSessionId?: string;
|
||||
}
|
||||
|
||||
interface TaskWorkbenchCreateSessionCommand {
|
||||
interface TaskWorkspaceCreateSessionCommand {
|
||||
model?: string;
|
||||
authSessionId?: string;
|
||||
}
|
||||
|
||||
interface TaskWorkbenchCreateSessionAndSendCommand {
|
||||
interface TaskWorkspaceCreateSessionAndSendCommand {
|
||||
model?: string;
|
||||
text: string;
|
||||
authSessionId?: string;
|
||||
}
|
||||
|
||||
interface TaskWorkbenchSessionCommand {
|
||||
interface TaskWorkspaceSessionCommand {
|
||||
sessionId: string;
|
||||
authSessionId?: string;
|
||||
}
|
||||
|
||||
export const task = actor({
|
||||
|
|
@ -126,16 +131,6 @@ export const task = actor({
|
|||
repoId: input.repoId,
|
||||
taskId: input.taskId,
|
||||
repoRemote: input.repoRemote,
|
||||
branchName: input.branchName,
|
||||
title: input.title,
|
||||
task: input.task,
|
||||
sandboxProviderId: input.sandboxProviderId,
|
||||
agentType: input.agentType,
|
||||
explicitTitle: input.explicitTitle,
|
||||
explicitBranchName: input.explicitBranchName,
|
||||
initialPrompt: input.initialPrompt,
|
||||
initialized: false,
|
||||
previousStatus: null as string | null,
|
||||
}),
|
||||
actions: {
|
||||
async initialize(c, cmd: InitializeCommand): Promise<TaskRecord> {
|
||||
|
|
@ -220,19 +215,19 @@ export const task = actor({
|
|||
return await getTaskSummary(c);
|
||||
},
|
||||
|
||||
async getTaskDetail(c) {
|
||||
return await getTaskDetail(c);
|
||||
async getTaskDetail(c, input?: { authSessionId?: string }) {
|
||||
return await getTaskDetail(c, input?.authSessionId);
|
||||
},
|
||||
|
||||
async getSessionDetail(c, input: { sessionId: string }) {
|
||||
return await getSessionDetail(c, input.sessionId);
|
||||
async getSessionDetail(c, input: { sessionId: string; authSessionId?: string }) {
|
||||
return await getSessionDetail(c, input.sessionId, input.authSessionId);
|
||||
},
|
||||
|
||||
async markWorkbenchUnread(c): Promise<void> {
|
||||
async markWorkspaceUnread(c, input?: { authSessionId?: string }): Promise<void> {
|
||||
const self = selfTask(c);
|
||||
await self.send(
|
||||
taskWorkflowQueueName("task.command.workbench.mark_unread"),
|
||||
{},
|
||||
taskWorkflowQueueName("task.command.workspace.mark_unread"),
|
||||
{ authSessionId: input?.authSessionId },
|
||||
{
|
||||
wait: true,
|
||||
timeout: 10_000,
|
||||
|
|
@ -240,26 +235,26 @@ export const task = actor({
|
|||
);
|
||||
},
|
||||
|
||||
async renameWorkbenchTask(c, input: TaskWorkbenchRenameInput): Promise<void> {
|
||||
async renameWorkspaceTask(c, input: TaskWorkspaceRenameInput): Promise<void> {
|
||||
const self = selfTask(c);
|
||||
await self.send(taskWorkflowQueueName("task.command.workbench.rename_task"), { value: input.value } satisfies TaskWorkbenchValueCommand, {
|
||||
wait: true,
|
||||
timeout: 20_000,
|
||||
});
|
||||
await self.send(
|
||||
taskWorkflowQueueName("task.command.workspace.rename_task"),
|
||||
{ value: input.value, authSessionId: input.authSessionId } satisfies TaskWorkspaceValueCommand,
|
||||
{
|
||||
wait: true,
|
||||
timeout: 20_000,
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
async renameWorkbenchBranch(c, input: TaskWorkbenchRenameInput): Promise<void> {
|
||||
const self = selfTask(c);
|
||||
await self.send(taskWorkflowQueueName("task.command.workbench.rename_branch"), { value: input.value } satisfies TaskWorkbenchValueCommand, {
|
||||
wait: false,
|
||||
});
|
||||
},
|
||||
|
||||
async createWorkbenchSession(c, input?: { model?: string }): Promise<{ sessionId: string }> {
|
||||
async createWorkspaceSession(c, input?: { model?: string; authSessionId?: string }): Promise<{ sessionId: string }> {
|
||||
const self = selfTask(c);
|
||||
const result = await self.send(
|
||||
taskWorkflowQueueName("task.command.workbench.create_session"),
|
||||
{ ...(input?.model ? { model: input.model } : {}) } satisfies TaskWorkbenchCreateSessionCommand,
|
||||
taskWorkflowQueueName("task.command.workspace.create_session"),
|
||||
{
|
||||
...(input?.model ? { model: input.model } : {}),
|
||||
...(input?.authSessionId ? { authSessionId: input.authSessionId } : {}),
|
||||
} satisfies TaskWorkspaceCreateSessionCommand,
|
||||
{
|
||||
wait: true,
|
||||
timeout: 10_000,
|
||||
|
|
@ -269,23 +264,23 @@ export const task = actor({
|
|||
},
|
||||
|
||||
/**
|
||||
* Fire-and-forget: creates a workbench session and sends the initial message.
|
||||
* Used by createWorkbenchTask so the caller doesn't block on session creation.
|
||||
* Fire-and-forget: creates a session and sends the initial message.
|
||||
* Used by createWorkspaceTask so the caller doesn't block on session creation.
|
||||
*/
|
||||
async createWorkbenchSessionAndSend(c, input: { model?: string; text: string }): Promise<void> {
|
||||
async createWorkspaceSessionAndSend(c, input: { model?: string; text: string; authSessionId?: string }): Promise<void> {
|
||||
const self = selfTask(c);
|
||||
await self.send(
|
||||
taskWorkflowQueueName("task.command.workbench.create_session_and_send"),
|
||||
{ model: input.model, text: input.text } satisfies TaskWorkbenchCreateSessionAndSendCommand,
|
||||
taskWorkflowQueueName("task.command.workspace.create_session_and_send"),
|
||||
{ model: input.model, text: input.text, authSessionId: input.authSessionId } satisfies TaskWorkspaceCreateSessionAndSendCommand,
|
||||
{ wait: false },
|
||||
);
|
||||
},
|
||||
|
||||
async renameWorkbenchSession(c, input: TaskWorkbenchRenameSessionInput): Promise<void> {
|
||||
async renameWorkspaceSession(c, input: TaskWorkspaceRenameSessionInput): Promise<void> {
|
||||
const self = selfTask(c);
|
||||
await self.send(
|
||||
taskWorkflowQueueName("task.command.workbench.rename_session"),
|
||||
{ sessionId: input.sessionId, title: input.title } satisfies TaskWorkbenchSessionTitleCommand,
|
||||
taskWorkflowQueueName("task.command.workspace.rename_session"),
|
||||
{ sessionId: input.sessionId, title: input.title, authSessionId: input.authSessionId } satisfies TaskWorkspaceSessionTitleCommand,
|
||||
{
|
||||
wait: true,
|
||||
timeout: 10_000,
|
||||
|
|
@ -293,11 +288,11 @@ export const task = actor({
|
|||
);
|
||||
},
|
||||
|
||||
async setWorkbenchSessionUnread(c, input: TaskWorkbenchSetSessionUnreadInput): Promise<void> {
|
||||
async setWorkspaceSessionUnread(c, input: TaskWorkspaceSetSessionUnreadInput): Promise<void> {
|
||||
const self = selfTask(c);
|
||||
await self.send(
|
||||
taskWorkflowQueueName("task.command.workbench.set_session_unread"),
|
||||
{ sessionId: input.sessionId, unread: input.unread } satisfies TaskWorkbenchSessionUnreadCommand,
|
||||
taskWorkflowQueueName("task.command.workspace.set_session_unread"),
|
||||
{ sessionId: input.sessionId, unread: input.unread, authSessionId: input.authSessionId } satisfies TaskWorkspaceSessionUnreadCommand,
|
||||
{
|
||||
wait: true,
|
||||
timeout: 10_000,
|
||||
|
|
@ -305,26 +300,27 @@ export const task = actor({
|
|||
);
|
||||
},
|
||||
|
||||
async updateWorkbenchDraft(c, input: TaskWorkbenchUpdateDraftInput): Promise<void> {
|
||||
async updateWorkspaceDraft(c, input: TaskWorkspaceUpdateDraftInput): Promise<void> {
|
||||
const self = selfTask(c);
|
||||
await self.send(
|
||||
taskWorkflowQueueName("task.command.workbench.update_draft"),
|
||||
taskWorkflowQueueName("task.command.workspace.update_draft"),
|
||||
{
|
||||
sessionId: input.sessionId,
|
||||
text: input.text,
|
||||
attachments: input.attachments,
|
||||
} satisfies TaskWorkbenchUpdateDraftCommand,
|
||||
authSessionId: input.authSessionId,
|
||||
} satisfies TaskWorkspaceUpdateDraftCommand,
|
||||
{
|
||||
wait: false,
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
async changeWorkbenchModel(c, input: TaskWorkbenchChangeModelInput): Promise<void> {
|
||||
async changeWorkspaceModel(c, input: TaskWorkspaceChangeModelInput): Promise<void> {
|
||||
const self = selfTask(c);
|
||||
await self.send(
|
||||
taskWorkflowQueueName("task.command.workbench.change_model"),
|
||||
{ sessionId: input.sessionId, model: input.model } satisfies TaskWorkbenchChangeModelCommand,
|
||||
taskWorkflowQueueName("task.command.workspace.change_model"),
|
||||
{ sessionId: input.sessionId, model: input.model, authSessionId: input.authSessionId } satisfies TaskWorkspaceChangeModelCommand,
|
||||
{
|
||||
wait: true,
|
||||
timeout: 10_000,
|
||||
|
|
@ -332,47 +328,56 @@ export const task = actor({
|
|||
);
|
||||
},
|
||||
|
||||
async sendWorkbenchMessage(c, input: TaskWorkbenchSendMessageInput): Promise<void> {
|
||||
async sendWorkspaceMessage(c, input: TaskWorkspaceSendMessageInput): Promise<void> {
|
||||
const self = selfTask(c);
|
||||
await self.send(
|
||||
taskWorkflowQueueName("task.command.workbench.send_message"),
|
||||
taskWorkflowQueueName("task.command.workspace.send_message"),
|
||||
{
|
||||
sessionId: input.sessionId,
|
||||
text: input.text,
|
||||
attachments: input.attachments,
|
||||
} satisfies TaskWorkbenchSendMessageCommand,
|
||||
authSessionId: input.authSessionId,
|
||||
} satisfies TaskWorkspaceSendMessageCommand,
|
||||
{
|
||||
wait: false,
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
async stopWorkbenchSession(c, input: TaskSessionCommand): Promise<void> {
|
||||
async stopWorkspaceSession(c, input: TaskSessionCommand): Promise<void> {
|
||||
const self = selfTask(c);
|
||||
await self.send(taskWorkflowQueueName("task.command.workbench.stop_session"), { sessionId: input.sessionId } satisfies TaskWorkbenchSessionCommand, {
|
||||
wait: false,
|
||||
});
|
||||
await self.send(
|
||||
taskWorkflowQueueName("task.command.workspace.stop_session"),
|
||||
{ sessionId: input.sessionId, authSessionId: input.authSessionId } satisfies TaskWorkspaceSessionCommand,
|
||||
{
|
||||
wait: false,
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
async syncWorkbenchSessionStatus(c, input: TaskStatusSyncCommand): Promise<void> {
|
||||
async syncWorkspaceSessionStatus(c, input: TaskStatusSyncCommand): Promise<void> {
|
||||
const self = selfTask(c);
|
||||
await self.send(taskWorkflowQueueName("task.command.workbench.sync_session_status"), input, {
|
||||
await self.send(taskWorkflowQueueName("task.command.workspace.sync_session_status"), input, {
|
||||
wait: true,
|
||||
timeout: 20_000,
|
||||
});
|
||||
},
|
||||
|
||||
async closeWorkbenchSession(c, input: TaskSessionCommand): Promise<void> {
|
||||
const self = selfTask(c);
|
||||
await self.send(taskWorkflowQueueName("task.command.workbench.close_session"), { sessionId: input.sessionId } satisfies TaskWorkbenchSessionCommand, {
|
||||
wait: false,
|
||||
});
|
||||
},
|
||||
|
||||
async publishWorkbenchPr(c): Promise<void> {
|
||||
async closeWorkspaceSession(c, input: TaskSessionCommand): Promise<void> {
|
||||
const self = selfTask(c);
|
||||
await self.send(
|
||||
taskWorkflowQueueName("task.command.workbench.publish_pr"),
|
||||
taskWorkflowQueueName("task.command.workspace.close_session"),
|
||||
{ sessionId: input.sessionId, authSessionId: input.authSessionId } satisfies TaskWorkspaceSessionCommand,
|
||||
{
|
||||
wait: false,
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
async publishWorkspacePr(c): Promise<void> {
|
||||
const self = selfTask(c);
|
||||
await self.send(
|
||||
taskWorkflowQueueName("task.command.workspace.publish_pr"),
|
||||
{},
|
||||
{
|
||||
wait: false,
|
||||
|
|
@ -380,9 +385,9 @@ export const task = actor({
|
|||
);
|
||||
},
|
||||
|
||||
async revertWorkbenchFile(c, input: { path: string }): Promise<void> {
|
||||
async revertWorkspaceFile(c, input: { path: string }): Promise<void> {
|
||||
const self = selfTask(c);
|
||||
await self.send(taskWorkflowQueueName("task.command.workbench.revert_file"), input, {
|
||||
await self.send(taskWorkflowQueueName("task.command.workspace.revert_file"), input, {
|
||||
wait: false,
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
import { eq } from "drizzle-orm";
|
||||
import { getTaskSandbox } from "../../handles.js";
|
||||
import { logActorWarning, resolveErrorMessage } from "../../logging.js";
|
||||
import { task as taskTable, taskRuntime } from "../db/schema.js";
|
||||
import { TASK_ROW_ID, appendHistory, getCurrentRecord, setTaskState } from "./common.js";
|
||||
import { task as taskTable } from "../db/schema.js";
|
||||
import { TASK_ROW_ID, appendAuditLog, getCurrentRecord, setTaskState } from "./common.js";
|
||||
import { pushActiveBranchActivity } from "./push.js";
|
||||
|
||||
async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {
|
||||
|
|
@ -25,6 +25,7 @@ async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: str
|
|||
export async function handleAttachActivity(loopCtx: any, msg: any): Promise<void> {
|
||||
const record = await getCurrentRecord(loopCtx);
|
||||
let target = record.sandboxes.find((sandbox: any) => sandbox.sandboxId === record.activeSandboxId)?.switchTarget ?? "";
|
||||
const sessionId = msg.body?.sessionId ?? null;
|
||||
|
||||
if (record.activeSandboxId) {
|
||||
try {
|
||||
|
|
@ -38,14 +39,14 @@ export async function handleAttachActivity(loopCtx: any, msg: any): Promise<void
|
|||
}
|
||||
}
|
||||
|
||||
await appendHistory(loopCtx, "task.attach", {
|
||||
await appendAuditLog(loopCtx, "task.attach", {
|
||||
target,
|
||||
sessionId: record.activeSessionId,
|
||||
sessionId,
|
||||
});
|
||||
|
||||
await msg.complete({
|
||||
target,
|
||||
sessionId: record.activeSessionId,
|
||||
sessionId,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -64,20 +65,17 @@ export async function handlePushActivity(loopCtx: any, msg: any): Promise<void>
|
|||
await msg.complete({ ok: true });
|
||||
}
|
||||
|
||||
export async function handleSimpleCommandActivity(loopCtx: any, msg: any, statusMessage: string, historyKind: string): Promise<void> {
|
||||
const db = loopCtx.db;
|
||||
await db.update(taskRuntime).set({ statusMessage, updatedAt: Date.now() }).where(eq(taskRuntime.id, TASK_ROW_ID)).run();
|
||||
|
||||
await appendHistory(loopCtx, historyKind, { reason: msg.body?.reason ?? null });
|
||||
export async function handleSimpleCommandActivity(loopCtx: any, msg: any, _statusMessage: string, historyKind: string): Promise<void> {
|
||||
await appendAuditLog(loopCtx, historyKind, { reason: msg.body?.reason ?? null });
|
||||
await msg.complete({ ok: true });
|
||||
}
|
||||
|
||||
export async function handleArchiveActivity(loopCtx: any, msg: any): Promise<void> {
|
||||
await setTaskState(loopCtx, "archive_stop_status_sync", "stopping status sync");
|
||||
await setTaskState(loopCtx, "archive_stop_status_sync");
|
||||
const record = await getCurrentRecord(loopCtx);
|
||||
|
||||
if (record.activeSandboxId) {
|
||||
await setTaskState(loopCtx, "archive_release_sandbox", "releasing sandbox");
|
||||
await setTaskState(loopCtx, "archive_release_sandbox");
|
||||
void withTimeout(getTaskSandbox(loopCtx, loopCtx.state.organizationId, record.activeSandboxId).destroy(), 45_000, "sandbox destroy").catch((error) => {
|
||||
logActorWarning("task.commands", "failed to release sandbox during archive", {
|
||||
organizationId: loopCtx.state.organizationId,
|
||||
|
|
@ -90,17 +88,15 @@ export async function handleArchiveActivity(loopCtx: any, msg: any): Promise<voi
|
|||
}
|
||||
|
||||
const db = loopCtx.db;
|
||||
await setTaskState(loopCtx, "archive_finalize", "finalizing archive");
|
||||
await setTaskState(loopCtx, "archive_finalize");
|
||||
await db.update(taskTable).set({ status: "archived", updatedAt: Date.now() }).where(eq(taskTable.id, TASK_ROW_ID)).run();
|
||||
|
||||
await db.update(taskRuntime).set({ activeSessionId: null, statusMessage: "archived", updatedAt: Date.now() }).where(eq(taskRuntime.id, TASK_ROW_ID)).run();
|
||||
|
||||
await appendHistory(loopCtx, "task.archive", { reason: msg.body?.reason ?? null });
|
||||
await appendAuditLog(loopCtx, "task.archive", { reason: msg.body?.reason ?? null });
|
||||
await msg.complete({ ok: true });
|
||||
}
|
||||
|
||||
export async function killDestroySandboxActivity(loopCtx: any): Promise<void> {
|
||||
await setTaskState(loopCtx, "kill_destroy_sandbox", "destroying sandbox");
|
||||
await setTaskState(loopCtx, "kill_destroy_sandbox");
|
||||
const record = await getCurrentRecord(loopCtx);
|
||||
if (!record.activeSandboxId) {
|
||||
return;
|
||||
|
|
@ -110,13 +106,11 @@ export async function killDestroySandboxActivity(loopCtx: any): Promise<void> {
|
|||
}
|
||||
|
||||
export async function killWriteDbActivity(loopCtx: any, msg: any): Promise<void> {
|
||||
await setTaskState(loopCtx, "kill_finalize", "finalizing kill");
|
||||
await setTaskState(loopCtx, "kill_finalize");
|
||||
const db = loopCtx.db;
|
||||
await db.update(taskTable).set({ status: "killed", updatedAt: Date.now() }).where(eq(taskTable.id, TASK_ROW_ID)).run();
|
||||
|
||||
await db.update(taskRuntime).set({ statusMessage: "killed", updatedAt: Date.now() }).where(eq(taskRuntime.id, TASK_ROW_ID)).run();
|
||||
|
||||
await appendHistory(loopCtx, "task.kill", { reason: msg.body?.reason ?? null });
|
||||
await appendAuditLog(loopCtx, "task.kill", { reason: msg.body?.reason ?? null });
|
||||
await msg.complete({ ok: true });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
import { eq } from "drizzle-orm";
|
||||
import type { TaskRecord, TaskStatus } from "@sandbox-agent/foundry-shared";
|
||||
import { task as taskTable, taskRuntime, taskSandboxes } from "../db/schema.js";
|
||||
import { historyKey } from "../../keys.js";
|
||||
import { broadcastTaskUpdate } from "../workbench.js";
|
||||
import { getOrCreateAuditLog } from "../../handles.js";
|
||||
import { broadcastTaskUpdate } from "../workspace.js";
|
||||
|
||||
export const TASK_ROW_ID = 1;
|
||||
|
||||
|
|
@ -56,33 +56,11 @@ export function buildAgentPrompt(task: string): string {
|
|||
return task.trim();
|
||||
}
|
||||
|
||||
export async function setTaskState(ctx: any, status: TaskStatus, statusMessage?: string): Promise<void> {
|
||||
export async function setTaskState(ctx: any, status: TaskStatus): Promise<void> {
|
||||
const now = Date.now();
|
||||
const db = ctx.db;
|
||||
await db.update(taskTable).set({ status, updatedAt: now }).where(eq(taskTable.id, TASK_ROW_ID)).run();
|
||||
|
||||
if (statusMessage != null) {
|
||||
await db
|
||||
.insert(taskRuntime)
|
||||
.values({
|
||||
id: TASK_ROW_ID,
|
||||
activeSandboxId: null,
|
||||
activeSessionId: null,
|
||||
activeSwitchTarget: null,
|
||||
activeCwd: null,
|
||||
statusMessage,
|
||||
updatedAt: now,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: taskRuntime.id,
|
||||
set: {
|
||||
statusMessage,
|
||||
updatedAt: now,
|
||||
},
|
||||
})
|
||||
.run();
|
||||
}
|
||||
|
||||
await broadcastTaskUpdate(ctx);
|
||||
}
|
||||
|
||||
|
|
@ -95,11 +73,7 @@ export async function getCurrentRecord(ctx: any): Promise<TaskRecord> {
|
|||
task: taskTable.task,
|
||||
sandboxProviderId: taskTable.sandboxProviderId,
|
||||
status: taskTable.status,
|
||||
statusMessage: taskRuntime.statusMessage,
|
||||
activeSandboxId: taskRuntime.activeSandboxId,
|
||||
activeSessionId: taskRuntime.activeSessionId,
|
||||
agentType: taskTable.agentType,
|
||||
prSubmitted: taskTable.prSubmitted,
|
||||
createdAt: taskTable.createdAt,
|
||||
updatedAt: taskTable.updatedAt,
|
||||
})
|
||||
|
|
@ -135,9 +109,7 @@ export async function getCurrentRecord(ctx: any): Promise<TaskRecord> {
|
|||
task: row.task,
|
||||
sandboxProviderId: row.sandboxProviderId,
|
||||
status: row.status,
|
||||
statusMessage: row.statusMessage ?? null,
|
||||
activeSandboxId: row.activeSandboxId ?? null,
|
||||
activeSessionId: row.activeSessionId ?? null,
|
||||
sandboxes: sandboxes.map((sb) => ({
|
||||
sandboxId: sb.sandboxId,
|
||||
sandboxProviderId: sb.sandboxProviderId,
|
||||
|
|
@ -147,12 +119,7 @@ export async function getCurrentRecord(ctx: any): Promise<TaskRecord> {
|
|||
createdAt: sb.createdAt,
|
||||
updatedAt: sb.updatedAt,
|
||||
})),
|
||||
agentType: row.agentType ?? null,
|
||||
prSubmitted: Boolean(row.prSubmitted),
|
||||
diffStat: null,
|
||||
hasUnpushed: null,
|
||||
conflictsWithMain: null,
|
||||
parentBranch: null,
|
||||
prUrl: null,
|
||||
prAuthor: null,
|
||||
ciStatus: null,
|
||||
|
|
@ -163,17 +130,20 @@ export async function getCurrentRecord(ctx: any): Promise<TaskRecord> {
|
|||
} as TaskRecord;
|
||||
}
|
||||
|
||||
export async function appendHistory(ctx: any, kind: string, payload: Record<string, unknown>): Promise<void> {
|
||||
const client = ctx.client();
|
||||
const history = await client.history.getOrCreate(historyKey(ctx.state.organizationId, ctx.state.repoId), {
|
||||
createWithInput: { organizationId: ctx.state.organizationId, repoId: ctx.state.repoId },
|
||||
});
|
||||
await history.append({
|
||||
kind,
|
||||
taskId: ctx.state.taskId,
|
||||
branchName: ctx.state.branchName,
|
||||
payload,
|
||||
});
|
||||
export async function appendAuditLog(ctx: any, kind: string, payload: Record<string, unknown>): Promise<void> {
|
||||
const auditLog = await getOrCreateAuditLog(ctx, ctx.state.organizationId, ctx.state.repoId);
|
||||
await auditLog.send(
|
||||
"auditLog.command.append",
|
||||
{
|
||||
kind,
|
||||
taskId: ctx.state.taskId,
|
||||
branchName: ctx.state.branchName,
|
||||
payload,
|
||||
},
|
||||
{
|
||||
wait: false,
|
||||
},
|
||||
);
|
||||
|
||||
await broadcastTaskUpdate(ctx);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,24 +14,23 @@ import {
|
|||
} from "./commands.js";
|
||||
import { TASK_QUEUE_NAMES } from "./queue.js";
|
||||
import {
|
||||
changeWorkbenchModel,
|
||||
closeWorkbenchSession,
|
||||
createWorkbenchSession,
|
||||
ensureWorkbenchSession,
|
||||
refreshWorkbenchDerivedState,
|
||||
refreshWorkbenchSessionTranscript,
|
||||
markWorkbenchUnread,
|
||||
publishWorkbenchPr,
|
||||
renameWorkbenchBranch,
|
||||
renameWorkbenchTask,
|
||||
renameWorkbenchSession,
|
||||
revertWorkbenchFile,
|
||||
sendWorkbenchMessage,
|
||||
setWorkbenchSessionUnread,
|
||||
stopWorkbenchSession,
|
||||
syncWorkbenchSessionStatus,
|
||||
updateWorkbenchDraft,
|
||||
} from "../workbench.js";
|
||||
changeWorkspaceModel,
|
||||
closeWorkspaceSession,
|
||||
createWorkspaceSession,
|
||||
ensureWorkspaceSession,
|
||||
refreshWorkspaceDerivedState,
|
||||
refreshWorkspaceSessionTranscript,
|
||||
markWorkspaceUnread,
|
||||
publishWorkspacePr,
|
||||
renameWorkspaceTask,
|
||||
renameWorkspaceSession,
|
||||
revertWorkspaceFile,
|
||||
sendWorkspaceMessage,
|
||||
setWorkspaceSessionUnread,
|
||||
stopWorkspaceSession,
|
||||
syncWorkspaceSessionStatus,
|
||||
updateWorkspaceDraft,
|
||||
} from "../workspace.js";
|
||||
|
||||
export { TASK_QUEUE_NAMES, taskWorkflowQueueName } from "./queue.js";
|
||||
|
||||
|
|
@ -113,31 +112,22 @@ const commandHandlers: Record<TaskQueueName, WorkflowHandler> = {
|
|||
await loopCtx.step("handle-get", async () => handleGetActivity(loopCtx, msg));
|
||||
},
|
||||
|
||||
"task.command.workbench.mark_unread": async (loopCtx, msg) => {
|
||||
await loopCtx.step("workbench-mark-unread", async () => markWorkbenchUnread(loopCtx));
|
||||
"task.command.workspace.mark_unread": async (loopCtx, msg) => {
|
||||
await loopCtx.step("workspace-mark-unread", async () => markWorkspaceUnread(loopCtx));
|
||||
await msg.complete({ ok: true });
|
||||
},
|
||||
|
||||
"task.command.workbench.rename_task": async (loopCtx, msg) => {
|
||||
await loopCtx.step("workbench-rename-task", async () => renameWorkbenchTask(loopCtx, msg.body.value));
|
||||
"task.command.workspace.rename_task": async (loopCtx, msg) => {
|
||||
await loopCtx.step("workspace-rename-task", async () => renameWorkspaceTask(loopCtx, msg.body.value));
|
||||
await msg.complete({ ok: true });
|
||||
},
|
||||
|
||||
"task.command.workbench.rename_branch": async (loopCtx, msg) => {
|
||||
await loopCtx.step({
|
||||
name: "workbench-rename-branch",
|
||||
timeout: 5 * 60_000,
|
||||
run: async () => renameWorkbenchBranch(loopCtx, msg.body.value),
|
||||
});
|
||||
await msg.complete({ ok: true });
|
||||
},
|
||||
|
||||
"task.command.workbench.create_session": async (loopCtx, msg) => {
|
||||
"task.command.workspace.create_session": async (loopCtx, msg) => {
|
||||
try {
|
||||
const created = await loopCtx.step({
|
||||
name: "workbench-create-session",
|
||||
name: "workspace-create-session",
|
||||
timeout: 5 * 60_000,
|
||||
run: async () => createWorkbenchSession(loopCtx, msg.body?.model),
|
||||
run: async () => createWorkspaceSession(loopCtx, msg.body?.model),
|
||||
});
|
||||
await msg.complete(created);
|
||||
} catch (error) {
|
||||
|
|
@ -145,17 +135,17 @@ const commandHandlers: Record<TaskQueueName, WorkflowHandler> = {
|
|||
}
|
||||
},
|
||||
|
||||
"task.command.workbench.create_session_and_send": async (loopCtx, msg) => {
|
||||
"task.command.workspace.create_session_and_send": async (loopCtx, msg) => {
|
||||
try {
|
||||
const created = await loopCtx.step({
|
||||
name: "workbench-create-session-for-send",
|
||||
name: "workspace-create-session-for-send",
|
||||
timeout: 5 * 60_000,
|
||||
run: async () => createWorkbenchSession(loopCtx, msg.body?.model),
|
||||
run: async () => createWorkspaceSession(loopCtx, msg.body?.model),
|
||||
});
|
||||
await loopCtx.step({
|
||||
name: "workbench-send-initial-message",
|
||||
name: "workspace-send-initial-message",
|
||||
timeout: 5 * 60_000,
|
||||
run: async () => sendWorkbenchMessage(loopCtx, created.sessionId, msg.body.text, []),
|
||||
run: async () => sendWorkspaceMessage(loopCtx, created.sessionId, msg.body.text, []),
|
||||
});
|
||||
} catch (error) {
|
||||
logActorWarning("task.workflow", "create_session_and_send failed", {
|
||||
|
|
@ -165,41 +155,41 @@ const commandHandlers: Record<TaskQueueName, WorkflowHandler> = {
|
|||
await msg.complete({ ok: true });
|
||||
},
|
||||
|
||||
"task.command.workbench.ensure_session": async (loopCtx, msg) => {
|
||||
"task.command.workspace.ensure_session": async (loopCtx, msg) => {
|
||||
await loopCtx.step({
|
||||
name: "workbench-ensure-session",
|
||||
name: "workspace-ensure-session",
|
||||
timeout: 5 * 60_000,
|
||||
run: async () => ensureWorkbenchSession(loopCtx, msg.body.sessionId, msg.body?.model),
|
||||
run: async () => ensureWorkspaceSession(loopCtx, msg.body.sessionId, msg.body?.model),
|
||||
});
|
||||
await msg.complete({ ok: true });
|
||||
},
|
||||
|
||||
"task.command.workbench.rename_session": async (loopCtx, msg) => {
|
||||
await loopCtx.step("workbench-rename-session", async () => renameWorkbenchSession(loopCtx, msg.body.sessionId, msg.body.title));
|
||||
"task.command.workspace.rename_session": async (loopCtx, msg) => {
|
||||
await loopCtx.step("workspace-rename-session", async () => renameWorkspaceSession(loopCtx, msg.body.sessionId, msg.body.title));
|
||||
await msg.complete({ ok: true });
|
||||
},
|
||||
|
||||
"task.command.workbench.set_session_unread": async (loopCtx, msg) => {
|
||||
await loopCtx.step("workbench-set-session-unread", async () => setWorkbenchSessionUnread(loopCtx, msg.body.sessionId, msg.body.unread));
|
||||
"task.command.workspace.set_session_unread": async (loopCtx, msg) => {
|
||||
await loopCtx.step("workspace-set-session-unread", async () => setWorkspaceSessionUnread(loopCtx, msg.body.sessionId, msg.body.unread));
|
||||
await msg.complete({ ok: true });
|
||||
},
|
||||
|
||||
"task.command.workbench.update_draft": async (loopCtx, msg) => {
|
||||
await loopCtx.step("workbench-update-draft", async () => updateWorkbenchDraft(loopCtx, msg.body.sessionId, msg.body.text, msg.body.attachments));
|
||||
"task.command.workspace.update_draft": async (loopCtx, msg) => {
|
||||
await loopCtx.step("workspace-update-draft", async () => updateWorkspaceDraft(loopCtx, msg.body.sessionId, msg.body.text, msg.body.attachments));
|
||||
await msg.complete({ ok: true });
|
||||
},
|
||||
|
||||
"task.command.workbench.change_model": async (loopCtx, msg) => {
|
||||
await loopCtx.step("workbench-change-model", async () => changeWorkbenchModel(loopCtx, msg.body.sessionId, msg.body.model));
|
||||
"task.command.workspace.change_model": async (loopCtx, msg) => {
|
||||
await loopCtx.step("workspace-change-model", async () => changeWorkspaceModel(loopCtx, msg.body.sessionId, msg.body.model));
|
||||
await msg.complete({ ok: true });
|
||||
},
|
||||
|
||||
"task.command.workbench.send_message": async (loopCtx, msg) => {
|
||||
"task.command.workspace.send_message": async (loopCtx, msg) => {
|
||||
try {
|
||||
await loopCtx.step({
|
||||
name: "workbench-send-message",
|
||||
name: "workspace-send-message",
|
||||
timeout: 10 * 60_000,
|
||||
run: async () => sendWorkbenchMessage(loopCtx, msg.body.sessionId, msg.body.text, msg.body.attachments),
|
||||
run: async () => sendWorkspaceMessage(loopCtx, msg.body.sessionId, msg.body.text, msg.body.attachments),
|
||||
});
|
||||
await msg.complete({ ok: true });
|
||||
} catch (error) {
|
||||
|
|
@ -207,61 +197,61 @@ const commandHandlers: Record<TaskQueueName, WorkflowHandler> = {
|
|||
}
|
||||
},
|
||||
|
||||
"task.command.workbench.stop_session": async (loopCtx, msg) => {
|
||||
"task.command.workspace.stop_session": async (loopCtx, msg) => {
|
||||
await loopCtx.step({
|
||||
name: "workbench-stop-session",
|
||||
name: "workspace-stop-session",
|
||||
timeout: 5 * 60_000,
|
||||
run: async () => stopWorkbenchSession(loopCtx, msg.body.sessionId),
|
||||
run: async () => stopWorkspaceSession(loopCtx, msg.body.sessionId),
|
||||
});
|
||||
await msg.complete({ ok: true });
|
||||
},
|
||||
|
||||
"task.command.workbench.sync_session_status": async (loopCtx, msg) => {
|
||||
await loopCtx.step("workbench-sync-session-status", async () => syncWorkbenchSessionStatus(loopCtx, msg.body.sessionId, msg.body.status, msg.body.at));
|
||||
"task.command.workspace.sync_session_status": async (loopCtx, msg) => {
|
||||
await loopCtx.step("workspace-sync-session-status", async () => syncWorkspaceSessionStatus(loopCtx, msg.body.sessionId, msg.body.status, msg.body.at));
|
||||
await msg.complete({ ok: true });
|
||||
},
|
||||
|
||||
"task.command.workbench.refresh_derived": async (loopCtx, msg) => {
|
||||
"task.command.workspace.refresh_derived": async (loopCtx, msg) => {
|
||||
await loopCtx.step({
|
||||
name: "workbench-refresh-derived",
|
||||
name: "workspace-refresh-derived",
|
||||
timeout: 5 * 60_000,
|
||||
run: async () => refreshWorkbenchDerivedState(loopCtx),
|
||||
run: async () => refreshWorkspaceDerivedState(loopCtx),
|
||||
});
|
||||
await msg.complete({ ok: true });
|
||||
},
|
||||
|
||||
"task.command.workbench.refresh_session_transcript": async (loopCtx, msg) => {
|
||||
"task.command.workspace.refresh_session_transcript": async (loopCtx, msg) => {
|
||||
await loopCtx.step({
|
||||
name: "workbench-refresh-session-transcript",
|
||||
name: "workspace-refresh-session-transcript",
|
||||
timeout: 60_000,
|
||||
run: async () => refreshWorkbenchSessionTranscript(loopCtx, msg.body.sessionId),
|
||||
run: async () => refreshWorkspaceSessionTranscript(loopCtx, msg.body.sessionId),
|
||||
});
|
||||
await msg.complete({ ok: true });
|
||||
},
|
||||
|
||||
"task.command.workbench.close_session": async (loopCtx, msg) => {
|
||||
"task.command.workspace.close_session": async (loopCtx, msg) => {
|
||||
await loopCtx.step({
|
||||
name: "workbench-close-session",
|
||||
name: "workspace-close-session",
|
||||
timeout: 5 * 60_000,
|
||||
run: async () => closeWorkbenchSession(loopCtx, msg.body.sessionId),
|
||||
run: async () => closeWorkspaceSession(loopCtx, msg.body.sessionId),
|
||||
});
|
||||
await msg.complete({ ok: true });
|
||||
},
|
||||
|
||||
"task.command.workbench.publish_pr": async (loopCtx, msg) => {
|
||||
"task.command.workspace.publish_pr": async (loopCtx, msg) => {
|
||||
await loopCtx.step({
|
||||
name: "workbench-publish-pr",
|
||||
name: "workspace-publish-pr",
|
||||
timeout: 10 * 60_000,
|
||||
run: async () => publishWorkbenchPr(loopCtx),
|
||||
run: async () => publishWorkspacePr(loopCtx),
|
||||
});
|
||||
await msg.complete({ ok: true });
|
||||
},
|
||||
|
||||
"task.command.workbench.revert_file": async (loopCtx, msg) => {
|
||||
"task.command.workspace.revert_file": async (loopCtx, msg) => {
|
||||
await loopCtx.step({
|
||||
name: "workbench-revert-file",
|
||||
name: "workspace-revert-file",
|
||||
timeout: 5 * 60_000,
|
||||
run: async () => revertWorkbenchFile(loopCtx, msg.body.path),
|
||||
run: async () => revertWorkspaceFile(loopCtx, msg.body.path),
|
||||
});
|
||||
await msg.complete({ ok: true });
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,27 +1,18 @@
|
|||
// @ts-nocheck
|
||||
import { eq } from "drizzle-orm";
|
||||
import { getActorRuntimeContext } from "../../context.js";
|
||||
import { getOrCreateHistory, selfTask } from "../../handles.js";
|
||||
import { selfTask } from "../../handles.js";
|
||||
import { resolveErrorMessage } from "../../logging.js";
|
||||
import { defaultSandboxProviderId } from "../../../sandbox-config.js";
|
||||
import { task as taskTable, taskRuntime } from "../db/schema.js";
|
||||
import { TASK_ROW_ID, appendHistory, collectErrorMessages, resolveErrorDetail, setTaskState } from "./common.js";
|
||||
import { TASK_ROW_ID, appendAuditLog, collectErrorMessages, resolveErrorDetail, setTaskState } from "./common.js";
|
||||
import { taskWorkflowQueueName } from "./queue.js";
|
||||
|
||||
async function ensureTaskRuntimeCacheColumns(db: any): Promise<void> {
|
||||
await db.execute(`ALTER TABLE task_runtime ADD COLUMN git_state_json text`).catch(() => {});
|
||||
await db.execute(`ALTER TABLE task_runtime ADD COLUMN git_state_updated_at integer`).catch(() => {});
|
||||
await db.execute(`ALTER TABLE task_runtime ADD COLUMN provision_stage text`).catch(() => {});
|
||||
await db.execute(`ALTER TABLE task_runtime ADD COLUMN provision_stage_updated_at integer`).catch(() => {});
|
||||
}
|
||||
|
||||
export async function initBootstrapDbActivity(loopCtx: any, body: any): Promise<void> {
|
||||
const { config } = getActorRuntimeContext();
|
||||
const sandboxProviderId = body?.sandboxProviderId ?? loopCtx.state.sandboxProviderId ?? defaultSandboxProviderId(config);
|
||||
const sandboxProviderId = body?.sandboxProviderId ?? defaultSandboxProviderId(config);
|
||||
const now = Date.now();
|
||||
|
||||
await ensureTaskRuntimeCacheColumns(loopCtx.db);
|
||||
|
||||
await loopCtx.db
|
||||
.insert(taskTable)
|
||||
.values({
|
||||
|
|
@ -31,7 +22,6 @@ export async function initBootstrapDbActivity(loopCtx: any, body: any): Promise<
|
|||
task: loopCtx.state.task,
|
||||
sandboxProviderId,
|
||||
status: "init_bootstrap_db",
|
||||
agentType: loopCtx.state.agentType ?? config.default_agent,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
|
|
@ -43,7 +33,6 @@ export async function initBootstrapDbActivity(loopCtx: any, body: any): Promise<
|
|||
task: loopCtx.state.task,
|
||||
sandboxProviderId,
|
||||
status: "init_bootstrap_db",
|
||||
agentType: loopCtx.state.agentType ?? config.default_agent,
|
||||
updatedAt: now,
|
||||
},
|
||||
})
|
||||
|
|
@ -54,26 +43,18 @@ export async function initBootstrapDbActivity(loopCtx: any, body: any): Promise<
|
|||
.values({
|
||||
id: TASK_ROW_ID,
|
||||
activeSandboxId: null,
|
||||
activeSessionId: null,
|
||||
activeSwitchTarget: null,
|
||||
activeCwd: null,
|
||||
statusMessage: "provisioning",
|
||||
gitStateJson: null,
|
||||
gitStateUpdatedAt: null,
|
||||
provisionStage: "queued",
|
||||
provisionStageUpdatedAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: taskRuntime.id,
|
||||
set: {
|
||||
activeSandboxId: null,
|
||||
activeSessionId: null,
|
||||
activeSwitchTarget: null,
|
||||
activeCwd: null,
|
||||
statusMessage: "provisioning",
|
||||
provisionStage: "queued",
|
||||
provisionStageUpdatedAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
})
|
||||
|
|
@ -81,16 +62,7 @@ export async function initBootstrapDbActivity(loopCtx: any, body: any): Promise<
|
|||
}
|
||||
|
||||
export async function initEnqueueProvisionActivity(loopCtx: any, body: any): Promise<void> {
|
||||
await setTaskState(loopCtx, "init_enqueue_provision", "provision queued");
|
||||
await loopCtx.db
|
||||
.update(taskRuntime)
|
||||
.set({
|
||||
provisionStage: "queued",
|
||||
provisionStageUpdatedAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
.where(eq(taskRuntime.id, TASK_ROW_ID))
|
||||
.run();
|
||||
await setTaskState(loopCtx, "init_enqueue_provision");
|
||||
|
||||
const self = selfTask(loopCtx);
|
||||
try {
|
||||
|
|
@ -111,29 +83,20 @@ export async function initEnqueueProvisionActivity(loopCtx: any, body: any): Pro
|
|||
export async function initCompleteActivity(loopCtx: any, body: any): Promise<void> {
|
||||
const now = Date.now();
|
||||
const { config } = getActorRuntimeContext();
|
||||
const sandboxProviderId = body?.sandboxProviderId ?? loopCtx.state.sandboxProviderId ?? defaultSandboxProviderId(config);
|
||||
const sandboxProviderId = body?.sandboxProviderId ?? defaultSandboxProviderId(config);
|
||||
|
||||
await setTaskState(loopCtx, "init_complete", "task initialized");
|
||||
await setTaskState(loopCtx, "init_complete");
|
||||
await loopCtx.db
|
||||
.update(taskRuntime)
|
||||
.set({
|
||||
statusMessage: "ready",
|
||||
provisionStage: "ready",
|
||||
provisionStageUpdatedAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
.where(eq(taskRuntime.id, TASK_ROW_ID))
|
||||
.run();
|
||||
|
||||
const history = await getOrCreateHistory(loopCtx, loopCtx.state.organizationId, loopCtx.state.repoId);
|
||||
await history.append({
|
||||
kind: "task.initialized",
|
||||
taskId: loopCtx.state.taskId,
|
||||
branchName: loopCtx.state.branchName,
|
||||
await appendAuditLog(loopCtx, "task.initialized", {
|
||||
payload: { sandboxProviderId },
|
||||
});
|
||||
|
||||
loopCtx.state.initialized = true;
|
||||
}
|
||||
|
||||
export async function initFailedActivity(loopCtx: any, error: unknown): Promise<void> {
|
||||
|
|
@ -141,7 +104,7 @@ export async function initFailedActivity(loopCtx: any, error: unknown): Promise<
|
|||
const detail = resolveErrorDetail(error);
|
||||
const messages = collectErrorMessages(error);
|
||||
const { config } = getActorRuntimeContext();
|
||||
const sandboxProviderId = loopCtx.state.sandboxProviderId ?? defaultSandboxProviderId(config);
|
||||
const sandboxProviderId = defaultSandboxProviderId(config);
|
||||
|
||||
await loopCtx.db
|
||||
.insert(taskTable)
|
||||
|
|
@ -152,7 +115,6 @@ export async function initFailedActivity(loopCtx: any, error: unknown): Promise<
|
|||
task: loopCtx.state.task,
|
||||
sandboxProviderId,
|
||||
status: "error",
|
||||
agentType: loopCtx.state.agentType ?? config.default_agent,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
|
|
@ -164,7 +126,6 @@ export async function initFailedActivity(loopCtx: any, error: unknown): Promise<
|
|||
task: loopCtx.state.task,
|
||||
sandboxProviderId,
|
||||
status: "error",
|
||||
agentType: loopCtx.state.agentType ?? config.default_agent,
|
||||
updatedAt: now,
|
||||
},
|
||||
})
|
||||
|
|
@ -175,30 +136,22 @@ export async function initFailedActivity(loopCtx: any, error: unknown): Promise<
|
|||
.values({
|
||||
id: TASK_ROW_ID,
|
||||
activeSandboxId: null,
|
||||
activeSessionId: null,
|
||||
activeSwitchTarget: null,
|
||||
activeCwd: null,
|
||||
statusMessage: detail,
|
||||
provisionStage: "error",
|
||||
provisionStageUpdatedAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: taskRuntime.id,
|
||||
set: {
|
||||
activeSandboxId: null,
|
||||
activeSessionId: null,
|
||||
activeSwitchTarget: null,
|
||||
activeCwd: null,
|
||||
statusMessage: detail,
|
||||
provisionStage: "error",
|
||||
provisionStageUpdatedAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
})
|
||||
.run();
|
||||
|
||||
await appendHistory(loopCtx, "task.error", {
|
||||
await appendAuditLog(loopCtx, "task.error", {
|
||||
detail,
|
||||
messages,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
import { eq } from "drizzle-orm";
|
||||
import { getTaskSandbox } from "../../handles.js";
|
||||
import { resolveOrganizationGithubAuth } from "../../../services/github-auth.js";
|
||||
import { taskRuntime, taskSandboxes } from "../db/schema.js";
|
||||
import { TASK_ROW_ID, appendHistory, getCurrentRecord } from "./common.js";
|
||||
import { taskSandboxes } from "../db/schema.js";
|
||||
import { appendAuditLog, getCurrentRecord } from "./common.js";
|
||||
|
||||
export interface PushActiveBranchOptions {
|
||||
reason?: string | null;
|
||||
|
|
@ -29,12 +29,6 @@ export async function pushActiveBranchActivity(loopCtx: any, options: PushActive
|
|||
}
|
||||
|
||||
const now = Date.now();
|
||||
await loopCtx.db
|
||||
.update(taskRuntime)
|
||||
.set({ statusMessage: `pushing branch ${branchName}`, updatedAt: now })
|
||||
.where(eq(taskRuntime.id, TASK_ROW_ID))
|
||||
.run();
|
||||
|
||||
await loopCtx.db
|
||||
.update(taskSandboxes)
|
||||
.set({ statusMessage: `pushing branch ${branchName}`, updatedAt: now })
|
||||
|
|
@ -69,19 +63,13 @@ export async function pushActiveBranchActivity(loopCtx: any, options: PushActive
|
|||
}
|
||||
|
||||
const updatedAt = Date.now();
|
||||
await loopCtx.db
|
||||
.update(taskRuntime)
|
||||
.set({ statusMessage: `push complete for ${branchName}`, updatedAt })
|
||||
.where(eq(taskRuntime.id, TASK_ROW_ID))
|
||||
.run();
|
||||
|
||||
await loopCtx.db
|
||||
.update(taskSandboxes)
|
||||
.set({ statusMessage: `push complete for ${branchName}`, updatedAt })
|
||||
.where(eq(taskSandboxes.sandboxId, activeSandboxId))
|
||||
.run();
|
||||
|
||||
await appendHistory(loopCtx, options.historyKind ?? "task.push", {
|
||||
await appendAuditLog(loopCtx, options.historyKind ?? "task.push", {
|
||||
reason: options.reason ?? null,
|
||||
branchName,
|
||||
sandboxId: activeSandboxId,
|
||||
|
|
|
|||
|
|
@ -9,24 +9,23 @@ export const TASK_QUEUE_NAMES = [
|
|||
"task.command.archive",
|
||||
"task.command.kill",
|
||||
"task.command.get",
|
||||
"task.command.workbench.mark_unread",
|
||||
"task.command.workbench.rename_task",
|
||||
"task.command.workbench.rename_branch",
|
||||
"task.command.workbench.create_session",
|
||||
"task.command.workbench.create_session_and_send",
|
||||
"task.command.workbench.ensure_session",
|
||||
"task.command.workbench.rename_session",
|
||||
"task.command.workbench.set_session_unread",
|
||||
"task.command.workbench.update_draft",
|
||||
"task.command.workbench.change_model",
|
||||
"task.command.workbench.send_message",
|
||||
"task.command.workbench.stop_session",
|
||||
"task.command.workbench.sync_session_status",
|
||||
"task.command.workbench.refresh_derived",
|
||||
"task.command.workbench.refresh_session_transcript",
|
||||
"task.command.workbench.close_session",
|
||||
"task.command.workbench.publish_pr",
|
||||
"task.command.workbench.revert_file",
|
||||
"task.command.workspace.mark_unread",
|
||||
"task.command.workspace.rename_task",
|
||||
"task.command.workspace.create_session",
|
||||
"task.command.workspace.create_session_and_send",
|
||||
"task.command.workspace.ensure_session",
|
||||
"task.command.workspace.rename_session",
|
||||
"task.command.workspace.set_session_unread",
|
||||
"task.command.workspace.update_draft",
|
||||
"task.command.workspace.change_model",
|
||||
"task.command.workspace.send_message",
|
||||
"task.command.workspace.stop_session",
|
||||
"task.command.workspace.sync_session_status",
|
||||
"task.command.workspace.refresh_derived",
|
||||
"task.command.workspace.refresh_session_transcript",
|
||||
"task.command.workspace.close_session",
|
||||
"task.command.workspace.publish_pr",
|
||||
"task.command.workspace.revert_file",
|
||||
] as const;
|
||||
|
||||
export function taskWorkflowQueueName(name: string): string {
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@ import { randomUUID } from "node:crypto";
|
|||
import { basename, dirname } from "node:path";
|
||||
import { asc, eq } from "drizzle-orm";
|
||||
import { getActorRuntimeContext } from "../context.js";
|
||||
import { getOrCreateRepository, getOrCreateTaskSandbox, getOrCreateOrganization, getTaskSandbox, selfTask } from "../handles.js";
|
||||
import { getOrCreateRepository, getOrCreateTaskSandbox, getTaskSandbox, selfTask } from "../handles.js";
|
||||
import { SANDBOX_REPO_CWD } from "../sandbox/index.js";
|
||||
import { resolveSandboxProviderId } from "../../sandbox-config.js";
|
||||
import { resolveOrganizationGithubAuth } from "../../services/github-auth.js";
|
||||
import { githubRepoFullNameFromRemote } from "../../services/repo.js";
|
||||
import { task as taskTable, taskRuntime, taskSandboxes, taskWorkbenchSessions } from "./db/schema.js";
|
||||
import { task as taskTable, taskRuntime, taskSandboxes, taskWorkspaceSessions } from "./db/schema.js";
|
||||
import { getCurrentRecord } from "./workflow/common.js";
|
||||
|
||||
function emptyGitState() {
|
||||
|
|
@ -20,42 +20,6 @@ function emptyGitState() {
|
|||
};
|
||||
}
|
||||
|
||||
async function ensureWorkbenchSessionTable(c: any): Promise<void> {
|
||||
await c.db.execute(`
|
||||
CREATE TABLE IF NOT EXISTS task_workbench_sessions (
|
||||
session_id text PRIMARY KEY NOT NULL,
|
||||
sandbox_session_id text,
|
||||
session_name text NOT NULL,
|
||||
model text NOT NULL,
|
||||
status text DEFAULT 'ready' NOT NULL,
|
||||
error_message text,
|
||||
transcript_json text DEFAULT '[]' NOT NULL,
|
||||
transcript_updated_at integer,
|
||||
unread integer DEFAULT 0 NOT NULL,
|
||||
draft_text text DEFAULT '' NOT NULL,
|
||||
draft_attachments_json text DEFAULT '[]' NOT NULL,
|
||||
draft_updated_at integer,
|
||||
created integer DEFAULT 1 NOT NULL,
|
||||
closed integer DEFAULT 0 NOT NULL,
|
||||
thinking_since_ms integer,
|
||||
created_at integer NOT NULL,
|
||||
updated_at integer NOT NULL
|
||||
)
|
||||
`);
|
||||
await c.db.execute(`ALTER TABLE task_workbench_sessions ADD COLUMN sandbox_session_id text`).catch(() => {});
|
||||
await c.db.execute(`ALTER TABLE task_workbench_sessions ADD COLUMN status text DEFAULT 'ready' NOT NULL`).catch(() => {});
|
||||
await c.db.execute(`ALTER TABLE task_workbench_sessions ADD COLUMN error_message text`).catch(() => {});
|
||||
await c.db.execute(`ALTER TABLE task_workbench_sessions ADD COLUMN transcript_json text DEFAULT '[]' NOT NULL`).catch(() => {});
|
||||
await c.db.execute(`ALTER TABLE task_workbench_sessions ADD COLUMN transcript_updated_at integer`).catch(() => {});
|
||||
}
|
||||
|
||||
async function ensureTaskRuntimeCacheColumns(c: any): Promise<void> {
|
||||
await c.db.execute(`ALTER TABLE task_runtime ADD COLUMN git_state_json text`).catch(() => {});
|
||||
await c.db.execute(`ALTER TABLE task_runtime ADD COLUMN git_state_updated_at integer`).catch(() => {});
|
||||
await c.db.execute(`ALTER TABLE task_runtime ADD COLUMN provision_stage text`).catch(() => {});
|
||||
await c.db.execute(`ALTER TABLE task_runtime ADD COLUMN provision_stage_updated_at integer`).catch(() => {});
|
||||
}
|
||||
|
||||
function defaultModelForAgent(agentType: string | null | undefined) {
|
||||
return agentType === "codex" ? "gpt-5.3-codex" : "claude-sonnet-4";
|
||||
}
|
||||
|
|
@ -168,8 +132,7 @@ export function shouldRecreateSessionForModelChange(meta: {
|
|||
}
|
||||
|
||||
async function listSessionMetaRows(c: any, options?: { includeClosed?: boolean }): Promise<Array<any>> {
|
||||
await ensureWorkbenchSessionTable(c);
|
||||
const rows = await c.db.select().from(taskWorkbenchSessions).orderBy(asc(taskWorkbenchSessions.createdAt)).all();
|
||||
const rows = await c.db.select().from(taskWorkspaceSessions).orderBy(asc(taskWorkspaceSessions.createdAt)).all();
|
||||
const mapped = rows.map((row: any) => ({
|
||||
...row,
|
||||
id: row.sessionId,
|
||||
|
|
@ -199,8 +162,7 @@ async function nextSessionName(c: any): Promise<string> {
|
|||
}
|
||||
|
||||
async function readSessionMeta(c: any, sessionId: string): Promise<any | null> {
|
||||
await ensureWorkbenchSessionTable(c);
|
||||
const row = await c.db.select().from(taskWorkbenchSessions).where(eq(taskWorkbenchSessions.sessionId, sessionId)).get();
|
||||
const row = await c.db.select().from(taskWorkspaceSessions).where(eq(taskWorkspaceSessions.sessionId, sessionId)).get();
|
||||
|
||||
if (!row) {
|
||||
return null;
|
||||
|
|
@ -236,7 +198,6 @@ async function ensureSessionMeta(
|
|||
errorMessage?: string | null;
|
||||
},
|
||||
): Promise<any> {
|
||||
await ensureWorkbenchSessionTable(c);
|
||||
const existing = await readSessionMeta(c, params.sessionId);
|
||||
if (existing) {
|
||||
return existing;
|
||||
|
|
@ -248,7 +209,7 @@ async function ensureSessionMeta(
|
|||
const unread = params.unread ?? false;
|
||||
|
||||
await c.db
|
||||
.insert(taskWorkbenchSessions)
|
||||
.insert(taskWorkspaceSessions)
|
||||
.values({
|
||||
sessionId: params.sessionId,
|
||||
sandboxSessionId: params.sandboxSessionId ?? null,
|
||||
|
|
@ -276,19 +237,18 @@ async function ensureSessionMeta(
|
|||
async function updateSessionMeta(c: any, sessionId: string, values: Record<string, unknown>): Promise<any> {
|
||||
await ensureSessionMeta(c, { sessionId });
|
||||
await c.db
|
||||
.update(taskWorkbenchSessions)
|
||||
.update(taskWorkspaceSessions)
|
||||
.set({
|
||||
...values,
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
.where(eq(taskWorkbenchSessions.sessionId, sessionId))
|
||||
.where(eq(taskWorkspaceSessions.sessionId, sessionId))
|
||||
.run();
|
||||
return await readSessionMeta(c, sessionId);
|
||||
}
|
||||
|
||||
async function readSessionMetaBySandboxSessionId(c: any, sandboxSessionId: string): Promise<any | null> {
|
||||
await ensureWorkbenchSessionTable(c);
|
||||
const row = await c.db.select().from(taskWorkbenchSessions).where(eq(taskWorkbenchSessions.sandboxSessionId, sandboxSessionId)).get();
|
||||
const row = await c.db.select().from(taskWorkspaceSessions).where(eq(taskWorkspaceSessions.sandboxSessionId, sandboxSessionId)).get();
|
||||
if (!row) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -298,17 +258,17 @@ async function readSessionMetaBySandboxSessionId(c: any, sandboxSessionId: strin
|
|||
async function requireReadySessionMeta(c: any, sessionId: string): Promise<any> {
|
||||
const meta = await readSessionMeta(c, sessionId);
|
||||
if (!meta) {
|
||||
throw new Error(`Unknown workbench session: ${sessionId}`);
|
||||
throw new Error(`Unknown workspace session: ${sessionId}`);
|
||||
}
|
||||
if (meta.status !== "ready" || !meta.sandboxSessionId) {
|
||||
throw new Error(meta.errorMessage ?? "This workbench session is still preparing");
|
||||
throw new Error(meta.errorMessage ?? "This workspace session is still preparing");
|
||||
}
|
||||
return meta;
|
||||
}
|
||||
|
||||
export function requireSendableSessionMeta(meta: any, sessionId: string): any {
|
||||
if (!meta) {
|
||||
throw new Error(`Unknown workbench session: ${sessionId}`);
|
||||
throw new Error(`Unknown workspace session: ${sessionId}`);
|
||||
}
|
||||
if (meta.status !== "ready" || !meta.sandboxSessionId) {
|
||||
throw new Error(`Session is not ready (status: ${meta.status}). Wait for session provisioning to complete.`);
|
||||
|
|
@ -389,7 +349,7 @@ async function getTaskSandboxRuntime(
|
|||
/**
|
||||
* Track whether the sandbox repo has been fully prepared (cloned + fetched + checked out)
|
||||
* for the current actor lifecycle. Subsequent calls can skip the expensive `git fetch`
|
||||
* when `skipFetch` is true (used by sendWorkbenchMessage to avoid blocking on every prompt).
|
||||
* when `skipFetch` is true (used by sendWorkspaceMessage to avoid blocking on every prompt).
|
||||
*/
|
||||
let sandboxRepoPrepared = false;
|
||||
|
||||
|
|
@ -452,7 +412,7 @@ async function executeInSandbox(
|
|||
label: string;
|
||||
},
|
||||
): Promise<{ exitCode: number; result: string }> {
|
||||
const record = await ensureWorkbenchSeeded(c);
|
||||
const record = await ensureWorkspaceSeeded(c);
|
||||
const runtime = await getTaskSandboxRuntime(c, record);
|
||||
await ensureSandboxRepo(c, runtime.sandbox, record);
|
||||
const response = await runtime.sandbox.runProcess({
|
||||
|
|
@ -555,7 +515,7 @@ function buildFileTree(paths: string[]): Array<any> {
|
|||
return sortNodes(root.children.values());
|
||||
}
|
||||
|
||||
async function collectWorkbenchGitState(c: any, record: any) {
|
||||
async function collectWorkspaceGitState(c: any, record: any) {
|
||||
const activeSandboxId = record.activeSandboxId;
|
||||
const activeSandbox = activeSandboxId != null ? ((record.sandboxes ?? []).find((candidate: any) => candidate.sandboxId === activeSandboxId) ?? null) : null;
|
||||
const cwd = activeSandbox?.cwd ?? record.sandboxes?.[0]?.cwd ?? null;
|
||||
|
|
@ -628,7 +588,6 @@ async function collectWorkbenchGitState(c: any, record: any) {
|
|||
}
|
||||
|
||||
async function readCachedGitState(c: any): Promise<{ fileChanges: Array<any>; diffs: Record<string, string>; fileTree: Array<any>; updatedAt: number | null }> {
|
||||
await ensureTaskRuntimeCacheColumns(c);
|
||||
const row = await c.db
|
||||
.select({
|
||||
gitStateJson: taskRuntime.gitStateJson,
|
||||
|
|
@ -645,7 +604,6 @@ async function readCachedGitState(c: any): Promise<{ fileChanges: Array<any>; di
|
|||
}
|
||||
|
||||
async function writeCachedGitState(c: any, gitState: { fileChanges: Array<any>; diffs: Record<string, string>; fileTree: Array<any> }): Promise<void> {
|
||||
await ensureTaskRuntimeCacheColumns(c);
|
||||
const now = Date.now();
|
||||
await c.db
|
||||
.update(taskRuntime)
|
||||
|
|
@ -687,19 +645,19 @@ async function writeSessionTranscript(c: any, sessionId: string, transcript: Arr
|
|||
});
|
||||
}
|
||||
|
||||
async function enqueueWorkbenchRefresh(
|
||||
async function enqueueWorkspaceRefresh(
|
||||
c: any,
|
||||
command: "task.command.workbench.refresh_derived" | "task.command.workbench.refresh_session_transcript",
|
||||
command: "task.command.workspace.refresh_derived" | "task.command.workspace.refresh_session_transcript",
|
||||
body: Record<string, unknown>,
|
||||
): Promise<void> {
|
||||
const self = selfTask(c);
|
||||
await self.send(command, body, { wait: false });
|
||||
}
|
||||
|
||||
async function enqueueWorkbenchEnsureSession(c: any, sessionId: string): Promise<void> {
|
||||
async function enqueueWorkspaceEnsureSession(c: any, sessionId: string): Promise<void> {
|
||||
const self = selfTask(c);
|
||||
await self.send(
|
||||
"task.command.workbench.ensure_session",
|
||||
"task.command.workspace.ensure_session",
|
||||
{
|
||||
sessionId,
|
||||
},
|
||||
|
|
@ -709,21 +667,21 @@ async function enqueueWorkbenchEnsureSession(c: any, sessionId: string): Promise
|
|||
);
|
||||
}
|
||||
|
||||
function pendingWorkbenchSessionStatus(record: any): "pending_provision" | "pending_session_create" {
|
||||
function pendingWorkspaceSessionStatus(record: any): "pending_provision" | "pending_session_create" {
|
||||
return record.activeSandboxId ? "pending_session_create" : "pending_provision";
|
||||
}
|
||||
|
||||
async function maybeScheduleWorkbenchRefreshes(c: any, record: any, sessions: Array<any>): Promise<void> {
|
||||
async function maybeScheduleWorkspaceRefreshes(c: any, record: any, sessions: Array<any>): Promise<void> {
|
||||
const gitState = await readCachedGitState(c);
|
||||
if (record.activeSandboxId && !gitState.updatedAt) {
|
||||
await enqueueWorkbenchRefresh(c, "task.command.workbench.refresh_derived", {});
|
||||
await enqueueWorkspaceRefresh(c, "task.command.workspace.refresh_derived", {});
|
||||
}
|
||||
|
||||
for (const session of sessions) {
|
||||
if (session.closed || session.status !== "ready" || !session.sandboxSessionId || session.transcriptUpdatedAt) {
|
||||
continue;
|
||||
}
|
||||
await enqueueWorkbenchRefresh(c, "task.command.workbench.refresh_session_transcript", {
|
||||
await enqueueWorkspaceRefresh(c, "task.command.workspace.refresh_session_transcript", {
|
||||
sessionId: session.sandboxSessionId,
|
||||
});
|
||||
}
|
||||
|
|
@ -756,8 +714,7 @@ async function readPullRequestSummary(c: any, branchName: string | null) {
|
|||
}
|
||||
}
|
||||
|
||||
export async function ensureWorkbenchSeeded(c: any): Promise<any> {
|
||||
await ensureTaskRuntimeCacheColumns(c);
|
||||
export async function ensureWorkspaceSeeded(c: any): Promise<any> {
|
||||
const record = await getCurrentRecord({ db: c.db, state: c.state });
|
||||
if (record.activeSessionId) {
|
||||
await ensureSessionMeta(c, {
|
||||
|
|
@ -826,13 +783,13 @@ function buildSessionDetailFromMeta(record: any, meta: any): any {
|
|||
}
|
||||
|
||||
/**
|
||||
* Builds a WorkbenchTaskSummary from local task actor state. Task actors push
|
||||
* Builds a WorkspaceTaskSummary from local task actor state. Task actors push
|
||||
* this to the parent organization actor so organization sidebar reads stay local.
|
||||
*/
|
||||
export async function buildTaskSummary(c: any): Promise<any> {
|
||||
const record = await ensureWorkbenchSeeded(c);
|
||||
const record = await ensureWorkspaceSeeded(c);
|
||||
const sessions = await listSessionMetaRows(c);
|
||||
await maybeScheduleWorkbenchRefreshes(c, record, sessions);
|
||||
await maybeScheduleWorkspaceRefreshes(c, record, sessions);
|
||||
|
||||
return {
|
||||
id: c.state.taskId,
|
||||
|
|
@ -848,14 +805,14 @@ export async function buildTaskSummary(c: any): Promise<any> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Builds a WorkbenchTaskDetail from local task actor state for direct task
|
||||
* Builds a WorkspaceTaskDetail from local task actor state for direct task
|
||||
* subscribers. This is a full replacement payload, not a patch.
|
||||
*/
|
||||
export async function buildTaskDetail(c: any): Promise<any> {
|
||||
const record = await ensureWorkbenchSeeded(c);
|
||||
const record = await ensureWorkspaceSeeded(c);
|
||||
const gitState = await readCachedGitState(c);
|
||||
const sessions = await listSessionMetaRows(c);
|
||||
await maybeScheduleWorkbenchRefreshes(c, record, sessions);
|
||||
await maybeScheduleWorkspaceRefreshes(c, record, sessions);
|
||||
const summary = await buildTaskSummary(c);
|
||||
|
||||
return {
|
||||
|
|
@ -882,13 +839,13 @@ export async function buildTaskDetail(c: any): Promise<any> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Builds a WorkbenchSessionDetail for a specific session.
|
||||
* Builds a WorkspaceSessionDetail for a specific session.
|
||||
*/
|
||||
export async function buildSessionDetail(c: any, sessionId: string): Promise<any> {
|
||||
const record = await ensureWorkbenchSeeded(c);
|
||||
const record = await ensureWorkspaceSeeded(c);
|
||||
const meta = await readSessionMeta(c, sessionId);
|
||||
if (!meta || meta.closed) {
|
||||
throw new Error(`Unknown workbench session: ${sessionId}`);
|
||||
throw new Error(`Unknown workspace session: ${sessionId}`);
|
||||
}
|
||||
|
||||
if (!meta.sandboxSessionId) {
|
||||
|
|
@ -925,7 +882,7 @@ export async function getSessionDetail(c: any, sessionId: string): Promise<any>
|
|||
}
|
||||
|
||||
/**
|
||||
* Replaces the old notifyWorkbenchUpdated pattern.
|
||||
* Replaces the old notifyWorkspaceUpdated pattern.
|
||||
*
|
||||
* The task actor emits two kinds of updates:
|
||||
* - Push summary state up to the parent organization actor so the sidebar
|
||||
|
|
@ -933,10 +890,10 @@ export async function getSessionDetail(c: any, sessionId: string): Promise<any>
|
|||
* - Broadcast full detail/session payloads down to direct task subscribers.
|
||||
*/
|
||||
export async function broadcastTaskUpdate(c: any, options?: { sessionId?: string }): Promise<void> {
|
||||
const organization = await getOrCreateOrganization(c, c.state.organizationId);
|
||||
await organization.applyTaskSummaryUpdate({ taskSummary: await buildTaskSummary(c) });
|
||||
const repository = await getOrCreateRepository(c, c.state.organizationId, c.state.repoId, c.state.repoRemote);
|
||||
await repository.applyTaskSummaryUpdate({ taskSummary: await buildTaskSummary(c) });
|
||||
c.broadcast("taskUpdated", {
|
||||
type: "taskDetailUpdated",
|
||||
type: "taskUpdated",
|
||||
detail: await buildTaskDetail(c),
|
||||
});
|
||||
|
||||
|
|
@ -948,15 +905,15 @@ export async function broadcastTaskUpdate(c: any, options?: { sessionId?: string
|
|||
}
|
||||
}
|
||||
|
||||
export async function refreshWorkbenchDerivedState(c: any): Promise<void> {
|
||||
const record = await ensureWorkbenchSeeded(c);
|
||||
const gitState = await collectWorkbenchGitState(c, record);
|
||||
export async function refreshWorkspaceDerivedState(c: any): Promise<void> {
|
||||
const record = await ensureWorkspaceSeeded(c);
|
||||
const gitState = await collectWorkspaceGitState(c, record);
|
||||
await writeCachedGitState(c, gitState);
|
||||
await broadcastTaskUpdate(c);
|
||||
}
|
||||
|
||||
export async function refreshWorkbenchSessionTranscript(c: any, sessionId: string): Promise<void> {
|
||||
const record = await ensureWorkbenchSeeded(c);
|
||||
export async function refreshWorkspaceSessionTranscript(c: any, sessionId: string): Promise<void> {
|
||||
const record = await ensureWorkspaceSeeded(c);
|
||||
const meta = (await readSessionMetaBySandboxSessionId(c, sessionId)) ?? (await readSessionMeta(c, sessionId));
|
||||
if (!meta?.sandboxSessionId) {
|
||||
return;
|
||||
|
|
@ -967,7 +924,7 @@ export async function refreshWorkbenchSessionTranscript(c: any, sessionId: strin
|
|||
await broadcastTaskUpdate(c, { sessionId: meta.sessionId });
|
||||
}
|
||||
|
||||
export async function renameWorkbenchTask(c: any, value: string): Promise<void> {
|
||||
export async function renameWorkspaceTask(c: any, value: string): Promise<void> {
|
||||
const nextTitle = value.trim();
|
||||
if (!nextTitle) {
|
||||
throw new Error("task title is required");
|
||||
|
|
@ -985,81 +942,30 @@ export async function renameWorkbenchTask(c: any, value: string): Promise<void>
|
|||
await broadcastTaskUpdate(c);
|
||||
}
|
||||
|
||||
export async function renameWorkbenchBranch(c: any, value: string): Promise<void> {
|
||||
const nextBranch = value.trim();
|
||||
if (!nextBranch) {
|
||||
throw new Error("branch name is required");
|
||||
}
|
||||
|
||||
const record = await ensureWorkbenchSeeded(c);
|
||||
if (!record.branchName) {
|
||||
throw new Error("cannot rename branch before task branch exists");
|
||||
}
|
||||
if (!record.activeSandboxId) {
|
||||
throw new Error("cannot rename branch without an active sandbox");
|
||||
}
|
||||
const activeSandbox = (record.sandboxes ?? []).find((candidate: any) => candidate.sandboxId === record.activeSandboxId) ?? null;
|
||||
if (!activeSandbox?.cwd) {
|
||||
throw new Error("cannot rename branch without a sandbox cwd");
|
||||
}
|
||||
|
||||
const renameResult = await executeInSandbox(c, {
|
||||
sandboxId: record.activeSandboxId,
|
||||
cwd: activeSandbox.cwd,
|
||||
command: [
|
||||
`git branch -m ${JSON.stringify(record.branchName)} ${JSON.stringify(nextBranch)}`,
|
||||
`if git ls-remote --exit-code --heads origin ${JSON.stringify(record.branchName)} >/dev/null 2>&1; then git push origin :${JSON.stringify(record.branchName)}; fi`,
|
||||
`git push origin ${JSON.stringify(nextBranch)}`,
|
||||
`git branch --set-upstream-to=${JSON.stringify(`origin/${nextBranch}`)} ${JSON.stringify(nextBranch)} || git push --set-upstream origin ${JSON.stringify(nextBranch)}`,
|
||||
].join(" && "),
|
||||
label: `git branch -m ${record.branchName} ${nextBranch}`,
|
||||
});
|
||||
if (renameResult.exitCode !== 0) {
|
||||
throw new Error(`branch rename failed (${renameResult.exitCode}): ${renameResult.result}`);
|
||||
}
|
||||
|
||||
await c.db
|
||||
.update(taskTable)
|
||||
.set({
|
||||
branchName: nextBranch,
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
.where(eq(taskTable.id, 1))
|
||||
.run();
|
||||
c.state.branchName = nextBranch;
|
||||
|
||||
const repository = await getOrCreateRepository(c, c.state.organizationId, c.state.repoId, c.state.repoRemote);
|
||||
await repository.registerTaskBranch({
|
||||
taskId: c.state.taskId,
|
||||
branchName: nextBranch,
|
||||
});
|
||||
await broadcastTaskUpdate(c);
|
||||
}
|
||||
|
||||
export async function createWorkbenchSession(c: any, model?: string): Promise<{ sessionId: string }> {
|
||||
export async function createWorkspaceSession(c: any, model?: string): Promise<{ sessionId: string }> {
|
||||
const sessionId = `session-${randomUUID()}`;
|
||||
const record = await ensureWorkbenchSeeded(c);
|
||||
const record = await ensureWorkspaceSeeded(c);
|
||||
await ensureSessionMeta(c, {
|
||||
sessionId,
|
||||
model: model ?? defaultModelForAgent(record.agentType),
|
||||
sandboxSessionId: null,
|
||||
status: pendingWorkbenchSessionStatus(record),
|
||||
status: pendingWorkspaceSessionStatus(record),
|
||||
created: false,
|
||||
});
|
||||
await broadcastTaskUpdate(c, { sessionId: sessionId });
|
||||
await enqueueWorkbenchEnsureSession(c, sessionId);
|
||||
await enqueueWorkspaceEnsureSession(c, sessionId);
|
||||
return { sessionId };
|
||||
}
|
||||
|
||||
export async function ensureWorkbenchSession(c: any, sessionId: string, model?: string): Promise<void> {
|
||||
export async function ensureWorkspaceSession(c: any, sessionId: string, model?: string): Promise<void> {
|
||||
const meta = await readSessionMeta(c, sessionId);
|
||||
if (!meta || meta.closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const record = await ensureWorkbenchSeeded(c);
|
||||
const record = await ensureWorkspaceSeeded(c);
|
||||
if (meta.sandboxSessionId && meta.status === "ready") {
|
||||
await enqueueWorkbenchRefresh(c, "task.command.workbench.refresh_session_transcript", {
|
||||
await enqueueWorkspaceRefresh(c, "task.command.workspace.refresh_session_transcript", {
|
||||
sessionId: meta.sandboxSessionId,
|
||||
});
|
||||
await broadcastTaskUpdate(c, { sessionId: sessionId });
|
||||
|
|
@ -1089,7 +995,7 @@ export async function ensureWorkbenchSession(c: any, sessionId: string, model?:
|
|||
status: "ready",
|
||||
errorMessage: null,
|
||||
});
|
||||
await enqueueWorkbenchRefresh(c, "task.command.workbench.refresh_session_transcript", {
|
||||
await enqueueWorkspaceRefresh(c, "task.command.workspace.refresh_session_transcript", {
|
||||
sessionId: meta.sandboxSessionId ?? sessionId,
|
||||
});
|
||||
} catch (error) {
|
||||
|
|
@ -1102,7 +1008,7 @@ export async function ensureWorkbenchSession(c: any, sessionId: string, model?:
|
|||
await broadcastTaskUpdate(c, { sessionId: sessionId });
|
||||
}
|
||||
|
||||
export async function enqueuePendingWorkbenchSessions(c: any): Promise<void> {
|
||||
export async function enqueuePendingWorkspaceSessions(c: any): Promise<void> {
|
||||
const self = selfTask(c);
|
||||
const pending = (await listSessionMetaRows(c, { includeClosed: true })).filter(
|
||||
(row) => row.closed !== true && row.status !== "ready" && row.status !== "error",
|
||||
|
|
@ -1110,7 +1016,7 @@ export async function enqueuePendingWorkbenchSessions(c: any): Promise<void> {
|
|||
|
||||
for (const row of pending) {
|
||||
await self.send(
|
||||
"task.command.workbench.ensure_session",
|
||||
"task.command.workspace.ensure_session",
|
||||
{
|
||||
sessionId: row.sessionId,
|
||||
model: row.model,
|
||||
|
|
@ -1122,7 +1028,7 @@ export async function enqueuePendingWorkbenchSessions(c: any): Promise<void> {
|
|||
}
|
||||
}
|
||||
|
||||
export async function renameWorkbenchSession(c: any, sessionId: string, title: string): Promise<void> {
|
||||
export async function renameWorkspaceSession(c: any, sessionId: string, title: string): Promise<void> {
|
||||
const trimmed = title.trim();
|
||||
if (!trimmed) {
|
||||
throw new Error("session title is required");
|
||||
|
|
@ -1133,14 +1039,14 @@ export async function renameWorkbenchSession(c: any, sessionId: string, title: s
|
|||
await broadcastTaskUpdate(c, { sessionId });
|
||||
}
|
||||
|
||||
export async function setWorkbenchSessionUnread(c: any, sessionId: string, unread: boolean): Promise<void> {
|
||||
export async function setWorkspaceSessionUnread(c: any, sessionId: string, unread: boolean): Promise<void> {
|
||||
await updateSessionMeta(c, sessionId, {
|
||||
unread: unread ? 1 : 0,
|
||||
});
|
||||
await broadcastTaskUpdate(c, { sessionId });
|
||||
}
|
||||
|
||||
export async function updateWorkbenchDraft(c: any, sessionId: string, text: string, attachments: Array<any>): Promise<void> {
|
||||
export async function updateWorkspaceDraft(c: any, sessionId: string, text: string, attachments: Array<any>): Promise<void> {
|
||||
await updateSessionMeta(c, sessionId, {
|
||||
draftText: text,
|
||||
draftAttachmentsJson: JSON.stringify(attachments),
|
||||
|
|
@ -1149,7 +1055,7 @@ export async function updateWorkbenchDraft(c: any, sessionId: string, text: stri
|
|||
await broadcastTaskUpdate(c, { sessionId });
|
||||
}
|
||||
|
||||
export async function changeWorkbenchModel(c: any, sessionId: string, model: string): Promise<void> {
|
||||
export async function changeWorkspaceModel(c: any, sessionId: string, model: string): Promise<void> {
|
||||
const meta = await readSessionMeta(c, sessionId);
|
||||
if (!meta || meta.closed) {
|
||||
return;
|
||||
|
|
@ -1159,7 +1065,7 @@ export async function changeWorkbenchModel(c: any, sessionId: string, model: str
|
|||
return;
|
||||
}
|
||||
|
||||
const record = await ensureWorkbenchSeeded(c);
|
||||
const record = await ensureWorkspaceSeeded(c);
|
||||
let nextMeta = await updateSessionMeta(c, sessionId, {
|
||||
model,
|
||||
});
|
||||
|
|
@ -1170,7 +1076,7 @@ export async function changeWorkbenchModel(c: any, sessionId: string, model: str
|
|||
await sandbox.destroySession(nextMeta.sandboxSessionId);
|
||||
nextMeta = await updateSessionMeta(c, sessionId, {
|
||||
sandboxSessionId: null,
|
||||
status: pendingWorkbenchSessionStatus(record),
|
||||
status: pendingWorkspaceSessionStatus(record),
|
||||
errorMessage: null,
|
||||
transcriptJson: "[]",
|
||||
transcriptUpdatedAt: null,
|
||||
|
|
@ -1191,20 +1097,20 @@ export async function changeWorkbenchModel(c: any, sessionId: string, model: str
|
|||
}
|
||||
} else if (nextMeta.status !== "ready") {
|
||||
nextMeta = await updateSessionMeta(c, sessionId, {
|
||||
status: pendingWorkbenchSessionStatus(record),
|
||||
status: pendingWorkspaceSessionStatus(record),
|
||||
errorMessage: null,
|
||||
});
|
||||
}
|
||||
|
||||
if (shouldEnsure) {
|
||||
await enqueueWorkbenchEnsureSession(c, sessionId);
|
||||
await enqueueWorkspaceEnsureSession(c, sessionId);
|
||||
}
|
||||
await broadcastTaskUpdate(c, { sessionId });
|
||||
}
|
||||
|
||||
export async function sendWorkbenchMessage(c: any, sessionId: string, text: string, attachments: Array<any>): Promise<void> {
|
||||
export async function sendWorkspaceMessage(c: any, sessionId: string, text: string, attachments: Array<any>): Promise<void> {
|
||||
const meta = requireSendableSessionMeta(await readSessionMeta(c, sessionId), sessionId);
|
||||
const record = await ensureWorkbenchSeeded(c);
|
||||
const record = await ensureWorkspaceSeeded(c);
|
||||
const runtime = await getTaskSandboxRuntime(c, record);
|
||||
// Skip git fetch on subsequent messages — the repo was already prepared during session
|
||||
// creation. This avoids a 5-30s network round-trip to GitHub on every prompt.
|
||||
|
|
@ -1234,25 +1140,25 @@ export async function sendWorkbenchMessage(c: any, sessionId: string, text: stri
|
|||
.where(eq(taskRuntime.id, 1))
|
||||
.run();
|
||||
|
||||
await syncWorkbenchSessionStatus(c, meta.sandboxSessionId, "running", Date.now());
|
||||
await syncWorkspaceSessionStatus(c, meta.sandboxSessionId, "running", Date.now());
|
||||
|
||||
try {
|
||||
await runtime.sandbox.sendPrompt({
|
||||
sessionId: meta.sandboxSessionId,
|
||||
prompt: prompt.join("\n\n"),
|
||||
});
|
||||
await syncWorkbenchSessionStatus(c, meta.sandboxSessionId, "idle", Date.now());
|
||||
await syncWorkspaceSessionStatus(c, meta.sandboxSessionId, "idle", Date.now());
|
||||
} catch (error) {
|
||||
await updateSessionMeta(c, sessionId, {
|
||||
status: "error",
|
||||
errorMessage: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
await syncWorkbenchSessionStatus(c, meta.sandboxSessionId, "error", Date.now());
|
||||
await syncWorkspaceSessionStatus(c, meta.sandboxSessionId, "error", Date.now());
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function stopWorkbenchSession(c: any, sessionId: string): Promise<void> {
|
||||
export async function stopWorkspaceSession(c: any, sessionId: string): Promise<void> {
|
||||
const meta = await requireReadySessionMeta(c, sessionId);
|
||||
const sandbox = getTaskSandbox(c, c.state.organizationId, stableSandboxId(c));
|
||||
await sandbox.destroySession(meta.sandboxSessionId);
|
||||
|
|
@ -1262,8 +1168,8 @@ export async function stopWorkbenchSession(c: any, sessionId: string): Promise<v
|
|||
await broadcastTaskUpdate(c, { sessionId });
|
||||
}
|
||||
|
||||
export async function syncWorkbenchSessionStatus(c: any, sessionId: string, status: "running" | "idle" | "error", at: number): Promise<void> {
|
||||
const record = await ensureWorkbenchSeeded(c);
|
||||
export async function syncWorkspaceSessionStatus(c: any, sessionId: string, status: "running" | "idle" | "error", at: number): Promise<void> {
|
||||
const record = await ensureWorkspaceSeeded(c);
|
||||
const meta = (await readSessionMetaBySandboxSessionId(c, sessionId)) ?? (await ensureSessionMeta(c, { sessionId: sessionId, sandboxSessionId: sessionId }));
|
||||
let changed = false;
|
||||
|
||||
|
|
@ -1318,18 +1224,18 @@ export async function syncWorkbenchSessionStatus(c: any, sessionId: string, stat
|
|||
}
|
||||
|
||||
if (changed) {
|
||||
await enqueueWorkbenchRefresh(c, "task.command.workbench.refresh_session_transcript", {
|
||||
await enqueueWorkspaceRefresh(c, "task.command.workspace.refresh_session_transcript", {
|
||||
sessionId,
|
||||
});
|
||||
if (status !== "running") {
|
||||
await enqueueWorkbenchRefresh(c, "task.command.workbench.refresh_derived", {});
|
||||
await enqueueWorkspaceRefresh(c, "task.command.workspace.refresh_derived", {});
|
||||
}
|
||||
await broadcastTaskUpdate(c, { sessionId: meta.sessionId });
|
||||
}
|
||||
}
|
||||
|
||||
export async function closeWorkbenchSession(c: any, sessionId: string): Promise<void> {
|
||||
const record = await ensureWorkbenchSeeded(c);
|
||||
export async function closeWorkspaceSession(c: any, sessionId: string): Promise<void> {
|
||||
const record = await ensureWorkspaceSeeded(c);
|
||||
const sessions = await listSessionMetaRows(c);
|
||||
if (sessions.filter((candidate) => candidate.closed !== true).length <= 1) {
|
||||
return;
|
||||
|
|
@ -1360,7 +1266,7 @@ export async function closeWorkbenchSession(c: any, sessionId: string): Promise<
|
|||
await broadcastTaskUpdate(c);
|
||||
}
|
||||
|
||||
export async function markWorkbenchUnread(c: any): Promise<void> {
|
||||
export async function markWorkspaceUnread(c: any): Promise<void> {
|
||||
const sessions = await listSessionMetaRows(c);
|
||||
const latest = sessions[sessions.length - 1];
|
||||
if (!latest) {
|
||||
|
|
@ -1372,8 +1278,8 @@ export async function markWorkbenchUnread(c: any): Promise<void> {
|
|||
await broadcastTaskUpdate(c, { sessionId: latest.sessionId });
|
||||
}
|
||||
|
||||
export async function publishWorkbenchPr(c: any): Promise<void> {
|
||||
const record = await ensureWorkbenchSeeded(c);
|
||||
export async function publishWorkspacePr(c: any): Promise<void> {
|
||||
const record = await ensureWorkspaceSeeded(c);
|
||||
if (!record.branchName) {
|
||||
throw new Error("cannot publish PR without a branch");
|
||||
}
|
||||
|
|
@ -1400,8 +1306,8 @@ export async function publishWorkbenchPr(c: any): Promise<void> {
|
|||
await broadcastTaskUpdate(c);
|
||||
}
|
||||
|
||||
export async function revertWorkbenchFile(c: any, path: string): Promise<void> {
|
||||
const record = await ensureWorkbenchSeeded(c);
|
||||
export async function revertWorkspaceFile(c: any, path: string): Promise<void> {
|
||||
const record = await ensureWorkspaceSeeded(c);
|
||||
if (!record.activeSandboxId) {
|
||||
throw new Error("cannot revert file without an active sandbox");
|
||||
}
|
||||
|
|
@ -1419,6 +1325,6 @@ export async function revertWorkbenchFile(c: any, path: string): Promise<void> {
|
|||
if (result.exitCode !== 0) {
|
||||
throw new Error(`file revert failed (${result.exitCode}): ${result.result}`);
|
||||
}
|
||||
await enqueueWorkbenchRefresh(c, "task.command.workbench.refresh_derived", {});
|
||||
await enqueueWorkspaceRefresh(c, "task.command.workspace.refresh_derived", {});
|
||||
await broadcastTaskUpdate(c);
|
||||
}
|
||||
|
|
@ -2,4 +2,4 @@ import { db } from "rivetkit/db/drizzle";
|
|||
import * as schema from "./schema.js";
|
||||
import migrations from "./migrations.js";
|
||||
|
||||
export const historyDb = db({ schema, migrations });
|
||||
export const userDb = db({ schema, migrations });
|
||||
|
|
@ -10,6 +10,12 @@ const journal = {
|
|||
tag: "0000_auth_user",
|
||||
breakpoints: true,
|
||||
},
|
||||
{
|
||||
idx: 1,
|
||||
when: 1773532800000,
|
||||
tag: "0001_user_task_state",
|
||||
breakpoints: true,
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
|
||||
|
|
@ -58,23 +64,39 @@ CREATE TABLE \`account\` (
|
|||
CREATE UNIQUE INDEX \`account_provider_account_idx\` ON \`account\` (\`provider_id\`, \`account_id\`);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE \`user_profiles\` (
|
||||
\`user_id\` text PRIMARY KEY NOT NULL,
|
||||
\`id\` integer PRIMARY KEY NOT NULL,
|
||||
\`user_id\` text NOT NULL,
|
||||
\`github_account_id\` text,
|
||||
\`github_login\` text,
|
||||
\`role_label\` text NOT NULL,
|
||||
\`default_model\` text DEFAULT 'claude-sonnet-4' NOT NULL,
|
||||
\`eligible_organization_ids_json\` text NOT NULL,
|
||||
\`starter_repo_status\` text NOT NULL,
|
||||
\`starter_repo_starred_at\` integer,
|
||||
\`starter_repo_skipped_at\` integer,
|
||||
\`created_at\` integer NOT NULL,
|
||||
\`updated_at\` integer NOT NULL
|
||||
\`updated_at\` integer NOT NULL,
|
||||
CONSTRAINT \`user_profiles_singleton_id_check\` CHECK(\`id\` = 1)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX \`user_profiles_user_id_idx\` ON \`user_profiles\` (\`user_id\`);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE \`session_state\` (
|
||||
\`session_id\` text PRIMARY KEY NOT NULL,
|
||||
\`active_organization_id\` text,
|
||||
\`created_at\` integer NOT NULL,
|
||||
\`updated_at\` integer NOT NULL
|
||||
);`,
|
||||
m0001: `CREATE TABLE \`user_task_state\` (
|
||||
\`task_id\` text NOT NULL,
|
||||
\`session_id\` text NOT NULL,
|
||||
\`active_session_id\` text,
|
||||
\`unread\` integer DEFAULT 0 NOT NULL,
|
||||
\`draft_text\` text DEFAULT '' NOT NULL,
|
||||
\`draft_attachments_json\` text DEFAULT '[]' NOT NULL,
|
||||
\`draft_updated_at\` integer,
|
||||
\`updated_at\` integer NOT NULL,
|
||||
PRIMARY KEY(\`task_id\`, \`session_id\`)
|
||||
);`,
|
||||
} as const,
|
||||
};
|
||||
103
foundry/packages/backend/src/actors/user/db/schema.ts
Normal file
103
foundry/packages/backend/src/actors/user/db/schema.ts
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import { check, integer, primaryKey, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
|
||||
import { sql } from "drizzle-orm";
|
||||
|
||||
/** Better Auth core model — schema defined at https://better-auth.com/docs/concepts/database */
|
||||
export const authUsers = sqliteTable("user", {
|
||||
id: text("id").notNull().primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
email: text("email").notNull(),
|
||||
emailVerified: integer("email_verified").notNull(),
|
||||
image: text("image"),
|
||||
createdAt: integer("created_at").notNull(),
|
||||
updatedAt: integer("updated_at").notNull(),
|
||||
});
|
||||
|
||||
/** Better Auth core model — schema defined at https://better-auth.com/docs/concepts/database */
|
||||
export const authSessions = sqliteTable(
|
||||
"session",
|
||||
{
|
||||
id: text("id").notNull().primaryKey(),
|
||||
token: text("token").notNull(),
|
||||
userId: text("user_id").notNull(),
|
||||
expiresAt: integer("expires_at").notNull(),
|
||||
ipAddress: text("ip_address"),
|
||||
userAgent: text("user_agent"),
|
||||
createdAt: integer("created_at").notNull(),
|
||||
updatedAt: integer("updated_at").notNull(),
|
||||
},
|
||||
(table) => ({
|
||||
tokenIdx: uniqueIndex("session_token_idx").on(table.token),
|
||||
}),
|
||||
);
|
||||
|
||||
/** Better Auth core model — schema defined at https://better-auth.com/docs/concepts/database */
|
||||
export const authAccounts = sqliteTable(
|
||||
"account",
|
||||
{
|
||||
id: text("id").notNull().primaryKey(),
|
||||
accountId: text("account_id").notNull(),
|
||||
providerId: text("provider_id").notNull(),
|
||||
userId: text("user_id").notNull(),
|
||||
accessToken: text("access_token"),
|
||||
refreshToken: text("refresh_token"),
|
||||
idToken: text("id_token"),
|
||||
accessTokenExpiresAt: integer("access_token_expires_at"),
|
||||
refreshTokenExpiresAt: integer("refresh_token_expires_at"),
|
||||
scope: text("scope"),
|
||||
password: text("password"),
|
||||
createdAt: integer("created_at").notNull(),
|
||||
updatedAt: integer("updated_at").notNull(),
|
||||
},
|
||||
(table) => ({
|
||||
providerAccountIdx: uniqueIndex("account_provider_account_idx").on(table.providerId, table.accountId),
|
||||
}),
|
||||
);
|
||||
|
||||
/** Custom Foundry table — not part of Better Auth. */
|
||||
export const userProfiles = sqliteTable(
|
||||
"user_profiles",
|
||||
{
|
||||
id: integer("id").primaryKey(),
|
||||
userId: text("user_id").notNull(),
|
||||
githubAccountId: text("github_account_id"),
|
||||
githubLogin: text("github_login"),
|
||||
roleLabel: text("role_label").notNull(),
|
||||
defaultModel: text("default_model").notNull().default("claude-sonnet-4"),
|
||||
eligibleOrganizationIdsJson: text("eligible_organization_ids_json").notNull(),
|
||||
starterRepoStatus: text("starter_repo_status").notNull(),
|
||||
starterRepoStarredAt: integer("starter_repo_starred_at"),
|
||||
starterRepoSkippedAt: integer("starter_repo_skipped_at"),
|
||||
createdAt: integer("created_at").notNull(),
|
||||
updatedAt: integer("updated_at").notNull(),
|
||||
},
|
||||
(table) => ({
|
||||
userIdIdx: uniqueIndex("user_profiles_user_id_idx").on(table.userId),
|
||||
singletonCheck: check("user_profiles_singleton_id_check", sql`${table.id} = 1`),
|
||||
}),
|
||||
);
|
||||
|
||||
/** Custom Foundry table — not part of Better Auth. */
|
||||
export const sessionState = sqliteTable("session_state", {
|
||||
sessionId: text("session_id").notNull().primaryKey(),
|
||||
activeOrganizationId: text("active_organization_id"),
|
||||
createdAt: integer("created_at").notNull(),
|
||||
updatedAt: integer("updated_at").notNull(),
|
||||
});
|
||||
|
||||
/** Custom Foundry table — not part of Better Auth. Stores per-user task/session UI state. */
|
||||
export const userTaskState = sqliteTable(
|
||||
"user_task_state",
|
||||
{
|
||||
taskId: text("task_id").notNull(),
|
||||
sessionId: text("session_id").notNull(),
|
||||
activeSessionId: text("active_session_id"),
|
||||
unread: integer("unread").notNull().default(0),
|
||||
draftText: text("draft_text").notNull().default(""),
|
||||
draftAttachmentsJson: text("draft_attachments_json").notNull().default("[]"),
|
||||
draftUpdatedAt: integer("draft_updated_at"),
|
||||
updatedAt: integer("updated_at").notNull(),
|
||||
},
|
||||
(table) => ({
|
||||
pk: primaryKey({ columns: [table.taskId, table.sessionId] }),
|
||||
}),
|
||||
);
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { and, asc, count as sqlCount, desc, eq, gt, gte, inArray, isNotNull, isNull, like, lt, lte, ne, notInArray, or } from "drizzle-orm";
|
||||
import { actor } from "rivetkit";
|
||||
import { authUserDb } from "./db/db.js";
|
||||
import { authAccounts, authSessions, authUsers, sessionState, userProfiles } from "./db/schema.js";
|
||||
import { userDb } from "./db/db.js";
|
||||
import { authAccounts, authSessions, authUsers, sessionState, userProfiles, userTaskState } from "./db/schema.js";
|
||||
|
||||
const tables = {
|
||||
user: authUsers,
|
||||
|
|
@ -9,12 +9,13 @@ const tables = {
|
|||
account: authAccounts,
|
||||
userProfiles,
|
||||
sessionState,
|
||||
userTaskState,
|
||||
} as const;
|
||||
|
||||
function tableFor(model: string) {
|
||||
const table = tables[model as keyof typeof tables];
|
||||
if (!table) {
|
||||
throw new Error(`Unsupported auth user model: ${model}`);
|
||||
throw new Error(`Unsupported user model: ${model}`);
|
||||
}
|
||||
return table as any;
|
||||
}
|
||||
|
|
@ -22,7 +23,7 @@ function tableFor(model: string) {
|
|||
function columnFor(table: any, field: string) {
|
||||
const column = table[field];
|
||||
if (!column) {
|
||||
throw new Error(`Unsupported auth user field: ${field}`);
|
||||
throw new Error(`Unsupported user field: ${field}`);
|
||||
}
|
||||
return column;
|
||||
}
|
||||
|
|
@ -150,10 +151,10 @@ async function applyJoinToRows(c: any, model: string, rows: any[], join: any) {
|
|||
return rows;
|
||||
}
|
||||
|
||||
export const authUser = actor({
|
||||
db: authUserDb,
|
||||
export const user = actor({
|
||||
db: userDb,
|
||||
options: {
|
||||
name: "Auth User",
|
||||
name: "User",
|
||||
icon: "shield",
|
||||
actionTimeout: 60_000,
|
||||
},
|
||||
|
|
@ -161,6 +162,8 @@ export const authUser = actor({
|
|||
userId: input.userId,
|
||||
}),
|
||||
actions: {
|
||||
// Better Auth adapter action — called by the Better Auth adapter in better-auth.ts.
|
||||
// Schema and behavior are constrained by Better Auth.
|
||||
async createAuthRecord(c, input: { model: string; data: Record<string, unknown> }) {
|
||||
const table = tableFor(input.model);
|
||||
await c.db
|
||||
|
|
@ -174,6 +177,8 @@ export const authUser = actor({
|
|||
.get();
|
||||
},
|
||||
|
||||
// Better Auth adapter action — called by the Better Auth adapter in better-auth.ts.
|
||||
// Schema and behavior are constrained by Better Auth.
|
||||
async findOneAuthRecord(c, input: { model: string; where: any[]; join?: any }) {
|
||||
const table = tableFor(input.model);
|
||||
const predicate = buildWhere(table, input.where);
|
||||
|
|
@ -181,6 +186,8 @@ export const authUser = actor({
|
|||
return await applyJoinToRow(c, input.model, row ?? null, input.join);
|
||||
},
|
||||
|
||||
// Better Auth adapter action — called by the Better Auth adapter in better-auth.ts.
|
||||
// Schema and behavior are constrained by Better Auth.
|
||||
async findManyAuthRecords(c, input: { model: string; where?: any[]; limit?: number; offset?: number; sortBy?: any; join?: any }) {
|
||||
const table = tableFor(input.model);
|
||||
const predicate = buildWhere(table, input.where);
|
||||
|
|
@ -202,6 +209,8 @@ export const authUser = actor({
|
|||
return await applyJoinToRows(c, input.model, rows, input.join);
|
||||
},
|
||||
|
||||
// Better Auth adapter action — called by the Better Auth adapter in better-auth.ts.
|
||||
// Schema and behavior are constrained by Better Auth.
|
||||
async updateAuthRecord(c, input: { model: string; where: any[]; update: Record<string, unknown> }) {
|
||||
const table = tableFor(input.model);
|
||||
const predicate = buildWhere(table, input.where);
|
||||
|
|
@ -216,6 +225,8 @@ export const authUser = actor({
|
|||
return await c.db.select().from(table).where(predicate).get();
|
||||
},
|
||||
|
||||
// Better Auth adapter action — called by the Better Auth adapter in better-auth.ts.
|
||||
// Schema and behavior are constrained by Better Auth.
|
||||
async updateManyAuthRecords(c, input: { model: string; where: any[]; update: Record<string, unknown> }) {
|
||||
const table = tableFor(input.model);
|
||||
const predicate = buildWhere(table, input.where);
|
||||
|
|
@ -231,6 +242,8 @@ export const authUser = actor({
|
|||
return row?.value ?? 0;
|
||||
},
|
||||
|
||||
// Better Auth adapter action — called by the Better Auth adapter in better-auth.ts.
|
||||
// Schema and behavior are constrained by Better Auth.
|
||||
async deleteAuthRecord(c, input: { model: string; where: any[] }) {
|
||||
const table = tableFor(input.model);
|
||||
const predicate = buildWhere(table, input.where);
|
||||
|
|
@ -240,6 +253,8 @@ export const authUser = actor({
|
|||
await c.db.delete(table).where(predicate).run();
|
||||
},
|
||||
|
||||
// Better Auth adapter action — called by the Better Auth adapter in better-auth.ts.
|
||||
// Schema and behavior are constrained by Better Auth.
|
||||
async deleteManyAuthRecords(c, input: { model: string; where: any[] }) {
|
||||
const table = tableFor(input.model);
|
||||
const predicate = buildWhere(table, input.where);
|
||||
|
|
@ -251,6 +266,8 @@ export const authUser = actor({
|
|||
return rows.length;
|
||||
},
|
||||
|
||||
// Better Auth adapter action — called by the Better Auth adapter in better-auth.ts.
|
||||
// Schema and behavior are constrained by Better Auth.
|
||||
async countAuthRecords(c, input: { model: string; where?: any[] }) {
|
||||
const table = tableFor(input.model);
|
||||
const predicate = buildWhere(table, input.where);
|
||||
|
|
@ -260,6 +277,7 @@ export const authUser = actor({
|
|||
return row?.value ?? 0;
|
||||
},
|
||||
|
||||
// Custom Foundry action — not part of Better Auth.
|
||||
async getAppAuthState(c, input: { sessionId: string }) {
|
||||
const session = await c.db.select().from(authSessions).where(eq(authSessions.id, input.sessionId)).get();
|
||||
if (!session) {
|
||||
|
|
@ -280,6 +298,7 @@ export const authUser = actor({
|
|||
};
|
||||
},
|
||||
|
||||
// Custom Foundry action — not part of Better Auth.
|
||||
async upsertUserProfile(
|
||||
c,
|
||||
input: {
|
||||
|
|
@ -288,6 +307,7 @@ export const authUser = actor({
|
|||
githubAccountId?: string | null;
|
||||
githubLogin?: string | null;
|
||||
roleLabel?: string;
|
||||
defaultModel?: string;
|
||||
eligibleOrganizationIdsJson?: string;
|
||||
starterRepoStatus?: string;
|
||||
starterRepoStarredAt?: number | null;
|
||||
|
|
@ -299,10 +319,12 @@ export const authUser = actor({
|
|||
await c.db
|
||||
.insert(userProfiles)
|
||||
.values({
|
||||
id: 1,
|
||||
userId: input.userId,
|
||||
githubAccountId: input.patch.githubAccountId ?? null,
|
||||
githubLogin: input.patch.githubLogin ?? null,
|
||||
roleLabel: input.patch.roleLabel ?? "GitHub user",
|
||||
defaultModel: input.patch.defaultModel ?? "claude-sonnet-4",
|
||||
eligibleOrganizationIdsJson: input.patch.eligibleOrganizationIdsJson ?? "[]",
|
||||
starterRepoStatus: input.patch.starterRepoStatus ?? "pending",
|
||||
starterRepoStarredAt: input.patch.starterRepoStarredAt ?? null,
|
||||
|
|
@ -316,6 +338,7 @@ export const authUser = actor({
|
|||
...(input.patch.githubAccountId !== undefined ? { githubAccountId: input.patch.githubAccountId } : {}),
|
||||
...(input.patch.githubLogin !== undefined ? { githubLogin: input.patch.githubLogin } : {}),
|
||||
...(input.patch.roleLabel !== undefined ? { roleLabel: input.patch.roleLabel } : {}),
|
||||
...(input.patch.defaultModel !== undefined ? { defaultModel: input.patch.defaultModel } : {}),
|
||||
...(input.patch.eligibleOrganizationIdsJson !== undefined ? { eligibleOrganizationIdsJson: input.patch.eligibleOrganizationIdsJson } : {}),
|
||||
...(input.patch.starterRepoStatus !== undefined ? { starterRepoStatus: input.patch.starterRepoStatus } : {}),
|
||||
...(input.patch.starterRepoStarredAt !== undefined ? { starterRepoStarredAt: input.patch.starterRepoStarredAt } : {}),
|
||||
|
|
@ -328,6 +351,7 @@ export const authUser = actor({
|
|||
return await c.db.select().from(userProfiles).where(eq(userProfiles.userId, input.userId)).get();
|
||||
},
|
||||
|
||||
// Custom Foundry action — not part of Better Auth.
|
||||
async upsertSessionState(c, input: { sessionId: string; activeOrganizationId: string | null }) {
|
||||
const now = Date.now();
|
||||
await c.db
|
||||
|
|
@ -349,5 +373,101 @@ export const authUser = actor({
|
|||
|
||||
return await c.db.select().from(sessionState).where(eq(sessionState.sessionId, input.sessionId)).get();
|
||||
},
|
||||
|
||||
// Custom Foundry action — not part of Better Auth.
|
||||
async getTaskState(c, input: { taskId: string }) {
|
||||
const rows = await c.db.select().from(userTaskState).where(eq(userTaskState.taskId, input.taskId)).all();
|
||||
const activeSessionId = rows.find((row) => typeof row.activeSessionId === "string" && row.activeSessionId.length > 0)?.activeSessionId ?? null;
|
||||
return {
|
||||
taskId: input.taskId,
|
||||
activeSessionId,
|
||||
sessions: rows.map((row) => ({
|
||||
sessionId: row.sessionId,
|
||||
unread: row.unread === 1,
|
||||
draftText: row.draftText,
|
||||
draftAttachmentsJson: row.draftAttachmentsJson,
|
||||
draftUpdatedAt: row.draftUpdatedAt ?? null,
|
||||
updatedAt: row.updatedAt,
|
||||
})),
|
||||
};
|
||||
},
|
||||
|
||||
// Custom Foundry action — not part of Better Auth.
|
||||
async upsertTaskState(
|
||||
c,
|
||||
input: {
|
||||
taskId: string;
|
||||
sessionId: string;
|
||||
patch: {
|
||||
activeSessionId?: string | null;
|
||||
unread?: boolean;
|
||||
draftText?: string;
|
||||
draftAttachmentsJson?: string;
|
||||
draftUpdatedAt?: number | null;
|
||||
};
|
||||
},
|
||||
) {
|
||||
const now = Date.now();
|
||||
const existing = await c.db
|
||||
.select()
|
||||
.from(userTaskState)
|
||||
.where(and(eq(userTaskState.taskId, input.taskId), eq(userTaskState.sessionId, input.sessionId)))
|
||||
.get();
|
||||
|
||||
if (input.patch.activeSessionId !== undefined) {
|
||||
await c.db
|
||||
.update(userTaskState)
|
||||
.set({
|
||||
activeSessionId: input.patch.activeSessionId,
|
||||
updatedAt: now,
|
||||
})
|
||||
.where(eq(userTaskState.taskId, input.taskId))
|
||||
.run();
|
||||
}
|
||||
|
||||
await c.db
|
||||
.insert(userTaskState)
|
||||
.values({
|
||||
taskId: input.taskId,
|
||||
sessionId: input.sessionId,
|
||||
activeSessionId: input.patch.activeSessionId ?? existing?.activeSessionId ?? null,
|
||||
unread: input.patch.unread !== undefined ? (input.patch.unread ? 1 : 0) : (existing?.unread ?? 0),
|
||||
draftText: input.patch.draftText ?? existing?.draftText ?? "",
|
||||
draftAttachmentsJson: input.patch.draftAttachmentsJson ?? existing?.draftAttachmentsJson ?? "[]",
|
||||
draftUpdatedAt: input.patch.draftUpdatedAt === undefined ? (existing?.draftUpdatedAt ?? null) : input.patch.draftUpdatedAt,
|
||||
updatedAt: now,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [userTaskState.taskId, userTaskState.sessionId],
|
||||
set: {
|
||||
...(input.patch.activeSessionId !== undefined ? { activeSessionId: input.patch.activeSessionId } : {}),
|
||||
...(input.patch.unread !== undefined ? { unread: input.patch.unread ? 1 : 0 } : {}),
|
||||
...(input.patch.draftText !== undefined ? { draftText: input.patch.draftText } : {}),
|
||||
...(input.patch.draftAttachmentsJson !== undefined ? { draftAttachmentsJson: input.patch.draftAttachmentsJson } : {}),
|
||||
...(input.patch.draftUpdatedAt !== undefined ? { draftUpdatedAt: input.patch.draftUpdatedAt } : {}),
|
||||
updatedAt: now,
|
||||
},
|
||||
})
|
||||
.run();
|
||||
|
||||
return await c.db
|
||||
.select()
|
||||
.from(userTaskState)
|
||||
.where(and(eq(userTaskState.taskId, input.taskId), eq(userTaskState.sessionId, input.sessionId)))
|
||||
.get();
|
||||
},
|
||||
|
||||
// Custom Foundry action — not part of Better Auth.
|
||||
async deleteTaskState(c, input: { taskId: string; sessionId?: string }) {
|
||||
if (input.sessionId) {
|
||||
await c.db
|
||||
.delete(userTaskState)
|
||||
.where(and(eq(userTaskState.taskId, input.taskId), eq(userTaskState.sessionId, input.sessionId)))
|
||||
.run();
|
||||
return;
|
||||
}
|
||||
|
||||
await c.db.delete(userTaskState).where(eq(userTaskState.taskId, input.taskId)).run();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { betterAuth } from "better-auth";
|
||||
import { createAdapterFactory } from "better-auth/adapters";
|
||||
import { APP_SHELL_ORGANIZATION_ID } from "../actors/organization/app-shell.js";
|
||||
import { authUserKey, organizationKey } from "../actors/keys.js";
|
||||
import { organizationKey, userKey } from "../actors/keys.js";
|
||||
import { logger } from "../logging.js";
|
||||
|
||||
const AUTH_BASE_PATH = "/v1/auth";
|
||||
|
|
@ -75,7 +75,7 @@ export function initBetterAuthService(actorClient: any, options: { apiUrl: strin
|
|||
}
|
||||
|
||||
// getOrCreate is intentional here: the adapter runs during Better Auth callbacks
|
||||
// which can fire before any explicit create path. The app organization and auth user
|
||||
// which can fire before any explicit create path. The app organization and user
|
||||
// actors must exist by the time the adapter needs them.
|
||||
const appOrganization = () =>
|
||||
actorClient.organization.getOrCreate(organizationKey(APP_SHELL_ORGANIZATION_ID), {
|
||||
|
|
@ -83,9 +83,9 @@ export function initBetterAuthService(actorClient: any, options: { apiUrl: strin
|
|||
});
|
||||
|
||||
// getOrCreate is intentional: Better Auth creates user records during OAuth
|
||||
// callbacks, so the auth-user actor must be lazily provisioned on first access.
|
||||
const getAuthUser = async (userId: string) =>
|
||||
await actorClient.authUser.getOrCreate(authUserKey(userId), {
|
||||
// callbacks, so the user actor must be lazily provisioned on first access.
|
||||
const getUser = async (userId: string) =>
|
||||
await actorClient.user.getOrCreate(userKey(userId), {
|
||||
createWithInput: { userId },
|
||||
});
|
||||
|
||||
|
|
@ -178,7 +178,7 @@ export function initBetterAuthService(actorClient: any, options: { apiUrl: strin
|
|||
throw new Error(`Unable to resolve auth actor for create(${model})`);
|
||||
}
|
||||
|
||||
const userActor = await getAuthUser(userId);
|
||||
const userActor = await getUser(userId);
|
||||
const created = await userActor.createAuthRecord({ model, data: transformed });
|
||||
const organization = await appOrganization();
|
||||
|
||||
|
|
@ -220,7 +220,7 @@ export function initBetterAuthService(actorClient: any, options: { apiUrl: strin
|
|||
return null;
|
||||
}
|
||||
|
||||
const userActor = await getAuthUser(userId);
|
||||
const userActor = await getUser(userId);
|
||||
const found = await userActor.findOneAuthRecord({ model, where: transformedWhere, join });
|
||||
return found ? ((await transformOutput(found, model, undefined, join)) as any) : null;
|
||||
},
|
||||
|
|
@ -259,7 +259,7 @@ export function initBetterAuthService(actorClient: any, options: { apiUrl: strin
|
|||
|
||||
const rows = [];
|
||||
for (const [userId, tokens] of byUser) {
|
||||
const userActor = await getAuthUser(userId);
|
||||
const userActor = await getUser(userId);
|
||||
const scopedWhere = transformedWhere.map((entry: any) =>
|
||||
entry.field === "token" && entry.operator === "in" ? { ...entry, value: tokens } : entry,
|
||||
);
|
||||
|
|
@ -275,7 +275,7 @@ export function initBetterAuthService(actorClient: any, options: { apiUrl: strin
|
|||
return [];
|
||||
}
|
||||
|
||||
const userActor = await getAuthUser(userId);
|
||||
const userActor = await getUser(userId);
|
||||
const found = await userActor.findManyAuthRecords({ model, where: transformedWhere, limit, sortBy, offset, join });
|
||||
return await Promise.all(found.map(async (row: any) => await transformOutput(row, model, undefined, join)));
|
||||
},
|
||||
|
|
@ -292,7 +292,7 @@ export function initBetterAuthService(actorClient: any, options: { apiUrl: strin
|
|||
return null;
|
||||
}
|
||||
|
||||
const userActor = await getAuthUser(userId);
|
||||
const userActor = await getUser(userId);
|
||||
const before =
|
||||
model === "user"
|
||||
? await userActor.findOneAuthRecord({ model, where: transformedWhere })
|
||||
|
|
@ -345,7 +345,7 @@ export function initBetterAuthService(actorClient: any, options: { apiUrl: strin
|
|||
return 0;
|
||||
}
|
||||
|
||||
const userActor = await getAuthUser(userId);
|
||||
const userActor = await getUser(userId);
|
||||
return await userActor.updateManyAuthRecords({ model, where: transformedWhere, update: transformedUpdate });
|
||||
},
|
||||
|
||||
|
|
@ -361,7 +361,7 @@ export function initBetterAuthService(actorClient: any, options: { apiUrl: strin
|
|||
return;
|
||||
}
|
||||
|
||||
const userActor = await getAuthUser(userId);
|
||||
const userActor = await getUser(userId);
|
||||
const organization = await appOrganization();
|
||||
const before = await userActor.findOneAuthRecord({ model, where: transformedWhere });
|
||||
await userActor.deleteAuthRecord({ model, where: transformedWhere });
|
||||
|
|
@ -397,7 +397,7 @@ export function initBetterAuthService(actorClient: any, options: { apiUrl: strin
|
|||
if (!userId) {
|
||||
return 0;
|
||||
}
|
||||
const userActor = await getAuthUser(userId);
|
||||
const userActor = await getUser(userId);
|
||||
const organization = await appOrganization();
|
||||
const sessions = await userActor.findManyAuthRecords({ model, where: transformedWhere, limit: 5000 });
|
||||
const deleted = await userActor.deleteManyAuthRecords({ model, where: transformedWhere });
|
||||
|
|
@ -415,7 +415,7 @@ export function initBetterAuthService(actorClient: any, options: { apiUrl: strin
|
|||
return 0;
|
||||
}
|
||||
|
||||
const userActor = await getAuthUser(userId);
|
||||
const userActor = await getUser(userId);
|
||||
const deleted = await userActor.deleteManyAuthRecords({ model, where: transformedWhere });
|
||||
return deleted;
|
||||
},
|
||||
|
|
@ -431,7 +431,7 @@ export function initBetterAuthService(actorClient: any, options: { apiUrl: strin
|
|||
return 0;
|
||||
}
|
||||
|
||||
const userActor = await getAuthUser(userId);
|
||||
const userActor = await getUser(userId);
|
||||
return await userActor.countAuthRecords({ model, where: transformedWhere });
|
||||
},
|
||||
};
|
||||
|
|
@ -481,12 +481,12 @@ export function initBetterAuthService(actorClient: any, options: { apiUrl: strin
|
|||
if (!route?.userId) {
|
||||
return null;
|
||||
}
|
||||
const userActor = await getAuthUser(route.userId);
|
||||
const userActor = await getUser(route.userId);
|
||||
return await userActor.getAppAuthState({ sessionId });
|
||||
},
|
||||
|
||||
async upsertUserProfile(userId: string, patch: Record<string, unknown>) {
|
||||
const userActor = await getAuthUser(userId);
|
||||
const userActor = await getUser(userId);
|
||||
return await userActor.upsertUserProfile({ userId, patch });
|
||||
},
|
||||
|
||||
|
|
@ -495,7 +495,7 @@ export function initBetterAuthService(actorClient: any, options: { apiUrl: strin
|
|||
if (!authState?.user?.id) {
|
||||
throw new Error(`Unknown auth session ${sessionId}`);
|
||||
}
|
||||
const userActor = await getAuthUser(authState.user.id);
|
||||
const userActor = await getUser(authState.user.id);
|
||||
return await userActor.upsertSessionState({ sessionId, activeOrganizationId });
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { githubDataKey, historyKey, organizationKey, repositoryKey, taskKey, taskSandboxKey } from "../src/actors/keys.js";
|
||||
import { auditLogKey, githubDataKey, organizationKey, repositoryKey, taskKey, taskSandboxKey } from "../src/actors/keys.js";
|
||||
|
||||
describe("actor keys", () => {
|
||||
it("prefixes every key with organization namespace", () => {
|
||||
|
|
@ -8,7 +8,7 @@ describe("actor keys", () => {
|
|||
repositoryKey("default", "repo"),
|
||||
taskKey("default", "repo", "task"),
|
||||
taskSandboxKey("default", "sbx"),
|
||||
historyKey("default", "repo"),
|
||||
auditLogKey("default", "repo"),
|
||||
githubDataKey("default"),
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { requireSendableSessionMeta, shouldMarkSessionUnreadForStatus, shouldRecreateSessionForModelChange } from "../src/actors/task/workbench.js";
|
||||
import { requireSendableSessionMeta, shouldMarkSessionUnreadForStatus, shouldRecreateSessionForModelChange } from "../src/actors/task/workspace.js";
|
||||
|
||||
describe("workbench unread status transitions", () => {
|
||||
describe("workspace unread status transitions", () => {
|
||||
it("marks unread when a running session first becomes idle", () => {
|
||||
expect(shouldMarkSessionUnreadForStatus({ thinkingSinceMs: Date.now() - 1_000 }, "idle")).toBe(true);
|
||||
});
|
||||
|
|
@ -15,7 +15,7 @@ describe("workbench unread status transitions", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("workbench model changes", () => {
|
||||
describe("workspace model changes", () => {
|
||||
it("recreates an unused ready session so the selected model takes effect", () => {
|
||||
expect(
|
||||
shouldRecreateSessionForModelChange({
|
||||
|
|
@ -58,9 +58,9 @@ describe("workbench model changes", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("workbench send readiness", () => {
|
||||
describe("workspace send readiness", () => {
|
||||
it("rejects unknown sessions", () => {
|
||||
expect(() => requireSendableSessionMeta(null, "session-1")).toThrow("Unknown workbench session: session-1");
|
||||
expect(() => requireSendableSessionMeta(null, "session-1")).toThrow("Unknown workspace session: session-1");
|
||||
});
|
||||
|
||||
it("rejects pending sessions", () => {
|
||||
|
|
@ -10,8 +10,8 @@
|
|||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"test:e2e:full": "HF_ENABLE_DAEMON_FULL_E2E=1 vitest run test/e2e/full-integration-e2e.test.ts",
|
||||
"test:e2e:workbench": "HF_ENABLE_DAEMON_WORKBENCH_E2E=1 vitest run test/e2e/workbench-e2e.test.ts",
|
||||
"test:e2e:workbench-load": "HF_ENABLE_DAEMON_WORKBENCH_LOAD_E2E=1 vitest run test/e2e/workbench-load-e2e.test.ts"
|
||||
"test:e2e:workspace": "HF_ENABLE_DAEMON_WORKBENCH_E2E=1 vitest run test/e2e/workspace-e2e.test.ts",
|
||||
"test:e2e:workspace-load": "HF_ENABLE_DAEMON_WORKBENCH_LOAD_E2E=1 vitest run test/e2e/workspace-load-e2e.test.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sandbox-agent/foundry-shared": "workspace:*",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import type {
|
|||
FoundryOrganization,
|
||||
FoundryUser,
|
||||
UpdateFoundryOrganizationProfileInput,
|
||||
WorkspaceModelId,
|
||||
} from "@sandbox-agent/foundry-shared";
|
||||
import type { BackendClient } from "./backend-client.js";
|
||||
import { getMockFoundryAppClient } from "./mock-app.js";
|
||||
|
|
@ -17,6 +18,7 @@ export interface FoundryAppClient {
|
|||
skipStarterRepo(): Promise<void>;
|
||||
starStarterRepo(organizationId: string): Promise<void>;
|
||||
selectOrganization(organizationId: string): Promise<void>;
|
||||
setDefaultModel(model: WorkspaceModelId): Promise<void>;
|
||||
updateOrganizationProfile(input: UpdateFoundryOrganizationProfileInput): Promise<void>;
|
||||
triggerGithubSync(organizationId: string): Promise<void>;
|
||||
completeHostedCheckout(organizationId: string, planId: FoundryBillingPlanId): Promise<void>;
|
||||
|
|
|
|||
|
|
@ -10,25 +10,25 @@ import type {
|
|||
SandboxProcessesEvent,
|
||||
TaskRecord,
|
||||
TaskSummary,
|
||||
TaskWorkbenchChangeModelInput,
|
||||
TaskWorkbenchCreateTaskInput,
|
||||
TaskWorkbenchCreateTaskResponse,
|
||||
TaskWorkbenchDiffInput,
|
||||
TaskWorkbenchRenameInput,
|
||||
TaskWorkbenchRenameSessionInput,
|
||||
TaskWorkbenchSelectInput,
|
||||
TaskWorkbenchSetSessionUnreadInput,
|
||||
TaskWorkbenchSendMessageInput,
|
||||
TaskWorkbenchSnapshot,
|
||||
TaskWorkbenchSessionInput,
|
||||
TaskWorkbenchUpdateDraftInput,
|
||||
TaskWorkspaceChangeModelInput,
|
||||
TaskWorkspaceCreateTaskInput,
|
||||
TaskWorkspaceCreateTaskResponse,
|
||||
TaskWorkspaceDiffInput,
|
||||
TaskWorkspaceRenameInput,
|
||||
TaskWorkspaceRenameSessionInput,
|
||||
TaskWorkspaceSelectInput,
|
||||
TaskWorkspaceSetSessionUnreadInput,
|
||||
TaskWorkspaceSendMessageInput,
|
||||
TaskWorkspaceSnapshot,
|
||||
TaskWorkspaceSessionInput,
|
||||
TaskWorkspaceUpdateDraftInput,
|
||||
TaskEvent,
|
||||
WorkbenchTaskDetail,
|
||||
WorkbenchTaskSummary,
|
||||
WorkbenchSessionDetail,
|
||||
WorkspaceTaskDetail,
|
||||
WorkspaceTaskSummary,
|
||||
WorkspaceSessionDetail,
|
||||
OrganizationEvent,
|
||||
OrganizationSummarySnapshot,
|
||||
HistoryEvent,
|
||||
AuditLogEvent as HistoryEvent,
|
||||
HistoryQueryInput,
|
||||
SandboxProviderId,
|
||||
RepoOverview,
|
||||
|
|
@ -37,6 +37,7 @@ import type {
|
|||
StarSandboxAgentRepoResult,
|
||||
SwitchResult,
|
||||
UpdateFoundryOrganizationProfileInput,
|
||||
WorkspaceModelId,
|
||||
} from "@sandbox-agent/foundry-shared";
|
||||
import type { ProcessCreateRequest, ProcessInfo, ProcessLogFollowQuery, ProcessLogsResponse, ProcessSignalQuery } from "sandbox-agent";
|
||||
import { createMockBackendClient } from "./mock/backend-client.js";
|
||||
|
|
@ -78,39 +79,36 @@ interface OrganizationHandle {
|
|||
createTask(input: CreateTaskInput): Promise<TaskRecord>;
|
||||
listTasks(input: { organizationId: string; repoId?: string }): Promise<TaskSummary[]>;
|
||||
getRepoOverview(input: { organizationId: string; repoId: string }): Promise<RepoOverview>;
|
||||
history(input: HistoryQueryInput): Promise<HistoryEvent[]>;
|
||||
switchTask(taskId: string): Promise<SwitchResult>;
|
||||
getTask(input: { organizationId: string; taskId: string }): Promise<TaskRecord>;
|
||||
attachTask(input: { organizationId: string; taskId: string; reason?: string }): Promise<{ target: string; sessionId: string | null }>;
|
||||
pushTask(input: { organizationId: string; taskId: string; reason?: string }): Promise<void>;
|
||||
syncTask(input: { organizationId: string; taskId: string; reason?: string }): Promise<void>;
|
||||
mergeTask(input: { organizationId: string; taskId: string; reason?: string }): Promise<void>;
|
||||
archiveTask(input: { organizationId: string; taskId: string; reason?: string }): Promise<void>;
|
||||
killTask(input: { organizationId: string; taskId: string; reason?: string }): Promise<void>;
|
||||
auditLog(input: HistoryQueryInput): Promise<HistoryEvent[]>;
|
||||
switchTask(input: { repoId: string; taskId: string }): Promise<SwitchResult>;
|
||||
getTask(input: { organizationId: string; repoId: string; taskId: string }): Promise<TaskRecord>;
|
||||
attachTask(input: { organizationId: string; repoId: string; taskId: string; reason?: string }): Promise<{ target: string; sessionId: string | null }>;
|
||||
pushTask(input: { organizationId: string; repoId: string; taskId: string; reason?: string }): Promise<void>;
|
||||
syncTask(input: { organizationId: string; repoId: string; taskId: string; reason?: string }): Promise<void>;
|
||||
mergeTask(input: { organizationId: string; repoId: string; taskId: string; reason?: string }): Promise<void>;
|
||||
archiveTask(input: { organizationId: string; repoId: string; taskId: string; reason?: string }): Promise<void>;
|
||||
killTask(input: { organizationId: string; repoId: string; taskId: string; reason?: string }): Promise<void>;
|
||||
useOrganization(input: { organizationId: string }): Promise<{ organizationId: string }>;
|
||||
starSandboxAgentRepo(input: StarSandboxAgentRepoInput): Promise<StarSandboxAgentRepoResult>;
|
||||
getOrganizationSummary(input: { organizationId: string }): Promise<OrganizationSummarySnapshot>;
|
||||
applyTaskSummaryUpdate(input: { taskSummary: WorkbenchTaskSummary }): Promise<void>;
|
||||
removeTaskSummary(input: { taskId: string }): Promise<void>;
|
||||
reconcileWorkbenchState(input: { organizationId: string }): Promise<OrganizationSummarySnapshot>;
|
||||
createWorkbenchTask(input: TaskWorkbenchCreateTaskInput): Promise<TaskWorkbenchCreateTaskResponse>;
|
||||
markWorkbenchUnread(input: TaskWorkbenchSelectInput): Promise<void>;
|
||||
renameWorkbenchTask(input: TaskWorkbenchRenameInput): Promise<void>;
|
||||
renameWorkbenchBranch(input: TaskWorkbenchRenameInput): Promise<void>;
|
||||
createWorkbenchSession(input: TaskWorkbenchSelectInput & { model?: string }): Promise<{ sessionId: string }>;
|
||||
renameWorkbenchSession(input: TaskWorkbenchRenameSessionInput): Promise<void>;
|
||||
setWorkbenchSessionUnread(input: TaskWorkbenchSetSessionUnreadInput): Promise<void>;
|
||||
updateWorkbenchDraft(input: TaskWorkbenchUpdateDraftInput): Promise<void>;
|
||||
changeWorkbenchModel(input: TaskWorkbenchChangeModelInput): Promise<void>;
|
||||
sendWorkbenchMessage(input: TaskWorkbenchSendMessageInput): Promise<void>;
|
||||
stopWorkbenchSession(input: TaskWorkbenchSessionInput): Promise<void>;
|
||||
closeWorkbenchSession(input: TaskWorkbenchSessionInput): Promise<void>;
|
||||
publishWorkbenchPr(input: TaskWorkbenchSelectInput): Promise<void>;
|
||||
revertWorkbenchFile(input: TaskWorkbenchDiffInput): Promise<void>;
|
||||
reloadGithubOrganization(): Promise<void>;
|
||||
reloadGithubPullRequests(): Promise<void>;
|
||||
reloadGithubRepository(input: { repoId: string }): Promise<void>;
|
||||
reloadGithubPullRequest(input: { repoId: string; prNumber: number }): Promise<void>;
|
||||
adminReconcileWorkspaceState(input: { organizationId: string }): Promise<OrganizationSummarySnapshot>;
|
||||
createWorkspaceTask(input: TaskWorkspaceCreateTaskInput): Promise<TaskWorkspaceCreateTaskResponse>;
|
||||
markWorkspaceUnread(input: TaskWorkspaceSelectInput): Promise<void>;
|
||||
renameWorkspaceTask(input: TaskWorkspaceRenameInput): Promise<void>;
|
||||
createWorkspaceSession(input: TaskWorkspaceSelectInput & { model?: string }): Promise<{ sessionId: string }>;
|
||||
renameWorkspaceSession(input: TaskWorkspaceRenameSessionInput): Promise<void>;
|
||||
setWorkspaceSessionUnread(input: TaskWorkspaceSetSessionUnreadInput): Promise<void>;
|
||||
updateWorkspaceDraft(input: TaskWorkspaceUpdateDraftInput): Promise<void>;
|
||||
changeWorkspaceModel(input: TaskWorkspaceChangeModelInput): Promise<void>;
|
||||
sendWorkspaceMessage(input: TaskWorkspaceSendMessageInput): Promise<void>;
|
||||
stopWorkspaceSession(input: TaskWorkspaceSessionInput): Promise<void>;
|
||||
closeWorkspaceSession(input: TaskWorkspaceSessionInput): Promise<void>;
|
||||
publishWorkspacePr(input: TaskWorkspaceSelectInput): Promise<void>;
|
||||
revertWorkspaceFile(input: TaskWorkspaceDiffInput): Promise<void>;
|
||||
adminReloadGithubOrganization(): Promise<void>;
|
||||
adminReloadGithubPullRequests(): Promise<void>;
|
||||
adminReloadGithubRepository(input: { repoId: string }): Promise<void>;
|
||||
adminReloadGithubPullRequest(input: { repoId: string; prNumber: number }): Promise<void>;
|
||||
}
|
||||
|
||||
interface AppOrganizationHandle {
|
||||
|
|
@ -119,6 +117,7 @@ interface AppOrganizationHandle {
|
|||
skipAppStarterRepo(input: { sessionId: string }): Promise<FoundryAppSnapshot>;
|
||||
starAppStarterRepo(input: { sessionId: string; organizationId: string }): Promise<FoundryAppSnapshot>;
|
||||
selectAppOrganization(input: { sessionId: string; organizationId: string }): Promise<FoundryAppSnapshot>;
|
||||
setAppDefaultModel(input: { sessionId: string; defaultModel: WorkspaceModelId }): Promise<FoundryAppSnapshot>;
|
||||
updateAppOrganizationProfile(input: UpdateFoundryOrganizationProfileInput & { sessionId: string }): Promise<FoundryAppSnapshot>;
|
||||
triggerAppRepoImport(input: { sessionId: string; organizationId: string }): Promise<FoundryAppSnapshot>;
|
||||
beginAppGithubInstall(input: { sessionId: string; organizationId: string }): Promise<{ url: string }>;
|
||||
|
|
@ -130,9 +129,9 @@ interface AppOrganizationHandle {
|
|||
}
|
||||
|
||||
interface TaskHandle {
|
||||
getTaskSummary(): Promise<WorkbenchTaskSummary>;
|
||||
getTaskDetail(): Promise<WorkbenchTaskDetail>;
|
||||
getSessionDetail(input: { sessionId: string }): Promise<WorkbenchSessionDetail>;
|
||||
getTaskSummary(): Promise<WorkspaceTaskSummary>;
|
||||
getTaskDetail(): Promise<WorkspaceTaskDetail>;
|
||||
getSessionDetail(input: { sessionId: string }): Promise<WorkspaceSessionDetail>;
|
||||
connect(): ActorConn;
|
||||
}
|
||||
|
||||
|
|
@ -192,6 +191,7 @@ export interface BackendClient {
|
|||
skipAppStarterRepo(): Promise<FoundryAppSnapshot>;
|
||||
starAppStarterRepo(organizationId: string): Promise<FoundryAppSnapshot>;
|
||||
selectAppOrganization(organizationId: string): Promise<FoundryAppSnapshot>;
|
||||
setAppDefaultModel(defaultModel: WorkspaceModelId): Promise<FoundryAppSnapshot>;
|
||||
updateAppOrganizationProfile(input: UpdateFoundryOrganizationProfileInput): Promise<FoundryAppSnapshot>;
|
||||
triggerAppRepoImport(organizationId: string): Promise<FoundryAppSnapshot>;
|
||||
reconnectAppGithub(organizationId: string): Promise<void>;
|
||||
|
|
@ -204,11 +204,11 @@ export interface BackendClient {
|
|||
createTask(input: CreateTaskInput): Promise<TaskRecord>;
|
||||
listTasks(organizationId: string, repoId?: string): Promise<TaskSummary[]>;
|
||||
getRepoOverview(organizationId: string, repoId: string): Promise<RepoOverview>;
|
||||
getTask(organizationId: string, taskId: string): Promise<TaskRecord>;
|
||||
getTask(organizationId: string, repoId: string, taskId: string): Promise<TaskRecord>;
|
||||
listHistory(input: HistoryQueryInput): Promise<HistoryEvent[]>;
|
||||
switchTask(organizationId: string, taskId: string): Promise<SwitchResult>;
|
||||
attachTask(organizationId: string, taskId: string): Promise<{ target: string; sessionId: string | null }>;
|
||||
runAction(organizationId: string, taskId: string, action: TaskAction): Promise<void>;
|
||||
switchTask(organizationId: string, repoId: string, taskId: string): Promise<SwitchResult>;
|
||||
attachTask(organizationId: string, repoId: string, taskId: string): Promise<{ target: string; sessionId: string | null }>;
|
||||
runAction(organizationId: string, repoId: string, taskId: string, action: TaskAction): Promise<void>;
|
||||
createSandboxSession(input: {
|
||||
organizationId: string;
|
||||
sandboxProviderId: SandboxProviderId;
|
||||
|
|
@ -280,28 +280,27 @@ export interface BackendClient {
|
|||
): Promise<{ sandboxProviderId: SandboxProviderId; sandboxId: string; state: string; at: number }>;
|
||||
getSandboxAgentConnection(organizationId: string, sandboxProviderId: SandboxProviderId, sandboxId: string): Promise<{ endpoint: string; token?: string }>;
|
||||
getOrganizationSummary(organizationId: string): Promise<OrganizationSummarySnapshot>;
|
||||
getTaskDetail(organizationId: string, repoId: string, taskId: string): Promise<WorkbenchTaskDetail>;
|
||||
getSessionDetail(organizationId: string, repoId: string, taskId: string, sessionId: string): Promise<WorkbenchSessionDetail>;
|
||||
getWorkbench(organizationId: string): Promise<TaskWorkbenchSnapshot>;
|
||||
subscribeWorkbench(organizationId: string, listener: () => void): () => void;
|
||||
createWorkbenchTask(organizationId: string, input: TaskWorkbenchCreateTaskInput): Promise<TaskWorkbenchCreateTaskResponse>;
|
||||
markWorkbenchUnread(organizationId: string, input: TaskWorkbenchSelectInput): Promise<void>;
|
||||
renameWorkbenchTask(organizationId: string, input: TaskWorkbenchRenameInput): Promise<void>;
|
||||
renameWorkbenchBranch(organizationId: string, input: TaskWorkbenchRenameInput): Promise<void>;
|
||||
createWorkbenchSession(organizationId: string, input: TaskWorkbenchSelectInput & { model?: string }): Promise<{ sessionId: string }>;
|
||||
renameWorkbenchSession(organizationId: string, input: TaskWorkbenchRenameSessionInput): Promise<void>;
|
||||
setWorkbenchSessionUnread(organizationId: string, input: TaskWorkbenchSetSessionUnreadInput): Promise<void>;
|
||||
updateWorkbenchDraft(organizationId: string, input: TaskWorkbenchUpdateDraftInput): Promise<void>;
|
||||
changeWorkbenchModel(organizationId: string, input: TaskWorkbenchChangeModelInput): Promise<void>;
|
||||
sendWorkbenchMessage(organizationId: string, input: TaskWorkbenchSendMessageInput): Promise<void>;
|
||||
stopWorkbenchSession(organizationId: string, input: TaskWorkbenchSessionInput): Promise<void>;
|
||||
closeWorkbenchSession(organizationId: string, input: TaskWorkbenchSessionInput): Promise<void>;
|
||||
publishWorkbenchPr(organizationId: string, input: TaskWorkbenchSelectInput): Promise<void>;
|
||||
revertWorkbenchFile(organizationId: string, input: TaskWorkbenchDiffInput): Promise<void>;
|
||||
reloadGithubOrganization(organizationId: string): Promise<void>;
|
||||
reloadGithubPullRequests(organizationId: string): Promise<void>;
|
||||
reloadGithubRepository(organizationId: string, repoId: string): Promise<void>;
|
||||
reloadGithubPullRequest(organizationId: string, repoId: string, prNumber: number): Promise<void>;
|
||||
getTaskDetail(organizationId: string, repoId: string, taskId: string): Promise<WorkspaceTaskDetail>;
|
||||
getSessionDetail(organizationId: string, repoId: string, taskId: string, sessionId: string): Promise<WorkspaceSessionDetail>;
|
||||
getWorkspace(organizationId: string): Promise<TaskWorkspaceSnapshot>;
|
||||
subscribeWorkspace(organizationId: string, listener: () => void): () => void;
|
||||
createWorkspaceTask(organizationId: string, input: TaskWorkspaceCreateTaskInput): Promise<TaskWorkspaceCreateTaskResponse>;
|
||||
markWorkspaceUnread(organizationId: string, input: TaskWorkspaceSelectInput): Promise<void>;
|
||||
renameWorkspaceTask(organizationId: string, input: TaskWorkspaceRenameInput): Promise<void>;
|
||||
createWorkspaceSession(organizationId: string, input: TaskWorkspaceSelectInput & { model?: string }): Promise<{ sessionId: string }>;
|
||||
renameWorkspaceSession(organizationId: string, input: TaskWorkspaceRenameSessionInput): Promise<void>;
|
||||
setWorkspaceSessionUnread(organizationId: string, input: TaskWorkspaceSetSessionUnreadInput): Promise<void>;
|
||||
updateWorkspaceDraft(organizationId: string, input: TaskWorkspaceUpdateDraftInput): Promise<void>;
|
||||
changeWorkspaceModel(organizationId: string, input: TaskWorkspaceChangeModelInput): Promise<void>;
|
||||
sendWorkspaceMessage(organizationId: string, input: TaskWorkspaceSendMessageInput): Promise<void>;
|
||||
stopWorkspaceSession(organizationId: string, input: TaskWorkspaceSessionInput): Promise<void>;
|
||||
closeWorkspaceSession(organizationId: string, input: TaskWorkspaceSessionInput): Promise<void>;
|
||||
publishWorkspacePr(organizationId: string, input: TaskWorkspaceSelectInput): Promise<void>;
|
||||
revertWorkspaceFile(organizationId: string, input: TaskWorkspaceDiffInput): Promise<void>;
|
||||
adminReloadGithubOrganization(organizationId: string): Promise<void>;
|
||||
adminReloadGithubPullRequests(organizationId: string): Promise<void>;
|
||||
adminReloadGithubRepository(organizationId: string, repoId: string): Promise<void>;
|
||||
adminReloadGithubPullRequest(organizationId: string, repoId: string, prNumber: number): Promise<void>;
|
||||
health(): Promise<{ ok: true }>;
|
||||
useOrganization(organizationId: string): Promise<{ organizationId: string }>;
|
||||
starSandboxAgentRepo(organizationId: string): Promise<StarSandboxAgentRepoResult>;
|
||||
|
|
@ -410,7 +409,7 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
const rivetApiEndpoint = endpoints.rivetEndpoint;
|
||||
const appApiEndpoint = endpoints.appEndpoint;
|
||||
const client = createClient({ endpoint: rivetApiEndpoint }) as unknown as RivetClient;
|
||||
const workbenchSubscriptions = new Map<
|
||||
const workspaceSubscriptions = new Map<
|
||||
string,
|
||||
{
|
||||
listeners: Set<() => void>;
|
||||
|
|
@ -563,7 +562,7 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
}
|
||||
};
|
||||
|
||||
const getWorkbenchCompat = async (organizationId: string): Promise<TaskWorkbenchSnapshot> => {
|
||||
const getWorkspaceCompat = async (organizationId: string): Promise<TaskWorkspaceSnapshot> => {
|
||||
const summary = await (await organization(organizationId)).getOrganizationSummary({ organizationId });
|
||||
const tasks = (
|
||||
await Promise.all(
|
||||
|
|
@ -590,7 +589,7 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
}
|
||||
}),
|
||||
);
|
||||
const sessionDetailsById = new Map(sessionDetails.filter((entry): entry is readonly [string, WorkbenchSessionDetail] => entry !== null));
|
||||
const sessionDetailsById = new Map(sessionDetails.filter((entry): entry is readonly [string, WorkspaceSessionDetail] => entry !== null));
|
||||
return {
|
||||
id: detail.id,
|
||||
repoId: detail.repoId,
|
||||
|
|
@ -623,7 +622,7 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
};
|
||||
}),
|
||||
)
|
||||
).filter((task): task is TaskWorkbenchSnapshot["tasks"][number] => task !== null);
|
||||
).filter((task): task is TaskWorkspaceSnapshot["tasks"][number] => task !== null);
|
||||
|
||||
const repositories = summary.repos
|
||||
.map((repo) => ({
|
||||
|
|
@ -642,14 +641,14 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
};
|
||||
};
|
||||
|
||||
const subscribeWorkbench = (organizationId: string, listener: () => void): (() => void) => {
|
||||
let entry = workbenchSubscriptions.get(organizationId);
|
||||
const subscribeWorkspace = (organizationId: string, listener: () => void): (() => void) => {
|
||||
let entry = workspaceSubscriptions.get(organizationId);
|
||||
if (!entry) {
|
||||
entry = {
|
||||
listeners: new Set(),
|
||||
disposeConnPromise: null,
|
||||
};
|
||||
workbenchSubscriptions.set(organizationId, entry);
|
||||
workspaceSubscriptions.set(organizationId, entry);
|
||||
}
|
||||
|
||||
entry.listeners.add(listener);
|
||||
|
|
@ -658,8 +657,8 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
entry.disposeConnPromise = (async () => {
|
||||
const handle = await organization(organizationId);
|
||||
const conn = (handle as any).connect();
|
||||
const unsubscribeEvent = conn.on("workbenchUpdated", () => {
|
||||
const current = workbenchSubscriptions.get(organizationId);
|
||||
const unsubscribeEvent = conn.on("organizationUpdated", () => {
|
||||
const current = workspaceSubscriptions.get(organizationId);
|
||||
if (!current) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -677,7 +676,7 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
}
|
||||
|
||||
return () => {
|
||||
const current = workbenchSubscriptions.get(organizationId);
|
||||
const current = workspaceSubscriptions.get(organizationId);
|
||||
if (!current) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -686,7 +685,7 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
return;
|
||||
}
|
||||
|
||||
workbenchSubscriptions.delete(organizationId);
|
||||
workspaceSubscriptions.delete(organizationId);
|
||||
void current.disposeConnPromise?.then(async (disposeConn) => {
|
||||
await disposeConn?.();
|
||||
});
|
||||
|
|
@ -849,6 +848,14 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
return await (await appOrganization()).selectAppOrganization({ sessionId, organizationId });
|
||||
},
|
||||
|
||||
async setAppDefaultModel(defaultModel: WorkspaceModelId): Promise<FoundryAppSnapshot> {
|
||||
const sessionId = await getSessionId();
|
||||
if (!sessionId) {
|
||||
throw new Error("No active auth session");
|
||||
}
|
||||
return await (await appOrganization()).setAppDefaultModel({ sessionId, defaultModel });
|
||||
},
|
||||
|
||||
async updateAppOrganizationProfile(input: UpdateFoundryOrganizationProfileInput): Promise<FoundryAppSnapshot> {
|
||||
const sessionId = await getSessionId();
|
||||
if (!sessionId) {
|
||||
|
|
@ -948,33 +955,36 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
return (await organization(organizationId)).getRepoOverview({ organizationId, repoId });
|
||||
},
|
||||
|
||||
async getTask(organizationId: string, taskId: string): Promise<TaskRecord> {
|
||||
async getTask(organizationId: string, repoId: string, taskId: string): Promise<TaskRecord> {
|
||||
return (await organization(organizationId)).getTask({
|
||||
organizationId,
|
||||
repoId,
|
||||
taskId,
|
||||
});
|
||||
},
|
||||
|
||||
async listHistory(input: HistoryQueryInput): Promise<HistoryEvent[]> {
|
||||
return (await organization(input.organizationId)).history(input);
|
||||
return (await organization(input.organizationId)).auditLog(input);
|
||||
},
|
||||
|
||||
async switchTask(organizationId: string, taskId: string): Promise<SwitchResult> {
|
||||
return (await organization(organizationId)).switchTask(taskId);
|
||||
async switchTask(organizationId: string, repoId: string, taskId: string): Promise<SwitchResult> {
|
||||
return (await organization(organizationId)).switchTask({ repoId, taskId });
|
||||
},
|
||||
|
||||
async attachTask(organizationId: string, taskId: string): Promise<{ target: string; sessionId: string | null }> {
|
||||
async attachTask(organizationId: string, repoId: string, taskId: string): Promise<{ target: string; sessionId: string | null }> {
|
||||
return (await organization(organizationId)).attachTask({
|
||||
organizationId,
|
||||
repoId,
|
||||
taskId,
|
||||
reason: "cli.attach",
|
||||
});
|
||||
},
|
||||
|
||||
async runAction(organizationId: string, taskId: string, action: TaskAction): Promise<void> {
|
||||
async runAction(organizationId: string, repoId: string, taskId: string, action: TaskAction): Promise<void> {
|
||||
if (action === "push") {
|
||||
await (await organization(organizationId)).pushTask({
|
||||
organizationId,
|
||||
repoId,
|
||||
taskId,
|
||||
reason: "cli.push",
|
||||
});
|
||||
|
|
@ -983,6 +993,7 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
if (action === "sync") {
|
||||
await (await organization(organizationId)).syncTask({
|
||||
organizationId,
|
||||
repoId,
|
||||
taskId,
|
||||
reason: "cli.sync",
|
||||
});
|
||||
|
|
@ -991,6 +1002,7 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
if (action === "merge") {
|
||||
await (await organization(organizationId)).mergeTask({
|
||||
organizationId,
|
||||
repoId,
|
||||
taskId,
|
||||
reason: "cli.merge",
|
||||
});
|
||||
|
|
@ -999,6 +1011,7 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
if (action === "archive") {
|
||||
await (await organization(organizationId)).archiveTask({
|
||||
organizationId,
|
||||
repoId,
|
||||
taskId,
|
||||
reason: "cli.archive",
|
||||
});
|
||||
|
|
@ -1006,6 +1019,7 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
}
|
||||
await (await organization(organizationId)).killTask({
|
||||
organizationId,
|
||||
repoId,
|
||||
taskId,
|
||||
reason: "cli.kill",
|
||||
});
|
||||
|
|
@ -1160,92 +1174,88 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
return (await organization(organizationId)).getOrganizationSummary({ organizationId });
|
||||
},
|
||||
|
||||
async getTaskDetail(organizationId: string, repoId: string, taskIdValue: string): Promise<WorkbenchTaskDetail> {
|
||||
async getTaskDetail(organizationId: string, repoId: string, taskIdValue: string): Promise<WorkspaceTaskDetail> {
|
||||
return (await task(organizationId, repoId, taskIdValue)).getTaskDetail();
|
||||
},
|
||||
|
||||
async getSessionDetail(organizationId: string, repoId: string, taskIdValue: string, sessionId: string): Promise<WorkbenchSessionDetail> {
|
||||
async getSessionDetail(organizationId: string, repoId: string, taskIdValue: string, sessionId: string): Promise<WorkspaceSessionDetail> {
|
||||
return (await task(organizationId, repoId, taskIdValue)).getSessionDetail({ sessionId });
|
||||
},
|
||||
|
||||
async getWorkbench(organizationId: string): Promise<TaskWorkbenchSnapshot> {
|
||||
return await getWorkbenchCompat(organizationId);
|
||||
async getWorkspace(organizationId: string): Promise<TaskWorkspaceSnapshot> {
|
||||
return await getWorkspaceCompat(organizationId);
|
||||
},
|
||||
|
||||
subscribeWorkbench(organizationId: string, listener: () => void): () => void {
|
||||
return subscribeWorkbench(organizationId, listener);
|
||||
subscribeWorkspace(organizationId: string, listener: () => void): () => void {
|
||||
return subscribeWorkspace(organizationId, listener);
|
||||
},
|
||||
|
||||
async createWorkbenchTask(organizationId: string, input: TaskWorkbenchCreateTaskInput): Promise<TaskWorkbenchCreateTaskResponse> {
|
||||
return (await organization(organizationId)).createWorkbenchTask(input);
|
||||
async createWorkspaceTask(organizationId: string, input: TaskWorkspaceCreateTaskInput): Promise<TaskWorkspaceCreateTaskResponse> {
|
||||
return (await organization(organizationId)).createWorkspaceTask(input);
|
||||
},
|
||||
|
||||
async markWorkbenchUnread(organizationId: string, input: TaskWorkbenchSelectInput): Promise<void> {
|
||||
await (await organization(organizationId)).markWorkbenchUnread(input);
|
||||
async markWorkspaceUnread(organizationId: string, input: TaskWorkspaceSelectInput): Promise<void> {
|
||||
await (await organization(organizationId)).markWorkspaceUnread(input);
|
||||
},
|
||||
|
||||
async renameWorkbenchTask(organizationId: string, input: TaskWorkbenchRenameInput): Promise<void> {
|
||||
await (await organization(organizationId)).renameWorkbenchTask(input);
|
||||
async renameWorkspaceTask(organizationId: string, input: TaskWorkspaceRenameInput): Promise<void> {
|
||||
await (await organization(organizationId)).renameWorkspaceTask(input);
|
||||
},
|
||||
|
||||
async renameWorkbenchBranch(organizationId: string, input: TaskWorkbenchRenameInput): Promise<void> {
|
||||
await (await organization(organizationId)).renameWorkbenchBranch(input);
|
||||
async createWorkspaceSession(organizationId: string, input: TaskWorkspaceSelectInput & { model?: string }): Promise<{ sessionId: string }> {
|
||||
return await (await organization(organizationId)).createWorkspaceSession(input);
|
||||
},
|
||||
|
||||
async createWorkbenchSession(organizationId: string, input: TaskWorkbenchSelectInput & { model?: string }): Promise<{ sessionId: string }> {
|
||||
return await (await organization(organizationId)).createWorkbenchSession(input);
|
||||
async renameWorkspaceSession(organizationId: string, input: TaskWorkspaceRenameSessionInput): Promise<void> {
|
||||
await (await organization(organizationId)).renameWorkspaceSession(input);
|
||||
},
|
||||
|
||||
async renameWorkbenchSession(organizationId: string, input: TaskWorkbenchRenameSessionInput): Promise<void> {
|
||||
await (await organization(organizationId)).renameWorkbenchSession(input);
|
||||
async setWorkspaceSessionUnread(organizationId: string, input: TaskWorkspaceSetSessionUnreadInput): Promise<void> {
|
||||
await (await organization(organizationId)).setWorkspaceSessionUnread(input);
|
||||
},
|
||||
|
||||
async setWorkbenchSessionUnread(organizationId: string, input: TaskWorkbenchSetSessionUnreadInput): Promise<void> {
|
||||
await (await organization(organizationId)).setWorkbenchSessionUnread(input);
|
||||
async updateWorkspaceDraft(organizationId: string, input: TaskWorkspaceUpdateDraftInput): Promise<void> {
|
||||
await (await organization(organizationId)).updateWorkspaceDraft(input);
|
||||
},
|
||||
|
||||
async updateWorkbenchDraft(organizationId: string, input: TaskWorkbenchUpdateDraftInput): Promise<void> {
|
||||
await (await organization(organizationId)).updateWorkbenchDraft(input);
|
||||
async changeWorkspaceModel(organizationId: string, input: TaskWorkspaceChangeModelInput): Promise<void> {
|
||||
await (await organization(organizationId)).changeWorkspaceModel(input);
|
||||
},
|
||||
|
||||
async changeWorkbenchModel(organizationId: string, input: TaskWorkbenchChangeModelInput): Promise<void> {
|
||||
await (await organization(organizationId)).changeWorkbenchModel(input);
|
||||
async sendWorkspaceMessage(organizationId: string, input: TaskWorkspaceSendMessageInput): Promise<void> {
|
||||
await (await organization(organizationId)).sendWorkspaceMessage(input);
|
||||
},
|
||||
|
||||
async sendWorkbenchMessage(organizationId: string, input: TaskWorkbenchSendMessageInput): Promise<void> {
|
||||
await (await organization(organizationId)).sendWorkbenchMessage(input);
|
||||
async stopWorkspaceSession(organizationId: string, input: TaskWorkspaceSessionInput): Promise<void> {
|
||||
await (await organization(organizationId)).stopWorkspaceSession(input);
|
||||
},
|
||||
|
||||
async stopWorkbenchSession(organizationId: string, input: TaskWorkbenchSessionInput): Promise<void> {
|
||||
await (await organization(organizationId)).stopWorkbenchSession(input);
|
||||
async closeWorkspaceSession(organizationId: string, input: TaskWorkspaceSessionInput): Promise<void> {
|
||||
await (await organization(organizationId)).closeWorkspaceSession(input);
|
||||
},
|
||||
|
||||
async closeWorkbenchSession(organizationId: string, input: TaskWorkbenchSessionInput): Promise<void> {
|
||||
await (await organization(organizationId)).closeWorkbenchSession(input);
|
||||
async publishWorkspacePr(organizationId: string, input: TaskWorkspaceSelectInput): Promise<void> {
|
||||
await (await organization(organizationId)).publishWorkspacePr(input);
|
||||
},
|
||||
|
||||
async publishWorkbenchPr(organizationId: string, input: TaskWorkbenchSelectInput): Promise<void> {
|
||||
await (await organization(organizationId)).publishWorkbenchPr(input);
|
||||
async revertWorkspaceFile(organizationId: string, input: TaskWorkspaceDiffInput): Promise<void> {
|
||||
await (await organization(organizationId)).revertWorkspaceFile(input);
|
||||
},
|
||||
|
||||
async revertWorkbenchFile(organizationId: string, input: TaskWorkbenchDiffInput): Promise<void> {
|
||||
await (await organization(organizationId)).revertWorkbenchFile(input);
|
||||
async adminReloadGithubOrganization(organizationId: string): Promise<void> {
|
||||
await (await organization(organizationId)).adminReloadGithubOrganization();
|
||||
},
|
||||
|
||||
async reloadGithubOrganization(organizationId: string): Promise<void> {
|
||||
await (await organization(organizationId)).reloadGithubOrganization();
|
||||
async adminReloadGithubPullRequests(organizationId: string): Promise<void> {
|
||||
await (await organization(organizationId)).adminReloadGithubPullRequests();
|
||||
},
|
||||
|
||||
async reloadGithubPullRequests(organizationId: string): Promise<void> {
|
||||
await (await organization(organizationId)).reloadGithubPullRequests();
|
||||
async adminReloadGithubRepository(organizationId: string, repoId: string): Promise<void> {
|
||||
await (await organization(organizationId)).adminReloadGithubRepository({ repoId });
|
||||
},
|
||||
|
||||
async reloadGithubRepository(organizationId: string, repoId: string): Promise<void> {
|
||||
await (await organization(organizationId)).reloadGithubRepository({ repoId });
|
||||
},
|
||||
|
||||
async reloadGithubPullRequest(organizationId: string, repoId: string, prNumber: number): Promise<void> {
|
||||
await (await organization(organizationId)).reloadGithubPullRequest({ repoId, prNumber });
|
||||
async adminReloadGithubPullRequest(organizationId: string, repoId: string, prNumber: number): Promise<void> {
|
||||
await (await organization(organizationId)).adminReloadGithubPullRequest({ repoId, prNumber });
|
||||
},
|
||||
|
||||
async health(): Promise<{ ok: true }> {
|
||||
|
|
|
|||
|
|
@ -8,4 +8,4 @@ export * from "./subscription/use-subscription.js";
|
|||
export * from "./keys.js";
|
||||
export * from "./mock-app.js";
|
||||
export * from "./view-model.js";
|
||||
export * from "./workbench-client.js";
|
||||
export * from "./workspace-client.js";
|
||||
|
|
|
|||
|
|
@ -16,6 +16,6 @@ export function taskSandboxKey(organizationId: string, sandboxId: string): Actor
|
|||
return ["org", organizationId, "sandbox", sandboxId];
|
||||
}
|
||||
|
||||
export function historyKey(organizationId: string, repoId: string): ActorKey {
|
||||
return ["org", organizationId, "repository", repoId, "history"];
|
||||
export function auditLogKey(organizationId: string, repoId: string): ActorKey {
|
||||
return ["org", organizationId, "repository", repoId, "audit-log"];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { WorkbenchModelId } from "@sandbox-agent/foundry-shared";
|
||||
import type { WorkspaceModelId } from "@sandbox-agent/foundry-shared";
|
||||
import { injectMockLatency } from "./mock/latency.js";
|
||||
import rivetDevFixture from "../../../scripts/data/rivet-dev.json" with { type: "json" };
|
||||
|
||||
|
|
@ -16,6 +16,7 @@ export interface MockFoundryUser {
|
|||
githubLogin: string;
|
||||
roleLabel: string;
|
||||
eligibleOrganizationIds: string[];
|
||||
defaultModel: WorkspaceModelId;
|
||||
}
|
||||
|
||||
export interface MockFoundryOrganizationMember {
|
||||
|
|
@ -61,7 +62,6 @@ export interface MockFoundryOrganizationSettings {
|
|||
slug: string;
|
||||
primaryDomain: string;
|
||||
seatAccrualMode: "first_prompt";
|
||||
defaultModel: WorkbenchModelId;
|
||||
autoImportRepos: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -111,6 +111,7 @@ export interface MockFoundryAppClient {
|
|||
skipStarterRepo(): Promise<void>;
|
||||
starStarterRepo(organizationId: string): Promise<void>;
|
||||
selectOrganization(organizationId: string): Promise<void>;
|
||||
setDefaultModel(model: WorkspaceModelId): Promise<void>;
|
||||
updateOrganizationProfile(input: UpdateMockOrganizationProfileInput): Promise<void>;
|
||||
triggerGithubSync(organizationId: string): Promise<void>;
|
||||
completeHostedCheckout(organizationId: string, planId: MockBillingPlanId): Promise<void>;
|
||||
|
|
@ -180,7 +181,6 @@ function buildRivetOrganization(): MockFoundryOrganization {
|
|||
slug: "rivet",
|
||||
primaryDomain: "rivet.dev",
|
||||
seatAccrualMode: "first_prompt",
|
||||
defaultModel: "gpt-5.3-codex",
|
||||
autoImportRepos: true,
|
||||
},
|
||||
github: {
|
||||
|
|
@ -233,6 +233,7 @@ function buildDefaultSnapshot(): MockFoundryAppSnapshot {
|
|||
githubLogin: "nathan",
|
||||
roleLabel: "Founder",
|
||||
eligibleOrganizationIds: ["personal-nathan", "acme", "rivet"],
|
||||
defaultModel: "gpt-5.3-codex",
|
||||
},
|
||||
{
|
||||
id: "user-maya",
|
||||
|
|
@ -241,6 +242,7 @@ function buildDefaultSnapshot(): MockFoundryAppSnapshot {
|
|||
githubLogin: "maya",
|
||||
roleLabel: "Staff Engineer",
|
||||
eligibleOrganizationIds: ["acme"],
|
||||
defaultModel: "claude-sonnet-4",
|
||||
},
|
||||
{
|
||||
id: "user-jamie",
|
||||
|
|
@ -249,6 +251,7 @@ function buildDefaultSnapshot(): MockFoundryAppSnapshot {
|
|||
githubLogin: "jamie",
|
||||
roleLabel: "Platform Lead",
|
||||
eligibleOrganizationIds: ["personal-jamie", "rivet"],
|
||||
defaultModel: "claude-opus-4",
|
||||
},
|
||||
],
|
||||
organizations: [
|
||||
|
|
@ -261,7 +264,6 @@ function buildDefaultSnapshot(): MockFoundryAppSnapshot {
|
|||
slug: "nathan",
|
||||
primaryDomain: "personal",
|
||||
seatAccrualMode: "first_prompt",
|
||||
defaultModel: "claude-sonnet-4",
|
||||
autoImportRepos: true,
|
||||
},
|
||||
github: {
|
||||
|
|
@ -297,7 +299,6 @@ function buildDefaultSnapshot(): MockFoundryAppSnapshot {
|
|||
slug: "acme",
|
||||
primaryDomain: "acme.dev",
|
||||
seatAccrualMode: "first_prompt",
|
||||
defaultModel: "claude-sonnet-4",
|
||||
autoImportRepos: true,
|
||||
},
|
||||
github: {
|
||||
|
|
@ -342,7 +343,6 @@ function buildDefaultSnapshot(): MockFoundryAppSnapshot {
|
|||
slug: "jamie",
|
||||
primaryDomain: "personal",
|
||||
seatAccrualMode: "first_prompt",
|
||||
defaultModel: "claude-opus-4",
|
||||
autoImportRepos: true,
|
||||
},
|
||||
github: {
|
||||
|
|
@ -538,6 +538,18 @@ class MockFoundryAppStore implements MockFoundryAppClient {
|
|||
}
|
||||
}
|
||||
|
||||
async setDefaultModel(model: WorkspaceModelId): Promise<void> {
|
||||
await this.injectAsyncLatency();
|
||||
const currentUserId = this.snapshot.auth.currentUserId;
|
||||
if (!currentUserId) {
|
||||
throw new Error("No signed-in mock user");
|
||||
}
|
||||
this.updateSnapshot((current) => ({
|
||||
...current,
|
||||
users: current.users.map((user) => (user.id === currentUserId ? { ...user, defaultModel: model } : user)),
|
||||
}));
|
||||
}
|
||||
|
||||
async updateOrganizationProfile(input: UpdateMockOrganizationProfileInput): Promise<void> {
|
||||
await this.injectAsyncLatency();
|
||||
this.requireOrganization(input.organizationId);
|
||||
|
|
|
|||
|
|
@ -6,25 +6,25 @@ import type {
|
|||
SessionEvent,
|
||||
TaskRecord,
|
||||
TaskSummary,
|
||||
TaskWorkbenchChangeModelInput,
|
||||
TaskWorkbenchCreateTaskInput,
|
||||
TaskWorkbenchCreateTaskResponse,
|
||||
TaskWorkbenchDiffInput,
|
||||
TaskWorkbenchRenameInput,
|
||||
TaskWorkbenchRenameSessionInput,
|
||||
TaskWorkbenchSelectInput,
|
||||
TaskWorkbenchSetSessionUnreadInput,
|
||||
TaskWorkbenchSendMessageInput,
|
||||
TaskWorkbenchSnapshot,
|
||||
TaskWorkbenchSessionInput,
|
||||
TaskWorkbenchUpdateDraftInput,
|
||||
TaskWorkspaceChangeModelInput,
|
||||
TaskWorkspaceCreateTaskInput,
|
||||
TaskWorkspaceCreateTaskResponse,
|
||||
TaskWorkspaceDiffInput,
|
||||
TaskWorkspaceRenameInput,
|
||||
TaskWorkspaceRenameSessionInput,
|
||||
TaskWorkspaceSelectInput,
|
||||
TaskWorkspaceSetSessionUnreadInput,
|
||||
TaskWorkspaceSendMessageInput,
|
||||
TaskWorkspaceSnapshot,
|
||||
TaskWorkspaceSessionInput,
|
||||
TaskWorkspaceUpdateDraftInput,
|
||||
TaskEvent,
|
||||
WorkbenchSessionDetail,
|
||||
WorkbenchTaskDetail,
|
||||
WorkbenchTaskSummary,
|
||||
WorkspaceSessionDetail,
|
||||
WorkspaceTaskDetail,
|
||||
WorkspaceTaskSummary,
|
||||
OrganizationEvent,
|
||||
OrganizationSummarySnapshot,
|
||||
HistoryEvent,
|
||||
AuditLogEvent as HistoryEvent,
|
||||
HistoryQueryInput,
|
||||
SandboxProviderId,
|
||||
RepoOverview,
|
||||
|
|
@ -34,7 +34,7 @@ import type {
|
|||
} from "@sandbox-agent/foundry-shared";
|
||||
import type { ProcessCreateRequest, ProcessLogFollowQuery, ProcessLogsResponse, ProcessSignalQuery } from "sandbox-agent";
|
||||
import type { ActorConn, BackendClient, SandboxProcessRecord, SandboxSessionEventRecord, SandboxSessionRecord } from "../backend-client.js";
|
||||
import { getSharedMockWorkbenchClient } from "./workbench-client.js";
|
||||
import { getSharedMockWorkspaceClient } from "./workspace-client.js";
|
||||
|
||||
interface MockProcessRecord extends SandboxProcessRecord {
|
||||
logText: string;
|
||||
|
|
@ -89,7 +89,7 @@ function toTaskStatus(status: TaskRecord["status"], archived: boolean): TaskReco
|
|||
}
|
||||
|
||||
export function createMockBackendClient(defaultOrganizationId = "default"): BackendClient {
|
||||
const workbench = getSharedMockWorkbenchClient();
|
||||
const workspace = getSharedMockWorkspaceClient();
|
||||
const listenersBySandboxId = new Map<string, Set<() => void>>();
|
||||
const processesBySandboxId = new Map<string, MockProcessRecord[]>();
|
||||
const connectionListeners = new Map<string, Set<(payload: any) => void>>();
|
||||
|
|
@ -97,7 +97,7 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
|||
let nextProcessId = 1;
|
||||
|
||||
const requireTask = (taskId: string) => {
|
||||
const task = workbench.getSnapshot().tasks.find((candidate) => candidate.id === taskId);
|
||||
const task = workspace.getSnapshot().tasks.find((candidate) => candidate.id === taskId);
|
||||
if (!task) {
|
||||
throw new Error(`Unknown mock task ${taskId}`);
|
||||
}
|
||||
|
|
@ -164,7 +164,7 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
|||
async dispose(): Promise<void> {},
|
||||
});
|
||||
|
||||
const buildTaskSummary = (task: TaskWorkbenchSnapshot["tasks"][number]): WorkbenchTaskSummary => ({
|
||||
const buildTaskSummary = (task: TaskWorkspaceSnapshot["tasks"][number]): WorkspaceTaskSummary => ({
|
||||
id: task.id,
|
||||
repoId: task.repoId,
|
||||
title: task.title,
|
||||
|
|
@ -187,7 +187,7 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
|||
})),
|
||||
});
|
||||
|
||||
const buildTaskDetail = (task: TaskWorkbenchSnapshot["tasks"][number]): WorkbenchTaskDetail => ({
|
||||
const buildTaskDetail = (task: TaskWorkspaceSnapshot["tasks"][number]): WorkspaceTaskDetail => ({
|
||||
...buildTaskSummary(task),
|
||||
task: task.title,
|
||||
agentType: task.sessions[0]?.agent === "Codex" ? "codex" : "claude",
|
||||
|
|
@ -211,7 +211,7 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
|||
activeSandboxId: task.id,
|
||||
});
|
||||
|
||||
const buildSessionDetail = (task: TaskWorkbenchSnapshot["tasks"][number], sessionId: string): WorkbenchSessionDetail => {
|
||||
const buildSessionDetail = (task: TaskWorkspaceSnapshot["tasks"][number], sessionId: string): WorkspaceSessionDetail => {
|
||||
const tab = task.sessions.find((candidate) => candidate.id === sessionId);
|
||||
if (!tab) {
|
||||
throw new Error(`Unknown mock session ${sessionId} for task ${task.id}`);
|
||||
|
|
@ -232,7 +232,7 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
|||
};
|
||||
|
||||
const buildOrganizationSummary = (): OrganizationSummarySnapshot => {
|
||||
const snapshot = workbench.getSnapshot();
|
||||
const snapshot = workspace.getSnapshot();
|
||||
const taskSummaries = snapshot.tasks.map(buildTaskSummary);
|
||||
return {
|
||||
organizationId: defaultOrganizationId,
|
||||
|
|
@ -256,20 +256,16 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
|||
`sandbox:${organizationId}:${sandboxProviderId}:${sandboxId}`;
|
||||
|
||||
const emitOrganizationSnapshot = (): void => {
|
||||
const summary = buildOrganizationSummary();
|
||||
const latestTask = [...summary.taskSummaries].sort((left, right) => right.updatedAtMs - left.updatedAtMs)[0] ?? null;
|
||||
if (latestTask) {
|
||||
emitConnectionEvent(organizationScope(defaultOrganizationId), "organizationUpdated", {
|
||||
type: "taskSummaryUpdated",
|
||||
taskSummary: latestTask,
|
||||
} satisfies OrganizationEvent);
|
||||
}
|
||||
emitConnectionEvent(organizationScope(defaultOrganizationId), "organizationUpdated", {
|
||||
type: "organizationUpdated",
|
||||
snapshot: buildOrganizationSummary(),
|
||||
} satisfies OrganizationEvent);
|
||||
};
|
||||
|
||||
const emitTaskUpdate = (taskId: string): void => {
|
||||
const task = requireTask(taskId);
|
||||
emitConnectionEvent(taskScope(defaultOrganizationId, task.repoId, task.id), "taskUpdated", {
|
||||
type: "taskDetailUpdated",
|
||||
type: "taskUpdated",
|
||||
detail: buildTaskDetail(task),
|
||||
} satisfies TaskEvent);
|
||||
};
|
||||
|
|
@ -400,6 +396,10 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
|||
return unsupportedAppSnapshot();
|
||||
},
|
||||
|
||||
async setAppDefaultModel(): Promise<FoundryAppSnapshot> {
|
||||
return unsupportedAppSnapshot();
|
||||
},
|
||||
|
||||
async updateAppOrganizationProfile(): Promise<FoundryAppSnapshot> {
|
||||
return unsupportedAppSnapshot();
|
||||
},
|
||||
|
|
@ -433,7 +433,7 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
|||
},
|
||||
|
||||
async listRepos(_organizationId: string): Promise<RepoRecord[]> {
|
||||
return workbench.getSnapshot().repos.map((repo) => ({
|
||||
return workspace.getSnapshot().repos.map((repo) => ({
|
||||
organizationId: defaultOrganizationId,
|
||||
repoId: repo.id,
|
||||
remoteUrl: mockRepoRemote(repo.label),
|
||||
|
|
@ -447,7 +447,7 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
|||
},
|
||||
|
||||
async listTasks(_organizationId: string, repoId?: string): Promise<TaskSummary[]> {
|
||||
return workbench
|
||||
return workspace
|
||||
.getSnapshot()
|
||||
.tasks.filter((task) => !repoId || task.repoId === repoId)
|
||||
.map((task) => ({
|
||||
|
|
@ -641,24 +641,24 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
|||
return buildOrganizationSummary();
|
||||
},
|
||||
|
||||
async getTaskDetail(_organizationId: string, _repoId: string, taskId: string): Promise<WorkbenchTaskDetail> {
|
||||
async getTaskDetail(_organizationId: string, _repoId: string, taskId: string): Promise<WorkspaceTaskDetail> {
|
||||
return buildTaskDetail(requireTask(taskId));
|
||||
},
|
||||
|
||||
async getSessionDetail(_organizationId: string, _repoId: string, taskId: string, sessionId: string): Promise<WorkbenchSessionDetail> {
|
||||
async getSessionDetail(_organizationId: string, _repoId: string, taskId: string, sessionId: string): Promise<WorkspaceSessionDetail> {
|
||||
return buildSessionDetail(requireTask(taskId), sessionId);
|
||||
},
|
||||
|
||||
async getWorkbench(): Promise<TaskWorkbenchSnapshot> {
|
||||
return workbench.getSnapshot();
|
||||
async getWorkspace(): Promise<TaskWorkspaceSnapshot> {
|
||||
return workspace.getSnapshot();
|
||||
},
|
||||
|
||||
subscribeWorkbench(_organizationId: string, listener: () => void): () => void {
|
||||
return workbench.subscribe(listener);
|
||||
subscribeWorkspace(_organizationId: string, listener: () => void): () => void {
|
||||
return workspace.subscribe(listener);
|
||||
},
|
||||
|
||||
async createWorkbenchTask(_organizationId: string, input: TaskWorkbenchCreateTaskInput): Promise<TaskWorkbenchCreateTaskResponse> {
|
||||
const created = await workbench.createTask(input);
|
||||
async createWorkspaceTask(_organizationId: string, input: TaskWorkspaceCreateTaskInput): Promise<TaskWorkspaceCreateTaskResponse> {
|
||||
const created = await workspace.createTask(input);
|
||||
emitOrganizationSnapshot();
|
||||
emitTaskUpdate(created.taskId);
|
||||
if (created.sessionId) {
|
||||
|
|
@ -667,99 +667,93 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
|||
return created;
|
||||
},
|
||||
|
||||
async markWorkbenchUnread(_organizationId: string, input: TaskWorkbenchSelectInput): Promise<void> {
|
||||
await workbench.markTaskUnread(input);
|
||||
async markWorkspaceUnread(_organizationId: string, input: TaskWorkspaceSelectInput): Promise<void> {
|
||||
await workspace.markTaskUnread(input);
|
||||
emitOrganizationSnapshot();
|
||||
emitTaskUpdate(input.taskId);
|
||||
},
|
||||
|
||||
async renameWorkbenchTask(_organizationId: string, input: TaskWorkbenchRenameInput): Promise<void> {
|
||||
await workbench.renameTask(input);
|
||||
async renameWorkspaceTask(_organizationId: string, input: TaskWorkspaceRenameInput): Promise<void> {
|
||||
await workspace.renameTask(input);
|
||||
emitOrganizationSnapshot();
|
||||
emitTaskUpdate(input.taskId);
|
||||
},
|
||||
|
||||
async renameWorkbenchBranch(_organizationId: string, input: TaskWorkbenchRenameInput): Promise<void> {
|
||||
await workbench.renameBranch(input);
|
||||
emitOrganizationSnapshot();
|
||||
emitTaskUpdate(input.taskId);
|
||||
},
|
||||
|
||||
async createWorkbenchSession(_organizationId: string, input: TaskWorkbenchSelectInput & { model?: string }): Promise<{ sessionId: string }> {
|
||||
const created = await workbench.addSession(input);
|
||||
async createWorkspaceSession(_organizationId: string, input: TaskWorkspaceSelectInput & { model?: string }): Promise<{ sessionId: string }> {
|
||||
const created = await workspace.addSession(input);
|
||||
emitOrganizationSnapshot();
|
||||
emitTaskUpdate(input.taskId);
|
||||
emitSessionUpdate(input.taskId, created.sessionId);
|
||||
return created;
|
||||
},
|
||||
|
||||
async renameWorkbenchSession(_organizationId: string, input: TaskWorkbenchRenameSessionInput): Promise<void> {
|
||||
await workbench.renameSession(input);
|
||||
async renameWorkspaceSession(_organizationId: string, input: TaskWorkspaceRenameSessionInput): Promise<void> {
|
||||
await workspace.renameSession(input);
|
||||
emitOrganizationSnapshot();
|
||||
emitTaskUpdate(input.taskId);
|
||||
emitSessionUpdate(input.taskId, input.sessionId);
|
||||
},
|
||||
|
||||
async setWorkbenchSessionUnread(_organizationId: string, input: TaskWorkbenchSetSessionUnreadInput): Promise<void> {
|
||||
await workbench.setSessionUnread(input);
|
||||
async setWorkspaceSessionUnread(_organizationId: string, input: TaskWorkspaceSetSessionUnreadInput): Promise<void> {
|
||||
await workspace.setSessionUnread(input);
|
||||
emitOrganizationSnapshot();
|
||||
emitTaskUpdate(input.taskId);
|
||||
emitSessionUpdate(input.taskId, input.sessionId);
|
||||
},
|
||||
|
||||
async updateWorkbenchDraft(_organizationId: string, input: TaskWorkbenchUpdateDraftInput): Promise<void> {
|
||||
await workbench.updateDraft(input);
|
||||
async updateWorkspaceDraft(_organizationId: string, input: TaskWorkspaceUpdateDraftInput): Promise<void> {
|
||||
await workspace.updateDraft(input);
|
||||
emitOrganizationSnapshot();
|
||||
emitTaskUpdate(input.taskId);
|
||||
emitSessionUpdate(input.taskId, input.sessionId);
|
||||
},
|
||||
|
||||
async changeWorkbenchModel(_organizationId: string, input: TaskWorkbenchChangeModelInput): Promise<void> {
|
||||
await workbench.changeModel(input);
|
||||
async changeWorkspaceModel(_organizationId: string, input: TaskWorkspaceChangeModelInput): Promise<void> {
|
||||
await workspace.changeModel(input);
|
||||
emitOrganizationSnapshot();
|
||||
emitTaskUpdate(input.taskId);
|
||||
emitSessionUpdate(input.taskId, input.sessionId);
|
||||
},
|
||||
|
||||
async sendWorkbenchMessage(_organizationId: string, input: TaskWorkbenchSendMessageInput): Promise<void> {
|
||||
await workbench.sendMessage(input);
|
||||
async sendWorkspaceMessage(_organizationId: string, input: TaskWorkspaceSendMessageInput): Promise<void> {
|
||||
await workspace.sendMessage(input);
|
||||
emitOrganizationSnapshot();
|
||||
emitTaskUpdate(input.taskId);
|
||||
emitSessionUpdate(input.taskId, input.sessionId);
|
||||
},
|
||||
|
||||
async stopWorkbenchSession(_organizationId: string, input: TaskWorkbenchSessionInput): Promise<void> {
|
||||
await workbench.stopAgent(input);
|
||||
async stopWorkspaceSession(_organizationId: string, input: TaskWorkspaceSessionInput): Promise<void> {
|
||||
await workspace.stopAgent(input);
|
||||
emitOrganizationSnapshot();
|
||||
emitTaskUpdate(input.taskId);
|
||||
emitSessionUpdate(input.taskId, input.sessionId);
|
||||
},
|
||||
|
||||
async closeWorkbenchSession(_organizationId: string, input: TaskWorkbenchSessionInput): Promise<void> {
|
||||
await workbench.closeSession(input);
|
||||
async closeWorkspaceSession(_organizationId: string, input: TaskWorkspaceSessionInput): Promise<void> {
|
||||
await workspace.closeSession(input);
|
||||
emitOrganizationSnapshot();
|
||||
emitTaskUpdate(input.taskId);
|
||||
},
|
||||
|
||||
async publishWorkbenchPr(_organizationId: string, input: TaskWorkbenchSelectInput): Promise<void> {
|
||||
await workbench.publishPr(input);
|
||||
async publishWorkspacePr(_organizationId: string, input: TaskWorkspaceSelectInput): Promise<void> {
|
||||
await workspace.publishPr(input);
|
||||
emitOrganizationSnapshot();
|
||||
emitTaskUpdate(input.taskId);
|
||||
},
|
||||
|
||||
async revertWorkbenchFile(_organizationId: string, input: TaskWorkbenchDiffInput): Promise<void> {
|
||||
await workbench.revertFile(input);
|
||||
async revertWorkspaceFile(_organizationId: string, input: TaskWorkspaceDiffInput): Promise<void> {
|
||||
await workspace.revertFile(input);
|
||||
emitOrganizationSnapshot();
|
||||
emitTaskUpdate(input.taskId);
|
||||
},
|
||||
|
||||
async reloadGithubOrganization(): Promise<void> {},
|
||||
async adminReloadGithubOrganization(): Promise<void> {},
|
||||
|
||||
async reloadGithubPullRequests(): Promise<void> {},
|
||||
async adminReloadGithubPullRequests(): Promise<void> {},
|
||||
|
||||
async reloadGithubRepository(): Promise<void> {},
|
||||
async adminReloadGithubRepository(): Promise<void> {},
|
||||
|
||||
async reloadGithubPullRequest(): Promise<void> {},
|
||||
async adminReloadGithubPullRequest(): Promise<void> {},
|
||||
|
||||
async health(): Promise<{ ok: true }> {
|
||||
return { ok: true };
|
||||
|
|
|
|||
|
|
@ -1,33 +1,33 @@
|
|||
import {
|
||||
MODEL_GROUPS,
|
||||
buildInitialMockLayoutViewModel,
|
||||
groupWorkbenchRepositories,
|
||||
groupWorkspaceRepositories,
|
||||
nowMs,
|
||||
providerAgent,
|
||||
randomReply,
|
||||
removeFileTreePath,
|
||||
slugify,
|
||||
uid,
|
||||
} from "../workbench-model.js";
|
||||
} from "../workspace-model.js";
|
||||
import type {
|
||||
TaskWorkbenchAddSessionResponse,
|
||||
TaskWorkbenchChangeModelInput,
|
||||
TaskWorkbenchCreateTaskInput,
|
||||
TaskWorkbenchCreateTaskResponse,
|
||||
TaskWorkbenchDiffInput,
|
||||
TaskWorkbenchRenameInput,
|
||||
TaskWorkbenchRenameSessionInput,
|
||||
TaskWorkbenchSelectInput,
|
||||
TaskWorkbenchSetSessionUnreadInput,
|
||||
TaskWorkbenchSendMessageInput,
|
||||
TaskWorkbenchSnapshot,
|
||||
TaskWorkbenchSessionInput,
|
||||
TaskWorkbenchUpdateDraftInput,
|
||||
WorkbenchSession as AgentSession,
|
||||
WorkbenchTask as Task,
|
||||
WorkbenchTranscriptEvent as TranscriptEvent,
|
||||
TaskWorkspaceAddSessionResponse,
|
||||
TaskWorkspaceChangeModelInput,
|
||||
TaskWorkspaceCreateTaskInput,
|
||||
TaskWorkspaceCreateTaskResponse,
|
||||
TaskWorkspaceDiffInput,
|
||||
TaskWorkspaceRenameInput,
|
||||
TaskWorkspaceRenameSessionInput,
|
||||
TaskWorkspaceSelectInput,
|
||||
TaskWorkspaceSetSessionUnreadInput,
|
||||
TaskWorkspaceSendMessageInput,
|
||||
TaskWorkspaceSnapshot,
|
||||
TaskWorkspaceSessionInput,
|
||||
TaskWorkspaceUpdateDraftInput,
|
||||
WorkspaceSession as AgentSession,
|
||||
WorkspaceTask as Task,
|
||||
WorkspaceTranscriptEvent as TranscriptEvent,
|
||||
} from "@sandbox-agent/foundry-shared";
|
||||
import type { TaskWorkbenchClient } from "../workbench-client.js";
|
||||
import type { TaskWorkspaceClient } from "../workspace-client.js";
|
||||
|
||||
function buildTranscriptEvent(params: {
|
||||
sessionId: string;
|
||||
|
|
@ -47,12 +47,12 @@ function buildTranscriptEvent(params: {
|
|||
};
|
||||
}
|
||||
|
||||
class MockWorkbenchStore implements TaskWorkbenchClient {
|
||||
class MockWorkspaceStore implements TaskWorkspaceClient {
|
||||
private snapshot = buildInitialMockLayoutViewModel();
|
||||
private listeners = new Set<() => void>();
|
||||
private pendingTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
|
||||
getSnapshot(): TaskWorkbenchSnapshot {
|
||||
getSnapshot(): TaskWorkspaceSnapshot {
|
||||
return this.snapshot;
|
||||
}
|
||||
|
||||
|
|
@ -63,7 +63,7 @@ class MockWorkbenchStore implements TaskWorkbenchClient {
|
|||
};
|
||||
}
|
||||
|
||||
async createTask(input: TaskWorkbenchCreateTaskInput): Promise<TaskWorkbenchCreateTaskResponse> {
|
||||
async createTask(input: TaskWorkspaceCreateTaskInput): Promise<TaskWorkspaceCreateTaskResponse> {
|
||||
const id = uid();
|
||||
const sessionId = `session-${id}`;
|
||||
const repo = this.snapshot.repos.find((candidate) => candidate.id === input.repoId);
|
||||
|
|
@ -109,7 +109,7 @@ class MockWorkbenchStore implements TaskWorkbenchClient {
|
|||
return { taskId: id, sessionId };
|
||||
}
|
||||
|
||||
async markTaskUnread(input: TaskWorkbenchSelectInput): Promise<void> {
|
||||
async markTaskUnread(input: TaskWorkspaceSelectInput): Promise<void> {
|
||||
this.updateTask(input.taskId, (task) => {
|
||||
const targetSession = task.sessions[task.sessions.length - 1] ?? null;
|
||||
if (!targetSession) {
|
||||
|
|
@ -123,7 +123,7 @@ class MockWorkbenchStore implements TaskWorkbenchClient {
|
|||
});
|
||||
}
|
||||
|
||||
async renameTask(input: TaskWorkbenchRenameInput): Promise<void> {
|
||||
async renameTask(input: TaskWorkspaceRenameInput): Promise<void> {
|
||||
const value = input.value.trim();
|
||||
if (!value) {
|
||||
throw new Error(`Cannot rename task ${input.taskId} to an empty title`);
|
||||
|
|
@ -131,19 +131,11 @@ class MockWorkbenchStore implements TaskWorkbenchClient {
|
|||
this.updateTask(input.taskId, (task) => ({ ...task, title: value, updatedAtMs: nowMs() }));
|
||||
}
|
||||
|
||||
async renameBranch(input: TaskWorkbenchRenameInput): Promise<void> {
|
||||
const value = input.value.trim();
|
||||
if (!value) {
|
||||
throw new Error(`Cannot rename branch for task ${input.taskId} to an empty value`);
|
||||
}
|
||||
this.updateTask(input.taskId, (task) => ({ ...task, branch: value, updatedAtMs: nowMs() }));
|
||||
}
|
||||
|
||||
async archiveTask(input: TaskWorkbenchSelectInput): Promise<void> {
|
||||
async archiveTask(input: TaskWorkspaceSelectInput): Promise<void> {
|
||||
this.updateTask(input.taskId, (task) => ({ ...task, status: "archived", updatedAtMs: nowMs() }));
|
||||
}
|
||||
|
||||
async publishPr(input: TaskWorkbenchSelectInput): Promise<void> {
|
||||
async publishPr(input: TaskWorkspaceSelectInput): Promise<void> {
|
||||
const nextPrNumber = Math.max(0, ...this.snapshot.tasks.map((task) => task.pullRequest?.number ?? 0)) + 1;
|
||||
this.updateTask(input.taskId, (task) => ({
|
||||
...task,
|
||||
|
|
@ -152,7 +144,7 @@ class MockWorkbenchStore implements TaskWorkbenchClient {
|
|||
}));
|
||||
}
|
||||
|
||||
async revertFile(input: TaskWorkbenchDiffInput): Promise<void> {
|
||||
async revertFile(input: TaskWorkspaceDiffInput): Promise<void> {
|
||||
this.updateTask(input.taskId, (task) => {
|
||||
const file = task.fileChanges.find((entry) => entry.path === input.path);
|
||||
const nextDiffs = { ...task.diffs };
|
||||
|
|
@ -167,7 +159,7 @@ class MockWorkbenchStore implements TaskWorkbenchClient {
|
|||
});
|
||||
}
|
||||
|
||||
async updateDraft(input: TaskWorkbenchUpdateDraftInput): Promise<void> {
|
||||
async updateDraft(input: TaskWorkspaceUpdateDraftInput): Promise<void> {
|
||||
this.assertSession(input.taskId, input.sessionId);
|
||||
this.updateTask(input.taskId, (task) => ({
|
||||
...task,
|
||||
|
|
@ -187,7 +179,7 @@ class MockWorkbenchStore implements TaskWorkbenchClient {
|
|||
}));
|
||||
}
|
||||
|
||||
async sendMessage(input: TaskWorkbenchSendMessageInput): Promise<void> {
|
||||
async sendMessage(input: TaskWorkspaceSendMessageInput): Promise<void> {
|
||||
const text = input.text.trim();
|
||||
if (!text) {
|
||||
throw new Error(`Cannot send an empty mock prompt for task ${input.taskId}`);
|
||||
|
|
@ -288,7 +280,7 @@ class MockWorkbenchStore implements TaskWorkbenchClient {
|
|||
this.pendingTimers.set(input.sessionId, timer);
|
||||
}
|
||||
|
||||
async stopAgent(input: TaskWorkbenchSessionInput): Promise<void> {
|
||||
async stopAgent(input: TaskWorkspaceSessionInput): Promise<void> {
|
||||
this.assertSession(input.taskId, input.sessionId);
|
||||
const existing = this.pendingTimers.get(input.sessionId);
|
||||
if (existing) {
|
||||
|
|
@ -311,14 +303,14 @@ class MockWorkbenchStore implements TaskWorkbenchClient {
|
|||
});
|
||||
}
|
||||
|
||||
async setSessionUnread(input: TaskWorkbenchSetSessionUnreadInput): Promise<void> {
|
||||
async setSessionUnread(input: TaskWorkspaceSetSessionUnreadInput): Promise<void> {
|
||||
this.updateTask(input.taskId, (currentTask) => ({
|
||||
...currentTask,
|
||||
sessions: currentTask.sessions.map((candidate) => (candidate.id === input.sessionId ? { ...candidate, unread: input.unread } : candidate)),
|
||||
}));
|
||||
}
|
||||
|
||||
async renameSession(input: TaskWorkbenchRenameSessionInput): Promise<void> {
|
||||
async renameSession(input: TaskWorkspaceRenameSessionInput): Promise<void> {
|
||||
const title = input.title.trim();
|
||||
if (!title) {
|
||||
throw new Error(`Cannot rename session ${input.sessionId} to an empty title`);
|
||||
|
|
@ -329,7 +321,7 @@ class MockWorkbenchStore implements TaskWorkbenchClient {
|
|||
}));
|
||||
}
|
||||
|
||||
async closeSession(input: TaskWorkbenchSessionInput): Promise<void> {
|
||||
async closeSession(input: TaskWorkspaceSessionInput): Promise<void> {
|
||||
this.updateTask(input.taskId, (currentTask) => {
|
||||
if (currentTask.sessions.length <= 1) {
|
||||
return currentTask;
|
||||
|
|
@ -342,7 +334,7 @@ class MockWorkbenchStore implements TaskWorkbenchClient {
|
|||
});
|
||||
}
|
||||
|
||||
async addSession(input: TaskWorkbenchSelectInput): Promise<TaskWorkbenchAddSessionResponse> {
|
||||
async addSession(input: TaskWorkspaceSelectInput): Promise<TaskWorkspaceAddSessionResponse> {
|
||||
this.assertTask(input.taskId);
|
||||
const nextSessionId = uid();
|
||||
const nextSession: AgentSession = {
|
||||
|
|
@ -368,7 +360,7 @@ class MockWorkbenchStore implements TaskWorkbenchClient {
|
|||
return { sessionId: nextSession.id };
|
||||
}
|
||||
|
||||
async changeModel(input: TaskWorkbenchChangeModelInput): Promise<void> {
|
||||
async changeModel(input: TaskWorkspaceChangeModelInput): Promise<void> {
|
||||
const group = MODEL_GROUPS.find((candidate) => candidate.models.some((entry) => entry.id === input.model));
|
||||
if (!group) {
|
||||
throw new Error(`Unable to resolve model provider for ${input.model}`);
|
||||
|
|
@ -382,11 +374,11 @@ class MockWorkbenchStore implements TaskWorkbenchClient {
|
|||
}));
|
||||
}
|
||||
|
||||
private updateState(updater: (current: TaskWorkbenchSnapshot) => TaskWorkbenchSnapshot): void {
|
||||
private updateState(updater: (current: TaskWorkspaceSnapshot) => TaskWorkspaceSnapshot): void {
|
||||
const nextSnapshot = updater(this.snapshot);
|
||||
this.snapshot = {
|
||||
...nextSnapshot,
|
||||
repositories: groupWorkbenchRepositories(nextSnapshot.repos, nextSnapshot.tasks),
|
||||
repositories: groupWorkspaceRepositories(nextSnapshot.repos, nextSnapshot.tasks),
|
||||
};
|
||||
this.notify();
|
||||
}
|
||||
|
|
@ -436,11 +428,11 @@ function candidateEventIndex(task: Task, sessionId: string): number {
|
|||
return (session?.transcript.length ?? 0) + 1;
|
||||
}
|
||||
|
||||
let sharedMockWorkbenchClient: TaskWorkbenchClient | null = null;
|
||||
let sharedMockWorkspaceClient: TaskWorkspaceClient | null = null;
|
||||
|
||||
export function getSharedMockWorkbenchClient(): TaskWorkbenchClient {
|
||||
if (!sharedMockWorkbenchClient) {
|
||||
sharedMockWorkbenchClient = new MockWorkbenchStore();
|
||||
export function getSharedMockWorkspaceClient(): TaskWorkspaceClient {
|
||||
if (!sharedMockWorkspaceClient) {
|
||||
sharedMockWorkspaceClient = new MockWorkspaceStore();
|
||||
}
|
||||
return sharedMockWorkbenchClient;
|
||||
return sharedMockWorkspaceClient;
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import type { FoundryAppSnapshot, FoundryBillingPlanId, UpdateFoundryOrganizationProfileInput } from "@sandbox-agent/foundry-shared";
|
||||
import type { FoundryAppSnapshot, FoundryBillingPlanId, UpdateFoundryOrganizationProfileInput, WorkspaceModelId } from "@sandbox-agent/foundry-shared";
|
||||
import type { BackendClient } from "../backend-client.js";
|
||||
import type { FoundryAppClient } from "../app-client.js";
|
||||
|
||||
|
|
@ -72,6 +72,11 @@ class RemoteFoundryAppStore implements FoundryAppClient {
|
|||
this.notify();
|
||||
}
|
||||
|
||||
async setDefaultModel(model: WorkspaceModelId): Promise<void> {
|
||||
this.snapshot = await this.backend.setAppDefaultModel(model);
|
||||
this.notify();
|
||||
}
|
||||
|
||||
async updateOrganizationProfile(input: UpdateFoundryOrganizationProfileInput): Promise<void> {
|
||||
this.snapshot = await this.backend.updateAppOrganizationProfile(input);
|
||||
this.notify();
|
||||
|
|
|
|||
|
|
@ -1,198 +0,0 @@
|
|||
import type {
|
||||
TaskWorkbenchAddSessionResponse,
|
||||
TaskWorkbenchChangeModelInput,
|
||||
TaskWorkbenchCreateTaskInput,
|
||||
TaskWorkbenchCreateTaskResponse,
|
||||
TaskWorkbenchDiffInput,
|
||||
TaskWorkbenchRenameInput,
|
||||
TaskWorkbenchRenameSessionInput,
|
||||
TaskWorkbenchSelectInput,
|
||||
TaskWorkbenchSetSessionUnreadInput,
|
||||
TaskWorkbenchSendMessageInput,
|
||||
TaskWorkbenchSnapshot,
|
||||
TaskWorkbenchSessionInput,
|
||||
TaskWorkbenchUpdateDraftInput,
|
||||
} from "@sandbox-agent/foundry-shared";
|
||||
import type { BackendClient } from "../backend-client.js";
|
||||
import { groupWorkbenchRepositories } from "../workbench-model.js";
|
||||
import type { TaskWorkbenchClient } from "../workbench-client.js";
|
||||
|
||||
export interface RemoteWorkbenchClientOptions {
|
||||
backend: BackendClient;
|
||||
organizationId: string;
|
||||
}
|
||||
|
||||
class RemoteWorkbenchStore implements TaskWorkbenchClient {
|
||||
private readonly backend: BackendClient;
|
||||
private readonly organizationId: string;
|
||||
private snapshot: TaskWorkbenchSnapshot;
|
||||
private readonly listeners = new Set<() => void>();
|
||||
private unsubscribeWorkbench: (() => void) | null = null;
|
||||
private refreshPromise: Promise<void> | null = null;
|
||||
private refreshRetryTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
constructor(options: RemoteWorkbenchClientOptions) {
|
||||
this.backend = options.backend;
|
||||
this.organizationId = options.organizationId;
|
||||
this.snapshot = {
|
||||
organizationId: options.organizationId,
|
||||
repos: [],
|
||||
repositories: [],
|
||||
tasks: [],
|
||||
};
|
||||
}
|
||||
|
||||
getSnapshot(): TaskWorkbenchSnapshot {
|
||||
return this.snapshot;
|
||||
}
|
||||
|
||||
subscribe(listener: () => void): () => void {
|
||||
this.listeners.add(listener);
|
||||
this.ensureStarted();
|
||||
return () => {
|
||||
this.listeners.delete(listener);
|
||||
if (this.listeners.size === 0 && this.refreshRetryTimeout) {
|
||||
clearTimeout(this.refreshRetryTimeout);
|
||||
this.refreshRetryTimeout = null;
|
||||
}
|
||||
if (this.listeners.size === 0 && this.unsubscribeWorkbench) {
|
||||
this.unsubscribeWorkbench();
|
||||
this.unsubscribeWorkbench = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async createTask(input: TaskWorkbenchCreateTaskInput): Promise<TaskWorkbenchCreateTaskResponse> {
|
||||
const created = await this.backend.createWorkbenchTask(this.organizationId, input);
|
||||
await this.refresh();
|
||||
return created;
|
||||
}
|
||||
|
||||
async markTaskUnread(input: TaskWorkbenchSelectInput): Promise<void> {
|
||||
await this.backend.markWorkbenchUnread(this.organizationId, input);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async renameTask(input: TaskWorkbenchRenameInput): Promise<void> {
|
||||
await this.backend.renameWorkbenchTask(this.organizationId, input);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async renameBranch(input: TaskWorkbenchRenameInput): Promise<void> {
|
||||
await this.backend.renameWorkbenchBranch(this.organizationId, input);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async archiveTask(input: TaskWorkbenchSelectInput): Promise<void> {
|
||||
await this.backend.runAction(this.organizationId, input.taskId, "archive");
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async publishPr(input: TaskWorkbenchSelectInput): Promise<void> {
|
||||
await this.backend.publishWorkbenchPr(this.organizationId, input);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async revertFile(input: TaskWorkbenchDiffInput): Promise<void> {
|
||||
await this.backend.revertWorkbenchFile(this.organizationId, input);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async updateDraft(input: TaskWorkbenchUpdateDraftInput): Promise<void> {
|
||||
await this.backend.updateWorkbenchDraft(this.organizationId, input);
|
||||
// Skip refresh — the server broadcast will trigger it, and the frontend
|
||||
// holds local draft state to avoid the round-trip overwriting user input.
|
||||
}
|
||||
|
||||
async sendMessage(input: TaskWorkbenchSendMessageInput): Promise<void> {
|
||||
await this.backend.sendWorkbenchMessage(this.organizationId, input);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async stopAgent(input: TaskWorkbenchSessionInput): Promise<void> {
|
||||
await this.backend.stopWorkbenchSession(this.organizationId, input);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async setSessionUnread(input: TaskWorkbenchSetSessionUnreadInput): Promise<void> {
|
||||
await this.backend.setWorkbenchSessionUnread(this.organizationId, input);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async renameSession(input: TaskWorkbenchRenameSessionInput): Promise<void> {
|
||||
await this.backend.renameWorkbenchSession(this.organizationId, input);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async closeSession(input: TaskWorkbenchSessionInput): Promise<void> {
|
||||
await this.backend.closeWorkbenchSession(this.organizationId, input);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async addSession(input: TaskWorkbenchSelectInput): Promise<TaskWorkbenchAddSessionResponse> {
|
||||
const created = await this.backend.createWorkbenchSession(this.organizationId, input);
|
||||
await this.refresh();
|
||||
return created;
|
||||
}
|
||||
|
||||
async changeModel(input: TaskWorkbenchChangeModelInput): Promise<void> {
|
||||
await this.backend.changeWorkbenchModel(this.organizationId, input);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
private ensureStarted(): void {
|
||||
if (!this.unsubscribeWorkbench) {
|
||||
this.unsubscribeWorkbench = this.backend.subscribeWorkbench(this.organizationId, () => {
|
||||
void this.refresh().catch(() => {
|
||||
this.scheduleRefreshRetry();
|
||||
});
|
||||
});
|
||||
}
|
||||
void this.refresh().catch(() => {
|
||||
this.scheduleRefreshRetry();
|
||||
});
|
||||
}
|
||||
|
||||
private scheduleRefreshRetry(): void {
|
||||
if (this.refreshRetryTimeout || this.listeners.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.refreshRetryTimeout = setTimeout(() => {
|
||||
this.refreshRetryTimeout = null;
|
||||
void this.refresh().catch(() => {
|
||||
this.scheduleRefreshRetry();
|
||||
});
|
||||
}, 1_000);
|
||||
}
|
||||
|
||||
private async refresh(): Promise<void> {
|
||||
if (this.refreshPromise) {
|
||||
await this.refreshPromise;
|
||||
return;
|
||||
}
|
||||
|
||||
this.refreshPromise = (async () => {
|
||||
const nextSnapshot = await this.backend.getWorkbench(this.organizationId);
|
||||
if (this.refreshRetryTimeout) {
|
||||
clearTimeout(this.refreshRetryTimeout);
|
||||
this.refreshRetryTimeout = null;
|
||||
}
|
||||
this.snapshot = {
|
||||
...nextSnapshot,
|
||||
repositories: nextSnapshot.repositories ?? groupWorkbenchRepositories(nextSnapshot.repos, nextSnapshot.tasks),
|
||||
};
|
||||
for (const listener of [...this.listeners]) {
|
||||
listener();
|
||||
}
|
||||
})().finally(() => {
|
||||
this.refreshPromise = null;
|
||||
});
|
||||
|
||||
await this.refreshPromise;
|
||||
}
|
||||
}
|
||||
|
||||
export function createRemoteWorkbenchClient(options: RemoteWorkbenchClientOptions): TaskWorkbenchClient {
|
||||
return new RemoteWorkbenchStore(options);
|
||||
}
|
||||
193
foundry/packages/client/src/remote/workspace-client.ts
Normal file
193
foundry/packages/client/src/remote/workspace-client.ts
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
import type {
|
||||
TaskWorkspaceAddSessionResponse,
|
||||
TaskWorkspaceChangeModelInput,
|
||||
TaskWorkspaceCreateTaskInput,
|
||||
TaskWorkspaceCreateTaskResponse,
|
||||
TaskWorkspaceDiffInput,
|
||||
TaskWorkspaceRenameInput,
|
||||
TaskWorkspaceRenameSessionInput,
|
||||
TaskWorkspaceSelectInput,
|
||||
TaskWorkspaceSetSessionUnreadInput,
|
||||
TaskWorkspaceSendMessageInput,
|
||||
TaskWorkspaceSnapshot,
|
||||
TaskWorkspaceSessionInput,
|
||||
TaskWorkspaceUpdateDraftInput,
|
||||
} from "@sandbox-agent/foundry-shared";
|
||||
import type { BackendClient } from "../backend-client.js";
|
||||
import { groupWorkspaceRepositories } from "../workspace-model.js";
|
||||
import type { TaskWorkspaceClient } from "../workspace-client.js";
|
||||
|
||||
export interface RemoteWorkspaceClientOptions {
|
||||
backend: BackendClient;
|
||||
organizationId: string;
|
||||
}
|
||||
|
||||
class RemoteWorkspaceStore implements TaskWorkspaceClient {
|
||||
private readonly backend: BackendClient;
|
||||
private readonly organizationId: string;
|
||||
private snapshot: TaskWorkspaceSnapshot;
|
||||
private readonly listeners = new Set<() => void>();
|
||||
private unsubscribeWorkspace: (() => void) | null = null;
|
||||
private refreshPromise: Promise<void> | null = null;
|
||||
private refreshRetryTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
constructor(options: RemoteWorkspaceClientOptions) {
|
||||
this.backend = options.backend;
|
||||
this.organizationId = options.organizationId;
|
||||
this.snapshot = {
|
||||
organizationId: options.organizationId,
|
||||
repos: [],
|
||||
repositories: [],
|
||||
tasks: [],
|
||||
};
|
||||
}
|
||||
|
||||
getSnapshot(): TaskWorkspaceSnapshot {
|
||||
return this.snapshot;
|
||||
}
|
||||
|
||||
subscribe(listener: () => void): () => void {
|
||||
this.listeners.add(listener);
|
||||
this.ensureStarted();
|
||||
return () => {
|
||||
this.listeners.delete(listener);
|
||||
if (this.listeners.size === 0 && this.refreshRetryTimeout) {
|
||||
clearTimeout(this.refreshRetryTimeout);
|
||||
this.refreshRetryTimeout = null;
|
||||
}
|
||||
if (this.listeners.size === 0 && this.unsubscribeWorkspace) {
|
||||
this.unsubscribeWorkspace();
|
||||
this.unsubscribeWorkspace = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async createTask(input: TaskWorkspaceCreateTaskInput): Promise<TaskWorkspaceCreateTaskResponse> {
|
||||
const created = await this.backend.createWorkspaceTask(this.organizationId, input);
|
||||
await this.refresh();
|
||||
return created;
|
||||
}
|
||||
|
||||
async markTaskUnread(input: TaskWorkspaceSelectInput): Promise<void> {
|
||||
await this.backend.markWorkspaceUnread(this.organizationId, input);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async renameTask(input: TaskWorkspaceRenameInput): Promise<void> {
|
||||
await this.backend.renameWorkspaceTask(this.organizationId, input);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async archiveTask(input: TaskWorkspaceSelectInput): Promise<void> {
|
||||
await this.backend.runAction(this.organizationId, input.repoId, input.taskId, "archive");
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async publishPr(input: TaskWorkspaceSelectInput): Promise<void> {
|
||||
await this.backend.publishWorkspacePr(this.organizationId, input);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async revertFile(input: TaskWorkspaceDiffInput): Promise<void> {
|
||||
await this.backend.revertWorkspaceFile(this.organizationId, input);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async updateDraft(input: TaskWorkspaceUpdateDraftInput): Promise<void> {
|
||||
await this.backend.updateWorkspaceDraft(this.organizationId, input);
|
||||
// Skip refresh — the server broadcast will trigger it, and the frontend
|
||||
// holds local draft state to avoid the round-trip overwriting user input.
|
||||
}
|
||||
|
||||
async sendMessage(input: TaskWorkspaceSendMessageInput): Promise<void> {
|
||||
await this.backend.sendWorkspaceMessage(this.organizationId, input);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async stopAgent(input: TaskWorkspaceSessionInput): Promise<void> {
|
||||
await this.backend.stopWorkspaceSession(this.organizationId, input);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async setSessionUnread(input: TaskWorkspaceSetSessionUnreadInput): Promise<void> {
|
||||
await this.backend.setWorkspaceSessionUnread(this.organizationId, input);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async renameSession(input: TaskWorkspaceRenameSessionInput): Promise<void> {
|
||||
await this.backend.renameWorkspaceSession(this.organizationId, input);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async closeSession(input: TaskWorkspaceSessionInput): Promise<void> {
|
||||
await this.backend.closeWorkspaceSession(this.organizationId, input);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async addSession(input: TaskWorkspaceSelectInput): Promise<TaskWorkspaceAddSessionResponse> {
|
||||
const created = await this.backend.createWorkspaceSession(this.organizationId, input);
|
||||
await this.refresh();
|
||||
return created;
|
||||
}
|
||||
|
||||
async changeModel(input: TaskWorkspaceChangeModelInput): Promise<void> {
|
||||
await this.backend.changeWorkspaceModel(this.organizationId, input);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
private ensureStarted(): void {
|
||||
if (!this.unsubscribeWorkspace) {
|
||||
this.unsubscribeWorkspace = this.backend.subscribeWorkspace(this.organizationId, () => {
|
||||
void this.refresh().catch(() => {
|
||||
this.scheduleRefreshRetry();
|
||||
});
|
||||
});
|
||||
}
|
||||
void this.refresh().catch(() => {
|
||||
this.scheduleRefreshRetry();
|
||||
});
|
||||
}
|
||||
|
||||
private scheduleRefreshRetry(): void {
|
||||
if (this.refreshRetryTimeout || this.listeners.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.refreshRetryTimeout = setTimeout(() => {
|
||||
this.refreshRetryTimeout = null;
|
||||
void this.refresh().catch(() => {
|
||||
this.scheduleRefreshRetry();
|
||||
});
|
||||
}, 1_000);
|
||||
}
|
||||
|
||||
private async refresh(): Promise<void> {
|
||||
if (this.refreshPromise) {
|
||||
await this.refreshPromise;
|
||||
return;
|
||||
}
|
||||
|
||||
this.refreshPromise = (async () => {
|
||||
const nextSnapshot = await this.backend.getWorkspace(this.organizationId);
|
||||
if (this.refreshRetryTimeout) {
|
||||
clearTimeout(this.refreshRetryTimeout);
|
||||
this.refreshRetryTimeout = null;
|
||||
}
|
||||
this.snapshot = {
|
||||
...nextSnapshot,
|
||||
repositories: nextSnapshot.repositories ?? groupWorkspaceRepositories(nextSnapshot.repos, nextSnapshot.tasks),
|
||||
};
|
||||
for (const listener of [...this.listeners]) {
|
||||
listener();
|
||||
}
|
||||
})().finally(() => {
|
||||
this.refreshPromise = null;
|
||||
});
|
||||
|
||||
await this.refreshPromise;
|
||||
}
|
||||
}
|
||||
|
||||
export function createRemoteWorkspaceClient(options: RemoteWorkspaceClientOptions): TaskWorkspaceClient {
|
||||
return new RemoteWorkspaceStore(options);
|
||||
}
|
||||
|
|
@ -5,8 +5,8 @@ import type {
|
|||
SandboxProcessesEvent,
|
||||
SessionEvent,
|
||||
TaskEvent,
|
||||
WorkbenchSessionDetail,
|
||||
WorkbenchTaskDetail,
|
||||
WorkspaceSessionDetail,
|
||||
WorkspaceTaskDetail,
|
||||
OrganizationEvent,
|
||||
OrganizationSummarySnapshot,
|
||||
} from "@sandbox-agent/foundry-shared";
|
||||
|
|
@ -48,16 +48,6 @@ export interface SandboxProcessesTopicParams {
|
|||
sandboxId: string;
|
||||
}
|
||||
|
||||
function upsertById<T extends { id: string }>(items: T[], nextItem: T, sort: (left: T, right: T) => number): T[] {
|
||||
const filtered = items.filter((item) => item.id !== nextItem.id);
|
||||
return [...filtered, nextItem].sort(sort);
|
||||
}
|
||||
|
||||
function upsertByPrId<T extends { prId: string }>(items: T[], nextItem: T, sort: (left: T, right: T) => number): T[] {
|
||||
const filtered = items.filter((item) => item.prId !== nextItem.prId);
|
||||
return [...filtered, nextItem].sort(sort);
|
||||
}
|
||||
|
||||
export const topicDefinitions = {
|
||||
app: {
|
||||
key: () => "app",
|
||||
|
|
@ -72,41 +62,7 @@ export const topicDefinitions = {
|
|||
event: "organizationUpdated",
|
||||
connect: (backend: BackendClient, params: OrganizationTopicParams) => backend.connectOrganization(params.organizationId),
|
||||
fetchInitial: (backend: BackendClient, params: OrganizationTopicParams) => backend.getOrganizationSummary(params.organizationId),
|
||||
applyEvent: (current: OrganizationSummarySnapshot, event: OrganizationEvent) => {
|
||||
switch (event.type) {
|
||||
case "taskSummaryUpdated":
|
||||
return {
|
||||
...current,
|
||||
taskSummaries: upsertById(current.taskSummaries, event.taskSummary, (left, right) => right.updatedAtMs - left.updatedAtMs),
|
||||
};
|
||||
case "taskRemoved":
|
||||
return {
|
||||
...current,
|
||||
taskSummaries: current.taskSummaries.filter((task) => task.id !== event.taskId),
|
||||
};
|
||||
case "repoAdded":
|
||||
case "repoUpdated":
|
||||
return {
|
||||
...current,
|
||||
repos: upsertById(current.repos, event.repo, (left, right) => right.latestActivityMs - left.latestActivityMs),
|
||||
};
|
||||
case "repoRemoved":
|
||||
return {
|
||||
...current,
|
||||
repos: current.repos.filter((repo) => repo.id !== event.repoId),
|
||||
};
|
||||
case "pullRequestUpdated":
|
||||
return {
|
||||
...current,
|
||||
openPullRequests: upsertByPrId(current.openPullRequests, event.pullRequest, (left, right) => right.updatedAtMs - left.updatedAtMs),
|
||||
};
|
||||
case "pullRequestRemoved":
|
||||
return {
|
||||
...current,
|
||||
openPullRequests: current.openPullRequests.filter((pullRequest) => pullRequest.prId !== event.prId),
|
||||
};
|
||||
}
|
||||
},
|
||||
applyEvent: (_current: OrganizationSummarySnapshot, event: OrganizationEvent) => event.snapshot,
|
||||
} satisfies TopicDefinition<OrganizationSummarySnapshot, OrganizationTopicParams, OrganizationEvent>,
|
||||
|
||||
task: {
|
||||
|
|
@ -114,8 +70,8 @@ export const topicDefinitions = {
|
|||
event: "taskUpdated",
|
||||
connect: (backend: BackendClient, params: TaskTopicParams) => backend.connectTask(params.organizationId, params.repoId, params.taskId),
|
||||
fetchInitial: (backend: BackendClient, params: TaskTopicParams) => backend.getTaskDetail(params.organizationId, params.repoId, params.taskId),
|
||||
applyEvent: (_current: WorkbenchTaskDetail, event: TaskEvent) => event.detail,
|
||||
} satisfies TopicDefinition<WorkbenchTaskDetail, TaskTopicParams, TaskEvent>,
|
||||
applyEvent: (_current: WorkspaceTaskDetail, event: TaskEvent) => event.detail,
|
||||
} satisfies TopicDefinition<WorkspaceTaskDetail, TaskTopicParams, TaskEvent>,
|
||||
|
||||
session: {
|
||||
key: (params: SessionTopicParams) => `session:${params.organizationId}:${params.taskId}:${params.sessionId}`,
|
||||
|
|
@ -123,13 +79,13 @@ export const topicDefinitions = {
|
|||
connect: (backend: BackendClient, params: SessionTopicParams) => backend.connectTask(params.organizationId, params.repoId, params.taskId),
|
||||
fetchInitial: (backend: BackendClient, params: SessionTopicParams) =>
|
||||
backend.getSessionDetail(params.organizationId, params.repoId, params.taskId, params.sessionId),
|
||||
applyEvent: (current: WorkbenchSessionDetail, event: SessionEvent) => {
|
||||
applyEvent: (current: WorkspaceSessionDetail, event: SessionEvent) => {
|
||||
if (event.session.sessionId !== current.sessionId) {
|
||||
return current;
|
||||
}
|
||||
return event.session;
|
||||
},
|
||||
} satisfies TopicDefinition<WorkbenchSessionDetail, SessionTopicParams, SessionEvent>,
|
||||
} satisfies TopicDefinition<WorkspaceSessionDetail, SessionTopicParams, SessionEvent>,
|
||||
|
||||
sandboxProcesses: {
|
||||
key: (params: SandboxProcessesTopicParams) => `sandbox:${params.organizationId}:${params.sandboxProviderId}:${params.sandboxId}`,
|
||||
|
|
|
|||
|
|
@ -1,64 +0,0 @@
|
|||
import type {
|
||||
TaskWorkbenchAddSessionResponse,
|
||||
TaskWorkbenchChangeModelInput,
|
||||
TaskWorkbenchCreateTaskInput,
|
||||
TaskWorkbenchCreateTaskResponse,
|
||||
TaskWorkbenchDiffInput,
|
||||
TaskWorkbenchRenameInput,
|
||||
TaskWorkbenchRenameSessionInput,
|
||||
TaskWorkbenchSelectInput,
|
||||
TaskWorkbenchSetSessionUnreadInput,
|
||||
TaskWorkbenchSendMessageInput,
|
||||
TaskWorkbenchSnapshot,
|
||||
TaskWorkbenchSessionInput,
|
||||
TaskWorkbenchUpdateDraftInput,
|
||||
} from "@sandbox-agent/foundry-shared";
|
||||
import type { BackendClient } from "./backend-client.js";
|
||||
import { getSharedMockWorkbenchClient } from "./mock/workbench-client.js";
|
||||
import { createRemoteWorkbenchClient } from "./remote/workbench-client.js";
|
||||
|
||||
export type TaskWorkbenchClientMode = "mock" | "remote";
|
||||
|
||||
export interface CreateTaskWorkbenchClientOptions {
|
||||
mode: TaskWorkbenchClientMode;
|
||||
backend?: BackendClient;
|
||||
organizationId?: string;
|
||||
}
|
||||
|
||||
export interface TaskWorkbenchClient {
|
||||
getSnapshot(): TaskWorkbenchSnapshot;
|
||||
subscribe(listener: () => void): () => void;
|
||||
createTask(input: TaskWorkbenchCreateTaskInput): Promise<TaskWorkbenchCreateTaskResponse>;
|
||||
markTaskUnread(input: TaskWorkbenchSelectInput): Promise<void>;
|
||||
renameTask(input: TaskWorkbenchRenameInput): Promise<void>;
|
||||
renameBranch(input: TaskWorkbenchRenameInput): Promise<void>;
|
||||
archiveTask(input: TaskWorkbenchSelectInput): Promise<void>;
|
||||
publishPr(input: TaskWorkbenchSelectInput): Promise<void>;
|
||||
revertFile(input: TaskWorkbenchDiffInput): Promise<void>;
|
||||
updateDraft(input: TaskWorkbenchUpdateDraftInput): Promise<void>;
|
||||
sendMessage(input: TaskWorkbenchSendMessageInput): Promise<void>;
|
||||
stopAgent(input: TaskWorkbenchSessionInput): Promise<void>;
|
||||
setSessionUnread(input: TaskWorkbenchSetSessionUnreadInput): Promise<void>;
|
||||
renameSession(input: TaskWorkbenchRenameSessionInput): Promise<void>;
|
||||
closeSession(input: TaskWorkbenchSessionInput): Promise<void>;
|
||||
addSession(input: TaskWorkbenchSelectInput): Promise<TaskWorkbenchAddSessionResponse>;
|
||||
changeModel(input: TaskWorkbenchChangeModelInput): Promise<void>;
|
||||
}
|
||||
|
||||
export function createTaskWorkbenchClient(options: CreateTaskWorkbenchClientOptions): TaskWorkbenchClient {
|
||||
if (options.mode === "mock") {
|
||||
return getSharedMockWorkbenchClient();
|
||||
}
|
||||
|
||||
if (!options.backend) {
|
||||
throw new Error("Remote task workbench client requires a backend client");
|
||||
}
|
||||
if (!options.organizationId) {
|
||||
throw new Error("Remote task workbench client requires a organization id");
|
||||
}
|
||||
|
||||
return createRemoteWorkbenchClient({
|
||||
backend: options.backend,
|
||||
organizationId: options.organizationId,
|
||||
});
|
||||
}
|
||||
63
foundry/packages/client/src/workspace-client.ts
Normal file
63
foundry/packages/client/src/workspace-client.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import type {
|
||||
TaskWorkspaceAddSessionResponse,
|
||||
TaskWorkspaceChangeModelInput,
|
||||
TaskWorkspaceCreateTaskInput,
|
||||
TaskWorkspaceCreateTaskResponse,
|
||||
TaskWorkspaceDiffInput,
|
||||
TaskWorkspaceRenameInput,
|
||||
TaskWorkspaceRenameSessionInput,
|
||||
TaskWorkspaceSelectInput,
|
||||
TaskWorkspaceSetSessionUnreadInput,
|
||||
TaskWorkspaceSendMessageInput,
|
||||
TaskWorkspaceSnapshot,
|
||||
TaskWorkspaceSessionInput,
|
||||
TaskWorkspaceUpdateDraftInput,
|
||||
} from "@sandbox-agent/foundry-shared";
|
||||
import type { BackendClient } from "./backend-client.js";
|
||||
import { getSharedMockWorkspaceClient } from "./mock/workspace-client.js";
|
||||
import { createRemoteWorkspaceClient } from "./remote/workspace-client.js";
|
||||
|
||||
export type TaskWorkspaceClientMode = "mock" | "remote";
|
||||
|
||||
export interface CreateTaskWorkspaceClientOptions {
|
||||
mode: TaskWorkspaceClientMode;
|
||||
backend?: BackendClient;
|
||||
organizationId?: string;
|
||||
}
|
||||
|
||||
export interface TaskWorkspaceClient {
|
||||
getSnapshot(): TaskWorkspaceSnapshot;
|
||||
subscribe(listener: () => void): () => void;
|
||||
createTask(input: TaskWorkspaceCreateTaskInput): Promise<TaskWorkspaceCreateTaskResponse>;
|
||||
markTaskUnread(input: TaskWorkspaceSelectInput): Promise<void>;
|
||||
renameTask(input: TaskWorkspaceRenameInput): Promise<void>;
|
||||
archiveTask(input: TaskWorkspaceSelectInput): Promise<void>;
|
||||
publishPr(input: TaskWorkspaceSelectInput): Promise<void>;
|
||||
revertFile(input: TaskWorkspaceDiffInput): Promise<void>;
|
||||
updateDraft(input: TaskWorkspaceUpdateDraftInput): Promise<void>;
|
||||
sendMessage(input: TaskWorkspaceSendMessageInput): Promise<void>;
|
||||
stopAgent(input: TaskWorkspaceSessionInput): Promise<void>;
|
||||
setSessionUnread(input: TaskWorkspaceSetSessionUnreadInput): Promise<void>;
|
||||
renameSession(input: TaskWorkspaceRenameSessionInput): Promise<void>;
|
||||
closeSession(input: TaskWorkspaceSessionInput): Promise<void>;
|
||||
addSession(input: TaskWorkspaceSelectInput): Promise<TaskWorkspaceAddSessionResponse>;
|
||||
changeModel(input: TaskWorkspaceChangeModelInput): Promise<void>;
|
||||
}
|
||||
|
||||
export function createTaskWorkspaceClient(options: CreateTaskWorkspaceClientOptions): TaskWorkspaceClient {
|
||||
if (options.mode === "mock") {
|
||||
return getSharedMockWorkspaceClient();
|
||||
}
|
||||
|
||||
if (!options.backend) {
|
||||
throw new Error("Remote task workspace client requires a backend client");
|
||||
}
|
||||
if (!options.organizationId) {
|
||||
throw new Error("Remote task workspace client requires a organization id");
|
||||
}
|
||||
|
||||
return createRemoteWorkspaceClient({
|
||||
backend: options.backend,
|
||||
organizationId: options.organizationId,
|
||||
});
|
||||
}
|
||||
|
|
@ -1,17 +1,17 @@
|
|||
import type {
|
||||
WorkbenchAgentKind as AgentKind,
|
||||
WorkbenchSession as AgentSession,
|
||||
WorkbenchDiffLineKind as DiffLineKind,
|
||||
WorkbenchFileTreeNode as FileTreeNode,
|
||||
WorkbenchTask as Task,
|
||||
TaskWorkbenchSnapshot,
|
||||
WorkbenchHistoryEvent as HistoryEvent,
|
||||
WorkbenchModelGroup as ModelGroup,
|
||||
WorkbenchModelId as ModelId,
|
||||
WorkbenchParsedDiffLine as ParsedDiffLine,
|
||||
WorkbenchRepositorySection,
|
||||
WorkbenchRepo,
|
||||
WorkbenchTranscriptEvent as TranscriptEvent,
|
||||
WorkspaceAgentKind as AgentKind,
|
||||
WorkspaceSession as AgentSession,
|
||||
WorkspaceDiffLineKind as DiffLineKind,
|
||||
WorkspaceFileTreeNode as FileTreeNode,
|
||||
WorkspaceTask as Task,
|
||||
TaskWorkspaceSnapshot,
|
||||
WorkspaceHistoryEvent as HistoryEvent,
|
||||
WorkspaceModelGroup as ModelGroup,
|
||||
WorkspaceModelId as ModelId,
|
||||
WorkspaceParsedDiffLine as ParsedDiffLine,
|
||||
WorkspaceRepositorySection,
|
||||
WorkspaceRepo,
|
||||
WorkspaceTranscriptEvent as TranscriptEvent,
|
||||
} from "@sandbox-agent/foundry-shared";
|
||||
import rivetDevFixture from "../../../scripts/data/rivet-dev.json" with { type: "json" };
|
||||
|
||||
|
|
@ -1300,7 +1300,7 @@ export function buildInitialTasks(): Task[] {
|
|||
* Uses real public repos so the mock sidebar matches what an actual rivet-dev
|
||||
* organization would show after a GitHub sync.
|
||||
*/
|
||||
function buildMockRepos(): WorkbenchRepo[] {
|
||||
function buildMockRepos(): WorkspaceRepo[] {
|
||||
return rivetDevFixture.repos.map((r) => ({
|
||||
id: repoIdFromFullName(r.fullName),
|
||||
label: r.fullName,
|
||||
|
|
@ -1349,19 +1349,19 @@ function buildPrTasks(): Task[] {
|
|||
});
|
||||
}
|
||||
|
||||
export function buildInitialMockLayoutViewModel(): TaskWorkbenchSnapshot {
|
||||
export function buildInitialMockLayoutViewModel(): TaskWorkspaceSnapshot {
|
||||
const repos = buildMockRepos();
|
||||
const tasks = [...buildInitialTasks(), ...buildPrTasks()];
|
||||
return {
|
||||
organizationId: "default",
|
||||
repos,
|
||||
repositories: groupWorkbenchRepositories(repos, tasks),
|
||||
repositories: groupWorkspaceRepositories(repos, tasks),
|
||||
tasks,
|
||||
};
|
||||
}
|
||||
|
||||
export function groupWorkbenchRepositories(repos: WorkbenchRepo[], tasks: Task[]): WorkbenchRepositorySection[] {
|
||||
const grouped = new Map<string, WorkbenchRepositorySection>();
|
||||
export function groupWorkspaceRepositories(repos: WorkspaceRepo[], tasks: Task[]): WorkspaceRepositorySection[] {
|
||||
const grouped = new Map<string, WorkspaceRepositorySection>();
|
||||
|
||||
for (const repo of repos) {
|
||||
grouped.set(repo.id, {
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { randomUUID } from "node:crypto";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { HistoryEvent, RepoOverview } from "@sandbox-agent/foundry-shared";
|
||||
import type { AuditLogEvent as HistoryEvent, RepoOverview } from "@sandbox-agent/foundry-shared";
|
||||
import { createBackendClient } from "../../src/backend-client.js";
|
||||
import { requireImportedRepo } from "./helpers.js";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import type { TaskRecord, HistoryEvent } from "@sandbox-agent/foundry-shared";
|
||||
import type { AuditLogEvent as HistoryEvent, TaskRecord } from "@sandbox-agent/foundry-shared";
|
||||
import { createBackendClient } from "../../src/backend-client.js";
|
||||
import { requireImportedRepo } from "./helpers.js";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import type { TaskWorkbenchSnapshot, WorkbenchSession, WorkbenchTask, WorkbenchModelId, WorkbenchTranscriptEvent } from "@sandbox-agent/foundry-shared";
|
||||
import type { TaskWorkspaceSnapshot, WorkspaceSession, WorkspaceTask, WorkspaceModelId, WorkspaceTranscriptEvent } from "@sandbox-agent/foundry-shared";
|
||||
import { createBackendClient } from "../../src/backend-client.js";
|
||||
import { requireImportedRepo } from "./helpers.js";
|
||||
|
||||
|
|
@ -13,7 +13,7 @@ function requiredEnv(name: string): string {
|
|||
return value;
|
||||
}
|
||||
|
||||
function workbenchModelEnv(name: string, fallback: WorkbenchModelId): WorkbenchModelId {
|
||||
function workspaceModelEnv(name: string, fallback: WorkspaceModelId): WorkspaceModelId {
|
||||
const value = process.env[name]?.trim();
|
||||
switch (value) {
|
||||
case "claude-sonnet-4":
|
||||
|
|
@ -50,7 +50,7 @@ async function poll<T>(label: string, timeoutMs: number, intervalMs: number, fn:
|
|||
}
|
||||
}
|
||||
|
||||
function findTask(snapshot: TaskWorkbenchSnapshot, taskId: string): WorkbenchTask {
|
||||
function findTask(snapshot: TaskWorkspaceSnapshot, taskId: string): WorkspaceTask {
|
||||
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
||||
if (!task) {
|
||||
throw new Error(`task ${taskId} missing from snapshot`);
|
||||
|
|
@ -58,7 +58,7 @@ function findTask(snapshot: TaskWorkbenchSnapshot, taskId: string): WorkbenchTas
|
|||
return task;
|
||||
}
|
||||
|
||||
function findTab(task: WorkbenchTask, sessionId: string): WorkbenchSession {
|
||||
function findTab(task: WorkspaceTask, sessionId: string): WorkspaceSession {
|
||||
const tab = task.sessions.find((candidate) => candidate.id === sessionId);
|
||||
if (!tab) {
|
||||
throw new Error(`tab ${sessionId} missing from task ${task.id}`);
|
||||
|
|
@ -66,7 +66,7 @@ function findTab(task: WorkbenchTask, sessionId: string): WorkbenchSession {
|
|||
return tab;
|
||||
}
|
||||
|
||||
function extractEventText(event: WorkbenchTranscriptEvent): string {
|
||||
function extractEventText(event: WorkspaceTranscriptEvent): string {
|
||||
const payload = event.payload;
|
||||
if (!payload || typeof payload !== "object") {
|
||||
return String(payload ?? "");
|
||||
|
|
@ -127,7 +127,7 @@ function extractEventText(event: WorkbenchTranscriptEvent): string {
|
|||
return JSON.stringify(payload);
|
||||
}
|
||||
|
||||
function transcriptIncludesAgentText(transcript: WorkbenchTranscriptEvent[], expectedText: string): boolean {
|
||||
function transcriptIncludesAgentText(transcript: WorkspaceTranscriptEvent[], expectedText: string): boolean {
|
||||
return transcript
|
||||
.filter((event) => event.sender === "agent")
|
||||
.map((event) => extractEventText(event))
|
||||
|
|
@ -135,15 +135,15 @@ function transcriptIncludesAgentText(transcript: WorkbenchTranscriptEvent[], exp
|
|||
.includes(expectedText);
|
||||
}
|
||||
|
||||
describe("e2e(client): workbench flows", () => {
|
||||
describe("e2e(client): workspace flows", () => {
|
||||
it.skipIf(!RUN_WORKBENCH_E2E)(
|
||||
"creates a task from an imported repo, adds sessions, exchanges messages, and manages workbench state",
|
||||
"creates a task from an imported repo, adds sessions, exchanges messages, and manages workspace state",
|
||||
{ timeout: 20 * 60_000 },
|
||||
async () => {
|
||||
const endpoint = process.env.HF_E2E_BACKEND_ENDPOINT?.trim() || "http://127.0.0.1:7741/v1/rivet";
|
||||
const organizationId = process.env.HF_E2E_WORKSPACE?.trim() || "default";
|
||||
const repoRemote = requiredEnv("HF_E2E_GITHUB_REPO");
|
||||
const model = workbenchModelEnv("HF_E2E_MODEL", "gpt-5.3-codex");
|
||||
const model = workspaceModelEnv("HF_E2E_MODEL", "gpt-5.3-codex");
|
||||
const runId = `wb-${Date.now().toString(36)}`;
|
||||
const expectedFile = `${runId}.txt`;
|
||||
const expectedInitialReply = `WORKBENCH_READY_${runId}`;
|
||||
|
|
@ -155,9 +155,9 @@ describe("e2e(client): workbench flows", () => {
|
|||
});
|
||||
|
||||
const repo = await requireImportedRepo(client, organizationId, repoRemote);
|
||||
const created = await client.createWorkbenchTask(organizationId, {
|
||||
const created = await client.createWorkspaceTask(organizationId, {
|
||||
repoId: repo.repoId,
|
||||
title: `Workbench E2E ${runId}`,
|
||||
title: `Workspace E2E ${runId}`,
|
||||
branch: `e2e/${runId}`,
|
||||
model,
|
||||
task: `Reply with exactly: ${expectedInitialReply}`,
|
||||
|
|
@ -167,7 +167,7 @@ describe("e2e(client): workbench flows", () => {
|
|||
"task provisioning",
|
||||
12 * 60_000,
|
||||
2_000,
|
||||
async () => findTask(await client.getWorkbench(organizationId), created.taskId),
|
||||
async () => findTask(await client.getWorkspace(organizationId), created.taskId),
|
||||
(task) => task.branch === `e2e/${runId}` && task.sessions.length > 0,
|
||||
);
|
||||
|
||||
|
|
@ -177,7 +177,7 @@ describe("e2e(client): workbench flows", () => {
|
|||
"initial agent response",
|
||||
12 * 60_000,
|
||||
2_000,
|
||||
async () => findTask(await client.getWorkbench(organizationId), created.taskId),
|
||||
async () => findTask(await client.getWorkspace(organizationId), created.taskId),
|
||||
(task) => {
|
||||
const tab = findTab(task, primaryTab.id);
|
||||
return task.status === "idle" && tab.status === "idle" && transcriptIncludesAgentText(tab.transcript, expectedInitialReply);
|
||||
|
|
@ -187,28 +187,28 @@ describe("e2e(client): workbench flows", () => {
|
|||
expect(findTab(initialCompleted, primaryTab.id).sessionId).toBeTruthy();
|
||||
expect(transcriptIncludesAgentText(findTab(initialCompleted, primaryTab.id).transcript, expectedInitialReply)).toBe(true);
|
||||
|
||||
await client.renameWorkbenchTask(organizationId, {
|
||||
await client.renameWorkspaceTask(organizationId, {
|
||||
taskId: created.taskId,
|
||||
value: `Workbench E2E ${runId} Renamed`,
|
||||
value: `Workspace E2E ${runId} Renamed`,
|
||||
});
|
||||
await client.renameWorkbenchSession(organizationId, {
|
||||
await client.renameWorkspaceSession(organizationId, {
|
||||
taskId: created.taskId,
|
||||
sessionId: primaryTab.id,
|
||||
title: "Primary Session",
|
||||
});
|
||||
|
||||
const secondTab = await client.createWorkbenchSession(organizationId, {
|
||||
const secondTab = await client.createWorkspaceSession(organizationId, {
|
||||
taskId: created.taskId,
|
||||
model,
|
||||
});
|
||||
|
||||
await client.renameWorkbenchSession(organizationId, {
|
||||
await client.renameWorkspaceSession(organizationId, {
|
||||
taskId: created.taskId,
|
||||
sessionId: secondTab.sessionId,
|
||||
title: "Follow-up Session",
|
||||
});
|
||||
|
||||
await client.updateWorkbenchDraft(organizationId, {
|
||||
await client.updateWorkspaceDraft(organizationId, {
|
||||
taskId: created.taskId,
|
||||
sessionId: secondTab.sessionId,
|
||||
text: [
|
||||
|
|
@ -226,11 +226,11 @@ describe("e2e(client): workbench flows", () => {
|
|||
],
|
||||
});
|
||||
|
||||
const drafted = findTask(await client.getWorkbench(organizationId), created.taskId);
|
||||
const drafted = findTask(await client.getWorkspace(organizationId), created.taskId);
|
||||
expect(findTab(drafted, secondTab.sessionId).draft.text).toContain(expectedReply);
|
||||
expect(findTab(drafted, secondTab.sessionId).draft.attachments).toHaveLength(1);
|
||||
|
||||
await client.sendWorkbenchMessage(organizationId, {
|
||||
await client.sendWorkspaceMessage(organizationId, {
|
||||
taskId: created.taskId,
|
||||
sessionId: secondTab.sessionId,
|
||||
text: [
|
||||
|
|
@ -252,7 +252,7 @@ describe("e2e(client): workbench flows", () => {
|
|||
"follow-up session response",
|
||||
10 * 60_000,
|
||||
2_000,
|
||||
async () => findTask(await client.getWorkbench(organizationId), created.taskId),
|
||||
async () => findTask(await client.getWorkspace(organizationId), created.taskId),
|
||||
(task) => {
|
||||
const tab = findTab(task, secondTab.sessionId);
|
||||
return (
|
||||
|
|
@ -265,17 +265,17 @@ describe("e2e(client): workbench flows", () => {
|
|||
expect(transcriptIncludesAgentText(secondTranscript, expectedReply)).toBe(true);
|
||||
expect(withSecondReply.fileChanges.some((file) => file.path === expectedFile)).toBe(true);
|
||||
|
||||
await client.setWorkbenchSessionUnread(organizationId, {
|
||||
await client.setWorkspaceSessionUnread(organizationId, {
|
||||
taskId: created.taskId,
|
||||
sessionId: secondTab.sessionId,
|
||||
unread: false,
|
||||
});
|
||||
await client.markWorkbenchUnread(organizationId, { taskId: created.taskId });
|
||||
await client.markWorkspaceUnread(organizationId, { taskId: created.taskId });
|
||||
|
||||
const unreadSnapshot = findTask(await client.getWorkbench(organizationId), created.taskId);
|
||||
const unreadSnapshot = findTask(await client.getWorkspace(organizationId), created.taskId);
|
||||
expect(unreadSnapshot.sessions.some((tab) => tab.unread)).toBe(true);
|
||||
|
||||
await client.closeWorkbenchSession(organizationId, {
|
||||
await client.closeWorkspaceSession(organizationId, {
|
||||
taskId: created.taskId,
|
||||
sessionId: secondTab.sessionId,
|
||||
});
|
||||
|
|
@ -284,26 +284,26 @@ describe("e2e(client): workbench flows", () => {
|
|||
"secondary session closed",
|
||||
30_000,
|
||||
1_000,
|
||||
async () => findTask(await client.getWorkbench(organizationId), created.taskId),
|
||||
async () => findTask(await client.getWorkspace(organizationId), created.taskId),
|
||||
(task) => !task.sessions.some((tab) => tab.id === secondTab.sessionId),
|
||||
);
|
||||
expect(closedSnapshot.sessions).toHaveLength(1);
|
||||
|
||||
await client.revertWorkbenchFile(organizationId, {
|
||||
await client.revertWorkspaceFile(organizationId, {
|
||||
taskId: created.taskId,
|
||||
path: expectedFile,
|
||||
});
|
||||
|
||||
const revertedSnapshot = await poll(
|
||||
"file revert reflected in workbench",
|
||||
"file revert reflected in workspace",
|
||||
30_000,
|
||||
1_000,
|
||||
async () => findTask(await client.getWorkbench(organizationId), created.taskId),
|
||||
async () => findTask(await client.getWorkspace(organizationId), created.taskId),
|
||||
(task) => !task.fileChanges.some((file) => file.path === expectedFile),
|
||||
);
|
||||
|
||||
expect(revertedSnapshot.fileChanges.some((file) => file.path === expectedFile)).toBe(false);
|
||||
expect(revertedSnapshot.title).toBe(`Workbench E2E ${runId} Renamed`);
|
||||
expect(revertedSnapshot.title).toBe(`Workspace E2E ${runId} Renamed`);
|
||||
expect(findTab(revertedSnapshot, primaryTab.id).sessionName).toBe("Primary Session");
|
||||
},
|
||||
);
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
createFoundryLogger,
|
||||
type TaskWorkbenchSnapshot,
|
||||
type WorkbenchSession,
|
||||
type WorkbenchTask,
|
||||
type WorkbenchModelId,
|
||||
type WorkbenchTranscriptEvent,
|
||||
type TaskWorkspaceSnapshot,
|
||||
type WorkspaceSession,
|
||||
type WorkspaceTask,
|
||||
type WorkspaceModelId,
|
||||
type WorkspaceTranscriptEvent,
|
||||
} from "@sandbox-agent/foundry-shared";
|
||||
import { createBackendClient } from "../../src/backend-client.js";
|
||||
import { requireImportedRepo } from "./helpers.js";
|
||||
|
|
@ -14,7 +14,7 @@ const RUN_WORKBENCH_LOAD_E2E = process.env.HF_ENABLE_DAEMON_WORKBENCH_LOAD_E2E =
|
|||
const logger = createFoundryLogger({
|
||||
service: "foundry-client-e2e",
|
||||
bindings: {
|
||||
suite: "workbench-load",
|
||||
suite: "workspace-load",
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -26,7 +26,7 @@ function requiredEnv(name: string): string {
|
|||
return value;
|
||||
}
|
||||
|
||||
function workbenchModelEnv(name: string, fallback: WorkbenchModelId): WorkbenchModelId {
|
||||
function workspaceModelEnv(name: string, fallback: WorkspaceModelId): WorkspaceModelId {
|
||||
const value = process.env[name]?.trim();
|
||||
switch (value) {
|
||||
case "claude-sonnet-4":
|
||||
|
|
@ -72,7 +72,7 @@ async function poll<T>(label: string, timeoutMs: number, intervalMs: number, fn:
|
|||
}
|
||||
}
|
||||
|
||||
function findTask(snapshot: TaskWorkbenchSnapshot, taskId: string): WorkbenchTask {
|
||||
function findTask(snapshot: TaskWorkspaceSnapshot, taskId: string): WorkspaceTask {
|
||||
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
||||
if (!task) {
|
||||
throw new Error(`task ${taskId} missing from snapshot`);
|
||||
|
|
@ -80,7 +80,7 @@ function findTask(snapshot: TaskWorkbenchSnapshot, taskId: string): WorkbenchTas
|
|||
return task;
|
||||
}
|
||||
|
||||
function findTab(task: WorkbenchTask, sessionId: string): WorkbenchSession {
|
||||
function findTab(task: WorkspaceTask, sessionId: string): WorkspaceSession {
|
||||
const tab = task.sessions.find((candidate) => candidate.id === sessionId);
|
||||
if (!tab) {
|
||||
throw new Error(`tab ${sessionId} missing from task ${task.id}`);
|
||||
|
|
@ -88,7 +88,7 @@ function findTab(task: WorkbenchTask, sessionId: string): WorkbenchSession {
|
|||
return tab;
|
||||
}
|
||||
|
||||
function extractEventText(event: WorkbenchTranscriptEvent): string {
|
||||
function extractEventText(event: WorkspaceTranscriptEvent): string {
|
||||
const payload = event.payload;
|
||||
if (!payload || typeof payload !== "object") {
|
||||
return String(payload ?? "");
|
||||
|
|
@ -138,7 +138,7 @@ function extractEventText(event: WorkbenchTranscriptEvent): string {
|
|||
return typeof envelope.method === "string" ? envelope.method : JSON.stringify(payload);
|
||||
}
|
||||
|
||||
function transcriptIncludesAgentText(transcript: WorkbenchTranscriptEvent[], expectedText: string): boolean {
|
||||
function transcriptIncludesAgentText(transcript: WorkspaceTranscriptEvent[], expectedText: string): boolean {
|
||||
return transcript
|
||||
.filter((event) => event.sender === "agent")
|
||||
.map((event) => extractEventText(event))
|
||||
|
|
@ -150,7 +150,7 @@ function average(values: number[]): number {
|
|||
return values.reduce((sum, value) => sum + value, 0) / Math.max(values.length, 1);
|
||||
}
|
||||
|
||||
async function measureWorkbenchSnapshot(
|
||||
async function measureWorkspaceSnapshot(
|
||||
client: ReturnType<typeof createBackendClient>,
|
||||
organizationId: string,
|
||||
iterations: number,
|
||||
|
|
@ -163,11 +163,11 @@ async function measureWorkbenchSnapshot(
|
|||
transcriptEventCount: number;
|
||||
}> {
|
||||
const durations: number[] = [];
|
||||
let snapshot: TaskWorkbenchSnapshot | null = null;
|
||||
let snapshot: TaskWorkspaceSnapshot | null = null;
|
||||
|
||||
for (let index = 0; index < iterations; index += 1) {
|
||||
const startedAt = performance.now();
|
||||
snapshot = await client.getWorkbench(organizationId);
|
||||
snapshot = await client.getWorkspace(organizationId);
|
||||
durations.push(performance.now() - startedAt);
|
||||
}
|
||||
|
||||
|
|
@ -191,12 +191,12 @@ async function measureWorkbenchSnapshot(
|
|||
};
|
||||
}
|
||||
|
||||
describe("e2e(client): workbench load", () => {
|
||||
describe("e2e(client): workspace load", () => {
|
||||
it.skipIf(!RUN_WORKBENCH_LOAD_E2E)("runs a simple sequential load profile against the real backend", { timeout: 30 * 60_000 }, async () => {
|
||||
const endpoint = process.env.HF_E2E_BACKEND_ENDPOINT?.trim() || "http://127.0.0.1:7741/v1/rivet";
|
||||
const organizationId = process.env.HF_E2E_WORKSPACE?.trim() || "default";
|
||||
const repoRemote = requiredEnv("HF_E2E_GITHUB_REPO");
|
||||
const model = workbenchModelEnv("HF_E2E_MODEL", "gpt-5.3-codex");
|
||||
const model = workspaceModelEnv("HF_E2E_MODEL", "gpt-5.3-codex");
|
||||
const taskCount = intEnv("HF_LOAD_TASK_COUNT", 3);
|
||||
const extraSessionCount = intEnv("HF_LOAD_EXTRA_SESSION_COUNT", 2);
|
||||
const pollIntervalMs = intEnv("HF_LOAD_POLL_INTERVAL_MS", 2_000);
|
||||
|
|
@ -220,16 +220,16 @@ describe("e2e(client): workbench load", () => {
|
|||
transcriptEventCount: number;
|
||||
}> = [];
|
||||
|
||||
snapshotSeries.push(await measureWorkbenchSnapshot(client, organizationId, 2));
|
||||
snapshotSeries.push(await measureWorkspaceSnapshot(client, organizationId, 2));
|
||||
|
||||
for (let taskIndex = 0; taskIndex < taskCount; taskIndex += 1) {
|
||||
const runId = `load-${taskIndex}-${Date.now().toString(36)}`;
|
||||
const initialReply = `LOAD_INIT_${runId}`;
|
||||
|
||||
const createStartedAt = performance.now();
|
||||
const created = await client.createWorkbenchTask(organizationId, {
|
||||
const created = await client.createWorkspaceTask(organizationId, {
|
||||
repoId: repo.repoId,
|
||||
title: `Workbench Load ${runId}`,
|
||||
title: `Workspace Load ${runId}`,
|
||||
branch: `load/${runId}`,
|
||||
model,
|
||||
task: `Reply with exactly: ${initialReply}`,
|
||||
|
|
@ -241,7 +241,7 @@ describe("e2e(client): workbench load", () => {
|
|||
`task ${runId} provisioning`,
|
||||
12 * 60_000,
|
||||
pollIntervalMs,
|
||||
async () => findTask(await client.getWorkbench(organizationId), created.taskId),
|
||||
async () => findTask(await client.getWorkspace(organizationId), created.taskId),
|
||||
(task) => {
|
||||
const tab = task.sessions[0];
|
||||
return Boolean(tab && task.status === "idle" && tab.status === "idle" && transcriptIncludesAgentText(tab.transcript, initialReply));
|
||||
|
|
@ -256,13 +256,13 @@ describe("e2e(client): workbench load", () => {
|
|||
for (let sessionIndex = 0; sessionIndex < extraSessionCount; sessionIndex += 1) {
|
||||
const expectedReply = `LOAD_REPLY_${runId}_${sessionIndex}`;
|
||||
const createSessionStartedAt = performance.now();
|
||||
const createdSession = await client.createWorkbenchSession(organizationId, {
|
||||
const createdSession = await client.createWorkspaceSession(organizationId, {
|
||||
taskId: created.taskId,
|
||||
model,
|
||||
});
|
||||
createSessionLatencies.push(performance.now() - createSessionStartedAt);
|
||||
|
||||
await client.sendWorkbenchMessage(organizationId, {
|
||||
await client.sendWorkspaceMessage(organizationId, {
|
||||
taskId: created.taskId,
|
||||
sessionId: createdSession.sessionId,
|
||||
text: `Run pwd in the repo, then reply with exactly: ${expectedReply}`,
|
||||
|
|
@ -274,7 +274,7 @@ describe("e2e(client): workbench load", () => {
|
|||
`task ${runId} session ${sessionIndex} reply`,
|
||||
10 * 60_000,
|
||||
pollIntervalMs,
|
||||
async () => findTask(await client.getWorkbench(organizationId), created.taskId),
|
||||
async () => findTask(await client.getWorkspace(organizationId), created.taskId),
|
||||
(task) => {
|
||||
const tab = findTab(task, createdSession.sessionId);
|
||||
return tab.status === "idle" && transcriptIncludesAgentText(tab.transcript, expectedReply);
|
||||
|
|
@ -285,14 +285,14 @@ describe("e2e(client): workbench load", () => {
|
|||
expect(transcriptIncludesAgentText(findTab(withReply, createdSession.sessionId).transcript, expectedReply)).toBe(true);
|
||||
}
|
||||
|
||||
const snapshotMetrics = await measureWorkbenchSnapshot(client, organizationId, 3);
|
||||
const snapshotMetrics = await measureWorkspaceSnapshot(client, organizationId, 3);
|
||||
snapshotSeries.push(snapshotMetrics);
|
||||
logger.info(
|
||||
{
|
||||
taskIndex: taskIndex + 1,
|
||||
...snapshotMetrics,
|
||||
},
|
||||
"workbench_load_snapshot",
|
||||
"workspace_load_snapshot",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -314,7 +314,7 @@ describe("e2e(client): workbench load", () => {
|
|||
snapshotTranscriptFinalCount: lastSnapshot.transcriptEventCount,
|
||||
};
|
||||
|
||||
logger.info(summary, "workbench_load_summary");
|
||||
logger.info(summary, "workspace_load_summary");
|
||||
|
||||
expect(createTaskLatencies.length).toBe(taskCount);
|
||||
expect(provisionLatencies.length).toBe(taskCount);
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { historyKey, organizationKey, repositoryKey, taskKey, taskSandboxKey } from "../src/keys.js";
|
||||
import { auditLogKey, organizationKey, repositoryKey, taskKey, taskSandboxKey } from "../src/keys.js";
|
||||
|
||||
describe("actor keys", () => {
|
||||
it("prefixes every key with organization namespace", () => {
|
||||
|
|
@ -8,7 +8,7 @@ describe("actor keys", () => {
|
|||
repositoryKey("default", "repo"),
|
||||
taskKey("default", "repo", "task"),
|
||||
taskSandboxKey("default", "sbx"),
|
||||
historyKey("default", "repo"),
|
||||
auditLogKey("default", "repo"),
|
||||
];
|
||||
|
||||
for (const key of keys) {
|
||||
|
|
|
|||
|
|
@ -115,17 +115,24 @@ describe("RemoteSubscriptionManager", () => {
|
|||
]);
|
||||
|
||||
conn.emit("organizationUpdated", {
|
||||
type: "taskSummaryUpdated",
|
||||
taskSummary: {
|
||||
id: "task-1",
|
||||
repoId: "repo-1",
|
||||
title: "Updated task",
|
||||
status: "running",
|
||||
repoName: "repo-1",
|
||||
updatedAtMs: 20,
|
||||
branch: "feature/live",
|
||||
pullRequest: null,
|
||||
sessionsSummary: [],
|
||||
type: "organizationUpdated",
|
||||
snapshot: {
|
||||
organizationId: "org-1",
|
||||
repos: [],
|
||||
taskSummaries: [
|
||||
{
|
||||
id: "task-1",
|
||||
repoId: "repo-1",
|
||||
title: "Updated task",
|
||||
status: "running",
|
||||
repoName: "repo-1",
|
||||
updatedAtMs: 20,
|
||||
branch: "feature/live",
|
||||
pullRequest: null,
|
||||
sessionsSummary: [],
|
||||
},
|
||||
],
|
||||
openPullRequests: [],
|
||||
},
|
||||
} satisfies OrganizationEvent);
|
||||
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ import type {
|
|||
FoundryAppSnapshot,
|
||||
FoundryOrganization,
|
||||
TaskStatus,
|
||||
TaskWorkbenchSnapshot,
|
||||
WorkbenchSandboxSummary,
|
||||
WorkbenchSessionSummary,
|
||||
WorkbenchTaskStatus,
|
||||
TaskWorkspaceSnapshot,
|
||||
WorkspaceSandboxSummary,
|
||||
WorkspaceSessionSummary,
|
||||
WorkspaceTaskStatus,
|
||||
} from "@sandbox-agent/foundry-shared";
|
||||
import { useSubscription } from "@sandbox-agent/foundry-client";
|
||||
import type { DebugSubscriptionTopic } from "@sandbox-agent/foundry-client";
|
||||
|
|
@ -18,7 +18,7 @@ import { describeTaskState } from "../features/tasks/status";
|
|||
|
||||
interface DevPanelProps {
|
||||
organizationId: string;
|
||||
snapshot: TaskWorkbenchSnapshot;
|
||||
snapshot: TaskWorkspaceSnapshot;
|
||||
organization?: FoundryOrganization | null;
|
||||
focusedTask?: DevPanelFocusedTask | null;
|
||||
}
|
||||
|
|
@ -27,14 +27,14 @@ export interface DevPanelFocusedTask {
|
|||
id: string;
|
||||
repoId: string;
|
||||
title: string | null;
|
||||
status: WorkbenchTaskStatus;
|
||||
status: WorkspaceTaskStatus;
|
||||
runtimeStatus?: TaskStatus | null;
|
||||
statusMessage?: string | null;
|
||||
branch?: string | null;
|
||||
activeSandboxId?: string | null;
|
||||
activeSessionId?: string | null;
|
||||
sandboxes?: WorkbenchSandboxSummary[];
|
||||
sessions?: WorkbenchSessionSummary[];
|
||||
sandboxes?: WorkspaceSandboxSummary[];
|
||||
sessions?: WorkspaceSessionSummary[];
|
||||
}
|
||||
|
||||
interface TopicInfo {
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ import { useStyletron } from "baseui";
|
|||
import {
|
||||
createErrorContext,
|
||||
type FoundryOrganization,
|
||||
type TaskWorkbenchSnapshot,
|
||||
type WorkbenchOpenPrSummary,
|
||||
type WorkbenchSessionSummary,
|
||||
type WorkbenchTaskDetail,
|
||||
type WorkbenchTaskSummary,
|
||||
type TaskWorkspaceSnapshot,
|
||||
type WorkspaceOpenPrSummary,
|
||||
type WorkspaceSessionSummary,
|
||||
type WorkspaceTaskDetail,
|
||||
type WorkspaceTaskSummary,
|
||||
} from "@sandbox-agent/foundry-shared";
|
||||
import { useSubscription } from "@sandbox-agent/foundry-client";
|
||||
|
||||
|
|
@ -39,7 +39,7 @@ import {
|
|||
type Message,
|
||||
type ModelId,
|
||||
} from "./mock-layout/view-model";
|
||||
import { activeMockOrganization, useMockAppSnapshot } from "../lib/mock-app";
|
||||
import { activeMockOrganization, activeMockUser, useMockAppClient, useMockAppSnapshot } from "../lib/mock-app";
|
||||
import { backendClient } from "../lib/backend";
|
||||
import { subscriptionManager } from "../lib/subscription";
|
||||
import { describeTaskState, isProvisioningTaskStatus } from "../features/tasks/status";
|
||||
|
|
@ -131,7 +131,7 @@ function GithubInstallationWarning({
|
|||
}
|
||||
|
||||
function toSessionModel(
|
||||
summary: WorkbenchSessionSummary,
|
||||
summary: WorkspaceSessionSummary,
|
||||
sessionDetail?: { draft: Task["sessions"][number]["draft"]; transcript: Task["sessions"][number]["transcript"] },
|
||||
): Task["sessions"][number] {
|
||||
return {
|
||||
|
|
@ -155,8 +155,8 @@ function toSessionModel(
|
|||
}
|
||||
|
||||
function toTaskModel(
|
||||
summary: WorkbenchTaskSummary,
|
||||
detail?: WorkbenchTaskDetail,
|
||||
summary: WorkspaceTaskSummary,
|
||||
detail?: WorkspaceTaskDetail,
|
||||
sessionCache?: Map<string, { draft: Task["sessions"][number]["draft"]; transcript: Task["sessions"][number]["transcript"] }>,
|
||||
): Task {
|
||||
const sessions = detail?.sessionsSummary ?? summary.sessionsSummary;
|
||||
|
|
@ -190,7 +190,7 @@ function isOpenPrTaskId(taskId: string): boolean {
|
|||
return taskId.startsWith(OPEN_PR_TASK_PREFIX);
|
||||
}
|
||||
|
||||
function toOpenPrTaskModel(pullRequest: WorkbenchOpenPrSummary): Task {
|
||||
function toOpenPrTaskModel(pullRequest: WorkspaceOpenPrSummary): Task {
|
||||
return {
|
||||
id: openPrTaskId(pullRequest.prId),
|
||||
repoId: pullRequest.repoId,
|
||||
|
|
@ -241,7 +241,7 @@ function groupRepositories(repos: Array<{ id: string; label: string }>, tasks: T
|
|||
.filter((repo) => repo.tasks.length > 0);
|
||||
}
|
||||
|
||||
interface WorkbenchActions {
|
||||
interface WorkspaceActions {
|
||||
createTask(input: {
|
||||
repoId: string;
|
||||
task: string;
|
||||
|
|
@ -252,7 +252,6 @@ interface WorkbenchActions {
|
|||
}): Promise<{ taskId: string; sessionId?: string }>;
|
||||
markTaskUnread(input: { taskId: string }): Promise<void>;
|
||||
renameTask(input: { taskId: string; value: string }): Promise<void>;
|
||||
renameBranch(input: { taskId: string; value: string }): Promise<void>;
|
||||
archiveTask(input: { taskId: string }): Promise<void>;
|
||||
publishPr(input: { taskId: string }): Promise<void>;
|
||||
revertFile(input: { taskId: string; path: string }): Promise<void>;
|
||||
|
|
@ -264,14 +263,14 @@ interface WorkbenchActions {
|
|||
closeSession(input: { taskId: string; sessionId: string }): Promise<void>;
|
||||
addSession(input: { taskId: string; model?: string }): Promise<{ sessionId: string }>;
|
||||
changeModel(input: { taskId: string; sessionId: string; model: ModelId }): Promise<void>;
|
||||
reloadGithubOrganization(): Promise<void>;
|
||||
reloadGithubPullRequests(): Promise<void>;
|
||||
reloadGithubRepository(repoId: string): Promise<void>;
|
||||
reloadGithubPullRequest(repoId: string, prNumber: number): Promise<void>;
|
||||
adminReloadGithubOrganization(): Promise<void>;
|
||||
adminReloadGithubPullRequests(): Promise<void>;
|
||||
adminReloadGithubRepository(repoId: string): Promise<void>;
|
||||
adminReloadGithubPullRequest(repoId: string, prNumber: number): Promise<void>;
|
||||
}
|
||||
|
||||
const TranscriptPanel = memo(function TranscriptPanel({
|
||||
taskWorkbenchClient,
|
||||
taskWorkspaceClient,
|
||||
task,
|
||||
hasSandbox,
|
||||
activeSessionId,
|
||||
|
|
@ -290,7 +289,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
|||
selectedSessionHydrating = false,
|
||||
onNavigateToUsage,
|
||||
}: {
|
||||
taskWorkbenchClient: WorkbenchActions;
|
||||
taskWorkspaceClient: WorkspaceActions;
|
||||
task: Task;
|
||||
hasSandbox: boolean;
|
||||
activeSessionId: string | null;
|
||||
|
|
@ -310,8 +309,11 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
|||
onNavigateToUsage?: () => void;
|
||||
}) {
|
||||
const t = useFoundryTokens();
|
||||
const [defaultModel, setDefaultModel] = useState<ModelId>("claude-sonnet-4");
|
||||
const [editingField, setEditingField] = useState<"title" | "branch" | null>(null);
|
||||
const appSnapshot = useMockAppSnapshot();
|
||||
const appClient = useMockAppClient();
|
||||
const currentUser = activeMockUser(appSnapshot);
|
||||
const defaultModel = currentUser?.defaultModel ?? "claude-sonnet-4";
|
||||
const [editingField, setEditingField] = useState<"title" | null>(null);
|
||||
const [editValue, setEditValue] = useState("");
|
||||
const [editingSessionId, setEditingSessionId] = useState<string | null>(null);
|
||||
const [editingSessionName, setEditingSessionName] = useState("");
|
||||
|
|
@ -436,14 +438,14 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
|||
return;
|
||||
}
|
||||
|
||||
void taskWorkbenchClient.setSessionUnread({
|
||||
void taskWorkspaceClient.setSessionUnread({
|
||||
taskId: task.id,
|
||||
sessionId: activeAgentSession.id,
|
||||
unread: false,
|
||||
});
|
||||
}, [activeAgentSession?.id, activeAgentSession?.unread, task.id]);
|
||||
|
||||
const startEditingField = useCallback((field: "title" | "branch", value: string) => {
|
||||
const startEditingField = useCallback((field: "title", value: string) => {
|
||||
setEditingField(field);
|
||||
setEditValue(value);
|
||||
}, []);
|
||||
|
|
@ -453,18 +455,14 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
|||
}, []);
|
||||
|
||||
const commitEditingField = useCallback(
|
||||
(field: "title" | "branch") => {
|
||||
(field: "title") => {
|
||||
const value = editValue.trim();
|
||||
if (!value) {
|
||||
setEditingField(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (field === "title") {
|
||||
void taskWorkbenchClient.renameTask({ taskId: task.id, value });
|
||||
} else {
|
||||
void taskWorkbenchClient.renameBranch({ taskId: task.id, value });
|
||||
}
|
||||
void taskWorkspaceClient.renameTask({ taskId: task.id, value });
|
||||
setEditingField(null);
|
||||
},
|
||||
[editValue, task.id],
|
||||
|
|
@ -474,7 +472,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
|||
|
||||
const flushDraft = useCallback(
|
||||
(text: string, nextAttachments: LineAttachment[], sessionId: string) => {
|
||||
void taskWorkbenchClient.updateDraft({
|
||||
void taskWorkspaceClient.updateDraft({
|
||||
taskId: task.id,
|
||||
sessionId,
|
||||
text,
|
||||
|
|
@ -535,7 +533,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
|||
|
||||
onSetActiveSessionId(promptSession.id);
|
||||
onSetLastAgentSessionId(promptSession.id);
|
||||
void taskWorkbenchClient.sendMessage({
|
||||
void taskWorkspaceClient.sendMessage({
|
||||
taskId: task.id,
|
||||
sessionId: promptSession.id,
|
||||
text,
|
||||
|
|
@ -548,7 +546,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
|||
return;
|
||||
}
|
||||
|
||||
void taskWorkbenchClient.stopAgent({
|
||||
void taskWorkspaceClient.stopAgent({
|
||||
taskId: task.id,
|
||||
sessionId: promptSession.id,
|
||||
});
|
||||
|
|
@ -562,7 +560,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
|||
onSetLastAgentSessionId(sessionId);
|
||||
const session = task.sessions.find((candidate) => candidate.id === sessionId);
|
||||
if (session?.unread) {
|
||||
void taskWorkbenchClient.setSessionUnread({
|
||||
void taskWorkspaceClient.setSessionUnread({
|
||||
taskId: task.id,
|
||||
sessionId,
|
||||
unread: false,
|
||||
|
|
@ -576,7 +574,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
|||
|
||||
const setSessionUnread = useCallback(
|
||||
(sessionId: string, unread: boolean) => {
|
||||
void taskWorkbenchClient.setSessionUnread({ taskId: task.id, sessionId, unread });
|
||||
void taskWorkspaceClient.setSessionUnread({ taskId: task.id, sessionId, unread });
|
||||
},
|
||||
[task.id],
|
||||
);
|
||||
|
|
@ -610,7 +608,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
|||
return;
|
||||
}
|
||||
|
||||
void taskWorkbenchClient.renameSession({
|
||||
void taskWorkspaceClient.renameSession({
|
||||
taskId: task.id,
|
||||
sessionId: editingSessionId,
|
||||
title: trimmedName,
|
||||
|
|
@ -631,7 +629,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
|||
}
|
||||
|
||||
onSyncRouteSession(task.id, nextSessionId);
|
||||
void taskWorkbenchClient.closeSession({ taskId: task.id, sessionId });
|
||||
void taskWorkspaceClient.closeSession({ taskId: task.id, sessionId });
|
||||
},
|
||||
[activeSessionId, task.id, task.sessions, lastAgentSessionId, onSetActiveSessionId, onSetLastAgentSessionId, onSyncRouteSession],
|
||||
);
|
||||
|
|
@ -651,7 +649,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
|||
|
||||
const addSession = useCallback(() => {
|
||||
void (async () => {
|
||||
const { sessionId } = await taskWorkbenchClient.addSession({ taskId: task.id });
|
||||
const { sessionId } = await taskWorkspaceClient.addSession({ taskId: task.id });
|
||||
onSetLastAgentSessionId(sessionId);
|
||||
onSetActiveSessionId(sessionId);
|
||||
onSyncRouteSession(task.id, sessionId);
|
||||
|
|
@ -664,7 +662,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
|||
throw new Error(`Unable to change model for task ${task.id} without an active prompt session`);
|
||||
}
|
||||
|
||||
void taskWorkbenchClient.changeModel({
|
||||
void taskWorkspaceClient.changeModel({
|
||||
taskId: task.id,
|
||||
sessionId: promptSession.id,
|
||||
model,
|
||||
|
|
@ -939,7 +937,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
|||
messageRefs={messageRefs}
|
||||
historyEvents={historyEvents}
|
||||
onSelectHistoryEvent={jumpToHistoryEvent}
|
||||
targetMessageId={pendingHistoryTarget && activeSessionId === pendingHistoryTarget.sessionId ? pendingHistoryTarget.messageId : null}
|
||||
targetMessageId={pendingHistoryTarget && activeAgentSession?.id === pendingHistoryTarget.sessionId ? pendingHistoryTarget.messageId : null}
|
||||
onTargetMessageResolved={() => setPendingHistoryTarget(null)}
|
||||
copiedMessageId={copiedMessageId}
|
||||
onCopyMessage={(message) => {
|
||||
|
|
@ -966,7 +964,9 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
|||
onStop={stopAgent}
|
||||
onRemoveAttachment={removeAttachment}
|
||||
onChangeModel={changeModel}
|
||||
onSetDefaultModel={setDefaultModel}
|
||||
onSetDefaultModel={(model) => {
|
||||
void appClient.setDefaultModel(model);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
|
@ -1280,27 +1280,26 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
|||
const [css] = useStyletron();
|
||||
const t = useFoundryTokens();
|
||||
const navigate = useNavigate();
|
||||
const taskWorkbenchClient = useMemo<WorkbenchActions>(
|
||||
const taskWorkspaceClient = useMemo<WorkspaceActions>(
|
||||
() => ({
|
||||
createTask: (input) => backendClient.createWorkbenchTask(organizationId, input),
|
||||
markTaskUnread: (input) => backendClient.markWorkbenchUnread(organizationId, input),
|
||||
renameTask: (input) => backendClient.renameWorkbenchTask(organizationId, input),
|
||||
renameBranch: (input) => backendClient.renameWorkbenchBranch(organizationId, input),
|
||||
archiveTask: async (input) => backendClient.runAction(organizationId, input.taskId, "archive"),
|
||||
publishPr: (input) => backendClient.publishWorkbenchPr(organizationId, input),
|
||||
revertFile: (input) => backendClient.revertWorkbenchFile(organizationId, input),
|
||||
updateDraft: (input) => backendClient.updateWorkbenchDraft(organizationId, input),
|
||||
sendMessage: (input) => backendClient.sendWorkbenchMessage(organizationId, input),
|
||||
stopAgent: (input) => backendClient.stopWorkbenchSession(organizationId, input),
|
||||
setSessionUnread: (input) => backendClient.setWorkbenchSessionUnread(organizationId, input),
|
||||
renameSession: (input) => backendClient.renameWorkbenchSession(organizationId, input),
|
||||
closeSession: (input) => backendClient.closeWorkbenchSession(organizationId, input),
|
||||
addSession: (input) => backendClient.createWorkbenchSession(organizationId, input),
|
||||
changeModel: (input) => backendClient.changeWorkbenchModel(organizationId, input),
|
||||
reloadGithubOrganization: () => backendClient.reloadGithubOrganization(organizationId),
|
||||
reloadGithubPullRequests: () => backendClient.reloadGithubPullRequests(organizationId),
|
||||
reloadGithubRepository: (repoId) => backendClient.reloadGithubRepository(organizationId, repoId),
|
||||
reloadGithubPullRequest: (repoId, prNumber) => backendClient.reloadGithubPullRequest(organizationId, repoId, prNumber),
|
||||
createTask: (input) => backendClient.createWorkspaceTask(organizationId, input),
|
||||
markTaskUnread: (input) => backendClient.markWorkspaceUnread(organizationId, input),
|
||||
renameTask: (input) => backendClient.renameWorkspaceTask(organizationId, input),
|
||||
archiveTask: async (input) => backendClient.runAction(organizationId, input.repoId, input.taskId, "archive"),
|
||||
publishPr: (input) => backendClient.publishWorkspacePr(organizationId, input),
|
||||
revertFile: (input) => backendClient.revertWorkspaceFile(organizationId, input),
|
||||
updateDraft: (input) => backendClient.updateWorkspaceDraft(organizationId, input),
|
||||
sendMessage: (input) => backendClient.sendWorkspaceMessage(organizationId, input),
|
||||
stopAgent: (input) => backendClient.stopWorkspaceSession(organizationId, input),
|
||||
setSessionUnread: (input) => backendClient.setWorkspaceSessionUnread(organizationId, input),
|
||||
renameSession: (input) => backendClient.renameWorkspaceSession(organizationId, input),
|
||||
closeSession: (input) => backendClient.closeWorkspaceSession(organizationId, input),
|
||||
addSession: (input) => backendClient.createWorkspaceSession(organizationId, input),
|
||||
changeModel: (input) => backendClient.changeWorkspaceModel(organizationId, input),
|
||||
adminReloadGithubOrganization: () => backendClient.adminReloadGithubOrganization(organizationId),
|
||||
adminReloadGithubPullRequests: () => backendClient.adminReloadGithubPullRequests(organizationId),
|
||||
adminReloadGithubRepository: (repoId) => backendClient.adminReloadGithubRepository(organizationId, repoId),
|
||||
adminReloadGithubPullRequest: (repoId, prNumber) => backendClient.adminReloadGithubPullRequest(organizationId, repoId, prNumber),
|
||||
}),
|
||||
[organizationId],
|
||||
);
|
||||
|
|
@ -1495,7 +1494,7 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
|||
}, [selectedOpenPullRequest, selectedTaskId, tasks]);
|
||||
|
||||
const materializeOpenPullRequest = useCallback(
|
||||
async (pullRequest: WorkbenchOpenPrSummary) => {
|
||||
async (pullRequest: WorkspaceOpenPrSummary) => {
|
||||
if (resolvingOpenPullRequestsRef.current.has(pullRequest.prId)) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -1504,7 +1503,7 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
|||
setMaterializingOpenPrId(pullRequest.prId);
|
||||
|
||||
try {
|
||||
const { taskId, sessionId } = await taskWorkbenchClient.createTask({
|
||||
const { taskId, sessionId } = await taskWorkspaceClient.createTask({
|
||||
repoId: pullRequest.repoId,
|
||||
task: `Continue work on GitHub PR #${pullRequest.number}: ${pullRequest.title}`,
|
||||
model: "gpt-5.3-codex",
|
||||
|
|
@ -1534,7 +1533,7 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
|||
);
|
||||
}
|
||||
},
|
||||
[navigate, taskWorkbenchClient, organizationId],
|
||||
[navigate, taskWorkspaceClient, organizationId],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -1664,7 +1663,7 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
|||
autoCreatingSessionForTaskRef.current.add(activeTask.id);
|
||||
void (async () => {
|
||||
try {
|
||||
const { sessionId } = await taskWorkbenchClient.addSession({ taskId: activeTask.id });
|
||||
const { sessionId } = await taskWorkspaceClient.addSession({ taskId: activeTask.id });
|
||||
syncRouteSession(activeTask.id, sessionId, true);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
|
|
@ -1672,13 +1671,13 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
|||
taskId: activeTask.id,
|
||||
...createErrorContext(error),
|
||||
},
|
||||
"failed_to_auto_create_workbench_session",
|
||||
"failed_to_auto_create_workspace_session",
|
||||
);
|
||||
// Keep the guard in the set on error to prevent retry storms.
|
||||
// The guard is cleared when sessions appear (line above) or the task changes.
|
||||
}
|
||||
})();
|
||||
}, [activeTask, selectedSessionId, syncRouteSession, taskWorkbenchClient]);
|
||||
}, [activeTask, selectedSessionId, syncRouteSession, taskWorkspaceClient]);
|
||||
|
||||
const createTask = useCallback(
|
||||
(overrideRepoId?: string, options?: { title?: string; task?: string; branch?: string; onBranch?: string }) => {
|
||||
|
|
@ -1688,7 +1687,7 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
|||
throw new Error("Cannot create a task without an available repo");
|
||||
}
|
||||
|
||||
const { taskId, sessionId } = await taskWorkbenchClient.createTask({
|
||||
const { taskId, sessionId } = await taskWorkspaceClient.createTask({
|
||||
repoId,
|
||||
task: options?.task ?? "New task",
|
||||
model: "gpt-5.3-codex",
|
||||
|
|
@ -1706,7 +1705,7 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
|||
});
|
||||
})();
|
||||
},
|
||||
[navigate, selectedNewTaskRepoId, taskWorkbenchClient, organizationId],
|
||||
[navigate, selectedNewTaskRepoId, taskWorkspaceClient, organizationId],
|
||||
);
|
||||
|
||||
const openDiffTab = useCallback(
|
||||
|
|
@ -1757,7 +1756,7 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
|||
);
|
||||
|
||||
const markTaskUnread = useCallback((id: string) => {
|
||||
void taskWorkbenchClient.markTaskUnread({ taskId: id });
|
||||
void taskWorkspaceClient.markTaskUnread({ taskId: id });
|
||||
}, []);
|
||||
|
||||
const renameTask = useCallback(
|
||||
|
|
@ -1777,29 +1776,7 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
|||
return;
|
||||
}
|
||||
|
||||
void taskWorkbenchClient.renameTask({ taskId: id, value: trimmedTitle });
|
||||
},
|
||||
[tasks],
|
||||
);
|
||||
|
||||
const renameBranch = useCallback(
|
||||
(id: string) => {
|
||||
const currentTask = tasks.find((task) => task.id === id);
|
||||
if (!currentTask) {
|
||||
throw new Error(`Unable to rename missing task ${id}`);
|
||||
}
|
||||
|
||||
const nextBranch = window.prompt("Rename branch", currentTask.branch ?? "");
|
||||
if (nextBranch === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const trimmedBranch = nextBranch.trim();
|
||||
if (!trimmedBranch) {
|
||||
return;
|
||||
}
|
||||
|
||||
void taskWorkbenchClient.renameBranch({ taskId: id, value: trimmedBranch });
|
||||
void taskWorkspaceClient.renameTask({ taskId: id, value: trimmedTitle });
|
||||
},
|
||||
[tasks],
|
||||
);
|
||||
|
|
@ -1808,14 +1785,14 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
|||
if (!activeTask) {
|
||||
throw new Error("Cannot archive without an active task");
|
||||
}
|
||||
void taskWorkbenchClient.archiveTask({ taskId: activeTask.id });
|
||||
void taskWorkspaceClient.archiveTask({ taskId: activeTask.id });
|
||||
}, [activeTask]);
|
||||
|
||||
const publishPr = useCallback(() => {
|
||||
if (!activeTask) {
|
||||
throw new Error("Cannot publish PR without an active task");
|
||||
}
|
||||
void taskWorkbenchClient.publishPr({ taskId: activeTask.id });
|
||||
void taskWorkspaceClient.publishPr({ taskId: activeTask.id });
|
||||
}, [activeTask]);
|
||||
|
||||
const revertFile = useCallback(
|
||||
|
|
@ -1835,7 +1812,7 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
|||
: (current[activeTask.id] ?? null),
|
||||
}));
|
||||
|
||||
void taskWorkbenchClient.revertFile({
|
||||
void taskWorkspaceClient.revertFile({
|
||||
taskId: activeTask.id,
|
||||
path,
|
||||
});
|
||||
|
|
@ -1939,14 +1916,13 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
|||
onSelectNewTaskRepo={setSelectedNewTaskRepoId}
|
||||
onMarkUnread={markTaskUnread}
|
||||
onRenameTask={renameTask}
|
||||
onRenameBranch={renameBranch}
|
||||
onReorderRepositories={reorderRepositories}
|
||||
taskOrderByRepository={taskOrderByRepository}
|
||||
onReorderTasks={reorderTasks}
|
||||
onReloadOrganization={() => void taskWorkbenchClient.reloadGithubOrganization()}
|
||||
onReloadPullRequests={() => void taskWorkbenchClient.reloadGithubPullRequests()}
|
||||
onReloadRepository={(repoId) => void taskWorkbenchClient.reloadGithubRepository(repoId)}
|
||||
onReloadPullRequest={(repoId, prNumber) => void taskWorkbenchClient.reloadGithubPullRequest(repoId, prNumber)}
|
||||
onReloadOrganization={() => void taskWorkspaceClient.adminReloadGithubOrganization()}
|
||||
onReloadPullRequests={() => void taskWorkspaceClient.adminReloadGithubPullRequests()}
|
||||
onReloadRepository={(repoId) => void taskWorkspaceClient.adminReloadGithubRepository(repoId)}
|
||||
onReloadPullRequest={(repoId, prNumber) => void taskWorkspaceClient.adminReloadGithubPullRequest(repoId, prNumber)}
|
||||
onToggleSidebar={() => setLeftSidebarOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -2079,7 +2055,7 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
|||
{showDevPanel && (
|
||||
<DevPanel
|
||||
organizationId={organizationId}
|
||||
snapshot={{ organizationId, repos: organizationRepos, repositories: rawRepositories, tasks } as TaskWorkbenchSnapshot}
|
||||
snapshot={{ organizationId, repos: organizationRepos, repositories: rawRepositories, tasks } as TaskWorkspaceSnapshot}
|
||||
organization={activeOrg}
|
||||
focusedTask={null}
|
||||
/>
|
||||
|
|
@ -2114,14 +2090,13 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
|||
onSelectNewTaskRepo={setSelectedNewTaskRepoId}
|
||||
onMarkUnread={markTaskUnread}
|
||||
onRenameTask={renameTask}
|
||||
onRenameBranch={renameBranch}
|
||||
onReorderRepositories={reorderRepositories}
|
||||
taskOrderByRepository={taskOrderByRepository}
|
||||
onReorderTasks={reorderTasks}
|
||||
onReloadOrganization={() => void taskWorkbenchClient.reloadGithubOrganization()}
|
||||
onReloadPullRequests={() => void taskWorkbenchClient.reloadGithubPullRequests()}
|
||||
onReloadRepository={(repoId) => void taskWorkbenchClient.reloadGithubRepository(repoId)}
|
||||
onReloadPullRequest={(repoId, prNumber) => void taskWorkbenchClient.reloadGithubPullRequest(repoId, prNumber)}
|
||||
onReloadOrganization={() => void taskWorkspaceClient.adminReloadGithubOrganization()}
|
||||
onReloadPullRequests={() => void taskWorkspaceClient.adminReloadGithubPullRequests()}
|
||||
onReloadRepository={(repoId) => void taskWorkspaceClient.adminReloadGithubRepository(repoId)}
|
||||
onReloadPullRequest={(repoId, prNumber) => void taskWorkspaceClient.adminReloadGithubPullRequest(repoId, prNumber)}
|
||||
onToggleSidebar={() => setLeftSidebarOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -2169,14 +2144,13 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
|||
onSelectNewTaskRepo={setSelectedNewTaskRepoId}
|
||||
onMarkUnread={markTaskUnread}
|
||||
onRenameTask={renameTask}
|
||||
onRenameBranch={renameBranch}
|
||||
onReorderRepositories={reorderRepositories}
|
||||
taskOrderByRepository={taskOrderByRepository}
|
||||
onReorderTasks={reorderTasks}
|
||||
onReloadOrganization={() => void taskWorkbenchClient.reloadGithubOrganization()}
|
||||
onReloadPullRequests={() => void taskWorkbenchClient.reloadGithubPullRequests()}
|
||||
onReloadRepository={(repoId) => void taskWorkbenchClient.reloadGithubRepository(repoId)}
|
||||
onReloadPullRequest={(repoId, prNumber) => void taskWorkbenchClient.reloadGithubPullRequest(repoId, prNumber)}
|
||||
onReloadOrganization={() => void taskWorkspaceClient.adminReloadGithubOrganization()}
|
||||
onReloadPullRequests={() => void taskWorkspaceClient.adminReloadGithubPullRequests()}
|
||||
onReloadRepository={(repoId) => void taskWorkspaceClient.adminReloadGithubRepository(repoId)}
|
||||
onReloadPullRequest={(repoId, prNumber) => void taskWorkspaceClient.adminReloadGithubPullRequest(repoId, prNumber)}
|
||||
onToggleSidebar={() => {
|
||||
setLeftSidebarPeeking(false);
|
||||
setLeftSidebarOpen(true);
|
||||
|
|
@ -2189,7 +2163,7 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
|||
{leftSidebarOpen ? <PanelResizeHandle onResizeStart={onLeftResizeStart} onResize={onLeftResize} /> : null}
|
||||
<div style={{ flex: 1, minWidth: 0, display: "flex", flexDirection: "column" }}>
|
||||
<TranscriptPanel
|
||||
taskWorkbenchClient={taskWorkbenchClient}
|
||||
taskWorkspaceClient={taskWorkspaceClient}
|
||||
task={activeTask}
|
||||
hasSandbox={hasSandbox}
|
||||
activeSessionId={activeSessionId}
|
||||
|
|
@ -2248,7 +2222,7 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
|||
{showDevPanel && (
|
||||
<DevPanel
|
||||
organizationId={organizationId}
|
||||
snapshot={{ organizationId, repos: organizationRepos, repositories: rawRepositories, tasks } as TaskWorkbenchSnapshot}
|
||||
snapshot={{ organizationId, repos: organizationRepos, repositories: rawRepositories, tasks } as TaskWorkspaceSnapshot}
|
||||
organization={activeOrg}
|
||||
focusedTask={{
|
||||
id: activeTask.id,
|
||||
|
|
|
|||
|
|
@ -68,7 +68,6 @@ export const Sidebar = memo(function Sidebar({
|
|||
onSelectNewTaskRepo,
|
||||
onMarkUnread,
|
||||
onRenameTask,
|
||||
onRenameBranch,
|
||||
onReorderRepositories,
|
||||
taskOrderByRepository,
|
||||
onReorderTasks,
|
||||
|
|
@ -87,7 +86,6 @@ export const Sidebar = memo(function Sidebar({
|
|||
onSelectNewTaskRepo: (repoId: string) => void;
|
||||
onMarkUnread: (id: string) => void;
|
||||
onRenameTask: (id: string) => void;
|
||||
onRenameBranch: (id: string) => void;
|
||||
onReorderRepositories: (fromIndex: number, toIndex: number) => void;
|
||||
taskOrderByRepository: Record<string, string[]>;
|
||||
onReorderTasks: (repositoryId: string, fromIndex: number, toIndex: number) => void;
|
||||
|
|
@ -729,7 +727,6 @@ export const Sidebar = memo(function Sidebar({
|
|||
}
|
||||
contextMenu.open(event, [
|
||||
{ label: "Rename task", onClick: () => onRenameTask(task.id) },
|
||||
{ label: "Rename branch", onClick: () => onRenameBranch(task.id) },
|
||||
{ label: "Mark as unread", onClick: () => onMarkUnread(task.id) },
|
||||
]);
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -30,11 +30,11 @@ export const TranscriptHeader = memo(function TranscriptHeader({
|
|||
task: Task;
|
||||
hasSandbox: boolean;
|
||||
activeSession: AgentSession | null | undefined;
|
||||
editingField: "title" | "branch" | null;
|
||||
editingField: "title" | null;
|
||||
editValue: string;
|
||||
onEditValueChange: (value: string) => void;
|
||||
onStartEditingField: (field: "title" | "branch", value: string) => void;
|
||||
onCommitEditingField: (field: "title" | "branch") => void;
|
||||
onStartEditingField: (field: "title", value: string) => void;
|
||||
onCommitEditingField: (field: "title") => void;
|
||||
onCancelEditingField: () => void;
|
||||
onSetActiveSessionUnread: (unread: boolean) => void;
|
||||
sidebarCollapsed?: boolean;
|
||||
|
|
@ -118,55 +118,20 @@ export const TranscriptHeader = memo(function TranscriptHeader({
|
|||
</LabelSmall>
|
||||
)}
|
||||
{task.branch ? (
|
||||
editingField === "branch" ? (
|
||||
<input
|
||||
autoFocus
|
||||
value={editValue}
|
||||
onChange={(event) => onEditValueChange(event.target.value)}
|
||||
onBlur={() => onCommitEditingField("branch")}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
onCommitEditingField("branch");
|
||||
} else if (event.key === "Escape") {
|
||||
onCancelEditingField();
|
||||
}
|
||||
}}
|
||||
className={css({
|
||||
appearance: "none",
|
||||
WebkitAppearance: "none",
|
||||
margin: "0",
|
||||
outline: "none",
|
||||
padding: "2px 8px",
|
||||
borderRadius: "999px",
|
||||
border: `1px solid ${t.borderFocus}`,
|
||||
backgroundColor: t.interactiveSubtle,
|
||||
color: t.textPrimary,
|
||||
fontSize: "11px",
|
||||
whiteSpace: "nowrap",
|
||||
fontFamily: '"IBM Plex Mono", monospace',
|
||||
minWidth: "60px",
|
||||
})}
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
title="Rename"
|
||||
onClick={() => onStartEditingField("branch", task.branch ?? "")}
|
||||
className={css({
|
||||
padding: "2px 8px",
|
||||
borderRadius: "999px",
|
||||
border: `1px solid ${t.borderMedium}`,
|
||||
backgroundColor: t.interactiveSubtle,
|
||||
color: t.textPrimary,
|
||||
fontSize: "11px",
|
||||
whiteSpace: "nowrap",
|
||||
fontFamily: '"IBM Plex Mono", monospace',
|
||||
cursor: "pointer",
|
||||
":hover": { borderColor: t.borderFocus },
|
||||
})}
|
||||
>
|
||||
{task.branch}
|
||||
</span>
|
||||
)
|
||||
<span
|
||||
className={css({
|
||||
padding: "2px 8px",
|
||||
borderRadius: "999px",
|
||||
border: `1px solid ${t.borderMedium}`,
|
||||
backgroundColor: t.interactiveSubtle,
|
||||
color: t.textPrimary,
|
||||
fontSize: "11px",
|
||||
whiteSpace: "nowrap",
|
||||
fontFamily: '"IBM Plex Mono", monospace',
|
||||
})}
|
||||
>
|
||||
{task.branch}
|
||||
</span>
|
||||
) : null}
|
||||
<HeaderStatusPill status={headerStatus} />
|
||||
<div className={css({ flex: 1 })} />
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import type { WorkbenchSession } from "@sandbox-agent/foundry-shared";
|
||||
import type { WorkspaceSession } from "@sandbox-agent/foundry-shared";
|
||||
import { buildDisplayMessages } from "./view-model";
|
||||
|
||||
function makeSession(transcript: WorkbenchSession["transcript"]): WorkbenchSession {
|
||||
function makeSession(transcript: WorkspaceSession["transcript"]): WorkspaceSession {
|
||||
return {
|
||||
id: "session-1",
|
||||
sessionId: "session-1",
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
import type {
|
||||
WorkbenchAgentKind as AgentKind,
|
||||
WorkbenchSession as AgentSession,
|
||||
WorkbenchDiffLineKind as DiffLineKind,
|
||||
WorkbenchFileChange as FileChange,
|
||||
WorkbenchFileTreeNode as FileTreeNode,
|
||||
WorkbenchTask as Task,
|
||||
WorkbenchHistoryEvent as HistoryEvent,
|
||||
WorkbenchLineAttachment as LineAttachment,
|
||||
WorkbenchModelGroup as ModelGroup,
|
||||
WorkbenchModelId as ModelId,
|
||||
WorkbenchParsedDiffLine as ParsedDiffLine,
|
||||
WorkbenchRepositorySection as RepositorySection,
|
||||
WorkbenchTranscriptEvent as TranscriptEvent,
|
||||
WorkspaceAgentKind as AgentKind,
|
||||
WorkspaceSession as AgentSession,
|
||||
WorkspaceDiffLineKind as DiffLineKind,
|
||||
WorkspaceFileChange as FileChange,
|
||||
WorkspaceFileTreeNode as FileTreeNode,
|
||||
WorkspaceTask as Task,
|
||||
WorkspaceHistoryEvent as HistoryEvent,
|
||||
WorkspaceLineAttachment as LineAttachment,
|
||||
WorkspaceModelGroup as ModelGroup,
|
||||
WorkspaceModelId as ModelId,
|
||||
WorkspaceParsedDiffLine as ParsedDiffLine,
|
||||
WorkspaceRepositorySection as RepositorySection,
|
||||
WorkspaceTranscriptEvent as TranscriptEvent,
|
||||
} from "@sandbox-agent/foundry-shared";
|
||||
import { extractEventText } from "../../features/sessions/model";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
||||
import type { AgentType, RepoBranchRecord, RepoOverview, TaskWorkbenchSnapshot, WorkbenchTaskStatus } from "@sandbox-agent/foundry-shared";
|
||||
import type { AgentType, RepoBranchRecord, RepoOverview, TaskWorkspaceSnapshot, WorkspaceTaskStatus } from "@sandbox-agent/foundry-shared";
|
||||
import { currentFoundryOrganization, useSubscription } from "@sandbox-agent/foundry-client";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { Link, useNavigate } from "@tanstack/react-router";
|
||||
|
|
@ -100,7 +100,7 @@ const AGENT_OPTIONS: SelectItem[] = [
|
|||
{ id: "claude", label: "claude" },
|
||||
];
|
||||
|
||||
function statusKind(status: WorkbenchTaskStatus): StatusTagKind {
|
||||
function statusKind(status: WorkspaceTaskStatus): StatusTagKind {
|
||||
if (status === "running") return "positive";
|
||||
if (status === "error") return "negative";
|
||||
if (status === "new" || String(status).startsWith("init_")) return "warning";
|
||||
|
|
@ -515,7 +515,7 @@ export function OrganizationDashboard({ organizationId, selectedTaskId, selected
|
|||
};
|
||||
}, [repoOverviewMode, selectedForSession, selectedSummary]);
|
||||
const devPanelSnapshot = useMemo(
|
||||
(): TaskWorkbenchSnapshot => ({
|
||||
(): TaskWorkspaceSnapshot => ({
|
||||
organizationId,
|
||||
repos: repos.map((repo) => ({ id: repo.id, label: repo.label })),
|
||||
repositories: [],
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { TaskStatus, WorkbenchSessionStatus } from "@sandbox-agent/foundry-shared";
|
||||
import type { TaskStatus, WorkspaceSessionStatus } from "@sandbox-agent/foundry-shared";
|
||||
import type { HeaderStatusInfo } from "../../components/mock-layout/ui";
|
||||
|
||||
export type TaskDisplayStatus = TaskStatus | "new";
|
||||
|
|
@ -73,7 +73,7 @@ export function describeTaskState(status: TaskDisplayStatus | null | undefined,
|
|||
export function deriveHeaderStatus(
|
||||
taskStatus: TaskDisplayStatus | null | undefined,
|
||||
taskStatusMessage: string | null | undefined,
|
||||
sessionStatus: WorkbenchSessionStatus | null | undefined,
|
||||
sessionStatus: WorkspaceSessionStatus | null | undefined,
|
||||
sessionErrorMessage: string | null | undefined,
|
||||
hasSandbox?: boolean,
|
||||
): HeaderStatusInfo {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,13 @@ import {
|
|||
eligibleFoundryOrganizations,
|
||||
type FoundryAppClient,
|
||||
} from "@sandbox-agent/foundry-client";
|
||||
import type { FoundryAppSnapshot, FoundryBillingPlanId, FoundryOrganization, UpdateFoundryOrganizationProfileInput } from "@sandbox-agent/foundry-shared";
|
||||
import type {
|
||||
FoundryAppSnapshot,
|
||||
FoundryBillingPlanId,
|
||||
FoundryOrganization,
|
||||
UpdateFoundryOrganizationProfileInput,
|
||||
WorkspaceModelId,
|
||||
} from "@sandbox-agent/foundry-shared";
|
||||
import { backendClient } from "./backend";
|
||||
import { subscriptionManager } from "./subscription";
|
||||
import { frontendClientMode } from "./env";
|
||||
|
|
@ -58,6 +64,9 @@ const remoteAppClient: FoundryAppClient = {
|
|||
async selectOrganization(organizationId: string): Promise<void> {
|
||||
await backendClient.selectAppOrganization(organizationId);
|
||||
},
|
||||
async setDefaultModel(defaultModel: WorkspaceModelId): Promise<void> {
|
||||
await backendClient.setAppDefaultModel(defaultModel);
|
||||
},
|
||||
async updateOrganizationProfile(input: UpdateFoundryOrganizationProfileInput): Promise<void> {
|
||||
await backendClient.updateAppOrganizationProfile(input);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -43,15 +43,27 @@ declare module "@sandbox-agent/react" {
|
|||
className?: string;
|
||||
classNames?: Partial<AgentTranscriptClassNames>;
|
||||
endRef?: RefObject<HTMLDivElement>;
|
||||
scrollRef?: RefObject<HTMLDivElement>;
|
||||
scrollToEntryId?: string | null;
|
||||
sessionError?: string | null;
|
||||
eventError?: string | null;
|
||||
isThinking?: boolean;
|
||||
agentId?: string;
|
||||
virtualize?: boolean;
|
||||
onAtBottomChange?: (atBottom: boolean) => void;
|
||||
onEventClick?: (eventId: string) => void;
|
||||
onPermissionReply?: (permissionId: string, reply: PermissionReply) => void;
|
||||
isDividerEntry?: (entry: TranscriptEntry) => boolean;
|
||||
canOpenEvent?: (entry: TranscriptEntry) => boolean;
|
||||
getToolGroupSummary?: (entries: TranscriptEntry[]) => string;
|
||||
renderMessageText?: (entry: TranscriptEntry) => ReactNode;
|
||||
renderInlinePendingIndicator?: () => ReactNode;
|
||||
renderThinkingState?: (context: { agentId?: string }) => ReactNode;
|
||||
renderToolItemIcon?: (entry: TranscriptEntry) => ReactNode;
|
||||
renderToolGroupIcon?: (entries: TranscriptEntry[], expanded: boolean) => ReactNode;
|
||||
renderChevron?: (expanded: boolean) => ReactNode;
|
||||
renderEventLinkContent?: (entry: TranscriptEntry) => ReactNode;
|
||||
renderPermissionIcon?: (entry: TranscriptEntry) => ReactNode;
|
||||
renderPermissionOptionContent?: (context: PermissionOptionRenderContext) => ReactNode;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { WorkbenchModelId } from "./workbench.js";
|
||||
import type { WorkspaceModelId } from "./workspace.js";
|
||||
|
||||
export type FoundryBillingPlanId = "free" | "team";
|
||||
export type FoundryBillingStatus = "active" | "trialing" | "past_due" | "scheduled_cancel";
|
||||
|
|
@ -14,6 +14,7 @@ export interface FoundryUser {
|
|||
githubLogin: string;
|
||||
roleLabel: string;
|
||||
eligibleOrganizationIds: string[];
|
||||
defaultModel: WorkspaceModelId;
|
||||
}
|
||||
|
||||
export interface FoundryOrganizationMember {
|
||||
|
|
@ -59,7 +60,6 @@ export interface FoundryOrganizationSettings {
|
|||
slug: string;
|
||||
primaryDomain: string;
|
||||
seatAccrualMode: "first_prompt";
|
||||
defaultModel: WorkbenchModelId;
|
||||
autoImportRepos: boolean;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -54,7 +54,6 @@ export const CreateTaskInputSchema = z.object({
|
|||
explicitTitle: z.string().trim().min(1).optional(),
|
||||
explicitBranchName: z.string().trim().min(1).optional(),
|
||||
sandboxProviderId: SandboxProviderIdSchema.optional(),
|
||||
agentType: AgentTypeSchema.optional(),
|
||||
onBranch: z.string().trim().min(1).optional(),
|
||||
});
|
||||
export type CreateTaskInput = z.infer<typeof CreateTaskInputSchema>;
|
||||
|
|
@ -69,9 +68,7 @@ export const TaskRecordSchema = z.object({
|
|||
task: z.string().min(1),
|
||||
sandboxProviderId: SandboxProviderIdSchema,
|
||||
status: TaskStatusSchema,
|
||||
statusMessage: z.string().nullable(),
|
||||
activeSandboxId: z.string().nullable(),
|
||||
activeSessionId: z.string().nullable(),
|
||||
sandboxes: z.array(
|
||||
z.object({
|
||||
sandboxId: z.string().min(1),
|
||||
|
|
@ -83,17 +80,12 @@ export const TaskRecordSchema = z.object({
|
|||
updatedAt: z.number().int(),
|
||||
}),
|
||||
),
|
||||
agentType: z.string().nullable(),
|
||||
prSubmitted: z.boolean(),
|
||||
diffStat: z.string().nullable(),
|
||||
prUrl: z.string().nullable(),
|
||||
prAuthor: z.string().nullable(),
|
||||
ciStatus: z.string().nullable(),
|
||||
reviewStatus: z.string().nullable(),
|
||||
reviewer: z.string().nullable(),
|
||||
conflictsWithMain: z.string().nullable(),
|
||||
hasUnpushed: z.string().nullable(),
|
||||
parentBranch: z.string().nullable(),
|
||||
createdAt: z.number().int(),
|
||||
updatedAt: z.number().int(),
|
||||
});
|
||||
|
|
@ -112,6 +104,7 @@ export type TaskSummary = z.infer<typeof TaskSummarySchema>;
|
|||
|
||||
export const TaskActionInputSchema = z.object({
|
||||
organizationId: OrganizationIdSchema,
|
||||
repoId: RepoIdSchema,
|
||||
taskId: z.string().min(1),
|
||||
});
|
||||
export type TaskActionInput = z.infer<typeof TaskActionInputSchema>;
|
||||
|
|
@ -180,7 +173,7 @@ export const HistoryQueryInputSchema = z.object({
|
|||
});
|
||||
export type HistoryQueryInput = z.infer<typeof HistoryQueryInputSchema>;
|
||||
|
||||
export const HistoryEventSchema = z.object({
|
||||
export const AuditLogEventSchema = z.object({
|
||||
id: z.number().int(),
|
||||
organizationId: OrganizationIdSchema,
|
||||
repoId: z.string().nullable(),
|
||||
|
|
@ -190,7 +183,7 @@ export const HistoryEventSchema = z.object({
|
|||
payloadJson: z.string().min(1),
|
||||
createdAt: z.number().int(),
|
||||
});
|
||||
export type HistoryEvent = z.infer<typeof HistoryEventSchema>;
|
||||
export type AuditLogEvent = z.infer<typeof AuditLogEventSchema>;
|
||||
|
||||
export const PruneInputSchema = z.object({
|
||||
organizationId: OrganizationIdSchema,
|
||||
|
|
@ -201,6 +194,7 @@ export type PruneInput = z.infer<typeof PruneInputSchema>;
|
|||
|
||||
export const KillInputSchema = z.object({
|
||||
organizationId: OrganizationIdSchema,
|
||||
repoId: RepoIdSchema,
|
||||
taskId: z.string().min(1),
|
||||
deleteBranch: z.boolean(),
|
||||
abandon: z.boolean(),
|
||||
|
|
|
|||
|
|
@ -3,5 +3,5 @@ export * from "./contracts.js";
|
|||
export * from "./config.js";
|
||||
export * from "./logging.js";
|
||||
export * from "./realtime-events.js";
|
||||
export * from "./workbench.js";
|
||||
export * from "./workspace.js";
|
||||
export * from "./organization.js";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { FoundryAppSnapshot } from "./app-shell.js";
|
||||
import type { WorkbenchOpenPrSummary, WorkbenchRepositorySummary, WorkbenchSessionDetail, WorkbenchTaskDetail, WorkbenchTaskSummary } from "./workbench.js";
|
||||
import type { OrganizationSummarySnapshot, WorkspaceSessionDetail, WorkspaceTaskDetail } from "./workspace.js";
|
||||
|
||||
export interface SandboxProcessSnapshot {
|
||||
id: string;
|
||||
|
|
@ -16,20 +16,13 @@ export interface SandboxProcessSnapshot {
|
|||
}
|
||||
|
||||
/** Organization-level events broadcast by the organization actor. */
|
||||
export type OrganizationEvent =
|
||||
| { type: "taskSummaryUpdated"; taskSummary: WorkbenchTaskSummary }
|
||||
| { type: "taskRemoved"; taskId: string }
|
||||
| { type: "repoAdded"; repo: WorkbenchRepositorySummary }
|
||||
| { type: "repoUpdated"; repo: WorkbenchRepositorySummary }
|
||||
| { type: "repoRemoved"; repoId: string }
|
||||
| { type: "pullRequestUpdated"; pullRequest: WorkbenchOpenPrSummary }
|
||||
| { type: "pullRequestRemoved"; prId: string };
|
||||
export type OrganizationEvent = { type: "organizationUpdated"; snapshot: OrganizationSummarySnapshot };
|
||||
|
||||
/** Task-level events broadcast by the task actor. */
|
||||
export type TaskEvent = { type: "taskDetailUpdated"; detail: WorkbenchTaskDetail };
|
||||
export type TaskEvent = { type: "taskUpdated"; detail: WorkspaceTaskDetail };
|
||||
|
||||
/** Session-level events broadcast by the task actor and filtered by sessionId on the client. */
|
||||
export type SessionEvent = { type: "sessionUpdated"; session: WorkbenchSessionDetail };
|
||||
export type SessionEvent = { type: "sessionUpdated"; session: WorkspaceSessionDetail };
|
||||
|
||||
/** App-level events broadcast by the app organization actor. */
|
||||
export type AppEvent = { type: "appUpdated"; snapshot: FoundryAppSnapshot };
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import type { AgentType, SandboxProviderId, TaskStatus } from "./contracts.js";
|
||||
import type { SandboxProviderId, TaskStatus } from "./contracts.js";
|
||||
|
||||
export type WorkbenchTaskStatus = TaskStatus | "new";
|
||||
export type WorkbenchAgentKind = "Claude" | "Codex" | "Cursor";
|
||||
export type WorkbenchModelId =
|
||||
export type WorkspaceTaskStatus = TaskStatus | "new";
|
||||
export type WorkspaceAgentKind = "Claude" | "Codex" | "Cursor";
|
||||
export type WorkspaceModelId =
|
||||
| "claude-sonnet-4"
|
||||
| "claude-opus-4"
|
||||
| "gpt-5.3-codex"
|
||||
|
|
@ -11,9 +11,9 @@ export type WorkbenchModelId =
|
|||
| "gpt-5.1-codex-max"
|
||||
| "gpt-5.2"
|
||||
| "gpt-5.1-codex-mini";
|
||||
export type WorkbenchSessionStatus = "pending_provision" | "pending_session_create" | "ready" | "running" | "idle" | "error";
|
||||
export type WorkspaceSessionStatus = "pending_provision" | "pending_session_create" | "ready" | "running" | "idle" | "error";
|
||||
|
||||
export interface WorkbenchTranscriptEvent {
|
||||
export interface WorkspaceTranscriptEvent {
|
||||
id: string;
|
||||
eventIndex: number;
|
||||
sessionId: string;
|
||||
|
|
@ -23,23 +23,23 @@ export interface WorkbenchTranscriptEvent {
|
|||
payload: unknown;
|
||||
}
|
||||
|
||||
export interface WorkbenchComposerDraft {
|
||||
export interface WorkspaceComposerDraft {
|
||||
text: string;
|
||||
attachments: WorkbenchLineAttachment[];
|
||||
attachments: WorkspaceLineAttachment[];
|
||||
updatedAtMs: number | null;
|
||||
}
|
||||
|
||||
/** Session metadata without transcript content. */
|
||||
export interface WorkbenchSessionSummary {
|
||||
export interface WorkspaceSessionSummary {
|
||||
id: string;
|
||||
/** Stable UI session id used for routing and task-local identity. */
|
||||
sessionId: string;
|
||||
/** Underlying sandbox session id when provisioning has completed. */
|
||||
sandboxSessionId?: string | null;
|
||||
sessionName: string;
|
||||
agent: WorkbenchAgentKind;
|
||||
model: WorkbenchModelId;
|
||||
status: WorkbenchSessionStatus;
|
||||
agent: WorkspaceAgentKind;
|
||||
model: WorkspaceModelId;
|
||||
status: WorkspaceSessionStatus;
|
||||
thinkingSinceMs: number | null;
|
||||
unread: boolean;
|
||||
created: boolean;
|
||||
|
|
@ -47,44 +47,44 @@ export interface WorkbenchSessionSummary {
|
|||
}
|
||||
|
||||
/** Full session content — only fetched when viewing a specific session. */
|
||||
export interface WorkbenchSessionDetail {
|
||||
export interface WorkspaceSessionDetail {
|
||||
/** Stable UI session id used for the session topic key and routing. */
|
||||
sessionId: string;
|
||||
sandboxSessionId: string | null;
|
||||
sessionName: string;
|
||||
agent: WorkbenchAgentKind;
|
||||
model: WorkbenchModelId;
|
||||
status: WorkbenchSessionStatus;
|
||||
agent: WorkspaceAgentKind;
|
||||
model: WorkspaceModelId;
|
||||
status: WorkspaceSessionStatus;
|
||||
thinkingSinceMs: number | null;
|
||||
unread: boolean;
|
||||
created: boolean;
|
||||
errorMessage?: string | null;
|
||||
draft: WorkbenchComposerDraft;
|
||||
transcript: WorkbenchTranscriptEvent[];
|
||||
draft: WorkspaceComposerDraft;
|
||||
transcript: WorkspaceTranscriptEvent[];
|
||||
}
|
||||
|
||||
export interface WorkbenchFileChange {
|
||||
export interface WorkspaceFileChange {
|
||||
path: string;
|
||||
added: number;
|
||||
removed: number;
|
||||
type: "M" | "A" | "D";
|
||||
}
|
||||
|
||||
export interface WorkbenchFileTreeNode {
|
||||
export interface WorkspaceFileTreeNode {
|
||||
name: string;
|
||||
path: string;
|
||||
isDir: boolean;
|
||||
children?: WorkbenchFileTreeNode[];
|
||||
children?: WorkspaceFileTreeNode[];
|
||||
}
|
||||
|
||||
export interface WorkbenchLineAttachment {
|
||||
export interface WorkspaceLineAttachment {
|
||||
id: string;
|
||||
filePath: string;
|
||||
lineNumber: number;
|
||||
lineContent: string;
|
||||
}
|
||||
|
||||
export interface WorkbenchHistoryEvent {
|
||||
export interface WorkspaceHistoryEvent {
|
||||
id: string;
|
||||
messageId: string;
|
||||
preview: string;
|
||||
|
|
@ -94,78 +94,67 @@ export interface WorkbenchHistoryEvent {
|
|||
detail: string;
|
||||
}
|
||||
|
||||
export type WorkbenchDiffLineKind = "context" | "add" | "remove" | "hunk";
|
||||
export type WorkspaceDiffLineKind = "context" | "add" | "remove" | "hunk";
|
||||
|
||||
export interface WorkbenchParsedDiffLine {
|
||||
kind: WorkbenchDiffLineKind;
|
||||
export interface WorkspaceParsedDiffLine {
|
||||
kind: WorkspaceDiffLineKind;
|
||||
lineNumber: number;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface WorkbenchPullRequestSummary {
|
||||
number: number;
|
||||
status: "draft" | "ready";
|
||||
}
|
||||
|
||||
export interface WorkbenchOpenPrSummary {
|
||||
prId: string;
|
||||
repoId: string;
|
||||
repoFullName: string;
|
||||
export interface WorkspacePullRequestSummary {
|
||||
number: number;
|
||||
title: string;
|
||||
state: string;
|
||||
url: string;
|
||||
headRefName: string;
|
||||
baseRefName: string;
|
||||
repoFullName: string;
|
||||
authorLogin: string | null;
|
||||
isDraft: boolean;
|
||||
updatedAtMs: number;
|
||||
}
|
||||
|
||||
export interface WorkbenchSandboxSummary {
|
||||
export interface WorkspaceSandboxSummary {
|
||||
sandboxProviderId: SandboxProviderId;
|
||||
sandboxId: string;
|
||||
cwd: string | null;
|
||||
}
|
||||
|
||||
/** Sidebar-level task data. Materialized in the organization actor's SQLite. */
|
||||
export interface WorkbenchTaskSummary {
|
||||
export interface WorkspaceTaskSummary {
|
||||
id: string;
|
||||
repoId: string;
|
||||
title: string;
|
||||
status: WorkbenchTaskStatus;
|
||||
status: WorkspaceTaskStatus;
|
||||
repoName: string;
|
||||
updatedAtMs: number;
|
||||
branch: string | null;
|
||||
pullRequest: WorkbenchPullRequestSummary | null;
|
||||
pullRequest: WorkspacePullRequestSummary | null;
|
||||
/** Summary of sessions — no transcript content. */
|
||||
sessionsSummary: WorkbenchSessionSummary[];
|
||||
sessionsSummary: WorkspaceSessionSummary[];
|
||||
}
|
||||
|
||||
/** Full task detail — only fetched when viewing a specific task. */
|
||||
export interface WorkbenchTaskDetail extends WorkbenchTaskSummary {
|
||||
export interface WorkspaceTaskDetail extends WorkspaceTaskSummary {
|
||||
/** Original task prompt/instructions shown in the detail view. */
|
||||
task: string;
|
||||
/** Agent choice used when creating new sandbox sessions for this task. */
|
||||
agentType: AgentType | null;
|
||||
/** Underlying task runtime status preserved for detail views and error handling. */
|
||||
runtimeStatus: TaskStatus;
|
||||
statusMessage: string | null;
|
||||
activeSessionId: string | null;
|
||||
diffStat: string | null;
|
||||
prUrl: string | null;
|
||||
reviewStatus: string | null;
|
||||
fileChanges: WorkbenchFileChange[];
|
||||
fileChanges: WorkspaceFileChange[];
|
||||
diffs: Record<string, string>;
|
||||
fileTree: WorkbenchFileTreeNode[];
|
||||
fileTree: WorkspaceFileTreeNode[];
|
||||
minutesUsed: number;
|
||||
/** Sandbox info for this task. */
|
||||
sandboxes: WorkbenchSandboxSummary[];
|
||||
sandboxes: WorkspaceSandboxSummary[];
|
||||
activeSandboxId: string | null;
|
||||
}
|
||||
|
||||
/** Repo-level summary for organization sidebar. */
|
||||
export interface WorkbenchRepositorySummary {
|
||||
export interface WorkspaceRepositorySummary {
|
||||
id: string;
|
||||
label: string;
|
||||
/** Aggregated branch/task overview state (replaces getRepoOverview polling). */
|
||||
|
|
@ -176,121 +165,126 @@ export interface WorkbenchRepositorySummary {
|
|||
/** Organization-level snapshot — initial fetch for the organization topic. */
|
||||
export interface OrganizationSummarySnapshot {
|
||||
organizationId: string;
|
||||
repos: WorkbenchRepositorySummary[];
|
||||
taskSummaries: WorkbenchTaskSummary[];
|
||||
openPullRequests: WorkbenchOpenPrSummary[];
|
||||
repos: WorkspaceRepositorySummary[];
|
||||
taskSummaries: WorkspaceTaskSummary[];
|
||||
}
|
||||
|
||||
export interface WorkbenchSession extends WorkbenchSessionSummary {
|
||||
draft: WorkbenchComposerDraft;
|
||||
transcript: WorkbenchTranscriptEvent[];
|
||||
export interface WorkspaceSession extends WorkspaceSessionSummary {
|
||||
draft: WorkspaceComposerDraft;
|
||||
transcript: WorkspaceTranscriptEvent[];
|
||||
}
|
||||
|
||||
export interface WorkbenchTask {
|
||||
export interface WorkspaceTask {
|
||||
id: string;
|
||||
repoId: string;
|
||||
title: string;
|
||||
status: WorkbenchTaskStatus;
|
||||
status: WorkspaceTaskStatus;
|
||||
runtimeStatus?: TaskStatus;
|
||||
statusMessage?: string | null;
|
||||
repoName: string;
|
||||
updatedAtMs: number;
|
||||
branch: string | null;
|
||||
pullRequest: WorkbenchPullRequestSummary | null;
|
||||
sessions: WorkbenchSession[];
|
||||
fileChanges: WorkbenchFileChange[];
|
||||
pullRequest: WorkspacePullRequestSummary | null;
|
||||
sessions: WorkspaceSession[];
|
||||
fileChanges: WorkspaceFileChange[];
|
||||
diffs: Record<string, string>;
|
||||
fileTree: WorkbenchFileTreeNode[];
|
||||
fileTree: WorkspaceFileTreeNode[];
|
||||
minutesUsed: number;
|
||||
activeSandboxId?: string | null;
|
||||
}
|
||||
|
||||
export interface WorkbenchRepo {
|
||||
export interface WorkspaceRepo {
|
||||
id: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface WorkbenchRepositorySection {
|
||||
export interface WorkspaceRepositorySection {
|
||||
id: string;
|
||||
label: string;
|
||||
updatedAtMs: number;
|
||||
tasks: WorkbenchTask[];
|
||||
tasks: WorkspaceTask[];
|
||||
}
|
||||
|
||||
export interface TaskWorkbenchSnapshot {
|
||||
export interface TaskWorkspaceSnapshot {
|
||||
organizationId: string;
|
||||
repos: WorkbenchRepo[];
|
||||
repositories: WorkbenchRepositorySection[];
|
||||
tasks: WorkbenchTask[];
|
||||
repos: WorkspaceRepo[];
|
||||
repositories: WorkspaceRepositorySection[];
|
||||
tasks: WorkspaceTask[];
|
||||
}
|
||||
|
||||
export interface WorkbenchModelOption {
|
||||
id: WorkbenchModelId;
|
||||
export interface WorkspaceModelOption {
|
||||
id: WorkspaceModelId;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface WorkbenchModelGroup {
|
||||
export interface WorkspaceModelGroup {
|
||||
provider: string;
|
||||
models: WorkbenchModelOption[];
|
||||
models: WorkspaceModelOption[];
|
||||
}
|
||||
|
||||
export interface TaskWorkbenchSelectInput {
|
||||
export interface TaskWorkspaceSelectInput {
|
||||
repoId: string;
|
||||
taskId: string;
|
||||
authSessionId?: string;
|
||||
}
|
||||
|
||||
export interface TaskWorkbenchCreateTaskInput {
|
||||
export interface TaskWorkspaceCreateTaskInput {
|
||||
repoId: string;
|
||||
task: string;
|
||||
title?: string;
|
||||
branch?: string;
|
||||
onBranch?: string;
|
||||
model?: WorkbenchModelId;
|
||||
model?: WorkspaceModelId;
|
||||
authSessionId?: string;
|
||||
}
|
||||
|
||||
export interface TaskWorkbenchRenameInput {
|
||||
export interface TaskWorkspaceRenameInput {
|
||||
repoId: string;
|
||||
taskId: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface TaskWorkbenchSendMessageInput {
|
||||
export interface TaskWorkspaceSendMessageInput {
|
||||
taskId: string;
|
||||
sessionId: string;
|
||||
text: string;
|
||||
attachments: WorkbenchLineAttachment[];
|
||||
attachments: WorkspaceLineAttachment[];
|
||||
authSessionId?: string;
|
||||
}
|
||||
|
||||
export interface TaskWorkbenchSessionInput {
|
||||
export interface TaskWorkspaceSessionInput {
|
||||
taskId: string;
|
||||
sessionId: string;
|
||||
authSessionId?: string;
|
||||
}
|
||||
|
||||
export interface TaskWorkbenchRenameSessionInput extends TaskWorkbenchSessionInput {
|
||||
export interface TaskWorkspaceRenameSessionInput extends TaskWorkspaceSessionInput {
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface TaskWorkbenchChangeModelInput extends TaskWorkbenchSessionInput {
|
||||
model: WorkbenchModelId;
|
||||
export interface TaskWorkspaceChangeModelInput extends TaskWorkspaceSessionInput {
|
||||
model: WorkspaceModelId;
|
||||
}
|
||||
|
||||
export interface TaskWorkbenchUpdateDraftInput extends TaskWorkbenchSessionInput {
|
||||
export interface TaskWorkspaceUpdateDraftInput extends TaskWorkspaceSessionInput {
|
||||
text: string;
|
||||
attachments: WorkbenchLineAttachment[];
|
||||
attachments: WorkspaceLineAttachment[];
|
||||
}
|
||||
|
||||
export interface TaskWorkbenchSetSessionUnreadInput extends TaskWorkbenchSessionInput {
|
||||
export interface TaskWorkspaceSetSessionUnreadInput extends TaskWorkspaceSessionInput {
|
||||
unread: boolean;
|
||||
}
|
||||
|
||||
export interface TaskWorkbenchDiffInput {
|
||||
export interface TaskWorkspaceDiffInput {
|
||||
repoId: string;
|
||||
taskId: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface TaskWorkbenchCreateTaskResponse {
|
||||
export interface TaskWorkspaceCreateTaskResponse {
|
||||
taskId: string;
|
||||
sessionId?: string;
|
||||
}
|
||||
|
||||
export interface TaskWorkbenchAddSessionResponse {
|
||||
export interface TaskWorkspaceAddSessionResponse {
|
||||
sessionId: string;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue