Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/renderer/src/app-shell/AppWorkspaceShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ function WorktreeSidebar({
<Sidebar
worktreeScrollOffsetRef={scrollRefs.scrollOffsetRef}
worktreeScrollAnchorRef={scrollRefs.scrollAnchorRef}
headerMinWidth={
layout.leftTitlebarChromeLayout.shouldMount ? layout.titlebarLeftMinWidth : 0
}
/>
</RecoverableRenderErrorBoundary>
)
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/src/app-shell/TitlebarLeftControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export function TitlebarLeftControls({ layout }: { layout: AppChromeLayout }): R
layout.leftTitlebarChromeLayout.isFloating ? ' w-max' : ' w-full'
}`}
>
<div className="flex h-full items-center">
<div className="flex h-full min-w-0 items-center">
{hasMacTrafficLights && !layout.isFullScreen ? (
<div className="titlebar-traffic-light-pad" />
) : hasCustomTitleBar ? (
Expand Down
33 changes: 33 additions & 0 deletions src/renderer/src/app-shell/titlebar-left-min-width.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
function horizontalPx(style: CSSStyleDeclaration, ...properties: string[]): number {
return properties.reduce(
(sum, property) => sum + (Number.parseFloat(style.getPropertyValue(property)) || 0),
0
)
}

/** Width the left titlebar row needs once the app name, its only shrinkable control, is squeezed to its padding. */
export function measureTitlebarLeftMinWidth(row: HTMLElement): number {
let width = 0
for (const child of row.children) {
if (!(child instanceof HTMLElement)) {
continue
}
// scrollWidth keeps content that overflows a shrunk group; left margins are auto free space, not need.
width +=
Math.max(child.scrollWidth, child.getBoundingClientRect().width) +
horizontalPx(getComputedStyle(child), 'margin-right')
}
const appName = row.querySelector('.titlebar-app-name')
if (appName instanceof HTMLElement) {
width -=
appName.getBoundingClientRect().width -
horizontalPx(
getComputedStyle(appName),
'padding-left',
'padding-right',
'border-left-width',
'border-right-width'
)
}
return Math.ceil(width)
}
12 changes: 12 additions & 0 deletions src/renderer/src/app-shell/use-app-chrome-layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { resolveLeftSidebarStyleVariables } from '@/lib/left-sidebar-appearance'
import { resolveLeftTitlebarChromeLayout } from '@/lib/titlebar-left-chrome'
import { shouldShowWorktreeCreationSurface } from '@/lib/worktree-creation-surface'
import { useAppStore } from '../store'
import { measureTitlebarLeftMinWidth } from './titlebar-left-min-width'
import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../shared/constants'
import { selectActiveTerminalChromeState } from '../store/active-terminal-chrome-selector'
import { useSystemPrefersDark } from '../components/terminal-pane/use-system-prefers-dark'
Expand Down Expand Up @@ -94,6 +95,7 @@ export function useAppChromeLayout() {

const titlebarLeftControlsRef = useRef<HTMLDivElement | null>(null)
const [collapsedSidebarHeaderWidth, setCollapsedSidebarHeaderWidth] = useState(0)
const [titlebarLeftMinWidth, setTitlebarLeftMinWidth] = useState(0)
useLayoutEffect(() => {
const controls = titlebarLeftControlsRef.current
if (!controls) {
Expand All @@ -102,15 +104,24 @@ export function useAppChromeLayout() {

const updateWidth = (): void => {
setCollapsedSidebarHeaderWidth(controls.getBoundingClientRect().width)
setTitlebarLeftMinWidth(measureTitlebarLeftMinWidth(controls))
}

updateWidth()
const observer = new ResizeObserver(() => {
updateWidth()
})
observer.observe(controls)
// Why: the zoom-scaled traffic-light pad can grow while the full-width row keeps its size.
for (const group of controls.children) {
observer.observe(group)
for (const control of group.children) {
observer.observe(control)
}
}
return () => observer.disconnect()
}, [
activeView,
isFullScreen,
settings?.showTitlebarAppName,
showSidebar,
Expand Down Expand Up @@ -142,6 +153,7 @@ export function useAppChromeLayout() {
// Retained workbench panes need a separate visibility condition.
terminalWorkbenchVisible: workspaceChromeActive,
titlebarLeftControlsRef,
titlebarLeftMinWidth,
workspaceChromeActive
}
}
5 changes: 5 additions & 0 deletions src/renderer/src/assets/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -981,6 +981,11 @@ html.native-shell .app-layout {
user-select: none;
}

/* Why: the sidebar alone sizes the left column; wider header controls clip instead of widening it past a shrunk sidebar. */
.titlebar-left:not(.titlebar-left-floating) {
contain: inline-size;
}

/* Why: when the sidebar is collapsed this header floats over the tab strip,
so keep the standard titlebar surface instead of the sidebar tint. */
.titlebar-left-floating {
Expand Down
11 changes: 8 additions & 3 deletions src/renderer/src/components/sidebar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,14 @@ export const WORKTREE_SIDEBAR_RESIZE_HANDLE_LINE_CLASS_NAME =
type SidebarProps = {
worktreeScrollOffsetRef: React.MutableRefObject<number>
worktreeScrollAnchorRef: React.MutableRefObject<VirtualizedScrollAnchor>
/** Width the titlebar header above the sidebar needs for its non-shrinking controls. */
headerMinWidth?: number
}

function Sidebar({
worktreeScrollOffsetRef,
worktreeScrollAnchorRef
worktreeScrollAnchorRef,
headerMinWidth = 0
}: SidebarProps): React.JSX.Element {
const sidebarOpen = useAppStore((s) => s.sidebarOpen)
const sidebarWidth = useAppStore((s) => s.sidebarWidth)
Expand Down Expand Up @@ -138,10 +141,12 @@ function Sidebar({
}
}, [agentDashboardDrawerOpen, setAgentDashboardDrawerOpen, showAgentDashboard])

// Why: zooming out widens the traffic-light pad past MIN_WIDTH; render at least that wide without rewriting the stored width.
const minWidth = Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, headerMinWidth))
const { containerRef, onResizeStart, isResizing } = useSidebarResize<HTMLDivElement>({
isOpen: sidebarOpen,
width: sidebarWidth,
minWidth: MIN_WIDTH,
width: Math.max(sidebarWidth, minWidth),
minWidth,
maxWidth: MAX_WIDTH,
deltaSign: 1,
setWidth: setSidebarWidth,
Expand Down
Loading