1
0
Эх сурвалжийг харах

Merge pull request #3547 from deepseek-harness/ihsiangzhang/tool-call-clickable-styles

feat(client): 统一回答中可点击链接样式——链接别名、hover 点状虚线、分类图标
ihsiang 1 өдөр өмнө
parent
commit
0180c5cdea
37 өөрчлөгдсөн 1056 нэмэгдсэн , 50 устгасан
  1. 6 0
      .agents/notes/implemented/feature/2026-09-04-web-clickable-link-styles.i18n.yaml
  2. 34 0
      .agents/notes/implemented/feature/2026-09-04-web-clickable-link-styles.md
  3. 34 0
      .agents/notes/implemented/feature/2026-09-04-web-clickable-link-styles.zh.md
  4. 431 0
      apps/web/tests/clickable-links-gallery.e2e.ts
  5. 202 0
      apps/web/tests/expected/clickable-links-gallery/ui.expected.md
  6. 1 0
      apps/web/tsconfig.json
  7. 2 2
      docs/web-styling.i18n.yaml
  8. 1 0
      docs/web-styling.md
  9. 1 0
      docs/web-styling.zh.md
  10. 45 19
      packages/client/ui-deliverables/src/client/ProducedFiles.module.css
  11. 4 1
      packages/client/ui-deliverables/src/client/ProducedFiles.tsx
  12. 2 2
      packages/client/ui-primitives/README.i18n.yaml
  13. 1 1
      packages/client/ui-primitives/README.md
  14. 1 1
      packages/client/ui-primitives/README.zh.md
  15. 86 0
      packages/client/ui-primitives/src/LinkIcon.tsx
  16. 23 6
      packages/client/ui-primitives/src/WebBlock.module.css
  17. 2 0
      packages/client/ui-primitives/src/WebBlock.tsx
  18. 2 0
      packages/client/ui-primitives/src/index.ts
  19. 25 8
      packages/client/ui-primitives/src/markdown/MarkdownText.module.css
  20. 18 5
      packages/client/ui-primitives/src/markdown/render.tsx
  21. 2 0
      packages/client/ui-primitives/tests/fixtures/markdown-dom/cjk-strong-and-inline-code-url.settled.txt
  22. 2 0
      packages/client/ui-primitives/tests/fixtures/markdown-dom/cjk-strong-and-inline-code-url.streaming.txt
  23. 2 0
      packages/client/ui-primitives/tests/fixtures/markdown-dom/footnotes.settled.txt
  24. 2 0
      packages/client/ui-primitives/tests/fixtures/markdown-dom/footnotes.streaming.txt
  25. 4 0
      packages/client/ui-primitives/tests/fixtures/markdown-dom/gfm-strikethrough-and-literals.settled.txt
  26. 4 0
      packages/client/ui-primitives/tests/fixtures/markdown-dom/gfm-strikethrough-and-literals.streaming.txt
  27. 16 0
      packages/client/ui-primitives/tests/fixtures/markdown-dom/links-and-autolinks.settled.txt
  28. 16 0
      packages/client/ui-primitives/tests/fixtures/markdown-dom/links-and-autolinks.streaming.txt
  29. 6 0
      packages/client/ui-primitives/tests/fixtures/markdown-dom/reference-links-and-images.settled.txt
  30. 6 0
      packages/client/ui-primitives/tests/fixtures/markdown-dom/reference-links-and-images.streaming.txt
  31. 2 0
      packages/client/ui-primitives/tests/fixtures/markdown-dom/table-with-alignment.settled.txt
  32. 2 0
      packages/client/ui-primitives/tests/fixtures/markdown-dom/table-with-alignment.streaming.txt
  33. 55 0
      packages/client/ui-primitives/tests/link-icon.client.spec.tsx
  34. 2 0
      packages/client/ui-primitives/tests/markdown-dom-parity.client.spec.tsx
  35. 4 2
      packages/client/ui-theme/src/styles/design-platform.css
  36. 9 3
      packages/client/ui-workflow-run/src/client/WorkflowRunPanel.module.css
  37. 1 0
      tsconfig.host.json

+ 6 - 0
.agents/notes/implemented/feature/2026-09-04-web-clickable-link-styles.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-04-web-clickable-link-styles.md
+2026-09-04-web-clickable-link-styles.md: 8781990a40de30e26d0c59c0ecc1a08ba41643ac
+2026-09-04-web-clickable-link-styles.zh.md: 7b5fde4c87319f6ae34b76d1c8e712e6f9f18392

+ 34 - 0
.agents/notes/implemented/feature/2026-09-04-web-clickable-link-styles.md

@@ -0,0 +1,34 @@
+# Agent Note: Web clickable-link language — link alias, dotted hover underline, category glyphs
+
+Status: implemented
+
+English | [中文](2026-09-04-web-clickable-link-styles.zh.md)
+
+## Problem
+
+Clickable artifact links in the chat transcript wore four different costumes: markdown anchors and prose file mentions were business-primary blue with a solid hover underline, web search/fetch links matched that pair, produced-file chips were grey pills (label-secondary text on interactive-bg-hover, 96px max width), and workflow member links carried a resting solid underline. Nothing marked what a link opens (browser, host app, Finder, in-app view), and link color was coupled to `--dsw-alias-state-business-primary`, which also drives focus rings and state dots, so tuning link color risked unrelated surfaces.
+
+## Decision
+
+One link language across the transcript's clickable-link surfaces — markdown anchors (including reference links, mailto, and URL-promoted inline code), prose file mentions, web search source links and the fetch URL, produced-file chips, and workflow member links:
+
+- Color comes through a dedicated `--dsw-alias-link` alias in `design-platform.css` (light `deepseek-500`, dark `deepseek-400`), decoupled from `state-business-primary`; links render at `font-weight: 500` with no underline at rest and `underline dotted` at 3px offset on hover/focus.
+- A leading category glyph — the new `LinkIcon` in ui-primitives with kinds `url` (globe), `folder`, `code`, `image`, `document`, and `other` (paper) — renders `currentColor` only; `classifyLinkPath` derives the file kinds from the extension, and code, web, and data extensions share the code glyph by design. Two anchor shapes carry no glyph: workflow member links (an in-app member view fits no file or URL category) and anchors wrapping only images (a badge or thumbnail — a dangling globe beside the picture leads no text). Inline glyphs sit at 1.1em with a −0.25em baseline offset; the flex-centered produced-file glyphs instead nudge 1.2px down because the 22px text box carries its glyphs below box center.
+- Produced-file chips drop the grey pill and the 96px cap: plain link-blue text at natural width that shrinks with ellipsis only when the row overflows; the container-query bands still budget 96px per chip when choosing how many chips to show.
+- Deliberately untouched: ToolRow's grey dotted file links, and the grey "Show in folder" action (it gains the folder glyph but keeps its grey style).
+- In the same pass, the inline-code chip tint moved from `neutral-bluish-100` to `neutral-50` (dark: `neutral-800`) and gained a 0.5px l1 border.
+
+Coverage: a LinkIcon unit spec (one distinct glyph per kind, classification table), refreshed markdown-dom fixtures, and the `clickable-links-gallery` web e2e — one settled keyless turn rendering every clickable link form — registered in the host compiler face (`tsconfig.host.json`) like its other scaffold-importing siblings.
+
+## Alternatives considered
+
+- **Colored Word/Excel/PPT/PDF brand glyphs.** Implemented, then removed: fixed brand fills break the icon set's currentColor-only rule, so those extensions fold into the single outline `document` glyph.
+- **Per-extension icons.** Collapsed to six categories: more glyphs than the eye can parse at 14px adds noise, and per-site favicons remain possible later behind the same `url` category.
+- **Keeping links on `state-business-primary`.** Darker link blues (blue-600/650/700 were auditioned and reverted) would have dragged focus rings and state dots along; the dedicated alias localizes any future tuning to one line.
+- **Glyphs on ToolRow path links.** Rejected: tool rows keep their quieter grey dotted affordance, and leading glyphs there would stack icons in already dense rows.
+
+## Consequences
+
+- A new clickable artifact surface should consume `--dsw-alias-link` and the LinkIcon vocabulary rather than introduce another color or underline form; the rule lives in [docs/web-styling.md](../../../../docs/web-styling.md).
+- Long produced-file names take their natural width; when a row overflows, flex shrinks all chips proportionally, so several long names shrink together instead of the last one yielding first.
+- mailto links currently share the `url` globe; a distinct mail category is a one-line addition if ever wanted.

+ 34 - 0
.agents/notes/implemented/feature/2026-09-04-web-clickable-link-styles.zh.md

@@ -0,0 +1,34 @@
+# Agent Note: Web 可点击链接语言——链接别名、hover 点状下划线、分类图标
+
+Status: implemented
+
+[English](2026-09-04-web-clickable-link-styles.md) | 中文
+
+## 问题
+
+会话记录里的可点击产物链接有四套互不一致的样式:Markdown 锚点和正文文件引用是 business-primary 蓝加 hover 实线下划线,网页搜索/抓取链接与之相同,产物 chips 是灰色药丸(interactive-bg-hover 底上的 label-secondary 文字,96px 最大宽度),workflow 成员链接则带常驻实线下划线。没有任何标记说明链接点开后去哪儿(浏览器、宿主应用、Finder、应用内视图),而且链接颜色耦合在 `--dsw-alias-state-business-primary` 上——它同时驱动焦点环和状态点,调链接色会波及无关表面。
+
+## 决策
+
+会话记录的可点击链接表面——Markdown 锚点(含引用式链接、mailto、被提升为链接的 inline code)、正文文件引用、网页搜索来源链接与抓取 URL、产物 chips、workflow 成员链接——统一为一套链接语言:
+
+- 颜色经由 `design-platform.css` 中专用的 `--dsw-alias-link` 别名(亮色 `deepseek-500`,暗色 `deepseek-400`),与 `state-business-primary` 解耦;链接以 `font-weight: 500` 呈现,默认无下划线,hover/focus 时为 3px offset 的 `underline dotted`。
+- 前置分类图标——ui-primitives 新增的 `LinkIcon`,kind 为 `url`(地球)、`folder`、`code`、`image`、`document`、`other`(纸张)——只渲染 `currentColor`;`classifyLinkPath` 按扩展名推导文件类别,代码、网页、数据扩展名按设计共用 code 图形。两类锚点不带图标:workflow 成员链接(应用内成员视图不属于任何文件或 URL 类别)和只包图片的锚点(徽章或缩略图——图片旁悬着的地球没有可引导的文字)。行内图标为 1.1em、基线偏移 −0.25em;flex 居中的产物图标则下移 1.2px,因为 22px 文字盒的字形低于盒中心。
+- 产物 chips 去掉灰色药丸和 96px 上限:纯链接蓝文字按自然宽度展示,仅当整行溢出时才收缩出省略号;容器查询档位在决定展示几个 chip 时仍按每个 96px 预算。
+- 刻意不动:ToolRow 的灰色点线文件链接,以及灰色的「在文件夹中显示」操作(它获得文件夹图标但保持灰色样式)。
+- 同一批次中,inline code 底色从 `neutral-bluish-100` 换到 `neutral-50`(暗色:`neutral-800`),并新增 0.5px l1 描边。
+
+覆盖:LinkIcon 单测(每个 kind 一个独立图形、分类表)、刷新后的 markdown-dom 夹具,以及 `clickable-links-gallery` web e2e——一个 settled 的 keyless 回合渲染全部可点击链接形态——像其他引 scaffold 的同类一样注册进 host 编译面(`tsconfig.host.json`)。
+
+## 备选方案
+
+- **彩色 Word/Excel/PPT/PDF 品牌图形。** 实现后又移除:固定品牌填充违反图标集 currentColor-only 规则,这些扩展名并入单一的 outline `document` 图形。
+- **每个扩展名一个图标。** 收敛为六个类别:14px 下超出肉眼可分辨数量的图形只会增加噪音,按站点的 favicon 以后仍可在同一 `url` 类别之下引入。
+- **链接继续用 `state-business-primary`。** 更深的链接蓝(试过 blue-600/650/700 又回退)会连带焦点环和状态点;专用别名把未来的调色收敛到一行。
+- **给 ToolRow 路径链接加图形。** 否决:工具行保持更安静的灰色点线示能,在已经很密的行里加前置图形会造成图标堆叠。
+
+## 后果
+
+- 新的可点击产物表面应消费 `--dsw-alias-link` 和 LinkIcon 词汇,而不是引入另一种颜色或下划线形态;规则记录在 [docs/web-styling.md](../../../../docs/web-styling.zh.md)。
+- 长产物文件名按自然宽度展示;整行溢出时 flex 按比例收缩所有 chip,几个长名字一起收缩,而不是最后一个先让位。
+- mailto 链接目前共用 `url` 地球图形;若将来需要独立的邮件类别,一行即可加上。

+ 431 - 0
apps/web/tests/clickable-links-gallery.e2e.ts

@@ -0,0 +1,431 @@
+// Web e2e gallery: every clickable link and artifact form the chat renders,
+// in one settled keyless turn — the regression anchor for unifying link
+// styles. One fixture turn produces:
+// - prose: Markdown link, reference-style link, mailto link, inline-code URL,
+//   produced-file mention, plus inert contrasts (ambiguous basename, unwritten
+//   file, command code, URL-with-flags code, javascript: destination,
+//   footnote superscript, remote image, fenced code block with its copy chrome)
+// - artifacts: seven produced files (chips overflow into the "+N" remainder
+//   and the show-in-folder affordance) with a failed write excluded
+// - tool rows: write/edit/str_replace_editor/read file links, a failure row
+//   without one, and — expanded — the web-search source links (one non-http
+//   source stays inert) and answer link, the web-fetch URL, the read card's
+//   inert path label and fold toggle, the diff card's inert path header, the
+//   grep card's fold-only file headers, a failing bash card's exit status, and
+//   a generic tool card's IN/OUT surfaces.
+// Image thumbnails and subagent rows stay out: the first needs real attachment
+// bytes (image-display.expected.e2e.ts owns that route) and the second needs a
+// child session (subagent-conversation owns it).
+import { fileURLToPath } from 'node:url'
+import type { Browser, Page } from 'playwright'
+import { chromium } from 'playwright'
+import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
+import { ToolCallId, createAssistantMessage, createToolResultMessage, createUserMessage } from '@deepseek-ai/dsh-llm'
+import { SESSION_FORMAT_VERSION, Session, SessionId } from '@deepseek-ai/dsh-session'
+import type { JsonValue } from '@deepseek-ai/dsh-util-values'
+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/clickable-links-gallery', import.meta.url))
+const UI_EXPECTED = fileURLToPath(new URL('./expected/clickable-links-gallery/ui.expected.md', import.meta.url))
+// The golden holds the show-in-folder affordance; pin the native-opener
+// capability so headless Linux CI and desktop developer hosts expose the same
+// UI branch (same pin as produced-files.e2e.ts, whose overlay this shares).
+const OVERLAY = fileURLToPath(new URL('./produced-files.overlay.yml', import.meta.url))
+const MODE = webSnapshotMode()
+const SEED_ID = 'clickable-links-gallery-web-e2e'
+const DONE = 'LINK_GALLERY_DONE'
+
+const GUIDE_URL = 'https://docs.example.test/guide'
+const API_URL = 'https://docs.example.test/api'
+const RELEASES_URL = 'https://docs.example.test/releases'
+const MAILTO_URL = 'mailto:owner@example.test'
+const SOURCE_URL = 'https://docs.example.test/links'
+const INERT_SOURCE_URL = 'ftp://mirror.example.test/spec'
+const FETCH_URL = 'https://docs.example.test/tokens'
+
+/** One-part text content for a built message. */
+function text(value: string): { type: 'text'; text: string }[] {
+  return [{ type: 'text', text: value }]
+}
+
+/** A settled root tool call: its call event arguments plus result content/meta. */
+interface GalleryCall {
+  name: string
+  args: Record<string, unknown>
+  result: string
+  meta?: JsonValue
+  isError?: true
+}
+
+/** The five successful writes; docs/press.md and src/tokens.css join via other tools. */
+const WRITES = ['site/report.html', 'a/style.css', 'b/style.css', 'site/index.html', 'site/app.js']
+
+/** The gallery turn's tool calls, in surface order. */
+const CALLS: GalleryCall[] = [
+  ...WRITES.map(path => ({
+    name: 'write',
+    args: { file_path: path, content: `content of ${path}\n` },
+    result: `Created ${path}`,
+  })),
+  {
+    name: 'edit',
+    args: {
+      file_path: 'src/tokens.css',
+      old_string: '--inline-code: #EBEEF2;',
+      new_string: '--inline-code: #F5F5F5;',
+    },
+    result: 'Edited src/tokens.css',
+    meta: {
+      diffs: [{
+        path: 'src/tokens.css',
+        oldText: '--inline-code: #EBEEF2;\n',
+        newText: '--inline-code: #F5F5F5;\n',
+      }],
+    },
+  },
+  {
+    name: 'str_replace_editor',
+    args: { command: 'create', path: 'docs/press.md', file_text: '# Press kit\n' },
+    result: 'Created docs/press.md',
+    meta: { diffs: [{ path: 'docs/press.md', oldText: null, newText: '# Press kit\n' }] },
+  },
+  {
+    name: 'write',
+    args: { file_path: 'c/broken.css', content: 'nope\n' },
+    result: 'permission denied: c/broken.css',
+    isError: true,
+  },
+  {
+    name: 'read',
+    args: { file_path: 'docs/guide.md' },
+    // The read card validates the model-facing envelope, not just the meta:
+    // without <path>/<type>/<content> the row falls back to the generic card.
+    result: [
+      '<path>docs/guide.md</path>',
+      '<type>file</type>',
+      '<content>',
+      ...Array.from({ length: 12 }, (_, index) =>
+        index === 0 ? '# Link style guide' : `guide line ${String(index + 1)}`),
+      '</content>',
+    ].join('\n'),
+    meta: {
+      path: 'docs/guide.md',
+      offset: 1,
+      lines: Array.from({ length: 12 }, (_, index) => ({
+        number: index + 1,
+        text: index === 0 ? '# Link style guide' : `guide line ${String(index + 1)}`,
+      })),
+      totalLines: 12,
+    },
+  },
+  {
+    name: 'grep',
+    args: { pattern: 'linkColor' },
+    result: 'nine matches across three files',
+    meta: {
+      shape: 'matches',
+      files: ['a/style.css', 'b/style.css', 'src/tokens.css'].map(path => ({
+        path,
+        matches: [3, 7, 11].map(lineNumber => ({ lineNumber, line: '  color: var(--linkColor);' })),
+      })),
+      truncated: false,
+      total: 9,
+    },
+  },
+  {
+    name: 'glob',
+    args: { pattern: '**/*.css' },
+    result: 'a/style.css\nb/style.css\nsrc/tokens.css',
+    meta: { shape: 'paths', paths: ['a/style.css', 'b/style.css', 'src/tokens.css'], truncated: false, total: 3 },
+  },
+  {
+    name: 'bash',
+    args: { command: 'ls site', description: 'List the built site' },
+    result: 'report.html\nindex.html\napp.js\n',
+  },
+  {
+    name: 'bash',
+    args: { command: 'pnpm run lint', description: 'Run the lint gate', workdir: 'site' },
+    result: 'style.css: unexpected hex literal\n[exit code: 1]',
+  },
+  {
+    name: 'web_search',
+    args: { queries: ['clickable link styles', 'produced files ui'] },
+    result: 'Two sources found.',
+    meta: {
+      truncated: false,
+      answer: `Unify links per [the guide](${GUIDE_URL}).`,
+      sources: [
+        { url: SOURCE_URL, title: 'Link styles reference', snippet: 'One cursor token, one focus ring.' },
+        { url: INERT_SOURCE_URL, title: 'Mirror spec (non-http)', snippet: 'A non-http source renders inert.' },
+      ],
+    },
+  },
+  {
+    name: 'web_fetch',
+    args: { url: FETCH_URL },
+    result: 'Design token reference page.',
+    meta: { url: FETCH_URL, statusCode: 200, truncated: false },
+  },
+  {
+    name: 'design_tokens_sync',
+    args: { source: 'design-platform.css', dryRun: false },
+    result: '{"synced":true,"tokens":12}',
+  },
+]
+
+/**
+ * Build the settled gallery turn: every call above plus a link-dense closing prose.
+ * @param imageUrl - scaffold-hosted image the prose embeds (kept same-origin so
+ *   the tripwire stays clean; the varying origin is tokenized in the golden).
+ */
+function galleryFixture(imageUrl: string): string {
+  const session = Session.create(SessionId('clickable-links-gallery-source'))
+  const eventTimeOrigin = new Date().setHours(12, 0, 0, 0)
+  session.append('turn/start', { turn: 1 })
+  const user = session.append('user/message', createUserMessage({
+    content: text('Assemble the link gallery: write the report and styles, inspect the sources, and summarize.'),
+    source: { kind: 'user' },
+  }), { surfaceOp: 'append' })
+  session.append('session/title', {
+    title: 'Clickable links gallery',
+    messageSeqs: [user.seq],
+    source: { kind: 'fallback' },
+  })
+  session.append('step/start', { turn: 1, step: 1 })
+  const calls = CALLS.map((call, index) => ({
+    ...call,
+    callId: ToolCallId(`gallery-${String(index)}`),
+    argsJson: JSON.stringify(call.args),
+  }))
+  session.append('assistant/message', {
+    stream: [],
+    turn: 1,
+    step: 1,
+    message: createAssistantMessage({
+      content: calls.map(call => ({
+        type: 'tool-call' as const,
+        id: call.callId,
+        name: call.name,
+        arguments: call.argsJson,
+      })),
+      source: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
+    }),
+  }, { surfaceOp: 'append' })
+  for (const call of calls) {
+    const source = session.append('tool/call', {
+      turn: 1,
+      step: 1,
+      callId: call.callId,
+      name: call.name,
+      arguments: call.argsJson,
+    })
+    session.append('tool/result', {
+      turn: 1,
+      step: 1,
+      message: createToolResultMessage({
+        callId: call.callId,
+        content: text(call.result),
+        isError: call.isError === true,
+      }),
+      ...(call.meta === undefined ? {} : { meta: call.meta }),
+    }, { surfaceOp: 'append', sourceEventSeqs: [source.seq] })
+  }
+  session.append('step/start', { turn: 1, step: 2 })
+  session.append('assistant/message', {
+    stream: [],
+    turn: 1,
+    step: 2,
+    message: createAssistantMessage({
+      content: text([
+        '## Link gallery',
+        '',
+        `Docs: [style guide](${GUIDE_URL}) and \`${API_URL}\`; see [the release notes][rel], `
+        + `contact [the maintainer](${MAILTO_URL}), and check the fine print[^1].`,
+        '',
+        `Inert contrasts: \`curl ${API_URL}\`, \`javascript:alert(1)\`, and \`pnpm run build\`.`,
+        '',
+        'Wrote `report.html` plus two `style.css` copies; `notes.md` untouched.',
+        '',
+        `![Token preview](${imageUrl})`,
+        '',
+        '```css',
+        '--inline-code: #F5F5F5;',
+        '```',
+        '',
+        DONE,
+        '',
+        `[rel]: ${RELEASES_URL}`,
+        '',
+        '[^1]: Footnote references stay inert superscripts.',
+      ].join('\n')),
+      source: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
+    }),
+  }, { surfaceOp: 'append' })
+  session.append('step/end', { turn: 1, step: 2 })
+  session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
+
+  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: eventTimeOrigin + event.seq * 1_000,
+    })),
+    '',
+  ].join('\n')
+}
+
+describe('web e2e: clickable links gallery', () => {
+  let scaffold: WebScaffold
+  let browser: Browser
+  let page: Page
+  let imageUrl: string
+  let tripwire: ReturnType<typeof watchConsole>
+
+  beforeAll(async () => {
+    scaffold = await launchWebScaffold({ extraOverlayPath: OVERLAY })
+    imageUrl = new URL('/favicon.svg', scaffold.baseUrl).toString()
+    await seedSession(scaffold, galleryFixture(imageUrl), SEED_ID)
+    browser = await chromium.launch()
+    page = await newEnglishPage(browser)
+    tripwire = watchConsole(page)
+    await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
+    await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
+  }, 120_000)
+
+  afterAll(async () => {
+    await browser?.close()
+    await scaffold?.close()
+  })
+
+  it.skipIf(MODE === 'record')('renders every clickable link and artifact form of the settled turn', async () => {
+    onTestFailed(() => saveFailureShot(page, 'web-e2e-clickable-links-gallery'))
+    const groupRow = page.locator('[role="treeitem"]').first()
+    await groupRow.waitFor({ timeout: 15_000 })
+    await groupRow.click()
+    const sessionRow = page.locator('[role="treeitem"]').nth(1)
+    await sessionRow.waitFor({ timeout: 10_000 })
+    await sessionRow.click()
+    await expect.poll(() => page.getByText(DONE, { exact: true }).count(), { timeout: 15_000 }).toBe(1)
+
+    // Markdown prose: plain, reference-style, and mailto links plus the
+    // inline-code URL are anchors; the URL-with-flags code, the javascript:
+    // destination, and plain command code stay inert.
+    const markdown = page.locator('[class*="markdown"]')
+    await expect.poll(() => markdown.locator(`a[href="${GUIDE_URL}"]`).count(), { timeout: 10_000 }).toBe(1)
+    expect(await markdown.locator(`a[href="${RELEASES_URL}"]`).count()).toBe(1)
+    expect(await markdown.locator(`a[href="${MAILTO_URL}"]`).count()).toBe(1)
+    const inlineCodeLink = markdown.locator(`code a[href="${API_URL}"]`)
+    expect(await inlineCodeLink.count()).toBe(1)
+    expect(await inlineCodeLink.getAttribute('target')).toBe('_blank')
+    expect(await page.getByText(`curl ${API_URL}`, { exact: true }).locator('a').count()).toBe(0)
+    expect(await page.getByText('javascript:alert(1)', { exact: true }).locator('a').count()).toBe(0)
+    expect(await markdown.locator(`img[src="${imageUrl}"]`).count()).toBe(1)
+
+    // Produced files: one unique-basename mention links; the shared basename
+    // and the unwritten file stay inert code. Seven produced paths overflow
+    // the chip row; the failed write joins neither surface.
+    const mentions = markdown.locator('code button')
+    expect(await mentions.count()).toBe(1)
+    expect(await mentions.first().getAttribute('title')).toBe('site/report.html')
+    expect(await page.getByText('Produced', { exact: true }).count()).toBe(1)
+    expect(await page.locator('[class*="centerCol"] button[aria-label^="Open "]').count()).toBeGreaterThanOrEqual(5)
+    expect(await page.locator('button[aria-label="Open c/broken.css"]').count()).toBe(0)
+
+    // Tool rows: five writes, the edit, and the read carry the dotted file
+    // link; the failed write row does not, and neither does str_replace_editor
+    // — TOOL_VARIANTS has no entry for it, so it falls to the generic row with
+    // no openable path even though its create still joins the produced chips.
+    expect(await page.locator('button[class*="fileLink"]').count()).toBe(7)
+
+    // Expanded cards. The turn-process group collapses a multi-call turn, so
+    // it opens first. Rows expand via a right-edge click: the row center can
+    // land on the nested fileLink button, which would hand the path to the
+    // Host's opener.
+    await page.getByRole('button', { name: `${String(CALLS.length)} tool calls` }).click()
+    for (const row of [
+      /^Search clickable link styles/,
+      /^Fetch /,
+      /^Read docs\/guide\.md/,
+      /^Edit src\/tokens\.css/,
+      /^Grep linkColor/,
+      /Run the lint gate|pnpm run lint/,
+    ]) {
+      const toggle = page.getByRole('button', { name: row }).first()
+      await toggle.waitFor({ timeout: 10_000 })
+      const box = await toggle.boundingBox()
+      await toggle.click(box === null ? {} : { position: { x: box.width - 8, y: box.height / 2 } })
+    }
+    // Both generic rows (the unclassified str_replace_editor and the unknown
+    // design_tokens_sync) expand to their IN/OUT surfaces.
+    const genericRows = page.getByRole('button', { name: /^Tool call/ })
+    for (let index = 0; index < await genericRows.count(); index += 1) {
+      const toggle = genericRows.nth(index)
+      const box = await toggle.boundingBox()
+      await toggle.click(box === null ? {} : { position: { x: box.width - 8, y: box.height / 2 } })
+    }
+    const sourceLink = page.locator(`a[href="${SOURCE_URL}"]`)
+    await expect.poll(() => sourceLink.count(), { timeout: 10_000 }).toBe(1)
+    expect(await page.locator('a[href^="ftp:"]').count()).toBe(0)
+    expect(await page.locator(`a[href="${FETCH_URL}"]`).count()).toBe(1)
+    expect(await page.locator(`a[href="${GUIDE_URL}"]`).count()).toBe(2)
+
+    const snapshot = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd))
+      .split(SEED_ID).join('{{seededId}}')
+      .split(imageUrl).join('{{imageUrl}}')
+    await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE)
+    expect(tripwire.pageErrors).toEqual([])
+    expect(tripwire.warnings).toEqual([])
+    await assertFixtureInventory(SNAPSHOT_DIR, ['ui.expected.md'])
+
+    // The link language itself — ARIA records none of it, so pin the computed
+    // styles: link-blue 500-weight text, no underline at rest, dotted underline
+    // on hover, and a leading currentColor glyph. Light theme, so the link
+    // alias resolves to deepseek-500.
+    const LINK_BLUE = 'rgb(65, 118, 230)'
+    const styleOf = async (target: ReturnType<Page['locator']>, property: string): Promise<string> =>
+      target.evaluate((el, p) => getComputedStyle(el).getPropertyValue(p), property)
+    const guideLink = markdown.locator(`a[href="${GUIDE_URL}"]`).first()
+    const chip = page.locator('button[aria-label="Open site/report.html"]').first()
+    for (const [name, link] of [
+      ['markdown anchor', guideLink],
+      ['file mention', mentions.first()],
+      ['search source', sourceLink.first()],
+      ['fetch url', page.locator(`a[href="${FETCH_URL}"]`).first()],
+      ['produced chip', chip],
+    ] as const) {
+      expect.soft(await styleOf(link, 'color'), `${name} color`).toBe(LINK_BLUE)
+      expect.soft(await styleOf(link, 'font-weight'), `${name} weight`).toBe('500')
+      expect.soft(await styleOf(link, 'text-decoration-line'), `${name} at rest`).toBe('none')
+      expect.soft(await link.locator('svg').count(), `${name} glyph`).toBe(1)
+    }
+    await guideLink.hover()
+    expect(await styleOf(guideLink, 'text-decoration-line')).toBe('underline')
+    expect(await styleOf(guideLink, 'text-decoration-style')).toBe('dotted')
+    expect(await styleOf(guideLink, 'text-underline-offset')).toBe('3px')
+    await chip.hover()
+    expect(await styleOf(chip, 'text-decoration-style')).toBe('dotted')
+    expect(await styleOf(chip, 'background-color')).toBe('rgba(0, 0, 0, 0)')
+    // The excluded grey affordance: tool-row file links keep their own color.
+    expect(await styleOf(page.locator('button[class*="fileLink"]').first(), 'color')).not.toBe(LINK_BLUE)
+  }, 90_000)
+})

+ 202 - 0
apps/web/tests/expected/clickable-links-gallery/ui.expected.md

@@ -0,0 +1,202 @@
+- banner:
+  - navigation "Session hierarchy":
+    - button "Clickable links gallery" [disabled]
+  - button "Session log":
+    - text: Session log
+    - img
+  - tablist:
+    - tab "Chat" [selected]
+    - tab "Trajectory"
+- text: "Assemble the link gallery: write the report and styles, inspect the sources, and summarize. {{clock}}"
+- button "Copy":
+  - img
+- button "16 tool calls" [expanded]:
+  - text: 16 tool calls
+  - img
+- button "Write site/report.html +1 -0":
+  - img
+  - img
+  - text: Write
+  - button "site/report.html"
+  - text: +1 -0
+- button "Write a/style.css +1 -0":
+  - img
+  - img
+  - text: Write
+  - button "a/style.css"
+  - text: +1 -0
+- button "Write b/style.css +1 -0":
+  - img
+  - img
+  - text: Write
+  - button "b/style.css"
+  - text: +1 -0
+- button "Write site/index.html +1 -0":
+  - img
+  - img
+  - text: Write
+  - button "site/index.html"
+  - text: +1 -0
+- button "Write site/app.js +1 -0":
+  - img
+  - img
+  - text: Write
+  - button "site/app.js"
+  - text: +1 -0
+- button "Edit src/tokens.css +1 -1" [expanded]:
+  - img
+  - text: Edit
+  - button "src/tokens.css"
+  - text: +1 -1
+- button "Copy"
+- text: "src/tokens.css - --inline-code: #EBEEF2; + --inline-code: #F5F5F5; └ +1 -1 · 1 file"
+- button "Inspect"
+- button "Tool call str_replace_editor · create" [expanded]:
+  - img
+  - text: Tool call str_replace_editor · create
+- text: "IN { \"command\": \"create\", \"path\": \"docs/press.md\", \"file_text\": \"# Press kit\\n\" } OUT Created docs/press.md"
+- button "Inspect"
+- text: Failed
+- 'button "Write permission denied: c/broken.css"':
+  - img
+  - text: "Write permission denied: c/broken.css"
+- button "Read docs/guide.md" [expanded]:
+  - img
+  - text: Read
+  - button "docs/guide.md"
+- text: docs/guide.md
+- button "Copy"
+- text: "# Link style guide guide line 2 guide line 3 guide line 4"
+- button "Expand 4 more lines": … 4 more lines
+- text: guide line 9 guide line 10 guide line 11 guide line 12
+- button "Inspect"
+- button "Grep linkColor" [expanded]:
+  - img
+  - text: Grep linkColor
+- text: 9 matches · 3 files
+- button "Copy"
+- button "a/style.css 3" [expanded]
+- text: "3: color: var(--linkColor); 7: color: var(--linkColor); 11: color: var(--linkColor);"
+- button "Expand 4 more result lines": … 4 more lines
+- button "src/tokens.css 3" [expanded]
+- text: "3: color: var(--linkColor); 7: color: var(--linkColor); 11: color: var(--linkColor);"
+- button "Inspect"
+- button "Glob **/*.css":
+  - img
+  - img
+  - text: Glob **/*.css
+- button "Bash List the built site":
+  - img
+  - img
+  - text: Bash List the built site
+- button "Failed Bash Run the lint gate" [expanded]:
+  - img
+  - text: Failed Bash Run the lint gate
+- text: Failed site pnpm run lint exit code 1
+- button "Copy"
+- text: "style.css: unexpected hex literal"
+- button "Inspect"
+- button "Search clickable link styles, produced files ui" [expanded]:
+  - img
+  - text: Search clickable link styles, produced files ui
+- paragraph:
+  - text: Unify links per
+  - link "the guide":
+    - /url: https://docs.example.test/guide
+  - text: .
+- list:
+  - listitem:
+    - link "Link styles reference":
+      - /url: https://docs.example.test/links
+    - text: One cursor token, one focus ring.
+  - listitem: Mirror spec (non-http) A non-http source renders inert.
+- button "Inspect"
+- button "Fetch https://docs.example.test/tokens" [expanded]:
+  - img
+  - text: Fetch https://docs.example.test/tokens
+- link "https://docs.example.test/tokens":
+  - /url: https://docs.example.test/tokens
+- text: HTTP 200
+- button "Inspect"
+- button "Tool call design_tokens_sync · design-platform.css" [expanded]:
+  - img
+  - text: Tool call design_tokens_sync · design-platform.css
+- text: "IN { \"source\": \"design-platform.css\", \"dryRun\": false } OUT {\"synced\":true,\"tokens\":12}"
+- button "Inspect"
+- heading "Link gallery" [level=2]
+- paragraph:
+  - text: "Docs:"
+  - link "style guide":
+    - /url: https://docs.example.test/guide
+  - text: and
+  - code:
+    - link "https://docs.example.test/api":
+      - /url: https://docs.example.test/api
+  - text: ; see
+  - link "the release notes":
+    - /url: https://docs.example.test/releases
+  - text: ", contact"
+  - link "the maintainer":
+    - /url: mailto:owner@example.test
+  - text: ", and check the fine print"
+  - superscript: "1"
+  - text: .
+- paragraph:
+  - text: "Inert contrasts:"
+  - code: curl https://docs.example.test/api
+  - text: ","
+  - code: javascript:alert(1)
+  - text: ", and"
+  - code: pnpm run build
+  - text: .
+- paragraph:
+  - text: Wrote
+  - code:
+    - button "Open site/report.html": report.html
+  - text: plus two
+  - code: style.css
+  - text: copies;
+  - code: notes.md
+  - text: untouched.
+- paragraph:
+  - img "Token preview"
+- text: css
+- button "Copy"
+- code: "--inline-code: #F5F5F5;"
+- paragraph: LINK_GALLERY_DONE
+- heading "Footnotes" [level=2]
+- list:
+  - listitem:
+    - paragraph: Footnote references stay inert superscripts. ↩
+- text: Produced
+- button "Open site/report.html": report.html
+- button "Open a/style.css": style.css
+- button "Open b/style.css": style.css
+- button "Open site/index.html": index.html
+- button "Open site/app.js": app.js
+- button "Open src/tokens.css": tokens.css
+- text: + 1 file
+- button "Show in folder"
+- 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}}
+- button "Back to bottom":
+  - img
+- textbox "Message or run a task... / commands, @ files or sessions"
+- button "Commands":
+  - 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}} · Tool call {{duration}}

+ 1 - 0
apps/web/tsconfig.json

@@ -73,6 +73,7 @@
     "tests/math-rendering.e2e.ts",
     "tests/markdown-cjk-strong.e2e.ts",
     "tests/markdown-inline-code-links.e2e.ts",
+    "tests/clickable-links-gallery.e2e.ts",
     "tests/queue-actions.e2e.ts",
     "tests/queue-image.e2e.ts",
     "tests/skill-invocation-policy.e2e.ts",

+ 2 - 2
docs/web-styling.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/web-styling.md
-web-styling.md: dd057a3121422e4decfac06b9a3cf57e52254011
-web-styling.zh.md: 5ec0b65390b29e915fe3da633bb7c56ef92e17d6
+web-styling.md: 250936b82f06b47d00c0b6646cbea5bdb41f42ea
+web-styling.zh.md: 9845b4f5d8341624d294a008f505c0fb1ec9757a

+ 1 - 0
docs/web-styling.md

@@ -22,6 +22,7 @@ Global style sheets belong in `ui-theme/src/styles/`. Component styles live besi
 - Rounded corners inherit the global superellipse smoothing from ui-theme's `corner-shape.css` on supporting engines. Pair `corner-shape: round` with every full-round `border-radius` (`50%`, `100%`, or a pill radius) so circles and capsules keep circular arcs; the ui-theme corner-shape spec enforces the pairing.
 - Elevated surfaces (menus, popovers, modals, panels, floating buttons, the composer) set `border: 0` and take `box-shadow: var(--dsw-elevation-panel)`, `var(--dsw-elevation-prominent)`, or the composer's `var(--dsw-elevation-soft)` (larger blur at lower alpha): the 0.5px hairline stroke is the first shadow layer, and `--dsw-elevation-stroke-color` rebinds or suppresses it per surface or state. Never pair a `--dsw-alias-border-*` border with an lv/elevation shadow — the ui-theme elevation spec rejects the pairing; state-colored borders (warn panels) stay real borders.
 - Flat borders and separators that use a neutral `--dsw-alias-border-*` token draw at `0.5px` — buttons, inputs, cards, row dividers, and separators drawn as filled boxes (menu separators, the conversation header seam, markdown `hr`, vertical rails) share the hairline weight, which Chromium paints as one device pixel. Dashed affordances and state-colored borders keep 1px; spinner ring tracks keep their width through the spec's explicit allowlist. The ui-theme elevation spec rejects wider neutral solid borders.
+- Clickable artifact links (markdown anchors, prose file mentions, web source and fetch links, produced-file chips, workflow member links) color through `--dsw-alias-link` at `font-weight: 500`, with no underline at rest and a dotted 3px-offset underline on hover/focus. Text-leading anchors also lead with the ui-primitives `LinkIcon` category glyph riding `currentColor`; workflow member links and image-only anchors carry no glyph, and tool-row file links keep their grey dotted affordance ([clickable-link Agent Note](../.agents/notes/implemented/feature/2026-09-04-web-clickable-link-styles.md)).
 
 ## Changing the system
 

+ 1 - 0
docs/web-styling.zh.md

@@ -22,6 +22,7 @@
 - 支持的引擎上,圆角继承 ui-theme `corner-shape.css` 的全局超级椭圆平滑。每个正圆 `border-radius`(`50%`、`100%` 或胶囊半径)必须配对 `corner-shape: round`,使圆形与胶囊保持圆弧;ui-theme 的 corner-shape spec 强制这一配对。
 - 高层级表面(菜单、浮层、对话框、面板、悬浮按钮、输入框)设 `border: 0` 并使用 `box-shadow: var(--dsw-elevation-panel)`、`var(--dsw-elevation-prominent)` 或输入框专用的 `var(--dsw-elevation-soft)`(更大模糊、更低透明度):0.5px 发丝描边是第一层投影,`--dsw-elevation-stroke-color` 可按表面或状态重绑或抑制描边。不得将 `--dsw-alias-border-*` border 与 lv/elevation 投影配对——ui-theme 的 elevation spec 会拒绝;状态色 border(warn 面板)保持真 border。
 - 使用中性 `--dsw-alias-border-*` token 的平面边框与分割线一律 `0.5px`——按钮、输入框、卡片、行分割线,以及以填充盒绘制的分隔线(菜单分隔、对话标题栏接缝、markdown `hr`、竖向轨道线)共用发丝线粗细,Chromium 将其绘制为一个设备像素。dashed 记号与状态色 border 保持 1px;spinner 圆环经 spec 的显式豁免保留原宽度。更宽的中性 solid border 会被 ui-theme elevation spec 拒绝。
+- 可点击产物链接(Markdown 锚点、正文文件引用、网页来源与抓取链接、产物 chips、workflow 成员链接)经 `--dsw-alias-link` 着色、`font-weight: 500`,默认无下划线,hover/focus 时为 3px offset 的点状下划线。带文字的锚点另以 ui-primitives 的 `LinkIcon` 分类图形(随 `currentColor`)作前置;workflow 成员链接与只包图片的锚点不带图形,工具行文件链接保持其灰色点线示能([可点击链接 Agent Note](../.agents/notes/implemented/feature/2026-09-04-web-clickable-link-styles.zh.md))。
 
 ## 变更系统
 

+ 45 - 19
packages/client/ui-deliverables/src/client/ProducedFiles.module.css

@@ -17,7 +17,6 @@
 }
 
 .lane {
-  --produced-file-chip-max: 96px;
   --produced-file-gap: 8px;
 
   grid-column: 2;
@@ -37,28 +36,53 @@
   overflow: hidden;
 }
 
-/* File names yield space to one another while the remainder stays readable. */
+/* File names render at natural width and only shrink (with ellipsis) when
+   the row truly runs out of space; the container-query bands still budget
+   96px per chip when deciding how many chips to show. Same link language as
+   markdown anchors and file mentions: link-blue at rest with no underline,
+   dotted underline on hover; the leading glyph names the file's category
+   and rides the link color. */
 .file {
   box-sizing: border-box;
+  display: inline-flex;
+  align-items: center;
+  gap: 5px;
   flex: 0 1 auto;
   min-width: 0;
-  max-width: var(--produced-file-chip-max);
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
   margin: 0;
-  padding: 0 8px;
+  padding: 0;
   border: none;
-  border-radius: 6px;
-  background: var(--dsw-alias-interactive-bg-hover);
-  color: var(--dsw-alias-label-secondary);
+  border-radius: 4px;
+  background: none;
+  color: var(--dsw-alias-link);
   font: inherit;
+  font-weight: 500;
+  text-decoration: none;
   cursor: pointer;
 }
 
-.file:hover {
-  color: var(--dsw-alias-label-primary);
-  text-decoration: underline;
+/* Flex centering tracks box centers, but the 22px text box carries its
+   glyphs below center (the baseline sits low in the leading); the nudge
+   drops the glyph onto the text's visual center. */
+.fileIcon {
+  flex: none;
+  width: 1.1em;
+  height: 1.1em;
+  position: relative;
+  top: 1.2px;
+}
+
+.fileName {
+  min-width: 0;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.file:hover,
+.file:focus-visible {
+  text-decoration: underline dotted;
+  text-underline-offset: 3px;
 }
 
 .file:focus-visible,
@@ -84,7 +108,7 @@
 }
 
 .lane:has(.more[data-shown='6']) > .showFolder {
-  display: block;
+  display: flex;
 }
 
 /* Each band budgets 96px per chip, 8px gaps, and 64px for the remainder. */
@@ -103,7 +127,7 @@
   }
 
   .lane:has(.more[data-shown='5']) > .showFolder {
-    display: block;
+    display: flex;
   }
 }
 
@@ -122,7 +146,7 @@
   }
 
   .lane:has(.more[data-shown='4']) > .showFolder {
-    display: block;
+    display: flex;
   }
 }
 
@@ -141,7 +165,7 @@
   }
 
   .lane:has(.more[data-shown='3']) > .showFolder {
-    display: block;
+    display: flex;
   }
 }
 
@@ -160,7 +184,7 @@
   }
 
   .lane:has(.more[data-shown='2']) > .showFolder {
-    display: block;
+    display: flex;
   }
 }
 
@@ -179,12 +203,14 @@
   }
 
   .lane:has(.more[data-shown='1']) > .showFolder {
-    display: block;
+    display: flex;
   }
 }
 
 .showFolder {
   justify-self: start;
+  align-items: center;
+  gap: 5px;
   margin: 0;
   padding: 0 2px;
   border: none;

+ 4 - 1
packages/client/ui-deliverables/src/client/ProducedFiles.tsx

@@ -1,4 +1,5 @@
 import { useEffect } from 'react'
+import { LinkIcon, classifyLinkPath } from '@deepseek-ai/dsh-client-ui-primitives'
 import type { HostObservable, InjectFace, PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'
 import type { TurnTailOwnerProps } from '@deepseek-ai/dsh-client-ui-chat/client'
 import { basename } from './turn-deliverables.ts'
@@ -57,7 +58,8 @@ export function ProducedFiles({
               aria-label={t('produced.open', { name: path })}
               onClick={() => { openFile(path) }}
             >
-              {basename(path)}
+              <LinkIcon kind={classifyLinkPath(path)} className={css.fileIcon} />
+              <span className={css.fileName}>{basename(path)}</span>
             </button>
           ))}
           {shown.map((_, index) => {
@@ -77,6 +79,7 @@ export function ProducedFiles({
             className={css.showFolder}
             onClick={() => { openFile('.') }}
           >
+            <LinkIcon kind="folder" className={css.fileIcon} />
             {t('produced.showInFolder')}
           </button>
         )}

+ 2 - 2
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: 758132bc9c193594a9407a105820633c75e58880
-README.zh.md: 8b3fffa9e73464d526c3a2f11b212797c2707c69
+README.md: ce18a574e0058b3e7e6f50e91fa2671af6563faa
+README.zh.md: 527f91a6b932ea05d8d6262058eba37de67c65ce

+ 1 - 1
packages/client/ui-primitives/README.md

@@ -29,7 +29,7 @@ Compose feature UI from these atoms whenever the web client needs a standard con
 
 ### Controls and icons
 
-`Button`, `Pill`, `Input`, `Menu`, `Modal`, `Tooltip`, `DisclosureRow`, `StateDot`, `HoverCard`, `Toast`, `ConnectionIndicator`, `RiskConfirmation`, and the `OnboardingSurface` first-run takeover cover the common interaction shapes. The `ic_ds_*` icon set and `FishLogo`/`BrandWordmark` marks fill brand and inline-icon slots. `ConnectionIndicator` renders a warning-colored disconnected action, a connecting label whose one-to-three dots advance every 500ms independently of retry timing, or a success-colored recovered status. Every state reserves the widest supplied label and uses fixed icon and text columns, so copy changes do not move or resize the control. Its owner supplies visibility, the recovery hold, localized labels, and the immediate-reconnect callback; the primitive uses no native title tooltip. `useAnchoredPosition` and `useAnchoredMaxHeight` keep floating panels and bottom-anchored overlays clamped to the viewport and following their anchor. `HoverCard` keeps its portaled preview reachable across the anchor gap and can expose a copy button through the `copyText` prop. `Toast` holds for the window its owner names through `holdMs`, because how long a banner has to stay depends on how much there is to read; the same value drives its unmount timer and the stylesheet's fade delay, so the two cannot disagree. `rankByName` is the `/` menu's shared candidate ranker for the command and skill sources: the query must be a case-insensitive ordered subsequence of the name; prefix hits rank first, then alignment score, then source order ([ranking decision](../../../.agents/notes/implemented/feature/2026-08-04-web-slash-command-fuzzy-discovery.md)).
+`Button`, `Pill`, `Input`, `Menu`, `Modal`, `Tooltip`, `DisclosureRow`, `StateDot`, `HoverCard`, `Toast`, `ConnectionIndicator`, `RiskConfirmation`, and the `OnboardingSurface` first-run takeover cover the common interaction shapes. The `ic_ds_*` icon set and `FishLogo`/`BrandWordmark` marks fill brand and inline-icon slots. `LinkIcon` draws the leading category glyph for clickable artifact links — globe, folder, code, image, document, or plain paper, all riding `currentColor` — and `classifyLinkPath` derives a file path's category from its extension. `ConnectionIndicator` renders a warning-colored disconnected action, a connecting label whose one-to-three dots advance every 500ms independently of retry timing, or a success-colored recovered status. Every state reserves the widest supplied label and uses fixed icon and text columns, so copy changes do not move or resize the control. Its owner supplies visibility, the recovery hold, localized labels, and the immediate-reconnect callback; the primitive uses no native title tooltip. `useAnchoredPosition` and `useAnchoredMaxHeight` keep floating panels and bottom-anchored overlays clamped to the viewport and following their anchor. `HoverCard` keeps its portaled preview reachable across the anchor gap and can expose a copy button through the `copyText` prop. `Toast` holds for the window its owner names through `holdMs`, because how long a banner has to stay depends on how much there is to read; the same value drives its unmount timer and the stylesheet's fade delay, so the two cannot disagree. `rankByName` is the `/` menu's shared candidate ranker for the command and skill sources: the query must be a case-insensitive ordered subsequence of the name; prefix hits rank first, then alignment score, then source order ([ranking decision](../../../.agents/notes/implemented/feature/2026-08-04-web-slash-command-fuzzy-discovery.md)).
 
 ### Rendering agent output
 

+ 1 - 1
packages/client/ui-primitives/README.zh.md

@@ -29,7 +29,7 @@ kind: "package-library"
 
 ### 控件与图标
 
-`Button`、`Pill`、`Input`、`Menu`、`Modal`、`Tooltip`、`DisclosureRow`、`StateDot`、`HoverCard`、`Toast`、`ConnectionIndicator`、`RiskConfirmation` 与首次运行接管层 `OnboardingSurface` 覆盖常见的交互形态。`ic_ds_*` 图标集与 `FishLogo`/`BrandWordmark` 标记填充品牌与行内图标 slot。`ConnectionIndicator` 可渲染警告色的断联操作、以独立于 retry 时序的 500ms 节奏推进一至三个点的连接中状态,或成功色的恢复状态。所有状态都为最长的输入 label 预留空间,并使用固定的图标列和文字列,因此文案变化不会移动控件或改变其宽度。它的 owner 提供可见性、恢复驻留时间、本地化 label 与立即重连回调;该原语不使用原生 title tooltip。`useAnchoredPosition` 与 `useAnchoredMaxHeight` 让浮动面板与底部锚定浮层始终钳制在视口内并跟随锚点。`HoverCard` 通过指针离开宽限期让采用 portal 的预览在跨过锚点间隙时仍可触及,并可通过 `copyText` prop 提供复制按钮。 `Toast` 的停留时长由使用方通过 `holdMs` 指定,因为横幅该留多久取决于有多少内容要读;同一个值同时驱动它的卸载定时器与样式表的淡出延迟,两者不可能再错位。 `rankByName` 是 `/` 菜单命令源与 skill 源共享的候选排序器:查询必须是名字的不区分大小写的有序子序列;前缀命中排最前,其次按对齐分数,再按来源顺序([排名决策](../../../.agents/notes/implemented/feature/2026-08-04-web-slash-command-fuzzy-discovery.zh.md))。
+`Button`、`Pill`、`Input`、`Menu`、`Modal`、`Tooltip`、`DisclosureRow`、`StateDot`、`HoverCard`、`Toast`、`ConnectionIndicator`、`RiskConfirmation` 与首次运行接管层 `OnboardingSurface` 覆盖常见的交互形态。`ic_ds_*` 图标集与 `FishLogo`/`BrandWordmark` 标记填充品牌与行内图标 slot。`LinkIcon` 为可点击产物链接绘制前置分类图形——地球、文件夹、代码、图片、文档或纸张,全部随 `currentColor`——`classifyLinkPath` 按扩展名推导文件路径的类别。`ConnectionIndicator` 可渲染警告色的断联操作、以独立于 retry 时序的 500ms 节奏推进一至三个点的连接中状态,或成功色的恢复状态。所有状态都为最长的输入 label 预留空间,并使用固定的图标列和文字列,因此文案变化不会移动控件或改变其宽度。它的 owner 提供可见性、恢复驻留时间、本地化 label 与立即重连回调;该原语不使用原生 title tooltip。`useAnchoredPosition` 与 `useAnchoredMaxHeight` 让浮动面板与底部锚定浮层始终钳制在视口内并跟随锚点。`HoverCard` 通过指针离开宽限期让采用 portal 的预览在跨过锚点间隙时仍可触及,并可通过 `copyText` prop 提供复制按钮。 `Toast` 的停留时长由使用方通过 `holdMs` 指定,因为横幅该留多久取决于有多少内容要读;同一个值同时驱动它的卸载定时器与样式表的淡出延迟,两者不可能再错位。 `rankByName` 是 `/` 菜单命令源与 skill 源共享的候选排序器:查询必须是名字的不区分大小写的有序子序列;前缀命中排最前,其次按对齐分数,再按来源顺序([排名决策](../../../.agents/notes/implemented/feature/2026-08-04-web-slash-command-fuzzy-discovery.zh.md))。
 
 ### 渲染 agent 输出
 

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 86 - 0
packages/client/ui-primitives/src/LinkIcon.tsx


+ 23 - 6
packages/client/ui-primitives/src/WebBlock.module.css

@@ -52,14 +52,27 @@
 }
 
 .sourceLink {
-  color: var(--dsw-alias-state-business-primary);
+  color: var(--dsw-alias-link);
   font-size: 14px;
+  font-weight: 500;
   line-height: 20px;
   word-break: break-word;
+  text-decoration: none;
 }
 
-.sourceLink:hover {
-  text-decoration: underline;
+/* Leading URL glyph inside source and fetch links; rides the link color.
+   1.1em follows each link's own font size. */
+.linkIcon {
+  width: 1.1em;
+  height: 1.1em;
+  vertical-align: -0.25em;
+  margin-right: 5px;
+}
+
+.sourceLink:hover,
+.sourceLink:focus-visible {
+  text-decoration: underline dotted;
+  text-underline-offset: 3px;
 }
 
 .snippet {
@@ -95,15 +108,19 @@
 }
 
 .fetchUrl {
-  color: var(--dsw-alias-state-business-primary);
+  color: var(--dsw-alias-link);
   font-family: var(--ds-font-family-code);
   font-size: 13px;
+  font-weight: 500;
   line-height: 19px;
   word-break: break-all;
+  text-decoration: none;
 }
 
-.fetchUrl:hover {
-  text-decoration: underline;
+.fetchUrl:hover,
+.fetchUrl:focus-visible {
+  text-decoration: underline dotted;
+  text-underline-offset: 3px;
 }
 
 .fetchMeta {

+ 2 - 0
packages/client/ui-primitives/src/WebBlock.tsx

@@ -1,5 +1,6 @@
 import clsx from 'clsx'
 import { MarkdownText, type MarkdownLabels } from './markdown/MarkdownText.tsx'
+import { LinkIcon } from './LinkIcon.tsx'
 import css from './WebBlock.module.css'
 
 /**
@@ -111,6 +112,7 @@ function SafeLink({ url, label, className }: { url: string; label: string; class
   if (href === undefined) return <span className={className}>{label}</span>
   return (
     <a className={className} href={href} target="_blank" rel="noopener noreferrer">
+      <LinkIcon kind="url" className={css.linkIcon} />
       {label}
     </a>
   )

+ 2 - 0
packages/client/ui-primitives/src/index.ts

@@ -28,6 +28,8 @@ export { BrandWordmark } from './BrandWordmark.tsx'
 export type { BrandWordmarkProps } from './BrandWordmark.tsx'
 export { ReferenceIcon } from './ReferenceIcon.tsx'
 export type { ReferenceIconKind, ReferenceIconProps } from './ReferenceIcon.tsx'
+export { LinkIcon, classifyLinkPath } from './LinkIcon.tsx'
+export type { LinkIconKind, LinkIconProps } from './LinkIcon.tsx'
 export { projectUserText } from './user-text.tsx'
 export { Tooltip } from './Tooltip.tsx'
 export type { TooltipSide } from './Tooltip.tsx'

+ 25 - 8
packages/client/ui-primitives/src/markdown/MarkdownText.module.css

@@ -57,9 +57,10 @@
 
 .markdown a {
   /* deepsuite markdown.css uses brand-text (blue in newDesign); this sheet
-     keeps design-platform brand-text as near-black, so links use the blue
-     business-primary alias instead. */
-  color: var(--dsw-alias-state-business-primary);
+     keeps design-platform brand-text as near-black, so links use the
+     dedicated link alias instead. */
+  color: var(--dsw-alias-link);
+  font-weight: 500;
   transition: box-shadow var(--ds-transition-duration) var(--ds-ease-in-out);
   position: relative;
   text-decoration: none;
@@ -75,13 +76,27 @@
 .markdown a:hover,
 .markdown a:focus {
   outline: none;
-  text-decoration: underline var(--dsw-alias-state-business-primary);
+  text-decoration: underline dotted var(--dsw-alias-link);
+  text-underline-offset: 3px;
 }
 
 .markdown a:focus-visible {
   box-shadow: 0 0 0 2px var(--dsw-alias-state-business-primary);
 }
 
+/* Leading link-category glyph inside anchors, file mentions, and
+   URL-promoted inline code: outline glyphs ride the link's currentColor.
+   1.1em keeps the glyph proportional to the surrounding text's size (it
+   shrinks with the code chip's 0.875em); the small negative offset seats
+   its optical center on the cap height. */
+.linkIcon {
+  flex: none;
+  width: 1.1em;
+  height: 1.1em;
+  vertical-align: -0.25em;
+  margin-right: 5px;
+}
+
 .markdown :where(ul, ol) {
   margin: 16px 0;
   padding-left: 18px;
@@ -151,6 +166,7 @@
   font-family: var(--ds-font-family-code);
   font-size: 0.875em !important;
   background-color: var(--dsw-alias-markdown-inline-code);
+  border: 0.5px solid var(--dsw-alias-border-l1);
   border-radius: 6px;
   padding: 0 5px;
 }
@@ -282,8 +298,8 @@
 }
 
 /* Inline file mention: the same link language this sheet gives anchors (and
-   thereby URL-promoted inline code) — link-blue at rest, underline only on
-   hover/focus. An underline at rest reads badly inside the code chip, where
+   thereby URL-promoted inline code) — link-blue at rest, dotted underline only
+   on hover/focus. An underline at rest reads badly inside the code chip, where
    it collides with monospace descenders and the pill background. */
 .fileMention {
   margin: 0;
@@ -291,7 +307,8 @@
   border: none;
   background: none;
   font: inherit;
-  color: var(--dsw-alias-state-business-primary);
+  font-weight: 500;
+  color: var(--dsw-alias-link);
   text-decoration: none;
   cursor: pointer;
 }
@@ -299,6 +316,6 @@
 .fileMention:hover,
 .fileMention:focus {
   outline: none;
-  text-decoration: underline var(--dsw-alias-state-business-primary);
+  text-decoration: underline dotted var(--dsw-alias-link);
   text-underline-offset: 3px;
 }

+ 18 - 5
packages/client/ui-primitives/src/markdown/render.tsx

@@ -24,6 +24,7 @@ import type {} from 'mdast-util-math'
 import { normalizeUri } from 'micromark-util-sanitize-uri'
 import { CodeBlock } from './CodeBlock.tsx'
 import { renderTexToReact } from './katex.tsx'
+import { LinkIcon, classifyLinkPath } from '../LinkIcon.tsx'
 import type { PositionedBlock } from './incremental.ts'
 import css from './MarkdownText.module.css'
 
@@ -263,6 +264,7 @@ function renderNode(node: Md.RootContent, key: Key, context: MarkdownRenderConte
               aria-label={mention.label}
               onClick={mention.open}
             >
+              <LinkIcon kind={classifyLinkPath(value)} className={css.linkIcon} />
               {value}
             </button>
           </code>
@@ -287,7 +289,7 @@ function renderNode(node: Md.RootContent, key: Key, context: MarkdownRenderConte
     case 'table':
       return renderTable(node, key, context)
     case 'link':
-      return renderAnchor(node.url, renderChildren(node.children, { ...context, inLink: true }), key)
+      return renderAnchor(node.url, renderChildren(node.children, { ...context, inLink: true }), key, !anchorWrapsOnlyImages(node.children))
     case 'linkReference':
       return renderLinkReference(node, key, context)
     case 'image':
@@ -465,8 +467,17 @@ function renderTableRow(
   return <tr key={key}>{cells}</tr>
 }
 
+/**
+ * True when an anchor's markdown children are all images, so the anchor is a
+ * clickable picture (badge, thumbnail): the leading URL glyph would dangle
+ * beside the image instead of leading link text, so those anchors skip it.
+ */
+function anchorWrapsOnlyImages(children: Md.PhrasingContent[]): boolean {
+  return children.length > 0 && children.every(child => child.type === 'image' || child.type === 'imageReference')
+}
+
 /** Anchor over an already-authored href: allowlisted or unwrapped, external links get the safe attributes. */
-function renderSafeLink(href: string, children: ReactNode[], key: Key): ReactNode {
+function renderSafeLink(href: string, children: ReactNode[], key: Key, glyph = true): ReactNode {
   const safeHref = sanitizeUrl(href)
   if (safeHref === '') return <Fragment key={key}>{children}</Fragment>
   const external = ['http:', 'https:'].includes(new URL(safeHref).protocol)
@@ -476,14 +487,15 @@ function renderSafeLink(href: string, children: ReactNode[], key: Key): ReactNod
       href={safeHref}
       {...(external ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
     >
+      {glyph && <LinkIcon kind="url" className={css.linkIcon} />}
       {children}
     </a>
   )
 }
 
 /** Anchor over a parsed markdown destination, which hast normalized before the allowlist saw it. */
-function renderAnchor(url: string, children: ReactNode[], key: Key): ReactNode {
-  return renderSafeLink(normalizeUri(url), children, key)
+function renderAnchor(url: string, children: ReactNode[], key: Key, glyph = true): ReactNode {
+  return renderSafeLink(normalizeUri(url), children, key, glyph)
 }
 
 /**
@@ -539,7 +551,8 @@ function renderLinkReference(
     // not an anchor, so mentions inside it stay live.
     return <Fragment key={key}>{'['}{renderChildren(node.children, context)}{referenceSuffix(node)}</Fragment>
   }
-  return renderAnchor(definition.url, renderChildren(node.children, { ...context, inLink: true }), key)
+  const rendered = renderChildren(node.children, { ...context, inLink: true })
+  return renderAnchor(definition.url, rendered, key, !anchorWrapsOnlyImages(node.children))
 }
 
 function renderImageReference(

+ 2 - 0
packages/client/ui-primitives/tests/fixtures/markdown-dom/cjk-strong-and-inline-code-url.settled.txt

@@ -10,6 +10,8 @@
   <p>
     <code>
       <a href="https://example.com/preview?q=one%20two#result" rel="noopener noreferrer" target="_blank">
+        <svg aria-hidden="true" class="_linkIcon_404681" fill="none" height="14" viewBox="0 0 20 20" width="14" xmlns="http://www.w3.org/2000/svg">
+          <path clip-rule="evenodd" d="M9.99951 1.68994C14.5889 1.68994 18.3098 5.41022 18.3101 9.99951C18.3101 14.589 14.589 18.3101 9.99951 18.3101C5.41022 18.3098 1.68994 14.5889 1.68994 9.99951C1.69017 5.41037 5.41037 1.69017 9.99951 1.68994ZM3.23193 10.7495C3.52924 13.463 5.41981 15.6965 7.94873 16.4946C7.74323 16.1395 7.56487 15.736 7.41162 15.3022C6.97892 14.0773 6.70459 12.4906 6.63232 10.7495H3.23193ZM13.3667 10.7495C13.2944 12.4906 13.0211 14.0773 12.5884 15.3022C12.4352 15.7359 12.2557 16.1386 12.0503 16.4937C14.5794 15.6956 16.4707 13.4631 16.7681 10.7495H13.3667ZM8.13428 10.7495C8.20587 12.3512 8.45916 13.7619 8.82666 14.8022C9.046 15.4231 9.29031 15.8714 9.52686 16.1499C9.76321 16.428 9.92505 16.4651 9.99951 16.4653C10.0737 16.4653 10.2362 16.4288 10.4731 16.1499C10.7097 15.8714 10.954 15.4231 11.1733 14.8022C11.5408 13.7619 11.7941 12.3512 11.8657 10.7495H8.13428ZM9.99951 3.18994C9.92505 3.19013 9.76321 3.22823 9.52686 3.50635C9.29035 3.78486 9.04595 4.23325 8.82666 4.854C8.43314 5.96808 8.16996 7.50638 8.12158 9.24951H11.8784C11.83 7.50638 11.5669 5.96808 11.1733 4.854C10.954 4.23324 10.7097 3.78486 10.4731 3.50635C10.2362 3.22748 10.0737 3.18994 9.99951 3.18994ZM12.27 3.57861C12.3862 3.82232 12.4924 4.0822 12.5884 4.354C13.0494 5.65902 13.33 7.37415 13.3784 9.24951H16.7681C16.4792 6.61525 14.6885 4.43369 12.27 3.57861ZM7.729 3.57861C5.3109 4.4338 3.52072 6.61541 3.23193 9.24951H6.62158C6.67001 7.37415 6.95063 5.65902 7.41162 4.354C7.50759 4.08233 7.61287 3.82221 7.729 3.57861Z" fill-rule="evenodd" fill="currentColor">
         #text "https://example.com/preview?q=one%20two#result"
     #text " 与 "
     <code>

+ 2 - 0
packages/client/ui-primitives/tests/fixtures/markdown-dom/cjk-strong-and-inline-code-url.streaming.txt

@@ -10,6 +10,8 @@
   <p>
     <code>
       <a href="https://example.com/preview?q=one%20two#result" rel="noopener noreferrer" target="_blank">
+        <svg aria-hidden="true" class="_linkIcon_404681" fill="none" height="14" viewBox="0 0 20 20" width="14" xmlns="http://www.w3.org/2000/svg">
+          <path clip-rule="evenodd" d="M9.99951 1.68994C14.5889 1.68994 18.3098 5.41022 18.3101 9.99951C18.3101 14.589 14.589 18.3101 9.99951 18.3101C5.41022 18.3098 1.68994 14.5889 1.68994 9.99951C1.69017 5.41037 5.41037 1.69017 9.99951 1.68994ZM3.23193 10.7495C3.52924 13.463 5.41981 15.6965 7.94873 16.4946C7.74323 16.1395 7.56487 15.736 7.41162 15.3022C6.97892 14.0773 6.70459 12.4906 6.63232 10.7495H3.23193ZM13.3667 10.7495C13.2944 12.4906 13.0211 14.0773 12.5884 15.3022C12.4352 15.7359 12.2557 16.1386 12.0503 16.4937C14.5794 15.6956 16.4707 13.4631 16.7681 10.7495H13.3667ZM8.13428 10.7495C8.20587 12.3512 8.45916 13.7619 8.82666 14.8022C9.046 15.4231 9.29031 15.8714 9.52686 16.1499C9.76321 16.428 9.92505 16.4651 9.99951 16.4653C10.0737 16.4653 10.2362 16.4288 10.4731 16.1499C10.7097 15.8714 10.954 15.4231 11.1733 14.8022C11.5408 13.7619 11.7941 12.3512 11.8657 10.7495H8.13428ZM9.99951 3.18994C9.92505 3.19013 9.76321 3.22823 9.52686 3.50635C9.29035 3.78486 9.04595 4.23325 8.82666 4.854C8.43314 5.96808 8.16996 7.50638 8.12158 9.24951H11.8784C11.83 7.50638 11.5669 5.96808 11.1733 4.854C10.954 4.23324 10.7097 3.78486 10.4731 3.50635C10.2362 3.22748 10.0737 3.18994 9.99951 3.18994ZM12.27 3.57861C12.3862 3.82232 12.4924 4.0822 12.5884 4.354C13.0494 5.65902 13.33 7.37415 13.3784 9.24951H16.7681C16.4792 6.61525 14.6885 4.43369 12.27 3.57861ZM7.729 3.57861C5.3109 4.4338 3.52072 6.61541 3.23193 9.24951H6.62158C6.67001 7.37415 6.95063 5.65902 7.41162 4.354C7.50759 4.08233 7.61287 3.82221 7.729 3.57861Z" fill-rule="evenodd" fill="currentColor">
         #text "https://example.com/preview?q=one%20two#result"
     #text " 与 "
     <code>

+ 2 - 0
packages/client/ui-primitives/tests/fixtures/markdown-dom/footnotes.settled.txt

@@ -18,6 +18,8 @@
         <p>
           #text "Footnote a body with "
           <a href="https://example.com" rel="noopener noreferrer" target="_blank">
+            <svg aria-hidden="true" class="_linkIcon_404681" fill="none" height="14" viewBox="0 0 20 20" width="14" xmlns="http://www.w3.org/2000/svg">
+              <path clip-rule="evenodd" d="M9.99951 1.68994C14.5889 1.68994 18.3098 5.41022 18.3101 9.99951C18.3101 14.589 14.589 18.3101 9.99951 18.3101C5.41022 18.3098 1.68994 14.5889 1.68994 9.99951C1.69017 5.41037 5.41037 1.69017 9.99951 1.68994ZM3.23193 10.7495C3.52924 13.463 5.41981 15.6965 7.94873 16.4946C7.74323 16.1395 7.56487 15.736 7.41162 15.3022C6.97892 14.0773 6.70459 12.4906 6.63232 10.7495H3.23193ZM13.3667 10.7495C13.2944 12.4906 13.0211 14.0773 12.5884 15.3022C12.4352 15.7359 12.2557 16.1386 12.0503 16.4937C14.5794 15.6956 16.4707 13.4631 16.7681 10.7495H13.3667ZM8.13428 10.7495C8.20587 12.3512 8.45916 13.7619 8.82666 14.8022C9.046 15.4231 9.29031 15.8714 9.52686 16.1499C9.76321 16.428 9.92505 16.4651 9.99951 16.4653C10.0737 16.4653 10.2362 16.4288 10.4731 16.1499C10.7097 15.8714 10.954 15.4231 11.1733 14.8022C11.5408 13.7619 11.7941 12.3512 11.8657 10.7495H8.13428ZM9.99951 3.18994C9.92505 3.19013 9.76321 3.22823 9.52686 3.50635C9.29035 3.78486 9.04595 4.23325 8.82666 4.854C8.43314 5.96808 8.16996 7.50638 8.12158 9.24951H11.8784C11.83 7.50638 11.5669 5.96808 11.1733 4.854C10.954 4.23324 10.7097 3.78486 10.4731 3.50635C10.2362 3.22748 10.0737 3.18994 9.99951 3.18994ZM12.27 3.57861C12.3862 3.82232 12.4924 4.0822 12.5884 4.354C13.0494 5.65902 13.33 7.37415 13.3784 9.24951H16.7681C16.4792 6.61525 14.6885 4.43369 12.27 3.57861ZM7.729 3.57861C5.3109 4.4338 3.52072 6.61541 3.23193 9.24951H6.62158C6.67001 7.37415 6.95063 5.65902 7.41162 4.354C7.50759 4.08233 7.61287 3.82221 7.729 3.57861Z" fill-rule="evenodd" fill="currentColor">
             #text "link"
           #text ". ↩ ↩"
           <sup>

+ 2 - 0
packages/client/ui-primitives/tests/fixtures/markdown-dom/footnotes.streaming.txt

@@ -18,6 +18,8 @@
         <p>
           #text "Footnote a body with "
           <a href="https://example.com" rel="noopener noreferrer" target="_blank">
+            <svg aria-hidden="true" class="_linkIcon_404681" fill="none" height="14" viewBox="0 0 20 20" width="14" xmlns="http://www.w3.org/2000/svg">
+              <path clip-rule="evenodd" d="M9.99951 1.68994C14.5889 1.68994 18.3098 5.41022 18.3101 9.99951C18.3101 14.589 14.589 18.3101 9.99951 18.3101C5.41022 18.3098 1.68994 14.5889 1.68994 9.99951C1.69017 5.41037 5.41037 1.69017 9.99951 1.68994ZM3.23193 10.7495C3.52924 13.463 5.41981 15.6965 7.94873 16.4946C7.74323 16.1395 7.56487 15.736 7.41162 15.3022C6.97892 14.0773 6.70459 12.4906 6.63232 10.7495H3.23193ZM13.3667 10.7495C13.2944 12.4906 13.0211 14.0773 12.5884 15.3022C12.4352 15.7359 12.2557 16.1386 12.0503 16.4937C14.5794 15.6956 16.4707 13.4631 16.7681 10.7495H13.3667ZM8.13428 10.7495C8.20587 12.3512 8.45916 13.7619 8.82666 14.8022C9.046 15.4231 9.29031 15.8714 9.52686 16.1499C9.76321 16.428 9.92505 16.4651 9.99951 16.4653C10.0737 16.4653 10.2362 16.4288 10.4731 16.1499C10.7097 15.8714 10.954 15.4231 11.1733 14.8022C11.5408 13.7619 11.7941 12.3512 11.8657 10.7495H8.13428ZM9.99951 3.18994C9.92505 3.19013 9.76321 3.22823 9.52686 3.50635C9.29035 3.78486 9.04595 4.23325 8.82666 4.854C8.43314 5.96808 8.16996 7.50638 8.12158 9.24951H11.8784C11.83 7.50638 11.5669 5.96808 11.1733 4.854C10.954 4.23324 10.7097 3.78486 10.4731 3.50635C10.2362 3.22748 10.0737 3.18994 9.99951 3.18994ZM12.27 3.57861C12.3862 3.82232 12.4924 4.0822 12.5884 4.354C13.0494 5.65902 13.33 7.37415 13.3784 9.24951H16.7681C16.4792 6.61525 14.6885 4.43369 12.27 3.57861ZM7.729 3.57861C5.3109 4.4338 3.52072 6.61541 3.23193 9.24951H6.62158C6.67001 7.37415 6.95063 5.65902 7.41162 4.354C7.50759 4.08233 7.61287 3.82221 7.729 3.57861Z" fill-rule="evenodd" fill="currentColor">
             #text "link"
           #text ". ↩ ↩"
           <sup>

+ 4 - 0
packages/client/ui-primitives/tests/fixtures/markdown-dom/gfm-strikethrough-and-literals.settled.txt

@@ -5,8 +5,12 @@
       #text "gone"
     #text " text with "
     <a href="http://www.example.com" rel="noopener noreferrer" target="_blank">
+      <svg aria-hidden="true" class="_linkIcon_404681" fill="none" height="14" viewBox="0 0 20 20" width="14" xmlns="http://www.w3.org/2000/svg">
+        <path clip-rule="evenodd" d="M9.99951 1.68994C14.5889 1.68994 18.3098 5.41022 18.3101 9.99951C18.3101 14.589 14.589 18.3101 9.99951 18.3101C5.41022 18.3098 1.68994 14.5889 1.68994 9.99951C1.69017 5.41037 5.41037 1.69017 9.99951 1.68994ZM3.23193 10.7495C3.52924 13.463 5.41981 15.6965 7.94873 16.4946C7.74323 16.1395 7.56487 15.736 7.41162 15.3022C6.97892 14.0773 6.70459 12.4906 6.63232 10.7495H3.23193ZM13.3667 10.7495C13.2944 12.4906 13.0211 14.0773 12.5884 15.3022C12.4352 15.7359 12.2557 16.1386 12.0503 16.4937C14.5794 15.6956 16.4707 13.4631 16.7681 10.7495H13.3667ZM8.13428 10.7495C8.20587 12.3512 8.45916 13.7619 8.82666 14.8022C9.046 15.4231 9.29031 15.8714 9.52686 16.1499C9.76321 16.428 9.92505 16.4651 9.99951 16.4653C10.0737 16.4653 10.2362 16.4288 10.4731 16.1499C10.7097 15.8714 10.954 15.4231 11.1733 14.8022C11.5408 13.7619 11.7941 12.3512 11.8657 10.7495H8.13428ZM9.99951 3.18994C9.92505 3.19013 9.76321 3.22823 9.52686 3.50635C9.29035 3.78486 9.04595 4.23325 8.82666 4.854C8.43314 5.96808 8.16996 7.50638 8.12158 9.24951H11.8784C11.83 7.50638 11.5669 5.96808 11.1733 4.854C10.954 4.23324 10.7097 3.78486 10.4731 3.50635C10.2362 3.22748 10.0737 3.18994 9.99951 3.18994ZM12.27 3.57861C12.3862 3.82232 12.4924 4.0822 12.5884 4.354C13.0494 5.65902 13.33 7.37415 13.3784 9.24951H16.7681C16.4792 6.61525 14.6885 4.43369 12.27 3.57861ZM7.729 3.57861C5.3109 4.4338 3.52072 6.61541 3.23193 9.24951H6.62158C6.67001 7.37415 6.95063 5.65902 7.41162 4.354C7.50759 4.08233 7.61287 3.82221 7.729 3.57861Z" fill-rule="evenodd" fill="currentColor">
       #text "www.example.com"
     #text " literal and "
     <a href="mailto:user@example.com">
+      <svg aria-hidden="true" class="_linkIcon_404681" fill="none" height="14" viewBox="0 0 20 20" width="14" xmlns="http://www.w3.org/2000/svg">
+        <path clip-rule="evenodd" d="M9.99951 1.68994C14.5889 1.68994 18.3098 5.41022 18.3101 9.99951C18.3101 14.589 14.589 18.3101 9.99951 18.3101C5.41022 18.3098 1.68994 14.5889 1.68994 9.99951C1.69017 5.41037 5.41037 1.69017 9.99951 1.68994ZM3.23193 10.7495C3.52924 13.463 5.41981 15.6965 7.94873 16.4946C7.74323 16.1395 7.56487 15.736 7.41162 15.3022C6.97892 14.0773 6.70459 12.4906 6.63232 10.7495H3.23193ZM13.3667 10.7495C13.2944 12.4906 13.0211 14.0773 12.5884 15.3022C12.4352 15.7359 12.2557 16.1386 12.0503 16.4937C14.5794 15.6956 16.4707 13.4631 16.7681 10.7495H13.3667ZM8.13428 10.7495C8.20587 12.3512 8.45916 13.7619 8.82666 14.8022C9.046 15.4231 9.29031 15.8714 9.52686 16.1499C9.76321 16.428 9.92505 16.4651 9.99951 16.4653C10.0737 16.4653 10.2362 16.4288 10.4731 16.1499C10.7097 15.8714 10.954 15.4231 11.1733 14.8022C11.5408 13.7619 11.7941 12.3512 11.8657 10.7495H8.13428ZM9.99951 3.18994C9.92505 3.19013 9.76321 3.22823 9.52686 3.50635C9.29035 3.78486 9.04595 4.23325 8.82666 4.854C8.43314 5.96808 8.16996 7.50638 8.12158 9.24951H11.8784C11.83 7.50638 11.5669 5.96808 11.1733 4.854C10.954 4.23324 10.7097 3.78486 10.4731 3.50635C10.2362 3.22748 10.0737 3.18994 9.99951 3.18994ZM12.27 3.57861C12.3862 3.82232 12.4924 4.0822 12.5884 4.354C13.0494 5.65902 13.33 7.37415 13.3784 9.24951H16.7681C16.4792 6.61525 14.6885 4.43369 12.27 3.57861ZM7.729 3.57861C5.3109 4.4338 3.52072 6.61541 3.23193 9.24951H6.62158C6.67001 7.37415 6.95063 5.65902 7.41162 4.354C7.50759 4.08233 7.61287 3.82221 7.729 3.57861Z" fill-rule="evenodd" fill="currentColor">
       #text "user@example.com"
     #text " email."

+ 4 - 0
packages/client/ui-primitives/tests/fixtures/markdown-dom/gfm-strikethrough-and-literals.streaming.txt

@@ -5,8 +5,12 @@
       #text "gone"
     #text " text with "
     <a href="http://www.example.com" rel="noopener noreferrer" target="_blank">
+      <svg aria-hidden="true" class="_linkIcon_404681" fill="none" height="14" viewBox="0 0 20 20" width="14" xmlns="http://www.w3.org/2000/svg">
+        <path clip-rule="evenodd" d="M9.99951 1.68994C14.5889 1.68994 18.3098 5.41022 18.3101 9.99951C18.3101 14.589 14.589 18.3101 9.99951 18.3101C5.41022 18.3098 1.68994 14.5889 1.68994 9.99951C1.69017 5.41037 5.41037 1.69017 9.99951 1.68994ZM3.23193 10.7495C3.52924 13.463 5.41981 15.6965 7.94873 16.4946C7.74323 16.1395 7.56487 15.736 7.41162 15.3022C6.97892 14.0773 6.70459 12.4906 6.63232 10.7495H3.23193ZM13.3667 10.7495C13.2944 12.4906 13.0211 14.0773 12.5884 15.3022C12.4352 15.7359 12.2557 16.1386 12.0503 16.4937C14.5794 15.6956 16.4707 13.4631 16.7681 10.7495H13.3667ZM8.13428 10.7495C8.20587 12.3512 8.45916 13.7619 8.82666 14.8022C9.046 15.4231 9.29031 15.8714 9.52686 16.1499C9.76321 16.428 9.92505 16.4651 9.99951 16.4653C10.0737 16.4653 10.2362 16.4288 10.4731 16.1499C10.7097 15.8714 10.954 15.4231 11.1733 14.8022C11.5408 13.7619 11.7941 12.3512 11.8657 10.7495H8.13428ZM9.99951 3.18994C9.92505 3.19013 9.76321 3.22823 9.52686 3.50635C9.29035 3.78486 9.04595 4.23325 8.82666 4.854C8.43314 5.96808 8.16996 7.50638 8.12158 9.24951H11.8784C11.83 7.50638 11.5669 5.96808 11.1733 4.854C10.954 4.23324 10.7097 3.78486 10.4731 3.50635C10.2362 3.22748 10.0737 3.18994 9.99951 3.18994ZM12.27 3.57861C12.3862 3.82232 12.4924 4.0822 12.5884 4.354C13.0494 5.65902 13.33 7.37415 13.3784 9.24951H16.7681C16.4792 6.61525 14.6885 4.43369 12.27 3.57861ZM7.729 3.57861C5.3109 4.4338 3.52072 6.61541 3.23193 9.24951H6.62158C6.67001 7.37415 6.95063 5.65902 7.41162 4.354C7.50759 4.08233 7.61287 3.82221 7.729 3.57861Z" fill-rule="evenodd" fill="currentColor">
       #text "www.example.com"
     #text " literal and "
     <a href="mailto:user@example.com">
+      <svg aria-hidden="true" class="_linkIcon_404681" fill="none" height="14" viewBox="0 0 20 20" width="14" xmlns="http://www.w3.org/2000/svg">
+        <path clip-rule="evenodd" d="M9.99951 1.68994C14.5889 1.68994 18.3098 5.41022 18.3101 9.99951C18.3101 14.589 14.589 18.3101 9.99951 18.3101C5.41022 18.3098 1.68994 14.5889 1.68994 9.99951C1.69017 5.41037 5.41037 1.69017 9.99951 1.68994ZM3.23193 10.7495C3.52924 13.463 5.41981 15.6965 7.94873 16.4946C7.74323 16.1395 7.56487 15.736 7.41162 15.3022C6.97892 14.0773 6.70459 12.4906 6.63232 10.7495H3.23193ZM13.3667 10.7495C13.2944 12.4906 13.0211 14.0773 12.5884 15.3022C12.4352 15.7359 12.2557 16.1386 12.0503 16.4937C14.5794 15.6956 16.4707 13.4631 16.7681 10.7495H13.3667ZM8.13428 10.7495C8.20587 12.3512 8.45916 13.7619 8.82666 14.8022C9.046 15.4231 9.29031 15.8714 9.52686 16.1499C9.76321 16.428 9.92505 16.4651 9.99951 16.4653C10.0737 16.4653 10.2362 16.4288 10.4731 16.1499C10.7097 15.8714 10.954 15.4231 11.1733 14.8022C11.5408 13.7619 11.7941 12.3512 11.8657 10.7495H8.13428ZM9.99951 3.18994C9.92505 3.19013 9.76321 3.22823 9.52686 3.50635C9.29035 3.78486 9.04595 4.23325 8.82666 4.854C8.43314 5.96808 8.16996 7.50638 8.12158 9.24951H11.8784C11.83 7.50638 11.5669 5.96808 11.1733 4.854C10.954 4.23324 10.7097 3.78486 10.4731 3.50635C10.2362 3.22748 10.0737 3.18994 9.99951 3.18994ZM12.27 3.57861C12.3862 3.82232 12.4924 4.0822 12.5884 4.354C13.0494 5.65902 13.33 7.37415 13.3784 9.24951H16.7681C16.4792 6.61525 14.6885 4.43369 12.27 3.57861ZM7.729 3.57861C5.3109 4.4338 3.52072 6.61541 3.23193 9.24951H6.62158C6.67001 7.37415 6.95063 5.65902 7.41162 4.354C7.50759 4.08233 7.61287 3.82221 7.729 3.57861Z" fill-rule="evenodd" fill="currentColor">
       #text "user@example.com"
     #text " email."

+ 16 - 0
packages/client/ui-primitives/tests/fixtures/markdown-dom/links-and-autolinks.settled.txt

@@ -1,25 +1,41 @@
 <div class="_markdown_404681">
   <p>
     <a href="https://example.com" rel="noopener noreferrer" target="_blank">
+      <svg aria-hidden="true" class="_linkIcon_404681" fill="none" height="14" viewBox="0 0 20 20" width="14" xmlns="http://www.w3.org/2000/svg">
+        <path clip-rule="evenodd" d="M9.99951 1.68994C14.5889 1.68994 18.3098 5.41022 18.3101 9.99951C18.3101 14.589 14.589 18.3101 9.99951 18.3101C5.41022 18.3098 1.68994 14.5889 1.68994 9.99951C1.69017 5.41037 5.41037 1.69017 9.99951 1.68994ZM3.23193 10.7495C3.52924 13.463 5.41981 15.6965 7.94873 16.4946C7.74323 16.1395 7.56487 15.736 7.41162 15.3022C6.97892 14.0773 6.70459 12.4906 6.63232 10.7495H3.23193ZM13.3667 10.7495C13.2944 12.4906 13.0211 14.0773 12.5884 15.3022C12.4352 15.7359 12.2557 16.1386 12.0503 16.4937C14.5794 15.6956 16.4707 13.4631 16.7681 10.7495H13.3667ZM8.13428 10.7495C8.20587 12.3512 8.45916 13.7619 8.82666 14.8022C9.046 15.4231 9.29031 15.8714 9.52686 16.1499C9.76321 16.428 9.92505 16.4651 9.99951 16.4653C10.0737 16.4653 10.2362 16.4288 10.4731 16.1499C10.7097 15.8714 10.954 15.4231 11.1733 14.8022C11.5408 13.7619 11.7941 12.3512 11.8657 10.7495H8.13428ZM9.99951 3.18994C9.92505 3.19013 9.76321 3.22823 9.52686 3.50635C9.29035 3.78486 9.04595 4.23325 8.82666 4.854C8.43314 5.96808 8.16996 7.50638 8.12158 9.24951H11.8784C11.83 7.50638 11.5669 5.96808 11.1733 4.854C10.954 4.23324 10.7097 3.78486 10.4731 3.50635C10.2362 3.22748 10.0737 3.18994 9.99951 3.18994ZM12.27 3.57861C12.3862 3.82232 12.4924 4.0822 12.5884 4.354C13.0494 5.65902 13.33 7.37415 13.3784 9.24951H16.7681C16.4792 6.61525 14.6885 4.43369 12.27 3.57861ZM7.729 3.57861C5.3109 4.4338 3.52072 6.61541 3.23193 9.24951H6.62158C6.67001 7.37415 6.95063 5.65902 7.41162 4.354C7.50759 4.08233 7.61287 3.82221 7.729 3.57861Z" fill-rule="evenodd" fill="currentColor">
       #text "https ok"
     #text " and "
     <a href="mailto:dev@example.com">
+      <svg aria-hidden="true" class="_linkIcon_404681" fill="none" height="14" viewBox="0 0 20 20" width="14" xmlns="http://www.w3.org/2000/svg">
+        <path clip-rule="evenodd" d="M9.99951 1.68994C14.5889 1.68994 18.3098 5.41022 18.3101 9.99951C18.3101 14.589 14.589 18.3101 9.99951 18.3101C5.41022 18.3098 1.68994 14.5889 1.68994 9.99951C1.69017 5.41037 5.41037 1.69017 9.99951 1.68994ZM3.23193 10.7495C3.52924 13.463 5.41981 15.6965 7.94873 16.4946C7.74323 16.1395 7.56487 15.736 7.41162 15.3022C6.97892 14.0773 6.70459 12.4906 6.63232 10.7495H3.23193ZM13.3667 10.7495C13.2944 12.4906 13.0211 14.0773 12.5884 15.3022C12.4352 15.7359 12.2557 16.1386 12.0503 16.4937C14.5794 15.6956 16.4707 13.4631 16.7681 10.7495H13.3667ZM8.13428 10.7495C8.20587 12.3512 8.45916 13.7619 8.82666 14.8022C9.046 15.4231 9.29031 15.8714 9.52686 16.1499C9.76321 16.428 9.92505 16.4651 9.99951 16.4653C10.0737 16.4653 10.2362 16.4288 10.4731 16.1499C10.7097 15.8714 10.954 15.4231 11.1733 14.8022C11.5408 13.7619 11.7941 12.3512 11.8657 10.7495H8.13428ZM9.99951 3.18994C9.92505 3.19013 9.76321 3.22823 9.52686 3.50635C9.29035 3.78486 9.04595 4.23325 8.82666 4.854C8.43314 5.96808 8.16996 7.50638 8.12158 9.24951H11.8784C11.83 7.50638 11.5669 5.96808 11.1733 4.854C10.954 4.23324 10.7097 3.78486 10.4731 3.50635C10.2362 3.22748 10.0737 3.18994 9.99951 3.18994ZM12.27 3.57861C12.3862 3.82232 12.4924 4.0822 12.5884 4.354C13.0494 5.65902 13.33 7.37415 13.3784 9.24951H16.7681C16.4792 6.61525 14.6885 4.43369 12.27 3.57861ZM7.729 3.57861C5.3109 4.4338 3.52072 6.61541 3.23193 9.24951H6.62158C6.67001 7.37415 6.95063 5.65902 7.41162 4.354C7.50759 4.08233 7.61287 3.82221 7.729 3.57861Z" fill-rule="evenodd" fill="currentColor">
       #text "mailto ok"
     #text "."
   <p>
     #text "relative dropped and js dropped and "
     <a href="HTTPS://example.com" rel="noopener noreferrer" target="_blank">
+      <svg aria-hidden="true" class="_linkIcon_404681" fill="none" height="14" viewBox="0 0 20 20" width="14" xmlns="http://www.w3.org/2000/svg">
+        <path clip-rule="evenodd" d="M9.99951 1.68994C14.5889 1.68994 18.3098 5.41022 18.3101 9.99951C18.3101 14.589 14.589 18.3101 9.99951 18.3101C5.41022 18.3098 1.68994 14.5889 1.68994 9.99951C1.69017 5.41037 5.41037 1.69017 9.99951 1.68994ZM3.23193 10.7495C3.52924 13.463 5.41981 15.6965 7.94873 16.4946C7.74323 16.1395 7.56487 15.736 7.41162 15.3022C6.97892 14.0773 6.70459 12.4906 6.63232 10.7495H3.23193ZM13.3667 10.7495C13.2944 12.4906 13.0211 14.0773 12.5884 15.3022C12.4352 15.7359 12.2557 16.1386 12.0503 16.4937C14.5794 15.6956 16.4707 13.4631 16.7681 10.7495H13.3667ZM8.13428 10.7495C8.20587 12.3512 8.45916 13.7619 8.82666 14.8022C9.046 15.4231 9.29031 15.8714 9.52686 16.1499C9.76321 16.428 9.92505 16.4651 9.99951 16.4653C10.0737 16.4653 10.2362 16.4288 10.4731 16.1499C10.7097 15.8714 10.954 15.4231 11.1733 14.8022C11.5408 13.7619 11.7941 12.3512 11.8657 10.7495H8.13428ZM9.99951 3.18994C9.92505 3.19013 9.76321 3.22823 9.52686 3.50635C9.29035 3.78486 9.04595 4.23325 8.82666 4.854C8.43314 5.96808 8.16996 7.50638 8.12158 9.24951H11.8784C11.83 7.50638 11.5669 5.96808 11.1733 4.854C10.954 4.23324 10.7097 3.78486 10.4731 3.50635C10.2362 3.22748 10.0737 3.18994 9.99951 3.18994ZM12.27 3.57861C12.3862 3.82232 12.4924 4.0822 12.5884 4.354C13.0494 5.65902 13.33 7.37415 13.3784 9.24951H16.7681C16.4792 6.61525 14.6885 4.43369 12.27 3.57861ZM7.729 3.57861C5.3109 4.4338 3.52072 6.61541 3.23193 9.24951H6.62158C6.67001 7.37415 6.95063 5.65902 7.41162 4.354C7.50759 4.08233 7.61287 3.82221 7.729 3.57861Z" fill-rule="evenodd" fill="currentColor">
       #text "upper kept"
     #text "."
   <p>
     <a href="https://deepseek.com" rel="noopener noreferrer" target="_blank">
+      <svg aria-hidden="true" class="_linkIcon_404681" fill="none" height="14" viewBox="0 0 20 20" width="14" xmlns="http://www.w3.org/2000/svg">
+        <path clip-rule="evenodd" d="M9.99951 1.68994C14.5889 1.68994 18.3098 5.41022 18.3101 9.99951C18.3101 14.589 14.589 18.3101 9.99951 18.3101C5.41022 18.3098 1.68994 14.5889 1.68994 9.99951C1.69017 5.41037 5.41037 1.69017 9.99951 1.68994ZM3.23193 10.7495C3.52924 13.463 5.41981 15.6965 7.94873 16.4946C7.74323 16.1395 7.56487 15.736 7.41162 15.3022C6.97892 14.0773 6.70459 12.4906 6.63232 10.7495H3.23193ZM13.3667 10.7495C13.2944 12.4906 13.0211 14.0773 12.5884 15.3022C12.4352 15.7359 12.2557 16.1386 12.0503 16.4937C14.5794 15.6956 16.4707 13.4631 16.7681 10.7495H13.3667ZM8.13428 10.7495C8.20587 12.3512 8.45916 13.7619 8.82666 14.8022C9.046 15.4231 9.29031 15.8714 9.52686 16.1499C9.76321 16.428 9.92505 16.4651 9.99951 16.4653C10.0737 16.4653 10.2362 16.4288 10.4731 16.1499C10.7097 15.8714 10.954 15.4231 11.1733 14.8022C11.5408 13.7619 11.7941 12.3512 11.8657 10.7495H8.13428ZM9.99951 3.18994C9.92505 3.19013 9.76321 3.22823 9.52686 3.50635C9.29035 3.78486 9.04595 4.23325 8.82666 4.854C8.43314 5.96808 8.16996 7.50638 8.12158 9.24951H11.8784C11.83 7.50638 11.5669 5.96808 11.1733 4.854C10.954 4.23324 10.7097 3.78486 10.4731 3.50635C10.2362 3.22748 10.0737 3.18994 9.99951 3.18994ZM12.27 3.57861C12.3862 3.82232 12.4924 4.0822 12.5884 4.354C13.0494 5.65902 13.33 7.37415 13.3784 9.24951H16.7681C16.4792 6.61525 14.6885 4.43369 12.27 3.57861ZM7.729 3.57861C5.3109 4.4338 3.52072 6.61541 3.23193 9.24951H6.62158C6.67001 7.37415 6.95063 5.65902 7.41162 4.354C7.50759 4.08233 7.61287 3.82221 7.729 3.57861Z" fill-rule="evenodd" fill="currentColor">
       #text "https://deepseek.com"
     #text " and bare autolink "
     <a href="https://autolink.example.com" rel="noopener noreferrer" target="_blank">
+      <svg aria-hidden="true" class="_linkIcon_404681" fill="none" height="14" viewBox="0 0 20 20" width="14" xmlns="http://www.w3.org/2000/svg">
+        <path clip-rule="evenodd" d="M9.99951 1.68994C14.5889 1.68994 18.3098 5.41022 18.3101 9.99951C18.3101 14.589 14.589 18.3101 9.99951 18.3101C5.41022 18.3098 1.68994 14.5889 1.68994 9.99951C1.69017 5.41037 5.41037 1.69017 9.99951 1.68994ZM3.23193 10.7495C3.52924 13.463 5.41981 15.6965 7.94873 16.4946C7.74323 16.1395 7.56487 15.736 7.41162 15.3022C6.97892 14.0773 6.70459 12.4906 6.63232 10.7495H3.23193ZM13.3667 10.7495C13.2944 12.4906 13.0211 14.0773 12.5884 15.3022C12.4352 15.7359 12.2557 16.1386 12.0503 16.4937C14.5794 15.6956 16.4707 13.4631 16.7681 10.7495H13.3667ZM8.13428 10.7495C8.20587 12.3512 8.45916 13.7619 8.82666 14.8022C9.046 15.4231 9.29031 15.8714 9.52686 16.1499C9.76321 16.428 9.92505 16.4651 9.99951 16.4653C10.0737 16.4653 10.2362 16.4288 10.4731 16.1499C10.7097 15.8714 10.954 15.4231 11.1733 14.8022C11.5408 13.7619 11.7941 12.3512 11.8657 10.7495H8.13428ZM9.99951 3.18994C9.92505 3.19013 9.76321 3.22823 9.52686 3.50635C9.29035 3.78486 9.04595 4.23325 8.82666 4.854C8.43314 5.96808 8.16996 7.50638 8.12158 9.24951H11.8784C11.83 7.50638 11.5669 5.96808 11.1733 4.854C10.954 4.23324 10.7097 3.78486 10.4731 3.50635C10.2362 3.22748 10.0737 3.18994 9.99951 3.18994ZM12.27 3.57861C12.3862 3.82232 12.4924 4.0822 12.5884 4.354C13.0494 5.65902 13.33 7.37415 13.3784 9.24951H16.7681C16.4792 6.61525 14.6885 4.43369 12.27 3.57861ZM7.729 3.57861C5.3109 4.4338 3.52072 6.61541 3.23193 9.24951H6.62158C6.67001 7.37415 6.95063 5.65902 7.41162 4.354C7.50759 4.08233 7.61287 3.82221 7.729 3.57861Z" fill-rule="evenodd" fill="currentColor">
       #text "https://autolink.example.com"
     #text " literal."
   <p>
     #text "[spaces encoded]("
     <a href="https://example.com/a" rel="noopener noreferrer" target="_blank">
+      <svg aria-hidden="true" class="_linkIcon_404681" fill="none" height="14" viewBox="0 0 20 20" width="14" xmlns="http://www.w3.org/2000/svg">
+        <path clip-rule="evenodd" d="M9.99951 1.68994C14.5889 1.68994 18.3098 5.41022 18.3101 9.99951C18.3101 14.589 14.589 18.3101 9.99951 18.3101C5.41022 18.3098 1.68994 14.5889 1.68994 9.99951C1.69017 5.41037 5.41037 1.69017 9.99951 1.68994ZM3.23193 10.7495C3.52924 13.463 5.41981 15.6965 7.94873 16.4946C7.74323 16.1395 7.56487 15.736 7.41162 15.3022C6.97892 14.0773 6.70459 12.4906 6.63232 10.7495H3.23193ZM13.3667 10.7495C13.2944 12.4906 13.0211 14.0773 12.5884 15.3022C12.4352 15.7359 12.2557 16.1386 12.0503 16.4937C14.5794 15.6956 16.4707 13.4631 16.7681 10.7495H13.3667ZM8.13428 10.7495C8.20587 12.3512 8.45916 13.7619 8.82666 14.8022C9.046 15.4231 9.29031 15.8714 9.52686 16.1499C9.76321 16.428 9.92505 16.4651 9.99951 16.4653C10.0737 16.4653 10.2362 16.4288 10.4731 16.1499C10.7097 15.8714 10.954 15.4231 11.1733 14.8022C11.5408 13.7619 11.7941 12.3512 11.8657 10.7495H8.13428ZM9.99951 3.18994C9.92505 3.19013 9.76321 3.22823 9.52686 3.50635C9.29035 3.78486 9.04595 4.23325 8.82666 4.854C8.43314 5.96808 8.16996 7.50638 8.12158 9.24951H11.8784C11.83 7.50638 11.5669 5.96808 11.1733 4.854C10.954 4.23324 10.7097 3.78486 10.4731 3.50635C10.2362 3.22748 10.0737 3.18994 9.99951 3.18994ZM12.27 3.57861C12.3862 3.82232 12.4924 4.0822 12.5884 4.354C13.0494 5.65902 13.33 7.37415 13.3784 9.24951H16.7681C16.4792 6.61525 14.6885 4.43369 12.27 3.57861ZM7.729 3.57861C5.3109 4.4338 3.52072 6.61541 3.23193 9.24951H6.62158C6.67001 7.37415 6.95063 5.65902 7.41162 4.354C7.50759 4.08233 7.61287 3.82221 7.729 3.57861Z" fill-rule="evenodd" fill="currentColor">
       #text "https://example.com/a"
     #text " b)"
+  <p>
+    <a href="https://example.com/target" rel="noopener noreferrer" target="_blank">
+      <img alt="linked badge" class="_image_404681" decoding="async" loading="lazy" referrerpolicy="no-referrer" src="https://example.com/badge.png">
+    #text " image-only anchors carry no leading glyph."

+ 16 - 0
packages/client/ui-primitives/tests/fixtures/markdown-dom/links-and-autolinks.streaming.txt

@@ -1,25 +1,41 @@
 <div class="_markdown_404681">
   <p>
     <a href="https://example.com" rel="noopener noreferrer" target="_blank">
+      <svg aria-hidden="true" class="_linkIcon_404681" fill="none" height="14" viewBox="0 0 20 20" width="14" xmlns="http://www.w3.org/2000/svg">
+        <path clip-rule="evenodd" d="M9.99951 1.68994C14.5889 1.68994 18.3098 5.41022 18.3101 9.99951C18.3101 14.589 14.589 18.3101 9.99951 18.3101C5.41022 18.3098 1.68994 14.5889 1.68994 9.99951C1.69017 5.41037 5.41037 1.69017 9.99951 1.68994ZM3.23193 10.7495C3.52924 13.463 5.41981 15.6965 7.94873 16.4946C7.74323 16.1395 7.56487 15.736 7.41162 15.3022C6.97892 14.0773 6.70459 12.4906 6.63232 10.7495H3.23193ZM13.3667 10.7495C13.2944 12.4906 13.0211 14.0773 12.5884 15.3022C12.4352 15.7359 12.2557 16.1386 12.0503 16.4937C14.5794 15.6956 16.4707 13.4631 16.7681 10.7495H13.3667ZM8.13428 10.7495C8.20587 12.3512 8.45916 13.7619 8.82666 14.8022C9.046 15.4231 9.29031 15.8714 9.52686 16.1499C9.76321 16.428 9.92505 16.4651 9.99951 16.4653C10.0737 16.4653 10.2362 16.4288 10.4731 16.1499C10.7097 15.8714 10.954 15.4231 11.1733 14.8022C11.5408 13.7619 11.7941 12.3512 11.8657 10.7495H8.13428ZM9.99951 3.18994C9.92505 3.19013 9.76321 3.22823 9.52686 3.50635C9.29035 3.78486 9.04595 4.23325 8.82666 4.854C8.43314 5.96808 8.16996 7.50638 8.12158 9.24951H11.8784C11.83 7.50638 11.5669 5.96808 11.1733 4.854C10.954 4.23324 10.7097 3.78486 10.4731 3.50635C10.2362 3.22748 10.0737 3.18994 9.99951 3.18994ZM12.27 3.57861C12.3862 3.82232 12.4924 4.0822 12.5884 4.354C13.0494 5.65902 13.33 7.37415 13.3784 9.24951H16.7681C16.4792 6.61525 14.6885 4.43369 12.27 3.57861ZM7.729 3.57861C5.3109 4.4338 3.52072 6.61541 3.23193 9.24951H6.62158C6.67001 7.37415 6.95063 5.65902 7.41162 4.354C7.50759 4.08233 7.61287 3.82221 7.729 3.57861Z" fill-rule="evenodd" fill="currentColor">
       #text "https ok"
     #text " and "
     <a href="mailto:dev@example.com">
+      <svg aria-hidden="true" class="_linkIcon_404681" fill="none" height="14" viewBox="0 0 20 20" width="14" xmlns="http://www.w3.org/2000/svg">
+        <path clip-rule="evenodd" d="M9.99951 1.68994C14.5889 1.68994 18.3098 5.41022 18.3101 9.99951C18.3101 14.589 14.589 18.3101 9.99951 18.3101C5.41022 18.3098 1.68994 14.5889 1.68994 9.99951C1.69017 5.41037 5.41037 1.69017 9.99951 1.68994ZM3.23193 10.7495C3.52924 13.463 5.41981 15.6965 7.94873 16.4946C7.74323 16.1395 7.56487 15.736 7.41162 15.3022C6.97892 14.0773 6.70459 12.4906 6.63232 10.7495H3.23193ZM13.3667 10.7495C13.2944 12.4906 13.0211 14.0773 12.5884 15.3022C12.4352 15.7359 12.2557 16.1386 12.0503 16.4937C14.5794 15.6956 16.4707 13.4631 16.7681 10.7495H13.3667ZM8.13428 10.7495C8.20587 12.3512 8.45916 13.7619 8.82666 14.8022C9.046 15.4231 9.29031 15.8714 9.52686 16.1499C9.76321 16.428 9.92505 16.4651 9.99951 16.4653C10.0737 16.4653 10.2362 16.4288 10.4731 16.1499C10.7097 15.8714 10.954 15.4231 11.1733 14.8022C11.5408 13.7619 11.7941 12.3512 11.8657 10.7495H8.13428ZM9.99951 3.18994C9.92505 3.19013 9.76321 3.22823 9.52686 3.50635C9.29035 3.78486 9.04595 4.23325 8.82666 4.854C8.43314 5.96808 8.16996 7.50638 8.12158 9.24951H11.8784C11.83 7.50638 11.5669 5.96808 11.1733 4.854C10.954 4.23324 10.7097 3.78486 10.4731 3.50635C10.2362 3.22748 10.0737 3.18994 9.99951 3.18994ZM12.27 3.57861C12.3862 3.82232 12.4924 4.0822 12.5884 4.354C13.0494 5.65902 13.33 7.37415 13.3784 9.24951H16.7681C16.4792 6.61525 14.6885 4.43369 12.27 3.57861ZM7.729 3.57861C5.3109 4.4338 3.52072 6.61541 3.23193 9.24951H6.62158C6.67001 7.37415 6.95063 5.65902 7.41162 4.354C7.50759 4.08233 7.61287 3.82221 7.729 3.57861Z" fill-rule="evenodd" fill="currentColor">
       #text "mailto ok"
     #text "."
   <p>
     #text "relative dropped and js dropped and "
     <a href="HTTPS://example.com" rel="noopener noreferrer" target="_blank">
+      <svg aria-hidden="true" class="_linkIcon_404681" fill="none" height="14" viewBox="0 0 20 20" width="14" xmlns="http://www.w3.org/2000/svg">
+        <path clip-rule="evenodd" d="M9.99951 1.68994C14.5889 1.68994 18.3098 5.41022 18.3101 9.99951C18.3101 14.589 14.589 18.3101 9.99951 18.3101C5.41022 18.3098 1.68994 14.5889 1.68994 9.99951C1.69017 5.41037 5.41037 1.69017 9.99951 1.68994ZM3.23193 10.7495C3.52924 13.463 5.41981 15.6965 7.94873 16.4946C7.74323 16.1395 7.56487 15.736 7.41162 15.3022C6.97892 14.0773 6.70459 12.4906 6.63232 10.7495H3.23193ZM13.3667 10.7495C13.2944 12.4906 13.0211 14.0773 12.5884 15.3022C12.4352 15.7359 12.2557 16.1386 12.0503 16.4937C14.5794 15.6956 16.4707 13.4631 16.7681 10.7495H13.3667ZM8.13428 10.7495C8.20587 12.3512 8.45916 13.7619 8.82666 14.8022C9.046 15.4231 9.29031 15.8714 9.52686 16.1499C9.76321 16.428 9.92505 16.4651 9.99951 16.4653C10.0737 16.4653 10.2362 16.4288 10.4731 16.1499C10.7097 15.8714 10.954 15.4231 11.1733 14.8022C11.5408 13.7619 11.7941 12.3512 11.8657 10.7495H8.13428ZM9.99951 3.18994C9.92505 3.19013 9.76321 3.22823 9.52686 3.50635C9.29035 3.78486 9.04595 4.23325 8.82666 4.854C8.43314 5.96808 8.16996 7.50638 8.12158 9.24951H11.8784C11.83 7.50638 11.5669 5.96808 11.1733 4.854C10.954 4.23324 10.7097 3.78486 10.4731 3.50635C10.2362 3.22748 10.0737 3.18994 9.99951 3.18994ZM12.27 3.57861C12.3862 3.82232 12.4924 4.0822 12.5884 4.354C13.0494 5.65902 13.33 7.37415 13.3784 9.24951H16.7681C16.4792 6.61525 14.6885 4.43369 12.27 3.57861ZM7.729 3.57861C5.3109 4.4338 3.52072 6.61541 3.23193 9.24951H6.62158C6.67001 7.37415 6.95063 5.65902 7.41162 4.354C7.50759 4.08233 7.61287 3.82221 7.729 3.57861Z" fill-rule="evenodd" fill="currentColor">
       #text "upper kept"
     #text "."
   <p>
     <a href="https://deepseek.com" rel="noopener noreferrer" target="_blank">
+      <svg aria-hidden="true" class="_linkIcon_404681" fill="none" height="14" viewBox="0 0 20 20" width="14" xmlns="http://www.w3.org/2000/svg">
+        <path clip-rule="evenodd" d="M9.99951 1.68994C14.5889 1.68994 18.3098 5.41022 18.3101 9.99951C18.3101 14.589 14.589 18.3101 9.99951 18.3101C5.41022 18.3098 1.68994 14.5889 1.68994 9.99951C1.69017 5.41037 5.41037 1.69017 9.99951 1.68994ZM3.23193 10.7495C3.52924 13.463 5.41981 15.6965 7.94873 16.4946C7.74323 16.1395 7.56487 15.736 7.41162 15.3022C6.97892 14.0773 6.70459 12.4906 6.63232 10.7495H3.23193ZM13.3667 10.7495C13.2944 12.4906 13.0211 14.0773 12.5884 15.3022C12.4352 15.7359 12.2557 16.1386 12.0503 16.4937C14.5794 15.6956 16.4707 13.4631 16.7681 10.7495H13.3667ZM8.13428 10.7495C8.20587 12.3512 8.45916 13.7619 8.82666 14.8022C9.046 15.4231 9.29031 15.8714 9.52686 16.1499C9.76321 16.428 9.92505 16.4651 9.99951 16.4653C10.0737 16.4653 10.2362 16.4288 10.4731 16.1499C10.7097 15.8714 10.954 15.4231 11.1733 14.8022C11.5408 13.7619 11.7941 12.3512 11.8657 10.7495H8.13428ZM9.99951 3.18994C9.92505 3.19013 9.76321 3.22823 9.52686 3.50635C9.29035 3.78486 9.04595 4.23325 8.82666 4.854C8.43314 5.96808 8.16996 7.50638 8.12158 9.24951H11.8784C11.83 7.50638 11.5669 5.96808 11.1733 4.854C10.954 4.23324 10.7097 3.78486 10.4731 3.50635C10.2362 3.22748 10.0737 3.18994 9.99951 3.18994ZM12.27 3.57861C12.3862 3.82232 12.4924 4.0822 12.5884 4.354C13.0494 5.65902 13.33 7.37415 13.3784 9.24951H16.7681C16.4792 6.61525 14.6885 4.43369 12.27 3.57861ZM7.729 3.57861C5.3109 4.4338 3.52072 6.61541 3.23193 9.24951H6.62158C6.67001 7.37415 6.95063 5.65902 7.41162 4.354C7.50759 4.08233 7.61287 3.82221 7.729 3.57861Z" fill-rule="evenodd" fill="currentColor">
       #text "https://deepseek.com"
     #text " and bare autolink "
     <a href="https://autolink.example.com" rel="noopener noreferrer" target="_blank">
+      <svg aria-hidden="true" class="_linkIcon_404681" fill="none" height="14" viewBox="0 0 20 20" width="14" xmlns="http://www.w3.org/2000/svg">
+        <path clip-rule="evenodd" d="M9.99951 1.68994C14.5889 1.68994 18.3098 5.41022 18.3101 9.99951C18.3101 14.589 14.589 18.3101 9.99951 18.3101C5.41022 18.3098 1.68994 14.5889 1.68994 9.99951C1.69017 5.41037 5.41037 1.69017 9.99951 1.68994ZM3.23193 10.7495C3.52924 13.463 5.41981 15.6965 7.94873 16.4946C7.74323 16.1395 7.56487 15.736 7.41162 15.3022C6.97892 14.0773 6.70459 12.4906 6.63232 10.7495H3.23193ZM13.3667 10.7495C13.2944 12.4906 13.0211 14.0773 12.5884 15.3022C12.4352 15.7359 12.2557 16.1386 12.0503 16.4937C14.5794 15.6956 16.4707 13.4631 16.7681 10.7495H13.3667ZM8.13428 10.7495C8.20587 12.3512 8.45916 13.7619 8.82666 14.8022C9.046 15.4231 9.29031 15.8714 9.52686 16.1499C9.76321 16.428 9.92505 16.4651 9.99951 16.4653C10.0737 16.4653 10.2362 16.4288 10.4731 16.1499C10.7097 15.8714 10.954 15.4231 11.1733 14.8022C11.5408 13.7619 11.7941 12.3512 11.8657 10.7495H8.13428ZM9.99951 3.18994C9.92505 3.19013 9.76321 3.22823 9.52686 3.50635C9.29035 3.78486 9.04595 4.23325 8.82666 4.854C8.43314 5.96808 8.16996 7.50638 8.12158 9.24951H11.8784C11.83 7.50638 11.5669 5.96808 11.1733 4.854C10.954 4.23324 10.7097 3.78486 10.4731 3.50635C10.2362 3.22748 10.0737 3.18994 9.99951 3.18994ZM12.27 3.57861C12.3862 3.82232 12.4924 4.0822 12.5884 4.354C13.0494 5.65902 13.33 7.37415 13.3784 9.24951H16.7681C16.4792 6.61525 14.6885 4.43369 12.27 3.57861ZM7.729 3.57861C5.3109 4.4338 3.52072 6.61541 3.23193 9.24951H6.62158C6.67001 7.37415 6.95063 5.65902 7.41162 4.354C7.50759 4.08233 7.61287 3.82221 7.729 3.57861Z" fill-rule="evenodd" fill="currentColor">
       #text "https://autolink.example.com"
     #text " literal."
   <p>
     #text "[spaces encoded]("
     <a href="https://example.com/a" rel="noopener noreferrer" target="_blank">
+      <svg aria-hidden="true" class="_linkIcon_404681" fill="none" height="14" viewBox="0 0 20 20" width="14" xmlns="http://www.w3.org/2000/svg">
+        <path clip-rule="evenodd" d="M9.99951 1.68994C14.5889 1.68994 18.3098 5.41022 18.3101 9.99951C18.3101 14.589 14.589 18.3101 9.99951 18.3101C5.41022 18.3098 1.68994 14.5889 1.68994 9.99951C1.69017 5.41037 5.41037 1.69017 9.99951 1.68994ZM3.23193 10.7495C3.52924 13.463 5.41981 15.6965 7.94873 16.4946C7.74323 16.1395 7.56487 15.736 7.41162 15.3022C6.97892 14.0773 6.70459 12.4906 6.63232 10.7495H3.23193ZM13.3667 10.7495C13.2944 12.4906 13.0211 14.0773 12.5884 15.3022C12.4352 15.7359 12.2557 16.1386 12.0503 16.4937C14.5794 15.6956 16.4707 13.4631 16.7681 10.7495H13.3667ZM8.13428 10.7495C8.20587 12.3512 8.45916 13.7619 8.82666 14.8022C9.046 15.4231 9.29031 15.8714 9.52686 16.1499C9.76321 16.428 9.92505 16.4651 9.99951 16.4653C10.0737 16.4653 10.2362 16.4288 10.4731 16.1499C10.7097 15.8714 10.954 15.4231 11.1733 14.8022C11.5408 13.7619 11.7941 12.3512 11.8657 10.7495H8.13428ZM9.99951 3.18994C9.92505 3.19013 9.76321 3.22823 9.52686 3.50635C9.29035 3.78486 9.04595 4.23325 8.82666 4.854C8.43314 5.96808 8.16996 7.50638 8.12158 9.24951H11.8784C11.83 7.50638 11.5669 5.96808 11.1733 4.854C10.954 4.23324 10.7097 3.78486 10.4731 3.50635C10.2362 3.22748 10.0737 3.18994 9.99951 3.18994ZM12.27 3.57861C12.3862 3.82232 12.4924 4.0822 12.5884 4.354C13.0494 5.65902 13.33 7.37415 13.3784 9.24951H16.7681C16.4792 6.61525 14.6885 4.43369 12.27 3.57861ZM7.729 3.57861C5.3109 4.4338 3.52072 6.61541 3.23193 9.24951H6.62158C6.67001 7.37415 6.95063 5.65902 7.41162 4.354C7.50759 4.08233 7.61287 3.82221 7.729 3.57861Z" fill-rule="evenodd" fill="currentColor">
       #text "https://example.com/a"
     #text " b)"
+  <p>
+    <a href="https://example.com/target" rel="noopener noreferrer" target="_blank">
+      <img alt="linked badge" class="_image_404681" decoding="async" loading="lazy" referrerpolicy="no-referrer" src="https://example.com/badge.png">
+    #text " image-only anchors carry no leading glyph."

+ 6 - 0
packages/client/ui-primitives/tests/fixtures/markdown-dom/reference-links-and-images.settled.txt

@@ -2,12 +2,18 @@
   <p>
     #text "A "
     <a href="https://example.com/ref" rel="noopener noreferrer" target="_blank">
+      <svg aria-hidden="true" class="_linkIcon_404681" fill="none" height="14" viewBox="0 0 20 20" width="14" xmlns="http://www.w3.org/2000/svg">
+        <path clip-rule="evenodd" d="M9.99951 1.68994C14.5889 1.68994 18.3098 5.41022 18.3101 9.99951C18.3101 14.589 14.589 18.3101 9.99951 18.3101C5.41022 18.3098 1.68994 14.5889 1.68994 9.99951C1.69017 5.41037 5.41037 1.69017 9.99951 1.68994ZM3.23193 10.7495C3.52924 13.463 5.41981 15.6965 7.94873 16.4946C7.74323 16.1395 7.56487 15.736 7.41162 15.3022C6.97892 14.0773 6.70459 12.4906 6.63232 10.7495H3.23193ZM13.3667 10.7495C13.2944 12.4906 13.0211 14.0773 12.5884 15.3022C12.4352 15.7359 12.2557 16.1386 12.0503 16.4937C14.5794 15.6956 16.4707 13.4631 16.7681 10.7495H13.3667ZM8.13428 10.7495C8.20587 12.3512 8.45916 13.7619 8.82666 14.8022C9.046 15.4231 9.29031 15.8714 9.52686 16.1499C9.76321 16.428 9.92505 16.4651 9.99951 16.4653C10.0737 16.4653 10.2362 16.4288 10.4731 16.1499C10.7097 15.8714 10.954 15.4231 11.1733 14.8022C11.5408 13.7619 11.7941 12.3512 11.8657 10.7495H8.13428ZM9.99951 3.18994C9.92505 3.19013 9.76321 3.22823 9.52686 3.50635C9.29035 3.78486 9.04595 4.23325 8.82666 4.854C8.43314 5.96808 8.16996 7.50638 8.12158 9.24951H11.8784C11.83 7.50638 11.5669 5.96808 11.1733 4.854C10.954 4.23324 10.7097 3.78486 10.4731 3.50635C10.2362 3.22748 10.0737 3.18994 9.99951 3.18994ZM12.27 3.57861C12.3862 3.82232 12.4924 4.0822 12.5884 4.354C13.0494 5.65902 13.33 7.37415 13.3784 9.24951H16.7681C16.4792 6.61525 14.6885 4.43369 12.27 3.57861ZM7.729 3.57861C5.3109 4.4338 3.52072 6.61541 3.23193 9.24951H6.62158C6.67001 7.37415 6.95063 5.65902 7.41162 4.354C7.50759 4.08233 7.61287 3.82221 7.729 3.57861Z" fill-rule="evenodd" fill="currentColor">
       #text "full"
     #text " reference, a "
     <a href="https://example.com/collapsed" rel="noopener noreferrer" target="_blank">
+      <svg aria-hidden="true" class="_linkIcon_404681" fill="none" height="14" viewBox="0 0 20 20" width="14" xmlns="http://www.w3.org/2000/svg">
+        <path clip-rule="evenodd" d="M9.99951 1.68994C14.5889 1.68994 18.3098 5.41022 18.3101 9.99951C18.3101 14.589 14.589 18.3101 9.99951 18.3101C5.41022 18.3098 1.68994 14.5889 1.68994 9.99951C1.69017 5.41037 5.41037 1.69017 9.99951 1.68994ZM3.23193 10.7495C3.52924 13.463 5.41981 15.6965 7.94873 16.4946C7.74323 16.1395 7.56487 15.736 7.41162 15.3022C6.97892 14.0773 6.70459 12.4906 6.63232 10.7495H3.23193ZM13.3667 10.7495C13.2944 12.4906 13.0211 14.0773 12.5884 15.3022C12.4352 15.7359 12.2557 16.1386 12.0503 16.4937C14.5794 15.6956 16.4707 13.4631 16.7681 10.7495H13.3667ZM8.13428 10.7495C8.20587 12.3512 8.45916 13.7619 8.82666 14.8022C9.046 15.4231 9.29031 15.8714 9.52686 16.1499C9.76321 16.428 9.92505 16.4651 9.99951 16.4653C10.0737 16.4653 10.2362 16.4288 10.4731 16.1499C10.7097 15.8714 10.954 15.4231 11.1733 14.8022C11.5408 13.7619 11.7941 12.3512 11.8657 10.7495H8.13428ZM9.99951 3.18994C9.92505 3.19013 9.76321 3.22823 9.52686 3.50635C9.29035 3.78486 9.04595 4.23325 8.82666 4.854C8.43314 5.96808 8.16996 7.50638 8.12158 9.24951H11.8784C11.83 7.50638 11.5669 5.96808 11.1733 4.854C10.954 4.23324 10.7097 3.78486 10.4731 3.50635C10.2362 3.22748 10.0737 3.18994 9.99951 3.18994ZM12.27 3.57861C12.3862 3.82232 12.4924 4.0822 12.5884 4.354C13.0494 5.65902 13.33 7.37415 13.3784 9.24951H16.7681C16.4792 6.61525 14.6885 4.43369 12.27 3.57861ZM7.729 3.57861C5.3109 4.4338 3.52072 6.61541 3.23193 9.24951H6.62158C6.67001 7.37415 6.95063 5.65902 7.41162 4.354C7.50759 4.08233 7.61287 3.82221 7.729 3.57861Z" fill-rule="evenodd" fill="currentColor">
       #text "collapsed"
     #text " one, and a "
     <a href="https://example.com/shortcut" rel="noopener noreferrer" target="_blank">
+      <svg aria-hidden="true" class="_linkIcon_404681" fill="none" height="14" viewBox="0 0 20 20" width="14" xmlns="http://www.w3.org/2000/svg">
+        <path clip-rule="evenodd" d="M9.99951 1.68994C14.5889 1.68994 18.3098 5.41022 18.3101 9.99951C18.3101 14.589 14.589 18.3101 9.99951 18.3101C5.41022 18.3098 1.68994 14.5889 1.68994 9.99951C1.69017 5.41037 5.41037 1.69017 9.99951 1.68994ZM3.23193 10.7495C3.52924 13.463 5.41981 15.6965 7.94873 16.4946C7.74323 16.1395 7.56487 15.736 7.41162 15.3022C6.97892 14.0773 6.70459 12.4906 6.63232 10.7495H3.23193ZM13.3667 10.7495C13.2944 12.4906 13.0211 14.0773 12.5884 15.3022C12.4352 15.7359 12.2557 16.1386 12.0503 16.4937C14.5794 15.6956 16.4707 13.4631 16.7681 10.7495H13.3667ZM8.13428 10.7495C8.20587 12.3512 8.45916 13.7619 8.82666 14.8022C9.046 15.4231 9.29031 15.8714 9.52686 16.1499C9.76321 16.428 9.92505 16.4651 9.99951 16.4653C10.0737 16.4653 10.2362 16.4288 10.4731 16.1499C10.7097 15.8714 10.954 15.4231 11.1733 14.8022C11.5408 13.7619 11.7941 12.3512 11.8657 10.7495H8.13428ZM9.99951 3.18994C9.92505 3.19013 9.76321 3.22823 9.52686 3.50635C9.29035 3.78486 9.04595 4.23325 8.82666 4.854C8.43314 5.96808 8.16996 7.50638 8.12158 9.24951H11.8784C11.83 7.50638 11.5669 5.96808 11.1733 4.854C10.954 4.23324 10.7097 3.78486 10.4731 3.50635C10.2362 3.22748 10.0737 3.18994 9.99951 3.18994ZM12.27 3.57861C12.3862 3.82232 12.4924 4.0822 12.5884 4.354C13.0494 5.65902 13.33 7.37415 13.3784 9.24951H16.7681C16.4792 6.61525 14.6885 4.43369 12.27 3.57861ZM7.729 3.57861C5.3109 4.4338 3.52072 6.61541 3.23193 9.24951H6.62158C6.67001 7.37415 6.95063 5.65902 7.41162 4.354C7.50759 4.08233 7.61287 3.82221 7.729 3.57861Z" fill-rule="evenodd" fill="currentColor">
       #text "shortcut"
     #text " one."
   <p>

+ 6 - 0
packages/client/ui-primitives/tests/fixtures/markdown-dom/reference-links-and-images.streaming.txt

@@ -2,12 +2,18 @@
   <p>
     #text "A "
     <a href="https://example.com/ref" rel="noopener noreferrer" target="_blank">
+      <svg aria-hidden="true" class="_linkIcon_404681" fill="none" height="14" viewBox="0 0 20 20" width="14" xmlns="http://www.w3.org/2000/svg">
+        <path clip-rule="evenodd" d="M9.99951 1.68994C14.5889 1.68994 18.3098 5.41022 18.3101 9.99951C18.3101 14.589 14.589 18.3101 9.99951 18.3101C5.41022 18.3098 1.68994 14.5889 1.68994 9.99951C1.69017 5.41037 5.41037 1.69017 9.99951 1.68994ZM3.23193 10.7495C3.52924 13.463 5.41981 15.6965 7.94873 16.4946C7.74323 16.1395 7.56487 15.736 7.41162 15.3022C6.97892 14.0773 6.70459 12.4906 6.63232 10.7495H3.23193ZM13.3667 10.7495C13.2944 12.4906 13.0211 14.0773 12.5884 15.3022C12.4352 15.7359 12.2557 16.1386 12.0503 16.4937C14.5794 15.6956 16.4707 13.4631 16.7681 10.7495H13.3667ZM8.13428 10.7495C8.20587 12.3512 8.45916 13.7619 8.82666 14.8022C9.046 15.4231 9.29031 15.8714 9.52686 16.1499C9.76321 16.428 9.92505 16.4651 9.99951 16.4653C10.0737 16.4653 10.2362 16.4288 10.4731 16.1499C10.7097 15.8714 10.954 15.4231 11.1733 14.8022C11.5408 13.7619 11.7941 12.3512 11.8657 10.7495H8.13428ZM9.99951 3.18994C9.92505 3.19013 9.76321 3.22823 9.52686 3.50635C9.29035 3.78486 9.04595 4.23325 8.82666 4.854C8.43314 5.96808 8.16996 7.50638 8.12158 9.24951H11.8784C11.83 7.50638 11.5669 5.96808 11.1733 4.854C10.954 4.23324 10.7097 3.78486 10.4731 3.50635C10.2362 3.22748 10.0737 3.18994 9.99951 3.18994ZM12.27 3.57861C12.3862 3.82232 12.4924 4.0822 12.5884 4.354C13.0494 5.65902 13.33 7.37415 13.3784 9.24951H16.7681C16.4792 6.61525 14.6885 4.43369 12.27 3.57861ZM7.729 3.57861C5.3109 4.4338 3.52072 6.61541 3.23193 9.24951H6.62158C6.67001 7.37415 6.95063 5.65902 7.41162 4.354C7.50759 4.08233 7.61287 3.82221 7.729 3.57861Z" fill-rule="evenodd" fill="currentColor">
       #text "full"
     #text " reference, a "
     <a href="https://example.com/collapsed" rel="noopener noreferrer" target="_blank">
+      <svg aria-hidden="true" class="_linkIcon_404681" fill="none" height="14" viewBox="0 0 20 20" width="14" xmlns="http://www.w3.org/2000/svg">
+        <path clip-rule="evenodd" d="M9.99951 1.68994C14.5889 1.68994 18.3098 5.41022 18.3101 9.99951C18.3101 14.589 14.589 18.3101 9.99951 18.3101C5.41022 18.3098 1.68994 14.5889 1.68994 9.99951C1.69017 5.41037 5.41037 1.69017 9.99951 1.68994ZM3.23193 10.7495C3.52924 13.463 5.41981 15.6965 7.94873 16.4946C7.74323 16.1395 7.56487 15.736 7.41162 15.3022C6.97892 14.0773 6.70459 12.4906 6.63232 10.7495H3.23193ZM13.3667 10.7495C13.2944 12.4906 13.0211 14.0773 12.5884 15.3022C12.4352 15.7359 12.2557 16.1386 12.0503 16.4937C14.5794 15.6956 16.4707 13.4631 16.7681 10.7495H13.3667ZM8.13428 10.7495C8.20587 12.3512 8.45916 13.7619 8.82666 14.8022C9.046 15.4231 9.29031 15.8714 9.52686 16.1499C9.76321 16.428 9.92505 16.4651 9.99951 16.4653C10.0737 16.4653 10.2362 16.4288 10.4731 16.1499C10.7097 15.8714 10.954 15.4231 11.1733 14.8022C11.5408 13.7619 11.7941 12.3512 11.8657 10.7495H8.13428ZM9.99951 3.18994C9.92505 3.19013 9.76321 3.22823 9.52686 3.50635C9.29035 3.78486 9.04595 4.23325 8.82666 4.854C8.43314 5.96808 8.16996 7.50638 8.12158 9.24951H11.8784C11.83 7.50638 11.5669 5.96808 11.1733 4.854C10.954 4.23324 10.7097 3.78486 10.4731 3.50635C10.2362 3.22748 10.0737 3.18994 9.99951 3.18994ZM12.27 3.57861C12.3862 3.82232 12.4924 4.0822 12.5884 4.354C13.0494 5.65902 13.33 7.37415 13.3784 9.24951H16.7681C16.4792 6.61525 14.6885 4.43369 12.27 3.57861ZM7.729 3.57861C5.3109 4.4338 3.52072 6.61541 3.23193 9.24951H6.62158C6.67001 7.37415 6.95063 5.65902 7.41162 4.354C7.50759 4.08233 7.61287 3.82221 7.729 3.57861Z" fill-rule="evenodd" fill="currentColor">
       #text "collapsed"
     #text " one, and a "
     <a href="https://example.com/shortcut" rel="noopener noreferrer" target="_blank">
+      <svg aria-hidden="true" class="_linkIcon_404681" fill="none" height="14" viewBox="0 0 20 20" width="14" xmlns="http://www.w3.org/2000/svg">
+        <path clip-rule="evenodd" d="M9.99951 1.68994C14.5889 1.68994 18.3098 5.41022 18.3101 9.99951C18.3101 14.589 14.589 18.3101 9.99951 18.3101C5.41022 18.3098 1.68994 14.5889 1.68994 9.99951C1.69017 5.41037 5.41037 1.69017 9.99951 1.68994ZM3.23193 10.7495C3.52924 13.463 5.41981 15.6965 7.94873 16.4946C7.74323 16.1395 7.56487 15.736 7.41162 15.3022C6.97892 14.0773 6.70459 12.4906 6.63232 10.7495H3.23193ZM13.3667 10.7495C13.2944 12.4906 13.0211 14.0773 12.5884 15.3022C12.4352 15.7359 12.2557 16.1386 12.0503 16.4937C14.5794 15.6956 16.4707 13.4631 16.7681 10.7495H13.3667ZM8.13428 10.7495C8.20587 12.3512 8.45916 13.7619 8.82666 14.8022C9.046 15.4231 9.29031 15.8714 9.52686 16.1499C9.76321 16.428 9.92505 16.4651 9.99951 16.4653C10.0737 16.4653 10.2362 16.4288 10.4731 16.1499C10.7097 15.8714 10.954 15.4231 11.1733 14.8022C11.5408 13.7619 11.7941 12.3512 11.8657 10.7495H8.13428ZM9.99951 3.18994C9.92505 3.19013 9.76321 3.22823 9.52686 3.50635C9.29035 3.78486 9.04595 4.23325 8.82666 4.854C8.43314 5.96808 8.16996 7.50638 8.12158 9.24951H11.8784C11.83 7.50638 11.5669 5.96808 11.1733 4.854C10.954 4.23324 10.7097 3.78486 10.4731 3.50635C10.2362 3.22748 10.0737 3.18994 9.99951 3.18994ZM12.27 3.57861C12.3862 3.82232 12.4924 4.0822 12.5884 4.354C13.0494 5.65902 13.33 7.37415 13.3784 9.24951H16.7681C16.4792 6.61525 14.6885 4.43369 12.27 3.57861ZM7.729 3.57861C5.3109 4.4338 3.52072 6.61541 3.23193 9.24951H6.62158C6.67001 7.37415 6.95063 5.65902 7.41162 4.354C7.50759 4.08233 7.61287 3.82221 7.729 3.57861Z" fill-rule="evenodd" fill="currentColor">
       #text "shortcut"
     #text " one."
   <p>

+ 2 - 0
packages/client/ui-primitives/tests/fixtures/markdown-dom/table-with-alignment.settled.txt

@@ -25,6 +25,8 @@
         <tr>
           <td style="text-align: left;">
             <a href="https://example.com" rel="noopener noreferrer" target="_blank">
+              <svg aria-hidden="true" class="_linkIcon_404681" fill="none" height="14" viewBox="0 0 20 20" width="14" xmlns="http://www.w3.org/2000/svg">
+                <path clip-rule="evenodd" d="M9.99951 1.68994C14.5889 1.68994 18.3098 5.41022 18.3101 9.99951C18.3101 14.589 14.589 18.3101 9.99951 18.3101C5.41022 18.3098 1.68994 14.5889 1.68994 9.99951C1.69017 5.41037 5.41037 1.69017 9.99951 1.68994ZM3.23193 10.7495C3.52924 13.463 5.41981 15.6965 7.94873 16.4946C7.74323 16.1395 7.56487 15.736 7.41162 15.3022C6.97892 14.0773 6.70459 12.4906 6.63232 10.7495H3.23193ZM13.3667 10.7495C13.2944 12.4906 13.0211 14.0773 12.5884 15.3022C12.4352 15.7359 12.2557 16.1386 12.0503 16.4937C14.5794 15.6956 16.4707 13.4631 16.7681 10.7495H13.3667ZM8.13428 10.7495C8.20587 12.3512 8.45916 13.7619 8.82666 14.8022C9.046 15.4231 9.29031 15.8714 9.52686 16.1499C9.76321 16.428 9.92505 16.4651 9.99951 16.4653C10.0737 16.4653 10.2362 16.4288 10.4731 16.1499C10.7097 15.8714 10.954 15.4231 11.1733 14.8022C11.5408 13.7619 11.7941 12.3512 11.8657 10.7495H8.13428ZM9.99951 3.18994C9.92505 3.19013 9.76321 3.22823 9.52686 3.50635C9.29035 3.78486 9.04595 4.23325 8.82666 4.854C8.43314 5.96808 8.16996 7.50638 8.12158 9.24951H11.8784C11.83 7.50638 11.5669 5.96808 11.1733 4.854C10.954 4.23324 10.7097 3.78486 10.4731 3.50635C10.2362 3.22748 10.0737 3.18994 9.99951 3.18994ZM12.27 3.57861C12.3862 3.82232 12.4924 4.0822 12.5884 4.354C13.0494 5.65902 13.33 7.37415 13.3784 9.24951H16.7681C16.4792 6.61525 14.6885 4.43369 12.27 3.57861ZM7.729 3.57861C5.3109 4.4338 3.52072 6.61541 3.23193 9.24951H6.62158C6.67001 7.37415 6.95063 5.65902 7.41162 4.354C7.50759 4.08233 7.61287 3.82221 7.729 3.57861Z" fill-rule="evenodd" fill="currentColor">
               #text "link"
           <td style="text-align: center;">
             <em>

+ 2 - 0
packages/client/ui-primitives/tests/fixtures/markdown-dom/table-with-alignment.streaming.txt

@@ -25,6 +25,8 @@
         <tr>
           <td style="text-align: left;">
             <a href="https://example.com" rel="noopener noreferrer" target="_blank">
+              <svg aria-hidden="true" class="_linkIcon_404681" fill="none" height="14" viewBox="0 0 20 20" width="14" xmlns="http://www.w3.org/2000/svg">
+                <path clip-rule="evenodd" d="M9.99951 1.68994C14.5889 1.68994 18.3098 5.41022 18.3101 9.99951C18.3101 14.589 14.589 18.3101 9.99951 18.3101C5.41022 18.3098 1.68994 14.5889 1.68994 9.99951C1.69017 5.41037 5.41037 1.69017 9.99951 1.68994ZM3.23193 10.7495C3.52924 13.463 5.41981 15.6965 7.94873 16.4946C7.74323 16.1395 7.56487 15.736 7.41162 15.3022C6.97892 14.0773 6.70459 12.4906 6.63232 10.7495H3.23193ZM13.3667 10.7495C13.2944 12.4906 13.0211 14.0773 12.5884 15.3022C12.4352 15.7359 12.2557 16.1386 12.0503 16.4937C14.5794 15.6956 16.4707 13.4631 16.7681 10.7495H13.3667ZM8.13428 10.7495C8.20587 12.3512 8.45916 13.7619 8.82666 14.8022C9.046 15.4231 9.29031 15.8714 9.52686 16.1499C9.76321 16.428 9.92505 16.4651 9.99951 16.4653C10.0737 16.4653 10.2362 16.4288 10.4731 16.1499C10.7097 15.8714 10.954 15.4231 11.1733 14.8022C11.5408 13.7619 11.7941 12.3512 11.8657 10.7495H8.13428ZM9.99951 3.18994C9.92505 3.19013 9.76321 3.22823 9.52686 3.50635C9.29035 3.78486 9.04595 4.23325 8.82666 4.854C8.43314 5.96808 8.16996 7.50638 8.12158 9.24951H11.8784C11.83 7.50638 11.5669 5.96808 11.1733 4.854C10.954 4.23324 10.7097 3.78486 10.4731 3.50635C10.2362 3.22748 10.0737 3.18994 9.99951 3.18994ZM12.27 3.57861C12.3862 3.82232 12.4924 4.0822 12.5884 4.354C13.0494 5.65902 13.33 7.37415 13.3784 9.24951H16.7681C16.4792 6.61525 14.6885 4.43369 12.27 3.57861ZM7.729 3.57861C5.3109 4.4338 3.52072 6.61541 3.23193 9.24951H6.62158C6.67001 7.37415 6.95063 5.65902 7.41162 4.354C7.50759 4.08233 7.61287 3.82221 7.729 3.57861Z" fill-rule="evenodd" fill="currentColor">
               #text "link"
           <td style="text-align: center;">
             <em>

+ 55 - 0
packages/client/ui-primitives/tests/link-icon.client.spec.tsx

@@ -0,0 +1,55 @@
+// @vitest-environment jsdom
+import { cleanup, render } from '@testing-library/react'
+import { afterEach, describe, expect, it } from 'vitest'
+import { LinkIcon, classifyLinkPath, type LinkIconKind } from '@deepseek-ai/dsh-client-ui-primitives'
+
+afterEach(cleanup)
+
+describe('classifyLinkPath', () => {
+  it.each([
+    ['src/markdown/render.tsx', 'code'],
+    ['site/index.html', 'code'],
+    ['data/export.CSV', 'code'],
+    ['shots/hero.png', 'image'],
+    ['report.pdf', 'document'],
+    ['deck.pptx', 'document'],
+    ['notes.unknownext', 'other'],
+    ['Makefile', 'other'],
+    ['C:\\work\\summary.docx', 'document'],
+    ['archive.tar/.hidden', 'other'],
+  ] as [string, LinkIconKind][])('%s → %s', (path, kind) => {
+    expect(classifyLinkPath(path)).toBe(kind)
+  })
+})
+
+describe('LinkIcon', () => {
+  const kinds: LinkIconKind[] = ['url', 'folder', 'code', 'image', 'document', 'other']
+
+  it.each(kinds)('%s renders a distinct aria-hidden svg with currentColor fills only', (kind) => {
+    const { container } = render(<LinkIcon kind={kind} />)
+    const svg = container.querySelector('svg')!
+    expect(svg).not.toBeNull()
+    expect(svg.getAttribute('aria-hidden')).toBe('true')
+    const markup = container.innerHTML
+    expect(markup).not.toMatch(/#[0-9a-fA-F]{3,8}"/)
+    expect(markup).toContain('currentColor')
+  })
+
+  it('every kind draws its own glyph', () => {
+    const paths = kinds.map((kind) => {
+      const { container } = render(<LinkIcon kind={kind} />)
+      return container.querySelector('path')!.getAttribute('d')
+    })
+    expect(new Set(paths).size).toBe(kinds.length)
+  })
+
+  it('defaults to the 14px inline link seat; size and className land on the svg', () => {
+    const { container } = render(<LinkIcon kind="url" />)
+    expect(container.querySelector('svg')!.getAttribute('width')).toBe('14')
+    const sized = render(<LinkIcon kind="folder" size={20} className="x" />)
+    const svg = sized.container.querySelector('svg')!
+    expect(svg.getAttribute('width')).toBe('20')
+    expect(svg.getAttribute('height')).toBe('20')
+    expect(svg.classList.contains('x')).toBe(true)
+  })
+})

+ 2 - 0
packages/client/ui-primitives/tests/markdown-dom-parity.client.spec.tsx

@@ -172,6 +172,8 @@ const CORPUS: Record<string, string> = {
     '<https://deepseek.com> and bare autolink https://autolink.example.com literal.',
     '',
     '[spaces encoded](https://example.com/a b)',
+    '',
+    '[![linked badge](https://example.com/badge.png)](https://example.com/target) image-only anchors carry no leading glyph.',
   ].join('\n'),
   'images': [
     '![https image](https://example.com/secure.png "img title")',

+ 4 - 2
packages/client/ui-theme/src/styles/design-platform.css

@@ -207,12 +207,13 @@ body {
   --dsw-alias-label-primary: var(--dsw-static-neutral-bluish-1000);
   --dsw-alias-label-secondary: var(--dsw-static-neutral-bluish-700);
   --dsw-alias-label-tertiary: var(--dsw-static-neutral-bluish-600);
+  --dsw-alias-link: var(--dsw-static-deepseek-500);
   --dsw-alias-markdown-citation: var(--dsw-static-neutral-bluish-100);
   --dsw-alias-markdown-code-block-banner: var(--dsw-static-neutral-bluish-50);
   --dsw-alias-markdown-code-block: var(--dsw-static-neutral-bluish-50);
   --dsw-alias-markdown-code-segment-selected: var(--dsw-static-neutral-bluish-00);
   --dsw-alias-markdown-code-segment-unselected: var(--dsw-static-neutral-bluish-75);
-  --dsw-alias-markdown-inline-code: var(--dsw-static-neutral-bluish-100);
+  --dsw-alias-markdown-inline-code: var(--dsw-static-neutral-50);
   --dsw-alias-markdown-placeholder: var(--dsw-static-neutral-bluish-60);
   --dsw-alias-markdown-tag: var(--dsw-static-neutral-bluish-75);
   --dsw-alias-scrollbar-bg-l1: var(--dsw-static-neutral-200);
@@ -299,12 +300,13 @@ body[data-ds-dark-theme] {
   --dsw-alias-label-primary: var(--dsw-static-neutral-bluish-50);
   --dsw-alias-label-secondary: var(--dsw-static-neutral-bluish-300);
   --dsw-alias-label-tertiary: var(--dsw-static-neutral-bluish-400);
+  --dsw-alias-link: var(--dsw-static-deepseek-400);
   --dsw-alias-markdown-citation: var(--dsw-static-neutral-bluish-800);
   --dsw-alias-markdown-code-block-banner: var(--dsw-static-neutral-bluish-850);
   --dsw-alias-markdown-code-block: var(--dsw-static-neutral-bluish-900);
   --dsw-alias-markdown-code-segment-selected: var(--dsw-static-neutral-bluish-800);
   --dsw-alias-markdown-code-segment-unselected: var(--dsw-static-neutral-bluish-900);
-  --dsw-alias-markdown-inline-code: var(--dsw-static-neutral-bluish-850);
+  --dsw-alias-markdown-inline-code: var(--dsw-static-neutral-800);
   --dsw-alias-markdown-placeholder: var(--dsw-static-neutral-bluish-850);
   --dsw-alias-markdown-tag: var(--dsw-static-neutral-bluish-850);
   --dsw-alias-scrollbar-bg-l1: var(--dsw-static-neutral-700);

+ 9 - 3
packages/client/ui-workflow-run/src/client/WorkflowRunPanel.module.css

@@ -184,9 +184,15 @@
 }
 
 .memberButton .memberLabel {
-  color: var(--dsw-alias-state-business-primary);
-  text-decoration: underline;
-  text-underline-position: from-font;
+  color: var(--dsw-alias-link);
+  font-weight: 500;
+  text-decoration: none;
+}
+
+.memberButton:hover .memberLabel,
+.memberButton:focus-visible .memberLabel {
+  text-decoration: underline dotted;
+  text-underline-offset: 3px;
 }
 
 .dotSlot {

+ 1 - 0
tsconfig.host.json

@@ -61,6 +61,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/clickable-links-gallery.e2e.ts",
     "apps/web/tests/queue-actions.e2e.ts",
     "apps/web/tests/queue-image.e2e.ts",
     "apps/web/tests/skill-invocation-policy.e2e.ts",

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно