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 ( -
-
+
+
- {/* When the goal tab is the next sibling, take its width and share its top edge. */} -
+
{ + cleanup() vi.restoreAllMocks() }) describe('NativeChatCodeBlock', () => { + it('shows a plain language label without a header icon or divider', () => { + const { container } = render() + const label = screen.getByText('TypeScript').closest('[data-code-language]') + const header = label?.parentElement + + expect(label).toHaveClass('font-sans', 'text-xs', 'text-chat-foreground-faint') + expect(header).toHaveClass('h-7.5') + expect(header).not.toHaveClass('border-b', 'border-border/60') + expect(header?.querySelector('svg')).toBeNull() + expect(container.querySelector('pre')).toHaveClass('px-3.5', 'pt-0.5', 'pb-3') + }) + it('copies only the fenced code and confirms success', async () => { const writeClipboardText = vi.fn().mockResolvedValue(undefined) Object.assign(window, { api: { ui: { writeClipboardText } } }) diff --git a/src/renderer/src/components/native-chat/NativeChatCodeBlock.tsx b/src/renderer/src/components/native-chat/NativeChatCodeBlock.tsx index 0e7d106c85d..36a6ac9e2eb 100644 --- a/src/renderer/src/components/native-chat/NativeChatCodeBlock.tsx +++ b/src/renderer/src/components/native-chat/NativeChatCodeBlock.tsx @@ -1,5 +1,4 @@ import React from 'react' -import { Code2 } from 'lucide-react' import { translate } from '@/i18n/i18n' import { cn } from '@/lib/utils' import { getCodeBlockLanguageLabel } from '@/components/editor/rich-markdown-code-block-languages' @@ -16,14 +15,13 @@ export function NativeChatCodeBlock({ const code = extractCodeText(children) return ( -
+
{language ? ( -
+
- {getCodeBlockLanguageLabel(language)} {code ? ( @@ -36,9 +34,10 @@ export function NativeChatCodeBlock({
) : null}
         {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 ? (
-
+