The toolbar popover used a fixed 18rem width, truncating long model
names in the dropdown list. Change to max-content so the popover
adapts to its content width (capped at the available viewport width).
Also add title attributes on dropdown items and the model trigger
button so the full name is accessible via hover and screen readers.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(web-shell): render a plain textarea composer on touch devices
Mobile browsers could not type into the Web Shell composer (#5958):
CodeMirror's contenteditable interacts poorly with virtual keyboards, and
three non-gesture view.focus() calls claim activeElement on iOS without
opening the keyboard, after which taps may never refocus the editor.
On touch devices ('(hover: none) and (pointer: coarse)' plus
maxTouchPoints > 0 — touch laptops keep the desktop editor) useComposerCore
now skips creating an EditorView entirely and exposes a mobileComposer
backend that ChatEditor renders as a controlled <textarea> at the same
mount point. The internal submit pipeline was hoisted out of the
editor-creation effect and accepts view: EditorView | null, so history,
prompt building, tags, images, and slash/! text interpretation are shared
unchanged between both backends. Enter inserts a newline natively;
submission goes through the Send button.
Programmatic (non-gesture) focus is additionally suppressed on
coarse-pointer devices even when CodeMirror is forced, and
?composer=textarea|codemirror serves as a debugging and rollback escape
hatch. The choice is frozen at mount so a mid-session flip cannot drop the
draft.
Known textarea-backend degradations (commands still work as typed text):
no slash/@ completion menus, no inline tag chips (fall back to the top
placement), no history arrow navigation, no large-paste placeholders, and
no followup Tab-accept.
Fixes#5958
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(web-shell): address review — textarea auto-grow, change notifications, caret restore
Address the five inline review suggestions on #7587:
- Auto-grow the mobile textarea with its content, capped by the computed
CSS max-height (so --chat-editor-input-max-height overrides stay
authoritative). Previously rows={1} plus resize:none meant multi-line
drafts scrolled inside ~1.5 visible lines and the CSS max-height was
dead. Asserted in the mobile e2e spec via bounding-box growth.
- Fire onInputTextChange from setMobileText, matching the CodeMirror
updateListener contract: programmatic draft changes (setText, history
restore, post-submit clear) now notify parent trackers too.
handleMobileChange delegates to setMobileText.
- Restore the caret after mobile insertText: a controlled textarea resets
the caret to the end on value change; setSelectionRange puts it back
after React re-renders (rAF with a setTimeout fallback), matching the
CodeMirror path's explicit selection anchor.
- Cover the mobile submitSearchMatch path: select a history match, submit
through the shared pipeline, draft cleared.
- Cover the ChatEditor mobile quick-action gating: the history quick
action opens the search UI (never dispatches into a missing EditorView)
and the keyboard shortcut hints grid is hidden on the mobile composer
with a desktop control.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(web-shell): decide the touch composer by media query alone
Independent macOS verification on #7587 found that Playwright's stock
WebKit iPhone profiles match '(hover: none) and (pointer: coarse)' but
report navigator.maxTouchPoints === 0, so the automatic detection selected
CodeMirror under unmodified WebKit emulation.
The maxTouchPoints requirement added nothing the AND media query does not
already provide: touch laptops are excluded by the query itself (their
primary pointer hovers and is fine), and the only devices that match the
query with zero touch points are emulated profiles and TV-style browsers,
where the plain textarea is a safe fallback. Dropping it makes stock
iPhone/WebKit Playwright runs exercise the automatic detection branch.
Real-device behavior is unchanged: phones and tablets match the query and
report touch points either way.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(web-shell): keep the mobile textarea scrollable past its height cap
As .editorArea's last child the textarea inherited `overflow: clip` from
the `.editorArea > :last-child` wrapper rule (written for the CodeMirror
container, whose inner .cm-scroller does the scrolling). `clip` also
forbids programmatic scrolling, so once auto-grow reached the CSS
max-height, content beyond the cap was unreachable — scrollTop stayed
pinned at 0.
Override with `overflow-y: auto` via `.editorArea > textarea.mobileTextarea`
(the extra type selector outweighs the wrapper rule's specificity). New
mobile e2e regression fills 20 lines, asserts growth stops at the computed
300px cap, and verifies the overflow stays reachable: scrollHeight above
clientHeight and scrollTop actually moving to the bottom — the exact probe
from the review, which pinned at 0 before this fix.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: ComplexSimply <rudy.arrowsong@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.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): git status chip, visual working-tree diff, and sidebar git status
Bring working-tree Git awareness to the Web Shell (browser daemon session UI):
- Toolbar branch chip becomes a live status indicator: dirty (staged/unstaged/
untracked), ahead/behind upstream, stash count, detached HEAD, in-progress
operation (merge/rebase/cherry-pick/revert/bisect), and conflict count, each
with a non-color cue.
- Read-only "Changes" dialog: working-tree-vs-HEAD file list with per-file,
line-level, per-side syntax-highlighted diffs; opens via /diff or a dirty
chip; untracked files expand as fully-added and deleted files still diff.
- Per-workspace git status in the sidebar: a compact icon-only chip per trusted
workspace (status dot + hover tooltip); click opens that workspace's dialog.
All git access goes through the daemon REST API with per-workspace trust
gating; new SDK status fields are optional and additive (v2).
* fix(web-shell): themed tooltips and git-chip review follow-ups
Tooltips now render on the themed popover surface (bg-popover /
text-popover-foreground / border + fill-popover arrow) instead of the
inverted bg-foreground default, so they read dark-on-dark rather than a
bright box on the dark theme. Fixing the shared primitive corrects the
git branch tooltip in the composer toolbar and sidebar, plus every other
tooltip, at once.
Also addressing review feedback on the git integration:
- Replace the hand-drawn detached/conflict/stash SVG icons with
lucide-react (CircleDot / TriangleAlert / Layers) per the web-shell
icon convention.
- Gate the tooltip "Working tree clean" message on an enriched status
(computedAt) so a branch-only status no longer asserts clean.
- Include the file path in the diff dialog row aria-label so screen
readers can distinguish files.
- Reset the toolbar git chip on workspace switch so it never shows the
previous repo's branch/counts while the new fetch resolves.
- Log a sidebar git poll failure only on the success->failure transition
to avoid spamming a long-lived tab.
- Correct the SDK doc for DaemonWorkspaceGitDiffFile.added/removed
(0, not undefined, for binary files).
* fix(web-shell): address git-integration review suggestions
Follow-ups from the /review pass on the git integration:
- GitBranchIndicator: include the short SHA in the detached-HEAD tooltip
title, and add the "Working tree clean" status to the aria-label (gated
on an enriched status, matching the tooltip) so the two never drift.
- WorkspaceSection: keep the last known git status on a transient poll
failure instead of blanking the chip for a whole interval.
- App: surface a toast for `/diff` when no workspace is available instead
of silently consuming the composer input.
- Tests: cover the diff dialog's list-load and per-file load error paths,
and detectGitOperation's revert/bisect branches.
- Design doc: align the getGitWorkingTreeStatus spec text with the
decision (transient states return status with `operation`; null is
reserved for non-repo / git failure).
* fix(web-shell): focus-visible ring for git chip button; align doc poll interval
- Add a :focus-visible outline to .gitBranchChipButton so keyboard users
get a visible focus indicator (the chip resets UA button chrome).
- Design doc: align the active-workspace poll-interval references at 30s
to match the implementation.
* fix(web-shell): surface capped diffs, catch row-build failures, cover degradation paths
Address the remaining review findings on the git integration:
- Truncation is no longer silent: fetchGitDiffHunksForFile now returns
{ hunks, truncated } — the parser records files that actually lost
lines to MAX_LINES_PER_FILE (tracked path), and the untracked
synthesis reports its byte/line caps. The route forwards an additive
`truncated` flag on the hunks response (absent when not truncated, so
older clients and daemons are unaffected), and the Changes dialog
renders a "Diff truncated" note under the visible window.
- DiffHunks catches an unexpected buildRows rejection (e.g. malformed
hunk lines) and shows the per-file error instead of leaving an
unhandled rejection and a silently empty diff area.
- New tests: untracked and tracked truncation at the core caps, the
route's truncated passthrough (and its absence when clean), the
branch-only degradation when the working-tree summary throws, the
malformed-hunks error path, and the Shiki success path (a fake
tokenizer proving add rows pull new-side tokens and del rows pull
old-side tokens, not the plain-text fallback).
* fix(web-shell): drop dialog backdrop-blur that froze the page on open
The dialog and alert-dialog overlays applied `backdrop-blur-xs`, which
forces the browser to rasterize and blur the entire content behind the
overlay when a dialog opens. With a long transcript behind it, that
main-thread paint+blur froze the whole page — e.g. clicking the git
branch chip to open the Changes dialog. Keep the bg-black/10 scrim for
separation and drop the blur.
* fix(core): guard synthesizeUntrackedHunk against non-regular files
synthesizeUntrackedHunk opened an untracked path before checking its
type, so an untracked FIFO (listed by `ls-files --others`) would block
on open() forever waiting on a writer — hanging the daemon's event loop
and leaving the Web Shell Changes dialog stuck on a permanent loading
state. lstat-gate on regular files before opening, matching the existing
guard in countUntrackedLines. Adds a FIFO regression test.
* fix(web-shell,core): rename expansion, no-newline marker, chip measurement
Round-5 review Criticals:
- core: key renamed diff entries by the real (post-rename) path and carry
the old path for display, so renamed rows can be expanded — the synthetic
`old => new` key was sent to git as a nonexistent literal path. The diff
dialog renders the rename as `old → new`.
- core: preserve Git's `\ No newline at end of file` marker through the hunk
parser so a trailing-newline-only edit isn't shown as identical
removed/added lines (the viewer already renders it as a meta row).
- web-shell: the toolbar's hidden git-chip measurement replica now renders
the full chip content via the extracted GitBranchChipContent, so the
expanded width includes the status indicators and the compact/expanded
toggle no longer oscillates near the responsive threshold.
* fix(build): generate git-commit info even when prepare build is skipped
The review tooling runs `npm ci` with QWEN_SKIP_PREPARE=1 (to skip the
heavy prepare build) and then builds only the changed workspaces. Because
`prepare` exited before generating the gitignored git-commit.ts, a
per-workspace build of packages/cli failed at the unchanged systemInfo.ts
on the missing `../generated/git-commit.js` module. Generate the git-commit
info in the skip path too — it is cheap and never fails hard — so a later
per-workspace build or typecheck finds the module. The non-skip path still
generates it via `npm run build`.
* fix(web-shell,cli): address round-6 review suggestions
- cli: carry the pre-rename path (oldPath) through DiffRenderRow and show
renamed files as `old → new` in both the Ink and plain-text renderers.
The rename-keying fix updated the daemon and web-shell dialog but not the
CLI `/diff` renderer, which silently dropped the old path.
- web-shell: key DiffFileRow by workspace + path so switching workspace
remounts the row instead of reusing another workspace's hunks/open state
for a path both workspaces share.
- web-shell: show a loading placeholder in DiffHunks while rows are (re)built
(e.g. after a theme switch) instead of an empty, jumpily-resized box.
- web-shell: cover the /diff local intercept in App.test.tsx (opens the
Changes dialog and is not forwarded to the agent).
* fix(web-shell,cli,core): address round-7 review suggestions
- cli: sanitize the rendered filename (and pre-rename oldPath) in the Ink
DiffStatsDisplay via sanitizeFilenameForDisplay, matching the plain-text
renderer so a crafted path can't inject into the interactive view.
- cli: apply the read headers before awaiting the per-file diff fetch (as
handleDiffList does) so error responses also carry no-store/nosniff.
- cli + web-shell: strip Unicode bidi embedding/isolate controls
(U+202A-202E, U+2066-2069) in the filename/control-char sanitizers so a
crafted filename can't visually spoof its extension.
- core: guard countStashEntries with an lstat type check before readFile, so
a symlink-to-FIFO at logs/refs/stash can't block the event loop (the same
hazard already guarded in the untracked-file readers).
- core: cover fetchGitDiffHunksForFile's transient-state guard with a test
(the sibling helpers already had one).
* fix(web-shell,cli,core): address round-8 review suggestions
- core: pass --no-optional-locks to the ls-files call in
fetchGitDiffHunksForFile, matching the other runGit calls so it doesn't
contend for an optional index-refresh lock alongside concurrent git
add/commit.
- cli: add a route test asserting a rename's oldPath survives serialization
end-to-end (keyed by the new path, old path carried alongside).
- web-shell: add a GitDiffDialog test for the hiddenCount>0 "N more files
not shown" note (every payload previously used hiddenCount: 0).
- web-shell: drop the nonexistent primaryLabel prop from the WorkspaceSection
test (it is not a WorkspaceSectionProps member).
- docs: correct the plan doc — large-diff virtual scrolling was explicitly
descoped (core caps + per-file lazy loading), not implemented in Phase 2.
* fix(cli,web-shell): address round-9 review findings
- cli: propagate the pre-rename oldPath through DiffDialog's
perFileToUnified and render renamed files as `old → new` in the
interactive diff viewer (the rename-keying fix had updated the daemon,
the web-shell dialog, and the /diff stats, but not this viewer).
- cli: cover DiffStatsDisplay's rename (`old → new`) rendering and the
sanitizeFilenameForDisplay path for hostile filenames carrying control
characters.
- web-shell: guard the GitBranchIndicator test afterEach against
double-unmounting an already-unmounted root (the localization tests
assert on getTranslator without calling render()).
* fix(core,cli,web-shell): rename-aware single-file diff (old→new)
fetchGitDiffHunksForFile pathspec-limited the diff to the new path, which
defeats git's rename detection — a renamed file was reported as fully
added (every line +) instead of its actual edit. Thread an optional
pre-rename path through the single-file endpoint (core → route → SDK →
dialog) and diff old→new with -M when it is present, so expanding a
renamed file shows its real content change.
* fix(cli): address round-10 review suggestions
- DiffDialog: split the path-width budget between old and new paths for a
rename (reserving the " → " separator) so the combined width stays within
maxPathChars instead of overflowing the row layout.
- textUtils: extend MULTILINE_CONTROL_CHARS_REGEX with the Unicode bidi
ranges (matching FILENAME_CONTROL_CHARS_REGEX) and add a test that
sanitizeFilenameForDisplay strips bidi embedding/isolate controls.
- workspace-git-diff route: add a test that ?oldPath= is parsed and
forwarded to fetchGitDiffHunksForFile.
* test(sdk),docs: cover diff client methods; align design doc
- sdk: add DaemonClient unit tests for workspaceGitDiff() and
workspaceGitDiffFile(path, oldPath?) — URL construction (incl. urlEncode
on path/oldPath, with and without oldPath, plus the workspace-qualified
route) and response deserialization, mirroring the existing workspaceGit()
test.
- docs: add the oldPath? param to the workspaceGitDiffFile API spec; record
that the diff client methods now have unit tests (correcting the claim
that workspaceGit() had none); attribute the bundle-limit bump to
packages/sdk-typescript/scripts/build.js; clarify ahead/behind are relative
to upstream (0, and ↑N/↓N not shown, without one).
* fix(web-shell,core): address round-11 review suggestions
- GitBranchIndicator: count conflicted entries as dirty — a merge where every
changed file is conflicted (staged=unstaged=untracked=0) is still
uncommitted, so the expanded chip's dirty dot / data-dirty now reflect it.
- core: split the status branch line at the last "..." (the branch/upstream
separator) so a dotted branch name isn't truncated at the first "...".
- GitDiffDialog: guard DiffFileRow's in-flight fetch against unmount via a
cancelled ref, matching DiffHunks / GitDiffDialog.
- tests: forward oldPath when expanding a renamed file in the web-shell
dialog; bidi-strip coverage for the web-shell sanitizeControlChars;
untrusted-guard coverage on the single-file diff route; conflicted-only
dirty; branch-line "..." split.
* fix(web-shell,cli): address round-12 review suggestions
- DiffDialog: only render the rename "old → new" when there's room for both
sides (≥19 cols, so each gets ≥8); otherwise fall back to the new path
alone, so a narrow terminal no longer overflows the row (the Math.max(8,…)
floor could exceed maxPathChars).
- GitBranchIndicator test: guard afterEach container.remove() for non-render
tests run in isolation, and make the compact-mode ↑-suppression assertion
non-vacuous by giving the fixture an ahead count.
- App: compute the active workspace once (useMemo) and share it between the
git-status effect and the Changes-dialog entry point, so the chip and the
dialog can't drift onto different repos.
* fix(core,docs): address round-13 review suggestions
- core: add a rebase-apply detection test (git am / an interrupted
`rebase --apply` creates rebase-apply, which detectGitOperation also maps
to 'rebase'); previously only rebase-merge was exercised.
- docs: correct section 5 to describe the actual diff-dialog mechanism
(diffWorkspaceCwd state, not the stale activePanel design).
* test(core): cover stray no-newline marker before any hunk header
parseGitDiff's pre-hunk guard already skips a "\ No newline at end of
file" marker that appears before any @@ header, so a malformed/truncated
diff can't throw on a null currentHunk and lose subsequent files' hunks;
add a regression test pinning that behavior.
* fix(web-shell): unstick per-file diff loading and skip non-path git poll
- DiffFileRow: reset the cancelled-fetch flag on mount so StrictMode's
mount/unmount/mount replay no longer leaves it latched at true, which
dropped the fetched hunks and froze the row on "Loading changes…" despite
a 200 response.
- WorkspaceSection: skip the git status poll when the workspace cwd is not an
absolute path. A synthetic fallback workspace carries a display name there,
which the cwd-qualified route rejects with a 400.
* fix(web-shell,cli): address review suggestions on the git diff surface
- GitDiffDialog: highlight each diff side independently so a small side
keeps syntax highlighting even when the other side exceeds the size cap
(the old guard dropped both as soon as either was too large).
- ChatEditor: complete the .gitBranchChipButton reset (font/color/padding/
margin) so the clickable dirty-tree chip matches the read-only output chip
instead of picking up UA button styling.
- DiffDialog: cover the interactive rename display (old to new on a wide
terminal), mirroring the rename tests DiffStatsDisplay and GitDiffDialog
already have.
* test(web-shell,cli): cover git chip clean/reload/traversal paths, fix doc
- GitDiffDialog: add the missing expect(header).not.toBeNull() guard to the
three expand-file tests that lacked it, matching the others in the block.
- GitBranchIndicator: cover the known-clean aria-label branch (computedAt set
and every change counter zero).
- WorkspaceSection: verify a reloadToken change re-fetches git status instead
of waiting for the next 60s poll.
- workspace-git-diff route: verify a traversal oldPath is forwarded to core
and surfaced as available:false rather than escaping the workspace.
- Design doc: /diff is handled via setDiffWorkspaceCwd, not setActivePanel.
* fix(core): allow literal `..foo` paths in diff normalization
- toRepoRelativePath: reject only a real climb-out (`..` or `../…`), not a
literal `..foo` filename at the repo root, which the bare startsWith('..')
over-rejected, leaving the diff viewer unable to render such a file.
- parseGitDiff: cover the truncatedPaths output set directly (it was only
exercised indirectly through fetchGitDiffHunksForFile).
---------
Co-authored-by: wenshao <wenshao@example.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
* 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>
* 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>
The Git branch chip kept max-width: 180px on narrow screens while the
mode/model buttons collapse to icon-only, so inside the wrapping
.toolbarLeft it claimed roughly half the row and pushed mode/model onto
a second line — making the composer taller (#6725).
Within the existing @container (max-width: 699px) block, keep the leading
controls on one row (flex-wrap: nowrap) and let the branch chip yield
space first, truncating via its existing ellipsis down to just the icon
if needed (max-width: 140px; flex-shrink: 1). The mode/model buttons stay
fixed-size. Desktop (composer > 699px) is unchanged.
* feat(web-shell): show more slash commands with category headers
The slash-command menu capped its visible height at exactly four rows, so
with 40+ merged commands users had to scroll a thin list to find anything,
and the built-in custom/skill/system grouping was only a faint 1px divider
with no label.
Raise the cap to min(12 rows, 40vh) and render the category name as a visible
header at each group boundary (custom / skill / system), keeping the divider
between groups. Sub-command menus are ungrouped and unchanged.
* feat(web-shell): fuzzy-match slash commands and show per-group counts
Typing in the slash menu now fuzzy-ranks commands with the same fzf engine the
TUI uses, so abbreviated input like "mdl" finds "model" and "arf" finds
"agent-reproduce-feature" — substring matching alone could not. An empty query
still browses the category-ordered list; a non-empty query switches to a flat
relevance-ranked list (headers are dropped since results interleave categories).
Each category header also shows how many commands the group holds (e.g. "Skill
commands 28"), so the volume hidden below the fold is visible at a glance.
The fzf index is built once per command set (keyed on the array identity) and
falls back to substring filtering if construction fails.
* refactor(web-shell): address slash menu review feedback
- Extract the section header/divider boundary logic into a pure
`planSlashSectionRows` helper and unit-test it (headers at group
boundaries, first row header without a divider, no repeated headers for
adjacent duplicate sections, per-group counts). This also moves the
section-count computation past the `!anchorRect` early return so it no
longer runs on first render.
- Simplify `--slash-panel-max-height` to a round `min(460px, 45vh)` instead
of a `12 * rowHeight` formula that ignored header/divider overhead and so
showed only ~9-10 rows; the panel now shows ~12-13 rows.
- Log a warning when fzf fuzzy search throws before falling back to
substring matching, so a silent failure is diagnosable.
- Add a completion test for the zero-match case returning null.
* fix(web-shell): prevent queued-prompt loss from drain race
The auto-drain effect popped a queued prompt, called setQueuedPrompts,
then submitted via setTimeout(0). Because the daemon flips streamingState
asynchronously, the setState re-render could re-run the effect and pop a
second prompt before the first registered as streaming — both submitted
back-to-back and the first was lost.
Arm an "awaiting turn start" gate synchronously at pop so the re-run is
blocked until streamingState goes non-idle, released by a dedicated
effect with a safety-net timer for a prompt that never streams (e.g. a
queued slash command). Cleanup no longer cancels/re-queues the pending
submit while the gate is armed.
* feat(web-shell): friendlier Esc interruption + queued-prompt UX
* refactor(web-shell): tidy Esc/queue code per review
Behavior-preserving cleanups addressing review feedback on the Esc-interruption
and queued-prompt changes:
- Remove the now-dead queue.footer i18n key (EN + ZH) and the unreferenced
.queuedHint CSS, orphaned when the Esc-clears-queue behavior was dropped.
- Co-locate the queued-prompt styles in QueuedPromptDisplay.module.css instead
of reaching into the parent App.module.css.
- Make the Esc confirm-window constants the single source of truth: export them
from escapeIntent.ts and drive the countdown-ring duration from one of them
via a CSS custom property.
- Nudge the queue-drain safety net with a dedicated tick counter instead of
cloning queuedPrompts, so it no longer re-renders the composer for a no-op.
- Drop a redundant !compact guard in StatusBar left over from flattening a
ternary.
- Document the pop/gate-arm ordering invariant in the drain effect.