💄 style(desktop): move panel toggle into titlebar top-left (#15515)

*  feat(desktop): move panel toggle into titlebar top-left

Place a persistent collapse/expand toggle at the titlebar's top-left
corner on desktop, to the right of the macOS traffic lights. The
NavigationBar now splits into a left group (toggle) and a right group
(back / forward / clock) with space-between: expanded, the right group
hugs the sidebar's right edge; collapsed, the controls cluster at the
left edge like codex.

ToggleLeftPanelButton gains an optional `id` prop so the titlebar
instance can opt out of the shared TOGGLE_BUTTON_ID, avoiding a
duplicate DOM id and NavPanelDraggable's hover-reveal CSS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(desktop): expand untracked directories in git status

`git status --porcelain` defaults to `--untracked-files=normal`, which
collapses whole untracked directories into a single `?? path/` entry.
That trailing-slash path then flowed into `readUntrackedAsPatch` as if
it were a file — `stat()` reported `isFile()=false`, an empty patch was
returned, and the Review panel rendered "无法加载该文件的 diff" against
a directory row. Pass `-u` so git expands those directories into their
individual files; each file then produces a real synthetic patch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 💄 style(desktop): scope titlebar toggle to macOS, hide in-page toggles there

The persistent titlebar toggle now renders only on macOS; Windows/Linux
keep the original right-aligned navigation controls and their in-page
toggles.

On macOS desktop, ToggleLeftPanelButton instances hide themselves (the
titlebar owns the control) unless `forceVisible` is set, removing the
now-redundant sidebar-header and content-header toggles. NavHeader also
skips rendering its empty toggle-only bar in this case.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Arvin Xu
2026-06-07 00:42:57 +08:00
committed by GitHub
parent 7b54edc665
commit 573cc5b798
4 changed files with 54 additions and 9 deletions
+3 -3
View File
@@ -637,7 +637,7 @@ export default class GitController extends ControllerModule {
async getGitWorkingTreeStatus(dirPath: string): Promise<GitWorkingTreeStatus> {
const execFileAsync = promisify(execFile);
try {
const { stdout } = await execFileAsync('git', ['status', '--porcelain', '-z'], {
const { stdout } = await execFileAsync('git', ['status', '--porcelain', '-u', '-z'], {
cwd: dirPath,
timeout: 5000,
});
@@ -689,7 +689,7 @@ export default class GitController extends ControllerModule {
const modified: string[] = [];
const deleted: string[] = [];
try {
const { stdout } = await execFileAsync('git', ['status', '--porcelain', '-z'], {
const { stdout } = await execFileAsync('git', ['status', '--porcelain', '-u', '-z'], {
cwd: dirPath,
timeout: 5000,
});
@@ -830,7 +830,7 @@ export default class GitController extends ControllerModule {
const entries: Entry[] = [];
const submoduleDirtyEntries: Entry[] = [];
try {
const { stdout } = await execFileAsync('git', ['status', '--porcelain', '-z'], {
const { stdout } = await execFileAsync('git', ['status', '--porcelain', '-u', '-z'], {
cwd: dirPath,
timeout: 5000,
});
@@ -6,6 +6,7 @@ import { ArrowLeft, ArrowRight, Clock } from 'lucide-react';
import { memo, useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import ToggleLeftPanelButton from '@/features/NavPanel/ToggleLeftPanelButton';
import { useGlobalStore } from '@/store/global';
import type { GlobalState } from '@/store/global/initialState';
import { systemStatusSelectors } from '@/store/global/selectors';
@@ -13,10 +14,21 @@ import { electronStylish } from '@/styles/electron';
import { isMacOS } from '@/utils/platform';
import { useNavigationHistory } from '../navigation/useNavigationHistory';
import { TITLE_BAR_HORIZONTAL_PADDING } from './layout';
import RecentlyViewed from './RecentlyViewed';
const isMac = isMacOS();
// Reserve space for macOS traffic lights so the toggle sits to their right.
// Matches the popup TitleBar's MAC_TRAFFIC_LIGHT_WIDTH (80) minus the titlebar's
// own horizontal padding, which already offsets the left edge.
const MAC_TRAFFIC_LIGHT_WIDTH = 80;
const macTrafficLightPadding = MAC_TRAFFIC_LIGHT_WIDTH - TITLE_BAR_HORIZONTAL_PADDING;
// A persistent titlebar toggle must not share the sidebar toggle's id, or it
// would create a duplicate DOM id and get caught by NavPanelDraggable's hover CSS.
const NAV_TOGGLE_ID = 'titlebar_toggle_left_panel_button';
const navPanelSelector = (s: GlobalState) => {
const showLeftPanel = systemStatusSelectors.showLeftPanel(s);
if (!showLeftPanel) return 0;
@@ -74,13 +86,24 @@ const NavigationBar = memo(() => {
horizontal
align="center"
data-width={leftPanelWidth}
justify="end"
gap={8}
justify={isMac ? 'space-between' : 'end'}
style={{
paddingLeft: isMac ? macTrafficLightPadding : 0,
paddingRight: 8,
width: isLeftPanelVisible ? `${leftPanelWidth - 12}px` : '150px',
// Expanded: span the sidebar width so the right group hugs its right edge.
// Collapsed (macOS): shrink to content so the controls cluster at the left edge.
width: isLeftPanelVisible ? `${leftPanelWidth - 12}px` : isMac ? 'auto' : '150px',
transition: !isLeftPanelVisible ? 'width 0.2s' : 'none',
}}
>
{/* The persistent panel toggle is macOS-only; other platforms keep the
in-page toggles, so the titlebar shows just the navigation controls. */}
{isMac && (
<Flexbox horizontal align="center" className={electronStylish.nodrag}>
<ToggleLeftPanelButton forceVisible id={NAV_TOGGLE_ID} size="small" />
</Flexbox>
)}
<Flexbox horizontal align="center" className={electronStylish.nodrag} gap={2}>
<ActionIcon disabled={!canGoBack} icon={ArrowLeft} size="small" onClick={goBack} />
<ActionIcon disabled={!canGoForward} icon={ArrowRight} size="small" onClick={goForward} />
+4 -2
View File
@@ -3,7 +3,7 @@ import { Flexbox, TooltipGroup } from '@lobehub/ui';
import { type CSSProperties, type ReactNode } from 'react';
import { memo } from 'react';
import ToggleLeftPanelButton from '@/features/NavPanel/ToggleLeftPanelButton';
import ToggleLeftPanelButton, { isMacDesktop } from '@/features/NavPanel/ToggleLeftPanelButton';
import { useGlobalStore } from '@/store/global';
import { systemStatusSelectors } from '@/store/global/selectors';
@@ -39,7 +39,9 @@ const NavHeader = memo<NavHeaderProps>(
const noContent = !left && !right;
if (noContent && expand) return;
// When empty, this header only rendered to host the collapse toggle. Hide it
// when expanded, and also on macOS desktop where the toggle moved to the titlebar.
if (noContent && (expand || isMacDesktop)) return;
return (
<Flexbox
@@ -9,22 +9,40 @@ import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { DESKTOP_HEADER_ICON_SMALL_SIZE } from '@/const/layoutTokens';
import { isDesktop } from '@/const/version';
import { useGlobalStore } from '@/store/global';
import { systemStatusSelectors } from '@/store/global/selectors';
import { useUserStore } from '@/store/user';
import { settingsSelectors } from '@/store/user/selectors';
import { isMacOS } from '@/utils/platform';
export const TOGGLE_BUTTON_ID = 'toggle_left_panel_button';
// On macOS desktop a persistent toggle lives in the titlebar (NavigationBar),
// so in-page instances hide themselves to avoid duplicate collapse buttons.
export const isMacDesktop = isDesktop && isMacOS();
interface ToggleLeftPanelButtonProps {
/**
* Render even on macOS desktop, where in-page instances hide by default.
* The persistent titlebar toggle sets this.
*/
forceVisible?: boolean;
icon?: ActionIconProps['icon'];
/**
* DOM id for the button. Defaults to the shared {@link TOGGLE_BUTTON_ID} which
* NavPanelDraggable targets for its hover-reveal CSS. Pass a custom id (or `null`)
* to opt out — e.g. for a persistent instance rendered outside the sidebar panel,
* to avoid duplicate ids and the hover-hide behavior.
*/
id?: string | null;
showActive?: boolean;
size?: ActionIconProps['size'];
title?: ReactNode;
}
const ToggleLeftPanelButton = memo<ToggleLeftPanelButtonProps>(
({ title, showActive, icon, size }) => {
({ title, showActive, icon, size, id = TOGGLE_BUTTON_ID, forceVisible }) => {
const [expand, togglePanel] = useGlobalStore((s) => [
systemStatusSelectors.showLeftPanel(s),
s.toggleLeftPanel,
@@ -33,11 +51,13 @@ const ToggleLeftPanelButton = memo<ToggleLeftPanelButtonProps>(
const { t } = useTranslation(['chat', 'hotkey']);
if (isMacDesktop && !forceVisible) return null;
return (
<ActionIcon
active={showActive ? expand : undefined}
icon={icon || (expand ? PanelLeftClose : PanelLeftOpen)}
id={TOGGLE_BUTTON_ID}
id={id ?? undefined}
size={size || DESKTOP_HEADER_ICON_SMALL_SIZE}
title={title || t('toggleLeftPanel.title', { ns: 'hotkey' })}
tooltipProps={{