Add HTML preview-to-side support for single unstaged/staged diffs (#8018)

- Move the preview button before the single-diff tooltip in the header
- Extend canOpenPreviewToSide to allow single diffs (not commit diffs)
  when the modified file still exists on disk, since the preview
  renders the working-tree file rather than diff content
- Add tests covering HTML edit tabs, unstaged diffs, deleted files,
  commit diffs, and non-HTML diffs
This commit is contained in:
Jinjing 2026-07-09 21:57:38 -07:00 committed by GitHub
parent 79abeeaa2a
commit 008ad62d3f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 103 additions and 32 deletions

View File

@ -100,6 +100,31 @@ export function EditorPanelHeader({
onOpenMarkdownPreview={onOpenMarkdownPreview}
onOpenContainingFolder={onOpenContainingFolder}
/>
{canOpenPreviewToSide && (
<TooltipProvider delayDuration={300}>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
className="p-1 rounded hover:bg-accent text-muted-foreground hover:text-foreground transition-colors flex-shrink-0"
onClick={onOpenPreviewToSide}
aria-label={translate(
'auto.components.editor.EditorPanelHeader.fb8331694e',
'Open Preview to the Side'
)}
>
<Eye size={14} />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}>
{translate(
'auto.components.editor.EditorPanelHeader.fb8331694e',
'Open Preview to the Side'
)}
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
{isSingleDiff && (
<TooltipProvider delayDuration={300}>
<Tooltip>
@ -149,31 +174,6 @@ export function EditorPanelHeader({
iconClassName="size-3"
/>
)}
{canOpenPreviewToSide && (
<TooltipProvider delayDuration={300}>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
className="p-1 rounded hover:bg-accent text-muted-foreground hover:text-foreground transition-colors flex-shrink-0"
onClick={onOpenPreviewToSide}
aria-label={translate(
'auto.components.editor.EditorPanelHeader.fb8331694e',
'Open Preview to the Side'
)}
>
<Eye size={14} />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}>
{translate(
'auto.components.editor.EditorPanelHeader.fb8331694e',
'Open Preview to the Side'
)}
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
{isDiffSurface && (
<TooltipProvider delayDuration={300}>
<Tooltip>

View File

@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest'
import type { OpenFile } from '@/store/slices/editor'
import { RICH_MARKDOWN_MAX_SIZE_BYTES } from '../../../../shared/constants'
import type { GitStatusEntry } from '../../../../shared/types'
import type { FileContent } from './editor-panel-content-types'
import { getEditorPanelRenderModel } from './editor-panel-render-model'
@ -31,18 +32,82 @@ function renderModel(args: {
editorDrafts?: Record<string, string>
markdownViewMode?: Record<string, 'source' | 'rich' | 'preview'>
isChangesMode?: boolean
gitStatusByWorktree?: Record<string, GitStatusEntry[]>
}) {
return getEditorPanelRenderModel({
activeFile: args.activeFile ?? markdownFile(),
fileContents: args.fileContents ?? { '/repo/README.md': textContent() },
editorDrafts: args.editorDrafts ?? {},
gitStatusByWorktree: {},
gitStatusByWorktree: args.gitStatusByWorktree ?? {},
gitBranchChangesByWorktree: {},
markdownViewMode: args.markdownViewMode ?? {},
isChangesMode: args.isChangesMode ?? false
})
}
function htmlFile(overrides: Partial<OpenFile> = {}): OpenFile {
return {
id: '/repo/mock.html',
filePath: '/repo/mock.html',
relativePath: 'mock.html',
worktreeId: 'wt-1',
language: 'html',
mode: 'edit',
isDirty: false,
...overrides
}
}
describe('getEditorPanelRenderModel HTML preview affordance', () => {
it('enables preview for HTML edit tabs', () => {
expect(renderModel({ activeFile: htmlFile(), fileContents: {} }).canOpenPreviewToSide).toBe(
true
)
})
it('enables preview for single HTML diffs whose file exists on disk', () => {
const model = renderModel({
activeFile: htmlFile({ mode: 'diff', diffSource: 'unstaged' } as Partial<OpenFile>),
fileContents: {},
gitStatusByWorktree: {
'wt-1': [{ path: 'mock.html', status: 'modified', area: 'unstaged' }]
}
})
expect(model.canOpenPreviewToSide).toBe(true)
})
it('disables preview for diffs of deleted HTML files', () => {
const model = renderModel({
activeFile: htmlFile({ mode: 'diff', diffSource: 'unstaged' } as Partial<OpenFile>),
fileContents: {},
gitStatusByWorktree: {
'wt-1': [{ path: 'mock.html', status: 'deleted', area: 'unstaged' }]
}
})
expect(model.canOpenPreviewToSide).toBe(false)
})
it('disables preview for commit diffs whose content may not match disk', () => {
const model = renderModel({
activeFile: htmlFile({ mode: 'diff', diffSource: 'commit' } as Partial<OpenFile>),
fileContents: {}
})
expect(model.canOpenPreviewToSide).toBe(false)
})
it('disables preview for non-HTML diffs', () => {
const model = renderModel({
activeFile: markdownFile({ mode: 'diff', diffSource: 'unstaged' } as Partial<OpenFile>),
fileContents: {}
})
expect(model.canOpenPreviewToSide).toBe(false)
})
})
describe('getEditorPanelRenderModel markdown export affordance', () => {
it('enables export for rendered markdown edit tabs', () => {
expect(renderModel({}).canExportMarkdownToPdf).toBe(true)

View File

@ -71,6 +71,11 @@ export function getEditorPanelRenderModel({
activeFile.mode === 'diff' && activeFile.diffSource === 'branch'
? (branchEntries.find((entry) => entry.path === activeFile.relativePath) ?? null)
: null
const openFileState = getEditorHeaderOpenFileState(
activeFile,
matchingWorktreeEntry,
matchingBranchEntry
)
const markdownViewModes = getMarkdownViewModes({
language: resolvedLanguage,
mode: activeFile.mode,
@ -141,16 +146,17 @@ export function getEditorPanelRenderModel({
isCombinedDiff,
worktreeEntries,
resolvedLanguage,
openFileState: getEditorHeaderOpenFileState(
activeFile,
matchingWorktreeEntry,
matchingBranchEntry
),
openFileState,
isMarkdown: resolvedLanguage === 'markdown',
isMermaid: resolvedLanguage === 'mermaid',
isCsv: resolvedLanguage === 'csv' || resolvedLanguage === 'tsv',
isNotebook: resolvedLanguage === 'notebook',
canOpenPreviewToSide: activeFile.mode === 'edit' && canPreviewLanguage(resolvedLanguage),
// Why: the preview renders the on-disk file, so diff surfaces only get it
// when the modified side still exists on disk (canOpen excludes deleted
// files and commit diffs whose content may not match the working tree).
canOpenPreviewToSide:
canPreviewLanguage(resolvedLanguage) &&
(activeFile.mode === 'edit' || (isSingleDiff && openFileState.canOpen)),
mdViewMode,
hasViewModeToggle,
availableEditorToggleModes,