Commit Graph
56 Commits
Author SHA1 Message Date
a8eb74fdf5 fix(web-shell): polish files and trim preview resources (#14143)
* 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]>
2026-10-16 16:45:56 +08:00
jinye f9df3df1ef feat(web-shell): integrate Managed sessions into the home workflow (#14316)
* feat(web-shell): integrate Managed session workflow

* docs(managed-agent): align model-switch acceptance translations

* fix(managed-agent): reconcile WebShell integration checks

* fix(managed-agent): address WebShell review feedback

* fix(managed-agent): align schema projections and CI probes
2026-10-16 14:12:04 +08:00
ytahdnand钉萁 a0e6678d66 feat(web-shell): show a reduced header on the new-task page (#14159)
* 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]>
2026-10-15 07:25:44 +00:00
pomeloandQwen-Coder b0cee09604 feat(web-shell): open context details in the side panel from the ring (#14086)
Clicking the composer context ring posted a Context Usage card into the
transcript, and that card's own detail action posted a second one, so
checking usage a few times left several near-identical stale snapshots
interleaved with the conversation and pulled the view back to the tail.

The right panel already carries a single-instance context usage tab keyed
per session, with the full breakdown, refresh and compression. Bind the
ring click and the transcript card's detail action to it instead. The
snapshot survives only for a composer whose host offers no details
surface, which today is just the embedded side-task pane; typing /context
still prints into the transcript.

Closes #14051

Co-authored-by: Qwen-Coder <[email protected]>
2026-10-14 02:33:44 +00:00
ytahdnand钉萁 df5470e430 feat(web-shell): Add workspace file browser and previews (#13878)
* feat(web-shell): Add workspace file browser and previews

* fix(web-shell): Restore source preview CI fixtures and notices

* fix(web-shell): Address file pane review blockers

* fix(web-shell): Resolve review regressions and strengthen file checks

---------

Co-authored-by: 钉萁 <[email protected]>
2026-10-13 09:38:47 +00:00
72316e6608 feat(web-shell): Reuse standard UI for Managed Agents (#13973)
* feat(web-shell): Reuse standard UI for Managed Agents

Reuse native chat, model and Workspace controls for Java Managed Agent sessions while preserving admission retries and scoped drafts.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(web-shell): Clear accepted drafts after editor remount

Keep accepted input ownership across session switches and response-loss
retries. Snapshot original editor text in the local pending record so a
reload can clear matching owner storage without an in-memory callback.
Preserve original commits, foreign drafts and newer different drafts.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(web-shell): Clarify abandoned first-input admission

Warn that first input may already be running and a new request can repeat
its effects. Refresh the owning Session list after local abandonment and
show uncertain recovery controls only once the active request settles.
Add the focused review regressions for composer input and standalone UI.

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
2026-10-13 07:30:12 +00:00
ytahdnand钉萁 364593cb25 feat(web-shell): add action reminders and in-panel notification access (#13832)
* 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]>
2026-10-12 10:51:11 +00:00
f2e069061a feat(web-shell): add adaptive navigation rail and unified Live settings (#12943)
* 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]>
2026-10-05 08:13:30 +00:00
jinye 6310dd38d5 feat(managed-agent): project durable tool results to WebShell (#13037)
* feat(managed-agent): project durable tool results to WebShell

* codex: address PR review feedback (#13037)

* fix(managed-agent): sync transcript exclusion text and localize forbidden output reads

- Name item.tool_result.updated alongside the other transcript-excluded
  event types in the WebShell transcript contract description, the
  generated client comment, and the event-replay design notes, keeping
  the three surfaces in sync as the event-replay post-merge review
  required.
- Explain the 3-byte UTF-8 page-boundary priming in
  ManagedToolResultPanel.
- Map HTTP 403 to managed.result.forbidden in ResultError instead of
  rendering raw error text, with a regression test.

* codex: address PR review feedback (#13037)

* codex: address PR review feedback (#13037)

* codex: address PR review feedback (#13037)
2026-10-01 06:19:23 +00:00
Shaojin Wen 5df67177df feat(web-shell): prepare updates and restart from sidebar (#12734)
* feat(web-shell): prepare updates and restart from sidebar

* test(serve): include daemon update in capability contracts

* test: complete update contracts and isolate shell output fixtures

* fix(web-shell): preserve task URLs and check update launchers

* fix(serve): support older Node declarations in update checks
2026-09-26 12:15:18 +00:00
qqqysandqqqys 0ab64d5066 feat(live): stream shared screens into browser voice calls (#12623)
* feat(live): stream shared screen into browser voice calls

* fix(live): clear provider readiness after interrupted startup

* fix(live): preserve call intent and clean up forced stops

* fix(web-shell): keep idle browser host takeover available

---------

Co-authored-by: qqqys <[email protected]>
2026-09-24 11:37:01 +00:00
dreamWB 1d30ddc9fa feat(web-shell): support selective host artifact integration (#12588)
* feat(web-shell): support selective host artifact handling and shared highlighting

* fix(web-shell): preserve legacy asynchronous panel handlers

* fix(web-shell): preserve native artifact downloads in embedded hosts

* test(web-shell): keep download timing checks in the shared helper test

* test(web-shell): focus host integration regression coverage

* docs(web-shell): keep host integration design independent of run status
2026-09-24 09:14:37 +00:00
8fb6ade2e4 feat(web-shell): inspect session tool calls by prompt (#12466)
* 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]>
2026-09-24 08:19:53 +00:00
ytahdnand钉萁 906418aa9b fix(web-shell): make mobile access opt-in and simplify split composer (#12537)
Co-authored-by: 钉萁 <[email protected]>
2026-09-24 01:37:22 +00:00
41f9a02efe feat(web-shell): add host controls for model management (#12345)
* feat(web-shell): add host controls for model management

* refactor(web-shell): separate model policy from queue dispatch

* fix(web-shell): close model-management policy gaps from review (#12345)

- isModelSetupCommand now mirrors the daemon's tokenization and altNames,
  so /login, /connect and whitespace after the slash can no longer slip
  past a host that disabled model setup
- refuse host-disabled setup commands in submitUserMessageEdit before the
  rewind, closing the edit-and-resend entrance (and its retry arm)
- a refused side-task initial prompt is now terminal for the tab: the panel
  notifies the app, which drops it from the live tabs and the per-session
  bucket mirror instead of re-arming the toast on every revisit
- the mid-turn path checks the caller policy before reading or uploading
  annotated files, and a failed compensating attachment delete is surfaced
  instead of swallowed
- unmask the /auth suggestion-list assertion (it previously passed via
  hiddenSlashCommands, not the policy clause) and extend the interception
  tests to the alias inputs
- pin the App->SplitView and ArtifactPanel->SideTaskPanel policy hops
- e2e: tag the policy-enforcing cases @smoke so the PR gate runs them,
  keep evidence screenshots inside the git-ignored tree, and drop the
  request-level assertions the mock daemon's recorder cannot see (the
  /prompt half of the /auth refusal check is live and stays)

* fix(web-shell): let host override /auth before the model-setup refusal (#12345)

Address review feedback on the model-management controls:

- Run the policy guards after the host's documented onSlashCommand
  override (and below ChatPane's runtimeStopped fence) instead of above
  it, so an allowAdd:false host keeps its own /auth entrance; the
  refusal still lands ahead of hidden-command daemon forwarding.
- Classify typed commands by the resolved session command (exact name,
  then altNames, case-sensitively) instead of the bare lowercased name:
  a project/user command shadowing auth/connect/login stays runnable,
  and /AUTH reaches the daemon as prose. Guards and suggestion filters
  share the one predicate; an unloaded snapshot still fails closed.
- Refuse /btw side <setup command> before a side-task session is
  provisioned; the panel's initial-prompt guard remains the backstop.
- Scope the compensating attachment-delete failure report to the owning
  session and treat a fulfilled removed:false as a failure.
- Pin the refusal toast in the App-level tests, use the maintained
  promptRequests() matcher with a poll and a positive control in the e2e
  spec, and sync README + both design docs with the intercepted set, the
  ordering, and the inline-edit boundary row.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(web-shell): handle model policy refusals consistently

* test(web-shell): run remaining model-management e2e arms at PR time

Co-authored-by: Qwen-Coder <[email protected]>

* fix(web-shell): fail closed on model setup when the ready snapshot has no auth entry (#12345)

The pane menu filter and the typed-submit guards classify through
isModelSetupCommand, which returned false when the command snapshot was
ready but carried no auth entry (slashCommands.disabled or the SSH policy
whitelist). The pinned local /auth entry stayed listed and dispatched with
no refusal. Treat an unresolved bare setup name on a ready snapshot as the
absent builtin and refuse it; a project/user shadow still resolves and
stays runnable.

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: 铃蓝 <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: qwen-code-ci-bot <[email protected]>
2026-09-23 19:13:06 +00:00
samuelhsin abd2ece552 feat(web-shell): search within the current conversation (#12234)
* feat(web-shell): add in-conversation content search

* fix(web-shell): move conversation search into timeline rail

* fix(web-shell): align search entry and stabilize search dialog

* fix(web-shell): honor search snapshot contract and export budget

* feat(web-shell): expose persisted message navigation to hosts

* fix(web-shell): preserve navigation when rejecting stale requests

* fix(web-shell): address conversation search review regressions

* fix(web-shell): preserve and expose search result selection

* fix(web-shell): preserve search navigation across viewport transitions

* fix(web-shell): settle cancelled and recovered search selections

* fix(web-shell): deduplicate live conversation search echoes

* fix(web-shell): preserve search render and live selection state

* fix(web-shell): cancel search navigation across reconnects
2026-09-23 16:47:10 +00:00
Lilian0122and铃蓝 43868d6a69 feat(web-shell): add shared URL navigation for pages and sessions (#12499)
* feat(web-shell): add shared URL navigation for pages and sessions

* test(web-shell): expect settings route after remote connection return

* fix(web-shell): preserve navigation state across workspace flows

---------

Co-authored-by: 铃蓝 <[email protected]>
2026-09-23 09:43:09 +00:00
97cb625888 fix(web-shell): keep mobile composer controls reachable above a soft keyboard (#12462)
* fix(web-shell): keep mobile composer controls reachable above a soft keyboard

Real-device testing of the touch composer on Android Chrome with Gboard
found three layout problems once the soft keyboard shrinks the viewport:

- The workspace/Git row sits inside the composer's height cap, so with the
  keyboard open the attachments strip collapsed to 16px and attached files
  could be neither seen nor removed. The cap now includes that row's height.
- History search opens upward from the composer with a fixed cap, so its
  search field and close button slid under the header. The panel is now also
  limited to the room above the composer, the same way the @ panel is.
- The editing row overlapped the workspace/Git row by 8px, so tapping the
  bottom of a workspace or branch chip recalled a history entry instead.

* fix(web-shell): keep mobile history search inside the visible chat area

Bound the history-search panel by the real clipping edge above the
composer (the chat pane under the header, or a host-declared
--web-shell-popover-safe-top) instead of a 48px guess. When not even its
96px minimum fits, the panel moves down over the composer so the search
field and close button stay below the header. Re-measurement is
coalesced to one pass per animation frame.

Measure the workspace/Git row instead of assuming 44px: with a second
workspace or a long branch it wraps onto two lines. The cap also adds
the 8px pull-up the editing row gives up below the row, and the editing
row keeps that pull-up when no workspace row is shown, as on main.

A declared zero safe top is now kept as zero; the @ reference panel
reads the variable through the same helper.

* refactor(web-shell): share the popover floor and fit the history panel imperatively

Round-2 review follow-up:

- Export POPOVER_MIN_HEIGHT from popoverRoom.ts and use it for both
  upward-opening popovers instead of two bare 96 literals.
- Derive the history panel's room/shift from its rendered height (a
  no-match panel is just the search bar) and write the two custom
  properties straight to the panel node, so per-frame composer movement
  no longer re-renders the whole composer through state.
- Observe the panel itself so a change in match count re-measures.
- Name the mobile composer cap once as --chat-editor-composer-cap and
  derive the :has(.mobileContextRow) override from it.
- Pin the ResizeObserver re-measure paths with target-tracking stubs,
  pin auto as a clipping overflow value, and add a 412x360 e2e asserting
  the workspace row never shrinks the attachments strip.
- Qualify the soft-keyboard acceptance criterion with the verified
  height and document --web-shell-popover-safe-top in the design docs
  and the web-shell README.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(web-shell): focus history search after input mounts

* test(web-shell): cover history search in short desktop windows

---------

Co-authored-by: probe <probe@local>
Co-authored-by: Qwen-Coder <[email protected]>
2026-09-23 03:37:42 +00:00
55d0a637ee feat(web-shell): support host settings item allowlists (#12328)
* feat(web-shell): support host settings item allowlists

* fix(web-shell): match every setting alias and warn on unknown item ids

Review follow-ups for the host settings allowlists:

- isSettingVisible collects every alias mapped to a key instead of the
  first, so a second stable alias keeps working; exclusion still wins.
- Warn once per unrecognized include/exclude id in development instead
  of dropping it silently (dev-only, compiled out of the published
  build, no new exports).
- Pin the allowlist permit direction for settings-launched pickers and
  Add Model, the builtin:connections filter mapping, and the full
  published alias-to-key table.
- Make the settings browser spec fully descriptor-driven instead of
  matching on case names.
- Cross-link the superseded exclusions design doc and note that
  builtin:connections only renders in the standalone build.

* fix(web-shell): ship the unknown settings id warning in production builds

The warnUnknownItemIds guard was gated on import.meta.env.DEV, which the
library build folds to false, so the whole diagnostic was dead-code
eliminated from dist/index.js — the only artifact embedding hosts
install. A runtime typo in includeItems/excludeIds then rendered the
generic empty state with no console diagnostic.

- Drop the DEV gate; the once-per-id dedup keeps production output to
  one line per distinct unknown id, and empty lists still never warn.
- Run the warn-once test under both DEV modes via vi.stubEnv so the
  shipped behavior is pinned (red on the gated source when DEV=false).
- Assert the excludeItems half of the validation loop with an unknown
  exclusion id (the spread was previously unwitnessed).
- Pin the permit direction for the settings-launched voice picker: open
  it under includeItems: ['setting:voice-model'] and keep it open across
  a still-including rerender.

---------

Co-authored-by: 铃蓝 <[email protected]>
Co-authored-by: qwen-code-ci-bot <[email protected]>
2026-09-21 02:43:05 +00:00
易良andQwen-Coder 1672eb7934 feat(web-shell): expose assistant turn settlement lifecycle (#11251)
* feat(web-shell): expose assistant turn settlement

* fix(web-shell): harden turn settlement callback

* fix(web-shell): skip unused settlement observer

* fix(web-shell): correct turn settlement message and replay delivery

- project the settled message off the raw blocks with the SDK's own
  final-visible-assistant predicate; a promptId-filtered re-projection
  through the render adapter dropped every tool block of the turn (the
  reducer never stamps promptId on tool blocks), merged the turn's text
  halves under the first block's id, and promoted subagent-owned text
- publish the settlement where a replayed terminal settles a locally
  bound prompt: the snapshot is released after injection and SSE resumes
  from lastEventId, so that terminal never reaches the live-loop publish
  and the host callback never fired for it
- default the settlement error code to 'turn_error', matching the
  DaemonHttpError that matchTurnEvent rejects the submitter with
- build the published host event field by field so an internal-only
  field cannot widen the public contract silently

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmtrow8ztpw

* fix(web-shell): preserve turn settlement across reloads

* fix(web-shell): omit unsettled assistant message

* fix(web-shell): retire abandoned prompts and pin turn-settlement gates

Retire locally-bound prompt keys whose terminal is destroyed rather than
delivered. An epoch reset records the bound prompt and, after the reload's
replay injection, publishes a `failed` settlement when the fresh snapshot
carried no terminal for it; auth/terminal-error cleanup and
heartbeat-detected missing sessions retire the bound prompt directly. The
removePendingPrompt stale-session branch now notifies prompt removal with
the foreign session id so the ledger delete keys the right session, and the
settlement projection restores the whitespace-only assistant block skip.

Add discriminating tests: replayed terminal after epoch reset, failed
retirement when the terminal is destroyed, replayed cancelled terminal, and
stale-session removal notification.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmtsl1npgrb

* fix(web-shell): keep turn settlement daemon-authoritative

* docs(web-shell): document settlement deduplication

* fix(web-shell): prettier-format the WebShell README table after adding the onAssistantTurnSettled row

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-conflict/jmtss6ux5rn

* fix(web-shell): define the split-pane active-prompt bridge mock in App tests

The split-session pending-panes test asserts `mockUseDaemonActivePromptBridge`
but the mock was never declared, so `Test (ubuntu-latest, Node 22.x)` failed
with `ReferenceError: mockUseDaemonActivePromptBridge is not defined`. Declare
the hoisted mock, route it through the session-catalog-hooks module mock, and
reset it in beforeEach with a return of the session active-prompt state.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmtt6h9fssa

* fix(web-shell): retire destroyed-turn settlements at the abandonment sites

Restore the retirement that 246d9cbe13 ("keep turn settlement
daemon-authoritative") removed together with its witness test: a prompt whose
terminal is destroyed rather than delivered never produces a terminal frame, so
neither the live nor the replay publish path can fire. Record the bound prompt
on epoch reset and, after replay injection, publish a `failed` settlement when
the fresh snapshot carried no terminal; auth/terminal-error cleanup and
heartbeat-detected missing sessions retire the bound prompt directly. Document
the client-inferred verdict (the load response's promptTerminals is not read) in
the design doc.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmtt6h9fssa

* test(web-shell): pin both streaming guards in the settlement projection

Add a fixture whose prompt's top-level assistant block is streaming (block
guard) and one where a merged message absorbs a streaming block from another
prompt (message guard). Both assert the settlement omits `message`, so each
goes red when its own guard is removed.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmtt6h9fssa

* test(web-shell): a refused stale-session removal keeps the admission key

A refused removal (`removed: false`) means the prompt is still running in the
foreign session; pin that onPromptRemoved is not called so the settlement
admission key survives and the turn's terminal is still published.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmtt6h9fssa

* fix(web-shell): add retireAbandonedPrompt to the session-load and heartbeat effect deps

The reconnect/load effect and the heartbeat effect both call
retireAbandonedPrompt but omit it from their dependency arrays, which fails
the --max-warnings 0 eslint gate. retireAbandonedPrompt is a useCallback whose
only dependency is publishPromptSettlement (itself a useCallback with an empty
dependency array), so it is a stable reference and adding it cannot re-trigger
either effect.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-conflict/jmttbhaw6si

* test(web-shell): pin settlement verdicts and guard stale-session removal

R3-2: a stale-session pending-prompt removal must not write its foreign
prompt id into the current session's turn-navigation store; add a
mutation-witness test that fails when the sessionId === undefined guard
is dropped.

R3-3/R3-4: the epoch-reset and cancelled resync-then-replay settlement
tests discarded the submitter's promise with void prompt.catch(() => {}),
letting one turn emit contradictory verdicts unobserved. Instrument the
promise so the cancelled resolution is pinned alongside the settlement.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmttcwqrask

* style(web-shell): format assistant-turn-settlement design doc

Prettier prefers underscore emphasis over asterisks; the doc otherwise
passes the repo's `node scripts/lint.js --prettier` check.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmttcwqrask

* test(web-shell): dedupe resync-then-replay settlement fixtures

Extract createResyncReplayFixture for the three near-verbatim
resync-then-replay settlement tests so the replay-snapshot literal and the
resync-gate generator live in one place instead of three ~50-line inline
copies. The submitter promise stays in each caller where it must be
awaited, closing the drift that let the epoch-reset case swallow a
cancelled resolution alongside a completed settlement.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmttf1wj0so

* fix(web-shell): settle abandoned prompt ledger

* fix(web-shell): keep turn settlement daemon-authoritative

* fix(web-shell): require whole-message ownership to settle a turn

`getSettledAssistantMessage` accepted a projected message that merely
intersected the settled prompt's block ids. The render adapter merges
consecutive top-level assistant blocks without consulting `promptId`
(`adapters/transcriptToMessages.ts:603-620`), and a continuation carries no
user echo (`acp-bridge/src/bridge.ts:10582`), so two adjacent turns project to
ONE message that keeps the first block's id and concatenates both texts. Both
settlements then published that same message: turn B's answer attributed to
turn A under a shared message id, with the per-mount dedup key already burned
so no corrected callback could follow.

Require the prompt to own every block that went into the message, and treat a
missing or empty `sourceBlockIds` as not owned — `every` is vacuously true for
an empty array. Blocks the reducer never stamped with a `promptId` are skipped
rather than rejected on, since its merge predicate admits deltas when one side
omits it and backfills afterwards. A cross-turn merged message now settles
without `message`, matching the existing "not attributable" semantics instead
of publishing contaminated text.

Pinned in `assistant-turn-settlement.test.tsx` by two cases: adjacent prompts
with no user echo between them (red at the previous head, where prompt A
published `{id: 'assistant-1', content: 'The answer is 42.next turn text'}`),
and the same stream with a user echo, which must still settle each turn to its
own distinct message id so the ownership test does not over-reject.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmtufhms2u4

* fix(web-shell): treat a finished unstamped assistant block as foreign

`promptOwnedIds` admitted any block with no `promptId` on the ground that
the reducer backfills a missing stamp from a later delta. That backfill
only fires while a later delta for the same block still arrives, so a
block that finished unstamped was admitted forever: goal-runtime and
background-notification turns never cross the `session/prompt` boundary
that sets `entry.activePromptId`, so their permanently unstamped text was
glued onto the settling prompt's message id — the same contamination the
whole-message ownership rule exists to stop, through the unstamped door.

Narrow the unstamped arm to blocks that can still be backfilled
(`streaming`), and keep non-assistant blocks admitted because the reducer
never stamps them. Add the two fixtures the mutation matrix showed were
missing: a finished unstamped sibling (kills the widening revert) and an
unstamped still-streaming sibling, the only shape that reaches the
message-level `isStreaming` guard (kills that guard's removal).

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmtuo29vduf

* fix(web-shell): settle a turn from its own final assistant block

`getSettledAssistantMessage` answered "which assistant message closes this
prompt" by re-running the render adapter over the whole transcript and
reconstructing ownership from `sourceBlockIds` — necessary because the
adapter drops `promptId`, and it merges consecutive top-level assistant
blocks without consulting it. Attribution inferred from a projection that
was never designed to carry it made every block shape this module did not
hand-model a way to publish a foreign turn's text, an earlier non-final
message of this turn, or nothing at all, under a `(sessionId, promptId)`
key that `publishPromptSettlement` burns before the listener runs.

Select on the blocks instead, with the exclusion terms the SDK already
uses for this exact question (`findFinalVisibleAssistantForPrompt`,
sdk-typescript `daemon/ui/transcript.ts`): top-level, stamped with this
prompt, non-empty text. Streaming stays "not yet settled" rather than
"keep looking", so partial text is never published. Kept local rather
than imported: reaching that helper through `@qwen-code/sdk/daemon` needs
a new public export threaded through three barrel files, and the
predicate is five lines.

Ownership is now identity, so the ownership set, the whole-message
`every` test, the cross-turn merge question, the unstamped-sibling
question and the insight-segment `isStreaming` gap all go away together
rather than one guard at a time. A `completed` turn whose own final text
is retained, top-level and stamped now always publishes it, which is what
issue #10389 asks for; the design doc's absence list (retention, partial
history, cancellation, failure) is unchanged and still exhaustive.

The four tests that pinned the omission are flipped to assert message id
and exact content, so reverting to the adapter-plus-`every` form goes red
on all four. Net -17 lines.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmtv7cpvvv6

* fix(web-shell): strip insight frames from a settled turn's answer

`getSettledAssistantMessage` published `block.text` verbatim, so a turn whose
final stamped block is an insight frame — `/insight` progress/ready frames
travel as ordinary assistant block text and are stamped with the running
prompt (`bridgeClient.ts:1066-1072`) — hands the host raw protocol JSON
(`{"insight_ready":{"path":"/tmp/report.md"}}`) as the answer, while the real
answer one slot earlier is never published. The renderer strips those frames
and emits no assistant text for a payload-only block, so `trim().length` was
measuring the wrong thing: the payload is non-empty even though the block
renders to nothing.

Reuse the renderer's own segmentation instead of re-deriving a boundary:
export `assistantVisibleTextOf` from the render adapter (a thin wrapper over
its private `splitInsightSegments`) and skip a block whose visible text is
empty, so the substantive answer one slot earlier still wins. This keeps
selection at the block layer — ownership is still read off `promptId`, and the
adapter's cross-turn merge is never re-entered — and keeps the published
content exactly what the renderer emits
(`adapters/transcriptToMessages.test.ts:52-95`).

Two new cases pin it: a payload-only final block publishes the previous
answer's id and content, and a frame glued to trailing text publishes only
that trailing text. Neutering the visibility filter turns both red.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmtvvo040vv

* fix(web-shell): exclude system-notice sources from turn settlement selection

R11-1: the backward scan in getSettledAssistantMessage excluded subagent
blocks, foreign prompt ids, streaming blocks and empty visible text, but not
the two meta.source values this package renders as `role: 'system'` notices.
A `vision_bridge_notice` (emitted as `role: 'assistant'` with
`qwenDiscreteMessage: true`, then stamped with the foreground prompt id by
bridgeClient) that trails the real answer — or is the only stamped block when
the turn ends before the model answers — won the scan and was published to
hosts as `message.content`, uncorrectably, because publishPromptSettlement
burns the (sessionId, promptId) key before the listener runs.

Adds the two-term guard the sibling implementation already uses
(turn-notification-context.ts) plus witness tests for both sources. Each term
was mutation-checked separately: removing `vision_bridge_notice` reddens the
vision witness, removing `background_notification` reddens the notification
witness. The rationale comment above the guard claimed background
notifications are unstamped; the inline drain case is stamped, so it is
corrected here.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmu6tb0j73c

* test(web-shell): pin the settlement dedupe and live repair-suppression guards

Two of the guards this PR adds were verified working against a real daemon but
nothing kept them working: dropping either one left the whole changed-workspace
suite green.

- `publishPromptSettlement`'s published-key early return is the only gate on a
  re-delivered live terminal, because the live publish is not conditioned on
  local admission. New test delivers one `turn_complete` twice.
- The live path's `!repairTargetsTerminal` term withholds publication while a
  live-journal repair targets the same prompt, so hosts never read the
  truncated projection as the turn's final message. New test arms the episode
  from a `history_truncated { scope: 'live_journal' }` marker and asserts the
  terminal publishes nothing, with the repair reload as positive control.

Each mutant was applied on its own: dropping the dedupe return reddens only the
dedupe test (two settlements instead of one); dropping `!repairTargetsTerminal`
reddens only the repair test (one settlement instead of none). Both files are
green with the guards restored.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmu6tb0j73c

* fix(web-shell): ask the adapter which assistant blocks are system notices

wenshao's round-2 finding: the settlement guard excluded system notices by
`meta.source`, but #12141 — which arrived with main during review, not from
this PR's two commits — added a third `role: 'system'` exit to the adapter's
assistant branch that is recognised by payload keys
(`meta.contextCompressionNotice` / `meta.contextCompression`) while its
`meta.source` stays `slash_command`. One `/compress` therefore published
"Context compressed (~2123 -> ~58)." as the turn's final assistant message,
uncorrectably, because `publishPromptSettlement` burns the
(sessionId, promptId) key before the listener runs. `meta.source` is the wrong
predicate for that exit, so a third list term would only close this entrance.

Instead the renderer's own decision is factored into
`classifyAssistantSystemNotice`, which the assistant branch now consumes, and
exported as `assistantBlockRendersAsSystemNotice`. Both consumers that publish
a turn's answer — the settlement guard and `getTurnNotificationContent`, whose
copied two-term list had the same hole — ask it, so a fourth notice source
reaches both by construction. The unreadable-payload fall-through, where the
block's own text renders as the answer, is preserved and pinned.

Witnesses: a compression line trailing a real answer no longer wins the scan;
a compression line as the only stamped block publishes no message; the
predicate covers all three notice exits and still reports a plain
`slash_command` answer as no notice.

Also pins wenshao's round-2 section 5 knowingly: the repair test's
post-repair `expect(settlements).toEqual([])` records current, not desired,
behavior — the live publish is not gated on local admission while the replay
publish is, so a successful repair can turn a delivered settlement into one
that never arrives for a prompt this client never admitted. Filed as #12230;
not fixed here, that would widen this PR past its budget.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmu7rlkyy52

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-09-19 09:52:25 +00:00
Yan ShenandShaojin Wen 8e9e9887d1 fix(web-shell): allow sequential SDK requests during session creation (#11913)
* fix(web-shell): allow sequential SDK requests during session creation

* fix(web-shell): clarify creation timeout guarantees and pin deadlines

* fix(web-shell): clarify restore budgets and verify timeout headroom

* test(web-shell): measure both session creation request budgets

* fix(web-shell): preserve navigation during session creation

* fix(web-shell): preserve workspace selection during session creation

---------

Co-authored-by: Shaojin Wen <[email protected]>
2026-09-18 06:52:16 +00:00
JabrailandShaojin Wen 535dfd9a11 feat(web-shell): add PWA installability and Android development shell (#11722)
* feat(web-shell): add installable PWA support

* fix(build): skip incomplete Web Shell assets when bundling

* feat(web-shell): declare browser support matrix and compat fallbacks

* feat(web-shell): make the Web Shell installable as a PWA

Serves /manifest.webmanifest and /sw.js pre-auth, registers the worker
deferred to load, and replaces the earlier service-worker.js draft with
sw.js as a Vite entry (PNG icons included).

* feat(mobile-shell): add Android WebView shell skeleton

WebView shell around the daemon-served Web Shell (no second UI), profile
tuples of URL/token/display name with #token= fragment delivery, native
WebView version check at the Chrome 107 floor, and a foreground-service
skeleton for SSE/notifications.

* chore(web-shell): drop superseded service worker draft files

* test(web-shell): cover service worker routing and lifecycle

* chore(web-shell): align @playwright/test with main lockfile

* chore: register mobile-shell importer in pnpm lockfile

* test(web-shell): parse sidebar stylesheet with postcss for @supports nesting

* chore: register mobile-shell in pnpm rewrite set

* fix(web-shell): address PWA and Android acceptance blockers

* docs(mobile-shell): make remaining native acceptance limits explicit

* fix(web-shell): address fourth PWA review round

* fix(mobile-shell): restore web view on back

---------

Co-authored-by: Shaojin Wen <[email protected]>
2026-09-17 22:23:58 +00:00
b82e02fa37 feat(web-shell): support host settings item exclusions (#11975)
* feat(web-shell): support host settings item exclusions

* test(web-shell): capture settings panel with host exclusions in visuals

The visual preview flagged that no scenario renders the settings panel,
leaving the host exclusion feature invisible to the before/after preview.
Add a settings harness page that maps ?exclude= onto the host-only
settings prop (the standalone entry never passes it) and two scenarios:
the default panel and the panel with representative exclusions, gated on
row/category assertions so a filtering regression fails loudly.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(web-shell): scope settings-exclusion dialog closes to the owning item

The exclusion force-close effect ran blanket resets on every dialog
state, so a stale settings key closed pickers later opened from a
command or the status bar, and the /model branch-head reset erased the
key on sub-paths that open no dialog, disarming exclusion for a dialog
that was still open. Close only the dialog the held key owns, clear the
key only where /model takes over a dialog surface, share one builtin-id
predicate, and drop the duplicated empty-state description. Pin each
behavior with mutation-probed tests.

* fix(web-shell): cover live settings exclusions and host forwarding

* fix(web-shell): close settings-exclusion gaps from review (#11975)

- add the missing setting:omni alias for omni.enabled, plus a cli-side
  drift test comparing the live served settings set against the published
  alias table in both directions
- pin the App -> SettingsMessage presentation handoff
- scope command-takeover clears to the surface each command owns; drop
  the bare-/model and --voice clears that could only disarm a
  settings-launched surface
- pin the close-ladder arms, builtin item ids, nav badge/scoping, card
  gate, and user-scope exclusion with falsifiable tests
- stop inventing the Model category during the initial settings load
- promote the README section to a top-level Chinese section with prop
  rows, and make the visual harness theme assertion falsifiable

* fix(web-shell): drop duplicate omni settings alias (#11975)

The merge of the two parallel fix branches kept both
'setting:omni-media-delivery' (maintainer's) and 'setting:omni' (bot's)
as public aliases for omni.enabled. Keep the maintainer-chosen
descriptive alias and remove the redundant one before the new alias
API ships; repoint the omni witness test at the surviving alias.

* test(web-shell): close settings-exclusion test gaps from review (#11975)

Address the remaining review findings on the settings presentation
exclusions PR:

- Pin the scoped /auth takeover against over-clearing: a settings-launched
  fast picker (held key 'fastModel') must still close when its alias is
  excluded after /auth ran, and must stay open across /auth without an
  exclusion. The existing auth fixture held exactly the key /auth clears,
  so scoped and blanket clears were indistinguishable.
- Make the served/rendered alias drift test reject two stable IDs mapping
  to one configuration key; the membership checks alone cannot see a
  duplicate, and a published duplicate cannot be withdrawn.
- Move gotoSettingsHarness into the visuals harness next to the other
  navigation helpers and add it to the freeze-before-navigate contract.
- Gate openSettingsPanel on a descriptor-derived nav button instead of a
  row, so reordering the fixture's first General descriptor cannot race
  the settings fetch.
- File the showTimestamps fixture row under General (matching the real
  schema) instead of an invented Output category, and make the chat-width
  absence assertion select the UI category first so it is no longer
  vacuous.

* test(web-shell): pin partially excluded category in settings visuals (#11975)

The host-exclusions capture pinned whole-category dropout (Tools) and a
builtin partial exclusion (builtin:chat-width in UI), but not a partial
exclusion of a descriptor row: with output.showTimestamps filed under
General in the real schema, excluding setting:timestamps must remove
only that row while the category and its surviving rows stay.

The two added assertions also restore the spec's declared assertion
surface after the previous commit moved gotoSettingsHarness (and its
two assertions) into the shared visuals harness, which the
deterministic gate measured as a net loss of two assertions.

---------

Co-authored-by: 铃蓝 <[email protected]>
Co-authored-by: qwen-code-ci-bot <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: Qwen Code Bot <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
2026-09-17 10:53:37 +00:00
易良andQwen-Coder 730932b87f fix(web-shell): honor ui.theme and general.language in the standalone Web Shell (#11961)
* fix(web-shell): let standalone entry defer to ui.theme/general.language settings

The standalone Web Shell entry (main.tsx) resolved its boot theme and
language from ?theme=/?language=, then localStorage, then hardcoded
defaults (dark + navigator.language), and passed the result down as
unconditional host props. App treats any host-supplied value as
authoritative and returns before reading the daemon's effective
ui.theme / general.language, so in Desktop and any qwen serve browser
session the settings-derived branch was dead code: the Settings panel
showed the configured values while the UI stayed dark + English.

- getInitialTheme()/getInitialLanguage() now return undefined ("no
  opinion") when neither the URL nor localStorage supplies a value,
  letting App resolve the effective settings (#11955).
- App reports settings-resolved values through new onThemeResolved /
  onLanguageResolved callbacks so the host can sync document chrome
  (html theme class, theme-color meta) without adopting the value as
  its own opinion — no prop latch, no localStorage write, so a later
  settings.json edit keeps applying.
- Explicit ?theme=/?language= params, stored in-app choices, and live
  panel edits keep overriding settings (host-override contract).

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-issue-patrol/jmu2v4pao6s

* style(web-shell): prettier-format main.test.tsx

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-issue-patrol/jmu2v4pao6s

* fix(web-shell): keep provider surfaces on dark palette and browser locale without a host opinion

WorkspaceSessionProvider's loading and error surfaces render before (or
instead of) App, so they never see the settings-resolved values App
reports through onThemeResolved/onLanguageResolved. With theme/language
now undefined for a no-opinion host, they fell back to light shadcn
tokens on the dark pre-paint page and unconditional English. Restore the
pre-#11955 fallbacks for these surfaces only: the pre-paint dark default
and navigator.language.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmu34f2au75

* fix(web-shell): never persist a rolled-back settings language pick as a host opinion

handleSettingsLanguageChange optimistically notified the host through
onLanguageChange, and both rollback paths (streaming-blocked, daemon
rejection) did the same with the previous value. With no host language
opinion that value is settings-derived, so the host persisted it as its
own opinion and every later settings.json edit was silently shadowed —
#11955 again on a browser where the user never chose a language. The
optimistic pick and the rollback now steer document chrome through the
observe-only onLanguageResolved channel; onLanguageChange fires only
after the daemon accepts the change.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmu34f2au75

* test(web-shell): reset document theme classes between StandaloneApp tests

The theme-light assertion in 'adopts and persists an in-app theme choice'
was satisfied by state leaked from the preceding test, so it could not
witness setDocumentTheme in handleThemeChange. Clear the shared jsdom
document classes in beforeEach so no test inherits another's chrome.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmu34f2au75

* fix(web-shell): keep resolved appearance scoped

* fix(web-shell): keep settings themes scoped

* fix(web-shell): preserve controlled language chrome

* fix(web-shell): preserve controlled language callbacks

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-09-16 12:09:34 +00:00
ytahdnand钉萁 e4755ef6ab feat(web-shell): customize context details and collapse snapshots (#11996)
Co-authored-by: 钉萁 <[email protected]>
2026-09-16 08:01:01 +00:00
20ecdaf6b2 feat(web-shell): add web previews with saved delivery history (#11276)
* feat(web-shell): add web previews with saved delivery history

* fix(serve): register saved preview telemetry route

* test(cli): update telemetry route drift count

* fix(web-shell): reclaim snapshot files and address preview findings (#11276)

* fix(web-shell): Replace undefined mock in split rerender tests (#11404) (#11406)

* fix(web-shell): Replace undefined mock in split rerender tests (#11404)

The two "does not rerender App for other split sessions" tests referenced
mockUseDaemonActivePromptBridge, which is not defined anywhere in
App.test.tsx — a deterministic ReferenceError that failed the main-branch
CI Test job at 70cf363395. The intended render spy is
mockUseDaemonSessionActivityBridge: App calls useDaemonSessionActivityBridge
on every render, and this file already uses that mock as the render-call
witness elsewhere.

Co-authored-by: Qwen-Coder <[email protected]>

* test(web-shell): Clear spy before rerender so the guard can fail (#11406)

The "does not rerender App for other split sessions" tests asserted
mockUseDaemonSessionActivityBridge toHaveBeenCalled() after rerender(),
but nothing cleared the spy since the top-level beforeEach mockReset(),
so it already held setup-time calls from renderApp()/flush()/the split
click and the assertion passed whether or not the rerender rendered App.
Clearing it immediately before rerender() makes the guard measure that
render only; the trailing mockClear() stays to isolate the loop below.

Mutation-verified: with the clear in place, deleting rerender() or the
useDaemonSessionActivityBridge() call in App.tsx turns both
parameterized cases red at the guard (both previously stayed green).

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>

* test(cli): complete worktree ACP config fixture

* fix(acp-bridge): keep restored saved-webpage records when snapshot retain fails (#11276)

* test(cli): make dangling-include screen test host-independent (#11276)

The test asserts runOneMutant's detail names vitest, which only holds
when findVitestBin throws "not found" from the bare fixture — but Node
resolution walks up from the probe tree, so a node_modules above the
runner's TMPDIR (observed on self-hosted CI, where jobs share one /tmp)
resolves a vitest, the probe then runs for real, and the detail carries
no 'vitest'. Plant a shadow vitest that declares no bin in the fixture:
the innermost node_modules wins resolution on every host, so the throw
is deterministic. Same pattern as the runControlMutant shadow in this
file.

* test(web-shell): align branding fixtures with workspace and sidebar behavior

* fix(acp): preserve non-strict ingestion on snapshot reference errors

---------

Co-authored-by: qwen-code-ci-bot <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
2026-09-11 10:01:34 +00:00
2f426a64f4 feat(web-shell): enrich browser notifications and open target sessions (#11447)
* feat(web-shell): enrich browser notifications and open target sessions

Co-authored-by: Qwen-Coder <[email protected]>

* feat(web-shell): include current prompt in browser notifications

Co-authored-by: Qwen-Coder <[email protected]>

* feat(web-shell): configure browser notification branding and defaults

Allow hosts to configure notification names, icons, and the initial preference. Enable notifications by default in the built-in page while preserving saved user choices and browser permission requirements.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(web-shell): address browser notification review feedback

Preserve code excerpts, bound pending notification text, and keep notification configuration changes from restarting session streams. Reconcile branding documentation and add regression coverage.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(web-shell): address notification follow-up review cases

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
2026-09-11 02:27:39 +00:00
Shaojin WenandQwen-Coder 13b69b3313 feat(web-shell): make the product name and logo configurable (#11244)
* feat(web-shell): make the product name and logo configurable

White-labelling the Web Shell meant editing six source files and rebuilding: the
product name was a literal in the sidebar, the welcome header, the About panel
and the document title, and the logo existed as two hand-duplicated copies of the
same inline SVG.

Brand is now configuration. A name and a logo path are read from the operator
settings scopes, and a new process-global daemon route resolves the logo into a
data URI for the client. Embedding hosts get the same control through a prop that
replaces the resolved value outright. The terminal banner already had its own
white-label settings, so this follows their sanitization and path-resolution
conventions rather than inventing parallel ones.

Two decisions carry the design. The workspace settings layer is never read, so a
repository's own settings file cannot rename the product or name a file for the
daemon to inline into every connected browser. And a custom logo is only ever
rendered as an image, never injected as markup, which is what makes it safe for
the daemon to skip sanitizing the SVG it read.

An untouched installation renders exactly what it did before: the resolved brand
is empty, every consumer falls back to its own built-in literal, and the served
document keeps its original title and favicon bytes, so the existing visual
baselines stay valid.

* fix(docs): sync the daemon capability tag count with the new brand tag

The daemon index states how many capability tags the registry holds, and a
contract test fails the build when the two disagree. Adding the brand tag made
the registry 158 while the index still said 157.

* chore: re-trigger CI on the current head

The previous push arrived while a re-run of the prior commit was still holding
the workflow's concurrency slot, so GitHub suppressed the new run instead of
queueing it and the event does not replay once the slot frees. This leaves the
branch head with no CI at all. An empty commit re-fires the synchronize event;
the repository squash-merges, so it does not survive into main.

* fix(web-shell): Address brand review feedback across resolver, client and docs

- Expose a brandSettled flag from the workspace provider so a
  settled-with-no-brand outcome (older daemon, withdrawn host prop) is
  reported and invalidates stale cached chrome, while the in-flight
  state still fires nothing
- Fall back to the built-in sidebar mark when a logo data URI fails to
  decode, and when a host logo node is falsy; remount the logo image
  per data URI so a decode failure cannot stick to the next logo
- Report an empty host brand name as unset in onBrandResolved, and type
  the callback payload as the document-actionable subset
- Exclude the favicon from the boot watchdog's fatal-resource
  classification and keep its content-free error out of the panel list
- Require a namespaced <svg> root in the resolver, cover the post-decode
  size cap, and pin the exact stderr prefix the protocol doc publishes
- Assert GET /brand answers JSON on the real app for any Accept header
- Sync the capability index count (159), settings docs and the design
  doc with the above

* fix(web-shell): Address second brand review round — settle semantics, SVG scanner, test fixtures

- Settle the brand fetch only on a definitive outcome: an answer, or a
  404 from a route-less daemon. A retryable failure (503 bootstrap, 429,
  transport, old SDK) is unknown rather than absent — settling it
  cleared the pre-paint cache and reset the tab title mid-session over
  a blip nothing retries (review Critical)
- Rewrite the SVG root scanner quote-aware: a quoted `>`, `[` or `]` in
  DOCTYPE literals or the root tag no longer rejects valid documents,
  and an xmlns-shaped substring inside another attribute's value no
  longer certifies a namespace-less document as SVG
- Warn (not reject) when the root svg carries no viewBox and no
  width/height: the browser loads such an image successfully but may
  paint a blank mark at the sidebar's fixed size, and only the daemon's
  stderr can say so
- Treat a nullish host brand prop as absent in the resolution gate,
  matching the `??` one line above
- Pin previously unwitnessed guards: the fd identity re-check and both
  fs soft-fail branches, the logo key remount, the favicon error-list
  exclusion, the unsettled-to-settled transition, whole-object host
  takeover against a daemon logo, and the superseded-client reset and
  catch-leg disposed guards
- Drive GET /brand on a real app with the SPA fallback mounted and a
  token configured, and pin the System settings layer to empty files so
  a maintainer's machine-wide brand cannot leak into the assertions
- Round-trip the pre-paint cache through the real inline script instead
  of pinning each side against its own copy of the key
- Document the hard-link and decoded-size rules, the namespace
  requirement, the viewBox advisory, and the corrected 503 semantics
  (bootstrap catch-all, retryable; a draining daemon does not reject
  this route); the README's onBrandResolved row now names the narrower
  payload type

* fix(web-shell): Close third brand review round — placeholder exclusion, scanner correctness, retry path

- Read brand keys from the pre-substitution snapshot and refuse
  environment placeholders with a warning: placeholder substitution
  draws from the process-wide environment, which a workspace populates
  first at boot, so resolving brand placeholders would let a repository
  supply the value — the workspace exclusion through a side door
  (review Critical)
- Blank quoted SVG attribute spans per UTF-16 code unit, not per code
  point: an astral character collapsed to one space and shifted every
  later index, refusing valid namespaced documents (review Critical)
- Accept prefix-bound <svg:svg> roots that bind xmlns:svg to the SVG
  namespace (browsers render them), keep refusing <svgfoo> near-misses
  and prefix-only bindings on an unprefixed root, and pin the
  whitespace-prefixed fake-xmlns shape that actually exercises the
  quote-blanking
- Make the scaling-geometry advisory value-aware: an empty viewBox,
  zero dimensions, or percentage width/height now warns (loads
  successfully but cannot scale into the sidebar box)
- Add refreshBrand beside refreshCapabilities — same 404-only settle
  rule, generation-guarded, gated to the genuinely-missing state and
  no in-flight fetch — and call it from the connection recovery path,
  so a retryable brand failure no longer leaves in-app chrome and tab
  chrome disagreeing for the page's lifetime
- Warn on console when the sidebar logo fails to decode (the one logo
  failure the daemon's root-tag validation cannot see)
- Pin the O_NONBLOCK FIFO defence with a real mkfifo fixture, the
  published 429/drain transport states on the real app, and make the
  ordering test's body discriminating with a fixture brand
- Document the placeholder carve-out, prefix-bound roots and the
  value-aware advisory across settings docs, schema, protocol reference
  and the design record

* fix(web-shell): Close fourth brand review round — env-var side door, retry machinery, scanner class fix

- Add QWEN_CODE_SYSTEM_SETTINGS_PATH / QWEN_CODE_SYSTEM_DEFAULTS_PATH to
  the hardcoded project-.env exclusions: they select which file becomes
  the System settings layer, so a repository could otherwise promote
  its own file above the operator's User layer and rebrand the shell
  with literal values (review Critical)
- Refuse brand placeholders on substitution, not syntax: run the real
  resolveEnvVarsInString and refuse only when it would change the
  value, so a literal name containing `$` survives while anything a
  workspace-tainted environment could have supplied is refused
- Emit one stderr line per brand warning instead of a joined line, so
  a log rule keyed on one key's prefix keeps firing when another
  misconfiguration exists
- Make the SVG root matcher prefix-agnostic and entity-aware: any
  prefix bound to the SVG namespace qualifies, character references in
  attribute values are decoded before comparison, a degenerate viewBox
  no longer suppresses the scaling advisory, and a prefix-bound root
  with unprefixed children gets its own advisory
- Retry a retryable brand failure once after a short delay and warn
  only when the retry is also refused (404 stays silent); wire
  refreshBrand into the recovery path with tests; gate it on
  in-flight; attribute retries per client instance; reset the brand in
  the render that observes a client change so no committed frame mixes
  two connections
- Pin the O_NONBLOCK flag with a flags-capture witness (the FIFO case
  alone cannot fail fast: a blocked open wedges the worker's event
  loop), skip the FIFO fixture on Windows where the defence does not
  exist, and mock the not-stattable path instead of relying on POSIX
  ENOTDIR
- Document the startup hold vs terminal 503 semantics correctly and
  retire the design record's pre-retry narrative

* fix(web-shell): Close fifth brand review round — real XML parser, docs alignment, hold test

- Parse the logo with the vendored fast-xml-parser instead of the
  hand-rolled root-tag scanner: well-formedness (duplicate attributes,
  misplaced declarations) is now rejected by a real validator, the
  namespace check reads parsed attributes with entity references
  decoded, any prefix bound to the SVG namespace qualifies, and the
  prefix-children advisory walks the parsed tree — construct-aware by
  construction. The DOCTYPE is stripped for the validator with a
  quote- and subset-aware scan because the validator's own DOCTYPE
  handling is quote-blind (review Suggestion, filed as the class-level
  finding)
- Bound entity decoding at Unicode's ceiling: an out-of-range
  character reference no longer throws a RangeError out of the
  resolver — which the route's catch-all turned into a 200 {} that
  dropped the validly configured brand NAME along with the logo
  (review Critical)
- Refuse brand placeholders by comparing the layer's substituted value
  against its pre-substitution snapshot — the authoritative engine
  WITH the home-.env fallback — and keep an unresolvable placeholder
  verbatim so a typo'd variable shows as literal text; say so in all
  five published artifacts, and hint when a missing logo's path still
  contains a placeholder token (review Critical)
- Gate the prefix-children advisory on the default namespace being
  absent: with both bindings, unprefixed children render fine
- Split the scaling advisory's message by cause (zero-area viewport,
  malformed viewBox, no usable geometry) and document both advisories
- Pin the startup-hold guarantee with a real deferred-runtime test:
  GET /brand issued before the runtime is ready is held and answered
  200 once it settles
- Drive the two retry tests on fake timers (BRAND_RETRY_DELAY_MS is
  exported for them) instead of sleeping through the production
  constant, and pin the route's one-stderr-line-per-cause invariant
  with a two-misconfiguration case

* fix(web-shell): Close sixth brand review round — saxes parser, lockfiles

Swap fast-xml-parser (subset validator plus pre-decoded attributes) for
saxes, the streaming XML parser jsdom uses: malformed documents are now
refused instead of shipped, attribute values are entity-decoded exactly
once so the xmlns gate cannot be faked by a second decode, and the parse
stream is closed after write so an unclosed root fails at EOF. Regenerate
both lockfiles for the dependency swap — the round-5 fast-xml-parser edge
was never recorded, which is what turned the frozen pnpm installs red —
and pin the refused well-formedness classes and the double-decode fixture
in the resolver tests.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(web-shell): Enable namespace processing in the brand logo parser

saxes was constructed with default options, so an undeclared namespace
prefix on a child (the classic legacy-export xlink:href with no
xmlns:xlink) parsed clean here while browsers fatal on it — the daemon
shipped a data URI no browser renders, with nothing on stderr. Enable
xmlns: true, matching jsdom (browsers always have namespace support),
flatten the object-shaped attribute values that mode returns, and pin
both sides: undeclared child prefixes refuse, implicitly-bound xml: and
properly declared bindings still accept. Mutation-checked: reverting the
option and the flattening turns the new refusal test red.

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-09-10 04:54:45 +00:00
Shaojin Wenandprobe a0f0d38d4c fix(web-shell): avoid duplicate cold session restoration (#11413)
* fix(web-shell): avoid duplicate cold session restoration

* fix(web-shell): expose known capabilities on initial connection

* fix(web-shell): recover from initial workspace discovery failures

* fix(web-shell): preserve discovery errors and stabilize history smoke

* fix(web-shell): retain localized workspace error guidance

---------

Co-authored-by: probe <probe@local>
2026-09-09 22:43:19 +00:00
jinyeandQwen-Coder a7a6addd4e feat(web-shell): add opt-in browser task notifications (#11398)
Co-authored-by: Qwen-Coder <[email protected]>
2026-09-09 05:46:06 +00:00
ytahdnand钉萁 663e55bd29 feat(web-shell): refine file cards and configure drop destination (#11298)
* feat(web-shell): customize artifact icons

* fix(web-shell): preserve artifact icon fallbacks

* fix(web-shell): improve artifact fallbacks

* feat(web-shell): refine file cards and configure drop destination

* fix(web-shell): address file card and drop review feedback

---------

Co-authored-by: 钉萁 <[email protected]>
2026-09-08 02:56:51 +00:00
ytahdnand钉萁 36f2c14f0e feat(web-shell): align sidebar session scopes (#11240)
* feat(web-shell): add composer add menu

* test(web-shell): target composer history search

* feat(web-shell): align sidebar session scopes

* test(web-shell): stabilize sidebar smoke coverage

---------

Co-authored-by: 钉萁 <[email protected]>
2026-09-07 05:51:29 +00:00
db8897d567 fix(export): shrink the exported transcript renderer to what a read-only transcript needs (#11038)
* fix(export): stop inlining the interactive Web Shell runtime in exported HTML

/export html builds document-main.tsx with esbuild bundle:true and inlines
the result into every exported file. The entry imported WebShellTranscript
from the @qwen-code/web-shell package root, whose facade also reaches App,
the daemon providers, the editor/terminal chrome and the full injected
component stylesheet, so each export carried 19,523,259 runtime bytes
(62x the legacy renderer) even for an empty session (#11031).

Add a transcript-only subpath entry (@qwen-code/web-shell/transcript,
mirroring the existing ./daemon-react-sdk subpath and the SDK's
./daemon/transcript pattern) that exports only the read-only transcript
renderer, and point document-main.tsx at it. The entry is built in its own
rollup run so it carries only the stylesheet reachable from transcript
rendering instead of the whole component sheet. Exports stay fully
self-contained: CSP, nonce handling and offline rendering are unchanged.

Also add a hard size budget on the inlined document runtime in the
template build, mirroring the MAX_*_BROWSER_BUNDLE_BYTES assertions in
packages/sdk-typescript/scripts/build.js. The limit sits below the
regression value so a repeat fails the build instead of landing silently.

Measured on the generated document template (empty session):
- before: 19,525,807 bytes raw / 4,775,943 gzip (runtime 19,523,259)
- after:  17,966,485 bytes raw / 4,512,650 gzip (runtime 17,963,937)

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-issue-patrol/jmtn7fhrmj3

* fix(export): drop dead Shiki and CodeMirror from the export runtime

The transcript-only entry stopped the interactive shell from reaching the
inlined document runtime, but two large graphs still did, and the size
budget could not see either of them.

- Shiki (~9.7 MB of pre-minify inputs, the single largest) is unreachable
  in an export: `CodeBlock` returns before touching the highlighter when
  `renderMode === 'document'` and always renders a plain `<pre>`. It is a
  static import of `codeHighlighter.ts`, so esbuild cannot drop it. The
  document build now resolves `shiki` / `@shikijs/*` to a stub. Its
  Oniguruma WASM engine could not have started under the export CSP
  (`script-src 'nonce-…'`, no `'wasm-unsafe-eval'`) in any case.
- CodeMirror (~1 MB) still reached the transcript entry: `UserMessage`
  imported three composer-tag string getters from `hooks/useComposerCore.ts`,
  which pulls the whole editor at top level. The getters move to
  `utils/composerTag.ts` (editor-free, and already imported by
  `UserMessage`); `useComposerCore` re-exports them for existing callers.

Replace the byte-cap-only ratchet with a structural one: the document build
now emits a metafile and fails when a forbidden module graph appears in the
inputs (Shiki, the web-shell package root, CodeMirror), naming the reason.
The byte cap stays as a coarse backstop and still needs re-measuring.

Also fix a latent collision introduced by building the transcript entry in
its own rollup run: both entries injected their stylesheet under the same
`data-qwen-web-shell="component"` key, so a host importing both would keep
whichever loaded first and silently lose the other's rules. Injection is now
keyed per entry, and shadow-root style adoption concatenates every match.

Refs #11031

* docs(verification): add the export-html runtime size verification plan

The change to the inlined `/export html` runtime rests on measurements that
cannot be taken on the machine that wrote it. Rather than leave that as a
caveat in the PR description, commit the plan to the branch so any
build-capable machine gets it by cloning: exact commands, the reference
numbers to compare against and whose machine they came from, the premise the
Shiki removal rests on plus a way to falsify it independently, the two budget
constants that are knowingly stale, and what to report back.

Follows the docs/verification/abort-controller-refactor layout; results belong
next to it as results.md.

Refs #11031

* test(web-shell): scope the transcript entry guard to its JS graph

The not.toContain('vaul') guard ran against the whole bundle, but
injectCssModules prepends the Tailwind-generated stylesheet as a
single-line __qwenWebShellCss constant, and Tailwind v4 compiles
classes from every scanned source file regardless of the entry's
module graph — so drawer.tsx's data-[vaul-drawer-direction=…] variant
selectors land in the transcript entry's CSS even though no transcript
JS imports vaul (verified: 64 'vaul' occurrences in the injected CSS
line, 0 in the JS remainder, no from"vaul" import specifier).

Strip the injected CSS line before the graph guards. If the injection
shape ever changes, the replace() is a no-op and the checks fail
loudly rather than passing falsely.

* fix(export): drop the dead echarts runtime from the export bundle

CI measured the merged branch at 8,456,076 runtime bytes, and named the
inputs: `echarts 3841596` and `zrender 624992` are still the largest single
block in it.

They arrive the same way mermaid does. `@datafe-open/markdown-chart-echarts`
ends `createEChartsRenderer` with

    const loadECharts = options.loadECharts ?? (async () => await import('echarts'));

and this repository never takes that default: `MarkdownChartRenderer` always
passes a `loadECharts`, because `adaptLegacyRuntimeLoader` returns a function
that throws 'Chart runtime is unavailable.' when no `loadEcharts` prop was
supplied, and no call site in `packages/web-shell/client/` supplies one. So the
fallback is dead in this codebase — but a bundler cannot prove that, and the
export build is esbuild `format: 'iife'` with a single outfile, which cannot
code-split, so the dynamic import is flattened straight into the renderer.

Resolves `echarts` to a stub on exactly the reasoning the Shiki stub already
uses in this file — unreachable in document mode — and adds echarts/zrender to
FORBIDDEN_DOCUMENT_INPUTS so it cannot come back unnoticed. Chart blocks in an
exported transcript already could not render; they threw
'Chart runtime is unavailable.' before this change and do the same after.
Whether exports *should* render charts and diagrams is #11091, and this does
not prejudge it: giving the renderer a real runtime stays a deliberate act.

Also lowers the runtime budget from 19,000,000 to 8,700,000 using the CI
measurement above. It is still loose, because the echarts stub in this same
commit lowers the real number again — tighten it from the next green run
rather than from a guess. The structural guard is the load-bearing check.

* fix(web-shell): make the transcript entry's "no daemon providers" claim true

Review found the entry docblock over-claiming. `WebShellTranscript.tsx`
value-imported `transcriptBlocksToLocalizedMessages` from `hooks/useMessages`,
and that module value-imports `useConnection` / `useTranscriptBlocks` /
`useWorkspace` from the `daemon-react-sdk` barrel — so `dist/transcript.js`
still carried the provider guards, including
`useDaemonActions must be used within DaemonSessionProvider`.

Gives that helper the same leaf-module treatment as the composer tags: the
pure projection (and the `Translator` type it needs) moves to
`adapters/localizedMessages.ts`, which imports nothing from the daemon SDK
barrel, and `useMessages.ts` re-exports both so existing callers and its own
test are unaffected. `build-artifact.test.ts` now asserts `dist/transcript.js`
contains neither `DaemonSessionProvider` nor `DaemonWorkspaceProvider`, so the
docblock's claim is pinned rather than asserted in prose.

Also fixes the read-only recipe in `packages/web-shell/README.md`, which still
taught the root import this PR makes wrong — a host following it would ship
the interactive shell, which the reviewer measured as landing a document at
~8.8 MB, past the new cap. Same for the example in the read-only transcript
design doc.

Tightens the runtime budget to 7,400,000 / 7,300,000 warning, from the
reviewer's local measurement of 7,275,173 bytes with the echarts stub in place
(CI's 8,456,076 predates it).

* fix(web-shell): drop the now-unused adapter import from useMessages

The projection that used transcriptBlocksToDaemonMessages moved to
adapters/localizedMessages.ts in 1d94060f5, leaving the import behind.
Lint & Static caught it:

  14:10  error  'transcriptBlocksToDaemonMessages' is defined but never used

* fix(web-shell): assert what the transcript entry delivers, not what it doesn't

The guard added in 1d94060f5 was red against a clean build. Moving
`transcriptBlocksToLocalizedMessages` out of `hooks/useMessages` was correct
but it was one of several paths into the daemon barrel, and not the load-
bearing one: review's static walk of the entry's value-import graph found
three more, through components the transcript genuinely renders —
`useArtifactWorkspaceTarget.ts` (`useWorkspace`, `useWorkspaceActions`),
`McpStatusMessage.tsx` (`useMcp`) and `TasksStatusMessage.tsx` (`useActions`).
Every one of those barrel aliases resolves to a strict hook, so the provider
guards ship no matter which module the import names. Consistent with that,
`dist/transcript.js` came out 90 bytes larger after the extraction, not
smaller.

Asserting `DaemonSessionProvider` is absent therefore asserted something this
entry does not deliver. Replaced with a bound on the transcript JS remainder —
1,140,948 measured bytes against 7,021,715 for `dist/index.js`, ceiling
1,300,000 — which pins the property the entry actually exists for.

The entry docblock now says the same thing: no `App`, no composer, no
editor/terminal chrome, and the daemon hook runtime named as a known residual
with #11100 as its home. That issue also carries the question this raised —
`TasksStatusMessage` calls `useActions()` unconditionally and
`WebShellTranscript` mounts no provider, so whether an export containing a
tasks-status block renders or throws depends on what the document projector
filters, and nobody has checked.

* fix(web-shell): preserve KaTeX cascade in transcript bundle

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
Co-authored-by: yiliang114 <[email protected]>
2026-09-06 06:15:42 +00:00
f4b49b0489 feat(web-shell): expose session artifact snapshots (#10747)
* feat(web-shell): expose session artifact snapshots

* fix(web-shell): re-arm artifact projection after reconnect

* fix(web-shell): deliver unprojected artifacts

---------

Co-authored-by: Shaojin Wen <[email protected]>
Co-authored-by: 易良 <[email protected]>
2026-09-03 09:33:15 +00:00
jinyeandQwen-Coder 6e99d02914 feat(web-shell): Add standalone chats (#10514)
* docs(plans): Add standalone PR6 WebShell UI plan

Implementation plan for the final PR of the standalone daemon sessions
sequence tracked by issue #8908: wiring Home/global New Chat, Recents,
deep links, and project-only surface hiding to the explicit session
contexts merged in #10418.

* docs(plans): Address PR6 plan review feedback

- Standalone creation omits workspace-only fields (sourceType/worktree/branch)
- Capability read is tri-state: loading waits, only loaded-absent falls back
- Pending context stays authoritative until attach commits
- Live current-session New Chat routes through startLive('new')
- Repair offered only for missing directories; compromised is fail-closed
- Recents standalone view model never exposes the internal cwd
- Creating deep links poll to a terminal state with explicit Retry
- Refresh anchors for the #9811 WebShell cutover

* docs(plans): Address second round of PR6 plan review

- Capture prompt-admission working_directory_missing into standalone
  connection state; Repair is owner-guarded and reloads on success
- Cancel stale deep-link resolution with an App-level route token
- Exclude standalone sessions from Split View in PR6
- Clear held composer attachments on context switch; guard at submit
- List archived standalone sessions in a standalone Archived lane with
  explicit refresh after create, prompt completion, and lifecycle actions
- Interpret per-session batch outcome envelopes before updating Recents

* docs(plans): Address third round of PR6 plan review

- Resolve standalone deep links at the root before the session provider
  mounts; cold loads issue zero workspace-load requests
- Give every new-session caller an explicit intent (global / inherit /
  workspace) instead of inferring from undefined cwd
- Block every Split View ingress (footer, ?split=, sessionStorage,
  controlled props), not only Recents rows
- Complete the outcome-unknown recovery state machine (creating /
  existing / absent / unknown) and block submission while unresolved

* docs(plans): Address archived-summary, composer-isolation, and split-classifier review

- Branch cold-link and recovery flows on isArchived: render archived
  state, unarchive with per-id success verification, then load
- Isolate composer commands/skills/atWorkspaceCwd and draft/history
  identity for standalone drafts; no primary fallback
- Specify the fail-closed Split View bare-id classifier before panes mount
- Rewrite project/Goals examples to the explicit intent contract

* docs(plans): Close remaining context-isolation paths in PR6 plan

- Gate workspace effects, command discovery, and submit-time handlers on
  effectiveContext.kind === 'workspace', not only rendered controls
- Disable the legacy global input-history fallback for standalone
  identities
- Give ScheduledTasksDialog.onCreateViaChat an explicit workspace intent

* docs(plans): Close transcript-branching and legacy dialog ingress gaps

- Exclude generic transcript branching (/branch, per-message Branch) from
  standalone contexts; the daemon route is rejectStandalone
- Gate session-overview, resume, delete, and release ingresses from
  non-workspace contexts; standalone /resume routes via explicit context

Co-authored-by: Qwen-Coder <[email protected]>

* docs(plans): Gate the workspace Web Terminal in non-workspace contexts

Terminal availability and the open handler require an explicit workspace
context; inherited terminal tabs are discarded when entering standalone or
Live, and ordinary shell/tool commands in the private directory remain
governed by the session permission pipeline.

Co-authored-by: Qwen-Coder <[email protected]>

* feat(web-shell): add standalone chats

Co-authored-by: Qwen-Coder <[email protected]>

* fix(web-shell): address PR review feedback (#10514)

Co-authored-by: Qwen-Coder <[email protected]>

* fix(web-shell): address PR review feedback (#10514)

Co-authored-by: Qwen-Coder <[email protected]>

* fix(web-shell): address PR review feedback (#10514)

Co-authored-by: Qwen-Coder <[email protected]>

* fix(web-shell): address PR review feedback (#10514)

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-09-01 09:43:35 +00:00
Kewenand恪文 dd5b857c8e feat(web-shell): allow copying terminal turn errors (#10001)
* feat(web-shell): allow copying terminal turn errors

* fix(web-shell): expose turn error copy on touch devices

* test(web-shell): cover turn error action visibility

---------

Co-authored-by: 恪文 <[email protected]>
2026-08-31 09:09:34 +00:00
fe34a5cf22 refactor(vscode-ide-companion): complete the WebShell UI cutover (#9811)
* feat(vscode-ide-companion): reuse WebShell transcript UI behind experimental flag

Bridge ACP session/update notifications into the shared SDK daemon transcript reducer and render the result with the WebShell transcript component, gated on qwen-code.experimental.webShellTranscript (default off).

The WebShell renderer and its heavy transitive dependencies (echarts, mermaid, shiki, codemirror, katex) are lazily loaded via esbuild code splitting, so the default configuration keeps the ~700KB webview bundle unchanged.

* fix(vscode-ide-companion): grant wasm-unsafe-eval for shiki WASM when WebShell transcript enabled

* feat(vscode-ide-companion): adopt WebShell transcript as default timeline

Drop the experimental flag and the legacy MessageList renderer. The companion timeline now always renders through the shared WebShell transcript component, fed by ACP session/update notifications via the SDK daemon transcript reducer (lazy loaded through esbuild code splitting).

The flag-gated wiring is removed: the qwen-code.experimental.webShellTranscript setting, the conditional CSP/body attribute in WebViewContent, and the legacy MessageList path in App.tsx (~850 lines). The webview CSP now grants wasm-unsafe-eval unconditionally for Shiki's Oniguruma WASM.

* fix(vscode-ide-companion): reset WebShell transcript state on session switch

The experimental useAcpTranscript hook only consumed transcriptUpdate
messages, so its reducer state survived session boundaries. When the
extension switched sessions it kept the webview mounted and replayed the
newly-selected session through ACP, causing the previous session's blocks
to merge with the new replay (e.g. user text "alpha" from session A leaked
into session B as "alphabeta").

Reset both the reducer state and the rendered blocks on the same
boundaries the legacy message flow uses: qwenSessionSwitched (sent before
the ACP replay of the selected session) and conversationCleared (new
session). Adds a regression test that replays two sessions with a switch
between them.

* fix(vscode-ide-companion): harden WebShell transcript session boundaries

- reset the transcript on `conversationLoaded` too, closing the same
  cross-session leak the previous commit fixed for `qwenSessionSwitched`
  and `conversationCleared` (agent reconnect posts only this boundary)
- track the active session id and drop late `transcriptUpdate` frames
  whose `sessionId` no longer matches, so a previous session's trailing
  frames cannot contaminate the next session's timeline
- seed the transcript from cached messages carried by
  `qwenSessionSwitched` so offline restores and load-failure fallbacks
  render their history instead of a blank timeline
- dispatch `assistant.done` on `streamEnd`/`sessionLoadComplete` so the
  final assistant/thought block of a turn (or history replay) does not
  stay `streaming: true` forever

* fix(vscode-ide-companion): adopt live ACP session id after load-failure fallback

* fix(vscode-ide-companion): echo user prompt into WebShell transcript

* fix(vscode-ide-companion): keep WebShell transcript expanded and clear of the composer

* fix(vscode-ide-companion): surface local error and interrupt notices in the transcript area

* fix(vscode-ide-companion): restore file-link opening from the WebShell transcript

* fix(vscode-ide-companion): restore contributed copy commands for the WebShell transcript

* fix(vscode-ide-companion): add localOnly marker to TextMessage state type

* fix(vscode-ide-companion): restore /insight progress card and report link in the transcript UI

* fix(vscode-ide-companion): finalize in-flight tools on timeout and pin session-switch seeding guard

Map streamEnd reasons timeout/session_expired onto the reducer's error reason so abandoned mid-tool turns no longer spin forever (ceuI). Add qwenSessionSwitched cases with no messages field and an empty cache array; the no-messages case fails when the seeding guard is forced true, pinning its false side (ceuN).

* fix(vscode-ide-companion): remove unreachable editMessage backend and dead submit options

The user-message edit/rewind UI was dropped in the WebShell-transcript migration, leaving editTargetTurnIndex/onSubmitted options in useMessageSubmit and the full editMessage/rewind flow in SessionMessageHandler unreachable. Remove the dead options, the editMessage dispatch case, the rewind/snapshot flow with its recovery branches, and their tests (R1-8 direction b).

* fix(vscode-ide-companion): drop write-only loadingMessage bookkeeping

The waiting-message renderer was removed with the WebShell transcript migration and the user prompt is echoed into the timeline at send time (bd09e19d86), so the loadingMessage string was write-only dead state. Keep the isWaitingForResponse flag (submit gating / cancel) and pin its API surface (R1-19 direction b).

* fix(vscode-ide-companion): align waiting-flag pin test with the argument-less setter

* fix(vscode-ide-companion): echo attached images into the transcript timeline

The prompt carries pasted/attached images as ACP resource_link blocks,
which the transcript reducer cannot render (no inline data), so user
images vanished from the timeline while the attach path stayed alive.
Read each saved prompt image back from disk and echo it alongside the
text echo as an inline user_message_chunk image part (the daemon-echo
content shape), which the shared reducer folds into the user block and
the WebShell renderer already displays. Unreadable images are skipped
without breaking the send.

* fix(vscode-ide-companion): track live VS Code theme for the transcript

webShellTheme was snapshotted once at mount via useMemo with an empty
dependency array, so switching the VS Code color theme left the
timeline on the stale theme (VS Code updates data-vscode-theme-kind on
<body> in place without reloading the webview). Hold the theme in state
and refresh it with a MutationObserver on the body theme attributes.

* fix(vscode-ide-companion): copy every transcript block kind and map ambiguous row keys

- Copy All Messages now includes tool, shell, user_shell, and status
  blocks via getBlockCopyText, matching the pre-PR copyAllMessages
  handler which included formatted tool calls (review 5001842059 S-1).
- findBlockByRowKey prefers an exact id match and otherwise the longest
  matching block id, so one block id that dash-prefixes a sibling (e.g.
  `a` vs `a-1`) can no longer capture the sibling's row key (S-4).

* fix(vscode-ide-companion): drop whitespace-only cached transcript rows

cachedMessageToNotification rejected empty strings but admitted
whitespace-only content, which the reducer turns into an empty block
when seeding history from cached rows. Reject content that trims to
nothing (review 5001842059 S-2).

* fix(vscode-ide-companion): ship missing third-party notices in NOTICES.txt

Extend generate-notices.js so the regenerated NOTICES.txt carries the
attribution texts it previously only pointed at or dropped:

- Append license files from a package's licenses/ directory (echarts'
  Apache LICENSE references licenses/LICENSE-d3 for its embedded
  d3-derived files; the BSD-3-Clause text is now shipped).
- Append a package's NOTICE file when present (Apache-2.0 §4(d)),
  covering echarts' Apache Software Foundation attribution.
- Accept string-form package.json repository values (full URLs and
  GitHub shorthand) instead of emitting "(No repository found)".
- Fall back to the standard MIT text (copyright holder from package.json
  metadata) for MIT-declared packages that ship no license file.

* fix(vscode-ide-companion): show a recoverable error state when the transcript chunk fails to load

* test(vscode-ide-companion): gate the transcript blocks wiring into the WebShell renderer

* test(vscode-ide-companion): gate the transcriptUpdate forwarding from agent to webview

* docs(vscode): plan complete Web Shell cutover

* refactor(web-shell): own daemon React bindings

* fix(webui): preserve package entry filenames

* refactor(vscode): complete WebShell UI cutover

* chore(vscode): refresh third-party notices

* fix(vscode): fill embedded chat viewport

* test(web-shell): disambiguate workspace visual locator

* fix(vscode): match embedded chat layout to host

* fix(vscode): compact embedded chat styling

* fix(vscode): align embedded chat density with VS Code

* fix(vscode): complete embedded composer integration

* fix(vscode): restore user message editing after cutover

* fix(vscode): complete WebShell feature parity

* test(vscode-ide-companion): repair host-wiring tests for the WebShell cutover

* refactor(vscode-ide-companion): replace webui build scanner with an ESLint boundary rule

The bespoke recursive source scanner reimplemented a dependency-boundary
check on every extension build. A scoped no-restricted-imports rule
enforces the same boundary on every lint run with less custom code; the
manifest dependency entry was already removed by the cutover.

* fix(web-shell): keep ChatEditor commands prop referentially stable (#9811)

The `additionalSlashCommands = []` destructure default allocated a fresh
array on every App render, invalidating the `commands` useMemo and breaking
ChatEditor memoization on every transcript-only re-render. Default to a
module-level constant instead, matching the existing EMPTY_* convention.

Also align the /skills completion expectation with the autoSubmit field the
completion source intentionally emits for leaf skill items.

* fix(vscode): distinguish the VS Code channel and localize its chrome

The companion now drives Web Shell against a shared `qwen serve` daemon,
so the CLI, the browser Web Shell, and this extension all create sessions
in the same workspace catalog. Web Shell recorded `'default'` for every
surface, leaving VS Code conversations indistinguishable from terminal and
browser ones — the panel's history listed sessions the user never opened
here, and nothing attributed a session back to the editor.

Give Web Shell a `sessionSourceType` prop (defaulting to today's
`'default'`) and have the companion stamp `'vscode'` on the sessions it
creates, then scope the history dropdown to that source. The host also
supplies a stable daemon `clientId`, which the bootstrap previously
declared but never sent.

Web Shell localizes its own surface from the `language` signal while the
companion's chrome was hardcoded English, so a zh-CN panel rendered a
Chinese transcript under an English header, history dropdown, onboarding
screen, and account dialog. Route that chrome through a small string table
driven by the same signal, including the host-only slash entries.

Also fix accessibility defects in the history dropdown: rename and delete
were revealed on hover alone and unreachable by keyboard, date headers sat
inside `role="listbox"` as invalid non-option children, arrow-key roving
stopped at group boundaries, `aria-modal` had no focus trap, and a primed
"Delete?" survived both search changes and the pointer leaving the row.

Formatting: `FileMessageHandler` and `SessionMessageHandler` were left
unformatted earlier in this branch and failed the Prettier gate.

* refactor(vscode): drop code orphaned by the WebShell cutover

The webview entry now renders EmbeddedApp against the daemon, which left
the ACP-era hook layer unreachable: nothing imports acpTranscriptAdapter,
useWebViewMessages, useAcpTranscript, useToolCalls, useSessionManagement,
useMessageHandling, useFileContext, useImage, or the permissionTypes added
by this branch. A reachability walk from webview/index.tsx reaches eight
modules; every reference to the rest comes from inside the orphaned set
itself, so it deletes as a closed unit.

EmbeddedWebShell goes with them. It was the host-driven entry point from
the earlier stage of this branch, superseded when EmbeddedApp moved to
WebShellWithProviders, and has had no consumer since — only its own DOM
test and a barrel export.

Also harden the daemon process lifecycle. `start()` returned the cached
runtime without comparing the workspace, so in a multi-root window the
second folder's chat silently reused a daemon bound to the first and
scoped every session, history page, and prompt to the wrong root. Bind the
daemon to its workspace and respawn on a change, keep a superseded child's
late exit from tearing down its successor, and report a post-startup exit
to the webview instead of leaving it fetching against a dead port.

* docs(vscode): describe the daemon architecture the cutover actually ships

The design doc still recorded the plan this branch started from: keep ACP
as the runtime boundary, add no daemon server or loopback port, and treat
"replacing ACP with daemon HTTP/SSE" as a non-goal. The final stage did
exactly that, so the document argued against the code beneath it.

Record the decision and its consequences instead — two processes per
workspace, a daemon shared with the CLI and browser Web Shell, the vscode
source type that keeps the panel's history its own, workspace rebinding in
multi-root windows, and the turn-driven host features that stopped firing.

* fix(vscode): repair round-2 review findings on the web-shell cutover (#9811)

- closeDiff now resolves workspace-relative paths the same way showDiff
  does, so permission-cycle diffs opened from daemon-relative paths can
  actually be matched and closed
- a superseded or disposed daemon child no longer reports its exit as a
  crash of the live daemon
- authCancelled no longer hides an already-authenticated session behind
  onboarding; only an unknown auth state settles to unauthenticated
- selection-only activeEditorChanged events no longer undo an explicit
  active-file exclusion
- prepareSubmit dedupes mentions in both path spaces and matches typed
  references on a whole-reference boundary
- permission diffs open only from the SDK's authoritative file_diff
  preview (writes included, model-controlled toolCall mining removed)
- the webview HTML carries VS Code's locale so chrome strings localize
- discontinued qwen-oauth models are no longer re-applied through the
  new-session initial-model route

* fix(vscode): repair round-3 critical findings on the web-shell cutover (#9811)

Co-authored-by: Qwen-Coder <[email protected]>

* fix(web-shell): import daemon-react-sdk from web-shell instead of webui

The cutover branch dropped the ./daemon-react-sdk export from @qwen-code/webui,
but the TerminalPanel merged in from main still imports it, breaking the
web-shell vite build (Missing "./daemon-react-sdk" specifier). Point the import
and its test mock at @qwen-code/web-shell/daemon-react-sdk, which re-exports the
same useWorkspace hook and matches every other web-shell call site.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(vscode): close WebShell UI regression gaps

Co-authored-by: Qwen-Coder <[email protected]>

* fix(vscode): initialize WebShell refs explicitly

Co-authored-by: Qwen-Coder <[email protected]>

* fix(vscode): narrow queued prompt edits

Co-authored-by: Qwen-Coder <[email protected]>

* fix(release): enumerate actual npm workspaces

Co-authored-by: Qwen-Coder <[email protected]>

* fix(web-shell): include hasOlderHistory in the render-item callback deps

The renderItem useCallback reads hasOlderHistory to gate the edit action
but omitted it from its dependency array, failing CI's
react-hooks/exhaustive-deps gate.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(web-shell): report each connection error once to stop the inline onError re-render loop (#10454)

* fix(web-shell): report each connection error once to stop the onError re-render loop

While a connection error persists (e.g. the daemon is unreachable), the
error-notification effect re-fires whenever the onError callback identity
changes. Hosts such as the VS Code embedded app pass an inline onError and
update their own state when it fires, so every notification triggers a host
re-render that hands the effect a fresh callback identity — re-notifying the
same persistent error forever (#10406).

Track the last reported connection.error value in a ref and notify only when
the value changes, resetting the tracker once the connection recovers. This
guards every inline-callback consumer, not just memoized hosts.

Fixes #10406

Co-authored-by: Qwen-Coder <[email protected]>

* fix(web-shell): only stamp the dedup ref once an onError handler exists

Stamping lastReportedConnectionErrorRef before delivery meant a host
that attaches onError after a persistent connection error appeared never
received it: the no-op delivery already marked the error as reported.
Guard on the handler first and add a regression test covering the
late-attach case (red when the guard is removed).

Co-authored-by: Qwen-Coder <[email protected]>

* docs(web-shell): document the onError dedup contract and fix comment wording

Describe the reported-once-per-distinct-error semantics, the reset on
recovery, and that replacing the handler mid-error does not re-deliver.
Reword the effect and test comments to describe the host class instead
of naming the VS Code embedded app, which passes a useCallback handler.

Co-authored-by: Qwen-Coder <[email protected]>

* test(vscode): mirror the web-shell value-dedup in the EmbeddedApp mock

The WebShellWithProviders mock re-notified on every onError identity
change, mirroring the loop App.tsx can no longer produce. Rewrite it to
report each distinct error value once (resetting on recovery), keep the
loop guard as a regression tripwire, exercise it with a changing
callback identity plus a post-delivery effect re-run, and refresh the
handleShellError comment that still cited the old loop as the
memoization reason.

Co-authored-by: Qwen-Coder <[email protected]>

* test(vscode): cast the captured onError prop for the mock wrapper

CapturedProps is an unknown index signature, so the destructured
onError needs the same cast the previous mock applied inline to stay
callable under tsc.

Co-authored-by: Qwen-Coder <[email protected]>

* test(vscode): bail the EmbeddedApp mock before stamping when no onError exists

The mirrored dedup effect stamped lastReportedError and counted a
notification even when no handler was attached, while App.tsx returns
before stamping on that path. Add the same early return so a handler
attached mid-error still receives the persistent error, and pin the
no-handler no-stamp behavior with a test that fails if the guard is
removed.

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>

* fix(web-shell): remove duplicate history dependency

Co-authored-by: Qwen-Coder <[email protected]>

* fix(vscode): close remaining WebShell cutover regressions

Co-authored-by: Qwen-Coder <[email protected]>

* fix(vscode): keep permission diff handling host-scoped

Co-authored-by: Qwen-Coder <[email protected]>

* test(vscode): remove orphaned completion trigger test

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Shaojin Wen <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: yiliang114 <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-08-29 16:37:47 +00:00
dreamWBand易良 6e878f539c feat(web-shell): allow hiding the session source switch (#10079)
* feat(web-shell): allow hiding the session source switch

* fix(web-shell): reset hidden session source state

* fix(web-shell): keep source state aligned before commit

---------

Co-authored-by: 易良 <[email protected]>
2026-08-27 06:02:11 +00:00
9a64c0a963 feat(serve): add pollable daemon turn status (#9080)
* feat(serve): add pollable turn-status endpoints for daemon sessions

Add GET /session/:id/turns/current and GET /session/:id/turns/:promptId
so external callers can poll a turn's lifecycle state (queued / running /
completed / cancelled / error) and result instead of holding the SSE
stream for the whole turn lifetime.

- Live state comes from the bridge's pending prompt queue; settled
  outcomes from persisted turn_result transcript records, so results
  survive daemon restarts and the daemon keeps no per-turn memory
- Each prompt captures its own recording and settles exactly that one,
  so overlapping turns (DAEMON-003 deadline overlap) can never
  misattribute one turn's outcome to another promptId
- Enforces the same client authorization as POST /session/:id/prompt

Refs #8680

* test(serve): update telemetry route count

* fix(serve): prefer settled turn outcome over deadline error overlay

When the prompt-deadline path latches an error terminal in the overlay and the child later settles and persists a non-error turn_result for the same promptId, the poll surface previously kept the overlay error while enriching it with the successful resultText, and flipped to completed only after overlay eviction or restart. Merge via mergeTerminalWithPersisted at the two enrich call sites so the persisted outcome supersedes a bridge-synthesized error terminal once it exists; the exactly-once turn_error event publication and FIFO release are unchanged. The different-promptId endedAt tie-break is intentionally untouched.

* fix(serve): pin turn-start session identity for turn-result settle

R4-1: settle resolved the ChatRecordingService at settle time, so a startNewSession rotation mid-turn could land the turn_result record in the new session's transcript while the poll surface kept reading the old one. Capture the recorder at turn start and settle on that instance; pin the outgoing service's session identity at rotation so the late append keeps the pre-rotation sessionId.

R4-2: reject empty error.message/error.code in turn_result payloads, mirroring the existing empty-promptId rejection.

Adds rotation/pin regression tests plus the round-4 test suggestions (extractor fallback, startedAt, cancel/error race matrix, resultCode defaulting, removed-prompt projections).

* fix(serve): enforce the turn_result bounded contract on the write path

R3-3: cap promptId, stopReason, and originatorClientId at 256 chars in isTurnResultRecordPayload, closing the unbounded echo of corrupted-transcript values through GET /session/:id/turns/:promptId; recordTurnResult now validates payloads against the same contract before appending, so type-correct but invalid shapes (error state without error, error on non-error states) can no longer produce records invisible to the restart scan.

Also lands the four round-5 test assertions: merged-payload error-leak pin, multi-model-call settle count, successor attribution in the superseded-throws test, and the early session-mismatch guard pin.

* fix(serve): address round-6 review findings on daemon turn status

- Session: settle a successor-aborted turn as cancelled only when the
  thrown error is the abort itself; genuine failures after a NEW_PROMPT
  abort surface as error, matching the send-loop contract
- bridge: serve repeat polls of a settled promptId from the enriched
  overlay instead of re-scanning the child transcript, and give the
  turn-status read the transcript timeout instead of the 10s init default
- bridge: forward the channel display text unchanged; Session treats an
  empty display text as absent for the turn record ([image] fallback)
- Session: cap streamed-response accumulation for turns without a
  channel delivery at the turn-result bound
- docs: document the bounded non-monotonicity of poll terminals

* fix(serve): guard turn-status reads against rewind races and keep the trusted prompt projection

A successful rewind that completes while a getSessionTurnStatus child
transcript scan is in flight could let the pre-rewind record be cached
into the freshly cleared overlay and served forever. Track a per-session
rewind generation captured before the scan and discard the scanned
outcome when it moved.

enrichTerminalTurnStatus and the deadline-supersede merge returned the
child-recorded promptText ahead of the bridge's trusted display
projection, leaking hidden channel context on the poll surface. Make
promptText/promptTextTruncated backfill-only and keep the terminal's
projection in the supersede path. Make the pinning test adversarial and
correct a false comment about the child's ''-as-absent fallback.

---------

Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: qqqys <[email protected]>
2026-08-18 06:35:17 +00:00
ytahdnand钉萁 addde7c1d0 feat(web-shell): decouple composer from catch-up and rebuild SSE on disconnected submit (#9323)
Catch-up (SSE history replay) no longer disables the composer input or
swaps its placeholder to "加载中": replaying history does not conflict
with typing, and a stuck catch-up must never lock the input.

A prompt submitted while the SSE stream is down is no longer blocked with
a toast: admission rebuilds the stream immediately by aborting the
reconnect backoff and resuming via Last-Event-ID, keeping the session
handle intact.

Drops the now-unreachable 'loading' placeholder state from the public
WebShellComposerPlaceholderState type and syncs docs (README, design doc,
provider and entry type comments).

Co-authored-by: 钉萁 <[email protected]>
2026-08-17 06:39:23 +00:00
0a3098a279 feat(web-shell): add contextual task panels (#7929)
* feat(web-shell): add contextual task panels

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: 钉萁 <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: qwen-code-ci-bot <[email protected]>
Co-authored-by: Qwen Code Autofix <[email protected]>
Co-authored-by: Qwen Code Bot <[email protected]>
2026-07-30 13:45:30 +00:00
ytahdnand钉萁 a95510cb7c fix(web-shell): translate legacy skills invocations (#8103)
Co-authored-by: 钉萁 <[email protected]>
2026-07-30 07:42:00 +00:00
zhangxy-zju 1d55d290a0 feat(web-shell): render streaming charts with markdown-chart (#7916)
* feat(web-shell): use markdown-chart for streaming charts

* fix(web-shell): address markdown chart review feedback

* fix(web-shell): preserve legacy chart ref caching

* fix(web-shell): preserve chart safety and loader stability

* test(web-shell): strengthen markdown chart safety contracts

* test(web-shell): cover legacy chart streaming adapter
2026-07-29 06:04:56 +00:00
ytahdnandytahdn 74a786dac3 fix(web-shell): isolate slash command plugin pages (#7581)
Co-authored-by: ytahdn <[email protected]>
2026-07-23 08:39:10 +00:00
ytahdnand钉萁 fee601ae77 feat(web-shell): add selective shadow DOM isolation (#7551)
Co-authored-by: 钉萁 <[email protected]>
2026-07-23 02:54:12 +00:00
3586f48171 feat(web-shell): support custom slash command actions (#7267)
* feat(web-shell): support custom slash command actions

* fix(web-shell): apply slash handlers consistently

---------

Co-authored-by: 钉萁 <[email protected]>
Co-authored-by: 易良 <[email protected]>
2026-07-20 11:23:55 +00:00
Changxiao Ruanandqwen-code-dev-bot 9b6da8e604 feat(webshell): replay ChatRecord history in readonly WebShell (#6999)
* feat(web-shell): add readonly transcript renderer

* feat(transcript): project chat records to daemon transcript

* chore: remove unrelated merge changes

* fix(cli): align transcript replay test mock

* fix(transcript): preserve replay metadata and todo context

Keep vision disclosures and assistant usage visible across tool replay boundaries.

Propagate plan tool-call identity through daemon projection and provide Todo contexts in WebShell so snapshots remain independent. Accept session_source records and cover the cross-layer behavior with regression tests.

* docs(transcript): translate designs and remove benchmark table

---------

Co-authored-by: qwen-code-dev-bot <[email protected]>
2026-07-19 00:09:44 +00:00
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: 钉萁 <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
2026-07-17 16:13:39 +00:00
ytahdnand钉萁 0c6212c0b0 feat(web-shell): add workspace path lock (#6853)
Co-authored-by: 钉萁 <[email protected]>
2026-07-14 06:24:52 +00:00
Shaojin Wen 71c7e448f0 feat(web-shell): editable user-scope settings and in-panel model management (#6768)
* feat(web-shell): editable user-scope settings and in-panel model management

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

- Model-fallbacks max-limit options use aria-disabled instead of the
  native disabled attribute so they stay hoverable and can surface the
  "limit reached" title (disabled buttons fire no events); the toggle
  handler already no-ops at the max. CSS updated to match.
- Inline delete confirmation dismisses on Escape (the conventional
  gesture) so keyboard users need not Tab to Cancel.
- scopeToWire throws on an unexpected SettingScope instead of silently
  reporting it as 'user'.
- Re-export DaemonWorkspaceProviderCurrent from the webui daemon facade
  alongside the sibling provider types.
2026-07-13 14:44:54 +00:00