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
12 changes: 8 additions & 4 deletions src/web-ui/src/app/components/SceneBar/SceneBar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -140,12 +140,16 @@ $_tab-v-margin: 6px; // symmetric top/bottom gap inside SceneBar
gap: $size-gap-2;
min-width: 0;
overflow: hidden;
padding: 0 $size-gap-3;
// Reserve right space so close button doesn't overlap text
padding-right: calc(#{$size-gap-3} + 20px);
// Keep the content group centered in the full tab. Closable tabs reserve
// the close-button footprint on both sides so the right-side control does
// not shift the visible title group away from the geometric center.
padding: 0 calc(#{$size-gap-3} + 20px);
}

// pinned only affects auto-eviction; all tabs have close button so no special padding needed
// Pinned tabs have no close button, so they need no mirrored reservation.
&--pinned &__content {
padding-inline: $size-gap-3;
}

&__icon {
flex-shrink: 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -581,7 +581,9 @@

// ==================== Center message ====================
&__message {
flex: 1;
position: absolute;
left: calc(var(--flowchat-header-side-width) + #{$size-gap-5});
right: calc(var(--flowchat-header-side-width) + #{$size-gap-5});
min-width: 0;
padding: 0 $size-gap-3;
display: flex;
Expand Down Expand Up @@ -630,14 +632,14 @@

// ==================== Actions ====================
&__actions {
position: relative;
z-index: 2;
display: flex;
align-items: center;
gap: $size-gap-1;
flex-shrink: 0;

&--left {
position: relative;
z-index: 2;
margin-right: $size-gap-2;
overflow: visible;
}
Expand All @@ -650,4 +652,3 @@
}
}
}

38 changes: 38 additions & 0 deletions src/web-ui/src/flow_chat/components/modern/FlowChatHeader.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,44 @@ describe('FlowChatHeader', () => {
root.unmount();
});
container.remove();
vi.restoreAllMocks();
});

it('reserves the larger action group width on both sides of the centered title', () => {
vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(function (
this: HTMLElement,
) {
const width = this.classList.contains('flowchat-header__actions--left')
? 32
: this.classList.contains('flowchat-header__actions')
? 196
: 0;

return {
x: 0,
y: 0,
width,
height: 36,
top: 0,
right: width,
bottom: 36,
left: 0,
toJSON: () => ({}),
};
});

act(() => {
root.render(<FlowChatHeader {...createProps()} visible={false} totalTurns={0} />);
});

expect(container.querySelector('.flowchat-header')).toBeNull();

act(() => {
root.render(<FlowChatHeader {...createProps()} />);
});

const header = container.querySelector<HTMLElement>('.flowchat-header');
expect(header?.style.getPropertyValue('--flowchat-header-side-width')).toBe('196px');
});

it('closes the turn list as soon as a different turn selection is accepted', () => {
Expand Down
42 changes: 38 additions & 4 deletions src/web-ui/src/flow_chat/components/modern/FlowChatHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Height matches side panel headers (40px).
*/

import React, { useEffect, useMemo, useRef, useState, useCallback } from 'react';
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState, useCallback } from 'react';
import { Activity, Bot, ChevronDown, ChevronUp, GitPullRequest, Keyboard, List, MoreHorizontal, Search, Square, Terminal, X } from 'lucide-react';
import { Tooltip, IconButton, Input } from '@/component-library';
import { useTranslation } from 'react-i18next';
Expand Down Expand Up @@ -140,6 +140,9 @@ export const FlowChatHeader: React.FC<FlowChatHeaderProps> = ({
const [openBackgroundSubagentMenuId, setOpenBackgroundSubagentMenuId] = useState<string | null>(null);
const [openBackgroundCommandMenuId, setOpenBackgroundCommandMenuId] = useState<string | null>(null);
const [isSearchOpen, setIsSearchOpen] = useState(false);
const headerRef = useRef<HTMLDivElement | null>(null);
const leftActionsRef = useRef<HTMLDivElement | null>(null);
const rightActionsRef = useRef<HTMLDivElement | null>(null);
const turnListRef = useRef<HTMLDivElement | null>(null);
const backgroundActivityPanelRef = useRef<HTMLDivElement | null>(null);
const activeTurnItemRef = useRef<HTMLButtonElement | null>(null);
Expand Down Expand Up @@ -263,6 +266,34 @@ export const FlowChatHeader: React.FC<FlowChatHeaderProps> = ({
};
}, [currentTurn, displayTurns.length, isTurnListOpen]);

useLayoutEffect(() => {
const header = headerRef.current;
const leftActions = leftActionsRef.current;
const rightActions = rightActionsRef.current;
if (!header || !leftActions || !rightActions) return;

const updateSideWidth = () => {
const sideWidth = Math.ceil(Math.max(
leftActions.getBoundingClientRect().width,
rightActions.getBoundingClientRect().width,
));
header.style.setProperty('--flowchat-header-side-width', `${sideWidth}px`);
};

updateSideWidth();

if (typeof ResizeObserver === 'undefined') {
window.addEventListener('resize', updateSideWidth);
return () => window.removeEventListener('resize', updateSideWidth);
}

const observer = new ResizeObserver(updateSideWidth);
observer.observe(leftActions);
observer.observe(rightActions);

return () => observer.disconnect();
}, [isSearchOpen, totalTurns, visible]);

const handleOpenSearch = useCallback(() => {
setIsSearchOpen(true);
}, []);
Expand Down Expand Up @@ -533,8 +564,11 @@ export const FlowChatHeader: React.FC<FlowChatHeaderProps> = ({
}

return (
<div className="flowchat-header">
<div className="flowchat-header__actions flowchat-header__actions--left">
<div className="flowchat-header" ref={headerRef}>
<div
className="flowchat-header__actions flowchat-header__actions--left"
ref={leftActionsRef}
>
<SessionFilesBadge sessionId={sessionId} />
</div>

Expand Down Expand Up @@ -563,7 +597,7 @@ export const FlowChatHeader: React.FC<FlowChatHeaderProps> = ({
</div>
</Tooltip>

<div className="flowchat-header__actions">
<div className="flowchat-header__actions" ref={rightActionsRef}>
<div className="flowchat-header__background-activity-nav" ref={backgroundActivityPanelRef}>
<IconButton
className={[
Expand Down