// Minimal pub/sub for ephemeral UI events that don't belong on the sessionEvents // bus (sessionEvents is for DB-state changes; this file is for UI-only signals // like "user clicked send-to-terminal on selected text"). // // Also exposes a tiny registry of currently-mounted terminal panes so the // MessageBubble context menu can list them. TerminalPane registers on mount, // unregisters on unmount. v1.10.4 adds a parallel ChatInput registry used by // the terminal floating menu's "Send to chat" submenu. type Listener = (payload: T) => void; interface EventBus { emit(payload: T): void; subscribe(listener: Listener): () => void; } function createEvent(): EventBus { const listeners = new Set>(); return { emit(payload) { for (const l of listeners) { try { l(payload); } catch { /* one bad listener shouldn't break others */ } } }, subscribe(listener) { listeners.add(listener); return () => { listeners.delete(listener); }; }, }; } export interface SendToTerminalPayload { pane_id: string; text: string; } export const sendToTerminal = createEvent(); // v1.10.4: reverse direction. Terminal floating menu "Send to chat" emits this // with the target chat's chat_id; ChatInput subscribes and appends to its draft. export interface SendToChatPayload { chat_id: string; text: string; } export const sendToChat = createEvent(); export interface TerminalRegistration { paneId: string; label: string; // v1.10.3 kbd-shortcuts: Cmd+` needs to focus the active terminal's xterm // input layer. TerminalPane binds this to term.focus(). focus: () => void; // v1.10.4: Cmd+F opens the search bar over the active terminal. Workspace // also binds a "Paste" button in the terminal pane header to paste(). openSearch: () => void; paste: () => void; } const terminalRegistry = new Map(); const registryListeners = new Set>(); function notifyRegistry(): void { for (const l of registryListeners) { try { l(); } catch { /* ignore */ } } } export const terminalsRegistry = { register( paneId: string, label: string, focus: () => void, openSearch: () => void, paste: () => void, ): () => void { terminalRegistry.set(paneId, { paneId, label, focus, openSearch, paste }); notifyRegistry(); return () => { terminalRegistry.delete(paneId); notifyRegistry(); }; }, list(): TerminalRegistration[] { return Array.from(terminalRegistry.values()); }, get(paneId: string): TerminalRegistration | undefined { return terminalRegistry.get(paneId); }, subscribe(listener: Listener): () => void { registryListeners.add(listener); return () => { registryListeners.delete(listener); }; }, }; // v1.10.4: parallel registry of mounted ChatInput components so the terminal // floating menu's "Send to chat" submenu can list open chats. Mirrors // terminalsRegistry exactly; same subscriber pattern. export interface ChatInputRegistration { chatId: string; label: string; focus: () => void; } const chatInputRegistry = new Map(); const chatInputListeners = new Set>(); function notifyChatInputs(): void { for (const l of chatInputListeners) { try { l(); } catch { /* ignore */ } } } export const chatInputsRegistry = { register(chatId: string, label: string, focus: () => void): () => void { chatInputRegistry.set(chatId, { chatId, label, focus }); notifyChatInputs(); return () => { chatInputRegistry.delete(chatId); notifyChatInputs(); }; }, list(): ChatInputRegistration[] { return Array.from(chatInputRegistry.values()); }, get(chatId: string): ChatInputRegistration | undefined { return chatInputRegistry.get(chatId); }, subscribe(listener: Listener): () => void { chatInputListeners.add(listener); return () => { chatInputListeners.delete(listener); }; }, };