From b58f8197dc362cb7647e85e70f82e2a60a877a7e Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Mon, 5 Oct 2026 21:40:55 -0700 Subject: [PATCH] Soften native chat colors and code surfaces (#25651) * feat(native-chat): add chat-scoped color tokens * feat(native-chat): soften transcript and composer appearance * fix(native-chat): refine code spacing and faint text styling * fix(native-chat): wrap prose links at word boundaries * test(native-chat): refresh background task strip snapshots --- src/renderer/src/assets/main.css | 41 +++++++++++++++++ .../native-chat/NativeChatApprovalCard.tsx | 13 +++--- .../NativeChatBackgroundTasksStatus.tsx | 4 +- .../native-chat/NativeChatCodeBlock.test.tsx | 15 +++++- .../native-chat/NativeChatCodeBlock.tsx | 13 +++--- .../native-chat/NativeChatComposerActions.tsx | 2 +- .../native-chat/NativeChatComposerField.tsx | 12 ++--- .../native-chat/NativeChatCopyButton.tsx | 2 +- .../NativeChatImageAttachmentPreview.tsx | 6 +-- .../native-chat/NativeChatLaunchRetry.tsx | 2 +- .../native-chat/NativeChatMarkdown.test.tsx | 46 +++++++++++++++++++ .../native-chat/NativeChatMarkdown.tsx | 11 +++++ ...ChatMessageList.rail-history-jump.test.tsx | 4 +- .../NativeChatMessageList.test.tsx | 2 +- .../native-chat/NativeChatMessageList.tsx | 12 +++-- ...ChatMessageList.windowing-test-support.tsx | 2 +- .../NativeChatMessageList.windowing.test.tsx | 2 +- .../native-chat/NativeChatMessageRow.test.tsx | 16 ++++++- .../native-chat/NativeChatMessageRow.tsx | 25 +++++----- .../NativeChatMessageTimestamp.tsx | 2 +- .../native-chat/NativeChatNoticeRow.tsx | 11 +++-- .../NativeChatPaneFileDropSurface.tsx | 2 +- .../native-chat/NativeChatPromptEditor.tsx | 2 +- .../native-chat/NativeChatQuestionCard.tsx | 4 +- .../NativeChatQueuedMessageList.tsx | 2 +- .../native-chat/NativeChatResolvedView.tsx | 2 +- .../NativeChatSessionOptionPickers.tsx | 4 +- .../NativeChatStructuredSession.tsx | 2 +- .../NativeChatStructuredSessionStatus.tsx | 4 +- .../NativeChatThreadGoalBanner.tsx | 2 +- .../NativeChatTranscriptChrome.tsx | 8 ++-- .../native-chat/NativeChatWorkingStatus.tsx | 10 ++-- .../native-chat/native-chat-markdown.css | 42 +++++++++++++++++ .../native-chat-windowing-test-harness.tsx | 2 +- ...nt-child-row-fallback-pixels.test.tsx.snap | 4 +- .../terminal-pane/NativeChatPaneCover.tsx | 2 +- 36 files changed, 255 insertions(+), 80 deletions(-) create mode 100644 src/renderer/src/components/native-chat/NativeChatMarkdown.test.tsx create mode 100644 src/renderer/src/components/native-chat/NativeChatMarkdown.tsx create mode 100644 src/renderer/src/components/native-chat/native-chat-markdown.css diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css index f33765602cd..aa4c597f108 100644 --- a/src/renderer/src/assets/main.css +++ b/src/renderer/src/assets/main.css @@ -43,6 +43,19 @@ @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); + --color-chat-canvas: var(--chat-canvas); + --color-chat-foreground: var(--chat-foreground); + --color-chat-foreground-strong: var(--chat-foreground-strong); + --color-chat-foreground-faint: var(--chat-foreground-faint); + --color-chat-user-surface: var(--chat-user-surface); + --color-chat-user-border: var(--chat-user-border); + --color-chat-code-foreground: var(--chat-code-foreground); + --color-chat-code-surface: var(--chat-code-surface); + --color-chat-code-border: var(--chat-code-border); + --color-chat-inline-code-surface: var(--chat-inline-code-surface); + --color-chat-inline-code-border: var(--chat-inline-code-border); + --color-chat-composer-surface: var(--chat-composer-surface); + --color-chat-composer-border: var(--chat-composer-border); --color-card: var(--card); --color-card-foreground: var(--card-foreground); --color-popover: var(--popover); @@ -160,6 +173,20 @@ --background: #fff; --editor-surface: #ffffff; --foreground: #0a0a0a; + --chat-canvas: var(--background); + --chat-foreground: color-mix(in srgb, var(--foreground) 82%, var(--chat-canvas)); + --chat-foreground-strong: color-mix(in srgb, var(--foreground) 92%, var(--chat-canvas)); + --chat-foreground-faint: var(--muted-foreground); + --chat-user-surface: color-mix(in srgb, var(--foreground) 4%, var(--chat-canvas)); + --chat-user-border: color-mix(in srgb, var(--foreground) 7%, transparent); + --chat-code-foreground: color-mix(in srgb, var(--foreground) 85%, var(--chat-canvas)); + --chat-code-surface: color-mix(in srgb, var(--foreground) 2%, var(--chat-canvas)); + --chat-code-border: color-mix(in srgb, var(--foreground) 7%, transparent); + --chat-inline-code-surface: color-mix(in srgb, var(--foreground) 4%, var(--chat-canvas)); + --chat-inline-code-border: color-mix(in srgb, var(--foreground) 9%, transparent); + --chat-composer-surface: var(--chat-canvas); + --chat-composer-border: color-mix(in srgb, var(--foreground) 11%, transparent); + --chat-content-max-width: 46rem; --card: #fff; --card-foreground: #0a0a0a; --popover: #fff; @@ -283,6 +310,20 @@ --background: #0a0a0a; --editor-surface: #1e1e1e; --foreground: #fafafa; + --chat-canvas: color-mix(in srgb, var(--foreground) 5%, var(--background)); + --chat-foreground: color-mix(in srgb, var(--foreground) 78%, var(--chat-canvas)); + --chat-foreground-strong: color-mix(in srgb, var(--foreground) 90%, var(--chat-canvas)); + --chat-foreground-faint: color-mix(in srgb, var(--muted-foreground) 83%, var(--chat-canvas)); + --chat-user-surface: color-mix(in srgb, var(--foreground) 7%, var(--chat-canvas)); + --chat-user-border: color-mix(in srgb, var(--foreground) 6%, transparent); + --chat-code-foreground: color-mix(in srgb, var(--foreground) 85%, var(--chat-canvas)); + --chat-code-surface: color-mix(in srgb, var(--foreground) 3.5%, transparent); + --chat-code-border: color-mix(in srgb, var(--foreground) 7%, transparent); + --chat-inline-code-surface: color-mix(in srgb, var(--foreground) 6%, transparent); + --chat-inline-code-border: color-mix(in srgb, var(--foreground) 8%, transparent); + --chat-composer-surface: color-mix(in srgb, var(--foreground) 4%, transparent); + --chat-composer-border: color-mix(in srgb, var(--foreground) 9%, transparent); + --chat-content-max-width: 46rem; --card: #171717; --card-foreground: #fafafa; --popover: #171717; diff --git a/src/renderer/src/components/native-chat/NativeChatApprovalCard.tsx b/src/renderer/src/components/native-chat/NativeChatApprovalCard.tsx index 8ab0d6d822c..fefca7a5a9d 100644 --- a/src/renderer/src/components/native-chat/NativeChatApprovalCard.tsx +++ b/src/renderer/src/components/native-chat/NativeChatApprovalCard.tsx @@ -2,9 +2,8 @@ import { useEffect, useRef } from 'react' import { ShieldQuestion, X } from 'lucide-react' import { cn } from '@/lib/utils' import { translate } from '@/i18n/i18n' -import CommentMarkdown, { - type CommentMarkdownLinkClickHandler -} from '@/components/sidebar/CommentMarkdown' +import type { CommentMarkdownLinkClickHandler } from '@/components/sidebar/CommentMarkdown' +import { NativeChatMarkdown } from './NativeChatMarkdown' import { isNewerApprovalSubject, isPlanApprovalSubject @@ -55,8 +54,8 @@ export function NativeChatApprovalCard({ }, [shouldFocus]) return ( -
{children}
diff --git a/src/renderer/src/components/native-chat/NativeChatComposerActions.tsx b/src/renderer/src/components/native-chat/NativeChatComposerActions.tsx
index a8fed079f69..f982a1a815a 100644
--- a/src/renderer/src/components/native-chat/NativeChatComposerActions.tsx
+++ b/src/renderer/src/components/native-chat/NativeChatComposerActions.tsx
@@ -95,7 +95,7 @@ export function NativeChatComposerActions({
)
return (
-
+
diff --git a/src/renderer/src/components/native-chat/NativeChatComposerField.tsx b/src/renderer/src/components/native-chat/NativeChatComposerField.tsx
index 77f33c63334..157594e215b 100644
--- a/src/renderer/src/components/native-chat/NativeChatComposerField.tsx
+++ b/src/renderer/src/components/native-chat/NativeChatComposerField.tsx
@@ -179,10 +179,10 @@ export function NativeChatComposerField({
}
return (
-
+
{/* Extra bottom padding keeps the input box off the window rim. */}
-
+
{autocomplete.mode === 'slash' ? (
diff --git a/src/renderer/src/components/native-chat/NativeChatCopyButton.tsx b/src/renderer/src/components/native-chat/NativeChatCopyButton.tsx
index 0e933584d41..d8ebf335a6b 100644
--- a/src/renderer/src/components/native-chat/NativeChatCopyButton.tsx
+++ b/src/renderer/src/components/native-chat/NativeChatCopyButton.tsx
@@ -58,7 +58,7 @@ export function NativeChatCopyButton({
aria-label={label}
title={label}
className={cn(
- 'flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
+ 'flex size-6 shrink-0 items-center justify-center rounded-md text-chat-foreground-faint transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
copied && 'text-status-success',
className
)}
diff --git a/src/renderer/src/components/native-chat/NativeChatImageAttachmentPreview.tsx b/src/renderer/src/components/native-chat/NativeChatImageAttachmentPreview.tsx
index 1a928c7c8ad..4c7971047bb 100644
--- a/src/renderer/src/components/native-chat/NativeChatImageAttachmentPreview.tsx
+++ b/src/renderer/src/components/native-chat/NativeChatImageAttachmentPreview.tsx
@@ -45,7 +45,7 @@ function RemoveAttachmentButton({ onRemove }: { onRemove: () => void }): React.J
'components.native-chat.composer.removeAttachment',
'Remove attachment'
)}
- className="absolute -right-1.5 -top-1.5 flex size-4 items-center justify-center rounded-full border border-border bg-background text-muted-foreground shadow-xs transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
+ className="absolute -right-1.5 -top-1.5 flex size-4 items-center justify-center rounded-full border border-border bg-chat-canvas text-muted-foreground shadow-xs transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
@@ -155,7 +155,7 @@ function NativeChatImageThumbnail({ attachment, onRemove }: Props): React.JSX.El
aria-busy={isPending}
title={label}
onClick={() => setIsOpen(true)}
- className="flex size-full items-center justify-center overflow-hidden rounded-md border border-border bg-background transition-colors hover:border-ring focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
+ className="flex size-full items-center justify-center overflow-hidden rounded-md border border-border bg-chat-canvas transition-colors hover:border-ring focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
{thumbnailSrc ? (
{isPending ? (
-
+
) : null}
diff --git a/src/renderer/src/components/native-chat/NativeChatLaunchRetry.tsx b/src/renderer/src/components/native-chat/NativeChatLaunchRetry.tsx
index c3984b6ef54..13d4c5fc9d5 100644
--- a/src/renderer/src/components/native-chat/NativeChatLaunchRetry.tsx
+++ b/src/renderer/src/components/native-chat/NativeChatLaunchRetry.tsx
@@ -40,7 +40,7 @@ export function NativeChatLaunchRetry({
)
: ''
return (
-
+
{cause ? joinSentences([message, cause]) : message}
diff --git a/src/renderer/src/components/native-chat/NativeChatMarkdown.test.tsx b/src/renderer/src/components/native-chat/NativeChatMarkdown.test.tsx
new file mode 100644
index 00000000000..8b189102ee3
--- /dev/null
+++ b/src/renderer/src/components/native-chat/NativeChatMarkdown.test.tsx
@@ -0,0 +1,46 @@
+// @vitest-environment happy-dom
+import '@testing-library/jest-dom/vitest'
+import { cleanup, render, screen } from '@testing-library/react'
+import { afterEach, describe, expect, it } from 'vitest'
+import CommentMarkdown from '@/components/sidebar/CommentMarkdown'
+import { NativeChatMarkdown } from './NativeChatMarkdown'
+
+afterEach(cleanup)
+
+describe('chat markdown isolation', () => {
+ it('opts chat into appearance styles without applying them to ordinary comments', () => {
+ render(
+ <>
+
+
+ >
+ )
+
+ expect(screen.getByTestId('chat')).toHaveClass('native-chat-markdown', 'text-sm')
+ expect(screen.getByTestId('comment')).not.toHaveClass('native-chat-markdown')
+ expect(screen.getByText('Chat message')).toBeInTheDocument()
+ expect(screen.getByText('Sidebar comment')).toBeInTheDocument()
+ })
+
+ it('scopes word-boundary link wrapping to chat links', () => {
+ render(
+ <>
+
+
+ >
+ )
+
+ expect(
+ screen.getByRole('link', { name: 'AppearancePane.tsx' }).matches('.native-chat-markdown a')
+ ).toBe(true)
+ expect(
+ screen.getByRole('link', { name: 'SidebarPane.tsx' }).matches('.native-chat-markdown a')
+ ).toBe(false)
+ })
+})
diff --git a/src/renderer/src/components/native-chat/NativeChatMarkdown.tsx b/src/renderer/src/components/native-chat/NativeChatMarkdown.tsx
new file mode 100644
index 00000000000..3f2e89fb07e
--- /dev/null
+++ b/src/renderer/src/components/native-chat/NativeChatMarkdown.tsx
@@ -0,0 +1,11 @@
+import type { ComponentProps } from 'react'
+import CommentMarkdown from '@/components/sidebar/CommentMarkdown'
+import { cn } from '@/lib/utils'
+import './native-chat-markdown.css'
+
+export function NativeChatMarkdown({
+ className,
+ ...props
+}: ComponentProps): React.JSX.Element {
+ return
+}
diff --git a/src/renderer/src/components/native-chat/NativeChatMessageList.rail-history-jump.test.tsx b/src/renderer/src/components/native-chat/NativeChatMessageList.rail-history-jump.test.tsx
index 171385054d6..d48147a620a 100644
--- a/src/renderer/src/components/native-chat/NativeChatMessageList.rail-history-jump.test.tsx
+++ b/src/renderer/src/components/native-chat/NativeChatMessageList.rail-history-jump.test.tsx
@@ -195,7 +195,9 @@ describe('jumping from the rail while following the end', () => {
configurable: true,
get: () =>
TOP_GUTTER_PX +
- (screen.queryByRole('button', { name: /load earlier messages/i })?.closest('.max-w-4xl')
+ (screen
+ .queryByRole('button', { name: /load earlier messages/i })
+ ?.closest('[data-native-chat-transcript-column]')
? OLDER_HISTORY_ROW_PX
: 0)
})
diff --git a/src/renderer/src/components/native-chat/NativeChatMessageList.test.tsx b/src/renderer/src/components/native-chat/NativeChatMessageList.test.tsx
index b72562d128a..c108e87bea8 100644
--- a/src/renderer/src/components/native-chat/NativeChatMessageList.test.tsx
+++ b/src/renderer/src/components/native-chat/NativeChatMessageList.test.tsx
@@ -421,7 +421,7 @@ describe('NativeChatMessageList childless spawn group', () => {
/** Every slot the transcript column lays out — one per row that mounted. */
function emptySlots(container: HTMLElement): Element[] {
- const column = container.querySelector('.max-w-4xl')
+ const column = container.querySelector('[data-native-chat-transcript-column]')
expect(column).not.toBeNull()
return Array.from(column!.children).filter((slot) => slot.textContent === '')
}
diff --git a/src/renderer/src/components/native-chat/NativeChatMessageList.tsx b/src/renderer/src/components/native-chat/NativeChatMessageList.tsx
index efe1c54dd01..1916af19c6b 100644
--- a/src/renderer/src/components/native-chat/NativeChatMessageList.tsx
+++ b/src/renderer/src/components/native-chat/NativeChatMessageList.tsx
@@ -360,7 +360,7 @@ export function NativeChatMessageList({
return (
-
+
{taskListState.list && taskListState.list.tasks.length > 0 ? (
-
+
{
expect(container.querySelector('[data-native-chat-window]')).toBeInTheDocument()
expect(container.querySelectorAll('[data-index]')).toHaveLength(0)
expect(screen.queryByText(/^marker-/)).toBeNull()
- const column = container.querySelector('.max-w-4xl')
+ const column = container.querySelector('[data-native-chat-transcript-column]')
expect(column?.children).toHaveLength(1)
viewportHeight = VIEWPORT_PX
diff --git a/src/renderer/src/components/native-chat/NativeChatMessageRow.test.tsx b/src/renderer/src/components/native-chat/NativeChatMessageRow.test.tsx
index f9e247d1354..ffbd3d8c1f1 100644
--- a/src/renderer/src/components/native-chat/NativeChatMessageRow.test.tsx
+++ b/src/renderer/src/components/native-chat/NativeChatMessageRow.test.tsx
@@ -131,6 +131,20 @@ describe('MessageRow control visibility', () => {
expect(screen.queryByRole('time')).toBeNull()
expect(screen.queryByRole('button')).toBeNull()
})
+
+ it.each(['reasoning', 'system'] as const)(
+ 'keeps %s rows upright and scopes their faint text',
+ (role) => {
+ const { container } = renderMessage(role)
+ const row = container.querySelector('[data-native-chat-message-tone="faint"]')
+ expect(row).toHaveClass('text-chat-foreground-faint')
+ expect(row).not.toHaveClass('italic')
+ expect(row).toContainElement(screen.getByText('Message text'))
+ if (role === 'reasoning') {
+ expect(row).toHaveClass('border-l-2')
+ }
+ }
+ )
})
describe('MessageRow send mode', () => {
@@ -211,7 +225,7 @@ describe('what a user message says about its delivery', () => {
const sending = screen.getByText('Sending…')
const copy = screen.getByRole('button', { name: 'Copy message' })
- expect(sending).toHaveClass('text-xs', 'text-muted-foreground')
+ expect(sending).toHaveClass('text-xs', 'text-chat-foreground-faint')
expect(Array.from(sending.parentElement!.children)).toEqual([copy, sending])
expect(sending.parentElement).not.toHaveClass('can-hover:opacity-0')
expect(sending.parentElement!.parentElement).toHaveClass('group')
diff --git a/src/renderer/src/components/native-chat/NativeChatMessageRow.tsx b/src/renderer/src/components/native-chat/NativeChatMessageRow.tsx
index 10c8a68de07..52dee6107b7 100644
--- a/src/renderer/src/components/native-chat/NativeChatMessageRow.tsx
+++ b/src/renderer/src/components/native-chat/NativeChatMessageRow.tsx
@@ -1,9 +1,8 @@
import { memo, useCallback, useRef } from 'react'
import { Goal, RotateCcw } from 'lucide-react'
import { Button } from '@/components/ui/button'
-import CommentMarkdown, {
- type CommentMarkdownLinkClickHandler
-} from '@/components/sidebar/CommentMarkdown'
+import type { CommentMarkdownLinkClickHandler } from '@/components/sidebar/CommentMarkdown'
+import { NativeChatMarkdown } from './NativeChatMarkdown'
import { cn } from '@/lib/utils'
import { translate } from '@/i18n/i18n'
import type {
@@ -59,7 +58,7 @@ function UserMessageMeta({
) : null}
{sending ? (
-
+
{translate('components.native-chat.messageSending', 'Sending…')}
) : (
@@ -178,9 +177,8 @@ export const MessageRow = memo(function MessageRow({
if (isUser) {
return (
- {/* User turns get a distinct muted fill (not the card/canvas color) so
- the prompt reads apart from the assistant's body copy. */}
-
+ {/* A distinct surface separates the user's prompt from the assistant's prose. */}
+
{markdown ? (
<>
-
{markdown ? (
-
diff --git a/src/renderer/src/components/native-chat/NativeChatNoticeRow.tsx b/src/renderer/src/components/native-chat/NativeChatNoticeRow.tsx
index b63eb73abd1..66ac58aeb75 100644
--- a/src/renderer/src/components/native-chat/NativeChatNoticeRow.tsx
+++ b/src/renderer/src/components/native-chat/NativeChatNoticeRow.tsx
@@ -1,7 +1,7 @@
import { AlertCircle, AlertTriangle, Info } from 'lucide-react'
-import CommentMarkdown, {
- type CommentMarkdownLinkClickHandler
-} from '@/components/sidebar/CommentMarkdown'
+import type { CommentMarkdownLinkClickHandler } from '@/components/sidebar/CommentMarkdown'
+import { NativeChatMarkdown } from './NativeChatMarkdown'
+import { NativeChatCodeBlock } from './NativeChatCodeBlock'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { translate } from '@/i18n/i18n'
import { cn } from '@/lib/utils'
@@ -74,10 +74,11 @@ export function NativeChatNoticeRow({
-
diff --git a/src/renderer/src/components/native-chat/NativeChatPromptEditor.tsx b/src/renderer/src/components/native-chat/NativeChatPromptEditor.tsx
index 66d5870e1e1..0f02869acb4 100644
--- a/src/renderer/src/components/native-chat/NativeChatPromptEditor.tsx
+++ b/src/renderer/src/components/native-chat/NativeChatPromptEditor.tsx
@@ -80,7 +80,7 @@ export function NativeChatPromptEditor({
role: 'textbox',
'aria-multiline': 'true',
'aria-label': placeholder,
- class: `${className ?? ''} whitespace-pre-wrap break-words [&_p]:m-0 [&_p.is-editor-empty:first-child]:before:content-[attr(data-placeholder)] [&_p.is-editor-empty:first-child]:before:text-muted-foreground/60 [&_p.is-editor-empty:first-child]:before:float-left [&_p.is-editor-empty:first-child]:before:h-0 [&_p.is-editor-empty:first-child]:before:pointer-events-none`,
+ class: `${className ?? ''} whitespace-pre-wrap break-words [&_p]:m-0 [&_p.is-editor-empty:first-child]:before:content-[attr(data-placeholder)] [&_p.is-editor-empty:first-child]:before:text-chat-foreground-faint [&_p.is-editor-empty:first-child]:before:float-left [&_p.is-editor-empty:first-child]:before:h-0 [&_p.is-editor-empty:first-child]:before:pointer-events-none`,
...Object.fromEntries(Object.entries(events).filter(([key]) => key.startsWith('aria-')))
},
// Clipboard input is always literal text; only the picker creates skill nodes.
diff --git a/src/renderer/src/components/native-chat/NativeChatQuestionCard.tsx b/src/renderer/src/components/native-chat/NativeChatQuestionCard.tsx
index b7c5dfefd8e..c7d50ebc816 100644
--- a/src/renderer/src/components/native-chat/NativeChatQuestionCard.tsx
+++ b/src/renderer/src/components/native-chat/NativeChatQuestionCard.tsx
@@ -158,8 +158,8 @@ export function NativeChatQuestionCard({
// Part of the composer: docked in the bottom input region, matching the
// composer's width and padding, rendered as the "ask" dialog card directly
// above the text input. Its free-text row is the answer input.
-
-
+
+
{total > 1 ? (
{prompt.questions.map((qq, i) => (
diff --git a/src/renderer/src/components/native-chat/NativeChatQueuedMessageList.tsx b/src/renderer/src/components/native-chat/NativeChatQueuedMessageList.tsx
index 17223e0194c..9c47a8c375e 100644
--- a/src/renderer/src/components/native-chat/NativeChatQueuedMessageList.tsx
+++ b/src/renderer/src/components/native-chat/NativeChatQueuedMessageList.tsx
@@ -39,7 +39,7 @@ export function NativeChatQueuedMessageList({
return (
{cards.length > 0 ? (
-
+
{/* One box: the pause row, when shown, is its first row, and each card a row below it. */}
{pause ? (
diff --git a/src/renderer/src/components/native-chat/NativeChatResolvedView.tsx b/src/renderer/src/components/native-chat/NativeChatResolvedView.tsx
index 6adf30e1e50..99a3cd7958f 100644
--- a/src/renderer/src/components/native-chat/NativeChatResolvedView.tsx
+++ b/src/renderer/src/components/native-chat/NativeChatResolvedView.tsx
@@ -348,7 +348,7 @@ export function NativeChatResolvedView({
onMouseUpCapture={contextMenu.onSelectionCapture}
onKeyUpCapture={contextMenu.onSelectionCapture}
onContextMenuCapture={contextMenu.onContextMenuCapture}
- className="flex h-full min-h-0 w-full flex-col bg-background focus:outline-none"
+ className="flex h-full min-h-0 w-full flex-col bg-chat-canvas focus:outline-none"
>
{viewState.kind === 'loading' ? (
diff --git a/src/renderer/src/components/native-chat/NativeChatSessionOptionPickers.tsx b/src/renderer/src/components/native-chat/NativeChatSessionOptionPickers.tsx
index 8f00ecef7a2..c4a6534fd2b 100644
--- a/src/renderer/src/components/native-chat/NativeChatSessionOptionPickers.tsx
+++ b/src/renderer/src/components/native-chat/NativeChatSessionOptionPickers.tsx
@@ -87,7 +87,7 @@ function PickerTrigger(props: {
variant="ghost"
size="xs"
aria-label={accessibleName}
- className="max-w-48 text-muted-foreground"
+ className="max-w-48"
>
{props.label}
@@ -249,7 +249,7 @@ function NativeChatSessionOptionPickersInner({
: null
return (
-
+
{viewState.kind === 'loading' ? (
diff --git a/src/renderer/src/components/native-chat/NativeChatStructuredSessionStatus.tsx b/src/renderer/src/components/native-chat/NativeChatStructuredSessionStatus.tsx
index e131d46fde7..1c1e6c5e541 100644
--- a/src/renderer/src/components/native-chat/NativeChatStructuredSessionStatus.tsx
+++ b/src/renderer/src/components/native-chat/NativeChatStructuredSessionStatus.tsx
@@ -62,12 +62,12 @@ export function NativeChatStructuredSessionStatus(props: {
return (
<>
{props.reconnecting && !props.error ? (
-
+
{translate('components.native-chat.state.reconnecting', 'Reconnecting to this chat…')}
) : null}
{props.error || props.composerError ? (
-
+
{props.error ?? props.composerError}
) : null}
diff --git a/src/renderer/src/components/native-chat/NativeChatThreadGoalBanner.tsx b/src/renderer/src/components/native-chat/NativeChatThreadGoalBanner.tsx
index 1c2b15b9044..d9cd3ce33ec 100644
--- a/src/renderer/src/components/native-chat/NativeChatThreadGoalBanner.tsx
+++ b/src/renderer/src/components/native-chat/NativeChatThreadGoalBanner.tsx
@@ -73,7 +73,7 @@ export function NativeChatThreadGoalBanner(props: {
className="group/goal relative -mb-2 shrink-0 px-3 sm:px-4"
data-native-chat-thread-goal={goal.status}
>
-
+
{/* Right after the task strip, that strip's bottom border is this tab's top edge. */}
diff --git a/src/renderer/src/components/native-chat/NativeChatTranscriptChrome.tsx b/src/renderer/src/components/native-chat/NativeChatTranscriptChrome.tsx
index bd93d0c2ee7..bbe503ba80c 100644
--- a/src/renderer/src/components/native-chat/NativeChatTranscriptChrome.tsx
+++ b/src/renderer/src/components/native-chat/NativeChatTranscriptChrome.tsx
@@ -129,7 +129,7 @@ function TranscriptImagePreview({
const viewImageLabel = translate('components.native-chat.composer.viewAttachment', 'View image')
const fallback = (
@@ -172,7 +172,7 @@ function TranscriptImagePreview({
aria-label={`${viewImageLabel}: ${label}`}
title={label}
onClick={() => setOpen(true)}
- className="flex size-full items-center justify-center overflow-hidden rounded-md border border-border bg-background transition-colors hover:border-ring focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
+ className="flex size-full items-center justify-center overflow-hidden rounded-md border border-border bg-chat-canvas transition-colors hover:border-ring focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
@@ -291,7 +291,7 @@ export function NativeChatAgentControls({
'Scroll this message to top'
)}
title={translate('components.native-chat.scrollMessageToTop', 'Scroll this message to top')}
- className="flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
+ className="flex size-6 shrink-0 items-center justify-center rounded-md text-chat-foreground-faint transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
diff --git a/src/renderer/src/components/native-chat/NativeChatWorkingStatus.tsx b/src/renderer/src/components/native-chat/NativeChatWorkingStatus.tsx
index c3210ea0370..c82befed7c4 100644
--- a/src/renderer/src/components/native-chat/NativeChatWorkingStatus.tsx
+++ b/src/renderer/src/components/native-chat/NativeChatWorkingStatus.tsx
@@ -1,5 +1,6 @@
import { ChevronRight } from 'lucide-react'
import { translate } from '@/i18n/i18n'
+import { cn } from '@/lib/utils'
import {
describeNativeChatTurnStatus,
formatNativeChatDuration,
@@ -68,11 +69,11 @@ export function NativeChatWorkingStatus({
const label = turnStatusLabel(key, duration)
// `tabular-nums`: the live clock reflows its own label every second otherwise.
const className =
- 'flex min-h-8 items-center gap-1 border-b border-border text-sm text-muted-foreground tabular-nums'
+ 'flex min-h-8 items-center gap-1 border-b border-border text-sm text-chat-foreground-faint tabular-nums'
const caret =
workedSeconds != null && onToggleExpanded ? (
) : null
@@ -81,7 +82,10 @@ export function NativeChatWorkingStatus({