* fix(web-shell): preserve images pasted with source URLs
Keep editable office text first and warn when advertised clipboard files are unavailable.
* fix(web-shell): keep command URL pastes as text
---------
Co-authored-by: 钉萁 <[email protected]>
Fix two correctness issues left by PR #15025 after its merge into main:
Managed summary reloads could delete unsent attachment archives, and
session allocation could retain an already accepted draft after removing
a submitted attachment or normalizing touch references.
Keep same-session capability knowledge during reload, reuse the existing
removal-tolerant acceptance predicate for migration, and preserve reference
identity when there is nothing to hoist. Keep strict host continuation
checks and protection for newer input.
Add focused regression coverage and synchronize the bilingual design.
Co-authored-by: 钉萁 <[email protected]>
* feat(web-shell): add approval mode allowlist
* fix(web-shell): complete allowlist i18n and test mocks
* fix(web-shell): harden approval mode allowlist per review
- Restrict ui.allowedApprovalModes to operator scopes: a repository's
workspace settings can no longer remove an operator's mode
restriction, and workspace-scope writes are refused.
- Report when a valid-but-hidden mode is selected instead of a silent
no-op, in both App and ChatPane, and pin the plan exemption.
- Close an open mode dropdown when the offered list becomes empty.
- Stabilize the host allowedApprovalModes array so inline literals do
not churn the composer and customization memos.
- Share the cycle step as nextApprovalMode in planMode.ts so the App
and ChatPane copies cannot drift.
- Complete DaemonSettingDescriptor fixtures with the required default
field and scope the README live-refresh claim to host Prop changes.
* fix(web-shell): fail closed on approval-mode read errors and pin review follow-ups
* fix(web-shell): scope the approval-mode focus handoff and gate its settings read
Round-4 review follow-ups:
- The focus handoff matched the shared toolbar-popover attribute on the
document-root active element, so it could steal focus from the model
dropdown or another editor instance. It now fires only for this
instance's own mode control, via a dedicated data-web-shell-mode-popover
marker plus toolbar containment, and only once the control is gone.
- The trigger case never fired in a real browser because a focused button
blurs to the body the moment it becomes disabled. Focus ownership is now
recorded by the control's wrapper, so the handoff works after that loss
without an ungated body check that would steal focus on mount.
- The handoff now also covers a narrowing list unmounting the focused row
while the picker stays open, and its disabled-state fallback skips
disabled targets, falling back to the enabled composer.
- The allowlist read is gated on the daemon advertising the
workspace-qualified settings features, so an older daemon keeps the
default list instead of failing closed for the whole session; a
reconnect retries a failed read.
- The approval dialog re-anchors the highlight by row identity, so a
same-length list swap cannot leave Enter committing a row the user
never highlighted.
- The e2e stub answers the scoped and primary routes with different lists
so applying the wrong workspace's read fails, and a new e2e empties the
allowlist under an open picker to pin the browser-level handoff.
- The design record (both languages) now documents the capability gate
and the fail-closed first-read outcome.
* fix(web-shell): repair approval-mode focus handoff and dialog highlight re-anchor (#14345)
Co-authored-by: Qwen-Coder <[email protected]>
---------
Co-authored-by: 钉萁 <[email protected]>
Co-authored-by: qwen-code-autofix <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
* fix(web-shell): polish files and trim preview resources
* test(cli): observe pending child Hook promises
* fix(web-shell): address Files browser review findings
* fix(vscode): sync Pierre modification notices
* chore: ignore the repo-local .pnpm-store fallback directory
pnpm falls back to a project-local store at .pnpm-store/ when the global
store directory is not writable (e.g. sandboxed CI runners). It is a
regenerable cache that node_modules hard-links into; its thousands of
untracked files otherwise dirty the working tree and fail deterministic
workspace-clean verification.
Co-authored-by: Qwen-Coder <[email protected]>
* fix(web-shell): address Pierre preview and file display review findings
- getPreviewLanguage folds case on the extension only, so README.MD and
config.YML highlight while whole-name detector keys (Gemfile, BUILD)
keep their case-sensitive behavior
- formatFileName strips the full Default_Ignorable_Code_Point class so
zero-advance Hangul fillers and variation selectors cannot desync the
tree search projection from the rendered name
- pin the previously unwitnessed pierre-build guards: redirected Shiki
subpaths count toward the 4 MiB budget, getModuleInfo may return null
for redirected ids, emitted assets are skipped, and entry-module
resolveId calls carry no importer
- pin the theme name->module mapping with exact token colors and give
the formatFileName display sites discriminating coverage with
repeated-space names
---------
Co-authored-by: 钉萁 <[email protected]>
Co-authored-by: qwen-code[bot] <qwen-code[bot]@users.noreply.github.com>
Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
* feat(web-shell): upload files by dropping into the file tree
* test(serve): allow hosted replay setup to settle under CI load
* fix(web-shell): address file tree upload review feedback
---------
Co-authored-by: 钉萁 <[email protected]>
* feat(web-shell): show a reduced header on the new-task page
The new-task page rendered no header at all. Show a reduced one with
only workspace/daemon-scoped entries (right-panel toggle, Local Control
QR, workspace location); the workspace icon stays hidden because the
composer below already names the workspace. The right panel offers only
workspace-scoped entries (files, terminal, web preview) before a
session exists, and its state on the new-task page now persists under a
per-workspace draft key, surviving reloads and session switches, and
migrates onto the session the first prompt creates.
* fix(web-shell): preserve new-task panel ownership and migration
---------
Co-authored-by: 钉萁 <[email protected]>
The environment panel's header entry gave no hint that a subagent or background task was still running inside it, so the only way to find out was to open the panel and look.
Light a breathing dot on that entry whenever the panel holds an in-progress task, reusing the sidebar's existing background-running indicator so one running task reads the same in both places. The dot keys off the same status predicate the panel's own spinner uses and respects the host's section configuration, so it cannot promise a row the panel would show as settled.
* feat(web-shell): notify for approvals and user questions
* fix(web-shell): isolate notification help and authorization attempts
* fix(web-shell): preserve notification authorization and keyboard focus
---------
Co-authored-by: 钉萁 <[email protected]>
* feat(web-shell): add adaptive navigation rail and unified Live settings
* style(web-shell): add space below the Home action section
* Revert "style(web-shell): add space below the Home action section"
This reverts commit a2e1c76223.
* fix(web-shell): Preserve navigation and Live drafts across refreshes
* fix(web-shell): keep rail navigation state, drag widths, and Live settings drafts consistent
Address the Critical findings from the latest review rounds:
- Build the standalone shell sidebar config from the sidebar's exported
defaults so the Agents entry can no longer silently drop out.
- Gate rail navigation's persisted uncollapse behind restoresColumn so
opening a full page (Settings, Plugins, ...) no longer rewrites the
user's collapse preference; only Home/Channels/Live restore the column.
- Reset the sidebar section when a panel closes and stop leaking 'split'
as a sidebar page, so a wide split keeps the home column and a working
collapse control.
- Clamp drag widths to the container-aware cap the CSS already renders,
floor the visual drag at the layout minimum, and restore the persisted
width from state instead of a mid-animation/container-capped rect.
- Cap the compact drawer's min-width at the container width.
- Keep the Live voice trigger reachable from the Live page header when
the sidebar's slot is hidden.
- Settle Live settings draft fields (and their conflict baselines) that a
refresh converges onto, so a later edit is not blocked by a stale
baseline.
* fix(web-shell): keep rail panel sections, status dots, and Live drafts consistent
Address review findings on the navigation-rail change: retain the
Channels/Live sidebar section when its panel closes (an explicit Home
choice sticks on the first click), load secondary-workspace attention
status whenever the Home column is hidden (idling the poll when nothing
runs), ignore zero-displacement presses on the resize handle, settle
padded Live settings drafts on trimmed refresh convergence, disable the
shortcut capture until status loads, key the empty-chat welcome chrome
off the shell container signal, and document /live in the pre-auth path
lists.
* fix(web-shell): close rail-era gaps in shortcut, dock, and status handling
Address the round-5 review findings on the navigation-rail change. The
Cmd/Ctrl+B handler resolved its target with e.target, which shadow-DOM
retargeting replaces with the portal host, so the editable-target
exemption never fired for portaled dialogs; it now reads
composedPath()[0] like its siblings. The same handler toggled the raw
sidebarCollapsed state, silently inverting the persisted preference in
the split-view fold band where the sidebar renders folded regardless;
the shortcut, the sidebar prop, and the environment-panel dock budget
now share one effective-collapsed expression. The rail's 56px is
excluded from the dock budget so the environment panel keeps docking at
1440px windows instead of being force-closed on load, with the e2e step
re-pinned at 1440 and the consequence recorded in the sidebar docs. The
secondary-workspace status queries now stay active while the Live
section column is open (the rail Home button is the only status surface
there), and the rail branch resets the project search when Live unmounts
its autoFocus field so a Home round-trip cannot steal focus. The Live
Voice card gates key removal and the required marker on the saved key
state, so a model edit leaving a route can no longer offer removal of a
key that was never stored.
* fix(web-shell): stop silent collapse-preference and drag-width rewrites
Address the round-5 review findings on the navigation-rail change, plus
the residual direction of the round-4 fold-band fix:
- The Cmd/Ctrl+B toggle computed from the effective folded state is a
constant expansion in the split-view fold band; it now rewrites the
unchanged preference there instead of flipping a stored `true` to
`false` on a keypress that changes nothing on screen.
- The rail's column restore fires `onCollapsedChange(false)` only when
the sidebar is actually collapsed, so a drawer tap on Channels/Live no
longer silently clears the desktop preference from the forced-open
drawer.
- The Channels rail entry restores the column only when the Channels
column exists (`channelNavigationEnabled`), matching openNavigation's
rule for full-page entries.
- The App's rail-Home callback drops its redundant preference write; the
sidebar's openNavigation already owns column restoration.
- A drag released at or past the container cap while the cap already
bound the start width no longer persists the capped width over a wider
stored preference, and releasing at the drag start restores the
pre-drag state width as well.
- The Live settings settle effect now runs only on a genuine status
change, so typing a value that briefly trims onto the saved one no
longer loses the just-typed characters.
Each behaviour is pinned by a unit case that goes red without its fix.
* fix(web-shell): settle Live drafts only on genuinely changed values
Follow-up to the round-5 settle fix: the daemon SDK returns a fresh
status object per fetch, so keying the settle effect on object identity
still let a 1s install poll or focus refresh with unchanged data
converge a padded draft the user was typing. Compare the settle-relevant
saved values against the previous status instead: only a refresh that
actually moved a saved value settles, while draft-only re-renders and
unchanged polls leave the in-progress text alone. Pinned by `does not
settle a padded draft when a poll returns unchanged values`, red against
the identity comparison and green with value comparison; the convergence
cases stay green.
* fix(web-shell): keep capped drags window-scoped and drop detached rail hints
Address the two round-6 Critical findings:
- The container-cap guard added last round only covered a release past the
cap. While the cap binds, the rendered edge cannot follow the pointer, so
the rect-based delta is container-scoped in both directions: a 2px
leftward jitter stored 298 over a 420 preference, and a later host
widening rendered the narrow value. Map the pointer delta onto the state
width whenever the cap bound at drag start, so persistence stays
window-scoped (418), while the past-cap branch keeps priority and an
uncapped drag still persists what the user saw.
- navigationHint held a live rail button and was only cleared by DOM
handlers on the nav. Removed elements fire neither pointerout nor blur,
so an unmounted rail - or just an unmounted hinted button, when its
feature gate flips - left the tooltip Popover open against a detached
node, painting the previous icon label near the viewport origin when the
rail returned. Clear the hint when the rail is gone or its anchor is
detached; the check runs every render because a dependency list cannot
observe a child unmounting while both deps stay unchanged.
Both are pinned by cases that go red without their fix: a leftward jitter
stores 418 (298 without the mapping, with the two neighbouring width cases
still green), and a rail tooltip does not survive a rail -> single -> rail
round trip.
* fix(web-shell): re-baseline hidden-surface and failed-save trackers
Address the two round-7 Critical findings:
- The secondary-workspace completion tracker only re-baselined while the
status surface was hidden, leaving a frozen running snapshot across a
visible Home period. Comparing the next refetch against it repainted a
completion the user watched happen as an unread one. The visible path now
nulls the per-source baseline, so the first hidden cycle after a visible
period is baseline-only: only completions actually observed while hidden
produce a dot. The always-hidden sibling path (and its existing test in
WebShellSidebar.collapse-persist.test.tsx) is untouched.
- A save rejected after the daemon persisted some of its writes (its
settings writes land before setEnabled, which rolls back only `enabled`)
was reported as settings.liveSetup.conflict: the stale apiKey baseline
compared against the refreshed keyConfigured/storedKey flipped by the
card's own request. The rejected save now queues a re-baseline of the
request keys and refreshes, and conflict with those keys is suspended
until the refreshed status lands and re-anchors them (state, not a ref,
so the re-baseline provokes the render that clears it).
Both pinned by cases that go red without the fix: no completed badge after
a plugins -> home -> plugins round trip while the sibling always-hidden
case stays green, and no conflict plus a retryable Save after a partially
landed save.
* fix(web-shell): stop certifying unfreshed secondary workspace status
Address the round-8 Critical (body-level, independently confirmed by
qqqys): the secondary catalog page is loaded and polled only while the
Home column is hidden, but the rail Home button folded its retained
snapshots into the indicator unconditionally. An approval resolved
elsewhere kept painting the amber dot and the "Waiting for approval"
accessible name until the user hid the Home column again — two render
sites for one stale certification, and a regression over the pre-PR
surface, which could only render the dot from a live, fetchable path.
build the rail indicator from the primary page plus only those secondary
sessions whose queries are currently armed. While the Home column is
visible the secondary contribution is dropped entirely; hiding the Home
column re-arms the queries and the still-waiting state comes back. On
live-state daemons the secondary catalog path is unloaded regardless, so
nothing live is hidden by this gate. Pinned by a plugins -> home ->
plugins round trip asserting both the dot attribute and the accessible
name, red without the gate while the always-hidden attention case stays
green.
* fix(web-shell): gate the completion tracker baseline on page freshness
Address round-9 Critical R9-1: nulling the secondary baseline on the
visible path closes only half the leak. On re-entering the hidden
surface the tracker installed the store's retained, never-refetched
catalog page as the new baseline (the guard rejected only loading and
error, never a stale page), so the poll arriving seconds later was
diffed against a snapshot taken before the visible window and a
completion the user watched was repainted as unread — exactly what the
visible-path null was supposed to prevent.
Stamp the moment the surface becomes hidden and refuse to install or
compare any secondary snapshot that is not provably fresh
(`(snapshot.updatedAt ?? 0) < hiddenSince`, an unstamped page counting
as not-fresh). The first after-hidden poll then installs the baseline
from truly fresh data. The strengthened round-trip case drives retained
running -> stale re-entry -> fresh idle; it is red without the gate
(the completion badge and the Finished accessible name both appear) and
green with it, while the always-hidden completion case stays green.
* fix(web-shell): clamp every collapse entrance and clear More on rail teardown
Address round-9 Criticals R9-2 and R9-3 (body-level):
- The split-view fold-band clamp lived only at the Cmd+B entrance; the
rail's Collapse/Expand button reached handleSidebarCollapsedChange
unclamped, so a click that visibly does nothing still overwrote the
stored preference and a stored `true` could never be re-written inside
the band. The clamp now lives once in the shared writer: in the fold
band it re-stores the current raw preference regardless of which value
was requested. (`sidebarCollapsed` and `splitFoldedSidebar` reach the
stable []-callback through refs — the former would close over the
mount-time value and the latter is declared later, TDZ.) The Cmd+B
site shrinks back to passing the toggle intent. Pinned by a third arm
in the fold-band App test that clicks the collapse toggle at 1100px
and finds the stored `true` untouched — red with the clamp removed.
- The More reset effect did not watch layout, so a rail -> single ->
rail round trip left moreOpen=true against an unmounted owner and the
rail returned with a phantom open menu (content in the document,
focus inside, never reopened). `layout` joins the dependency list and
a round-trip case in WebShellSidebar.collapse-persist.test.tsx goes
red without it.
* fix(web-shell): let exit-split navigation restore the column under the fold clamp
Address maintainer P2 inline: after the shared-writer clamp landed, a
Home click from the split-view fold band was still intercepted — the
rail's restoresColumn `onCollapsedChange(false)` wrote the folded raw
value back (correct for taps that stay in split view), and `onOpenHome`
then closed split view with the raw preference unchanged, leaving the
Home column folded until a second click.
Split the intent at the shared writer: `exitFoldBand` marks entrances
that reorganize the layout away from split view, bypassing preservation,
and the rail Home handler uses it when the fold band was the overriding
state at click time. Taps that stay in the fold (Cmd/Ctrl+B, the rail
Collapse/Expand button) keep their preference protection untouched, and
the write still never happens inside the mobile drawer, where
splitFoldedSidebar is forced false by construction — so the drawer case
is unaffected (drawer protection is preserved).
Pinned by a fourth arm in `does not invert the stored collapse
preference in the split-view fold band`: stored `true` at 1100px split
view, click `[data-testid="open-home"]` once, and the column is expanded
with the stored key `false`; dropping the bypass inverts that arm while
the three protected arms stay green.
* fix(web-shell): cover Channels and Live in the exit-split fold exception
Address maintainer P2 follow-up: the exitFoldBand carve-out applied only
to Home, so Channels and Live — whose `openPanel` call exits split view
through the web-shell page set (`setMainView('chat')`) the same way —
still had their restoresColumn write clamped when the fold band owned
the sidebar, leaving the section column folded until a second rail tap.
Extent the same bypass to those two entrances, with each entry's own
column-existing gate: Channels applies it only when channelSidebarEnabled
would actually produce a column (mirroring the rail's
channelNavigationEnabled rule, which still leaves the full-page channels
variant folded), and the Live handler inherits the liveSidebarEnabled
guard it's already defined under.
Pinned by two parallel cases beside the fold-band keyboard/click arms:
stored `true` at 1100px split view → one rail tap on `[data-testid=
"open-channels"]` (with `session_source_metadata`) and one on
`[data-testid="open-live"]` (with showLive) — each expands the column and
writes `false`; dropping both bypasses turns both cases red while the
protected Home arm stays green.
---------
Co-authored-by: 钉萁 <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
* feat(web-shell): present structured shell execution results
* fix(shell): preserve cleanup notices and clarify result fallbacks
* fix(test): sync shell result source mappings
* fix(web-shell): address shell command card review findings (#12311)
- Documents: stop duplicating the command above the fallback text and
label an empty legacy result as 'No output' (maintainer N1/N2)
- Escape control and bidi characters at every shell-card render site
while keeping clipboard copies byte-exact
- Render a string rawOutput display instead of model-facing content so
background-promotion and refusal messages stay visible
- Show a finished command's elapsed time from startTime/endTime
- Suppress the Command section when the legacy envelope already leads
with the same command, keeping it once per expanded card
- Pin the wasCancelled status, the background timeout gates, the
synthetic exit-zero headline and the status icons; freeze the
running-elapsed fixture's clock
- Tag the card smoke test and wait for the SSE connection before
driving live frames
- Drop the dead .expandedBash selector and align both design docs with
the undisclosed live-frame transport for line/byte counts
Co-authored-by: Qwen-Coder <[email protected]>
* test(core): isolate gitPush tests from host git config (#12311)
The deterministic verification gate ran the core suite on a persistent
runner whose real ~/.gitconfig carries remote.pushDefault=gd, and the
three gitPush tests that resolve the push remote failed with
'fatal: gd does not appear to be a git repository'. gitEnv strips only
the GIT_CONFIG_* env overrides, so the code under test still honors the
host HOME config — the exact gap the file's hermeticEnv() helper
documents and every gitPull test already guards against.
Pass hermeticEnv() at all six gitPush call sites so host config can no
longer steer push resolution. Reproduced with a poisoned HOME
(remote.pushDefault=gd): the three gate failures appear on the pre-fix
tree and the file is 107/107 green after, in both clean and poisoned
environments.
Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
* fix(core): stage browser-use runtime on demand in core prebuild (#12311)
The deterministic verification gate rebuilds core with a scoped
npm run build --workspace packages/core, but the review-address job
installs with QWEN_SKIP_PREPARE=1 and restores only the root and core
dist artifacts, so packages/browser-use/dist is absent and the core
build dies in copyBrowserUseAssets. The dependency on the built
browser-use runtime was only expressed through the root build's
ordering, leaving every scoped core build (the gate's, or a
developer's on a prepare-skipped install) broken.
Add a prebuild that stages the browser-use runtime only when its
dist/index.js sentinel is missing. The root build keeps building
browser-use first, so the prebuild is a no-op skip there and the
fallback fires exactly when the runtime is absent.
Probe matrix: scoped core build fails without the runtime pre-fix
(the gate rejection, reproduced locally), passes with the runtime
absent (fallback stages it), and passes unchanged with the runtime
present (skip path).
Co-authored-by: Qwen-Coder <[email protected]>
* fix(web-shell): treat empty string shell display as authoritative (#12311)
A persisted empty display string from a silent command is falsy, so the
card fell back to the model-facing envelope in content and the
legacyOutputRepeatsCommand guard then suppressed the Command panel,
rendering a bare envelope with no copy action. Gate the string branch on
the type alone so an empty string renders as empty output next to the
command.
* fix(core): preserve shell display and recording diagnostics
* feat(web-shell): add session tool calls panel with prompt selection
* fix(web-shell): address tool calls review findings
* fix(web-shell): keep prompt text out of persisted dock state
* fix(web-shell): restore sender tool call identity and review fixes
* docs(web-shell): record real daemon sender verification
---------
Co-authored-by: 钉萁 <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: Qwen Autofix <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
* fix(web-shell): preserve reference tags across reloads
* fix(acp): validate daemon input annotations at record, replay, and render
Address review findings on the user-message tag replay PR:
- Skip non-object annotation elements when recording (new
readDaemonInputAnnotations reader), replaying (per-element filter),
and rendering (null guard in splitComposerTagContentByAnnotations),
so one malformed element can no longer poison a persisted session on
every reload.
- Cap caller-supplied annotations at MAX_DAEMON_INPUT_ANNOTATIONS = 256,
matching the sibling attachment-references bound.
- Export DAEMON_INPUT_ANNOTATIONS_META_KEY from bridgeTypes and route
the recorder, replay, and live-echo read sites through it so the
wire/persist key cannot drift silently.
- Restore the deferred /advisor branch's no-payload test pin and split
its annotation assertions into a dedicated case.
- Document the annotations payload trigger in hooks.md and the
intentional queued-strip/composer styling scope in both design docs.
* test(cli): make post-index-change hook cases deterministic across filesystem ticks (#12404)
The ungated-status control assumed `git status` always rewrites the
index on a dirty tree, firing the tree-shipped post-index-change hook.
The rewrite only happens when an entry is racily clean, which held only
because beforeEach's add and commit usually land in one filesystem tick;
a loaded runner straddles the tick, status writes nothing, and the
control fails. Force raciness by re-adding README.md with a future-dated
mtime (resetting the canary the re-add's own index write fires), and
share the fixture with the guarded case so its negative assertion is
exercised against a hook that would genuinely fire ungated.
---------
Co-authored-by: 钉萁 <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
* feat(web-shell): present structured shell execution results
* fix(shell): preserve cleanup notices and clarify result fallbacks
* fix(test): sync shell result source mappings
* fix(web-shell): address shell command card review findings (#12311)
- Documents: stop duplicating the command above the fallback text and
label an empty legacy result as 'No output' (maintainer N1/N2)
- Escape control and bidi characters at every shell-card render site
while keeping clipboard copies byte-exact
- Render a string rawOutput display instead of model-facing content so
background-promotion and refusal messages stay visible
- Show a finished command's elapsed time from startTime/endTime
- Suppress the Command section when the legacy envelope already leads
with the same command, keeping it once per expanded card
- Pin the wasCancelled status, the background timeout gates, the
synthetic exit-zero headline and the status icons; freeze the
running-elapsed fixture's clock
- Tag the card smoke test and wait for the SSE connection before
driving live frames
- Drop the dead .expandedBash selector and align both design docs with
the undisclosed live-frame transport for line/byte counts
Co-authored-by: Qwen-Coder <[email protected]>
* test(core): isolate gitPush tests from host git config (#12311)
The deterministic verification gate ran the core suite on a persistent
runner whose real ~/.gitconfig carries remote.pushDefault=gd, and the
three gitPush tests that resolve the push remote failed with
'fatal: gd does not appear to be a git repository'. gitEnv strips only
the GIT_CONFIG_* env overrides, so the code under test still honors the
host HOME config — the exact gap the file's hermeticEnv() helper
documents and every gitPull test already guards against.
Pass hermeticEnv() at all six gitPush call sites so host config can no
longer steer push resolution. Reproduced with a poisoned HOME
(remote.pushDefault=gd): the three gate failures appear on the pre-fix
tree and the file is 107/107 green after, in both clean and poisoned
environments.
Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
* fix(core): stage browser-use runtime on demand in core prebuild (#12311)
The deterministic verification gate rebuilds core with a scoped
npm run build --workspace packages/core, but the review-address job
installs with QWEN_SKIP_PREPARE=1 and restores only the root and core
dist artifacts, so packages/browser-use/dist is absent and the core
build dies in copyBrowserUseAssets. The dependency on the built
browser-use runtime was only expressed through the root build's
ordering, leaving every scoped core build (the gate's, or a
developer's on a prepare-skipped install) broken.
Add a prebuild that stages the browser-use runtime only when its
dist/index.js sentinel is missing. The root build keeps building
browser-use first, so the prebuild is a no-op skip there and the
fallback fires exactly when the runtime is absent.
Probe matrix: scoped core build fails without the runtime pre-fix
(the gate rejection, reproduced locally), passes with the runtime
absent (fallback stages it), and passes unchanged with the runtime
present (skip path).
Co-authored-by: Qwen-Coder <[email protected]>
* fix(web-shell): treat empty string shell display as authoritative (#12311)
A persisted empty display string from a silent command is falsy, so the
card fell back to the model-facing envelope in content and the
legacyOutputRepeatsCommand guard then suppressed the Command panel,
rendering a bare envelope with no copy action. Gate the string branch on
the type alone so an empty string renders as empty output next to the
command.
* fix(core): preserve shell display and recording diagnostics
---------
Co-authored-by: 钉萁 <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: Qwen Autofix <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
A history session's name lives only in the session catalog: the load
response and the metadata events never carry it, and the session status
route reports the daemon's in-memory entry, which is never hydrated from
disk. The header therefore fell back to `session.new` for the whole load
round-trip, and a turn-start or turn-end refresh could blank a title the
catalog had already resolved — the placeholder flashed back.
Seed the title from the catalog cache as soon as a session becomes active,
and keep resolved titles from being overwritten by an empty status
response.
The workspace selector and the git branch chip both say where a prompt goes,
not how it runs, yet they sat inside the composer toolbar between the "+" menu
and the mode/model controls. In an existing session that was worse than
cosmetic: the workspace selector only ever acts on the next session, so it sat
among controls that act on the current one.
ChatEditor takes a contextChipPlacement prop. While the chat is empty it moves
both chips into their own row under the composer box, so the composer stays
about the prompt; once the session exists the chat header reports the workspace
as a single leading folder icon, with the folder name and full path on hover,
and the composer keeps only git. The header icon uses the same glyph whether or
not a workspace exists -- only the tooltip differs -- because a single icon is
the one place that always answers which workspace the session is in.
A recorded link artifact is an entry to a resource rather than a document the
side panel can render, so its turn card now opens the address instead of a
metadata tab: the control becomes an anchor routed through the shell's external
opener, which hands the URL to the OS browser in the packaged desktop window and
falls back to a native new tab in a plain one. Destinations that opener cannot
take, such as a redacted locator, keep the metadata panel, and Artifact-published
deliveries keep the panel's live preview.
The live-preview design doc records the split in both languages, and the e2e case
that pinned the old metadata behaviour now asserts the new one.
* feat(web-shell): expose slash-command exports as artifacts
* docs(web-shell): clarify export preview loading and CSP scope
* fix(web-shell): address export-artifact review findings (#12050)
- Localize the registration-failure toast on the main view: the hook
mounted from App's own body runs above the I18nProvider App renders,
so it received the key-passthrough default translator.
- Subscribe the registration hook to the throttled animation-frame
transcript snapshot instead of the raw per-delta store, restoring the
shell's input-latency protection during streaming.
- Treat the cross-client session_artifact_forbidden 403 as already
registered — the daemon only raises it after resolving an existing
artifact, so a failed catalog resync no longer surfaces a false
conflict toast; refresh() reverts to Promise<void>.
- Abort the sibling renderer-asset fetch when one arm rejects, and
localize the rendered-preview loading placeholder.
- Pin the stylesheet arm of the preview allow-list matrix, the
secondary-workspace preview read window, and restore the DOMParser
spy the 1 MiB boundary matrix leaked.
* fix(web-shell): address deferred export-artifact review findings (#12050)
- Memoize the built preview document across source/rendered toggles so
switching views no longer re-fetches and re-encodes the renderer assets.
- Measure the 1 MiB gate with TextEncoder instead of allocating a Blob
copy of the content, and thread the artifact catalog sizeBytes through
the detail view so the fallback stops being the common path.
- Pin the producer-to-Web-Shell descriptor wire contract and the renderer
URL/version lockstep with cross-package integration tests, and pin the
reader's per-guard rejections, the preview failure branch, and the
stale-owner registration guard with focused unit tests.
---------
Co-authored-by: 钉萁 <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
A reader had no way to tell Web Shell that one particular answer was
satisfactory, and a host embedding it had nowhere to hook that signal:
the existing answer-footer extension point renders in its own block
above the action row, so it cannot put controls beside Copy.
Add two mutually exclusive marks to the right of Copy on a completed
turn's final answer, plus an opt-in `assistantFeedback` customization
item that notifies a host about every transition. The host is a
listener only: the mark lands first, and throwing, rejecting, or
cancelling a dialog in the callback never changes it.
The mark is deliberately local — kept in localStorage, not in the
session record — so no daemon route, record type, or transcript
projection is added and the wire stays untouched. It is keyed by the
daemon's promptId, the only value that survives a reload, since a
block id is a per-projection ordinal and a persisted replay stamps the
prompt's block rather than the answer's.
No telemetry: marking emits no UserFeedbackEvent and is not gated by
the usage-statistics setting.
Zoom was a transform on the box that also carried the canvas background and
the diagram's own horizontal scrollbar. A transform moves paint, not layout,
so zooming out scaled the scroll viewport along with the diagram and left the
pan range computed from the unscaled width: at half size a 3223px diagram
painted 1612px inside a 489px sliding strip in the middle of a 978px row,
while the scrollable range stayed 2277px and matched nothing on screen.
Panning was a second translate on the same box on top of that — unbounded
horizontally, so dragging past the content origin pushed the leading edge of
the diagram out of the scrollable region, where no scroll brought it back.
Give the three roles their own boxes: the wrapper is the canvas and the only
scroller, the inner box carries nothing but the diagram, scaling uses `zoom`
so layout moves with the paint and the reachable range tracks the diagram
rather than its full-size box, and dragging drives the scroll position instead
of a transform. The canvas takes the diagram's own size, read from its svg:
mermaid marks everything outside flowchart `width="100%"`, so those diagrams
keep shrinking to the canvas width while the rest sit at their design size — a
`fit-content` box cannot resolve that percentage and would fall back to the
300x150 default object size. The canvas holds the diagram's unscaled height so
zooming never reflows the transcript, and it offers the grab cursor only while
part of the diagram is out of view: a diagram that already fits cannot be
moved by dragging, and the cursor should not promise otherwise.
* feat(web-shell): render the compression outcome from structured meta
The compression commands stream their outcome as English sentences over ACP,
which a host that owns its own UI language cannot render. They now carry the
outcome as machine-readable `_meta` next to the sentence, the transcript record
and replay pass it through, and the web shell renders it in the user's language
with the token counts formatted for that language.
A compression that finds nothing to strip reports a terminal no-op phase rather
than reusing the pending one: the payload-driven row has to state the outcome,
and a pending row is dropped once the turn ends.
The invocation note rides its own `_meta` key rather than sharing the result's.
The daemon UI reducer folds a turn's text frames into one block and spreads
`_meta` key by key, so a shared key would be overwritten by the frame that
follows. Nothing else reads the new keys and no frame gains the
discrete-message marker, so a host that renders text as-is sees the same
sentences it saw before.
Opening a session whose last action was a compression also reconciles the
composer's context counters once. They are seeded from the last usage frame in
the replay, and a compression emits none, so they described the context it
replaced; a read is only written back while the session is live and its model
and context window have settled, which is what the reconcile waits for.
* docs(design): describe the structured compression outcome
The design records why the outcome travels as metadata beside the sentence, why
the invocation note needs its own key, why the no-op is a terminal phase, and
how the composer counters reconcile on an attach that ends on a compression. It
also keeps the compatibility analysis for other clients, which is a constraint
of this change rather than a follow-up.
A paste carrying text/plain fell through to the editor and was inserted at any
size, so a long paste became a wall of text the user could neither review nor
edit, and three limits downstream failed with nothing said at paste time: the
daemon rejects a prompt body above 10 MB, a mid-turn insertion carries at most
16,384 characters, and the text-only session draft stops being restorable after
a reload.
A paste of at least 200 lines or 8,000 characters now folds into an attachment
card when its UTF-8 size is within the attachment upload ceiling; a larger paste
stays inline, because an attachment that size could never be sent. The card is
the existing chip, named and titled by the beginning of the paste itself so the
sent message shows the string the card showed, carrying a size and one action
that moves the text into the editor as a single undoable edit. The name keeps
its .txt suffix: the bridge re-derives a stored attachment's type from its name,
so a name without one would resolve the text as an opaque blob.
Exemptions keep today's behavior wherever there is no prompt to attach to: a
disabled composer, shell mode, a host with attachments turned off, a paste that
would replace selected text, and a paste that would leave a `!` or `/` command
leading the draft. A folded paste also outranks a pending follow-up suggestion,
which an empty draft would otherwise submit in its place.
Folding does not reduce what the model sees — an uploaded attachment is resolved
back into prompt text before the turn is built. The gain is in composing and
transport. Design notes and the accepted Risks are recorded in
docs/design/web-shell/web-shell-paste-as-attachment.md.
Co-authored-by: 钉萁 <[email protected]>
The session recap had two triggers that rendered differently: `/recap`
wrote a local anchored message, while the automatic recap (fired when the
user returns after being away) appended a status block to the transcript
store. Two consequences: repeated automatic recaps accumulated instead of
replacing each other, and every recap landed above the "Edited N files"
card a turn produces, because that card is appended after all items.
The automatic recap now writes the same local annotation the manual one
does — one slot, latest request wins — and a turn's outputs card is
placed above a recap that trails the turn. Requests carry the generation
they were issued under, so a slow answer cannot replace a newer recap,
and a superseded request still drops its own "Generating recap"
placeholder. Clearing the screen drops the recap too, on screen or in
flight, which the clear handler previously left to `store.reset()`.
Co-authored-by: 钉萁 <[email protected]>
* fix(web-shell): stop the approval sheet resizing while reading
Hovering the command or content block released its 3-line clamp. Because
the sheet is an in-flow bottom sheet, the released clamp grew the sheet
and shifted the transcript above it: on a six-line command the sheet went
222.5px -> 281px and its top edge moved up 58.5px.
The block now shows the whole command by default and scrolls past 300px,
so the sheet's height no longer depends on where the pointer is.
The sheet also becomes a flex column with its chrome pinned, because
unclamping let a long command grow the content past the sheet's own
max-height, pushing Allow below the visible edge by 66px. The content
region now absorbs the overflow, so the decision stays on screen at every
viewport.
* fix(web-shell): keep the approval sheet's fallback scroller
Pausing the sheet's own scroller while pinning every chrome child with
flex-shrink: 0 left the command region as the only shrinkable item, with
no floor under it. Once the pinned chrome alone exceeded the sheet's
max-height, the command collapsed to zero height and the decision buttons
were clipped inside a box that wheel, touch and drag could no longer
scroll. Measured at 844x390 with a three-line description, the explain
row and four wrapping options: the command block rendered at 0px, all
four buttons sat below the sheet's visible bottom, and a wheel gesture
left the sheet's scrollTop at 0. The base was scrollable there, so this
was a regression rather than a pre-existing limit.
The flex budget, not the overflow value, is what bounds the sheet, so the
ordinary case is untouched: at 1280x720, 1600x1200 and 1440x900 a 30-line
command leaves the sheet's scrollHeight equal to its clientHeight and
scrolls inside its own block. The sheet's scroller now engages only when
the pinned chrome cannot fit, which is exactly the case that used to
clip, and at 844x390 the command keeps 33px with every button back inside
the sheet.
The command and content regions get a 3.5em floor in place of a zero
minimum so neither can collapse to a sliver; the exit-plan content keeps
its own zero minimum.
---------
Co-authored-by: 钉萁 <[email protected]>
* feat: track background result execution across daemon and web shell
* fix(cli): preserve runtime imports during lint normalization
* fix: preserve background execution ownership across clients
* fix: reconcile background completion across waiting and replay
* fix: close background-turn lifecycle gaps from review (#11636)
Four correctness fixes from the round-3 review, each with a
regression test that fails without it:
- Session: race the _qwencode/start_turn admission RPC against the
mid-turn deadline so a host that silently drops it defers the
notification (existing retry path) instead of wedging the pipeline.
- bridgeClient: finishBackgroundTurn now cancels the turn's pending
permission requests via a per-prompt mediator cancel, so an orphaned
approval resolves cancelled instead of pending for the session's
life; a live user prompt's approvals are untouched.
- qwen-live adaptor: background-stamped updates no longer adopt the
session's active job, and background terminals are dropped
unconditionally, so a background execution is never reported as an
orchestrator job's completion.
- DaemonSessionProvider: a background turn's terminal no longer
settles a restored foreground prompt while the daemon live-state
authority reports the prompt in flight.
- live-task-service: the wait_threads wake-suppression guard fails
open when the session summary is gone mid-stream, so a delivered
wake is not swallowed as neither-woke-nor-timed-out.
* test(cli): synchronize output-style dialog test on the selection marker (#11636)
The deterministic verification gate rejected the previous round:
'clears the configured style only after default is selected' timed out
waiting for setOutputStyle(undefined) in the full packages/cli changed
suite.
The test waited only for the 'Concise' row text, which renders in the
catalog commit with the mount-time selection (index 0 = default). The
pre-selection of the active style (index 1) lands in a later
passive-effect commit. Under CI worker load the interleaving up ->
derive(1) -> return picks Concise instead of default, so the expected
undefined call never happens. Wait for the '>' selection marker on the
Concise row instead, the same synchronization the sibling tests use, so
the selection effect has committed before the keys are sent.
* fix: close background-turn attribution and admission gaps from review (#11636)
Round-4 review follow-ups, each pinned by a witness test that fails
without the fix:
- acp-bridge: resolve a prompt's activePromptTerminal record at install
when its terminal was already published during attachment resolution
(deadline expiry, teardown flush); otherwise the terminalPublished
latch suppresses the resolve and a background admission ordering on
that record hangs.
- acp-bridge: keep the queueOnly rejection while a background turn
runs — an automatic execution has no coordinator collector, so Live
steering queued into it is silently consumed without a response.
- qwen-live: background-turn updates no longer stamp the unowned
background turnId as an activity's jobRef, and a background terminal
now releases busy state the turn seeded (attach during the turn, or
a mid-turn drain it consumed) instead of latching isBusy().
- web-shell: a single top-level background agent now shows the
awaiting-processing badge in its ToolGroup row meta (its SubAgentPanel
header is hidden), and the sidebar's background-dot action-hide is
scoped to hover-capable devices so touch rows keep their
always-visible actions.
* test(acp-bridge): remove duplicate SessionAttachmentStore import (#11636)
The prior round added a second import at the top of bridge.test.ts while
the file already imported the symbol further down, failing tsc --build
with TS2300 and the deterministic build gate. Drop the duplicate; the
surviving import covers all usage sites.
* fix: close background-turn collector contamination and lifecycle gaps from review (#11636)
Co-authored-by: Qwen-Coder <[email protected]>
* test(cli): fix async callback typing in background ALS frame witness (#11636)
runWithAgentContext's callback is typed () => Promise<T>; the witness for
the runOutsideAgentContext drain wrap passed a sync arrow returning void,
which fails tsc --build (TS2322). Make the arrow async and await the call;
the callback body still runs synchronously inside the subagent ALS frame,
so the test exercises the identical path (probe: still red with the
Session.ts guard bypassed, green with it restored).
Co-authored-by: Qwen-Coder <[email protected]>
* fix(daemon): isolate background handoffs and foreground replies
---------
Co-authored-by: 钉萁 <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: Qwen Autofix <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
#11644 removed the sidebar's always-visible Git control and left only a
plain-text status summary in the folder hover popover, which also made branch
checkout, pull/push, View Changes and Commit unreachable for any workspace that
was not the selected one. That conflated a request-reduction goal with a
capability loss; the two are separable, and this separates them.
The hover popover's branch row is that workspace's Git entry point again: it
carries a chevron, opens the branch picker as a nested layer, and shows the
branch's severity as a dot beside the name. The picker keeps the details
pinned while it is open and collapses them when it closes, unless the pointer
is still on the row. Rows with no Git wiring keep the plain-text summary.
When Git is read does not change: the polling gate is untouched, and only a
trusted workspace with a real path becomes interactive.
Co-authored-by: 钉萁 <[email protected]>
The mobile composer's "more actions" panel hid its keyboard-hint column
entirely on the touch textarea backend, leaving a blank right side and
hint buttons that could never work (no CodeMirror to dispatch into).
Always render the hint grid and apply each key's desktop effect directly
to the textarea: arrows browse prompt history (now touch-capable in
useComposerCore) or move the caret, Esc exits shell mode or cancels the
running turn. Editing after a history walk re-arms draft persistence.
Co-authored-by: 钉萁 <[email protected]>
The daemon's slow_client_warning frame reports this SSE subscriber's event
queue backing up — a transport diagnostic, not a user-facing failure.
Rendering it as a chat status read as a broken connection mid-turn and sent
users refreshing or abandoning sessions during normal tool execution. Log the
queue watermarks to the console instead; client_evicted remains the real
disconnect signal and keeps its own error path.
Co-authored-by: 钉萁 <[email protected]>
* feat(serve): allow relocating session attachment storage via env var
Adds QWEN_SERVE_SESSION_ATTACHMENTS_ROOT, which stores session attachments
under <root>/<projectHash>/attachments instead of the runtime temp dir so
operators can pin them to a dedicated volume. Reads and removes that miss
the configured root fall back to the default dir so pre-switch attachments
stay readable and removable; archive cleanup removes both roots. New uploads
never shadow a legacy fallback name, and both roots are removed via the same
tombstone dance so a deletion racing a session restore cannot sweep up a
successor directory.
* fix(serve): keep attachment root resolver off fast path
* fix(serve): harden session attachment fallback against degraded roots
read() and remove() no longer force-create the configured root before
consulting the fallback, so a degraded configured volume serves and
removes pre-switch attachments from the healthy default dir instead of
failing; delete() removes the fallback root first, mirroring remove(),
so a failure on the legacy root keeps the primary copy intact;
QWEN_SERVE_SESSION_ATTACHMENTS_ROOT is trimmed before use. Corrects the
docs to say attachment cleanup happens on session delete, not archive.
* fix(serve): address round-2 review findings on session attachment storage (#10066)
Co-authored-by: Qwen-Coder <[email protected]>
* fix(cli): stop restore-probe tests depending on ambient host git state (#10066)
* fix(acp): stabilize attachment fallback handling
* fix(acp): skip attachments being removed during copy
---------
Co-authored-by: 钉萁 <[email protected]>
Co-authored-by: Qwen Code Autofix <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: wenshao <[email protected]>
* feat(web-shell): add opt-in interactive browser terminal
Adds a browser terminal to the web-shell right panel, backed by shared node-pty sessions exposed through an authenticated /terminal WebSocket route on the serve daemon. The feature is opt-in (QWEN_WEB_TERMINAL=1 plus bearer auth) and gated behind a web_terminal capability so default deployments never expose the entry point.
Terminals are workspace-scoped: the daemon resolves the client workspace selector against the live registry, binds each PTY to that runtime's cwd and effective environment, and releases sessions when the workspace drains. Reconnects claim the same session id and replay bounded scrollback (including exit state) without duplicating output; controls resize/release ride text frames while PTY data stays binary. A session with no listeners is reclaimed after 15 minutes.
Co-authored-by: Qwen-Coder <[email protected]>
* chore(vscode): update third-party notices
* test(cli): include web terminal capability
* test(integration): include web terminal capability
* fix(web-shell): harden terminal lifecycle
* docs(serve): document web terminal capability
* fix(web-shell): address terminal review findings
* fix(web-shell): preserve terminal sessions and key events
* fix(web-shell): harden terminal session cleanup
* fix(web-shell): make terminal limits recoverable
* fix(web-shell): isolate terminal sessions
---------
Co-authored-by: 钉萁 <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>