// 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, 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; workspace_panes: import('@/api/types').WorkspacePane[]; } 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; } // 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 type SessionEvent = | SessionRenamedEvent | ProjectCreatedEvent | ProjectDeletedEvent | SessionCreatedEvent | SessionDeletedEvent | SessionUpdatedEvent | SessionWorkspaceUpdatedEvent | SessionLoadedEvent | OpenFileInBrowserEvent | AttachChatFileEvent | OpenChatInActivePaneEvent | OpenSettingsPaneEvent | SessionArchivedEvent | ChatCreatedEvent | ChatUpdatedEvent | ChatArchivedEvent | ChatUnarchivedEvent | ChatDeletedEvent | ProjectArchivedEvent | ProjectUnarchivedEvent | ProjectUpdatedEvent | ChatStatusEvent; 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); }; }, };