diff --git a/docs/openapi.json b/docs/openapi.json index 262f639..a984b28 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -10,7 +10,7 @@ "license": { "name": "Apache-2.0" }, - "version": "0.3.0" + "version": "0.3.1" }, "servers": [ { diff --git a/factory/packages/client/src/workbench-model.ts b/factory/packages/client/src/workbench-model.ts index 13a554b..a0b28b7 100644 --- a/factory/packages/client/src/workbench-model.ts +++ b/factory/packages/client/src/workbench-model.ts @@ -266,20 +266,21 @@ export function removeFileTreePath(nodes: FileTreeNode[], targetPath: string): F export function buildInitialHandoffs(): Handoff[] { return [ + // ── rivet-dev/sandbox-agent ── { id: "h1", - repoId: "acme-backend", - title: "Fix auth token refresh", + repoId: "sandbox-agent", + title: "Normalize Pi ACP bootstrap payloads", status: "idle", - repoName: "acme/backend", - updatedAtMs: minutesAgo(2), - branch: "fix/auth-token-refresh", - pullRequest: { number: 47, status: "draft" }, + repoName: "rivet-dev/sandbox-agent", + updatedAtMs: minutesAgo(8), + branch: "NathanFlurry/pi-bootstrap-fix", + pullRequest: { number: 227, status: "ready" }, tabs: [ { id: "t1", sessionId: "t1", - sessionName: "Auth token fix", + sessionName: "Pi payload fix", agent: "Claude", model: "claude-sonnet-4", status: "idle", @@ -292,45 +293,45 @@ export function buildInitialHandoffs(): Handoff[] { id: "m1", role: "agent", agent: "claude", - createdAtMs: minutesAgo(12), + createdAtMs: minutesAgo(18), lines: [ - "I'll fix the auth token refresh logic. Let me start by examining the current implementation in `src/auth/token-manager.ts`.", + "I'll fix the Pi agent ACP bootstrap payloads. The `initialize` method sends `protocolVersion` as a string but Pi expects a number. Let me examine `acp_proxy_runtime.rs`.", "", - "Found the issue - the refresh interval is set to 1 hour but the token expires in 5 minutes. Updating now.", + "Found the issue — the ACP proxy forwards the raw JSON-RPC payload without normalizing field types per-agent. Adding a `normalize_payload_for_agent` pass before dispatch.", ], - durationMs: 12_000, + durationMs: 14_000, }, { id: "m2", role: "agent", agent: "claude", - createdAtMs: minutesAgo(11), + createdAtMs: minutesAgo(15), lines: [ - "Fixed token refresh in `src/auth/token-manager.ts`. Also updated the retry logic in `src/api/client.ts` to handle 401 responses gracefully.", + "Done. Added `normalize_pi_payload()` in `acp_proxy_runtime.rs` that converts `protocolVersion` from string to number for `initialize`, and ensures `mcpServers` is present in `session/new` params.", ], - durationMs: 18_000, + durationMs: 22_000, }, { id: "m3", role: "user", agent: null, - createdAtMs: minutesAgo(10), - lines: ["Can you also add unit tests for the refresh logic?"], + createdAtMs: minutesAgo(12), + lines: ["Does this also handle the case where protocolVersion is a float string like \"2.0\"?"], }, { id: "m4", role: "agent", agent: "claude", - createdAtMs: minutesAgo(9), - lines: ["Writing tests now in `src/auth/__tests__/token-manager.test.ts`..."], - durationMs: 9_000, + createdAtMs: minutesAgo(11), + lines: ["Yes — the `parse_json_number` helper tries u64, then i64, then f64 parsing in order. So \"2.0\" becomes `2.0` as a JSON number."], + durationMs: 8_000, }, ]), }, { id: "t2", sessionId: "t2", - sessionName: "Code analysis", + sessionName: "Test coverage", agent: "Codex", model: "gpt-4o", status: "idle", @@ -343,147 +344,191 @@ export function buildInitialHandoffs(): Handoff[] { id: "m5", role: "agent", agent: "codex", - createdAtMs: minutesAgo(15), - lines: ["Analyzed the codebase. The auth module uses a simple in-memory token cache with no refresh mechanism."], - durationMs: 21_000, - }, - { - id: "m6", - role: "agent", - agent: "codex", - createdAtMs: minutesAgo(14), - lines: ["Suggested approach: add a refresh timer that fires before token expiry. I'll wait for instructions."], - durationMs: 7_000, + createdAtMs: minutesAgo(20), + lines: ["Analyzed the normalize_pi_payload function. It handles `initialize` and `session/new` methods. I'll add unit tests for edge cases."], + durationMs: 18_000, }, ]), }, ], fileChanges: [ - { path: "src/auth/token-manager.ts", added: 18, removed: 5, type: "M" }, - { path: "src/api/client.ts", added: 8, removed: 3, type: "M" }, - { path: "src/auth/__tests__/token-manager.test.ts", added: 21, removed: 0, type: "A" }, - { path: "src/types/auth.ts", added: 0, removed: 4, type: "M" }, + { path: "server/packages/sandbox-agent/src/acp_proxy_runtime.rs", added: 51, removed: 0, type: "M" }, + { path: "server/packages/sandbox-agent/src/acp_proxy_runtime_test.rs", added: 38, removed: 0, type: "A" }, ], diffs: { - "src/auth/token-manager.ts": [ - "@@ -21,10 +21,15 @@ import { TokenCache } from './cache';", - " export class TokenManager {", - " private refreshInterval: number;", + "server/packages/sandbox-agent/src/acp_proxy_runtime.rs": [ + "@@ -134,6 +134,8 @@ impl AcpProxyRuntime {", + " \"acp_proxy: instance resolved\"", + " );", " ", - "- const REFRESH_MS = 3_600_000; // 1 hour", - "+ const REFRESH_MS = 300_000; // 5 minutes", - " ", - "+ async refreshToken(): Promise {", - "+ const newToken = await this.fetchNewToken();", - "+ this.cache.set(newToken);", - "+ return newToken;", - "+ }", - " ", - "- private async onExpiry() {", - "- console.log('token expired');", - "- this.logout();", - "- }", - "+ private async onExpiry() {", - "+ try {", - "+ await this.refreshToken();", - "+ } catch { this.logout(); }", - "+ }", - ].join("\n"), - "src/api/client.ts": [ - "@@ -45,8 +45,16 @@ export class ApiClient {", - " private async request(url: string, opts?: RequestInit): Promise {", - " const token = await this.tokenManager.getToken();", - " const res = await fetch(url, {", - "- ...opts,", - "- headers: { Authorization: `Bearer ${token}` },", - "+ ...opts, headers: {", - "+ ...opts?.headers,", - "+ Authorization: `Bearer ${token}`,", - "+ },", - " });", - "- return res.json();", - "+ if (res.status === 401) {", - "+ const freshToken = await this.tokenManager.refreshToken();", - "+ const retry = await fetch(url, {", - "+ ...opts, headers: { ...opts?.headers, Authorization: `Bearer ${freshToken}` },", - "+ });", - "+ return retry.json();", - "+ }", - "+ return res.json() as T;", - " }", - ].join("\n"), - "src/auth/__tests__/token-manager.test.ts": [ - "@@ -0,0 +1,21 @@", - "+import { describe, it, expect, vi } from 'vitest';", - "+import { TokenManager } from '../token-manager';", + "+ let payload = normalize_payload_for_agent(instance.agent, payload);", "+", - "+describe('TokenManager', () => {", - "+ it('refreshes token before expiry', async () => {", - "+ const mgr = new TokenManager({ expiresIn: 100 });", - "+ const first = await mgr.getToken();", - "+ await new Promise(r => setTimeout(r, 150));", - "+ const second = await mgr.getToken();", - "+ expect(second).not.toBe(first);", - "+ });", - "+", - "+ it('retries on 401', async () => {", - "+ const mgr = new TokenManager();", - "+ const spy = vi.spyOn(mgr, 'refreshToken');", - "+ await mgr.handleUnauthorized();", - "+ expect(spy).toHaveBeenCalledOnce();", - "+ });", - "+", - "+ it('logs out after max retries', async () => {", - "+ const mgr = new TokenManager({ maxRetries: 0 });", - "+ await expect(mgr.handleUnauthorized()).rejects.toThrow();", - "+ });", - "+});", - ].join("\n"), - "src/types/auth.ts": [ - "@@ -8,10 +8,6 @@ export interface AuthConfig {", - " clientId: string;", - " clientSecret: string;", - " redirectUri: string;", - "- /** @deprecated Use refreshInterval instead */", - "- legacyTimeout?: number;", - "- /** @deprecated */", - "- useOldRefresh?: boolean;", + " match instance.runtime.post(payload).await {", + "@@ -510,6 +512,57 @@ fn map_adapter_error(err: AdapterError) -> SandboxError {", " }", " ", - " export interface TokenPayload {", + "+fn normalize_payload_for_agent(agent: AgentId, payload: Value) -> Value {", + "+ if agent != AgentId::Pi {", + "+ return payload;", + "+ }", + "+ normalize_pi_payload(payload)", + "+}", + "+", + "+fn normalize_pi_payload(mut payload: Value) -> Value {", + "+ let method = payload", + "+ .get(\"method\")", + "+ .and_then(Value::as_str)", + "+ .unwrap_or_default();", + "+", + "+ match method {", + "+ \"initialize\" => {", + "+ if let Some(protocol) = payload.pointer_mut(\"/params/protocolVersion\") {", + "+ if let Some(raw) = protocol.as_str() {", + "+ if let Some(number) = parse_json_number(raw) {", + "+ *protocol = Value::Number(number);", + "+ }", + "+ }", + "+ }", + "+ }", + "+ \"session/new\" => {", + "+ if let Some(params) = payload.get_mut(\"params\").and_then(Value::as_object_mut) {", + "+ params.entry(\"mcpServers\".to_string())", + "+ .or_insert_with(|| Value::Array(Vec::new()));", + "+ }", + "+ }", + "+ _ => {}", + "+ }", + "+ payload", + "+}", ].join("\n"), }, fileTree: [ { - name: "src", - path: "src", + name: "server", + path: "server", isDir: true, children: [ { - name: "api", - path: "src/api", - isDir: true, - children: [{ name: "client.ts", path: "src/api/client.ts", isDir: false }], - }, - { - name: "auth", - path: "src/auth", + name: "packages", + path: "server/packages", isDir: true, children: [ { - name: "__tests__", - path: "src/auth/__tests__", + name: "sandbox-agent", + path: "server/packages/sandbox-agent", isDir: true, - children: [{ name: "token-manager.test.ts", path: "src/auth/__tests__/token-manager.test.ts", isDir: false }], + children: [ + { + name: "src", + path: "server/packages/sandbox-agent/src", + isDir: true, + children: [ + { name: "acp_proxy_runtime.rs", path: "server/packages/sandbox-agent/src/acp_proxy_runtime.rs", isDir: false }, + { name: "acp_proxy_runtime_test.rs", path: "server/packages/sandbox-agent/src/acp_proxy_runtime_test.rs", isDir: false }, + ], + }, + ], }, - { name: "token-manager.ts", path: "src/auth/token-manager.ts", isDir: false }, ], }, + ], + }, + ], + }, + { + id: "h2", + repoId: "sandbox-agent", + title: "Auto-inject builtin agent skills at startup", + status: "running", + repoName: "rivet-dev/sandbox-agent", + updatedAtMs: minutesAgo(3), + branch: "feat/builtin-agent-skills", + pullRequest: { number: 223, status: "draft" }, + tabs: [ + { + id: "t3", + sessionId: "t3", + sessionName: "Skills injection", + agent: "Claude", + model: "claude-opus-4", + status: "running", + thinkingSinceMs: NOW_MS - 45_000, + unread: false, + created: true, + draft: { text: "", attachments: [], updatedAtMs: null }, + transcript: transcriptFromLegacyMessages("t3", [ { - name: "types", - path: "src/types", + id: "m10", + role: "user", + agent: null, + createdAtMs: minutesAgo(30), + lines: ["Add builtin skill injection to agent startup. Skills should be loaded from the skills registry and written to the agent's CLAUDE.md."], + }, + { + id: "m11", + role: "agent", + agent: "claude", + createdAtMs: minutesAgo(28), + lines: [ + "I'll implement this in the agent management package. The approach:", + "1. Load skills from the registry during agent install", + "2. Inject skill definitions into the agent's working directory as `.claude/skills/`", + "3. Append skill references to CLAUDE.md if present", + "", + "Working on `server/packages/agent-management/src/agents/install.rs` now...", + ], + durationMs: 32_000, + }, + ]), + }, + ], + fileChanges: [ + { path: "server/packages/agent-management/src/agents/install.rs", added: 87, removed: 12, type: "M" }, + { path: "server/packages/agent-management/src/skills/mod.rs", added: 145, removed: 0, type: "A" }, + { path: "server/packages/agent-management/src/skills/registry.rs", added: 63, removed: 0, type: "A" }, + ], + diffs: {}, + fileTree: [ + { + name: "server", + path: "server", + isDir: true, + children: [ + { + name: "packages", + path: "server/packages", isDir: true, - children: [{ name: "auth.ts", path: "src/types/auth.ts", isDir: false }], + children: [ + { + name: "agent-management", + path: "server/packages/agent-management", + isDir: true, + children: [ + { + name: "src", + path: "server/packages/agent-management/src", + isDir: true, + children: [ + { + name: "agents", + path: "server/packages/agent-management/src/agents", + isDir: true, + children: [{ name: "install.rs", path: "server/packages/agent-management/src/agents/install.rs", isDir: false }], + }, + { + name: "skills", + path: "server/packages/agent-management/src/skills", + isDir: true, + children: [ + { name: "mod.rs", path: "server/packages/agent-management/src/skills/mod.rs", isDir: false }, + { name: "registry.rs", path: "server/packages/agent-management/src/skills/registry.rs", isDir: false }, + ], + }, + ], + }, + ], + }, + ], }, ], }, @@ -491,20 +536,20 @@ export function buildInitialHandoffs(): Handoff[] { }, { id: "h3", - repoId: "acme-backend", - title: "Refactor user service", + repoId: "sandbox-agent", + title: "Add hooks example for Claude, Codex, and OpenCode", status: "idle", - repoName: "acme/backend", - updatedAtMs: minutesAgo(5), - branch: "refactor/user-service", - pullRequest: { number: 52, status: "ready" }, + repoName: "rivet-dev/sandbox-agent", + updatedAtMs: minutesAgo(45), + branch: "hooks-example", + pullRequest: { number: 225, status: "ready" }, tabs: [ { id: "t4", sessionId: "t4", - sessionName: "DI refactor", + sessionName: "Example docs", agent: "Claude", - model: "claude-opus-4", + model: "claude-sonnet-4", status: "idle", thinkingSinceMs: null, unread: false, @@ -515,325 +560,68 @@ export function buildInitialHandoffs(): Handoff[] { id: "m20", role: "user", agent: null, - createdAtMs: minutesAgo(35), - lines: ["Refactor the user service to use dependency injection."], + createdAtMs: minutesAgo(60), + lines: ["Create an example showing how to use hooks with Claude, Codex, and OpenCode agents."], }, { id: "m21", role: "agent", agent: "claude", - createdAtMs: minutesAgo(34), - lines: ["Starting refactor. I'll extract interfaces and set up a DI container..."], - durationMs: 14_000, + createdAtMs: minutesAgo(58), + lines: [ + "Done. Created `examples/hooks/` with a TypeScript example that demonstrates lifecycle hooks for all three agents. Includes `onPermissionRequest`, `onSessionEvent`, and `onAgentOutput` hooks.", + ], + durationMs: 16_000, }, ]), }, ], fileChanges: [ - { path: "src/services/user-service.ts", added: 45, removed: 30, type: "M" }, - { path: "src/services/interfaces.ts", added: 22, removed: 0, type: "A" }, - { path: "src/container.ts", added: 15, removed: 0, type: "A" }, + { path: "examples/hooks/src/index.ts", added: 120, removed: 0, type: "A" }, + { path: "examples/hooks/package.json", added: 18, removed: 0, type: "A" }, + { path: "examples/hooks/tsconfig.json", added: 12, removed: 0, type: "A" }, ], - diffs: { - "src/services/user-service.ts": [ - "@@ -1,35 +1,50 @@", - "-import { db } from '../db';", - "-import { hashPassword, verifyPassword } from '../utils/crypto';", - "-import { sendEmail } from '../utils/email';", - "+import type { IUserRepository, IEmailService, IHashService } from './interfaces';", - " ", - "-export class UserService {", - "- async createUser(email: string, password: string) {", - "- const hash = await hashPassword(password);", - "- const user = await db.users.create({", - "- email,", - "- passwordHash: hash,", - "- createdAt: new Date(),", - "- });", - "- await sendEmail(email, 'Welcome!', 'Thanks for signing up.');", - "- return user;", - "+export class UserService {", - "+ constructor(", - "+ private readonly users: IUserRepository,", - "+ private readonly email: IEmailService,", - "+ private readonly hash: IHashService,", - "+ ) {}", - "+", - "+ async createUser(email: string, password: string) {", - "+ const passwordHash = await this.hash.hash(password);", - "+ const user = await this.users.create({ email, passwordHash });", - "+ await this.email.send(email, 'Welcome!', 'Thanks for signing up.');", - "+ return user;", - " }", - " ", - "- async authenticate(email: string, password: string) {", - "- const user = await db.users.findByEmail(email);", - "+ async authenticate(email: string, password: string) {", - "+ const user = await this.users.findByEmail(email);", - " if (!user) throw new Error('User not found');", - "- const valid = await verifyPassword(password, user.passwordHash);", - "+ const valid = await this.hash.verify(password, user.passwordHash);", - " if (!valid) throw new Error('Invalid password');", - " return user;", - " }", - " ", - "- async deleteUser(id: string) {", - "- await db.users.delete(id);", - "+ async deleteUser(id: string) {", - "+ const user = await this.users.findById(id);", - "+ if (!user) throw new Error('User not found');", - "+ await this.users.delete(id);", - "+ await this.email.send(user.email, 'Account deleted', 'Your account has been removed.');", - " }", - " }", - ].join("\n"), - "src/services/interfaces.ts": [ - "@@ -0,0 +1,22 @@", - "+export interface IUserRepository {", - "+ create(data: { email: string; passwordHash: string }): Promise;", - "+ findByEmail(email: string): Promise;", - "+ findById(id: string): Promise;", - "+ delete(id: string): Promise;", - "+}", - "+", - "+export interface IEmailService {", - "+ send(to: string, subject: string, body: string): Promise;", - "+}", - "+", - "+export interface IHashService {", - "+ hash(password: string): Promise;", - "+ verify(password: string, hash: string): Promise;", - "+}", - "+", - "+export interface User {", - "+ id: string;", - "+ email: string;", - "+ passwordHash: string;", - "+ createdAt: Date;", - "+}", - ].join("\n"), - "src/container.ts": [ - "@@ -0,0 +1,15 @@", - "+import { UserService } from './services/user-service';", - "+import { DrizzleUserRepository } from './repos/user-repo';", - "+import { ResendEmailService } from './providers/email';", - "+import { Argon2HashService } from './providers/hash';", - "+import { db } from './db';", - "+", - "+const userRepo = new DrizzleUserRepository(db);", - "+const emailService = new ResendEmailService();", - "+const hashService = new Argon2HashService();", - "+", - "+export const userService = new UserService(", - "+ userRepo,", - "+ emailService,", - "+ hashService,", - "+);", - ].join("\n"), - }, + diffs: {}, fileTree: [ { - name: "src", - path: "src", + name: "examples", + path: "examples", isDir: true, children: [ - { name: "container.ts", path: "src/container.ts", isDir: false }, { - name: "services", - path: "src/services", + name: "hooks", + path: "examples/hooks", isDir: true, children: [ - { name: "interfaces.ts", path: "src/services/interfaces.ts", isDir: false }, - { name: "user-service.ts", path: "src/services/user-service.ts", isDir: false }, + { name: "package.json", path: "examples/hooks/package.json", isDir: false }, + { name: "tsconfig.json", path: "examples/hooks/tsconfig.json", isDir: false }, + { + name: "src", + path: "examples/hooks/src", + isDir: true, + children: [{ name: "index.ts", path: "examples/hooks/src/index.ts", isDir: false }], + }, ], }, ], }, ], }, + // ── rivet-dev/rivet ── { - id: "h2", - repoId: "acme-frontend", - title: "Add dark mode toggle", + id: "h4", + repoId: "rivet", + title: "Add actor reschedule endpoint", status: "idle", - repoName: "acme/frontend", + repoName: "rivet-dev/rivet", updatedAtMs: minutesAgo(15), - branch: "feat/dark-mode", - pullRequest: null, + branch: "actor-reschedule-endpoint", + pullRequest: { number: 4400, status: "ready" }, tabs: [ { - id: "t3", - sessionId: "t3", - sessionName: "Dark mode", - agent: "Claude", - model: "claude-sonnet-4", - status: "idle", - thinkingSinceMs: null, - unread: true, - created: true, - draft: { text: "", attachments: [], updatedAtMs: null }, - transcript: transcriptFromLegacyMessages("t3", [ - { - id: "m10", - role: "user", - agent: null, - createdAtMs: minutesAgo(75), - lines: ["Add a dark mode toggle to the settings page."], - }, - { - id: "m11", - role: "agent", - agent: "claude", - createdAtMs: minutesAgo(74), - lines: [ - "I've added a dark mode toggle to the settings page. The implementation uses CSS custom properties for theming and persists the user's preference to localStorage.", - ], - durationMs: 26_000, - }, - ]), - }, - ], - fileChanges: [ - { path: "src/components/settings.tsx", added: 32, removed: 2, type: "M" }, - { path: "src/styles/theme.css", added: 45, removed: 0, type: "A" }, - ], - diffs: { - "src/components/settings.tsx": [ - "@@ -1,5 +1,6 @@", - " import React, { useState, useEffect } from 'react';", - "+import { useTheme } from '../hooks/use-theme';", - " import { Card } from './ui/card';", - " import { Toggle } from './ui/toggle';", - " import { Label } from './ui/label';", - "@@ -15,8 +16,38 @@ export function Settings() {", - " const [notifications, setNotifications] = useState(true);", - "+ const { theme, setTheme } = useTheme();", - "+ const [isDark, setIsDark] = useState(theme === 'dark');", - " ", - " return (", - '
', - "+ ", - "+

Appearance

", - '+
', - '+ ', - "+ {", - "+ setIsDark(checked);", - "+ setTheme(checked ? 'dark' : 'light');", - "+ document.documentElement.setAttribute(", - "+ 'data-theme',", - "+ checked ? 'dark' : 'light'", - "+ );", - "+ }}", - "+ />", - "+
", - '+

', - "+ Toggle between light and dark color schemes.", - "+ Your preference is saved to localStorage.", - "+

", - "+
", - "+", - " ", - "

Notifications

", - '
', - "- ", - "- ", - '+ ', - '+ ', - "
", - "
", - ].join("\n"), - "src/styles/theme.css": [ - "@@ -0,0 +1,45 @@", - "+:root {", - "+ --bg-primary: #ffffff;", - "+ --bg-secondary: #f8f9fa;", - "+ --bg-tertiary: #e9ecef;", - "+ --text-primary: #212529;", - "+ --text-secondary: #495057;", - "+ --text-muted: #868e96;", - "+ --border-color: #dee2e6;", - "+ --accent: #228be6;", - "+ --accent-hover: #1c7ed6;", - "+}", - "+", - "+[data-theme='dark'] {", - "+ --bg-primary: #09090b;", - "+ --bg-secondary: #18181b;", - "+ --bg-tertiary: #27272a;", - "+ --text-primary: #fafafa;", - "+ --text-secondary: #a1a1aa;", - "+ --text-muted: #71717a;", - "+ --border-color: #3f3f46;", - "+ --accent: #ff4f00;", - "+ --accent-hover: #ff6a00;", - "+}", - "+", - "+body {", - "+ background: var(--bg-primary);", - "+ color: var(--text-primary);", - "+ transition: background 0.2s ease, color 0.2s ease;", - "+}", - "+", - "+.card {", - "+ background: var(--bg-secondary);", - "+ border: 1px solid var(--border-color);", - "+ border-radius: 8px;", - "+ padding: 16px 20px;", - "+}", - "+", - "+.setting-row {", - "+ display: flex;", - "+ align-items: center;", - "+ justify-content: space-between;", - "+ padding: 8px 0;", - "+}", - "+", - "+.setting-description {", - "+ color: var(--text-muted);", - "+ font-size: 13px;", - "+ margin-top: 4px;", - "+}", - ].join("\n"), - }, - fileTree: [ - { - name: "src", - path: "src", - isDir: true, - children: [ - { - name: "components", - path: "src/components", - isDir: true, - children: [{ name: "settings.tsx", path: "src/components/settings.tsx", isDir: false }], - }, - { - name: "styles", - path: "src/styles", - isDir: true, - children: [{ name: "theme.css", path: "src/styles/theme.css", isDir: false }], - }, - ], - }, - ], - }, - { - id: "h5", - repoId: "acme-infra", - title: "Update CI pipeline", - status: "archived", - repoName: "acme/infra", - updatedAtMs: minutesAgo(2 * 24 * 60 + 10), - branch: "chore/ci-pipeline", - pullRequest: { number: 38, status: "ready" }, - tabs: [ - { - id: "t6", - sessionId: "t6", - sessionName: "CI pipeline", + id: "t5", + sessionId: "t5", + sessionName: "Reschedule API", agent: "Claude", model: "claude-sonnet-4", status: "idle", @@ -841,87 +629,493 @@ export function buildInitialHandoffs(): Handoff[] { unread: false, created: true, draft: { text: "", attachments: [], updatedAtMs: null }, - transcript: transcriptFromLegacyMessages("t6", [ + transcript: transcriptFromLegacyMessages("t5", [ { id: "m30", + role: "user", + agent: null, + createdAtMs: minutesAgo(90), + lines: ["Implement a POST /actors/{actor_id}/reschedule endpoint that signals the actor workflow to reschedule."], + }, + { + id: "m31", role: "agent", agent: "claude", - createdAtMs: minutesAgo(2 * 24 * 60 + 60), - lines: ["CI pipeline updated. Added caching for node_modules and parallel test execution."], - durationMs: 33_000, + createdAtMs: minutesAgo(87), + lines: [ + "I'll add the reschedule endpoint to `api-peer`. The flow is:", + "1. Resolve actor by ID and verify namespace ownership", + "2. Send `Reschedule` signal to the actor workflow", + "3. Return 200 on success, 404 if actor not found", + "", + "Created `engine/packages/api-peer/src/actors/reschedule.rs` and wired it into the router.", + ], + durationMs: 28_000, }, ]), }, ], - fileChanges: [{ path: ".github/workflows/ci.yml", added: 20, removed: 8, type: "M" }], + fileChanges: [ + { path: "engine/packages/api-peer/src/actors/reschedule.rs", added: 64, removed: 0, type: "A" }, + { path: "engine/packages/api-peer/src/actors/mod.rs", added: 1, removed: 0, type: "M" }, + { path: "engine/packages/api-peer/src/router.rs", added: 12, removed: 3, type: "M" }, + { path: "engine/packages/api-types/src/actors/reschedule.rs", added: 24, removed: 0, type: "A" }, + ], diffs: { - ".github/workflows/ci.yml": [ - "@@ -12,14 +12,26 @@ jobs:", - " build:", - " runs-on: ubuntu-latest", - " steps:", - "- - uses: actions/checkout@v3", - "- - uses: actions/setup-node@v3", - "+ - uses: actions/checkout@v4", - "+ - uses: actions/setup-node@v4", - " with:", - " node-version: 20", - "- - run: npm ci", - "- - run: npm run build", - "- - run: npm test", - "+ cache: 'pnpm'", - "+ - uses: pnpm/action-setup@v4", - "+ - run: pnpm install --frozen-lockfile", - "+ - run: pnpm build", + "engine/packages/api-peer/src/actors/reschedule.rs": [ + "@@ -0,0 +1,64 @@", + "+use anyhow::Result;", + "+use gas::prelude::*;", + "+use rivet_api_builder::ApiCtx;", + "+use rivet_api_types::actors::reschedule::*;", + "+use rivet_util::Id;", "+", - "+ test:", - "+ runs-on: ubuntu-latest", - "+ needs: build", - "+ strategy:", - "+ matrix:", - "+ shard: [1, 2, 3]", - "+ steps:", - "+ - uses: actions/checkout@v4", - "+ - uses: actions/setup-node@v4", - "+ with:", - "+ node-version: 20", - "+ cache: 'pnpm'", - "+ - uses: pnpm/action-setup@v4", - "+ - run: pnpm install --frozen-lockfile", - "+ - run: pnpm test -- --shard=${{ matrix.shard }}/3", - " ", - "- deploy:", - "- needs: build", - "- if: github.ref == 'refs/heads/main'", - "+ deploy:", - "+ needs: [build, test]", - "+ if: github.ref == 'refs/heads/main'", + "+#[utoipa::path(", + "+ post,", + "+ operation_id = \"actors_reschedule\",", + "+ path = \"/actors/{actor_id}/reschedule\",", + "+)]", + "+#[tracing::instrument(skip_all)]", + "+pub async fn reschedule(", + "+ ctx: ApiCtx,", + "+ path: ReschedulePath,", + "+ query: RescheduleQuery,", + "+) -> Result {", + "+ let actors_res = ctx.op(pegboard::ops::actor::get::Input {", + "+ actor_ids: vec![path.actor_id],", + "+ fetch_error: false,", + "+ }).await?;", + "+", + "+ let actor = actors_res.actors.into_iter().next()", + "+ .ok_or_else(|| pegboard::errors::Actor::NotFound.build())?;", + "+", + "+ ctx.signal(pegboard::workflows::actor::Reschedule {", + "+ reset_rescheduling: true,", + "+ })", + "+ .to_workflow::()", + "+ .tag(\"actor_id\", path.actor_id)", + "+ .send().await?;", + "+", + "+ Ok(RescheduleResponse {})", + "+}", ].join("\n"), }, fileTree: [ { - name: ".github", - path: ".github", + name: "engine", + path: "engine", isDir: true, children: [ { - name: "workflows", - path: ".github/workflows", + name: "packages", + path: "engine/packages", isDir: true, - children: [{ name: "ci.yml", path: ".github/workflows/ci.yml", isDir: false }], + children: [ + { + name: "api-peer", + path: "engine/packages/api-peer", + isDir: true, + children: [ + { + name: "src", + path: "engine/packages/api-peer/src", + isDir: true, + children: [ + { + name: "actors", + path: "engine/packages/api-peer/src/actors", + isDir: true, + children: [ + { name: "mod.rs", path: "engine/packages/api-peer/src/actors/mod.rs", isDir: false }, + { name: "reschedule.rs", path: "engine/packages/api-peer/src/actors/reschedule.rs", isDir: false }, + ], + }, + { name: "router.rs", path: "engine/packages/api-peer/src/router.rs", isDir: false }, + ], + }, + ], + }, + ], }, ], }, ], }, + { + id: "h5", + repoId: "rivet", + title: "Dynamic actors", + status: "idle", + repoName: "rivet-dev/rivet", + updatedAtMs: minutesAgo(35), + branch: "feat/dynamic-actors", + pullRequest: { number: 4395, status: "draft" }, + tabs: [ + { + id: "t6", + sessionId: "t6", + sessionName: "Dynamic actors impl", + agent: "Claude", + model: "claude-opus-4", + status: "idle", + thinkingSinceMs: null, + unread: true, + created: true, + draft: { text: "", attachments: [], updatedAtMs: null }, + transcript: transcriptFromLegacyMessages("t6", [ + { + id: "m40", + role: "user", + agent: null, + createdAtMs: minutesAgo(120), + lines: ["Implement dynamic actor support — actors that can be created at runtime without pre-registration in the registry."], + }, + { + id: "m41", + role: "agent", + agent: "claude", + createdAtMs: minutesAgo(115), + lines: [ + "This is a large change spanning the RivetKit runtime, the engine scheduler, and the SDK. I'll start with the core runtime changes and work outward.", + "", + "Key design decisions:", + "- Dynamic actors use a special `__dynamic` registry entry", + "- They receive their behavior module at creation time via `createDynamic()`", + "- State persistence works identically to registered actors", + ], + durationMs: 45_000, + }, + ]), + }, + ], + fileChanges: [ + { path: "rivetkit-typescript/packages/rivetkit/src/dynamic.ts", added: 280, removed: 0, type: "A" }, + { path: "rivetkit-typescript/packages/rivetkit/src/registry.ts", added: 45, removed: 12, type: "M" }, + { path: "engine/packages/pegboard/src/workflows/actor.rs", added: 120, removed: 30, type: "M" }, + ], + diffs: {}, + fileTree: [], + }, + // ── rivet-dev/cloud ── + { + id: "h6", + repoId: "cloud", + title: "Use full cloud run pool name for routing", + status: "idle", + repoName: "rivet-dev/cloud", + updatedAtMs: minutesAgo(25), + branch: "fix-use-full-cloud-run-pool-name", + pullRequest: { number: 235, status: "ready" }, + tabs: [ + { + id: "t7", + sessionId: "t7", + sessionName: "Pool routing fix", + agent: "Claude", + model: "claude-sonnet-4", + status: "idle", + thinkingSinceMs: null, + unread: false, + created: true, + draft: { text: "", attachments: [], updatedAtMs: null }, + transcript: transcriptFromLegacyMessages("t7", [ + { + id: "m50", + role: "agent", + agent: "claude", + createdAtMs: minutesAgo(40), + lines: [ + "Fixed the managed pool routing issue. The Cloud Run service was using a truncated pool name for routing, causing 404s on pools with long names. Updated the gateway routing endpoint to use the full pool name.", + ], + durationMs: 24_000, + }, + { + id: "m51", + role: "user", + agent: null, + createdAtMs: minutesAgo(38), + lines: ["Does this also update the SDK type exports?"], + }, + { + id: "m52", + role: "agent", + agent: "claude", + createdAtMs: minutesAgo(36), + lines: ["Yes — the `Registry` type is now exported from `actors/index.ts` so downstream consumers can reference it. Also bumped rivetkit to `2.0.4-rc.1` in pnpm overrides."], + durationMs: 11_000, + }, + ]), + }, + ], + fileChanges: [ + { path: "packages/api/src/actors/index.ts", added: 4, removed: 2, type: "M" }, + { path: "package.json", added: 2, removed: 1, type: "M" }, + { path: "packages/api/scripts/managed-pools-e2e.ts", added: 2, removed: 2, type: "M" }, + ], + diffs: { + "packages/api/src/actors/index.ts": [ + "@@ -28,6 +28,8 @@ export const registry = setup({", + " inspector: {},", + " });", + " ", + "+export type Registry = typeof registry;", + "+", + " export type ActorClient = ReturnType;", + " ", + " let _client: ActorClient | null = null;", + "@@ -37,7 +39,7 @@ function createActorClient() {", + " const managerPort = process.env.RIVETKIT_MANAGER_PORT", + " ? Number.parseInt(process.env.RIVETKIT_MANAGER_PORT, 10)", + " : 6420;", + "- return createClient({", + "+ return createClient({", + " endpoint: `http://127.0.0.1:${managerPort}`,", + ].join("\n"), + }, + fileTree: [ + { + name: "packages", + path: "packages", + isDir: true, + children: [ + { + name: "api", + path: "packages/api", + isDir: true, + children: [ + { + name: "src", + path: "packages/api/src", + isDir: true, + children: [ + { + name: "actors", + path: "packages/api/src/actors", + isDir: true, + children: [{ name: "index.ts", path: "packages/api/src/actors/index.ts", isDir: false }], + }, + ], + }, + ], + }, + ], + }, + ], + }, + // ── rivet-dev/engine-ee ── + { + id: "h7", + repoId: "engine-ee", + title: "Route compute gateway path correctly", + status: "idle", + repoName: "rivet-dev/engine-ee", + updatedAtMs: minutesAgo(50), + branch: "fix-guard-support-https-targets", + pullRequest: { number: 125, status: "ready" }, + tabs: [ + { + id: "t8", + sessionId: "t8", + sessionName: "Guard routing", + agent: "Claude", + model: "claude-sonnet-4", + status: "idle", + thinkingSinceMs: null, + unread: false, + created: true, + draft: { text: "", attachments: [], updatedAtMs: null }, + transcript: transcriptFromLegacyMessages("t8", [ + { + id: "m60", + role: "agent", + agent: "claude", + createdAtMs: minutesAgo(65), + lines: [ + "Fixed the guard proxy to support HTTPS targets and correct compute gateway path routing. The proxy was using an HTTP-only connector — switched to `hyper_tls::HttpsConnector`. Also fixed path-based routing to strip the `/compute/gateway` prefix before forwarding.", + ], + durationMs: 30_000, + }, + ]), + }, + ], + fileChanges: [ + { path: "engine/packages/guard-core/src/proxy_service.rs", added: 8, removed: 4, type: "M" }, + { path: "engine/packages/guard/src/routing/compute_gateway.rs", added: 42, removed: 8, type: "M" }, + { path: "engine/packages/guard-core/Cargo.toml", added: 1, removed: 0, type: "M" }, + { path: "Cargo.lock", added: 37, removed: 5, type: "M" }, + ], + diffs: { + "engine/packages/guard-core/src/proxy_service.rs": [ + "@@ -309,15 +309,19 @@ pub struct ProxyService {", + " remote_addr: SocketAddr,", + "- client: Client>,", + "+ client: Client<", + "+ hyper_tls::HttpsConnector,", + "+ Full,", + "+ >,", + " }", + " ", + " impl ProxyService {", + " pub fn new(state: Arc, remote_addr: SocketAddr) -> Self {", + "+ let https_connector = hyper_tls::HttpsConnector::new();", + " let client = Client::builder(TokioExecutor::new())", + " .pool_idle_timeout(Duration::from_secs(30))", + "- .build_http();", + "+ .build(https_connector);", + ].join("\n"), + }, + fileTree: [ + { + name: "engine", + path: "engine", + isDir: true, + children: [ + { + name: "packages", + path: "engine/packages", + isDir: true, + children: [ + { + name: "guard-core", + path: "engine/packages/guard-core", + isDir: true, + children: [ + { name: "Cargo.toml", path: "engine/packages/guard-core/Cargo.toml", isDir: false }, + { + name: "src", + path: "engine/packages/guard-core/src", + isDir: true, + children: [{ name: "proxy_service.rs", path: "engine/packages/guard-core/src/proxy_service.rs", isDir: false }], + }, + ], + }, + { + name: "guard", + path: "engine/packages/guard", + isDir: true, + children: [ + { + name: "src", + path: "engine/packages/guard/src", + isDir: true, + children: [ + { + name: "routing", + path: "engine/packages/guard/src/routing", + isDir: true, + children: [{ name: "compute_gateway.rs", path: "engine/packages/guard/src/routing/compute_gateway.rs", isDir: false }], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + // ── rivet-dev/engine-ee (archived) ── + { + id: "h8", + repoId: "engine-ee", + title: "Move compute gateway to guard", + status: "archived", + repoName: "rivet-dev/engine-ee", + updatedAtMs: minutesAgo(2 * 24 * 60), + branch: "chore-move-compute-gateway-to", + pullRequest: { number: 123, status: "ready" }, + tabs: [ + { + id: "t9", + sessionId: "t9", + sessionName: "Gateway migration", + agent: "Claude", + model: "claude-sonnet-4", + status: "idle", + thinkingSinceMs: null, + unread: false, + created: true, + draft: { text: "", attachments: [], updatedAtMs: null }, + transcript: transcriptFromLegacyMessages("t9", [ + { + id: "m70", + role: "agent", + agent: "claude", + createdAtMs: minutesAgo(2 * 24 * 60 + 30), + lines: ["Migrated the compute gateway from its standalone service into the guard package. Removed 469 lines of duplicated routing logic."], + durationMs: 38_000, + }, + ]), + }, + ], + fileChanges: [ + { path: "engine/packages/guard/src/routing/compute_gateway.rs", added: 180, removed: 0, type: "A" }, + { path: "engine/packages/compute-gateway/src/lib.rs", added: 0, removed: 320, type: "D" }, + ], + diffs: {}, + fileTree: [], + }, + // ── rivet-dev/secure-exec ── + { + id: "h9", + repoId: "secure-exec", + title: "Harden namespace isolation for nested containers", + status: "idle", + repoName: "rivet-dev/secure-exec", + updatedAtMs: minutesAgo(90), + branch: "fix/namespace-isolation", + pullRequest: null, + tabs: [ + { + id: "t10", + sessionId: "t10", + sessionName: "Namespace fix", + agent: "Codex", + model: "gpt-4o", + status: "idle", + thinkingSinceMs: null, + unread: true, + created: true, + draft: { text: "", attachments: [], updatedAtMs: null }, + transcript: transcriptFromLegacyMessages("t10", [ + { + id: "m80", + role: "user", + agent: null, + createdAtMs: minutesAgo(100), + lines: ["Audit and harden the namespace isolation for nested container execution. Make sure PID, network, and mount namespaces are correctly unshared."], + }, + { + id: "m81", + role: "agent", + agent: "codex", + createdAtMs: minutesAgo(97), + lines: [ + "Audited the sandbox creation path. Found that the PID namespace was shared with the host in certain fallback paths. Fixed by always calling `unshare(CLONE_NEWPID)` before `fork()`. Also tightened the seccomp filter to block `setns` calls from within the sandbox.", + ], + durationMs: 42_000, + }, + ]), + }, + ], + fileChanges: [ + { path: "src/sandbox/namespace.ts", added: 35, removed: 8, type: "M" }, + { path: "src/sandbox/seccomp.ts", added: 12, removed: 2, type: "M" }, + ], + diffs: {}, + fileTree: [], + }, ]; } export function buildInitialMockLayoutViewModel(): HandoffWorkbenchSnapshot { const repos: WorkbenchRepo[] = [ - { id: "acme-backend", label: "acme/backend" }, - { id: "acme-frontend", label: "acme/frontend" }, - { id: "acme-infra", label: "acme/infra" }, + { id: "sandbox-agent", label: "rivet-dev/sandbox-agent" }, + { id: "rivet", label: "rivet-dev/rivet" }, + { id: "cloud", label: "rivet-dev/cloud" }, + { id: "engine-ee", label: "rivet-dev/engine-ee" }, + { id: "secure-exec", label: "rivet-dev/secure-exec" }, ]; const handoffs = buildInitialHandoffs(); return { diff --git a/factory/packages/frontend/index.html b/factory/packages/frontend/index.html index f4d55ce..9319e9d 100644 --- a/factory/packages/frontend/index.html +++ b/factory/packages/frontend/index.html @@ -10,7 +10,7 @@ - OpenHandoff + Foundry
diff --git a/factory/packages/frontend/src/components/mock-layout.tsx b/factory/packages/frontend/src/components/mock-layout.tsx index 46402a7..5fdff9d 100644 --- a/factory/packages/frontend/src/components/mock-layout.tsx +++ b/factory/packages/frontend/src/components/mock-layout.tsx @@ -352,7 +352,7 @@ const TranscriptPanel = memo(function TranscriptPanel({ const changeModel = useCallback( (model: ModelId) => { if (!promptTab) { - throw new Error(`Unable to change model for handoff ${handoff.id} without an active prompt tab`); + throw new Error(`Unable to change model for task ${handoff.id} without an active prompt tab`); } void handoffWorkbenchClient.changeModel({ @@ -487,7 +487,9 @@ const TranscriptPanel = memo(function TranscriptPanel({ }} >

Create the first session

-

Sessions are where you chat with the agent. Start one now to send the first prompt on this handoff.

+

+ Sessions are where you chat with the agent. Start one now to send the first prompt on this task. +

diff --git a/factory/packages/frontend/src/components/mock-layout/history-minimap.tsx b/factory/packages/frontend/src/components/mock-layout/history-minimap.tsx index 1021224..a5a91cf 100644 --- a/factory/packages/frontend/src/components/mock-layout/history-minimap.tsx +++ b/factory/packages/frontend/src/components/mock-layout/history-minimap.tsx @@ -43,7 +43,7 @@ export const HistoryMinimap = memo(function HistoryMinimap({ events, onSelect }: >
- Handoff Events + Task Events {events.length}
diff --git a/factory/packages/frontend/src/components/mock-layout/message-list.tsx b/factory/packages/frontend/src/components/mock-layout/message-list.tsx index e213330..5e2ccbb 100644 --- a/factory/packages/frontend/src/components/mock-layout/message-list.tsx +++ b/factory/packages/frontend/src/components/mock-layout/message-list.tsx @@ -55,11 +55,11 @@ const TranscriptMessageBody = memo(function TranscriptMessageBody({ borderBottomRightRadius: "4px", } : { - backgroundColor: "rgba(255, 255, 255, 0.06)", - border: `1px solid ${theme.colors.borderOpaque}`, + backgroundColor: "transparent", + border: "none", color: "#e4e4e7", - borderBottomLeftRadius: "4px", - borderBottomRightRadius: "16px", + borderRadius: "0", + padding: "0", }), })} > @@ -163,12 +163,6 @@ export const MessageList = memo(function MessageList({ }), message: css({ display: "flex", - '&[data-variant="user"]': { - justifyContent: "flex-end", - }, - '&[data-variant="assistant"]': { - justifyContent: "flex-start", - }, }), messageContent: messageContentClass, messageText: css({ @@ -193,6 +187,11 @@ export const MessageList = memo(function MessageList({ return ( <> + {historyEvents.length > 0 ? : null}
(null); return ( -
+
{MODEL_GROUPS.map((group) => (
@@ -100,22 +103,26 @@ export const ModelPicker = memo(function ModelPicker({ onSetDefault: (id: ModelId) => void; }) { const [css, theme] = useStyletron(); + const [isOpen, setIsOpen] = useState(false); return ( setIsOpen(true)} + onClose={() => setIsOpen(false)} overrides={{ Body: { style: { - backgroundColor: "#000000", - borderTopLeftRadius: "8px", - borderTopRightRadius: "8px", - borderBottomLeftRadius: "8px", - borderBottomRightRadius: "8px", - border: `1px solid ${theme.colors.borderOpaque}`, - boxShadow: "0 8px 24px rgba(0, 0, 0, 0.6)", + backgroundColor: "rgba(32, 32, 32, 0.98)", + backdropFilter: "blur(12px)", + borderTopLeftRadius: "10px", + borderTopRightRadius: "10px", + borderBottomLeftRadius: "10px", + borderBottomRightRadius: "10px", + border: "1px solid rgba(255, 255, 255, 0.10)", + boxShadow: "0 8px 32px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.04)", zIndex: 100, }, }, @@ -141,13 +148,13 @@ export const ModelPicker = memo(function ModelPicker({ fontSize: "12px", fontWeight: 500, color: theme.colors.contentSecondary, - backgroundColor: theme.colors.backgroundTertiary, - border: `1px solid ${theme.colors.borderOpaque}`, - ":hover": { color: theme.colors.contentPrimary }, + backgroundColor: "rgba(255, 255, 255, 0.10)", + border: "1px solid rgba(255, 255, 255, 0.14)", + ":hover": { color: theme.colors.contentPrimary, backgroundColor: "rgba(255, 255, 255, 0.14)" }, })} > {modelLabel(value)} - + {isOpen ? : }
diff --git a/factory/packages/frontend/src/components/mock-layout/prompt-composer.tsx b/factory/packages/frontend/src/components/mock-layout/prompt-composer.tsx index e809180..d036d03 100644 --- a/factory/packages/frontend/src/components/mock-layout/prompt-composer.tsx +++ b/factory/packages/frontend/src/components/mock-layout/prompt-composer.tsx @@ -43,25 +43,27 @@ export const PromptComposer = memo(function PromptComposer({ backgroundColor: "rgba(255, 255, 255, 0.06)", border: `1px solid ${theme.colors.borderOpaque}`, borderRadius: "16px", - minHeight: `${PROMPT_TEXTAREA_MIN_HEIGHT}px`, + minHeight: `${PROMPT_TEXTAREA_MIN_HEIGHT + 36}px`, transition: "border-color 200ms ease", ":focus-within": { borderColor: "rgba(255, 255, 255, 0.3)" }, + display: "flex", + flexDirection: "column", }), input: css({ display: "block", width: "100%", - minHeight: `${PROMPT_TEXTAREA_MIN_HEIGHT}px`, - padding: "12px 58px 12px 14px", + minHeight: `${PROMPT_TEXTAREA_MIN_HEIGHT + 20}px`, + padding: "14px 58px 8px 14px", background: "transparent", border: "none", - borderRadius: "16px", + borderRadius: "16px 16px 0 0", color: theme.colors.contentPrimary, fontSize: "13px", fontFamily: "inherit", resize: "none", outline: "none", lineHeight: "1.4", - maxHeight: `${PROMPT_TEXTAREA_MAX_HEIGHT}px`, + maxHeight: `${PROMPT_TEXTAREA_MAX_HEIGHT + 40}px`, boxSizing: "border-box", overflowY: "hidden", "::placeholder": { color: theme.colors.contentSecondary }, @@ -101,7 +103,7 @@ export const PromptComposer = memo(function PromptComposer({
(isRunning ? : )} + renderFooter={() => ( +
+ +
+ )} /> -
); }); diff --git a/factory/packages/frontend/src/components/mock-layout/sidebar.tsx b/factory/packages/frontend/src/components/mock-layout/sidebar.tsx index 9a02f96..2141fea 100644 --- a/factory/packages/frontend/src/components/mock-layout/sidebar.tsx +++ b/factory/packages/frontend/src/components/mock-layout/sidebar.tsx @@ -1,11 +1,27 @@ import { memo, useState } from "react"; import { useStyletron } from "baseui"; import { LabelSmall, LabelXSmall } from "baseui/typography"; -import { CloudUpload, GitPullRequestDraft, Plus } from "lucide-react"; +import { ChevronDown, ChevronUp, CloudUpload, GitPullRequestDraft, ListChecks, Plus } from "lucide-react"; import { formatRelativeAge, type Handoff, type ProjectSection } from "./view-model"; import { ContextMenuOverlay, HandoffIndicator, PanelHeaderBar, SPanel, ScrollBody, useContextMenu } from "./ui"; +const PROJECT_COLORS = ["#6366f1", "#f59e0b", "#10b981", "#ef4444", "#8b5cf6", "#ec4899", "#06b6d4", "#f97316"]; + +function projectInitial(label: string): string { + const parts = label.split("/"); + const name = parts[parts.length - 1] ?? label; + return name.charAt(0).toUpperCase(); +} + +function projectIconColor(label: string): string { + let hash = 0; + for (let i = 0; i < label.length; i++) { + hash = (hash * 31 + label.charCodeAt(i)) | 0; + } + return PROJECT_COLORS[Math.abs(hash) % PROJECT_COLORS.length]!; +} + export const Sidebar = memo(function Sidebar({ projects, activeId, @@ -25,13 +41,25 @@ export const Sidebar = memo(function Sidebar({ }) { const [css, theme] = useStyletron(); const contextMenu = useContextMenu(); - const [expandedProjects, setExpandedProjects] = useState>({}); + const [collapsedProjects, setCollapsedProjects] = useState>({}); return ( + - - Handoffs + + + Tasks - ) : null}
); })} diff --git a/factory/packages/frontend/src/components/mock-layout/transcript-header.tsx b/factory/packages/frontend/src/components/mock-layout/transcript-header.tsx index badc499..820cff6 100644 --- a/factory/packages/frontend/src/components/mock-layout/transcript-header.tsx +++ b/factory/packages/frontend/src/components/mock-layout/transcript-header.tsx @@ -1,7 +1,7 @@ import { memo } from "react"; import { useStyletron } from "baseui"; import { LabelSmall } from "baseui/typography"; -import { MailOpen } from "lucide-react"; +import { Clock, MailOpen } from "lucide-react"; import { PanelHeaderBar } from "./ui"; import { type AgentTab, type Handoff } from "./view-model"; @@ -46,7 +46,7 @@ export const TranscriptHeader = memo(function TranscriptHeader({ }} className={css({ all: "unset", - fontWeight: 600, + fontWeight: 500, fontSize: "14px", color: theme.colors.contentPrimary, borderBottom: "1px solid rgba(255, 255, 255, 0.3)", @@ -58,7 +58,7 @@ export const TranscriptHeader = memo(function TranscriptHeader({ onStartEditingField("title", handoff.title)} > {handoff.title} @@ -113,6 +113,24 @@ export const TranscriptHeader = memo(function TranscriptHeader({ ) ) : null}
+
+ + 847 min used +
{activeTab ? (
@@ -845,7 +845,9 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep ) : null} - {!handoffsQuery.isLoading && repoGroups.length === 0 ? No repos or handoffs yet. Add a repo to start a workspace. : null} + {!handoffsQuery.isLoading && repoGroups.length === 0 ? ( + No repos or tasks yet. Add a repo to start a workspace. + ) : null} {repoGroups.map((group) => (
- Create Handoff + Create Task
@@ -1172,7 +1174,9 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep {formatRelativeAge(branch.updatedAt)} - {branch.handoffId ? "handoff" : "unmapped"} + + {branch.handoffId ? "task" : "unmapped"} + {branch.trackedInStack ? stack : null}
@@ -1264,7 +1268,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep }} data-testid={`repo-overview-create-${branchToken}`} > - Create Handoff + Create Task ) : null} @@ -1302,7 +1306,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep > - {selectedForSession ? (selectedForSession.title ?? "Determining title...") : "No handoff selected"} + {selectedForSession ? selectedForSession.title ?? "Determining title..." : "No task selected"} {selectedForSession ? {selectedForSession.status} : null} @@ -1333,7 +1337,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep })} > {!selectedForSession ? ( - Select a handoff from the left sidebar. + Select a task from the left sidebar. ) : ( <>
) : null} @@ -1525,7 +1529,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep - {repoOverviewMode ? "Repo Details" : "Handoff Details"} + {repoOverviewMode ? "Repo Details" : "Task Details"} @@ -1577,7 +1581,10 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep - +
)} @@ -1585,7 +1592,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep ) ) : !selectedForSession ? ( - No handoff selected. + No task selected. ) : ( <> @@ -1601,7 +1608,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep gap: theme.sizing.scale300, })} > - + @@ -1728,7 +1735,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep }} overrides={modalOverrides} > - Create Handoff + Create Task
- Pick a repo, describe the task, and the backend will create a handoff. + Pick a repo, describe the task, and the backend will create a task.
@@ -1876,7 +1883,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep }} data-testid="handoff-create-submit" > - Create Handoff + Create Task diff --git a/frontend/packages/inspector/index.html b/frontend/packages/inspector/index.html index e28187b..5893717 100644 --- a/frontend/packages/inspector/index.html +++ b/frontend/packages/inspector/index.html @@ -1510,10 +1510,11 @@ } .message.assistant .message-content { - background: var(--surface); - border: 1px solid var(--border); + background: none; + border: none; color: var(--text-secondary); - border-bottom-left-radius: 4px; + border-radius: 0; + padding: 0 16px; } .message.system .avatar { diff --git a/sdks/react/src/ChatComposer.tsx b/sdks/react/src/ChatComposer.tsx index 38b8520..7f92ebd 100644 --- a/sdks/react/src/ChatComposer.tsx +++ b/sdks/react/src/ChatComposer.tsx @@ -26,6 +26,7 @@ export interface ChatComposerProps { rows?: number; textareaProps?: Omit, "className" | "disabled" | "onChange" | "onKeyDown" | "placeholder" | "rows" | "value">; renderSubmitContent?: () => ReactNode; + renderFooter?: () => ReactNode; } const DEFAULT_CLASS_NAMES: ChatComposerClassNames = { @@ -62,6 +63,7 @@ export const ChatComposer = ({ rows = 1, textareaProps, renderSubmitContent, + renderFooter, }: ChatComposerProps) => { const resolvedClassNames = mergeClassNames(DEFAULT_CLASS_NAMES, classNameOverrides); const isSubmitDisabled = disabled || submitDisabled || (!allowEmptySubmit && message.trim().length === 0); @@ -92,6 +94,7 @@ export const ChatComposer = ({ rows={rows} disabled={disabled} /> + {renderFooter?.()}