Merge branch 'openchamber:main' into github-usage-rework
This commit is contained in:
@@ -77,6 +77,7 @@ import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
||||
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
|
||||
import { usePermissionStore } from '@/stores/permissionStore';
|
||||
import { togglePermissionAutoAccept } from './permissionAutoAccept';
|
||||
import { useKeybind } from '@/hooks/useKeybind';
|
||||
import { extractGitChangedFiles } from './changedFiles';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { sessionEvents } from '@/lib/sessionEvents';
|
||||
@@ -2562,6 +2563,11 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
|
||||
t,
|
||||
]);
|
||||
|
||||
useKeybind('toggle_permission_auto_accept', () => {
|
||||
if (!isPermissionAutoAcceptInteractive) return false;
|
||||
handlePermissionAutoAcceptToggle();
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
const pendingAbortBanner = Boolean(abortPromptSessionId) && abortPromptSessionId === currentSessionId;
|
||||
if (!prevWasAbortedRef.current && pendingAbortBanner && !showAbortStatus) {
|
||||
|
||||
@@ -1534,6 +1534,54 @@ const MessageList = React.forwardRef<MessageListHandle, MessageListProps>(({
|
||||
return true;
|
||||
}, [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 container = resolveScrollContainer();
|
||||
if (!container) {
|
||||
@@ -1569,14 +1617,19 @@ const MessageList = React.forwardRef<MessageListHandle, MessageListProps>(({
|
||||
if (!container) {
|
||||
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) {
|
||||
turnElement.scrollIntoView({ behavior, block: 'start' });
|
||||
if (behavior !== 'smooth') settleNavigationTarget(findTurnElement, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
return scrollHistoryIndexIntoView(index);
|
||||
if (!scrollHistoryIndexIntoView(index)) {
|
||||
return false;
|
||||
}
|
||||
if (behavior !== 'smooth') settleNavigationTarget(findTurnElement, 0);
|
||||
return true;
|
||||
},
|
||||
|
||||
scrollToMessageId: (messageId: string, options?: { behavior?: ScrollBehavior }) => {
|
||||
@@ -1586,8 +1639,12 @@ const MessageList = React.forwardRef<MessageListHandle, MessageListProps>(({
|
||||
return false;
|
||||
}
|
||||
|
||||
return scrollMessageElementIntoView(messageId, behavior)
|
||||
const didScroll = scrollMessageElementIntoView(messageId, behavior)
|
||||
|| scrollHistoryIndexIntoView(index);
|
||||
if (didScroll && behavior !== 'smooth') {
|
||||
settleNavigationTarget(() => findMessageElement(messageId), 50);
|
||||
}
|
||||
return didScroll;
|
||||
},
|
||||
|
||||
holdViewportAnchor: (anchor) => {
|
||||
@@ -1730,7 +1787,7 @@ const MessageList = React.forwardRef<MessageListHandle, MessageListProps>(({
|
||||
return () => {
|
||||
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 resolved = resolveChatListAnchoredEndSpace(
|
||||
|
||||
@@ -10,6 +10,10 @@ import { Icon } from "@/components/icon/Icon";
|
||||
import { DiffPreview, WritePreview } from './DiffPreview';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { getVisiblePermissionPatterns } from './permissionCardPatterns';
|
||||
import { formatShortcutForDisplay } from '@/lib/shortcuts';
|
||||
|
||||
// Newest pending card owns the keyboard; older cards wait their turn.
|
||||
const activePermissionCardIds: string[] = [];
|
||||
|
||||
const PERMISSION_BASH_CUSTOM_STYLE: React.CSSProperties = {
|
||||
margin: 0,
|
||||
@@ -126,6 +130,33 @@ export const PermissionCard: React.FC<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) {
|
||||
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" />
|
||||
Allow Once
|
||||
<kbd className="ml-1 hidden sm:inline typography-micro opacity-60">{formatShortcutForDisplay('alt+enter')}</kbd>
|
||||
</button>
|
||||
|
||||
{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" />
|
||||
Always Allow
|
||||
<kbd className="ml-1 hidden sm:inline typography-micro opacity-60">{formatShortcutForDisplay('alt+shift+enter')}</kbd>
|
||||
</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" />
|
||||
Deny
|
||||
<kbd className="ml-1 hidden sm:inline typography-micro opacity-60">{formatShortcutForDisplay('alt+backspace')}</kbd>
|
||||
</button>
|
||||
|
||||
{isResponding && (
|
||||
|
||||
@@ -36,6 +36,7 @@ import { cn } from '@/lib/utils';
|
||||
import { useFeatureFlagsStore } from '@/stores/useFeatureFlagsStore';
|
||||
import { useGitStatus } from '@/stores/useGitStore';
|
||||
import { normalizeContextPanelDirectoryKey, useUIStore } from '@/stores/useUIStore';
|
||||
import { ContextRailSurfacesDialog } from './ContextRailSurfacesDialog';
|
||||
|
||||
const RAIL_TOOLTIP_DELAY_MS = 150;
|
||||
// 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 workStatusPanelVisible = useUIStore((state) => state.workStatusPanelVisible);
|
||||
const contextRailOrder = useUIStore((state) => state.contextRailOrder);
|
||||
const contextRailHiddenSurfaces = useUIStore((state) => state.contextRailHiddenSurfaces);
|
||||
const setContextRailOrder = useUIStore((state) => state.setContextRailOrder);
|
||||
const openContextSurface = useUIStore((state) => state.openContextSurface);
|
||||
const shortcutOverrides = useUIStore((state) => state.shortcutOverrides);
|
||||
@@ -256,12 +258,15 @@ export const ContextPanelRail: React.FC = () => {
|
||||
const surfaces = React.useMemo(() => {
|
||||
return getVisibleContextRailSurfaces({
|
||||
railOrder: contextRailOrder,
|
||||
hiddenSurfaces: contextRailHiddenSurfaces,
|
||||
planModeEnabled,
|
||||
isVSCode: isVSCodeRuntime(),
|
||||
screenWidth,
|
||||
tabs,
|
||||
});
|
||||
}, [contextRailOrder, planModeEnabled, screenWidth, tabs]);
|
||||
}, [contextRailHiddenSurfaces, contextRailOrder, planModeEnabled, screenWidth, tabs]);
|
||||
|
||||
const [isSurfacesDialogOpen, setIsSurfacesDialogOpen] = React.useState(false);
|
||||
|
||||
const handleDragEnd = React.useCallback((event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
@@ -331,6 +336,24 @@ export const ContextPanelRail: React.FC = () => {
|
||||
})}
|
||||
</SortableContext>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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({
|
||||
rename_current_session: () => {
|
||||
if (!currentSessionId || isMobile) return false;
|
||||
beginHeaderSessionRename();
|
||||
},
|
||||
toggle_services_menu: () => {
|
||||
if (isDesktopServicesOpen) {
|
||||
setIsDesktopServicesOpen(false);
|
||||
|
||||
@@ -50,6 +50,7 @@ import { scoreByFuzzyQuery } from '@/lib/search/fuzzySearch';
|
||||
import { truncatePathMiddle } from '@/lib/utils';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { sessionEvents } from '@/lib/sessionEvents';
|
||||
import { copyTextToClipboard } from '@/lib/clipboard';
|
||||
import { useProjectsStore } from '@/stores/useProjectsStore';
|
||||
import { buildCommandPaletteFileSearchKey, scoreCommandPaletteFiles } from './commandPaletteFilesState';
|
||||
|
||||
@@ -59,6 +60,9 @@ type CommandEntry = {
|
||||
icon: React.ReactNode;
|
||||
shortcutId?: string;
|
||||
searchText: string;
|
||||
/** Search-only command: reachable by typing, hidden from the initial list
|
||||
so the first screen stays scroll-free. */
|
||||
secondary?: boolean;
|
||||
onSelect: () => void;
|
||||
};
|
||||
|
||||
@@ -90,9 +94,14 @@ export const CommandPalette: React.FC = () => {
|
||||
const openContextSurface = useUIStore((s) => s.openContextSurface);
|
||||
const openContextFile = useUIStore((s) => s.openContextFile);
|
||||
const shortcutOverrides = useUIStore((s) => s.shortcutOverrides);
|
||||
const openMultiRunLauncher = useUIStore((s) => s.openMultiRunLauncher);
|
||||
const setArchivePageOpen = useUIStore((s) => s.setArchivePageOpen);
|
||||
const setProjectContextTab = useUIStore((s) => s.setProjectContextTab);
|
||||
|
||||
const openNewSessionDraft = useSessionUIStore((s) => s.openNewSessionDraft);
|
||||
const setCurrentSession = useSessionUIStore((s) => s.setCurrentSession);
|
||||
const currentSessionId = useSessionUIStore((s) => s.currentSessionId);
|
||||
const togglePinnedSession = useSessionPinnedStore((s) => s.toggle);
|
||||
|
||||
const activeSessions = useGlobalSessionsStore(React.useCallback(
|
||||
(state) => isCommandPaletteOpen ? state.activeSessions : EMPTY_SESSIONS,
|
||||
@@ -233,6 +242,7 @@ export const CommandPalette: React.FC = () => {
|
||||
},
|
||||
{
|
||||
id: 'cycle-theme',
|
||||
secondary: true,
|
||||
title: t('commandPalette.item.cycleTheme'),
|
||||
icon: <Icon name="palette" className="mr-2 h-4 w-4" />,
|
||||
shortcutId: 'cycle_theme',
|
||||
@@ -243,6 +253,7 @@ export const CommandPalette: React.FC = () => {
|
||||
},
|
||||
{
|
||||
id: 'open-status',
|
||||
secondary: true,
|
||||
title: t('commandPalette.item.showOpenCodeStatus'),
|
||||
icon: <Icon name="pulse" className="mr-2 h-4 w-4" />,
|
||||
searchText: t('commandPalette.item.showOpenCodeStatus'),
|
||||
@@ -259,8 +270,90 @@ export const CommandPalette: React.FC = () => {
|
||||
onSelect: run(() => setSettingsDialogOpen(true)),
|
||||
},
|
||||
];
|
||||
list.push(
|
||||
{
|
||||
id: 'pin-session',
|
||||
secondary: true,
|
||||
title: t('commandPalette.item.pinSession'),
|
||||
icon: <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({
|
||||
id: 'toggle-memory-debug',
|
||||
secondary: true,
|
||||
title: t('commandPalette.item.toggleMemoryDebug'),
|
||||
icon: <Icon name="bug" className="mr-2 h-4 w-4" />,
|
||||
searchText: t('commandPalette.item.toggleMemoryDebug'),
|
||||
@@ -299,6 +392,11 @@ export const CommandPalette: React.FC = () => {
|
||||
setSettingsDialogOpen,
|
||||
activeProject?.id,
|
||||
activeProject?.path,
|
||||
currentSessionId,
|
||||
togglePinnedSession,
|
||||
openMultiRunLauncher,
|
||||
setArchivePageOpen,
|
||||
setProjectContextTab,
|
||||
]);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -407,7 +505,9 @@ export const CommandPalette: React.FC = () => {
|
||||
const hasQuery = liveTrimmed.length > 0;
|
||||
|
||||
const scoredCommands = React.useMemo(() => {
|
||||
if (!hasQuery) return commands.map((item) => ({ item, score: 0 }));
|
||||
if (!hasQuery) {
|
||||
return commands.filter((item) => !item.secondary).map((item) => ({ item, score: 0 }));
|
||||
}
|
||||
return scoreByFuzzyQuery(commands, liveTrimmed, (c) => c.searchText, {
|
||||
limit: 7,
|
||||
noFuzzy: true,
|
||||
|
||||
@@ -1768,6 +1768,37 @@ export const DiffView: React.FC<DiffViewProps> = ({
|
||||
scrollToFile(value);
|
||||
}, [cancelPendingScrollAlignment, expandStackedFile, scrollToFile]);
|
||||
|
||||
// Step review to the adjacent changed file (alt+arrow): selects, expands
|
||||
// a collapsed section, and scrolls to it. Window-level because the diff
|
||||
// surface has no persistent focus target; guarded off editable fields.
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (!event.altKey || event.metaKey || event.ctrlKey || event.shiftKey) return;
|
||||
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return;
|
||||
const target = event.target;
|
||||
if (target instanceof HTMLElement && (
|
||||
target.isContentEditable
|
||||
|| target.tagName === 'INPUT'
|
||||
|| target.tagName === 'TEXTAREA'
|
||||
|| target.closest('[role="dialog"]')
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
if (changedFiles.length === 0) return;
|
||||
const delta = event.key === 'ArrowDown' ? 1 : -1;
|
||||
const index = displayFile ? changedFiles.findIndex((file) => file.path === displayFile) : -1;
|
||||
const nextIndex = index === -1
|
||||
? (delta > 0 ? 0 : changedFiles.length - 1)
|
||||
: index + delta;
|
||||
const next = changedFiles[nextIndex];
|
||||
if (!next) return;
|
||||
event.preventDefault();
|
||||
handleSelectFileAndScroll(next.path);
|
||||
};
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [changedFiles, displayFile, handleSelectFileAndScroll]);
|
||||
|
||||
const handleHeaderLayoutChange = React.useCallback((mode: DiffViewMode) => {
|
||||
const nextLayout: 'inline' | 'side-by-side' =
|
||||
mode === 'side-by-side' ? 'side-by-side' : 'inline';
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useI18n } from '@/lib/i18n';
|
||||
interface CommitInputProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
onSubmit?: () => void;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
hasTouchInput?: boolean;
|
||||
@@ -18,6 +19,7 @@ const MAX_HEIGHT = 200;
|
||||
export const CommitInput: React.FC<CommitInputProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
onSubmit,
|
||||
placeholder,
|
||||
disabled = false,
|
||||
hasTouchInput = false,
|
||||
@@ -58,6 +60,12 @@ export const CommitInput: React.FC<CommitInputProps> = ({
|
||||
ref={textareaRef}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey) {
|
||||
e.preventDefault();
|
||||
onSubmit?.();
|
||||
}
|
||||
}}
|
||||
placeholder={placeholder ?? t('gitView.commit.messagePlaceholder')}
|
||||
rows={1}
|
||||
disabled={disabled}
|
||||
|
||||
@@ -68,6 +68,9 @@ export const CommitSection: React.FC<CommitSectionProps> = ({
|
||||
<CommitInput
|
||||
value={commitMessage}
|
||||
onChange={onCommitMessageChange}
|
||||
onSubmit={() => {
|
||||
if (canCommit && !isGeneratingMessage) onCommit();
|
||||
}}
|
||||
placeholder={t('gitView.commit.messagePlaceholder')}
|
||||
disabled={commitAction !== null}
|
||||
hasTouchInput={hasTouchInput}
|
||||
|
||||
@@ -7,7 +7,9 @@ import { useUIStore } from '@/stores/useUIStore';
|
||||
import {
|
||||
CHAT_LIST_ANCHOR_OFFSET,
|
||||
getAnchoredTurnMetrics,
|
||||
getRowBottom,
|
||||
resolveTimelineIsAtEnd,
|
||||
TIMELINE_FOLLOW_REARM_THRESHOLD_PX,
|
||||
type TimelineListMeasurementState,
|
||||
type TimelineScrollMode,
|
||||
} 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
|
||||
// overlay scrollbar suppression instead of the anchor's mere existence.
|
||||
const [userOwnsScroll, setUserOwnsScroll] = React.useState(false);
|
||||
const userOwnsScrollRef = React.useRef(userOwnsScroll);
|
||||
userOwnsScrollRef.current = userOwnsScroll;
|
||||
|
||||
const modeRef = React.useRef<TimelineScrollMode>('following-end');
|
||||
const isAtEndRef = React.useRef(true);
|
||||
@@ -547,9 +551,12 @@ export const useChatTimelineScroll = ({
|
||||
// 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
|
||||
// list's size compensation instead. Deliberately NO snap back to the end
|
||||
// afterwards: a slow drag settles repeatedly, and each snap reads as the
|
||||
// very jump this suspension removes — geometry changed, staying where the
|
||||
// reader is beats re-asserting the edge.
|
||||
// afterwards for a mid-conversation reader: a slow drag settles
|
||||
// repeatedly, and each snap reads as the very jump this suspension
|
||||
// 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);
|
||||
React.useEffect(() => {
|
||||
if (!scrollNode || typeof ResizeObserver === 'undefined') return;
|
||||
@@ -569,6 +576,9 @@ export const useChatTimelineScroll = ({
|
||||
quietTimer = setTimeout(() => {
|
||||
quietTimer = null;
|
||||
widthResizingRef.current = false;
|
||||
if (isAtEndRef.current && pendingAnchorRef.current === null) {
|
||||
void listRef.current?.scrollToEnd({ animated: false });
|
||||
}
|
||||
}, 350);
|
||||
});
|
||||
observer.observe(scrollNode);
|
||||
@@ -580,7 +590,57 @@ export const useChatTimelineScroll = ({
|
||||
|
||||
const onTimelineDataChange = React.useCallback(() => {
|
||||
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;
|
||||
|
||||
// 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
|
||||
// array, so data-change callbacks are silent for the entire stream. The
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
import { isTerminalEventTarget } from '@/lib/terminalFocus';
|
||||
import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||
import { activateSessionTabByIndex, closeSessionTabAndActivateNeighbour } from '@/lib/sessionTabs';
|
||||
import { activateAdjacentSessionTab, activateSessionTabByIndex, closeSessionTabAndActivateNeighbour } from '@/lib/sessionTabs';
|
||||
import { navigateSessionHistory } from '@/lib/sessionNavigationHistory';
|
||||
import { useSelectionStore } from '@/sync/selection-store';
|
||||
import * as sessionActions from '@/sync/session-actions';
|
||||
import { normalizeContextPanelDirectoryKey, useUIStore } from '@/stores/useUIStore';
|
||||
@@ -169,6 +170,14 @@ export const useKeyboardShortcuts = () => {
|
||||
console.warn('[keyboard-shortcuts] failed to open draft mini chat window', error);
|
||||
});
|
||||
},
|
||||
switch_session_previous: () => {
|
||||
if (!isVSCodeRuntime() && useUIStore.getState().sessionTabsEnabled && activateAdjacentSessionTab(-1)) return;
|
||||
return navigateSessionHistory(-1) ? undefined : false;
|
||||
},
|
||||
switch_session_next: () => {
|
||||
if (!isVSCodeRuntime() && useUIStore.getState().sessionTabsEnabled && activateAdjacentSessionTab(1)) return;
|
||||
return navigateSessionHistory(1) ? undefined : false;
|
||||
},
|
||||
close_session_tab: () => {
|
||||
if (isVSCodeRuntime() || !useUIStore.getState().sessionTabsEnabled) return false;
|
||||
if (currentSessionId) {
|
||||
@@ -490,6 +499,7 @@ export const useKeyboardShortcuts = () => {
|
||||
const panel = state.contextPanelByDirectory[directory];
|
||||
const visibleSurfaces = getVisibleContextRailSurfaces({
|
||||
railOrder: state.contextRailOrder,
|
||||
hiddenSurfaces: state.contextRailHiddenSurfaces,
|
||||
planModeEnabled: useFeatureFlagsStore.getState().planModeEnabled,
|
||||
isVSCode: isVSCodeRuntime(),
|
||||
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.suffix': ' + 1…0',
|
||||
'settings.openchamber.keyboardShortcuts.action.new_chat.label': 'Neue Sitzung',
|
||||
'settings.openchamber.keyboardShortcuts.action.switch_session_previous.label': 'Vorherige Sitzung',
|
||||
'settings.openchamber.keyboardShortcuts.action.switch_session_next.label': 'Nächste Sitzung',
|
||||
'settings.openchamber.keyboardShortcuts.action.rename_current_session.label': 'Aktuelle Sitzung umbenennen',
|
||||
'settings.openchamber.keyboardShortcuts.action.toggle_permission_auto_accept.label': 'Auto-Genehmigung umschalten',
|
||||
'settings.openchamber.keyboardShortcuts.action.close_session_tab.label': 'Sitzungs-Tab schließen',
|
||||
'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': 'Neuer Worktree-Entwurf',
|
||||
'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': 'Neues Mini-Chat-Fenster',
|
||||
|
||||
@@ -2292,6 +2292,12 @@ export const dict = {
|
||||
'commandPalette.item.cycleTheme': 'Thema wechseln',
|
||||
'commandPalette.item.showOpenCodeStatus': 'OpenCode-Status anzeigen',
|
||||
'commandPalette.item.toggleMemoryDebug': 'Memory-Debug-Panel umschalten',
|
||||
'commandPalette.item.pinSession': 'Sitzung anheften oder lösen',
|
||||
'commandPalette.item.copySessionId': 'Sitzungs-ID kopieren',
|
||||
'commandPalette.item.openMultiRun': 'Multi-Run-Launcher öffnen',
|
||||
'commandPalette.item.openArchive': 'Archivierte Sitzungen öffnen',
|
||||
'commandPalette.item.openNotes': 'Notizbereich öffnen',
|
||||
'commandPalette.item.openTodos': 'To-do-Bereich öffnen',
|
||||
'commandPalette.item.openSettings': 'Einstellungen öffnen...',
|
||||
'commandPalette.session.untitled': 'Unbenannte Sitzung',
|
||||
'openCodeStatusDialog.title': 'OpenCode-Status',
|
||||
@@ -2989,6 +2995,11 @@ export const dict = {
|
||||
'gitView.pr.segment.comments': 'Kommentare',
|
||||
'gitView.pr.comments.addAll': 'Alle hinzufügen',
|
||||
'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',
|
||||
'contextPanel.editorEmpty.title': 'Kein Kontext ausgewählt',
|
||||
'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.suffix': ' + 1…0',
|
||||
'settings.openchamber.keyboardShortcuts.action.new_chat.label': 'New session',
|
||||
'settings.openchamber.keyboardShortcuts.action.switch_session_previous.label': 'Previous session',
|
||||
'settings.openchamber.keyboardShortcuts.action.switch_session_next.label': 'Next session',
|
||||
'settings.openchamber.keyboardShortcuts.action.rename_current_session.label': 'Rename current session',
|
||||
'settings.openchamber.keyboardShortcuts.action.toggle_permission_auto_accept.label': 'Toggle permission auto-accept',
|
||||
'settings.openchamber.keyboardShortcuts.action.close_session_tab.label': 'Close session tab',
|
||||
'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': 'New worktree draft',
|
||||
'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': 'New Mini Chat window',
|
||||
|
||||
@@ -1139,6 +1139,11 @@ export const dict = {
|
||||
'contextPanel.mode.context': 'Context',
|
||||
'contextPanel.mode.preview': 'Preview',
|
||||
'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',
|
||||
'contextPanel.editorEmpty.title': 'No file open',
|
||||
'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.showOpenCodeStatus': 'Show OpenCode status',
|
||||
'commandPalette.item.toggleMemoryDebug': 'Toggle memory debug panel',
|
||||
'commandPalette.item.pinSession': 'Pin or unpin session',
|
||||
'commandPalette.item.copySessionId': 'Copy session ID',
|
||||
'commandPalette.item.openMultiRun': 'Open multi-run launcher',
|
||||
'commandPalette.item.openArchive': 'Open archived sessions',
|
||||
'commandPalette.item.openNotes': 'Open notes surface',
|
||||
'commandPalette.item.openTodos': 'Open todos surface',
|
||||
'commandPalette.item.openSettings': 'Open Settings...',
|
||||
'commandPalette.session.untitled': 'Untitled Session',
|
||||
'openCodeStatusDialog.title': 'OpenCode Status',
|
||||
|
||||
@@ -1115,6 +1115,10 @@ export const settingsDict = {
|
||||
"settings.openchamber.keyboardShortcuts.action.switch_context_surface.label": "Cambiar superficie del panel de contexto",
|
||||
"settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix": " + 1…0",
|
||||
"settings.openchamber.keyboardShortcuts.action.new_chat.label": "Nueva sesión",
|
||||
"settings.openchamber.keyboardShortcuts.action.switch_session_previous.label": "Sesión anterior",
|
||||
"settings.openchamber.keyboardShortcuts.action.switch_session_next.label": "Sesión siguiente",
|
||||
"settings.openchamber.keyboardShortcuts.action.rename_current_session.label": "Renombrar sesión actual",
|
||||
"settings.openchamber.keyboardShortcuts.action.toggle_permission_auto_accept.label": "Alternar aprobación automática",
|
||||
"settings.openchamber.keyboardShortcuts.action.close_session_tab.label": "Cerrar pestaña de sesión",
|
||||
"settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label": "Nuevo borrador de worktree",
|
||||
"settings.openchamber.keyboardShortcuts.action.new_mini_chat.label": "Nueva ventana Mini Chat",
|
||||
|
||||
@@ -1140,6 +1140,11 @@ export const dict: Record<I18nKey, string> = {
|
||||
"contextPanel.mode.context": "Contexto",
|
||||
"contextPanel.mode.preview": "Vista previa",
|
||||
"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",
|
||||
"contextPanel.editorEmpty.title": "Ningún archivo abierto",
|
||||
"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.showOpenCodeStatus": "Mostrar estado de OpenCode",
|
||||
"commandPalette.item.toggleMemoryDebug": "Alternar panel de depuración de memoria",
|
||||
"commandPalette.item.pinSession": "Anclar o desanclar sesión",
|
||||
"commandPalette.item.copySessionId": "Copiar ID de sesión",
|
||||
"commandPalette.item.openMultiRun": "Abrir lanzador multi-run",
|
||||
"commandPalette.item.openArchive": "Abrir sesiones archivadas",
|
||||
"commandPalette.item.openNotes": "Abrir panel de notas",
|
||||
"commandPalette.item.openTodos": "Abrir panel de tareas",
|
||||
"commandPalette.item.openSettings": "Abrir configuración...",
|
||||
"commandPalette.session.untitled": "Sesión sin título",
|
||||
"openCodeStatusDialog.title": "Estado de OpenCode",
|
||||
|
||||
@@ -1033,6 +1033,10 @@ export const settingsDict = {
|
||||
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': 'Basculer la surface du panneau contextuel',
|
||||
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0',
|
||||
'settings.openchamber.keyboardShortcuts.action.new_chat.label': 'Nouvelle session',
|
||||
'settings.openchamber.keyboardShortcuts.action.switch_session_previous.label': 'Session précédente',
|
||||
'settings.openchamber.keyboardShortcuts.action.switch_session_next.label': 'Session suivante',
|
||||
'settings.openchamber.keyboardShortcuts.action.rename_current_session.label': 'Renommer la session actuelle',
|
||||
'settings.openchamber.keyboardShortcuts.action.toggle_permission_auto_accept.label': 'Basculer l’approbation automatique',
|
||||
'settings.openchamber.keyboardShortcuts.action.close_session_tab.label': 'Fermer l’onglet de session',
|
||||
'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': 'Nouvelle ébauche d\'worktree',
|
||||
'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': 'Nouvelle fenêtre de mini-chat',
|
||||
|
||||
@@ -959,6 +959,11 @@ export const dict = {
|
||||
'contextPanel.mode.context': 'Contexte',
|
||||
'contextPanel.mode.preview': 'Aperçu',
|
||||
'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',
|
||||
'contextPanel.editorEmpty.title': 'Aucun fichier ouvert',
|
||||
'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.showOpenCodeStatus': 'Afficher le statut OpenCode',
|
||||
'commandPalette.item.toggleMemoryDebug': 'Basculer le panneau de débogage mémoire',
|
||||
'commandPalette.item.pinSession': 'Épingler ou désépingler la session',
|
||||
'commandPalette.item.copySessionId': 'Copier l\'ID de session',
|
||||
'commandPalette.item.openMultiRun': 'Ouvrir le lanceur multi-run',
|
||||
'commandPalette.item.openArchive': 'Ouvrir les sessions archivées',
|
||||
'commandPalette.item.openNotes': 'Ouvrir le panneau de notes',
|
||||
'commandPalette.item.openTodos': 'Ouvrir le panneau de tâches',
|
||||
'commandPalette.item.openSettings': 'Ouvrez les paramètres...',
|
||||
'commandPalette.session.untitled': 'Session sans titre',
|
||||
'openCodeStatusDialog.title': 'Statut OpenCode',
|
||||
|
||||
@@ -1148,6 +1148,10 @@ export const settingsDict = {
|
||||
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': 'コンテキストパネルのサーフェスを切り替え',
|
||||
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0',
|
||||
'settings.openchamber.keyboardShortcuts.action.new_chat.label': '新しい Session',
|
||||
'settings.openchamber.keyboardShortcuts.action.switch_session_previous.label': '前のセッション',
|
||||
'settings.openchamber.keyboardShortcuts.action.switch_session_next.label': '次のセッション',
|
||||
'settings.openchamber.keyboardShortcuts.action.rename_current_session.label': '現在のセッション名を変更',
|
||||
'settings.openchamber.keyboardShortcuts.action.toggle_permission_auto_accept.label': '権限の自動承認を切り替え',
|
||||
'settings.openchamber.keyboardShortcuts.action.close_session_tab.label': 'セッションタブを閉じる',
|
||||
'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': '新しい Worktree 下書き',
|
||||
'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': '新しいミニチャットウィンドウ',
|
||||
|
||||
@@ -1136,6 +1136,11 @@ export const dict: Record<I18nKey, string> = {
|
||||
'contextPanel.mode.context': 'コンテキスト',
|
||||
'contextPanel.mode.preview': 'プレビュー',
|
||||
'contextPanel.mode.browser': 'ブラウザ',
|
||||
'contextRail.configure.open': 'パネルを設定',
|
||||
'contextRail.configure.dialogTitle': 'レールのパネル',
|
||||
'contextRail.configure.dialogDescription': 'レールに表示するパネルを選択します。非表示のパネルもデータは保持され、コマンドパレットから引き続き開けます。',
|
||||
'contextRail.configure.showAll': 'すべて表示',
|
||||
'contextRail.configure.noneWarning': 'すべてのパネルが非表示です。',
|
||||
'contextRail.aria.rail': 'パネルサーフェス',
|
||||
'contextPanel.editorEmpty.title': 'ファイルが開かれていません',
|
||||
'contextPanel.editorEmpty.description': 'ツリーからファイルを選んで編集を始めましょう。',
|
||||
@@ -2481,6 +2486,12 @@ export const dict: Record<I18nKey, string> = {
|
||||
'commandPalette.item.cycleTheme': 'テーマを順に切替',
|
||||
'commandPalette.item.showOpenCodeStatus': 'OpenCode のステータスを表示',
|
||||
'commandPalette.item.toggleMemoryDebug': 'メモリデバッグパネルの切替',
|
||||
'commandPalette.item.pinSession': 'セッションをピン留め/解除',
|
||||
'commandPalette.item.copySessionId': 'セッションIDをコピー',
|
||||
'commandPalette.item.openMultiRun': 'マルチラン起動画面を開く',
|
||||
'commandPalette.item.openArchive': 'アーカイブ済みセッションを開く',
|
||||
'commandPalette.item.openNotes': 'ノートパネルを開く',
|
||||
'commandPalette.item.openTodos': 'ToDoパネルを開く',
|
||||
'commandPalette.item.openSettings': '設定を開く...',
|
||||
'commandPalette.session.untitled': '無題のセッション',
|
||||
'openCodeStatusDialog.title': 'OpenCodeステータス',
|
||||
|
||||
@@ -1115,6 +1115,10 @@ export const settingsDict = {
|
||||
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': '컨텍스트 패널 서피스 전환',
|
||||
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0',
|
||||
'settings.openchamber.keyboardShortcuts.action.new_chat.label': '새 세션',
|
||||
'settings.openchamber.keyboardShortcuts.action.switch_session_previous.label': '이전 세션',
|
||||
'settings.openchamber.keyboardShortcuts.action.switch_session_next.label': '다음 세션',
|
||||
'settings.openchamber.keyboardShortcuts.action.rename_current_session.label': '현재 세션 이름 바꾸기',
|
||||
'settings.openchamber.keyboardShortcuts.action.toggle_permission_auto_accept.label': '권한 자동 승인 전환',
|
||||
'settings.openchamber.keyboardShortcuts.action.close_session_tab.label': '세션 탭 닫기',
|
||||
'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': '새 worktree 초안',
|
||||
'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': '새 Mini Chat 창',
|
||||
|
||||
@@ -1140,6 +1140,11 @@ export const dict: Record<I18nKey, string> = {
|
||||
'contextPanel.mode.context': '컨텍스트',
|
||||
'contextPanel.mode.preview': '미리보기',
|
||||
'contextPanel.mode.browser': '브라우저',
|
||||
'contextRail.configure.open': '패널 구성',
|
||||
'contextRail.configure.dialogTitle': '레일 패널',
|
||||
'contextRail.configure.dialogDescription': '레일에 표시할 패널을 선택하세요. 숨긴 패널의 데이터는 유지되며 명령 팔레트에서 계속 열 수 있습니다.',
|
||||
'contextRail.configure.showAll': '모두 표시',
|
||||
'contextRail.configure.noneWarning': '모든 패널이 숨겨져 있습니다.',
|
||||
'contextRail.aria.rail': '패널 서피스',
|
||||
'contextPanel.editorEmpty.title': '열린 파일 없음',
|
||||
'contextPanel.editorEmpty.description': '트리에서 파일을 선택해 편집을 시작하세요.',
|
||||
@@ -2482,6 +2487,12 @@ export const dict: Record<I18nKey, string> = {
|
||||
'commandPalette.item.cycleTheme': '테마 순환',
|
||||
'commandPalette.item.showOpenCodeStatus': 'OpenCode 상태 표시',
|
||||
'commandPalette.item.toggleMemoryDebug': '메모리 디버그 패널 토글',
|
||||
'commandPalette.item.pinSession': '세션 고정 또는 고정 해제',
|
||||
'commandPalette.item.copySessionId': '세션 ID 복사',
|
||||
'commandPalette.item.openMultiRun': '멀티 런 런처 열기',
|
||||
'commandPalette.item.openArchive': '보관된 세션 열기',
|
||||
'commandPalette.item.openNotes': '노트 패널 열기',
|
||||
'commandPalette.item.openTodos': '할 일 패널 열기',
|
||||
'commandPalette.item.openSettings': '설정... 열기',
|
||||
'commandPalette.session.untitled': '제목 없는 세션',
|
||||
'openCodeStatusDialog.title': 'OpenCode 상태',
|
||||
|
||||
@@ -824,6 +824,10 @@ export const settingsDict = {
|
||||
'settings.openchamber.keyboardShortcuts.action.toggle_prompt_navigator.label': 'Przełącz nawigator promptów',
|
||||
'settings.openchamber.keyboardShortcuts.action.focus_input.label': 'Skup pole wprowadzania',
|
||||
'settings.openchamber.keyboardShortcuts.action.new_chat.label': 'Nowa sesja',
|
||||
'settings.openchamber.keyboardShortcuts.action.switch_session_previous.label': 'Poprzednia sesja',
|
||||
'settings.openchamber.keyboardShortcuts.action.switch_session_next.label': 'Następna sesja',
|
||||
'settings.openchamber.keyboardShortcuts.action.rename_current_session.label': 'Zmień nazwę bieżącej sesji',
|
||||
'settings.openchamber.keyboardShortcuts.action.toggle_permission_auto_accept.label': 'Przełącz automatyczne zatwierdzanie',
|
||||
'settings.openchamber.keyboardShortcuts.action.close_session_tab.label': 'Zamknij kartę sesji',
|
||||
'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': 'Nowy szkic obszaru roboczego',
|
||||
'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': 'Nowe okno Mini Chat',
|
||||
|
||||
@@ -1455,6 +1455,12 @@ export const dict: Record<I18nKey, string> = {
|
||||
'commandPalette.item.cycleTheme': 'Przełącz motyw',
|
||||
'commandPalette.item.showOpenCodeStatus': 'Pokaż status OpenCode',
|
||||
'commandPalette.item.toggleMemoryDebug': 'Przełącz panel debugowania pamięci',
|
||||
'commandPalette.item.pinSession': 'Przypnij lub odepnij sesję',
|
||||
'commandPalette.item.copySessionId': 'Kopiuj ID sesji',
|
||||
'commandPalette.item.openMultiRun': 'Otwórz panel multi-run',
|
||||
'commandPalette.item.openArchive': 'Otwórz zarchiwizowane sesje',
|
||||
'commandPalette.item.openNotes': 'Otwórz panel notatek',
|
||||
'commandPalette.item.openTodos': 'Otwórz panel zadań',
|
||||
'commandPalette.session.untitled': 'Nienazwana sesja',
|
||||
'commandPalette.title': 'Paleta poleceń',
|
||||
'contextPanel.actions.closePanel': 'Zamknij panel',
|
||||
@@ -1472,6 +1478,11 @@ export const dict: Record<I18nKey, string> = {
|
||||
'contextPanel.mode.pr': 'Pull Request',
|
||||
'contextPanel.mode.preview': 'Podgląd',
|
||||
'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',
|
||||
'contextPanel.editorEmpty.title': 'Brak otwartego pliku',
|
||||
'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.suffix": " + 1…0",
|
||||
"settings.openchamber.keyboardShortcuts.action.new_chat.label": "Nova sessão",
|
||||
"settings.openchamber.keyboardShortcuts.action.switch_session_previous.label": "Sessão anterior",
|
||||
"settings.openchamber.keyboardShortcuts.action.switch_session_next.label": "Próxima sessão",
|
||||
"settings.openchamber.keyboardShortcuts.action.rename_current_session.label": "Renomear sessão atual",
|
||||
"settings.openchamber.keyboardShortcuts.action.toggle_permission_auto_accept.label": "Alternar aprovação automática",
|
||||
"settings.openchamber.keyboardShortcuts.action.close_session_tab.label": "Fechar aba da sessão",
|
||||
"settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label": "Novo rascunho de worktree",
|
||||
"settings.openchamber.keyboardShortcuts.action.new_mini_chat.label": "Nova janela Mini Chat",
|
||||
|
||||
@@ -1140,6 +1140,11 @@ export const dict: Record<I18nKey, string> = {
|
||||
"contextPanel.mode.context": "Contexto",
|
||||
"contextPanel.mode.preview": "Prévia",
|
||||
"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",
|
||||
"contextPanel.editorEmpty.title": "Nenhum arquivo aberto",
|
||||
"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.showOpenCodeStatus": "Mostrar status do OpenCode",
|
||||
"commandPalette.item.toggleMemoryDebug": "Alternar painel de depuração de memória",
|
||||
"commandPalette.item.pinSession": "Fixar ou desafixar sessão",
|
||||
"commandPalette.item.copySessionId": "Copiar ID da sessão",
|
||||
"commandPalette.item.openMultiRun": "Abrir lançador multi-run",
|
||||
"commandPalette.item.openArchive": "Abrir sessões arquivadas",
|
||||
"commandPalette.item.openNotes": "Abrir painel de notas",
|
||||
"commandPalette.item.openTodos": "Abrir painel de tarefas",
|
||||
"commandPalette.item.openSettings": "Abrir configurações...",
|
||||
"commandPalette.session.untitled": "Sessão sem título",
|
||||
"openCodeStatusDialog.title": "Status do OpenCode",
|
||||
|
||||
@@ -1115,6 +1115,10 @@ export const settingsDict = {
|
||||
"settings.openchamber.keyboardShortcuts.action.switch_context_surface.label": "Перемкнути поверхню панелі контексту",
|
||||
"settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix": " + 1…0",
|
||||
"settings.openchamber.keyboardShortcuts.action.new_chat.label": "Нова сесія",
|
||||
"settings.openchamber.keyboardShortcuts.action.switch_session_previous.label": "Попередня сесія",
|
||||
"settings.openchamber.keyboardShortcuts.action.switch_session_next.label": "Наступна сесія",
|
||||
"settings.openchamber.keyboardShortcuts.action.rename_current_session.label": "Перейменувати поточну сесію",
|
||||
"settings.openchamber.keyboardShortcuts.action.toggle_permission_auto_accept.label": "Перемкнути авто-дозволи",
|
||||
"settings.openchamber.keyboardShortcuts.action.close_session_tab.label": "Закрити вкладку сесії",
|
||||
"settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label": "Нова чернетка worktree",
|
||||
"settings.openchamber.keyboardShortcuts.action.new_mini_chat.label": "Нове вікно Mini Chat",
|
||||
|
||||
@@ -1140,6 +1140,11 @@ export const dict: Record<I18nKey, string> = {
|
||||
"contextPanel.mode.context": "Контекст",
|
||||
"contextPanel.mode.preview": "Перегляд",
|
||||
"contextPanel.mode.browser": "Браузер",
|
||||
"contextRail.configure.open": "Налаштувати панелі",
|
||||
"contextRail.configure.dialogTitle": "Панелі рейки",
|
||||
"contextRail.configure.dialogDescription": "Обери, які панелі показує рейка. Приховані панелі зберігають дані й доступні з палітри команд.",
|
||||
"contextRail.configure.showAll": "Показати всі",
|
||||
"contextRail.configure.noneWarning": "Усі панелі приховано.",
|
||||
"contextRail.aria.rail": "Поверхні панелі",
|
||||
"contextPanel.editorEmpty.title": "Файл не відкрито",
|
||||
"contextPanel.editorEmpty.description": "Виберіть файл у дереві, щоб почати редагування.",
|
||||
@@ -2448,6 +2453,12 @@ export const dict: Record<I18nKey, string> = {
|
||||
"commandPalette.item.cycleTheme": "Перемкнути тему",
|
||||
"commandPalette.item.showOpenCodeStatus": "Показати статус OpenCode",
|
||||
"commandPalette.item.toggleMemoryDebug": "Показати/сховати панель memory debug",
|
||||
"commandPalette.item.pinSession": "Прикріпити або відкріпити сесію",
|
||||
"commandPalette.item.copySessionId": "Скопіювати ID сесії",
|
||||
"commandPalette.item.openMultiRun": "Відкрити лаунчер multi-run",
|
||||
"commandPalette.item.openArchive": "Відкрити архівовані сесії",
|
||||
"commandPalette.item.openNotes": "Відкрити панель нотаток",
|
||||
"commandPalette.item.openTodos": "Відкрити панель завдань",
|
||||
"commandPalette.item.openSettings": "Відкрити налаштування...",
|
||||
"commandPalette.session.untitled": "Сесія без назви",
|
||||
"openCodeStatusDialog.title": "Статус OpenCode",
|
||||
|
||||
@@ -1115,6 +1115,10 @@ export const settingsDict = {
|
||||
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': '切换上下文面板界面',
|
||||
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0',
|
||||
'settings.openchamber.keyboardShortcuts.action.new_chat.label': '新建会话',
|
||||
'settings.openchamber.keyboardShortcuts.action.switch_session_previous.label': '上一个会话',
|
||||
'settings.openchamber.keyboardShortcuts.action.switch_session_next.label': '下一个会话',
|
||||
'settings.openchamber.keyboardShortcuts.action.rename_current_session.label': '重命名当前会话',
|
||||
'settings.openchamber.keyboardShortcuts.action.toggle_permission_auto_accept.label': '切换权限自动批准',
|
||||
'settings.openchamber.keyboardShortcuts.action.close_session_tab.label': '关闭会话标签页',
|
||||
'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': '新建工作树草稿',
|
||||
'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': '新建 Mini Chat 窗口',
|
||||
|
||||
@@ -1140,6 +1140,11 @@ export const dict: Record<I18nKey, string> = {
|
||||
'contextPanel.mode.context': '上下文',
|
||||
'contextPanel.mode.preview': '预览',
|
||||
'contextPanel.mode.browser': '浏览器',
|
||||
'contextRail.configure.open': '配置面板',
|
||||
'contextRail.configure.dialogTitle': '侧栏面板',
|
||||
'contextRail.configure.dialogDescription': '选择侧栏显示哪些面板。隐藏的面板会保留数据,仍可通过命令面板打开。',
|
||||
'contextRail.configure.showAll': '全部显示',
|
||||
'contextRail.configure.noneWarning': '所有面板均已隐藏。',
|
||||
'contextRail.aria.rail': '面板界面',
|
||||
'contextPanel.editorEmpty.title': '未打开文件',
|
||||
'contextPanel.editorEmpty.description': '从文件树中选择一个文件开始编辑。',
|
||||
@@ -2448,6 +2453,12 @@ export const dict: Record<I18nKey, string> = {
|
||||
'commandPalette.item.cycleTheme': '轮换主题',
|
||||
'commandPalette.item.showOpenCodeStatus': '显示 OpenCode 状态',
|
||||
'commandPalette.item.toggleMemoryDebug': '切换内存调试面板',
|
||||
'commandPalette.item.pinSession': '固定或取消固定会话',
|
||||
'commandPalette.item.copySessionId': '复制会话 ID',
|
||||
'commandPalette.item.openMultiRun': '打开多任务启动器',
|
||||
'commandPalette.item.openArchive': '打开已归档会话',
|
||||
'commandPalette.item.openNotes': '打开笔记面板',
|
||||
'commandPalette.item.openTodos': '打开待办面板',
|
||||
'commandPalette.item.openSettings': '打开设置...',
|
||||
'commandPalette.session.untitled': '未命名会话',
|
||||
'openCodeStatusDialog.title': 'OpenCode 状态',
|
||||
|
||||
@@ -1022,6 +1022,10 @@ export const settingsDict = {
|
||||
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': '切換上下文面板介面',
|
||||
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0',
|
||||
'settings.openchamber.keyboardShortcuts.action.new_chat.label': '新建工作階段',
|
||||
'settings.openchamber.keyboardShortcuts.action.switch_session_previous.label': '上一個工作階段',
|
||||
'settings.openchamber.keyboardShortcuts.action.switch_session_next.label': '下一個工作階段',
|
||||
'settings.openchamber.keyboardShortcuts.action.rename_current_session.label': '重新命名目前的工作階段',
|
||||
'settings.openchamber.keyboardShortcuts.action.toggle_permission_auto_accept.label': '切換權限自動核准',
|
||||
'settings.openchamber.keyboardShortcuts.action.close_session_tab.label': '關閉工作階段分頁',
|
||||
'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': '新增 worktree 草稿',
|
||||
'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': '新建 Mini Chat 視窗',
|
||||
|
||||
@@ -1152,6 +1152,11 @@ export const dict: Record<I18nKey, string> = {
|
||||
'contextPanel.mode.context': '上下文',
|
||||
'contextPanel.mode.preview': '預覽',
|
||||
'contextPanel.mode.browser': '瀏覽器',
|
||||
'contextRail.configure.open': '設定面板',
|
||||
'contextRail.configure.dialogTitle': '側欄面板',
|
||||
'contextRail.configure.dialogDescription': '選擇側欄顯示哪些面板。隱藏的面板會保留資料,仍可透過命令面板開啟。',
|
||||
'contextRail.configure.showAll': '全部顯示',
|
||||
'contextRail.configure.noneWarning': '所有面板皆已隱藏。',
|
||||
'contextRail.aria.rail': '面板介面',
|
||||
'contextPanel.editorEmpty.title': '未開啟檔案',
|
||||
'contextPanel.editorEmpty.description': '從檔案樹選擇檔案開始編輯。',
|
||||
@@ -2452,6 +2457,12 @@ export const dict: Record<I18nKey, string> = {
|
||||
'commandPalette.item.cycleTheme': '輪換主題',
|
||||
'commandPalette.item.showOpenCodeStatus': '顯示 OpenCode 狀態',
|
||||
'commandPalette.item.toggleMemoryDebug': '切換記憶體偵錯面板',
|
||||
'commandPalette.item.pinSession': '釘選或取消釘選會話',
|
||||
'commandPalette.item.copySessionId': '複製會話 ID',
|
||||
'commandPalette.item.openMultiRun': '開啟多任務啟動器',
|
||||
'commandPalette.item.openArchive': '開啟已封存會話',
|
||||
'commandPalette.item.openNotes': '開啟筆記面板',
|
||||
'commandPalette.item.openTodos': '開啟待辦面板',
|
||||
'commandPalette.item.openSettings': '開啟設定...',
|
||||
'commandPalette.session.untitled': '未命名會話',
|
||||
'openCodeStatusDialog.title': 'OpenCode 狀態',
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -26,6 +26,28 @@ export const activateSessionTabByIndex = (index: number): boolean => {
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Activate the tab one step right (+1) or left (-1) of the current session
|
||||
* in the rendered strip order, wrapping around the ends. Returns false when
|
||||
* the current session has no tab or there is nothing to move to.
|
||||
*/
|
||||
export const activateAdjacentSessionTab = (delta: -1 | 1): boolean => {
|
||||
const { tabIds } = useSessionTabsStore.getState();
|
||||
const { currentSessionId, setCurrentSession } = useSessionUIStore.getState();
|
||||
const sessionsById = new Map(
|
||||
useGlobalSessionsStore.getState().activeSessions.map((session) => [session.id, session] as const),
|
||||
);
|
||||
const renderable = tabIds.filter((id) => sessionsById.has(id));
|
||||
if (!currentSessionId || renderable.length < 2) return false;
|
||||
const index = renderable.indexOf(currentSessionId);
|
||||
if (index === -1) return false;
|
||||
const nextId = renderable[(index + delta + renderable.length) % renderable.length];
|
||||
const next = sessionsById.get(nextId);
|
||||
if (!next) return false;
|
||||
setCurrentSession(next.id, resolveGlobalSessionDirectory(next));
|
||||
return true;
|
||||
};
|
||||
|
||||
export const closeSessionTabAndActivateNeighbour = (sessionId: string): void => {
|
||||
const { tabIds, closeTab } = useSessionTabsStore.getState();
|
||||
if (!tabIds.includes(sessionId)) return;
|
||||
|
||||
@@ -51,6 +51,34 @@ const SHORTCUT_GROUPS = {
|
||||
customizable: true,
|
||||
settingsLabelKey: 'settings.openchamber.keyboardShortcuts.action.new_chat.label',
|
||||
},
|
||||
{
|
||||
id: 'switch_session_previous',
|
||||
defaultBinding: 'mod+alt+arrowleft',
|
||||
customizable: true,
|
||||
settingsLabelKey:
|
||||
'settings.openchamber.keyboardShortcuts.action.switch_session_previous.label',
|
||||
},
|
||||
{
|
||||
id: 'switch_session_next',
|
||||
defaultBinding: 'mod+alt+arrowright',
|
||||
customizable: true,
|
||||
settingsLabelKey:
|
||||
'settings.openchamber.keyboardShortcuts.action.switch_session_next.label',
|
||||
},
|
||||
{
|
||||
id: 'rename_current_session',
|
||||
defaultBinding: 'mod+k r',
|
||||
customizable: true,
|
||||
settingsLabelKey:
|
||||
'settings.openchamber.keyboardShortcuts.action.rename_current_session.label',
|
||||
},
|
||||
{
|
||||
id: 'toggle_permission_auto_accept',
|
||||
defaultBinding: 'mod+k a',
|
||||
customizable: true,
|
||||
settingsLabelKey:
|
||||
'settings.openchamber.keyboardShortcuts.action.toggle_permission_auto_accept.label',
|
||||
},
|
||||
{
|
||||
id: 'close_session_tab',
|
||||
defaultBinding: 'alt+w',
|
||||
|
||||
@@ -22,7 +22,10 @@ edge (`components/layout/ContextPanelRail.tsx`) and rendered by
|
||||
registry's default order and appends any missing surfaces.
|
||||
- `getVisibleContextRailSurfaces` is the single visibility filter shared by the
|
||||
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
|
||||
`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
|
||||
|
||||
@@ -187,6 +187,9 @@ export const sortContextSurfaces = (railOrder: readonly string[]): ContextSurfac
|
||||
|
||||
type VisibleRailSurfacesOptions = {
|
||||
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;
|
||||
isVSCode: boolean;
|
||||
screenWidth: number;
|
||||
@@ -203,6 +206,9 @@ type VisibleRailSurfacesOptions = {
|
||||
*/
|
||||
export const getVisibleContextRailSurfaces = (options: VisibleRailSurfacesOptions): ContextSurfaceDescriptor[] => {
|
||||
return sortContextSurfaces(options.railOrder).filter((surface) => {
|
||||
if (options.hiddenSurfaces?.includes(surface.id)) {
|
||||
return false;
|
||||
}
|
||||
if (surface.id === 'plan' && !options.planModeEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -607,6 +607,9 @@ interface UIStore {
|
||||
hasManuallyResizedLeftSidebar: boolean;
|
||||
contextPanelByDirectory: Record<string, ContextPanelDirectoryState>;
|
||||
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;
|
||||
contextEditorTreeWidth: number;
|
||||
notesPanelHeight: number;
|
||||
@@ -828,6 +831,8 @@ interface UIStore {
|
||||
setWorkStatusOverlayOpen: (open: boolean) => void;
|
||||
setWorkStatusSectionVisible: (sectionId: string, visible: boolean) => void;
|
||||
setWorkStatusHiddenSections: (sectionIds: string[]) => void;
|
||||
setContextRailSurfaceVisible: (surfaceId: string, visible: boolean) => void;
|
||||
setContextRailHiddenSurfaces: (surfaceIds: string[]) => void;
|
||||
setSessionSwitcherOpen: (open: boolean) => void;
|
||||
setSessionDropdownOpen: (open: boolean) => void;
|
||||
setPendingDiffFile: (filePath: string | null, staged?: boolean, scope?: PendingDiffScope | null) => void;
|
||||
@@ -990,6 +995,7 @@ export const useUIStore = create<UIStore>()(
|
||||
hasManuallyResizedLeftSidebar: false,
|
||||
contextPanelByDirectory: {},
|
||||
contextRailOrder: [],
|
||||
contextRailHiddenSurfaces: [],
|
||||
contextEditorTreeVisible: true,
|
||||
contextEditorTreeWidth: 240,
|
||||
notesPanelHeight: 112,
|
||||
@@ -1599,6 +1605,23 @@ export const useUIStore = create<UIStore>()(
|
||||
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) => {
|
||||
if (get().isSessionSwitcherOpen === open) {
|
||||
@@ -2627,6 +2650,9 @@ export const useUIStore = create<UIStore>()(
|
||||
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 as unknown[]).filter((id): id is string => typeof id === 'string' && id.trim() !== '')
|
||||
: [];
|
||||
@@ -2639,6 +2665,7 @@ export const useUIStore = create<UIStore>()(
|
||||
sidebarWidth: state.sidebarWidth,
|
||||
contextPanelByDirectory: state.contextPanelByDirectory,
|
||||
contextRailOrder: state.contextRailOrder,
|
||||
contextRailHiddenSurfaces: state.contextRailHiddenSurfaces,
|
||||
contextEditorTreeVisible: state.contextEditorTreeVisible,
|
||||
contextEditorTreeWidth: state.contextEditorTreeWidth,
|
||||
notesPanelHeight: state.notesPanelHeight,
|
||||
|
||||
Reference in New Issue
Block a user