// Tiny in-app event bus for session metadata changes that need to propagate // across hooks (e.g. AI rename arriving via WS in the session view needs to // also refresh the sidebar's session list). import type { Chat, ErrorReason, HtmlArtifactState, MarkdownArtifactState, OrchestratorState, Project, Session, } from '@/api/types'; import type { Attachment } from '@/lib/attachments'; export interface SessionRenamedEvent { type: 'session_renamed'; session_id: string; name: string; } export interface ProjectCreatedEvent { type: 'project_created'; project: Project; } export interface ProjectDeletedEvent { type: 'project_deleted'; project_id: string; } export interface SessionCreatedEvent { type: 'session_created'; session: Session; project_id: string; } export interface SessionDeletedEvent { type: 'session_deleted'; session_id: string; project_id: string; } export interface SessionUpdatedEvent { type: 'session_updated'; session_id: string; project_id: string; name: string; updated_at: string; } export interface SessionWorkspaceUpdatedEvent { type: 'session_workspace_updated'; session_id: string; // Legacy bare array OR the new envelope — useWorkspacePanes normalizes both // via toWorkspaceState. workspace_panes: | import('@/api/types').WorkspacePane[] | import('@/api/types').WorkspaceState; } export interface SessionLoadedEvent { type: 'session_loaded'; session_id: string; project_id: string; } export interface OpenFileInBrowserEvent { type: 'open_file_in_browser'; path: string; // project-relative } export interface AttachChatFileEvent { type: 'attach_chat_file'; attachment: Omit; } export interface OpenChatInActivePaneEvent { type: 'open_chat_in_active_pane'; chat_id: string; } // Open a whole chat in a fresh split pane (vs the active pane). Emitted by the // ChatTabBar tab context menu ("Open in new pane") and by MessageBubble.fork() // so a fork lands beside the original. useWorkspacePanes subscribes. export interface OpenChatInNewPaneEvent { type: 'open_chat_in_new_pane'; chat_id: string; } // v1.14.x-html-artifact-panes: ActionRow's "Open in pane" button emits one of // these; useWorkspacePanes subscribes and inserts the corresponding artifact // pane (or focuses an existing one keyed by message_id). export interface OpenMarkdownArtifactPaneEvent { type: 'open_markdown_artifact_pane'; state: MarkdownArtifactState; } export interface OpenHtmlArtifactPaneEvent { type: 'open_html_artifact_pane'; state: HtmlArtifactState; } // Client-side event fired by the sidebar Settings button when a session is // currently mounted. Session.tsx subscribes and calls // panesHook.toggleSettingsPane() (open on first click, close on second). // Sidebar handles the no-session case by navigating to /settings directly. export interface OpenSettingsPaneEvent { type: 'open_settings_pane'; } export interface SessionArchivedEvent { type: 'session_archived'; session_id: string; project_id: string; } export interface ChatCreatedEvent { type: 'chat_created'; chat: Chat; session_id: string; } export interface ChatUpdatedEvent { type: 'chat_updated'; chat_id: string; session_id: string; name: string | null; updated_at: string; } export interface ChatArchivedEvent { type: 'chat_archived'; chat_id: string; session_id: string; } export interface ChatUnarchivedEvent { type: 'chat_unarchived'; chat: Chat; } export interface ChatDeletedEvent { type: 'chat_deleted'; chat_id: string; session_id: string; } export interface ProjectArchivedEvent { type: 'project_archived'; project_id: string; } export interface ProjectUnarchivedEvent { type: 'project_unarchived'; project: Project; } export interface ProjectUpdatedEvent { type: 'project_updated'; project_id: string; name: string; } // v1.8 mobile-tabs: broadcast on user channel from inference.ts so any device // subscribed sees a chat working/idle/error. Frontend stores per-chat; panes // derive their dot from pane.activeChatId. // v1.8.2: optional `reason` carries a machine-readable code when status is // 'error'. UI prefers reason for inline error rendering. export interface ChatStatusEvent { type: 'chat_status'; chat_id: string; status: 'streaming' | 'tool_running' | 'waiting_for_input' | 'idle' | 'error'; at: string; reason?: ErrorReason; } export interface RefetchMessagesEvent { type: 'refetch_messages'; } // git-diff-panel Phase 1: emitted client-side to trigger a panel refresh. // Not a WS frame — no @boocode/contracts change required. export interface GitDiffRefreshEvent { type: 'git_diff_refresh'; } // Orchestrator: emitted client-side to open an orchestrator pane. // useWorkspacePanes subscribes and inserts the pane (or focuses an existing one). // placement carries the surface context: 'new' (+ menu) or 'split' (split-pane // menu). addOrchestratorPane appends in both cases; the hint is available for // future positional differentiation. export interface OpenOrchestratorPaneEvent { type: 'open_orchestrator_pane'; state: OrchestratorState; placement?: 'new' | 'split'; } // Orchestrator: emitted by the Workflow button on ChatInput (or "New Orchestrator" // menu items) to request the flow launcher dialog. Carries the current pane's // project and the placement context ('new' from the + menu, 'split' from the // split-pane menu) so the resulting open_orchestrator_pane can be placed correctly. export interface OpenFlowLauncherEvent { type: 'open_flow_launcher'; project_id: string; placement?: 'new' | 'split'; } // Orchestrator: run-level frames forwarded from the coder user channel by // useCoderUserEvents. OrchestratorPane subscribes to update its roster/report. export interface FlowRunStartedEvent { type: 'flow_run_started'; run_id: string; flow_name: string; band: 'small' | 'medium' | 'large'; steps: Array<{ step_id: string; agent: string; kind: 'agent' | 'code'; chat_id: string; label: string; }>; } export interface FlowRunStepUpdatedEvent { type: 'flow_run_step_updated'; run_id: string; step_id: string; status: 'pending' | 'running' | 'completed' | 'failed' | 'skipped' | 'cancelled'; run_status?: 'running' | 'completed' | 'failed' | 'cancelled'; report?: string; } export type SessionEvent = | SessionRenamedEvent | ProjectCreatedEvent | ProjectDeletedEvent | SessionCreatedEvent | SessionDeletedEvent | SessionUpdatedEvent | SessionWorkspaceUpdatedEvent | SessionLoadedEvent | OpenFileInBrowserEvent | AttachChatFileEvent | OpenChatInActivePaneEvent | OpenChatInNewPaneEvent | OpenMarkdownArtifactPaneEvent | OpenHtmlArtifactPaneEvent | OpenSettingsPaneEvent | SessionArchivedEvent | ChatCreatedEvent | ChatUpdatedEvent | ChatArchivedEvent | ChatUnarchivedEvent | ChatDeletedEvent | ProjectArchivedEvent | ProjectUnarchivedEvent | ProjectUpdatedEvent | ChatStatusEvent | RefetchMessagesEvent | GitDiffRefreshEvent | OpenOrchestratorPaneEvent | FlowRunStartedEvent | FlowRunStepUpdatedEvent | OpenFlowLauncherEvent; type Listener = (event: SessionEvent) => void; const listeners = new Set(); export const sessionEvents = { emit(event: SessionEvent) { for (const listener of listeners) { try { listener(event); } catch { // swallow — one bad listener shouldn't break others } } }, subscribe(listener: Listener): () => void { listeners.add(listener); return () => { listeners.delete(listener); }; }, };