Files
boocode/apps/web/src/lib/events.ts
indifferentketchup ea9d261f0f v1.10.4: booterm mobile UX — copy/paste, swipe-close, send-to-chat, search
- Long-press selection + floating menu (mobile + desktop right-click): Copy,
  Paste, Select All, Search, Send to chat. Tap-outside / Esc dismiss.
- Pane-header Paste button (📋) for iOS user-gesture clipboard read.
- Swipe-left-to-close on mobile pane pill with red "Close" overlay and
  translateX visual hint; spring-back below 80px threshold.
- Send-to-chat reverse path: chatInputsRegistry + sendToChat event mirror
  the existing terminalsRegistry pattern. ChatInput appends with newline
  separator on receive and focuses (no auto-send).
- Scrollback search via xterm-addon-search@^0.13.0: SearchBar overlay with
  N-of-M match counter (onDidChangeResults), Enter/Shift-Enter cycling.
- Cmd/Ctrl+F intercept in Session.tsx when active pane is terminal; xterm
  also intercepts when focused. Browser native find passes through elsewhere.
- terminalsRegistry signature extended with openSearch + paste callbacks.

Includes deferred CLAUDE.md updates documenting v1.10/v1.10.1/v1.10.2/v1.10.3
learnings (uid 1000 collision, libc match, two event buses, vite proxy order,
mobile pane URL sync, xterm canvas selection).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 17:16:47 +00:00

152 lines
4.1 KiB
TypeScript

// 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<T> = (payload: T) => void;
interface EventBus<T> {
emit(payload: T): void;
subscribe(listener: Listener<T>): () => void;
}
function createEvent<T>(): EventBus<T> {
const listeners = new Set<Listener<T>>();
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<SendToTerminalPayload>();
// 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<SendToChatPayload>();
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<string, TerminalRegistration>();
const registryListeners = new Set<Listener<void>>();
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>): () => 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<string, ChatInputRegistration>();
const chatInputListeners = new Set<Listener<void>>();
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>): () => void {
chatInputListeners.add(listener);
return () => {
chatInputListeners.delete(listener);
};
},
};