diff --git a/desktop/src/components/chat/CurrentTurnChangeCard.test.tsx b/desktop/src/components/chat/CurrentTurnChangeCard.test.tsx index 3792e2e4..bea56635 100644 --- a/desktop/src/components/chat/CurrentTurnChangeCard.test.tsx +++ b/desktop/src/components/chat/CurrentTurnChangeCard.test.tsx @@ -134,6 +134,36 @@ function renderCard(filesChanged: string[]) { // ────────────────────────────────────────────────────────────────────────────── // Tests // ────────────────────────────────────────────────────────────────────────────── +describe('CurrentTurnChangeCard – rich file row (icon / name / type)', () => { + beforeEach(() => { + vi.clearAllMocks() + ensureTargetsMock.mockResolvedValue(undefined) + openPreviewSpy.mockResolvedValue(undefined) + }) + + it('renders the filename (not just full path) for each file', () => { + renderCard(['/w/proj/README.md', '/w/proj/src/index.ts']) + expect(screen.getByText('README.md')).toBeInTheDocument() + expect(screen.getByText('index.ts')).toBeInTheDocument() + }) + + it('renders the extension badge for a markdown file', () => { + renderCard(['/w/proj/README.md']) + // The type subtitle contains the ext in uppercase: "· MD" + expect(screen.getByText(/MD/)).toBeInTheDocument() + }) + + it('renders the extension badge for a TypeScript file', () => { + renderCard(['/w/proj/src/main.ts']) + expect(screen.getByText(/TS/)).toBeInTheDocument() + }) + + it('renders the extension badge for an HTML file', () => { + renderCard(['/w/proj/index.html']) + expect(screen.getByText(/HTML/)).toBeInTheDocument() + }) +}) + describe('CurrentTurnChangeCard – open-with buttons', () => { beforeEach(() => { vi.clearAllMocks() diff --git a/desktop/src/components/chat/CurrentTurnChangeCard.tsx b/desktop/src/components/chat/CurrentTurnChangeCard.tsx index 8405fed6..5b5bb71b 100644 --- a/desktop/src/components/chat/CurrentTurnChangeCard.tsx +++ b/desktop/src/components/chat/CurrentTurnChangeCard.tsx @@ -1,10 +1,11 @@ import { useCallback, useMemo, useState } from 'react' import type { MouseEvent as ReactMouseEvent } from 'react' +import { ChevronDown } from 'lucide-react' import { sessionsApi, type SessionTurnCheckpoint } from '../../api/sessions' import { useTranslation, type TranslationKey } from '../../i18n' import { WorkspaceDiffSurface } from '../workspace/WorkspaceCodeSurface' import { OpenWithMenu } from '../common/OpenWithMenu' -import { buildOpenWithItems, type OpenWithItem } from '../../lib/openWithItems' +import { buildOpenWithItems, describeFileType, type OpenWithItem } from '../../lib/openWithItems' import { openWithContextForWorkspaceFile } from '../../lib/openWithContextForHref' import { getServerBaseUrl } from '../../lib/desktopRuntime' import { useOpenTargetStore } from '../../stores/openTargetStore' @@ -173,9 +174,11 @@ export function CurrentTurnChangeCard({ {files.map((fileEntry) => { const isExpanded = expandedPath === fileEntry.apiPath const diffState = diffByPath[fileEntry.apiPath] + const fileName = fileEntry.displayPath.split('/').pop() || fileEntry.displayPath + const typeInfo = describeFileType(fileEntry.displayPath) return (
-
+
diff --git a/desktop/src/components/chat/MessageList.test.tsx b/desktop/src/components/chat/MessageList.test.tsx index 96322fde..e59eb8a0 100644 --- a/desktop/src/components/chat/MessageList.test.tsx +++ b/desktop/src/components/chat/MessageList.test.tsx @@ -3015,9 +3015,9 @@ describe('MessageList nested tool calls', () => { const cards = await screen.findAllByLabelText('Turn changed files') expect(cards).toHaveLength(2) - expect(screen.getByText('src/first.ts')).toBeTruthy() - expect(screen.getByText('src/second.ts')).toBeTruthy() - expect(screen.queryByText('src/third.ts')).toBeNull() + expect(screen.getByText('first.ts')).toBeTruthy() + expect(screen.getByText('second.ts')).toBeTruthy() + expect(screen.queryByText('third.ts')).toBeNull() }) it('expands a historical turn diff through the turn checkpoint diff API', async () => { @@ -3231,7 +3231,7 @@ describe('MessageList nested tool calls', () => { render() - expect(await screen.findByText('src/live.ts')).toBeTruthy() + expect(await screen.findByText('live.ts')).toBeTruthy() fireEvent.click(screen.getByRole('button', { name: 'Show diff for src/live.ts' })) await screen.findByTestId('workspace-code') expect(sessionsApi.getTurnCheckpointDiff).toHaveBeenCalledWith( @@ -3284,7 +3284,7 @@ describe('MessageList nested tool calls', () => { render() - expect(await screen.findByText('src/blank-response.ts')).toBeTruthy() + expect(await screen.findByText('blank-response.ts')).toBeTruthy() }) it('keeps historical turn change cards visible while the next turn is running', async () => { @@ -3329,7 +3329,7 @@ describe('MessageList nested tool calls', () => { render() - expect(await screen.findByText('src/first.ts')).toBeTruthy() + expect(await screen.findByText('first.ts')).toBeTruthy() act(() => { useChatStore.setState({ @@ -3343,7 +3343,7 @@ describe('MessageList nested tool calls', () => { }) await waitFor(() => { - expect(screen.getByText('src/first.ts')).toBeTruthy() + expect(screen.getByText('first.ts')).toBeTruthy() }) }) @@ -3452,7 +3452,7 @@ describe('MessageList nested tool calls', () => { render() - const historicalCard = (await screen.findByText('src/first.ts')).closest('section') + const historicalCard = (await screen.findByText('first.ts')).closest('section') expect(historicalCard).toBeTruthy() fireEvent.click( within(historicalCard as HTMLElement).getByRole('button', { @@ -3553,8 +3553,8 @@ describe('MessageList nested tool calls', () => { const cards = await screen.findAllByLabelText('Turn changed files') expect(cards).toHaveLength(1) - expect(screen.getByText('src/first.ts')).toBeTruthy() - expect(screen.queryByText('src/second.ts')).toBeNull() + expect(screen.getByText('first.ts')).toBeTruthy() + expect(screen.queryByText('second.ts')).toBeNull() }) it('shows raw startup details under translated CLI startup errors', () => { diff --git a/desktop/src/i18n/locales/en.ts b/desktop/src/i18n/locales/en.ts index 7bd485a3..05eb94d0 100644 --- a/desktop/src/i18n/locales/en.ts +++ b/desktop/src/i18n/locales/en.ts @@ -99,6 +99,11 @@ export const en = { 'openWith.workspacePreview': 'Workspace preview', 'openWith.openInTarget': 'Open in {target}', 'openWith.revealInTarget': 'Reveal in {target}', + 'openWith.fileType.document': 'Document', + 'openWith.fileType.web': 'Web', + 'openWith.fileType.image': 'Image', + 'openWith.fileType.code': 'Code', + 'openWith.fileType.file': 'File', // ─── Workspace Panel ─────────────────────────────── 'workspace.changedFiles': 'Changed files', diff --git a/desktop/src/i18n/locales/zh.ts b/desktop/src/i18n/locales/zh.ts index f89addbb..cf20c251 100644 --- a/desktop/src/i18n/locales/zh.ts +++ b/desktop/src/i18n/locales/zh.ts @@ -101,6 +101,11 @@ export const zh: Record = { 'openWith.workspacePreview': '工作台预览', 'openWith.openInTarget': '用 {target} 打开', 'openWith.revealInTarget': '在 {target} 中显示', + 'openWith.fileType.document': '文档', + 'openWith.fileType.web': '网页', + 'openWith.fileType.image': '图片', + 'openWith.fileType.code': '代码', + 'openWith.fileType.file': '文件', // ─── Workspace Panel ─────────────────────────────── 'workspace.changedFiles': '已更改文件', diff --git a/desktop/src/lib/openWithItems.test.ts b/desktop/src/lib/openWithItems.test.ts index 05756db1..9a65f19b 100644 --- a/desktop/src/lib/openWithItems.test.ts +++ b/desktop/src/lib/openWithItems.test.ts @@ -1,7 +1,52 @@ import { describe, expect, it, vi } from 'vitest' -import { buildOpenWithItems, type OpenWithContext, type OpenWithDeps } from './openWithItems' +import { buildOpenWithItems, describeFileType, type OpenWithContext, type OpenWithDeps } from './openWithItems' import type { OpenTarget } from '../stores/openTargetStore' +// ────────────────────────────────────────────────────────────────────────────── +// describeFileType tests +// ────────────────────────────────────────────────────────────────────────────── +describe('describeFileType', () => { + it('markdown → document icon, document categoryKey, uppercased ext', () => { + expect(describeFileType('a.md')).toEqual({ + icon: 'description', + categoryKey: 'openWith.fileType.document', + ext: 'MD', + }) + }) + + it('HTML (uppercase path) → web icon, web categoryKey', () => { + expect(describeFileType('x.HTML')).toEqual({ + icon: 'html', + categoryKey: 'openWith.fileType.web', + ext: 'HTML', + }) + }) + + it('png → image icon, image categoryKey', () => { + expect(describeFileType('y.png')).toEqual({ + icon: 'image', + categoryKey: 'openWith.fileType.image', + ext: 'PNG', + }) + }) + + it('tsx → code icon, code categoryKey', () => { + expect(describeFileType('z.tsx')).toEqual({ + icon: 'code', + categoryKey: 'openWith.fileType.code', + ext: 'TSX', + }) + }) + + it('unknown extension → generic file icon, file categoryKey', () => { + expect(describeFileType('w.bin')).toEqual({ + icon: 'insert_drive_file', + categoryKey: 'openWith.fileType.file', + ext: 'BIN', + }) + }) +}) + function makeT() { return (key: string, vars?: Record) => vars?.target != null ? `${key}:${vars.target}` : key diff --git a/desktop/src/lib/openWithItems.ts b/desktop/src/lib/openWithItems.ts index b749a473..3c6fe17b 100644 --- a/desktop/src/lib/openWithItems.ts +++ b/desktop/src/lib/openWithItems.ts @@ -1,5 +1,26 @@ import type { OpenTarget } from '../stores/openTargetStore' +// ─── File-type description ──────────────────────────────────────────────────── + +export type FileTypeInfo = { icon: string; categoryKey: string; ext: string } + +const FILE_TYPE_RULES: Array<{ re: RegExp; key: string; icon: string }> = [ + { re: /\.(md|markdown|txt|rst)$/i, key: 'document', icon: 'description' }, + { re: /\.(html?|xhtml)$/i, key: 'web', icon: 'html' }, + { re: /\.(png|jpe?g|gif|svg|webp|avif|bmp|ico)$/i, key: 'image', icon: 'image' }, + { re: /\.(ts|tsx|js|jsx|mjs|cjs|json|css|scss|less|py|rs|go|java|rb|php|c|cc|cpp|h|hpp|sh|ya?ml|toml)$/i, key: 'code', icon: 'code' }, +] + +export function describeFileType(path: string): FileTypeInfo { + const ext = (path.split('.').pop() ?? '').toUpperCase() + for (const rule of FILE_TYPE_RULES) { + if (rule.re.test(path)) return { icon: rule.icon, categoryKey: `openWith.fileType.${rule.key}`, ext } + } + return { icon: 'insert_drive_file', categoryKey: 'openWith.fileType.file', ext } +} + +// ─── Open-with items ────────────────────────────────────────────────────────── + export type OpenWithIcon = 'in-app-browser' | 'system' | 'ide' | 'file-manager' | 'preview' export type OpenWithItem = {