Skip to content

Commit c0ddeda

Browse files
authored
Merge pull request #644 from Yumiue/codex/fix-web-collapsed-sidebar-rail-click
fix(web): 修复收起侧边栏按钮点击失效
2 parents ffdd512 + 283bfa7 commit c0ddeda

5 files changed

Lines changed: 135 additions & 3 deletions

File tree

web/src/components/layout/AppLayout.test.tsx

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@ import { useUIStore } from '@/stores/useUIStore'
55
import { useSessionStore } from '@/stores/useSessionStore'
66
import { useWorkspaceStore } from '@/stores/useWorkspaceStore'
77

8-
vi.mock('./Sidebar', () => ({ default: ({ collapsed }: { collapsed?: boolean }) => <div>{collapsed ? 'sidebar-collapsed' : 'sidebar-open'}</div> }))
8+
vi.mock('./Sidebar', () => ({
9+
default: ({ collapsed }: { collapsed?: boolean }) => (
10+
<div data-testid={collapsed ? 'sidebar-collapsed-content' : 'sidebar-open-content'}>
11+
{collapsed ? 'sidebar-collapsed' : 'sidebar-open'}
12+
</div>
13+
),
14+
}))
915
vi.mock('@/components/chat/ChatPanel', () => ({ default: () => <div>chat-panel</div> }))
1016
vi.mock('@/components/panels/FileChangePanel', () => ({ default: () => <div>changes-panel</div> }))
1117
vi.mock('@/components/panels/FileTreePanel', () => ({ default: () => <div>tree-panel</div> }))
@@ -49,6 +55,23 @@ describe('AppLayout', () => {
4955
expect(screen.getByText('tree-panel')).toBeInTheDocument()
5056
})
5157

