fix(web): cover code-card overflow and refresh browser expectations

This commit is contained in:
yixiangihsiang committed 2026-09-21 22:40:55 +08:00
1 parent 8d2cd0cf72
commit f114233545
31 files changed
+248 -42

No files matched your search

+160
View File
@@ -0,0 +1,160 @@
/** Browser geometry for read offsets and horizontally scrolled diff fills. */
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 { launchWebScaffold, seedSession, watchConsole, type WebScaffold } from './scaffold.ts'
import { expandTurnProcesses, newEnglishPage, saveFailureShot } from './support.ts'
const LONG_LINE = 'source_columns_'.repeat(80)
const OFFSETS = [999, 99_999]
/** Seed durable tool results without invoking a model or allocating a large file. */
function layoutFixture(): string {
const calls: { name: string; args: Record<string, JsonValue>; result: string; meta: JsonValue }[] = OFFSETS.map((offset) => {
const path = `offset-${offset}.txt`
const lines = [{ number: offset, text: 'short' }, { number: offset + 1, text: LONG_LINE }]
return {
name: 'read', args: { file_path: path, offset },
result: `<path>${path}</path>\n<type>file</type>\n<content>\n${lines.map(line => `${line.number}: ${line.text}`).join('\n')}\n</content>`,
meta: { path, offset, lines, totalLines: offset + 2 },
}
})
const oldText = `old_${LONG_LINE}\nshort old`
const newText = `new_${LONG_LINE}\nshort new`
calls.push({
name: 'edit', args: { file_path: 'long.txt', old_string: oldText, new_string: newText },
result: 'Edited long.txt', meta: { diffs: [{ path: 'long.txt', oldText, newText }] },
})
const session = Session.create(SessionId('code-card-layout-source'))
session.append('turn/start', { turn: 1 })
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'Inspect the source windows and edit the long lines.' }], source: { kind: 'user' },
}), { surfaceOp: 'append' })
session.append('step/start', { turn: 1, step: 1 })
const namedCalls = calls.map((call, index) => ({ ...call, id: ToolCallId(`layout-${index}`), arguments: JSON.stringify(call.args) }))
session.append('assistant/message', {
stream: [], turn: 1, step: 1,
message: createAssistantMessage({
content: namedCalls.map(call => ({ type: 'tool-call', id: call.id, name: call.name, arguments: call.arguments })),
source: { provider: 'fixture', model: 'fixture' },
}),
}, { surfaceOp: 'append' })
for (const call of namedCalls) {
const source = session.append('tool/call', { turn: 1, step: 1, callId: call.id, name: call.name, arguments: call.arguments })
session.append('tool/result', {
turn: 1, step: 1, meta: call.meta,
message: createToolResultMessage({ callId: call.id, content: [{ type: 'text', text: call.result }], isError: false }),
}, { surfaceOp: 'append', sourceEventSeqs: [source.seq] })
}
session.append('step/end', { turn: 1, step: 1 })
session.append('step/start', { turn: 1, step: 2 })
session.append('assistant/message', {
stream: [], turn: 1, step: 2,
message: createAssistantMessage({
content: [{ type: 'text', text: 'CODE_LAYOUT_DONE' }], source: { provider: 'fixture', model: 'fixture' },
}),
}, { 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)), '',
].join('\n')
}
describe('web e2e: code-card layout', () => {
let scaffold: WebScaffold
let browser: Browser
let page: Page
let tripwire: ReturnType<typeof watchConsole>
beforeAll(async () => {
scaffold = await launchWebScaffold({})
await seedSession(scaffold, layoutFixture(), 'code-card-layout')
browser = await chromium.launch()
page = await newEnglishPage(browser)
tripwire = watchConsole(page)
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
await page.locator('[role="treeitem"]').first().click()
await page.locator('[role="treeitem"]').nth(1).click()
await page.getByText('CODE_LAYOUT_DONE', { exact: true }).waitFor()
await expandTurnProcesses(page)
for (const row of await page.locator('[data-variant="read"], [data-variant="edit"]').all()) {
await row.locator('[data-expandable]').click()
}
await page.locator('[data-read]').nth(1).waitFor()
await page.locator('[data-diff]').waitFor()
})
afterAll(async () => {
try { await browser?.close() } finally { await scaffold?.close() }
})
it.each(OFFSETS)('keeps offset %i line numbers clear of aligned source in both wrap modes', async (offset) => {
onTestFailed(() => saveFailureShot(page, `code-card-offset-${offset}`))
const card = page.locator('[data-read]').filter({ hasText: `offset-${offset}.txt` })
const wrap = card.getByRole('button', { name: 'Wrap lines', exact: true })
const heights: number[] = []
for (const wrapped of [false, true]) {
if (wrapped) await wrap.click()
expect(await wrap.getAttribute('aria-pressed')).toBe(String(wrapped))
const rows = await card.locator('[class*="gutter"]').evaluateAll(gutters => gutters.map((gutter) => {
const content = gutter.nextElementSibling!
const range = document.createRange()
range.selectNodeContents(gutter)
const glyphs = range.getBoundingClientRect()
const slot = gutter.getBoundingClientRect()
const source = content.getBoundingClientRect()
return {
fits: glyphs.left >= slot.left - 1 && glyphs.right <= slot.right + 1,
gap: source.left - glyphs.right, left: source.left, height: source.height,
}
}))
expect(rows).toHaveLength(2)
expect(rows.every(row => row.fits && row.gap >= 11)).toBe(true)
expect(rows[0]!.left).toBe(rows[1]!.left)
heights.push(rows[1]!.height)
}
expect(heights[1]!).toBeGreaterThan(heights[0]!)
await wrap.click()
})
it.each([false, true])('fills long diff rows through horizontal scrolling with dark=%s', async (dark) => {
onTestFailed(() => saveFailureShot(page, `code-card-diff-${dark}`))
await page.evaluate(value => document.body.toggleAttribute('data-ds-dark-theme', value), dark)
const card = page.locator('[data-diff]')
const body = card.locator(':scope > div').nth(1)
const wrap = card.getByRole('button', { name: 'Wrap lines', exact: true })
for (const wrapped of [false, true]) {
if (wrapped) await wrap.click()
const result = await body.evaluate((element) => {
element.scrollLeft = element.scrollWidth
const viewport = element.getBoundingClientRect()
const rows = [...element.children].filter(row => ['+ ', '- '].includes(getComputedStyle(row, '::before').content.replaceAll('"', '')))
return {
overflow: element.scrollWidth - element.clientWidth,
fills: rows.map(row => ({
width: row.getBoundingClientRect().width, right: row.getBoundingClientRect().right,
fill: getComputedStyle(row).backgroundColor, marker: getComputedStyle(row).boxShadow,
})),
scrollWidth: element.scrollWidth, right: viewport.right,
}
})
expect(result.fills).toHaveLength(4)
if (wrapped) expect(result.overflow).toBeLessThanOrEqual(1)
else expect(result.overflow).toBeGreaterThan(100)
for (const row of result.fills) {
expect(row.width).toBeGreaterThanOrEqual(result.scrollWidth - 1)
expect(row.right).toBeGreaterThanOrEqual(result.right - 1)
expect(row.fill).toMatch(dark ? /0\.12\)$/ : /0\.08\)$/)
expect(row.marker).toContain('inset')
}
}
await wrap.click()
expect(tripwire.pageErrors).toEqual([])
expect(tripwire.warnings).toEqual([])
})
})
@@ -137,7 +137,7 @@
- paragraph:
- img "Token preview"
- text: css
- button "Do not wrap lines" [pressed]
- button "Wrap lines" [pressed]
- button "Copy"
- code: "--inline-code: #F5F5F5;"
- paragraph: LINK_GALLERY_DONE
@@ -22,6 +22,7 @@
- tab "Host" [selected]
- tabpanel "Host":
- text: javascript
- button "Wrap lines" [pressed]
- button "Copy"
- code: "return { name: \"snapshot-noop\", apply(ctx) {} }"
- text: Result Defined snap-1/pkg-1 (snapshot noop); it is not running yet. Use cordis_run to activate this Package. Run controls live in the Cordis panel above Settings
@@ -1,4 +1,4 @@
- text: Code block
- button "Do not wrap lines" [pressed]
- button "Wrap lines" [pressed]
- button "Copy"
- code: echo WATERFALL
+4 -2
View File
@@ -27,7 +27,8 @@ const TRAJECTORY_EXPECTED = join(SNAPSHOT_DIR, 'trajectory.expected.md')
const TIMING_EXPECTED = join(SNAPSHOT_DIR, 'timing.expected.md')
const SEARCH_EXPECTED = join(SNAPSHOT_DIR, 'search-results.expected.md')
const TERMINAL_EXPECTED = join(SNAPSHOT_DIR, 'terminal-card.expected.md')
const CODE_CARD_EXPECTED = fileURLToPath(new URL('./expected/navigation-panes/code-card.expected.md', import.meta.url))
const CODE_CARD_DIR = fileURLToPath(new URL('./expected/navigation-panes', import.meta.url))
const CODE_CARD_EXPECTED = join(CODE_CARD_DIR, 'code-card.expected.md')
const MODE = webSnapshotMode()
const SEED_ID = 'navigation-panes-web-e2e'
const EXPORTED_LOG_FILE = `session.v${SESSION_FORMAT_VERSION}.jsonl`
@@ -231,7 +232,7 @@ describe('web e2e: navigation & panes over a rich seeded session', () => {
const copy = block.getByRole('button', { name: 'Copy', exact: true })
await copy.hover()
await page.getByRole('tooltip', { name: 'Copy', exact: true }).waitFor()
const unwrap = block.getByRole('button', { name: 'Do not wrap lines', exact: true })
const unwrap = block.getByRole('button', { name: 'Wrap lines', exact: true })
await unwrap.hover()
await page.getByRole('tooltip', { name: 'Do not wrap lines', exact: true }).waitFor()
await unwrap.click()
@@ -544,6 +545,7 @@ describe('web e2e: navigation & panes over a rich seeded session', () => {
}, 60_000)
it.skipIf(MODE === 'record')('keeps the recorded fixture inventory exact', async () => {
await assertFixtureInventory(CODE_CARD_DIR, ['code-card.expected.md'])
await assertFixtureInventory(SNAPSHOT_DIR, [
'session.v3.jsonl', 'search-results.expected.md', 'trajectory.expected.md',
'terminal-card.expected.md', 'timing.expected.md',
@@ -10,6 +10,7 @@
- button "Copy"
- button "Context injection runtime-context"
- text: ts
- button "Wrap lines" [pressed]
- button "Copy"
- code: "const first: number = 1 const second = \"two\" let tail"
- status: Deep diving...
@@ -85,7 +85,7 @@ async function fenceTree(block: ReturnType<Page['locator']>): Promise<FenceTree>
const pre = element.querySelector<HTMLPreElement>('pre.shiki')
if (pre === null) throw new Error('streaming fence did not render through the shiki arm')
return {
language: element.querySelector('[class*="infostring"]')?.textContent ?? '',
language: element.querySelector('[data-code-block-banner] [class*="language"]')?.textContent ?? '',
pre: {
className: pre.className,
style: pre.style.cssText,
+1
View File
@@ -24,6 +24,7 @@
"exclude": [
"tests/default-product-isolation.e2e.ts",
"tests/diff-context.e2e.ts",
"tests/code-card-layout.e2e.ts",
"tests/scaffold.ts",
"tests/voice-input.e2e.ts",
"tests/voice-setup.e2e.ts",
@@ -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: 069aea78dd08175a9ebb1e590bb03e97dc08daab
README.zh.md: d235e19a9f9c829d4d5d6587fdd147fda9a2884e
README.md: 09eab3840b7cfea7f84d12688787f6bd0344efc0
README.zh.md: c5e46728f9ae8dfe33dc3e67812602ecc6d58af0
+1 -1
View File
@@ -87,7 +87,7 @@ The nearest `MarkdownDelegateProvider` supplies optional `openExternalLink` and
`MarkdownText` defaults to `variant="body"`. Use `variant="compact"` for secondary content: its 13px text and 20px line height follow the content-size setting, all heading levels use the same size with weight 600, and paragraphs and lists use tighter spacing. Text, links, and code keep the tertiary color; dotted underlines distinguish links. Code headers scroll with their blocks. Tables and math stay enabled at the surrounding text size and scroll horizontally within the available width. Both variants share the parser and streaming cache.
`CodeBlock.toolbarLabels` enables the shared code-card header and spacing. `ReadBlock` and `DiffBlock` use the same header: secondary-colored language text, copy and wrap icons, and hover or keyboard-focus tooltips. Missing or unsupported languages use the owner’s localized code-block title; mixed-language diffs use that title too. Code cards share typography and content insets. Wrapping is local to each mounted card and never changes copied text; code fences start wrapped, while reads and diffs preserve source columns until wrapping is enabled. Compact Markdown omits this toolbar. With `toolbarLabels`, source previews can pass `wrap` to follow their owner’s preference and show only the language and copy icon; owner CSS retains the source layout.
`CodeBlock.toolbarLabels` enables the shared code-card header and spacing. `ReadBlock` and `DiffBlock` use the same header: secondary-colored language text, copy and wrap icons, and hover or keyboard-focus tooltips. Missing or unsupported languages use the owner’s localized code-block title; mixed-language diffs use that title too. Code cards share typography and content insets. Read gutters fit the largest returned line number, and diff fills cover the full scrollable row. The wrap toggle keeps a fixed accessible name and exposes its state through `aria-pressed`; its tooltip describes the next action. Wrapping is local to each mounted card and never changes copied text; code fences start wrapped, while reads and diffs preserve source columns until wrapping is enabled. Compact Markdown omits this toolbar. With `toolbarLabels`, source previews can pass `wrap` to follow their owner’s preference and show only the language and copy icon; owner CSS retains the source layout.
`DiffBlock` compares the old and new content by line. It shows actual additions and deletions with up to three neutral context lines on each side, separates distant changes with `⋯`, and excludes shared context from both summary and footer totals. Search stops beyond 256 line additions/deletions per fragment; those fragments display and count the complete old and new contents as a coarse replacement, including shared lines. Copy includes the full displayed diff with its prefixes. A final newline is treated as a terminator; differences only in the presence of a final newline are not displayed.
+1 -1
View File
@@ -87,7 +87,7 @@ kind: "package-library"
`MarkdownText` 默认为 `variant="body"`。次级内容使用 `variant="compact"`:其 13px 字号与 20px 行高跟随内容字号设置,各级标题保持同一字号并使用 600 字重,段落与列表采用更紧凑的间距。正文、链接和代码均保持 tertiary 颜色,以点状下划线区分链接。代码标题栏随代码块滚动。表格和公式仍然启用,使用周围文字的字号,并在可用宽度内横向滚动。两个变体共享解析器与流式缓存。
`CodeBlock.toolbarLabels` 启用共享代码卡片的标题栏和间距。`ReadBlock` 与 `DiffBlock` 使用相同的标题栏:secondary 色的语言名称、复制与换行图标,以及悬停或键盘聚焦时的提示。语言缺失或不受支持时显示调用方提供的本地化代码块标题;混合语言的 Diff 也使用该标题。代码卡片共用字体和内容内边距。换行设置仅属于当前挂载的卡片,不改变复制的文本;代码围栏默认换行,读取和差异卡片则在启用换行前保留源码列位置。紧凑 Markdown 不使用此工具栏。源码预览传入 `toolbarLabels` 后,可用 `wrap` 跟随所属视图的换行设置,仅显示语言和复制图标;所属视图的 CSS 保留源码布局。
`CodeBlock.toolbarLabels` 启用共享代码卡片的标题栏和间距。`ReadBlock` 与 `DiffBlock` 使用相同的标题栏:secondary 色的语言名称、复制与换行图标,以及悬停或键盘聚焦时的提示。语言缺失或不受支持时显示调用方提供的本地化代码块标题;混合语言的 Diff 也使用该标题。代码卡片共用字体和内容内边距。读取卡片的行号栏容纳返回窗口中的最大行号,Diff 底色覆盖完整可滚动行。换行按钮保留固定的无障碍名称,以 `aria-pressed` 表达状态;提示文本说明下一步操作。换行设置仅属于当前挂载的卡片,不改变复制的文本;代码围栏默认换行,读取和差异卡片则在启用换行前保留源码列位置。紧凑 Markdown 不使用此工具栏。源码预览传入 `toolbarLabels` 后,可用 `wrap` 跟随所属视图的换行设置,仅显示语言和复制图标;所属视图的 CSS 保留源码布局。
`DiffBlock` 按行比较新旧内容。它显示实际增删行及两侧最多三行中性上下文,用 `⋯` 分隔远距离改动,摘要和底部统计都不计入共享上下文。若一个片段需要超过 256 次行新增或删除,则停止精确比较;该片段按完整新旧内容显示和统计为粗粒度替换,包含共享行。复制包含完整显示 diff 及其前缀。末尾换行视为行终止符;仅末尾换行不同不会显示为改动。
@@ -47,7 +47,7 @@ export function CodeToolbar({ lang, title, status, labels, copyLabel, copiedLabe
<div className={css.actions}>
{status !== undefined && <span className={css.status}>{status}</span>}
{onWrap !== undefined && <Tooltip label={wrapLabel} side="top" portal>
<button type="button" className={css.action} aria-label={wrapLabel} aria-pressed={wrapped} onClick={onWrap}>
<button type="button" className={css.action} aria-label={labels.wrapLabel} aria-pressed={wrapped} onClick={onWrap}>
{wrapped ? <IconNowrapFillRegular size={14} /> : <IconWrapFillRegular size={14} />}
</button>
</Tooltip>}
@@ -3,6 +3,8 @@
}
.body {
display: grid;
grid-template-columns: minmax(max-content, 1fr);
padding: 6px 0 16px;
font: var(--dsw-font-markdown-code-block);
overflow-x: auto;
@@ -15,6 +17,10 @@
white-space: pre;
}
.block[data-code-wrap='true'] .body {
grid-template-columns: minmax(0, 1fr);
}
.block[data-code-wrap='true'] .line {
white-space: pre-wrap;
overflow-wrap: anywhere;
@@ -1,7 +1,3 @@
.block {
--dsl-read-gutter: 3ch;
}
.line {
display: flex;
min-height: 1lh;
@@ -1,4 +1,4 @@
import { useCallback, useMemo, useRef, useState, useSyncExternalStore } from 'react'
import { useCallback, useMemo, useRef, useState, useSyncExternalStore, type CSSProperties } from 'react'
import clsx from 'clsx'
import { FoldToggle } from './FoldToggle.tsx'
import { writeClipboard } from './clipboard.ts'
@@ -119,14 +119,18 @@ export function ReadBlock({
</div>
))
const gutterDigits = lines.reduce((digits, line) => Math.max(digits, String(line.number).length), 3)
const gutterStyle = { '--dsl-read-gutter': `${gutterDigits}ch` } as CSSProperties
const paired = lines.map((line, index): readonly [ReadBlockLine, readonly HighlightSpan[] | undefined] =>
[line, highlighted?.[index]])
return (
<div ref={rootRef} className={clsx(cardCss.card, css.block, className)} data-read="" data-code-wrap={wrapped}>
<div ref={rootRef} className={clsx(cardCss.card, css.block, className)} data-read="" data-code-wrap={wrapped} style={gutterStyle}>
<CodeToolbar
lang={lang} title={label} status={windowed ? labels.window(lines.length, totalLines) : undefined}
labels={labels} copyLabel={labels.copy} copiedLabel={labels.copied} copied={copied} wrapped={wrapped}
// Empty files must not replace the clipboard with empty text.
onCopy={lines.length > 0 ? onCopy : undefined} onWrap={() => { setWrapped(value => !value) }}
/>
<div className={cardCss.body}>
@@ -2,6 +2,7 @@
* Cordis-free React primitives styled only through `--dsw-*` tokens.
*/
export type { CodeToolbarLabels } from './CodeToolbar.tsx'
export { StateDot } from './StateDot.tsx'
export type { StateDotState } from './StateDot.tsx'
export { DisclosureRow } from './DisclosureRow.tsx'
@@ -104,10 +104,6 @@
color: var(--dsw-alias-label-primary);
}
.card {
--dsl-code-block-content-font: var(--dsw-font-markdown-code-block);
}
.card :where(pre) {
padding: 6px 18px 16px;
word-break: normal;
@@ -22,6 +22,7 @@ import clsx from 'clsx'
import type * as Md from 'mdast'
import type {} from 'mdast-util-math'
import { normalizeUri } from 'micromark-util-sanitize-uri'
import type { CodeToolbarLabels } from '../CodeToolbar.tsx'
import { CodeBlock } from './CodeBlock.tsx'
import { parseFileLink } from './file-link.ts'
import { renderTexToReact } from './katex.tsx'
@@ -37,7 +38,7 @@ export interface MarkdownCodeLabels {
/** Copy-button label during the post-copy confirmation window. */
copiedLabel: string
/** Shared card controls; omitted in compact and owner-styled source views. */
toolbarLabels?: import('../CodeToolbar.tsx').CodeToolbarLabels | undefined
toolbarLabels?: CodeToolbarLabels | undefined
}
/** Localized chrome for a Markdown document. */
@@ -37,10 +37,11 @@ describe('code-card controls', () => {
: kind === 'read'
? <ReadBlock lines={[{ number: 24, text: code }]} totalLines={30} lang="ts" labels={{ ...readBlockLabels, ...toolbarLabels }} />
: <DiffBlock diffs={[{ path: 'example.ts', oldText: 'old', newText: code }]} labels={{ ...diffBlockLabels, ...toolbarLabels }} />)
const button = screen.getByRole('button', { name: kind === 'code' ? 'Do not wrap lines' : 'Wrap lines' })
const button = screen.getByRole('button', { name: 'Wrap lines' })
const wasWrapped = button.getAttribute('aria-pressed') === 'true'
fireEvent.click(button)
expect(button.getAttribute('aria-pressed')).toBe(String(!wasWrapped))
expect(button.getAttribute('aria-label')).toBe('Wrap lines')
expect(view.container.textContent).toContain(code)
fireEvent.click(button)
expect(button.getAttribute('aria-pressed')).toBe(String(wasWrapped))
@@ -52,7 +53,7 @@ describe('code-card controls', () => {
fireEvent.mouseEnter(copy)
expect(screen.getByRole('tooltip').textContent).toBe('Copy')
fireEvent.mouseLeave(copy)
const wrap = screen.getByRole('button', { name: 'Do not wrap lines' })
const wrap = screen.getByRole('button', { name: 'Wrap lines' })
fireEvent.mouseEnter(wrap)
expect(screen.getByRole('tooltip').textContent).toBe('Do not wrap lines')
fireEvent.click(wrap)
+2 -2
View File
@@ -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-theme/README.md
README.md: 9526078829a882f18b08e5cc7ac8a9bb5ac7dfe4
README.zh.md: e487d85191d1afdb4d1d013459f94c0742d93b1d
README.md: f1fb30221bb176648cc99f1b2a39984ff3b84871
README.zh.md: c1e44eb5b9307dbb49e0868b0c68af3410b38928
+1 -1
View File
@@ -51,7 +51,7 @@ The service owns theme and font-size state and publishes snapshots. The ui-layou
### Stylesheets
`src/styles/` holds six sheets imported in order by ui-theme's dynamic client entry: `base.css`, `corner-shape.css`, `design-platform.css`, `scrollbar.css`, `gradient-shadow-text.css`, and `shiki.css`. The client bundle compiles and injects them as plugin-owned global styles, so unload and HMR remove them with ui-theme. `scrollbar.css` consumes the `--dsw-alias-scrollbar-*` tokens and must follow `design-platform.css`, which declares them. Status marks use their own semantic state tokens.
`src/styles/` holds six sheets imported in order by ui-theme's dynamic client entry: `base.css`, `corner-shape.css`, `design-platform.css`, `scrollbar.css`, `gradient-shadow-text.css`, and `shiki.css`. The client bundle compiles and injects them as plugin-owned global styles, so unload and HMR remove them with ui-theme. `scrollbar.css` consumes the `--dsw-alias-scrollbar-*` tokens and must follow `design-platform.css`, which declares them. Status marks use their own semantic state tokens. `design-platform.css` also owns the code-diff fill aliases and their static alpha palette entries; `shiki.css` owns syntax colors.
`corner-shape.css` smooths every rounded corner: inside `@supports (corner-shape: superellipse(1.5))` it defines `--dsw-corner-shape` and applies it to all elements and their `::before`/`::after` through the universal selector, so engines without `corner-shape` keep circular corners. Full-round shapes — `border-radius: 50%` circles and pill radii — pair `corner-shape: round` with their radius in the owning component sheet because a superellipse deforms them; the corner-shape stylesheet spec enforces that pairing across every package stylesheet.
+1 -1
View File
@@ -51,7 +51,7 @@ kind: "package-reference"
### 样式表
`src/styles/` 下有六张样式表,由 ui-theme 的动态客户端 entry 依次导入:`base.css`、`corner-shape.css`、`design-platform.css`、`scrollbar.css`、`gradient-shadow-text.css` 与 `shiki.css`。客户端 bundle 将其编译并注入为插件持有的全局样式,因此卸载与 HMR(热模块替换)会随 ui-theme 一同移除。`scrollbar.css` 消费 `--dsw-alias-scrollbar-*` token,必须排在声明这些 token 的 `design-platform.css` 之后。状态标记使用各自的语义状态 token。
`src/styles/` 下有六张样式表,由 ui-theme 的动态客户端 entry 依次导入:`base.css`、`corner-shape.css`、`design-platform.css`、`scrollbar.css`、`gradient-shadow-text.css` 与 `shiki.css`。客户端 bundle 将其编译并注入为插件持有的全局样式,因此卸载与 HMR(热模块替换)会随 ui-theme 一同移除。`scrollbar.css` 消费 `--dsw-alias-scrollbar-*` token,必须排在声明这些 token 的 `design-platform.css` 之后。状态标记使用各自的语义状态 token。`design-platform.css` 还负责代码差异底色的别名及其静态透明度色阶;`shiki.css` 负责语法颜色。
`corner-shape.css` 平滑所有圆角:在 `@supports (corner-shape: superellipse(1.5))` 内定义 `--dsw-corner-shape`,并通过通配选择器应用到所有元素及其 `::before`/`::after`,因此不支持 `corner-shape` 的引擎保持普通圆弧。正圆形状——`border-radius: 50%` 的圆与胶囊半径——因超级椭圆会使其变形,须在所属组件样式表中把 `corner-shape: round` 与半径声明配对;corner-shape 样式表 spec 跨全部包样式表强制这一配对。
@@ -34,6 +34,8 @@ body {
--dsw-static-green-100: rgb(230, 250, 237);
--dsw-static-green-400: rgb(78, 209, 126);
--dsw-static-green-500: rgb(34, 197, 94);
--dsw-static-green-500-a08: rgb(34 197 94 / 8%);
--dsw-static-green-500-a12: rgb(34 197 94 / 12%);
--dsw-static-green-900: rgb(35, 60, 44);
--dsw-static-neutral-00: rgb(255, 255, 255);
--dsw-static-neutral-1000: rgb(0, 0, 0);
@@ -72,9 +74,11 @@ body {
--dsw-static-neutral-bluish-950: rgb(21, 21, 23);
--dsw-static-red-100: rgb(254, 226, 226);
--dsw-static-red-400: rgb(242, 90, 90);
--dsw-static-red-400-a12: rgb(242 90 90 / 12%);
--dsw-static-red-500: rgb(239, 68, 68);
--dsw-static-red-50: rgb(254, 242, 242);
--dsw-static-red-600: rgb(236, 19, 19);
--dsw-static-red-600-a08: rgb(236 19 19 / 8%);
--dsw-static-red-900: rgb(87, 12, 12);
}
@@ -111,6 +115,8 @@ body[data-ds-dark-theme] {
--dsw-static-green-100: rgb(230, 250, 237);
--dsw-static-green-400: rgb(78, 209, 126);
--dsw-static-green-500: rgb(34, 197, 94);
--dsw-static-green-500-a08: rgb(34 197 94 / 8%);
--dsw-static-green-500-a12: rgb(34 197 94 / 12%);
--dsw-static-green-900: rgb(35, 60, 44);
--dsw-static-neutral-00: rgb(255, 255, 255);
--dsw-static-neutral-1000: rgb(0, 0, 0);
@@ -149,9 +155,11 @@ body[data-ds-dark-theme] {
--dsw-static-neutral-bluish-950: rgb(21, 21, 23);
--dsw-static-red-100: rgb(254, 226, 226);
--dsw-static-red-400: rgb(242, 90, 90);
--dsw-static-red-400-a12: rgb(242 90 90 / 12%);
--dsw-static-red-500: rgb(239, 68, 68);
--dsw-static-red-50: rgb(254, 242, 242);
--dsw-static-red-600: rgb(236, 19, 19);
--dsw-static-red-600-a08: rgb(236 19 19 / 8%);
--dsw-static-red-900: rgb(87, 12, 12);
}
@@ -226,6 +234,8 @@ body {
--dsw-alias-scrollbar-hover-l2: var(--dsw-static-neutral-300);
--dsw-alias-state-business-primary: var(--dsw-static-deepseek-500);
--dsw-alias-state-business-tertiary: var(--dsw-static-deepseek-100);
--dsw-alias-code-diff-added: var(--dsw-static-green-500-a08);
--dsw-alias-code-diff-deleted: var(--dsw-static-red-600-a08);
--dsw-alias-state-error-primary: var(--dsw-static-red-600);
--dsw-alias-state-error-secondary: var(--dsw-static-red-400);
--dsw-alias-state-idle-primary: var(--dsw-static-neutral-300);
@@ -324,6 +334,8 @@ body[data-ds-dark-theme] {
--dsw-alias-scrollbar-hover-l2: var(--dsw-static-neutral-550);
--dsw-alias-state-business-primary: var(--dsw-static-deepseek-400);
--dsw-alias-state-business-tertiary: var(--dsw-static-deepseek-800);
--dsw-alias-code-diff-added: var(--dsw-static-green-500-a12);
--dsw-alias-code-diff-deleted: var(--dsw-static-red-400-a12);
--dsw-alias-state-error-primary: var(--dsw-static-red-400);
--dsw-alias-state-error-secondary: var(--dsw-static-red-400);
--dsw-alias-state-idle-primary: var(--dsw-static-neutral-600);
@@ -4,11 +4,6 @@
cascade as every other token sheet. Background/foreground deliberately
alias the markdown code-block tokens so highlighted and plain blocks agree. */
body {
--dsw-alias-code-diff-added: color-mix(in srgb, var(--dsw-alias-state-success-primary) 8%, transparent);
--dsw-alias-code-diff-deleted: color-mix(in srgb, var(--dsw-alias-state-error-primary) 8%, transparent);
}
:root {
--shiki-foreground: var(--dsw-alias-label-primary);
--shiki-background: var(--dsw-alias-markdown-code-block);
@@ -24,8 +19,6 @@ body {
}
body[data-ds-dark-theme] {
--dsw-alias-code-diff-added: color-mix(in srgb, var(--dsw-alias-state-success-primary) 12%, transparent);
--dsw-alias-code-diff-deleted: color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent);
--shiki-token-constant: #4dabf7;
--shiki-token-string: #69db7c;
--shiki-token-comment: #adb5bd;
@@ -1,7 +1,7 @@
/** Localized copy adapters for Cordis-free UI primitives used by Tool cards. */
import type {
CodeBlockProps,
CodeToolbarLabels,
DiffBlockLabels,
MarkdownLabels,
ReadBlockLabels,
@@ -17,7 +17,7 @@ type T = TranslateNS<'conversation'>
* @param t - Conversation locale seat.
* @returns Language fallback and wrapping actions.
*/
export function codeToolbarLabels(t: T): NonNullable<CodeBlockProps['toolbarLabels']> {
export function codeToolbarLabels(t: T): CodeToolbarLabels {
return { codeLabel: t('codeBlock.title'), wrapLabel: t('codeBlock.wrap'), unwrapLabel: t('codeBlock.unwrap') }
}
@@ -2,6 +2,8 @@
- text: Edit
- button "large.txt"
- text: +130 -130
- text: Code block
- button "Wrap lines"
- button "Copy"
- text: large.txt - shared heading - old setting 0 - old setting 1 - old setting 2
- button "Expand 252 more diff lines": … 252 more lines
@@ -2,6 +2,8 @@
- text: Edit
- button "config.txt"
- text: +1 -1
- text: Code block
- button "Wrap lines"
- button "Copy"
- text: config.txt - mode=DEBUG + mode=RELEASE level=info └ +1 -1 · 1 file
- button "Inspect"
@@ -31,8 +31,12 @@
- text: — 随机选中的包:
- code: packages/context/session-reference
- text: 📦
- text: Code block
- button "Wrap lines" [pressed]
- button "Copy"
- code: session-reference ├── README.md ├── package.json ├── src │ ├── index.ts │ └── uri.ts └── tests └── session-reference.spec.ts
- text: Code block
- button "Wrap lines" [pressed]
- button "Copy"
- code: "session-reference/ ├── README.md # 包说明文档 ├── package.json # 包元数据与依赖声明 ├── src/ │ ├── index.ts # 入口,导出公共 API │ └── uri.ts # 会话引用 URI 相关实现 └── tests/ └── session-reference.spec.ts # 单元测试"
- paragraph: 这是一个很典型的轻量 TypeScript 包结构:入口 + 实现 + 测试。这一轮到此结束,等系统开启下一个 turn。
@@ -54,8 +58,12 @@
- text: — 随机选中的包:
- code: packages/llm/token-meter
- text: 📦
- text: Code block
- button "Wrap lines" [pressed]
- button "Copy"
- code: token-meter ├── README.md ├── package.json ├── src │ └── index.ts └── tests └── token-meter.spec.ts
- text: Code block
- button "Wrap lines" [pressed]
- button "Copy"
- code: "token-meter/ ├── README.md # 包说明文档 ├── package.json # 包元数据与依赖声明 ├── src/ │ └── index.ts # 入口,导出公共 API(token 计量逻辑) └── tests/ └── token-meter.spec.ts # 单元测试"
- paragraph:
@@ -76,14 +84,16 @@
- strong: Turn 1
- text: — 随机选中
- code: packages/context/session-reference
- text: ,输出了文件结构:
- text: ,输出了文件结构: Code block
- button "Wrap lines" [pressed]
- button "Copy"
- code: session-reference/ ├── README.md ├── package.json ├── src/ │ ├── index.ts │ └── uri.ts └── tests/ └── session-reference.spec.ts
- listitem:
- strong: Turn 2
- text: — 随机选中
- code: packages/llm/token-meter
- text: ,输出了文件结构:
- text: ,输出了文件结构: Code block
- button "Wrap lines" [pressed]
- button "Copy"
- code: token-meter/ ├── README.md ├── package.json ├── src/ │ └── index.ts └── tests/ └── token-meter.spec.ts
- paragraph:
@@ -17,8 +17,12 @@
- text: — 随机选中的包:
- code: packages/context/session-reference
- text: 📦
- text: Code block
- button "Wrap lines" [pressed]
- button "Copy"
- code: session-reference ├── README.md ├── package.json ├── src │ ├── index.ts │ └── uri.ts └── tests └── session-reference.spec.ts
- text: Code block
- button "Wrap lines" [pressed]
- button "Copy"
- code: "session-reference/ ├── README.md # 包说明文档 ├── package.json # 包元数据与依赖声明 ├── src/ │ ├── index.ts # 入口,导出公共 API │ └── uri.ts # 会话引用 URI 相关实现 └── tests/ └── session-reference.spec.ts # 单元测试"
- paragraph: 这是一个很典型的轻量 TypeScript 包结构:入口 + 实现 + 测试。这一轮到此结束,等系统开启下一个 turn。
@@ -36,14 +40,16 @@
- strong: Turn 1
- text: — 随机选中
- code: packages/context/session-reference
- text: ,输出了文件结构:
- text: ,输出了文件结构: Code block
- button "Wrap lines" [pressed]
- button "Copy"
- code: session-reference/ ├── README.md ├── package.json ├── src/ │ ├── index.ts │ └── uri.ts └── tests/ └── session-reference.spec.ts
- listitem:
- strong: Turn 2
- text: — 随机选中
- code: packages/llm/token-meter
- text: ,输出了文件结构:
- text: ,输出了文件结构: Code block
- button "Wrap lines" [pressed]
- button "Copy"
- code: token-meter/ ├── README.md ├── package.json ├── src/ │ └── index.ts └── tests/ └── token-meter.spec.ts
- paragraph:
+12 -2
View File
@@ -129,6 +129,7 @@
- list:
- listitem:
- text: json
- button "Wrap lines" [pressed]
- button "Copy"
- code: "{ \"type\": \"object\", \"properties\": { \"service\": { \"type\": \"string\" } } }"
- group:
@@ -136,6 +137,7 @@
- list:
- listitem:
- text: json
- button "Wrap lines" [pressed]
- button "Copy"
- code: "{ \"type\": \"object\" }"
- listitem:
@@ -154,6 +156,7 @@
- list:
- listitem:
- text: json
- button "Wrap lines" [pressed]
- button "Copy"
- code: "{ \"type\": \"object\", \"properties\": { \"root\": { \"type\": \"string\" } } }"
- group:
@@ -161,6 +164,7 @@
- list:
- listitem:
- text: json
- button "Wrap lines" [pressed]
- button "Copy"
- code: "{ \"type\": \"object\" }"
- button "Inspect"
@@ -224,6 +228,7 @@
- list:
- listitem:
- text: javascript
- button "Wrap lines" [pressed]
- button "Copy"
- code: "return { reviewed: 12, needsDetails: [\"list_agents\", \"job_list\", \"terminal_list\"] };"
- listitem:
@@ -440,7 +445,8 @@
- text: Recorded result
- list:
- listitem:
- text: job-demo-build Running
- text: job-demo-build Running Code block
- button "Wrap lines" [pressed]
- button "Copy"
- code: Building client assets… ✓ 184 modules transformed. dist/index.js 92.4 kB
- button "Inspect"
@@ -458,6 +464,8 @@
- text: preview Started terminal-demo-1
- term: Type
- definition: shell
- text: Code block
- button "Wrap lines" [pressed]
- button "Copy"
- code: /workspace/demo %
- button "Inspect"
@@ -465,7 +473,8 @@
- text: Recorded result
- list:
- listitem:
- text: terminal-demo-1 Lines 16–18 of 18
- text: terminal-demo-1 Lines 16–18 of 18 Code block
- button "Wrap lines" [pressed]
- button "Copy"
- code: "VITE v7.0.0 ready in {{duration}} Local: http://localhost:5173/ Watching for file changes…"
- button "Inspect"
@@ -545,6 +554,7 @@
- listitem:
- button "src/tool-details.ts:24:1"
- text: typescript
- button "Wrap lines" [pressed]
- button "Copy"
- code: "function renderToolDetails(model: ToolDetailsModel): ReactNode"
- paragraph: Render compact fields and list rows from a recorded tool result.
+1
View File
@@ -12,6 +12,7 @@
"apps/web/product-isolation.ts",
"apps/web/tests/default-product-isolation.e2e.ts",
"apps/web/tests/diff-context.e2e.ts",
"apps/web/tests/code-card-layout.e2e.ts",
"apps/web/tests/scaffold.ts",
"apps/web/tests/voice-input.e2e.ts",
"apps/web/tests/voice-setup.e2e.ts",