mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-20 12:04:43 +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`.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
- 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:
|
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.
|
- `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.
|
- `sendWorkspaceMessage` → 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.
|
- `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.
|
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
|
- Friction/issue
|
||||||
- Attempted fix/workaround and outcome
|
- 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
|
- create
|
||||||
- attach
|
- attach
|
||||||
|
|
@ -331,6 +331,8 @@ Log notable workflow changes to `events` so `hf history` remains complete:
|
||||||
- status transitions
|
- status transitions
|
||||||
- PR state transitions
|
- PR state transitions
|
||||||
|
|
||||||
|
When adding new task/workspace commands, always add a corresponding audit log event.
|
||||||
|
|
||||||
## Validation After Changes
|
## Validation After Changes
|
||||||
|
|
||||||
Always run and fix failures:
|
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
|
```text
|
||||||
OrganizationActor
|
OrganizationActor
|
||||||
├─ HistoryActor(organization-scoped global feed)
|
├─ AuditLogActor(organization-scoped global feed)
|
||||||
├─ GithubDataActor
|
├─ GithubDataActor
|
||||||
├─ RepositoryActor(repo)
|
├─ RepositoryActor(repo)
|
||||||
│ └─ TaskActor(task)
|
│ └─ TaskActor(task)
|
||||||
│ ├─ TaskSessionActor(session) × N
|
│ ├─ TaskSessionActor(session) × N
|
||||||
│ │ └─ SessionStatusSyncActor(session) × 0..1
|
│ │ └─ SessionStatusSyncActor(session) × 0..1
|
||||||
│ └─ Task-local workbench state
|
│ └─ Task-local workspace state
|
||||||
└─ SandboxInstanceActor(sandboxProviderId, sandboxId) × N
|
└─ SandboxInstanceActor(sandboxProviderId, sandboxId) × N
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -46,12 +46,12 @@ OrganizationActor (coordinator for repos + auth users)
|
||||||
│ └─ TaskActor (coordinator for sessions + sandboxes)
|
│ └─ TaskActor (coordinator for sessions + sandboxes)
|
||||||
│ │
|
│ │
|
||||||
│ │ Index tables:
|
│ │ Index tables:
|
||||||
│ │ ├─ taskWorkbenchSessions → Session index (session metadata, transcript, draft)
|
│ │ ├─ taskWorkspaceSessions → Session index (session metadata, transcript, draft)
|
||||||
│ │ └─ taskSandboxes → SandboxInstanceActor index (sandbox history)
|
│ │ └─ taskSandboxes → SandboxInstanceActor index (sandbox history)
|
||||||
│ │
|
│ │
|
||||||
│ └─ SandboxInstanceActor (leaf)
|
│ └─ 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)
|
└─ 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
|
## Ownership Rules
|
||||||
|
|
||||||
- `OrganizationActor` is the organization coordinator and lookup/index owner.
|
- `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.
|
- `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` is one branch. Treat `1 task = 1 branch` once branch assignment is finalized.
|
||||||
- `TaskActor` can have many sessions.
|
- `TaskActor` can have many sessions.
|
||||||
- `TaskActor` can reference many sandbox instances historically, but should have only one active sandbox/session at a time.
|
- `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.
|
- Session unread state and draft prompts are backend-owned workspace state, not frontend-local state.
|
||||||
- Branch rename is a real git operation, not just metadata.
|
- Branch names are immutable after task creation. Do not implement branch-rename flows.
|
||||||
- `SandboxInstanceActor` stays separate from `TaskActor`; tasks/sessions reference it by identity.
|
- `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()`.
|
- 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.
|
- 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.
|
- 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.
|
- 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
|
## 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.
|
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.
|
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
|
## Maintenance
|
||||||
|
|
||||||
- Keep this file up to date whenever actor ownership, hierarchy, or lifecycle responsibilities change.
|
- 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 * as schema from "./schema.js";
|
||||||
import migrations from "./migrations.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"),
|
taskId: text("task_id"),
|
||||||
branchName: text("branch_name"),
|
branchName: text("branch_name"),
|
||||||
kind: text("kind").notNull(),
|
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(),
|
payloadJson: text("payload_json").notNull(),
|
||||||
createdAt: integer("created_at").notNull(),
|
createdAt: integer("created_at").notNull(),
|
||||||
});
|
});
|
||||||
|
|
@ -2,32 +2,31 @@
|
||||||
import { and, desc, eq } from "drizzle-orm";
|
import { and, desc, eq } from "drizzle-orm";
|
||||||
import { actor, queue } from "rivetkit";
|
import { actor, queue } from "rivetkit";
|
||||||
import { Loop, workflow } from "rivetkit/workflow";
|
import { Loop, workflow } from "rivetkit/workflow";
|
||||||
import type { HistoryEvent } from "@sandbox-agent/foundry-shared";
|
import type { AuditLogEvent } from "@sandbox-agent/foundry-shared";
|
||||||
import { selfHistory } from "../handles.js";
|
import { auditLogDb } from "./db/db.js";
|
||||||
import { historyDb } from "./db/db.js";
|
|
||||||
import { events } from "./db/schema.js";
|
import { events } from "./db/schema.js";
|
||||||
|
|
||||||
export interface HistoryInput {
|
export interface AuditLogInput {
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
repoId: string;
|
repoId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppendHistoryCommand {
|
export interface AppendAuditLogCommand {
|
||||||
kind: string;
|
kind: string;
|
||||||
taskId?: string;
|
taskId?: string;
|
||||||
branchName?: string;
|
branchName?: string;
|
||||||
payload: Record<string, unknown>;
|
payload: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ListHistoryParams {
|
export interface ListAuditLogParams {
|
||||||
branch?: string;
|
branch?: string;
|
||||||
taskId?: string;
|
taskId?: string;
|
||||||
limit?: number;
|
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();
|
const now = Date.now();
|
||||||
await loopCtx.db
|
await loopCtx.db
|
||||||
.insert(events)
|
.insert(events)
|
||||||
|
|
@ -41,18 +40,18 @@ async function appendHistoryRow(loopCtx: any, body: AppendHistoryCommand): Promi
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runHistoryWorkflow(ctx: any): Promise<void> {
|
async function runAuditLogWorkflow(ctx: any): Promise<void> {
|
||||||
await ctx.loop("history-command-loop", async (loopCtx: any) => {
|
await ctx.loop("audit-log-command-loop", async (loopCtx: any) => {
|
||||||
const msg = await loopCtx.queue.next("next-history-command", {
|
const msg = await loopCtx.queue.next("next-audit-log-command", {
|
||||||
names: [...HISTORY_QUEUE_NAMES],
|
names: [...AUDIT_LOG_QUEUE_NAMES],
|
||||||
completable: true,
|
completable: true,
|
||||||
});
|
});
|
||||||
if (!msg) {
|
if (!msg) {
|
||||||
return Loop.continue(undefined);
|
return Loop.continue(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg.name === "history.command.append") {
|
if (msg.name === "auditLog.command.append") {
|
||||||
await loopCtx.step("append-history-row", async () => appendHistoryRow(loopCtx, msg.body as AppendHistoryCommand));
|
await loopCtx.step("append-audit-log-row", async () => appendAuditLogRow(loopCtx, msg.body as AppendAuditLogCommand));
|
||||||
await msg.complete({ ok: true });
|
await msg.complete({ ok: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -60,26 +59,21 @@ async function runHistoryWorkflow(ctx: any): Promise<void> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const history = actor({
|
export const auditLog = actor({
|
||||||
db: historyDb,
|
db: auditLogDb,
|
||||||
queues: {
|
queues: {
|
||||||
"history.command.append": queue(),
|
"auditLog.command.append": queue(),
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
name: "History",
|
name: "Audit Log",
|
||||||
icon: "database",
|
icon: "database",
|
||||||
},
|
},
|
||||||
createState: (_c, input: HistoryInput) => ({
|
createState: (_c, input: AuditLogInput) => ({
|
||||||
organizationId: input.organizationId,
|
organizationId: input.organizationId,
|
||||||
repoId: input.repoId,
|
repoId: input.repoId,
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
async append(c, command: AppendHistoryCommand): Promise<void> {
|
async list(c, params?: ListAuditLogParams): Promise<AuditLogEvent[]> {
|
||||||
const self = selfHistory(c);
|
|
||||||
await self.send("history.command.append", command, { wait: true, timeout: 15_000 });
|
|
||||||
},
|
|
||||||
|
|
||||||
async list(c, params?: ListHistoryParams): Promise<HistoryEvent[]> {
|
|
||||||
const whereParts = [];
|
const whereParts = [];
|
||||||
if (params?.taskId) {
|
if (params?.taskId) {
|
||||||
whereParts.push(eq(events.taskId, 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,
|
\`installation_id\` integer,
|
||||||
\`last_sync_label\` text NOT NULL,
|
\`last_sync_label\` text NOT NULL,
|
||||||
\`last_sync_at\` integer,
|
\`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
|
--> statement-breakpoint
|
||||||
CREATE TABLE \`github_repositories\` (
|
CREATE TABLE \`github_repositories\` (
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
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", {
|
export const githubMeta = sqliteTable(
|
||||||
|
"github_meta",
|
||||||
|
{
|
||||||
id: integer("id").primaryKey(),
|
id: integer("id").primaryKey(),
|
||||||
connectedAccount: text("connected_account").notNull(),
|
connectedAccount: text("connected_account").notNull(),
|
||||||
installationStatus: text("installation_status").notNull(),
|
installationStatus: text("installation_status").notNull(),
|
||||||
|
|
@ -9,7 +12,9 @@ export const githubMeta = sqliteTable("github_meta", {
|
||||||
lastSyncLabel: text("last_sync_label").notNull(),
|
lastSyncLabel: text("last_sync_label").notNull(),
|
||||||
lastSyncAt: integer("last_sync_at"),
|
lastSyncAt: integer("last_sync_at"),
|
||||||
updatedAt: integer("updated_at").notNull(),
|
updatedAt: integer("updated_at").notNull(),
|
||||||
});
|
},
|
||||||
|
(table) => [check("github_meta_singleton_id_check", sql`${table.id} = 1`)],
|
||||||
|
);
|
||||||
|
|
||||||
export const githubRepositories = sqliteTable("github_repositories", {
|
export const githubRepositories = sqliteTable("github_repositories", {
|
||||||
repoId: text("repo_id").notNull().primaryKey(),
|
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);
|
return await runFullSync(c, input);
|
||||||
},
|
},
|
||||||
|
|
||||||
async reloadOrganization(c) {
|
async adminReloadOrganization(c) {
|
||||||
return await runFullSync(c, { label: "Reloading GitHub organization..." });
|
return await runFullSync(c, { label: "Reloading GitHub organization..." });
|
||||||
},
|
},
|
||||||
|
|
||||||
async reloadAllPullRequests(c) {
|
async adminReloadAllPullRequests(c) {
|
||||||
return await runFullSync(c, { label: "Reloading GitHub pull requests..." });
|
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);
|
const beforeRows = await readAllPullRequestRows(c);
|
||||||
await c.db.delete(githubPullRequests).run();
|
await c.db.delete(githubPullRequests).run();
|
||||||
await c.db.delete(githubBranches).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) {
|
export function actorClient(c: any) {
|
||||||
return c.client();
|
return c.client();
|
||||||
|
|
@ -10,14 +10,14 @@ export async function getOrCreateOrganization(c: any, organizationId: string) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getOrCreateAuthUser(c: any, userId: string) {
|
export async function getOrCreateUser(c: any, userId: string) {
|
||||||
return await actorClient(c).authUser.getOrCreate(authUserKey(userId), {
|
return await actorClient(c).user.getOrCreate(userKey(userId), {
|
||||||
createWithInput: { userId },
|
createWithInput: { userId },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAuthUser(c: any, userId: string) {
|
export function getUser(c: any, userId: string) {
|
||||||
return actorClient(c).authUser.get(authUserKey(userId));
|
return actorClient(c).user.get(userKey(userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getOrCreateRepository(c: any, organizationId: string, repoId: string, remoteUrl: string) {
|
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) {
|
export async function getOrCreateAuditLog(c: any, organizationId: string, repoId: string) {
|
||||||
return await actorClient(c).history.getOrCreate(historyKey(organizationId, repoId), {
|
return await actorClient(c).auditLog.getOrCreate(auditLogKey(organizationId, repoId), {
|
||||||
createWithInput: {
|
createWithInput: {
|
||||||
organizationId,
|
organizationId,
|
||||||
repoId,
|
repoId,
|
||||||
|
|
@ -75,8 +75,8 @@ export async function getOrCreateTaskSandbox(c: any, organizationId: string, san
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function selfHistory(c: any) {
|
export function selfAuditLog(c: any) {
|
||||||
return actorClient(c).history.getForId(c.actorId);
|
return actorClient(c).auditLog.getForId(c.actorId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function selfTask(c: any) {
|
export function selfTask(c: any) {
|
||||||
|
|
@ -91,8 +91,8 @@ export function selfRepository(c: any) {
|
||||||
return actorClient(c).repository.getForId(c.actorId);
|
return actorClient(c).repository.getForId(c.actorId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function selfAuthUser(c: any) {
|
export function selfUser(c: any) {
|
||||||
return actorClient(c).authUser.getForId(c.actorId);
|
return actorClient(c).user.getForId(c.actorId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function selfGithubData(c: any) {
|
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 { setup } from "rivetkit";
|
||||||
import { githubData } from "./github-data/index.js";
|
import { githubData } from "./github-data/index.js";
|
||||||
import { task } from "./task/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 { repository } from "./repository/index.js";
|
||||||
import { taskSandbox } from "./sandbox/index.js";
|
import { taskSandbox } from "./sandbox/index.js";
|
||||||
import { organization } from "./organization/index.js";
|
import { organization } from "./organization/index.js";
|
||||||
|
|
@ -21,22 +21,22 @@ export const registry = setup({
|
||||||
baseLogger: logger,
|
baseLogger: logger,
|
||||||
},
|
},
|
||||||
use: {
|
use: {
|
||||||
authUser,
|
user,
|
||||||
organization,
|
organization,
|
||||||
repository,
|
repository,
|
||||||
task,
|
task,
|
||||||
taskSandbox,
|
taskSandbox,
|
||||||
history,
|
auditLog,
|
||||||
githubData,
|
githubData,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export * from "./context.js";
|
export * from "./context.js";
|
||||||
export * from "./events.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 "./github-data/index.js";
|
||||||
export * from "./task/index.js";
|
export * from "./task/index.js";
|
||||||
export * from "./history/index.js";
|
|
||||||
export * from "./keys.js";
|
export * from "./keys.js";
|
||||||
export * from "./repository/index.js";
|
export * from "./repository/index.js";
|
||||||
export * from "./sandbox/index.js";
|
export * from "./sandbox/index.js";
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ export function organizationKey(organizationId: string): ActorKey {
|
||||||
return ["org", organizationId];
|
return ["org", organizationId];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function authUserKey(userId: string): ActorKey {
|
export function userKey(userId: string): ActorKey {
|
||||||
return ["org", "app", "user", userId];
|
return ["org", "app", "user", userId];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -20,8 +20,8 @@ export function taskSandboxKey(organizationId: string, sandboxId: string): Actor
|
||||||
return ["org", organizationId, "sandbox", sandboxId];
|
return ["org", organizationId, "sandbox", sandboxId];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function historyKey(organizationId: string, repoId: string): ActorKey {
|
export function auditLogKey(organizationId: string, repoId: string): ActorKey {
|
||||||
return ["org", organizationId, "repository", repoId, "history"];
|
return ["org", organizationId, "repository", repoId, "audit-log"];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function githubDataKey(organizationId: string): ActorKey {
|
export function githubDataKey(organizationId: string): ActorKey {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { desc, eq } from "drizzle-orm";
|
||||||
import { Loop } from "rivetkit/workflow";
|
import { Loop } from "rivetkit/workflow";
|
||||||
import type {
|
import type {
|
||||||
CreateTaskInput,
|
CreateTaskInput,
|
||||||
HistoryEvent,
|
AuditLogEvent,
|
||||||
HistoryQueryInput,
|
HistoryQueryInput,
|
||||||
ListTasksInput,
|
ListTasksInput,
|
||||||
SandboxProviderId,
|
SandboxProviderId,
|
||||||
|
|
@ -14,32 +14,30 @@ import type {
|
||||||
SwitchResult,
|
SwitchResult,
|
||||||
TaskRecord,
|
TaskRecord,
|
||||||
TaskSummary,
|
TaskSummary,
|
||||||
TaskWorkbenchChangeModelInput,
|
TaskWorkspaceChangeModelInput,
|
||||||
TaskWorkbenchCreateTaskInput,
|
TaskWorkspaceCreateTaskInput,
|
||||||
TaskWorkbenchDiffInput,
|
TaskWorkspaceDiffInput,
|
||||||
TaskWorkbenchRenameInput,
|
TaskWorkspaceRenameInput,
|
||||||
TaskWorkbenchRenameSessionInput,
|
TaskWorkspaceRenameSessionInput,
|
||||||
TaskWorkbenchSelectInput,
|
TaskWorkspaceSelectInput,
|
||||||
TaskWorkbenchSetSessionUnreadInput,
|
TaskWorkspaceSetSessionUnreadInput,
|
||||||
TaskWorkbenchSendMessageInput,
|
TaskWorkspaceSendMessageInput,
|
||||||
TaskWorkbenchSessionInput,
|
TaskWorkspaceSessionInput,
|
||||||
TaskWorkbenchUpdateDraftInput,
|
TaskWorkspaceUpdateDraftInput,
|
||||||
WorkbenchOpenPrSummary,
|
WorkspaceRepositorySummary,
|
||||||
WorkbenchRepositorySummary,
|
WorkspaceTaskSummary,
|
||||||
WorkbenchSessionSummary,
|
|
||||||
WorkbenchTaskSummary,
|
|
||||||
OrganizationEvent,
|
OrganizationEvent,
|
||||||
OrganizationSummarySnapshot,
|
OrganizationSummarySnapshot,
|
||||||
OrganizationUseInput,
|
OrganizationUseInput,
|
||||||
} from "@sandbox-agent/foundry-shared";
|
} from "@sandbox-agent/foundry-shared";
|
||||||
import { getActorRuntimeContext } from "../context.js";
|
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 { logActorWarning, resolveErrorMessage } from "../logging.js";
|
||||||
import { defaultSandboxProviderId } from "../../sandbox-config.js";
|
import { defaultSandboxProviderId } from "../../sandbox-config.js";
|
||||||
import { repoIdFromRemote } from "../../services/repo.js";
|
import { repoIdFromRemote } from "../../services/repo.js";
|
||||||
import { resolveOrganizationGithubAuth } from "../../services/github-auth.js";
|
import { resolveOrganizationGithubAuth } from "../../services/github-auth.js";
|
||||||
import { organizationProfile, taskLookup, repos, taskSummaries } from "./db/schema.js";
|
import { organizationProfile, repos } from "./db/schema.js";
|
||||||
import { agentTypeForModel } from "../task/workbench.js";
|
import { agentTypeForModel } from "../task/workspace.js";
|
||||||
import { expectQueueResponse } from "../../services/queue.js";
|
import { expectQueueResponse } from "../../services/queue.js";
|
||||||
import { organizationAppActions } from "./app-shell.js";
|
import { organizationAppActions } from "./app-shell.js";
|
||||||
|
|
||||||
|
|
@ -49,6 +47,7 @@ interface OrganizationState {
|
||||||
|
|
||||||
interface GetTaskInput {
|
interface GetTaskInput {
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
|
repoId?: string;
|
||||||
taskId: string;
|
taskId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -72,7 +71,7 @@ export function organizationWorkflowQueueName(name: OrganizationQueueName): Orga
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ORGANIZATION_PROFILE_ROW_ID = "profile";
|
const ORGANIZATION_PROFILE_ROW_ID = 1;
|
||||||
|
|
||||||
function assertOrganization(c: { state: OrganizationState }, organizationId: string): void {
|
function assertOrganization(c: { state: OrganizationState }, organizationId: string): void {
|
||||||
if (organizationId !== c.state.organizationId) {
|
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[]> {
|
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();
|
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;
|
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 repoTasks = taskRows.filter((task) => task.repoId === repoRow.repoId);
|
||||||
const latestActivityMs = repoTasks.reduce((latest, task) => Math.max(latest, task.updatedAtMs), repoRow.updatedAt);
|
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) {
|
async function resolveRepositoryForTask(c: any, taskId: string, repoId?: string | null) {
|
||||||
return {
|
if (repoId) {
|
||||||
taskId: taskSummary.id,
|
const repoRow = await c.db.select({ remoteUrl: repos.remoteUrl }).from(repos).where(eq(repos.repoId, repoId)).get();
|
||||||
repoId: taskSummary.repoId,
|
if (!repoRow) {
|
||||||
title: taskSummary.title,
|
throw new Error(`Unknown repo: ${repoId}`);
|
||||||
status: taskSummary.status,
|
}
|
||||||
repoName: taskSummary.repoName,
|
const repository = await getOrCreateRepository(c, c.state.organizationId, repoId, repoRow.remoteUrl);
|
||||||
updatedAtMs: taskSummary.updatedAtMs,
|
return { repoId, repository };
|
||||||
branch: taskSummary.branch,
|
|
||||||
pullRequestJson: JSON.stringify(taskSummary.pullRequest),
|
|
||||||
sessionsSummaryJson: JSON.stringify(taskSummary.sessionsSummary),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function taskSummaryFromRow(row: any): WorkbenchTaskSummary {
|
const repoRows = await c.db.select({ repoId: repos.repoId, remoteUrl: repos.remoteUrl }).from(repos).orderBy(desc(repos.updatedAt)).all();
|
||||||
return {
|
for (const row of repoRows) {
|
||||||
id: row.taskId,
|
const repository = await getOrCreateRepository(c, c.state.organizationId, row.repoId, row.remoteUrl);
|
||||||
repoId: row.repoId,
|
const summaries = await repository.listTaskSummaries({ includeArchived: true });
|
||||||
title: row.title,
|
if (summaries.some((summary: TaskSummary) => summary.taskId === taskId)) {
|
||||||
status: row.status,
|
return { repoId: row.repoId, repository };
|
||||||
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[]> {
|
throw new Error(`Unknown task: ${taskId}`);
|
||||||
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
|
const repoRows = await c.db
|
||||||
.select({ repoId: repos.repoId, remoteUrl: repos.remoteUrl, updatedAt: repos.updatedAt })
|
.select({ repoId: repos.repoId, remoteUrl: repos.remoteUrl, updatedAt: repos.updatedAt })
|
||||||
.from(repos)
|
.from(repos)
|
||||||
.orderBy(desc(repos.updatedAt))
|
.orderBy(desc(repos.updatedAt))
|
||||||
.all();
|
.all();
|
||||||
|
|
||||||
const taskRows: WorkbenchTaskSummary[] = [];
|
const taskRows: WorkspaceTaskSummary[] = [];
|
||||||
for (const row of repoRows) {
|
for (const row of repoRows) {
|
||||||
try {
|
try {
|
||||||
const repository = await getOrCreateRepository(c, c.state.organizationId, row.repoId, row.remoteUrl);
|
const repository = await getOrCreateRepository(c, c.state.organizationId, row.repoId, row.remoteUrl);
|
||||||
const summaries = await repository.listTaskSummaries({ includeArchived: true });
|
taskRows.push(...(await repository.listWorkspaceTaskSummaries({})));
|
||||||
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) {
|
} catch (error) {
|
||||||
logActorWarning("organization", "failed collecting task summary during reconciliation", {
|
logActorWarning("organization", "failed collecting repo during workspace reconciliation", {
|
||||||
organizationId: c.state.organizationId,
|
|
||||||
repoId: row.repoId,
|
|
||||||
taskId: summary.taskId,
|
|
||||||
error: resolveErrorMessage(error),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logActorWarning("organization", "failed collecting repo during workbench reconciliation", {
|
|
||||||
organizationId: c.state.organizationId,
|
organizationId: c.state.organizationId,
|
||||||
repoId: row.repoId,
|
repoId: row.repoId,
|
||||||
error: resolveErrorMessage(error),
|
error: resolveErrorMessage(error),
|
||||||
|
|
@ -249,19 +175,17 @@ async function reconcileWorkbenchProjection(c: any): Promise<OrganizationSummary
|
||||||
organizationId: c.state.organizationId,
|
organizationId: c.state.organizationId,
|
||||||
repos: repoRows.map((row) => buildRepoSummary(row, taskRows)).sort((left, right) => right.latestActivityMs - left.latestActivityMs),
|
repos: repoRows.map((row) => buildRepoSummary(row, taskRows)).sort((left, right) => right.latestActivityMs - left.latestActivityMs),
|
||||||
taskSummaries: taskRows,
|
taskSummaries: taskRows,
|
||||||
openPullRequests: await listOpenPullRequestsSnapshot(c, taskRows),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function requireWorkbenchTask(c: any, taskId: string) {
|
async function requireWorkspaceTask(c: any, repoId: string, taskId: string) {
|
||||||
const repoId = await resolveRepoId(c, taskId);
|
return getTaskHandle(c, c.state.organizationId, repoId, taskId);
|
||||||
return getTask(c, c.state.organizationId, repoId, taskId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads the organization sidebar snapshot from the organization actor's local SQLite
|
* Reads the organization sidebar snapshot by fanning out one level to the
|
||||||
* plus the org-scoped GitHub actor for open PRs. Task actors still push
|
* repository coordinators. Task summaries are repository-owned; organization
|
||||||
* summary updates into `task_summaries`, so the hot read path stays bounded.
|
* only aggregates them.
|
||||||
*/
|
*/
|
||||||
async function getOrganizationSummarySnapshot(c: any): Promise<OrganizationSummarySnapshot> {
|
async function getOrganizationSummarySnapshot(c: any): Promise<OrganizationSummarySnapshot> {
|
||||||
const repoRows = await c.db
|
const repoRows = await c.db
|
||||||
|
|
@ -273,25 +197,33 @@ async function getOrganizationSummarySnapshot(c: any): Promise<OrganizationSumma
|
||||||
.from(repos)
|
.from(repos)
|
||||||
.orderBy(desc(repos.updatedAt))
|
.orderBy(desc(repos.updatedAt))
|
||||||
.all();
|
.all();
|
||||||
const taskRows = await c.db.select().from(taskSummaries).orderBy(desc(taskSummaries.updatedAtMs)).all();
|
const summaries: WorkspaceTaskSummary[] = [];
|
||||||
const summaries = taskRows.map(taskSummaryFromRow);
|
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 {
|
return {
|
||||||
organizationId: c.state.organizationId,
|
organizationId: c.state.organizationId,
|
||||||
repos: repoRows.map((row) => buildRepoSummary(row, summaries)).sort((left, right) => right.latestActivityMs - left.latestActivityMs),
|
repos: repoRows.map((row) => buildRepoSummary(row, summaries)).sort((left, right) => right.latestActivityMs - left.latestActivityMs),
|
||||||
taskSummaries: summaries,
|
taskSummaries: summaries,
|
||||||
openPullRequests: await listOpenPullRequestsSnapshot(c, summaries),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function broadcastRepoSummary(
|
async function broadcastOrganizationSnapshot(c: any): Promise<void> {
|
||||||
c: any,
|
c.broadcast("organizationUpdated", {
|
||||||
type: "repoAdded" | "repoUpdated",
|
type: "organizationUpdated",
|
||||||
repoRow: { repoId: string; remoteUrl: string; updatedAt: number },
|
snapshot: await getOrganizationSummarySnapshot(c),
|
||||||
): Promise<void> {
|
} satisfies OrganizationEvent);
|
||||||
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 createTaskMutation(c: any, input: CreateTaskInput): Promise<TaskRecord> {
|
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,
|
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;
|
return created;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -451,67 +357,8 @@ export const organizationActions = {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
async refreshOrganizationSnapshot(c: any): Promise<void> {
|
||||||
* Called by task actors when their summary-level state changes.
|
await broadcastOrganizationSnapshot(c);
|
||||||
* 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 applyGithubRepositoryProjection(c: any, input: { repoId: string; remoteUrl: string }): Promise<void> {
|
async applyGithubRepositoryProjection(c: any, input: { repoId: string; remoteUrl: string }): Promise<void> {
|
||||||
|
|
@ -533,11 +380,7 @@ export const organizationActions = {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
await broadcastRepoSummary(c, existing ? "repoUpdated" : "repoAdded", {
|
await broadcastOrganizationSnapshot(c);
|
||||||
repoId: input.repoId,
|
|
||||||
remoteUrl: input.remoteUrl,
|
|
||||||
updatedAt: now,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async applyGithubDataProjection(
|
async applyGithubDataProjection(
|
||||||
|
|
@ -576,11 +419,7 @@ export const organizationActions = {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
await broadcastRepoSummary(c, existingById.has(repoId) ? "repoUpdated" : "repoAdded", {
|
await broadcastOrganizationSnapshot(c);
|
||||||
repoId,
|
|
||||||
remoteUrl: repository.cloneUrl,
|
|
||||||
updatedAt: now,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const repo of existingRepos) {
|
for (const repo of existingRepos) {
|
||||||
|
|
@ -588,7 +427,7 @@ export const organizationActions = {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
await c.db.delete(repos).where(eq(repos.repoId, repo.repoId)).run();
|
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
|
const profile = await c.db
|
||||||
|
|
@ -648,12 +487,12 @@ export const organizationActions = {
|
||||||
return await getOrganizationSummarySnapshot(c);
|
return await getOrganizationSummarySnapshot(c);
|
||||||
},
|
},
|
||||||
|
|
||||||
async reconcileWorkbenchState(c: any, input: OrganizationUseInput): Promise<OrganizationSummarySnapshot> {
|
async adminReconcileWorkspaceState(c: any, input: OrganizationUseInput): Promise<OrganizationSummarySnapshot> {
|
||||||
assertOrganization(c, input.organizationId);
|
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).
|
// Step 1: Create the task record (wait: true — local state mutations only).
|
||||||
const created = await organizationActions.createTask(c, {
|
const created = await organizationActions.createTask(c, {
|
||||||
organizationId: c.state.organizationId,
|
organizationId: c.state.organizationId,
|
||||||
|
|
@ -668,8 +507,8 @@ export const organizationActions = {
|
||||||
// The task workflow creates the session record and sends the message in
|
// The task workflow creates the session record and sends the message in
|
||||||
// the background. The client observes progress via push events on the
|
// the background. The client observes progress via push events on the
|
||||||
// task subscription topic.
|
// task subscription topic.
|
||||||
const task = await requireWorkbenchTask(c, created.taskId);
|
const task = await requireWorkspaceTask(c, input.repoId, created.taskId);
|
||||||
await task.createWorkbenchSessionAndSend({
|
await task.createWorkspaceSessionAndSend({
|
||||||
model: input.model,
|
model: input.model,
|
||||||
text: input.task,
|
text: input.task,
|
||||||
});
|
});
|
||||||
|
|
@ -677,84 +516,79 @@ export const organizationActions = {
|
||||||
return { taskId: created.taskId };
|
return { taskId: created.taskId };
|
||||||
},
|
},
|
||||||
|
|
||||||
async markWorkbenchUnread(c: any, input: TaskWorkbenchSelectInput): Promise<void> {
|
async markWorkspaceUnread(c: any, input: TaskWorkspaceSelectInput): Promise<void> {
|
||||||
const task = await requireWorkbenchTask(c, input.taskId);
|
const task = await requireWorkspaceTask(c, input.repoId, input.taskId);
|
||||||
await task.markWorkbenchUnread({});
|
await task.markWorkspaceUnread({});
|
||||||
},
|
},
|
||||||
|
|
||||||
async renameWorkbenchTask(c: any, input: TaskWorkbenchRenameInput): Promise<void> {
|
async renameWorkspaceTask(c: any, input: TaskWorkspaceRenameInput): Promise<void> {
|
||||||
const task = await requireWorkbenchTask(c, input.taskId);
|
const task = await requireWorkspaceTask(c, input.repoId, input.taskId);
|
||||||
await task.renameWorkbenchTask(input);
|
await task.renameWorkspaceTask(input);
|
||||||
},
|
},
|
||||||
|
|
||||||
async renameWorkbenchBranch(c: any, input: TaskWorkbenchRenameInput): Promise<void> {
|
async createWorkspaceSession(c: any, input: TaskWorkspaceSelectInput & { model?: string }): Promise<{ sessionId: string }> {
|
||||||
const task = await requireWorkbenchTask(c, input.taskId);
|
const task = await requireWorkspaceTask(c, input.repoId, input.taskId);
|
||||||
await task.renameWorkbenchBranch(input);
|
return await task.createWorkspaceSession({ ...(input.model ? { model: input.model } : {}) });
|
||||||
},
|
},
|
||||||
|
|
||||||
async createWorkbenchSession(c: any, input: TaskWorkbenchSelectInput & { model?: string }): Promise<{ sessionId: string }> {
|
async renameWorkspaceSession(c: any, input: TaskWorkspaceRenameSessionInput): Promise<void> {
|
||||||
const task = await requireWorkbenchTask(c, input.taskId);
|
const task = await requireWorkspaceTask(c, input.repoId, input.taskId);
|
||||||
return await task.createWorkbenchSession({ ...(input.model ? { model: input.model } : {}) });
|
await task.renameWorkspaceSession(input);
|
||||||
},
|
},
|
||||||
|
|
||||||
async renameWorkbenchSession(c: any, input: TaskWorkbenchRenameSessionInput): Promise<void> {
|
async setWorkspaceSessionUnread(c: any, input: TaskWorkspaceSetSessionUnreadInput): Promise<void> {
|
||||||
const task = await requireWorkbenchTask(c, input.taskId);
|
const task = await requireWorkspaceTask(c, input.repoId, input.taskId);
|
||||||
await task.renameWorkbenchSession(input);
|
await task.setWorkspaceSessionUnread(input);
|
||||||
},
|
},
|
||||||
|
|
||||||
async setWorkbenchSessionUnread(c: any, input: TaskWorkbenchSetSessionUnreadInput): Promise<void> {
|
async updateWorkspaceDraft(c: any, input: TaskWorkspaceUpdateDraftInput): Promise<void> {
|
||||||
const task = await requireWorkbenchTask(c, input.taskId);
|
const task = await requireWorkspaceTask(c, input.repoId, input.taskId);
|
||||||
await task.setWorkbenchSessionUnread(input);
|
await task.updateWorkspaceDraft(input);
|
||||||
},
|
},
|
||||||
|
|
||||||
async updateWorkbenchDraft(c: any, input: TaskWorkbenchUpdateDraftInput): Promise<void> {
|
async changeWorkspaceModel(c: any, input: TaskWorkspaceChangeModelInput): Promise<void> {
|
||||||
const task = await requireWorkbenchTask(c, input.taskId);
|
const task = await requireWorkspaceTask(c, input.repoId, input.taskId);
|
||||||
await task.updateWorkbenchDraft(input);
|
await task.changeWorkspaceModel(input);
|
||||||
},
|
},
|
||||||
|
|
||||||
async changeWorkbenchModel(c: any, input: TaskWorkbenchChangeModelInput): Promise<void> {
|
async sendWorkspaceMessage(c: any, input: TaskWorkspaceSendMessageInput): Promise<void> {
|
||||||
const task = await requireWorkbenchTask(c, input.taskId);
|
const task = await requireWorkspaceTask(c, input.repoId, input.taskId);
|
||||||
await task.changeWorkbenchModel(input);
|
await task.sendWorkspaceMessage(input);
|
||||||
},
|
},
|
||||||
|
|
||||||
async sendWorkbenchMessage(c: any, input: TaskWorkbenchSendMessageInput): Promise<void> {
|
async stopWorkspaceSession(c: any, input: TaskWorkspaceSessionInput): Promise<void> {
|
||||||
const task = await requireWorkbenchTask(c, input.taskId);
|
const task = await requireWorkspaceTask(c, input.repoId, input.taskId);
|
||||||
await task.sendWorkbenchMessage(input);
|
await task.stopWorkspaceSession(input);
|
||||||
},
|
},
|
||||||
|
|
||||||
async stopWorkbenchSession(c: any, input: TaskWorkbenchSessionInput): Promise<void> {
|
async closeWorkspaceSession(c: any, input: TaskWorkspaceSessionInput): Promise<void> {
|
||||||
const task = await requireWorkbenchTask(c, input.taskId);
|
const task = await requireWorkspaceTask(c, input.repoId, input.taskId);
|
||||||
await task.stopWorkbenchSession(input);
|
await task.closeWorkspaceSession(input);
|
||||||
},
|
},
|
||||||
|
|
||||||
async closeWorkbenchSession(c: any, input: TaskWorkbenchSessionInput): Promise<void> {
|
async publishWorkspacePr(c: any, input: TaskWorkspaceSelectInput): Promise<void> {
|
||||||
const task = await requireWorkbenchTask(c, input.taskId);
|
const task = await requireWorkspaceTask(c, input.repoId, input.taskId);
|
||||||
await task.closeWorkbenchSession(input);
|
await task.publishWorkspacePr({});
|
||||||
},
|
},
|
||||||
|
|
||||||
async publishWorkbenchPr(c: any, input: TaskWorkbenchSelectInput): Promise<void> {
|
async revertWorkspaceFile(c: any, input: TaskWorkspaceDiffInput): Promise<void> {
|
||||||
const task = await requireWorkbenchTask(c, input.taskId);
|
const task = await requireWorkspaceTask(c, input.repoId, input.taskId);
|
||||||
await task.publishWorkbenchPr({});
|
await task.revertWorkspaceFile(input);
|
||||||
},
|
},
|
||||||
|
|
||||||
async revertWorkbenchFile(c: any, input: TaskWorkbenchDiffInput): Promise<void> {
|
async adminReloadGithubOrganization(c: any): Promise<void> {
|
||||||
const task = await requireWorkbenchTask(c, input.taskId);
|
await getOrCreateGithubData(c, c.state.organizationId).adminReloadOrganization({});
|
||||||
await task.revertWorkbenchFile(input);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async reloadGithubOrganization(c: any): Promise<void> {
|
async adminReloadGithubPullRequests(c: any): Promise<void> {
|
||||||
await getOrCreateGithubData(c, c.state.organizationId).reloadOrganization({});
|
await getOrCreateGithubData(c, c.state.organizationId).adminReloadAllPullRequests({});
|
||||||
},
|
},
|
||||||
|
|
||||||
async reloadGithubPullRequests(c: any): Promise<void> {
|
async adminReloadGithubRepository(c: any, input: { repoId: string }): Promise<void> {
|
||||||
await getOrCreateGithubData(c, c.state.organizationId).reloadAllPullRequests({});
|
|
||||||
},
|
|
||||||
|
|
||||||
async reloadGithubRepository(c: any, input: { repoId: string }): Promise<void> {
|
|
||||||
await getOrCreateGithubData(c, c.state.organizationId).reloadRepository(input);
|
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);
|
await getOrCreateGithubData(c, c.state.organizationId).reloadPullRequest(input);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -786,39 +620,39 @@ export const organizationActions = {
|
||||||
return await repository.getRepoOverview({});
|
return await repository.getRepoOverview({});
|
||||||
},
|
},
|
||||||
|
|
||||||
async switchTask(c: any, taskId: string): Promise<SwitchResult> {
|
async switchTask(c: any, input: { repoId?: string; taskId: string }): Promise<SwitchResult> {
|
||||||
const repoId = await resolveRepoId(c, taskId);
|
const { repoId } = await resolveRepositoryForTask(c, input.taskId, input.repoId);
|
||||||
const h = getTask(c, c.state.organizationId, repoId, taskId);
|
const h = getTaskHandle(c, c.state.organizationId, repoId, input.taskId);
|
||||||
const record = await h.get();
|
const record = await h.get();
|
||||||
const switched = await h.switch();
|
const switched = await h.switch();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
organizationId: c.state.organizationId,
|
organizationId: c.state.organizationId,
|
||||||
taskId,
|
taskId: input.taskId,
|
||||||
sandboxProviderId: record.sandboxProviderId,
|
sandboxProviderId: record.sandboxProviderId,
|
||||||
switchTarget: switched.switchTarget,
|
switchTarget: switched.switchTarget,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
async history(c: any, input: HistoryQueryInput): Promise<HistoryEvent[]> {
|
async auditLog(c: any, input: HistoryQueryInput): Promise<AuditLogEvent[]> {
|
||||||
assertOrganization(c, input.organizationId);
|
assertOrganization(c, input.organizationId);
|
||||||
|
|
||||||
const limit = input.limit ?? 20;
|
const limit = input.limit ?? 20;
|
||||||
const repoRows = await c.db.select({ repoId: repos.repoId }).from(repos).all();
|
const repoRows = await c.db.select({ repoId: repos.repoId }).from(repos).all();
|
||||||
|
|
||||||
const allEvents: HistoryEvent[] = [];
|
const allEvents: AuditLogEvent[] = [];
|
||||||
|
|
||||||
for (const row of repoRows) {
|
for (const row of repoRows) {
|
||||||
try {
|
try {
|
||||||
const hist = await getOrCreateHistory(c, c.state.organizationId, row.repoId);
|
const auditLog = await getOrCreateAuditLog(c, c.state.organizationId, row.repoId);
|
||||||
const items = await hist.list({
|
const items = await auditLog.list({
|
||||||
branch: input.branch,
|
branch: input.branch,
|
||||||
taskId: input.taskId,
|
taskId: input.taskId,
|
||||||
limit,
|
limit,
|
||||||
});
|
});
|
||||||
allEvents.push(...items);
|
allEvents.push(...items);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logActorWarning("organization", "history lookup failed for repo", {
|
logActorWarning("organization", "audit log lookup failed for repo", {
|
||||||
organizationId: c.state.organizationId,
|
organizationId: c.state.organizationId,
|
||||||
repoId: row.repoId,
|
repoId: row.repoId,
|
||||||
error: resolveErrorMessage(error),
|
error: resolveErrorMessage(error),
|
||||||
|
|
@ -832,57 +666,49 @@ export const organizationActions = {
|
||||||
|
|
||||||
async getTask(c: any, input: GetTaskInput): Promise<TaskRecord> {
|
async getTask(c: any, input: GetTaskInput): Promise<TaskRecord> {
|
||||||
assertOrganization(c, input.organizationId);
|
assertOrganization(c, input.organizationId);
|
||||||
|
const { repoId } = await resolveRepositoryForTask(c, input.taskId, input.repoId);
|
||||||
const repoId = await resolveRepoId(c, input.taskId);
|
return await getTaskHandle(c, c.state.organizationId, repoId, input.taskId).get();
|
||||||
|
|
||||||
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 });
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async attachTask(c: any, input: TaskProxyActionInput): Promise<{ target: string; sessionId: string | null }> {
|
async attachTask(c: any, input: TaskProxyActionInput): Promise<{ target: string; sessionId: string | null }> {
|
||||||
assertOrganization(c, input.organizationId);
|
assertOrganization(c, input.organizationId);
|
||||||
const repoId = await resolveRepoId(c, input.taskId);
|
const { repoId } = await resolveRepositoryForTask(c, input.taskId, input.repoId);
|
||||||
const h = getTask(c, c.state.organizationId, repoId, input.taskId);
|
const h = getTaskHandle(c, c.state.organizationId, repoId, input.taskId);
|
||||||
return await h.attach({ reason: input.reason });
|
return await h.attach({ reason: input.reason });
|
||||||
},
|
},
|
||||||
|
|
||||||
async pushTask(c: any, input: TaskProxyActionInput): Promise<void> {
|
async pushTask(c: any, input: TaskProxyActionInput): Promise<void> {
|
||||||
assertOrganization(c, input.organizationId);
|
assertOrganization(c, input.organizationId);
|
||||||
const repoId = await resolveRepoId(c, input.taskId);
|
const { repoId } = await resolveRepositoryForTask(c, input.taskId, input.repoId);
|
||||||
const h = getTask(c, c.state.organizationId, repoId, input.taskId);
|
const h = getTaskHandle(c, c.state.organizationId, repoId, input.taskId);
|
||||||
await h.push({ reason: input.reason });
|
await h.push({ reason: input.reason });
|
||||||
},
|
},
|
||||||
|
|
||||||
async syncTask(c: any, input: TaskProxyActionInput): Promise<void> {
|
async syncTask(c: any, input: TaskProxyActionInput): Promise<void> {
|
||||||
assertOrganization(c, input.organizationId);
|
assertOrganization(c, input.organizationId);
|
||||||
const repoId = await resolveRepoId(c, input.taskId);
|
const { repoId } = await resolveRepositoryForTask(c, input.taskId, input.repoId);
|
||||||
const h = getTask(c, c.state.organizationId, repoId, input.taskId);
|
const h = getTaskHandle(c, c.state.organizationId, repoId, input.taskId);
|
||||||
await h.sync({ reason: input.reason });
|
await h.sync({ reason: input.reason });
|
||||||
},
|
},
|
||||||
|
|
||||||
async mergeTask(c: any, input: TaskProxyActionInput): Promise<void> {
|
async mergeTask(c: any, input: TaskProxyActionInput): Promise<void> {
|
||||||
assertOrganization(c, input.organizationId);
|
assertOrganization(c, input.organizationId);
|
||||||
const repoId = await resolveRepoId(c, input.taskId);
|
const { repoId } = await resolveRepositoryForTask(c, input.taskId, input.repoId);
|
||||||
const h = getTask(c, c.state.organizationId, repoId, input.taskId);
|
const h = getTaskHandle(c, c.state.organizationId, repoId, input.taskId);
|
||||||
await h.merge({ reason: input.reason });
|
await h.merge({ reason: input.reason });
|
||||||
},
|
},
|
||||||
|
|
||||||
async archiveTask(c: any, input: TaskProxyActionInput): Promise<void> {
|
async archiveTask(c: any, input: TaskProxyActionInput): Promise<void> {
|
||||||
assertOrganization(c, input.organizationId);
|
assertOrganization(c, input.organizationId);
|
||||||
const repoId = await resolveRepoId(c, input.taskId);
|
const { repoId } = await resolveRepositoryForTask(c, input.taskId, input.repoId);
|
||||||
const h = getTask(c, c.state.organizationId, repoId, input.taskId);
|
const h = getTaskHandle(c, c.state.organizationId, repoId, input.taskId);
|
||||||
await h.archive({ reason: input.reason });
|
await h.archive({ reason: input.reason });
|
||||||
},
|
},
|
||||||
|
|
||||||
async killTask(c: any, input: TaskProxyActionInput): Promise<void> {
|
async killTask(c: any, input: TaskProxyActionInput): Promise<void> {
|
||||||
assertOrganization(c, input.organizationId);
|
assertOrganization(c, input.organizationId);
|
||||||
const repoId = await resolveRepoId(c, input.taskId);
|
const { repoId } = await resolveRepositoryForTask(c, input.taskId, input.repoId);
|
||||||
const h = getTask(c, c.state.organizationId, repoId, input.taskId);
|
const h = getTaskHandle(c, c.state.organizationId, repoId, input.taskId);
|
||||||
await h.kill({ reason: input.reason });
|
await h.kill({ reason: input.reason });
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import type {
|
||||||
FoundryOrganizationMember,
|
FoundryOrganizationMember,
|
||||||
FoundryUser,
|
FoundryUser,
|
||||||
UpdateFoundryOrganizationProfileInput,
|
UpdateFoundryOrganizationProfileInput,
|
||||||
|
WorkspaceModelId,
|
||||||
} from "@sandbox-agent/foundry-shared";
|
} from "@sandbox-agent/foundry-shared";
|
||||||
import { getActorRuntimeContext } from "../context.js";
|
import { getActorRuntimeContext } from "../context.js";
|
||||||
import { getOrCreateGithubData, getOrCreateOrganization, selfOrganization } from "../handles.js";
|
import { getOrCreateGithubData, getOrCreateOrganization, selfOrganization } from "../handles.js";
|
||||||
|
|
@ -98,7 +99,7 @@ const githubWebhookLogger = logger.child({
|
||||||
scope: "github-webhook",
|
scope: "github-webhook",
|
||||||
});
|
});
|
||||||
|
|
||||||
const PROFILE_ROW_ID = "profile";
|
const PROFILE_ROW_ID = 1;
|
||||||
|
|
||||||
function roundDurationMs(start: number): number {
|
function roundDurationMs(start: number): number {
|
||||||
return Math.round((performance.now() - start) * 100) / 100;
|
return Math.round((performance.now() - start) * 100) / 100;
|
||||||
|
|
@ -359,6 +360,7 @@ async function buildAppSnapshot(c: any, sessionId: string, allowOrganizationRepa
|
||||||
githubLogin: profile?.githubLogin ?? "",
|
githubLogin: profile?.githubLogin ?? "",
|
||||||
roleLabel: profile?.roleLabel ?? "GitHub user",
|
roleLabel: profile?.roleLabel ?? "GitHub user",
|
||||||
eligibleOrganizationIds,
|
eligibleOrganizationIds,
|
||||||
|
defaultModel: profile?.defaultModel ?? "claude-sonnet-4",
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
|
@ -685,7 +687,6 @@ async function buildOrganizationStateFromRow(c: any, row: any, startedAt: number
|
||||||
slug: row.slug,
|
slug: row.slug,
|
||||||
primaryDomain: row.primaryDomain,
|
primaryDomain: row.primaryDomain,
|
||||||
seatAccrualMode: "first_prompt",
|
seatAccrualMode: "first_prompt",
|
||||||
defaultModel: row.defaultModel,
|
|
||||||
autoImportRepos: row.autoImportRepos === 1,
|
autoImportRepos: row.autoImportRepos === 1,
|
||||||
},
|
},
|
||||||
github: {
|
github: {
|
||||||
|
|
@ -1078,6 +1079,15 @@ export const organizationAppActions = {
|
||||||
return await buildAppSnapshot(c, input.sessionId);
|
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(
|
async updateAppOrganizationProfile(
|
||||||
c: any,
|
c: any,
|
||||||
input: { sessionId: string; organizationId: string } & UpdateFoundryOrganizationProfileInput,
|
input: { sessionId: string; organizationId: string } & UpdateFoundryOrganizationProfileInput,
|
||||||
|
|
@ -1393,14 +1403,14 @@ export const organizationAppActions = {
|
||||||
"installation_event",
|
"installation_event",
|
||||||
);
|
);
|
||||||
if (body.action === "deleted") {
|
if (body.action === "deleted") {
|
||||||
await githubData.clearState({
|
await githubData.adminClearState({
|
||||||
connectedAccount: accountLogin,
|
connectedAccount: accountLogin,
|
||||||
installationStatus: "install_required",
|
installationStatus: "install_required",
|
||||||
installationId: null,
|
installationId: null,
|
||||||
label: "GitHub App installation removed",
|
label: "GitHub App installation removed",
|
||||||
});
|
});
|
||||||
} else if (body.action === "created") {
|
} else if (body.action === "created") {
|
||||||
await githubData.fullSync({
|
await githubData.adminFullSync({
|
||||||
connectedAccount: accountLogin,
|
connectedAccount: accountLogin,
|
||||||
installationStatus: "connected",
|
installationStatus: "connected",
|
||||||
installationId: body.installation?.id ?? null,
|
installationId: body.installation?.id ?? null,
|
||||||
|
|
@ -1409,14 +1419,14 @@ export const organizationAppActions = {
|
||||||
label: "Syncing GitHub data from installation webhook...",
|
label: "Syncing GitHub data from installation webhook...",
|
||||||
});
|
});
|
||||||
} else if (body.action === "suspend") {
|
} else if (body.action === "suspend") {
|
||||||
await githubData.clearState({
|
await githubData.adminClearState({
|
||||||
connectedAccount: accountLogin,
|
connectedAccount: accountLogin,
|
||||||
installationStatus: "reconnect_required",
|
installationStatus: "reconnect_required",
|
||||||
installationId: body.installation?.id ?? null,
|
installationId: body.installation?.id ?? null,
|
||||||
label: "GitHub App installation suspended",
|
label: "GitHub App installation suspended",
|
||||||
});
|
});
|
||||||
} else if (body.action === "unsuspend") {
|
} else if (body.action === "unsuspend") {
|
||||||
await githubData.fullSync({
|
await githubData.adminFullSync({
|
||||||
connectedAccount: accountLogin,
|
connectedAccount: accountLogin,
|
||||||
installationStatus: "connected",
|
installationStatus: "connected",
|
||||||
installationId: body.installation?.id ?? null,
|
installationId: body.installation?.id ?? null,
|
||||||
|
|
@ -1440,7 +1450,7 @@ export const organizationAppActions = {
|
||||||
},
|
},
|
||||||
"repository_membership_changed",
|
"repository_membership_changed",
|
||||||
);
|
);
|
||||||
await githubData.fullSync({
|
await githubData.adminFullSync({
|
||||||
connectedAccount: accountLogin,
|
connectedAccount: accountLogin,
|
||||||
installationStatus: "connected",
|
installationStatus: "connected",
|
||||||
installationId: body.installation?.id ?? null,
|
installationId: body.installation?.id ?? null,
|
||||||
|
|
@ -1578,7 +1588,6 @@ export const organizationAppActions = {
|
||||||
displayName: input.displayName,
|
displayName: input.displayName,
|
||||||
slug,
|
slug,
|
||||||
primaryDomain: existing?.primaryDomain ?? (input.kind === "personal" ? "personal" : `${slug}.github`),
|
primaryDomain: existing?.primaryDomain ?? (input.kind === "personal" ? "personal" : `${slug}.github`),
|
||||||
defaultModel: existing?.defaultModel ?? "claude-sonnet-4",
|
|
||||||
autoImportRepos: existing?.autoImportRepos ?? 1,
|
autoImportRepos: existing?.autoImportRepos ?? 1,
|
||||||
repoImportStatus: existing?.repoImportStatus ?? "not_started",
|
repoImportStatus: existing?.repoImportStatus ?? "not_started",
|
||||||
githubConnectedAccount: input.githubLogin,
|
githubConnectedAccount: input.githubLogin,
|
||||||
|
|
|
||||||
|
|
@ -10,24 +10,6 @@ const journal = {
|
||||||
tag: "0000_melted_viper",
|
tag: "0000_melted_viper",
|
||||||
breakpoints: true,
|
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;
|
} as const;
|
||||||
|
|
||||||
|
|
@ -73,7 +55,7 @@ CREATE TABLE \`organization_members\` (
|
||||||
);
|
);
|
||||||
--> statement-breakpoint
|
--> statement-breakpoint
|
||||||
CREATE TABLE \`organization_profile\` (
|
CREATE TABLE \`organization_profile\` (
|
||||||
\`id\` text PRIMARY KEY NOT NULL,
|
\`id\` integer PRIMARY KEY NOT NULL,
|
||||||
\`kind\` text NOT NULL,
|
\`kind\` text NOT NULL,
|
||||||
\`github_account_id\` text NOT NULL,
|
\`github_account_id\` text NOT NULL,
|
||||||
\`github_login\` text NOT NULL,
|
\`github_login\` text NOT NULL,
|
||||||
|
|
@ -81,7 +63,6 @@ CREATE TABLE \`organization_profile\` (
|
||||||
\`display_name\` text NOT NULL,
|
\`display_name\` text NOT NULL,
|
||||||
\`slug\` text NOT NULL,
|
\`slug\` text NOT NULL,
|
||||||
\`primary_domain\` text NOT NULL,
|
\`primary_domain\` text NOT NULL,
|
||||||
\`default_model\` text NOT NULL,
|
|
||||||
\`auto_import_repos\` integer NOT NULL,
|
\`auto_import_repos\` integer NOT NULL,
|
||||||
\`repo_import_status\` text NOT NULL,
|
\`repo_import_status\` text NOT NULL,
|
||||||
\`github_connected_account\` text NOT NULL,
|
\`github_connected_account\` text NOT NULL,
|
||||||
|
|
@ -102,7 +83,8 @@ CREATE TABLE \`organization_profile\` (
|
||||||
\`billing_renewal_at\` text,
|
\`billing_renewal_at\` text,
|
||||||
\`billing_payment_method_label\` text NOT NULL,
|
\`billing_payment_method_label\` text NOT NULL,
|
||||||
\`created_at\` integer 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
|
--> statement-breakpoint
|
||||||
CREATE TABLE \`repos\` (
|
CREATE TABLE \`repos\` (
|
||||||
|
|
@ -122,56 +104,6 @@ CREATE TABLE \`stripe_lookup\` (
|
||||||
\`organization_id\` text NOT NULL,
|
\`organization_id\` text NOT NULL,
|
||||||
\`updated_at\` integer 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,
|
} 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.
|
// SQLite is per organization actor instance, so no organizationId column needed.
|
||||||
|
|
||||||
|
|
@ -14,36 +15,10 @@ export const repos = sqliteTable("repos", {
|
||||||
updatedAt: integer("updated_at").notNull(),
|
updatedAt: integer("updated_at").notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
export const organizationProfile = sqliteTable(
|
||||||
* Coordinator index of TaskActor instances.
|
"organization_profile",
|
||||||
* Fast taskId → repoId lookup so the organization can route requests
|
{
|
||||||
* to the correct RepositoryActor without scanning all repos.
|
id: integer("id").primaryKey(),
|
||||||
*/
|
|
||||||
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(),
|
kind: text("kind").notNull(),
|
||||||
githubAccountId: text("github_account_id").notNull(),
|
githubAccountId: text("github_account_id").notNull(),
|
||||||
githubLogin: text("github_login").notNull(),
|
githubLogin: text("github_login").notNull(),
|
||||||
|
|
@ -51,7 +26,6 @@ export const organizationProfile = sqliteTable("organization_profile", {
|
||||||
displayName: text("display_name").notNull(),
|
displayName: text("display_name").notNull(),
|
||||||
slug: text("slug").notNull(),
|
slug: text("slug").notNull(),
|
||||||
primaryDomain: text("primary_domain").notNull(),
|
primaryDomain: text("primary_domain").notNull(),
|
||||||
defaultModel: text("default_model").notNull(),
|
|
||||||
autoImportRepos: integer("auto_import_repos").notNull(),
|
autoImportRepos: integer("auto_import_repos").notNull(),
|
||||||
repoImportStatus: text("repo_import_status").notNull(),
|
repoImportStatus: text("repo_import_status").notNull(),
|
||||||
githubConnectedAccount: text("github_connected_account").notNull(),
|
githubConnectedAccount: text("github_connected_account").notNull(),
|
||||||
|
|
@ -73,7 +47,9 @@ export const organizationProfile = sqliteTable("organization_profile", {
|
||||||
billingPaymentMethodLabel: text("billing_payment_method_label").notNull(),
|
billingPaymentMethodLabel: text("billing_payment_method_label").notNull(),
|
||||||
createdAt: integer("created_at").notNull(),
|
createdAt: integer("created_at").notNull(),
|
||||||
updatedAt: integer("updated_at").notNull(),
|
updatedAt: integer("updated_at").notNull(),
|
||||||
});
|
},
|
||||||
|
(table) => [check("organization_profile_singleton_id_check", sql`${table.id} = 1`)],
|
||||||
|
);
|
||||||
|
|
||||||
export const organizationMembers = sqliteTable("organization_members", {
|
export const organizationMembers = sqliteTable("organization_members", {
|
||||||
id: text("id").notNull().primaryKey(),
|
id: text("id").notNull().primaryKey(),
|
||||||
|
|
@ -133,6 +109,7 @@ export const authAccountIndex = sqliteTable("auth_account_index", {
|
||||||
updatedAt: integer("updated_at").notNull(),
|
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", {
|
export const authVerification = sqliteTable("auth_verification", {
|
||||||
id: text("id").notNull().primaryKey(),
|
id: text("id").notNull().primaryKey(),
|
||||||
identifier: text("identifier").notNull(),
|
identifier: text("identifier").notNull(),
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,21 @@
|
||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import { and, desc, eq, isNotNull, ne } from "drizzle-orm";
|
import { and, desc, eq, isNotNull, ne } from "drizzle-orm";
|
||||||
import { Loop } from "rivetkit/workflow";
|
import { Loop } from "rivetkit/workflow";
|
||||||
import type { AgentType, RepoOverview, SandboxProviderId, TaskRecord, TaskSummary } from "@sandbox-agent/foundry-shared";
|
import type {
|
||||||
import { getGithubData, getOrCreateHistory, getOrCreateTask, getTask, selfRepository } from "../handles.js";
|
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 { deriveFallbackTitle, resolveCreateFlowDecision } from "../../services/create-flow.js";
|
||||||
import { expectQueueResponse } from "../../services/queue.js";
|
import { expectQueueResponse } from "../../services/queue.js";
|
||||||
import { isActorNotFoundError, logActorWarning, resolveErrorMessage } from "../logging.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 {
|
interface CreateTaskCommand {
|
||||||
task: string;
|
task: string;
|
||||||
|
|
@ -29,10 +38,6 @@ interface ListTaskSummariesCommand {
|
||||||
includeArchived?: boolean;
|
includeArchived?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GetTaskEnrichedCommand {
|
|
||||||
taskId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GetPullRequestForBranchCommand {
|
interface GetPullRequestForBranchCommand {
|
||||||
branchName: string;
|
branchName: string;
|
||||||
}
|
}
|
||||||
|
|
@ -52,6 +57,61 @@ function isStaleTaskReferenceError(error: unknown): boolean {
|
||||||
return isActorNotFoundError(error) || message.startsWith("Task not found:");
|
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> {
|
async function persistRemoteUrl(c: any, remoteUrl: string): Promise<void> {
|
||||||
c.state.remoteUrl = remoteUrl;
|
c.state.remoteUrl = remoteUrl;
|
||||||
await c.db
|
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);
|
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) {
|
async function resolveGitHubRepository(c: any) {
|
||||||
const githubData = getGithubData(c, c.state.organizationId);
|
const githubData = getGithubData(c, c.state.organizationId);
|
||||||
return await githubData.getRepository({ repoId: c.state.repoId }).catch(() => null);
|
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(() => []);
|
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> {
|
async function createTaskMutation(c: any, cmd: CreateTaskCommand): Promise<TaskRecord> {
|
||||||
const organizationId = c.state.organizationId;
|
const organizationId = c.state.organizationId;
|
||||||
const repoId = c.state.repoId;
|
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 created = await taskHandle.initialize({ sandboxProviderId: cmd.sandboxProviderId });
|
||||||
|
|
||||||
const history = await getOrCreateHistory(c, organizationId, repoId);
|
try {
|
||||||
await history.append({
|
await upsertTaskSummary(c, await taskHandle.getTaskSummary({}));
|
||||||
|
await notifyOrganizationSnapshotChanged(c);
|
||||||
|
} catch (error) {
|
||||||
|
logActorWarning("repository", "failed seeding task summary after task creation", {
|
||||||
|
organizationId,
|
||||||
|
repoId,
|
||||||
|
taskId,
|
||||||
|
error: resolveErrorMessage(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const auditLog = await getOrCreateAuditLog(c, organizationId, repoId);
|
||||||
|
await auditLog.send(
|
||||||
|
"auditLog.command.append",
|
||||||
|
{
|
||||||
kind: "task.created",
|
kind: "task.created",
|
||||||
taskId,
|
taskId,
|
||||||
payload: {
|
payload: {
|
||||||
repoId,
|
repoId,
|
||||||
sandboxProviderId: cmd.sandboxProviderId,
|
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;
|
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 }> {
|
async function registerTaskBranchMutation(c: any, cmd: RegisterTaskBranchCommand): Promise<{ branchName: string; headSha: string }> {
|
||||||
const branchName = cmd.branchName.trim();
|
const branchName = cmd.branchName.trim();
|
||||||
if (!branchName) {
|
if (!branchName) {
|
||||||
|
|
@ -289,40 +402,23 @@ async function registerTaskBranchMutation(c: any, cmd: RegisterTaskBranchCommand
|
||||||
}
|
}
|
||||||
|
|
||||||
async function listTaskSummaries(c: any, includeArchived = false): Promise<TaskSummary[]> {
|
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 rows = await c.db.select().from(tasks).orderBy(desc(tasks.updatedAtMs)).all();
|
||||||
const records: TaskSummary[] = [];
|
return rows
|
||||||
|
.map((row) => ({
|
||||||
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,
|
organizationId: c.state.organizationId,
|
||||||
repoId: c.state.repoId,
|
repoId: c.state.repoId,
|
||||||
taskId: row.taskId,
|
taskId: row.taskId,
|
||||||
error: resolveErrorMessage(error),
|
branchName: row.branch ?? null,
|
||||||
});
|
title: row.title,
|
||||||
}
|
status: row.status,
|
||||||
|
updatedAt: row.updatedAtMs,
|
||||||
|
}))
|
||||||
|
.filter((row) => includeArchived || row.status !== "archived");
|
||||||
}
|
}
|
||||||
|
|
||||||
records.sort((a, b) => b.updatedAt - a.updatedAt);
|
async function listWorkspaceTaskSummaries(c: any): Promise<WorkspaceTaskSummary[]> {
|
||||||
return records;
|
const rows = await c.db.select().from(tasks).orderBy(desc(tasks.updatedAtMs)).all();
|
||||||
|
return rows.map(workspaceTaskSummaryFromRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortOverviewBranches(
|
function sortOverviewBranches(
|
||||||
|
|
@ -415,38 +511,12 @@ export const repositoryActions = {
|
||||||
return await listKnownTaskBranches(c);
|
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[]> {
|
async listTaskSummaries(c: any, cmd?: ListTaskSummariesCommand): Promise<TaskSummary[]> {
|
||||||
return await listTaskSummaries(c, cmd?.includeArchived === true);
|
return await listTaskSummaries(c, cmd?.includeArchived === true);
|
||||||
},
|
},
|
||||||
|
|
||||||
async getTaskEnriched(c: any, cmd: GetTaskEnrichedCommand): Promise<TaskRecord> {
|
async listWorkspaceTaskSummaries(c: any): Promise<WorkspaceTaskSummary[]> {
|
||||||
const row = await c.db.select({ taskId: taskIndex.taskId }).from(taskIndex).where(eq(taskIndex.taskId, cmd.taskId)).get();
|
return await listWorkspaceTaskSummaries(c);
|
||||||
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 getRepositoryMetadata(c: any): Promise<{ defaultBranch: string | null; fullName: string | null; remoteUrl: string }> {
|
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 prRows = await githubData.listPullRequestsForRepository({ repoId: c.state.repoId }).catch(() => []);
|
||||||
const prByBranch = new Map(prRows.map((row) => [row.headRefName, row]));
|
const prByBranch = new Map(prRows.map((row) => [row.headRefName, row]));
|
||||||
|
|
||||||
const taskRows = await c.db
|
const taskRows = await c.db.select().from(tasks).all();
|
||||||
.select({
|
|
||||||
taskId: taskIndex.taskId,
|
|
||||||
branchName: taskIndex.branchName,
|
|
||||||
updatedAt: taskIndex.updatedAt,
|
|
||||||
})
|
|
||||||
.from(taskIndex)
|
|
||||||
.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) {
|
for (const row of taskRows) {
|
||||||
if (!row.branchName) {
|
if (!row.branch) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
taskMetaByBranch.set(row.branch, {
|
||||||
const record = await getTask(c, c.state.organizationId, c.state.repoId, row.taskId).get();
|
|
||||||
taskMetaByBranch.set(row.branchName, {
|
|
||||||
taskId: row.taskId,
|
taskId: row.taskId,
|
||||||
title: record.title ?? null,
|
title: row.title ?? null,
|
||||||
status: record.status,
|
status: row.status,
|
||||||
updatedAt: record.updatedAt,
|
updatedAt: row.updatedAtMs,
|
||||||
|
pullRequest: parseJsonValue<WorkspacePullRequestSummary | null>(row.pullRequestJson, null),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
|
||||||
if (isStaleTaskReferenceError(error)) {
|
|
||||||
await deleteStaleTaskIndexRow(c, row.taskId);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const branchMap = new Map<string, { branchName: string; commitSha: string }>();
|
const branchMap = new Map<string, { branchName: string; commitSha: string }>();
|
||||||
|
|
@ -514,7 +573,7 @@ export const repositoryActions = {
|
||||||
const branches = sortOverviewBranches(
|
const branches = sortOverviewBranches(
|
||||||
[...branchMap.values()].map((branch) => {
|
[...branchMap.values()].map((branch) => {
|
||||||
const taskMeta = taskMetaByBranch.get(branch.branchName);
|
const taskMeta = taskMetaByBranch.get(branch.branchName);
|
||||||
const pr = prByBranch.get(branch.branchName);
|
const pr = taskMeta?.pullRequest ?? prByBranch.get(branch.branchName) ?? null;
|
||||||
return {
|
return {
|
||||||
branchName: branch.branchName,
|
branchName: branch.branchName,
|
||||||
commitSha: branch.commitSha,
|
commitSha: branch.commitSha,
|
||||||
|
|
@ -522,10 +581,10 @@ export const repositoryActions = {
|
||||||
taskTitle: taskMeta?.title ?? null,
|
taskTitle: taskMeta?.title ?? null,
|
||||||
taskStatus: taskMeta?.status ?? null,
|
taskStatus: taskMeta?.status ?? null,
|
||||||
prNumber: pr?.number ?? null,
|
prNumber: pr?.number ?? null,
|
||||||
prState: pr?.state ?? null,
|
prState: "state" in (pr ?? {}) ? pr.state : null,
|
||||||
prUrl: pr?.url ?? null,
|
prUrl: "url" in (pr ?? {}) ? pr.url : null,
|
||||||
ciStatus: null,
|
ciStatus: null,
|
||||||
reviewStatus: null,
|
reviewStatus: pr && "isDraft" in pr ? (pr.isDraft ? "draft" : "ready") : null,
|
||||||
reviewer: pr?.authorLogin ?? null,
|
reviewer: pr?.authorLogin ?? null,
|
||||||
updatedAt: Math.max(taskMeta?.updatedAt ?? 0, pr?.updatedAtMs ?? 0, now),
|
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();
|
const branchName = cmd.branchName?.trim();
|
||||||
if (!branchName) {
|
if (!branchName) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const githubData = getGithubData(c, c.state.organizationId);
|
const githubData = getGithubData(c, c.state.organizationId);
|
||||||
return await githubData.getPullRequestForBranch({
|
const rows = await githubData.listPullRequestsForRepository({
|
||||||
repoId: c.state.repoId,
|
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",
|
tag: "0000_useful_la_nuit",
|
||||||
breakpoints: true,
|
breakpoints: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
idx: 1,
|
|
||||||
when: 1778900000000,
|
|
||||||
tag: "0001_remove_local_git_state",
|
|
||||||
breakpoints: true,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
@ -23,21 +17,30 @@ export default {
|
||||||
journal,
|
journal,
|
||||||
migrations: {
|
migrations: {
|
||||||
m0000: `CREATE TABLE \`repo_meta\` (
|
m0000: `CREATE TABLE \`repo_meta\` (
|
||||||
\t\`id\` integer PRIMARY KEY NOT NULL,
|
\`id\` integer PRIMARY KEY NOT NULL,
|
||||||
\t\`remote_url\` text NOT NULL,
|
\`remote_url\` text NOT NULL,
|
||||||
\t\`updated_at\` integer NOT NULL
|
\`updated_at\` integer NOT NULL,
|
||||||
|
CONSTRAINT \`repo_meta_singleton_id_check\` CHECK(\`id\` = 1)
|
||||||
);
|
);
|
||||||
--> statement-breakpoint
|
--> statement-breakpoint
|
||||||
CREATE TABLE \`task_index\` (
|
CREATE TABLE \`task_index\` (
|
||||||
\t\`task_id\` text PRIMARY KEY NOT NULL,
|
\`task_id\` text PRIMARY KEY NOT NULL,
|
||||||
\t\`branch_name\` text,
|
\`branch_name\` text,
|
||||||
\t\`created_at\` integer NOT NULL,
|
\`created_at\` integer NOT NULL,
|
||||||
\t\`updated_at\` integer NOT NULL
|
\`updated_at\` integer NOT NULL
|
||||||
);
|
);
|
||||||
`,
|
|
||||||
m0001: `DROP TABLE IF EXISTS \`branches\`;
|
|
||||||
--> statement-breakpoint
|
--> 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,
|
} 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).
|
// SQLite is per repository actor instance (organizationId+repoId).
|
||||||
|
|
||||||
export const repoMeta = sqliteTable("repo_meta", {
|
export const repoMeta = sqliteTable(
|
||||||
|
"repo_meta",
|
||||||
|
{
|
||||||
id: integer("id").primaryKey(),
|
id: integer("id").primaryKey(),
|
||||||
remoteUrl: text("remote_url").notNull(),
|
remoteUrl: text("remote_url").notNull(),
|
||||||
updatedAt: integer("updated_at").notNull(),
|
updatedAt: integer("updated_at").notNull(),
|
||||||
});
|
},
|
||||||
|
(table) => [check("repo_meta_singleton_id_check", sql`${table.id} = 1`)],
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Coordinator index of TaskActor instances.
|
* Coordinator index of TaskActor instances.
|
||||||
* The repository actor is the coordinator for tasks. Each row maps a
|
* The repository actor is the coordinator for tasks. Each row maps a
|
||||||
* taskId to its branch name. Used for branch conflict checking and
|
* taskId to its immutable branch name. Used for branch conflict checking
|
||||||
* task-by-branch lookups. Rows are inserted at task creation and
|
* and task-by-branch lookups. Rows are inserted at task creation.
|
||||||
* updated on branch rename.
|
|
||||||
*/
|
*/
|
||||||
export const taskIndex = sqliteTable("task_index", {
|
export const taskIndex = sqliteTable("task_index", {
|
||||||
taskId: text("task_id").notNull().primaryKey(),
|
taskId: text("task_id").notNull().primaryKey(),
|
||||||
|
|
@ -21,3 +25,35 @@ export const taskIndex = sqliteTable("task_index", {
|
||||||
createdAt: integer("created_at").notNull(),
|
createdAt: integer("created_at").notNull(),
|
||||||
updatedAt: integer("updated_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,
|
`branch_name` text,
|
||||||
`title` text,
|
`title` text,
|
||||||
`task` text NOT NULL,
|
`task` text NOT NULL,
|
||||||
`provider_id` text NOT NULL,
|
`sandbox_provider_id` text NOT NULL,
|
||||||
`status` text NOT NULL,
|
`status` text NOT NULL,
|
||||||
`agent_type` text DEFAULT 'claude',
|
`agent_type` text DEFAULT 'claude',
|
||||||
`pr_submitted` integer DEFAULT 0,
|
`pr_submitted` integer DEFAULT 0,
|
||||||
|
|
@ -19,13 +19,17 @@ CREATE TABLE `task_runtime` (
|
||||||
`active_switch_target` text,
|
`active_switch_target` text,
|
||||||
`active_cwd` text,
|
`active_cwd` text,
|
||||||
`status_message` 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,
|
`updated_at` integer NOT NULL,
|
||||||
CONSTRAINT "task_runtime_singleton_id_check" CHECK("task_runtime"."id" = 1)
|
CONSTRAINT "task_runtime_singleton_id_check" CHECK("task_runtime"."id" = 1)
|
||||||
);
|
);
|
||||||
--> statement-breakpoint
|
--> statement-breakpoint
|
||||||
CREATE TABLE `task_sandboxes` (
|
CREATE TABLE `task_sandboxes` (
|
||||||
`sandbox_id` text PRIMARY KEY NOT NULL,
|
`sandbox_id` text PRIMARY KEY NOT NULL,
|
||||||
`provider_id` text NOT NULL,
|
`sandbox_provider_id` text NOT NULL,
|
||||||
`sandbox_actor_id` text,
|
`sandbox_actor_id` text,
|
||||||
`switch_target` text NOT NULL,
|
`switch_target` text NOT NULL,
|
||||||
`cwd` text,
|
`cwd` text,
|
||||||
|
|
@ -34,10 +38,15 @@ CREATE TABLE `task_sandboxes` (
|
||||||
`updated_at` integer NOT NULL
|
`updated_at` integer NOT NULL
|
||||||
);
|
);
|
||||||
--> statement-breakpoint
|
--> statement-breakpoint
|
||||||
CREATE TABLE `task_workbench_sessions` (
|
CREATE TABLE `task_workspace_sessions` (
|
||||||
`session_id` text PRIMARY KEY NOT NULL,
|
`session_id` text PRIMARY KEY NOT NULL,
|
||||||
|
`sandbox_session_id` text,
|
||||||
`session_name` text NOT NULL,
|
`session_name` text NOT NULL,
|
||||||
`model` 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,
|
`unread` integer DEFAULT 0 NOT NULL,
|
||||||
`draft_text` text DEFAULT '' NOT NULL,
|
`draft_text` text DEFAULT '' NOT NULL,
|
||||||
`draft_attachments_json` text DEFAULT '[]' NOT NULL,
|
`draft_attachments_json` text DEFAULT '[]' NOT NULL,
|
||||||
|
|
|
||||||
|
|
@ -221,8 +221,8 @@
|
||||||
"uniqueConstraints": {},
|
"uniqueConstraints": {},
|
||||||
"checkConstraints": {}
|
"checkConstraints": {}
|
||||||
},
|
},
|
||||||
"task_workbench_sessions": {
|
"task_workspace_sessions": {
|
||||||
"name": "task_workbench_sessions",
|
"name": "task_workspace_sessions",
|
||||||
"columns": {
|
"columns": {
|
||||||
"session_id": {
|
"session_id": {
|
||||||
"name": "session_id",
|
"name": "session_id",
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,6 @@ const journal = {
|
||||||
tag: "0000_charming_maestro",
|
tag: "0000_charming_maestro",
|
||||||
breakpoints: true,
|
breakpoints: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
idx: 1,
|
|
||||||
when: 1773810000000,
|
|
||||||
tag: "0001_sandbox_provider_columns",
|
|
||||||
breakpoints: true,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
@ -27,10 +21,8 @@ export default {
|
||||||
\`branch_name\` text,
|
\`branch_name\` text,
|
||||||
\`title\` text,
|
\`title\` text,
|
||||||
\`task\` text NOT NULL,
|
\`task\` text NOT NULL,
|
||||||
\`provider_id\` text NOT NULL,
|
\`sandbox_provider_id\` text NOT NULL,
|
||||||
\`status\` text NOT NULL,
|
\`status\` text NOT NULL,
|
||||||
\`agent_type\` text DEFAULT 'claude',
|
|
||||||
\`pr_submitted\` integer DEFAULT 0,
|
|
||||||
\`created_at\` integer NOT NULL,
|
\`created_at\` integer NOT NULL,
|
||||||
\`updated_at\` integer NOT NULL,
|
\`updated_at\` integer NOT NULL,
|
||||||
CONSTRAINT "task_singleton_id_check" CHECK("task"."id" = 1)
|
CONSTRAINT "task_singleton_id_check" CHECK("task"."id" = 1)
|
||||||
|
|
@ -39,17 +31,17 @@ export default {
|
||||||
CREATE TABLE \`task_runtime\` (
|
CREATE TABLE \`task_runtime\` (
|
||||||
\`id\` integer PRIMARY KEY NOT NULL,
|
\`id\` integer PRIMARY KEY NOT NULL,
|
||||||
\`active_sandbox_id\` text,
|
\`active_sandbox_id\` text,
|
||||||
\`active_session_id\` text,
|
|
||||||
\`active_switch_target\` text,
|
\`active_switch_target\` text,
|
||||||
\`active_cwd\` text,
|
\`active_cwd\` text,
|
||||||
\`status_message\` text,
|
\`git_state_json\` text,
|
||||||
|
\`git_state_updated_at\` integer,
|
||||||
\`updated_at\` integer NOT NULL,
|
\`updated_at\` integer NOT NULL,
|
||||||
CONSTRAINT "task_runtime_singleton_id_check" CHECK("task_runtime"."id" = 1)
|
CONSTRAINT "task_runtime_singleton_id_check" CHECK("task_runtime"."id" = 1)
|
||||||
);
|
);
|
||||||
--> statement-breakpoint
|
--> statement-breakpoint
|
||||||
CREATE TABLE \`task_sandboxes\` (
|
CREATE TABLE \`task_sandboxes\` (
|
||||||
\`sandbox_id\` text PRIMARY KEY NOT NULL,
|
\`sandbox_id\` text PRIMARY KEY NOT NULL,
|
||||||
\`provider_id\` text NOT NULL,
|
\`sandbox_provider_id\` text NOT NULL,
|
||||||
\`sandbox_actor_id\` text,
|
\`sandbox_actor_id\` text,
|
||||||
\`switch_target\` text NOT NULL,
|
\`switch_target\` text NOT NULL,
|
||||||
\`cwd\` text,
|
\`cwd\` text,
|
||||||
|
|
@ -58,24 +50,21 @@ CREATE TABLE \`task_sandboxes\` (
|
||||||
\`updated_at\` integer NOT NULL
|
\`updated_at\` integer NOT NULL
|
||||||
);
|
);
|
||||||
--> statement-breakpoint
|
--> statement-breakpoint
|
||||||
CREATE TABLE \`task_workbench_sessions\` (
|
CREATE TABLE \`task_workspace_sessions\` (
|
||||||
\`session_id\` text PRIMARY KEY NOT NULL,
|
\`session_id\` text PRIMARY KEY NOT NULL,
|
||||||
|
\`sandbox_session_id\` text,
|
||||||
\`session_name\` text NOT NULL,
|
\`session_name\` text NOT NULL,
|
||||||
\`model\` text NOT NULL,
|
\`model\` text NOT NULL,
|
||||||
\`unread\` integer DEFAULT 0 NOT NULL,
|
\`status\` text DEFAULT 'ready' NOT NULL,
|
||||||
\`draft_text\` text DEFAULT '' NOT NULL,
|
\`error_message\` text,
|
||||||
\`draft_attachments_json\` text DEFAULT '[]' NOT NULL,
|
\`transcript_json\` text DEFAULT '[]' NOT NULL,
|
||||||
\`draft_updated_at\` integer,
|
\`transcript_updated_at\` integer,
|
||||||
\`created\` integer DEFAULT 1 NOT NULL,
|
\`created\` integer DEFAULT 1 NOT NULL,
|
||||||
\`closed\` integer DEFAULT 0 NOT NULL,
|
\`closed\` integer DEFAULT 0 NOT NULL,
|
||||||
\`thinking_since_ms\` integer,
|
\`thinking_since_ms\` integer,
|
||||||
\`created_at\` integer NOT NULL,
|
\`created_at\` integer NOT NULL,
|
||||||
\`updated_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,
|
} as const,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,6 @@ export const task = sqliteTable(
|
||||||
task: text("task").notNull(),
|
task: text("task").notNull(),
|
||||||
sandboxProviderId: text("sandbox_provider_id").notNull(),
|
sandboxProviderId: text("sandbox_provider_id").notNull(),
|
||||||
status: text("status").notNull(),
|
status: text("status").notNull(),
|
||||||
agentType: text("agent_type").default("claude"),
|
|
||||||
prSubmitted: integer("pr_submitted").default(0),
|
|
||||||
createdAt: integer("created_at").notNull(),
|
createdAt: integer("created_at").notNull(),
|
||||||
updatedAt: integer("updated_at").notNull(),
|
updatedAt: integer("updated_at").notNull(),
|
||||||
},
|
},
|
||||||
|
|
@ -24,14 +22,10 @@ export const taskRuntime = sqliteTable(
|
||||||
{
|
{
|
||||||
id: integer("id").primaryKey(),
|
id: integer("id").primaryKey(),
|
||||||
activeSandboxId: text("active_sandbox_id"),
|
activeSandboxId: text("active_sandbox_id"),
|
||||||
activeSessionId: text("active_session_id"),
|
|
||||||
activeSwitchTarget: text("active_switch_target"),
|
activeSwitchTarget: text("active_switch_target"),
|
||||||
activeCwd: text("active_cwd"),
|
activeCwd: text("active_cwd"),
|
||||||
statusMessage: text("status_message"),
|
|
||||||
gitStateJson: text("git_state_json"),
|
gitStateJson: text("git_state_json"),
|
||||||
gitStateUpdatedAt: integer("git_state_updated_at"),
|
gitStateUpdatedAt: integer("git_state_updated_at"),
|
||||||
provisionStage: text("provision_stage"),
|
|
||||||
provisionStageUpdatedAt: integer("provision_stage_updated_at"),
|
|
||||||
updatedAt: integer("updated_at").notNull(),
|
updatedAt: integer("updated_at").notNull(),
|
||||||
},
|
},
|
||||||
(table) => [check("task_runtime_singleton_id_check", sql`${table.id} = 1`)],
|
(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
|
* The task actor is the coordinator for sessions. Each row holds session
|
||||||
* metadata, model, status, transcript, and draft state. Sessions are
|
* metadata, model, status, transcript, and draft state. Sessions are
|
||||||
* sub-entities of the task — no separate session actor in the DB.
|
* 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(),
|
sessionId: text("session_id").notNull().primaryKey(),
|
||||||
sandboxSessionId: text("sandbox_session_id"),
|
sandboxSessionId: text("sandbox_session_id"),
|
||||||
sessionName: text("session_name").notNull(),
|
sessionName: text("session_name").notNull(),
|
||||||
|
|
@ -68,11 +62,6 @@ export const taskWorkbenchSessions = sqliteTable("task_workbench_sessions", {
|
||||||
errorMessage: text("error_message"),
|
errorMessage: text("error_message"),
|
||||||
transcriptJson: text("transcript_json").notNull().default("[]"),
|
transcriptJson: text("transcript_json").notNull().default("[]"),
|
||||||
transcriptUpdatedAt: integer("transcript_updated_at"),
|
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),
|
created: integer("created").notNull().default(1),
|
||||||
closed: integer("closed").notNull().default(0),
|
closed: integer("closed").notNull().default(0),
|
||||||
thinkingSinceMs: integer("thinking_since_ms"),
|
thinkingSinceMs: integer("thinking_since_ms"),
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
import { actor, queue } from "rivetkit";
|
import { actor, queue } from "rivetkit";
|
||||||
import { workflow } from "rivetkit/workflow";
|
import { workflow } from "rivetkit/workflow";
|
||||||
import type {
|
import type {
|
||||||
AgentType,
|
|
||||||
TaskRecord,
|
TaskRecord,
|
||||||
TaskWorkbenchChangeModelInput,
|
TaskWorkspaceChangeModelInput,
|
||||||
TaskWorkbenchRenameInput,
|
TaskWorkspaceRenameInput,
|
||||||
TaskWorkbenchRenameSessionInput,
|
TaskWorkspaceRenameSessionInput,
|
||||||
TaskWorkbenchSetSessionUnreadInput,
|
TaskWorkspaceSetSessionUnreadInput,
|
||||||
TaskWorkbenchSendMessageInput,
|
TaskWorkspaceSendMessageInput,
|
||||||
TaskWorkbenchUpdateDraftInput,
|
TaskWorkspaceUpdateDraftInput,
|
||||||
SandboxProviderId,
|
SandboxProviderId,
|
||||||
} from "@sandbox-agent/foundry-shared";
|
} from "@sandbox-agent/foundry-shared";
|
||||||
import { expectQueueResponse } from "../../services/queue.js";
|
import { expectQueueResponse } from "../../services/queue.js";
|
||||||
|
|
@ -16,24 +15,23 @@ import { selfTask } from "../handles.js";
|
||||||
import { taskDb } from "./db/db.js";
|
import { taskDb } from "./db/db.js";
|
||||||
import { getCurrentRecord } from "./workflow/common.js";
|
import { getCurrentRecord } from "./workflow/common.js";
|
||||||
import {
|
import {
|
||||||
changeWorkbenchModel,
|
changeWorkspaceModel,
|
||||||
closeWorkbenchSession,
|
closeWorkspaceSession,
|
||||||
createWorkbenchSession,
|
createWorkspaceSession,
|
||||||
getSessionDetail,
|
getSessionDetail,
|
||||||
getTaskDetail,
|
getTaskDetail,
|
||||||
getTaskSummary,
|
getTaskSummary,
|
||||||
markWorkbenchUnread,
|
markWorkspaceUnread,
|
||||||
publishWorkbenchPr,
|
publishWorkspacePr,
|
||||||
renameWorkbenchBranch,
|
renameWorkspaceTask,
|
||||||
renameWorkbenchTask,
|
renameWorkspaceSession,
|
||||||
renameWorkbenchSession,
|
revertWorkspaceFile,
|
||||||
revertWorkbenchFile,
|
sendWorkspaceMessage,
|
||||||
sendWorkbenchMessage,
|
syncWorkspaceSessionStatus,
|
||||||
syncWorkbenchSessionStatus,
|
setWorkspaceSessionUnread,
|
||||||
setWorkbenchSessionUnread,
|
stopWorkspaceSession,
|
||||||
stopWorkbenchSession,
|
updateWorkspaceDraft,
|
||||||
updateWorkbenchDraft,
|
} from "./workspace.js";
|
||||||
} from "./workbench.js";
|
|
||||||
import { TASK_QUEUE_NAMES, taskWorkflowQueueName, runTaskWorkflow } from "./workflow/index.js";
|
import { TASK_QUEUE_NAMES, taskWorkflowQueueName, runTaskWorkflow } from "./workflow/index.js";
|
||||||
|
|
||||||
export interface TaskInput {
|
export interface TaskInput {
|
||||||
|
|
@ -45,10 +43,8 @@ export interface TaskInput {
|
||||||
title: string | null;
|
title: string | null;
|
||||||
task: string;
|
task: string;
|
||||||
sandboxProviderId: SandboxProviderId;
|
sandboxProviderId: SandboxProviderId;
|
||||||
agentType: AgentType | null;
|
|
||||||
explicitTitle: string | null;
|
explicitTitle: string | null;
|
||||||
explicitBranchName: string | null;
|
explicitBranchName: string | null;
|
||||||
initialPrompt: string | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InitializeCommand {
|
interface InitializeCommand {
|
||||||
|
|
@ -69,48 +65,57 @@ interface TaskStatusSyncCommand {
|
||||||
at: number;
|
at: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TaskWorkbenchValueCommand {
|
interface TaskWorkspaceValueCommand {
|
||||||
value: string;
|
value: string;
|
||||||
|
authSessionId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TaskWorkbenchSessionTitleCommand {
|
interface TaskWorkspaceSessionTitleCommand {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
authSessionId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TaskWorkbenchSessionUnreadCommand {
|
interface TaskWorkspaceSessionUnreadCommand {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
unread: boolean;
|
unread: boolean;
|
||||||
|
authSessionId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TaskWorkbenchUpdateDraftCommand {
|
interface TaskWorkspaceUpdateDraftCommand {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
text: string;
|
text: string;
|
||||||
attachments: Array<any>;
|
attachments: Array<any>;
|
||||||
|
authSessionId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TaskWorkbenchChangeModelCommand {
|
interface TaskWorkspaceChangeModelCommand {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
model: string;
|
model: string;
|
||||||
|
authSessionId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TaskWorkbenchSendMessageCommand {
|
interface TaskWorkspaceSendMessageCommand {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
text: string;
|
text: string;
|
||||||
attachments: Array<any>;
|
attachments: Array<any>;
|
||||||
|
authSessionId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TaskWorkbenchCreateSessionCommand {
|
interface TaskWorkspaceCreateSessionCommand {
|
||||||
model?: string;
|
model?: string;
|
||||||
|
authSessionId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TaskWorkbenchCreateSessionAndSendCommand {
|
interface TaskWorkspaceCreateSessionAndSendCommand {
|
||||||
model?: string;
|
model?: string;
|
||||||
text: string;
|
text: string;
|
||||||
|
authSessionId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TaskWorkbenchSessionCommand {
|
interface TaskWorkspaceSessionCommand {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
|
authSessionId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const task = actor({
|
export const task = actor({
|
||||||
|
|
@ -126,16 +131,6 @@ export const task = actor({
|
||||||
repoId: input.repoId,
|
repoId: input.repoId,
|
||||||
taskId: input.taskId,
|
taskId: input.taskId,
|
||||||
repoRemote: input.repoRemote,
|
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: {
|
actions: {
|
||||||
async initialize(c, cmd: InitializeCommand): Promise<TaskRecord> {
|
async initialize(c, cmd: InitializeCommand): Promise<TaskRecord> {
|
||||||
|
|
@ -220,19 +215,19 @@ export const task = actor({
|
||||||
return await getTaskSummary(c);
|
return await getTaskSummary(c);
|
||||||
},
|
},
|
||||||
|
|
||||||
async getTaskDetail(c) {
|
async getTaskDetail(c, input?: { authSessionId?: string }) {
|
||||||
return await getTaskDetail(c);
|
return await getTaskDetail(c, input?.authSessionId);
|
||||||
},
|
},
|
||||||
|
|
||||||
async getSessionDetail(c, input: { sessionId: string }) {
|
async getSessionDetail(c, input: { sessionId: string; authSessionId?: string }) {
|
||||||
return await getSessionDetail(c, input.sessionId);
|
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);
|
const self = selfTask(c);
|
||||||
await self.send(
|
await self.send(
|
||||||
taskWorkflowQueueName("task.command.workbench.mark_unread"),
|
taskWorkflowQueueName("task.command.workspace.mark_unread"),
|
||||||
{},
|
{ authSessionId: input?.authSessionId },
|
||||||
{
|
{
|
||||||
wait: true,
|
wait: true,
|
||||||
timeout: 10_000,
|
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);
|
const self = selfTask(c);
|
||||||
await self.send(taskWorkflowQueueName("task.command.workbench.rename_task"), { value: input.value } satisfies TaskWorkbenchValueCommand, {
|
await self.send(
|
||||||
|
taskWorkflowQueueName("task.command.workspace.rename_task"),
|
||||||
|
{ value: input.value, authSessionId: input.authSessionId } satisfies TaskWorkspaceValueCommand,
|
||||||
|
{
|
||||||
wait: true,
|
wait: true,
|
||||||
timeout: 20_000,
|
timeout: 20_000,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
async renameWorkbenchBranch(c, input: TaskWorkbenchRenameInput): Promise<void> {
|
async createWorkspaceSession(c, input?: { model?: string; authSessionId?: string }): Promise<{ sessionId: string }> {
|
||||||
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 }> {
|
|
||||||
const self = selfTask(c);
|
const self = selfTask(c);
|
||||||
const result = await self.send(
|
const result = await self.send(
|
||||||
taskWorkflowQueueName("task.command.workbench.create_session"),
|
taskWorkflowQueueName("task.command.workspace.create_session"),
|
||||||
{ ...(input?.model ? { model: input.model } : {}) } satisfies TaskWorkbenchCreateSessionCommand,
|
{
|
||||||
|
...(input?.model ? { model: input.model } : {}),
|
||||||
|
...(input?.authSessionId ? { authSessionId: input.authSessionId } : {}),
|
||||||
|
} satisfies TaskWorkspaceCreateSessionCommand,
|
||||||
{
|
{
|
||||||
wait: true,
|
wait: true,
|
||||||
timeout: 10_000,
|
timeout: 10_000,
|
||||||
|
|
@ -269,23 +264,23 @@ export const task = actor({
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fire-and-forget: creates a workbench session and sends the initial message.
|
* Fire-and-forget: creates a session and sends the initial message.
|
||||||
* Used by createWorkbenchTask so the caller doesn't block on session creation.
|
* 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);
|
const self = selfTask(c);
|
||||||
await self.send(
|
await self.send(
|
||||||
taskWorkflowQueueName("task.command.workbench.create_session_and_send"),
|
taskWorkflowQueueName("task.command.workspace.create_session_and_send"),
|
||||||
{ model: input.model, text: input.text } satisfies TaskWorkbenchCreateSessionAndSendCommand,
|
{ model: input.model, text: input.text, authSessionId: input.authSessionId } satisfies TaskWorkspaceCreateSessionAndSendCommand,
|
||||||
{ wait: false },
|
{ wait: false },
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
async renameWorkbenchSession(c, input: TaskWorkbenchRenameSessionInput): Promise<void> {
|
async renameWorkspaceSession(c, input: TaskWorkspaceRenameSessionInput): Promise<void> {
|
||||||
const self = selfTask(c);
|
const self = selfTask(c);
|
||||||
await self.send(
|
await self.send(
|
||||||
taskWorkflowQueueName("task.command.workbench.rename_session"),
|
taskWorkflowQueueName("task.command.workspace.rename_session"),
|
||||||
{ sessionId: input.sessionId, title: input.title } satisfies TaskWorkbenchSessionTitleCommand,
|
{ sessionId: input.sessionId, title: input.title, authSessionId: input.authSessionId } satisfies TaskWorkspaceSessionTitleCommand,
|
||||||
{
|
{
|
||||||
wait: true,
|
wait: true,
|
||||||
timeout: 10_000,
|
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);
|
const self = selfTask(c);
|
||||||
await self.send(
|
await self.send(
|
||||||
taskWorkflowQueueName("task.command.workbench.set_session_unread"),
|
taskWorkflowQueueName("task.command.workspace.set_session_unread"),
|
||||||
{ sessionId: input.sessionId, unread: input.unread } satisfies TaskWorkbenchSessionUnreadCommand,
|
{ sessionId: input.sessionId, unread: input.unread, authSessionId: input.authSessionId } satisfies TaskWorkspaceSessionUnreadCommand,
|
||||||
{
|
{
|
||||||
wait: true,
|
wait: true,
|
||||||
timeout: 10_000,
|
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);
|
const self = selfTask(c);
|
||||||
await self.send(
|
await self.send(
|
||||||
taskWorkflowQueueName("task.command.workbench.update_draft"),
|
taskWorkflowQueueName("task.command.workspace.update_draft"),
|
||||||
{
|
{
|
||||||
sessionId: input.sessionId,
|
sessionId: input.sessionId,
|
||||||
text: input.text,
|
text: input.text,
|
||||||
attachments: input.attachments,
|
attachments: input.attachments,
|
||||||
} satisfies TaskWorkbenchUpdateDraftCommand,
|
authSessionId: input.authSessionId,
|
||||||
|
} satisfies TaskWorkspaceUpdateDraftCommand,
|
||||||
{
|
{
|
||||||
wait: false,
|
wait: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
async changeWorkbenchModel(c, input: TaskWorkbenchChangeModelInput): Promise<void> {
|
async changeWorkspaceModel(c, input: TaskWorkspaceChangeModelInput): Promise<void> {
|
||||||
const self = selfTask(c);
|
const self = selfTask(c);
|
||||||
await self.send(
|
await self.send(
|
||||||
taskWorkflowQueueName("task.command.workbench.change_model"),
|
taskWorkflowQueueName("task.command.workspace.change_model"),
|
||||||
{ sessionId: input.sessionId, model: input.model } satisfies TaskWorkbenchChangeModelCommand,
|
{ sessionId: input.sessionId, model: input.model, authSessionId: input.authSessionId } satisfies TaskWorkspaceChangeModelCommand,
|
||||||
{
|
{
|
||||||
wait: true,
|
wait: true,
|
||||||
timeout: 10_000,
|
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);
|
const self = selfTask(c);
|
||||||
await self.send(
|
await self.send(
|
||||||
taskWorkflowQueueName("task.command.workbench.send_message"),
|
taskWorkflowQueueName("task.command.workspace.send_message"),
|
||||||
{
|
{
|
||||||
sessionId: input.sessionId,
|
sessionId: input.sessionId,
|
||||||
text: input.text,
|
text: input.text,
|
||||||
attachments: input.attachments,
|
attachments: input.attachments,
|
||||||
} satisfies TaskWorkbenchSendMessageCommand,
|
authSessionId: input.authSessionId,
|
||||||
|
} satisfies TaskWorkspaceSendMessageCommand,
|
||||||
{
|
{
|
||||||
wait: false,
|
wait: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
async stopWorkbenchSession(c, input: TaskSessionCommand): Promise<void> {
|
async stopWorkspaceSession(c, input: TaskSessionCommand): Promise<void> {
|
||||||
const self = selfTask(c);
|
const self = selfTask(c);
|
||||||
await self.send(taskWorkflowQueueName("task.command.workbench.stop_session"), { sessionId: input.sessionId } satisfies TaskWorkbenchSessionCommand, {
|
await self.send(
|
||||||
|
taskWorkflowQueueName("task.command.workspace.stop_session"),
|
||||||
|
{ sessionId: input.sessionId, authSessionId: input.authSessionId } satisfies TaskWorkspaceSessionCommand,
|
||||||
|
{
|
||||||
wait: false,
|
wait: false,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
async syncWorkbenchSessionStatus(c, input: TaskStatusSyncCommand): Promise<void> {
|
async syncWorkspaceSessionStatus(c, input: TaskStatusSyncCommand): Promise<void> {
|
||||||
const self = selfTask(c);
|
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,
|
wait: true,
|
||||||
timeout: 20_000,
|
timeout: 20_000,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async closeWorkbenchSession(c, input: TaskSessionCommand): Promise<void> {
|
async closeWorkspaceSession(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> {
|
|
||||||
const self = selfTask(c);
|
const self = selfTask(c);
|
||||||
await self.send(
|
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,
|
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);
|
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,
|
wait: false,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { getTaskSandbox } from "../../handles.js";
|
import { getTaskSandbox } from "../../handles.js";
|
||||||
import { logActorWarning, resolveErrorMessage } from "../../logging.js";
|
import { logActorWarning, resolveErrorMessage } from "../../logging.js";
|
||||||
import { task as taskTable, taskRuntime } from "../db/schema.js";
|
import { task as taskTable } from "../db/schema.js";
|
||||||
import { TASK_ROW_ID, appendHistory, getCurrentRecord, setTaskState } from "./common.js";
|
import { TASK_ROW_ID, appendAuditLog, getCurrentRecord, setTaskState } from "./common.js";
|
||||||
import { pushActiveBranchActivity } from "./push.js";
|
import { pushActiveBranchActivity } from "./push.js";
|
||||||
|
|
||||||
async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {
|
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> {
|
export async function handleAttachActivity(loopCtx: any, msg: any): Promise<void> {
|
||||||
const record = await getCurrentRecord(loopCtx);
|
const record = await getCurrentRecord(loopCtx);
|
||||||
let target = record.sandboxes.find((sandbox: any) => sandbox.sandboxId === record.activeSandboxId)?.switchTarget ?? "";
|
let target = record.sandboxes.find((sandbox: any) => sandbox.sandboxId === record.activeSandboxId)?.switchTarget ?? "";
|
||||||
|
const sessionId = msg.body?.sessionId ?? null;
|
||||||
|
|
||||||
if (record.activeSandboxId) {
|
if (record.activeSandboxId) {
|
||||||
try {
|
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,
|
target,
|
||||||
sessionId: record.activeSessionId,
|
sessionId,
|
||||||
});
|
});
|
||||||
|
|
||||||
await msg.complete({
|
await msg.complete({
|
||||||
target,
|
target,
|
||||||
sessionId: record.activeSessionId,
|
sessionId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -64,20 +65,17 @@ export async function handlePushActivity(loopCtx: any, msg: any): Promise<void>
|
||||||
await msg.complete({ ok: true });
|
await msg.complete({ ok: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleSimpleCommandActivity(loopCtx: any, msg: any, statusMessage: string, historyKind: string): Promise<void> {
|
export async function handleSimpleCommandActivity(loopCtx: any, msg: any, _statusMessage: string, historyKind: string): Promise<void> {
|
||||||
const db = loopCtx.db;
|
await appendAuditLog(loopCtx, historyKind, { reason: msg.body?.reason ?? null });
|
||||||
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 });
|
|
||||||
await msg.complete({ ok: true });
|
await msg.complete({ ok: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleArchiveActivity(loopCtx: any, msg: any): Promise<void> {
|
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);
|
const record = await getCurrentRecord(loopCtx);
|
||||||
|
|
||||||
if (record.activeSandboxId) {
|
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) => {
|
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", {
|
logActorWarning("task.commands", "failed to release sandbox during archive", {
|
||||||
organizationId: loopCtx.state.organizationId,
|
organizationId: loopCtx.state.organizationId,
|
||||||
|
|
@ -90,17 +88,15 @@ export async function handleArchiveActivity(loopCtx: any, msg: any): Promise<voi
|
||||||
}
|
}
|
||||||
|
|
||||||
const db = loopCtx.db;
|
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(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 appendAuditLog(loopCtx, "task.archive", { reason: msg.body?.reason ?? null });
|
||||||
|
|
||||||
await appendHistory(loopCtx, "task.archive", { reason: msg.body?.reason ?? null });
|
|
||||||
await msg.complete({ ok: true });
|
await msg.complete({ ok: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function killDestroySandboxActivity(loopCtx: any): Promise<void> {
|
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);
|
const record = await getCurrentRecord(loopCtx);
|
||||||
if (!record.activeSandboxId) {
|
if (!record.activeSandboxId) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -110,13 +106,11 @@ export async function killDestroySandboxActivity(loopCtx: any): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function killWriteDbActivity(loopCtx: any, msg: 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;
|
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(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 appendAuditLog(loopCtx, "task.kill", { reason: msg.body?.reason ?? null });
|
||||||
|
|
||||||
await appendHistory(loopCtx, "task.kill", { reason: msg.body?.reason ?? null });
|
|
||||||
await msg.complete({ ok: true });
|
await msg.complete({ ok: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import type { TaskRecord, TaskStatus } from "@sandbox-agent/foundry-shared";
|
import type { TaskRecord, TaskStatus } from "@sandbox-agent/foundry-shared";
|
||||||
import { task as taskTable, taskRuntime, taskSandboxes } from "../db/schema.js";
|
import { task as taskTable, taskRuntime, taskSandboxes } from "../db/schema.js";
|
||||||
import { historyKey } from "../../keys.js";
|
import { getOrCreateAuditLog } from "../../handles.js";
|
||||||
import { broadcastTaskUpdate } from "../workbench.js";
|
import { broadcastTaskUpdate } from "../workspace.js";
|
||||||
|
|
||||||
export const TASK_ROW_ID = 1;
|
export const TASK_ROW_ID = 1;
|
||||||
|
|
||||||
|
|
@ -56,33 +56,11 @@ export function buildAgentPrompt(task: string): string {
|
||||||
return task.trim();
|
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 now = Date.now();
|
||||||
const db = ctx.db;
|
const db = ctx.db;
|
||||||
await db.update(taskTable).set({ status, updatedAt: now }).where(eq(taskTable.id, TASK_ROW_ID)).run();
|
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);
|
await broadcastTaskUpdate(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -95,11 +73,7 @@ export async function getCurrentRecord(ctx: any): Promise<TaskRecord> {
|
||||||
task: taskTable.task,
|
task: taskTable.task,
|
||||||
sandboxProviderId: taskTable.sandboxProviderId,
|
sandboxProviderId: taskTable.sandboxProviderId,
|
||||||
status: taskTable.status,
|
status: taskTable.status,
|
||||||
statusMessage: taskRuntime.statusMessage,
|
|
||||||
activeSandboxId: taskRuntime.activeSandboxId,
|
activeSandboxId: taskRuntime.activeSandboxId,
|
||||||
activeSessionId: taskRuntime.activeSessionId,
|
|
||||||
agentType: taskTable.agentType,
|
|
||||||
prSubmitted: taskTable.prSubmitted,
|
|
||||||
createdAt: taskTable.createdAt,
|
createdAt: taskTable.createdAt,
|
||||||
updatedAt: taskTable.updatedAt,
|
updatedAt: taskTable.updatedAt,
|
||||||
})
|
})
|
||||||
|
|
@ -135,9 +109,7 @@ export async function getCurrentRecord(ctx: any): Promise<TaskRecord> {
|
||||||
task: row.task,
|
task: row.task,
|
||||||
sandboxProviderId: row.sandboxProviderId,
|
sandboxProviderId: row.sandboxProviderId,
|
||||||
status: row.status,
|
status: row.status,
|
||||||
statusMessage: row.statusMessage ?? null,
|
|
||||||
activeSandboxId: row.activeSandboxId ?? null,
|
activeSandboxId: row.activeSandboxId ?? null,
|
||||||
activeSessionId: row.activeSessionId ?? null,
|
|
||||||
sandboxes: sandboxes.map((sb) => ({
|
sandboxes: sandboxes.map((sb) => ({
|
||||||
sandboxId: sb.sandboxId,
|
sandboxId: sb.sandboxId,
|
||||||
sandboxProviderId: sb.sandboxProviderId,
|
sandboxProviderId: sb.sandboxProviderId,
|
||||||
|
|
@ -147,12 +119,7 @@ export async function getCurrentRecord(ctx: any): Promise<TaskRecord> {
|
||||||
createdAt: sb.createdAt,
|
createdAt: sb.createdAt,
|
||||||
updatedAt: sb.updatedAt,
|
updatedAt: sb.updatedAt,
|
||||||
})),
|
})),
|
||||||
agentType: row.agentType ?? null,
|
|
||||||
prSubmitted: Boolean(row.prSubmitted),
|
|
||||||
diffStat: null,
|
diffStat: null,
|
||||||
hasUnpushed: null,
|
|
||||||
conflictsWithMain: null,
|
|
||||||
parentBranch: null,
|
|
||||||
prUrl: null,
|
prUrl: null,
|
||||||
prAuthor: null,
|
prAuthor: null,
|
||||||
ciStatus: null,
|
ciStatus: null,
|
||||||
|
|
@ -163,17 +130,20 @@ export async function getCurrentRecord(ctx: any): Promise<TaskRecord> {
|
||||||
} as TaskRecord;
|
} as TaskRecord;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function appendHistory(ctx: any, kind: string, payload: Record<string, unknown>): Promise<void> {
|
export async function appendAuditLog(ctx: any, kind: string, payload: Record<string, unknown>): Promise<void> {
|
||||||
const client = ctx.client();
|
const auditLog = await getOrCreateAuditLog(ctx, ctx.state.organizationId, ctx.state.repoId);
|
||||||
const history = await client.history.getOrCreate(historyKey(ctx.state.organizationId, ctx.state.repoId), {
|
await auditLog.send(
|
||||||
createWithInput: { organizationId: ctx.state.organizationId, repoId: ctx.state.repoId },
|
"auditLog.command.append",
|
||||||
});
|
{
|
||||||
await history.append({
|
|
||||||
kind,
|
kind,
|
||||||
taskId: ctx.state.taskId,
|
taskId: ctx.state.taskId,
|
||||||
branchName: ctx.state.branchName,
|
branchName: ctx.state.branchName,
|
||||||
payload,
|
payload,
|
||||||
});
|
},
|
||||||
|
{
|
||||||
|
wait: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
await broadcastTaskUpdate(ctx);
|
await broadcastTaskUpdate(ctx);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,24 +14,23 @@ import {
|
||||||
} from "./commands.js";
|
} from "./commands.js";
|
||||||
import { TASK_QUEUE_NAMES } from "./queue.js";
|
import { TASK_QUEUE_NAMES } from "./queue.js";
|
||||||
import {
|
import {
|
||||||
changeWorkbenchModel,
|
changeWorkspaceModel,
|
||||||
closeWorkbenchSession,
|
closeWorkspaceSession,
|
||||||
createWorkbenchSession,
|
createWorkspaceSession,
|
||||||
ensureWorkbenchSession,
|
ensureWorkspaceSession,
|
||||||
refreshWorkbenchDerivedState,
|
refreshWorkspaceDerivedState,
|
||||||
refreshWorkbenchSessionTranscript,
|
refreshWorkspaceSessionTranscript,
|
||||||
markWorkbenchUnread,
|
markWorkspaceUnread,
|
||||||
publishWorkbenchPr,
|
publishWorkspacePr,
|
||||||
renameWorkbenchBranch,
|
renameWorkspaceTask,
|
||||||
renameWorkbenchTask,
|
renameWorkspaceSession,
|
||||||
renameWorkbenchSession,
|
revertWorkspaceFile,
|
||||||
revertWorkbenchFile,
|
sendWorkspaceMessage,
|
||||||
sendWorkbenchMessage,
|
setWorkspaceSessionUnread,
|
||||||
setWorkbenchSessionUnread,
|
stopWorkspaceSession,
|
||||||
stopWorkbenchSession,
|
syncWorkspaceSessionStatus,
|
||||||
syncWorkbenchSessionStatus,
|
updateWorkspaceDraft,
|
||||||
updateWorkbenchDraft,
|
} from "../workspace.js";
|
||||||
} from "../workbench.js";
|
|
||||||
|
|
||||||
export { TASK_QUEUE_NAMES, taskWorkflowQueueName } from "./queue.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));
|
await loopCtx.step("handle-get", async () => handleGetActivity(loopCtx, msg));
|
||||||
},
|
},
|
||||||
|
|
||||||
"task.command.workbench.mark_unread": async (loopCtx, msg) => {
|
"task.command.workspace.mark_unread": async (loopCtx, msg) => {
|
||||||
await loopCtx.step("workbench-mark-unread", async () => markWorkbenchUnread(loopCtx));
|
await loopCtx.step("workspace-mark-unread", async () => markWorkspaceUnread(loopCtx));
|
||||||
await msg.complete({ ok: true });
|
await msg.complete({ ok: true });
|
||||||
},
|
},
|
||||||
|
|
||||||
"task.command.workbench.rename_task": async (loopCtx, msg) => {
|
"task.command.workspace.rename_task": async (loopCtx, msg) => {
|
||||||
await loopCtx.step("workbench-rename-task", async () => renameWorkbenchTask(loopCtx, msg.body.value));
|
await loopCtx.step("workspace-rename-task", async () => renameWorkspaceTask(loopCtx, msg.body.value));
|
||||||
await msg.complete({ ok: true });
|
await msg.complete({ ok: true });
|
||||||
},
|
},
|
||||||
|
|
||||||
"task.command.workbench.rename_branch": async (loopCtx, msg) => {
|
"task.command.workspace.create_session": 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) => {
|
|
||||||
try {
|
try {
|
||||||
const created = await loopCtx.step({
|
const created = await loopCtx.step({
|
||||||
name: "workbench-create-session",
|
name: "workspace-create-session",
|
||||||
timeout: 5 * 60_000,
|
timeout: 5 * 60_000,
|
||||||
run: async () => createWorkbenchSession(loopCtx, msg.body?.model),
|
run: async () => createWorkspaceSession(loopCtx, msg.body?.model),
|
||||||
});
|
});
|
||||||
await msg.complete(created);
|
await msg.complete(created);
|
||||||
} catch (error) {
|
} 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 {
|
try {
|
||||||
const created = await loopCtx.step({
|
const created = await loopCtx.step({
|
||||||
name: "workbench-create-session-for-send",
|
name: "workspace-create-session-for-send",
|
||||||
timeout: 5 * 60_000,
|
timeout: 5 * 60_000,
|
||||||
run: async () => createWorkbenchSession(loopCtx, msg.body?.model),
|
run: async () => createWorkspaceSession(loopCtx, msg.body?.model),
|
||||||
});
|
});
|
||||||
await loopCtx.step({
|
await loopCtx.step({
|
||||||
name: "workbench-send-initial-message",
|
name: "workspace-send-initial-message",
|
||||||
timeout: 5 * 60_000,
|
timeout: 5 * 60_000,
|
||||||
run: async () => sendWorkbenchMessage(loopCtx, created.sessionId, msg.body.text, []),
|
run: async () => sendWorkspaceMessage(loopCtx, created.sessionId, msg.body.text, []),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logActorWarning("task.workflow", "create_session_and_send failed", {
|
logActorWarning("task.workflow", "create_session_and_send failed", {
|
||||||
|
|
@ -165,41 +155,41 @@ const commandHandlers: Record<TaskQueueName, WorkflowHandler> = {
|
||||||
await msg.complete({ ok: true });
|
await msg.complete({ ok: true });
|
||||||
},
|
},
|
||||||
|
|
||||||
"task.command.workbench.ensure_session": async (loopCtx, msg) => {
|
"task.command.workspace.ensure_session": async (loopCtx, msg) => {
|
||||||
await loopCtx.step({
|
await loopCtx.step({
|
||||||
name: "workbench-ensure-session",
|
name: "workspace-ensure-session",
|
||||||
timeout: 5 * 60_000,
|
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 });
|
await msg.complete({ ok: true });
|
||||||
},
|
},
|
||||||
|
|
||||||
"task.command.workbench.rename_session": async (loopCtx, msg) => {
|
"task.command.workspace.rename_session": async (loopCtx, msg) => {
|
||||||
await loopCtx.step("workbench-rename-session", async () => renameWorkbenchSession(loopCtx, msg.body.sessionId, msg.body.title));
|
await loopCtx.step("workspace-rename-session", async () => renameWorkspaceSession(loopCtx, msg.body.sessionId, msg.body.title));
|
||||||
await msg.complete({ ok: true });
|
await msg.complete({ ok: true });
|
||||||
},
|
},
|
||||||
|
|
||||||
"task.command.workbench.set_session_unread": async (loopCtx, msg) => {
|
"task.command.workspace.set_session_unread": async (loopCtx, msg) => {
|
||||||
await loopCtx.step("workbench-set-session-unread", async () => setWorkbenchSessionUnread(loopCtx, msg.body.sessionId, msg.body.unread));
|
await loopCtx.step("workspace-set-session-unread", async () => setWorkspaceSessionUnread(loopCtx, msg.body.sessionId, msg.body.unread));
|
||||||
await msg.complete({ ok: true });
|
await msg.complete({ ok: true });
|
||||||
},
|
},
|
||||||
|
|
||||||
"task.command.workbench.update_draft": async (loopCtx, msg) => {
|
"task.command.workspace.update_draft": async (loopCtx, msg) => {
|
||||||
await loopCtx.step("workbench-update-draft", async () => updateWorkbenchDraft(loopCtx, msg.body.sessionId, msg.body.text, msg.body.attachments));
|
await loopCtx.step("workspace-update-draft", async () => updateWorkspaceDraft(loopCtx, msg.body.sessionId, msg.body.text, msg.body.attachments));
|
||||||
await msg.complete({ ok: true });
|
await msg.complete({ ok: true });
|
||||||
},
|
},
|
||||||
|
|
||||||
"task.command.workbench.change_model": async (loopCtx, msg) => {
|
"task.command.workspace.change_model": async (loopCtx, msg) => {
|
||||||
await loopCtx.step("workbench-change-model", async () => changeWorkbenchModel(loopCtx, msg.body.sessionId, msg.body.model));
|
await loopCtx.step("workspace-change-model", async () => changeWorkspaceModel(loopCtx, msg.body.sessionId, msg.body.model));
|
||||||
await msg.complete({ ok: true });
|
await msg.complete({ ok: true });
|
||||||
},
|
},
|
||||||
|
|
||||||
"task.command.workbench.send_message": async (loopCtx, msg) => {
|
"task.command.workspace.send_message": async (loopCtx, msg) => {
|
||||||
try {
|
try {
|
||||||
await loopCtx.step({
|
await loopCtx.step({
|
||||||
name: "workbench-send-message",
|
name: "workspace-send-message",
|
||||||
timeout: 10 * 60_000,
|
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 });
|
await msg.complete({ ok: true });
|
||||||
} catch (error) {
|
} 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({
|
await loopCtx.step({
|
||||||
name: "workbench-stop-session",
|
name: "workspace-stop-session",
|
||||||
timeout: 5 * 60_000,
|
timeout: 5 * 60_000,
|
||||||
run: async () => stopWorkbenchSession(loopCtx, msg.body.sessionId),
|
run: async () => stopWorkspaceSession(loopCtx, msg.body.sessionId),
|
||||||
});
|
});
|
||||||
await msg.complete({ ok: true });
|
await msg.complete({ ok: true });
|
||||||
},
|
},
|
||||||
|
|
||||||
"task.command.workbench.sync_session_status": async (loopCtx, msg) => {
|
"task.command.workspace.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));
|
await loopCtx.step("workspace-sync-session-status", async () => syncWorkspaceSessionStatus(loopCtx, msg.body.sessionId, msg.body.status, msg.body.at));
|
||||||
await msg.complete({ ok: true });
|
await msg.complete({ ok: true });
|
||||||
},
|
},
|
||||||
|
|
||||||
"task.command.workbench.refresh_derived": async (loopCtx, msg) => {
|
"task.command.workspace.refresh_derived": async (loopCtx, msg) => {
|
||||||
await loopCtx.step({
|
await loopCtx.step({
|
||||||
name: "workbench-refresh-derived",
|
name: "workspace-refresh-derived",
|
||||||
timeout: 5 * 60_000,
|
timeout: 5 * 60_000,
|
||||||
run: async () => refreshWorkbenchDerivedState(loopCtx),
|
run: async () => refreshWorkspaceDerivedState(loopCtx),
|
||||||
});
|
});
|
||||||
await msg.complete({ ok: true });
|
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({
|
await loopCtx.step({
|
||||||
name: "workbench-refresh-session-transcript",
|
name: "workspace-refresh-session-transcript",
|
||||||
timeout: 60_000,
|
timeout: 60_000,
|
||||||
run: async () => refreshWorkbenchSessionTranscript(loopCtx, msg.body.sessionId),
|
run: async () => refreshWorkspaceSessionTranscript(loopCtx, msg.body.sessionId),
|
||||||
});
|
});
|
||||||
await msg.complete({ ok: true });
|
await msg.complete({ ok: true });
|
||||||
},
|
},
|
||||||
|
|
||||||
"task.command.workbench.close_session": async (loopCtx, msg) => {
|
"task.command.workspace.close_session": async (loopCtx, msg) => {
|
||||||
await loopCtx.step({
|
await loopCtx.step({
|
||||||
name: "workbench-close-session",
|
name: "workspace-close-session",
|
||||||
timeout: 5 * 60_000,
|
timeout: 5 * 60_000,
|
||||||
run: async () => closeWorkbenchSession(loopCtx, msg.body.sessionId),
|
run: async () => closeWorkspaceSession(loopCtx, msg.body.sessionId),
|
||||||
});
|
});
|
||||||
await msg.complete({ ok: true });
|
await msg.complete({ ok: true });
|
||||||
},
|
},
|
||||||
|
|
||||||
"task.command.workbench.publish_pr": async (loopCtx, msg) => {
|
"task.command.workspace.publish_pr": async (loopCtx, msg) => {
|
||||||
await loopCtx.step({
|
await loopCtx.step({
|
||||||
name: "workbench-publish-pr",
|
name: "workspace-publish-pr",
|
||||||
timeout: 10 * 60_000,
|
timeout: 10 * 60_000,
|
||||||
run: async () => publishWorkbenchPr(loopCtx),
|
run: async () => publishWorkspacePr(loopCtx),
|
||||||
});
|
});
|
||||||
await msg.complete({ ok: true });
|
await msg.complete({ ok: true });
|
||||||
},
|
},
|
||||||
|
|
||||||
"task.command.workbench.revert_file": async (loopCtx, msg) => {
|
"task.command.workspace.revert_file": async (loopCtx, msg) => {
|
||||||
await loopCtx.step({
|
await loopCtx.step({
|
||||||
name: "workbench-revert-file",
|
name: "workspace-revert-file",
|
||||||
timeout: 5 * 60_000,
|
timeout: 5 * 60_000,
|
||||||
run: async () => revertWorkbenchFile(loopCtx, msg.body.path),
|
run: async () => revertWorkspaceFile(loopCtx, msg.body.path),
|
||||||
});
|
});
|
||||||
await msg.complete({ ok: true });
|
await msg.complete({ ok: true });
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,18 @@
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { getActorRuntimeContext } from "../../context.js";
|
import { getActorRuntimeContext } from "../../context.js";
|
||||||
import { getOrCreateHistory, selfTask } from "../../handles.js";
|
import { selfTask } from "../../handles.js";
|
||||||
import { resolveErrorMessage } from "../../logging.js";
|
import { resolveErrorMessage } from "../../logging.js";
|
||||||
import { defaultSandboxProviderId } from "../../../sandbox-config.js";
|
import { defaultSandboxProviderId } from "../../../sandbox-config.js";
|
||||||
import { task as taskTable, taskRuntime } from "../db/schema.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";
|
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> {
|
export async function initBootstrapDbActivity(loopCtx: any, body: any): Promise<void> {
|
||||||
const { config } = getActorRuntimeContext();
|
const { config } = getActorRuntimeContext();
|
||||||
const sandboxProviderId = body?.sandboxProviderId ?? loopCtx.state.sandboxProviderId ?? defaultSandboxProviderId(config);
|
const sandboxProviderId = body?.sandboxProviderId ?? defaultSandboxProviderId(config);
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
await ensureTaskRuntimeCacheColumns(loopCtx.db);
|
|
||||||
|
|
||||||
await loopCtx.db
|
await loopCtx.db
|
||||||
.insert(taskTable)
|
.insert(taskTable)
|
||||||
.values({
|
.values({
|
||||||
|
|
@ -31,7 +22,6 @@ export async function initBootstrapDbActivity(loopCtx: any, body: any): Promise<
|
||||||
task: loopCtx.state.task,
|
task: loopCtx.state.task,
|
||||||
sandboxProviderId,
|
sandboxProviderId,
|
||||||
status: "init_bootstrap_db",
|
status: "init_bootstrap_db",
|
||||||
agentType: loopCtx.state.agentType ?? config.default_agent,
|
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
})
|
})
|
||||||
|
|
@ -43,7 +33,6 @@ export async function initBootstrapDbActivity(loopCtx: any, body: any): Promise<
|
||||||
task: loopCtx.state.task,
|
task: loopCtx.state.task,
|
||||||
sandboxProviderId,
|
sandboxProviderId,
|
||||||
status: "init_bootstrap_db",
|
status: "init_bootstrap_db",
|
||||||
agentType: loopCtx.state.agentType ?? config.default_agent,
|
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
@ -54,26 +43,18 @@ export async function initBootstrapDbActivity(loopCtx: any, body: any): Promise<
|
||||||
.values({
|
.values({
|
||||||
id: TASK_ROW_ID,
|
id: TASK_ROW_ID,
|
||||||
activeSandboxId: null,
|
activeSandboxId: null,
|
||||||
activeSessionId: null,
|
|
||||||
activeSwitchTarget: null,
|
activeSwitchTarget: null,
|
||||||
activeCwd: null,
|
activeCwd: null,
|
||||||
statusMessage: "provisioning",
|
|
||||||
gitStateJson: null,
|
gitStateJson: null,
|
||||||
gitStateUpdatedAt: null,
|
gitStateUpdatedAt: null,
|
||||||
provisionStage: "queued",
|
|
||||||
provisionStageUpdatedAt: now,
|
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
})
|
})
|
||||||
.onConflictDoUpdate({
|
.onConflictDoUpdate({
|
||||||
target: taskRuntime.id,
|
target: taskRuntime.id,
|
||||||
set: {
|
set: {
|
||||||
activeSandboxId: null,
|
activeSandboxId: null,
|
||||||
activeSessionId: null,
|
|
||||||
activeSwitchTarget: null,
|
activeSwitchTarget: null,
|
||||||
activeCwd: null,
|
activeCwd: null,
|
||||||
statusMessage: "provisioning",
|
|
||||||
provisionStage: "queued",
|
|
||||||
provisionStageUpdatedAt: now,
|
|
||||||
updatedAt: 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> {
|
export async function initEnqueueProvisionActivity(loopCtx: any, body: any): Promise<void> {
|
||||||
await setTaskState(loopCtx, "init_enqueue_provision", "provision queued");
|
await setTaskState(loopCtx, "init_enqueue_provision");
|
||||||
await loopCtx.db
|
|
||||||
.update(taskRuntime)
|
|
||||||
.set({
|
|
||||||
provisionStage: "queued",
|
|
||||||
provisionStageUpdatedAt: Date.now(),
|
|
||||||
updatedAt: Date.now(),
|
|
||||||
})
|
|
||||||
.where(eq(taskRuntime.id, TASK_ROW_ID))
|
|
||||||
.run();
|
|
||||||
|
|
||||||
const self = selfTask(loopCtx);
|
const self = selfTask(loopCtx);
|
||||||
try {
|
try {
|
||||||
|
|
@ -111,29 +83,20 @@ export async function initEnqueueProvisionActivity(loopCtx: any, body: any): Pro
|
||||||
export async function initCompleteActivity(loopCtx: any, body: any): Promise<void> {
|
export async function initCompleteActivity(loopCtx: any, body: any): Promise<void> {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const { config } = getActorRuntimeContext();
|
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
|
await loopCtx.db
|
||||||
.update(taskRuntime)
|
.update(taskRuntime)
|
||||||
.set({
|
.set({
|
||||||
statusMessage: "ready",
|
|
||||||
provisionStage: "ready",
|
|
||||||
provisionStageUpdatedAt: now,
|
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
})
|
})
|
||||||
.where(eq(taskRuntime.id, TASK_ROW_ID))
|
.where(eq(taskRuntime.id, TASK_ROW_ID))
|
||||||
.run();
|
.run();
|
||||||
|
|
||||||
const history = await getOrCreateHistory(loopCtx, loopCtx.state.organizationId, loopCtx.state.repoId);
|
await appendAuditLog(loopCtx, "task.initialized", {
|
||||||
await history.append({
|
|
||||||
kind: "task.initialized",
|
|
||||||
taskId: loopCtx.state.taskId,
|
|
||||||
branchName: loopCtx.state.branchName,
|
|
||||||
payload: { sandboxProviderId },
|
payload: { sandboxProviderId },
|
||||||
});
|
});
|
||||||
|
|
||||||
loopCtx.state.initialized = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function initFailedActivity(loopCtx: any, error: unknown): Promise<void> {
|
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 detail = resolveErrorDetail(error);
|
||||||
const messages = collectErrorMessages(error);
|
const messages = collectErrorMessages(error);
|
||||||
const { config } = getActorRuntimeContext();
|
const { config } = getActorRuntimeContext();
|
||||||
const sandboxProviderId = loopCtx.state.sandboxProviderId ?? defaultSandboxProviderId(config);
|
const sandboxProviderId = defaultSandboxProviderId(config);
|
||||||
|
|
||||||
await loopCtx.db
|
await loopCtx.db
|
||||||
.insert(taskTable)
|
.insert(taskTable)
|
||||||
|
|
@ -152,7 +115,6 @@ export async function initFailedActivity(loopCtx: any, error: unknown): Promise<
|
||||||
task: loopCtx.state.task,
|
task: loopCtx.state.task,
|
||||||
sandboxProviderId,
|
sandboxProviderId,
|
||||||
status: "error",
|
status: "error",
|
||||||
agentType: loopCtx.state.agentType ?? config.default_agent,
|
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
})
|
})
|
||||||
|
|
@ -164,7 +126,6 @@ export async function initFailedActivity(loopCtx: any, error: unknown): Promise<
|
||||||
task: loopCtx.state.task,
|
task: loopCtx.state.task,
|
||||||
sandboxProviderId,
|
sandboxProviderId,
|
||||||
status: "error",
|
status: "error",
|
||||||
agentType: loopCtx.state.agentType ?? config.default_agent,
|
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
@ -175,30 +136,22 @@ export async function initFailedActivity(loopCtx: any, error: unknown): Promise<
|
||||||
.values({
|
.values({
|
||||||
id: TASK_ROW_ID,
|
id: TASK_ROW_ID,
|
||||||
activeSandboxId: null,
|
activeSandboxId: null,
|
||||||
activeSessionId: null,
|
|
||||||
activeSwitchTarget: null,
|
activeSwitchTarget: null,
|
||||||
activeCwd: null,
|
activeCwd: null,
|
||||||
statusMessage: detail,
|
|
||||||
provisionStage: "error",
|
|
||||||
provisionStageUpdatedAt: now,
|
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
})
|
})
|
||||||
.onConflictDoUpdate({
|
.onConflictDoUpdate({
|
||||||
target: taskRuntime.id,
|
target: taskRuntime.id,
|
||||||
set: {
|
set: {
|
||||||
activeSandboxId: null,
|
activeSandboxId: null,
|
||||||
activeSessionId: null,
|
|
||||||
activeSwitchTarget: null,
|
activeSwitchTarget: null,
|
||||||
activeCwd: null,
|
activeCwd: null,
|
||||||
statusMessage: detail,
|
|
||||||
provisionStage: "error",
|
|
||||||
provisionStageUpdatedAt: now,
|
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
|
|
||||||
await appendHistory(loopCtx, "task.error", {
|
await appendAuditLog(loopCtx, "task.error", {
|
||||||
detail,
|
detail,
|
||||||
messages,
|
messages,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { getTaskSandbox } from "../../handles.js";
|
import { getTaskSandbox } from "../../handles.js";
|
||||||
import { resolveOrganizationGithubAuth } from "../../../services/github-auth.js";
|
import { resolveOrganizationGithubAuth } from "../../../services/github-auth.js";
|
||||||
import { taskRuntime, taskSandboxes } from "../db/schema.js";
|
import { taskSandboxes } from "../db/schema.js";
|
||||||
import { TASK_ROW_ID, appendHistory, getCurrentRecord } from "./common.js";
|
import { appendAuditLog, getCurrentRecord } from "./common.js";
|
||||||
|
|
||||||
export interface PushActiveBranchOptions {
|
export interface PushActiveBranchOptions {
|
||||||
reason?: string | null;
|
reason?: string | null;
|
||||||
|
|
@ -29,12 +29,6 @@ export async function pushActiveBranchActivity(loopCtx: any, options: PushActive
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = Date.now();
|
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
|
await loopCtx.db
|
||||||
.update(taskSandboxes)
|
.update(taskSandboxes)
|
||||||
.set({ statusMessage: `pushing branch ${branchName}`, updatedAt: now })
|
.set({ statusMessage: `pushing branch ${branchName}`, updatedAt: now })
|
||||||
|
|
@ -69,19 +63,13 @@ export async function pushActiveBranchActivity(loopCtx: any, options: PushActive
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedAt = Date.now();
|
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
|
await loopCtx.db
|
||||||
.update(taskSandboxes)
|
.update(taskSandboxes)
|
||||||
.set({ statusMessage: `push complete for ${branchName}`, updatedAt })
|
.set({ statusMessage: `push complete for ${branchName}`, updatedAt })
|
||||||
.where(eq(taskSandboxes.sandboxId, activeSandboxId))
|
.where(eq(taskSandboxes.sandboxId, activeSandboxId))
|
||||||
.run();
|
.run();
|
||||||
|
|
||||||
await appendHistory(loopCtx, options.historyKind ?? "task.push", {
|
await appendAuditLog(loopCtx, options.historyKind ?? "task.push", {
|
||||||
reason: options.reason ?? null,
|
reason: options.reason ?? null,
|
||||||
branchName,
|
branchName,
|
||||||
sandboxId: activeSandboxId,
|
sandboxId: activeSandboxId,
|
||||||
|
|
|
||||||
|
|
@ -9,24 +9,23 @@ export const TASK_QUEUE_NAMES = [
|
||||||
"task.command.archive",
|
"task.command.archive",
|
||||||
"task.command.kill",
|
"task.command.kill",
|
||||||
"task.command.get",
|
"task.command.get",
|
||||||
"task.command.workbench.mark_unread",
|
"task.command.workspace.mark_unread",
|
||||||
"task.command.workbench.rename_task",
|
"task.command.workspace.rename_task",
|
||||||
"task.command.workbench.rename_branch",
|
"task.command.workspace.create_session",
|
||||||
"task.command.workbench.create_session",
|
"task.command.workspace.create_session_and_send",
|
||||||
"task.command.workbench.create_session_and_send",
|
"task.command.workspace.ensure_session",
|
||||||
"task.command.workbench.ensure_session",
|
"task.command.workspace.rename_session",
|
||||||
"task.command.workbench.rename_session",
|
"task.command.workspace.set_session_unread",
|
||||||
"task.command.workbench.set_session_unread",
|
"task.command.workspace.update_draft",
|
||||||
"task.command.workbench.update_draft",
|
"task.command.workspace.change_model",
|
||||||
"task.command.workbench.change_model",
|
"task.command.workspace.send_message",
|
||||||
"task.command.workbench.send_message",
|
"task.command.workspace.stop_session",
|
||||||
"task.command.workbench.stop_session",
|
"task.command.workspace.sync_session_status",
|
||||||
"task.command.workbench.sync_session_status",
|
"task.command.workspace.refresh_derived",
|
||||||
"task.command.workbench.refresh_derived",
|
"task.command.workspace.refresh_session_transcript",
|
||||||
"task.command.workbench.refresh_session_transcript",
|
"task.command.workspace.close_session",
|
||||||
"task.command.workbench.close_session",
|
"task.command.workspace.publish_pr",
|
||||||
"task.command.workbench.publish_pr",
|
"task.command.workspace.revert_file",
|
||||||
"task.command.workbench.revert_file",
|
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export function taskWorkflowQueueName(name: string): string {
|
export function taskWorkflowQueueName(name: string): string {
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,12 @@ import { randomUUID } from "node:crypto";
|
||||||
import { basename, dirname } from "node:path";
|
import { basename, dirname } from "node:path";
|
||||||
import { asc, eq } from "drizzle-orm";
|
import { asc, eq } from "drizzle-orm";
|
||||||
import { getActorRuntimeContext } from "../context.js";
|
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 { SANDBOX_REPO_CWD } from "../sandbox/index.js";
|
||||||
import { resolveSandboxProviderId } from "../../sandbox-config.js";
|
import { resolveSandboxProviderId } from "../../sandbox-config.js";
|
||||||
import { resolveOrganizationGithubAuth } from "../../services/github-auth.js";
|
import { resolveOrganizationGithubAuth } from "../../services/github-auth.js";
|
||||||
import { githubRepoFullNameFromRemote } from "../../services/repo.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";
|
import { getCurrentRecord } from "./workflow/common.js";
|
||||||
|
|
||||||
function emptyGitState() {
|
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) {
|
function defaultModelForAgent(agentType: string | null | undefined) {
|
||||||
return agentType === "codex" ? "gpt-5.3-codex" : "claude-sonnet-4";
|
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>> {
|
async function listSessionMetaRows(c: any, options?: { includeClosed?: boolean }): Promise<Array<any>> {
|
||||||
await ensureWorkbenchSessionTable(c);
|
const rows = await c.db.select().from(taskWorkspaceSessions).orderBy(asc(taskWorkspaceSessions.createdAt)).all();
|
||||||
const rows = await c.db.select().from(taskWorkbenchSessions).orderBy(asc(taskWorkbenchSessions.createdAt)).all();
|
|
||||||
const mapped = rows.map((row: any) => ({
|
const mapped = rows.map((row: any) => ({
|
||||||
...row,
|
...row,
|
||||||
id: row.sessionId,
|
id: row.sessionId,
|
||||||
|
|
@ -199,8 +162,7 @@ async function nextSessionName(c: any): Promise<string> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readSessionMeta(c: any, sessionId: string): Promise<any | null> {
|
async function readSessionMeta(c: any, sessionId: string): Promise<any | null> {
|
||||||
await ensureWorkbenchSessionTable(c);
|
const row = await c.db.select().from(taskWorkspaceSessions).where(eq(taskWorkspaceSessions.sessionId, sessionId)).get();
|
||||||
const row = await c.db.select().from(taskWorkbenchSessions).where(eq(taskWorkbenchSessions.sessionId, sessionId)).get();
|
|
||||||
|
|
||||||
if (!row) {
|
if (!row) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -236,7 +198,6 @@ async function ensureSessionMeta(
|
||||||
errorMessage?: string | null;
|
errorMessage?: string | null;
|
||||||
},
|
},
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
await ensureWorkbenchSessionTable(c);
|
|
||||||
const existing = await readSessionMeta(c, params.sessionId);
|
const existing = await readSessionMeta(c, params.sessionId);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
return existing;
|
return existing;
|
||||||
|
|
@ -248,7 +209,7 @@ async function ensureSessionMeta(
|
||||||
const unread = params.unread ?? false;
|
const unread = params.unread ?? false;
|
||||||
|
|
||||||
await c.db
|
await c.db
|
||||||
.insert(taskWorkbenchSessions)
|
.insert(taskWorkspaceSessions)
|
||||||
.values({
|
.values({
|
||||||
sessionId: params.sessionId,
|
sessionId: params.sessionId,
|
||||||
sandboxSessionId: params.sandboxSessionId ?? null,
|
sandboxSessionId: params.sandboxSessionId ?? null,
|
||||||
|
|
@ -276,19 +237,18 @@ async function ensureSessionMeta(
|
||||||
async function updateSessionMeta(c: any, sessionId: string, values: Record<string, unknown>): Promise<any> {
|
async function updateSessionMeta(c: any, sessionId: string, values: Record<string, unknown>): Promise<any> {
|
||||||
await ensureSessionMeta(c, { sessionId });
|
await ensureSessionMeta(c, { sessionId });
|
||||||
await c.db
|
await c.db
|
||||||
.update(taskWorkbenchSessions)
|
.update(taskWorkspaceSessions)
|
||||||
.set({
|
.set({
|
||||||
...values,
|
...values,
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
})
|
})
|
||||||
.where(eq(taskWorkbenchSessions.sessionId, sessionId))
|
.where(eq(taskWorkspaceSessions.sessionId, sessionId))
|
||||||
.run();
|
.run();
|
||||||
return await readSessionMeta(c, sessionId);
|
return await readSessionMeta(c, sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readSessionMetaBySandboxSessionId(c: any, sandboxSessionId: string): Promise<any | null> {
|
async function readSessionMetaBySandboxSessionId(c: any, sandboxSessionId: string): Promise<any | null> {
|
||||||
await ensureWorkbenchSessionTable(c);
|
const row = await c.db.select().from(taskWorkspaceSessions).where(eq(taskWorkspaceSessions.sandboxSessionId, sandboxSessionId)).get();
|
||||||
const row = await c.db.select().from(taskWorkbenchSessions).where(eq(taskWorkbenchSessions.sandboxSessionId, sandboxSessionId)).get();
|
|
||||||
if (!row) {
|
if (!row) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -298,17 +258,17 @@ async function readSessionMetaBySandboxSessionId(c: any, sandboxSessionId: strin
|
||||||
async function requireReadySessionMeta(c: any, sessionId: string): Promise<any> {
|
async function requireReadySessionMeta(c: any, sessionId: string): Promise<any> {
|
||||||
const meta = await readSessionMeta(c, sessionId);
|
const meta = await readSessionMeta(c, sessionId);
|
||||||
if (!meta) {
|
if (!meta) {
|
||||||
throw new Error(`Unknown workbench session: ${sessionId}`);
|
throw new Error(`Unknown workspace session: ${sessionId}`);
|
||||||
}
|
}
|
||||||
if (meta.status !== "ready" || !meta.sandboxSessionId) {
|
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;
|
return meta;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function requireSendableSessionMeta(meta: any, sessionId: string): any {
|
export function requireSendableSessionMeta(meta: any, sessionId: string): any {
|
||||||
if (!meta) {
|
if (!meta) {
|
||||||
throw new Error(`Unknown workbench session: ${sessionId}`);
|
throw new Error(`Unknown workspace session: ${sessionId}`);
|
||||||
}
|
}
|
||||||
if (meta.status !== "ready" || !meta.sandboxSessionId) {
|
if (meta.status !== "ready" || !meta.sandboxSessionId) {
|
||||||
throw new Error(`Session is not ready (status: ${meta.status}). Wait for session provisioning to complete.`);
|
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)
|
* 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`
|
* 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;
|
let sandboxRepoPrepared = false;
|
||||||
|
|
||||||
|
|
@ -452,7 +412,7 @@ async function executeInSandbox(
|
||||||
label: string;
|
label: string;
|
||||||
},
|
},
|
||||||
): Promise<{ exitCode: number; result: string }> {
|
): Promise<{ exitCode: number; result: string }> {
|
||||||
const record = await ensureWorkbenchSeeded(c);
|
const record = await ensureWorkspaceSeeded(c);
|
||||||
const runtime = await getTaskSandboxRuntime(c, record);
|
const runtime = await getTaskSandboxRuntime(c, record);
|
||||||
await ensureSandboxRepo(c, runtime.sandbox, record);
|
await ensureSandboxRepo(c, runtime.sandbox, record);
|
||||||
const response = await runtime.sandbox.runProcess({
|
const response = await runtime.sandbox.runProcess({
|
||||||
|
|
@ -555,7 +515,7 @@ function buildFileTree(paths: string[]): Array<any> {
|
||||||
return sortNodes(root.children.values());
|
return sortNodes(root.children.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
async function collectWorkbenchGitState(c: any, record: any) {
|
async function collectWorkspaceGitState(c: any, record: any) {
|
||||||
const activeSandboxId = record.activeSandboxId;
|
const activeSandboxId = record.activeSandboxId;
|
||||||
const activeSandbox = activeSandboxId != null ? ((record.sandboxes ?? []).find((candidate: any) => candidate.sandboxId === activeSandboxId) ?? null) : null;
|
const activeSandbox = activeSandboxId != null ? ((record.sandboxes ?? []).find((candidate: any) => candidate.sandboxId === activeSandboxId) ?? null) : null;
|
||||||
const cwd = activeSandbox?.cwd ?? record.sandboxes?.[0]?.cwd ?? 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 }> {
|
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
|
const row = await c.db
|
||||||
.select({
|
.select({
|
||||||
gitStateJson: taskRuntime.gitStateJson,
|
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> {
|
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();
|
const now = Date.now();
|
||||||
await c.db
|
await c.db
|
||||||
.update(taskRuntime)
|
.update(taskRuntime)
|
||||||
|
|
@ -687,19 +645,19 @@ async function writeSessionTranscript(c: any, sessionId: string, transcript: Arr
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function enqueueWorkbenchRefresh(
|
async function enqueueWorkspaceRefresh(
|
||||||
c: any,
|
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>,
|
body: Record<string, unknown>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const self = selfTask(c);
|
const self = selfTask(c);
|
||||||
await self.send(command, body, { wait: false });
|
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);
|
const self = selfTask(c);
|
||||||
await self.send(
|
await self.send(
|
||||||
"task.command.workbench.ensure_session",
|
"task.command.workspace.ensure_session",
|
||||||
{
|
{
|
||||||
sessionId,
|
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";
|
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);
|
const gitState = await readCachedGitState(c);
|
||||||
if (record.activeSandboxId && !gitState.updatedAt) {
|
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) {
|
for (const session of sessions) {
|
||||||
if (session.closed || session.status !== "ready" || !session.sandboxSessionId || session.transcriptUpdatedAt) {
|
if (session.closed || session.status !== "ready" || !session.sandboxSessionId || session.transcriptUpdatedAt) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
await enqueueWorkbenchRefresh(c, "task.command.workbench.refresh_session_transcript", {
|
await enqueueWorkspaceRefresh(c, "task.command.workspace.refresh_session_transcript", {
|
||||||
sessionId: session.sandboxSessionId,
|
sessionId: session.sandboxSessionId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -756,8 +714,7 @@ async function readPullRequestSummary(c: any, branchName: string | null) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function ensureWorkbenchSeeded(c: any): Promise<any> {
|
export async function ensureWorkspaceSeeded(c: any): Promise<any> {
|
||||||
await ensureTaskRuntimeCacheColumns(c);
|
|
||||||
const record = await getCurrentRecord({ db: c.db, state: c.state });
|
const record = await getCurrentRecord({ db: c.db, state: c.state });
|
||||||
if (record.activeSessionId) {
|
if (record.activeSessionId) {
|
||||||
await ensureSessionMeta(c, {
|
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.
|
* this to the parent organization actor so organization sidebar reads stay local.
|
||||||
*/
|
*/
|
||||||
export async function buildTaskSummary(c: any): Promise<any> {
|
export async function buildTaskSummary(c: any): Promise<any> {
|
||||||
const record = await ensureWorkbenchSeeded(c);
|
const record = await ensureWorkspaceSeeded(c);
|
||||||
const sessions = await listSessionMetaRows(c);
|
const sessions = await listSessionMetaRows(c);
|
||||||
await maybeScheduleWorkbenchRefreshes(c, record, sessions);
|
await maybeScheduleWorkspaceRefreshes(c, record, sessions);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: c.state.taskId,
|
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.
|
* subscribers. This is a full replacement payload, not a patch.
|
||||||
*/
|
*/
|
||||||
export async function buildTaskDetail(c: any): Promise<any> {
|
export async function buildTaskDetail(c: any): Promise<any> {
|
||||||
const record = await ensureWorkbenchSeeded(c);
|
const record = await ensureWorkspaceSeeded(c);
|
||||||
const gitState = await readCachedGitState(c);
|
const gitState = await readCachedGitState(c);
|
||||||
const sessions = await listSessionMetaRows(c);
|
const sessions = await listSessionMetaRows(c);
|
||||||
await maybeScheduleWorkbenchRefreshes(c, record, sessions);
|
await maybeScheduleWorkspaceRefreshes(c, record, sessions);
|
||||||
const summary = await buildTaskSummary(c);
|
const summary = await buildTaskSummary(c);
|
||||||
|
|
||||||
return {
|
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> {
|
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);
|
const meta = await readSessionMeta(c, sessionId);
|
||||||
if (!meta || meta.closed) {
|
if (!meta || meta.closed) {
|
||||||
throw new Error(`Unknown workbench session: ${sessionId}`);
|
throw new Error(`Unknown workspace session: ${sessionId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!meta.sandboxSessionId) {
|
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:
|
* The task actor emits two kinds of updates:
|
||||||
* - Push summary state up to the parent organization actor so the sidebar
|
* - 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.
|
* - Broadcast full detail/session payloads down to direct task subscribers.
|
||||||
*/
|
*/
|
||||||
export async function broadcastTaskUpdate(c: any, options?: { sessionId?: string }): Promise<void> {
|
export async function broadcastTaskUpdate(c: any, options?: { sessionId?: string }): Promise<void> {
|
||||||
const organization = await getOrCreateOrganization(c, c.state.organizationId);
|
const repository = await getOrCreateRepository(c, c.state.organizationId, c.state.repoId, c.state.repoRemote);
|
||||||
await organization.applyTaskSummaryUpdate({ taskSummary: await buildTaskSummary(c) });
|
await repository.applyTaskSummaryUpdate({ taskSummary: await buildTaskSummary(c) });
|
||||||
c.broadcast("taskUpdated", {
|
c.broadcast("taskUpdated", {
|
||||||
type: "taskDetailUpdated",
|
type: "taskUpdated",
|
||||||
detail: await buildTaskDetail(c),
|
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> {
|
export async function refreshWorkspaceDerivedState(c: any): Promise<void> {
|
||||||
const record = await ensureWorkbenchSeeded(c);
|
const record = await ensureWorkspaceSeeded(c);
|
||||||
const gitState = await collectWorkbenchGitState(c, record);
|
const gitState = await collectWorkspaceGitState(c, record);
|
||||||
await writeCachedGitState(c, gitState);
|
await writeCachedGitState(c, gitState);
|
||||||
await broadcastTaskUpdate(c);
|
await broadcastTaskUpdate(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function refreshWorkbenchSessionTranscript(c: any, sessionId: string): Promise<void> {
|
export async function refreshWorkspaceSessionTranscript(c: any, sessionId: string): Promise<void> {
|
||||||
const record = await ensureWorkbenchSeeded(c);
|
const record = await ensureWorkspaceSeeded(c);
|
||||||
const meta = (await readSessionMetaBySandboxSessionId(c, sessionId)) ?? (await readSessionMeta(c, sessionId));
|
const meta = (await readSessionMetaBySandboxSessionId(c, sessionId)) ?? (await readSessionMeta(c, sessionId));
|
||||||
if (!meta?.sandboxSessionId) {
|
if (!meta?.sandboxSessionId) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -967,7 +924,7 @@ export async function refreshWorkbenchSessionTranscript(c: any, sessionId: strin
|
||||||
await broadcastTaskUpdate(c, { sessionId: meta.sessionId });
|
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();
|
const nextTitle = value.trim();
|
||||||
if (!nextTitle) {
|
if (!nextTitle) {
|
||||||
throw new Error("task title is required");
|
throw new Error("task title is required");
|
||||||
|
|
@ -985,81 +942,30 @@ export async function renameWorkbenchTask(c: any, value: string): Promise<void>
|
||||||
await broadcastTaskUpdate(c);
|
await broadcastTaskUpdate(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function renameWorkbenchBranch(c: any, value: string): Promise<void> {
|
export async function createWorkspaceSession(c: any, model?: string): Promise<{ sessionId: string }> {
|
||||||
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 }> {
|
|
||||||
const sessionId = `session-${randomUUID()}`;
|
const sessionId = `session-${randomUUID()}`;
|
||||||
const record = await ensureWorkbenchSeeded(c);
|
const record = await ensureWorkspaceSeeded(c);
|
||||||
await ensureSessionMeta(c, {
|
await ensureSessionMeta(c, {
|
||||||
sessionId,
|
sessionId,
|
||||||
model: model ?? defaultModelForAgent(record.agentType),
|
model: model ?? defaultModelForAgent(record.agentType),
|
||||||
sandboxSessionId: null,
|
sandboxSessionId: null,
|
||||||
status: pendingWorkbenchSessionStatus(record),
|
status: pendingWorkspaceSessionStatus(record),
|
||||||
created: false,
|
created: false,
|
||||||
});
|
});
|
||||||
await broadcastTaskUpdate(c, { sessionId: sessionId });
|
await broadcastTaskUpdate(c, { sessionId: sessionId });
|
||||||
await enqueueWorkbenchEnsureSession(c, sessionId);
|
await enqueueWorkspaceEnsureSession(c, sessionId);
|
||||||
return { 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);
|
const meta = await readSessionMeta(c, sessionId);
|
||||||
if (!meta || meta.closed) {
|
if (!meta || meta.closed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const record = await ensureWorkbenchSeeded(c);
|
const record = await ensureWorkspaceSeeded(c);
|
||||||
if (meta.sandboxSessionId && meta.status === "ready") {
|
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,
|
sessionId: meta.sandboxSessionId,
|
||||||
});
|
});
|
||||||
await broadcastTaskUpdate(c, { sessionId: sessionId });
|
await broadcastTaskUpdate(c, { sessionId: sessionId });
|
||||||
|
|
@ -1089,7 +995,7 @@ export async function ensureWorkbenchSession(c: any, sessionId: string, model?:
|
||||||
status: "ready",
|
status: "ready",
|
||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
});
|
});
|
||||||
await enqueueWorkbenchRefresh(c, "task.command.workbench.refresh_session_transcript", {
|
await enqueueWorkspaceRefresh(c, "task.command.workspace.refresh_session_transcript", {
|
||||||
sessionId: meta.sandboxSessionId ?? sessionId,
|
sessionId: meta.sandboxSessionId ?? sessionId,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -1102,7 +1008,7 @@ export async function ensureWorkbenchSession(c: any, sessionId: string, model?:
|
||||||
await broadcastTaskUpdate(c, { sessionId: sessionId });
|
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 self = selfTask(c);
|
||||||
const pending = (await listSessionMetaRows(c, { includeClosed: true })).filter(
|
const pending = (await listSessionMetaRows(c, { includeClosed: true })).filter(
|
||||||
(row) => row.closed !== true && row.status !== "ready" && row.status !== "error",
|
(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) {
|
for (const row of pending) {
|
||||||
await self.send(
|
await self.send(
|
||||||
"task.command.workbench.ensure_session",
|
"task.command.workspace.ensure_session",
|
||||||
{
|
{
|
||||||
sessionId: row.sessionId,
|
sessionId: row.sessionId,
|
||||||
model: row.model,
|
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();
|
const trimmed = title.trim();
|
||||||
if (!trimmed) {
|
if (!trimmed) {
|
||||||
throw new Error("session title is required");
|
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 });
|
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, {
|
await updateSessionMeta(c, sessionId, {
|
||||||
unread: unread ? 1 : 0,
|
unread: unread ? 1 : 0,
|
||||||
});
|
});
|
||||||
await broadcastTaskUpdate(c, { sessionId });
|
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, {
|
await updateSessionMeta(c, sessionId, {
|
||||||
draftText: text,
|
draftText: text,
|
||||||
draftAttachmentsJson: JSON.stringify(attachments),
|
draftAttachmentsJson: JSON.stringify(attachments),
|
||||||
|
|
@ -1149,7 +1055,7 @@ export async function updateWorkbenchDraft(c: any, sessionId: string, text: stri
|
||||||
await broadcastTaskUpdate(c, { sessionId });
|
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);
|
const meta = await readSessionMeta(c, sessionId);
|
||||||
if (!meta || meta.closed) {
|
if (!meta || meta.closed) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -1159,7 +1065,7 @@ export async function changeWorkbenchModel(c: any, sessionId: string, model: str
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const record = await ensureWorkbenchSeeded(c);
|
const record = await ensureWorkspaceSeeded(c);
|
||||||
let nextMeta = await updateSessionMeta(c, sessionId, {
|
let nextMeta = await updateSessionMeta(c, sessionId, {
|
||||||
model,
|
model,
|
||||||
});
|
});
|
||||||
|
|
@ -1170,7 +1076,7 @@ export async function changeWorkbenchModel(c: any, sessionId: string, model: str
|
||||||
await sandbox.destroySession(nextMeta.sandboxSessionId);
|
await sandbox.destroySession(nextMeta.sandboxSessionId);
|
||||||
nextMeta = await updateSessionMeta(c, sessionId, {
|
nextMeta = await updateSessionMeta(c, sessionId, {
|
||||||
sandboxSessionId: null,
|
sandboxSessionId: null,
|
||||||
status: pendingWorkbenchSessionStatus(record),
|
status: pendingWorkspaceSessionStatus(record),
|
||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
transcriptJson: "[]",
|
transcriptJson: "[]",
|
||||||
transcriptUpdatedAt: null,
|
transcriptUpdatedAt: null,
|
||||||
|
|
@ -1191,20 +1097,20 @@ export async function changeWorkbenchModel(c: any, sessionId: string, model: str
|
||||||
}
|
}
|
||||||
} else if (nextMeta.status !== "ready") {
|
} else if (nextMeta.status !== "ready") {
|
||||||
nextMeta = await updateSessionMeta(c, sessionId, {
|
nextMeta = await updateSessionMeta(c, sessionId, {
|
||||||
status: pendingWorkbenchSessionStatus(record),
|
status: pendingWorkspaceSessionStatus(record),
|
||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldEnsure) {
|
if (shouldEnsure) {
|
||||||
await enqueueWorkbenchEnsureSession(c, sessionId);
|
await enqueueWorkspaceEnsureSession(c, sessionId);
|
||||||
}
|
}
|
||||||
await broadcastTaskUpdate(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 meta = requireSendableSessionMeta(await readSessionMeta(c, sessionId), sessionId);
|
||||||
const record = await ensureWorkbenchSeeded(c);
|
const record = await ensureWorkspaceSeeded(c);
|
||||||
const runtime = await getTaskSandboxRuntime(c, record);
|
const runtime = await getTaskSandboxRuntime(c, record);
|
||||||
// Skip git fetch on subsequent messages — the repo was already prepared during session
|
// 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.
|
// 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))
|
.where(eq(taskRuntime.id, 1))
|
||||||
.run();
|
.run();
|
||||||
|
|
||||||
await syncWorkbenchSessionStatus(c, meta.sandboxSessionId, "running", Date.now());
|
await syncWorkspaceSessionStatus(c, meta.sandboxSessionId, "running", Date.now());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await runtime.sandbox.sendPrompt({
|
await runtime.sandbox.sendPrompt({
|
||||||
sessionId: meta.sandboxSessionId,
|
sessionId: meta.sandboxSessionId,
|
||||||
prompt: prompt.join("\n\n"),
|
prompt: prompt.join("\n\n"),
|
||||||
});
|
});
|
||||||
await syncWorkbenchSessionStatus(c, meta.sandboxSessionId, "idle", Date.now());
|
await syncWorkspaceSessionStatus(c, meta.sandboxSessionId, "idle", Date.now());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await updateSessionMeta(c, sessionId, {
|
await updateSessionMeta(c, sessionId, {
|
||||||
status: "error",
|
status: "error",
|
||||||
errorMessage: error instanceof Error ? error.message : String(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;
|
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 meta = await requireReadySessionMeta(c, sessionId);
|
||||||
const sandbox = getTaskSandbox(c, c.state.organizationId, stableSandboxId(c));
|
const sandbox = getTaskSandbox(c, c.state.organizationId, stableSandboxId(c));
|
||||||
await sandbox.destroySession(meta.sandboxSessionId);
|
await sandbox.destroySession(meta.sandboxSessionId);
|
||||||
|
|
@ -1262,8 +1168,8 @@ export async function stopWorkbenchSession(c: any, sessionId: string): Promise<v
|
||||||
await broadcastTaskUpdate(c, { sessionId });
|
await broadcastTaskUpdate(c, { sessionId });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function syncWorkbenchSessionStatus(c: any, sessionId: string, status: "running" | "idle" | "error", at: number): Promise<void> {
|
export async function syncWorkspaceSessionStatus(c: any, sessionId: string, status: "running" | "idle" | "error", at: number): Promise<void> {
|
||||||
const record = await ensureWorkbenchSeeded(c);
|
const record = await ensureWorkspaceSeeded(c);
|
||||||
const meta = (await readSessionMetaBySandboxSessionId(c, sessionId)) ?? (await ensureSessionMeta(c, { sessionId: sessionId, sandboxSessionId: sessionId }));
|
const meta = (await readSessionMetaBySandboxSessionId(c, sessionId)) ?? (await ensureSessionMeta(c, { sessionId: sessionId, sandboxSessionId: sessionId }));
|
||||||
let changed = false;
|
let changed = false;
|
||||||
|
|
||||||
|
|
@ -1318,18 +1224,18 @@ export async function syncWorkbenchSessionStatus(c: any, sessionId: string, stat
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changed) {
|
if (changed) {
|
||||||
await enqueueWorkbenchRefresh(c, "task.command.workbench.refresh_session_transcript", {
|
await enqueueWorkspaceRefresh(c, "task.command.workspace.refresh_session_transcript", {
|
||||||
sessionId,
|
sessionId,
|
||||||
});
|
});
|
||||||
if (status !== "running") {
|
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 });
|
await broadcastTaskUpdate(c, { sessionId: meta.sessionId });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function closeWorkbenchSession(c: any, sessionId: string): Promise<void> {
|
export async function closeWorkspaceSession(c: any, sessionId: string): Promise<void> {
|
||||||
const record = await ensureWorkbenchSeeded(c);
|
const record = await ensureWorkspaceSeeded(c);
|
||||||
const sessions = await listSessionMetaRows(c);
|
const sessions = await listSessionMetaRows(c);
|
||||||
if (sessions.filter((candidate) => candidate.closed !== true).length <= 1) {
|
if (sessions.filter((candidate) => candidate.closed !== true).length <= 1) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -1360,7 +1266,7 @@ export async function closeWorkbenchSession(c: any, sessionId: string): Promise<
|
||||||
await broadcastTaskUpdate(c);
|
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 sessions = await listSessionMetaRows(c);
|
||||||
const latest = sessions[sessions.length - 1];
|
const latest = sessions[sessions.length - 1];
|
||||||
if (!latest) {
|
if (!latest) {
|
||||||
|
|
@ -1372,8 +1278,8 @@ export async function markWorkbenchUnread(c: any): Promise<void> {
|
||||||
await broadcastTaskUpdate(c, { sessionId: latest.sessionId });
|
await broadcastTaskUpdate(c, { sessionId: latest.sessionId });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function publishWorkbenchPr(c: any): Promise<void> {
|
export async function publishWorkspacePr(c: any): Promise<void> {
|
||||||
const record = await ensureWorkbenchSeeded(c);
|
const record = await ensureWorkspaceSeeded(c);
|
||||||
if (!record.branchName) {
|
if (!record.branchName) {
|
||||||
throw new Error("cannot publish PR without a branch");
|
throw new Error("cannot publish PR without a branch");
|
||||||
}
|
}
|
||||||
|
|
@ -1400,8 +1306,8 @@ export async function publishWorkbenchPr(c: any): Promise<void> {
|
||||||
await broadcastTaskUpdate(c);
|
await broadcastTaskUpdate(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function revertWorkbenchFile(c: any, path: string): Promise<void> {
|
export async function revertWorkspaceFile(c: any, path: string): Promise<void> {
|
||||||
const record = await ensureWorkbenchSeeded(c);
|
const record = await ensureWorkspaceSeeded(c);
|
||||||
if (!record.activeSandboxId) {
|
if (!record.activeSandboxId) {
|
||||||
throw new Error("cannot revert file without an active sandbox");
|
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) {
|
if (result.exitCode !== 0) {
|
||||||
throw new Error(`file revert failed (${result.exitCode}): ${result.result}`);
|
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);
|
await broadcastTaskUpdate(c);
|
||||||
}
|
}
|
||||||
|
|
@ -2,4 +2,4 @@ import { db } from "rivetkit/db/drizzle";
|
||||||
import * as schema from "./schema.js";
|
import * as schema from "./schema.js";
|
||||||
import migrations from "./migrations.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",
|
tag: "0000_auth_user",
|
||||||
breakpoints: true,
|
breakpoints: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
idx: 1,
|
||||||
|
when: 1773532800000,
|
||||||
|
tag: "0001_user_task_state",
|
||||||
|
breakpoints: true,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
@ -58,23 +64,39 @@ CREATE TABLE \`account\` (
|
||||||
CREATE UNIQUE INDEX \`account_provider_account_idx\` ON \`account\` (\`provider_id\`, \`account_id\`);
|
CREATE UNIQUE INDEX \`account_provider_account_idx\` ON \`account\` (\`provider_id\`, \`account_id\`);
|
||||||
--> statement-breakpoint
|
--> statement-breakpoint
|
||||||
CREATE TABLE \`user_profiles\` (
|
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_account_id\` text,
|
||||||
\`github_login\` text,
|
\`github_login\` text,
|
||||||
\`role_label\` text NOT NULL,
|
\`role_label\` text NOT NULL,
|
||||||
|
\`default_model\` text DEFAULT 'claude-sonnet-4' NOT NULL,
|
||||||
\`eligible_organization_ids_json\` text NOT NULL,
|
\`eligible_organization_ids_json\` text NOT NULL,
|
||||||
\`starter_repo_status\` text NOT NULL,
|
\`starter_repo_status\` text NOT NULL,
|
||||||
\`starter_repo_starred_at\` integer,
|
\`starter_repo_starred_at\` integer,
|
||||||
\`starter_repo_skipped_at\` integer,
|
\`starter_repo_skipped_at\` integer,
|
||||||
\`created_at\` integer NOT NULL,
|
\`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
|
--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX \`user_profiles_user_id_idx\` ON \`user_profiles\` (\`user_id\`);
|
||||||
|
--> statement-breakpoint
|
||||||
CREATE TABLE \`session_state\` (
|
CREATE TABLE \`session_state\` (
|
||||||
\`session_id\` text PRIMARY KEY NOT NULL,
|
\`session_id\` text PRIMARY KEY NOT NULL,
|
||||||
\`active_organization_id\` text,
|
\`active_organization_id\` text,
|
||||||
\`created_at\` integer NOT NULL,
|
\`created_at\` integer NOT NULL,
|
||||||
\`updated_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,
|
} 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 { 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 { actor } from "rivetkit";
|
||||||
import { authUserDb } from "./db/db.js";
|
import { userDb } from "./db/db.js";
|
||||||
import { authAccounts, authSessions, authUsers, sessionState, userProfiles } from "./db/schema.js";
|
import { authAccounts, authSessions, authUsers, sessionState, userProfiles, userTaskState } from "./db/schema.js";
|
||||||
|
|
||||||
const tables = {
|
const tables = {
|
||||||
user: authUsers,
|
user: authUsers,
|
||||||
|
|
@ -9,12 +9,13 @@ const tables = {
|
||||||
account: authAccounts,
|
account: authAccounts,
|
||||||
userProfiles,
|
userProfiles,
|
||||||
sessionState,
|
sessionState,
|
||||||
|
userTaskState,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
function tableFor(model: string) {
|
function tableFor(model: string) {
|
||||||
const table = tables[model as keyof typeof tables];
|
const table = tables[model as keyof typeof tables];
|
||||||
if (!table) {
|
if (!table) {
|
||||||
throw new Error(`Unsupported auth user model: ${model}`);
|
throw new Error(`Unsupported user model: ${model}`);
|
||||||
}
|
}
|
||||||
return table as any;
|
return table as any;
|
||||||
}
|
}
|
||||||
|
|
@ -22,7 +23,7 @@ function tableFor(model: string) {
|
||||||
function columnFor(table: any, field: string) {
|
function columnFor(table: any, field: string) {
|
||||||
const column = table[field];
|
const column = table[field];
|
||||||
if (!column) {
|
if (!column) {
|
||||||
throw new Error(`Unsupported auth user field: ${field}`);
|
throw new Error(`Unsupported user field: ${field}`);
|
||||||
}
|
}
|
||||||
return column;
|
return column;
|
||||||
}
|
}
|
||||||
|
|
@ -150,10 +151,10 @@ async function applyJoinToRows(c: any, model: string, rows: any[], join: any) {
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const authUser = actor({
|
export const user = actor({
|
||||||
db: authUserDb,
|
db: userDb,
|
||||||
options: {
|
options: {
|
||||||
name: "Auth User",
|
name: "User",
|
||||||
icon: "shield",
|
icon: "shield",
|
||||||
actionTimeout: 60_000,
|
actionTimeout: 60_000,
|
||||||
},
|
},
|
||||||
|
|
@ -161,6 +162,8 @@ export const authUser = actor({
|
||||||
userId: input.userId,
|
userId: input.userId,
|
||||||
}),
|
}),
|
||||||
actions: {
|
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> }) {
|
async createAuthRecord(c, input: { model: string; data: Record<string, unknown> }) {
|
||||||
const table = tableFor(input.model);
|
const table = tableFor(input.model);
|
||||||
await c.db
|
await c.db
|
||||||
|
|
@ -174,6 +177,8 @@ export const authUser = actor({
|
||||||
.get();
|
.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 }) {
|
async findOneAuthRecord(c, input: { model: string; where: any[]; join?: any }) {
|
||||||
const table = tableFor(input.model);
|
const table = tableFor(input.model);
|
||||||
const predicate = buildWhere(table, input.where);
|
const predicate = buildWhere(table, input.where);
|
||||||
|
|
@ -181,6 +186,8 @@ export const authUser = actor({
|
||||||
return await applyJoinToRow(c, input.model, row ?? null, input.join);
|
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 }) {
|
async findManyAuthRecords(c, input: { model: string; where?: any[]; limit?: number; offset?: number; sortBy?: any; join?: any }) {
|
||||||
const table = tableFor(input.model);
|
const table = tableFor(input.model);
|
||||||
const predicate = buildWhere(table, input.where);
|
const predicate = buildWhere(table, input.where);
|
||||||
|
|
@ -202,6 +209,8 @@ export const authUser = actor({
|
||||||
return await applyJoinToRows(c, input.model, rows, input.join);
|
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> }) {
|
async updateAuthRecord(c, input: { model: string; where: any[]; update: Record<string, unknown> }) {
|
||||||
const table = tableFor(input.model);
|
const table = tableFor(input.model);
|
||||||
const predicate = buildWhere(table, input.where);
|
const predicate = buildWhere(table, input.where);
|
||||||
|
|
@ -216,6 +225,8 @@ export const authUser = actor({
|
||||||
return await c.db.select().from(table).where(predicate).get();
|
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> }) {
|
async updateManyAuthRecords(c, input: { model: string; where: any[]; update: Record<string, unknown> }) {
|
||||||
const table = tableFor(input.model);
|
const table = tableFor(input.model);
|
||||||
const predicate = buildWhere(table, input.where);
|
const predicate = buildWhere(table, input.where);
|
||||||
|
|
@ -231,6 +242,8 @@ export const authUser = actor({
|
||||||
return row?.value ?? 0;
|
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[] }) {
|
async deleteAuthRecord(c, input: { model: string; where: any[] }) {
|
||||||
const table = tableFor(input.model);
|
const table = tableFor(input.model);
|
||||||
const predicate = buildWhere(table, input.where);
|
const predicate = buildWhere(table, input.where);
|
||||||
|
|
@ -240,6 +253,8 @@ export const authUser = actor({
|
||||||
await c.db.delete(table).where(predicate).run();
|
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[] }) {
|
async deleteManyAuthRecords(c, input: { model: string; where: any[] }) {
|
||||||
const table = tableFor(input.model);
|
const table = tableFor(input.model);
|
||||||
const predicate = buildWhere(table, input.where);
|
const predicate = buildWhere(table, input.where);
|
||||||
|
|
@ -251,6 +266,8 @@ export const authUser = actor({
|
||||||
return rows.length;
|
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[] }) {
|
async countAuthRecords(c, input: { model: string; where?: any[] }) {
|
||||||
const table = tableFor(input.model);
|
const table = tableFor(input.model);
|
||||||
const predicate = buildWhere(table, input.where);
|
const predicate = buildWhere(table, input.where);
|
||||||
|
|
@ -260,6 +277,7 @@ export const authUser = actor({
|
||||||
return row?.value ?? 0;
|
return row?.value ?? 0;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Custom Foundry action — not part of Better Auth.
|
||||||
async getAppAuthState(c, input: { sessionId: string }) {
|
async getAppAuthState(c, input: { sessionId: string }) {
|
||||||
const session = await c.db.select().from(authSessions).where(eq(authSessions.id, input.sessionId)).get();
|
const session = await c.db.select().from(authSessions).where(eq(authSessions.id, input.sessionId)).get();
|
||||||
if (!session) {
|
if (!session) {
|
||||||
|
|
@ -280,6 +298,7 @@ export const authUser = actor({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Custom Foundry action — not part of Better Auth.
|
||||||
async upsertUserProfile(
|
async upsertUserProfile(
|
||||||
c,
|
c,
|
||||||
input: {
|
input: {
|
||||||
|
|
@ -288,6 +307,7 @@ export const authUser = actor({
|
||||||
githubAccountId?: string | null;
|
githubAccountId?: string | null;
|
||||||
githubLogin?: string | null;
|
githubLogin?: string | null;
|
||||||
roleLabel?: string;
|
roleLabel?: string;
|
||||||
|
defaultModel?: string;
|
||||||
eligibleOrganizationIdsJson?: string;
|
eligibleOrganizationIdsJson?: string;
|
||||||
starterRepoStatus?: string;
|
starterRepoStatus?: string;
|
||||||
starterRepoStarredAt?: number | null;
|
starterRepoStarredAt?: number | null;
|
||||||
|
|
@ -299,10 +319,12 @@ export const authUser = actor({
|
||||||
await c.db
|
await c.db
|
||||||
.insert(userProfiles)
|
.insert(userProfiles)
|
||||||
.values({
|
.values({
|
||||||
|
id: 1,
|
||||||
userId: input.userId,
|
userId: input.userId,
|
||||||
githubAccountId: input.patch.githubAccountId ?? null,
|
githubAccountId: input.patch.githubAccountId ?? null,
|
||||||
githubLogin: input.patch.githubLogin ?? null,
|
githubLogin: input.patch.githubLogin ?? null,
|
||||||
roleLabel: input.patch.roleLabel ?? "GitHub user",
|
roleLabel: input.patch.roleLabel ?? "GitHub user",
|
||||||
|
defaultModel: input.patch.defaultModel ?? "claude-sonnet-4",
|
||||||
eligibleOrganizationIdsJson: input.patch.eligibleOrganizationIdsJson ?? "[]",
|
eligibleOrganizationIdsJson: input.patch.eligibleOrganizationIdsJson ?? "[]",
|
||||||
starterRepoStatus: input.patch.starterRepoStatus ?? "pending",
|
starterRepoStatus: input.patch.starterRepoStatus ?? "pending",
|
||||||
starterRepoStarredAt: input.patch.starterRepoStarredAt ?? null,
|
starterRepoStarredAt: input.patch.starterRepoStarredAt ?? null,
|
||||||
|
|
@ -316,6 +338,7 @@ export const authUser = actor({
|
||||||
...(input.patch.githubAccountId !== undefined ? { githubAccountId: input.patch.githubAccountId } : {}),
|
...(input.patch.githubAccountId !== undefined ? { githubAccountId: input.patch.githubAccountId } : {}),
|
||||||
...(input.patch.githubLogin !== undefined ? { githubLogin: input.patch.githubLogin } : {}),
|
...(input.patch.githubLogin !== undefined ? { githubLogin: input.patch.githubLogin } : {}),
|
||||||
...(input.patch.roleLabel !== undefined ? { roleLabel: input.patch.roleLabel } : {}),
|
...(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.eligibleOrganizationIdsJson !== undefined ? { eligibleOrganizationIdsJson: input.patch.eligibleOrganizationIdsJson } : {}),
|
||||||
...(input.patch.starterRepoStatus !== undefined ? { starterRepoStatus: input.patch.starterRepoStatus } : {}),
|
...(input.patch.starterRepoStatus !== undefined ? { starterRepoStatus: input.patch.starterRepoStatus } : {}),
|
||||||
...(input.patch.starterRepoStarredAt !== undefined ? { starterRepoStarredAt: input.patch.starterRepoStarredAt } : {}),
|
...(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();
|
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 }) {
|
async upsertSessionState(c, input: { sessionId: string; activeOrganizationId: string | null }) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
await c.db
|
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();
|
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 { betterAuth } from "better-auth";
|
||||||
import { createAdapterFactory } from "better-auth/adapters";
|
import { createAdapterFactory } from "better-auth/adapters";
|
||||||
import { APP_SHELL_ORGANIZATION_ID } from "../actors/organization/app-shell.js";
|
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";
|
import { logger } from "../logging.js";
|
||||||
|
|
||||||
const AUTH_BASE_PATH = "/v1/auth";
|
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
|
// 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.
|
// actors must exist by the time the adapter needs them.
|
||||||
const appOrganization = () =>
|
const appOrganization = () =>
|
||||||
actorClient.organization.getOrCreate(organizationKey(APP_SHELL_ORGANIZATION_ID), {
|
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
|
// getOrCreate is intentional: Better Auth creates user records during OAuth
|
||||||
// callbacks, so the auth-user actor must be lazily provisioned on first access.
|
// callbacks, so the user actor must be lazily provisioned on first access.
|
||||||
const getAuthUser = async (userId: string) =>
|
const getUser = async (userId: string) =>
|
||||||
await actorClient.authUser.getOrCreate(authUserKey(userId), {
|
await actorClient.user.getOrCreate(userKey(userId), {
|
||||||
createWithInput: { 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})`);
|
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 created = await userActor.createAuthRecord({ model, data: transformed });
|
||||||
const organization = await appOrganization();
|
const organization = await appOrganization();
|
||||||
|
|
||||||
|
|
@ -220,7 +220,7 @@ export function initBetterAuthService(actorClient: any, options: { apiUrl: strin
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userActor = await getAuthUser(userId);
|
const userActor = await getUser(userId);
|
||||||
const found = await userActor.findOneAuthRecord({ model, where: transformedWhere, join });
|
const found = await userActor.findOneAuthRecord({ model, where: transformedWhere, join });
|
||||||
return found ? ((await transformOutput(found, model, undefined, join)) as any) : null;
|
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 = [];
|
const rows = [];
|
||||||
for (const [userId, tokens] of byUser) {
|
for (const [userId, tokens] of byUser) {
|
||||||
const userActor = await getAuthUser(userId);
|
const userActor = await getUser(userId);
|
||||||
const scopedWhere = transformedWhere.map((entry: any) =>
|
const scopedWhere = transformedWhere.map((entry: any) =>
|
||||||
entry.field === "token" && entry.operator === "in" ? { ...entry, value: tokens } : entry,
|
entry.field === "token" && entry.operator === "in" ? { ...entry, value: tokens } : entry,
|
||||||
);
|
);
|
||||||
|
|
@ -275,7 +275,7 @@ export function initBetterAuthService(actorClient: any, options: { apiUrl: strin
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const userActor = await getAuthUser(userId);
|
const userActor = await getUser(userId);
|
||||||
const found = await userActor.findManyAuthRecords({ model, where: transformedWhere, limit, sortBy, offset, join });
|
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)));
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userActor = await getAuthUser(userId);
|
const userActor = await getUser(userId);
|
||||||
const before =
|
const before =
|
||||||
model === "user"
|
model === "user"
|
||||||
? await userActor.findOneAuthRecord({ model, where: transformedWhere })
|
? await userActor.findOneAuthRecord({ model, where: transformedWhere })
|
||||||
|
|
@ -345,7 +345,7 @@ export function initBetterAuthService(actorClient: any, options: { apiUrl: strin
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userActor = await getAuthUser(userId);
|
const userActor = await getUser(userId);
|
||||||
return await userActor.updateManyAuthRecords({ model, where: transformedWhere, update: transformedUpdate });
|
return await userActor.updateManyAuthRecords({ model, where: transformedWhere, update: transformedUpdate });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -361,7 +361,7 @@ export function initBetterAuthService(actorClient: any, options: { apiUrl: strin
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userActor = await getAuthUser(userId);
|
const userActor = await getUser(userId);
|
||||||
const organization = await appOrganization();
|
const organization = await appOrganization();
|
||||||
const before = await userActor.findOneAuthRecord({ model, where: transformedWhere });
|
const before = await userActor.findOneAuthRecord({ model, where: transformedWhere });
|
||||||
await userActor.deleteAuthRecord({ model, where: transformedWhere });
|
await userActor.deleteAuthRecord({ model, where: transformedWhere });
|
||||||
|
|
@ -397,7 +397,7 @@ export function initBetterAuthService(actorClient: any, options: { apiUrl: strin
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
const userActor = await getAuthUser(userId);
|
const userActor = await getUser(userId);
|
||||||
const organization = await appOrganization();
|
const organization = await appOrganization();
|
||||||
const sessions = await userActor.findManyAuthRecords({ model, where: transformedWhere, limit: 5000 });
|
const sessions = await userActor.findManyAuthRecords({ model, where: transformedWhere, limit: 5000 });
|
||||||
const deleted = await userActor.deleteManyAuthRecords({ model, where: transformedWhere });
|
const deleted = await userActor.deleteManyAuthRecords({ model, where: transformedWhere });
|
||||||
|
|
@ -415,7 +415,7 @@ export function initBetterAuthService(actorClient: any, options: { apiUrl: strin
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userActor = await getAuthUser(userId);
|
const userActor = await getUser(userId);
|
||||||
const deleted = await userActor.deleteManyAuthRecords({ model, where: transformedWhere });
|
const deleted = await userActor.deleteManyAuthRecords({ model, where: transformedWhere });
|
||||||
return deleted;
|
return deleted;
|
||||||
},
|
},
|
||||||
|
|
@ -431,7 +431,7 @@ export function initBetterAuthService(actorClient: any, options: { apiUrl: strin
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userActor = await getAuthUser(userId);
|
const userActor = await getUser(userId);
|
||||||
return await userActor.countAuthRecords({ model, where: transformedWhere });
|
return await userActor.countAuthRecords({ model, where: transformedWhere });
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -481,12 +481,12 @@ export function initBetterAuthService(actorClient: any, options: { apiUrl: strin
|
||||||
if (!route?.userId) {
|
if (!route?.userId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const userActor = await getAuthUser(route.userId);
|
const userActor = await getUser(route.userId);
|
||||||
return await userActor.getAppAuthState({ sessionId });
|
return await userActor.getAppAuthState({ sessionId });
|
||||||
},
|
},
|
||||||
|
|
||||||
async upsertUserProfile(userId: string, patch: Record<string, unknown>) {
|
async upsertUserProfile(userId: string, patch: Record<string, unknown>) {
|
||||||
const userActor = await getAuthUser(userId);
|
const userActor = await getUser(userId);
|
||||||
return await userActor.upsertUserProfile({ userId, patch });
|
return await userActor.upsertUserProfile({ userId, patch });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -495,7 +495,7 @@ export function initBetterAuthService(actorClient: any, options: { apiUrl: strin
|
||||||
if (!authState?.user?.id) {
|
if (!authState?.user?.id) {
|
||||||
throw new Error(`Unknown auth session ${sessionId}`);
|
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 });
|
return await userActor.upsertSessionState({ sessionId, activeOrganizationId });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { describe, expect, it } from "vitest";
|
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", () => {
|
describe("actor keys", () => {
|
||||||
it("prefixes every key with organization namespace", () => {
|
it("prefixes every key with organization namespace", () => {
|
||||||
|
|
@ -8,7 +8,7 @@ describe("actor keys", () => {
|
||||||
repositoryKey("default", "repo"),
|
repositoryKey("default", "repo"),
|
||||||
taskKey("default", "repo", "task"),
|
taskKey("default", "repo", "task"),
|
||||||
taskSandboxKey("default", "sbx"),
|
taskSandboxKey("default", "sbx"),
|
||||||
historyKey("default", "repo"),
|
auditLogKey("default", "repo"),
|
||||||
githubDataKey("default"),
|
githubDataKey("default"),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { describe, expect, it } from "vitest";
|
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", () => {
|
it("marks unread when a running session first becomes idle", () => {
|
||||||
expect(shouldMarkSessionUnreadForStatus({ thinkingSinceMs: Date.now() - 1_000 }, "idle")).toBe(true);
|
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", () => {
|
it("recreates an unused ready session so the selected model takes effect", () => {
|
||||||
expect(
|
expect(
|
||||||
shouldRecreateSessionForModelChange({
|
shouldRecreateSessionForModelChange({
|
||||||
|
|
@ -58,9 +58,9 @@ describe("workbench model changes", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("workbench send readiness", () => {
|
describe("workspace send readiness", () => {
|
||||||
it("rejects unknown sessions", () => {
|
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", () => {
|
it("rejects pending sessions", () => {
|
||||||
|
|
@ -10,8 +10,8 @@
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:e2e:full": "HF_ENABLE_DAEMON_FULL_E2E=1 vitest run test/e2e/full-integration-e2e.test.ts",
|
"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:workspace": "HF_ENABLE_DAEMON_WORKBENCH_E2E=1 vitest run test/e2e/workspace-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-load": "HF_ENABLE_DAEMON_WORKBENCH_LOAD_E2E=1 vitest run test/e2e/workspace-load-e2e.test.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sandbox-agent/foundry-shared": "workspace:*",
|
"@sandbox-agent/foundry-shared": "workspace:*",
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import type {
|
||||||
FoundryOrganization,
|
FoundryOrganization,
|
||||||
FoundryUser,
|
FoundryUser,
|
||||||
UpdateFoundryOrganizationProfileInput,
|
UpdateFoundryOrganizationProfileInput,
|
||||||
|
WorkspaceModelId,
|
||||||
} from "@sandbox-agent/foundry-shared";
|
} from "@sandbox-agent/foundry-shared";
|
||||||
import type { BackendClient } from "./backend-client.js";
|
import type { BackendClient } from "./backend-client.js";
|
||||||
import { getMockFoundryAppClient } from "./mock-app.js";
|
import { getMockFoundryAppClient } from "./mock-app.js";
|
||||||
|
|
@ -17,6 +18,7 @@ export interface FoundryAppClient {
|
||||||
skipStarterRepo(): Promise<void>;
|
skipStarterRepo(): Promise<void>;
|
||||||
starStarterRepo(organizationId: string): Promise<void>;
|
starStarterRepo(organizationId: string): Promise<void>;
|
||||||
selectOrganization(organizationId: string): Promise<void>;
|
selectOrganization(organizationId: string): Promise<void>;
|
||||||
|
setDefaultModel(model: WorkspaceModelId): Promise<void>;
|
||||||
updateOrganizationProfile(input: UpdateFoundryOrganizationProfileInput): Promise<void>;
|
updateOrganizationProfile(input: UpdateFoundryOrganizationProfileInput): Promise<void>;
|
||||||
triggerGithubSync(organizationId: string): Promise<void>;
|
triggerGithubSync(organizationId: string): Promise<void>;
|
||||||
completeHostedCheckout(organizationId: string, planId: FoundryBillingPlanId): Promise<void>;
|
completeHostedCheckout(organizationId: string, planId: FoundryBillingPlanId): Promise<void>;
|
||||||
|
|
|
||||||
|
|
@ -10,25 +10,25 @@ import type {
|
||||||
SandboxProcessesEvent,
|
SandboxProcessesEvent,
|
||||||
TaskRecord,
|
TaskRecord,
|
||||||
TaskSummary,
|
TaskSummary,
|
||||||
TaskWorkbenchChangeModelInput,
|
TaskWorkspaceChangeModelInput,
|
||||||
TaskWorkbenchCreateTaskInput,
|
TaskWorkspaceCreateTaskInput,
|
||||||
TaskWorkbenchCreateTaskResponse,
|
TaskWorkspaceCreateTaskResponse,
|
||||||
TaskWorkbenchDiffInput,
|
TaskWorkspaceDiffInput,
|
||||||
TaskWorkbenchRenameInput,
|
TaskWorkspaceRenameInput,
|
||||||
TaskWorkbenchRenameSessionInput,
|
TaskWorkspaceRenameSessionInput,
|
||||||
TaskWorkbenchSelectInput,
|
TaskWorkspaceSelectInput,
|
||||||
TaskWorkbenchSetSessionUnreadInput,
|
TaskWorkspaceSetSessionUnreadInput,
|
||||||
TaskWorkbenchSendMessageInput,
|
TaskWorkspaceSendMessageInput,
|
||||||
TaskWorkbenchSnapshot,
|
TaskWorkspaceSnapshot,
|
||||||
TaskWorkbenchSessionInput,
|
TaskWorkspaceSessionInput,
|
||||||
TaskWorkbenchUpdateDraftInput,
|
TaskWorkspaceUpdateDraftInput,
|
||||||
TaskEvent,
|
TaskEvent,
|
||||||
WorkbenchTaskDetail,
|
WorkspaceTaskDetail,
|
||||||
WorkbenchTaskSummary,
|
WorkspaceTaskSummary,
|
||||||
WorkbenchSessionDetail,
|
WorkspaceSessionDetail,
|
||||||
OrganizationEvent,
|
OrganizationEvent,
|
||||||
OrganizationSummarySnapshot,
|
OrganizationSummarySnapshot,
|
||||||
HistoryEvent,
|
AuditLogEvent as HistoryEvent,
|
||||||
HistoryQueryInput,
|
HistoryQueryInput,
|
||||||
SandboxProviderId,
|
SandboxProviderId,
|
||||||
RepoOverview,
|
RepoOverview,
|
||||||
|
|
@ -37,6 +37,7 @@ import type {
|
||||||
StarSandboxAgentRepoResult,
|
StarSandboxAgentRepoResult,
|
||||||
SwitchResult,
|
SwitchResult,
|
||||||
UpdateFoundryOrganizationProfileInput,
|
UpdateFoundryOrganizationProfileInput,
|
||||||
|
WorkspaceModelId,
|
||||||
} from "@sandbox-agent/foundry-shared";
|
} from "@sandbox-agent/foundry-shared";
|
||||||
import type { ProcessCreateRequest, ProcessInfo, ProcessLogFollowQuery, ProcessLogsResponse, ProcessSignalQuery } from "sandbox-agent";
|
import type { ProcessCreateRequest, ProcessInfo, ProcessLogFollowQuery, ProcessLogsResponse, ProcessSignalQuery } from "sandbox-agent";
|
||||||
import { createMockBackendClient } from "./mock/backend-client.js";
|
import { createMockBackendClient } from "./mock/backend-client.js";
|
||||||
|
|
@ -78,39 +79,36 @@ interface OrganizationHandle {
|
||||||
createTask(input: CreateTaskInput): Promise<TaskRecord>;
|
createTask(input: CreateTaskInput): Promise<TaskRecord>;
|
||||||
listTasks(input: { organizationId: string; repoId?: string }): Promise<TaskSummary[]>;
|
listTasks(input: { organizationId: string; repoId?: string }): Promise<TaskSummary[]>;
|
||||||
getRepoOverview(input: { organizationId: string; repoId: string }): Promise<RepoOverview>;
|
getRepoOverview(input: { organizationId: string; repoId: string }): Promise<RepoOverview>;
|
||||||
history(input: HistoryQueryInput): Promise<HistoryEvent[]>;
|
auditLog(input: HistoryQueryInput): Promise<HistoryEvent[]>;
|
||||||
switchTask(taskId: string): Promise<SwitchResult>;
|
switchTask(input: { repoId: string; taskId: string }): Promise<SwitchResult>;
|
||||||
getTask(input: { organizationId: string; taskId: string }): Promise<TaskRecord>;
|
getTask(input: { organizationId: string; repoId: string; taskId: string }): Promise<TaskRecord>;
|
||||||
attachTask(input: { organizationId: string; taskId: string; reason?: string }): Promise<{ target: string; sessionId: string | null }>;
|
attachTask(input: { organizationId: string; repoId: string; taskId: string; reason?: string }): Promise<{ target: string; sessionId: string | null }>;
|
||||||
pushTask(input: { organizationId: string; taskId: string; reason?: string }): Promise<void>;
|
pushTask(input: { organizationId: string; repoId: string; taskId: string; reason?: string }): Promise<void>;
|
||||||
syncTask(input: { organizationId: string; taskId: string; reason?: string }): Promise<void>;
|
syncTask(input: { organizationId: string; repoId: string; taskId: string; reason?: string }): Promise<void>;
|
||||||
mergeTask(input: { organizationId: string; taskId: string; reason?: string }): Promise<void>;
|
mergeTask(input: { organizationId: string; repoId: string; taskId: string; reason?: string }): Promise<void>;
|
||||||
archiveTask(input: { organizationId: string; taskId: string; reason?: string }): Promise<void>;
|
archiveTask(input: { organizationId: string; repoId: string; taskId: string; reason?: string }): Promise<void>;
|
||||||
killTask(input: { organizationId: 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 }>;
|
useOrganization(input: { organizationId: string }): Promise<{ organizationId: string }>;
|
||||||
starSandboxAgentRepo(input: StarSandboxAgentRepoInput): Promise<StarSandboxAgentRepoResult>;
|
starSandboxAgentRepo(input: StarSandboxAgentRepoInput): Promise<StarSandboxAgentRepoResult>;
|
||||||
getOrganizationSummary(input: { organizationId: string }): Promise<OrganizationSummarySnapshot>;
|
getOrganizationSummary(input: { organizationId: string }): Promise<OrganizationSummarySnapshot>;
|
||||||
applyTaskSummaryUpdate(input: { taskSummary: WorkbenchTaskSummary }): Promise<void>;
|
adminReconcileWorkspaceState(input: { organizationId: string }): Promise<OrganizationSummarySnapshot>;
|
||||||
removeTaskSummary(input: { taskId: string }): Promise<void>;
|
createWorkspaceTask(input: TaskWorkspaceCreateTaskInput): Promise<TaskWorkspaceCreateTaskResponse>;
|
||||||
reconcileWorkbenchState(input: { organizationId: string }): Promise<OrganizationSummarySnapshot>;
|
markWorkspaceUnread(input: TaskWorkspaceSelectInput): Promise<void>;
|
||||||
createWorkbenchTask(input: TaskWorkbenchCreateTaskInput): Promise<TaskWorkbenchCreateTaskResponse>;
|
renameWorkspaceTask(input: TaskWorkspaceRenameInput): Promise<void>;
|
||||||
markWorkbenchUnread(input: TaskWorkbenchSelectInput): Promise<void>;
|
createWorkspaceSession(input: TaskWorkspaceSelectInput & { model?: string }): Promise<{ sessionId: string }>;
|
||||||
renameWorkbenchTask(input: TaskWorkbenchRenameInput): Promise<void>;
|
renameWorkspaceSession(input: TaskWorkspaceRenameSessionInput): Promise<void>;
|
||||||
renameWorkbenchBranch(input: TaskWorkbenchRenameInput): Promise<void>;
|
setWorkspaceSessionUnread(input: TaskWorkspaceSetSessionUnreadInput): Promise<void>;
|
||||||
createWorkbenchSession(input: TaskWorkbenchSelectInput & { model?: string }): Promise<{ sessionId: string }>;
|
updateWorkspaceDraft(input: TaskWorkspaceUpdateDraftInput): Promise<void>;
|
||||||
renameWorkbenchSession(input: TaskWorkbenchRenameSessionInput): Promise<void>;
|
changeWorkspaceModel(input: TaskWorkspaceChangeModelInput): Promise<void>;
|
||||||
setWorkbenchSessionUnread(input: TaskWorkbenchSetSessionUnreadInput): Promise<void>;
|
sendWorkspaceMessage(input: TaskWorkspaceSendMessageInput): Promise<void>;
|
||||||
updateWorkbenchDraft(input: TaskWorkbenchUpdateDraftInput): Promise<void>;
|
stopWorkspaceSession(input: TaskWorkspaceSessionInput): Promise<void>;
|
||||||
changeWorkbenchModel(input: TaskWorkbenchChangeModelInput): Promise<void>;
|
closeWorkspaceSession(input: TaskWorkspaceSessionInput): Promise<void>;
|
||||||
sendWorkbenchMessage(input: TaskWorkbenchSendMessageInput): Promise<void>;
|
publishWorkspacePr(input: TaskWorkspaceSelectInput): Promise<void>;
|
||||||
stopWorkbenchSession(input: TaskWorkbenchSessionInput): Promise<void>;
|
revertWorkspaceFile(input: TaskWorkspaceDiffInput): Promise<void>;
|
||||||
closeWorkbenchSession(input: TaskWorkbenchSessionInput): Promise<void>;
|
adminReloadGithubOrganization(): Promise<void>;
|
||||||
publishWorkbenchPr(input: TaskWorkbenchSelectInput): Promise<void>;
|
adminReloadGithubPullRequests(): Promise<void>;
|
||||||
revertWorkbenchFile(input: TaskWorkbenchDiffInput): Promise<void>;
|
adminReloadGithubRepository(input: { repoId: string }): Promise<void>;
|
||||||
reloadGithubOrganization(): Promise<void>;
|
adminReloadGithubPullRequest(input: { repoId: string; prNumber: number }): Promise<void>;
|
||||||
reloadGithubPullRequests(): Promise<void>;
|
|
||||||
reloadGithubRepository(input: { repoId: string }): Promise<void>;
|
|
||||||
reloadGithubPullRequest(input: { repoId: string; prNumber: number }): Promise<void>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AppOrganizationHandle {
|
interface AppOrganizationHandle {
|
||||||
|
|
@ -119,6 +117,7 @@ interface AppOrganizationHandle {
|
||||||
skipAppStarterRepo(input: { sessionId: string }): Promise<FoundryAppSnapshot>;
|
skipAppStarterRepo(input: { sessionId: string }): Promise<FoundryAppSnapshot>;
|
||||||
starAppStarterRepo(input: { sessionId: string; organizationId: string }): Promise<FoundryAppSnapshot>;
|
starAppStarterRepo(input: { sessionId: string; organizationId: string }): Promise<FoundryAppSnapshot>;
|
||||||
selectAppOrganization(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>;
|
updateAppOrganizationProfile(input: UpdateFoundryOrganizationProfileInput & { sessionId: string }): Promise<FoundryAppSnapshot>;
|
||||||
triggerAppRepoImport(input: { sessionId: string; organizationId: string }): Promise<FoundryAppSnapshot>;
|
triggerAppRepoImport(input: { sessionId: string; organizationId: string }): Promise<FoundryAppSnapshot>;
|
||||||
beginAppGithubInstall(input: { sessionId: string; organizationId: string }): Promise<{ url: string }>;
|
beginAppGithubInstall(input: { sessionId: string; organizationId: string }): Promise<{ url: string }>;
|
||||||
|
|
@ -130,9 +129,9 @@ interface AppOrganizationHandle {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TaskHandle {
|
interface TaskHandle {
|
||||||
getTaskSummary(): Promise<WorkbenchTaskSummary>;
|
getTaskSummary(): Promise<WorkspaceTaskSummary>;
|
||||||
getTaskDetail(): Promise<WorkbenchTaskDetail>;
|
getTaskDetail(): Promise<WorkspaceTaskDetail>;
|
||||||
getSessionDetail(input: { sessionId: string }): Promise<WorkbenchSessionDetail>;
|
getSessionDetail(input: { sessionId: string }): Promise<WorkspaceSessionDetail>;
|
||||||
connect(): ActorConn;
|
connect(): ActorConn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -192,6 +191,7 @@ export interface BackendClient {
|
||||||
skipAppStarterRepo(): Promise<FoundryAppSnapshot>;
|
skipAppStarterRepo(): Promise<FoundryAppSnapshot>;
|
||||||
starAppStarterRepo(organizationId: string): Promise<FoundryAppSnapshot>;
|
starAppStarterRepo(organizationId: string): Promise<FoundryAppSnapshot>;
|
||||||
selectAppOrganization(organizationId: string): Promise<FoundryAppSnapshot>;
|
selectAppOrganization(organizationId: string): Promise<FoundryAppSnapshot>;
|
||||||
|
setAppDefaultModel(defaultModel: WorkspaceModelId): Promise<FoundryAppSnapshot>;
|
||||||
updateAppOrganizationProfile(input: UpdateFoundryOrganizationProfileInput): Promise<FoundryAppSnapshot>;
|
updateAppOrganizationProfile(input: UpdateFoundryOrganizationProfileInput): Promise<FoundryAppSnapshot>;
|
||||||
triggerAppRepoImport(organizationId: string): Promise<FoundryAppSnapshot>;
|
triggerAppRepoImport(organizationId: string): Promise<FoundryAppSnapshot>;
|
||||||
reconnectAppGithub(organizationId: string): Promise<void>;
|
reconnectAppGithub(organizationId: string): Promise<void>;
|
||||||
|
|
@ -204,11 +204,11 @@ export interface BackendClient {
|
||||||
createTask(input: CreateTaskInput): Promise<TaskRecord>;
|
createTask(input: CreateTaskInput): Promise<TaskRecord>;
|
||||||
listTasks(organizationId: string, repoId?: string): Promise<TaskSummary[]>;
|
listTasks(organizationId: string, repoId?: string): Promise<TaskSummary[]>;
|
||||||
getRepoOverview(organizationId: string, repoId: string): Promise<RepoOverview>;
|
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[]>;
|
listHistory(input: HistoryQueryInput): Promise<HistoryEvent[]>;
|
||||||
switchTask(organizationId: string, taskId: string): Promise<SwitchResult>;
|
switchTask(organizationId: string, repoId: string, taskId: string): Promise<SwitchResult>;
|
||||||
attachTask(organizationId: string, taskId: string): Promise<{ target: string; sessionId: string | null }>;
|
attachTask(organizationId: string, repoId: string, taskId: string): Promise<{ target: string; sessionId: string | null }>;
|
||||||
runAction(organizationId: string, taskId: string, action: TaskAction): Promise<void>;
|
runAction(organizationId: string, repoId: string, taskId: string, action: TaskAction): Promise<void>;
|
||||||
createSandboxSession(input: {
|
createSandboxSession(input: {
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
sandboxProviderId: SandboxProviderId;
|
sandboxProviderId: SandboxProviderId;
|
||||||
|
|
@ -280,28 +280,27 @@ export interface BackendClient {
|
||||||
): Promise<{ sandboxProviderId: SandboxProviderId; sandboxId: string; state: string; at: number }>;
|
): Promise<{ sandboxProviderId: SandboxProviderId; sandboxId: string; state: string; at: number }>;
|
||||||
getSandboxAgentConnection(organizationId: string, sandboxProviderId: SandboxProviderId, sandboxId: string): Promise<{ endpoint: string; token?: string }>;
|
getSandboxAgentConnection(organizationId: string, sandboxProviderId: SandboxProviderId, sandboxId: string): Promise<{ endpoint: string; token?: string }>;
|
||||||
getOrganizationSummary(organizationId: string): Promise<OrganizationSummarySnapshot>;
|
getOrganizationSummary(organizationId: string): Promise<OrganizationSummarySnapshot>;
|
||||||
getTaskDetail(organizationId: string, repoId: string, taskId: string): Promise<WorkbenchTaskDetail>;
|
getTaskDetail(organizationId: string, repoId: string, taskId: string): Promise<WorkspaceTaskDetail>;
|
||||||
getSessionDetail(organizationId: string, repoId: string, taskId: string, sessionId: string): Promise<WorkbenchSessionDetail>;
|
getSessionDetail(organizationId: string, repoId: string, taskId: string, sessionId: string): Promise<WorkspaceSessionDetail>;
|
||||||
getWorkbench(organizationId: string): Promise<TaskWorkbenchSnapshot>;
|
getWorkspace(organizationId: string): Promise<TaskWorkspaceSnapshot>;
|
||||||
subscribeWorkbench(organizationId: string, listener: () => void): () => void;
|
subscribeWorkspace(organizationId: string, listener: () => void): () => void;
|
||||||
createWorkbenchTask(organizationId: string, input: TaskWorkbenchCreateTaskInput): Promise<TaskWorkbenchCreateTaskResponse>;
|
createWorkspaceTask(organizationId: string, input: TaskWorkspaceCreateTaskInput): Promise<TaskWorkspaceCreateTaskResponse>;
|
||||||
markWorkbenchUnread(organizationId: string, input: TaskWorkbenchSelectInput): Promise<void>;
|
markWorkspaceUnread(organizationId: string, input: TaskWorkspaceSelectInput): Promise<void>;
|
||||||
renameWorkbenchTask(organizationId: string, input: TaskWorkbenchRenameInput): Promise<void>;
|
renameWorkspaceTask(organizationId: string, input: TaskWorkspaceRenameInput): Promise<void>;
|
||||||
renameWorkbenchBranch(organizationId: string, input: TaskWorkbenchRenameInput): Promise<void>;
|
createWorkspaceSession(organizationId: string, input: TaskWorkspaceSelectInput & { model?: string }): Promise<{ sessionId: string }>;
|
||||||
createWorkbenchSession(organizationId: string, input: TaskWorkbenchSelectInput & { model?: string }): Promise<{ sessionId: string }>;
|
renameWorkspaceSession(organizationId: string, input: TaskWorkspaceRenameSessionInput): Promise<void>;
|
||||||
renameWorkbenchSession(organizationId: string, input: TaskWorkbenchRenameSessionInput): Promise<void>;
|
setWorkspaceSessionUnread(organizationId: string, input: TaskWorkspaceSetSessionUnreadInput): Promise<void>;
|
||||||
setWorkbenchSessionUnread(organizationId: string, input: TaskWorkbenchSetSessionUnreadInput): Promise<void>;
|
updateWorkspaceDraft(organizationId: string, input: TaskWorkspaceUpdateDraftInput): Promise<void>;
|
||||||
updateWorkbenchDraft(organizationId: string, input: TaskWorkbenchUpdateDraftInput): Promise<void>;
|
changeWorkspaceModel(organizationId: string, input: TaskWorkspaceChangeModelInput): Promise<void>;
|
||||||
changeWorkbenchModel(organizationId: string, input: TaskWorkbenchChangeModelInput): Promise<void>;
|
sendWorkspaceMessage(organizationId: string, input: TaskWorkspaceSendMessageInput): Promise<void>;
|
||||||
sendWorkbenchMessage(organizationId: string, input: TaskWorkbenchSendMessageInput): Promise<void>;
|
stopWorkspaceSession(organizationId: string, input: TaskWorkspaceSessionInput): Promise<void>;
|
||||||
stopWorkbenchSession(organizationId: string, input: TaskWorkbenchSessionInput): Promise<void>;
|
closeWorkspaceSession(organizationId: string, input: TaskWorkspaceSessionInput): Promise<void>;
|
||||||
closeWorkbenchSession(organizationId: string, input: TaskWorkbenchSessionInput): Promise<void>;
|
publishWorkspacePr(organizationId: string, input: TaskWorkspaceSelectInput): Promise<void>;
|
||||||
publishWorkbenchPr(organizationId: string, input: TaskWorkbenchSelectInput): Promise<void>;
|
revertWorkspaceFile(organizationId: string, input: TaskWorkspaceDiffInput): Promise<void>;
|
||||||
revertWorkbenchFile(organizationId: string, input: TaskWorkbenchDiffInput): Promise<void>;
|
adminReloadGithubOrganization(organizationId: string): Promise<void>;
|
||||||
reloadGithubOrganization(organizationId: string): Promise<void>;
|
adminReloadGithubPullRequests(organizationId: string): Promise<void>;
|
||||||
reloadGithubPullRequests(organizationId: string): Promise<void>;
|
adminReloadGithubRepository(organizationId: string, repoId: string): Promise<void>;
|
||||||
reloadGithubRepository(organizationId: string, repoId: string): Promise<void>;
|
adminReloadGithubPullRequest(organizationId: string, repoId: string, prNumber: number): Promise<void>;
|
||||||
reloadGithubPullRequest(organizationId: string, repoId: string, prNumber: number): Promise<void>;
|
|
||||||
health(): Promise<{ ok: true }>;
|
health(): Promise<{ ok: true }>;
|
||||||
useOrganization(organizationId: string): Promise<{ organizationId: string }>;
|
useOrganization(organizationId: string): Promise<{ organizationId: string }>;
|
||||||
starSandboxAgentRepo(organizationId: string): Promise<StarSandboxAgentRepoResult>;
|
starSandboxAgentRepo(organizationId: string): Promise<StarSandboxAgentRepoResult>;
|
||||||
|
|
@ -410,7 +409,7 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
||||||
const rivetApiEndpoint = endpoints.rivetEndpoint;
|
const rivetApiEndpoint = endpoints.rivetEndpoint;
|
||||||
const appApiEndpoint = endpoints.appEndpoint;
|
const appApiEndpoint = endpoints.appEndpoint;
|
||||||
const client = createClient({ endpoint: rivetApiEndpoint }) as unknown as RivetClient;
|
const client = createClient({ endpoint: rivetApiEndpoint }) as unknown as RivetClient;
|
||||||
const workbenchSubscriptions = new Map<
|
const workspaceSubscriptions = new Map<
|
||||||
string,
|
string,
|
||||||
{
|
{
|
||||||
listeners: Set<() => void>;
|
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 summary = await (await organization(organizationId)).getOrganizationSummary({ organizationId });
|
||||||
const tasks = (
|
const tasks = (
|
||||||
await Promise.all(
|
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 {
|
return {
|
||||||
id: detail.id,
|
id: detail.id,
|
||||||
repoId: detail.repoId,
|
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
|
const repositories = summary.repos
|
||||||
.map((repo) => ({
|
.map((repo) => ({
|
||||||
|
|
@ -642,14 +641,14 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const subscribeWorkbench = (organizationId: string, listener: () => void): (() => void) => {
|
const subscribeWorkspace = (organizationId: string, listener: () => void): (() => void) => {
|
||||||
let entry = workbenchSubscriptions.get(organizationId);
|
let entry = workspaceSubscriptions.get(organizationId);
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
entry = {
|
entry = {
|
||||||
listeners: new Set(),
|
listeners: new Set(),
|
||||||
disposeConnPromise: null,
|
disposeConnPromise: null,
|
||||||
};
|
};
|
||||||
workbenchSubscriptions.set(organizationId, entry);
|
workspaceSubscriptions.set(organizationId, entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
entry.listeners.add(listener);
|
entry.listeners.add(listener);
|
||||||
|
|
@ -658,8 +657,8 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
||||||
entry.disposeConnPromise = (async () => {
|
entry.disposeConnPromise = (async () => {
|
||||||
const handle = await organization(organizationId);
|
const handle = await organization(organizationId);
|
||||||
const conn = (handle as any).connect();
|
const conn = (handle as any).connect();
|
||||||
const unsubscribeEvent = conn.on("workbenchUpdated", () => {
|
const unsubscribeEvent = conn.on("organizationUpdated", () => {
|
||||||
const current = workbenchSubscriptions.get(organizationId);
|
const current = workspaceSubscriptions.get(organizationId);
|
||||||
if (!current) {
|
if (!current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -677,7 +676,7 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
const current = workbenchSubscriptions.get(organizationId);
|
const current = workspaceSubscriptions.get(organizationId);
|
||||||
if (!current) {
|
if (!current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -686,7 +685,7 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
workbenchSubscriptions.delete(organizationId);
|
workspaceSubscriptions.delete(organizationId);
|
||||||
void current.disposeConnPromise?.then(async (disposeConn) => {
|
void current.disposeConnPromise?.then(async (disposeConn) => {
|
||||||
await disposeConn?.();
|
await disposeConn?.();
|
||||||
});
|
});
|
||||||
|
|
@ -849,6 +848,14 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
||||||
return await (await appOrganization()).selectAppOrganization({ sessionId, organizationId });
|
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> {
|
async updateAppOrganizationProfile(input: UpdateFoundryOrganizationProfileInput): Promise<FoundryAppSnapshot> {
|
||||||
const sessionId = await getSessionId();
|
const sessionId = await getSessionId();
|
||||||
if (!sessionId) {
|
if (!sessionId) {
|
||||||
|
|
@ -948,33 +955,36 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
||||||
return (await organization(organizationId)).getRepoOverview({ organizationId, repoId });
|
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({
|
return (await organization(organizationId)).getTask({
|
||||||
organizationId,
|
organizationId,
|
||||||
|
repoId,
|
||||||
taskId,
|
taskId,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async listHistory(input: HistoryQueryInput): Promise<HistoryEvent[]> {
|
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> {
|
async switchTask(organizationId: string, repoId: string, taskId: string): Promise<SwitchResult> {
|
||||||
return (await organization(organizationId)).switchTask(taskId);
|
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({
|
return (await organization(organizationId)).attachTask({
|
||||||
organizationId,
|
organizationId,
|
||||||
|
repoId,
|
||||||
taskId,
|
taskId,
|
||||||
reason: "cli.attach",
|
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") {
|
if (action === "push") {
|
||||||
await (await organization(organizationId)).pushTask({
|
await (await organization(organizationId)).pushTask({
|
||||||
organizationId,
|
organizationId,
|
||||||
|
repoId,
|
||||||
taskId,
|
taskId,
|
||||||
reason: "cli.push",
|
reason: "cli.push",
|
||||||
});
|
});
|
||||||
|
|
@ -983,6 +993,7 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
||||||
if (action === "sync") {
|
if (action === "sync") {
|
||||||
await (await organization(organizationId)).syncTask({
|
await (await organization(organizationId)).syncTask({
|
||||||
organizationId,
|
organizationId,
|
||||||
|
repoId,
|
||||||
taskId,
|
taskId,
|
||||||
reason: "cli.sync",
|
reason: "cli.sync",
|
||||||
});
|
});
|
||||||
|
|
@ -991,6 +1002,7 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
||||||
if (action === "merge") {
|
if (action === "merge") {
|
||||||
await (await organization(organizationId)).mergeTask({
|
await (await organization(organizationId)).mergeTask({
|
||||||
organizationId,
|
organizationId,
|
||||||
|
repoId,
|
||||||
taskId,
|
taskId,
|
||||||
reason: "cli.merge",
|
reason: "cli.merge",
|
||||||
});
|
});
|
||||||
|
|
@ -999,6 +1011,7 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
||||||
if (action === "archive") {
|
if (action === "archive") {
|
||||||
await (await organization(organizationId)).archiveTask({
|
await (await organization(organizationId)).archiveTask({
|
||||||
organizationId,
|
organizationId,
|
||||||
|
repoId,
|
||||||
taskId,
|
taskId,
|
||||||
reason: "cli.archive",
|
reason: "cli.archive",
|
||||||
});
|
});
|
||||||
|
|
@ -1006,6 +1019,7 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
||||||
}
|
}
|
||||||
await (await organization(organizationId)).killTask({
|
await (await organization(organizationId)).killTask({
|
||||||
organizationId,
|
organizationId,
|
||||||
|
repoId,
|
||||||
taskId,
|
taskId,
|
||||||
reason: "cli.kill",
|
reason: "cli.kill",
|
||||||
});
|
});
|
||||||
|
|
@ -1160,92 +1174,88 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
||||||
return (await organization(organizationId)).getOrganizationSummary({ organizationId });
|
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();
|
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 });
|
return (await task(organizationId, repoId, taskIdValue)).getSessionDetail({ sessionId });
|
||||||
},
|
},
|
||||||
|
|
||||||
async getWorkbench(organizationId: string): Promise<TaskWorkbenchSnapshot> {
|
async getWorkspace(organizationId: string): Promise<TaskWorkspaceSnapshot> {
|
||||||
return await getWorkbenchCompat(organizationId);
|
return await getWorkspaceCompat(organizationId);
|
||||||
},
|
},
|
||||||
|
|
||||||
subscribeWorkbench(organizationId: string, listener: () => void): () => void {
|
subscribeWorkspace(organizationId: string, listener: () => void): () => void {
|
||||||
return subscribeWorkbench(organizationId, listener);
|
return subscribeWorkspace(organizationId, listener);
|
||||||
},
|
},
|
||||||
|
|
||||||
async createWorkbenchTask(organizationId: string, input: TaskWorkbenchCreateTaskInput): Promise<TaskWorkbenchCreateTaskResponse> {
|
async createWorkspaceTask(organizationId: string, input: TaskWorkspaceCreateTaskInput): Promise<TaskWorkspaceCreateTaskResponse> {
|
||||||
return (await organization(organizationId)).createWorkbenchTask(input);
|
return (await organization(organizationId)).createWorkspaceTask(input);
|
||||||
},
|
},
|
||||||
|
|
||||||
async markWorkbenchUnread(organizationId: string, input: TaskWorkbenchSelectInput): Promise<void> {
|
async markWorkspaceUnread(organizationId: string, input: TaskWorkspaceSelectInput): Promise<void> {
|
||||||
await (await organization(organizationId)).markWorkbenchUnread(input);
|
await (await organization(organizationId)).markWorkspaceUnread(input);
|
||||||
},
|
},
|
||||||
|
|
||||||
async renameWorkbenchTask(organizationId: string, input: TaskWorkbenchRenameInput): Promise<void> {
|
async renameWorkspaceTask(organizationId: string, input: TaskWorkspaceRenameInput): Promise<void> {
|
||||||
await (await organization(organizationId)).renameWorkbenchTask(input);
|
await (await organization(organizationId)).renameWorkspaceTask(input);
|
||||||
},
|
},
|
||||||
|
|
||||||
async renameWorkbenchBranch(organizationId: string, input: TaskWorkbenchRenameInput): Promise<void> {
|
async createWorkspaceSession(organizationId: string, input: TaskWorkspaceSelectInput & { model?: string }): Promise<{ sessionId: string }> {
|
||||||
await (await organization(organizationId)).renameWorkbenchBranch(input);
|
return await (await organization(organizationId)).createWorkspaceSession(input);
|
||||||
},
|
},
|
||||||
|
|
||||||
async createWorkbenchSession(organizationId: string, input: TaskWorkbenchSelectInput & { model?: string }): Promise<{ sessionId: string }> {
|
async renameWorkspaceSession(organizationId: string, input: TaskWorkspaceRenameSessionInput): Promise<void> {
|
||||||
return await (await organization(organizationId)).createWorkbenchSession(input);
|
await (await organization(organizationId)).renameWorkspaceSession(input);
|
||||||
},
|
},
|
||||||
|
|
||||||
async renameWorkbenchSession(organizationId: string, input: TaskWorkbenchRenameSessionInput): Promise<void> {
|
async setWorkspaceSessionUnread(organizationId: string, input: TaskWorkspaceSetSessionUnreadInput): Promise<void> {
|
||||||
await (await organization(organizationId)).renameWorkbenchSession(input);
|
await (await organization(organizationId)).setWorkspaceSessionUnread(input);
|
||||||
},
|
},
|
||||||
|
|
||||||
async setWorkbenchSessionUnread(organizationId: string, input: TaskWorkbenchSetSessionUnreadInput): Promise<void> {
|
async updateWorkspaceDraft(organizationId: string, input: TaskWorkspaceUpdateDraftInput): Promise<void> {
|
||||||
await (await organization(organizationId)).setWorkbenchSessionUnread(input);
|
await (await organization(organizationId)).updateWorkspaceDraft(input);
|
||||||
},
|
},
|
||||||
|
|
||||||
async updateWorkbenchDraft(organizationId: string, input: TaskWorkbenchUpdateDraftInput): Promise<void> {
|
async changeWorkspaceModel(organizationId: string, input: TaskWorkspaceChangeModelInput): Promise<void> {
|
||||||
await (await organization(organizationId)).updateWorkbenchDraft(input);
|
await (await organization(organizationId)).changeWorkspaceModel(input);
|
||||||
},
|
},
|
||||||
|
|
||||||
async changeWorkbenchModel(organizationId: string, input: TaskWorkbenchChangeModelInput): Promise<void> {
|
async sendWorkspaceMessage(organizationId: string, input: TaskWorkspaceSendMessageInput): Promise<void> {
|
||||||
await (await organization(organizationId)).changeWorkbenchModel(input);
|
await (await organization(organizationId)).sendWorkspaceMessage(input);
|
||||||
},
|
},
|
||||||
|
|
||||||
async sendWorkbenchMessage(organizationId: string, input: TaskWorkbenchSendMessageInput): Promise<void> {
|
async stopWorkspaceSession(organizationId: string, input: TaskWorkspaceSessionInput): Promise<void> {
|
||||||
await (await organization(organizationId)).sendWorkbenchMessage(input);
|
await (await organization(organizationId)).stopWorkspaceSession(input);
|
||||||
},
|
},
|
||||||
|
|
||||||
async stopWorkbenchSession(organizationId: string, input: TaskWorkbenchSessionInput): Promise<void> {
|
async closeWorkspaceSession(organizationId: string, input: TaskWorkspaceSessionInput): Promise<void> {
|
||||||
await (await organization(organizationId)).stopWorkbenchSession(input);
|
await (await organization(organizationId)).closeWorkspaceSession(input);
|
||||||
},
|
},
|
||||||
|
|
||||||
async closeWorkbenchSession(organizationId: string, input: TaskWorkbenchSessionInput): Promise<void> {
|
async publishWorkspacePr(organizationId: string, input: TaskWorkspaceSelectInput): Promise<void> {
|
||||||
await (await organization(organizationId)).closeWorkbenchSession(input);
|
await (await organization(organizationId)).publishWorkspacePr(input);
|
||||||
},
|
},
|
||||||
|
|
||||||
async publishWorkbenchPr(organizationId: string, input: TaskWorkbenchSelectInput): Promise<void> {
|
async revertWorkspaceFile(organizationId: string, input: TaskWorkspaceDiffInput): Promise<void> {
|
||||||
await (await organization(organizationId)).publishWorkbenchPr(input);
|
await (await organization(organizationId)).revertWorkspaceFile(input);
|
||||||
},
|
},
|
||||||
|
|
||||||
async revertWorkbenchFile(organizationId: string, input: TaskWorkbenchDiffInput): Promise<void> {
|
async adminReloadGithubOrganization(organizationId: string): Promise<void> {
|
||||||
await (await organization(organizationId)).revertWorkbenchFile(input);
|
await (await organization(organizationId)).adminReloadGithubOrganization();
|
||||||
},
|
},
|
||||||
|
|
||||||
async reloadGithubOrganization(organizationId: string): Promise<void> {
|
async adminReloadGithubPullRequests(organizationId: string): Promise<void> {
|
||||||
await (await organization(organizationId)).reloadGithubOrganization();
|
await (await organization(organizationId)).adminReloadGithubPullRequests();
|
||||||
},
|
},
|
||||||
|
|
||||||
async reloadGithubPullRequests(organizationId: string): Promise<void> {
|
async adminReloadGithubRepository(organizationId: string, repoId: string): Promise<void> {
|
||||||
await (await organization(organizationId)).reloadGithubPullRequests();
|
await (await organization(organizationId)).adminReloadGithubRepository({ repoId });
|
||||||
},
|
},
|
||||||
|
|
||||||
async reloadGithubRepository(organizationId: string, repoId: string): Promise<void> {
|
async adminReloadGithubPullRequest(organizationId: string, repoId: string, prNumber: number): Promise<void> {
|
||||||
await (await organization(organizationId)).reloadGithubRepository({ repoId });
|
await (await organization(organizationId)).adminReloadGithubPullRequest({ repoId, prNumber });
|
||||||
},
|
|
||||||
|
|
||||||
async reloadGithubPullRequest(organizationId: string, repoId: string, prNumber: number): Promise<void> {
|
|
||||||
await (await organization(organizationId)).reloadGithubPullRequest({ repoId, prNumber });
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async health(): Promise<{ ok: true }> {
|
async health(): Promise<{ ok: true }> {
|
||||||
|
|
|
||||||
|
|
@ -8,4 +8,4 @@ export * from "./subscription/use-subscription.js";
|
||||||
export * from "./keys.js";
|
export * from "./keys.js";
|
||||||
export * from "./mock-app.js";
|
export * from "./mock-app.js";
|
||||||
export * from "./view-model.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];
|
return ["org", organizationId, "sandbox", sandboxId];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function historyKey(organizationId: string, repoId: string): ActorKey {
|
export function auditLogKey(organizationId: string, repoId: string): ActorKey {
|
||||||
return ["org", organizationId, "repository", repoId, "history"];
|
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 { injectMockLatency } from "./mock/latency.js";
|
||||||
import rivetDevFixture from "../../../scripts/data/rivet-dev.json" with { type: "json" };
|
import rivetDevFixture from "../../../scripts/data/rivet-dev.json" with { type: "json" };
|
||||||
|
|
||||||
|
|
@ -16,6 +16,7 @@ export interface MockFoundryUser {
|
||||||
githubLogin: string;
|
githubLogin: string;
|
||||||
roleLabel: string;
|
roleLabel: string;
|
||||||
eligibleOrganizationIds: string[];
|
eligibleOrganizationIds: string[];
|
||||||
|
defaultModel: WorkspaceModelId;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MockFoundryOrganizationMember {
|
export interface MockFoundryOrganizationMember {
|
||||||
|
|
@ -61,7 +62,6 @@ export interface MockFoundryOrganizationSettings {
|
||||||
slug: string;
|
slug: string;
|
||||||
primaryDomain: string;
|
primaryDomain: string;
|
||||||
seatAccrualMode: "first_prompt";
|
seatAccrualMode: "first_prompt";
|
||||||
defaultModel: WorkbenchModelId;
|
|
||||||
autoImportRepos: boolean;
|
autoImportRepos: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -111,6 +111,7 @@ export interface MockFoundryAppClient {
|
||||||
skipStarterRepo(): Promise<void>;
|
skipStarterRepo(): Promise<void>;
|
||||||
starStarterRepo(organizationId: string): Promise<void>;
|
starStarterRepo(organizationId: string): Promise<void>;
|
||||||
selectOrganization(organizationId: string): Promise<void>;
|
selectOrganization(organizationId: string): Promise<void>;
|
||||||
|
setDefaultModel(model: WorkspaceModelId): Promise<void>;
|
||||||
updateOrganizationProfile(input: UpdateMockOrganizationProfileInput): Promise<void>;
|
updateOrganizationProfile(input: UpdateMockOrganizationProfileInput): Promise<void>;
|
||||||
triggerGithubSync(organizationId: string): Promise<void>;
|
triggerGithubSync(organizationId: string): Promise<void>;
|
||||||
completeHostedCheckout(organizationId: string, planId: MockBillingPlanId): Promise<void>;
|
completeHostedCheckout(organizationId: string, planId: MockBillingPlanId): Promise<void>;
|
||||||
|
|
@ -180,7 +181,6 @@ function buildRivetOrganization(): MockFoundryOrganization {
|
||||||
slug: "rivet",
|
slug: "rivet",
|
||||||
primaryDomain: "rivet.dev",
|
primaryDomain: "rivet.dev",
|
||||||
seatAccrualMode: "first_prompt",
|
seatAccrualMode: "first_prompt",
|
||||||
defaultModel: "gpt-5.3-codex",
|
|
||||||
autoImportRepos: true,
|
autoImportRepos: true,
|
||||||
},
|
},
|
||||||
github: {
|
github: {
|
||||||
|
|
@ -233,6 +233,7 @@ function buildDefaultSnapshot(): MockFoundryAppSnapshot {
|
||||||
githubLogin: "nathan",
|
githubLogin: "nathan",
|
||||||
roleLabel: "Founder",
|
roleLabel: "Founder",
|
||||||
eligibleOrganizationIds: ["personal-nathan", "acme", "rivet"],
|
eligibleOrganizationIds: ["personal-nathan", "acme", "rivet"],
|
||||||
|
defaultModel: "gpt-5.3-codex",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "user-maya",
|
id: "user-maya",
|
||||||
|
|
@ -241,6 +242,7 @@ function buildDefaultSnapshot(): MockFoundryAppSnapshot {
|
||||||
githubLogin: "maya",
|
githubLogin: "maya",
|
||||||
roleLabel: "Staff Engineer",
|
roleLabel: "Staff Engineer",
|
||||||
eligibleOrganizationIds: ["acme"],
|
eligibleOrganizationIds: ["acme"],
|
||||||
|
defaultModel: "claude-sonnet-4",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "user-jamie",
|
id: "user-jamie",
|
||||||
|
|
@ -249,6 +251,7 @@ function buildDefaultSnapshot(): MockFoundryAppSnapshot {
|
||||||
githubLogin: "jamie",
|
githubLogin: "jamie",
|
||||||
roleLabel: "Platform Lead",
|
roleLabel: "Platform Lead",
|
||||||
eligibleOrganizationIds: ["personal-jamie", "rivet"],
|
eligibleOrganizationIds: ["personal-jamie", "rivet"],
|
||||||
|
defaultModel: "claude-opus-4",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
organizations: [
|
organizations: [
|
||||||
|
|
@ -261,7 +264,6 @@ function buildDefaultSnapshot(): MockFoundryAppSnapshot {
|
||||||
slug: "nathan",
|
slug: "nathan",
|
||||||
primaryDomain: "personal",
|
primaryDomain: "personal",
|
||||||
seatAccrualMode: "first_prompt",
|
seatAccrualMode: "first_prompt",
|
||||||
defaultModel: "claude-sonnet-4",
|
|
||||||
autoImportRepos: true,
|
autoImportRepos: true,
|
||||||
},
|
},
|
||||||
github: {
|
github: {
|
||||||
|
|
@ -297,7 +299,6 @@ function buildDefaultSnapshot(): MockFoundryAppSnapshot {
|
||||||
slug: "acme",
|
slug: "acme",
|
||||||
primaryDomain: "acme.dev",
|
primaryDomain: "acme.dev",
|
||||||
seatAccrualMode: "first_prompt",
|
seatAccrualMode: "first_prompt",
|
||||||
defaultModel: "claude-sonnet-4",
|
|
||||||
autoImportRepos: true,
|
autoImportRepos: true,
|
||||||
},
|
},
|
||||||
github: {
|
github: {
|
||||||
|
|
@ -342,7 +343,6 @@ function buildDefaultSnapshot(): MockFoundryAppSnapshot {
|
||||||
slug: "jamie",
|
slug: "jamie",
|
||||||
primaryDomain: "personal",
|
primaryDomain: "personal",
|
||||||
seatAccrualMode: "first_prompt",
|
seatAccrualMode: "first_prompt",
|
||||||
defaultModel: "claude-opus-4",
|
|
||||||
autoImportRepos: true,
|
autoImportRepos: true,
|
||||||
},
|
},
|
||||||
github: {
|
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> {
|
async updateOrganizationProfile(input: UpdateMockOrganizationProfileInput): Promise<void> {
|
||||||
await this.injectAsyncLatency();
|
await this.injectAsyncLatency();
|
||||||
this.requireOrganization(input.organizationId);
|
this.requireOrganization(input.organizationId);
|
||||||
|
|
|
||||||
|
|
@ -6,25 +6,25 @@ import type {
|
||||||
SessionEvent,
|
SessionEvent,
|
||||||
TaskRecord,
|
TaskRecord,
|
||||||
TaskSummary,
|
TaskSummary,
|
||||||
TaskWorkbenchChangeModelInput,
|
TaskWorkspaceChangeModelInput,
|
||||||
TaskWorkbenchCreateTaskInput,
|
TaskWorkspaceCreateTaskInput,
|
||||||
TaskWorkbenchCreateTaskResponse,
|
TaskWorkspaceCreateTaskResponse,
|
||||||
TaskWorkbenchDiffInput,
|
TaskWorkspaceDiffInput,
|
||||||
TaskWorkbenchRenameInput,
|
TaskWorkspaceRenameInput,
|
||||||
TaskWorkbenchRenameSessionInput,
|
TaskWorkspaceRenameSessionInput,
|
||||||
TaskWorkbenchSelectInput,
|
TaskWorkspaceSelectInput,
|
||||||
TaskWorkbenchSetSessionUnreadInput,
|
TaskWorkspaceSetSessionUnreadInput,
|
||||||
TaskWorkbenchSendMessageInput,
|
TaskWorkspaceSendMessageInput,
|
||||||
TaskWorkbenchSnapshot,
|
TaskWorkspaceSnapshot,
|
||||||
TaskWorkbenchSessionInput,
|
TaskWorkspaceSessionInput,
|
||||||
TaskWorkbenchUpdateDraftInput,
|
TaskWorkspaceUpdateDraftInput,
|
||||||
TaskEvent,
|
TaskEvent,
|
||||||
WorkbenchSessionDetail,
|
WorkspaceSessionDetail,
|
||||||
WorkbenchTaskDetail,
|
WorkspaceTaskDetail,
|
||||||
WorkbenchTaskSummary,
|
WorkspaceTaskSummary,
|
||||||
OrganizationEvent,
|
OrganizationEvent,
|
||||||
OrganizationSummarySnapshot,
|
OrganizationSummarySnapshot,
|
||||||
HistoryEvent,
|
AuditLogEvent as HistoryEvent,
|
||||||
HistoryQueryInput,
|
HistoryQueryInput,
|
||||||
SandboxProviderId,
|
SandboxProviderId,
|
||||||
RepoOverview,
|
RepoOverview,
|
||||||
|
|
@ -34,7 +34,7 @@ import type {
|
||||||
} from "@sandbox-agent/foundry-shared";
|
} from "@sandbox-agent/foundry-shared";
|
||||||
import type { ProcessCreateRequest, ProcessLogFollowQuery, ProcessLogsResponse, ProcessSignalQuery } from "sandbox-agent";
|
import type { ProcessCreateRequest, ProcessLogFollowQuery, ProcessLogsResponse, ProcessSignalQuery } from "sandbox-agent";
|
||||||
import type { ActorConn, BackendClient, SandboxProcessRecord, SandboxSessionEventRecord, SandboxSessionRecord } from "../backend-client.js";
|
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 {
|
interface MockProcessRecord extends SandboxProcessRecord {
|
||||||
logText: string;
|
logText: string;
|
||||||
|
|
@ -89,7 +89,7 @@ function toTaskStatus(status: TaskRecord["status"], archived: boolean): TaskReco
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createMockBackendClient(defaultOrganizationId = "default"): BackendClient {
|
export function createMockBackendClient(defaultOrganizationId = "default"): BackendClient {
|
||||||
const workbench = getSharedMockWorkbenchClient();
|
const workspace = getSharedMockWorkspaceClient();
|
||||||
const listenersBySandboxId = new Map<string, Set<() => void>>();
|
const listenersBySandboxId = new Map<string, Set<() => void>>();
|
||||||
const processesBySandboxId = new Map<string, MockProcessRecord[]>();
|
const processesBySandboxId = new Map<string, MockProcessRecord[]>();
|
||||||
const connectionListeners = new Map<string, Set<(payload: any) => void>>();
|
const connectionListeners = new Map<string, Set<(payload: any) => void>>();
|
||||||
|
|
@ -97,7 +97,7 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
||||||
let nextProcessId = 1;
|
let nextProcessId = 1;
|
||||||
|
|
||||||
const requireTask = (taskId: string) => {
|
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) {
|
if (!task) {
|
||||||
throw new Error(`Unknown mock task ${taskId}`);
|
throw new Error(`Unknown mock task ${taskId}`);
|
||||||
}
|
}
|
||||||
|
|
@ -164,7 +164,7 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
||||||
async dispose(): Promise<void> {},
|
async dispose(): Promise<void> {},
|
||||||
});
|
});
|
||||||
|
|
||||||
const buildTaskSummary = (task: TaskWorkbenchSnapshot["tasks"][number]): WorkbenchTaskSummary => ({
|
const buildTaskSummary = (task: TaskWorkspaceSnapshot["tasks"][number]): WorkspaceTaskSummary => ({
|
||||||
id: task.id,
|
id: task.id,
|
||||||
repoId: task.repoId,
|
repoId: task.repoId,
|
||||||
title: task.title,
|
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),
|
...buildTaskSummary(task),
|
||||||
task: task.title,
|
task: task.title,
|
||||||
agentType: task.sessions[0]?.agent === "Codex" ? "codex" : "claude",
|
agentType: task.sessions[0]?.agent === "Codex" ? "codex" : "claude",
|
||||||
|
|
@ -211,7 +211,7 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
||||||
activeSandboxId: task.id,
|
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);
|
const tab = task.sessions.find((candidate) => candidate.id === sessionId);
|
||||||
if (!tab) {
|
if (!tab) {
|
||||||
throw new Error(`Unknown mock session ${sessionId} for task ${task.id}`);
|
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 buildOrganizationSummary = (): OrganizationSummarySnapshot => {
|
||||||
const snapshot = workbench.getSnapshot();
|
const snapshot = workspace.getSnapshot();
|
||||||
const taskSummaries = snapshot.tasks.map(buildTaskSummary);
|
const taskSummaries = snapshot.tasks.map(buildTaskSummary);
|
||||||
return {
|
return {
|
||||||
organizationId: defaultOrganizationId,
|
organizationId: defaultOrganizationId,
|
||||||
|
|
@ -256,20 +256,16 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
||||||
`sandbox:${organizationId}:${sandboxProviderId}:${sandboxId}`;
|
`sandbox:${organizationId}:${sandboxProviderId}:${sandboxId}`;
|
||||||
|
|
||||||
const emitOrganizationSnapshot = (): void => {
|
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", {
|
emitConnectionEvent(organizationScope(defaultOrganizationId), "organizationUpdated", {
|
||||||
type: "taskSummaryUpdated",
|
type: "organizationUpdated",
|
||||||
taskSummary: latestTask,
|
snapshot: buildOrganizationSummary(),
|
||||||
} satisfies OrganizationEvent);
|
} satisfies OrganizationEvent);
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const emitTaskUpdate = (taskId: string): void => {
|
const emitTaskUpdate = (taskId: string): void => {
|
||||||
const task = requireTask(taskId);
|
const task = requireTask(taskId);
|
||||||
emitConnectionEvent(taskScope(defaultOrganizationId, task.repoId, task.id), "taskUpdated", {
|
emitConnectionEvent(taskScope(defaultOrganizationId, task.repoId, task.id), "taskUpdated", {
|
||||||
type: "taskDetailUpdated",
|
type: "taskUpdated",
|
||||||
detail: buildTaskDetail(task),
|
detail: buildTaskDetail(task),
|
||||||
} satisfies TaskEvent);
|
} satisfies TaskEvent);
|
||||||
};
|
};
|
||||||
|
|
@ -400,6 +396,10 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
||||||
return unsupportedAppSnapshot();
|
return unsupportedAppSnapshot();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async setAppDefaultModel(): Promise<FoundryAppSnapshot> {
|
||||||
|
return unsupportedAppSnapshot();
|
||||||
|
},
|
||||||
|
|
||||||
async updateAppOrganizationProfile(): Promise<FoundryAppSnapshot> {
|
async updateAppOrganizationProfile(): Promise<FoundryAppSnapshot> {
|
||||||
return unsupportedAppSnapshot();
|
return unsupportedAppSnapshot();
|
||||||
},
|
},
|
||||||
|
|
@ -433,7 +433,7 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
||||||
},
|
},
|
||||||
|
|
||||||
async listRepos(_organizationId: string): Promise<RepoRecord[]> {
|
async listRepos(_organizationId: string): Promise<RepoRecord[]> {
|
||||||
return workbench.getSnapshot().repos.map((repo) => ({
|
return workspace.getSnapshot().repos.map((repo) => ({
|
||||||
organizationId: defaultOrganizationId,
|
organizationId: defaultOrganizationId,
|
||||||
repoId: repo.id,
|
repoId: repo.id,
|
||||||
remoteUrl: mockRepoRemote(repo.label),
|
remoteUrl: mockRepoRemote(repo.label),
|
||||||
|
|
@ -447,7 +447,7 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
||||||
},
|
},
|
||||||
|
|
||||||
async listTasks(_organizationId: string, repoId?: string): Promise<TaskSummary[]> {
|
async listTasks(_organizationId: string, repoId?: string): Promise<TaskSummary[]> {
|
||||||
return workbench
|
return workspace
|
||||||
.getSnapshot()
|
.getSnapshot()
|
||||||
.tasks.filter((task) => !repoId || task.repoId === repoId)
|
.tasks.filter((task) => !repoId || task.repoId === repoId)
|
||||||
.map((task) => ({
|
.map((task) => ({
|
||||||
|
|
@ -641,24 +641,24 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
||||||
return buildOrganizationSummary();
|
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));
|
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);
|
return buildSessionDetail(requireTask(taskId), sessionId);
|
||||||
},
|
},
|
||||||
|
|
||||||
async getWorkbench(): Promise<TaskWorkbenchSnapshot> {
|
async getWorkspace(): Promise<TaskWorkspaceSnapshot> {
|
||||||
return workbench.getSnapshot();
|
return workspace.getSnapshot();
|
||||||
},
|
},
|
||||||
|
|
||||||
subscribeWorkbench(_organizationId: string, listener: () => void): () => void {
|
subscribeWorkspace(_organizationId: string, listener: () => void): () => void {
|
||||||
return workbench.subscribe(listener);
|
return workspace.subscribe(listener);
|
||||||
},
|
},
|
||||||
|
|
||||||
async createWorkbenchTask(_organizationId: string, input: TaskWorkbenchCreateTaskInput): Promise<TaskWorkbenchCreateTaskResponse> {
|
async createWorkspaceTask(_organizationId: string, input: TaskWorkspaceCreateTaskInput): Promise<TaskWorkspaceCreateTaskResponse> {
|
||||||
const created = await workbench.createTask(input);
|
const created = await workspace.createTask(input);
|
||||||
emitOrganizationSnapshot();
|
emitOrganizationSnapshot();
|
||||||
emitTaskUpdate(created.taskId);
|
emitTaskUpdate(created.taskId);
|
||||||
if (created.sessionId) {
|
if (created.sessionId) {
|
||||||
|
|
@ -667,99 +667,93 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
||||||
return created;
|
return created;
|
||||||
},
|
},
|
||||||
|
|
||||||
async markWorkbenchUnread(_organizationId: string, input: TaskWorkbenchSelectInput): Promise<void> {
|
async markWorkspaceUnread(_organizationId: string, input: TaskWorkspaceSelectInput): Promise<void> {
|
||||||
await workbench.markTaskUnread(input);
|
await workspace.markTaskUnread(input);
|
||||||
emitOrganizationSnapshot();
|
emitOrganizationSnapshot();
|
||||||
emitTaskUpdate(input.taskId);
|
emitTaskUpdate(input.taskId);
|
||||||
},
|
},
|
||||||
|
|
||||||
async renameWorkbenchTask(_organizationId: string, input: TaskWorkbenchRenameInput): Promise<void> {
|
async renameWorkspaceTask(_organizationId: string, input: TaskWorkspaceRenameInput): Promise<void> {
|
||||||
await workbench.renameTask(input);
|
await workspace.renameTask(input);
|
||||||
emitOrganizationSnapshot();
|
emitOrganizationSnapshot();
|
||||||
emitTaskUpdate(input.taskId);
|
emitTaskUpdate(input.taskId);
|
||||||
},
|
},
|
||||||
|
|
||||||
async renameWorkbenchBranch(_organizationId: string, input: TaskWorkbenchRenameInput): Promise<void> {
|
async createWorkspaceSession(_organizationId: string, input: TaskWorkspaceSelectInput & { model?: string }): Promise<{ sessionId: string }> {
|
||||||
await workbench.renameBranch(input);
|
const created = await workspace.addSession(input);
|
||||||
emitOrganizationSnapshot();
|
|
||||||
emitTaskUpdate(input.taskId);
|
|
||||||
},
|
|
||||||
|
|
||||||
async createWorkbenchSession(_organizationId: string, input: TaskWorkbenchSelectInput & { model?: string }): Promise<{ sessionId: string }> {
|
|
||||||
const created = await workbench.addSession(input);
|
|
||||||
emitOrganizationSnapshot();
|
emitOrganizationSnapshot();
|
||||||
emitTaskUpdate(input.taskId);
|
emitTaskUpdate(input.taskId);
|
||||||
emitSessionUpdate(input.taskId, created.sessionId);
|
emitSessionUpdate(input.taskId, created.sessionId);
|
||||||
return created;
|
return created;
|
||||||
},
|
},
|
||||||
|
|
||||||
async renameWorkbenchSession(_organizationId: string, input: TaskWorkbenchRenameSessionInput): Promise<void> {
|
async renameWorkspaceSession(_organizationId: string, input: TaskWorkspaceRenameSessionInput): Promise<void> {
|
||||||
await workbench.renameSession(input);
|
await workspace.renameSession(input);
|
||||||
emitOrganizationSnapshot();
|
emitOrganizationSnapshot();
|
||||||
emitTaskUpdate(input.taskId);
|
emitTaskUpdate(input.taskId);
|
||||||
emitSessionUpdate(input.taskId, input.sessionId);
|
emitSessionUpdate(input.taskId, input.sessionId);
|
||||||
},
|
},
|
||||||
|
|
||||||
async setWorkbenchSessionUnread(_organizationId: string, input: TaskWorkbenchSetSessionUnreadInput): Promise<void> {
|
async setWorkspaceSessionUnread(_organizationId: string, input: TaskWorkspaceSetSessionUnreadInput): Promise<void> {
|
||||||
await workbench.setSessionUnread(input);
|
await workspace.setSessionUnread(input);
|
||||||
emitOrganizationSnapshot();
|
emitOrganizationSnapshot();
|
||||||
emitTaskUpdate(input.taskId);
|
emitTaskUpdate(input.taskId);
|
||||||
emitSessionUpdate(input.taskId, input.sessionId);
|
emitSessionUpdate(input.taskId, input.sessionId);
|
||||||
},
|
},
|
||||||
|
|
||||||
async updateWorkbenchDraft(_organizationId: string, input: TaskWorkbenchUpdateDraftInput): Promise<void> {
|
async updateWorkspaceDraft(_organizationId: string, input: TaskWorkspaceUpdateDraftInput): Promise<void> {
|
||||||
await workbench.updateDraft(input);
|
await workspace.updateDraft(input);
|
||||||
emitOrganizationSnapshot();
|
emitOrganizationSnapshot();
|
||||||
emitTaskUpdate(input.taskId);
|
emitTaskUpdate(input.taskId);
|
||||||
emitSessionUpdate(input.taskId, input.sessionId);
|
emitSessionUpdate(input.taskId, input.sessionId);
|
||||||
},
|
},
|
||||||
|
|
||||||
async changeWorkbenchModel(_organizationId: string, input: TaskWorkbenchChangeModelInput): Promise<void> {
|
async changeWorkspaceModel(_organizationId: string, input: TaskWorkspaceChangeModelInput): Promise<void> {
|
||||||
await workbench.changeModel(input);
|
await workspace.changeModel(input);
|
||||||
emitOrganizationSnapshot();
|
emitOrganizationSnapshot();
|
||||||
emitTaskUpdate(input.taskId);
|
emitTaskUpdate(input.taskId);
|
||||||
emitSessionUpdate(input.taskId, input.sessionId);
|
emitSessionUpdate(input.taskId, input.sessionId);
|
||||||
},
|
},
|
||||||
|
|
||||||
async sendWorkbenchMessage(_organizationId: string, input: TaskWorkbenchSendMessageInput): Promise<void> {
|
async sendWorkspaceMessage(_organizationId: string, input: TaskWorkspaceSendMessageInput): Promise<void> {
|
||||||
await workbench.sendMessage(input);
|
await workspace.sendMessage(input);
|
||||||
emitOrganizationSnapshot();
|
emitOrganizationSnapshot();
|
||||||
emitTaskUpdate(input.taskId);
|
emitTaskUpdate(input.taskId);
|
||||||
emitSessionUpdate(input.taskId, input.sessionId);
|
emitSessionUpdate(input.taskId, input.sessionId);
|
||||||
},
|
},
|
||||||
|
|
||||||
async stopWorkbenchSession(_organizationId: string, input: TaskWorkbenchSessionInput): Promise<void> {
|
async stopWorkspaceSession(_organizationId: string, input: TaskWorkspaceSessionInput): Promise<void> {
|
||||||
await workbench.stopAgent(input);
|
await workspace.stopAgent(input);
|
||||||
emitOrganizationSnapshot();
|
emitOrganizationSnapshot();
|
||||||
emitTaskUpdate(input.taskId);
|
emitTaskUpdate(input.taskId);
|
||||||
emitSessionUpdate(input.taskId, input.sessionId);
|
emitSessionUpdate(input.taskId, input.sessionId);
|
||||||
},
|
},
|
||||||
|
|
||||||
async closeWorkbenchSession(_organizationId: string, input: TaskWorkbenchSessionInput): Promise<void> {
|
async closeWorkspaceSession(_organizationId: string, input: TaskWorkspaceSessionInput): Promise<void> {
|
||||||
await workbench.closeSession(input);
|
await workspace.closeSession(input);
|
||||||
emitOrganizationSnapshot();
|
emitOrganizationSnapshot();
|
||||||
emitTaskUpdate(input.taskId);
|
emitTaskUpdate(input.taskId);
|
||||||
},
|
},
|
||||||
|
|
||||||
async publishWorkbenchPr(_organizationId: string, input: TaskWorkbenchSelectInput): Promise<void> {
|
async publishWorkspacePr(_organizationId: string, input: TaskWorkspaceSelectInput): Promise<void> {
|
||||||
await workbench.publishPr(input);
|
await workspace.publishPr(input);
|
||||||
emitOrganizationSnapshot();
|
emitOrganizationSnapshot();
|
||||||
emitTaskUpdate(input.taskId);
|
emitTaskUpdate(input.taskId);
|
||||||
},
|
},
|
||||||
|
|
||||||
async revertWorkbenchFile(_organizationId: string, input: TaskWorkbenchDiffInput): Promise<void> {
|
async revertWorkspaceFile(_organizationId: string, input: TaskWorkspaceDiffInput): Promise<void> {
|
||||||
await workbench.revertFile(input);
|
await workspace.revertFile(input);
|
||||||
emitOrganizationSnapshot();
|
emitOrganizationSnapshot();
|
||||||
emitTaskUpdate(input.taskId);
|
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 }> {
|
async health(): Promise<{ ok: true }> {
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,33 @@
|
||||||
import {
|
import {
|
||||||
MODEL_GROUPS,
|
MODEL_GROUPS,
|
||||||
buildInitialMockLayoutViewModel,
|
buildInitialMockLayoutViewModel,
|
||||||
groupWorkbenchRepositories,
|
groupWorkspaceRepositories,
|
||||||
nowMs,
|
nowMs,
|
||||||
providerAgent,
|
providerAgent,
|
||||||
randomReply,
|
randomReply,
|
||||||
removeFileTreePath,
|
removeFileTreePath,
|
||||||
slugify,
|
slugify,
|
||||||
uid,
|
uid,
|
||||||
} from "../workbench-model.js";
|
} from "../workspace-model.js";
|
||||||
import type {
|
import type {
|
||||||
TaskWorkbenchAddSessionResponse,
|
TaskWorkspaceAddSessionResponse,
|
||||||
TaskWorkbenchChangeModelInput,
|
TaskWorkspaceChangeModelInput,
|
||||||
TaskWorkbenchCreateTaskInput,
|
TaskWorkspaceCreateTaskInput,
|
||||||
TaskWorkbenchCreateTaskResponse,
|
TaskWorkspaceCreateTaskResponse,
|
||||||
TaskWorkbenchDiffInput,
|
TaskWorkspaceDiffInput,
|
||||||
TaskWorkbenchRenameInput,
|
TaskWorkspaceRenameInput,
|
||||||
TaskWorkbenchRenameSessionInput,
|
TaskWorkspaceRenameSessionInput,
|
||||||
TaskWorkbenchSelectInput,
|
TaskWorkspaceSelectInput,
|
||||||
TaskWorkbenchSetSessionUnreadInput,
|
TaskWorkspaceSetSessionUnreadInput,
|
||||||
TaskWorkbenchSendMessageInput,
|
TaskWorkspaceSendMessageInput,
|
||||||
TaskWorkbenchSnapshot,
|
TaskWorkspaceSnapshot,
|
||||||
TaskWorkbenchSessionInput,
|
TaskWorkspaceSessionInput,
|
||||||
TaskWorkbenchUpdateDraftInput,
|
TaskWorkspaceUpdateDraftInput,
|
||||||
WorkbenchSession as AgentSession,
|
WorkspaceSession as AgentSession,
|
||||||
WorkbenchTask as Task,
|
WorkspaceTask as Task,
|
||||||
WorkbenchTranscriptEvent as TranscriptEvent,
|
WorkspaceTranscriptEvent as TranscriptEvent,
|
||||||
} from "@sandbox-agent/foundry-shared";
|
} from "@sandbox-agent/foundry-shared";
|
||||||
import type { TaskWorkbenchClient } from "../workbench-client.js";
|
import type { TaskWorkspaceClient } from "../workspace-client.js";
|
||||||
|
|
||||||
function buildTranscriptEvent(params: {
|
function buildTranscriptEvent(params: {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
|
|
@ -47,12 +47,12 @@ function buildTranscriptEvent(params: {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
class MockWorkbenchStore implements TaskWorkbenchClient {
|
class MockWorkspaceStore implements TaskWorkspaceClient {
|
||||||
private snapshot = buildInitialMockLayoutViewModel();
|
private snapshot = buildInitialMockLayoutViewModel();
|
||||||
private listeners = new Set<() => void>();
|
private listeners = new Set<() => void>();
|
||||||
private pendingTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
private pendingTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
||||||
|
|
||||||
getSnapshot(): TaskWorkbenchSnapshot {
|
getSnapshot(): TaskWorkspaceSnapshot {
|
||||||
return this.snapshot;
|
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 id = uid();
|
||||||
const sessionId = `session-${id}`;
|
const sessionId = `session-${id}`;
|
||||||
const repo = this.snapshot.repos.find((candidate) => candidate.id === input.repoId);
|
const repo = this.snapshot.repos.find((candidate) => candidate.id === input.repoId);
|
||||||
|
|
@ -109,7 +109,7 @@ class MockWorkbenchStore implements TaskWorkbenchClient {
|
||||||
return { taskId: id, sessionId };
|
return { taskId: id, sessionId };
|
||||||
}
|
}
|
||||||
|
|
||||||
async markTaskUnread(input: TaskWorkbenchSelectInput): Promise<void> {
|
async markTaskUnread(input: TaskWorkspaceSelectInput): Promise<void> {
|
||||||
this.updateTask(input.taskId, (task) => {
|
this.updateTask(input.taskId, (task) => {
|
||||||
const targetSession = task.sessions[task.sessions.length - 1] ?? null;
|
const targetSession = task.sessions[task.sessions.length - 1] ?? null;
|
||||||
if (!targetSession) {
|
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();
|
const value = input.value.trim();
|
||||||
if (!value) {
|
if (!value) {
|
||||||
throw new Error(`Cannot rename task ${input.taskId} to an empty title`);
|
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() }));
|
this.updateTask(input.taskId, (task) => ({ ...task, title: value, updatedAtMs: nowMs() }));
|
||||||
}
|
}
|
||||||
|
|
||||||
async renameBranch(input: TaskWorkbenchRenameInput): Promise<void> {
|
async archiveTask(input: TaskWorkspaceSelectInput): 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> {
|
|
||||||
this.updateTask(input.taskId, (task) => ({ ...task, status: "archived", updatedAtMs: nowMs() }));
|
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;
|
const nextPrNumber = Math.max(0, ...this.snapshot.tasks.map((task) => task.pullRequest?.number ?? 0)) + 1;
|
||||||
this.updateTask(input.taskId, (task) => ({
|
this.updateTask(input.taskId, (task) => ({
|
||||||
...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) => {
|
this.updateTask(input.taskId, (task) => {
|
||||||
const file = task.fileChanges.find((entry) => entry.path === input.path);
|
const file = task.fileChanges.find((entry) => entry.path === input.path);
|
||||||
const nextDiffs = { ...task.diffs };
|
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.assertSession(input.taskId, input.sessionId);
|
||||||
this.updateTask(input.taskId, (task) => ({
|
this.updateTask(input.taskId, (task) => ({
|
||||||
...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();
|
const text = input.text.trim();
|
||||||
if (!text) {
|
if (!text) {
|
||||||
throw new Error(`Cannot send an empty mock prompt for task ${input.taskId}`);
|
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);
|
this.pendingTimers.set(input.sessionId, timer);
|
||||||
}
|
}
|
||||||
|
|
||||||
async stopAgent(input: TaskWorkbenchSessionInput): Promise<void> {
|
async stopAgent(input: TaskWorkspaceSessionInput): Promise<void> {
|
||||||
this.assertSession(input.taskId, input.sessionId);
|
this.assertSession(input.taskId, input.sessionId);
|
||||||
const existing = this.pendingTimers.get(input.sessionId);
|
const existing = this.pendingTimers.get(input.sessionId);
|
||||||
if (existing) {
|
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) => ({
|
this.updateTask(input.taskId, (currentTask) => ({
|
||||||
...currentTask,
|
...currentTask,
|
||||||
sessions: currentTask.sessions.map((candidate) => (candidate.id === input.sessionId ? { ...candidate, unread: input.unread } : candidate)),
|
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();
|
const title = input.title.trim();
|
||||||
if (!title) {
|
if (!title) {
|
||||||
throw new Error(`Cannot rename session ${input.sessionId} to an empty 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) => {
|
this.updateTask(input.taskId, (currentTask) => {
|
||||||
if (currentTask.sessions.length <= 1) {
|
if (currentTask.sessions.length <= 1) {
|
||||||
return currentTask;
|
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);
|
this.assertTask(input.taskId);
|
||||||
const nextSessionId = uid();
|
const nextSessionId = uid();
|
||||||
const nextSession: AgentSession = {
|
const nextSession: AgentSession = {
|
||||||
|
|
@ -368,7 +360,7 @@ class MockWorkbenchStore implements TaskWorkbenchClient {
|
||||||
return { sessionId: nextSession.id };
|
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));
|
const group = MODEL_GROUPS.find((candidate) => candidate.models.some((entry) => entry.id === input.model));
|
||||||
if (!group) {
|
if (!group) {
|
||||||
throw new Error(`Unable to resolve model provider for ${input.model}`);
|
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);
|
const nextSnapshot = updater(this.snapshot);
|
||||||
this.snapshot = {
|
this.snapshot = {
|
||||||
...nextSnapshot,
|
...nextSnapshot,
|
||||||
repositories: groupWorkbenchRepositories(nextSnapshot.repos, nextSnapshot.tasks),
|
repositories: groupWorkspaceRepositories(nextSnapshot.repos, nextSnapshot.tasks),
|
||||||
};
|
};
|
||||||
this.notify();
|
this.notify();
|
||||||
}
|
}
|
||||||
|
|
@ -436,11 +428,11 @@ function candidateEventIndex(task: Task, sessionId: string): number {
|
||||||
return (session?.transcript.length ?? 0) + 1;
|
return (session?.transcript.length ?? 0) + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
let sharedMockWorkbenchClient: TaskWorkbenchClient | null = null;
|
let sharedMockWorkspaceClient: TaskWorkspaceClient | null = null;
|
||||||
|
|
||||||
export function getSharedMockWorkbenchClient(): TaskWorkbenchClient {
|
export function getSharedMockWorkspaceClient(): TaskWorkspaceClient {
|
||||||
if (!sharedMockWorkbenchClient) {
|
if (!sharedMockWorkspaceClient) {
|
||||||
sharedMockWorkbenchClient = new MockWorkbenchStore();
|
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 { BackendClient } from "../backend-client.js";
|
||||||
import type { FoundryAppClient } from "../app-client.js";
|
import type { FoundryAppClient } from "../app-client.js";
|
||||||
|
|
||||||
|
|
@ -72,6 +72,11 @@ class RemoteFoundryAppStore implements FoundryAppClient {
|
||||||
this.notify();
|
this.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setDefaultModel(model: WorkspaceModelId): Promise<void> {
|
||||||
|
this.snapshot = await this.backend.setAppDefaultModel(model);
|
||||||
|
this.notify();
|
||||||
|
}
|
||||||
|
|
||||||
async updateOrganizationProfile(input: UpdateFoundryOrganizationProfileInput): Promise<void> {
|
async updateOrganizationProfile(input: UpdateFoundryOrganizationProfileInput): Promise<void> {
|
||||||
this.snapshot = await this.backend.updateAppOrganizationProfile(input);
|
this.snapshot = await this.backend.updateAppOrganizationProfile(input);
|
||||||
this.notify();
|
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,
|
SandboxProcessesEvent,
|
||||||
SessionEvent,
|
SessionEvent,
|
||||||
TaskEvent,
|
TaskEvent,
|
||||||
WorkbenchSessionDetail,
|
WorkspaceSessionDetail,
|
||||||
WorkbenchTaskDetail,
|
WorkspaceTaskDetail,
|
||||||
OrganizationEvent,
|
OrganizationEvent,
|
||||||
OrganizationSummarySnapshot,
|
OrganizationSummarySnapshot,
|
||||||
} from "@sandbox-agent/foundry-shared";
|
} from "@sandbox-agent/foundry-shared";
|
||||||
|
|
@ -48,16 +48,6 @@ export interface SandboxProcessesTopicParams {
|
||||||
sandboxId: string;
|
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 = {
|
export const topicDefinitions = {
|
||||||
app: {
|
app: {
|
||||||
key: () => "app",
|
key: () => "app",
|
||||||
|
|
@ -72,41 +62,7 @@ export const topicDefinitions = {
|
||||||
event: "organizationUpdated",
|
event: "organizationUpdated",
|
||||||
connect: (backend: BackendClient, params: OrganizationTopicParams) => backend.connectOrganization(params.organizationId),
|
connect: (backend: BackendClient, params: OrganizationTopicParams) => backend.connectOrganization(params.organizationId),
|
||||||
fetchInitial: (backend: BackendClient, params: OrganizationTopicParams) => backend.getOrganizationSummary(params.organizationId),
|
fetchInitial: (backend: BackendClient, params: OrganizationTopicParams) => backend.getOrganizationSummary(params.organizationId),
|
||||||
applyEvent: (current: OrganizationSummarySnapshot, event: OrganizationEvent) => {
|
applyEvent: (_current: OrganizationSummarySnapshot, event: OrganizationEvent) => event.snapshot,
|
||||||
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),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
} satisfies TopicDefinition<OrganizationSummarySnapshot, OrganizationTopicParams, OrganizationEvent>,
|
} satisfies TopicDefinition<OrganizationSummarySnapshot, OrganizationTopicParams, OrganizationEvent>,
|
||||||
|
|
||||||
task: {
|
task: {
|
||||||
|
|
@ -114,8 +70,8 @@ export const topicDefinitions = {
|
||||||
event: "taskUpdated",
|
event: "taskUpdated",
|
||||||
connect: (backend: BackendClient, params: TaskTopicParams) => backend.connectTask(params.organizationId, params.repoId, params.taskId),
|
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),
|
fetchInitial: (backend: BackendClient, params: TaskTopicParams) => backend.getTaskDetail(params.organizationId, params.repoId, params.taskId),
|
||||||
applyEvent: (_current: WorkbenchTaskDetail, event: TaskEvent) => event.detail,
|
applyEvent: (_current: WorkspaceTaskDetail, event: TaskEvent) => event.detail,
|
||||||
} satisfies TopicDefinition<WorkbenchTaskDetail, TaskTopicParams, TaskEvent>,
|
} satisfies TopicDefinition<WorkspaceTaskDetail, TaskTopicParams, TaskEvent>,
|
||||||
|
|
||||||
session: {
|
session: {
|
||||||
key: (params: SessionTopicParams) => `session:${params.organizationId}:${params.taskId}:${params.sessionId}`,
|
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),
|
connect: (backend: BackendClient, params: SessionTopicParams) => backend.connectTask(params.organizationId, params.repoId, params.taskId),
|
||||||
fetchInitial: (backend: BackendClient, params: SessionTopicParams) =>
|
fetchInitial: (backend: BackendClient, params: SessionTopicParams) =>
|
||||||
backend.getSessionDetail(params.organizationId, params.repoId, params.taskId, params.sessionId),
|
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) {
|
if (event.session.sessionId !== current.sessionId) {
|
||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
return event.session;
|
return event.session;
|
||||||
},
|
},
|
||||||
} satisfies TopicDefinition<WorkbenchSessionDetail, SessionTopicParams, SessionEvent>,
|
} satisfies TopicDefinition<WorkspaceSessionDetail, SessionTopicParams, SessionEvent>,
|
||||||
|
|
||||||
sandboxProcesses: {
|
sandboxProcesses: {
|
||||||
key: (params: SandboxProcessesTopicParams) => `sandbox:${params.organizationId}:${params.sandboxProviderId}:${params.sandboxId}`,
|
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 {
|
import type {
|
||||||
WorkbenchAgentKind as AgentKind,
|
WorkspaceAgentKind as AgentKind,
|
||||||
WorkbenchSession as AgentSession,
|
WorkspaceSession as AgentSession,
|
||||||
WorkbenchDiffLineKind as DiffLineKind,
|
WorkspaceDiffLineKind as DiffLineKind,
|
||||||
WorkbenchFileTreeNode as FileTreeNode,
|
WorkspaceFileTreeNode as FileTreeNode,
|
||||||
WorkbenchTask as Task,
|
WorkspaceTask as Task,
|
||||||
TaskWorkbenchSnapshot,
|
TaskWorkspaceSnapshot,
|
||||||
WorkbenchHistoryEvent as HistoryEvent,
|
WorkspaceHistoryEvent as HistoryEvent,
|
||||||
WorkbenchModelGroup as ModelGroup,
|
WorkspaceModelGroup as ModelGroup,
|
||||||
WorkbenchModelId as ModelId,
|
WorkspaceModelId as ModelId,
|
||||||
WorkbenchParsedDiffLine as ParsedDiffLine,
|
WorkspaceParsedDiffLine as ParsedDiffLine,
|
||||||
WorkbenchRepositorySection,
|
WorkspaceRepositorySection,
|
||||||
WorkbenchRepo,
|
WorkspaceRepo,
|
||||||
WorkbenchTranscriptEvent as TranscriptEvent,
|
WorkspaceTranscriptEvent as TranscriptEvent,
|
||||||
} from "@sandbox-agent/foundry-shared";
|
} from "@sandbox-agent/foundry-shared";
|
||||||
import rivetDevFixture from "../../../scripts/data/rivet-dev.json" with { type: "json" };
|
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
|
* Uses real public repos so the mock sidebar matches what an actual rivet-dev
|
||||||
* organization would show after a GitHub sync.
|
* organization would show after a GitHub sync.
|
||||||
*/
|
*/
|
||||||
function buildMockRepos(): WorkbenchRepo[] {
|
function buildMockRepos(): WorkspaceRepo[] {
|
||||||
return rivetDevFixture.repos.map((r) => ({
|
return rivetDevFixture.repos.map((r) => ({
|
||||||
id: repoIdFromFullName(r.fullName),
|
id: repoIdFromFullName(r.fullName),
|
||||||
label: r.fullName,
|
label: r.fullName,
|
||||||
|
|
@ -1349,19 +1349,19 @@ function buildPrTasks(): Task[] {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildInitialMockLayoutViewModel(): TaskWorkbenchSnapshot {
|
export function buildInitialMockLayoutViewModel(): TaskWorkspaceSnapshot {
|
||||||
const repos = buildMockRepos();
|
const repos = buildMockRepos();
|
||||||
const tasks = [...buildInitialTasks(), ...buildPrTasks()];
|
const tasks = [...buildInitialTasks(), ...buildPrTasks()];
|
||||||
return {
|
return {
|
||||||
organizationId: "default",
|
organizationId: "default",
|
||||||
repos,
|
repos,
|
||||||
repositories: groupWorkbenchRepositories(repos, tasks),
|
repositories: groupWorkspaceRepositories(repos, tasks),
|
||||||
tasks,
|
tasks,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function groupWorkbenchRepositories(repos: WorkbenchRepo[], tasks: Task[]): WorkbenchRepositorySection[] {
|
export function groupWorkspaceRepositories(repos: WorkspaceRepo[], tasks: Task[]): WorkspaceRepositorySection[] {
|
||||||
const grouped = new Map<string, WorkbenchRepositorySection>();
|
const grouped = new Map<string, WorkspaceRepositorySection>();
|
||||||
|
|
||||||
for (const repo of repos) {
|
for (const repo of repos) {
|
||||||
grouped.set(repo.id, {
|
grouped.set(repo.id, {
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import { describe, expect, it } from "vitest";
|
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 { createBackendClient } from "../../src/backend-client.js";
|
||||||
import { requireImportedRepo } from "./helpers.js";
|
import { requireImportedRepo } from "./helpers.js";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { describe, expect, it } from "vitest";
|
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 { createBackendClient } from "../../src/backend-client.js";
|
||||||
import { requireImportedRepo } from "./helpers.js";
|
import { requireImportedRepo } from "./helpers.js";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { describe, expect, it } from "vitest";
|
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 { createBackendClient } from "../../src/backend-client.js";
|
||||||
import { requireImportedRepo } from "./helpers.js";
|
import { requireImportedRepo } from "./helpers.js";
|
||||||
|
|
||||||
|
|
@ -13,7 +13,7 @@ function requiredEnv(name: string): string {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function workbenchModelEnv(name: string, fallback: WorkbenchModelId): WorkbenchModelId {
|
function workspaceModelEnv(name: string, fallback: WorkspaceModelId): WorkspaceModelId {
|
||||||
const value = process.env[name]?.trim();
|
const value = process.env[name]?.trim();
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case "claude-sonnet-4":
|
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);
|
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
||||||
if (!task) {
|
if (!task) {
|
||||||
throw new Error(`task ${taskId} missing from snapshot`);
|
throw new Error(`task ${taskId} missing from snapshot`);
|
||||||
|
|
@ -58,7 +58,7 @@ function findTask(snapshot: TaskWorkbenchSnapshot, taskId: string): WorkbenchTas
|
||||||
return task;
|
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);
|
const tab = task.sessions.find((candidate) => candidate.id === sessionId);
|
||||||
if (!tab) {
|
if (!tab) {
|
||||||
throw new Error(`tab ${sessionId} missing from task ${task.id}`);
|
throw new Error(`tab ${sessionId} missing from task ${task.id}`);
|
||||||
|
|
@ -66,7 +66,7 @@ function findTab(task: WorkbenchTask, sessionId: string): WorkbenchSession {
|
||||||
return tab;
|
return tab;
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractEventText(event: WorkbenchTranscriptEvent): string {
|
function extractEventText(event: WorkspaceTranscriptEvent): string {
|
||||||
const payload = event.payload;
|
const payload = event.payload;
|
||||||
if (!payload || typeof payload !== "object") {
|
if (!payload || typeof payload !== "object") {
|
||||||
return String(payload ?? "");
|
return String(payload ?? "");
|
||||||
|
|
@ -127,7 +127,7 @@ function extractEventText(event: WorkbenchTranscriptEvent): string {
|
||||||
return JSON.stringify(payload);
|
return JSON.stringify(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
function transcriptIncludesAgentText(transcript: WorkbenchTranscriptEvent[], expectedText: string): boolean {
|
function transcriptIncludesAgentText(transcript: WorkspaceTranscriptEvent[], expectedText: string): boolean {
|
||||||
return transcript
|
return transcript
|
||||||
.filter((event) => event.sender === "agent")
|
.filter((event) => event.sender === "agent")
|
||||||
.map((event) => extractEventText(event))
|
.map((event) => extractEventText(event))
|
||||||
|
|
@ -135,15 +135,15 @@ function transcriptIncludesAgentText(transcript: WorkbenchTranscriptEvent[], exp
|
||||||
.includes(expectedText);
|
.includes(expectedText);
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("e2e(client): workbench flows", () => {
|
describe("e2e(client): workspace flows", () => {
|
||||||
it.skipIf(!RUN_WORKBENCH_E2E)(
|
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 },
|
{ timeout: 20 * 60_000 },
|
||||||
async () => {
|
async () => {
|
||||||
const endpoint = process.env.HF_E2E_BACKEND_ENDPOINT?.trim() || "http://127.0.0.1:7741/v1/rivet";
|
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 organizationId = process.env.HF_E2E_WORKSPACE?.trim() || "default";
|
||||||
const repoRemote = requiredEnv("HF_E2E_GITHUB_REPO");
|
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 runId = `wb-${Date.now().toString(36)}`;
|
||||||
const expectedFile = `${runId}.txt`;
|
const expectedFile = `${runId}.txt`;
|
||||||
const expectedInitialReply = `WORKBENCH_READY_${runId}`;
|
const expectedInitialReply = `WORKBENCH_READY_${runId}`;
|
||||||
|
|
@ -155,9 +155,9 @@ describe("e2e(client): workbench flows", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const repo = await requireImportedRepo(client, organizationId, repoRemote);
|
const repo = await requireImportedRepo(client, organizationId, repoRemote);
|
||||||
const created = await client.createWorkbenchTask(organizationId, {
|
const created = await client.createWorkspaceTask(organizationId, {
|
||||||
repoId: repo.repoId,
|
repoId: repo.repoId,
|
||||||
title: `Workbench E2E ${runId}`,
|
title: `Workspace E2E ${runId}`,
|
||||||
branch: `e2e/${runId}`,
|
branch: `e2e/${runId}`,
|
||||||
model,
|
model,
|
||||||
task: `Reply with exactly: ${expectedInitialReply}`,
|
task: `Reply with exactly: ${expectedInitialReply}`,
|
||||||
|
|
@ -167,7 +167,7 @@ describe("e2e(client): workbench flows", () => {
|
||||||
"task provisioning",
|
"task provisioning",
|
||||||
12 * 60_000,
|
12 * 60_000,
|
||||||
2_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,
|
(task) => task.branch === `e2e/${runId}` && task.sessions.length > 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -177,7 +177,7 @@ describe("e2e(client): workbench flows", () => {
|
||||||
"initial agent response",
|
"initial agent response",
|
||||||
12 * 60_000,
|
12 * 60_000,
|
||||||
2_000,
|
2_000,
|
||||||
async () => findTask(await client.getWorkbench(organizationId), created.taskId),
|
async () => findTask(await client.getWorkspace(organizationId), created.taskId),
|
||||||
(task) => {
|
(task) => {
|
||||||
const tab = findTab(task, primaryTab.id);
|
const tab = findTab(task, primaryTab.id);
|
||||||
return task.status === "idle" && tab.status === "idle" && transcriptIncludesAgentText(tab.transcript, expectedInitialReply);
|
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(findTab(initialCompleted, primaryTab.id).sessionId).toBeTruthy();
|
||||||
expect(transcriptIncludesAgentText(findTab(initialCompleted, primaryTab.id).transcript, expectedInitialReply)).toBe(true);
|
expect(transcriptIncludesAgentText(findTab(initialCompleted, primaryTab.id).transcript, expectedInitialReply)).toBe(true);
|
||||||
|
|
||||||
await client.renameWorkbenchTask(organizationId, {
|
await client.renameWorkspaceTask(organizationId, {
|
||||||
taskId: created.taskId,
|
taskId: created.taskId,
|
||||||
value: `Workbench E2E ${runId} Renamed`,
|
value: `Workspace E2E ${runId} Renamed`,
|
||||||
});
|
});
|
||||||
await client.renameWorkbenchSession(organizationId, {
|
await client.renameWorkspaceSession(organizationId, {
|
||||||
taskId: created.taskId,
|
taskId: created.taskId,
|
||||||
sessionId: primaryTab.id,
|
sessionId: primaryTab.id,
|
||||||
title: "Primary Session",
|
title: "Primary Session",
|
||||||
});
|
});
|
||||||
|
|
||||||
const secondTab = await client.createWorkbenchSession(organizationId, {
|
const secondTab = await client.createWorkspaceSession(organizationId, {
|
||||||
taskId: created.taskId,
|
taskId: created.taskId,
|
||||||
model,
|
model,
|
||||||
});
|
});
|
||||||
|
|
||||||
await client.renameWorkbenchSession(organizationId, {
|
await client.renameWorkspaceSession(organizationId, {
|
||||||
taskId: created.taskId,
|
taskId: created.taskId,
|
||||||
sessionId: secondTab.sessionId,
|
sessionId: secondTab.sessionId,
|
||||||
title: "Follow-up Session",
|
title: "Follow-up Session",
|
||||||
});
|
});
|
||||||
|
|
||||||
await client.updateWorkbenchDraft(organizationId, {
|
await client.updateWorkspaceDraft(organizationId, {
|
||||||
taskId: created.taskId,
|
taskId: created.taskId,
|
||||||
sessionId: secondTab.sessionId,
|
sessionId: secondTab.sessionId,
|
||||||
text: [
|
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.text).toContain(expectedReply);
|
||||||
expect(findTab(drafted, secondTab.sessionId).draft.attachments).toHaveLength(1);
|
expect(findTab(drafted, secondTab.sessionId).draft.attachments).toHaveLength(1);
|
||||||
|
|
||||||
await client.sendWorkbenchMessage(organizationId, {
|
await client.sendWorkspaceMessage(organizationId, {
|
||||||
taskId: created.taskId,
|
taskId: created.taskId,
|
||||||
sessionId: secondTab.sessionId,
|
sessionId: secondTab.sessionId,
|
||||||
text: [
|
text: [
|
||||||
|
|
@ -252,7 +252,7 @@ describe("e2e(client): workbench flows", () => {
|
||||||
"follow-up session response",
|
"follow-up session response",
|
||||||
10 * 60_000,
|
10 * 60_000,
|
||||||
2_000,
|
2_000,
|
||||||
async () => findTask(await client.getWorkbench(organizationId), created.taskId),
|
async () => findTask(await client.getWorkspace(organizationId), created.taskId),
|
||||||
(task) => {
|
(task) => {
|
||||||
const tab = findTab(task, secondTab.sessionId);
|
const tab = findTab(task, secondTab.sessionId);
|
||||||
return (
|
return (
|
||||||
|
|
@ -265,17 +265,17 @@ describe("e2e(client): workbench flows", () => {
|
||||||
expect(transcriptIncludesAgentText(secondTranscript, expectedReply)).toBe(true);
|
expect(transcriptIncludesAgentText(secondTranscript, expectedReply)).toBe(true);
|
||||||
expect(withSecondReply.fileChanges.some((file) => file.path === expectedFile)).toBe(true);
|
expect(withSecondReply.fileChanges.some((file) => file.path === expectedFile)).toBe(true);
|
||||||
|
|
||||||
await client.setWorkbenchSessionUnread(organizationId, {
|
await client.setWorkspaceSessionUnread(organizationId, {
|
||||||
taskId: created.taskId,
|
taskId: created.taskId,
|
||||||
sessionId: secondTab.sessionId,
|
sessionId: secondTab.sessionId,
|
||||||
unread: false,
|
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);
|
expect(unreadSnapshot.sessions.some((tab) => tab.unread)).toBe(true);
|
||||||
|
|
||||||
await client.closeWorkbenchSession(organizationId, {
|
await client.closeWorkspaceSession(organizationId, {
|
||||||
taskId: created.taskId,
|
taskId: created.taskId,
|
||||||
sessionId: secondTab.sessionId,
|
sessionId: secondTab.sessionId,
|
||||||
});
|
});
|
||||||
|
|
@ -284,26 +284,26 @@ describe("e2e(client): workbench flows", () => {
|
||||||
"secondary session closed",
|
"secondary session closed",
|
||||||
30_000,
|
30_000,
|
||||||
1_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),
|
(task) => !task.sessions.some((tab) => tab.id === secondTab.sessionId),
|
||||||
);
|
);
|
||||||
expect(closedSnapshot.sessions).toHaveLength(1);
|
expect(closedSnapshot.sessions).toHaveLength(1);
|
||||||
|
|
||||||
await client.revertWorkbenchFile(organizationId, {
|
await client.revertWorkspaceFile(organizationId, {
|
||||||
taskId: created.taskId,
|
taskId: created.taskId,
|
||||||
path: expectedFile,
|
path: expectedFile,
|
||||||
});
|
});
|
||||||
|
|
||||||
const revertedSnapshot = await poll(
|
const revertedSnapshot = await poll(
|
||||||
"file revert reflected in workbench",
|
"file revert reflected in workspace",
|
||||||
30_000,
|
30_000,
|
||||||
1_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),
|
(task) => !task.fileChanges.some((file) => file.path === expectedFile),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(revertedSnapshot.fileChanges.some((file) => file.path === expectedFile)).toBe(false);
|
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");
|
expect(findTab(revertedSnapshot, primaryTab.id).sessionName).toBe("Primary Session");
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import {
|
import {
|
||||||
createFoundryLogger,
|
createFoundryLogger,
|
||||||
type TaskWorkbenchSnapshot,
|
type TaskWorkspaceSnapshot,
|
||||||
type WorkbenchSession,
|
type WorkspaceSession,
|
||||||
type WorkbenchTask,
|
type WorkspaceTask,
|
||||||
type WorkbenchModelId,
|
type WorkspaceModelId,
|
||||||
type WorkbenchTranscriptEvent,
|
type WorkspaceTranscriptEvent,
|
||||||
} from "@sandbox-agent/foundry-shared";
|
} from "@sandbox-agent/foundry-shared";
|
||||||
import { createBackendClient } from "../../src/backend-client.js";
|
import { createBackendClient } from "../../src/backend-client.js";
|
||||||
import { requireImportedRepo } from "./helpers.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({
|
const logger = createFoundryLogger({
|
||||||
service: "foundry-client-e2e",
|
service: "foundry-client-e2e",
|
||||||
bindings: {
|
bindings: {
|
||||||
suite: "workbench-load",
|
suite: "workspace-load",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -26,7 +26,7 @@ function requiredEnv(name: string): string {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function workbenchModelEnv(name: string, fallback: WorkbenchModelId): WorkbenchModelId {
|
function workspaceModelEnv(name: string, fallback: WorkspaceModelId): WorkspaceModelId {
|
||||||
const value = process.env[name]?.trim();
|
const value = process.env[name]?.trim();
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case "claude-sonnet-4":
|
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);
|
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
||||||
if (!task) {
|
if (!task) {
|
||||||
throw new Error(`task ${taskId} missing from snapshot`);
|
throw new Error(`task ${taskId} missing from snapshot`);
|
||||||
|
|
@ -80,7 +80,7 @@ function findTask(snapshot: TaskWorkbenchSnapshot, taskId: string): WorkbenchTas
|
||||||
return task;
|
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);
|
const tab = task.sessions.find((candidate) => candidate.id === sessionId);
|
||||||
if (!tab) {
|
if (!tab) {
|
||||||
throw new Error(`tab ${sessionId} missing from task ${task.id}`);
|
throw new Error(`tab ${sessionId} missing from task ${task.id}`);
|
||||||
|
|
@ -88,7 +88,7 @@ function findTab(task: WorkbenchTask, sessionId: string): WorkbenchSession {
|
||||||
return tab;
|
return tab;
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractEventText(event: WorkbenchTranscriptEvent): string {
|
function extractEventText(event: WorkspaceTranscriptEvent): string {
|
||||||
const payload = event.payload;
|
const payload = event.payload;
|
||||||
if (!payload || typeof payload !== "object") {
|
if (!payload || typeof payload !== "object") {
|
||||||
return String(payload ?? "");
|
return String(payload ?? "");
|
||||||
|
|
@ -138,7 +138,7 @@ function extractEventText(event: WorkbenchTranscriptEvent): string {
|
||||||
return typeof envelope.method === "string" ? envelope.method : JSON.stringify(payload);
|
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
|
return transcript
|
||||||
.filter((event) => event.sender === "agent")
|
.filter((event) => event.sender === "agent")
|
||||||
.map((event) => extractEventText(event))
|
.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);
|
return values.reduce((sum, value) => sum + value, 0) / Math.max(values.length, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function measureWorkbenchSnapshot(
|
async function measureWorkspaceSnapshot(
|
||||||
client: ReturnType<typeof createBackendClient>,
|
client: ReturnType<typeof createBackendClient>,
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
iterations: number,
|
iterations: number,
|
||||||
|
|
@ -163,11 +163,11 @@ async function measureWorkbenchSnapshot(
|
||||||
transcriptEventCount: number;
|
transcriptEventCount: number;
|
||||||
}> {
|
}> {
|
||||||
const durations: number[] = [];
|
const durations: number[] = [];
|
||||||
let snapshot: TaskWorkbenchSnapshot | null = null;
|
let snapshot: TaskWorkspaceSnapshot | null = null;
|
||||||
|
|
||||||
for (let index = 0; index < iterations; index += 1) {
|
for (let index = 0; index < iterations; index += 1) {
|
||||||
const startedAt = performance.now();
|
const startedAt = performance.now();
|
||||||
snapshot = await client.getWorkbench(organizationId);
|
snapshot = await client.getWorkspace(organizationId);
|
||||||
durations.push(performance.now() - startedAt);
|
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 () => {
|
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 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 organizationId = process.env.HF_E2E_WORKSPACE?.trim() || "default";
|
||||||
const repoRemote = requiredEnv("HF_E2E_GITHUB_REPO");
|
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 taskCount = intEnv("HF_LOAD_TASK_COUNT", 3);
|
||||||
const extraSessionCount = intEnv("HF_LOAD_EXTRA_SESSION_COUNT", 2);
|
const extraSessionCount = intEnv("HF_LOAD_EXTRA_SESSION_COUNT", 2);
|
||||||
const pollIntervalMs = intEnv("HF_LOAD_POLL_INTERVAL_MS", 2_000);
|
const pollIntervalMs = intEnv("HF_LOAD_POLL_INTERVAL_MS", 2_000);
|
||||||
|
|
@ -220,16 +220,16 @@ describe("e2e(client): workbench load", () => {
|
||||||
transcriptEventCount: number;
|
transcriptEventCount: number;
|
||||||
}> = [];
|
}> = [];
|
||||||
|
|
||||||
snapshotSeries.push(await measureWorkbenchSnapshot(client, organizationId, 2));
|
snapshotSeries.push(await measureWorkspaceSnapshot(client, organizationId, 2));
|
||||||
|
|
||||||
for (let taskIndex = 0; taskIndex < taskCount; taskIndex += 1) {
|
for (let taskIndex = 0; taskIndex < taskCount; taskIndex += 1) {
|
||||||
const runId = `load-${taskIndex}-${Date.now().toString(36)}`;
|
const runId = `load-${taskIndex}-${Date.now().toString(36)}`;
|
||||||
const initialReply = `LOAD_INIT_${runId}`;
|
const initialReply = `LOAD_INIT_${runId}`;
|
||||||
|
|
||||||
const createStartedAt = performance.now();
|
const createStartedAt = performance.now();
|
||||||
const created = await client.createWorkbenchTask(organizationId, {
|
const created = await client.createWorkspaceTask(organizationId, {
|
||||||
repoId: repo.repoId,
|
repoId: repo.repoId,
|
||||||
title: `Workbench Load ${runId}`,
|
title: `Workspace Load ${runId}`,
|
||||||
branch: `load/${runId}`,
|
branch: `load/${runId}`,
|
||||||
model,
|
model,
|
||||||
task: `Reply with exactly: ${initialReply}`,
|
task: `Reply with exactly: ${initialReply}`,
|
||||||
|
|
@ -241,7 +241,7 @@ describe("e2e(client): workbench load", () => {
|
||||||
`task ${runId} provisioning`,
|
`task ${runId} provisioning`,
|
||||||
12 * 60_000,
|
12 * 60_000,
|
||||||
pollIntervalMs,
|
pollIntervalMs,
|
||||||
async () => findTask(await client.getWorkbench(organizationId), created.taskId),
|
async () => findTask(await client.getWorkspace(organizationId), created.taskId),
|
||||||
(task) => {
|
(task) => {
|
||||||
const tab = task.sessions[0];
|
const tab = task.sessions[0];
|
||||||
return Boolean(tab && task.status === "idle" && tab.status === "idle" && transcriptIncludesAgentText(tab.transcript, initialReply));
|
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) {
|
for (let sessionIndex = 0; sessionIndex < extraSessionCount; sessionIndex += 1) {
|
||||||
const expectedReply = `LOAD_REPLY_${runId}_${sessionIndex}`;
|
const expectedReply = `LOAD_REPLY_${runId}_${sessionIndex}`;
|
||||||
const createSessionStartedAt = performance.now();
|
const createSessionStartedAt = performance.now();
|
||||||
const createdSession = await client.createWorkbenchSession(organizationId, {
|
const createdSession = await client.createWorkspaceSession(organizationId, {
|
||||||
taskId: created.taskId,
|
taskId: created.taskId,
|
||||||
model,
|
model,
|
||||||
});
|
});
|
||||||
createSessionLatencies.push(performance.now() - createSessionStartedAt);
|
createSessionLatencies.push(performance.now() - createSessionStartedAt);
|
||||||
|
|
||||||
await client.sendWorkbenchMessage(organizationId, {
|
await client.sendWorkspaceMessage(organizationId, {
|
||||||
taskId: created.taskId,
|
taskId: created.taskId,
|
||||||
sessionId: createdSession.sessionId,
|
sessionId: createdSession.sessionId,
|
||||||
text: `Run pwd in the repo, then reply with exactly: ${expectedReply}`,
|
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`,
|
`task ${runId} session ${sessionIndex} reply`,
|
||||||
10 * 60_000,
|
10 * 60_000,
|
||||||
pollIntervalMs,
|
pollIntervalMs,
|
||||||
async () => findTask(await client.getWorkbench(organizationId), created.taskId),
|
async () => findTask(await client.getWorkspace(organizationId), created.taskId),
|
||||||
(task) => {
|
(task) => {
|
||||||
const tab = findTab(task, createdSession.sessionId);
|
const tab = findTab(task, createdSession.sessionId);
|
||||||
return tab.status === "idle" && transcriptIncludesAgentText(tab.transcript, expectedReply);
|
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);
|
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);
|
snapshotSeries.push(snapshotMetrics);
|
||||||
logger.info(
|
logger.info(
|
||||||
{
|
{
|
||||||
taskIndex: taskIndex + 1,
|
taskIndex: taskIndex + 1,
|
||||||
...snapshotMetrics,
|
...snapshotMetrics,
|
||||||
},
|
},
|
||||||
"workbench_load_snapshot",
|
"workspace_load_snapshot",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -314,7 +314,7 @@ describe("e2e(client): workbench load", () => {
|
||||||
snapshotTranscriptFinalCount: lastSnapshot.transcriptEventCount,
|
snapshotTranscriptFinalCount: lastSnapshot.transcriptEventCount,
|
||||||
};
|
};
|
||||||
|
|
||||||
logger.info(summary, "workbench_load_summary");
|
logger.info(summary, "workspace_load_summary");
|
||||||
|
|
||||||
expect(createTaskLatencies.length).toBe(taskCount);
|
expect(createTaskLatencies.length).toBe(taskCount);
|
||||||
expect(provisionLatencies.length).toBe(taskCount);
|
expect(provisionLatencies.length).toBe(taskCount);
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { describe, expect, it } from "vitest";
|
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", () => {
|
describe("actor keys", () => {
|
||||||
it("prefixes every key with organization namespace", () => {
|
it("prefixes every key with organization namespace", () => {
|
||||||
|
|
@ -8,7 +8,7 @@ describe("actor keys", () => {
|
||||||
repositoryKey("default", "repo"),
|
repositoryKey("default", "repo"),
|
||||||
taskKey("default", "repo", "task"),
|
taskKey("default", "repo", "task"),
|
||||||
taskSandboxKey("default", "sbx"),
|
taskSandboxKey("default", "sbx"),
|
||||||
historyKey("default", "repo"),
|
auditLogKey("default", "repo"),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
|
|
|
||||||
|
|
@ -115,8 +115,12 @@ describe("RemoteSubscriptionManager", () => {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
conn.emit("organizationUpdated", {
|
conn.emit("organizationUpdated", {
|
||||||
type: "taskSummaryUpdated",
|
type: "organizationUpdated",
|
||||||
taskSummary: {
|
snapshot: {
|
||||||
|
organizationId: "org-1",
|
||||||
|
repos: [],
|
||||||
|
taskSummaries: [
|
||||||
|
{
|
||||||
id: "task-1",
|
id: "task-1",
|
||||||
repoId: "repo-1",
|
repoId: "repo-1",
|
||||||
title: "Updated task",
|
title: "Updated task",
|
||||||
|
|
@ -127,6 +131,9 @@ describe("RemoteSubscriptionManager", () => {
|
||||||
pullRequest: null,
|
pullRequest: null,
|
||||||
sessionsSummary: [],
|
sessionsSummary: [],
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
openPullRequests: [],
|
||||||
|
},
|
||||||
} satisfies OrganizationEvent);
|
} satisfies OrganizationEvent);
|
||||||
|
|
||||||
expect(manager.getSnapshot("organization", params)?.taskSummaries[0]?.title).toBe("Updated task");
|
expect(manager.getSnapshot("organization", params)?.taskSummaries[0]?.title).toBe("Updated task");
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,10 @@ import type {
|
||||||
FoundryAppSnapshot,
|
FoundryAppSnapshot,
|
||||||
FoundryOrganization,
|
FoundryOrganization,
|
||||||
TaskStatus,
|
TaskStatus,
|
||||||
TaskWorkbenchSnapshot,
|
TaskWorkspaceSnapshot,
|
||||||
WorkbenchSandboxSummary,
|
WorkspaceSandboxSummary,
|
||||||
WorkbenchSessionSummary,
|
WorkspaceSessionSummary,
|
||||||
WorkbenchTaskStatus,
|
WorkspaceTaskStatus,
|
||||||
} from "@sandbox-agent/foundry-shared";
|
} from "@sandbox-agent/foundry-shared";
|
||||||
import { useSubscription } from "@sandbox-agent/foundry-client";
|
import { useSubscription } from "@sandbox-agent/foundry-client";
|
||||||
import type { DebugSubscriptionTopic } 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 {
|
interface DevPanelProps {
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
snapshot: TaskWorkbenchSnapshot;
|
snapshot: TaskWorkspaceSnapshot;
|
||||||
organization?: FoundryOrganization | null;
|
organization?: FoundryOrganization | null;
|
||||||
focusedTask?: DevPanelFocusedTask | null;
|
focusedTask?: DevPanelFocusedTask | null;
|
||||||
}
|
}
|
||||||
|
|
@ -27,14 +27,14 @@ export interface DevPanelFocusedTask {
|
||||||
id: string;
|
id: string;
|
||||||
repoId: string;
|
repoId: string;
|
||||||
title: string | null;
|
title: string | null;
|
||||||
status: WorkbenchTaskStatus;
|
status: WorkspaceTaskStatus;
|
||||||
runtimeStatus?: TaskStatus | null;
|
runtimeStatus?: TaskStatus | null;
|
||||||
statusMessage?: string | null;
|
statusMessage?: string | null;
|
||||||
branch?: string | null;
|
branch?: string | null;
|
||||||
activeSandboxId?: string | null;
|
activeSandboxId?: string | null;
|
||||||
activeSessionId?: string | null;
|
activeSessionId?: string | null;
|
||||||
sandboxes?: WorkbenchSandboxSummary[];
|
sandboxes?: WorkspaceSandboxSummary[];
|
||||||
sessions?: WorkbenchSessionSummary[];
|
sessions?: WorkspaceSessionSummary[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TopicInfo {
|
interface TopicInfo {
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,11 @@ import { useStyletron } from "baseui";
|
||||||
import {
|
import {
|
||||||
createErrorContext,
|
createErrorContext,
|
||||||
type FoundryOrganization,
|
type FoundryOrganization,
|
||||||
type TaskWorkbenchSnapshot,
|
type TaskWorkspaceSnapshot,
|
||||||
type WorkbenchOpenPrSummary,
|
type WorkspaceOpenPrSummary,
|
||||||
type WorkbenchSessionSummary,
|
type WorkspaceSessionSummary,
|
||||||
type WorkbenchTaskDetail,
|
type WorkspaceTaskDetail,
|
||||||
type WorkbenchTaskSummary,
|
type WorkspaceTaskSummary,
|
||||||
} from "@sandbox-agent/foundry-shared";
|
} from "@sandbox-agent/foundry-shared";
|
||||||
import { useSubscription } from "@sandbox-agent/foundry-client";
|
import { useSubscription } from "@sandbox-agent/foundry-client";
|
||||||
|
|
||||||
|
|
@ -39,7 +39,7 @@ import {
|
||||||
type Message,
|
type Message,
|
||||||
type ModelId,
|
type ModelId,
|
||||||
} from "./mock-layout/view-model";
|
} 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 { backendClient } from "../lib/backend";
|
||||||
import { subscriptionManager } from "../lib/subscription";
|
import { subscriptionManager } from "../lib/subscription";
|
||||||
import { describeTaskState, isProvisioningTaskStatus } from "../features/tasks/status";
|
import { describeTaskState, isProvisioningTaskStatus } from "../features/tasks/status";
|
||||||
|
|
@ -131,7 +131,7 @@ function GithubInstallationWarning({
|
||||||
}
|
}
|
||||||
|
|
||||||
function toSessionModel(
|
function toSessionModel(
|
||||||
summary: WorkbenchSessionSummary,
|
summary: WorkspaceSessionSummary,
|
||||||
sessionDetail?: { draft: Task["sessions"][number]["draft"]; transcript: Task["sessions"][number]["transcript"] },
|
sessionDetail?: { draft: Task["sessions"][number]["draft"]; transcript: Task["sessions"][number]["transcript"] },
|
||||||
): Task["sessions"][number] {
|
): Task["sessions"][number] {
|
||||||
return {
|
return {
|
||||||
|
|
@ -155,8 +155,8 @@ function toSessionModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
function toTaskModel(
|
function toTaskModel(
|
||||||
summary: WorkbenchTaskSummary,
|
summary: WorkspaceTaskSummary,
|
||||||
detail?: WorkbenchTaskDetail,
|
detail?: WorkspaceTaskDetail,
|
||||||
sessionCache?: Map<string, { draft: Task["sessions"][number]["draft"]; transcript: Task["sessions"][number]["transcript"] }>,
|
sessionCache?: Map<string, { draft: Task["sessions"][number]["draft"]; transcript: Task["sessions"][number]["transcript"] }>,
|
||||||
): Task {
|
): Task {
|
||||||
const sessions = detail?.sessionsSummary ?? summary.sessionsSummary;
|
const sessions = detail?.sessionsSummary ?? summary.sessionsSummary;
|
||||||
|
|
@ -190,7 +190,7 @@ function isOpenPrTaskId(taskId: string): boolean {
|
||||||
return taskId.startsWith(OPEN_PR_TASK_PREFIX);
|
return taskId.startsWith(OPEN_PR_TASK_PREFIX);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toOpenPrTaskModel(pullRequest: WorkbenchOpenPrSummary): Task {
|
function toOpenPrTaskModel(pullRequest: WorkspaceOpenPrSummary): Task {
|
||||||
return {
|
return {
|
||||||
id: openPrTaskId(pullRequest.prId),
|
id: openPrTaskId(pullRequest.prId),
|
||||||
repoId: pullRequest.repoId,
|
repoId: pullRequest.repoId,
|
||||||
|
|
@ -241,7 +241,7 @@ function groupRepositories(repos: Array<{ id: string; label: string }>, tasks: T
|
||||||
.filter((repo) => repo.tasks.length > 0);
|
.filter((repo) => repo.tasks.length > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WorkbenchActions {
|
interface WorkspaceActions {
|
||||||
createTask(input: {
|
createTask(input: {
|
||||||
repoId: string;
|
repoId: string;
|
||||||
task: string;
|
task: string;
|
||||||
|
|
@ -252,7 +252,6 @@ interface WorkbenchActions {
|
||||||
}): Promise<{ taskId: string; sessionId?: string }>;
|
}): Promise<{ taskId: string; sessionId?: string }>;
|
||||||
markTaskUnread(input: { taskId: string }): Promise<void>;
|
markTaskUnread(input: { taskId: string }): Promise<void>;
|
||||||
renameTask(input: { taskId: string; value: 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>;
|
archiveTask(input: { taskId: string }): Promise<void>;
|
||||||
publishPr(input: { taskId: string }): Promise<void>;
|
publishPr(input: { taskId: string }): Promise<void>;
|
||||||
revertFile(input: { taskId: string; path: 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>;
|
closeSession(input: { taskId: string; sessionId: string }): Promise<void>;
|
||||||
addSession(input: { taskId: string; model?: string }): Promise<{ sessionId: string }>;
|
addSession(input: { taskId: string; model?: string }): Promise<{ sessionId: string }>;
|
||||||
changeModel(input: { taskId: string; sessionId: string; model: ModelId }): Promise<void>;
|
changeModel(input: { taskId: string; sessionId: string; model: ModelId }): Promise<void>;
|
||||||
reloadGithubOrganization(): Promise<void>;
|
adminReloadGithubOrganization(): Promise<void>;
|
||||||
reloadGithubPullRequests(): Promise<void>;
|
adminReloadGithubPullRequests(): Promise<void>;
|
||||||
reloadGithubRepository(repoId: string): Promise<void>;
|
adminReloadGithubRepository(repoId: string): Promise<void>;
|
||||||
reloadGithubPullRequest(repoId: string, prNumber: number): Promise<void>;
|
adminReloadGithubPullRequest(repoId: string, prNumber: number): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TranscriptPanel = memo(function TranscriptPanel({
|
const TranscriptPanel = memo(function TranscriptPanel({
|
||||||
taskWorkbenchClient,
|
taskWorkspaceClient,
|
||||||
task,
|
task,
|
||||||
hasSandbox,
|
hasSandbox,
|
||||||
activeSessionId,
|
activeSessionId,
|
||||||
|
|
@ -290,7 +289,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
||||||
selectedSessionHydrating = false,
|
selectedSessionHydrating = false,
|
||||||
onNavigateToUsage,
|
onNavigateToUsage,
|
||||||
}: {
|
}: {
|
||||||
taskWorkbenchClient: WorkbenchActions;
|
taskWorkspaceClient: WorkspaceActions;
|
||||||
task: Task;
|
task: Task;
|
||||||
hasSandbox: boolean;
|
hasSandbox: boolean;
|
||||||
activeSessionId: string | null;
|
activeSessionId: string | null;
|
||||||
|
|
@ -310,8 +309,11 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
||||||
onNavigateToUsage?: () => void;
|
onNavigateToUsage?: () => void;
|
||||||
}) {
|
}) {
|
||||||
const t = useFoundryTokens();
|
const t = useFoundryTokens();
|
||||||
const [defaultModel, setDefaultModel] = useState<ModelId>("claude-sonnet-4");
|
const appSnapshot = useMockAppSnapshot();
|
||||||
const [editingField, setEditingField] = useState<"title" | "branch" | null>(null);
|
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 [editValue, setEditValue] = useState("");
|
||||||
const [editingSessionId, setEditingSessionId] = useState<string | null>(null);
|
const [editingSessionId, setEditingSessionId] = useState<string | null>(null);
|
||||||
const [editingSessionName, setEditingSessionName] = useState("");
|
const [editingSessionName, setEditingSessionName] = useState("");
|
||||||
|
|
@ -436,14 +438,14 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
void taskWorkbenchClient.setSessionUnread({
|
void taskWorkspaceClient.setSessionUnread({
|
||||||
taskId: task.id,
|
taskId: task.id,
|
||||||
sessionId: activeAgentSession.id,
|
sessionId: activeAgentSession.id,
|
||||||
unread: false,
|
unread: false,
|
||||||
});
|
});
|
||||||
}, [activeAgentSession?.id, activeAgentSession?.unread, task.id]);
|
}, [activeAgentSession?.id, activeAgentSession?.unread, task.id]);
|
||||||
|
|
||||||
const startEditingField = useCallback((field: "title" | "branch", value: string) => {
|
const startEditingField = useCallback((field: "title", value: string) => {
|
||||||
setEditingField(field);
|
setEditingField(field);
|
||||||
setEditValue(value);
|
setEditValue(value);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -453,18 +455,14 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const commitEditingField = useCallback(
|
const commitEditingField = useCallback(
|
||||||
(field: "title" | "branch") => {
|
(field: "title") => {
|
||||||
const value = editValue.trim();
|
const value = editValue.trim();
|
||||||
if (!value) {
|
if (!value) {
|
||||||
setEditingField(null);
|
setEditingField(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (field === "title") {
|
void taskWorkspaceClient.renameTask({ taskId: task.id, value });
|
||||||
void taskWorkbenchClient.renameTask({ taskId: task.id, value });
|
|
||||||
} else {
|
|
||||||
void taskWorkbenchClient.renameBranch({ taskId: task.id, value });
|
|
||||||
}
|
|
||||||
setEditingField(null);
|
setEditingField(null);
|
||||||
},
|
},
|
||||||
[editValue, task.id],
|
[editValue, task.id],
|
||||||
|
|
@ -474,7 +472,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
||||||
|
|
||||||
const flushDraft = useCallback(
|
const flushDraft = useCallback(
|
||||||
(text: string, nextAttachments: LineAttachment[], sessionId: string) => {
|
(text: string, nextAttachments: LineAttachment[], sessionId: string) => {
|
||||||
void taskWorkbenchClient.updateDraft({
|
void taskWorkspaceClient.updateDraft({
|
||||||
taskId: task.id,
|
taskId: task.id,
|
||||||
sessionId,
|
sessionId,
|
||||||
text,
|
text,
|
||||||
|
|
@ -535,7 +533,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
||||||
|
|
||||||
onSetActiveSessionId(promptSession.id);
|
onSetActiveSessionId(promptSession.id);
|
||||||
onSetLastAgentSessionId(promptSession.id);
|
onSetLastAgentSessionId(promptSession.id);
|
||||||
void taskWorkbenchClient.sendMessage({
|
void taskWorkspaceClient.sendMessage({
|
||||||
taskId: task.id,
|
taskId: task.id,
|
||||||
sessionId: promptSession.id,
|
sessionId: promptSession.id,
|
||||||
text,
|
text,
|
||||||
|
|
@ -548,7 +546,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
void taskWorkbenchClient.stopAgent({
|
void taskWorkspaceClient.stopAgent({
|
||||||
taskId: task.id,
|
taskId: task.id,
|
||||||
sessionId: promptSession.id,
|
sessionId: promptSession.id,
|
||||||
});
|
});
|
||||||
|
|
@ -562,7 +560,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
||||||
onSetLastAgentSessionId(sessionId);
|
onSetLastAgentSessionId(sessionId);
|
||||||
const session = task.sessions.find((candidate) => candidate.id === sessionId);
|
const session = task.sessions.find((candidate) => candidate.id === sessionId);
|
||||||
if (session?.unread) {
|
if (session?.unread) {
|
||||||
void taskWorkbenchClient.setSessionUnread({
|
void taskWorkspaceClient.setSessionUnread({
|
||||||
taskId: task.id,
|
taskId: task.id,
|
||||||
sessionId,
|
sessionId,
|
||||||
unread: false,
|
unread: false,
|
||||||
|
|
@ -576,7 +574,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
||||||
|
|
||||||
const setSessionUnread = useCallback(
|
const setSessionUnread = useCallback(
|
||||||
(sessionId: string, unread: boolean) => {
|
(sessionId: string, unread: boolean) => {
|
||||||
void taskWorkbenchClient.setSessionUnread({ taskId: task.id, sessionId, unread });
|
void taskWorkspaceClient.setSessionUnread({ taskId: task.id, sessionId, unread });
|
||||||
},
|
},
|
||||||
[task.id],
|
[task.id],
|
||||||
);
|
);
|
||||||
|
|
@ -610,7 +608,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
void taskWorkbenchClient.renameSession({
|
void taskWorkspaceClient.renameSession({
|
||||||
taskId: task.id,
|
taskId: task.id,
|
||||||
sessionId: editingSessionId,
|
sessionId: editingSessionId,
|
||||||
title: trimmedName,
|
title: trimmedName,
|
||||||
|
|
@ -631,7 +629,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
||||||
}
|
}
|
||||||
|
|
||||||
onSyncRouteSession(task.id, nextSessionId);
|
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],
|
[activeSessionId, task.id, task.sessions, lastAgentSessionId, onSetActiveSessionId, onSetLastAgentSessionId, onSyncRouteSession],
|
||||||
);
|
);
|
||||||
|
|
@ -651,7 +649,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
||||||
|
|
||||||
const addSession = useCallback(() => {
|
const addSession = useCallback(() => {
|
||||||
void (async () => {
|
void (async () => {
|
||||||
const { sessionId } = await taskWorkbenchClient.addSession({ taskId: task.id });
|
const { sessionId } = await taskWorkspaceClient.addSession({ taskId: task.id });
|
||||||
onSetLastAgentSessionId(sessionId);
|
onSetLastAgentSessionId(sessionId);
|
||||||
onSetActiveSessionId(sessionId);
|
onSetActiveSessionId(sessionId);
|
||||||
onSyncRouteSession(task.id, 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`);
|
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,
|
taskId: task.id,
|
||||||
sessionId: promptSession.id,
|
sessionId: promptSession.id,
|
||||||
model,
|
model,
|
||||||
|
|
@ -939,7 +937,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
||||||
messageRefs={messageRefs}
|
messageRefs={messageRefs}
|
||||||
historyEvents={historyEvents}
|
historyEvents={historyEvents}
|
||||||
onSelectHistoryEvent={jumpToHistoryEvent}
|
onSelectHistoryEvent={jumpToHistoryEvent}
|
||||||
targetMessageId={pendingHistoryTarget && activeSessionId === pendingHistoryTarget.sessionId ? pendingHistoryTarget.messageId : null}
|
targetMessageId={pendingHistoryTarget && activeAgentSession?.id === pendingHistoryTarget.sessionId ? pendingHistoryTarget.messageId : null}
|
||||||
onTargetMessageResolved={() => setPendingHistoryTarget(null)}
|
onTargetMessageResolved={() => setPendingHistoryTarget(null)}
|
||||||
copiedMessageId={copiedMessageId}
|
copiedMessageId={copiedMessageId}
|
||||||
onCopyMessage={(message) => {
|
onCopyMessage={(message) => {
|
||||||
|
|
@ -966,7 +964,9 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
||||||
onStop={stopAgent}
|
onStop={stopAgent}
|
||||||
onRemoveAttachment={removeAttachment}
|
onRemoveAttachment={removeAttachment}
|
||||||
onChangeModel={changeModel}
|
onChangeModel={changeModel}
|
||||||
onSetDefaultModel={setDefaultModel}
|
onSetDefaultModel={(model) => {
|
||||||
|
void appClient.setDefaultModel(model);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1280,27 +1280,26 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
||||||
const [css] = useStyletron();
|
const [css] = useStyletron();
|
||||||
const t = useFoundryTokens();
|
const t = useFoundryTokens();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const taskWorkbenchClient = useMemo<WorkbenchActions>(
|
const taskWorkspaceClient = useMemo<WorkspaceActions>(
|
||||||
() => ({
|
() => ({
|
||||||
createTask: (input) => backendClient.createWorkbenchTask(organizationId, input),
|
createTask: (input) => backendClient.createWorkspaceTask(organizationId, input),
|
||||||
markTaskUnread: (input) => backendClient.markWorkbenchUnread(organizationId, input),
|
markTaskUnread: (input) => backendClient.markWorkspaceUnread(organizationId, input),
|
||||||
renameTask: (input) => backendClient.renameWorkbenchTask(organizationId, input),
|
renameTask: (input) => backendClient.renameWorkspaceTask(organizationId, input),
|
||||||
renameBranch: (input) => backendClient.renameWorkbenchBranch(organizationId, input),
|
archiveTask: async (input) => backendClient.runAction(organizationId, input.repoId, input.taskId, "archive"),
|
||||||
archiveTask: async (input) => backendClient.runAction(organizationId, input.taskId, "archive"),
|
publishPr: (input) => backendClient.publishWorkspacePr(organizationId, input),
|
||||||
publishPr: (input) => backendClient.publishWorkbenchPr(organizationId, input),
|
revertFile: (input) => backendClient.revertWorkspaceFile(organizationId, input),
|
||||||
revertFile: (input) => backendClient.revertWorkbenchFile(organizationId, input),
|
updateDraft: (input) => backendClient.updateWorkspaceDraft(organizationId, input),
|
||||||
updateDraft: (input) => backendClient.updateWorkbenchDraft(organizationId, input),
|
sendMessage: (input) => backendClient.sendWorkspaceMessage(organizationId, input),
|
||||||
sendMessage: (input) => backendClient.sendWorkbenchMessage(organizationId, input),
|
stopAgent: (input) => backendClient.stopWorkspaceSession(organizationId, input),
|
||||||
stopAgent: (input) => backendClient.stopWorkbenchSession(organizationId, input),
|
setSessionUnread: (input) => backendClient.setWorkspaceSessionUnread(organizationId, input),
|
||||||
setSessionUnread: (input) => backendClient.setWorkbenchSessionUnread(organizationId, input),
|
renameSession: (input) => backendClient.renameWorkspaceSession(organizationId, input),
|
||||||
renameSession: (input) => backendClient.renameWorkbenchSession(organizationId, input),
|
closeSession: (input) => backendClient.closeWorkspaceSession(organizationId, input),
|
||||||
closeSession: (input) => backendClient.closeWorkbenchSession(organizationId, input),
|
addSession: (input) => backendClient.createWorkspaceSession(organizationId, input),
|
||||||
addSession: (input) => backendClient.createWorkbenchSession(organizationId, input),
|
changeModel: (input) => backendClient.changeWorkspaceModel(organizationId, input),
|
||||||
changeModel: (input) => backendClient.changeWorkbenchModel(organizationId, input),
|
adminReloadGithubOrganization: () => backendClient.adminReloadGithubOrganization(organizationId),
|
||||||
reloadGithubOrganization: () => backendClient.reloadGithubOrganization(organizationId),
|
adminReloadGithubPullRequests: () => backendClient.adminReloadGithubPullRequests(organizationId),
|
||||||
reloadGithubPullRequests: () => backendClient.reloadGithubPullRequests(organizationId),
|
adminReloadGithubRepository: (repoId) => backendClient.adminReloadGithubRepository(organizationId, repoId),
|
||||||
reloadGithubRepository: (repoId) => backendClient.reloadGithubRepository(organizationId, repoId),
|
adminReloadGithubPullRequest: (repoId, prNumber) => backendClient.adminReloadGithubPullRequest(organizationId, repoId, prNumber),
|
||||||
reloadGithubPullRequest: (repoId, prNumber) => backendClient.reloadGithubPullRequest(organizationId, repoId, prNumber),
|
|
||||||
}),
|
}),
|
||||||
[organizationId],
|
[organizationId],
|
||||||
);
|
);
|
||||||
|
|
@ -1495,7 +1494,7 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
||||||
}, [selectedOpenPullRequest, selectedTaskId, tasks]);
|
}, [selectedOpenPullRequest, selectedTaskId, tasks]);
|
||||||
|
|
||||||
const materializeOpenPullRequest = useCallback(
|
const materializeOpenPullRequest = useCallback(
|
||||||
async (pullRequest: WorkbenchOpenPrSummary) => {
|
async (pullRequest: WorkspaceOpenPrSummary) => {
|
||||||
if (resolvingOpenPullRequestsRef.current.has(pullRequest.prId)) {
|
if (resolvingOpenPullRequestsRef.current.has(pullRequest.prId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -1504,7 +1503,7 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
||||||
setMaterializingOpenPrId(pullRequest.prId);
|
setMaterializingOpenPrId(pullRequest.prId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { taskId, sessionId } = await taskWorkbenchClient.createTask({
|
const { taskId, sessionId } = await taskWorkspaceClient.createTask({
|
||||||
repoId: pullRequest.repoId,
|
repoId: pullRequest.repoId,
|
||||||
task: `Continue work on GitHub PR #${pullRequest.number}: ${pullRequest.title}`,
|
task: `Continue work on GitHub PR #${pullRequest.number}: ${pullRequest.title}`,
|
||||||
model: "gpt-5.3-codex",
|
model: "gpt-5.3-codex",
|
||||||
|
|
@ -1534,7 +1533,7 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[navigate, taskWorkbenchClient, organizationId],
|
[navigate, taskWorkspaceClient, organizationId],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -1664,7 +1663,7 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
||||||
autoCreatingSessionForTaskRef.current.add(activeTask.id);
|
autoCreatingSessionForTaskRef.current.add(activeTask.id);
|
||||||
void (async () => {
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
const { sessionId } = await taskWorkbenchClient.addSession({ taskId: activeTask.id });
|
const { sessionId } = await taskWorkspaceClient.addSession({ taskId: activeTask.id });
|
||||||
syncRouteSession(activeTask.id, sessionId, true);
|
syncRouteSession(activeTask.id, sessionId, true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
|
|
@ -1672,13 +1671,13 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
||||||
taskId: activeTask.id,
|
taskId: activeTask.id,
|
||||||
...createErrorContext(error),
|
...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.
|
// 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.
|
// The guard is cleared when sessions appear (line above) or the task changes.
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [activeTask, selectedSessionId, syncRouteSession, taskWorkbenchClient]);
|
}, [activeTask, selectedSessionId, syncRouteSession, taskWorkspaceClient]);
|
||||||
|
|
||||||
const createTask = useCallback(
|
const createTask = useCallback(
|
||||||
(overrideRepoId?: string, options?: { title?: string; task?: string; branch?: string; onBranch?: string }) => {
|
(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");
|
throw new Error("Cannot create a task without an available repo");
|
||||||
}
|
}
|
||||||
|
|
||||||
const { taskId, sessionId } = await taskWorkbenchClient.createTask({
|
const { taskId, sessionId } = await taskWorkspaceClient.createTask({
|
||||||
repoId,
|
repoId,
|
||||||
task: options?.task ?? "New task",
|
task: options?.task ?? "New task",
|
||||||
model: "gpt-5.3-codex",
|
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(
|
const openDiffTab = useCallback(
|
||||||
|
|
@ -1757,7 +1756,7 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
||||||
);
|
);
|
||||||
|
|
||||||
const markTaskUnread = useCallback((id: string) => {
|
const markTaskUnread = useCallback((id: string) => {
|
||||||
void taskWorkbenchClient.markTaskUnread({ taskId: id });
|
void taskWorkspaceClient.markTaskUnread({ taskId: id });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const renameTask = useCallback(
|
const renameTask = useCallback(
|
||||||
|
|
@ -1777,29 +1776,7 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
void taskWorkbenchClient.renameTask({ taskId: id, value: trimmedTitle });
|
void taskWorkspaceClient.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 });
|
|
||||||
},
|
},
|
||||||
[tasks],
|
[tasks],
|
||||||
);
|
);
|
||||||
|
|
@ -1808,14 +1785,14 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
||||||
if (!activeTask) {
|
if (!activeTask) {
|
||||||
throw new Error("Cannot archive without an active task");
|
throw new Error("Cannot archive without an active task");
|
||||||
}
|
}
|
||||||
void taskWorkbenchClient.archiveTask({ taskId: activeTask.id });
|
void taskWorkspaceClient.archiveTask({ taskId: activeTask.id });
|
||||||
}, [activeTask]);
|
}, [activeTask]);
|
||||||
|
|
||||||
const publishPr = useCallback(() => {
|
const publishPr = useCallback(() => {
|
||||||
if (!activeTask) {
|
if (!activeTask) {
|
||||||
throw new Error("Cannot publish PR without an active task");
|
throw new Error("Cannot publish PR without an active task");
|
||||||
}
|
}
|
||||||
void taskWorkbenchClient.publishPr({ taskId: activeTask.id });
|
void taskWorkspaceClient.publishPr({ taskId: activeTask.id });
|
||||||
}, [activeTask]);
|
}, [activeTask]);
|
||||||
|
|
||||||
const revertFile = useCallback(
|
const revertFile = useCallback(
|
||||||
|
|
@ -1835,7 +1812,7 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
||||||
: (current[activeTask.id] ?? null),
|
: (current[activeTask.id] ?? null),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
void taskWorkbenchClient.revertFile({
|
void taskWorkspaceClient.revertFile({
|
||||||
taskId: activeTask.id,
|
taskId: activeTask.id,
|
||||||
path,
|
path,
|
||||||
});
|
});
|
||||||
|
|
@ -1939,14 +1916,13 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
||||||
onSelectNewTaskRepo={setSelectedNewTaskRepoId}
|
onSelectNewTaskRepo={setSelectedNewTaskRepoId}
|
||||||
onMarkUnread={markTaskUnread}
|
onMarkUnread={markTaskUnread}
|
||||||
onRenameTask={renameTask}
|
onRenameTask={renameTask}
|
||||||
onRenameBranch={renameBranch}
|
|
||||||
onReorderRepositories={reorderRepositories}
|
onReorderRepositories={reorderRepositories}
|
||||||
taskOrderByRepository={taskOrderByRepository}
|
taskOrderByRepository={taskOrderByRepository}
|
||||||
onReorderTasks={reorderTasks}
|
onReorderTasks={reorderTasks}
|
||||||
onReloadOrganization={() => void taskWorkbenchClient.reloadGithubOrganization()}
|
onReloadOrganization={() => void taskWorkspaceClient.adminReloadGithubOrganization()}
|
||||||
onReloadPullRequests={() => void taskWorkbenchClient.reloadGithubPullRequests()}
|
onReloadPullRequests={() => void taskWorkspaceClient.adminReloadGithubPullRequests()}
|
||||||
onReloadRepository={(repoId) => void taskWorkbenchClient.reloadGithubRepository(repoId)}
|
onReloadRepository={(repoId) => void taskWorkspaceClient.adminReloadGithubRepository(repoId)}
|
||||||
onReloadPullRequest={(repoId, prNumber) => void taskWorkbenchClient.reloadGithubPullRequest(repoId, prNumber)}
|
onReloadPullRequest={(repoId, prNumber) => void taskWorkspaceClient.adminReloadGithubPullRequest(repoId, prNumber)}
|
||||||
onToggleSidebar={() => setLeftSidebarOpen(false)}
|
onToggleSidebar={() => setLeftSidebarOpen(false)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -2079,7 +2055,7 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
||||||
{showDevPanel && (
|
{showDevPanel && (
|
||||||
<DevPanel
|
<DevPanel
|
||||||
organizationId={organizationId}
|
organizationId={organizationId}
|
||||||
snapshot={{ organizationId, repos: organizationRepos, repositories: rawRepositories, tasks } as TaskWorkbenchSnapshot}
|
snapshot={{ organizationId, repos: organizationRepos, repositories: rawRepositories, tasks } as TaskWorkspaceSnapshot}
|
||||||
organization={activeOrg}
|
organization={activeOrg}
|
||||||
focusedTask={null}
|
focusedTask={null}
|
||||||
/>
|
/>
|
||||||
|
|
@ -2114,14 +2090,13 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
||||||
onSelectNewTaskRepo={setSelectedNewTaskRepoId}
|
onSelectNewTaskRepo={setSelectedNewTaskRepoId}
|
||||||
onMarkUnread={markTaskUnread}
|
onMarkUnread={markTaskUnread}
|
||||||
onRenameTask={renameTask}
|
onRenameTask={renameTask}
|
||||||
onRenameBranch={renameBranch}
|
|
||||||
onReorderRepositories={reorderRepositories}
|
onReorderRepositories={reorderRepositories}
|
||||||
taskOrderByRepository={taskOrderByRepository}
|
taskOrderByRepository={taskOrderByRepository}
|
||||||
onReorderTasks={reorderTasks}
|
onReorderTasks={reorderTasks}
|
||||||
onReloadOrganization={() => void taskWorkbenchClient.reloadGithubOrganization()}
|
onReloadOrganization={() => void taskWorkspaceClient.adminReloadGithubOrganization()}
|
||||||
onReloadPullRequests={() => void taskWorkbenchClient.reloadGithubPullRequests()}
|
onReloadPullRequests={() => void taskWorkspaceClient.adminReloadGithubPullRequests()}
|
||||||
onReloadRepository={(repoId) => void taskWorkbenchClient.reloadGithubRepository(repoId)}
|
onReloadRepository={(repoId) => void taskWorkspaceClient.adminReloadGithubRepository(repoId)}
|
||||||
onReloadPullRequest={(repoId, prNumber) => void taskWorkbenchClient.reloadGithubPullRequest(repoId, prNumber)}
|
onReloadPullRequest={(repoId, prNumber) => void taskWorkspaceClient.adminReloadGithubPullRequest(repoId, prNumber)}
|
||||||
onToggleSidebar={() => setLeftSidebarOpen(false)}
|
onToggleSidebar={() => setLeftSidebarOpen(false)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -2169,14 +2144,13 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
||||||
onSelectNewTaskRepo={setSelectedNewTaskRepoId}
|
onSelectNewTaskRepo={setSelectedNewTaskRepoId}
|
||||||
onMarkUnread={markTaskUnread}
|
onMarkUnread={markTaskUnread}
|
||||||
onRenameTask={renameTask}
|
onRenameTask={renameTask}
|
||||||
onRenameBranch={renameBranch}
|
|
||||||
onReorderRepositories={reorderRepositories}
|
onReorderRepositories={reorderRepositories}
|
||||||
taskOrderByRepository={taskOrderByRepository}
|
taskOrderByRepository={taskOrderByRepository}
|
||||||
onReorderTasks={reorderTasks}
|
onReorderTasks={reorderTasks}
|
||||||
onReloadOrganization={() => void taskWorkbenchClient.reloadGithubOrganization()}
|
onReloadOrganization={() => void taskWorkspaceClient.adminReloadGithubOrganization()}
|
||||||
onReloadPullRequests={() => void taskWorkbenchClient.reloadGithubPullRequests()}
|
onReloadPullRequests={() => void taskWorkspaceClient.adminReloadGithubPullRequests()}
|
||||||
onReloadRepository={(repoId) => void taskWorkbenchClient.reloadGithubRepository(repoId)}
|
onReloadRepository={(repoId) => void taskWorkspaceClient.adminReloadGithubRepository(repoId)}
|
||||||
onReloadPullRequest={(repoId, prNumber) => void taskWorkbenchClient.reloadGithubPullRequest(repoId, prNumber)}
|
onReloadPullRequest={(repoId, prNumber) => void taskWorkspaceClient.adminReloadGithubPullRequest(repoId, prNumber)}
|
||||||
onToggleSidebar={() => {
|
onToggleSidebar={() => {
|
||||||
setLeftSidebarPeeking(false);
|
setLeftSidebarPeeking(false);
|
||||||
setLeftSidebarOpen(true);
|
setLeftSidebarOpen(true);
|
||||||
|
|
@ -2189,7 +2163,7 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
||||||
{leftSidebarOpen ? <PanelResizeHandle onResizeStart={onLeftResizeStart} onResize={onLeftResize} /> : null}
|
{leftSidebarOpen ? <PanelResizeHandle onResizeStart={onLeftResizeStart} onResize={onLeftResize} /> : null}
|
||||||
<div style={{ flex: 1, minWidth: 0, display: "flex", flexDirection: "column" }}>
|
<div style={{ flex: 1, minWidth: 0, display: "flex", flexDirection: "column" }}>
|
||||||
<TranscriptPanel
|
<TranscriptPanel
|
||||||
taskWorkbenchClient={taskWorkbenchClient}
|
taskWorkspaceClient={taskWorkspaceClient}
|
||||||
task={activeTask}
|
task={activeTask}
|
||||||
hasSandbox={hasSandbox}
|
hasSandbox={hasSandbox}
|
||||||
activeSessionId={activeSessionId}
|
activeSessionId={activeSessionId}
|
||||||
|
|
@ -2248,7 +2222,7 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
||||||
{showDevPanel && (
|
{showDevPanel && (
|
||||||
<DevPanel
|
<DevPanel
|
||||||
organizationId={organizationId}
|
organizationId={organizationId}
|
||||||
snapshot={{ organizationId, repos: organizationRepos, repositories: rawRepositories, tasks } as TaskWorkbenchSnapshot}
|
snapshot={{ organizationId, repos: organizationRepos, repositories: rawRepositories, tasks } as TaskWorkspaceSnapshot}
|
||||||
organization={activeOrg}
|
organization={activeOrg}
|
||||||
focusedTask={{
|
focusedTask={{
|
||||||
id: activeTask.id,
|
id: activeTask.id,
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,6 @@ export const Sidebar = memo(function Sidebar({
|
||||||
onSelectNewTaskRepo,
|
onSelectNewTaskRepo,
|
||||||
onMarkUnread,
|
onMarkUnread,
|
||||||
onRenameTask,
|
onRenameTask,
|
||||||
onRenameBranch,
|
|
||||||
onReorderRepositories,
|
onReorderRepositories,
|
||||||
taskOrderByRepository,
|
taskOrderByRepository,
|
||||||
onReorderTasks,
|
onReorderTasks,
|
||||||
|
|
@ -87,7 +86,6 @@ export const Sidebar = memo(function Sidebar({
|
||||||
onSelectNewTaskRepo: (repoId: string) => void;
|
onSelectNewTaskRepo: (repoId: string) => void;
|
||||||
onMarkUnread: (id: string) => void;
|
onMarkUnread: (id: string) => void;
|
||||||
onRenameTask: (id: string) => void;
|
onRenameTask: (id: string) => void;
|
||||||
onRenameBranch: (id: string) => void;
|
|
||||||
onReorderRepositories: (fromIndex: number, toIndex: number) => void;
|
onReorderRepositories: (fromIndex: number, toIndex: number) => void;
|
||||||
taskOrderByRepository: Record<string, string[]>;
|
taskOrderByRepository: Record<string, string[]>;
|
||||||
onReorderTasks: (repositoryId: string, fromIndex: number, toIndex: number) => void;
|
onReorderTasks: (repositoryId: string, fromIndex: number, toIndex: number) => void;
|
||||||
|
|
@ -729,7 +727,6 @@ export const Sidebar = memo(function Sidebar({
|
||||||
}
|
}
|
||||||
contextMenu.open(event, [
|
contextMenu.open(event, [
|
||||||
{ label: "Rename task", onClick: () => onRenameTask(task.id) },
|
{ label: "Rename task", onClick: () => onRenameTask(task.id) },
|
||||||
{ label: "Rename branch", onClick: () => onRenameBranch(task.id) },
|
|
||||||
{ label: "Mark as unread", onClick: () => onMarkUnread(task.id) },
|
{ label: "Mark as unread", onClick: () => onMarkUnread(task.id) },
|
||||||
]);
|
]);
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -30,11 +30,11 @@ export const TranscriptHeader = memo(function TranscriptHeader({
|
||||||
task: Task;
|
task: Task;
|
||||||
hasSandbox: boolean;
|
hasSandbox: boolean;
|
||||||
activeSession: AgentSession | null | undefined;
|
activeSession: AgentSession | null | undefined;
|
||||||
editingField: "title" | "branch" | null;
|
editingField: "title" | null;
|
||||||
editValue: string;
|
editValue: string;
|
||||||
onEditValueChange: (value: string) => void;
|
onEditValueChange: (value: string) => void;
|
||||||
onStartEditingField: (field: "title" | "branch", value: string) => void;
|
onStartEditingField: (field: "title", value: string) => void;
|
||||||
onCommitEditingField: (field: "title" | "branch") => void;
|
onCommitEditingField: (field: "title") => void;
|
||||||
onCancelEditingField: () => void;
|
onCancelEditingField: () => void;
|
||||||
onSetActiveSessionUnread: (unread: boolean) => void;
|
onSetActiveSessionUnread: (unread: boolean) => void;
|
||||||
sidebarCollapsed?: boolean;
|
sidebarCollapsed?: boolean;
|
||||||
|
|
@ -118,39 +118,7 @@ export const TranscriptHeader = memo(function TranscriptHeader({
|
||||||
</LabelSmall>
|
</LabelSmall>
|
||||||
)}
|
)}
|
||||||
{task.branch ? (
|
{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
|
<span
|
||||||
title="Rename"
|
|
||||||
onClick={() => onStartEditingField("branch", task.branch ?? "")}
|
|
||||||
className={css({
|
className={css({
|
||||||
padding: "2px 8px",
|
padding: "2px 8px",
|
||||||
borderRadius: "999px",
|
borderRadius: "999px",
|
||||||
|
|
@ -160,13 +128,10 @@ export const TranscriptHeader = memo(function TranscriptHeader({
|
||||||
fontSize: "11px",
|
fontSize: "11px",
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: "nowrap",
|
||||||
fontFamily: '"IBM Plex Mono", monospace',
|
fontFamily: '"IBM Plex Mono", monospace',
|
||||||
cursor: "pointer",
|
|
||||||
":hover": { borderColor: t.borderFocus },
|
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{task.branch}
|
{task.branch}
|
||||||
</span>
|
</span>
|
||||||
)
|
|
||||||
) : null}
|
) : null}
|
||||||
<HeaderStatusPill status={headerStatus} />
|
<HeaderStatusPill status={headerStatus} />
|
||||||
<div className={css({ flex: 1 })} />
|
<div className={css({ flex: 1 })} />
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { describe, expect, it } from "vitest";
|
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";
|
import { buildDisplayMessages } from "./view-model";
|
||||||
|
|
||||||
function makeSession(transcript: WorkbenchSession["transcript"]): WorkbenchSession {
|
function makeSession(transcript: WorkspaceSession["transcript"]): WorkspaceSession {
|
||||||
return {
|
return {
|
||||||
id: "session-1",
|
id: "session-1",
|
||||||
sessionId: "session-1",
|
sessionId: "session-1",
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
import type {
|
import type {
|
||||||
WorkbenchAgentKind as AgentKind,
|
WorkspaceAgentKind as AgentKind,
|
||||||
WorkbenchSession as AgentSession,
|
WorkspaceSession as AgentSession,
|
||||||
WorkbenchDiffLineKind as DiffLineKind,
|
WorkspaceDiffLineKind as DiffLineKind,
|
||||||
WorkbenchFileChange as FileChange,
|
WorkspaceFileChange as FileChange,
|
||||||
WorkbenchFileTreeNode as FileTreeNode,
|
WorkspaceFileTreeNode as FileTreeNode,
|
||||||
WorkbenchTask as Task,
|
WorkspaceTask as Task,
|
||||||
WorkbenchHistoryEvent as HistoryEvent,
|
WorkspaceHistoryEvent as HistoryEvent,
|
||||||
WorkbenchLineAttachment as LineAttachment,
|
WorkspaceLineAttachment as LineAttachment,
|
||||||
WorkbenchModelGroup as ModelGroup,
|
WorkspaceModelGroup as ModelGroup,
|
||||||
WorkbenchModelId as ModelId,
|
WorkspaceModelId as ModelId,
|
||||||
WorkbenchParsedDiffLine as ParsedDiffLine,
|
WorkspaceParsedDiffLine as ParsedDiffLine,
|
||||||
WorkbenchRepositorySection as RepositorySection,
|
WorkspaceRepositorySection as RepositorySection,
|
||||||
WorkbenchTranscriptEvent as TranscriptEvent,
|
WorkspaceTranscriptEvent as TranscriptEvent,
|
||||||
} from "@sandbox-agent/foundry-shared";
|
} from "@sandbox-agent/foundry-shared";
|
||||||
import { extractEventText } from "../../features/sessions/model";
|
import { extractEventText } from "../../features/sessions/model";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
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 { currentFoundryOrganization, useSubscription } from "@sandbox-agent/foundry-client";
|
||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import { Link, useNavigate } from "@tanstack/react-router";
|
import { Link, useNavigate } from "@tanstack/react-router";
|
||||||
|
|
@ -100,7 +100,7 @@ const AGENT_OPTIONS: SelectItem[] = [
|
||||||
{ id: "claude", label: "claude" },
|
{ id: "claude", label: "claude" },
|
||||||
];
|
];
|
||||||
|
|
||||||
function statusKind(status: WorkbenchTaskStatus): StatusTagKind {
|
function statusKind(status: WorkspaceTaskStatus): StatusTagKind {
|
||||||
if (status === "running") return "positive";
|
if (status === "running") return "positive";
|
||||||
if (status === "error") return "negative";
|
if (status === "error") return "negative";
|
||||||
if (status === "new" || String(status).startsWith("init_")) return "warning";
|
if (status === "new" || String(status).startsWith("init_")) return "warning";
|
||||||
|
|
@ -515,7 +515,7 @@ export function OrganizationDashboard({ organizationId, selectedTaskId, selected
|
||||||
};
|
};
|
||||||
}, [repoOverviewMode, selectedForSession, selectedSummary]);
|
}, [repoOverviewMode, selectedForSession, selectedSummary]);
|
||||||
const devPanelSnapshot = useMemo(
|
const devPanelSnapshot = useMemo(
|
||||||
(): TaskWorkbenchSnapshot => ({
|
(): TaskWorkspaceSnapshot => ({
|
||||||
organizationId,
|
organizationId,
|
||||||
repos: repos.map((repo) => ({ id: repo.id, label: repo.label })),
|
repos: repos.map((repo) => ({ id: repo.id, label: repo.label })),
|
||||||
repositories: [],
|
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";
|
import type { HeaderStatusInfo } from "../../components/mock-layout/ui";
|
||||||
|
|
||||||
export type TaskDisplayStatus = TaskStatus | "new";
|
export type TaskDisplayStatus = TaskStatus | "new";
|
||||||
|
|
@ -73,7 +73,7 @@ export function describeTaskState(status: TaskDisplayStatus | null | undefined,
|
||||||
export function deriveHeaderStatus(
|
export function deriveHeaderStatus(
|
||||||
taskStatus: TaskDisplayStatus | null | undefined,
|
taskStatus: TaskDisplayStatus | null | undefined,
|
||||||
taskStatusMessage: string | null | undefined,
|
taskStatusMessage: string | null | undefined,
|
||||||
sessionStatus: WorkbenchSessionStatus | null | undefined,
|
sessionStatus: WorkspaceSessionStatus | null | undefined,
|
||||||
sessionErrorMessage: string | null | undefined,
|
sessionErrorMessage: string | null | undefined,
|
||||||
hasSandbox?: boolean,
|
hasSandbox?: boolean,
|
||||||
): HeaderStatusInfo {
|
): HeaderStatusInfo {
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,13 @@ import {
|
||||||
eligibleFoundryOrganizations,
|
eligibleFoundryOrganizations,
|
||||||
type FoundryAppClient,
|
type FoundryAppClient,
|
||||||
} from "@sandbox-agent/foundry-client";
|
} 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 { backendClient } from "./backend";
|
||||||
import { subscriptionManager } from "./subscription";
|
import { subscriptionManager } from "./subscription";
|
||||||
import { frontendClientMode } from "./env";
|
import { frontendClientMode } from "./env";
|
||||||
|
|
@ -58,6 +64,9 @@ const remoteAppClient: FoundryAppClient = {
|
||||||
async selectOrganization(organizationId: string): Promise<void> {
|
async selectOrganization(organizationId: string): Promise<void> {
|
||||||
await backendClient.selectAppOrganization(organizationId);
|
await backendClient.selectAppOrganization(organizationId);
|
||||||
},
|
},
|
||||||
|
async setDefaultModel(defaultModel: WorkspaceModelId): Promise<void> {
|
||||||
|
await backendClient.setAppDefaultModel(defaultModel);
|
||||||
|
},
|
||||||
async updateOrganizationProfile(input: UpdateFoundryOrganizationProfileInput): Promise<void> {
|
async updateOrganizationProfile(input: UpdateFoundryOrganizationProfileInput): Promise<void> {
|
||||||
await backendClient.updateAppOrganizationProfile(input);
|
await backendClient.updateAppOrganizationProfile(input);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -43,15 +43,27 @@ declare module "@sandbox-agent/react" {
|
||||||
className?: string;
|
className?: string;
|
||||||
classNames?: Partial<AgentTranscriptClassNames>;
|
classNames?: Partial<AgentTranscriptClassNames>;
|
||||||
endRef?: RefObject<HTMLDivElement>;
|
endRef?: RefObject<HTMLDivElement>;
|
||||||
|
scrollRef?: RefObject<HTMLDivElement>;
|
||||||
|
scrollToEntryId?: string | null;
|
||||||
sessionError?: string | null;
|
sessionError?: string | null;
|
||||||
eventError?: string | null;
|
eventError?: string | null;
|
||||||
isThinking?: boolean;
|
isThinking?: boolean;
|
||||||
agentId?: string;
|
agentId?: string;
|
||||||
|
virtualize?: boolean;
|
||||||
|
onAtBottomChange?: (atBottom: boolean) => void;
|
||||||
onEventClick?: (eventId: string) => void;
|
onEventClick?: (eventId: string) => void;
|
||||||
onPermissionReply?: (permissionId: string, reply: PermissionReply) => void;
|
onPermissionReply?: (permissionId: string, reply: PermissionReply) => void;
|
||||||
|
isDividerEntry?: (entry: TranscriptEntry) => boolean;
|
||||||
|
canOpenEvent?: (entry: TranscriptEntry) => boolean;
|
||||||
|
getToolGroupSummary?: (entries: TranscriptEntry[]) => string;
|
||||||
renderMessageText?: (entry: TranscriptEntry) => ReactNode;
|
renderMessageText?: (entry: TranscriptEntry) => ReactNode;
|
||||||
renderInlinePendingIndicator?: () => ReactNode;
|
renderInlinePendingIndicator?: () => ReactNode;
|
||||||
renderThinkingState?: (context: { agentId?: string }) => 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;
|
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 FoundryBillingPlanId = "free" | "team";
|
||||||
export type FoundryBillingStatus = "active" | "trialing" | "past_due" | "scheduled_cancel";
|
export type FoundryBillingStatus = "active" | "trialing" | "past_due" | "scheduled_cancel";
|
||||||
|
|
@ -14,6 +14,7 @@ export interface FoundryUser {
|
||||||
githubLogin: string;
|
githubLogin: string;
|
||||||
roleLabel: string;
|
roleLabel: string;
|
||||||
eligibleOrganizationIds: string[];
|
eligibleOrganizationIds: string[];
|
||||||
|
defaultModel: WorkspaceModelId;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FoundryOrganizationMember {
|
export interface FoundryOrganizationMember {
|
||||||
|
|
@ -59,7 +60,6 @@ export interface FoundryOrganizationSettings {
|
||||||
slug: string;
|
slug: string;
|
||||||
primaryDomain: string;
|
primaryDomain: string;
|
||||||
seatAccrualMode: "first_prompt";
|
seatAccrualMode: "first_prompt";
|
||||||
defaultModel: WorkbenchModelId;
|
|
||||||
autoImportRepos: boolean;
|
autoImportRepos: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,6 @@ export const CreateTaskInputSchema = z.object({
|
||||||
explicitTitle: z.string().trim().min(1).optional(),
|
explicitTitle: z.string().trim().min(1).optional(),
|
||||||
explicitBranchName: z.string().trim().min(1).optional(),
|
explicitBranchName: z.string().trim().min(1).optional(),
|
||||||
sandboxProviderId: SandboxProviderIdSchema.optional(),
|
sandboxProviderId: SandboxProviderIdSchema.optional(),
|
||||||
agentType: AgentTypeSchema.optional(),
|
|
||||||
onBranch: z.string().trim().min(1).optional(),
|
onBranch: z.string().trim().min(1).optional(),
|
||||||
});
|
});
|
||||||
export type CreateTaskInput = z.infer<typeof CreateTaskInputSchema>;
|
export type CreateTaskInput = z.infer<typeof CreateTaskInputSchema>;
|
||||||
|
|
@ -69,9 +68,7 @@ export const TaskRecordSchema = z.object({
|
||||||
task: z.string().min(1),
|
task: z.string().min(1),
|
||||||
sandboxProviderId: SandboxProviderIdSchema,
|
sandboxProviderId: SandboxProviderIdSchema,
|
||||||
status: TaskStatusSchema,
|
status: TaskStatusSchema,
|
||||||
statusMessage: z.string().nullable(),
|
|
||||||
activeSandboxId: z.string().nullable(),
|
activeSandboxId: z.string().nullable(),
|
||||||
activeSessionId: z.string().nullable(),
|
|
||||||
sandboxes: z.array(
|
sandboxes: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
sandboxId: z.string().min(1),
|
sandboxId: z.string().min(1),
|
||||||
|
|
@ -83,17 +80,12 @@ export const TaskRecordSchema = z.object({
|
||||||
updatedAt: z.number().int(),
|
updatedAt: z.number().int(),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
agentType: z.string().nullable(),
|
|
||||||
prSubmitted: z.boolean(),
|
|
||||||
diffStat: z.string().nullable(),
|
diffStat: z.string().nullable(),
|
||||||
prUrl: z.string().nullable(),
|
prUrl: z.string().nullable(),
|
||||||
prAuthor: z.string().nullable(),
|
prAuthor: z.string().nullable(),
|
||||||
ciStatus: z.string().nullable(),
|
ciStatus: z.string().nullable(),
|
||||||
reviewStatus: z.string().nullable(),
|
reviewStatus: z.string().nullable(),
|
||||||
reviewer: z.string().nullable(),
|
reviewer: z.string().nullable(),
|
||||||
conflictsWithMain: z.string().nullable(),
|
|
||||||
hasUnpushed: z.string().nullable(),
|
|
||||||
parentBranch: z.string().nullable(),
|
|
||||||
createdAt: z.number().int(),
|
createdAt: z.number().int(),
|
||||||
updatedAt: z.number().int(),
|
updatedAt: z.number().int(),
|
||||||
});
|
});
|
||||||
|
|
@ -112,6 +104,7 @@ export type TaskSummary = z.infer<typeof TaskSummarySchema>;
|
||||||
|
|
||||||
export const TaskActionInputSchema = z.object({
|
export const TaskActionInputSchema = z.object({
|
||||||
organizationId: OrganizationIdSchema,
|
organizationId: OrganizationIdSchema,
|
||||||
|
repoId: RepoIdSchema,
|
||||||
taskId: z.string().min(1),
|
taskId: z.string().min(1),
|
||||||
});
|
});
|
||||||
export type TaskActionInput = z.infer<typeof TaskActionInputSchema>;
|
export type TaskActionInput = z.infer<typeof TaskActionInputSchema>;
|
||||||
|
|
@ -180,7 +173,7 @@ export const HistoryQueryInputSchema = z.object({
|
||||||
});
|
});
|
||||||
export type HistoryQueryInput = z.infer<typeof HistoryQueryInputSchema>;
|
export type HistoryQueryInput = z.infer<typeof HistoryQueryInputSchema>;
|
||||||
|
|
||||||
export const HistoryEventSchema = z.object({
|
export const AuditLogEventSchema = z.object({
|
||||||
id: z.number().int(),
|
id: z.number().int(),
|
||||||
organizationId: OrganizationIdSchema,
|
organizationId: OrganizationIdSchema,
|
||||||
repoId: z.string().nullable(),
|
repoId: z.string().nullable(),
|
||||||
|
|
@ -190,7 +183,7 @@ export const HistoryEventSchema = z.object({
|
||||||
payloadJson: z.string().min(1),
|
payloadJson: z.string().min(1),
|
||||||
createdAt: z.number().int(),
|
createdAt: z.number().int(),
|
||||||
});
|
});
|
||||||
export type HistoryEvent = z.infer<typeof HistoryEventSchema>;
|
export type AuditLogEvent = z.infer<typeof AuditLogEventSchema>;
|
||||||
|
|
||||||
export const PruneInputSchema = z.object({
|
export const PruneInputSchema = z.object({
|
||||||
organizationId: OrganizationIdSchema,
|
organizationId: OrganizationIdSchema,
|
||||||
|
|
@ -201,6 +194,7 @@ export type PruneInput = z.infer<typeof PruneInputSchema>;
|
||||||
|
|
||||||
export const KillInputSchema = z.object({
|
export const KillInputSchema = z.object({
|
||||||
organizationId: OrganizationIdSchema,
|
organizationId: OrganizationIdSchema,
|
||||||
|
repoId: RepoIdSchema,
|
||||||
taskId: z.string().min(1),
|
taskId: z.string().min(1),
|
||||||
deleteBranch: z.boolean(),
|
deleteBranch: z.boolean(),
|
||||||
abandon: z.boolean(),
|
abandon: z.boolean(),
|
||||||
|
|
|
||||||
|
|
@ -3,5 +3,5 @@ export * from "./contracts.js";
|
||||||
export * from "./config.js";
|
export * from "./config.js";
|
||||||
export * from "./logging.js";
|
export * from "./logging.js";
|
||||||
export * from "./realtime-events.js";
|
export * from "./realtime-events.js";
|
||||||
export * from "./workbench.js";
|
export * from "./workspace.js";
|
||||||
export * from "./organization.js";
|
export * from "./organization.js";
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { FoundryAppSnapshot } from "./app-shell.js";
|
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 {
|
export interface SandboxProcessSnapshot {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -16,20 +16,13 @@ export interface SandboxProcessSnapshot {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Organization-level events broadcast by the organization actor. */
|
/** Organization-level events broadcast by the organization actor. */
|
||||||
export type OrganizationEvent =
|
export type OrganizationEvent = { type: "organizationUpdated"; snapshot: OrganizationSummarySnapshot };
|
||||||
| { 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 };
|
|
||||||
|
|
||||||
/** Task-level events broadcast by the task actor. */
|
/** 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. */
|
/** 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. */
|
/** App-level events broadcast by the app organization actor. */
|
||||||
export type AppEvent = { type: "appUpdated"; snapshot: FoundryAppSnapshot };
|
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 WorkspaceTaskStatus = TaskStatus | "new";
|
||||||
export type WorkbenchAgentKind = "Claude" | "Codex" | "Cursor";
|
export type WorkspaceAgentKind = "Claude" | "Codex" | "Cursor";
|
||||||
export type WorkbenchModelId =
|
export type WorkspaceModelId =
|
||||||
| "claude-sonnet-4"
|
| "claude-sonnet-4"
|
||||||
| "claude-opus-4"
|
| "claude-opus-4"
|
||||||
| "gpt-5.3-codex"
|
| "gpt-5.3-codex"
|
||||||
|
|
@ -11,9 +11,9 @@ export type WorkbenchModelId =
|
||||||
| "gpt-5.1-codex-max"
|
| "gpt-5.1-codex-max"
|
||||||
| "gpt-5.2"
|
| "gpt-5.2"
|
||||||
| "gpt-5.1-codex-mini";
|
| "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;
|
id: string;
|
||||||
eventIndex: number;
|
eventIndex: number;
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
|
|
@ -23,23 +23,23 @@ export interface WorkbenchTranscriptEvent {
|
||||||
payload: unknown;
|
payload: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkbenchComposerDraft {
|
export interface WorkspaceComposerDraft {
|
||||||
text: string;
|
text: string;
|
||||||
attachments: WorkbenchLineAttachment[];
|
attachments: WorkspaceLineAttachment[];
|
||||||
updatedAtMs: number | null;
|
updatedAtMs: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Session metadata without transcript content. */
|
/** Session metadata without transcript content. */
|
||||||
export interface WorkbenchSessionSummary {
|
export interface WorkspaceSessionSummary {
|
||||||
id: string;
|
id: string;
|
||||||
/** Stable UI session id used for routing and task-local identity. */
|
/** Stable UI session id used for routing and task-local identity. */
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
/** Underlying sandbox session id when provisioning has completed. */
|
/** Underlying sandbox session id when provisioning has completed. */
|
||||||
sandboxSessionId?: string | null;
|
sandboxSessionId?: string | null;
|
||||||
sessionName: string;
|
sessionName: string;
|
||||||
agent: WorkbenchAgentKind;
|
agent: WorkspaceAgentKind;
|
||||||
model: WorkbenchModelId;
|
model: WorkspaceModelId;
|
||||||
status: WorkbenchSessionStatus;
|
status: WorkspaceSessionStatus;
|
||||||
thinkingSinceMs: number | null;
|
thinkingSinceMs: number | null;
|
||||||
unread: boolean;
|
unread: boolean;
|
||||||
created: boolean;
|
created: boolean;
|
||||||
|
|
@ -47,44 +47,44 @@ export interface WorkbenchSessionSummary {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Full session content — only fetched when viewing a specific session. */
|
/** 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. */
|
/** Stable UI session id used for the session topic key and routing. */
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
sandboxSessionId: string | null;
|
sandboxSessionId: string | null;
|
||||||
sessionName: string;
|
sessionName: string;
|
||||||
agent: WorkbenchAgentKind;
|
agent: WorkspaceAgentKind;
|
||||||
model: WorkbenchModelId;
|
model: WorkspaceModelId;
|
||||||
status: WorkbenchSessionStatus;
|
status: WorkspaceSessionStatus;
|
||||||
thinkingSinceMs: number | null;
|
thinkingSinceMs: number | null;
|
||||||
unread: boolean;
|
unread: boolean;
|
||||||
created: boolean;
|
created: boolean;
|
||||||
errorMessage?: string | null;
|
errorMessage?: string | null;
|
||||||
draft: WorkbenchComposerDraft;
|
draft: WorkspaceComposerDraft;
|
||||||
transcript: WorkbenchTranscriptEvent[];
|
transcript: WorkspaceTranscriptEvent[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkbenchFileChange {
|
export interface WorkspaceFileChange {
|
||||||
path: string;
|
path: string;
|
||||||
added: number;
|
added: number;
|
||||||
removed: number;
|
removed: number;
|
||||||
type: "M" | "A" | "D";
|
type: "M" | "A" | "D";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkbenchFileTreeNode {
|
export interface WorkspaceFileTreeNode {
|
||||||
name: string;
|
name: string;
|
||||||
path: string;
|
path: string;
|
||||||
isDir: boolean;
|
isDir: boolean;
|
||||||
children?: WorkbenchFileTreeNode[];
|
children?: WorkspaceFileTreeNode[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkbenchLineAttachment {
|
export interface WorkspaceLineAttachment {
|
||||||
id: string;
|
id: string;
|
||||||
filePath: string;
|
filePath: string;
|
||||||
lineNumber: number;
|
lineNumber: number;
|
||||||
lineContent: string;
|
lineContent: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkbenchHistoryEvent {
|
export interface WorkspaceHistoryEvent {
|
||||||
id: string;
|
id: string;
|
||||||
messageId: string;
|
messageId: string;
|
||||||
preview: string;
|
preview: string;
|
||||||
|
|
@ -94,78 +94,67 @@ export interface WorkbenchHistoryEvent {
|
||||||
detail: string;
|
detail: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WorkbenchDiffLineKind = "context" | "add" | "remove" | "hunk";
|
export type WorkspaceDiffLineKind = "context" | "add" | "remove" | "hunk";
|
||||||
|
|
||||||
export interface WorkbenchParsedDiffLine {
|
export interface WorkspaceParsedDiffLine {
|
||||||
kind: WorkbenchDiffLineKind;
|
kind: WorkspaceDiffLineKind;
|
||||||
lineNumber: number;
|
lineNumber: number;
|
||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkbenchPullRequestSummary {
|
export interface WorkspacePullRequestSummary {
|
||||||
number: number;
|
|
||||||
status: "draft" | "ready";
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WorkbenchOpenPrSummary {
|
|
||||||
prId: string;
|
|
||||||
repoId: string;
|
|
||||||
repoFullName: string;
|
|
||||||
number: number;
|
number: number;
|
||||||
title: string;
|
title: string;
|
||||||
state: string;
|
state: string;
|
||||||
url: string;
|
url: string;
|
||||||
headRefName: string;
|
headRefName: string;
|
||||||
baseRefName: string;
|
baseRefName: string;
|
||||||
|
repoFullName: string;
|
||||||
authorLogin: string | null;
|
authorLogin: string | null;
|
||||||
isDraft: boolean;
|
isDraft: boolean;
|
||||||
updatedAtMs: number;
|
updatedAtMs: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkbenchSandboxSummary {
|
export interface WorkspaceSandboxSummary {
|
||||||
sandboxProviderId: SandboxProviderId;
|
sandboxProviderId: SandboxProviderId;
|
||||||
sandboxId: string;
|
sandboxId: string;
|
||||||
cwd: string | null;
|
cwd: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sidebar-level task data. Materialized in the organization actor's SQLite. */
|
/** Sidebar-level task data. Materialized in the organization actor's SQLite. */
|
||||||
export interface WorkbenchTaskSummary {
|
export interface WorkspaceTaskSummary {
|
||||||
id: string;
|
id: string;
|
||||||
repoId: string;
|
repoId: string;
|
||||||
title: string;
|
title: string;
|
||||||
status: WorkbenchTaskStatus;
|
status: WorkspaceTaskStatus;
|
||||||
repoName: string;
|
repoName: string;
|
||||||
updatedAtMs: number;
|
updatedAtMs: number;
|
||||||
branch: string | null;
|
branch: string | null;
|
||||||
pullRequest: WorkbenchPullRequestSummary | null;
|
pullRequest: WorkspacePullRequestSummary | null;
|
||||||
/** Summary of sessions — no transcript content. */
|
/** Summary of sessions — no transcript content. */
|
||||||
sessionsSummary: WorkbenchSessionSummary[];
|
sessionsSummary: WorkspaceSessionSummary[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Full task detail — only fetched when viewing a specific task. */
|
/** 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. */
|
/** Original task prompt/instructions shown in the detail view. */
|
||||||
task: string;
|
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. */
|
/** Underlying task runtime status preserved for detail views and error handling. */
|
||||||
runtimeStatus: TaskStatus;
|
runtimeStatus: TaskStatus;
|
||||||
statusMessage: string | null;
|
|
||||||
activeSessionId: string | null;
|
|
||||||
diffStat: string | null;
|
diffStat: string | null;
|
||||||
prUrl: string | null;
|
prUrl: string | null;
|
||||||
reviewStatus: string | null;
|
reviewStatus: string | null;
|
||||||
fileChanges: WorkbenchFileChange[];
|
fileChanges: WorkspaceFileChange[];
|
||||||
diffs: Record<string, string>;
|
diffs: Record<string, string>;
|
||||||
fileTree: WorkbenchFileTreeNode[];
|
fileTree: WorkspaceFileTreeNode[];
|
||||||
minutesUsed: number;
|
minutesUsed: number;
|
||||||
/** Sandbox info for this task. */
|
/** Sandbox info for this task. */
|
||||||
sandboxes: WorkbenchSandboxSummary[];
|
sandboxes: WorkspaceSandboxSummary[];
|
||||||
activeSandboxId: string | null;
|
activeSandboxId: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Repo-level summary for organization sidebar. */
|
/** Repo-level summary for organization sidebar. */
|
||||||
export interface WorkbenchRepositorySummary {
|
export interface WorkspaceRepositorySummary {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
/** Aggregated branch/task overview state (replaces getRepoOverview polling). */
|
/** Aggregated branch/task overview state (replaces getRepoOverview polling). */
|
||||||
|
|
@ -176,121 +165,126 @@ export interface WorkbenchRepositorySummary {
|
||||||
/** Organization-level snapshot — initial fetch for the organization topic. */
|
/** Organization-level snapshot — initial fetch for the organization topic. */
|
||||||
export interface OrganizationSummarySnapshot {
|
export interface OrganizationSummarySnapshot {
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
repos: WorkbenchRepositorySummary[];
|
repos: WorkspaceRepositorySummary[];
|
||||||
taskSummaries: WorkbenchTaskSummary[];
|
taskSummaries: WorkspaceTaskSummary[];
|
||||||
openPullRequests: WorkbenchOpenPrSummary[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkbenchSession extends WorkbenchSessionSummary {
|
export interface WorkspaceSession extends WorkspaceSessionSummary {
|
||||||
draft: WorkbenchComposerDraft;
|
draft: WorkspaceComposerDraft;
|
||||||
transcript: WorkbenchTranscriptEvent[];
|
transcript: WorkspaceTranscriptEvent[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkbenchTask {
|
export interface WorkspaceTask {
|
||||||
id: string;
|
id: string;
|
||||||
repoId: string;
|
repoId: string;
|
||||||
title: string;
|
title: string;
|
||||||
status: WorkbenchTaskStatus;
|
status: WorkspaceTaskStatus;
|
||||||
runtimeStatus?: TaskStatus;
|
runtimeStatus?: TaskStatus;
|
||||||
statusMessage?: string | null;
|
|
||||||
repoName: string;
|
repoName: string;
|
||||||
updatedAtMs: number;
|
updatedAtMs: number;
|
||||||
branch: string | null;
|
branch: string | null;
|
||||||
pullRequest: WorkbenchPullRequestSummary | null;
|
pullRequest: WorkspacePullRequestSummary | null;
|
||||||
sessions: WorkbenchSession[];
|
sessions: WorkspaceSession[];
|
||||||
fileChanges: WorkbenchFileChange[];
|
fileChanges: WorkspaceFileChange[];
|
||||||
diffs: Record<string, string>;
|
diffs: Record<string, string>;
|
||||||
fileTree: WorkbenchFileTreeNode[];
|
fileTree: WorkspaceFileTreeNode[];
|
||||||
minutesUsed: number;
|
minutesUsed: number;
|
||||||
activeSandboxId?: string | null;
|
activeSandboxId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkbenchRepo {
|
export interface WorkspaceRepo {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkbenchRepositorySection {
|
export interface WorkspaceRepositorySection {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
updatedAtMs: number;
|
updatedAtMs: number;
|
||||||
tasks: WorkbenchTask[];
|
tasks: WorkspaceTask[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TaskWorkbenchSnapshot {
|
export interface TaskWorkspaceSnapshot {
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
repos: WorkbenchRepo[];
|
repos: WorkspaceRepo[];
|
||||||
repositories: WorkbenchRepositorySection[];
|
repositories: WorkspaceRepositorySection[];
|
||||||
tasks: WorkbenchTask[];
|
tasks: WorkspaceTask[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkbenchModelOption {
|
export interface WorkspaceModelOption {
|
||||||
id: WorkbenchModelId;
|
id: WorkspaceModelId;
|
||||||
label: string;
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkbenchModelGroup {
|
export interface WorkspaceModelGroup {
|
||||||
provider: string;
|
provider: string;
|
||||||
models: WorkbenchModelOption[];
|
models: WorkspaceModelOption[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TaskWorkbenchSelectInput {
|
export interface TaskWorkspaceSelectInput {
|
||||||
|
repoId: string;
|
||||||
taskId: string;
|
taskId: string;
|
||||||
|
authSessionId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TaskWorkbenchCreateTaskInput {
|
export interface TaskWorkspaceCreateTaskInput {
|
||||||
repoId: string;
|
repoId: string;
|
||||||
task: string;
|
task: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
branch?: string;
|
branch?: string;
|
||||||
onBranch?: string;
|
onBranch?: string;
|
||||||
model?: WorkbenchModelId;
|
model?: WorkspaceModelId;
|
||||||
|
authSessionId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TaskWorkbenchRenameInput {
|
export interface TaskWorkspaceRenameInput {
|
||||||
|
repoId: string;
|
||||||
taskId: string;
|
taskId: string;
|
||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TaskWorkbenchSendMessageInput {
|
export interface TaskWorkspaceSendMessageInput {
|
||||||
taskId: string;
|
taskId: string;
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
text: string;
|
text: string;
|
||||||
attachments: WorkbenchLineAttachment[];
|
attachments: WorkspaceLineAttachment[];
|
||||||
|
authSessionId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TaskWorkbenchSessionInput {
|
export interface TaskWorkspaceSessionInput {
|
||||||
taskId: string;
|
taskId: string;
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
|
authSessionId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TaskWorkbenchRenameSessionInput extends TaskWorkbenchSessionInput {
|
export interface TaskWorkspaceRenameSessionInput extends TaskWorkspaceSessionInput {
|
||||||
title: string;
|
title: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TaskWorkbenchChangeModelInput extends TaskWorkbenchSessionInput {
|
export interface TaskWorkspaceChangeModelInput extends TaskWorkspaceSessionInput {
|
||||||
model: WorkbenchModelId;
|
model: WorkspaceModelId;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TaskWorkbenchUpdateDraftInput extends TaskWorkbenchSessionInput {
|
export interface TaskWorkspaceUpdateDraftInput extends TaskWorkspaceSessionInput {
|
||||||
text: string;
|
text: string;
|
||||||
attachments: WorkbenchLineAttachment[];
|
attachments: WorkspaceLineAttachment[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TaskWorkbenchSetSessionUnreadInput extends TaskWorkbenchSessionInput {
|
export interface TaskWorkspaceSetSessionUnreadInput extends TaskWorkspaceSessionInput {
|
||||||
unread: boolean;
|
unread: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TaskWorkbenchDiffInput {
|
export interface TaskWorkspaceDiffInput {
|
||||||
|
repoId: string;
|
||||||
taskId: string;
|
taskId: string;
|
||||||
path: string;
|
path: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TaskWorkbenchCreateTaskResponse {
|
export interface TaskWorkspaceCreateTaskResponse {
|
||||||
taskId: string;
|
taskId: string;
|
||||||
sessionId?: string;
|
sessionId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TaskWorkbenchAddSessionResponse {
|
export interface TaskWorkspaceAddSessionResponse {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue