diff --git a/packages/electron/main.mjs b/packages/electron/main.mjs index ca7a493e..e9bca981 100644 --- a/packages/electron/main.mjs +++ b/packages/electron/main.mjs @@ -2255,6 +2255,13 @@ const dispatchMenuAction = (action) => { dispatchDomEventToWindow(target, 'openchamber:menu-action', action); }; +// Append-style menu actions must reach the renderer exactly once. Dual IPC+DOM +// delivery (dispatchMenuAction) would insert the selection twice. +const dispatchAddSelectionToChat = () => { + const target = getMenuTargetWindow(); + if (target) emitToWindow(target, 'openchamber:menu-action', 'add-selection-to-chat'); +}; + // Mini-chat draft windows are not deduplicated, so this must reach the renderer // exactly once — emitToWindow alone (no DOM-event double dispatch). The renderer // resolves the active directory/project and opens the window. @@ -4564,6 +4571,7 @@ const buildMacMenu = () => { { type: 'separator' }, { role: 'cut' }, { label: 'Copy', accelerator: 'Cmd+C', click: () => handleCopyAction() }, + { label: 'Add Selection to Chat', accelerator: 'Cmd+L', registerAccelerator: false, click: () => dispatchAddSelectionToChat() }, { role: 'paste' }, { role: 'selectAll' }, ], @@ -4582,7 +4590,7 @@ const buildMacMenu = () => { { label: 'Dark Theme', click: () => dispatchAction('theme-dark') }, { label: 'System Theme', click: () => dispatchAction('theme-system') }, { type: 'separator' }, - { label: 'Toggle Session Sidebar', accelerator: 'Cmd+L', click: () => dispatchAction('toggle-sidebar') }, + { label: 'Toggle Session Sidebar', accelerator: 'Cmd+Alt+L', click: () => dispatchAction('toggle-sidebar') }, { label: 'Toggle Memory Debug', accelerator: 'Cmd+Shift+D', click: () => dispatchAction('toggle-memory-debug') }, { type: 'separator' }, { role: 'togglefullscreen' }, @@ -4661,6 +4669,7 @@ const buildAutoHiddenMenu = () => { { type: 'separator' }, { role: 'cut' }, { label: 'Copy', accelerator: 'Ctrl+C', click: () => handleCopyAction() }, + { label: 'Add Selection to Chat', accelerator: 'Ctrl+L', registerAccelerator: false, click: () => dispatchAddSelectionToChat() }, { role: 'paste' }, { role: 'selectAll' }, ], @@ -4683,7 +4692,7 @@ const buildAutoHiddenMenu = () => { { label: 'Dark Theme', click: () => dispatchAction('theme-dark') }, { label: 'System Theme', click: () => dispatchAction('theme-system') }, { type: 'separator' }, - { label: 'Toggle Session Sidebar', accelerator: 'Ctrl+L', click: () => dispatchAction('toggle-sidebar') }, + { label: 'Toggle Session Sidebar', accelerator: 'Ctrl+Alt+L', click: () => dispatchAction('toggle-sidebar') }, { label: 'Toggle Memory Debug', accelerator: 'Ctrl+Shift+D', click: () => dispatchAction('toggle-memory-debug') }, { type: 'separator' }, { role: 'togglefullscreen' }, diff --git a/packages/ui/src/components/chat/message/TextSelectionMenu.tsx b/packages/ui/src/components/chat/message/TextSelectionMenu.tsx index b8d1486c..c8ecc9f4 100644 --- a/packages/ui/src/components/chat/message/TextSelectionMenu.tsx +++ b/packages/ui/src/components/chat/message/TextSelectionMenu.tsx @@ -16,6 +16,7 @@ import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory'; import { isVSCodeRuntime } from '@/lib/desktop'; import { useI18n } from '@/lib/i18n'; import { rangeToMarkdown, trimSelectionValue, wrapMarkdownSelectionForChat } from './selectionMarkdown'; +import { focusChatInput } from '@/components/chat/composer/editor/dom'; interface TextSelectionMenuProps { containerRef: React.RefObject; @@ -309,6 +310,9 @@ export const TextSelectionMenu: React.FC = ({ containerR // Clear selection window.getSelection()?.removeAllRanges(); + queueMicrotask(() => { + focusChatInput(); + }); }, [selectedTextMarkdown, setPendingInputText, hideMenu]); const handleCreateNewSession = React.useCallback(async () => { diff --git a/packages/ui/src/components/ui/HelpDialog.tsx b/packages/ui/src/components/ui/HelpDialog.tsx index f74b9ee6..027e8833 100644 --- a/packages/ui/src/components/ui/HelpDialog.tsx +++ b/packages/ui/src/components/ui/HelpDialog.tsx @@ -65,6 +65,12 @@ export const HelpDialog: React.FC = () => { icon: "layout-left", keys: '', }, + { + id: 'add_selection_to_chat', + descriptionKey: "helpDialog.item.addSelectionToChat", + icon: "add", + keys: '', + }, { id: 'cycle_agent', keys: '', diff --git a/packages/ui/src/hooks/useKeyboardShortcuts.ts b/packages/ui/src/hooks/useKeyboardShortcuts.ts index c3f5a6f4..87cabf01 100644 --- a/packages/ui/src/hooks/useKeyboardShortcuts.ts +++ b/packages/ui/src/hooks/useKeyboardShortcuts.ts @@ -16,6 +16,7 @@ import { useDirectoryStore } from '@/stores/useDirectoryStore'; import { useProjectsStore } from '@/stores/useProjectsStore'; import { getCycledPrimaryAgentName } from '@/components/chat/mobileControlsUtils'; import { focusChatInput } from '@/components/chat/composer/editor/dom'; +import { addSelectionToChat } from '@/lib/addSelectionToChat'; import { hasOpenDropdown } from './keyboard-shortcut-dom'; export const useKeyboardShortcuts = () => { @@ -337,6 +338,12 @@ export const useKeyboardShortcuts = () => { return; } + if (eventMatchesShortcut(e, combo('add_selection_to_chat'))) { + e.preventDefault(); + addSelectionToChat(); + return; + } + if (eventMatchesShortcut(e, combo('toggle_sidebar'))) { e.preventDefault(); const { isMobile, isSessionSwitcherOpen } = useUIStore.getState(); diff --git a/packages/ui/src/hooks/useMenuActions.ts b/packages/ui/src/hooks/useMenuActions.ts index 6b6abc09..d74243ec 100644 --- a/packages/ui/src/hooks/useMenuActions.ts +++ b/packages/ui/src/hooks/useMenuActions.ts @@ -10,6 +10,7 @@ import { useThemeSystem } from '@/contexts/useThemeSystem'; import { sessionEvents } from '@/lib/sessionEvents'; import { createWorktreeSession } from '@/lib/worktreeSessionCreator'; import { showOpenCodeStatus } from '@/lib/openCodeStatus'; +import { addSelectionToChat } from '@/lib/addSelectionToChat'; const getActiveElementSelectedText = (): string => { if (typeof document === 'undefined') { @@ -77,6 +78,7 @@ type MenuAction = | 'toggle-terminal' | 'toggle-terminal-expanded' | 'copy' + | 'add-selection-to-chat' | 'theme-light' | 'theme-dark' | 'theme-system' @@ -278,6 +280,10 @@ export const useMenuActions = ( setThemeMode('system'); break; + case 'add-selection-to-chat': + addSelectionToChat(); + break; + case 'toggle-sidebar': toggleSidebar(); break; diff --git a/packages/ui/src/lib/addSelectionToChat.test.ts b/packages/ui/src/lib/addSelectionToChat.test.ts new file mode 100644 index 00000000..6acc225a --- /dev/null +++ b/packages/ui/src/lib/addSelectionToChat.test.ts @@ -0,0 +1,298 @@ +import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'; + +const focusChatInputCalls: number[] = []; +const pendingInputCalls: Array<{ text: string | null; mode?: string }> = []; +const activeMainTabCalls: string[] = []; +const sessionSwitcherCalls: boolean[] = []; +const codeMirrorDispatches: Array<{ selection: { anchor: number } }> = []; + +type MockCodeMirrorView = { + state: { + selection: { main: { from: number; to: number } }; + sliceDoc: (from: number, to: number) => string; + }; + dispatch: (transaction: { selection: { anchor: number } }) => void; +}; + +let codeMirrorView: MockCodeMirrorView | null = null; + +mock.module('@codemirror/view', () => ({ + EditorView: { + findFromDOM: () => codeMirrorView, + }, +})); + +mock.module('@/components/chat/composer/editor/dom', () => ({ + focusChatInput: () => { + focusChatInputCalls.push(1); + }, +})); + +mock.module('@/sync/input-store', () => ({ + useInputStore: { + getState: () => ({ + setPendingInputText: (text: string | null, mode?: string) => { + pendingInputCalls.push({ text, mode }); + }, + }), + }, +})); + +mock.module('@/stores/useUIStore', () => ({ + useUIStore: { + getState: () => ({ + setActiveMainTab: (tab: string) => { + activeMainTabCalls.push(tab); + }, + setSessionSwitcherOpen: (open: boolean) => { + sessionSwitcherCalls.push(open); + }, + }), + }, +})); + +const { addSelectionToChat, captureSelectionMarkdownForChat } = await import('./addSelectionToChat'); + +const originalDocument = globalThis.document; +const originalWindow = globalThis.window; + +const installSelectionEnvironment = (options: { + activeElement?: Element | null; + focusedCodeMirror?: Element | null; + selection?: Selection | null; +} = {}) => { + const { + activeElement = null, + focusedCodeMirror = null, + selection = null, + } = options; + + const documentLike = { + activeElement, + querySelector: (selector: string) => { + if (selector === '.cm-editor.cm-focused') { + return focusedCodeMirror; + } + return null; + }, + }; + const windowLike = { + getSelection: () => selection, + }; + Object.defineProperty(globalThis, 'document', { value: documentLike, configurable: true }); + Object.defineProperty(globalThis, 'window', { value: windowLike, configurable: true }); +}; + +const clearCalls = () => { + focusChatInputCalls.length = 0; + pendingInputCalls.length = 0; + activeMainTabCalls.length = 0; + sessionSwitcherCalls.length = 0; + codeMirrorDispatches.length = 0; + codeMirrorView = null; +}; + +afterEach(() => { + Object.defineProperty(globalThis, 'document', { value: originalDocument, configurable: true }); + Object.defineProperty(globalThis, 'window', { value: originalWindow, configurable: true }); +}); + +describe('captureSelectionMarkdownForChat', () => { + beforeEach(() => { + clearCalls(); + }); + + test('returns null when nothing is selected', () => { + installSelectionEnvironment(); + expect(captureSelectionMarkdownForChat()).toBeNull(); + }); + + test('captures a textarea selection outside the composer and collapses it', () => { + const textarea = { + tagName: 'TEXTAREA', + value: 'alpha beta gamma', + selectionStart: 6, + selectionEnd: 10, + closest: () => null, + } as unknown as HTMLTextAreaElement; + + installSelectionEnvironment({ activeElement: textarea }); + expect(captureSelectionMarkdownForChat()).toBe('```md\nbeta\n```'); + expect(textarea.selectionStart).toBe(10); + expect(textarea.selectionEnd).toBe(10); + }); + + test('ignores selections inside the chat composer', () => { + const textarea = { + tagName: 'TEXTAREA', + value: 'draft text', + selectionStart: 0, + selectionEnd: 5, + closest: (selector: string) => (selector === '[data-chat-input="true"]' ? textarea : null), + } as unknown as HTMLTextAreaElement; + + installSelectionEnvironment({ activeElement: textarea }); + expect(captureSelectionMarkdownForChat()).toBeNull(); + }); + + test('captures a focused CodeMirror selection outside the composer and collapses it', () => { + const focusedEditor = { + closest: () => null, + } as unknown as HTMLElement; + + codeMirrorView = { + state: { + selection: { main: { from: 4, to: 11 } }, + sliceDoc: (from: number, to: number) => 'const x'.slice(0, to - from), + }, + dispatch: (transaction) => { + codeMirrorDispatches.push(transaction); + codeMirrorView!.state.selection.main = { + from: transaction.selection.anchor, + to: transaction.selection.anchor, + }; + }, + }; + + // sliceDoc should return the selected slice; use explicit text instead of slice math. + codeMirrorView.state.sliceDoc = () => 'const x'; + + installSelectionEnvironment({ focusedCodeMirror: focusedEditor }); + expect(captureSelectionMarkdownForChat()).toBe('```\nconst x\n```'); + expect(codeMirrorDispatches).toEqual([{ selection: { anchor: 11 } }]); + expect(codeMirrorView.state.selection.main).toEqual({ from: 11, to: 11 }); + }); + + test('ignores a focused CodeMirror editor inside the chat composer', () => { + const focusedEditor = { + closest: (selector: string) => (selector === '[data-chat-input="true"]' ? focusedEditor : null), + } as unknown as HTMLElement; + + codeMirrorView = { + state: { + selection: { main: { from: 0, to: 5 } }, + sliceDoc: () => 'draft', + }, + dispatch: (transaction) => { + codeMirrorDispatches.push(transaction); + }, + }; + + installSelectionEnvironment({ focusedCodeMirror: focusedEditor }); + expect(captureSelectionMarkdownForChat()).toBeNull(); + expect(codeMirrorDispatches).toEqual([]); + }); + + test('captures a DOM selection from chat-message content and clears it', () => { + const parent = { + closest: (selector: string) => (selector === 'pre code' ? null : null), + }; + const textNode = { + nodeType: 3, + parentElement: parent, + }; + let rangeCount = 1; + let collapsed = false; + const selection = { + get rangeCount() { + return rangeCount; + }, + get isCollapsed() { + return collapsed; + }, + toString: () => 'Hello world', + getRangeAt: () => ({ + commonAncestorContainer: textNode, + startContainer: textNode, + endContainer: textNode, + cloneContents: () => ({ childNodes: [] }), + }), + removeAllRanges: () => { + rangeCount = 0; + collapsed = true; + }, + } as unknown as Selection; + + installSelectionEnvironment({ selection }); + expect(captureSelectionMarkdownForChat()).toBe('```md\nHello world\n```'); + expect(selection.rangeCount).toBe(0); + expect(selection.isCollapsed).toBe(true); + }); + + test('ignores a DOM selection inside the chat composer', () => { + const composerHost = {}; + const parent = { + closest: (selector: string) => (selector === '[data-chat-input="true"]' ? composerHost : null), + }; + const textNode = { + nodeType: 3, + parentElement: parent, + }; + const selection = { + rangeCount: 1, + isCollapsed: false, + toString: () => 'draft', + getRangeAt: () => ({ + commonAncestorContainer: textNode, + startContainer: textNode, + endContainer: textNode, + cloneContents: () => ({ childNodes: [] }), + }), + removeAllRanges: () => undefined, + } as unknown as Selection; + + installSelectionEnvironment({ selection }); + expect(captureSelectionMarkdownForChat()).toBeNull(); + }); +}); + +describe('addSelectionToChat', () => { + beforeEach(() => { + clearCalls(); + }); + + test('appends captured selection and focuses chat input', async () => { + const textarea = { + tagName: 'TEXTAREA', + value: 'selected', + selectionStart: 0, + selectionEnd: 8, + closest: () => null, + } as unknown as HTMLTextAreaElement; + installSelectionEnvironment({ activeElement: textarea }); + + expect(addSelectionToChat()).toBe(true); + expect(activeMainTabCalls).toEqual(['chat']); + expect(sessionSwitcherCalls).toEqual([false]); + expect(pendingInputCalls).toEqual([{ text: '```md\nselected\n```', mode: 'append' }]); + + await Promise.resolve(); + expect(focusChatInputCalls.length).toBe(1); + }); + + test('second capture after textarea collapse does not append again', () => { + const textarea = { + tagName: 'TEXTAREA', + value: 'selected', + selectionStart: 0, + selectionEnd: 8, + closest: () => null, + } as unknown as HTMLTextAreaElement; + installSelectionEnvironment({ activeElement: textarea }); + + expect(addSelectionToChat()).toBe(true); + expect(addSelectionToChat()).toBe(false); + expect(pendingInputCalls).toEqual([{ text: '```md\nselected\n```', mode: 'append' }]); + }); + + test('focuses chat input when nothing is selected', async () => { + installSelectionEnvironment(); + + expect(addSelectionToChat()).toBe(false); + expect(pendingInputCalls).toEqual([]); + expect(activeMainTabCalls).toEqual(['chat']); + + await Promise.resolve(); + expect(focusChatInputCalls.length).toBe(1); + }); +}); diff --git a/packages/ui/src/lib/addSelectionToChat.ts b/packages/ui/src/lib/addSelectionToChat.ts new file mode 100644 index 00000000..ce296569 --- /dev/null +++ b/packages/ui/src/lib/addSelectionToChat.ts @@ -0,0 +1,166 @@ +import { EditorView } from '@codemirror/view'; +import { focusChatInput } from '@/components/chat/composer/editor/dom'; +import { + formatCodeSelectionMarkdown, + rangeToMarkdown, + trimSelectionValue, + wrapMarkdownSelectionForChat, +} from '@/components/chat/message/selectionMarkdown'; +import { useInputStore } from '@/sync/input-store'; +import { useUIStore } from '@/stores/useUIStore'; + +const CHAT_INPUT_HOST_SELECTOR = '[data-chat-input="true"]'; + +const isInsideChatComposer = (node: Node | null): boolean => { + if (!node) { + return false; + } + const asElement = node as Element; + const element = typeof asElement.closest === 'function' + ? asElement + : (node as Node).parentElement; + return Boolean(element?.closest(CHAT_INPUT_HOST_SELECTOR)); +}; + +const readTextControlSelection = (element: Element): string | null => { + if (isInsideChatComposer(element)) { + return null; + } + + const tag = element.tagName?.toLowerCase(); + if (tag === 'textarea') { + const control = element as HTMLTextAreaElement; + const start = control.selectionStart ?? 0; + const end = control.selectionEnd ?? 0; + const text = trimSelectionValue(control.value.slice(start, end)); + if (!text) { + return null; + } + // Collapse so a duplicate menu delivery cannot append the same range twice. + control.selectionStart = end; + control.selectionEnd = end; + return text; + } + + if (tag === 'input') { + const control = element as HTMLInputElement; + const type = control.type?.toLowerCase() ?? 'text'; + if (!['text', 'search', 'url', 'tel', 'password'].includes(type)) { + return null; + } + const start = control.selectionStart ?? 0; + const end = control.selectionEnd ?? 0; + const text = trimSelectionValue(control.value.slice(start, end)); + if (!text) { + return null; + } + control.selectionStart = end; + control.selectionEnd = end; + return text; + } + + return null; +}; + +const captureActiveElementSelection = (): string | null => { + if (typeof document === 'undefined') { + return null; + } + + const activeElement = document.activeElement; + if (!activeElement || typeof (activeElement as Element).tagName !== 'string') { + return null; + } + + const text = readTextControlSelection(activeElement as Element); + return text ? wrapMarkdownSelectionForChat(text) : null; +}; + +const captureCodeMirrorSelection = (): string | null => { + if (typeof document === 'undefined') { + return null; + } + + const focusedEditor = document.querySelector('.cm-editor.cm-focused'); + if (!focusedEditor || isInsideChatComposer(focusedEditor)) { + return null; + } + + const view = EditorView.findFromDOM(focusedEditor); + if (!view) { + return null; + } + + const { from, to } = view.state.selection.main; + if (from === to) { + return null; + } + + const text = trimSelectionValue(view.state.sliceDoc(from, to)); + if (!text) { + return null; + } + + view.dispatch({ + selection: { anchor: to }, + }); + + return formatCodeSelectionMarkdown(text); +}; + +const captureDomSelection = (): string | null => { + if (typeof window === 'undefined') { + return null; + } + + const selection = window.getSelection(); + if (!selection || selection.rangeCount === 0 || selection.isCollapsed) { + return null; + } + + const range = selection.getRangeAt(0); + if (isInsideChatComposer(range.commonAncestorContainer)) { + return null; + } + + const plainText = trimSelectionValue(selection.toString()); + if (!plainText) { + return null; + } + + const markdown = rangeToMarkdown(range, plainText); + selection.removeAllRanges(); + return wrapMarkdownSelectionForChat(markdown); +}; + +/** + * Capture the current non-composer selection as chat-ready markdown. + * Returns null when nothing usable is selected. + */ +export const captureSelectionMarkdownForChat = (): string | null => { + return captureCodeMirrorSelection() + ?? captureActiveElementSelection() + ?? captureDomSelection(); +}; + +/** + * Append the current selection to the chat composer. + * When nothing is selected, focuses the chat input (Cursor-style Ctrl/Cmd+L). + * Returns true when selected text was appended. + */ +export const addSelectionToChat = (): boolean => { + const markdown = captureSelectionMarkdownForChat(); + + useUIStore.getState().setActiveMainTab('chat'); + useUIStore.getState().setSessionSwitcherOpen(false); + + if (markdown) { + useInputStore.getState().setPendingInputText(markdown, 'append'); + } + + queueMicrotask(() => { + focusChatInput(); + }); + + return markdown !== null; +}; diff --git a/packages/ui/src/lib/i18n/messages/de.settings.ts b/packages/ui/src/lib/i18n/messages/de.settings.ts index 2bf271a5..fc3cfa86 100644 --- a/packages/ui/src/lib/i18n/messages/de.settings.ts +++ b/packages/ui/src/lib/i18n/messages/de.settings.ts @@ -1038,6 +1038,7 @@ export const settingsDict = { 'settings.openchamber.keyboardShortcuts.action.open_settings.label': 'Einstellungen öffnen', 'settings.openchamber.keyboardShortcuts.action.toggle_terminal.label': 'Terminal-Dock umschalten', 'settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label': 'Terminal erweitert umschalten', + 'settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label': 'Auswahl zum Chat hinzufügen', 'settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label': 'Seitenleiste umschalten', 'settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label': 'Rechte Seitenleiste umschalten', 'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label': 'Git-Tab der rechten Seitenleiste öffnen', diff --git a/packages/ui/src/lib/i18n/messages/de.ts b/packages/ui/src/lib/i18n/messages/de.ts index e740b352..99a50cd0 100644 --- a/packages/ui/src/lib/i18n/messages/de.ts +++ b/packages/ui/src/lib/i18n/messages/de.ts @@ -1532,6 +1532,7 @@ export const dict = { 'helpDialog.item.openCommandPalette': 'Befehlspalette öffnen', 'helpDialog.item.showKeyboardShortcuts': 'Tastaturkürzel anzeigen (dieses Dialogfeld)', 'helpDialog.item.toggleSessionSidebar': 'Sitzungs-Seitenleiste umschalten', + 'helpDialog.item.addSelectionToChat': 'Auswahl zum Chat hinzufügen', 'helpDialog.item.cycleAgent': 'Agent wechseln (Chat-Eingabe)', 'helpDialog.item.openModelSelector': 'Modell-Auswahldialog öffnen', 'helpDialog.item.navigateModels': 'Modelle navigieren (in Auswahl)', diff --git a/packages/ui/src/lib/i18n/messages/en.settings.ts b/packages/ui/src/lib/i18n/messages/en.settings.ts index 6261cb9c..4a1deb81 100644 --- a/packages/ui/src/lib/i18n/messages/en.settings.ts +++ b/packages/ui/src/lib/i18n/messages/en.settings.ts @@ -1103,6 +1103,7 @@ export const settingsDict = { 'settings.openchamber.keyboardShortcuts.action.open_settings.label': 'Open settings', 'settings.openchamber.keyboardShortcuts.action.toggle_terminal.label': 'Toggle terminal dock', 'settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label': 'Toggle terminal expanded', + 'settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label': 'Add selection to chat', 'settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label': 'Toggle sidebar', 'settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label': 'Toggle context panel', 'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label': 'Open Git surface', diff --git a/packages/ui/src/lib/i18n/messages/en.ts b/packages/ui/src/lib/i18n/messages/en.ts index fadeeaab..1bb6b1c1 100644 --- a/packages/ui/src/lib/i18n/messages/en.ts +++ b/packages/ui/src/lib/i18n/messages/en.ts @@ -1679,6 +1679,7 @@ export const dict = { 'helpDialog.item.openCommandPalette': 'Open Command Palette', 'helpDialog.item.showKeyboardShortcuts': 'Show Keyboard Shortcuts (this dialog)', 'helpDialog.item.toggleSessionSidebar': 'Toggle Session Sidebar', + 'helpDialog.item.addSelectionToChat': 'Add Selection to Chat', 'helpDialog.item.cycleAgent': 'Cycle Agent (chat input)', 'helpDialog.item.openModelSelector': 'Open Model Selector', 'helpDialog.item.navigateModels': 'Navigate Models (in picker)', diff --git a/packages/ui/src/lib/i18n/messages/es.settings.ts b/packages/ui/src/lib/i18n/messages/es.settings.ts index 7f13e2e3..31b07208 100644 --- a/packages/ui/src/lib/i18n/messages/es.settings.ts +++ b/packages/ui/src/lib/i18n/messages/es.settings.ts @@ -1070,6 +1070,7 @@ export const settingsDict = { "settings.openchamber.keyboardShortcuts.action.open_settings.label": "Abrir configuración", "settings.openchamber.keyboardShortcuts.action.toggle_terminal.label": "Mostrar u ocultar panel de terminal", "settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label": "Expandir o contraer terminal", + "settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label": "Agregar selección al chat", "settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label": "Mostrar u ocultar barra lateral", "settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label": 'Alternar panel de contexto', "settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label": 'Abrir superficie de Git', diff --git a/packages/ui/src/lib/i18n/messages/es.ts b/packages/ui/src/lib/i18n/messages/es.ts index 76c6dec0..6719cc2b 100644 --- a/packages/ui/src/lib/i18n/messages/es.ts +++ b/packages/ui/src/lib/i18n/messages/es.ts @@ -1657,6 +1657,7 @@ export const dict: Record = { "helpDialog.item.openCommandPalette": "Abrir paleta de comandos", "helpDialog.item.showKeyboardShortcuts": "Mostrar atajos de teclado (este diálogo)", "helpDialog.item.toggleSessionSidebar": "Mostrar u ocultar barra lateral de sesión", + "helpDialog.item.addSelectionToChat": "Agregar selección al chat", "helpDialog.item.cycleAgent": "Cambiar agente (entrada de chat)", "helpDialog.item.openModelSelector": "Abrir selector de modelos", "helpDialog.item.navigateModels": "Navegar modelos (en selector)", diff --git a/packages/ui/src/lib/i18n/messages/fr.settings.ts b/packages/ui/src/lib/i18n/messages/fr.settings.ts index d0034fa8..f5dc26d4 100644 --- a/packages/ui/src/lib/i18n/messages/fr.settings.ts +++ b/packages/ui/src/lib/i18n/messages/fr.settings.ts @@ -991,6 +991,7 @@ export const settingsDict = { 'settings.openchamber.keyboardShortcuts.action.open_settings.label': 'Ouvrir les paramètres', 'settings.openchamber.keyboardShortcuts.action.toggle_terminal.label': 'Basculer la station d\'accueil du terminal', 'settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label': 'Terminal à bascule étendu', + 'settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label': 'Ajouter la sélection au chat', 'settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label': 'Basculer la barre latérale', 'settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label': 'Afficher/masquer le panneau de contexte', 'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label': 'Ouvrir la surface Git', diff --git a/packages/ui/src/lib/i18n/messages/fr.ts b/packages/ui/src/lib/i18n/messages/fr.ts index 4104c436..042f21c3 100644 --- a/packages/ui/src/lib/i18n/messages/fr.ts +++ b/packages/ui/src/lib/i18n/messages/fr.ts @@ -1492,6 +1492,7 @@ export const dict = { 'helpDialog.item.openCommandPalette': 'Ouvrir la palette de commandes', 'helpDialog.item.showKeyboardShortcuts': 'Afficher les raccourcis clavier (cette boîte de dialogue)', 'helpDialog.item.toggleSessionSidebar': 'Toggle la barre latérale de la session', + 'helpDialog.item.addSelectionToChat': 'Ajouter la sélection au chat', 'helpDialog.item.cycleAgent': 'Agent de cycle (entrée de chat)', 'helpDialog.item.openModelSelector': 'Ouvrir le sélecteur de modèle', 'helpDialog.item.navigateModels': 'Naviguer dans les modèles (dans le sélecteur)', diff --git a/packages/ui/src/lib/i18n/messages/ja.settings.ts b/packages/ui/src/lib/i18n/messages/ja.settings.ts index f61974f8..3d7565a9 100644 --- a/packages/ui/src/lib/i18n/messages/ja.settings.ts +++ b/packages/ui/src/lib/i18n/messages/ja.settings.ts @@ -1103,6 +1103,7 @@ export const settingsDict = { 'settings.openchamber.keyboardShortcuts.action.open_settings.label': '設定を開く', 'settings.openchamber.keyboardShortcuts.action.toggle_terminal.label': 'ターミナルドックの切替', 'settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label': 'ターミナル拡大の切替', + 'settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label': '選択範囲をチャットに追加', 'settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label': 'サイドバーの切替', 'settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label': 'コンテキストパネルの表示切替', 'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label': 'Git サーフェスを開く', diff --git a/packages/ui/src/lib/i18n/messages/ja.ts b/packages/ui/src/lib/i18n/messages/ja.ts index fd1189d5..0feeccc7 100644 --- a/packages/ui/src/lib/i18n/messages/ja.ts +++ b/packages/ui/src/lib/i18n/messages/ja.ts @@ -1675,6 +1675,7 @@ export const dict: Record = { 'helpDialog.item.openCommandPalette': 'コマンドパレットを開く', 'helpDialog.item.showKeyboardShortcuts': 'キーボードショートカットを表示(このダイアログ)', 'helpDialog.item.toggleSessionSidebar': 'セッションサイドバーの切り替え', + 'helpDialog.item.addSelectionToChat': '選択範囲をチャットに追加', 'helpDialog.item.cycleAgent': 'エージェント切り替え(チャット入力)', 'helpDialog.item.openModelSelector': 'モデルセレクターを開く', 'helpDialog.item.navigateModels': 'モデルを移動(ピッカー内)', diff --git a/packages/ui/src/lib/i18n/messages/ko.settings.ts b/packages/ui/src/lib/i18n/messages/ko.settings.ts index 54f276d4..df181c60 100644 --- a/packages/ui/src/lib/i18n/messages/ko.settings.ts +++ b/packages/ui/src/lib/i18n/messages/ko.settings.ts @@ -1070,6 +1070,7 @@ export const settingsDict = { 'settings.openchamber.keyboardShortcuts.action.open_settings.label': '설정 열기', 'settings.openchamber.keyboardShortcuts.action.toggle_terminal.label': '터미널 dock 토글', 'settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label': '터미널 확장 토글', + 'settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label': '선택 내용을 채팅에 추가', 'settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label': '사이드바 토글', 'settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label': '컨텍스트 패널 표시 전환', 'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label': 'Git 서피스 열기', diff --git a/packages/ui/src/lib/i18n/messages/ko.ts b/packages/ui/src/lib/i18n/messages/ko.ts index 3d0a4e7b..c47b078b 100644 --- a/packages/ui/src/lib/i18n/messages/ko.ts +++ b/packages/ui/src/lib/i18n/messages/ko.ts @@ -1681,6 +1681,7 @@ export const dict: Record = { 'helpDialog.item.openCommandPalette': '명령 팔레트 열기', 'helpDialog.item.showKeyboardShortcuts': '키보드 단축키 보기(이 대화상자)', 'helpDialog.item.toggleSessionSidebar': '토글 세션 사이드바', + 'helpDialog.item.addSelectionToChat': '선택 내용을 채팅에 추가', 'helpDialog.item.cycleAgent': '에이전트 순환(채팅 입력)', 'helpDialog.item.openModelSelector': '모델 선택기 열기', 'helpDialog.item.navigateModels': '모델 이동(선택기)', diff --git a/packages/ui/src/lib/i18n/messages/pl.settings.ts b/packages/ui/src/lib/i18n/messages/pl.settings.ts index c0ac580c..dcc3d774 100644 --- a/packages/ui/src/lib/i18n/messages/pl.settings.ts +++ b/packages/ui/src/lib/i18n/messages/pl.settings.ts @@ -825,6 +825,7 @@ export const settingsDict = { 'settings.openchamber.keyboardShortcuts.action.toggle_context_plan.label': 'Przełącz panel kontekstu planu', 'settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label': 'Przełącz panel kontekstu', 'settings.openchamber.keyboardShortcuts.action.toggle_services_menu.label': 'Przełącz menu usług', + 'settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label': 'Dodaj zaznaczenie do czatu', 'settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label': 'Przełącz pasek boczny', 'settings.openchamber.keyboardShortcuts.action.toggle_terminal.label': 'Przełącz dokowanie terminala', 'settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label': 'Przełącz rozszerzony terminal', diff --git a/packages/ui/src/lib/i18n/messages/pl.ts b/packages/ui/src/lib/i18n/messages/pl.ts index 2312a1d9..acc03cfd 100644 --- a/packages/ui/src/lib/i18n/messages/pl.ts +++ b/packages/ui/src/lib/i18n/messages/pl.ts @@ -2325,6 +2325,7 @@ export const dict: Record = { 'helpDialog.item.toggleRightSidebar': 'Przełącz panel kontekstu', 'helpDialog.item.toggleServicesMenu': 'Przełącz menu usług', 'helpDialog.item.toggleSessionSidebar': 'Przełącz panel sesji', + 'helpDialog.item.addSelectionToChat': 'Dodaj zaznaczenie do czatu', 'helpDialog.item.toggleTerminalDock': 'Przełącz dolny terminal', 'helpDialog.item.toggleTerminalExpanded': 'Przełącz rozszerzenie terminala', 'helpDialog.keyCombiner.or': 'lub', diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts b/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts index 19dc9b39..5950ec10 100644 --- a/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts +++ b/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts @@ -1070,6 +1070,7 @@ export const settingsDict = { "settings.openchamber.keyboardShortcuts.action.open_settings.label": "Abrir configurações", "settings.openchamber.keyboardShortcuts.action.toggle_terminal.label": "Mostrar ou ocultar painel de terminal", "settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label": "Expandir ou recolher terminal", + "settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label": "Adicionar seleção ao chat", "settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label": "Mostrar ou ocultar barra lateral", "settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label": 'Alternar painel de contexto', "settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label": 'Abrir superfície do Git', diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.ts b/packages/ui/src/lib/i18n/messages/pt-BR.ts index 1111a781..748628b3 100644 --- a/packages/ui/src/lib/i18n/messages/pt-BR.ts +++ b/packages/ui/src/lib/i18n/messages/pt-BR.ts @@ -1657,6 +1657,7 @@ export const dict: Record = { "helpDialog.item.openCommandPalette": "Abrir paleta de comandos", "helpDialog.item.showKeyboardShortcuts": "Mostrar atalhos de teclado (este diálogo)", "helpDialog.item.toggleSessionSidebar": "Mostrar ou ocultar barra lateral de sessão", + "helpDialog.item.addSelectionToChat": "Adicionar seleção ao chat", "helpDialog.item.cycleAgent": "Alternar agente (entrada do chat)", "helpDialog.item.openModelSelector": "Abrir seletor de modelos", "helpDialog.item.navigateModels": "Navegar por modelos (no seletor)", diff --git a/packages/ui/src/lib/i18n/messages/uk.settings.ts b/packages/ui/src/lib/i18n/messages/uk.settings.ts index a7325ccb..306608d2 100644 --- a/packages/ui/src/lib/i18n/messages/uk.settings.ts +++ b/packages/ui/src/lib/i18n/messages/uk.settings.ts @@ -1070,6 +1070,7 @@ export const settingsDict = { "settings.openchamber.keyboardShortcuts.action.open_settings.label": "Відкрити налаштування", "settings.openchamber.keyboardShortcuts.action.toggle_terminal.label": "Перемкнути панель терміналу", "settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label": "Розгорнути або згорнути термінал", + "settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label": "Додати виділення в чат", "settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label": "Перемкнути бічну панель", "settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label": 'Перемкнути контекстну панель', "settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label": 'Відкрити поверхню Git', diff --git a/packages/ui/src/lib/i18n/messages/uk.ts b/packages/ui/src/lib/i18n/messages/uk.ts index d6c43bbb..c573b669 100644 --- a/packages/ui/src/lib/i18n/messages/uk.ts +++ b/packages/ui/src/lib/i18n/messages/uk.ts @@ -1657,6 +1657,7 @@ export const dict: Record = { "helpDialog.item.openCommandPalette": "Відкрити палітру команд", "helpDialog.item.showKeyboardShortcuts": "Показати комбінації клавіш (це діалогове вікно)", "helpDialog.item.toggleSessionSidebar": "Перемкнути бічну панель сесій", + "helpDialog.item.addSelectionToChat": "Додати виділення в чат", "helpDialog.item.cycleAgent": "Перемкнути агента (введення в чат)", "helpDialog.item.openModelSelector": "Відкрити засіб вибору моделі", "helpDialog.item.navigateModels": "Навігація моделями (у засобі вибору)", diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts b/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts index adc5a8b6..42aa7d0a 100644 --- a/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts +++ b/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts @@ -1070,6 +1070,7 @@ export const settingsDict = { 'settings.openchamber.keyboardShortcuts.action.open_settings.label': '打开设置', 'settings.openchamber.keyboardShortcuts.action.toggle_terminal.label': '切换终端停靠区', 'settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label': '切换终端展开', + 'settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label': '将选中内容添加到聊天', 'settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label': '切换侧边栏', 'settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label': '切换上下文面板', 'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label': '打开 Git 界面', diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.ts b/packages/ui/src/lib/i18n/messages/zh-CN.ts index dd7cccc8..558cc2fa 100644 --- a/packages/ui/src/lib/i18n/messages/zh-CN.ts +++ b/packages/ui/src/lib/i18n/messages/zh-CN.ts @@ -1645,6 +1645,7 @@ export const dict: Record = { 'helpDialog.item.openCommandPalette': '打开命令面板', 'helpDialog.item.showKeyboardShortcuts': '显示键盘快捷键(此对话框)', 'helpDialog.item.toggleSessionSidebar': '切换会话侧边栏', + 'helpDialog.item.addSelectionToChat': '将选中内容添加到聊天', 'helpDialog.item.cycleAgent': '循环切换智能体(聊天输入)', 'helpDialog.item.openModelSelector': '打开模型选择器', 'helpDialog.item.navigateModels': '导航模型(选择器中)', diff --git a/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts b/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts index 24571ef4..a1d814b0 100644 --- a/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts +++ b/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts @@ -977,6 +977,7 @@ 'settings.openchamber.keyboardShortcuts.action.open_settings.label': '開啟設定', 'settings.openchamber.keyboardShortcuts.action.toggle_terminal.label': '切換終端機停靠區', 'settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label': '切換終端機展開', + 'settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label': '將選取內容加入聊天', 'settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label': '切換側邊欄', 'settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label': '切換上下文面板', 'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label': '開啟 Git 介面', diff --git a/packages/ui/src/lib/i18n/messages/zh-TW.ts b/packages/ui/src/lib/i18n/messages/zh-TW.ts index 9de1163e..6bf7546a 100644 --- a/packages/ui/src/lib/i18n/messages/zh-TW.ts +++ b/packages/ui/src/lib/i18n/messages/zh-TW.ts @@ -1649,6 +1649,7 @@ export const dict: Record = { 'helpDialog.item.openCommandPalette': '開啟命令面板', 'helpDialog.item.showKeyboardShortcuts': '顯示鍵盤快速鍵(此對話方塊)', 'helpDialog.item.toggleSessionSidebar': '切換會話側邊欄', + 'helpDialog.item.addSelectionToChat': '將選取內容加入聊天', 'helpDialog.item.cycleAgent': '循環切換 Agent(聊天輸入)', 'helpDialog.item.openModelSelector': '開啟模型選擇器', 'helpDialog.item.navigateModels': '導覽模型(選擇器中)', diff --git a/packages/ui/src/lib/shortcuts.ts b/packages/ui/src/lib/shortcuts.ts index e6e16e29..6afb4656 100644 --- a/packages/ui/src/lib/shortcuts.ts +++ b/packages/ui/src/lib/shortcuts.ts @@ -159,8 +159,15 @@ const SHORTCUT_ACTIONS: ReadonlyArray = [ description: 'Toggle the files panel', }, { - id: 'toggle_sidebar', + id: 'add_selection_to_chat', defaultCombo: 'mod+l', + label: 'Add selection to chat', + description: 'Add the selected text to the chat input', + customizable: true, + }, + { + id: 'toggle_sidebar', + defaultCombo: 'mod+alt+l', label: 'Toggle sidebar', description: 'Toggle the session sidebar', customizable: true,