qwen-code/scripts/verify-capture.mjs
易良 077c57d200
refactor!: retire @qwen-code/webui (#9812)
* feat(transcript): add cross-host document export pipeline

Establish a shared transcript model and document-mode projection so Web
Shell, VS Code, and HTML export can consume the same stable conversation
semantics without changing interactive rendering.

- Preserve daemon and ACP segment identity across replay and normalization
- Add export-safe previews and a versioned transcript document builder
- Add document-mode Web Shell rendering with bounded Mermaid processing
- Lock direct-daemon and ACP behavior with contract fixtures
- Cover render and export equivalence in integration tests

* feat(transcript): wire real VS Code and HTML export consumers

Move transcript prevalidation onto real product paths while preserving
the legacy VS Code timeline as the default fallback.

- Add version-bound document HTML with strict CSP and schema checks
- Add opt-in ACP projection with stable source identity
- Reuse product export code across hosts and the integration runner
- Remove test-only production probes and consolidate shared validation

Note: Browser, host-action, VSIX, and packaging gates remain pending.

* build(deps): sync transcript workspace dependencies

Keep the npm lockfile aligned with the VS Code and HTML export package
manifests added by the transcript consumer migration.

- Lock the VS Code Web Shell workspace dependency
- Lock Web Templates SDK, Web Shell, and React build dependencies

* 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

* fix(transcript): harden export and identity paths

Resolve review findings across document export and the VS Code ACP
timeline while preserving default interactive and readonly semantics.

- Preserve stable text and non-text identity across live and replay
- Harden export projection, budgets, URL handling, CSP, and nonces
- Make document rendering complete, inert, and browser-validated
- Keep VS Code transcript scope, theme, copy, and flags reactive
- Restore fixture, schema, hash, and compatibility contract locks

Note: The overall gate remains failed pending VSIX, host-action, and
packaged-artifact evidence.

* 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(transcript): harden identity and document export

Close the latest review findings across transcript identity, VS Code
rehydration, and the HTML export security boundary.

- Terminate discrete ACP segments and bind automatic turn provenance
- Reset VS Code transcript scopes across replay and reconnect lifecycles
- Align Markdown sanitization and envelope budgets with document rendering
- Run browser gates with Chromium and complete third-party notices

* 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(transcript): simplify adapters and preserve segment boundaries

Reduce the shared transcript review surface while keeping the VS Code and
HTML Export consumers and their security gates intact.

- Merge VS Code feature state into the ACP transcript hook
- Collect source identity once and strip it at the compatibility boundary
- Remove test-only export adapters, gate reports, and duplicate helpers
- Keep shell output separated when stable producer segments change
- Mark third-party notices as generated review content

* fix(transcript): address cross-host review regressions

* 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

* docs(webui): plan legacy package retirement

* docs(webui): link cutover prerequisite

* 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

* fix(transcript): harden export sanitization and user identity

Close the latest review findings around document resource safety and
recorded-user replay consistency.

- Redact local home paths structurally without corrupting remote URLs
- Sanitize nested Markdown images inside otherwise safe links
- Advance merged segment provenance without duplicating separators
- Keep recorded-user stable IDs anchored to durable record identity

* 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

* docs: clarify webui retirement prerequisites

* fix(transcript): resolve export and CI blockers

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

* fix(vscode): compact embedded chat styling

* test(ci): cover dual Playwright installs

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

* fix(vscode): complete embedded composer integration

* refactor: retire legacy webui package

* chore: refresh lockfile after webui removal

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

* refactor: narrow webui retirement to export and removal

* 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 <qwen-coder@alibabacloud.com>

* 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 <qwen-coder@alibabacloud.com>

* fix(vscode): close WebShell UI regression gaps

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

* fix(vscode): initialize WebShell refs explicitly

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

* fix(vscode): narrow queued prompt edits

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

* fix(release): enumerate actual npm workspaces

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

* 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 <qwen-coder@alibabacloud.com>

* 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 <qwen-coder@alibabacloud.com>

* 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 <qwen-coder@alibabacloud.com>

* 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 <qwen-coder@alibabacloud.com>

* 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 <qwen-coder@alibabacloud.com>

* 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 <qwen-coder@alibabacloud.com>

* 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 <qwen-coder@alibabacloud.com>

---------

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

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

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

* fix(vscode): close remaining WebShell cutover regressions

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

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

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

* test(vscode): remove orphaned completion trigger test

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

* test(web-shell): update daemon SDK mock import

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

* fix(export): reject unsupported legacy JSONL

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

* docs(transcript): record the legacy HTML renderer deletion evidence

The contract prevalidation doc is this design's single normative source
(§0.1), so retiring the legacy renderer has to be written back into it.
Three places had drifted:

- §2.1's consumer table still said the HTML Export path "keeps legacy
  compatibility for public calls without records". `toHtml` now requires
  records and the legacy branch is gone.
- §14's completion gate said the legacy HTML renderer is removed only once
  there is deletion evidence. New §12.4 records that evidence — no product
  consumer left, signature tightened, fallback explicitly rejected rather
  than silently degraded, and a `check:no-webui` guard against
  reintroduction — and keeps the VS Code legacy timeline half of the gate
  untouched.
- §10.7 promised credential removal without stating its scope. Spell out
  that it is http(s) only, and that non-navigable schemes, bare flag
  credentials and `code` / `inlineCode` nodes ship verbatim, so the
  document boundary is not read as a general secret scanner.

Also comment the two export format signatures: `records` is required
because HTML projects from original records, and the other formatters
ignore the argument.

Docs and comments only; no build or test was run locally.

* fix(docs): correct web-shell SDK hook names, daemon dir table, followup wiring

- daemon-client-adapters/web-shell.md: use the names the barrel actually
  exports (useActions/useConnection/usePendingPermissions/
  useTranscriptBlocks) in the import example and minimal React shape
- daemon/14-cli-tui-adapter.md: rewrite the
  packages/web-shell/client/daemon/ file table to the real layout
  (session/DaemonSessionProvider.tsx; the transcript adapter now lives
  in client/adapters/ and only exports extractPendingPermission)
- users/features/followup-suggestions.md: suggestion generation is
  automatic server-side; Web Shell hosts need no trigger wiring

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtngpuscjl

* fix(scripts): catch bare packages/webui references in check:no-webui

Widen the second forbidden pattern to /packages\/webui\b/g so
references followed by spaces, punctuation, or end-of-line are flagged
(CI step text, YAML list items, parenthesized mentions), while
lookalikes such as packages/webuix stay clean. Extract the detection
into containsForbiddenReference() and pin it with scripts/tests
fixtures, following the check-tui-dep-direction export convention.
The current tree still scans clean:
node scripts/check-no-webui-dependency.js exits 0.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtngpuscjl

* fix(docs): drop dead Adapter Matrix citation and stale Chromatic line

- 16-vscode-ide-adapter.md: 01-architecture.md has no "Adapter Matrix"
  section (headings verified at HEAD); drop the dead citation tail and
  keep the verified embedding claim (R1-11).
- terminal-capture/motivation.md: Chromatic retired with the webui
  package; point the "complement" item at the Playwright-based Web Shell
  visual tests already shown in this file's diagram (R1-12).

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtntktgsk8

* fix(comments): correct stale webui-era notes in SDK test and followup state

- daemonUi.test.ts: the referenced previewMarkdown/rawOutput preservation
  test does not exist in web-shell (transcriptAdapter.test.ts only covers
  extractPendingPermission); rewrite the note to say the enrichment path
  retired with the webui package instead of citing a nonexistent test
  (R1-6). Wording avoids the retired package literal so check:no-webui
  still passes.
- followupState.ts: no web-shell file imports this module (its daemon
  followup hook keeps its own FollowupState/controller; the only
  createFollowupController consumer is the CLI Ink hook); fix the module
  purpose comment and the createFollowupController docblock accordingly
  (R1-13).

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtntktgsk8

* fix(export): load transcript renderer from unpkg (#11035)

* fix(export): load transcript renderer from CDN

* fix(export): harden CDN renderer loading

* fix(export): host transcript renderer on project OSS

* fix(export): serve npm renderer through unpkg

* fix(release): verify export renderer before VSIX packaging

* docs(daemon): retire the stale ACPAdapter and web-ui.md pointers

R1-2/R1-4 from the review round: the Consumers note still claimed the old
ACPAdapter host postMessage path "remains available" while this PR deletes
the only ACPAdapter, and the migration-status bullet still pointed at
web-ui.md after this PR renamed it to web-shell.md. Both lines now state
what ships: the postMessage path retired with packages/webui (webviews
embed Web Shell, matching 16-vscode-ide-adapter.md), and the adapter
design doc is web-shell.md.

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

* docs: avoid retired webui path reference

* fix: close WebUI retirement review gaps

---------

Co-authored-by: heyang.why <heyang.why@alibaba-inc.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: yiliang114 <jinjing.zzj@gmail.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-09-05 09:13:54 +00:00

296 lines
10 KiB
JavaScript

#!/usr/bin/env node
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Render a command's terminal output to a PNG, for `/verify` evidence images.
*
* Why this exists: the `verify-pr` skill asked the agent to assemble its own
* node-pty -> xterm.js -> Playwright pipeline. Those dependencies do resolve
* from this repo (node-pty is a root optionalDependency shipping prebuilt
* binaries; playwright is declared at the repository root and in
* integration-tests/terminal-capture), but the route needs a browser, is slow,
* and — the real fragility — integration-tests/terminal-capture is not a root
* workspace, so its package.json is never installed as a unit and resolves only
* because every dependency happens to be hoisted. Four live runs still produced
* zero images. This makes a capture one fast command on deps already installed:
* no browser, no pseudo-terminal.
*
* Pipeline: run the command, feed its bytes to @xterm/headless (which parses
* ANSI into a cell grid with colour and bold attributes), emit that grid as
* SVG, and let sharp rasterise it. No browser, no pseudo-terminal.
*
* Usage:
* node scripts/verify-capture.mjs --out evidence/01-ab.png -- npm test -w pkg
* some-harness | node scripts/verify-capture.mjs --out evidence/02-matrix.png
*
* Options:
* --out <path> required; parent dirs are created
* --cols <n> terminal width (default 100)
* --rows <n> max rows kept (default 40, trailing blanks trimmed)
* --title <text> caption drawn above the output
*
* Exit codes: 0 on a written PNG, 1 on usage or render failure. The captured
* command's own exit code is reported on stderr but does NOT fail the capture —
* a failing command is usually exactly what is being captured.
*/
import { spawnSync } from 'node:child_process';
import { mkdirSync, readFileSync } from 'node:fs';
import { createRequire } from 'node:module';
import { dirname, resolve } from 'node:path';
const require = createRequire(import.meta.url);
// xterm's headless build is CommonJS; a named ESM import of `Terminal` throws.
const { Terminal } = require('@xterm/headless');
const sharp = require('sharp');
// The 16 ANSI colours as xterm reports them from getFgColor()/getBgColor().
const ANSI = [
'#1e1e1e',
'#cd3131',
'#0dbc79',
'#e5e510',
'#2472c8',
'#bc3fbc',
'#11a8cd',
'#e5e5e5',
'#666666',
'#f14c4c',
'#23d18b',
'#f5f543',
'#3b8eea',
'#d670d6',
'#29b8db',
'#ffffff',
];
const FG_DEFAULT = '#d4d4d4';
const BG = '#1e1e1e';
const CELL_W = 8.4;
const CELL_H = 18;
const PAD = 12;
const FONT_SIZE = 14;
function usage(message) {
process.stderr.write(`verify-capture: ${message}\n`);
process.stderr.write(
'usage: verify-capture.mjs --out <png> [--cols n] [--rows n] [--title s] [-- cmd ...]\n',
);
process.exit(1);
}
function parseArgs(argv) {
const opts = { cols: 100, rows: 40, out: '', title: '' };
const cmd = [];
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
if (arg === '--') {
cmd.push(...argv.slice(i + 1));
break;
}
const next = () => {
i += 1;
if (i >= argv.length) usage(`${arg} needs a value`);
return argv[i];
};
switch (arg) {
case '--out':
opts.out = next();
break;
case '--cols':
opts.cols = Number(next());
break;
case '--rows':
opts.rows = Number(next());
break;
case '--title':
opts.title = next();
break;
default:
usage(`unknown option ${arg}`);
}
}
if (!opts.out) usage('--out is required');
// Guard the geometry: a NaN or absurd value would otherwise reach sharp as a
// broken SVG and fail with something unrelated to the real mistake.
for (const key of ['cols', 'rows']) {
if (!Number.isInteger(opts[key]) || opts[key] < 1 || opts[key] > 500) {
usage(`--${key} must be an integer between 1 and 500`);
}
}
return { opts, cmd };
}
const escapeXml = (s) =>
s.replace(
/[<>&"]/g,
(c) => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;' })[c],
);
/** Collect the bytes to render: either a child command's output, or stdin. */
function collectOutput(cmd) {
if (cmd.length === 0) {
if (process.stdin.isTTY)
usage('no command given and nothing piped to stdin');
try {
return readFileSync(0, 'utf8');
} catch {
usage('no command given and stdin is empty');
}
}
const env = { ...process.env };
delete env.NO_COLOR;
const res = spawnSync(cmd[0], cmd.slice(1), {
encoding: 'utf8',
maxBuffer: 32 * 1024 * 1024,
// Ask for colour without a pty: most tools honour one of these, and a
// purpose-built harness emits ANSI unconditionally anyway.
env: { ...env, FORCE_COLOR: '1', CLICOLOR_FORCE: '1' },
});
if (res.error) {
process.stderr.write(`verify-capture: ${res.error.message}\n`);
process.exit(1);
}
// A non-zero exit is not a capture failure — capturing a failing base arm is
// the normal case for an A/B cell. A signal-killed child has status === null,
// so name the signal rather than printing "exited null".
const how =
res.signal != null ? `killed by ${res.signal}` : `exited ${res.status}`;
process.stderr.write(`verify-capture: command ${how}\n`);
// Only insert a separator when stdout lacks a trailing newline; a
// console.log-terminated stdout already ends with \n, and join('\n') would
// add a phantom blank row that never appeared on the real terminal.
return res.stdout && res.stderr && !res.stdout.endsWith('\n')
? res.stdout + '\n' + res.stderr
: res.stdout + res.stderr;
}
/** Parse ANSI into a cell grid, then emit it as SVG. */
async function render(raw, opts) {
const term = new Terminal({
cols: opts.cols,
rows: opts.rows,
scrollback: 0,
allowProposedApi: true,
});
// U+FE0F (emoji variation selector) makes Pango abort() in native code when
// no colour-emoji font exists — uncatchable here — so strip it; the base
// codepoint still renders. CRLF is required or a bare LF leaves the cursor in
// the old column and indents every later line. Await xterm's write callback
// (fires once the parser has drained the input) rather than a fixed sleep, so
// a large capture is not read mid-parse and silently come out blank.
await new Promise((r) =>
term.write(raw.replace(/\uFE0F/g, '').replace(/\r?\n/g, '\r\n'), r),
);
const buf = term.buffer.active;
const rows = [];
for (let y = 0; y < opts.rows; y += 1) {
const line = buf.getLine(y);
if (!line) break;
const cells = [];
for (let x = 0; x < opts.cols; x += 1) {
const cell = line.getCell(x);
const chars = cell?.getChars();
if (!chars) continue;
cells.push({
x,
chars,
fg: cell.getFgColor(),
bold: cell.isBold() !== 0,
blank: chars === ' ',
});
}
rows.push(cells);
}
// Trim trailing blank rows so a 40-row default does not pad every capture
// with empty space.
while (rows.length > 0 && rows.at(-1).every((c) => c.blank)) rows.pop();
if (rows.length === 0) {
process.stderr.write('verify-capture: nothing to render (empty output)\n');
process.exit(1);
}
const titleRows = opts.title ? 1 : 0;
const width = Math.round(PAD * 2 + opts.cols * CELL_W);
const height = PAD * 2 + (rows.length + titleRows) * CELL_H;
let body = '';
if (opts.title) {
body +=
`<text x="${PAD}" y="${PAD + CELL_H - 5}" fill="#9cdcfe" ` +
`font-weight="bold">${escapeXml(opts.title)}</text>`;
}
rows.forEach((cells, y) => {
const baseline = PAD + (y + titleRows + 1) * CELL_H - 5;
for (const cell of cells) {
if (cell.blank) continue;
const mapped =
cell.fg >= 0 && cell.fg < ANSI.length ? ANSI[cell.fg] : FG_DEFAULT;
// SGR 30 maps to #1e1e1e — identical to the canvas BG — so black-foreground
// text (the normal way to label a coloured badge, e.g. vitest's project
// badge) would vanish as black-on-black; lift it to the default grey.
const colour = mapped === BG ? FG_DEFAULT : mapped;
body +=
`<text x="${(PAD + cell.x * CELL_W).toFixed(1)}" y="${baseline}" ` +
`fill="${colour}"${cell.bold ? ' font-weight="bold"' : ''}>` +
`${escapeXml(cell.chars)}</text>`;
}
});
const svg =
`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">` +
`<rect width="${width}" height="${height}" fill="${BG}"/>` +
`<g font-family="DejaVu Sans Mono,Menlo,Consolas,monospace" ` +
`font-size="${FONT_SIZE}" xml:space="preserve">${body}</g></svg>`;
const out = resolve(opts.out);
mkdirSync(dirname(out), { recursive: true });
const info = await sharp(Buffer.from(svg))
.png({ compressionLevel: 9 })
.toFile(out);
// scrollback: 0 keeps only the last --rows rows, so a taller input loses its
// top — often the header — with no visible sign; say so rather than shipping
// an image that looks complete but starts halfway down. Count wrapped rows
// (a line wider than --cols occupies ceil(len / cols) terminal rows) so the
// warning also fires when wrapping, not just newlines, pushes past --rows.
const ESC = String.fromCharCode(27);
const stripAnsi = (s) =>
s.replace(new RegExp(`${ESC}\\[[0-9;]*[a-zA-Z]`, 'g'), '');
const wrappedRows = raw
.replace(/\r?\n$/, '')
.split(/\r?\n/)
.reduce(
(sum, line) =>
sum + Math.max(1, Math.ceil(stripAnsi(line).length / opts.cols)),
0,
);
// Newline-terminated output needs one row BEYOND its last line: the final
// CRLF advances the cursor off the viewport and scrolls one row away, so with
// scrollback: 0 the usable capacity is rows - 1, not rows. Comparing against
// opts.rows instead let input of exactly --rows lines lose its top silently —
// the very case this warning exists for.
const capacity = /\r?\n$/.test(raw) ? opts.rows - 1 : opts.rows;
if (wrappedRows > capacity) {
process.stderr.write(
`verify-capture: warning: input occupies ${wrappedRows} terminal rows; ` +
`--rows ${opts.rows} kept the last ${capacity} and dropped the top ` +
`${wrappedRows - capacity}\n`,
);
}
process.stdout.write(
`${out} ${info.width}x${info.height} ${info.size}B ${rows.length} rows\n`,
);
}
const { opts, cmd } = parseArgs(process.argv.slice(2));
try {
await render(collectOutput(cmd), opts);
} catch (error) {
process.stderr.write(`verify-capture: ${error?.message ?? error}\n`);
process.exit(1);
}