58+
it('renders collapsed sidebar in a dedicated rail beside the main area', () => {
59+
useUIStore.setState({
60+
sidebarOpen: false,
61+
} as any)
62+
render(<AppLayout />)
63+
64+
const workspace = document.querySelector('.app-workspace')
65+
const rail = screen.getByTestId('sidebar-collapsed-rail')
66+
const collapsedContent = screen.getByTestId('sidebar-collapsed-content')
67+
const mainArea = document.querySelector('.main-area')
68+
69+
expect(workspace).toContainElement(rail)
70+
expect(rail).toContainElement(collapsedContent)
71+
expect(mainArea?.parentElement).toBe(workspace)
72+
expect(collapsedContent.closest('.main-area')).toBeNull()
73+
})
74+
5275
it('handles ctrl/cmd+n shortcut', () => {
5376
const prepareNewChat = vi.fn()
5477
useSessionStore.setState({ prepareNewChat } as any)

web/src/components/layout/AppLayout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ export default function AppLayout({ shellMode = 'electron' }: AppLayoutProps) {
9191
/>
9292
</div>
9393
) : (
94-
<div className="sidebar-collapsed-wrapper">
94+
<div className="sidebar-collapsed-wrapper" data-testid="sidebar-collapsed-rail">
9595
<Sidebar collapsed />
9696
</div>
9797
)}

web/src/components/layout/Sidebar.test.tsx

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,43 @@ describe('Sidebar ProviderModal', () => {
2323
beforeEach(() => {
2424
cleanup()
2525
mockGatewayAPI = {
26+
listMCPServers: vi.fn().mockResolvedValue({
27+
payload: {
28+
servers: [
29+
{
30+
id: 'stdio-weather',
31+
enabled: true,
32+
trust: false,
33+
transport: 'stdio',
34+
stdio: { command: 'weather', args: [], env: [] },
35+
},
36+
],
37+
},
38+
}),
39+
setMCPServerEnabled: vi.fn().mockResolvedValue(undefined),
40+
deleteMCPServer: vi.fn().mockResolvedValue(undefined),
41+
upsertMCPServer: vi.fn().mockResolvedValue(undefined),
42+
listAvailableSkills: vi.fn().mockResolvedValue({
43+
payload: {
44+
skills: [
45+
{
46+
descriptor: {
47+
id: 'skill-refactor',
48+
name: 'Skill Refactor',
49+
description: 'Refactor code safely',
50+
},
51+
active: false,
52+
},
53+
],
54+
},
55+
}),
56+
listSessionSkills: vi.fn().mockResolvedValue({
57+
payload: {
58+
skills: [],
59+
},
60+
}),
61+
activateSessionSkill: vi.fn().mockResolvedValue(undefined),
62+
deactivateSessionSkill: vi.fn().mockResolvedValue(undefined),
2663
listProviders: vi.fn().mockResolvedValue({
2764
payload: {
2865
providers: [
@@ -290,4 +327,53 @@ describe('Sidebar ProviderModal', () => {
290327
expect(chevronFor(workspaceTwo)).toHaveClass('expanded')
291328
})
292329
})
330+
331+
it('immediately dispatches collapsed-rail actions', async () => {
332+
const toggleSidebar = vi.fn()
333+
const prepareNewChat = vi.fn()
334+
useUIStore.setState({
335+
toggleSidebar,
336+
} as any)
337+
useSessionStore.setState({
338+
prepareNewChat,
339+
} as any)
340+
341+
const { container } = render(<Sidebar collapsed />)
342+
const collapsedButtons = Array.from(container.querySelectorAll('.sidebar-strip-btn'))
343+
344+
expect(collapsedButtons).toHaveLength(5)
345+
346+
fireEvent.click(collapsedButtons[0] as HTMLButtonElement)
347+
expect(toggleSidebar).toHaveBeenCalledTimes(1)
348+
349+
fireEvent.click(collapsedButtons[1] as HTMLButtonElement)
350+
expect(prepareNewChat).toHaveBeenCalledTimes(1)
351+
352+
fireEvent.click(collapsedButtons[2] as HTMLButtonElement)
353+
await waitFor(() => {
354+
expect(mockGatewayAPI.listMCPServers).toHaveBeenCalled()
355+
})
356+
357+
fireEvent.click(collapsedButtons[3] as HTMLButtonElement)
358+
await waitFor(() => {
359+
expect(mockGatewayAPI.listAvailableSkills).toHaveBeenCalled()
360+
expect(screen.getByText('Skill Refactor')).toBeInTheDocument()
361+
})
362+
363+
fireEvent.click(collapsedButtons[4] as HTMLButtonElement)
364+
await waitFor(() => {
365+
expect(mockGatewayAPI.listProviders).toHaveBeenCalled()
366+
expect(screen.getByText('Gemini')).toBeInTheDocument()
367+
})
368+
})
369+
370+
it('keeps the collapsed rail style above neighboring panels', () => {
371+
const railRule = appCss.match(/\.sidebar-collapsed-wrapper\s*{(?<body>[^}]*)}/)?.groups?.body ?? ''
372+
expect(railRule).toContain('position: relative')
373+
expect(railRule).toContain('z-index: 20')
374+
expect(railRule).toContain('isolation: isolate')
375+
expect(railRule).toContain('width: 44px')
376+
expect(railRule).toContain('min-width: 44px')
377+
expect(railRule).toContain('flex: 0 0 44px')
378+
})
293379
})

web/src/components/layout/Sidebar.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useState, useEffect, useCallback } from 'react'
2+
import { createPortal } from 'react-dom'
23
import { useSessionStore } from '@/stores/useSessionStore'
34
import { useChatStore } from '@/stores/useChatStore'
45
import { useUIStore } from '@/stores/useUIStore'
@@ -147,6 +148,17 @@ export default function Sidebar({ collapsed }: SidebarProps) {
147148
store.prepareNewChat()
148149
}
149150

151+
const modalOverlays = typeof document === 'undefined'
152+
? null
153+
: createPortal(
154+
<>
155+
{mcpModalOpen && <McpModal onClose={() => setMcpModalOpen(false)} />}
156+
{skillModalOpen && <SkillModal onClose={() => setSkillModalOpen(false)} />}
157+
{providerModalOpen && <ProviderModal onClose={() => setProviderModalOpen(false)} />}
158+
</>,
159+
document.body,
160+
)
161+
150162
// Collapsed sidebar strip
151163
if (collapsed) {
152164
return (
@@ -167,6 +179,7 @@ export default function Sidebar({ collapsed }: SidebarProps) {
167179
<button className="sidebar-strip-btn" onClick={() => setProviderModalOpen(true)} title="供应商">
168180
<Server size={16} />
169181
</button>
182+
{modalOverlays}
170183
</>
171184
)
172185
}

web/src/index.css

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,8 @@ html, body, #root {
512512
flex-direction: column;
513513
overflow: hidden;
514514
background: var(--bg-app);
515+
position: relative;
516+
z-index: 0;
515517
}
516518

517519
/* ── Chat Header ── */
@@ -1750,6 +1752,8 @@ html, body, #root {
17501752
background: var(--bg-sidebar);
17511753
border-left: none;
17521754
overflow: hidden;
1755+
position: relative;
1756+
z-index: 0;
17531757
}
17541758

17551759
.git-diff-preview-host-inline .monaco-editor .inline-deleted-margin-view-zone {
@@ -1770,7 +1774,13 @@ html, body, #root {
17701774

17711775
/* ── Collapsed Sidebar Wrapper (used from AppLayout) ── */
17721776
.sidebar-collapsed-wrapper {
1773-
width: 44px; flex-shrink: 0;
1777+
position: relative;
1778+
z-index: 20;
1779+
isolation: isolate;
1780+
width: 44px;
1781+
min-width: 44px;
1782+
flex: 0 0 44px;
1783+
align-self: stretch;
17741784
background: var(--bg-sidebar);
17751785
display: flex; flex-direction: column;
17761786
align-items: center;

0 commit comments

Comments
 (0)