diff --git a/CHANGELOG.md b/CHANGELOG.md index 89e2b34e..9688673e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,11 @@ All notable changes to this project will be documented in this file. - **Session tabs (opt-in):** the web/desktop header can show your open sessions as browser-style tabs — turn them on in Settings → General → Navigation → Session tabs. Every session you open joins the strip, clicking a tab switches the whole workspace (chat, project, panels), and closing one (its × button, middle-click, or Alt+W — rebindable in Shortcuts) never touches the session itself. Tabs reorder by drag, scroll behind the header buttons when there are many and carry the sidebar's running/unread dot. Each tab has the full session menu — on the "..." button or right-click — plus Close other tabs; renaming works right in the tab. - **Keyboard shortcuts learned sequences** (thanks @ChangeHow): shortcuts can now be two steps — press Cmd/Ctrl+S, then a letter: P opens the draft's project picker, G its branch picker, L the session list. A held sequence shows no menu but forgives you: Escape or three quiet seconds cancel it, and typing into a text field never triggers one armed elsewhere. Dropdown menus and pickers now also answer Ctrl+N/Ctrl+P for down/up, the session switcher opens focused on your current session, and every tooltip and menu label shows the binding you actually have set, not the default. - **Keyboard shortcuts redesigned.** Defaults now follow one model: single chords for everyday actions, a two-step Cmd/Ctrl+K leader for open/go actions (K then P/G/L — project picker, branch picker, session list; T timeline, N prompt navigator, I services, H shortcut help, C theme), held Cmd/Ctrl+digit for header session tabs and held Cmd/Ctrl+Option+digit for context panel surfaces. Cmd/Ctrl+B now toggles the sidebar; when it's already open, the session-list shortcut jumps into its search. Rare actions moved into the command palette instead of carrying obscure default bindings, and custom bindings recorded under the old layout are reset once. Shortcuts also stopped requiring an English keyboard layout — bindings follow the physical key on non-Latin layouts (and Option-modified digits on macOS), including when recording custom ones. +- **Permission cards answer to the keyboard:** Alt+Enter allows once, Alt+Shift+Enter allows always, Alt+Backspace denies — the keys are printed on the buttons, and the newest pending card is the one that listens. The auto-accept toggle also got a shortcut (Cmd/Ctrl+K, A). +- Sessions: Cmd/Ctrl+Alt+Left/Right steps back and forward through the sessions you opened in this window, browser-history style; with session tabs enabled it moves between neighbouring tabs instead. Cmd/Ctrl+K, R renames the current session right in the header. +- Git: Cmd/Ctrl+Enter in the commit message box commits, like every git client. +- Diff: Alt+Down/Up jumps review to the next or previous changed file, expanding it if collapsed. +- Command palette: rarely used commands (pin session, copy session ID, multi-run launcher, archived sessions, notes, todos, status, theme, memory debug) are now found by typing but stay off the first screen, which keeps the initial list scroll-free. - Chat: comment on a reply — select text in a chat message (or in a rendered markdown preview in Files) and choose Comment to attach exactly that quote, with a source line range when it can be located, plus your note to the next message. The selection stays highlighted while you type, and the selection menu was restyled — Add to chat is now Add to input. - Diff: comment like a review — hovering a line shows a + button in the gutter; clicking it, clicking a line, or dragging across lines opens the comment editor for that line or range. The comment editor and saved-comment cards match the chat's comment style. - Composer: hovering or tapping a context chip above the input opens a stacked preview of everything attached, where a comment can be edited in place or an item removed before sending. diff --git a/packages/ui/src/components/chat/ChatInput.tsx b/packages/ui/src/components/chat/ChatInput.tsx index f088d16d..2cf2578b 100644 --- a/packages/ui/src/components/chat/ChatInput.tsx +++ b/packages/ui/src/components/chat/ChatInput.tsx @@ -77,6 +77,7 @@ import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs'; import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory'; import { usePermissionStore } from '@/stores/permissionStore'; import { togglePermissionAutoAccept } from './permissionAutoAccept'; +import { useKeybind } from '@/hooks/useKeybind'; import { extractGitChangedFiles } from './changedFiles'; import { useI18n } from '@/lib/i18n'; import { sessionEvents } from '@/lib/sessionEvents'; @@ -2562,6 +2563,11 @@ const ChatInputComponent: React.FC = ({ t, ]); + useKeybind('toggle_permission_auto_accept', () => { + if (!isPermissionAutoAcceptInteractive) return false; + handlePermissionAutoAcceptToggle(); + }); + React.useEffect(() => { const pendingAbortBanner = Boolean(abortPromptSessionId) && abortPromptSessionId === currentSessionId; if (!prevWasAbortedRef.current && pendingAbortBanner && !showAbortStatus) { diff --git a/packages/ui/src/components/chat/PermissionCard.tsx b/packages/ui/src/components/chat/PermissionCard.tsx index cd63576b..851d70d3 100644 --- a/packages/ui/src/components/chat/PermissionCard.tsx +++ b/packages/ui/src/components/chat/PermissionCard.tsx @@ -10,6 +10,10 @@ import { Icon } from "@/components/icon/Icon"; import { DiffPreview, WritePreview } from './DiffPreview'; import { useI18n } from '@/lib/i18n'; import { getVisiblePermissionPatterns } from './permissionCardPatterns'; +import { formatShortcutForDisplay } from '@/lib/shortcuts'; + +// Newest pending card owns the keyboard; older cards wait their turn. +const activePermissionCardIds: string[] = []; const PERMISSION_BASH_CUSTOM_STYLE: React.CSSProperties = { margin: 0, @@ -126,6 +130,33 @@ export const PermissionCard: React.FC = ({ } }; + const handleResponseRef = React.useRef(handleResponse); + handleResponseRef.current = handleResponse; + + React.useEffect(() => { + if (hasResponded) return; + activePermissionCardIds.push(permission.id); + const handleKeyDown = (event: KeyboardEvent) => { + if (activePermissionCardIds.at(-1) !== permission.id) return; + if (!event.altKey || event.metaKey || event.ctrlKey) return; + const response = event.key === 'Enter' + ? (event.shiftKey ? 'always' as const : 'once' as const) + : event.key === 'Backspace' && !event.shiftKey + ? 'reject' as const + : null; + if (!response) return; + event.preventDefault(); + event.stopPropagation(); + void handleResponseRef.current(response); + }; + window.addEventListener('keydown', handleKeyDown, true); + return () => { + window.removeEventListener('keydown', handleKeyDown, true); + const index = activePermissionCardIds.lastIndexOf(permission.id); + if (index !== -1) activePermissionCardIds.splice(index, 1); + }; + }, [hasResponded, permission.id]); + if (hasResponded) { return null; } @@ -380,6 +411,7 @@ export const PermissionCard: React.FC = ({ > Allow Once + {formatShortcutForDisplay('alt+enter')} {permission.always.length > 0 ? ( @@ -436,6 +468,7 @@ export const PermissionCard: React.FC = ({ > Always Allow + {formatShortcutForDisplay('alt+shift+enter')} )} @@ -459,6 +492,7 @@ export const PermissionCard: React.FC = ({ > Deny + {formatShortcutForDisplay('alt+backspace')} {isResponding && ( diff --git a/packages/ui/src/components/layout/Header.tsx b/packages/ui/src/components/layout/Header.tsx index 09fad963..90573996 100644 --- a/packages/ui/src/components/layout/Header.tsx +++ b/packages/ui/src/components/layout/Header.tsx @@ -1463,6 +1463,10 @@ export const Header: React.FC = () => { useKeybinds({ + rename_current_session: () => { + if (!currentSessionId || isMobile) return false; + beginHeaderSessionRename(); + }, toggle_services_menu: () => { if (isDesktopServicesOpen) { setIsDesktopServicesOpen(false); diff --git a/packages/ui/src/components/ui/CommandPalette.tsx b/packages/ui/src/components/ui/CommandPalette.tsx index 3acc62b4..5f2d5c86 100644 --- a/packages/ui/src/components/ui/CommandPalette.tsx +++ b/packages/ui/src/components/ui/CommandPalette.tsx @@ -50,6 +50,7 @@ import { scoreByFuzzyQuery } from '@/lib/search/fuzzySearch'; import { truncatePathMiddle } from '@/lib/utils'; import { useI18n } from '@/lib/i18n'; import { sessionEvents } from '@/lib/sessionEvents'; +import { copyTextToClipboard } from '@/lib/clipboard'; import { useProjectsStore } from '@/stores/useProjectsStore'; import { buildCommandPaletteFileSearchKey, scoreCommandPaletteFiles } from './commandPaletteFilesState'; @@ -59,6 +60,9 @@ type CommandEntry = { icon: React.ReactNode; shortcutId?: string; searchText: string; + /** Search-only command: reachable by typing, hidden from the initial list + so the first screen stays scroll-free. */ + secondary?: boolean; onSelect: () => void; }; @@ -90,9 +94,14 @@ export const CommandPalette: React.FC = () => { const openContextSurface = useUIStore((s) => s.openContextSurface); const openContextFile = useUIStore((s) => s.openContextFile); const shortcutOverrides = useUIStore((s) => s.shortcutOverrides); + const openMultiRunLauncher = useUIStore((s) => s.openMultiRunLauncher); + const setArchivePageOpen = useUIStore((s) => s.setArchivePageOpen); + const setProjectContextTab = useUIStore((s) => s.setProjectContextTab); const openNewSessionDraft = useSessionUIStore((s) => s.openNewSessionDraft); const setCurrentSession = useSessionUIStore((s) => s.setCurrentSession); + const currentSessionId = useSessionUIStore((s) => s.currentSessionId); + const togglePinnedSession = useSessionPinnedStore((s) => s.toggle); const activeSessions = useGlobalSessionsStore(React.useCallback( (state) => isCommandPaletteOpen ? state.activeSessions : EMPTY_SESSIONS, @@ -233,6 +242,7 @@ export const CommandPalette: React.FC = () => { }, { id: 'cycle-theme', + secondary: true, title: t('commandPalette.item.cycleTheme'), icon: , shortcutId: 'cycle_theme', @@ -243,6 +253,7 @@ export const CommandPalette: React.FC = () => { }, { id: 'open-status', + secondary: true, title: t('commandPalette.item.showOpenCodeStatus'), icon: , searchText: t('commandPalette.item.showOpenCodeStatus'), @@ -259,8 +270,90 @@ export const CommandPalette: React.FC = () => { onSelect: run(() => setSettingsDialogOpen(true)), }, ]; + list.push( + { + id: 'pin-session', + secondary: true, + title: t('commandPalette.item.pinSession'), + icon: , + searchText: t('commandPalette.item.pinSession'), + onSelect: run(() => { + if (currentSessionId && currentDirectory) { + togglePinnedSession({ directory: currentDirectory, sessionId: currentSessionId }); + } + }), + }, + { + id: 'copy-session-id', + secondary: true, + title: t('commandPalette.item.copySessionId'), + icon: , + searchText: t('commandPalette.item.copySessionId'), + onSelect: run(() => { + if (!currentSessionId) return; + void copyTextToClipboard(currentSessionId) + .then((result) => { + if (result.ok) { + toast.success(t('sessions.sidebar.session.copyId.success')); + return; + } + toast.error(t('sessions.sidebar.session.copyId.error')); + }) + .catch(() => toast.error(t('sessions.sidebar.session.copyId.error'))); + }), + }, + { + id: 'open-multi-run', + secondary: true, + title: t('commandPalette.item.openMultiRun'), + icon: , + searchText: t('commandPalette.item.openMultiRun'), + onSelect: run(() => { + setSessionSwitcherOpen(false); + openMultiRunLauncher(); + }), + }, + { + id: 'open-archive', + secondary: true, + title: t('commandPalette.item.openArchive'), + icon: , + searchText: t('commandPalette.item.openArchive'), + onSelect: run(() => { + setSessionSwitcherOpen(false); + setArchivePageOpen(true); + }), + }, + { + id: 'open-notes', + secondary: true, + title: t('commandPalette.item.openNotes'), + icon: , + searchText: t('commandPalette.item.openNotes'), + onSelect: run(() => { + if (currentDirectory) { + setProjectContextTab('notes'); + openContextSurface(currentDirectory, 'notes'); + } + }), + }, + { + id: 'open-todos', + secondary: true, + title: t('commandPalette.item.openTodos'), + icon: , + searchText: t('commandPalette.item.openTodos'), + onSelect: run(() => { + if (currentDirectory) { + setProjectContextTab('todos'); + openContextSurface(currentDirectory, 'notes'); + } + }), + }, + ); list.push({ id: 'toggle-memory-debug', + secondary: true, title: t('commandPalette.item.toggleMemoryDebug'), icon: , searchText: t('commandPalette.item.toggleMemoryDebug'), @@ -299,6 +392,11 @@ export const CommandPalette: React.FC = () => { setSettingsDialogOpen, activeProject?.id, activeProject?.path, + currentSessionId, + togglePinnedSession, + openMultiRunLauncher, + setArchivePageOpen, + setProjectContextTab, ]); // --------------------------------------------------------------------------- @@ -407,7 +505,9 @@ export const CommandPalette: React.FC = () => { const hasQuery = liveTrimmed.length > 0; const scoredCommands = React.useMemo(() => { - if (!hasQuery) return commands.map((item) => ({ item, score: 0 })); + if (!hasQuery) { + return commands.filter((item) => !item.secondary).map((item) => ({ item, score: 0 })); + } return scoreByFuzzyQuery(commands, liveTrimmed, (c) => c.searchText, { limit: 7, noFuzzy: true, diff --git a/packages/ui/src/components/views/DiffView.tsx b/packages/ui/src/components/views/DiffView.tsx index 28a15721..90e370a4 100644 --- a/packages/ui/src/components/views/DiffView.tsx +++ b/packages/ui/src/components/views/DiffView.tsx @@ -1768,6 +1768,37 @@ export const DiffView: React.FC = ({ scrollToFile(value); }, [cancelPendingScrollAlignment, expandStackedFile, scrollToFile]); + // Step review to the adjacent changed file (alt+arrow): selects, expands + // a collapsed section, and scrolls to it. Window-level because the diff + // surface has no persistent focus target; guarded off editable fields. + React.useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (!event.altKey || event.metaKey || event.ctrlKey || event.shiftKey) return; + if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return; + const target = event.target; + if (target instanceof HTMLElement && ( + target.isContentEditable + || target.tagName === 'INPUT' + || target.tagName === 'TEXTAREA' + || target.closest('[role="dialog"]') + )) { + return; + } + if (changedFiles.length === 0) return; + const delta = event.key === 'ArrowDown' ? 1 : -1; + const index = displayFile ? changedFiles.findIndex((file) => file.path === displayFile) : -1; + const nextIndex = index === -1 + ? (delta > 0 ? 0 : changedFiles.length - 1) + : index + delta; + const next = changedFiles[nextIndex]; + if (!next) return; + event.preventDefault(); + handleSelectFileAndScroll(next.path); + }; + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [changedFiles, displayFile, handleSelectFileAndScroll]); + const handleHeaderLayoutChange = React.useCallback((mode: DiffViewMode) => { const nextLayout: 'inline' | 'side-by-side' = mode === 'side-by-side' ? 'side-by-side' : 'inline'; diff --git a/packages/ui/src/components/views/git/CommitInput.tsx b/packages/ui/src/components/views/git/CommitInput.tsx index a724cb69..304629c7 100644 --- a/packages/ui/src/components/views/git/CommitInput.tsx +++ b/packages/ui/src/components/views/git/CommitInput.tsx @@ -6,6 +6,7 @@ import { useI18n } from '@/lib/i18n'; interface CommitInputProps { value: string; onChange: (value: string) => void; + onSubmit?: () => void; placeholder?: string; disabled?: boolean; hasTouchInput?: boolean; @@ -18,6 +19,7 @@ const MAX_HEIGHT = 200; export const CommitInput: React.FC = ({ value, onChange, + onSubmit, placeholder, disabled = false, hasTouchInput = false, @@ -58,6 +60,12 @@ export const CommitInput: React.FC = ({ ref={textareaRef} value={value} onChange={(e) => onChange(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter' && (e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey) { + e.preventDefault(); + onSubmit?.(); + } + }} placeholder={placeholder ?? t('gitView.commit.messagePlaceholder')} rows={1} disabled={disabled} diff --git a/packages/ui/src/components/views/git/CommitSection.tsx b/packages/ui/src/components/views/git/CommitSection.tsx index 7c7ac7b8..a8cf0b34 100644 --- a/packages/ui/src/components/views/git/CommitSection.tsx +++ b/packages/ui/src/components/views/git/CommitSection.tsx @@ -68,6 +68,9 @@ export const CommitSection: React.FC = ({ { + if (canCommit && !isGeneratingMessage) onCommit(); + }} placeholder={t('gitView.commit.messagePlaceholder')} disabled={commitAction !== null} hasTouchInput={hasTouchInput} diff --git a/packages/ui/src/hooks/useKeyboardShortcuts.ts b/packages/ui/src/hooks/useKeyboardShortcuts.ts index 09ef0327..d04a1a9e 100644 --- a/packages/ui/src/hooks/useKeyboardShortcuts.ts +++ b/packages/ui/src/hooks/useKeyboardShortcuts.ts @@ -1,7 +1,8 @@ import React from 'react'; import { isTerminalEventTarget } from '@/lib/terminalFocus'; import { useSessionUIStore } from '@/sync/session-ui-store'; -import { activateSessionTabByIndex, closeSessionTabAndActivateNeighbour } from '@/lib/sessionTabs'; +import { activateAdjacentSessionTab, activateSessionTabByIndex, closeSessionTabAndActivateNeighbour } from '@/lib/sessionTabs'; +import { navigateSessionHistory } from '@/lib/sessionNavigationHistory'; import { useSelectionStore } from '@/sync/selection-store'; import * as sessionActions from '@/sync/session-actions'; import { normalizeContextPanelDirectoryKey, useUIStore } from '@/stores/useUIStore'; @@ -169,6 +170,14 @@ export const useKeyboardShortcuts = () => { console.warn('[keyboard-shortcuts] failed to open draft mini chat window', error); }); }, + switch_session_previous: () => { + if (!isVSCodeRuntime() && useUIStore.getState().sessionTabsEnabled && activateAdjacentSessionTab(-1)) return; + return navigateSessionHistory(-1) ? undefined : false; + }, + switch_session_next: () => { + if (!isVSCodeRuntime() && useUIStore.getState().sessionTabsEnabled && activateAdjacentSessionTab(1)) return; + return navigateSessionHistory(1) ? undefined : false; + }, close_session_tab: () => { if (isVSCodeRuntime() || !useUIStore.getState().sessionTabsEnabled) return false; if (currentSessionId) { diff --git a/packages/ui/src/lib/i18n/messages/de.settings.ts b/packages/ui/src/lib/i18n/messages/de.settings.ts index 6f8c5352..cf91d54d 100644 --- a/packages/ui/src/lib/i18n/messages/de.settings.ts +++ b/packages/ui/src/lib/i18n/messages/de.settings.ts @@ -1085,6 +1085,10 @@ export const settingsDict = { 'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': 'Kontextpanel-Oberfläche wechseln', 'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0', 'settings.openchamber.keyboardShortcuts.action.new_chat.label': 'Neue Sitzung', + 'settings.openchamber.keyboardShortcuts.action.switch_session_previous.label': 'Vorherige Sitzung', + 'settings.openchamber.keyboardShortcuts.action.switch_session_next.label': 'Nächste Sitzung', + 'settings.openchamber.keyboardShortcuts.action.rename_current_session.label': 'Aktuelle Sitzung umbenennen', + 'settings.openchamber.keyboardShortcuts.action.toggle_permission_auto_accept.label': 'Auto-Genehmigung umschalten', 'settings.openchamber.keyboardShortcuts.action.close_session_tab.label': 'Sitzungs-Tab schließen', 'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': 'Neuer Worktree-Entwurf', 'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': 'Neues Mini-Chat-Fenster', diff --git a/packages/ui/src/lib/i18n/messages/de.ts b/packages/ui/src/lib/i18n/messages/de.ts index adc8076b..815186a0 100644 --- a/packages/ui/src/lib/i18n/messages/de.ts +++ b/packages/ui/src/lib/i18n/messages/de.ts @@ -2292,6 +2292,12 @@ export const dict = { 'commandPalette.item.cycleTheme': 'Thema wechseln', 'commandPalette.item.showOpenCodeStatus': 'OpenCode-Status anzeigen', 'commandPalette.item.toggleMemoryDebug': 'Memory-Debug-Panel umschalten', + 'commandPalette.item.pinSession': 'Sitzung anheften oder lösen', + 'commandPalette.item.copySessionId': 'Sitzungs-ID kopieren', + 'commandPalette.item.openMultiRun': 'Multi-Run-Launcher öffnen', + 'commandPalette.item.openArchive': 'Archivierte Sitzungen öffnen', + 'commandPalette.item.openNotes': 'Notizbereich öffnen', + 'commandPalette.item.openTodos': 'To-do-Bereich öffnen', 'commandPalette.item.openSettings': 'Einstellungen öffnen...', 'commandPalette.session.untitled': 'Unbenannte Sitzung', 'openCodeStatusDialog.title': 'OpenCode-Status', diff --git a/packages/ui/src/lib/i18n/messages/en.settings.ts b/packages/ui/src/lib/i18n/messages/en.settings.ts index ef9d6265..8990d2ec 100644 --- a/packages/ui/src/lib/i18n/messages/en.settings.ts +++ b/packages/ui/src/lib/i18n/messages/en.settings.ts @@ -1147,6 +1147,10 @@ export const settingsDict = { 'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': 'Switch context panel surface', 'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0', 'settings.openchamber.keyboardShortcuts.action.new_chat.label': 'New session', + 'settings.openchamber.keyboardShortcuts.action.switch_session_previous.label': 'Previous session', + 'settings.openchamber.keyboardShortcuts.action.switch_session_next.label': 'Next session', + 'settings.openchamber.keyboardShortcuts.action.rename_current_session.label': 'Rename current session', + 'settings.openchamber.keyboardShortcuts.action.toggle_permission_auto_accept.label': 'Toggle permission auto-accept', 'settings.openchamber.keyboardShortcuts.action.close_session_tab.label': 'Close session tab', 'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': 'New worktree draft', 'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': 'New Mini Chat window', diff --git a/packages/ui/src/lib/i18n/messages/en.ts b/packages/ui/src/lib/i18n/messages/en.ts index 686698de..62ab1973 100644 --- a/packages/ui/src/lib/i18n/messages/en.ts +++ b/packages/ui/src/lib/i18n/messages/en.ts @@ -2482,6 +2482,12 @@ export const dict = { 'commandPalette.item.cycleTheme': 'Cycle theme', 'commandPalette.item.showOpenCodeStatus': 'Show OpenCode status', 'commandPalette.item.toggleMemoryDebug': 'Toggle memory debug panel', + 'commandPalette.item.pinSession': 'Pin or unpin session', + 'commandPalette.item.copySessionId': 'Copy session ID', + 'commandPalette.item.openMultiRun': 'Open multi-run launcher', + 'commandPalette.item.openArchive': 'Open archived sessions', + 'commandPalette.item.openNotes': 'Open notes surface', + 'commandPalette.item.openTodos': 'Open todos surface', 'commandPalette.item.openSettings': 'Open Settings...', 'commandPalette.session.untitled': 'Untitled Session', 'openCodeStatusDialog.title': 'OpenCode Status', diff --git a/packages/ui/src/lib/i18n/messages/es.settings.ts b/packages/ui/src/lib/i18n/messages/es.settings.ts index 63f646fb..6cf7245c 100644 --- a/packages/ui/src/lib/i18n/messages/es.settings.ts +++ b/packages/ui/src/lib/i18n/messages/es.settings.ts @@ -1115,6 +1115,10 @@ export const settingsDict = { "settings.openchamber.keyboardShortcuts.action.switch_context_surface.label": "Cambiar superficie del panel de contexto", "settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix": " + 1…0", "settings.openchamber.keyboardShortcuts.action.new_chat.label": "Nueva sesión", + "settings.openchamber.keyboardShortcuts.action.switch_session_previous.label": "Sesión anterior", + "settings.openchamber.keyboardShortcuts.action.switch_session_next.label": "Sesión siguiente", + "settings.openchamber.keyboardShortcuts.action.rename_current_session.label": "Renombrar sesión actual", + "settings.openchamber.keyboardShortcuts.action.toggle_permission_auto_accept.label": "Alternar aprobación automática", "settings.openchamber.keyboardShortcuts.action.close_session_tab.label": "Cerrar pestaña de sesión", "settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label": "Nuevo borrador de worktree", "settings.openchamber.keyboardShortcuts.action.new_mini_chat.label": "Nueva ventana Mini Chat", diff --git a/packages/ui/src/lib/i18n/messages/es.ts b/packages/ui/src/lib/i18n/messages/es.ts index 031d9b82..4760f8a1 100644 --- a/packages/ui/src/lib/i18n/messages/es.ts +++ b/packages/ui/src/lib/i18n/messages/es.ts @@ -2448,6 +2448,12 @@ export const dict: Record = { "commandPalette.item.cycleTheme": "Cambiar tema", "commandPalette.item.showOpenCodeStatus": "Mostrar estado de OpenCode", "commandPalette.item.toggleMemoryDebug": "Alternar panel de depuración de memoria", + "commandPalette.item.pinSession": "Anclar o desanclar sesión", + "commandPalette.item.copySessionId": "Copiar ID de sesión", + "commandPalette.item.openMultiRun": "Abrir lanzador multi-run", + "commandPalette.item.openArchive": "Abrir sesiones archivadas", + "commandPalette.item.openNotes": "Abrir panel de notas", + "commandPalette.item.openTodos": "Abrir panel de tareas", "commandPalette.item.openSettings": "Abrir configuración...", "commandPalette.session.untitled": "Sesión sin título", "openCodeStatusDialog.title": "Estado de OpenCode", diff --git a/packages/ui/src/lib/i18n/messages/fr.settings.ts b/packages/ui/src/lib/i18n/messages/fr.settings.ts index f4df769b..4e1af53b 100644 --- a/packages/ui/src/lib/i18n/messages/fr.settings.ts +++ b/packages/ui/src/lib/i18n/messages/fr.settings.ts @@ -1033,6 +1033,10 @@ export const settingsDict = { 'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': 'Basculer la surface du panneau contextuel', 'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0', 'settings.openchamber.keyboardShortcuts.action.new_chat.label': 'Nouvelle session', + 'settings.openchamber.keyboardShortcuts.action.switch_session_previous.label': 'Session précédente', + 'settings.openchamber.keyboardShortcuts.action.switch_session_next.label': 'Session suivante', + 'settings.openchamber.keyboardShortcuts.action.rename_current_session.label': 'Renommer la session actuelle', + 'settings.openchamber.keyboardShortcuts.action.toggle_permission_auto_accept.label': 'Basculer l’approbation automatique', 'settings.openchamber.keyboardShortcuts.action.close_session_tab.label': 'Fermer l’onglet de session', 'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': 'Nouvelle ébauche d\'worktree', 'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': 'Nouvelle fenêtre de mini-chat', diff --git a/packages/ui/src/lib/i18n/messages/fr.ts b/packages/ui/src/lib/i18n/messages/fr.ts index 982cfd30..34528808 100644 --- a/packages/ui/src/lib/i18n/messages/fr.ts +++ b/packages/ui/src/lib/i18n/messages/fr.ts @@ -2186,6 +2186,12 @@ export const dict = { 'commandPalette.item.cycleTheme': 'Changer de thème', 'commandPalette.item.showOpenCodeStatus': 'Afficher le statut OpenCode', 'commandPalette.item.toggleMemoryDebug': 'Basculer le panneau de débogage mémoire', + 'commandPalette.item.pinSession': 'Épingler ou désépingler la session', + 'commandPalette.item.copySessionId': 'Copier l\'ID de session', + 'commandPalette.item.openMultiRun': 'Ouvrir le lanceur multi-run', + 'commandPalette.item.openArchive': 'Ouvrir les sessions archivées', + 'commandPalette.item.openNotes': 'Ouvrir le panneau de notes', + 'commandPalette.item.openTodos': 'Ouvrir le panneau de tâches', 'commandPalette.item.openSettings': 'Ouvrez les paramètres...', 'commandPalette.session.untitled': 'Session sans titre', 'openCodeStatusDialog.title': 'Statut OpenCode', diff --git a/packages/ui/src/lib/i18n/messages/ja.settings.ts b/packages/ui/src/lib/i18n/messages/ja.settings.ts index 63e54c6b..4f664809 100644 --- a/packages/ui/src/lib/i18n/messages/ja.settings.ts +++ b/packages/ui/src/lib/i18n/messages/ja.settings.ts @@ -1148,6 +1148,10 @@ export const settingsDict = { 'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': 'コンテキストパネルのサーフェスを切り替え', 'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0', 'settings.openchamber.keyboardShortcuts.action.new_chat.label': '新しい Session', + 'settings.openchamber.keyboardShortcuts.action.switch_session_previous.label': '前のセッション', + 'settings.openchamber.keyboardShortcuts.action.switch_session_next.label': '次のセッション', + 'settings.openchamber.keyboardShortcuts.action.rename_current_session.label': '現在のセッション名を変更', + 'settings.openchamber.keyboardShortcuts.action.toggle_permission_auto_accept.label': '権限の自動承認を切り替え', 'settings.openchamber.keyboardShortcuts.action.close_session_tab.label': 'セッションタブを閉じる', 'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': '新しい Worktree 下書き', 'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': '新しいミニチャットウィンドウ', diff --git a/packages/ui/src/lib/i18n/messages/ja.ts b/packages/ui/src/lib/i18n/messages/ja.ts index d0c55cfc..daedcf79 100644 --- a/packages/ui/src/lib/i18n/messages/ja.ts +++ b/packages/ui/src/lib/i18n/messages/ja.ts @@ -2481,6 +2481,12 @@ export const dict: Record = { 'commandPalette.item.cycleTheme': 'テーマを順に切替', 'commandPalette.item.showOpenCodeStatus': 'OpenCode のステータスを表示', 'commandPalette.item.toggleMemoryDebug': 'メモリデバッグパネルの切替', + 'commandPalette.item.pinSession': 'セッションをピン留め/解除', + 'commandPalette.item.copySessionId': 'セッションIDをコピー', + 'commandPalette.item.openMultiRun': 'マルチラン起動画面を開く', + 'commandPalette.item.openArchive': 'アーカイブ済みセッションを開く', + 'commandPalette.item.openNotes': 'ノートパネルを開く', + 'commandPalette.item.openTodos': 'ToDoパネルを開く', 'commandPalette.item.openSettings': '設定を開く...', 'commandPalette.session.untitled': '無題のセッション', 'openCodeStatusDialog.title': 'OpenCodeステータス', diff --git a/packages/ui/src/lib/i18n/messages/ko.settings.ts b/packages/ui/src/lib/i18n/messages/ko.settings.ts index a9ec57e3..32d73070 100644 --- a/packages/ui/src/lib/i18n/messages/ko.settings.ts +++ b/packages/ui/src/lib/i18n/messages/ko.settings.ts @@ -1115,6 +1115,10 @@ export const settingsDict = { 'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': '컨텍스트 패널 서피스 전환', 'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0', 'settings.openchamber.keyboardShortcuts.action.new_chat.label': '새 세션', + 'settings.openchamber.keyboardShortcuts.action.switch_session_previous.label': '이전 세션', + 'settings.openchamber.keyboardShortcuts.action.switch_session_next.label': '다음 세션', + 'settings.openchamber.keyboardShortcuts.action.rename_current_session.label': '현재 세션 이름 바꾸기', + 'settings.openchamber.keyboardShortcuts.action.toggle_permission_auto_accept.label': '권한 자동 승인 전환', 'settings.openchamber.keyboardShortcuts.action.close_session_tab.label': '세션 탭 닫기', 'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': '새 worktree 초안', 'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': '새 Mini Chat 창', diff --git a/packages/ui/src/lib/i18n/messages/ko.ts b/packages/ui/src/lib/i18n/messages/ko.ts index cba5551d..da927fe9 100644 --- a/packages/ui/src/lib/i18n/messages/ko.ts +++ b/packages/ui/src/lib/i18n/messages/ko.ts @@ -2482,6 +2482,12 @@ export const dict: Record = { 'commandPalette.item.cycleTheme': '테마 순환', 'commandPalette.item.showOpenCodeStatus': 'OpenCode 상태 표시', 'commandPalette.item.toggleMemoryDebug': '메모리 디버그 패널 토글', + 'commandPalette.item.pinSession': '세션 고정 또는 고정 해제', + 'commandPalette.item.copySessionId': '세션 ID 복사', + 'commandPalette.item.openMultiRun': '멀티 런 런처 열기', + 'commandPalette.item.openArchive': '보관된 세션 열기', + 'commandPalette.item.openNotes': '노트 패널 열기', + 'commandPalette.item.openTodos': '할 일 패널 열기', 'commandPalette.item.openSettings': '설정... 열기', 'commandPalette.session.untitled': '제목 없는 세션', 'openCodeStatusDialog.title': 'OpenCode 상태', diff --git a/packages/ui/src/lib/i18n/messages/pl.settings.ts b/packages/ui/src/lib/i18n/messages/pl.settings.ts index b14793e1..6ca6a1f1 100644 --- a/packages/ui/src/lib/i18n/messages/pl.settings.ts +++ b/packages/ui/src/lib/i18n/messages/pl.settings.ts @@ -824,6 +824,10 @@ export const settingsDict = { 'settings.openchamber.keyboardShortcuts.action.toggle_prompt_navigator.label': 'Przełącz nawigator promptów', 'settings.openchamber.keyboardShortcuts.action.focus_input.label': 'Skup pole wprowadzania', 'settings.openchamber.keyboardShortcuts.action.new_chat.label': 'Nowa sesja', + 'settings.openchamber.keyboardShortcuts.action.switch_session_previous.label': 'Poprzednia sesja', + 'settings.openchamber.keyboardShortcuts.action.switch_session_next.label': 'Następna sesja', + 'settings.openchamber.keyboardShortcuts.action.rename_current_session.label': 'Zmień nazwę bieżącej sesji', + 'settings.openchamber.keyboardShortcuts.action.toggle_permission_auto_accept.label': 'Przełącz automatyczne zatwierdzanie', 'settings.openchamber.keyboardShortcuts.action.close_session_tab.label': 'Zamknij kartę sesji', 'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': 'Nowy szkic obszaru roboczego', 'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': 'Nowe okno Mini Chat', diff --git a/packages/ui/src/lib/i18n/messages/pl.ts b/packages/ui/src/lib/i18n/messages/pl.ts index 43d81bad..dcd5e758 100644 --- a/packages/ui/src/lib/i18n/messages/pl.ts +++ b/packages/ui/src/lib/i18n/messages/pl.ts @@ -1455,6 +1455,12 @@ export const dict: Record = { 'commandPalette.item.cycleTheme': 'Przełącz motyw', 'commandPalette.item.showOpenCodeStatus': 'Pokaż status OpenCode', 'commandPalette.item.toggleMemoryDebug': 'Przełącz panel debugowania pamięci', + 'commandPalette.item.pinSession': 'Przypnij lub odepnij sesję', + 'commandPalette.item.copySessionId': 'Kopiuj ID sesji', + 'commandPalette.item.openMultiRun': 'Otwórz panel multi-run', + 'commandPalette.item.openArchive': 'Otwórz zarchiwizowane sesje', + 'commandPalette.item.openNotes': 'Otwórz panel notatek', + 'commandPalette.item.openTodos': 'Otwórz panel zadań', 'commandPalette.session.untitled': 'Nienazwana sesja', 'commandPalette.title': 'Paleta poleceń', 'contextPanel.actions.closePanel': 'Zamknij panel', 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 6f5a4319..47ccf4f8 100644 --- a/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts +++ b/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts @@ -1115,6 +1115,10 @@ export const settingsDict = { "settings.openchamber.keyboardShortcuts.action.switch_context_surface.label": "Alternar superfície do painel de contexto", "settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix": " + 1…0", "settings.openchamber.keyboardShortcuts.action.new_chat.label": "Nova sessão", + "settings.openchamber.keyboardShortcuts.action.switch_session_previous.label": "Sessão anterior", + "settings.openchamber.keyboardShortcuts.action.switch_session_next.label": "Próxima sessão", + "settings.openchamber.keyboardShortcuts.action.rename_current_session.label": "Renomear sessão atual", + "settings.openchamber.keyboardShortcuts.action.toggle_permission_auto_accept.label": "Alternar aprovação automática", "settings.openchamber.keyboardShortcuts.action.close_session_tab.label": "Fechar aba da sessão", "settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label": "Novo rascunho de worktree", "settings.openchamber.keyboardShortcuts.action.new_mini_chat.label": "Nova janela Mini Chat", diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.ts b/packages/ui/src/lib/i18n/messages/pt-BR.ts index cdc7c27c..4da1628e 100644 --- a/packages/ui/src/lib/i18n/messages/pt-BR.ts +++ b/packages/ui/src/lib/i18n/messages/pt-BR.ts @@ -2448,6 +2448,12 @@ export const dict: Record = { "commandPalette.item.cycleTheme": "Alternar tema", "commandPalette.item.showOpenCodeStatus": "Mostrar status do OpenCode", "commandPalette.item.toggleMemoryDebug": "Alternar painel de depuração de memória", + "commandPalette.item.pinSession": "Fixar ou desafixar sessão", + "commandPalette.item.copySessionId": "Copiar ID da sessão", + "commandPalette.item.openMultiRun": "Abrir lançador multi-run", + "commandPalette.item.openArchive": "Abrir sessões arquivadas", + "commandPalette.item.openNotes": "Abrir painel de notas", + "commandPalette.item.openTodos": "Abrir painel de tarefas", "commandPalette.item.openSettings": "Abrir configurações...", "commandPalette.session.untitled": "Sessão sem título", "openCodeStatusDialog.title": "Status do OpenCode", diff --git a/packages/ui/src/lib/i18n/messages/uk.settings.ts b/packages/ui/src/lib/i18n/messages/uk.settings.ts index 0af768cb..3e1ac7a9 100644 --- a/packages/ui/src/lib/i18n/messages/uk.settings.ts +++ b/packages/ui/src/lib/i18n/messages/uk.settings.ts @@ -1115,6 +1115,10 @@ export const settingsDict = { "settings.openchamber.keyboardShortcuts.action.switch_context_surface.label": "Перемкнути поверхню панелі контексту", "settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix": " + 1…0", "settings.openchamber.keyboardShortcuts.action.new_chat.label": "Нова сесія", + "settings.openchamber.keyboardShortcuts.action.switch_session_previous.label": "Попередня сесія", + "settings.openchamber.keyboardShortcuts.action.switch_session_next.label": "Наступна сесія", + "settings.openchamber.keyboardShortcuts.action.rename_current_session.label": "Перейменувати поточну сесію", + "settings.openchamber.keyboardShortcuts.action.toggle_permission_auto_accept.label": "Перемкнути авто-дозволи", "settings.openchamber.keyboardShortcuts.action.close_session_tab.label": "Закрити вкладку сесії", "settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label": "Нова чернетка worktree", "settings.openchamber.keyboardShortcuts.action.new_mini_chat.label": "Нове вікно Mini Chat", diff --git a/packages/ui/src/lib/i18n/messages/uk.ts b/packages/ui/src/lib/i18n/messages/uk.ts index 8d17da5a..f7c6adcd 100644 --- a/packages/ui/src/lib/i18n/messages/uk.ts +++ b/packages/ui/src/lib/i18n/messages/uk.ts @@ -2448,6 +2448,12 @@ export const dict: Record = { "commandPalette.item.cycleTheme": "Перемкнути тему", "commandPalette.item.showOpenCodeStatus": "Показати статус OpenCode", "commandPalette.item.toggleMemoryDebug": "Показати/сховати панель memory debug", + "commandPalette.item.pinSession": "Прикріпити або відкріпити сесію", + "commandPalette.item.copySessionId": "Скопіювати ID сесії", + "commandPalette.item.openMultiRun": "Відкрити лаунчер multi-run", + "commandPalette.item.openArchive": "Відкрити архівовані сесії", + "commandPalette.item.openNotes": "Відкрити панель нотаток", + "commandPalette.item.openTodos": "Відкрити панель завдань", "commandPalette.item.openSettings": "Відкрити налаштування...", "commandPalette.session.untitled": "Сесія без назви", "openCodeStatusDialog.title": "Статус OpenCode", 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 f898b951..8bebdd26 100644 --- a/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts +++ b/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts @@ -1115,6 +1115,10 @@ export const settingsDict = { 'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': '切换上下文面板界面', 'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0', 'settings.openchamber.keyboardShortcuts.action.new_chat.label': '新建会话', + 'settings.openchamber.keyboardShortcuts.action.switch_session_previous.label': '上一个会话', + 'settings.openchamber.keyboardShortcuts.action.switch_session_next.label': '下一个会话', + 'settings.openchamber.keyboardShortcuts.action.rename_current_session.label': '重命名当前会话', + 'settings.openchamber.keyboardShortcuts.action.toggle_permission_auto_accept.label': '切换权限自动批准', 'settings.openchamber.keyboardShortcuts.action.close_session_tab.label': '关闭会话标签页', 'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': '新建工作树草稿', 'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': '新建 Mini Chat 窗口', diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.ts b/packages/ui/src/lib/i18n/messages/zh-CN.ts index 3e0d8295..267b2ab7 100644 --- a/packages/ui/src/lib/i18n/messages/zh-CN.ts +++ b/packages/ui/src/lib/i18n/messages/zh-CN.ts @@ -2448,6 +2448,12 @@ export const dict: Record = { 'commandPalette.item.cycleTheme': '轮换主题', 'commandPalette.item.showOpenCodeStatus': '显示 OpenCode 状态', 'commandPalette.item.toggleMemoryDebug': '切换内存调试面板', + 'commandPalette.item.pinSession': '固定或取消固定会话', + 'commandPalette.item.copySessionId': '复制会话 ID', + 'commandPalette.item.openMultiRun': '打开多任务启动器', + 'commandPalette.item.openArchive': '打开已归档会话', + 'commandPalette.item.openNotes': '打开笔记面板', + 'commandPalette.item.openTodos': '打开待办面板', 'commandPalette.item.openSettings': '打开设置...', 'commandPalette.session.untitled': '未命名会话', 'openCodeStatusDialog.title': 'OpenCode 状态', 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 fe7d3153..c38af529 100644 --- a/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts +++ b/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts @@ -1022,6 +1022,10 @@ export const settingsDict = { 'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': '切換上下文面板介面', 'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0', 'settings.openchamber.keyboardShortcuts.action.new_chat.label': '新建工作階段', + 'settings.openchamber.keyboardShortcuts.action.switch_session_previous.label': '上一個工作階段', + 'settings.openchamber.keyboardShortcuts.action.switch_session_next.label': '下一個工作階段', + 'settings.openchamber.keyboardShortcuts.action.rename_current_session.label': '重新命名目前的工作階段', + 'settings.openchamber.keyboardShortcuts.action.toggle_permission_auto_accept.label': '切換權限自動核准', 'settings.openchamber.keyboardShortcuts.action.close_session_tab.label': '關閉工作階段分頁', 'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': '新增 worktree 草稿', 'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': '新建 Mini Chat 視窗', diff --git a/packages/ui/src/lib/i18n/messages/zh-TW.ts b/packages/ui/src/lib/i18n/messages/zh-TW.ts index 52468dab..f392a509 100644 --- a/packages/ui/src/lib/i18n/messages/zh-TW.ts +++ b/packages/ui/src/lib/i18n/messages/zh-TW.ts @@ -2452,6 +2452,12 @@ export const dict: Record = { 'commandPalette.item.cycleTheme': '輪換主題', 'commandPalette.item.showOpenCodeStatus': '顯示 OpenCode 狀態', 'commandPalette.item.toggleMemoryDebug': '切換記憶體偵錯面板', + 'commandPalette.item.pinSession': '釘選或取消釘選會話', + 'commandPalette.item.copySessionId': '複製會話 ID', + 'commandPalette.item.openMultiRun': '開啟多任務啟動器', + 'commandPalette.item.openArchive': '開啟已封存會話', + 'commandPalette.item.openNotes': '開啟筆記面板', + 'commandPalette.item.openTodos': '開啟待辦面板', 'commandPalette.item.openSettings': '開啟設定...', 'commandPalette.session.untitled': '未命名會話', 'openCodeStatusDialog.title': 'OpenCode 狀態', diff --git a/packages/ui/src/lib/sessionNavigationHistory.test.ts b/packages/ui/src/lib/sessionNavigationHistory.test.ts new file mode 100644 index 00000000..b9be6857 --- /dev/null +++ b/packages/ui/src/lib/sessionNavigationHistory.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, test } from 'bun:test'; +import type { Session } from '@opencode-ai/sdk/v2'; +import { navigateSessionHistory } from './sessionNavigationHistory'; +import { useSessionUIStore } from '@/sync/session-ui-store'; +import { useGlobalSessionsStore } from '@/stores/useGlobalSessionsStore'; + +// SAFETY: the history module only reads a session's id and directory metadata. +const session = (id: string): Session => ({ + id, + title: id, + directory: '/repo', + projectID: 'p1', + version: '1', + time: { created: 1, updated: 1 }, +} as Session); + +describe('sessionNavigationHistory', () => { + test('steps back and forward through the visit order', () => { + useGlobalSessionsStore.setState({ activeSessions: [session('s1'), session('s2'), session('s3')] }); + + useSessionUIStore.setState({ currentSessionId: 's1' }); + useSessionUIStore.setState({ currentSessionId: 's2' }); + useSessionUIStore.setState({ currentSessionId: 's3' }); + + expect(navigateSessionHistory(-1)).toBe(true); + expect(useSessionUIStore.getState().currentSessionId).toBe('s2'); + expect(navigateSessionHistory(-1)).toBe(true); + expect(useSessionUIStore.getState().currentSessionId).toBe('s1'); + expect(navigateSessionHistory(-1)).toBe(false); + + expect(navigateSessionHistory(1)).toBe(true); + expect(useSessionUIStore.getState().currentSessionId).toBe('s2'); + }); + + test('a fresh visit truncates the forward branch', () => { + // Continues from the previous test's state: at s2 with s3 forward. + useSessionUIStore.setState({ currentSessionId: 's1' }); + expect(navigateSessionHistory(1)).toBe(false); + expect(navigateSessionHistory(-1)).toBe(true); + expect(useSessionUIStore.getState().currentSessionId).toBe('s2'); + }); + + test('skips and drops entries whose session no longer exists', () => { + useSessionUIStore.setState({ currentSessionId: 's3' }); + useGlobalSessionsStore.setState({ activeSessions: [session('s1'), session('s3')] }); + // History behind s3 contains s2 (dead) then s1 (alive). + expect(navigateSessionHistory(-1)).toBe(true); + expect(useSessionUIStore.getState().currentSessionId).toBe('s1'); + }); +}); diff --git a/packages/ui/src/lib/sessionNavigationHistory.ts b/packages/ui/src/lib/sessionNavigationHistory.ts new file mode 100644 index 00000000..6f4edf9a --- /dev/null +++ b/packages/ui/src/lib/sessionNavigationHistory.ts @@ -0,0 +1,61 @@ +import { useSessionUIStore } from '@/sync/session-ui-store'; +import { useGlobalSessionsStore, resolveGlobalSessionDirectory } from '@/stores/useGlobalSessionsStore'; + +// Browser-style back/forward over the order sessions were opened in this +// window. A normal session switch truncates the forward part and appends; +// stepping through history moves only the cursor, so back stays back even +// after several presses. In-memory by design: the stack describes this +// window's journey, not durable state. + +const MAX_HISTORY = 100; + +let visitedSessionIds: string[] = []; +let cursor = -1; +let navigating = false; + +const recordVisit = (sessionId: string): void => { + if (visitedSessionIds[cursor] === sessionId) return; + visitedSessionIds = [...visitedSessionIds.slice(0, cursor + 1), sessionId].slice(-MAX_HISTORY); + cursor = visitedSessionIds.length - 1; +}; + +useSessionUIStore.subscribe((state, previousState) => { + if (state.currentSessionId === previousState.currentSessionId) return; + if (!state.currentSessionId || navigating) return; + recordVisit(state.currentSessionId); +}); + +/** + * Steps the current session back (-1) or forward (+1) through this window's + * open history. Entries whose session no longer exists in the loaded list are + * skipped and dropped. Returns false when there is nowhere to go. + */ +export const navigateSessionHistory = (delta: -1 | 1): boolean => { + const sessionsById = new Map( + useGlobalSessionsStore.getState().activeSessions.map((session) => [session.id, session] as const), + ); + let nextCursor = cursor + delta; + while (nextCursor >= 0 && nextCursor < visitedSessionIds.length) { + const session = sessionsById.get(visitedSessionIds[nextCursor]); + if (session) { + cursor = nextCursor; + navigating = true; + try { + useSessionUIStore.getState().setCurrentSession(session.id, resolveGlobalSessionDirectory(session)); + } finally { + navigating = false; + } + return true; + } + // Drop the dead entry at nextCursor and keep scanning in the same + // direction: a removal shifts later entries one index down, so the next + // forward candidate lands on the same index while a backward scan steps. + visitedSessionIds = [ + ...visitedSessionIds.slice(0, nextCursor), + ...visitedSessionIds.slice(nextCursor + 1), + ]; + if (nextCursor < cursor) cursor -= 1; + if (delta < 0) nextCursor -= 1; + } + return false; +}; diff --git a/packages/ui/src/lib/sessionTabs.ts b/packages/ui/src/lib/sessionTabs.ts index a1b490b0..62d22bfe 100644 --- a/packages/ui/src/lib/sessionTabs.ts +++ b/packages/ui/src/lib/sessionTabs.ts @@ -26,6 +26,28 @@ export const activateSessionTabByIndex = (index: number): boolean => { return true; }; +/** + * Activate the tab one step right (+1) or left (-1) of the current session + * in the rendered strip order, wrapping around the ends. Returns false when + * the current session has no tab or there is nothing to move to. + */ +export const activateAdjacentSessionTab = (delta: -1 | 1): boolean => { + const { tabIds } = useSessionTabsStore.getState(); + const { currentSessionId, setCurrentSession } = useSessionUIStore.getState(); + const sessionsById = new Map( + useGlobalSessionsStore.getState().activeSessions.map((session) => [session.id, session] as const), + ); + const renderable = tabIds.filter((id) => sessionsById.has(id)); + if (!currentSessionId || renderable.length < 2) return false; + const index = renderable.indexOf(currentSessionId); + if (index === -1) return false; + const nextId = renderable[(index + delta + renderable.length) % renderable.length]; + const next = sessionsById.get(nextId); + if (!next) return false; + setCurrentSession(next.id, resolveGlobalSessionDirectory(next)); + return true; +}; + export const closeSessionTabAndActivateNeighbour = (sessionId: string): void => { const { tabIds, closeTab } = useSessionTabsStore.getState(); if (!tabIds.includes(sessionId)) return; diff --git a/packages/ui/src/lib/shortcuts/config.ts b/packages/ui/src/lib/shortcuts/config.ts index 0cdf024b..c9d8dbe3 100644 --- a/packages/ui/src/lib/shortcuts/config.ts +++ b/packages/ui/src/lib/shortcuts/config.ts @@ -51,6 +51,34 @@ const SHORTCUT_GROUPS = { customizable: true, settingsLabelKey: 'settings.openchamber.keyboardShortcuts.action.new_chat.label', }, + { + id: 'switch_session_previous', + defaultBinding: 'mod+alt+arrowleft', + customizable: true, + settingsLabelKey: + 'settings.openchamber.keyboardShortcuts.action.switch_session_previous.label', + }, + { + id: 'switch_session_next', + defaultBinding: 'mod+alt+arrowright', + customizable: true, + settingsLabelKey: + 'settings.openchamber.keyboardShortcuts.action.switch_session_next.label', + }, + { + id: 'rename_current_session', + defaultBinding: 'mod+k r', + customizable: true, + settingsLabelKey: + 'settings.openchamber.keyboardShortcuts.action.rename_current_session.label', + }, + { + id: 'toggle_permission_auto_accept', + defaultBinding: 'mod+k a', + customizable: true, + settingsLabelKey: + 'settings.openchamber.keyboardShortcuts.action.toggle_permission_auto_accept.label', + }, { id: 'close_session_tab', defaultBinding: 'alt+w', diff --git a/packages/vscode/CHANGELOG.md b/packages/vscode/CHANGELOG.md index d905260d..dbd27b32 100644 --- a/packages/vscode/CHANGELOG.md +++ b/packages/vscode/CHANGELOG.md @@ -8,6 +8,7 @@ - Chat: @ file mentions now rank files and directories together by how well they match, so the file you typed is at the top instead of below unrelated directories. Multi-word queries match in any order, and long paths keep the folder next to the file name visible. - Search: Ctrl/Cmd+P now matches the whole file path, not just the file name — searching a folder name finds the files inside it. - Search in dropdowns: searchable pickers (agents, models, providers, branches) now put the best matches first, match multi-word queries in any order, and ignore punctuation (so "gpt4o" finds "gpt-4o"). +- Permission cards answer to the keyboard: Alt+Enter allows once, Alt+Shift+Enter allows always, Alt+Backspace denies; the keys are printed on the buttons. - Keyboard: dropdown menus and pickers answer Ctrl+N/Ctrl+P for down/up, the session switcher opens focused on your current session, and shortcut labels in tooltips and menus show the binding you actually have set (thanks @ChangeHow). - Chat: OpenCode notices now share one style. - The timeline dialog now fits small windows instead of squeezing the message list to a couple of rows (thanks to @gaojunran).