Commit graph

225 commits

Author SHA1 Message Date
ytahdn
59d2ebc851
fix(webui): stabilize history pagination (#8001)
Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
2026-07-29 08:13:46 +00:00
ytahdn
6672573433
fix(web-shell): reduce composer input latency (#8015)
Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
2026-07-29 08:06:53 +00:00
qwen-code-ci-bot
3144046c6b
chore(release): v0.21.1 (#7958)
* chore(release): v0.21.1

* docs(changelog): sync for v0.21.1

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-29 00:25:30 +00:00
qqqys
090b213ad8
feat(web-shell): add native workspace folder picker (#7849)
* feat(web-shell): add native workspace folder picker

* fix(serve): harden native directory picker and add coverage (#7849)

Add a 5-minute timeout to each native picker subprocess and to the
webui action so a dismissed dialog cannot leave an orphaned GUI process,
and distinguish a headless Linux "cannot open display" failure from a
deliberate zenity cancellation. Cover pickNativeDirectory's platform
branches, the route's 501/500 error paths, and the dialog's picker
failure path with focused tests.

* fix(serve): log directory picker failures to daemon stderr (#7849)

* fix(serve): abort directory picker on client disconnect and stagger timeouts (#7849)

* test(webui): add unit tests for pickWorkspaceDirectory action (#7849)

* fix(serve): set UTF-8 console encoding for PowerShell picker and treat timeout kills as cancels (#7849)

* fix(serve): abort directory picker on response close, not request close (#7849)

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
Co-authored-by: Qwen Code CI <qwen-code-ci@users.noreply.github.com>
2026-07-28 05:58:13 +00:00
Shaojin Wen
22b9086678
fix(webui): fall back to history_truncated marker recordId for transcript pagination anchor (#7829)
* fix(webui): fall back to history_truncated marker recordId for transcript pagination anchor

A long in-flight turn can push the live journal past its cap with only
streaming session_updates (no turn-boundary recordId). The retained
replay window then has no `qwen.session.recordId` to anchor transcript
pagination, so `historyHasMore` collapsed to false and the
'History truncated' banner rendered with no loadMore recovery path.

The compaction engine now tracks the last-seen recordId and stamps it
on both history_truncated markers (compacted replay and live journal).
The webui's getPersistedReplayRecordId falls back to the marker's
recordId when no session_update in the retained window carries one,
unlocking transcript pagination again.

Backward compatible:
- Old daemon (no recordId field): marker is field-less, frontend
  behavior matches pre-fix (banner with no loadMore).
- New daemon + old web-shell: extra recordId field ignored by SDK
  validator/normalizer/hasFullTranscriptBeforeReplay.

Tests:
- compactionEngine: marker carries recordId on journal overflow,
  post-seed ingest rebuilds activeRecordId, seedReplayEvents captures
  evicted recordId, marker from evicted head when retained lacks one.
- DaemonSessionProvider: marker recordId used as pagination anchor
  when session_updates lack one (regression for the retained 10000 /
  dropped 7602 scenario).

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

* fix(acp-bridge): freeze replay marker anchor at eviction boundary (#7829)

The replay-path history_truncated marker stamped its recordId from
activeRecordId, which ingest() advances on every post-seed turn
boundary — pushing the anchor past records the client already
displays and causing duplicate transcript blocks on pagination.

Track replayAnchorRecordId separately, captured at the first
replay-window eviction and frozen thereafter. The live-journal
marker continues to use activeRecordId (correct for in-flight
turns). On the client, prefer session_update recordIds over the
marker's stamped anchor so the earliest retained recordId wins
the pagination scan.

Also: reset activeRecordId before the seedReplayEvents pre-scan
(stale-value guard), and add the matching pre-scan to seed() so
eviction cannot lose the only recordId anchor.

* refactor(acp-bridge): extract shared lastRecordIdIn helper (#7829)

* fix(acp-bridge): backfill transcript pagination anchor for live sessions

The marker-recordId fallback only covers sessions whose retained window
holds at least one recordId-bearing event. Live sessions never do:
`qwen.session.recordId` is stamped solely during replay of the persisted
transcript (HistoryReplayer), never on the live event stream. A long
in-flight turn that caps the live journal before any turn boundary fires
leaves the retained window — and thus the truncation marker — with no
recordId at all, so `historyHasMore` still collapsed to false and the
'History truncated' banner rendered with no loadMore recovery path
(observed: retained 10000, dropped 1259, window 8388608 bytes, 20
concurrent Web Shell sessions).

The daemon now backfills a `historyAnchorRecordId` on the load response:
when the replay snapshot carries a truncation marker with no recordId
anywhere, it reads the latest recordId from the persisted transcript and
returns it as a top-level field. The webui uses it as the last-resort
`beforeRecordId` anchor (after session_update and marker recordIds), so
transcript pagination works even for first-turn / mid-turn live sessions.

Best-effort by design: any transcript read failure omits the field and
the client degrades to the pre-fix banner behavior.

Tests:
- bridge: backfills historyAnchorRecordId from the transcript when the
  marker carries no recordId (seeded replay without recordIds + attach
  via in-memory snapshot).
- DaemonSessionProvider: uses daemon historyAnchorRecordId when neither
  marker nor session_updates carry a recordId.

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

* fix(acp-bridge): anchor replay marker at eviction boundary, dedup prepended transcript

Review follow-up addressing two Critical findings on the pagination
anchor and one Suggestion.

Critical — replay-path marker anchor (compactionEngine):
`replayAnchorRecordId ??= activeRecordId` froze the pre-scanned
`activeRecordId` — the LAST recordId across ALL seed events. When a
retained segment carried that recordId, the anchor sat inside the
retained window, so the client's `beforeRecordId` re-fetched records it
already displays and `prependTranscriptHistory` (no dedup) rendered them
twice. Now the anchor prefers the FIRST retained recordId (the eviction
boundary — `beforeRecordId` fetches exactly the dropped records with no
overlap), falling back to the last DROPPED recordId only when the
retained window carries no recordId at all.

Critical — prepend overlap safety net (webui):
Even a well-placed anchor can overlap the retained window in edge cases
(the daemon's transcript backfill for a live-journal overflow returns
the latest recordId by design). `prependTranscriptHistory` now drops
fetched events whose `sourceRecordIds` are already displayed, so any
anchor source yields duplicate-free history.

Suggestion — reuse `getString` for the marker recordId instead of an
inline `typeof` check.

Tests:
- compactionEngine: anchor is the first retained recordId, not the last
  overall (3-segment eviction where retained holds rec-B and rec-C).
- DaemonSessionProvider: fetched events whose records are already
  displayed are dropped, not duplicated.

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

* fix(acp-bridge): correct anchor docs, skip backfill when marker has recordId, close attach race (#7829)

---------

Co-authored-by: wenshao <wenshao@example.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
2026-07-28 04:32:17 +00:00
pratik wayase
60be40e8f9
feat(web-shell): persist terminal history pagination errors (#7709)
* feat(web-shell): persist terminal history pagination errors

* fix: address review feedback for pagination error state
2026-07-26 11:31:36 +00:00
qqqys
df54a7d252
feat(webui): add workspace Channel management hook (#7728)
* feat(webui): add workspace channel management hook

* test(webui): cover manual Channel workspace reload

* test(webui): cover Channel mutation delegation

* test(webui): cover Channel hook failures

* fix(webui): preserve Channel mutation state
2026-07-26 02:17:02 +00:00
yijie zhao
2049d50823
test(web-shell): cover restored-history pagination retries (#7657) 2026-07-25 11:30:51 +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
qwen-code-ci-bot
9e7acec863
chore(release): v0.21.0 (#7675)
* chore(release): v0.21.0

* docs(changelog): sync for v0.21.0

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-24 13:47:34 +00:00
Shaojin Wen
203e61b59b
feat(web-shell): add git mode selector for new session creation (#7471)
* feat(web-shell): add git mode selector for new session creation

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Address review feedback on the git mode selector:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
Co-authored-by: wenshao <wenshao@example.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
2026-07-24 03:48:43 +00:00
ytahdn
c0fee1b609
feat(web-shell): add workspace agent management (#7572)
* feat(web-shell): add workspace agent management

* test(web-shell): remove unstable extensions page tests

* fix(agents): address management review feedback

---------

Co-authored-by: ytahdn <ytahdn@gmail.com>
2026-07-23 08:42:08 +00:00
ytahdn
dc74279103
feat(serve): add workspace-level generation (#7552)
* feat(serve): add workspace-level generation

* docs(serve): document workspace generation capability

* fix(serve): align workspace generation contracts

---------

Co-authored-by: ytahdn <ytahdn@gmail.com>
2026-07-23 05:17:03 +00:00
ytahdn
cd5e1973e5
fix(web-shell): polish embedded shell interactions (#7477)
* fix(web-shell): polish embedded shell interactions

* fix(web-shell): complete file preview integration

* test(web-shell): enable worktree toggle in app tests

---------

Co-authored-by: ytahdn <ytahdn@gmail.com>
2026-07-22 05:21:50 +00:00
ytahdn
694eea27e7
perf(web-shell): optimize long session rendering (#7408)
* perf(web-shell): optimize long session rendering

* test(web-shell): align long-session expectations

* fix(web-shell): address long-session review feedback

* test(web-shell): cover bounded refresh metadata and auto-pagination status suppression

* fix(web-shell): harden long-session refresh fallback and turn animation (#7408)

---------

Co-authored-by: ytahdn <ytahdn@gmail.com>
Co-authored-by: Qwen Autofix <autofix@qwen-code.dev>
2026-07-22 03:28:47 +00:00
ytahdn
80863c0858
feat(web-shell): show subagent sessions in detail panel (#7380)
* feat(web-shell): show subagent sessions in detail panel

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

* chore(sdk): account for subagent client APIs

* fix(core): keep subagent resume history valid

* fix(subagents): stabilize detail session streaming

* test(cli): update telemetry route count

* fix(subagents): address review feedback

* fix(subagents): harden transcript refresh state

* fix(subagents): address maintainer review

* fix(subagents): preserve compact status details

* fix(subagents): address review feedback round 3 (#7380)

* fix(subagents): address remaining detail review feedback

---------

Co-authored-by: ytahdn <ytahdn@gmail.com>
Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>
2026-07-22 02:31:08 +00:00
qwen-code-ci-bot
97a834bc0d
chore(release): v0.20.1 (#7461)
* chore(release): v0.20.1

* docs(changelog): sync for v0.20.1

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-22 02:00:22 +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
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
jinye
6872b48c28
feat(daemon): Advertise ACP preheat readiness (#7200)
* feat(daemon): Advertise ACP preheat readiness

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

* codex: address PR review feedback (#7200)

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

* codex: address PR review feedback (#7200)

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-07-19 13:00:32 +00:00
qwen-code-ci-bot
bc0e2cd180
chore(release): v0.20.0 (#7211)
* chore(release): v0.20.0

* docs(changelog): sync for v0.20.0

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-19 07:35:40 +00:00
qwen-code-ci-bot
3ede6261ea
chore(release): v0.19.12 (#7176)
* chore(release): v0.19.12

* docs(changelog): sync for v0.19.12

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-18 14:58:42 +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
ytahdn
de44c74732
feat(web-shell): paginate restored session history (#7064)
* feat(web-shell): paginate restored session history

* test(web-shell): align workspace visual assertion

* fix(web-shell): harden history pagination

* fix(web-shell): keep history paging retryable

* fix(webui): skip malformed transcript page events

---------

Co-authored-by: ytahdn <ytahdn@gmail.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-07-17 22:09:19 +00:00
ytahdn
b9e5629d08
fix(web-shell): optionally restart SSE after prompt admission (#7080)
* fix(web-shell): optionally restart SSE after prompt admission

* fix(web-shell): allow prompt recovery while disconnected

* test(webui): cover throwing SSE restart path

---------

Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-07-17 16:13:39 +00:00
ytahdn
0ecba4b3c7
feat(web-shell): add skill management pages (#7018)
* feat(web-shell): add skill management pages

* fix(cli): inject GitHub token for skill installs

* test(integration): include skill management capability

* fix(cli): harden skill installation failures

* fix(skills): preserve management compatibility

* fix(cli): isolate skill install transactions

* fix(cli): address skill install review findings

---------

Co-authored-by: ytahdn <ytahdn@gmail.com>
2026-07-17 06:42:46 +00:00
Shaojin Wen
c56ae42fed
fix(web-shell): batch transcript dispatch to avoid tab-return freeze (#7012)
* fix(web-shell): batch transcript dispatch to avoid tab-return freeze

Dispatching each buffered SSE event individually makes a tab-return burst O(events x blocks) on the main thread (per-dispatch block-array copy + freeze), freezing very long sessions for minutes. Coalesce the live stream into one dispatch per macrotask, cap the client's in-memory transcript window, and skip the dev-only block freeze in production.

* fix(web-shell): flush transcript buffer on teardown, guard freeze for browser

Address review feedback: teardown now flushes buffered transcript events instead of dropping them (the SSE client advances lastSeenEventId as events are yielded, so a dropped buffer would be skipped by a same-session incremental resume). Guard FREEZE_TRANSCRIPT_BLOCKS with typeof process so an unbundled browser consumer of the daemon/ui surface does not throw a ReferenceError. Add a dispatch-count assertion to the burst test and an unmount-flush regression test, and align the design doc (setTimeout-only flush, verification plan).

* fix(web-shell): flush before observer debug guard to keep assistant bursts in one block

Address ytahdn's PR #7012 review: the batched-dispatch debug guard read the committed store's activeAssistantBlockId, which lags the pending buffer within a burst, so a debug event interleaved in an observer assistant burst was not filtered and split the block. Flush the buffer before the guard, scoped to observer-mode debug events (rare) so steady streaming keeps batching. Add a focused burst regression test, make the unmount-flush test deterministic with fake timers (it was timing-racy), and update the design doc.

* fix(web-shell): flush buffered transcript on SSE loop error

The catch block at the end of the connection loop skipped the post-loop
flush, leaving buffered transcript events on a scheduled timer. The
retriable path resumes via Last-Event-ID without resetting the store,
and lastSeenEventId has already advanced past those events, so clearing
the buffer would drop them on the incremental delta-resume. Flush
instead.

Also route the restored-prompt settle and replay_complete control
dispatches through dispatchTranscriptNow so each is self-contained
(flush + dispatch) rather than relying on an earlier flush by timing,
and tighten the burst regression test from toContain(CHUNK_COUNT) to
toEqual([CHUNK_COUNT]) so a regression emitting redundant per-event
dispatches also fails.

Addresses the ci-bot review.

* fix(web-shell): keep a batched transcript dispatch throw from cascading

A reducer throw inside runTranscriptFlush escaped as an uncaught
setTimeout error on the macrotask path and, via flushTranscriptSync,
propagated out of the catch block (aborting lastSeenEventId bookkeeping,
reconnect, auth branching, terminal cleanup, and pendingSessionLoad
rejection) and out of the useEffect cleanup (leaving half-torn-down
state). Wrap the dispatch in try/catch and log it with the batch size so
the throw is surfaced without crashing the session or skipping teardown;
one guard fixes all three paths.

Also document the flush precondition on settleActivePromptFromTurnEvent,
which dispatches assistant.done directly and previously carried that
contract only as an inline comment at the call site.

Addresses the ci-bot review.
2026-07-17 00:58:27 +00:00
qwen-code-ci-bot
bbec6dffb9
chore(release): v0.19.11 (#7042)
* chore(release): v0.19.11

* docs(changelog): sync for v0.19.11

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-16 15:23:33 +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
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
19fc52aa93
feat(daemon): add stateless generation SSE (#6947)
* feat(daemon): add stateless generation SSE

* test(integration): expect session generation capability

* fix(daemon): address generation review findings

* fix(daemon): harden generation regressions

* fix(daemon): preserve generation error events

---------

Co-authored-by: ytahdn <ytahdn@gmail.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-07-16 00:00:08 +00:00
chinesepowered
d92c3b27ba
fix(webui): route useLocalStorage functional updates through prev state (#6905)
* fix(webui): route useLocalStorage functional updates through prev state

setValue applied a functional updater to the closed-over storedValue instead
of React's previous state, so two setValue(fn) calls batched in one render
both derived from the same base — the first update was lost and the
stale-derived value was persisted to localStorage. Route the update through
the setStoredValue(prev => …) form and persist the committed value. Adds a
jsdom regression test for the batched-update case.

* refactor(webui): persist via effect and guard against throwing updaters

Address review:
- Restore error handling around the functional updater: a throwing
  updater is caught and leaves state unchanged (previously it could
  propagate through render and crash the tree).
- Move the localStorage write into an effect so the state updater stays
  pure (StrictMode-safe), keeping a first-run baseline ref so the initial
  value is never written on mount.
- Add tests for the no-mount-write contract, invalid-JSON hydration
  fallback, and state updating when setItem throws.
2026-07-15 05:03:04 +00:00
易良
2fb6c785db
fix(webui): honor skipped followup accept callbacks (#6862)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
2026-07-14 15:05:16 +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
qwen-code-ci-bot
42d7d28d11
chore(release): v0.19.10 (#6855)
* chore(release): v0.19.10

* docs(changelog): sync for v0.19.10

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-14 05:12:22 +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
Shaojin Wen
71c7e448f0
feat(web-shell): editable user-scope settings and in-panel model management (#6768)
* feat(web-shell): editable user-scope settings and in-panel model management

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

- Model-fallbacks max-limit options use aria-disabled instead of the
  native disabled attribute so they stay hoverable and can surface the
  "limit reached" title (disabled buttons fire no events); the toggle
  handler already no-ops at the max. CSS updated to match.
- Inline delete confirmation dismisses on Escape (the conventional
  gesture) so keyboard users need not Tab to Cancel.
- scopeToWire throws on an unexpected SettingScope instead of silently
  reporting it as 'user'.
- Re-export DaemonWorkspaceProviderCurrent from the webui daemon facade
  alongside the sibling provider types.
2026-07-13 14:44:54 +00:00
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
qwen-code-dev-bot
55528ae2c9
fix(webui): use workspace link for @qwen-code/sdk instead of npm registry range (#6823)
Some checks are pending
E2E Tests / web-shell Browser Regression (push) Waiting to run
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
packages/webui/package.json declared "@qwen-code/sdk": "~0.1.8" as a
registry version range, but 0.1.8 stable does not exist on npm yet.
This blocks the SDK release workflow at the "Set SDK package version"
step because `npm version -w @qwen-code/sdk` fails with ETARGET.

Change to "file:../sdk-typescript" to match the pattern used by cli
and web-shell workspace packages.

Fixes #6822

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-07-13 09:46:39 +00:00
Shaojin Wen
30aafe0914
feat(web-shell): aggregate scheduled tasks across all workspaces (#6759)
* feat(web-shell): aggregate scheduled tasks across all workspaces

The Web Shell scheduled-tasks page was hard-wired to the primary workspace. On a multi-workspace daemon, creating a task while viewing another workspace stored it under (and ran it in) the primary, and tasks belonging to secondary workspaces never appeared on the page.

Add a workspace-qualified `/workspaces/:workspace/scheduled-tasks` surface that shares the existing CRUD handlers, resolving each request to that workspace's own cron file and session bridge after a trust check — the same pattern every other qualified route uses. The page now aggregates every trusted workspace's tasks into one list, badges each card with its workspace, and lets the New-task form target a workspace. Keepalive and boot rehydration run per registered workspace so a bound task in any workspace fires and survives a restart, instead of only the primary's.

* fix(web-shell): keep an untrusted primary in the scheduled-tasks aggregate

The aggregated list and the New-task workspace picker were both built from the trusted workspaces only. When folder trust is enabled and the primary workspace itself is untrusted, that dropped the primary from the picker and skipped it during the fan-out — so its tasks disappeared from the list even though the primary's trust-free route can still read them, and the form's default (which targets the primary) no longer matched any option, letting the UI show a secondary while a create actually landed on the primary.

Include the primary in the operable set regardless of its trust flag; it is always reachable through the unqualified route the single-workspace page has always used. Secondary workspaces stay gated on trust, since their qualified route rejects an untrusted read or write.
2026-07-12 12:03:30 +00:00
jinye
60fcc8cbce
fix: Make chat recording failures durable and visible (#6743)
* fix(core): Stop chat recording after write failure

Keep the canonical JSONL write chain rejected after the first asynchronous failure so queued descendants are skipped and flush reports the original error consistently.

Cover sticky failures across ordinary, strict, parent-session, ACP close, rename, branch, and rewind paths.

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

* fix: Surface chat recording failures

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

* fix(core): Await custom title persistence

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

* codex: address PR review feedback (#6743)

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

* docs(cli): clarify degraded branch behavior

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

* fix(acp): align recording state entry type

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

* fix: address PR review feedback (#6743)

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

* fix: address durability review findings (#6743)

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

* fix(core): allow artifact migration after recording failure

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

* docs(daemon): correct UI event counts

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-07-12 10:52:26 +00:00
han
5b4d0a3583
feat(web-shell): show current git branch in composer toolbar (#6725)
* feat(web-shell): show current git branch in composer toolbar

* fix(web-shell): address git branch indicator review

* test(web-shell): cover git branch review suggestions

* test(web-shell): fix chat editor test lint

---------

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-12 05:33:40 +00:00
ever-o
ec43c1e951
feat(web-shell): render composer references in user messages (#6537)
* feat(web-shell): render composer references in user messages

* refactor(web-shell): consolidate composer tag utilities

* fix(web-shell): leave custom references as text

* fix(web-shell): avoid ambiguous reference chips

* fix(web-shell): thread composer tag icons to messages

* feat(web-shell): render user references from annotations

* fix(web-shell): include inline tags in input annotations

* fix(web-shell): remove duplicate composer tag icon option

* fix(web-shell): forward plan prompt annotations

* test(web-shell): cover composer annotation edge cases

* fix(web-shell): forward split pane prompt annotations

* fix(web-shell): guard malformed input annotations

---------

Co-authored-by: zhanghuapeng.zhp <zhanghuapeng.zhp@alibaba-inc.com>
Co-authored-by: 易良 <1204183885@qq.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-07-11 17:44:47 +00:00
Shaojin Wen
10ae7d2395
fix(web-shell): support model & approval-mode changes for non-primary workspace sessions (#6737)
POST /session/:id/model and /session/:id/approval-mode used withMutableSession,
which rejects non-primary workspace sessions with a Phase 2a "primary-only" 400.
Creating a session in a newly registered (non-primary) workspace in Web Shell
surfaced "Set model failed" / "Set approval mode failed" error toasts.

Switch both routes to withOwnerMutableSession + runtime.bridge, resolving the
session's owning workspace runtime — the same pattern POST /session/:id/prompt
already uses. Each WorkspaceRuntime owns a full AcpSessionBridge, so the
mutation lands on the correct workspace (and approval-mode persist targets that
workspace's own settings). Primary sessions are unchanged: the primary runtime's
bridge is the same object as the closed-over primary bridge.

Also fold the initial approval mode into the Web Shell create request
(POST /session already applies it via spawnOrAttach) so a new session applies
its mode atomically at spawn: one fewer round-trip, and fail-closed on the
approval setting — a mode that can't be applied aborts creation instead of
silently running in a different mode than requested. The model stays a
best-effort follow-up because creation only accepts a modelServiceId, not the
composer's plain modelId (that follow-up now works on non-primary workspaces
too, via the route change above).
2026-07-11 17:29:56 +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
qwen-code-ci-bot
40ed6b21d7
chore(release): v0.19.9 (#6693)
* chore(release): v0.19.9

* docs(changelog): sync for v0.19.9

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-11 00:35:15 +00:00
ytahdn
0ef3a76bda
feat(web-shell): add artifact right panel (#6591)
* feat(web-shell): add artifact right panel

* fix(web-shell): address artifact panel review feedback

* fix(web-shell): handle artifact panel review edge cases

* fix(web-shell): tighten scheduled task parsing

* fix(web-shell): address artifact panel review followups

* fix(web-shell): guard large file diff stats

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

* test(webui): stabilize heartbeat prompt cleanup test

* fix(web-shell): address artifact review refresh issues

* test(web-shell): stabilize ChatPane artifact hook mock

* fix(web-shell): clear stale session artifacts while loading

* fix(web-shell): preserve artifact tabs during refresh

* fix(web-shell): address artifact review followups

* fix(web-shell): respect workspace cwd for artifact outputs

* fix(web-shell): scope artifact panel actions to pane

* fix(web-shell): resolve split pane merge conflict

* fix(web-shell): clear stale artifact panel state

* fix(web-shell): preserve leading turn outputs

* fix(web-shell): tighten turn output selectors

* fix(web-shell): harden artifact preview sanitizer

* fix(web-shell): address artifact panel review regressions

* fix(web-shell): reconcile split pane artifact snapshots

* fix(web-shell): clear pane artifacts on session switch

* fix(web-shell): clear stale right panel snapshots

* fix(web-shell): repair scheduled task hint string

---------

Co-authored-by: ytahdn <ytahdn@gmail.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-07-11 00:13:49 +00:00
Shaojin Wen
65393d0377
feat(daemon): record & query sub-session parentSessionId; drop isolated scheduled-task mode (#6676)
Some checks are pending
E2E Tests / web-shell Browser Regression (push) Waiting to run
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
* feat(daemon): record sub-session parentSessionId; drop isolated scheduled-task mode

Two changes bundled from this session (recovered after an external
merge-test harness force-reset the worktree):

1. Remove the scheduled-task "isolated" run mode and its precondition
   machinery. All tasks now run in their bound session (shared). The
   create_sub_session tool is retained so users can request isolation
   from a prompt. Drops runMode/condition/withheld across the persistence
   type, scheduler, Session dispatch, REST route, Web Shell UI, i18n, and
   the daemon/SDK contract, with tests updated.

2. Add an immutable parentSessionId to sub-sessions created via
   create_sub_session, wired end to end: BridgeSpawnRequest -> SessionEntry
   -> session summary, persisted into the child transcript (new
   parent_session record + qwen/control/session/parent ext-method) and
   rehydrated by listSessions on daemon restart, surfaced on
   BridgeSessionSummary / DaemonSessionSummary.

* test(daemon): cover sub-session parentSessionId end to end

- chatRecordingService: recordParentSession writes a parent_session record
- sessionService: listSessions reads parentSessionId back from the transcript,
  including the head-window fallback past the 64KB tail
- bridge: spawnOrAttach threads parentSessionId to the session summary and
  dispatches the sessionParent ext-method (and not when there is no parent)
- create-sub-session launcher: passes callerSessionId as parentSessionId

* feat(daemon): add parentSessionId filter to the session list API

GET /workspace/:id/sessions accepts a `parentSessionId` query param that
returns only the sessions spawned by that parent (via create_sub_session).
The default path gathers the whole workspace (persisted + live) and filters
before paginating, so a page is never short of matches; results are sorted
newest-activity-first with an opaque activity cursor. Rejected (400) when
combined with view=organized. Threaded through the SDK
(DaemonSessionListPageOptions.parentSessionId / listWorkspaceSessions).

* test(daemon): cover the parentSessionId session-list filter

Response-builder cases: filters to a parent's children, empty when none,
paginates the filtered set completely (no dupes/gaps). Route cases: the
query param filters GET /workspace/:id/sessions, 400s with view=organized
(invalid_parent_session_filter) and on an empty value
(invalid_parent_session_id).

* fix(daemon): address review — fail closed on removed scheduled-task fields; scope & persist parentSessionId

Review follow-ups on the isolated-removal + parentSessionId work:

- scheduled-tasks REST: reject a POST/PATCH that still carries `runMode` or
  `condition` (400 unsupported_field) instead of silently ignoring them, so a
  stale SDK / cached Web Shell fails closed rather than getting a plain
  unconditional task in place of the guarded one it asked for.
- cron scheduler: a legacy on-disk task that still carries a `condition`
  precondition is skipped (left on disk), not run inline unconditionally —
  a removed safety gate must not silently become "always run". One-time
  operator breadcrumb points at remediation.
- bridge: await + verify the sessionParent persistence instead of
  fire-and-forget, so a dropped transcript write is surfaced loudly rather
  than silently degrading the parent link to live-only after a restart. The
  child is not rolled back on failure.
- session list: the ?parentSessionId= page cursor now binds its
  parentSessionId + archiveState and rejects a cursor replayed against a
  different parent / archive scope, matching the organized cursor contract.

Tests added for all four.

* fix(daemon): address review round 2 — legacy tasks fail closed on all paths; surface parentSessionId persistence outcome

- cron: remove obsolete callback-level "guarded task" tests (headless +
  interactive) — the guard moved to the scheduler load (fail-closed), so
  those callbacks never receive a guarded job. Fixes the CI failures.
- scheduled-tasks REST: a legacy on-disk task that still carries a
  `condition` is now failed closed on EVERY path, not just the scheduler
  tick — GET reports it `enabled:false` with no `nextRunAt`, and POST /run
  rejects it (409 task_legacy_unsupported). Shared `taskHasLegacyCondition`
  helper (exported from core) is the single source of truth.
- bridge: the sub-session parent-lineage write is now timeout-bounded
  (withTimeout, like the other init round-trips) and retried, and its
  outcome is surfaced to the caller via BridgeSession.parentSessionPersisted
  (threaded through the launcher → ext-method → spawner → tool) instead of
  only stderr. create_sub_session warns the model when the link is
  live-only. Requires persisted===true for success.

Tests added for all of the above.

* fix(daemon): reject enabling a legacy guarded task via PATCH

Follow-up: toView reports a legacy `condition` task as disabled, so the
only PATCH the Web Shell sends for it is the Enable toggle. Accepting that
(200) then reading back disabled again is an Enable control that can never
succeed with no error. PATCH `{enabled:true}` on such a task now returns
409 task_legacy_unsupported with the recreate remediation. Test added.

* fix(daemon): address review round 3 — warn on legacy isolated tasks; idempotent parent record; perf + dedup

Addressing ytahdn's review:
- cron: a bare `runMode: 'isolated'` task (no precondition) has no safety
  gate, so it still fires — but it now accumulates history in its bound
  session instead of a fresh per-run one. It runs, with a one-time
  operator warning (the condition subset still fails closed). New shared
  `taskHasLegacyRunMode` helper.
- chatRecordingService: `recordParentSession` is now idempotent (skips a
  repeat append for the same immutable lineage) — matters because the
  bridge write is now retried.
- sessionService: read `parentSessionId` from the records already loaded
  for the listing instead of a second per-file open (the record is written
  at creation, within the scan window) — removes the extra I/O on the hot
  listing path.
- session-list: extract the persisted+live merge into one
  `mergeLiveSessionSummary` helper (was duplicated across the default,
  organized and by-parent paths).

Tests: legacy-runMode fires+warns-once, recordParentSession idempotency,
and the acpAgent sessionParent ext-method handler (valid / no-recording /
invalid-params).

* fix(daemon): address deep review — lineage on fork/restore/SDK; durable parent write; legacy tasks fail closed everywhere

GPT-5 review round + a two-pass reverse audit:

- forkSession no longer copies the source's `parent_session` record, so a
  fork is a fresh top-level session, not a phantom child of the original.
- restore/resume now re-seed the persisted `parentSessionId` on the live
  entry (both the REST handler and the ACP-HTTP transport — the audit caught
  the ACP path being missed), so a restored sub-session still reports its
  parent after a daemon restart.
- SDK: the ACP route mapping + `session/list` dispatcher now forward,
  filter, and project `parentSessionId`; `WorkspaceDaemonClient` serializes
  it too — the filter works over every SDK transport, not just the root HTTP
  client.
- recordParentSession is awaited via the strict append path, so
  `persisted: true` never claims a write that silently failed; it stays
  idempotent so a retry can't double-append.
- bridge parent-write is raced against transport close and treats a timeout
  as terminal (not a retry that would overlap an in-flight request), under
  one deadline.
- legacy guarded tasks (removed isolated mode + precondition) now fail closed
  on ALL consumers — cron_list reports them disabled, keepalive won't bind or
  keep their sessions resident — not just the scheduler tick.
- a bare legacy `runMode: 'isolated'` task (no gate) still runs, with a
  one-time behavior-change warning.
- restored `withheld` as a read-only compat field so pre-upgrade withheld
  run-history is marked "skipped", not shown as a successful run.

Tests added/updated across all of the above.

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-07-10 17:15:13 +00:00