From ee35e40bc83e45faeab86b62c74c5e207c72737a Mon Sep 17 00:00:00 2001 From: fz Date: Mon, 7 Sep 2026 23:25:09 +0800 Subject: [PATCH 01/27] feat(ui): preview Mermaid diagrams in Chat code blocks --- .../2026-09-07-web-mermaid-preview.i18n.yaml | 6 + .../feature/2026-09-07-web-mermaid-preview.md | 31 +++++ .../2026-09-07-web-mermaid-preview.zh.md | 31 +++++ THIRD_PARTY_NOTICES.md | 2 +- .../expected/markdown-mermaid/ui.expected.md | 53 ++++++++ .../expected/markdown-mermaid/zh.expected.md | 53 ++++++++ apps/web/tests/markdown-mermaid.e2e.ts | 127 ++++++++++++++++++ apps/web/tsconfig.json | 1 + packages/client/locale/src/locales/en.ts | 5 + packages/client/locale/src/locales/zh.ts | 5 + packages/client/ui-chat/README.i18n.yaml | 4 +- packages/client/ui-chat/README.md | 8 ++ packages/client/ui-chat/README.zh.md | 8 ++ .../ui-chat/src/client/markdown-labels.ts | 7 + .../client/ui-primitives/README.i18n.yaml | 4 +- packages/client/ui-primitives/README.md | 4 + packages/client/ui-primitives/README.zh.md | 4 + packages/client/ui-primitives/package.json | 3 +- packages/client/ui-primitives/src/index.ts | 2 + .../src/markdown/CodeBlock.module.css | 1 + .../ui-primitives/src/markdown/CodeBlock.tsx | 19 ++- .../src/markdown/MermaidPreview.module.css | 26 ++++ .../src/markdown/MermaidPreview.tsx | 42 ++++++ .../ui-primitives/src/markdown/mermaid.ts | 59 ++++++++ .../ui-primitives/src/markdown/render.tsx | 9 ++ .../tests/code-block.client.spec.tsx | 2 +- .../tests/mermaid-preview.client.spec.tsx | 107 +++++++++++++++ .../tests/mermaid-runtime.client.spec.ts | 83 ++++++++++++ pnpm-lock.yaml | 3 + tsconfig.host.json | 1 + 30 files changed, 697 insertions(+), 13 deletions(-) create mode 100644 .agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.i18n.yaml create mode 100644 .agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.md create mode 100644 .agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.zh.md create mode 100644 apps/web/tests/expected/markdown-mermaid/ui.expected.md create mode 100644 apps/web/tests/expected/markdown-mermaid/zh.expected.md create mode 100644 apps/web/tests/markdown-mermaid.e2e.ts create mode 100644 packages/client/ui-primitives/src/markdown/MermaidPreview.module.css create mode 100644 packages/client/ui-primitives/src/markdown/MermaidPreview.tsx create mode 100644 packages/client/ui-primitives/src/markdown/mermaid.ts create mode 100644 packages/client/ui-primitives/tests/mermaid-preview.client.spec.tsx create mode 100644 packages/client/ui-primitives/tests/mermaid-runtime.client.spec.ts diff --git a/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.i18n.yaml b/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.i18n.yaml new file mode 100644 index 0000000000..f40e8c5054 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.md +2026-09-07-web-mermaid-preview.md: c891b4133e48a555de945c250679555f91f44c2f +2026-09-07-web-mermaid-preview.zh.md: a4c79c430a07b2655c85354a4406da3914bbd4c4 diff --git a/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.md b/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.md new file mode 100644 index 0000000000..c891b4133e --- /dev/null +++ b/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.md @@ -0,0 +1,31 @@ +# Agent Note: Mermaid previews in Chat code fences + +Status: implemented + +English | [中文](2026-09-07-web-mermaid-preview.zh.md) + +## Problem + +Assistant replies can describe diagrams in Mermaid code fences, but readers must interpret the source or copy it to another renderer. Rendering must handle incomplete streaming input and untrusted diagram content while remaining reusable outside Chat. + +## Decision + +Chat enables settled fence previews through `MarkdownLabels.mermaid`. The shared Markdown renderer uses the parsed fence language; streaming fences and consumers without those labels retain code display. The static [UI primitives package](../../../../packages/client/ui-primitives/README.md) owns `MermaidPreview`, which accepts source and localized labels without Session, file, or Cordis dependencies. `CodeBlock.preview` reuses the banner and copy action; copying always reads the source prop. + +Mermaid loads on demand. Its public render API serializes diagram work, and each call removes its temporary measurement DOM in `finally`. Strict security, disabled HTML labels, the neutral theme, and error-rendering policy cannot be overridden by diagram configuration. Generated SVG is displayed as an image without installing diagram links or scripts. Intrinsic dimensions come from the SVG viewBox; large diagrams shrink to fit, and the canvas stays light in both application themes. + +Source replacement and unmounting cancel result publication. Cancellation before runtime loading completes prevents rendering; an active Mermaid render finishes and releases its DOM but cannot publish to a cancelled component. Failures show a localized error and the original source. Replacing invalid source with a valid diagram recovers the preview. + +## Alternatives considered + +**Render each streamed chunk.** Incomplete diagrams are frequently invalid, and repeated layout work competes with text streaming. The existing message-settlement boundary provides a complete source value. + +**Put rendering inside Chat or add a general preview registry.** A shared primitive with plain props satisfies reuse without another registry or feature-plugin dependency. This follows the [shared-control rule](../architecture/2026-09-05-shared-client-control-primitives.md). + +**Insert interactive SVG into the message.** The requested preview needs diagram display and source access. Displaying the result as an image keeps diagram links and event handlers inactive. + +## Consequences + +The feature changes presentation without changing persisted messages, provider requests, tools, or Host APIs. Mermaid adds lazily loaded browser assets. Rendering still runs on the browser thread, and work already handed to the library cannot be interrupted. The initial feature has no editing, export, zoom controls, or interactive diagram links. + +Component tests cover delayed completion, stale success and failure, unmounting, source fallback, and copying. The keyless [browser scenario](../../../../apps/web/tests/markdown-mermaid.e2e.ts) exercises the assembled Chat with Chinese flowcharts, sequence diagrams, malformed source, configuration overrides, and English/Chinese UI snapshots. diff --git a/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.zh.md b/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.zh.md new file mode 100644 index 0000000000..a4c79c430a --- /dev/null +++ b/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.zh.md @@ -0,0 +1,31 @@ +# Agent Note: Chat 代码 fence 中的 Mermaid 预览 + +Status: implemented + +[English](2026-09-07-web-mermaid-preview.md) | 中文 + +## 问题 + +Assistant 回复可以在 Mermaid 代码 fence 中描述图表,但读者必须理解源码或把它复制到其他渲染器。渲染需要处理不完整的流式输入与不可信图表内容,同时保持在 Chat 之外的可复用性。 + +## 决策 + +Chat 通过 `MarkdownLabels.mermaid` 启用已定稿 fence 预览。共享 Markdown 渲染器使用解析后的 fence 语言;流式 fence 和未传入这些 label 的调用方保留代码显示。静态 [UI primitives 包](../../../../packages/client/ui-primitives/README.zh.md)拥有 `MermaidPreview`,它接收源码与本地化 label,不依赖 Session、文件或 Cordis。`CodeBlock.preview` 复用标题栏和复制操作;复制始终读取源码 prop。 + +Mermaid 按需加载。它的公开 render API 串行执行图表工作,每次调用在 `finally` 中移除临时测量 DOM。图表配置无法覆盖严格安全模式、禁用 HTML label、中性主题和错误渲染策略。生成的 SVG 以图片显示,不安装图内链接或脚本。固有尺寸取自 SVG viewBox;大图缩小以适应宽度,画布在两种应用主题下均保持浅色。 + +替换源码与卸载组件会取消结果发布。在运行时加载完成前取消可阻止渲染;已经开始的 Mermaid 渲染会完成并释放 DOM,但无法向已取消的组件发布结果。失败时显示本地化错误和原始源码。用有效图表替换非法源码可恢复预览。 + +## 考虑过的替代方案 + +**渲染每个流式分片。** 不完整的图表经常无效,反复布局也会与文本流式输出竞争。已有的消息定稿边界提供完整源码值。 + +**把渲染放在 Chat 内或增加通用预览注册表。** 接收普通 prop 的共享原语无需另一套注册表或功能插件依赖即可满足复用。这遵循[共享控件规则](../architecture/2026-09-05-shared-client-control-primitives.zh.md)。 + +**把可交互 SVG 插入消息。** 此次预览需求只需要图表显示与源码访问。将结果显示为图片后,图内链接和事件处理器不会生效。 + +## 后果 + +功能只改变展示,不改变持久化消息、提供者请求、工具或 Host API。Mermaid 增加按需加载的浏览器资源。渲染仍在浏览器线程上执行,已交给库的工作无法中断。首版不包含编辑、导出、缩放控件或可交互图内链接。 + +组件测试覆盖延迟完成、过期成功与失败、卸载、源码回退和复制。无密钥[浏览器场景](../../../../apps/web/tests/markdown-mermaid.e2e.ts)通过完整 Chat 组合验证中文流程图、时序图、非法源码、配置覆盖以及英文/中文 UI 快照。 diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 31eebdab8d..c2705ac530 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -79,6 +79,7 @@ External packages installed for runtime use or distributed inside the prebuilt b | [`mdast-util-from-markdown`](https://github.com/syntax-tree/mdast-util-from-markdown) | MIT | | [`mdast-util-gfm`](https://github.com/syntax-tree/mdast-util-gfm) | MIT | | [`mdast-util-math`](https://github.com/syntax-tree/mdast-util-math) | MIT | +| [`mermaid`](https://github.com/mermaid-js/mermaid) | MIT | | [`micromark-core-commonmark`](https://github.com/micromark/micromark/tree/main/packages/micromark-core-commonmark) | MIT | | [`micromark-extension-gfm`](https://github.com/micromark/micromark-extension-gfm) | MIT | | [`micromark-extension-math`](https://github.com/micromark/micromark-extension-math) | MIT | @@ -189,7 +190,6 @@ External packages **directly declared** for development, tests, types, or toolin | [`jsdom`](https://github.com/jsdom/jsdom) | MIT | | [`lefthook`](https://github.com/evilmartians/lefthook) | MIT | | [`lightningcss`](https://github.com/parcel-bundler/lightningcss) | MPL-2.0 | -| [`mermaid`](https://github.com/mermaid-js/mermaid) | MIT | | [`micromark-util-types`](https://github.com/micromark/micromark/tree/main/packages/micromark-util-types) | MIT | | [`msgpackr`](http://github.com/kriszyp/msgpackr) | MIT | | [`oxlint`](https://github.com/oxc-project/oxc) | MIT | diff --git a/apps/web/tests/expected/markdown-mermaid/ui.expected.md b/apps/web/tests/expected/markdown-mermaid/ui.expected.md new file mode 100644 index 0000000000..2b9d095829 --- /dev/null +++ b/apps/web/tests/expected/markdown-mermaid/ui.expected.md @@ -0,0 +1,53 @@ +- banner: + - navigation "Session hierarchy": + - button "Mermaid previews" [disabled] + - button "Session log": + - text: Session log + - img + - tablist: + - tab "Chat" [selected] + - tab "Trajectory" +- text: Preview these Mermaid diagrams. {{clock}} +- button "Copy": + - img +- heading "Mermaid previews" [level=1] +- text: mermaid +- button "Source" +- button "Copy" +- img "Mermaid diagram" +- text: mermaid +- button "Source" +- button "Copy" +- img "Mermaid diagram" +- text: mermaid +- button "Source" +- button "Copy" +- status: Unable to render this diagram. The source is shown below. +- code: flowchart LR A[unfinished +- text: mermaid +- button "Source" +- button "Copy" +- img "Mermaid diagram" +- button "Copy": + - img +- button "Good response": + - img +- button "Bad response": + - img +- button "Branch into a new conversation": + - img +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} +- textbox "Message or run a task... / commands, @ files or sessions" +- button "Commands": + - img +- button "Add attachment": + - img +- 'button "Access mode, current: Workspace Write"': Workspace Write +- button "Select model, current DeepSeek-V4-Flash": + - text: DeepSeek-V4-Flash + - img +- button "Send message" [disabled] +- text: 1 turns · 1 steps LLM {{duration}} diff --git a/apps/web/tests/expected/markdown-mermaid/zh.expected.md b/apps/web/tests/expected/markdown-mermaid/zh.expected.md new file mode 100644 index 0000000000..07cb8ac9d6 --- /dev/null +++ b/apps/web/tests/expected/markdown-mermaid/zh.expected.md @@ -0,0 +1,53 @@ +- banner: + - navigation "会话层级": + - button "Mermaid previews" [disabled] + - button "Session 日志": + - text: Session 日志 + - img + - tablist: + - tab "对话" [selected] + - tab "轨迹" +- text: Preview these Mermaid diagrams. {{clock}} +- button "复制": + - img +- heading "Mermaid previews" [level=1] +- text: mermaid +- button "源码" +- button "复制" +- img "Mermaid 图表" +- text: mermaid +- button "源码" +- button "复制" +- img "Mermaid 图表" +- text: mermaid +- button "源码" +- button "复制" +- status: 无法渲染此图表,源码如下。 +- code: flowchart LR A[unfinished +- text: mermaid +- button "源码" +- button "复制" +- img "Mermaid 图表" +- button "复制": + - img +- button "好的回答": + - img +- button "有问题的回答": + - img +- button "在新对话中分支": + - img +- button "用时 {{duration}}": + - img + - text: 用时 {{duration}} +- text: {{clock}} +- textbox "发消息或做任务… / 调用指令 @ 文件或对话" +- button "指令": + - img +- button "添加附件": + - img +- button "访问模式,当前:工作区内修改": 工作区内修改 +- button "选择模型,当前 DeepSeek-V4-Flash": + - text: DeepSeek-V4-Flash + - img +- button "发送消息" [disabled] +- text: 1 轮 · 1 步 LLM {{duration}} diff --git a/apps/web/tests/markdown-mermaid.e2e.ts b/apps/web/tests/markdown-mermaid.e2e.ts new file mode 100644 index 0000000000..d23ca2f8ad --- /dev/null +++ b/apps/web/tests/markdown-mermaid.e2e.ts @@ -0,0 +1,127 @@ +import { fileURLToPath } from 'node:url' +import { join } from 'node:path' +import type { Browser, Page } from 'playwright' +import { chromium } from 'playwright' +import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest' +import { createMessage, createUserMessage } from '@deepseek-ai/dsh-llm' +import { SESSION_FORMAT_VERSION, Session, SessionId } from '@deepseek-ai/dsh-session' +import type {} from '@deepseek-ai/dsh-session-title' +import { + assertFixtureInventory, captureStableAria, compareOrRefreshGolden, launchWebScaffold, + seedSession, watchConsole, webSnapshotMode, type WebScaffold, +} from './scaffold.ts' +import { newEnglishPage, saveFailureShot } from './support.ts' + +const SNAPSHOT_DIR = fileURLToPath(new URL('./expected/markdown-mermaid', import.meta.url)) +const MODE = webSnapshotMode() +const SEED_ID = 'markdown-mermaid-web-e2e' +const FLOW = 'flowchart LR\n A[输入] --> B[共享渲染器] --> C[图形预览]' +const SEQUENCE = 'sequenceDiagram\n participant U as User\n participant R as Renderer\n U->>R: Mermaid source\n R-->>U: Diagram' +const INVALID = 'flowchart LR\n A[unfinished' +const UNTRUSTED = [ + '%%{init: {"securityLevel":"loose","htmlLabels":true,"themeCSS":"body {display:none!important}"}}%%', + 'flowchart LR', + ' A[""] --> B[Safe]', + ' click B "javascript:alert(1)"', +].join('\n') + +function fixture(): string { + const session = Session.create(SessionId('markdown-mermaid-source')) + session.append('turn/start', { turn: 1 }) + const user = session.append('user/message', createUserMessage({ + content: [{ type: 'text', text: 'Preview these Mermaid diagrams.' }], source: { kind: 'user' }, + }), { surfaceOp: 'append' }) + session.append('session/title', { title: 'Mermaid previews', messageSeqs: [user.seq], source: { kind: 'fallback' } }) + session.append('step/start', { turn: 1, step: 1 }) + session.append('assistant/message', { + stream: [], turn: 1, step: 1, + message: createMessage({ + role: 'assistant', + content: [{ type: 'text', text: [ + '# Mermaid previews', + ...[FLOW, SEQUENCE, INVALID, UNTRUSTED].map(code => `\`\`\`mermaid\n${code}\n\`\`\``), + ].join('\n\n') }], + source: { kind: 'model', provider: 'fixture', model: 'fixture' }, + }), + }, { surfaceOp: 'append' }) + session.append('step/end', { turn: 1, step: 1 }) + session.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) + const noon = new Date().setHours(12, 0, 0, 0) + return [ + JSON.stringify({ + type: 'session', version: SESSION_FORMAT_VERSION, id: '{{sessionId}}', createdAt: 0, + cwd: '{{cwd}}', isSeeded: false, delegationDepth: 0, + }), + ...session.snapshotEvents().map(event => JSON.stringify({ ...event, time: noon + event.seq * 1000 })), + '', + ].join('\n') +} + +async function openConversation(page: Page, scaffold: WebScaffold): Promise { + await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' }) + await page.getByRole('treeitem').first().click({ timeout: 30_000 }) + await page.getByRole('treeitem').nth(1).click() + await page.getByRole('heading', { name: 'Mermaid previews' }).waitFor() +} + +describe('web e2e: Mermaid chat previews', () => { + let scaffold: WebScaffold + let browser: Browser + beforeAll(async () => { + scaffold = await launchWebScaffold({}) + await seedSession(scaffold, fixture(), SEED_ID) + browser = await chromium.launch() + }, 120_000) + afterAll(async () => { + await browser?.close() + await scaffold?.close() + }) + + it.skipIf(MODE === 'record')('renders diagrams, switches to source, copies source, and contains malformed content', async () => { + const page = await newEnglishPage(browser) + onTestFailed(() => saveFailureShot(page, 'web-e2e-markdown-mermaid')) + const tripwire = watchConsole(page) + const dialogs: string[] = [] + page.on('dialog', (dialog) => { dialogs.push(dialog.message()); void dialog.dismiss() }) + await page.context().grantPermissions(['clipboard-read', 'clipboard-write']) + await openConversation(page, scaffold) + const images = page.getByRole('img', { name: 'Mermaid diagram' }) + await expect.poll(() => images.count(), { timeout: 20_000 }).toBe(3) + await expect.poll(() => images.evaluateAll(nodes => nodes.every(node => (node as HTMLImageElement).naturalWidth > 0))).toBe(true) + expect(await images.evaluateAll(nodes => nodes.every(node => + node.getBoundingClientRect().width <= (node as HTMLImageElement).naturalWidth))).toBe(true) + expect(await page.getByText('Unable to render this diagram. The source is shown below.', { exact: true }).count()).toBe(1) + expect(await page.locator('pre code').allTextContents()).toContain(INVALID) + expect(await images.first().evaluate(node => decodeURIComponent((node as HTMLImageElement).src))).toContain('共享渲染器') + const first = page.locator('.md-code-block').first() + await first.getByRole('button', { name: 'Copy', exact: true }).click() + expect(await page.evaluate(() => navigator.clipboard.readText())).toBe(FLOW) + await first.getByRole('button', { name: 'Source', exact: true }).click() + expect(await first.locator('pre code').textContent()).toBe(FLOW) + await first.getByRole('button', { name: 'Preview', exact: true }).click() + await first.getByRole('img').waitFor() + await first.getByRole('button', { name: 'Copy', exact: true }).waitFor() + expect(await page.locator('body').evaluate(node => getComputedStyle(node).display)).not.toBe('none') + expect(await page.locator('[id^="dsh-mermaid-"]').count()).toBe(0) + expect(dialogs).toEqual([]) + expect(tripwire.pageErrors).toEqual([]) + expect(tripwire.warnings).toEqual([]) + const snapshot = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)) + .split(SEED_ID).join('{{seededId}}') + await compareOrRefreshGolden(join(SNAPSHOT_DIR, 'ui.expected.md'), snapshot, MODE) + await page.close() + }, 60_000) + + it.skipIf(MODE === 'record')('localizes the preview and failure states in Chinese', async () => { + const page = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: 'zh-CN', timezoneId: 'Asia/Shanghai' }) + await openConversation(page, scaffold) + await expect.poll(() => page.getByRole('img', { name: 'Mermaid 图表' }).count(), { timeout: 20_000 }).toBe(3) + expect(await page.getByRole('button', { name: '源码', exact: true }).count()).toBe(4) + expect(await page.getByText('无法渲染此图表,源码如下。', { exact: true }).count()).toBe(1) + const snapshot = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)) + .split(SEED_ID).join('{{seededId}}') + await compareOrRefreshGolden(join(SNAPSHOT_DIR, 'zh.expected.md'), snapshot, MODE) + await assertFixtureInventory(SNAPSHOT_DIR, ['ui.expected.md', 'zh.expected.md']) + await page.close() + }, 60_000) +}) diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 3b01e3990c..a6e3b24bfd 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -79,6 +79,7 @@ "tests/math-rendering.e2e.ts", "tests/markdown-cjk-strong.e2e.ts", "tests/markdown-inline-code-links.e2e.ts", + "tests/markdown-mermaid.e2e.ts", "tests/clickable-links-gallery.e2e.ts", "tests/queue-actions.e2e.ts", "tests/queue-image.e2e.ts", diff --git a/packages/client/locale/src/locales/en.ts b/packages/client/locale/src/locales/en.ts index 70ce31598c..6261002e3a 100644 --- a/packages/client/locale/src/locales/en.ts +++ b/packages/client/locale/src/locales/en.ts @@ -38,6 +38,11 @@ export const en = { 'json.expandNode': 'Expand JSON node', 'json.label': 'JSON', 'markdown.footnotes': 'Footnotes', + 'markdown.mermaid.diagram': 'Mermaid diagram', + 'markdown.mermaid.preview': 'Preview', + 'markdown.mermaid.source': 'Source', + 'markdown.mermaid.loading': 'Rendering diagram…', + 'markdown.mermaid.error': 'Unable to render this diagram. The source is shown below.', 'markdown.truncatedCharacters': '… truncated at {total} characters', 'number.thousand': '{value}K', 'number.million': '{value}M', diff --git a/packages/client/locale/src/locales/zh.ts b/packages/client/locale/src/locales/zh.ts index 30cee308d5..9a03ac9b7f 100644 --- a/packages/client/locale/src/locales/zh.ts +++ b/packages/client/locale/src/locales/zh.ts @@ -36,6 +36,11 @@ export const zh = { 'json.expandNode': '展开 JSON 节点', 'json.label': 'JSON', 'markdown.footnotes': '脚注', + 'markdown.mermaid.diagram': 'Mermaid 图表', + 'markdown.mermaid.preview': '预览', + 'markdown.mermaid.source': '源码', + 'markdown.mermaid.loading': '正在渲染图表…', + 'markdown.mermaid.error': '无法渲染此图表,源码如下。', 'markdown.truncatedCharacters': '… 已截断,共 {total} 字符', 'number.thousand': '{value}K', 'number.million': '{value}M', diff --git a/packages/client/ui-chat/README.i18n.yaml b/packages/client/ui-chat/README.i18n.yaml index 4db27677d0..39b3cefe79 100644 --- a/packages/client/ui-chat/README.i18n.yaml +++ b/packages/client/ui-chat/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-chat/README.md -README.md: 860dcb9eeb9c92a14d9128d9d8c95c29796726f8 -README.zh.md: 4dd06a16b19b14cacc0ded603cc1cf8869dd1a43 +README.md: 6115e18f303cfa6ecc63379eabf8a7ad6f56b121 +README.zh.md: 3cb069cdf6cedb6d0c5c045f95e106c2c26959b6 diff --git a/packages/client/ui-chat/README.md b/packages/client/ui-chat/README.md index 860dcb9eeb..6115e18f30 100644 --- a/packages/client/ui-chat/README.md +++ b/packages/client/ui-chat/README.md @@ -15,6 +15,7 @@ File-mention providers receive the viewed Session ID with the closing-turn owner ## Table of Contents - [System prompt row](#system-prompt-row) +- [Mermaid previews](#mermaid-previews) - [Turn token usage](#turn-token-usage) - [Turn Process Folding](#turn-process-folding) - [Scroll ownership](#scroll-ownership) @@ -29,6 +30,13 @@ File-mention providers receive the viewed Session ID with the closing-turn owner Each nonempty appended `system/message` owns a collapsed prompt row, including a complete prompt at the start of a headerless window; the same-step header does not duplicate it. Chat also shows a collapsed `System prompt` row for a non-empty initial request, explicit message-series start, or `system/message` surface node replacement whose text differs, reading the last nonempty surviving system node in surface order at the `request/header`; a non-initial request whose preceding header is outside the loaded history window also shows one. A resume repeats the row even when its system text is unchanged, including after pagination supplies the preceding header and system node; same-series config-only or tool-only changes, tool steps, and retries create no repetition, and a `system/message` event is never rendered as a transcript message. The row appears before that request's user messages, matching the provider envelope, and expands to the exact model-visible text with its original line breaks. A request whose system node is empty or outside the loaded window creates no row until the page holding the node arrives. + +## Mermaid previews + +Settled Assistant Markdown shows `mermaid` code fences as diagrams. Each fence offers Source/Preview switching and copies the original Mermaid source in either view. Streaming fences remain code until the message settles; invalid diagrams show an error and their source without disrupting other blocks. Chat enables the shared [Mermaid preview primitive](../ui-primitives/README.md) through localized labels. This is a presentation change and adds no model prompt, tool, or Session event. + +----- + ## Turn token usage diff --git a/packages/client/ui-chat/README.zh.md b/packages/client/ui-chat/README.zh.md index 4dd06a16b1..3cb069cdf6 100644 --- a/packages/client/ui-chat/README.zh.md +++ b/packages/client/ui-chat/README.zh.md @@ -15,6 +15,7 @@ kind: "package-reference" ## 目录 - [系统提示词行](#system-prompt-row) +- [Mermaid 预览](#mermaid-previews) - [轮次 token 用量](#turn-token-usage) - [轮次过程折叠](#turn-process-folding) - [滚动归属](#scroll-ownership) @@ -31,6 +32,13 @@ kind: "package-reference" ----- + +## Mermaid 预览 + +已定稿的 Assistant Markdown 把 `mermaid` 代码 fence 显示为图表。每个 fence 支持源码/预览切换,在任一视图中复制的都是原始 Mermaid 源码。流式 fence 在消息定稿前保持代码显示;非法图表显示错误与源码,不影响其他块。Chat 通过本地化 label 启用共享的 [Mermaid 预览原语](../ui-primitives/README.zh.md)。这项展示变更不增加模型提示词、工具或 Session 事件。 + +----- + ## 轮次 token 用量 diff --git a/packages/client/ui-chat/src/client/markdown-labels.ts b/packages/client/ui-chat/src/client/markdown-labels.ts index 6c30b99396..d42e6e5f83 100644 --- a/packages/client/ui-chat/src/client/markdown-labels.ts +++ b/packages/client/ui-chat/src/client/markdown-labels.ts @@ -12,5 +12,12 @@ export function markdownLabels(t: ChatViewSlotProps['t']): MarkdownLabels { return { code: { copyLabel: t('copy'), copiedLabel: t('copied') }, footnotes: t('markdown.footnotes'), + mermaid: { + diagram: t('markdown.mermaid.diagram'), + preview: t('markdown.mermaid.preview'), + source: t('markdown.mermaid.source'), + loading: t('markdown.mermaid.loading'), + error: t('markdown.mermaid.error'), + }, } } diff --git a/packages/client/ui-primitives/README.i18n.yaml b/packages/client/ui-primitives/README.i18n.yaml index 138d94440f..7b19fb03f8 100644 --- a/packages/client/ui-primitives/README.i18n.yaml +++ b/packages/client/ui-primitives/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-primitives/README.md -README.md: 8fd59c5ecc98a609f1383e428701cf590da95e73 -README.zh.md: 2478330825681f65aa83415c698cf0edc54fdad6 +README.md: 1e9ee01f6c2243c8b980e45cf512dda1af330af0 +README.zh.md: 02c0417c359070c2f4a4ca11a22709c401847cec diff --git a/packages/client/ui-primitives/README.md b/packages/client/ui-primitives/README.md index 8fd59c5ecc..1e9ee01f6c 100644 --- a/packages/client/ui-primitives/README.md +++ b/packages/client/ui-primitives/README.md @@ -53,6 +53,7 @@ Check this table before writing a control in a feature package. A plugin cannot | `Toast` | Transient top-center banner held for the owner's `holdMs`. | | `JsonTree`, `JsonBlock` | Read-only JSON inspection. | | `MarkdownText`, `CodeBlock` | Untrusted GFM with TeX math, and highlighted code. `CodeBlock` accepts opt-in `lineNumbers`; copied source excludes the gutter. | +| `MermaidPreview` | Read-only Mermaid diagram image with loading and source fallback states. | | `TerminalBlock`, `ReadBlock`, `DiffBlock`, `SearchBlock`, `WebBlock` | The agent-output card matching each tool-result intent. | | `icons/*`, `FishLogo`, `BrandWordmark`, `ReferenceIcon`, `LinkIcon`, `DocumentFileIcon` | Glyphs and brand marks, all riding `currentColor`. | | `FileTypeIcon` | The coloured file-type sheet (code, html, image, markdown, pdf, sheet, slides, document, other); `classifyFileType` picks the kind from a path's extension. | @@ -73,6 +74,9 @@ The catalog above lists what each export is for; this section covers the behavio `MarkdownText` renders untrusted GFM and TeX math, blocks unsafe links and images, and can turn resolved file mentions into explicit controls. When the owner passes a `pathImages` vocabulary, image destinations that are local media paths rewrite to displayable URLs on settled renders only (the same streaming gate as file mentions); without a vocabulary, local destinations remain inert alt text. A load or decode failure replaces the image with its authored alt text, or the original destination when alt is empty. Changing the image source permits a fresh load. While a reply streams, it freezes completed blocks, advances a top-level open fence by completed lines, and highlights that fence from saved Shiki grammar state. Completed token lines enter fixed-size React groups, so later chunks reconcile only the growing group; an unchanged fence retains that DOM when the final full parse resolves cross-document syntax. `TerminalBlock`, `ReadBlock`, `DiffBlock`, `SearchBlock`, and `WebBlock` render the matching tool-result intent with copy controls, overflow handling, and ANSI processing where applicable. `JsonTree` and `JsonBlock` inspect JSON values read-only, while `projectUserText` projects sent user text into inline plain runs and reference chips for the message bubble and queue rows. +`MermaidPreview` renders complete Mermaid source on a light canvas. Rendering loads Mermaid on demand, uses strict security, and exposes the generated SVG as an image with no diagram link handlers. A render failure shows the original source with the supplied error label; replacing the source discards late results from the previous render. The diagram keeps its intrinsic size and shrinks to fit the available width. + +Supply `MarkdownLabels.mermaid` to enable `mermaid` fence previews in a Markdown consumer; without it, fences remain code. Previews start after the message settles. `CodeBlock.preview` supplies an optional alternate body and localized Source/Preview switch labels while the existing Copy button always copies the source. [The Mermaid decision](../../../.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.md) records the rendering and reuse choices. ### Localizing copy diff --git a/packages/client/ui-primitives/README.zh.md b/packages/client/ui-primitives/README.zh.md index 2478330825..02c0417c35 100644 --- a/packages/client/ui-primitives/README.zh.md +++ b/packages/client/ui-primitives/README.zh.md @@ -53,6 +53,7 @@ kind: "package-library" | `Toast` | 顶部居中的瞬时横幅,保持时长由所有者的 `holdMs` 决定。 | | `JsonTree`、`JsonBlock` | 只读 JSON 查看。 | | `MarkdownText`、`CodeBlock` | 不可信 GFM 与 TeX 数学,以及高亮代码。`CodeBlock` 可通过 `lineNumbers` 开启行号;复制的源码不含行号栏。 | +| `MermaidPreview` | 只读 Mermaid 图表图片,包含加载状态与源码回退状态。 | | `TerminalBlock`、`ReadBlock`、`DiffBlock`、`SearchBlock`、`WebBlock` | 与各类工具结果意图对应的 agent 输出卡片。 | | `icons/*`、`FishLogo`、`BrandWordmark`、`ReferenceIcon`、`LinkIcon`、`DocumentFileIcon` | 字形与品牌标识,全部随 `currentColor`。 | | `FileTypeIcon` | 彩色文件类型纸片(code、html、image、markdown、pdf、sheet、slides、document、other);`classifyFileType` 按路径扩展名选出类型。 | @@ -73,6 +74,9 @@ kind: "package-library" `MarkdownText` 渲染不可信的 GFM 与 TeX 公式、阻止不安全的链接与图片,并可把已解析的文件提及转换为显式控件。当 owner 传入 `pathImages` 词表时,本地媒体路径的图片目标只在落定渲染阶段重写为可展示 URL(与 file mentions 相同的流式门);不传词表时本地目标保持惰性 alt 文本。加载或解码失败后,图片替换为作者的 alt 文本;alt 为空时显示原始目标路径。图片源变化后可重新加载。回复流式输出时,它冻结已完成的块、按已完成行推进顶层未闭合 fence,并从保存的 Shiki grammar state 为该 fence 增量高亮。已完成的 token 行进入固定大小的 React 分组,后续分片只 reconcile 正在增长的分组;最终全量解析解决跨文档语法时,未变化的 fence 会保留该 DOM。`TerminalBlock`、`ReadBlock`、`DiffBlock`、`SearchBlock` 与 `WebBlock` 把对应的工具结果意图渲染为带复制控件、溢出处理及适用时 ANSI 处理的卡片。`JsonTree` 与 `JsonBlock` 以只读方式检查 JSON 值;`projectUserText` 把已发送的用户文本投影为行内普通文本段与引用 chip,供消息气泡和排队行使用。 +`MermaidPreview` 在浅色画布上渲染完整的 Mermaid 源码。渲染按需加载 Mermaid、使用严格安全模式,并把生成的 SVG 显示为图片,不绑定图内链接处理器。渲染失败时显示原始源码与传入的错误文案;替换源码后会丢弃前一次渲染的延迟结果。图表保留固有尺寸,并在可用宽度不足时缩小。 + +为 Markdown 调用方传入 `MarkdownLabels.mermaid` 即可启用 `mermaid` fence 预览;未传入时,fence 保持代码显示。预览在消息定稿后开始。`CodeBlock.preview` 提供可选的替代正文和本地化的「源码/预览」切换文案,现有「复制」按钮始终复制源码。[Mermaid 决策](../../../.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.zh.md)记录了渲染与复用选择。 ### 本地化文案 diff --git a/packages/client/ui-primitives/package.json b/packages/client/ui-primitives/package.json index 2613c3aa98..6502b6564d 100644 --- a/packages/client/ui-primitives/package.json +++ b/packages/client/ui-primitives/package.json @@ -45,7 +45,8 @@ "mdast-util-gfm": "^3.1.0", "mdast-util-math": "^3.0.0", "micromark-extension-gfm": "^3.0.0", - "micromark-util-sanitize-uri": "^2.0.1" + "micromark-util-sanitize-uri": "^2.0.1", + "mermaid": "11.16.0" }, "files": [ "lib/index.js", diff --git a/packages/client/ui-primitives/src/index.ts b/packages/client/ui-primitives/src/index.ts index d9b12eda0e..7600a0f9ca 100644 --- a/packages/client/ui-primitives/src/index.ts +++ b/packages/client/ui-primitives/src/index.ts @@ -64,6 +64,8 @@ export type { } from './WebBlock.tsx' export { CodeBlock } from './markdown/CodeBlock.tsx' export type { CodeBlockProps } from './markdown/CodeBlock.tsx' +export { MermaidPreview } from './markdown/MermaidPreview.tsx' +export type { MermaidPreviewLabels } from './markdown/MermaidPreview.tsx' export { JsonBlock } from './markdown/JsonBlock.tsx' export { MarkdownText } from './markdown/MarkdownText.tsx' export type { MarkdownCodeLabels, MarkdownFileMentions, MarkdownLabels, MarkdownPathImages } from './markdown/MarkdownText.tsx' diff --git a/packages/client/ui-primitives/src/markdown/CodeBlock.module.css b/packages/client/ui-primitives/src/markdown/CodeBlock.module.css index 5f726e8474..28271cbbdf 100644 --- a/packages/client/ui-primitives/src/markdown/CodeBlock.module.css +++ b/packages/client/ui-primitives/src/markdown/CodeBlock.module.css @@ -56,6 +56,7 @@ display: flex; align-items: center; flex-shrink: 0; + gap: 12px; } .copyButton { diff --git a/packages/client/ui-primitives/src/markdown/CodeBlock.tsx b/packages/client/ui-primitives/src/markdown/CodeBlock.tsx index fff6a00a45..7f55844a91 100644 --- a/packages/client/ui-primitives/src/markdown/CodeBlock.tsx +++ b/packages/client/ui-primitives/src/markdown/CodeBlock.tsx @@ -31,6 +31,8 @@ export interface CodeBlockProps { copyLabel: string /** Copy-button label during the post-copy confirmation window. */ copiedLabel: string + /** Optional settled preview with localized switch labels; copying always retains the source text. */ + preview?: { content: ReactNode; previewLabel: string; sourceLabel: string } | undefined } /** @@ -59,7 +61,7 @@ function renderLine(line: readonly HighlightSpan[], index: number): ReactNode { ) } -export function CodeBlock({ code, lang, streaming, className, lineNumbers = false, copyLabel, copiedLabel }: CodeBlockProps) { +export function CodeBlock({ code, lang, streaming, className, lineNumbers = false, copyLabel, copiedLabel, preview }: CodeBlockProps) { const trimmed = code.endsWith('\n') ? code.slice(0, -1) : code const sourceLines = lineNumbers ? trimmed.split('\n') : undefined const rootRef = useRef(null) @@ -143,13 +145,13 @@ export function CodeBlock({ code, lang, streaming, className, lineNumbers = fals [streaming, highlighting, streamedBody, trimmed, lang, loaded], ) const [copied, setCopied] = useState(false) + const [showSource, setShowSource] = useState(false) + const previewAvailable = preview !== undefined && streaming !== true + const showingPreview = previewAvailable && !showSource const onCopy = useCallback(() => { if (copied) return - /* v8 ignore next -- both arms always mount a
; trimmed is the
-       typed fallback if the DOM shape ever diverges. */
-    const text = rootRef.current?.querySelector('pre')?.textContent ?? trimmed
-    void writeClipboard(text).then((ok) => {
+    void writeClipboard(trimmed).then((ok) => {
       if (!ok) return
       setCopied(true)
       window.setTimeout(() => { setCopied(false) }, 1000)
@@ -181,13 +183,18 @@ export function CodeBlock({ code, lang, streaming, className, lineNumbers = fals
         
{lang ?? ''}
+ {previewAvailable && ( + + )}
- {body} + {showingPreview ? preview.content : body} ) } diff --git a/packages/client/ui-primitives/src/markdown/MermaidPreview.module.css b/packages/client/ui-primitives/src/markdown/MermaidPreview.module.css new file mode 100644 index 0000000000..bd24e36251 --- /dev/null +++ b/packages/client/ui-primitives/src/markdown/MermaidPreview.module.css @@ -0,0 +1,26 @@ +.canvas { + overflow: auto; + padding: 16px; + background: var(--dsw-static-neutral-00); + border-radius: 0 0 12px 12px; +} + +.diagram { + display: block; + max-width: 100%; + height: auto; + margin: 0 auto; +} + +.status { + padding: 16px; + color: var(--dsw-alias-label-secondary); +} + +.staging { + position: fixed; + top: 0; + left: -100000px; + visibility: hidden; + pointer-events: none; +} diff --git a/packages/client/ui-primitives/src/markdown/MermaidPreview.tsx b/packages/client/ui-primitives/src/markdown/MermaidPreview.tsx new file mode 100644 index 0000000000..b59a09a48c --- /dev/null +++ b/packages/client/ui-primitives/src/markdown/MermaidPreview.tsx @@ -0,0 +1,42 @@ +/** Read-only diagram preview with source fallback and per-source async ownership. */ + +import { useEffect, useState } from 'react' +import { renderMermaid } from './mermaid.ts' +import css from './MermaidPreview.module.css' + +/** Localized preview states; the diagram source remains verbatim. */ +export interface MermaidPreviewLabels { + diagram: string + loading: string + error: string +} + +type Result = { code: string; src: string } | { code: string; error: true } + +/** + * Display a complete Mermaid document on a light diagram canvas, or its source if rendering fails. + * @param props - Source and complete localized labels. Changing source discards the previous result. + * @returns A loading status, an inert SVG image, or an error with the original source. + */ +export function MermaidPreview({ code, labels }: { code: string; labels: MermaidPreviewLabels }) { + const [result, setResult] = useState(null) + useEffect(() => { + const controller = new AbortController() + void renderMermaid(code, controller.signal).then( + (src) => { if (!controller.signal.aborted) setResult({ code, src }) }, + () => { if (!controller.signal.aborted) setResult({ code, error: true }) }, + ) + return () => { controller.abort() } + }, [code]) + + if (result?.code !== code) return
{labels.loading}
+ if ('error' in result) { + return ( +
+
{labels.error}
+
{code}
+
+ ) + } + return
{labels.diagram}
+} diff --git a/packages/client/ui-primitives/src/markdown/mermaid.ts b/packages/client/ui-primitives/src/markdown/mermaid.ts new file mode 100644 index 0000000000..4db2a64b6c --- /dev/null +++ b/packages/client/ui-primitives/src/markdown/mermaid.ts @@ -0,0 +1,59 @@ +/** Lazy Mermaid runtime; each render owns and removes its temporary measurement DOM. */ + +import type { Mermaid } from 'mermaid' +import clsx from 'clsx' +import css from './MermaidPreview.module.css' + +let runtime: Promise | undefined +let nextDiagramId = 0 + +function loadMermaid(): Promise { + runtime ??= import('mermaid').then(({ default: mermaid }) => { + mermaid.initialize({ + startOnLoad: false, + securityLevel: 'strict', + suppressErrorRendering: true, + theme: 'neutral', + htmlLabels: false, + secure: [ + 'secure', 'securityLevel', 'startOnLoad', 'maxTextSize', 'maxEdges', + 'suppressErrorRendering', 'theme', 'themeVariables', 'themeCSS', 'htmlLabels', 'flowchart', + ], + }) + return mermaid + }).catch((error: unknown) => { + runtime = undefined + throw error + }) + return runtime +} + +/** + * Render untrusted diagram source as an SVG image URL, without installing SVG or link handlers in the UI. + * @param code - Complete Mermaid source. + * @param signal - Cancels work waiting for the runtime; an active Mermaid render finishes before cleanup. + * @returns An SVG data URL. Import, parse, rendering, and cancellation failures reject. + */ +export async function renderMermaid(code: string, signal: AbortSignal): Promise { + const mermaid = await loadMermaid() + signal.throwIfAborted() + const stage = document.createElement('div') + stage.className = clsx(css.staging) + stage.setAttribute('aria-hidden', 'true') + document.body.append(stage) + try { + // Mermaid's public render API serializes diagram work; the lower-level mermaidAPI does not. + const { svg } = await mermaid.render(`dsh-mermaid-${nextDiagramId++}`, code, stage) + const root = new DOMParser().parseFromString(svg, 'image/svg+xml').documentElement + const viewBox = root.getAttribute('viewBox') + if (viewBox !== null) { + // An SVG image needs intrinsic dimensions; Mermaid's percentage width is for inline SVG. + const [, , width, height] = viewBox.split(/\s+/) as [string, string, string, string] + root.setAttribute('width', width) + root.setAttribute('height', height) + } + return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(new XMLSerializer().serializeToString(root))}` + } finally { + stage.remove() + } +} diff --git a/packages/client/ui-primitives/src/markdown/render.tsx b/packages/client/ui-primitives/src/markdown/render.tsx index 4d7805553b..d946c06893 100644 --- a/packages/client/ui-primitives/src/markdown/render.tsx +++ b/packages/client/ui-primitives/src/markdown/render.tsx @@ -23,6 +23,8 @@ import type * as Md from 'mdast' import type {} from 'mdast-util-math' import { normalizeUri } from 'micromark-util-sanitize-uri' import { CodeBlock } from './CodeBlock.tsx' +import { MermaidPreview } from './MermaidPreview.tsx' +import type { MermaidPreviewLabels } from './MermaidPreview.tsx' import { renderTexToReact } from './katex.tsx' import { LinkIcon, classifyLinkPath } from '../LinkIcon.tsx' import type { PositionedBlock } from './incremental.ts' @@ -40,6 +42,8 @@ export interface MarkdownCodeLabels { export interface MarkdownLabels { code: MarkdownCodeLabels footnotes: string + /** Opt into settled Mermaid fence previews by supplying their complete localized chrome. */ + mermaid?: MermaidPreviewLabels & { preview: string; source: string } } function sanitizeUrl(url: string): string { @@ -394,6 +398,11 @@ function renderCode(node: Md.Code, key: Key, context: MarkdownRenderContext): Re streaming={context.streaming} copyLabel={context.labels.code.copyLabel} copiedLabel={context.labels.code.copiedLabel} + preview={lang === 'mermaid' && !context.streaming && context.labels.mermaid !== undefined ? { + content: , + previewLabel: context.labels.mermaid.preview, + sourceLabel: context.labels.mermaid.source, + } : undefined} /> ) } diff --git a/packages/client/ui-primitives/tests/code-block.client.spec.tsx b/packages/client/ui-primitives/tests/code-block.client.spec.tsx index 4413cf46d2..83c016c8f0 100644 --- a/packages/client/ui-primitives/tests/code-block.client.spec.tsx +++ b/packages/client/ui-primitives/tests/code-block.client.spec.tsx @@ -122,7 +122,7 @@ describe('CodeBlock', () => { expect(view.getByText('plain text')).toBeTruthy() }) - it('shows the language banner and copies the pre textContent', async () => { + it('shows the language banner and copies the displayed source text', async () => { vi.useFakeTimers() const writeText = vi.fn().mockResolvedValue(undefined) Object.defineProperty(navigator, 'clipboard', { diff --git a/packages/client/ui-primitives/tests/mermaid-preview.client.spec.tsx b/packages/client/ui-primitives/tests/mermaid-preview.client.spec.tsx new file mode 100644 index 0000000000..4a716c4886 --- /dev/null +++ b/packages/client/ui-primitives/tests/mermaid-preview.client.spec.tsx @@ -0,0 +1,107 @@ +// @vitest-environment jsdom + +import { act, cleanup, fireEvent, render, screen } from '@testing-library/react' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { MermaidPreview } from '../src/markdown/MermaidPreview.tsx' +import { renderMermaid } from '../src/markdown/mermaid.ts' +import { MarkdownText } from '../src/markdown/MarkdownText.tsx' +import { markdownLabels } from './labels.client.ts' + +vi.mock('../src/markdown/mermaid.ts', () => ({ renderMermaid: vi.fn() })) + +const labels = { + diagram: 'Mermaid diagram', loading: 'Rendering diagram…', error: 'Unable to render this diagram.', + preview: 'Preview', source: 'Source', +} +const source = 'flowchart LR\n A[Input] --> B[Preview]' +const imageUrl = 'data:image/svg+xml;charset=utf-8,%3Csvg%2F%3E' + +afterEach(() => { cleanup(); vi.unstubAllGlobals() }) +beforeEach(() => { vi.resetAllMocks() }) + +describe('MermaidPreview', () => { + it('shows loading until rendering completes, then displays an image without inserting SVG', async () => { + const pending = Promise.withResolvers() + vi.mocked(renderMermaid).mockReturnValue(pending.promise) + const view = render() + expect(screen.getByRole('status').textContent).toBe(labels.loading) + expect(screen.queryByRole('img')).toBeNull() + await act(async () => { pending.resolve(imageUrl) }) + expect(screen.getByRole('img', { name: labels.diagram }).getAttribute('src')).toBe(imageUrl) + expect(view.container.querySelector('svg')).toBeNull() + expect(screen.queryByRole('status')).toBeNull() + }) + + it('retains invalid source and recovers when the source changes', async () => { + vi.mocked(renderMermaid).mockRejectedValueOnce(new Error('Parse error')) + const view = render() + expect((await screen.findByText(labels.error)).getAttribute('role')).toBe('status') + expect(view.container.querySelector('pre code')?.textContent).toBe('invalid') + const next = Promise.withResolvers() + vi.mocked(renderMermaid).mockReturnValue(next.promise) + view.rerender() + expect(screen.getByRole('status').textContent).toBe(labels.loading) + expect(view.container.querySelector('pre')).toBeNull() + await act(async () => { next.resolve(imageUrl) }) + expect(screen.getByRole('img').getAttribute('src')).toBe(imageUrl) + }) + + it.each(['resolve', 'reject'] as const)('ignores a stale %s after a newer source finishes', async (outcome) => { + const old = Promise.withResolvers() + const next = Promise.withResolvers() + vi.mocked(renderMermaid).mockReturnValueOnce(old.promise).mockReturnValueOnce(next.promise) + const view = render() + const oldSignal = vi.mocked(renderMermaid).mock.calls[0]![1] + view.rerender() + expect(oldSignal.aborted).toBe(true) + await act(async () => { next.resolve(imageUrl) }) + await act(async () => { + if (outcome === 'resolve') old.resolve('obsolete-image') + else old.reject(new Error('obsolete-error')) + }) + expect(screen.getByRole('img').getAttribute('src')).toBe(imageUrl) + expect(screen.queryByRole('status')).toBeNull() + }) + + it('cancels the owner when unmounted while loading', async () => { + const pending = Promise.withResolvers() + vi.mocked(renderMermaid).mockReturnValue(pending.promise) + const view = render() + const signal = vi.mocked(renderMermaid).mock.calls[0]![1] + view.unmount() + expect(signal.aborted).toBe(true) + await act(async () => { pending.reject(new Error('cancelled')) }) + expect(screen.queryByRole('img')).toBeNull() + }) +}) + +describe('Markdown Mermaid fences', () => { + it('keeps streaming source literal, then offers preview, source and source copying', async () => { + const writeText = vi.fn().mockResolvedValue(undefined) + vi.stubGlobal('navigator', Object.create(navigator, { clipboard: { value: { writeText } } })) + vi.mocked(renderMermaid).mockResolvedValue(imageUrl) + const props = { text: `\`\`\`mermaid\n${source}\n\`\`\``, labels: { ...markdownLabels, mermaid: labels } } + const view = render() + expect(view.container.querySelector('pre code')?.textContent).toBe(source) + expect(renderMermaid).not.toHaveBeenCalled() + expect(screen.queryByRole('button', { name: labels.source })).toBeNull() + view.rerender() + await screen.findByRole('img', { name: labels.diagram }) + fireEvent.click(screen.getByRole('button', { name: markdownLabels.code.copyLabel })) + await screen.findByRole('button', { name: markdownLabels.code.copiedLabel }) + expect(writeText).toHaveBeenCalledWith(source) + fireEvent.click(screen.getByRole('button', { name: labels.source })) + expect(view.container.querySelector('pre code')?.textContent).toBe(source) + expect(screen.queryByRole('img')).toBeNull() + fireEvent.click(screen.getByRole('button', { name: labels.preview })) + await screen.findByRole('img', { name: labels.diagram }) + }) + + it('leaves other languages and consumers without preview labels as code', () => { + const view = render() + expect(view.container.querySelector('pre code')?.textContent).toBe(source) + view.rerender() + expect(view.container.querySelector('pre code')?.textContent).toBe('flowchart LR') + expect(renderMermaid).not.toHaveBeenCalled() + }) +}) diff --git a/packages/client/ui-primitives/tests/mermaid-runtime.client.spec.ts b/packages/client/ui-primitives/tests/mermaid-runtime.client.spec.ts new file mode 100644 index 0000000000..8b79a42b9a --- /dev/null +++ b/packages/client/ui-primitives/tests/mermaid-runtime.client.spec.ts @@ -0,0 +1,83 @@ +// @vitest-environment jsdom + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +const initialize = vi.fn() +const renderDiagram = vi.fn() + +beforeEach(() => { + vi.resetModules() + vi.resetAllMocks() + vi.doMock('mermaid', () => ({ default: { initialize, render: renderDiagram } })) +}) +afterEach(() => { vi.doUnmock('mermaid') }) + +describe('Mermaid runtime', () => { + it('shares lazy initialization and removes each measurement container after rendering', async () => { + const { renderMermaid } = await import('../src/markdown/mermaid.ts') + expect(initialize).not.toHaveBeenCalled() + const stages: HTMLElement[] = [] + const ids: string[] = [] + renderDiagram.mockImplementation(async (id: string, code: string, stage: HTMLElement) => { + expect(stage.isConnected).toBe(true) + expect(stage.getAttribute('aria-hidden')).toBe('true') + stages.push(stage) + ids.push(id) + return { svg: `${code}` } + }) + const results = await Promise.all([ + renderMermaid('中文', new AbortController().signal), + renderMermaid('second', new AbortController().signal), + ]) + expect(initialize).toHaveBeenCalledOnce() + expect(initialize).toHaveBeenCalledWith(expect.objectContaining({ + startOnLoad: false, securityLevel: 'strict', suppressErrorRendering: true, htmlLabels: false, + secure: [ + 'secure', 'securityLevel', 'startOnLoad', 'maxTextSize', 'maxEdges', + 'suppressErrorRendering', 'theme', 'themeVariables', 'themeCSS', 'htmlLabels', 'flowchart', + ], + })) + expect(results.map(url => decodeURIComponent(url.split(',')[1]!))).toEqual(['中文', 'second']) + expect(new Set(ids).size).toBe(2) + expect(stages.every(stage => !stage.isConnected)).toBe(true) + }) + + it('removes measurement DOM even when Mermaid rejects', async () => { + const { renderMermaid } = await import('../src/markdown/mermaid.ts') + let stage: HTMLElement | undefined + const error = new Error('bad diagram') + renderDiagram.mockImplementation(async (_id: string, _code: string, target: HTMLElement) => { + stage = target + target.innerHTML = 'partial render' + throw error + }) + await expect(renderMermaid('bad', new AbortController().signal)).rejects.toBe(error) + expect(stage?.isConnected).toBe(false) + }) + + it('preserves intrinsic diagram size instead of stretching percentage-width SVG images', async () => { + const { renderMermaid } = await import('../src/markdown/mermaid.ts') + renderDiagram.mockResolvedValue({ svg: '' }) + const url = await renderMermaid('flowchart LR', new AbortController().signal) + const svg = new DOMParser().parseFromString(decodeURIComponent(url.split(',')[1]!), 'image/svg+xml').documentElement + expect(svg.getAttribute('width')).toBe('420') + expect(svg.getAttribute('height')).toBe('180') + }) + + it('does not start cancelled work after loading the runtime', async () => { + const { renderMermaid } = await import('../src/markdown/mermaid.ts') + const controller = new AbortController() + controller.abort() + await expect(renderMermaid('unused', controller.signal)).rejects.toMatchObject({ name: 'AbortError' }) + expect(renderDiagram).not.toHaveBeenCalled() + }) + + it('allows another attempt after runtime initialization fails', async () => { + const { renderMermaid } = await import('../src/markdown/mermaid.ts') + initialize.mockImplementationOnce(() => { throw new Error('initialization failed') }) + await expect(renderMermaid('first', new AbortController().signal)).rejects.toThrow('initialization failed') + renderDiagram.mockResolvedValue({ svg: '' }) + await expect(renderMermaid('retry', new AbortController().signal)).resolves.toContain('data:image/svg+xml') + expect(initialize).toHaveBeenCalledTimes(2) + }) +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bcea481379..d1c1098a48 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3323,6 +3323,9 @@ importers: mdast-util-math: specifier: ^3.0.0 version: 3.0.0 + mermaid: + specifier: 11.16.0 + version: 11.16.0 micromark-core-commonmark: specifier: ^2.0.3 version: 2.0.3 diff --git a/tsconfig.host.json b/tsconfig.host.json index c5a3f118b3..e7abea1600 100644 --- a/tsconfig.host.json +++ b/tsconfig.host.json @@ -67,6 +67,7 @@ "apps/web/tests/math-rendering.e2e.ts", "apps/web/tests/markdown-cjk-strong.e2e.ts", "apps/web/tests/markdown-inline-code-links.e2e.ts", + "apps/web/tests/markdown-mermaid.e2e.ts", "apps/web/tests/clickable-links-gallery.e2e.ts", "apps/web/tests/queue-actions.e2e.ts", "apps/web/tests/queue-image.e2e.ts", From fc0c885287f327d67ea2b6df49960d1287b054ed Mon Sep 17 00:00:00 2001 From: fz Date: Mon, 7 Sep 2026 23:37:48 +0800 Subject: [PATCH 02/27] style(ui): simplify Mermaid preview controls --- .../2026-09-07-web-mermaid-preview.i18n.yaml | 4 +- .../feature/2026-09-07-web-mermaid-preview.md | 4 +- .../2026-09-07-web-mermaid-preview.zh.md | 4 +- .../expected/markdown-mermaid/ui.expected.md | 13 ++- .../expected/markdown-mermaid/zh.expected.md | 6 +- apps/web/tests/markdown-mermaid.e2e.ts | 25 +++++- packages/client/ui-chat/README.i18n.yaml | 4 +- packages/client/ui-chat/README.md | 2 +- packages/client/ui-chat/README.zh.md | 2 +- .../client/ui-primitives/README.i18n.yaml | 4 +- packages/client/ui-primitives/README.md | 2 +- packages/client/ui-primitives/README.zh.md | 2 +- .../src/markdown/CodeBlock.module.css | 81 +++++++++++++++++++ .../ui-primitives/src/markdown/CodeBlock.tsx | 41 +++++++--- .../src/markdown/MermaidPreview.module.css | 2 +- .../tests/mermaid-preview.client.spec.tsx | 4 + 16 files changed, 165 insertions(+), 35 deletions(-) diff --git a/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.i18n.yaml b/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.i18n.yaml index f40e8c5054..5bc79823f6 100644 --- a/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.md -2026-09-07-web-mermaid-preview.md: c891b4133e48a555de945c250679555f91f44c2f -2026-09-07-web-mermaid-preview.zh.md: a4c79c430a07b2655c85354a4406da3914bbd4c4 +2026-09-07-web-mermaid-preview.md: df50e7c7f8b78a869d7d5b761864c06648f8d064 +2026-09-07-web-mermaid-preview.zh.md: 282e6fb4365a56425f2b341e2367e4a2cdae5c1c diff --git a/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.md b/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.md index c891b4133e..df50e7c7f8 100644 --- a/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.md +++ b/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.md @@ -10,7 +10,7 @@ Assistant replies can describe diagrams in Mermaid code fences, but readers must ## Decision -Chat enables settled fence previews through `MarkdownLabels.mermaid`. The shared Markdown renderer uses the parsed fence language; streaming fences and consumers without those labels retain code display. The static [UI primitives package](../../../../packages/client/ui-primitives/README.md) owns `MermaidPreview`, which accepts source and localized labels without Session, file, or Cordis dependencies. `CodeBlock.preview` reuses the banner and copy action; copying always reads the source prop. +Chat enables settled fence previews through `MarkdownLabels.mermaid`. The shared Markdown renderer uses the parsed fence language; streaming fences and consumers without those labels retain code display. The static [UI primitives package](../../../../packages/client/ui-primitives/README.md) owns `MermaidPreview`, which accepts source and localized labels without Session, file, or Cordis dependencies. `CodeBlock.preview` owns view switching and source copying. Previews omit the language banner and expose compact icon actions on hover or keyboard focus; touch devices keep those actions visible. Source and preview share the same focused toggle button, and copying always reads the source prop. Mermaid loads on demand. Its public render API serializes diagram work, and each call removes its temporary measurement DOM in `finally`. Strict security, disabled HTML labels, the neutral theme, and error-rendering policy cannot be overridden by diagram configuration. Generated SVG is displayed as an image without installing diagram links or scripts. Intrinsic dimensions come from the SVG viewBox; large diagrams shrink to fit, and the canvas stays light in both application themes. @@ -18,6 +18,8 @@ Source replacement and unmounting cancel result publication. Cancellation before ## Alternatives considered +**Keep the code banner above diagrams.** A language label and permanent text controls distract from the diagram. Overlay actions retain source access without reserving a title row. + **Render each streamed chunk.** Incomplete diagrams are frequently invalid, and repeated layout work competes with text streaming. The existing message-settlement boundary provides a complete source value. **Put rendering inside Chat or add a general preview registry.** A shared primitive with plain props satisfies reuse without another registry or feature-plugin dependency. This follows the [shared-control rule](../architecture/2026-09-05-shared-client-control-primitives.md). diff --git a/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.zh.md b/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.zh.md index a4c79c430a..282e6fb436 100644 --- a/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.zh.md +++ b/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.zh.md @@ -10,7 +10,7 @@ Assistant 回复可以在 Mermaid 代码 fence 中描述图表,但读者必须 ## 决策 -Chat 通过 `MarkdownLabels.mermaid` 启用已定稿 fence 预览。共享 Markdown 渲染器使用解析后的 fence 语言;流式 fence 和未传入这些 label 的调用方保留代码显示。静态 [UI primitives 包](../../../../packages/client/ui-primitives/README.zh.md)拥有 `MermaidPreview`,它接收源码与本地化 label,不依赖 Session、文件或 Cordis。`CodeBlock.preview` 复用标题栏和复制操作;复制始终读取源码 prop。 +Chat 通过 `MarkdownLabels.mermaid` 启用已定稿 fence 预览。共享 Markdown 渲染器使用解析后的 fence 语言;流式 fence 和未传入这些 label 的调用方保留代码显示。静态 [UI primitives 包](../../../../packages/client/ui-primitives/README.zh.md)拥有 `MermaidPreview`,它接收源码与本地化 label,不依赖 Session、文件或 Cordis。`CodeBlock.preview` 拥有视图切换与源码复制。预览省略语言标题栏,悬停或键盘聚焦时显示紧凑的图标操作;触屏上保持操作可见。源码与预览使用同一个已聚焦的切换按钮,复制始终读取源码 prop。 Mermaid 按需加载。它的公开 render API 串行执行图表工作,每次调用在 `finally` 中移除临时测量 DOM。图表配置无法覆盖严格安全模式、禁用 HTML label、中性主题和错误渲染策略。生成的 SVG 以图片显示,不安装图内链接或脚本。固有尺寸取自 SVG viewBox;大图缩小以适应宽度,画布在两种应用主题下均保持浅色。 @@ -18,6 +18,8 @@ Mermaid 按需加载。它的公开 render API 串行执行图表工作,每次 ## 考虑过的替代方案 +**在图表上方保留代码标题栏。** 语言标记和常驻的文字操作分散了对图表的注意力。浮层操作无需占用标题行即可保留源码入口。 + **渲染每个流式分片。** 不完整的图表经常无效,反复布局也会与文本流式输出竞争。已有的消息定稿边界提供完整源码值。 **把渲染放在 Chat 内或增加通用预览注册表。** 接收普通 prop 的共享原语无需另一套注册表或功能插件依赖即可满足复用。这遵循[共享控件规则](../architecture/2026-09-05-shared-client-control-primitives.zh.md)。 diff --git a/apps/web/tests/expected/markdown-mermaid/ui.expected.md b/apps/web/tests/expected/markdown-mermaid/ui.expected.md index 2b9d095829..58657a3bf6 100644 --- a/apps/web/tests/expected/markdown-mermaid/ui.expected.md +++ b/apps/web/tests/expected/markdown-mermaid/ui.expected.md @@ -4,6 +4,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" @@ -11,20 +13,17 @@ - button "Copy": - img - heading "Mermaid previews" [level=1] -- text: mermaid +- button "Source" +- button "Copy" +- tooltip "Copy" +- img "Mermaid diagram" - button "Source" - button "Copy" - img "Mermaid diagram" -- text: mermaid -- button "Source" -- button "Copy" -- img "Mermaid diagram" -- text: mermaid - button "Source" - button "Copy" - status: Unable to render this diagram. The source is shown below. - code: flowchart LR A[unfinished -- text: mermaid - button "Source" - button "Copy" - img "Mermaid diagram" diff --git a/apps/web/tests/expected/markdown-mermaid/zh.expected.md b/apps/web/tests/expected/markdown-mermaid/zh.expected.md index 07cb8ac9d6..e3a3b84157 100644 --- a/apps/web/tests/expected/markdown-mermaid/zh.expected.md +++ b/apps/web/tests/expected/markdown-mermaid/zh.expected.md @@ -4,6 +4,8 @@ - button "Session 日志": - text: Session 日志 - img + - button "展开侧栏": + - img - tablist: - tab "对话" [selected] - tab "轨迹" @@ -11,20 +13,16 @@ - button "复制": - img - heading "Mermaid previews" [level=1] -- text: mermaid - button "源码" - button "复制" - img "Mermaid 图表" -- text: mermaid - button "源码" - button "复制" - img "Mermaid 图表" -- text: mermaid - button "源码" - button "复制" - status: 无法渲染此图表,源码如下。 - code: flowchart LR A[unfinished -- text: mermaid - button "源码" - button "复制" - img "Mermaid 图表" diff --git a/apps/web/tests/markdown-mermaid.e2e.ts b/apps/web/tests/markdown-mermaid.e2e.ts index d23ca2f8ad..3c6986801f 100644 --- a/apps/web/tests/markdown-mermaid.e2e.ts +++ b/apps/web/tests/markdown-mermaid.e2e.ts @@ -94,9 +94,20 @@ describe('web e2e: Mermaid chat previews', () => { expect(await page.locator('pre code').allTextContents()).toContain(INVALID) expect(await images.first().evaluate(node => decodeURIComponent((node as HTMLImageElement).src))).toContain('共享渲染器') const first = page.locator('.md-code-block').first() + const controls = first.locator('[class*="bannerWrap"]') + expect(await first.getByText('mermaid', { exact: true }).count()).toBe(0) + await page.mouse.move(0, 0) + expect(await controls.evaluate(node => getComputedStyle(node).opacity)).toBe('0') + await first.hover() + expect(await controls.evaluate(node => getComputedStyle(node).opacity)).toBe('1') await first.getByRole('button', { name: 'Copy', exact: true }).click() expect(await page.evaluate(() => navigator.clipboard.readText())).toBe(FLOW) - await first.getByRole('button', { name: 'Source', exact: true }).click() + await page.mouse.move(0, 0) + await first.getByRole('button', { name: 'Source', exact: true }).focus() + expect(await controls.evaluate(node => getComputedStyle(node).opacity)).toBe('1') + await page.keyboard.press('Enter') + expect(await first.getByRole('button', { name: 'Preview', exact: true }) + .evaluate(node => node === document.activeElement)).toBe(true) expect(await first.locator('pre code').textContent()).toBe(FLOW) await first.getByRole('button', { name: 'Preview', exact: true }).click() await first.getByRole('img').waitFor() @@ -106,6 +117,8 @@ describe('web e2e: Mermaid chat previews', () => { expect(dialogs).toEqual([]) expect(tripwire.pageErrors).toEqual([]) expect(tripwire.warnings).toEqual([]) + await page.getByRole('heading', { name: 'Mermaid previews' }).click() + await page.mouse.move(0, 0) const snapshot = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)) .split(SEED_ID).join('{{seededId}}') await compareOrRefreshGolden(join(SNAPSHOT_DIR, 'ui.expected.md'), snapshot, MODE) @@ -113,11 +126,19 @@ describe('web e2e: Mermaid chat previews', () => { }, 60_000) it.skipIf(MODE === 'record')('localizes the preview and failure states in Chinese', async () => { - const page = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: 'zh-CN', timezoneId: 'Asia/Shanghai' }) + const page = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: 'zh-CN', timezoneId: 'Asia/Shanghai', hasTouch: true }) await openConversation(page, scaffold) await expect.poll(() => page.getByRole('img', { name: 'Mermaid 图表' }).count(), { timeout: 20_000 }).toBe(3) expect(await page.getByRole('button', { name: '源码', exact: true }).count()).toBe(4) + const controls = page.locator('.md-code-block').first().locator('[class*="bannerWrap"]') + expect(await controls.evaluate(node => getComputedStyle(node).opacity)).toBe('1') + expect(await page.getByRole('button', { name: '源码', exact: true }).first() + .evaluate(node => node.getBoundingClientRect().width)).toBe(44) expect(await page.getByText('无法渲染此图表,源码如下。', { exact: true }).count()).toBe(1) + const firstImage = page.getByRole('img', { name: 'Mermaid 图表' }).first() + await expect.poll(() => firstImage.evaluate(node => (node as HTMLImageElement).naturalWidth)).toBeGreaterThan(0) + expect(await controls.evaluate(node => node.getBoundingClientRect().top)) + .toBeGreaterThanOrEqual(await firstImage.evaluate(node => node.getBoundingClientRect().bottom)) const snapshot = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)) .split(SEED_ID).join('{{seededId}}') await compareOrRefreshGolden(join(SNAPSHOT_DIR, 'zh.expected.md'), snapshot, MODE) diff --git a/packages/client/ui-chat/README.i18n.yaml b/packages/client/ui-chat/README.i18n.yaml index 39b3cefe79..386c06ea91 100644 --- a/packages/client/ui-chat/README.i18n.yaml +++ b/packages/client/ui-chat/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-chat/README.md -README.md: 6115e18f303cfa6ecc63379eabf8a7ad6f56b121 -README.zh.md: 3cb069cdf6cedb6d0c5c045f95e106c2c26959b6 +README.md: 951f88e5fa61a27802fb7df11a3347ba9676a66c +README.zh.md: 3a180afb43c4262d6054e1f19deefa490b9b36de diff --git a/packages/client/ui-chat/README.md b/packages/client/ui-chat/README.md index 6115e18f30..951f88e5fa 100644 --- a/packages/client/ui-chat/README.md +++ b/packages/client/ui-chat/README.md @@ -33,7 +33,7 @@ Each nonempty appended `system/message` owns a collapsed prompt row, including a ## Mermaid previews -Settled Assistant Markdown shows `mermaid` code fences as diagrams. Each fence offers Source/Preview switching and copies the original Mermaid source in either view. Streaming fences remain code until the message settles; invalid diagrams show an error and their source without disrupting other blocks. Chat enables the shared [Mermaid preview primitive](../ui-primitives/README.md) through localized labels. This is a presentation change and adds no model prompt, tool, or Session event. +Settled Assistant Markdown shows `mermaid` code fences as diagrams. Diagrams have no language banner. Source/Preview and Copy icon actions appear on hover or keyboard focus and remain visible on touch devices; both views copy the original Mermaid source. Streaming fences remain code until the message settles; invalid diagrams show an error and their source without disrupting other blocks. Chat enables the shared [Mermaid preview primitive](../ui-primitives/README.md) through localized labels. This is a presentation change and adds no model prompt, tool, or Session event. ----- diff --git a/packages/client/ui-chat/README.zh.md b/packages/client/ui-chat/README.zh.md index 3cb069cdf6..3a180afb43 100644 --- a/packages/client/ui-chat/README.zh.md +++ b/packages/client/ui-chat/README.zh.md @@ -35,7 +35,7 @@ kind: "package-reference" ## Mermaid 预览 -已定稿的 Assistant Markdown 把 `mermaid` 代码 fence 显示为图表。每个 fence 支持源码/预览切换,在任一视图中复制的都是原始 Mermaid 源码。流式 fence 在消息定稿前保持代码显示;非法图表显示错误与源码,不影响其他块。Chat 通过本地化 label 启用共享的 [Mermaid 预览原语](../ui-primitives/README.zh.md)。这项展示变更不增加模型提示词、工具或 Session 事件。 +已定稿的 Assistant Markdown 把 `mermaid` 代码 fence 显示为图表。图表不显示语言标题栏。源码/预览切换与复制图标在悬停或键盘聚焦时出现,触屏上保持可见;两种视图复制的都是原始 Mermaid 源码。流式 fence 在消息定稿前保持代码显示;非法图表显示错误与源码,不影响其他块。Chat 通过本地化 label 启用共享的 [Mermaid 预览原语](../ui-primitives/README.zh.md)。这项展示变更不增加模型提示词、工具或 Session 事件。 ----- diff --git a/packages/client/ui-primitives/README.i18n.yaml b/packages/client/ui-primitives/README.i18n.yaml index 7b19fb03f8..a8e5104e37 100644 --- a/packages/client/ui-primitives/README.i18n.yaml +++ b/packages/client/ui-primitives/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-primitives/README.md -README.md: 1e9ee01f6c2243c8b980e45cf512dda1af330af0 -README.zh.md: 02c0417c359070c2f4a4ca11a22709c401847cec +README.md: f2694588e2ce96f10055802adaf5ff23b9cae9f1 +README.zh.md: cbd7fec0c1708b9d902dff2302c9472ed2bbb56b diff --git a/packages/client/ui-primitives/README.md b/packages/client/ui-primitives/README.md index 1e9ee01f6c..f2694588e2 100644 --- a/packages/client/ui-primitives/README.md +++ b/packages/client/ui-primitives/README.md @@ -76,7 +76,7 @@ The catalog above lists what each export is for; this section covers the behavio `MermaidPreview` renders complete Mermaid source on a light canvas. Rendering loads Mermaid on demand, uses strict security, and exposes the generated SVG as an image with no diagram link handlers. A render failure shows the original source with the supplied error label; replacing the source discards late results from the previous render. The diagram keeps its intrinsic size and shrinks to fit the available width. -Supply `MarkdownLabels.mermaid` to enable `mermaid` fence previews in a Markdown consumer; without it, fences remain code. Previews start after the message settles. `CodeBlock.preview` supplies an optional alternate body and localized Source/Preview switch labels while the existing Copy button always copies the source. [The Mermaid decision](../../../.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.md) records the rendering and reuse choices. +Supply `MarkdownLabels.mermaid` to enable `mermaid` fence previews in a Markdown consumer; without it, fences remain code. Previews start after the message settles. `CodeBlock.preview` supplies an optional headerless body with icon actions for source switching and copying. Actions appear on hover or keyboard focus, and stay visible on touch devices; copying always retains the source. [The Mermaid decision](../../../.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.md) records the rendering and reuse choices. ### Localizing copy diff --git a/packages/client/ui-primitives/README.zh.md b/packages/client/ui-primitives/README.zh.md index 02c0417c35..cbd7fec0c1 100644 --- a/packages/client/ui-primitives/README.zh.md +++ b/packages/client/ui-primitives/README.zh.md @@ -76,7 +76,7 @@ kind: "package-library" `MermaidPreview` 在浅色画布上渲染完整的 Mermaid 源码。渲染按需加载 Mermaid、使用严格安全模式,并把生成的 SVG 显示为图片,不绑定图内链接处理器。渲染失败时显示原始源码与传入的错误文案;替换源码后会丢弃前一次渲染的延迟结果。图表保留固有尺寸,并在可用宽度不足时缩小。 -为 Markdown 调用方传入 `MarkdownLabels.mermaid` 即可启用 `mermaid` fence 预览;未传入时,fence 保持代码显示。预览在消息定稿后开始。`CodeBlock.preview` 提供可选的替代正文和本地化的「源码/预览」切换文案,现有「复制」按钮始终复制源码。[Mermaid 决策](../../../.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.zh.md)记录了渲染与复用选择。 +为 Markdown 调用方传入 `MarkdownLabels.mermaid` 即可启用 `mermaid` fence 预览;未传入时,fence 保持代码显示。预览在消息定稿后开始。`CodeBlock.preview` 提供无标题栏的替代正文,以及切换源码和复制的图标操作。操作在悬停或键盘聚焦时出现,触屏上保持可见;复制始终保留源码。[Mermaid 决策](../../../.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.zh.md)记录了渲染与复用选择。 ### 本地化文案 diff --git a/packages/client/ui-primitives/src/markdown/CodeBlock.module.css b/packages/client/ui-primitives/src/markdown/CodeBlock.module.css index 28271cbbdf..c25252aaf9 100644 --- a/packages/client/ui-primitives/src/markdown/CodeBlock.module.css +++ b/packages/client/ui-primitives/src/markdown/CodeBlock.module.css @@ -122,3 +122,84 @@ content: counter(source-line); user-select: none; } + +/* Alternate content is a figure; its actions overlay instead of taking a title row. */ +.preview.showingPreview { + background: transparent; +} + +.preview .bannerWrap { + position: absolute; + top: 8px; + right: 8px; + z-index: 1; + background: transparent; +} + +.preview .banner { + padding: 2px; + background: var(--dsw-alias-bg-base); + border: 0; + border-radius: 8px; + box-shadow: var(--dsw-elevation-panel); +} + +.preview .action { + gap: 2px; +} + +.iconButton { + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 6px; + color: var(--dsw-alias-label-secondary); +} + +.iconButton span { + display: flex; +} + +.iconButton:hover { + color: var(--dsw-alias-label-primary); + background: var(--dsw-alias-interactive-bg-hover); +} + +.preview:not(.showingPreview) :where(pre) { + padding-top: 48px; +} + +@media (hover: hover) and (pointer: fine) { + .preview .bannerWrap { + opacity: 0; + pointer-events: none; + } + + .preview:hover .bannerWrap, + .preview:focus-within .bannerWrap { + opacity: 1; + pointer-events: auto; + } +} + +@media (pointer: coarse) { + .preview { + padding-bottom: 56px; + } + + .preview .bannerWrap { + top: auto; + bottom: 8px; + } + + .preview .iconButton { + width: 44px; + height: 44px; + } + + .preview:not(.showingPreview) :where(pre) { + padding-top: 16px; + } +} diff --git a/packages/client/ui-primitives/src/markdown/CodeBlock.tsx b/packages/client/ui-primitives/src/markdown/CodeBlock.tsx index 7f55844a91..3613ed4f16 100644 --- a/packages/client/ui-primitives/src/markdown/CodeBlock.tsx +++ b/packages/client/ui-primitives/src/markdown/CodeBlock.tsx @@ -2,6 +2,8 @@ import { Fragment, useCallback, useMemo, useRef, useState, useSyncExternalStore import type { CSSProperties, ReactNode } from 'react' import clsx from 'clsx' import { writeClipboard } from '../clipboard.ts' +import { Tooltip } from '../Tooltip.tsx' +import { IconCheckOutline16, IconCodeOutline16, IconCopyOutline16 } from '../icons/index.tsx' import { StreamingHighlightSession, grammarLoadCount, highlightToHtml, subscribeGrammarLoaded, } from './highlight.ts' @@ -31,7 +33,7 @@ export interface CodeBlockProps { copyLabel: string /** Copy-button label during the post-copy confirmation window. */ copiedLabel: string - /** Optional settled preview with localized switch labels; copying always retains the source text. */ + /** Headerless settled preview with hover/focus actions; copying always retains the source text. */ preview?: { content: ReactNode; previewLabel: string; sourceLabel: string } | undefined } @@ -173,24 +175,45 @@ export function CodeBlock({ code, lang, streaming, className, lineNumbers = fals
) + const copyActionLabel = copied ? copiedLabel : copyLabel + const copyAction = ( + + ) + return ( -
-
{lang ?? ''}
+ {!previewAvailable &&
{lang ?? ''}
}
{previewAvailable && ( - + + + )} - + {previewAvailable + ? {copyAction} + : copyAction}
diff --git a/packages/client/ui-primitives/src/markdown/MermaidPreview.module.css b/packages/client/ui-primitives/src/markdown/MermaidPreview.module.css index bd24e36251..f96c14418b 100644 --- a/packages/client/ui-primitives/src/markdown/MermaidPreview.module.css +++ b/packages/client/ui-primitives/src/markdown/MermaidPreview.module.css @@ -2,7 +2,7 @@ overflow: auto; padding: 16px; background: var(--dsw-static-neutral-00); - border-radius: 0 0 12px 12px; + border-radius: 12px; } .diagram { diff --git a/packages/client/ui-primitives/tests/mermaid-preview.client.spec.tsx b/packages/client/ui-primitives/tests/mermaid-preview.client.spec.tsx index 4a716c4886..4f477902f7 100644 --- a/packages/client/ui-primitives/tests/mermaid-preview.client.spec.tsx +++ b/packages/client/ui-primitives/tests/mermaid-preview.client.spec.tsx @@ -87,12 +87,16 @@ describe('Markdown Mermaid fences', () => { expect(screen.queryByRole('button', { name: labels.source })).toBeNull() view.rerender() await screen.findByRole('img', { name: labels.diagram }) + expect(screen.queryByText('mermaid')).toBeNull() + expect(screen.getByRole('button', { name: labels.source }).textContent).toBe('') + expect(screen.getByRole('button', { name: markdownLabels.code.copyLabel }).textContent).toBe('') fireEvent.click(screen.getByRole('button', { name: markdownLabels.code.copyLabel })) await screen.findByRole('button', { name: markdownLabels.code.copiedLabel }) expect(writeText).toHaveBeenCalledWith(source) fireEvent.click(screen.getByRole('button', { name: labels.source })) expect(view.container.querySelector('pre code')?.textContent).toBe(source) expect(screen.queryByRole('img')).toBeNull() + expect(screen.queryByText('mermaid')).toBeNull() fireEvent.click(screen.getByRole('button', { name: labels.preview })) await screen.findByRole('img', { name: labels.diagram }) }) From 220103670aef3ae830cf91f47abfccd31c1bdca2 Mon Sep 17 00:00:00 2001 From: fz Date: Tue, 8 Sep 2026 00:24:41 +0800 Subject: [PATCH 03/27] fix(ui): retain Mermaid previews and support hybrid touch controls --- .../2026-09-07-web-mermaid-preview.i18n.yaml | 4 +-- .../feature/2026-09-07-web-mermaid-preview.md | 2 +- .../2026-09-07-web-mermaid-preview.zh.md | 2 +- apps/web/tests/markdown-mermaid.e2e.ts | 32 ++++++++++++++++++- .../client/ui-primitives/README.i18n.yaml | 4 +-- packages/client/ui-primitives/README.md | 3 +- packages/client/ui-primitives/README.zh.md | 3 +- .../src/markdown/CodeBlock.module.css | 4 ++- .../ui-primitives/src/markdown/CodeBlock.tsx | 3 +- .../ui-primitives/src/markdown/render.tsx | 2 +- .../tests/mermaid-preview.client.spec.tsx | 7 ++-- 11 files changed, 52 insertions(+), 14 deletions(-) diff --git a/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.i18n.yaml b/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.i18n.yaml index 5bc79823f6..f86ae95548 100644 --- a/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.md -2026-09-07-web-mermaid-preview.md: df50e7c7f8b78a869d7d5b761864c06648f8d064 -2026-09-07-web-mermaid-preview.zh.md: 282e6fb4365a56425f2b341e2367e4a2cdae5c1c +2026-09-07-web-mermaid-preview.md: 811f688713491333587404d32946317d81ff2795 +2026-09-07-web-mermaid-preview.zh.md: 0fdb9cf318561f20fdb7042121bc3517c4d6c9a0 diff --git a/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.md b/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.md index df50e7c7f8..811f688713 100644 --- a/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.md +++ b/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.md @@ -10,7 +10,7 @@ Assistant replies can describe diagrams in Mermaid code fences, but readers must ## Decision -Chat enables settled fence previews through `MarkdownLabels.mermaid`. The shared Markdown renderer uses the parsed fence language; streaming fences and consumers without those labels retain code display. The static [UI primitives package](../../../../packages/client/ui-primitives/README.md) owns `MermaidPreview`, which accepts source and localized labels without Session, file, or Cordis dependencies. `CodeBlock.preview` owns view switching and source copying. Previews omit the language banner and expose compact icon actions on hover or keyboard focus; touch devices keep those actions visible. Source and preview share the same focused toggle button, and copying always reads the source prop. +Chat enables settled fence previews through `MarkdownLabels.mermaid`. The shared Markdown renderer uses the parsed fence language; streaming fences and consumers without those labels retain code display. The static [UI primitives package](../../../../packages/client/ui-primitives/README.md) owns `MermaidPreview`, which accepts source and localized labels without Session, file, or Cordis dependencies. `CodeBlock.preview` owns view switching and source copying. Previews omit the language banner and expose compact icon actions on hover or keyboard focus; devices with any touch input keep those actions visible below the diagram. Switching to source hides the mounted preview, so returning preserves completed rendering and pending work. Source and preview share the same focused toggle button, and copying always reads the source prop. Mermaid loads on demand. Its public render API serializes diagram work, and each call removes its temporary measurement DOM in `finally`. Strict security, disabled HTML labels, the neutral theme, and error-rendering policy cannot be overridden by diagram configuration. Generated SVG is displayed as an image without installing diagram links or scripts. Intrinsic dimensions come from the SVG viewBox; large diagrams shrink to fit, and the canvas stays light in both application themes. diff --git a/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.zh.md b/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.zh.md index 282e6fb436..0fdb9cf318 100644 --- a/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.zh.md +++ b/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.zh.md @@ -10,7 +10,7 @@ Assistant 回复可以在 Mermaid 代码 fence 中描述图表,但读者必须 ## 决策 -Chat 通过 `MarkdownLabels.mermaid` 启用已定稿 fence 预览。共享 Markdown 渲染器使用解析后的 fence 语言;流式 fence 和未传入这些 label 的调用方保留代码显示。静态 [UI primitives 包](../../../../packages/client/ui-primitives/README.zh.md)拥有 `MermaidPreview`,它接收源码与本地化 label,不依赖 Session、文件或 Cordis。`CodeBlock.preview` 拥有视图切换与源码复制。预览省略语言标题栏,悬停或键盘聚焦时显示紧凑的图标操作;触屏上保持操作可见。源码与预览使用同一个已聚焦的切换按钮,复制始终读取源码 prop。 +Chat 通过 `MarkdownLabels.mermaid` 启用已定稿 fence 预览。共享 Markdown 渲染器使用解析后的 fence 语言;流式 fence 和未传入这些 label 的调用方保留代码显示。静态 [UI primitives 包](../../../../packages/client/ui-primitives/README.zh.md)拥有 `MermaidPreview`,它接收源码与本地化 label,不依赖 Session、文件或 Cordis。`CodeBlock.preview` 拥有视图切换与源码复制。预览省略语言标题栏,悬停或键盘聚焦时显示紧凑的图标操作;具备任何触控输入的设备均在图表下方保持操作可见。切换到源码时隐藏已挂载的预览,因此返回时保留已完成的渲染与进行中的工作。源码与预览使用同一个已聚焦的切换按钮,复制始终读取源码 prop。 Mermaid 按需加载。它的公开 render API 串行执行图表工作,每次调用在 `finally` 中移除临时测量 DOM。图表配置无法覆盖严格安全模式、禁用 HTML label、中性主题和错误渲染策略。生成的 SVG 以图片显示,不安装图内链接或脚本。固有尺寸取自 SVG viewBox;大图缩小以适应宽度,画布在两种应用主题下均保持浅色。 diff --git a/apps/web/tests/markdown-mermaid.e2e.ts b/apps/web/tests/markdown-mermaid.e2e.ts index 3c6986801f..928e6cbf40 100644 --- a/apps/web/tests/markdown-mermaid.e2e.ts +++ b/apps/web/tests/markdown-mermaid.e2e.ts @@ -94,6 +94,7 @@ describe('web e2e: Mermaid chat previews', () => { expect(await page.locator('pre code').allTextContents()).toContain(INVALID) expect(await images.first().evaluate(node => decodeURIComponent((node as HTMLImageElement).src))).toContain('共享渲染器') const first = page.locator('.md-code-block').first() + const diagram = await first.getByRole('img').elementHandle() const controls = first.locator('[class*="bannerWrap"]') expect(await first.getByText('mermaid', { exact: true }).count()).toBe(0) await page.mouse.move(0, 0) @@ -109,8 +110,10 @@ describe('web e2e: Mermaid chat previews', () => { expect(await first.getByRole('button', { name: 'Preview', exact: true }) .evaluate(node => node === document.activeElement)).toBe(true) expect(await first.locator('pre code').textContent()).toBe(FLOW) + expect(await diagram!.evaluate(node => node.isConnected)).toBe(true) + expect(await diagram!.isVisible()).toBe(false) await first.getByRole('button', { name: 'Preview', exact: true }).click() - await first.getByRole('img').waitFor() + expect(await diagram!.isVisible()).toBe(true) await first.getByRole('button', { name: 'Copy', exact: true }).waitFor() expect(await page.locator('body').evaluate(node => getComputedStyle(node).display)).not.toBe('none') expect(await page.locator('[id^="dsh-mermaid-"]').count()).toBe(0) @@ -145,4 +148,31 @@ describe('web e2e: Mermaid chat previews', () => { await assertFixtureInventory(SNAPSHOT_DIR, ['ui.expected.md', 'zh.expected.md']) await page.close() }, 60_000) + + it.skipIf(MODE === 'record')('keeps actions visible when a mouse and touchscreen are both available', async () => { + const hybridBrowser = await chromium.launch({ + args: ['--blink-settings=availablePointerTypes=6,primaryPointerType=4,availableHoverTypes=2,primaryHoverType=2'], + }) + try { + const page = await newEnglishPage(hybridBrowser) + expect(await page.evaluate(() => + matchMedia('(hover: hover) and (pointer: fine) and (any-pointer: coarse)').matches)).toBe(true) + await openConversation(page, scaffold) + const first = page.locator('.md-code-block').first() + const diagram = first.getByRole('img', { name: 'Mermaid diagram' }) + await expect.poll(() => diagram.evaluate(node => (node as HTMLImageElement).naturalWidth)).toBeGreaterThan(0) + await page.mouse.move(0, 0) + const controls = first.locator('[class*="bannerWrap"]') + expect(await controls.evaluate(node => getComputedStyle(node).opacity)).toBe('1') + expect(await controls.evaluate(node => getComputedStyle(node).pointerEvents)).toBe('auto') + expect(await first.getByRole('button', { name: 'Source', exact: true }) + .evaluate(node => node.getBoundingClientRect().width)).toBe(44) + expect(await controls.evaluate(node => node.getBoundingClientRect().top)) + .toBeGreaterThanOrEqual(await diagram.evaluate(node => node.getBoundingClientRect().bottom)) + await first.getByRole('button', { name: 'Source', exact: true }).click() + expect(await first.locator('pre code').textContent()).toBe(FLOW) + } finally { + await hybridBrowser.close() + } + }, 60_000) }) diff --git a/packages/client/ui-primitives/README.i18n.yaml b/packages/client/ui-primitives/README.i18n.yaml index a8e5104e37..a0fd81c2bc 100644 --- a/packages/client/ui-primitives/README.i18n.yaml +++ b/packages/client/ui-primitives/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-primitives/README.md -README.md: f2694588e2ce96f10055802adaf5ff23b9cae9f1 -README.zh.md: cbd7fec0c1708b9d902dff2302c9472ed2bbb56b +README.md: 0ba4d6baaa54bbacc82202c490423b37613874df +README.zh.md: 06a266b61a79d4ff5e38ca0de9877a6d8ec66af0 diff --git a/packages/client/ui-primitives/README.md b/packages/client/ui-primitives/README.md index f2694588e2..0ba4d6baaa 100644 --- a/packages/client/ui-primitives/README.md +++ b/packages/client/ui-primitives/README.md @@ -76,7 +76,7 @@ The catalog above lists what each export is for; this section covers the behavio `MermaidPreview` renders complete Mermaid source on a light canvas. Rendering loads Mermaid on demand, uses strict security, and exposes the generated SVG as an image with no diagram link handlers. A render failure shows the original source with the supplied error label; replacing the source discards late results from the previous render. The diagram keeps its intrinsic size and shrinks to fit the available width. -Supply `MarkdownLabels.mermaid` to enable `mermaid` fence previews in a Markdown consumer; without it, fences remain code. Previews start after the message settles. `CodeBlock.preview` supplies an optional headerless body with icon actions for source switching and copying. Actions appear on hover or keyboard focus, and stay visible on touch devices; copying always retains the source. [The Mermaid decision](../../../.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.md) records the rendering and reuse choices. +Supply `MarkdownLabels.mermaid` to enable `mermaid` fence previews in a Markdown consumer; without it, fences remain code. Previews start after the message settles. `CodeBlock.preview` supplies an optional headerless body with icon actions for source switching and copying. Actions appear on hover or keyboard focus, and stay visible below the diagram whenever a touchscreen is available, including with a mouse attached. Switching to source keeps the preview mounted, so returning reuses its result; copying always retains the source. [The Mermaid decision](../../../.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.md) records the rendering and reuse choices. ### Localizing copy @@ -145,6 +145,7 @@ None; this package neither assembles nor sends a provider request. These limits define how the atoms behave at the edges; they are current package constraints, not a component roadmap. +- **Mermaid rendering runs on the browser thread** — every mounted settled preview starts rendering, including off-screen diagrams. Mermaid serializes layouts; work already submitted to it cannot be interrupted. Preview virtualization and worker rendering are not provided. - **Streaming defers cross-boundary reference resolution** — a reference-style link or footnote whose definition sits on the other side of the incremental freeze boundary renders as literal text while the reply streams; the settled full parse at finalize resolves it. - **A long highlighted fence retains its complete token DOM** — streaming avoids re-parsing, re-tokenizing, and reconciling the completed prefix, but it does not discard old colors or virtualize token spans. Final DOM cardinality therefore still follows the fence's token count; nested/container fences and a pathological single long line remain on the general tail path. - **Glyph-level icons are redrawn approximations** — the fish logo and the sparkle mark come from font glyphs whose vector geometry is not exportable from the local design data; hand-authored recreations stand in until an exact export path exists. diff --git a/packages/client/ui-primitives/README.zh.md b/packages/client/ui-primitives/README.zh.md index cbd7fec0c1..06a266b61a 100644 --- a/packages/client/ui-primitives/README.zh.md +++ b/packages/client/ui-primitives/README.zh.md @@ -76,7 +76,7 @@ kind: "package-library" `MermaidPreview` 在浅色画布上渲染完整的 Mermaid 源码。渲染按需加载 Mermaid、使用严格安全模式,并把生成的 SVG 显示为图片,不绑定图内链接处理器。渲染失败时显示原始源码与传入的错误文案;替换源码后会丢弃前一次渲染的延迟结果。图表保留固有尺寸,并在可用宽度不足时缩小。 -为 Markdown 调用方传入 `MarkdownLabels.mermaid` 即可启用 `mermaid` fence 预览;未传入时,fence 保持代码显示。预览在消息定稿后开始。`CodeBlock.preview` 提供无标题栏的替代正文,以及切换源码和复制的图标操作。操作在悬停或键盘聚焦时出现,触屏上保持可见;复制始终保留源码。[Mermaid 决策](../../../.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.zh.md)记录了渲染与复用选择。 +为 Markdown 调用方传入 `MarkdownLabels.mermaid` 即可启用 `mermaid` fence 预览;未传入时,fence 保持代码显示。预览在消息定稿后开始。`CodeBlock.preview` 提供无标题栏的替代正文,以及切换源码和复制的图标操作。操作在悬停或键盘聚焦时出现;只要设备具备触屏,就在图表下方保持可见,包括同时连接鼠标的情况。切换到源码时保留已挂载的预览,返回时复用其结果;复制始终保留源码。[Mermaid 决策](../../../.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.zh.md)记录了渲染与复用选择。 ### 本地化文案 @@ -145,6 +145,7 @@ kind: "package-library" 这些限制说明原子组件在边缘情况下的行为;它们是当前包约束,不是组件路线图。 +- **Mermaid 渲染在浏览器线程上执行**:每个已挂载的定稿预览都会开始渲染,包括视口外的图表。Mermaid 串行执行布局,已提交给它的工作无法中断。当前不提供预览虚拟化或 worker 渲染。 - **流式期间跨边界引用解析被推迟**:定义落在增量冻结边界另一侧的引用式链接或脚注,在回复流式输出期间渲染为字面文本;定稿时的全量解析会将其解析。 - **长高亮 fence 会保留完整 token DOM**:流式路径避免重新解析、重新 tokenize 和 reconcile 已完成前缀,但不会丢弃旧颜色或虚拟化 token span。因此最终 DOM 数量仍随 fence 的 token 数增长;嵌套/容器内 fence 与病态的单个超长行仍走通用尾部路径。 - **字形级图标是重新绘制的近似版本**:鱼形标志与闪光标记来自字体字形,而本地设计数据无法导出其矢量几何;在获得精确导出路径前,使用手工重建版本代替。 diff --git a/packages/client/ui-primitives/src/markdown/CodeBlock.module.css b/packages/client/ui-primitives/src/markdown/CodeBlock.module.css index c25252aaf9..33a114d102 100644 --- a/packages/client/ui-primitives/src/markdown/CodeBlock.module.css +++ b/packages/client/ui-primitives/src/markdown/CodeBlock.module.css @@ -184,7 +184,7 @@ } } -@media (pointer: coarse) { +@media (any-pointer: coarse) { .preview { padding-bottom: 56px; } @@ -192,6 +192,8 @@ .preview .bannerWrap { top: auto; bottom: 8px; + opacity: 1; + pointer-events: auto; } .preview .iconButton { diff --git a/packages/client/ui-primitives/src/markdown/CodeBlock.tsx b/packages/client/ui-primitives/src/markdown/CodeBlock.tsx index 3613ed4f16..2504f7eddc 100644 --- a/packages/client/ui-primitives/src/markdown/CodeBlock.tsx +++ b/packages/client/ui-primitives/src/markdown/CodeBlock.tsx @@ -217,7 +217,8 @@ export function CodeBlock({ code, lang, streaming, className, lineNumbers = fals
- {showingPreview ? preview.content : body} + {previewAvailable && } + {!showingPreview && body} ) } diff --git a/packages/client/ui-primitives/src/markdown/render.tsx b/packages/client/ui-primitives/src/markdown/render.tsx index d946c06893..eb3d3c2172 100644 --- a/packages/client/ui-primitives/src/markdown/render.tsx +++ b/packages/client/ui-primitives/src/markdown/render.tsx @@ -42,7 +42,7 @@ export interface MarkdownCodeLabels { export interface MarkdownLabels { code: MarkdownCodeLabels footnotes: string - /** Opt into settled Mermaid fence previews by supplying their complete localized chrome. */ + /** Opt into settled Mermaid fence previews by supplying their complete localized labels. */ mermaid?: MermaidPreviewLabels & { preview: string; source: string } } diff --git a/packages/client/ui-primitives/tests/mermaid-preview.client.spec.tsx b/packages/client/ui-primitives/tests/mermaid-preview.client.spec.tsx index 4f477902f7..c7cf95a9d4 100644 --- a/packages/client/ui-primitives/tests/mermaid-preview.client.spec.tsx +++ b/packages/client/ui-primitives/tests/mermaid-preview.client.spec.tsx @@ -86,7 +86,7 @@ describe('Markdown Mermaid fences', () => { expect(renderMermaid).not.toHaveBeenCalled() expect(screen.queryByRole('button', { name: labels.source })).toBeNull() view.rerender() - await screen.findByRole('img', { name: labels.diagram }) + const diagram = await screen.findByRole('img', { name: labels.diagram }) expect(screen.queryByText('mermaid')).toBeNull() expect(screen.getByRole('button', { name: labels.source }).textContent).toBe('') expect(screen.getByRole('button', { name: markdownLabels.code.copyLabel }).textContent).toBe('') @@ -96,9 +96,12 @@ describe('Markdown Mermaid fences', () => { fireEvent.click(screen.getByRole('button', { name: labels.source })) expect(view.container.querySelector('pre code')?.textContent).toBe(source) expect(screen.queryByRole('img')).toBeNull() + expect(diagram.isConnected).toBe(true) expect(screen.queryByText('mermaid')).toBeNull() fireEvent.click(screen.getByRole('button', { name: labels.preview })) - await screen.findByRole('img', { name: labels.diagram }) + expect(screen.getByRole('img', { name: labels.diagram })).toBe(diagram) + expect(screen.queryByRole('status')).toBeNull() + expect(renderMermaid).toHaveBeenCalledOnce() }) it('leaves other languages and consumers without preview labels as code', () => { From 0bfd594875662bce0007371195e9354a5cf84ac4 Mon Sep 17 00:00:00 2001 From: fz Date: Tue, 8 Sep 2026 12:41:09 +0800 Subject: [PATCH 04/27] refactor(ui): use tagged Mermaid preview results --- .../src/markdown/MermaidPreview.tsx | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/packages/client/ui-primitives/src/markdown/MermaidPreview.tsx b/packages/client/ui-primitives/src/markdown/MermaidPreview.tsx index b59a09a48c..5dadb6a6b3 100644 --- a/packages/client/ui-primitives/src/markdown/MermaidPreview.tsx +++ b/packages/client/ui-primitives/src/markdown/MermaidPreview.tsx @@ -11,7 +11,12 @@ export interface MermaidPreviewLabels { error: string } -type Result = { code: string; src: string } | { code: string; error: true } +type Result = { kind: 'ok'; code: string; src: string } | { kind: 'error'; code: string } + +/* v8 ignore next 3 -- closed-union backstop; only reached if a result is forged */ +function assertNever(value: never): never { + throw new Error(`unreachable Mermaid result: ${String(value)}`) +} /** * Display a complete Mermaid document on a light diagram canvas, or its source if rendering fails. @@ -23,20 +28,24 @@ export function MermaidPreview({ code, labels }: { code: string; labels: Mermaid useEffect(() => { const controller = new AbortController() void renderMermaid(code, controller.signal).then( - (src) => { if (!controller.signal.aborted) setResult({ code, src }) }, - () => { if (!controller.signal.aborted) setResult({ code, error: true }) }, + (src) => { if (!controller.signal.aborted) setResult({ kind: 'ok', code, src }) }, + () => { if (!controller.signal.aborted) setResult({ kind: 'error', code }) }, ) return () => { controller.abort() } }, [code]) if (result?.code !== code) return
{labels.loading}
- if ('error' in result) { - return ( -
-
{labels.error}
-
{code}
-
- ) + switch (result.kind) { + case 'ok': + return
{labels.diagram}
+ case 'error': + return ( +
+
{labels.error}
+
{code}
+
+ ) + /* v8 ignore next -- closed-union backstop; only reached if a result is forged */ + default: return assertNever(result) } - return
{labels.diagram}
} From 4fabbd39c588039e8967f95f3d427b1158b0cce7 Mon Sep 17 00:00:00 2001 From: fz Date: Tue, 8 Sep 2026 12:52:41 +0800 Subject: [PATCH 05/27] test(web): scope Mermaid snapshots to diagram content --- .../expected/markdown-mermaid/ui.expected.md | 37 ------------------- .../expected/markdown-mermaid/zh.expected.md | 37 ------------------- apps/web/tests/markdown-mermaid.e2e.ts | 7 ++-- 3 files changed, 3 insertions(+), 78 deletions(-) diff --git a/apps/web/tests/expected/markdown-mermaid/ui.expected.md b/apps/web/tests/expected/markdown-mermaid/ui.expected.md index 58657a3bf6..ac9ceb3164 100644 --- a/apps/web/tests/expected/markdown-mermaid/ui.expected.md +++ b/apps/web/tests/expected/markdown-mermaid/ui.expected.md @@ -1,17 +1,3 @@ -- banner: - - navigation "Session hierarchy": - - button "Mermaid previews" [disabled] - - button "Session log": - - text: Session log - - img - - button "Open the sidebar": - - img - - tablist: - - tab "Chat" [selected] - - tab "Trajectory" -- text: Preview these Mermaid diagrams. {{clock}} -- button "Copy": - - img - heading "Mermaid previews" [level=1] - button "Source" - button "Copy" @@ -27,26 +13,3 @@ - button "Source" - button "Copy" - img "Mermaid diagram" -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} -- text: {{clock}} -- textbox "Message or run a task... / commands, @ files or sessions" -- button "Commands": - - img -- button "Add attachment": - - img -- 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img -- button "Send message" [disabled] -- text: 1 turns · 1 steps LLM {{duration}} diff --git a/apps/web/tests/expected/markdown-mermaid/zh.expected.md b/apps/web/tests/expected/markdown-mermaid/zh.expected.md index e3a3b84157..066e88f589 100644 --- a/apps/web/tests/expected/markdown-mermaid/zh.expected.md +++ b/apps/web/tests/expected/markdown-mermaid/zh.expected.md @@ -1,17 +1,3 @@ -- banner: - - navigation "会话层级": - - button "Mermaid previews" [disabled] - - button "Session 日志": - - text: Session 日志 - - img - - button "展开侧栏": - - img - - tablist: - - tab "对话" [selected] - - tab "轨迹" -- text: Preview these Mermaid diagrams. {{clock}} -- button "复制": - - img - heading "Mermaid previews" [level=1] - button "源码" - button "复制" @@ -26,26 +12,3 @@ - button "源码" - button "复制" - img "Mermaid 图表" -- button "复制": - - img -- button "好的回答": - - img -- button "有问题的回答": - - img -- button "在新对话中分支": - - img -- button "用时 {{duration}}": - - img - - text: 用时 {{duration}} -- text: {{clock}} -- textbox "发消息或做任务… / 调用指令 @ 文件或对话" -- button "指令": - - img -- button "添加附件": - - img -- button "访问模式,当前:工作区内修改": 工作区内修改 -- button "选择模型,当前 DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img -- button "发送消息" [disabled] -- text: 1 轮 · 1 步 LLM {{duration}} diff --git a/apps/web/tests/markdown-mermaid.e2e.ts b/apps/web/tests/markdown-mermaid.e2e.ts index 928e6cbf40..82789caa63 100644 --- a/apps/web/tests/markdown-mermaid.e2e.ts +++ b/apps/web/tests/markdown-mermaid.e2e.ts @@ -15,6 +15,7 @@ import { newEnglishPage, saveFailureShot } from './support.ts' const SNAPSHOT_DIR = fileURLToPath(new URL('./expected/markdown-mermaid', import.meta.url)) const MODE = webSnapshotMode() const SEED_ID = 'markdown-mermaid-web-e2e' +const DIAGRAM_MESSAGE = '[class*="markdown"]:has(.md-code-block)' const FLOW = 'flowchart LR\n A[输入] --> B[共享渲染器] --> C[图形预览]' const SEQUENCE = 'sequenceDiagram\n participant U as User\n participant R as Renderer\n U->>R: Mermaid source\n R-->>U: Diagram' const INVALID = 'flowchart LR\n A[unfinished' @@ -122,8 +123,7 @@ describe('web e2e: Mermaid chat previews', () => { expect(tripwire.warnings).toEqual([]) await page.getByRole('heading', { name: 'Mermaid previews' }).click() await page.mouse.move(0, 0) - const snapshot = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)) - .split(SEED_ID).join('{{seededId}}') + const snapshot = await captureStableAria(page, DIAGRAM_MESSAGE, scaffold.workspaceCwd) await compareOrRefreshGolden(join(SNAPSHOT_DIR, 'ui.expected.md'), snapshot, MODE) await page.close() }, 60_000) @@ -142,8 +142,7 @@ describe('web e2e: Mermaid chat previews', () => { await expect.poll(() => firstImage.evaluate(node => (node as HTMLImageElement).naturalWidth)).toBeGreaterThan(0) expect(await controls.evaluate(node => node.getBoundingClientRect().top)) .toBeGreaterThanOrEqual(await firstImage.evaluate(node => node.getBoundingClientRect().bottom)) - const snapshot = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)) - .split(SEED_ID).join('{{seededId}}') + const snapshot = await captureStableAria(page, DIAGRAM_MESSAGE, scaffold.workspaceCwd) await compareOrRefreshGolden(join(SNAPSHOT_DIR, 'zh.expected.md'), snapshot, MODE) await assertFixtureInventory(SNAPSHOT_DIR, ['ui.expected.md', 'zh.expected.md']) await page.close() From a8f9d81f3a68266f4d268b877fd2715697062c87 Mon Sep 17 00:00:00 2001 From: yudshj Date: Wed, 9 Sep 2026 12:18:09 +0800 Subject: [PATCH 06/27] feat(client): preview Graphviz, SVG, and HTML Markdown fences --- .../2026-09-07-web-mermaid-preview.i18n.yaml | 4 +- .../feature/2026-09-07-web-mermaid-preview.md | 2 + .../2026-09-07-web-mermaid-preview.zh.md | 2 + ...6-09-09-markdown-static-previews.i18n.yaml | 6 + .../2026-09-09-markdown-static-previews.md | 31 + .../2026-09-09-markdown-static-previews.zh.md | 31 + THIRD_PARTY_NOTICES.md | 4 + .../expected/markdown-mermaid/ui.expected.md | 9 + .../expected/markdown-mermaid/zh.expected.md | 9 + apps/web/tests/markdown-mermaid.e2e.ts | 55 +- apps/web/vite.config.ts | 16 +- packages/client/locale/src/locales/en.ts | 5 + packages/client/locale/src/locales/zh.ts | 5 + packages/client/ui-chat/README.i18n.yaml | 4 +- packages/client/ui-chat/README.md | 2 +- packages/client/ui-chat/README.zh.md | 2 +- .../ui-chat/src/client/markdown-labels.ts | 9 + .../client/ui-primitives/README.i18n.yaml | 4 +- packages/client/ui-primitives/README.md | 6 +- packages/client/ui-primitives/README.zh.md | 6 +- .../THIRD_PARTY_PREVIEW_NOTICES.txt | 688 ++++++++++++++++++ packages/client/ui-primitives/package.json | 5 +- .../src/markdown/MermaidPreview.module.css | 9 + .../src/markdown/MermaidPreview.tsx | 52 +- .../src/markdown/SourcePreview.tsx | 56 ++ .../ui-primitives/src/markdown/graphviz.ts | 22 + .../src/markdown/preview-document.ts | 44 ++ .../ui-primitives/src/markdown/render.tsx | 41 +- .../tests/fence-previews.client.spec.tsx | 103 +++ .../tests/graphviz-runtime.client.spec.ts | 35 + pnpm-lock.yaml | 11 + scripts/check-workspace-constraints.spec.ts | 14 + scripts/check-workspace-constraints.ts | 2 +- scripts/gen-third-party-notices.spec.ts | 24 + scripts/gen-third-party-notices.ts | 34 + 35 files changed, 1290 insertions(+), 62 deletions(-) create mode 100644 .agents/notes/implemented/feature/2026-09-09-markdown-static-previews.i18n.yaml create mode 100644 .agents/notes/implemented/feature/2026-09-09-markdown-static-previews.md create mode 100644 .agents/notes/implemented/feature/2026-09-09-markdown-static-previews.zh.md create mode 100644 packages/client/ui-primitives/THIRD_PARTY_PREVIEW_NOTICES.txt create mode 100644 packages/client/ui-primitives/src/markdown/SourcePreview.tsx create mode 100644 packages/client/ui-primitives/src/markdown/graphviz.ts create mode 100644 packages/client/ui-primitives/src/markdown/preview-document.ts create mode 100644 packages/client/ui-primitives/tests/fence-previews.client.spec.tsx create mode 100644 packages/client/ui-primitives/tests/graphviz-runtime.client.spec.ts diff --git a/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.i18n.yaml b/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.i18n.yaml index f86ae95548..190b6ffb3e 100644 --- a/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.md -2026-09-07-web-mermaid-preview.md: 811f688713491333587404d32946317d81ff2795 -2026-09-07-web-mermaid-preview.zh.md: 0fdb9cf318561f20fdb7042121bc3517c4d6c9a0 +2026-09-07-web-mermaid-preview.md: 246b38e764e1e5812427f935c4a41c28f0fa24a0 +2026-09-07-web-mermaid-preview.zh.md: c1dd25c1b6dc21c36d92e0d7c1477fcea9ae99c2 diff --git a/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.md b/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.md index 811f688713..246b38e764 100644 --- a/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.md +++ b/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.md @@ -26,6 +26,8 @@ Source replacement and unmounting cancel result publication. Cancellation before **Insert interactive SVG into the message.** The requested preview needs diagram display and source access. Displaying the result as an image keeps diagram links and event handlers inactive. +The [static fence preview decision](2026-09-09-markdown-static-previews.md) owns Graphviz, SVG, and HTML sandboxing and shared preview license distribution. + ## Consequences The feature changes presentation without changing persisted messages, provider requests, tools, or Host APIs. Mermaid adds lazily loaded browser assets. Rendering still runs on the browser thread, and work already handed to the library cannot be interrupted. The initial feature has no editing, export, zoom controls, or interactive diagram links. diff --git a/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.zh.md b/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.zh.md index 0fdb9cf318..c1dd25c1b6 100644 --- a/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.zh.md +++ b/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.zh.md @@ -26,6 +26,8 @@ Mermaid 按需加载。它的公开 render API 串行执行图表工作,每次 **把可交互 SVG 插入消息。** 此次预览需求只需要图表显示与源码访问。将结果显示为图片后,图内链接和事件处理器不会生效。 +[静态 fence 预览决策](2026-09-09-markdown-static-previews.zh.md)负责 Graphviz、SVG、HTML 的 sandbox 及共享预览许可证分发。 + ## 后果 功能只改变展示,不改变持久化消息、提供者请求、工具或 Host API。Mermaid 增加按需加载的浏览器资源。渲染仍在浏览器线程上执行,已交给库的工作无法中断。首版不包含编辑、导出、缩放控件或可交互图内链接。 diff --git a/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.i18n.yaml b/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.i18n.yaml new file mode 100644 index 0000000000..6cb3a179eb --- /dev/null +++ b/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-09-markdown-static-previews.md +2026-09-09-markdown-static-previews.md: fd3772cabc2160f07e79b06dffa4374fe104cbd1 +2026-09-09-markdown-static-previews.zh.md: 92f4b030879acaeb484eed33e078574750a1801e diff --git a/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.md b/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.md new file mode 100644 index 0000000000..fd3772cabc --- /dev/null +++ b/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.md @@ -0,0 +1,31 @@ +# Agent Note: Static Markdown fence previews + +Status: implemented + +English | [中文](2026-09-09-markdown-static-previews.zh.md) + +## Problem + +Readers need to see DOT diagrams, SVG artwork, and HTML examples directly in Assistant replies. These sources are untrusted, and a renderer's npm license field may omit compiled components with separate distribution obligations. + +## Decision + +The shared Markdown renderer enables settled `graphviz`/`dot`, `svg`, and `html` fences through localized `MarkdownLabels.preview`. `CodeBlock.preview` owns default visualization, source switching, and verbatim copying. `SourcePreview` owns loading, failure, and cancellation of stale result publication for both these documents and the [Mermaid preview](2026-09-07-web-mermaid-preview.md). Consumers without preview labels and streaming messages retain code. + +HTML uses DOMPurify with navigation attributes and document-loading elements forbidden, followed by an opaque `sandbox=""` iframe. A trusted CSP precedes source markup and permits only inline CSS, data images, and data fonts. Scripts, external resources, child frames, form submission, and same-origin access are unavailable. SVG is parsed as XML and placed in an image inside the frame, so SVG scripts and links never become active. Frames have a fixed scrollable viewport: measuring their content would require additional origin access or trusted frame scripts. Mermaid keeps its existing strict renderer and inert image presentation. + +Graphviz uses the pinned, unmodified `@viz-js/viz` 3.30.0 WebAssembly distribution with the `dot` layout engine. Its npm MIT declaration covers the wrapper; its build provenance identifies Graphviz 16.0.0 (EPL-2.0), Expat 2.8.4 (MIT), and Emscripten 5.0.7 (MIT/NCSA). Distribution retains these component terms and the exact Graphviz source download under EPL-2.0 section 3.1. [Full preview notices](../../../../packages/client/ui-primitives/THIRD_PARTY_PREVIEW_NOTICES.txt) also retain Mermaid's MIT text and select Apache-2.0 for DOMPurify. The UI primitives package ships the notices and the Web build emits the same bytes alongside its assets. License review covers embedded payloads separately from the general permissive npm-metadata policy. + +## Alternatives considered + +**Execute HTML scripts in a frame.** Static chat examples do not need script execution. An empty sandbox plus sanitization and CSP gives the preview fewer capabilities and avoids introducing a frame messaging protocol. + +**Insert rendered markup into Chat.** HTML styles and executable SVG would share the application document. Iframes isolate layout and origin; SVG image mode additionally disables SVG behavior. + +**Treat Viz.js as MIT-only or use a remote Graphviz service.** The compiled Graphviz license still applies locally; a remote renderer would send conversation content off-device. The bundled renderer retains source availability and legal notices without network rendering. + +## Consequences + +No Session event, prompt, or tool changes. Inline HTML styles work, while interactive scripts, external assets, and links do not. Graphviz adds a lazy WebAssembly asset and synchronous browser-thread layout; source cancellation prevents publication but cannot preempt active layout. Mermaid's existing layout limitations remain documented in its owning note. + +Component tests cover source copying, default preview, fallback, cancellation, and recovery. The keyless [browser scenario](../../../../apps/web/tests/markdown-mermaid.e2e.ts) verifies opaque frames, real image decoding, source toggling, blocked script/navigation/resource attempts, locale snapshots, and served license text. Notices checks pin the reviewed wrapper and native provenance so upgrades require renewed review. diff --git a/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.zh.md b/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.zh.md new file mode 100644 index 0000000000..92f4b03087 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.zh.md @@ -0,0 +1,31 @@ +# Agent Note: 静态 Markdown fence 预览 + +Status: implemented + +[English](2026-09-09-markdown-static-previews.md) | 中文 + +## Problem + +读者需要直接在 Assistant 回复中查看 DOT 图表、SVG 图像和 HTML 示例。这些源码不可信,而渲染器的 npm 许可证字段可能遗漏编译组件各自的分发义务。 + +## Decision + +共享 Markdown 渲染器通过本地化的 `MarkdownLabels.preview` 启用定稿后的 `graphviz`/`dot`、`svg` 和 `html` fence。`CodeBlock.preview` 负责默认可视化、源码切换与原样复制。`SourcePreview` 为这些文档及 [Mermaid 预览](2026-09-07-web-mermaid-preview.zh.md)负责加载、失败和取消过期结果发布。未提供预览文案的调用方与流式消息保留代码显示。 + +HTML 先由 DOMPurify 清理,禁止导航属性与文档加载元素,再进入不透明来源的 `sandbox=""` iframe。可信 CSP 位于源码标记之前,仅允许内联 CSS、data 图片与 data 字体。脚本、外部资源、子 iframe、表单提交与同源访问均不可用。SVG 按 XML 解析后作为图片放进 iframe,因此 SVG 脚本与链接不会激活。iframe 使用固定的可滚动视口:测量内容需要额外的来源访问权限或可信 iframe 脚本。Mermaid 保留既有的严格渲染器与不可执行图片展示。 + +Graphviz 使用固定且未修改的 `@viz-js/viz` 3.30.0 WebAssembly 发布包,布局引擎为 `dot`。其 npm MIT 声明覆盖包装层;构建来源记录标明 Graphviz 16.0.0(EPL-2.0)、Expat 2.8.4(MIT)与 Emscripten 5.0.7(MIT/NCSA)。分发保留各组件条款,并按 EPL-2.0 第 3.1 节提供准确的 Graphviz 源码下载地址。[完整预览声明](../../../../packages/client/ui-primitives/THIRD_PARTY_PREVIEW_NOTICES.txt)还保留 Mermaid 的 MIT 文本,并为 DOMPurify 选择 Apache-2.0。UI primitives 包携带该声明,Web 构建在资源旁输出相同字节。内嵌产物的许可证检查独立于通用的宽松 npm 元数据策略。 + +## Alternatives considered + +**在 iframe 内执行 HTML 脚本。** 静态聊天示例不需要脚本执行。空 sandbox、清理与 CSP 降低预览权限,也避免引入 iframe 消息协议。 + +**把渲染标记插入 Chat。** HTML 样式和可执行 SVG 会与应用共享文档。iframe 隔离布局与来源,SVG 图片模式额外禁用 SVG 行为。 + +**将 Viz.js 视为仅使用 MIT,或使用远程 Graphviz 服务。** 本地编译的 Graphviz 仍受其许可证约束;远程渲染会把对话内容发送到设备之外。内置渲染器保留源码可获取性与法律声明,无需网络渲染。 + +## Consequences + +不改变 Session 事件、提示词或工具。HTML 内联样式可用,交互脚本、外部资源和链接不可用。Graphviz 增加按需加载的 WebAssembly 资源与浏览器线程同步布局;源码取消可以阻止结果发布,但无法抢占进行中的布局。Mermaid 既有布局限制仍记录在其所属决策中。 + +组件测试覆盖源码复制、默认预览、回退、取消与恢复。无密钥[浏览器场景](../../../../apps/web/tests/markdown-mermaid.e2e.ts)验证不透明来源 iframe、真实图片解码、源码切换、脚本/导航/资源请求被阻止、本地化快照和已提供的许可证文本。声明检查固定已审查的包装层与原生构建来源,升级时须重新审查。 diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index c2705ac530..2452325d62 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -55,6 +55,7 @@ External packages installed for runtime use or distributed inside the prebuilt b | [`@shikijs/langs`](https://github.com/shikijs/shiki) | MIT | | [`@standard-schema/spec`](https://github.com/standard-schema/standard-schema) | MIT | | [`@tanstack/react-virtual`](https://github.com/TanStack/virtual) | MIT | +| [`@viz-js/viz`](https://github.com/mdaines/viz-js) | MIT | | [`@vscode/ripgrep`](https://github.com/microsoft/vscode-ripgrep) | MIT | | [`@xterm/headless`](https://github.com/xtermjs/xterm.js) | MIT | | [`@yarnpkg/parsers`](https://github.com/yarnpkg/berry) | BSD-2-Clause | @@ -66,6 +67,7 @@ External packages installed for runtime use or distributed inside the prebuilt b | [`commander`](https://github.com/tj/commander.js) | MIT | | [`compression`](https://github.com/expressjs/compression) | MIT | | [`diff`](https://github.com/kpdecker/jsdiff) | BSD-3-Clause | +| [`dompurify`](https://github.com/cure53/DOMPurify) | (MPL-2.0 OR Apache-2.0) | | [`e2b`](https://github.com/e2b-dev/e2b) | MIT | | [`electron-updater`](https://github.com/electron-userland/electron-builder) | MIT | | [`eventsource-parser`](https://github.com/rexxars/eventsource-parser) | MIT | @@ -113,6 +115,8 @@ External packages installed for runtime use or distributed inside the prebuilt b | [`zod`](https://github.com/colinhacks/zod) | MIT | | [`zustand`](https://github.com/pmndrs/zustand) | MIT | +The Markdown preview distribution also contains Graphviz 16.0.0 (EPL-2.0), Expat 2.8.4 (MIT), and Emscripten 5.0.7 runtime code (MIT/NCSA) inside `@viz-js/viz` 3.30.0. The wrapper's MIT metadata does not relicense these components. [Preview notices](packages/client/ui-primitives/THIRD_PARTY_PREVIEW_NOTICES.txt) preserve their full license texts and Graphviz source availability, together with Mermaid's MIT and DOMPurify's selected Apache-2.0 terms. The UI primitives npm package includes this file; the Web build emits it as `preview-third-party-notices.txt`. + pnpm applies local patches to the following packages at install time, so shipped artifacts carry modified copies; each patch file is the complete record of the modification: - `@yao-pkg/pkg@6.21.0` — [`patches/@yao-pkg__pkg@6.21.0.patch`](patches/@yao-pkg__pkg@6.21.0.patch) diff --git a/apps/web/tests/expected/markdown-mermaid/ui.expected.md b/apps/web/tests/expected/markdown-mermaid/ui.expected.md index ac9ceb3164..369f0ef21d 100644 --- a/apps/web/tests/expected/markdown-mermaid/ui.expected.md +++ b/apps/web/tests/expected/markdown-mermaid/ui.expected.md @@ -13,3 +13,12 @@ - button "Source" - button "Copy" - img "Mermaid diagram" +- button "Source" +- button "Copy" +- iframe +- button "Source" +- button "Copy" +- iframe +- button "Source" +- button "Copy" +- iframe diff --git a/apps/web/tests/expected/markdown-mermaid/zh.expected.md b/apps/web/tests/expected/markdown-mermaid/zh.expected.md index 066e88f589..74a348e8be 100644 --- a/apps/web/tests/expected/markdown-mermaid/zh.expected.md +++ b/apps/web/tests/expected/markdown-mermaid/zh.expected.md @@ -12,3 +12,12 @@ - button "源码" - button "复制" - img "Mermaid 图表" +- button "源码" +- button "复制" +- iframe +- button "源码" +- button "复制" +- iframe +- button "源码" +- button "复制" +- iframe diff --git a/apps/web/tests/markdown-mermaid.e2e.ts b/apps/web/tests/markdown-mermaid.e2e.ts index 82789caa63..c009e1d836 100644 --- a/apps/web/tests/markdown-mermaid.e2e.ts +++ b/apps/web/tests/markdown-mermaid.e2e.ts @@ -26,6 +26,15 @@ const UNTRUSTED = [ ' click B "javascript:alert(1)"', ].join('\n') +const DOT = 'digraph { rankdir=LR; Input -> Preview }' +const SVG = 'SVG preview' +const HTML = `

Static HTML

+ + +Disabled navigation + +` + function fixture(): string { const session = Session.create(SessionId('markdown-mermaid-source')) session.append('turn/start', { turn: 1 }) @@ -41,6 +50,7 @@ function fixture(): string { content: [{ type: 'text', text: [ '# Mermaid previews', ...[FLOW, SEQUENCE, INVALID, UNTRUSTED].map(code => `\`\`\`mermaid\n${code}\n\`\`\``), + ...[['dot', DOT], ['svg', SVG], ['html', HTML]].map(([lang, code]) => `\`\`\`${lang}\n${code}\n\`\`\``), ].join('\n\n') }], source: { kind: 'model', provider: 'fixture', model: 'fixture' }, }), @@ -132,7 +142,7 @@ describe('web e2e: Mermaid chat previews', () => { const page = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: 'zh-CN', timezoneId: 'Asia/Shanghai', hasTouch: true }) await openConversation(page, scaffold) await expect.poll(() => page.getByRole('img', { name: 'Mermaid 图表' }).count(), { timeout: 20_000 }).toBe(3) - expect(await page.getByRole('button', { name: '源码', exact: true }).count()).toBe(4) + expect(await page.getByRole('button', { name: '源码', exact: true }).count()).toBe(7) const controls = page.locator('.md-code-block').first().locator('[class*="bannerWrap"]') expect(await controls.evaluate(node => getComputedStyle(node).opacity)).toBe('1') expect(await page.getByRole('button', { name: '源码', exact: true }).first() @@ -148,6 +158,49 @@ describe('web e2e: Mermaid chat previews', () => { await page.close() }, 60_000) + it.skipIf(MODE === 'record')('previews DOT, SVG, and static HTML inside isolated frames', async () => { + const page = await newEnglishPage(browser) + const requests: string[] = [] + const dialogs: string[] = [] + // Routing observes requests eligible for transport; Chromium also emits 'request' for CSP-blocked URLs. + await page.route('https://preview.invalid/**', async (route) => { + requests.push(route.request().url()) + await route.abort() + }) + page.on('dialog', (dialog) => { dialogs.push(dialog.message()); void dialog.dismiss() }) + await page.context().grantPermissions(['clipboard-read', 'clipboard-write']) + await openConversation(page, scaffold) + for (const [title, code] of [['Graphviz diagram', DOT], ['SVG preview', SVG], ['HTML preview', HTML]]) { + const frame = page.locator(`iframe[title="${title}"]`) + await frame.waitFor() + expect(await frame.getAttribute('sandbox')).toBe('') + const body = frame.contentFrame() + if (title === 'HTML preview') { + await body.getByRole('heading', { name: 'Static HTML' }).waitFor() + expect(await body.getByRole('heading').evaluate(node => getComputedStyle(node).color)).toBe('rgb(0, 128, 0)') + await body.getByText('Disabled navigation').click() + expect(await frame.evaluate(node => (node as HTMLIFrameElement).contentDocument)).toBeNull() + } else { + await expect.poll(() => body.locator('img').evaluate(node => (node as HTMLImageElement).naturalWidth)).toBeGreaterThan(0) + } + const block = page.locator('.md-code-block').filter({ has: frame }) + await block.hover() + await block.getByRole('button', { name: 'Copy', exact: true }).click() + expect(await page.evaluate(() => navigator.clipboard.readText())).toBe(code) + await block.getByRole('button', { name: 'Source', exact: true }).click() + expect(await block.locator('pre code').textContent()).toBe(code) + await block.getByRole('button', { name: 'Preview', exact: true }).click() + expect(await frame.isVisible()).toBe(true) + } + expect(await page.locator('body').getAttribute('data-preview-escaped')).toBeNull() + expect(dialogs).toEqual([]) + expect(requests).toEqual([]) + const notices = await page.request.get(new URL('/preview-third-party-notices.txt', scaffold.authenticatedUrl).href) + expect(notices.ok()).toBe(true) + expect(await notices.text()).toContain('Eclipse Public License - v 2.0') + await page.close() + }) + it.skipIf(MODE === 'record')('keeps actions visible when a mouse and touchscreen are both available', async () => { const hybridBrowser = await chromium.launch({ args: ['--blink-settings=availablePointerTypes=6,primaryPointerType=4,availableHoverTypes=2,primaryHoverType=2'], diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 0e15b4f088..f6c90861f1 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -27,6 +27,20 @@ function clientDocumentTitle(): Plugin { } } +/** Preserve preview licenses and Graphviz source availability in every browser distribution. */ +function previewNotices(): Plugin { + return { + name: 'dsh-preview-notices', + async generateBundle() { + this.emitFile({ + type: 'asset', + fileName: 'preview-third-party-notices.txt', + source: await readFile(src('../../packages/client/ui-primitives/THIRD_PARTY_PREVIEW_NOTICES.txt'), 'utf8'), + }) + }, + } +} + /** Fail before a Vite dev or preview server can expose the boot-manifest-free shell. */ function rejectStandaloneServe(): Plugin { return { @@ -147,7 +161,7 @@ export default defineConfig({ // Relative asset URLs: preview.html mounts the same output under any base // directory, and the served index resolves identically from the site root. base: './', - plugins: [rejectStandaloneServe(), clientDocumentTitle(), react(), emitPreviewPage()], + plugins: [previewNotices(), rejectStandaloneServe(), clientDocumentTitle(), react(), emitPreviewPage()], build: { // The worker bootstrap holds its page at top-level await; Vite's default // `modules` target (es2020-era) rejects that syntax. diff --git a/packages/client/locale/src/locales/en.ts b/packages/client/locale/src/locales/en.ts index 6261002e3a..1d97566db1 100644 --- a/packages/client/locale/src/locales/en.ts +++ b/packages/client/locale/src/locales/en.ts @@ -38,6 +38,11 @@ export const en = { 'json.expandNode': 'Expand JSON node', 'json.label': 'JSON', 'markdown.footnotes': 'Footnotes', + 'markdown.preview.graphviz': 'Graphviz diagram', + 'markdown.preview.svg': 'SVG preview', + 'markdown.preview.html': 'HTML preview', + 'markdown.preview.loading': 'Rendering preview…', + 'markdown.preview.error': 'Unable to render this preview. The source is shown below.', 'markdown.mermaid.diagram': 'Mermaid diagram', 'markdown.mermaid.preview': 'Preview', 'markdown.mermaid.source': 'Source', diff --git a/packages/client/locale/src/locales/zh.ts b/packages/client/locale/src/locales/zh.ts index 9a03ac9b7f..a799548c28 100644 --- a/packages/client/locale/src/locales/zh.ts +++ b/packages/client/locale/src/locales/zh.ts @@ -36,6 +36,11 @@ export const zh = { 'json.expandNode': '展开 JSON 节点', 'json.label': 'JSON', 'markdown.footnotes': '脚注', + 'markdown.preview.graphviz': 'Graphviz 图表', + 'markdown.preview.svg': 'SVG 预览', + 'markdown.preview.html': 'HTML 预览', + 'markdown.preview.loading': '正在渲染预览…', + 'markdown.preview.error': '无法渲染此预览,源码如下。', 'markdown.mermaid.diagram': 'Mermaid 图表', 'markdown.mermaid.preview': '预览', 'markdown.mermaid.source': '源码', diff --git a/packages/client/ui-chat/README.i18n.yaml b/packages/client/ui-chat/README.i18n.yaml index 386c06ea91..554709bb19 100644 --- a/packages/client/ui-chat/README.i18n.yaml +++ b/packages/client/ui-chat/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-chat/README.md -README.md: 951f88e5fa61a27802fb7df11a3347ba9676a66c -README.zh.md: 3a180afb43c4262d6054e1f19deefa490b9b36de +README.md: 1bd7f008e46b695899c703c928ebe8bb76fbe4c8 +README.zh.md: 5919622396dde5b0c8cbf51f71999672a11f13ec diff --git a/packages/client/ui-chat/README.md b/packages/client/ui-chat/README.md index 951f88e5fa..1bd7f008e4 100644 --- a/packages/client/ui-chat/README.md +++ b/packages/client/ui-chat/README.md @@ -33,7 +33,7 @@ Each nonempty appended `system/message` owns a collapsed prompt row, including a ## Mermaid previews -Settled Assistant Markdown shows `mermaid` code fences as diagrams. Diagrams have no language banner. Source/Preview and Copy icon actions appear on hover or keyboard focus and remain visible on touch devices; both views copy the original Mermaid source. Streaming fences remain code until the message settles; invalid diagrams show an error and their source without disrupting other blocks. Chat enables the shared [Mermaid preview primitive](../ui-primitives/README.md) through localized labels. This is a presentation change and adds no model prompt, tool, or Session event. +Settled Assistant Markdown shows `mermaid` code fences as diagrams. Diagrams have no language banner. Source/Preview and Copy icon actions appear on hover or keyboard focus and remain visible on touch devices; both views copy the original Mermaid source. Streaming fences remain code until the message settles; invalid diagrams show an error and their source without disrupting other blocks. Chat enables the shared [Mermaid preview primitive](../ui-primitives/README.md) through localized labels. Chat also previews `graphviz`/`dot`, `svg`, and static `html` fences in isolated iframes, with the same default visualization, source switching, and copying. HTML scripts, navigation, and remote resources are disabled. This is a presentation change and adds no model prompt, tool, or Session event. ----- diff --git a/packages/client/ui-chat/README.zh.md b/packages/client/ui-chat/README.zh.md index 3a180afb43..5919622396 100644 --- a/packages/client/ui-chat/README.zh.md +++ b/packages/client/ui-chat/README.zh.md @@ -35,7 +35,7 @@ kind: "package-reference" ## Mermaid 预览 -已定稿的 Assistant Markdown 把 `mermaid` 代码 fence 显示为图表。图表不显示语言标题栏。源码/预览切换与复制图标在悬停或键盘聚焦时出现,触屏上保持可见;两种视图复制的都是原始 Mermaid 源码。流式 fence 在消息定稿前保持代码显示;非法图表显示错误与源码,不影响其他块。Chat 通过本地化 label 启用共享的 [Mermaid 预览原语](../ui-primitives/README.zh.md)。这项展示变更不增加模型提示词、工具或 Session 事件。 +已定稿的 Assistant Markdown 把 `mermaid` 代码 fence 显示为图表。图表不显示语言标题栏。源码/预览切换与复制图标在悬停或键盘聚焦时出现,触屏上保持可见;两种视图复制的都是原始 Mermaid 源码。流式 fence 在消息定稿前保持代码显示;非法图表显示错误与源码,不影响其他块。Chat 通过本地化 label 启用共享的 [Mermaid 预览原语](../ui-primitives/README.zh.md)。Chat 还在隔离 iframe 中预览 `graphviz`/`dot`、`svg` 和静态 `html` fence,共用默认可视化、源码切换与复制操作。HTML 脚本、导航与远程资源均被禁用。这项展示变更不增加模型提示词、工具或 Session 事件。 ----- diff --git a/packages/client/ui-chat/src/client/markdown-labels.ts b/packages/client/ui-chat/src/client/markdown-labels.ts index d42e6e5f83..ea70175c57 100644 --- a/packages/client/ui-chat/src/client/markdown-labels.ts +++ b/packages/client/ui-chat/src/client/markdown-labels.ts @@ -12,6 +12,15 @@ export function markdownLabels(t: ChatViewSlotProps['t']): MarkdownLabels { return { code: { copyLabel: t('copy'), copiedLabel: t('copied') }, footnotes: t('markdown.footnotes'), + preview: { + graphviz: t('markdown.preview.graphviz'), + svg: t('markdown.preview.svg'), + html: t('markdown.preview.html'), + preview: t('markdown.mermaid.preview'), + source: t('markdown.mermaid.source'), + loading: t('markdown.preview.loading'), + error: t('markdown.preview.error'), + }, mermaid: { diagram: t('markdown.mermaid.diagram'), preview: t('markdown.mermaid.preview'), diff --git a/packages/client/ui-primitives/README.i18n.yaml b/packages/client/ui-primitives/README.i18n.yaml index a0fd81c2bc..bdd6417054 100644 --- a/packages/client/ui-primitives/README.i18n.yaml +++ b/packages/client/ui-primitives/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-primitives/README.md -README.md: 0ba4d6baaa54bbacc82202c490423b37613874df -README.zh.md: 06a266b61a79d4ff5e38ca0de9877a6d8ec66af0 +README.md: a1e877c5249817c7ab2e9fa1f6a8ad9582c44a3a +README.zh.md: 81923da65cc21f794d2e7f5d822feb69c06f7499 diff --git a/packages/client/ui-primitives/README.md b/packages/client/ui-primitives/README.md index 0ba4d6baaa..a1e877c524 100644 --- a/packages/client/ui-primitives/README.md +++ b/packages/client/ui-primitives/README.md @@ -78,6 +78,10 @@ The catalog above lists what each export is for; this section covers the behavio Supply `MarkdownLabels.mermaid` to enable `mermaid` fence previews in a Markdown consumer; without it, fences remain code. Previews start after the message settles. `CodeBlock.preview` supplies an optional headerless body with icon actions for source switching and copying. Actions appear on hover or keyboard focus, and stay visible below the diagram whenever a touchscreen is available, including with a mouse attached. Switching to source keeps the preview mounted, so returning reuses its result; copying always retains the source. [The Mermaid decision](../../../.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.md) records the rendering and reuse choices. +Supply `MarkdownLabels.preview` to enable `graphviz`/`dot`, `svg`, and `html` fences. Each settled fence defaults to visualization and shares the source/copy controls. Graphviz loads `@viz-js/viz` on demand and renders DOT with the `dot` engine. SVG and Graphviz output are inert images inside opaque sandbox iframes; malformed SVG and DOT retain their source with an error. HTML accepts complete documents or fragments and preserves inline styles, but strips scripts and navigation. Frames have no sandbox permissions, and their Content Security Policy blocks external resources, embedded frames, forms, and scripts. Frames use a 400px scrollable viewport. Raw HTML outside a code fence remains ordinary Markdown input. + +[Preview license notices](THIRD_PARTY_PREVIEW_NOTICES.txt) ship with this package and the Web frontend. Mermaid is MIT; Viz.js is MIT but embeds Graphviz under EPL-2.0. The notices retain license texts and the exact Graphviz source download. Dependency upgrades require reviewing the embedded payload as well as npm metadata; the [preview decision](../../../.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.md) records the distribution and sandbox choices. + ### Localizing copy The atoms cannot read the application locale, so every piece of user-facing copy arrives through required label props. `HoverCard`, `TerminalBlock`, `JsonTree`, `CodeBlock`, `MarkdownText`, `JsonBlock`, `ConnectionIndicator`, `Modal`, `DiffBlock`, `ReadBlock`, `SearchBlock`, and `WebBlock` accept complete localized labels. The package owns no language fallback; omission fails typechecking, and each feature maps its typed `t` seat into the primitive's label interface. @@ -145,7 +149,7 @@ None; this package neither assembles nor sends a provider request. These limits define how the atoms behave at the edges; they are current package constraints, not a component roadmap. -- **Mermaid rendering runs on the browser thread** — every mounted settled preview starts rendering, including off-screen diagrams. Mermaid serializes layouts; work already submitted to it cannot be interrupted. Preview virtualization and worker rendering are not provided. +- **Diagram rendering runs on the browser thread** — every mounted settled preview starts rendering, including off-screen diagrams. Mermaid serializes layouts; Graphviz performs synchronous WebAssembly layout. Submitted layout work cannot be interrupted. Preview virtualization and worker rendering are not provided. - **Streaming defers cross-boundary reference resolution** — a reference-style link or footnote whose definition sits on the other side of the incremental freeze boundary renders as literal text while the reply streams; the settled full parse at finalize resolves it. - **A long highlighted fence retains its complete token DOM** — streaming avoids re-parsing, re-tokenizing, and reconciling the completed prefix, but it does not discard old colors or virtualize token spans. Final DOM cardinality therefore still follows the fence's token count; nested/container fences and a pathological single long line remain on the general tail path. - **Glyph-level icons are redrawn approximations** — the fish logo and the sparkle mark come from font glyphs whose vector geometry is not exportable from the local design data; hand-authored recreations stand in until an exact export path exists. diff --git a/packages/client/ui-primitives/README.zh.md b/packages/client/ui-primitives/README.zh.md index 06a266b61a..81923da65c 100644 --- a/packages/client/ui-primitives/README.zh.md +++ b/packages/client/ui-primitives/README.zh.md @@ -78,6 +78,10 @@ kind: "package-library" 为 Markdown 调用方传入 `MarkdownLabels.mermaid` 即可启用 `mermaid` fence 预览;未传入时,fence 保持代码显示。预览在消息定稿后开始。`CodeBlock.preview` 提供无标题栏的替代正文,以及切换源码和复制的图标操作。操作在悬停或键盘聚焦时出现;只要设备具备触屏,就在图表下方保持可见,包括同时连接鼠标的情况。切换到源码时保留已挂载的预览,返回时复用其结果;复制始终保留源码。[Mermaid 决策](../../../.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.zh.md)记录了渲染与复用选择。 +传入 `MarkdownLabels.preview` 可启用 `graphviz`/`dot`、`svg` 和 `html` fence。每个定稿 fence 默认显示可视化,并共享源码切换与复制操作。Graphviz 按需加载 `@viz-js/viz`,用 `dot` 引擎渲染 DOT。SVG 与 Graphviz 输出作为不可执行的图片放入不透明来源的 sandbox iframe;非法 SVG 与 DOT 显示错误并保留源码。HTML 接受完整文档或片段、保留内联样式,同时移除脚本与导航。iframe 不授予任何 sandbox 权限,Content Security Policy 阻止外部资源、嵌套 iframe、表单与脚本。iframe 使用可滚动的 400px 视口。代码 fence 外的原始 HTML 仍按普通 Markdown 输入处理。 + +[预览许可证声明](THIRD_PARTY_PREVIEW_NOTICES.txt)随本包与 Web 前端分发。Mermaid 使用 MIT;Viz.js 使用 MIT,但内嵌 Graphviz 使用 EPL-2.0。声明保留完整许可证文本及准确的 Graphviz 源码下载地址。升级依赖时必须同时检查内嵌产物和 npm 元数据;[预览决策](../../../.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.zh.md)记录分发与 sandbox 选择。 + ### 本地化文案 这些原子组件无法读取应用 locale,因此每段面向用户的文案都必须通过 label prop 提供。`HoverCard`、`TerminalBlock`、`JsonTree`、`CodeBlock`、`MarkdownText`、`JsonBlock`、`ConnectionIndicator`、`Modal`、`DiffBlock`、`ReadBlock`、`SearchBlock` 与 `WebBlock` 接收完整的本地化 label。本包不拥有语言回退;遗漏会导致类型检查失败,各功能会把带类型的 `t` 席位映射到 primitive 的 label 接口。 @@ -145,7 +149,7 @@ kind: "package-library" 这些限制说明原子组件在边缘情况下的行为;它们是当前包约束,不是组件路线图。 -- **Mermaid 渲染在浏览器线程上执行**:每个已挂载的定稿预览都会开始渲染,包括视口外的图表。Mermaid 串行执行布局,已提交给它的工作无法中断。当前不提供预览虚拟化或 worker 渲染。 +- **图表渲染在浏览器线程上执行**:每个已挂载的定稿预览都会开始渲染,包括视口外的图表。Mermaid 串行执行布局;Graphviz 同步执行 WebAssembly 布局。已提交的布局工作无法中断。当前不提供预览虚拟化或 worker 渲染。 - **流式期间跨边界引用解析被推迟**:定义落在增量冻结边界另一侧的引用式链接或脚注,在回复流式输出期间渲染为字面文本;定稿时的全量解析会将其解析。 - **长高亮 fence 会保留完整 token DOM**:流式路径避免重新解析、重新 tokenize 和 reconcile 已完成前缀,但不会丢弃旧颜色或虚拟化 token span。因此最终 DOM 数量仍随 fence 的 token 数增长;嵌套/容器内 fence 与病态的单个超长行仍走通用尾部路径。 - **字形级图标是重新绘制的近似版本**:鱼形标志与闪光标记来自字体字形,而本地设计数据无法导出其矢量几何;在获得精确导出路径前,使用手工重建版本代替。 diff --git a/packages/client/ui-primitives/THIRD_PARTY_PREVIEW_NOTICES.txt b/packages/client/ui-primitives/THIRD_PARTY_PREVIEW_NOTICES.txt new file mode 100644 index 0000000000..3e1156a536 --- /dev/null +++ b/packages/client/ui-primitives/THIRD_PARTY_PREVIEW_NOTICES.txt @@ -0,0 +1,688 @@ +Markdown preview third-party notices + +Mermaid 11.16.0: MIT; source https://github.com/mermaid-js/mermaid/tree/mermaid%4011.16.0 +@viz-js/viz 3.30.0: MIT JavaScript wrapper; source and build instructions: +https://github.com/mdaines/viz-js/tree/99da545270e6e7b127a5c7ba65974b8a604a6358 + +The Viz.js WebAssembly payload contains Graphviz 16.0.0 under EPL-2.0, +Expat 2.8.4 under MIT, and Emscripten 5.0.7 runtime code under MIT/NCSA. +Graphviz remains under EPL-2.0; it is not relicensed as MIT by DeepSeek Harness. +Graphviz source code is available under EPL-2.0 at: +https://gitlab.com/api/v4/projects/4207231/packages/generic/graphviz-releases/16.0.0/graphviz-16.0.0.tar.gz +SHA-256: 36a1de1aaf5a2023b14f95170a5f8f0b12522d1c305b517fc261966597050749 +Viz.js's backend build instructions and additions are available at the commit above. +DeepSeek Harness uses the unmodified upstream Viz.js package. + +Expat source: https://github.com/libexpat/libexpat/tree/R_2_8_4 +Emscripten source: https://github.com/emscripten-core/emscripten/tree/5.0.7 +DOMPurify 3.4.11: Apache-2.0 OR MPL-2.0; this distribution selects Apache-2.0. +Source: https://github.com/cure53/DOMPurify/tree/3.4.11 + +The following license texts and copyright notices apply to their respective components. + + +===== Mermaid ===== + +The MIT License (MIT) + +Copyright (c) 2014 - 2022 Knut Sveidqvist + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +===== Viz.js ===== + +MIT License + +Copyright (c) Michael Daines + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +===== Graphviz ===== + +Eclipse Public License - v 2.0 + + THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE + PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION + OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + +1. DEFINITIONS + +"Contribution" means: + + a) in the case of the initial Contributor, the initial content + Distributed under this Agreement, and + + b) in the case of each subsequent Contributor: + i) changes to the Program, and + ii) additions to the Program; + where such changes and/or additions to the Program originate from + and are Distributed by that particular Contributor. A Contribution + "originates" from a Contributor if it was added to the Program by + such Contributor itself or anyone acting on such Contributor's behalf. + Contributions do not include changes or additions to the Program that + are not Modified Works. + +"Contributor" means any person or entity that Distributes the Program. + +"Licensed Patents" mean patent claims licensable by a Contributor which +are necessarily infringed by the use or sale of its Contribution alone +or when combined with the Program. + +"Program" means the Contributions Distributed in accordance with this +Agreement. + +"Recipient" means anyone who receives the Program under this Agreement +or any Secondary License (as applicable), including Contributors. + +"Derivative Works" shall mean any work, whether in Source Code or other +form, that is based on (or derived from) the Program and for which the +editorial revisions, annotations, elaborations, or other modifications +represent, as a whole, an original work of authorship. + +"Modified Works" shall mean any work in Source Code or other form that +results from an addition to, deletion from, or modification of the +contents of the Program, including, for purposes of clarity any new file +in Source Code form that contains any contents of the Program. Modified +Works shall not include works that contain only declarations, +interfaces, types, classes, structures, or files of the Program solely +in each case in order to link to, bind by name, or subclass the Program +or Modified Works thereof. + +"Distribute" means the acts of a) distributing or b) making available +in any manner that enables the transfer of a copy. + +"Source Code" means the form of a Program preferred for making +modifications, including but not limited to software source code, +documentation source, and configuration files. + +"Secondary License" means either the GNU General Public License, +Version 2.0, or any later versions of that license, including any +exceptions or additional permissions as identified by the initial +Contributor. + +2. GRANT OF RIGHTS + + a) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free copyright + license to reproduce, prepare Derivative Works of, publicly display, + publicly perform, Distribute and sublicense the Contribution of such + Contributor, if any, and such Derivative Works. + + b) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free patent + license under Licensed Patents to make, use, sell, offer to sell, + import and otherwise transfer the Contribution of such Contributor, + if any, in Source Code or other form. This patent license shall + apply to the combination of the Contribution and the Program if, at + the time the Contribution is added by the Contributor, such addition + of the Contribution causes such combination to be covered by the + Licensed Patents. The patent license shall not apply to any other + combinations which include the Contribution. No hardware per se is + licensed hereunder. + + c) Recipient understands that although each Contributor grants the + licenses to its Contributions set forth herein, no assurances are + provided by any Contributor that the Program does not infringe the + patent or other intellectual property rights of any other entity. + Each Contributor disclaims any liability to Recipient for claims + brought by any other entity based on infringement of intellectual + property rights or otherwise. As a condition to exercising the + rights and licenses granted hereunder, each Recipient hereby + assumes sole responsibility to secure any other intellectual + property rights needed, if any. For example, if a third party + patent license is required to allow Recipient to Distribute the + Program, it is Recipient's responsibility to acquire that license + before distributing the Program. + + d) Each Contributor represents that to its knowledge it has + sufficient copyright rights in its Contribution, if any, to grant + the copyright license set forth in this Agreement. + + e) Notwithstanding the terms of any Secondary License, no + Contributor makes additional grants to any Recipient (other than + those set forth in this Agreement) as a result of such Recipient's + receipt of the Program under the terms of a Secondary License + (if permitted under the terms of Section 3). + +3. REQUIREMENTS + +3.1 If a Contributor Distributes the Program in any form, then: + + a) the Program must also be made available as Source Code, in + accordance with section 3.2, and the Contributor must accompany + the Program with a statement that the Source Code for the Program + is available under this Agreement, and informs Recipients how to + obtain it in a reasonable manner on or through a medium customarily + used for software exchange; and + + b) the Contributor may Distribute the Program under a license + different than this Agreement, provided that such license: + i) effectively disclaims on behalf of all other Contributors all + warranties and conditions, express and implied, including + warranties or conditions of title and non-infringement, and + implied warranties or conditions of merchantability and fitness + for a particular purpose; + + ii) effectively excludes on behalf of all other Contributors all + liability for damages, including direct, indirect, special, + incidental and consequential damages, such as lost profits; + + iii) does not attempt to limit or alter the recipients' rights + in the Source Code under section 3.2; and + + iv) requires any subsequent distribution of the Program by any + party to be under a license that satisfies the requirements + of this section 3. + +3.2 When the Program is Distributed as Source Code: + + a) it must be made available under this Agreement, or if the + Program (i) is combined with other material in a separate file or + files made available under a Secondary License, and (ii) the initial + Contributor attached to the Source Code the notice described in + Exhibit A of this Agreement, then the Program may be made available + under the terms of such Secondary Licenses, and + + b) a copy of this Agreement must be included with each copy of + the Program. + +3.3 Contributors may not remove or alter any copyright, patent, +trademark, attribution notices, disclaimers of warranty, or limitations +of liability ("notices") contained within the Program from any copy of +the Program which they Distribute, provided that Contributors may add +their own appropriate notices. + +4. COMMERCIAL DISTRIBUTION + +Commercial distributors of software may accept certain responsibilities +with respect to end users, business partners and the like. While this +license is intended to facilitate the commercial use of the Program, +the Contributor who includes the Program in a commercial product +offering should do so in a manner which does not create potential +liability for other Contributors. Therefore, if a Contributor includes +the Program in a commercial product offering, such Contributor +("Commercial Contributor") hereby agrees to defend and indemnify every +other Contributor ("Indemnified Contributor") against any losses, +damages and costs (collectively "Losses") arising from claims, lawsuits +and other legal actions brought by a third party against the Indemnified +Contributor to the extent caused by the acts or omissions of such +Commercial Contributor in connection with its distribution of the Program +in a commercial product offering. The obligations in this section do not +apply to any claims or Losses relating to any actual or alleged +intellectual property infringement. In order to qualify, an Indemnified +Contributor must: a) promptly notify the Commercial Contributor in +writing of such claim, and b) allow the Commercial Contributor to control, +and cooperate with the Commercial Contributor in, the defense and any +related settlement negotiations. The Indemnified Contributor may +participate in any such claim at its own expense. + +For example, a Contributor might include the Program in a commercial +product offering, Product X. That Contributor is then a Commercial +Contributor. If that Commercial Contributor then makes performance +claims, or offers warranties related to Product X, those performance +claims and warranties are such Commercial Contributor's responsibility +alone. Under this section, the Commercial Contributor would have to +defend claims against the other Contributors related to those performance +claims and warranties, and if a court requires any other Contributor to +pay any damages as a result, the Commercial Contributor must pay +those damages. + +5. NO WARRANTY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" +BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR +IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF +TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR +PURPOSE. Each Recipient is solely responsible for determining the +appropriateness of using and distributing the Program and assumes all +risks associated with its exercise of rights under this Agreement, +including but not limited to the risks and costs of program errors, +compliance with applicable laws, damage to or loss of data, programs +or equipment, and unavailability or interruption of operations. + +6. DISCLAIMER OF LIABILITY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS +SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST +PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE +EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + +7. GENERAL + +If any provision of this Agreement is invalid or unenforceable under +applicable law, it shall not affect the validity or enforceability of +the remainder of the terms of this Agreement, and without further +action by the parties hereto, such provision shall be reformed to the +minimum extent necessary to make such provision valid and enforceable. + +If Recipient institutes patent litigation against any entity +(including a cross-claim or counterclaim in a lawsuit) alleging that the +Program itself (excluding combinations of the Program with other software +or hardware) infringes such Recipient's patent(s), then such Recipient's +rights granted under Section 2(b) shall terminate as of the date such +litigation is filed. + +All Recipient's rights under this Agreement shall terminate if it +fails to comply with any of the material terms or conditions of this +Agreement and does not cure such failure in a reasonable period of +time after becoming aware of such noncompliance. If all Recipient's +rights under this Agreement terminate, Recipient agrees to cease use +and distribution of the Program as soon as reasonably practicable. +However, Recipient's obligations under this Agreement and any licenses +granted by Recipient relating to the Program shall continue and survive. + +Everyone is permitted to copy and distribute copies of this Agreement, +but in order to avoid inconsistency the Agreement is copyrighted and +may only be modified in the following manner. The Agreement Steward +reserves the right to publish new versions (including revisions) of +this Agreement from time to time. No one other than the Agreement +Steward has the right to modify this Agreement. The Eclipse Foundation +is the initial Agreement Steward. The Eclipse Foundation may assign the +responsibility to serve as the Agreement Steward to a suitable separate +entity. Each new version of the Agreement will be given a distinguishing +version number. The Program (including Contributions) may always be +Distributed subject to the version of the Agreement under which it was +received. In addition, after a new version of the Agreement is published, +Contributor may elect to Distribute the Program (including its +Contributions) under the new version. + +Except as expressly stated in Sections 2(a) and 2(b) above, Recipient +receives no rights or licenses to the intellectual property of any +Contributor under this Agreement, whether expressly, by implication, +estoppel or otherwise. All rights in the Program not expressly granted +under this Agreement are reserved. Nothing in this Agreement is intended +to be enforceable by any entity that is not a Contributor or Recipient. +No third-party beneficiary rights are created under this Agreement. + +Exhibit A - Form of Secondary Licenses Notice + +"This Source Code may also be made available under the following +Secondary Licenses when the conditions for such availability set forth +in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), +version(s), and exceptions or additional permissions here}." + + Simply including a copy of this Agreement, including this Exhibit A + is not sufficient to license the Source Code under Secondary Licenses. + + If it is not possible or desirable to put the notice in a particular + file, then You may include the notice in a location (such as a LICENSE + file in a relevant directory) where a recipient would be likely to + look for such a notice. + + You may add additional accurate notices of copyright ownership. + + +===== Expat ===== + +Copyright (c) 1998-2000 Thai Open Source Software Center Ltd and Clark Cooper +Copyright (c) 2001-2025 Expat maintainers + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +===== Emscripten ===== + +Emscripten is available under 2 licenses, the MIT license and the +University of Illinois/NCSA Open Source License. + +Both are permissive open source licenses, with little if any +practical difference between them. + +The reason for offering both is that (1) the MIT license is +well-known, while (2) the University of Illinois/NCSA Open Source +License allows Emscripten's code to be integrated upstream into +LLVM, which uses that license, should the opportunity arise. + +The full text of both licenses follows. + +============================================================================== + +Copyright (c) 2010-2014 Emscripten authors, see AUTHORS file. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +============================================================================== + +Copyright (c) 2010-2014 Emscripten authors, see AUTHORS file. +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal with the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + + Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimers. + + Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following disclaimers + in the documentation and/or other materials provided with the + distribution. + + Neither the names of Mozilla, + nor the names of its contributors may be used to endorse + or promote products derived from this Software without specific prior + written permission. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE SOFTWARE. + +============================================================================== + +This program uses portions of Node.js source code located in src/library_path.js, +in accordance with the terms of the MIT license. Node's license follows: + + """ + Copyright Joyent, Inc. and other Node contributors. All rights reserved. + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + IN THE SOFTWARE. + """ + +The musl libc project is bundled in this repo, and it has the MIT license, see +system/lib/libc/musl/COPYRIGHT + +The third_party/ subdirectory contains code with other licenses. None of it is +used by default, but certain options use it (e.g., the optional closure compiler +flag will run closure compiler from third_party/). + + +===== DOMPurify ===== + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/client/ui-primitives/package.json b/packages/client/ui-primitives/package.json index 6502b6564d..587cd95ee6 100644 --- a/packages/client/ui-primitives/package.json +++ b/packages/client/ui-primitives/package.json @@ -46,11 +46,14 @@ "mdast-util-math": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "micromark-util-sanitize-uri": "^2.0.1", - "mermaid": "11.16.0" + "mermaid": "11.16.0", + "@viz-js/viz": "3.30.0", + "dompurify": "3.4.11" }, "files": [ "lib/index.js", "lib/**/*.css", + "THIRD_PARTY_PREVIEW_NOTICES.txt", "lib/types/**/*.d.ts" ], "peerDependencies": { diff --git a/packages/client/ui-primitives/src/markdown/MermaidPreview.module.css b/packages/client/ui-primitives/src/markdown/MermaidPreview.module.css index f96c14418b..e21d84e284 100644 --- a/packages/client/ui-primitives/src/markdown/MermaidPreview.module.css +++ b/packages/client/ui-primitives/src/markdown/MermaidPreview.module.css @@ -24,3 +24,12 @@ visibility: hidden; pointer-events: none; } + +.frame { + display: block; + width: 100%; + height: 400px; + border: 0; + border-radius: 12px; + background: var(--dsw-static-neutral-00); +} diff --git a/packages/client/ui-primitives/src/markdown/MermaidPreview.tsx b/packages/client/ui-primitives/src/markdown/MermaidPreview.tsx index 5dadb6a6b3..dc330e9da8 100644 --- a/packages/client/ui-primitives/src/markdown/MermaidPreview.tsx +++ b/packages/client/ui-primitives/src/markdown/MermaidPreview.tsx @@ -1,51 +1,17 @@ -/** Read-only diagram preview with source fallback and per-source async ownership. */ +/** Mermaid image preview using the shared asynchronous source renderer. */ -import { useEffect, useState } from 'react' import { renderMermaid } from './mermaid.ts' -import css from './MermaidPreview.module.css' +import { SourcePreview } from './SourcePreview.tsx' +import type { PreviewLabels } from './SourcePreview.tsx' -/** Localized preview states; the diagram source remains verbatim. */ -export interface MermaidPreviewLabels { - diagram: string - loading: string - error: string -} - -type Result = { kind: 'ok'; code: string; src: string } | { kind: 'error'; code: string } - -/* v8 ignore next 3 -- closed-union backstop; only reached if a result is forged */ -function assertNever(value: never): never { - throw new Error(`unreachable Mermaid result: ${String(value)}`) -} +/** Localized Mermaid preview states; source remains verbatim. */ +export type MermaidPreviewLabels = PreviewLabels /** - * Display a complete Mermaid document on a light diagram canvas, or its source if rendering fails. - * @param props - Source and complete localized labels. Changing source discards the previous result. - * @returns A loading status, an inert SVG image, or an error with the original source. + * Display a complete Mermaid document as an inert SVG image. + * @param props - Source and complete localized labels. + * @returns A loading status, diagram image, or error with the original source. */ export function MermaidPreview({ code, labels }: { code: string; labels: MermaidPreviewLabels }) { - const [result, setResult] = useState(null) - useEffect(() => { - const controller = new AbortController() - void renderMermaid(code, controller.signal).then( - (src) => { if (!controller.signal.aborted) setResult({ kind: 'ok', code, src }) }, - () => { if (!controller.signal.aborted) setResult({ kind: 'error', code }) }, - ) - return () => { controller.abort() } - }, [code]) - - if (result?.code !== code) return
{labels.loading}
- switch (result.kind) { - case 'ok': - return
{labels.diagram}
- case 'error': - return ( -
-
{labels.error}
-
{code}
-
- ) - /* v8 ignore next -- closed-union backstop; only reached if a result is forged */ - default: return assertNever(result) - } + return } diff --git a/packages/client/ui-primitives/src/markdown/SourcePreview.tsx b/packages/client/ui-primitives/src/markdown/SourcePreview.tsx new file mode 100644 index 0000000000..376b69710a --- /dev/null +++ b/packages/client/ui-primitives/src/markdown/SourcePreview.tsx @@ -0,0 +1,56 @@ +/** Read-only diagram preview with source fallback and per-source async ownership. */ + +import { useEffect, useState } from 'react' +import css from './MermaidPreview.module.css' + +/** Localized preview states; the diagram source remains verbatim. */ +export interface PreviewLabels { + diagram: string + loading: string + error: string +} + +type Result = { kind: 'ok'; code: string; src: string } | { kind: 'error'; code: string } + +/* v8 ignore next 3 -- closed-union backstop; only reached if a result is forged */ +function assertNever(value: never): never { + throw new Error(`unreachable preview result: ${String(value)}`) +} + +/** + * Display a complete document or diagram, retaining the source when rendering fails. + * @param props - Source and complete localized labels. Changing source discards the previous result. + * @returns A loading status, an inert image or sandboxed document, or an error with the original source. + */ +export function SourcePreview({ code, labels, render, document: isDocument = false }: { + code: string + labels: PreviewLabels + render: (code: string, signal: AbortSignal) => string | Promise + document?: boolean +}) { + const [result, setResult] = useState(null) + useEffect(() => { + const controller = new AbortController() + void (async () => await render(code, controller.signal))().then( + (src) => { if (!controller.signal.aborted) setResult({ kind: 'ok', code, src }) }, + () => { if (!controller.signal.aborted) setResult({ kind: 'error', code }) }, + ) + return () => { controller.abort() } + }, [code, render]) + + if (result?.code !== code) return
{labels.loading}
+ switch (result.kind) { + case 'ok': + if (isDocument) return +
`, new AbortController().signal) + const parsed = new DOMParser().parseFromString(doc, 'text/html') + expect(parsed.querySelector('h1')?.textContent).toBe('Example') + expect(parsed.querySelector('style')?.textContent).toBe('h1 { color: green }') + expect(parsed.querySelector('script, iframe, base, [href], [action], [formaction], [onload]')).toBeNull() + expect(parsed.querySelectorAll('meta[http-equiv]').length).toBe(1) + expect(parsed.querySelector('meta[http-equiv]')?.getAttribute('content')).toContain("default-src 'none'") + }) + + it('encodes SVG as an image rather than executable document markup', () => { + const code = '' + const parsed = new DOMParser().parseFromString(renderSvg(code, new AbortController().signal), 'text/html') + expect(parsed.querySelector('svg, script')).toBeNull() + expect(decodeURIComponent(parsed.querySelector('img')!.getAttribute('src')!)).toContain(code) + }) + + it.each(['', '', ''])('rejects non-SVG or malformed XML: %s', (code) => { + expect(() => renderSvg(code, new AbortController().signal)).toThrow('Invalid SVG document') + }) + + it.each([renderHtml, renderSvg])('does not prepare a cancelled document', (renderDocument) => { + const controller = new AbortController() + controller.abort() + expect(() => renderDocument(svg, controller.signal)).toThrow(controller.signal.reason) + }) +}) diff --git a/packages/client/ui-primitives/tests/graphviz-runtime.client.spec.ts b/packages/client/ui-primitives/tests/graphviz-runtime.client.spec.ts new file mode 100644 index 0000000000..354c0f9eaa --- /dev/null +++ b/packages/client/ui-primitives/tests/graphviz-runtime.client.spec.ts @@ -0,0 +1,35 @@ +// @vitest-environment jsdom + +import { afterEach, beforeEach, expect, it, vi } from 'vitest' + +const instance = vi.fn() +beforeEach(() => { + vi.resetModules() + instance.mockReset() + vi.doMock('@viz-js/viz', () => ({ instance })) +}) +afterEach(() => { vi.doUnmock('@viz-js/viz') }) + +it('retries failed initialization and reuses the loaded runtime', async () => { + const renderString = vi.fn().mockReturnValue('') + instance.mockRejectedValueOnce(new Error('WASM unavailable')).mockResolvedValue({ renderString }) + const { renderGraphviz } = await import('../src/markdown/graphviz.ts') + await expect(renderGraphviz('digraph {}', new AbortController().signal)).rejects.toThrow('WASM unavailable') + await expect(renderGraphviz('digraph {}', new AbortController().signal)).resolves.toContain('data:image/svg+xml') + await renderGraphviz('digraph {}', new AbortController().signal) + expect(instance).toHaveBeenCalledTimes(2) +}) + +it('skips layout after cancellation during runtime loading', async () => { + const pending = Promise.withResolvers<{ renderString: ReturnType }>() + instance.mockReturnValue(pending.promise) + const { renderGraphviz } = await import('../src/markdown/graphviz.ts') + const controller = new AbortController() + const result = renderGraphviz('digraph {}', controller.signal) + const rejection = expect(result).rejects.toMatchObject({ name: 'AbortError' }) + controller.abort() + const renderString = vi.fn() + pending.resolve({ renderString }) + await rejection + expect(renderString).not.toHaveBeenCalled() +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d1c1098a48..6f08b3708d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3305,12 +3305,18 @@ importers: '@types/react-dom': specifier: ~18.3.0 version: 18.3.7(@types/react@18.3.31) + '@viz-js/viz': + specifier: 3.30.0 + version: 3.30.0 anser: specifier: ^2.3.5 version: 2.3.5 clsx: specifier: ^2.0.0 version: 2.1.1 + dompurify: + specifier: 3.4.11 + version: 3.4.11 katex: specifier: ^0.16.47 version: 0.16.47 @@ -14255,6 +14261,9 @@ packages: '@vitest/utils@4.1.8': resolution: {integrity: sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==} + '@viz-js/viz@3.30.0': + resolution: {integrity: sha512-2zmcP55QY44uQfZ+GvgNHmGd8S6KUD+2eMur9AbYybSzrOd/m8nMUg0ZircJNiJRKGgVBKAE8kh0hAwpUFUFQQ==} + '@vscode/ripgrep-darwin-arm64@1.18.0': resolution: {integrity: sha512-r3ktHSvbFycQNF6sl7sNDPocpsI7J+mEzh1IaZFkY0spm3k2Z9t8hPAeOK7+p0l6p6/swkQC14XWX01low+94Q==} cpu: [arm64] @@ -20864,6 +20873,8 @@ snapshots: convert-source-map: 2.0.0 tinyrainbow: 3.1.0 + '@viz-js/viz@3.30.0': {} + '@vscode/ripgrep-darwin-arm64@1.18.0': optional: true diff --git a/scripts/check-workspace-constraints.spec.ts b/scripts/check-workspace-constraints.spec.ts index 5ac40d3c99..d3dcddb787 100644 --- a/scripts/check-workspace-constraints.spec.ts +++ b/scripts/check-workspace-constraints.spec.ts @@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest' import { checkDshFamilyVersion, + checkWorkspaceManifest, checkExperimentalDependencyIsolation, checkExperimentalManifest, expectedDshPackageFiles, @@ -121,3 +122,16 @@ describe('package payload constraints', () => { ]) }) }) + +it('requires preview license notices in the published UI primitives payload', () => { + const name = '@deepseek-ai/dsh-client-ui-primitives' + const files = expectedDshPackageFiles({ name }) + expect(files).toContain('THIRD_PARTY_PREVIEW_NOTICES.txt') + const check = (entries: readonly string[]) => checkWorkspaceManifest({ + dir: 'packages/client/ui-primitives', manifest: { name, files: [...entries] }, + }).filter(message => message.includes('package.json files')) + expect(check(files)).toEqual([]) + expect(check(files.filter(file => file !== 'THIRD_PARTY_PREVIEW_NOTICES.txt'))).toEqual([ + expect.stringContaining('package.json files must be'), + ]) +}) diff --git a/scripts/check-workspace-constraints.ts b/scripts/check-workspace-constraints.ts index e7b0e948e8..73f32d72a6 100644 --- a/scripts/check-workspace-constraints.ts +++ b/scripts/check-workspace-constraints.ts @@ -154,7 +154,7 @@ const packageFileExtras: Readonly> = { // them through its own CSS pipeline, so the sheets are published artifacts. // The glob covers whichever sheets a package emits; sourcemaps stay // unpublished, as everywhere else in the repository. - '@deepseek-ai/dsh-client-ui-primitives': ['lib/**/*.css'], + '@deepseek-ai/dsh-client-ui-primitives': ['lib/**/*.css', 'THIRD_PARTY_PREVIEW_NOTICES.txt'], '@deepseek-ai/dsh-client-ui-dockkit': ['lib/**/*.css'], '@deepseek-ai/dsh-client-web': ['lib/**/*.css'], '@deepseek-ai/dsh-client-ui-theme': ['lib/styles'], diff --git a/scripts/gen-third-party-notices.spec.ts b/scripts/gen-third-party-notices.spec.ts index fabce6e8c9..46b1b35d11 100644 --- a/scripts/gen-third-party-notices.spec.ts +++ b/scripts/gen-third-party-notices.spec.ts @@ -4,6 +4,7 @@ import { tmpdir } from 'node:os' import { describe, expect, it } from 'vitest' import { CLAUDE_AGENT_SDK_PACKAGE, + assertPreviewDistribution, assertRuntimeLicenses, claudeDistributionFromManifest, collectPythonDependencies, @@ -395,3 +396,26 @@ describe('manifestPatterns', () => { ]) }) }) + +describe('preview distribution licenses', () => { + const dependencies = { mermaid: '11.16.0', '@viz-js/viz': '3.30.0', dompurify: '3.4.11' } + const sources = [ + 'pkg:docker/emscripten/emsdk@5.0.7?platform=linux%2Famd64', + 'https://github.com/libexpat/libexpat/releases/download/R_2_8_4/expat-2.8.4.tar.gz', + 'https://gitlab.com/api/v4/projects/4207231/packages/generic/graphviz-releases/16.0.0/graphviz-16.0.0.tar.gz', + ] + it('accepts the reviewed wrapper and native sources', () => { + expect(() => { assertPreviewDistribution(dependencies, sources) }).not.toThrow() + const notices = readFileSync(resolve(root, 'packages/client/ui-primitives/THIRD_PARTY_PREVIEW_NOTICES.txt'), 'utf8') + expect(notices).toContain('Eclipse Public License - v 2.0') + expect(notices).toContain(sources[2]) + expect(notices).toContain(readFileSync(resolve(root, 'packages/client/ui-primitives/node_modules/mermaid/LICENSE'), 'utf8').trim()) + expect(notices).toContain(readFileSync(resolve(root, 'packages/client/ui-primitives/node_modules/dompurify/LICENSE'), 'utf8').trim()) + }) + it.each(Object.keys(dependencies))('rejects an unreviewed %s upgrade', (name) => { + expect(() => { assertPreviewDistribution({ ...dependencies, [name]: '99.0.0' }, sources) }).toThrow('preview notices: review') + }) + it.each([sources.slice(1), [...sources, 'https://example.com/new-library'], [...sources.slice(0, 2), 'graphviz-next']])('rejects changed embedded sources', (...uris) => { + expect(() => { assertPreviewDistribution(dependencies, uris) }).toThrow('native sources') + }) +}) diff --git a/scripts/gen-third-party-notices.ts b/scripts/gen-third-party-notices.ts index 9e83956e5e..2130723202 100644 --- a/scripts/gen-third-party-notices.ts +++ b/scripts/gen-third-party-notices.ts @@ -685,6 +685,28 @@ ${rows.join('\n')} ` } +/** + * Require renewed license review when a preview dependency or embedded source changes. + * @param dependencies - UI primitives' browser build dependencies. + * @param sources - Native source URIs from the installed Viz.js build provenance. + * @returns Nothing; throws when a version or native source differs from the reviewed distribution. + */ +export function assertPreviewDistribution(dependencies: Record, sources: readonly string[]): void { + for (const [name, version] of Object.entries({ mermaid: '11.16.0', '@viz-js/viz': '3.30.0', dompurify: '3.4.11' })) { + if (dependencies[name] !== version) { + throw new Error(`preview notices: review ${name} ${String(dependencies[name])} licenses and update THIRD_PARTY_PREVIEW_NOTICES.txt`) + } + } + const expected = [ + 'pkg:docker/emscripten/emsdk@5.0.7?platform=linux%2Famd64', + 'https://github.com/libexpat/libexpat/releases/download/R_2_8_4/expat-2.8.4.tar.gz', + 'https://gitlab.com/api/v4/projects/4207231/packages/generic/graphviz-releases/16.0.0/graphviz-16.0.0.tar.gz', + ] + if (sources.length !== expected.length || expected.some(uri => !sources.includes(uri))) { + throw new Error('preview notices: review the changed Viz.js native sources and their distribution licenses') + } +} + /** * Render the complete notices document. * @returns The exact bytes THIRD_PARTY_NOTICES.md must hold after resolving browser inputs. @@ -695,6 +717,16 @@ export async function render(): Promise { // the manifests map it was resolved from; render() owns that single load. workspaceLinkedManifestCache.clear() const { manifests, names } = loadWorkspaceManifests() + const previewPackage = 'packages/client/ui-primitives' + const provenance = JSON.parse(readFileSync(resolve(root, previewPackage, 'node_modules/@viz-js/viz/lib/provenance.json'), 'utf8')) as { + predicate: { buildDefinition: { resolvedDependencies: { uri: string }[] } } + } + const previewDependencies = manifests.get(`${previewPackage}/package.json`)?.devDependencies + if (previewDependencies === undefined) throw new Error('preview notices: missing UI primitives browser dependencies') + assertPreviewDistribution( + previewDependencies, + provenance.predicate.buildDefinition.resolvedDependencies.map(source => source.uri), + ) const npm = collectNpmDeps(manifests, names, browser) const runtimeDeps = npm.filter(dep => dep.runtime) const devDeps = npm.filter(dep => !dep.runtime) @@ -735,6 +767,8 @@ External packages installed for runtime use or distributed inside the prebuilt b ${renderNpmTable(runtimeDeps)} +The Markdown preview distribution also contains Graphviz 16.0.0 (EPL-2.0), Expat 2.8.4 (MIT), and Emscripten 5.0.7 runtime code (MIT/NCSA) inside \`@viz-js/viz\` 3.30.0. The wrapper's MIT metadata does not relicense these components. [Preview notices](packages/client/ui-primitives/THIRD_PARTY_PREVIEW_NOTICES.txt) preserve their full license texts and Graphviz source availability, together with Mermaid's MIT and DOMPurify's selected Apache-2.0 terms. The UI primitives npm package includes this file; the Web build emits it as \`preview-third-party-notices.txt\`. + pnpm applies local patches to the following packages at install time, so shipped artifacts carry modified copies; each patch file is the complete record of the modification: ${patchedLines.join('\n')} From fb84576ea7a4cae41b52997901d722ab02cbc2f5 Mon Sep 17 00:00:00 2001 From: yudshj Date: Wed, 9 Sep 2026 13:11:55 +0800 Subject: [PATCH 07/27] refactor(web): unify markdown preview components and notes --- .../2026-09-07-web-mermaid-preview.i18n.yaml | 6 --- .../feature/2026-09-07-web-mermaid-preview.md | 35 ------------- .../2026-09-07-web-mermaid-preview.zh.md | 35 ------------- ...6-09-09-markdown-static-previews.i18n.yaml | 4 +- .../2026-09-09-markdown-static-previews.md | 24 ++++++--- .../2026-09-09-markdown-static-previews.zh.md | 24 ++++++--- packages/client/locale/src/locales/en.ts | 4 +- packages/client/locale/src/locales/zh.ts | 4 +- .../ui-chat/src/client/markdown-labels.ts | 25 ++++------ .../client/ui-primitives/README.i18n.yaml | 4 +- packages/client/ui-primitives/README.md | 7 ++- packages/client/ui-primitives/README.zh.md | 7 ++- packages/client/ui-primitives/src/index.ts | 2 - .../src/markdown/CodeBlock.module.css | 50 +++++++++---------- .../src/markdown/MermaidPreview.tsx | 17 ------- ...ew.module.css => SourcePreview.module.css} | 0 .../src/markdown/SourcePreview.tsx | 2 +- .../ui-primitives/src/markdown/mermaid.ts | 2 +- .../ui-primitives/src/markdown/render.tsx | 30 ++++------- .../tests/fence-previews.client.spec.tsx | 22 ++++---- ...pec.tsx => source-preview.client.spec.tsx} | 43 ++++++++++++---- 21 files changed, 144 insertions(+), 203 deletions(-) delete mode 100644 .agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.i18n.yaml delete mode 100644 .agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.md delete mode 100644 .agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.zh.md delete mode 100644 packages/client/ui-primitives/src/markdown/MermaidPreview.tsx rename packages/client/ui-primitives/src/markdown/{MermaidPreview.module.css => SourcePreview.module.css} (100%) rename packages/client/ui-primitives/tests/{mermaid-preview.client.spec.tsx => source-preview.client.spec.tsx} (73%) diff --git a/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.i18n.yaml b/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.i18n.yaml deleted file mode 100644 index 190b6ffb3e..0000000000 --- a/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.i18n.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# side as of the last confirmed-consistent state. Both languages carry equal authority; -# after editing either side, bring the other along and re-record with: -# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.md -2026-09-07-web-mermaid-preview.md: 246b38e764e1e5812427f935c4a41c28f0fa24a0 -2026-09-07-web-mermaid-preview.zh.md: c1dd25c1b6dc21c36d92e0d7c1477fcea9ae99c2 diff --git a/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.md b/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.md deleted file mode 100644 index 246b38e764..0000000000 --- a/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.md +++ /dev/null @@ -1,35 +0,0 @@ -# Agent Note: Mermaid previews in Chat code fences - -Status: implemented - -English | [中文](2026-09-07-web-mermaid-preview.zh.md) - -## Problem - -Assistant replies can describe diagrams in Mermaid code fences, but readers must interpret the source or copy it to another renderer. Rendering must handle incomplete streaming input and untrusted diagram content while remaining reusable outside Chat. - -## Decision - -Chat enables settled fence previews through `MarkdownLabels.mermaid`. The shared Markdown renderer uses the parsed fence language; streaming fences and consumers without those labels retain code display. The static [UI primitives package](../../../../packages/client/ui-primitives/README.md) owns `MermaidPreview`, which accepts source and localized labels without Session, file, or Cordis dependencies. `CodeBlock.preview` owns view switching and source copying. Previews omit the language banner and expose compact icon actions on hover or keyboard focus; devices with any touch input keep those actions visible below the diagram. Switching to source hides the mounted preview, so returning preserves completed rendering and pending work. Source and preview share the same focused toggle button, and copying always reads the source prop. - -Mermaid loads on demand. Its public render API serializes diagram work, and each call removes its temporary measurement DOM in `finally`. Strict security, disabled HTML labels, the neutral theme, and error-rendering policy cannot be overridden by diagram configuration. Generated SVG is displayed as an image without installing diagram links or scripts. Intrinsic dimensions come from the SVG viewBox; large diagrams shrink to fit, and the canvas stays light in both application themes. - -Source replacement and unmounting cancel result publication. Cancellation before runtime loading completes prevents rendering; an active Mermaid render finishes and releases its DOM but cannot publish to a cancelled component. Failures show a localized error and the original source. Replacing invalid source with a valid diagram recovers the preview. - -## Alternatives considered - -**Keep the code banner above diagrams.** A language label and permanent text controls distract from the diagram. Overlay actions retain source access without reserving a title row. - -**Render each streamed chunk.** Incomplete diagrams are frequently invalid, and repeated layout work competes with text streaming. The existing message-settlement boundary provides a complete source value. - -**Put rendering inside Chat or add a general preview registry.** A shared primitive with plain props satisfies reuse without another registry or feature-plugin dependency. This follows the [shared-control rule](../architecture/2026-09-05-shared-client-control-primitives.md). - -**Insert interactive SVG into the message.** The requested preview needs diagram display and source access. Displaying the result as an image keeps diagram links and event handlers inactive. - -The [static fence preview decision](2026-09-09-markdown-static-previews.md) owns Graphviz, SVG, and HTML sandboxing and shared preview license distribution. - -## Consequences - -The feature changes presentation without changing persisted messages, provider requests, tools, or Host APIs. Mermaid adds lazily loaded browser assets. Rendering still runs on the browser thread, and work already handed to the library cannot be interrupted. The initial feature has no editing, export, zoom controls, or interactive diagram links. - -Component tests cover delayed completion, stale success and failure, unmounting, source fallback, and copying. The keyless [browser scenario](../../../../apps/web/tests/markdown-mermaid.e2e.ts) exercises the assembled Chat with Chinese flowcharts, sequence diagrams, malformed source, configuration overrides, and English/Chinese UI snapshots. diff --git a/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.zh.md b/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.zh.md deleted file mode 100644 index c1dd25c1b6..0000000000 --- a/.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.zh.md +++ /dev/null @@ -1,35 +0,0 @@ -# Agent Note: Chat 代码 fence 中的 Mermaid 预览 - -Status: implemented - -[English](2026-09-07-web-mermaid-preview.md) | 中文 - -## 问题 - -Assistant 回复可以在 Mermaid 代码 fence 中描述图表,但读者必须理解源码或把它复制到其他渲染器。渲染需要处理不完整的流式输入与不可信图表内容,同时保持在 Chat 之外的可复用性。 - -## 决策 - -Chat 通过 `MarkdownLabels.mermaid` 启用已定稿 fence 预览。共享 Markdown 渲染器使用解析后的 fence 语言;流式 fence 和未传入这些 label 的调用方保留代码显示。静态 [UI primitives 包](../../../../packages/client/ui-primitives/README.zh.md)拥有 `MermaidPreview`,它接收源码与本地化 label,不依赖 Session、文件或 Cordis。`CodeBlock.preview` 拥有视图切换与源码复制。预览省略语言标题栏,悬停或键盘聚焦时显示紧凑的图标操作;具备任何触控输入的设备均在图表下方保持操作可见。切换到源码时隐藏已挂载的预览,因此返回时保留已完成的渲染与进行中的工作。源码与预览使用同一个已聚焦的切换按钮,复制始终读取源码 prop。 - -Mermaid 按需加载。它的公开 render API 串行执行图表工作,每次调用在 `finally` 中移除临时测量 DOM。图表配置无法覆盖严格安全模式、禁用 HTML label、中性主题和错误渲染策略。生成的 SVG 以图片显示,不安装图内链接或脚本。固有尺寸取自 SVG viewBox;大图缩小以适应宽度,画布在两种应用主题下均保持浅色。 - -替换源码与卸载组件会取消结果发布。在运行时加载完成前取消可阻止渲染;已经开始的 Mermaid 渲染会完成并释放 DOM,但无法向已取消的组件发布结果。失败时显示本地化错误和原始源码。用有效图表替换非法源码可恢复预览。 - -## 考虑过的替代方案 - -**在图表上方保留代码标题栏。** 语言标记和常驻的文字操作分散了对图表的注意力。浮层操作无需占用标题行即可保留源码入口。 - -**渲染每个流式分片。** 不完整的图表经常无效,反复布局也会与文本流式输出竞争。已有的消息定稿边界提供完整源码值。 - -**把渲染放在 Chat 内或增加通用预览注册表。** 接收普通 prop 的共享原语无需另一套注册表或功能插件依赖即可满足复用。这遵循[共享控件规则](../architecture/2026-09-05-shared-client-control-primitives.zh.md)。 - -**把可交互 SVG 插入消息。** 此次预览需求只需要图表显示与源码访问。将结果显示为图片后,图内链接和事件处理器不会生效。 - -[静态 fence 预览决策](2026-09-09-markdown-static-previews.zh.md)负责 Graphviz、SVG、HTML 的 sandbox 及共享预览许可证分发。 - -## 后果 - -功能只改变展示,不改变持久化消息、提供者请求、工具或 Host API。Mermaid 增加按需加载的浏览器资源。渲染仍在浏览器线程上执行,已交给库的工作无法中断。首版不包含编辑、导出、缩放控件或可交互图内链接。 - -组件测试覆盖延迟完成、过期成功与失败、卸载、源码回退和复制。无密钥[浏览器场景](../../../../apps/web/tests/markdown-mermaid.e2e.ts)通过完整 Chat 组合验证中文流程图、时序图、非法源码、配置覆盖以及英文/中文 UI 快照。 diff --git a/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.i18n.yaml b/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.i18n.yaml index 6cb3a179eb..c70073de17 100644 --- a/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-09-markdown-static-previews.md -2026-09-09-markdown-static-previews.md: fd3772cabc2160f07e79b06dffa4374fe104cbd1 -2026-09-09-markdown-static-previews.zh.md: 92f4b030879acaeb484eed33e078574750a1801e +2026-09-09-markdown-static-previews.md: a6d1802546e9e976da7ae4d1ecc024924414e9a4 +2026-09-09-markdown-static-previews.zh.md: 42488547efafdbf5b1944b1300af13f359365d74 diff --git a/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.md b/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.md index fd3772cabc..a6d1802546 100644 --- a/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.md +++ b/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.md @@ -6,26 +6,38 @@ English | [中文](2026-09-09-markdown-static-previews.zh.md) ## Problem -Readers need to see DOT diagrams, SVG artwork, and HTML examples directly in Assistant replies. These sources are untrusted, and a renderer's npm license field may omit compiled components with separate distribution obligations. +Readers need to see Mermaid and DOT diagrams, SVG artwork, and HTML examples directly in Assistant replies without interpreting source or copying it to another renderer. These sources are untrusted, and a renderer's npm license field may omit compiled components with separate distribution obligations. ## Decision -The shared Markdown renderer enables settled `graphviz`/`dot`, `svg`, and `html` fences through localized `MarkdownLabels.preview`. `CodeBlock.preview` owns default visualization, source switching, and verbatim copying. `SourcePreview` owns loading, failure, and cancellation of stale result publication for both these documents and the [Mermaid preview](2026-09-07-web-mermaid-preview.md). Consumers without preview labels and streaming messages retain code. +The shared Markdown renderer enables settled `mermaid`, `graphviz`/`dot`, `svg`, and `html` fences through localized `MarkdownLabels.preview` and the parsed fence language. The [UI primitives package](../../../../packages/client/ui-primitives/README.md) owns `SourcePreview`, which accepts a renderer, source, and labels without Session, file, or Cordis dependencies. Mermaid and Graphviz each supply a `.ts` renderer to that same component. Consumers without preview labels and streaming messages retain code. -HTML uses DOMPurify with navigation attributes and document-loading elements forbidden, followed by an opaque `sandbox=""` iframe. A trusted CSP precedes source markup and permits only inline CSS, data images, and data fonts. Scripts, external resources, child frames, form submission, and same-origin access are unavailable. SVG is parsed as XML and placed in an image inside the frame, so SVG scripts and links never become active. Frames have a fixed scrollable viewport: measuring their content would require additional origin access or trusted frame scripts. Mermaid keeps its existing strict renderer and inert image presentation. +`CodeBlock.preview` owns default visualization, source switching, and verbatim copying. Previews omit the language banner and expose compact icon actions on hover or keyboard focus; devices with any touch input keep actions visible below the diagram. Switching to source hides the mounted preview, preserving completed rendering and pending work. Both views use the same focused toggle button, and copying always reads the source prop. + +`SourcePreview` owns loading, failure, and cancellation of stale result publication. Source replacement and unmounting cancel publication; cancellation before runtime loading completes skips layout. Failures show a localized error and the original source, and replacing invalid source with valid input recovers the preview. + +Mermaid loads on demand. Its public render API serializes diagram work, and each call removes its temporary measurement DOM in `finally`. Strict security, disabled HTML labels, the neutral theme, and error-rendering policy cannot be overridden by diagram configuration. Generated SVG is displayed as an image without installing links or scripts. Intrinsic dimensions come from the SVG viewBox; large diagrams shrink to fit, and the canvas stays light in both application themes. + +HTML uses DOMPurify with navigation attributes and document-loading elements forbidden, followed by an opaque `sandbox=""` iframe. A trusted CSP precedes source markup and permits only inline CSS, data images, and data fonts. Scripts, external resources, child frames, form submission, and same-origin access are unavailable. SVG is parsed as XML and placed in an image inside the frame, so SVG scripts and links never become active. Frames have a fixed scrollable viewport: measuring their content would require additional origin access or trusted frame scripts. Graphviz uses the pinned, unmodified `@viz-js/viz` 3.30.0 WebAssembly distribution with the `dot` layout engine. Its npm MIT declaration covers the wrapper; its build provenance identifies Graphviz 16.0.0 (EPL-2.0), Expat 2.8.4 (MIT), and Emscripten 5.0.7 (MIT/NCSA). Distribution retains these component terms and the exact Graphviz source download under EPL-2.0 section 3.1. [Full preview notices](../../../../packages/client/ui-primitives/THIRD_PARTY_PREVIEW_NOTICES.txt) also retain Mermaid's MIT text and select Apache-2.0 for DOMPurify. The UI primitives package ships the notices and the Web build emits the same bytes alongside its assets. License review covers embedded payloads separately from the general permissive npm-metadata policy. ## Alternatives considered +**Keep the code banner above diagrams.** A language label and permanent text controls distract from the diagram. Overlay actions retain source access without reserving a title row. + +**Render each streamed chunk.** Incomplete diagrams are frequently invalid, and repeated layout work competes with text streaming. Message settlement provides complete source. + +**Put rendering inside Chat or add a general preview registry.** A shared primitive with plain props satisfies reuse without another registry or feature-plugin dependency. This follows the [shared-control rule](../architecture/2026-09-05-shared-client-control-primitives.md). + **Execute HTML scripts in a frame.** Static chat examples do not need script execution. An empty sandbox plus sanitization and CSP gives the preview fewer capabilities and avoids introducing a frame messaging protocol. -**Insert rendered markup into Chat.** HTML styles and executable SVG would share the application document. Iframes isolate layout and origin; SVG image mode additionally disables SVG behavior. +**Insert rendered markup into Chat.** The requested preview needs diagram display and source access; HTML styles and executable SVG would share the application document. Iframes isolate layout and origin; SVG image mode additionally disables SVG behavior. **Treat Viz.js as MIT-only or use a remote Graphviz service.** The compiled Graphviz license still applies locally; a remote renderer would send conversation content off-device. The bundled renderer retains source availability and legal notices without network rendering. ## Consequences -No Session event, prompt, or tool changes. Inline HTML styles work, while interactive scripts, external assets, and links do not. Graphviz adds a lazy WebAssembly asset and synchronous browser-thread layout; source cancellation prevents publication but cannot preempt active layout. Mermaid's existing layout limitations remain documented in its owning note. +The feature changes presentation without changing persisted messages, provider requests, tools, or Host APIs. Inline HTML styles work, while interactive scripts, external assets, and links do not. Mermaid and Graphviz add lazy browser assets and run layout on the browser thread. Cancellation cannot preempt active layout; Mermaid finishes and releases its measurement DOM even when its result cannot be published. The previews have no editing, export, zoom controls, or interactive diagram links. -Component tests cover source copying, default preview, fallback, cancellation, and recovery. The keyless [browser scenario](../../../../apps/web/tests/markdown-mermaid.e2e.ts) verifies opaque frames, real image decoding, source toggling, blocked script/navigation/resource attempts, locale snapshots, and served license text. Notices checks pin the reviewed wrapper and native provenance so upgrades require renewed review. +Component tests cover source copying, default preview, delayed completion, stale success and failure, unmounting, fallback, cancellation, runtime loading failure, and recovery. The keyless [browser scenario](../../../../apps/web/tests/markdown-mermaid.e2e.ts) verifies Chinese Mermaid flowcharts, sequence diagrams, malformed source, configuration overrides, opaque frames, real image decoding, source toggling, blocked script/navigation/resource attempts, English/Chinese UI snapshots, and served license text. Notices checks pin the reviewed wrapper and native provenance so upgrades require renewed review. diff --git a/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.zh.md b/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.zh.md index 92f4b03087..42488547ef 100644 --- a/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.zh.md +++ b/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.zh.md @@ -6,26 +6,38 @@ Status: implemented ## Problem -读者需要直接在 Assistant 回复中查看 DOT 图表、SVG 图像和 HTML 示例。这些源码不可信,而渲染器的 npm 许可证字段可能遗漏编译组件各自的分发义务。 +读者需要直接在 Assistant 回复中查看 Mermaid 和 DOT 图表、SVG 图像和 HTML 示例,而不必解读源码或复制到其他渲染器。这些源码不可信,而渲染器的 npm 许可证字段可能遗漏编译组件各自的分发义务。 ## Decision -共享 Markdown 渲染器通过本地化的 `MarkdownLabels.preview` 启用定稿后的 `graphviz`/`dot`、`svg` 和 `html` fence。`CodeBlock.preview` 负责默认可视化、源码切换与原样复制。`SourcePreview` 为这些文档及 [Mermaid 预览](2026-09-07-web-mermaid-preview.zh.md)负责加载、失败和取消过期结果发布。未提供预览文案的调用方与流式消息保留代码显示。 +共享 Markdown 渲染器通过本地化的 `MarkdownLabels.preview` 和解析后的 fence 语言启用定稿后的 `mermaid`、`graphviz`/`dot`、`svg` 和 `html` fence。[UI primitives 包](../../../../packages/client/ui-primitives/README.zh.md)拥有 `SourcePreview`,它接收渲染器、源码和文案,不依赖 Session、文件或 Cordis。Mermaid 与 Graphviz 各自向同一组件提供 `.ts` 渲染器。未提供预览文案的调用方与流式消息保留代码显示。 -HTML 先由 DOMPurify 清理,禁止导航属性与文档加载元素,再进入不透明来源的 `sandbox=""` iframe。可信 CSP 位于源码标记之前,仅允许内联 CSS、data 图片与 data 字体。脚本、外部资源、子 iframe、表单提交与同源访问均不可用。SVG 按 XML 解析后作为图片放进 iframe,因此 SVG 脚本与链接不会激活。iframe 使用固定的可滚动视口:测量内容需要额外的来源访问权限或可信 iframe 脚本。Mermaid 保留既有的严格渲染器与不可执行图片展示。 +`CodeBlock.preview` 负责默认可视化、源码切换与原样复制。预览省略语言横幅,在悬停或键盘聚焦时显示紧凑的图标操作;具有任何触控输入的设备始终在图表下方显示操作。切换到源码会隐藏已挂载的预览,保留完成的渲染和待完成的工作。两种视图使用同一个保持焦点的切换按钮,复制始终读取源码属性。 + +`SourcePreview` 负责加载、失败和取消过期结果发布。替换源码和卸载组件会取消结果发布;在运行时加载完成前取消会跳过布局。失败时显示本地化错误与原始源码,用有效输入替换无效源码后可以恢复预览。 + +Mermaid 按需加载。其公开渲染 API 串行执行图表工作,每次调用都在 `finally` 中删除临时测量 DOM。图表配置无法覆盖严格安全模式、禁用 HTML 标签、中性色主题和错误渲染策略。生成的 SVG 作为图片显示,不安装链接或脚本。固有尺寸来自 SVG viewBox;大图缩小以适应宽度,两种应用主题下的画布均保持浅色。 + +HTML 先由 DOMPurify 清理,禁止导航属性与文档加载元素,再进入不透明来源的 `sandbox=""` iframe。可信 CSP 位于源码标记之前,仅允许内联 CSS、data 图片与 data 字体。脚本、外部资源、子 iframe、表单提交与同源访问均不可用。SVG 按 XML 解析后作为图片放进 iframe,因此 SVG 脚本与链接不会激活。iframe 使用固定的可滚动视口:测量内容需要额外的来源访问权限或可信 iframe 脚本。 Graphviz 使用固定且未修改的 `@viz-js/viz` 3.30.0 WebAssembly 发布包,布局引擎为 `dot`。其 npm MIT 声明覆盖包装层;构建来源记录标明 Graphviz 16.0.0(EPL-2.0)、Expat 2.8.4(MIT)与 Emscripten 5.0.7(MIT/NCSA)。分发保留各组件条款,并按 EPL-2.0 第 3.1 节提供准确的 Graphviz 源码下载地址。[完整预览声明](../../../../packages/client/ui-primitives/THIRD_PARTY_PREVIEW_NOTICES.txt)还保留 Mermaid 的 MIT 文本,并为 DOMPurify 选择 Apache-2.0。UI primitives 包携带该声明,Web 构建在资源旁输出相同字节。内嵌产物的许可证检查独立于通用的宽松 npm 元数据策略。 ## Alternatives considered +**在图表上方保留代码横幅。** 语言标签和常驻文字控件会分散对图表的注意力。浮层操作保留源码访问,不占用标题行。 + +**渲染每个流式片段。** 未完成的图表通常无效,反复布局会与文本流式输出争用资源。消息定稿可提供完整源码。 + +**在 Chat 内部渲染,或添加通用预览注册表。** 使用普通属性的共享基础组件即可满足复用,无需另加注册表或功能插件依赖。这遵循[共享控件规则](../architecture/2026-09-05-shared-client-control-primitives.zh.md)。 + **在 iframe 内执行 HTML 脚本。** 静态聊天示例不需要脚本执行。空 sandbox、清理与 CSP 降低预览权限,也避免引入 iframe 消息协议。 -**把渲染标记插入 Chat。** HTML 样式和可执行 SVG 会与应用共享文档。iframe 隔离布局与来源,SVG 图片模式额外禁用 SVG 行为。 +**把渲染标记插入 Chat。** 所需预览只需显示图表和访问源码;HTML 样式和可执行 SVG 会与应用共享文档。iframe 隔离布局与来源,SVG 图片模式额外禁用 SVG 行为。 **将 Viz.js 视为仅使用 MIT,或使用远程 Graphviz 服务。** 本地编译的 Graphviz 仍受其许可证约束;远程渲染会把对话内容发送到设备之外。内置渲染器保留源码可获取性与法律声明,无需网络渲染。 ## Consequences -不改变 Session 事件、提示词或工具。HTML 内联样式可用,交互脚本、外部资源和链接不可用。Graphviz 增加按需加载的 WebAssembly 资源与浏览器线程同步布局;源码取消可以阻止结果发布,但无法抢占进行中的布局。Mermaid 既有布局限制仍记录在其所属决策中。 +该功能改变呈现,不改变持久化消息、provider 请求、工具或 Host API。HTML 内联样式可用,交互脚本、外部资源和链接不可用。Mermaid 和 Graphviz 增加按需加载的浏览器资源,并在浏览器线程上执行布局。取消无法抢占进行中的布局;即使结果无法发布,Mermaid 仍会完成并释放测量 DOM。预览不提供编辑、导出、缩放控件或交互式图表链接。 -组件测试覆盖源码复制、默认预览、回退、取消与恢复。无密钥[浏览器场景](../../../../apps/web/tests/markdown-mermaid.e2e.ts)验证不透明来源 iframe、真实图片解码、源码切换、脚本/导航/资源请求被阻止、本地化快照和已提供的许可证文本。声明检查固定已审查的包装层与原生构建来源,升级时须重新审查。 +组件测试覆盖源码复制、默认预览、延迟完成、过期成功与失败、卸载、回退、取消、运行时加载失败与恢复。无密钥[浏览器场景](../../../../apps/web/tests/markdown-mermaid.e2e.ts)验证中文 Mermaid 流程图、时序图、无效源码、配置覆盖、不透明来源 iframe、真实图片解码、源码切换、脚本/导航/资源请求被阻止、中英文 UI 快照和已提供的许可证文本。声明检查固定已审查的包装层与原生构建来源,升级时须重新审查。 diff --git a/packages/client/locale/src/locales/en.ts b/packages/client/locale/src/locales/en.ts index 1d97566db1..374de7c7b0 100644 --- a/packages/client/locale/src/locales/en.ts +++ b/packages/client/locale/src/locales/en.ts @@ -44,8 +44,8 @@ export const en = { 'markdown.preview.loading': 'Rendering preview…', 'markdown.preview.error': 'Unable to render this preview. The source is shown below.', 'markdown.mermaid.diagram': 'Mermaid diagram', - 'markdown.mermaid.preview': 'Preview', - 'markdown.mermaid.source': 'Source', + 'markdown.preview.preview': 'Preview', + 'markdown.preview.source': 'Source', 'markdown.mermaid.loading': 'Rendering diagram…', 'markdown.mermaid.error': 'Unable to render this diagram. The source is shown below.', 'markdown.truncatedCharacters': '… truncated at {total} characters', diff --git a/packages/client/locale/src/locales/zh.ts b/packages/client/locale/src/locales/zh.ts index a799548c28..df1aa1d19e 100644 --- a/packages/client/locale/src/locales/zh.ts +++ b/packages/client/locale/src/locales/zh.ts @@ -42,8 +42,8 @@ export const zh = { 'markdown.preview.loading': '正在渲染预览…', 'markdown.preview.error': '无法渲染此预览,源码如下。', 'markdown.mermaid.diagram': 'Mermaid 图表', - 'markdown.mermaid.preview': '预览', - 'markdown.mermaid.source': '源码', + 'markdown.preview.preview': '预览', + 'markdown.preview.source': '源码', 'markdown.mermaid.loading': '正在渲染图表…', 'markdown.mermaid.error': '无法渲染此图表,源码如下。', 'markdown.truncatedCharacters': '… 已截断,共 {total} 字符', diff --git a/packages/client/ui-chat/src/client/markdown-labels.ts b/packages/client/ui-chat/src/client/markdown-labels.ts index ea70175c57..c77cb8e9e4 100644 --- a/packages/client/ui-chat/src/client/markdown-labels.ts +++ b/packages/client/ui-chat/src/client/markdown-labels.ts @@ -9,24 +9,21 @@ import type { ChatViewSlotProps } from './contract/slots.ts' * @returns Labels for code fences and footnotes. */ export function markdownLabels(t: ChatViewSlotProps['t']): MarkdownLabels { + const status = { loading: t('markdown.preview.loading'), error: t('markdown.preview.error') } return { code: { copyLabel: t('copy'), copiedLabel: t('copied') }, footnotes: t('markdown.footnotes'), preview: { - graphviz: t('markdown.preview.graphviz'), - svg: t('markdown.preview.svg'), - html: t('markdown.preview.html'), - preview: t('markdown.mermaid.preview'), - source: t('markdown.mermaid.source'), - loading: t('markdown.preview.loading'), - error: t('markdown.preview.error'), - }, - mermaid: { - diagram: t('markdown.mermaid.diagram'), - preview: t('markdown.mermaid.preview'), - source: t('markdown.mermaid.source'), - loading: t('markdown.mermaid.loading'), - error: t('markdown.mermaid.error'), + preview: t('markdown.preview.preview'), + source: t('markdown.preview.source'), + mermaid: { + diagram: t('markdown.mermaid.diagram'), + loading: t('markdown.mermaid.loading'), + error: t('markdown.mermaid.error'), + }, + graphviz: { ...status, diagram: t('markdown.preview.graphviz') }, + svg: { ...status, diagram: t('markdown.preview.svg') }, + html: { ...status, diagram: t('markdown.preview.html') }, }, } } diff --git a/packages/client/ui-primitives/README.i18n.yaml b/packages/client/ui-primitives/README.i18n.yaml index bdd6417054..a3d90c6a34 100644 --- a/packages/client/ui-primitives/README.i18n.yaml +++ b/packages/client/ui-primitives/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-primitives/README.md -README.md: a1e877c5249817c7ab2e9fa1f6a8ad9582c44a3a -README.zh.md: 81923da65cc21f794d2e7f5d822feb69c06f7499 +README.md: a4fe046ba31212b2029e7689a542b55dd9eceb8b +README.zh.md: 59a3f0cae2b9253c303e0fb244e13c61e17d0417 diff --git a/packages/client/ui-primitives/README.md b/packages/client/ui-primitives/README.md index a1e877c524..a4fe046ba3 100644 --- a/packages/client/ui-primitives/README.md +++ b/packages/client/ui-primitives/README.md @@ -53,7 +53,6 @@ Check this table before writing a control in a feature package. A plugin cannot | `Toast` | Transient top-center banner held for the owner's `holdMs`. | | `JsonTree`, `JsonBlock` | Read-only JSON inspection. | | `MarkdownText`, `CodeBlock` | Untrusted GFM with TeX math, and highlighted code. `CodeBlock` accepts opt-in `lineNumbers`; copied source excludes the gutter. | -| `MermaidPreview` | Read-only Mermaid diagram image with loading and source fallback states. | | `TerminalBlock`, `ReadBlock`, `DiffBlock`, `SearchBlock`, `WebBlock` | The agent-output card matching each tool-result intent. | | `icons/*`, `FishLogo`, `BrandWordmark`, `ReferenceIcon`, `LinkIcon`, `DocumentFileIcon` | Glyphs and brand marks, all riding `currentColor`. | | `FileTypeIcon` | The coloured file-type sheet (code, html, image, markdown, pdf, sheet, slides, document, other); `classifyFileType` picks the kind from a path's extension. | @@ -74,11 +73,11 @@ The catalog above lists what each export is for; this section covers the behavio `MarkdownText` renders untrusted GFM and TeX math, blocks unsafe links and images, and can turn resolved file mentions into explicit controls. When the owner passes a `pathImages` vocabulary, image destinations that are local media paths rewrite to displayable URLs on settled renders only (the same streaming gate as file mentions); without a vocabulary, local destinations remain inert alt text. A load or decode failure replaces the image with its authored alt text, or the original destination when alt is empty. Changing the image source permits a fresh load. While a reply streams, it freezes completed blocks, advances a top-level open fence by completed lines, and highlights that fence from saved Shiki grammar state. Completed token lines enter fixed-size React groups, so later chunks reconcile only the growing group; an unchanged fence retains that DOM when the final full parse resolves cross-document syntax. `TerminalBlock`, `ReadBlock`, `DiffBlock`, `SearchBlock`, and `WebBlock` render the matching tool-result intent with copy controls, overflow handling, and ANSI processing where applicable. `JsonTree` and `JsonBlock` inspect JSON values read-only, while `projectUserText` projects sent user text into inline plain runs and reference chips for the message bubble and queue rows. -`MermaidPreview` renders complete Mermaid source on a light canvas. Rendering loads Mermaid on demand, uses strict security, and exposes the generated SVG as an image with no diagram link handlers. A render failure shows the original source with the supplied error label; replacing the source discards late results from the previous render. The diagram keeps its intrinsic size and shrinks to fit the available width. +Supply `MarkdownLabels.preview` to enable settled `mermaid`, `graphviz`/`dot`, `svg`, and `html` fences. Each language supplies localized preview states, and consumers without these labels retain code. Mermaid renders complete source on a light canvas. Rendering loads Mermaid on demand, uses strict security, and exposes the generated SVG as an image with no diagram link handlers. A render failure shows the original source with the supplied error label; replacing the source discards late results from the previous render. The diagram keeps its intrinsic size and shrinks to fit the available width. -Supply `MarkdownLabels.mermaid` to enable `mermaid` fence previews in a Markdown consumer; without it, fences remain code. Previews start after the message settles. `CodeBlock.preview` supplies an optional headerless body with icon actions for source switching and copying. Actions appear on hover or keyboard focus, and stay visible below the diagram whenever a touchscreen is available, including with a mouse attached. Switching to source keeps the preview mounted, so returning reuses its result; copying always retains the source. [The Mermaid decision](../../../.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.md) records the rendering and reuse choices. +`CodeBlock.preview` supplies an optional headerless body with icon actions for source switching and copying. Actions appear on hover or keyboard focus, and stay visible below the diagram whenever a touchscreen is available, including with a mouse attached. Switching to source keeps the preview mounted, so returning reuses its result; copying always retains the source. -Supply `MarkdownLabels.preview` to enable `graphviz`/`dot`, `svg`, and `html` fences. Each settled fence defaults to visualization and shares the source/copy controls. Graphviz loads `@viz-js/viz` on demand and renders DOT with the `dot` engine. SVG and Graphviz output are inert images inside opaque sandbox iframes; malformed SVG and DOT retain their source with an error. HTML accepts complete documents or fragments and preserves inline styles, but strips scripts and navigation. Frames have no sandbox permissions, and their Content Security Policy blocks external resources, embedded frames, forms, and scripts. Frames use a 400px scrollable viewport. Raw HTML outside a code fence remains ordinary Markdown input. +Each settled fence defaults to visualization and shares the source/copy controls. Graphviz loads `@viz-js/viz` on demand and renders DOT with the `dot` engine. SVG and Graphviz output are inert images inside opaque sandbox iframes; malformed SVG and DOT retain their source with an error. HTML accepts complete documents or fragments and preserves inline styles, but strips scripts and navigation. Frames have no sandbox permissions, and their Content Security Policy blocks external resources, embedded frames, forms, and scripts. Frames use a 400px scrollable viewport. Raw HTML outside a code fence remains ordinary Markdown input. [Preview license notices](THIRD_PARTY_PREVIEW_NOTICES.txt) ship with this package and the Web frontend. Mermaid is MIT; Viz.js is MIT but embeds Graphviz under EPL-2.0. The notices retain license texts and the exact Graphviz source download. Dependency upgrades require reviewing the embedded payload as well as npm metadata; the [preview decision](../../../.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.md) records the distribution and sandbox choices. diff --git a/packages/client/ui-primitives/README.zh.md b/packages/client/ui-primitives/README.zh.md index 81923da65c..59a3f0cae2 100644 --- a/packages/client/ui-primitives/README.zh.md +++ b/packages/client/ui-primitives/README.zh.md @@ -53,7 +53,6 @@ kind: "package-library" | `Toast` | 顶部居中的瞬时横幅,保持时长由所有者的 `holdMs` 决定。 | | `JsonTree`、`JsonBlock` | 只读 JSON 查看。 | | `MarkdownText`、`CodeBlock` | 不可信 GFM 与 TeX 数学,以及高亮代码。`CodeBlock` 可通过 `lineNumbers` 开启行号;复制的源码不含行号栏。 | -| `MermaidPreview` | 只读 Mermaid 图表图片,包含加载状态与源码回退状态。 | | `TerminalBlock`、`ReadBlock`、`DiffBlock`、`SearchBlock`、`WebBlock` | 与各类工具结果意图对应的 agent 输出卡片。 | | `icons/*`、`FishLogo`、`BrandWordmark`、`ReferenceIcon`、`LinkIcon`、`DocumentFileIcon` | 字形与品牌标识,全部随 `currentColor`。 | | `FileTypeIcon` | 彩色文件类型纸片(code、html、image、markdown、pdf、sheet、slides、document、other);`classifyFileType` 按路径扩展名选出类型。 | @@ -74,11 +73,11 @@ kind: "package-library" `MarkdownText` 渲染不可信的 GFM 与 TeX 公式、阻止不安全的链接与图片,并可把已解析的文件提及转换为显式控件。当 owner 传入 `pathImages` 词表时,本地媒体路径的图片目标只在落定渲染阶段重写为可展示 URL(与 file mentions 相同的流式门);不传词表时本地目标保持惰性 alt 文本。加载或解码失败后,图片替换为作者的 alt 文本;alt 为空时显示原始目标路径。图片源变化后可重新加载。回复流式输出时,它冻结已完成的块、按已完成行推进顶层未闭合 fence,并从保存的 Shiki grammar state 为该 fence 增量高亮。已完成的 token 行进入固定大小的 React 分组,后续分片只 reconcile 正在增长的分组;最终全量解析解决跨文档语法时,未变化的 fence 会保留该 DOM。`TerminalBlock`、`ReadBlock`、`DiffBlock`、`SearchBlock` 与 `WebBlock` 把对应的工具结果意图渲染为带复制控件、溢出处理及适用时 ANSI 处理的卡片。`JsonTree` 与 `JsonBlock` 以只读方式检查 JSON 值;`projectUserText` 把已发送的用户文本投影为行内普通文本段与引用 chip,供消息气泡和排队行使用。 -`MermaidPreview` 在浅色画布上渲染完整的 Mermaid 源码。渲染按需加载 Mermaid、使用严格安全模式,并把生成的 SVG 显示为图片,不绑定图内链接处理器。渲染失败时显示原始源码与传入的错误文案;替换源码后会丢弃前一次渲染的延迟结果。图表保留固有尺寸,并在可用宽度不足时缩小。 +提供 `MarkdownLabels.preview` 可启用定稿后的 `mermaid`、`graphviz`/`dot`、`svg` 和 `html` fence。每种语言提供本地化预览状态,未提供这些文案的调用方保留代码显示。Mermaid 在浅色画布上渲染完整源码。渲染按需加载 Mermaid、使用严格安全模式,并把生成的 SVG 显示为图片,不绑定图内链接处理器。渲染失败时显示原始源码与传入的错误文案;替换源码后会丢弃前一次渲染的延迟结果。图表保留固有尺寸,并在可用宽度不足时缩小。 -为 Markdown 调用方传入 `MarkdownLabels.mermaid` 即可启用 `mermaid` fence 预览;未传入时,fence 保持代码显示。预览在消息定稿后开始。`CodeBlock.preview` 提供无标题栏的替代正文,以及切换源码和复制的图标操作。操作在悬停或键盘聚焦时出现;只要设备具备触屏,就在图表下方保持可见,包括同时连接鼠标的情况。切换到源码时保留已挂载的预览,返回时复用其结果;复制始终保留源码。[Mermaid 决策](../../../.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.zh.md)记录了渲染与复用选择。 +`CodeBlock.preview` 提供无标题栏的替代正文,以及切换源码和复制的图标操作。操作在悬停或键盘聚焦时出现;只要设备具备触屏,就在图表下方保持可见,包括同时连接鼠标的情况。切换到源码时保留已挂载的预览,返回时复用其结果;复制始终保留源码。 -传入 `MarkdownLabels.preview` 可启用 `graphviz`/`dot`、`svg` 和 `html` fence。每个定稿 fence 默认显示可视化,并共享源码切换与复制操作。Graphviz 按需加载 `@viz-js/viz`,用 `dot` 引擎渲染 DOT。SVG 与 Graphviz 输出作为不可执行的图片放入不透明来源的 sandbox iframe;非法 SVG 与 DOT 显示错误并保留源码。HTML 接受完整文档或片段、保留内联样式,同时移除脚本与导航。iframe 不授予任何 sandbox 权限,Content Security Policy 阻止外部资源、嵌套 iframe、表单与脚本。iframe 使用可滚动的 400px 视口。代码 fence 外的原始 HTML 仍按普通 Markdown 输入处理。 +每个定稿 fence 默认显示可视化,并共享源码切换与复制操作。Graphviz 按需加载 `@viz-js/viz`,用 `dot` 引擎渲染 DOT。SVG 与 Graphviz 输出作为不可执行的图片放入不透明来源的 sandbox iframe;非法 SVG 与 DOT 显示错误并保留源码。HTML 接受完整文档或片段、保留内联样式,同时移除脚本与导航。iframe 不授予任何 sandbox 权限,Content Security Policy 阻止外部资源、嵌套 iframe、表单与脚本。iframe 使用可滚动的 400px 视口。代码 fence 外的原始 HTML 仍按普通 Markdown 输入处理。 [预览许可证声明](THIRD_PARTY_PREVIEW_NOTICES.txt)随本包与 Web 前端分发。Mermaid 使用 MIT;Viz.js 使用 MIT,但内嵌 Graphviz 使用 EPL-2.0。声明保留完整许可证文本及准确的 Graphviz 源码下载地址。升级依赖时必须同时检查内嵌产物和 npm 元数据;[预览决策](../../../.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.zh.md)记录分发与 sandbox 选择。 diff --git a/packages/client/ui-primitives/src/index.ts b/packages/client/ui-primitives/src/index.ts index 7600a0f9ca..d9b12eda0e 100644 --- a/packages/client/ui-primitives/src/index.ts +++ b/packages/client/ui-primitives/src/index.ts @@ -64,8 +64,6 @@ export type { } from './WebBlock.tsx' export { CodeBlock } from './markdown/CodeBlock.tsx' export type { CodeBlockProps } from './markdown/CodeBlock.tsx' -export { MermaidPreview } from './markdown/MermaidPreview.tsx' -export type { MermaidPreviewLabels } from './markdown/MermaidPreview.tsx' export { JsonBlock } from './markdown/JsonBlock.tsx' export { MarkdownText } from './markdown/MarkdownText.tsx' export type { MarkdownCodeLabels, MarkdownFileMentions, MarkdownLabels, MarkdownPathImages } from './markdown/MarkdownText.tsx' diff --git a/packages/client/ui-primitives/src/markdown/CodeBlock.module.css b/packages/client/ui-primitives/src/markdown/CodeBlock.module.css index 33a114d102..91c122c61c 100644 --- a/packages/client/ui-primitives/src/markdown/CodeBlock.module.css +++ b/packages/client/ui-primitives/src/markdown/CodeBlock.module.css @@ -98,31 +98,6 @@ color: var(--dsw-alias-label-primary); } -.numbered :where(pre) code { - display: block; - counter-reset: source-line; - white-space: normal; -} - -.numbered :where(pre) code > :global(.line) { - position: relative; - display: block; - min-height: 1lh; - padding-inline-start: calc(var(--dsl-code-block-line-number-width) + 12px); - counter-increment: source-line; - white-space: var(--dsl-code-block-line-white-space, pre-wrap); -} - -.numbered :where(pre) code > :global(.line)::before { - position: absolute; - inset-inline-start: 0; - width: var(--dsl-code-block-line-number-width); - color: var(--dsw-alias-label-tertiary); - text-align: end; - content: counter(source-line); - user-select: none; -} - /* Alternate content is a figure; its actions overlay instead of taking a title row. */ .preview.showingPreview { background: transparent; @@ -205,3 +180,28 @@ padding-top: 16px; } } + +.numbered :where(pre) code { + display: block; + counter-reset: source-line; + white-space: normal; +} + +.numbered :where(pre) code > :global(.line) { + position: relative; + display: block; + min-height: 1lh; + padding-inline-start: calc(var(--dsl-code-block-line-number-width) + 12px); + counter-increment: source-line; + white-space: var(--dsl-code-block-line-white-space, pre-wrap); +} + +.numbered :where(pre) code > :global(.line)::before { + position: absolute; + inset-inline-start: 0; + width: var(--dsl-code-block-line-number-width); + color: var(--dsw-alias-label-tertiary); + text-align: end; + content: counter(source-line); + user-select: none; +} diff --git a/packages/client/ui-primitives/src/markdown/MermaidPreview.tsx b/packages/client/ui-primitives/src/markdown/MermaidPreview.tsx deleted file mode 100644 index dc330e9da8..0000000000 --- a/packages/client/ui-primitives/src/markdown/MermaidPreview.tsx +++ /dev/null @@ -1,17 +0,0 @@ -/** Mermaid image preview using the shared asynchronous source renderer. */ - -import { renderMermaid } from './mermaid.ts' -import { SourcePreview } from './SourcePreview.tsx' -import type { PreviewLabels } from './SourcePreview.tsx' - -/** Localized Mermaid preview states; source remains verbatim. */ -export type MermaidPreviewLabels = PreviewLabels - -/** - * Display a complete Mermaid document as an inert SVG image. - * @param props - Source and complete localized labels. - * @returns A loading status, diagram image, or error with the original source. - */ -export function MermaidPreview({ code, labels }: { code: string; labels: MermaidPreviewLabels }) { - return -} diff --git a/packages/client/ui-primitives/src/markdown/MermaidPreview.module.css b/packages/client/ui-primitives/src/markdown/SourcePreview.module.css similarity index 100% rename from packages/client/ui-primitives/src/markdown/MermaidPreview.module.css rename to packages/client/ui-primitives/src/markdown/SourcePreview.module.css diff --git a/packages/client/ui-primitives/src/markdown/SourcePreview.tsx b/packages/client/ui-primitives/src/markdown/SourcePreview.tsx index 376b69710a..be0871673c 100644 --- a/packages/client/ui-primitives/src/markdown/SourcePreview.tsx +++ b/packages/client/ui-primitives/src/markdown/SourcePreview.tsx @@ -1,7 +1,7 @@ /** Read-only diagram preview with source fallback and per-source async ownership. */ import { useEffect, useState } from 'react' -import css from './MermaidPreview.module.css' +import css from './SourcePreview.module.css' /** Localized preview states; the diagram source remains verbatim. */ export interface PreviewLabels { diff --git a/packages/client/ui-primitives/src/markdown/mermaid.ts b/packages/client/ui-primitives/src/markdown/mermaid.ts index 4db2a64b6c..888a01cdca 100644 --- a/packages/client/ui-primitives/src/markdown/mermaid.ts +++ b/packages/client/ui-primitives/src/markdown/mermaid.ts @@ -2,7 +2,7 @@ import type { Mermaid } from 'mermaid' import clsx from 'clsx' -import css from './MermaidPreview.module.css' +import css from './SourcePreview.module.css' let runtime: Promise | undefined let nextDiagramId = 0 diff --git a/packages/client/ui-primitives/src/markdown/render.tsx b/packages/client/ui-primitives/src/markdown/render.tsx index cb1bd91eb1..f96884df64 100644 --- a/packages/client/ui-primitives/src/markdown/render.tsx +++ b/packages/client/ui-primitives/src/markdown/render.tsx @@ -26,8 +26,8 @@ import { CodeBlock } from './CodeBlock.tsx' import { SourcePreview } from './SourcePreview.tsx' import { renderGraphviz } from './graphviz.ts' import { renderHtml, renderSvg } from './preview-document.ts' -import { MermaidPreview } from './MermaidPreview.tsx' -import type { MermaidPreviewLabels } from './MermaidPreview.tsx' +import { renderMermaid } from './mermaid.ts' +import type { PreviewLabels } from './SourcePreview.tsx' import { renderTexToReact } from './katex.tsx' import { LinkIcon, classifyLinkPath } from '../LinkIcon.tsx' import type { PositionedBlock } from './incremental.ts' @@ -45,17 +45,14 @@ export interface MarkdownCodeLabels { export interface MarkdownLabels { code: MarkdownCodeLabels footnotes: string - /** Opt into settled Mermaid fence previews by supplying their complete localized labels. */ - mermaid?: MermaidPreviewLabels & { preview: string; source: string } - /** Opt into settled Graphviz, SVG, and static HTML previews with complete localized labels. */ + /** Opt into settled Mermaid, Graphviz, SVG, and static HTML previews with complete localized labels. */ preview?: { - graphviz: string - svg: string - html: string + mermaid: PreviewLabels + graphviz: PreviewLabels + svg: PreviewLabels + html: PreviewLabels preview: string source: string - loading: string - error: string } } @@ -418,20 +415,13 @@ function renderCode(node: Md.Code, key: Key, context: MarkdownRenderContext): Re /** Resolve the supported fence language without treating arbitrary HTML in Markdown as a preview. */ function fencePreview(lang: string | undefined, code: string, labels: MarkdownLabels) { - if (lang === 'mermaid' && labels.mermaid !== undefined) { - return { - content: , - previewLabel: labels.mermaid.preview, - sourceLabel: labels.mermaid.source, - } - } if (labels.preview === undefined) return undefined const kind = lang === 'dot' ? 'graphviz' : lang - if (kind !== 'graphviz' && kind !== 'svg' && kind !== 'html') return undefined - const render = { graphviz: renderGraphviz, svg: renderSvg, html: renderHtml }[kind] + if (kind !== 'mermaid' && kind !== 'graphviz' && kind !== 'svg' && kind !== 'html') return undefined + const render = { mermaid: renderMermaid, graphviz: renderGraphviz, svg: renderSvg, html: renderHtml }[kind] return { content: ( - + ), previewLabel: labels.preview.preview, sourceLabel: labels.preview.source, diff --git a/packages/client/ui-primitives/tests/fence-previews.client.spec.tsx b/packages/client/ui-primitives/tests/fence-previews.client.spec.tsx index 50e4b3d2d9..8e8b4abf58 100644 --- a/packages/client/ui-primitives/tests/fence-previews.client.spec.tsx +++ b/packages/client/ui-primitives/tests/fence-previews.client.spec.tsx @@ -7,16 +7,20 @@ import { renderGraphviz } from '../src/markdown/graphviz.ts' import { renderHtml, renderSvg } from '../src/markdown/preview-document.ts' import { markdownLabels } from './labels.client.ts' +const status = { loading: 'Loading', error: 'Cannot preview' } const preview = { - graphviz: 'Graphviz diagram', svg: 'SVG preview', html: 'HTML preview', - preview: 'Preview', source: 'Source', loading: 'Loading', error: 'Cannot preview', + mermaid: { ...status, diagram: 'Mermaid diagram' }, + graphviz: { ...status, diagram: 'Graphviz diagram' }, + svg: { ...status, diagram: 'SVG preview' }, + html: { ...status, diagram: 'HTML preview' }, + preview: 'Preview', source: 'Source', } const svg = '示例' const cases = [ - ['graphviz', 'digraph { Input -> Preview }', preview.graphviz], - ['dot', 'digraph { Input -> Preview }', preview.graphviz], - ['svg', svg, preview.svg], - ['html', '

Example

', preview.html], + ['graphviz', 'digraph { Input -> Preview }', preview.graphviz.diagram], + ['dot', 'digraph { Input -> Preview }', preview.graphviz.diagram], + ['svg', svg, preview.svg.diagram], + ['html', '

Example

', preview.html.diagram], ] as const afterEach(() => { cleanup(); vi.unstubAllGlobals() }) @@ -53,11 +57,11 @@ describe('Markdown fence previews', () => { it.each(['svg', 'graphviz'])('retains invalid %s source and recovers after replacement', async (lang) => { const view = render() - await screen.findByText(preview.error) + await screen.findByText(status.error) expect(view.container.querySelector('pre code')?.textContent).toBe('broken') view.rerender() - await screen.findByTitle(preview.svg) - expect(screen.queryByText(preview.error)).toBeNull() + await screen.findByTitle(preview.svg.diagram) + expect(screen.queryByText(status.error)).toBeNull() }) it('requires an opted-in code fence and leaves raw HTML and other languages unrendered', () => { diff --git a/packages/client/ui-primitives/tests/mermaid-preview.client.spec.tsx b/packages/client/ui-primitives/tests/source-preview.client.spec.tsx similarity index 73% rename from packages/client/ui-primitives/tests/mermaid-preview.client.spec.tsx rename to packages/client/ui-primitives/tests/source-preview.client.spec.tsx index c7cf95a9d4..fe356f64bb 100644 --- a/packages/client/ui-primitives/tests/mermaid-preview.client.spec.tsx +++ b/packages/client/ui-primitives/tests/source-preview.client.spec.tsx @@ -2,7 +2,8 @@ import { act, cleanup, fireEvent, render, screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { MermaidPreview } from '../src/markdown/MermaidPreview.tsx' +import { SourcePreview } from '../src/markdown/SourcePreview.tsx' +import { CodeBlock } from '../src/markdown/CodeBlock.tsx' import { renderMermaid } from '../src/markdown/mermaid.ts' import { MarkdownText } from '../src/markdown/MarkdownText.tsx' import { markdownLabels } from './labels.client.ts' @@ -13,17 +14,21 @@ const labels = { diagram: 'Mermaid diagram', loading: 'Rendering diagram…', error: 'Unable to render this diagram.', preview: 'Preview', source: 'Source', } +const preview = { + preview: labels.preview, source: labels.source, + mermaid: labels, graphviz: labels, svg: labels, html: labels, +} const source = 'flowchart LR\n A[Input] --> B[Preview]' const imageUrl = 'data:image/svg+xml;charset=utf-8,%3Csvg%2F%3E' afterEach(() => { cleanup(); vi.unstubAllGlobals() }) beforeEach(() => { vi.resetAllMocks() }) -describe('MermaidPreview', () => { +describe('SourcePreview', () => { it('shows loading until rendering completes, then displays an image without inserting SVG', async () => { const pending = Promise.withResolvers() vi.mocked(renderMermaid).mockReturnValue(pending.promise) - const view = render() + const view = render() expect(screen.getByRole('status').textContent).toBe(labels.loading) expect(screen.queryByRole('img')).toBeNull() await act(async () => { pending.resolve(imageUrl) }) @@ -34,12 +39,12 @@ describe('MermaidPreview', () => { it('retains invalid source and recovers when the source changes', async () => { vi.mocked(renderMermaid).mockRejectedValueOnce(new Error('Parse error')) - const view = render() + const view = render() expect((await screen.findByText(labels.error)).getAttribute('role')).toBe('status') expect(view.container.querySelector('pre code')?.textContent).toBe('invalid') const next = Promise.withResolvers() vi.mocked(renderMermaid).mockReturnValue(next.promise) - view.rerender() + view.rerender() expect(screen.getByRole('status').textContent).toBe(labels.loading) expect(view.container.querySelector('pre')).toBeNull() await act(async () => { next.resolve(imageUrl) }) @@ -50,9 +55,9 @@ describe('MermaidPreview', () => { const old = Promise.withResolvers() const next = Promise.withResolvers() vi.mocked(renderMermaid).mockReturnValueOnce(old.promise).mockReturnValueOnce(next.promise) - const view = render() + const view = render() const oldSignal = vi.mocked(renderMermaid).mock.calls[0]![1] - view.rerender() + view.rerender() expect(oldSignal.aborted).toBe(true) await act(async () => { next.resolve(imageUrl) }) await act(async () => { @@ -66,7 +71,7 @@ describe('MermaidPreview', () => { it('cancels the owner when unmounted while loading', async () => { const pending = Promise.withResolvers() vi.mocked(renderMermaid).mockReturnValue(pending.promise) - const view = render() + const view = render() const signal = vi.mocked(renderMermaid).mock.calls[0]![1] view.unmount() expect(signal.aborted).toBe(true) @@ -76,11 +81,29 @@ describe('MermaidPreview', () => { }) describe('Markdown Mermaid fences', () => { + it('retains a preview when switching to numbered source', async () => { + vi.mocked(renderMermaid).mockResolvedValue(imageUrl) + const view = render(, + previewLabel: labels.preview, sourceLabel: labels.source, + }} />) + const diagram = await screen.findByRole('img', { name: labels.diagram }) + expect(view.container.querySelector('[data-line-numbers]')).not.toBeNull() + fireEvent.click(screen.getByRole('button', { name: labels.source })) + expect([...view.container.querySelectorAll('code > .line')].map(line => line.textContent)).toEqual(source.split('\n')) + expect(diagram.isConnected).toBe(true) + expect(screen.queryByRole('img')).toBeNull() + fireEvent.click(screen.getByRole('button', { name: labels.preview })) + expect(screen.getByRole('img', { name: labels.diagram })).toBe(diagram) + expect(renderMermaid).toHaveBeenCalledOnce() + }) + it('keeps streaming source literal, then offers preview, source and source copying', async () => { const writeText = vi.fn().mockResolvedValue(undefined) vi.stubGlobal('navigator', Object.create(navigator, { clipboard: { value: { writeText } } })) vi.mocked(renderMermaid).mockResolvedValue(imageUrl) - const props = { text: `\`\`\`mermaid\n${source}\n\`\`\``, labels: { ...markdownLabels, mermaid: labels } } + const props = { text: `\`\`\`mermaid\n${source}\n\`\`\``, labels: { ...markdownLabels, preview } } const view = render() expect(view.container.querySelector('pre code')?.textContent).toBe(source) expect(renderMermaid).not.toHaveBeenCalled() @@ -107,7 +130,7 @@ describe('Markdown Mermaid fences', () => { it('leaves other languages and consumers without preview labels as code', () => { const view = render() expect(view.container.querySelector('pre code')?.textContent).toBe(source) - view.rerender() + view.rerender() expect(view.container.querySelector('pre code')?.textContent).toBe('flowchart LR') expect(renderMermaid).not.toHaveBeenCalled() }) From 9e050260e4559707a65c904bf0a9427c98096d05 Mon Sep 17 00:00:00 2001 From: "HYD@OVERTON" Date: Wed, 9 Sep 2026 17:04:20 +0800 Subject: [PATCH 08/27] feat(web): adapt diagram previews to application theme --- ...6-09-09-markdown-static-previews.i18n.yaml | 4 +- .../2026-09-09-markdown-static-previews.md | 2 +- .../2026-09-09-markdown-static-previews.zh.md | 2 +- apps/web/tests/markdown-mermaid.e2e.ts | 28 +++++++ .../client/ui-primitives/README.i18n.yaml | 4 +- packages/client/ui-primitives/README.md | 4 +- packages/client/ui-primitives/README.zh.md | 4 +- .../src/markdown/SourcePreview.module.css | 4 +- .../src/markdown/SourcePreview.tsx | 29 ++++++-- .../ui-primitives/src/markdown/graphviz.ts | 23 +++++- .../ui-primitives/src/markdown/mermaid.ts | 74 ++++++++++++++----- .../src/markdown/preview-document.ts | 4 +- .../src/markdown/preview-theme.ts | 27 +++++++ .../tests/graphviz-runtime.client.spec.ts | 32 ++++++++ .../tests/mermaid-runtime.client.spec.ts | 43 ++++++++++- .../tests/source-preview.client.spec.tsx | 25 +++++++ 16 files changed, 267 insertions(+), 42 deletions(-) create mode 100644 packages/client/ui-primitives/src/markdown/preview-theme.ts diff --git a/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.i18n.yaml b/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.i18n.yaml index c70073de17..8712ee2070 100644 --- a/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-09-markdown-static-previews.md -2026-09-09-markdown-static-previews.md: a6d1802546e9e976da7ae4d1ecc024924414e9a4 -2026-09-09-markdown-static-previews.zh.md: 42488547efafdbf5b1944b1300af13f359365d74 +2026-09-09-markdown-static-previews.md: a5fa79fcc8c192dbcd3949debbabbccf85cb3b62 +2026-09-09-markdown-static-previews.zh.md: 30efe897702377c180c9e1650fda93ae42f6e580 diff --git a/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.md b/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.md index a6d1802546..a5fa79fcc8 100644 --- a/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.md +++ b/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.md @@ -16,7 +16,7 @@ The shared Markdown renderer enables settled `mermaid`, `graphviz`/`dot`, `svg`, `SourcePreview` owns loading, failure, and cancellation of stale result publication. Source replacement and unmounting cancel publication; cancellation before runtime loading completes skips layout. Failures show a localized error and the original source, and replacing invalid source with valid input recovers the preview. -Mermaid loads on demand. Its public render API serializes diagram work, and each call removes its temporary measurement DOM in `finally`. Strict security, disabled HTML labels, the neutral theme, and error-rendering policy cannot be overridden by diagram configuration. Generated SVG is displayed as an image without installing links or scripts. Intrinsic dimensions come from the SVG viewBox; large diagrams shrink to fit, and the canvas stays light in both application themes. +Mermaid loads on demand. A shared queue serializes theme initialization with diagram work, and each call removes its temporary measurement DOM in `finally`. Strict security, disabled HTML labels, the application palette, and error-rendering policy cannot be overridden by diagram configuration. Generated SVG is displayed as an image without installing links or scripts. Intrinsic dimensions come from the SVG viewBox; large diagrams shrink to fit, and the canvas follows the code-block background. Mounted previews observe document theme attributes and regenerate only when resolved colors change; obsolete renders cannot publish. Graphviz default colors follow the same palette, while authored DOT, SVG, and HTML colors remain intact. HTML uses DOMPurify with navigation attributes and document-loading elements forbidden, followed by an opaque `sandbox=""` iframe. A trusted CSP precedes source markup and permits only inline CSS, data images, and data fonts. Scripts, external resources, child frames, form submission, and same-origin access are unavailable. SVG is parsed as XML and placed in an image inside the frame, so SVG scripts and links never become active. Frames have a fixed scrollable viewport: measuring their content would require additional origin access or trusted frame scripts. diff --git a/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.zh.md b/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.zh.md index 42488547ef..30efe89770 100644 --- a/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.zh.md +++ b/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.zh.md @@ -16,7 +16,7 @@ Status: implemented `SourcePreview` 负责加载、失败和取消过期结果发布。替换源码和卸载组件会取消结果发布;在运行时加载完成前取消会跳过布局。失败时显示本地化错误与原始源码,用有效输入替换无效源码后可以恢复预览。 -Mermaid 按需加载。其公开渲染 API 串行执行图表工作,每次调用都在 `finally` 中删除临时测量 DOM。图表配置无法覆盖严格安全模式、禁用 HTML 标签、中性色主题和错误渲染策略。生成的 SVG 作为图片显示,不安装链接或脚本。固有尺寸来自 SVG viewBox;大图缩小以适应宽度,两种应用主题下的画布均保持浅色。 +Mermaid 按需加载。共享队列将主题初始化与图表渲染一起串行执行,每次调用都在 `finally` 中删除临时测量 DOM。图表配置无法覆盖严格安全模式、禁用 HTML 标签、应用配色和错误渲染策略。生成的 SVG 作为图片显示,不安装链接或脚本。固有尺寸来自 SVG viewBox;大图缩小以适应宽度,画布使用代码块背景。已挂载的预览观察文档主题属性,仅在解析后的配色变化时重新生成;过期渲染不能发布结果。Graphviz 的默认颜色采用同一配色,DOT、SVG 和 HTML 中明确指定的颜色保持原样。 HTML 先由 DOMPurify 清理,禁止导航属性与文档加载元素,再进入不透明来源的 `sandbox=""` iframe。可信 CSP 位于源码标记之前,仅允许内联 CSS、data 图片与 data 字体。脚本、外部资源、子 iframe、表单提交与同源访问均不可用。SVG 按 XML 解析后作为图片放进 iframe,因此 SVG 脚本与链接不会激活。iframe 使用固定的可滚动视口:测量内容需要额外的来源访问权限或可信 iframe 脚本。 diff --git a/apps/web/tests/markdown-mermaid.e2e.ts b/apps/web/tests/markdown-mermaid.e2e.ts index c009e1d836..d970cc9e11 100644 --- a/apps/web/tests/markdown-mermaid.e2e.ts +++ b/apps/web/tests/markdown-mermaid.e2e.ts @@ -88,6 +88,34 @@ describe('web e2e: Mermaid chat previews', () => { await scaffold?.close() }) + it.skipIf(MODE === 'record')('updates existing diagrams when the document switches between light and dark', async () => { + const page = await newEnglishPage(browser) + try { + await openConversation(page, scaffold) + const image = page.getByRole('img', { name: 'Mermaid diagram' }).first() + await image.waitFor() + let previous = await image.getAttribute('src') + for (const scheme of ['dark', 'light'] as const) { + await page.evaluate((value) => { + document.documentElement.style.colorScheme = value + document.body.toggleAttribute('data-ds-dark-theme', value === 'dark') + }, scheme) + await expect.poll(() => image.getAttribute('src')).not.toBe(previous) + await expect.poll(() => image.evaluate(node => (node as HTMLImageElement).naturalWidth)).toBeGreaterThan(0) + const colors = await image.evaluate(node => ({ + canvas: getComputedStyle(node.parentElement!).backgroundColor, + expected: getComputedStyle(document.body).getPropertyValue('--dsw-alias-markdown-code-block').trim(), + })) + expect(colors.canvas).toBe(colors.expected) + await expect.poll(() => page.getByTitle('HTML preview', { exact: true }).getAttribute('srcdoc')) + .toContain(`color-scheme:${scheme}`) + previous = await image.getAttribute('src') + } + } finally { + await page.close() + } + }) + it.skipIf(MODE === 'record')('renders diagrams, switches to source, copies source, and contains malformed content', async () => { const page = await newEnglishPage(browser) onTestFailed(() => saveFailureShot(page, 'web-e2e-markdown-mermaid')) diff --git a/packages/client/ui-primitives/README.i18n.yaml b/packages/client/ui-primitives/README.i18n.yaml index a3d90c6a34..b3a61421d4 100644 --- a/packages/client/ui-primitives/README.i18n.yaml +++ b/packages/client/ui-primitives/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-primitives/README.md -README.md: a4fe046ba31212b2029e7689a542b55dd9eceb8b -README.zh.md: 59a3f0cae2b9253c303e0fb244e13c61e17d0417 +README.md: 8bdd2b7f90ebe4a998be8158351e49f815343c78 +README.zh.md: b861f9e17ff73f20717aed63b5b087358daeae10 diff --git a/packages/client/ui-primitives/README.md b/packages/client/ui-primitives/README.md index a4fe046ba3..8bdd2b7f90 100644 --- a/packages/client/ui-primitives/README.md +++ b/packages/client/ui-primitives/README.md @@ -73,12 +73,14 @@ The catalog above lists what each export is for; this section covers the behavio `MarkdownText` renders untrusted GFM and TeX math, blocks unsafe links and images, and can turn resolved file mentions into explicit controls. When the owner passes a `pathImages` vocabulary, image destinations that are local media paths rewrite to displayable URLs on settled renders only (the same streaming gate as file mentions); without a vocabulary, local destinations remain inert alt text. A load or decode failure replaces the image with its authored alt text, or the original destination when alt is empty. Changing the image source permits a fresh load. While a reply streams, it freezes completed blocks, advances a top-level open fence by completed lines, and highlights that fence from saved Shiki grammar state. Completed token lines enter fixed-size React groups, so later chunks reconcile only the growing group; an unchanged fence retains that DOM when the final full parse resolves cross-document syntax. `TerminalBlock`, `ReadBlock`, `DiffBlock`, `SearchBlock`, and `WebBlock` render the matching tool-result intent with copy controls, overflow handling, and ANSI processing where applicable. `JsonTree` and `JsonBlock` inspect JSON values read-only, while `projectUserText` projects sent user text into inline plain runs and reference chips for the message bubble and queue rows. -Supply `MarkdownLabels.preview` to enable settled `mermaid`, `graphviz`/`dot`, `svg`, and `html` fences. Each language supplies localized preview states, and consumers without these labels retain code. Mermaid renders complete source on a light canvas. Rendering loads Mermaid on demand, uses strict security, and exposes the generated SVG as an image with no diagram link handlers. A render failure shows the original source with the supplied error label; replacing the source discards late results from the previous render. The diagram keeps its intrinsic size and shrinks to fit the available width. +Supply `MarkdownLabels.preview` to enable settled `mermaid`, `graphviz`/`dot`, `svg`, and `html` fences. Each language supplies localized preview states, and consumers without these labels retain code. Mermaid and Graphviz use the active document palette on the code-block background and update mounted previews when the theme changes. Explicit colors in DOT, SVG, and HTML remain authored content. Rendering loads Mermaid on demand, uses strict security, and exposes the generated SVG as an image with no diagram link handlers. A render failure shows the original source with the supplied error label; replacing the source discards late results from the previous render. The diagram keeps its intrinsic size and shrinks to fit the available width. `CodeBlock.preview` supplies an optional headerless body with icon actions for source switching and copying. Actions appear on hover or keyboard focus, and stay visible below the diagram whenever a touchscreen is available, including with a mouse attached. Switching to source keeps the preview mounted, so returning reuses its result; copying always retains the source. Each settled fence defaults to visualization and shares the source/copy controls. Graphviz loads `@viz-js/viz` on demand and renders DOT with the `dot` engine. SVG and Graphviz output are inert images inside opaque sandbox iframes; malformed SVG and DOT retain their source with an error. HTML accepts complete documents or fragments and preserves inline styles, but strips scripts and navigation. Frames have no sandbox permissions, and their Content Security Policy blocks external resources, embedded frames, forms, and scripts. Frames use a 400px scrollable viewport. Raw HTML outside a code fence remains ordinary Markdown input. +Internal render functions accept optional native `MermaidConfig` and Viz.js `RenderOptions` for developer customization. Attribute and theme-variable overrides merge with document defaults; Mermaid preview restrictions and Graphviz SVG output remain fixed. These parameters have no settings UI. + [Preview license notices](THIRD_PARTY_PREVIEW_NOTICES.txt) ship with this package and the Web frontend. Mermaid is MIT; Viz.js is MIT but embeds Graphviz under EPL-2.0. The notices retain license texts and the exact Graphviz source download. Dependency upgrades require reviewing the embedded payload as well as npm metadata; the [preview decision](../../../.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.md) records the distribution and sandbox choices. ### Localizing copy diff --git a/packages/client/ui-primitives/README.zh.md b/packages/client/ui-primitives/README.zh.md index 59a3f0cae2..b861f9e17f 100644 --- a/packages/client/ui-primitives/README.zh.md +++ b/packages/client/ui-primitives/README.zh.md @@ -73,12 +73,14 @@ kind: "package-library" `MarkdownText` 渲染不可信的 GFM 与 TeX 公式、阻止不安全的链接与图片,并可把已解析的文件提及转换为显式控件。当 owner 传入 `pathImages` 词表时,本地媒体路径的图片目标只在落定渲染阶段重写为可展示 URL(与 file mentions 相同的流式门);不传词表时本地目标保持惰性 alt 文本。加载或解码失败后,图片替换为作者的 alt 文本;alt 为空时显示原始目标路径。图片源变化后可重新加载。回复流式输出时,它冻结已完成的块、按已完成行推进顶层未闭合 fence,并从保存的 Shiki grammar state 为该 fence 增量高亮。已完成的 token 行进入固定大小的 React 分组,后续分片只 reconcile 正在增长的分组;最终全量解析解决跨文档语法时,未变化的 fence 会保留该 DOM。`TerminalBlock`、`ReadBlock`、`DiffBlock`、`SearchBlock` 与 `WebBlock` 把对应的工具结果意图渲染为带复制控件、溢出处理及适用时 ANSI 处理的卡片。`JsonTree` 与 `JsonBlock` 以只读方式检查 JSON 值;`projectUserText` 把已发送的用户文本投影为行内普通文本段与引用 chip,供消息气泡和排队行使用。 -提供 `MarkdownLabels.preview` 可启用定稿后的 `mermaid`、`graphviz`/`dot`、`svg` 和 `html` fence。每种语言提供本地化预览状态,未提供这些文案的调用方保留代码显示。Mermaid 在浅色画布上渲染完整源码。渲染按需加载 Mermaid、使用严格安全模式,并把生成的 SVG 显示为图片,不绑定图内链接处理器。渲染失败时显示原始源码与传入的错误文案;替换源码后会丢弃前一次渲染的延迟结果。图表保留固有尺寸,并在可用宽度不足时缩小。 +提供 `MarkdownLabels.preview` 可启用定稿后的 `mermaid`、`graphviz`/`dot`、`svg` 和 `html` fence。每种语言提供本地化预览状态,未提供这些文案的调用方保留代码显示。Mermaid 与 Graphviz 在代码块背景上使用当前文档配色,主题切换时同步更新已挂载的预览。DOT、SVG 与 HTML 中明确指定的颜色仍保留原样。渲染按需加载 Mermaid、使用严格安全模式,并把生成的 SVG 显示为图片,不绑定图内链接处理器。渲染失败时显示原始源码与传入的错误文案;替换源码后会丢弃前一次渲染的延迟结果。图表保留固有尺寸,并在可用宽度不足时缩小。 `CodeBlock.preview` 提供无标题栏的替代正文,以及切换源码和复制的图标操作。操作在悬停或键盘聚焦时出现;只要设备具备触屏,就在图表下方保持可见,包括同时连接鼠标的情况。切换到源码时保留已挂载的预览,返回时复用其结果;复制始终保留源码。 每个定稿 fence 默认显示可视化,并共享源码切换与复制操作。Graphviz 按需加载 `@viz-js/viz`,用 `dot` 引擎渲染 DOT。SVG 与 Graphviz 输出作为不可执行的图片放入不透明来源的 sandbox iframe;非法 SVG 与 DOT 显示错误并保留源码。HTML 接受完整文档或片段、保留内联样式,同时移除脚本与导航。iframe 不授予任何 sandbox 权限,Content Security Policy 阻止外部资源、嵌套 iframe、表单与脚本。iframe 使用可滚动的 400px 视口。代码 fence 外的原始 HTML 仍按普通 Markdown 输入处理。 +内部渲染函数接受可选的原生 `MermaidConfig` 与 Viz.js `RenderOptions`,供开发者定制。属性与主题变量覆盖项和文档默认值合并;Mermaid 预览限制与 Graphviz SVG 输出保持固定。这些参数没有设置界面。 + [预览许可证声明](THIRD_PARTY_PREVIEW_NOTICES.txt)随本包与 Web 前端分发。Mermaid 使用 MIT;Viz.js 使用 MIT,但内嵌 Graphviz 使用 EPL-2.0。声明保留完整许可证文本及准确的 Graphviz 源码下载地址。升级依赖时必须同时检查内嵌产物和 npm 元数据;[预览决策](../../../.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.zh.md)记录分发与 sandbox 选择。 ### 本地化文案 diff --git a/packages/client/ui-primitives/src/markdown/SourcePreview.module.css b/packages/client/ui-primitives/src/markdown/SourcePreview.module.css index e21d84e284..46a674ed0a 100644 --- a/packages/client/ui-primitives/src/markdown/SourcePreview.module.css +++ b/packages/client/ui-primitives/src/markdown/SourcePreview.module.css @@ -1,7 +1,7 @@ .canvas { overflow: auto; padding: 16px; - background: var(--dsw-static-neutral-00); + background: var(--dsw-alias-markdown-code-block); border-radius: 12px; } @@ -31,5 +31,5 @@ height: 400px; border: 0; border-radius: 12px; - background: var(--dsw-static-neutral-00); + background: var(--dsw-alias-markdown-code-block); } diff --git a/packages/client/ui-primitives/src/markdown/SourcePreview.tsx b/packages/client/ui-primitives/src/markdown/SourcePreview.tsx index be0871673c..3ce07fb34d 100644 --- a/packages/client/ui-primitives/src/markdown/SourcePreview.tsx +++ b/packages/client/ui-primitives/src/markdown/SourcePreview.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react' import css from './SourcePreview.module.css' +import { readPreviewTheme } from './preview-theme.ts' /** Localized preview states; the diagram source remains verbatim. */ export interface PreviewLabels { @@ -19,7 +20,7 @@ function assertNever(value: never): never { /** * Display a complete document or diagram, retaining the source when rendering fails. - * @param props - Source and complete localized labels. Changing source discards the previous result. + * @param props - Source and complete localized labels. Source and document theme changes cancel obsolete renders. * @returns A loading status, an inert image or sandboxed document, or an error with the original source. */ export function SourcePreview({ code, labels, render, document: isDocument = false }: { @@ -30,12 +31,26 @@ export function SourcePreview({ code, labels, render, document: isDocument = fal }) { const [result, setResult] = useState(null) useEffect(() => { - const controller = new AbortController() - void (async () => await render(code, controller.signal))().then( - (src) => { if (!controller.signal.aborted) setResult({ kind: 'ok', code, src }) }, - () => { if (!controller.signal.aborted) setResult({ kind: 'error', code }) }, - ) - return () => { controller.abort() } + let controller: AbortController | undefined + let palette: string | undefined + const update = () => { + const next = JSON.stringify(readPreviewTheme()) + if (next === palette) return + palette = next + controller?.abort() + const current = new AbortController() + controller = current + void (async () => await render(code, current.signal))().then( + (src) => { if (!current.signal.aborted) setResult({ kind: 'ok', code, src }) }, + () => { if (!current.signal.aborted) setResult({ kind: 'error', code }) }, + ) + } + // Isolated image/frame documents cannot inherit the host's CSS variables. + const observer = new MutationObserver(update) + observer.observe(document.documentElement, { attributes: true, attributeFilter: ['style'] }) + observer.observe(document.body, { attributes: true, attributeFilter: ['style', 'data-ds-dark-theme'] }) + update() + return () => { observer.disconnect(); controller?.abort() } }, [code, render]) if (result?.code !== code) return
{labels.loading}
diff --git a/packages/client/ui-primitives/src/markdown/graphviz.ts b/packages/client/ui-primitives/src/markdown/graphviz.ts index 8c00f84766..fc8f90315b 100644 --- a/packages/client/ui-primitives/src/markdown/graphviz.ts +++ b/packages/client/ui-primitives/src/markdown/graphviz.ts @@ -1,22 +1,37 @@ /** Lazy Graphviz WebAssembly layout; generated SVG is displayed only as an inert image. */ -import type { instance } from '@viz-js/viz' +import type { instance, RenderOptions } from '@viz-js/viz' import { renderSvg } from './preview-document.ts' +import { readPreviewTheme } from './preview-theme.ts' let runtime: ReturnType | undefined /** - * Render Graphviz DOT as a sandboxed preview document. + * Render Graphviz DOT with document-theme defaults; authored attributes override those defaults. * @param code - Complete Graphviz DOT source. * @param signal - Cancels work waiting for the runtime; synchronous layout cannot be interrupted. + * @param options - Native overrides; attribute maps merge over theme defaults and output remains SVG regardless of format. * @returns A srcdoc document. Import, layout, invalid source, and cancellation failures reject. */ -export async function renderGraphviz(code: string, signal: AbortSignal): Promise { +export async function renderGraphviz(code: string, signal: AbortSignal, options?: RenderOptions): Promise { runtime ??= import('@viz-js/viz').then(module => module.instance()).catch((error: unknown) => { runtime = undefined throw error }) const viz = await runtime signal.throwIfAborted() - return renderSvg(viz.renderString(code, { format: 'svg', engine: 'dot' }), signal) + const palette = readPreviewTheme() + const color = (value: string): string => value.replace( + /^rgb\(\s*(\d+)[, ]+\s*(\d+)[, ]+\s*(\d+)\s*\)$/, + (_match, r: string, g: string, b: string) => `#${[r, g, b].map(channel => Number(channel).toString(16).padStart(2, '0')).join('')}`, + ) + return renderSvg(viz.renderString(code, { + engine: 'dot', ...options, format: 'svg', + graphAttributes: { bgcolor: 'transparent', fontcolor: color(palette.text), ...options?.graphAttributes }, + nodeAttributes: { + color: color(palette.border), fontcolor: color(palette.text), fillcolor: color(palette.surface), style: 'filled', + ...options?.nodeAttributes, + }, + edgeAttributes: { color: color(palette.line), fontcolor: color(palette.text), ...options?.edgeAttributes }, + }), signal) } diff --git a/packages/client/ui-primitives/src/markdown/mermaid.ts b/packages/client/ui-primitives/src/markdown/mermaid.ts index 888a01cdca..9d7b8c721e 100644 --- a/packages/client/ui-primitives/src/markdown/mermaid.ts +++ b/packages/client/ui-primitives/src/markdown/mermaid.ts @@ -1,48 +1,86 @@ /** Lazy Mermaid runtime; each render owns and removes its temporary measurement DOM. */ -import type { Mermaid } from 'mermaid' +import type { Mermaid, MermaidConfig } from 'mermaid' import clsx from 'clsx' import css from './SourcePreview.module.css' +import { readPreviewTheme } from './preview-theme.ts' let runtime: Promise | undefined let nextDiagramId = 0 +let pending: Promise = Promise.resolve() function loadMermaid(): Promise { - runtime ??= import('mermaid').then(({ default: mermaid }) => { - mermaid.initialize({ - startOnLoad: false, - securityLevel: 'strict', - suppressErrorRendering: true, - theme: 'neutral', - htmlLabels: false, - secure: [ - 'secure', 'securityLevel', 'startOnLoad', 'maxTextSize', 'maxEdges', - 'suppressErrorRendering', 'theme', 'themeVariables', 'themeCSS', 'htmlLabels', 'flowchart', - ], - }) - return mermaid - }).catch((error: unknown) => { + runtime ??= import('mermaid').then(({ default: mermaid }) => mermaid).catch((error: unknown) => { runtime = undefined throw error }) return runtime } +function initialize(mermaid: Mermaid, config: MermaidConfig | undefined): void { + const palette = readPreviewTheme() + mermaid.initialize({ + theme: 'base', + ...config, + startOnLoad: false, + securityLevel: 'strict', + suppressErrorRendering: true, + themeVariables: { + darkMode: palette.dark, + background: palette.background, + primaryColor: palette.surface, + primaryTextColor: palette.text, + primaryBorderColor: palette.border, + secondaryColor: palette.surface, + tertiaryColor: palette.surface, + lineColor: palette.line, + textColor: palette.text, + actorBkg: palette.surface, + actorBorder: palette.border, + actorTextColor: palette.text, + actorLineColor: palette.border, + signalColor: palette.line, + signalTextColor: palette.text, + labelBoxBkgColor: palette.surface, + labelTextColor: palette.text, + edgeLabelBackground: palette.background, + ...config?.themeVariables as Record | undefined, + }, + htmlLabels: false, + // Mermaid still accepts this legacy override alongside the global HTML-label setting. + // oxlint-disable-next-line typescript/no-deprecated + flowchart: { ...config?.flowchart, htmlLabels: false }, + secure: [ + 'secure', 'securityLevel', 'startOnLoad', 'maxTextSize', 'maxEdges', + 'suppressErrorRendering', 'theme', 'themeVariables', 'themeCSS', 'htmlLabels', 'flowchart', + ...(config?.secure ?? []), + ], + }) +} + /** - * Render untrusted diagram source as an SVG image URL, without installing SVG or link handlers in the UI. + * Render untrusted diagram source using the document palette, without installing SVG or link handlers in the UI. * @param code - Complete Mermaid source. * @param signal - Cancels work waiting for the runtime; an active Mermaid render finishes before cleanup. + * @param config - Native overrides of theme defaults; strict security, manual rendering, and disabled HTML labels remain fixed. * @returns An SVG data URL. Import, parse, rendering, and cancellation failures reject. */ -export async function renderMermaid(code: string, signal: AbortSignal): Promise { +export async function renderMermaid(code: string, signal: AbortSignal, config?: MermaidConfig): Promise { + const task = pending.then(() => renderOne(code, signal, config)) + pending = task.catch(() => { /* The caller owns rejection; subsequent diagrams still render. */ }) + return task +} + +async function renderOne(code: string, signal: AbortSignal, config: MermaidConfig | undefined): Promise { const mermaid = await loadMermaid() signal.throwIfAborted() + initialize(mermaid, config) const stage = document.createElement('div') stage.className = clsx(css.staging) stage.setAttribute('aria-hidden', 'true') document.body.append(stage) try { - // Mermaid's public render API serializes diagram work; the lower-level mermaidAPI does not. + // Initialization and rendering share the queue because Mermaid owns global configuration. const { svg } = await mermaid.render(`dsh-mermaid-${nextDiagramId++}`, code, stage) const root = new DOMParser().parseFromString(svg, 'image/svg+xml').documentElement const viewBox = root.getAttribute('viewBox') diff --git a/packages/client/ui-primitives/src/markdown/preview-document.ts b/packages/client/ui-primitives/src/markdown/preview-document.ts index c0de8b4497..15aa3bf2f3 100644 --- a/packages/client/ui-primitives/src/markdown/preview-document.ts +++ b/packages/client/ui-primitives/src/markdown/preview-document.ts @@ -1,12 +1,14 @@ /** Static preview documents: opaque sandbox frames with no scripts, navigation, or remote resources. */ import DOMPurify from 'dompurify' +import { readPreviewTheme } from './preview-theme.ts' // This policy belongs to the preview document, before any source-controlled markup. const CSP = "default-src 'none'; script-src 'none'; style-src 'unsafe-inline'; img-src data:; font-src data:; base-uri 'none'; form-action 'none'" function documentOf(body: string): string { - return `${body}` + const scheme = readPreviewTheme().dark ? 'dark' : 'light' + return `${body}` } /** diff --git a/packages/client/ui-primitives/src/markdown/preview-theme.ts b/packages/client/ui-primitives/src/markdown/preview-theme.ts new file mode 100644 index 0000000000..e497e10a80 --- /dev/null +++ b/packages/client/ui-primitives/src/markdown/preview-theme.ts @@ -0,0 +1,27 @@ +/** Resolved document colors used by isolated diagram renderers. */ + +interface PreviewTheme { + dark: boolean + background: string + surface: string + text: string + line: string + border: string +} + +/** + * Read the palette after the application's theme presenter updates the DOM. + * @returns Semantic preview colors and the resolved browser color scheme. + */ +export function readPreviewTheme(): PreviewTheme { + const style = getComputedStyle(document.body) + const color = (name: string): string => style.getPropertyValue(`--dsw-alias-${name}`).trim() + return { + dark: getComputedStyle(document.documentElement).colorScheme === 'dark', + background: color('markdown-code-block'), + surface: color('bg-layer-2'), + text: color('label-primary'), + line: color('label-secondary'), + border: color('label-tertiary'), + } +} diff --git a/packages/client/ui-primitives/tests/graphviz-runtime.client.spec.ts b/packages/client/ui-primitives/tests/graphviz-runtime.client.spec.ts index 354c0f9eaa..1a7b170516 100644 --- a/packages/client/ui-primitives/tests/graphviz-runtime.client.spec.ts +++ b/packages/client/ui-primitives/tests/graphviz-runtime.client.spec.ts @@ -33,3 +33,35 @@ it('skips layout after cancellation during runtime loading', async () => { await rejection expect(renderString).not.toHaveBeenCalled() }) + +it('uses theme defaults without rewriting authored DOT attributes', async () => { + const previous = document.body.getAttribute('style') + try { + document.body.style.setProperty('--dsw-alias-label-primary', 'rgb(230, 231, 232)') + document.body.style.setProperty('--dsw-alias-label-secondary', '#aabbcc') + document.body.style.setProperty('--dsw-alias-label-tertiary', '#8899aa') + document.body.style.setProperty('--dsw-alias-bg-layer-2', '#222222') + const renderString = vi.fn().mockReturnValue('') + instance.mockResolvedValue({ renderString }) + const { renderGraphviz } = await import('../src/markdown/graphviz.ts') + const code = 'digraph { a [color="red"]; a -> b }' + await renderGraphviz(code, new AbortController().signal) + expect(renderString).toHaveBeenCalledWith(code, expect.objectContaining({ + nodeAttributes: { color: '#8899aa', fontcolor: '#e6e7e8', fillcolor: '#222222', style: 'filled' }, + edgeAttributes: { color: '#aabbcc', fontcolor: '#e6e7e8' }, + })) + await renderGraphviz(code, new AbortController().signal, { + engine: 'neato', format: 'plain', yInvert: true, + graphAttributes: { ranksep: 1 }, nodeAttributes: { shape: 'box' }, edgeAttributes: { color: 'red' }, + }) + expect(renderString).toHaveBeenLastCalledWith(code, { + engine: 'neato', format: 'svg', yInvert: true, + graphAttributes: { bgcolor: 'transparent', fontcolor: '#e6e7e8', ranksep: 1 }, + nodeAttributes: { color: '#8899aa', fontcolor: '#e6e7e8', fillcolor: '#222222', style: 'filled', shape: 'box' }, + edgeAttributes: { color: 'red', fontcolor: '#e6e7e8' }, + }) + } finally { + if (previous === null) document.body.removeAttribute('style') + else document.body.setAttribute('style', previous) + } +}) diff --git a/packages/client/ui-primitives/tests/mermaid-runtime.client.spec.ts b/packages/client/ui-primitives/tests/mermaid-runtime.client.spec.ts index 8b79a42b9a..863abde53d 100644 --- a/packages/client/ui-primitives/tests/mermaid-runtime.client.spec.ts +++ b/packages/client/ui-primitives/tests/mermaid-runtime.client.spec.ts @@ -13,7 +13,24 @@ beforeEach(() => { afterEach(() => { vi.doUnmock('mermaid') }) describe('Mermaid runtime', () => { - it('shares lazy initialization and removes each measurement container after rendering', async () => { + it('accepts native configuration without weakening preview restrictions', async () => { + const { renderMermaid } = await import('../src/markdown/mermaid.ts') + renderDiagram.mockResolvedValue({ svg: '' }) + await renderMermaid('flowchart LR', new AbortController().signal, { + // oxlint-disable-next-line typescript/no-deprecated + layout: 'dagre', flowchart: { nodeSpacing: 40, htmlLabels: true }, + themeVariables: { primaryColor: '#123456' }, + securityLevel: 'loose', startOnLoad: true, suppressErrorRendering: false, htmlLabels: true, secure: [], + }) + expect(initialize.mock.calls[0]![0]).toMatchObject({ + layout: 'dagre', flowchart: { nodeSpacing: 40, htmlLabels: false }, + themeVariables: { primaryColor: '#123456', darkMode: false }, + securityLevel: 'strict', startOnLoad: false, suppressErrorRendering: true, htmlLabels: false, + }) + expect(initialize.mock.calls[0]![0]).toHaveProperty('secure', expect.arrayContaining(['securityLevel'])) + }) + + it('reuses the runtime and removes each measurement container after rendering', async () => { const { renderMermaid } = await import('../src/markdown/mermaid.ts') expect(initialize).not.toHaveBeenCalled() const stages: HTMLElement[] = [] @@ -29,7 +46,7 @@ describe('Mermaid runtime', () => { renderMermaid('中文', new AbortController().signal), renderMermaid('second', new AbortController().signal), ]) - expect(initialize).toHaveBeenCalledOnce() + expect(initialize).toHaveBeenCalledTimes(2) expect(initialize).toHaveBeenCalledWith(expect.objectContaining({ startOnLoad: false, securityLevel: 'strict', suppressErrorRendering: true, htmlLabels: false, secure: [ @@ -42,6 +59,28 @@ describe('Mermaid runtime', () => { expect(stages.every(stage => !stage.isConnected)).toBe(true) }) + it('keeps theme initialization with its render and recovers the queue after failure', async () => { + const { renderMermaid } = await import('../src/markdown/mermaid.ts') + const first = Promise.withResolvers<{ svg: string }>() + renderDiagram.mockReturnValueOnce(first.promise).mockResolvedValue({ svg: '' }) + const light = renderMermaid('first', new AbortController().signal) + const failure = expect(light).rejects.toThrow('invalid') + await vi.waitFor(() => { expect(renderDiagram).toHaveBeenCalledOnce() }) + const previous = document.documentElement.style.colorScheme + try { + document.documentElement.style.colorScheme = 'dark' + const dark = renderMermaid('second', new AbortController().signal) + await Promise.resolve() + expect(initialize).toHaveBeenCalledOnce() + first.reject(new Error('invalid')) + await failure + await dark + expect(initialize.mock.calls[1]![0]).toMatchObject({ themeVariables: { darkMode: true } }) + } finally { + document.documentElement.style.colorScheme = previous + } + }) + it('removes measurement DOM even when Mermaid rejects', async () => { const { renderMermaid } = await import('../src/markdown/mermaid.ts') let stage: HTMLElement | undefined diff --git a/packages/client/ui-primitives/tests/source-preview.client.spec.tsx b/packages/client/ui-primitives/tests/source-preview.client.spec.tsx index fe356f64bb..7e63f06929 100644 --- a/packages/client/ui-primitives/tests/source-preview.client.spec.tsx +++ b/packages/client/ui-primitives/tests/source-preview.client.spec.tsx @@ -25,6 +25,31 @@ afterEach(() => { cleanup(); vi.unstubAllGlobals() }) beforeEach(() => { vi.resetAllMocks() }) describe('SourcePreview', () => { + it('updates mounted previews on theme changes and ignores obsolete completions', async () => { + const previous = document.documentElement.style.colorScheme + const old = Promise.withResolvers() + vi.mocked(renderMermaid).mockReturnValueOnce(old.promise).mockResolvedValue(imageUrl) + const view = render() + try { + const oldSignal = vi.mocked(renderMermaid).mock.calls[0]![1] + await act(async () => { document.documentElement.style.colorScheme = 'dark' }) + expect(oldSignal.aborted).toBe(true) + const image = await screen.findByRole('img') + await act(async () => { old.resolve('obsolete') }) + expect(image.getAttribute('src')).toBe(imageUrl) + await act(async () => { document.body.style.setProperty('--unrelated', '1') }) + expect(renderMermaid).toHaveBeenCalledTimes(2) + await act(async () => { document.documentElement.style.colorScheme = 'light' }) + expect(renderMermaid).toHaveBeenCalledTimes(3) + view.unmount() + await act(async () => { document.documentElement.style.colorScheme = 'dark' }) + expect(renderMermaid).toHaveBeenCalledTimes(3) + } finally { + view.unmount() + document.documentElement.style.colorScheme = previous + document.body.style.removeProperty('--unrelated') + } + }) it('shows loading until rendering completes, then displays an image without inserting SVG', async () => { const pending = Promise.withResolvers() vi.mocked(renderMermaid).mockReturnValue(pending.promise) From 4e0a405a9b0723b5b0d817077265590ef8ea3258 Mon Sep 17 00:00:00 2001 From: yudshj Date: Wed, 9 Sep 2026 18:01:36 +0800 Subject: [PATCH 09/27] fix(web): size diagram previews to image content --- ...9-content-sized-diagram-previews.i18n.yaml | 6 ++ ...26-09-09-content-sized-diagram-previews.md | 25 +++++ ...09-09-content-sized-diagram-previews.zh.md | 25 +++++ ...6-09-09-markdown-static-previews.i18n.yaml | 4 +- .../2026-09-09-markdown-static-previews.md | 2 +- .../2026-09-09-markdown-static-previews.zh.md | 2 +- .../expected/markdown-mermaid/ui.expected.md | 4 +- .../expected/markdown-mermaid/zh.expected.md | 4 +- apps/web/tests/markdown-mermaid.e2e.ts | 92 ++++++++++++++++--- .../client/ui-primitives/README.i18n.yaml | 4 +- packages/client/ui-primitives/README.md | 2 +- packages/client/ui-primitives/README.zh.md | 2 +- .../ui-primitives/src/markdown/graphviz.ts | 2 +- .../src/markdown/preview-document.ts | 10 +- .../ui-primitives/src/markdown/render.tsx | 2 +- .../tests/fence-previews.client.spec.tsx | 31 ++++--- 16 files changed, 170 insertions(+), 47 deletions(-) create mode 100644 .agents/notes/implemented/bug-fix/2026-09-09-content-sized-diagram-previews.i18n.yaml create mode 100644 .agents/notes/implemented/bug-fix/2026-09-09-content-sized-diagram-previews.md create mode 100644 .agents/notes/implemented/bug-fix/2026-09-09-content-sized-diagram-previews.zh.md diff --git a/.agents/notes/implemented/bug-fix/2026-09-09-content-sized-diagram-previews.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-09-09-content-sized-diagram-previews.i18n.yaml new file mode 100644 index 0000000000..be4173fd02 --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-09-09-content-sized-diagram-previews.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-09-09-content-sized-diagram-previews.md +2026-09-09-content-sized-diagram-previews.md: ceb29238eec09f2dde312835dbf376855998a507 +2026-09-09-content-sized-diagram-previews.zh.md: 2689602bfe3469f53e532367877f7e49d52d3d5d diff --git a/.agents/notes/implemented/bug-fix/2026-09-09-content-sized-diagram-previews.md b/.agents/notes/implemented/bug-fix/2026-09-09-content-sized-diagram-previews.md new file mode 100644 index 0000000000..ceb29238ee --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-09-09-content-sized-diagram-previews.md @@ -0,0 +1,25 @@ +# Agent Note: Content-sized diagram previews + +Status: implemented + +English | [中文](2026-09-09-content-sized-diagram-previews.zh.md) + +## Problem + +Short Graphviz and SVG images leave large empty regions when displayed inside 400px frames. A 59px Graphviz diagram occupies a 400px preview even though its rendered dimensions are already available to the browser. + +## Decision + +Mermaid, Graphviz, and SVG share the inert image canvas in `SourcePreview`. The browser derives its height from the image, preserves aspect ratio when shrinking to the available width, and adds 16px padding. Source switching retains the image and copying reads the original code. Theme changes regenerate Graphviz colors without introducing a separate sizing observer. + +This replaces the diagram-frame choice in [Static Markdown fence previews](../feature/2026-09-09-markdown-static-previews.md); that note continues to own HTML isolation, rendering lifecycle, and license obligations. HTML retains its opaque, script-free, 400px frame. SVG image mode disables scripts, link interaction, and external resource loading without inserting source-controlled markup into the application document. + +## Alternatives considered + +**Set iframe height to auto.** An iframe does not derive its outer height from its inner document. This still reserves an unrelated viewport. + +**Add a frame measurement script or same-origin access.** A diagram already has image dimensions. Extra execution or origin permissions and resize messaging are unnecessary for that content. + +## Consequences + +Short diagrams occupy only their rendered height and canvas padding. The browser geometry regression compares image and container height in light and dark modes, then verifies proportional scaling in a narrow viewport. Browser security checks include SVG scripts, event handlers, nested HTML, and external images; copy and source toggling remain covered. HTML automatic height remains outside this change because arbitrary document layout has different measurement and isolation requirements. diff --git a/.agents/notes/implemented/bug-fix/2026-09-09-content-sized-diagram-previews.zh.md b/.agents/notes/implemented/bug-fix/2026-09-09-content-sized-diagram-previews.zh.md new file mode 100644 index 0000000000..2689602bfe --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-09-09-content-sized-diagram-previews.zh.md @@ -0,0 +1,25 @@ +# Agent Note: 按内容定高的图表预览 + +Status: implemented + +[English](2026-09-09-content-sized-diagram-previews.md) | 中文 + +## Problem + +较矮的 Graphviz 与 SVG 图片显示在 400px iframe 中时,会留下大块空白。浏览器已经知道图片的渲染尺寸,但一张 59px 高的 Graphviz 图表仍占据 400px 的预览区域。 + +## Decision + +Mermaid、Graphviz 与 SVG 共享 `SourcePreview` 的不可执行图片画布。浏览器根据图片决定高度,在可用宽度不足时按比例缩小,并添加 16px 内边距。源码切换保留图片,复制读取原始代码。主题变化重新生成 Graphviz 配色,无需单独的尺寸观察器。 + +这替代了[静态 Markdown fence 预览](../feature/2026-09-09-markdown-static-previews.zh.md)中的图表 iframe 选择;该记录继续维护 HTML 隔离、渲染生命周期与许可证义务。HTML 保留不透明来源、禁用脚本的 400px iframe。SVG 图片模式禁用脚本、链接交互与外部资源加载,无需把源码控制的标记插入应用文档。 + +## Alternatives considered + +**把 iframe 高度设为 auto。** iframe 不会根据内部文档决定外部高度,仍然会保留与内容无关的视口。 + +**添加 iframe 测量脚本或同源访问。** 图表已经具备图片尺寸,不需要额外执行权限、来源权限或尺寸消息协议。 + +## Consequences + +较矮图表只占据渲染高度与画布内边距。浏览器几何回归用例在浅色和深色下比较图片与容器高度,再验证窄视口中的等比例缩放。浏览器安全检查包含 SVG 脚本、事件处理器、嵌套 HTML 与外部图片,并保留复制和源码切换覆盖。HTML 自动高度不属于本次改动,因为任意文档布局具有不同的测量与隔离要求。 diff --git a/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.i18n.yaml b/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.i18n.yaml index 8712ee2070..8a24afcd59 100644 --- a/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-09-markdown-static-previews.md -2026-09-09-markdown-static-previews.md: a5fa79fcc8c192dbcd3949debbabbccf85cb3b62 -2026-09-09-markdown-static-previews.zh.md: 30efe897702377c180c9e1650fda93ae42f6e580 +2026-09-09-markdown-static-previews.md: f3a0363e3d7c4cb19ca67dfc5011e936f7c024cf +2026-09-09-markdown-static-previews.zh.md: 24f87cd73504c1fbd274e1420a3692b3ca55ed5e diff --git a/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.md b/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.md index a5fa79fcc8..f3a0363e3d 100644 --- a/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.md +++ b/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.md @@ -18,7 +18,7 @@ The shared Markdown renderer enables settled `mermaid`, `graphviz`/`dot`, `svg`, Mermaid loads on demand. A shared queue serializes theme initialization with diagram work, and each call removes its temporary measurement DOM in `finally`. Strict security, disabled HTML labels, the application palette, and error-rendering policy cannot be overridden by diagram configuration. Generated SVG is displayed as an image without installing links or scripts. Intrinsic dimensions come from the SVG viewBox; large diagrams shrink to fit, and the canvas follows the code-block background. Mounted previews observe document theme attributes and regenerate only when resolved colors change; obsolete renders cannot publish. Graphviz default colors follow the same palette, while authored DOT, SVG, and HTML colors remain intact. -HTML uses DOMPurify with navigation attributes and document-loading elements forbidden, followed by an opaque `sandbox=""` iframe. A trusted CSP precedes source markup and permits only inline CSS, data images, and data fonts. Scripts, external resources, child frames, form submission, and same-origin access are unavailable. SVG is parsed as XML and placed in an image inside the frame, so SVG scripts and links never become active. Frames have a fixed scrollable viewport: measuring their content would require additional origin access or trusted frame scripts. +HTML uses DOMPurify with navigation attributes and document-loading elements forbidden, followed by an opaque `sandbox=""` iframe. A trusted CSP precedes source markup and permits only inline CSS, data images, and data fonts. Scripts, external resources, child frames, form submission, and same-origin access are unavailable. SVG is parsed as XML and displayed with Graphviz output as inert images in a [content-sized canvas](../bug-fix/2026-09-09-content-sized-diagram-previews.md), so SVG scripts and links never become active. HTML frames have a fixed scrollable viewport: measuring their content would require additional origin access or trusted frame scripts. Graphviz uses the pinned, unmodified `@viz-js/viz` 3.30.0 WebAssembly distribution with the `dot` layout engine. Its npm MIT declaration covers the wrapper; its build provenance identifies Graphviz 16.0.0 (EPL-2.0), Expat 2.8.4 (MIT), and Emscripten 5.0.7 (MIT/NCSA). Distribution retains these component terms and the exact Graphviz source download under EPL-2.0 section 3.1. [Full preview notices](../../../../packages/client/ui-primitives/THIRD_PARTY_PREVIEW_NOTICES.txt) also retain Mermaid's MIT text and select Apache-2.0 for DOMPurify. The UI primitives package ships the notices and the Web build emits the same bytes alongside its assets. License review covers embedded payloads separately from the general permissive npm-metadata policy. diff --git a/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.zh.md b/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.zh.md index 30efe89770..24f87cd735 100644 --- a/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.zh.md +++ b/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.zh.md @@ -18,7 +18,7 @@ Status: implemented Mermaid 按需加载。共享队列将主题初始化与图表渲染一起串行执行,每次调用都在 `finally` 中删除临时测量 DOM。图表配置无法覆盖严格安全模式、禁用 HTML 标签、应用配色和错误渲染策略。生成的 SVG 作为图片显示,不安装链接或脚本。固有尺寸来自 SVG viewBox;大图缩小以适应宽度,画布使用代码块背景。已挂载的预览观察文档主题属性,仅在解析后的配色变化时重新生成;过期渲染不能发布结果。Graphviz 的默认颜色采用同一配色,DOT、SVG 和 HTML 中明确指定的颜色保持原样。 -HTML 先由 DOMPurify 清理,禁止导航属性与文档加载元素,再进入不透明来源的 `sandbox=""` iframe。可信 CSP 位于源码标记之前,仅允许内联 CSS、data 图片与 data 字体。脚本、外部资源、子 iframe、表单提交与同源访问均不可用。SVG 按 XML 解析后作为图片放进 iframe,因此 SVG 脚本与链接不会激活。iframe 使用固定的可滚动视口:测量内容需要额外的来源访问权限或可信 iframe 脚本。 +HTML 先由 DOMPurify 清理,禁止导航属性与文档加载元素,再进入不透明来源的 `sandbox=""` iframe。可信 CSP 位于源码标记之前,仅允许内联 CSS、data 图片与 data 字体。脚本、外部资源、子 iframe、表单提交与同源访问均不可用。SVG 按 XML 解析后,与 Graphviz 输出一同作为不可执行图片显示在[按内容定高的画布](../bug-fix/2026-09-09-content-sized-diagram-previews.zh.md)中,因此 SVG 脚本与链接不会激活。HTML iframe 使用固定的可滚动视口:测量内容需要额外的来源访问权限或可信 iframe 脚本。 Graphviz 使用固定且未修改的 `@viz-js/viz` 3.30.0 WebAssembly 发布包,布局引擎为 `dot`。其 npm MIT 声明覆盖包装层;构建来源记录标明 Graphviz 16.0.0(EPL-2.0)、Expat 2.8.4(MIT)与 Emscripten 5.0.7(MIT/NCSA)。分发保留各组件条款,并按 EPL-2.0 第 3.1 节提供准确的 Graphviz 源码下载地址。[完整预览声明](../../../../packages/client/ui-primitives/THIRD_PARTY_PREVIEW_NOTICES.txt)还保留 Mermaid 的 MIT 文本,并为 DOMPurify 选择 Apache-2.0。UI primitives 包携带该声明,Web 构建在资源旁输出相同字节。内嵌产物的许可证检查独立于通用的宽松 npm 元数据策略。 diff --git a/apps/web/tests/expected/markdown-mermaid/ui.expected.md b/apps/web/tests/expected/markdown-mermaid/ui.expected.md index 369f0ef21d..985f68fee0 100644 --- a/apps/web/tests/expected/markdown-mermaid/ui.expected.md +++ b/apps/web/tests/expected/markdown-mermaid/ui.expected.md @@ -15,10 +15,10 @@ - img "Mermaid diagram" - button "Source" - button "Copy" -- iframe +- img "Graphviz diagram" - button "Source" - button "Copy" -- iframe +- img "SVG preview" - button "Source" - button "Copy" - iframe diff --git a/apps/web/tests/expected/markdown-mermaid/zh.expected.md b/apps/web/tests/expected/markdown-mermaid/zh.expected.md index 74a348e8be..52d7053ded 100644 --- a/apps/web/tests/expected/markdown-mermaid/zh.expected.md +++ b/apps/web/tests/expected/markdown-mermaid/zh.expected.md @@ -14,10 +14,10 @@ - img "Mermaid 图表" - button "源码" - button "复制" -- iframe +- img "Graphviz 图表" - button "源码" - button "复制" -- iframe +- img "SVG 预览" - button "源码" - button "复制" - iframe diff --git a/apps/web/tests/markdown-mermaid.e2e.ts b/apps/web/tests/markdown-mermaid.e2e.ts index d970cc9e11..95249740bf 100644 --- a/apps/web/tests/markdown-mermaid.e2e.ts +++ b/apps/web/tests/markdown-mermaid.e2e.ts @@ -27,7 +27,12 @@ const UNTRUSTED = [ ].join('\n') const DOT = 'digraph { rankdir=LR; Input -> Preview }' -const SVG = 'SVG preview' +const SVG = ` +SVG preview + + +
+
` const HTML = `

Static HTML

@@ -88,6 +93,52 @@ describe('web e2e: Mermaid chat previews', () => { await scaffold?.close() }) + it.skipIf(MODE === 'record').each(['light', 'dark'] as const)( + 'tightly wraps short diagram previews in %s mode', async (scheme) => { + const page = await newEnglishPage(browser) + try { + await page.emulateMedia({ colorScheme: scheme }) + await openConversation(page, scaffold) + await expect.poll(() => page.evaluate(() => getComputedStyle(document.documentElement).colorScheme)).toBe(scheme) + + const mermaid = page.getByRole('img', { name: 'Mermaid diagram' }).first() + await mermaid.evaluate(async (node: HTMLImageElement) => { await node.decode() }) + const mermaidHeight = await mermaid.evaluate(node => node.getBoundingClientRect().height) + const canvasHeight = await mermaid.evaluate(node => node.parentElement!.getBoundingClientRect().height) + // Compact diagrams need at most the Mermaid canvas's 16px padding on each side. + expect(canvasHeight - mermaidHeight).toBeLessThanOrEqual(32) + + for (const title of ['Graphviz diagram', 'SVG preview']) { + const image = page.getByRole('img', { name: title, exact: true }) + await image.evaluate(async (node: HTMLImageElement) => { await node.decode() }) + const imageHeight = await image.evaluate(node => node.getBoundingClientRect().height) + const containerHeight = await image.evaluate(node => node.parentElement!.getBoundingClientRect().height) + expect(imageHeight).toBeGreaterThan(0) + expect(containerHeight).toBeGreaterThanOrEqual(imageHeight) + expect.soft(containerHeight - imageHeight, + `${scheme} ${title}: ${containerHeight}px container around ${imageHeight}px content`, + ).toBeLessThanOrEqual(32) + } + await page.setViewportSize({ width: 360, height: 800 }) + const svg = page.getByRole('img', { name: 'SVG preview', exact: true }) + await expect.poll(() => svg.evaluate((node) => { + const { width, height } = node.getBoundingClientRect() + return width > 0 && height > 0 && width < 460 + })).toBe(true) + const compact = await svg.evaluate((node: HTMLImageElement) => ({ + width: node.getBoundingClientRect().width, + height: node.getBoundingClientRect().height, + canvasHeight: node.parentElement!.getBoundingClientRect().height, + ratio: node.naturalWidth / node.naturalHeight, + })) + expect(compact.width / compact.height).toBeCloseTo(compact.ratio, 1) + expect(compact.canvasHeight - compact.height).toBeLessThanOrEqual(32) + } finally { + await page.close() + } + }, + ) + it.skipIf(MODE === 'record')('updates existing diagrams when the document switches between light and dark', async () => { const page = await newEnglishPage(browser) try { @@ -102,10 +153,19 @@ describe('web e2e: Mermaid chat previews', () => { }, scheme) await expect.poll(() => image.getAttribute('src')).not.toBe(previous) await expect.poll(() => image.evaluate(node => (node as HTMLImageElement).naturalWidth)).toBeGreaterThan(0) - const colors = await image.evaluate(node => ({ - canvas: getComputedStyle(node.parentElement!).backgroundColor, - expected: getComputedStyle(document.body).getPropertyValue('--dsw-alias-markdown-code-block').trim(), - })) + const colors = await image.evaluate((node) => { + const reference = document.createElement('span') + reference.style.backgroundColor = 'var(--dsw-alias-markdown-code-block)' + document.body.append(reference) + try { + return { + canvas: getComputedStyle(node.parentElement!).backgroundColor, + expected: getComputedStyle(reference).backgroundColor, + } + } finally { + reference.remove() + } + }) expect(colors.canvas).toBe(colors.expected) await expect.poll(() => page.getByTitle('HTML preview', { exact: true }).getAttribute('srcdoc')) .toContain(`color-scheme:${scheme}`) @@ -186,7 +246,7 @@ describe('web e2e: Mermaid chat previews', () => { await page.close() }, 60_000) - it.skipIf(MODE === 'record')('previews DOT, SVG, and static HTML inside isolated frames', async () => { + it.skipIf(MODE === 'record')('previews inert DOT and SVG images and isolates static HTML', async () => { const page = await newEnglishPage(browser) const requests: string[] = [] const dialogs: string[] = [] @@ -198,27 +258,29 @@ describe('web e2e: Mermaid chat previews', () => { page.on('dialog', (dialog) => { dialogs.push(dialog.message()); void dialog.dismiss() }) await page.context().grantPermissions(['clipboard-read', 'clipboard-write']) await openConversation(page, scaffold) - for (const [title, code] of [['Graphviz diagram', DOT], ['SVG preview', SVG], ['HTML preview', HTML]]) { - const frame = page.locator(`iframe[title="${title}"]`) - await frame.waitFor() - expect(await frame.getAttribute('sandbox')).toBe('') - const body = frame.contentFrame() + for (const [title, code] of [['Graphviz diagram', DOT], ['SVG preview', SVG], ['HTML preview', HTML]] as const) { + const preview = title === 'HTML preview' + ? page.getByTitle(title, { exact: true }) + : page.getByRole('img', { name: title, exact: true, includeHidden: true }) + await preview.waitFor() if (title === 'HTML preview') { + expect(await preview.getAttribute('sandbox')).toBe('') + const body = preview.contentFrame() await body.getByRole('heading', { name: 'Static HTML' }).waitFor() expect(await body.getByRole('heading').evaluate(node => getComputedStyle(node).color)).toBe('rgb(0, 128, 0)') await body.getByText('Disabled navigation').click() - expect(await frame.evaluate(node => (node as HTMLIFrameElement).contentDocument)).toBeNull() + expect(await preview.evaluate(node => (node as HTMLIFrameElement).contentDocument)).toBeNull() } else { - await expect.poll(() => body.locator('img').evaluate(node => (node as HTMLImageElement).naturalWidth)).toBeGreaterThan(0) + await expect.poll(() => preview.evaluate(node => (node as HTMLImageElement).naturalWidth)).toBeGreaterThan(0) } - const block = page.locator('.md-code-block').filter({ has: frame }) + const block = page.locator('.md-code-block').filter({ has: preview }) await block.hover() await block.getByRole('button', { name: 'Copy', exact: true }).click() expect(await page.evaluate(() => navigator.clipboard.readText())).toBe(code) await block.getByRole('button', { name: 'Source', exact: true }).click() expect(await block.locator('pre code').textContent()).toBe(code) await block.getByRole('button', { name: 'Preview', exact: true }).click() - expect(await frame.isVisible()).toBe(true) + expect(await preview.isVisible()).toBe(true) } expect(await page.locator('body').getAttribute('data-preview-escaped')).toBeNull() expect(dialogs).toEqual([]) diff --git a/packages/client/ui-primitives/README.i18n.yaml b/packages/client/ui-primitives/README.i18n.yaml index b3a61421d4..bca6072d44 100644 --- a/packages/client/ui-primitives/README.i18n.yaml +++ b/packages/client/ui-primitives/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-primitives/README.md -README.md: 8bdd2b7f90ebe4a998be8158351e49f815343c78 -README.zh.md: b861f9e17ff73f20717aed63b5b087358daeae10 +README.md: e6f63196375a4f96416ea05efc03cb72e8182823 +README.zh.md: 323a09e396c9b40b1401909a81998bf35af0ae6f diff --git a/packages/client/ui-primitives/README.md b/packages/client/ui-primitives/README.md index 8bdd2b7f90..e6f6319637 100644 --- a/packages/client/ui-primitives/README.md +++ b/packages/client/ui-primitives/README.md @@ -77,7 +77,7 @@ Supply `MarkdownLabels.preview` to enable settled `mermaid`, `graphviz`/`dot`, ` `CodeBlock.preview` supplies an optional headerless body with icon actions for source switching and copying. Actions appear on hover or keyboard focus, and stay visible below the diagram whenever a touchscreen is available, including with a mouse attached. Switching to source keeps the preview mounted, so returning reuses its result; copying always retains the source. -Each settled fence defaults to visualization and shares the source/copy controls. Graphviz loads `@viz-js/viz` on demand and renders DOT with the `dot` engine. SVG and Graphviz output are inert images inside opaque sandbox iframes; malformed SVG and DOT retain their source with an error. HTML accepts complete documents or fragments and preserves inline styles, but strips scripts and navigation. Frames have no sandbox permissions, and their Content Security Policy blocks external resources, embedded frames, forms, and scripts. Frames use a 400px scrollable viewport. Raw HTML outside a code fence remains ordinary Markdown input. +Each settled fence defaults to visualization and shares the source/copy controls. Graphviz loads `@viz-js/viz` on demand and renders DOT with the `dot` engine. SVG and Graphviz output are inert images in the same content-sized canvas as Mermaid; malformed SVG and DOT retain their source with an error. HTML accepts complete documents or fragments and preserves inline styles, but strips scripts and navigation. HTML frames have no sandbox permissions, and their Content Security Policy blocks external resources, embedded frames, forms, and scripts. Diagram canvases follow the rendered image height with 16px padding and shrink images proportionally to fit the available width. HTML frames use a 400px scrollable viewport. Raw HTML outside a code fence remains ordinary Markdown input. Internal render functions accept optional native `MermaidConfig` and Viz.js `RenderOptions` for developer customization. Attribute and theme-variable overrides merge with document defaults; Mermaid preview restrictions and Graphviz SVG output remain fixed. These parameters have no settings UI. diff --git a/packages/client/ui-primitives/README.zh.md b/packages/client/ui-primitives/README.zh.md index b861f9e17f..323a09e396 100644 --- a/packages/client/ui-primitives/README.zh.md +++ b/packages/client/ui-primitives/README.zh.md @@ -77,7 +77,7 @@ kind: "package-library" `CodeBlock.preview` 提供无标题栏的替代正文,以及切换源码和复制的图标操作。操作在悬停或键盘聚焦时出现;只要设备具备触屏,就在图表下方保持可见,包括同时连接鼠标的情况。切换到源码时保留已挂载的预览,返回时复用其结果;复制始终保留源码。 -每个定稿 fence 默认显示可视化,并共享源码切换与复制操作。Graphviz 按需加载 `@viz-js/viz`,用 `dot` 引擎渲染 DOT。SVG 与 Graphviz 输出作为不可执行的图片放入不透明来源的 sandbox iframe;非法 SVG 与 DOT 显示错误并保留源码。HTML 接受完整文档或片段、保留内联样式,同时移除脚本与导航。iframe 不授予任何 sandbox 权限,Content Security Policy 阻止外部资源、嵌套 iframe、表单与脚本。iframe 使用可滚动的 400px 视口。代码 fence 外的原始 HTML 仍按普通 Markdown 输入处理。 +每个定稿 fence 默认显示可视化,并共享源码切换与复制操作。Graphviz 按需加载 `@viz-js/viz`,用 `dot` 引擎渲染 DOT。SVG 与 Graphviz 输出作为不可执行的图片,使用与 Mermaid 相同的按内容定高画布;非法 SVG 与 DOT 显示错误并保留源码。HTML 接受完整文档或片段、保留内联样式,同时移除脚本与导航。HTML iframe 不授予任何 sandbox 权限,Content Security Policy 阻止外部资源、嵌套 iframe、表单与脚本。图表画布随渲染后的图片高度收缩,保留 16px 内边距,并在可用宽度不足时等比例缩小图片。HTML iframe 使用可滚动的 400px 视口。代码 fence 外的原始 HTML 仍按普通 Markdown 输入处理。 内部渲染函数接受可选的原生 `MermaidConfig` 与 Viz.js `RenderOptions`,供开发者定制。属性与主题变量覆盖项和文档默认值合并;Mermaid 预览限制与 Graphviz SVG 输出保持固定。这些参数没有设置界面。 diff --git a/packages/client/ui-primitives/src/markdown/graphviz.ts b/packages/client/ui-primitives/src/markdown/graphviz.ts index fc8f90315b..766fe0633c 100644 --- a/packages/client/ui-primitives/src/markdown/graphviz.ts +++ b/packages/client/ui-primitives/src/markdown/graphviz.ts @@ -11,7 +11,7 @@ let runtime: ReturnType | undefined * @param code - Complete Graphviz DOT source. * @param signal - Cancels work waiting for the runtime; synchronous layout cannot be interrupted. * @param options - Native overrides; attribute maps merge over theme defaults and output remains SVG regardless of format. - * @returns A srcdoc document. Import, layout, invalid source, and cancellation failures reject. + * @returns An SVG image data URL. Import, layout, invalid source, and cancellation failures reject. */ export async function renderGraphviz(code: string, signal: AbortSignal, options?: RenderOptions): Promise { runtime ??= import('@viz-js/viz').then(module => module.instance()).catch((error: unknown) => { diff --git a/packages/client/ui-primitives/src/markdown/preview-document.ts b/packages/client/ui-primitives/src/markdown/preview-document.ts index 15aa3bf2f3..f62e227f44 100644 --- a/packages/client/ui-primitives/src/markdown/preview-document.ts +++ b/packages/client/ui-primitives/src/markdown/preview-document.ts @@ -1,4 +1,4 @@ -/** Static preview documents: opaque sandbox frames with no scripts, navigation, or remote resources. */ +/** Static HTML documents and SVG image URLs without executable source markup. */ import DOMPurify from 'dompurify' import { readPreviewTheme } from './preview-theme.ts' @@ -28,10 +28,10 @@ export function renderHtml(code: string, signal: AbortSignal): string { } /** - * Prepare an SVG image inside an isolated document, without activating SVG scripts or links. + * Prepare an SVG image URL without activating SVG scripts, links, or external resources. * @param code - Complete SVG XML; malformed XML or a non-SVG root throws. * @param signal - Prevents preparation after the preview owner is cancelled. - * @returns A srcdoc document containing a responsive inert SVG image. + * @returns A data URL for an image whose intrinsic dimensions determine the preview height. */ export function renderSvg(code: string, signal: AbortSignal): string { signal.throwIfAborted() @@ -40,7 +40,5 @@ export function renderSvg(code: string, signal: AbortSignal): string { || parsed.documentElement.namespaceURI !== 'http://www.w3.org/2000/svg') { throw new Error('Invalid SVG document') } - // Double-quoted attributes keep encodeURIComponent's literal apostrophes inert. - const url = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(code)}` - return documentOf(``) + return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(code)}` } diff --git a/packages/client/ui-primitives/src/markdown/render.tsx b/packages/client/ui-primitives/src/markdown/render.tsx index f96884df64..0f67ce6a7d 100644 --- a/packages/client/ui-primitives/src/markdown/render.tsx +++ b/packages/client/ui-primitives/src/markdown/render.tsx @@ -421,7 +421,7 @@ function fencePreview(lang: string | undefined, code: string, labels: MarkdownLa const render = { mermaid: renderMermaid, graphviz: renderGraphviz, svg: renderSvg, html: renderHtml }[kind] return { content: ( - + ), previewLabel: labels.preview.preview, sourceLabel: labels.preview.source, diff --git a/packages/client/ui-primitives/tests/fence-previews.client.spec.tsx b/packages/client/ui-primitives/tests/fence-previews.client.spec.tsx index 8e8b4abf58..f7fe436821 100644 --- a/packages/client/ui-primitives/tests/fence-previews.client.spec.tsx +++ b/packages/client/ui-primitives/tests/fence-previews.client.spec.tsx @@ -35,22 +35,29 @@ describe('Markdown fence previews', () => { expect(view.container.querySelector('iframe')).toBeNull() expect(view.container.querySelector('pre code')?.textContent).toBe(code) view.rerender() - const frame = await screen.findByTitle(title) - expect(frame.getAttribute('sandbox')).toBe('') - expect(frame.getAttribute('referrerpolicy')).toBe('no-referrer') - expect(frame.getAttribute('srcdoc')).toContain('Content-Security-Policy') + const findPreview = async () => lang === 'html' ? await screen.findByTitle(title) : await screen.findByRole('img', { name: title }) + const element = await findPreview() + if (lang === 'html') { + expect(element.getAttribute('sandbox')).toBe('') + expect(element.getAttribute('referrerpolicy')).toBe('no-referrer') + expect(element.getAttribute('srcdoc')).toContain('Content-Security-Policy') + } else { + expect(element.getAttribute('src')).toMatch(/^data:image\/svg\+xml/) + expect(view.container.querySelector('iframe')).toBeNull() + expect(element.parentElement!.querySelector('svg')).toBeNull() + } expect(view.container.querySelector('pre code')).toBeNull() fireEvent.click(screen.getByRole('button', { name: 'Source' })) expect(view.container.querySelector('pre code')?.textContent).toBe(code) - expect(frame.closest('[hidden]')).not.toBeNull() + expect(element.closest('[hidden]')).not.toBeNull() fireEvent.click(screen.getByRole('button', { name: markdownLabels.code.copyLabel })) await waitFor(() => { expect(writeText).toHaveBeenCalledWith(code) }) fireEvent.click(screen.getByRole('button', { name: 'Preview' })) - expect(screen.getByTitle(title)).toBe(frame) - expect(frame.closest('[hidden]')).toBeNull() + expect(await findPreview()).toBe(element) + expect(element.closest('[hidden]')).toBeNull() view.unmount() render() - await screen.findByTitle(title) + await findPreview() fireEvent.click(screen.getByRole('button', { name: markdownLabels.code.copyLabel })) await waitFor(() => { expect(writeText).toHaveBeenCalledTimes(2) }) }) @@ -60,7 +67,7 @@ describe('Markdown fence previews', () => { await screen.findByText(status.error) expect(view.container.querySelector('pre code')?.textContent).toBe('broken') view.rerender() - await screen.findByTitle(preview.svg.diagram) + await screen.findByRole('img', { name: preview.svg.diagram }) expect(screen.queryByText(status.error)).toBeNull() }) @@ -90,9 +97,9 @@ describe('static preview documents', () => { it('encodes SVG as an image rather than executable document markup', () => { const code = '' - const parsed = new DOMParser().parseFromString(renderSvg(code, new AbortController().signal), 'text/html') - expect(parsed.querySelector('svg, script')).toBeNull() - expect(decodeURIComponent(parsed.querySelector('img')!.getAttribute('src')!)).toContain(code) + const url = renderSvg(code, new AbortController().signal) + expect(url).toMatch(/^data:image\/svg\+xml;charset=utf-8,/) + expect(decodeURIComponent(url.slice(url.indexOf(',') + 1))).toBe(code) }) it.each(['', '', ''])('rejects non-SVG or malformed XML: %s', (code) => { From 3c4c6c3d7b7079d1ea3f721ddf3d519ba8ba7cc9 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Wed, 9 Sep 2026 18:20:01 +0800 Subject: [PATCH 10/27] fix(ci): cancel superseded PR and master validation --- .../2026-07-26-ci-failover-runbook.i18n.yaml | 4 +- .../process/2026-07-26-ci-failover-runbook.md | 10 +-- .../2026-07-26-ci-failover-runbook.zh.md | 10 +-- ...26-09-06-master-only-platform-ci.i18n.yaml | 4 +- .../2026-09-06-master-only-platform-ci.md | 2 +- .../2026-09-06-master-only-platform-ci.zh.md | 2 +- .../2026-09-09-cancel-superseded-ci.i18n.yaml | 6 ++ .../2026-09-09-cancel-superseded-ci.md | 39 +++++++++++ .../2026-09-09-cancel-superseded-ci.zh.md | 39 +++++++++++ .../2026-06-19-real-api-e2e-ci.i18n.yaml | 4 +- .../testing/2026-06-19-real-api-e2e-ci.md | 2 +- .../testing/2026-06-19-real-api-e2e-ci.zh.md | 2 +- .../workflows/build-exe-for-python-sdk.yml | 3 +- .github/workflows/ci-master.yml | 9 +-- .github/workflows/ci.yml | 10 ++- .github/workflows/e2e.yml | 6 +- .github/workflows/release-vendor.yml | 2 +- .github/workflows/release.yml | 2 +- scripts/ci-workflow.spec.ts | 65 +++++++++++++------ scripts/tests/ci-master-platforms.spec.ts | 41 +++++++++++- scripts/tests/ci-release-selfhosted.spec.ts | 2 +- 21 files changed, 198 insertions(+), 66 deletions(-) create mode 100644 .agents/notes/implemented/process/2026-09-09-cancel-superseded-ci.i18n.yaml create mode 100644 .agents/notes/implemented/process/2026-09-09-cancel-superseded-ci.md create mode 100644 .agents/notes/implemented/process/2026-09-09-cancel-superseded-ci.zh.md diff --git a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml index 90fdf49b49..62e46dfa22 100644 --- a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml +++ b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md -2026-07-26-ci-failover-runbook.md: f24cb8b8239141cd1ccf468a566dba620dd3cfdd -2026-07-26-ci-failover-runbook.zh.md: 57c4a92a3af720d9b11b7a1ce7a1515b83c77339 +2026-07-26-ci-failover-runbook.md: 559fa61bcf416bed3bd58b3ffcbc145038cdce22 +2026-07-26-ci-failover-runbook.zh.md: e2098b928b1a1f158bcbfabd940ca23a5cd0e28e diff --git a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md index f24cb8b823..559fa61bcf 100644 --- a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md +++ b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md @@ -12,11 +12,7 @@ The three required Linux worker jobs in [CI](../../../../.github/workflows/ci.ym The three primary Linux jobs (`node-24`, `node-24-coverage`, `node-24-consumers`), the three `node-compat` matrix entries, and `all-checks-passed` resolve through `DSH_CI_FAILOVER_LINUX`; the native Windows jobs resolve through `DSH_CI_FAILOVER_WINDOWS`. A platform switch does not redirect the other platform. Set to `selfhosted` by a repository writer, the applicable trusted jobs select `vm-backup` or `dsh-win-ci`; otherwise they retain their workflow-defined hosted fallbacks. Node compatibility jobs require a same-repository, non-fork head and a non-Dependabot author, use isolated runtime setup, and retain `ubuntu-latest` fallback. Linux failover bounds snapshot concurrency and skips hosted package-cache restores. The verdict follows its workers so it does not remain queued on an unavailable hosted pool. Each switch is writer-manageable repository state, not a merge, so it works while checks are red. The `serial / linux (self-hosted standby)` and `serial / windows (self-hosted standby)` lanes re-prove the complete unsharded aggregates on master pushes. -`ci-master.yml` exempts exactly one event from `cancel-in-progress` (`${{ github.event_name != 'push' }}`), so one master push does not cancel the drill still running from the previous one. Each drill runs its complete unsharded aggregate with one gate worker, which takes longer than the interval between master merges; under unconditional cancellation a drill is superseded before reaching a verdict and the lane yields no readiness evidence for a responder to check. - -The exemption is narrower than "a drill always finishes", in two ways. GitHub keeps a single pending entry per group, so a newer pending run displaces an older one and intermediate push runs still end as `cancelled` during busy periods. And the expression is evaluated against the *newly triggered* run, so a run whose own event is not `push` — a benchmark dispatched on master within `ci-master.yml`, sharing its group `CI master-` — evaluates to `true` and does cancel a drill that is mid-flight. That is a rare manual action and the next master push restores the evidence, so it does not warrant further mechanism. What the carve-out buys is that the lane periodically reaches a verdict at all, which is what makes it usable as evidence. - -The decision belongs at workflow level because cancellation applies to the whole superseded run: a job-level `concurrency` group does not exempt its job. The negated form is load-bearing rather than cosmetic: naming `pull_request` alone would also stop cancelling `workflow_dispatch`, and each runner benchmark fans out to twelve larger runners for up to fifteen minutes inside this same group on master, so a re-dispatch would queue ahead of a drill instead of replacing a stale measurement. What bounds the cost is that a master push in `ci-master.yml` carries the [post-merge runtime and Wine checks](2026-09-06-master-only-platform-ci.md) and these two drills; the pull-request jobs live in the separate `ci.yml` (which does not see `push`), and the benchmarks are `workflow_dispatch`-gated within `ci-master.yml`. `scripts/ci-workflow.spec.ts` pins that push-reachable set — classifying by exact condition, since a negated event test mentions the event it excludes — so a new push-reachable job cannot quietly start accumulating uncancelled runs. +The [superseded-CI cancellation policy](2026-09-09-cancel-superseded-ci.md) governs master pushes and manual runs in the same workflow/ref group, including standby drills. Rapid master updates can starve a drill before it reaches a verdict. Use the latest completed standby verdict and check its age and commit before treating it as readiness evidence; a cancelled or merely scheduled run is not proof of readiness. ### Release rehearsals share the Linux switch @@ -44,7 +40,7 @@ The two switches are independent: flip only the one whose platform is degraded. ## Capacity during failover -Capacity includes the master standby, main-CI jobs, and three release-rehearsal jobs for each eligible PR or master push while the Linux switch is set. Each trusted PR also adds three Node compatibility jobs at gate concurrency one, including the build-backed Node 22 leg and cold temporary runtime downloads. The release workflows do not cancel running rehearsals when another run arrives, so overlapping refs can add sustained build, pack, and install load. Check current CPU, memory, disk, and queue pressure before extending self-hosted operation; extra registrations on this VM add scheduling slots, not machine resources. Do not infer spare capacity from the standby alone. When host resources permit extra registrations, use an org registration token (org Settings → Actions → Runners → New runner). Clone an existing runner directory **excluding its identity files** — `rsync -a --exclude '.runner*' --exclude '.credentials*' --exclude '_diag' --exclude '_work' / /` (the globs also catch `.runner_migrated`/`.credentials_migrated`, which GitHub writes on migrated runners and which equally trigger the already-configured refusal) — then run `config.sh` (copying `.runner`/`.credentials` verbatim makes it refuse with "already configured"), and **start the listener**: `sudo ./svc.sh install ubuntu && sudo ./svc.sh start`. Registration alone leaves the runner offline; a started service adds a scheduling slot, not CPU or memory. +Capacity includes the master standby, main-CI jobs, and three release-rehearsal jobs for each eligible PR or master push while the Linux switch is set. Each trusted PR also adds three Node compatibility jobs at gate concurrency one, including the build-backed Node 22 leg and cold temporary runtime downloads. The release rehearsal workflows cancel superseded runs within each workflow/ref group under the [cancellation policy](2026-09-09-cancel-superseded-ci.md); different refs can still add concurrent build, pack, and install load. Check current CPU, memory, disk, and queue pressure before extending self-hosted operation; extra registrations on this VM add scheduling slots, not machine resources. Do not infer spare capacity from the standby alone. When host resources permit extra registrations, use an org registration token (org Settings → Actions → Runners → New runner). Clone an existing runner directory **excluding its identity files** — `rsync -a --exclude '.runner*' --exclude '.credentials*' --exclude '_diag' --exclude '_work' / /` (the globs also catch `.runner_migrated`/`.credentials_migrated`, which GitHub writes on migrated runners and which equally trigger the already-configured refusal) — then run `config.sh` (copying `.runner`/`.credentials` verbatim makes it refuse with "already configured"), and **start the listener**: `sudo ./svc.sh install ubuntu && sudo ./svc.sh start`. Registration alone leaves the runner offline; a started service adds a scheduling slot, not CPU or memory. ### Switch back @@ -63,4 +59,4 @@ The variables are writer-manageable repository state; a pull request event itsel ## Consequences -Recovering from a hosted-pool outage is flipping the affected platform's variable (any writer) plus a re-run, with no merge on the critical path. The cost is a second runner topology per platform to keep working: the standby lanes exercise them on every master push so the failover targets never go stale, and the snapshot-concurrency and cache-restore branches in `ci.yml` carry a `selfhosted` leg (Linux only) that must stay in step with the hosted leg. Splitting the switch by platform adds one more variable to manage but bounds the blast radius of each switch to the jobs of a single platform. +Recovering from a hosted-pool outage is flipping the affected platform's variable (any writer) plus a re-run, with no merge on the critical path. The cost is a second runner topology per platform to keep working: master pushes schedule the standby lanes, but only completed verdicts establish readiness under the [cancellation policy](2026-09-09-cancel-superseded-ci.md), and the snapshot-concurrency and cache-restore branches in `ci.yml` carry a `selfhosted` leg (Linux only) that must stay in step with the hosted leg. Splitting the switch by platform adds one more variable to manage but bounds the blast radius of each switch to the jobs of a single platform. diff --git a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md index 57c4a92a3a..e2098b928b 100644 --- a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md +++ b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md @@ -12,11 +12,7 @@ Status: implemented 三个主要 Linux 作业(`node-24`、`node-24-coverage`、`node-24-consumers`)、三个 `node-compat` 矩阵条目和 `all-checks-passed` 通过 `DSH_CI_FAILOVER_LINUX` 解析;原生 Windows 作业通过 `DSH_CI_FAILOVER_WINDOWS` 解析。一个平台的开关不会重定向另一个平台。仓库写者将变量设为 `selfhosted` 时,适用的可信作业选择 `vm-backup` 或 `dsh-win-ci`;否则保留工作流定义的托管回退。Node 兼容性作业要求同仓库且非 fork 的头部以及非 Dependabot 作者,使用隔离运行时设置,并保留 `ubuntu-latest` 回退。Linux 故障切换限制快照并发,并跳过托管软件包缓存恢复。判定作业跟随工作作业,避免继续在不可用的托管池排队。每个开关都是写者可管理的仓库状态而非一次合并,因此在检查失败时仍然有效。`serial / linux (self-hosted standby)` 与 `serial / windows (self-hosted standby)` 通道在 master 推送上重新验证完整的未分片聚合流程。 -`ci-master.yml` 只豁免一个事件不做取消(`${{ github.event_name != 'push' }}`),因此一次 master 推送不会取消上一次推送留下的、仍在运行的演练。每次演练以单门禁工作进程执行完整的未分片聚合流程,耗时长于 master 合并的间隔;在无条件取消下,演练会在得出结论前被后续运行取代,该通道无法产出供响应者查看的就绪证据。 - -这项豁免比「演练总能跑完」要窄,有两点限制。其一,GitHub 每个组只保留一个待运行条目,更新的待运行条目会顶掉更早的,繁忙时段中间的推送运行仍会以 `cancelled` 结束。其二,该表达式是针对**新触发的运行**求值的,因此自身事件不是 `push` 的运行——例如在 `ci-master.yml` 内的 master 上派发的基准测试,与其演练共用 `CI master-` 组——求值为 `true`,会取消正在运行中的演练。这属于罕见的手动操作,且下一次 master 推送即可恢复证据,因此不值得为它再加机制。这项豁免换来的是该通道**周期性**地得出结论,而这正是它能作为证据的前提。 - -这个决定必须放在工作流级:取消作用于被取代的整个运行,作业级 `concurrency` 组并不能豁免其所属作业。采用否定式写法而非仅指名 `pull_request`,是有实质作用的:后者会连 `workflow_dispatch` 一起停止取消,而每次运行器基准测试会在 master 上的同一并发组内同时占用 12 台大规格运行器、最长 15 分钟,届时重复派发会排在演练之前,而不是替换掉已过时的测量。成本之所以可控,是因为 `ci-master.yml` 中一次 master 推送承载[合并后的运行时与 Wine 检查](2026-09-06-master-only-platform-ci.zh.md)和这两条演练;拉取请求作业位于独立的 `ci.yml`(不监听 `push`),而基准测试在 `ci-master.yml` 内受 `workflow_dispatch` 门控。`scripts/ci-workflow.spec.ts` 会锁定这个推送可达集合——按条件精确匹配,因为否定式事件判断会包含它所排除的事件名——使新的推送可达作业无法悄悄开始累积未取消的运行。 +[被取代 CI 的取消策略](2026-09-09-cancel-superseded-ci.zh.md) 管理同一工作流/引用组内的 master 推送和手动运行,包括热备演练。master 快速更新可能让演练因反复被取消而始终无法得出结论。判断就绪状态时,使用最近一次已完成的热备结论,并核对其时间和提交;已取消或仅被调度的运行不构成就绪证据。 ### 发布演练共用 Linux 开关 @@ -44,7 +40,7 @@ Status: implemented ## 切换期间的容量 -Linux 开关启用期间,容量需覆盖 master 热备、主 CI 作业,以及每个符合条件的 PR 或 master 推送的三个发布演练作业。每个可信 PR 还会增加三个门禁并发度为一的 Node 兼容性作业,包括需要构建的 Node 22 条目和冷临时运行时下载。发布工作流不会因为新运行到来而取消正在执行的演练,因此不同引用的重叠运行会增加持续的构建、打包和安装负载。延长自托管运行前,检查当前 CPU、内存、磁盘和队列压力;同一虚拟机上新增注册只增加调度槽位,不增加机器资源。不能只依据热备负载推断空闲容量。主机资源允许增加注册实例时,使用组织级注册 token(组织 Settings → Actions → Runners → New runner)。复制现有 runner 目录时**必须排除身份文件**——`rsync -a --exclude '.runner*' --exclude '.credentials*' --exclude '_diag' --exclude '_work' / /`(通配同时排除 `.runner_migrated`/`.credentials_migrated`——GitHub 会在迁移过的运行器上写入这些文件,它们同样会触发 already-configured 拒绝)——再跑 `config.sh`(原样拷贝 `.runner`/`.credentials` 会使其以 "already configured" 拒绝),然后**启动监听器**:`sudo ./svc.sh install ubuntu && sudo ./svc.sh start`。仅注册不会上线;启动服务增加的是调度槽位,而非 CPU 或内存。 +Linux 开关启用期间,容量需覆盖 master 热备、主 CI 作业,以及每个符合条件的 PR 或 master 推送的三个发布演练作业。每个可信 PR 还会增加三个门禁并发度为一的 Node 兼容性作业,包括需要构建的 Node 22 条目和冷临时运行时下载。发布演练工作流依据[取消策略](2026-09-09-cancel-superseded-ci.zh.md)取消各工作流/引用组内被取代的运行;不同引用仍可能增加并发构建、打包和安装负载。延长自托管运行前,检查当前 CPU、内存、磁盘和队列压力;同一虚拟机上新增注册只增加调度槽位,不增加机器资源。不能只依据热备负载推断空闲容量。主机资源允许增加注册实例时,使用组织级注册 token(组织 Settings → Actions → Runners → New runner)。复制现有 runner 目录时**必须排除身份文件**——`rsync -a --exclude '.runner*' --exclude '.credentials*' --exclude '_diag' --exclude '_work' / /`(通配同时排除 `.runner_migrated`/`.credentials_migrated`——GitHub 会在迁移过的运行器上写入这些文件,它们同样会触发 already-configured 拒绝)——再跑 `config.sh`(原样拷贝 `.runner`/`.credentials` 会使其以 "already configured" 拒绝),然后**启动监听器**:`sudo ./svc.sh install ubuntu && sudo ./svc.sh start`。仅注册不会上线;启动服务增加的是调度槽位,而非 CPU 或内存。 ### 切回 @@ -63,4 +59,4 @@ Linux 开关启用期间,容量需覆盖 master 热备、主 CI 作业,以 ## 后果 -从托管池故障中恢复只需切换受影响平台的变量(任何写者可设)加一次重跑,关键路径上没有合并。代价是每个平台都要维护第二套运行器拓扑:热备通道在每次 master 推送时都运行它们,避免故障切换目标变得陈旧;而 `ci.yml` 中的快照并发与缓存恢复分支带有一条 `selfhosted` 支路(仅 Linux),必须与托管支路保持同步。按平台拆分开关多了一个需要管理的变量,但把每个开关的影响范围限定在单个平台的作业上。 +从托管池故障中恢复只需切换受影响平台的变量(任何写者可设)加一次重跑,关键路径上没有合并。代价是每个平台都要维护第二套运行器拓扑:master 推送会调度热备通道,但依据[取消策略](2026-09-09-cancel-superseded-ci.zh.md),只有已完成的结论才能证明就绪状态;而 `ci.yml` 中的快照并发与缓存恢复分支带有一条 `selfhosted` 支路(仅 Linux),必须与托管支路保持同步。按平台拆分开关多了一个需要管理的变量,但把每个开关的影响范围限定在单个平台的作业上。 diff --git a/.agents/notes/implemented/process/2026-09-06-master-only-platform-ci.i18n.yaml b/.agents/notes/implemented/process/2026-09-06-master-only-platform-ci.i18n.yaml index 1ce78b6e0f..6006f0447a 100644 --- a/.agents/notes/implemented/process/2026-09-06-master-only-platform-ci.i18n.yaml +++ b/.agents/notes/implemented/process/2026-09-06-master-only-platform-ci.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-09-06-master-only-platform-ci.md -2026-09-06-master-only-platform-ci.md: 28284206c8c6d3fbb5de8ecadbcdf2035a5bb8c0 -2026-09-06-master-only-platform-ci.zh.md: eed843b0d235c80256343890e91b1de84f482174 +2026-09-06-master-only-platform-ci.md: d32864a4eafa81433c6d4fd0e47892b4d17091b1 +2026-09-06-master-only-platform-ci.zh.md: 4fb55d5e95f8fe75e6d1efb8b57295b8e8469e8d diff --git a/.agents/notes/implemented/process/2026-09-06-master-only-platform-ci.md b/.agents/notes/implemented/process/2026-09-06-master-only-platform-ci.md index 28284206c8..d32864a4ea 100644 --- a/.agents/notes/implemented/process/2026-09-06-master-only-platform-ci.md +++ b/.agents/notes/implemented/process/2026-09-06-master-only-platform-ci.md @@ -14,7 +14,7 @@ Python runtime builds on macOS Intel and ARM and Linux ARM64, plus Windows build Wine runs once as an independent hosted Ubuntu master job. Its existing image-keyed apt cache restore/save also supplies default-branch cache production, so it needs no separate cache-seeding job. The native Linux and Windows serial aggregates do not invoke Wine. Keeping Wine hosted avoids shared-host apt transactions and shared Wine-prefix cleanup on the persistent Linux VM. The script owns a scratch snapshot, a checkout-local Wine prefix, and a checksum-verified Windows Node cache; provisioning, failure propagation, and always-run cleanup remain intact. -The parent and reusable runtime workflows preserve running master-push checks against subsequent master pushes. GitHub concurrency still permits replacement of pending runs; manual benchmarks can cancel the parent run. A master push schedules all three selected carriers but does not guarantee every intermediate commit reaches a result. PR, manual, and release cancellation retain their existing behavior. +The [superseded-CI cancellation policy](2026-09-09-cancel-superseded-ci.md) applies to the parent and reusable runtime workflows: newer master pushes or manual runs cancel older validation in the same workflow/ref group, while release-owned builds remain protected. A master push schedules all three selected carriers but does not guarantee every intermediate commit reaches a result. This decision partially supersedes scheduling in the [installed-wheel validation](../testing/2026-08-23-installed-python-wheel-black-box-ci.md), [native Windows CI](2026-08-08-native-windows-pull-request-ci.md), [serial references](2026-07-21-serial-cross-platform-ci-reference.md), and [failover runbook](2026-07-26-ci-failover-runbook.md). Those notes remain active for artifact provenance, platform fidelity, serial completeness, and trust rules. diff --git a/.agents/notes/implemented/process/2026-09-06-master-only-platform-ci.zh.md b/.agents/notes/implemented/process/2026-09-06-master-only-platform-ci.zh.md index eed843b0d2..4fb55d5e95 100644 --- a/.agents/notes/implemented/process/2026-09-06-master-only-platform-ci.zh.md +++ b/.agents/notes/implemented/process/2026-09-06-master-only-platform-ci.zh.md @@ -14,7 +14,7 @@ macOS Intel、ARM 与 Linux ARM64 上的 Python 运行时构建,以及通过 W Wine 作为独立的托管 Ubuntu master 作业运行一次。其现有的按镜像标识的 apt 缓存恢复和保存也负责生成默认分支缓存,因此不需要单独的缓存预热作业。原生 Linux 与 Windows 串行聚合不调用 Wine。Wine 保持托管运行,避免在持久 Linux VM 上执行共享宿主机 apt 事务和共享 Wine prefix 清理。脚本负责临时快照、checkout 内的 Wine prefix 和经过校验和验证的 Windows Node 缓存;环境准备、失败传播及始终执行的清理保持不变。 -父工作流与可复用运行时工作流均保留正在执行的 master 推送检查,不被后续 master 推送取消。GitHub 并发机制仍允许替换待执行的运行;手动基准测试可以取消父工作流。master 推送会调度全部三个选定载体,但不保证每个中间提交都得到结果。PR(Pull Request)、手动和发布运行的取消行为保持不变。 +[被取代 CI 的取消策略](2026-09-09-cancel-superseded-ci.zh.md) 适用于父工作流与可复用运行时工作流:更新的 master 推送或手动运行会取消同一工作流/引用组内的旧验证,而发布所属的构建仍受保护。master 推送会调度全部三个选定载体,但不保证每个中间提交都得到结果。 本决策部分取代[安装后 wheel 包验证](../testing/2026-08-23-installed-python-wheel-black-box-ci.zh.md)、[原生 Windows CI](2026-08-08-native-windows-pull-request-ci.zh.md)、[串行参考](2026-07-21-serial-cross-platform-ci-reference.zh.md)和[故障切换手册](2026-07-26-ci-failover-runbook.zh.md)中的调度策略。这些记录仍保留产物来源、平台保真度、串行完整性与信任规则的决策价值。 diff --git a/.agents/notes/implemented/process/2026-09-09-cancel-superseded-ci.i18n.yaml b/.agents/notes/implemented/process/2026-09-09-cancel-superseded-ci.i18n.yaml new file mode 100644 index 0000000000..5ef13299ed --- /dev/null +++ b/.agents/notes/implemented/process/2026-09-09-cancel-superseded-ci.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-09-09-cancel-superseded-ci.md +2026-09-09-cancel-superseded-ci.md: 8de1f757870a2b18852a224ec2f1cc33d9f6a95e +2026-09-09-cancel-superseded-ci.zh.md: 3ae6ec6ad58b0dfc94e803a33ba92ded4e239485 diff --git a/.agents/notes/implemented/process/2026-09-09-cancel-superseded-ci.md b/.agents/notes/implemented/process/2026-09-09-cancel-superseded-ci.md new file mode 100644 index 0000000000..8de1f75787 --- /dev/null +++ b/.agents/notes/implemented/process/2026-09-09-cancel-superseded-ci.md @@ -0,0 +1,39 @@ +# Agent Note: Cancel superseded CI validation + +Status: implemented + +English | [中文](2026-09-09-cancel-superseded-ci.zh.md) + +## Problem + +Validation of an obsolete PR revision or master commit consumes runner capacity without establishing the newest revision’s status. Unconditional aggregate verdicts and coverage-history uploads can also keep cancelled runs doing bookkeeping. Preserving older post-merge runs favors historical completion over current validation, especially on the shared self-hosted pools. + +## Decision + +Validation favors the newest run within each workflow/ref group. [CI](../../../../.github/workflows/ci.yml), [CI master](../../../../.github/workflows/ci-master.yml), [real-API e2e](../../../../.github/workflows/e2e.yml), and the credential-free [dsh](../../../../.github/workflows/release.yml) and [vendor](../../../../.github/workflows/release-vendor.yml) pack validations use `cancel-in-progress: true` with `${{ github.workflow }}-${{ github.ref }}`. Different PR refs and different workflows do not cancel each other. Event type is not part of the group: master pushes and manual benchmarks can supersede each other in CI master, and e2e pushes, scheduled runs, and manual runs can supersede each other on the same ref. + +The [reusable Python runtime builder](../../../../.github/workflows/build-exe-for-python-sdk.yml) uses `${{ !inputs.release }}`. Its `build-single-exe-${{ github.workflow }}-${{ github.ref }}` group remains distinct from its caller’s group, and the caller workflow name isolates ordinary CI from release-owned builds. Release-owned builds are exempt because they belong to an intentional publication transaction. Publication, deployment, and metadata workflows retain their own policies; this decision does not apply cancellation indiscriminately across workflows. + +The PR aggregate uses `${{ !cancelled() && github.event_name == 'pull_request' }}`. The explicit status function preserves evaluation after failed or skipped dependencies rather than accepting GitHub’s default success-only condition. The aggregate still fails on any failure, cancellation, or skip among its dependencies when the workflow itself is not cancelled; cancellation of the whole workflow suppresses its obsolete verdict. Coverage duration history uses `!cancelled()` too: failed coverage can still save useful measurements, but cancelled coverage does not upload them. Wine’s `always()` cleanup remains necessary resource cleanup rather than optional bookkeeping. + +This reverses the cancellation exemption in the [failover runbook](2026-07-26-ci-failover-runbook.md), [master-only platform CI](2026-09-06-master-only-platform-ci.md), and [real-API e2e decision](../testing/2026-06-19-real-api-e2e-ci.md). Those notes retain independent value for pool trust and switching, platform coverage, and secret exposure. The [release rehearsal decision](2026-09-06-release-rehearsal-selfhosted.md) retains runner selection and isolation ownership. None is fully superseded or archived. + +## Alternatives considered + +**Preserve running master-push drills.** The former `${{ github.event_name != 'push' }}` exemption favored periodic readiness evidence: each standby executes its complete unsharded aggregate with one gate worker and can outlast the interval between master merges. Even that policy did not guarantee every drill completed. GitHub retains one pending run per group, replacing intermediate pending pushes; cancellation is evaluated on the newly triggered run, so a manual benchmark sharing the master group could still cancel a drill. That rare manual interruption was accepted on the expectation of evidence from a subsequent push. The exemption’s cost was bounded by the master-only runtime checks, Wine, and two drills; PR jobs remained in a separate workflow, and exact-condition regression checks pinned the push-reachable job set. This policy is rejected in favor of freeing capacity for current validation, explicitly accepting standby starvation. + +**Protect a drill with job-level concurrency, or cancel only PR events.** A job-level group cannot exempt a job from cancellation of its entire workflow. A PR-only cancellation condition also exempts manual dispatch: a repeated runner benchmark can occupy twelve larger runners for up to fifteen minutes rather than replacing an obsolete measurement. Workflow-level cancellation covers both pushes and manual runs. + +**Keep every post-merge, nightly, and pack run.** Historical completion provides more per-commit and per-trigger evidence, but obsolete validation competes with the newest run. These validations do not publish packages, so preserving every run is not the same requirement as protecting an intentional publication transaction. + +**Replace every `always()` condition.** Failure aggregation and resource cleanup have different obligations. A success-only aggregate can hide failed dependencies behind a skipped required check; removing unconditional Wine cleanup can leave resources running. Only cancelled-run bookkeeping is suppressed. + +## Consequences + +Rapid master updates can repeatedly cancel the longer standby drills before they produce a verdict. Operators use the latest completed standby verdict, checking its age and commit before relying on it for failover readiness; a scheduled, running, or cancelled drill is not readiness evidence. The policy does not guarantee that every intermediate commit, nightly trigger, or benchmark completes. Different refs can still compete for shared host capacity. + +Cancellation is a request handled by GitHub Actions and its runners, not a guarantee of immediate termination or bounded queue delay. Cleanup can still take time. The policy makes obsolete validation cancellable; it does not promise a fixed runtime or cancellation latency. + +## Verification + +[Workflow regressions](../../../../scripts/ci-workflow.spec.ts) pin workflow/ref isolation, release-owned exemptions, aggregate status conditions, coverage-history cancellation, and retained Wine cleanup. [Platform routing regressions](../../../../scripts/tests/ci-master-platforms.spec.ts) preserve the master/PR target split and release matrix; [release rehearsal regressions](../../../../scripts/tests/ci-release-selfhosted.spec.ts) preserve cancellation alongside runner eligibility and publication isolation. These configuration checks do not reproduce GitHub scheduling or runner shutdown. Live supersession and completed standby evidence remain CI verification responsibilities. diff --git a/.agents/notes/implemented/process/2026-09-09-cancel-superseded-ci.zh.md b/.agents/notes/implemented/process/2026-09-09-cancel-superseded-ci.zh.md new file mode 100644 index 0000000000..3ae6ec6ad5 --- /dev/null +++ b/.agents/notes/implemented/process/2026-09-09-cancel-superseded-ci.zh.md @@ -0,0 +1,39 @@ +# Agent Note: 取消被取代的 CI 验证 + +Status: implemented + +[English](2026-09-09-cancel-superseded-ci.md) | 中文 + +## 问题 + +验证已被取代的 PR(Pull Request)修订或 master 提交会消耗运行器容量,却不能确定最新修订的状态。无条件执行的聚合判定和覆盖率耗时历史上传,还可能让已取消的运行继续处理记账任务。保留旧的合并后运行,意味着优先完成历史验证而非当前验证,在共享自托管池上尤其如此。 + +## 决策 + +验证优先保留各工作流/引用组内最新的运行。[CI](../../../../.github/workflows/ci.yml)、[CI master](../../../../.github/workflows/ci-master.yml)、[真实 API e2e](../../../../.github/workflows/e2e.yml),以及无凭据的 [dsh](../../../../.github/workflows/release.yml) 和 [vendor](../../../../.github/workflows/release-vendor.yml) 打包验证,均在 `${{ github.workflow }}-${{ github.ref }}` 组中使用 `cancel-in-progress: true`。不同 PR 引用和不同工作流不会相互取消。事件类型不参与分组:CI master 中的 master 推送与手动基准测试可以相互取代,e2e 的推送、定时运行和手动运行也可以在同一引用上相互取代。 + +[可复用 Python 运行时构建器](../../../../.github/workflows/build-exe-for-python-sdk.yml)使用 `${{ !inputs.release }}`。其 `build-single-exe-${{ github.workflow }}-${{ github.ref }}` 组与调用方的组保持区分,调用方工作流名称将普通 CI 与发布所属的构建隔离。发布所属的构建获得豁免,因为它们属于一次有意发起的发布事务。发布、部署和元数据工作流保留各自的策略;本决策不会不加区分地对所有工作流应用取消。 + +PR 聚合使用 `${{ !cancelled() && github.event_name == 'pull_request' }}`。显式状态函数使其在依赖失败或跳过后仍然求值,而非采用 GitHub 默认的仅成功条件。当工作流本身未被取消时,聚合仍会因任意依赖失败、取消或跳过而失败;整个工作流被取消时则抑制其已失去用途的判定。覆盖率耗时历史也使用 `!cancelled()`:覆盖率失败时仍可保存有用的测量数据,但被取消的覆盖率运行不上传。Wine 的 `always()` 清理仍是必要的资源清理,而非可选记账任务。 + +本决策推翻[故障切换手册](2026-07-26-ci-failover-runbook.zh.md)、[仅 master 执行的平台 CI](2026-09-06-master-only-platform-ci.zh.md) 和[真实 API e2e 决策](../testing/2026-06-19-real-api-e2e-ci.zh.md)中的取消豁免。这些记录对运行器池信任与切换、平台覆盖和密钥暴露仍有独立价值。[发布演练决策](2026-09-06-release-rehearsal-selfhosted.zh.md)仍负责运行器选择与隔离。没有记录被完全取代或归档。 + +## 曾考虑的替代方案 + +**保留正在执行的 master 推送演练。** 原有 `${{ github.event_name != 'push' }}` 豁免优先保证周期性就绪证据:每条热备以单门禁工作进程执行完整的未分片聚合流程,耗时可能长于 master 合并间隔。即便该策略也不保证每次演练完成。GitHub 每个组仅保留一个待运行条目,会替换中间的待执行推送;取消条件针对新触发的运行求值,因此共享 master 组的手动基准测试仍可取消演练。当时接受了这种罕见的手动中断,期望后续推送提供证据。豁免成本被限定为仅 master 执行的运行时检查、Wine 和两条演练;PR 作业仍在独立工作流中,按精确条件匹配的回归检查固定推送可达作业集合。为释放容量给当前验证而否决该策略,明确接受热备因反复被取消而无法完成。 + +**用作业级并发保护演练,或仅取消 PR 事件。** 作业级分组不能让作业免于整个工作流的取消。仅针对 PR 的取消条件还会豁免手动触发:重复派发的运行器基准测试可能占用十二台大型运行器长达十五分钟,而非替换陈旧的测量。工作流级取消同时覆盖推送和手动运行。 + +**保留每次合并后、每夜和打包运行。** 完成历史运行能提供更多按提交和触发划分的证据,但已被取代的验证会与最新运行竞争。这些验证不发布包,因此保留每次运行与保护有意发起的发布事务并不是同一项要求。 + +**替换每个 `always()` 条件。** 失败聚合与资源清理承担不同义务。仅成功时执行的聚合可能把依赖失败隐藏为跳过的必需检查;移除无条件 Wine 清理则可能留下仍在运行的资源。只有已取消运行的记账任务被抑制。 + +## 后果 + +master 快速更新可能反复取消耗时更长的热备演练,使其无法产出结论。运维人员使用最近一次已完成的热备结论,并在依赖它判断故障切换就绪状态前核对其时间和提交;已调度、正在执行或已取消的演练都不构成就绪证据。该策略不保证每个中间提交、每夜触发或基准测试都能完成。不同引用仍会竞争共享主机容量。 + +取消是由 GitHub Actions 及其运行器处理的请求,不保证立即终止或限定排队时长。清理仍可能耗时。该策略让已被取代的验证可以被取消,但不承诺固定运行时长或取消延迟。 + +## 验证 + +[工作流回归测试](../../../../scripts/ci-workflow.spec.ts)固定工作流/引用隔离、发布所属构建豁免、聚合状态条件、覆盖率历史取消及保留的 Wine 清理。[平台路由回归测试](../../../../scripts/tests/ci-master-platforms.spec.ts)保留 master/PR 目标划分与发布矩阵;[发布演练回归测试](../../../../scripts/tests/ci-release-selfhosted.spec.ts)在验证取消策略的同时保留运行器准入与发布隔离。这些配置检查不重现 GitHub 调度或运行器停止过程。真实运行取代行为与已完成热备证据仍由 CI 负责验证。 diff --git a/.agents/notes/implemented/testing/2026-06-19-real-api-e2e-ci.i18n.yaml b/.agents/notes/implemented/testing/2026-06-19-real-api-e2e-ci.i18n.yaml index 676b7c29a9..e50f4af442 100644 --- a/.agents/notes/implemented/testing/2026-06-19-real-api-e2e-ci.i18n.yaml +++ b/.agents/notes/implemented/testing/2026-06-19-real-api-e2e-ci.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/testing/2026-06-19-real-api-e2e-ci.md -2026-06-19-real-api-e2e-ci.md: 4f51032d90b6773d01db7bbfdced6328bd882ad0 -2026-06-19-real-api-e2e-ci.zh.md: 390df23aebec3a6a54bc48eb52022fe02e872164 +2026-06-19-real-api-e2e-ci.md: b218d77f0817416b01459fbb70f5cacffe048ee8 +2026-06-19-real-api-e2e-ci.zh.md: 52c988dad00ddb3ee7f0cd2d23da9771f0900808 diff --git a/.agents/notes/implemented/testing/2026-06-19-real-api-e2e-ci.md b/.agents/notes/implemented/testing/2026-06-19-real-api-e2e-ci.md index 4f51032d90..b218d77f08 100644 --- a/.agents/notes/implemented/testing/2026-06-19-real-api-e2e-ci.md +++ b/.agents/notes/implemented/testing/2026-06-19-real-api-e2e-ci.md @@ -54,7 +54,7 @@ The repo secret is named `DEEPSEEK_API_KEY_EXTERNAL`; it is mapped to the `DEEPS ### Scope, runtime shape -The job runs only `test:e2e` on Node 24; keyless gates and version compatibility belong to the main CI workflow. Tests run unbuilt through the workspace paths map with a bounded configurable worker pool, per-test retries, and a job timeout. Superseded PR runs are cancelled, while push and scheduled runs complete for post-merge signal. +The job runs only `test:e2e` on Node 24; keyless gates and version compatibility belong to the main CI workflow. Tests run unbuilt through the workspace paths map with a bounded configurable worker pool, per-test retries, and a job timeout. The [superseded-CI cancellation policy](../process/2026-09-09-cancel-superseded-ci.md) cancels older runs in the same workflow/ref group across PR, push, schedule, and manual triggers; a post-merge or nightly trigger does not guarantee completion. The DeepSeek native `web_search` probe is registered but skipped. The live Anthropic-compatible endpoint can return a successful response without structured source blocks, so its positive-source assertion is not a reliable merge signal; unit coverage still pins response parsing, but CI does not prove the live source-block wire shape. diff --git a/.agents/notes/implemented/testing/2026-06-19-real-api-e2e-ci.zh.md b/.agents/notes/implemented/testing/2026-06-19-real-api-e2e-ci.zh.md index 390df23aeb..52c988dad0 100644 --- a/.agents/notes/implemented/testing/2026-06-19-real-api-e2e-ci.zh.md +++ b/.agents/notes/implemented/testing/2026-06-19-real-api-e2e-ci.zh.md @@ -54,7 +54,7 @@ repo secret 命名为 `DEEPSEEK_API_KEY_EXTERNAL`;映射到适配器和测试 ### 范围与运行时形态 -job 仅在 Node 24 上运行 `test:e2e`;无密钥门禁和版本兼容性属于主 CI 工作流。测试通过 workspace paths 映射以未构建形式运行,使用有界的可配置 worker 池、逐测试重试和 job 超时。被取代的 PR 运行会被取消,而 push 和 schedule 运行完整执行以提供合并后信号。 +job 仅在 Node 24 上运行 `test:e2e`;无密钥门禁和版本兼容性属于主 CI 工作流。测试通过 workspace paths 映射以未构建形式运行,使用有界的可配置 worker 池、逐测试重试和 job 超时。[被取代 CI 的取消策略](../process/2026-09-09-cancel-superseded-ci.zh.md)会在 PR、push、schedule 和手动触发之间取消同一工作流/引用组内的旧运行;合并后或每夜触发并不保证完成。 DeepSeek 原生 `web_search` 探测已注册但会跳过。线上 Anthropic 兼容端点可能返回成功响应却没有结构化来源块,因此对来源存在性的正向断言不是可靠的合并信号;单元测试仍会锁定响应解析行为,但 CI 不会验证线上端点返回的来源块协议格式(wire format)。 diff --git a/.github/workflows/build-exe-for-python-sdk.yml b/.github/workflows/build-exe-for-python-sdk.yml index ae7dd61554..49866ba5ea 100644 --- a/.github/workflows/build-exe-for-python-sdk.yml +++ b/.github/workflows/build-exe-for-python-sdk.yml @@ -46,7 +46,8 @@ concurrency: # github.workflow identifies the caller inside a reusable workflow and keeps # an ordinary CI run from cancelling a full release validation on the same ref. group: build-single-exe-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: ${{ github.event_name != 'push' || github.ref != 'refs/heads/master' }} + # Release-owned builds are part of an intentional publication transaction. + cancel-in-progress: ${{ !inputs.release }} permissions: contents: read diff --git a/.github/workflows/ci-master.yml b/.github/workflows/ci-master.yml index df9f7f2bcc..dd861115d4 100644 --- a/.github/workflows/ci-master.yml +++ b/.github/workflows/ci-master.yml @@ -14,14 +14,11 @@ on: - larger-runner-benchmark - consolidated-runner-benchmark -# Master runs platform runtime checks, Wine, and two self-hosted standby drills. -# The drills outlast the interval between master merges, so -# push is exempt from cancellation (see ci-failover-runbook). workflow_dispatch -# keeps cancelling: a re-dispatched runner benchmark holds up to 12 larger -# runners for 15 minutes in this same group. +# New master pushes and manual runs replace obsolete checks in this workflow/ref. +# Standby drills share cancellation; use completed runs as readiness evidence. concurrency: group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: ${{ github.event_name != 'push' }} + cancel-in-progress: true permissions: contents: read diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4e05b444a4..e7cff1454e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -562,7 +562,7 @@ jobs: # Coverage flakes must not prevent the cache from building; the # measured durations remain useful even when a partition failed. The # per-run key keeps every save a fresh immutable cache entry. - if: always() + if: ${{ !cancelled() }} uses: actions/cache/save@v4 with: path: .coverage-times.json @@ -666,10 +666,8 @@ jobs: # `needs`. Native Windows build and process checks are required; Wine and # the deferred Python runtime targets live in ci-master.yml and do not # participate in this PR verdict. `needs` cannot cross workflow files. - # `if: always()` is load-bearing: without it a failed dependency - # would SKIP this job, and GitHub counts a skipped required check as passing - # — so this job always runs and fails on any non-success result, including - # 'cancelled' and 'skipped'. + # An explicit status function runs the verdict after failed/skipped needs + # without keeping a cancelled workflow alive for an obsolete verdict. all-checks-passed: name: all checks passed # This bookkeeping-only verdict must not depend on custom-pool @@ -684,7 +682,7 @@ jobs: && fromJSON('["self-hosted", "linux", "x64", "vm-backup"]') || 'ubuntu-latest' }} needs: [node-24, node-24-coverage, node-24-bench, node-24-consumers, node-compat, python-sdk, python-runtime, windows-build, windows-native-tests] - if: always() && github.event_name == 'pull_request' + if: ${{ !cancelled() && github.event_name == 'pull_request' }} steps: - name: Fail if any needed job did not succeed if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') || contains(needs.*.result, 'skipped') diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 099908616c..3b7ff878f8 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -36,11 +36,11 @@ on: # 00:17 UTC nightly = 08:17 Asia/Shanghai — off the top-of-hour cron stampede. - cron: '17 0 * * *' -# Cancel a superseded PR run (it is on a stale commit); never cancel a -# push/schedule run — it is already producing the post-merge/nightly signal. +# Keep only the newest validation in each workflow/ref, including master +# pushes, nightly runs, and manual dispatches. concurrency: group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} + cancel-in-progress: true # Least privilege: this job only reads the repo to run tests. permissions: diff --git a/.github/workflows/release-vendor.yml b/.github/workflows/release-vendor.yml index ddf3c3f96c..75fb865767 100644 --- a/.github/workflows/release-vendor.yml +++ b/.github/workflows/release-vendor.yml @@ -20,7 +20,7 @@ permissions: concurrency: # Pack runs per ref so concurrent pull requests never displace each other. group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: false + cancel-in-progress: true env: PRIMARY_NODE_VERSION: '24' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1415d1f9e9..633186c8f1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ permissions: concurrency: # Pack runs per ref so concurrent pull requests never displace each other. group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: false + cancel-in-progress: true env: PRIMARY_NODE_VERSION: '24' diff --git a/scripts/ci-workflow.spec.ts b/scripts/ci-workflow.spec.ts index 46177da403..c0aba73898 100644 --- a/scripts/ci-workflow.spec.ts +++ b/scripts/ci-workflow.spec.ts @@ -8,6 +8,43 @@ const runnerPrivatePnpmDestination = /^\$\{\{ runner\.temp \}\}\/setup-pnpm-\$\{ const nativeWindowsPnpmDestination = '${{ runner.temp }}/setup-pnpm-js-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}' describe('CI workflow', () => { + it.each(['ci.yml', 'ci-master.yml', 'e2e.yml', 'release.yml', 'release-vendor.yml'])( + '%s cancels superseded validation runs without crossing workflow or ref boundaries', (name) => { + const workflow = loadWorkflow('.github/workflows/' + name) + expect(workflow.concurrency).toEqual({ + group: '${{ github.workflow }}-${{ github.ref }}', + 'cancel-in-progress': true, + }) + }, + ) + + it('cancels reusable CI builds without cancelling release-owned builds', () => { + const workflow = loadWorkflow('.github/workflows/build-exe-for-python-sdk.yml') + expect(workflow.concurrency).toEqual({ + group: 'build-single-exe-${{ github.workflow }}-${{ github.ref }}', + 'cancel-in-progress': '${{ !inputs.release }}', + }) + }) + + it('does not cancel protected publication or deployment transactions', () => { + for (const name of ['release-publish.yml', 'release-vendor-publish.yml']) { + const publish = workflowJob(loadWorkflow('.github/workflows/' + name), 'publish') + expect(publish.concurrency).toMatchObject({ 'cancel-in-progress': false }) + } + for (const name of ['python-release.yml', 'node-addon-system-release.yml', 'docs-pages.yml']) { + expect(loadWorkflow('.github/workflows/' + name).concurrency).toMatchObject({ 'cancel-in-progress': false }) + } + }) + + it('skips coverage-history uploads on cancellation but retains Wine cleanup', () => { + const coverage = workflowJob(loadWorkflow('.github/workflows/ci.yml'), 'windows-coverage') + const wine = workflowJob(loadWorkflow('.github/workflows/ci-master.yml'), 'windows') + expect(coverage.steps).toContainEqual(expect.objectContaining({ + name: 'Save coverage duration history', if: '${{ !cancelled() }}', + })) + expect(wine.steps).toContainEqual(expect.objectContaining({ name: 'Shut down wineserver', if: 'always()' })) + }) + it('isolates every pnpm action setup destination per runner', () => { const files = ['.github/workflows/ci.yml', '.github/workflows/ci-master.yml'] const setups: Array<{ jobName: string; step: unknown }> = [] @@ -346,7 +383,7 @@ describe('CI workflow', () => { ) }) - it('exempts push from cancellation in ci-master, so one master merge does not cancel the running drill', () => { + it('cancels superseded master runs without changing the post-merge job inventory', () => { const workflow = loadWorkflow('.github/workflows/ci-master.yml') const prWorkflow = loadWorkflow('.github/workflows/ci.yml') if (!isRecord(workflow.jobs) || !isRecord(workflow.concurrency)) { @@ -356,21 +393,11 @@ describe('CI workflow', () => { throw new TypeError('ci workflow must define jobs') } - // Cancellation applies to the whole superseded RUN, so this has to be - // decided at workflow level and gated on the event: a job-level group - // cannot exempt its job from its run being cancelled. Only push is exempt — - // a drill takes longer than the interval between master merges. The negated - // form is load-bearing: `== 'pull_request'` would also stop cancelling - // workflow_dispatch, and a re-dispatched runner benchmark holds up to 12 - // larger runners for 15 minutes in this same group on master. - expect(workflow.concurrency['cancel-in-progress']).toBe("${{ github.event_name != 'push' }}") - - // The PR-only ci.yml still cancels a superseded run on a new push, so a - // fresh head does not stack a second full 9-job run behind a stale one. - // Unlike ci-master it has no push carve-out: every PR event supersedes. - expect(prWorkflow.concurrency).toMatchObject({ + expect(workflow.concurrency).toEqual({ + group: '${{ github.workflow }}-${{ github.ref }}', 'cancel-in-progress': true, }) + expect(prWorkflow.concurrency).toEqual(workflow.concurrency) // The exact event sets are what keep master-only jobs out of the PR check // panel: ci-master triggers only on push(master) + workflow_dispatch and @@ -382,13 +409,12 @@ describe('CI workflow', () => { expect(Object.keys(workflow.on).sort()).toEqual(['push', 'workflow_dispatch']) expect(Object.keys(prWorkflow.on)).toEqual(['pull_request']) - // Neither drill may carry a job-level group: it would not exempt the job - // from run-scoped cancellation. + // Drills share the parent run’s supersession policy. for (const name of ['serial-linux-selfhosted', 'serial-windows']) { const job = workflow.jobs[name] if (!isRecord(job)) throw new TypeError(`${name} must be defined`) expect(job.concurrency).toBeUndefined() - // Both stay master-push-only; that is what makes the push carve-out safe. + // Standby drills remain post-merge work, but share run cancellation. expect(job.if).toBe("github.event_name == 'push' && github.ref == 'refs/heads/master'") } @@ -409,10 +435,7 @@ describe('CI workflow', () => { .sort() expect(pushReachable).toEqual(['python-runtime', 'serial-linux-selfhosted', 'serial-windows', 'windows']) - // Why workflow_dispatch must keep cancelling: each benchmark fans out to a - // dozen larger runners at once, in this same group on master. If it stopped - // cancelling, a re-dispatch would queue ahead of a drill instead of - // replacing the stale measurement. + // Manual benchmarks retain their bounded fan-out. for (const name of ['larger-runner-benchmark', 'consolidated-runner-benchmark']) { const job = workflow.jobs[name] if (!isRecord(job) || !isRecord(job.strategy)) { diff --git a/scripts/tests/ci-master-platforms.spec.ts b/scripts/tests/ci-master-platforms.spec.ts index b07f71cd62..3cdbb00f9b 100644 --- a/scripts/tests/ci-master-platforms.spec.ts +++ b/scripts/tests/ci-master-platforms.spec.ts @@ -1,6 +1,7 @@ /** Scheduling policy for post-merge native runtime carriers and Wine. */ import { readFileSync } from 'node:fs' import { resolve } from 'node:path' +import { runInNewContext } from 'node:vm' import { load } from 'js-yaml' import { describe, expect, it } from 'vitest' import { gatesForMode } from '../run-gates.ts' @@ -34,7 +35,43 @@ function commands(job: Job): string[] { return (job.steps ?? []).flatMap(step => step.run ? [step.run] : []) } +// These boolean/string cases share Actions and JavaScript semantics. GitHub +// supplies status functions; this probe is not a general Actions interpreter. +function evaluateCondition(expression: string, cancelled: boolean, results: string[], event = 'pull_request'): boolean { + const source = expression.trim().replace(/^[$][{][{]|[}][}]$/g, '') + .replaceAll('needs.*.result', 'results') + return runInNewContext(source, { + cancelled: () => cancelled, + always: () => true, + contains: (values: string[], value: string) => values.includes(value), + results, + github: { event_name: event }, + }, { timeout: 1000 }) as boolean +} + describe('master-only platform scheduling', () => { + it.each(['success', 'failure', 'skipped', 'cancelled'])( + 'reports %s dependencies in active runs but never starts a cancelled-run verdict', (result) => { + const aggregate = workflow('ci.yml').jobs['all-checks-passed']! + const results = aggregate.needs!.map(() => 'success') + results[0] = result + const condition = aggregate.if as string + // A status function prevents Actions from implicitly gating on success(). + expect(condition).toContain('!cancelled()') + expect(evaluateCondition(condition, false, results)).toBe(true) + expect(evaluateCondition(condition, true, results)).toBe(false) + expect(evaluateCondition(condition, false, results, 'push')).toBe(false) + const failureStep = aggregate.steps!.find(step => step.name === 'Fail if any needed job did not succeed')! + expect(evaluateCondition(failureStep.if!, false, results)).toBe(result !== 'success') + expect(failureStep.run).toContain('exit 1') + }, + ) + + it('distinguishes the obsolete always verdict from the cancellable status guard', () => { + expect(evaluateCondition("always() && github.event_name == 'pull_request'", true, ['success'])).toBe(true) + expect(evaluateCondition(workflow('ci.yml').jobs['all-checks-passed']!.if as string, true, ['success'])).toBe(false) + }) + it('keeps only Linux and Windows x64 runtimes in required PR CI', () => { const pr = workflow('ci.yml') expect(Object.keys(pr.on)).toEqual(['pull_request']) @@ -49,7 +86,7 @@ describe('master-only platform scheduling', () => { expect(aggregate.needs).toContain('python-runtime') expect(aggregate.needs).not.toContain('windows') expect(aggregate.needs!.every(id => id in pr.jobs)).toBe(true) - expect(aggregate.if).toBe("always() && github.event_name == 'pull_request'") + expect(aggregate.if).toBe("${{ !cancelled() && github.event_name == 'pull_request' }}") expect(aggregate.steps).toContainEqual(expect.objectContaining({ if: "contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') || contains(needs.*.result, 'skipped')", })) @@ -70,7 +107,7 @@ describe('master-only platform scheduling', () => { expect(runtime['continue-on-error']).toBeUndefined() const builder = workflow('build-exe-for-python-sdk.yml') expect(builder.concurrency?.['cancel-in-progress']).toBe( - "${{ github.event_name != 'push' || github.ref != 'refs/heads/master' }}", + '${{ !inputs.release }}', ) const build = builder.jobs.build! const preflight = build.steps!.find(step => step.name === 'Preflight installed-wheel real API test (POSIX)')! diff --git a/scripts/tests/ci-release-selfhosted.spec.ts b/scripts/tests/ci-release-selfhosted.spec.ts index 4a357114ba..ade0291f5f 100644 --- a/scripts/tests/ci-release-selfhosted.spec.ts +++ b/scripts/tests/ci-release-selfhosted.spec.ts @@ -88,7 +88,7 @@ for (const [file, jobIds] of [['release.yml', ['dependencies', 'pack']], ['relea expect(Object.keys(release.jobs)).toEqual(jobIds) expect(release.on).toEqual({ pull_request: null, push: { branches: ['master'] }, workflow_dispatch: null }) expect(release.permissions).toEqual({ contents: 'read' }) - expect(release.concurrency).toEqual({ group: '${{ github.workflow }}-${{ github.ref }}', 'cancel-in-progress': false }) + expect(release.concurrency).toEqual({ group: '${{ github.workflow }}-${{ github.ref }}', 'cancel-in-progress': true }) }) for (const jobId of jobIds) { describe(jobId, () => { From 516bfd5f30fd4d388fddb7f73da9f4bc38b923eb Mon Sep 17 00:00:00 2001 From: Yudong Date: Wed, 9 Sep 2026 19:49:56 +0800 Subject: [PATCH 11/27] test(web): cover preview import recovery and color schemes --- .../tests/fence-previews.client.spec.tsx | 12 ++++++++++++ .../tests/mermaid-runtime.client.spec.ts | 11 +++++++++++ 2 files changed, 23 insertions(+) diff --git a/packages/client/ui-primitives/tests/fence-previews.client.spec.tsx b/packages/client/ui-primitives/tests/fence-previews.client.spec.tsx index f7fe436821..cc240f75c1 100644 --- a/packages/client/ui-primitives/tests/fence-previews.client.spec.tsx +++ b/packages/client/ui-primitives/tests/fence-previews.client.spec.tsx @@ -81,6 +81,18 @@ describe('Markdown fence previews', () => { }) describe('static preview documents', () => { + it.each(['light', 'dark'])('uses the document %s color scheme for HTML previews', (scheme) => { + const previous = document.documentElement.style.colorScheme + try { + document.documentElement.style.colorScheme = scheme + const doc = renderHtml('

Example

', new AbortController().signal) + const parsed = new DOMParser().parseFromString(doc, 'text/html') + expect(parsed.documentElement.style.colorScheme).toBe(scheme) + } finally { + document.documentElement.style.colorScheme = previous + } + }) + it('preserves HTML styling while removing scripts, navigation, and nested documents', () => { const doc = renderHtml(` diff --git a/packages/client/ui-primitives/tests/mermaid-runtime.client.spec.ts b/packages/client/ui-primitives/tests/mermaid-runtime.client.spec.ts index 863abde53d..c5a4ccd6d3 100644 --- a/packages/client/ui-primitives/tests/mermaid-runtime.client.spec.ts +++ b/packages/client/ui-primitives/tests/mermaid-runtime.client.spec.ts @@ -13,6 +13,17 @@ beforeEach(() => { afterEach(() => { vi.doUnmock('mermaid') }) describe('Mermaid runtime', () => { + it('allows another attempt after the runtime import fails', async () => { + vi.doMock('mermaid', () => { throw new Error('runtime unavailable') }) + const { renderMermaid } = await import('../src/markdown/mermaid.ts') + await expect(renderMermaid('first', new AbortController().signal)).rejects.toThrow() + expect(initialize).not.toHaveBeenCalled() + vi.doMock('mermaid', () => ({ default: { initialize, render: renderDiagram } })) + renderDiagram.mockResolvedValue({ svg: '' }) + await expect(renderMermaid('retry', new AbortController().signal)).resolves.toContain('data:image/svg+xml') + expect(renderDiagram).toHaveBeenCalledOnce() + }) + it('accepts native configuration without weakening preview restrictions', async () => { const { renderMermaid } = await import('../src/markdown/mermaid.ts') renderDiagram.mockResolvedValue({ svg: '' }) From 8870a13aa941fee98455fc4e82877b0bc0ca69a7 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Wed, 9 Sep 2026 20:01:24 +0800 Subject: [PATCH 12/27] feat(sidebar): preview image files --- ...9-08-document-preview-operations.i18n.yaml | 4 +- .../2026-09-08-document-preview-operations.md | 6 +- ...26-09-08-document-preview-operations.zh.md | 6 +- ...debar-text-preview-and-file-tree.i18n.yaml | 4 +- ...9-05-sidebar-text-preview-and-file-tree.md | 4 +- ...5-sidebar-text-preview-and-file-tree.zh.md | 4 +- apps/web/tests/document-preview.e2e.ts | 57 ++++++++- .../README.i18n.yaml | 4 +- .../ui-sidebar-documentpreview/README.md | 14 ++- .../ui-sidebar-documentpreview/README.zh.md | 14 ++- .../ui-sidebar-documentpreview/package.json | 2 +- .../src/client/image/ImageBody.module.css | 40 +++++++ .../src/client/image/ImageBody.tsx | 106 +++++++++++++++++ .../src/client/image/index.ts | 41 +++++++ .../src/client/image/locales.ts | 27 +++++ .../src/client/index.ts | 2 + .../tests/apply.client.spec.ts | 3 + .../tests/image-body.client.spec.tsx | 111 ++++++++++++++++++ .../tests/image-registration.client.spec.ts | 63 ++++++++++ .../src/client/slot-catalog.ts | 1 + 20 files changed, 485 insertions(+), 28 deletions(-) create mode 100644 packages/client/ui-sidebar-documentpreview/src/client/image/ImageBody.module.css create mode 100644 packages/client/ui-sidebar-documentpreview/src/client/image/ImageBody.tsx create mode 100644 packages/client/ui-sidebar-documentpreview/src/client/image/index.ts create mode 100644 packages/client/ui-sidebar-documentpreview/src/client/image/locales.ts create mode 100644 packages/client/ui-sidebar-documentpreview/tests/image-body.client.spec.tsx create mode 100644 packages/client/ui-sidebar-documentpreview/tests/image-registration.client.spec.ts diff --git a/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.i18n.yaml index 70fc2a365d..7f1944282d 100644 --- a/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-08-document-preview-operations.md -2026-09-08-document-preview-operations.md: 316f33371d393846cce5b598ee23289cecd6c178 -2026-09-08-document-preview-operations.zh.md: 97759db32d139a44fe33fd2c5e2eb7ec0c8960fd +2026-09-08-document-preview-operations.md: 3703933273e743c8df32bf0352fc276fe21dcb93 +2026-09-08-document-preview-operations.zh.md: b4896e95959d0f276ee69dfeaee9528319981714 diff --git a/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.md b/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.md index 316f33371d..3703933273 100644 --- a/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.md +++ b/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.md @@ -18,7 +18,7 @@ Readable files use `dsh-resource://file/session//`. The path ma [Document Preview](../../../../packages/client/ui-sidebar-documentpreview/README.md) owns format selection and loading policy. Metadata registers with `ctx.documentPreviews`; components register separately into the keyed `sidebar.right.tab.document` Slot. Extension registrations precede builtins, then longer suffixes and registration order decide. The toolbar lists matching alternatives and remembers a manual choice per tab; plain text is the fallback. The child receives accumulated text or complete native bytes, the original resource address, and the standard `useResource` and `useTabInfo` hooks. Preview calls existing `read`, `readAll`, and `readRelated` through ordinary injection and decodes bytes in its own `rpc.ts`. Refresh remains per tab, with no resource reload, shared `changed` acknowledgement, extra resource wrapper, or content Session. -Markdown and code reuse the incremental primitives with cumulative paged text. HTML and PDF read complete `Uint8Array` data; Host transport remains base64. Published buffers are borrowed read-only and never persist into layout or Session JSON. PDF.js runs in an owned Worker with version-matched bundled font and decoder data, and copies input before transfer to preserve Preview's retained buffer. HTML runs in a Blob iframe with `sandbox="allow-scripts"`, without same-origin, popup, form, download, or top-navigation privileges. The browser retains its normal external-network rules. Bounded static local JS/CSS reads stay in the parent; the opaque frame creates its own asset Blobs, because it cannot load parent-origin Blobs. Replacing the document replaces the browsing context and revokes its root Blob. +Markdown and code reuse the incremental primitives with cumulative paged text. HTML, PDF, and images read complete `Uint8Array` data; Host transport remains base64. Published buffers are borrowed read-only and never persist into layout or Session JSON. PDF.js runs in an owned Worker with version-matched bundled font and decoder data, and copies input before transfer to preserve Preview's retained buffer. HTML runs in a Blob iframe with `sandbox="allow-scripts"`, without same-origin, popup, form, download, or top-navigation privileges. The browser retains its normal external-network rules. Bounded static local JS/CSS reads stay in the parent; the opaque frame creates its own asset Blobs, because it cannot load parent-origin Blobs. PNG, JPEG, GIF, WebP, BMP, ICO, and SVG use image-specific Blob URLs in an `` static-image context. They retain intrinsic CSS-pixel dimensions; auto margins centre images smaller than the shared scroller, while larger dimensions extend its horizontal or vertical scroll range. The renderer provides no zoom or drag-to-pan. SVG markup never enters the application DOM or an iframe, so scripts remain inert and cannot reach the parent page. Replacing HTML or an image revokes its root Blob URL. ## Alternatives considered @@ -34,6 +34,8 @@ Markdown and code reuse the incremental primitives with cumulative paged text. H **A local server, virtual host, or `file:` iframe.** These require extra hosting or filesystem authority. The preview is for static generated pages, not a complete application runtime; modules, dynamic filesystem requests, and arbitrary nested asset graphs are outside its support. +**Sanitize SVG into the application DOM or an iframe.** A sanitizer would add a second SVG parser and an evolving active-content policy before placing untrusted markup in an interactive document. The `` static-image context preserves native SVG rendering and intrinsic dimensions without giving the markup a script-capable DOM. + ## Consequences -Renderers can be replaced without changing the tab or file protocol. Full-file formats pay bounded whole-file memory and PDF adds bundled Worker/font/decoder bytes. Format selection and view state are page-local, not durable Session data. Preview owns RPC cancellation and native buffers independently of metadata observation. A tab retains its read version and the observation version captured at read start; refreshing it neither discards another tab's content nor clears its change notice. File reads remain non-transactional, and opaque versions are compared for equality, not ordering. The [recorded browser scenario](../../../../apps/web/tests/document-preview.e2e.ts) exercises the shared toolbar, incremental text, isolated HTML dependencies, and lazy continuous PDF Worker rendering. +Renderers can be replaced without changing the tab or file protocol. Full-file formats pay bounded whole-file memory and PDF adds bundled Worker/font/decoder bytes. Format selection and view state are page-local, not durable Session data. Preview owns RPC cancellation and native buffers independently of metadata observation. A tab retains its read version and the observation version captured at read start; refreshing it neither discards another tab's content nor clears its change notice. File reads remain non-transactional, and opaque versions are compared for equality, not ordering. The [recorded browser scenario](../../../../apps/web/tests/document-preview.e2e.ts) exercises the shared toolbar, incremental text, isolated HTML dependencies, intrinsic raster and SVG rendering with two-axis scrolling, inert SVG scripts, and lazy continuous PDF Worker rendering. diff --git a/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.zh.md b/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.zh.md index 97759db32d..b4896e9595 100644 --- a/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.zh.md @@ -18,7 +18,7 @@ Document Preview 将资源观察与内容读取分开。[资源模型](2026-09-0 [Document Preview](../../../../packages/client/ui-sidebar-documentpreview/README.zh.md) 负责格式选择和加载策略。元数据通过 `ctx.documentPreviews` 注册;组件单独注册到 keyed `sidebar.right.tab.document` Slot。扩展注册优先于内置注册,其次比较后缀长度和注册顺序。工具栏列出匹配候选,按 tab 记住手动选择;纯文本是兜底。子组件收到累积文本或完整原生字节、原始资源地址,以及标准 `useResource` 和 `useTabInfo` 钩子。Preview 经普通注入调用既有 `read`、`readAll` 与 `readRelated`,在自己的 `rpc.ts` 解码字节。刷新仍按 tab 独立进行,不引入资源 reload、共享 `changed` 确认、额外资源包装层或内容 Session。 -Markdown 和代码通过累积的分页文本复用增量渲染原语。HTML 和 PDF 读取完整 `Uint8Array` 数据;Host 传输保持 base64。发布后的缓冲区只读借用,绝不持久化进布局或 Session JSON。PDF.js 在自有 Worker 中运行,字体和解码数据以相同版本随包发布,转移输入前先复制,以保留 Preview 的缓冲区。HTML 在 Blob iframe 中运行,设置 `sandbox="allow-scripts"`,不授予同源、弹窗、表单、下载或顶层导航权限。浏览器保持正常的外部网络规则。有上限的静态本地 JS/CSS 读取由父页面负责;不透明源 iframe 创建自己的资源 Blob,因为它不能加载父源创建的 Blob。替换文档会替换浏览上下文,并撤销其根 Blob。 +Markdown 和代码通过累积的分页文本复用增量渲染原语。HTML、PDF 和图片读取完整 `Uint8Array` 数据;Host 传输保持 base64。发布后的缓冲区只读借用,绝不持久化进布局或 Session JSON。PDF.js 在自有 Worker 中运行,字体和解码数据以相同版本随包发布,转移输入前先复制,以保留 Preview 的缓冲区。HTML 在 Blob iframe 中运行,设置 `sandbox="allow-scripts"`,不授予同源、弹窗、表单、下载或顶层导航权限。浏览器保持正常的外部网络规则。有上限的静态本地 JS/CSS 读取由父页面负责;不透明源 iframe 创建自己的资源 Blob,因为它不能加载父源创建的 Blob。PNG、JPEG、GIF、WebP、BMP、ICO 和 SVG 使用图片专用 Blob URL,在 `` 静态图片上下文中渲染。它们保留固有 CSS 像素尺寸;auto margin 让小于共享滚动区的图片居中,较大的尺寸则扩展横向或纵向滚动范围。渲染器不提供缩放或拖拽平移。SVG 标记绝不进入应用 DOM 或 iframe,因此脚本保持不可执行,也无法访问父页面。替换 HTML 或图片时会撤销其根 Blob URL。 ## 考虑过的替代方案 @@ -34,6 +34,8 @@ Markdown 和代码通过累积的分页文本复用增量渲染原语。HTML 和 **本地服务器、虚拟主机或 `file:` iframe。** 这些方案需要额外托管或文件系统权限。预览面向静态生成页面,而非完整应用运行时;模块、动态文件系统请求和任意嵌套资源图不在支持范围内。 +**清理 SVG 后放入应用 DOM 或 iframe。** sanitizer 会增加第二套 SVG parser 和一套持续演进的主动内容策略,之后仍要把不可信标记放进可交互文档。`` 静态图片上下文保留浏览器原生 SVG 渲染与固有尺寸,同时不给标记一个能运行脚本的 DOM。 + ## 影响 -替换渲染器不需要改变 Tab 或文件协议。全文格式承担有上限的整文件内存成本,PDF 增加随包发布的 Worker、字体和解码器字节。格式选择和查看状态仅属于当前页面,不是持久 Session 数据。Preview 独立于元数据观察,拥有 RPC 取消和原生缓冲区。tab 保留读取版本及读取开始时捕获的观察版本;刷新它既不丢弃其他 tab 的内容,也不清除其变更提示。文件读取仍非事务,不透明版本只比较相等性、不排序。[录制的浏览器场景](../../../../apps/web/tests/document-preview.e2e.ts) 覆盖共用工具栏、增量文本、隔离的 HTML 依赖,以及惰性连续 PDF Worker 渲染。 +替换渲染器不需要改变 Tab 或文件协议。全文格式承担有上限的整文件内存成本,PDF 增加随包发布的 Worker、字体和解码器字节。格式选择和查看状态仅属于当前页面,不是持久 Session 数据。Preview 独立于元数据观察,拥有 RPC 取消和原生缓冲区。tab 保留读取版本及读取开始时捕获的观察版本;刷新它既不丢弃其他 tab 的内容,也不清除其变更提示。文件读取仍非事务,不透明版本只比较相等性、不排序。[录制的浏览器场景](../../../../apps/web/tests/document-preview.e2e.ts) 覆盖共用工具栏、增量文本、隔离的 HTML 依赖、可双轴滚动的固有尺寸位图与 SVG 渲染、不可执行的 SVG 脚本,以及惰性连续 PDF Worker 渲染。 diff --git a/.agents/notes/implemented/feature/2026-09-05-sidebar-text-preview-and-file-tree.i18n.yaml b/.agents/notes/implemented/feature/2026-09-05-sidebar-text-preview-and-file-tree.i18n.yaml index 4e4b039406..5c04c6fdb7 100644 --- a/.agents/notes/implemented/feature/2026-09-05-sidebar-text-preview-and-file-tree.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-09-05-sidebar-text-preview-and-file-tree.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-05-sidebar-text-preview-and-file-tree.md -2026-09-05-sidebar-text-preview-and-file-tree.md: 9c0b6db832f19ba758ba2d3851db72f9a1fdb785 -2026-09-05-sidebar-text-preview-and-file-tree.zh.md: a5173c20b5203942df3c6f86b2bef5961946fa08 +2026-09-05-sidebar-text-preview-and-file-tree.md: e25e1c55f5434f2b3b1b2703f886cf3c1e9fe80c +2026-09-05-sidebar-text-preview-and-file-tree.zh.md: f313d30fa68f9d8afba84a470e5e80902b13d1e3 diff --git a/.agents/notes/implemented/feature/2026-09-05-sidebar-text-preview-and-file-tree.md b/.agents/notes/implemented/feature/2026-09-05-sidebar-text-preview-and-file-tree.md index 9c0b6db832..e25e1c55f5 100644 --- a/.agents/notes/implemented/feature/2026-09-05-sidebar-text-preview-and-file-tree.md +++ b/.agents/notes/implemented/feature/2026-09-05-sidebar-text-preview-and-file-tree.md @@ -108,12 +108,12 @@ Copy is the `sidebarFiles` namespace, thirteen keys. Row states: `loading` 「 ## Testing -The text preview's `tests/` cover the registry claim and yielding (through the real `SidebarRightTabRegistry`), the address translation (`sessionFileOf` accepting the `session` scope and throwing on others), the store's page, version, reset, view, and forget actions, the face's in-flight, failure, aborted, and reload paths, the page arithmetic (`linesOf`, `offsetsOf`, `lastLineLoaded`), the body's first read, load-more, retry, change bar, navigation walk, jump-once, remount, wrap default and toggle, header controls, and forget-on-abort, the failure-line mapping, and the plugin's registrations and their removal on dispose. A Chromium probe against the built app recorded the fill and scroll numbers (`.artifacts/sidebar-tab-types/app-probe.log`, `ROUND3`): a short file's preview is the pane body's content height, a long file scrolls inside the preview body, and the pane body never scrolls. The file tree's `tests/` cover ordering, lazy loading, collapse memory, reload, the three entry types, truncation and failure rows, and forget-on-abort. `apps/web/tests/sidebar-right.e2e.ts` opens a produced file from the conversation into the preview over the real Remote carrier. +The text preview's `tests/` cover the registry claim and yielding (through the real `SidebarRightTabRegistry`), the address translation (`sessionFileOf` accepting the `session` scope and throwing on others), the store's page, version, reset, view, and forget actions, the face's in-flight, failure, aborted, and reload paths, the page arithmetic (`linesOf`, `offsetsOf`, `lastLineLoaded`), the body's first read, load-more, retry, change bar, navigation walk, jump-once, remount, wrap default and toggle, header controls, and forget-on-abort, the failure-line mapping, and the plugin's registrations and their removal on dispose. A Chromium probe against the built app recorded the fill and scroll numbers (`.artifacts/sidebar-tab-types/app-probe.log`, `ROUND3`): a short file's preview is the pane body's content height, a long file scrolls inside the preview body, and the pane body never scrolls. The file tree's `tests/` cover ordering, lazy loading, collapse memory, reload, the three entry types, truncation and failure rows, and forget-on-abort. `apps/web/tests/sidebar-right.e2e.ts` opens a produced file from the conversation into the preview over the real Remote carrier. `apps/web/tests/document-preview.e2e.ts` covers centred intrinsic-size images, two-axis image scrolling, and inert SVG scripts. ## Deferred - Virtualized or seekable page loading (pages load in order), a reload that restores the loaded range, throttled scroll persistence, and a wrap icon in `ui-primitives`. -- Images, search, a total line count, and an end-of-file marker. +- Search, a total line count, and an end-of-file marker. - Search, an artifact filter, drag-and-drop, rename, a context menu, current-file highlight, filesystem watching, and browsing above the workspace root in the file tree. - Product review of the guide's copy, and the guide's behaviour when a type contributes several entries. diff --git a/.agents/notes/implemented/feature/2026-09-05-sidebar-text-preview-and-file-tree.zh.md b/.agents/notes/implemented/feature/2026-09-05-sidebar-text-preview-and-file-tree.zh.md index a5173c20b5..f313d30fa6 100644 --- a/.agents/notes/implemented/feature/2026-09-05-sidebar-text-preview-and-file-tree.zh.md +++ b/.agents/notes/implemented/feature/2026-09-05-sidebar-text-preview-and-file-tree.zh.md @@ -108,12 +108,12 @@ face 是树唯一的异步半边。`start(tabId, root, signal)` 以根展开态 ## Testing -文本预览的 `tests/` 覆盖:注册表认领与让位(经真实的 `SidebarRightTabRegistry`)、地址翻译(`sessionFileOf` 接受 `session` 作用域、其他一律抛错)、store 的页、版本、reset、视图与 forget 各 action、face 的进行中、失败、abort 与重载路径、页算术(`linesOf`、`offsetsOf`、`lastLineLoaded`)、体的首读、加载更多、重试、变更提示条、导航补页、只跳一次、重新挂载、换行默认与切换、头部控件与 abort 即忘、失败行映射,以及插件的各项注册与 dispose 时的撤销。针对已构建应用的 Chromium 探针记录了撑满与滚动的数字(`.artifacts/sidebar-tab-types/app-probe.log`,`ROUND3`):短文件的预览高度等于 pane 体内容区高度,长文件在预览体内滚动,pane 体从不滚动。文件树的 `tests/` 覆盖排序、懒加载、折叠记忆、重新读取、三种条目类型、截断与失败行,以及 abort 即忘。`apps/web/tests/sidebar-right.e2e.ts` 经真实 Remote 载体把会话里的产物文件打开进预览。 +文本预览的 `tests/` 覆盖:注册表认领与让位(经真实的 `SidebarRightTabRegistry`)、地址翻译(`sessionFileOf` 接受 `session` 作用域、其他一律抛错)、store 的页、版本、reset、视图与 forget 各 action、face 的进行中、失败、abort 与重载路径、页算术(`linesOf`、`offsetsOf`、`lastLineLoaded`)、体的首读、加载更多、重试、变更提示条、导航补页、只跳一次、重新挂载、换行默认与切换、头部控件与 abort 即忘、失败行映射,以及插件的各项注册与 dispose 时的撤销。针对已构建应用的 Chromium 探针记录了撑满与滚动的数字(`.artifacts/sidebar-tab-types/app-probe.log`,`ROUND3`):短文件的预览高度等于 pane 体内容区高度,长文件在预览体内滚动,pane 体从不滚动。文件树的 `tests/` 覆盖排序、懒加载、折叠记忆、重新读取、三种条目类型、截断与失败行,以及 abort 即忘。`apps/web/tests/sidebar-right.e2e.ts` 经真实 Remote 载体把会话里的产物文件打开进预览。`apps/web/tests/document-preview.e2e.ts` 覆盖居中的固有尺寸图片、双轴图片滚动和不可执行的 SVG 脚本。 ## Deferred - 虚拟化或可 seek 的分页加载(页按顺序加载)、恢复已加载范围的重新载入、节流的滚动位置持久化,以及 `ui-primitives` 里的换行图标。 -- 图片、搜索、总行数与文件末尾标记。 +- 搜索、总行数与文件末尾标记。 - 文件树的搜索、产物过滤、拖拽、重命名、右键菜单、高亮当前文件、文件系统监听,以及浏览到工作区根之上。 - 引导页文案的产品评审,以及一个类型贡献多个入口时引导页的行为。 diff --git a/apps/web/tests/document-preview.e2e.ts b/apps/web/tests/document-preview.e2e.ts index 58b9913ce6..41af1afa6b 100644 --- a/apps/web/tests/document-preview.e2e.ts +++ b/apps/web/tests/document-preview.e2e.ts @@ -18,6 +18,10 @@ const PAGE_LINES = 64 const SHOT_DIR = fileURLToPath(new URL('../../../.artifacts/screenshots/0908-document-preview', import.meta.url)) const PROMPT = 'Reply with the single word LIGHTHOUSE and stop.' const MODE = webSnapshotMode() +const TINY_PNG = Buffer.from( + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', + 'base64', +) /** Successful render evidence stays outside the committed snapshot inventory. */ async function successShot(page: Page, name: string): Promise { @@ -76,7 +80,7 @@ describe.skipIf(MODE === 'record')('web e2e: document preview through Files', () } }) - it('opens Markdown, isolated HTML, and a rendered PDF from the Session workspace', async () => { + it('opens text, isolated HTML, intrinsic images, and rendered PDF from the Session workspace', async () => { onTestFailed(async () => { await mkdir(SHOT_DIR, { recursive: true }) await saveFailureShot(page, `screenshots/0908-document-preview/smoke-${process.pid}`) @@ -118,6 +122,13 @@ describe.skipIf(MODE === 'record')('web e2e: document preview through Files', () writeFile(join(cwd, 'local.js'), 'document.getElementById("local-result").textContent="LOCAL_JS_OK";'), writeFile(join(cwd, 'local.css'), '#local-result { color: rgb(12, 34, 56); }'), writeFile(outsideScript, 'document.getElementById("outside-result").textContent="OUTSIDE_JS_OK";'), + writeFile(join(cwd, 'tiny.png'), TINY_PNG), + writeFile(join(cwd, 'large.svg'), [ + '', + '', + '', + '', + ].join('')), writeFile(join(cwd, 'smoke.pdf'), pdfFixture()), ]) @@ -292,6 +303,50 @@ describe.skipIf(MODE === 'record')('web e2e: document preview through Files', () `- Same tab: ${String(await pdfTab.getAttribute('data-dockkit-tab') === pdfTabId)}`, ].join('\n')) + await openFile('tiny.png') + await expect.poll(() => viewer.innerText()).toBe('Image') + const tinyImage = preview.getByRole('img', { name: 'Image preview: tiny.png', exact: true }) + await tinyImage.waitFor({ state: 'visible', timeout: 15_000 }) + expect(await tinyImage.evaluate(node => ({ + width: (node as HTMLImageElement).naturalWidth, + height: (node as HTMLImageElement).naturalHeight, + draggable: (node as HTMLImageElement).draggable, + }))).toEqual({ width: 1, height: 1, draggable: false }) + const centering = await tinyImage.evaluate((node) => { + const image = node.getBoundingClientRect() + const scroller = node.closest('[data-textpreview-body]')?.getBoundingClientRect() + if (scroller === undefined) throw new Error('image document scroller is unavailable') + return { + horizontal: Math.abs((image.left + image.width / 2) - (scroller.left + scroller.width / 2)), + vertical: Math.abs((image.top + image.height / 2) - (scroller.top + scroller.height / 2)), + } + }) + expect(centering.horizontal).toBeLessThan(10) + expect(centering.vertical).toBeLessThan(10) + + await openFile('large.svg') + await expect.poll(() => viewer.innerText()).toBe('Image') + const largeImage = preview.getByRole('img', { name: 'Image preview: large.svg', exact: true }) + await largeImage.waitFor({ state: 'visible', timeout: 15_000 }) + expect(await largeImage.evaluate(node => ({ + naturalWidth: (node as HTMLImageElement).naturalWidth, + naturalHeight: (node as HTMLImageElement).naturalHeight, + width: getComputedStyle(node).width, + height: getComputedStyle(node).height, + }))).toEqual({ naturalWidth: 1200, naturalHeight: 1600, width: '1200px', height: '1600px' }) + expect(await body.evaluate(node => ({ + horizontal: node.scrollWidth > node.clientWidth, + vertical: node.scrollHeight > node.clientHeight, + }))).toEqual({ horizontal: true, vertical: true }) + const scrolled = await body.evaluate((node) => { + node.scrollLeft = node.scrollWidth + node.scrollTop = node.scrollHeight + return { left: node.scrollLeft, top: node.scrollTop } + }) + expect(scrolled.left).toBeGreaterThan(0) + expect(scrolled.top).toBeGreaterThan(0) + expect(await page.locator('html').getAttribute('data-image-preview-escape')).toBeNull() + const releaseRead = Promise.withResolvers() let waitingForRead = false const readPage = scaffold.ctx.workspaceFiles.read.bind(scaffold.ctx.workspaceFiles) diff --git a/packages/client/ui-sidebar-documentpreview/README.i18n.yaml b/packages/client/ui-sidebar-documentpreview/README.i18n.yaml index 68e01296cc..91a920665d 100644 --- a/packages/client/ui-sidebar-documentpreview/README.i18n.yaml +++ b/packages/client/ui-sidebar-documentpreview/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-sidebar-documentpreview/README.md -README.md: 9f664664a5d00add4d2f360c1c6b6af2e885bf72 -README.zh.md: 2cdad6964ae26e8e10597a405b0925cf70d7a278 +README.md: fbfbfaf4d80e425e5fbc62c082509fb6c4a93962 +README.zh.md: c27a359b608d8b72ace0700c55c34a58a302ec60 diff --git a/packages/client/ui-sidebar-documentpreview/README.md b/packages/client/ui-sidebar-documentpreview/README.md index 9f664664a5..fbfbfaf4d8 100644 --- a/packages/client/ui-sidebar-documentpreview/README.md +++ b/packages/client/ui-sidebar-documentpreview/README.md @@ -1,5 +1,5 @@ --- -description: "Document previews in the right Sidebar: shared file loading and controls, selectable Markdown, code, PDF and HTML renderers, and plain-text fallback." +description: "Document previews in the right Sidebar: shared file loading and controls, selectable Markdown, code, image, PDF and HTML renderers, and plain-text fallback." kind: "package-reference" --- @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -Preview readable files in the right Sidebar and choose among registered renderers without opening another tab. Markdown and code receive accumulated text pages; PDF and HTML receive complete bytes; unknown file extensions use plain text. The tab owns loading, file status, renderer selection, wrap, and reload, while document bodies register through the same metadata registry and child slot. The Sidebar tab kind is `text`. +Preview readable files in the right Sidebar and choose among registered renderers without opening another tab. Markdown and code receive accumulated text pages; PDF, HTML, and common images receive complete bytes; unknown file extensions use plain text. The tab owns loading, file status, renderer selection, wrap, and reload, while document bodies register through the same metadata registry and child slot. The Sidebar tab kind is `text`. ## Table of Contents @@ -44,14 +44,16 @@ The body reads its record, navigation and lifetime through `useTabInfo().tab`. ` - The resource snapshot contains only `status`, `value`, and `failure`; `value` is `WorkspaceFileStat` metadata. Content reads do not wait for the first metadata frame once the provider is available. Observation failures take precedence over Preview's change notice; neither automatically replaces loaded content. - **Text pages** — plain text, Markdown, and code read through an inject callback to `remote.workspaceFiles.read(sessionId, path, { offset }, signal)`. The first mount reads page one; scrolling to the body end or **Load more** requests the next page until `eof`. The owner delivers the accumulated prefix as `{ kind: 'text', text, pages, eof }`, including source offsets and line counts. Markdown and code render that prefix incrementally; they do not render each page as a separate document. A newer-version page past page one restarts from the beginning rather than mixing versions. A failure before any content fills the body with the file-type icon, explanation, and retry; a later failure retains loaded content and adds the retry below it. -- **Complete bytes** — PDF and HTML use an inject callback to `remote.workspaceFiles.readAll(sessionId, path, signal)`. `rpc.ts` decodes the wire base64 into `data: Uint8Array` for `{ kind: 'bytes', data }`. The Host's `maxFileBytes` cap rejects oversized files rather than truncating them. PDF copies retained bytes before worker transfer, keeping the Preview buffer usable. Bytes stay in transient view state, never persisted layouts or Session JSONL. Loading-mode changes retire previous results. +- **Complete bytes** — PDF, HTML, and common images use an inject callback to `remote.workspaceFiles.readAll(sessionId, path, signal)`. `rpc.ts` decodes the wire base64 into `data: Uint8Array` for `{ kind: 'bytes', data }`. The Host's `maxFileBytes` cap rejects oversized files rather than truncating them. PDF copies retained bytes before worker transfer, keeping the Preview buffer usable. Bytes stay in transient view state, never persisted layouts or Session JSONL. Loading-mode changes retire previous results. - **Reload** — only the current Preview tab rereads through its Remote callbacks, preserving its scroll preference and retiring older requests. Its change notice compares the read version and the observation captured at read start with later `resource.value.version`; an already observed version does not become a new change after refresh. Reads neither refresh shared metadata nor clear another tab's notice. HTML runs in a Blob iframe with exactly `sandbox="allow-scripts"`, without `allow-same-origin`; scripts cannot access the parent application's origin or file reader. The renderer loads directly declared relative `.js` classic scripts and `.css` stylesheets through its ordinary inject callback to `remote.workspaceFiles.readRelated`, with fixed safety limits of 4 MiB per asset, 32 MiB total, and 64 distinct assets. Host code resolves the related path; `rpc.ts` decodes the returned bytes. Inside the renderer, base64 is used only to embed the iframe bootstrap payload in script text. A `` leaves dependency resolution to the browser, as do HTTPS resources. Local module imports, CSS `url()`/`@import`, and dynamic `fetch` do not use Host file access. Read failures, invalid UTF-8, or exceeded limits fail the preview rather than publishing a partial asset package. Replacing or unmounting the document releases its Blob URL. +PNG, JPEG, GIF, WebP, BMP, ICO, and SVG render through Blob URLs in an `` static-image context. The image keeps its intrinsic CSS-pixel dimensions; a smaller image centres in the shared scroller, and larger dimensions scroll on either axis. The renderer provides neither zoom nor drag-to-pan. SVG markup never enters the application DOM or an iframe, so its scripts cannot execute or reach the parent page. Replacing or unmounting the image revokes its Blob URL. + Shared copy comes from `sidebarDocumentPreview`; each builtin renderer owns its localized labels. -Initial reads, additional pages, and HTML/PDF preparation share a loading indicator that respects reduced-motion preferences. Loaded pages stay visible while another page loads. PDF pages form one vertical, width-fitted sequence and render lazily near the viewport. Code previews show source line numbers by default without including them in copied text; plain text uses the same font size and line height as code. Code sits on the pane's own background rather than the chat card's fill, and with wrap off both axes scroll in the shared document scroller while the copy banner stays pinned to the visible width. +Initial reads, additional pages, and HTML/PDF/image preparation share a loading indicator that respects reduced-motion preferences. Loaded pages stay visible while another page loads. PDF pages form one vertical, width-fitted sequence and render lazily near the viewport. Code previews show source line numbers by default without including them in copied text; plain text uses the same font size and line height as code. Code sits on the pane's own background rather than the chat card's fill, and with wrap off both axes scroll in the shared document scroller while the copy banner stays pinned to the visible width. ## Navigation @@ -71,8 +73,8 @@ No direct effect; what the user reads here never enters a model request. - **Preview, not editing.** The viewers provide no file editing or shared search interface; a directory address fails with `not-regular-file`. Unknown extensions use the plain-text reader and remain subject to its UTF-8/NUL checks. -- **Sequential text and bounded complete files.** Deep source lines require the preceding pages; PDF and HTML require a complete result within the Host's `maxFileBytes` cap. -- **Byte-view scroll state is not restored.** PDF and HTML can return to the top when their renderer remounts or reloads; HTML iframe scrolling belongs to its opaque browsing context. +- **Sequential text and bounded complete files.** Deep source lines require the preceding pages; PDF, HTML, and images require a complete result within the Host's `maxFileBytes` cap. +- **Byte-view scroll state is not restored.** PDF, HTML, and images can return to the top when their renderer remounts or reloads; image horizontal position is never restored, and HTML iframe scrolling belongs to its opaque browsing context. - **Finite local HTML dependencies.** Only direct classic `.js` and stylesheet `.css` references are packed. Browser-resolved resources retain browser origin and network restrictions; no runtime file-read bridge is exposed to the iframe. - **Package-local wrap glyphs.** `IconWrapFill16` and `IconNowrapFill16` live in `src/client/icons.tsx` until the shared icon set carries them; their props already match the shared icon contract. - **Scroll writes are unthrottled.** Every scroll event records its offset in the store; the line blocks are memoized so the resulting re-render hands React the same elements back. diff --git a/packages/client/ui-sidebar-documentpreview/README.zh.md b/packages/client/ui-sidebar-documentpreview/README.zh.md index 2cdad6964a..c27a359b60 100644 --- a/packages/client/ui-sidebar-documentpreview/README.zh.md +++ b/packages/client/ui-sidebar-documentpreview/README.zh.md @@ -1,5 +1,5 @@ --- -description: "右侧 Sidebar 的文档预览:共享文件加载与控件,可选 Markdown、代码、PDF 和 HTML 渲染器,并以纯文本兜底。" +description: "右侧 Sidebar 的文档预览:共享文件加载与控件,可选 Markdown、代码、图片、PDF 和 HTML 渲染器,并以纯文本兜底。" kind: "package-reference" --- @@ -9,7 +9,7 @@ kind: "package-reference" ## 概述 -在右侧 Sidebar 预览可读文件,无需另开 tab 即可切换已注册的渲染器。Markdown 和代码接收累计文本页;PDF 和 HTML 接收完整字节;未知文件扩展名使用纯文本。tab 负责加载、文件状态、渲染器选择、换行和重新载入,文档正文通过同一元数据注册表与子 slot 注册。Sidebar tab 的 kind 为 `text`。 +在右侧 Sidebar 预览可读文件,无需另开 tab 即可切换已注册的渲染器。Markdown 和代码接收累计文本页;PDF、HTML 和常见图片接收完整字节;未知文件扩展名使用纯文本。tab 负责加载、文件状态、渲染器选择、换行和重新载入,文档正文通过同一元数据注册表与子 slot 注册。Sidebar tab 的 kind 为 `text`。 ## 目录 @@ -44,14 +44,16 @@ tab 使用 `fileAddressFor` 构造的 Session 地址,携带相对或绝对路 - 资源快照仅包含 `status`、`value` 和 `failure`;`value` 是 `WorkspaceFileStat` 元数据。提供方可用后,内容读取无需等待首个元数据帧。观察失败优先于 Preview 的变更提示显示;两者都不会自动替换已加载内容。 - **文本页** —— 纯文本、Markdown 和代码通过 inject 回调调用 `remote.workspaceFiles.read(sessionId, path, { offset }, signal)`。首次挂载读取第一页;滚动到正文末尾或点击 **加载更多** 会读取下一页,直到 `eof`。owner 以 `{ kind: 'text', text, pages, eof }` 提供累计前缀,包含源码行偏移和行数。Markdown 和代码增量渲染此前缀,不把每页当成独立文档。第一页之后到达的更新版本页会使读取从头开始,避免混合版本。尚无内容时,失败会以文件类型图标、说明与重试按钮填满正文;较晚的失败保留已有内容并在其下提供重试。 -- **完整字节** —— PDF 和 HTML 通过 inject 回调调用 `remote.workspaceFiles.readAll(sessionId, path, signal)`。`rpc.ts` 将线路上的 base64 解码为 `data: Uint8Array`,供 `{ kind: 'bytes', data }` 使用。Host 的 `maxFileBytes` 上限拒绝超大文件,不截断。PDF 在传给 worker 前复制保留的字节,使 Preview 缓冲区仍可使用。字节仅保存在临时视图状态中,绝不进入持久布局或 Session JSONL。加载模式变化会淘汰先前结果。 +- **完整字节** —— PDF、HTML 和常见图片通过 inject 回调调用 `remote.workspaceFiles.readAll(sessionId, path, signal)`。`rpc.ts` 将线路上的 base64 解码为 `data: Uint8Array`,供 `{ kind: 'bytes', data }` 使用。Host 的 `maxFileBytes` 上限拒绝超大文件,不截断。PDF 在传给 worker 前复制保留的字节,使 Preview 缓冲区仍可使用。字节仅保存在临时视图状态中,绝不进入持久布局或 Session JSONL。加载模式变化会淘汰先前结果。 - **重新载入** —— 仅当前 Preview tab 通过自己的 Remote 回调重读,保留滚动偏好并淘汰旧请求。变更提示将读取版本及起读时的观察版本与后续 `resource.value.version` 比较;刷新前已观察到的版本不会被当成新变化。读取既不刷新共享元数据,也不清除其它 tab 的提示。 HTML 在 Blob iframe 中运行,沙箱属性严格为 `sandbox="allow-scripts"`,不含 `allow-same-origin`;脚本无法访问父应用的源或文件读取接口。渲染器通过普通 inject 回调调用 `remote.workspaceFiles.readRelated`,加载直接声明的相对 `.js` 经典脚本和 `.css` 样式表;固定安全上限为单个资源 4 MiB、总计 32 MiB、64 个不同资源。Host 代码解析关联路径,`rpc.ts` 解码返回的字节。在渲染器内部,base64 仅用于把 iframe 引导载荷嵌入脚本文本。`` 将依赖解析交给浏览器,HTTPS 资源也由浏览器处理。本地模块 import、CSS `url()`/`@import` 和动态 `fetch` 不使用 Host 文件访问。读取失败、无效 UTF-8 或超出上限都使预览失败,不发布部分资源包。替换或卸载文档会释放其 Blob URL。 +PNG、JPEG、GIF、WebP、BMP、ICO 和 SVG 通过 Blob URL 在 `` 静态图片上下文中渲染。图片保持固有 CSS 像素尺寸;小图在共享滚动区内居中,大图可沿任一轴滚动。渲染器既不提供缩放,也不提供拖拽平移。SVG 标记绝不进入应用 DOM 或 iframe,因此其中的脚本无法执行,也无法访问父页面。替换或卸载图片会撤销其 Blob URL。 + 共享文案来自 `sidebarDocumentPreview`;各内置渲染器拥有自己的本地化标签。 -首次读取、追加页及 HTML/PDF 准备共用加载指示器,并遵循减少动态效果偏好。下一页加载期间保留已显示的内容。PDF 页面组成一个纵向、适配宽度的连续序列,并在接近视口时惰性渲染。代码预览默认显示源码行号,但复制文本不包含行号;纯文本与代码使用相同字号和行高。代码直接坐在分栏自身的背景上,而不是会话卡片的填充色;关闭折行时两个轴都在共享文档滚动区滚动,复制条钉在可视宽度内不随横向滚动偏移。 +首次读取、追加页及 HTML/PDF/图片准备共用加载指示器,并遵循减少动态效果偏好。下一页加载期间保留已显示的内容。PDF 页面组成一个纵向、适配宽度的连续序列,并在接近视口时惰性渲染。代码预览默认显示源码行号,但复制文本不包含行号;纯文本与代码使用相同字号和行高。代码直接坐在分栏自身的背景上,而不是会话卡片的填充色;关闭折行时两个轴都在共享文档滚动区滚动,复制条钉在可视宽度内不随横向滚动偏移。 ## 导航 @@ -71,8 +73,8 @@ HTML 在 Blob iframe 中运行,沙箱属性严格为 `sandbox="allow-scripts"` - **预览而非编辑。** 查看器不提供文件编辑或共享搜索接口;目录地址以 `not-regular-file` 失败。未知扩展名使用纯文本读取,仍受其 UTF-8/NUL 检查限制。 -- **文本顺序分页,完整文件受限。** 定位深处源码行需要先加载此前各页;PDF 和 HTML 必须取得 Host `maxFileBytes` 上限内的完整结果。 -- **字节视图不恢复滚动位置。** PDF 与 HTML 的渲染器重新挂载或重新载入时可能回到顶部;HTML iframe 的滚动属于其不透明浏览上下文。 +- **文本顺序分页,完整文件受限。** 定位深处源码行需要先加载此前各页;PDF、HTML 和图片必须取得 Host `maxFileBytes` 上限内的完整结果。 +- **字节视图不恢复滚动位置。** PDF、HTML 与图片的渲染器重新挂载或重新载入时可能回到顶部;图片的横向位置始终不恢复,HTML iframe 的滚动属于其不透明浏览上下文。 - **本地 HTML 依赖集合有限。** 只打包直接引用的经典 `.js` 脚本和 `.css` 样式表。浏览器解析的资源仍受浏览器源与网络规则限制;iframe 不获得运行时文件读取桥接。 - **换行图标为包内自绘。** `IconWrapFill16` 与 `IconNowrapFill16` 住在 `src/client/icons.tsx`,直到共享图标集提供为止;它们的 props 已与共享图标契约一致。 - **滚动写入未节流。** 每次滚动事件都把偏移记进 store;行块已 memo 化,于是由此引发的重渲染交还给 React 的是同一批元素。 diff --git a/packages/client/ui-sidebar-documentpreview/package.json b/packages/client/ui-sidebar-documentpreview/package.json index a008ef19ee..8d592decc0 100644 --- a/packages/client/ui-sidebar-documentpreview/package.json +++ b/packages/client/ui-sidebar-documentpreview/package.json @@ -1,6 +1,6 @@ { "name": "@deepseek-ai/dsh-client-ui-sidebar-documentpreview", - "description": "Extensible document previews for Sidebar files: Markdown, highlighted code, PDF, HTML, and plain text", + "description": "Extensible document previews for Sidebar files: Markdown, highlighted code, images, PDF, HTML, and plain text", "version": "0.1.5-alpha.1", "publishConfig": { "access": "public" diff --git a/packages/client/ui-sidebar-documentpreview/src/client/image/ImageBody.module.css b/packages/client/ui-sidebar-documentpreview/src/client/image/ImageBody.module.css new file mode 100644 index 0000000000..b26bb81a61 --- /dev/null +++ b/packages/client/ui-sidebar-documentpreview/src/client/image/ImageBody.module.css @@ -0,0 +1,40 @@ +.frame { + display: flex; + box-sizing: border-box; + width: max-content; + min-width: 100%; + height: max-content; + min-height: 100%; + font-family: var(--dsw-font, sans-serif); + white-space: normal; +} + +.image { + display: block; + flex: none; + width: auto; + max-width: none; + height: auto; + max-height: none; + margin: auto; + user-select: none; +} + +.image[hidden] { + display: none; +} + +.status { + display: flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + width: 100%; + min-height: 100%; + margin: 0; + padding: 10px; + color: var(--dsw-alias-label-secondary); + font-size: 13px; + line-height: 1.5; + white-space: normal; +} diff --git a/packages/client/ui-sidebar-documentpreview/src/client/image/ImageBody.tsx b/packages/client/ui-sidebar-documentpreview/src/client/image/ImageBody.tsx new file mode 100644 index 0000000000..f01ec88d1c --- /dev/null +++ b/packages/client/ui-sidebar-documentpreview/src/client/image/ImageBody.tsx @@ -0,0 +1,106 @@ +/** Complete image bytes rendered at their intrinsic CSS-pixel dimensions. */ +import { useEffect, useMemo, useState, type ReactNode } from 'react' +import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots' +import { pathPartsOf } from '@deepseek-ai/dsh-util-workspace-path' +import type { DocumentPreviewProps } from '../document/contract.ts' +import { LoadingIndicator } from '../LoadingIndicator.tsx' +import { hostFileOf } from '../rpc.ts' +import type {} from './locales.ts' +import css from './ImageBody.module.css' + +const IMAGE_MEDIA_TYPES = { + png: 'image/png', + jpg: 'image/jpeg', + jpeg: 'image/jpeg', + gif: 'image/gif', + webp: 'image/webp', + bmp: 'image/bmp', + ico: 'image/x-icon', + svg: 'image/svg+xml', +} as const + +type ImageMediaType = typeof IMAGE_MEDIA_TYPES[keyof typeof IMAGE_MEDIA_TYPES] + +/** Standard document props plus the image renderer's dictionary. */ +export type ImageBodyProps = DocumentPreviewProps & PropsLocale<'sidebarImage'> + +type ImageSource = + | { + readonly kind: 'ready' + readonly data: Uint8Array + readonly mediaType: ImageMediaType + readonly url: string + } + | { readonly kind: 'failed'; readonly data: Uint8Array; readonly mediaType: ImageMediaType } + +/** + * Resolve a supported filename to the media type assigned to its Blob. + * @param path - decoded workspace file path. + * @returns the image media type, or undefined for an unregistered suffix. + */ +export function imageMediaType(path: string): ImageMediaType | undefined { + const normalized = path.replaceAll('\\', '/') + const name = normalized.slice(normalized.lastIndexOf('/') + 1).toLowerCase() + const extension = name.slice(name.lastIndexOf('.') + 1) as keyof typeof IMAGE_MEDIA_TYPES + return IMAGE_MEDIA_TYPES[extension] +} + +/** + * Present complete image bytes without fitting or scaling them to the pane. + * @param props - document bytes, resource identity, and locale. + * @returns an intrinsic-size image whose containing document body provides scrolling. + */ +export function ImageBody({ content, resourceAddress, t }: ImageBodyProps): ReactNode { + const path = useMemo(() => hostFileOf(resourceAddress).path, [resourceAddress]) + const mediaType = imageMediaType(path) + const data = content.kind === 'bytes' ? content.data : undefined + const [source, setSource] = useState() + + useEffect(() => { + if (data === undefined || mediaType === undefined) return + let url: string | undefined + try { + url = URL.createObjectURL(new Blob([data], { type: mediaType })) + setSource({ kind: 'ready', data, mediaType, url }) + } catch { + setSource({ kind: 'failed', data, mediaType }) + } + return () => { + if (url !== undefined) URL.revokeObjectURL(url) + } + }, [data, mediaType]) + + if (data === undefined || mediaType === undefined) { + return

{t('unsupported')}

+ } + if (source?.data !== data || source.mediaType !== mediaType) { + return + } + if (source.kind === 'failed') return

{t('failed')}

+ const { name } = pathPartsOf(path) + return +} + +/** SVG stays in the browser's static image mode because its bytes only reach an img Blob URL. */ +function LoadedImage({ url, name, t }: { + readonly url: string + readonly name: string + readonly t: ImageBodyProps['t'] +}): ReactNode { + const [state, setState] = useState<'loading' | 'ready' | 'failed'>('loading') + return
+ {state === 'loading' && } + {state === 'failed' &&

{t('failed')}

} + { setState('ready') }} + onError={() => { setState('failed') }} + /> +
+} diff --git a/packages/client/ui-sidebar-documentpreview/src/client/image/index.ts b/packages/client/ui-sidebar-documentpreview/src/client/image/index.ts new file mode 100644 index 0000000000..28586d1398 --- /dev/null +++ b/packages/client/ui-sidebar-documentpreview/src/client/image/index.ts @@ -0,0 +1,41 @@ +/** Builtin image metadata and keyed document-body registration. */ +import type { Context } from '@deepseek-ai/cordis' +import type {} from '../index.ts' +import type { DocumentPreviewDefinition } from '../document/registry.ts' +import { ImageBody } from './ImageBody.tsx' +import { en, zh } from './locales.ts' + +/** Image implementation identity, shared by metadata and the keyed slot. */ +export const IMAGE_BODY_ID = '@deepseek-ai/dsh-client-ui-sidebar-documentpreview/image' + +/** File suffixes rendered by the builtin image body. */ +export const IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'ico', 'svg'] as const + +/** + * Describe the builtin image renderer independently from its keyed body slot. + * @param title - locale-owned implementation name. + * @returns metadata for complete image files. + */ +export function imageBodyDefinition(title: () => string): DocumentPreviewDefinition { + return { + id: IMAGE_BODY_ID, + extensions: IMAGE_EXTENSIONS, + priority: 'builtin', + title, + loading: 'bytes-complete', + wrap: false, + } +} + +/** + * Register the image dictionary, metadata, and body with reversible effects. + * @param ctx - owning plugin context. + */ +export function apply(ctx: Context): void { + const t = ctx.locale.bind('sidebarImage') + ctx.effect(() => ctx.locale.register('sidebarImage', { zh, en }), 'document-image: dictionaries') + ctx.effect(() => ctx.documentPreviews.register(imageBodyDefinition(() => t('title'))), 'document-image: metadata') + ctx.effect(() => ctx.slots.inject('sidebar.right.tab.document', () => ctx.slots.register( + { name: 'sidebar.right.tab.document', key: IMAGE_BODY_ID, locale: 'sidebarImage' }, ImageBody, + )), 'document-image: body') +} diff --git a/packages/client/ui-sidebar-documentpreview/src/client/image/locales.ts b/packages/client/ui-sidebar-documentpreview/src/client/image/locales.ts new file mode 100644 index 0000000000..9859985ac8 --- /dev/null +++ b/packages/client/ui-sidebar-documentpreview/src/client/image/locales.ts @@ -0,0 +1,27 @@ +/** Locale-owned image renderer labels and status text. */ +export const zh = { + title: '图片', + preview: '图片预览:{name}', + loading: '正在打开图片…', + failed: '无法显示这张图片。', + unsupported: '图片预览需要完整文件内容。', +} satisfies Record + +/** Image renderer dictionary keys. */ +export type ImagePreviewKey = keyof typeof zh + +/** English dictionary with the same keys as the Chinese dictionary. */ +export const en = { + title: 'Image', + preview: 'Image preview: {name}', + loading: 'Opening image…', + failed: 'This image could not be displayed.', + unsupported: 'Image preview requires the complete file contents.', +} satisfies Record + +declare module '@deepseek-ai/dsh-client-ui-slots' { + interface LocaleNamespaceMap { + /** Image preview selection, accessible name, and status text. */ + sidebarImage: ImagePreviewKey + } +} diff --git a/packages/client/ui-sidebar-documentpreview/src/client/index.ts b/packages/client/ui-sidebar-documentpreview/src/client/index.ts index 1e25723c13..ef76084e07 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/index.ts +++ b/packages/client/ui-sidebar-documentpreview/src/client/index.ts @@ -33,6 +33,7 @@ import { documentTabInfoFactory } from './document/contract.ts' import { apply as registerText } from './text/index.ts' import { apply as registerMarkdown } from './markdown/index.ts' import { apply as registerHtml } from './html/index.ts' +import { apply as registerImage } from './image/index.ts' import { apply as registerPdf } from './pdf/index.ts' import { apply as registerCode } from './code/index.ts' @@ -111,6 +112,7 @@ export function apply(ctx: ClientContext): void { registerText(ctx) registerMarkdown(ctx) registerHtml(ctx) + registerImage(ctx) registerPdf(ctx) registerCode(ctx) } diff --git a/packages/client/ui-sidebar-documentpreview/tests/apply.client.spec.ts b/packages/client/ui-sidebar-documentpreview/tests/apply.client.spec.ts index 649f4ba868..c6a3093b03 100644 --- a/packages/client/ui-sidebar-documentpreview/tests/apply.client.spec.ts +++ b/packages/client/ui-sidebar-documentpreview/tests/apply.client.spec.ts @@ -21,6 +21,8 @@ import { MarkdownBody } from '../src/client/markdown/MarkdownBody.tsx' import { MARKDOWN_BODY_ID } from '../src/client/markdown/index.ts' import { HtmlBody } from '../src/client/html/HtmlBody.tsx' import { HTML_BODY_ID } from '../src/client/html/index.ts' +import { ImageBody } from '../src/client/image/ImageBody.tsx' +import { IMAGE_BODY_ID } from '../src/client/image/index.ts' import { PdfBody } from '../src/client/pdf/PdfBody.tsx' import { PDF_BODY_ID } from '../src/client/pdf/index.ts' import { CodeBody } from '../src/client/code/CodeBody.tsx' @@ -93,6 +95,7 @@ describe('ui-sidebar-documentpreview apply', () => { ['sidebar.right.tab.document', PLAIN_BODY_ID, undefined, TextBody], ['sidebar.right.tab.document', MARKDOWN_BODY_ID, 'documentMarkdown', MarkdownBody], ['sidebar.right.tab.document', HTML_BODY_ID, 'documentHtml', HtmlBody], + ['sidebar.right.tab.document', IMAGE_BODY_ID, 'sidebarImage', ImageBody], ['sidebar.right.tab.document', PDF_BODY_ID, 'sidebarPdf', PdfBody], ['sidebar.right.tab.document', '@deepseek-ai/dsh-client-ui-sidebar-documentpreview/code', 'sidebarCodePreview', CodeBody], ]) diff --git a/packages/client/ui-sidebar-documentpreview/tests/image-body.client.spec.tsx b/packages/client/ui-sidebar-documentpreview/tests/image-body.client.spec.tsx new file mode 100644 index 0000000000..377f93514a --- /dev/null +++ b/packages/client/ui-sidebar-documentpreview/tests/image-body.client.spec.tsx @@ -0,0 +1,111 @@ +// @vitest-environment jsdom +/** Image Blob ownership, media types, intrinsic rendering, and failure states. */ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { cleanup, fireEvent, render, screen } from '@testing-library/react' +import type { SessionId } from '@deepseek-ai/dsh-session/types' +import { ImageBody, imageMediaType, type ImageBodyProps } from '../src/client/image/ImageBody.tsx' +import { en } from '../src/client/image/locales.ts' + +const translations: ReadonlyMap = new Map(Object.entries(en)) +let createDescriptor: PropertyDescriptor | undefined +let revokeDescriptor: PropertyDescriptor | undefined +const create = vi.fn<(blob: Blob) => string>() +const revoke = vi.fn<(url: string) => void>() + +beforeEach(() => { + createDescriptor = Object.getOwnPropertyDescriptor(URL, 'createObjectURL') + revokeDescriptor = Object.getOwnPropertyDescriptor(URL, 'revokeObjectURL') + create.mockReset().mockImplementation(() => `blob:https://preview.invalid/${create.mock.calls.length}`) + revoke.mockReset() + Object.defineProperty(URL, 'createObjectURL', { configurable: true, value: create }) + Object.defineProperty(URL, 'revokeObjectURL', { configurable: true, value: revoke }) +}) + +afterEach(() => { + try { cleanup() } finally { + if (createDescriptor === undefined) Reflect.deleteProperty(URL, 'createObjectURL') + else Object.defineProperty(URL, 'createObjectURL', createDescriptor) + if (revokeDescriptor === undefined) Reflect.deleteProperty(URL, 'revokeObjectURL') + else Object.defineProperty(URL, 'revokeObjectURL', revokeDescriptor) + } +}) + +function props(path = 'asset.png', data: Uint8Array = new Uint8Array([1, 2, 3])): ImageBodyProps { + return { + resourceAddress: `dsh-resource://file/session/image/${path}`, + content: { kind: 'bytes', data }, + wrap: false, + sessionId: 'image' as SessionId, + useTabInfo: () => ({ tab: { signal: new AbortController().signal } }), + useResource: () => ({ value: undefined }), + t: (key, params) => { + const value = translations.get(key) ?? key + return params === undefined ? value : value.replace('{name}', String(params.name)) + }, + } as ImageBodyProps +} + +describe('ImageBody', () => { + it.each([ + ['png', 'image/png'], + ['jpg', 'image/jpeg'], + ['jpeg', 'image/jpeg'], + ['gif', 'image/gif'], + ['webp', 'image/webp'], + ['bmp', 'image/bmp'], + ['ico', 'image/x-icon'], + ['svg', 'image/svg+xml'], + ] as const)('assigns .%s bytes the %s Blob media type', async (extension, mediaType) => { + const view = render() + const image = await screen.findByRole('img', { hidden: true }) + expect(create.mock.calls[0]?.[0].type).toBe(mediaType) + expect(image.getAttribute('src')).toBe('blob:https://preview.invalid/1') + view.unmount() + expect(revoke).toHaveBeenCalledExactlyOnceWith('blob:https://preview.invalid/1') + }) + + it('keeps the image unscaled and non-draggable, then shows it after decoding succeeds', async () => { + render() + const image = await screen.findByRole('img', { hidden: true }) + expect(image.getAttribute('alt')).toBe('Image preview: photo.svg') + expect(image.getAttribute('decoding')).toBe('async') + expect(image.getAttribute('draggable')).toBe('false') + expect(image.getAttribute('referrerpolicy')).toBe('no-referrer') + expect(image.hasAttribute('hidden')).toBe(true) + fireEvent.load(image) + expect(image.hasAttribute('hidden')).toBe(false) + expect(screen.queryByRole('status')).toBeNull() + }) + + it('revokes replaced bytes and reports image decode and Blob creation failures', async () => { + const initial = props() + const view = render() + const first = await screen.findByRole('img', { hidden: true }) + fireEvent.error(first) + expect(screen.getByRole('alert').textContent).toBe(en.failed) + const changed = props('asset.png', new Uint8Array([4, 5, 6])) + view.rerender() + await screen.findByRole('img', { hidden: true }) + expect(revoke).toHaveBeenCalledWith('blob:https://preview.invalid/1') + create.mockImplementationOnce(() => { throw new Error('Blob unavailable') }) + view.rerender() + expect((await screen.findByRole('alert')).textContent).toBe(en.failed) + view.unmount() + expect(revoke).toHaveBeenCalledWith('blob:https://preview.invalid/2') + }) + + it('rejects text delivery and an unregistered suffix without creating a Blob', () => { + const initial = props() + const view = render() + expect(screen.getByRole('alert').textContent).toBe(en.unsupported) + view.rerender() + expect(screen.getByRole('alert').textContent).toBe(en.unsupported) + expect(create).not.toHaveBeenCalled() + }) + + it('matches media types case-insensitively on decoded path suffixes', () => { + expect(imageMediaType('folder/PHOTO.JPEG')).toBe('image/jpeg') + expect(imageMediaType('folder/no-extension')).toBeUndefined() + expect(imageMediaType('folder/photo.avif')).toBeUndefined() + }) +}) diff --git a/packages/client/ui-sidebar-documentpreview/tests/image-registration.client.spec.ts b/packages/client/ui-sidebar-documentpreview/tests/image-registration.client.spec.ts new file mode 100644 index 0000000000..eb7ac01dd1 --- /dev/null +++ b/packages/client/ui-sidebar-documentpreview/tests/image-registration.client.spec.ts @@ -0,0 +1,63 @@ +/** Image metadata, keyed slot, dictionary, and disposal registration. */ +import { Context } from '@deepseek-ai/cordis' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { DocumentPreviewRegistry } from '../src/client/document/registry.ts' +import { ImageBody } from '../src/client/image/ImageBody.tsx' +import { apply, IMAGE_BODY_ID, IMAGE_EXTENSIONS, imageBodyDefinition } from '../src/client/image/index.ts' +import { en, zh } from '../src/client/image/locales.ts' + +let dispose: (() => Promise) | undefined +afterEach(async () => { await dispose?.(); dispose = undefined }) + +describe('image registration', () => { + it('claims common image suffixes as a builtin complete-byte renderer without wrap', () => { + const title = vi.fn(() => 'localized image') + const definition = imageBodyDefinition(title) + expect(definition).toEqual({ + id: IMAGE_BODY_ID, + extensions: IMAGE_EXTENSIONS, + priority: 'builtin', + title, + loading: 'bytes-complete', + wrap: false, + }) + expect(title).not.toHaveBeenCalled() + expect(definition.title()).toBe('localized image') + }) + + it('registers its dictionary and matching keyed body, then removes every contribution', async () => { + const ctx = new Context() + const registry = new DocumentPreviewRegistry() + const dictionaries = new Map() + const bodies = new Map() + const register = vi.fn((options: { key: string }, body: unknown) => { + bodies.set(options.key, body) + return () => { bodies.delete(options.key) } + }) + ctx.provide('documentPreviews', registry) + ctx.provide('slots', { inject: (_key: string, callback: () => () => void) => callback(), register } as never) + ctx.provide('locale', { + bind: () => (key: keyof typeof en) => en[key], + register: (name: string, value: unknown) => { + dictionaries.set(name, value) + return () => { dictionaries.delete(name) } + }, + } as never) + const fiber = ctx.plugin({ apply }) + dispose = async () => { await fiber.dispose() } + await fiber.await() + for (const extension of IMAGE_EXTENSIONS) { + expect(registry.candidates(`ASSET.${extension}`).map(entry => entry.id)).toEqual([IMAGE_BODY_ID]) + } + expect(registry.getSnapshot()[0]?.title()).toBe(en.title) + expect(dictionaries.get('sidebarImage')).toEqual({ zh, en }) + expect(register).toHaveBeenCalledExactlyOnceWith( + { name: 'sidebar.right.tab.document', key: IMAGE_BODY_ID, locale: 'sidebarImage' }, + ImageBody, + ) + await dispose() + expect(registry.getSnapshot()).toEqual([]) + expect(bodies.size).toBe(0) + expect(dictionaries.size).toBe(0) + }) +}) diff --git a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts index 0945d530e8..c0299970cf 100644 --- a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts +++ b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts @@ -2356,6 +2356,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ occupants: [ 'client-ui-sidebar-documentpreview CodeBody', 'client-ui-sidebar-documentpreview HtmlBody', + 'client-ui-sidebar-documentpreview ImageBody', 'client-ui-sidebar-documentpreview MarkdownBody', 'client-ui-sidebar-documentpreview PdfBody', 'client-ui-sidebar-documentpreview TextBody', From 58956c1a8a3643677fe2820a8a40f3aee5612b39 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Wed, 9 Sep 2026 22:01:10 +0800 Subject: [PATCH 13/27] Revert "feat(web): preview Mermaid, Graphviz, SVG, and HTML code fences" --- ...9-content-sized-diagram-previews.i18n.yaml | 6 - ...26-09-09-content-sized-diagram-previews.md | 25 - ...09-09-content-sized-diagram-previews.zh.md | 25 - ...6-09-09-markdown-static-previews.i18n.yaml | 6 - .../2026-09-09-markdown-static-previews.md | 43 -- .../2026-09-09-markdown-static-previews.zh.md | 43 -- THIRD_PARTY_NOTICES.md | 6 +- .../expected/markdown-mermaid/ui.expected.md | 24 - .../expected/markdown-mermaid/zh.expected.md | 23 - apps/web/tests/markdown-mermaid.e2e.ts | 320 -------- apps/web/tsconfig.json | 1 - apps/web/vite.config.ts | 16 +- packages/client/locale/src/locales/en.ts | 10 - packages/client/locale/src/locales/zh.ts | 10 - packages/client/ui-chat/README.i18n.yaml | 4 +- packages/client/ui-chat/README.md | 8 - packages/client/ui-chat/README.zh.md | 8 - .../ui-chat/src/client/markdown-labels.ts | 13 - .../client/ui-primitives/README.i18n.yaml | 4 +- packages/client/ui-primitives/README.md | 10 - packages/client/ui-primitives/README.zh.md | 10 - .../THIRD_PARTY_PREVIEW_NOTICES.txt | 688 ------------------ packages/client/ui-primitives/package.json | 6 +- .../src/markdown/CodeBlock.module.css | 84 --- .../ui-primitives/src/markdown/CodeBlock.tsx | 53 +- .../src/markdown/SourcePreview.module.css | 35 - .../src/markdown/SourcePreview.tsx | 71 -- .../ui-primitives/src/markdown/graphviz.ts | 37 - .../ui-primitives/src/markdown/mermaid.ts | 97 --- .../src/markdown/preview-document.ts | 44 -- .../src/markdown/preview-theme.ts | 27 - .../ui-primitives/src/markdown/render.tsx | 30 - .../tests/code-block.client.spec.tsx | 2 +- .../tests/fence-previews.client.spec.tsx | 126 ---- .../tests/graphviz-runtime.client.spec.ts | 67 -- .../tests/mermaid-runtime.client.spec.ts | 133 ---- .../tests/source-preview.client.spec.tsx | 162 ----- pnpm-lock.yaml | 14 - scripts/check-workspace-constraints.spec.ts | 14 - scripts/check-workspace-constraints.ts | 2 +- scripts/gen-third-party-notices.spec.ts | 24 - scripts/gen-third-party-notices.ts | 34 - tsconfig.host.json | 1 - 43 files changed, 20 insertions(+), 2346 deletions(-) delete mode 100644 .agents/notes/implemented/bug-fix/2026-09-09-content-sized-diagram-previews.i18n.yaml delete mode 100644 .agents/notes/implemented/bug-fix/2026-09-09-content-sized-diagram-previews.md delete mode 100644 .agents/notes/implemented/bug-fix/2026-09-09-content-sized-diagram-previews.zh.md delete mode 100644 .agents/notes/implemented/feature/2026-09-09-markdown-static-previews.i18n.yaml delete mode 100644 .agents/notes/implemented/feature/2026-09-09-markdown-static-previews.md delete mode 100644 .agents/notes/implemented/feature/2026-09-09-markdown-static-previews.zh.md delete mode 100644 apps/web/tests/expected/markdown-mermaid/ui.expected.md delete mode 100644 apps/web/tests/expected/markdown-mermaid/zh.expected.md delete mode 100644 apps/web/tests/markdown-mermaid.e2e.ts delete mode 100644 packages/client/ui-primitives/THIRD_PARTY_PREVIEW_NOTICES.txt delete mode 100644 packages/client/ui-primitives/src/markdown/SourcePreview.module.css delete mode 100644 packages/client/ui-primitives/src/markdown/SourcePreview.tsx delete mode 100644 packages/client/ui-primitives/src/markdown/graphviz.ts delete mode 100644 packages/client/ui-primitives/src/markdown/mermaid.ts delete mode 100644 packages/client/ui-primitives/src/markdown/preview-document.ts delete mode 100644 packages/client/ui-primitives/src/markdown/preview-theme.ts delete mode 100644 packages/client/ui-primitives/tests/fence-previews.client.spec.tsx delete mode 100644 packages/client/ui-primitives/tests/graphviz-runtime.client.spec.ts delete mode 100644 packages/client/ui-primitives/tests/mermaid-runtime.client.spec.ts delete mode 100644 packages/client/ui-primitives/tests/source-preview.client.spec.tsx diff --git a/.agents/notes/implemented/bug-fix/2026-09-09-content-sized-diagram-previews.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-09-09-content-sized-diagram-previews.i18n.yaml deleted file mode 100644 index be4173fd02..0000000000 --- a/.agents/notes/implemented/bug-fix/2026-09-09-content-sized-diagram-previews.i18n.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# side as of the last confirmed-consistent state. Both languages carry equal authority; -# after editing either side, bring the other along and re-record with: -# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-09-09-content-sized-diagram-previews.md -2026-09-09-content-sized-diagram-previews.md: ceb29238eec09f2dde312835dbf376855998a507 -2026-09-09-content-sized-diagram-previews.zh.md: 2689602bfe3469f53e532367877f7e49d52d3d5d diff --git a/.agents/notes/implemented/bug-fix/2026-09-09-content-sized-diagram-previews.md b/.agents/notes/implemented/bug-fix/2026-09-09-content-sized-diagram-previews.md deleted file mode 100644 index ceb29238ee..0000000000 --- a/.agents/notes/implemented/bug-fix/2026-09-09-content-sized-diagram-previews.md +++ /dev/null @@ -1,25 +0,0 @@ -# Agent Note: Content-sized diagram previews - -Status: implemented - -English | [中文](2026-09-09-content-sized-diagram-previews.zh.md) - -## Problem - -Short Graphviz and SVG images leave large empty regions when displayed inside 400px frames. A 59px Graphviz diagram occupies a 400px preview even though its rendered dimensions are already available to the browser. - -## Decision - -Mermaid, Graphviz, and SVG share the inert image canvas in `SourcePreview`. The browser derives its height from the image, preserves aspect ratio when shrinking to the available width, and adds 16px padding. Source switching retains the image and copying reads the original code. Theme changes regenerate Graphviz colors without introducing a separate sizing observer. - -This replaces the diagram-frame choice in [Static Markdown fence previews](../feature/2026-09-09-markdown-static-previews.md); that note continues to own HTML isolation, rendering lifecycle, and license obligations. HTML retains its opaque, script-free, 400px frame. SVG image mode disables scripts, link interaction, and external resource loading without inserting source-controlled markup into the application document. - -## Alternatives considered - -**Set iframe height to auto.** An iframe does not derive its outer height from its inner document. This still reserves an unrelated viewport. - -**Add a frame measurement script or same-origin access.** A diagram already has image dimensions. Extra execution or origin permissions and resize messaging are unnecessary for that content. - -## Consequences - -Short diagrams occupy only their rendered height and canvas padding. The browser geometry regression compares image and container height in light and dark modes, then verifies proportional scaling in a narrow viewport. Browser security checks include SVG scripts, event handlers, nested HTML, and external images; copy and source toggling remain covered. HTML automatic height remains outside this change because arbitrary document layout has different measurement and isolation requirements. diff --git a/.agents/notes/implemented/bug-fix/2026-09-09-content-sized-diagram-previews.zh.md b/.agents/notes/implemented/bug-fix/2026-09-09-content-sized-diagram-previews.zh.md deleted file mode 100644 index 2689602bfe..0000000000 --- a/.agents/notes/implemented/bug-fix/2026-09-09-content-sized-diagram-previews.zh.md +++ /dev/null @@ -1,25 +0,0 @@ -# Agent Note: 按内容定高的图表预览 - -Status: implemented - -[English](2026-09-09-content-sized-diagram-previews.md) | 中文 - -## Problem - -较矮的 Graphviz 与 SVG 图片显示在 400px iframe 中时,会留下大块空白。浏览器已经知道图片的渲染尺寸,但一张 59px 高的 Graphviz 图表仍占据 400px 的预览区域。 - -## Decision - -Mermaid、Graphviz 与 SVG 共享 `SourcePreview` 的不可执行图片画布。浏览器根据图片决定高度,在可用宽度不足时按比例缩小,并添加 16px 内边距。源码切换保留图片,复制读取原始代码。主题变化重新生成 Graphviz 配色,无需单独的尺寸观察器。 - -这替代了[静态 Markdown fence 预览](../feature/2026-09-09-markdown-static-previews.zh.md)中的图表 iframe 选择;该记录继续维护 HTML 隔离、渲染生命周期与许可证义务。HTML 保留不透明来源、禁用脚本的 400px iframe。SVG 图片模式禁用脚本、链接交互与外部资源加载,无需把源码控制的标记插入应用文档。 - -## Alternatives considered - -**把 iframe 高度设为 auto。** iframe 不会根据内部文档决定外部高度,仍然会保留与内容无关的视口。 - -**添加 iframe 测量脚本或同源访问。** 图表已经具备图片尺寸,不需要额外执行权限、来源权限或尺寸消息协议。 - -## Consequences - -较矮图表只占据渲染高度与画布内边距。浏览器几何回归用例在浅色和深色下比较图片与容器高度,再验证窄视口中的等比例缩放。浏览器安全检查包含 SVG 脚本、事件处理器、嵌套 HTML 与外部图片,并保留复制和源码切换覆盖。HTML 自动高度不属于本次改动,因为任意文档布局具有不同的测量与隔离要求。 diff --git a/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.i18n.yaml b/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.i18n.yaml deleted file mode 100644 index 8a24afcd59..0000000000 --- a/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.i18n.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# side as of the last confirmed-consistent state. Both languages carry equal authority; -# after editing either side, bring the other along and re-record with: -# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-09-markdown-static-previews.md -2026-09-09-markdown-static-previews.md: f3a0363e3d7c4cb19ca67dfc5011e936f7c024cf -2026-09-09-markdown-static-previews.zh.md: 24f87cd73504c1fbd274e1420a3692b3ca55ed5e diff --git a/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.md b/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.md deleted file mode 100644 index f3a0363e3d..0000000000 --- a/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.md +++ /dev/null @@ -1,43 +0,0 @@ -# Agent Note: Static Markdown fence previews - -Status: implemented - -English | [中文](2026-09-09-markdown-static-previews.zh.md) - -## Problem - -Readers need to see Mermaid and DOT diagrams, SVG artwork, and HTML examples directly in Assistant replies without interpreting source or copying it to another renderer. These sources are untrusted, and a renderer's npm license field may omit compiled components with separate distribution obligations. - -## Decision - -The shared Markdown renderer enables settled `mermaid`, `graphviz`/`dot`, `svg`, and `html` fences through localized `MarkdownLabels.preview` and the parsed fence language. The [UI primitives package](../../../../packages/client/ui-primitives/README.md) owns `SourcePreview`, which accepts a renderer, source, and labels without Session, file, or Cordis dependencies. Mermaid and Graphviz each supply a `.ts` renderer to that same component. Consumers without preview labels and streaming messages retain code. - -`CodeBlock.preview` owns default visualization, source switching, and verbatim copying. Previews omit the language banner and expose compact icon actions on hover or keyboard focus; devices with any touch input keep actions visible below the diagram. Switching to source hides the mounted preview, preserving completed rendering and pending work. Both views use the same focused toggle button, and copying always reads the source prop. - -`SourcePreview` owns loading, failure, and cancellation of stale result publication. Source replacement and unmounting cancel publication; cancellation before runtime loading completes skips layout. Failures show a localized error and the original source, and replacing invalid source with valid input recovers the preview. - -Mermaid loads on demand. A shared queue serializes theme initialization with diagram work, and each call removes its temporary measurement DOM in `finally`. Strict security, disabled HTML labels, the application palette, and error-rendering policy cannot be overridden by diagram configuration. Generated SVG is displayed as an image without installing links or scripts. Intrinsic dimensions come from the SVG viewBox; large diagrams shrink to fit, and the canvas follows the code-block background. Mounted previews observe document theme attributes and regenerate only when resolved colors change; obsolete renders cannot publish. Graphviz default colors follow the same palette, while authored DOT, SVG, and HTML colors remain intact. - -HTML uses DOMPurify with navigation attributes and document-loading elements forbidden, followed by an opaque `sandbox=""` iframe. A trusted CSP precedes source markup and permits only inline CSS, data images, and data fonts. Scripts, external resources, child frames, form submission, and same-origin access are unavailable. SVG is parsed as XML and displayed with Graphviz output as inert images in a [content-sized canvas](../bug-fix/2026-09-09-content-sized-diagram-previews.md), so SVG scripts and links never become active. HTML frames have a fixed scrollable viewport: measuring their content would require additional origin access or trusted frame scripts. - -Graphviz uses the pinned, unmodified `@viz-js/viz` 3.30.0 WebAssembly distribution with the `dot` layout engine. Its npm MIT declaration covers the wrapper; its build provenance identifies Graphviz 16.0.0 (EPL-2.0), Expat 2.8.4 (MIT), and Emscripten 5.0.7 (MIT/NCSA). Distribution retains these component terms and the exact Graphviz source download under EPL-2.0 section 3.1. [Full preview notices](../../../../packages/client/ui-primitives/THIRD_PARTY_PREVIEW_NOTICES.txt) also retain Mermaid's MIT text and select Apache-2.0 for DOMPurify. The UI primitives package ships the notices and the Web build emits the same bytes alongside its assets. License review covers embedded payloads separately from the general permissive npm-metadata policy. - -## Alternatives considered - -**Keep the code banner above diagrams.** A language label and permanent text controls distract from the diagram. Overlay actions retain source access without reserving a title row. - -**Render each streamed chunk.** Incomplete diagrams are frequently invalid, and repeated layout work competes with text streaming. Message settlement provides complete source. - -**Put rendering inside Chat or add a general preview registry.** A shared primitive with plain props satisfies reuse without another registry or feature-plugin dependency. This follows the [shared-control rule](../architecture/2026-09-05-shared-client-control-primitives.md). - -**Execute HTML scripts in a frame.** Static chat examples do not need script execution. An empty sandbox plus sanitization and CSP gives the preview fewer capabilities and avoids introducing a frame messaging protocol. - -**Insert rendered markup into Chat.** The requested preview needs diagram display and source access; HTML styles and executable SVG would share the application document. Iframes isolate layout and origin; SVG image mode additionally disables SVG behavior. - -**Treat Viz.js as MIT-only or use a remote Graphviz service.** The compiled Graphviz license still applies locally; a remote renderer would send conversation content off-device. The bundled renderer retains source availability and legal notices without network rendering. - -## Consequences - -The feature changes presentation without changing persisted messages, provider requests, tools, or Host APIs. Inline HTML styles work, while interactive scripts, external assets, and links do not. Mermaid and Graphviz add lazy browser assets and run layout on the browser thread. Cancellation cannot preempt active layout; Mermaid finishes and releases its measurement DOM even when its result cannot be published. The previews have no editing, export, zoom controls, or interactive diagram links. - -Component tests cover source copying, default preview, delayed completion, stale success and failure, unmounting, fallback, cancellation, runtime loading failure, and recovery. The keyless [browser scenario](../../../../apps/web/tests/markdown-mermaid.e2e.ts) verifies Chinese Mermaid flowcharts, sequence diagrams, malformed source, configuration overrides, opaque frames, real image decoding, source toggling, blocked script/navigation/resource attempts, English/Chinese UI snapshots, and served license text. Notices checks pin the reviewed wrapper and native provenance so upgrades require renewed review. diff --git a/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.zh.md b/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.zh.md deleted file mode 100644 index 24f87cd735..0000000000 --- a/.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.zh.md +++ /dev/null @@ -1,43 +0,0 @@ -# Agent Note: 静态 Markdown fence 预览 - -Status: implemented - -[English](2026-09-09-markdown-static-previews.md) | 中文 - -## Problem - -读者需要直接在 Assistant 回复中查看 Mermaid 和 DOT 图表、SVG 图像和 HTML 示例,而不必解读源码或复制到其他渲染器。这些源码不可信,而渲染器的 npm 许可证字段可能遗漏编译组件各自的分发义务。 - -## Decision - -共享 Markdown 渲染器通过本地化的 `MarkdownLabels.preview` 和解析后的 fence 语言启用定稿后的 `mermaid`、`graphviz`/`dot`、`svg` 和 `html` fence。[UI primitives 包](../../../../packages/client/ui-primitives/README.zh.md)拥有 `SourcePreview`,它接收渲染器、源码和文案,不依赖 Session、文件或 Cordis。Mermaid 与 Graphviz 各自向同一组件提供 `.ts` 渲染器。未提供预览文案的调用方与流式消息保留代码显示。 - -`CodeBlock.preview` 负责默认可视化、源码切换与原样复制。预览省略语言横幅,在悬停或键盘聚焦时显示紧凑的图标操作;具有任何触控输入的设备始终在图表下方显示操作。切换到源码会隐藏已挂载的预览,保留完成的渲染和待完成的工作。两种视图使用同一个保持焦点的切换按钮,复制始终读取源码属性。 - -`SourcePreview` 负责加载、失败和取消过期结果发布。替换源码和卸载组件会取消结果发布;在运行时加载完成前取消会跳过布局。失败时显示本地化错误与原始源码,用有效输入替换无效源码后可以恢复预览。 - -Mermaid 按需加载。共享队列将主题初始化与图表渲染一起串行执行,每次调用都在 `finally` 中删除临时测量 DOM。图表配置无法覆盖严格安全模式、禁用 HTML 标签、应用配色和错误渲染策略。生成的 SVG 作为图片显示,不安装链接或脚本。固有尺寸来自 SVG viewBox;大图缩小以适应宽度,画布使用代码块背景。已挂载的预览观察文档主题属性,仅在解析后的配色变化时重新生成;过期渲染不能发布结果。Graphviz 的默认颜色采用同一配色,DOT、SVG 和 HTML 中明确指定的颜色保持原样。 - -HTML 先由 DOMPurify 清理,禁止导航属性与文档加载元素,再进入不透明来源的 `sandbox=""` iframe。可信 CSP 位于源码标记之前,仅允许内联 CSS、data 图片与 data 字体。脚本、外部资源、子 iframe、表单提交与同源访问均不可用。SVG 按 XML 解析后,与 Graphviz 输出一同作为不可执行图片显示在[按内容定高的画布](../bug-fix/2026-09-09-content-sized-diagram-previews.zh.md)中,因此 SVG 脚本与链接不会激活。HTML iframe 使用固定的可滚动视口:测量内容需要额外的来源访问权限或可信 iframe 脚本。 - -Graphviz 使用固定且未修改的 `@viz-js/viz` 3.30.0 WebAssembly 发布包,布局引擎为 `dot`。其 npm MIT 声明覆盖包装层;构建来源记录标明 Graphviz 16.0.0(EPL-2.0)、Expat 2.8.4(MIT)与 Emscripten 5.0.7(MIT/NCSA)。分发保留各组件条款,并按 EPL-2.0 第 3.1 节提供准确的 Graphviz 源码下载地址。[完整预览声明](../../../../packages/client/ui-primitives/THIRD_PARTY_PREVIEW_NOTICES.txt)还保留 Mermaid 的 MIT 文本,并为 DOMPurify 选择 Apache-2.0。UI primitives 包携带该声明,Web 构建在资源旁输出相同字节。内嵌产物的许可证检查独立于通用的宽松 npm 元数据策略。 - -## Alternatives considered - -**在图表上方保留代码横幅。** 语言标签和常驻文字控件会分散对图表的注意力。浮层操作保留源码访问,不占用标题行。 - -**渲染每个流式片段。** 未完成的图表通常无效,反复布局会与文本流式输出争用资源。消息定稿可提供完整源码。 - -**在 Chat 内部渲染,或添加通用预览注册表。** 使用普通属性的共享基础组件即可满足复用,无需另加注册表或功能插件依赖。这遵循[共享控件规则](../architecture/2026-09-05-shared-client-control-primitives.zh.md)。 - -**在 iframe 内执行 HTML 脚本。** 静态聊天示例不需要脚本执行。空 sandbox、清理与 CSP 降低预览权限,也避免引入 iframe 消息协议。 - -**把渲染标记插入 Chat。** 所需预览只需显示图表和访问源码;HTML 样式和可执行 SVG 会与应用共享文档。iframe 隔离布局与来源,SVG 图片模式额外禁用 SVG 行为。 - -**将 Viz.js 视为仅使用 MIT,或使用远程 Graphviz 服务。** 本地编译的 Graphviz 仍受其许可证约束;远程渲染会把对话内容发送到设备之外。内置渲染器保留源码可获取性与法律声明,无需网络渲染。 - -## Consequences - -该功能改变呈现,不改变持久化消息、provider 请求、工具或 Host API。HTML 内联样式可用,交互脚本、外部资源和链接不可用。Mermaid 和 Graphviz 增加按需加载的浏览器资源,并在浏览器线程上执行布局。取消无法抢占进行中的布局;即使结果无法发布,Mermaid 仍会完成并释放测量 DOM。预览不提供编辑、导出、缩放控件或交互式图表链接。 - -组件测试覆盖源码复制、默认预览、延迟完成、过期成功与失败、卸载、回退、取消、运行时加载失败与恢复。无密钥[浏览器场景](../../../../apps/web/tests/markdown-mermaid.e2e.ts)验证中文 Mermaid 流程图、时序图、无效源码、配置覆盖、不透明来源 iframe、真实图片解码、源码切换、脚本/导航/资源请求被阻止、中英文 UI 快照和已提供的许可证文本。声明检查固定已审查的包装层与原生构建来源,升级时须重新审查。 diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 2452325d62..31eebdab8d 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -55,7 +55,6 @@ External packages installed for runtime use or distributed inside the prebuilt b | [`@shikijs/langs`](https://github.com/shikijs/shiki) | MIT | | [`@standard-schema/spec`](https://github.com/standard-schema/standard-schema) | MIT | | [`@tanstack/react-virtual`](https://github.com/TanStack/virtual) | MIT | -| [`@viz-js/viz`](https://github.com/mdaines/viz-js) | MIT | | [`@vscode/ripgrep`](https://github.com/microsoft/vscode-ripgrep) | MIT | | [`@xterm/headless`](https://github.com/xtermjs/xterm.js) | MIT | | [`@yarnpkg/parsers`](https://github.com/yarnpkg/berry) | BSD-2-Clause | @@ -67,7 +66,6 @@ External packages installed for runtime use or distributed inside the prebuilt b | [`commander`](https://github.com/tj/commander.js) | MIT | | [`compression`](https://github.com/expressjs/compression) | MIT | | [`diff`](https://github.com/kpdecker/jsdiff) | BSD-3-Clause | -| [`dompurify`](https://github.com/cure53/DOMPurify) | (MPL-2.0 OR Apache-2.0) | | [`e2b`](https://github.com/e2b-dev/e2b) | MIT | | [`electron-updater`](https://github.com/electron-userland/electron-builder) | MIT | | [`eventsource-parser`](https://github.com/rexxars/eventsource-parser) | MIT | @@ -81,7 +79,6 @@ External packages installed for runtime use or distributed inside the prebuilt b | [`mdast-util-from-markdown`](https://github.com/syntax-tree/mdast-util-from-markdown) | MIT | | [`mdast-util-gfm`](https://github.com/syntax-tree/mdast-util-gfm) | MIT | | [`mdast-util-math`](https://github.com/syntax-tree/mdast-util-math) | MIT | -| [`mermaid`](https://github.com/mermaid-js/mermaid) | MIT | | [`micromark-core-commonmark`](https://github.com/micromark/micromark/tree/main/packages/micromark-core-commonmark) | MIT | | [`micromark-extension-gfm`](https://github.com/micromark/micromark-extension-gfm) | MIT | | [`micromark-extension-math`](https://github.com/micromark/micromark-extension-math) | MIT | @@ -115,8 +112,6 @@ External packages installed for runtime use or distributed inside the prebuilt b | [`zod`](https://github.com/colinhacks/zod) | MIT | | [`zustand`](https://github.com/pmndrs/zustand) | MIT | -The Markdown preview distribution also contains Graphviz 16.0.0 (EPL-2.0), Expat 2.8.4 (MIT), and Emscripten 5.0.7 runtime code (MIT/NCSA) inside `@viz-js/viz` 3.30.0. The wrapper's MIT metadata does not relicense these components. [Preview notices](packages/client/ui-primitives/THIRD_PARTY_PREVIEW_NOTICES.txt) preserve their full license texts and Graphviz source availability, together with Mermaid's MIT and DOMPurify's selected Apache-2.0 terms. The UI primitives npm package includes this file; the Web build emits it as `preview-third-party-notices.txt`. - pnpm applies local patches to the following packages at install time, so shipped artifacts carry modified copies; each patch file is the complete record of the modification: - `@yao-pkg/pkg@6.21.0` — [`patches/@yao-pkg__pkg@6.21.0.patch`](patches/@yao-pkg__pkg@6.21.0.patch) @@ -194,6 +189,7 @@ External packages **directly declared** for development, tests, types, or toolin | [`jsdom`](https://github.com/jsdom/jsdom) | MIT | | [`lefthook`](https://github.com/evilmartians/lefthook) | MIT | | [`lightningcss`](https://github.com/parcel-bundler/lightningcss) | MPL-2.0 | +| [`mermaid`](https://github.com/mermaid-js/mermaid) | MIT | | [`micromark-util-types`](https://github.com/micromark/micromark/tree/main/packages/micromark-util-types) | MIT | | [`msgpackr`](http://github.com/kriszyp/msgpackr) | MIT | | [`oxlint`](https://github.com/oxc-project/oxc) | MIT | diff --git a/apps/web/tests/expected/markdown-mermaid/ui.expected.md b/apps/web/tests/expected/markdown-mermaid/ui.expected.md deleted file mode 100644 index 985f68fee0..0000000000 --- a/apps/web/tests/expected/markdown-mermaid/ui.expected.md +++ /dev/null @@ -1,24 +0,0 @@ -- heading "Mermaid previews" [level=1] -- button "Source" -- button "Copy" -- tooltip "Copy" -- img "Mermaid diagram" -- button "Source" -- button "Copy" -- img "Mermaid diagram" -- button "Source" -- button "Copy" -- status: Unable to render this diagram. The source is shown below. -- code: flowchart LR A[unfinished -- button "Source" -- button "Copy" -- img "Mermaid diagram" -- button "Source" -- button "Copy" -- img "Graphviz diagram" -- button "Source" -- button "Copy" -- img "SVG preview" -- button "Source" -- button "Copy" -- iframe diff --git a/apps/web/tests/expected/markdown-mermaid/zh.expected.md b/apps/web/tests/expected/markdown-mermaid/zh.expected.md deleted file mode 100644 index 52d7053ded..0000000000 --- a/apps/web/tests/expected/markdown-mermaid/zh.expected.md +++ /dev/null @@ -1,23 +0,0 @@ -- heading "Mermaid previews" [level=1] -- button "源码" -- button "复制" -- img "Mermaid 图表" -- button "源码" -- button "复制" -- img "Mermaid 图表" -- button "源码" -- button "复制" -- status: 无法渲染此图表,源码如下。 -- code: flowchart LR A[unfinished -- button "源码" -- button "复制" -- img "Mermaid 图表" -- button "源码" -- button "复制" -- img "Graphviz 图表" -- button "源码" -- button "复制" -- img "SVG 预览" -- button "源码" -- button "复制" -- iframe diff --git a/apps/web/tests/markdown-mermaid.e2e.ts b/apps/web/tests/markdown-mermaid.e2e.ts deleted file mode 100644 index 95249740bf..0000000000 --- a/apps/web/tests/markdown-mermaid.e2e.ts +++ /dev/null @@ -1,320 +0,0 @@ -import { fileURLToPath } from 'node:url' -import { join } from 'node:path' -import type { Browser, Page } from 'playwright' -import { chromium } from 'playwright' -import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest' -import { createMessage, createUserMessage } from '@deepseek-ai/dsh-llm' -import { SESSION_FORMAT_VERSION, Session, SessionId } from '@deepseek-ai/dsh-session' -import type {} from '@deepseek-ai/dsh-session-title' -import { - assertFixtureInventory, captureStableAria, compareOrRefreshGolden, launchWebScaffold, - seedSession, watchConsole, webSnapshotMode, type WebScaffold, -} from './scaffold.ts' -import { newEnglishPage, saveFailureShot } from './support.ts' - -const SNAPSHOT_DIR = fileURLToPath(new URL('./expected/markdown-mermaid', import.meta.url)) -const MODE = webSnapshotMode() -const SEED_ID = 'markdown-mermaid-web-e2e' -const DIAGRAM_MESSAGE = '[class*="markdown"]:has(.md-code-block)' -const FLOW = 'flowchart LR\n A[输入] --> B[共享渲染器] --> C[图形预览]' -const SEQUENCE = 'sequenceDiagram\n participant U as User\n participant R as Renderer\n U->>R: Mermaid source\n R-->>U: Diagram' -const INVALID = 'flowchart LR\n A[unfinished' -const UNTRUSTED = [ - '%%{init: {"securityLevel":"loose","htmlLabels":true,"themeCSS":"body {display:none!important}"}}%%', - 'flowchart LR', - ' A[""] --> B[Safe]', - ' click B "javascript:alert(1)"', -].join('\n') - -const DOT = 'digraph { rankdir=LR; Input -> Preview }' -const SVG = ` -SVG preview - - -
-
` -const HTML = `

Static HTML

- - -Disabled navigation - -` - -function fixture(): string { - const session = Session.create(SessionId('markdown-mermaid-source')) - session.append('turn/start', { turn: 1 }) - const user = session.append('user/message', createUserMessage({ - content: [{ type: 'text', text: 'Preview these Mermaid diagrams.' }], source: { kind: 'user' }, - }), { surfaceOp: 'append' }) - session.append('session/title', { title: 'Mermaid previews', messageSeqs: [user.seq], source: { kind: 'fallback' } }) - session.append('step/start', { turn: 1, step: 1 }) - session.append('assistant/message', { - stream: [], turn: 1, step: 1, - message: createMessage({ - role: 'assistant', - content: [{ type: 'text', text: [ - '# Mermaid previews', - ...[FLOW, SEQUENCE, INVALID, UNTRUSTED].map(code => `\`\`\`mermaid\n${code}\n\`\`\``), - ...[['dot', DOT], ['svg', SVG], ['html', HTML]].map(([lang, code]) => `\`\`\`${lang}\n${code}\n\`\`\``), - ].join('\n\n') }], - source: { kind: 'model', provider: 'fixture', model: 'fixture' }, - }), - }, { surfaceOp: 'append' }) - session.append('step/end', { turn: 1, step: 1 }) - session.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) - const noon = new Date().setHours(12, 0, 0, 0) - return [ - JSON.stringify({ - type: 'session', version: SESSION_FORMAT_VERSION, id: '{{sessionId}}', createdAt: 0, - cwd: '{{cwd}}', isSeeded: false, delegationDepth: 0, - }), - ...session.snapshotEvents().map(event => JSON.stringify({ ...event, time: noon + event.seq * 1000 })), - '', - ].join('\n') -} - -async function openConversation(page: Page, scaffold: WebScaffold): Promise { - await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' }) - await page.getByRole('treeitem').first().click({ timeout: 30_000 }) - await page.getByRole('treeitem').nth(1).click() - await page.getByRole('heading', { name: 'Mermaid previews' }).waitFor() -} - -describe('web e2e: Mermaid chat previews', () => { - let scaffold: WebScaffold - let browser: Browser - beforeAll(async () => { - scaffold = await launchWebScaffold({}) - await seedSession(scaffold, fixture(), SEED_ID) - browser = await chromium.launch() - }, 120_000) - afterAll(async () => { - await browser?.close() - await scaffold?.close() - }) - - it.skipIf(MODE === 'record').each(['light', 'dark'] as const)( - 'tightly wraps short diagram previews in %s mode', async (scheme) => { - const page = await newEnglishPage(browser) - try { - await page.emulateMedia({ colorScheme: scheme }) - await openConversation(page, scaffold) - await expect.poll(() => page.evaluate(() => getComputedStyle(document.documentElement).colorScheme)).toBe(scheme) - - const mermaid = page.getByRole('img', { name: 'Mermaid diagram' }).first() - await mermaid.evaluate(async (node: HTMLImageElement) => { await node.decode() }) - const mermaidHeight = await mermaid.evaluate(node => node.getBoundingClientRect().height) - const canvasHeight = await mermaid.evaluate(node => node.parentElement!.getBoundingClientRect().height) - // Compact diagrams need at most the Mermaid canvas's 16px padding on each side. - expect(canvasHeight - mermaidHeight).toBeLessThanOrEqual(32) - - for (const title of ['Graphviz diagram', 'SVG preview']) { - const image = page.getByRole('img', { name: title, exact: true }) - await image.evaluate(async (node: HTMLImageElement) => { await node.decode() }) - const imageHeight = await image.evaluate(node => node.getBoundingClientRect().height) - const containerHeight = await image.evaluate(node => node.parentElement!.getBoundingClientRect().height) - expect(imageHeight).toBeGreaterThan(0) - expect(containerHeight).toBeGreaterThanOrEqual(imageHeight) - expect.soft(containerHeight - imageHeight, - `${scheme} ${title}: ${containerHeight}px container around ${imageHeight}px content`, - ).toBeLessThanOrEqual(32) - } - await page.setViewportSize({ width: 360, height: 800 }) - const svg = page.getByRole('img', { name: 'SVG preview', exact: true }) - await expect.poll(() => svg.evaluate((node) => { - const { width, height } = node.getBoundingClientRect() - return width > 0 && height > 0 && width < 460 - })).toBe(true) - const compact = await svg.evaluate((node: HTMLImageElement) => ({ - width: node.getBoundingClientRect().width, - height: node.getBoundingClientRect().height, - canvasHeight: node.parentElement!.getBoundingClientRect().height, - ratio: node.naturalWidth / node.naturalHeight, - })) - expect(compact.width / compact.height).toBeCloseTo(compact.ratio, 1) - expect(compact.canvasHeight - compact.height).toBeLessThanOrEqual(32) - } finally { - await page.close() - } - }, - ) - - it.skipIf(MODE === 'record')('updates existing diagrams when the document switches between light and dark', async () => { - const page = await newEnglishPage(browser) - try { - await openConversation(page, scaffold) - const image = page.getByRole('img', { name: 'Mermaid diagram' }).first() - await image.waitFor() - let previous = await image.getAttribute('src') - for (const scheme of ['dark', 'light'] as const) { - await page.evaluate((value) => { - document.documentElement.style.colorScheme = value - document.body.toggleAttribute('data-ds-dark-theme', value === 'dark') - }, scheme) - await expect.poll(() => image.getAttribute('src')).not.toBe(previous) - await expect.poll(() => image.evaluate(node => (node as HTMLImageElement).naturalWidth)).toBeGreaterThan(0) - const colors = await image.evaluate((node) => { - const reference = document.createElement('span') - reference.style.backgroundColor = 'var(--dsw-alias-markdown-code-block)' - document.body.append(reference) - try { - return { - canvas: getComputedStyle(node.parentElement!).backgroundColor, - expected: getComputedStyle(reference).backgroundColor, - } - } finally { - reference.remove() - } - }) - expect(colors.canvas).toBe(colors.expected) - await expect.poll(() => page.getByTitle('HTML preview', { exact: true }).getAttribute('srcdoc')) - .toContain(`color-scheme:${scheme}`) - previous = await image.getAttribute('src') - } - } finally { - await page.close() - } - }) - - it.skipIf(MODE === 'record')('renders diagrams, switches to source, copies source, and contains malformed content', async () => { - const page = await newEnglishPage(browser) - onTestFailed(() => saveFailureShot(page, 'web-e2e-markdown-mermaid')) - const tripwire = watchConsole(page) - const dialogs: string[] = [] - page.on('dialog', (dialog) => { dialogs.push(dialog.message()); void dialog.dismiss() }) - await page.context().grantPermissions(['clipboard-read', 'clipboard-write']) - await openConversation(page, scaffold) - const images = page.getByRole('img', { name: 'Mermaid diagram' }) - await expect.poll(() => images.count(), { timeout: 20_000 }).toBe(3) - await expect.poll(() => images.evaluateAll(nodes => nodes.every(node => (node as HTMLImageElement).naturalWidth > 0))).toBe(true) - expect(await images.evaluateAll(nodes => nodes.every(node => - node.getBoundingClientRect().width <= (node as HTMLImageElement).naturalWidth))).toBe(true) - expect(await page.getByText('Unable to render this diagram. The source is shown below.', { exact: true }).count()).toBe(1) - expect(await page.locator('pre code').allTextContents()).toContain(INVALID) - expect(await images.first().evaluate(node => decodeURIComponent((node as HTMLImageElement).src))).toContain('共享渲染器') - const first = page.locator('.md-code-block').first() - const diagram = await first.getByRole('img').elementHandle() - const controls = first.locator('[class*="bannerWrap"]') - expect(await first.getByText('mermaid', { exact: true }).count()).toBe(0) - await page.mouse.move(0, 0) - expect(await controls.evaluate(node => getComputedStyle(node).opacity)).toBe('0') - await first.hover() - expect(await controls.evaluate(node => getComputedStyle(node).opacity)).toBe('1') - await first.getByRole('button', { name: 'Copy', exact: true }).click() - expect(await page.evaluate(() => navigator.clipboard.readText())).toBe(FLOW) - await page.mouse.move(0, 0) - await first.getByRole('button', { name: 'Source', exact: true }).focus() - expect(await controls.evaluate(node => getComputedStyle(node).opacity)).toBe('1') - await page.keyboard.press('Enter') - expect(await first.getByRole('button', { name: 'Preview', exact: true }) - .evaluate(node => node === document.activeElement)).toBe(true) - expect(await first.locator('pre code').textContent()).toBe(FLOW) - expect(await diagram!.evaluate(node => node.isConnected)).toBe(true) - expect(await diagram!.isVisible()).toBe(false) - await first.getByRole('button', { name: 'Preview', exact: true }).click() - expect(await diagram!.isVisible()).toBe(true) - await first.getByRole('button', { name: 'Copy', exact: true }).waitFor() - expect(await page.locator('body').evaluate(node => getComputedStyle(node).display)).not.toBe('none') - expect(await page.locator('[id^="dsh-mermaid-"]').count()).toBe(0) - expect(dialogs).toEqual([]) - expect(tripwire.pageErrors).toEqual([]) - expect(tripwire.warnings).toEqual([]) - await page.getByRole('heading', { name: 'Mermaid previews' }).click() - await page.mouse.move(0, 0) - const snapshot = await captureStableAria(page, DIAGRAM_MESSAGE, scaffold.workspaceCwd) - await compareOrRefreshGolden(join(SNAPSHOT_DIR, 'ui.expected.md'), snapshot, MODE) - await page.close() - }, 60_000) - - it.skipIf(MODE === 'record')('localizes the preview and failure states in Chinese', async () => { - const page = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: 'zh-CN', timezoneId: 'Asia/Shanghai', hasTouch: true }) - await openConversation(page, scaffold) - await expect.poll(() => page.getByRole('img', { name: 'Mermaid 图表' }).count(), { timeout: 20_000 }).toBe(3) - expect(await page.getByRole('button', { name: '源码', exact: true }).count()).toBe(7) - const controls = page.locator('.md-code-block').first().locator('[class*="bannerWrap"]') - expect(await controls.evaluate(node => getComputedStyle(node).opacity)).toBe('1') - expect(await page.getByRole('button', { name: '源码', exact: true }).first() - .evaluate(node => node.getBoundingClientRect().width)).toBe(44) - expect(await page.getByText('无法渲染此图表,源码如下。', { exact: true }).count()).toBe(1) - const firstImage = page.getByRole('img', { name: 'Mermaid 图表' }).first() - await expect.poll(() => firstImage.evaluate(node => (node as HTMLImageElement).naturalWidth)).toBeGreaterThan(0) - expect(await controls.evaluate(node => node.getBoundingClientRect().top)) - .toBeGreaterThanOrEqual(await firstImage.evaluate(node => node.getBoundingClientRect().bottom)) - const snapshot = await captureStableAria(page, DIAGRAM_MESSAGE, scaffold.workspaceCwd) - await compareOrRefreshGolden(join(SNAPSHOT_DIR, 'zh.expected.md'), snapshot, MODE) - await assertFixtureInventory(SNAPSHOT_DIR, ['ui.expected.md', 'zh.expected.md']) - await page.close() - }, 60_000) - - it.skipIf(MODE === 'record')('previews inert DOT and SVG images and isolates static HTML', async () => { - const page = await newEnglishPage(browser) - const requests: string[] = [] - const dialogs: string[] = [] - // Routing observes requests eligible for transport; Chromium also emits 'request' for CSP-blocked URLs. - await page.route('https://preview.invalid/**', async (route) => { - requests.push(route.request().url()) - await route.abort() - }) - page.on('dialog', (dialog) => { dialogs.push(dialog.message()); void dialog.dismiss() }) - await page.context().grantPermissions(['clipboard-read', 'clipboard-write']) - await openConversation(page, scaffold) - for (const [title, code] of [['Graphviz diagram', DOT], ['SVG preview', SVG], ['HTML preview', HTML]] as const) { - const preview = title === 'HTML preview' - ? page.getByTitle(title, { exact: true }) - : page.getByRole('img', { name: title, exact: true, includeHidden: true }) - await preview.waitFor() - if (title === 'HTML preview') { - expect(await preview.getAttribute('sandbox')).toBe('') - const body = preview.contentFrame() - await body.getByRole('heading', { name: 'Static HTML' }).waitFor() - expect(await body.getByRole('heading').evaluate(node => getComputedStyle(node).color)).toBe('rgb(0, 128, 0)') - await body.getByText('Disabled navigation').click() - expect(await preview.evaluate(node => (node as HTMLIFrameElement).contentDocument)).toBeNull() - } else { - await expect.poll(() => preview.evaluate(node => (node as HTMLImageElement).naturalWidth)).toBeGreaterThan(0) - } - const block = page.locator('.md-code-block').filter({ has: preview }) - await block.hover() - await block.getByRole('button', { name: 'Copy', exact: true }).click() - expect(await page.evaluate(() => navigator.clipboard.readText())).toBe(code) - await block.getByRole('button', { name: 'Source', exact: true }).click() - expect(await block.locator('pre code').textContent()).toBe(code) - await block.getByRole('button', { name: 'Preview', exact: true }).click() - expect(await preview.isVisible()).toBe(true) - } - expect(await page.locator('body').getAttribute('data-preview-escaped')).toBeNull() - expect(dialogs).toEqual([]) - expect(requests).toEqual([]) - const notices = await page.request.get(new URL('/preview-third-party-notices.txt', scaffold.authenticatedUrl).href) - expect(notices.ok()).toBe(true) - expect(await notices.text()).toContain('Eclipse Public License - v 2.0') - await page.close() - }) - - it.skipIf(MODE === 'record')('keeps actions visible when a mouse and touchscreen are both available', async () => { - const hybridBrowser = await chromium.launch({ - args: ['--blink-settings=availablePointerTypes=6,primaryPointerType=4,availableHoverTypes=2,primaryHoverType=2'], - }) - try { - const page = await newEnglishPage(hybridBrowser) - expect(await page.evaluate(() => - matchMedia('(hover: hover) and (pointer: fine) and (any-pointer: coarse)').matches)).toBe(true) - await openConversation(page, scaffold) - const first = page.locator('.md-code-block').first() - const diagram = first.getByRole('img', { name: 'Mermaid diagram' }) - await expect.poll(() => diagram.evaluate(node => (node as HTMLImageElement).naturalWidth)).toBeGreaterThan(0) - await page.mouse.move(0, 0) - const controls = first.locator('[class*="bannerWrap"]') - expect(await controls.evaluate(node => getComputedStyle(node).opacity)).toBe('1') - expect(await controls.evaluate(node => getComputedStyle(node).pointerEvents)).toBe('auto') - expect(await first.getByRole('button', { name: 'Source', exact: true }) - .evaluate(node => node.getBoundingClientRect().width)).toBe(44) - expect(await controls.evaluate(node => node.getBoundingClientRect().top)) - .toBeGreaterThanOrEqual(await diagram.evaluate(node => node.getBoundingClientRect().bottom)) - await first.getByRole('button', { name: 'Source', exact: true }).click() - expect(await first.locator('pre code').textContent()).toBe(FLOW) - } finally { - await hybridBrowser.close() - } - }, 60_000) -}) diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index a6e3b24bfd..3b01e3990c 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -79,7 +79,6 @@ "tests/math-rendering.e2e.ts", "tests/markdown-cjk-strong.e2e.ts", "tests/markdown-inline-code-links.e2e.ts", - "tests/markdown-mermaid.e2e.ts", "tests/clickable-links-gallery.e2e.ts", "tests/queue-actions.e2e.ts", "tests/queue-image.e2e.ts", diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index f6c90861f1..0e15b4f088 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -27,20 +27,6 @@ function clientDocumentTitle(): Plugin { } } -/** Preserve preview licenses and Graphviz source availability in every browser distribution. */ -function previewNotices(): Plugin { - return { - name: 'dsh-preview-notices', - async generateBundle() { - this.emitFile({ - type: 'asset', - fileName: 'preview-third-party-notices.txt', - source: await readFile(src('../../packages/client/ui-primitives/THIRD_PARTY_PREVIEW_NOTICES.txt'), 'utf8'), - }) - }, - } -} - /** Fail before a Vite dev or preview server can expose the boot-manifest-free shell. */ function rejectStandaloneServe(): Plugin { return { @@ -161,7 +147,7 @@ export default defineConfig({ // Relative asset URLs: preview.html mounts the same output under any base // directory, and the served index resolves identically from the site root. base: './', - plugins: [previewNotices(), rejectStandaloneServe(), clientDocumentTitle(), react(), emitPreviewPage()], + plugins: [rejectStandaloneServe(), clientDocumentTitle(), react(), emitPreviewPage()], build: { // The worker bootstrap holds its page at top-level await; Vite's default // `modules` target (es2020-era) rejects that syntax. diff --git a/packages/client/locale/src/locales/en.ts b/packages/client/locale/src/locales/en.ts index 374de7c7b0..70ce31598c 100644 --- a/packages/client/locale/src/locales/en.ts +++ b/packages/client/locale/src/locales/en.ts @@ -38,16 +38,6 @@ export const en = { 'json.expandNode': 'Expand JSON node', 'json.label': 'JSON', 'markdown.footnotes': 'Footnotes', - 'markdown.preview.graphviz': 'Graphviz diagram', - 'markdown.preview.svg': 'SVG preview', - 'markdown.preview.html': 'HTML preview', - 'markdown.preview.loading': 'Rendering preview…', - 'markdown.preview.error': 'Unable to render this preview. The source is shown below.', - 'markdown.mermaid.diagram': 'Mermaid diagram', - 'markdown.preview.preview': 'Preview', - 'markdown.preview.source': 'Source', - 'markdown.mermaid.loading': 'Rendering diagram…', - 'markdown.mermaid.error': 'Unable to render this diagram. The source is shown below.', 'markdown.truncatedCharacters': '… truncated at {total} characters', 'number.thousand': '{value}K', 'number.million': '{value}M', diff --git a/packages/client/locale/src/locales/zh.ts b/packages/client/locale/src/locales/zh.ts index df1aa1d19e..30cee308d5 100644 --- a/packages/client/locale/src/locales/zh.ts +++ b/packages/client/locale/src/locales/zh.ts @@ -36,16 +36,6 @@ export const zh = { 'json.expandNode': '展开 JSON 节点', 'json.label': 'JSON', 'markdown.footnotes': '脚注', - 'markdown.preview.graphviz': 'Graphviz 图表', - 'markdown.preview.svg': 'SVG 预览', - 'markdown.preview.html': 'HTML 预览', - 'markdown.preview.loading': '正在渲染预览…', - 'markdown.preview.error': '无法渲染此预览,源码如下。', - 'markdown.mermaid.diagram': 'Mermaid 图表', - 'markdown.preview.preview': '预览', - 'markdown.preview.source': '源码', - 'markdown.mermaid.loading': '正在渲染图表…', - 'markdown.mermaid.error': '无法渲染此图表,源码如下。', 'markdown.truncatedCharacters': '… 已截断,共 {total} 字符', 'number.thousand': '{value}K', 'number.million': '{value}M', diff --git a/packages/client/ui-chat/README.i18n.yaml b/packages/client/ui-chat/README.i18n.yaml index 554709bb19..4db27677d0 100644 --- a/packages/client/ui-chat/README.i18n.yaml +++ b/packages/client/ui-chat/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-chat/README.md -README.md: 1bd7f008e46b695899c703c928ebe8bb76fbe4c8 -README.zh.md: 5919622396dde5b0c8cbf51f71999672a11f13ec +README.md: 860dcb9eeb9c92a14d9128d9d8c95c29796726f8 +README.zh.md: 4dd06a16b19b14cacc0ded603cc1cf8869dd1a43 diff --git a/packages/client/ui-chat/README.md b/packages/client/ui-chat/README.md index 1bd7f008e4..860dcb9eeb 100644 --- a/packages/client/ui-chat/README.md +++ b/packages/client/ui-chat/README.md @@ -15,7 +15,6 @@ File-mention providers receive the viewed Session ID with the closing-turn owner ## Table of Contents - [System prompt row](#system-prompt-row) -- [Mermaid previews](#mermaid-previews) - [Turn token usage](#turn-token-usage) - [Turn Process Folding](#turn-process-folding) - [Scroll ownership](#scroll-ownership) @@ -30,13 +29,6 @@ File-mention providers receive the viewed Session ID with the closing-turn owner Each nonempty appended `system/message` owns a collapsed prompt row, including a complete prompt at the start of a headerless window; the same-step header does not duplicate it. Chat also shows a collapsed `System prompt` row for a non-empty initial request, explicit message-series start, or `system/message` surface node replacement whose text differs, reading the last nonempty surviving system node in surface order at the `request/header`; a non-initial request whose preceding header is outside the loaded history window also shows one. A resume repeats the row even when its system text is unchanged, including after pagination supplies the preceding header and system node; same-series config-only or tool-only changes, tool steps, and retries create no repetition, and a `system/message` event is never rendered as a transcript message. The row appears before that request's user messages, matching the provider envelope, and expands to the exact model-visible text with its original line breaks. A request whose system node is empty or outside the loaded window creates no row until the page holding the node arrives. - -## Mermaid previews - -Settled Assistant Markdown shows `mermaid` code fences as diagrams. Diagrams have no language banner. Source/Preview and Copy icon actions appear on hover or keyboard focus and remain visible on touch devices; both views copy the original Mermaid source. Streaming fences remain code until the message settles; invalid diagrams show an error and their source without disrupting other blocks. Chat enables the shared [Mermaid preview primitive](../ui-primitives/README.md) through localized labels. Chat also previews `graphviz`/`dot`, `svg`, and static `html` fences in isolated iframes, with the same default visualization, source switching, and copying. HTML scripts, navigation, and remote resources are disabled. This is a presentation change and adds no model prompt, tool, or Session event. - ------ - ## Turn token usage diff --git a/packages/client/ui-chat/README.zh.md b/packages/client/ui-chat/README.zh.md index 5919622396..4dd06a16b1 100644 --- a/packages/client/ui-chat/README.zh.md +++ b/packages/client/ui-chat/README.zh.md @@ -15,7 +15,6 @@ kind: "package-reference" ## 目录 - [系统提示词行](#system-prompt-row) -- [Mermaid 预览](#mermaid-previews) - [轮次 token 用量](#turn-token-usage) - [轮次过程折叠](#turn-process-folding) - [滚动归属](#scroll-ownership) @@ -32,13 +31,6 @@ kind: "package-reference" ----- - -## Mermaid 预览 - -已定稿的 Assistant Markdown 把 `mermaid` 代码 fence 显示为图表。图表不显示语言标题栏。源码/预览切换与复制图标在悬停或键盘聚焦时出现,触屏上保持可见;两种视图复制的都是原始 Mermaid 源码。流式 fence 在消息定稿前保持代码显示;非法图表显示错误与源码,不影响其他块。Chat 通过本地化 label 启用共享的 [Mermaid 预览原语](../ui-primitives/README.zh.md)。Chat 还在隔离 iframe 中预览 `graphviz`/`dot`、`svg` 和静态 `html` fence,共用默认可视化、源码切换与复制操作。HTML 脚本、导航与远程资源均被禁用。这项展示变更不增加模型提示词、工具或 Session 事件。 - ------ - ## 轮次 token 用量 diff --git a/packages/client/ui-chat/src/client/markdown-labels.ts b/packages/client/ui-chat/src/client/markdown-labels.ts index c77cb8e9e4..6c30b99396 100644 --- a/packages/client/ui-chat/src/client/markdown-labels.ts +++ b/packages/client/ui-chat/src/client/markdown-labels.ts @@ -9,21 +9,8 @@ import type { ChatViewSlotProps } from './contract/slots.ts' * @returns Labels for code fences and footnotes. */ export function markdownLabels(t: ChatViewSlotProps['t']): MarkdownLabels { - const status = { loading: t('markdown.preview.loading'), error: t('markdown.preview.error') } return { code: { copyLabel: t('copy'), copiedLabel: t('copied') }, footnotes: t('markdown.footnotes'), - preview: { - preview: t('markdown.preview.preview'), - source: t('markdown.preview.source'), - mermaid: { - diagram: t('markdown.mermaid.diagram'), - loading: t('markdown.mermaid.loading'), - error: t('markdown.mermaid.error'), - }, - graphviz: { ...status, diagram: t('markdown.preview.graphviz') }, - svg: { ...status, diagram: t('markdown.preview.svg') }, - html: { ...status, diagram: t('markdown.preview.html') }, - }, } } diff --git a/packages/client/ui-primitives/README.i18n.yaml b/packages/client/ui-primitives/README.i18n.yaml index 2c354d584f..5672f45bb3 100644 --- a/packages/client/ui-primitives/README.i18n.yaml +++ b/packages/client/ui-primitives/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-primitives/README.md -README.md: 22f450914078799f2f9da31389670f83cefe66cd -README.zh.md: c9fd355d4b16ae98bd0f194d13d4331a52513044 +README.md: aebb0be860ebaba4543a8fd262599507ead64556 +README.zh.md: a545aff9837036890b6250ab1aa32fb651af0a18 diff --git a/packages/client/ui-primitives/README.md b/packages/client/ui-primitives/README.md index 22f4509140..aebb0be860 100644 --- a/packages/client/ui-primitives/README.md +++ b/packages/client/ui-primitives/README.md @@ -73,15 +73,6 @@ The catalog above lists what each export is for; this section covers the behavio `MarkdownText` renders untrusted GFM and TeX math, blocks unsafe links and images, and can turn resolved file mentions into explicit controls. When the owner passes a `pathImages` vocabulary, image destinations that are local media paths rewrite to displayable URLs on settled renders only (the same streaming gate as file mentions); without a vocabulary, local destinations remain inert alt text. A load or decode failure replaces the image with its authored alt text, or the original destination when alt is empty. Changing the image source permits a fresh load. While a reply streams, it freezes completed blocks, advances a top-level open fence by completed lines, and highlights that fence from saved Shiki grammar state. Completed token lines enter fixed-size React groups, so later chunks reconcile only the growing group; an unchanged fence retains that DOM when the final full parse resolves cross-document syntax. `TerminalBlock`, `ReadBlock`, `DiffBlock`, `SearchBlock`, and `WebBlock` render the matching tool-result intent with copy controls, overflow handling, and ANSI processing where applicable. `JsonTree` and `JsonBlock` inspect JSON values read-only, while `projectUserText` projects sent user text into inline plain runs and reference chips for the message bubble and queue rows. -Supply `MarkdownLabels.preview` to enable settled `mermaid`, `graphviz`/`dot`, `svg`, and `html` fences. Each language supplies localized preview states, and consumers without these labels retain code. Mermaid and Graphviz use the active document palette on the code-block background and update mounted previews when the theme changes. Explicit colors in DOT, SVG, and HTML remain authored content. Rendering loads Mermaid on demand, uses strict security, and exposes the generated SVG as an image with no diagram link handlers. A render failure shows the original source with the supplied error label; replacing the source discards late results from the previous render. The diagram keeps its intrinsic size and shrinks to fit the available width. - -`CodeBlock.preview` supplies an optional headerless body with icon actions for source switching and copying. Actions appear on hover or keyboard focus, and stay visible below the diagram whenever a touchscreen is available, including with a mouse attached. Switching to source keeps the preview mounted, so returning reuses its result; copying always retains the source. - -Each settled fence defaults to visualization and shares the source/copy controls. Graphviz loads `@viz-js/viz` on demand and renders DOT with the `dot` engine. SVG and Graphviz output are inert images in the same content-sized canvas as Mermaid; malformed SVG and DOT retain their source with an error. HTML accepts complete documents or fragments and preserves inline styles, but strips scripts and navigation. HTML frames have no sandbox permissions, and their Content Security Policy blocks external resources, embedded frames, forms, and scripts. Diagram canvases follow the rendered image height with 16px padding and shrink images proportionally to fit the available width. HTML frames use a 400px scrollable viewport. Raw HTML outside a code fence remains ordinary Markdown input. - -Internal render functions accept optional native `MermaidConfig` and Viz.js `RenderOptions` for developer customization. Attribute and theme-variable overrides merge with document defaults; Mermaid preview restrictions and Graphviz SVG output remain fixed. These parameters have no settings UI. - -[Preview license notices](THIRD_PARTY_PREVIEW_NOTICES.txt) ship with this package and the Web frontend. Mermaid is MIT; Viz.js is MIT but embeds Graphviz under EPL-2.0. The notices retain license texts and the exact Graphviz source download. Dependency upgrades require reviewing the embedded payload as well as npm metadata; the [preview decision](../../../.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.md) records the distribution and sandbox choices. ### Localizing copy @@ -150,7 +141,6 @@ None; this package neither assembles nor sends a provider request. These limits define how the atoms behave at the edges; they are current package constraints, not a component roadmap. -- **Diagram rendering runs on the browser thread** — every mounted settled preview starts rendering, including off-screen diagrams. Mermaid serializes layouts; Graphviz performs synchronous WebAssembly layout. Submitted layout work cannot be interrupted. Preview virtualization and worker rendering are not provided. - **Streaming defers cross-boundary reference resolution** — a reference-style link or footnote whose definition sits on the other side of the incremental freeze boundary renders as literal text while the reply streams; the settled full parse at finalize resolves it. - **A long highlighted fence retains its complete token DOM** — streaming avoids re-parsing, re-tokenizing, and reconciling the completed prefix, but it does not discard old colors or virtualize token spans. Final DOM cardinality therefore still follows the fence's token count; nested/container fences and a pathological single long line remain on the general tail path. - **Glyph-level icons are redrawn approximations** — the fish logo and the sparkle mark come from font glyphs whose vector geometry is not exportable from the local design data; hand-authored recreations stand in until an exact export path exists. diff --git a/packages/client/ui-primitives/README.zh.md b/packages/client/ui-primitives/README.zh.md index c9fd355d4b..a545aff983 100644 --- a/packages/client/ui-primitives/README.zh.md +++ b/packages/client/ui-primitives/README.zh.md @@ -73,15 +73,6 @@ kind: "package-library" `MarkdownText` 渲染不可信的 GFM 与 TeX 公式、阻止不安全的链接与图片,并可把已解析的文件提及转换为显式控件。当 owner 传入 `pathImages` 词表时,本地媒体路径的图片目标只在落定渲染阶段重写为可展示 URL(与 file mentions 相同的流式门);不传词表时本地目标保持惰性 alt 文本。加载或解码失败后,图片替换为作者的 alt 文本;alt 为空时显示原始目标路径。图片源变化后可重新加载。回复流式输出时,它冻结已完成的块、按已完成行推进顶层未闭合 fence,并从保存的 Shiki grammar state 为该 fence 增量高亮。已完成的 token 行进入固定大小的 React 分组,后续分片只 reconcile 正在增长的分组;最终全量解析解决跨文档语法时,未变化的 fence 会保留该 DOM。`TerminalBlock`、`ReadBlock`、`DiffBlock`、`SearchBlock` 与 `WebBlock` 把对应的工具结果意图渲染为带复制控件、溢出处理及适用时 ANSI 处理的卡片。`JsonTree` 与 `JsonBlock` 以只读方式检查 JSON 值;`projectUserText` 把已发送的用户文本投影为行内普通文本段与引用 chip,供消息气泡和排队行使用。 -提供 `MarkdownLabels.preview` 可启用定稿后的 `mermaid`、`graphviz`/`dot`、`svg` 和 `html` fence。每种语言提供本地化预览状态,未提供这些文案的调用方保留代码显示。Mermaid 与 Graphviz 在代码块背景上使用当前文档配色,主题切换时同步更新已挂载的预览。DOT、SVG 与 HTML 中明确指定的颜色仍保留原样。渲染按需加载 Mermaid、使用严格安全模式,并把生成的 SVG 显示为图片,不绑定图内链接处理器。渲染失败时显示原始源码与传入的错误文案;替换源码后会丢弃前一次渲染的延迟结果。图表保留固有尺寸,并在可用宽度不足时缩小。 - -`CodeBlock.preview` 提供无标题栏的替代正文,以及切换源码和复制的图标操作。操作在悬停或键盘聚焦时出现;只要设备具备触屏,就在图表下方保持可见,包括同时连接鼠标的情况。切换到源码时保留已挂载的预览,返回时复用其结果;复制始终保留源码。 - -每个定稿 fence 默认显示可视化,并共享源码切换与复制操作。Graphviz 按需加载 `@viz-js/viz`,用 `dot` 引擎渲染 DOT。SVG 与 Graphviz 输出作为不可执行的图片,使用与 Mermaid 相同的按内容定高画布;非法 SVG 与 DOT 显示错误并保留源码。HTML 接受完整文档或片段、保留内联样式,同时移除脚本与导航。HTML iframe 不授予任何 sandbox 权限,Content Security Policy 阻止外部资源、嵌套 iframe、表单与脚本。图表画布随渲染后的图片高度收缩,保留 16px 内边距,并在可用宽度不足时等比例缩小图片。HTML iframe 使用可滚动的 400px 视口。代码 fence 外的原始 HTML 仍按普通 Markdown 输入处理。 - -内部渲染函数接受可选的原生 `MermaidConfig` 与 Viz.js `RenderOptions`,供开发者定制。属性与主题变量覆盖项和文档默认值合并;Mermaid 预览限制与 Graphviz SVG 输出保持固定。这些参数没有设置界面。 - -[预览许可证声明](THIRD_PARTY_PREVIEW_NOTICES.txt)随本包与 Web 前端分发。Mermaid 使用 MIT;Viz.js 使用 MIT,但内嵌 Graphviz 使用 EPL-2.0。声明保留完整许可证文本及准确的 Graphviz 源码下载地址。升级依赖时必须同时检查内嵌产物和 npm 元数据;[预览决策](../../../.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.zh.md)记录分发与 sandbox 选择。 ### 本地化文案 @@ -150,7 +141,6 @@ kind: "package-library" 这些限制说明原子组件在边缘情况下的行为;它们是当前包约束,不是组件路线图。 -- **图表渲染在浏览器线程上执行**:每个已挂载的定稿预览都会开始渲染,包括视口外的图表。Mermaid 串行执行布局;Graphviz 同步执行 WebAssembly 布局。已提交的布局工作无法中断。当前不提供预览虚拟化或 worker 渲染。 - **流式期间跨边界引用解析被推迟**:定义落在增量冻结边界另一侧的引用式链接或脚注,在回复流式输出期间渲染为字面文本;定稿时的全量解析会将其解析。 - **长高亮 fence 会保留完整 token DOM**:流式路径避免重新解析、重新 tokenize 和 reconcile 已完成前缀,但不会丢弃旧颜色或虚拟化 token span。因此最终 DOM 数量仍随 fence 的 token 数增长;嵌套/容器内 fence 与病态的单个超长行仍走通用尾部路径。 - **字形级图标是重新绘制的近似版本**:鱼形标志与闪光标记来自字体字形,而本地设计数据无法导出其矢量几何;在获得精确导出路径前,使用手工重建版本代替。 diff --git a/packages/client/ui-primitives/THIRD_PARTY_PREVIEW_NOTICES.txt b/packages/client/ui-primitives/THIRD_PARTY_PREVIEW_NOTICES.txt deleted file mode 100644 index 3e1156a536..0000000000 --- a/packages/client/ui-primitives/THIRD_PARTY_PREVIEW_NOTICES.txt +++ /dev/null @@ -1,688 +0,0 @@ -Markdown preview third-party notices - -Mermaid 11.16.0: MIT; source https://github.com/mermaid-js/mermaid/tree/mermaid%4011.16.0 -@viz-js/viz 3.30.0: MIT JavaScript wrapper; source and build instructions: -https://github.com/mdaines/viz-js/tree/99da545270e6e7b127a5c7ba65974b8a604a6358 - -The Viz.js WebAssembly payload contains Graphviz 16.0.0 under EPL-2.0, -Expat 2.8.4 under MIT, and Emscripten 5.0.7 runtime code under MIT/NCSA. -Graphviz remains under EPL-2.0; it is not relicensed as MIT by DeepSeek Harness. -Graphviz source code is available under EPL-2.0 at: -https://gitlab.com/api/v4/projects/4207231/packages/generic/graphviz-releases/16.0.0/graphviz-16.0.0.tar.gz -SHA-256: 36a1de1aaf5a2023b14f95170a5f8f0b12522d1c305b517fc261966597050749 -Viz.js's backend build instructions and additions are available at the commit above. -DeepSeek Harness uses the unmodified upstream Viz.js package. - -Expat source: https://github.com/libexpat/libexpat/tree/R_2_8_4 -Emscripten source: https://github.com/emscripten-core/emscripten/tree/5.0.7 -DOMPurify 3.4.11: Apache-2.0 OR MPL-2.0; this distribution selects Apache-2.0. -Source: https://github.com/cure53/DOMPurify/tree/3.4.11 - -The following license texts and copyright notices apply to their respective components. - - -===== Mermaid ===== - -The MIT License (MIT) - -Copyright (c) 2014 - 2022 Knut Sveidqvist - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - - -===== Viz.js ===== - -MIT License - -Copyright (c) Michael Daines - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - - -===== Graphviz ===== - -Eclipse Public License - v 2.0 - - THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE - PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION - OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. - -1. DEFINITIONS - -"Contribution" means: - - a) in the case of the initial Contributor, the initial content - Distributed under this Agreement, and - - b) in the case of each subsequent Contributor: - i) changes to the Program, and - ii) additions to the Program; - where such changes and/or additions to the Program originate from - and are Distributed by that particular Contributor. A Contribution - "originates" from a Contributor if it was added to the Program by - such Contributor itself or anyone acting on such Contributor's behalf. - Contributions do not include changes or additions to the Program that - are not Modified Works. - -"Contributor" means any person or entity that Distributes the Program. - -"Licensed Patents" mean patent claims licensable by a Contributor which -are necessarily infringed by the use or sale of its Contribution alone -or when combined with the Program. - -"Program" means the Contributions Distributed in accordance with this -Agreement. - -"Recipient" means anyone who receives the Program under this Agreement -or any Secondary License (as applicable), including Contributors. - -"Derivative Works" shall mean any work, whether in Source Code or other -form, that is based on (or derived from) the Program and for which the -editorial revisions, annotations, elaborations, or other modifications -represent, as a whole, an original work of authorship. - -"Modified Works" shall mean any work in Source Code or other form that -results from an addition to, deletion from, or modification of the -contents of the Program, including, for purposes of clarity any new file -in Source Code form that contains any contents of the Program. Modified -Works shall not include works that contain only declarations, -interfaces, types, classes, structures, or files of the Program solely -in each case in order to link to, bind by name, or subclass the Program -or Modified Works thereof. - -"Distribute" means the acts of a) distributing or b) making available -in any manner that enables the transfer of a copy. - -"Source Code" means the form of a Program preferred for making -modifications, including but not limited to software source code, -documentation source, and configuration files. - -"Secondary License" means either the GNU General Public License, -Version 2.0, or any later versions of that license, including any -exceptions or additional permissions as identified by the initial -Contributor. - -2. GRANT OF RIGHTS - - a) Subject to the terms of this Agreement, each Contributor hereby - grants Recipient a non-exclusive, worldwide, royalty-free copyright - license to reproduce, prepare Derivative Works of, publicly display, - publicly perform, Distribute and sublicense the Contribution of such - Contributor, if any, and such Derivative Works. - - b) Subject to the terms of this Agreement, each Contributor hereby - grants Recipient a non-exclusive, worldwide, royalty-free patent - license under Licensed Patents to make, use, sell, offer to sell, - import and otherwise transfer the Contribution of such Contributor, - if any, in Source Code or other form. This patent license shall - apply to the combination of the Contribution and the Program if, at - the time the Contribution is added by the Contributor, such addition - of the Contribution causes such combination to be covered by the - Licensed Patents. The patent license shall not apply to any other - combinations which include the Contribution. No hardware per se is - licensed hereunder. - - c) Recipient understands that although each Contributor grants the - licenses to its Contributions set forth herein, no assurances are - provided by any Contributor that the Program does not infringe the - patent or other intellectual property rights of any other entity. - Each Contributor disclaims any liability to Recipient for claims - brought by any other entity based on infringement of intellectual - property rights or otherwise. As a condition to exercising the - rights and licenses granted hereunder, each Recipient hereby - assumes sole responsibility to secure any other intellectual - property rights needed, if any. For example, if a third party - patent license is required to allow Recipient to Distribute the - Program, it is Recipient's responsibility to acquire that license - before distributing the Program. - - d) Each Contributor represents that to its knowledge it has - sufficient copyright rights in its Contribution, if any, to grant - the copyright license set forth in this Agreement. - - e) Notwithstanding the terms of any Secondary License, no - Contributor makes additional grants to any Recipient (other than - those set forth in this Agreement) as a result of such Recipient's - receipt of the Program under the terms of a Secondary License - (if permitted under the terms of Section 3). - -3. REQUIREMENTS - -3.1 If a Contributor Distributes the Program in any form, then: - - a) the Program must also be made available as Source Code, in - accordance with section 3.2, and the Contributor must accompany - the Program with a statement that the Source Code for the Program - is available under this Agreement, and informs Recipients how to - obtain it in a reasonable manner on or through a medium customarily - used for software exchange; and - - b) the Contributor may Distribute the Program under a license - different than this Agreement, provided that such license: - i) effectively disclaims on behalf of all other Contributors all - warranties and conditions, express and implied, including - warranties or conditions of title and non-infringement, and - implied warranties or conditions of merchantability and fitness - for a particular purpose; - - ii) effectively excludes on behalf of all other Contributors all - liability for damages, including direct, indirect, special, - incidental and consequential damages, such as lost profits; - - iii) does not attempt to limit or alter the recipients' rights - in the Source Code under section 3.2; and - - iv) requires any subsequent distribution of the Program by any - party to be under a license that satisfies the requirements - of this section 3. - -3.2 When the Program is Distributed as Source Code: - - a) it must be made available under this Agreement, or if the - Program (i) is combined with other material in a separate file or - files made available under a Secondary License, and (ii) the initial - Contributor attached to the Source Code the notice described in - Exhibit A of this Agreement, then the Program may be made available - under the terms of such Secondary Licenses, and - - b) a copy of this Agreement must be included with each copy of - the Program. - -3.3 Contributors may not remove or alter any copyright, patent, -trademark, attribution notices, disclaimers of warranty, or limitations -of liability ("notices") contained within the Program from any copy of -the Program which they Distribute, provided that Contributors may add -their own appropriate notices. - -4. COMMERCIAL DISTRIBUTION - -Commercial distributors of software may accept certain responsibilities -with respect to end users, business partners and the like. While this -license is intended to facilitate the commercial use of the Program, -the Contributor who includes the Program in a commercial product -offering should do so in a manner which does not create potential -liability for other Contributors. Therefore, if a Contributor includes -the Program in a commercial product offering, such Contributor -("Commercial Contributor") hereby agrees to defend and indemnify every -other Contributor ("Indemnified Contributor") against any losses, -damages and costs (collectively "Losses") arising from claims, lawsuits -and other legal actions brought by a third party against the Indemnified -Contributor to the extent caused by the acts or omissions of such -Commercial Contributor in connection with its distribution of the Program -in a commercial product offering. The obligations in this section do not -apply to any claims or Losses relating to any actual or alleged -intellectual property infringement. In order to qualify, an Indemnified -Contributor must: a) promptly notify the Commercial Contributor in -writing of such claim, and b) allow the Commercial Contributor to control, -and cooperate with the Commercial Contributor in, the defense and any -related settlement negotiations. The Indemnified Contributor may -participate in any such claim at its own expense. - -For example, a Contributor might include the Program in a commercial -product offering, Product X. That Contributor is then a Commercial -Contributor. If that Commercial Contributor then makes performance -claims, or offers warranties related to Product X, those performance -claims and warranties are such Commercial Contributor's responsibility -alone. Under this section, the Commercial Contributor would have to -defend claims against the other Contributors related to those performance -claims and warranties, and if a court requires any other Contributor to -pay any damages as a result, the Commercial Contributor must pay -those damages. - -5. NO WARRANTY - -EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT -PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" -BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR -IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF -TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR -PURPOSE. Each Recipient is solely responsible for determining the -appropriateness of using and distributing the Program and assumes all -risks associated with its exercise of rights under this Agreement, -including but not limited to the risks and costs of program errors, -compliance with applicable laws, damage to or loss of data, programs -or equipment, and unavailability or interruption of operations. - -6. DISCLAIMER OF LIABILITY - -EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT -PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS -SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST -PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE -EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - -7. GENERAL - -If any provision of this Agreement is invalid or unenforceable under -applicable law, it shall not affect the validity or enforceability of -the remainder of the terms of this Agreement, and without further -action by the parties hereto, such provision shall be reformed to the -minimum extent necessary to make such provision valid and enforceable. - -If Recipient institutes patent litigation against any entity -(including a cross-claim or counterclaim in a lawsuit) alleging that the -Program itself (excluding combinations of the Program with other software -or hardware) infringes such Recipient's patent(s), then such Recipient's -rights granted under Section 2(b) shall terminate as of the date such -litigation is filed. - -All Recipient's rights under this Agreement shall terminate if it -fails to comply with any of the material terms or conditions of this -Agreement and does not cure such failure in a reasonable period of -time after becoming aware of such noncompliance. If all Recipient's -rights under this Agreement terminate, Recipient agrees to cease use -and distribution of the Program as soon as reasonably practicable. -However, Recipient's obligations under this Agreement and any licenses -granted by Recipient relating to the Program shall continue and survive. - -Everyone is permitted to copy and distribute copies of this Agreement, -but in order to avoid inconsistency the Agreement is copyrighted and -may only be modified in the following manner. The Agreement Steward -reserves the right to publish new versions (including revisions) of -this Agreement from time to time. No one other than the Agreement -Steward has the right to modify this Agreement. The Eclipse Foundation -is the initial Agreement Steward. The Eclipse Foundation may assign the -responsibility to serve as the Agreement Steward to a suitable separate -entity. Each new version of the Agreement will be given a distinguishing -version number. The Program (including Contributions) may always be -Distributed subject to the version of the Agreement under which it was -received. In addition, after a new version of the Agreement is published, -Contributor may elect to Distribute the Program (including its -Contributions) under the new version. - -Except as expressly stated in Sections 2(a) and 2(b) above, Recipient -receives no rights or licenses to the intellectual property of any -Contributor under this Agreement, whether expressly, by implication, -estoppel or otherwise. All rights in the Program not expressly granted -under this Agreement are reserved. Nothing in this Agreement is intended -to be enforceable by any entity that is not a Contributor or Recipient. -No third-party beneficiary rights are created under this Agreement. - -Exhibit A - Form of Secondary Licenses Notice - -"This Source Code may also be made available under the following -Secondary Licenses when the conditions for such availability set forth -in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), -version(s), and exceptions or additional permissions here}." - - Simply including a copy of this Agreement, including this Exhibit A - is not sufficient to license the Source Code under Secondary Licenses. - - If it is not possible or desirable to put the notice in a particular - file, then You may include the notice in a location (such as a LICENSE - file in a relevant directory) where a recipient would be likely to - look for such a notice. - - You may add additional accurate notices of copyright ownership. - - -===== Expat ===== - -Copyright (c) 1998-2000 Thai Open Source Software Center Ltd and Clark Cooper -Copyright (c) 2001-2025 Expat maintainers - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be included -in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -===== Emscripten ===== - -Emscripten is available under 2 licenses, the MIT license and the -University of Illinois/NCSA Open Source License. - -Both are permissive open source licenses, with little if any -practical difference between them. - -The reason for offering both is that (1) the MIT license is -well-known, while (2) the University of Illinois/NCSA Open Source -License allows Emscripten's code to be integrated upstream into -LLVM, which uses that license, should the opportunity arise. - -The full text of both licenses follows. - -============================================================================== - -Copyright (c) 2010-2014 Emscripten authors, see AUTHORS file. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -============================================================================== - -Copyright (c) 2010-2014 Emscripten authors, see AUTHORS file. -All rights reserved. - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the -"Software"), to deal with the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - - Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimers. - - Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following disclaimers - in the documentation and/or other materials provided with the - distribution. - - Neither the names of Mozilla, - nor the names of its contributors may be used to endorse - or promote products derived from this Software without specific prior - written permission. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR -ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE SOFTWARE. - -============================================================================== - -This program uses portions of Node.js source code located in src/library_path.js, -in accordance with the terms of the MIT license. Node's license follows: - - """ - Copyright Joyent, Inc. and other Node contributors. All rights reserved. - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to - deal in the Software without restriction, including without limitation the - rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - sell copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - IN THE SOFTWARE. - """ - -The musl libc project is bundled in this repo, and it has the MIT license, see -system/lib/libc/musl/COPYRIGHT - -The third_party/ subdirectory contains code with other licenses. None of it is -used by default, but certain options use it (e.g., the optional closure compiler -flag will run closure compiler from third_party/). - - -===== DOMPurify ===== - - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/packages/client/ui-primitives/package.json b/packages/client/ui-primitives/package.json index 587cd95ee6..2613c3aa98 100644 --- a/packages/client/ui-primitives/package.json +++ b/packages/client/ui-primitives/package.json @@ -45,15 +45,11 @@ "mdast-util-gfm": "^3.1.0", "mdast-util-math": "^3.0.0", "micromark-extension-gfm": "^3.0.0", - "micromark-util-sanitize-uri": "^2.0.1", - "mermaid": "11.16.0", - "@viz-js/viz": "3.30.0", - "dompurify": "3.4.11" + "micromark-util-sanitize-uri": "^2.0.1" }, "files": [ "lib/index.js", "lib/**/*.css", - "THIRD_PARTY_PREVIEW_NOTICES.txt", "lib/types/**/*.d.ts" ], "peerDependencies": { diff --git a/packages/client/ui-primitives/src/markdown/CodeBlock.module.css b/packages/client/ui-primitives/src/markdown/CodeBlock.module.css index a443ff1e07..a6e25ba39f 100644 --- a/packages/client/ui-primitives/src/markdown/CodeBlock.module.css +++ b/packages/client/ui-primitives/src/markdown/CodeBlock.module.css @@ -57,7 +57,6 @@ display: flex; align-items: center; flex-shrink: 0; - gap: 12px; } .copyButton { @@ -99,89 +98,6 @@ color: var(--dsw-alias-label-primary); } -/* Alternate content is a figure; its actions overlay instead of taking a title row. */ -.preview.showingPreview { - background: transparent; -} - -.preview .bannerWrap { - position: absolute; - top: 8px; - right: 8px; - z-index: 1; - background: transparent; -} - -.preview .banner { - padding: 2px; - background: var(--dsw-alias-bg-base); - border: 0; - border-radius: 8px; - box-shadow: var(--dsw-elevation-panel); -} - -.preview .action { - gap: 2px; -} - -.iconButton { - display: inline-flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - border-radius: 6px; - color: var(--dsw-alias-label-secondary); -} - -.iconButton span { - display: flex; -} - -.iconButton:hover { - color: var(--dsw-alias-label-primary); - background: var(--dsw-alias-interactive-bg-hover); -} - -.preview:not(.showingPreview) :where(pre) { - padding-top: 48px; -} - -@media (hover: hover) and (pointer: fine) { - .preview .bannerWrap { - opacity: 0; - pointer-events: none; - } - - .preview:hover .bannerWrap, - .preview:focus-within .bannerWrap { - opacity: 1; - pointer-events: auto; - } -} - -@media (any-pointer: coarse) { - .preview { - padding-bottom: 56px; - } - - .preview .bannerWrap { - top: auto; - bottom: 8px; - opacity: 1; - pointer-events: auto; - } - - .preview .iconButton { - width: 44px; - height: 44px; - } - - .preview:not(.showingPreview) :where(pre) { - padding-top: 16px; - } -} - .numbered :where(pre) code { display: block; counter-reset: source-line; diff --git a/packages/client/ui-primitives/src/markdown/CodeBlock.tsx b/packages/client/ui-primitives/src/markdown/CodeBlock.tsx index 367ae5febe..02cdbe683a 100644 --- a/packages/client/ui-primitives/src/markdown/CodeBlock.tsx +++ b/packages/client/ui-primitives/src/markdown/CodeBlock.tsx @@ -2,8 +2,6 @@ import { Fragment, useCallback, useMemo, useRef, useState, useSyncExternalStore import type { CSSProperties, ReactNode } from 'react' import clsx from 'clsx' import { writeClipboard } from '../clipboard.ts' -import { Tooltip } from '../Tooltip.tsx' -import { IconCheckOutline16, IconCodeOutline16, IconCopyOutline16 } from '../icons/index.tsx' import { StreamingHighlightSession, grammarLoadCount, highlightToHtml, subscribeGrammarLoaded, } from './highlight.ts' @@ -33,8 +31,6 @@ export interface CodeBlockProps { copyLabel: string /** Copy-button label during the post-copy confirmation window. */ copiedLabel: string - /** Headerless settled preview with hover/focus actions; copying always retains the source text. */ - preview?: { content: ReactNode; previewLabel: string; sourceLabel: string } | undefined } /** @@ -63,7 +59,7 @@ function renderLine(line: readonly HighlightSpan[], index: number): ReactNode { ) } -export function CodeBlock({ code, lang, streaming, className, lineNumbers = false, copyLabel, copiedLabel, preview }: CodeBlockProps) { +export function CodeBlock({ code, lang, streaming, className, lineNumbers = false, copyLabel, copiedLabel }: CodeBlockProps) { const trimmed = code.endsWith('\n') ? code.slice(0, -1) : code const sourceLines = lineNumbers ? trimmed.split('\n') : undefined const rootRef = useRef(null) @@ -147,13 +143,13 @@ export function CodeBlock({ code, lang, streaming, className, lineNumbers = fals [streaming, highlighting, streamedBody, trimmed, lang, loaded], ) const [copied, setCopied] = useState(false) - const [showSource, setShowSource] = useState(false) - const previewAvailable = preview !== undefined && streaming !== true - const showingPreview = previewAvailable && !showSource const onCopy = useCallback(() => { if (copied) return - void writeClipboard(trimmed).then((ok) => { + /* v8 ignore next -- both arms always mount a
; trimmed is the
+       typed fallback if the DOM shape ever diverges. */
+    const text = rootRef.current?.querySelector('pre')?.textContent ?? trimmed
+    void writeClipboard(text).then((ok) => {
       if (!ok) return
       setCopied(true)
       window.setTimeout(() => { setCopied(false) }, 1000)
@@ -175,22 +171,8 @@ export function CodeBlock({ code, lang, streaming, className, lineNumbers = fals
         
) - const copyActionLabel = copied ? copiedLabel : copyLabel - const copyAction = ( - - ) - return ( -
{/* The attribute carries no style here; owner stylesheets use it to pin the banner (sidebar code preview, horizontal scroll). */}
- {!previewAvailable &&
{lang ?? ''}
} +
{lang ?? ''}
- {previewAvailable && ( - - - - )} - {previewAvailable - ? {copyAction} - : copyAction} +
- {previewAvailable && } - {!showingPreview && body} + {body}
) } diff --git a/packages/client/ui-primitives/src/markdown/SourcePreview.module.css b/packages/client/ui-primitives/src/markdown/SourcePreview.module.css deleted file mode 100644 index 46a674ed0a..0000000000 --- a/packages/client/ui-primitives/src/markdown/SourcePreview.module.css +++ /dev/null @@ -1,35 +0,0 @@ -.canvas { - overflow: auto; - padding: 16px; - background: var(--dsw-alias-markdown-code-block); - border-radius: 12px; -} - -.diagram { - display: block; - max-width: 100%; - height: auto; - margin: 0 auto; -} - -.status { - padding: 16px; - color: var(--dsw-alias-label-secondary); -} - -.staging { - position: fixed; - top: 0; - left: -100000px; - visibility: hidden; - pointer-events: none; -} - -.frame { - display: block; - width: 100%; - height: 400px; - border: 0; - border-radius: 12px; - background: var(--dsw-alias-markdown-code-block); -} diff --git a/packages/client/ui-primitives/src/markdown/SourcePreview.tsx b/packages/client/ui-primitives/src/markdown/SourcePreview.tsx deleted file mode 100644 index 3ce07fb34d..0000000000 --- a/packages/client/ui-primitives/src/markdown/SourcePreview.tsx +++ /dev/null @@ -1,71 +0,0 @@ -/** Read-only diagram preview with source fallback and per-source async ownership. */ - -import { useEffect, useState } from 'react' -import css from './SourcePreview.module.css' -import { readPreviewTheme } from './preview-theme.ts' - -/** Localized preview states; the diagram source remains verbatim. */ -export interface PreviewLabels { - diagram: string - loading: string - error: string -} - -type Result = { kind: 'ok'; code: string; src: string } | { kind: 'error'; code: string } - -/* v8 ignore next 3 -- closed-union backstop; only reached if a result is forged */ -function assertNever(value: never): never { - throw new Error(`unreachable preview result: ${String(value)}`) -} - -/** - * Display a complete document or diagram, retaining the source when rendering fails. - * @param props - Source and complete localized labels. Source and document theme changes cancel obsolete renders. - * @returns A loading status, an inert image or sandboxed document, or an error with the original source. - */ -export function SourcePreview({ code, labels, render, document: isDocument = false }: { - code: string - labels: PreviewLabels - render: (code: string, signal: AbortSignal) => string | Promise - document?: boolean -}) { - const [result, setResult] = useState(null) - useEffect(() => { - let controller: AbortController | undefined - let palette: string | undefined - const update = () => { - const next = JSON.stringify(readPreviewTheme()) - if (next === palette) return - palette = next - controller?.abort() - const current = new AbortController() - controller = current - void (async () => await render(code, current.signal))().then( - (src) => { if (!current.signal.aborted) setResult({ kind: 'ok', code, src }) }, - () => { if (!current.signal.aborted) setResult({ kind: 'error', code }) }, - ) - } - // Isolated image/frame documents cannot inherit the host's CSS variables. - const observer = new MutationObserver(update) - observer.observe(document.documentElement, { attributes: true, attributeFilter: ['style'] }) - observer.observe(document.body, { attributes: true, attributeFilter: ['style', 'data-ds-dark-theme'] }) - update() - return () => { observer.disconnect(); controller?.abort() } - }, [code, render]) - - if (result?.code !== code) return
{labels.loading}
- switch (result.kind) { - case 'ok': - if (isDocument) return -
`, new AbortController().signal) - const parsed = new DOMParser().parseFromString(doc, 'text/html') - expect(parsed.querySelector('h1')?.textContent).toBe('Example') - expect(parsed.querySelector('style')?.textContent).toBe('h1 { color: green }') - expect(parsed.querySelector('script, iframe, base, [href], [action], [formaction], [onload]')).toBeNull() - expect(parsed.querySelectorAll('meta[http-equiv]').length).toBe(1) - expect(parsed.querySelector('meta[http-equiv]')?.getAttribute('content')).toContain("default-src 'none'") - }) - - it('encodes SVG as an image rather than executable document markup', () => { - const code = '' - const url = renderSvg(code, new AbortController().signal) - expect(url).toMatch(/^data:image\/svg\+xml;charset=utf-8,/) - expect(decodeURIComponent(url.slice(url.indexOf(',') + 1))).toBe(code) - }) - - it.each(['', '', ''])('rejects non-SVG or malformed XML: %s', (code) => { - expect(() => renderSvg(code, new AbortController().signal)).toThrow('Invalid SVG document') - }) - - it.each([renderHtml, renderSvg])('does not prepare a cancelled document', (renderDocument) => { - const controller = new AbortController() - controller.abort() - expect(() => renderDocument(svg, controller.signal)).toThrow(controller.signal.reason) - }) -}) diff --git a/packages/client/ui-primitives/tests/graphviz-runtime.client.spec.ts b/packages/client/ui-primitives/tests/graphviz-runtime.client.spec.ts deleted file mode 100644 index 1a7b170516..0000000000 --- a/packages/client/ui-primitives/tests/graphviz-runtime.client.spec.ts +++ /dev/null @@ -1,67 +0,0 @@ -// @vitest-environment jsdom - -import { afterEach, beforeEach, expect, it, vi } from 'vitest' - -const instance = vi.fn() -beforeEach(() => { - vi.resetModules() - instance.mockReset() - vi.doMock('@viz-js/viz', () => ({ instance })) -}) -afterEach(() => { vi.doUnmock('@viz-js/viz') }) - -it('retries failed initialization and reuses the loaded runtime', async () => { - const renderString = vi.fn().mockReturnValue('') - instance.mockRejectedValueOnce(new Error('WASM unavailable')).mockResolvedValue({ renderString }) - const { renderGraphviz } = await import('../src/markdown/graphviz.ts') - await expect(renderGraphviz('digraph {}', new AbortController().signal)).rejects.toThrow('WASM unavailable') - await expect(renderGraphviz('digraph {}', new AbortController().signal)).resolves.toContain('data:image/svg+xml') - await renderGraphviz('digraph {}', new AbortController().signal) - expect(instance).toHaveBeenCalledTimes(2) -}) - -it('skips layout after cancellation during runtime loading', async () => { - const pending = Promise.withResolvers<{ renderString: ReturnType }>() - instance.mockReturnValue(pending.promise) - const { renderGraphviz } = await import('../src/markdown/graphviz.ts') - const controller = new AbortController() - const result = renderGraphviz('digraph {}', controller.signal) - const rejection = expect(result).rejects.toMatchObject({ name: 'AbortError' }) - controller.abort() - const renderString = vi.fn() - pending.resolve({ renderString }) - await rejection - expect(renderString).not.toHaveBeenCalled() -}) - -it('uses theme defaults without rewriting authored DOT attributes', async () => { - const previous = document.body.getAttribute('style') - try { - document.body.style.setProperty('--dsw-alias-label-primary', 'rgb(230, 231, 232)') - document.body.style.setProperty('--dsw-alias-label-secondary', '#aabbcc') - document.body.style.setProperty('--dsw-alias-label-tertiary', '#8899aa') - document.body.style.setProperty('--dsw-alias-bg-layer-2', '#222222') - const renderString = vi.fn().mockReturnValue('') - instance.mockResolvedValue({ renderString }) - const { renderGraphviz } = await import('../src/markdown/graphviz.ts') - const code = 'digraph { a [color="red"]; a -> b }' - await renderGraphviz(code, new AbortController().signal) - expect(renderString).toHaveBeenCalledWith(code, expect.objectContaining({ - nodeAttributes: { color: '#8899aa', fontcolor: '#e6e7e8', fillcolor: '#222222', style: 'filled' }, - edgeAttributes: { color: '#aabbcc', fontcolor: '#e6e7e8' }, - })) - await renderGraphviz(code, new AbortController().signal, { - engine: 'neato', format: 'plain', yInvert: true, - graphAttributes: { ranksep: 1 }, nodeAttributes: { shape: 'box' }, edgeAttributes: { color: 'red' }, - }) - expect(renderString).toHaveBeenLastCalledWith(code, { - engine: 'neato', format: 'svg', yInvert: true, - graphAttributes: { bgcolor: 'transparent', fontcolor: '#e6e7e8', ranksep: 1 }, - nodeAttributes: { color: '#8899aa', fontcolor: '#e6e7e8', fillcolor: '#222222', style: 'filled', shape: 'box' }, - edgeAttributes: { color: 'red', fontcolor: '#e6e7e8' }, - }) - } finally { - if (previous === null) document.body.removeAttribute('style') - else document.body.setAttribute('style', previous) - } -}) diff --git a/packages/client/ui-primitives/tests/mermaid-runtime.client.spec.ts b/packages/client/ui-primitives/tests/mermaid-runtime.client.spec.ts deleted file mode 100644 index c5a4ccd6d3..0000000000 --- a/packages/client/ui-primitives/tests/mermaid-runtime.client.spec.ts +++ /dev/null @@ -1,133 +0,0 @@ -// @vitest-environment jsdom - -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' - -const initialize = vi.fn() -const renderDiagram = vi.fn() - -beforeEach(() => { - vi.resetModules() - vi.resetAllMocks() - vi.doMock('mermaid', () => ({ default: { initialize, render: renderDiagram } })) -}) -afterEach(() => { vi.doUnmock('mermaid') }) - -describe('Mermaid runtime', () => { - it('allows another attempt after the runtime import fails', async () => { - vi.doMock('mermaid', () => { throw new Error('runtime unavailable') }) - const { renderMermaid } = await import('../src/markdown/mermaid.ts') - await expect(renderMermaid('first', new AbortController().signal)).rejects.toThrow() - expect(initialize).not.toHaveBeenCalled() - vi.doMock('mermaid', () => ({ default: { initialize, render: renderDiagram } })) - renderDiagram.mockResolvedValue({ svg: '' }) - await expect(renderMermaid('retry', new AbortController().signal)).resolves.toContain('data:image/svg+xml') - expect(renderDiagram).toHaveBeenCalledOnce() - }) - - it('accepts native configuration without weakening preview restrictions', async () => { - const { renderMermaid } = await import('../src/markdown/mermaid.ts') - renderDiagram.mockResolvedValue({ svg: '' }) - await renderMermaid('flowchart LR', new AbortController().signal, { - // oxlint-disable-next-line typescript/no-deprecated - layout: 'dagre', flowchart: { nodeSpacing: 40, htmlLabels: true }, - themeVariables: { primaryColor: '#123456' }, - securityLevel: 'loose', startOnLoad: true, suppressErrorRendering: false, htmlLabels: true, secure: [], - }) - expect(initialize.mock.calls[0]![0]).toMatchObject({ - layout: 'dagre', flowchart: { nodeSpacing: 40, htmlLabels: false }, - themeVariables: { primaryColor: '#123456', darkMode: false }, - securityLevel: 'strict', startOnLoad: false, suppressErrorRendering: true, htmlLabels: false, - }) - expect(initialize.mock.calls[0]![0]).toHaveProperty('secure', expect.arrayContaining(['securityLevel'])) - }) - - it('reuses the runtime and removes each measurement container after rendering', async () => { - const { renderMermaid } = await import('../src/markdown/mermaid.ts') - expect(initialize).not.toHaveBeenCalled() - const stages: HTMLElement[] = [] - const ids: string[] = [] - renderDiagram.mockImplementation(async (id: string, code: string, stage: HTMLElement) => { - expect(stage.isConnected).toBe(true) - expect(stage.getAttribute('aria-hidden')).toBe('true') - stages.push(stage) - ids.push(id) - return { svg: `${code}` } - }) - const results = await Promise.all([ - renderMermaid('中文', new AbortController().signal), - renderMermaid('second', new AbortController().signal), - ]) - expect(initialize).toHaveBeenCalledTimes(2) - expect(initialize).toHaveBeenCalledWith(expect.objectContaining({ - startOnLoad: false, securityLevel: 'strict', suppressErrorRendering: true, htmlLabels: false, - secure: [ - 'secure', 'securityLevel', 'startOnLoad', 'maxTextSize', 'maxEdges', - 'suppressErrorRendering', 'theme', 'themeVariables', 'themeCSS', 'htmlLabels', 'flowchart', - ], - })) - expect(results.map(url => decodeURIComponent(url.split(',')[1]!))).toEqual(['中文', 'second']) - expect(new Set(ids).size).toBe(2) - expect(stages.every(stage => !stage.isConnected)).toBe(true) - }) - - it('keeps theme initialization with its render and recovers the queue after failure', async () => { - const { renderMermaid } = await import('../src/markdown/mermaid.ts') - const first = Promise.withResolvers<{ svg: string }>() - renderDiagram.mockReturnValueOnce(first.promise).mockResolvedValue({ svg: '' }) - const light = renderMermaid('first', new AbortController().signal) - const failure = expect(light).rejects.toThrow('invalid') - await vi.waitFor(() => { expect(renderDiagram).toHaveBeenCalledOnce() }) - const previous = document.documentElement.style.colorScheme - try { - document.documentElement.style.colorScheme = 'dark' - const dark = renderMermaid('second', new AbortController().signal) - await Promise.resolve() - expect(initialize).toHaveBeenCalledOnce() - first.reject(new Error('invalid')) - await failure - await dark - expect(initialize.mock.calls[1]![0]).toMatchObject({ themeVariables: { darkMode: true } }) - } finally { - document.documentElement.style.colorScheme = previous - } - }) - - it('removes measurement DOM even when Mermaid rejects', async () => { - const { renderMermaid } = await import('../src/markdown/mermaid.ts') - let stage: HTMLElement | undefined - const error = new Error('bad diagram') - renderDiagram.mockImplementation(async (_id: string, _code: string, target: HTMLElement) => { - stage = target - target.innerHTML = 'partial render' - throw error - }) - await expect(renderMermaid('bad', new AbortController().signal)).rejects.toBe(error) - expect(stage?.isConnected).toBe(false) - }) - - it('preserves intrinsic diagram size instead of stretching percentage-width SVG images', async () => { - const { renderMermaid } = await import('../src/markdown/mermaid.ts') - renderDiagram.mockResolvedValue({ svg: '' }) - const url = await renderMermaid('flowchart LR', new AbortController().signal) - const svg = new DOMParser().parseFromString(decodeURIComponent(url.split(',')[1]!), 'image/svg+xml').documentElement - expect(svg.getAttribute('width')).toBe('420') - expect(svg.getAttribute('height')).toBe('180') - }) - - it('does not start cancelled work after loading the runtime', async () => { - const { renderMermaid } = await import('../src/markdown/mermaid.ts') - const controller = new AbortController() - controller.abort() - await expect(renderMermaid('unused', controller.signal)).rejects.toMatchObject({ name: 'AbortError' }) - expect(renderDiagram).not.toHaveBeenCalled() - }) - - it('allows another attempt after runtime initialization fails', async () => { - const { renderMermaid } = await import('../src/markdown/mermaid.ts') - initialize.mockImplementationOnce(() => { throw new Error('initialization failed') }) - await expect(renderMermaid('first', new AbortController().signal)).rejects.toThrow('initialization failed') - renderDiagram.mockResolvedValue({ svg: '' }) - await expect(renderMermaid('retry', new AbortController().signal)).resolves.toContain('data:image/svg+xml') - expect(initialize).toHaveBeenCalledTimes(2) - }) -}) diff --git a/packages/client/ui-primitives/tests/source-preview.client.spec.tsx b/packages/client/ui-primitives/tests/source-preview.client.spec.tsx deleted file mode 100644 index 7e63f06929..0000000000 --- a/packages/client/ui-primitives/tests/source-preview.client.spec.tsx +++ /dev/null @@ -1,162 +0,0 @@ -// @vitest-environment jsdom - -import { act, cleanup, fireEvent, render, screen } from '@testing-library/react' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { SourcePreview } from '../src/markdown/SourcePreview.tsx' -import { CodeBlock } from '../src/markdown/CodeBlock.tsx' -import { renderMermaid } from '../src/markdown/mermaid.ts' -import { MarkdownText } from '../src/markdown/MarkdownText.tsx' -import { markdownLabels } from './labels.client.ts' - -vi.mock('../src/markdown/mermaid.ts', () => ({ renderMermaid: vi.fn() })) - -const labels = { - diagram: 'Mermaid diagram', loading: 'Rendering diagram…', error: 'Unable to render this diagram.', - preview: 'Preview', source: 'Source', -} -const preview = { - preview: labels.preview, source: labels.source, - mermaid: labels, graphviz: labels, svg: labels, html: labels, -} -const source = 'flowchart LR\n A[Input] --> B[Preview]' -const imageUrl = 'data:image/svg+xml;charset=utf-8,%3Csvg%2F%3E' - -afterEach(() => { cleanup(); vi.unstubAllGlobals() }) -beforeEach(() => { vi.resetAllMocks() }) - -describe('SourcePreview', () => { - it('updates mounted previews on theme changes and ignores obsolete completions', async () => { - const previous = document.documentElement.style.colorScheme - const old = Promise.withResolvers() - vi.mocked(renderMermaid).mockReturnValueOnce(old.promise).mockResolvedValue(imageUrl) - const view = render() - try { - const oldSignal = vi.mocked(renderMermaid).mock.calls[0]![1] - await act(async () => { document.documentElement.style.colorScheme = 'dark' }) - expect(oldSignal.aborted).toBe(true) - const image = await screen.findByRole('img') - await act(async () => { old.resolve('obsolete') }) - expect(image.getAttribute('src')).toBe(imageUrl) - await act(async () => { document.body.style.setProperty('--unrelated', '1') }) - expect(renderMermaid).toHaveBeenCalledTimes(2) - await act(async () => { document.documentElement.style.colorScheme = 'light' }) - expect(renderMermaid).toHaveBeenCalledTimes(3) - view.unmount() - await act(async () => { document.documentElement.style.colorScheme = 'dark' }) - expect(renderMermaid).toHaveBeenCalledTimes(3) - } finally { - view.unmount() - document.documentElement.style.colorScheme = previous - document.body.style.removeProperty('--unrelated') - } - }) - it('shows loading until rendering completes, then displays an image without inserting SVG', async () => { - const pending = Promise.withResolvers() - vi.mocked(renderMermaid).mockReturnValue(pending.promise) - const view = render() - expect(screen.getByRole('status').textContent).toBe(labels.loading) - expect(screen.queryByRole('img')).toBeNull() - await act(async () => { pending.resolve(imageUrl) }) - expect(screen.getByRole('img', { name: labels.diagram }).getAttribute('src')).toBe(imageUrl) - expect(view.container.querySelector('svg')).toBeNull() - expect(screen.queryByRole('status')).toBeNull() - }) - - it('retains invalid source and recovers when the source changes', async () => { - vi.mocked(renderMermaid).mockRejectedValueOnce(new Error('Parse error')) - const view = render() - expect((await screen.findByText(labels.error)).getAttribute('role')).toBe('status') - expect(view.container.querySelector('pre code')?.textContent).toBe('invalid') - const next = Promise.withResolvers() - vi.mocked(renderMermaid).mockReturnValue(next.promise) - view.rerender() - expect(screen.getByRole('status').textContent).toBe(labels.loading) - expect(view.container.querySelector('pre')).toBeNull() - await act(async () => { next.resolve(imageUrl) }) - expect(screen.getByRole('img').getAttribute('src')).toBe(imageUrl) - }) - - it.each(['resolve', 'reject'] as const)('ignores a stale %s after a newer source finishes', async (outcome) => { - const old = Promise.withResolvers() - const next = Promise.withResolvers() - vi.mocked(renderMermaid).mockReturnValueOnce(old.promise).mockReturnValueOnce(next.promise) - const view = render() - const oldSignal = vi.mocked(renderMermaid).mock.calls[0]![1] - view.rerender() - expect(oldSignal.aborted).toBe(true) - await act(async () => { next.resolve(imageUrl) }) - await act(async () => { - if (outcome === 'resolve') old.resolve('obsolete-image') - else old.reject(new Error('obsolete-error')) - }) - expect(screen.getByRole('img').getAttribute('src')).toBe(imageUrl) - expect(screen.queryByRole('status')).toBeNull() - }) - - it('cancels the owner when unmounted while loading', async () => { - const pending = Promise.withResolvers() - vi.mocked(renderMermaid).mockReturnValue(pending.promise) - const view = render() - const signal = vi.mocked(renderMermaid).mock.calls[0]![1] - view.unmount() - expect(signal.aborted).toBe(true) - await act(async () => { pending.reject(new Error('cancelled')) }) - expect(screen.queryByRole('img')).toBeNull() - }) -}) - -describe('Markdown Mermaid fences', () => { - it('retains a preview when switching to numbered source', async () => { - vi.mocked(renderMermaid).mockResolvedValue(imageUrl) - const view = render(, - previewLabel: labels.preview, sourceLabel: labels.source, - }} />) - const diagram = await screen.findByRole('img', { name: labels.diagram }) - expect(view.container.querySelector('[data-line-numbers]')).not.toBeNull() - fireEvent.click(screen.getByRole('button', { name: labels.source })) - expect([...view.container.querySelectorAll('code > .line')].map(line => line.textContent)).toEqual(source.split('\n')) - expect(diagram.isConnected).toBe(true) - expect(screen.queryByRole('img')).toBeNull() - fireEvent.click(screen.getByRole('button', { name: labels.preview })) - expect(screen.getByRole('img', { name: labels.diagram })).toBe(diagram) - expect(renderMermaid).toHaveBeenCalledOnce() - }) - - it('keeps streaming source literal, then offers preview, source and source copying', async () => { - const writeText = vi.fn().mockResolvedValue(undefined) - vi.stubGlobal('navigator', Object.create(navigator, { clipboard: { value: { writeText } } })) - vi.mocked(renderMermaid).mockResolvedValue(imageUrl) - const props = { text: `\`\`\`mermaid\n${source}\n\`\`\``, labels: { ...markdownLabels, preview } } - const view = render() - expect(view.container.querySelector('pre code')?.textContent).toBe(source) - expect(renderMermaid).not.toHaveBeenCalled() - expect(screen.queryByRole('button', { name: labels.source })).toBeNull() - view.rerender() - const diagram = await screen.findByRole('img', { name: labels.diagram }) - expect(screen.queryByText('mermaid')).toBeNull() - expect(screen.getByRole('button', { name: labels.source }).textContent).toBe('') - expect(screen.getByRole('button', { name: markdownLabels.code.copyLabel }).textContent).toBe('') - fireEvent.click(screen.getByRole('button', { name: markdownLabels.code.copyLabel })) - await screen.findByRole('button', { name: markdownLabels.code.copiedLabel }) - expect(writeText).toHaveBeenCalledWith(source) - fireEvent.click(screen.getByRole('button', { name: labels.source })) - expect(view.container.querySelector('pre code')?.textContent).toBe(source) - expect(screen.queryByRole('img')).toBeNull() - expect(diagram.isConnected).toBe(true) - expect(screen.queryByText('mermaid')).toBeNull() - fireEvent.click(screen.getByRole('button', { name: labels.preview })) - expect(screen.getByRole('img', { name: labels.diagram })).toBe(diagram) - expect(screen.queryByRole('status')).toBeNull() - expect(renderMermaid).toHaveBeenCalledOnce() - }) - - it('leaves other languages and consumers without preview labels as code', () => { - const view = render() - expect(view.container.querySelector('pre code')?.textContent).toBe(source) - view.rerender() - expect(view.container.querySelector('pre code')?.textContent).toBe('flowchart LR') - expect(renderMermaid).not.toHaveBeenCalled() - }) -}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 90df336c0a..04b7a7e33a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3327,18 +3327,12 @@ importers: '@types/react-dom': specifier: ~18.3.0 version: 18.3.7(@types/react@18.3.31) - '@viz-js/viz': - specifier: 3.30.0 - version: 3.30.0 anser: specifier: ^2.3.5 version: 2.3.5 clsx: specifier: ^2.0.0 version: 2.1.1 - dompurify: - specifier: 3.4.11 - version: 3.4.11 katex: specifier: ^0.16.47 version: 0.16.47 @@ -3351,9 +3345,6 @@ importers: mdast-util-math: specifier: ^3.0.0 version: 3.0.0 - mermaid: - specifier: 11.16.0 - version: 11.16.0 micromark-core-commonmark: specifier: ^2.0.3 version: 2.0.3 @@ -14295,9 +14286,6 @@ packages: '@vitest/utils@4.1.8': resolution: {integrity: sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==} - '@viz-js/viz@3.30.0': - resolution: {integrity: sha512-2zmcP55QY44uQfZ+GvgNHmGd8S6KUD+2eMur9AbYybSzrOd/m8nMUg0ZircJNiJRKGgVBKAE8kh0hAwpUFUFQQ==} - '@vscode/ripgrep-darwin-arm64@1.18.0': resolution: {integrity: sha512-r3ktHSvbFycQNF6sl7sNDPocpsI7J+mEzh1IaZFkY0spm3k2Z9t8hPAeOK7+p0l6p6/swkQC14XWX01low+94Q==} cpu: [arm64] @@ -20907,8 +20895,6 @@ snapshots: convert-source-map: 2.0.0 tinyrainbow: 3.1.0 - '@viz-js/viz@3.30.0': {} - '@vscode/ripgrep-darwin-arm64@1.18.0': optional: true diff --git a/scripts/check-workspace-constraints.spec.ts b/scripts/check-workspace-constraints.spec.ts index 7d0177eb45..2042754128 100644 --- a/scripts/check-workspace-constraints.spec.ts +++ b/scripts/check-workspace-constraints.spec.ts @@ -3,7 +3,6 @@ import { describe, expect, it } from 'vitest' import { checkDshFamilyVersion, - checkWorkspaceManifest, checkExperimentalDependencyIsolation, checkExperimentalManifest, expectedDshPackageFiles, @@ -144,16 +143,3 @@ describe('package payload constraints', () => { ]) }) }) - -it('requires preview license notices in the published UI primitives payload', () => { - const name = '@deepseek-ai/dsh-client-ui-primitives' - const files = expectedDshPackageFiles({ name }) - expect(files).toContain('THIRD_PARTY_PREVIEW_NOTICES.txt') - const check = (entries: readonly string[]) => checkWorkspaceManifest({ - dir: 'packages/client/ui-primitives', manifest: { name, files: [...entries] }, - }).filter(message => message.includes('package.json files')) - expect(check(files)).toEqual([]) - expect(check(files.filter(file => file !== 'THIRD_PARTY_PREVIEW_NOTICES.txt'))).toEqual([ - expect.stringContaining('package.json files must be'), - ]) -}) diff --git a/scripts/check-workspace-constraints.ts b/scripts/check-workspace-constraints.ts index 744b5be9ca..95a8835eeb 100644 --- a/scripts/check-workspace-constraints.ts +++ b/scripts/check-workspace-constraints.ts @@ -155,7 +155,7 @@ const packageFileExtras: Readonly> = { // them through its own CSS pipeline, so the sheets are published artifacts. // The glob covers whichever sheets a package emits; sourcemaps stay // unpublished, as everywhere else in the repository. - '@deepseek-ai/dsh-client-ui-primitives': ['lib/**/*.css', 'THIRD_PARTY_PREVIEW_NOTICES.txt'], + '@deepseek-ai/dsh-client-ui-primitives': ['lib/**/*.css'], '@deepseek-ai/dsh-client-ui-dockkit': ['lib/**/*.css'], '@deepseek-ai/dsh-client-web': ['lib/**/*.css'], '@deepseek-ai/dsh-client-ui-theme': ['lib/styles'], diff --git a/scripts/gen-third-party-notices.spec.ts b/scripts/gen-third-party-notices.spec.ts index 46b1b35d11..fabce6e8c9 100644 --- a/scripts/gen-third-party-notices.spec.ts +++ b/scripts/gen-third-party-notices.spec.ts @@ -4,7 +4,6 @@ import { tmpdir } from 'node:os' import { describe, expect, it } from 'vitest' import { CLAUDE_AGENT_SDK_PACKAGE, - assertPreviewDistribution, assertRuntimeLicenses, claudeDistributionFromManifest, collectPythonDependencies, @@ -396,26 +395,3 @@ describe('manifestPatterns', () => { ]) }) }) - -describe('preview distribution licenses', () => { - const dependencies = { mermaid: '11.16.0', '@viz-js/viz': '3.30.0', dompurify: '3.4.11' } - const sources = [ - 'pkg:docker/emscripten/emsdk@5.0.7?platform=linux%2Famd64', - 'https://github.com/libexpat/libexpat/releases/download/R_2_8_4/expat-2.8.4.tar.gz', - 'https://gitlab.com/api/v4/projects/4207231/packages/generic/graphviz-releases/16.0.0/graphviz-16.0.0.tar.gz', - ] - it('accepts the reviewed wrapper and native sources', () => { - expect(() => { assertPreviewDistribution(dependencies, sources) }).not.toThrow() - const notices = readFileSync(resolve(root, 'packages/client/ui-primitives/THIRD_PARTY_PREVIEW_NOTICES.txt'), 'utf8') - expect(notices).toContain('Eclipse Public License - v 2.0') - expect(notices).toContain(sources[2]) - expect(notices).toContain(readFileSync(resolve(root, 'packages/client/ui-primitives/node_modules/mermaid/LICENSE'), 'utf8').trim()) - expect(notices).toContain(readFileSync(resolve(root, 'packages/client/ui-primitives/node_modules/dompurify/LICENSE'), 'utf8').trim()) - }) - it.each(Object.keys(dependencies))('rejects an unreviewed %s upgrade', (name) => { - expect(() => { assertPreviewDistribution({ ...dependencies, [name]: '99.0.0' }, sources) }).toThrow('preview notices: review') - }) - it.each([sources.slice(1), [...sources, 'https://example.com/new-library'], [...sources.slice(0, 2), 'graphviz-next']])('rejects changed embedded sources', (...uris) => { - expect(() => { assertPreviewDistribution(dependencies, uris) }).toThrow('native sources') - }) -}) diff --git a/scripts/gen-third-party-notices.ts b/scripts/gen-third-party-notices.ts index 2130723202..9e83956e5e 100644 --- a/scripts/gen-third-party-notices.ts +++ b/scripts/gen-third-party-notices.ts @@ -685,28 +685,6 @@ ${rows.join('\n')} ` } -/** - * Require renewed license review when a preview dependency or embedded source changes. - * @param dependencies - UI primitives' browser build dependencies. - * @param sources - Native source URIs from the installed Viz.js build provenance. - * @returns Nothing; throws when a version or native source differs from the reviewed distribution. - */ -export function assertPreviewDistribution(dependencies: Record, sources: readonly string[]): void { - for (const [name, version] of Object.entries({ mermaid: '11.16.0', '@viz-js/viz': '3.30.0', dompurify: '3.4.11' })) { - if (dependencies[name] !== version) { - throw new Error(`preview notices: review ${name} ${String(dependencies[name])} licenses and update THIRD_PARTY_PREVIEW_NOTICES.txt`) - } - } - const expected = [ - 'pkg:docker/emscripten/emsdk@5.0.7?platform=linux%2Famd64', - 'https://github.com/libexpat/libexpat/releases/download/R_2_8_4/expat-2.8.4.tar.gz', - 'https://gitlab.com/api/v4/projects/4207231/packages/generic/graphviz-releases/16.0.0/graphviz-16.0.0.tar.gz', - ] - if (sources.length !== expected.length || expected.some(uri => !sources.includes(uri))) { - throw new Error('preview notices: review the changed Viz.js native sources and their distribution licenses') - } -} - /** * Render the complete notices document. * @returns The exact bytes THIRD_PARTY_NOTICES.md must hold after resolving browser inputs. @@ -717,16 +695,6 @@ export async function render(): Promise { // the manifests map it was resolved from; render() owns that single load. workspaceLinkedManifestCache.clear() const { manifests, names } = loadWorkspaceManifests() - const previewPackage = 'packages/client/ui-primitives' - const provenance = JSON.parse(readFileSync(resolve(root, previewPackage, 'node_modules/@viz-js/viz/lib/provenance.json'), 'utf8')) as { - predicate: { buildDefinition: { resolvedDependencies: { uri: string }[] } } - } - const previewDependencies = manifests.get(`${previewPackage}/package.json`)?.devDependencies - if (previewDependencies === undefined) throw new Error('preview notices: missing UI primitives browser dependencies') - assertPreviewDistribution( - previewDependencies, - provenance.predicate.buildDefinition.resolvedDependencies.map(source => source.uri), - ) const npm = collectNpmDeps(manifests, names, browser) const runtimeDeps = npm.filter(dep => dep.runtime) const devDeps = npm.filter(dep => !dep.runtime) @@ -767,8 +735,6 @@ External packages installed for runtime use or distributed inside the prebuilt b ${renderNpmTable(runtimeDeps)} -The Markdown preview distribution also contains Graphviz 16.0.0 (EPL-2.0), Expat 2.8.4 (MIT), and Emscripten 5.0.7 runtime code (MIT/NCSA) inside \`@viz-js/viz\` 3.30.0. The wrapper's MIT metadata does not relicense these components. [Preview notices](packages/client/ui-primitives/THIRD_PARTY_PREVIEW_NOTICES.txt) preserve their full license texts and Graphviz source availability, together with Mermaid's MIT and DOMPurify's selected Apache-2.0 terms. The UI primitives npm package includes this file; the Web build emits it as \`preview-third-party-notices.txt\`. - pnpm applies local patches to the following packages at install time, so shipped artifacts carry modified copies; each patch file is the complete record of the modification: ${patchedLines.join('\n')} diff --git a/tsconfig.host.json b/tsconfig.host.json index e7abea1600..c5a3f118b3 100644 --- a/tsconfig.host.json +++ b/tsconfig.host.json @@ -67,7 +67,6 @@ "apps/web/tests/math-rendering.e2e.ts", "apps/web/tests/markdown-cjk-strong.e2e.ts", "apps/web/tests/markdown-inline-code-links.e2e.ts", - "apps/web/tests/markdown-mermaid.e2e.ts", "apps/web/tests/clickable-links-gallery.e2e.ts", "apps/web/tests/queue-actions.e2e.ts", "apps/web/tests/queue-image.e2e.ts", From dd393c18202b25f9734d51cb61aac8fa8b2c5ecf Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Wed, 9 Sep 2026 20:18:50 +0800 Subject: [PATCH 14/27] release(dsh): 0.1.5-alpha.2 --- apps/cli/package.json | 2 +- apps/desktop-host/package.json | 2 +- apps/desktop/package.json | 2 +- apps/web/package.json | 2 +- package.json | 2 +- packages/acp/acp/package.json | 2 +- packages/api/gateway/package.json | 2 +- packages/api/remotes/package.json | 2 +- packages/api/session-controller/package.json | 2 +- packages/api/settings-controller/package.json | 2 +- packages/api/workspace-controller/package.json | 2 +- packages/api/workspace-files/package.json | 2 +- packages/attachment/attachment-local/package.json | 2 +- packages/attachment/attachment/package.json | 2 +- packages/boot/app-boot/package.json | 2 +- packages/boot/cmdline/package.json | 2 +- packages/bundle/acp-app/package.json | 2 +- packages/bundle/base/package.json | 2 +- packages/bundle/headless/package.json | 2 +- packages/bundle/sdk-app/package.json | 2 +- packages/bundle/sdk-minimal/package.json | 2 +- packages/bundle/web-app/package.json | 2 +- packages/client/connection/package.json | 2 +- packages/client/file-upload/package.json | 2 +- packages/client/hmr/package.json | 2 +- packages/client/locale/package.json | 2 +- packages/client/modules/package.json | 2 +- packages/client/resources/package.json | 2 +- packages/client/store/package.json | 2 +- packages/client/ui-agent-preset/package.json | 2 +- packages/client/ui-approval/package.json | 2 +- packages/client/ui-attachment/package.json | 2 +- packages/client/ui-brand-official/package.json | 2 +- packages/client/ui-chat/package.json | 2 +- packages/client/ui-commands/package.json | 2 +- packages/client/ui-conversation/package.json | 2 +- packages/client/ui-deliverables/package.json | 2 +- packages/client/ui-directory-picker-browse/package.json | 2 +- packages/client/ui-directory-picker-native/package.json | 2 +- packages/client/ui-dockkit/package.json | 2 +- packages/client/ui-goal/package.json | 2 +- packages/client/ui-input-trigger/package.json | 2 +- packages/client/ui-jobs/package.json | 2 +- packages/client/ui-layout/package.json | 2 +- packages/client/ui-message-feedback/package.json | 2 +- packages/client/ui-model-selection/package.json | 2 +- packages/client/ui-open-in-app/package.json | 2 +- packages/client/ui-permission-presets/package.json | 2 +- packages/client/ui-plan/package.json | 2 +- packages/client/ui-primitives/package.json | 2 +- packages/client/ui-reference/package.json | 2 +- packages/client/ui-renderer/package.json | 2 +- packages/client/ui-schedule/package.json | 2 +- packages/client/ui-session/package.json | 2 +- packages/client/ui-settings-general/package.json | 2 +- packages/client/ui-settings-models/package.json | 2 +- packages/client/ui-settings-plugin-inventory/package.json | 2 +- packages/client/ui-settings-plugins/package.json | 2 +- packages/client/ui-settings/package.json | 2 +- packages/client/ui-sidebar-documentpreview/package.json | 2 +- packages/client/ui-sidebar-files/package.json | 2 +- packages/client/ui-sidebar-right/package.json | 2 +- packages/client/ui-sidebar/package.json | 2 +- packages/client/ui-skill/package.json | 2 +- packages/client/ui-slots/package.json | 2 +- packages/client/ui-subagent/package.json | 2 +- packages/client/ui-theme/package.json | 2 +- packages/client/ui-tool/package.json | 2 +- packages/client/ui-trajectory/package.json | 2 +- packages/client/ui-user-questions/package.json | 2 +- packages/client/ui-workflow-run/package.json | 2 +- packages/client/ui-workspace/package.json | 2 +- packages/client/web/package.json | 2 +- packages/code-runtime/code-runtime-worker-thread/package.json | 2 +- packages/code-runtime/code-runtime/package.json | 2 +- packages/compaction/command-compact/package.json | 2 +- packages/compaction/compaction-basic/package.json | 2 +- packages/compaction/compaction-tool-result-pruner/package.json | 2 +- packages/compaction/compaction/package.json | 2 +- packages/context/agent-instructions/package.json | 2 +- packages/context/file-reference-local/package.json | 2 +- packages/context/file-reference/package.json | 2 +- packages/context/session-reference/package.json | 2 +- packages/context/time-context/package.json | 2 +- packages/context/tmux-context/package.json | 2 +- packages/core/agent-default-model/package.json | 2 +- packages/core/agent-loop/package.json | 2 +- packages/core/agent-tool-presentation/package.json | 2 +- packages/core/agent/package.json | 2 +- packages/core/scope/package.json | 2 +- packages/core/session/package.json | 2 +- packages/core/system-prompt/package.json | 2 +- packages/core/tools/package.json | 2 +- packages/credentials/authorization/package.json | 2 +- packages/credentials/credentials-local/package.json | 2 +- packages/credentials/credentials/package.json | 2 +- packages/e2b/e2b/package.json | 2 +- packages/e2b/fs-e2b/package.json | 2 +- packages/e2b/subprocess-e2b/package.json | 2 +- packages/experimental/agent-team-profile/package.json | 2 +- packages/experimental/agent-team-web-profile/package.json | 2 +- packages/experimental/agent-team/package.json | 2 +- packages/experimental/client-ui-agent-team/package.json | 2 +- packages/experimental/code-runtime-python/package.json | 2 +- packages/experimental/inspector/package.json | 2 +- packages/experimental/tool-agent-team/package.json | 2 +- packages/experimental/webworker-packer/package.json | 2 +- packages/experimental/webworker-runtime/package.json | 2 +- packages/extensions/cordis-client-runner/package.json | 2 +- packages/extensions/cordis-host-runner/package.json | 2 +- packages/extensions/tool-cordis/package.json | 2 +- packages/extensions/ui-cordis/package.json | 2 +- packages/feedback/command-feedback/package.json | 2 +- packages/feedback/message-feedback/package.json | 2 +- packages/fs/fs-local/package.json | 2 +- packages/fs/fs-observation-policy/package.json | 2 +- packages/fs/fs-sandbox/package.json | 2 +- packages/fs/fs/package.json | 2 +- packages/fs/tool-fs-search/package.json | 2 +- packages/fs/tool-fs/package.json | 2 +- packages/fs/tool-present/package.json | 2 +- packages/fs/tool-str-replace-editor/package.json | 2 +- packages/goal/command-goal/package.json | 2 +- packages/goal/goal-round-driver/package.json | 2 +- packages/goal/goal/package.json | 2 +- packages/goal/tool-goal/package.json | 2 +- packages/guard/repeat-tool-reminder/package.json | 2 +- packages/guard/timeout-policy/package.json | 2 +- packages/hooks/hook-protocol/package.json | 2 +- packages/hooks/hooks-claude-code/package.json | 2 +- packages/hooks/hooks-codex/package.json | 2 +- packages/host/directory-picker-auto/package.json | 2 +- packages/host/directory-picker-browse/package.json | 2 +- packages/host/directory-picker-native/package.json | 2 +- packages/host/directory-picker/package.json | 2 +- packages/host/frontend-static/package.json | 2 +- packages/host/open-in-app/package.json | 2 +- packages/host/plugin-inventory/package.json | 2 +- packages/host/webserver/package.json | 2 +- packages/identity/anonymous-user-id/package.json | 2 +- packages/interaction/commands/package.json | 2 +- packages/interaction/permission-presets/package.json | 2 +- packages/interaction/tool-ask-user/package.json | 2 +- packages/interaction/user-approval/package.json | 2 +- packages/interaction/user-questions/package.json | 2 +- packages/jobs/jobs-local/package.json | 2 +- packages/jobs/jobs/package.json | 2 +- packages/jobs/tool-jobs/package.json | 2 +- packages/llm/deepseek-llm-api-extensions/package.json | 2 +- packages/llm/llm-deepseek/package.json | 2 +- packages/llm/llm-pi-ai/package.json | 2 +- packages/llm/llm-retry/package.json | 2 +- packages/llm/llm/package.json | 2 +- packages/llm/plugin-package-inventory-deepseek/package.json | 2 +- packages/llm/token-meter/package.json | 2 +- packages/lsp/lsp-stdio/package.json | 2 +- packages/lsp/lsp/package.json | 2 +- packages/lsp/tool-lsp/package.json | 2 +- packages/mcp/mcp-client/package.json | 2 +- packages/plan/plan-mode/package.json | 2 +- packages/preset/agent-presets/package.json | 2 +- packages/preset/persona/package.json | 2 +- packages/runtime-diagnostics/invariants/package.json | 2 +- packages/sandbox/sandbox-local/package.json | 2 +- packages/sandbox/sandbox-policy/package.json | 2 +- packages/sandbox/sandbox-windows-acl/package.json | 2 +- packages/sandbox/sandbox/package.json | 2 +- packages/schedule/schedule/package.json | 2 +- packages/sdk/client/package.json | 2 +- packages/sdk/protocol/package.json | 2 +- packages/sdk/server/package.json | 2 +- packages/session-query/session-log-export/package.json | 2 +- packages/session-query/session-query-sqlite/package.json | 2 +- packages/session-query/session-query/package.json | 2 +- packages/session-query/tool-session-query/package.json | 2 +- packages/session/session-checkpoint-policy/package.json | 2 +- packages/session/session-format-catalog/package.json | 2 +- packages/session/session-format-v0-to-v1/package.json | 2 +- packages/session/session-format-v1-to-v2/package.json | 2 +- packages/session/session-format-v2-to-v3/package.json | 2 +- packages/session/session-format/package.json | 2 +- packages/session/session-log-deepseek/package.json | 2 +- packages/session/session-persistence-jsonl/package.json | 2 +- packages/session/session-persistence/package.json | 2 +- packages/session/session-projection-cache/package.json | 2 +- packages/session/session-projection/package.json | 2 +- packages/session/session-stats/package.json | 2 +- packages/session/session-telemetry-otel/package.json | 2 +- packages/session/session-telemetry/package.json | 2 +- packages/session/session-title-all-prompts-llm/package.json | 2 +- packages/session/session-title-first-prompt-llm/package.json | 2 +- packages/session/session-title-llm/package.json | 2 +- packages/session/session-title/package.json | 2 +- packages/session/session-turn-outline/package.json | 2 +- packages/settings/settings-file/package.json | 2 +- packages/settings/settings/package.json | 2 +- packages/shell/bash-local/package.json | 2 +- packages/shell/bash-sandbox/package.json | 2 +- packages/shell/pwsh-local/package.json | 2 +- packages/shell/pwsh-sandbox/package.json | 2 +- packages/shell/shell-env/package.json | 2 +- packages/shell/shell/package.json | 2 +- packages/shell/tool-bash-persistent/package.json | 2 +- packages/shell/tool-bash/package.json | 2 +- packages/shell/tool-pwsh-persistent/package.json | 2 +- packages/shell/tool-pwsh/package.json | 2 +- packages/skill/skill-badge/package.json | 2 +- packages/skill/skill-filesystem/package.json | 2 +- packages/skill/skill/package.json | 2 +- packages/skill/tool-skill/package.json | 2 +- packages/spill/spill-local/package.json | 2 +- packages/spill/spill-policy/package.json | 2 +- packages/spill/spill/package.json | 2 +- packages/storage/storage-domain/package.json | 2 +- packages/storage/storage-json/package.json | 2 +- packages/storage/storage-sqlite/package.json | 2 +- packages/storage/storage/package.json | 2 +- packages/subagent/subagent-acp/package.json | 2 +- packages/subagent/subagent-claude-code/package.json | 2 +- packages/subagent/subagent-codex/package.json | 2 +- packages/subagent/subagent-dsh-sdk/package.json | 2 +- packages/subagent/subagent-fork-in-process/package.json | 2 +- packages/subagent/subagent-in-process-driver/package.json | 2 +- packages/subagent/subagent-spawn-in-process/package.json | 2 +- packages/subagent/subagent/package.json | 2 +- packages/subagent/tool-subagent-control/package.json | 2 +- packages/subagent/tool-subagent/package.json | 2 +- packages/subprocess/subprocess-local/package.json | 2 +- packages/subprocess/subprocess/package.json | 2 +- packages/subprocess/win32-process/package.json | 2 +- packages/terminal/terminal-bash/package.json | 2 +- packages/terminal/terminal/package.json | 2 +- packages/terminal/tool-terminal/package.json | 2 +- packages/test-support/agent-loop-testkit/package.json | 2 +- packages/test-support/client-runtime/package.json | 2 +- packages/test-support/llm-mock-server/package.json | 2 +- packages/test-support/llm-replay/package.json | 2 +- packages/test-support/loader-smoke/package.json | 2 +- packages/test-support/session-snapshot/package.json | 2 +- packages/todo/tool-todo/package.json | 2 +- packages/typert/generator/package.json | 2 +- packages/typert/loader/package.json | 2 +- packages/typert/protocol/package.json | 2 +- packages/typert/registry/package.json | 2 +- packages/util/atomic-write/package.json | 2 +- packages/util/brand/package.json | 2 +- packages/util/chunked-list/package.json | 2 +- packages/util/crypto/package.json | 2 +- packages/util/deque/package.json | 2 +- packages/util/home-paths/package.json | 2 +- packages/util/http-proxy/package.json | 2 +- packages/util/launch-environment/package.json | 2 +- packages/util/native-command/package.json | 2 +- packages/util/output-retention/package.json | 2 +- packages/util/package-manifest/package.json | 2 +- packages/util/time/package.json | 2 +- packages/util/timeout/package.json | 2 +- packages/util/values/package.json | 2 +- packages/util/workspace-path/package.json | 2 +- packages/web/tool-web/package.json | 2 +- packages/web/web-fetch-http/package.json | 2 +- packages/web/web-search-deepseek/package.json | 2 +- packages/web/web-search-exa/package.json | 2 +- packages/web/web-search-perplexity/package.json | 2 +- packages/web/web/package.json | 2 +- packages/webhook/webhook-github/package.json | 2 +- packages/webhook/webhook/package.json | 2 +- packages/workflow/tool-ralph/package.json | 2 +- packages/workflow/tool-workflow/package.json | 2 +- packages/workflow/workflow-worker-thread/package.json | 2 +- packages/workflow/workflow/package.json | 2 +- packages/workspace/workspace/package.json | 2 +- 272 files changed, 272 insertions(+), 272 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index 57084f2ac5..430451a4d4 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh", "description": "dsh CLI: profile boot, plugin management, and the browser UI alias", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/apps/desktop-host/package.json b/apps/desktop-host/package.json index fc44413c3d..68996a4894 100644 --- a/apps/desktop-host/package.json +++ b/apps/desktop-host/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-desktop-host", "description": "Private upstream-Node host process for the Electron desktop application", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "private": true, "license": "MIT", "type": "module", diff --git a/apps/desktop/package.json b/apps/desktop/package.json index d4a8146a1c..b428a0550c 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-desktop", "description": "Electron desktop shell for an isolated pnpm-installed dsh runtime", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "private": true, "license": "MIT", "type": "module", diff --git a/apps/web/package.json b/apps/web/package.json index 6c6dbc31aa..439036b0db 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-web-frontend", "description": "Web application entry: vite build over the @deepseek-ai/dsh-client-web shell library; dist/ served by apps/cli's dsh web", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/package.json b/package.json index a09fa3f997..09627ea0d6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@deepseek-ai/dsh-root", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "license": "MIT", "private": true, "type": "module", diff --git a/packages/acp/acp/package.json b/packages/acp/acp/package.json index c7a821c5d5..a5cf5a2d64 100644 --- a/packages/acp/acp/package.json +++ b/packages/acp/acp/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-acp", "description": "Automation-only Agent Client Protocol server for driving DeepSeek Harness agents over JSON-RPC stdio", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/api/gateway/package.json b/packages/api/gateway/package.json index 8530dfd990..8228fefce4 100644 --- a/packages/api/gateway/package.json +++ b/packages/api/gateway/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-api-gateway", "description": "Typert Remote Host dispatcher and Client API endpoint", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/api/remotes/package.json b/packages/api/remotes/package.json index 0d7280d631..fafd62f2e3 100644 --- a/packages/api/remotes/package.json +++ b/packages/api/remotes/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-api-remotes", "description": "Remote BFF assembly for application-selected Host capabilities", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/api/session-controller/package.json b/packages/api/session-controller/package.json index 3603490bf4..0376282f77 100644 --- a/packages/api/session-controller/package.json +++ b/packages/api/session-controller/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-api-session-controller", "description": "Session Remote commands, cold reads, and live control transport", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/api/settings-controller/package.json b/packages/api/settings-controller/package.json index 2cfeb1e89a..91da932736 100644 --- a/packages/api/settings-controller/package.json +++ b/packages/api/settings-controller/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-api-settings-controller", "description": "Remote owner for the configuration surfaces over the settings-domain seams", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/api/workspace-controller/package.json b/packages/api/workspace-controller/package.json index fcfd014594..ffdc9e2080 100644 --- a/packages/api/workspace-controller/package.json +++ b/packages/api/workspace-controller/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-api-workspace-controller", "description": "Workspace Remote commands and reconnect-safe state transport", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/api/workspace-files/package.json b/packages/api/workspace-files/package.json index f867c86a1e..5ee4f54e67 100644 --- a/packages/api/workspace-files/package.json +++ b/packages/api/workspace-files/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-api-workspace-files", "description": "Workspace file service and Client resource provider: bounded reads, directory listing, and live metadata over the workspaceFiles Remote namespace", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/attachment/attachment-local/package.json b/packages/attachment/attachment-local/package.json index 71034eb684..afa52d914f 100644 --- a/packages/attachment/attachment-local/package.json +++ b/packages/attachment/attachment-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-attachment-local", "description": "Private content-addressed DSH_HOME attachment storage", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/attachment/attachment/package.json b/packages/attachment/attachment/package.json index 48cde83037..d98db07b61 100644 --- a/packages/attachment/attachment/package.json +++ b/packages/attachment/attachment/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-attachment", "description": "Durable immutable attachment storage seam for the DeepSeek Harness", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/boot/app-boot/package.json b/packages/boot/app-boot/package.json index 2e49da08de..64eedc7ae1 100644 --- a/packages/boot/app-boot/package.json +++ b/packages/boot/app-boot/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-app-boot", "description": "Shared boot glue for the app bins: .env loading, fail-loud Loader guards, snapshot-aware config resolution, and the Loader boot sequence", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/boot/cmdline/package.json b/packages/boot/cmdline/package.json index 6a4a9679d9..ea10cc6960 100644 --- a/packages/boot/cmdline/package.json +++ b/packages/boot/cmdline/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-cmdline", "description": "Immutable command-line handoff from a dsh launcher to any app plugin that injects cmdlineArgs", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/bundle/acp-app/package.json b/packages/bundle/acp-app/package.json index 3fb7dfaa3b..085b7341e5 100644 --- a/packages/bundle/acp-app/package.json +++ b/packages/bundle/acp-app/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-acp-app", "description": "The dsh ACP profile bundle: automation-only JSON-RPC stdio and process lifecycle over dsh-base", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/bundle/base/package.json b/packages/bundle/base/package.json index 4f3407903a..64e33f55b1 100644 --- a/packages/bundle/base/package.json +++ b/packages/bundle/base/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-base", "description": "The shared dsh core as a profile bundle: the first patch layer of base-backed profiles, inserting core rows over the empty profile root", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/bundle/headless/package.json b/packages/bundle/headless/package.json index b8f809ebf3..1b9e611457 100644 --- a/packages/bundle/headless/package.json +++ b/packages/bundle/headless/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-headless", "description": "The dsh one-shot bundle: a direct core Agent/Session runner over dsh-base with no Host, HTTP, or browser layer", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/bundle/sdk-app/package.json b/packages/bundle/sdk-app/package.json index 1f39ae000b..4fc726caa8 100644 --- a/packages/bundle/sdk-app/package.json +++ b/packages/bundle/sdk-app/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sdk-app", "description": "The dsh SDK profile bundle: stdio JSON-RPC serving and process lifecycle over dsh-base", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/bundle/sdk-minimal/package.json b/packages/bundle/sdk-minimal/package.json index d95375faed..630f448daf 100644 --- a/packages/bundle/sdk-minimal/package.json +++ b/packages/bundle/sdk-minimal/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sdk-minimal", "description": "The standalone minimal SDK profile bundle: JSON-RPC, one DeepSeek adapter, persistent shell, and JSONL sessions", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/bundle/web-app/package.json b/packages/bundle/web-app/package.json index 692a6be952..6782a6e444 100644 --- a/packages/bundle/web-app/package.json +++ b/packages/bundle/web-app/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-web-app", "description": "The dsh browser-surface bundle: the web patch layer over dsh-base plus the runtime glue plugin (frontend dist serving, web-surface prompt, bash runtime variables, URL line)", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/connection/package.json b/packages/client/connection/package.json index 9c999a025b..16dceff71f 100644 --- a/packages/client/connection/package.json +++ b/packages/client/connection/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-connection", "description": "Authenticated RPC transport, generation lifecycle, and browser fixture", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/file-upload/package.json b/packages/client/file-upload/package.json index 015a125351..fce7fd086d 100644 --- a/packages/client/file-upload/package.json +++ b/packages/client/file-upload/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-file-upload", "description": "Agent-scoped browser file upload, streaming intake, and staged receipt service", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/hmr/package.json b/packages/client/hmr/package.json index 6295fa479f..a3df146d0f 100644 --- a/packages/client/hmr/package.json +++ b/packages/client/hmr/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-hmr", "description": "Dev-only hot-reload driver for script-loaded client entries: SSE rebuilt frames → invalidate/prefetch → fiber swap through the vendored Loader entry", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/locale/package.json b/packages/client/locale/package.json index 10177f91e2..49928c9321 100644 --- a/packages/client/locale/package.json +++ b/packages/client/locale/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-locale", "description": "Locale plugin: Host-backed preference, extensible language catalog, browser fallback, and typed built-in dictionaries", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/modules/package.json b/packages/client/modules/package.json index 1c09baf8a6..fadb06d13e 100644 --- a/packages/client/modules/package.json +++ b/packages/client/modules/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-modules", "description": "Client module system, dual-face: node half composes the __DSH_BOOT__ entry graph (incremental dsh.client scan, bundle route, index tap, webPlugins service); browser half is the lazy-CJS module table the vendored cordis Loader consumes as its internal seam", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/resources/package.json b/packages/client/resources/package.json index cdcd4eed2d..21a47b7544 100644 --- a/packages/client/resources/package.json +++ b/packages/client/resources/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-resources", "description": "Unified client resource model: protocol-registered providers turn URL addresses into live values, consumed through the useResource global standard hook", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/store/package.json b/packages/client/store/package.json index 6bb0c0a212..fe71ce96ce 100644 --- a/packages/client/store/package.json +++ b/packages/client/store/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-store", "description": "React-free observable and snapshot-store contracts with the shared Zustand/Immer engine", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-agent-preset/package.json b/packages/client/ui-agent-preset/package.json index 9984d47613..857bc9c906 100644 --- a/packages/client/ui-agent-preset/package.json +++ b/packages/client/ui-agent-preset/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-agent-preset", "description": "Agent-preset surfaces: the default for later sessions, this session's seat, and the composition editor", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-approval/package.json b/packages/client/ui-approval/package.json index b4cecaa31d..00138c179e 100644 --- a/packages/client/ui-approval/package.json +++ b/packages/client/ui-approval/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-approval", "description": "Approval composer takeover over the scoped Remote Event waterfall", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-attachment/package.json b/packages/client/ui-attachment/package.json index 9adf41af13..429f1955b3 100644 --- a/packages/client/ui-attachment/package.json +++ b/packages/client/ui-attachment/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-attachment", "description": "Dynamic attachment presentation plugin for conversation input, message-image, and trajectory image slots", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-brand-official/package.json b/packages/client/ui-brand-official/package.json index 0db54ad4f0..62ddf9c91e 100644 --- a/packages/client/ui-brand-official/package.json +++ b/packages/client/ui-brand-official/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-brand-official", "description": "Official DeepSeek Harness brand occupants for the Web client's sidebar slots", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-chat/package.json b/packages/client/ui-chat/package.json index 7a5cb18771..ea9e994e91 100644 --- a/packages/client/ui-chat/package.json +++ b/packages/client/ui-chat/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-chat", "description": "Chat Conversation target, node definitions, renderers, and details surface", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-commands/package.json b/packages/client/ui-commands/package.json index 40ce5a3bff..4dc37a8261 100644 --- a/packages/client/ui-commands/package.json +++ b/packages/client/ui-commands/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-commands", "description": "Client command surface: global directory cache, '/' source, three command UI kinds, popupSelect registry", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-conversation/package.json b/packages/client/ui-conversation/package.json index 987aa9574f..c1e62e4100 100644 --- a/packages/client/ui-conversation/package.json +++ b/packages/client/ui-conversation/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-conversation", "description": "Target-neutral Conversation assembly, shell, composer, queue, and view navigation", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-deliverables/package.json b/packages/client/ui-deliverables/package.json index 16534d6f8d..dcac6ce807 100644 --- a/packages/client/ui-deliverables/package.json +++ b/packages/client/ui-deliverables/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-deliverables", "description": "Produced-files turn tail and clickable final-response file references for Web", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-directory-picker-browse/package.json b/packages/client/ui-directory-picker-browse/package.json index ec0a9d8ed0..f6544117ae 100644 --- a/packages/client/ui-directory-picker-browse/package.json +++ b/packages/client/ui-directory-picker-browse/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-directory-picker-browse", "description": "In-app directory browsing surface: the workspace directory-flow owner rendering the host's listing and creation primitives", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-directory-picker-native/package.json b/packages/client/ui-directory-picker-native/package.json index 9b370e32bb..1b81a714a8 100644 --- a/packages/client/ui-directory-picker-native/package.json +++ b/packages/client/ui-directory-picker-native/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-directory-picker-native", "description": "Native directory-picker surface: the renderless workspace directory-flow occupant driving the host's OS chooser", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-dockkit/package.json b/packages/client/ui-dockkit/package.json index d4ce40915b..df264e82f8 100644 --- a/packages/client/ui-dockkit/package.json +++ b/packages/client/ui-dockkit/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-dockkit", "description": "Docking layout kit: split-tree engine with invertible operations, and the React components that render and drive it (zero cordis)", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-goal/package.json b/packages/client/ui-goal/package.json index 5d516249f9..49f1105462 100644 --- a/packages/client/ui-goal/package.json +++ b/packages/client/ui-goal/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-goal", "description": "Session goal surface: GoalBar docked above the composer, read from the goal session projection", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-input-trigger/package.json b/packages/client/ui-input-trigger/package.json index 65e30abb16..d6f3bd7fb6 100644 --- a/packages/client/ui-input-trigger/package.json +++ b/packages/client/ui-input-trigger/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-input-trigger", "description": "Input trigger pipeline: '/' and '@' detection, candidate menu, pick routing to registered sources", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-jobs/package.json b/packages/client/ui-jobs/package.json index cd0ea7ebe9..196567bb7a 100644 --- a/packages/client/ui-jobs/package.json +++ b/packages/client/ui-jobs/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-jobs", "description": "Session-header background-job list: live registry state mirrored from session/jobs frames", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "type": "module", "main": "lib/index.js", "types": "lib/types/index.d.ts", diff --git a/packages/client/ui-layout/package.json b/packages/client/ui-layout/package.json index a2c025f39d..1beae6f3b0 100644 --- a/packages/client/ui-layout/package.json +++ b/packages/client/ui-layout/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-layout", "description": "Shell plugin: three-column AppFrame with drag handles, ctx.layout viewing-state service (navigation + panels)", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-message-feedback/package.json b/packages/client/ui-message-feedback/package.json index d5fd7aed7e..343f289faa 100644 --- a/packages/client/ui-message-feedback/package.json +++ b/packages/client/ui-message-feedback/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-message-feedback", "description": "The Web feedback surface: per-message Like/Dislike in the assistant-message action strip and the feedback dialog behind Dislike and /feedback, backed by the messageFeedback and sessionFeedback Host Remotes", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-model-selection/package.json b/packages/client/ui-model-selection/package.json index d3c89480e8..d0951c3d7c 100644 --- a/packages/client/ui-model-selection/package.json +++ b/packages/client/ui-model-selection/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-model-selection", "description": "Model selection over the shared model catalog, Session projection, and session.selectModel", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-open-in-app/package.json b/packages/client/ui-open-in-app/package.json index 3b39d1e5d8..97c063097d 100644 --- a/packages/client/ui-open-in-app/package.json +++ b/packages/client/ui-open-in-app/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-open-in-app", "description": "Web Session-header \"Open In...\" split button opening the session workspace directory in a locally installed application", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-permission-presets/package.json b/packages/client/ui-permission-presets/package.json index e065186bcd..708f3fba3f 100644 --- a/packages/client/ui-permission-presets/package.json +++ b/packages/client/ui-permission-presets/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-permission-presets", "description": "Permission surfaces: a new-session default in General settings and a current-session /permission popup over the permissions projection", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-plan/package.json b/packages/client/ui-plan/package.json index 7080b89644..6fa0ddcc2f 100644 --- a/packages/client/ui-plan/package.json +++ b/packages/client/ui-plan/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-plan", "description": "Plan-mode composer control: the conversation.input.plan seat over the plan projection and the /plan command channel", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-primitives/package.json b/packages/client/ui-primitives/package.json index 2613c3aa98..ca37e9b16f 100644 --- a/packages/client/ui-primitives/package.json +++ b/packages/client/ui-primitives/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-primitives", "description": "Pure React atoms for the dsh web UI: controls, icons, markdown, and JSON inspectors (zero cordis)", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-reference/package.json b/packages/client/ui-reference/package.json index 2e57deca88..7bfe165621 100644 --- a/packages/client/ui-reference/package.json +++ b/packages/client/ui-reference/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-reference", "description": "Unified Web @file and @session reference source", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-renderer/package.json b/packages/client/ui-renderer/package.json index 2aefdb4b1e..f1f6d4721d 100644 --- a/packages/client/ui-renderer/package.json +++ b/packages/client/ui-renderer/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-renderer", "description": "Browser UI renderer: React slot bindings, ctx.uiRenderer, and the assembled application root", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-schedule/package.json b/packages/client/ui-schedule/package.json index e47d9ad54a..a2a6536d3d 100644 --- a/packages/client/ui-schedule/package.json +++ b/packages/client/ui-schedule/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-schedule", "description": "Read-only active Schedule catalog in the Web Session header", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "type": "module", "main": "lib/index.js", "types": "lib/types/index.d.ts", diff --git a/packages/client/ui-session/package.json b/packages/client/ui-session/package.json index 9996019293..3e09735e19 100644 --- a/packages/client/ui-session/package.json +++ b/packages/client/ui-session/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-session", "description": "Session Controller adapter for React and session-scoped slots", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings-general/package.json b/packages/client/ui-settings-general/package.json index 2d33e995f4..8327d2ca1e 100644 --- a/packages/client/ui-settings-general/package.json +++ b/packages/client/ui-settings-general/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings-general", "description": "Settings ownerless-copy and product onboarding plugin: the General section, shell trigger/header chrome content, settings dictionaries, and the versioned welcome notice", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings-models/package.json b/packages/client/ui-settings-models/package.json index da707bee60..a3d882b2e8 100644 --- a/packages/client/ui-settings-models/package.json +++ b/packages/client/ui-settings-models/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings-models", "description": "Models settings and shared product-onboarding dialogs over existing settings and credential joins", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings-plugin-inventory/package.json b/packages/client/ui-settings-plugin-inventory/package.json index 84d0b26e40..0755e07977 100644 --- a/packages/client/ui-settings-plugin-inventory/package.json +++ b/packages/client/ui-settings-plugin-inventory/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings-plugin-inventory", "description": "Read-only Cordis Loader inventory tab in Web Plugins settings", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings-plugins/package.json b/packages/client/ui-settings-plugins/package.json index c4b27fae88..acc7a836dd 100644 --- a/packages/client/ui-settings-plugins/package.json +++ b/packages/client/ui-settings-plugins/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings-plugins", "description": "Plugins settings section with feature-owned tabs and configurable host-plane plugin cards", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings/package.json b/packages/client/ui-settings/package.json index 305ecef2fa..dffc545328 100644 --- a/packages/client/ui-settings/package.json +++ b/packages/client/ui-settings/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings", "description": "Settings domain base plugin: the settings-namespace scope service and the canonical settings slot-type contract", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-sidebar-documentpreview/package.json b/packages/client/ui-sidebar-documentpreview/package.json index 8d592decc0..af3d91613f 100644 --- a/packages/client/ui-sidebar-documentpreview/package.json +++ b/packages/client/ui-sidebar-documentpreview/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-sidebar-documentpreview", "description": "Extensible document previews for Sidebar files: Markdown, highlighted code, images, PDF, HTML, and plain text", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-sidebar-files/package.json b/packages/client/ui-sidebar-files/package.json index ce97ee1ffc..619884d036 100644 --- a/packages/client/ui-sidebar-files/package.json +++ b/packages/client/ui-sidebar-files/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-sidebar-files", "description": "Workspace file tree tab type for the right Sidebar: lazy directory listing over the workspaceFiles Remote namespace, opening files into the Sidebar", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-sidebar-right/package.json b/packages/client/ui-sidebar-right/package.json index c8fad0215c..31b8055429 100644 --- a/packages/client/ui-sidebar-right/package.json +++ b/packages/client/ui-sidebar-right/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-sidebar-right", "description": "Right Sidebar: the docking surface's session-bound state, its panel and header expand control, and the navigation service over it", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-sidebar/package.json b/packages/client/ui-sidebar/package.json index 3fb78453b2..6fda5a97db 100644 --- a/packages/client/ui-sidebar/package.json +++ b/packages/client/ui-sidebar/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-sidebar", "description": "Sidebar plugin: session multi-level tree, search, grouping, state dots", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-skill/package.json b/packages/client/ui-skill/package.json index cefa0086f2..9db4e24efd 100644 --- a/packages/client/ui-skill/package.json +++ b/packages/client/ui-skill/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-skill", "description": "Web skill references and the dedicated skill tool row", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-slots/package.json b/packages/client/ui-slots/package.json index fce80c249e..f01270278e 100644 --- a/packages/client/ui-slots/package.json +++ b/packages/client/ui-slots/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-slots", "description": "Slot registry pure core: SlotMap declaration merging, single register composition API, four-share props types, store-seat types, renderer install seam", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-subagent/package.json b/packages/client/ui-subagent/package.json index fe75323062..1835c7f7a2 100644 --- a/packages/client/ui-subagent/package.json +++ b/packages/client/ui-subagent/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-subagent", "description": "Subagent conversation catalog, continuation routing UI, and '@' reference source", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-theme/package.json b/packages/client/ui-theme/package.json index 5f9dd4940c..610bd399ab 100644 --- a/packages/client/ui-theme/package.json +++ b/packages/client/ui-theme/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-theme", "description": "Theme plugin: Host bootstrap for the pre-plugin palette; DOM-free ThemeRuntime for light/dark/system state; --dsw-* token styles and Appearance settings row", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-tool/package.json b/packages/client/ui-tool/package.json index 3a3722d64d..14030de47a 100644 --- a/packages/client/ui-tool/package.json +++ b/packages/client/ui-tool/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-tool", "description": "Client Tool call-tree renderer and keyed per-tool presentation slot", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-trajectory/package.json b/packages/client/ui-trajectory/package.json index 7f9752a6a8..417c8a47e3 100644 --- a/packages/client/ui-trajectory/package.json +++ b/packages/client/ui-trajectory/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-trajectory", "description": "Trajectory event ledger with an interactive timing overview: pure-consumer plugin registering into the conversation ViewMap (no service)", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-user-questions/package.json b/packages/client/ui-user-questions/package.json index b32352c522..16883d3686 100644 --- a/packages/client/ui-user-questions/package.json +++ b/packages/client/ui-user-questions/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-user-questions", "description": "Web ask_user_question composer takeover and plan-review presentation UI", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-workflow-run/package.json b/packages/client/ui-workflow-run/package.json index 15ca5a0d2d..a44c93a427 100644 --- a/packages/client/ui-workflow-run/package.json +++ b/packages/client/ui-workflow-run/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-workflow-run", "description": "Durable workflow-run Conversation Node and nested member disclosure for dsh web", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-workspace/package.json b/packages/client/ui-workspace/package.json index 3a35cae4d4..72b3b8ff33 100644 --- a/packages/client/ui-workspace/package.json +++ b/packages/client/ui-workspace/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-workspace", "description": "Workspace picker plugin: one WorkspacePicker registered into the sidebar and empty-state workspace slots", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/web/package.json b/packages/client/web/package.json index 64b36efc5c..6a9a8364c0 100644 --- a/packages/client/web/package.json +++ b/packages/client/web/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-web", "description": "Web boot kernel: static module table, Cordis loader, framework-free boot page, and UI-renderer handoff", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/code-runtime/code-runtime-worker-thread/package.json b/packages/code-runtime/code-runtime-worker-thread/package.json index e69ccc2f52..a31e670376 100644 --- a/packages/code-runtime/code-runtime-worker-thread/package.json +++ b/packages/code-runtime/code-runtime-worker-thread/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-code-runtime-worker-thread", "description": "Worker-thread implementation of the DeepSeek Harness code-execution seam", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/code-runtime/code-runtime/package.json b/packages/code-runtime/code-runtime/package.json index a49f2cf0a9..4321c52d38 100644 --- a/packages/code-runtime/code-runtime/package.json +++ b/packages/code-runtime/code-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-code-runtime", "description": "Abstract code-execution seam (ctx.codeRuntime) for the DeepSeek Harness", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/compaction/command-compact/package.json b/packages/compaction/command-compact/package.json index faffbe4943..a18140fa85 100644 --- a/packages/compaction/command-compact/package.json +++ b/packages/compaction/command-compact/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-command-compact", "description": "Human-facing slash command for explicit session compaction", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/compaction/compaction-basic/package.json b/packages/compaction/compaction-basic/package.json index 69de3038dd..8de83e4f12 100644 --- a/packages/compaction/compaction-basic/package.json +++ b/packages/compaction/compaction-basic/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-compaction-basic", "description": "Token-meter-driven compaction policy and LLM summarization backend for the DeepSeek Harness", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/compaction/compaction-tool-result-pruner/package.json b/packages/compaction/compaction-tool-result-pruner/package.json index 3cf8b5d169..57dd414347 100644 --- a/packages/compaction/compaction-tool-result-pruner/package.json +++ b/packages/compaction/compaction-tool-result-pruner/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-compaction-tool-result-pruner", "description": "Replay-safe model-free head/middle/tail pruning for tool-result surface nodes", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/compaction/compaction/package.json b/packages/compaction/compaction/package.json index 54ad37fdd1..110298ffbf 100644 --- a/packages/compaction/compaction/package.json +++ b/packages/compaction/compaction/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-compaction", "description": "Abstract compaction service seam (ctx.compaction) for the DeepSeek Harness", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/context/agent-instructions/package.json b/packages/context/agent-instructions/package.json index 6c94153e57..a9b57dc9c0 100644 --- a/packages/context/agent-instructions/package.json +++ b/packages/context/agent-instructions/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-agent-instructions", "description": "Workspace context loader for AGENTS.md/CLAUDE.md instruction files", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/context/file-reference-local/package.json b/packages/context/file-reference-local/package.json index 8f419495dd..cb46191bc6 100644 --- a/packages/context/file-reference-local/package.json +++ b/packages/context/file-reference-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-file-reference-local", "description": "Local-filesystem ctx.fileReferences provider with bounded fuzzy indexes", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/context/file-reference/package.json b/packages/context/file-reference/package.json index c33a4cfece..890676082d 100644 --- a/packages/context/file-reference/package.json +++ b/packages/context/file-reference/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-file-reference", "description": "File-reference discovery contract and shared @file grammar", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/context/session-reference/package.json b/packages/context/session-reference/package.json index 444f4f5d7e..c5899f1094 100644 --- a/packages/context/session-reference/package.json +++ b/packages/context/session-reference/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-reference", "description": "Cross-session snapshot references and durable untrusted model context (ctx.sessionReferenceResolver)", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/context/time-context/package.json b/packages/context/time-context/package.json index 1d6e5a374c..634bfc2058 100644 --- a/packages/context/time-context/package.json +++ b/packages/context/time-context/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-time-context", "description": "Opt-in durable per-step context with the current time and elapsed time", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/context/tmux-context/package.json b/packages/context/tmux-context/package.json index 113aa345c3..9a3696c994 100644 --- a/packages/context/tmux-context/package.json +++ b/packages/context/tmux-context/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tmux-context", "description": "Opt-in durable per-step context with this agent's tmux pane and window location", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/core/agent-default-model/package.json b/packages/core/agent-default-model/package.json index 409b297f35..02f25d90b0 100644 --- a/packages/core/agent-default-model/package.json +++ b/packages/core/agent-default-model/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-agent-default-model", "description": "Default model selection shared by Agent entry points", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/core/agent-loop/package.json b/packages/core/agent-loop/package.json index d73c8085ea..627c46ff44 100644 --- a/packages/core/agent-loop/package.json +++ b/packages/core/agent-loop/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-agent-loop", "description": "The concrete agent loop plugin for the DeepSeek Harness", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/core/agent-tool-presentation/package.json b/packages/core/agent-tool-presentation/package.json index 55f24b532e..b1069260d4 100644 --- a/packages/core/agent-tool-presentation/package.json +++ b/packages/core/agent-tool-presentation/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-agent-tool-presentation", "description": "Agent-plane presentation selector: composes one agent's tools as PTC mode, native, or both", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/core/agent/package.json b/packages/core/agent/package.json index 2df9571f97..e654ac0d2f 100644 --- a/packages/core/agent/package.json +++ b/packages/core/agent/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-agent", "description": "Agent interface, registry, initiator scope, and event vocabulary for the DeepSeek Harness", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/core/scope/package.json b/packages/core/scope/package.json index 2922794f45..bb0d51aa0e 100644 --- a/packages/core/scope/package.json +++ b/packages/core/scope/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-scope", "description": "Scoped-context registration primitive (scope tags, scope-filtered event dispatch) for the DeepSeek Harness", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/core/session/package.json b/packages/core/session/package.json index 65db598136..7b7858cb3d 100644 --- a/packages/core/session/package.json +++ b/packages/core/session/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session", "description": "Event-sourced session store for the DeepSeek Harness", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/core/system-prompt/package.json b/packages/core/system-prompt/package.json index 7ca95e44c7..cb431d3fbd 100644 --- a/packages/core/system-prompt/package.json +++ b/packages/core/system-prompt/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-system-prompt", "description": "System prompt assembly registry for the DeepSeek Harness", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/core/tools/package.json b/packages/core/tools/package.json index ebb966cb43..174d6f6902 100644 --- a/packages/core/tools/package.json +++ b/packages/core/tools/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tools", "description": "Tool registry and execution pipeline for the DeepSeek Harness", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/credentials/authorization/package.json b/packages/credentials/authorization/package.json index 728f091539..2bfb81078c 100644 --- a/packages/credentials/authorization/package.json +++ b/packages/credentials/authorization/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-authorization", "description": "Authorization seam (ctx.authorization): plugin-owned flows that obtain a credential through a conversation with the human", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/credentials/credentials-local/package.json b/packages/credentials/credentials-local/package.json index 4bd2a9daa0..5e0a6ce461 100644 --- a/packages/credentials/credentials-local/package.json +++ b/packages/credentials/credentials-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-credentials-local", "description": "File-backed credentials provider ($DSH_HOME/.env under the live process environment) for the DeepSeek Harness", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/credentials/credentials/package.json b/packages/credentials/credentials/package.json index 0ccc71df24..d41f37d3ec 100644 --- a/packages/credentials/credentials/package.json +++ b/packages/credentials/credentials/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-credentials", "description": "Abstract credential seam (ctx.credentials): settings carry references to secrets, providers own the values", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/e2b/e2b/package.json b/packages/e2b/e2b/package.json index efcaa3d50c..373e619db7 100644 --- a/packages/e2b/e2b/package.json +++ b/packages/e2b/e2b/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-e2b", "description": "Shared E2B sandbox lifecycle for DeepSeek Harness provider adapters", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/e2b/fs-e2b/package.json b/packages/e2b/fs-e2b/package.json index c646180f50..e63f687f60 100644 --- a/packages/e2b/fs-e2b/package.json +++ b/packages/e2b/fs-e2b/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-fs-e2b", "description": "E2B filesystem implementation for DeepSeek Harness", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/e2b/subprocess-e2b/package.json b/packages/e2b/subprocess-e2b/package.json index e75003e983..a5f999e7d5 100644 --- a/packages/e2b/subprocess-e2b/package.json +++ b/packages/e2b/subprocess-e2b/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subprocess-e2b", "description": "E2B subprocess implementation for DeepSeek Harness", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/agent-team-profile/package.json b/packages/experimental/agent-team-profile/package.json index 699ebc29ec..219b268ddd 100644 --- a/packages/experimental/agent-team-profile/package.json +++ b/packages/experimental/agent-team-profile/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-agent-team-profile", "description": "Experimental profile bundle enabling Agent Teams over dsh-base", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/agent-team-web-profile/package.json b/packages/experimental/agent-team-web-profile/package.json index 4ca780b18c..48ea6d71ba 100644 --- a/packages/experimental/agent-team-web-profile/package.json +++ b/packages/experimental/agent-team-web-profile/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-agent-team-web-profile", "description": "Experimental Web profile layer for Agent Teams Remote and UI plugins", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/agent-team/package.json b/packages/experimental/agent-team/package.json index dee4fc8756..5f04d02601 100644 --- a/packages/experimental/agent-team/package.json +++ b/packages/experimental/agent-team/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-agent-team", "description": "Implicit-root Agent Teams roster, durable peer mailbox, and shared task DAG", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/client-ui-agent-team/package.json b/packages/experimental/client-ui-agent-team/package.json index 12fa3b7275..3584f6493c 100644 --- a/packages/experimental/client-ui-agent-team/package.json +++ b/packages/experimental/client-ui-agent-team/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-client-ui-agent-team", "description": "Web Agent Teams roster, task board, and teammate navigation", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/code-runtime-python/package.json b/packages/experimental/code-runtime-python/package.json index b179c1a16b..7db8fe5dfe 100644 --- a/packages/experimental/code-runtime-python/package.json +++ b/packages/experimental/code-runtime-python/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-code-runtime-python", "description": "CPython subprocess implementation of the DeepSeek Harness code-execution seam", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "repository": { "type": "git", "url": "git+https://github.com/deepseek-ai/deepseek-harness.git", diff --git a/packages/experimental/inspector/package.json b/packages/experimental/inspector/package.json index e195c6f09a..a9c1e10725 100644 --- a/packages/experimental/inspector/package.json +++ b/packages/experimental/inspector/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-inspector", "description": "Experimental cross-realm CDP hub for Host debugging and Client Runtime inspection", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "private": true, "repository": { "type": "git", diff --git a/packages/experimental/tool-agent-team/package.json b/packages/experimental/tool-agent-team/package.json index eb41e65d5f..fb5779f90c 100644 --- a/packages/experimental/tool-agent-team/package.json +++ b/packages/experimental/tool-agent-team/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-tool-agent-team", "description": "Scoped model-facing Agent Teams tools over ctx.agentTeams", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/webworker-packer/package.json b/packages/experimental/webworker-packer/package.json index ba52e9e94e..264e8edea7 100644 --- a/packages/experimental/webworker-packer/package.json +++ b/packages/experimental/webworker-packer/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-webworker-packer", "description": "Build-time packer for the browser runtime's base VFS image and ordered data-overlay archives", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "private": true, "repository": { "type": "git", diff --git a/packages/experimental/webworker-runtime/package.json b/packages/experimental/webworker-runtime/package.json index a1a54e1e0f..a0d088ce71 100644 --- a/packages/experimental/webworker-runtime/package.json +++ b/packages/experimental/webworker-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-webworker-runtime", "description": "Browser-only harness runtime: in-memory VFS, module transform and loader, postMessage tunnel, and the dedicated Web Worker assembly, with the Node-compatibility layer that lets the host tree run unchanged", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "private": true, "repository": { "type": "git", diff --git a/packages/extensions/cordis-client-runner/package.json b/packages/extensions/cordis-client-runner/package.json index b6b0e3c9ac..c57e78e8ab 100644 --- a/packages/extensions/cordis-client-runner/package.json +++ b/packages/extensions/cordis-client-runner/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-cordis-client-runner", "description": "Browser half of dynamic dual-half plugin packages: event subscription, closure evaluation, guard facade, and loader entries", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/extensions/cordis-host-runner/package.json b/packages/extensions/cordis-host-runner/package.json index c673c2faaf..53e27100c2 100644 --- a/packages/extensions/cordis-host-runner/package.json +++ b/packages/extensions/cordis-host-runner/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-cordis-host-runner", "description": "Dynamic package definition registry, host-half sandbox lifecycle, and invoke handler table for model-mounted dual-half packages", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/extensions/tool-cordis/package.json b/packages/extensions/tool-cordis/package.json index 1499e1db10..da941bf469 100644 --- a/packages/extensions/tool-cordis/package.json +++ b/packages/extensions/tool-cordis/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-cordis", "description": "Self-referential cordis toolset: inspect the live runtime, mount and dispose model-written plugins", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/extensions/ui-cordis/package.json b/packages/extensions/ui-cordis/package.json index 20b473f2f6..07b34e3d7b 100644 --- a/packages/extensions/ui-cordis/package.json +++ b/packages/extensions/ui-cordis/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-cordis", "description": "Cordis dynamic-plugin definition card: the keyed cordis_define tool row with its run/stop switch", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/feedback/command-feedback/package.json b/packages/feedback/command-feedback/package.json index 39e700ddae..357e06639c 100644 --- a/packages/feedback/command-feedback/package.json +++ b/packages/feedback/command-feedback/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-command-feedback", "description": "Log-only session feedback: the record event, the sessionFeedback Host Remote, and the human-facing slash command", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/feedback/message-feedback/package.json b/packages/feedback/message-feedback/package.json index 456800ba92..7b0031ab1f 100644 --- a/packages/feedback/message-feedback/package.json +++ b/packages/feedback/message-feedback/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-message-feedback", "description": "Canonical Session-log ratings and notes for finalized assistant messages", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/fs/fs-local/package.json b/packages/fs/fs-local/package.json index ea0ba6695b..3b7492c05b 100644 --- a/packages/fs/fs-local/package.json +++ b/packages/fs/fs-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-fs-local", "description": "Local-filesystem implementation of the DeepSeek Harness filesystem seam (ctx.fs)", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/fs/fs-observation-policy/package.json b/packages/fs/fs-observation-policy/package.json index 4e59ada1ac..bb2f3d986f 100644 --- a/packages/fs/fs-observation-policy/package.json +++ b/packages/fs/fs-observation-policy/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-fs-observation-policy", "description": "File-context policy plugin for the DeepSeek Harness — observed-state, read-before-edit, and version-guarded write/edit added over the ctx.fs provider seam through the fs/* event gate (no service API)", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/fs/fs-sandbox/package.json b/packages/fs/fs-sandbox/package.json index 841e8ca041..f1eda99e86 100644 --- a/packages/fs/fs-sandbox/package.json +++ b/packages/fs/fs-sandbox/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-fs-sandbox", "description": "Sandbox-enforcing implementation of the DeepSeek Harness filesystem seam: fences write/edit by the per-call sandbox mode (read-only denies mutation, workspace-write contains it to the workspace + temp roots) while reads pass through", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/fs/fs/package.json b/packages/fs/fs/package.json index 6d4a55441d..4944f3f7db 100644 --- a/packages/fs/fs/package.json +++ b/packages/fs/fs/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-fs", "description": "Abstract filesystem capability seam (ctx.fs) for the DeepSeek Harness — vocabulary types, the FileSystem service (text IO + optional version-guarded atomic mutations), and the fs/* policy event vocabulary", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/fs/tool-fs-search/package.json b/packages/fs/tool-fs-search/package.json index 5d9842a174..db3715b502 100644 --- a/packages/fs/tool-fs-search/package.json +++ b/packages/fs/tool-fs-search/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-fs-search", "description": "Model-facing filesystem discovery tools (glob, grep) backed by the packaged ripgrep binary (@vscode/ripgrep)", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/fs/tool-fs/package.json b/packages/fs/tool-fs/package.json index e44c7a7f07..062a553050 100644 --- a/packages/fs/tool-fs/package.json +++ b/packages/fs/tool-fs/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-fs", "description": "Model-facing filesystem tools (read, write, edit) over the DeepSeek Harness filesystem seam (ctx.fs)", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/fs/tool-present/package.json b/packages/fs/tool-present/package.json index d1847ded9d..0baffa2345 100644 --- a/packages/fs/tool-present/package.json +++ b/packages/fs/tool-present/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-present", "description": "Explicit workspace file delivery declarations for the DeepSeek Harness", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/fs/tool-str-replace-editor/package.json b/packages/fs/tool-str-replace-editor/package.json index 9eb68b3a95..669f8da8a5 100644 --- a/packages/fs/tool-str-replace-editor/package.json +++ b/packages/fs/tool-str-replace-editor/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-str-replace-editor", "description": "Model-facing view, create, literal replace, and line insert tool over the Harness filesystem service", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/goal/command-goal/package.json b/packages/goal/command-goal/package.json index 238a65e732..c279129914 100644 --- a/packages/goal/command-goal/package.json +++ b/packages/goal/command-goal/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-command-goal", "description": "Human-facing slash command for persisted same-session goals", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/goal/goal-round-driver/package.json b/packages/goal/goal-round-driver/package.json index 7fe8fb72ff..43704663e2 100644 --- a/packages/goal/goal-round-driver/package.json +++ b/packages/goal/goal-round-driver/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-goal-round-driver", "description": "Race-fenced same-session goal-round driver", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/goal/goal/package.json b/packages/goal/goal/package.json index 4558efc896..013acc8c31 100644 --- a/packages/goal/goal/package.json +++ b/packages/goal/goal/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-goal", "description": "Event-sourced same-session goal state and lifecycle service for the DeepSeek Harness", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/goal/tool-goal/package.json b/packages/goal/tool-goal/package.json index 1204bbee09..c6836cf722 100644 --- a/packages/goal/tool-goal/package.json +++ b/packages/goal/tool-goal/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-goal", "description": "Model-facing same-session goal tools with execution-time authority checks", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/guard/repeat-tool-reminder/package.json b/packages/guard/repeat-tool-reminder/package.json index 9f4876b47c..76a61f7a6d 100644 --- a/packages/guard/repeat-tool-reminder/package.json +++ b/packages/guard/repeat-tool-reminder/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-repeat-tool-reminder", "description": "Repeat-tool-call guard plugin: advisory reminders when an agent loops on identical tool calls", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/guard/timeout-policy/package.json b/packages/guard/timeout-policy/package.json index 59f19a67ad..50c9064807 100644 --- a/packages/guard/timeout-policy/package.json +++ b/packages/guard/timeout-policy/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-call-timeout-policy", "description": "Tool-call timeout policy: a tools/execute wrapper that arms a per-tool deadline on exec.signal and returns TOOL_TIMEOUT when it wins", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/hooks/hook-protocol/package.json b/packages/hooks/hook-protocol/package.json index 90fa06db8d..953ff92fce 100644 --- a/packages/hooks/hook-protocol/package.json +++ b/packages/hooks/hook-protocol/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-hook-protocol", "description": "Shared Claude Code / Codex hook wire protocol: matcher engine, stdin/exit-code/stdout codec, multi-hook merge, and hook/* session events", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/hooks/hooks-claude-code/package.json b/packages/hooks/hooks-claude-code/package.json index 6b7cd0594b..8e77285198 100644 --- a/packages/hooks/hooks-claude-code/package.json +++ b/packages/hooks/hooks-claude-code/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-hooks-claude-code", "description": "Bridge plugin: run a Claude Code hooks.json / settings hook config on the DeepSeek Harness interception seams", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/hooks/hooks-codex/package.json b/packages/hooks/hooks-codex/package.json index 4da41ec1da..846f035e97 100644 --- a/packages/hooks/hooks-codex/package.json +++ b/packages/hooks/hooks-codex/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-hooks-codex", "description": "Bridge plugin: run a Codex hooks.json hook config on the DeepSeek Harness interception seams", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/host/directory-picker-auto/package.json b/packages/host/directory-picker-auto/package.json index 68d31835d3..32278ecaba 100644 --- a/packages/host/directory-picker-auto/package.json +++ b/packages/host/directory-picker-auto/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-host-directory-picker-auto", "description": "Adaptive chooser of the directory-picker seam: resolves the host situation at boot and mounts the native or browse backend for the DeepSeek Harness web GUI host", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/host/directory-picker-browse/package.json b/packages/host/directory-picker-browse/package.json index 66198388ae..8ea96e6341 100644 --- a/packages/host/directory-picker-browse/package.json +++ b/packages/host/directory-picker-browse/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-host-directory-picker-browse", "description": "In-app browsing backend of the directory-picker seam (listing/creation primitives over the host filesystem)", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/host/directory-picker-native/package.json b/packages/host/directory-picker-native/package.json index 9e1547073c..b6bb6d9110 100644 --- a/packages/host/directory-picker-native/package.json +++ b/packages/host/directory-picker-native/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-host-directory-picker-native", "description": "Native-OS-chooser backend of the directory-picker seam for the DeepSeek Harness web GUI host", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/host/directory-picker/package.json b/packages/host/directory-picker/package.json index 10d3b4e2a0..ef0e68372d 100644 --- a/packages/host/directory-picker/package.json +++ b/packages/host/directory-picker/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-host-directory-picker", "description": "Abstract workspace-directory picking seam (ctx.directoryPicker) for the DeepSeek Harness web GUI host", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/host/frontend-static/package.json b/packages/host/frontend-static/package.json index f1413302a0..e348e08d2a 100644 --- a/packages/host/frontend-static/package.json +++ b/packages/host/frontend-static/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-host-frontend-static", "description": "SPA dist server for the Web shell: owns the webserver fallback seat, serving explicit index entries and static assets with traversal rejection and 404 misses", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/host/open-in-app/package.json b/packages/host/open-in-app/package.json index ceb0c74b33..c88582a6fd 100644 --- a/packages/host/open-in-app/package.json +++ b/packages/host/open-in-app/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-host-open-in-app", "description": "Host half of open-in-app: resolved application catalog, icons, and the launch endpoint as three webServer routes", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/host/plugin-inventory/package.json b/packages/host/plugin-inventory/package.json index a3bdbe1e8f..65ed93c1d9 100644 --- a/packages/host/plugin-inventory/package.json +++ b/packages/host/plugin-inventory/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-host-plugin-inventory", "description": "Read-only Remote projection of current Cordis Loader plugin state", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/host/webserver/package.json b/packages/host/webserver/package.json index 01c1dd58aa..96ab1c4704 100644 --- a/packages/host/webserver/package.json +++ b/packages/host/webserver/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-host-webserver", "description": "Web route-registration plugin: HTTP and upgrade routes, index transform taps, and static dist fallback; knows no harness concepts", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/identity/anonymous-user-id/package.json b/packages/identity/anonymous-user-id/package.json index 1873a8dbb7..2d9d2d82b4 100644 --- a/packages/identity/anonymous-user-id/package.json +++ b/packages/identity/anonymous-user-id/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-anonymous-user-id", "description": "Shared anonymous user identity for DeepSeek Harness telemetry and feedback correlation", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/interaction/commands/package.json b/packages/interaction/commands/package.json index c29b99c7f0..1fe03def46 100644 --- a/packages/interaction/commands/package.json +++ b/packages/interaction/commands/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-commands", "description": "Plugin-owned human command registry for DeepSeek Harness UIs", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/interaction/permission-presets/package.json b/packages/interaction/permission-presets/package.json index fb5c7cd1aa..ef46950a5b 100644 --- a/packages/interaction/permission-presets/package.json +++ b/packages/interaction/permission-presets/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-permission-presets", "description": "User-facing permission presets (ctx.permissionPresets) for the DeepSeek Harness: one product-level Permissions select bundling the sandbox-mode and approval-policy knobs, written through to their own session events", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/interaction/tool-ask-user/package.json b/packages/interaction/tool-ask-user/package.json index bcc4d7c002..91b43a476d 100644 --- a/packages/interaction/tool-ask-user/package.json +++ b/packages/interaction/tool-ask-user/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-ask-user", "description": "Model-facing ask_user_question tool over the ctx.userQuestions seam", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/interaction/user-approval/package.json b/packages/interaction/user-approval/package.json index 786ac6f456..30ac058e29 100644 --- a/packages/interaction/user-approval/package.json +++ b/packages/interaction/user-approval/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-user-approval", "description": "User-approval seam (ctx.approval) for the DeepSeek Harness: one-shot permission decisions dispatched to composed answerers over the approval/request waterfall, fail-closed by default", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/interaction/user-questions/package.json b/packages/interaction/user-questions/package.json index c65c17c91d..c9936649b9 100644 --- a/packages/interaction/user-questions/package.json +++ b/packages/interaction/user-questions/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-user-questions", "description": "Abstract user-questions seam (ctx.userQuestions) for asking the human during agent runs", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/jobs/jobs-local/package.json b/packages/jobs/jobs-local/package.json index 208bb4f66e..5a262665d7 100644 --- a/packages/jobs/jobs-local/package.json +++ b/packages/jobs/jobs-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-jobs-local", "description": "Process-local implementation of the DeepSeek Harness background job registry seam", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/jobs/jobs/package.json b/packages/jobs/jobs/package.json index 94744c6a2a..b567121e03 100644 --- a/packages/jobs/jobs/package.json +++ b/packages/jobs/jobs/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-jobs", "description": "Background job registry (ctx.jobs) for the DeepSeek Harness — shared ids, owner isolation, polling, cancellation, and completion listeners for long-running tool work", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/jobs/tool-jobs/package.json b/packages/jobs/tool-jobs/package.json index d908add0a6..03615aea99 100644 --- a/packages/jobs/tool-jobs/package.json +++ b/packages/jobs/tool-jobs/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-jobs", "description": "Model-facing background job control tools (job_output, job_list, job_kill) over the ctx.jobs registry", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/llm/deepseek-llm-api-extensions/package.json b/packages/llm/deepseek-llm-api-extensions/package.json index 24804f96db..411cd64bba 100644 --- a/packages/llm/deepseek-llm-api-extensions/package.json +++ b/packages/llm/deepseek-llm-api-extensions/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-deepseek-llm-api-extensions", "description": "Additive request-field registry for the official DeepSeek LLM API adapter", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/llm/llm-deepseek/package.json b/packages/llm/llm-deepseek/package.json index b10ec1ff14..f9a8d5c09a 100644 --- a/packages/llm/llm-deepseek/package.json +++ b/packages/llm/llm-deepseek/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-llm-deepseek", "description": "DeepSeek chat-completions adapter for the DeepSeek Harness LLM seam", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/llm/llm-pi-ai/package.json b/packages/llm/llm-pi-ai/package.json index 28652bf860..2c7d5ca5c6 100644 --- a/packages/llm/llm-pi-ai/package.json +++ b/packages/llm/llm-pi-ai/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-llm-pi-ai", "description": "pi-ai-backed DeepSeek adapter for the DeepSeek Harness LLM seam (design-verification twin of dsh-llm-deepseek)", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/llm/llm-retry/package.json b/packages/llm/llm-retry/package.json index 5e3fef8315..b0033c09a7 100644 --- a/packages/llm/llm-retry/package.json +++ b/packages/llm/llm-retry/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-llm-retry", "description": "Provider-routed LLM request retry policy for the DeepSeek Harness", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/llm/llm/package.json b/packages/llm/llm/package.json index 73235b3521..9e4a322e0b 100644 --- a/packages/llm/llm/package.json +++ b/packages/llm/llm/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-llm", "description": "Provider-neutral LLM service interface for the DeepSeek Harness", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/llm/plugin-package-inventory-deepseek/package.json b/packages/llm/plugin-package-inventory-deepseek/package.json index 16cd26163a..46193fec94 100644 --- a/packages/llm/plugin-package-inventory-deepseek/package.json +++ b/packages/llm/plugin-package-inventory-deepseek/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-plugin-package-inventory-deepseek", "description": "Active Loader-backed plugin package inventory for official DeepSeek LLM API requests", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/llm/token-meter/package.json b/packages/llm/token-meter/package.json index 5a19084762..983f61a42f 100644 --- a/packages/llm/token-meter/package.json +++ b/packages/llm/token-meter/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-token-meter", "description": "Replay-aware token measurement service (ctx.tokenMeter) for the DeepSeek Harness", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/lsp/lsp-stdio/package.json b/packages/lsp/lsp-stdio/package.json index 99f8f79231..5ede6bd195 100644 --- a/packages/lsp/lsp-stdio/package.json +++ b/packages/lsp/lsp-stdio/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-lsp-stdio", "description": "Generic stdio language-server provider for the DeepSeek Harness LSP capability seam (ctx.lsp) — spawns configured servers, translates JSON-RPC, and serves transient-open goToDefinition/findReferences/goToImplementation/hover queries in the host filesystem namespace", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/lsp/lsp/package.json b/packages/lsp/lsp/package.json index 175508a801..71a7be6754 100644 --- a/packages/lsp/lsp/package.json +++ b/packages/lsp/lsp/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-lsp", "description": "Abstract LSP capability seam (ctx.lsp) for the DeepSeek Harness — language-server provider registry keyed by branded id and extension mapping, order-independent per-query selection, normalized definition/references/implementation/hover requests and results, and the LspError taxonomy", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/lsp/tool-lsp/package.json b/packages/lsp/tool-lsp/package.json index aef7b01bc8..2cb4805bc0 100644 --- a/packages/lsp/tool-lsp/package.json +++ b/packages/lsp/tool-lsp/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-lsp", "description": "Model-facing lsp tool over the DeepSeek Harness LSP capability seam (ctx.lsp) — one read-only tool with goToDefinition/findReferences/goToImplementation/hover operations, one-based UTF-16 cursor coordinates, bounded location rendering, and hover normalization", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/mcp/mcp-client/package.json b/packages/mcp/mcp-client/package.json index 3cc384ca25..b4d921a813 100644 --- a/packages/mcp/mcp-client/package.json +++ b/packages/mcp/mcp-client/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-mcp-client", "description": "MCP client bridge: connects to MCP servers and registers their tools on ctx.tools", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/plan/plan-mode/package.json b/packages/plan/plan-mode/package.json index cab618dccd..759f290dda 100644 --- a/packages/plan/plan-mode/package.json +++ b/packages/plan/plan-mode/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-plan-mode", "description": "Logged per-agent plan mode with deployment guidance, a direct slash command, and a user-reviewed exit", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/preset/agent-presets/package.json b/packages/preset/agent-presets/package.json index a632b21ee0..e6a026225a 100644 --- a/packages/preset/agent-presets/package.json +++ b/packages/preset/agent-presets/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-agent-presets", "description": "Per-session agent composition from preset cordis.yml files for the DeepSeek Harness", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/preset/persona/package.json b/packages/preset/persona/package.json index ec81978b55..ac69830a75 100644 --- a/packages/preset/persona/package.json +++ b/packages/preset/persona/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-persona", "description": "Composition-authored deployment persona section for the DeepSeek Harness", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/runtime-diagnostics/invariants/package.json b/packages/runtime-diagnostics/invariants/package.json index 4666c078d5..6dcf6f5517 100644 --- a/packages/runtime-diagnostics/invariants/package.json +++ b/packages/runtime-diagnostics/invariants/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-invariants", "description": "Registry service for package-owned DeepSeek Harness runtime invariants", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/sandbox/sandbox-local/package.json b/packages/sandbox/sandbox-local/package.json index c9a5618507..02200c0e13 100644 --- a/packages/sandbox/sandbox-local/package.json +++ b/packages/sandbox/sandbox-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sandbox-local", "description": "Local process-sandbox backends for the DeepSeek Harness sandbox seam: bwrap, the npm-distributed landlock-run launcher, macOS Seatbelt, or the Windows ACL restricted-token runner — functionally probed, fail-closed", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/sandbox/sandbox-policy/package.json b/packages/sandbox/sandbox-policy/package.json index 7a21457c90..3f59b340c6 100644 --- a/packages/sandbox/sandbox-policy/package.json +++ b/packages/sandbox/sandbox-policy/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sandbox-policy", "description": "Per-call sandbox policy resolver and current model context: deployment fallbacks plus each session's mode and workspace root, shared by every enforcing capability family", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/sandbox/sandbox-windows-acl/package.json b/packages/sandbox/sandbox-windows-acl/package.json index 27ca514db8..5de9c52edf 100644 --- a/packages/sandbox/sandbox-windows-acl/package.json +++ b/packages/sandbox/sandbox-windows-acl/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sandbox-windows-acl", "description": "Windows ACL write-restriction sandbox backend (restricted-token spawn with capability-SID write allowlist) for the DeepSeek Harness sandbox seam", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/sandbox/sandbox/package.json b/packages/sandbox/sandbox/package.json index 02c4e0eee8..f817a85049 100644 --- a/packages/sandbox/sandbox/package.json +++ b/packages/sandbox/sandbox/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sandbox", "description": "Abstract process-sandbox seam (ctx.sandbox) for the DeepSeek Harness: same-world confinement vocabulary and the SandboxProvider contract", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/schedule/schedule/package.json b/packages/schedule/schedule/package.json index 78fac782ec..37fa4589c9 100644 --- a/packages/schedule/schedule/package.json +++ b/packages/schedule/schedule/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-schedule", "description": "Agent-scoped durable after, at, and fixed-rate reminders over the session event log", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/sdk/client/package.json b/packages/sdk/client/package.json index 5534f7028e..f3f9321ca3 100644 --- a/packages/sdk/client/package.json +++ b/packages/sdk/client/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sdk-client", "description": "TypeScript client SDK for driving a DeepSeek Harness runtime subprocess over stdio JSON-RPC: the DeepSeekHarness high-level turns API and the lower-level HarnessClient", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/sdk/protocol/package.json b/packages/sdk/protocol/package.json index 145a79d7a7..753fa4ac1e 100644 --- a/packages/sdk/protocol/package.json +++ b/packages/sdk/protocol/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sdk-protocol", "description": "Shared wire protocol for the DeepSeek Harness SDK runtime: the newline-delimited JSON-RPC stdio transport and the named request, result, and notification types spoken between the runtime server and SDK clients", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/sdk/server/package.json b/packages/sdk/server/package.json index 58920ce23b..aaf98612ea 100644 --- a/packages/sdk/server/package.json +++ b/packages/sdk/server/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sdk-jsonrpc-server", "description": "Stdio JSON-RPC server plugin for out-of-process DeepSeek Harness SDK clients", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session-query/session-log-export/package.json b/packages/session-query/session-log-export/package.json index e107281710..91966f3773 100644 --- a/packages/session-query/session-log-export/package.json +++ b/packages/session-query/session-log-export/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-log-export", "description": "Web Session-log export command and shared download dialog", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session-query/session-query-sqlite/package.json b/packages/session-query/session-query-sqlite/package.json index 560e1056cf..01d3178e31 100644 --- a/packages/session-query/session-query-sqlite/package.json +++ b/packages/session-query/session-query-sqlite/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-query-sqlite", "description": "Concrete ctx.sessionQuery backend with SQLite FTS5 search", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session-query/session-query/package.json b/packages/session-query/session-query/package.json index 80e928341b..6141578ea4 100644 --- a/packages/session-query/session-query/package.json +++ b/packages/session-query/session-query/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-query", "description": "Combined session query service contract with concrete reads, traces, and filters", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session-query/tool-session-query/package.json b/packages/session-query/tool-session-query/package.json index b54d4eae26..e3244c2e76 100644 --- a/packages/session-query/tool-session-query/package.json +++ b/packages/session-query/tool-session-query/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-session-query", "description": "Workspace-authorized model-facing session history search, trace, and event read tools", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-checkpoint-policy/package.json b/packages/session/session-checkpoint-policy/package.json index f59985e5b3..796e177b29 100644 --- a/packages/session/session-checkpoint-policy/package.json +++ b/packages/session/session-checkpoint-policy/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-checkpoint-policy", "description": "Semantic session durability checkpoints before model requests and tool side effects", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-format-catalog/package.json b/packages/session/session-format-catalog/package.json index b2f197c13c..56406e219b 100644 --- a/packages/session/session-format-catalog/package.json +++ b/packages/session/session-format-catalog/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-format-catalog", "description": "Build-static first-party Session format codec and migration catalog", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-format-v0-to-v1/package.json b/packages/session/session-format-v0-to-v1/package.json index 58e4b3f677..42f7f1c4a9 100644 --- a/packages/session/session-format-v0-to-v1/package.json +++ b/packages/session/session-format-v0-to-v1/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-format-v0-to-v1", "description": "Frozen released-v0 Session codec and identity migration to v1", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-format-v1-to-v2/package.json b/packages/session/session-format-v1-to-v2/package.json index b44f957bfd..11955848ea 100644 --- a/packages/session/session-format-v1-to-v2/package.json +++ b/packages/session/session-format-v1-to-v2/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-format-v1-to-v2", "description": "Frozen released-v1 Session codec and assistant-stream migration to v2", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-format-v2-to-v3/package.json b/packages/session/session-format-v2-to-v3/package.json index 99b1ca46d8..4c0c17815e 100644 --- a/packages/session/session-format-v2-to-v3/package.json +++ b/packages/session/session-format-v2-to-v3/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-format-v2-to-v3", "description": "Streaming system-prompt, canonical-envelope and PTC migration into V3", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-format/package.json b/packages/session/session-format/package.json index 069a8c2e95..cc55b09ce3 100644 --- a/packages/session/session-format/package.json +++ b/packages/session/session-format/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-format", "description": "Streaming adjacent Session format migration machinery", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-log-deepseek/package.json b/packages/session/session-log-deepseek/package.json index 9dfdf7e6dd..3c7f0e65fc 100644 --- a/packages/session/session-log-deepseek/package.json +++ b/packages/session/session-log-deepseek/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-log-deepseek", "description": "Incremental lossless session-log request extension for the official DeepSeek LLM API", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-persistence-jsonl/package.json b/packages/session/session-persistence-jsonl/package.json index 0233f1c4e5..11f50ba97a 100644 --- a/packages/session/session-persistence-jsonl/package.json +++ b/packages/session/session-persistence-jsonl/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-persistence-jsonl", "description": "JSONL durable session persistence backend for the DeepSeek Harness", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-persistence/package.json b/packages/session/session-persistence/package.json index 00c06bd87b..36f7009ebd 100644 --- a/packages/session/session-persistence/package.json +++ b/packages/session/session-persistence/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-persistence", "description": "Abstract durable session persistence seam (ctx.sessionPersistence) for the DeepSeek Harness", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-projection-cache/package.json b/packages/session/session-projection-cache/package.json index 4572668791..7d9cd2587e 100644 --- a/packages/session/session-projection-cache/package.json +++ b/packages/session/session-projection-cache/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-projection-cache", "description": "Persisted projection cache (ctx.sessionProjectionCache): durable per-session checkpoint records on the session_projcache storage domain (per-record layout), throttled write-behind, and the cached listing read", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-projection/package.json b/packages/session/session-projection/package.json index 0820666165..e36e2dc4a8 100644 --- a/packages/session/session-projection/package.json +++ b/packages/session/session-projection/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-projection", "description": "Session-projection seam: the merge-extensible projection type table, the provider contract, and the ctx.sessionProjections registry serving whole current values of log-derived per-session state", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-stats/package.json b/packages/session/session-stats/package.json index 507f87a96b..cfaf57382b 100644 --- a/packages/session/session-stats/package.json +++ b/packages/session/session-stats/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-stats", "description": "Whole-log conversation counts and wall times projection (sessionStats) for the DeepSeek Harness", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-telemetry-otel/package.json b/packages/session/session-telemetry-otel/package.json index 707e536ad4..51bb035943 100644 --- a/packages/session/session-telemetry-otel/package.json +++ b/packages/session/session-telemetry-otel/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-telemetry-otel", "description": "OpenTelemetry backend for the DeepSeek Harness telemetry seam: hands captured session records to the OTel JS SDK's log pipeline", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-telemetry/package.json b/packages/session/session-telemetry/package.json index a7e1b7c31a..7927c46409 100644 --- a/packages/session/session-telemetry/package.json +++ b/packages/session/session-telemetry/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-telemetry", "description": "SessionTelemetryBackend seam for the DeepSeek Harness: session-event capture, projection, redaction, and handoff to a reporting backend", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-title-all-prompts-llm/package.json b/packages/session/session-title-all-prompts-llm/package.json index 730c1d46de..c51f74eed9 100644 --- a/packages/session/session-title-all-prompts-llm/package.json +++ b/packages/session/session-title-all-prompts-llm/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-title-all-prompts-llm", "description": "All-user-messages LLM provider plugin for DeepSeek Harness session titles", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-title-first-prompt-llm/package.json b/packages/session/session-title-first-prompt-llm/package.json index 14343665f7..3a3717fcc7 100644 --- a/packages/session/session-title-first-prompt-llm/package.json +++ b/packages/session/session-title-first-prompt-llm/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-title-first-prompt-llm", "description": "First-message LLM provider plugin for DeepSeek Harness session titles", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-title-llm/package.json b/packages/session/session-title-llm/package.json index 2a13d395ca..89cd217004 100644 --- a/packages/session/session-title-llm/package.json +++ b/packages/session/session-title-llm/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-title-llm", "description": "Shared LLM generation policy for DeepSeek Harness session-title providers", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-title/package.json b/packages/session/session-title/package.json index 4f244531e9..79170df18d 100644 --- a/packages/session/session-title/package.json +++ b/packages/session/session-title/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-title", "description": "Log-backed session title service and provider registry for the DeepSeek Harness", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-turn-outline/package.json b/packages/session/session-turn-outline/package.json index 711e009430..892bd66c9b 100644 --- a/packages/session/session-turn-outline/package.json +++ b/packages/session/session-turn-outline/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-turn-outline", "description": "Whole-log turn outline projection (turnOutline) for the DeepSeek Harness", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/settings/settings-file/package.json b/packages/settings/settings-file/package.json index f0eb2ddb46..5a91ea8a02 100644 --- a/packages/settings/settings-file/package.json +++ b/packages/settings/settings-file/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-settings-file", "description": "File-backed settings provider (settings.yaml) for the DeepSeek Harness", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/settings/settings/package.json b/packages/settings/settings/package.json index c7a2fbec40..55343abb6c 100644 --- a/packages/settings/settings/package.json +++ b/packages/settings/settings/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-settings", "description": "Abstract user-settings seam (ctx.settings) for the DeepSeek Harness", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/shell/bash-local/package.json b/packages/shell/bash-local/package.json index 9359c2de1b..0f1b558a61 100644 --- a/packages/shell/bash-local/package.json +++ b/packages/shell/bash-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-bash-local", "description": "Local-subprocess implementation of the DeepSeek Harness bash executor seam", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/shell/bash-sandbox/package.json b/packages/shell/bash-sandbox/package.json index 7ea041a1a5..d7405f3cc4 100644 --- a/packages/shell/bash-sandbox/package.json +++ b/packages/shell/bash-sandbox/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-bash-sandbox", "description": "Sandbox-consuming implementation of the DeepSeek Harness bash executor seam (confines every command via ctx.sandbox, reports denial/enforcement result facts)", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/shell/pwsh-local/package.json b/packages/shell/pwsh-local/package.json index ebea606ae7..7b102e4c30 100644 --- a/packages/shell/pwsh-local/package.json +++ b/packages/shell/pwsh-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-pwsh-local", "description": "Local PowerShell implementation of the DeepSeek Harness bash executor seam", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/shell/pwsh-sandbox/package.json b/packages/shell/pwsh-sandbox/package.json index bc911347ea..6d5185ef9b 100644 --- a/packages/shell/pwsh-sandbox/package.json +++ b/packages/shell/pwsh-sandbox/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-pwsh-sandbox", "description": "Sandbox-consuming implementation of the DeepSeek Harness PowerShell executor seam (confines every command via ctx.sandbox, reports denial/enforcement result facts)", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/shell/shell-env/package.json b/packages/shell/shell-env/package.json index 3558307a0c..e0b4fb2368 100644 --- a/packages/shell/shell-env/package.json +++ b/packages/shell/shell-env/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-shell-env", "description": "Tool-independent managed DSH_* shell environment registry", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/shell/shell/package.json b/packages/shell/shell/package.json index 6fce0fb257..88503f4385 100644 --- a/packages/shell/shell/package.json +++ b/packages/shell/shell/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-shell", "description": "Abstract bash executor seam (ctx.shell) for the DeepSeek Harness", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/shell/tool-bash-persistent/package.json b/packages/shell/tool-bash-persistent/package.json index 7b817703b5..a8c99b3c5c 100644 --- a/packages/shell/tool-bash-persistent/package.json +++ b/packages/shell/tool-bash-persistent/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-bash-persistent", "description": "Model-facing owner-scoped persistent Bash tool backed by the Harness PTY service", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/shell/tool-bash/package.json b/packages/shell/tool-bash/package.json index 3ed1a01a9f..c8648afd57 100644 --- a/packages/shell/tool-bash/package.json +++ b/packages/shell/tool-bash/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-bash", "description": "Model-facing bash tool with optional generic background-job and sandbox-escalation support", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/shell/tool-pwsh-persistent/package.json b/packages/shell/tool-pwsh-persistent/package.json index 780948e7a0..c42a4bde52 100644 --- a/packages/shell/tool-pwsh-persistent/package.json +++ b/packages/shell/tool-pwsh-persistent/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-pwsh-persistent", "description": "Model-facing owner-scoped persistent PowerShell tool backed by the Harness PTY service", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/shell/tool-pwsh/package.json b/packages/shell/tool-pwsh/package.json index 7c003409c2..b01b2e3cd9 100644 --- a/packages/shell/tool-pwsh/package.json +++ b/packages/shell/tool-pwsh/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-pwsh", "description": "Model-facing pwsh tool over the bash executor seam", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/skill/skill-badge/package.json b/packages/skill/skill-badge/package.json index c5db5bbec3..1fd637dabb 100644 --- a/packages/skill/skill-badge/package.json +++ b/packages/skill/skill-badge/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-skill-badge", "description": "Bundled dsh badge skill provider for DeepSeek Harness", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/skill/skill-filesystem/package.json b/packages/skill/skill-filesystem/package.json index 09dc51f2dd..484c7ea0e1 100644 --- a/packages/skill/skill-filesystem/package.json +++ b/packages/skill/skill-filesystem/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-skill-filesystem", "description": "Local filesystem skill provider for the DeepSeek Harness", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/skill/skill/package.json b/packages/skill/skill/package.json index 8705f3481f..56154b531d 100644 --- a/packages/skill/skill/package.json +++ b/packages/skill/skill/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-skill", "description": "Agent skill provider registry for the DeepSeek Harness", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/skill/tool-skill/package.json b/packages/skill/tool-skill/package.json index 633473717e..120876880c 100644 --- a/packages/skill/tool-skill/package.json +++ b/packages/skill/tool-skill/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-skill", "description": "Model-facing skill loading tool for the DeepSeek Harness", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/spill/spill-local/package.json b/packages/spill/spill-local/package.json index 7cc5eda514..089535ff13 100644 --- a/packages/spill/spill-local/package.json +++ b/packages/spill/spill-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-spill-local", "description": "Local-filesystem implementation of the DeepSeek Harness spill storage seam (private session-scoped files)", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/spill/spill-policy/package.json b/packages/spill/spill-policy/package.json index 9990cdf288..d3a0ed5eb6 100644 --- a/packages/spill/spill-policy/package.json +++ b/packages/spill/spill-policy/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-spill-policy", "description": "Tool-result spill policy for the DeepSeek Harness — replaces oversized plain-text tool results with a retained preview plus a spill-file path (no service API)", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/spill/spill/package.json b/packages/spill/spill/package.json index eeedb76373..651531bf82 100644 --- a/packages/spill/spill/package.json +++ b/packages/spill/spill/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-spill", "description": "Abstract spill storage seam (ctx.spillStore) for the DeepSeek Harness — save oversized tool text and return a retrieval locator", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/storage/storage-domain/package.json b/packages/storage/storage-domain/package.json index ab2bc8c205..dbbd79571c 100644 --- a/packages/storage/storage-domain/package.json +++ b/packages/storage/storage-domain/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-storage-domain", "description": "Domain data form (ctx.storage.domain): schema-validated, event-emitting KV domains over storage backends for the DeepSeek Harness", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/storage/storage-json/package.json b/packages/storage/storage-json/package.json index f35b45d72f..4e79b26fc0 100644 --- a/packages/storage/storage-json/package.json +++ b/packages/storage/storage-json/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-storage-json", "description": "JSON file KV storage backend for the DeepSeek Harness storage hub", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/storage/storage-sqlite/package.json b/packages/storage/storage-sqlite/package.json index 00797f7c07..638ad2e283 100644 --- a/packages/storage/storage-sqlite/package.json +++ b/packages/storage/storage-sqlite/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-storage-sqlite", "description": "SQLite storage backend (kv facet) for the DeepSeek Harness storage hub", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/storage/storage/package.json b/packages/storage/storage/package.json index 471ac26288..c45cc74a11 100644 --- a/packages/storage/storage/package.json +++ b/packages/storage/storage/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-storage", "description": "Storage hub (ctx.storage): named backend registry plus mounted data-form facilities for the DeepSeek Harness", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/subagent/subagent-acp/package.json b/packages/subagent/subagent-acp/package.json index 4c6faca8b0..6e42076bc3 100644 --- a/packages/subagent/subagent-acp/package.json +++ b/packages/subagent/subagent-acp/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subagent-acp", "description": "Out-of-process ACP subagent backend: drives a child agent in a spawned subprocess over the Agent Client Protocol", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/subagent/subagent-claude-code/package.json b/packages/subagent/subagent-claude-code/package.json index f4f1daea88..b4e328eff0 100644 --- a/packages/subagent/subagent-claude-code/package.json +++ b/packages/subagent/subagent-claude-code/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subagent-claude-code", "description": "One-shot Claude Code subagent provider over the official Agent SDK", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/subagent/subagent-codex/package.json b/packages/subagent/subagent-codex/package.json index e25cfd69f8..79bda26eec 100644 --- a/packages/subagent/subagent-codex/package.json +++ b/packages/subagent/subagent-codex/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subagent-codex", "description": "One-shot Codex subagent provider over the official app-server protocol", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/subagent/subagent-dsh-sdk/package.json b/packages/subagent/subagent-dsh-sdk/package.json index 43fbea39e5..505ee00c38 100644 --- a/packages/subagent/subagent-dsh-sdk/package.json +++ b/packages/subagent/subagent-dsh-sdk/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subagent-dsh-sdk", "description": "Out-of-process SDK subagent backend: drives a child DeepSeek Harness runtime subprocess over stdio JSON-RPC through the TypeScript SDK client", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/subagent/subagent-fork-in-process/package.json b/packages/subagent/subagent-fork-in-process/package.json index 6c4c040b9b..2deb14834d 100644 --- a/packages/subagent/subagent-fork-in-process/package.json +++ b/packages/subagent/subagent-fork-in-process/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subagent-fork-in-process", "description": "In-process fork subagent backend: runs a child agent seeded with a prefix of the parent's log", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/subagent/subagent-in-process-driver/package.json b/packages/subagent/subagent-in-process-driver/package.json index 51c6b2240b..ee7d0febc4 100644 --- a/packages/subagent/subagent-in-process-driver/package.json +++ b/packages/subagent/subagent-in-process-driver/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subagent-in-process-driver", "description": "Shared in-process subagent run driver: drives a child agent on ctx.agents (used by the spawn and fork backends)", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/subagent/subagent-spawn-in-process/package.json b/packages/subagent/subagent-spawn-in-process/package.json index 7f0b7f2dba..73fd12c1fc 100644 --- a/packages/subagent/subagent-spawn-in-process/package.json +++ b/packages/subagent/subagent-spawn-in-process/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subagent-spawn-in-process", "description": "In-process spawn subagent backend: runs a fresh child agent on ctx.agents", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/subagent/subagent/package.json b/packages/subagent/subagent/package.json index 22e000973f..319d9ecbe5 100644 --- a/packages/subagent/subagent/package.json +++ b/packages/subagent/subagent/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subagent", "description": "Abstract subagent seam (ctx.subagents): named-provider registry for delegating to child agents", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/subagent/tool-subagent-control/package.json b/packages/subagent/tool-subagent-control/package.json index 71d256c372..667f86351b 100644 --- a/packages/subagent/tool-subagent-control/package.json +++ b/packages/subagent/tool-subagent-control/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-subagent-control", "description": "Globally named send_message, interrupt_agent, and list_agents tools over ctx.subagents continuations", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/subagent/tool-subagent/package.json b/packages/subagent/tool-subagent/package.json index 119d39ad1b..284c093f12 100644 --- a/packages/subagent/tool-subagent/package.json +++ b/packages/subagent/tool-subagent/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-subagent", "description": "Model-facing subagent delegation tool over the ctx.subagents seam", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/subprocess/subprocess-local/package.json b/packages/subprocess/subprocess-local/package.json index 5b19229c75..7465d787f9 100644 --- a/packages/subprocess/subprocess-local/package.json +++ b/packages/subprocess/subprocess-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subprocess-local", "description": "Local-subprocess implementation of the DeepSeek Harness subprocess seam", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/subprocess/subprocess/package.json b/packages/subprocess/subprocess/package.json index 627077a52a..2f20d3a6f9 100644 --- a/packages/subprocess/subprocess/package.json +++ b/packages/subprocess/subprocess/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subprocess", "description": "Subprocess seam (ctx.subprocess) for the DeepSeek Harness — managed process groups, bounded spill-backed output, and escalated kills behind one abstract service", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/subprocess/win32-process/package.json b/packages/subprocess/win32-process/package.json index a7b3a2fbb1..28fc6c2685 100644 --- a/packages/subprocess/win32-process/package.json +++ b/packages/subprocess/win32-process/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-win32-process", "description": "Shared low-level Win32 process, stdio, and Job Object primitives", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/terminal/terminal-bash/package.json b/packages/terminal/terminal-bash/package.json index 92f346a595..c433ad9375 100644 --- a/packages/terminal/terminal-bash/package.json +++ b/packages/terminal/terminal-bash/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-terminal-bash", "description": "Persistent shell PTY backend over the DeepSeek Harness subprocess terminal primitive", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/terminal/terminal/package.json b/packages/terminal/terminal/package.json index 4a65ed9e29..ec2fc7c719 100644 --- a/packages/terminal/terminal/package.json +++ b/packages/terminal/terminal/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-terminal", "description": "Persistent PTY session seam for the DeepSeek Harness — owner-scoped ids, backend registry, interactive sends, reads, signals, and awaited cleanup", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/terminal/tool-terminal/package.json b/packages/terminal/tool-terminal/package.json index c0e1707fc6..bfcbeddef1 100644 --- a/packages/terminal/tool-terminal/package.json +++ b/packages/terminal/tool-terminal/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-terminal", "description": "Six model-facing persistent PTY tools with owner isolation and generic background-job integration", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/test-support/agent-loop-testkit/package.json b/packages/test-support/agent-loop-testkit/package.json index 7bc3890fec..1e2eb18ee4 100644 --- a/packages/test-support/agent-loop-testkit/package.json +++ b/packages/test-support/agent-loop-testkit/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-agent-loop-testkit", "description": "Prerequisite mounting, production AgentLoop drivers, and Inbox stubs for tests", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/test-support/client-runtime/package.json b/packages/test-support/client-runtime/package.json index 88298cd72b..326a78083a 100644 --- a/packages/test-support/client-runtime/package.json +++ b/packages/test-support/client-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-test-runtime", "description": "jsdom slot test runtime: real Cordis Context + SlotRegistry + UI renderer with test-owned session/workspace doubles for feature specs", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/test-support/llm-mock-server/package.json b/packages/test-support/llm-mock-server/package.json index 09076a4f03..70538bbbad 100644 --- a/packages/test-support/llm-mock-server/package.json +++ b/packages/test-support/llm-mock-server/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-llm-mock-server", "description": "Scriptable OpenAI-compatible HTTP/SSE fault server for LLM recovery tests", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/test-support/llm-replay/package.json b/packages/test-support/llm-replay/package.json index f5281eb4f5..54e9d293b0 100644 --- a/packages/test-support/llm-replay/package.json +++ b/packages/test-support/llm-replay/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-llm-replay", "description": "Replay LLM plugin: short-circuits llm/stream with model chunks reconstructed from a recorded session JSONL (keyless snapshot tests)", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/test-support/loader-smoke/package.json b/packages/test-support/loader-smoke/package.json index f158d852c0..2d435c9d9e 100644 --- a/packages/test-support/loader-smoke/package.json +++ b/packages/test-support/loader-smoke/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-loader-smoke", "description": "Shared subprocess and direct-agent harness for keyless real-Loader example smoke tests", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/test-support/session-snapshot/package.json b/packages/test-support/session-snapshot/package.json index 8bf4945353..1b4576dccc 100644 --- a/packages/test-support/session-snapshot/package.json +++ b/packages/test-support/session-snapshot/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-snapshot", "description": "Session-log snapshot core with an ACP protocol adapter, expected-output normalization, and fixture invariants", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/todo/tool-todo/package.json b/packages/todo/tool-todo/package.json index d539bfd420..863345c42a 100644 --- a/packages/todo/tool-todo/package.json +++ b/packages/todo/tool-todo/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-todo", "description": "Model-facing todo_write tool over the DeepSeek Harness event-sourced session log", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/typert/generator/package.json b/packages/typert/generator/package.json index 8f4052432c..8ef779b90c 100644 --- a/packages/typert/generator/package.json +++ b/packages/typert/generator/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-typert-generator", "description": "TypeScript project analyzer and model-driven Typert artifact generator", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/typert/loader/package.json b/packages/typert/loader/package.json index 99873e3c8c..1e58aa5723 100644 --- a/packages/typert/loader/package.json +++ b/packages/typert/loader/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-typert-loader", "description": "Loader integration for generated Typert package contributions", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/typert/protocol/package.json b/packages/typert/protocol/package.json index 128a91015b..67b30c4377 100644 --- a/packages/typert/protocol/package.json +++ b/packages/typert/protocol/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-typert-protocol", "description": "Compiler-independent Remote metadata and Typert provider protocols", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/typert/registry/package.json b/packages/typert/registry/package.json index 57cc0269b7..2392b2f0c4 100644 --- a/packages/typert/registry/package.json +++ b/packages/typert/registry/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-typert-registry", "description": "Runtime registry for generated package reflection and Zod schemas", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/util/atomic-write/package.json b/packages/util/atomic-write/package.json index debc1821e2..c18e75ac26 100644 --- a/packages/util/atomic-write/package.json +++ b/packages/util/atomic-write/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-atomic-write", "description": "Zero-dependency atomic file replacement: exclusive-create random-suffix temp + rename carrying the caller-stated permissions (writeFileAtomic)", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/util/brand/package.json b/packages/util/brand/package.json index eb8f05ad2b..a7c8974297 100644 --- a/packages/util/brand/package.json +++ b/packages/util/brand/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-brand", "description": "Stateless branded primitive types for the DeepSeek Harness", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/util/chunked-list/package.json b/packages/util/chunked-list/package.json index 2a3fc316da..f27dee18f1 100644 --- a/packages/util/chunked-list/package.json +++ b/packages/util/chunked-list/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-chunked-list", "description": "Persistent append-only chunked lists with bounded copying and JSON checkpoint validation", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/util/crypto/package.json b/packages/util/crypto/package.json index 29c97db176..3e54890d57 100644 --- a/packages/util/crypto/package.json +++ b/packages/util/crypto/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-util-crypto", "description": "Zero-dependency browser-safe UUID and byte-encoding helpers", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/util/deque/package.json b/packages/util/deque/package.json index 9e770c6ebc..bdea147fb7 100644 --- a/packages/util/deque/package.json +++ b/packages/util/deque/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-deque", "description": "Zero-dependency circular deque with amortized constant-time end operations and bounded vacant storage", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/util/home-paths/package.json b/packages/util/home-paths/package.json index e2248ef6d4..34291f69ba 100644 --- a/packages/util/home-paths/package.json +++ b/packages/util/home-paths/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-home-paths", "description": "Shared filesystem path helpers for the DeepSeek Harness", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/util/http-proxy/package.json b/packages/util/http-proxy/package.json index 557ff05008..4091676849 100644 --- a/packages/util/http-proxy/package.json +++ b/packages/util/http-proxy/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-http-proxy", "description": "Process-wide outbound HTTP proxy policy for DeepSeek Harness: resolve it from the launch environment and install it as undici's global dispatcher", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/util/launch-environment/package.json b/packages/util/launch-environment/package.json index 05721e99de..69c88ef587 100644 --- a/packages/util/launch-environment/package.json +++ b/packages/util/launch-environment/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-launch-environment", "description": "Immutable DeepSeek Harness launch environment that records which layer supplied each value", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/util/native-command/package.json b/packages/util/native-command/package.json index ff25cef7b4..32b2562fd5 100644 --- a/packages/util/native-command/package.json +++ b/packages/util/native-command/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-native-command", "description": "Host-native command and path-opening utilities with shell-free execution, cancellation, desktop detection, and WSL handoff", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/util/output-retention/package.json b/packages/util/output-retention/package.json index 60f35e488c..ab3f8c5143 100644 --- a/packages/util/output-retention/package.json +++ b/packages/util/output-retention/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-output-retention", "description": "Zero-dependency bounded-retention primitive: ItemRetainer/TextRetainer + neutral notice helpers (what did we keep, what did we omit)", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/util/package-manifest/package.json b/packages/util/package-manifest/package.json index d5ac0e87bc..57e6beceda 100644 --- a/packages/util/package-manifest/package.json +++ b/packages/util/package-manifest/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-package-manifest", "description": "Shared type declarations for package.json.dsh configuration fields", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/util/time/package.json b/packages/util/time/package.json index 396b587e04..20b7468982 100644 --- a/packages/util/time/package.json +++ b/packages/util/time/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-util-time", "description": "Zero-dependency time vocabulary shared by wire boundaries: canonicalClientTimeZone (IANA zone validation and canonicalization only, no formatting)", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/util/timeout/package.json b/packages/util/timeout/package.json index 52601ba8e9..35500070eb 100644 --- a/packages/util/timeout/package.json +++ b/packages/util/timeout/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-timeout", "description": "Zero-dependency timeout/deadline primitive: clampTimeout, deadline, timeoutOf, TimeoutReason (timing + classification only, no termination)", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/util/values/package.json b/packages/util/values/package.json index 0caecde0da..8b08968d19 100644 --- a/packages/util/values/package.json +++ b/packages/util/values/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-util-values", "description": "Duplicate-install-safe value primitives for the DeepSeek Harness", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/util/workspace-path/package.json b/packages/util/workspace-path/package.json index 52831afa03..2d9f7d4cb7 100644 --- a/packages/util/workspace-path/package.json +++ b/packages/util/workspace-path/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-util-workspace-path", "description": "Browser-safe Workspace path and display helpers", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/web/tool-web/package.json b/packages/web/tool-web/package.json index 00029269e1..463248c787 100644 --- a/packages/web/tool-web/package.json +++ b/packages/web/tool-web/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-web", "description": "Model-facing web tools (web_search, web_fetch) over the DeepSeek Harness web capability seam (ctx.web)", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/web/web-fetch-http/package.json b/packages/web/web-fetch-http/package.json index 69deb08084..2c52a4b0f7 100644 --- a/packages/web/web-fetch-http/package.json +++ b/packages/web/web-fetch-http/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-web-fetch-http", "description": "Anonymous public HTTP(S) fetch provider for the DeepSeek Harness web capability seam (ctx.web)", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/web/web-search-deepseek/package.json b/packages/web/web-search-deepseek/package.json index 01ac736fcc..6e7ef73ca2 100644 --- a/packages/web/web-search-deepseek/package.json +++ b/packages/web/web-search-deepseek/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-web-search-deepseek", "description": "DeepSeek-backed search provider (native web_search via the Anthropic-compatible API) for the DeepSeek Harness web capability seam (ctx.web)", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/web/web-search-exa/package.json b/packages/web/web-search-exa/package.json index e50850670a..daefc8d2d3 100644 --- a/packages/web/web-search-exa/package.json +++ b/packages/web/web-search-exa/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-web-search-exa", "description": "Exa-backed search provider for the DeepSeek Harness web capability seam (ctx.web)", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/web/web-search-perplexity/package.json b/packages/web/web-search-perplexity/package.json index 8d58b9d76c..819966ac3a 100644 --- a/packages/web/web-search-perplexity/package.json +++ b/packages/web/web-search-perplexity/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-web-search-perplexity", "description": "Perplexity-backed search provider for the DeepSeek Harness web capability seam (ctx.web)", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/web/web/package.json b/packages/web/web/package.json index 7afab4d2bb..75b51cae79 100644 --- a/packages/web/web/package.json +++ b/packages/web/web/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-web", "description": "Abstract web access capability seam (ctx.web) for the DeepSeek Harness — search/fetch provider registry, registration-order-independent selection, request/result vocabulary, and the WebError taxonomy", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/webhook/webhook-github/package.json b/packages/webhook/webhook-github/package.json index 8ad3788e28..81b8c9cdeb 100644 --- a/packages/webhook/webhook-github/package.json +++ b/packages/webhook/webhook-github/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-webhook-github", "description": "Signed GitHub HTTP webhook adapter for the DeepSeek Harness webhook runtime", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/webhook/webhook/package.json b/packages/webhook/webhook/package.json index 27baa443ce..ee8416fe68 100644 --- a/packages/webhook/webhook/package.json +++ b/packages/webhook/webhook/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-webhook", "description": "Fire-and-forget webhook rule runtime that creates Workspace-backed DeepSeek Harness Sessions", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/workflow/tool-ralph/package.json b/packages/workflow/tool-ralph/package.json index 23921d85c6..a1b6609049 100644 --- a/packages/workflow/tool-ralph/package.json +++ b/packages/workflow/tool-ralph/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-ralph", "description": "Model-facing fresh-agent Ralph loop over the workflow and subagent seams", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/workflow/tool-workflow/package.json b/packages/workflow/tool-workflow/package.json index 71f9830317..06cb66d839 100644 --- a/packages/workflow/tool-workflow/package.json +++ b/packages/workflow/tool-workflow/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-workflow", "description": "Model-facing workflow tool: run a JavaScript orchestration script over ctx.workflowEngine", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/workflow/workflow-worker-thread/package.json b/packages/workflow/workflow-worker-thread/package.json index 030baacd98..2a91e2d2dc 100644 --- a/packages/workflow/workflow-worker-thread/package.json +++ b/packages/workflow/workflow-worker-thread/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-workflow-worker-thread", "description": "worker-thread workflow engine: executes model-written orchestration scripts off the host event loop, bridging agent() calls back to ctx.subagents", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/workflow/workflow/package.json b/packages/workflow/workflow/package.json index c5f414941c..8e03f25a7c 100644 --- a/packages/workflow/workflow/package.json +++ b/packages/workflow/workflow/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-workflow", "description": "Workflow capability seam: ctx.workflowEngine service, run vocabulary, and workflow/* events", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/workspace/workspace/package.json b/packages/workspace/workspace/package.json index 4a30eea728..bf1ad56f88 100644 --- a/packages/workspace/workspace/package.json +++ b/packages/workspace/workspace/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-workspace", "description": "Workspace entity registry (ctx.workspaceRegistry): durable workspace records with validated session attachment over the domain data form for the DeepSeek Harness", - "version": "0.1.5-alpha.1", + "version": "0.1.5-alpha.2", "publishConfig": { "access": "public" }, From 284ab155195d9ed7c06199a7ed0928b57a4d67e2 Mon Sep 17 00:00:00 2001 From: imccyu Date: Thu, 10 Sep 2026 01:04:55 +0800 Subject: [PATCH 15/27] fix(client): refine sidebar document previews --- ...r-and-preview-interaction-polish.i18n.yaml | 4 +- ...-sidebar-and-preview-interaction-polish.md | 6 +- ...debar-and-preview-interaction-polish.zh.md | 6 +- apps/web/tests/document-preview.e2e.ts | 29 ++++--- packages/client/ui-dockkit/README.i18n.yaml | 4 +- packages/client/ui-dockkit/README.md | 2 +- packages/client/ui-dockkit/README.zh.md | 2 +- .../src/components/dockkit.module.css | 2 + .../src/markdown/CodeBlock.module.css | 6 ++ .../ui-primitives/src/markdown/CodeBlock.tsx | 3 +- .../markdown-dom/code-fences.settled.txt | 79 ++++++++++--------- .../markdown-dom/code-fences.streaming.txt | 75 ++++++++++-------- .../fence-trailing-blank-lines.settled.txt | 40 +++++----- .../fence-trailing-blank-lines.streaming.txt | 38 ++++----- .../lists-tight-loose-nested.settled.txt | 7 +- .../lists-tight-loose-nested.streaming.txt | 7 +- .../math-edge-cases.streaming.txt | 7 +- .../README.i18n.yaml | 4 +- .../ui-sidebar-documentpreview/README.md | 8 +- .../ui-sidebar-documentpreview/README.zh.md | 8 +- .../src/client/TextPreview.module.css | 33 ++++---- .../src/client/TextPreview.tsx | 30 +++++-- .../src/client/code/CodeBody.module.css | 49 ++++++------ .../src/client/image/ImageBody.module.css | 1 + .../src/client/pdf/PdfBody.module.css | 2 + .../src/client/text/TextBody.tsx | 38 +++++---- .../src/client/text/lines.ts | 10 +-- .../tests/code-body.client.spec.tsx | 9 ++- .../tests/text-preview.client.spec.tsx | 26 +++--- 29 files changed, 301 insertions(+), 234 deletions(-) diff --git a/.agents/notes/implemented/feature/2026-09-09-sidebar-and-preview-interaction-polish.i18n.yaml b/.agents/notes/implemented/feature/2026-09-09-sidebar-and-preview-interaction-polish.i18n.yaml index 530553e7cc..2d723e4d15 100644 --- a/.agents/notes/implemented/feature/2026-09-09-sidebar-and-preview-interaction-polish.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-09-09-sidebar-and-preview-interaction-polish.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-09-sidebar-and-preview-interaction-polish.md -2026-09-09-sidebar-and-preview-interaction-polish.md: df0e768aa1aea913344c356d84c2a7d3386ccc6b -2026-09-09-sidebar-and-preview-interaction-polish.zh.md: d3f8b00fd86ec12d867d48a917cd4a65cab18ba9 +2026-09-09-sidebar-and-preview-interaction-polish.md: ad99e37984916fab638670bb3fb47dc2ade2d0bf +2026-09-09-sidebar-and-preview-interaction-polish.zh.md: 037ab2d147a0afad35902dfe57551b148c39ed90 diff --git a/.agents/notes/implemented/feature/2026-09-09-sidebar-and-preview-interaction-polish.md b/.agents/notes/implemented/feature/2026-09-09-sidebar-and-preview-interaction-polish.md index df0e768aa1..ad99e37984 100644 --- a/.agents/notes/implemented/feature/2026-09-09-sidebar-and-preview-interaction-polish.md +++ b/.agents/notes/implemented/feature/2026-09-09-sidebar-and-preview-interaction-polish.md @@ -18,7 +18,7 @@ Five small interaction defects around the right Sidebar and the document preview **Menus close on focus entering an iframe.** [Menu.tsx](../../../../packages/client/ui-primitives/src/Menu.tsx) adds a window `blur` listener gated on `document.activeElement instanceof HTMLIFrameElement` — the focus move is the only signal a pointerdown inside a cross-origin iframe leaves, and the gate keeps app or tab switches from closing the list. -**The code preview pins its banner and drops the card fill.** With wrap off, [CodeBody.module.css](../../../../packages/client/ui-sidebar-documentpreview/src/client/code/CodeBody.module.css) sizes the renderer `max-content` so the sticky banner has the full scroll width to ride, and pins the banner `sticky; left: 0; width: 100cqw` against the document scroller (`container-type: inline-size` on the preview body). The shared CodeBlock's fill is routed through a new `--dsl-code-block-background` variable (default unchanged, so chat keeps its gray card) and the shared banner carries an inert `data-code-block-banner` hook; the preview sets the variable to `transparent` so code sits on the pane's own background. +**The code preview separates its banner from scrolling source and drops the card fill.** The shared CodeBlock wraps its rendered source in a stable `data-code-block-content` node that defaults to `display: contents`, so existing consumers keep their layout. [CodeBody.module.css](../../../../packages/client/ui-sidebar-documentpreview/src/client/code/CodeBody.module.css) materializes that node as a full-height inner scrollport below the banner; the document owner caches that node for position restoration, paging, and line navigation. The shared CodeBlock's fill is routed through `--dsl-code-block-background` (default unchanged, so chat keeps its gray card), while the preview sets it to `transparent` so code sits on the pane's own background. ## Alternatives considered @@ -28,8 +28,8 @@ Five small interaction defects around the right Sidebar and the document preview **A bare window-blur close for menus.** Closes the list on every app or tab switch; the `activeElement` gate scopes the close to the one case the document cannot see. -**An inner code scroller for the horizontal axis.** Restoring `overflow-x: auto` on the `pre` keeps the banner still, but puts the horizontal scrollbar at the bottom of the whole block — unreachable in a long file — and both axes deliberately live in the document owner's scroller. +**Keep code in the shared document scroller.** A child banner cannot cover its parent's native scrollbar. Giving the stable source wrapper both scroll axes keeps the scrollbar at the visible viewport edge below the adjacent banner, rather than at the end of a long code block. ## Consequences -`planDropTab`'s factory parameter is new kit API any embedder may pass; `planSettle` already accepted an absent factory, which now also names the Sidebar's collapsed-state behavior. The `--dsl-code-block-background` variable and `data-code-block-banner` attribute are the code block's owner-styling seam; no shared stylesheet rule targets the attribute. Kit planner specs cover the backfilled self-split and its focus order; Sidebar store, service, and seat specs cover lazy seeding, pane-scoped page merges, and the empty collapsed layout; a Menu spec covers the gated blur close. The `ui-sidebar-right`, `ui-dockkit`, and `ui-sidebar-documentpreview` READMEs restate the rules. +`planDropTab`'s factory parameter is new kit API any embedder may pass; `planSettle` already accepted an absent factory, which now also names the Sidebar's collapsed-state behavior. The `--dsl-code-block-background` variable keeps chat's default gray card while the preview uses the pane background, and `data-code-block-content` lets an owner materialize a dedicated source viewport without changing other CodeBlock layouts. Kit planner specs cover the backfilled self-split and its focus order; Sidebar store, service, and seat specs cover lazy seeding, pane-scoped page merges, and the empty collapsed layout; document-preview specs cover inner scrolling and line navigation; a Menu spec covers the gated blur close. The `ui-sidebar-right`, `ui-dockkit`, and `ui-sidebar-documentpreview` READMEs restate the rules. diff --git a/.agents/notes/implemented/feature/2026-09-09-sidebar-and-preview-interaction-polish.zh.md b/.agents/notes/implemented/feature/2026-09-09-sidebar-and-preview-interaction-polish.zh.md index d3f8b00fd8..037ab2d147 100644 --- a/.agents/notes/implemented/feature/2026-09-09-sidebar-and-preview-interaction-polish.zh.md +++ b/.agents/notes/implemented/feature/2026-09-09-sidebar-and-preview-interaction-polish.zh.md @@ -18,7 +18,7 @@ Status: implemented **焦点进入 iframe 时关闭菜单。**[Menu.tsx](../../../../packages/client/ui-primitives/src/Menu.tsx) 增加 window `blur` 监听,以 `document.activeElement instanceof HTMLIFrameElement` 为门:焦点移动是跨源 iframe 内 pointerdown 留下的唯一信号,这道门也让应用或标签页切换不会误关列表。 -**代码预览钉住复制条并去掉卡片填充。**关闭折行时,[CodeBody.module.css](../../../../packages/client/ui-sidebar-documentpreview/src/client/code/CodeBody.module.css) 把渲染器设为 `max-content`,让吸附的复制条拥有完整滚动宽度可骑行,并以 `sticky; left: 0; width: 100cqw` 把它钉在文档滚动区上(预览正文设 `container-type: inline-size`)。共享 CodeBlock 的填充改经新变量 `--dsl-code-block-background`(默认值不变,会话保持灰色卡片),共享复制条带上惰性的 `data-code-block-banner` 钩子;预览把变量设为 `transparent`,代码于是坐在分栏自身的背景上。 +**代码预览把复制条与滚动源码分开,并去掉卡片填充。**共享 CodeBlock 用稳定的 `data-code-block-content` 节点包裹渲染后的源码;该节点默认使用 `display: contents`,因此既有消费者保持原布局。[CodeBody.module.css](../../../../packages/client/ui-sidebar-documentpreview/src/client/code/CodeBody.module.css) 将该节点实体化为复制条下方占满剩余高度的内部滚动区;文档 owner 缓存这个节点,用于位置恢复、分页和行跳转。共享 CodeBlock 的填充通过 `--dsl-code-block-background` 设置(默认值不变,会话保持灰色卡片),预览将它设为 `transparent`,代码因此直接使用分栏背景。 ## Alternatives considered @@ -28,8 +28,8 @@ Status: implemented **菜单用裸的 window blur 关闭。**每次应用或标签页切换都会关掉列表;`activeElement` 门把关闭收窄到父文档看不见的那一种情形。 -**代码横轴用内层滚动。**在 `pre` 上恢复 `overflow-x: auto` 能让复制条不动,但横向滚动条会落在整个代码块底部——长文件里够不着——而且两个轴本就有意放在文档 owner 的滚动区里。 +**让代码继续使用共享文档滚动区。**子级复制条无法覆盖父级的原生滚动条。让稳定的源码包装节点同时承载两个滚动轴,滚动条会停在复制条下方的可见视口边缘,而不是长代码块的末端。 ## Consequences -`planDropTab` 的工厂参数是任何嵌入方都可传的新套件 API;`planSettle` 本就接受缺省工厂,如今它同时命名了侧边栏的折叠态行为。`--dsl-code-block-background` 变量与 `data-code-block-banner` 属性是代码块的 owner 定制接缝;共享样式表没有任何规则指向该属性。套件 planner 规格覆盖带回填的本格分栏及其聚焦顺序;侧边栏 store、service 与 seat 规格覆盖惰性播种、格内页合并与折叠后的空布局;一条 Menu 规格覆盖带门的 blur 关闭。`ui-sidebar-right`、`ui-dockkit` 与 `ui-sidebar-documentpreview` 的 README 重述了这些规则。 +`planDropTab` 的工厂参数是任何嵌入方都可传的新套件 API;`planSettle` 本就接受缺省工厂,如今它同时命名了侧边栏的折叠态行为。`--dsl-code-block-background` 变量保留 Chat 默认的灰色卡片,Preview 则使用分栏背景;`data-code-block-content` 允许 owner 实体化专用源码视口,而不改变其它 CodeBlock 布局。套件 planner 规格覆盖带回填的本格分栏及其聚焦顺序;侧边栏 store、service 与 seat 规格覆盖惰性播种、格内页合并与折叠后的空布局;文档预览规格覆盖内部滚动与行跳转;一条 Menu 规格覆盖带门的 blur 关闭。`ui-sidebar-right`、`ui-dockkit` 与 `ui-sidebar-documentpreview` 的 README 重述了这些规则。 diff --git a/apps/web/tests/document-preview.e2e.ts b/apps/web/tests/document-preview.e2e.ts index 41af1afa6b..8715b98d3d 100644 --- a/apps/web/tests/document-preview.e2e.ts +++ b/apps/web/tests/document-preview.e2e.ts @@ -240,6 +240,18 @@ describe.skipIf(MODE === 'record')('web e2e: document preview through Files', () const iframe = preview.locator('[data-html-preview]') await iframe.waitFor({ timeout: 15_000 }) expect(await iframe.getAttribute('sandbox')).toBe('allow-scripts') + expect(await iframe.evaluate((node) => { + const host = node.closest('[data-textpreview-body]') + if (!(host instanceof HTMLElement)) throw new Error('HTML preview body is unavailable') + const outer = host.getBoundingClientRect() + const frame = node.getBoundingClientRect() + return { + top: Math.round(frame.top - outer.top), + right: Math.round(outer.right - frame.right), + bottom: Math.round(outer.bottom - frame.bottom), + left: Math.round(frame.left - outer.left), + } + })).toEqual({ top: 0, right: 0, bottom: 0, left: 0 }) const html = page.frameLocator('[data-html-preview]') await html.getByRole('heading', { name: 'HTML smoke', exact: true }).waitFor({ timeout: 15_000 }) await expect.poll(() => html.locator('#result').innerText()).toBe('INLINE_OK') @@ -379,6 +391,7 @@ describe.skipIf(MODE === 'record')('web e2e: document preview through Files', () const highlightedLines = preview.locator('.shiki .line') await expect.poll(() => highlightedLines.count(), { timeout: 15_000 }).toBe(PAGE_LINES) const codeBlock = preview.locator('.md-code-block') + const codeScrollport = preview.locator('[data-code-block-content]') expect(await codeBlock.getAttribute('data-line-numbers')).toBe('true') await expect.poll(() => highlightedLines.first().evaluate(node => getComputedStyle(node, '::before').content)) .not.toMatch(/^(?:none|normal)$/u) @@ -397,33 +410,31 @@ describe.skipIf(MODE === 'record')('web e2e: document preview through Files', () const prefix = await highlightedLines.allTextContents() expect(prefix).toEqual(codeLines.slice(0, PAGE_LINES)) await expect.poll(() => preview.locator('[data-textpreview-more]').isEnabled()).toBe(true) - await scrollForNextPage(body) + await scrollForNextPage(codeScrollport) await expect.poll(() => highlightedLines.count(), { timeout: 15_000 }).toBe(codeLines.length) const completed = await highlightedLines.allTextContents() expect(completed).toEqual(codeLines) await expect.poll(() => preview.locator('[data-textpreview-more]').count()).toBe(0) - const scrollTop = await body.evaluate((node) => { + const scrollTop = await codeScrollport.evaluate((node) => { const target = Math.floor((node.scrollHeight - node.clientHeight) / 2) if (target <= 0) throw new Error('code fixture does not overflow the document body') node.scrollTop = target return target }) - await expect.poll(() => body.evaluate((node) => { - const banner = node.querySelector('.md-code-block')?.firstElementChild + await expect.poll(() => codeScrollport.evaluate((node) => { + const codeBlock = node.parentElement + const banner = codeBlock?.firstElementChild const firstLine = node.querySelector('.shiki .line') if (!(banner instanceof HTMLElement) || firstLine === null) throw new Error('missing rendered code banner or source line') const bounds = node.getBoundingClientRect() const clipTop = bounds.top + node.clientTop const bannerBounds = banner.getBoundingClientRect() - const hit = document.elementFromPoint(bounds.left + node.clientLeft + node.clientWidth / 2, clipTop + 1) return { scrollTop: node.scrollTop, - position: getComputedStyle(banner).position, - topGap: bannerBounds.top - clipTop, + scrollportBelowBanner: Math.abs(bannerBounds.bottom - bounds.top) < 1, firstLineAbove: firstLine.getBoundingClientRect().top < clipTop, - topCoveredByBanner: hit !== null && banner.contains(hit), } - })).toEqual({ scrollTop, position: 'sticky', topGap: 0, firstLineAbove: true, topCoveredByBanner: true }) + })).toEqual({ scrollTop, scrollportBelowBanner: true, firstLineAbove: true }) await page.context().grantPermissions(['clipboard-read', 'clipboard-write'], { origin: new URL(page.url()).origin }) await page.evaluate(() => navigator.clipboard.writeText('')) await codeBlock.getByRole('button', { name: 'Copy', exact: true }).click() diff --git a/packages/client/ui-dockkit/README.i18n.yaml b/packages/client/ui-dockkit/README.i18n.yaml index d7e58f30ee..92fa2f6fde 100644 --- a/packages/client/ui-dockkit/README.i18n.yaml +++ b/packages/client/ui-dockkit/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-dockkit/README.md -README.md: 41100530b77f9776c768010b53ecbc6ad67b5902 -README.zh.md: 237ce5f0aedf1124e4dc3a214bf10529dea1703f +README.md: f41668ad2eb6c16e13deb5fcc554c8717811a468 +README.zh.md: f92828b99ef5678458614261304c05d382bc34f4 diff --git a/packages/client/ui-dockkit/README.md b/packages/client/ui-dockkit/README.md index 41100530b7..f41668ad2e 100644 --- a/packages/client/ui-dockkit/README.md +++ b/packages/client/ui-dockkit/README.md @@ -52,7 +52,7 @@ Everything host-specific arrives through props: `DockController` satisfies `DockIntents` as written, so the simplest embedding hands the controller straight to `DockSurface`. An embedder that routes through its own store implements the same method names instead. Three props carry control policy rather than gestures: `canSplit` (surface-wide, the pane budget; disables the split control with `splitPaneDisabled`), `canAddTab(paneId)` (per pane, omits the add control; leave it out to draw one in every pane), and `canCloseTab(tabId)` (per tab, withholds the chip's close control and the menu's close item together; leave it out to keep every tab closable). Hiding the add control moves nothing else in the strip, and a withheld close moves nothing in the chip — the close control paints over the title's end rather than beside it. A pane's lone chip whose close is withheld draws quiet — no capsule, no hover fill — since there is nothing to select against and nothing to do to it. The kit adds one policy of its own, the room rule below, which disables a pane's split control with `splitPaneNarrow`; `onRoom(fits)` reports its readings so an embedder splitting programmatically can honour the same rule. -`dropZones="horizontal"` offers two half-pane hints; once budget or width forbids another split, the whole body accepts a move. A hint is a dashed card inset 8px inside its region, showing the zone's glyph and `labels.dropZone[zone]`; the card under the pointer takes the accent and its neighbour stays a quiet outline. `minPaneFraction` sets the preview minimum, and `planResizeSplit` accepts the same minimum for the committed operation. The Sidebar uses 0.2 and enforces two panes in its own store. The generic engine retains its tree and other split directions. `hideSplitWhenBlocked` hides a blocked split control — pane budget spent or pane too narrow — instead of rendering it disabled; its default is false. +`dropZones="horizontal"` offers two half-pane hints; once budget or width forbids another split, the whole body accepts a move. A hint is a dashed card inset 8px inside its region, showing the zone's glyph and `labels.dropZone[zone]`; the preview layer covers all tab-body content, while the card under the pointer takes the accent and its neighbour stays a quiet outline. `minPaneFraction` sets the preview minimum, and `planResizeSplit` accepts the same minimum for the committed operation. The Sidebar uses 0.2 and enforces two panes in its own store. The generic engine retains its tree and other split directions. `hideSplitWhenBlocked` hides a blocked split control — pane budget spent or pane too narrow — instead of rendering it disabled; its default is false. A tab's `kind` is an opaque string. Seeded tabs are factories (`DockControllerOptions`), so what a fresh pane contains is the embedder's decision, not this package's. Content identity is the pair (`kind`, `contentId`): `findContentTab(state, contentId, kind?)` finds the tab showing it anywhere and `findPaneContentTab(state, paneId, contentId, kind?)` within one pane, and `planOpenContent` focuses that tab instead of opening another unless told `revealIfOpened: false`; an explicit `index` seats a new tab at a strip slot rather than at the end. diff --git a/packages/client/ui-dockkit/README.zh.md b/packages/client/ui-dockkit/README.zh.md index 237ce5f0ae..f92828b99e 100644 --- a/packages/client/ui-dockkit/README.zh.md +++ b/packages/client/ui-dockkit/README.zh.md @@ -52,7 +52,7 @@ kind: "package-reference" `DockController` 原样满足 `DockIntents`,所以最简单的嵌入就是把 controller 直接交给 `DockSurface`。经由自己 store 路由的嵌入方则实现同名方法。有三个 props 承载的是控制策略而非手势:`canSplit`(整面有效,即格预算;用 `splitPaneDisabled` 禁用分栏控件)、`canAddTab(paneId)`(按格,省略添加控件;不传则每格都画)与 `canCloseTab(tabId)`(按 tab,把 chip 的关闭控件和菜单的关闭项一并收起;不传则每个 tab 都可关闭)。隐藏添加控件不会移动 tab 条里的其它任何东西,收起关闭也不会移动 chip 里的任何东西——关闭控件压在标题末端之上而非并排。某格仅剩的一个 chip 在关闭被收起时画成安静样式——没有胶囊底色,没有悬停填充——因为既没有别的 tab 可供选择,也没有任何可对它做的事。套件自己再加一条策略,即下文的空间规则,它用 `splitPaneNarrow` 禁用某格的分栏控件;`onRoom(fits)` 上报其读数,让以编程方式分栏的嵌入方能遵守同一规则。 -`dropZones="horizontal"` 提供左右两个半区提示;预算或宽度不允许再拆时,正文整格接收移动。提示是一张内缩 8px 的虚线卡片,显示该落区的图形和 `labels.dropZone[zone]`;指针所在的卡片取强调色,另一张保持安静的轮廓。`minPaneFraction` 控制预览的最小比例,`planResizeSplit` 接受相同最小值以约束提交;Sidebar使用0.2并在自己的store限制两格。通用引擎仍保留原有树与其它分割方向。 `hideSplitWhenBlocked` 在分栏被阻止时(窗格预算已满或格太窄)直接隐藏分栏控件而不是渲染禁用态,默认值为 false。 +`dropZones="horizontal"` 提供左右两个半区提示;预算或宽度不允许再拆时,正文整格接收移动。提示是一张内缩 8px 的虚线卡片,显示该落区的图形和 `labels.dropZone[zone]`;预览层覆盖全部 tab 正文,指针所在的卡片取强调色,另一张保持安静的轮廓。`minPaneFraction` 控制预览的最小比例,`planResizeSplit` 接受相同最小值以约束提交;Sidebar使用0.2并在自己的store限制两格。通用引擎仍保留原有树与其它分割方向。 `hideSplitWhenBlocked` 在分栏被阻止时(窗格预算已满或格太窄)直接隐藏分栏控件而不是渲染禁用态,默认值为 false。 tab 的 `kind` 是不透明字符串。种子 tab 是工厂(`DockControllerOptions`),因此新格里放什么由嵌入方决定,与本包无关。内容身份是二元组(`kind`、`contentId`):`findContentTab(state, contentId, kind?)` 在任意位置找到展示它的 tab,`findPaneContentTab(state, paneId, contentId, kind?)` 在一个格内找;`planOpenContent` 会聚焦该 tab 而非再开一个,除非被告知 `revealIfOpened: false`;显式的 `index` 把新 tab 放到 tab 条的某个位置而非末尾。 diff --git a/packages/client/ui-dockkit/src/components/dockkit.module.css b/packages/client/ui-dockkit/src/components/dockkit.module.css index 972ec3b951..c86f6cac54 100644 --- a/packages/client/ui-dockkit/src/components/dockkit.module.css +++ b/packages/client/ui-dockkit/src/components/dockkit.module.css @@ -509,6 +509,7 @@ .dockScrim { position: absolute; inset: 0; + z-index: 10; pointer-events: none; /* The ground the panes sit on, not a raised layer: in dark mode `bg-layer-2` is a lighter bluish step than the column's `bg-base`, which tinted the @@ -520,6 +521,7 @@ .dockHint { position: absolute; + z-index: 10; display: flex; box-sizing: border-box; padding: 8px; diff --git a/packages/client/ui-primitives/src/markdown/CodeBlock.module.css b/packages/client/ui-primitives/src/markdown/CodeBlock.module.css index a6e25ba39f..96a9b62ec1 100644 --- a/packages/client/ui-primitives/src/markdown/CodeBlock.module.css +++ b/packages/client/ui-primitives/src/markdown/CodeBlock.module.css @@ -69,6 +69,12 @@ font: inherit; } +/* Consumers may turn the stable content node into a viewport without changing + the default CodeBlock layout. */ +.content { + display: contents; +} + .block :where(pre) { font: var(--dsl-code-block-content-font); padding: 16px; diff --git a/packages/client/ui-primitives/src/markdown/CodeBlock.tsx b/packages/client/ui-primitives/src/markdown/CodeBlock.tsx index 02cdbe683a..dd9bee4d5f 100644 --- a/packages/client/ui-primitives/src/markdown/CodeBlock.tsx +++ b/packages/client/ui-primitives/src/markdown/CodeBlock.tsx @@ -178,7 +178,6 @@ export function CodeBlock({ code, lang, streaming, className, lineNumbers = fals '--dsl-code-block-line-number-width': `${Math.max(2, String(sourceLines.length).length)}ch`, } as CSSProperties}>
- {/* The attribute carries no style here; owner stylesheets use it to pin the banner (sidebar code preview, horizontal scroll). */}
{lang ?? ''}
@@ -188,7 +187,7 @@ export function CodeBlock({ code, lang, streaming, className, lineNumbers = fals
- {body} +
{body}
) } diff --git a/packages/client/ui-primitives/tests/fixtures/markdown-dom/code-fences.settled.txt b/packages/client/ui-primitives/tests/fixtures/markdown-dom/code-fences.settled.txt index 66dd41f69e..5b41c005f8 100644 --- a/packages/client/ui-primitives/tests/fixtures/markdown-dom/code-fences.settled.txt +++ b/packages/client/ui-primitives/tests/fixtures/markdown-dom/code-fences.settled.txt @@ -7,22 +7,23 @@
{open && createPortal(
{ expect(tokens.textContent).toContain('Cache hit90%') expect(tokens.textContent).toContain('Uncached input10 tok') expect(tokens.textContent).toContain('Cached input90 tok') - expect(tokens.textContent).toContain('Cache write0 tok') + // A session that never wrote cache drops the row rather than showing 0. + expect(tokens.textContent).not.toContain('Cache write') expect(tokens.textContent).toContain('Output5 tok') // The time split lives on the counts pill's own dialog, not here. expect(dialog.textContent).not.toContain('LLM time') @@ -428,6 +429,9 @@ describe('StatsPills', () => { }, })} />) expect(view.getAllByRole('button')[0]!.textContent).toBe('207 tok·Cache hit 45%') + // A session that did write cache keeps the row, exact. + fireEvent.click(view.getAllByRole('button')[0]!) + expect(view.getByRole('dialog').textContent).toContain('Cache write100 tok') }) it('renders ZERO times during streaming chunk frames (RFC hard acceptance)', () => { diff --git a/packages/client/ui-chat/tests/turn-usage-panel.client.spec.tsx b/packages/client/ui-chat/tests/turn-usage-panel.client.spec.tsx index cd1704eb61..bee247b4af 100644 --- a/packages/client/ui-chat/tests/turn-usage-panel.client.spec.tsx +++ b/packages/client/ui-chat/tests/turn-usage-panel.client.spec.tsx @@ -122,12 +122,25 @@ describe('TurnTimePanel', () => { expect(view.queryByRole('dialog')).toBeNull() }) - it('omits unrecorded speed and TTFT rows', () => { + it('renders plain text without a dialog while only the duration is known', () => { const view = render() - fireEvent.click(view.getByRole('button')) - const dialog = view.getByRole('dialog') - expect(dialog.textContent).toContain('Total run time3s') - expect(dialog.textContent).not.toContain('Tokens per second') - expect(dialog.textContent).not.toContain('Time to first token') + // The dialog would only repeat the pill's own duration, so there is no trigger at all. + expect(view.queryByRole('button')).toBeNull() + expect(view.container.textContent).toBe('Ran for 3s') + }) + + it('keeps the dialog and omits the unrecorded row while one of speed and TTFT is known', () => { + const speedOnly = render() + fireEvent.click(speedOnly.getByRole('button')) + expect(speedOnly.getByRole('dialog').textContent).toContain('Total run time3s') + expect(speedOnly.getByRole('dialog').textContent).toContain('Tokens per second (TPS)20 tok/s') + expect(speedOnly.getByRole('dialog').textContent).not.toContain('Time to first token') + fireEvent.keyDown(document, { key: 'Escape' }) + speedOnly.unmount() + + const ttftOnly = render() + fireEvent.click(ttftOnly.getByRole('button')) + expect(ttftOnly.getByRole('dialog').textContent).not.toContain('Tokens per second') + expect(ttftOnly.getByRole('dialog').textContent).toContain('Time to first token (TTFT)0.8s') }) }) diff --git a/packages/client/ui-sidebar-files/README.md b/packages/client/ui-sidebar-files/README.md index cadadc03f9..28e052b5a6 100644 --- a/packages/client/ui-sidebar-files/README.md +++ b/packages/client/ui-sidebar-files/README.md @@ -24,11 +24,11 @@ The right Sidebar's navigator tab type: the session's workspace root as a tree, ## What it registers -- **The type** — `ctx.sidebarRightTabs.register(...)` with kind `files`, id `@deepseek-ai/dsh-client-ui-sidebar-files`, band `builtin`, no patterns, and one guide entry (order 10, titled from the `sidebarFiles` namespace) that opens the type. +- **The type** — `ctx.sidebarRightTabs.register(...)` with kind `files`, id `@deepseek-ai/dsh-client-ui-sidebar-files`, band `builtin`, no patterns, and one guide entry (order 10, its title and description from the `sidebarFiles` namespace, its glyph the shared folder icon) that opens the type. - **The body** — the keyed `sidebar.right.pane.tab` seat under that id: a header row under the strip, then the tree. The header row is the document preview's (`ui-sidebar-documentpreview`): the root path, its directories greyed and its last segment in full ink, never ellipsized (a path wider than the row keeps its end and fades its start), with the one control, reload, at its right. The row is copied rather than shared because a plugin bundle shares runtime code only through the platform modules; once the artifact and slot surfaces settle, one copy in `ui-primitives` could serve every pane header. - **The chip title** — the keyed `sidebar.right.pane.tab.title` seat under that id: a shared `FileTypeIcon` folder glyph at 16px before the type's label. The tree's own rows never draw this sheet. -Seven source files under `src/client/`: `definition.ts` (the type), `store.ts` (what it keeps), `face.ts` (how it lists, Remote binding included), `FilesBody.tsx` (what it draws, with its ordering and failure-line helpers), `FilesTitle.tsx` (the chip title), `locales.ts` (what it says), and `index.ts` (the wiring). +Seven source files under `src/client/`: `definition.tsx` (the type), `store.ts` (what it keeps), `face.ts` (how it lists, Remote binding included), `FilesBody.tsx` (what it draws, with its ordering and failure-line helpers), `FilesTitle.tsx` (the chip title), `locales.ts` (what it says), and `index.ts` (the wiring). ## The tree diff --git a/packages/client/ui-sidebar-files/README.zh.md b/packages/client/ui-sidebar-files/README.zh.md index e25d86b275..f9724c159e 100644 --- a/packages/client/ui-sidebar-files/README.zh.md +++ b/packages/client/ui-sidebar-files/README.zh.md @@ -24,11 +24,11 @@ kind: "package-reference" ## 注册了什么 -- **类型**:`ctx.sidebarRightTabs.register(...)`,kind 为 `files`,id 为 `@deepseek-ai/dsh-client-ui-sidebar-files`,档位 `builtin`,没有 patterns,另有一个打开该类型的引导页入口(order 10,标题取自 `sidebarFiles` 命名空间)。 +- **类型**:`ctx.sidebarRightTabs.register(...)`,kind 为 `files`,id 为 `@deepseek-ai/dsh-client-ui-sidebar-files`,档位 `builtin`,没有 patterns,另有一个打开该类型的引导页入口(order 10,标题与描述取自 `sidebarFiles` 命名空间,图标是共享的文件夹图标)。 - **正文**:以该 id 为键的 `sidebar.right.pane.tab` 坑位:strip 下的一行标题行,然后是树。标题行与文档预览(`ui-sidebar-documentpreview`)的相同:根路径,目录部分灰色、最后一段正色,从不省略号截断(比行宽的路径保留末尾、淡出开头),右端是它唯一的控件、重新读取。这一行是复制而非共享,因为插件 bundle 只经平台模块共享运行时代码;待 artifact 与各 slot 的形态定下来后,可以在 `ui-primitives` 放一份供每个 pane 标题行使用。 - **标签页标题**:以该 id 为键的 `sidebar.right.pane.tab.title` 坑位:类型标签前的一枚 16px 共享 `FileTypeIcon` 文件夹图标。树本身的行不画这枚图标。 -`src/client/` 下七个源文件:`definition.ts`(类型是什么)、`store.ts`(它保存什么)、`face.ts`(它如何列目录,含 Remote 绑定)、`FilesBody.tsx`(它画什么,含排序与失败行两个辅助函数)、`FilesTitle.tsx`(标签页标题)、`locales.ts`(它说什么)、`index.ts`(接线)。 +`src/client/` 下七个源文件:`definition.tsx`(类型是什么)、`store.ts`(它保存什么)、`face.ts`(它如何列目录,含 Remote 绑定)、`FilesBody.tsx`(它画什么,含排序与失败行两个辅助函数)、`FilesTitle.tsx`(标签页标题)、`locales.ts`(它说什么)、`index.ts`(接线)。 ## 树 diff --git a/packages/client/ui-sidebar-files/src/client/definition.ts b/packages/client/ui-sidebar-files/src/client/definition.tsx similarity index 74% rename from packages/client/ui-sidebar-files/src/client/definition.ts rename to packages/client/ui-sidebar-files/src/client/definition.tsx index 00b475c829..4397b117e6 100644 --- a/packages/client/ui-sidebar-files/src/client/definition.ts +++ b/packages/client/ui-sidebar-files/src/client/definition.tsx @@ -8,7 +8,7 @@ import type { SidebarRightTabDefinition } from '@deepseek-ai/dsh-client-ui-sidebar-right/client' import type { TranslateNS } from '@deepseek-ai/dsh-client-locale/client' import type {} from './locales.ts' -import { IconFolderClose16 } from '@deepseek-ai/dsh-client-ui-primitives' +import { FileTypeIcon, type IconProps } from '@deepseek-ai/dsh-client-ui-primitives' /** The tab kind this package owns. */ export const FILES_KIND = 'files' @@ -16,6 +16,11 @@ export const FILES_KIND = 'files' /** This implementation's identity in the tab system, and the key its body registers under. */ export const FILES_ID = '@deepseek-ai/dsh-client-ui-sidebar-files' +/** The type's coloured folder sheet at the guide capsule's glyph size, as the chip title draws it. */ +function FolderSheetGlyph({ size, className }: IconProps) { + return +} + /** * The files type's registry definition. * @param t - namespace-bound translate, read fresh on every label call. @@ -30,7 +35,8 @@ export function filesDefinition(t: TranslateNS<'sidebarFiles'>): SidebarRightTab guide: [{ order: 10, title: () => t('guide.title'), - icon: IconFolderClose16, + description: () => t('guide.description'), + icon: FolderSheetGlyph, }], } } diff --git a/packages/client/ui-sidebar-files/src/client/index.ts b/packages/client/ui-sidebar-files/src/client/index.ts index 5f5b391a80..572f5583b5 100644 --- a/packages/client/ui-sidebar-files/src/client/index.ts +++ b/packages/client/ui-sidebar-files/src/client/index.ts @@ -6,7 +6,7 @@ * the keyed `sidebar.right.pane.tab.title` seat, both under the type's `id`. * * The file split is this package's layering: what the type IS - * (`definition.ts`), what it keeps (`store.ts`), how it lists (`face.ts`), what + * (`definition.tsx`), what it keeps (`store.ts`), how it lists (`face.ts`), what * it draws (`FilesBody.tsx`, `FilesTitle.tsx`), what it says (`locales.ts`), * and this module, which only wires them together. */ @@ -15,7 +15,7 @@ import type {} from '@deepseek-ai/dsh-api-remotes/client' import type {} from '@deepseek-ai/dsh-client-ui-renderer/client' import type {} from '@deepseek-ai/dsh-client-ui-session/client' import type {} from '@deepseek-ai/dsh-client-ui-sidebar-right/client' -import { FILES_ID, filesDefinition } from './definition.ts' +import { FILES_ID, filesDefinition } from './definition.tsx' import { createList, filesFace } from './face.ts' import { FilesBody } from './FilesBody.tsx' import { FilesTitle } from './FilesTitle.tsx' diff --git a/packages/client/ui-sidebar-files/src/client/locales.ts b/packages/client/ui-sidebar-files/src/client/locales.ts index dfca3151de..b88e6bab40 100644 --- a/packages/client/ui-sidebar-files/src/client/locales.ts +++ b/packages/client/ui-sidebar-files/src/client/locales.ts @@ -22,6 +22,7 @@ declare module '@deepseek-ai/dsh-client-ui-slots' { export const zh = { 'type.label': '文件', 'guide.title': '工作区文件', + 'guide.description': '浏览会话工作区的文件', loading: '正在读取…', empty: '空目录', truncated: '条目太多,只显示了一部分。', @@ -41,6 +42,7 @@ export type SidebarFilesKey = keyof typeof zh export const en = { 'type.label': 'Files', 'guide.title': 'Workspace files', + 'guide.description': 'Browse files in this session\'s workspace', loading: 'Reading…', empty: 'Empty directory', truncated: 'Too many entries, showing only some of them.', diff --git a/packages/client/ui-sidebar-files/tests/apply.client.spec.ts b/packages/client/ui-sidebar-files/tests/apply.client.spec.ts index 13f29d498b..e81d21dd8d 100644 --- a/packages/client/ui-sidebar-files/tests/apply.client.spec.ts +++ b/packages/client/ui-sidebar-files/tests/apply.client.spec.ts @@ -10,7 +10,7 @@ import { describe, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' import { SidebarRightTabRegistry } from '@deepseek-ai/dsh-client-ui-sidebar-right/src/client/tab-registry.ts' -import { FILES_ID, FILES_KIND } from '../src/client/definition.ts' +import { FILES_ID, FILES_KIND } from '../src/client/definition.tsx' import { apply, inject } from '../src/client/index.ts' import { apply as hostApply } from '../src/index.ts' import { FilesBody } from '../src/client/FilesBody.tsx' @@ -69,7 +69,8 @@ describe('ui-sidebar-files apply', () => { expect(definition?.id).toBe(FILES_ID) expect(definition?.priority).toBe('builtin') expect(definition?.title('sidebar://files')).toBe('type.label') - expect(definition?.guide?.map(entry => [entry.order, entry.title()])).toEqual([[10, 'guide.title']]) + expect(definition?.guide?.map(entry => [entry.order, entry.title(), entry.description?.()])) + .toEqual([[10, 'guide.title', 'guide.description']]) expect(dictionaries.get('sidebarFiles')).toEqual({ zh, en }) // The seat key is the implementation's id, not the kind: an extension may // take the kind over, and the seat must still find this body. diff --git a/packages/client/ui-sidebar-files/tests/definition.client.spec.ts b/packages/client/ui-sidebar-files/tests/definition.client.spec.ts index ad88a03c44..933455c544 100644 --- a/packages/client/ui-sidebar-files/tests/definition.client.spec.ts +++ b/packages/client/ui-sidebar-files/tests/definition.client.spec.ts @@ -10,7 +10,7 @@ import { SidebarRightTabRegistry } from '@deepseek-ai/dsh-client-ui-sidebar-righ import { sessionFileAddress } from '@deepseek-ai/dsh-util-workspace-path' import { FILES_ID, FILES_KIND, filesDefinition, -} from '../src/client/definition.ts' +} from '../src/client/definition.tsx' import { zh } from '../src/client/locales.ts' const t = makeTranslate(zh) @@ -31,6 +31,7 @@ describe('filesDefinition', () => { expect(entry?.order).toBe(10) expect(entry?.kind).toBe(FILES_KIND) expect(entry?.title()).toBe(zh['guide.title']) + expect(entry?.description?.()).toBe(zh['guide.description']) expect(entry?.icon).toBeDefined() }) diff --git a/packages/client/ui-sidebar-right/README.md b/packages/client/ui-sidebar-right/README.md index d0bf1b9a52..c947b5411f 100644 --- a/packages/client/ui-sidebar-right/README.md +++ b/packages/client/ui-sidebar-right/README.md @@ -98,7 +98,7 @@ The Tab domain retains navigation, an abort signal, and bound actions per (Sessi Default pages depend on the number of registered guide entries, not the number of tab types or open tabs. Exactly one entry opens its page directly (Files in the shipped composition); zero or multiple entries open the guide. Explicitly adding a guide still opens the guide, even with one entry. The sole docked guide is the only tab that cannot close; closing any other sole tab also collapses the column. The chip, context menu, and `close` API apply the same rule. -The guide tab is one entry capsule (a glyph and a title, nothing else) per `guide` entry the registered types contributed, in `order`, centred in the body; the guide has no words of its own. Picking a capsule calls `tab.actions.openTab(entry.kind, { replaceTab: true })`, so the guide gives way to the page it opened. A pane holds at most one guide tab. The strip's add control is drawn only while its pane holds none and opens one there with `openTab('guide', { paneId, revealIfOpened: false })`, so a guide in another pane does not capture the click; opening the guide into a pane that already has one focuses it instead; a guide dragged, dropped, or docked into such a pane merges into it — the arriving guide closes and the pane's own is focused; `duplicateTab` on the guide records nothing. A split, an expanded empty root pane, and the pane a sole tab vacates by dropping on its own edge use the same default-page rule, one tab per new pane; the self-edge drop leaves the dragged tab focused. A plain `openTab('guide')` opens or focuses the guide only within the active or named pane. Splitting an empty pane does nothing and returns no new pane. The product allows two horizontal panes, initially equal, with divider ratios limited to 20%–80%. Insufficient width blocks a new split; with two panes already present, a body drop moves the tab between panes instead of creating a third. At the two-pane limit, split controls are hidden; closing back to one pane restores them. +The guide tab is a muted compass over one entry capsule per `guide` entry the registered types contributed, in `order`, centred in the body; the guide has no words of its own. A capsule shows the entry's glyph — or the guide's quieter cube placeholder when the entry registered none — and its title; while at most four entries are listed, an entry that registered a `description` shows it under the title, and a longer list drops every description. Picking a capsule calls `tab.actions.openTab(entry.kind, { replaceTab: true })`, so the guide gives way to the page it opened. A pane holds at most one guide tab. The strip's add control is drawn only while its pane holds none and opens one there with `openTab('guide', { paneId, revealIfOpened: false })`, so a guide in another pane does not capture the click; opening the guide into a pane that already has one focuses it instead; a guide dragged, dropped, or docked into such a pane merges into it — the arriving guide closes and the pane's own is focused; `duplicateTab` on the guide records nothing. A split, an expanded empty root pane, and the pane a sole tab vacates by dropping on its own edge use the same default-page rule, one tab per new pane; the self-edge drop leaves the dragged tab focused. A plain `openTab('guide')` opens or focuses the guide only within the active or named pane. Splitting an empty pane does nothing and returns no new pane. The product allows two horizontal panes, initially equal, with divider ratios limited to 20%–80%. Insufficient width blocks a new split; with two panes already present, a body drop moves the tab between panes instead of creating a third. At the two-pane limit, split controls are hidden; closing back to one pane restores them. ## Copy diff --git a/packages/client/ui-sidebar-right/README.zh.md b/packages/client/ui-sidebar-right/README.zh.md index 087c55d1ae..51a5fd9e31 100644 --- a/packages/client/ui-sidebar-right/README.zh.md +++ b/packages/client/ui-sidebar-right/README.zh.md @@ -98,7 +98,7 @@ Tab域按(Session,Tab id)保留导航、中止信号与绑定动作;私 默认页取决于已注册的引导入口数,不取决于 tab 类型数或已打开的 tab 数。恰好一个入口时直接打开对应页面(随包组合中为 Files);没有入口或有多个入口时打开引导页。即使只有一个入口,显式添加引导页仍会打开引导页。只有作为唯一停靠 tab 的引导页不可关闭;关闭其它任何唯一 tab 时会同时收起整列。chip、上下文菜单与 `close` API 使用同一规则。 -引导 tab 是各已注册类型贡献的每个 `guide` 条目一个入口胶囊(一个图标加一个标题,别无其他),按 `order` 排列并在正文中居中;引导页自己没有文字。选中一个胶囊会调用 `tab.actions.openTab(entry.kind, { replaceTab: true })`,于是引导页让位给它打开的页。一个格最多持有一个引导 tab。tab 条的添加控件只在该格没有引导 tab 时绘制,并以 `openTab('guide', { paneId, revealIfOpened: false })` 在该格打开一个,这样别的格里的引导页不会截走这次点击;把引导页开进已有引导页的格则改为聚焦它;把引导页拖入、放入或收回到这样的格会合并进去——来者关闭,该格自己的被聚焦;对引导页 `duplicateTab` 不记录任何东西。分栏、展开且为空的根格、以及唯一 tab 拖到本格边缘分屏后腾出的格使用相同的默认页规则,每个新格一个 tab;这种本格边缘拖放让被拖的 tab 保持聚焦。普通的 `openTab('guide')` 只在活跃或指定分栏内打开或聚焦引导页。对空分栏执行 split 不产生变化,也不返回新分栏。产品最多保留左右两格,默认均分,分隔条限定20%~80%。宽度不足以容纳两格时不允许新分栏;已有两格时,正文拖放用于跨格移动,不再创建第三格。达到两格上限时隐藏分栏控件;关闭回单格后恢复。 +引导 tab 是一枚弱化的罗盘,下方是各已注册类型贡献的每个 `guide` 条目一个入口胶囊,按 `order` 排列并在正文中居中;引导页自己没有文字。胶囊显示条目的图标——条目没注册图标时用引导页自己更浅的立方体占位符——和标题;列出的条目不超过四个时,注册了 `description` 的条目在标题下显示它,更长的列表则去掉所有描述。选中一个胶囊会调用 `tab.actions.openTab(entry.kind, { replaceTab: true })`,于是引导页让位给它打开的页。一个格最多持有一个引导 tab。tab 条的添加控件只在该格没有引导 tab 时绘制,并以 `openTab('guide', { paneId, revealIfOpened: false })` 在该格打开一个,这样别的格里的引导页不会截走这次点击;把引导页开进已有引导页的格则改为聚焦它;把引导页拖入、放入或收回到这样的格会合并进去——来者关闭,该格自己的被聚焦;对引导页 `duplicateTab` 不记录任何东西。分栏、展开且为空的根格、以及唯一 tab 拖到本格边缘分屏后腾出的格使用相同的默认页规则,每个新格一个 tab;这种本格边缘拖放让被拖的 tab 保持聚焦。普通的 `openTab('guide')` 只在活跃或指定分栏内打开或聚焦引导页。对空分栏执行 split 不产生变化,也不返回新分栏。产品最多保留左右两格,默认均分,分隔条限定20%~80%。宽度不足以容纳两格时不允许新分栏;已有两格时,正文拖放用于跨格移动,不再创建第三格。达到两格上限时隐藏分栏控件;关闭回单格后恢复。 ## 文案 diff --git a/packages/client/ui-sidebar-right/src/client/tab-registry.ts b/packages/client/ui-sidebar-right/src/client/tab-registry.ts index 47c737b98f..1b93096010 100644 --- a/packages/client/ui-sidebar-right/src/client/tab-registry.ts +++ b/packages/client/ui-sidebar-right/src/client/tab-registry.ts @@ -21,8 +21,8 @@ * builtin resumes when the extension unregisters. Everything else colliding on * a kind throws, as does a second registration of an `id`. * - * Thunked copy (`title`, `guide[].title`) is read again on every use, so a - * language change needs no re-registration. + * Thunked copy (`title`, `guide[].title`, `guide[].description`) is read again + * on every use, so a language change needs no re-registration. */ import type { ComponentType } from 'react' import type { Context } from '@deepseek-ai/cordis' @@ -62,11 +62,18 @@ export interface SidebarRightGuideEntry { /** Ascending position among every registered type's entries. */ readonly order: number /** - * The capsule's title, its only words. + * The capsule's title. * @returns the title in the current language. */ readonly title: () => string - /** Optional glyph, drawn before the title. */ + /** + * One line under the title on what picking the capsule opens. The guide shows + * it only while it lists few enough entries to stay light; a crowded guide + * falls back to titles alone, so a type must stand on its title. + * @returns the description in the current language. + */ + readonly description?: () => string + /** Optional glyph, drawn before the title; without one the guide draws its cube placeholder. */ readonly icon?: ComponentType } diff --git a/packages/client/ui-sidebar-right/src/client/tabs/guide/GuideBody.module.css b/packages/client/ui-sidebar-right/src/client/tabs/guide/GuideBody.module.css index 86d514e065..dd1ca24894 100644 --- a/packages/client/ui-sidebar-right/src/client/tabs/guide/GuideBody.module.css +++ b/packages/client/ui-sidebar-right/src/client/tabs/guide/GuideBody.module.css @@ -1,12 +1,12 @@ /* The guide body's own sheet: its domain owns these rules, so the shell's stylesheet does not carry them. Tokens only. */ -/* The capsules in one centred column, lifted above the body's middle by the - spacer below — the empty-state offset. */ +/* The capsules in one centred column at the column's own 14px gap, lifted + above the body's middle by the spacer below — the empty-state offset. */ .guide { display: flex; flex-direction: column; - gap: 8px; + gap: 14px; align-items: center; justify-content: center; box-sizing: border-box; @@ -15,31 +15,42 @@ } /* The lift, as a share of the pane's height (a column flex basis resolves - against it): the centred column rises 6% of the pane, and a short pane + against it): the centred column rises 5% of the pane, and a short pane gives it up before the capsules clip. */ .guide::after { content: ''; - flex: 0 1 12%; + flex: 0 1 10%; } -/* A 48px capsule, 360px wide by default; `max-width: 100%` shrinks it inside - the column's own 24px insets, so a narrow pane still keeps that gutter. A - long title ellipsizes inside rather than widening it. */ +/* The guide tab's own compass above the capsules, at 56px on the dimmed ink — + a watermark, not a control. Its margin over the column's gap holds the + first capsule 30px below it; the capsules keep the bare gap between them. */ +.hero { + display: flex; + margin-bottom: 16px; + color: var(--dsw-alias-label-dimmed); +} + +/* A filled pill, 380px wide by default, its icon and text anchored at the + left inset; `max-width: 100%` shrinks it inside the column's own 24px + insets, so a narrow pane still keeps that gutter. The 56px minimum is the + title-only height; a capsule with a description grows on its vertical + padding. Long text ellipsizes inside rather than widening it. */ .entry { display: flex; - gap: 8px; + gap: 14px; align-items: center; - justify-content: center; box-sizing: border-box; - width: 360px; + width: 380px; max-width: 100%; - height: 48px; - padding: 0 20px; + min-height: 56px; + padding: 14px 20px; color: var(--dsw-alias-label-primary); font: inherit; - background: transparent; + text-align: left; + background: var(--dsw-alias-bg-layer-1); border: 0.5px solid var(--dsw-alias-border-l4); - border-radius: 20px; + border-radius: 24px; cursor: pointer; } @@ -47,22 +58,55 @@ background: var(--dsw-alias-interactive-bg-hover); } +/* A fixed 26px box — the described glyph's size — so an entry's glyph sits + centred and every capsule's words start at the same left edge while glyph + sizes differ. */ .entryIcon { display: flex; flex: none; + align-items: center; + justify-content: center; + width: 26px; + height: 26px; color: var(--dsw-alias-label-secondary); } -.entryTitle { +/* The capsule's placeholder cube on ink one step quieter than a registered + glyph's, marking the unclaimed slot. */ +.placeholderInk { + color: var(--dsw-alias-label-tertiary); +} + +/* The capsule's words in one column beside the glyph: the title, and the + description while the guide shows one. */ +.entryText { + display: flex; + flex-direction: column; + gap: 3px; min-width: 0; +} + +.entryTitle { overflow: hidden; - font-size: var(--dsh-content-font-size, 14px); + font-size: 15px; line-height: 1.4; white-space: nowrap; text-overflow: ellipsis; } -/* The chip title's planet, beside the label the strip lays out. */ +/* One line under the title on the quieter ink; it ellipsizes like the title. */ +.entryDescription { + overflow: hidden; + color: var(--dsw-alias-label-caption); + font-size: 13px; + line-height: 1.4; + white-space: nowrap; + text-overflow: ellipsis; +} + +/* The chip title's compass, beside the label the strip lays out; the glyph + draws on currentColor, so this ink is the chip rendering's own. */ .titleIcon { flex: none; + color: var(--dsw-alias-label-tertiary); } diff --git a/packages/client/ui-sidebar-right/src/client/tabs/guide/GuideBody.tsx b/packages/client/ui-sidebar-right/src/client/tabs/guide/GuideBody.tsx index 5a9851739a..dcc46bac8f 100644 --- a/packages/client/ui-sidebar-right/src/client/tabs/guide/GuideBody.tsx +++ b/packages/client/ui-sidebar-right/src/client/tabs/guide/GuideBody.tsx @@ -8,8 +8,12 @@ * entry of its own, so there is always exactly one body and the shipped one * cannot be outvoted by accident. * - * The shipped guide is the entry capsules every registered type contributed, - * centred in the body, and nothing else. Picking one opens that type as a page + * The shipped guide is a muted compass over the entry capsules every + * registered type contributed, centred in the body, and nothing else — no + * heading, as a browser start page shows its doors without a caption. While + * at most four entries are listed, a capsule with a description shows it + * under the title; a longer list drops every description to stay light. + * Picking one opens that type as a page * in this tab's place, so the guide is a doorway rather than a page that stays * open. */ @@ -17,6 +21,7 @@ import type { ReactNode } from 'react' import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store' import type { ChainRenderOpts, HookContextOf, InjectFace, PropsRenderSlots, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' import type { SidebarRightGuideBox } from '../../tab-registry.ts' +import { CompassGlyph, CubeGlyph } from './GuideTitle.tsx' import css from './GuideBody.module.css' /** What the guide body needs from its host beyond the framework shares. */ @@ -25,15 +30,23 @@ export interface GuideInjected { readonly hooks: { readonly guideEntries: ObservableSnapshot } } -/** The guide body's composed props: the tab it draws, its chain child, and the entries. Its words are the entries' own. */ +/** The guide body's composed props: the tab it draws, its chain child, and the entries. */ export type GuideBodyProps = & PropsRuntime<'sidebar.right.pane.tab'> & PropsRenderSlots<'sidebar.right.tab.guide'> & InjectFace -/** One entry capsule: the contributing type's glyph and title. */ -function EntryBox({ entry, onPick }: { entry: SidebarRightGuideBox; onPick: (entry: SidebarRightGuideBox) => void }): ReactNode { - const Icon = entry.icon +/** Entry count past which the guide drops the capsules' descriptions to stay light. */ +const MAX_DESCRIBED_ENTRIES = 4 + +/** One entry capsule: the contributing type's glyph and title, and its description while the guide is short. */ +function EntryBox({ entry, described, onPick }: { + entry: SidebarRightGuideBox + described: boolean + onPick: (entry: SidebarRightGuideBox) => void +}): ReactNode { + const Icon = entry.icon ?? CubeGlyph + const description = described ? entry.description?.() : undefined return ( ) } -/** The shipped guide: the doors out of the column. */ +/** The shipped guide: the tab's own compass over the doors out of the column. */ function ShippedGuide({ entries, onPick }: { entries: readonly SidebarRightGuideBox[] onPick: (entry: SidebarRightGuideBox) => void }): ReactNode { return (
+ {/* Keyed by position in the ordered list: one type may contribute several capsules, and `order` is not unique. */} - {entries.map((entry, index) => )} + {entries.map((entry, index) => ( + + ))}
) } @@ -67,7 +89,8 @@ export function GuideBody({ useTabInfo, useGuideEntries, renderSlotChain }: Guid const options = { hookContext: useTabInfo, fallback: ( - { tab.actions.openTab(entry.kind, { replaceTab: true }) }} /> + { tab.actions.openTab(entry.kind, { replaceTab: true }) }} /> ), } satisfies ChainRenderOpts & { hookContext: HookContextOf<'sidebar.right.tab.guide'> } return renderSlotChain('sidebar.right.tab.guide', {}, options) diff --git a/packages/client/ui-sidebar-right/src/client/tabs/guide/GuideTitle.tsx b/packages/client/ui-sidebar-right/src/client/tabs/guide/GuideTitle.tsx index 2b8428b789..3c763ce95a 100644 --- a/packages/client/ui-sidebar-right/src/client/tabs/guide/GuideTitle.tsx +++ b/packages/client/ui-sidebar-right/src/client/tabs/guide/GuideTitle.tsx @@ -1,20 +1,53 @@ /** * The guide type's chip title: the compass before the type's label. Registered * under `sidebar.right.pane.tab.title`; without it the chip would show the - * bare label. + * bare label. Both guide glyphs live here: the compass the chip and the body's + * hero draw, and the cube the body's icon-less capsules fall back to. */ import type { ReactNode } from 'react' +import type { IconProps } from '@deepseek-ai/dsh-client-ui-primitives' import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' import css from './GuideBody.module.css' /** - * The compass: a grey ring with the needle's rhombus pointing north-east. + * The compass: a ring with the needle's rhombus pointing north-east, on + * `currentColor` so each rendering picks its own ink. + * @param props - rendered size and class. + * @returns the compass glyph. */ -function CompassGlyph(): ReactNode { +export function CompassGlyph({ size = 16, className }: IconProps): ReactNode { return ( -
-
{body}
+
{body}
) } diff --git a/packages/client/ui-primitives/tests/code-block.client.spec.tsx b/packages/client/ui-primitives/tests/code-block.client.spec.tsx index 4413cf46d2..5e6475297c 100644 --- a/packages/client/ui-primitives/tests/code-block.client.spec.tsx +++ b/packages/client/ui-primitives/tests/code-block.client.spec.tsx @@ -59,6 +59,17 @@ describe('highlightToHtml', () => { }) describe('CodeBlock', () => { + it('reports the stable source-content wrapper to its owner', () => { + const contentRef = vi.fn<(node: HTMLDivElement | null) => void>() + const view = render() + const content = view.container.querySelector('[data-code-block-content]') + expect(contentRef).toHaveBeenCalledWith(content) + view.rerender() + expect(view.container.querySelector('[data-code-block-content]')).toBe(content) + view.unmount() + expect(contentRef).toHaveBeenLastCalledWith(null) + }) + it('renders the highlighted tree for TypeScript', () => { const view = render() const pre = view.container.querySelector('pre.shiki') diff --git a/packages/client/ui-sidebar-documentpreview/README.i18n.yaml b/packages/client/ui-sidebar-documentpreview/README.i18n.yaml index 1705ddecfe..2f5dcd7db5 100644 --- a/packages/client/ui-sidebar-documentpreview/README.i18n.yaml +++ b/packages/client/ui-sidebar-documentpreview/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-sidebar-documentpreview/README.md -README.md: f86d769b9074cd0f272a5f2882251644574f0688 -README.zh.md: 72693000dfc54e7da5f0d397ae540c69586bbf23 +README.md: e3659ed08a7c82f7c5ef314806ea07262f421caa +README.zh.md: 2c2fe2225a87b333f501e58f4988ff8034c14273 diff --git a/packages/client/ui-sidebar-documentpreview/README.md b/packages/client/ui-sidebar-documentpreview/README.md index f86d769b90..e3659ed08a 100644 --- a/packages/client/ui-sidebar-documentpreview/README.md +++ b/packages/client/ui-sidebar-documentpreview/README.md @@ -27,10 +27,10 @@ Preview readable files in the right Sidebar and choose among registered renderer ## What it registers - **The type** — `ctx.sidebarRightTabs.register(...)` with id `@deepseek-ai/dsh-client-ui-sidebar-documentpreview` (this implementation's identity in the tab system, and the key its body registers under), kind `text`, pattern `dsh-resource://file/**`, band `fallback`. `canOpen` accepts only Session addresses, whose paths may be relative or absolute; bare `absolute` addresses are not claimed. A type registered at the `extension` or `builtin` band for a narrower pattern (say `*.png`) takes those addresses; other supported files land here. The whole address is the content identity, so two files with one name in different directories, or one path under two sessions, are two tabs; the decoded basename is the tab title, and the keyed `sidebar.right.pane.tab.title` seat places its extension-specific `FileTypeIcon` before that title. -- **The body** — the keyed `sidebar.right.pane.tab` seat under the type's id. Its fixed header shows the Host's absolute path when available, otherwise the requested path; directories use tertiary label colour, the name uses primary label colour, and a clipped path retains and fades toward its final segment while its tooltip exposes the full value. A dropdown selects among matching renderers and plain text. A wrap toggle appears only when the selected renderer declares `wrap: true`; its glyph describes the mode the click selects, and the per-tab preference starts on. Reload stays in this header, not the Sidebar's tab strip. The body reaches every pane edge; each renderer owns its content inset and may own an inner scrollport. +- **The body** — the keyed `sidebar.right.pane.tab` seat under the type's id. Its fixed header shows the Host's absolute path when available, otherwise the requested path; directories use tertiary label colour, the name uses primary label colour, and a clipped path retains and fades toward its final segment while its tooltip exposes the full value. A dropdown selects among matching renderers and plain text. A wrap toggle appears only when the selected renderer declares `wrap: true`; its glyph describes the mode the click selects, and the per-tab preference starts on. Reload stays in this header, not the Sidebar's tab strip. The body reaches every pane edge; each renderer owns its content inset and may own an inner scrollport. This intentionally differs from the Files tab's 2px right-side scrollbar offset: previews keep the full pane width so edge-to-edge HTML and code scrollports end at the pane edge. - **Shared loading and view state**, session-scoped and bucketed by tab id. The store holds accumulated pages or complete bytes, read and observed versions, loading/failure state, renderer choice, scroll offset, wrap, and the answered navigation revision. The ordinary inject face calls Remote readers and writes through declared store actions. Reloads and loading-mode changes retire older requests; the tab's abort signal forgets its state. -Document implementations register metadata with `ctx.documentPreviews.register({ id, extensions, priority, title, loading, wrap? })` and a body under the same `id` in the keyed, Session-scoped `sidebar.right.tab.document` child slot. Own both registrations with effects and wait for the child slot through `ctx.slots.inject`. Bodies receive `resourceAddress`, prepared `content`, `wrap`, and the standard `useTabInfo`/`useResource` hooks; they do not receive a custom resource loader. Metadata declares `loading: 'text-pages'` or `'bytes-complete'`. The registry retains all matching alternatives: `extension` (the default) ranks above `builtin`, then longer suffixes rank first, then registration order. The dropdown preserves a selected implementation while it remains available; removing it selects the next candidate. Builtin bodies use these same registrations. +Document implementations register metadata with `ctx.documentPreviews.register({ id, extensions, priority, title, loading, wrap? })` and a body under the same `id` in the keyed, Session-scoped `sidebar.right.tab.document` child slot. Own both registrations with effects and wait for the child slot through `ctx.slots.inject`. Bodies receive `resourceAddress`, prepared `content`, `wrap`, `scrollportRef`, and the standard `useTabInfo`/`useResource` hooks; they do not receive a custom resource loader. A renderer that owns an inner scrolling element attaches `scrollportRef` to it, and the owner returns to the shared body when that element unmounts. Metadata declares `loading: 'text-pages'` or `'bytes-complete'`. The registry retains all matching alternatives: `extension` (the default) ranks above `builtin`, then longer suffixes rank first, then registration order. The dropdown preserves a selected implementation while it remains available; removing it selects the next candidate. Builtin bodies use these same registrations. ## Addresses diff --git a/packages/client/ui-sidebar-documentpreview/README.zh.md b/packages/client/ui-sidebar-documentpreview/README.zh.md index 72693000df..2c2fe2225a 100644 --- a/packages/client/ui-sidebar-documentpreview/README.zh.md +++ b/packages/client/ui-sidebar-documentpreview/README.zh.md @@ -27,10 +27,10 @@ kind: "package-reference" ## 注册了什么 - **类型** —— `ctx.sidebarRightTabs.register(...)`,id 为 `@deepseek-ai/dsh-client-ui-sidebar-documentpreview`(这个实现在 tab 系统里的唯一键,也是其体注册所用的 key),kind `text`,pattern `dsh-resource://file/**`,档位 `fallback`。`canOpen` 只接受 Session 地址,其中路径可为相对或绝对路径;不认领裸 `absolute` 地址。在 `extension` 或 `builtin` 档以更窄 pattern(比如 `*.png`)注册的类型接走那些地址;其他受支持文件落到这里。整个地址就是内容身份,所以不同目录下同名的两个文件、或同一路径在两个会话之下,是两个 tab;解码后的 basename 是 tab 标题,keyed slot `sidebar.right.pane.tab.title` 会在标题前放置按扩展名选择的 `FileTypeIcon`。 -- **正文** —— keyed slot `sidebar.right.pane.tab`,键为类型的 id。固定头部在可用时显示 Host 的绝对路径,否则显示请求路径;目录使用三级标签色,文件名使用一级标签色,路径过长时保留末段并向开头淡出,提示中仍提供完整值。下拉菜单可在匹配的渲染器与纯文本间切换。仅当所选渲染器声明 `wrap: true` 时显示换行开关;图标表示点击后切换到的模式,该偏好按 tab 保存,初始开启。重新载入仍在此头部,不放入 Sidebar 的 tab 条。正文贴合格的每条边,各渲染器自行提供内容留白,并可拥有内部滚动区。 +- **正文** —— keyed slot `sidebar.right.pane.tab`,键为类型的 id。固定头部在可用时显示 Host 的绝对路径,否则显示请求路径;目录使用三级标签色,文件名使用一级标签色,路径过长时保留末段并向开头淡出,提示中仍提供完整值。下拉菜单可在匹配的渲染器与纯文本间切换。仅当所选渲染器声明 `wrap: true` 时显示换行开关;图标表示点击后切换到的模式,该偏好按 tab 保存,初始开启。重新载入仍在此头部,不放入 Sidebar 的 tab 条。正文贴合格的每条边,各渲染器自行提供内容留白,并可拥有内部滚动区。这与 Files tab 右侧预留 2px 滚动条间距的布局有意不同:Preview 使用格的完整宽度,使贴边 HTML 与代码滚动区终止于格的边缘。 - **共享加载与视图状态**,会话作用域、按 tab id 分桶。store 持有累计页或完整字节、读取与观察版本、加载/失败状态、渲染器选择、滚动位置、换行和已响应的导航 revision。普通 inject face 调用 Remote 读取,并经声明的 store action 写入。重新载入和加载模式变化会淘汰旧请求;tab 的中止信号清理其状态。 -文档实现在 `ctx.documentPreviews.register({ id, extensions, priority, title, loading, wrap? })` 注册元数据,并以相同 `id` 向 keyed、Session 作用域的子 slot `sidebar.right.tab.document` 注册正文。两处注册都由 effect 持有,通过 `ctx.slots.inject` 等待子 slot。正文接收 `resourceAddress`、准备好的 `content`、`wrap` 和标准 `useTabInfo`/`useResource` 钩子,不接收自定义资源加载器。元数据声明 `loading: 'text-pages'` 或 `'bytes-complete'`。注册表保留所有匹配备选:`extension`(默认)优先于 `builtin`,随后按更长的后缀、再按注册顺序排列。所选实现仍可用时,下拉选择保持不变;移除后选择下一个候选。内置正文也使用相同注册方式。 +文档实现在 `ctx.documentPreviews.register({ id, extensions, priority, title, loading, wrap? })` 注册元数据,并以相同 `id` 向 keyed、Session 作用域的子 slot `sidebar.right.tab.document` 注册正文。两处注册都由 effect 持有,通过 `ctx.slots.inject` 等待子 slot。正文接收 `resourceAddress`、准备好的 `content`、`wrap`、`scrollportRef` 和标准 `useTabInfo`/`useResource` 钩子,不接收自定义资源加载器。拥有内部滚动元素的渲染器把 `scrollportRef` 挂到该元素上;该元素卸载后,owner 恢复使用共享正文。元数据声明 `loading: 'text-pages'` 或 `'bytes-complete'`。注册表保留所有匹配备选:`extension`(默认)优先于 `builtin`,随后按更长的后缀、再按注册顺序排列。所选实现仍可用时,下拉选择保持不变;移除后选择下一个候选。内置正文也使用相同注册方式。 ## 地址 diff --git a/packages/client/ui-sidebar-documentpreview/src/client/TextPreview.module.css b/packages/client/ui-sidebar-documentpreview/src/client/TextPreview.module.css index f1b8f1dda1..1e19b311ac 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/TextPreview.module.css +++ b/packages/client/ui-sidebar-documentpreview/src/client/TextPreview.module.css @@ -1,6 +1,6 @@ -/* The pane body is a block scroller with a definite height, not a flex - container, so the preview takes that height outright: the header row stays - put and the file body below is the one scroller, however short the file. */ +/* The preview takes the pane's definite height outright, so the header stays + put while the body below owns the remaining height. A renderer may replace + the body's scrolling role with an inner scrollport. */ .preview { display: flex; flex: 1 1 auto; diff --git a/packages/client/ui-sidebar-documentpreview/src/client/TextPreview.tsx b/packages/client/ui-sidebar-documentpreview/src/client/TextPreview.tsx index 192f05ee80..5bebee5d22 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/TextPreview.tsx +++ b/packages/client/ui-sidebar-documentpreview/src/client/TextPreview.tsx @@ -10,7 +10,7 @@ * with the same reload. The type's controls, viewer choice, wrap and reload, sit at the end of * the path row; the Sidebar's strip carries none of them. */ -import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' +import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' import type { ReactNode, RefObject } from 'react' import clsx from 'clsx' import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store' @@ -69,13 +69,6 @@ export type TextPreviewProps = & InjectFace & PropsLocale<'sidebarDocumentPreview'> -/** The selected renderer's scrolling element; code owns an inner scrollport. */ -function scrollportOf(body: HTMLDivElement | null): HTMLElement | null { - if (body === null) return null - const code = body.querySelector('[data-code-preview]') - return code?.querySelector('[data-code-block-content]') ?? body -} - /** * The text type's body, registered under `sidebar.right.pane.tab` as `text`. * @param props - composed slot props. @@ -102,6 +95,7 @@ export function TextPreview({ const current = (state?.mode ?? 'text-pages') === mode ? state : undefined const bodyRef = useRef(null) const scrollportRef = useRef(null) + const storedScrollTopRef = useRef(0) const pathRef = useRef(null) const pathTextRef = useRef(null) const [menuOpen, setMenuOpen] = useState(false) @@ -114,11 +108,17 @@ export function TextPreview({ const loaded = useMemo(() => loadedPages(pages ?? {}), [pages]) const loadedThrough = lastLineLoaded(loaded) const hasContent = loaded.length > 0 || current?.complete !== undefined - - useLayoutEffect(() => { - scrollportRef.current = scrollportOf(bodyRef.current) - return () => { scrollportRef.current = null } - }, [hasContent, selected?.id]) + storedScrollTopRef.current = state?.scrollTop ?? 0 + const bindBody = useCallback((body: HTMLDivElement | null): void => { + const previous = bodyRef.current + bodyRef.current = body + if (scrollportRef.current === null || scrollportRef.current === previous) scrollportRef.current = body + }, []) + const bindScrollport = useCallback((scrollport: HTMLElement | null): void => { + const next = scrollport ?? bodyRef.current + scrollportRef.current = next + if (scrollport !== null) scrollport.scrollTop = storedScrollTopRef.current + }, []) // First mount reads the first page; a body coming back to a tab with content // reads nothing, because the store outlives the body. @@ -280,7 +280,7 @@ export function TextPreview({
)} {content !== undefined && renderSlot('sidebar.right.tab.document', { - resourceAddress: tab.contentId, content, wrap: state.wrap, + resourceAddress: tab.contentId, content, wrap: state.wrap, scrollportRef: bindScrollport, }, { entryKey: selected.id, hookContext: useTabInfo, fallback:

{t('rendererUnavailable', { name: selected.title() })}

, diff --git a/packages/client/ui-sidebar-documentpreview/src/client/code/CodeBody.tsx b/packages/client/ui-sidebar-documentpreview/src/client/code/CodeBody.tsx index 605309594c..473c6f01c4 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/code/CodeBody.tsx +++ b/packages/client/ui-sidebar-documentpreview/src/client/code/CodeBody.tsx @@ -12,7 +12,7 @@ import css from './CodeBody.module.css' export type CodeBodyProps = DocumentPreviewProps & PropsLocale<'sidebarCodePreview'> /** @param props - accumulated document contents and framework props. @returns one stable CodeBlock, or no body for byte contents. */ -export function CodeBody({ resourceAddress, content, wrap, t }: CodeBodyProps): ReactNode { +export function CodeBody({ resourceAddress, content, wrap, scrollportRef, t }: CodeBodyProps): ReactNode { if (content.kind !== 'text') return null const file = parseFileAddress(resourceAddress) if (file === undefined) throw new Error(`ui-sidebar-documentpreview: not a file address "${resourceAddress}"`) @@ -21,6 +21,7 @@ export function CodeBody({ resourceAddress, content, wrap, t }: CodeBodyProps):
} hookContext: UseSidebarRightTabInfo inject: { diff --git a/packages/client/ui-sidebar-documentpreview/tests/markdown-registration.client.spec.tsx b/packages/client/ui-sidebar-documentpreview/tests/markdown-registration.client.spec.tsx index 9b7be7285c..582fdb8220 100644 --- a/packages/client/ui-sidebar-documentpreview/tests/markdown-registration.client.spec.tsx +++ b/packages/client/ui-sidebar-documentpreview/tests/markdown-registration.client.spec.tsx @@ -54,6 +54,7 @@ describe('Markdown implementation registration', () => { resourceAddress: 'dsh-resource://file/session/markdown-registration/notes.md', content: { kind: 'text', text: '# Notes\n\n```ts\nconst value = 1\n```', pages: [], eof: true }, wrap: false, + scrollportRef: vi.fn(), }, { entryKey: MARKDOWN_BODY_ID, hookContext: useTabInfo, fallback: })) const view = runtime.renderRoot() expect(view.getByRole('heading', { name: 'Notes' })).toBeDefined() diff --git a/packages/client/ui-sidebar-documentpreview/tests/text-preview.client.spec.tsx b/packages/client/ui-sidebar-documentpreview/tests/text-preview.client.spec.tsx index 4c17b83d18..b20e10704c 100644 --- a/packages/client/ui-sidebar-documentpreview/tests/text-preview.client.spec.tsx +++ b/packages/client/ui-sidebar-documentpreview/tests/text-preview.client.spec.tsx @@ -426,6 +426,28 @@ describe('TextPreview — the file\'s metadata', () => { }) describe('TextPreview — navigation and view', () => { + it('rebinds scrolling when the selected Slot body is replaced without changing the renderer id', async () => { + const h = harness({ 1: page(1, ['a', 'b', 'c'], true) }) + const code = codeProps(h, { revision: 1 }) + const fallback: TextPreviewProps = { ...code, renderSlot: () =>
} + const view = render() + await settle() + const outer = body(view.container) + fireEvent.scroll(outer, { target: { scrollTop: 120 } }) + expect(h.instance.getSnapshot().byTab[TAB_ID]?.scrollTop).toBe(120) + + view.rerender() + const inner = scrollport(view.container) + expect(inner).not.toBe(outer) + expect(inner.scrollTop).toBe(120) + fireEvent.scroll(inner, { target: { scrollTop: 240 } }) + expect(h.instance.getSnapshot().byTab[TAB_ID]?.scrollTop).toBe(240) + + view.rerender() + fireEvent.scroll(outer, { target: { scrollTop: 360 } }) + expect(h.instance.getSnapshot().byTab[TAB_ID]?.scrollTop).toBe(360) + }) + it.each([ ['Code', 'code', 2 * LINE_HEIGHT], ['Plain text', PLAIN_BODY_ID, 2 * LINE_HEIGHT], From ded26f225dd9b3a7d3e6d21616e729bf47025603 Mon Sep 17 00:00:00 2001 From: imccyu Date: Thu, 10 Sep 2026 01:57:14 +0800 Subject: [PATCH 19/27] chore(client): refresh slot catalog --- .../extensions/cordis-client-runner/src/client/slot-catalog.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts index c0299970cf..5de0596ffe 100644 --- a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts +++ b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts @@ -2363,7 +2363,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ ], replaceRisk: 'none', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'sidebar.right.tab.document\', () => ctx.slots.register(\n { name: \'sidebar.right.tab.document\', key: \'\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-sidebar-documentpreview/src/client/document/contract.ts:23', + source: 'packages/client/ui-sidebar-documentpreview/src/client/document/contract.ts:24', }, { key: 'sidebar.right.tab.guide', From 3435dbd690967fe49b6fce92b39535e8e6785637 Mon Sep 17 00:00:00 2001 From: imccyu Date: Thu, 10 Sep 2026 02:45:17 +0800 Subject: [PATCH 20/27] fix(web): address sidebar and stats review feedback --- ...sidebar-tab-types-and-navigation.i18n.yaml | 4 +-- ...-09-05-sidebar-tab-types-and-navigation.md | 2 +- ...-05-sidebar-tab-types-and-navigation.zh.md | 2 +- ...debar-text-preview-and-file-tree.i18n.yaml | 4 +-- ...9-05-sidebar-text-preview-and-file-tree.md | 6 ++--- ...5-sidebar-text-preview-and-file-tree.zh.md | 6 ++--- ...-07-composer-session-stats-pills.i18n.yaml | 4 +-- ...2026-09-07-composer-session-stats-pills.md | 4 +-- ...6-09-07-composer-session-stats-pills.zh.md | 4 +-- ...t-page-and-stat-pill-refinements.i18n.yaml | 4 +-- ...de-start-page-and-stat-pill-refinements.md | 12 +++------ ...start-page-and-stat-pill-refinements.zh.md | 12 +++------ apps/web/tests/sidebar-right.e2e.ts | 2 +- docs/subsystems/sidebar-right.i18n.yaml | 4 +-- docs/subsystems/sidebar-right.md | 4 +-- docs/subsystems/sidebar-right.zh.md | 4 +-- .../src/client/chat/TurnUsagePanel.module.css | 12 --------- .../src/client/chat/TurnUsagePanel.tsx | 17 +------------ .../tests/turn-usage-panel.client.spec.tsx | 25 +++++-------------- .../ui-primitives/src/markdown/CodeBlock.tsx | 1 + .../src/client/TextPreview.tsx | 4 +-- .../tests/text-preview.client.spec.tsx | 1 + .../client/ui-sidebar-files/README.i18n.yaml | 4 +-- .../tests/definition.client.spec.ts | 11 ++++++-- .../client/ui-sidebar-right/README.i18n.yaml | 4 +-- .../client/tabs/guide/GuideBody.module.css | 7 +++--- 26 files changed, 63 insertions(+), 101 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-09-05-sidebar-tab-types-and-navigation.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-05-sidebar-tab-types-and-navigation.i18n.yaml index f7a6978cca..d5073475ba 100644 --- a/.agents/notes/implemented/architecture/2026-09-05-sidebar-tab-types-and-navigation.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-05-sidebar-tab-types-and-navigation.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-05-sidebar-tab-types-and-navigation.md -2026-09-05-sidebar-tab-types-and-navigation.md: a79292eba25828287ce43ad16b7eb917dcdddcb4 -2026-09-05-sidebar-tab-types-and-navigation.zh.md: bf661948036257366714617b192525ac35b0905f +2026-09-05-sidebar-tab-types-and-navigation.md: 24332c844d13c41e4f966f39aa7d2fe79fd8553f +2026-09-05-sidebar-tab-types-and-navigation.zh.md: 3aeb38e2ec9c997b9aaf040ae0cb575fa08fa235 diff --git a/.agents/notes/implemented/architecture/2026-09-05-sidebar-tab-types-and-navigation.md b/.agents/notes/implemented/architecture/2026-09-05-sidebar-tab-types-and-navigation.md index a79292eba2..24332c844d 100644 --- a/.agents/notes/implemented/architecture/2026-09-05-sidebar-tab-types-and-navigation.md +++ b/.agents/notes/implemented/architecture/2026-09-05-sidebar-tab-types-and-navigation.md @@ -36,7 +36,7 @@ interface SidebarRightTabDefinition { `priority` is one of three literal bands, spelled as strings so that a type from another package needs no runtime import: `extension` is the band of a type from outside the product and the highest, so a type that declares nothing outranks every viewer shipped here; `builtin` is the ordinary band for shipped types; `fallback` is the plain-content position that anything more specific should beat, which VS Code's text editor holds implicitly and our text preview holds explicitly. `candidates(address)` returns every type whose globs match and whose `canOpen` does not veto, ranked by band, then by the length of the longest pattern that matched, then by registration order. `claim(address, kind?)` takes the best candidate, or the named kind's type in force when the caller overrides (its globs are not consulted; naming the type is the decision), and throws for an address nothing will open — a wiring mistake, not a user error. `get(kind)` returns the type in force; `entries()` and `guide()` list the types and their guide boxes in force; `subscribe` observes changes. -`title(address)` and `guide[].title()` are thunks read on every use, so a language change needs no re-registration. The registry itself is a plain object provided at `apply`'s top level **without** `Service.tracker`: a tracker would rebind `this.ctx` to the caller's context, and a cross-package `register()` would then add its effect to the caller's fiber while that fiber is the active scope, stalling the browser boot with no error. +`title(address)`, `guide[].title()`, and optional `guide[].description()` are thunks read on every use, so a language change needs no re-registration. [Guide start page and stat pill refinements](../feature/2026-09-10-guide-start-page-and-stat-pill-refinements.md) owns the current description visibility and fallback-glyph rules. The registry itself is a plain object provided at `apply`'s top level **without** `Service.tracker`: a tracker would rebind `this.ctx` to the caller's context, and a cross-package `register()` would then add its effect to the caller's fiber while that fiber is the active scope, stalling the browser boot with no error. ### Bodies and titles: keyed Slot seats under the definition's `id` diff --git a/.agents/notes/implemented/architecture/2026-09-05-sidebar-tab-types-and-navigation.zh.md b/.agents/notes/implemented/architecture/2026-09-05-sidebar-tab-types-and-navigation.zh.md index bf66194803..3aeb38e2ec 100644 --- a/.agents/notes/implemented/architecture/2026-09-05-sidebar-tab-types-and-navigation.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-05-sidebar-tab-types-and-navigation.zh.md @@ -36,7 +36,7 @@ interface SidebarRightTabDefinition { `priority` 是三个字面量档位之一,写成字符串,好让别的包的类型不需要任何运行时引入:`extension` 是来自产品之外的类型的档位也是最高档,所以什么都不声明的类型压过这里随包交付的每个查看器;`builtin` 是随包类型的常规档;`fallback` 是任何更具体的东西都应压过的纯内容位置,VS Code 的文本编辑器隐含地占据它,我们的文本预览明确地占据它。`candidates(address)` 返回 glob 命中且 `canOpen` 未否决的每个类型,按档位、再按命中的最长 pattern 长度、再按注册顺序排序。`claim(address, kind?)` 取最佳候选,或在调用方指定时取该 kind 生效的类型(不查它的 glob;点名即决定),对无人愿开的地址抛错——这是接线错误,不是用户错误。`get(kind)` 返回生效类型;`entries()` 与 `guide()` 列出生效类型及其引导入口;`subscribe` 观察变化。 -`title(address)` 与 `guide[].title()` 是每次使用时重读的 thunk,语言切换无需重新注册。注册表本身是 `apply` 顶层提供的普通对象,**不带** `Service.tracker`:tracker 会把 `this.ctx` 重绑到调用方上下文,跨包 `register()` 就会在调用方 fiber 仍是活动作用域时往它上加 effect,浏览器启动会无声卡死。 +`title(address)`、`guide[].title()` 与可选的 `guide[].description()` 都是每次使用时重读的 thunk,语言切换无需重新注册。[引导起始页与统计 pill 的细化](../feature/2026-09-10-guide-start-page-and-stat-pill-refinements.zh.md)负责当前的描述显示与兜底图标规则。注册表本身是 `apply` 顶层提供的普通对象,**不带** `Service.tracker`:tracker 会把 `this.ctx` 重绑到调用方上下文,跨包 `register()` 就会在调用方 fiber 仍是活动作用域时往它上加 effect,浏览器启动会无声卡死。 ### 正文与标题:按定义 `id` keyed 的 Slot 坑位 diff --git a/.agents/notes/implemented/feature/2026-09-05-sidebar-text-preview-and-file-tree.i18n.yaml b/.agents/notes/implemented/feature/2026-09-05-sidebar-text-preview-and-file-tree.i18n.yaml index 5c04c6fdb7..a2f18c11cb 100644 --- a/.agents/notes/implemented/feature/2026-09-05-sidebar-text-preview-and-file-tree.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-09-05-sidebar-text-preview-and-file-tree.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-05-sidebar-text-preview-and-file-tree.md -2026-09-05-sidebar-text-preview-and-file-tree.md: e25e1c55f5434f2b3b1b2703f886cf3c1e9fe80c -2026-09-05-sidebar-text-preview-and-file-tree.zh.md: f313d30fa68f9d8afba84a470e5e80902b13d1e3 +2026-09-05-sidebar-text-preview-and-file-tree.md: 0142b325004a1fc018e944ed908d600e75f0ab0d +2026-09-05-sidebar-text-preview-and-file-tree.zh.md: d40ca8c790c9a7208a2243422c0973fc293a5915 diff --git a/.agents/notes/implemented/feature/2026-09-05-sidebar-text-preview-and-file-tree.md b/.agents/notes/implemented/feature/2026-09-05-sidebar-text-preview-and-file-tree.md index e25e1c55f5..0142b32500 100644 --- a/.agents/notes/implemented/feature/2026-09-05-sidebar-text-preview-and-file-tree.md +++ b/.agents/notes/implemented/feature/2026-09-05-sidebar-text-preview-and-file-tree.md @@ -20,7 +20,7 @@ Three tab types ship with the Sidebar: the **guide** (`ui-sidebar-right`), the * The guide is what a pane shows before it holds content. Its registration is `{ id: '@deepseek-ai/dsh-client-ui-sidebar-right/guide', kind: 'guide', priority: 'builtin', title }` with no `patterns`: a guide views nothing, so it is opened by kind through `openTab` and recorded under the page address `sidebar://guide`, which is the registry's bookkeeping and never composed by a caller. The tab's title is `开始` / `Start`, captured into the layout record when the pane is seeded, so a later language change relabels the type and not tabs already open. -The body is a centred column — a lead line (`侧栏用来放你想一直看着的东西。` / `The sidebar holds what you want to keep looking at.`), one line of copy (`会话里的文件和产物会开在这一栏,也可以从下面的入口打开。` / `Files and artifacts from the conversation open in this column; the entries below open more.`), and a grid of entry boxes at most 480px wide, each box at least 160px, filling as many columns as fit. The boxes are projected from every registered type's `guide[]` in `order`, through the registry's observable `guide()` list, so a type registering later appears without the guide knowing it. A box shows the contributing type's glyph, title, and description, and picking it calls `tabActions.openTab(entry.kind, { replaceTab: true })`: the picked type opens in the guide's own tab, and the guide is gone. The guide is a doorway, not a page that stays open beside what it opened. +The body projects every registered type's `guide[]` in `order` through the registry's observable `guide()` list, so a type registering later appears without the guide knowing it. [Guide start page and stat pill refinements](2026-09-10-guide-start-page-and-stat-pill-refinements.md) owns the compass, optional descriptions, fallback glyph, and current capsule layout. Picking a capsule calls `tabActions.openTab(entry.kind, { replaceTab: true })`: the picked type opens in the guide's own tab, and the guide is gone. The guide is a doorway, not a page that stays open beside what it opened. The body is also the replacement seam. It renders the `sidebar.right.tab.guide` chain with the shipped guide as the chain's fallback, so a product that registers its own entry takes the whole body, and with no entry, or every entry declining, the shipped guide draws. Because the shipped guide is the fallback and not a chain entry, there is always exactly one body and it cannot be outvoted by accident. @@ -42,13 +42,13 @@ Navigation is a `line`. The `read` tool row passes its 1-based `offset` as `open A changed file is announced, not applied. The body compares the loaded version and the observation captured at read start with later `WorkspaceFileStat.version`; a difference shows the change bar. Reload rereads only this tab through the Preview face and does not mutate shared resource metadata or another tab. A resource failure takes the same bar's place above any content already loaded. -The body's header is one row: the full file path on the left and the matching-renderer menu, conditional wrap toggle, and reload button on the right. The [Document Preview README](../../../../packages/client/ui-sidebar-documentpreview/README.md) owns the current controls and renderer behavior. The preview takes the pane body's full height, and its document body is the scroller beneath the fixed header and change bar. +The body's header is one row: the full file path on the left and the matching-renderer menu, conditional wrap toggle, and reload button on the right. The [Document Preview README](../../../../packages/client/ui-sidebar-documentpreview/README.md) owns the current controls, renderer behavior, and scrolling. The preview takes the pane body's full height. A failed read keeps content already shown and adds a localized failure with a retry action. The Preview names actionable file failures and falls back to the carrier message for other codes; `outside-workspace` belongs to directory listing and is not a Preview-specific failure. ### The file tree -`files` is a page type, not a viewer: it claims no address. Its registration is `{ kind: 'files', id: '@deepseek-ai/dsh-client-ui-sidebar-files', priority: 'builtin', title, guide: [{ order: 10, title, description, icon: IconFolderClose16 }] }` — no `patterns`, because nothing navigates *to* a file tree by address; the guide's entry box opens the type itself. `id` is the implementation's identity in the Tab system and doubles as the `key` of the body seat `sidebar.right.pane.tab`, so the same string names the type and the component that draws it. `register()` returns a disposer and goes through `ctx.effect`, as every registration does. +`files` is a page type, not a viewer: it claims no address. Its registration is `{ kind: 'files', id: '@deepseek-ai/dsh-client-ui-sidebar-files', priority: 'builtin', title, guide: [{ order: 10, title, description, icon: FolderSheetGlyph }] }` — no `patterns`, because nothing navigates *to* a file tree by address; the guide's entry box opens the type itself. `FolderSheetGlyph` adapts the shared coloured folder sheet to the guide's requested glyph size; the [guide refinement](2026-09-10-guide-start-page-and-stat-pill-refinements.md) owns that presentation choice. `id` is the implementation's identity in the Tab system and doubles as the `key` of the body seat `sidebar.right.pane.tab`, so the same string names the type and the component that draws it. `register()` returns a disposer and goes through `ctx.effect`, as every registration does. The root is the session's working directory as the Host reports it in the session list (`useSessions().byId[sessionId].cwd`), labelled by `workspaceTitleOf` from `dsh-util-workspace-path` — the final non-empty path segment — with the root string itself as the label when the path is separator-only. A session without a working directory shows one line (`noWorkspace`) and issues no request. There is no root chooser and no way to browse upward: the Host's `list` refuses paths outside the Session's workspace root, so the one directory the client can list is the one it shows. diff --git a/.agents/notes/implemented/feature/2026-09-05-sidebar-text-preview-and-file-tree.zh.md b/.agents/notes/implemented/feature/2026-09-05-sidebar-text-preview-and-file-tree.zh.md index f313d30fa6..d40ca8c790 100644 --- a/.agents/notes/implemented/feature/2026-09-05-sidebar-text-preview-and-file-tree.zh.md +++ b/.agents/notes/implemented/feature/2026-09-05-sidebar-text-preview-and-file-tree.zh.md @@ -20,7 +20,7 @@ Sidebar 随包交付三个 tab 类型:**引导页**(`ui-sidebar-right`)、 引导页是 pane 承载内容之前显示的东西。它的注册定义是 `{ id: '@deepseek-ai/dsh-client-ui-sidebar-right/guide', kind: 'guide', priority: 'builtin', title }`,没有 `patterns`:引导页不查看任何东西,所以经 `openTab` 按 kind 打开,并记在页地址 `sidebar://guide` 之下——那是注册表自己的记账,调用方从不拼它。tab 标题是 `开始` / `Start`,在 pane 播种时捕获进布局记录,于是之后切换语言只重标类型,不改已开着的 tab。 -体是一根居中的列——一句引导语(`侧栏用来放你想一直看着的东西。` / `The sidebar holds what you want to keep looking at.`)、一行文案(`会话里的文件和产物会开在这一栏,也可以从下面的入口打开。` / `Files and artifacts from the conversation open in this column; the entries below open more.`),以及一组最宽 480px 的入口框栅格,每框至少 160px,能放几列放几列。入口框按 `order` 从每个已注册类型的 `guide[]` 投影而来,经注册表可观察的 `guide()` 列表,因此后注册的类型不用引导页知道就能出现。一个框显示贡献类型的图标、标题与说明;点选它调用 `tabActions.openTab(entry.kind, { replaceTab: true })`:被选的类型在引导页自己的 tab 里打开,引导页随之消失。引导页是一扇门,不是留在被打开者旁边的一页。 +正文按 `order` 从每个已注册类型的 `guide[]` 投影入口,并通过注册表可观察的 `guide()` 列表更新,因此后注册的类型无需引导页感知即可出现。[引导起始页与统计 pill 的细化](2026-09-10-guide-start-page-and-stat-pill-refinements.zh.md)负责罗盘、可选描述、兜底图标和当前胶囊布局。点选胶囊会调用 `tabActions.openTab(entry.kind, { replaceTab: true })`:被选的类型在引导页自己的 tab 里打开,引导页随之消失。引导页是一扇门,不是留在被打开者旁边的一页。 体同时也是替换接缝。它渲染 `sidebar.right.tab.guide` 链,并以随包交付的引导页作为链的 fallback,于是注册了自己入口的产品接管整个体,而没有入口、或每个入口都拒绝时,随包交付的引导页照常绘制。因为随包交付的引导页是 fallback 而不是链上的一个入口,所以永远恰有一个体,也不可能被意外投掉。 @@ -42,13 +42,13 @@ store 是 Slot 标准件:每会话一个独占实例,按 tab id 分桶,持 文件变了只提示,不应用。正文把已加载版本及读取开始时捕获的观察版本与后续 `WorkspaceFileStat.version` 比较;不同则显示变更提示。重新载入只通过 Preview face 重读当前 tab,不修改共享资源元数据或其他 tab。资源失败占用同一个提示位置,已加载内容仍保留在下方。 -正文头部为一行:左侧显示完整文件路径,右侧放匹配渲染器菜单、按条件出现的换行开关和重新载入按钮。[Document Preview README](../../../../packages/client/ui-sidebar-documentpreview/README.zh.md)负责当前控件与渲染器行为。预览占满 pane 正文的全部高度,其文档正文是固定头部与变更提示条下方的滚动区域。 +正文头部为一行:左侧显示完整文件路径,右侧放匹配渲染器菜单、按条件出现的换行开关和重新载入按钮。[Document Preview README](../../../../packages/client/ui-sidebar-documentpreview/README.zh.md)负责当前控件、渲染器行为与滚动方式。预览占满 pane 正文的全部高度。 读取失败时保留已显示的内容,并增加本地化失败说明与重试操作。Preview 为可处理的文件错误提供专用文案,其他代码使用载体消息兜底;`outside-workspace` 属于目录列举,不是 Preview 专用失败。 ### 文件树 -`files` 是页类型,不是查看器:它不认领任何地址。注册定义是 `{ kind: 'files', id: '@deepseek-ai/dsh-client-ui-sidebar-files', priority: 'builtin', title, guide: [{ order: 10, title, description, icon: IconFolderClose16 }] }`——没有 `patterns`,因为没有谁按地址导航*到*一棵文件树;引导页的入口框打开的是类型本身。`id` 是这个实现在 Tab 系统里的唯一键,同时也是体坑位 `sidebar.right.pane.tab` 的 `key`,于是同一个串既命名类型也命名画它的组件。`register()` 返回 disposer 并经 `ctx.effect` 注册,与所有注册一致。 +`files` 是页类型,不是查看器:它不认领任何地址。注册定义是 `{ kind: 'files', id: '@deepseek-ai/dsh-client-ui-sidebar-files', priority: 'builtin', title, guide: [{ order: 10, title, description, icon: FolderSheetGlyph }] }`——没有 `patterns`,因为没有谁按地址导航*到*一棵文件树;引导页的入口框打开的是类型本身。`FolderSheetGlyph` 把共享的彩色文件夹页适配到引导页请求的图标尺寸;[引导页细化记录](2026-09-10-guide-start-page-and-stat-pill-refinements.zh.md)负责这项展示选择。`id` 是这个实现在 Tab 系统里的唯一键,同时也是体坑位 `sidebar.right.pane.tab` 的 `key`,于是同一个串既命名类型也命名画它的组件。`register()` 返回 disposer 并经 `ctx.effect` 注册,与所有注册一致。 根是 Host 在会话列表里上报的会话工作目录(`useSessions().byId[sessionId].cwd`),标签由 `dsh-util-workspace-path` 的 `workspaceTitleOf` 给出——路径最后一个非空段——路径只有分隔符时用根串本身作标签。没有工作目录的会话只显示一行(`noWorkspace`),不发请求。没有根选择器,也不能往上浏览:Host 的 `list` 拒绝会话工作区根之外的路径,所以客户端能列的那一个目录就是它显示的目录。 diff --git a/.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.i18n.yaml b/.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.i18n.yaml index 3fae385422..6be2091ff6 100644 --- a/.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.md -2026-09-07-composer-session-stats-pills.md: 818c3c156f72816eb6540d505782be8e03dc6aa0 -2026-09-07-composer-session-stats-pills.zh.md: 8a74cddd3176026d799fb5b4223a51371ee8e253 +2026-09-07-composer-session-stats-pills.md: 541e5a363d78c4333b1b8e3927928a19754822fa +2026-09-07-composer-session-stats-pills.zh.md: b6ef6da3c37a46d9e07e7ebdf77439e26896fddc diff --git a/.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.md b/.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.md index 818c3c156f..541e5a363d 100644 --- a/.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.md +++ b/.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.md @@ -12,7 +12,7 @@ The session stats strip under the composer (`StatsLine`, ui-chat, mounted on `co `StatsPills` (packages/client/ui-chat/src/client/chat/StatsPills.tsx) replaces `StatsLine` on the same `conversation.composer.dock` slot; the losing variant is deleted, its shared helpers (`deriveStats`, `formatDuration`, `cacheHitPercent`, `billedInputTokens`) absorbed into the new module, and the dead `stats.llm`, `stats.toolCall`, `stats.ttftAverage`, `stats.tokensPerSecond`, and `stats.tokens` locale keys removed. -- **Two icon pills, two dialogs.** A gauge pill (new `IconGaugeOutline16`, dial center optically dropped to y=8.75 because the bottom-open arc reads high) shows `{turns} 轮 {steps} 步` plus output TPS and click-opens the 会话统计 dialog (LLM time, tool time, average TTFT, TPS); a log with no timed figure would open an empty dialog, so that pill renders as a static reading instead of a button. A database pill (`IconDatabaseOutline16`) shows the compact billed total plus cache-hit share and click-opens the Token 用量 dialog (cache hit, uncached input, cache read, cache write, output — exact counts). Both dialogs wear the shared `stat-dialog` module (portal panel, anchored placement, outside-dismiss, optionally externally owned open state) extracted for exactly this two-consumer split; the pills row owns one exclusive open slot, so opening either dialog closes the other, and each button carries an explicit `aria-label` that separates with ` · ` the segments the aria-hidden sep glyph joins visually. +- **Two icon pills, two dialogs.** A gauge pill (new `IconGaugeOutline16`, dial center optically dropped to y=8.75 because the bottom-open arc reads high) shows `{turns} 轮 {steps} 步` plus output TPS and click-opens the 会话统计 dialog (LLM time, tool time, average TTFT, TPS); a log with no timed figure would open an empty dialog, so that pill renders as a static reading instead of a button. A database pill (`IconDatabaseOutline16`) shows the compact billed total plus cache-hit share and click-opens the Token 用量 dialog (cache hit, uncached input, cache read, output, and cache write when non-zero — exact counts). Both dialogs wear the shared `stat-dialog` module (portal panel, anchored placement, outside-dismiss, optionally externally owned open state) extracted for exactly this two-consumer split; the pills row owns one exclusive open slot, so opening either dialog closes the other, and each button carries an explicit `aria-label` that separates with ` · ` the segments the aria-hidden sep glyph joins visually. [Guide start page and stat pill refinements](2026-09-10-guide-start-page-and-stat-pill-refinements.md) owns the zero cache-write omission. - **Data sourcing is unchanged in architecture.** Counts and times prefer the durable `sessionStats` projection with the window fold as the assembly-without-the-unit fallback ([whole-session counts](../../archived/bug-fix/2026-08-12-full-session-turn-step-counts.md)); token figures ride `tokenUsage` only, so an absent projection drops the usage pill rather than showing window-derived billing. Cache writes stay in the billed total and the cache-hit denominator ([projection decision](../architecture/2026-07-29-projected-token-usage-and-request-context.md)). Context occupancy stays on the composer's ContextMeter ring, where it already lived beside `StatsLine` — the strip never carried it. - **Render discipline.** The row folds settled nodes only (`chat.legacy.nodes` identity), so streaming chunk frames cause zero rerenders — pinned by a render-count unit test. A session with no closed step and no billed tokens renders nothing. - **`data-composer-stats` is a cross-package attribute contract.** The pills' root carries it; ui-conversation's `InputBar.module.css` `:has([data-composer-stats])` rule tightens the composer's bottom clearance to 4px when the row is mounted. The producer side pins the attribute in unit tests, following the `data-trigger-menu` precedent. @@ -21,7 +21,7 @@ The session stats strip under the composer (`StatsLine`, ui-chat, mounted on `co - **The single-line variant (StatsLine, the A/B loser).** All figures resident in one text row, with a hover tooltip restating the full line when it truncated. Lost on crowding and reach: exact token counts appeared nowhere (the line and its tooltip both carried compact totals only), and one row gave time and billing figures no visual grouping. - **Three resident groups with one shared dialog.** An intermediate iteration kept counts, time, and tokens as three inline groups. Two pills won because the time/usage split matches the two underlying projections one-to-one and each pill's icon telegraphs its dialog. -- **Extracting the dl bucket rows shared with `TurnUsagePanel`.** The session-total dialog and the per-turn panel render the same skin but different contracts (all buckets always present vs optional per-turn fields plus model routes); a shared component would be conditionals around nine lines. The mirror is marked `jscpd:ignore` with the reason inline. +- **Extracting the dl bucket rows shared with `TurnUsagePanel`.** The session-total dialog and the per-turn panel render the same skin but different contracts (required session input, cache-read, and output rows plus a non-zero cache-write row, versus optional per-turn fields and model routes); a shared component would be conditionals around nine lines. The mirror is marked `jscpd:ignore` with the reason inline. ## Consequences diff --git a/.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.zh.md b/.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.zh.md index 8a74cddd31..b6ef6da3c3 100644 --- a/.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.zh.md +++ b/.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.zh.md @@ -12,7 +12,7 @@ Status: implemented `StatsPills`(packages/client/ui-chat/src/client/chat/StatsPills.tsx)在同一 `conversation.composer.dock` 插槽上取代 `StatsLine`;落选变体已删除,其共享工具函数(`deriveStats`、`formatDuration`、`cacheHitPercent`、`billedInputTokens`)并入新模块,废弃的 `stats.llm`、`stats.toolCall`、`stats.ttftAverage`、`stats.tokensPerSecond`、`stats.tokens` 文案键一并移除。 -- **两个图标 pill、两个弹层。** 仪表盘 pill(新增 `IconGaugeOutline16`,因下开口圆弧视觉偏高而把表盘中心光学下移到 y=8.75)展示 `{turns} 轮 {steps} 步` 加输出 TPS,点击打开「会话统计」弹层(模型用时、工具调用用时、首 token 平均、输出速度);日志里没有任何计时数字时弹层会是空的,此时该 pill 渲染为静态读数而非按钮。数据库 pill(`IconDatabaseOutline16`)展示紧凑计费总量加缓存命中率,点击打开「Token 用量」弹层(缓存命中、未缓存输入、缓存读取、缓存写入、输出——精确计数)。两个弹层共用为这两处消费者抽出的 `stat-dialog` 模块(portal 面板、锚定定位、点击外部关闭、可选的外部持有开合状态);pill 行持有唯一的互斥开合槽位,打开任一弹层即关闭另一个,且每个按钮携带显式 `aria-label`,用 ` · ` 分隔 aria-hidden 分隔符在视觉上连接的两段文本。 +- **两个图标 pill、两个弹层。** 仪表盘 pill(新增 `IconGaugeOutline16`,因下开口圆弧视觉偏高而把表盘中心光学下移到 y=8.75)展示 `{turns} 轮 {steps} 步` 加输出 TPS,点击打开「会话统计」弹层(模型用时、工具调用用时、首 token 平均、输出速度);日志里没有任何计时数字时弹层会是空的,此时该 pill 渲染为静态读数而非按钮。数据库 pill(`IconDatabaseOutline16`)展示紧凑计费总量加缓存命中率,点击打开「Token 用量」弹层(缓存命中、未缓存输入、缓存读取、输出,以及非零时的缓存写入——精确计数)。两个弹层共用为这两处消费者抽出的 `stat-dialog` 模块(portal 面板、锚定定位、点击外部关闭、可选的外部持有开合状态);pill 行持有唯一的互斥开合槽位,打开任一弹层即关闭另一个,且每个按钮携带显式 `aria-label`,用 ` · ` 分隔 aria-hidden 分隔符在视觉上连接的两段文本。[引导起始页与统计 pill 的细化](2026-09-10-guide-start-page-and-stat-pill-refinements.zh.md)负责缓存写入为零时省略该行的规则。 - **数据来源架构不变。** 计数与用时优先读取持久的 `sessionStats` 投影,窗口折叠仅作无该单元装配时的回退([全会话计数](../../archived/bug-fix/2026-08-12-full-session-turn-step-counts.md));token 数字只走 `tokenUsage`,投影缺席时直接不渲染用量 pill,而非展示窗口推算的计费。缓存写入仍计入计费总量与缓存命中分母([投影决定](../architecture/2026-07-29-projected-token-usage-and-request-context.zh.md))。上下文占用仍在输入框旁的 ContextMeter 圆环上,`StatsLine` 时代它就在那里——统计条从未承载过它。 - **渲染纪律。** 该行只折叠已定稿节点(`chat.legacy.nodes` 身份),流式 chunk 帧零重渲染——由渲染计数单测钉住。无已完成步且无计费 token 的会话什么都不渲染。 - **`data-composer-stats` 是跨包属性契约。** pill 行根元素携带它;ui-conversation 的 `InputBar.module.css` 用 `:has([data-composer-stats])` 在该行挂载时把输入框底部留白收紧到 4px。生产方在单测里钉住该属性,沿用 `data-trigger-menu` 先例。 @@ -21,7 +21,7 @@ Status: implemented - **单行变体(StatsLine,A/B 落选方)。** 全部数字常驻一行文本,悬停提示只在截断时复述整行。败在拥挤与可达性:精确 token 计数无处可看(行内和提示里都只有紧凑总量),单行也无法给时间与计费数字分组。 - **三个常驻分组共享一个弹层。** 中间迭代曾把计数、时间、token 保持为三个内联分组。双 pill 胜出是因为时间/用量的切分与两个底层投影一一对应,且每个 pill 的图标直接预告其弹层内容。 -- **抽出与 `TurnUsagePanel` 共享的 dl 分桶行。** 会话总量弹层与逐轮面板皮肤相同但契约不同(此处所有分桶恒在;逐轮字段可选且含模型路由);共享组件只是给九行代码包一层条件。该镜像以 `jscpd:ignore` 标注并内联说明理由。 +- **抽出与 `TurnUsagePanel` 共享的 dl 分桶行。** 会话总量弹层与逐轮面板皮肤相同但约定不同(会话输入、缓存读取与输出行始终存在,缓存写入行仅在非零时出现;逐轮字段可选且含模型路由);共享组件只是给九行代码包一层条件。该镜像以 `jscpd:ignore` 标注并内联说明理由。 ## 后果 diff --git a/.agents/notes/implemented/feature/2026-09-10-guide-start-page-and-stat-pill-refinements.i18n.yaml b/.agents/notes/implemented/feature/2026-09-10-guide-start-page-and-stat-pill-refinements.i18n.yaml index d904aac0aa..dd2b16d82c 100644 --- a/.agents/notes/implemented/feature/2026-09-10-guide-start-page-and-stat-pill-refinements.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-09-10-guide-start-page-and-stat-pill-refinements.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-10-guide-start-page-and-stat-pill-refinements.md -2026-09-10-guide-start-page-and-stat-pill-refinements.md: 1d537479c13b44c57bbc9ba7a0dbe907b2c4c582 -2026-09-10-guide-start-page-and-stat-pill-refinements.zh.md: a4c261bb90ff36f1818d07762646cba3516f5316 +2026-09-10-guide-start-page-and-stat-pill-refinements.md: 987e46fd597b11046843a943c60187fdb1a769d0 +2026-09-10-guide-start-page-and-stat-pill-refinements.zh.md: 590f18a2963507a8918a0aca30997aac23c6e477 diff --git a/.agents/notes/implemented/feature/2026-09-10-guide-start-page-and-stat-pill-refinements.md b/.agents/notes/implemented/feature/2026-09-10-guide-start-page-and-stat-pill-refinements.md index 1d537479c1..987e46fd59 100644 --- a/.agents/notes/implemented/feature/2026-09-10-guide-start-page-and-stat-pill-refinements.md +++ b/.agents/notes/implemented/feature/2026-09-10-guide-start-page-and-stat-pill-refinements.md @@ -6,17 +6,15 @@ English | [中文](2026-09-10-guide-start-page-and-stat-pill-refinements.zh.md) ## Problem -The right Sidebar's guide tab was a bare list of entry capsules: no visual anchor above them, a capsule could only say its title, and an entry whose type registered no glyph rendered with no icon at all, so a mixed list read as broken rather than sparse. Separately, two chat stat surfaces showed noise: the session token-usage dialog under the composer printed a `Cache write 0 tok` row for sessions that never wrote cache, and the per-turn time pill always opened a dialog even when that dialog held a single row repeating the pill's own label — same formatter, same value — so the click bought nothing. +The right Sidebar's guide tab was a bare list of entry capsules: no visual anchor above them, a capsule could only say its title, and an entry whose type registered no glyph rendered with no icon at all, so a mixed list read as broken rather than sparse. Separately, the session token-usage dialog under the composer printed a `Cache write 0 tok` row for sessions that never wrote cache. ## Decision -**The guide is a compass over self-describing capsules.** [GuideBody.tsx](../../../../packages/client/ui-sidebar-right/src/client/tabs/guide/GuideBody.tsx) draws a muted 56px compass hero over the entry capsules and no heading, as a browser start page shows its doors without a caption. [`SidebarRightGuideEntry`](../../../../packages/client/ui-sidebar-right/src/client/tab-registry.ts) gains an optional thunked `description` — read fresh on every render like `title`, so language changes need no re-registration. A capsule shows its description under the title only while the guide lists at most `MAX_DESCRIBED_ENTRIES` (4) entries; a longer list drops every description to stay light, so a type must stand on its title. The glyph rides the capsule's height: 22px beside a bare title, 26px beside two lines. +**The guide is a compass over self-describing capsules.** This refines the guide contracts in [Right Sidebar tab types and navigation](../architecture/2026-09-05-sidebar-tab-types-and-navigation.md) and [Sidebar text preview and file tree](2026-09-05-sidebar-text-preview-and-file-tree.md). [GuideBody.tsx](../../../../packages/client/ui-sidebar-right/src/client/tabs/guide/GuideBody.tsx) draws a muted 56px compass hero over the entry capsules and no heading, as a browser start page shows its doors without a caption. [`SidebarRightGuideEntry`](../../../../packages/client/ui-sidebar-right/src/client/tab-registry.ts) gains an optional thunked `description` — read fresh on every render like `title`, so language changes need no re-registration. A capsule shows its description under the title only while the guide lists at most `MAX_DESCRIBED_ENTRIES` (4) entries; a longer list drops every description to stay light, so a type must stand on its title. The glyph rides the capsule's height: 22px beside a bare title, 26px beside two lines. **Icon-less entries fall back to a shipped cube placeholder.** The fallback is decided at the render site (`entry.icon ?? CubeGlyph`), not at registration, so every contributor — builtin or extension — gets it uniformly and a chain replacement of the body replaces the rule with it. `CubeGlyph` lives beside `CompassGlyph` in [GuideTitle.tsx](../../../../packages/client/ui-sidebar-right/src/client/tabs/guide/GuideTitle.tsx): an isometric box in 1.1px straight strokes with rounded joins on `currentColor`, drawn on `--dsw-alias-label-tertiary` — one step quieter than a registered glyph's ink — to mark the slot as unclaimed. The files type registers a description and the shared folder glyph in [definition.tsx](../../../../packages/client/ui-sidebar-files/src/client/definition.tsx). -**The session usage dialog drops the zero cache-write row.** [StatsPills.tsx](../../../../packages/client/ui-chat/src/client/chat/StatsPills.tsx) renders the `Cache write` row only when `cacheWriteTokens !== 0`, as the per-turn panel already drops its absent optional fields; the always-present buckets (input, cache read, output) keep their rows. - -**The turn-time pill goes static when its dialog would only repeat it.** [TurnUsagePanel.tsx](../../../../packages/client/ui-chat/src/client/chat/TurnUsagePanel.tsx)'s `TurnTimePanel` renders plain text — no hover, no dialog — exactly when both `tokensPerSecond` and `ttftMs` are undefined, because the dialog's only unconditional row is the duration the pill already shows through the same formatter. With either figure recorded the pill stays a button. The static variant rides the `.trigger` class for geometry and a `.static` class quiets its hover and pointer, keeping the narrow-viewport icon collapse. +**The session usage dialog drops the zero cache-write row.** This refines [Composer session stats](2026-09-07-composer-session-stats-pills.md). [StatsPills.tsx](../../../../packages/client/ui-chat/src/client/chat/StatsPills.tsx) renders the `Cache write` row only when `cacheWriteTokens !== 0`, as the per-turn panel already drops its absent optional fields; the always-present buckets (input, cache read, output) keep their rows. ## Alternatives considered @@ -26,8 +24,6 @@ The right Sidebar's guide tab was a bare list of entry capsules: no visual ancho **Show `Cache write 0`.** A session on a provider that never writes cache would carry the row forever; zero here means "not a thing", not a measurement. -**Keep the one-row time dialog.** A dialog whose whole content duplicates its trigger label teaches users that the affordance is empty; hiding the affordance is the honest signal. - ## Consequences -`description` is new pre-stable registry API; every consumer was updated (the files entry registers one). The 4-entry threshold is a shipped constant of the guide body, not configuration. Guide-body specs cover the placeholder (size and ink), the description threshold on both sides, and the registered-glyph path; chat-stats specs cover the dropped and present cache-write row; turn-usage-panel specs cover the static pill and both one-figure dialogs. The `ui-sidebar-right` and `ui-sidebar-files` READMEs restate the guide rules. +`description` is new pre-stable registry API; every consumer was updated (the files entry registers one). The 4-entry threshold is a shipped constant of the guide body, not configuration. Guide-body specs cover the placeholder (size and ink), the description threshold on both sides, and the registered-glyph path; chat-stats specs cover the dropped and present cache-write row. The `ui-sidebar-right` and `ui-sidebar-files` READMEs restate the guide rules. diff --git a/.agents/notes/implemented/feature/2026-09-10-guide-start-page-and-stat-pill-refinements.zh.md b/.agents/notes/implemented/feature/2026-09-10-guide-start-page-and-stat-pill-refinements.zh.md index a4c261bb90..590f18a296 100644 --- a/.agents/notes/implemented/feature/2026-09-10-guide-start-page-and-stat-pill-refinements.zh.md +++ b/.agents/notes/implemented/feature/2026-09-10-guide-start-page-and-stat-pill-refinements.zh.md @@ -6,17 +6,15 @@ Status: implemented ## Problem -右侧边栏的引导 tab 只是一列光秃秃的入口胶囊:上方没有视觉锚点,胶囊只能显示标题,而类型没注册图标的入口干脆不画图标,混合列表看起来像坏了而不是稀疏。另外两个聊天统计面有噪音:输入框下方的会话 token 用量弹窗给从未写入缓存的会话打印一行 `Cache write 0 tok`;turn 级用时 pill 总是可以点开弹窗,即使弹窗只有一行、且内容重复 pill 自己的标签——同一格式化函数、同一数值——点击毫无所得。 +右侧边栏的引导 tab 只是一列光秃秃的入口胶囊:上方没有视觉锚点,胶囊只能显示标题,而类型没注册图标的入口干脆不画图标,混合列表看起来像坏了而不是稀疏。另外,输入框下方的会话 token 用量弹窗会给从未写入缓存的会话打印一行 `Cache write 0 tok`。 ## Decision -**引导页是罗盘压着能自我说明的胶囊。** [GuideBody.tsx](../../../../packages/client/ui-sidebar-right/src/client/tabs/guide/GuideBody.tsx) 在入口胶囊上方画一枚弱化的 56px 罗盘,不加标题,如同浏览器起始页不给它的入口配说明文字。[`SidebarRightGuideEntry`](../../../../packages/client/ui-sidebar-right/src/client/tab-registry.ts) 新增可选的 thunk 化 `description`——与 `title` 一样每次渲染重新读取,语言切换无需重新注册。仅当引导页列出的入口不超过 `MAX_DESCRIBED_ENTRIES`(4)个时,胶囊才在标题下显示描述;更长的列表去掉所有描述以保持轻盈,所以类型必须靠标题立得住。图标随胶囊高度变化:单行标题旁 22px,两行旁 26px。 +**引导页是罗盘压着能自我说明的胶囊。** 本决议细化[右侧 Sidebar tab 类型与导航](../architecture/2026-09-05-sidebar-tab-types-and-navigation.zh.md)和 [Sidebar 文本预览与文件树](2026-09-05-sidebar-text-preview-and-file-tree.zh.md)中的引导页约定。[GuideBody.tsx](../../../../packages/client/ui-sidebar-right/src/client/tabs/guide/GuideBody.tsx) 在入口胶囊上方画一枚弱化的 56px 罗盘,不加标题,如同浏览器起始页不给它的入口配说明文字。[`SidebarRightGuideEntry`](../../../../packages/client/ui-sidebar-right/src/client/tab-registry.ts) 新增可选的 thunk 化 `description`——与 `title` 一样每次渲染重新读取,语言切换无需重新注册。仅当引导页列出的入口不超过 `MAX_DESCRIBED_ENTRIES`(4)个时,胶囊才在标题下显示描述;更长的列表去掉所有描述以保持轻盈,所以类型必须靠标题立得住。图标随胶囊高度变化:单行标题旁 22px,两行旁 26px。 **没有图标的入口回退到内置的立方体占位符。** 回退在渲染点决定(`entry.icon ?? CubeGlyph`)而不在注册时,因此每个贡献者——内置或扩展——得到统一的占位符,链式替换 body 时规则随之整体替换。`CubeGlyph` 与 `CompassGlyph` 一起放在 [GuideTitle.tsx](../../../../packages/client/ui-sidebar-right/src/client/tabs/guide/GuideTitle.tsx):一只等距视角的盒子,1.1px 直线描边、圆角拼接、走 `currentColor`,用 `--dsw-alias-label-tertiary` 着色——比注册图标的墨色浅一档——标记这个槽位无人认领。files 类型在 [definition.tsx](../../../../packages/client/ui-sidebar-files/src/client/definition.tsx) 注册了描述和共享的文件夹图标。 -**会话用量弹窗去掉为零的缓存写入行。** [StatsPills.tsx](../../../../packages/client/ui-chat/src/client/chat/StatsPills.tsx) 仅当 `cacheWriteTokens !== 0` 时渲染 `Cache write` 行,与 per-turn 面板去掉缺失可选字段的做法一致;始终存在的桶(输入、缓存读取、输出)保留各自的行。 - -**turn 用时 pill 在弹窗只会重复自己时变为静态。** [TurnUsagePanel.tsx](../../../../packages/client/ui-chat/src/client/chat/TurnUsagePanel.tsx) 的 `TurnTimePanel` 恰好在 `tokensPerSecond` 与 `ttftMs` 都缺失时渲染纯文本——无 hover、无弹窗——因为弹窗唯一无条件的行就是 pill 已经用同一格式化函数展示的时长。两者任一有记录时 pill 仍是按钮。静态形态复用 `.trigger` 类保持几何尺寸,再用 `.static` 类压掉 hover 和指针,窄视口的图标折叠行为保持不变。 +**会话用量弹窗去掉为零的缓存写入行。** 本决议细化[输入框下的会话统计](2026-09-07-composer-session-stats-pills.zh.md)。[StatsPills.tsx](../../../../packages/client/ui-chat/src/client/chat/StatsPills.tsx) 仅当 `cacheWriteTokens !== 0` 时渲染 `Cache write` 行,与 per-turn 面板去掉缺失可选字段的做法一致;始终存在的桶(输入、缓存读取、输出)保留各自的行。 ## Alternatives considered @@ -26,8 +24,6 @@ Status: implemented **显示 `Cache write 0`。** 用不写缓存的 provider 的会话会永远挂着这一行;这里的零意味着"没有这回事",不是一次测量。 -**保留单行的用时弹窗。** 全部内容都在重复触发器标签的弹窗会教用户认为这个交互是空的;隐藏交互本身才是诚实的信号。 - ## Consequences -`description` 是新的 pre-stable 注册表 API;所有消费者已同步更新(files 入口注册了一个)。4 个入口的阈值是引导 body 的内置常量,不是配置。guide-body 用例覆盖占位符(尺寸与墨色)、描述阈值两侧和已注册图标路径;chat-stats 用例覆盖缓存写入行的去除与保留;turn-usage-panel 用例覆盖静态 pill 和两种单数据弹窗。`ui-sidebar-right` 与 `ui-sidebar-files` 的 README 重述了引导页规则。 +`description` 是新的 pre-stable 注册表 API;所有消费者已同步更新(files 入口注册了一个)。4 个入口的阈值是引导 body 的内置常量,不是配置。guide-body 用例覆盖占位符(尺寸与墨色)、描述阈值两侧和已注册图标路径;chat-stats 用例覆盖缓存写入行的去除与保留。`ui-sidebar-right` 与 `ui-sidebar-files` 的 README 重述了引导页规则。 diff --git a/apps/web/tests/sidebar-right.e2e.ts b/apps/web/tests/sidebar-right.e2e.ts index e0a64084f5..d1387af303 100644 --- a/apps/web/tests/sidebar-right.e2e.ts +++ b/apps/web/tests/sidebar-right.e2e.ts @@ -1061,7 +1061,7 @@ describe('web e2e: shipped right Sidebar', () => { expect(await width(column)).toBeGreaterThan(300) await expect.poll(async () => await tabTitles(column)).toEqual(['文件', '开始']) await expect.poll(async () => await guide.locator('[data-sidebar-right-guide-entry="files"]').innerText()) - .toBe('工作区文件') + .toBe('工作区文件\n浏览会话工作区的文件') await shot(zhPage, '05-guide-copy-zh') expect(zhTripwire.pageErrors).toEqual([]) diff --git a/docs/subsystems/sidebar-right.i18n.yaml b/docs/subsystems/sidebar-right.i18n.yaml index 4877bb7a0e..4c47c51e7d 100644 --- a/docs/subsystems/sidebar-right.i18n.yaml +++ b/docs/subsystems/sidebar-right.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/sidebar-right.md -sidebar-right.md: 807edc5873c97a256cb66923e776b0573af3186f -sidebar-right.zh.md: a56ebe819e8fbc63f5e6aa7697eb08e03b8d1c46 +sidebar-right.md: 3b6e5eb08fc2c0a0bac74369bde4a48b2ee0fdb6 +sidebar-right.zh.md: 680e2ae16f8f8aeeea57b11172be8c8c9e359122 diff --git a/docs/subsystems/sidebar-right.md b/docs/subsystems/sidebar-right.md index 807edc5873..3b6e5eb08f 100644 --- a/docs/subsystems/sidebar-right.md +++ b/docs/subsystems/sidebar-right.md @@ -43,7 +43,7 @@ Tab identity is the pair `(kind, address)`: the registry's claim uses the addres | `priority` | One of three literal bands: `extension` (the default and the highest: a type from outside the product outranks every shipped viewer), `builtin` (types shipped with the product), `fallback` (plain-content viewers anything more specific should beat). | | `canOpen(address)` | Optional synchronous veto of a glob match; it runs on every routing decision. | | `title(address)` | The chip's text, captured into the layout record when the tab opens and never rewritten. | -| `guide` | Optional entry boxes for the guide page: `{ order, title(), description(), icon? }`. Picking a box opens the contributing type as a page; omit to stay off the page. | +| `guide` | Optional entry boxes for the guide page: `{ order, title(), description?(), icon? }`. Picking a box opens the contributing type as a page; omit to stay off the page. | Routing is a ranked claim. `candidates(address)` ranks the types whose patterns match and whose `canOpen` does not veto: by band, then by the length of the longest matched pattern, then by registration order. `claim(address, kind?)` picks the first candidate, or the named `kind` outright — its globs are skipped, its `canOpen` still applies — and returns `{ kind, contentId: address, title }`. An address no type claims throws: it is a wiring mistake, not a user error. @@ -130,7 +130,7 @@ The Host `ctx.workspaceFiles` service and generated `workspaceFiles` Remote name ## Shipped types -- **`guide`** — `builtin`, opened as `openTab('guide')`. A centred title, one line, and one entry box per `guide` entry the registered types contributed, in `order`; picking a box opens the contributing type as a page in the guide tab's place. A pane holds at most one guide tab, and the strip's add control appears only while its pane has none. A new pane receives the registered default page: the sole guide entry directly, or the guide when the entry count is not one ([guide](../../packages/client/ui-sidebar-right/README.md#the-guide)). +- **`guide`** — `builtin`, opened as `openTab('guide')`. A muted compass sits above one capsule per contributed `guide` entry, in `order`; short lists show registered descriptions, and every missing icon uses the shipped placeholder. Picking a capsule opens the contributing type as a page in the guide tab's place. A pane holds at most one guide tab, and the strip's add control appears only while its pane has none. A new pane receives the registered default page: the sole guide entry directly, or the guide when the entry count is not one ([guide](../../packages/client/ui-sidebar-right/README.md#the-guide)). - **`text`** — `fallback`, `dsh-resource://file/**`, claiming Session addresses only. Document Preview observes metadata through `useResource<'file'>`, loads content through Remote callbacks, and owns renderer selection, the toolbar, per-tab refresh, scroll, and source navigation; unknown extensions render as plain text ([README](../../packages/client/ui-sidebar-documentpreview/README.md)). - **`files`** — `builtin`, opened as `openTab('files')`. The workspace directory tree, listed lazily through `list`, opening a file with `tab.actions.openResource(fileAddressFor(sessionId, root, path))` into its own pane ([README](../../packages/client/ui-sidebar-files/README.md)). diff --git a/docs/subsystems/sidebar-right.zh.md b/docs/subsystems/sidebar-right.zh.md index a56ebe819e..680e2ae16f 100644 --- a/docs/subsystems/sidebar-right.zh.md +++ b/docs/subsystems/sidebar-right.zh.md @@ -43,7 +43,7 @@ tab 身份是 `(kind, address)` 二元组:注册表的认领把地址原文用 | `priority` | 三档字面量之一:`extension`(缺省且最高:产品之外的类型压过所有内置查看器)、`builtin`(随产品发布的类型)、`fallback`(任何更具体的类型都应压过的纯内容查看器)。 | | `canOpen(address)` | 可选的同步否决,对 glob 命中生效;每次路由决策都会调用。 | | `title(address)` | chip 文本,在 tab 打开时捕获进布局记录,之后不再改写。 | -| `guide` | 可选的引导页入口框:`{ order, title(), description(), icon? }`。点一框即把贡献它的类型作为页面打开;省略即不上引导页。 | +| `guide` | 可选的引导页入口框:`{ order, title(), description?(), icon? }`。点一框即把贡献它的类型作为页面打开;省略即不上引导页。 | 路由是一次排序认领。`candidates(address)` 对模式命中且未被 `canOpen` 否决的类型排序:先按档,再按最长命中模式的长度,最后按注册顺序。`claim(address, kind?)` 取第一个候选,或直接用点名的 `kind`——跳过它的 glob,但 `canOpen` 仍生效——返回 `{ kind, contentId: address, title }`。没有任何类型认领的地址会抛错:这是接线错误,不是用户错误。 @@ -130,7 +130,7 @@ Host 的 `ctx.workspaceFiles` 服务与生成的 `workspaceFiles` Remote 命名 ## 内置类型 -- **`guide`**——`builtin`,以 `openTab('guide')` 打开。居中标题、一行说明,以及已注册类型贡献的每个 `guide` 入口一框、按 `order` 排列;点一框即在引导 tab 的位置把贡献它的类型作为页面打开。每个 pane 最多一个引导 tab,tab 条的新增控件只在本 pane 没有引导时出现。新 pane 使用已注册的默认页:只有一个引导入口时直接使用该入口,否则使用引导页([引导](../../packages/client/ui-sidebar-right/README.zh.md#the-guide))。 +- **`guide`**——`builtin`,以 `openTab('guide')` 打开。一枚弱化的罗盘位于各类型按 `order` 贡献的入口胶囊上方;入口较少时显示已注册的描述,未提供图标的入口统一使用内置占位符。点选胶囊即在引导 tab 的位置把贡献它的类型作为页面打开。每个 pane 最多一个引导 tab,tab 条的新增控件只在本 pane 没有引导时出现。新 pane 使用已注册的默认页:只有一个引导入口时直接使用该入口,否则使用引导页([引导](../../packages/client/ui-sidebar-right/README.zh.md#the-guide))。 - **`text`**——`fallback`,`dsh-resource://file/**`,只认领 Session 地址。Document Preview 通过 `useResource<'file'>` 观察元数据,经 Remote 回调加载内容,并拥有渲染器选择、工具栏、逐 tab 刷新、滚动与源码定位;未知扩展名按纯文本渲染([README](../../packages/client/ui-sidebar-documentpreview/README.zh.md))。 - **`files`**——`builtin`,以 `openTab('files')` 打开。工作区目录树,经 `list` 懒加载,用 `tab.actions.openResource(fileAddressFor(sessionId, root, path))` 在自己所在 pane 打开文件([README](../../packages/client/ui-sidebar-files/README.zh.md))。 diff --git a/packages/client/ui-chat/src/client/chat/TurnUsagePanel.module.css b/packages/client/ui-chat/src/client/chat/TurnUsagePanel.module.css index 5325390b4d..52058f8be7 100644 --- a/packages/client/ui-chat/src/client/chat/TurnUsagePanel.module.css +++ b/packages/client/ui-chat/src/client/chat/TurnUsagePanel.module.css @@ -54,18 +54,6 @@ color: var(--dsw-alias-label-secondary); } -/* The time pill while it has no dialog to open (neither speed nor TTFT - recorded): rides .trigger for geometry, and quiets its hover and pointer. - Declared after the hover rules so the equal-specificity override wins. */ -.static { - cursor: default; -} - -.static:hover { - background: transparent; - color: var(--dsw-alias-label-tertiary); -} - /* Narrow viewport: the pills collapse to bare icons with the sibling `.action` geometry (28px circle, 6px padding, centered glyph); the label-padding rebate no longer applies, so the pair keeps the row's plain diff --git a/packages/client/ui-chat/src/client/chat/TurnUsagePanel.tsx b/packages/client/ui-chat/src/client/chat/TurnUsagePanel.tsx index 209c70b93d..0d64ff9e08 100644 --- a/packages/client/ui-chat/src/client/chat/TurnUsagePanel.tsx +++ b/packages/client/ui-chat/src/client/chat/TurnUsagePanel.tsx @@ -126,26 +126,11 @@ export function TurnUsagePanel({ usage, t }: TurnUsagePanelProps) { /** * Turn-time IconActions pill with a click-open Turn-time details dialog. - * - * With neither speed nor TTFT recorded the dialog would hold one row repeating - * the pill's own duration (same formatter, same value), so the pill renders as - * plain text instead: no hover, no dialog. * @param props - Turn timing facts and locale seat. * @returns The clock-and-duration trigger and, while open, its portaled dialog anchored above the trigger. */ export function TurnTimePanel({ runMs, tokensPerSecond, ttftMs, t }: TurnTimePanelProps) { const { open, setOpen, rootRef, panelRef, pos } = useStatDialog() - const label = t('message.ranFor', { duration: formatRunDuration(runMs, t) }) - if (tokensPerSecond === undefined && ttftMs === undefined) { - return ( - - - - {label} - - - ) - } return ( {open && createPortal(
{ expect(view.queryByRole('dialog')).toBeNull() }) - it('renders plain text without a dialog while only the duration is known', () => { + it('omits unrecorded speed and TTFT rows', () => { const view = render() - // The dialog would only repeat the pill's own duration, so there is no trigger at all. - expect(view.queryByRole('button')).toBeNull() - expect(view.container.textContent).toBe('Ran for 3s') - }) - - it('keeps the dialog and omits the unrecorded row while one of speed and TTFT is known', () => { - const speedOnly = render() - fireEvent.click(speedOnly.getByRole('button')) - expect(speedOnly.getByRole('dialog').textContent).toContain('Total run time3s') - expect(speedOnly.getByRole('dialog').textContent).toContain('Tokens per second (TPS)20 tok/s') - expect(speedOnly.getByRole('dialog').textContent).not.toContain('Time to first token') - fireEvent.keyDown(document, { key: 'Escape' }) - speedOnly.unmount() - - const ttftOnly = render() - fireEvent.click(ttftOnly.getByRole('button')) - expect(ttftOnly.getByRole('dialog').textContent).not.toContain('Tokens per second') - expect(ttftOnly.getByRole('dialog').textContent).toContain('Time to first token (TTFT)0.8s') + fireEvent.click(view.getByRole('button')) + const dialog = view.getByRole('dialog') + expect(dialog.textContent).toContain('Total run time3s') + expect(dialog.textContent).not.toContain('Tokens per second') + expect(dialog.textContent).not.toContain('Time to first token') }) }) diff --git a/packages/client/ui-primitives/src/markdown/CodeBlock.tsx b/packages/client/ui-primitives/src/markdown/CodeBlock.tsx index 86fffbf02b..4bd573d57e 100644 --- a/packages/client/ui-primitives/src/markdown/CodeBlock.tsx +++ b/packages/client/ui-primitives/src/markdown/CodeBlock.tsx @@ -179,6 +179,7 @@ export function CodeBlock({ code, lang, streaming, className, contentRef, lineNu style={sourceLines === undefined ? undefined : { '--dsl-code-block-line-number-width': `${Math.max(2, String(sourceLines.length).length)}ch`, } as CSSProperties}> + {/* These paired attributes are stable semantic hooks for owner styling and DOM tests. */}
{lang ?? ''}
diff --git a/packages/client/ui-sidebar-documentpreview/src/client/TextPreview.tsx b/packages/client/ui-sidebar-documentpreview/src/client/TextPreview.tsx index 5bebee5d22..3aaa51e367 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/TextPreview.tsx +++ b/packages/client/ui-sidebar-documentpreview/src/client/TextPreview.tsx @@ -117,7 +117,7 @@ export function TextPreview({ const bindScrollport = useCallback((scrollport: HTMLElement | null): void => { const next = scrollport ?? bodyRef.current scrollportRef.current = next - if (scrollport !== null) scrollport.scrollTop = storedScrollTopRef.current + if (next !== null) next.scrollTop = storedScrollTopRef.current }, []) // First mount reads the first page; a body coming back to a tab with content @@ -286,7 +286,7 @@ export function TextPreview({ data-textpreview-wrap={state.wrap ? '' : undefined} onScrollCapture={(event) => { const body = scrollportRef.current - /* v8 ignore next -- the layout effect binds the scrollport before user input. */ + /* v8 ignore next -- callback refs bind the scrollport during commit, before user input. */ if (body === null) return if (event.target !== body) return actions.scrolled(tab.id, body.scrollTop) diff --git a/packages/client/ui-sidebar-documentpreview/tests/text-preview.client.spec.tsx b/packages/client/ui-sidebar-documentpreview/tests/text-preview.client.spec.tsx index b20e10704c..5cebce9fe0 100644 --- a/packages/client/ui-sidebar-documentpreview/tests/text-preview.client.spec.tsx +++ b/packages/client/ui-sidebar-documentpreview/tests/text-preview.client.spec.tsx @@ -444,6 +444,7 @@ describe('TextPreview — navigation and view', () => { expect(h.instance.getSnapshot().byTab[TAB_ID]?.scrollTop).toBe(240) view.rerender() + expect(outer.scrollTop).toBe(240) fireEvent.scroll(outer, { target: { scrollTop: 360 } }) expect(h.instance.getSnapshot().byTab[TAB_ID]?.scrollTop).toBe(360) }) diff --git a/packages/client/ui-sidebar-files/README.i18n.yaml b/packages/client/ui-sidebar-files/README.i18n.yaml index d6c937ecf4..446c0ddb2a 100644 --- a/packages/client/ui-sidebar-files/README.i18n.yaml +++ b/packages/client/ui-sidebar-files/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-sidebar-files/README.md -README.md: cadadc03f96f39f9b48813f25179f4506b6de791 -README.zh.md: e25d86b27556bac746c53abf016b37ce1ded81df +README.md: 28e052b5a6739118c691431aad88964a8f6d19cd +README.zh.md: f9724c159e358658b0cf6d342b26e80e8c59cf01 diff --git a/packages/client/ui-sidebar-files/tests/definition.client.spec.ts b/packages/client/ui-sidebar-files/tests/definition.client.spec.ts index 933455c544..7e4c212a91 100644 --- a/packages/client/ui-sidebar-files/tests/definition.client.spec.ts +++ b/packages/client/ui-sidebar-files/tests/definition.client.spec.ts @@ -1,9 +1,12 @@ +// @vitest-environment jsdom /** * Stage one, as the registry sees it: the type is a page that claims no * address, sits in the builtin band, and offers the guide page one entry that * opens its kind. */ -import { describe, expect, it } from 'vitest' +import { afterEach, describe, expect, it } from 'vitest' +import { createElement } from 'react' +import { cleanup, render } from '@testing-library/react' import { Context } from '@deepseek-ai/cordis' import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime' import { SidebarRightTabRegistry } from '@deepseek-ai/dsh-client-ui-sidebar-right/src/client/tab-registry.ts' @@ -15,6 +18,8 @@ import { zh } from '../src/client/locales.ts' const t = makeTranslate(zh) +afterEach(cleanup) + describe('filesDefinition', () => { it('registers under its kind and id and claims no address', () => { const registry = new SidebarRightTabRegistry(new Context()) @@ -32,7 +37,9 @@ describe('filesDefinition', () => { expect(entry?.kind).toBe(FILES_KIND) expect(entry?.title()).toBe(zh['guide.title']) expect(entry?.description?.()).toBe(zh['guide.description']) - expect(entry?.icon).toBeDefined() + if (entry?.icon === undefined) throw new Error('expected the guide icon') + const icon = render(createElement(entry.icon, { size: 26 })) + expect(icon.container.querySelector('svg')?.getAttribute('width')).toBe('26') }) it('sits in the builtin band and titles itself from the dictionary', () => { diff --git a/packages/client/ui-sidebar-right/README.i18n.yaml b/packages/client/ui-sidebar-right/README.i18n.yaml index 9824bd3a81..184159aae6 100644 --- a/packages/client/ui-sidebar-right/README.i18n.yaml +++ b/packages/client/ui-sidebar-right/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-sidebar-right/README.md -README.md: d0bf1b9a52aa93760d388f1677181d111faebb72 -README.zh.md: 087c55d1ae12d22173e2d2d2c1b5e164df58c774 +README.md: c947b5411f51aff54f480949af203d192662adbf +README.zh.md: 51a5fd9e3193ff1d7be019d14380a6a124ba477a diff --git a/packages/client/ui-sidebar-right/src/client/tabs/guide/GuideBody.module.css b/packages/client/ui-sidebar-right/src/client/tabs/guide/GuideBody.module.css index dad7a8fe96..484791f46e 100644 --- a/packages/client/ui-sidebar-right/src/client/tabs/guide/GuideBody.module.css +++ b/packages/client/ui-sidebar-right/src/client/tabs/guide/GuideBody.module.css @@ -1,8 +1,9 @@ /* The guide body's own sheet: its domain owns these rules, so the shell's stylesheet does not carry them. Tokens only. */ -/* The capsules in one centred column at the column's own 14px gap, lifted - above the body's middle by the spacer below — the empty-state offset. */ +/* The capsules form one centred column at the column's own 14px gap, lifted + above the body's middle by the spacer below. The minimum height centres a + short guide while allowing a tall guide to grow into the pane's scroller. */ .guide { display: flex; flex-direction: column; @@ -10,7 +11,7 @@ align-items: center; justify-content: center; box-sizing: border-box; - height: 100%; + min-height: 100%; padding: 0 24px; } From bc5fd3b8dc3125b262bc1d52169029d2dca4ac94 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Wed, 9 Sep 2026 16:57:50 +0800 Subject: [PATCH 21/27] feat(llm): default Chat Completions to DeepSeek V41 Flash --- ...istory-system-prompt-replacement.i18n.yaml | 4 +- ...02-in-history-system-prompt-replacement.md | 2 +- ...in-history-system-prompt-replacement.zh.md | 2 +- .../models.expected.md | 30 ++------- .../tests/onboarding-deepseek-config.e2e.ts | 36 +++++++---- docs/config-catalog.i18n.yaml | 4 +- docs/config-catalog.md | 4 +- docs/config-catalog.zh.md | 4 +- docs/user/guide/providers.i18n.yaml | 4 +- docs/user/guide/providers.md | 2 +- docs/user/guide/providers.zh.md | 2 +- packages/bundle/base/cordis.patch.yml | 2 +- packages/llm/llm-deepseek/README.i18n.yaml | 4 +- packages/llm/llm-deepseek/README.md | 5 +- packages/llm/llm-deepseek/README.zh.md | 5 +- packages/llm/llm-deepseek/package.json | 1 + packages/llm/llm-deepseek/src/index.ts | 19 ++---- .../llm/llm-deepseek/tests/adapter.e2e.ts | 40 +++++++++++- .../llm/llm-deepseek/tests/adapter.spec.ts | 57 ++++-------------- .../llm-deepseek/tests/dynamic-config.spec.ts | 8 +-- .../llm/llm-deepseek/tests/fixtures/red.png | Bin 0 -> 69 bytes pnpm-lock.yaml | 3 + .../notifications.expected.jsonl | 8 +-- .../system-prompt-in-history/session.v3.jsonl | 8 +-- .../cordis.snapshot.yml | 6 +- .../system-prompt-in-history/cordis.yml | 6 +- .../system-prompt-in-history/session.v3.jsonl | 8 +-- .../system-prompt.expected.md | 4 +- 28 files changed, 130 insertions(+), 148 deletions(-) create mode 100644 packages/llm/llm-deepseek/tests/fixtures/red.png diff --git a/.agents/notes/implemented/feature/2026-09-02-in-history-system-prompt-replacement.i18n.yaml b/.agents/notes/implemented/feature/2026-09-02-in-history-system-prompt-replacement.i18n.yaml index 466ef2e05f..ffb1560ce8 100644 --- a/.agents/notes/implemented/feature/2026-09-02-in-history-system-prompt-replacement.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-09-02-in-history-system-prompt-replacement.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-02-in-history-system-prompt-replacement.md -2026-09-02-in-history-system-prompt-replacement.md: b34a0d2b3591c1b62aba16d79963940be787f373 -2026-09-02-in-history-system-prompt-replacement.zh.md: 296932a977ae852c4ef32de1c23b2c5d9e4a1bbf +2026-09-02-in-history-system-prompt-replacement.md: 8e5fa65ec4649537889cefc75459d2a4958165e5 +2026-09-02-in-history-system-prompt-replacement.zh.md: 73003b04de82f45154f9c673654cc492eb62a4fd diff --git a/.agents/notes/implemented/feature/2026-09-02-in-history-system-prompt-replacement.md b/.agents/notes/implemented/feature/2026-09-02-in-history-system-prompt-replacement.md index b34a0d2b35..8e5fa65ec4 100644 --- a/.agents/notes/implemented/feature/2026-09-02-in-history-system-prompt-replacement.md +++ b/.agents/notes/implemented/feature/2026-09-02-in-history-system-prompt-replacement.md @@ -18,7 +18,7 @@ For a model route that declares the capability, the loop appends a new `system/m ### Capability -`dsh-llm` defines `SystemPromptUpdate = 'in-history'` and carries it as an optional sibling field, `systemPromptUpdate`, on `LlmResolvedModelInfo` and `PreparedLlmCall`; `normalizeModelInfo` rejects any other value with an `LlmError` whose code is `INVALID_MODEL_INFO`. The DeepSeek adapter's catalog model (`DeepSeekCatalogModel.systemPromptUpdate`, validated by zod at load) and the replay provider's `ReplayModelConfig.systemPromptUpdate` declare it per model; absence means the model needs message 0 rewritten. No default catalog entry declares it; a deployment enables it through the `models` list in `cordis.yml`, and every `dsh-llm-pi-ai` route keeps the replace behaviour. +`dsh-llm` defines `SystemPromptUpdate = 'in-history'` and carries it as an optional sibling field, `systemPromptUpdate`, on `LlmResolvedModelInfo` and `PreparedLlmCall`; `normalizeModelInfo` rejects any other value with an `LlmError` whose code is `INVALID_MODEL_INFO`. The DeepSeek adapter's catalog model (`DeepSeekCatalogModel.systemPromptUpdate`, validated by zod at load) and the replay provider's `ReplayModelConfig.systemPromptUpdate` declare it per model; absence means the model needs message 0 rewritten. The sole built-in entry in `dsh-llm-deepseek`, `deepseek-flash`, declares it alongside text and image input. This exact catalog entry records the model capability; names and protocol families do not imply support for other models. A deployment can replace the catalog through the `models` list in `cordis.yml`, and every `dsh-llm-pi-ai` route keeps the replace behaviour. The loop records the mode in the session: `RequestContext.systemPromptUpdate` joins provider, model, and capacity as a `request/context` field, logged whenever any of them differs from the latest snapshot. Admission reads `PreparedLlmCall.systemPromptUpdate` from the actual call prepared after `agent/request`; the preceding snapshot is not an admission input. First requests, resumed sessions, route changes, and same-route capability changes therefore use the capability of the bound adapter that will serve the call. diff --git a/.agents/notes/implemented/feature/2026-09-02-in-history-system-prompt-replacement.zh.md b/.agents/notes/implemented/feature/2026-09-02-in-history-system-prompt-replacement.zh.md index 296932a977..73003b04de 100644 --- a/.agents/notes/implemented/feature/2026-09-02-in-history-system-prompt-replacement.zh.md +++ b/.agents/notes/implemented/feature/2026-09-02-in-history-system-prompt-replacement.zh.md @@ -18,7 +18,7 @@ Status: implemented ### 能力 -`dsh-llm` 定义 `SystemPromptUpdate = 'in-history'`,并把它作为可选的并列字段 `systemPromptUpdate` 放在 `LlmResolvedModelInfo` 与 `PreparedLlmCall` 上;`normalizeModelInfo` 用代码为 `INVALID_MODEL_INFO` 的 `LlmError` 拒绝任何其他值。DeepSeek 适配器的目录模型(`DeepSeekCatalogModel.systemPromptUpdate`,加载时由 zod 校验)与回放提供者的 `ReplayModelConfig.systemPromptUpdate` 逐模型声明它;缺省表示该模型需要重写消息 0。没有默认目录条目声明它;部署方通过 `cordis.yml` 的 `models` 列表启用,所有 `dsh-llm-pi-ai` 路由保持替换行为。 +`dsh-llm` 定义 `SystemPromptUpdate = 'in-history'`,并把它作为可选的并列字段 `systemPromptUpdate` 放在 `LlmResolvedModelInfo` 与 `PreparedLlmCall` 上;`normalizeModelInfo` 用代码为 `INVALID_MODEL_INFO` 的 `LlmError` 拒绝任何其他值。DeepSeek 适配器的目录模型(`DeepSeekCatalogModel.systemPromptUpdate`,加载时由 zod 校验)与回放提供者的 `ReplayModelConfig.systemPromptUpdate` 逐模型声明它;缺省表示该模型需要重写消息 0。`dsh-llm-deepseek` 仅内置 `deepseek-flash` 条目,在该条目上声明它,同时声明文本和图片输入。该精确目录条目记录模型能力;名称和协议类别不能推导其他模型是否支持。部署方可以通过 `cordis.yml` 的 `models` 列表替换目录,所有 `dsh-llm-pi-ai` 路由保持替换行为。 循环把该模式记录进会话:`RequestContext.systemPromptUpdate` 与 provider、model、容量并列成为 `request/context` 的字段,其中任一项与最新快照不同时就记录一次。准入读取 `agent/request` 之后实际准备调用的 `PreparedLlmCall.systemPromptUpdate`;先前快照不是准入输入。因此首次请求、恢复的会话、路由变更以及同一路由的能力变更,都使用将服务该调用的绑定适配器的能力。 diff --git a/apps/web/tests/expected/onboarding-deepseek-config/models.expected.md b/apps/web/tests/expected/onboarding-deepseek-config/models.expected.md index a302932e65..c0943df32e 100644 --- a/apps/web/tests/expected/onboarding-deepseek-config/models.expected.md +++ b/apps/web/tests/expected/onboarding-deepseek-config/models.expected.md @@ -36,40 +36,20 @@ - button "恢复默认模型" - textbox "模型 ID 1": - /placeholder: 模型 ID - - text: deepseek-v4-pro + - text: private-preview - textbox "显示名称 1": - /placeholder: 显示名称 - - text: DeepSeek-V4-Pro - - button "容量 1": + - text: Private Preview + - button "容量 1" [expanded]: - img - button "删除模型 1": - img - - textbox "模型 ID 2": - - /placeholder: 模型 ID - - text: deepseek-v4-flash-vision-exp - - textbox "显示名称 2": - - /placeholder: 显示名称 - - text: DeepSeek-V4-Flash-Vision-Exp - - button "容量 2": - - img - - button "删除模型 2": - - img - - textbox "模型 ID 3": - - /placeholder: 模型 ID - - text: private-preview - - textbox "显示名称 3": - - /placeholder: 显示名称 - - text: Private Preview - - button "容量 3" [expanded]: - - img - - button "删除模型 3": - - img - text: 上下文窗口 - - textbox "上下文窗口 3": + - textbox "上下文窗口 1": - /placeholder: 1M - text: "131072" - text: 最大输出 token 数 - - textbox "最大输出 token 数 3": + - textbox "最大输出 token 数 1": - /placeholder: 256K - text: 64K - button "添加模型": diff --git a/apps/web/tests/onboarding-deepseek-config.e2e.ts b/apps/web/tests/onboarding-deepseek-config.e2e.ts index 97cf82cd19..368c414c0a 100644 --- a/apps/web/tests/onboarding-deepseek-config.e2e.ts +++ b/apps/web/tests/onboarding-deepseek-config.e2e.ts @@ -202,15 +202,32 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup await deepSeek.waitFor({ timeout: 10_000 }) await deepSeek.locator('xpath=ancestor::li').getByRole('button', { name: '编辑' }).click() await settings.getByText('自定义设置').click() + expect(await settings.getByLabel('模型 ID 1').inputValue()).toBe('deepseek-flash') + expect(await settings.getByLabel('显示名称 1').inputValue()).toBe('DeepSeek-V41-Flash') + expect(await settings.getByRole('button', { name: /删除模型/ }).count()).toBe(1) + await settings.getByLabel('显示名称 1').fill('Configured Flash') + await settings.getByRole('button', { name: '保存', exact: true }).click() + await settings.getByLabel('模型 ID 1').waitFor({ state: 'detached', timeout: 15_000 }) + const savedDefaults = await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8') + expect(savedDefaults).toContain('id: deepseek-flash') + expect(savedDefaults).toContain('inputModalities:') + expect(savedDefaults).toContain('- text') + expect(savedDefaults).toContain('- image') + expect(savedDefaults).toContain('systemPromptUpdate: in-history') + await expect(scaffold.ctx.llm.resolveModelInfo('deepseek-official', 'deepseek-flash')).resolves.toMatchObject({ + name: 'Configured Flash', inputModalities: ['text', 'image'], systemPromptUpdate: 'in-history', + }) + await deepSeek.locator('xpath=ancestor::li').getByRole('button', { name: '编辑' }).click() + await settings.getByText('自定义设置').click() await settings.getByRole('button', { name: /删除模型/ }).first().click() await settings.getByRole('button', { name: '添加模型' }).click() - const customModelId = settings.getByLabel('模型 ID 3') + const customModelId = settings.getByLabel('模型 ID 1') await customModelId.fill('private-preview') - await settings.getByLabel('显示名称 3').fill('Private Preview') + await settings.getByLabel('显示名称 1').fill('Private Preview') // Capacities live behind the row's own disclosure, as in the pi-ai form. - await settings.getByRole('button', { name: '容量 3' }).click() - await settings.getByLabel('上下文窗口 3').fill('131072') - await settings.getByLabel('最大输出 token 数 3').fill('64K') + await settings.getByRole('button', { name: '容量 1' }).click() + await settings.getByLabel('上下文窗口 1').fill('131072') + await settings.getByLabel('最大输出 token 数 1').fill('64K') await expect.poll( () => settings.getByLabel('API 密钥', { exact: true }).getAttribute('placeholder'), @@ -222,15 +239,11 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup await customModelId.waitFor({ state: 'detached', timeout: 15_000 }) const document = await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8') - expect(document).toContain('id: deepseek-v4-pro') - expect(document).toContain('id: deepseek-v4-flash-vision-exp') - expect(document).toContain('inputModalities:') - expect(document).toContain('- image') expect(document).toContain('id: private-preview') expect(document).toContain('name: Private Preview') expect(document).toContain('contextWindow: 131072') expect(document).toContain('maxTokens: 64000') - expect(document).not.toMatch(/^\s*- id: deepseek-v4-flash$/m) + expect(document).not.toContain('id: deepseek-flash') await page.keyboard.press('Escape') // A connected Workspace is what puts a live composer — and its model @@ -241,8 +254,7 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup await modelTrigger.waitFor({ timeout: 10_000 }) await modelTrigger.click() await page.getByRole('menuitem', { name: /模型/ }).click() - expect(await page.getByText('deepseek-v4-flash', { exact: true }).count()).toBe(0) - await page.getByRole('menuitemradio', { name: 'DeepSeek-V4-Flash-Vision-Exp' }).waitFor({ timeout: 10_000 }) + expect(await page.getByText('Configured Flash', { exact: true }).count()).toBe(0) await page.getByRole('menuitemradio', { name: 'Private Preview' }).waitFor({ timeout: 10_000 }) expect(tripwire.warnings).toEqual([]) expect(tripwire.pageErrors).toEqual([]) diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml index 901cce8be7..7f8e520a32 100644 --- a/docs/config-catalog.i18n.yaml +++ b/docs/config-catalog.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/config-catalog.md -config-catalog.md: 9adf44b995601196b602e9219e53c52208039051 -config-catalog.zh.md: e2f6c6babc18bfa689f6830c81231012b4d9de18 +config-catalog.md: 8f6907a878c878b7d6f5dc5d51fc51576af426f2 +config-catalog.zh.md: e3fc5ce54aad731a510bcfd51f04cfb5ed30bb2b diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 9adf44b995..8f6907a878 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -1037,7 +1037,7 @@ export interface Config { maxTokens?: number /** Positive context capacity used when the selected model has no exact value (default 1,000,000). */ defaultContextWindow?: number - /** Advisory models shown by discovery consumers; defaults to V4 Flash, V4 Pro, and V4 Flash Vision Exp. */ + /** Advisory models shown by discovery consumers; defaults to DeepSeek-V41-Flash. */ models?: DeepSeekCatalogModel[] /** Maximum provider idle time while one stream read is outstanding (default five minutes). */ streamIdleTimeoutMs?: number @@ -1094,7 +1094,7 @@ export interface DeepSeekCatalogModel { Depends on: [`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts) · [`SystemPromptUpdate`](../packages/llm/llm/src/index.ts) -Source: [`packages/llm/llm-deepseek/src/index.ts:125`](../packages/llm/llm-deepseek/src/index.ts) +Source: [`packages/llm/llm-deepseek/src/index.ts:114`](../packages/llm/llm-deepseek/src/index.ts) diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index e2f6c6babc..e3fc5ce54a 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -1039,7 +1039,7 @@ export interface Config { maxTokens?: number /** Positive context capacity used when the selected model has no exact value (default 1,000,000). */ defaultContextWindow?: number - /** Advisory models shown by discovery consumers; defaults to V4 Flash, V4 Pro, and V4 Flash Vision Exp. */ + /** Advisory models shown by discovery consumers; defaults to DeepSeek-V41-Flash. */ models?: DeepSeekCatalogModel[] /** Maximum provider idle time while one stream read is outstanding (default five minutes). */ streamIdleTimeoutMs?: number @@ -1096,7 +1096,7 @@ export interface DeepSeekCatalogModel { 依赖:[`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts) · [`SystemPromptUpdate`](../packages/llm/llm/src/index.ts) -来源:[`packages/llm/llm-deepseek/src/index.ts:125`](../packages/llm/llm-deepseek/src/index.ts) +来源:[`packages/llm/llm-deepseek/src/index.ts:114`](../packages/llm/llm-deepseek/src/index.ts) diff --git a/docs/user/guide/providers.i18n.yaml b/docs/user/guide/providers.i18n.yaml index 46b27e23a4..1da2f58e9e 100644 --- a/docs/user/guide/providers.i18n.yaml +++ b/docs/user/guide/providers.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/user/guide/providers.md -providers.md: 0db3280760c487c8469399bc9668b0685213e375 -providers.zh.md: 3af9096f5a77ee40afe2ee788aa22a0758dd0e86 +providers.md: 90d3bb2c96a80185fbc5e3e70f1ff577f6923acc +providers.zh.md: 2937d676253ee45dad48655398736d36fc37dff2 diff --git a/docs/user/guide/providers.md b/docs/user/guide/providers.md index 0db3280760..90d3bb2c96 100644 --- a/docs/user/guide/providers.md +++ b/docs/user/guide/providers.md @@ -186,5 +186,5 @@ Every switch, its accepted values, and the protocols that take it are listed und - **The Effort menu does not appear for a model you entered by hand** — It declares no levels. Add `reasoningEfforts` to the model in `settings.yaml`. - **`off` does not stop a DeepSeek model from thinking** — An empty `off` sends no reasoning field at all, and an endpoint that thinks by default keeps thinking. Set `compat.thinkingFormat: deepseek` on the model or the route. - **A compat switch is refused as having no value** — A key written with nothing after the colon. Give it a value, or remove the key to keep the installed catalog's. -- **An image is refused before sending** — The model declares no image modality. Give a custom provider's model `input: [text, image]`; on DeepSeek's own route, select `deepseek-v4-flash-vision-exp`, the model that declares images. +- **An image is refused before sending** — The model declares no image modality. Give a custom provider's model `input: [text, image]`; on DeepSeek's own route, select an image-capable entry from the configured catalog (`deepseek-flash` by default) and confirm that your gateway serves that model with image input. - **The provider rejects a request carrying an image** — The model declares images its endpoint does not actually serve. Remove `image` from whichever list granted it — the model's `input`, or the route's `defaultInput` — then start a new session: the attached image stays in the session log, so the same request repeats until the session moves off it. diff --git a/docs/user/guide/providers.zh.md b/docs/user/guide/providers.zh.md index 3af9096f5a..2937d67625 100644 --- a/docs/user/guide/providers.zh.md +++ b/docs/user/guide/providers.zh.md @@ -186,5 +186,5 @@ llm-pi-ai: - **手动录入的模型没有推理等级菜单**:该模型没有声明任何等级。在 `settings.yaml` 中给该模型加上 `reasoningEfforts`。 - **`off` 无法让 DeepSeek 模型停止思考**:留空的 `off` 不发送任何推理字段,默认思考的端点就继续思考。请在模型或路由上设置 `compat.thinkingFormat: deepseek`。 - **某个 compat 开关因没有值而被拒绝**:冒号后什么都没写。给它一个值,或删掉该键以沿用已安装 catalog 的值。 -- **图片在发送前被拒绝**:该模型未声明图片模态。请给自定义提供方的模型加上 `input: [text, image]`;在 DeepSeek 自身的路由上,请选择声明了图片能力的模型 `deepseek-v4-flash-vision-exp`。 +- **图片在发送前被拒绝**:该模型未声明图片模态。请给自定义提供方的模型加上 `input: [text, image]`;在 DeepSeek 自身的路由上,请从配置的目录中选择支持图片的条目(默认为 `deepseek-flash`),并确认网关提供该模型且支持图片输入。 - **提供方拒绝了带图片的请求**:该模型声明了其端点实际并不提供的图片能力。请从授予它图片能力的那个列表中移除 `image`——可能是模型的 `input`,也可能是路由的 `defaultInput`——然后开启新会话:附加的图片会留在会话日志里,因此在会话离开它之前,同一个请求会不断重复。 diff --git a/packages/bundle/base/cordis.patch.yml b/packages/bundle/base/cordis.patch.yml index 5c50261d0d..f906ea8601 100644 --- a/packages/bundle/base/cordis.patch.yml +++ b/packages/bundle/base/cordis.patch.yml @@ -76,7 +76,7 @@ name: '@deepseek-ai/dsh-agent-default-model' config: provider: deepseek-official - model: deepseek-v4-flash + model: deepseek-flash - id: jobs name: '@deepseek-ai/dsh-jobs-local' diff --git a/packages/llm/llm-deepseek/README.i18n.yaml b/packages/llm/llm-deepseek/README.i18n.yaml index 9e5ab2f8bf..077db2b487 100644 --- a/packages/llm/llm-deepseek/README.i18n.yaml +++ b/packages/llm/llm-deepseek/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/llm/llm-deepseek/README.md -README.md: 64d09214b189605ad4f72f6dedde72f0099aef0a -README.zh.md: 3b8a2dc93fc0106e166c96ce49eab8fb59ba6203 +README.md: 19025b801943ecccd9a17a28870fc364fede7d69 +README.zh.md: 89fa42491aaa2f13ef09b2d0a70c5297cfb5c345 diff --git a/packages/llm/llm-deepseek/README.md b/packages/llm/llm-deepseek/README.md index 64d09214b1..19025b8019 100644 --- a/packages/llm/llm-deepseek/README.md +++ b/packages/llm/llm-deepseek/README.md @@ -46,7 +46,7 @@ Choose this adapter when the deployment targets DeepSeek's official API, optiona filesApiTimeoutMs: 60000 ``` -A request selects the route with `provider: deepseek-official`; the model id passes through to the wire, so new DeepSeek models need no re-registration. Omitted `models` advertises `deepseek-v4-flash` as the fast, economical choice for focused work, `deepseek-v4-pro` as the stronger, higher-cost choice for complex or quality-critical work, and the image-capable `deepseek-v4-flash-vision-exp`; each has a 1,000,000-token context window. An explicit list replaces those defaults, and unlisted model ids still pass through as text-only routes. Clients, including model discovery tools, can read the advisory entries through `ctx.llm.listModels('deepseek-official')`. Image-capable entries may set `imagePixelBudget` to a positive integer or `low`, and may set `imageMaxBytes`. An entry may declare `systemPromptUpdate: in-history` when its endpoint reads the latest `system` message at any position of `messages` as the complete effective system prompt; the adapter reports the mode on the resolved model and the prepared call, and the agent loop then appends a changed prompt after the cached history instead of rewriting the leading system message ([decision rule](../../core/agent-loop/README.md#understand-the-implementation)). No default entry declares the mode: a deployment opts in per model through `models`, and any value other than `in-history` fails at load with `llm-deepseek: catalog model "" systemPromptUpdate must be "in-history" when present`. +A request selects the route with `provider: deepseek-official`; the model id passes through to the wire, so new DeepSeek models need no re-registration. Omitted `models` advertises only the text- and image-capable `deepseek-flash`, with a 1,000,000-token context window. An explicit list replaces those defaults, and unlisted model ids still pass through as text-only routes. Clients, including model discovery tools, can read the advisory entries through `ctx.llm.listModels('deepseek-official')`. Image-capable entries may set `imagePixelBudget` to a positive integer or `low`, and may set `imageMaxBytes`. An entry may declare `systemPromptUpdate: in-history` when its endpoint reads the latest `system` message at any position of `messages` as the complete effective system prompt; the adapter reports the mode on the resolved model and the prepared call, and the agent loop then appends a changed prompt after the cached history instead of rewriting the leading system message ([decision rule](../../core/agent-loop/README.md#understand-the-implementation)). The default `deepseek-flash` entry declares this mode; other models require an explicit `models` declaration, and any value other than `in-history` fails at load with `llm-deepseek: catalog model "" systemPromptUpdate must be "in-history" when present`. | Field | Default | Meaning | |---|---|---| @@ -56,7 +56,7 @@ A request selects the route with `provider: deepseek-official`; the model id pas | `reasoningEffort` | `high` | Default effort: `off`, `low`, `high`, or `max` | | `maxTokens` | `256,000` | Per-request output cap; a model's own cap and explicit request values win | | `defaultContextWindow` | `1,000,000` | Capacity fallback for models without an exact value | -| `models` | V4 Flash + V4 Pro + V4 Flash Vision Exp | Advisory catalog shown by discovery consumers | +| `models` | DeepSeek-V41-Flash | Advisory catalog shown by discovery consumers | | `streamIdleTimeoutMs` | `300,000` | Maximum provider idle time per outstanding stream read | | `maxRequestFilesBytes` | `128 MiB` | High watermark for retained request-image bytes before oldest-first offload | | `maxInlineRequestImageBytes` | `20 MiB` | Independent base64 fallback high watermark | @@ -188,6 +188,7 @@ These limits define where the adapter stops and future work begins. They are cur - **Requests use raw `fetch`, not `@cordisjs/plugin-http`** — no shared proxy or interception configuration. - **Plugin-added content block types are skipped** — core text and supported image blocks are serialized, and empty tool output crosses the wire as the literal `(no output)`. - **Images are input-only durable attachments** — direct external URLs and assistant image output are not supported; DeepSeek input normally uses the Files API and uses inline base64 only for per-request recovery. +- The default catalog pre-registers `deepseek-flash` and its text/image and in-history capabilities without probing gateway availability. Requests can fail with `INVALID_REQUEST` until the gateway enables the id. With `DEEPSEEK_API_KEY` and a supporting gateway configured, `DEEPSEEK_FLASH_E2E=1` enables the Chat Completions check in [this package's e2e suite](tests/adapter.e2e.ts). ### Dev Note diff --git a/packages/llm/llm-deepseek/README.zh.md b/packages/llm/llm-deepseek/README.zh.md index 3b8a2dc93f..89fa42491a 100644 --- a/packages/llm/llm-deepseek/README.zh.md +++ b/packages/llm/llm-deepseek/README.zh.md @@ -46,7 +46,7 @@ kind: "package-reference" filesApiTimeoutMs: 60000 ``` -请求用 `provider: deepseek-official` 选择路由;模型 id 原样传到协议,因此新增 DeepSeek 模型无需重新注册。省略 `models` 时会公布适合专注任务、快速且经济的 `deepseek-v4-flash`,适合复杂或质量关键任务、能力更强且成本更高的 `deepseek-v4-pro`,以及支持图像的 `deepseek-v4-flash-vision-exp`;每个模型都有 1,000,000 token 上下文窗口。显式列表会替换这些默认值,未列出的模型 id 仍作为纯文本路由原样通过。包括模型发现工具在内的客户端可通过 `ctx.llm.listModels('deepseek-official')` 读取这些建议性条目。支持图片的条目可把 `imagePixelBudget` 设置为正整数或 `low`,也可以设置 `imageMaxBytes`。当端点把 `messages` 中任意位置最新的 `system` 消息读作完整的有效系统提示词时,条目可以声明 `systemPromptUpdate: in-history`;适配器会在已解析模型与已准备调用上报告该模式,agent loop(智能体循环)随后把变化后的提示词追加到已缓存历史之后,而不是改写开头的 system 消息([决策规则](../../core/agent-loop/README.zh.md#understand-the-implementation))。没有任何默认条目声明该模式:部署通过 `models` 逐模型启用,`in-history` 以外的任何值都会在加载时以 `llm-deepseek: catalog model "" systemPromptUpdate must be "in-history" when present` 失败。 +请求用 `provider: deepseek-official` 选择路由;模型 id 原样传到协议,因此新增 DeepSeek 模型无需重新注册。省略 `models` 时仅公布支持文本和图像的 `deepseek-flash`,上下文窗口为 1,000,000 token。显式列表会替换这些默认值,未列出的模型 id 仍作为纯文本路由原样通过。包括模型发现工具在内的客户端可通过 `ctx.llm.listModels('deepseek-official')` 读取这些建议性条目。支持图片的条目可把 `imagePixelBudget` 设置为正整数或 `low`,也可以设置 `imageMaxBytes`。当端点把 `messages` 中任意位置最新的 `system` 消息读作完整的有效系统提示词时,条目可以声明 `systemPromptUpdate: in-history`;适配器会在已解析模型与已准备调用上报告该模式,agent loop(智能体循环)随后把变化后的提示词追加到已缓存历史之后,而不是改写开头的 system 消息([决策规则](../../core/agent-loop/README.zh.md#understand-the-implementation))。默认的 `deepseek-flash` 条目声明该模式;其他模型需通过 `models` 显式声明,`in-history` 以外的任何值都会在加载时以 `llm-deepseek: catalog model "" systemPromptUpdate must be "in-history" when present` 失败。 | 字段 | 默认值 | 含义 | |---|---|---| @@ -56,7 +56,7 @@ kind: "package-reference" | `reasoningEffort` | `high` | 默认强度:`off`、`low`、`high` 或 `max` | | `maxTokens` | `256,000` | 单次请求输出上限;模型自身上限与显式请求值优先 | | `defaultContextWindow` | `1,000,000` | 无精确值模型的容量回退 | -| `models` | V4 Flash + V4 Pro + V4 Flash Vision Exp | 供发现消费方查看的建议性目录 | +| `models` | DeepSeek-V41-Flash | 供发现消费方查看的建议性目录 | | `streamIdleTimeoutMs` | `300,000` | 单次流读取未完成的最大提供方空闲时间 | | `maxRequestFilesBytes` | `128 MiB` | 按最旧优先卸载前保留的请求图片字节高水位 | | `maxInlineRequestImageBytes` | `20 MiB` | 独立的 base64 回退高水位 | @@ -188,6 +188,7 @@ loop 保留的响应块会追加到下一个请求,并保留其更早的可复 - **请求使用原始 `fetch`,而非 `@cordisjs/plugin-http`**——没有共享代理或拦截配置。 - **跳过插件新增的内容块类型**——核心文本与受支持图片块会被序列化,空工具输出以字面量 `(no output)` 过线。 - **图片是仅输入的持久附件**——不支持直接外部 URL 与 assistant 图片输出;DeepSeek 输入通常使用 Files API,仅在单次请求恢复时使用内联 base64。 +- 默认目录预注册 `deepseek-flash` 及其文本、图片和历史内更新能力,不探测网关可用性。网关开放该 ID 前,请求可能以 `INVALID_REQUEST` 失败。配置 `DEEPSEEK_API_KEY` 和支持该 ID 的网关后,设置 `DEEPSEEK_FLASH_E2E=1` 可启用[本包 e2e 测试文件](tests/adapter.e2e.ts)中的 Chat Completions 协议验证。 ### 开发备注 diff --git a/packages/llm/llm-deepseek/package.json b/packages/llm/llm-deepseek/package.json index f9a8d5c09a..e8c177a75e 100644 --- a/packages/llm/llm-deepseek/package.json +++ b/packages/llm/llm-deepseek/package.json @@ -52,6 +52,7 @@ "@deepseek-ai/dsh-anonymous-user-id": "workspace:^", "@deepseek-ai/dsh-atomic-write": "workspace:^", "@deepseek-ai/dsh-attachment": "workspace:^", + "@deepseek-ai/dsh-attachment-local": "workspace:^", "@deepseek-ai/dsh-credentials": "workspace:^", "@deepseek-ai/dsh-deepseek-llm-api-extensions": "workspace:^", "@deepseek-ai/dsh-fs": "workspace:^", diff --git a/packages/llm/llm-deepseek/src/index.ts b/packages/llm/llm-deepseek/src/index.ts index 1a865c2507..c0058fd340 100644 --- a/packages/llm/llm-deepseek/src/index.ts +++ b/packages/llm/llm-deepseek/src/index.ts @@ -91,24 +91,13 @@ const PROVIDER = 'deepseek-official' const DEFAULT_MODELS: DeepSeekCatalogModel[] = [ { - id: 'deepseek-v4-flash', - name: 'DeepSeek-V4-Flash', - description: 'Fast, efficient, and economical; suited to focused, routine, or parallel tasks.', - contextWindow: DEFAULT_CONTEXT_WINDOW, - }, - { - id: 'deepseek-v4-pro', - name: 'DeepSeek-V4-Pro', - description: 'Stronger agentic coding, knowledge, and difficult reasoning; suited to complex or quality-critical tasks at higher cost.', - contextWindow: DEFAULT_CONTEXT_WINDOW, - }, - { - id: 'deepseek-v4-flash-vision-exp', - name: 'DeepSeek-V4-Flash-Vision-Exp', + id: 'deepseek-flash', + name: 'DeepSeek-V41-Flash', contextWindow: DEFAULT_CONTEXT_WINDOW, inputModalities: ['text', 'image'], imagePixelBudget: DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET, imageMaxBytes: DEFAULT_REQUEST_IMAGE_MAX_BYTES, + systemPromptUpdate: 'in-history', }, ] @@ -135,7 +124,7 @@ export interface Config { maxTokens?: number /** Positive context capacity used when the selected model has no exact value (default 1,000,000). */ defaultContextWindow?: number - /** Advisory models shown by discovery consumers; defaults to V4 Flash, V4 Pro, and V4 Flash Vision Exp. */ + /** Advisory models shown by discovery consumers; defaults to DeepSeek-V41-Flash. */ models?: DeepSeekCatalogModel[] /** Maximum provider idle time while one stream read is outstanding (default five minutes). */ streamIdleTimeoutMs?: number diff --git a/packages/llm/llm-deepseek/tests/adapter.e2e.ts b/packages/llm/llm-deepseek/tests/adapter.e2e.ts index 1aeb6611fe..fc14b03e55 100644 --- a/packages/llm/llm-deepseek/tests/adapter.e2e.ts +++ b/packages/llm/llm-deepseek/tests/adapter.e2e.ts @@ -10,6 +10,7 @@ import AgentRegistry from '@deepseek-ai/dsh-agent' import LlmRuntime, { createUserMessage, ToolCallId, ReasoningEffortId, createMessage, createSystemMessage } from '@deepseek-ai/dsh-llm' import type { Message, ToolSchema } from '@deepseek-ai/dsh-llm' import AttachmentStore, { AttachmentId, ImageVariantId } from '@deepseek-ai/dsh-attachment' +import LocalAttachments from '@deepseek-ai/dsh-attachment-local' import type { ImageAttachmentLimits, ImageAttachmentRef, @@ -32,7 +33,8 @@ import { assemble, type AssembledResult } from './assemble.ts' * Real-API e2e for the direct-fetch adapter: V4 Flash across thinking modes * and a max-effort tool round trip with reasoning passback. The suite skips * entirely without $DEEPSEEK_API_KEY; the pre-release vision smoke additionally - * requires $DEEPSEEK_VISION_E2E=1 (see vitest.e2e.config.ts). + * requires $DEEPSEEK_VISION_E2E=1, and the Flash image/system-update smoke + * requires $DEEPSEEK_FLASH_E2E=1 (see vitest.e2e.config.ts). */ const FLASH = 'deepseek-v4-flash' @@ -102,12 +104,15 @@ beforeEach(async () => { vi.stubEnv('DSH_HOME', identityHome) }) -async function harness(_model: string, config: Partial = {}) { +async function harness(model: string, config: Partial = {}) { const ctx = new Context() contexts.push(ctx) await ctx.plugin(LlmRuntime) await ctx.plugin(E2eAttachmentStore) - await ctx.plugin(LlmDeepSeek, config) + await ctx.plugin(LlmDeepSeek, { + ...model === VISION ? { models: [{ id: VISION, inputModalities: ['text', 'image'] }] } : {}, + ...config, + }) return ctx } @@ -143,6 +148,35 @@ const weatherTool: ToolSchema = { } describe.skipIf(!process.env.DEEPSEEK_API_KEY)('llm-deepseek e2e (real API)', () => { + it.skipIf(process.env.DEEPSEEK_FLASH_E2E !== '1')('deepseek-flash accepts images and retains system updates', async () => { + const ctx = new Context() + contexts.push(ctx) + await ctx.plugin(LlmRuntime) + await ctx.plugin(LocalAttachments) + await ctx.plugin(LlmDeepSeek, { maxTokens: 4096 }) + const model = 'deepseek-flash' + await expect(ctx.llm.resolveModelInfo('deepseek-official', model)).resolves.toMatchObject({ + inputModalities: ['text', 'image'], systemPromptUpdate: 'in-history', + }) + const attachment = await ctx.attachments.saveImage({ data: readFileSync(new URL('fixtures/red.png', import.meta.url)), mediaType: 'image/png' }) + const message = ask('What is the dominant color of this image?')[0]! + const history: Message[] = [ + createSystemMessage('Answer with one English color word.', 'test'), + { ...message, content: [...message.content, { type: 'image', attachment }] }, + ] + const reply = async () => { + const response = await assemble(ctx, { model, messages: history, reasoningEffort: ReasoningEffortId('high') }) + expect(response.finish.kind, JSON.stringify(response.finish)).toBe('stop') + history.push(response.message) + return textOf(response).trim().toLowerCase() + } + expect(await reply()).toMatch(/^red[.!]?$/) + history.push(createSystemMessage('Reply to every user message with exactly banana.', 'test'), ...ask('Answer now.')) + expect(await reply()).toBe('banana') + history.push(...ask('Answer again.')) + expect(await reply()).toBe('banana') + }) + it.skipIf(!VISION_E2E_ENABLED)('uses the built-in official route to upload, reference, and delete one image', async () => { const key = process.env.DEEPSEEK_API_KEY if (key === undefined) throw new Error('e2e ran without DEEPSEEK_API_KEY') diff --git a/packages/llm/llm-deepseek/tests/adapter.spec.ts b/packages/llm/llm-deepseek/tests/adapter.spec.ts index 31885fa4d9..b3870a2290 100644 --- a/packages/llm/llm-deepseek/tests/adapter.spec.ts +++ b/packages/llm/llm-deepseek/tests/adapter.spec.ts @@ -379,7 +379,7 @@ describe('DeepSeekAdapter against a mock server', () => { await drain(adapter.stream({ provider: 'deepseek-official', - model: 'deepseek-v4-flash-vision-exp', + model: 'deepseek-flash', messages: [createUserMessage({ content: [ { type: 'text', text: 'describe ' }, @@ -390,7 +390,7 @@ describe('DeepSeekAdapter against a mock server', () => { })) expect(server.requests[0]).toMatchObject({ - model: 'deepseek-v4-flash-vision-exp', + model: 'deepseek-flash', messages: [{ role: 'user', content: [ @@ -1697,27 +1697,15 @@ describe('plugin registration and config', () => { await ctx.plugin(LlmDeepSeek, { baseURL: 'http://127.0.0.1:1' }) expect(ctx.llm.listProviders()).toEqual([{ id: 'deepseek-official', name: 'DeepSeek' }]) await expect(ctx.llm.listModels('deepseek-official')).resolves.toEqual([ - { - provider: 'deepseek-official', - id: 'deepseek-v4-flash', - name: 'DeepSeek-V4-Flash', - description: 'Fast, efficient, and economical; suited to focused, routine, or parallel tasks.', - inputModalities: ['text'], - }, - { - provider: 'deepseek-official', - id: 'deepseek-v4-pro', - name: 'DeepSeek-V4-Pro', - description: 'Stronger agentic coding, knowledge, and difficult reasoning; suited to complex or quality-critical tasks at higher cost.', - inputModalities: ['text'], - }, - { provider: 'deepseek-official', id: 'deepseek-v4-flash-vision-exp', name: 'DeepSeek-V4-Flash-Vision-Exp', inputModalities: ['text', 'image'] }, + { provider: 'deepseek-official', id: 'deepseek-flash', name: 'DeepSeek-V41-Flash', inputModalities: ['text', 'image'] }, ]) - await expect(ctx.llm.resolveModelInfo('deepseek-official', 'deepseek-v4-flash')) + await expect(ctx.llm.resolveModelInfo('deepseek-official', 'deepseek-flash')) .resolves.toMatchObject({ provider: 'deepseek-official', - id: 'deepseek-v4-flash', - name: 'DeepSeek-V4-Flash', + id: 'deepseek-flash', + name: 'DeepSeek-V41-Flash', + inputModalities: ['text', 'image'], + systemPromptUpdate: 'in-history', context: { contextWindow: 1_000_000 }, defaultMaxTokens: 256_000, reasoning: { @@ -1730,15 +1718,6 @@ describe('plugin registration and config', () => { defaultEffort: ReasoningEffortId('high'), }, }) - await expect(ctx.llm.resolveModelInfo('deepseek-official', 'deepseek-v4-flash-vision-exp')) - .resolves.toMatchObject({ - provider: 'deepseek-official', - id: 'deepseek-v4-flash-vision-exp', - name: 'DeepSeek-V4-Flash-Vision-Exp', - inputModalities: ['text', 'image'], - context: { contextWindow: 1_000_000 }, - defaultMaxTokens: 256_000, - }) }) it.each(['off', 'low', 'max'] as const)('uses the configured %s reasoning default', async (effort) => { @@ -1824,21 +1803,7 @@ describe('plugin registration and config', () => { await ctx.plugin(LlmRuntime) LlmDeepSeek.apply(ctx, { baseURL: 'http://127.0.0.1:1' }) await expect(ctx.llm.listModels('deepseek-official')).resolves.toEqual([ - { - provider: 'deepseek-official', - id: 'deepseek-v4-flash', - name: 'DeepSeek-V4-Flash', - description: 'Fast, efficient, and economical; suited to focused, routine, or parallel tasks.', - inputModalities: ['text'], - }, - { - provider: 'deepseek-official', - id: 'deepseek-v4-pro', - name: 'DeepSeek-V4-Pro', - description: 'Stronger agentic coding, knowledge, and difficult reasoning; suited to complex or quality-critical tasks at higher cost.', - inputModalities: ['text'], - }, - { provider: 'deepseek-official', id: 'deepseek-v4-flash-vision-exp', name: 'DeepSeek-V4-Flash-Vision-Exp', inputModalities: ['text', 'image'] }, + { provider: 'deepseek-official', id: 'deepseek-flash', name: 'DeepSeek-V41-Flash', inputModalities: ['text', 'image'] }, ]) }) @@ -2154,7 +2119,7 @@ describe('plugin registration and config', () => { // First-boot onboarding: the route registers so models stay discoverable; // only the request itself needs a key. expect(ctx.llm.listProviders()).toEqual([{ id: 'deepseek-official', name: 'DeepSeek' }]) - await expect(ctx.llm.listModels('deepseek-official')).resolves.toHaveLength(3) + await expect(ctx.llm.listModels('deepseek-official')).resolves.toHaveLength(1) const first = await assemble(ctx, { model: 'deepseek-v4-flash', messages: [] }) expect(first.finish).toMatchObject({ kind: 'error', failure: { code: 'MISSING_CREDENTIAL' } }) // The guidance leads with the managed credential store. @@ -2242,7 +2207,7 @@ describe('plugin registration and config', () => { expect(adapter).toBeInstanceOf(DeepSeekAdapter) // Direct embedding shares the plugin's one resolve step, so it advertises // the same default catalog instead of a divergent empty one. - await expect(adapter.listModels('deepseek-official')).resolves.toHaveLength(3) + await expect(adapter.listModels('deepseek-official')).resolves.toHaveLength(1) }) it('resolves connection facts and the credential exactly once per stream call', async () => { diff --git a/packages/llm/llm-deepseek/tests/dynamic-config.spec.ts b/packages/llm/llm-deepseek/tests/dynamic-config.spec.ts index d8b27a2407..f8857b5bb7 100644 --- a/packages/llm/llm-deepseek/tests/dynamic-config.spec.ts +++ b/packages/llm/llm-deepseek/tests/dynamic-config.spec.ts @@ -191,7 +191,7 @@ describe('request-level dynamic configuration', () => { const dir = await home() const { ctx } = await boot(dir, { baseURL: 'http://127.0.0.1:1' }) - await expect(ctx.llm.listModels('deepseek-official')).resolves.toHaveLength(3) + await expect(ctx.llm.listModels('deepseek-official')).resolves.toHaveLength(1) await ctx.settings.update(NS, { models: [{ id: 'settings-model', name: 'From Settings', inputModalities: ['text', 'image'] }], }) @@ -217,9 +217,9 @@ describe('request-level dynamic configuration', () => { source: { kind: 'plugin', plugin: 'test' }, })] - await assemble(ctx, { model: 'deepseek-v4-flash-vision-exp', messages }) + await assemble(ctx, { model: 'deepseek-flash', messages }) await ctx.settings.update(NS, { maxRequestFilesBytes: 4, imageOffloadByteQuantum: 2 }) - await assemble(ctx, { model: 'deepseek-v4-flash-vision-exp', messages }) + await assemble(ctx, { model: 'deepseek-flash', messages }) const first = (server.requests[0] as { messages: Array<{ content: unknown }> }).messages[0]?.content const second = (server.requests[1] as { messages: Array<{ content: unknown }> }).messages[0]?.content @@ -261,7 +261,7 @@ describe('request-level dynamic configuration', () => { // Schema-valid but resolver-invalid: duplicate catalog ids pass the array // schema and fail the explicit resolve step. await ctx.settings.update(NS, { models: [{ id: 'dup' }, { id: 'dup' }] }) - await expect(ctx.llm.listModels('deepseek-official')).resolves.toHaveLength(3) + await expect(ctx.llm.listModels('deepseek-official')).resolves.toHaveLength(1) await ctx.settings.update(NS, { models: [{ id: 'recovered' }] }) await expect(ctx.llm.listModels('deepseek-official')).resolves.toEqual([ { provider: 'deepseek-official', id: 'recovered', name: 'recovered', inputModalities: ['text'] }, diff --git a/packages/llm/llm-deepseek/tests/fixtures/red.png b/packages/llm/llm-deepseek/tests/fixtures/red.png new file mode 100644 index 0000000000000000000000000000000000000000..62a5f8f47fec02344e5bf9061888262f677cf5d6 GIT binary patch literal 69 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1SBVv2j2ryJf1F&Ar*6yf1E$Sz`)GN$Z+!C Rr1wB^22WQ%mvv4FO#seq5RCu; literal 0 HcmV?d00001 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 04b7a7e33a..be1647c24b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7034,6 +7034,9 @@ importers: '@deepseek-ai/dsh-attachment': specifier: workspace:^ version: link:../../attachment/attachment + '@deepseek-ai/dsh-attachment-local': + specifier: workspace:^ + version: link:../../attachment/attachment-local '@deepseek-ai/dsh-credentials': specifier: workspace:^ version: link:../../credentials/credentials diff --git a/snapshots/sdk/system-prompt-in-history/notifications.expected.jsonl b/snapshots/sdk/system-prompt-in-history/notifications.expected.jsonl index 566f3a5c7c..0ded34c8db 100644 --- a/snapshots/sdk/system-prompt-in-history/notifications.expected.jsonl +++ b/snapshots/sdk/system-prompt-in-history/notifications.expected.jsonl @@ -6,16 +6,16 @@ {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"system/message","seq":7,"time":0,"data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"Read task.txt with the read tool, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash","systemPromptUpdate":"in-history"}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-flash"},"tools":"{{tools}}"},"reason":"initial"}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-flash","systemPromptUpdate":"in-history"}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"session/title","seq":12,"time":0,"data":{"title":"Read task.txt with the read","messageSeqs":[8],"source":{"kind":"fallback"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":13,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_task_read","name":"read","arguments":"{\"file_path\":\"task.txt\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"call_task_read","name":"read","args":["{\"file_path\":\"task.txt\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_task_read","name":"read","arguments":"{\"file_path\":\"task.txt\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":13,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_task_read","name":"read","arguments":"{\"file_path\":\"task.txt\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"call_task_read","name":"read","args":["{\"file_path\":\"task.txt\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_task_read","name":"read","arguments":"{\"file_path\":\"task.txt\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":14,"time":0,"data":{"turn":1,"step":1,"callId":"call_task_read","name":"read","arguments":"{\"file_path\":\"task.txt\"}"}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":15,"time":0,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_task_read"},"content":[{"type":"tool-result","toolCallId":"call_task_read","content":[{"type":"text","text":"{{cwd}}/task.txt\nfile\n\n1: Reply with the single word DONE after reading this file.\n\n(End of file - total 1 lines)\n"}],"isError":false}],"role":"user","id":"{{sessionId}}"},"meta":{"path":"{{cwd}}/task.txt","offset":1,"lines":[{"number":1,"text":"Reply with the single word DONE after reading this file."}],"totalLines":1}},"sourceEventSeqs":[14],"surfaceOp":"append"}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":16,"time":0,"data":{"turn":1,"step":1}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":17,"time":0,"data":{"turn":1,"step":2}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"system/message","seq":18,"time":0,"data":{"turn":1,"step":2,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":19,"time":0,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":12,"outputTokens":2,"cacheReadTokens":10},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["DONE"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":12,"outputTokens":2,"cacheReadTokens":10}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":19,"time":0,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":12,"outputTokens":2,"cacheReadTokens":10},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["DONE"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":12,"outputTokens":2,"cacheReadTokens":10}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":20,"time":0,"data":{"turn":1,"step":2}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"turn/end","seq":21,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}}}} {"method":"session.status","params":{"sessionId":"{{sessionId}}","status":"idle"}} diff --git a/snapshots/sdk/system-prompt-in-history/session.v3.jsonl b/snapshots/sdk/system-prompt-in-history/session.v3.jsonl index 14d90db6e6..ddbbbc88b2 100644 --- a/snapshots/sdk/system-prompt-in-history/session.v3.jsonl +++ b/snapshots/sdk/system-prompt-in-history/session.v3.jsonl @@ -9,15 +9,15 @@ {"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"id":"{{message:2}}"}},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Read task.txt with the read tool, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}} -{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash","systemPromptUpdate":"in-history"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-flash"},"tools":"{{tools}}"},"reason":"initial"}} +{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-flash","systemPromptUpdate":"in-history"}} {"type":"session/title","data":{"title":"Read task.txt with the read","messageSeqs":[8],"source":{"kind":"fallback"}}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_task_read","name":"read","arguments":"{\"file_path\":\"task.txt\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"call_task_read","name":"read","args":["{\"file_path\":\"task.txt\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_task_read","name":"read","arguments":"{\"file_path\":\"task.txt\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_task_read","name":"read","arguments":"{\"file_path\":\"task.txt\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"call_task_read","name":"read","args":["{\"file_path\":\"task.txt\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_task_read","name":"read","arguments":"{\"file_path\":\"task.txt\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_task_read","name":"read","arguments":"{\"file_path\":\"task.txt\"}"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_task_read"},"content":[{"type":"tool-result","toolCallId":"call_task_read","content":[{"type":"text","text":"{{cwd}}/task.txt\nfile\n\n1: Reply with the single word DONE after reading this file.\n\n(End of file - total 1 lines)\n"}],"isError":false}],"role":"user","id":"{{message:5}}"},"meta":{"path":"{{cwd}}/task.txt","offset":1,"lines":[{"number":1,"text":"Reply with the single word DONE after reading this file."}],"totalLines":1}},"sourceEventSeqs":[14],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} {"type":"system/message","data":{"turn":1,"step":2,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"id":"{{message:6}}"}},"surfaceOp":"append"} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"usage":{"inputTokens":12,"outputTokens":2,"cacheReadTokens":10},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["DONE"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":12,"outputTokens":2,"cacheReadTokens":10}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-flash"},"id":"{{message:7}}"},"usage":{"inputTokens":12,"outputTokens":2,"cacheReadTokens":10},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["DONE"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":12,"outputTokens":2,"cacheReadTokens":10}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/session/system-prompt-in-history/cordis.snapshot.yml b/snapshots/session/system-prompt-in-history/cordis.snapshot.yml index 37d84f4a8a..34c9f21ad3 100644 --- a/snapshots/session/system-prompt-in-history/cordis.snapshot.yml +++ b/snapshots/session/system-prompt-in-history/cordis.snapshot.yml @@ -8,7 +8,7 @@ name: '@deepseek-ai/dsh-agent-default-model' config: provider: deepseek-official - model: deepseek-v4-flash + model: deepseek-flash - id: session-persistence-jsonl name: '@deepseek-ai/dsh-session-persistence-jsonl' @@ -24,8 +24,8 @@ - id: deepseek-official name: DeepSeek models: - - id: deepseek-v4-flash + - id: deepseek-flash systemPromptUpdate: in-history - - id: deepseek-v4-pro + inputModalities: [text, image] - id: in-history-prompt-update name: '../../../packages/test-support/session-snapshot/tests/fixtures/in-history-prompt-update.ts' diff --git a/snapshots/session/system-prompt-in-history/cordis.yml b/snapshots/session/system-prompt-in-history/cordis.yml index 67380106a1..51196acfd0 100644 --- a/snapshots/session/system-prompt-in-history/cordis.yml +++ b/snapshots/session/system-prompt-in-history/cordis.yml @@ -7,16 +7,12 @@ config: thinking: enabled reasoningEffort: max - models: - - id: deepseek-v4-flash - systemPromptUpdate: in-history - - id: deepseek-v4-pro - id: agent-default-model name: '@deepseek-ai/dsh-agent-default-model' config: provider: deepseek-official - model: deepseek-v4-flash + model: deepseek-flash - id: session-persistence-jsonl name: '@deepseek-ai/dsh-session-persistence-jsonl' diff --git a/snapshots/session/system-prompt-in-history/session.v3.jsonl b/snapshots/session/system-prompt-in-history/session.v3.jsonl index 14d90db6e6..ddbbbc88b2 100644 --- a/snapshots/session/system-prompt-in-history/session.v3.jsonl +++ b/snapshots/session/system-prompt-in-history/session.v3.jsonl @@ -9,15 +9,15 @@ {"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"id":"{{message:2}}"}},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Read task.txt with the read tool, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}} -{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash","systemPromptUpdate":"in-history"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-flash"},"tools":"{{tools}}"},"reason":"initial"}} +{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-flash","systemPromptUpdate":"in-history"}} {"type":"session/title","data":{"title":"Read task.txt with the read","messageSeqs":[8],"source":{"kind":"fallback"}}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_task_read","name":"read","arguments":"{\"file_path\":\"task.txt\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"call_task_read","name":"read","args":["{\"file_path\":\"task.txt\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_task_read","name":"read","arguments":"{\"file_path\":\"task.txt\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_task_read","name":"read","arguments":"{\"file_path\":\"task.txt\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"call_task_read","name":"read","args":["{\"file_path\":\"task.txt\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_task_read","name":"read","arguments":"{\"file_path\":\"task.txt\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_task_read","name":"read","arguments":"{\"file_path\":\"task.txt\"}"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_task_read"},"content":[{"type":"tool-result","toolCallId":"call_task_read","content":[{"type":"text","text":"{{cwd}}/task.txt\nfile\n\n1: Reply with the single word DONE after reading this file.\n\n(End of file - total 1 lines)\n"}],"isError":false}],"role":"user","id":"{{message:5}}"},"meta":{"path":"{{cwd}}/task.txt","offset":1,"lines":[{"number":1,"text":"Reply with the single word DONE after reading this file."}],"totalLines":1}},"sourceEventSeqs":[14],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} {"type":"system/message","data":{"turn":1,"step":2,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"id":"{{message:6}}"}},"surfaceOp":"append"} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"usage":{"inputTokens":12,"outputTokens":2,"cacheReadTokens":10},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["DONE"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":12,"outputTokens":2,"cacheReadTokens":10}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-flash"},"id":"{{message:7}}"},"usage":{"inputTokens":12,"outputTokens":2,"cacheReadTokens":10},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["DONE"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":12,"outputTokens":2,"cacheReadTokens":10}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/session/system-prompt-in-history/system-prompt.expected.md b/snapshots/session/system-prompt-in-history/system-prompt.expected.md index 023bf213c7..02d5da51ae 100644 --- a/snapshots/session/system-prompt-in-history/system-prompt.expected.md +++ b/snapshots/session/system-prompt-in-history/system-prompt.expected.md @@ -1,6 +1,6 @@ You are an AI agent powered by DeepSeek Harness. -You are a coding assistant powered by the deepseek-v4-flash model. Your working directory is {{cwd}}. Your bash tool runs under a file sandbox — a `[sandbox: file access denied …]` result is policy, not a command bug. +You are a coding assistant powered by the deepseek-flash model. Your working directory is {{cwd}}. Your bash tool runs under a file sandbox — a `[sandbox: file access denied …]` result is policy, not a command bug. Verify your work by running the code or tests. Keep answers brief and factual. @@ -35,7 +35,7 @@ Use subagent in the background by default. Start independent delegations togethe You are an AI agent powered by DeepSeek Harness. -You are a coding assistant powered by the deepseek-v4-flash model. Your working directory is {{cwd}}. Your bash tool runs under a file sandbox — a `[sandbox: file access denied …]` result is policy, not a command bug. +You are a coding assistant powered by the deepseek-flash model. Your working directory is {{cwd}}. Your bash tool runs under a file sandbox — a `[sandbox: file access denied …]` result is policy, not a command bug. Verify your work by running the code or tests. Keep answers brief and factual. From 61c59543723d2be5682b5cb8aa33f08aeb9f6699 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Wed, 9 Sep 2026 22:51:55 +0800 Subject: [PATCH 22/27] test: pin model routes in webhook and Cordis fixtures --- apps/cli/tests/fixtures/github-webhook/cordis.yml | 6 ++++++ apps/web/tests/cordis-tool-round.e2e.ts | 1 + apps/web/tests/cordis-tool-round.overlay.yml | 5 +++++ 3 files changed, 12 insertions(+) create mode 100644 apps/web/tests/cordis-tool-round.overlay.yml diff --git a/apps/cli/tests/fixtures/github-webhook/cordis.yml b/apps/cli/tests/fixtures/github-webhook/cordis.yml index 465d90ee9f..ad5bcd8cb4 100644 --- a/apps/cli/tests/fixtures/github-webhook/cordis.yml +++ b/apps/cli/tests/fixtures/github-webhook/cordis.yml @@ -1,6 +1,12 @@ # Real-product test overlay: the CLI, provider, Web servers, webhook runtime, # adapter, rule, Workspace, Session, and Agent all remain production modules. +# Webhook delivery uses the same live model as the other real-API smokes. +- id: agent-default-model + config: + provider: deepseek-official + model: deepseek-v4-flash + - insert: - id: webhook-runtime name: '@deepseek-ai/dsh-webhook' diff --git a/apps/web/tests/cordis-tool-round.e2e.ts b/apps/web/tests/cordis-tool-round.e2e.ts index 1000bc0aaf..55435d86da 100644 --- a/apps/web/tests/cordis-tool-round.e2e.ts +++ b/apps/web/tests/cordis-tool-round.e2e.ts @@ -75,6 +75,7 @@ describe('web e2e: Cordis tools use their owned cards', () => { beforeAll(async () => { scaffold = await launchWebScaffold({ cordisTools: true, + extraOverlayPath: fileURLToPath(new URL('./cordis-tool-round.overlay.yml', import.meta.url)), compareReplaySession: true, ...(MODE === 'record' ? {} : { replayFixture: FIXTURE, paceMs: 15 }), }) diff --git a/apps/web/tests/cordis-tool-round.overlay.yml b/apps/web/tests/cordis-tool-round.overlay.yml new file mode 100644 index 0000000000..e22f8fc759 --- /dev/null +++ b/apps/web/tests/cordis-tool-round.overlay.yml @@ -0,0 +1,5 @@ +# Keep the recorded Cordis lifecycle on its original model route. +- id: agent-default-model + config: + provider: deepseek-official + model: deepseek-v4-flash From 719331ecb755846acb558fa42c76b6fdabb52104 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Wed, 9 Sep 2026 22:53:13 +0800 Subject: [PATCH 23/27] test: keep webhook smoke on the shipped model route --- apps/cli/tests/fixtures/github-webhook/cordis.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/apps/cli/tests/fixtures/github-webhook/cordis.yml b/apps/cli/tests/fixtures/github-webhook/cordis.yml index ad5bcd8cb4..465d90ee9f 100644 --- a/apps/cli/tests/fixtures/github-webhook/cordis.yml +++ b/apps/cli/tests/fixtures/github-webhook/cordis.yml @@ -1,12 +1,6 @@ # Real-product test overlay: the CLI, provider, Web servers, webhook runtime, # adapter, rule, Workspace, Session, and Agent all remain production modules. -# Webhook delivery uses the same live model as the other real-API smokes. -- id: agent-default-model - config: - provider: deepseek-official - model: deepseek-v4-flash - - insert: - id: webhook-runtime name: '@deepseek-ai/dsh-webhook' From c98fe6ecdd71ba25843991e0596dd5cfafdcaf91 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Wed, 9 Sep 2026 23:10:11 +0800 Subject: [PATCH 24/27] test(web): pin the recorded model before scenario overlays --- apps/web/tests/cordis-tool-round.e2e.ts | 1 - apps/web/tests/cordis-tool-round.overlay.yml | 5 ----- apps/web/tests/scaffold.ts | 4 ++++ 3 files changed, 4 insertions(+), 6 deletions(-) delete mode 100644 apps/web/tests/cordis-tool-round.overlay.yml diff --git a/apps/web/tests/cordis-tool-round.e2e.ts b/apps/web/tests/cordis-tool-round.e2e.ts index 55435d86da..1000bc0aaf 100644 --- a/apps/web/tests/cordis-tool-round.e2e.ts +++ b/apps/web/tests/cordis-tool-round.e2e.ts @@ -75,7 +75,6 @@ describe('web e2e: Cordis tools use their owned cards', () => { beforeAll(async () => { scaffold = await launchWebScaffold({ cordisTools: true, - extraOverlayPath: fileURLToPath(new URL('./cordis-tool-round.overlay.yml', import.meta.url)), compareReplaySession: true, ...(MODE === 'record' ? {} : { replayFixture: FIXTURE, paceMs: 15 }), }) diff --git a/apps/web/tests/cordis-tool-round.overlay.yml b/apps/web/tests/cordis-tool-round.overlay.yml deleted file mode 100644 index e22f8fc759..0000000000 --- a/apps/web/tests/cordis-tool-round.overlay.yml +++ /dev/null @@ -1,5 +0,0 @@ -# Keep the recorded Cordis lifecycle on its original model route. -- id: agent-default-model - config: - provider: deepseek-official - model: deepseek-v4-flash diff --git a/apps/web/tests/scaffold.ts b/apps/web/tests/scaffold.ts index 2d9b118753..d1586408fd 100644 --- a/apps/web/tests/scaffold.ts +++ b/apps/web/tests/scaffold.ts @@ -515,6 +515,10 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise Date: Wed, 9 Sep 2026 23:47:09 +0800 Subject: [PATCH 25/27] feat(llm): retain V4 models alongside V41 Flash --- .../default-models.expected.md | 76 +++++++++++++++++++ .../tests/onboarding-deepseek-config.e2e.ts | 13 +++- docs/config-catalog.i18n.yaml | 4 +- docs/config-catalog.md | 4 +- docs/config-catalog.zh.md | 4 +- packages/llm/llm-deepseek/README.i18n.yaml | 4 +- packages/llm/llm-deepseek/README.md | 4 +- packages/llm/llm-deepseek/README.zh.md | 4 +- packages/llm/llm-deepseek/src/index.ts | 14 +++- .../llm/llm-deepseek/tests/adapter.spec.ts | 46 ++++++++++- .../llm-deepseek/tests/dynamic-config.spec.ts | 4 +- 11 files changed, 157 insertions(+), 20 deletions(-) create mode 100644 apps/web/tests/expected/onboarding-deepseek-config/default-models.expected.md diff --git a/apps/web/tests/expected/onboarding-deepseek-config/default-models.expected.md b/apps/web/tests/expected/onboarding-deepseek-config/default-models.expected.md new file mode 100644 index 0000000000..c4380a0436 --- /dev/null +++ b/apps/web/tests/expected/onboarding-deepseek-config/default-models.expected.md @@ -0,0 +1,76 @@ +- dialog "设置": + - navigation: + - text: 设置 + - button "通用设置": + - img + - text: 通用设置 + - button "模型": + - img + - text: 模型 + - button "插件": + - img + - text: 插件 + - button "Agent 预设": + - img + - text: Agent 预设 + - button "打开配置文件" + - button "关闭": + - img + - text: 关闭 + - heading "模型" [level=2] + - paragraph: 填入各提供方的 API 密钥即可使用其模型。 + - list: + - listitem: + - text: DeepSeek + - img "API 密钥已配置" + - button "编辑 DeepSeek (deepseek-official)": 编辑 + - text: DeepSeek deepseek-official API 密钥 + - textbox "API 密钥": + - /placeholder: 已配置——输入新值可替换 + - group: + - text: 自定义设置 API 地址 + - textbox "API 地址": + - /placeholder: https://api.deepseek.com + - region "模型目录": + - text: 模型目录 正在使用适配器默认模型 + - textbox "模型 ID 1": + - /placeholder: 模型 ID + - text: deepseek-flash + - textbox "显示名称 1": + - /placeholder: 显示名称 + - text: DeepSeek-V41-Flash + - button "容量 1": + - img + - button "删除模型 1": + - img + - textbox "模型 ID 2": + - /placeholder: 模型 ID + - text: deepseek-v4-flash + - textbox "显示名称 2": + - /placeholder: 显示名称 + - text: DeepSeek-V4-Flash + - button "容量 2": + - img + - button "删除模型 2": + - img + - textbox "模型 ID 3": + - /placeholder: 模型 ID + - text: deepseek-v4-pro + - textbox "显示名称 3": + - /placeholder: 显示名称 + - text: DeepSeek-V4-Pro + - button "容量 3": + - img + - button "删除模型 3": + - img + - button "添加模型": + - img + - text: 添加模型 + - button "取消" + - button "保存" + - button "添加提供方": + - img + - text: 添加提供方 + - button "添加自定义提供方": + - img + - text: 添加自定义提供方 diff --git a/apps/web/tests/onboarding-deepseek-config.e2e.ts b/apps/web/tests/onboarding-deepseek-config.e2e.ts index 368c414c0a..94b41615e3 100644 --- a/apps/web/tests/onboarding-deepseek-config.e2e.ts +++ b/apps/web/tests/onboarding-deepseek-config.e2e.ts @@ -21,6 +21,7 @@ const SNAPSHOT_DIR = fileURLToPath(new URL('./expected/onboarding-deepseek-confi const WELCOME_EXPECTED = join(SNAPSHOT_DIR, 'welcome.expected.md') const MISSING_EXPECTED = join(SNAPSHOT_DIR, 'missing.expected.md') const MODELS_EXPECTED = join(SNAPSHOT_DIR, 'models.expected.md') +const DEFAULT_MODELS_EXPECTED = join(SNAPSHOT_DIR, 'default-models.expected.md') const MODE = webSnapshotMode() describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup', () => { @@ -204,7 +205,11 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup await settings.getByText('自定义设置').click() expect(await settings.getByLabel('模型 ID 1').inputValue()).toBe('deepseek-flash') expect(await settings.getByLabel('显示名称 1').inputValue()).toBe('DeepSeek-V41-Flash') - expect(await settings.getByRole('button', { name: /删除模型/ }).count()).toBe(1) + expect(await settings.getByLabel('模型 ID 2').inputValue()).toBe('deepseek-v4-flash') + expect(await settings.getByLabel('模型 ID 3').inputValue()).toBe('deepseek-v4-pro') + expect(await settings.getByRole('button', { name: /删除模型/ }).count()).toBe(3) + const defaultModels = await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd) + await compareOrRefreshGolden(DEFAULT_MODELS_EXPECTED, defaultModels, MODE) await settings.getByLabel('显示名称 1').fill('Configured Flash') await settings.getByRole('button', { name: '保存', exact: true }).click() await settings.getByLabel('模型 ID 1').waitFor({ state: 'detached', timeout: 15_000 }) @@ -219,7 +224,9 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup }) await deepSeek.locator('xpath=ancestor::li').getByRole('button', { name: '编辑' }).click() await settings.getByText('自定义设置').click() - await settings.getByRole('button', { name: /删除模型/ }).first().click() + for (let index = 0; index < 3; index++) { + await settings.getByRole('button', { name: /删除模型/ }).first().click() + } await settings.getByRole('button', { name: '添加模型' }).click() const customModelId = settings.getByLabel('模型 ID 1') await customModelId.fill('private-preview') @@ -263,7 +270,7 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup it('keeps the fixture inventory closed', async () => { await assertFixtureInventory( SNAPSHOT_DIR, - ['welcome.expected.md', 'missing.expected.md', 'models.expected.md'], + ['welcome.expected.md', 'missing.expected.md', 'models.expected.md', 'default-models.expected.md'], ) }) }) diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml index 7f8e520a32..0f276204f9 100644 --- a/docs/config-catalog.i18n.yaml +++ b/docs/config-catalog.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/config-catalog.md -config-catalog.md: 8f6907a878c878b7d6f5dc5d51fc51576af426f2 -config-catalog.zh.md: e3fc5ce54aad731a510bcfd51f04cfb5ed30bb2b +config-catalog.md: 628ed0fc8ddbb6d1463a9ae186112a8b1b6aa9df +config-catalog.zh.md: 95ffed68aa257574496d698153ac88b1efac656b diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 8f6907a878..628ed0fc8d 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -1037,7 +1037,7 @@ export interface Config { maxTokens?: number /** Positive context capacity used when the selected model has no exact value (default 1,000,000). */ defaultContextWindow?: number - /** Advisory models shown by discovery consumers; defaults to DeepSeek-V41-Flash. */ + /** Advisory models shown by discovery consumers; defaults to V41 Flash, V4 Flash, and V4 Pro. */ models?: DeepSeekCatalogModel[] /** Maximum provider idle time while one stream read is outstanding (default five minutes). */ streamIdleTimeoutMs?: number @@ -1094,7 +1094,7 @@ export interface DeepSeekCatalogModel { Depends on: [`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts) · [`SystemPromptUpdate`](../packages/llm/llm/src/index.ts) -Source: [`packages/llm/llm-deepseek/src/index.ts:114`](../packages/llm/llm-deepseek/src/index.ts) +Source: [`packages/llm/llm-deepseek/src/index.ts:126`](../packages/llm/llm-deepseek/src/index.ts) diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index e3fc5ce54a..95ffed68aa 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -1039,7 +1039,7 @@ export interface Config { maxTokens?: number /** Positive context capacity used when the selected model has no exact value (default 1,000,000). */ defaultContextWindow?: number - /** Advisory models shown by discovery consumers; defaults to DeepSeek-V41-Flash. */ + /** Advisory models shown by discovery consumers; defaults to V41 Flash, V4 Flash, and V4 Pro. */ models?: DeepSeekCatalogModel[] /** Maximum provider idle time while one stream read is outstanding (default five minutes). */ streamIdleTimeoutMs?: number @@ -1096,7 +1096,7 @@ export interface DeepSeekCatalogModel { 依赖:[`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts) · [`SystemPromptUpdate`](../packages/llm/llm/src/index.ts) -来源:[`packages/llm/llm-deepseek/src/index.ts:114`](../packages/llm/llm-deepseek/src/index.ts) +来源:[`packages/llm/llm-deepseek/src/index.ts:126`](../packages/llm/llm-deepseek/src/index.ts) diff --git a/packages/llm/llm-deepseek/README.i18n.yaml b/packages/llm/llm-deepseek/README.i18n.yaml index 077db2b487..b1177f588d 100644 --- a/packages/llm/llm-deepseek/README.i18n.yaml +++ b/packages/llm/llm-deepseek/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/llm/llm-deepseek/README.md -README.md: 19025b801943ecccd9a17a28870fc364fede7d69 -README.zh.md: 89fa42491aaa2f13ef09b2d0a70c5297cfb5c345 +README.md: 45bd4c1c939b6ea8da9be1b5a36fb12dc500afab +README.zh.md: aee12fbe0fe06e883ec27315a60dc3fc93bda471 diff --git a/packages/llm/llm-deepseek/README.md b/packages/llm/llm-deepseek/README.md index 19025b8019..45bd4c1c93 100644 --- a/packages/llm/llm-deepseek/README.md +++ b/packages/llm/llm-deepseek/README.md @@ -46,7 +46,7 @@ Choose this adapter when the deployment targets DeepSeek's official API, optiona filesApiTimeoutMs: 60000 ``` -A request selects the route with `provider: deepseek-official`; the model id passes through to the wire, so new DeepSeek models need no re-registration. Omitted `models` advertises only the text- and image-capable `deepseek-flash`, with a 1,000,000-token context window. An explicit list replaces those defaults, and unlisted model ids still pass through as text-only routes. Clients, including model discovery tools, can read the advisory entries through `ctx.llm.listModels('deepseek-official')`. Image-capable entries may set `imagePixelBudget` to a positive integer or `low`, and may set `imageMaxBytes`. An entry may declare `systemPromptUpdate: in-history` when its endpoint reads the latest `system` message at any position of `messages` as the complete effective system prompt; the adapter reports the mode on the resolved model and the prepared call, and the agent loop then appends a changed prompt after the cached history instead of rewriting the leading system message ([decision rule](../../core/agent-loop/README.md#understand-the-implementation)). The default `deepseek-flash` entry declares this mode; other models require an explicit `models` declaration, and any value other than `in-history` fails at load with `llm-deepseek: catalog model "" systemPromptUpdate must be "in-history" when present`. +A request selects the route with `provider: deepseek-official`; the model id passes through to the wire, so new DeepSeek models need no re-registration. Omitted `models` advertises the text- and image-capable `deepseek-flash` alongside the text-only `deepseek-v4-flash` and `deepseek-v4-pro`, each with a 1,000,000-token context window. An explicit list replaces those defaults, and unlisted model ids still pass through as text-only routes. Clients, including model discovery tools, can read the advisory entries through `ctx.llm.listModels('deepseek-official')`. Image-capable entries may set `imagePixelBudget` to a positive integer or `low`, and may set `imageMaxBytes`. An entry may declare `systemPromptUpdate: in-history` when its endpoint reads the latest `system` message at any position of `messages` as the complete effective system prompt; the adapter reports the mode on the resolved model and the prepared call, and the agent loop then appends a changed prompt after the cached history instead of rewriting the leading system message ([decision rule](../../core/agent-loop/README.md#understand-the-implementation)). The default `deepseek-flash` entry declares this mode; other models require an explicit `models` declaration, and any value other than `in-history` fails at load with `llm-deepseek: catalog model "" systemPromptUpdate must be "in-history" when present`. | Field | Default | Meaning | |---|---|---| @@ -56,7 +56,7 @@ A request selects the route with `provider: deepseek-official`; the model id pas | `reasoningEffort` | `high` | Default effort: `off`, `low`, `high`, or `max` | | `maxTokens` | `256,000` | Per-request output cap; a model's own cap and explicit request values win | | `defaultContextWindow` | `1,000,000` | Capacity fallback for models without an exact value | -| `models` | DeepSeek-V41-Flash | Advisory catalog shown by discovery consumers | +| `models` | V41 Flash + V4 Flash + V4 Pro | Advisory catalog shown by discovery consumers | | `streamIdleTimeoutMs` | `300,000` | Maximum provider idle time per outstanding stream read | | `maxRequestFilesBytes` | `128 MiB` | High watermark for retained request-image bytes before oldest-first offload | | `maxInlineRequestImageBytes` | `20 MiB` | Independent base64 fallback high watermark | diff --git a/packages/llm/llm-deepseek/README.zh.md b/packages/llm/llm-deepseek/README.zh.md index 89fa42491a..aee12fbe0f 100644 --- a/packages/llm/llm-deepseek/README.zh.md +++ b/packages/llm/llm-deepseek/README.zh.md @@ -46,7 +46,7 @@ kind: "package-reference" filesApiTimeoutMs: 60000 ``` -请求用 `provider: deepseek-official` 选择路由;模型 id 原样传到协议,因此新增 DeepSeek 模型无需重新注册。省略 `models` 时仅公布支持文本和图像的 `deepseek-flash`,上下文窗口为 1,000,000 token。显式列表会替换这些默认值,未列出的模型 id 仍作为纯文本路由原样通过。包括模型发现工具在内的客户端可通过 `ctx.llm.listModels('deepseek-official')` 读取这些建议性条目。支持图片的条目可把 `imagePixelBudget` 设置为正整数或 `low`,也可以设置 `imageMaxBytes`。当端点把 `messages` 中任意位置最新的 `system` 消息读作完整的有效系统提示词时,条目可以声明 `systemPromptUpdate: in-history`;适配器会在已解析模型与已准备调用上报告该模式,agent loop(智能体循环)随后把变化后的提示词追加到已缓存历史之后,而不是改写开头的 system 消息([决策规则](../../core/agent-loop/README.zh.md#understand-the-implementation))。默认的 `deepseek-flash` 条目声明该模式;其他模型需通过 `models` 显式声明,`in-history` 以外的任何值都会在加载时以 `llm-deepseek: catalog model "" systemPromptUpdate must be "in-history" when present` 失败。 +请求用 `provider: deepseek-official` 选择路由;模型 id 原样传到协议,因此新增 DeepSeek 模型无需重新注册。省略 `models` 时公布支持文本和图像的 `deepseek-flash`,以及仅支持文本的 `deepseek-v4-flash` 和 `deepseek-v4-pro`,各自的上下文窗口均为 1,000,000 token。显式列表会替换这些默认值,未列出的模型 id 仍作为纯文本路由原样通过。包括模型发现工具在内的客户端可通过 `ctx.llm.listModels('deepseek-official')` 读取这些建议性条目。支持图片的条目可把 `imagePixelBudget` 设置为正整数或 `low`,也可以设置 `imageMaxBytes`。当端点把 `messages` 中任意位置最新的 `system` 消息读作完整的有效系统提示词时,条目可以声明 `systemPromptUpdate: in-history`;适配器会在已解析模型与已准备调用上报告该模式,agent loop(智能体循环)随后把变化后的提示词追加到已缓存历史之后,而不是改写开头的 system 消息([决策规则](../../core/agent-loop/README.zh.md#understand-the-implementation))。默认的 `deepseek-flash` 条目声明该模式;其他模型需通过 `models` 显式声明,`in-history` 以外的任何值都会在加载时以 `llm-deepseek: catalog model "" systemPromptUpdate must be "in-history" when present` 失败。 | 字段 | 默认值 | 含义 | |---|---|---| @@ -56,7 +56,7 @@ kind: "package-reference" | `reasoningEffort` | `high` | 默认强度:`off`、`low`、`high` 或 `max` | | `maxTokens` | `256,000` | 单次请求输出上限;模型自身上限与显式请求值优先 | | `defaultContextWindow` | `1,000,000` | 无精确值模型的容量回退 | -| `models` | DeepSeek-V41-Flash | 供发现消费方查看的建议性目录 | +| `models` | V41 Flash + V4 Flash + V4 Pro | 供发现消费方查看的建议性目录 | | `streamIdleTimeoutMs` | `300,000` | 单次流读取未完成的最大提供方空闲时间 | | `maxRequestFilesBytes` | `128 MiB` | 按最旧优先卸载前保留的请求图片字节高水位 | | `maxInlineRequestImageBytes` | `20 MiB` | 独立的 base64 回退高水位 | diff --git a/packages/llm/llm-deepseek/src/index.ts b/packages/llm/llm-deepseek/src/index.ts index c0058fd340..5830790cb0 100644 --- a/packages/llm/llm-deepseek/src/index.ts +++ b/packages/llm/llm-deepseek/src/index.ts @@ -99,6 +99,18 @@ const DEFAULT_MODELS: DeepSeekCatalogModel[] = [ imageMaxBytes: DEFAULT_REQUEST_IMAGE_MAX_BYTES, systemPromptUpdate: 'in-history', }, + { + id: 'deepseek-v4-flash', + name: 'DeepSeek-V4-Flash', + description: 'Fast, efficient, and economical; suited to focused, routine, or parallel tasks.', + contextWindow: DEFAULT_CONTEXT_WINDOW, + }, + { + id: 'deepseek-v4-pro', + name: 'DeepSeek-V4-Pro', + description: 'Stronger agentic coding, knowledge, and difficult reasoning; suited to complex or quality-critical tasks at higher cost.', + contextWindow: DEFAULT_CONTEXT_WINDOW, + }, ] const MODEL_MODALITIES = ['text', 'image'] as const satisfies readonly ModelModality[] @@ -124,7 +136,7 @@ export interface Config { maxTokens?: number /** Positive context capacity used when the selected model has no exact value (default 1,000,000). */ defaultContextWindow?: number - /** Advisory models shown by discovery consumers; defaults to DeepSeek-V41-Flash. */ + /** Advisory models shown by discovery consumers; defaults to V41 Flash, V4 Flash, and V4 Pro. */ models?: DeepSeekCatalogModel[] /** Maximum provider idle time while one stream read is outstanding (default five minutes). */ streamIdleTimeoutMs?: number diff --git a/packages/llm/llm-deepseek/tests/adapter.spec.ts b/packages/llm/llm-deepseek/tests/adapter.spec.ts index b3870a2290..bc399c57f8 100644 --- a/packages/llm/llm-deepseek/tests/adapter.spec.ts +++ b/packages/llm/llm-deepseek/tests/adapter.spec.ts @@ -1698,6 +1698,20 @@ describe('plugin registration and config', () => { expect(ctx.llm.listProviders()).toEqual([{ id: 'deepseek-official', name: 'DeepSeek' }]) await expect(ctx.llm.listModels('deepseek-official')).resolves.toEqual([ { provider: 'deepseek-official', id: 'deepseek-flash', name: 'DeepSeek-V41-Flash', inputModalities: ['text', 'image'] }, + { + provider: 'deepseek-official', + id: 'deepseek-v4-flash', + name: 'DeepSeek-V4-Flash', + description: 'Fast, efficient, and economical; suited to focused, routine, or parallel tasks.', + inputModalities: ['text'], + }, + { + provider: 'deepseek-official', + id: 'deepseek-v4-pro', + name: 'DeepSeek-V4-Pro', + description: 'Stronger agentic coding, knowledge, and difficult reasoning; suited to complex or quality-critical tasks at higher cost.', + inputModalities: ['text'], + }, ]) await expect(ctx.llm.resolveModelInfo('deepseek-official', 'deepseek-flash')) .resolves.toMatchObject({ @@ -1720,6 +1734,20 @@ describe('plugin registration and config', () => { }) }) + it.each(['deepseek-v4-flash', 'deepseek-v4-pro'])('keeps %s available with its V4 capabilities', async (model) => { + const ctx = new Context() + await ctx.plugin(LlmRuntime) + await ctx.plugin(LlmDeepSeek, { baseURL: 'http://127.0.0.1:1' }) + const info = await ctx.llm.resolveModelInfo('deepseek-official', model) + expect(info).toMatchObject({ + id: model, + inputModalities: ['text'], + context: { contextWindow: 1_000_000 }, + defaultMaxTokens: 256_000, + }) + expect(info?.systemPromptUpdate).toBeUndefined() + }) + it.each(['off', 'low', 'max'] as const)('uses the configured %s reasoning default', async (effort) => { const ctx = new Context() await ctx.plugin(LlmRuntime) @@ -1804,6 +1832,20 @@ describe('plugin registration and config', () => { LlmDeepSeek.apply(ctx, { baseURL: 'http://127.0.0.1:1' }) await expect(ctx.llm.listModels('deepseek-official')).resolves.toEqual([ { provider: 'deepseek-official', id: 'deepseek-flash', name: 'DeepSeek-V41-Flash', inputModalities: ['text', 'image'] }, + { + provider: 'deepseek-official', + id: 'deepseek-v4-flash', + name: 'DeepSeek-V4-Flash', + description: 'Fast, efficient, and economical; suited to focused, routine, or parallel tasks.', + inputModalities: ['text'], + }, + { + provider: 'deepseek-official', + id: 'deepseek-v4-pro', + name: 'DeepSeek-V4-Pro', + description: 'Stronger agentic coding, knowledge, and difficult reasoning; suited to complex or quality-critical tasks at higher cost.', + inputModalities: ['text'], + }, ]) }) @@ -2119,7 +2161,7 @@ describe('plugin registration and config', () => { // First-boot onboarding: the route registers so models stay discoverable; // only the request itself needs a key. expect(ctx.llm.listProviders()).toEqual([{ id: 'deepseek-official', name: 'DeepSeek' }]) - await expect(ctx.llm.listModels('deepseek-official')).resolves.toHaveLength(1) + await expect(ctx.llm.listModels('deepseek-official')).resolves.toHaveLength(3) const first = await assemble(ctx, { model: 'deepseek-v4-flash', messages: [] }) expect(first.finish).toMatchObject({ kind: 'error', failure: { code: 'MISSING_CREDENTIAL' } }) // The guidance leads with the managed credential store. @@ -2207,7 +2249,7 @@ describe('plugin registration and config', () => { expect(adapter).toBeInstanceOf(DeepSeekAdapter) // Direct embedding shares the plugin's one resolve step, so it advertises // the same default catalog instead of a divergent empty one. - await expect(adapter.listModels('deepseek-official')).resolves.toHaveLength(1) + await expect(adapter.listModels('deepseek-official')).resolves.toHaveLength(3) }) it('resolves connection facts and the credential exactly once per stream call', async () => { diff --git a/packages/llm/llm-deepseek/tests/dynamic-config.spec.ts b/packages/llm/llm-deepseek/tests/dynamic-config.spec.ts index f8857b5bb7..b6992336ee 100644 --- a/packages/llm/llm-deepseek/tests/dynamic-config.spec.ts +++ b/packages/llm/llm-deepseek/tests/dynamic-config.spec.ts @@ -191,7 +191,7 @@ describe('request-level dynamic configuration', () => { const dir = await home() const { ctx } = await boot(dir, { baseURL: 'http://127.0.0.1:1' }) - await expect(ctx.llm.listModels('deepseek-official')).resolves.toHaveLength(1) + await expect(ctx.llm.listModels('deepseek-official')).resolves.toHaveLength(3) await ctx.settings.update(NS, { models: [{ id: 'settings-model', name: 'From Settings', inputModalities: ['text', 'image'] }], }) @@ -261,7 +261,7 @@ describe('request-level dynamic configuration', () => { // Schema-valid but resolver-invalid: duplicate catalog ids pass the array // schema and fail the explicit resolve step. await ctx.settings.update(NS, { models: [{ id: 'dup' }, { id: 'dup' }] }) - await expect(ctx.llm.listModels('deepseek-official')).resolves.toHaveLength(1) + await expect(ctx.llm.listModels('deepseek-official')).resolves.toHaveLength(3) await ctx.settings.update(NS, { models: [{ id: 'recovered' }] }) await expect(ctx.llm.listModels('deepseek-official')).resolves.toEqual([ { provider: 'deepseek-official', id: 'recovered', name: 'recovered', inputModalities: ['text'] }, From 0729dbec6668b6502e9d3512101a5fbb031236bc Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Wed, 9 Sep 2026 23:58:04 +0800 Subject: [PATCH 26/27] feat(llm): restore V4 Flash Vision Exp catalog entry --- .../default-models.expected.md | 10 ++++++++++ apps/web/tests/onboarding-deepseek-config.e2e.ts | 5 +++-- docs/config-catalog.i18n.yaml | 4 ++-- docs/config-catalog.md | 4 ++-- docs/config-catalog.zh.md | 4 ++-- packages/llm/llm-deepseek/README.i18n.yaml | 4 ++-- packages/llm/llm-deepseek/README.md | 4 ++-- packages/llm/llm-deepseek/README.zh.md | 4 ++-- packages/llm/llm-deepseek/src/index.ts | 10 +++++++++- packages/llm/llm-deepseek/tests/adapter.spec.ts | 14 ++++++++++---- .../llm/llm-deepseek/tests/dynamic-config.spec.ts | 4 ++-- 11 files changed, 46 insertions(+), 21 deletions(-) diff --git a/apps/web/tests/expected/onboarding-deepseek-config/default-models.expected.md b/apps/web/tests/expected/onboarding-deepseek-config/default-models.expected.md index c4380a0436..25e8ef46a0 100644 --- a/apps/web/tests/expected/onboarding-deepseek-config/default-models.expected.md +++ b/apps/web/tests/expected/onboarding-deepseek-config/default-models.expected.md @@ -63,6 +63,16 @@ - img - button "删除模型 3": - img + - textbox "模型 ID 4": + - /placeholder: 模型 ID + - text: deepseek-v4-flash-vision-exp + - textbox "显示名称 4": + - /placeholder: 显示名称 + - text: DeepSeek-V4-Flash-Vision-Exp + - button "容量 4": + - img + - button "删除模型 4": + - img - button "添加模型": - img - text: 添加模型 diff --git a/apps/web/tests/onboarding-deepseek-config.e2e.ts b/apps/web/tests/onboarding-deepseek-config.e2e.ts index 94b41615e3..e2d24ed599 100644 --- a/apps/web/tests/onboarding-deepseek-config.e2e.ts +++ b/apps/web/tests/onboarding-deepseek-config.e2e.ts @@ -207,7 +207,8 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup expect(await settings.getByLabel('显示名称 1').inputValue()).toBe('DeepSeek-V41-Flash') expect(await settings.getByLabel('模型 ID 2').inputValue()).toBe('deepseek-v4-flash') expect(await settings.getByLabel('模型 ID 3').inputValue()).toBe('deepseek-v4-pro') - expect(await settings.getByRole('button', { name: /删除模型/ }).count()).toBe(3) + expect(await settings.getByLabel('模型 ID 4').inputValue()).toBe('deepseek-v4-flash-vision-exp') + expect(await settings.getByRole('button', { name: /删除模型/ }).count()).toBe(4) const defaultModels = await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd) await compareOrRefreshGolden(DEFAULT_MODELS_EXPECTED, defaultModels, MODE) await settings.getByLabel('显示名称 1').fill('Configured Flash') @@ -224,7 +225,7 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup }) await deepSeek.locator('xpath=ancestor::li').getByRole('button', { name: '编辑' }).click() await settings.getByText('自定义设置').click() - for (let index = 0; index < 3; index++) { + for (let index = 0; index < 4; index++) { await settings.getByRole('button', { name: /删除模型/ }).first().click() } await settings.getByRole('button', { name: '添加模型' }).click() diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml index 0f276204f9..d3235e1203 100644 --- a/docs/config-catalog.i18n.yaml +++ b/docs/config-catalog.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/config-catalog.md -config-catalog.md: 628ed0fc8ddbb6d1463a9ae186112a8b1b6aa9df -config-catalog.zh.md: 95ffed68aa257574496d698153ac88b1efac656b +config-catalog.md: 79878fc8daaa5ba3e64d4ffb27d279ff172b6749 +config-catalog.zh.md: f254241618d280acaf738cc011ba1f7c5a2b6b21 diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 628ed0fc8d..79878fc8da 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -1037,7 +1037,7 @@ export interface Config { maxTokens?: number /** Positive context capacity used when the selected model has no exact value (default 1,000,000). */ defaultContextWindow?: number - /** Advisory models shown by discovery consumers; defaults to V41 Flash, V4 Flash, and V4 Pro. */ + /** Advisory models shown by discovery consumers; defaults to V41 Flash, V4 Flash, V4 Pro, and V4 Flash Vision Exp. */ models?: DeepSeekCatalogModel[] /** Maximum provider idle time while one stream read is outstanding (default five minutes). */ streamIdleTimeoutMs?: number @@ -1094,7 +1094,7 @@ export interface DeepSeekCatalogModel { Depends on: [`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts) · [`SystemPromptUpdate`](../packages/llm/llm/src/index.ts) -Source: [`packages/llm/llm-deepseek/src/index.ts:126`](../packages/llm/llm-deepseek/src/index.ts) +Source: [`packages/llm/llm-deepseek/src/index.ts:134`](../packages/llm/llm-deepseek/src/index.ts) diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index 95ffed68aa..f254241618 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -1039,7 +1039,7 @@ export interface Config { maxTokens?: number /** Positive context capacity used when the selected model has no exact value (default 1,000,000). */ defaultContextWindow?: number - /** Advisory models shown by discovery consumers; defaults to V41 Flash, V4 Flash, and V4 Pro. */ + /** Advisory models shown by discovery consumers; defaults to V41 Flash, V4 Flash, V4 Pro, and V4 Flash Vision Exp. */ models?: DeepSeekCatalogModel[] /** Maximum provider idle time while one stream read is outstanding (default five minutes). */ streamIdleTimeoutMs?: number @@ -1096,7 +1096,7 @@ export interface DeepSeekCatalogModel { 依赖:[`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts) · [`SystemPromptUpdate`](../packages/llm/llm/src/index.ts) -来源:[`packages/llm/llm-deepseek/src/index.ts:126`](../packages/llm/llm-deepseek/src/index.ts) +来源:[`packages/llm/llm-deepseek/src/index.ts:134`](../packages/llm/llm-deepseek/src/index.ts) diff --git a/packages/llm/llm-deepseek/README.i18n.yaml b/packages/llm/llm-deepseek/README.i18n.yaml index b1177f588d..bfd1fb2394 100644 --- a/packages/llm/llm-deepseek/README.i18n.yaml +++ b/packages/llm/llm-deepseek/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/llm/llm-deepseek/README.md -README.md: 45bd4c1c939b6ea8da9be1b5a36fb12dc500afab -README.zh.md: aee12fbe0fe06e883ec27315a60dc3fc93bda471 +README.md: 73992b3d5e6b72215cb3994881c361147c6885d8 +README.zh.md: 195d66206d02c309ca661f71dfe6e1005d9bc736 diff --git a/packages/llm/llm-deepseek/README.md b/packages/llm/llm-deepseek/README.md index 45bd4c1c93..73992b3d5e 100644 --- a/packages/llm/llm-deepseek/README.md +++ b/packages/llm/llm-deepseek/README.md @@ -46,7 +46,7 @@ Choose this adapter when the deployment targets DeepSeek's official API, optiona filesApiTimeoutMs: 60000 ``` -A request selects the route with `provider: deepseek-official`; the model id passes through to the wire, so new DeepSeek models need no re-registration. Omitted `models` advertises the text- and image-capable `deepseek-flash` alongside the text-only `deepseek-v4-flash` and `deepseek-v4-pro`, each with a 1,000,000-token context window. An explicit list replaces those defaults, and unlisted model ids still pass through as text-only routes. Clients, including model discovery tools, can read the advisory entries through `ctx.llm.listModels('deepseek-official')`. Image-capable entries may set `imagePixelBudget` to a positive integer or `low`, and may set `imageMaxBytes`. An entry may declare `systemPromptUpdate: in-history` when its endpoint reads the latest `system` message at any position of `messages` as the complete effective system prompt; the adapter reports the mode on the resolved model and the prepared call, and the agent loop then appends a changed prompt after the cached history instead of rewriting the leading system message ([decision rule](../../core/agent-loop/README.md#understand-the-implementation)). The default `deepseek-flash` entry declares this mode; other models require an explicit `models` declaration, and any value other than `in-history` fails at load with `llm-deepseek: catalog model "" systemPromptUpdate must be "in-history" when present`. +A request selects the route with `provider: deepseek-official`; the model id passes through to the wire, so new DeepSeek models need no re-registration. Omitted `models` advertises the text- and image-capable `deepseek-flash` and `deepseek-v4-flash-vision-exp` alongside the text-only `deepseek-v4-flash` and `deepseek-v4-pro`, each with a 1,000,000-token context window. An explicit list replaces those defaults, and unlisted model ids still pass through as text-only routes. Clients, including model discovery tools, can read the advisory entries through `ctx.llm.listModels('deepseek-official')`. Image-capable entries may set `imagePixelBudget` to a positive integer or `low`, and may set `imageMaxBytes`. An entry may declare `systemPromptUpdate: in-history` when its endpoint reads the latest `system` message at any position of `messages` as the complete effective system prompt; the adapter reports the mode on the resolved model and the prepared call, and the agent loop then appends a changed prompt after the cached history instead of rewriting the leading system message ([decision rule](../../core/agent-loop/README.md#understand-the-implementation)). The default `deepseek-flash` entry declares this mode; other models require an explicit `models` declaration, and any value other than `in-history` fails at load with `llm-deepseek: catalog model "" systemPromptUpdate must be "in-history" when present`. | Field | Default | Meaning | |---|---|---| @@ -56,7 +56,7 @@ A request selects the route with `provider: deepseek-official`; the model id pas | `reasoningEffort` | `high` | Default effort: `off`, `low`, `high`, or `max` | | `maxTokens` | `256,000` | Per-request output cap; a model's own cap and explicit request values win | | `defaultContextWindow` | `1,000,000` | Capacity fallback for models without an exact value | -| `models` | V41 Flash + V4 Flash + V4 Pro | Advisory catalog shown by discovery consumers | +| `models` | V41 Flash + V4 Flash + V4 Pro + V4 Flash Vision Exp | Advisory catalog shown by discovery consumers | | `streamIdleTimeoutMs` | `300,000` | Maximum provider idle time per outstanding stream read | | `maxRequestFilesBytes` | `128 MiB` | High watermark for retained request-image bytes before oldest-first offload | | `maxInlineRequestImageBytes` | `20 MiB` | Independent base64 fallback high watermark | diff --git a/packages/llm/llm-deepseek/README.zh.md b/packages/llm/llm-deepseek/README.zh.md index aee12fbe0f..195d66206d 100644 --- a/packages/llm/llm-deepseek/README.zh.md +++ b/packages/llm/llm-deepseek/README.zh.md @@ -46,7 +46,7 @@ kind: "package-reference" filesApiTimeoutMs: 60000 ``` -请求用 `provider: deepseek-official` 选择路由;模型 id 原样传到协议,因此新增 DeepSeek 模型无需重新注册。省略 `models` 时公布支持文本和图像的 `deepseek-flash`,以及仅支持文本的 `deepseek-v4-flash` 和 `deepseek-v4-pro`,各自的上下文窗口均为 1,000,000 token。显式列表会替换这些默认值,未列出的模型 id 仍作为纯文本路由原样通过。包括模型发现工具在内的客户端可通过 `ctx.llm.listModels('deepseek-official')` 读取这些建议性条目。支持图片的条目可把 `imagePixelBudget` 设置为正整数或 `low`,也可以设置 `imageMaxBytes`。当端点把 `messages` 中任意位置最新的 `system` 消息读作完整的有效系统提示词时,条目可以声明 `systemPromptUpdate: in-history`;适配器会在已解析模型与已准备调用上报告该模式,agent loop(智能体循环)随后把变化后的提示词追加到已缓存历史之后,而不是改写开头的 system 消息([决策规则](../../core/agent-loop/README.zh.md#understand-the-implementation))。默认的 `deepseek-flash` 条目声明该模式;其他模型需通过 `models` 显式声明,`in-history` 以外的任何值都会在加载时以 `llm-deepseek: catalog model "" systemPromptUpdate must be "in-history" when present` 失败。 +请求用 `provider: deepseek-official` 选择路由;模型 id 原样传到协议,因此新增 DeepSeek 模型无需重新注册。省略 `models` 时公布支持文本和图像的 `deepseek-flash` 和 `deepseek-v4-flash-vision-exp`,以及仅支持文本的 `deepseek-v4-flash` 和 `deepseek-v4-pro`,各自的上下文窗口均为 1,000,000 token。显式列表会替换这些默认值,未列出的模型 id 仍作为纯文本路由原样通过。包括模型发现工具在内的客户端可通过 `ctx.llm.listModels('deepseek-official')` 读取这些建议性条目。支持图片的条目可把 `imagePixelBudget` 设置为正整数或 `low`,也可以设置 `imageMaxBytes`。当端点把 `messages` 中任意位置最新的 `system` 消息读作完整的有效系统提示词时,条目可以声明 `systemPromptUpdate: in-history`;适配器会在已解析模型与已准备调用上报告该模式,agent loop(智能体循环)随后把变化后的提示词追加到已缓存历史之后,而不是改写开头的 system 消息([决策规则](../../core/agent-loop/README.zh.md#understand-the-implementation))。默认的 `deepseek-flash` 条目声明该模式;其他模型需通过 `models` 显式声明,`in-history` 以外的任何值都会在加载时以 `llm-deepseek: catalog model "" systemPromptUpdate must be "in-history" when present` 失败。 | 字段 | 默认值 | 含义 | |---|---|---| @@ -56,7 +56,7 @@ kind: "package-reference" | `reasoningEffort` | `high` | 默认强度:`off`、`low`、`high` 或 `max` | | `maxTokens` | `256,000` | 单次请求输出上限;模型自身上限与显式请求值优先 | | `defaultContextWindow` | `1,000,000` | 无精确值模型的容量回退 | -| `models` | V41 Flash + V4 Flash + V4 Pro | 供发现消费方查看的建议性目录 | +| `models` | V41 Flash + V4 Flash + V4 Pro + V4 Flash Vision Exp | 供发现消费方查看的建议性目录 | | `streamIdleTimeoutMs` | `300,000` | 单次流读取未完成的最大提供方空闲时间 | | `maxRequestFilesBytes` | `128 MiB` | 按最旧优先卸载前保留的请求图片字节高水位 | | `maxInlineRequestImageBytes` | `20 MiB` | 独立的 base64 回退高水位 | diff --git a/packages/llm/llm-deepseek/src/index.ts b/packages/llm/llm-deepseek/src/index.ts index 5830790cb0..d536fc1f9b 100644 --- a/packages/llm/llm-deepseek/src/index.ts +++ b/packages/llm/llm-deepseek/src/index.ts @@ -111,6 +111,14 @@ const DEFAULT_MODELS: DeepSeekCatalogModel[] = [ description: 'Stronger agentic coding, knowledge, and difficult reasoning; suited to complex or quality-critical tasks at higher cost.', contextWindow: DEFAULT_CONTEXT_WINDOW, }, + { + id: 'deepseek-v4-flash-vision-exp', + name: 'DeepSeek-V4-Flash-Vision-Exp', + contextWindow: DEFAULT_CONTEXT_WINDOW, + inputModalities: ['text', 'image'], + imagePixelBudget: DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET, + imageMaxBytes: DEFAULT_REQUEST_IMAGE_MAX_BYTES, + }, ] const MODEL_MODALITIES = ['text', 'image'] as const satisfies readonly ModelModality[] @@ -136,7 +144,7 @@ export interface Config { maxTokens?: number /** Positive context capacity used when the selected model has no exact value (default 1,000,000). */ defaultContextWindow?: number - /** Advisory models shown by discovery consumers; defaults to V41 Flash, V4 Flash, and V4 Pro. */ + /** Advisory models shown by discovery consumers; defaults to V41 Flash, V4 Flash, V4 Pro, and V4 Flash Vision Exp. */ models?: DeepSeekCatalogModel[] /** Maximum provider idle time while one stream read is outstanding (default five minutes). */ streamIdleTimeoutMs?: number diff --git a/packages/llm/llm-deepseek/tests/adapter.spec.ts b/packages/llm/llm-deepseek/tests/adapter.spec.ts index bc399c57f8..fefe4815b4 100644 --- a/packages/llm/llm-deepseek/tests/adapter.spec.ts +++ b/packages/llm/llm-deepseek/tests/adapter.spec.ts @@ -1712,6 +1712,7 @@ describe('plugin registration and config', () => { description: 'Stronger agentic coding, knowledge, and difficult reasoning; suited to complex or quality-critical tasks at higher cost.', inputModalities: ['text'], }, + { provider: 'deepseek-official', id: 'deepseek-v4-flash-vision-exp', name: 'DeepSeek-V4-Flash-Vision-Exp', inputModalities: ['text', 'image'] }, ]) await expect(ctx.llm.resolveModelInfo('deepseek-official', 'deepseek-flash')) .resolves.toMatchObject({ @@ -1734,14 +1735,18 @@ describe('plugin registration and config', () => { }) }) - it.each(['deepseek-v4-flash', 'deepseek-v4-pro'])('keeps %s available with its V4 capabilities', async (model) => { + it.each([ + { model: 'deepseek-v4-flash', inputModalities: ['text'] }, + { model: 'deepseek-v4-pro', inputModalities: ['text'] }, + { model: 'deepseek-v4-flash-vision-exp', inputModalities: ['text', 'image'] }, + ])('keeps $model available with its V4 capabilities', async ({ model, inputModalities }) => { const ctx = new Context() await ctx.plugin(LlmRuntime) await ctx.plugin(LlmDeepSeek, { baseURL: 'http://127.0.0.1:1' }) const info = await ctx.llm.resolveModelInfo('deepseek-official', model) expect(info).toMatchObject({ id: model, - inputModalities: ['text'], + inputModalities, context: { contextWindow: 1_000_000 }, defaultMaxTokens: 256_000, }) @@ -1846,6 +1851,7 @@ describe('plugin registration and config', () => { description: 'Stronger agentic coding, knowledge, and difficult reasoning; suited to complex or quality-critical tasks at higher cost.', inputModalities: ['text'], }, + { provider: 'deepseek-official', id: 'deepseek-v4-flash-vision-exp', name: 'DeepSeek-V4-Flash-Vision-Exp', inputModalities: ['text', 'image'] }, ]) }) @@ -2161,7 +2167,7 @@ describe('plugin registration and config', () => { // First-boot onboarding: the route registers so models stay discoverable; // only the request itself needs a key. expect(ctx.llm.listProviders()).toEqual([{ id: 'deepseek-official', name: 'DeepSeek' }]) - await expect(ctx.llm.listModels('deepseek-official')).resolves.toHaveLength(3) + await expect(ctx.llm.listModels('deepseek-official')).resolves.toHaveLength(4) const first = await assemble(ctx, { model: 'deepseek-v4-flash', messages: [] }) expect(first.finish).toMatchObject({ kind: 'error', failure: { code: 'MISSING_CREDENTIAL' } }) // The guidance leads with the managed credential store. @@ -2249,7 +2255,7 @@ describe('plugin registration and config', () => { expect(adapter).toBeInstanceOf(DeepSeekAdapter) // Direct embedding shares the plugin's one resolve step, so it advertises // the same default catalog instead of a divergent empty one. - await expect(adapter.listModels('deepseek-official')).resolves.toHaveLength(3) + await expect(adapter.listModels('deepseek-official')).resolves.toHaveLength(4) }) it('resolves connection facts and the credential exactly once per stream call', async () => { diff --git a/packages/llm/llm-deepseek/tests/dynamic-config.spec.ts b/packages/llm/llm-deepseek/tests/dynamic-config.spec.ts index b6992336ee..ffac6880e5 100644 --- a/packages/llm/llm-deepseek/tests/dynamic-config.spec.ts +++ b/packages/llm/llm-deepseek/tests/dynamic-config.spec.ts @@ -191,7 +191,7 @@ describe('request-level dynamic configuration', () => { const dir = await home() const { ctx } = await boot(dir, { baseURL: 'http://127.0.0.1:1' }) - await expect(ctx.llm.listModels('deepseek-official')).resolves.toHaveLength(3) + await expect(ctx.llm.listModels('deepseek-official')).resolves.toHaveLength(4) await ctx.settings.update(NS, { models: [{ id: 'settings-model', name: 'From Settings', inputModalities: ['text', 'image'] }], }) @@ -261,7 +261,7 @@ describe('request-level dynamic configuration', () => { // Schema-valid but resolver-invalid: duplicate catalog ids pass the array // schema and fail the explicit resolve step. await ctx.settings.update(NS, { models: [{ id: 'dup' }, { id: 'dup' }] }) - await expect(ctx.llm.listModels('deepseek-official')).resolves.toHaveLength(3) + await expect(ctx.llm.listModels('deepseek-official')).resolves.toHaveLength(4) await ctx.settings.update(NS, { models: [{ id: 'recovered' }] }) await expect(ctx.llm.listModels('deepseek-official')).resolves.toEqual([ { provider: 'deepseek-official', id: 'recovered', name: 'recovered', inputModalities: ['text'] }, From 1ef9c1fa9afbea78c5537bfc8b6d1c27d598798e Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Thu, 10 Sep 2026 03:44:43 +0800 Subject: [PATCH 27/27] release(dsh): 0.1.5-rc.1 --- apps/cli/package.json | 2 +- apps/desktop-host/package.json | 2 +- apps/desktop/package.json | 2 +- apps/web/package.json | 2 +- package.json | 2 +- packages/acp/acp/package.json | 2 +- packages/api/gateway/package.json | 2 +- packages/api/remotes/package.json | 2 +- packages/api/session-controller/package.json | 2 +- packages/api/settings-controller/package.json | 2 +- packages/api/workspace-controller/package.json | 2 +- packages/api/workspace-files/package.json | 2 +- packages/attachment/attachment-local/package.json | 2 +- packages/attachment/attachment/package.json | 2 +- packages/boot/app-boot/package.json | 2 +- packages/boot/cmdline/package.json | 2 +- packages/bundle/acp-app/package.json | 2 +- packages/bundle/base/package.json | 2 +- packages/bundle/headless/package.json | 2 +- packages/bundle/sdk-app/package.json | 2 +- packages/bundle/sdk-minimal/package.json | 2 +- packages/bundle/web-app/package.json | 2 +- packages/client/connection/package.json | 2 +- packages/client/file-upload/package.json | 2 +- packages/client/hmr/package.json | 2 +- packages/client/locale/package.json | 2 +- packages/client/modules/package.json | 2 +- packages/client/resources/package.json | 2 +- packages/client/store/package.json | 2 +- packages/client/ui-agent-preset/package.json | 2 +- packages/client/ui-approval/package.json | 2 +- packages/client/ui-attachment/package.json | 2 +- packages/client/ui-brand-official/package.json | 2 +- packages/client/ui-chat/package.json | 2 +- packages/client/ui-commands/package.json | 2 +- packages/client/ui-conversation/package.json | 2 +- packages/client/ui-deliverables/package.json | 2 +- packages/client/ui-directory-picker-browse/package.json | 2 +- packages/client/ui-directory-picker-native/package.json | 2 +- packages/client/ui-dockkit/package.json | 2 +- packages/client/ui-goal/package.json | 2 +- packages/client/ui-input-trigger/package.json | 2 +- packages/client/ui-jobs/package.json | 2 +- packages/client/ui-layout/package.json | 2 +- packages/client/ui-message-feedback/package.json | 2 +- packages/client/ui-model-selection/package.json | 2 +- packages/client/ui-open-in-app/package.json | 2 +- packages/client/ui-permission-presets/package.json | 2 +- packages/client/ui-plan/package.json | 2 +- packages/client/ui-primitives/package.json | 2 +- packages/client/ui-reference/package.json | 2 +- packages/client/ui-renderer/package.json | 2 +- packages/client/ui-schedule/package.json | 2 +- packages/client/ui-session/package.json | 2 +- packages/client/ui-settings-general/package.json | 2 +- packages/client/ui-settings-models/package.json | 2 +- packages/client/ui-settings-plugin-inventory/package.json | 2 +- packages/client/ui-settings-plugins/package.json | 2 +- packages/client/ui-settings/package.json | 2 +- packages/client/ui-sidebar-documentpreview/package.json | 2 +- packages/client/ui-sidebar-files/package.json | 2 +- packages/client/ui-sidebar-right/package.json | 2 +- packages/client/ui-sidebar/package.json | 2 +- packages/client/ui-skill/package.json | 2 +- packages/client/ui-slots/package.json | 2 +- packages/client/ui-subagent/package.json | 2 +- packages/client/ui-theme/package.json | 2 +- packages/client/ui-tool/package.json | 2 +- packages/client/ui-trajectory/package.json | 2 +- packages/client/ui-user-questions/package.json | 2 +- packages/client/ui-workflow-run/package.json | 2 +- packages/client/ui-workspace/package.json | 2 +- packages/client/web/package.json | 2 +- packages/code-runtime/code-runtime-worker-thread/package.json | 2 +- packages/code-runtime/code-runtime/package.json | 2 +- packages/compaction/command-compact/package.json | 2 +- packages/compaction/compaction-basic/package.json | 2 +- packages/compaction/compaction-tool-result-pruner/package.json | 2 +- packages/compaction/compaction/package.json | 2 +- packages/context/agent-instructions/package.json | 2 +- packages/context/file-reference-local/package.json | 2 +- packages/context/file-reference/package.json | 2 +- packages/context/session-reference/package.json | 2 +- packages/context/time-context/package.json | 2 +- packages/context/tmux-context/package.json | 2 +- packages/core/agent-default-model/package.json | 2 +- packages/core/agent-loop/package.json | 2 +- packages/core/agent-tool-presentation/package.json | 2 +- packages/core/agent/package.json | 2 +- packages/core/scope/package.json | 2 +- packages/core/session/package.json | 2 +- packages/core/system-prompt/package.json | 2 +- packages/core/tools/package.json | 2 +- packages/credentials/authorization/package.json | 2 +- packages/credentials/credentials-local/package.json | 2 +- packages/credentials/credentials/package.json | 2 +- packages/e2b/e2b/package.json | 2 +- packages/e2b/fs-e2b/package.json | 2 +- packages/e2b/subprocess-e2b/package.json | 2 +- packages/experimental/agent-team-profile/package.json | 2 +- packages/experimental/agent-team-web-profile/package.json | 2 +- packages/experimental/agent-team/package.json | 2 +- packages/experimental/client-ui-agent-team/package.json | 2 +- packages/experimental/code-runtime-python/package.json | 2 +- packages/experimental/inspector/package.json | 2 +- packages/experimental/tool-agent-team/package.json | 2 +- packages/experimental/webworker-packer/package.json | 2 +- packages/experimental/webworker-runtime/package.json | 2 +- packages/extensions/cordis-client-runner/package.json | 2 +- packages/extensions/cordis-host-runner/package.json | 2 +- packages/extensions/tool-cordis/package.json | 2 +- packages/extensions/ui-cordis/package.json | 2 +- packages/feedback/command-feedback/package.json | 2 +- packages/feedback/message-feedback/package.json | 2 +- packages/fs/fs-local/package.json | 2 +- packages/fs/fs-observation-policy/package.json | 2 +- packages/fs/fs-sandbox/package.json | 2 +- packages/fs/fs/package.json | 2 +- packages/fs/tool-fs-search/package.json | 2 +- packages/fs/tool-fs/package.json | 2 +- packages/fs/tool-present/package.json | 2 +- packages/fs/tool-str-replace-editor/package.json | 2 +- packages/goal/command-goal/package.json | 2 +- packages/goal/goal-round-driver/package.json | 2 +- packages/goal/goal/package.json | 2 +- packages/goal/tool-goal/package.json | 2 +- packages/guard/repeat-tool-reminder/package.json | 2 +- packages/guard/timeout-policy/package.json | 2 +- packages/hooks/hook-protocol/package.json | 2 +- packages/hooks/hooks-claude-code/package.json | 2 +- packages/hooks/hooks-codex/package.json | 2 +- packages/host/directory-picker-auto/package.json | 2 +- packages/host/directory-picker-browse/package.json | 2 +- packages/host/directory-picker-native/package.json | 2 +- packages/host/directory-picker/package.json | 2 +- packages/host/frontend-static/package.json | 2 +- packages/host/open-in-app/package.json | 2 +- packages/host/plugin-inventory/package.json | 2 +- packages/host/webserver/package.json | 2 +- packages/identity/anonymous-user-id/package.json | 2 +- packages/interaction/commands/package.json | 2 +- packages/interaction/permission-presets/package.json | 2 +- packages/interaction/tool-ask-user/package.json | 2 +- packages/interaction/user-approval/package.json | 2 +- packages/interaction/user-questions/package.json | 2 +- packages/jobs/jobs-local/package.json | 2 +- packages/jobs/jobs/package.json | 2 +- packages/jobs/tool-jobs/package.json | 2 +- packages/llm/deepseek-llm-api-extensions/package.json | 2 +- packages/llm/llm-deepseek/package.json | 2 +- packages/llm/llm-pi-ai/package.json | 2 +- packages/llm/llm-retry/package.json | 2 +- packages/llm/llm/package.json | 2 +- packages/llm/plugin-package-inventory-deepseek/package.json | 2 +- packages/llm/token-meter/package.json | 2 +- packages/lsp/lsp-stdio/package.json | 2 +- packages/lsp/lsp/package.json | 2 +- packages/lsp/tool-lsp/package.json | 2 +- packages/mcp/mcp-client/package.json | 2 +- packages/plan/plan-mode/package.json | 2 +- packages/preset/agent-presets/package.json | 2 +- packages/preset/persona/package.json | 2 +- packages/runtime-diagnostics/invariants/package.json | 2 +- packages/sandbox/sandbox-local/package.json | 2 +- packages/sandbox/sandbox-policy/package.json | 2 +- packages/sandbox/sandbox-windows-acl/package.json | 2 +- packages/sandbox/sandbox/package.json | 2 +- packages/schedule/schedule/package.json | 2 +- packages/sdk/client/package.json | 2 +- packages/sdk/protocol/package.json | 2 +- packages/sdk/server/package.json | 2 +- packages/session-query/session-log-export/package.json | 2 +- packages/session-query/session-query-sqlite/package.json | 2 +- packages/session-query/session-query/package.json | 2 +- packages/session-query/tool-session-query/package.json | 2 +- packages/session/session-checkpoint-policy/package.json | 2 +- packages/session/session-format-catalog/package.json | 2 +- packages/session/session-format-v0-to-v1/package.json | 2 +- packages/session/session-format-v1-to-v2/package.json | 2 +- packages/session/session-format-v2-to-v3/package.json | 2 +- packages/session/session-format/package.json | 2 +- packages/session/session-log-deepseek/package.json | 2 +- packages/session/session-persistence-jsonl/package.json | 2 +- packages/session/session-persistence/package.json | 2 +- packages/session/session-projection-cache/package.json | 2 +- packages/session/session-projection/package.json | 2 +- packages/session/session-stats/package.json | 2 +- packages/session/session-telemetry-otel/package.json | 2 +- packages/session/session-telemetry/package.json | 2 +- packages/session/session-title-all-prompts-llm/package.json | 2 +- packages/session/session-title-first-prompt-llm/package.json | 2 +- packages/session/session-title-llm/package.json | 2 +- packages/session/session-title/package.json | 2 +- packages/session/session-turn-outline/package.json | 2 +- packages/settings/settings-file/package.json | 2 +- packages/settings/settings/package.json | 2 +- packages/shell/bash-local/package.json | 2 +- packages/shell/bash-sandbox/package.json | 2 +- packages/shell/pwsh-local/package.json | 2 +- packages/shell/pwsh-sandbox/package.json | 2 +- packages/shell/shell-env/package.json | 2 +- packages/shell/shell/package.json | 2 +- packages/shell/tool-bash-persistent/package.json | 2 +- packages/shell/tool-bash/package.json | 2 +- packages/shell/tool-pwsh-persistent/package.json | 2 +- packages/shell/tool-pwsh/package.json | 2 +- packages/skill/skill-badge/package.json | 2 +- packages/skill/skill-filesystem/package.json | 2 +- packages/skill/skill/package.json | 2 +- packages/skill/tool-skill/package.json | 2 +- packages/spill/spill-local/package.json | 2 +- packages/spill/spill-policy/package.json | 2 +- packages/spill/spill/package.json | 2 +- packages/storage/storage-domain/package.json | 2 +- packages/storage/storage-json/package.json | 2 +- packages/storage/storage-sqlite/package.json | 2 +- packages/storage/storage/package.json | 2 +- packages/subagent/subagent-acp/package.json | 2 +- packages/subagent/subagent-claude-code/package.json | 2 +- packages/subagent/subagent-codex/package.json | 2 +- packages/subagent/subagent-dsh-sdk/package.json | 2 +- packages/subagent/subagent-fork-in-process/package.json | 2 +- packages/subagent/subagent-in-process-driver/package.json | 2 +- packages/subagent/subagent-spawn-in-process/package.json | 2 +- packages/subagent/subagent/package.json | 2 +- packages/subagent/tool-subagent-control/package.json | 2 +- packages/subagent/tool-subagent/package.json | 2 +- packages/subprocess/subprocess-local/package.json | 2 +- packages/subprocess/subprocess/package.json | 2 +- packages/subprocess/win32-process/package.json | 2 +- packages/terminal/terminal-bash/package.json | 2 +- packages/terminal/terminal/package.json | 2 +- packages/terminal/tool-terminal/package.json | 2 +- packages/test-support/agent-loop-testkit/package.json | 2 +- packages/test-support/client-runtime/package.json | 2 +- packages/test-support/llm-mock-server/package.json | 2 +- packages/test-support/llm-replay/package.json | 2 +- packages/test-support/loader-smoke/package.json | 2 +- packages/test-support/session-snapshot/package.json | 2 +- packages/todo/tool-todo/package.json | 2 +- packages/typert/generator/package.json | 2 +- packages/typert/loader/package.json | 2 +- packages/typert/protocol/package.json | 2 +- packages/typert/registry/package.json | 2 +- packages/util/atomic-write/package.json | 2 +- packages/util/brand/package.json | 2 +- packages/util/chunked-list/package.json | 2 +- packages/util/crypto/package.json | 2 +- packages/util/deque/package.json | 2 +- packages/util/home-paths/package.json | 2 +- packages/util/http-proxy/package.json | 2 +- packages/util/launch-environment/package.json | 2 +- packages/util/native-command/package.json | 2 +- packages/util/output-retention/package.json | 2 +- packages/util/package-manifest/package.json | 2 +- packages/util/time/package.json | 2 +- packages/util/timeout/package.json | 2 +- packages/util/values/package.json | 2 +- packages/util/workspace-path/package.json | 2 +- packages/web/tool-web/package.json | 2 +- packages/web/web-fetch-http/package.json | 2 +- packages/web/web-search-deepseek/package.json | 2 +- packages/web/web-search-exa/package.json | 2 +- packages/web/web-search-perplexity/package.json | 2 +- packages/web/web/package.json | 2 +- packages/webhook/webhook-github/package.json | 2 +- packages/webhook/webhook/package.json | 2 +- packages/workflow/tool-ralph/package.json | 2 +- packages/workflow/tool-workflow/package.json | 2 +- packages/workflow/workflow-worker-thread/package.json | 2 +- packages/workflow/workflow/package.json | 2 +- packages/workspace/workspace/package.json | 2 +- 272 files changed, 272 insertions(+), 272 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index 430451a4d4..d6324bd286 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh", "description": "dsh CLI: profile boot, plugin management, and the browser UI alias", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/apps/desktop-host/package.json b/apps/desktop-host/package.json index 68996a4894..670c35d5f2 100644 --- a/apps/desktop-host/package.json +++ b/apps/desktop-host/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-desktop-host", "description": "Private upstream-Node host process for the Electron desktop application", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "private": true, "license": "MIT", "type": "module", diff --git a/apps/desktop/package.json b/apps/desktop/package.json index b428a0550c..5d642754a7 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-desktop", "description": "Electron desktop shell for an isolated pnpm-installed dsh runtime", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "private": true, "license": "MIT", "type": "module", diff --git a/apps/web/package.json b/apps/web/package.json index 439036b0db..6efa855df2 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-web-frontend", "description": "Web application entry: vite build over the @deepseek-ai/dsh-client-web shell library; dist/ served by apps/cli's dsh web", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/package.json b/package.json index 09627ea0d6..8bde0e242a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@deepseek-ai/dsh-root", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "license": "MIT", "private": true, "type": "module", diff --git a/packages/acp/acp/package.json b/packages/acp/acp/package.json index a5cf5a2d64..c5617c9349 100644 --- a/packages/acp/acp/package.json +++ b/packages/acp/acp/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-acp", "description": "Automation-only Agent Client Protocol server for driving DeepSeek Harness agents over JSON-RPC stdio", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/api/gateway/package.json b/packages/api/gateway/package.json index 8228fefce4..255a2382d0 100644 --- a/packages/api/gateway/package.json +++ b/packages/api/gateway/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-api-gateway", "description": "Typert Remote Host dispatcher and Client API endpoint", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/api/remotes/package.json b/packages/api/remotes/package.json index fafd62f2e3..50db672549 100644 --- a/packages/api/remotes/package.json +++ b/packages/api/remotes/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-api-remotes", "description": "Remote BFF assembly for application-selected Host capabilities", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/api/session-controller/package.json b/packages/api/session-controller/package.json index 0376282f77..10362cdc61 100644 --- a/packages/api/session-controller/package.json +++ b/packages/api/session-controller/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-api-session-controller", "description": "Session Remote commands, cold reads, and live control transport", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/api/settings-controller/package.json b/packages/api/settings-controller/package.json index 91da932736..c3f836b3f2 100644 --- a/packages/api/settings-controller/package.json +++ b/packages/api/settings-controller/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-api-settings-controller", "description": "Remote owner for the configuration surfaces over the settings-domain seams", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/api/workspace-controller/package.json b/packages/api/workspace-controller/package.json index ffdc9e2080..5bf6be8d5a 100644 --- a/packages/api/workspace-controller/package.json +++ b/packages/api/workspace-controller/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-api-workspace-controller", "description": "Workspace Remote commands and reconnect-safe state transport", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/api/workspace-files/package.json b/packages/api/workspace-files/package.json index 5ee4f54e67..b1170a9d7f 100644 --- a/packages/api/workspace-files/package.json +++ b/packages/api/workspace-files/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-api-workspace-files", "description": "Workspace file service and Client resource provider: bounded reads, directory listing, and live metadata over the workspaceFiles Remote namespace", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/attachment/attachment-local/package.json b/packages/attachment/attachment-local/package.json index afa52d914f..8b45903d74 100644 --- a/packages/attachment/attachment-local/package.json +++ b/packages/attachment/attachment-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-attachment-local", "description": "Private content-addressed DSH_HOME attachment storage", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/attachment/attachment/package.json b/packages/attachment/attachment/package.json index d98db07b61..3ca33ec97c 100644 --- a/packages/attachment/attachment/package.json +++ b/packages/attachment/attachment/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-attachment", "description": "Durable immutable attachment storage seam for the DeepSeek Harness", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/boot/app-boot/package.json b/packages/boot/app-boot/package.json index 64eedc7ae1..2397bb9bbf 100644 --- a/packages/boot/app-boot/package.json +++ b/packages/boot/app-boot/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-app-boot", "description": "Shared boot glue for the app bins: .env loading, fail-loud Loader guards, snapshot-aware config resolution, and the Loader boot sequence", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/boot/cmdline/package.json b/packages/boot/cmdline/package.json index ea10cc6960..13ea938412 100644 --- a/packages/boot/cmdline/package.json +++ b/packages/boot/cmdline/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-cmdline", "description": "Immutable command-line handoff from a dsh launcher to any app plugin that injects cmdlineArgs", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/bundle/acp-app/package.json b/packages/bundle/acp-app/package.json index 085b7341e5..d9ec52d0b1 100644 --- a/packages/bundle/acp-app/package.json +++ b/packages/bundle/acp-app/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-acp-app", "description": "The dsh ACP profile bundle: automation-only JSON-RPC stdio and process lifecycle over dsh-base", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/bundle/base/package.json b/packages/bundle/base/package.json index 64e33f55b1..39bdacbcdf 100644 --- a/packages/bundle/base/package.json +++ b/packages/bundle/base/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-base", "description": "The shared dsh core as a profile bundle: the first patch layer of base-backed profiles, inserting core rows over the empty profile root", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/bundle/headless/package.json b/packages/bundle/headless/package.json index 1b9e611457..d59f4edaab 100644 --- a/packages/bundle/headless/package.json +++ b/packages/bundle/headless/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-headless", "description": "The dsh one-shot bundle: a direct core Agent/Session runner over dsh-base with no Host, HTTP, or browser layer", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/bundle/sdk-app/package.json b/packages/bundle/sdk-app/package.json index 4fc726caa8..db2fcd33d0 100644 --- a/packages/bundle/sdk-app/package.json +++ b/packages/bundle/sdk-app/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sdk-app", "description": "The dsh SDK profile bundle: stdio JSON-RPC serving and process lifecycle over dsh-base", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/bundle/sdk-minimal/package.json b/packages/bundle/sdk-minimal/package.json index 630f448daf..6dfc9ee616 100644 --- a/packages/bundle/sdk-minimal/package.json +++ b/packages/bundle/sdk-minimal/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sdk-minimal", "description": "The standalone minimal SDK profile bundle: JSON-RPC, one DeepSeek adapter, persistent shell, and JSONL sessions", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/bundle/web-app/package.json b/packages/bundle/web-app/package.json index 6782a6e444..ee26017e1c 100644 --- a/packages/bundle/web-app/package.json +++ b/packages/bundle/web-app/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-web-app", "description": "The dsh browser-surface bundle: the web patch layer over dsh-base plus the runtime glue plugin (frontend dist serving, web-surface prompt, bash runtime variables, URL line)", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/connection/package.json b/packages/client/connection/package.json index 16dceff71f..ce13fa2b58 100644 --- a/packages/client/connection/package.json +++ b/packages/client/connection/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-connection", "description": "Authenticated RPC transport, generation lifecycle, and browser fixture", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/file-upload/package.json b/packages/client/file-upload/package.json index fce7fd086d..e179fda33f 100644 --- a/packages/client/file-upload/package.json +++ b/packages/client/file-upload/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-file-upload", "description": "Agent-scoped browser file upload, streaming intake, and staged receipt service", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/hmr/package.json b/packages/client/hmr/package.json index a3df146d0f..01866dac7a 100644 --- a/packages/client/hmr/package.json +++ b/packages/client/hmr/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-hmr", "description": "Dev-only hot-reload driver for script-loaded client entries: SSE rebuilt frames → invalidate/prefetch → fiber swap through the vendored Loader entry", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/locale/package.json b/packages/client/locale/package.json index 49928c9321..01ce886f49 100644 --- a/packages/client/locale/package.json +++ b/packages/client/locale/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-locale", "description": "Locale plugin: Host-backed preference, extensible language catalog, browser fallback, and typed built-in dictionaries", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/modules/package.json b/packages/client/modules/package.json index fadb06d13e..12348841cb 100644 --- a/packages/client/modules/package.json +++ b/packages/client/modules/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-modules", "description": "Client module system, dual-face: node half composes the __DSH_BOOT__ entry graph (incremental dsh.client scan, bundle route, index tap, webPlugins service); browser half is the lazy-CJS module table the vendored cordis Loader consumes as its internal seam", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/resources/package.json b/packages/client/resources/package.json index 21a47b7544..e1b71668f9 100644 --- a/packages/client/resources/package.json +++ b/packages/client/resources/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-resources", "description": "Unified client resource model: protocol-registered providers turn URL addresses into live values, consumed through the useResource global standard hook", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/store/package.json b/packages/client/store/package.json index fe71ce96ce..772dbc2ba9 100644 --- a/packages/client/store/package.json +++ b/packages/client/store/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-store", "description": "React-free observable and snapshot-store contracts with the shared Zustand/Immer engine", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-agent-preset/package.json b/packages/client/ui-agent-preset/package.json index 857bc9c906..f941d9eb93 100644 --- a/packages/client/ui-agent-preset/package.json +++ b/packages/client/ui-agent-preset/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-agent-preset", "description": "Agent-preset surfaces: the default for later sessions, this session's seat, and the composition editor", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-approval/package.json b/packages/client/ui-approval/package.json index 00138c179e..a240cee720 100644 --- a/packages/client/ui-approval/package.json +++ b/packages/client/ui-approval/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-approval", "description": "Approval composer takeover over the scoped Remote Event waterfall", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-attachment/package.json b/packages/client/ui-attachment/package.json index 429f1955b3..1bb4b119de 100644 --- a/packages/client/ui-attachment/package.json +++ b/packages/client/ui-attachment/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-attachment", "description": "Dynamic attachment presentation plugin for conversation input, message-image, and trajectory image slots", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-brand-official/package.json b/packages/client/ui-brand-official/package.json index 62ddf9c91e..9407f6900f 100644 --- a/packages/client/ui-brand-official/package.json +++ b/packages/client/ui-brand-official/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-brand-official", "description": "Official DeepSeek Harness brand occupants for the Web client's sidebar slots", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-chat/package.json b/packages/client/ui-chat/package.json index ea9e994e91..3951341f3d 100644 --- a/packages/client/ui-chat/package.json +++ b/packages/client/ui-chat/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-chat", "description": "Chat Conversation target, node definitions, renderers, and details surface", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-commands/package.json b/packages/client/ui-commands/package.json index 4dc37a8261..46538d757a 100644 --- a/packages/client/ui-commands/package.json +++ b/packages/client/ui-commands/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-commands", "description": "Client command surface: global directory cache, '/' source, three command UI kinds, popupSelect registry", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-conversation/package.json b/packages/client/ui-conversation/package.json index c1e62e4100..64cc9baef7 100644 --- a/packages/client/ui-conversation/package.json +++ b/packages/client/ui-conversation/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-conversation", "description": "Target-neutral Conversation assembly, shell, composer, queue, and view navigation", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-deliverables/package.json b/packages/client/ui-deliverables/package.json index dcac6ce807..6164f30324 100644 --- a/packages/client/ui-deliverables/package.json +++ b/packages/client/ui-deliverables/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-deliverables", "description": "Produced-files turn tail and clickable final-response file references for Web", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-directory-picker-browse/package.json b/packages/client/ui-directory-picker-browse/package.json index f6544117ae..741d1c2818 100644 --- a/packages/client/ui-directory-picker-browse/package.json +++ b/packages/client/ui-directory-picker-browse/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-directory-picker-browse", "description": "In-app directory browsing surface: the workspace directory-flow owner rendering the host's listing and creation primitives", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-directory-picker-native/package.json b/packages/client/ui-directory-picker-native/package.json index 1b81a714a8..bd47b06288 100644 --- a/packages/client/ui-directory-picker-native/package.json +++ b/packages/client/ui-directory-picker-native/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-directory-picker-native", "description": "Native directory-picker surface: the renderless workspace directory-flow occupant driving the host's OS chooser", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-dockkit/package.json b/packages/client/ui-dockkit/package.json index df264e82f8..182c33fce0 100644 --- a/packages/client/ui-dockkit/package.json +++ b/packages/client/ui-dockkit/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-dockkit", "description": "Docking layout kit: split-tree engine with invertible operations, and the React components that render and drive it (zero cordis)", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-goal/package.json b/packages/client/ui-goal/package.json index 49f1105462..05f3fd7bc0 100644 --- a/packages/client/ui-goal/package.json +++ b/packages/client/ui-goal/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-goal", "description": "Session goal surface: GoalBar docked above the composer, read from the goal session projection", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-input-trigger/package.json b/packages/client/ui-input-trigger/package.json index d6f3bd7fb6..0db08b9c2a 100644 --- a/packages/client/ui-input-trigger/package.json +++ b/packages/client/ui-input-trigger/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-input-trigger", "description": "Input trigger pipeline: '/' and '@' detection, candidate menu, pick routing to registered sources", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-jobs/package.json b/packages/client/ui-jobs/package.json index 196567bb7a..e55e2d976c 100644 --- a/packages/client/ui-jobs/package.json +++ b/packages/client/ui-jobs/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-jobs", "description": "Session-header background-job list: live registry state mirrored from session/jobs frames", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "type": "module", "main": "lib/index.js", "types": "lib/types/index.d.ts", diff --git a/packages/client/ui-layout/package.json b/packages/client/ui-layout/package.json index 1beae6f3b0..d72ce51e8c 100644 --- a/packages/client/ui-layout/package.json +++ b/packages/client/ui-layout/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-layout", "description": "Shell plugin: three-column AppFrame with drag handles, ctx.layout viewing-state service (navigation + panels)", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-message-feedback/package.json b/packages/client/ui-message-feedback/package.json index 343f289faa..5904d1a096 100644 --- a/packages/client/ui-message-feedback/package.json +++ b/packages/client/ui-message-feedback/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-message-feedback", "description": "The Web feedback surface: per-message Like/Dislike in the assistant-message action strip and the feedback dialog behind Dislike and /feedback, backed by the messageFeedback and sessionFeedback Host Remotes", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-model-selection/package.json b/packages/client/ui-model-selection/package.json index d0951c3d7c..e0ec65380c 100644 --- a/packages/client/ui-model-selection/package.json +++ b/packages/client/ui-model-selection/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-model-selection", "description": "Model selection over the shared model catalog, Session projection, and session.selectModel", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-open-in-app/package.json b/packages/client/ui-open-in-app/package.json index 97c063097d..fc8e9558b1 100644 --- a/packages/client/ui-open-in-app/package.json +++ b/packages/client/ui-open-in-app/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-open-in-app", "description": "Web Session-header \"Open In...\" split button opening the session workspace directory in a locally installed application", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-permission-presets/package.json b/packages/client/ui-permission-presets/package.json index 708f3fba3f..a80a2e5563 100644 --- a/packages/client/ui-permission-presets/package.json +++ b/packages/client/ui-permission-presets/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-permission-presets", "description": "Permission surfaces: a new-session default in General settings and a current-session /permission popup over the permissions projection", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-plan/package.json b/packages/client/ui-plan/package.json index 6fa0ddcc2f..959d790afc 100644 --- a/packages/client/ui-plan/package.json +++ b/packages/client/ui-plan/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-plan", "description": "Plan-mode composer control: the conversation.input.plan seat over the plan projection and the /plan command channel", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-primitives/package.json b/packages/client/ui-primitives/package.json index ca37e9b16f..80a9ecee39 100644 --- a/packages/client/ui-primitives/package.json +++ b/packages/client/ui-primitives/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-primitives", "description": "Pure React atoms for the dsh web UI: controls, icons, markdown, and JSON inspectors (zero cordis)", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-reference/package.json b/packages/client/ui-reference/package.json index 7bfe165621..1375b73238 100644 --- a/packages/client/ui-reference/package.json +++ b/packages/client/ui-reference/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-reference", "description": "Unified Web @file and @session reference source", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-renderer/package.json b/packages/client/ui-renderer/package.json index f1f6d4721d..4307db79ed 100644 --- a/packages/client/ui-renderer/package.json +++ b/packages/client/ui-renderer/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-renderer", "description": "Browser UI renderer: React slot bindings, ctx.uiRenderer, and the assembled application root", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-schedule/package.json b/packages/client/ui-schedule/package.json index a2a6536d3d..4aa8d3872f 100644 --- a/packages/client/ui-schedule/package.json +++ b/packages/client/ui-schedule/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-schedule", "description": "Read-only active Schedule catalog in the Web Session header", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "type": "module", "main": "lib/index.js", "types": "lib/types/index.d.ts", diff --git a/packages/client/ui-session/package.json b/packages/client/ui-session/package.json index 3e09735e19..7cc1dcc427 100644 --- a/packages/client/ui-session/package.json +++ b/packages/client/ui-session/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-session", "description": "Session Controller adapter for React and session-scoped slots", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings-general/package.json b/packages/client/ui-settings-general/package.json index 8327d2ca1e..a4932dd534 100644 --- a/packages/client/ui-settings-general/package.json +++ b/packages/client/ui-settings-general/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings-general", "description": "Settings ownerless-copy and product onboarding plugin: the General section, shell trigger/header chrome content, settings dictionaries, and the versioned welcome notice", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings-models/package.json b/packages/client/ui-settings-models/package.json index a3d882b2e8..b7272db74f 100644 --- a/packages/client/ui-settings-models/package.json +++ b/packages/client/ui-settings-models/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings-models", "description": "Models settings and shared product-onboarding dialogs over existing settings and credential joins", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings-plugin-inventory/package.json b/packages/client/ui-settings-plugin-inventory/package.json index 0755e07977..4bf7c82dbd 100644 --- a/packages/client/ui-settings-plugin-inventory/package.json +++ b/packages/client/ui-settings-plugin-inventory/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings-plugin-inventory", "description": "Read-only Cordis Loader inventory tab in Web Plugins settings", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings-plugins/package.json b/packages/client/ui-settings-plugins/package.json index acc7a836dd..509ab80afc 100644 --- a/packages/client/ui-settings-plugins/package.json +++ b/packages/client/ui-settings-plugins/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings-plugins", "description": "Plugins settings section with feature-owned tabs and configurable host-plane plugin cards", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings/package.json b/packages/client/ui-settings/package.json index dffc545328..909e4545db 100644 --- a/packages/client/ui-settings/package.json +++ b/packages/client/ui-settings/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings", "description": "Settings domain base plugin: the settings-namespace scope service and the canonical settings slot-type contract", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-sidebar-documentpreview/package.json b/packages/client/ui-sidebar-documentpreview/package.json index af3d91613f..b8f78014ce 100644 --- a/packages/client/ui-sidebar-documentpreview/package.json +++ b/packages/client/ui-sidebar-documentpreview/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-sidebar-documentpreview", "description": "Extensible document previews for Sidebar files: Markdown, highlighted code, images, PDF, HTML, and plain text", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-sidebar-files/package.json b/packages/client/ui-sidebar-files/package.json index 619884d036..cfa515b3dd 100644 --- a/packages/client/ui-sidebar-files/package.json +++ b/packages/client/ui-sidebar-files/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-sidebar-files", "description": "Workspace file tree tab type for the right Sidebar: lazy directory listing over the workspaceFiles Remote namespace, opening files into the Sidebar", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-sidebar-right/package.json b/packages/client/ui-sidebar-right/package.json index 31b8055429..4c6786e22d 100644 --- a/packages/client/ui-sidebar-right/package.json +++ b/packages/client/ui-sidebar-right/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-sidebar-right", "description": "Right Sidebar: the docking surface's session-bound state, its panel and header expand control, and the navigation service over it", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-sidebar/package.json b/packages/client/ui-sidebar/package.json index 6fda5a97db..1529d8a370 100644 --- a/packages/client/ui-sidebar/package.json +++ b/packages/client/ui-sidebar/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-sidebar", "description": "Sidebar plugin: session multi-level tree, search, grouping, state dots", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-skill/package.json b/packages/client/ui-skill/package.json index 9db4e24efd..870ebff496 100644 --- a/packages/client/ui-skill/package.json +++ b/packages/client/ui-skill/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-skill", "description": "Web skill references and the dedicated skill tool row", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-slots/package.json b/packages/client/ui-slots/package.json index f01270278e..3da3989d32 100644 --- a/packages/client/ui-slots/package.json +++ b/packages/client/ui-slots/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-slots", "description": "Slot registry pure core: SlotMap declaration merging, single register composition API, four-share props types, store-seat types, renderer install seam", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-subagent/package.json b/packages/client/ui-subagent/package.json index 1835c7f7a2..75a68280a9 100644 --- a/packages/client/ui-subagent/package.json +++ b/packages/client/ui-subagent/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-subagent", "description": "Subagent conversation catalog, continuation routing UI, and '@' reference source", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-theme/package.json b/packages/client/ui-theme/package.json index 610bd399ab..33f5cb95a7 100644 --- a/packages/client/ui-theme/package.json +++ b/packages/client/ui-theme/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-theme", "description": "Theme plugin: Host bootstrap for the pre-plugin palette; DOM-free ThemeRuntime for light/dark/system state; --dsw-* token styles and Appearance settings row", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-tool/package.json b/packages/client/ui-tool/package.json index 14030de47a..0a4b9fc85c 100644 --- a/packages/client/ui-tool/package.json +++ b/packages/client/ui-tool/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-tool", "description": "Client Tool call-tree renderer and keyed per-tool presentation slot", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-trajectory/package.json b/packages/client/ui-trajectory/package.json index 417c8a47e3..fdd4ee0700 100644 --- a/packages/client/ui-trajectory/package.json +++ b/packages/client/ui-trajectory/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-trajectory", "description": "Trajectory event ledger with an interactive timing overview: pure-consumer plugin registering into the conversation ViewMap (no service)", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-user-questions/package.json b/packages/client/ui-user-questions/package.json index 16883d3686..afaca0b4c7 100644 --- a/packages/client/ui-user-questions/package.json +++ b/packages/client/ui-user-questions/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-user-questions", "description": "Web ask_user_question composer takeover and plan-review presentation UI", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-workflow-run/package.json b/packages/client/ui-workflow-run/package.json index a44c93a427..b0d6fb3d7a 100644 --- a/packages/client/ui-workflow-run/package.json +++ b/packages/client/ui-workflow-run/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-workflow-run", "description": "Durable workflow-run Conversation Node and nested member disclosure for dsh web", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-workspace/package.json b/packages/client/ui-workspace/package.json index 72b3b8ff33..432bfa873d 100644 --- a/packages/client/ui-workspace/package.json +++ b/packages/client/ui-workspace/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-workspace", "description": "Workspace picker plugin: one WorkspacePicker registered into the sidebar and empty-state workspace slots", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/web/package.json b/packages/client/web/package.json index 6a9a8364c0..36b3bf6251 100644 --- a/packages/client/web/package.json +++ b/packages/client/web/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-web", "description": "Web boot kernel: static module table, Cordis loader, framework-free boot page, and UI-renderer handoff", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/code-runtime/code-runtime-worker-thread/package.json b/packages/code-runtime/code-runtime-worker-thread/package.json index a31e670376..5aba4c84cb 100644 --- a/packages/code-runtime/code-runtime-worker-thread/package.json +++ b/packages/code-runtime/code-runtime-worker-thread/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-code-runtime-worker-thread", "description": "Worker-thread implementation of the DeepSeek Harness code-execution seam", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/code-runtime/code-runtime/package.json b/packages/code-runtime/code-runtime/package.json index 4321c52d38..59d4c303c7 100644 --- a/packages/code-runtime/code-runtime/package.json +++ b/packages/code-runtime/code-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-code-runtime", "description": "Abstract code-execution seam (ctx.codeRuntime) for the DeepSeek Harness", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/compaction/command-compact/package.json b/packages/compaction/command-compact/package.json index a18140fa85..6553c6b102 100644 --- a/packages/compaction/command-compact/package.json +++ b/packages/compaction/command-compact/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-command-compact", "description": "Human-facing slash command for explicit session compaction", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/compaction/compaction-basic/package.json b/packages/compaction/compaction-basic/package.json index 8de83e4f12..4e7d624403 100644 --- a/packages/compaction/compaction-basic/package.json +++ b/packages/compaction/compaction-basic/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-compaction-basic", "description": "Token-meter-driven compaction policy and LLM summarization backend for the DeepSeek Harness", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/compaction/compaction-tool-result-pruner/package.json b/packages/compaction/compaction-tool-result-pruner/package.json index 57dd414347..caf335c1d3 100644 --- a/packages/compaction/compaction-tool-result-pruner/package.json +++ b/packages/compaction/compaction-tool-result-pruner/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-compaction-tool-result-pruner", "description": "Replay-safe model-free head/middle/tail pruning for tool-result surface nodes", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/compaction/compaction/package.json b/packages/compaction/compaction/package.json index 110298ffbf..a81efbc09a 100644 --- a/packages/compaction/compaction/package.json +++ b/packages/compaction/compaction/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-compaction", "description": "Abstract compaction service seam (ctx.compaction) for the DeepSeek Harness", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/context/agent-instructions/package.json b/packages/context/agent-instructions/package.json index a9b57dc9c0..4f3075a46b 100644 --- a/packages/context/agent-instructions/package.json +++ b/packages/context/agent-instructions/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-agent-instructions", "description": "Workspace context loader for AGENTS.md/CLAUDE.md instruction files", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/context/file-reference-local/package.json b/packages/context/file-reference-local/package.json index cb46191bc6..35133997f2 100644 --- a/packages/context/file-reference-local/package.json +++ b/packages/context/file-reference-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-file-reference-local", "description": "Local-filesystem ctx.fileReferences provider with bounded fuzzy indexes", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/context/file-reference/package.json b/packages/context/file-reference/package.json index 890676082d..7b61041415 100644 --- a/packages/context/file-reference/package.json +++ b/packages/context/file-reference/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-file-reference", "description": "File-reference discovery contract and shared @file grammar", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/context/session-reference/package.json b/packages/context/session-reference/package.json index c5899f1094..3273428e35 100644 --- a/packages/context/session-reference/package.json +++ b/packages/context/session-reference/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-reference", "description": "Cross-session snapshot references and durable untrusted model context (ctx.sessionReferenceResolver)", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/context/time-context/package.json b/packages/context/time-context/package.json index 634bfc2058..a27f801b3b 100644 --- a/packages/context/time-context/package.json +++ b/packages/context/time-context/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-time-context", "description": "Opt-in durable per-step context with the current time and elapsed time", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/context/tmux-context/package.json b/packages/context/tmux-context/package.json index 9a3696c994..aaab689106 100644 --- a/packages/context/tmux-context/package.json +++ b/packages/context/tmux-context/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tmux-context", "description": "Opt-in durable per-step context with this agent's tmux pane and window location", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/core/agent-default-model/package.json b/packages/core/agent-default-model/package.json index 02f25d90b0..296510d8ad 100644 --- a/packages/core/agent-default-model/package.json +++ b/packages/core/agent-default-model/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-agent-default-model", "description": "Default model selection shared by Agent entry points", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/core/agent-loop/package.json b/packages/core/agent-loop/package.json index 627c46ff44..986973d782 100644 --- a/packages/core/agent-loop/package.json +++ b/packages/core/agent-loop/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-agent-loop", "description": "The concrete agent loop plugin for the DeepSeek Harness", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/core/agent-tool-presentation/package.json b/packages/core/agent-tool-presentation/package.json index b1069260d4..a7329fe690 100644 --- a/packages/core/agent-tool-presentation/package.json +++ b/packages/core/agent-tool-presentation/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-agent-tool-presentation", "description": "Agent-plane presentation selector: composes one agent's tools as PTC mode, native, or both", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/core/agent/package.json b/packages/core/agent/package.json index e654ac0d2f..031cdee0ad 100644 --- a/packages/core/agent/package.json +++ b/packages/core/agent/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-agent", "description": "Agent interface, registry, initiator scope, and event vocabulary for the DeepSeek Harness", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/core/scope/package.json b/packages/core/scope/package.json index bb0d51aa0e..418572f660 100644 --- a/packages/core/scope/package.json +++ b/packages/core/scope/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-scope", "description": "Scoped-context registration primitive (scope tags, scope-filtered event dispatch) for the DeepSeek Harness", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/core/session/package.json b/packages/core/session/package.json index 7b7858cb3d..f840ae6136 100644 --- a/packages/core/session/package.json +++ b/packages/core/session/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session", "description": "Event-sourced session store for the DeepSeek Harness", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/core/system-prompt/package.json b/packages/core/system-prompt/package.json index cb431d3fbd..0a5a6deca2 100644 --- a/packages/core/system-prompt/package.json +++ b/packages/core/system-prompt/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-system-prompt", "description": "System prompt assembly registry for the DeepSeek Harness", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/core/tools/package.json b/packages/core/tools/package.json index 174d6f6902..8aa41f993f 100644 --- a/packages/core/tools/package.json +++ b/packages/core/tools/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tools", "description": "Tool registry and execution pipeline for the DeepSeek Harness", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/credentials/authorization/package.json b/packages/credentials/authorization/package.json index 2bfb81078c..d7c3190037 100644 --- a/packages/credentials/authorization/package.json +++ b/packages/credentials/authorization/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-authorization", "description": "Authorization seam (ctx.authorization): plugin-owned flows that obtain a credential through a conversation with the human", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/credentials/credentials-local/package.json b/packages/credentials/credentials-local/package.json index 5e0a6ce461..cceff4b390 100644 --- a/packages/credentials/credentials-local/package.json +++ b/packages/credentials/credentials-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-credentials-local", "description": "File-backed credentials provider ($DSH_HOME/.env under the live process environment) for the DeepSeek Harness", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/credentials/credentials/package.json b/packages/credentials/credentials/package.json index d41f37d3ec..4d2fab8274 100644 --- a/packages/credentials/credentials/package.json +++ b/packages/credentials/credentials/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-credentials", "description": "Abstract credential seam (ctx.credentials): settings carry references to secrets, providers own the values", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/e2b/e2b/package.json b/packages/e2b/e2b/package.json index 373e619db7..bfe05b0245 100644 --- a/packages/e2b/e2b/package.json +++ b/packages/e2b/e2b/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-e2b", "description": "Shared E2B sandbox lifecycle for DeepSeek Harness provider adapters", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/e2b/fs-e2b/package.json b/packages/e2b/fs-e2b/package.json index e63f687f60..5629ae184b 100644 --- a/packages/e2b/fs-e2b/package.json +++ b/packages/e2b/fs-e2b/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-fs-e2b", "description": "E2B filesystem implementation for DeepSeek Harness", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/e2b/subprocess-e2b/package.json b/packages/e2b/subprocess-e2b/package.json index a5f999e7d5..9ab4c2c954 100644 --- a/packages/e2b/subprocess-e2b/package.json +++ b/packages/e2b/subprocess-e2b/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subprocess-e2b", "description": "E2B subprocess implementation for DeepSeek Harness", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/agent-team-profile/package.json b/packages/experimental/agent-team-profile/package.json index 219b268ddd..d35803a238 100644 --- a/packages/experimental/agent-team-profile/package.json +++ b/packages/experimental/agent-team-profile/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-agent-team-profile", "description": "Experimental profile bundle enabling Agent Teams over dsh-base", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/agent-team-web-profile/package.json b/packages/experimental/agent-team-web-profile/package.json index 48ea6d71ba..fd41141b51 100644 --- a/packages/experimental/agent-team-web-profile/package.json +++ b/packages/experimental/agent-team-web-profile/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-agent-team-web-profile", "description": "Experimental Web profile layer for Agent Teams Remote and UI plugins", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/agent-team/package.json b/packages/experimental/agent-team/package.json index 5f04d02601..fd174f04c9 100644 --- a/packages/experimental/agent-team/package.json +++ b/packages/experimental/agent-team/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-agent-team", "description": "Implicit-root Agent Teams roster, durable peer mailbox, and shared task DAG", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/client-ui-agent-team/package.json b/packages/experimental/client-ui-agent-team/package.json index 3584f6493c..d090fd6832 100644 --- a/packages/experimental/client-ui-agent-team/package.json +++ b/packages/experimental/client-ui-agent-team/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-client-ui-agent-team", "description": "Web Agent Teams roster, task board, and teammate navigation", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/code-runtime-python/package.json b/packages/experimental/code-runtime-python/package.json index 7db8fe5dfe..fa589196f1 100644 --- a/packages/experimental/code-runtime-python/package.json +++ b/packages/experimental/code-runtime-python/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-code-runtime-python", "description": "CPython subprocess implementation of the DeepSeek Harness code-execution seam", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "repository": { "type": "git", "url": "git+https://github.com/deepseek-ai/deepseek-harness.git", diff --git a/packages/experimental/inspector/package.json b/packages/experimental/inspector/package.json index a9c1e10725..09fe122bd9 100644 --- a/packages/experimental/inspector/package.json +++ b/packages/experimental/inspector/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-inspector", "description": "Experimental cross-realm CDP hub for Host debugging and Client Runtime inspection", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "private": true, "repository": { "type": "git", diff --git a/packages/experimental/tool-agent-team/package.json b/packages/experimental/tool-agent-team/package.json index fb5779f90c..d5b6600431 100644 --- a/packages/experimental/tool-agent-team/package.json +++ b/packages/experimental/tool-agent-team/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-tool-agent-team", "description": "Scoped model-facing Agent Teams tools over ctx.agentTeams", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/webworker-packer/package.json b/packages/experimental/webworker-packer/package.json index 264e8edea7..16930c170b 100644 --- a/packages/experimental/webworker-packer/package.json +++ b/packages/experimental/webworker-packer/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-webworker-packer", "description": "Build-time packer for the browser runtime's base VFS image and ordered data-overlay archives", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "private": true, "repository": { "type": "git", diff --git a/packages/experimental/webworker-runtime/package.json b/packages/experimental/webworker-runtime/package.json index a0d088ce71..cbeb482553 100644 --- a/packages/experimental/webworker-runtime/package.json +++ b/packages/experimental/webworker-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-webworker-runtime", "description": "Browser-only harness runtime: in-memory VFS, module transform and loader, postMessage tunnel, and the dedicated Web Worker assembly, with the Node-compatibility layer that lets the host tree run unchanged", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "private": true, "repository": { "type": "git", diff --git a/packages/extensions/cordis-client-runner/package.json b/packages/extensions/cordis-client-runner/package.json index c57e78e8ab..91b51cc051 100644 --- a/packages/extensions/cordis-client-runner/package.json +++ b/packages/extensions/cordis-client-runner/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-cordis-client-runner", "description": "Browser half of dynamic dual-half plugin packages: event subscription, closure evaluation, guard facade, and loader entries", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/extensions/cordis-host-runner/package.json b/packages/extensions/cordis-host-runner/package.json index 53e27100c2..8da629b7cf 100644 --- a/packages/extensions/cordis-host-runner/package.json +++ b/packages/extensions/cordis-host-runner/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-cordis-host-runner", "description": "Dynamic package definition registry, host-half sandbox lifecycle, and invoke handler table for model-mounted dual-half packages", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/extensions/tool-cordis/package.json b/packages/extensions/tool-cordis/package.json index da941bf469..d895cd9b1d 100644 --- a/packages/extensions/tool-cordis/package.json +++ b/packages/extensions/tool-cordis/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-cordis", "description": "Self-referential cordis toolset: inspect the live runtime, mount and dispose model-written plugins", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/extensions/ui-cordis/package.json b/packages/extensions/ui-cordis/package.json index 07b34e3d7b..b4aad537a9 100644 --- a/packages/extensions/ui-cordis/package.json +++ b/packages/extensions/ui-cordis/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-cordis", "description": "Cordis dynamic-plugin definition card: the keyed cordis_define tool row with its run/stop switch", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/feedback/command-feedback/package.json b/packages/feedback/command-feedback/package.json index 357e06639c..4b08161f2d 100644 --- a/packages/feedback/command-feedback/package.json +++ b/packages/feedback/command-feedback/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-command-feedback", "description": "Log-only session feedback: the record event, the sessionFeedback Host Remote, and the human-facing slash command", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/feedback/message-feedback/package.json b/packages/feedback/message-feedback/package.json index 7b0031ab1f..66fab4fb94 100644 --- a/packages/feedback/message-feedback/package.json +++ b/packages/feedback/message-feedback/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-message-feedback", "description": "Canonical Session-log ratings and notes for finalized assistant messages", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/fs/fs-local/package.json b/packages/fs/fs-local/package.json index 3b7492c05b..6d6ebd5643 100644 --- a/packages/fs/fs-local/package.json +++ b/packages/fs/fs-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-fs-local", "description": "Local-filesystem implementation of the DeepSeek Harness filesystem seam (ctx.fs)", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/fs/fs-observation-policy/package.json b/packages/fs/fs-observation-policy/package.json index bb2f3d986f..0b206c3c16 100644 --- a/packages/fs/fs-observation-policy/package.json +++ b/packages/fs/fs-observation-policy/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-fs-observation-policy", "description": "File-context policy plugin for the DeepSeek Harness — observed-state, read-before-edit, and version-guarded write/edit added over the ctx.fs provider seam through the fs/* event gate (no service API)", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/fs/fs-sandbox/package.json b/packages/fs/fs-sandbox/package.json index f1eda99e86..5500995467 100644 --- a/packages/fs/fs-sandbox/package.json +++ b/packages/fs/fs-sandbox/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-fs-sandbox", "description": "Sandbox-enforcing implementation of the DeepSeek Harness filesystem seam: fences write/edit by the per-call sandbox mode (read-only denies mutation, workspace-write contains it to the workspace + temp roots) while reads pass through", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/fs/fs/package.json b/packages/fs/fs/package.json index 4944f3f7db..73b9975426 100644 --- a/packages/fs/fs/package.json +++ b/packages/fs/fs/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-fs", "description": "Abstract filesystem capability seam (ctx.fs) for the DeepSeek Harness — vocabulary types, the FileSystem service (text IO + optional version-guarded atomic mutations), and the fs/* policy event vocabulary", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/fs/tool-fs-search/package.json b/packages/fs/tool-fs-search/package.json index db3715b502..e3e8aadaca 100644 --- a/packages/fs/tool-fs-search/package.json +++ b/packages/fs/tool-fs-search/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-fs-search", "description": "Model-facing filesystem discovery tools (glob, grep) backed by the packaged ripgrep binary (@vscode/ripgrep)", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/fs/tool-fs/package.json b/packages/fs/tool-fs/package.json index 062a553050..adc2de7cb9 100644 --- a/packages/fs/tool-fs/package.json +++ b/packages/fs/tool-fs/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-fs", "description": "Model-facing filesystem tools (read, write, edit) over the DeepSeek Harness filesystem seam (ctx.fs)", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/fs/tool-present/package.json b/packages/fs/tool-present/package.json index 0baffa2345..669b79b3e4 100644 --- a/packages/fs/tool-present/package.json +++ b/packages/fs/tool-present/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-present", "description": "Explicit workspace file delivery declarations for the DeepSeek Harness", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/fs/tool-str-replace-editor/package.json b/packages/fs/tool-str-replace-editor/package.json index 669f8da8a5..56456279bc 100644 --- a/packages/fs/tool-str-replace-editor/package.json +++ b/packages/fs/tool-str-replace-editor/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-str-replace-editor", "description": "Model-facing view, create, literal replace, and line insert tool over the Harness filesystem service", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/goal/command-goal/package.json b/packages/goal/command-goal/package.json index c279129914..eb6dc7bd80 100644 --- a/packages/goal/command-goal/package.json +++ b/packages/goal/command-goal/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-command-goal", "description": "Human-facing slash command for persisted same-session goals", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/goal/goal-round-driver/package.json b/packages/goal/goal-round-driver/package.json index 43704663e2..1c1b3f461f 100644 --- a/packages/goal/goal-round-driver/package.json +++ b/packages/goal/goal-round-driver/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-goal-round-driver", "description": "Race-fenced same-session goal-round driver", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/goal/goal/package.json b/packages/goal/goal/package.json index 013acc8c31..04cac37baf 100644 --- a/packages/goal/goal/package.json +++ b/packages/goal/goal/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-goal", "description": "Event-sourced same-session goal state and lifecycle service for the DeepSeek Harness", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/goal/tool-goal/package.json b/packages/goal/tool-goal/package.json index c6836cf722..5af8513dc2 100644 --- a/packages/goal/tool-goal/package.json +++ b/packages/goal/tool-goal/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-goal", "description": "Model-facing same-session goal tools with execution-time authority checks", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/guard/repeat-tool-reminder/package.json b/packages/guard/repeat-tool-reminder/package.json index 76a61f7a6d..05046c8ec7 100644 --- a/packages/guard/repeat-tool-reminder/package.json +++ b/packages/guard/repeat-tool-reminder/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-repeat-tool-reminder", "description": "Repeat-tool-call guard plugin: advisory reminders when an agent loops on identical tool calls", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/guard/timeout-policy/package.json b/packages/guard/timeout-policy/package.json index 50c9064807..968f3eb851 100644 --- a/packages/guard/timeout-policy/package.json +++ b/packages/guard/timeout-policy/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-call-timeout-policy", "description": "Tool-call timeout policy: a tools/execute wrapper that arms a per-tool deadline on exec.signal and returns TOOL_TIMEOUT when it wins", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/hooks/hook-protocol/package.json b/packages/hooks/hook-protocol/package.json index 953ff92fce..6f7999e48a 100644 --- a/packages/hooks/hook-protocol/package.json +++ b/packages/hooks/hook-protocol/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-hook-protocol", "description": "Shared Claude Code / Codex hook wire protocol: matcher engine, stdin/exit-code/stdout codec, multi-hook merge, and hook/* session events", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/hooks/hooks-claude-code/package.json b/packages/hooks/hooks-claude-code/package.json index 8e77285198..bd189d6cc6 100644 --- a/packages/hooks/hooks-claude-code/package.json +++ b/packages/hooks/hooks-claude-code/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-hooks-claude-code", "description": "Bridge plugin: run a Claude Code hooks.json / settings hook config on the DeepSeek Harness interception seams", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/hooks/hooks-codex/package.json b/packages/hooks/hooks-codex/package.json index 846f035e97..9baa76b184 100644 --- a/packages/hooks/hooks-codex/package.json +++ b/packages/hooks/hooks-codex/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-hooks-codex", "description": "Bridge plugin: run a Codex hooks.json hook config on the DeepSeek Harness interception seams", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/host/directory-picker-auto/package.json b/packages/host/directory-picker-auto/package.json index 32278ecaba..99cd165d16 100644 --- a/packages/host/directory-picker-auto/package.json +++ b/packages/host/directory-picker-auto/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-host-directory-picker-auto", "description": "Adaptive chooser of the directory-picker seam: resolves the host situation at boot and mounts the native or browse backend for the DeepSeek Harness web GUI host", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/host/directory-picker-browse/package.json b/packages/host/directory-picker-browse/package.json index 8ea96e6341..d9f21b15b2 100644 --- a/packages/host/directory-picker-browse/package.json +++ b/packages/host/directory-picker-browse/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-host-directory-picker-browse", "description": "In-app browsing backend of the directory-picker seam (listing/creation primitives over the host filesystem)", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/host/directory-picker-native/package.json b/packages/host/directory-picker-native/package.json index b6bb6d9110..b8aa415ce8 100644 --- a/packages/host/directory-picker-native/package.json +++ b/packages/host/directory-picker-native/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-host-directory-picker-native", "description": "Native-OS-chooser backend of the directory-picker seam for the DeepSeek Harness web GUI host", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/host/directory-picker/package.json b/packages/host/directory-picker/package.json index ef0e68372d..26e93f2440 100644 --- a/packages/host/directory-picker/package.json +++ b/packages/host/directory-picker/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-host-directory-picker", "description": "Abstract workspace-directory picking seam (ctx.directoryPicker) for the DeepSeek Harness web GUI host", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/host/frontend-static/package.json b/packages/host/frontend-static/package.json index e348e08d2a..c8e54a088b 100644 --- a/packages/host/frontend-static/package.json +++ b/packages/host/frontend-static/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-host-frontend-static", "description": "SPA dist server for the Web shell: owns the webserver fallback seat, serving explicit index entries and static assets with traversal rejection and 404 misses", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/host/open-in-app/package.json b/packages/host/open-in-app/package.json index c88582a6fd..6e9c16bbcb 100644 --- a/packages/host/open-in-app/package.json +++ b/packages/host/open-in-app/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-host-open-in-app", "description": "Host half of open-in-app: resolved application catalog, icons, and the launch endpoint as three webServer routes", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/host/plugin-inventory/package.json b/packages/host/plugin-inventory/package.json index 65ed93c1d9..792d4ce238 100644 --- a/packages/host/plugin-inventory/package.json +++ b/packages/host/plugin-inventory/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-host-plugin-inventory", "description": "Read-only Remote projection of current Cordis Loader plugin state", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/host/webserver/package.json b/packages/host/webserver/package.json index 96ab1c4704..e5c6cb19c3 100644 --- a/packages/host/webserver/package.json +++ b/packages/host/webserver/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-host-webserver", "description": "Web route-registration plugin: HTTP and upgrade routes, index transform taps, and static dist fallback; knows no harness concepts", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/identity/anonymous-user-id/package.json b/packages/identity/anonymous-user-id/package.json index 2d9d2d82b4..f97f8f67f8 100644 --- a/packages/identity/anonymous-user-id/package.json +++ b/packages/identity/anonymous-user-id/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-anonymous-user-id", "description": "Shared anonymous user identity for DeepSeek Harness telemetry and feedback correlation", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/interaction/commands/package.json b/packages/interaction/commands/package.json index 1fe03def46..5cee082b4f 100644 --- a/packages/interaction/commands/package.json +++ b/packages/interaction/commands/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-commands", "description": "Plugin-owned human command registry for DeepSeek Harness UIs", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/interaction/permission-presets/package.json b/packages/interaction/permission-presets/package.json index ef46950a5b..dd85caf5f5 100644 --- a/packages/interaction/permission-presets/package.json +++ b/packages/interaction/permission-presets/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-permission-presets", "description": "User-facing permission presets (ctx.permissionPresets) for the DeepSeek Harness: one product-level Permissions select bundling the sandbox-mode and approval-policy knobs, written through to their own session events", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/interaction/tool-ask-user/package.json b/packages/interaction/tool-ask-user/package.json index 91b43a476d..e825300c74 100644 --- a/packages/interaction/tool-ask-user/package.json +++ b/packages/interaction/tool-ask-user/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-ask-user", "description": "Model-facing ask_user_question tool over the ctx.userQuestions seam", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/interaction/user-approval/package.json b/packages/interaction/user-approval/package.json index 30ac058e29..71faeb1b71 100644 --- a/packages/interaction/user-approval/package.json +++ b/packages/interaction/user-approval/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-user-approval", "description": "User-approval seam (ctx.approval) for the DeepSeek Harness: one-shot permission decisions dispatched to composed answerers over the approval/request waterfall, fail-closed by default", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/interaction/user-questions/package.json b/packages/interaction/user-questions/package.json index c9936649b9..0e428bbdb4 100644 --- a/packages/interaction/user-questions/package.json +++ b/packages/interaction/user-questions/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-user-questions", "description": "Abstract user-questions seam (ctx.userQuestions) for asking the human during agent runs", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/jobs/jobs-local/package.json b/packages/jobs/jobs-local/package.json index 5a262665d7..31f9267c94 100644 --- a/packages/jobs/jobs-local/package.json +++ b/packages/jobs/jobs-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-jobs-local", "description": "Process-local implementation of the DeepSeek Harness background job registry seam", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/jobs/jobs/package.json b/packages/jobs/jobs/package.json index b567121e03..c5d4a858cf 100644 --- a/packages/jobs/jobs/package.json +++ b/packages/jobs/jobs/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-jobs", "description": "Background job registry (ctx.jobs) for the DeepSeek Harness — shared ids, owner isolation, polling, cancellation, and completion listeners for long-running tool work", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/jobs/tool-jobs/package.json b/packages/jobs/tool-jobs/package.json index 03615aea99..6857a8ef28 100644 --- a/packages/jobs/tool-jobs/package.json +++ b/packages/jobs/tool-jobs/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-jobs", "description": "Model-facing background job control tools (job_output, job_list, job_kill) over the ctx.jobs registry", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/llm/deepseek-llm-api-extensions/package.json b/packages/llm/deepseek-llm-api-extensions/package.json index 411cd64bba..73810ece1a 100644 --- a/packages/llm/deepseek-llm-api-extensions/package.json +++ b/packages/llm/deepseek-llm-api-extensions/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-deepseek-llm-api-extensions", "description": "Additive request-field registry for the official DeepSeek LLM API adapter", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/llm/llm-deepseek/package.json b/packages/llm/llm-deepseek/package.json index e8c177a75e..13fd0827f4 100644 --- a/packages/llm/llm-deepseek/package.json +++ b/packages/llm/llm-deepseek/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-llm-deepseek", "description": "DeepSeek chat-completions adapter for the DeepSeek Harness LLM seam", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/llm/llm-pi-ai/package.json b/packages/llm/llm-pi-ai/package.json index 2c7d5ca5c6..c1a9fea59e 100644 --- a/packages/llm/llm-pi-ai/package.json +++ b/packages/llm/llm-pi-ai/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-llm-pi-ai", "description": "pi-ai-backed DeepSeek adapter for the DeepSeek Harness LLM seam (design-verification twin of dsh-llm-deepseek)", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/llm/llm-retry/package.json b/packages/llm/llm-retry/package.json index b0033c09a7..ce6ffd89f7 100644 --- a/packages/llm/llm-retry/package.json +++ b/packages/llm/llm-retry/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-llm-retry", "description": "Provider-routed LLM request retry policy for the DeepSeek Harness", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/llm/llm/package.json b/packages/llm/llm/package.json index 9e4a322e0b..a96f7ea519 100644 --- a/packages/llm/llm/package.json +++ b/packages/llm/llm/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-llm", "description": "Provider-neutral LLM service interface for the DeepSeek Harness", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/llm/plugin-package-inventory-deepseek/package.json b/packages/llm/plugin-package-inventory-deepseek/package.json index 46193fec94..d32368def6 100644 --- a/packages/llm/plugin-package-inventory-deepseek/package.json +++ b/packages/llm/plugin-package-inventory-deepseek/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-plugin-package-inventory-deepseek", "description": "Active Loader-backed plugin package inventory for official DeepSeek LLM API requests", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/llm/token-meter/package.json b/packages/llm/token-meter/package.json index 983f61a42f..5b080f0800 100644 --- a/packages/llm/token-meter/package.json +++ b/packages/llm/token-meter/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-token-meter", "description": "Replay-aware token measurement service (ctx.tokenMeter) for the DeepSeek Harness", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/lsp/lsp-stdio/package.json b/packages/lsp/lsp-stdio/package.json index 5ede6bd195..86977faa2f 100644 --- a/packages/lsp/lsp-stdio/package.json +++ b/packages/lsp/lsp-stdio/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-lsp-stdio", "description": "Generic stdio language-server provider for the DeepSeek Harness LSP capability seam (ctx.lsp) — spawns configured servers, translates JSON-RPC, and serves transient-open goToDefinition/findReferences/goToImplementation/hover queries in the host filesystem namespace", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/lsp/lsp/package.json b/packages/lsp/lsp/package.json index 71a7be6754..aeaa6822c6 100644 --- a/packages/lsp/lsp/package.json +++ b/packages/lsp/lsp/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-lsp", "description": "Abstract LSP capability seam (ctx.lsp) for the DeepSeek Harness — language-server provider registry keyed by branded id and extension mapping, order-independent per-query selection, normalized definition/references/implementation/hover requests and results, and the LspError taxonomy", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/lsp/tool-lsp/package.json b/packages/lsp/tool-lsp/package.json index 2cb4805bc0..1ff7e5756d 100644 --- a/packages/lsp/tool-lsp/package.json +++ b/packages/lsp/tool-lsp/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-lsp", "description": "Model-facing lsp tool over the DeepSeek Harness LSP capability seam (ctx.lsp) — one read-only tool with goToDefinition/findReferences/goToImplementation/hover operations, one-based UTF-16 cursor coordinates, bounded location rendering, and hover normalization", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/mcp/mcp-client/package.json b/packages/mcp/mcp-client/package.json index b4d921a813..77e86da0d7 100644 --- a/packages/mcp/mcp-client/package.json +++ b/packages/mcp/mcp-client/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-mcp-client", "description": "MCP client bridge: connects to MCP servers and registers their tools on ctx.tools", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/plan/plan-mode/package.json b/packages/plan/plan-mode/package.json index 759f290dda..710f6eb629 100644 --- a/packages/plan/plan-mode/package.json +++ b/packages/plan/plan-mode/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-plan-mode", "description": "Logged per-agent plan mode with deployment guidance, a direct slash command, and a user-reviewed exit", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/preset/agent-presets/package.json b/packages/preset/agent-presets/package.json index e6a026225a..0e54d1ec7d 100644 --- a/packages/preset/agent-presets/package.json +++ b/packages/preset/agent-presets/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-agent-presets", "description": "Per-session agent composition from preset cordis.yml files for the DeepSeek Harness", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/preset/persona/package.json b/packages/preset/persona/package.json index ac69830a75..b17e8bda02 100644 --- a/packages/preset/persona/package.json +++ b/packages/preset/persona/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-persona", "description": "Composition-authored deployment persona section for the DeepSeek Harness", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/runtime-diagnostics/invariants/package.json b/packages/runtime-diagnostics/invariants/package.json index 6dcf6f5517..cd4668a70a 100644 --- a/packages/runtime-diagnostics/invariants/package.json +++ b/packages/runtime-diagnostics/invariants/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-invariants", "description": "Registry service for package-owned DeepSeek Harness runtime invariants", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/sandbox/sandbox-local/package.json b/packages/sandbox/sandbox-local/package.json index 02200c0e13..1e91e6eba2 100644 --- a/packages/sandbox/sandbox-local/package.json +++ b/packages/sandbox/sandbox-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sandbox-local", "description": "Local process-sandbox backends for the DeepSeek Harness sandbox seam: bwrap, the npm-distributed landlock-run launcher, macOS Seatbelt, or the Windows ACL restricted-token runner — functionally probed, fail-closed", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/sandbox/sandbox-policy/package.json b/packages/sandbox/sandbox-policy/package.json index 3f59b340c6..e1589090a3 100644 --- a/packages/sandbox/sandbox-policy/package.json +++ b/packages/sandbox/sandbox-policy/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sandbox-policy", "description": "Per-call sandbox policy resolver and current model context: deployment fallbacks plus each session's mode and workspace root, shared by every enforcing capability family", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/sandbox/sandbox-windows-acl/package.json b/packages/sandbox/sandbox-windows-acl/package.json index 5de9c52edf..5c9827262d 100644 --- a/packages/sandbox/sandbox-windows-acl/package.json +++ b/packages/sandbox/sandbox-windows-acl/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sandbox-windows-acl", "description": "Windows ACL write-restriction sandbox backend (restricted-token spawn with capability-SID write allowlist) for the DeepSeek Harness sandbox seam", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/sandbox/sandbox/package.json b/packages/sandbox/sandbox/package.json index f817a85049..9fdfb59152 100644 --- a/packages/sandbox/sandbox/package.json +++ b/packages/sandbox/sandbox/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sandbox", "description": "Abstract process-sandbox seam (ctx.sandbox) for the DeepSeek Harness: same-world confinement vocabulary and the SandboxProvider contract", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/schedule/schedule/package.json b/packages/schedule/schedule/package.json index 37fa4589c9..142ef6a2dc 100644 --- a/packages/schedule/schedule/package.json +++ b/packages/schedule/schedule/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-schedule", "description": "Agent-scoped durable after, at, and fixed-rate reminders over the session event log", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/sdk/client/package.json b/packages/sdk/client/package.json index f3f9321ca3..c668128c2f 100644 --- a/packages/sdk/client/package.json +++ b/packages/sdk/client/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sdk-client", "description": "TypeScript client SDK for driving a DeepSeek Harness runtime subprocess over stdio JSON-RPC: the DeepSeekHarness high-level turns API and the lower-level HarnessClient", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/sdk/protocol/package.json b/packages/sdk/protocol/package.json index 753fa4ac1e..c56b34204f 100644 --- a/packages/sdk/protocol/package.json +++ b/packages/sdk/protocol/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sdk-protocol", "description": "Shared wire protocol for the DeepSeek Harness SDK runtime: the newline-delimited JSON-RPC stdio transport and the named request, result, and notification types spoken between the runtime server and SDK clients", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/sdk/server/package.json b/packages/sdk/server/package.json index aaf98612ea..3a9b9123ab 100644 --- a/packages/sdk/server/package.json +++ b/packages/sdk/server/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sdk-jsonrpc-server", "description": "Stdio JSON-RPC server plugin for out-of-process DeepSeek Harness SDK clients", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/session-query/session-log-export/package.json b/packages/session-query/session-log-export/package.json index 91966f3773..8992ab79e9 100644 --- a/packages/session-query/session-log-export/package.json +++ b/packages/session-query/session-log-export/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-log-export", "description": "Web Session-log export command and shared download dialog", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/session-query/session-query-sqlite/package.json b/packages/session-query/session-query-sqlite/package.json index 01d3178e31..198bd6a8f4 100644 --- a/packages/session-query/session-query-sqlite/package.json +++ b/packages/session-query/session-query-sqlite/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-query-sqlite", "description": "Concrete ctx.sessionQuery backend with SQLite FTS5 search", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/session-query/session-query/package.json b/packages/session-query/session-query/package.json index 6141578ea4..f925820e48 100644 --- a/packages/session-query/session-query/package.json +++ b/packages/session-query/session-query/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-query", "description": "Combined session query service contract with concrete reads, traces, and filters", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/session-query/tool-session-query/package.json b/packages/session-query/tool-session-query/package.json index e3244c2e76..2296fb8ec4 100644 --- a/packages/session-query/tool-session-query/package.json +++ b/packages/session-query/tool-session-query/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-session-query", "description": "Workspace-authorized model-facing session history search, trace, and event read tools", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-checkpoint-policy/package.json b/packages/session/session-checkpoint-policy/package.json index 796e177b29..50cb01f60f 100644 --- a/packages/session/session-checkpoint-policy/package.json +++ b/packages/session/session-checkpoint-policy/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-checkpoint-policy", "description": "Semantic session durability checkpoints before model requests and tool side effects", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-format-catalog/package.json b/packages/session/session-format-catalog/package.json index 56406e219b..abc714542c 100644 --- a/packages/session/session-format-catalog/package.json +++ b/packages/session/session-format-catalog/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-format-catalog", "description": "Build-static first-party Session format codec and migration catalog", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-format-v0-to-v1/package.json b/packages/session/session-format-v0-to-v1/package.json index 42f7f1c4a9..e2e6285992 100644 --- a/packages/session/session-format-v0-to-v1/package.json +++ b/packages/session/session-format-v0-to-v1/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-format-v0-to-v1", "description": "Frozen released-v0 Session codec and identity migration to v1", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-format-v1-to-v2/package.json b/packages/session/session-format-v1-to-v2/package.json index 11955848ea..b2e9861786 100644 --- a/packages/session/session-format-v1-to-v2/package.json +++ b/packages/session/session-format-v1-to-v2/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-format-v1-to-v2", "description": "Frozen released-v1 Session codec and assistant-stream migration to v2", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-format-v2-to-v3/package.json b/packages/session/session-format-v2-to-v3/package.json index 4c0c17815e..8402b49575 100644 --- a/packages/session/session-format-v2-to-v3/package.json +++ b/packages/session/session-format-v2-to-v3/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-format-v2-to-v3", "description": "Streaming system-prompt, canonical-envelope and PTC migration into V3", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-format/package.json b/packages/session/session-format/package.json index cc55b09ce3..bf064258a8 100644 --- a/packages/session/session-format/package.json +++ b/packages/session/session-format/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-format", "description": "Streaming adjacent Session format migration machinery", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-log-deepseek/package.json b/packages/session/session-log-deepseek/package.json index 3c7f0e65fc..19af009069 100644 --- a/packages/session/session-log-deepseek/package.json +++ b/packages/session/session-log-deepseek/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-log-deepseek", "description": "Incremental lossless session-log request extension for the official DeepSeek LLM API", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-persistence-jsonl/package.json b/packages/session/session-persistence-jsonl/package.json index 11f50ba97a..5949c1cfea 100644 --- a/packages/session/session-persistence-jsonl/package.json +++ b/packages/session/session-persistence-jsonl/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-persistence-jsonl", "description": "JSONL durable session persistence backend for the DeepSeek Harness", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-persistence/package.json b/packages/session/session-persistence/package.json index 36f7009ebd..e6dc547141 100644 --- a/packages/session/session-persistence/package.json +++ b/packages/session/session-persistence/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-persistence", "description": "Abstract durable session persistence seam (ctx.sessionPersistence) for the DeepSeek Harness", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-projection-cache/package.json b/packages/session/session-projection-cache/package.json index 7d9cd2587e..b9922285fd 100644 --- a/packages/session/session-projection-cache/package.json +++ b/packages/session/session-projection-cache/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-projection-cache", "description": "Persisted projection cache (ctx.sessionProjectionCache): durable per-session checkpoint records on the session_projcache storage domain (per-record layout), throttled write-behind, and the cached listing read", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-projection/package.json b/packages/session/session-projection/package.json index e36e2dc4a8..4b5fdbbbf1 100644 --- a/packages/session/session-projection/package.json +++ b/packages/session/session-projection/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-projection", "description": "Session-projection seam: the merge-extensible projection type table, the provider contract, and the ctx.sessionProjections registry serving whole current values of log-derived per-session state", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-stats/package.json b/packages/session/session-stats/package.json index cfaf57382b..b604c54f83 100644 --- a/packages/session/session-stats/package.json +++ b/packages/session/session-stats/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-stats", "description": "Whole-log conversation counts and wall times projection (sessionStats) for the DeepSeek Harness", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-telemetry-otel/package.json b/packages/session/session-telemetry-otel/package.json index 51bb035943..ac0eea1446 100644 --- a/packages/session/session-telemetry-otel/package.json +++ b/packages/session/session-telemetry-otel/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-telemetry-otel", "description": "OpenTelemetry backend for the DeepSeek Harness telemetry seam: hands captured session records to the OTel JS SDK's log pipeline", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-telemetry/package.json b/packages/session/session-telemetry/package.json index 7927c46409..195062fad7 100644 --- a/packages/session/session-telemetry/package.json +++ b/packages/session/session-telemetry/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-telemetry", "description": "SessionTelemetryBackend seam for the DeepSeek Harness: session-event capture, projection, redaction, and handoff to a reporting backend", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-title-all-prompts-llm/package.json b/packages/session/session-title-all-prompts-llm/package.json index c51f74eed9..3d1ff5afc1 100644 --- a/packages/session/session-title-all-prompts-llm/package.json +++ b/packages/session/session-title-all-prompts-llm/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-title-all-prompts-llm", "description": "All-user-messages LLM provider plugin for DeepSeek Harness session titles", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-title-first-prompt-llm/package.json b/packages/session/session-title-first-prompt-llm/package.json index 3a3717fcc7..ac6538d538 100644 --- a/packages/session/session-title-first-prompt-llm/package.json +++ b/packages/session/session-title-first-prompt-llm/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-title-first-prompt-llm", "description": "First-message LLM provider plugin for DeepSeek Harness session titles", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-title-llm/package.json b/packages/session/session-title-llm/package.json index 89cd217004..abddcf8b71 100644 --- a/packages/session/session-title-llm/package.json +++ b/packages/session/session-title-llm/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-title-llm", "description": "Shared LLM generation policy for DeepSeek Harness session-title providers", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-title/package.json b/packages/session/session-title/package.json index 79170df18d..87124a468c 100644 --- a/packages/session/session-title/package.json +++ b/packages/session/session-title/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-title", "description": "Log-backed session title service and provider registry for the DeepSeek Harness", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-turn-outline/package.json b/packages/session/session-turn-outline/package.json index 892bd66c9b..ca2196013a 100644 --- a/packages/session/session-turn-outline/package.json +++ b/packages/session/session-turn-outline/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-turn-outline", "description": "Whole-log turn outline projection (turnOutline) for the DeepSeek Harness", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/settings/settings-file/package.json b/packages/settings/settings-file/package.json index 5a91ea8a02..52779af384 100644 --- a/packages/settings/settings-file/package.json +++ b/packages/settings/settings-file/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-settings-file", "description": "File-backed settings provider (settings.yaml) for the DeepSeek Harness", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/settings/settings/package.json b/packages/settings/settings/package.json index 55343abb6c..c17b020019 100644 --- a/packages/settings/settings/package.json +++ b/packages/settings/settings/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-settings", "description": "Abstract user-settings seam (ctx.settings) for the DeepSeek Harness", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/shell/bash-local/package.json b/packages/shell/bash-local/package.json index 0f1b558a61..ed2bfdbdd0 100644 --- a/packages/shell/bash-local/package.json +++ b/packages/shell/bash-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-bash-local", "description": "Local-subprocess implementation of the DeepSeek Harness bash executor seam", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/shell/bash-sandbox/package.json b/packages/shell/bash-sandbox/package.json index d7405f3cc4..b66267f811 100644 --- a/packages/shell/bash-sandbox/package.json +++ b/packages/shell/bash-sandbox/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-bash-sandbox", "description": "Sandbox-consuming implementation of the DeepSeek Harness bash executor seam (confines every command via ctx.sandbox, reports denial/enforcement result facts)", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/shell/pwsh-local/package.json b/packages/shell/pwsh-local/package.json index 7b102e4c30..0e2d994d9a 100644 --- a/packages/shell/pwsh-local/package.json +++ b/packages/shell/pwsh-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-pwsh-local", "description": "Local PowerShell implementation of the DeepSeek Harness bash executor seam", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/shell/pwsh-sandbox/package.json b/packages/shell/pwsh-sandbox/package.json index 6d5185ef9b..2140443825 100644 --- a/packages/shell/pwsh-sandbox/package.json +++ b/packages/shell/pwsh-sandbox/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-pwsh-sandbox", "description": "Sandbox-consuming implementation of the DeepSeek Harness PowerShell executor seam (confines every command via ctx.sandbox, reports denial/enforcement result facts)", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/shell/shell-env/package.json b/packages/shell/shell-env/package.json index e0b4fb2368..bc3f4bfe98 100644 --- a/packages/shell/shell-env/package.json +++ b/packages/shell/shell-env/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-shell-env", "description": "Tool-independent managed DSH_* shell environment registry", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/shell/shell/package.json b/packages/shell/shell/package.json index 88503f4385..388c903035 100644 --- a/packages/shell/shell/package.json +++ b/packages/shell/shell/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-shell", "description": "Abstract bash executor seam (ctx.shell) for the DeepSeek Harness", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/shell/tool-bash-persistent/package.json b/packages/shell/tool-bash-persistent/package.json index a8c99b3c5c..b42c104c3a 100644 --- a/packages/shell/tool-bash-persistent/package.json +++ b/packages/shell/tool-bash-persistent/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-bash-persistent", "description": "Model-facing owner-scoped persistent Bash tool backed by the Harness PTY service", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/shell/tool-bash/package.json b/packages/shell/tool-bash/package.json index c8648afd57..35cdadb6da 100644 --- a/packages/shell/tool-bash/package.json +++ b/packages/shell/tool-bash/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-bash", "description": "Model-facing bash tool with optional generic background-job and sandbox-escalation support", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/shell/tool-pwsh-persistent/package.json b/packages/shell/tool-pwsh-persistent/package.json index c42a4bde52..a464c68997 100644 --- a/packages/shell/tool-pwsh-persistent/package.json +++ b/packages/shell/tool-pwsh-persistent/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-pwsh-persistent", "description": "Model-facing owner-scoped persistent PowerShell tool backed by the Harness PTY service", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/shell/tool-pwsh/package.json b/packages/shell/tool-pwsh/package.json index b01b2e3cd9..064cf4d4c5 100644 --- a/packages/shell/tool-pwsh/package.json +++ b/packages/shell/tool-pwsh/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-pwsh", "description": "Model-facing pwsh tool over the bash executor seam", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/skill/skill-badge/package.json b/packages/skill/skill-badge/package.json index 1fd637dabb..c9ad804b17 100644 --- a/packages/skill/skill-badge/package.json +++ b/packages/skill/skill-badge/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-skill-badge", "description": "Bundled dsh badge skill provider for DeepSeek Harness", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/skill/skill-filesystem/package.json b/packages/skill/skill-filesystem/package.json index 484c7ea0e1..23a5aae17a 100644 --- a/packages/skill/skill-filesystem/package.json +++ b/packages/skill/skill-filesystem/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-skill-filesystem", "description": "Local filesystem skill provider for the DeepSeek Harness", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/skill/skill/package.json b/packages/skill/skill/package.json index 56154b531d..67b9a14e2c 100644 --- a/packages/skill/skill/package.json +++ b/packages/skill/skill/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-skill", "description": "Agent skill provider registry for the DeepSeek Harness", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/skill/tool-skill/package.json b/packages/skill/tool-skill/package.json index 120876880c..916f22c0c0 100644 --- a/packages/skill/tool-skill/package.json +++ b/packages/skill/tool-skill/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-skill", "description": "Model-facing skill loading tool for the DeepSeek Harness", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/spill/spill-local/package.json b/packages/spill/spill-local/package.json index 089535ff13..57570f70cf 100644 --- a/packages/spill/spill-local/package.json +++ b/packages/spill/spill-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-spill-local", "description": "Local-filesystem implementation of the DeepSeek Harness spill storage seam (private session-scoped files)", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/spill/spill-policy/package.json b/packages/spill/spill-policy/package.json index d3a0ed5eb6..816effcb20 100644 --- a/packages/spill/spill-policy/package.json +++ b/packages/spill/spill-policy/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-spill-policy", "description": "Tool-result spill policy for the DeepSeek Harness — replaces oversized plain-text tool results with a retained preview plus a spill-file path (no service API)", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/spill/spill/package.json b/packages/spill/spill/package.json index 651531bf82..0c76ff0273 100644 --- a/packages/spill/spill/package.json +++ b/packages/spill/spill/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-spill", "description": "Abstract spill storage seam (ctx.spillStore) for the DeepSeek Harness — save oversized tool text and return a retrieval locator", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/storage/storage-domain/package.json b/packages/storage/storage-domain/package.json index dbbd79571c..cd0bd016c3 100644 --- a/packages/storage/storage-domain/package.json +++ b/packages/storage/storage-domain/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-storage-domain", "description": "Domain data form (ctx.storage.domain): schema-validated, event-emitting KV domains over storage backends for the DeepSeek Harness", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/storage/storage-json/package.json b/packages/storage/storage-json/package.json index 4e79b26fc0..64a6bb9e71 100644 --- a/packages/storage/storage-json/package.json +++ b/packages/storage/storage-json/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-storage-json", "description": "JSON file KV storage backend for the DeepSeek Harness storage hub", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/storage/storage-sqlite/package.json b/packages/storage/storage-sqlite/package.json index 638ad2e283..0f60db9f4e 100644 --- a/packages/storage/storage-sqlite/package.json +++ b/packages/storage/storage-sqlite/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-storage-sqlite", "description": "SQLite storage backend (kv facet) for the DeepSeek Harness storage hub", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/storage/storage/package.json b/packages/storage/storage/package.json index c45cc74a11..1bdb7236d6 100644 --- a/packages/storage/storage/package.json +++ b/packages/storage/storage/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-storage", "description": "Storage hub (ctx.storage): named backend registry plus mounted data-form facilities for the DeepSeek Harness", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/subagent/subagent-acp/package.json b/packages/subagent/subagent-acp/package.json index 6e42076bc3..024d961e53 100644 --- a/packages/subagent/subagent-acp/package.json +++ b/packages/subagent/subagent-acp/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subagent-acp", "description": "Out-of-process ACP subagent backend: drives a child agent in a spawned subprocess over the Agent Client Protocol", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/subagent/subagent-claude-code/package.json b/packages/subagent/subagent-claude-code/package.json index b4e328eff0..9bd02b02a4 100644 --- a/packages/subagent/subagent-claude-code/package.json +++ b/packages/subagent/subagent-claude-code/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subagent-claude-code", "description": "One-shot Claude Code subagent provider over the official Agent SDK", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/subagent/subagent-codex/package.json b/packages/subagent/subagent-codex/package.json index 79bda26eec..62b8a989b3 100644 --- a/packages/subagent/subagent-codex/package.json +++ b/packages/subagent/subagent-codex/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subagent-codex", "description": "One-shot Codex subagent provider over the official app-server protocol", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/subagent/subagent-dsh-sdk/package.json b/packages/subagent/subagent-dsh-sdk/package.json index 505ee00c38..7180f10662 100644 --- a/packages/subagent/subagent-dsh-sdk/package.json +++ b/packages/subagent/subagent-dsh-sdk/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subagent-dsh-sdk", "description": "Out-of-process SDK subagent backend: drives a child DeepSeek Harness runtime subprocess over stdio JSON-RPC through the TypeScript SDK client", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/subagent/subagent-fork-in-process/package.json b/packages/subagent/subagent-fork-in-process/package.json index 2deb14834d..e5cf64fe0c 100644 --- a/packages/subagent/subagent-fork-in-process/package.json +++ b/packages/subagent/subagent-fork-in-process/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subagent-fork-in-process", "description": "In-process fork subagent backend: runs a child agent seeded with a prefix of the parent's log", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/subagent/subagent-in-process-driver/package.json b/packages/subagent/subagent-in-process-driver/package.json index ee7d0febc4..3a256d0f24 100644 --- a/packages/subagent/subagent-in-process-driver/package.json +++ b/packages/subagent/subagent-in-process-driver/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subagent-in-process-driver", "description": "Shared in-process subagent run driver: drives a child agent on ctx.agents (used by the spawn and fork backends)", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/subagent/subagent-spawn-in-process/package.json b/packages/subagent/subagent-spawn-in-process/package.json index 73fd12c1fc..0f6449cb2c 100644 --- a/packages/subagent/subagent-spawn-in-process/package.json +++ b/packages/subagent/subagent-spawn-in-process/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subagent-spawn-in-process", "description": "In-process spawn subagent backend: runs a fresh child agent on ctx.agents", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/subagent/subagent/package.json b/packages/subagent/subagent/package.json index 319d9ecbe5..2ba82d9c88 100644 --- a/packages/subagent/subagent/package.json +++ b/packages/subagent/subagent/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subagent", "description": "Abstract subagent seam (ctx.subagents): named-provider registry for delegating to child agents", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/subagent/tool-subagent-control/package.json b/packages/subagent/tool-subagent-control/package.json index 667f86351b..ede810039a 100644 --- a/packages/subagent/tool-subagent-control/package.json +++ b/packages/subagent/tool-subagent-control/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-subagent-control", "description": "Globally named send_message, interrupt_agent, and list_agents tools over ctx.subagents continuations", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/subagent/tool-subagent/package.json b/packages/subagent/tool-subagent/package.json index 284c093f12..9eeda985de 100644 --- a/packages/subagent/tool-subagent/package.json +++ b/packages/subagent/tool-subagent/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-subagent", "description": "Model-facing subagent delegation tool over the ctx.subagents seam", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/subprocess/subprocess-local/package.json b/packages/subprocess/subprocess-local/package.json index 7465d787f9..1b17ce8366 100644 --- a/packages/subprocess/subprocess-local/package.json +++ b/packages/subprocess/subprocess-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subprocess-local", "description": "Local-subprocess implementation of the DeepSeek Harness subprocess seam", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/subprocess/subprocess/package.json b/packages/subprocess/subprocess/package.json index 2f20d3a6f9..fbba4abec0 100644 --- a/packages/subprocess/subprocess/package.json +++ b/packages/subprocess/subprocess/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subprocess", "description": "Subprocess seam (ctx.subprocess) for the DeepSeek Harness — managed process groups, bounded spill-backed output, and escalated kills behind one abstract service", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/subprocess/win32-process/package.json b/packages/subprocess/win32-process/package.json index 28fc6c2685..c3e2823e96 100644 --- a/packages/subprocess/win32-process/package.json +++ b/packages/subprocess/win32-process/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-win32-process", "description": "Shared low-level Win32 process, stdio, and Job Object primitives", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/terminal/terminal-bash/package.json b/packages/terminal/terminal-bash/package.json index c433ad9375..a00d58715b 100644 --- a/packages/terminal/terminal-bash/package.json +++ b/packages/terminal/terminal-bash/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-terminal-bash", "description": "Persistent shell PTY backend over the DeepSeek Harness subprocess terminal primitive", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/terminal/terminal/package.json b/packages/terminal/terminal/package.json index ec2fc7c719..a3c3d2fe0e 100644 --- a/packages/terminal/terminal/package.json +++ b/packages/terminal/terminal/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-terminal", "description": "Persistent PTY session seam for the DeepSeek Harness — owner-scoped ids, backend registry, interactive sends, reads, signals, and awaited cleanup", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/terminal/tool-terminal/package.json b/packages/terminal/tool-terminal/package.json index bfcbeddef1..1d4f1073e2 100644 --- a/packages/terminal/tool-terminal/package.json +++ b/packages/terminal/tool-terminal/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-terminal", "description": "Six model-facing persistent PTY tools with owner isolation and generic background-job integration", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/test-support/agent-loop-testkit/package.json b/packages/test-support/agent-loop-testkit/package.json index 1e2eb18ee4..da125d7e3e 100644 --- a/packages/test-support/agent-loop-testkit/package.json +++ b/packages/test-support/agent-loop-testkit/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-agent-loop-testkit", "description": "Prerequisite mounting, production AgentLoop drivers, and Inbox stubs for tests", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/test-support/client-runtime/package.json b/packages/test-support/client-runtime/package.json index 326a78083a..44cd37bc91 100644 --- a/packages/test-support/client-runtime/package.json +++ b/packages/test-support/client-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-test-runtime", "description": "jsdom slot test runtime: real Cordis Context + SlotRegistry + UI renderer with test-owned session/workspace doubles for feature specs", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/test-support/llm-mock-server/package.json b/packages/test-support/llm-mock-server/package.json index 70538bbbad..ab000270b4 100644 --- a/packages/test-support/llm-mock-server/package.json +++ b/packages/test-support/llm-mock-server/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-llm-mock-server", "description": "Scriptable OpenAI-compatible HTTP/SSE fault server for LLM recovery tests", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/test-support/llm-replay/package.json b/packages/test-support/llm-replay/package.json index 54e9d293b0..27e9dbb497 100644 --- a/packages/test-support/llm-replay/package.json +++ b/packages/test-support/llm-replay/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-llm-replay", "description": "Replay LLM plugin: short-circuits llm/stream with model chunks reconstructed from a recorded session JSONL (keyless snapshot tests)", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/test-support/loader-smoke/package.json b/packages/test-support/loader-smoke/package.json index 2d435c9d9e..51d3e84d81 100644 --- a/packages/test-support/loader-smoke/package.json +++ b/packages/test-support/loader-smoke/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-loader-smoke", "description": "Shared subprocess and direct-agent harness for keyless real-Loader example smoke tests", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/test-support/session-snapshot/package.json b/packages/test-support/session-snapshot/package.json index 1b4576dccc..92c1c56cbd 100644 --- a/packages/test-support/session-snapshot/package.json +++ b/packages/test-support/session-snapshot/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-snapshot", "description": "Session-log snapshot core with an ACP protocol adapter, expected-output normalization, and fixture invariants", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/todo/tool-todo/package.json b/packages/todo/tool-todo/package.json index 863345c42a..676d98dada 100644 --- a/packages/todo/tool-todo/package.json +++ b/packages/todo/tool-todo/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-todo", "description": "Model-facing todo_write tool over the DeepSeek Harness event-sourced session log", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/typert/generator/package.json b/packages/typert/generator/package.json index 8ef779b90c..83d831de18 100644 --- a/packages/typert/generator/package.json +++ b/packages/typert/generator/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-typert-generator", "description": "TypeScript project analyzer and model-driven Typert artifact generator", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/typert/loader/package.json b/packages/typert/loader/package.json index 1e58aa5723..09a1445300 100644 --- a/packages/typert/loader/package.json +++ b/packages/typert/loader/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-typert-loader", "description": "Loader integration for generated Typert package contributions", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/typert/protocol/package.json b/packages/typert/protocol/package.json index 67b30c4377..ece54b6a65 100644 --- a/packages/typert/protocol/package.json +++ b/packages/typert/protocol/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-typert-protocol", "description": "Compiler-independent Remote metadata and Typert provider protocols", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/typert/registry/package.json b/packages/typert/registry/package.json index 2392b2f0c4..1cb89d6249 100644 --- a/packages/typert/registry/package.json +++ b/packages/typert/registry/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-typert-registry", "description": "Runtime registry for generated package reflection and Zod schemas", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/util/atomic-write/package.json b/packages/util/atomic-write/package.json index c18e75ac26..95afd47638 100644 --- a/packages/util/atomic-write/package.json +++ b/packages/util/atomic-write/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-atomic-write", "description": "Zero-dependency atomic file replacement: exclusive-create random-suffix temp + rename carrying the caller-stated permissions (writeFileAtomic)", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/util/brand/package.json b/packages/util/brand/package.json index a7c8974297..2b1627e7c0 100644 --- a/packages/util/brand/package.json +++ b/packages/util/brand/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-brand", "description": "Stateless branded primitive types for the DeepSeek Harness", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/util/chunked-list/package.json b/packages/util/chunked-list/package.json index f27dee18f1..0796d94d1c 100644 --- a/packages/util/chunked-list/package.json +++ b/packages/util/chunked-list/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-chunked-list", "description": "Persistent append-only chunked lists with bounded copying and JSON checkpoint validation", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/util/crypto/package.json b/packages/util/crypto/package.json index 3e54890d57..aa8f431c3a 100644 --- a/packages/util/crypto/package.json +++ b/packages/util/crypto/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-util-crypto", "description": "Zero-dependency browser-safe UUID and byte-encoding helpers", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/util/deque/package.json b/packages/util/deque/package.json index bdea147fb7..e6312b1afb 100644 --- a/packages/util/deque/package.json +++ b/packages/util/deque/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-deque", "description": "Zero-dependency circular deque with amortized constant-time end operations and bounded vacant storage", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/util/home-paths/package.json b/packages/util/home-paths/package.json index 34291f69ba..be60eef526 100644 --- a/packages/util/home-paths/package.json +++ b/packages/util/home-paths/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-home-paths", "description": "Shared filesystem path helpers for the DeepSeek Harness", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/util/http-proxy/package.json b/packages/util/http-proxy/package.json index 4091676849..815d2e3d85 100644 --- a/packages/util/http-proxy/package.json +++ b/packages/util/http-proxy/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-http-proxy", "description": "Process-wide outbound HTTP proxy policy for DeepSeek Harness: resolve it from the launch environment and install it as undici's global dispatcher", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/util/launch-environment/package.json b/packages/util/launch-environment/package.json index 69c88ef587..1e51d0a00a 100644 --- a/packages/util/launch-environment/package.json +++ b/packages/util/launch-environment/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-launch-environment", "description": "Immutable DeepSeek Harness launch environment that records which layer supplied each value", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/util/native-command/package.json b/packages/util/native-command/package.json index 32b2562fd5..bac8bc22a8 100644 --- a/packages/util/native-command/package.json +++ b/packages/util/native-command/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-native-command", "description": "Host-native command and path-opening utilities with shell-free execution, cancellation, desktop detection, and WSL handoff", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/util/output-retention/package.json b/packages/util/output-retention/package.json index ab3f8c5143..ff72365c3c 100644 --- a/packages/util/output-retention/package.json +++ b/packages/util/output-retention/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-output-retention", "description": "Zero-dependency bounded-retention primitive: ItemRetainer/TextRetainer + neutral notice helpers (what did we keep, what did we omit)", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/util/package-manifest/package.json b/packages/util/package-manifest/package.json index 57e6beceda..fdb42e767c 100644 --- a/packages/util/package-manifest/package.json +++ b/packages/util/package-manifest/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-package-manifest", "description": "Shared type declarations for package.json.dsh configuration fields", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/util/time/package.json b/packages/util/time/package.json index 20b7468982..6c0746c340 100644 --- a/packages/util/time/package.json +++ b/packages/util/time/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-util-time", "description": "Zero-dependency time vocabulary shared by wire boundaries: canonicalClientTimeZone (IANA zone validation and canonicalization only, no formatting)", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/util/timeout/package.json b/packages/util/timeout/package.json index 35500070eb..9dbeb4f71c 100644 --- a/packages/util/timeout/package.json +++ b/packages/util/timeout/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-timeout", "description": "Zero-dependency timeout/deadline primitive: clampTimeout, deadline, timeoutOf, TimeoutReason (timing + classification only, no termination)", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/util/values/package.json b/packages/util/values/package.json index 8b08968d19..9018389817 100644 --- a/packages/util/values/package.json +++ b/packages/util/values/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-util-values", "description": "Duplicate-install-safe value primitives for the DeepSeek Harness", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/util/workspace-path/package.json b/packages/util/workspace-path/package.json index 2d9f7d4cb7..2a83879277 100644 --- a/packages/util/workspace-path/package.json +++ b/packages/util/workspace-path/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-util-workspace-path", "description": "Browser-safe Workspace path and display helpers", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/web/tool-web/package.json b/packages/web/tool-web/package.json index 463248c787..d95a2943c4 100644 --- a/packages/web/tool-web/package.json +++ b/packages/web/tool-web/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-web", "description": "Model-facing web tools (web_search, web_fetch) over the DeepSeek Harness web capability seam (ctx.web)", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/web/web-fetch-http/package.json b/packages/web/web-fetch-http/package.json index 2c52a4b0f7..d6d341279e 100644 --- a/packages/web/web-fetch-http/package.json +++ b/packages/web/web-fetch-http/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-web-fetch-http", "description": "Anonymous public HTTP(S) fetch provider for the DeepSeek Harness web capability seam (ctx.web)", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/web/web-search-deepseek/package.json b/packages/web/web-search-deepseek/package.json index 6e7ef73ca2..f3263acc41 100644 --- a/packages/web/web-search-deepseek/package.json +++ b/packages/web/web-search-deepseek/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-web-search-deepseek", "description": "DeepSeek-backed search provider (native web_search via the Anthropic-compatible API) for the DeepSeek Harness web capability seam (ctx.web)", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/web/web-search-exa/package.json b/packages/web/web-search-exa/package.json index daefc8d2d3..c4300449bc 100644 --- a/packages/web/web-search-exa/package.json +++ b/packages/web/web-search-exa/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-web-search-exa", "description": "Exa-backed search provider for the DeepSeek Harness web capability seam (ctx.web)", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/web/web-search-perplexity/package.json b/packages/web/web-search-perplexity/package.json index 819966ac3a..67f36184e9 100644 --- a/packages/web/web-search-perplexity/package.json +++ b/packages/web/web-search-perplexity/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-web-search-perplexity", "description": "Perplexity-backed search provider for the DeepSeek Harness web capability seam (ctx.web)", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/web/web/package.json b/packages/web/web/package.json index 75b51cae79..2a111c79c1 100644 --- a/packages/web/web/package.json +++ b/packages/web/web/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-web", "description": "Abstract web access capability seam (ctx.web) for the DeepSeek Harness — search/fetch provider registry, registration-order-independent selection, request/result vocabulary, and the WebError taxonomy", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/webhook/webhook-github/package.json b/packages/webhook/webhook-github/package.json index 81b8c9cdeb..571995f31b 100644 --- a/packages/webhook/webhook-github/package.json +++ b/packages/webhook/webhook-github/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-webhook-github", "description": "Signed GitHub HTTP webhook adapter for the DeepSeek Harness webhook runtime", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/webhook/webhook/package.json b/packages/webhook/webhook/package.json index ee8416fe68..a279af4632 100644 --- a/packages/webhook/webhook/package.json +++ b/packages/webhook/webhook/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-webhook", "description": "Fire-and-forget webhook rule runtime that creates Workspace-backed DeepSeek Harness Sessions", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/workflow/tool-ralph/package.json b/packages/workflow/tool-ralph/package.json index a1b6609049..1ac7810895 100644 --- a/packages/workflow/tool-ralph/package.json +++ b/packages/workflow/tool-ralph/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-ralph", "description": "Model-facing fresh-agent Ralph loop over the workflow and subagent seams", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/workflow/tool-workflow/package.json b/packages/workflow/tool-workflow/package.json index 06cb66d839..0a49b53eb1 100644 --- a/packages/workflow/tool-workflow/package.json +++ b/packages/workflow/tool-workflow/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-workflow", "description": "Model-facing workflow tool: run a JavaScript orchestration script over ctx.workflowEngine", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/workflow/workflow-worker-thread/package.json b/packages/workflow/workflow-worker-thread/package.json index 2a91e2d2dc..d56e7bb1f8 100644 --- a/packages/workflow/workflow-worker-thread/package.json +++ b/packages/workflow/workflow-worker-thread/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-workflow-worker-thread", "description": "worker-thread workflow engine: executes model-written orchestration scripts off the host event loop, bridging agent() calls back to ctx.subagents", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/workflow/workflow/package.json b/packages/workflow/workflow/package.json index 8e03f25a7c..3b73206524 100644 --- a/packages/workflow/workflow/package.json +++ b/packages/workflow/workflow/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-workflow", "description": "Workflow capability seam: ctx.workflowEngine service, run vocabulary, and workflow/* events", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/workspace/workspace/package.json b/packages/workspace/workspace/package.json index bf1ad56f88..46770c874f 100644 --- a/packages/workspace/workspace/package.json +++ b/packages/workspace/workspace/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-workspace", "description": "Workspace entity registry (ctx.workspaceRegistry): durable workspace records with validated session attachment over the domain data form for the DeepSeek Harness", - "version": "0.1.5-alpha.2", + "version": "0.1.5-rc.1", "publishConfig": { "access": "public" },