From ac0cb4d0a01046b52c3d4442b1917f31a0c1d7d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Sat, 30 May 2026 17:44:38 +0800 Subject: [PATCH] feat(desktop): only previewable change rows (md/html/image) get the open-with pill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A turn can touch many files; an open-with affordance on every row is noise. Restrict the per-file 打开方式 pill to rendered-previewable types via a new isPreviewableChangedFile() predicate. Source rows keep their inline diff toggle. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../chat/CurrentTurnChangeCard.test.tsx | 14 ++++++++++- .../components/chat/CurrentTurnChangeCard.tsx | 23 +++++++++++-------- desktop/src/lib/openWithItems.test.ts | 22 +++++++++++++++++- desktop/src/lib/openWithItems.ts | 12 ++++++++++ 4 files changed, 59 insertions(+), 12 deletions(-) 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'