Commit graph

12 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
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
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
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
易良
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
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