fix(desktop): toggle browser icon and preserve webview state on coll… (#1424)

* fix(desktop): toggle browser icon and preserve webview state on collapse

* fix(desktop): stabilize context panel collapse behavior

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
HearingSmile
2026-05-26 20:30:16 +03:00
committed by GitHub
co-authored by Bohdan Triapitsyn
parent 51901b980e
commit 6a173211b0
3 changed files with 92 additions and 23 deletions
@@ -23,7 +23,7 @@ import { Icon } from "@/components/icon/Icon";
import { OpenChamberLogo } from "@/components/ui/OpenChamberLogo";
import { invokeDesktopCommand } from '@/lib/desktopNative';
const CONTEXT_PANEL_MIN_WIDTH = 360;
const CONTEXT_PANEL_MIN_WIDTH = 380;
const CONTEXT_PANEL_MAX_WIDTH = 1400;
const CONTEXT_PANEL_DEFAULT_WIDTH = 600;
const CONTEXT_TAB_LABEL_MAX_CHARS = 24;
@@ -1588,6 +1588,7 @@ export const ContextPanel: React.FC = () => {
const width = clampWidth(panelState?.width ?? CONTEXT_PANEL_DEFAULT_WIDTH);
const [isResizing, setIsResizing] = React.useState(false);
const [suppressWidthTransition, setSuppressWidthTransition] = React.useState(false);
const startXRef = React.useRef(0);
const startWidthRef = React.useRef(width);
const resizingWidthRef = React.useRef<number | null>(null);
@@ -1595,6 +1596,41 @@ export const ContextPanel: React.FC = () => {
const panelRef = React.useRef<HTMLElement | null>(null);
const chatFrameRefs = React.useRef<Map<string, HTMLIFrameElement>>(new Map());
const wasOpenRef = React.useRef(false);
const previousIsOpenRef = React.useRef(isOpen);
const suppressWidthTransitionFrameRef = React.useRef<number | null>(null);
const suppressWidthTransitionForFrame = React.useCallback(() => {
setSuppressWidthTransition(true);
if (suppressWidthTransitionFrameRef.current !== null) {
window.cancelAnimationFrame(suppressWidthTransitionFrameRef.current);
}
suppressWidthTransitionFrameRef.current = window.requestAnimationFrame(() => {
suppressWidthTransitionFrameRef.current = null;
setSuppressWidthTransition(false);
});
}, []);
React.useEffect(() => () => {
if (suppressWidthTransitionFrameRef.current !== null) {
window.cancelAnimationFrame(suppressWidthTransitionFrameRef.current);
}
}, []);
React.useLayoutEffect(() => {
const wasOpen = previousIsOpenRef.current;
previousIsOpenRef.current = isOpen;
if (!isOpen) {
setSuppressWidthTransition(false);
return;
}
if (wasOpen) {
return;
}
suppressWidthTransitionForFrame();
}, [isOpen, suppressWidthTransitionForFrame]);
React.useEffect(() => {
if (!isOpen || wasOpenRef.current) {
@@ -1666,11 +1702,13 @@ export const ContextPanel: React.FC = () => {
}
const finalWidth = clampWidthToAvailableSpace(resizingWidthRef.current ?? width, panelRef.current);
suppressWidthTransitionForFrame();
applyLiveWidth(finalWidth);
resizingWidthRef.current = finalWidth;
setContextPanelWidth(directoryKey, finalWidth);
setIsResizing(false);
activeResizePointerIDRef.current = null;
resizingWidthRef.current = null;
setContextPanelWidth(directoryKey, finalWidth);
}, [directoryKey, setContextPanelWidth, width]);
}, [applyLiveWidth, directoryKey, setContextPanelWidth, suppressWidthTransitionForFrame, width]);
React.useEffect(() => {
if (!isResizing) {
@@ -1910,36 +1948,44 @@ export const ContextPanel: React.FC = () => {
</header>
);
if (!isOpen) {
return null;
}
const panelStyle: React.CSSProperties = isExpanded
const panelStyle: React.CSSProperties = !isOpen
? {
['--oc-context-panel-width' as string]: '100%',
width: '100%',
minWidth: '100%',
maxWidth: '100%',
}
: {
width: 'min(var(--oc-context-panel-width), 100%)',
minWidth: `min(${CONTEXT_PANEL_MIN_WIDTH}px, 100%)`,
maxWidth: '100%',
['--oc-context-panel-width' as string]: `${isResizing ? (resizingWidthRef.current ?? width) : width}px`,
};
width: 0,
minWidth: 0,
maxWidth: 0,
opacity: 0,
overflow: 'hidden',
visibility: 'hidden',
}
: isExpanded
? {
['--oc-context-panel-width' as string]: '100%',
width: '100%',
minWidth: '100%',
maxWidth: '100%',
}
: {
width: 'min(var(--oc-context-panel-width), 100%)',
minWidth: `min(${CONTEXT_PANEL_MIN_WIDTH}px, 100%)`,
maxWidth: '100%',
['--oc-context-panel-width' as string]: `${isResizing ? (resizingWidthRef.current ?? width) : width}px`,
};
return (
<aside
ref={panelRef}
data-context-panel="true"
tabIndex={-1}
inert={!isOpen || undefined}
className={cn(
'flex min-h-0 flex-col overflow-hidden bg-background',
!isExpanded && 'border-l border-border/40',
isExpanded
? 'absolute inset-0 z-20 min-w-0'
: 'relative h-full flex-shrink-0',
isResizing ? 'transition-none' : 'transition-[width] duration-200 ease-in-out'
!isOpen && 'pointer-events-none',
isResizing || !isOpen || suppressWidthTransition ? 'transition-none' : 'transition-[width] duration-200 ease-in-out'
)}
onKeyDownCapture={handlePanelKeyDownCapture}
style={panelStyle}
+25 -2
View File
@@ -81,6 +81,7 @@ type HeaderIconActionButtonProps = {
className?: string;
Icon: IconName;
iconClassName?: string;
pressed?: boolean;
};
const HeaderIconActionButton = React.memo(function HeaderIconActionButton({
@@ -91,6 +92,7 @@ const HeaderIconActionButton = React.memo(function HeaderIconActionButton({
className,
Icon: iconName,
iconClassName,
pressed = false,
}: HeaderIconActionButtonProps) {
if (!visible) {
return null;
@@ -103,7 +105,11 @@ const HeaderIconActionButton = React.memo(function HeaderIconActionButton({
type="button"
onClick={onClick}
aria-label={ariaLabel}
className={className ?? DESKTOP_HEADER_ICON_BUTTON_CLASS}
aria-pressed={pressed}
className={cn(
className ?? DESKTOP_HEADER_ICON_BUTTON_CLASS,
pressed && 'bg-interactive-selection text-interactive-selection-foreground'
)}
>
<Icon name={iconName} className={iconClassName ?? 'h-[18px] w-[18px]'} />
</button>
@@ -1426,8 +1432,15 @@ export const Header: React.FC<HeaderProps> = ({
if (!directory) {
return;
}
const panelState = contextPanelByDirectory[directory];
if (getActiveContextMode(panelState) === 'browser') {
closeContextPanel(directory);
return;
}
openContextBrowser(directory);
}, [openContextBrowser, openDirectory]);
}, [closeContextPanel, contextPanelByDirectory, openContextBrowser, openDirectory]);
const isContextPlanActive = React.useMemo(() => {
const directory = normalize(openDirectory || '');
@@ -1438,6 +1451,15 @@ export const Header: React.FC<HeaderProps> = ({
return getActiveContextMode(panelState) === 'plan';
}, [contextPanelByDirectory, openDirectory]);
const isContextBrowserActive = React.useMemo(() => {
const directory = normalize(openDirectory || '');
if (!directory) {
return false;
}
const panelState = contextPanelByDirectory[directory];
return getActiveContextMode(panelState) === 'browser';
}, [contextPanelByDirectory, openDirectory]);
const desktopHeaderIconButtonClass = DESKTOP_HEADER_ICON_BUTTON_CLASS;
const mobileHeaderIconButtonClass = MOBILE_HEADER_ICON_BUTTON_CLASS;
const mobileActiveHeaderItem = React.useMemo(() => {
@@ -1943,6 +1965,7 @@ export const Header: React.FC<HeaderProps> = ({
title={t('contextPanel.browser.open')}
ariaLabel={t('contextPanel.browser.open')}
onClick={handleOpenContextBrowser}
pressed={isContextBrowserActive}
Icon={'global'}
/>
) : null}
+1 -1
View File
@@ -329,7 +329,7 @@ const upsertContextPanelTab = (
? {
...tab,
mode: nextTab.mode,
targetPath: nextTab.targetPath,
targetPath: nextTab.targetPath || tab.targetPath,
dedupeKey: nextTab.dedupeKey,
label: nextTab.label,
stagedDiff: nextTab.stagedDiff,