* 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>
Add support for creating sessions in isolated git worktrees from the
Web Shell, enabling multiple tasks to run in parallel within the same
workspace without polluting the main working directory.
Daemon:
- POST /session accepts optional worktree param, creates worktree via
GitWorktreeService, relocates session via changeSessionCwd
- Worktree metadata persisted in SessionEntry, BridgeSessionSummary,
and sidecar file (<sessionId>.worktree.json) for daemon restart
recovery
- GET /workspaces/:workspace/git supports ?cwd= for worktree-scoped
git status queries (path.resolve + containment check)
SDK:
- CreateSessionRequest/DaemonSession/DaemonSessionSummary gain
worktree field; DaemonSessionClient exposes worktree getter
- WorkspaceDaemonClient.workspaceGit() accepts optional cwd param
Web Shell:
- Workspace branch pill dropdown offers 'New Worktree Task' (git repos
only) with purple GitForkIcon and description
- Git chip turns purple with GitForkIcon for worktree sessions
- Session list shows inline ⑂ badge for worktree sessions
- Empty-state welcome badge explains worktree isolation
- Git status queries target worktree path, not workspace root
- session_cwd_changed event filtered from chat transcript
Design doc: docs/design/2026-07-19-webshell-worktree-sessions.md
* feat(web-shell): color-code each split pane by workspace
On a narrow split (split-screen / mobile), it was hard to tell which
workspace each pane belonged to: a pane's header showed only its session
name, and the sole workspace signal — the composer chip at the bottom —
collapsed to a bare folder icon that looked identical for every
workspace, so the workspace was discoverable only by hovering each one.
Surface the workspace where you actually scan — the pane header — and
give each workspace a stable accent color so panes read apart at a
glance and same-workspace panes read as a group:
- Add a colored workspace tag (dot + basename) at the start of each
pane header on a multi-workspace daemon, and colorize the header
divider with the same accent. The dot never shrinks, so panes stay
distinguishable even when the name and session title ellipsize.
- Derive a stable per-workspace color from the workspace's position in
the daemon's advertised workspaces[], reusing the sidebar
session-group palette so the two surfaces speak the same color
language. Extracted into a shared workspaceAccent.module.css.
- Tint the composer workspace chip with the same accent (folder + faint
background) so it stays distinguishable even in its icon-only compact
state, instead of a generic folder.
Single-workspace daemons are unchanged: no tag, and the header divider
falls back to the neutral border.
* refactor(web-shell): address review on split-pane workspace accent
- Rename workspaceAccent.module.css -> WorkspaceAccent.module.css to match the
PascalCase convention used by every other component .module.css; update both
import sites.
- Hoist the four raw-hex accent colors (red/orange/yellow/green) into shared
--accent-* theme tokens in App.module.css, and point the workspace accent
module, the sidebar group dots, and the overview badges at them. The palette
now has a single source of truth and can't drift between the four surfaces
(values are unchanged, so rendering is identical).
- Add a compile-time exhaustiveness guard so adding a
DaemonSessionGroupPresetColor without extending WORKSPACE_ACCENT_COLORS (and
its CSS class) fails the build instead of silently dropping that accent.
- Give the pane-header workspace tag role="img" so its "Workspace: <name>"
aria-label is reliably announced; aria-label on a bare span (generic role)
is not.
* refactor(web-shell): address follow-up review on workspace accent
- Hoist the four --accent-* tokens out of both theme blocks into the
theme-independent .app scope, so they are declared once (the values do not
vary by theme) — a genuine single declaration rather than two kept in sync.
- Add a dev-only runtime check that every accent color has a matching class in
WorkspaceAccent.module.css, closing the gap the compile-time guard cannot
cover: CSS modules are typed Record<string, string>, so a renamed/removed
class would otherwise silently drop that color's accent.
- Rename the "same workspace same color" test to describe what it actually
asserts (a stable color per cwd, and distinct colors across workspaces).
* refactor(web-shell): address second follow-up review on workspace accent
- WorkspaceIndicator tests: assert on imported CSS-module class names instead of
string literals, so a CSS-module naming change can't silently make the
substring checks vacuous; add an expanded-mode (non-compact) accent test so a
refactor that gated the accent on `compact` would be caught.
- workspaceColor.ts: run the CSS-class contract check unconditionally — throw in
dev, but console.error in production — so a missing class in a prod build is
at least diagnosable instead of a silent accent drop.
- WorkspaceAccent.module.css: correct the docstring to state exactly which
tokens come from where — red/orange/yellow/green from --accent-* in
App.module.css, blue/purple deliberately reusing the --agent-* brand tokens.
---------
Co-authored-by: wenshao <wenshao@example.com>
* feat(web-shell): support custom Hex session group colors
* docs(web-shell): add custom group color screenshot
* fix(web-shell): address custom Hex color review feedback
* fix(web-shell): validate group presets against daemon catalog and auto-prefix Hex input
Review follow-ups for the custom Hex group color editor:
- Validate the preset branch against the daemon-provided color catalog
instead of the hardcoded palette, so future preset additions stay
selectable in the editor.
- Auto-prefix bare values with '#' in the Hex field so pasted bare Hex
validates, and free text can no longer collide with a preset name and
silently flip the select out of Custom mode.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(web-shell): cap custom Hex input length
---------
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(web-shell): add workspace picker for new sessions (issue #6378 phase 4)
Multi-workspace daemons now show a new-session workspace picker in the sidebar (default primary, untrusted disabled); the chosen workspace cwd is sent on POST /session so the session spawns in that workspace. daemon-react-sdk createSession gains an optional per-call workspaceCwd override covering both the detached and active-session paths; omitting it preserves the previous primary behavior.
* feat(web-shell): workspace management with dynamic registration
Replace the new-session workspace picker with a full workspace
management sidebar. Registered workspaces render as a parallel,
collapsible list (folder icon per workspace), each with its own
sessions nested underneath, and a "+" entry registers an existing
directory as a new workspace at runtime with no daemon restart.
Backend: WorkspaceRegistry becomes mutable (add()/onChange()); a new
POST /workspaces route validates the directory (exists, not a
duplicate, not nested) and registers it; run-qwen-serve exposes a
runtime factory that builds a complete workspace runtime (bridge, fs
factory, channel factory, workspace service) on demand. The SDK
DaemonClient and daemon-react-sdk gain addWorkspace().
* fix(web-shell): show newly registered workspace without a reload
Registering a workspace via the sidebar "+" left the list unchanged
until a full page reload. handleAddWorkspace called
workspace.getCapabilities(), which returns a cached promise and only
feeds setCapabilities from the mount effect, so the refresh was a no-op.
Add DaemonWorkspaceProvider.refreshCapabilities(): it bypasses the
promise cache, issues a fresh /capabilities fetch, and pushes the
result into state so consumers re-render. handleAddWorkspace now awaits
it (best-effort, so a refresh failure never masks a successful
registration).
* fix(web-shell): address review feedback for workspace management
- registry: list() returns a frozen snapshot so callers can't mutate the
internal runtimes array (restores the push()-throws invariant)
- POST /workspaces: reject relative paths on the raw input, canonicalize
via realpath so symlink aliases can't bypass the duplicate/nesting
checks, and serialize concurrent registrations to close a TOCTOU race
that leaked bridge/channel infrastructure
- sidebar: restore a compact single-workspace project header (name,
search toggle, collapse) so single-workspace users keep those
affordances and searchOpen/projectExpanded are no longer dead
- daemon session: include the target workspace in the create-session
failure message
- tests: rework WebShellSidebar tests for the WorkspaceSection UI (add
the useWorkspace mock, query workspace buttons, cover primary->undefined),
use the canonical DaemonWorkspaceCapability type, and add a createSession
workspaceCwd forwarding test
* fix(cli): harden dynamic workspace registration per review
- POST /workspaces: bound cwd by MAX_WORKSPACE_PATH_LENGTH before any
filesystem work, and return a generic 500 (log the full error to
stderr) so responses can't leak internal filesystem paths
- createDynamicWorkspaceRuntime: log a stderr warning when a workspace's
settings can't be read, matching the startup secondary-workspace path
* qwen: address PR review feedback (#6625)
Dynamic workspace reloadDaemonEnv now mirrors the startup secondary path:
after reloadEnvironment() it rebuilds the runtime env via
buildRuntimeEnvironment(), calls wsEnv.replace(), and updates the env
metadata (envFileReadFailed / envFileReadFailures / overlayKeys /
envFilePaths). Without this, .env changes on a dynamically registered
workspace never propagated to that workspace's spawned child processes.
* qwen: address PR review feedback (#6625)
Harden POST /workspaces and the workspace registry per review:
- canonicalize with realpathSync.native (matches startup) so the same
physical dir on a case-insensitive FS can't register twice
- nesting guard now also checks in-flight registrations, closing a
concurrent parent/child registration race
- error responses no longer echo resolved/other-workspace paths
- registry add() isolates onChange listener throws so a bad listener
can't abort a caller after the workspace is already committed
* qwen: address PR review feedback (#6625)
- POST /workspaces: cap total registered workspaces (startup + dynamic)
to guard against unbounded registration exhausting resources
- createDynamicWorkspaceRuntime: register shutdown-cleanup arrays only
after the runtime is fully built, so a throw during workspace-service
construction can't orphan the bridge/channel
- web-shell App: reset selectedWorkspaceCwd after session creation so the
workspace picker is one-shot (next new chat defaults to primary)
* qwen: address human review suggestions (batch 1)
- WorkspaceSection: add console.warn on session-poll failure (was silent)
- WorkspaceSection: add aria-expanded for screen readers
- AddWorkspaceDialog: associate label/input (htmlFor/id), i18n the
absolute-path error, accept Windows drive-letter paths
- i18n: remove unused workspaceUntrustedHint key, add addWorkspaceAbsError
* qwen: address human review suggestions (batch 2)
- Remove dead CSS (.workspacePickerSelect, .workspaceItem* classes from
the old select-based picker, replaced by WorkspaceSection)
- Add title tooltip to single-workspace project name (shows full path)
- WorkspaceSection: sync expanded state on workspace.primary change
* qwen: address human review suggestions (batch 3)
- DaemonWorkspaceProvider: refreshCapabilities now clears error on
success and sets error+status on failure (was incomplete vs mount)
- Remove unused onChange/WorkspaceRegistryEvent from workspace registry
per simplicity-first (no consumer exists; defers API surface until
a real subscriber like SSE push is needed)
* qwen: add workspace-management route test coverage
Tests cover: 501 (no factory), 400 (missing/empty/relative/long/
nonexistent cwd), 409 (duplicate canonical path), 201 (success),
and verifying error messages are generic (no path leak).
* qwen: fix CI build failure — add explicit types in route test
The CLI's tsconfig includes test files in tsc --build, so all
noImplicitAny violations in tests cause build failures. Add explicit
type annotations to mock parameters.
* qwen: add type/title to single-workspace add-button
---------
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Prevent the 'Settings' label from wrapping to a new line when the
sidebar is narrow. Instead, the text is clipped via overflow:hidden
and only the gear icon remains visible.
Place the Qwen brand mark to the left of the sidebar's New chat button.
The artwork is the same SVG used for the browser-tab favicon (and the
QwenLM GitHub avatar), inlined rather than hot-linked because the Web
Shell CSP is `img-src 'self' data: blob:`, which blocks remote images.
When the sidebar is collapsed there is no room beside the compact
button, so the mark is hidden and only the New chat button remains.
Add an Archive quick action and a "..." overflow menu (Rename / Archive / Delete) to each session row in the web-shell sidebar, plus a collapsible "Archived" section that lazily lists archived sessions with Restore / Delete. Thread the daemon's existing archiveState filter and archive/unarchive endpoints through the webui workspace facade and the useDaemonSessions hook; rename stays limited to the current live session.
* feat(web-shell): show the qwen-code version in the sidebar footer
The Web Shell had no visible version. Show the running qwen-code version (from the daemon capabilities) in the sidebar footer, inline with the Settings button so it stays visible without taking its own row.
Render the version consistently wherever it appears:
- Prefix "v" only for a real semver release; a non-semver fallback such as "unknown" is shown as-is, so we never render a bogus "vunknown". Applied to the Web Shell badge and the TUI header.
- Dev builds (scripts/dev.js) now report the real package version instead of the "dev" sentinel, matching scripts/start.js, so the UI shows the actual version (e.g. v0.19.4). DEV=true / NODE_ENV=development remain the signals that mark a dev build.
* test: add readFileSync to node:fs mock in dev.test.js
scripts/dev.js now reads package.json via readFileSync at module load to
report the real CLI_VERSION, but the node:fs mock in dev.test.js did not
export readFileSync, causing vitest to throw "No readFileSync export is
defined on the node:fs mock" and failing the suite.
* feat(web-shell): add mobile sidebar drawer with session list
Replace the display:none behavior at viewport <=760px with an overlay
drawer pattern. A hamburger menu button appears on mobile, tapping it
slides the existing WebShellSidebar in as a fixed overlay with a
semi-transparent backdrop. Selecting or creating a session auto-closes
the drawer. Desktop layout (>=761px) is unaffected.
Closes#6000
* fix(web-shell): address review feedback for mobile sidebar drawer
- Use display:contents for desktop wrapper transparency (Critical: sidebar was hidden)
- Fix z-index stacking so sidebar renders above backdrop in drawer
- Force sidebar expand when mobile drawer is open (collapsed state)
- Hide resizeHandle on mobile to prevent touch scroll conflicts
- Reset drawer state on viewport resize via matchMedia listener
- Add role=dialog, aria-modal, Escape key dismissal, body scroll lock
- Add aria-expanded to hamburger button
- Close drawer when opening Settings or resuming sessions
* fix(web-shell): address second round of review feedback
- Remove dead :global(.sidebar) selector (CSS Modules hash class names)
- Fix Escape key capture-phase handler to not intercept sidebar inputs
- Conditionally apply role=dialog/aria-modal only when drawer is open
- Stop toggling collapsed prop on drawer open/close to preserve sidebar state
- Add closeMobileDrawer() for bare /resume command path
- Fix hamburger button vertical centering in empty chat state on mobile
* fix(web-shell): fix stacking context and escape handler in mobile drawer
* fix(web-shell): prevent iOS Safari background scroll when drawer is open
* chore: remove accidentally committed .qwen-session and gitignore it
The .qwen-session file is a developer-local session UUID generated by
qwen serve. It was accidentally committed to the repo and should never
be tracked.
* fix(web-shell): address review feedback for mobile drawer
- Don't preventDefault touchmove inside the drawer so the session list
can scroll natively; only block scrolling on the page behind it.
- Defer Escape to a pending tool/permission approval (reject) instead of
closing the drawer when a prompt is visible.
- Reuse isEditableTarget from utils/dom and only bail out for editable
targets outside the drawer, so the drawer search input still closes on
the first Escape.
- Close the drawer before awaiting loadSession so it doesn't linger over
the old transcript, matching the other session-switch paths.
- Keep the drawer panel visible until the backdrop finishes fading out to
avoid a one-frame flicker on close.
Co-Authored-By: Qwen-Coder <noreply@qwen.ai>
* fix(web-shell): mobile drawer ignores collapsed rail + block backdrop scroll
- collapsed: a user who collapsed the desktop sidebar got a mobile drawer that
still rendered as the icon rail (no session list — the whole point of the
drawer). Force the expanded layout while the drawer is open.
- touchmove: the allowlist matched the outer [data-mobile-drawer] wrapper, which
also contains the full-screen backdrop, so a touchmove starting on the dim
backdrop skipped preventDefault and let iOS Safari scroll the page behind.
Exclude the backdrop so only the panel keeps native scroll.
Co-Authored-By: Qwen-Coder <noreply@qwen.ai>
* fix(web-shell): harden mobile drawer collapse, error path, and width cap
- Hide the sidebar collapse button while the mobile drawer is open so its
no-op toggle can no longer silently persist desktop collapsed state.
- Close the drawer before awaiting createSession() so a failed create no
longer leaves the drawer stuck open with page scroll locked.
- Drop redundant width/min-width/position from .sidebar.mobileOpen and cap
it with max-width:100vw so a wide persisted width can't overflow phones.
Co-Authored-By: Qwen-Coder <noreply@qwen.ai>
---------
Co-authored-by: pomelo-nwu <czynwu@gmail.com>
Co-authored-by: Qwen-Coder <noreply@qwen.ai>