Commit graph

2 commits

Author SHA1 Message Date
Shaojin Wen
732d85df39
fix(web-shell): correct Add Workspace dialog theming and multi-workspace session rows (#6705)
* fix(web-shell): theme and lay out the Add Workspace dialog correctly

The dialog's stylesheet referenced CSS variables that are defined nowhere
(--text-secondary, --input-bg, --border-color, --accent-color, --hover-bg), so
they fell back to hardcoded dark values — in light mode the input rendered
dark-on-light with a purple focus ring, inconsistent with every other dialog. It
also reused a two-column form-row grid meant for label/value pairs, which cramped
the path input into a narrow column.

Rebuild the dialog on the shared dialog primitives (themed .dialog-form input,
dialog-inline-button, dialog-primary-button) so it tracks the active theme in
both light and dark and gives the path a full-width input. Add a proactive
absolute-path hint, an accessible inline error (role="alert", aria-describedby,
aria-invalid) that clears as you type, and a localized "Adding…" state. Keep the
hint and error as siblings of the input rather than nested in the label, so their
text stays out of the input's accessible name.

* fix(web-shell): render full session rows for every workspace in the sidebar

Registering a second workspace switched the sidebar to the multi-workspace view,
which rendered each workspace's sessions with a bespoke minimal row: a smaller
font (12px vs 14px) and no per-session actions. The rich single-workspace row
(hover actions, current-session highlight, inline rename, running/unread state)
was hidden entirely, so even the primary workspace's list degraded.

Render sessions through the sidebar's shared renderSessionRow so every workspace
matches the single-workspace list. Gate the mutation actions to the primary
(daemon-bound) workspace: the daemon can't resolve another workspace's session
for pin/archive/export/delete (they 404 or silently no-op), so non-primary rows
are read-only — they keep click-to-load and the font/highlight but drop the
action buttons. Session mutations now also bump the per-workspace poll token so
those lists refresh promptly.

* refactor(web-shell): move Add Workspace footer padding into a CSS class

The footer sits inside .dialog-form (which already pads its sides) and reuses the
shared dialog-footer-actions primitive, doubling the horizontal padding; the
override lived as an inline style, hiding the deviation from the stylesheet. Move
it to a local .footer class applied alongside the shared class. A doubled
selector keeps it winning over the primitive regardless of stylesheet order.

* test(web-shell): cover workspace read-only gating and Add Workspace dialog

Add tests for the behaviors introduced by the sidebar and dialog fixes:
- non-primary workspace rows render the session but expose no action buttons
  (the daemon, bound to the primary workspace, can't service their mutations),
  while the primary workspace keeps its full actions;
- a session mutation re-polls the per-workspace list instead of waiting for the
  10s interval;
- the Add Workspace dialog's absolute-path validation, accessible error wiring
  (role="alert", aria-describedby/aria-invalid), error-clear-on-edit, trimmed
  submit, and onAdd-failure handling.

* refactor(web-shell): centralize the workspace reload-token bump in a helper

Per review: the setWorkspaceSessionsReloadToken bump was repeated verbatim across
the session-mutation handlers. Extract a stable bumpWorkspaceReload() helper and
route every site through it, including assignSessionGroup and assignSessionColor
— the two organization mutations that were missing the bump — so assigning a
group or color now also re-polls the per-workspace session lists instead of
waiting for the 10s interval.

* test(web-shell): cover Windows paths and non-Error rejections in Add Workspace dialog

Per review: add the two untested handleSubmit branches — a Windows-style absolute
path accepted by the drive-letter regex, and a non-Error onAdd rejection falling
back to the generic error message.

* fix(web-shell): gate inline rename on readOnly and refresh on group-create assign

Per review: the readOnly option hid the hover action buttons but not the inline
rename — onDoubleClick and the isEditing branch still fired on read-only rows, so
a session shown in multiple workspaces could render an editable rename input on
its non-primary (read-only) copy. Gate both on !readOnly. Also add the missing
bumpWorkspaceReload() to saveGroupEditor's create-with-target-session path so
that organization mutation refreshes the per-workspace lists like the others.

* test(web-shell): cover readOnly rename gating and dialog submitting state

Per review: assert the read-only (non-primary) row does not open the inline
rename form when the shared session is renamed from the primary row, and that the
Add Workspace dialog shows the localized "Adding…" label with disabled controls
while onAdd is pending.
2026-07-11 11:19:32 +00:00
jinye
2523a36b52
feat(web-shell): workspace management sidebar with dynamic registration (daemon multi-workspace phase 4) (#6625)
* 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>
2026-07-10 16:30:20 +00:00