From bde18e994e8d5a606363a6fd36b1da4f4344c9db Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Mon, 19 Oct 2026 03:27:12 +0800 Subject: [PATCH 01/10] test(web): align fork snapshot clocks --- apps/web/tests/message-actions.e2e.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/apps/web/tests/message-actions.e2e.ts b/apps/web/tests/message-actions.e2e.ts index d70558dab4..466393228c 100644 --- a/apps/web/tests/message-actions.e2e.ts +++ b/apps/web/tests/message-actions.e2e.ts @@ -15,7 +15,7 @@ import { acknowledgeReloadConnectionLoss, assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts, launchWebScaffold, parseSeedFixture, realizeSeedFixture, renderSeedFixture, seedSession, watchConsole, webSnapshotMode, type WebScaffold, } from './scaffold.ts' -import { openSettings, newEnglishPage, pinBrowserClock, saveFailureShot, WEB_FIXTURE_TIME } from './support.ts' +import { openSettings, newEnglishPage, pinBrowserClock, pinHostClock, saveFailureShot, WEB_FIXTURE_TIME } from './support.ts' const SNAPSHOT_DIR = fileURLToPath(new URL('../../../snapshots/web/message-actions', import.meta.url)) // Borrowed read-only: this scenario needs any settled user+assistant pair, not @@ -191,6 +191,7 @@ describe('web e2e: message IconActions and clocks on settled history', () => { let browser: Browser let page: Page let tripwire: ReturnType + let hostClock: ReturnType | undefined beforeAll(async () => { scaffold = await launchWebScaffold() @@ -211,6 +212,8 @@ describe('web e2e: message IconActions and clocks on settled history', () => { await seedSession(scaffold, raw, SEED_ID, undefined, { createdAt: WEB_FIXTURE_TIME - 90_000 }) browser = await chromium.launch() page = await newEnglishPage(browser) + // Fork summaries and browser placeholders must rank against the same clock. + hostClock = pinHostClock() await pinBrowserClock(page) tripwire = watchConsole(page) await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' }) @@ -218,8 +221,12 @@ describe('web e2e: message IconActions and clocks on settled history', () => { }, 120_000) afterAll(async () => { - await browser?.close() - await scaffold?.close() + try { + await browser?.close() + await scaffold?.close() + } finally { + hostClock?.mockRestore() + } }) it.skipIf(MODE === 'record')('enables branch only on the completed transcript tail', async () => { From 36a80c41b336a671ee9fe7611010cd365d00e782 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Mon, 19 Oct 2026 03:35:05 +0800 Subject: [PATCH 02/10] test(web): explain the adjacent fork clock pins --- apps/web/tests/message-actions.e2e.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/web/tests/message-actions.e2e.ts b/apps/web/tests/message-actions.e2e.ts index 466393228c..2f96305714 100644 --- a/apps/web/tests/message-actions.e2e.ts +++ b/apps/web/tests/message-actions.e2e.ts @@ -212,7 +212,9 @@ describe('web e2e: message IconActions and clocks on settled history', () => { await seedSession(scaffold, raw, SEED_ID, undefined, { createdAt: WEB_FIXTURE_TIME - 90_000 }) browser = await chromium.launch() page = await newEnglishPage(browser) - // Fork summaries and browser placeholders must rank against the same clock. + // Fork summaries use Host time; placeholders use browser time. Each pin + // starts at the fixture date, so keep these calls adjacent: setup between + // them can create enough clock skew to reverse the fork order. hostClock = pinHostClock() await pinBrowserClock(page) tripwire = watchConsole(page) From ce7414da1497943bf3bd5d0ded3c64f8af5c61be Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Mon, 19 Oct 2026 03:58:22 +0800 Subject: [PATCH 03/10] test(office): report render routing failure evidence --- .../skill/skill-office/tests/render-tools.e2e.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/skill/skill-office/tests/render-tools.e2e.ts b/packages/skill/skill-office/tests/render-tools.e2e.ts index f2dc0cfbf2..d795b86aec 100644 --- a/packages/skill/skill-office/tests/render-tools.e2e.ts +++ b/packages/skill/skill-office/tests/render-tools.e2e.ts @@ -63,17 +63,22 @@ it.skipIf(!process.env.DEEPSEEK_API_KEY || !runtime).each([{ format: 'docx', mod const pixels = await execa(python!, ['-c', "from pathlib import Path\nfrom PIL import Image\nfiles=list(Path('preview').glob('*.png'))\nassert files\nfor f in files:\n im=Image.open(f).convert('RGBA')\n visible=Image.alpha_composite(Image.new('RGBA',im.size,'white'),im).convert('RGB')\n print('content' if any(a!=b for a,b in visible.getextrema()) else 'blank')"], { cwd }) expect(pixels.exitCode).toBe(0) if (format === 'pdf') expect(pixels.stdout.trim()).toBe('content') + const final = events.filter(e => e.type === 'assistant/message').at(-1)?.data.message.content + .filter(b => b.type === 'text').map(b => b.text).join(' ') + const diagnostic = JSON.stringify({ + format, mode, callOrder: calls.map(call => call.name), skills, commands, pixels: pixels.stdout, final, + }, null, 2) const authoring = calls.findIndex(call => call.name === 'skill' && /office-(docx|pptx|xlsx)/u.test(call.arguments)) if (pixels.stdout.includes('blank')) { if (authoring !== -1) { const rendered = calls.findIndex(call => (call.name === 'bash' || call.name === 'pwsh') && /\srender\b/u.test(call.arguments)) expect(authoring, 'content checks after a blank preview may need the format skill').toBeGreaterThan(rendered) } - const final = events.filter(e => e.type === 'assistant/message').at(-1)?.data.message.content - .filter(b => b.type === 'text').map(b => b.text).join(' ') - expect(final).toMatch(/visual (?:inspection|check)[^.!?]{0,100}(?:not completed|incomplete|could not be completed|unverified)/iu) + expect(final, diagnostic).toMatch( + /visual (?:inspection|check)[^.!?]{0,100}(?:not completed|incomplete|could not be completed|unverified)/iu, + ) } else { - expect(authoring, JSON.stringify(skills)).toBe(-1) + expect(authoring, diagnostic).toBe(-1) } expect(calls.some(e => e.name === 'read_image')).toBe(true) }, From b416e328471b8eff85af05fa9e3c224e9bc7c504 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Mon, 19 Oct 2026 04:11:05 +0800 Subject: [PATCH 04/10] test(office): scope render checks to the exported image --- packages/skill/skill-office/tests/render-tools.e2e.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/skill/skill-office/tests/render-tools.e2e.ts b/packages/skill/skill-office/tests/render-tools.e2e.ts index d795b86aec..126290b3c2 100644 --- a/packages/skill/skill-office/tests/render-tools.e2e.ts +++ b/packages/skill/skill-office/tests/render-tools.e2e.ts @@ -37,17 +37,22 @@ async function sessionEvents(cwd: string) { } it.skipIf(!process.env.DEEPSEEK_API_KEY || !runtime).each([{ format: 'docx', mode: 'native' }, { format: 'pdf', mode: 'native' }, { format: 'docx', mode: 'ptc' }])('loads the operation skill and renders $format through the shell in $mode mode', async ({ format, mode }) => { + let original: Buffer | undefined await runLoaderSmoke({ label: 'Office CLI rendering', tempDirPrefix: 'dsh-office-render-', ...launch, env: { ...launch.env, DSH_TOOLS_MODE: mode }, binArgs: ['--profile', 'headless', '--patch', 'office.patch.json', - `Export page 1 of input.${format} as PNG into preview and check that the exported image is readable. Report briefly in English.`], + // Source-content verification can require an authoring skill; this case covers image-only inspection. + `Export page 1 of input.${format} as PNG into preview. Check only that the exported image is readable; ` + + 'do not inspect the source document content or metadata, and keep the source file unchanged. Report briefly in English.'], async prepare(cwd) { await execa(python!, ['-c', "from docx import Document\nd=Document()\nd.add_heading('Quarterly revenue',0)\nd.add_paragraph('Q1: 12; Q2: 18; Total: 30')\nd.save('input.docx')"], { cwd }) if (format === 'pdf') await execa(python!, ['-c', "from PIL import Image, ImageDraw\nim=Image.new('RGB',(300,200),'white')\nImageDraw.Draw(im).rectangle((30,30,150,100),fill='black')\nim.save('input.pdf','PDF')"], { cwd }) await prepareOffice(cwd) + original = await readFile(join(cwd, `input.${format}`)) }, async inspect(cwd) { + expect(await readFile(join(cwd, `input.${format}`))).toEqual(original) const events = await sessionEvents(cwd) const calls = events.flatMap((event) => { if (event.type === 'tool/call') return [event.data] From 2f348776b45a8043092e092a5903c91ebc12f653 Mon Sep 17 00:00:00 2001 From: Yif <877193178@qq.com> Date: Mon, 19 Oct 2026 21:01:10 +0800 Subject: [PATCH 05/10] feat(ui-schedule): refresh automation empty-state illustration --- packages/client/ui-schedule/README.i18n.yaml | 4 +- packages/client/ui-schedule/README.md | 2 +- packages/client/ui-schedule/README.zh.md | 2 +- .../src/client/ScheduleEmptyIllustration.tsx | 33 ++++++++++++++++ .../src/client/TaskManagerPage.module.css | 9 +++++ .../src/client/TaskManagerPage.tsx | 3 +- .../tests/task-manager-page.client.spec.tsx | 24 ++++++++++++ packages/client/ui-theme/README.i18n.yaml | 4 +- packages/client/ui-theme/README.md | 2 + packages/client/ui-theme/README.zh.md | 2 + .../ui-theme/src/styles/design-platform.css | 12 ++++++ .../tests/illustration-colors.client.spec.ts | 39 +++++++++++++++++++ 12 files changed, 129 insertions(+), 7 deletions(-) create mode 100644 packages/client/ui-schedule/src/client/ScheduleEmptyIllustration.tsx create mode 100644 packages/client/ui-theme/tests/illustration-colors.client.spec.ts diff --git a/packages/client/ui-schedule/README.i18n.yaml b/packages/client/ui-schedule/README.i18n.yaml index 205e4c6b84..8a2a17dc02 100644 --- a/packages/client/ui-schedule/README.i18n.yaml +++ b/packages/client/ui-schedule/README.i18n.yaml @@ -15,8 +15,8 @@ en: d152484eb41ac6b4 zh: 405b30feaf48c087 /deepseek-ai-dsh-client-ui-schedule/use-this-package: - en: edba0e1573f3ce42 - zh: 77875b469d94cc62 + en: 7d44cd1640bebfad + zh: 1d6b6c28a3c27115 /deepseek-ai-dsh-client-ui-schedule/use-this-package/time-zone-data-source: en: 6bd6882dbbbefcb0 zh: 03aa15f1a65d13dc diff --git a/packages/client/ui-schedule/README.md b/packages/client/ui-schedule/README.md index 220af1e6c7..6f02ed050a 100644 --- a/packages/client/ui-schedule/README.md +++ b/packages/client/ui-schedule/README.md @@ -25,7 +25,7 @@ Manage active and inactive tasks from the Automation tasks page and the right Si ## Use this package -The shipped Web composition mounts `ui-schedule` with the Host Schedule service. The clock context that supplies the model's current time is preset-owned: `standard`, `cordis`, and `ptc` declare it, and `minimal` declares neither it nor the reminder tools. The Automation tasks sidebar entry opens a global page without selecting or activating a Session. The page header's New action starts a new Session, where the reminder is created by asking the model; the page itself has no creation form. Its task list supports text search and one All, Enabled, and Inactive status filter row; search matches the stored task name, the instruction, and the internal Session id, not a resolved Session title, and the empty state keeps the heading's New action beside the notice. Selecting an active or inactive task opens a second detail level with Rules and Records tabs. Every list row shows the task's stored title with its status, frequency, and next run and never the original Session name; the detail's name control shows the stored title. Rules contains the complete reminder, status, next target for active tasks, original recurrence rule, and task id. Daily rule labels show the saved local time and exact stored IANA zone, preserving nonzero seconds and milliseconds rather than substituting the browser's zone. The Linked session entry in the detail tab strip names the linked Session by its catalog title, falling back to the Session id when no title is available. When the strip cannot fit the tab list beside the labelled entry, the entry becomes an icon button with the same button style as More task actions, with the Session chat icon and a Linked session tooltip that also shows while the entry is disabled; the entry keeps focus when it switches layouts; the tab list scrolls without a visible scrollbar. It opens an available, unarchived Session after metadata loads; pending Session or Workspace metadata, an archived Session, missing Session membership, or a Workspace query error keeps it disabled with an explanation. The click rechecks availability; browsing does not unarchive the Session. Only this action requests Session activation. Deletion is available from the More task actions menu in the detail's top strip, separate from navigation. +The shipped Web composition mounts `ui-schedule` with the Host Schedule service. The clock context that supplies the model's current time is preset-owned: `standard`, `cordis`, and `ptc` declare it, and `minimal` declares neither it nor the reminder tools. The Automation tasks sidebar entry opens a global page without selecting or activating a Session. The page header's New action starts a new Session, where the reminder is created by asking the model; the page itself has no creation form. Its task list supports text search and one All, Enabled, and Inactive status filter row; search matches the stored task name, the instruction, and the internal Session id, not a resolved Session title, and the empty state shows a decorative document-and-clock illustration above its notice and New action, with colors that follow the light or dark theme. Selecting an active or inactive task opens a second detail level with Rules and Records tabs. Every list row shows the task's stored title with its status, frequency, and next run and never the original Session name; the detail's name control shows the stored title. Rules contains the complete reminder, status, next target for active tasks, original recurrence rule, and task id. Daily rule labels show the saved local time and exact stored IANA zone, preserving nonzero seconds and milliseconds rather than substituting the browser's zone. The Linked session entry in the detail tab strip names the linked Session by its catalog title, falling back to the Session id when no title is available. When the strip cannot fit the tab list beside the labelled entry, the entry becomes an icon button with the same button style as More task actions, with the Session chat icon and a Linked session tooltip that also shows while the entry is disabled; the entry keeps focus when it switches layouts; the tab list scrolls without a visible scrollbar. It opens an available, unarchived Session after metadata loads; pending Session or Workspace metadata, an archived Session, missing Session membership, or a Workspace query error keeps it disabled with an explanation. The click rechecks availability; browsing does not unarchive the Session. Only this action requests Session activation. Deletion is available from the More task actions menu in the detail's top strip, separate from navigation. The page header keeps the title at its natural width and moves New to another line when the two no longer fit together. The action label stays on one line. diff --git a/packages/client/ui-schedule/README.zh.md b/packages/client/ui-schedule/README.zh.md index 88575c4405..b7de6727ed 100644 --- a/packages/client/ui-schedule/README.zh.md +++ b/packages/client/ui-schedule/README.zh.md @@ -25,7 +25,7 @@ kind: "package-reference" ## 使用本包 -随发行版交付的 Web 组合把 `ui-schedule` 与 Host Schedule 服务一起挂载。为模型提供当前时间的时钟上下文归 preset 所有:`standard`、`cordis` 与 `ptc` 声明它,`minimal` 既不声明它,也不声明提醒工具。侧栏的自动化任务入口打开全局页面,不选择或激活会话。页面页头的“新建”操作会打开一个新会话,提醒由模型在该会话中创建;页面本身没有创建表单。列表支持文本搜索和一行“全部/已开启/未运行”状态筛选;搜索匹配任务已存储的名称、指令和内部会话 id,不匹配解析出的会话标题,空状态在提示旁保留页头的“新建”操作。选择活动或未运行任务后,进入含“规则”和“任务运行记录”页签的第二层详情。每条列表行显示任务已存储的标题及其状态、频率和下次运行,不显示原会话名称;详情中的名称输入框显示任务已存储的标题。“规则”显示完整提醒、状态、活动任务的下一目标时间、原始重复规则和任务 id。每天的规则标签显示已保存的本地时间与精确的存储 IANA 时区,保留非零秒与毫秒,而不替换为浏览器时区。详情页签条内的“关联会话”入口按会话标题显示,取不到标题时回退显示会话 id。页签条放不下页签列表和带文字的入口时,入口收为与“更多任务操作”同样式的图标按钮,使用会话对话图标,悬停提示“关联会话”,入口禁用时也会显示;切换形态时保留焦点;页签列表可滚动但不显示滚动条。元数据加载后,它可打开可用且未归档的会话;会话或 Workspace 元数据尚未就绪、会话已归档、会话不在列表中或 Workspace 查询失败时,入口保持禁用并显示原因。点击时会重新检查可用性;浏览不会取消会话归档。仅此操作请求激活会话。删除位于详情顶部条带的“更多任务操作”菜单。 +随发行版交付的 Web 组合把 `ui-schedule` 与 Host Schedule 服务一起挂载。为模型提供当前时间的时钟上下文归 preset 所有:`standard`、`cordis` 与 `ptc` 声明它,`minimal` 既不声明它,也不声明提醒工具。侧栏的自动化任务入口打开全局页面,不选择或激活会话。页面页头的“新建”操作会打开一个新会话,提醒由模型在该会话中创建;页面本身没有创建表单。列表支持文本搜索和一行“全部/已开启/未运行”状态筛选;搜索匹配任务已存储的名称、指令和内部会话 id,不匹配解析出的会话标题,空状态在提示和“新建”操作上方显示装饰性的文档与时钟插画,配色跟随浅色或深色主题。选择活动或未运行任务后,进入含“规则”和“任务运行记录”页签的第二层详情。每条列表行显示任务已存储的标题及其状态、频率和下次运行,不显示原会话名称;详情中的名称输入框显示任务已存储的标题。“规则”显示完整提醒、状态、活动任务的下一目标时间、原始重复规则和任务 id。每天的规则标签显示已保存的本地时间与精确的存储 IANA 时区,保留非零秒与毫秒,而不替换为浏览器时区。详情页签条内的“关联会话”入口按会话标题显示,取不到标题时回退显示会话 id。页签条放不下页签列表和带文字的入口时,入口收为与“更多任务操作”同样式的图标按钮,使用会话对话图标,悬停提示“关联会话”,入口禁用时也会显示;切换形态时保留焦点;页签列表可滚动但不显示滚动条。元数据加载后,它可打开可用且未归档的会话;会话或 Workspace 元数据尚未就绪、会话已归档、会话不在列表中或 Workspace 查询失败时,入口保持禁用并显示原因。点击时会重新检查可用性;浏览不会取消会话归档。仅此操作请求激活会话。删除位于详情顶部条带的“更多任务操作”菜单。 页头按标题的自然宽度排布;标题和“新建”无法并排容纳时,将按钮移到下一行。操作文案保持单行。 diff --git a/packages/client/ui-schedule/src/client/ScheduleEmptyIllustration.tsx b/packages/client/ui-schedule/src/client/ScheduleEmptyIllustration.tsx new file mode 100644 index 0000000000..9d8693453d --- /dev/null +++ b/packages/client/ui-schedule/src/client/ScheduleEmptyIllustration.tsx @@ -0,0 +1,33 @@ +/** Decorative document-and-clock artwork with theme-owned colors. */ + +/** + * Render the empty-state illustration without adding an accessible announcement. + * @param props - page-owned illustration sizing and spacing. + * @returns the same vector paths in either theme. + */ +export function ScheduleEmptyIllustration({ className }: { className: string | undefined }) { + return ( + + ) +} diff --git a/packages/client/ui-schedule/src/client/TaskManagerPage.module.css b/packages/client/ui-schedule/src/client/TaskManagerPage.module.css index ba3ee2a030..7fea4b416f 100644 --- a/packages/client/ui-schedule/src/client/TaskManagerPage.module.css +++ b/packages/client/ui-schedule/src/client/TaskManagerPage.module.css @@ -388,6 +388,15 @@ color: var(--dsw-alias-label-caption); } +.emptyIllustration { + display: block; + flex: none; + width: 110px; + max-width: 100%; + height: auto; + margin-bottom: 12px; +} + /* The mock's `.empty h3 + .btn`: the empty state's action keeps the mock's 16px above. */ .emptyAction { diff --git a/packages/client/ui-schedule/src/client/TaskManagerPage.tsx b/packages/client/ui-schedule/src/client/TaskManagerPage.tsx index 8f09c96137..7d18063ae8 100644 --- a/packages/client/ui-schedule/src/client/TaskManagerPage.tsx +++ b/packages/client/ui-schedule/src/client/TaskManagerPage.tsx @@ -13,6 +13,7 @@ import { TaskDetail, useTaskDetail, type TaskDetailInjected } from './TaskDetail import { useRelativeClock } from './relative-clock.ts' import { formatScheduleFrequency, nextRunParts, taskName, zoneLabel } from './schedule-format.ts' import type { FrequencyZone } from './schedule-format.ts' +import { ScheduleEmptyIllustration } from './ScheduleEmptyIllustration.tsx' import css from './TaskManagerPage.module.css' /** Injected catalog and task actions for the management page. */ @@ -151,7 +152,7 @@ export function TaskManagerPage(props: TaskManagerPageProps) {
{selected === undefined && 0} onRetry={onRetry} t={t} />} {status === 'ready' && rows.length === 0 &&
- +

{t(emptyTitle)}