Skip to content

Commit 06b36ab

Browse files
committed
fix(flow-chat): center session header titles
1 parent 63cb3af commit 06b36ab

4 files changed

Lines changed: 89 additions & 12 deletions

File tree

src/web-ui/src/app/components/SceneBar/SceneBar.scss

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -140,12 +140,16 @@ $_tab-v-margin: 6px; // symmetric top/bottom gap inside SceneBar
140140
gap: $size-gap-2;
141141
min-width: 0;
142142
overflow: hidden;
143-
padding: 0 $size-gap-3;
144-
// Reserve right space so close button doesn't overlap text
145-
padding-right: calc(#{$size-gap-3} + 20px);
143+
// Keep the content group centered in the full tab. Closable tabs reserve
144+
// the close-button footprint on both sides so the right-side control does
145+
// not shift the visible title group away from the geometric center.
146+
padding: 0 calc(#{$size-gap-3} + 20px);
146147
}
147148

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

150154
&__icon {
151155
flex-shrink: 0;

src/web-ui/src/flow_chat/components/modern/FlowChatHeader.scss

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -581,7 +581,9 @@
581581

582582
// ==================== Center message ====================
583583
&__message {
584-
flex: 1;
584+
position: absolute;
585+
left: calc(var(--flowchat-header-side-width, 0px) + #{$size-gap-5});
586+
right: calc(var(--flowchat-header-side-width, 0px) + #{$size-gap-5});
585587
min-width: 0;
586588
padding: 0 $size-gap-3;
587589
display: flex;
@@ -630,14 +632,14 @@
630632

631633
// ==================== Actions ====================
632634
&__actions {
635+
position: relative;
636+
z-index: 2;
633637
display: flex;
634638
align-items: center;
635639
gap: $size-gap-1;
636640
flex-shrink: 0;
637641

638642
&--left {
639-
position: relative;
640-
z-index: 2;
641643
margin-right: $size-gap-2;
642644
overflow: visible;
643645
}
@@ -650,4 +652,3 @@
650652
}
651653
}
652654
}
653-

src/web-ui/src/flow_chat/components/modern/FlowChatHeader.test.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,44 @@ describe('FlowChatHeader', () => {
9090
root.unmount();
9191
});
9292
container.remove();
93+
vi.restoreAllMocks();
94+
});
95+
96+
it('reserves the larger action group width on both sides of the centered title', () => {
97+
vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(function (
98+
this: HTMLElement,
99+
) {
100+
const width = this.classList.contains('flowchat-header__actions--left')
101+
? 32
102+
: this.classList.contains('flowchat-header__actions')
103+
? 196
104+
: 0;
105+
106+
return {
107+
x: 0,
108+
y: 0,
109+
width,
110+
height: 36,
111+
top: 0,
112+
right: width,
113+
bottom: 36,
114+
left: 0,
115+
toJSON: () => ({}),
116+
};
117+
});
118+
119+
act(() => {
120+
root.render(<FlowChatHeader {...createProps()} visible={false} totalTurns={0} />);
121+
});
122+
123+
expect(container.querySelector('.flowchat-header')).toBeNull();
124+
125+
act(() => {
126+
root.render(<FlowChatHeader {...createProps()} />);
127+
});
128+
129+
const header = container.querySelector<HTMLElement>('.flowchat-header');
130+
expect(header?.style.getPropertyValue('--flowchat-header-side-width')).toBe('196px');
93131
});
94132

95133
it('closes the turn list as soon as a different turn selection is accepted', () => {

src/web-ui/src/flow_chat/components/modern/FlowChatHeader.tsx

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* Height matches side panel headers (40px).
55
*/
66

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

269+
useLayoutEffect(() => {
270+
const header = headerRef.current;
271+
const leftActions = leftActionsRef.current;
272+
const rightActions = rightActionsRef.current;
273+
if (!header || !leftActions || !rightActions) return;
274+
275+
const updateSideWidth = () => {
276+
const sideWidth = Math.ceil(Math.max(
277+
leftActions.getBoundingClientRect().width,
278+
rightActions.getBoundingClientRect().width,
279+
));
280+
header.style.setProperty('--flowchat-header-side-width', `${sideWidth}px`);
281+
};
282+
283+
updateSideWidth();
284+
285+
if (typeof ResizeObserver === 'undefined') {
286+
window.addEventListener('resize', updateSideWidth);
287+
return () => window.removeEventListener('resize', updateSideWidth);
288+
}
289+
290+
const observer = new ResizeObserver(updateSideWidth);
291+
observer.observe(leftActions);
292+
observer.observe(rightActions);
293+
294+
return () => observer.disconnect();
295+
}, [isSearchOpen, totalTurns, visible]);
296+
266297
const handleOpenSearch = useCallback(() => {
267298
setIsSearchOpen(true);
268299
}, []);
@@ -533,8 +564,11 @@ export const FlowChatHeader: React.FC<FlowChatHeaderProps> = ({
533564
}
534565

535566
return (
536-
<div className="flowchat-header">
537-
<div className="flowchat-header__actions flowchat-header__actions--left">
567+
<div className="flowchat-header" ref={headerRef}>
568+
<div
569+
className="flowchat-header__actions flowchat-header__actions--left"
570+
ref={leftActionsRef}
571+
>
538572
<SessionFilesBadge sessionId={sessionId} />
539573
</div>
540574

@@ -563,7 +597,7 @@ export const FlowChatHeader: React.FC<FlowChatHeaderProps> = ({
563597
</div>
564598
</Tooltip>
565599

566-
<div className="flowchat-header__actions">
600+
<div className="flowchat-header__actions" ref={rightActionsRef}>
567601
<div className="flowchat-header__background-activity-nav" ref={backgroundActivityPanelRef}>
568602
<IconButton
569603
className={[

0 commit comments

Comments
 (0)