fix(tasks): 扫光改为整条完整扫过 + 折叠/展开按钮分向

用户反馈两条:

1) 扫光只有「部分」:第一版是 45% 宽的窄带 + background-position 位移,
   亮带始终只覆盖小条的一部分(峰值 22.5%→77.5%,两端扫不到),看起来像
   中间游走的一块光斑。改为与小条等宽的渐变带 + transform
   translateX(-100%) → translateX(100%):光带从左外侧进入、中途覆盖整条、
   再向右外侧离开,行程用条宽本身表达(任意条宽成立),末 1/3 周期停在
   右侧外侧留白。theme.spec 新增守护钉住该形状(宽度 100%、transform 行程、
   不再有 background-position/size 位移)。

2) 折叠按钮与展开按钮长得一样:三处控件原本共用向下箭头(控制条的折叠开关
   更是无论折叠还是展开都画同一个清单图标)。现按动作方向区分——
   卡片小条的逐节点按钮 = chevron up(收进聚合),聚合卡片 = chevron down
   (展开),控制条开关随状态在两者之间切换(并复用同一个折叠图标)。
   组件守护见 tasks-page.spec 的「draws COLLAPSE and EXPAND with different
   glyphs」。

真机证据(3080,真实运行中的后台任务):242px 小条上每 90ms 采样 ::after 的
matrix 平移量得到 [-23,32,85,131,163,-139,-94,-46,9,64,114,152,179,188]
(左侧 -139 → 右侧 +188,跨度超过一个条宽,单调推进);三处 svg 比对
expandVsCollapse / clusterMatchesExpand / clusterFlips / clusterCollapseMatchesCard
全部为 true。

验证:typecheck / lint ✅;pnpm test 1256 passed / 9 skipped(新增 2 例)。

(cherry picked from commit 0bb8228e6a7acb4302d9a8a9881d2df04a285152)
This commit is contained in:
Menghuan1918
2026-09-27 22:18:57 +08:00
parent d38fefe55d
commit 7cce50c493
6 changed files with 123 additions and 15 deletions
+29
View File
@@ -718,6 +718,35 @@ describe('Tasks page: the shared task window', () => {
unmount()
})
it('draws COLLAPSE and EXPAND with different glyphs (cluster, card, aggregate)', async () => {
const store = makeStore(snapshotWithChildren(2))
const { container, unmount } = renderRoot(
createElement(SubagentView, { sessionId: 'root', active: true, ctx: makeCtx(store) }),
)
await flushJobs()
/** The icon markup of one control (the glyph is what the reader compares). */
const glyph = (selector: string): string =>
container.querySelector(selector)?.querySelector('svg')?.innerHTML ?? ''
// Folded by default: the cluster's switch offers EXPAND, and it wears the
// same expand glyph the aggregate card's bar does.
const expandGlyph = glyph('button[aria-label="展开已完成的节点"]')
expect(expandGlyph).not.toBe('')
expect(glyph('[data-graph-node^="fold:"] [data-card-bar]')).toBe(expandGlyph)
// Unfold: the cards' own control COLLAPSES one node, and it must not share
// the expand glyph (the reader's report was that both looked identical).
await act(async () => {
(container.querySelector('button[aria-label="展开已完成的节点"]') as HTMLButtonElement).click()
})
const collapseGlyph = glyph('[data-graph-node] button[aria-label="收进已完成聚合"]')
expect(collapseGlyph).not.toBe('')
expect(collapseGlyph).not.toBe(expandGlyph)
// The cluster switch flips to the collapse direction with the same glyph.
expect(glyph('button[aria-label="折叠已完成的节点"]')).toBe(collapseGlyph)
unmount()
})
it('offers no fold chevron on a running card or on the current session', async () => {
livePayload = { 'child-0': { running: true, summary: { counts: [], runningDetail: '' } } }
const store = makeStore(snapshotWithChildren(2))
+20
View File
@@ -161,6 +161,26 @@ describe('skin contract: the plugin owns no color of its own', () => {
}
})
it('the running bar sweeps the FULL width (a band either side, not a partial blob)', () => {
// Reader report: the first cut moved a 45%-wide band with
// `background-position`, so the bar was only ever partially lit. The
// corrected rule is a bar-wide band whose TRANSFORM travels one full width
// either side of the bar — entered at the left edge, covering the bar
// mid-flight, gone past the right edge.
const styles = readFileSync(resolve(ROOT, 'src/client/tasks-graph.module.css'), 'utf8')
const block = /\.cardBar\[data-running='true'\]::after \{([\s\S]*?)\n\}/.exec(styles)?.[1]
if (block === undefined) throw new Error('the running bar sweep rule must exist')
expect(block).toContain('width: 100%')
expect(block).toContain('transform: translateX(-100%)')
// No residual background-position travel (the partial-sweep mechanism).
expect(block).not.toContain('background-position')
expect(block).not.toContain('background-size')
const frames = /@keyframes dsh-tasks-bar-sweep \{([\s\S]*?)\n\}/.exec(styles)?.[1]
if (frames === undefined) throw new Error('the sweep keyframes must exist')
expect(frames).toContain('transform: translateX(-100%)')
expect(frames).toContain('transform: translateX(100%)')
})
it('the empty-pane welcome capsule follows the host guide recipe', () => {
// The card is the same surface as DSH's own guide capsule ("pick what
// opens here"), so this pins the shared geometry and the token-only