mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-18 13:23:33 +08:00
Keep newly activated desktop tabs visible
When the tab strip overflows, newly opened sessions were active but could remain outside the visible horizontal viewport. The tab bar already owns element refs for drag and close behavior, so the focused fix scrolls the active tab into view after activation and refreshes overflow controls on the next frame. Constraint: Preserve existing tab ordering, drag behavior, and close-button affordances. Rejected: Reorder new sessions to the left | changes established tab order and makes history harder to scan. Confidence: high Scope-risk: narrow Directive: Keep active-tab visibility behavior in TabBar so all activation paths share it. Tested: cd desktop && bun run test -- TabBar Tested: cd desktop && bun run lint Tested: cd desktop && bun run build Tested: agent-browser overflow smoke with six tabs confirmed active Untitled Session fullyVisible=true Not-tested: Full bun run verify gate.
This commit is contained in:
parent
12f6cb1b63
commit
eadbd4be14
@ -9,6 +9,7 @@ const getCurrentWindowMock = vi.hoisted(() => vi.fn(() => ({
|
||||
const windowControlsMock = vi.hoisted(() => ({
|
||||
show: true,
|
||||
}))
|
||||
const scrollIntoViewMock = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('@tauri-apps/api/window', () => ({
|
||||
getCurrentWindow: getCurrentWindowMock,
|
||||
@ -64,8 +65,14 @@ describe('TabBar', () => {
|
||||
value: {},
|
||||
})
|
||||
|
||||
Object.defineProperty(window.HTMLElement.prototype, 'scrollIntoView', {
|
||||
configurable: true,
|
||||
value: scrollIntoViewMock,
|
||||
})
|
||||
|
||||
startDraggingMock.mockClear()
|
||||
getCurrentWindowMock.mockClear()
|
||||
scrollIntoViewMock.mockClear()
|
||||
windowControlsMock.show = true
|
||||
vi.resetModules()
|
||||
})
|
||||
@ -88,6 +95,42 @@ describe('TabBar', () => {
|
||||
delete (window as typeof window & { __TAURI__?: unknown }).__TAURI__
|
||||
})
|
||||
|
||||
it('scrolls the active tab into view when the active tab changes', async () => {
|
||||
const { TabBar } = await import('./TabBar')
|
||||
const { useTabStore } = await import('../../stores/tabStore')
|
||||
const { useChatStore } = await import('../../stores/chatStore')
|
||||
|
||||
useTabStore.setState({
|
||||
tabs: [
|
||||
{ sessionId: 'tab-1', title: 'First Session', type: 'session', status: 'idle' },
|
||||
{ sessionId: 'tab-2', title: 'Second Session', type: 'session', status: 'idle' },
|
||||
{ sessionId: 'tab-3', title: 'Third Session', type: 'session', status: 'idle' },
|
||||
{ sessionId: 'tab-4', title: 'Fourth Session', type: 'session', status: 'idle' },
|
||||
{ sessionId: 'tab-5', title: 'New Session', type: 'session', status: 'idle' },
|
||||
],
|
||||
activeTabId: 'tab-1',
|
||||
})
|
||||
useChatStore.setState({
|
||||
sessions: {},
|
||||
disconnectSession: vi.fn(),
|
||||
} as Partial<ReturnType<typeof useChatStore.getState>>)
|
||||
|
||||
await act(async () => {
|
||||
render(<TabBar />)
|
||||
})
|
||||
scrollIntoViewMock.mockClear()
|
||||
|
||||
await act(async () => {
|
||||
useTabStore.getState().setActiveTab('tab-5')
|
||||
})
|
||||
|
||||
expect(scrollIntoViewMock).toHaveBeenCalledWith({
|
||||
block: 'nearest',
|
||||
inline: 'nearest',
|
||||
behavior: 'smooth',
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps the overflow button flush against window controls on Windows', async () => {
|
||||
const { TabBar } = await import('./TabBar')
|
||||
const { useTabStore } = await import('../../stores/tabStore')
|
||||
|
||||
@ -93,6 +93,21 @@ export function TabBar() {
|
||||
}
|
||||
}, [updateScrollState, tabs.length])
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeTabId) return
|
||||
const activeTabEl = tabRefs.current.get(activeTabId)
|
||||
if (!activeTabEl) return
|
||||
|
||||
activeTabEl.scrollIntoView({
|
||||
block: 'nearest',
|
||||
inline: 'nearest',
|
||||
behavior: 'smooth',
|
||||
})
|
||||
|
||||
const frame = window.requestAnimationFrame(updateScrollState)
|
||||
return () => window.cancelAnimationFrame(frame)
|
||||
}, [activeTabId, tabs.length, updateScrollState])
|
||||
|
||||
useEffect(() => {
|
||||
if (!contextMenu) return
|
||||
const close = () => setContextMenu(null)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user