mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-03 13:24:41 +00:00
* feat(web-shell): add managed workspace selector Let Web Shell create and select daemon-managed workspaces without changing ownership of existing sessions. - Add capability-gated existing and scratch workspace registration - Validate scratch roots, trust provenance, capacity, and shutdown races - Serialize workspace mutations, session switching, and refresh results - Add SDK/WebUI wiring and focused cross-package regression coverage # Conflicts: # packages/web-shell/client/App.tsx # packages/web-shell/client/components/sidebar/WebShellSidebar.tsx # Conflicts: # packages/cli/src/serve/capabilities.ts # packages/cli/src/serve/routes/workspace-management.ts # packages/cli/src/serve/server.test.ts # packages/sdk-typescript/src/daemon/DaemonClient.ts # packages/web-shell/client/App.tsx # packages/web-shell/client/components/dialogs/AddWorkspaceDialog.tsx # packages/web-shell/client/components/sidebar/WebShellSidebar.tsx * fix(web-shell): revalidate workspace before session creation Prevent a stale workspace selection from bypassing the latest trusted capability snapshot during lazy session creation. - Validate the selected workspace before passing it to the daemon - Fall back to the primary workspace when trust has been revoked - Add a regression test for the pre-effect race window - Remove stale branch state and clarify add-workspace ownership * fix(web-shell): improve workspace removal feedback Keep workspace removal controls legible and make blocked force removals visibly inactive. - Size the action menu independently from its narrow icon trigger - Add a disabled affordance and suppress destructive hover styling - Cover the removal menu width override with a regression test * fix(web-shell): centralize existing workspace registration Route sidebar and composer entry points through the App-owned dialog so capability gating and workspace reconciliation remain consistent. - Forward display names only when the daemon advertises support - Hide and suppress persistence when registration is runtime-only - Mark directory registrations with existing-workspace provenance - Cover both entry points and capability combinations with tests * fix(web-shell): address review feedback on workspace dialogs and capability docs (#7390) - Document dynamic_workspace_registration and scratch_workspace_registration in the conditional serve-features table so the capabilities-docs-contract test passes. - Gate DialogShell backdrop-click and Escape dismissal on the dismissible prop so non-dismissible dialogs ignore both gestures. - Surface an inline error when an added folder registers but the capability refresh fails, mirroring the scratch recovery path. - Add coverage for the active-session workspace switch and the add-folder refresh-failure paths. * fix(web-shell): address review feedback on workspace dialogs and capability docs (#7390) --------- Co-authored-by: heyang.why <heyang.why@alibaba-inc.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com> Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
165 lines
5.1 KiB
TypeScript
165 lines
5.1 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
import { act } from 'react';
|
|
import { createRoot, type Root } from 'react-dom/client';
|
|
import { I18nProvider } from '../../i18n';
|
|
import { ThemeProvider } from '../../themeContext';
|
|
import { DialogShell } from './DialogShell';
|
|
|
|
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
|
|
|
|
let container: HTMLDivElement | null = null;
|
|
let root: Root | null = null;
|
|
|
|
function render(showBottom: boolean, onTopClose = vi.fn()) {
|
|
root!.render(
|
|
<I18nProvider language="en">
|
|
<ThemeProvider value="dark">
|
|
{showBottom && (
|
|
<DialogShell title="Bottom" onClose={vi.fn()}>
|
|
<button type="button">bottom</button>
|
|
</DialogShell>
|
|
)}
|
|
<DialogShell title="Top" onClose={onTopClose}>
|
|
<button type="button" data-testid="top-focus">
|
|
top
|
|
</button>
|
|
</DialogShell>
|
|
</ThemeProvider>
|
|
</I18nProvider>,
|
|
);
|
|
}
|
|
|
|
afterEach(() => {
|
|
act(() => root?.unmount());
|
|
container?.remove();
|
|
root = null;
|
|
container = null;
|
|
});
|
|
|
|
describe('DialogShell', () => {
|
|
it('restores focus to the remaining top shell when a lower shell unmounts', () => {
|
|
container = document.createElement('div');
|
|
document.body.appendChild(container);
|
|
root = createRoot(container);
|
|
|
|
act(() => render(true));
|
|
const topButton = document.querySelector<HTMLElement>(
|
|
'[data-testid="top-focus"]',
|
|
)!;
|
|
document.querySelector<HTMLElement>('button:not([data-testid])')!.focus();
|
|
|
|
act(() => render(false));
|
|
|
|
expect(document.activeElement).toBe(topButton);
|
|
});
|
|
|
|
it('leaves an IME Escape event unhandled', () => {
|
|
container = document.createElement('div');
|
|
document.body.appendChild(container);
|
|
root = createRoot(container);
|
|
const onClose = vi.fn();
|
|
|
|
act(() => render(false, onClose));
|
|
const event = new KeyboardEvent('keydown', {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
isComposing: true,
|
|
key: 'Escape',
|
|
});
|
|
const target = document.querySelector<HTMLElement>(
|
|
'[data-testid="top-focus"]',
|
|
)!;
|
|
act(() => target.dispatchEvent(event));
|
|
|
|
expect(event.defaultPrevented).toBe(false);
|
|
expect(event.key).toBe('Escape');
|
|
expect(onClose).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('closes once when the backdrop is clicked', () => {
|
|
container = document.createElement('div');
|
|
document.body.appendChild(container);
|
|
root = createRoot(container);
|
|
const onClose = vi.fn();
|
|
|
|
act(() => render(false, onClose));
|
|
const backdrop = document.querySelector<HTMLElement>(
|
|
'[data-slot="dialog-overlay"]',
|
|
)!;
|
|
act(() => {
|
|
backdrop.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true }));
|
|
backdrop.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
|
|
backdrop.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
|
|
backdrop.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
});
|
|
|
|
expect(onClose).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('stays open when a drag starts in the panel and ends on the backdrop', () => {
|
|
container = document.createElement('div');
|
|
document.body.appendChild(container);
|
|
root = createRoot(container);
|
|
const onClose = vi.fn();
|
|
|
|
act(() => render(false, onClose));
|
|
const backdrop = document.querySelector<HTMLElement>(
|
|
'[data-slot="dialog-overlay"]',
|
|
)!;
|
|
const panel = document.querySelector<HTMLElement>('[role="dialog"]')!;
|
|
act(() => {
|
|
panel.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true }));
|
|
panel.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
|
|
backdrop.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
|
|
backdrop.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
});
|
|
|
|
expect(onClose).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('ignores backdrop clicks and Escape when not dismissible', () => {
|
|
container = document.createElement('div');
|
|
document.body.appendChild(container);
|
|
root = createRoot(container);
|
|
const onClose = vi.fn();
|
|
|
|
act(() => {
|
|
root!.render(
|
|
<I18nProvider language="en">
|
|
<ThemeProvider value="dark">
|
|
<DialogShell title="Locked" onClose={onClose} dismissible={false}>
|
|
<button type="button" data-testid="locked-focus">
|
|
locked
|
|
</button>
|
|
</DialogShell>
|
|
</ThemeProvider>
|
|
</I18nProvider>,
|
|
);
|
|
});
|
|
|
|
const backdrop = document.querySelector<HTMLElement>(
|
|
'[data-slot="dialog-overlay"]',
|
|
)!;
|
|
act(() => {
|
|
backdrop.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
|
|
backdrop.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
|
|
backdrop.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
});
|
|
expect(onClose).not.toHaveBeenCalled();
|
|
|
|
const target = document.querySelector<HTMLElement>(
|
|
'[data-testid="locked-focus"]',
|
|
)!;
|
|
act(() =>
|
|
target.dispatchEvent(
|
|
new KeyboardEvent('keydown', {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
key: 'Escape',
|
|
}),
|
|
),
|
|
);
|
|
expect(onClose).not.toHaveBeenCalled();
|
|
});
|
|
});
|