fix(web): drop the branch action from user and steering bubbles

The branch control on user and consumed-steering bubbles could enable
only when a completed turn ended with no node after the message at all,
so readers saw a permanently unavailable control whose tooltip promised
a state it could not reach. An enabled one would mislead anyway: a fork
at a message seq cuts at the containing turn/end and takes the answer
along, the opposite of the branch-to-re-ask reading a control on one's
own bubble suggests.

MessageItem loses its fork props, PendingSteeringBubble loses the
showBranch special case, and messageBranchSeqs narrows to
assistantBranchSeqs: only a completed turn's transcript tail that is
the turn's own content-text assistant may fork. A steered turn keeps
its fork point under the settled answer, because fork is a log-prefix
cut and the steer is model-visible history the child inherits.

Web aria goldens drop the user-bubble disabled-branch row and its
hidden explanation text; the nested-subagent golden also loses the one
enabled user-tail fork handle, a loss the decision note accepts.
This commit is contained in:
creatixchu
2026-08-06 17:46:18 +08:00
parent 6902b51fea
commit 84a6bae1c7
49 changed files with 154 additions and 199 deletions

View File

@@ -107,17 +107,17 @@ describe('web e2e: message IconActions and clocks on settled history', () => {
await expect.poll(() => page.getByText('DONE', { exact: true }).count(), { timeout: 15_000 }).toBe(1)
// Focus-reveal the footers (hover:hover keeps them opacity-hidden until
// hover/focus-within). Every durable message footer keeps branch visible,
// but only the final assistant at a completed transcript tail enables it.
// hover/focus-within). Branch renders only under assistant answers — user
// bubbles carry none — and only a completed transcript tail enables it.
const copyButtons = page.getByRole('button', { name: 'Copy' })
await expect.poll(() => copyButtons.count(), { timeout: 10_000 }).toBeGreaterThanOrEqual(4)
await copyButtons.first().focus()
const branchButtons = page.getByRole('button', { name: 'Branch into a new conversation' })
await expect.poll(() => branchButtons.count(), { timeout: 5_000 }).toBe(4)
await expect.poll(() => branchButtons.count(), { timeout: 5_000 }).toBe(2)
await expect.poll(
() => branchButtons.evaluateAll(buttons => buttons.map(button => button.getAttribute('aria-disabled'))),
{ timeout: 5_000 },
).toEqual(['true', 'true', 'true', null])
).toEqual(['true', null])
await branchButtons.first().focus()
await expect.poll(() => page.getByRole('tooltip').textContent(), { timeout: 5_000 })
.toBe('Available only on the last message of a completed turn')