From ee6a248ec93c3149fe9feb26de49bfde205af3fd Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Tue, 3 Nov 2026 21:49:09 -0800 Subject: [PATCH] Remove the New divider from Chat UI (#38044) --- docs/styleguide/native-chat.md | 2 +- src/renderer/src/assets/main.css | 3 - ...veChatMessageList.new-rows-marker.test.tsx | 149 -------- .../native-chat/NativeChatMessageList.tsx | 12 +- .../NativeChatNewRowsMarker.test.tsx | 106 ------ .../native-chat/NativeChatNewRowsMarker.tsx | 82 ---- .../native-chat/NativeChatTranscriptItems.tsx | 11 +- .../native-chat/NativeChatTranscriptRow.tsx | 12 +- .../native-chat-appearance-palette.test.ts | 63 ---- .../native-chat-new-rows-marker.test.tsx | 355 ------------------ .../native-chat-new-rows-marker.ts | 192 ---------- .../native-chat/native-chat-pinned-rows.ts | 11 +- .../use-native-chat-reveal-latest.ts | 9 +- .../use-native-chat-transcript-window.ts | 7 +- src/renderer/src/i18n/locales/de.json | 2 - src/renderer/src/i18n/locales/en.json | 2 - src/renderer/src/i18n/locales/es.json | 2 - src/renderer/src/i18n/locales/fr.json | 2 - src/renderer/src/i18n/locales/ja.json | 2 - src/renderer/src/i18n/locales/ko.json | 2 - src/renderer/src/i18n/locales/pt.json | 2 - src/renderer/src/i18n/locales/ru.json | 2 - src/renderer/src/i18n/locales/tr.json | 2 - src/renderer/src/i18n/locales/zh-Hant.json | 2 - src/renderer/src/i18n/locales/zh.json | 2 - 25 files changed, 11 insertions(+), 1025 deletions(-) delete mode 100644 src/renderer/src/components/native-chat/NativeChatMessageList.new-rows-marker.test.tsx delete mode 100644 src/renderer/src/components/native-chat/NativeChatNewRowsMarker.test.tsx delete mode 100644 src/renderer/src/components/native-chat/NativeChatNewRowsMarker.tsx delete mode 100644 src/renderer/src/components/native-chat/native-chat-new-rows-marker.test.tsx delete mode 100644 src/renderer/src/components/native-chat/native-chat-new-rows-marker.ts diff --git a/docs/styleguide/native-chat.md b/docs/styleguide/native-chat.md index 8a731890d7b..3e3bb759446 100644 --- a/docs/styleguide/native-chat.md +++ b/docs/styleguide/native-chat.md @@ -25,4 +25,4 @@ The chat root (`.native-chat-appearance`) redefines these steps as ratios of the - **Secondary text on the chat canvas** (transcript rows, notices, labels between messages) uses `text-chat-foreground-faint`. `text-muted-foreground` is for text on a lifted surface inside the chat: cards, menus, popovers and the docks above the composer. - **Row hover** in the transcript is `hover:bg-accent/20`. Buttons, menu items and card controls use `hover:bg-accent`. - **Focus rings:** full-width transcript rows use the inset ring (`focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring`) so it isn't clipped by the scroll container; standalone controls use `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring`. Inside the chat `--ring` is `--chat-focus-ring`, which clears 3:1 on the canvas; don't lower its opacity. -- **State colours:** warnings use `text-chat-warning`; diff lines use `text-chat-diff-added` / `text-chat-diff-removed`; what the reader has not seen yet uses `text-chat-unread`. They are mixed toward the foreground so they pass text contrast in light and dark. Don't use raw palette colours (`text-yellow-500`) or the git decoration tokens for text on a tinted row. +- **State colours:** warnings use `text-chat-warning`; diff lines use `text-chat-diff-added` / `text-chat-diff-removed`. They are mixed toward the foreground so they pass text contrast in light and dark. Don't use raw palette colours (`text-yellow-500`) or the git decoration tokens for text on a tinted row. diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css index 7de3ba42d9b..e1e8f32a337 100644 --- a/src/renderer/src/assets/main.css +++ b/src/renderer/src/assets/main.css @@ -61,7 +61,6 @@ --color-chat-foreground: var(--chat-foreground); --color-chat-foreground-strong: var(--chat-foreground-strong); --color-chat-foreground-faint: var(--chat-foreground-faint); - --color-chat-unread: var(--chat-unread); --color-chat-user-surface: var(--chat-user-surface); --color-chat-user-border: var(--chat-user-border); --color-chat-code-foreground: var(--chat-code-foreground); @@ -697,8 +696,6 @@ body[data-modal-overlay-open] /* Mixed toward the foreground so each clears text contrast on the canvas in either scheme. */ --chat-focus-ring: color-mix(in srgb, var(--chat-source-foreground) 50%, var(--chat-canvas)); --chat-warning: color-mix(in srgb, var(--status-warning) 70%, var(--chat-source-foreground)); - /* What the reader has not seen yet: the transcript's "New" marker, in the note hue. */ - --chat-unread: color-mix(in srgb, var(--callout-note) 70%, var(--chat-source-foreground)); --chat-diff-added: color-mix(in srgb, var(--diff-added) 70%, var(--chat-source-foreground)); --chat-diff-removed: color-mix(in srgb, var(--diff-removed) 70%, var(--chat-source-foreground)); } diff --git a/src/renderer/src/components/native-chat/NativeChatMessageList.new-rows-marker.test.tsx b/src/renderer/src/components/native-chat/NativeChatMessageList.new-rows-marker.test.tsx deleted file mode 100644 index 1bd21b684ac..00000000000 --- a/src/renderer/src/components/native-chat/NativeChatMessageList.new-rows-marker.test.tsx +++ /dev/null @@ -1,149 +0,0 @@ -// @vitest-environment happy-dom - -import '@testing-library/jest-dom/vitest' - -import { act, cleanup, screen } from '@testing-library/react' -import { render } from './native-chat-tooltip-test-harness' -import { createRef } from 'react' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { NativeChatMessage } from '../../../../shared/native-chat/native-chat-types' -import { NativeChatMessageList } from './NativeChatMessageList' -import type { NativeChatMessageListHandle } from './use-native-chat-reveal-latest' -import { - deliverResizes, - layout, - marker, - ROW_PITCH_PX, - scrollTranscript, - session, - stubLayout, - stubResizeObserver, - TRANSCRIPT_LENGTH -} from './native-chat-windowing-test-harness' - -// Pacing is not this test's subject. -vi.mock('./use-native-chat-paced-text', async (importOriginal) => - (await import('./native-chat-unpaced-text-fixture')).unpacedTextModule(importOriginal) -) - -afterEach(cleanup) - -function chat( - length: number, - isVisible: boolean, - { - ref, - readCurrent = true - }: { ref?: React.Ref; readCurrent?: boolean } = {} -): React.JSX.Element { - const messages: NativeChatMessage[] = Array.from({ length }, (_, index) => ({ - ...marker(index), - journalPosition: { sequence: index + 1, index: 0 } - })) - return ( - - ) -} - -describe('the "New" marker in the transcript', () => { - let paneShown = true - let restore: (() => void)[] = [] - beforeEach(() => { - paneShown = true - vi.spyOn(document, 'hasFocus').mockReturnValue(true) - restore = [ - stubLayout({ scrollGeometry: true, offsetChain: true, isVisible: () => paneShown }), - stubResizeObserver() - ] - }) - afterEach(() => { - for (const undo of restore.toReversed()) { - undo() - } - layout.measuredRowHeights = [] - vi.restoreAllMocks() - }) - - // The window mounts only rows near the view; the marker's row is far from the end it opens at. - it('is drawn above the first row that came while the chat was hidden, however far back', () => { - const { rerender } = render(chat(TRANSCRIPT_LENGTH, true)) - rerender(chat(TRANSCRIPT_LENGTH, false)) - rerender(chat(TRANSCRIPT_LENGTH + 60, true)) - - const row = screen.getByRole('separator', { name: 'New' }).closest('[data-index]') - expect(row?.dataset.index).toBe(String(TRANSCRIPT_LENGTH)) - }) - - it('tells a screen reader that new messages came, in a region that was already there', () => { - const { container, rerender } = render(chat(TRANSCRIPT_LENGTH, true)) - const region = container.querySelector('[data-native-chat-scroll] ~ [aria-live="polite"]') - expect(region).toBeEmptyDOMElement() - - rerender(chat(TRANSCRIPT_LENGTH, false)) - rerender(chat(TRANSCRIPT_LENGTH + 60, true)) - expect(region).toHaveTextContent('New messages arrived while you were away') - }) - - it('goes once the reader sends something', () => { - const ref = createRef() - const { rerender } = render(chat(TRANSCRIPT_LENGTH, true, { ref })) - rerender(chat(TRANSCRIPT_LENGTH, false, { ref })) - rerender(chat(TRANSCRIPT_LENGTH + 60, true, { ref })) - expect(screen.getByRole('separator', { name: 'New' })).toBeInTheDocument() - - act(() => ref.current?.revealLatest()) - - expect(screen.queryByRole('separator', { name: 'New' })).toBeNull() - }) - - describe('when the reader comes back to new rows', () => { - /** Catch up after restoring the hidden pane’s scroll position. */ - function returnToNewRows(rerender: (ui: React.JSX.Element) => void, count = 60): void { - paneShown = false - rerender(chat(TRANSCRIPT_LENGTH, false, { readCurrent: false })) - paneShown = true - rerender(chat(TRANSCRIPT_LENGTH, true, { readCurrent: false })) - rerender(chat(TRANSCRIPT_LENGTH + count, true)) - } - const scrollTop = (container: HTMLElement): number | undefined => - container.querySelector('[data-native-chat-scroll]')?.scrollTop - - it('brings a reader who was at the end to where the new rows begin', () => { - const { container, rerender } = render(chat(TRANSCRIPT_LENGTH, true)) - returnToNewRows(rerender) - - expect(scrollTop(container)).toBe(TRANSCRIPT_LENGTH * ROW_PITCH_PX) - }) - - it('keeps the reader where the new rows begin while a reply is still being drawn', async () => { - const { container, rerender } = render(chat(TRANSCRIPT_LENGTH, true)) - returnToNewRows(rerender, 1) - - const drawn: number[] = [] - drawn[TRANSCRIPT_LENGTH] = 2_000 - layout.measuredRowHeights = drawn - act(() => void deliverResizes()) - await act(async () => { - await new Promise((resolve) => requestAnimationFrame(() => resolve())) - }) - - expect(scrollTop(container)).toBe(TRANSCRIPT_LENGTH * ROW_PITCH_PX) - }) - - it('leaves a reader who was parked further up where they were', () => { - const { container, rerender } = render(chat(TRANSCRIPT_LENGTH, true)) - const readingAt = 2_000 - scrollTranscript(container, readingAt) - returnToNewRows(rerender) - - expect(screen.getByRole('separator', { name: 'New' })).toBeInTheDocument() - expect(scrollTop(container)).toBe(readingAt) - }) - }) -}) diff --git a/src/renderer/src/components/native-chat/NativeChatMessageList.tsx b/src/renderer/src/components/native-chat/NativeChatMessageList.tsx index 29ffcc8ef20..fd4bc0f7066 100644 --- a/src/renderer/src/components/native-chat/NativeChatMessageList.tsx +++ b/src/renderer/src/components/native-chat/NativeChatMessageList.tsx @@ -4,7 +4,6 @@ import { NATIVE_CHAT_TRANSCRIPT_COLUMN_CLASS } from './native-chat-appearance-style' import { NativeChatJumpToLatest } from './NativeChatJumpToLatest' -import { NativeChatNewRowsAnnouncement } from './NativeChatNewRowsMarker' import { useNativeChatRowTypography } from './use-native-chat-row-typography' import { useCallback, useMemo, useRef, useState } from 'react' import type { CommentMarkdownLinkClickHandler } from '@/components/sidebar/CommentMarkdown' @@ -40,7 +39,6 @@ import { useNativeChatSubagentSections } from './use-native-chat-subagent-sectio import { toggleNativeChatExpandedKey } from './native-chat-expanded-keys' import { useNativeChatTurnMembership } from './use-native-chat-turn-membership' import { useNativeChatTranscriptScroll } from './use-native-chat-transcript-scroll' -import { useNativeChatNewRowsMarker } from './native-chat-new-rows-marker' import { useNativeChatOlderHistoryAutoload } from './use-native-chat-older-history-autoload' import { NativeChatOlderHistoryRow } from './NativeChatOlderHistoryRow' import { useNativeChatMessageRail } from './use-native-chat-message-rail' @@ -226,18 +224,12 @@ export function NativeChatMessageList({ ), [allSlots, journalItems, stopping, journalSubmissions, latestTurn, hostWork] ) - const { marker: newRowsMarker, dismiss } = useNativeChatNewRowsMarker( - slots, - waitingSlots, - isVisible && session.readCurrent === true - ) const transcriptWindow = useNativeChatTranscriptWindow({ scrollRef, slots, isVisible, // Rail jumps and diff reveals share one mutually exclusive pin. revealIndex: nativeChatSlotIndexOf(slots, railJump?.messageId ?? revealedDiff?.messageId), - newRowsMarkerIndex: nativeChatSlotIndexOf(slots, newRowsMarker?.key), typography }) const transcriptScroll = useNativeChatTranscriptScroll({ @@ -320,7 +312,7 @@ export function NativeChatMessageList({ onLeaveEnd: readerLeavesEnd, onScrollGesture: transcriptScroll.forwardedGesture }) - useNativeChatMessageListHandle(ref, jumpToLatest, readerInput.revealFindMatch, dismiss, allSlots) + useNativeChatMessageListHandle(ref, jumpToLatest, readerInput.revealFindMatch, allSlots) useNativeChatRailJumpLanding({ railJump, slots, @@ -381,7 +373,6 @@ export function NativeChatMessageList({ loadedMessageIds={loadedMessageIds} context={rowContext} window={transcriptWindow} - newRowsMarker={newRowsMarker} forwardGesture={readerInput.forwardedGesture} /> {liveLine ? ( @@ -405,7 +396,6 @@ export function NativeChatMessageList({ pendingId={railHistoryJump.pendingId} /> - diff --git a/src/renderer/src/components/native-chat/NativeChatNewRowsMarker.test.tsx b/src/renderer/src/components/native-chat/NativeChatNewRowsMarker.test.tsx deleted file mode 100644 index efa97c1abde..00000000000 --- a/src/renderer/src/components/native-chat/NativeChatNewRowsMarker.test.tsx +++ /dev/null @@ -1,106 +0,0 @@ -// @vitest-environment happy-dom - -import { cleanup, fireEvent, render, screen } from '@testing-library/react' -import { afterEach, describe, expect, it, vi } from 'vitest' -import { NativeChatNewRowsMarker } from './NativeChatNewRowsMarker' -import type { NativeChatNewRowsMarkerState as Marker } from './native-chat-new-rows-marker' -import { overrideLayoutProperty } from './native-chat-windowing-test-harness' - -afterEach(cleanup) - -function marker(overrides: Partial = {}): Marker { - return { - key: 'answer', - place: 'above-row', - arriving: true, - onArrived: vi.fn(), - onDismiss: vi.fn(), - ...overrides - } -} - -const VIEWPORT_PX = 600 -const ROW_TOP_PX = 1_000 - -let restoreLayout: (() => void)[] = [] - -/** Place the view bottom aboveMarkerPx short of a marker positioned intoRowPx within its row. */ -function pane( - value: Marker, - { intoRowPx = 0, aboveMarkerPx = 0 }: { intoRowPx?: number; aboveMarkerPx?: number } = {} -) { - const isRow = (element: HTMLElement): boolean => element.hasAttribute('data-index') - const isMarker = (element: HTMLElement): boolean => element.getAttribute('role') === 'separator' - const layout = (name: string, get: (this: HTMLElement) => unknown) => - overrideLayoutProperty(name, { get }) - restoreLayout = [ - layout('clientHeight', () => VIEWPORT_PX), - layout('scrollTop', () => ROW_TOP_PX + intoRowPx - VIEWPORT_PX - aboveMarkerPx), - layout('offsetTop', function () { - return isRow(this) ? ROW_TOP_PX : isMarker(this) ? intoRowPx : 0 - }), - layout('offsetParent', function () { - return isRow(this) || isMarker(this) ? this.parentElement : null - }) - ] - const onReveal = vi.fn() - const view = render( -
-
- -
-
-
- -
-
-