mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
fix(web-shell): align no-workspace category header with peers (#15897)
* fix(web-shell): align no-workspace category header with peers * test(web-shell): preserve archived no-workspace header presentation * docs(web-shell): show sidebar category header comparison
This commit is contained in:
1 parent
4d74e86aae
commit
0976826090
6 files changed
+176
-26
No files matched your search
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
@@ -37,6 +37,22 @@ The sidebar trigger, dialog heading, and non-browse submit button all use
|
||||
The Recent heading and its region name continue to share the same translation
|
||||
key; a separate descriptive region label is outside this copy-only update.
|
||||
|
||||
## Sidebar category headers
|
||||
|
||||
Pinned, Workspaces, No-workspace sessions, and Recent are peer collapsible
|
||||
categories. Their active sidebar headers use the same height, spacing, muted
|
||||
text, and trailing expansion chevron, including hover and keyboard-focus reveal
|
||||
rules. No-workspace sessions retains its independent saved expansion preference
|
||||
and search behavior. Its header in Archived settings keeps the workspace-entry
|
||||
presentation.
|
||||
|
||||
Chromium comparison with simulated daemon responses (hovering the collapsed
|
||||
No-workspace sessions category):
|
||||
|
||||
| Before | After |
|
||||
| -------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
|
||||
|  |  |
|
||||
|
||||
## Shared UI Contract
|
||||
|
||||
Use the TypeScript SDK daemon UI exports as the common boundary:
|
||||
|
||||
@@ -194,7 +194,9 @@ describe('StandaloneRecents', () => {
|
||||
const newSession = container.querySelector<HTMLButtonElement>(
|
||||
'[data-testid="standalone-active-group"] button[aria-label="sidebar.newTask"]',
|
||||
);
|
||||
expect(newSession?.parentElement?.className).toContain('invisible');
|
||||
expect(newSession?.parentElement?.className).toContain(
|
||||
'projectsHeaderActions',
|
||||
);
|
||||
const sessions = container.querySelector(
|
||||
'[data-testid="standalone-active-group"] > div:nth-child(2)',
|
||||
);
|
||||
@@ -202,6 +204,11 @@ describe('StandaloneRecents', () => {
|
||||
const header = container.querySelector<HTMLButtonElement>(
|
||||
'[data-testid="standalone-active-group"] > div > button',
|
||||
)!;
|
||||
expect(header.parentElement?.className).toContain('projectsHeader');
|
||||
expect(header.className).toContain('projectsHeaderToggle');
|
||||
expect(header.querySelector('span + svg')?.classList).toContain(
|
||||
'lucide-chevron-down',
|
||||
);
|
||||
header.focus();
|
||||
await act(async () =>
|
||||
header.dispatchEvent(
|
||||
@@ -209,6 +216,9 @@ describe('StandaloneRecents', () => {
|
||||
),
|
||||
);
|
||||
expect(document.activeElement).not.toBe(header);
|
||||
expect(header.querySelector('span + svg')?.classList).toContain(
|
||||
'lucide-chevron-right',
|
||||
);
|
||||
await act(async () =>
|
||||
header.dispatchEvent(
|
||||
new MouseEvent('click', { bubbles: true, detail: 1 }),
|
||||
@@ -313,6 +323,16 @@ describe('StandaloneRecents', () => {
|
||||
it('loads only the requested archived group', async () => {
|
||||
const { renderSession } = await render({ archiveState: 'archived' });
|
||||
|
||||
const group = container.querySelector(
|
||||
'[data-testid="standalone-archived-group"]',
|
||||
)!;
|
||||
expect(group.classList).toContain('mb-3');
|
||||
const header = group.querySelector('button[aria-expanded]')!;
|
||||
expect(header.parentElement?.className).toContain('headerRow');
|
||||
expect(header.querySelector('svg')?.classList).toContain(
|
||||
'lucide-circle-dashed',
|
||||
);
|
||||
expect(header.querySelector('span + svg')).toBeNull();
|
||||
expect(container.textContent).toContain('Archived chat');
|
||||
expect(container.textContent).not.toContain('Active chat');
|
||||
expect(renderSession).toHaveBeenCalledWith(
|
||||
|
||||
@@ -5,7 +5,12 @@ import {
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
import { CircleDashedIcon, SquarePenIcon } from 'lucide-react';
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
ChevronRightIcon,
|
||||
CircleDashedIcon,
|
||||
SquarePenIcon,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
STANDALONE_SESSIONS_CAPABILITY,
|
||||
type DaemonSessionArchiveState,
|
||||
@@ -469,7 +474,11 @@ export function StandaloneRecents({
|
||||
return (
|
||||
<>
|
||||
<section
|
||||
className={`${workspaceStyles.section} mb-3`}
|
||||
className={
|
||||
archiveState === 'active'
|
||||
? workspaceStyles.section
|
||||
: `${workspaceStyles.section} mb-3`
|
||||
}
|
||||
aria-label={
|
||||
archiveState === 'active'
|
||||
? t('sidebar.noWorkspaceSessions')
|
||||
@@ -477,10 +486,20 @@ export function StandaloneRecents({
|
||||
}
|
||||
data-testid={`standalone-${archiveState}-group`}
|
||||
>
|
||||
<div className={`${workspaceStyles.headerRow} group`}>
|
||||
<div
|
||||
className={
|
||||
archiveState === 'active'
|
||||
? sidebarStyles.projectsHeader
|
||||
: `${workspaceStyles.headerRow} group`
|
||||
}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={workspaceStyles.header}
|
||||
className={
|
||||
archiveState === 'active'
|
||||
? sidebarStyles.projectsHeaderToggle
|
||||
: workspaceStyles.header
|
||||
}
|
||||
aria-expanded={expanded}
|
||||
disabled={Boolean(query)}
|
||||
onClick={(event) => {
|
||||
@@ -490,22 +509,33 @@ export function StandaloneRecents({
|
||||
if (event.detail > 0) event.currentTarget.blur();
|
||||
}}
|
||||
>
|
||||
<CircleDashedIcon
|
||||
className={workspaceStyles.folderIcon}
|
||||
size={14}
|
||||
strokeWidth={1.4}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className={workspaceStyles.headerContent}>
|
||||
<span className={workspaceStyles.name}>
|
||||
{t('sidebar.noWorkspaceSessions')}
|
||||
</span>
|
||||
</span>
|
||||
{archiveState === 'active' ? (
|
||||
<>
|
||||
<span>{t('sidebar.noWorkspaceSessions')}</span>
|
||||
{expanded ? (
|
||||
<ChevronDownIcon size={14} aria-hidden="true" />
|
||||
) : (
|
||||
<ChevronRightIcon size={14} aria-hidden="true" />
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CircleDashedIcon
|
||||
className={workspaceStyles.folderIcon}
|
||||
size={14}
|
||||
strokeWidth={1.4}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className={workspaceStyles.headerContent}>
|
||||
<span className={workspaceStyles.name}>
|
||||
{t('sidebar.noWorkspaceSessions')}
|
||||
</span>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
{archiveState === 'active' && onNewSession && (
|
||||
<div
|
||||
className={`${sidebarStyles.workspaceHeaderActions} invisible group-hover:visible group-focus-within:visible`}
|
||||
>
|
||||
<div className={sidebarStyles.projectsHeaderActions}>
|
||||
<button
|
||||
className={sidebarStyles.workspaceHeaderAction}
|
||||
type="button"
|
||||
@@ -549,7 +579,13 @@ export function StandaloneRecents({
|
||||
</div>
|
||||
)}
|
||||
{(expanded || Boolean(query)) && (
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<div
|
||||
className={
|
||||
archiveState === 'active'
|
||||
? sidebarStyles.pinnedSessionList
|
||||
: 'flex flex-col gap-0.5'
|
||||
}
|
||||
>
|
||||
{displayedSessions.map((session) => {
|
||||
const isCurrent = session.sessionId === currentSessionId;
|
||||
return renderSession(session, {
|
||||
|
||||
@@ -110,12 +110,10 @@ test('keeps workspace navigation reachable inside a standalone chat @smoke', asy
|
||||
|
||||
// Row actions are hidden until the row is hovered.
|
||||
await workspaceRow.hover();
|
||||
const newTaskButtons = sidebar.getByRole('button', {
|
||||
name: 'New session',
|
||||
exact: true,
|
||||
});
|
||||
await expect(newTaskButtons).toHaveCount(2);
|
||||
await newTaskButtons.last().click();
|
||||
await workspaceRow
|
||||
.locator('..')
|
||||
.getByRole('button', { name: 'New session', exact: true })
|
||||
.click();
|
||||
|
||||
await fillComposer(page, 'Back to the workspace');
|
||||
await page.locator('[data-web-shell-composer-submit]').click();
|
||||
@@ -267,6 +265,86 @@ test('no-workspace sessions remain visible beside collapsed Workspaces and persi
|
||||
);
|
||||
});
|
||||
|
||||
test('no-workspace category matches peer header geometry and expansion affordances @smoke', async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
const scenario = createWebShellDaemonScenario({
|
||||
capabilities: {
|
||||
features: [
|
||||
...standaloneFeatures,
|
||||
'workspace_qualified_rest_core',
|
||||
'session_organization',
|
||||
],
|
||||
},
|
||||
standaloneSessions: [standaloneSummary('standalone-header', 'Standalone')],
|
||||
});
|
||||
scenario.sessions[0].isPinned = true;
|
||||
await installScenario(page, scenario, testInfo);
|
||||
await page.goto('/');
|
||||
|
||||
const sidebar = page.getByRole('complementary');
|
||||
const names = ['Pinned', 'Workspaces', 'No-workspace sessions', 'Recent'];
|
||||
const toggles = names.map((name) =>
|
||||
sidebar.getByRole('button', { name, exact: true }).first(),
|
||||
);
|
||||
for (const toggle of toggles) {
|
||||
await expect(toggle).toHaveAttribute('aria-expanded', 'true');
|
||||
await toggle.click();
|
||||
await expect(toggle).toHaveAttribute('aria-expanded', 'false');
|
||||
}
|
||||
|
||||
const geometry = await Promise.all(
|
||||
toggles.map((toggle) =>
|
||||
toggle.evaluate((button) => {
|
||||
const header = button.parentElement!;
|
||||
const label = button.querySelector('span')!;
|
||||
const style = getComputedStyle(button);
|
||||
return {
|
||||
height: header.getBoundingClientRect().height,
|
||||
labelLeft: label.getBoundingClientRect().left,
|
||||
labelTop:
|
||||
label.getBoundingClientRect().top -
|
||||
header.getBoundingClientRect().top,
|
||||
color: style.color,
|
||||
marginBottom: getComputedStyle(header.parentElement!).marginBottom,
|
||||
};
|
||||
}),
|
||||
),
|
||||
);
|
||||
for (const header of geometry) {
|
||||
expect(header.height).toBe(32);
|
||||
expect(header.labelLeft).toBe(geometry[1].labelLeft);
|
||||
expect(header.labelTop).toBe(geometry[1].labelTop);
|
||||
expect(header.color).toBe(geometry[1].color);
|
||||
}
|
||||
expect(geometry[2].marginBottom).toBe('0px');
|
||||
|
||||
const toggle = toggles[2];
|
||||
const chevron = toggle.locator('svg');
|
||||
await expect(chevron).toHaveCount(1);
|
||||
await expect(chevron).toHaveAttribute('aria-hidden', 'true');
|
||||
const labelBox = await toggle.locator('span').boundingBox();
|
||||
const iconBox = await chevron.boundingBox();
|
||||
expect(iconBox!.x).toBeGreaterThanOrEqual(labelBox!.x + labelBox!.width);
|
||||
await page.mouse.move(1000, 700);
|
||||
await expect(chevron).toHaveCSS('opacity', '0');
|
||||
await toggle.hover();
|
||||
await expect(chevron).toHaveCSS('opacity', '1');
|
||||
await page.mouse.move(1000, 700);
|
||||
await page.keyboard.press('Tab');
|
||||
await toggle.focus();
|
||||
await expect(chevron).toHaveCSS('opacity', '1');
|
||||
const collapsedPath = await chevron.locator('path').getAttribute('d');
|
||||
await page.keyboard.press('Enter');
|
||||
await expect(toggle).toHaveAttribute('aria-expanded', 'true');
|
||||
await expect(chevron.locator('path')).not.toHaveAttribute(
|
||||
'd',
|
||||
collapsedPath!,
|
||||
);
|
||||
await page.keyboard.press('Space');
|
||||
await expect(toggle).toHaveAttribute('aria-expanded', 'false');
|
||||
});
|
||||
|
||||
test('standalone Recents keeps lifecycle actions on exact standalone routes @smoke', async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
|
||||
Reference in new issue
Block a user