-
Notifications
You must be signed in to change notification settings - Fork 3k
feat(web-shell): modernize multi-workspace sidebar #6804
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -41,6 +41,7 @@ type ChatEditorTestProps = { | |
| const { | ||
| mockConnection, | ||
| mockSessionActions, | ||
| mockWorkspace, | ||
| mockWorkspaceActions, | ||
| mockStore, | ||
| mockFollowup, | ||
|
|
@@ -67,6 +68,11 @@ const { | |
| loadingTranscript: false, | ||
| catchingUp: false, | ||
| }; | ||
| const workspaceClient = { | ||
| workspaceByCwd: vi.fn(() => ({ | ||
| workspaceGit: vi.fn().mockResolvedValue({ branch: 'main' }), | ||
| })), | ||
| }; | ||
| return { | ||
| mockConnection: connection, | ||
| mockSessionActions: { | ||
|
|
@@ -88,6 +94,12 @@ const { | |
| loadArtifacts: vi.fn().mockResolvedValue({ artifacts: [] }), | ||
| loadSession: vi.fn().mockResolvedValue(undefined), | ||
| }, | ||
| mockWorkspace: { | ||
| capabilities: { | ||
| workspaces: [{ id: 'primary', cwd: '/workspace', primary: true }], | ||
| }, | ||
| client: workspaceClient, | ||
| }, | ||
| mockWorkspaceActions: { | ||
| loadSkillsStatus: vi.fn().mockResolvedValue({ skills: [] }), | ||
| loadProviders: vi.fn().mockResolvedValue({ current: null }), | ||
|
|
@@ -153,6 +165,7 @@ vi.mock('@qwen-code/webui/daemon-react-sdk', () => ({ | |
| useStreamingState: () => testState.streamingState, | ||
| useTranscriptBlocks: () => testState.blocks, | ||
| useTranscriptStore: () => mockStore, | ||
| useWorkspace: () => mockWorkspace, | ||
| useWorkspaceActions: () => mockWorkspaceActions, | ||
| useWorkspaceEventSignals: () => ({ | ||
| artifactsVersion: 0, | ||
|
|
@@ -401,6 +414,8 @@ mockComponent('./components/WelcomeHeader', 'WelcomeHeader'); | |
| mockComponent('./components/dialogs/ApprovalModeDialog', 'ApprovalModeDialog'); | ||
| mockComponent('./components/dialogs/ResumeDialog', 'ResumeDialog'); | ||
| mockComponent('./components/dialogs/ToolsDialog', 'ToolsDialog'); | ||
| mockComponent('./components/tools/ToolsManagerPage', 'ToolsManagerPage'); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] These mocks for — qwen3.7-max via Qwen Code /review |
||
| mockComponent('./components/skills/SkillsManagerPage', 'SkillsManagerPage'); | ||
| mockComponent('./components/dialogs/DaemonStatusDialog', 'DaemonStatusDialog'); | ||
| mockComponent('./components/SessionOverviewPanel', 'SessionOverviewPanel'); | ||
| vi.doMock('./components/SplitView', async () => { | ||
|
|
@@ -1722,142 +1737,6 @@ describe('App session callbacks', () => { | |
| expect(panel?.getAttribute('aria-label')).toBe('Session Overview'); | ||
| }); | ||
|
|
||
| it('forces the compact session drawer from the external shell ref', async () => { | ||
| const shellRef = createRef<WebShellApi>(); | ||
| const { container } = renderApp({ sidebar: true, shellRef }); | ||
| await flush(); | ||
|
|
||
| await act(async () => { | ||
| shellRef.current?.openSessionDrawer(); | ||
| await Promise.resolve(); | ||
| }); | ||
|
|
||
| const drawer = container.querySelector( | ||
| '[data-sidebar-shell][role="dialog"]', | ||
| ); | ||
| expect(drawer).not.toBeNull(); | ||
| expect(drawer?.className).toContain('mobileDrawerForced'); | ||
| }); | ||
|
|
||
| it('returns a forced compact drawer to viewport control when the user dismisses it', async () => { | ||
| const shellRef = createRef<WebShellApi>(); | ||
| const { container } = renderApp({ sidebar: true, shellRef }); | ||
| await flush(); | ||
|
|
||
| await act(async () => { | ||
| shellRef.current?.openSessionDrawer(); | ||
| await Promise.resolve(); | ||
| }); | ||
| expect( | ||
| container.querySelector('[data-sidebar-shell]')?.className, | ||
| ).toContain('mobileDrawerForced'); | ||
|
|
||
| await act(async () => { | ||
| container | ||
| .querySelector<HTMLElement>( | ||
| '[data-sidebar-shell] > div[aria-hidden="true"]', | ||
| ) | ||
| ?.click(); | ||
| await Promise.resolve(); | ||
| }); | ||
| expect( | ||
| container.querySelector('[data-sidebar-shell]')?.className, | ||
| ).not.toContain('mobileDrawerForced'); | ||
| expect( | ||
| container.querySelector('[data-sidebar-shell][role="dialog"]'), | ||
| ).toBeNull(); | ||
| }); | ||
|
|
||
| it('returns to chat and clears the current page when the external shell opens the compact drawer', async () => { | ||
| const shellRef = createRef<WebShellApi>(); | ||
| const { container } = renderApp({ sidebar: true, shellRef }); | ||
| await flush(); | ||
|
|
||
| await act(async () => { | ||
| shellRef.current?.openSessionOverview(); | ||
| await Promise.resolve(); | ||
| }); | ||
| expect( | ||
| container.querySelector('[data-testid="inline-panel"]'), | ||
| ).not.toBeNull(); | ||
|
|
||
| await act(async () => { | ||
| shellRef.current?.openSessionDrawer(); | ||
| await Promise.resolve(); | ||
| }); | ||
| expect(container.querySelector('[data-testid="inline-panel"]')).toBeNull(); | ||
|
|
||
| await act(async () => { | ||
| shellRef.current?.openSplitView(); | ||
| await Promise.resolve(); | ||
| }); | ||
| expect( | ||
| container.querySelector('[data-testid="split-view-page"]'), | ||
| ).not.toBeNull(); | ||
|
|
||
| await act(async () => { | ||
| shellRef.current?.openSessionDrawer(); | ||
| await Promise.resolve(); | ||
| }); | ||
| expect( | ||
| container.querySelector('[data-testid="split-view-page"]'), | ||
| ).toBeNull(); | ||
| expect( | ||
| container.querySelector('[data-sidebar-shell][role="dialog"]'), | ||
| ).not.toBeNull(); | ||
| }); | ||
|
|
||
| it('clears a forced compact drawer after crossing to a wide viewport', async () => { | ||
| let mobileChangeHandler: | ||
| | ((event: { matches: boolean }) => void) | ||
| | undefined; | ||
| Object.defineProperty(window, 'matchMedia', { | ||
| configurable: true, | ||
| value: vi.fn().mockImplementation((query: string) => ({ | ||
| matches: query.includes('min-width'), | ||
| media: query, | ||
| addEventListener: ( | ||
| _type: string, | ||
| handler: (event: { matches: boolean }) => void, | ||
| ) => { | ||
| if (query.includes('max-width')) mobileChangeHandler = handler; | ||
| }, | ||
| removeEventListener: vi.fn(), | ||
| })), | ||
| }); | ||
| const shellRef = createRef<WebShellApi>(); | ||
| const { container } = renderApp({ sidebar: true, shellRef }); | ||
| await flush(); | ||
|
|
||
| await act(async () => { | ||
| shellRef.current?.openSessionDrawer(); | ||
| await Promise.resolve(); | ||
| }); | ||
| expect( | ||
| container.querySelector('[data-sidebar-shell]')?.className, | ||
| ).toContain('mobileDrawerForced'); | ||
|
|
||
| await act(async () => { | ||
| mobileChangeHandler?.({ matches: false }); | ||
| await Promise.resolve(); | ||
| }); | ||
| expect( | ||
| container.querySelector('[data-sidebar-shell]')?.className, | ||
| ).not.toContain('mobileDrawerForced'); | ||
| expect( | ||
| container.querySelector('[data-sidebar-shell][role="dialog"]'), | ||
| ).toBeNull(); | ||
| }); | ||
|
|
||
| it('lets a host hide the built-in compact sidebar toggle', async () => { | ||
| const { container } = renderApp({ | ||
| sidebar: { enabled: true, showCompactToggle: false }, | ||
| }); | ||
| await flush(); | ||
|
|
||
| expect(container.querySelector('[aria-label="Toggle menu"]')).toBeNull(); | ||
| }); | ||
|
|
||
| it('returns to the Session Overview when leaving the split view', async () => { | ||
| const { container } = renderApp(); | ||
| await flush(); | ||
|
|
@@ -2892,7 +2771,9 @@ describe('App manual-run orchestration (scheduled tasks)', () => { | |
| await act(async () => { | ||
| await expect(run('do the thing', 'session-1')).resolves.toBeUndefined(); | ||
| }); | ||
| expect(mockSessionActions.loadSession).toHaveBeenCalledWith('session-1'); | ||
| expect(mockSessionActions.loadSession).toHaveBeenCalledWith('session-1', { | ||
| workspaceCwd: undefined, | ||
| }); | ||
| }); | ||
|
|
||
| it('supersedes an older pending bound run with a newer one', async () => { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Suggestion] The integration example persists only
sessionId, and the prop documentation omits the new workspace identity callback value. Consumers following this README cannot round-trip secondary sessions across reloads. Document and demonstrate persisting both session and workspace IDs.— Codex $qreview via Qwen Code /review