mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
feat(desktop): only previewable change rows (md/html/image) get the open-with pill
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) <noreply@anthropic.com>
This commit is contained in:
parent
78baace37e
commit
ac0cb4d0a0
@ -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' })
|
||||
|
||||
@ -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 (
|
||||
<div key={fileEntry.apiPath}>
|
||||
<div className="flex items-center gap-2">
|
||||
@ -196,15 +197,17 @@ export function CurrentTurnChangeCard({
|
||||
</span>
|
||||
<span className="material-symbols-outlined shrink-0 text-[18px] text-[var(--color-text-tertiary)]">{isExpanded ? 'keyboard_arrow_down' : 'chevron_right'}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('openWith.title')}
|
||||
onClick={(event) => handleOpenWith(event, fileEntry)}
|
||||
className="mr-2 inline-flex h-8 shrink-0 items-center gap-1 rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-surface)] px-2.5 text-xs font-medium text-[var(--color-text-secondary)] transition-colors hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-primary)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-brand)]/35"
|
||||
>
|
||||
{t('openWith.title')}
|
||||
<ChevronDown size={14} strokeWidth={1.9} />
|
||||
</button>
|
||||
{previewable && (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('openWith.title')}
|
||||
onClick={(event) => handleOpenWith(event, fileEntry)}
|
||||
className="mr-2 inline-flex h-8 shrink-0 items-center gap-1 rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-surface)] px-2.5 text-xs font-medium text-[var(--color-text-secondary)] transition-colors hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-primary)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-brand)]/35"
|
||||
>
|
||||
{t('openWith.title')}
|
||||
<ChevronDown size={14} strokeWidth={1.9} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
|
||||
@ -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<string, string>) =>
|
||||
vars?.target != null ? `${key}:${vars.target}` : key
|
||||
|
||||
@ -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'
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user