Commit graph

13 commits

Author SHA1 Message Date
Shaojin Wen
dade3ab334
feat(web-shell): add git branch picker, commit dialog, and create PR flow (#7731)
* feat(web-shell): add git branch picker, commit dialog, and create PR flow

Add an IntelliJ-style branch picker popover to the web shell git
workspace, accessible from the branch chip in both the composer toolbar
and sidebar. The picker provides search-filtered branch listing (local,
remote, tags, recent), branch checkout, new branch creation, pull, push,
and a commit view integrated into the existing GitDialog.

The commit view reuses the diff panel (expandable file diffs with syntax
highlighting, fullscreen support) and adds a commit message textarea with
Commit / Commit and Push buttons. When a session is available (or one is
auto-created), the commit message and PR title/body are generated via the
model using session side-queries (btwSession), giving the agent full
conversation context for accurate generation.

The Create PR flow provides an inline form with auto-detected base branch
and model-generated title/description, backed by a new daemon route that
shells out to gh pr create.

New daemon routes:
- GET  /workspaces/:workspace/git/branches
- POST /workspaces/:workspace/git/checkout
- POST /workspaces/:workspace/git/branch
- POST /workspaces/:workspace/git/push
- POST /workspaces/:workspace/git/pull
- POST /workspaces/:workspace/git/commit
- POST /workspaces/:workspace/github/prs/create
- GET  /workspaces/:workspace/github/default-branch

* fix(web-shell): resolve correct workspace session for AI generation

The commit message and PR title/body generation now resolves the
most recent session for the target workspace via listWorkspaceSessions,
rather than using the globally active connection.sessionId which may
belong to a different workspace or session. Falls back to creating a
new session only when no sessions exist for the workspace.

Also improves the PR body editor with an Edit/Preview toggle using
the existing Markdown component, and updates the generation prompt
to follow the project PR template structure from AGENTS.md.

* fix(web-shell): stabilize session resolver prop to prevent infinite re-generation

Pass resolveSessionForWorkspace as a stable useCallback reference
instead of an inline arrow function. The inline function created a
new reference on every App render, causing the GitDialog useEffect
to abort and restart generation in an infinite loop.

* fix(web-shell): group remote branches by remote name and add PR target branch dropdown

Remote branches in the branch picker are now grouped by remote
(origin, upstream, etc.) with sub-headers, making fork workflows
clear. The PR create form's base branch field is now a select
dropdown populated from the workspace's branch list, grouped by
remote with optgroup labels, instead of a free-text input.

* fix(web-shell): use ref for session resolver to prevent effect re-run abort

When resolveSessionForWorkspace creates a new session, it updates
connection.sessionId in the provider, which changes the useCallback
reference, which triggers the useEffect to re-run and abort the
in-flight btwSession generation. Store the callback in a ref so the
effect never depends on its identity.

* fix(web-shell): resolve session per workspace, not from global active session

The commit/PR generation effects used connection.sessionId directly
without checking if it belongs to the target workspace. When opening
the commit dialog from a sidebar workspace different from the active
session's workspace, the wrong session was used for generation,
producing incorrect content. Now always routes through
resolveSessionForWorkspace(workspaceCwd) which checks workspace
membership before reusing the active session.

Also replaces 'PR' with 'Pull Request' / '合并请求' in all UI strings
and adds error logging to the generation catch blocks.

* fix(web-shell): retry btwSession with fresh session when stale session detected

When listWorkspaceSessions returns a session that no longer exists in
the daemon's memory (e.g. after daemon restart), btwSession fails with
'No session with id ...'. The generation effects now catch this error,
force-create a new session via resolveSessionForWorkspace(cwd, true),
and retry the btwSession call once. Both btwWithRetry and
resolveSessionForWorkspace are stored in refs to avoid useEffect
dependency chain aborts.

* fix(web-shell): base PR generation on branch diff, not working tree

PR title/body generation now fetches the commit log between the
resolved base branch and HEAD (git log <base>..HEAD) plus any
uncommitted changes, instead of only the working tree diff. The
base branch is resolved inside the effect's promise chain (not
from state) to avoid stale values and dependency warnings. Also
adds range parameter support to fetchGitLog and workspaceGitLog.

* feat(web-shell): replace PR base branch select with searchable popover

The native <select> for the PR target branch is replaced with a
custom searchable popover (BranchSelect) that shows a search input
and a filtered branch list grouped by remote. The default selection
is the target repository's main branch (resolved via
getDefaultBranch). Supports filtering by typing in the search box.

* fix(web-shell): show full remote ref in branch select (origin/main)

Branch select now stores and displays full remote refs like
origin/main instead of stripped names. getDefaultBranch returns
the full ref (origin/main) instead of stripping the prefix.
When creating the PR, the remote prefix is stripped for the
gh pr create --base flag (origin/main → main).

