Skip to content

record:path classifies a won stage differently on desktop and mobile, so one stage paints (and now announces) two ways by viewport #5998

Description

@yinlianghui

Found while implementing #5956 + #5957 (PR on branch claude/issue-5956-record-path-a11y-residue). Outside both cards' fences — both are about what a stage says; this is about which stages get classified as the goal in the first place. Filing rather than folding in.

The asymmetry

packages/plugin-detail/src/renderers/record-path.tsx renders two rows from the same stages[], and they do not agree on which stage is a won terminus.

Desktop restricts it to the last forward stage:

constisWonTerminus=forwardKinds[idx]==='won'&&idx===last;// ...terminal: isWonTerminus ? 'won' : undefined,

Mobile passes the classification through for every won-classified stage:

constkind=stageKinds[idx];// ...terminal: kind,

renderStage hands that same terminal to both railClass and stageAriaLabel, so the two rows diverge in the paint and, after #5957, in the accessible name too.

Why it is reachable

classify() reaches won through the WON_TOKENS heuristic (won|success|成交|赢|完成) as well as an explicit terminal: 'won', and 完成 is an ordinary mid-path word. A path like 草稿 → 完成 → 已归档 classifies index 1 as won while last is 2, so:

  • desktop: index 1 gets terminal: undefinedbg-muted, announces {{stage}}, upcoming
  • mobile: index 1 gets terminal: 'won'bg-emerald-500/30, announces {{stage}}, goal stage, not reached

One control, one record, two answers, chosen by viewport width.

Not a regression from #5957

The paint half predates it — railClass has always consumed the same asymmetric terminal. #5957's fix deliberately derives the name from the very value railClass receives, precisely so the name tracks the paint on each row rather than introducing a second classification. That makes the name correct per row and leaves this underlying disagreement exactly as it was, now visible in two channels instead of one.

What needs deciding

Which row is right. Two defensible readings, and picking one is a product-ish call about what the heuristic is for, which is why this is not folded in:

  1. Desktop is right — "the goal" is the terminus of the forward path, so only the last forward stage can be it, and a mid-path 完成 is a false positive of the heuristic. Fix: mobile adopts the idx === last restriction.
  2. Mobile is right — any stage the heuristic calls won is a successful outcome worth marking. Fix: desktop drops the idx === last restriction.

Reading 1 also narrows the heuristic's blast radius, which is the more conservative direction. Either way the two rows should share one classification, computed once, rather than each deriving its own.

Related: #5916 (the accessible-name mechanism), #5956 and #5957 (the two residues, being implemented together).


Generated by Claude Code

Metadata

Metadata

Assignees

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:dispatched

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions