feat(web): reflow the automation tasks header at narrow widths

The page header keeps the title at its natural width and moves the New action
onto another line once the two no longer fit, so a narrow content column never
splits the title across lines.

Register the new browser scenario in both web programs: the host aggregate owns
files that boot the host spine, and the client-registered app project excludes
them.

The scenario asserts the title stays on one line, the action separates when the
column narrows, and focus survives the change.
This commit is contained in:
Yif committed 2026-10-12 21:56:15 +08:00
1 parent ee558c82d7
commit 89c4652add
8 files changed
+117 -4

No files matched your search

@@ -0,0 +1,17 @@
# Automation tasks header
en 1680px: title and New single-line; no overlap or overflow; New retains focus
en 800px: title and New single-line; no overlap or overflow; New retains focus
en 520px: title and New single-line; no overlap or overflow; New retains focus
en 440px: title and New single-line; no overlap or overflow; New retains focus
en 390px: title and New single-line; no overlap or overflow; New retains focus
en 430px: title and New single-line; no overlap or overflow; New retains focus
en 1024px: title and New single-line; no overlap or overflow; New retains focus
en 509px with sidebar expanded: title single-line; New moves below the title
zh 1680px: title and New single-line; no overlap or overflow; New retains focus
zh 800px: title and New single-line; no overlap or overflow; New retains focus
zh 520px: title and New single-line; no overlap or overflow; New retains focus
zh 440px: title and New single-line; no overlap or overflow; New retains focus
zh 390px: title and New single-line; no overlap or overflow; New retains focus
zh 430px: title and New single-line; no overlap or overflow; New retains focus
zh 1024px: title and New single-line; no overlap or overflow; New retains focus
+87
View File
@@ -0,0 +1,87 @@
// UI-only geometry for the Automation tasks page; no model calls or task mutations.
import { fileURLToPath } from 'node:url'
import { chromium } from 'playwright'
import { expect, it } from 'vitest'
import { compareOrRefreshGolden, launchWebScaffold, watchConsole, webSnapshotMode } from './scaffold.ts'
it('keeps the automation title intact before moving New to another line', async () => {
const scaffold = await launchWebScaffold()
try {
const browser = await chromium.launch()
try {
const facts: string[] = []
for (const language of ['en', 'zh'] as const) {
const context = await browser.newContext({
viewport: { width: 1680, height: 1000 },
locale: language === 'en' ? 'en-US' : 'zh-CN',
reducedMotion: 'reduce',
})
try {
const page = await context.newPage()
const consoleWatch = watchConsole(page)
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
const title = language === 'en' ? 'Automation tasks' : '自动化任务'
await page.getByRole('button', { name: title, exact: true }).click()
const panel = page.getByTestId('task-manager-page')
const heading = panel.getByRole('heading', { name: title, exact: true })
await heading.waitFor()
const header = heading.locator('..')
const create = header.getByRole('button', { name: language === 'en' ? 'New' : '新建', exact: true })
await page.evaluate(() => document.fonts.ready)
await create.focus()
for (const width of [1680, 800, 520, 440, 390, 430, 1024]) {
await page.setViewportSize({ width, height: 1000 })
await expect.poll(() => header.evaluate((element) => {
const title = element.querySelector('h1')!
const button = element.querySelector('button')!
const titleRect = title.getBoundingClientRect()
const buttonRect = button.getBoundingClientRect()
const headerRect = element.getBoundingClientRect()
const range = document.createRange()
range.selectNodeContents(title)
const titleLines = Array.from(range.getClientRects())
return {
titleSingleLine: titleLines.length === 1,
buttonSingleLine: button.scrollHeight <= button.clientHeight,
separated: buttonRect.left >= titleRect.right - 1 || buttonRect.top >= titleRect.bottom - 1,
contained: titleRect.left >= headerRect.left - 1 && titleRect.right <= headerRect.right + 1
&& buttonRect.left >= headerRect.left - 1 && buttonRect.right <= headerRect.right + 1,
focused: document.activeElement === button,
}
})).toEqual({
titleSingleLine: true, buttonSingleLine: true, separated: true, contained: true, focused: true,
})
facts.push(`${language} ${width}px: title and New single-line; no overlap or overflow; New retains focus`)
}
if (language === 'en') {
await page.setViewportSize({ width: 509, height: 1000 })
await page.getByRole('button', { name: 'Open sidebar', exact: true }).first().click()
await expect.poll(() => header.evaluate((element) => {
const title = element.querySelector('h1')!
const button = element.querySelector('button')!
const range = document.createRange()
range.selectNodeContents(title)
return {
titleSingleLine: range.getClientRects().length === 1,
actionBelow: button.getBoundingClientRect().top >= title.getBoundingClientRect().bottom,
}
})).toEqual({ titleSingleLine: true, actionBelow: true })
facts.push('en 509px with sidebar expanded: title single-line; New moves below the title')
}
expect(consoleWatch.pageErrors).toEqual([])
} finally {
await context.close()
}
}
await compareOrRefreshGolden(
fileURLToPath(new URL('./expected/schedule-header.expected.md', import.meta.url)),
`# Automation tasks header\n\n${facts.join('\n')}`,
webSnapshotMode(),
)
} finally {
await browser.close()
}
} finally {
await scaffold.close()
}
})
+1
View File
@@ -78,6 +78,7 @@
"tests/plugin-config.e2e.ts",
"tests/input-plugin-extensions.e2e.ts",
"tests/plugin-manager.e2e.ts",
"tests/schedule-header.e2e.ts",
"tests/worktree-opt-in.e2e.ts",
"tests/session-inspector.e2e.ts",
"tests/shared-inspector.e2e.ts",
+2 -2
View File
@@ -15,8 +15,8 @@
en: d152484eb41ac6b4
zh: 405b30feaf48c087
/deepseek-ai-dsh-client-ui-schedule/use-this-package:
en: c668e3d223102e47
zh: efedf3996679fb17
en: edba0e1573f3ce42
zh: 77875b469d94cc62
/deepseek-ai-dsh-client-ui-schedule/use-this-package/time-zone-data-source:
en: 6bd6882dbbbefcb0
zh: 03aa15f1a65d13dc
+2
View File
@@ -27,6 +27,8 @@ Manage active and inactive tasks from the Automation tasks page and the right Si
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 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.
Selecting a task from the list opens Rules; refreshing that task preserves the selected tab. Rules edits an active task's name, instruction, and run time; the edits stay in a local draft until saved, and inactive tasks remain read-only. Unmodified Left/Right arrows cycle the tabs, Home/End select the first/last tab, and keyboard focus follows the selection. The tabs do not consume these keys with Alt, Ctrl, Meta, or Shift. The linked-Session control in the detail tab strip stays available on the Rules tab, and confirmed deletion is available from either view.
Rules shows the rule in a Run time card whose rows edit a local draft: Repeat is a menu of Weekly, Monday to Friday, Every day, Every N hours, Every N minutes, Every N seconds, Once, and Custom (cron), a weekly rule adds a Weekday row, a fixed-rate choice edits only the interval quantity in the unit it names, a one-shot rule edits separate Date and Time rows, and Custom (cron) edits one Cron expression row holding a five-field expression, interpreted in the selected time zone. The card parses that expression in the browser and, under the row, shows a localized sentence describing it; an expression the Host would reject shows the localized validation message in place of that sentence and blocks Save. A stored weekly rule whose weekday set is exactly Monday through Friday selects the Monday to Friday choice. The Date row is a read-only trigger showing the staged date as `YYYY/MM/DD` and opening a month calendar with localized weekday headings, month navigation, and a marked today; the draft and the submitted text keep the ISO form; the Time row is a read-only trigger showing one 24-hour clock and opening three scrolling columns of hours, minutes, and seconds with the staged value marked and scrolled into view. A calendar or clock pick stages the draft under the same staged save as every other row, arrow keys walk the open column or grid, Enter stages the marked value, Escape closes the panel and returns focus to its row, and a disabled row cannot open one. Clock rows show whole seconds, an untouched row keeps a stored millisecond value for its submit, and editing a time submits whole-second precision. Time zone is a searchable, height-bounded menu. An unlabeled top block keeps the five most recently selected exact zones in browser-local storage; first use seeds the actual system zone and UTC rather than inferring a location from UI language. The system zone is marked, and the remaining inventory is ordered by current UTC offset and canonical IANA id. Each visible label puts `UTC±HH:MM` before the localized zone name; canonical ids stay internal but remain searchable. Zones that ICU renders with the same offset and localized name share one visible row; every grouped id remains searchable, and an already stored id remains selected instead of being rewritten. The inventory comes from `Intl.supportedValuesOf('timeZone')`, and names come from `Intl.DateTimeFormat(..., { timeZoneName: 'longGeneric' })`: both are browser ICU/CLDR data, not a scraped or product-maintained translation table. There is therefore no crawler or generated zone-name resource to refresh. A third-party locale supplies its ordinary `time.locale` value and automatically receives the corresponding ICU names; only the selector's surrounding UI copy needs translation. When the runtime cannot enumerate zones, a short identifier fallback is offered, and a stored zone outside the offered set is kept. Daily, Weekly, and Cron keep their stored rule and IANA zone, and each frequency line names the zone only when it differs from the host's system zone. A one-shot After/At seeds the date and time from the record's stored zone, or this device's zone when the record stores none, so the seeded clock names the stored instant; changing that zone keeps the entered clock values and changes the instant. A kind the card seeds on selection uses the committed occurrence's clock and the choice's zone, which is the record's stored zone or this device's zone, a seeded weekly rule starts from that occurrence's weekday in that zone, and a seeded cron rule states that occurrence's clock as a daily expression. Every accepts at least 60 whole seconds, independent of time zones. One Save submits the complete expected record with the staged change and preserves the id, original Session, latest receipt, and saved history; [Schedule](../../schedule/schedule/README.md#use-this-package) defines future-target selection, reanchoring, and unchanged-rule no-ops.
+2
View File
@@ -27,6 +27,8 @@ kind: "package-reference"
随发行版交付的 Web 组合把 `ui-schedule` 与 Host Schedule 服务一起挂载。为模型提供当前时间的时钟上下文归 preset 所有:`standard`、`cordis` 与 `ptc` 声明它,`minimal` 既不声明它,也不声明提醒工具。侧栏的自动化任务入口打开全局页面,不选择或激活会话。页面页头的“新建”操作会打开一个新会话,提醒由模型在该会话中创建;页面本身没有创建表单。列表支持文本搜索和一行“全部/已开启/未运行”状态筛选;搜索匹配任务已存储的名称、指令和内部会话 id,不匹配解析出的会话标题,空状态在提示旁保留页头的“新建”操作。选择活动或未运行任务后,进入含“规则”和“任务运行记录”页签的第二层详情。每条列表行显示任务已存储的标题及其状态、频率和下次运行,不显示原会话名称;详情中的名称输入框显示任务已存储的标题。“规则”显示完整提醒、状态、活动任务的下一目标时间、原始重复规则和任务 id。每天的规则标签显示已保存的本地时间与精确的存储 IANA 时区,保留非零秒与毫秒,而不替换为浏览器时区。详情页签条内的“关联会话”入口按会话标题显示,取不到标题时回退显示会话 id。页签条放不下页签列表和带文字的入口时,入口收为与“更多任务操作”同样式的图标按钮,使用会话对话图标,悬停提示“关联会话”,入口禁用时也会显示;切换形态时保留焦点;页签列表可滚动但不显示滚动条。元数据加载后,它可打开可用且未归档的会话;会话或 Workspace 元数据尚未就绪、会话已归档、会话不在列表中或 Workspace 查询失败时,入口保持禁用并显示原因。点击时会重新检查可用性;浏览不会取消会话归档。仅此操作请求激活会话。删除位于详情顶部条带的“更多任务操作”菜单。
页头按标题的自然宽度排布;标题和“新建”无法并排容纳时,将按钮移到下一行。操作文案保持单行。
从列表选择任务时默认打开“规则”;刷新同一任务会保留当前页签。活动任务的“规则”修改名称、指令和运行时间;这些修改保留在本地草稿中直到保存,未运行任务保持只读。不带修饰键的左右方向键循环切换页签,Home/End 选择第一个/最后一个页签,键盘焦点随选择移动。页签不会拦截这些键与 Alt、Ctrl、Meta 或 Shift 的组合。共用的会话入口位于详情页签条内,在“规则”页签可见;确认删除操作在两种页签下都可用。
“规则”在“运行时间”卡片中以行编辑本地草稿:“重复”是含每周、周一至周五、每天、每 N 小时、每 N 分钟、每 N 秒、仅一次和自定义(cron)的菜单,每周规则增加“星期”行,固定速率选项只修改它所属单位的间隔数量,一次性规则用独立的“日期”和“时间”两行编辑目标,自定义(cron)用一行“Cron 表达式”编辑按所选时区解释的 5 字段表达式。卡片在浏览器中解析该表达式,并在该行下方显示描述它的本地化语句;Host 会拒绝的表达式改为显示本地化校验信息,并在解析通过前阻止保存。已存储的每周规则若星期集合恰好为周一至周五,则选中“周一至周五”选项。“日期”行是只读触发器,以 `YYYY/MM/DD` 显示暂存的日期并展开带本地化星期表头、月份切换和今天标记的月历;草稿与提交文本保持 ISO 形态;“时间”行是只读触发器,显示统一的 24 小时时钟,并展开时、分、秒三列可滚动列表,当前值高亮并在打开时滚动到可见。在月历或时钟上点选即按与其他行相同的分阶段保存写入草稿;方向键在打开的列或网格内移动,Enter 选定当前值,Escape 关闭面板并把焦点还给所在行,被禁用的行无法打开面板。时钟行显示到秒,未改动的行在提交时保留已存储的毫秒值,编辑时间时按整秒精度提交。“时区”使用高度受限的可搜索菜单。顶部无标题区域保留浏览器本地最近选过的 5 个准确时区;首次使用按真实系统时区和 UTC 初始化,而不根据界面语言猜测所在地。系统时区带有标记,其余清单按当前 UTC 偏移、再按规范 IANA 标识排序。可见文案把 `UTC±HH:MM` 放在本地化时区名称之前;IANA 标识仅在内部使用,但可用于搜索。ICU 显示成相同偏移和本地化名称的多个时区只占一行;所有被合并的标识仍可用于搜索,任务已保存的标识也会继续作为选中值,不会被改写。时区清单来自 `Intl.supportedValuesOf('timeZone')`,名称来自 `Intl.DateTimeFormat(..., { timeZoneName: 'longGeneric' })`:两者都是浏览器内置的 ICU/CLDR 数据,不是抓取或产品自维护的翻译表。因此没有需要刷新的爬虫或生成时区资源;第三方语言包只需提供常规的 `time.locale` 值,即可自动获得对应的 ICU 名称,仅需翻译选择器周边的 UI 文案。运行时无法枚举时改用简短的标识候选列表,已存储但不在其中的时区会保留。每天、每周和 cron 保留各自已存储的规则与时区,其频率行仅在时区与宿主系统时区不同时标出时区。一次性 After/At 以记录已存储的时区初始化日期和时间;记录未保存时区时取本设备时区,初始钟表值在该时区下仍表示同一瞬时;更改时区会保留输入的钟表值并改变时点。因选择种类而重新初始化的种类使用已提交发生时点的钟表值和该选项的时区,该时区取记录已存储的时区或本设备时区:每周的星期取自该发生时点在该时区下的星期,cron 则把该发生时点的钟表值写成每天执行的表达式。固定间隔至少为 60 整秒,不受时区影响。一次保存以分阶段的变更提交完整预期记录,并保留 id、原会话、最近一次回执及已保存历史;未来目标选择、重新确定起算时间和未变更规则的无写入语义见 [Schedule](../../schedule/schedule/README.zh.md#use-this-package)。
@@ -45,9 +45,10 @@
padding: 0 clamp(24px, 4vw, 48px) 48px;
}
/* The page name and the creation actions on one line. */
/* Page title and creation actions. */
.pageHeading {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 16px;
@@ -68,8 +69,9 @@
their titles share one size. The theme carries no size token for this step,
which is why every first-level page states the same literal. */
.pageHeading h1 {
flex: 1;
flex: 0 1 auto;
min-width: 0;
max-width: 100%;
margin: 0;
font-size: 20px;
line-height: 28px;
@@ -82,6 +84,7 @@
flex: none;
align-items: center;
gap: 16px;
white-space: nowrap;
}
/* The Plugins toolbar's Add-plugin step: a 32px capsule with the 13px label. */
+1
View File
@@ -71,6 +71,7 @@
"apps/web/tests/plugin-config.e2e.ts",
"apps/web/tests/input-plugin-extensions.e2e.ts",
"apps/web/tests/plugin-manager.e2e.ts",
"apps/web/tests/schedule-header.e2e.ts",
"apps/web/tests/worktree-opt-in.e2e.ts",
"apps/web/tests/session-inspector.e2e.ts",
"apps/web/tests/shared-inspector.e2e.ts",