Commit graph

4 commits

Author SHA1 Message Date
Shaojin Wen
49497f5076
feat(web-shell): color-code each split pane by workspace (#6971)
* 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>
2026-07-16 02:38:58 +00:00
Shaojin Wen
38429bc100
fix(web-shell): show workspace chip tooltip on narrow composer (#6958)
* fix(web-shell): show workspace chip tooltip on narrow composer

The composer's workspace chip surfaced its full cwd only through a native
`title` attribute, unlike the sibling git-branch and model chips which use a
styled Radix tooltip. On a narrow (split-screen / mobile) composer the chip
ellipsizes or collapses to an icon, so the workspace is discoverable only on
hover — and a native `title` is inconsistent and never fires on touch.

Give WorkspaceIndicator the same Radix tooltip as GitBranchIndicator (with the
full cwd as content), completing the documented "mirrors GitBranchIndicator"
intent. Its visually-hidden tooltip mirror also exposes the cwd to screen
readers, which the native `title` did not do reliably.

* test(web-shell): assert the workspace tooltip renders on hover

Address review feedback: the WorkspaceIndicator tests checked the
`data-web-shell-workspace-title` hook but never opened the tooltip, so a
regression rendering the short name (or nothing) in the Radix `TooltipContent`
would have gone unnoticed. Open the tooltip via a `pointermove` (jsdom has no
`PointerEvent`; Radix opens on mouse move after `delayDuration`) and assert the
portalled `[role="tooltip"]` shows the full cwd — and, in compact mode, assert
the `workspaceChipCompact` icon-only class is actually applied.

* test(web-shell): guard the compact chip before asserting on it

Move the `if (!chip)` null guard above the assertions in the compact-mode test
so a failure to render surfaces the descriptive "workspace chip was not
rendered" error instead of an opaque `expect(undefined)` throw from the
optional-chained access. Matches the first test in the file.

---------

Co-authored-by: wenshao <wenshao@example.com>
2026-07-15 12:32:17 +00:00
ytahdn
f88a8aa6fc
feat(web-shell): use popovers for composer controls (#6877)
* feat(web-shell): use popovers for composer controls

* fix(web-shell): address popover review feedback

* fix(web-shell): update popover regression coverage

* fix(web-shell): address popover review feedback

* fix(web-shell): stabilize toolbar label collapse

---------

Co-authored-by: ytahdn <ytahdn@gmail.com>
2026-07-15 04:54:47 +00:00
Shaojin Wen
720d26b1bb
feat(web-shell): flesh out the multi-workspace split view (cross-workspace sessions, workspace labels, responsive layout) (#6746)
* fix(web-shell): surface cross-workspace sessions in split view & overview

The split-view "add session" picker and the Session Overview only listed the
primary workspace's sessions, so on a multi-workspace daemon
(`qwen serve --workspace A --workspace B …`) sessions in non-primary
workspaces could not be picked into a split pane or triaged from the overview
— even though the sidebar already lists them and the load path already drives
them.

Both surfaces now merge the primary workspace's sessions with the live
sessions of every other trusted workspace, label each session by its
workspace, and — in the split view — attach each pane under its session's own
workspace so a non-primary session no longer 409s against the primary cwd. On
a single-workspace daemon the behavior is unchanged.

- add useOtherWorkspaceSessions: fans out listWorkspaceSessions over the
  non-primary trusted workspaces in capabilities.workspaces (Promise.allSettled,
  tolerant of one failing), and empty on a single-workspace daemon
- SplitView: merge the lists, tag picker items by workspace, and pass each
  pane's workspaceCwd to its DaemonSessionProvider
- SessionOverviewPanel: merge the lists and add a per-card workspace badge
- add utils/workspace helpers, plus unit tests across all four surfaces

* fix(web-shell): repair stale composerTagIcons import that broke the build

ScheduledTasksDialog imported getComposerTagIconUrl from `../composerTagIcons`,
a module deleted when the helper was consolidated into `utils/composerTag.ts`.
The stale path failed `vite build` (and the web-shell browser-regression e2e).
Point it at the current location.

* fix(web-shell): address review feedback for cross-workspace sessions

- useOtherWorkspaceSessions: return the shared EMPTY sentinel when every
  workspace responds empty (skip a no-op re-render), guard the load effect
  against a stale in-flight fetch overwriting a newer one, and keep the
  single-workspace path fully synchronous (no fetch, no post-render setState)
- fetch non-primary workspaces at the primary list's page size so a busy
  workspace is not truncated at the daemon's smaller default
- split picker: tag primary sessions with the same localized "primary" label
  the Session Overview uses, so both surfaces read consistently
- tests: cover reload(), the poll re-query, empty-primary merge, and the
  primary badge render; quieten the act() warnings

* feat(web-shell): show each split pane's workspace in its composer placeholder

In a multi-workspace split, once a session becomes a pane there was nothing
indicating which workspace it belongs to. Name the pane's own workspace in the
composer placeholder ("Message this session in <workspace>…"), so it's clear
which workspace a message is going to before you send it. Single-workspace
daemons are unchanged.

* Revert "feat(web-shell): show each split pane's workspace in its composer placeholder"

This reverts commit c58e667127.

* feat(web-shell): label each split pane's workspace in its composer toolbar

On a multi-workspace daemon, split-view panes can hold sessions from
different workspaces, but nothing showed which workspace a pane's message
would go to. Add a compact, non-interactive workspace chip to the pane
composer toolbar (next to where the git-branch chip sits), mirroring
GitBranchIndicator.

The chip renders only on a multi-workspace daemon, is fed each pane's
workspace explicitly by the split view (which knows it per session), and
keeps its label visible as panes narrow — only tightening and truncating
rather than collapsing to an icon — since it is the pane's identity.

* feat(web-shell): show the workspace chip in the main composer too

Extend the per-pane workspace label to the main (single-session) chat
composer: on a multi-workspace daemon the composer toolbar now names the
current session's workspace, so it is always clear which workspace a
message targets — not only in split view.

Place the workspace chip before the git-branch chip. Reuses the existing
WorkspaceIndicator and `workspace` toolbar action, fed from the active
connection's capabilities and workspace cwd.

* fix(web-shell): keep a session when a shrink closes the split view

When the viewport shrinks below the large-screen breakpoint the split
view auto-closes and folds back to the single chat. If that chat had no
session of its own — the common case when the split was opened straight
from the Session Overview or a `?split=a,b` link — the user was stranded
on an empty "new chat". Fall back to the split's first pane instead.

Best-effort and gated to the uncontrolled (standalone) split: a load
failure (e.g. a non-primary-workspace session the single connection
cannot own) simply leaves the empty chat, i.e. the previous behavior.

* fix(web-shell): restore the split view when the screen grows back

A shrink below the large-screen breakpoint folds the split view down to
the single chat (its entry points are hidden on small screens). That fold
used to be permanent — widening the window back left the user on a single
chat with their panes gone. Fold it away only temporarily instead:
remember that a shrink folded it, and restore the same split once the
screen grows back past the breakpoint, so a transient resize is lossless.

Standalone/uncontrolled split only; a controlled host still owns its own
split lifecycle. While folded, the narrow chat still falls back to the
split's first pane so it isn't an empty new chat.

* fix(web-shell): keep the chat's git branch when folding the split on shrink

Folding the split on a shrink no longer switches the single chat's
session. The previous shrink-time loadSession(firstPane) re-pointed the
main connection at the split's first pane; when that pane lived in a
different (e.g. git-less) workspace it wiped the chat's git branch — and
more broadly changed the session/URL the user drops back to. Fold the
split away without touching the chat's connection, so its session, git
branch and URL are exactly what they were once the screen grows back.

* feat(web-shell): auto-collapse the sidebar in a narrow split view

In split view the session sidebar competes with the panes for width. Below
1200px it now auto-collapses to its icon rail so the panes get the room,
and expands again once the window grows back — the session list and the
"New chat" label no longer eat space a narrow split needs. A wide split
(>= 1200px) keeps the full sidebar and the user's own collapse preference;
nothing changes outside split view.

* test(web-shell): cover the cross-workspace fetch race, quiet split-picker act() warnings

Address review suggestions on the cross-workspace session listing:
- useOtherWorkspaceSessions: add a test that a stale in-flight fetch is
  discarded by the `cancelled` guard when the target workspace set changes
  mid-flight (a workspace registered/unregistered while a list is loading).
- SplitView: flush after opening the picker so the reload()/reloadOther()
  the picker-open effect fires no longer leak act() warnings in the two
  cross-workspace tests.

* test(web-shell): use a valid DaemonMode and assert the pane workspace prop

Two review suggestions on the cross-workspace tests:
- workspace.test.ts: the caps() mock used `mode: 'workspace'`, not a valid
  DaemonMode (`'http-bridge' | 'native'`) — use `'native'`.
- SplitView.test.tsx: the ChatPane mock now captures `workspaceCwd`, and the
  cross-workspace attach test asserts the pane receives it (for the composer
  chip) — so dropping that prop pass-through would now fail the test rather
  than only the provider's `data-workspace`.

* perf(web-shell): memoize pane toolbar actions; cover chip tooltip fallback & deep-link remount

Address review suggestions on the composer workspace chip:
- ChatPane: memoize `paneToolbarActions` so its array identity is stable —
  ChatEditor is `React.memo`, and a fresh `[...]` each render defeated it,
  re-rendering the composer on every pane render.
- ChatEditor.test: cover the `workspaceTitle ?? workspaceName` tooltip
  fallback (chip rendered with a name but no explicit title).
- SplitView.test: cover the deep-link remount path — a pane mounted before
  the other-workspace fan-out resolves remounts under its own workspace once
  `workspaceCwdById` populates (pane key `b1:` → `b1:/wsB`).

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-07-12 11:55:21 +00:00