From ef57c44f8305f50fb3e6e19d5702ebc931299ec2 Mon Sep 17 00:00:00 2001 From: Brad Harris Date: Thu, 3 Sep 2026 03:06:29 -0600 Subject: [PATCH 1/2] Derive web's Agent type from the server's AgentRecord MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit apps/web/src/components/app/types.ts hand-restated the agent row that apps/server/src/agents/types.ts already declares as AgentRecord — the last big server<->web wire-type gap left after the UiEvent consolidation (#1016). The web copy omitted six columns the server has always sent (simulatorUdid, archiveCleanupMode, gitContextStale, gitContextUpdatedAt, launchedByAgentId, cliSessionId), typed `type` as a bare string, and hand-wrote reviewAgentType's member list. Agent is now `Omit & Partial> & { hasStream?: boolean }` — the same derivation idiom the file already uses for DiffStats. AgentStatus, AgentPin and PinShortcutVariant are re-exported from the server module instead of restated, so every existing importer is untouched. Five test fixtures gained `metadata: {}` on their latestEvent literals: the producer (agents/manager.ts:1429) COALESCEs the column to '{}'::jsonb, so the wire never omits that field. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/components/app/agent-card.test.tsx | 2 + .../components/app/child-agent-row.test.tsx | 2 + apps/web/src/components/app/types.ts | 153 ++++++------------ packages/shared/src/ui-event-types.ts | 9 +- 4 files changed, 61 insertions(+), 105 deletions(-) diff --git a/apps/web/src/components/app/agent-card.test.tsx b/apps/web/src/components/app/agent-card.test.tsx index ccf0ce88..b9c12d89 100644 --- a/apps/web/src/components/app/agent-card.test.tsx +++ b/apps/web/src/components/app/agent-card.test.tsx @@ -419,6 +419,7 @@ describe("AgentCardStatus wiring", () => { type: "blocked", message: "Waiting on a merge conflict", updatedAt: new Date().toISOString(), + metadata: {}, }, }); const { rerender } = renderCard({ agent }); @@ -435,6 +436,7 @@ describe("AgentCardStatus wiring", () => { type: "working" as const, message: "Running tests", updatedAt: new Date().toISOString(), + metadata: {}, }; const { rerender } = renderCard({ agent: makeAgent({ type: "terminal", latestEvent }), diff --git a/apps/web/src/components/app/child-agent-row.test.tsx b/apps/web/src/components/app/child-agent-row.test.tsx index 7653336b..f32df4e2 100644 --- a/apps/web/src/components/app/child-agent-row.test.tsx +++ b/apps/web/src/components/app/child-agent-row.test.tsx @@ -25,6 +25,7 @@ const baseAgent: Agent = { type: "working", message: "Reviewing changed routes", updatedAt: "2026-07-15T12:00:00.000Z", + metadata: {}, }, mediaDir: null, persona: "security-review", @@ -103,6 +104,7 @@ describe("ChildAgentRow", () => { type: "done", message: "Incorrect stale event", updatedAt: "2026-07-15T12:00:00.000Z", + metadata: {}, }, }); diff --git a/apps/web/src/components/app/types.ts b/apps/web/src/components/app/types.ts index 74425d7c..03bf8bbf 100644 --- a/apps/web/src/components/app/types.ts +++ b/apps/web/src/components/app/types.ts @@ -1,109 +1,60 @@ import type { DiffStats as ServerDiffStats } from "@dispatch/shared"; -export type AgentStatus = - | "creating" - | "running" - | "stopping" - | "stopped" - | "archiving" - | "error" - | "unknown"; +import type { AgentRecord } from "../../../../server/src/agents/types"; -export type PinShortcutVariant = "default" | "primary" | "destructive"; +/** + * Agent wire types, derived from the server's `AgentRecord` rather than + * restated so a column added on one side can't be missed. Re-exported from + * here so the components that already import them from this module keep + * resolving. + */ +export type { + AgentPin, + AgentStatus, + PinShortcutVariant, +} from "../../../../server/src/agents/types"; -export type AgentPin = { - id?: string; - label: string; - value: string; - type: - | "string" - | "url" - | "port" - | "code" - | "pr" - | "filename" - | "markdown" - | "shortcut"; - /** - * Caption rendered under a shortcut pin's button — inline markdown. - * Shortcut pins only. - */ - caption?: string; - /** Icon name for a shortcut pin's button. Shortcut pins only. */ - icon?: string; - /** - * Renders this pin under a shared heading with every other pin carrying the - * same group name. Any pin type may set it. - */ - group?: string; - /** Button styling for a shortcut pin. Shortcut pins only. */ - variant?: PinShortcutVariant; - /** When true, clicking a shortcut pin asks for confirmation first. */ - confirm?: boolean; - /** - * When true, the shortcut renders non-interactive instead of being - * deleted — for an action that has become temporarily or permanently - * unavailable but is still worth showing (e.g. a launch pin once its - * builder is already running). `caption` doubles as the reason shown in - * place of its normal subtitle. Shortcut pins only. - */ - disabled?: boolean; -}; +/** + * Fields the client treats as optional even though the server always sends + * them. `AgentRecord` has them as required-nullable, but web builds partial + * agents in a lot of places (test fixtures, optimistic cache entries), and a + * component that reads one of these already handles `undefined` the same way + * it handles `null`. + */ +type LenientAgentField = + | "type" + | "role" + | "setupPhase" + | "archivePhase" + | "archiveCleanupMode" + | "simulatorUdid" + | "lastError" + | "latestEvent" + | "pins" + | "gitContext" + | "gitContextStale" + | "gitContextUpdatedAt" + | "persona" + | "parentAgentId" + | "launchedByAgentId" + | "personaContext" + | "reviewAgentType" + | "submittedReviewId" + | "baseBranch" + | "templateId" + | "autoReview" + | "cliSessionId"; -export type Agent = { - id: string; - name: string; - type?: string; - role?: "standard" | "review" | "assisted_update"; - status: AgentStatus; - cwd: string; - worktreePath: string | null; - worktreeBranch: string | null; - tmuxSession: string | null; - agentArgs: string[]; - model: string | null; - fullAccess: boolean; - setupPhase?: "worktree" | "env" | "deps" | "session" | null; - archivePhase?: - | "stopping" - | "worktree-check" - | "worktree-cleanup" - | "finalizing" - | null; - lastError?: string | null; - latestEvent?: { - type: "working" | "blocked" | "waiting_user" | "done" | "idle"; - message: string; - updatedAt: string; - metadata?: Record | null; - } | null; - pins?: AgentPin[]; - mediaDir: string | null; - gitContext?: { - repoRoot: string; - branch: string; - worktreePath: string; - worktreeName: string; - isWorktree: boolean; - repoIconPath?: string | null; - } | null; - persona?: string | null; - parentAgentId?: string | null; - personaContext?: string | null; - reviewAgentType?: "codex" | "claude" | "opencode" | "cursor" | null; - submittedReviewId?: number | null; - baseBranch?: string | null; - templateId?: string | null; - autoReview?: boolean; - jobRun?: { - continuationEnabled: boolean; - iteration: number | null; - maxIterations: number | null; - } | null; - hasStream?: boolean; - createdAt: string; - updatedAt: string; -}; +/** + * An agent as it arrives over the wire. Both the `snapshot` and + * `agent.upsert` payloads are `AgentRecord` enriched with `hasStream` by + * `withStreamFlag` (apps/server/src/server.ts), which is why that field is + * declared here rather than on `AgentRecord` itself. + */ +export type Agent = Omit & + Partial> & { + hasStream?: boolean; + }; export type MediaFile = { name: string; diff --git a/packages/shared/src/ui-event-types.ts b/packages/shared/src/ui-event-types.ts index 79bdce23..6b603cf5 100644 --- a/packages/shared/src/ui-event-types.ts +++ b/packages/shared/src/ui-event-types.ts @@ -44,10 +44,11 @@ export type InjectionHoldState = { * * NOT here, on purpose — each side declares these four itself because the * payload types genuinely differ: - * - `snapshot` / `agent.upsert` — the server publishes `AgentRecord`, while - * the web client models the same rows with a deliberately lenient `Agent` - * (server-only columns dropped, most fields optional, plus the - * `hasStream` flag the publish sites attach). + * - `snapshot` / `agent.upsert` — `AgentRecord` is declared in a server + * module whose closure reaches runtime code (the agent-type and pin-type + * tables), so it cannot move here. Web derives its `Agent` from it across + * the workspace boundary instead: same columns, but the always-sent ones + * relaxed to optional, plus the `hasStream` flag the publish sites attach. * - `agent.diff_state_changed` — web's `DiffStats` makes `excludingTests` * optional so an older server can still drive a newer bundle. * - `release.cached_info_changed` — `ReleaseInfoSnapshot` is declared in a From d798023cbb7dac9e4967927d37efab1c548f49bb Mon Sep 17 00:00:00 2001 From: Brad Harris Date: Thu, 3 Sep 2026 03:17:15 -0600 Subject: [PATCH 2/2] Move the AgentRecord wire contract into @dispatch/shared MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to the first commit, per architecture-review 1065: web now takes `AgentRecord` from `@dispatch/shared` rather than reaching into `apps/server/src/agents/types.ts`, which reversed the intended dependency direction and coupled the web build to server-internal file layout. New shared modules: - packages/shared/src/agent-types.ts — AGENT_TYPES / CLI_AGENT_TYPES - packages/shared/src/pin-types.ts — VALID_PIN_TYPES / VALID_PIN_SHORTCUT_VARIANTS - packages/shared/src/agent-record.ts — AgentRecord and its member unions The index.ts charter already permits "plain constants that both sides genuinely have to agree on" (DIFF_IMAGE_MAX_BYTES is the precedent), so the runtime tables were not a blocker as the first commit's comment claimed. Validation stays server-side: apps/server/src/shared/agent-types.ts keeps isAgentType/isCliAgentType/sanitizeEnabledAgentTypes and the server-only PLUGIN_AGENT_TYPES table; apps/server/src/pins.ts keeps every pin validator. Both re-export what they moved, so no server importer changed. Co-Authored-By: Claude Opus 5 (1M context) --- apps/server/src/agents/types.ts | 144 +++++--------------------- apps/server/src/pins.ts | 28 ++--- apps/server/src/shared/agent-types.ts | 35 +++---- apps/web/src/components/app/types.ts | 18 ++-- packages/shared/src/agent-record.ts | 131 +++++++++++++++++++++++ packages/shared/src/agent-types.ts | 28 +++++ packages/shared/src/index.ts | 16 +++ packages/shared/src/pin-types.ts | 24 +++++ packages/shared/src/ui-event-types.ts | 10 +- 9 files changed, 261 insertions(+), 173 deletions(-) create mode 100644 packages/shared/src/agent-record.ts create mode 100644 packages/shared/src/agent-types.ts create mode 100644 packages/shared/src/pin-types.ts diff --git a/apps/server/src/agents/types.ts b/apps/server/src/agents/types.ts index d88b7083..f9810ff1 100644 --- a/apps/server/src/agents/types.ts +++ b/apps/server/src/agents/types.ts @@ -1,80 +1,33 @@ -export type AgentStatus = - | "creating" - | "running" - | "stopping" - | "stopped" - | "archiving" - | "error" - | "unknown"; +/** + * Server-side agent types. + * + * The wire contract itself (`AgentRecord` and its member unions) lives in + * `@dispatch/shared` so the web client agrees on it without reaching into the + * server. It is re-exported here because the modules that already import these + * names from this path — and, through `agents/manager.ts`, the route layer — + * keep resolving unchanged. + */ + +import type { AgentLatestEventType, AgentRecord } from "@dispatch/shared"; + +export type { + AgentGitContext, + AgentLatestEvent, + AgentLatestEventType, + AgentPin, + AgentRecord, + AgentRole, + AgentStatus, + ArchivePhase, + SetupPhase, + WorktreeCleanupMode, +} from "@dispatch/shared"; // Re-exported so the ~15 modules that already import AgentType from here keep // working, while the member list itself lives in one place. export type { AgentType } from "../shared/agent-types.js"; -import type { AgentType } from "../shared/agent-types.js"; - -export type AgentRole = "standard" | "review" | "assisted_update"; - -export type AgentLatestEventType = - | "working" - | "blocked" - | "waiting_user" - | "done" - | "idle"; - -export type SetupPhase = "worktree" | "env" | "deps" | "session" | null; - -export type ArchivePhase = - | "stopping" - | "worktree-check" - | "worktree-cleanup" - | "finalizing" - | null; export type { PinShortcutVariant, PinType } from "../pins.js"; -import type { PinShortcutVariant, PinType } from "../pins.js"; - -export type AgentPin = { - id?: string; - label: string; - value: string; - type: PinType; - /** Inline-markdown caption rendered under the pin. Any pin type. */ - caption?: string; - /** Renders this pin under a shared heading with pins of the same group. */ - group?: string; - /** Icon name for a shortcut pin's button. Shortcut pins only. */ - icon?: string; - /** Button styling for a shortcut pin. Shortcut pins only. */ - variant?: PinShortcutVariant; - /** When true, clicking a shortcut pin asks for confirmation first. */ - confirm?: boolean; - /** - * When true, the shortcut renders non-interactive instead of being - * deleted — for an action that has become temporarily or permanently - * unavailable but is still worth showing (e.g. a launch pin once its - * builder is already running). `caption` doubles as the reason shown in - * place of its normal subtitle. Shortcut pins only. - */ - disabled?: boolean; -}; - -export type AgentLatestEvent = { - type: AgentLatestEventType; - message: string; - updatedAt: string; - metadata: Record | null; -}; - -export type AgentGitContext = { - repoRoot: string; - branch: string; - worktreePath: string; - worktreeName: string; - isWorktree: boolean; - repoIconPath?: string | null; -}; - -export type WorktreeCleanupMode = "auto" | "keep" | "force"; // Canonical home is `shared/git/worktree-status.ts` — this re-export is // here so existing importers (manager.ts's public surface, and through @@ -91,53 +44,4 @@ export type AgentLatestEventInput = { metadata?: Record; }; -export type AgentRecord = { - id: string; - name: string; - type: AgentType; - role: AgentRole; - status: AgentStatus; - cwd: string; - worktreePath: string | null; - worktreeBranch: string | null; - tmuxSession: string | null; - simulatorUdid: string | null; - mediaDir: string | null; - agentArgs: string[]; - model: string | null; - fullAccess: boolean; - setupPhase: SetupPhase; - archivePhase: ArchivePhase; - archiveCleanupMode: WorktreeCleanupMode | null; - lastError: string | null; - latestEvent: AgentLatestEvent | null; - pins: AgentPin[]; - gitContext: AgentGitContext | null; - gitContextStale: boolean; - gitContextUpdatedAt: string | null; - persona: string | null; - parentAgentId: string | null; - /** - * The agent that ran dispatch_launch_agent / dispatch_launch_persona to - * create this one. Set for every agent-originated launch, including - * `child: false` launches whose `parentAgentId` is deliberately null. - */ - launchedByAgentId: string | null; - personaContext: string | null; - reviewAgentType: AgentType | null; - submittedReviewId: number | null; - baseBranch: string | null; - templateId: string | null; - autoReview: boolean; - /** Present when this agent was spawned for a job run. */ - jobRun?: { - continuationEnabled: boolean; - iteration: number | null; - maxIterations: number | null; - } | null; - cliSessionId: string | null; - createdAt: string; - updatedAt: string; -}; - export type AgentEventListener = (agent: AgentRecord) => void; diff --git a/apps/server/src/pins.ts b/apps/server/src/pins.ts index 0741f8e3..5ceaf7d0 100644 --- a/apps/server/src/pins.ts +++ b/apps/server/src/pins.ts @@ -1,18 +1,14 @@ -const VALID_PIN_TYPES = [ - "string", - "url", - "port", - "code", - "pr", - "filename", - "markdown", - "shortcut", -] as const; -const VALID_PIN_SHORTCUT_VARIANTS = [ - "default", - "primary", - "destructive", -] as const; +import { + VALID_PIN_SHORTCUT_VARIANTS, + VALID_PIN_TYPES, + type PinShortcutVariant, + type PinType, +} from "@dispatch/shared"; + +// The tables live in @dispatch/shared because AgentPin is part of the agent +// wire contract; validation and sanitization stay here. +export type { PinShortcutVariant, PinType }; + /** * Icons a shortcut pin may use. Mirrored by the web icon map * (apps/web/src/lib/pin-shortcut-icons.ts); a guard test asserts the two stay @@ -118,8 +114,6 @@ function validateMarkdownPinValue(value: string): void { } } -export type PinType = (typeof VALID_PIN_TYPES)[number]; -export type PinShortcutVariant = (typeof VALID_PIN_SHORTCUT_VARIANTS)[number]; export type PinShortcutIcon = (typeof VALID_PIN_SHORTCUT_ICONS)[number]; export function isPinType(value: string): value is PinType { diff --git a/apps/server/src/shared/agent-types.ts b/apps/server/src/shared/agent-types.ts index 0c40e617..2b0f58c2 100644 --- a/apps/server/src/shared/agent-types.ts +++ b/apps/server/src/shared/agent-types.ts @@ -1,31 +1,22 @@ /** - * Agent-type tables and predicates. + * Agent-type predicates over the shared table. * - * Single source of truth shared by the server (settings sanitization, job - * and persona agent validation) and the web client (agent pickers, settings - * toggles) — web imports this module directly across the workspace boundary - * (see apps/web/src/lib/agent-types.ts). Keep it dependency-free: no node + * The member lists themselves live in `@dispatch/shared` so the web client + * agrees on them without reaching into the server. They are re-exported here + * because ~15 server modules (and apps/web/src/lib/agent-types.ts) already + * import them from this path. Keep this module dependency-free: no node * imports, no browser globals. */ -export const AGENT_TYPES = [ - "claude", - "codex", - "cursor", - "opencode", - "terminal", -] as const; -export type AgentType = (typeof AGENT_TYPES)[number]; +import { + AGENT_TYPES, + CLI_AGENT_TYPES, + type AgentType, + type CliAgentType, +} from "@dispatch/shared"; -// Agent types that run an AI CLI — eligible for jobs, review assignment, and -// persona launches. Terminal agents are excluded because they don't run a CLI. -export const CLI_AGENT_TYPES = [ - "claude", - "codex", - "cursor", - "opencode", -] as const; -export type CliAgentType = (typeof CLI_AGENT_TYPES)[number]; +export { AGENT_TYPES, CLI_AGENT_TYPES }; +export type { AgentType, CliAgentType }; export function isCliAgentType(value: unknown): value is CliAgentType { return ( diff --git a/apps/web/src/components/app/types.ts b/apps/web/src/components/app/types.ts index 03bf8bbf..651a7b49 100644 --- a/apps/web/src/components/app/types.ts +++ b/apps/web/src/components/app/types.ts @@ -1,18 +1,18 @@ -import type { DiffStats as ServerDiffStats } from "@dispatch/shared"; - -import type { AgentRecord } from "../../../../server/src/agents/types"; +import type { + AgentRecord, + DiffStats as ServerDiffStats, +} from "@dispatch/shared"; /** - * Agent wire types, derived from the server's `AgentRecord` rather than - * restated so a column added on one side can't be missed. Re-exported from - * here so the components that already import them from this module keep - * resolving. + * Agent wire types, taken from the shared contract rather than restated so a + * column added on one side can't be missed. Re-exported from here so the + * components that already import them from this module keep resolving. */ export type { AgentPin, AgentStatus, PinShortcutVariant, -} from "../../../../server/src/agents/types"; +} from "@dispatch/shared"; /** * Fields the client treats as optional even though the server always sends @@ -49,7 +49,7 @@ type LenientAgentField = * An agent as it arrives over the wire. Both the `snapshot` and * `agent.upsert` payloads are `AgentRecord` enriched with `hasStream` by * `withStreamFlag` (apps/server/src/server.ts), which is why that field is - * declared here rather than on `AgentRecord` itself. + * declared here rather than on the shared `AgentRecord` itself. */ export type Agent = Omit & Partial> & { diff --git a/packages/shared/src/agent-record.ts b/packages/shared/src/agent-record.ts new file mode 100644 index 00000000..23da8d20 --- /dev/null +++ b/packages/shared/src/agent-record.ts @@ -0,0 +1,131 @@ +/** + * The agent row as it goes over the wire. + * + * `AgentRecord` is the payload of the `snapshot` and `agent.upsert` SSE events + * and of every `/api/v1/agents` response, so both apps have to agree on it. + * The server's `apps/server/src/agents/types.ts` re-exports everything here so + * its existing importers are untouched; the web client derives its lenient + * `Agent` view from it in `apps/web/src/components/app/types.ts`. + */ + +import type { AgentType } from "./agent-types.js"; +import type { PinShortcutVariant, PinType } from "./pin-types.js"; + +export type AgentStatus = + | "creating" + | "running" + | "stopping" + | "stopped" + | "archiving" + | "error" + | "unknown"; + +export type AgentRole = "standard" | "review" | "assisted_update"; + +export type AgentLatestEventType = + | "working" + | "blocked" + | "waiting_user" + | "done" + | "idle"; + +export type SetupPhase = "worktree" | "env" | "deps" | "session" | null; + +export type ArchivePhase = + | "stopping" + | "worktree-check" + | "worktree-cleanup" + | "finalizing" + | null; + +export type WorktreeCleanupMode = "auto" | "keep" | "force"; + +export type AgentPin = { + id?: string; + label: string; + value: string; + type: PinType; + /** Inline-markdown caption rendered under the pin. Any pin type. */ + caption?: string; + /** Renders this pin under a shared heading with pins of the same group. */ + group?: string; + /** Icon name for a shortcut pin's button. Shortcut pins only. */ + icon?: string; + /** Button styling for a shortcut pin. Shortcut pins only. */ + variant?: PinShortcutVariant; + /** When true, clicking a shortcut pin asks for confirmation first. */ + confirm?: boolean; + /** + * When true, the shortcut renders non-interactive instead of being + * deleted — for an action that has become temporarily or permanently + * unavailable but is still worth showing (e.g. a launch pin once its + * builder is already running). `caption` doubles as the reason shown in + * place of its normal subtitle. Shortcut pins only. + */ + disabled?: boolean; +}; + +export type AgentLatestEvent = { + type: AgentLatestEventType; + message: string; + updatedAt: string; + metadata: Record | null; +}; + +export type AgentGitContext = { + repoRoot: string; + branch: string; + worktreePath: string; + worktreeName: string; + isWorktree: boolean; + repoIconPath?: string | null; +}; + +export type AgentRecord = { + id: string; + name: string; + type: AgentType; + role: AgentRole; + status: AgentStatus; + cwd: string; + worktreePath: string | null; + worktreeBranch: string | null; + tmuxSession: string | null; + simulatorUdid: string | null; + mediaDir: string | null; + agentArgs: string[]; + model: string | null; + fullAccess: boolean; + setupPhase: SetupPhase; + archivePhase: ArchivePhase; + archiveCleanupMode: WorktreeCleanupMode | null; + lastError: string | null; + latestEvent: AgentLatestEvent | null; + pins: AgentPin[]; + gitContext: AgentGitContext | null; + gitContextStale: boolean; + gitContextUpdatedAt: string | null; + persona: string | null; + parentAgentId: string | null; + /** + * The agent that ran dispatch_launch_agent / dispatch_launch_persona to + * create this one. Set for every agent-originated launch, including + * `child: false` launches whose `parentAgentId` is deliberately null. + */ + launchedByAgentId: string | null; + personaContext: string | null; + reviewAgentType: AgentType | null; + submittedReviewId: number | null; + baseBranch: string | null; + templateId: string | null; + autoReview: boolean; + /** Present when this agent was spawned for a job run. */ + jobRun?: { + continuationEnabled: boolean; + iteration: number | null; + maxIterations: number | null; + } | null; + cliSessionId: string | null; + createdAt: string; + updatedAt: string; +}; diff --git a/packages/shared/src/agent-types.ts b/packages/shared/src/agent-types.ts new file mode 100644 index 00000000..ff09fec8 --- /dev/null +++ b/packages/shared/src/agent-types.ts @@ -0,0 +1,28 @@ +/** + * The agent-type table. + * + * Both apps have to agree on this member list: the server validates settings, + * job and persona launches against it, and the web client builds its agent + * pickers and settings toggles from it. Predicates and the server-only + * plugin-agent subset stay in `apps/server/src/shared/agent-types.ts`, which + * re-exports these so its existing importers are untouched. + */ + +export const AGENT_TYPES = [ + "claude", + "codex", + "cursor", + "opencode", + "terminal", +] as const; +export type AgentType = (typeof AGENT_TYPES)[number]; + +// Agent types that run an AI CLI — eligible for jobs, review assignment, and +// persona launches. Terminal agents are excluded because they don't run a CLI. +export const CLI_AGENT_TYPES = [ + "claude", + "codex", + "cursor", + "opencode", +] as const; +export type CliAgentType = (typeof CLI_AGENT_TYPES)[number]; diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 5ae90e72..79b09dc3 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -7,6 +7,22 @@ * and in the compiled server binary at once, so keep those to plain constants * that both sides genuinely have to agree on. */ +export { AGENT_TYPES, CLI_AGENT_TYPES } from "./agent-types.js"; +export type { AgentType, CliAgentType } from "./agent-types.js"; +export type { + AgentGitContext, + AgentLatestEvent, + AgentLatestEventType, + AgentPin, + AgentRecord, + AgentRole, + AgentStatus, + ArchivePhase, + SetupPhase, + WorktreeCleanupMode, +} from "./agent-record.js"; +export { VALID_PIN_SHORTCUT_VARIANTS, VALID_PIN_TYPES } from "./pin-types.js"; +export type { PinShortcutVariant, PinType } from "./pin-types.js"; export { DIFF_IMAGE_MAX_BYTES } from "./diff-types.js"; export type { DiffFile, diff --git a/packages/shared/src/pin-types.ts b/packages/shared/src/pin-types.ts new file mode 100644 index 00000000..049ff384 --- /dev/null +++ b/packages/shared/src/pin-types.ts @@ -0,0 +1,24 @@ +/** + * Pin type tables, shared because `AgentPin` is part of the agent wire + * contract. Validation and sanitization live in `apps/server/src/pins.ts`, + * which imports these tables and re-exports the derived types. + */ + +export const VALID_PIN_TYPES = [ + "string", + "url", + "port", + "code", + "pr", + "filename", + "markdown", + "shortcut", +] as const; +export type PinType = (typeof VALID_PIN_TYPES)[number]; + +export const VALID_PIN_SHORTCUT_VARIANTS = [ + "default", + "primary", + "destructive", +] as const; +export type PinShortcutVariant = (typeof VALID_PIN_SHORTCUT_VARIANTS)[number]; diff --git a/packages/shared/src/ui-event-types.ts b/packages/shared/src/ui-event-types.ts index 6b603cf5..bc388015 100644 --- a/packages/shared/src/ui-event-types.ts +++ b/packages/shared/src/ui-event-types.ts @@ -44,11 +44,11 @@ export type InjectionHoldState = { * * NOT here, on purpose — each side declares these four itself because the * payload types genuinely differ: - * - `snapshot` / `agent.upsert` — `AgentRecord` is declared in a server - * module whose closure reaches runtime code (the agent-type and pin-type - * tables), so it cannot move here. Web derives its `Agent` from it across - * the workspace boundary instead: same columns, but the always-sent ones - * relaxed to optional, plus the `hasStream` flag the publish sites attach. + * - `snapshot` / `agent.upsert` — `AgentRecord` lives in `./agent-record.js`, + * but the payload types still differ: the server publishes it enriched + * with the `hasStream` flag, and the web client models the same rows with + * a deliberately lenient `Agent` that relaxes the always-sent columns to + * optional. * - `agent.diff_state_changed` — web's `DiffStats` makes `excludingTests` * optional so an older server can still drive a newer bundle. * - `release.cached_info_changed` — `ReleaseInfoSnapshot` is declared in a