Commit graph

54 commits

Author SHA1 Message Date
dreamWB
a182bdf618
fix(web-shell): constrain session details and add copy action (#8127)
Some checks are pending
E2E Tests / E2E Test - macOS - shard 1/2 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 2/2 (push) Waiting to run
E2E Tests / channel-plugin E2E (nightly) (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 3/3 (push) Waiting to run
* fix(web-shell): constrain session details and add copy action

* fix(web-shell): harden constrained session details

* fix(web-shell): close session details review gaps

* fix(web-shell): address session details follow-up
2026-07-31 02:25:44 +00:00
ytahdn
0a3098a279
feat(web-shell): add contextual task panels (#7929)
* feat(web-shell): add contextual task panels

* fix(web-shell): harden contextual task panels

* fix(web-shell): preserve side task titles

* fix(web-shell): address review feedback on context panels PR (#7929)

- Add POST /session/:id/side-task to telemetry route catalog (51 routes)
- Increase SDK browser bundle size limit to 184KB
- Fix duplicated data-testid="chat-pane" → "chat-pane-container" on container
- Gate sourceType behind session_source_metadata capability check
- Add removeSession cleanup after killSession in !res.writable path
- Add i18n key sideTask.renameFailed for error fallback
- Add unit tests for selectVisibleHistoryRecords invariant

* fix(cli): update telemetry-catalog route drift guard to 51 routes (#7929)

* fix(web-shell): address review feedback round 2 on context panels PR (#7929)

- Fix /fork sider discarding createSideTask() return value: show toast
  when side tasks are unavailable
- Fix layout feedback loop: availableWidth no longer depends on
  environmentPanelVisible since the CSS overlay does not change the
  chat pane DOM width
- Remove dead environmentPanelSuppressed state (never set to true)
- Restore setArtifactPanelOpen(false) in closeArtifactPanelTab when
  the last tab is closed
- Extract agentDisplayName(task) to a local variable to avoid triple
  invocation per render

* fix(web-shell): dedupe completed background agents in environment panel (#7929)

getEnvironmentAgentTasks correlated a transcript tool card with the live
/tasks snapshot only on toolUseId, the notification taskId, and a
<subagentType>-<callId> derived id. A completed background agent can lose
that linkage (its live task carries no usable toolUseId and its daemon id
is general-purpose-<internalId>), so the trailing loop appended the live
task as a second entry. Add a conservative content fallback (prompt, or
description+subagentType) mirroring the daemon's legacy resolver.

* feat(web-shell): support side tasks during active turns

* fix(web-shell): deduplicate completed subagents and gate sourceType on capability (#7929)

* fix(web-shell): restore background agent reconciliation and fix agent dedupe (#7929)

Restore the one-shot subagent reconciliation for inline background Agent tool
cards. Persisted notification records do not always retain a toolUseId, so the
SSE discrete-notification path alone can leave a card stuck in Running; the
documented fallback resolves pending cards through the subagent endpoint after
catch-up, reconnect, and terminal notifications.

Also stop the loose description content fallback in getEnvironmentAgentTasks
from claiming a live task that another transcript tool call already links
precisely (by toolUseId, message taskId, or derived id). Two agents sharing a
description previously collapsed into one: the fallback stole the linked task,
its owner re-matched the same task, and the orphan was dropped.

* fix(web-shell): address critical review feedback on context panels (#7929)

* fix(web-shell): reconcile side-task state across sessions and listings (#7929)

* fix(web-shell): preserve contextual panel fallbacks

---------

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>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.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>
2026-07-30 13:45:30 +00:00
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
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
b0ce7dc518
feat(web-shell): allow widening sidebar up to half the window width (#7778)
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.
2026-07-27 02:46:02 +00:00
Shaojin Wen
596abd9664
fix(web-shell): allow pin and group for secondary workspace sessions (#7716)
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
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.
2026-07-25 14:14:16 +00:00
Shaojin Wen
d61b0ea475
perf(web-shell): paint the composer git chip before git status completes (#7680)
* 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>
2026-07-25 07:05:52 +00:00
Shaojin Wen
1130865949
fix(web-shell): show full session names on hover (#7662)
* fix(web-shell): show full session names on hover

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

* test(web-shell): cover archived session tooltip attribute (#7662)

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>
2026-07-24 12:10:58 +00:00
dreamWB
5561ba1e92
fix(web-shell): honor locked workspace session actions (#7629)
* fix(web-shell): honor locked workspace session actions

* fix(web-shell): preserve scoped session actions

* test(web-shell): strengthen untrusted action coverage
2026-07-24 06:14:45 +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
ytahdn
38ff85430c
fix(web-shell): avoid redundant git status requests (#7496)
Co-authored-by: ytahdn <ytahdn@gmail.com>
2026-07-22 09:30:59 +00:00
Heyang Wang
6f5d6dfd65
feat(web-shell): add workspace selector button with add/switch dropdown in composer toolbar (#7390)
* feat(web-shell): add managed workspace selector

Let Web Shell create and select daemon-managed workspaces without
changing ownership of existing sessions.

- Add capability-gated existing and scratch workspace registration
- Validate scratch roots, trust provenance, capacity, and shutdown races
- Serialize workspace mutations, session switching, and refresh results
- Add SDK/WebUI wiring and focused cross-package regression coverage

# Conflicts:
#	packages/web-shell/client/App.tsx
#	packages/web-shell/client/components/sidebar/WebShellSidebar.tsx

# Conflicts:
#	packages/cli/src/serve/capabilities.ts
#	packages/cli/src/serve/routes/workspace-management.ts
#	packages/cli/src/serve/server.test.ts
#	packages/sdk-typescript/src/daemon/DaemonClient.ts
#	packages/web-shell/client/App.tsx
#	packages/web-shell/client/components/dialogs/AddWorkspaceDialog.tsx
#	packages/web-shell/client/components/sidebar/WebShellSidebar.tsx

* fix(web-shell): revalidate workspace before session creation

Prevent a stale workspace selection from bypassing the latest trusted
capability snapshot during lazy session creation.

- Validate the selected workspace before passing it to the daemon
- Fall back to the primary workspace when trust has been revoked
- Add a regression test for the pre-effect race window
- Remove stale branch state and clarify add-workspace ownership

* fix(web-shell): improve workspace removal feedback

Keep workspace removal controls legible and make blocked force removals
visibly inactive.

- Size the action menu independently from its narrow icon trigger
- Add a disabled affordance and suppress destructive hover styling
- Cover the removal menu width override with a regression test

* fix(web-shell): centralize existing workspace registration

Route sidebar and composer entry points through the App-owned dialog so
capability gating and workspace reconciliation remain consistent.

- Forward display names only when the daemon advertises support
- Hide and suppress persistence when registration is runtime-only
- Mark directory registrations with existing-workspace provenance
- Cover both entry points and capability combinations with tests

* fix(web-shell): address review feedback on workspace dialogs and capability docs (#7390)

- Document dynamic_workspace_registration and scratch_workspace_registration
  in the conditional serve-features table so the capabilities-docs-contract
  test passes.
- Gate DialogShell backdrop-click and Escape dismissal on the dismissible
  prop so non-dismissible dialogs ignore both gestures.
- Surface an inline error when an added folder registers but the capability
  refresh fails, mirroring the scratch recovery path.
- Add coverage for the active-session workspace switch and the add-folder
  refresh-failure paths.

* fix(web-shell): address review feedback on workspace dialogs and capability docs (#7390)

---------

Co-authored-by: heyang.why <heyang.why@alibaba-inc.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
2026-07-22 01:10:18 +00:00
yuanyuanAli
ff14e65792
feat(web-shell): Add sidebar customization API for branding, navigation, session actions, and footer (#7379)
* feat(web-shell): Add sidebar customization API for branding, navigation, session actions, and footer

Add new sidebar configuration options to WebShellSidebarOptions:

- primaryNav.items: Control which built-in nav buttons are shown (newTask, plugins, scheduledTasks, goals)
- primaryNav.render(): Append custom content after built-in nav buttons
- hideProjectHeader: Hide the 'Projects' header row (search + add workspace)
- sessionActions.items: Control which session action items appear (both inline and dropdown)
- sessionActions.inlineItems: Control which items render as inline hover buttons (supports all action types with icon/text fallback)
- footer.render(): Inject custom UI elements on the left side of the footer
- Move scheduledTasks and goals from footer.items to primaryNav.items

Visibility follows a strict 'hide-first' policy: inline buttons require all three conditions (items includes + inlineItems includes + built-in capability check) to render.

* fix(web-shell): Prevent inline/dropdown duplication and add pin/archive dropdown fallback

- Critical fix 1: Dropdown items now exclude items already shown as inline buttons (added !inlineActionItems.has guard to each dropdown entry and trigger visibility).
- Critical fix 2: pin and archive now have dropdown menu entries as fallback when not configured as inline items, preventing them from becoming inaccessible.
- Narrowed inlineItems type to WebShellSidebarSessionInlineActionItem (excludes details/group which have no working inline handlers), preventing dead buttons.
- Added destructive color styling (var(--destructive)) to inline delete button when not disabled.

* fix(web-shell): Re-export new sidebar types and add visibility matrix tests

- Re-export WebShellSidebarPrimaryNavOptions, WebShellSidebarPrimaryNavItem, WebShellSidebarSessionActionsOptions, WebShellSidebarSessionActionItem, and WebShellSidebarSessionInlineActionItem from client/index.tsx so consumers can import them by name.
- Add session-action-visibility.test.ts: table-driven tests covering the items × inlineItems × capability matrix (default config, empty items/inlineItems, dedup guarantee, pin fallback to dropdown). 7 test cases, all pass.

* fix(web-shell): gate readOnly archive on sessionActionItems, fix footer null safety and docs accuracy (#7379)

---------

Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
2026-07-21 11:20:51 +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
samuelhsin
9e822d6004
feat: support workspace display names (#7179)
* feat(sdk): support workspace display names

* docs: add Web Shell screenshot

* feat(web-shell): add workspace display names

* fix(serve): harden workspace display name updates

* refactor(serve): simplify workspace display names

* fix(serve): validate trimmed workspace display names

* feat(serve): add workspace update API

* docs(serve): clarify workspace display name null handling

* docs(sdk): list addWorkspace in daemon client methods
2026-07-20 15:16:44 +00:00
Shaojin Wen
0c271659df
feat(daemon): worktree-isolated sessions for parallel tasks (#7221)
Add support for creating sessions in isolated git worktrees from the
Web Shell, enabling multiple tasks to run in parallel within the same
workspace without polluting the main working directory.

Daemon:
- POST /session accepts optional worktree param, creates worktree via
  GitWorktreeService, relocates session via changeSessionCwd
- Worktree metadata persisted in SessionEntry, BridgeSessionSummary,
  and sidecar file (<sessionId>.worktree.json) for daemon restart
  recovery
- GET /workspaces/:workspace/git supports ?cwd= for worktree-scoped
  git status queries (path.resolve + containment check)

SDK:
- CreateSessionRequest/DaemonSession/DaemonSessionSummary gain
  worktree field; DaemonSessionClient exposes worktree getter
- WorkspaceDaemonClient.workspaceGit() accepts optional cwd param

Web Shell:
- Workspace branch pill dropdown offers 'New Worktree Task' (git repos
  only) with purple GitForkIcon and description
- Git chip turns purple with GitForkIcon for worktree sessions
- Session list shows inline ⑂ badge for worktree sessions
- Empty-state welcome badge explains worktree isolation
- Git status queries target worktree path, not workspace root
- session_cwd_changed event filtered from chat transcript

Design doc: docs/design/2026-07-19-webshell-worktree-sessions.md
2026-07-19 23:47:03 +00:00
Shaojin Wen
582fb49603
feat(web-shell): git status chip, visual working-tree diff, and sidebar git status (#7054)
* feat(web-shell): git status chip, visual working-tree diff, and sidebar git status

Bring working-tree Git awareness to the Web Shell (browser daemon session UI):

- Toolbar branch chip becomes a live status indicator: dirty (staged/unstaged/
  untracked), ahead/behind upstream, stash count, detached HEAD, in-progress
  operation (merge/rebase/cherry-pick/revert/bisect), and conflict count, each
  with a non-color cue.
- Read-only "Changes" dialog: working-tree-vs-HEAD file list with per-file,
  line-level, per-side syntax-highlighted diffs; opens via /diff or a dirty
  chip; untracked files expand as fully-added and deleted files still diff.
- Per-workspace git status in the sidebar: a compact icon-only chip per trusted
  workspace (status dot + hover tooltip); click opens that workspace's dialog.

All git access goes through the daemon REST API with per-workspace trust
gating; new SDK status fields are optional and additive (v2).

* fix(web-shell): themed tooltips and git-chip review follow-ups

Tooltips now render on the themed popover surface (bg-popover /
text-popover-foreground / border + fill-popover arrow) instead of the
inverted bg-foreground default, so they read dark-on-dark rather than a
bright box on the dark theme. Fixing the shared primitive corrects the
git branch tooltip in the composer toolbar and sidebar, plus every other
tooltip, at once.

Also addressing review feedback on the git integration:
- Replace the hand-drawn detached/conflict/stash SVG icons with
  lucide-react (CircleDot / TriangleAlert / Layers) per the web-shell
  icon convention.
- Gate the tooltip "Working tree clean" message on an enriched status
  (computedAt) so a branch-only status no longer asserts clean.
- Include the file path in the diff dialog row aria-label so screen
  readers can distinguish files.
- Reset the toolbar git chip on workspace switch so it never shows the
  previous repo's branch/counts while the new fetch resolves.
- Log a sidebar git poll failure only on the success->failure transition
  to avoid spamming a long-lived tab.
- Correct the SDK doc for DaemonWorkspaceGitDiffFile.added/removed
  (0, not undefined, for binary files).

* fix(web-shell): address git-integration review suggestions

Follow-ups from the /review pass on the git integration:

- GitBranchIndicator: include the short SHA in the detached-HEAD tooltip
  title, and add the "Working tree clean" status to the aria-label (gated
  on an enriched status, matching the tooltip) so the two never drift.
- WorkspaceSection: keep the last known git status on a transient poll
  failure instead of blanking the chip for a whole interval.
- App: surface a toast for `/diff` when no workspace is available instead
  of silently consuming the composer input.
- Tests: cover the diff dialog's list-load and per-file load error paths,
  and detectGitOperation's revert/bisect branches.
- Design doc: align the getGitWorkingTreeStatus spec text with the
  decision (transient states return status with `operation`; null is
  reserved for non-repo / git failure).

* fix(web-shell): focus-visible ring for git chip button; align doc poll interval

- Add a :focus-visible outline to .gitBranchChipButton so keyboard users
  get a visible focus indicator (the chip resets UA button chrome).
- Design doc: align the active-workspace poll-interval references at 30s
  to match the implementation.

* fix(web-shell): surface capped diffs, catch row-build failures, cover degradation paths

Address the remaining review findings on the git integration:

- Truncation is no longer silent: fetchGitDiffHunksForFile now returns
  { hunks, truncated } — the parser records files that actually lost
  lines to MAX_LINES_PER_FILE (tracked path), and the untracked
  synthesis reports its byte/line caps. The route forwards an additive
  `truncated` flag on the hunks response (absent when not truncated, so
  older clients and daemons are unaffected), and the Changes dialog
  renders a "Diff truncated" note under the visible window.
- DiffHunks catches an unexpected buildRows rejection (e.g. malformed
  hunk lines) and shows the per-file error instead of leaving an
  unhandled rejection and a silently empty diff area.
- New tests: untracked and tracked truncation at the core caps, the
  route's truncated passthrough (and its absence when clean), the
  branch-only degradation when the working-tree summary throws, the
  malformed-hunks error path, and the Shiki success path (a fake
  tokenizer proving add rows pull new-side tokens and del rows pull
  old-side tokens, not the plain-text fallback).

* fix(web-shell): drop dialog backdrop-blur that froze the page on open

The dialog and alert-dialog overlays applied `backdrop-blur-xs`, which
forces the browser to rasterize and blur the entire content behind the
overlay when a dialog opens. With a long transcript behind it, that
main-thread paint+blur froze the whole page — e.g. clicking the git
branch chip to open the Changes dialog. Keep the bg-black/10 scrim for
separation and drop the blur.

* fix(core): guard synthesizeUntrackedHunk against non-regular files

synthesizeUntrackedHunk opened an untracked path before checking its
type, so an untracked FIFO (listed by `ls-files --others`) would block
on open() forever waiting on a writer — hanging the daemon's event loop
and leaving the Web Shell Changes dialog stuck on a permanent loading
state. lstat-gate on regular files before opening, matching the existing
guard in countUntrackedLines. Adds a FIFO regression test.

* fix(web-shell,core): rename expansion, no-newline marker, chip measurement

Round-5 review Criticals:

- core: key renamed diff entries by the real (post-rename) path and carry
  the old path for display, so renamed rows can be expanded — the synthetic
  `old => new` key was sent to git as a nonexistent literal path. The diff
  dialog renders the rename as `old → new`.
- core: preserve Git's `\ No newline at end of file` marker through the hunk
  parser so a trailing-newline-only edit isn't shown as identical
  removed/added lines (the viewer already renders it as a meta row).
- web-shell: the toolbar's hidden git-chip measurement replica now renders
  the full chip content via the extracted GitBranchChipContent, so the
  expanded width includes the status indicators and the compact/expanded
  toggle no longer oscillates near the responsive threshold.

* fix(build): generate git-commit info even when prepare build is skipped

The review tooling runs `npm ci` with QWEN_SKIP_PREPARE=1 (to skip the
heavy prepare build) and then builds only the changed workspaces. Because
`prepare` exited before generating the gitignored git-commit.ts, a
per-workspace build of packages/cli failed at the unchanged systemInfo.ts
on the missing `../generated/git-commit.js` module. Generate the git-commit
info in the skip path too — it is cheap and never fails hard — so a later
per-workspace build or typecheck finds the module. The non-skip path still
generates it via `npm run build`.

* fix(web-shell,cli): address round-6 review suggestions

- cli: carry the pre-rename path (oldPath) through DiffRenderRow and show
  renamed files as `old → new` in both the Ink and plain-text renderers.
  The rename-keying fix updated the daemon and web-shell dialog but not the
  CLI `/diff` renderer, which silently dropped the old path.
- web-shell: key DiffFileRow by workspace + path so switching workspace
  remounts the row instead of reusing another workspace's hunks/open state
  for a path both workspaces share.
- web-shell: show a loading placeholder in DiffHunks while rows are (re)built
  (e.g. after a theme switch) instead of an empty, jumpily-resized box.
- web-shell: cover the /diff local intercept in App.test.tsx (opens the
  Changes dialog and is not forwarded to the agent).

* fix(web-shell,cli,core): address round-7 review suggestions

- cli: sanitize the rendered filename (and pre-rename oldPath) in the Ink
  DiffStatsDisplay via sanitizeFilenameForDisplay, matching the plain-text
  renderer so a crafted path can't inject into the interactive view.
- cli: apply the read headers before awaiting the per-file diff fetch (as
  handleDiffList does) so error responses also carry no-store/nosniff.
- cli + web-shell: strip Unicode bidi embedding/isolate controls
  (U+202A-202E, U+2066-2069) in the filename/control-char sanitizers so a
  crafted filename can't visually spoof its extension.
- core: guard countStashEntries with an lstat type check before readFile, so
  a symlink-to-FIFO at logs/refs/stash can't block the event loop (the same
  hazard already guarded in the untracked-file readers).
- core: cover fetchGitDiffHunksForFile's transient-state guard with a test
  (the sibling helpers already had one).

* fix(web-shell,cli,core): address round-8 review suggestions

- core: pass --no-optional-locks to the ls-files call in
  fetchGitDiffHunksForFile, matching the other runGit calls so it doesn't
  contend for an optional index-refresh lock alongside concurrent git
  add/commit.
- cli: add a route test asserting a rename's oldPath survives serialization
  end-to-end (keyed by the new path, old path carried alongside).
- web-shell: add a GitDiffDialog test for the hiddenCount>0 "N more files
  not shown" note (every payload previously used hiddenCount: 0).
- web-shell: drop the nonexistent primaryLabel prop from the WorkspaceSection
  test (it is not a WorkspaceSectionProps member).
- docs: correct the plan doc — large-diff virtual scrolling was explicitly
  descoped (core caps + per-file lazy loading), not implemented in Phase 2.

* fix(cli,web-shell): address round-9 review findings

- cli: propagate the pre-rename oldPath through DiffDialog's
  perFileToUnified and render renamed files as `old → new` in the
  interactive diff viewer (the rename-keying fix had updated the daemon,
  the web-shell dialog, and the /diff stats, but not this viewer).
- cli: cover DiffStatsDisplay's rename (`old → new`) rendering and the
  sanitizeFilenameForDisplay path for hostile filenames carrying control
  characters.
- web-shell: guard the GitBranchIndicator test afterEach against
  double-unmounting an already-unmounted root (the localization tests
  assert on getTranslator without calling render()).

* fix(core,cli,web-shell): rename-aware single-file diff (old→new)

fetchGitDiffHunksForFile pathspec-limited the diff to the new path, which
defeats git's rename detection — a renamed file was reported as fully
added (every line +) instead of its actual edit. Thread an optional
pre-rename path through the single-file endpoint (core → route → SDK →
dialog) and diff old→new with -M when it is present, so expanding a
renamed file shows its real content change.

* fix(cli): address round-10 review suggestions

- DiffDialog: split the path-width budget between old and new paths for a
  rename (reserving the " → " separator) so the combined width stays within
  maxPathChars instead of overflowing the row layout.
- textUtils: extend MULTILINE_CONTROL_CHARS_REGEX with the Unicode bidi
  ranges (matching FILENAME_CONTROL_CHARS_REGEX) and add a test that
  sanitizeFilenameForDisplay strips bidi embedding/isolate controls.
- workspace-git-diff route: add a test that ?oldPath= is parsed and
  forwarded to fetchGitDiffHunksForFile.

* test(sdk),docs: cover diff client methods; align design doc

- sdk: add DaemonClient unit tests for workspaceGitDiff() and
  workspaceGitDiffFile(path, oldPath?) — URL construction (incl. urlEncode
  on path/oldPath, with and without oldPath, plus the workspace-qualified
  route) and response deserialization, mirroring the existing workspaceGit()
  test.
- docs: add the oldPath? param to the workspaceGitDiffFile API spec; record
  that the diff client methods now have unit tests (correcting the claim
  that workspaceGit() had none); attribute the bundle-limit bump to
  packages/sdk-typescript/scripts/build.js; clarify ahead/behind are relative
  to upstream (0, and ↑N/↓N not shown, without one).

* fix(web-shell,core): address round-11 review suggestions

- GitBranchIndicator: count conflicted entries as dirty — a merge where every
  changed file is conflicted (staged=unstaged=untracked=0) is still
  uncommitted, so the expanded chip's dirty dot / data-dirty now reflect it.
- core: split the status branch line at the last "..." (the branch/upstream
  separator) so a dotted branch name isn't truncated at the first "...".
- GitDiffDialog: guard DiffFileRow's in-flight fetch against unmount via a
  cancelled ref, matching DiffHunks / GitDiffDialog.
- tests: forward oldPath when expanding a renamed file in the web-shell
  dialog; bidi-strip coverage for the web-shell sanitizeControlChars;
  untrusted-guard coverage on the single-file diff route; conflicted-only
  dirty; branch-line "..." split.

* fix(web-shell,cli): address round-12 review suggestions

- DiffDialog: only render the rename "old → new" when there's room for both
  sides (≥19 cols, so each gets ≥8); otherwise fall back to the new path
  alone, so a narrow terminal no longer overflows the row (the Math.max(8,…)
  floor could exceed maxPathChars).
- GitBranchIndicator test: guard afterEach container.remove() for non-render
  tests run in isolation, and make the compact-mode ↑-suppression assertion
  non-vacuous by giving the fixture an ahead count.
- App: compute the active workspace once (useMemo) and share it between the
  git-status effect and the Changes-dialog entry point, so the chip and the
  dialog can't drift onto different repos.

* fix(core,docs): address round-13 review suggestions

- core: add a rebase-apply detection test (git am / an interrupted
  `rebase --apply` creates rebase-apply, which detectGitOperation also maps
  to 'rebase'); previously only rebase-merge was exercised.
- docs: correct section 5 to describe the actual diff-dialog mechanism
  (diffWorkspaceCwd state, not the stale activePanel design).

* test(core): cover stray no-newline marker before any hunk header

parseGitDiff's pre-hunk guard already skips a "\ No newline at end of
file" marker that appears before any @@ header, so a malformed/truncated
diff can't throw on a null currentHunk and lose subsequent files' hunks;
add a regression test pinning that behavior.

* fix(web-shell): unstick per-file diff loading and skip non-path git poll

- DiffFileRow: reset the cancelled-fetch flag on mount so StrictMode's
  mount/unmount/mount replay no longer leaves it latched at true, which
  dropped the fetched hunks and froze the row on "Loading changes…" despite
  a 200 response.
- WorkspaceSection: skip the git status poll when the workspace cwd is not an
  absolute path. A synthetic fallback workspace carries a display name there,
  which the cwd-qualified route rejects with a 400.

* fix(web-shell,cli): address review suggestions on the git diff surface

- GitDiffDialog: highlight each diff side independently so a small side
  keeps syntax highlighting even when the other side exceeds the size cap
  (the old guard dropped both as soon as either was too large).
- ChatEditor: complete the .gitBranchChipButton reset (font/color/padding/
  margin) so the clickable dirty-tree chip matches the read-only output chip
  instead of picking up UA button styling.
- DiffDialog: cover the interactive rename display (old to new on a wide
  terminal), mirroring the rename tests DiffStatsDisplay and GitDiffDialog
  already have.

* test(web-shell,cli): cover git chip clean/reload/traversal paths, fix doc

- GitDiffDialog: add the missing expect(header).not.toBeNull() guard to the
  three expand-file tests that lacked it, matching the others in the block.
- GitBranchIndicator: cover the known-clean aria-label branch (computedAt set
  and every change counter zero).
- WorkspaceSection: verify a reloadToken change re-fetches git status instead
  of waiting for the next 60s poll.
- workspace-git-diff route: verify a traversal oldPath is forwarded to core
  and surfaced as available:false rather than escaping the workspace.
- Design doc: /diff is handled via setDiffWorkspaceCwd, not setActivePanel.

* fix(core): allow literal `..foo` paths in diff normalization

- toRepoRelativePath: reject only a real climb-out (`..` or `../…`), not a
  literal `..foo` filename at the repo root, which the bare startsWith('..')
  over-rejected, leaving the diff viewer unable to render such a file.
- parseGitDiff: cover the truncatedPaths output set directly (it was only
  exercised indirectly through fetchGitDiffHunksForFile).

---------

Co-authored-by: wenshao <wenshao@example.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-07-18 10:06:07 +00:00
Shaojin Wen
6dce543491
feat(web-shell): add a workspace Goals page, and stop losing /goal on daemon resume (#6561)
* fix(goals): persist goal cards and restore the hook on daemon resume

In daemon mode a `/goal` was silently lost whenever its session was
reloaded or `qwen serve` restarted: the goal card vanished from the
transcript and the Stop hook was never re-registered, so the loop simply
stopped advancing. The TUI does neither of these things wrong; the ACP
path was missing both halves.

Goal cards were only ever emitted as live SSE `_meta` (MessageEmitter's
emitGoalStatus / emitGoalTerminal) and never written to the transcript,
so the one durable store — the ChatRecord JSONL — had nothing to restore
from. Record them from Session.emitGoalStatus, the single choke point for
`set` and `cleared` (the sessionGoalClear ext method routes through it
too), and from the goal terminal observer for `achieved` / `failed` /
`aborted`. Persisting `cleared` matters on its own: without it the last
stored card stays `set`, and a later resume would revive a goal the user
explicitly dropped.

HistoryReplayer dropped those records on the way back out — it reads only
`item['text']`, and a goal card has no `text` field — so re-emit them as
`_meta.goalStatus`. Per-iteration `checking` cards are skipped: a TUI
transcript stores one per stop-hook turn and clients suppress them as
noise. That costs no fidelity, because restore reads the records directly
rather than the replay output.

With the transcript carrying the goal again, add #restoreGoalOnResume to
loadSession and unstable_resumeSession, alongside #restoreWorktreeOnResume.
It rebuilds the goal cards from the resumed ChatRecords (they live inside
system/slash_command records' outputHistoryItems) and reuses the existing
findGoalToRestore / findLastTerminalGoal / registerGoalHook logic, trust
and hook-policy gates included.

* feat(web-shell): add a workspace Goals page

`/goal` had no visual surface in the web shell. You could set and clear
one from the composer, but the only feedback was a status-bar pill and a
transcript card, and there was no way to see every goal running in the
workspace at once. Add a full-pane Goals page alongside Scheduled Tasks.

Each row shows the condition, the session driving it, whether the loop is
mid-turn, the judge's turn count and last verdict, and how long the goal
has been running. A row opens its session — the transcript IS the goal's
history — or clears the goal. A form starts a new goal in a fresh session,
so the loop doesn't take over a conversation already in progress.

Reading the goals needs a round trip. They live in the owning `qwen --acp`
child's in-memory store, and serve runs in a separate process holding only
a bridge, so there is nothing local to read. Add a `sessionGoalGet` ext
method that reports one session's goal state, wrap it in
bridge.getSessionGoal (mirroring clearSessionGoal), and have `GET /goals`
fan out over the workspace's live sessions concurrently — one timeout for
a wedged child rather than one per session. A session whose probe rejects
is dropped rather than failing the whole list. Clearing reuses
`POST /session/:id/goal/clear`, so the page and a `/goal clear` typed in
chat take the same path through the daemon.

Only loaded sessions appear, which is the honest answer rather than a
limitation: a goal advances only while its session is resident.

Three entry points: a sidebar button, the status-bar goal pill (now a
button), and a bare `/goal`, which opens the page instead of asking the
daemon to print its status as text — matching how `/schedule` behaves. It
sends no prompt and touches no session, so it works mid-turn too.
`/goal <condition>` and `/goal clear` are unchanged.

The integration test exercises the whole chain against a real daemon:
`GET /goals` -> bridge -> ext method in a spawned `qwen --acp` child.

* fix(web-shell): stop the Goals poll from overlapping itself

`GET /goals` fans out one ext-method probe per live session, and a wedged
child holds it for the bridge's 10s `initTimeoutMs` — the same order as the
10s poll interval. `withActionTimeout` rejects the wait at 30s but never
aborts the underlying fetch, so a fixed `setInterval` could stack several
fan-outs against an already-struggling daemon. `reloadSeqRef` only keeps a
stale response from overwriting state; it does nothing about the pile-up.

Replace the interval with a single self-chaining loop that owns both the
initial load and the polling, scheduling each fetch only once the previous
one has settled. Folding the mount load into the chain matters: left in its
own effect, the first timer would still fire while it was in flight.

Reported by Copilot on #6561.

* fix(goals): address review — clear-keyword condition, silent failures, theme vars

From the /review suggestions on #6561. Applied the ones that held up under
verification; the rest are answered in the PR thread with evidence.

- The New goal form accepted a clear keyword as a condition. It travels as
  `/goal <condition>`, so "clear" (or stop/off/reset/none/cancel) reached the
  daemon as a clear command: the fresh session dropped its own goal the instant
  it was set, with nothing to show for it. Reject it in the form. The keyword
  list and `/goal` arg parsing move to `utils/goalCondition.ts` so the page and
  App share one definition instead of the page reaching into App.

- Starting a goal failed silently. `onCreateGoal` switches to the chat view
  first, which unmounts the Goals page, so the inline form error that
  `sendPrompt` rejection produced was dropped by the page's own unmount guard.
  Surface it as a toast instead.

- `GoalsDialog.module.css` used `var(--destructive, #dc2626)`, but nothing
  defines `--destructive`; the hardcoded fallback stayed the same red in both
  themes. Use `--error-color` and match ScheduledTasksDialog's focus outline.

- `recordGoalStatusItem` swallowed recording failures with a bare `catch {}`.
  Silently losing that write is precisely the failure this recording exists to
  prevent, so log it.

- `GET /goals` dropped failed probes silently — an empty page and a page whose
  probes all failed look identical to the client. Log the dropped sessions and
  their reasons.

Tests: clear-keyword and MAX_GOAL_LENGTH form validation, goalCondition unit
tests, `sessionGoalGet` argument validation, session load surviving a throwing
goal restore, `/goals` drop logging, and a regression test showing `/goal clear`
sent as a prompt does persist its cleared card (a reviewer flagged this as
missing; it is not).

* fix(goals): cap restored conditions, keep goal-creation errors on screen

Second round of review on #6561.

- `restoreGoalFromHistory` re-registered whatever condition the transcript
  held, skipping the 4000-char cap `/goal` enforces at set time. A transcript
  is a file: a corrupted or hand-edited `condition` would ride along in every
  judge call and continuation prompt for the rest of the session. Gate it
  alongside the existing trust and hook-policy gates. `MAX_GOAL_LENGTH` moves
  to `restoreGoal.ts` and `goalCommand.ts` imports it — the reverse direction
  would be a cycle, since goalCommand already depends on this module.

- Starting a goal switched to the chat view before awaiting `sendPrompt`,
  which unmounted the Goals page. The previous commit routed the rejection to
  a toast, but the better fix is not to leave: switch views only once the
  prompt is admitted, so the error lands in the form the user is looking at.
  `GoalsDialog` keeps a toast fallback for the case where the page is closed
  while the prompt is still in flight.

- Move the `debugLogger` declaration below the imports in `restoreGoal.ts`.
  Imports are hoisted so this compiled, but a statement wedged between two
  import blocks is not something to leave behind.

* fix(goals): surface restore/record failures, report unprobed sessions

Third round of review on #6561.

- `debugLogger.warn` no-ops unless a debug session is active
  (`debugLogger.ts:216`), so a failed goal restore and a failed goal-card
  write were both invisible in production — the two failure modes this PR
  exists to fix. Promote them to `writeStderrLine`, which both `ui/App.tsx`
  and `session/Session.ts` already use.

- `GET /goals` now returns `droppedCount`. A brownout in which every probe
  fails returned `{ goals: [] }`, indistinguishable from a workspace with no
  goals — so the user re-creates goals that are already running. The Goals
  page shows a notice when the list is incomplete.

- `running` on the wire is really "the owning session is mid-turn", which a
  manual prompt in that session also sets. Renamed to `hasActivePrompt` so
  the field reports what the daemon actually knows. The UI still maps it to
  Working/Waiting.

- Fix the stale "keep in sync" pointer in `goalCommand.ts`: the clear keywords
  moved from `App.tsx` to `utils/goalCondition.ts` in the previous commit.

Tests for the four coverage gaps the review named: the `systemMessage` fallback
in `goalTerminalEventToHistoryItem` (including the known lossy collapse when
both fields are set), `#restoreGoalOnResume` on an empty transcript,
`listGoals`/`clearGoal` in `actions.ts`, and the `sendPrompt`-after-
`createNewSession` failure path (added last commit). Plus `droppedCount`
projection and the degradation notice.

* test(goals): update the /goals integration test for droppedCount

Adding `droppedCount` to the `GET /goals` payload broke the end-to-end
assertions, which still expected `{ v: 1, goals: [] }`. Caught in review, not
by CI: the Integration Tests job is gated off for this PR, so nothing ran
these against a real daemon after the shape changed.

`droppedCount: 0` is the load-bearing half of the live-session assertion. A
dropped probe also yields an empty `goals`, so the old assertion could not
tell a successful ext-method round trip from a silently failed one.

Re-ran against a spawned `qwen serve` + `qwen --acp` child: green with the
fix, red without it.

* fix(goals): refuse to replay an oversized goal card

`restoreGoalFromHistory` gates the condition at MAX_GOAL_LENGTH, but
`HistoryReplayer` did not: a corrupted or hand-edited transcript could still
ship an unbounded `condition` to every client inside `_meta.goalStatus`. Apply
the same gate at the replay emit site, so neither the card nor the hook
survives an oversized condition.

The gate deliberately does NOT move into `parseGoalStatusItem`, which would be
the tidier-looking place. `findGoalToRestore` and `findLastTerminalGoal` scan
backwards and stop at the FIRST goal card they meet, so dropping a card at
parse time silently promotes the card before it. A transcript ending in an
oversized `cleared` would then restore the `set` that preceded it — resurrecting
a goal the user explicitly cleared, the exact failure persisting `cleared` was
added to prevent. Parsing therefore stays lossless and the length check lives at
each consumer.

Tests pin both halves: replay refuses at 4001 and emits at exactly 4000, and
three scanner tests show an oversized card still wins the scan so restore can
fail closed on it.

* fix(goals): keep the terminal observer alive across ACP resume

Addresses the latest review round on #6561.

`registerGoalHook` calls `unregisterGoalHook`, which clears the session's
goal-terminal observer. The ACP restore path passes no `addItem`, so nothing
reinstalled it: a restored goal reached achieved/failed/aborted with no wire
update and no persisted terminal card, and the next reload revived a goal that
had already finished. The no-goal branch unregisters too, so every ACP resume
lost the observer, not just ones with a goal. `#restoreGoalOnResume` now
reinstalls it unconditionally.

A restore blocked by trust or hook policy left the client showing an active
goal that nothing drives. Restore now reports `blockedBy`, and history replay
emits a trailing `cleared` card naming the reason. The card is emitted, not
recorded, so a later resume in a trusted folder still restores the goal. It is
emitted from inside replay because `loadSession` batches replay updates into
its response, and a notification sent afterwards would reach the client first.
Gated behind a `HistoryReplayer` option: export and `restoreSessionHistory`
render a transcript rather than resume it, and the export config is a stub that
throws on any method it does not implement.

Transcript payloads are now treated as untrusted. `outputHistoryItems` is
checked with `Array.isArray` before iteration and each entry for being a plain
object before any field is read; a hand-edited record could otherwise throw and
take the whole restore down, skipping the hook while replay still showed the
goal as active.

Also:

- Carry `setAt` across resume instead of restarting the clock, scanning back to
  the run's `set` card when the newest card is a `checking` card (which had no
  `setAt`; they now persist one).
- Refuse to restore an empty condition, as `/goal` does.
- Warn instead of silently no-opping when no chat recording service is present.
- Cap `GET /goals` session probes at 10 in flight.
- Drop `lastTerminal` from the `sessionGoalGet` response and `BridgeSessionGoal`
  — no consumer reads it, and it was returned unprojected.
- `GoalsDialog` keeps the form and the typed condition when creation fails, and
  clears a stale dropped-session count when a reload fails outright.
- Cross-package test pinning `GOAL_CLEAR_KEYWORDS` and `MAX_GOAL_LENGTH` against
  the CLI sources they mirror.

* fix(goals): drop the condition length cap on restore and in the web shell

#6665 removed the 4,000-character cap `/goal` applied when setting a goal, but
the restore path and the Web Shell form still enforced it. After merging main
that split the surfaces: a long condition `/goal` now accepts was persisted as a
`set` card, then refused by `restoreGoalFromHistory` on the next resume and
dropped from the replay entirely — the goal died on reload and the user never
saw a card explaining why.

Remove the cap everywhere rather than reinstate it at set time. A corrupted or
hand-edited transcript can now restore an arbitrarily long condition, but that
is exactly what `/goal` itself permits, so it is no longer a distinct risk. The
empty-condition gate stays: it is the one case that is meaningless rather than
merely large.

- `goalConditionBlockedBy` rejects only an empty condition.
- `HistoryReplayer` no longer skips long goal cards.
- `GoalsDialog` drops the form check and the `maxLength` attribute, which had
  been silently truncating a long condition before the user could submit it.
- `MAX_GOAL_LENGTH` and the now-orphaned `goals.error.tooLong` i18n strings are
  deleted, along with the drift test's length half; the clear-keyword half of
  that test still guards the constant that is genuinely duplicated.

Also drops the `MAX_GOAL_LENGTH` import #6665 left unused in `goalCommand.ts`,
which failed `eslint --max-warnings 0`.

* fix(web-shell): reuse the empty session a failed goal attempt leaves behind

Setting a goal starts a fresh session and then sends `/goal <condition>` into
it. The daemon session is not created by the "new session" step, though —
`clearSession` only detaches and clears local state. `ensureSessionForPrompt`
creates the session lazily inside `sendPrompt`, so a prompt that fails after
the session exists leaves a created-but-empty one behind.

The Goals form keeps the condition and invites a retry, and the retry called
`createNewSession()` again: the empty session from the previous attempt was
abandoned and another created in its place. A user retrying a few times against
a busy daemon ended up with a column of blank chats in the sidebar.

Remember the stranded session and reuse it when it is still the current one,
rather than creating another. Nothing is deleted — a session is only reused
when the failed attempt left it empty and it has not been switched away from.
Once a goal actually lands, the session belongs to it, so the next goal starts
a fresh one as before.

* fix(goals): forget the stranded goal session on leaving the Goals page

Addresses the latest review round on #6561.

The stranded-session reuse added in bee3295aa was only safe while the Goals
page stayed up. Leaving it (Back button) and then talking to that session from
the composer turned it into a real conversation, but the ref still pointed at
it: returning to Goals and setting a goal would reuse it and drop the goal loop
on top of the user's conversation — the exact thing starting a fresh session
exists to prevent. The ref is now cleared whenever the view leaves 'goals', so
reuse can only ever hit a session the failed attempt itself created.

Also:

- `registerGoalHook` rejects a `setAt` in the future, not just a non-finite or
  non-positive one. Every duration downstream is `Date.now() - setAt`, so a
  transcript claiming the goal starts tomorrow rendered negative elapsed times.
- `makeRestoreInnerConfig` gains `isTrustedFolder`. Without it, `goalRestoreBlockedBy`
  threw `config.isTrustedFolder is not a function` on every resume in these
  tests, and `#restoreGoalOnResume` swallowed it — so the goal-gate assertions
  passed through the catch rather than the branch each one names. The
  hooks-disabled test now pins the branch it took, and fails if the config
  regresses.
- The status-bar goal pill names the goal in its accessible label. The visible
  pill is only "◎ /goal active (2m)" and the condition lived solely in `title`,
  a hover tooltip screen readers do not reliably announce.
- `.iconAction` gains a `:focus-visible` rule, matching `.iconButton` in
  DialogShell.module.css; keyboard users had no focus indicator on the
  clear-goal button.
- `GoalsDialog.test.tsx` restores real timers in `afterEach` rather than inline
  per test, so a failing assertion can no longer leak fake timers into the rest
  of the file.
- Tests for the Goals form's Cancel button and for the status-bar pill, neither
  of which had any coverage.

* fix(goals): identify a goal run by its condition, not just its card kinds

Addresses the latest review round on #6561.

`findSetAtOfRun` walked back from the active card for the `setAt` on the `set`
card that opened the run, stopping at any card that was not `set`/`checking`.
That assumed a terminal card always separates two goals, and a transcript is a
file: hand-edited, truncated, or written by a version that did not persist
terminal cards, it can hold two goals back to back. The scan then walked past
the second goal's cards into the first and returned ITS start time, so the
active goal's elapsed time was measured from a goal that had already ended. The
condition is what identifies a run, so the scan now stops when it changes.

Also:

- A malformed condition is reported once on resume, not twice.
  `restoreGoalFromHistory` is the only caller that knows the condition is bad,
  and three of its four callers (the TUI ones) discard the result entirely, so
  it stays the reporter; `#restoreGoalOnResume` no longer adds a second line for
  `condition-invalid`. The env gates were already reporting exactly once.
- Goal-restore stderr can no longer take down a session load. `writeStderrLine`
  reaches `process.stderr.write`, which throws on EPIPE or a closed fd; a throw
  from the catch block would have escaped into `loadSession`, so a best-effort
  restore would fail the very load it promises not to block.
- `isGoalClearCommand` checks the `/goal` prefix instead of assuming it.
  `goalArgOf` returns unrecognised text unchanged, so a bare `"clear"` — an
  ordinary thing to type into a chat box — answered true. Latent today because
  every caller pre-validates the prefix, but the contract was a trap.
- Tests for the throw path reinstalling the terminal observer, and for the Goals
  page opening a goal's session (success and failure), neither of which had any
  coverage.

* fix(web-shell): announce Goals dialog errors and give its buttons a focus ring

Addresses the latest review round on #6561.

The form-validation error and the goal-list load error were painted but never
announced: `role="alert"` puts them in a live region, so a screen-reader user
learns the submit was rejected instead of believing the goal was created, and
learns the list went stale on a poll that failed after the page was already up.
Matches the existing pattern in RewindDialog.

`.primaryButton` / `.secondaryButton` had no `:focus-visible` rule, so keyboard
users tabbing to Set goal / Cancel saw no focus indicator — an inconsistency
with `.iconAction` and `.sessionLink` in the same file. They now take the ring
the form controls already use (`outline: 2px solid var(--primary)`), offset
outwards rather than inset: `.primaryButton` is filled with `--primary`, so an
inset ring in that colour would be invisible on it.

* fix(cli): stop a broken stderr from abandoning a transcript replay

Addresses the latest review round on #6561.

`process.stderr.write` throws on EPIPE or a closed fd — reachable whenever the
reader goes away (`qwen … | head`) or a daemon redirects its stderr. The goal
path writes diagnostics from inside work that must not be destroyed by a failed
diagnostic, and `bee3295aa` only guarded one of the five sites.

The worst of the rest was in `HistoryReplayer`: the "skipping a goal card whose
condition is empty" line sits inside the loop over a record's cards. A throw
there abandoned that record's remaining cards, propagated to the record loop,
and aborted the whole replay — the user lost their transcript because we failed
to complain about one bad card.

Add `writeStderrLineSafe` to stdioHelpers and route the goal path's five sites
through it, replacing the one-off `#warnGoalRestore` wrapper in acpAgent so
there is a single implementation. It is deliberately not the default:
`writeStderrLine` still throws, because most of the CLI wants a broken stderr to
be loud. This variant is for writes that are incidental to real work.

Also adds the first tests for `stdioHelpers`, and covers two untested Goals
dialog behaviours: the Refresh button, and the clear button disabling itself
while its clear is in flight (a double-click otherwise fired two concurrent
clears at the same session).

* fix(web-shell): keep the Goals page mounted across createNewSession

main's `createNewSession` gained a `setMainView('chat')` of its own, fired
synchronously before any await. That silently defeated the Goals handler's
deferred switch: by the time `sendPrompt` rejected, the page — and the form
that renders the error — was already gone, dropping the user into an empty
chat with no explanation. This is the exact failure the deferred switch was
written to prevent; the two changes only had to meet for it to come back.

`createNewSession` takes a `keepView` opt-out, and the Goals handler uses it,
so the page survives until the prompt is admitted. Saving and restoring
`mainView` around the call would also work but flips the view to chat and back,
which the user would see. A test pins the page staying mounted across a failed
submit; it fails if `keepView` stops being honoured.

Also from the same round:

- `registerGoalHook`'s `initialSetAt` guards are now tested — a future
  timestamp, NaN, Infinity, 0 and a negative all fall back to now, and a usable
  value survives. The future case is the one with teeth: `Date.now() - setAt`
  renders a negative elapsed time rather than failing loudly, and nothing
  covered it.
- The goals list carries `role="list"` / `role="listitem"`. They are divs, and
  even a real `<ul>` loses its implicit role under `display: flex` in Safari.
- The open-session button names the action *and* the session. Its visible text
  is only the session name, which says nothing about what activating it does;
  the name stays in the accessible name so it still contains the visible label.
- `.fieldLabel` matches ScheduledTasksDialog's `--muted-foreground`. The two
  dialogs sit side by side and had drifted.

Not taken: deferring `setMainView` in `onOpenSession` until the load resolves.
The sibling `handleOpenSessionFromOverview` switches first by the same pattern,
and `loadSidebarSession` clears the transcript and shows a loading skeleton —
which is the feedback for the common success path. Deferring would leave a
click looking dead until the load lands, and would make Goals diverge from the
Session Overview panel. If we want that behaviour it should change both.

* fix(web-shell): stop the visuals spec asserting a badge #7035 removed

The "Capture web-shell visuals" job fails on this PR at
`screenshots.spec.ts:395`, asserting the sidebar's "Primary" badge is visible:

    Error: expect(locator).toBeVisible() failed
    Error: element(s) not found

Not from this branch. The chain is on main:

- 2026-07-15  #6880 adds the visuals spec, asserting the "Primary" badge —
  correct at the time.
- 2026-07-17  #7035 drops that badge as redundant (the workspace selector's
  checkmark already conveys the default target), removing the `primaryLabel`
  prop and its `<span className={styles.badge}>` render, and updates the *unit*
  test to assert its absence — but leaves this spec asserting it is visible.

The capture job only runs on pull requests (it needs a PR head and a
merge-base), so main never went red for it and the breakage surfaces on the
next PR to merge main — this one.

Assert the badge's absence instead of deleting the check, mirroring the unit
test #7035 added, so a regression re-adding it still fails here.

---------

Co-authored-by: 易良 <1204183885@qq.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-07-18 08:52:07 +00:00
Nothing Chan
620effef09
feat(web-shell): add directory autocomplete to the Add Workspace dialog (#7125)
Typing the full absolute path of a project by hand into the Add
Workspace dialog was slow and error-prone, and the only feedback was a
generic error after submitting. The existing GET /list route could not
back an autocomplete here because it resolves paths through a
registered workspace's filesystem boundary, and the path being picked
is not a workspace yet.

Add a deliberately narrow read-only daemon route,
GET /workspace-path-suggestions?prefix=<absolute>, that returns only
the names of subdirectories matching the prefix (case-insensitive on
the final segment, dot-directories only once the filter starts with a
dot, symlinked directories included, capped at 50 entries). It shares
the trust surface of POST /workspaces, which already lets an
authenticated client stat and register any absolute directory.

The dialog's path field becomes a combobox fed by that route through
DaemonClient.workspacePathSuggestions() and a new
suggestWorkspacePaths workspace action: suggestions render in a
listbox under the input (debounced 150ms, stale responses dropped),
ArrowUp/Down move the highlight, Enter/Tab or click accepts a
directory and descends into it, and Escape closes just the list —
intercepted on window capture so Radix does not close the whole
dialog.

Fixes #7102

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 22:25:22 +00:00
Nothing Chan
488a86b209
feat(web-shell): toggle the session sidebar with Cmd+B / Ctrl+B (#7135)
The #5074 sidebar request is largely implemented (session list, search,
rename, delete, collapse persistence), but the keyboard shortcut item
was still missing: there was no way to toggle the sidebar without
reaching for the mouse.

Add the editor-convention binding: Cmd+B (macOS) / Ctrl+B collapses and
expands the sidebar, persisting the preference through the existing
writeSidebarCollapsed path. Phone-width layouts render the sidebar as a
drawer, so the shortcut toggles the drawer there instead. Shift/Alt
variants and the ambiguous Cmd+Ctrl combination are left untouched for
the browser and other bindings, and the matcher lives in a small pure
module with its own tests.

Refs #5074

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 22:11:48 +00:00
Shaojin Wen
9e95505551
refactor(web-shell): drop redundant primary-workspace label (#7035)
* refactor(web-shell): drop redundant primary-workspace label

The workspace selector in the composer already marks the default target
with its own checkmark, so appending "· Primary" to the primary entry's
name carried no extra information. Remove that tag everywhere it showed:

- composer selector: trigger, tooltip, and dropdown list
- sidebar workspace header badge (also lets the name show untruncated)
- session overview / split-view picker badges — the primary now shows
  its folder basename, consistent with the other workspaces
- scheduled-tasks dialog workspace labels

Delete the now-unused i18n keys (sidebar.workspacePrimary,
scheduledTasks.workspacePrimaryTag; en + zh) and update the two tests
that asserted the old tag.

* refactor(web-shell): reuse workspaceBasename + cover primary-badge removal

Address /review suggestions on the primary-workspace-label cleanup:

- ScheduledTasksDialog's local workspaceLabel() is now functionally identical to the shared workspaceBasename() util (both return the cwd's last path segment), so reuse the util and delete the duplicate.
- Add a WebShellSidebar test asserting the primary workspace header no longer renders a "Primary" badge, so a regression re-adding it fails.

* test(web-shell): assert SplitView primary picker item has no "Primary" tag

Covers the fourth /review suggestion (terminal-only): the multi-workspace picker test now asserts primary-workspace sessions render their basename, not the removed "Primary" label.

* test(web-shell): assert scheduled-tasks picker option text drops (primary)

Covers the re-review suggestion: the workspace <select> picker options were checked for count and value but not visible text, so a regression re-adding a "(primary)" suffix to the primary option would pass undetected. Assert the option labels are the bare basenames.

---------

Co-authored-by: wenshao <wenshao@example.com>
2026-07-17 00:30:51 +00:00
jinye
660ae9f712
feat(web-shell): add archived session export (#6910)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-07-16 17:38:43 +00:00
ytahdn
bd87dcb5ce
fix(web-shell): filter sessions by source (#6995)
Co-authored-by: ytahdn <ytahdn@gmail.com>
2026-07-16 04:56:50 +00:00
Shaojin Wen
49497f5076
feat(web-shell): color-code each split pane by workspace (#6971)
* feat(web-shell): color-code each split pane by workspace

On a narrow split (split-screen / mobile), it was hard to tell which
workspace each pane belonged to: a pane's header showed only its session
name, and the sole workspace signal — the composer chip at the bottom —
collapsed to a bare folder icon that looked identical for every
workspace, so the workspace was discoverable only by hovering each one.

Surface the workspace where you actually scan — the pane header — and
give each workspace a stable accent color so panes read apart at a
glance and same-workspace panes read as a group:

- Add a colored workspace tag (dot + basename) at the start of each
  pane header on a multi-workspace daemon, and colorize the header
  divider with the same accent. The dot never shrinks, so panes stay
  distinguishable even when the name and session title ellipsize.
- Derive a stable per-workspace color from the workspace's position in
  the daemon's advertised workspaces[], reusing the sidebar
  session-group palette so the two surfaces speak the same color
  language. Extracted into a shared workspaceAccent.module.css.
- Tint the composer workspace chip with the same accent (folder + faint
  background) so it stays distinguishable even in its icon-only compact
  state, instead of a generic folder.

Single-workspace daemons are unchanged: no tag, and the header divider
falls back to the neutral border.

* refactor(web-shell): address review on split-pane workspace accent

- Rename workspaceAccent.module.css -> WorkspaceAccent.module.css to match the
  PascalCase convention used by every other component .module.css; update both
  import sites.
- Hoist the four raw-hex accent colors (red/orange/yellow/green) into shared
  --accent-* theme tokens in App.module.css, and point the workspace accent
  module, the sidebar group dots, and the overview badges at them. The palette
  now has a single source of truth and can't drift between the four surfaces
  (values are unchanged, so rendering is identical).
- Add a compile-time exhaustiveness guard so adding a
  DaemonSessionGroupPresetColor without extending WORKSPACE_ACCENT_COLORS (and
  its CSS class) fails the build instead of silently dropping that accent.
- Give the pane-header workspace tag role="img" so its "Workspace: <name>"
  aria-label is reliably announced; aria-label on a bare span (generic role)
  is not.

* refactor(web-shell): address follow-up review on workspace accent

- Hoist the four --accent-* tokens out of both theme blocks into the
  theme-independent .app scope, so they are declared once (the values do not
  vary by theme) — a genuine single declaration rather than two kept in sync.
- Add a dev-only runtime check that every accent color has a matching class in
  WorkspaceAccent.module.css, closing the gap the compile-time guard cannot
  cover: CSS modules are typed Record<string, string>, so a renamed/removed
  class would otherwise silently drop that color's accent.
- Rename the "same workspace same color" test to describe what it actually
  asserts (a stable color per cwd, and distinct colors across workspaces).

* refactor(web-shell): address second follow-up review on workspace accent

- WorkspaceIndicator tests: assert on imported CSS-module class names instead of
  string literals, so a CSS-module naming change can't silently make the
  substring checks vacuous; add an expanded-mode (non-compact) accent test so a
  refactor that gated the accent on `compact` would be caught.
- workspaceColor.ts: run the CSS-class contract check unconditionally — throw in
  dev, but console.error in production — so a missing class in a prod build is
  at least diagnosable instead of a silent accent drop.
- WorkspaceAccent.module.css: correct the docstring to state exactly which
  tokens come from where — red/orange/yellow/green from --accent-* in
  App.module.css, blue/purple deliberately reusing the --agent-* brand tokens.

---------

Co-authored-by: wenshao <wenshao@example.com>
2026-07-16 02:38:58 +00:00
ytahdn
4bc31cb608
feat(serve): add workspace MCP management (#6954)
* feat(serve): add workspace MCP management

* fix(serve): refine workspace MCP management

* fix(web-shell): align MCP action expectation

* fix(serve): address MCP review findings

---------

Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.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>
2026-07-16 02:24:07 +00:00
ytahdn
03e796ec9b
feat(web-shell): show sessions awaiting user action (#6956)
* feat(web-shell): show sessions awaiting user action

* test(web-shell): cover question count fallback

* fix(web-shell): clarify pending input state

---------

Co-authored-by: ytahdn <ytahdn@gmail.com>
2026-07-15 14:40:10 +00:00
jinye
ca5019968a
fix(web-shell): harden non-primary archive actions (#6912)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-07-15 04:57:07 +00:00
samuelhsin
c8290f8e49
fix(web-shell): persist collapsed session group sections across reload (#6878)
* fix(web-shell): persist collapsed session group sections across reload

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

Fixes QwenLM/qwen-code#6870

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-07-15 00:25:54 +00:00
ytahdn
b59b341a0a
feat(web-shell): add extension management page (#6815)
* feat(daemon): support interactive extension installs

* feat(web-shell): add extension management page

* fix(web-shell): align extension update behavior

* fix(web-shell): polish extension management UI

* fix(extensions): harden interactive operations

* fix(web-shell): address extension review suggestions

* fix(web-shell): refine extension interaction handling

* fix(web-shell): resolve extension operation races

* fix(web-shell): harden extension action admission

* fix(web-shell): surface extension recovery failures

* fix(web-shell): preserve extension card titles

* fix(web-shell): refine extension card layout

* fix(extensions): address operation review findings

* test(extensions): close remaining review gaps

---------

Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
Co-authored-by: ytahdn <ytahdn@gmail.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>
2026-07-14 08:31:13 +00:00
ytahdn
0c6212c0b0
feat(web-shell): add workspace path lock (#6853)
Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
2026-07-14 06:24:52 +00:00
jinye
c7250df8ea
feat(serve): Add workspace-qualified Voice (#6839)
* feat(serve): add workspace-qualified voice

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

* fix(serve): harden workspace voice lifecycle

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

* codex: address PR review feedback (#6839)

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

* codex: address PR review feedback (#6839)

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

* test(cli): address workspace Voice review feedback

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

* codex: address PR review feedback (#6839)

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

* fix(cli): clean up Voice lifecycle resources

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

* fix(cli): address Voice review feedback

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-07-14 03:42:58 +00:00
jinye
13c224f5e9
feat(serve): support runtime workspace removal (#6745)
* feat(serve): support runtime workspace removal

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

* fix(cli): address workspace removal review feedback

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

* test(cli): strengthen workspace removal regressions

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

* test(webui): fix timeout assertion lint

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

* fix(cli): address workspace removal review feedback

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

* fix(cli): update workspace Git test registry

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

* fix(daemon): address workspace removal review feedback

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

* codex: address PR review feedback (#6745)

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

* codex: address PR review feedback (#6745)

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

* codex: address PR review feedback (#6745)

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

* codex: address PR review feedback (#6745)

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

* codex: address PR review feedback (#6745)

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

* codex: address PR review feedback (#6745)

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

* codex: address PR review feedback (#6745)

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

* codex: address PR review feedback (#6745)

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

* codex: address PR review feedback (#6745)

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

* codex: fix CI failure on PR #6745

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

* codex: address PR review feedback (#6745)

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

* codex: address PR review feedback (#6745)

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

* test(web-shell): cover workspace removal after sidebar rebase

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

* codex: address PR review feedback (#6745)

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-07-13 15:43:38 +00:00
ytahdn
79ae054bb8
feat(web-shell): modernize multi-workspace sidebar (#6804)
* feat(web-shell): modernize multi-workspace sidebar

* fix(web-shell): address sidebar review feedback

* fix(web-shell): address remaining review feedback

---------

Co-authored-by: ytahdn <ytahdn@gmail.com>
2026-07-13 11:45:20 +00:00
samuelhsin
7468e75e3d
feat(web-shell): support custom Hex session group colors (#6752)
* feat(web-shell): support custom Hex session group colors

* docs(web-shell): add custom group color screenshot

* fix(web-shell): address custom Hex color review feedback

* fix(web-shell): validate group presets against daemon catalog and auto-prefix Hex input

Review follow-ups for the custom Hex group color editor:

- Validate the preset branch against the daemon-provided color catalog
  instead of the hardcoded palette, so future preset additions stay
  selectable in the editor.
- Auto-prefix bare values with '#' in the Hex field so pasted bare Hex
  validates, and free text can no longer collide with a preset name and
  silently flip the select out of Custom mode.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(web-shell): cap custom Hex input length

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 10:41:17 +00:00
dreamWB
077bf2b304
feat(web-shell): make session sidebar configurable (#6750)
Some checks failed
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
SDK Python / Classify PR (push) Has been cancelled
SDK Python / SDK Python (3.10) (push) Has been cancelled
SDK Python / SDK Python (3.11) (push) Has been cancelled
SDK Python / SDK Python (3.12) (push) Has been cancelled
* feat(web-shell): make session sidebar configurable

* test(web-shell): cover sidebar host controls

* fix(web-shell): improve sidebar menu accessibility

* fix(web-shell): restore sidebar menu focus
2026-07-12 07:53:27 +00:00
jinye
51d4ce48db
feat(serve): persist dynamic workspace registrations (#6716)
* feat(serve): persist dynamic workspace registrations

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

* codex: address PR review feedback (#6716)

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

* codex: address PR review feedback (#6716)

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

* codex: address PR review feedback (#6716)

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

* codex: address PR review feedback (#6716)

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

* codex: address PR review feedback (#6716)

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-07-11 16:49:40 +00:00
jinye
e403246dc2
feat(serve): Expose read-only untrusted session catalogs (#6717)
* feat(serve): expose read-only untrusted session catalogs

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

* refactor(cli): address session catalog review feedback

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

* codex: address PR review feedback (#6717)

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

* codex: address PR review feedback (#6717)

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

* codex: address PR review feedback (#6717)

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-07-11 15:28:41 +00:00
Shaojin Wen
732d85df39
fix(web-shell): correct Add Workspace dialog theming and multi-workspace session rows (#6705)
* fix(web-shell): theme and lay out the Add Workspace dialog correctly

The dialog's stylesheet referenced CSS variables that are defined nowhere
(--text-secondary, --input-bg, --border-color, --accent-color, --hover-bg), so
they fell back to hardcoded dark values — in light mode the input rendered
dark-on-light with a purple focus ring, inconsistent with every other dialog. It
also reused a two-column form-row grid meant for label/value pairs, which cramped
the path input into a narrow column.

Rebuild the dialog on the shared dialog primitives (themed .dialog-form input,
dialog-inline-button, dialog-primary-button) so it tracks the active theme in
both light and dark and gives the path a full-width input. Add a proactive
absolute-path hint, an accessible inline error (role="alert", aria-describedby,
aria-invalid) that clears as you type, and a localized "Adding…" state. Keep the
hint and error as siblings of the input rather than nested in the label, so their
text stays out of the input's accessible name.

* fix(web-shell): render full session rows for every workspace in the sidebar

Registering a second workspace switched the sidebar to the multi-workspace view,
which rendered each workspace's sessions with a bespoke minimal row: a smaller
font (12px vs 14px) and no per-session actions. The rich single-workspace row
(hover actions, current-session highlight, inline rename, running/unread state)
was hidden entirely, so even the primary workspace's list degraded.

Render sessions through the sidebar's shared renderSessionRow so every workspace
matches the single-workspace list. Gate the mutation actions to the primary
(daemon-bound) workspace: the daemon can't resolve another workspace's session
for pin/archive/export/delete (they 404 or silently no-op), so non-primary rows
are read-only — they keep click-to-load and the font/highlight but drop the
action buttons. Session mutations now also bump the per-workspace poll token so
those lists refresh promptly.

* refactor(web-shell): move Add Workspace footer padding into a CSS class

The footer sits inside .dialog-form (which already pads its sides) and reuses the
shared dialog-footer-actions primitive, doubling the horizontal padding; the
override lived as an inline style, hiding the deviation from the stylesheet. Move
it to a local .footer class applied alongside the shared class. A doubled
selector keeps it winning over the primitive regardless of stylesheet order.

* test(web-shell): cover workspace read-only gating and Add Workspace dialog

Add tests for the behaviors introduced by the sidebar and dialog fixes:
- non-primary workspace rows render the session but expose no action buttons
  (the daemon, bound to the primary workspace, can't service their mutations),
  while the primary workspace keeps its full actions;
- a session mutation re-polls the per-workspace list instead of waiting for the
  10s interval;
- the Add Workspace dialog's absolute-path validation, accessible error wiring
  (role="alert", aria-describedby/aria-invalid), error-clear-on-edit, trimmed
  submit, and onAdd-failure handling.

* refactor(web-shell): centralize the workspace reload-token bump in a helper

Per review: the setWorkspaceSessionsReloadToken bump was repeated verbatim across
the session-mutation handlers. Extract a stable bumpWorkspaceReload() helper and
route every site through it, including assignSessionGroup and assignSessionColor
— the two organization mutations that were missing the bump — so assigning a
group or color now also re-polls the per-workspace session lists instead of
waiting for the 10s interval.

* test(web-shell): cover Windows paths and non-Error rejections in Add Workspace dialog

Per review: add the two untested handleSubmit branches — a Windows-style absolute
path accepted by the drive-letter regex, and a non-Error onAdd rejection falling
back to the generic error message.

* fix(web-shell): gate inline rename on readOnly and refresh on group-create assign

Per review: the readOnly option hid the hover action buttons but not the inline
rename — onDoubleClick and the isEditing branch still fired on read-only rows, so
a session shown in multiple workspaces could render an editable rename input on
its non-primary (read-only) copy. Gate both on !readOnly. Also add the missing
bumpWorkspaceReload() to saveGroupEditor's create-with-target-session path so
that organization mutation refreshes the per-workspace lists like the others.

* test(web-shell): cover readOnly rename gating and dialog submitting state

Per review: assert the read-only (non-primary) row does not open the inline
rename form when the shared session is renamed from the primary row, and that the
Add Workspace dialog shows the localized "Adding…" label with disabled controls
while onAdd is pending.
2026-07-11 11:19:32 +00:00
jinye
2523a36b52
feat(web-shell): workspace management sidebar with dynamic registration (daemon multi-workspace phase 4) (#6625)
* feat(web-shell): add workspace picker for new sessions (issue #6378 phase 4)

Multi-workspace daemons now show a new-session workspace picker in the sidebar (default primary, untrusted disabled); the chosen workspace cwd is sent on POST /session so the session spawns in that workspace. daemon-react-sdk createSession gains an optional per-call workspaceCwd override covering both the detached and active-session paths; omitting it preserves the previous primary behavior.

* feat(web-shell): workspace management with dynamic registration

Replace the new-session workspace picker with a full workspace
management sidebar. Registered workspaces render as a parallel,
collapsible list (folder icon per workspace), each with its own
sessions nested underneath, and a "+" entry registers an existing
directory as a new workspace at runtime with no daemon restart.

Backend: WorkspaceRegistry becomes mutable (add()/onChange()); a new
POST /workspaces route validates the directory (exists, not a
duplicate, not nested) and registers it; run-qwen-serve exposes a
runtime factory that builds a complete workspace runtime (bridge, fs
factory, channel factory, workspace service) on demand. The SDK
DaemonClient and daemon-react-sdk gain addWorkspace().

* fix(web-shell): show newly registered workspace without a reload

Registering a workspace via the sidebar "+" left the list unchanged
until a full page reload. handleAddWorkspace called
workspace.getCapabilities(), which returns a cached promise and only
feeds setCapabilities from the mount effect, so the refresh was a no-op.

Add DaemonWorkspaceProvider.refreshCapabilities(): it bypasses the
promise cache, issues a fresh /capabilities fetch, and pushes the
result into state so consumers re-render. handleAddWorkspace now awaits
it (best-effort, so a refresh failure never masks a successful
registration).

* fix(web-shell): address review feedback for workspace management

- registry: list() returns a frozen snapshot so callers can't mutate the
  internal runtimes array (restores the push()-throws invariant)
- POST /workspaces: reject relative paths on the raw input, canonicalize
  via realpath so symlink aliases can't bypass the duplicate/nesting
  checks, and serialize concurrent registrations to close a TOCTOU race
  that leaked bridge/channel infrastructure
- sidebar: restore a compact single-workspace project header (name,
  search toggle, collapse) so single-workspace users keep those
  affordances and searchOpen/projectExpanded are no longer dead
- daemon session: include the target workspace in the create-session
  failure message
- tests: rework WebShellSidebar tests for the WorkspaceSection UI (add
  the useWorkspace mock, query workspace buttons, cover primary->undefined),
  use the canonical DaemonWorkspaceCapability type, and add a createSession
  workspaceCwd forwarding test

* fix(cli): harden dynamic workspace registration per review

- POST /workspaces: bound cwd by MAX_WORKSPACE_PATH_LENGTH before any
  filesystem work, and return a generic 500 (log the full error to
  stderr) so responses can't leak internal filesystem paths
- createDynamicWorkspaceRuntime: log a stderr warning when a workspace's
  settings can't be read, matching the startup secondary-workspace path

* qwen: address PR review feedback (#6625)

Dynamic workspace reloadDaemonEnv now mirrors the startup secondary path:
after reloadEnvironment() it rebuilds the runtime env via
buildRuntimeEnvironment(), calls wsEnv.replace(), and updates the env
metadata (envFileReadFailed / envFileReadFailures / overlayKeys /
envFilePaths). Without this, .env changes on a dynamically registered
workspace never propagated to that workspace's spawned child processes.

* qwen: address PR review feedback (#6625)

Harden POST /workspaces and the workspace registry per review:
- canonicalize with realpathSync.native (matches startup) so the same
  physical dir on a case-insensitive FS can't register twice
- nesting guard now also checks in-flight registrations, closing a
  concurrent parent/child registration race
- error responses no longer echo resolved/other-workspace paths
- registry add() isolates onChange listener throws so a bad listener
  can't abort a caller after the workspace is already committed

* qwen: address PR review feedback (#6625)

- POST /workspaces: cap total registered workspaces (startup + dynamic)
  to guard against unbounded registration exhausting resources
- createDynamicWorkspaceRuntime: register shutdown-cleanup arrays only
  after the runtime is fully built, so a throw during workspace-service
  construction can't orphan the bridge/channel
- web-shell App: reset selectedWorkspaceCwd after session creation so the
  workspace picker is one-shot (next new chat defaults to primary)

* qwen: address human review suggestions (batch 1)

- WorkspaceSection: add console.warn on session-poll failure (was silent)
- WorkspaceSection: add aria-expanded for screen readers
- AddWorkspaceDialog: associate label/input (htmlFor/id), i18n the
  absolute-path error, accept Windows drive-letter paths
- i18n: remove unused workspaceUntrustedHint key, add addWorkspaceAbsError

* qwen: address human review suggestions (batch 2)

- Remove dead CSS (.workspacePickerSelect, .workspaceItem* classes from
  the old select-based picker, replaced by WorkspaceSection)
- Add title tooltip to single-workspace project name (shows full path)
- WorkspaceSection: sync expanded state on workspace.primary change

* qwen: address human review suggestions (batch 3)

- DaemonWorkspaceProvider: refreshCapabilities now clears error on
  success and sets error+status on failure (was incomplete vs mount)
- Remove unused onChange/WorkspaceRegistryEvent from workspace registry
  per simplicity-first (no consumer exists; defers API surface until
  a real subscriber like SSE push is needed)

* qwen: add workspace-management route test coverage

Tests cover: 501 (no factory), 400 (missing/empty/relative/long/
nonexistent cwd), 409 (duplicate canonical path), 201 (success),
and verifying error messages are generic (no path leak).

* qwen: fix CI build failure — add explicit types in route test

The CLI's tsconfig includes test files in tsc --build, so all
noImplicitAny violations in tests cause build failures. Add explicit
type annotations to mock parameters.

* qwen: add type/title to single-workspace add-button

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-07-10 16:30:20 +00:00
callmeYe
727c2d580c
fix(web-shell): prevent sidebar footer overflow (#6522) 2026-07-08 08:28:12 +00:00
Shaojin Wen
045bbee6ce
fix(web-shell): hide sidebar settings text when width is insufficient (#6494)
Prevent the 'Settings' label from wrapping to a new line when the
sidebar is narrow. Instead, the text is clipped via overflow:hidden
and only the gear icon remains visible.
2026-07-08 02:41:41 +00:00
Shaojin Wen
f7296d0333
feat(web-shell): add Qwen logo beside the sidebar new-chat button (#6437)
Place the Qwen brand mark to the left of the sidebar's New chat button.
The artwork is the same SVG used for the browser-tab favicon (and the
QwenLM GitHub avatar), inlined rather than hot-linked because the Web
Shell CSP is `img-src 'self' data: blob:`, which blocks remote images.
When the sidebar is collapsed there is no room beside the compact
button, so the mark is hidden and only the New chat button remains.
2026-07-07 08:36:48 +00:00
Shaojin Wen
f41e95ac18
feat(web-shell): add Session Overview panel and in-window split view (#6400)
* feat(web-shell): add Session Overview panel and in-window split view

Add a large-screen "Session Overview" mission-control panel and an
in-window split view so users can monitor and drive multiple daemon
sessions at once.

- SessionOverviewPanel: ranked live cards (needs-approval -> running ->
  idle) merging the workspace session list with the detail=full status
  report. Multi-select opens the selected sessions as a split view in
  the current tab ("Open in split") or in a new browser tab ("Open in
  new tab", via a ?split=a,b URL).
- SplitView + ChatPane: one DaemonWorkspaceProvider hosting N
  DaemonSessionProvider panes, each a self-contained interactive chat
  (transcript, composer, streaming, tool/ask approvals). Browser focus
  scopes the keyboard per pane, so panes never contend over approvals.
- Sidebar entry points gated to large screens; the split view's Back
  returns to the Session Overview.

* refactor(web-shell): address review feedback on the session overview / split view

- SessionOverviewPanel: prune the selection Set when a session leaves the list
  (so a reappearing session isn't silently reselected) and make select-all use
  the intersection rather than prev.size.
- Extract isAskUserPermission into a shared util so App.tsx and ChatPane.tsx no
  longer keep verbatim copies that can drift.
- SplitView: dismiss the "add session" picker on Escape or a click outside it.
- Tests: MAX_PANES cap, popup-blocked path, checkbox-selects-without-navigating,
  stale-selection pruning, and a direct test for the extracted util.

* fix(web-shell): address /review findings on the split view

- ToolApproval: add a `keyboardActive` prop; split panes pass false so global
  Enter/Escape/digit shortcuts can't confirm the wrong session's approval, and
  the outer session's approval overlay is no longer rendered behind the split
  (where it would keep its global shortcuts while hidden).
- ChatPane: defer the composer commit until sendPrompt resolves, so a rejected
  prompt (transcript loading / disconnected / turn active) preserves the draft
  instead of silently dropping it.
- SplitView: include a per-mount nonce in each pane's clientId so two tabs
  opening the same split don't share a client id — which suppressOwnUserEcho
  would treat as a self-echo and drop from the transcript.
- SessionOverviewPanel: cap the split selection to MAX_SPLIT_PANES before
  building the ?split= URL or opening the in-window split, with a hint when more
  are selected; also dismiss the split picker on Escape / click-outside.
- Tests covering each.

* fix(web-shell): address second /review round on the split view

- SplitView: wrap each pane in its own ErrorBoundary, so a render crash in one
  pane (malformed block, unexpected tool shape) shows an inline fallback with a
  close action instead of white-screening the whole split.
- splitUrl / overview: carry the daemon token into the new-tab split URL's
  fragment. The current tab has already stripped the token from its URL, so a
  token-auth (`serve --open`) deployment would otherwise open the split tab
  unauthenticated. The token rides the hash (never sent to the server / logs).
- Tests: per-pane error isolation, token-in-fragment (and none without a token),
  and the overview polling effects (interval fires, document.hidden skips, and
  the in-flight guard prevents overlapping polls).

* fix(web-shell): hide the outer chat under the split and share app-level contexts

- App: hide (display:none) + aria-hide the outer chat subtree whenever
  mainView !== 'chat', not only when a panel is open. Previously the outer
  chat/composer/toolbar stayed reachable by keyboard/AT behind the full-page
  split (it was only covered visually). State is preserved (node stays mounted).
- App: wrap SplitView in the app-level WebShellCustomizationProvider and
  CompactModeContext so split panes render markdown / tool-headers / thinking
  the same way the single-session chat does. Todo contexts stay chat-only —
  they belong to the outer session, not the panes.

* refactor(web-shell): address review suggestions — coverage, dedup, split UX

- ToolApproval: add a dedicated test on the real component that the global
  keyboard shortcut is armed by default and NOT armed when keyboardActive=false
  (the cross-pane approval safety mechanism).
- SplitView: auto-exit to the Session Overview when the last pane is closed
  (guarded so an initial empty seed doesn't bounce straight back out).
- ChatPane: add tests for the cancel action, the empty/whitespace submit guard,
  and error routing to the onError prop.
- Extract the shared session-list page size + organization feature flag into
  constants/sessions.ts, used by the overview, split view, and sidebar, so the
  values can't drift between the three.

* fix(web-shell): surface outer approval + failed refresh in overview/split

- Split view: when the outer (main) session is waiting on an approval
  that's hidden behind the split, show a non-blocking notice banner with
  a "Go to it" button that returns to the chat where the approval lives.
- Auto-close the split (like the overview panel) when the viewport shrinks
  below the large-screen breakpoint, so users aren't stranded.
- Session Overview: surface a failed refresh inline (keeping the last-good
  cards) instead of silently swallowing it once cards are on screen.
- Tests: status-report poll cadence, picker dismiss (Escape / outside /
  inside click), inline refresh-failure banner.

* fix(web-shell): sever window.opener on split tab; tighten hidden-chat test

- openSelectedInNewTab now clears win.opener (the split tab carries a
  daemon token in its URL fragment) to prevent reverse tabnabbing, matching
  the existing bug-report window.open path.
- Strengthen the split-view App test so a missing outer-chat subtree fails
  instead of passing vacuously through an optional chain.

* fix(web-shell): split-view focus/stability/robustness follow-ups

- Refocus the composer after a shrink-driven split close so keyboard users
  aren't dropped onto <body> (skips when an approval or panel takes over).
- Stabilize SplitView onExit via useCallback so its last-pane-close effect
  doesn't re-fire on every App re-render.
- ChatPane: surface a per-pane connection-loss banner instead of silently
  showing stale messages when a pane's daemon connection drops.
- ChatPane: anchor the streaming timer to the active turn's start (last user
  message timestamp) so a pane opened mid-turn shows real elapsed time.
- Tests: split auto-close on shrink, outer-approval split notice + return-to-
  chat, connection banner, and streaming-timer anchoring.
2026-07-07 02:44:18 +00:00
Shaojin Wen
9a63c03224
feat(web-shell): add a Scheduled Tasks management page (#6348)
* feat(web-shell): add scheduled tasks management page

Add a "Scheduled tasks" page to the Web Shell for managing durable cron tasks against the current workspace.

- Sidebar entry opens a full-pane page (replaces the chat area, not a modal) listing tasks with enable/disable toggle, delete, run-now, and human-readable schedules.
- "New scheduled task" opens a modal with a schedule builder (daily / weekdays / weekly / hourly / every-N-minutes / custom cron) and a live preview.
- "Create via chat" returns to the chat and primes the composer so the agent creates the task through its cron_create tool.
- Daemon CRUD routes (GET/POST/PATCH/DELETE /scheduled-tasks) read/write the existing per-project scheduled_tasks.json; task firing stays with the session-side scheduler.
- Extend DurableCronTask with optional name/enabled (backward compatible); the scheduler skips tasks with enabled:false.
- Add /scheduled-tasks to the vite dev-server proxy allowlist so the page works under npm run dev:daemon.

* chore(web-shell): address review feedback on scheduled tasks

- cron_list: surface name/enabled so the agent can tell a disabled durable task from an active one (a disabled task no longer looks identical to an active one).
- core: export only the tasks-file functions the daemon route actually uses (drop unused addCronTask / getCronFilePath / CRON_TASKS_DISPLAY_PATH from the public barrel).
- CronScheduler: warn when a durable reload fails and the prior view is kept, since a just-disabled or -deleted task can keep firing until the next successful reload.
- Extract the schedule helpers (buildCron / describeCron / parseHhmm / describeLastRun) into a pure module and add unit tests for them.
- Add route tests for PATCH cron/prompt/recurring, empty-patch rejection, and POST field-length / boolean-type validation.

* chore(web-shell): address second review round on scheduled tasks

- Log CRUD errors server-side (writeStderrLine) in each route catch block, matching the other daemon routes.
- Share one id generator (generateCronTaskId in cronTasksFile) between the scheduler and the daemon route instead of duplicating it.
- describeCron: recognize cron day-of-week 7 as an alternate notation for Sunday.
- Reset the builder time to :00 when switching to the hourly frequency (its time picker is hidden, so it no longer silently carries the daily minute).
- Tests: cron_list name/disabled output; route Feb-30 impossible-cron and corrupt-file 500 read-failure; describeCron dow=7.

* chore(web-shell): address third review round on scheduled tasks

- Run now: report sendPrompt rejections via the toast/error path instead of dropping the promise.
- Block chat interaction while the full-pane Scheduled Tasks view is open, so the covered composer can't receive keystrokes/Escape.
- Guard reload() with a request-sequence id so a slow load can't overwrite a newer list after a mutation.
- Re-enabling a task that had genuinely fired resumes from now instead of catching up work paused while it was disabled.
- Restrict "every N minutes" to divisors of 60 (a non-divisor */N fires more often than the label claims).
- Show a Repeats / Runs once label on each card so tool-created one-shots aren't mistaken for repeating schedules.
- Return generic 500 client messages (no internal file path); the detail is logged server-side.
- Tests: SDK scheduled-task methods (method/URL/id-encoding/headers/errors); route re-enable behavior both ways.

* chore(web-shell): address fourth review round (minor suggestions)

- Route error logs interpolate the actual task id instead of the literal ":id".
- cron_list returnDisplay includes the task name (matching llmContent) so terminal /cron list shows UI-assigned names.
- Truncate the delete-confirm label so an unnamed task's long prompt doesn't blow up the confirm() dialog.
- Cap the create-form prompt textarea at MAX_PROMPT_LENGTH and drop the dead typeof-window guard.
- Test generateCronTaskId (format + near-uniqueness).

* chore(web-shell): address fifth review round on scheduled tasks

- Re-enable now resumes any recurring task from now (stamp on every false→true), not only ones that had already fired — a task disabled before its first run no longer catch-up-fires the slot it was paused through.
- describeCron applies the same divisor-of-60 check as buildCron, so a hand-edited/persisted */45 falls back to the raw expression instead of a misleading "every 45 minutes".
- Strengthen the corrupt-file route test to assert the generic client message and no leaked file path.
- Tests: recurring-disabled-before-first-run and one-shot re-enable; describeCron non-divisor fallback.

* test(cli): cover legacy scheduled-task normalization on GET

Seed a pre-fields task (no name/enabled) directly to disk and assert the GET response normalizes it to name:null / enabled:true, guarding backward compatibility with existing scheduled_tasks.json files.

* fix(core): cap durable cron loads against a durable-only budget

The daemon route accepts up to MAX_JOBS durable tasks on disk, but the scheduler previously capped durable loads against its combined job map (session-only + durable). A session holding session-only cron jobs could push the map to MAX_JOBS and make loadFileTasks silently skip durable tasks the route had already accepted — a create that returned 201 would then never fire.

Cap durable installs against a durable-only count instead, and share one MAX_JOBS constant between the scheduler and the daemon route, so a successful create is always loadable. Adds a scheduler test that 40 session-only jobs no longer crowd out 20 durable loads.
2026-07-06 03:47:17 +00:00
Shaojin Wen
edc0555ed1
feat(web-shell): named session groups and color tags in the sidebar (#6350)
* feat(web-shell): named session groups and color tags in the sidebar

Extend web-shell session organization with named groups (create / rename /
delete, assign a session to a group) alongside quick color tags, and surface
pin / archive state. The grouping data is plumbed end-to-end through the
daemon.

- core: session-organization-service carries group id / name / color and
  pin / archive metadata on organized-list entries
- sdk / acp-bridge: session-list entries gain groupId / groupName /
  groupColor / archivedAt; add SessionGroupColor and list-session-groups
  result types
- cli/serve: dispatch + session routes expose listing and assigning groups
- web-shell: sidebar group management UI (create / rename / delete groups,
  color picker, pin, archive) and reuse the shared "Group" label for the
  group action, dropping the redundant "Move to group" string

* fix(cli): exclude color-tagged sessions from the ungrouped filter

Color / named group / recent are mutually exclusive buckets in the web-shell
sidebar — a color-tagged session shows in its color section, not "recent".
But the organized session-list `group=ungrouped` filter only checked
`groupId == null`, so a color-tagged session with no named group leaked into
ungrouped results for REST/ACP consumers, disagreeing with the UI taxonomy.

Align the server filter: ungrouped means no named group and no color tag.
Adds an ACP session/list test asserting a color-tagged session is excluded
from group=ungrouped (fails on the old filter, passes on the new one).

* fix(web-shell): clear color tag when creating a group for a session

saveGroupEditor's create-with-target path assigned the new group but left any
existing color tag in place, unlike the sibling assignSessionGroup /
assignSessionColor paths that keep color and named group mutually exclusive.
Because color takes precedence in the sidebar's section bucketing, the session
stayed in its color section and the group assignment had no visible effect.

Send `color: null` alongside `groupId` on that path, and extend the
create-group dialog test to assert the assignment clears the color.

* fix(cli): exclude color-tagged sessions from the named-group filter

Follow-up to the ungrouped filter fix: the per-group filter (group=<id>) also
ignored color precedence. Core and the REST/ACP update paths can persist both
groupId and color, and the sidebar renders such a session in its color bucket,
so group=<id> API consumers saw a session the web-shell shows elsewhere.
Require `color == null` there too, matching the sidebar taxonomy
(color > group > recent).

Adds an ACP session/list test for a session with both groupId and color set.
2026-07-06 02:19:16 +00:00
ytahdn
a8a99f0ed6
fix(web-shell): finalize deferred gated submissions (#6342)
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
* fix(web-shell): finalize deferred gated submissions

* test(web-shell): fix sidebar render result usage

---------

Co-authored-by: ytahdn <ytahdn@gmail.com>
2026-07-05 14:21:27 +00:00
ytahdn
7605c8bd15
feat(web-shell): add onSessionChange and onSubmitBefore callbacks (#6333)
* feat(web-shell): add onSessionChange and onSubmitBefore callbacks

Add session-level event callbacks and a pre-submit interception hook
to WebShellProps, enabling external consumers to observe session
lifecycle events and gate prompt submissions.

New APIs:
- onSessionChange: fires on rename (SSE-driven), submit (direct and
  queued), and turn_complete (streamingState transition with error
  context including block ID).
- onSubmitBefore: async hook called before prompt submission; reject
  cancels the prompt with full retry-state rollback (lastSubmittedPrompt,
  lastSubmittedImages, retriedTurnErrorId, showRetryHint).

Sidebar integration:
- sessionListReloadToken triggers sidebar reload on session events
  with pollInFlightRef + document.hidden guards.
- Delayed 2s reload after submit to account for daemon registration lag.

Safety:
- isPreparingPrompt loading state during onSubmitBefore prevents
  duplicate submissions.
- streamingSessionIdRef prevents spurious turn_complete on session switch.
- All slash commands (including internal /language, /model) go through
  onSubmitBefore; queued prompts intentionally bypass it.

* fix(web-shell): add null initial value to delayedReloadTimerRef

React 19's useRef requires an explicit initial value argument.
Match the existing escapeTimerRef pattern: | null + null.

* fix(web-shell): move clearFollowup after onSubmitBefore gate and add tests

- Move clearFollowup() to after onSubmitBefore succeeds so that
  followup context is preserved when the before hook rejects
- Add null guard for clearTimeout on delayedReloadTimerRef
- Add 5 unit tests for sidebar sessionListReloadToken effect
  covering: token change, undefined, unchanged, document.hidden,
  and poll-in-flight gate conditions

Addresses PR #6333 review feedback.

* feat(web-shell): call onSubmitBefore for queued prompts

Previously enqueuePrompt bypassed onSubmitBefore entirely. Now the
before hook is also invoked for queued prompts — if it rejects, the
prompt is cancelled and not added to the queue. The composer still
clears synchronously (fire-and-forget) since the Composer's onSubmit
contract is synchronous (boolean | void).

Also updates the onSubmitBefore JSDoc to reflect this behavior.

Addresses PR #6333 review feedback on security gap.

* test(web-shell): cover session callback behavior

* fix(web-shell): preserve rejected queued prompts

* fix(web-shell): preserve rejected direct prompts

---------

Co-authored-by: ytahdn <ytahdn@gmail.com>
2026-07-05 12:10:47 +00:00
jinye
7a528d078a
feat(daemon): Add session organization (#6305)
* feat(daemon): add session organization

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

* fix(daemon): address session organization review feedback

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

* test(daemon): cover session organization review cases

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

* codex: address PR review feedback (#6305)

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

* codex: address PR review feedback (#6305)

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

* codex: address PR review feedback (#6305)

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

* codex: address PR review feedback (#6305)

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

* codex: address PR review feedback (#6305)

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

* codex: address PR review feedback (#6305)

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

* fix(web-shell): Address session organization review feedback

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

* fix(core): Harden session organization review edge cases

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

* fix(core): Address session organization review feedback

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-07-05 07:52:56 +00:00
jinye
59e771cef6
feat(daemon): Add session export endpoint (#6297)
* feat(daemon): add session export endpoint

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

* codex: address PR review feedback (#6297)

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

* codex: fix PR integration capability baseline (#6297)

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

* codex: address export tool call id review (#6297)

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-07-04 09:33:44 +00:00
Shaojin Wen
c37cb23ccc
feat(web-shell): manage sessions from the sidebar (archive, unarchive, delete) (#6293)
Add an Archive quick action and a "..." overflow menu (Rename / Archive / Delete) to each session row in the web-shell sidebar, plus a collapsible "Archived" section that lazily lists archived sessions with Restore / Delete. Thread the daemon's existing archiveState filter and archive/unarchive endpoints through the webui workspace facade and the useDaemonSessions hook; rename stays limited to the current live session.
2026-07-04 04:27:22 +00:00