Merge branch 'openchamber:main' into github-usage-rework

This commit is contained in:
Jakub Syty
2026-08-26 16:02:15 +02:00
committed by GitHub
43 changed files with 796 additions and 14 deletions
+5
View File
@@ -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. - **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 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. - **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. - 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. - 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. - 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.
@@ -77,6 +77,7 @@ import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory'; import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
import { usePermissionStore } from '@/stores/permissionStore'; import { usePermissionStore } from '@/stores/permissionStore';
import { togglePermissionAutoAccept } from './permissionAutoAccept'; import { togglePermissionAutoAccept } from './permissionAutoAccept';
import { useKeybind } from '@/hooks/useKeybind';
import { extractGitChangedFiles } from './changedFiles'; import { extractGitChangedFiles } from './changedFiles';
import { useI18n } from '@/lib/i18n'; import { useI18n } from '@/lib/i18n';
import { sessionEvents } from '@/lib/sessionEvents'; import { sessionEvents } from '@/lib/sessionEvents';
@@ -2562,6 +2563,11 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
t, t,
]); ]);
useKeybind('toggle_permission_auto_accept', () => {
if (!isPermissionAutoAcceptInteractive) return false;
handlePermissionAutoAcceptToggle();
});
React.useEffect(() => { React.useEffect(() => {
const pendingAbortBanner = Boolean(abortPromptSessionId) && abortPromptSessionId === currentSessionId; const pendingAbortBanner = Boolean(abortPromptSessionId) && abortPromptSessionId === currentSessionId;
if (!prevWasAbortedRef.current && pendingAbortBanner && !showAbortStatus) { if (!prevWasAbortedRef.current && pendingAbortBanner && !showAbortStatus) {
@@ -1534,6 +1534,54 @@ const MessageList = React.forwardRef<MessageListHandle, MessageListProps>(({
return true; return true;
}, [allEntries.length]); }, [allEntries.length]);
// A navigation scroll lands on estimates: an unmounted target teleports
// to its estimated offset, and even a mounted one drifts when neighbours
// finish measuring a frame later. This settle loop re-aligns the target to
// the requested viewport position until the layout stops moving, and backs
// off the moment the user touches the scroll.
const settleNavigationTarget = React.useCallback((
findElement: () => HTMLElement | null,
desiredOffsetTop: number,
) => {
const container = resolveScrollContainer();
if (!container || typeof window === 'undefined') {
return;
}
let frames = 0;
let stable = 0;
let cancelled = false;
const cancelOnUserInput = () => {
cancelled = true;
container.removeEventListener('touchstart', cancelOnUserInput);
container.removeEventListener('wheel', cancelOnUserInput);
};
container.addEventListener('touchstart', cancelOnUserInput, { passive: true });
container.addEventListener('wheel', cancelOnUserInput, { passive: true });
const step = () => {
if (cancelled) return;
const element = findElement();
if (element) {
const delta = element.getBoundingClientRect().top
- container.getBoundingClientRect().top
- desiredOffsetTop;
if (Math.abs(delta) > 0.5) {
container.scrollTop += delta;
stable = 0;
} else {
stable += 1;
}
}
frames += 1;
if (stable >= ANCHOR_HOLD_STABLE_FRAMES || frames >= ANCHOR_HOLD_MAX_FRAMES) {
container.removeEventListener('touchstart', cancelOnUserInput);
container.removeEventListener('wheel', cancelOnUserInput);
return;
}
window.requestAnimationFrame(step);
};
window.requestAnimationFrame(step);
}, [resolveScrollContainer]);
const scrollMessageElementIntoView = React.useCallback((messageId: string, behavior: ScrollBehavior = 'auto') => { const scrollMessageElementIntoView = React.useCallback((messageId: string, behavior: ScrollBehavior = 'auto') => {
const container = resolveScrollContainer(); const container = resolveScrollContainer();
if (!container) { if (!container) {
@@ -1569,14 +1617,19 @@ const MessageList = React.forwardRef<MessageListHandle, MessageListProps>(({
if (!container) { if (!container) {
return false; return false;
} }
const turnElement = container.querySelector<HTMLElement>(`[data-turn-id="${turnId}"]`); const findTurnElement = () => container.querySelector<HTMLElement>(`[data-turn-id="${turnId}"]`);
const turnElement = findTurnElement();
if (turnElement) { if (turnElement) {
turnElement.scrollIntoView({ behavior, block: 'start' }); turnElement.scrollIntoView({ behavior, block: 'start' });
if (behavior !== 'smooth') settleNavigationTarget(findTurnElement, 0);
return true; return true;
} }
if (!scrollHistoryIndexIntoView(index)) {
return scrollHistoryIndexIntoView(index); return false;
}
if (behavior !== 'smooth') settleNavigationTarget(findTurnElement, 0);
return true;
}, },
scrollToMessageId: (messageId: string, options?: { behavior?: ScrollBehavior }) => { scrollToMessageId: (messageId: string, options?: { behavior?: ScrollBehavior }) => {
@@ -1586,8 +1639,12 @@ const MessageList = React.forwardRef<MessageListHandle, MessageListProps>(({
return false; return false;
} }
return scrollMessageElementIntoView(messageId, behavior) const didScroll = scrollMessageElementIntoView(messageId, behavior)
|| scrollHistoryIndexIntoView(index); || scrollHistoryIndexIntoView(index);
if (didScroll && behavior !== 'smooth') {
settleNavigationTarget(() => findMessageElement(messageId), 50);
}
return didScroll;
}, },
holdViewportAnchor: (anchor) => { holdViewportAnchor: (anchor) => {
@@ -1730,7 +1787,7 @@ const MessageList = React.forwardRef<MessageListHandle, MessageListProps>(({
return () => { return () => {
objectRef.current = null; objectRef.current = null;
}; };
}, [findMessageElement, historyEntries.length, messageIndexMap, resolveScrollContainer, scrollHistoryIndexIntoView, scrollMessageElementIntoView, turnIndexMap, ref]); }, [findMessageElement, historyEntries.length, messageIndexMap, resolveScrollContainer, scrollHistoryIndexIntoView, scrollMessageElementIntoView, settleNavigationTarget, turnIndexMap, ref]);
const anchoredEndSpace = React.useMemo<TimelineAnchoredEndSpace | undefined>(() => { const anchoredEndSpace = React.useMemo<TimelineAnchoredEndSpace | undefined>(() => {
const resolved = resolveChatListAnchoredEndSpace( const resolved = resolveChatListAnchoredEndSpace(
@@ -10,6 +10,10 @@ import { Icon } from "@/components/icon/Icon";
import { DiffPreview, WritePreview } from './DiffPreview'; import { DiffPreview, WritePreview } from './DiffPreview';
import { useI18n } from '@/lib/i18n'; import { useI18n } from '@/lib/i18n';
import { getVisiblePermissionPatterns } from './permissionCardPatterns'; 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 = { const PERMISSION_BASH_CUSTOM_STYLE: React.CSSProperties = {
margin: 0, margin: 0,
@@ -126,6 +130,33 @@ export const PermissionCard: React.FC<PermissionCardProps> = ({
} }
}; };
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) { if (hasResponded) {
return null; return null;
} }
@@ -380,6 +411,7 @@ export const PermissionCard: React.FC<PermissionCardProps> = ({
> >
<Icon name="check" className="h-3.5 w-3.5 sm:h-3 sm:w-3 flex-shrink-0" /> <Icon name="check" className="h-3.5 w-3.5 sm:h-3 sm:w-3 flex-shrink-0" />
Allow Once Allow Once
<kbd className="ml-1 hidden sm:inline typography-micro opacity-60">{formatShortcutForDisplay('alt+enter')}</kbd>
</button> </button>
{permission.always.length > 0 ? ( {permission.always.length > 0 ? (
@@ -436,6 +468,7 @@ export const PermissionCard: React.FC<PermissionCardProps> = ({
> >
<Icon name="time" className="h-3.5 w-3.5 sm:h-3 sm:w-3 flex-shrink-0" /> <Icon name="time" className="h-3.5 w-3.5 sm:h-3 sm:w-3 flex-shrink-0" />
Always Allow Always Allow
<kbd className="ml-1 hidden sm:inline typography-micro opacity-60">{formatShortcutForDisplay('alt+shift+enter')}</kbd>
</button> </button>
)} )}
@@ -459,6 +492,7 @@ export const PermissionCard: React.FC<PermissionCardProps> = ({
> >
<Icon name="close" className="h-3.5 w-3.5 sm:h-3 sm:w-3 flex-shrink-0" /> <Icon name="close" className="h-3.5 w-3.5 sm:h-3 sm:w-3 flex-shrink-0" />
Deny Deny
<kbd className="ml-1 hidden sm:inline typography-micro opacity-60">{formatShortcutForDisplay('alt+backspace')}</kbd>
</button> </button>
{isResponding && ( {isResponding && (
@@ -36,6 +36,7 @@ import { cn } from '@/lib/utils';
import { useFeatureFlagsStore } from '@/stores/useFeatureFlagsStore'; import { useFeatureFlagsStore } from '@/stores/useFeatureFlagsStore';
import { useGitStatus } from '@/stores/useGitStore'; import { useGitStatus } from '@/stores/useGitStore';
import { normalizeContextPanelDirectoryKey, useUIStore } from '@/stores/useUIStore'; import { normalizeContextPanelDirectoryKey, useUIStore } from '@/stores/useUIStore';
import { ContextRailSurfacesDialog } from './ContextRailSurfacesDialog';
const RAIL_TOOLTIP_DELAY_MS = 150; const RAIL_TOOLTIP_DELAY_MS = 150;
// Hold the surface-switch modifier for this long before revealing the order // Hold the surface-switch modifier for this long before revealing the order
@@ -161,6 +162,7 @@ export const ContextPanelRail: React.FC = () => {
const panelState = useUIStore((state) => (directoryKey ? state.contextPanelByDirectory[directoryKey] : undefined)); const panelState = useUIStore((state) => (directoryKey ? state.contextPanelByDirectory[directoryKey] : undefined));
const workStatusPanelVisible = useUIStore((state) => state.workStatusPanelVisible); const workStatusPanelVisible = useUIStore((state) => state.workStatusPanelVisible);
const contextRailOrder = useUIStore((state) => state.contextRailOrder); const contextRailOrder = useUIStore((state) => state.contextRailOrder);
const contextRailHiddenSurfaces = useUIStore((state) => state.contextRailHiddenSurfaces);
const setContextRailOrder = useUIStore((state) => state.setContextRailOrder); const setContextRailOrder = useUIStore((state) => state.setContextRailOrder);
const openContextSurface = useUIStore((state) => state.openContextSurface); const openContextSurface = useUIStore((state) => state.openContextSurface);
const shortcutOverrides = useUIStore((state) => state.shortcutOverrides); const shortcutOverrides = useUIStore((state) => state.shortcutOverrides);
@@ -256,12 +258,15 @@ export const ContextPanelRail: React.FC = () => {
const surfaces = React.useMemo(() => { const surfaces = React.useMemo(() => {
return getVisibleContextRailSurfaces({ return getVisibleContextRailSurfaces({
railOrder: contextRailOrder, railOrder: contextRailOrder,
hiddenSurfaces: contextRailHiddenSurfaces,
planModeEnabled, planModeEnabled,
isVSCode: isVSCodeRuntime(), isVSCode: isVSCodeRuntime(),
screenWidth, screenWidth,
tabs, tabs,
}); });
}, [contextRailOrder, planModeEnabled, screenWidth, tabs]); }, [contextRailHiddenSurfaces, contextRailOrder, planModeEnabled, screenWidth, tabs]);
const [isSurfacesDialogOpen, setIsSurfacesDialogOpen] = React.useState(false);
const handleDragEnd = React.useCallback((event: DragEndEvent) => { const handleDragEnd = React.useCallback((event: DragEndEvent) => {
const { active, over } = event; const { active, over } = event;
@@ -331,6 +336,24 @@ export const ContextPanelRail: React.FC = () => {
})} })}
</SortableContext> </SortableContext>
</DndContext> </DndContext>
{/* Outside the sortable list on purpose: this button takes no digit,
cannot be dragged, and configures the rail rather than living on it. */}
<Tooltip delayDuration={RAIL_TOOLTIP_DELAY_MS}>
<TooltipTrigger asChild>
<button
type="button"
aria-label={t('contextRail.configure.open')}
onClick={() => setIsSurfacesDialogOpen(true)}
className="flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground/70 transition-colors hover:text-foreground"
>
<Icon name="equalizer-2" className="h-[18px] w-[18px]" />
</button>
</TooltipTrigger>
<TooltipContent side="left" sideOffset={8}>
{t('contextRail.configure.open')}
</TooltipContent>
</Tooltip>
<ContextRailSurfacesDialog open={isSurfacesDialogOpen} onOpenChange={setIsSurfacesDialogOpen} />
</nav> </nav>
); );
}; };
@@ -0,0 +1,78 @@
import React from 'react';
import { useI18n } from '@/lib/i18n';
import { useUIStore } from '@/stores/useUIStore';
import { SettingsCheckboxRow } from '@/components/sections/shared/SettingsSection';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { sortContextSurfaces } from '@/lib/surfaces/registry';
/**
* Which surfaces the context rail shows. Everything is on by default and the
* choice is stored as the *hidden* set, so a surface added in a later release
* appears for everyone rather than staying invisible to whoever had saved
* settings before it existed. Hidden surfaces also leave the digit shortcuts
* (the rail and the shortcut share one visibility filter).
*/
export const ContextRailSurfacesDialog: React.FC<{
open: boolean;
onOpenChange: (open: boolean) => void;
}> = ({ open, onOpenChange }) => {
const { t } = useI18n();
const contextRailOrder = useUIStore((state) => state.contextRailOrder);
const hidden = useUIStore((state) => state.contextRailHiddenSurfaces);
const setSurfaceVisible = useUIStore((state) => state.setContextRailSurfaceVisible);
const setHiddenSurfaces = useUIStore((state) => state.setContextRailHiddenSurfaces);
// The full registry in the user's rail order — including surfaces a runtime
// filter currently drops, so a choice made on desktop is editable anywhere.
const surfaces = React.useMemo(() => sortContextSurfaces(contextRailOrder), [contextRailOrder]);
const allVisible = hidden.length === 0;
const noneVisible = surfaces.every((surface) => hidden.includes(surface.id));
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>{t('contextRail.configure.dialogTitle')}</DialogTitle>
<DialogDescription>{t('contextRail.configure.dialogDescription')}</DialogDescription>
</DialogHeader>
<div className="flex flex-col">
{surfaces.map((surface) => (
<SettingsCheckboxRow
key={surface.id}
settingsItem={`layout.context-rail.surface.${surface.id}`}
checked={!hidden.includes(surface.id)}
onChange={(checked) => setSurfaceVisible(surface.id, checked)}
label={t(surface.labelKey)}
ariaLabel={t(surface.labelKey)}
/>
))}
</div>
{!allVisible ? (
<div className="flex items-center justify-between border-t pt-3">
{noneVisible ? (
<span className="text-xs text-destructive">{t('contextRail.configure.noneWarning')}</span>
) : <span />}
<Button
variant="link"
size="xs"
onClick={() => setHiddenSurfaces([])}
className="normal-case text-muted-foreground hover:text-foreground"
>
{t('contextRail.configure.showAll')}
</Button>
</div>
) : null}
</DialogContent>
</Dialog>
);
};
@@ -1463,6 +1463,10 @@ export const Header: React.FC = () => {
useKeybinds({ useKeybinds({
rename_current_session: () => {
if (!currentSessionId || isMobile) return false;
beginHeaderSessionRename();
},
toggle_services_menu: () => { toggle_services_menu: () => {
if (isDesktopServicesOpen) { if (isDesktopServicesOpen) {
setIsDesktopServicesOpen(false); setIsDesktopServicesOpen(false);
@@ -50,6 +50,7 @@ import { scoreByFuzzyQuery } from '@/lib/search/fuzzySearch';
import { truncatePathMiddle } from '@/lib/utils'; import { truncatePathMiddle } from '@/lib/utils';
import { useI18n } from '@/lib/i18n'; import { useI18n } from '@/lib/i18n';
import { sessionEvents } from '@/lib/sessionEvents'; import { sessionEvents } from '@/lib/sessionEvents';
import { copyTextToClipboard } from '@/lib/clipboard';
import { useProjectsStore } from '@/stores/useProjectsStore'; import { useProjectsStore } from '@/stores/useProjectsStore';
import { buildCommandPaletteFileSearchKey, scoreCommandPaletteFiles } from './commandPaletteFilesState'; import { buildCommandPaletteFileSearchKey, scoreCommandPaletteFiles } from './commandPaletteFilesState';
@@ -59,6 +60,9 @@ type CommandEntry = {
icon: React.ReactNode; icon: React.ReactNode;
shortcutId?: string; shortcutId?: string;
searchText: 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; onSelect: () => void;
}; };
@@ -90,9 +94,14 @@ export const CommandPalette: React.FC = () => {
const openContextSurface = useUIStore((s) => s.openContextSurface); const openContextSurface = useUIStore((s) => s.openContextSurface);
const openContextFile = useUIStore((s) => s.openContextFile); const openContextFile = useUIStore((s) => s.openContextFile);
const shortcutOverrides = useUIStore((s) => s.shortcutOverrides); 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 openNewSessionDraft = useSessionUIStore((s) => s.openNewSessionDraft);
const setCurrentSession = useSessionUIStore((s) => s.setCurrentSession); const setCurrentSession = useSessionUIStore((s) => s.setCurrentSession);
const currentSessionId = useSessionUIStore((s) => s.currentSessionId);
const togglePinnedSession = useSessionPinnedStore((s) => s.toggle);
const activeSessions = useGlobalSessionsStore(React.useCallback( const activeSessions = useGlobalSessionsStore(React.useCallback(
(state) => isCommandPaletteOpen ? state.activeSessions : EMPTY_SESSIONS, (state) => isCommandPaletteOpen ? state.activeSessions : EMPTY_SESSIONS,
@@ -233,6 +242,7 @@ export const CommandPalette: React.FC = () => {
}, },
{ {
id: 'cycle-theme', id: 'cycle-theme',
secondary: true,
title: t('commandPalette.item.cycleTheme'), title: t('commandPalette.item.cycleTheme'),
icon: <Icon name="palette" className="mr-2 h-4 w-4" />, icon: <Icon name="palette" className="mr-2 h-4 w-4" />,
shortcutId: 'cycle_theme', shortcutId: 'cycle_theme',
@@ -243,6 +253,7 @@ export const CommandPalette: React.FC = () => {
}, },
{ {
id: 'open-status', id: 'open-status',
secondary: true,
title: t('commandPalette.item.showOpenCodeStatus'), title: t('commandPalette.item.showOpenCodeStatus'),
icon: <Icon name="pulse" className="mr-2 h-4 w-4" />, icon: <Icon name="pulse" className="mr-2 h-4 w-4" />,
searchText: t('commandPalette.item.showOpenCodeStatus'), searchText: t('commandPalette.item.showOpenCodeStatus'),
@@ -259,8 +270,90 @@ export const CommandPalette: React.FC = () => {
onSelect: run(() => setSettingsDialogOpen(true)), onSelect: run(() => setSettingsDialogOpen(true)),
}, },
]; ];
list.push(
{
id: 'pin-session',
secondary: true,
title: t('commandPalette.item.pinSession'),
icon: <Icon name="pushpin" className="mr-2 h-4 w-4" />,
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: <Icon name="file-copy" className="mr-2 h-4 w-4" />,
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: <Icon name="checkbox-multiple" className="mr-2 h-4 w-4" />,
searchText: t('commandPalette.item.openMultiRun'),
onSelect: run(() => {
setSessionSwitcherOpen(false);
openMultiRunLauncher();
}),
},
{
id: 'open-archive',
secondary: true,
title: t('commandPalette.item.openArchive'),
icon: <Icon name="archive" className="mr-2 h-4 w-4" />,
searchText: t('commandPalette.item.openArchive'),
onSelect: run(() => {
setSessionSwitcherOpen(false);
setArchivePageOpen(true);
}),
},
{
id: 'open-notes',
secondary: true,
title: t('commandPalette.item.openNotes'),
icon: <Icon name="sticky-note" className="mr-2 h-4 w-4" />,
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: <Icon name="checkbox-circle" className="mr-2 h-4 w-4" />,
searchText: t('commandPalette.item.openTodos'),
onSelect: run(() => {
if (currentDirectory) {
setProjectContextTab('todos');
openContextSurface(currentDirectory, 'notes');
}
}),
},
);
list.push({ list.push({
id: 'toggle-memory-debug', id: 'toggle-memory-debug',
secondary: true,
title: t('commandPalette.item.toggleMemoryDebug'), title: t('commandPalette.item.toggleMemoryDebug'),
icon: <Icon name="bug" className="mr-2 h-4 w-4" />, icon: <Icon name="bug" className="mr-2 h-4 w-4" />,
searchText: t('commandPalette.item.toggleMemoryDebug'), searchText: t('commandPalette.item.toggleMemoryDebug'),
@@ -299,6 +392,11 @@ export const CommandPalette: React.FC = () => {
setSettingsDialogOpen, setSettingsDialogOpen,
activeProject?.id, activeProject?.id,
activeProject?.path, activeProject?.path,
currentSessionId,
togglePinnedSession,
openMultiRunLauncher,
setArchivePageOpen,
setProjectContextTab,
]); ]);
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -407,7 +505,9 @@ export const CommandPalette: React.FC = () => {
const hasQuery = liveTrimmed.length > 0; const hasQuery = liveTrimmed.length > 0;
const scoredCommands = React.useMemo(() => { 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, { return scoreByFuzzyQuery(commands, liveTrimmed, (c) => c.searchText, {
limit: 7, limit: 7,
noFuzzy: true, noFuzzy: true,
@@ -1768,6 +1768,37 @@ export const DiffView: React.FC<DiffViewProps> = ({
scrollToFile(value); scrollToFile(value);
}, [cancelPendingScrollAlignment, expandStackedFile, scrollToFile]); }, [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 handleHeaderLayoutChange = React.useCallback((mode: DiffViewMode) => {
const nextLayout: 'inline' | 'side-by-side' = const nextLayout: 'inline' | 'side-by-side' =
mode === 'side-by-side' ? 'side-by-side' : 'inline'; mode === 'side-by-side' ? 'side-by-side' : 'inline';
@@ -6,6 +6,7 @@ import { useI18n } from '@/lib/i18n';
interface CommitInputProps { interface CommitInputProps {
value: string; value: string;
onChange: (value: string) => void; onChange: (value: string) => void;
onSubmit?: () => void;
placeholder?: string; placeholder?: string;
disabled?: boolean; disabled?: boolean;
hasTouchInput?: boolean; hasTouchInput?: boolean;
@@ -18,6 +19,7 @@ const MAX_HEIGHT = 200;
export const CommitInput: React.FC<CommitInputProps> = ({ export const CommitInput: React.FC<CommitInputProps> = ({
value, value,
onChange, onChange,
onSubmit,
placeholder, placeholder,
disabled = false, disabled = false,
hasTouchInput = false, hasTouchInput = false,
@@ -58,6 +60,12 @@ export const CommitInput: React.FC<CommitInputProps> = ({
ref={textareaRef} ref={textareaRef}
value={value} value={value}
onChange={(e) => onChange(e.target.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')} placeholder={placeholder ?? t('gitView.commit.messagePlaceholder')}
rows={1} rows={1}
disabled={disabled} disabled={disabled}
@@ -68,6 +68,9 @@ export const CommitSection: React.FC<CommitSectionProps> = ({
<CommitInput <CommitInput
value={commitMessage} value={commitMessage}
onChange={onCommitMessageChange} onChange={onCommitMessageChange}
onSubmit={() => {
if (canCommit && !isGeneratingMessage) onCommit();
}}
placeholder={t('gitView.commit.messagePlaceholder')} placeholder={t('gitView.commit.messagePlaceholder')}
disabled={commitAction !== null} disabled={commitAction !== null}
hasTouchInput={hasTouchInput} hasTouchInput={hasTouchInput}
+65 -5
View File
@@ -7,7 +7,9 @@ import { useUIStore } from '@/stores/useUIStore';
import { import {
CHAT_LIST_ANCHOR_OFFSET, CHAT_LIST_ANCHOR_OFFSET,
getAnchoredTurnMetrics, getAnchoredTurnMetrics,
getRowBottom,
resolveTimelineIsAtEnd, resolveTimelineIsAtEnd,
TIMELINE_FOLLOW_REARM_THRESHOLD_PX,
type TimelineListMeasurementState, type TimelineListMeasurementState,
type TimelineScrollMode, type TimelineScrollMode,
} from '@/components/chat/lib/scroll/timelineScrollAnchoring'; } from '@/components/chat/lib/scroll/timelineScrollAnchoring';
@@ -129,6 +131,8 @@ export const useChatTimelineScroll = ({
// True after a real gesture until an explicit opt back in; drives the // True after a real gesture until an explicit opt back in; drives the
// overlay scrollbar suppression instead of the anchor's mere existence. // overlay scrollbar suppression instead of the anchor's mere existence.
const [userOwnsScroll, setUserOwnsScroll] = React.useState(false); const [userOwnsScroll, setUserOwnsScroll] = React.useState(false);
const userOwnsScrollRef = React.useRef(userOwnsScroll);
userOwnsScrollRef.current = userOwnsScroll;
const modeRef = React.useRef<TimelineScrollMode>('following-end'); const modeRef = React.useRef<TimelineScrollMode>('following-end');
const isAtEndRef = React.useRef(true); const isAtEndRef = React.useRef(true);
@@ -547,9 +551,12 @@ export const useChatTimelineScroll = ({
// per-frame row re-measure and the pinned viewport shakes. Corrections // per-frame row re-measure and the pinned viewport shakes. Corrections
// stand down for the whole resize and the visible content is held by the // stand down for the whole resize and the visible content is held by the
// list's size compensation instead. Deliberately NO snap back to the end // list's size compensation instead. Deliberately NO snap back to the end
// afterwards: a slow drag settles repeatedly, and each snap reads as the // afterwards for a mid-conversation reader: a slow drag settles
// very jump this suspension removes — geometry changed, staying where the // repeatedly, and each snap reads as the very jump this suspension
// reader is beats re-asserting the edge. // removes. A reader who WAS at the end is the exception — after rows
// re-wrap, stale cached sizes can leave a large phantom gap below the
// last row, so re-asserting the end once on settle is what "staying
// where the reader is" means for them.
const widthResizingRef = React.useRef(false); const widthResizingRef = React.useRef(false);
React.useEffect(() => { React.useEffect(() => {
if (!scrollNode || typeof ResizeObserver === 'undefined') return; if (!scrollNode || typeof ResizeObserver === 'undefined') return;
@@ -569,6 +576,9 @@ export const useChatTimelineScroll = ({
quietTimer = setTimeout(() => { quietTimer = setTimeout(() => {
quietTimer = null; quietTimer = null;
widthResizingRef.current = false; widthResizingRef.current = false;
if (isAtEndRef.current && pendingAnchorRef.current === null) {
void listRef.current?.scrollToEnd({ animated: false });
}
}, 350); }, 350);
}); });
observer.observe(scrollNode); observer.observe(scrollNode);
@@ -580,7 +590,57 @@ export const useChatTimelineScroll = ({
const onTimelineDataChange = React.useCallback(() => { const onTimelineDataChange = React.useCallback(() => {
if (widthResizingRef.current) return; if (widthResizingRef.current) return;
if (!streamingAutoFollowEnabledRef.current) return;
// Stranded-viewport rescue, independent of any follow mode or
// preference: when off-screen size estimates settle smaller than
// estimated, the measured content can end ABOVE the viewport while
// the scroll offset stays at the stale end — the reader faces a blank
// phantom tail with every row out of reach above. That state is never
// intentional, so it is corrected even when auto-follow is off. Only
// a fully blank viewport qualifies; partial visibility is left alone.
if (!userOwnsScrollRef.current) {
const list = listRef.current;
if (list) {
const state = list.getState();
const lastIndex = state.data.length - 1;
const lastBottom = lastIndex >= 0 ? getRowBottom(state, lastIndex) : null;
if (lastBottom !== null && state.scroll > lastBottom) {
const visibleLength = Math.max(
0,
state.scrollLength - composerOverlayHeightRef.current - CHAT_LIST_ANCHOR_OFFSET,
);
void list.scrollToOffset({
offset: Math.max(0, lastBottom - visibleLength),
animated: false,
});
return;
}
}
}
if (!streamingAutoFollowEnabledRef.current) {
// With auto-follow off nothing moves the viewport, so a growing
// reply slides below the visible area without a single scroll
// event — and the at-end transition that offers the pill never
// fires. Content growth is the signal here: once the real last
// row extends past what the composer leaves visible, the reader
// is factually behind and the pill must say so.
const list = listRef.current;
if (list && isAtEndRef.current) {
const state = list.getState();
const lastIndex = state.data.length - 1;
const lastBottom = lastIndex >= 0 ? getRowBottom(state, lastIndex) : null;
if (lastBottom !== null) {
const visibleBottom = state.scroll + state.scrollLength - composerOverlayHeightRef.current;
if (lastBottom - visibleBottom > TIMELINE_FOLLOW_REARM_THRESHOLD_PX) {
isAtEndRef.current = false;
setIsPinned(false);
scheduleShowScrollButton();
}
}
}
return;
}
if (!isLiveFollowActive()) return; if (!isLiveFollowActive()) return;
// Since @legendapp/list 3.3.x, maintainScrollAtEnd follows content // Since @legendapp/list 3.3.x, maintainScrollAtEnd follows content
@@ -637,7 +697,7 @@ export const useChatTimelineScroll = ({
}); });
}); });
}, [isLiveFollowActive]); }, [isLiveFollowActive, scheduleShowScrollButton]);
// The streaming tail grows inside one row without changing the entries // The streaming tail grows inside one row without changing the entries
// array, so data-change callbacks are silent for the entire stream. The // array, so data-change callbacks are silent for the entire stream. The
+11 -1
View File
@@ -1,7 +1,8 @@
import React from 'react'; import React from 'react';
import { isTerminalEventTarget } from '@/lib/terminalFocus'; import { isTerminalEventTarget } from '@/lib/terminalFocus';
import { useSessionUIStore } from '@/sync/session-ui-store'; 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 { useSelectionStore } from '@/sync/selection-store';
import * as sessionActions from '@/sync/session-actions'; import * as sessionActions from '@/sync/session-actions';
import { normalizeContextPanelDirectoryKey, useUIStore } from '@/stores/useUIStore'; 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); 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: () => { close_session_tab: () => {
if (isVSCodeRuntime() || !useUIStore.getState().sessionTabsEnabled) return false; if (isVSCodeRuntime() || !useUIStore.getState().sessionTabsEnabled) return false;
if (currentSessionId) { if (currentSessionId) {
@@ -490,6 +499,7 @@ export const useKeyboardShortcuts = () => {
const panel = state.contextPanelByDirectory[directory]; const panel = state.contextPanelByDirectory[directory];
const visibleSurfaces = getVisibleContextRailSurfaces({ const visibleSurfaces = getVisibleContextRailSurfaces({
railOrder: state.contextRailOrder, railOrder: state.contextRailOrder,
hiddenSurfaces: state.contextRailHiddenSurfaces,
planModeEnabled: useFeatureFlagsStore.getState().planModeEnabled, planModeEnabled: useFeatureFlagsStore.getState().planModeEnabled,
isVSCode: isVSCodeRuntime(), isVSCode: isVSCodeRuntime(),
screenWidth: window.innerWidth, screenWidth: window.innerWidth,
@@ -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.label': 'Kontextpanel-Oberfläche wechseln',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0', 'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0',
'settings.openchamber.keyboardShortcuts.action.new_chat.label': 'Neue Sitzung', '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.close_session_tab.label': 'Sitzungs-Tab schließen',
'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': 'Neuer Worktree-Entwurf', 'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': 'Neuer Worktree-Entwurf',
'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': 'Neues Mini-Chat-Fenster', 'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': 'Neues Mini-Chat-Fenster',
+11
View File
@@ -2292,6 +2292,12 @@ export const dict = {
'commandPalette.item.cycleTheme': 'Thema wechseln', 'commandPalette.item.cycleTheme': 'Thema wechseln',
'commandPalette.item.showOpenCodeStatus': 'OpenCode-Status anzeigen', 'commandPalette.item.showOpenCodeStatus': 'OpenCode-Status anzeigen',
'commandPalette.item.toggleMemoryDebug': 'Memory-Debug-Panel umschalten', '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.item.openSettings': 'Einstellungen öffnen...',
'commandPalette.session.untitled': 'Unbenannte Sitzung', 'commandPalette.session.untitled': 'Unbenannte Sitzung',
'openCodeStatusDialog.title': 'OpenCode-Status', 'openCodeStatusDialog.title': 'OpenCode-Status',
@@ -2989,6 +2995,11 @@ export const dict = {
'gitView.pr.segment.comments': 'Kommentare', 'gitView.pr.segment.comments': 'Kommentare',
'gitView.pr.comments.addAll': 'Alle hinzufügen', 'gitView.pr.comments.addAll': 'Alle hinzufügen',
'contextPanel.mode.pr': 'PR', 'contextPanel.mode.pr': 'PR',
'contextRail.configure.open': 'Panels konfigurieren',
'contextRail.configure.dialogTitle': 'Leisten-Panels',
'contextRail.configure.dialogDescription': 'Wähle, welche Panels die Leiste zeigt. Ausgeblendete Panels behalten ihre Daten und bleiben über die Befehlspalette erreichbar.',
'contextRail.configure.showAll': 'Alle anzeigen',
'contextRail.configure.noneWarning': 'Alle Panels sind ausgeblendet.',
'contextRail.aria.rail': 'Kontextleiste', 'contextRail.aria.rail': 'Kontextleiste',
'contextPanel.editorEmpty.title': 'Kein Kontext ausgewählt', 'contextPanel.editorEmpty.title': 'Kein Kontext ausgewählt',
'contextPanel.editorEmpty.description': 'Wählen Sie etwas aus der Seitenleiste aus, um Kontext anzuzeigen.', 'contextPanel.editorEmpty.description': 'Wählen Sie etwas aus der Seitenleiste aus, um Kontext anzuzeigen.',
@@ -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.label': 'Switch context panel surface',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0', 'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0',
'settings.openchamber.keyboardShortcuts.action.new_chat.label': 'New session', '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.close_session_tab.label': 'Close session tab',
'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': 'New worktree draft', 'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': 'New worktree draft',
'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': 'New Mini Chat window', 'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': 'New Mini Chat window',
+11
View File
@@ -1139,6 +1139,11 @@ export const dict = {
'contextPanel.mode.context': 'Context', 'contextPanel.mode.context': 'Context',
'contextPanel.mode.preview': 'Preview', 'contextPanel.mode.preview': 'Preview',
'contextPanel.mode.browser': 'Browser', 'contextPanel.mode.browser': 'Browser',
'contextRail.configure.open': 'Configure panels',
'contextRail.configure.dialogTitle': 'Rail panels',
'contextRail.configure.dialogDescription': 'Choose which panels the rail shows. Hidden panels keep their data and stay reachable from the command palette.',
'contextRail.configure.showAll': 'Show all',
'contextRail.configure.noneWarning': 'All panels are hidden.',
'contextRail.aria.rail': 'Panel surfaces', 'contextRail.aria.rail': 'Panel surfaces',
'contextPanel.editorEmpty.title': 'No file open', 'contextPanel.editorEmpty.title': 'No file open',
'contextPanel.editorEmpty.description': 'Pick a file from the tree to start editing.', 'contextPanel.editorEmpty.description': 'Pick a file from the tree to start editing.',
@@ -2482,6 +2487,12 @@ export const dict = {
'commandPalette.item.cycleTheme': 'Cycle theme', 'commandPalette.item.cycleTheme': 'Cycle theme',
'commandPalette.item.showOpenCodeStatus': 'Show OpenCode status', 'commandPalette.item.showOpenCodeStatus': 'Show OpenCode status',
'commandPalette.item.toggleMemoryDebug': 'Toggle memory debug panel', '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.item.openSettings': 'Open Settings...',
'commandPalette.session.untitled': 'Untitled Session', 'commandPalette.session.untitled': 'Untitled Session',
'openCodeStatusDialog.title': 'OpenCode Status', 'openCodeStatusDialog.title': 'OpenCode Status',
@@ -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.label": "Cambiar superficie del panel de contexto",
"settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix": " + 1…0", "settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix": " + 1…0",
"settings.openchamber.keyboardShortcuts.action.new_chat.label": "Nueva sesión", "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.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_chat_worktree.label": "Nuevo borrador de worktree",
"settings.openchamber.keyboardShortcuts.action.new_mini_chat.label": "Nueva ventana Mini Chat", "settings.openchamber.keyboardShortcuts.action.new_mini_chat.label": "Nueva ventana Mini Chat",
+11
View File
@@ -1140,6 +1140,11 @@ export const dict: Record<I18nKey, string> = {
"contextPanel.mode.context": "Contexto", "contextPanel.mode.context": "Contexto",
"contextPanel.mode.preview": "Vista previa", "contextPanel.mode.preview": "Vista previa",
"contextPanel.mode.browser": "Navegador", "contextPanel.mode.browser": "Navegador",
"contextRail.configure.open": "Configurar paneles",
"contextRail.configure.dialogTitle": "Paneles de la barra",
"contextRail.configure.dialogDescription": "Elige qué paneles muestra la barra. Los paneles ocultos conservan sus datos y siguen accesibles desde la paleta de comandos.",
"contextRail.configure.showAll": "Mostrar todos",
"contextRail.configure.noneWarning": "Todos los paneles están ocultos.",
"contextRail.aria.rail": "Superficies del panel", "contextRail.aria.rail": "Superficies del panel",
"contextPanel.editorEmpty.title": "Ningún archivo abierto", "contextPanel.editorEmpty.title": "Ningún archivo abierto",
"contextPanel.editorEmpty.description": "Elige un archivo del árbol para empezar a editar.", "contextPanel.editorEmpty.description": "Elige un archivo del árbol para empezar a editar.",
@@ -2448,6 +2453,12 @@ export const dict: Record<I18nKey, string> = {
"commandPalette.item.cycleTheme": "Cambiar tema", "commandPalette.item.cycleTheme": "Cambiar tema",
"commandPalette.item.showOpenCodeStatus": "Mostrar estado de OpenCode", "commandPalette.item.showOpenCodeStatus": "Mostrar estado de OpenCode",
"commandPalette.item.toggleMemoryDebug": "Alternar panel de depuración de memoria", "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.item.openSettings": "Abrir configuración...",
"commandPalette.session.untitled": "Sesión sin título", "commandPalette.session.untitled": "Sesión sin título",
"openCodeStatusDialog.title": "Estado de OpenCode", "openCodeStatusDialog.title": "Estado de OpenCode",
@@ -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.label': 'Basculer la surface du panneau contextuel',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0', 'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0',
'settings.openchamber.keyboardShortcuts.action.new_chat.label': 'Nouvelle session', '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.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_chat_worktree.label': 'Nouvelle ébauche d\'worktree',
'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': 'Nouvelle fenêtre de mini-chat', 'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': 'Nouvelle fenêtre de mini-chat',
+11
View File
@@ -959,6 +959,11 @@ export const dict = {
'contextPanel.mode.context': 'Contexte', 'contextPanel.mode.context': 'Contexte',
'contextPanel.mode.preview': 'Aperçu', 'contextPanel.mode.preview': 'Aperçu',
'contextPanel.mode.browser': 'Navigateur', 'contextPanel.mode.browser': 'Navigateur',
'contextRail.configure.open': 'Configurer les panneaux',
'contextRail.configure.dialogTitle': 'Panneaux de la barre',
'contextRail.configure.dialogDescription': 'Choisissez les panneaux affichés par la barre. Les panneaux masqués conservent leurs données et restent accessibles via la palette de commandes.',
'contextRail.configure.showAll': 'Tout afficher',
'contextRail.configure.noneWarning': 'Tous les panneaux sont masqués.',
'contextRail.aria.rail': 'Surfaces du panneau', 'contextRail.aria.rail': 'Surfaces du panneau',
'contextPanel.editorEmpty.title': 'Aucun fichier ouvert', 'contextPanel.editorEmpty.title': 'Aucun fichier ouvert',
'contextPanel.editorEmpty.description': 'Choisissez un fichier dans l’arborescence pour commencer.', 'contextPanel.editorEmpty.description': 'Choisissez un fichier dans l’arborescence pour commencer.',
@@ -2186,6 +2191,12 @@ export const dict = {
'commandPalette.item.cycleTheme': 'Changer de thème', 'commandPalette.item.cycleTheme': 'Changer de thème',
'commandPalette.item.showOpenCodeStatus': 'Afficher le statut OpenCode', 'commandPalette.item.showOpenCodeStatus': 'Afficher le statut OpenCode',
'commandPalette.item.toggleMemoryDebug': 'Basculer le panneau de débogage mémoire', '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.item.openSettings': 'Ouvrez les paramètres...',
'commandPalette.session.untitled': 'Session sans titre', 'commandPalette.session.untitled': 'Session sans titre',
'openCodeStatusDialog.title': 'Statut OpenCode', 'openCodeStatusDialog.title': 'Statut OpenCode',
@@ -1148,6 +1148,10 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': 'コンテキストパネルのサーフェスを切り替え', 'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': 'コンテキストパネルのサーフェスを切り替え',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0', 'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0',
'settings.openchamber.keyboardShortcuts.action.new_chat.label': '新しい Session', '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.close_session_tab.label': 'セッションタブを閉じる',
'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': '新しい Worktree 下書き', 'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': '新しい Worktree 下書き',
'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': '新しいミニチャットウィンドウ', 'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': '新しいミニチャットウィンドウ',
+11
View File
@@ -1136,6 +1136,11 @@ export const dict: Record<I18nKey, string> = {
'contextPanel.mode.context': 'コンテキスト', 'contextPanel.mode.context': 'コンテキスト',
'contextPanel.mode.preview': 'プレビュー', 'contextPanel.mode.preview': 'プレビュー',
'contextPanel.mode.browser': 'ブラウザ', 'contextPanel.mode.browser': 'ブラウザ',
'contextRail.configure.open': 'パネルを設定',
'contextRail.configure.dialogTitle': 'レールのパネル',
'contextRail.configure.dialogDescription': 'レールに表示するパネルを選択します。非表示のパネルもデータは保持され、コマンドパレットから引き続き開けます。',
'contextRail.configure.showAll': 'すべて表示',
'contextRail.configure.noneWarning': 'すべてのパネルが非表示です。',
'contextRail.aria.rail': 'パネルサーフェス', 'contextRail.aria.rail': 'パネルサーフェス',
'contextPanel.editorEmpty.title': 'ファイルが開かれていません', 'contextPanel.editorEmpty.title': 'ファイルが開かれていません',
'contextPanel.editorEmpty.description': 'ツリーからファイルを選んで編集を始めましょう。', 'contextPanel.editorEmpty.description': 'ツリーからファイルを選んで編集を始めましょう。',
@@ -2481,6 +2486,12 @@ export const dict: Record<I18nKey, string> = {
'commandPalette.item.cycleTheme': 'テーマを順に切替', 'commandPalette.item.cycleTheme': 'テーマを順に切替',
'commandPalette.item.showOpenCodeStatus': 'OpenCode のステータスを表示', 'commandPalette.item.showOpenCodeStatus': 'OpenCode のステータスを表示',
'commandPalette.item.toggleMemoryDebug': 'メモリデバッグパネルの切替', '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.item.openSettings': '設定を開く...',
'commandPalette.session.untitled': '無題のセッション', 'commandPalette.session.untitled': '無題のセッション',
'openCodeStatusDialog.title': 'OpenCodeステータス', 'openCodeStatusDialog.title': 'OpenCodeステータス',
@@ -1115,6 +1115,10 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': '컨텍스트 패널 서피스 전환', 'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': '컨텍스트 패널 서피스 전환',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0', 'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0',
'settings.openchamber.keyboardShortcuts.action.new_chat.label': '새 세션', '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.close_session_tab.label': '세션 탭 닫기',
'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': '새 worktree 초안', 'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': '새 worktree 초안',
'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': '새 Mini Chat 창', 'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': '새 Mini Chat 창',
+11
View File
@@ -1140,6 +1140,11 @@ export const dict: Record<I18nKey, string> = {
'contextPanel.mode.context': '컨텍스트', 'contextPanel.mode.context': '컨텍스트',
'contextPanel.mode.preview': '미리보기', 'contextPanel.mode.preview': '미리보기',
'contextPanel.mode.browser': '브라우저', 'contextPanel.mode.browser': '브라우저',
'contextRail.configure.open': '패널 구성',
'contextRail.configure.dialogTitle': '레일 패널',
'contextRail.configure.dialogDescription': '레일에 표시할 패널을 선택하세요. 숨긴 패널의 데이터는 유지되며 명령 팔레트에서 계속 열 수 있습니다.',
'contextRail.configure.showAll': '모두 표시',
'contextRail.configure.noneWarning': '모든 패널이 숨겨져 있습니다.',
'contextRail.aria.rail': '패널 서피스', 'contextRail.aria.rail': '패널 서피스',
'contextPanel.editorEmpty.title': '열린 파일 없음', 'contextPanel.editorEmpty.title': '열린 파일 없음',
'contextPanel.editorEmpty.description': '트리에서 파일을 선택해 편집을 시작하세요.', 'contextPanel.editorEmpty.description': '트리에서 파일을 선택해 편집을 시작하세요.',
@@ -2482,6 +2487,12 @@ export const dict: Record<I18nKey, string> = {
'commandPalette.item.cycleTheme': '테마 순환', 'commandPalette.item.cycleTheme': '테마 순환',
'commandPalette.item.showOpenCodeStatus': 'OpenCode 상태 표시', 'commandPalette.item.showOpenCodeStatus': 'OpenCode 상태 표시',
'commandPalette.item.toggleMemoryDebug': '메모리 디버그 패널 토글', '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.item.openSettings': '설정... 열기',
'commandPalette.session.untitled': '제목 없는 세션', 'commandPalette.session.untitled': '제목 없는 세션',
'openCodeStatusDialog.title': 'OpenCode 상태', 'openCodeStatusDialog.title': 'OpenCode 상태',
@@ -824,6 +824,10 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.toggle_prompt_navigator.label': 'Przełącz nawigator promptów', '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.focus_input.label': 'Skup pole wprowadzania',
'settings.openchamber.keyboardShortcuts.action.new_chat.label': 'Nowa sesja', '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.close_session_tab.label': 'Zamknij kartę sesji',
'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': 'Nowy szkic obszaru roboczego', 'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': 'Nowy szkic obszaru roboczego',
'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': 'Nowe okno Mini Chat', 'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': 'Nowe okno Mini Chat',
+11
View File
@@ -1455,6 +1455,12 @@ export const dict: Record<I18nKey, string> = {
'commandPalette.item.cycleTheme': 'Przełącz motyw', 'commandPalette.item.cycleTheme': 'Przełącz motyw',
'commandPalette.item.showOpenCodeStatus': 'Pokaż status OpenCode', 'commandPalette.item.showOpenCodeStatus': 'Pokaż status OpenCode',
'commandPalette.item.toggleMemoryDebug': 'Przełącz panel debugowania pamięci', '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.session.untitled': 'Nienazwana sesja',
'commandPalette.title': 'Paleta poleceń', 'commandPalette.title': 'Paleta poleceń',
'contextPanel.actions.closePanel': 'Zamknij panel', 'contextPanel.actions.closePanel': 'Zamknij panel',
@@ -1472,6 +1478,11 @@ export const dict: Record<I18nKey, string> = {
'contextPanel.mode.pr': 'Pull Request', 'contextPanel.mode.pr': 'Pull Request',
'contextPanel.mode.preview': 'Podgląd', 'contextPanel.mode.preview': 'Podgląd',
'contextPanel.mode.browser': 'Przeglądarka', 'contextPanel.mode.browser': 'Przeglądarka',
'contextRail.configure.open': 'Konfiguruj panele',
'contextRail.configure.dialogTitle': 'Panele paska',
'contextRail.configure.dialogDescription': 'Wybierz, które panele pokazuje pasek. Ukryte panele zachowują dane i pozostają dostępne z palety poleceń.',
'contextRail.configure.showAll': 'Pokaż wszystkie',
'contextRail.configure.noneWarning': 'Wszystkie panele są ukryte.',
'contextRail.aria.rail': 'Powierzchnie panelu', 'contextRail.aria.rail': 'Powierzchnie panelu',
'contextPanel.editorEmpty.title': 'Brak otwartego pliku', 'contextPanel.editorEmpty.title': 'Brak otwartego pliku',
'contextPanel.editorEmpty.description': 'Wybierz plik z drzewa, aby rozpocząć edycję.', 'contextPanel.editorEmpty.description': 'Wybierz plik z drzewa, aby rozpocząć edycję.',
@@ -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.label": "Alternar superfície do painel de contexto",
"settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix": " + 1…0", "settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix": " + 1…0",
"settings.openchamber.keyboardShortcuts.action.new_chat.label": "Nova sessão", "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.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_chat_worktree.label": "Novo rascunho de worktree",
"settings.openchamber.keyboardShortcuts.action.new_mini_chat.label": "Nova janela Mini Chat", "settings.openchamber.keyboardShortcuts.action.new_mini_chat.label": "Nova janela Mini Chat",
@@ -1140,6 +1140,11 @@ export const dict: Record<I18nKey, string> = {
"contextPanel.mode.context": "Contexto", "contextPanel.mode.context": "Contexto",
"contextPanel.mode.preview": "Prévia", "contextPanel.mode.preview": "Prévia",
"contextPanel.mode.browser": "Navegador", "contextPanel.mode.browser": "Navegador",
"contextRail.configure.open": "Configurar painéis",
"contextRail.configure.dialogTitle": "Painéis da barra",
"contextRail.configure.dialogDescription": "Escolha quais painéis a barra mostra. Painéis ocultos mantêm seus dados e continuam acessíveis pela paleta de comandos.",
"contextRail.configure.showAll": "Mostrar todos",
"contextRail.configure.noneWarning": "Todos os painéis estão ocultos.",
"contextRail.aria.rail": "Superfícies do painel", "contextRail.aria.rail": "Superfícies do painel",
"contextPanel.editorEmpty.title": "Nenhum arquivo aberto", "contextPanel.editorEmpty.title": "Nenhum arquivo aberto",
"contextPanel.editorEmpty.description": "Escolha um arquivo na árvore para começar a editar.", "contextPanel.editorEmpty.description": "Escolha um arquivo na árvore para começar a editar.",
@@ -2448,6 +2453,12 @@ export const dict: Record<I18nKey, string> = {
"commandPalette.item.cycleTheme": "Alternar tema", "commandPalette.item.cycleTheme": "Alternar tema",
"commandPalette.item.showOpenCodeStatus": "Mostrar status do OpenCode", "commandPalette.item.showOpenCodeStatus": "Mostrar status do OpenCode",
"commandPalette.item.toggleMemoryDebug": "Alternar painel de depuração de memória", "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.item.openSettings": "Abrir configurações...",
"commandPalette.session.untitled": "Sessão sem título", "commandPalette.session.untitled": "Sessão sem título",
"openCodeStatusDialog.title": "Status do OpenCode", "openCodeStatusDialog.title": "Status do OpenCode",
@@ -1115,6 +1115,10 @@ export const settingsDict = {
"settings.openchamber.keyboardShortcuts.action.switch_context_surface.label": "Перемкнути поверхню панелі контексту", "settings.openchamber.keyboardShortcuts.action.switch_context_surface.label": "Перемкнути поверхню панелі контексту",
"settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix": " + 1…0", "settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix": " + 1…0",
"settings.openchamber.keyboardShortcuts.action.new_chat.label": "Нова сесія", "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.close_session_tab.label": "Закрити вкладку сесії",
"settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label": "Нова чернетка worktree", "settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label": "Нова чернетка worktree",
"settings.openchamber.keyboardShortcuts.action.new_mini_chat.label": "Нове вікно Mini Chat", "settings.openchamber.keyboardShortcuts.action.new_mini_chat.label": "Нове вікно Mini Chat",
+11
View File
@@ -1140,6 +1140,11 @@ export const dict: Record<I18nKey, string> = {
"contextPanel.mode.context": "Контекст", "contextPanel.mode.context": "Контекст",
"contextPanel.mode.preview": "Перегляд", "contextPanel.mode.preview": "Перегляд",
"contextPanel.mode.browser": "Браузер", "contextPanel.mode.browser": "Браузер",
"contextRail.configure.open": "Налаштувати панелі",
"contextRail.configure.dialogTitle": "Панелі рейки",
"contextRail.configure.dialogDescription": "Обери, які панелі показує рейка. Приховані панелі зберігають дані й доступні з палітри команд.",
"contextRail.configure.showAll": "Показати всі",
"contextRail.configure.noneWarning": "Усі панелі приховано.",
"contextRail.aria.rail": "Поверхні панелі", "contextRail.aria.rail": "Поверхні панелі",
"contextPanel.editorEmpty.title": "Файл не відкрито", "contextPanel.editorEmpty.title": "Файл не відкрито",
"contextPanel.editorEmpty.description": "Виберіть файл у дереві, щоб почати редагування.", "contextPanel.editorEmpty.description": "Виберіть файл у дереві, щоб почати редагування.",
@@ -2448,6 +2453,12 @@ export const dict: Record<I18nKey, string> = {
"commandPalette.item.cycleTheme": "Перемкнути тему", "commandPalette.item.cycleTheme": "Перемкнути тему",
"commandPalette.item.showOpenCodeStatus": "Показати статус OpenCode", "commandPalette.item.showOpenCodeStatus": "Показати статус OpenCode",
"commandPalette.item.toggleMemoryDebug": "Показати/сховати панель memory debug", "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.item.openSettings": "Відкрити налаштування...",
"commandPalette.session.untitled": "Сесія без назви", "commandPalette.session.untitled": "Сесія без назви",
"openCodeStatusDialog.title": "Статус OpenCode", "openCodeStatusDialog.title": "Статус OpenCode",
@@ -1115,6 +1115,10 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': '切换上下文面板界面', 'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': '切换上下文面板界面',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0', 'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0',
'settings.openchamber.keyboardShortcuts.action.new_chat.label': '新建会话', '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.close_session_tab.label': '关闭会话标签页',
'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': '新建工作树草稿', 'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': '新建工作树草稿',
'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': '新建 Mini Chat 窗口', 'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': '新建 Mini Chat 窗口',
@@ -1140,6 +1140,11 @@ export const dict: Record<I18nKey, string> = {
'contextPanel.mode.context': '上下文', 'contextPanel.mode.context': '上下文',
'contextPanel.mode.preview': '预览', 'contextPanel.mode.preview': '预览',
'contextPanel.mode.browser': '浏览器', 'contextPanel.mode.browser': '浏览器',
'contextRail.configure.open': '配置面板',
'contextRail.configure.dialogTitle': '侧栏面板',
'contextRail.configure.dialogDescription': '选择侧栏显示哪些面板。隐藏的面板会保留数据,仍可通过命令面板打开。',
'contextRail.configure.showAll': '全部显示',
'contextRail.configure.noneWarning': '所有面板均已隐藏。',
'contextRail.aria.rail': '面板界面', 'contextRail.aria.rail': '面板界面',
'contextPanel.editorEmpty.title': '未打开文件', 'contextPanel.editorEmpty.title': '未打开文件',
'contextPanel.editorEmpty.description': '从文件树中选择一个文件开始编辑。', 'contextPanel.editorEmpty.description': '从文件树中选择一个文件开始编辑。',
@@ -2448,6 +2453,12 @@ export const dict: Record<I18nKey, string> = {
'commandPalette.item.cycleTheme': '轮换主题', 'commandPalette.item.cycleTheme': '轮换主题',
'commandPalette.item.showOpenCodeStatus': '显示 OpenCode 状态', 'commandPalette.item.showOpenCodeStatus': '显示 OpenCode 状态',
'commandPalette.item.toggleMemoryDebug': '切换内存调试面板', '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.item.openSettings': '打开设置...',
'commandPalette.session.untitled': '未命名会话', 'commandPalette.session.untitled': '未命名会话',
'openCodeStatusDialog.title': 'OpenCode 状态', 'openCodeStatusDialog.title': 'OpenCode 状态',
@@ -1022,6 +1022,10 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': '切換上下文面板介面', 'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': '切換上下文面板介面',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0', 'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0',
'settings.openchamber.keyboardShortcuts.action.new_chat.label': '新建工作階段', '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.close_session_tab.label': '關閉工作階段分頁',
'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': '新增 worktree 草稿', 'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': '新增 worktree 草稿',
'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': '新建 Mini Chat 視窗', 'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': '新建 Mini Chat 視窗',
@@ -1152,6 +1152,11 @@ export const dict: Record<I18nKey, string> = {
'contextPanel.mode.context': '上下文', 'contextPanel.mode.context': '上下文',
'contextPanel.mode.preview': '預覽', 'contextPanel.mode.preview': '預覽',
'contextPanel.mode.browser': '瀏覽器', 'contextPanel.mode.browser': '瀏覽器',
'contextRail.configure.open': '設定面板',
'contextRail.configure.dialogTitle': '側欄面板',
'contextRail.configure.dialogDescription': '選擇側欄顯示哪些面板。隱藏的面板會保留資料,仍可透過命令面板開啟。',
'contextRail.configure.showAll': '全部顯示',
'contextRail.configure.noneWarning': '所有面板皆已隱藏。',
'contextRail.aria.rail': '面板介面', 'contextRail.aria.rail': '面板介面',
'contextPanel.editorEmpty.title': '未開啟檔案', 'contextPanel.editorEmpty.title': '未開啟檔案',
'contextPanel.editorEmpty.description': '從檔案樹選擇檔案開始編輯。', 'contextPanel.editorEmpty.description': '從檔案樹選擇檔案開始編輯。',
@@ -2452,6 +2457,12 @@ export const dict: Record<I18nKey, string> = {
'commandPalette.item.cycleTheme': '輪換主題', 'commandPalette.item.cycleTheme': '輪換主題',
'commandPalette.item.showOpenCodeStatus': '顯示 OpenCode 狀態', 'commandPalette.item.showOpenCodeStatus': '顯示 OpenCode 狀態',
'commandPalette.item.toggleMemoryDebug': '切換記憶體偵錯面板', '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.item.openSettings': '開啟設定...',
'commandPalette.session.untitled': '未命名會話', 'commandPalette.session.untitled': '未命名會話',
'openCodeStatusDialog.title': 'OpenCode 狀態', 'openCodeStatusDialog.title': 'OpenCode 狀態',
@@ -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');
});
});
@@ -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;
};
+22
View File
@@ -26,6 +26,28 @@ export const activateSessionTabByIndex = (index: number): boolean => {
return true; 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 => { export const closeSessionTabAndActivateNeighbour = (sessionId: string): void => {
const { tabIds, closeTab } = useSessionTabsStore.getState(); const { tabIds, closeTab } = useSessionTabsStore.getState();
if (!tabIds.includes(sessionId)) return; if (!tabIds.includes(sessionId)) return;
+28
View File
@@ -51,6 +51,34 @@ const SHORTCUT_GROUPS = {
customizable: true, customizable: true,
settingsLabelKey: 'settings.openchamber.keyboardShortcuts.action.new_chat.label', 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', id: 'close_session_tab',
defaultBinding: 'alt+w', defaultBinding: 'alt+w',
@@ -22,7 +22,10 @@ edge (`components/layout/ContextPanelRail.tsx`) and rendered by
registry's default order and appends any missing surfaces. registry's default order and appends any missing surfaces.
- `getVisibleContextRailSurfaces` is the single visibility filter shared by the - `getVisibleContextRailSurfaces` is the single visibility filter shared by the
rail and the global surface-switch shortcut (`switch_context_surface` in rail and the global surface-switch shortcut (`switch_context_surface` in
`lib/shortcuts.ts`): it drops the plan surface unless plan mode is enabled, `lib/shortcuts`): it drops surfaces the user hid
(`useUIStore.contextRailHiddenSurfaces`, edited from the rail's trailing
configure button — `ContextRailSurfacesDialog`), drops the plan surface
unless plan mode is enabled,
drops the walkthrough on VS Code and below `WALKTHROUGH_MIN_WIDTH`, and hides drops the walkthrough on VS Code and below `WALKTHROUGH_MIN_WIDTH`, and hides
`has-content` surfaces until a tab of their mode exists. Both consumers use `has-content` surfaces until a tab of their mode exists. Both consumers use
it so the digit shown on a rail badge always maps to the same surface the it so the digit shown on a rail badge always maps to the same surface the
+6
View File
@@ -187,6 +187,9 @@ export const sortContextSurfaces = (railOrder: readonly string[]): ContextSurfac
type VisibleRailSurfacesOptions = { type VisibleRailSurfacesOptions = {
railOrder: readonly string[]; railOrder: readonly string[];
/** Surfaces the user chose to hide from the rail (and from the digit
shortcuts, which share this filter). */
hiddenSurfaces?: readonly string[];
planModeEnabled: boolean; planModeEnabled: boolean;
isVSCode: boolean; isVSCode: boolean;
screenWidth: number; screenWidth: number;
@@ -203,6 +206,9 @@ type VisibleRailSurfacesOptions = {
*/ */
export const getVisibleContextRailSurfaces = (options: VisibleRailSurfacesOptions): ContextSurfaceDescriptor[] => { export const getVisibleContextRailSurfaces = (options: VisibleRailSurfacesOptions): ContextSurfaceDescriptor[] => {
return sortContextSurfaces(options.railOrder).filter((surface) => { return sortContextSurfaces(options.railOrder).filter((surface) => {
if (options.hiddenSurfaces?.includes(surface.id)) {
return false;
}
if (surface.id === 'plan' && !options.planModeEnabled) { if (surface.id === 'plan' && !options.planModeEnabled) {
return false; return false;
} }
+27
View File
@@ -607,6 +607,9 @@ interface UIStore {
hasManuallyResizedLeftSidebar: boolean; hasManuallyResizedLeftSidebar: boolean;
contextPanelByDirectory: Record<string, ContextPanelDirectoryState>; contextPanelByDirectory: Record<string, ContextPanelDirectoryState>;
contextRailOrder: string[]; contextRailOrder: string[];
/** Surface ids the user hid from the context rail; stored as the hidden set
so surfaces added later appear for everyone. */
contextRailHiddenSurfaces: string[];
contextEditorTreeVisible: boolean; contextEditorTreeVisible: boolean;
contextEditorTreeWidth: number; contextEditorTreeWidth: number;
notesPanelHeight: number; notesPanelHeight: number;
@@ -828,6 +831,8 @@ interface UIStore {
setWorkStatusOverlayOpen: (open: boolean) => void; setWorkStatusOverlayOpen: (open: boolean) => void;
setWorkStatusSectionVisible: (sectionId: string, visible: boolean) => void; setWorkStatusSectionVisible: (sectionId: string, visible: boolean) => void;
setWorkStatusHiddenSections: (sectionIds: string[]) => void; setWorkStatusHiddenSections: (sectionIds: string[]) => void;
setContextRailSurfaceVisible: (surfaceId: string, visible: boolean) => void;
setContextRailHiddenSurfaces: (surfaceIds: string[]) => void;
setSessionSwitcherOpen: (open: boolean) => void; setSessionSwitcherOpen: (open: boolean) => void;
setSessionDropdownOpen: (open: boolean) => void; setSessionDropdownOpen: (open: boolean) => void;
setPendingDiffFile: (filePath: string | null, staged?: boolean, scope?: PendingDiffScope | null) => void; setPendingDiffFile: (filePath: string | null, staged?: boolean, scope?: PendingDiffScope | null) => void;
@@ -990,6 +995,7 @@ export const useUIStore = create<UIStore>()(
hasManuallyResizedLeftSidebar: false, hasManuallyResizedLeftSidebar: false,
contextPanelByDirectory: {}, contextPanelByDirectory: {},
contextRailOrder: [], contextRailOrder: [],
contextRailHiddenSurfaces: [],
contextEditorTreeVisible: true, contextEditorTreeVisible: true,
contextEditorTreeWidth: 240, contextEditorTreeWidth: 240,
notesPanelHeight: 112, notesPanelHeight: 112,
@@ -1599,6 +1605,23 @@ export const useUIStore = create<UIStore>()(
set({ workStatusHiddenSections: [...new Set(sectionIds)] }); set({ workStatusHiddenSections: [...new Set(sectionIds)] });
}, },
setContextRailSurfaceVisible: (surfaceId, visible) => {
set((state) => {
const hidden = state.contextRailHiddenSurfaces;
const isHidden = hidden.includes(surfaceId);
if (visible === !isHidden) return state;
return {
contextRailHiddenSurfaces: visible
? hidden.filter((entry) => entry !== surfaceId)
: [...hidden, surfaceId],
};
});
},
setContextRailHiddenSurfaces: (surfaceIds) => {
set({ contextRailHiddenSurfaces: [...new Set(surfaceIds)] });
},
setSessionSwitcherOpen: (open) => { setSessionSwitcherOpen: (open) => {
if (get().isSessionSwitcherOpen === open) { if (get().isSessionSwitcherOpen === open) {
@@ -2627,6 +2650,9 @@ export const useUIStore = create<UIStore>()(
state.autoSaveEnabled = true; state.autoSaveEnabled = true;
} }
state.contextRailHiddenSurfaces = Array.isArray(state.contextRailHiddenSurfaces)
? (state.contextRailHiddenSurfaces as unknown[]).filter((id): id is string => typeof id === 'string' && id.trim() !== '')
: [];
state.contextRailOrder = Array.isArray(state.contextRailOrder) state.contextRailOrder = Array.isArray(state.contextRailOrder)
? (state.contextRailOrder as unknown[]).filter((id): id is string => typeof id === 'string' && id.trim() !== '') ? (state.contextRailOrder as unknown[]).filter((id): id is string => typeof id === 'string' && id.trim() !== '')
: []; : [];
@@ -2639,6 +2665,7 @@ export const useUIStore = create<UIStore>()(
sidebarWidth: state.sidebarWidth, sidebarWidth: state.sidebarWidth,
contextPanelByDirectory: state.contextPanelByDirectory, contextPanelByDirectory: state.contextPanelByDirectory,
contextRailOrder: state.contextRailOrder, contextRailOrder: state.contextRailOrder,
contextRailHiddenSurfaces: state.contextRailHiddenSurfaces,
contextEditorTreeVisible: state.contextEditorTreeVisible, contextEditorTreeVisible: state.contextEditorTreeVisible,
contextEditorTreeWidth: state.contextEditorTreeWidth, contextEditorTreeWidth: state.contextEditorTreeWidth,
notesPanelHeight: state.notesPanelHeight, notesPanelHeight: state.notesPanelHeight,
+1
View File
@@ -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. - 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: 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"). - 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). - 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. - 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). - The timeline dialog now fits small windows instead of squeezing the message list to a couple of rows (thanks to @gaojunran).