mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-18 05:04:45 +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>
101 lines
2.9 KiB
TypeScript
101 lines
2.9 KiB
TypeScript
// @vitest-environment jsdom
|
|
|
|
import { act } from 'react';
|
|
import { createRoot, type Root } from 'react-dom/client';
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
import { I18nProvider } from '../i18n';
|
|
import { WorkspaceSelector } from './WorkspaceSelector';
|
|
|
|
let root: Root | undefined;
|
|
let container: HTMLDivElement | undefined;
|
|
|
|
afterEach(async () => {
|
|
await act(async () => root?.unmount());
|
|
container?.remove();
|
|
});
|
|
|
|
function renderSelector(
|
|
overrides: Partial<React.ComponentProps<typeof WorkspaceSelector>> = {},
|
|
) {
|
|
container = document.createElement('div');
|
|
document.body.appendChild(container);
|
|
root = createRoot(container);
|
|
act(() => {
|
|
root?.render(
|
|
<I18nProvider language="en">
|
|
<WorkspaceSelector
|
|
workspaces={[
|
|
{
|
|
id: 'primary',
|
|
cwd: '/primary',
|
|
label: 'primary',
|
|
primary: true,
|
|
trusted: true,
|
|
},
|
|
{
|
|
id: 'locked',
|
|
cwd: '/locked',
|
|
label: 'locked',
|
|
primary: false,
|
|
trusted: false,
|
|
},
|
|
]}
|
|
scratchSupported
|
|
existingFolderSupported
|
|
onSelectWorkspace={vi.fn()}
|
|
onCreateScratch={vi.fn()}
|
|
onOpenExistingFolder={vi.fn()}
|
|
{...overrides}
|
|
/>
|
|
</I18nProvider>,
|
|
);
|
|
});
|
|
return container;
|
|
}
|
|
|
|
describe('WorkspaceSelector', () => {
|
|
it('hides for a single workspace without creation capabilities', () => {
|
|
const element = renderSelector({
|
|
workspaces: [
|
|
{
|
|
id: 'primary',
|
|
cwd: '/primary',
|
|
label: 'primary',
|
|
primary: true,
|
|
trusted: true,
|
|
},
|
|
],
|
|
scratchSupported: false,
|
|
existingFolderSupported: false,
|
|
});
|
|
expect(element.querySelector('button')).toBeNull();
|
|
});
|
|
|
|
it('gates creation actions and disables untrusted workspaces', async () => {
|
|
const onCreateScratch = vi.fn();
|
|
const element = renderSelector({ onCreateScratch });
|
|
const trigger = element.querySelector('button')!;
|
|
await act(async () => {
|
|
trigger.dispatchEvent(
|
|
new MouseEvent('pointerdown', { bubbles: true, button: 0 }),
|
|
);
|
|
});
|
|
|
|
expect(document.body.textContent).toContain('New workspace');
|
|
expect(document.body.textContent).toContain('untrusted');
|
|
const newWorkspace = document.querySelector(
|
|
'[data-slot="dropdown-menu-sub-trigger"]',
|
|
)!;
|
|
await act(async () => {
|
|
newWorkspace.dispatchEvent(
|
|
new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }),
|
|
);
|
|
});
|
|
expect(document.body.textContent).toContain('Start from scratch');
|
|
expect(document.body.textContent).toContain('Use an existing folder');
|
|
const locked = [
|
|
...document.querySelectorAll('[role="menuitemradio"]'),
|
|
].find((entry) => entry.textContent?.includes('locked'));
|
|
expect(locked?.getAttribute('data-disabled')).not.toBeNull();
|
|
});
|
|
});
|