* feat(web-shell): add split pane header action slot with overflow
Let hosts render per-session actions in each split pane header, collapsing them into a … menu when the pane is too narrow.
Co-authored-by: Cursor <cursoragent@cursor.com>
* docs(web-shell): add pane header actions PR screenshots
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web-shell): tighten pane header overflow measurement
Drop the per-render children effect dependency that rebuilt ResizeObserver during streaming, and reserve workspace-tag width when computing available header space.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web-shell): address pane header overflow review blockers
Mount host actions in only one tree, and wrap overflow entries as DropdownMenuItems so Radix selection and keyboard navigation work.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web-shell): keep pane header actions alive across overflow
Flatten Fragment host actions before building the overflow menu, and keep the same host instances mounted when collapsing so stateful actions are not reset.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web-shell): address pane header overflow review suggestions (#7808)
* fix(web-shell): proxy overflow clicks via action slots
Wrap host pane actions in stable slots so the overflow menu can activate interactive descendants without requiring opaque custom components to forward internal data attributes.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web-shell): address overflow menu review suggestions (#7808)
* fix(web-shell): harden pane header overflow actions (#7808)
Restore the 8px gap between the built-in maximize/close controls, ignore
aria-hidden glyphs when labelling overflow items, omit non-interactive
children from the overflow menu, and document the popover constraint on
renderHeaderActions. Refreshes the design doc to match the mount-once
implementation.
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.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 Autofix <qwen-code-autofix@users.noreply.github.com>
* fix(web-shell): preserve session URL context
* fix(web-shell): keep daemon token out of the session URL (#7926)
Restore the daemon token stripping that was dropped alongside the
base-path fix: removeDaemonTokenFromUrl() on startup and the ?token=
delete in replaceStandaloneSessionUrl. The ?token= query path is still
supported for backward compatibility, so without stripping it leaks into
the address bar, history, access logs, and Referer headers.
Also extract the session pathname building into buildSessionPathname()
with unit tests covering root/sub-path deployments, the no-session case,
trailing slashes, and id encoding.
* fix(web-shell): anchor session URL parser to agree with writer (#7926)
Extract parseSessionId() next to buildSessionPathname() and anchor it to the last /session/<id> segment so the parser agrees with the greedy writer. Previously a base path ending in a session segment produced /app/session/session/<id>, which the first-match parser read back as the literal id "session". Add round-trip and trailing-slash coverage.
* test(web-shell): cover parseSessionId malformed-encoding catch branch (#7926)
* fix(web-shell): preserve base path in split-view URL (#7926)
---------
Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
* 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>
* fix(web-shell): report intended workspace to host when starting a new chat
Clearing a session leaves connection.workspaceCwd pointing at the previous session's workspace. The onSessionIdChange notification read that stale value, so starting a new chat in workspace A routed the host back to the old workspace (e.g. one with a running task) and the composer showed the wrong workspace. With no active session, report the workspace picked for the next session instead.
* fix(web-shell): reuse activeWorkspaceCwd for the no-session host report (#7910)
---------
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
* feat(web-shell): add native workspace folder picker
* fix(serve): harden native directory picker and add coverage (#7849)
Add a 5-minute timeout to each native picker subprocess and to the
webui action so a dismissed dialog cannot leave an orphaned GUI process,
and distinguish a headless Linux "cannot open display" failure from a
deliberate zenity cancellation. Cover pickNativeDirectory's platform
branches, the route's 501/500 error paths, and the dialog's picker
failure path with focused tests.
* fix(serve): log directory picker failures to daemon stderr (#7849)
* fix(serve): abort directory picker on client disconnect and stagger timeouts (#7849)
* test(webui): add unit tests for pickWorkspaceDirectory action (#7849)
* fix(serve): set UTF-8 console encoding for PowerShell picker and treat timeout kills as cancels (#7849)
* fix(serve): abort directory picker on response close, not request close (#7849)
---------
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 CI <qwen-code-ci@users.noreply.github.com>
selectCodeBlock seeded lang from the first token before the loop that
classifies tokens. For '/copy 3' the one token was used twice: it set
lang to "3" and requestedIndex to 3, so the filter looked for blocks
written in a language called "3", found none, and returned early with
'No matching code block found' before the index was consulted.
The command's own argument hint advertises
'[code|<lang>|latex|inline-latex] [index]' with both groups optional, so
a bare index is a documented form - and it was the only one that failed.
Drop the pre-loop assignment. It was redundant: when the first token is
not 'code' it is already in selectorTokens, and the loop assigns lang for
every non-numeric token.
* feat(core): add full-resolution image zoom tool
* chore(vscode): refresh third-party notices
* fix(ui): satisfy zoom image display drift checks
* fix(web-shell): translate zoom image tool
* fix(core): wire sharp into packaging and load it lazily (#7809)
Externalize sharp in esbuild and declare it (plus the @img platform
binaries) in the published package's optionalDependencies so an
npm-installed CLI resolves the native binding. Import sharp dynamically
inside execute() so a missing binding returns a bounded tool error
instead of crashing startup during strict tool warmup, and so the module
is only loaded when zoom_image actually runs. Also pin the EXIF
auto-orientation test with a discriminating fixture and cover the
.qwenignore and y1>=y2 validation paths.
* fix(core): gate zoom_image at execute time and cap tiny-crop upscale (#7809)
Register zoom_image unconditionally and move the image-modality check to
execute time so first-run sessions and hot /model switches resolve the
tool without re-running initialize(). Cap magnification at 8x so a tiny
crop no longer inflates to the full image-token budget. Drop the redundant
@img/* platform pins; sharp's own optionalDependencies install the matching
binary for each OS/arch.
* fix(core): emit zoom_image file telemetry and cover guard branches (#7809)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
---------
Co-authored-by: qwen-code-autofix <qwen-code-autofix@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(web-shell): render background notifications as system messages
* fix(web-shell): use basic table rendering by default
---------
Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
* fix(web-shell): allow shell commands in new tasks without a session
Previously, typing a ! command in a brand-new task showed "No active
session yet" because requireActiveSessionForLocalCommand() rejected
the command when no session existed. Replace it with
ensureSessionForPrompt() — the same lazy session creation path used
by regular messages — so the session is created on demand before the
shell command executes.
* feat(web-shell): queue shell commands during a turn instead of blocking
When a turn is running, ! commands are now queued client-side and
executed sequentially after the turn finishes, instead of showing
an error toast. A drain effect watches streamingState returning to
idle and runs pending commands via sendShellCommand(). The user
gets an info toast confirming the command was queued.
* fix(web-shell): clear queued shell commands on session switch
* fix(web-shell): pin drain session, clear queue on cancel, deduplicate reload (#7724)
* fix(web-shell): stop active drain on cancel, warn on dropped commands (#7724)
* fix(web-shell): keep draining queued shell commands as streamingState changes (#7724)
* fix(web-shell): defer composer clear for ! commands until session creation succeeds (#7724)
* fix(web-shell): scope drain cancellation to generation and harden new-task ! branch (#7724)
* fix(web-shell): warn on cancel drop and guard queue wipe against language change (#7724)
* fix(web-shell): assert cancel call in tests and document drain ordering (#7724)
* fix(web-shell): guard inline ! command against cancel and session-switch races (#7724)
* fix(web-shell): exempt lazy session creation from drain-generation bump (#7724)
The session-switch effect bumped drainGenerationRef on every sessionId
transition, including the one caused by the submit's own lazy session
creation. This silently cancelled the ! command: the generation guard in
the .then() saw a changed counter and returned before sendShellCommand.
Skip the bump when the transition target matches preparingSessionIdRef
(the session ensureSessionForPrompt is creating), so only genuine
user-initiated switches cancel in-flight commands.
* fix(web-shell): prevent drain preemption when commands are queued mid-drain
Add isDrainingRef guard to prevent a new drain from starting while an
existing drain is still processing. The running drain now re-reads the
queue after each batch to pick up commands added during execution.
The drain lock is tied to the generation counter: cancel and session-switch
release the lock when they bump the generation, and the drain IIFE only
releases it in finally if it's still the active drainer. This allows a new
drain to start after a cancel while preventing the idle-transition preemption
that was dropping commands.
Fixes the Critical findings rc:3652998624 and rc:3653109525.
(#7724)
* fix(web-shell): reset shellSubmitInFlightRef on cancel to unblock new commands (#7724)
* fix(web-shell): drop whole queue on drain bail to preserve FIFO (#7724)
* fix(web-shell): scope drain-bail queue wipe to disconnect only (#7724)
The unconditional queue wipe in the drain bail path destroyed commands
queued after a cancel or session switch, because those generation-bump
sites already wipe the queue themselves. Only the disconnect case lacks
its own wipe, so scope the drain-bail wipe to that case alone.
Adds two regression probes: commands queued after cancel and after a
session switch while a drain command is still in flight must survive
the stale drain's bail and be dispatched by the next drain.
* fix(web-shell): use returned session ID instead of stale connectionRef (#7724)
connectionRef.current.sessionId is only updated at render time, but the
.then() microtask after ensureSessionForPrompt() runs before React
commits the setConnection state update — so the post-creation cleanup
(editor clear, session-change dispatch, sidebar reload) never executed
for lazily-created sessions.
Have ensureSessionForPrompt() return the allocated session ID and use
it directly in the submit handler. Also include the command text in
drain-loop error toasts and assert the queue-path return value in the
queueing test.
* test(web-shell): cover sendShellCommand rejection and bare-bang guard (#7724)
---------
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 Autofix <qwen-code-autofix@users.noreply.github.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
Co-authored-by: Qwen Code <qwen-code@alibabacloud.com>
The sidebar max width was a fixed constant, so long session names could
never be fully revealed by dragging. The cap now scales with the window
(50% of window width, floored at the previous fixed cap), and a resize
listener re-clamps a stored wider sidebar when the window shrinks.
* fix(web-shell): parse 256-color and truecolor SGR sequences in parseAnsi
parseAnsi read every ';'-separated SGR parameter as a standalone code,
but the arguments of 38/48/58 (extended foreground/background/underline
color) are not codes. `38;5;<n>` and `38;2;<r>;<g>;<b>` were fed back
into the code loop, so:
- `38;5;2` set dim (from color index 2) and produced no color;
- any truecolor value with a zero channel hit the `code === 0` branch
and wiped the color, bold and dim already set on the line;
- a background such as `48;5;22` fed 22 to the reset-intensity branch,
silently un-bolding the text.
Consume the 38/48/58 arguments instead of reading them as codes, and
resolve the foreground to hex: 0-15 map onto the existing themed palette,
16-231 onto the 6x6x6 cube, 232-255 onto the grayscale ramp. Background
and underline color are parsed but not rendered (Segment has no such
field) so their arguments still cannot leak into the code stream.
parseAnsi feeds shell tool-output rendering in ToolGroup, so this
affects any 256-color CLI. Adds ansi.test.ts (none existed); the four
new cases fail on the previous parser.
* fix(web-shell): keep the current color when an extended-color sequence is malformed
An unreadable 38/48/58 sequence was assigning its undefined result straight
to the color, so `\x1b[31m\x1b[38;5;300m` dropped the red that code 31 had
already set. Ignore the sequence instead and leave the color as-is; a
well-formed one still replaces it.
Also widen toHex/xterm256 to accept `number | undefined` so the truncated
arguments they are actually handed match their declared types, and drop the
non-null assertions that were hiding that mismatch from the compiler.
Add the regression case the existing malformed-sequence test could not catch:
it started from no color, so a cleared color was invisible to it.
* test(web-shell): pin the 58 branch and the truecolor channel guard
The 58;5;2 case asserted only bold, so dropping 58 from the extended-color
trio left the test green: the leaked 2 argument turns on dim, not bold.
Assert the whole segment instead.
No case used an out-of-range truecolor channel either, so toHex's 0-255
guard was unpinned. Add 38;2;999;0;0 to both malformed-sequence loops.
Both gaps found by wenshao's mutation run in review.
Organization actions (pin/group) only mutate display metadata and never
execute code or touch the filesystem, so they are safe for any trusted
workspace — not just locked ones. Decouple them from the stricter
canUseWorkspaceQualifiedActions gate into a new canUseOrganizationActions
check that allows primary, locked, and restricted scopes (rejecting only
unknown/untrusted). Destructive actions (rename, delete, export) remain
gated behind the original locked-or-primary requirement.
* fix(web-shell): enable Changes and History dialogs for worktree sessions
* fix(web-shell): add containment and gitCwd forwarding tests, fix SDK nits (#7695)
* fix(cli): use realpathSync in resolveContainedCwd test assertions (#7695)
On macOS, os.tmpdir() returns /var/folders/... (a symlink to
/private/var/folders/...), but resolveContainedCwd resolves paths
via fs.realpathSync. The test assertions compared against the raw
os.tmpdir()-based paths, causing two tests to fail on macOS. Wrap
the expected values in fs.realpathSync to match the function's
actual return value on all platforms.
* test(web-shell): cover gitCwd forwarding through log pagination (#7695)
* test(sdk): cover cwd query encoding in workspace git client methods (#7695)
* fix(sdk): bump browser bundle size limit for worktree cwd params
---------
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* perf(web-shell): paint the composer git chip before git status completes
New sessions gated the chip on a full `git status --porcelain` subprocess
behind GET /workspaces/:ws/git, so the branch chip appeared hundreds of
milliseconds (worst case seconds) after the composer was ready.
The daemon now keeps a per-workspace last-known summary with in-flight
dedup and a 2s background-refresh throttle: the default GET returns the
cached status (branch-only on a cold start) immediately and recomputes in
the background, publishing git_status_changed over SSE only on a delta,
while ?wait=1 keeps the previous blocking semantics. The composer fetches
both paths concurrently — the fresh GET also covers the no-session state,
which has no per-session SSE stream — so the branch paints in ~3ms and
the counters land when the computation finishes. The sidebar keeps
wait:true since it has no SSE fill-in path.
* fix(web-shell): add exhaustiveness guard and worktree git-status test (#7680)
* fix(web-shell): add exhaustiveness guard and worktree git-status test (#7680)
* fix(web-shell): add exhaustiveness guard and worktree git-status test (#7680)
* fix(cli): use writeStderrLineSafe in git-status refresh error path (#7680)
* fix(web-shell): add debug trail to fresh-path catch and test branch-watcher dispose guard (#7680)
* fix(cli): assert writeStderrLineSafe in git-status refresh failure test (#7680)
---------
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
* 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>
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.
* feat(core): add configurable image generation models
* fix(core): keep undici out of ACP bundle
* fix(core): address review feedback for image generation (#7607)
- Use loadUndici() instead of direct import('undici') in downloadPng
to handle esbuild CJS bundling where named exports are unavailable
- Check response.ok before parsing JSON body so non-JSON error pages
(e.g. 502 HTML) produce structured HTTP status errors
- Use matched.baseUrl instead of matched.registryBaseUrl in the image
model handler for consistency with the vision model handler
- Add zh-CN and zh-TW translations for ImageGen tool display name,
model command description with --image, and all new image model
UI strings (fixes i18n test failures)
- Add tests: redirect-following path, max redirect limit, non-JSON
error body, permission-disabled registration, imageOnly vision guard
* fix(cli): add missing English i18n keys for image model feature (#7607)
* fix(core): prevent signed-URL leak via error cause chain in image-gen (#7607)
* fix(core): add web-shell image_gen display name and fix safe-mode re-read (#7607)
---------
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 Autofix <qwen-code-autofix@users.noreply.github.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
* 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>
* 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>
* feat(core): restore background agent roster
* fix(web-shell): add list_agents to TOOL_DISPLAY_NAMES
The new list_agents core wire tool was added to core's ToolNames but not
to the web-shell TOOL_DISPLAY_NAMES map, causing toolFormatting.drift.test.ts
to fail (expected ['list_agents'] to deeply equal []). Add the missing
'ListAgents' display-name entry so the browser panel shows a friendly name
instead of the raw wire name and the drift guard passes.
* fix(cli): reload old-session background agents on failed resume rollback
When /resume fails after core has swapped but before the UI swap, the catch
block rolls core back to the old session via startNewSession(oldSessionId).
However the forward path already called resetBackgroundStateForSessionSwitch,
which cleared the old session's in-memory background agents. The rollback did
not reload them, so list_agents returned empty for the old session (whose
sidecars are still on disk) until the next process start or successful resume.
Reload the old session's paused background agents after rolling core back, so
the restored roster matches on-disk state. Placed after startNewSession so the
loadPausedBackgroundAgents current-session guard is satisfied; best-effort via
.catch so it never blocks the rollback path.
* fix(web-shell): add zh translation for list_agents tool name
The toolFormatting test 'has a zh translation for every tool in the
display-name map' failed with expected ['list_agents'] to deeply equal []
because list_agents was added to TOOL_DISPLAY_NAMES without a matching
toolName.list_agents zh-CN entry. Add the translation to restore parity.
* fix(cli): resolve CI failures for background-agent roster restore
- Add toolDisplayName.ListAgents translations (en, zh, zh-TW, ca) so the
new list_agents tool has a zh entry; fixes i18n/index.test.ts.
- Add loadPausedBackgroundAgents and consumePendingRecoveredAgentsNotice
to the acpAgent worktree test config mock, which loadSession now calls
via #restoreBackgroundAgentsOnResume; fixes acpAgent.worktree.test.ts.
* refactor(core): extract incompatible-isolation blocked reason to a const
Move the incompatible-isolation blocked-reason string out of an inline
literal into a module-level INCOMPATIBLE_ISOLATION_BLOCKED_REASON const,
matching its four sibling reasons so the text is discoverable by
constant-name grep and edited alongside the others.
* fix(core): preserve retained activity state on failed agent revive
Address review feedback on the background-agent roster restore:
- On a failed completed-agent revive, restore UI state with a non-empty
guard instead of `??`. Because `restorePausedEntry` resets the paused
entry's `recentActivities` to `[]`, the previous `failedEntry?.field ??
completedEntry.field` kept that empty array and dropped the pre-revive
snapshot (the UI Progress section rendered empty). Applied consistently
to pendingMessages, recentActivities, and pendingApprovals.
Add regression coverage for previously untested paths:
- failed revive preserves pre-revive recentActivities
- terminal-agent cap admits only the newest MAX_RETAINED_TERMINAL_AGENTS
completed sidecars on restore
- /resume rollback reloads the old session's background agents
- headless resume prepends the recovered-agents notice to the prompt
* test(cli): cover interrupted-turn continuation not consuming recovered-agents notice
Add ACP and headless regression tests asserting an interrupted-turn
continuation does not consume the one-shot recovered-agents notice
(the !isContinue / !continueInterrupted guards), so it is delivered on
the user's next ordinary prompt. Mirrors the existing slash-command
coverage.
---------
Co-authored-by: Claude <noreply@anthropic.com>