/** * ProjectionClient - Local TUI-side projection consumer * TUI copies minimal projection types from contracts to avoid INV-4 violation * (TUI must only depend on contracts; dd §13.2 / c4/code-view §2 rule 3). * * The runtime package provides the authoritative ProjectionClient in * `runtime/projection/ProjectionClient.ts`. TUI defines its own local copy * with the same surface so that subscriptions work in-process. * * @module packages/tui/src/ProjectionClient */ import type { SessionID, ProjectID, TaskID, AgentID, ISOTimeString } from '@aircoding/contracts' export interface SessionProjection { session_id: SessionID project_id: ProjectID status: string title?: string tasks: TaskProjection[] agents: AgentProjection[] } export interface TaskProjection { id: TaskID type: string status: string title: string retry_count: number attempts: number created_at: string } export interface AgentProjection { id: AgentID type: string status: string task_id?: TaskID last_heartbeat?: string } export type ProjectionSubscriber = (projection: SessionProjection) => void export class ProjectionClient { private snapshot: SessionProjection | null = null private subscribers: Set = new Set() /** * Receive and cache a projection snapshot. */ receive_snapshot(projection: SessionProjection): void { this.snapshot = projection for (const sub of this.subscribers) { sub(projection) } } /** * Subscribe to projection updates. */ subscribe(subscriber: ProjectionSubscriber): () => void { this.subscribers.add(subscriber) return () => this.subscribers.delete(subscriber) } /** * Get current snapshot. */ get_snapshot(): SessionProjection | null { return this.snapshot } }