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).
This commit is contained in:
yiliang114 2026-09-05 20:10:40 +09:00
parent 1837026a9d
commit 1d94060f5b
7 changed files with 86 additions and 30 deletions

View file

@ -137,7 +137,7 @@ Notes:
Example:
```tsx
import { WebShellTranscript } from '@qwen-code/web-shell';
import { WebShellTranscript } from '@qwen-code/web-shell/transcript';
import type { DaemonTranscriptBlock } from '@qwen-code/sdk/daemon';
export function HistoryView({

View file

@ -181,9 +181,12 @@ export function App() {
审批或 session mutation。浏览器宿主可以逐行解析 JSONL再通过 SDK 的 opt-in facade
投影:
> 只渲染 transcript 的宿主请从 `@qwen-code/web-shell/transcript` 子路径导入。包根会连带
> `App`、daemon providers 和编辑器/终端相关代码,不要依赖 tree shaking 把它们摇掉。
```tsx
import { projectChatRecordsToDaemonTranscript } from '@qwen-code/sdk/daemon/transcript';
import { WebShellTranscript } from '@qwen-code/web-shell';
import { WebShellTranscript } from '@qwen-code/web-shell/transcript';
const records = jsonl
.split(/\r?\n/)

View file

@ -0,0 +1,47 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Leaf module for projecting transcript blocks into localized messages.
*
* This lives outside `hooks/useMessages.ts` on purpose. That module is a daemon
* consumer it value-imports `useConnection` / `useTranscriptBlocks` /
* `useWorkspace` from the `daemon-react-sdk` barrel so importing this one
* function from it dragged the daemon provider stack into the read-only
* transcript entry, which advertises the opposite. Same treatment as
* `utils/composerTag.ts`: the pure projection lives in a module with no daemon
* or editor imports, and `useMessages.ts` re-exports it for existing callers.
*
* Keep this module free of React hooks and of anything reaching
* `@qwen-code/web-shell/daemon-react-sdk`. `client/build-artifact.test.ts`
* asserts that `dist/transcript.js` carries no daemon provider code.
*/
import type { DaemonTranscriptBlock } from '@qwen-code/sdk/daemon';
import { transcriptBlocksToDaemonMessages } from './transcriptToMessages';
import type { Message } from './types';
export type Translator = (
key: string,
vars?: Record<string, string | number>,
) => string;
export function transcriptBlocksToLocalizedMessages(
blocks: readonly DaemonTranscriptBlock[],
t: Translator,
safeToolProjection = false,
): Message[] {
return transcriptBlocksToDaemonMessages(blocks, {
safeToolProjection,
includeSourceIdentity: true,
labels: {
promptCancelled: t('request.cancelled'),
branchSuccess: (name) => t('branch.success', { name }),
modelStreamInterrupted: t('error.modelStreamInterrupted'),
loopDetected: t('error.loopDetected'),
},
});
}

View file

@ -334,6 +334,22 @@ describe('build artifact — transcript entry (#11031)', () => {
expect(js).not.toContain('vaul');
});
it('does not pull the daemon provider stack into the transcript entry', () => {
// The entry advertises "no daemon providers". It did not hold: the
// transcript imported transcriptBlocksToLocalizedMessages from
// hooks/useMessages, which value-imports useConnection /
// useTranscriptBlocks / useWorkspace from the daemon-react-sdk barrel, so
// dist/transcript.js still carried the provider guards. The projection now
// lives in adapters/localizedMessages.ts. These strings are provider
// invariant messages, which survive minification verbatim.
const js = readTranscriptBundle().replace(
/^const __qwenWebShellCss=[^\n]*\n/,
'',
);
expect(js).not.toContain('DaemonSessionProvider');
expect(js).not.toContain('DaemonWorkspaceProvider');
});
it('still carries what a transcript actually renders', () => {
const bundle = readTranscriptBundle();
expect(bundle).toContain('react-markdown');

View file

@ -30,7 +30,7 @@ import {
normalizeLanguage,
type WebShellLanguage,
} from '../i18n';
import { transcriptBlocksToLocalizedMessages } from '../hooks/useMessages';
import { transcriptBlocksToLocalizedMessages } from '../adapters/localizedMessages';
import { WebShellPortalRootContext } from '../portalRoot';
import { computeTodoDetails, computeTodoTimeline } from '../utils/todos';
import {

View file

@ -12,6 +12,10 @@ import {
useWorkspace,
} from '@qwen-code/web-shell/daemon-react-sdk';
import { transcriptBlocksToDaemonMessages } from '../adapters/transcriptToMessages';
import {
transcriptBlocksToLocalizedMessages,
type Translator,
} from '../adapters/localizedMessages';
import type { Message } from '../adapters/types';
import {
isActiveToolStatus,
@ -20,10 +24,13 @@ import {
projectTerminalBackgroundAgentTool,
} from '../adapters/toolClassification';
type Translator = (
key: string,
vars?: Record<string, string | number>,
) => string;
// Re-exported for existing callers. The projection itself lives in a leaf module
// so the read-only transcript entry does not pull this file's daemon imports —
// see adapters/localizedMessages.ts.
export {
transcriptBlocksToLocalizedMessages,
type Translator,
} from '../adapters/localizedMessages';
const BACKGROUND_AGENT_RECONCILIATION_RETRY_BASE_MS = 3_000;
const BACKGROUND_AGENT_RECONCILIATION_RETRY_MAX_MS = 60_000;
@ -66,23 +73,6 @@ interface ReconciliationRound {
succeeded: ReadonlyArray<string>;
}
export function transcriptBlocksToLocalizedMessages(
blocks: readonly DaemonTranscriptBlock[],
t: Translator,
safeToolProjection = false,
): Message[] {
return transcriptBlocksToDaemonMessages(blocks, {
safeToolProjection,
includeSourceIdentity: true,
labels: {
promptCancelled: t('request.cancelled'),
branchSuccess: (name) => t('branch.success', { name }),
modelStreamInterrupted: t('error.modelStreamInterrupted'),
loopDetected: t('error.loopDetected'),
},
});
}
function reuseUnchangedProjectedPrefix(
previous: MessageProjection | undefined,
blocks: readonly DaemonTranscriptBlock[],

View file

@ -40,12 +40,12 @@ const exportTranscriptMaxEnvelopeBytes = 32 * 1024 * 1024;
// cd packages/web-templates && node src/export-html/build.mjs
// (the build prints `Document export runtime is N bytes`.)
//
// Last measured at 8,456,076 bytes, by CI on this branch's merge with main
// (run 33959200199, "Lint & Static"), before the echarts stub below landed —
// so the real number is now lower and these two are still loose. Tighten them
// from the next green CI run rather than from a local guess.
const DOCUMENT_RUNTIME_WARNING_BYTES = 8_500_000;
const MAX_DOCUMENT_RUNTIME_BYTES = 8_700_000;
// Last measured at 7,275,173 bytes, with the echarts stub below in place, by a
// reviewer building this branch locally (PR #11038). The prior CI measurement
// on the same branch without that stub was 8,456,076. Re-measure and lower
// these two again after any change to the document entry's dependencies.
const DOCUMENT_RUNTIME_WARNING_BYTES = 7_300_000;
const MAX_DOCUMENT_RUNTIME_BYTES = 7_400_000;
// Modules that must not be reachable from the document entry, checked against
// the esbuild metafile inputs after the bundle is produced.