mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-04 13:51:13 +00:00
223 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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 |
||
|
|
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 |
||
|
|
2049d50823
|
test(web-shell): cover restored-history pagination retries (#7657) | ||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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 |
||
|
|
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 |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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. |
||
|
|
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> |
||
|
|
bd87dcb5ce
|
fix(web-shell): filter sessions by source (#6995)
Co-authored-by: ytahdn <ytahdn@gmail.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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. |
||
|
|
2fb6c785db
|
fix(webui): honor skipped followup accept callbacks (#6862)
Some checks are pending
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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.
|
||
|
|
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> |
||
|
|
55528ae2c9
|
fix(webui): use workspace link for @qwen-code/sdk instead of npm registry range (#6823)
Some checks are pending
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> |
||
|
|
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. |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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). |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
65393d0377
|
feat(daemon): record & query sub-session parentSessionId; drop isolated scheduled-task mode (#6676)
Some checks are pending
* 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>
|
||
|
|
38384ae7b9
|
feat(serve): Add cursor-paged transcript replay endpoint (#6525)
* feat(serve): Add cursor-paged transcript replay endpoint Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): Bound transcript replay indexing Limit transcript index builds to bounded snapshots and surface oversized transcript errors as 413 responses. Give transcript status calls a dedicated timeout and update the capabilities integration baseline. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): Validate transcript cursors Sign transcript cursors so forged snapshot sizes cannot bypass the index cache, and keep hasMore tied to persisted record availability when replay conversion returns a partial page. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): Lazy-init transcript cursor secret Avoid generating the transcript cursor HMAC key while importing the core barrel so unrelated tests with narrow crypto mocks can load core without requiring randomBytes. Keep the VS Code companion crypto mock partial so it only replaces the auth-token UUID behavior it asserts on. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(serve): Address transcript replay review suggestions Mark bounded replay truncation frames as having a transcript endpoint, sanitize paged transcript replay conversion errors, and remove the core reader's incomplete pre-encoded cursor field so cursors are only emitted after replay continuation state is merged. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(serve): Stabilize transcript replay pagination Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): Avoid quadratic transcript line scanning Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): Mark transcript history gaps Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): Address transcript reader review comments Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(serve): Address transcript replay review feedback Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(serve): align transcript cursor preflight errors Return transcript snapshot conflicts for cursor pagination when the active JSONL can no longer be found during route preflight. Add route-level and integration coverage for full transcript paging, and document the boolean fullTranscriptAvailable SDK contract. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(cli): Cover paged dangling tool call replay Add a HistoryReplayer.replayPage regression test that carries a dangling tool call through pendingToolCalls and finalizes it on a later page. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): Bound transcript index cache bytes Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix: Address transcript replay review follow-ups Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): Preserve pending tool calls on transcript replay errors Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6525) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: fix CI failure on PR #6525 Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(serve): warm transcript-replay tools leniently The read-only transcript-replay Config sets skipSkillManager, but Config.initialize() still runs toolRegistry.warmAll({ strict: true }), which constructs SkillTool whose constructor throws when no SkillManager exists. The throw escaped the replay try/catch and surfaced as JSON-RPC -32603, so GET /session/:id/transcript returned HTTP 500 for every persisted session. Add a lenientToolWarmup initialize option and set it for the replay Config so tools that cannot construct under the deliberately-skipped subsystems are logged and skipped instead of aborting initialize(). Replay only needs optional tool_call metadata and ToolCallEmitter already falls back to the recorded tool name, so buildable tools keep full title/kind. This supersedes the narrower excludeTools:[Skill] guard, which is removed. * fix(core): invalidate transcript index cache on in-place rewrites An in-place transcript rewrite that keeps the inode and byte length (e.g. rsync --inplace or a redaction pass) reused a stale cached index, because makeCacheKey() keyed only on path:dev:ino:size. readSegmentRecords then found each recorded offset parsing to a different uuid and dropped it, so GET /session/:id/transcript answered 200 with an empty events array instead of the documented 409. Include the file mtime in the index cache key so a fresh read after a same-size rewrite rebuilds the index, and raise SessionTranscriptSnapshotUnavailableError (-> 409) on a uuid mismatch or missing fragment instead of silently returning a short/empty transcript. Also make the qwen-serve docs explicit that at the default --channel-idle-timeout-ms 0 each page rebuilds the index (O(snapshotSize)). * codex: address PR review feedback (#6525) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * qwen: fix CI failure on PR #6525 The Run ESLint step failed on vitest/valid-expect in packages/acp-bridge/src/bridge.test.ts: the getSessionTranscriptPage timeout test stores expect(request).rejects.toBeInstanceOf(BridgeTimeoutError) and awaits it only after advancing the fake timers (a deliberate deferred await so the pending timeout rejection has a handler before it fires). Auto-fixing would add an inline await and deadlock the test, so scope-disable the rule on that assignment with a rationale. lint:ci and the affected test pass. * qwen: address PR review feedback (#6525) Withhold nextCursor on a mid-page transcript replay error. When collectHistoryReplayUpdatesPage catches a replayError partway through a page, records after the failed one are dropped and pendingToolCalls reflect partial state; still emitting nextCursor advanced the client past the dropped records and carried corrupted pendingToolCalls forward (phantom in-progress tool calls on later pages). Now nextCursor is withheld whenever replay.replayError is set — the page is already flagged partial + replayError, so the client stops instead of paginating with corrupted cursor state. Update the handler test to assert no cursor is issued on a replay error. * qwen: address PR review feedback (#6525) Log when parseTranscriptReplayState drops malformed pending tool calls from a replay cursor. Previously rawPending.filter(isPendingReplayToolCall) silently discarded entries that no longer matched the shape (e.g. a cursor from a newer daemon or corrupted in transit), turning a version-mismatch/corruption into a hard-to-diagnose 'tool never completed' artifact on later pages. Now emit a debug warning with the dropped/total counts; behavior is otherwise unchanged. * fix(serve): address transcript review feedback Dispose superseded replay configs, preserve structured resolution errors, sanitize multi-workspace failures, and expand transcript replay coverage across unit and real-daemon integration paths. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * qwen: address transcript review feedback (#6525) - [Critical] Map a missing transcript session to HTTP 404: the child throws a raw resourceNotFound (ENOENT without a cursor) that fell through sendBridgeError to 500. bridge.getSessionTranscriptPage now translates it to SessionNotFoundError, mirroring the load/resume path, with a bridge test. - Dedup the untrusted-session-owner 403 onto the shared sendUntrustedWorkspaceResponse so the response format/message stay consistent across session routes (route logging + context preserved). - Add coverage for parseTranscriptReplayState's non-object replay branch (cursor replay=garbage) -> empty pendingToolCalls + default cumulativeUsage. - Document that cursorHmacKeys are cached for the daemon lifetime (external key rotation requires a restart). * qwen: adopt transcript review suggestions (#6525) - Add a handler test that a mid-page replay error preserves already-emitted events (events>=1) alongside partial+replayError and withholds the cursor. - Add a two-call handler test for the cross-page cumulativeUsage round-trip: page 1 folds the bumped usage into the encoded cursor; page 2 decodes and propagates it into the replay context. - Log (not silently drop) a superseded structured error in the multi-workspace transcript resolution fallback. * qwen: clean up transcript test fixtures to fix no-AK CI flake (#6525) The transcript-paging integration suite wrote ~6 persisted chats/*.jsonl sessions into the daemon's project dir and never removed them. Because vitest runs a file's suites sequentially, those leftover sessions widened a pre-existing race in the later 'PATCH /session/:id/metadata > updates displayName' test (a freshly-created session can exist on disk but not yet appear in the listWorkspaceSessions page), making it fail deterministically in the no-AK smoke run. Add an afterAll to the transcript suite that removes the project chats/ dir, restoring a clean session list for subsequent suites. Verified: full no-AK suite now passes 43/43 across repeated runs. * qwen: harden transcript reader test timestamps + assert page fields (#6525) The record() helper derived the ISO timestamp seconds from text.length, producing invalid values (e.g. 00:00:013) once a record's text reached 10+ chars — harmless today only because no test asserted startTime. Replace it with a monotonic base+offset timestamp (always valid, strictly increasing). Also assert the previously-unchecked required SessionTranscriptRecordPage fields (sessionId, filePath, startTime, lastUpdated); the strict-ISO checks on startTime/lastUpdated guard against the timestamp-helper class of bug. --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> |
||
|
|
2523a36b52
|
feat(web-shell): workspace management sidebar with dynamic registration (daemon multi-workspace phase 4) (#6625)
* feat(web-shell): add workspace picker for new sessions (issue #6378 phase 4) Multi-workspace daemons now show a new-session workspace picker in the sidebar (default primary, untrusted disabled); the chosen workspace cwd is sent on POST /session so the session spawns in that workspace. daemon-react-sdk createSession gains an optional per-call workspaceCwd override covering both the detached and active-session paths; omitting it preserves the previous primary behavior. * feat(web-shell): workspace management with dynamic registration Replace the new-session workspace picker with a full workspace management sidebar. Registered workspaces render as a parallel, collapsible list (folder icon per workspace), each with its own sessions nested underneath, and a "+" entry registers an existing directory as a new workspace at runtime with no daemon restart. Backend: WorkspaceRegistry becomes mutable (add()/onChange()); a new POST /workspaces route validates the directory (exists, not a duplicate, not nested) and registers it; run-qwen-serve exposes a runtime factory that builds a complete workspace runtime (bridge, fs factory, channel factory, workspace service) on demand. The SDK DaemonClient and daemon-react-sdk gain addWorkspace(). * fix(web-shell): show newly registered workspace without a reload Registering a workspace via the sidebar "+" left the list unchanged until a full page reload. handleAddWorkspace called workspace.getCapabilities(), which returns a cached promise and only feeds setCapabilities from the mount effect, so the refresh was a no-op. Add DaemonWorkspaceProvider.refreshCapabilities(): it bypasses the promise cache, issues a fresh /capabilities fetch, and pushes the result into state so consumers re-render. handleAddWorkspace now awaits it (best-effort, so a refresh failure never masks a successful registration). * fix(web-shell): address review feedback for workspace management - registry: list() returns a frozen snapshot so callers can't mutate the internal runtimes array (restores the push()-throws invariant) - POST /workspaces: reject relative paths on the raw input, canonicalize via realpath so symlink aliases can't bypass the duplicate/nesting checks, and serialize concurrent registrations to close a TOCTOU race that leaked bridge/channel infrastructure - sidebar: restore a compact single-workspace project header (name, search toggle, collapse) so single-workspace users keep those affordances and searchOpen/projectExpanded are no longer dead - daemon session: include the target workspace in the create-session failure message - tests: rework WebShellSidebar tests for the WorkspaceSection UI (add the useWorkspace mock, query workspace buttons, cover primary->undefined), use the canonical DaemonWorkspaceCapability type, and add a createSession workspaceCwd forwarding test * fix(cli): harden dynamic workspace registration per review - POST /workspaces: bound cwd by MAX_WORKSPACE_PATH_LENGTH before any filesystem work, and return a generic 500 (log the full error to stderr) so responses can't leak internal filesystem paths - createDynamicWorkspaceRuntime: log a stderr warning when a workspace's settings can't be read, matching the startup secondary-workspace path * qwen: address PR review feedback (#6625) Dynamic workspace reloadDaemonEnv now mirrors the startup secondary path: after reloadEnvironment() it rebuilds the runtime env via buildRuntimeEnvironment(), calls wsEnv.replace(), and updates the env metadata (envFileReadFailed / envFileReadFailures / overlayKeys / envFilePaths). Without this, .env changes on a dynamically registered workspace never propagated to that workspace's spawned child processes. * qwen: address PR review feedback (#6625) Harden POST /workspaces and the workspace registry per review: - canonicalize with realpathSync.native (matches startup) so the same physical dir on a case-insensitive FS can't register twice - nesting guard now also checks in-flight registrations, closing a concurrent parent/child registration race - error responses no longer echo resolved/other-workspace paths - registry add() isolates onChange listener throws so a bad listener can't abort a caller after the workspace is already committed * qwen: address PR review feedback (#6625) - POST /workspaces: cap total registered workspaces (startup + dynamic) to guard against unbounded registration exhausting resources - createDynamicWorkspaceRuntime: register shutdown-cleanup arrays only after the runtime is fully built, so a throw during workspace-service construction can't orphan the bridge/channel - web-shell App: reset selectedWorkspaceCwd after session creation so the workspace picker is one-shot (next new chat defaults to primary) * qwen: address human review suggestions (batch 1) - WorkspaceSection: add console.warn on session-poll failure (was silent) - WorkspaceSection: add aria-expanded for screen readers - AddWorkspaceDialog: associate label/input (htmlFor/id), i18n the absolute-path error, accept Windows drive-letter paths - i18n: remove unused workspaceUntrustedHint key, add addWorkspaceAbsError * qwen: address human review suggestions (batch 2) - Remove dead CSS (.workspacePickerSelect, .workspaceItem* classes from the old select-based picker, replaced by WorkspaceSection) - Add title tooltip to single-workspace project name (shows full path) - WorkspaceSection: sync expanded state on workspace.primary change * qwen: address human review suggestions (batch 3) - DaemonWorkspaceProvider: refreshCapabilities now clears error on success and sets error+status on failure (was incomplete vs mount) - Remove unused onChange/WorkspaceRegistryEvent from workspace registry per simplicity-first (no consumer exists; defers API surface until a real subscriber like SSE push is needed) * qwen: add workspace-management route test coverage Tests cover: 501 (no factory), 400 (missing/empty/relative/long/ nonexistent cwd), 409 (duplicate canonical path), 201 (success), and verifying error messages are generic (no path leak). * qwen: fix CI build failure — add explicit types in route test The CLI's tsconfig includes test files in tsc --build, so all noImplicitAny violations in tests cause build failures. Add explicit type annotations to mock parameters. * qwen: add type/title to single-workspace add-button --------- Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> |