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:
@@ -122,6 +122,40 @@ client 半(src/client/)
|
||||
当前会话改用加粗 accent 描边;**圆角 4px → 8px**。任务输出的锚定浮窗也换成了常驻浮动窗。
|
||||
以新文档为准。
|
||||
|
||||
## 后续修正(2026-09-27,用户反馈)
|
||||
|
||||
用户反馈两条,都在真机上定位并修掉:
|
||||
|
||||
### 1. 扫光只有「部分」→ 改成整条完整扫过
|
||||
|
||||
第一版用 `background-size: 45%` 的窄带 + `background-position: 0% → 100%`:亮带确实在移动,
|
||||
但它始终只覆盖小条的一部分(峰值从 22.5% 走到 77.5%,两端永远扫不到),看起来像中间游走的
|
||||
一小块光斑。现改为**与小条等宽的渐变带 + `transform: translateX(-100%) → translateX(100%)`**:
|
||||
光带从左边外侧进入、中途覆盖整条、再从右边外侧离开,行程就是用条宽本身表达的(任意条宽都成立),
|
||||
末 1/3 周期停在右侧外侧(两轮之间留白)。`tests/theme.spec.ts` 新增一条守护钉住这个形状。
|
||||
|
||||
真机证据(242px 宽的小条,每 90ms 采样一次 `::after` 的 `matrix` 平移量):
|
||||
`[-23, 32, 85, 131, 163, -139, -94, -46, 9, 64, 114, 152, 179, 188]` —— 光带在小条左侧
|
||||
(-139)到右侧(+188)之间被连续观察到,单调推进、跨度超过一个条宽;并截到一张行程中的帧。
|
||||
|
||||
### 2. 折叠按钮与展开按钮长得一样 → 两者分向
|
||||
|
||||
三处控件原来共用向下箭头(`IconChevronDownOutlineRegular`):卡片的逐节点「收进已完成聚合」按钮、
|
||||
聚合卡片的「展开」、以及控制条的折叠开关(后者更糟:无论将要折叠还是展开都画同一个清单图标)。
|
||||
现在按**动作方向**区分:
|
||||
|
||||
| 控件 | 折叠方向(chevron up) | 展开方向(chevron down) |
|
||||
|---|---|---|
|
||||
| 卡片小条的逐节点按钮 | ✅ `收进已完成聚合` | — |
|
||||
| 聚合卡片的小条 | — | ✅ `展开已完成的节点` |
|
||||
| 控制条折叠开关 | ✅(当前已展开时) | ✅(当前已折叠时) |
|
||||
|
||||
真机证据(比对三处 `<svg>` 内容):`expandVsCollapse: true`(两个方向确实不同)、
|
||||
`clusterMatchesExpand: true`(控制条在折叠态画的就是聚合卡的展开箭头)、
|
||||
`clusterFlips: true`(状态切换时图标跟着换)、`clusterCollapseMatchesCard: true`
|
||||
(控制条的折叠箭头与卡片逐节点按钮一致)。组件级守护见 `tests/tasks-page.spec.tsx`
|
||||
的「draws COLLAPSE and EXPAND with different glyphs」。
|
||||
|
||||
## 实施偏差记录(写给复审)
|
||||
|
||||
1. **`process-activity.ts` 是裁剪版而不是原样搬运**:本轮「阶段」= 徽标 + 颜色,逐阶段列表没有消费者,留着就是死代码。裁剪后 44 例 → 30 例(阶段相关用例随机制一起删除)。
|
||||
|
||||
@@ -30,7 +30,8 @@ import {
|
||||
} from 'react'
|
||||
import clsx from 'clsx'
|
||||
import {
|
||||
Button, IconChecklistOutlineRegular, IconChevronDownOutlineRegular, IconFullscreenOutlineRegular,
|
||||
Button, IconChevronDownOutlineRegular, IconChevronUpOutlineRegular,
|
||||
IconFullscreenOutlineRegular,
|
||||
IconLoadingOutlineRegular, IconTreeCornerRegular,
|
||||
} from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { TasksAgentNode, TasksFoldNode, TasksNode, TasksWorkflowNode } from './tasks-model.ts'
|
||||
@@ -164,6 +165,14 @@ export function ViewModeToggle(props: {
|
||||
}
|
||||
|
||||
/** The fold toggle (expand / re-collapse the settled aggregates). */
|
||||
/**
|
||||
* The cluster's fold switch. Its glyph names the action it will take, not the
|
||||
* state it is in: while the settled nodes are folded the button offers to
|
||||
* EXPAND (chevron down), and while they are open it offers to FOLD (chevron
|
||||
* up, the same collapse direction the cards' own bar buttons wear). The old
|
||||
* single checklist glyph read identically in both states, which is what the
|
||||
* reader reported as "折叠按钮和展开一样".
|
||||
*/
|
||||
export function FoldToggleButton(props: { folded: boolean; onToggleFold(): void }): ReactNode {
|
||||
const { folded, onToggleFold } = props
|
||||
return (
|
||||
@@ -171,7 +180,9 @@ export function FoldToggleButton(props: { folded: boolean; onToggleFold(): void
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={clsx(css.controlBtn, folded && css.controlBtnActive)}
|
||||
icon={<IconChecklistOutlineRegular size={13} />}
|
||||
icon={folded
|
||||
? <IconChevronDownOutlineRegular size={13} />
|
||||
: <IconChevronUpOutlineRegular size={13} />}
|
||||
aria-pressed={folded}
|
||||
aria-label={t(folded ? 'tasksFoldExpand' : 'tasksFoldCollapse')}
|
||||
title={t(folded ? 'tasksFoldExpand' : 'tasksFoldCollapse')}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
*/
|
||||
import type { ReactNode } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { IconChevronDownOutlineRegular, StateDot } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import { IconChevronUpOutlineRegular, StateDot } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { TasksNodeState } from './tasks-model.ts'
|
||||
import { nodeDotState, stateLabel } from './tasks-shared.tsx'
|
||||
import { t } from './locales.ts'
|
||||
@@ -150,7 +150,13 @@ export function CardBar(props: {
|
||||
props.onFold?.(event)
|
||||
}}
|
||||
>
|
||||
<IconChevronDownOutlineRegular size={12} />
|
||||
{/*
|
||||
The COLLAPSE direction (chevron up = gather into the group): the
|
||||
aggregate card at the other end of the same motion offers the
|
||||
EXPAND one (chevron down). Two opposite actions must not share
|
||||
a glyph — the first cut drew both as a down chevron.
|
||||
*/}
|
||||
<IconChevronUpOutlineRegular size={12} />
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -317,30 +317,38 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Executing: the bar's background SWEEPS left → right (the same motion the
|
||||
main conversation's shimmer uses, applied to a strip instead of text). The
|
||||
band is 45% of the bar and travels the full width; the last third of the
|
||||
cycle rests, so a fast glance still catches it at rest. */
|
||||
/* Executing: a light band SWEEPS the bar left → right (the same motion the
|
||||
main conversation's shimmer uses, applied to a strip instead of text).
|
||||
The band is as WIDE as the bar and travels a full width either side of it:
|
||||
it enters at the left edge, covers the whole bar mid-flight, and leaves past
|
||||
the right edge — a narrower band with `background-position` (the first cut)
|
||||
only ever lit a 45% slice, which read as a blob wandering in the middle
|
||||
instead of a sweep. `transform` is used rather than background-position
|
||||
because the travel is then literal (`translateX(-100%) → translateX(100%)`
|
||||
of the bar's own width), so it stays correct at every bar width. The last
|
||||
third of the cycle holds the band off the right edge: a clean pause between
|
||||
sweeps, and a fast glance still catches the bar at rest. */
|
||||
.cardBar[data-running='true']::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
background-image: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
color-mix(in srgb, var(--dsw-alias-state-business-primary) 14%, transparent) 50%,
|
||||
color-mix(in srgb, var(--dsw-alias-state-business-primary) 18%, transparent) 50%,
|
||||
transparent 100%
|
||||
);
|
||||
background-size: 45% 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 0% center;
|
||||
transform: translateX(-100%);
|
||||
animation: dsh-tasks-bar-sweep 1.5s cubic-bezier(0.33, 0, 0.67, 1) infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes dsh-tasks-bar-sweep {
|
||||
0% { background-position: 0% center; }
|
||||
66.6667%, 100% { background-position: 100% center; }
|
||||
0% { transform: translateX(-100%); }
|
||||
66.6667%, 100% { transform: translateX(100%); }
|
||||
}
|
||||
|
||||
/* Motion is decoration here: the state word and the activity text carry the
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user