fix(web): keep ineligible message forks disabled

This commit is contained in:
kingwl
2026-08-03 16:19:49 +08:00
parent c43eaaec82
commit c5050f018e
41 changed files with 220 additions and 48 deletions

View File

@@ -325,14 +325,19 @@ describe('ChatView', () => {
expect(view.getAllByText('interrupt now')).toHaveLength(1)
expect(view.container.querySelector('[data-pending-steering]')).toBeNull()
expect(view.getAllByRole('button', { name: '复制' })).toHaveLength(2)
expect(view.queryByRole('button', { name: '在新对话中分支' })).toBeNull()
const durableBubble = view.getByText('interrupt now').closest('[class*="userRow"]') as HTMLElement
const unavailable = within(durableBubble).getByRole('button', { name: '在新对话中分支' })
expect(unavailable.getAttribute('aria-disabled')).toBe('true')
fireEvent.click(unavailable)
expect(h.forkAt).not.toHaveBeenCalled()
act(() => {
h.set({ running: false, turnEnds: new Map([[1, 3]]) })
})
const branchButtons = view.getAllByRole('button', { name: '在新对话中分支' })
expect(branchButtons).toHaveLength(1)
fireEvent.click(branchButtons[0]!)
expect(branchButtons).toHaveLength(2)
expect(branchButtons.map(button => button.getAttribute('aria-disabled'))).toEqual(['true', null])
fireEvent.click(branchButtons[1]!)
expect(h.forkAt).toHaveBeenCalledWith(2)
})
@@ -435,24 +440,28 @@ describe('ChatView', () => {
turnEnds: new Map([[1, 4], [2, 6]]),
})
const view = render(<h.ChatView {...h.props} />)
// User rows keep copy/clock, while only the two completed assistant tails may branch.
// Every message footer keeps branch visible; only completed assistant tails enable it.
expect(view.getAllByRole('button', { name: '复制' })).toHaveLength(4)
expect(view.getAllByRole('button', { name: '在新对话中分支' })).toHaveLength(2)
const branchButtons = view.getAllByRole('button', { name: '在新对话中分支' })
expect(branchButtons).toHaveLength(4)
expect(branchButtons.map(button => button.getAttribute('aria-disabled'))).toEqual(['true', null, 'true', null])
})
it('forks only from a finalized assistant at the completed transcript tail', () => {
it('enables fork only on the finalized assistant at the completed transcript tail', () => {
const h = makeHarness({
nodes: [user(1, 'question'), assistant(2, 'answer')],
turnEnds: new Map([[1, 3]]),
})
const view = render(<h.ChatView {...h.props} />)
const buttons = view.getAllByRole('button', { name: '在新对话中分支' })
expect(buttons).toHaveLength(1)
expect(buttons).toHaveLength(2)
expect(buttons.map(button => button.getAttribute('aria-disabled'))).toEqual(['true', null])
fireEvent.click(buttons[0]!)
fireEvent.click(buttons[1]!)
expect(h.forkAt.mock.calls).toEqual([[2]])
})
it('keeps copy chrome but hides branch when tool and interrupted Think follow the response', () => {
it('keeps branch visible but unavailable when tool and interrupted Think follow the response', () => {
const interruptedThink: AssistantMessageNode = {
kind: 'assistant', seq: 4.1, time: 4_100, turn: 1, step: 2,
blocks: [{ kind: 'reasoning', text: 'bad path' }], interrupted: true,
@@ -463,7 +472,12 @@ describe('ChatView', () => {
})
const view = render(<h.ChatView {...h.props} />)
expect(view.getAllByRole('button', { name: '复制' })).toHaveLength(2)
expect(view.queryByRole('button', { name: '在新对话中分支' })).toBeNull()
const buttons = view.getAllByRole('button', { name: '在新对话中分支' })
expect(buttons).toHaveLength(2)
expect(buttons.every(button => button.getAttribute('aria-disabled') === 'true')).toBe(true)
fireEvent.click(buttons[0]!)
fireEvent.click(buttons[1]!)
expect(h.forkAt).not.toHaveBeenCalled()
})
it('renders assistant Markdown across history, streaming, final, and interrupted states while user text stays literal', () => {