diff --git a/desktop/src/components/chat/CurrentTurnChangeCard.test.tsx b/desktop/src/components/chat/CurrentTurnChangeCard.test.tsx
index bea56635..c1526f77 100644
--- a/desktop/src/components/chat/CurrentTurnChangeCard.test.tsx
+++ b/desktop/src/components/chat/CurrentTurnChangeCard.test.tsx
@@ -171,13 +171,25 @@ describe('CurrentTurnChangeCard – open-with buttons', () => {
openPreviewSpy.mockResolvedValue(undefined)
})
- it('renders one "open-with" button per file', () => {
+ it('renders an "open-with" button for each previewable file', () => {
renderCard(['/w/proj/README.md', '/w/proj/index.html'])
// aria-label is the i18n key itself (identity mock)
const buttons = screen.getAllByRole('button', { name: 'openWith.title' })
expect(buttons).toHaveLength(2)
})
+ it('does NOT render an "open-with" button for a source file (diff toggle stays)', () => {
+ renderCard(['/w/proj/src/main.ts'])
+ expect(screen.queryAllByRole('button', { name: 'openWith.title' })).toHaveLength(0)
+ // source files keep their inline diff toggle — only the open-with pill is dropped
+ expect(screen.getByRole('button', { name: /turnChangesShowDiffAria/ })).toBeInTheDocument()
+ })
+
+ it('mixed turn: only previewable rows (md/html) get the open-with button, not .ts', () => {
+ renderCard(['/w/proj/README.md', '/w/proj/src/main.ts', '/w/proj/index.html'])
+ expect(screen.getAllByRole('button', { name: 'openWith.title' })).toHaveLength(2)
+ })
+
it('clicking README.md open-with opens menu with workspace preview item', async () => {
renderCard(['/w/proj/README.md'])
const [openWithBtn] = screen.getAllByRole('button', { name: 'openWith.title' })
diff --git a/desktop/src/components/chat/CurrentTurnChangeCard.tsx b/desktop/src/components/chat/CurrentTurnChangeCard.tsx
index 5b5bb71b..64f403e0 100644
--- a/desktop/src/components/chat/CurrentTurnChangeCard.tsx
+++ b/desktop/src/components/chat/CurrentTurnChangeCard.tsx
@@ -5,7 +5,7 @@ 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, describeFileType, type OpenWithItem } from '../../lib/openWithItems'
+import { buildOpenWithItems, describeFileType, isPreviewableChangedFile, type OpenWithItem } from '../../lib/openWithItems'
import { openWithContextForWorkspaceFile } from '../../lib/openWithContextForHref'
import { getServerBaseUrl } from '../../lib/desktopRuntime'
import { useOpenTargetStore } from '../../stores/openTargetStore'
@@ -176,6 +176,7 @@ export function CurrentTurnChangeCard({
const diffState = diffByPath[fileEntry.apiPath]
const fileName = fileEntry.displayPath.split('/').pop() || fileEntry.displayPath
const typeInfo = describeFileType(fileEntry.displayPath)
+ const previewable = isPreviewableChangedFile(fileEntry.displayPath)
return (
@@ -196,15 +197,17 @@ export function CurrentTurnChangeCard({
{isExpanded ? 'keyboard_arrow_down' : 'chevron_right'}
-
+ {previewable && (
+
+ )}
{isExpanded && (
diff --git a/desktop/src/lib/openWithItems.test.ts b/desktop/src/lib/openWithItems.test.ts
index 9a65f19b..f7cd6990 100644
--- a/desktop/src/lib/openWithItems.test.ts
+++ b/desktop/src/lib/openWithItems.test.ts
@@ -1,5 +1,5 @@
import { describe, expect, it, vi } from 'vitest'
-import { buildOpenWithItems, describeFileType, type OpenWithContext, type OpenWithDeps } from './openWithItems'
+import { buildOpenWithItems, describeFileType, isPreviewableChangedFile, type OpenWithContext, type OpenWithDeps } from './openWithItems'
import type { OpenTarget } from '../stores/openTargetStore'
// ──────────────────────────────────────────────────────────────────────────────
@@ -47,6 +47,26 @@ describe('describeFileType', () => {
})
})
+// ──────────────────────────────────────────────────────────────────────────────
+// isPreviewableChangedFile tests — only md/html/image get the open-with affordance
+// ──────────────────────────────────────────────────────────────────────────────
+describe('isPreviewableChangedFile', () => {
+ it.each([
+ 'a.md', 'a.markdown', 'x.html', 'x.htm', 'X.HTML',
+ 'y.png', 'y.JPG', 'z.jpeg', 'g.gif', 'w.webp', 'v.svg',
+ 'docs/sub/readme.md',
+ ])('previewable: %s → true', (p) => {
+ expect(isPreviewableChangedFile(p)).toBe(true)
+ })
+
+ it.each([
+ 'main.ts', 'main.tsx', 'data.json', 'style.css', 'notes.txt',
+ 'lib.rs', 'Makefile', 'archive.zip', 'no-ext', 'a.mdx',
+ ])('non-previewable: %s → false', (p) => {
+ expect(isPreviewableChangedFile(p)).toBe(false)
+ })
+})
+
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 3c6fe17b..ecca7989 100644
--- a/desktop/src/lib/openWithItems.ts
+++ b/desktop/src/lib/openWithItems.ts
@@ -19,6 +19,18 @@ export function describeFileType(path: string): FileTypeInfo {
return { icon: 'insert_drive_file', categoryKey: 'openWith.fileType.file', ext }
}
+const PREVIEWABLE_CHANGED_FILE_RE = /\.(md|markdown|html?|png|jpe?g|gif|webp|svg)$/i
+
+/**
+ * True only for changed-file types with a meaningful *rendered* preview
+ * (markdown / html / image). Source files (.ts/.json/.css …) return false.
+ * Used to decide which change-card rows get the "open with" affordance —
+ * we don't want an open-with pill on every file when a turn touches many.
+ */
+export function isPreviewableChangedFile(path: string): boolean {
+ return PREVIEWABLE_CHANGED_FILE_RE.test(path)
+}
+
// ─── Open-with items ──────────────────────────────────────────────────────────
export type OpenWithIcon = 'in-app-browser' | 'system' | 'ide' | 'file-manager' | 'preview'