- 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>
152 lines
4.1 KiB
TypeScript
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);
|
|
};
|
|
},
|
|
};
|