* fix(web-shell): stop pointer propagation in branch picker list to prevent popover dismiss

Radix Popover's outside-click detection was incorrectly firing when
clicking section headers (Recent/Local/Remote/Tags) inside the popover
content, causing the popover to close immediately. Adding
onPointerDown stopPropagation on the list container prevents pointer
events from reaching Radix's document-level handlers.

* fix(web-shell): use onPointerDownOutside guard for branch picker popover

The previous stopPropagation approach failed because Radix Popover
uses capture-phase document listeners for outside-click detection,
which fire before bubble-phase stopPropagation. The correct fix is
onPointerDownOutside on PopoverContent: when Radix incorrectly fires
the outside handler for a click that is actually inside the content
(can happen with portal containers), we check contentRef.contains()
and preventDefault to keep the popover open.

* fix(web-shell): stop click propagation on branch picker popover content

Root cause: the ChatEditor composer container has onClick that calls
core.focus(), stealing focus from the popover. React synthetic events
bubble through the React tree (not DOM tree), so portaled popover
clicks reach the container handler. Radix then detects focus-outside
and dismisses the popover.

Fix: onClick stopPropagation on PopoverContent, matching the existing
pattern in GitModePopover and ToolbarPopover which already have this
fix with an explanatory comment.

* docs: add PR verification screenshots for branch picker feature

* fix(web-shell): mock useWorkspace in tests for BranchPickerPopover

BranchPickerPopover calls useWorkspace() which requires
DaemonWorkspaceProvider context. The existing WorkspaceSection and
ChatEditor tests didn't provide this context, causing 5 test failures.
Added vi.mock with importActual to preserve other exports while
providing a mock useWorkspace. Also updated the git chip click test
to reflect that clicking now opens the branch picker popover instead
of directly calling onOpenGitDiff.

* fix: harden git write paths against argument injection

Address review feedback on the web-shell git surface:

- Reject option/pathspec injection in git checkout ref and branch start
  point (isValidCheckoutRef), and terminate `git checkout` argv with `--`.
- Drop `git log` range values that start with `-` and terminate the argv
  with `--` so a range can never be reinterpreted as `--output=<file>`.
- Fix getDefaultBranch always falling back to origin/main: the promisified
  exec lacked `encoding: 'utf8'`, so stdout was a Buffer and .trim() threw.
- Parameterize ghErrorMessage so `gh pr create` timeouts name the right
  command and duration; sanitize workspace paths in PR-create errors.
- GitDialog: guard doCommit against double-submit (button + keyboard), and
  strip only a known remote prefix from the PR base so local branches with
  "/" are not mangled; use theme tokens for commit button/success colors.
- BranchPickerPopover: guard checkout/new-branch behind busyAction, reset
  inline-input text on reopen, and hide the commit action when unavailable.

Adds regression tests for the checkout/branch validation and the git log
range guard.

* fix(web-shell): address review feedback on branch picker PR (#7731)

- Fix Commit+Push error masking: split try/catch so push failure
  reports alongside the successful commit SHA
- Replace hardcoded screenshot path with captureScreenshot harness
- Replace silent if-isVisible skip with explicit assertion in visual test
- Add focus-visible style for search input accessibility
- Fix CSS specificity for active PR tab hover state
- Add viewChanges to actionsVisible search filter
- Wrap toggleSection in useCallback to avoid unnecessary re-renders
- Add windowsHide: true to getDefaultBranch subprocess
- Fix trailing slash handling in mockDaemon git action routing
- Add git methods to top-level client mock in tests
- Remove dead branchPicker.commitSuccess i18n key
- Remove dead .actionShortcut CSS class
- Show generation failure feedback in commit message placeholder

* fix(web-shell): address review feedback on branch picker PR (#7731)

- Add workspace trust checks to bound git branch routes
- Use workspace-scoped client in BranchPickerPopover (fixes wrong-workspace mutation)
- Add branch name validation and -- terminator to gitCreateBranch
- Filter refs/remotes/*/HEAD from branch listings
- Force LC_ALL=C for reflog parsing (non-English locale fix)
- Narrow 'could not resolve' error regex to avoid DNS false positives
- Add range validation to git log (reject path traversal)
- Fix commit+push error i18n (dedicated key instead of concatenation)
- Add i18n for BranchSelect component strings
- Fix commit tab ARIA attributes
- Add onBranchChanged callback to handlePush
- Add btw to mockDaemon isDaemonPath regex
- Add workspace_github_prs to visuals spec capabilities
- Add -- terminator regression test
- Remove docs/pr-assets/ from repo

* fix(web-shell): address review feedback on branch picker PR (#7731)

* fix(cli): reject dash-prefixed branch name with 400 in branch route (#7731)

* fix(web-shell): address review feedback on git branch picker (#7731)

Security:
- Clear GIT_DIR/GIT_WORK_TREE/GIT_COMMON_DIR/GIT_INDEX_FILE from git
  subprocess env to prevent repository redirection
- Add strict mutation gate to all POST git branch routes
- Add generation guard to qualified write routes
- Fail closed on invalid ?cwd= in mutation routes (resolveContainedCwdOrFail)
- Reject wrong-typed startPoint, fetchOnly, rebase, and PR options with 400

Correctness:
- Filter remote symbolic refs (origin/HEAD) by %(symref) instead of /HEAD
  name suffix, preserving valid branches like feature/HEAD
- Add git rev-parse --git-dir probe so non-git dirs get 404 instead of
  empty available:true
- Push preserves existing upstream; only adds --set-upstream when unset,
  resolving the remote from branch config or the sole configured remote
- git commit -a replaced with git add -A + git commit so untracked files
  displayed in the UI are included
- Always pass --body to gh pr create to prevent interactive prompts
- getDefaultBranch returns null instead of fabricating origin/main
- Memoize workspaceByCwd client in BranchPickerPopover to fix infinite
  render loop
- Move sessionId to a ref in GitDialog effects to prevent self-abort
- Bound commit-message prompt to fit /btw 4096-char limit
- Mark all platforms as unverified in PR template (no fabricated )
- Guard PR auto-fill effect against wiping user edits on reconnect

Accessibility:
- Add tabIndex and onKeyDown to commit-mode tab span
- Add aria-label to BranchSelect trigger and search input

Cleanup:
- Remove dead CSS (.prInputSmall, .prSelect)
- Remove 9 unused i18n keys
- Add ^ to git log range validation regex
- Add busyAction guard to handlePush/handlePull
- Add unit tests for gitCommit, gitPull, and route input validation

* fix(web-shell): address review feedback on git branch picker PR (#7731)

- Change commit tab from <span> to <button> for keyboard accessibility
- Move setCommitMsg('') to success-only branches so the message is
  preserved when push fails after a successful commit
- Add mutate middleware and generationGuard to PR creation route,
  matching all other POST mutation routes
- Set genFailed when session resolution returns undefined so the user
  sees the failure indicator instead of a silent empty textarea
- Make PR number nullable when URL regex does not match instead of
  returning a misleading 0
- Add LC_ALL=C and LANG=C to gitEnv() so for-each-ref upstream track
  parsing is locale-independent
- Validate setUpstream and force as booleans in handlePush, matching
  the existing validation in handlePull
- Add missing workspaceCwd and available fields to test mocks
- Use stable data-web-shell-git-branch attribute in e2e selector

* fix(web-shell): address R5 review feedback on git branch picker PR (#7731)

- Classify git errors on stdout+stderr instead of err.message to fix
  false-positive no_upstream on every push failure and dead
  nothing_to_commit classifier
- Sanitize workspace paths and cap error message length in sendGitError
- Fix remote branch checkout to strip remote prefix so git DWIM creates
  a local tracking branch instead of detaching HEAD
- Restore keyboard accessibility on composer branch chip (span → button)
- Trim startPoint in handleCreateBranch before forwarding to git
- Return bare branch name from getDefaultBranch (strip remote prefix)
- Fix i18n shortcut hint to show ⌘/Ctrl+Enter for cross-platform
- Update aria-label to reflect git management menu, not just changes
- Add available: true to mockDaemon gitDiff default payload
- Add regression tests: upstream preservation, sole remote resolution,
  strengthened fetch-only with divergent remote commit
- Add aria-expanded assertion to sidebar picker test

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(web-shell): address R6 review feedback on git branch picker PR (#7731)

* fix(web-shell): address R7 review feedback on git branch picker PR (#7731)

* fix(web-shell): address R8 review feedback on git branch picker PR (#7731)

* fix(web-shell): address R9 review feedback on git branch picker PR (#7731)

* fix(web-shell): address R10 review feedback on git branch picker PR (#7731)

* fix(web-shell): address R11 review feedback on git branch picker PR (#7731)

* fix(web-shell): address R12 review feedback on git branch picker PR (#7731)

* fix(web-shell): address R13 review feedback on git branch picker PR (#7731)

* fix(web-shell): address R14 review feedback on git branch picker PR (#7731)

* fix(web-shell): address R15 review feedback on git branch picker PR (#7731)

- Validate localName derived from remote-tracking ref to prevent option
  injection (e.g. origin/-f → git checkout -f)
- Add gitCwd prop to BranchPickerPopover and pass it to all git SDK calls
  so worktree sessions target the correct directory
- Add symlink-escape and non-existent-path tests for resolveContainedCwdOrFail
- Pin initial branch name in makeRepo() with git init -b master
- Add gitPull merge and rebase integration tests

* fix(web-shell): address git branch picker review feedback (#7731)

* fix(web-shell): address R6 review feedback on git branch picker PR (#7731)

* fix(web-shell): address review feedback on git branch picker PR (#7731)

- Strip GIT_CONFIG_GLOBAL/SYSTEM/NOSYSTEM in gitEnv to prevent
  inherited config redirection (consistent with extension/github.ts)
- Pass gitCwd to workspaceGitBranches in GitDialog loadPrBranches
  so worktree sessions fetch branches from the correct repository
- Add aria-expanded to collapsible branch section headers
- Add happy-path tests for PR create (201) and default-branch (200)
  routes, including the null fallback to origin/main

* fix(cli): add sendGenerationClosedError to POST routes and cover untested branches (#7731)

* fix(web-shell): address review feedback on branch picker and PR creation (#7731)

- Refresh branch list after push/pull to avoid stale ahead/behind counts
- Add pre-flight check for unpushed branches before PR creation
- Fix base branch prefix stripping when branch list is unavailable
- Cap PR body file list at MAX_SUMMARY_CHARS to bound model prompt size
- Add qualified route tests: trust guard, input validation, cwd containment

* fix(web-shell): hoist MAX_SUMMARY_CHARS to module scope for PR body generation (#7731)

* fix(web-shell): address review feedback for git branch picker (#7731)

- Hoist onOpenCommit to useCallback to fix App.test.tsx prop stability test
- Keep commit tab visible after navigating away (startedInCommit ref)
- Add onClick handler to commit tab for navigation back to commit view
- Fix branch-prefix strip mangling local branch names containing '/'
- Update sessionIdRef after force-creating a stale session replacement
- Pin core.hooksPath in test makeRepo for reliable rollback tests
- Add test asserting --force-with-lease is used for force pushes

* fix(web-shell): target the worktree for sidebar commits and harden git actions (#7731)

Scope the sidebar commit dialog to the active session's worktree checkout
(matching the composer path) so linked-worktree sessions commit to the right
checkout, guard PR creation against a double-click race, and surface an error
when an invalid branch name is submitted. Adds focused coverage for the branch
picker action wiring and the git branch route validation paths.

* fix(web-shell): address review feedback for git branch picker (#7731)

---------

Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
Co-authored-by: Qwen Code <qwen-code@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>
2026-07-28 14:25:11 +00:00
qqqys
1ada1b1fcd
feat(web-shell): manage Channel pairing requests (#7909)
Co-authored-by: qwen-code-dev-bot <269191875+qwen-code-dev-bot@users.noreply.github.com>
2026-07-28 13:41:42 +00:00
qqqys
2db663bec8
feat(web-shell): add Channel configuration flows (#7893)
* feat(web-shell): add Channel configuration flows

* fix(web-shell): hide invalid secret clear action

---------

Co-authored-by: qwen-code-dev-bot <269191875+qwen-code-dev-bot@users.noreply.github.com>
2026-07-28 05:58:26 +00:00
qqqys
3209b89f3b
feat(web-shell): add Channel management page (#7793)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
* feat(web-shell): add Channel management page

* fix(web-shell): address Channel manager review blockers
2026-07-27 17:58:11 +00:00
jinye
2210a18482
feat(web-shell): Scope voice to composer workspace (#7754)
* feat(web-shell): Scope voice to composer workspace

Route voice status, settings, model discovery, and streaming through the workspace that owns each main or split-view composer while preserving legacy primary behavior.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(web-shell): Keep legacy voice fallback scoped

Prevent the Voice-only legacy workspace fallback from activating pre-session git polling, and cover both behaviors together.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(web-shell): Preserve active Voice capture owners

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(web-shell): Pin Voice trust and ambiguity gates

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-07-27 15:47:45 +00:00
Shaojin Wen
a4f5e50d19
feat(web-shell): add read-only GitHub pull requests panel (#7683)
* feat(web-shell): add read-only GitHub pull requests panel

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(web-shell): address review findings for GitHub PRs panel

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(web-shell): clamp GitDialog view when PR capability is withdrawn mid-session

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(web-shell): address review feedback for GitHub PRs panel (#7683)

- Add workspace_github_prs to integration test baseline capabilities
- Sanitize git root path in error responses to prevent path leakage
  when workspace is a repo subdirectory
- Add NEUTRAL check-run conclusion test case
- Add not.toContain path-leak assertion for sanitization test
- Add pending checks indicator UI test
- Add timeAgo utility unit test

* fix(web-shell): address review feedback for GitHub PRs panel (#7683)

- Sanitize workspace paths before truncating the error message so a path
  straddling the 512-char display boundary is redacted, not cut mid-token
- Render a badge for the review_required decision instead of leaving it dead
- Align PR row icon sizes (12px) with sibling git dialogs

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.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>
2026-07-25 02:44:50 +00:00
Shaojin Wen
2709d1fccd
feat(web-shell): surface worktree isolation in the new-session empty state (#7365)
* feat(web-shell): surface worktree isolation in the new-session empty state

The worktree-isolated session entry was buried in the sidebar git-branch
pill dropdown, making it hard to discover. Add a visible toggle to the
chat empty state — the de-facto new-session page — that reuses the
existing pending-worktree state machine and lazy session creation, so no
SDK or daemon changes are needed. Enabling it shows the pending badge
with a cancel affordance; the first prompt then creates the session in an
isolated worktree. The toggle is offered only when the target workspace
is trusted and is a git repository, mirroring the sidebar entry gating.

Also simplify the sidebar git pill: drop the now-redundant "New worktree
task" item and make the pill open the changes view directly instead of a
single-item dropdown.

* chore: add PR verification screenshots for the worktree toggle

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(web-shell): capture the new-session empty state in the visuals suite

The worktree toggle lives in the new-session empty state, and every visuals
scenario navigates to /session/:id via gotoSession — so the suite had never
rendered the empty state at all, and the before/after preview reported "no
screenshot changes" for this PR despite the new UI.

Add a `gotoNewSession` harness helper (primes the theme, lands on `/`, asserts
the theme took effect; no replay to settle) and a `worktree empty state`
scenario using the git-ready workspace this PR already made mockable
(`gitStatus` + the /workspaces/:cwd/git route). It captures both states — the
offered toggle and, after clicking, the pending-worktree badge with its cancel
affordance — and asserts the swap, so a regression fails an assertion rather
than only differing in the screenshot. All four captures are byte-stable
across runs (0% pixel diff).

The helper also closes the structural gap: any future empty-state work
(onboarding copy, first-run affordances) now has a way into the preview.

* refactor(web-shell): drop dead worktree session opt; click-test git chip (#7365)

* fix(web-shell): address review feedback on worktree toggle (#7365)

- Move focus to cancel button on toggle enable and back on cancel (a11y)
- Include branch name in git-pill button aria-label (a11y)
- Replace hardcoded flush() ticks with vi.waitFor() in test helper
- Move git-repo mock default from afterEach to beforeEach
- Add test: sidebar New chat clears pending worktree intent

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: wenshao <wenshao@example.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
2026-07-21 07:38:33 +00:00
callmeYe
414d7d162b
fix(web-shell): respect voice enabled setting (#7345)
* fix(web-shell): respect voice enabled setting

* fix(web-shell): abort capture when voice is hidden
2026-07-20 22:25:35 +00:00
Shaojin Wen
0d9a675c7c
test(web-shell): add extensions-manager visual scenario (#6997)
* test(web-shell): add extensions-manager visual scenario

Add a full-page Extensions manager scenario to the web-shell visual
suite, proving a manager PAGE (not just a transcript or dialog) is
reachable in the mock-daemon harness and captured in both themes.

- Make the mocked workspace extensions scenario-driven: empty by
  default (mirroring skills/settings/tools), so a scenario can seed
  sample extensions via createWebShellDaemonScenario({ extensions }).
- Mock the two endpoints the manager fires on mount so the captured
  page renders without a spurious error banner:
  GET /workspace/extensions/operations (idle poll) and
  POST /workspace/extensions/check-updates (no updates available).
- Seed three extensions (enabled/disabled, marketplace/local, with
  varied capability counts) so the manager renders real cards.

* test(web-shell): structural locators + scenario-driven extension routes

Address review feedback on the extensions-manager visual scenario:

- Gate the scenario on the page heading (a stable `heading` role) and
  assert the seeded card via its `button` role, instead of a bare
  getByText('Context7') that a card-heading refactor or a toast/sidebar
  match could break.
- Wire the /operations and /check-updates mock routes through the
  scenario (new extensionOperations / extensionUpdateCheck fields with
  idle defaults) so a future test can preview an in-flight install or a
  pending update, matching how every other workspace route delegates to
  the scenario rather than returning a hardcoded inline object.

* test(web-shell): serve mocked extensions directly from the scenario

Address review: inline the trivial `workspaceExtensions()` pass-through
at its one call site (`await json(route, scenario.extensions)`) and drop
the function. This matches how the other full-object scenario fields
(providers/skills/settings) are served directly, rather than the
synthesizing helpers (workspaceTools/workspaceMcp) that build a fresh
object each call.

* test(web-shell): assert the disabled, local-source extension renders

Address review: the scenario seeds a disabled/local extension but only
asserted the enabled one, so a regression that hides `isActive: false`
or local-source rows would pass here and only differ in the (visually
reviewed) screenshot. Also assert the "Local Notes" card is visible.

---------

Co-authored-by: wenshao <wenshao@example.com>
2026-07-16 12:43:26 +00:00
ytahdn
bd87dcb5ce
fix(web-shell): filter sessions by source (#6995)
Co-authored-by: ytahdn <ytahdn@gmail.com>
2026-07-16 04:56:50 +00:00
samuelhsin
c8290f8e49
fix(web-shell): persist collapsed session group sections across reload (#6878)
* fix(web-shell): persist collapsed session group sections across reload

Store collapsed section ids in localStorage using the existing
qwen-code-web-shell-* key namespace, and skip the first catalog sync
auto-collapse so restored expand/collapse state survives remount.

Fixes QwenLM/qwen-code#6870

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(web-shell): clarify collapsed-groups demo GIF storyboard

Crop to the sidebar, caption the four beats (expand → collapse →
reload → still collapsed), and keep Pinned out of the organized
session list mock so the Backend collapse is obvious.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web-shell): address collapsed-groups CR feedback

Export the storage key for unit tests, use an explicit first-catalog
latch instead of size===0, and cover corrupt/disabled storage plus
mid-session auto-collapse of newly appeared sections.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web-shell): harden collapsed-groups persistence for CR feedback

Wait for groups+sessions catalog settlement before the initial latch,
persist secondary-workspace collapse via shared namespaced localStorage,
and keep primary/workspace writers from clobbering each other.

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(web-shell): drop demo-capture tooling from collapsed-groups test

The committed GIF, capture script, and frame-assembly helper only served
the PR description's embedded image and were referenced by nothing else
in the repo; the CAPTURE_DEMO branches in the e2e spec were pure
screenshot staging with no assertions. The remaining spec still covers
every acceptance criterion of #6870 and keeps its @smoke tag.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014KuV2hx2ZqEqdRwsJz4KJH

* fix(web-shell): keep collapse latch armed until catalogs settle successfully

Two paths could consume the first-sync latch against a partial catalog
and then auto-collapse (and persist over) the user's restored expansions:
a failed initial sessions/groups request counted as settled, and a
mid-session organization_enabled flip let the auto-collapse effect run
one commit before the groups gate closed. Errors no longer settle either
readiness gate, and the gate now closes during the flip render itself.

Also drop the WorkspaceSection reload effect and exhaustive-deps
suppression that defended a workspace.id change which cannot happen (the
render site keys the component by workspace id), and import the storage
key in tests from collapsedSessionSections directly instead of
re-exporting it through WebShellSidebar.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014KuV2hx2ZqEqdRwsJz4KJH

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-07-15 00:25:54 +00:00
Shaojin Wen
71c7e448f0
feat(web-shell): editable user-scope settings and in-panel model management (#6768)
* feat(web-shell): editable user-scope settings and in-panel model management

Make the Settings panel able to manage ~/.qwen/settings.json (user scope),
and add model management inside the panel's Model category.

User scope: the User tab was read-only; enable writing user-scope settings
end-to-end (route allows the user scope, and the client/UI thread it through),
so the same structured controls edit ~/.qwen/settings.json.

Model management: list configured models (grouped by provider), set the
current model, add a model (reusing the existing provider-setup wizard), and
delete a model. Delete is the only new backend surface: DELETE /workspace/models
rewrites modelProviders in the owning scope, empties (rather than drops) a
provider whose last model is removed so the format-preserving settings writer
clears it cleanly, and clears model.name when the active model is deleted.

Voice Model and Model Fallbacks now use pickers instead of free text: Voice
Model reuses the voice picker; Model Fallbacks opens a multi-select (up to 3,
ordered) whose value round-trips through the comma-separated setting.

* fix(web-shell): label model sub-dialog buttons "Select" not "Edit"

Fast/Vision/Voice Model and Model Fallbacks open a picker, so their
empty-state button now reads "Select" instead of "Edit", which wrongly
implied free-text editing.

* fix(sdk): export DaemonModelDelete{Request,Result} from daemon entry

The DELETE /workspace/models types were added to daemon types but not
re-exported from @qwen-code/sdk/daemon, so consumers resolving the built
dist (webui's dts rollup in CI) failed with TS2305.

* fix(web-shell): address automated review of model management

- reset modelSettingScope when the Add Model (auth) dialog closes too, not
  only the model picker / fallbacks dialog (Critical)
- delete: don't surface a reload failure as "delete failed"
- ModelFallbacksDialog: drop role=listbox/option (no arrow-key nav) for
  aria-pressed toggle buttons matching the click-only interaction
- add :focus-visible outlines to model-management and fallbacks buttons
- mock daemon: handle DELETE /workspace/models
- document first-id-match-wins in removeModelFromProviders
- tests: cover 500 path, broadcast assertions, parseTarget validation codes,
  active-model clearing with a pinned baseUrl, and scope the runtime-model
  no-delete assertion to that row

* fix(serve): keep workspace-qualified settings route workspace-only

Widening VALID_WRITE_SCOPES to include 'user' for the primary
/workspace/settings route also loosened the trust-gated
/workspaces/:workspace/settings route, breaking its deliberate
"reject user scope" contract. Give the qualified route its own
workspace-only scope set; the primary route keeps user scope.

* fix(web-shell): address second review round of model management

- isActiveModelSelection: when the active model is pinned to a baseUrl, an
  id-only delete no longer clears it (may have removed a different variant)
- DELETE /workspace/models: on a partial multi-key persist failure, broadcast
  the committed writes before returning 500 (matches workspace-voice)
- model pickers (fast/vision/voice + fallbacks) read the value for the scope
  being edited, so the User tab no longer shows/clears workspace values
- voice sub-dialog: functional setState so a late loadProviders().then() can't
  clobber a picker the user opened meanwhile
- ModelManagementSection cancel button honors the busy state
- doc fix (emptied provider keys are kept as empty arrays), plus tests for the
  baseUrl-asymmetry and partial-persist paths

* fix(web-shell): address third review round of model management

- DELETE /workspace/models returns a structured partial-persist response
  ({ code: 'partial_persist_error', committedKeys }) so callers can reconcile
- scrub the deleted model id out of modelFallbacks so no dangling reference
  remains; trim padded request fields before matching
- reload workspace settings after a delete so a cleared active model / scrubbed
  fallback isn't shown stale
- narrow the workspace-qualified SDK client back to scope: 'workspace' (that
  route is workspace-only); drop the now-dead qualified scope ternary
- remove the unused providerKey from RemoveModelResult

* fix(web-shell): address fourth review round of model management

- handleFallbacksConfirm isolates the settings-reload failure from the
  save-failed toast (a reload reject no longer looks like a save failure)
- readScopedModelSetting returns only the edited scope's value (no effective
  fallback), so the User tab doesn't show/appear-to-clear inherited values
- Add Model button honors the busy state
- widen DaemonSettingUpdateResult.scope to 'workspace' | 'user' to match the
  server echo
- tests: workspace-scope owner path, modelFallbacks scrub broadcast,
  baseModelId current-match, and a delete target without baseUrl

* fix(web-shell): use theme --error-color for model delete buttons

Replace hardcoded #d64545 with var(--error-color) so the destructive model
buttons match the per-theme error color used across the web-shell (dark
#fc8181 / light #c0362c) instead of a fixed mid-red.

* fix(web-shell): address qwen /review self-review of model management

- surface requiresRestart for modelFallbacks changes: handleFallbacksConfirm
  and handleDeleteModel show the restart notice, and DELETE /workspace/models
  reports requiresRestart when a committed write targets a restart-required key
- only scrub a deleted model from modelFallbacks when no other provider still
  configures the same bare id (fallbacks are bare-id, so a same-id model under
  another provider may still want that fallback)
- widen DaemonModelDeleteResult with requiresRestart; add tests for the
  keep-fallback case and the requiresRestart response

* fix(web-shell): address fifth review round of model management

Backend (mixed-scope correctness for model deletion):
- Clear the active model selection in every writable scope whose own
  selection names the deleted model, comparing against the removed
  entry's stored (unsanitized) baseUrl so a credential-bearing URL is
  still recognized after the providers status sanitizes it.
- Scrub modelFallbacks in its own owning scope rather than the
  modelProviders owner scope; the two are independently scoped.
- removeModelFromProviders now reports removedBaseUrl.

CLI:
- /language ui accepts --project/--global so the settings panel can
  persist a UI-language change to the selected scope while still
  switching the daemon's live locale.

Web-shell:
- Fast-model picker forwards the selected scope via --project/--global.
- Theme/Language controls display the selected scope's value; Language
  persists through the scoped command.
- The model-management "current" badge uses a single-winner,
  endpoint-aware match so a bare current id no longer marks every
  same-base-id row current.
- Serialize Set current through the shared busy flag; reset the recorded
  scope if voice-provider loading rejects.

Tests: mixed-scope route cases, strict-mutation/client-id harness
assertions, SDK setWorkspaceSetting/deleteModel transport tests, a
useDaemonProviders hook test, language scope-flag tests, and dom tests
for the current-badge and fallbacks normalization.

* fix(web-shell): address second qwen /review self-review of model management

- onSubDialog now records the model persist scope per model sub-dialog
  (fast/vision/voice/fallbacks) and no longer for the non-model
  approvalMode dialog — the reset effect is gated on the dialog/fallback/
  auth flags, so it never covers approvalMode and would otherwise leave a
  stale scope for a later command-launched picker. (The flagged voice
  "scope-reset race" does not actually occur: that same effect gating means
  no render between the synchronous scope set and the picker opening
  re-runs it — but the scope handling is now explicit per branch.)
- Add an aria-label to the delete-confirm Cancel button so screen readers
  can tell which model's confirmation is being cancelled.

Tests:
- Unit tests for getWritableScopes / getOwnKeyScope (trust + per-scope
  ownership, incl. explicitly-set falsy values).
- Fast-model User-tab test asserting the /model --fast --global flag.
- Theme scoped-read test: the control shows the selected scope's value,
  not the effective merge.
- Fix the useDaemonProviders mock: `current` is a provider-current object,
  not a bare id.

* fix(web-shell): fix voice-picker scope race and provider memoization

- Voice model scope race: the voice picker opens asynchronously (after
  loadProviders), so recording the persist scope synchronously up front
  let it be clobbered — if the user opened and closed another picker while
  loadProviders was in flight, the reset effect reset the scope and the
  voice model persisted to the wrong scope. Now the scope is captured from
  the click and applied together with the open, guarded by a
  modelDialogMode ref so it only opens (and sets scope) when no other
  surface opened meanwhile. (Vision/fast are synchronous and unaffected.)
- Depend on the stable `reload` fn (extracted as reloadProviders) instead
  of the fresh-every-render providersState object, so handleDeleteModel /
  handleCloseAuthDialog aren't recreated each render.
- Add role="group" + aria-label to the model-fallbacks option list so
  screen readers announce it as a labeled multi-select group (+ test).

* fix(web-shell): address review round on model-management follow-up

Frontend:
- Remove the redundant `data-keyboard-scope` from ModelFallbacksDialog's
  inner div — the wrapping DialogShell already provides it, and the extra
  scope became the last match in DialogShell's close cleanup, whose
  role="dialog" lookup then failed and dropped focus when the dialog
  closed while another was stacked.
- Voice picker open-guard now also checks the fallbacks/auth dialog flags
  (via refs), matching "no other surface opened meanwhile" so it can't
  open on top of a dialog opened while providers were loading.
- Provider group key includes the index (two providers can share an
  authType) to avoid duplicate-key reconciliation.
- handleCloseAuthDialog logs a failed provider reload instead of
  swallowing it, like the sibling handlers.
- Model-fallbacks options at the max show a `title` explaining the limit
  (new localized string).

Backend:
- Split the DELETE /workspace/models baseUrl validation so a too-long
  value reports a length error, not "must be a string".

Tests:
- Workspace-scoped language change (`/language ui --project`).
- Security-sensitive key (tools.approvalMode) rejected at user scope.
- baseUrl length-limit rejection.
- Model-fallbacks Cancel → onClose; delete-confirm Cancel path restores
  the Delete button; fallbacks accessible grouping already covered.

* fix(web-shell): a11y + robustness follow-ups on model management

- Model-fallbacks max-limit options use aria-disabled instead of the
  native disabled attribute so they stay hoverable and can surface the
  "limit reached" title (disabled buttons fire no events); the toggle
  handler already no-ops at the max. CSS updated to match.
- Inline delete confirmation dismisses on Escape (the conventional
  gesture) so keyboard users need not Tab to Cancel.
- scopeToWire throws on an unexpected SettingScope instead of silently
  reporting it as 'user'.
- Re-export DaemonWorkspaceProviderCurrent from the webui daemon facade
  alongside the sibling provider types.
2026-07-13 14:44:54 +00:00
ermin.zem
5c82857fea
Add harness infrastructure for web-shell package (#6517)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
* test(web-shell): add browser and lint harness

* test(web-shell): harden browser smoke harness

* fix(web-shell): guard mock daemon model state

* test(web-shell): remove unused scenario harness

* fix(web-shell): remove stale lint disables

* test(web-shell): make matchMedia stub writable

* fix(web-shell): exclude tests from package typecheck

* test(web-shell): tighten mock daemon route contract

* Update packages/web-shell/client/e2e/utils/mockDaemon.ts

Co-authored-by: qwen-code-ci-bot <qwen-code-ci@service.alibaba.com>

* test(web-shell): clear stale SSE connections

* ci(web-shell): gate smoke on full CI profile

---------

Co-authored-by: ermin.zem <ermin.zem@alibaba-inc.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci@service.alibaba.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: 易良 <1204183885@qq.com>
2026-07-09 08:11:58 +00:00