diff --git a/packages/cli/src/ui/components/messages/DenseToolMessage.test.tsx b/packages/cli/src/ui/components/messages/DenseToolMessage.test.tsx index a4b3120b3f..221d702ef6 100644 --- a/packages/cli/src/ui/components/messages/DenseToolMessage.test.tsx +++ b/packages/cli/src/ui/components/messages/DenseToolMessage.test.tsx @@ -522,4 +522,54 @@ describe('DenseToolMessage', () => { expect(lastFrame()).not.toContain('new line'); }); }); + + describe('Visual Regression', () => { + it('matches SVG snapshot for an Accepted file edit with diff stats', async () => { + const diffResult: FileDiff = { + fileName: 'test.ts', + filePath: '/mock/test.ts', + fileDiff: '--- a/test.ts\n+++ b/test.ts\n@@ -1 +1 @@\n-old\n+new', + originalContent: 'old', + newContent: 'new', + diffStat: { + model_added_lines: 1, + model_removed_lines: 1, + model_added_chars: 3, + model_removed_chars: 3, + user_added_lines: 0, + user_removed_lines: 0, + user_added_chars: 0, + user_removed_chars: 0, + }, + }; + + const renderResult = await renderWithProviders( + , + ); + + await renderResult.waitUntilReady(); + await expect(renderResult).toMatchSvgSnapshot(); + }); + + it('matches SVG snapshot for a Rejected tool call', async () => { + const renderResult = await renderWithProviders( + , + ); + + await renderResult.waitUntilReady(); + await expect(renderResult).toMatchSvgSnapshot(); + }); + }); }); diff --git a/packages/cli/src/ui/components/messages/DenseToolMessage.tsx b/packages/cli/src/ui/components/messages/DenseToolMessage.tsx index 6dc9497d25..1c5f53d7a8 100644 --- a/packages/cli/src/ui/components/messages/DenseToolMessage.tsx +++ b/packages/cli/src/ui/components/messages/DenseToolMessage.tsx @@ -33,6 +33,7 @@ import { COMPACT_TOOL_SUBVIEW_MAX_LINES } from '../../constants.js'; import { useSettings } from '../../contexts/SettingsContext.js'; import { colorizeCode } from '../../utils/CodeColorizer.js'; import { useToolActions } from '../../contexts/ToolActionsContext.js'; +import { getFileExtension } from '../../utils/fileUtils.js'; interface DenseToolMessageProps extends IndividualToolCallDisplay { terminalWidth?: number; @@ -455,7 +456,9 @@ export const DenseToolMessage: React.FC = (props) => { .filter((line) => line.type === 'add') .map((line) => line.content) .join('\n'); - const fileExtension = diff.fileName?.split('.').pop() || null; + + const fileExtension = getFileExtension(diff.fileName); + return colorizeCode({ code: addedContent, language: fileExtension, diff --git a/packages/cli/src/ui/components/messages/DiffRenderer.tsx b/packages/cli/src/ui/components/messages/DiffRenderer.tsx index fec44bc19a..2a0d5b39c4 100644 --- a/packages/cli/src/ui/components/messages/DiffRenderer.tsx +++ b/packages/cli/src/ui/components/messages/DiffRenderer.tsx @@ -13,6 +13,7 @@ import { MaxSizedBox } from '../shared/MaxSizedBox.js'; import { theme as semanticTheme } from '../../semantic-colors.js'; import type { Theme } from '../../themes/theme.js'; import { useSettings } from '../../contexts/SettingsContext.js'; +import { getFileExtension } from '../../utils/fileUtils.js'; export interface DiffLine { type: 'add' | 'del' | 'context' | 'hunk' | 'other'; @@ -150,7 +151,7 @@ export const DiffRenderer: React.FC = ({ .map((line) => line.content) .join('\n'); // Attempt to infer language from filename, default to plain text if no filename - const fileExtension = filename?.split('.').pop() || null; + const fileExtension = getFileExtension(filename); const language = fileExtension ? getLanguageFromExtension(fileExtension) : null; @@ -259,7 +260,7 @@ export const renderDiffLines = ({ ); const gutterWidth = Math.max(1, maxLineNumber.toString().length); - const fileExtension = filename?.split('.').pop() || null; + const fileExtension = getFileExtension(filename); const language = fileExtension ? getLanguageFromExtension(fileExtension) : null; diff --git a/packages/cli/src/ui/components/messages/__snapshots__/DenseToolMessage-DenseToolMessage-Visual-Regression-matches-SVG-snapshot-for-a-Rejected-tool-call.snap.svg b/packages/cli/src/ui/components/messages/__snapshots__/DenseToolMessage-DenseToolMessage-Visual-Regression-matches-SVG-snapshot-for-a-Rejected-tool-call.snap.svg new file mode 100644 index 0000000000..96d89e7416 --- /dev/null +++ b/packages/cli/src/ui/components/messages/__snapshots__/DenseToolMessage-DenseToolMessage-Visual-Regression-matches-SVG-snapshot-for-a-Rejected-tool-call.snap.svg @@ -0,0 +1,11 @@ + + + + + - + read_file + Reading important.txt + + \ No newline at end of file diff --git a/packages/cli/src/ui/components/messages/__snapshots__/DenseToolMessage-DenseToolMessage-Visual-Regression-matches-SVG-snapshot-for-an-Accepted-file-edit-with-diff-stats.snap.svg b/packages/cli/src/ui/components/messages/__snapshots__/DenseToolMessage-DenseToolMessage-Visual-Regression-matches-SVG-snapshot-for-an-Accepted-file-edit-with-diff-stats.snap.svg new file mode 100644 index 0000000000..7b21bd65a0 --- /dev/null +++ b/packages/cli/src/ui/components/messages/__snapshots__/DenseToolMessage-DenseToolMessage-Visual-Regression-matches-SVG-snapshot-for-an-Accepted-file-edit-with-diff-stats.snap.svg @@ -0,0 +1,33 @@ + + + + + + edit + test.ts + → Accepted + ( + +1 + , + -1 + ) + + 1 + + + - + + + old + + 1 + + + + + + + new + + \ No newline at end of file diff --git a/packages/cli/src/ui/components/messages/__snapshots__/DenseToolMessage.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/DenseToolMessage.test.tsx.snap index e7cb9cb195..93c2dad9fd 100644 --- a/packages/cli/src/ui/components/messages/__snapshots__/DenseToolMessage.test.tsx.snap +++ b/packages/cli/src/ui/components/messages/__snapshots__/DenseToolMessage.test.tsx.snap @@ -13,6 +13,16 @@ exports[`DenseToolMessage > Toggleable Diff View (Alternate Buffer) > shows diff " `; +exports[`DenseToolMessage > Visual Regression > matches SVG snapshot for a Rejected tool call 1`] = `" - read_file Reading important.txt"`; + +exports[`DenseToolMessage > Visual Regression > matches SVG snapshot for an Accepted file edit with diff stats 1`] = ` +" ✓ edit test.ts → Accepted (+1, -1) + + 1 - old + 1 + new +" +`; + exports[`DenseToolMessage > does not render result arrow if resultDisplay is missing 1`] = ` " o test-tool Test description " diff --git a/packages/cli/src/ui/utils/fileUtils.ts b/packages/cli/src/ui/utils/fileUtils.ts new file mode 100644 index 0000000000..a1f3472aa4 --- /dev/null +++ b/packages/cli/src/ui/utils/fileUtils.ts @@ -0,0 +1,19 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as path from 'node:path'; + +/** + * Gets the file extension from a filename or path, excluding the leading dot. + * Returns null if no extension is found. + */ +export function getFileExtension( + filename: string | null | undefined, +): string | null { + if (!filename) return null; + const ext = path.extname(filename); + return ext ? ext.slice(1) : null; +}