From 938d17f5dab729c45ebeb580fa36129a8dcc2056 Mon Sep 17 00:00:00 2001 From: OpenAI Codex <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 22:36:00 +0000 Subject: [PATCH 1/3] fix(platform): retain comment edits across log window threshold --- .../task-log-window.browser.test.tsx | 20 +++++++++++++++++++ .../tasks/hooks/use-task-log-window.tsx | 11 +++++----- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/services/platform/app/features/tasks/components/task-log-window.browser.test.tsx b/services/platform/app/features/tasks/components/task-log-window.browser.test.tsx index f8aba0d423..d560b5883e 100644 --- a/services/platform/app/features/tasks/components/task-log-window.browser.test.tsx +++ b/services/platform/app/features/tasks/components/task-log-window.browser.test.tsx @@ -326,6 +326,26 @@ describe('task modal log window', () => { expect(list.querySelectorAll('[data-index]').length).toBeLessThan(40); }); + it('keeps an active edit mounted when the discussion crosses the window threshold', async () => { + state.comments = state.comments.slice(0, 90); + const view = render(); + const comment = await screen.findByText('Comment 0 opening line.'); + const row = comment.closest('li'); + if (row === null) throw new Error('Missing comment row'); + await userEvent.click( + within(row).getByRole('button', { name: 'actions.edit' }), + ); + const field = within(row).getByRole('textbox'); + await userEvent.fill(field, 'Draft retained across windowing'); + state.comments = Array.from({ length: 120 }, (_, index) => ({ + ...state.comments[index % state.comments.length], + messageId: `comment-${index}`, + })); + view.rerender(); + await waitFor(() => expect(field).toBeInTheDocument()); + expect(field).toHaveValue('Draft retained across windowing'); + }); + it('keeps an edit draft mounted after scrolling into older pages and moving focus', async () => { render(); const scroller = screen.getByTestId('scrollport'); diff --git a/services/platform/app/features/tasks/hooks/use-task-log-window.tsx b/services/platform/app/features/tasks/hooks/use-task-log-window.tsx index cd1f3a5995..ec4ef9c9d0 100644 --- a/services/platform/app/features/tasks/hooks/use-task-log-window.tsx +++ b/services/platform/app/features/tasks/hooks/use-task-log-window.tsx @@ -20,7 +20,6 @@ const TaskLogViewportContext = type RowKey = string | number; type SetRowActive = (rowKey: RowKey, source: string, active: boolean) => void; -const NOOP_SET_ROW_ACTIVE: SetRowActive = () => undefined; const TaskLogRowActivityContext = createContext< ((source: string, active: boolean) => void) | null >(null); @@ -170,12 +169,12 @@ export function useTaskLogWindow({ return () => observer.disconnect(); }, [getScrollElement, window.scrollElement, listElement]); - // Small discussions render every row and do not need an activity pin. A - // no-op keeps editor focus from causing a second list reconciliation in that - // common case; large windows still pin dirty or portaled rows as they leave - // the viewport. + // Register activity in every mode. A row can become active while the list is + // below the window threshold and remain active when new rows turn + // virtualization on; keeping the registration avoids dropping its draft on + // that crossing render. return Object.assign(window, { listRef: attachList, - setRowActive: window.virtualized ? setRowActive : NOOP_SET_ROW_ACTIVE, + setRowActive, }); } From 6cd20568ac0ad120fc515be4004bbf1cd3615fc1 Mon Sep 17 00:00:00 2001 From: OpenAI Codex <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 23:12:19 +0000 Subject: [PATCH 2/3] test(platform): cover blurred and focused log window crossings --- .../task-log-window.browser.test.tsx | 100 ++++++++++++++---- 1 file changed, 81 insertions(+), 19 deletions(-) diff --git a/services/platform/app/features/tasks/components/task-log-window.browser.test.tsx b/services/platform/app/features/tasks/components/task-log-window.browser.test.tsx index d560b5883e..84c4af419a 100644 --- a/services/platform/app/features/tasks/components/task-log-window.browser.test.tsx +++ b/services/platform/app/features/tasks/components/task-log-window.browser.test.tsx @@ -326,25 +326,87 @@ describe('task modal log window', () => { expect(list.querySelectorAll('[data-index]').length).toBeLessThan(40); }); - it('keeps an active edit mounted when the discussion crosses the window threshold', async () => { - state.comments = state.comments.slice(0, 90); - const view = render(); - const comment = await screen.findByText('Comment 0 opening line.'); - const row = comment.closest('li'); - if (row === null) throw new Error('Missing comment row'); - await userEvent.click( - within(row).getByRole('button', { name: 'actions.edit' }), - ); - const field = within(row).getByRole('textbox'); - await userEvent.fill(field, 'Draft retained across windowing'); - state.comments = Array.from({ length: 120 }, (_, index) => ({ - ...state.comments[index % state.comments.length], - messageId: `comment-${index}`, - })); - view.rerender(); - await waitFor(() => expect(field).toBeInTheDocument()); - expect(field).toHaveValue('Draft retained across windowing'); - }); + it.each(['blurred', 'focused'] as const)( + 'retains the same %s offscreen editor when the discussion crosses the window threshold', + async (focus) => { + const comments = state.comments.slice(0, 120); + state.comments = comments.slice(0, 90); + const view = render(); + const scroller = screen.getByTestId('scrollport'); + const list = scroller.querySelector('ul'); + if (list === null) throw new Error('Missing comment list'); + expect(list.querySelectorAll('[data-index]')).toHaveLength(90); + await scrollTo(scroller, listStart(list, scroller)); + const comment = await screen.findByText('Comment 0 opening line.'); + const row = comment.closest('li'); + if (row === null) throw new Error('Missing comment row'); + await userEvent.click( + within(row).getByRole('button', { name: 'actions.edit' }), + ); + const field = within(row).getByRole('textbox'); + const draft = 'Draft retained across windowing'; + await userEvent.fill(field, draft); + expect(field).toHaveFocus(); + if (focus === 'blurred') { + await userEvent.click( + screen.getByRole('button', { name: 'detail.showEarlierComments' }), + ); + expect(field).not.toHaveFocus(); + expect(row).not.toContainElement(document.activeElement); + } + await scrollTo( + scroller, + listStart(list, scroller) + list.offsetHeight - scroller.clientHeight, + ); + await waitFor(() => { + expect(screen.getByText('Comment 89 opening line.')).toBeVisible(); + expect(row.getBoundingClientRect().bottom).toBeLessThan( + scroller.getBoundingClientRect().top, + ); + }); + // Allow the shared focus bridge's deferred blur release to finish before + // crossing. Otherwise its focus pin could mask missing edit registration. + await act(async () => { + for (let frame = 0; frame < 3; frame++) { + await new Promise((resolve) => + requestAnimationFrame(() => resolve()), + ); + } + }); + if (focus === 'focused') expect(field).toHaveFocus(); + else expect(row).not.toContainElement(document.activeElement); + state.comments = comments; + view.rerender(); + await waitFor(() => { + expect(list.querySelectorAll('[data-index]').length).toBeGreaterThan(0); + expect(list.querySelectorAll('[data-index]').length).toBeLessThan(40); + // A nearby unpinned row must leave the range as well: bounded rows + // alone would not prove that the edit is outside the visible window. + expect(screen.queryByText('Comment 10 opening line.')).toBeNull(); + expect(row.getBoundingClientRect().bottom).toBeLessThan( + scroller.getBoundingClientRect().top, + ); + }); + await act(async () => { + for (let frame = 0; frame < 3; frame++) { + await new Promise((resolve) => + requestAnimationFrame(() => resolve()), + ); + } + }); + expect(list.querySelectorAll('[data-index]').length).toBeLessThan(40); + expect(field).toBeInTheDocument(); + expect(within(row).getByRole('textbox')).toBe(field); + expect(field).toHaveValue(draft); + if (focus === 'focused') { + expect(field).toHaveFocus(); + expect(document.activeElement).not.toBe(document.body); + } else { + expect(field).not.toHaveFocus(); + expect(row).not.toContainElement(document.activeElement); + } + }, + ); it('keeps an edit draft mounted after scrolling into older pages and moving focus', async () => { render(); From d907a83db30d3b0bea4ea1565434aefee0c9b373 Mon Sep 17 00:00:00 2001 From: OpenAI Codex <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 23:36:33 +0000 Subject: [PATCH 3/3] fix(platform): type active element assertions --- .../tasks/components/task-log-window.browser.test.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/platform/app/features/tasks/components/task-log-window.browser.test.tsx b/services/platform/app/features/tasks/components/task-log-window.browser.test.tsx index 84c4af419a..8d2ee4d0f1 100644 --- a/services/platform/app/features/tasks/components/task-log-window.browser.test.tsx +++ b/services/platform/app/features/tasks/components/task-log-window.browser.test.tsx @@ -352,7 +352,7 @@ describe('task modal log window', () => { screen.getByRole('button', { name: 'detail.showEarlierComments' }), ); expect(field).not.toHaveFocus(); - expect(row).not.toContainElement(document.activeElement); + expect(row.contains(document.activeElement)).toBe(false); } await scrollTo( scroller, @@ -374,7 +374,7 @@ describe('task modal log window', () => { } }); if (focus === 'focused') expect(field).toHaveFocus(); - else expect(row).not.toContainElement(document.activeElement); + else expect(row.contains(document.activeElement)).toBe(false); state.comments = comments; view.rerender(); await waitFor(() => { @@ -403,7 +403,7 @@ describe('task modal log window', () => { expect(document.activeElement).not.toBe(document.body); } else { expect(field).not.toHaveFocus(); - expect(row).not.toContainElement(document.activeElement); + expect(row.contains(document.activeElement)).toBe(false); } }, );