Commit graph

28 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
ytahdn
6a432ad2eb
fix(web-shell): isolate history and session drafts (#7810)
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
* fix(web-shell): isolate history and session drafts

* fix(web-shell): reset history-browse flag on early commit return (#7810)

* fix(web-shell): address review feedback on paste and draft handling (#7810)

* fix(web-shell): address review feedback on paste pruning, draft flush, and mobile draft notify (#7810)

* fix(web-shell): update smoke test for large paste placeholder behavior (#7810)

---------

Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen Code <qwen-code@users.noreply.github.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
2026-07-28 01:45:52 +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
ytahdn
2a248cc5a9
fix(web-shell): preserve pasted text in composer (#7824)
Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
2026-07-27 12:26:00 +00:00
ytahdn
1f9a1a90a3
fix(web-shell): stabilize mobile voice input (#7806)
Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
2026-07-27 06:28:00 +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
3493dab8a0
test(web-shell): capture the git-mode new-branch sub-state in the visuals suite (#7672)
* test(web-shell): capture the git-mode new-branch sub-state in the visuals suite

The `git mode selector` visuals scenario captured the composer chip and the
opened three-mode popover, but stopped there: selecting an option used to
dismiss the popover ~100ms later (the click bubbled through the React tree out
of the portaled content to the composer surface's onClick → core.focus() →
Radix focus-outside close), so the branch-name sub-state couldn't be shot
stably. #7668 fixed that dismissal, so it now can.

Extend the scenario to click "New branch", fill a valid branch name, and
capture the revealed input (validated) with its Create-branch affordance, in
both themes. All six git-mode captures are byte-stable across runs (0% pixel
diff). Match the option by role — its label is split across a name and a
description span, so getByText('New branch') is ambiguous (also fixed in #7668).

Beyond covering a state the preview never showed, this doubles as a visual
regression guard for #7668: if the popover ever dismisses on option-click again,
the input goes missing and the assertion fails here, not only in the screenshot.

* test(web-shell): strengthen git-mode branch assertion and trim comment (#7672)

* test(web-shell): harden git-mode branch capture into a real #7668 guard

Scope the New branch option to the popover locator and settle past the ~100ms dismissal window before re-asserting the popover and input stay visible, so a regression of #7668 hard-fails here instead of only producing a wrong (visually reviewed) screenshot. Mirrors the proven guard in web-shell.git-mode.spec.ts.

---------

Co-authored-by: wenshao <wenshao@example.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
2026-07-25 00:57:56 +00:00
Shaojin Wen
cc742ca31e
fix(web-shell): keep git mode popover open when picking branch/worktree (#7668)
Clicking the "New branch" or "Worktree" option dismissed the popover
instead of revealing the branch-name input / confirm button. The option
click bubbles as a React synthetic event through the portal up to the
composer surface onClick, which calls core.focus() and moves focus
outside the popover — tripping the Radix focus-outside dismissal (the
popover only guarded the pointer path via onInteractOutside).

Stop click propagation on the popover content, matching the composer
ToolbarPopover pattern in ChatEditor. Also fix the e2e selectors
(getByText('New branch') matched both the name and description spans)
and add a delayed still-open assertion so the flash-then-dismiss
regression cannot false-pass.
2026-07-24 12:36:34 +00:00
Shaojin Wen
203e61b59b
feat(web-shell): add git mode selector for new session creation (#7471)
* feat(web-shell): add git mode selector for new session creation

Support three git workflows when creating a new session:
1. Current branch (default, unchanged behavior)
2. New branch — daemon runs git checkout -b before spawning
3. Worktree isolation (existing, now unified into the same UI)

The mode selector lives in the composer's git chip as a popover,
replacing the previous worktree-only toggle in the welcome header.

API: POST /session accepts branch: { name } (mutually exclusive
with worktree). Server validates branch name, checks dirty tree,
creates branch, and rolls back on spawn failure.

Design doc: docs/design/2026-07-22-webshell-session-git-mode.md

* fix(web-shell): prevent Radix popover dismissal in git mode selector

The portal container used by Web Shell's Popover primitive is not
recognized by Radix's DismissableLayer, causing the popover to close
on any interaction. Add onInteractOutside prevention so the popover
only closes via explicit selection.

Also replace prototype screenshots with real Playwright captures and
add e2e test + screenshot capture script.

* refactor(web-shell): remove redundant worktree toggle from welcome header

The composer git chip popover now fully covers worktree selection,
making the welcome header toggle/badge redundant. Remove the toggle
UI, its state (worktreeToggleEligible, refs, handlers, focus effect),
and all associated tests (unit + e2e + visuals).

* chore: re-capture PR screenshots after worktree toggle removal

* fix(web-shell): audit fixes for git mode selector

- Add missing setSessionBranch(undefined) in loadSidebarSession,
  createNewSession, and session switch effect (!sid path)
- Add setSessionBranch(summary.branch) in session status restore
- Add branch rollback in client disconnect (!res.writable) path
- Fix onInteractOutside: use containment check instead of
  unconditional prevention so genuine outside clicks close popover
- Hoist promisify(execFile) to module level
- Narrow reserved branch name check to only HEAD (FETCH_HEAD etc.
  are valid branch names)

* fix(web-shell): address review feedback for git mode selector (#7471)

* test(web-shell): capture the git-mode selector in the visuals suite

This PR adds the new-session git-mode selector (current branch / new branch /
worktree) but no visuals scenario renders it, so the before/after preview showed
no image for an entirely new UI — the empty result was a coverage gap, not a
clean bill of health. The PR also removed the `worktree empty state` scenario
(its `worktree-welcome-toggle` no longer exists, replaced by this popover),
leaving the suite with no view of the new-session empty state at all.

Add a `git mode selector` scenario that seeds a trusted git-repo workspace and
lands on the empty state (the only place App.tsx wires the intent props), then
captures the composer chip and the opened three-mode popover in both themes.
Both are byte-stable across runs (0% pixel diff), and asserting an option is
visible makes a regression that fails to open the popover fail here rather than
only in the screenshot.

The branch-name sub-state is deliberately not captured: its input autoFocuses
and the popover then dismisses on the idle frame the capture waits for, so it
can't be shot stably through this pipeline — the functional
web-shell.git-mode.spec.ts already drives that path. Restores the empty-state
coverage this PR dropped and gives the new selector a head-only (NEW) preview.

* fix: address review feedback for git mode selector (#7471)

- Forward the branch override in createDetachedSession so the cold-start
  (no active session) path no longer silently drops a user-selected new
  branch.
- Reserve the workspace before 'git checkout -b' to close the TOCTOU in
  the activeBranchSessions guard; two concurrent branch creations could
  both pass the guard and race on HEAD. The reservation is released on
  every exit path.
- Return (and close the browser) when the branch input never appears in
  the screenshot script instead of falling through to a guaranteed throw.
- Add an e2e test asserting the default current-branch submit sends
  neither branch nor worktree.

* fix(web-shell): address review feedback for git mode selector (#7471)

* fix(web-shell): address review feedback for git mode selector (#7471)

* fix(web-shell): address review feedback for git mode selector (#7471)

* fix(cli): sync ink patch with semantic selection types (#7471)

* fix(web-shell): remove orphaned worktree CSS and dead i18n keys (#7471)

* fix: address review feedback for git mode selector (#7471)

* fix: address review feedback for git mode selector (#7471)

Release the route-local in-flight branch reservation in the
disconnect-after-spawn cleanup path so a throwing killSession/
removeSession no longer permanently blocks the workspace from new
branch sessions. Also reject branch names ending in .git on both
the server and the composer validator, associate the branch-name
label with its input, abort the screenshot capture script cleanly
when the chip or popover is missing, and add focused coverage for
the git-mode gating, branch forwarding, and branch pass-through.

* test(cli): cover branch session route validation and mutual exclusion (#7471)

* fix(cli,web-shell): accept Unicode branch names in validation (#7471)

The branch name validation regex rejected all non-ASCII characters,
preventing users from creating branches with Unicode names that git
accepts (e.g. 功能/fix-login). Replace the ASCII-only character class
with Unicode property escapes (\p{L}\p{N}) and the u flag, applied
consistently to both the server-side route and the client-side
GitModePopover validation.

* fix(web-shell): address review feedback on git mode selector (#7471)

- Revert unrelated ink patch change (transformers: [] → newTransformers)
- Extract duplicated branch rollback logic into rollbackBranchCreation helper
- Pessimistically track activeBranchSessions when killSession throws in
  disconnect-reap path, preventing concurrent branch session on surviving
  session
- Use ref pattern for gitModeIntent in ensureSessionForPrompt to avoid
  callback cascade on every git-mode toggle
- Add aria-label to git mode clear button for screen reader accessibility

* fix(cli): harden git branch session creation and clarify UX (#7471)

Address review feedback on the git mode selector:

- Bound every branch git operation with a 30s timeout (mirroring
  GitWorktreeService) so a stuck repository lock or slow hook can no
  longer hang the request and leave the workspace permanently reserved
  in inFlightBranchWorkspaces.
- Run branch shape/name validation before the active-session conflict
  check so a malformed body gets 400 instead of 409.
- Compare the reserved HEAD name case-insensitively (ref storage is
  case-folding on macOS/Windows), in both the route and the popover.
- Surface the design-doc "switches the working directory to a new
  branch" hint in the popover so users know HEAD will move.
- Correct the design doc: branch metadata is in-memory only and does
  not survive a daemon restart.

* fix(web-shell,cli): fix light theme, stale intent, and branch init guard (#7471)

- Replace undefined --web-shell-* CSS variables with shadcn design tokens
  (--foreground, --muted-foreground, --border, --popover-foreground, etc.)
  and add --git-mode-* accent variables to both .themeDark and .themeLight
  so the git mode popover is readable in light theme.
- Add useEffect to clear gitModeIntent when gitModeEligible flips to
  false, preventing stale branch intent from leaking to another workspace.
- Move gitModeIntentRef assignment from render body into useEffect to
  avoid ref mutation during render (concurrent React safety).
- Wrap GitWorktreeService constructor in try/catch on the branch path,
  matching the worktree path's guard, so a constructor throw returns 500
  instead of hanging the request.
- Show branchConflictWarning hint only when a valid branch name is
  entered, not as a static default hint.
- Reword GIT_RESERVED_BRANCH comment and add cross-reference comments
  between the duplicated client/server validation predicates.

* fix(cli,web-shell): address review feedback on git-mode PR (#7471)

- Add clearBranchSessionEntry cleanup hook on session close/delete to
  prevent stale activeBranchSessions entries from causing spurious
  409 branch_session_conflict on the next branch creation request.

- Extract git branch mutations (rev-parse, status, checkout -b,
  rollback) into a mockable git-branch-ops module, closing the test
  gap on the git-mutation paths that previously had no CI coverage.

- Add 6 new server tests: branch_already_exists, branch_dirty_tree,
  branch_checkout_failed, happy-path 200 with branch metadata,
  rollback on spawn failure, and branch_session_conflict.

- Export validateBranchName and add shared test vectors matching the
  server-side validation to catch future client/server drift.

* fix(cli): close concurrency guard gap in branch session creation (#7471)

The synchronous reserve point only re-checked inFlightBranchWorkspaces,
not activeBranchSessions. A request that passed the early guard before a
concurrent request registered could slip through after the first request
completed and cleared inFlightBranchWorkspaces. Re-check both structures
at the reserve point (no await between check and add) to close the window.

Also clarifies the dirty-tree gate comment to explain the real intent
(surprise-prevention, not data protection).

* test(cli,web-shell): cover worktree intent forwarding and branch session delete lifecycle (#7471)

* fix(cli): address review feedback on git-mode branch sessions (#7471)

* test(cli): cover git-branch-ops git command semantics (#7471)

* fix(cli,web-shell): roll back failed checkouts and guard shared-checkout branch creation (#7471)

* fix(cli,web-shell): address review feedback on git-mode branch sessions (#7471)

---------

Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.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-24 03:48:43 +00:00
ComplexSimply
16f024338a
fix(web-shell): render a plain textarea composer on touch devices (#7587)
* 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>
2026-07-23 13:14:25 +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
Edenman
c33ca7227a
fix(web-shell): restore scheduled task reference interactions (#7313)
* fix(web-shell): restore scheduled task reference interactions

* chore(web-shell): remove PR screenshot artifact

* fix(web-shell): refine scheduled task tag removal

---------

Co-authored-by: 克竟 <dingbingzhi.dbz@alibaba-inc.com>
2026-07-21 07:10:26 +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
adf2caea39
feat(web-shell): persist the split view across refresh, per tab (#7136)
The split view (2+ sessions side by side) was lost on every refresh: its
pane set lived only in React state, and the one-shot ?split= deep link is
consumed on load. Persist the live session set to sessionStorage while the
split is the active view, and restore it on load when no ?split= deep link
is present, so a refresh brings the split back.

sessionStorage (not localStorage) is deliberate: it is scoped per browser
tab, so a split opened in its own tab and the in-window split never clobber
each other, and a fresh unrelated tab restores nothing — while still
surviving a refresh of the same tab. The ?split= URL stays the shareable,
cross-tab channel.

Only an explicit close (the split's back button) clears the persisted set;
detours to a single session keep it, so the split is treated as the user's
lasting context until they close it. Controlled hosts (which own their split
lifecycle) never auto-persist or auto-restore.

Co-authored-by: wenshao <wenshao@example.com>
2026-07-18 08:29:23 +00:00
易良
16a10fbd12
test(web-shell): align workspace sidebar visual smoke (#7107) 2026-07-17 11:23:52 +00:00
Shaojin Wen
ed2b0dfdee
test(web-shell): make visual-preview captures deterministic + add workspace-sidebar scenario (#7041)
* test(web-shell): replace flaky split-view-restored shot with a workspace-sidebar scenario

The split-view "restored" screenshot was byte-nondeterministic between
identical renders (the reappearing pane re-renders its content just after
the restore click), so it periodically diffed above the before/after
threshold and surfaced a false-positive "changed view" unrelated to the
PR under review. It is also visually identical to the tiled `split view`
shot. Drop the capture but keep the restore click + "both panes back"
assertion, so the restore path still has behavioral coverage.

Add a `workspace sidebar` scenario with two workspaces so the sidebar
groups sessions per workspace and tags the primary one. This is the only
scenario that renders the primary-workspace label/badge (it is gated on
more than one displayed workspace), so changes to those labels — which no
single-workspace scenario can surface — now show up in the visual preview.

* test(web-shell): freeze looping animations so captures are deterministic

The sidebar's activity spinner is a GPU-composited transform loop that
Playwright's `animations: 'disabled'` captures mid-rotation at a random
angle, so `sidebar attention` differed in ~0.12% of pixels between two
identical renders — above the 0.02% before/after threshold, i.e. a
false-positive "changed view" on any PR that renders it.

Before each capture, pause every infinite Web Animation and rewind it to
time 0 (a two-frame wait lets the compositor commit the frozen frame);
finite animations are still left to `animations: 'disabled'`. Verified
with a pixel diff: the whole suite now renders pixel-identical across two
runs (worst 0.0001% of pixels, vs the 0.02% threshold).

* test(web-shell): document freeze scope, pin scenario deps, test the freeze

Address review on the visual-capture determinism changes:

- Note freezeLoopingAnimations' coverage scope in its docstring (WAAPI +
  CSS @keyframes via document.getAnimations(), not a hand-rolled
  requestAnimationFrame loop), so a future spinner rewrite that
  reintroduces the flake leads a debugger back to this function.
- Pin the workspace-sidebar scenario's primary workspace cwd and loaded
  session name explicitly rather than leaning on createWebShellDaemonScenario
  defaults, so renaming those defaults in mockDaemon.ts can't turn the
  settle-wait into a cryptic "not visible" failure.
- Add harness.spec.ts pinning the freeze contract: an infinite animation
  is paused and rewound to time 0, while a finite one is left running.

---------

Co-authored-by: wenshao <wenshao@example.com>
2026-07-17 00:46:01 +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
Shaojin Wen
2deefbf9c7
test(web-shell): add mermaid, split-view + sidebar visual scenarios (#6964)
* test(web-shell): add a mermaid diagram visual scenario

Add a `mermaid diagram` scenario to the visuals suite so the preview
covers the Mermaid rendering surface — an assistant message with a
mermaid fenced flowchart. It renders the real MermaidBlock (async
mermaid import, injected <svg>) in light and dark, waiting on the
rendered SVG so the capture is never the "rendering…" placeholder.

This is the surface the diagram zoom/pan work (#6881) enriches, so once
the before/after preview lands it gives that PR a real before/after
target instead of an unrelated canned screenshot.

* test(web-shell): add a split-view (+ maximize) visual scenario

Add a `split view` scenario: enter the two-pane split via the `?split=a,b`
deep link, then maximize one pane (#6951). Captures the tiled state (both
panes, with the maximize controls) and the maximized state (one pane
filling, restore control) in light and dark, driving the real SplitView
against the mock daemon serving two sessions.

* test(web-shell): add a sidebar attention-badge visual scenario

Add a `sidebar attention` scenario: four sessions in distinct states —
waiting-on-permission, waiting-on-user-question, running, idle — so the
sidebar renders #6956's "Waiting for approval" / "User input needed"
attention pills. Renders in light and dark; asserts on session names
(present with or without the pills) so the frame is the same shape on
main and the PR, letting the before/after preview surface the pills.

* test(web-shell): derive the split view's second session from the scenario list

Addresses a review suggestion: the split view test hardcoded the
'previous-session' id, which only worked because it is in
createWebShellDaemonScenario's default sessions list. Derive the second
pane's session from the scenario's own list instead (and throw a clear
error if absent), so a future rename/removal of that default surfaces as
a self-explaining failure rather than a confusing SSE connection timeout.

* test(web-shell): tidy split copy and mermaid width in visual scenarios

Address review nits on the visual scenarios:
- Split scenario: the mock replays the same events into both panes, so
  "Here is the first pane of the split." read wrong in the second pane.
  Use pane-neutral copy ("Here are the two sessions, side by side.").
- Mermaid scenario: the flowchart's rightmost node clipped at the code-block
  edge at the 1280px capture viewport. Shorten the node labels (same nodes and
  flow) so the whole diagram fits with margin.

Re-ran both scenarios (light + dark) locally: 4/4 pass, and confirmed in the
captures that the diagram no longer clips and the neutral copy reads correctly
in both panes.

* test(web-shell): capture split-view restore and assert all sidebar sessions

Address review nits on the visual scenarios:
- Split view: after maximize, click "Restore pane" and capture the restored
  tiled layout, asserting the maximize control returns on both panes — so a
  regression in the restore path is caught, not just the tiled and maximized
  states.
- Sidebar attention: assert all four session names render (not just the two
  waiting ones). The running session is also the loaded one, so its name also
  shows in the main view — scope the running/idle checks to the sidebar
  landmark so the match stays unambiguous.

---------

Co-authored-by: wenshao <wenshao@example.com>
2026-07-16 00:58:28 +00:00
Shaojin Wen
02c79beb62
feat(web-shell): auto-post visual previews (screenshots + flow GIFs) on PRs (#6880)
* feat(web-shell): auto-post visual previews (screenshots + flow GIFs) on PRs

PRs that touch the web-shell UI now get an auto-updated comment with
light/dark screenshots of key views (transcript, slash menu, model/theme
dialogs, permission panel) and short GIF recordings of common flows,
rendered against the existing mock daemon — no real backend, no secrets.

Split into two workflows for security, since capture runs untrusted PR code:

- web-shell-visuals.yml (pull_request): checks out the PR head, builds and
  renders it with Playwright, captures PNGs + webm, converts webm->GIF with
  ffmpeg, and uploads an artifact. `contents: read` only, references no
  secrets — fork PRs run with a read-only token and no secrets.
- web-shell-visuals-publish.yml (workflow_run): downloads the artifact,
  binds it to its real PR by requiring the PR head SHA to equal the run's
  authenticated head SHA, hosts the images on a per-PR `pr-assets/*` branch
  (referenced by immutable commit SHA), and posts/updates one inline
  comment. Never checks out or runs PR code; the write token lives only here.

Capture infra is self-contained in packages/web-shell
(playwright.visuals.config.ts + client/e2e/visuals/*), reusing the mock
daemon harness. Run locally with:
`npm run test:e2e:visuals --workspace=packages/web-shell`.

* fix(web-shell): guard empty gh api response in visuals publish

Addresses review feedback on #6880: if `gh api` returns empty (network
error / rate limit), jq on empty stdin errors and `set -e` kills the
publish job. Skip gracefully instead.

* fix(web-shell): address review nits on visuals capture

- harness recordFlow: wrap video saveAs/delete in try/catch so a video
  I/O error (e.g. drive failed before navigation) can't mask the real
  driveError.
- capture workflow: drop the unused head_sha.txt artifact field; the
  publish job binds to the authenticated workflow_run.head_sha, and an
  artifact-sourced SHA would be untrusted.

* fix(web-shell): address second review round on visuals capture

- context.close() in recordFlow's finally is now best-effort (try/catch)
  so a close/crash error can't mask the real driveError.
- add a flows spec that asserts a throwing drive propagates its own error.
- trigger the capture workflow on playwright.visuals.config.ts changes too.

* fix(web-shell): address third review round on visuals capture

- harness: log (don't silently swallow) a video save/null when drive
  succeeded; keep masking-suppression only when driveError is set.
- publish: HTML-escape interpolated values in the comment builder (defense
  in depth, independent of the upstream filename sanitization); fix the
  stale 'single pr-assets branch' comment and key concurrency on source
  repo+branch so different PRs (incl. same-named fork branches) parallelize.
- capture: bump checkout to v6.0.3 (repo standard); surface ffmpeg's stderr
  on GIF-conversion failure instead of discarding it.

* fix(web-shell): harden visuals publish/capture (review round 4)

Publish (privileged workflow_run):
- CRITICAL: capture basename before `tr` so its trailing newline isn't
  turned into `_` (which broke the .png/.gif filter -> empty preview).
- dedup only against the bot's OWN comment (author + marker), not any
  marker-bearing comment a participant can post.
- bound the pr-assets branch: force-push a single orphan snapshot per run
  (previous snapshot GC'd) instead of appending unbounded untrusted content;
  this also removes the rebase/retry path.
- cap EXAMINED candidates (not just accepted) before validation; tighten
  per-file (3MiB) and accepted-image (14) caps.
- re-validate PR open + head-SHA immediately before the comment write
  (TOCTOU); retry the comment listing and abort rather than POST a duplicate
  when listing fails.
- esc() the runUrl for consistency with the self-defending HTML.

Capture (pull_request):
- upload raw recordings as a SEPARATE artifact the publisher never downloads,
  so an untrusted multi-GB video can't exhaust the privileged job.
- also trigger on packages/webui/src and packages/sdk-typescript/src (the
  visuals dev server aliases them).
- create screenshots/gifs dirs before the metadata counts (defensive).

Harness recordFlow:
- track drive failure with an explicit boolean (handles `throw undefined`);
  discard the recording on failure so a failed flow leaves no bogus webm.

* refactor(web-shell): extract + unit-test the visuals publish staging/comment

Addresses the review's testability gap (the class of bug that let the
filename sanitizer break the whole preview slip through green CI). The image
validation (magic bytes, filename sanitization, examined/accepted/size caps)
and the comment builder (light/dark pairing, flow labels, HTML escaping) move
from inline workflow bash/node into .github/scripts/web-shell-visuals-publish
.mjs, covered by web-shell-visuals-publish.test.mjs (run in ci.yml's
node --test line). The publish workflow sparse-checks-out and calls the
script instead. Behaviour is unchanged; it just gained a test surface.

* fix(web-shell): retry the visuals asset force-push; drop stale comment

Round-4 switched hosting to a force-push but left a comment referencing a
'push-retry loop' that no longer existed, and the force-push was a single
call that set -e would abort on a transient failure. Add a bounded retry and
correct the comment.

* fix(web-shell): harden visuals publish/capture (review round 6)

Script (unit-tested):
- flow labels: own-property lookup so `toString.gif`/`constructor.gif` can't
  leak Object.prototype members into the comment.
- per-kind image caps (screenshots vs gifs) so a large screenshot set can't
  silently starve the flow GIFs from the preview.
- tests for both, plus the per-kind cap.

Publish:
- bind the artifact PR number to the run's authenticated head repo+branch
  (not just head SHA), rejecting a sibling PR that shares the same commit.
- re-validate before the force-push and again right before the comment write
  (close the download/stage/lookup TOCTOU windows).

Capture:
- bound artifact contents before upload (drop oversized / excess files) so an
  untrusted spec can't bloat the published or video artifact.
- trigger on the capture workflow file itself.

- new close-trigger cleanup workflow deletes a PR's asset branch on close, so
  pr-assets/* refs don't accumulate without bound.
- single-source the capture viewport (constants.ts) shared by config + harness.
- model-switch flow asserts the daemon model request actually fired.

* fix(web-shell): stricter visuals error handling (review round 7)

Harness recordFlow:
- when the drive SUCCEEDS, a failed context.close() or video.saveAs() (or a
  missing recording) now FAILS the flow instead of a swallowed console.warn —
  a silent pass with no .webm makes the downstream GIF step fail confusingly.
  A drive FAILURE still discards the partial video and rethrows the original
  error (unchanged).

Publish:
- validate_pr distinguishes a transient API failure (empty after retries ->
  exit 1, re-triggerable) from a genuine invalid state (closed / head mismatch
  -> skip), via a `gate` wrapper used at all three checkpoints.
- add a 2s backoff between comment-listing retries (matching the push retry).

---------

Co-authored-by: wenshao <wenshao@example.com>
2026-07-15 06:48:52 +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
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
dreamWB
3e81315add
fix(web-shell): make composer height adaptive (#6872)
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
2026-07-14 11:38:29 +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