fix(chat): close a reader-opened process group from its height cap

This commit is contained in:
imccyu committed 2026-10-19 12:23:46 +08:00
1 parent 147fee4186
commit 1260d96ef3
2 files changed
+50 -3

No files matched your search

@@ -176,10 +176,14 @@ export const ChatGroupSeat = memo(function ChatGroupSeat({ groupKey, useChatGrou
if (!open) initialize(closed === false ? 'bottom' : 'top')
setOpen(!open)
}, [closed, initialize, open, setOpen])
// With next-input timing the height cap applies only to a group the reader opened. A closing body keeps
// the cap it was last shown with, so the transition starts from what is on screen: a reader-opened group
// closes from its cap, and a flat latest Turn becoming historical closes from its flat height.
const shownCap = !grouped ? false : open ? true : undefined
const [closingCap, setClosingCap] = useState(shownCap ?? true)
if (shownCap !== undefined && shownCap !== closingCap) setClosingCap(shownCap)
if (members === undefined) return null
// The height cap applies only to a group the reader opened; a body that is closing keeps its flat
// height so the transition starts from what is on screen instead of snapping to the cap first.
const capped = grouped && (!deferCompletedTurns || open)
const capped = grouped && (!deferCompletedTurns || (shownCap ?? closingCap))
const classes = [css.body, !capped ? css.expandedBody : '',
capped && edges.canScrollUp ? css.fadeTop : '', capped && edges.canScrollDown ? css.fadeBottom : '']
return (
@@ -974,6 +974,49 @@ describe('ChatView', () => {
},
)
// jsdom applies no CSS, so the cap is observed through the class that carries `max-height`; the fold
// animation measures the body in the commit that closes it.
const uncapped = (body: HTMLElement): boolean => body.className.includes('expandedBody')
it.each(['compact', 'standard'] as const)(
'closes a reader-opened running %s group from its height cap with next-input timing', (mode) => {
const h = makeCompletionHarness(mode, 'next-input')
const view = render(<h.ChatView {...h.props} />)
const root = view.container.querySelector<HTMLElement>('[data-chat-group-key][data-chat-turn="2"]')!
const header = root.querySelector<HTMLButtonElement>('[data-process-activity]')!
const body = root.querySelector<HTMLElement>('[data-step-process-body]')!
fireEvent.click(header)
expect(body.hasAttribute('hidden')).toBe(false)
expect(uncapped(body)).toBe(false)
fireEvent.click(header)
expect(header.getAttribute('aria-expanded')).toBe('false')
expect(body.hasAttribute('hidden')).toBe(true)
expect(uncapped(body)).toBe(false)
},
)
it('closes flat Detailed rows at full height and a reader-opened historical group from its cap', () => {
const h = makeCompletionHarness('detailed', 'next-input')
const view = render(<h.ChatView {...h.props} />)
act(() => { h.complete() })
const root = view.container.querySelector<HTMLElement>('[data-chat-group-key][data-chat-turn="2"]')!
const header = root.querySelector<HTMLButtonElement>('[data-process-activity]')!
const body = root.querySelector<HTMLElement>('[data-step-process-body]')!
expect(body.hasAttribute('hidden')).toBe(false)
expect(uncapped(body)).toBe(true)
act(() => { h.sendNext() })
act(() => { h.startNext() })
expect(body.hasAttribute('hidden')).toBe(true)
expect(uncapped(body)).toBe(true)
fireEvent.click(view.container.querySelector<HTMLButtonElement>('[data-turn-process="2"]')!)
fireEvent.click(header)
expect(body.hasAttribute('hidden')).toBe(false)
expect(uncapped(body)).toBe(false)
fireEvent.click(header)
expect(body.hasAttribute('hidden')).toBe(true)
expect(uncapped(body)).toBe(false)
})
it.each([
{ initialHeight: 200, closed: true },
{ initialHeight: 600, closed: true },