+
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 = {