Merge pull request #245 from btriapitsyn/fix-traffic-lights

Adjust macOS traffic lights handling and header padding
This commit is contained in:
Iuliia Ivashko
2026-01-30 02:58:51 +02:00
committed by GitHub
10 changed files with 103 additions and 66 deletions
@@ -755,16 +755,20 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
onContentChange?.('structural');
}, [isUser, onContentChange]);
const setImagePreviewOpen = useUIStore((state) => state.setImagePreviewOpen);
const handleShowPopup = React.useCallback((content: ToolPopupContent) => {
if (content.image) {
setPopupContent(content);
setImagePreviewOpen(true);
}
}, []);
}, [setImagePreviewOpen]);
const handlePopupChange = React.useCallback((open: boolean) => {
setPopupContent((prev) => ({ ...prev, open }));
}, []);
setImagePreviewOpen(open);
}, [setImagePreviewOpen]);
const isAnimationSettled = Boolean(getMessageInfoProp(message.info, 'animationSettled'));
const isStreamingPhase = streamPhase === 'streaming';
+42 -3
View File
@@ -107,6 +107,31 @@ export const Header: React.FC = () => {
return /Macintosh|Mac OS X/.test(navigator.userAgent || '');
}, []);
const macosMajorVersion = React.useMemo(() => {
if (typeof window === 'undefined') {
return null;
}
// Use Tauri-provided version if available (accurate), otherwise fall back to UA parsing
const desktopApi = (window as typeof window & { opencodeDesktop?: { macosMajorVersion?: number | null } }).opencodeDesktop;
if (desktopApi?.macosMajorVersion != null) {
return desktopApi.macosMajorVersion;
}
// Fallback: WebKit reports "Mac OS X 10_15_7" format where 10 is legacy prefix
if (typeof navigator === 'undefined') {
return null;
}
const match = (navigator.userAgent || '').match(/Mac OS X (\d+)[._](\d+)/);
if (!match) {
return null;
}
const first = Number.parseInt(match[1], 10);
const second = Number.parseInt(match[2], 10);
if (Number.isNaN(first)) {
return null;
}
return first === 10 ? second : first;
}, []);
useEffect(() => {
if (typeof window === 'undefined') {
return;
@@ -284,11 +309,24 @@ export const Header: React.FC = () => {
const desktopPaddingClass = React.useMemo(() => {
if (isDesktopApp && isMacPlatform) {
// Always reserve space for Mac traffic lights since header is always on top
return 'pl-[5.75rem]';
return 'pl-[5.5rem]';
}
return 'pl-3';
}, [isDesktopApp, isMacPlatform]);
const macosHeaderSizeClass = React.useMemo(() => {
if (!isDesktopApp || !isMacPlatform || macosMajorVersion === null) {
return '';
}
if (macosMajorVersion >= 26) {
return 'h-12';
}
if (macosMajorVersion <= 15) {
return 'h-14';
}
return '';
}, [isDesktopApp, isMacPlatform, macosMajorVersion]);
const updateHeaderHeight = React.useCallback(() => {
if (typeof document === 'undefined') {
return;
@@ -329,7 +367,7 @@ export const Header: React.FC = () => {
useEffect(() => {
updateHeaderHeight();
}, [updateHeaderHeight, isMobile]);
}, [updateHeaderHeight, isMobile, macosHeaderSizeClass]);
const handleDragStart = React.useCallback(async (e: React.MouseEvent) => {
if ((e.target as HTMLElement).closest('button, a, input, select, textarea')) {
@@ -463,7 +501,8 @@ export const Header: React.FC = () => {
onMouseDown={handleDragStart}
className={cn(
'app-region-drag relative flex h-12 select-none items-center',
desktopPaddingClass
desktopPaddingClass,
macosHeaderSizeClass
)}
role="tablist"
aria-label="Main navigation"
@@ -116,7 +116,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
const desktopHeaderPaddingClass = React.useMemo(() => {
if (isDesktopApp && isMacPlatform) {
// Match main app header: reserve space for Mac traffic lights.
return 'pl-[5.75rem]';
return 'pl-[5.5rem]';
}
return 'pl-3';
}, [isDesktopApp, isMacPlatform]);
@@ -139,6 +139,20 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
}
}, [isDesktopApp]);
// Handle ESC key to dismiss
React.useEffect(() => {
if (!onCancel) return;
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
e.preventDefault();
e.stopPropagation();
onCancel();
}
};
window.addEventListener('keydown', handleKeyDown, true);
return () => window.removeEventListener('keydown', handleKeyDown, true);
}, [onCancel]);
// Use the BranchSelector hook for branch state management
const [worktreeBaseBranch, setWorktreeBaseBranch] = React.useState<string>('HEAD');
const { isLoading: isLoadingWorktreeBaseBranches, isGitRepository } = useBranchOptions(currentDirectory);
@@ -306,31 +320,27 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
<header
onMouseDown={handleDragStart}
className={cn(
'flex h-12 items-center justify-between border-b app-region-drag select-none',
'relative flex h-12 items-center justify-center border-b app-region-drag select-none',
desktopHeaderPaddingClass
)}
style={{ borderColor: 'var(--interactive-border)' }}
>
<div
className="flex items-center gap-3"
>
<h1 className="typography-ui-label font-medium">New Multi-Run</h1>
</div>
<h1 className="typography-ui-label font-medium">New Multi-Run</h1>
{onCancel && (
<div className="flex items-center pr-3">
<div className="absolute right-0 flex items-center pr-3">
<Tooltip delayDuration={500}>
<TooltipTrigger asChild>
<button
type="button"
onClick={onCancel}
aria-label="Close"
aria-label="Close (Esc)"
className="inline-flex h-9 w-9 items-center justify-center p-2 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary app-region-no-drag"
>
<RiCloseLine className="h-5 w-5" />
</button>
</TooltipTrigger>
<TooltipContent>
<p>Close</p>
<p>Close (Esc)</p>
</TooltipContent>
</Tooltip>
</div>
@@ -1234,17 +1234,17 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
disabled={isMissingDirectory}
onClick={() => handleSessionSelect(session.id, sessionDirectory, isMissingDirectory, projectId)}
className={cn(
'flex min-w-0 flex-1 flex-col gap-0 rounded-sm text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 text-foreground select-none',
'flex min-w-0 flex-1 flex-col gap-0 overflow-hidden rounded-sm text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 text-foreground select-none',
)}
>
{}
<div className="flex items-center gap-2 min-w-0 flex-1">
<div className="flex w-full items-center gap-2 min-w-0 flex-1 overflow-hidden">
{isStreaming ? (
<GridLoader size="xs" className="text-primary flex-shrink-0" />
) : null}
<span className="truncate typography-ui-label font-normal text-foreground">
<div className="block min-w-0 flex-1 truncate typography-ui-label font-normal text-foreground">
{sessionTitle}
</span>
</div>
{pendingPermissionCount > 0 ? (
<span
@@ -226,6 +226,8 @@ export const useKeyboardShortcuts = () => {
isHelpDialogOpen,
isSessionSwitcherOpen,
isAboutDialogOpen,
isMultiRunLauncherOpen,
isImagePreviewOpen,
activeMainTab,
} = useUIStore.getState();
@@ -238,7 +240,7 @@ export const useKeyboardShortcuts = () => {
}
// Check if any overlay is open or not on chat tab - don't process abort
const hasOverlay = isCommandPaletteOpen || isHelpDialogOpen || isSessionSwitcherOpen || isAboutDialogOpen;
const hasOverlay = isCommandPaletteOpen || isHelpDialogOpen || isSessionSwitcherOpen || isAboutDialogOpen || isMultiRunLauncherOpen || isImagePreviewOpen;
const isChatActive = activeMainTab === 'chat';
if (hasOverlay || !isChatActive) {
+1
View File
@@ -89,6 +89,7 @@ export type DesktopSettingsApi = {
export type DesktopApi = {
homeDirectory?: string;
macosMajorVersion?: number | null;
getServerInfo: () => Promise<DesktopServerInfo>;
restartOpenCode: () => Promise<{ success: boolean }>;
shutdown: () => Promise<{ success: boolean }>;
+7
View File
@@ -62,6 +62,7 @@ interface UIStore {
diffWrapLines: boolean;
diffViewMode: 'single' | 'stacked';
isTimelineDialogOpen: boolean;
isImagePreviewOpen: boolean;
nativeNotificationsEnabled: boolean;
notificationMode: 'always' | 'hidden-only';
@@ -113,6 +114,7 @@ interface UIStore {
setDiffViewMode: (mode: 'single' | 'stacked') => void;
setMultiRunLauncherOpen: (open: boolean) => void;
setTimelineDialogOpen: (open: boolean) => void;
setImagePreviewOpen: (open: boolean) => void;
setNativeNotificationsEnabled: (value: boolean) => void;
setNotificationMode: (mode: 'always' | 'hidden-only') => void;
openMultiRunLauncher: () => void;
@@ -165,6 +167,7 @@ export const useUIStore = create<UIStore>()(
diffWrapLines: false,
diffViewMode: 'stacked',
isTimelineDialogOpen: false,
isImagePreviewOpen: false,
nativeNotificationsEnabled: false,
notificationMode: 'hidden-only',
@@ -534,6 +537,10 @@ export const useUIStore = create<UIStore>()(
set({ isTimelineDialogOpen: open });
},
setImagePreviewOpen: (open) => {
set({ isImagePreviewOpen: open });
},
setNativeNotificationsEnabled: (value) => {
set({ nativeNotificationsEnabled: value });
},