From 008ad62d3fe92ef8aaaf63392c8fa8faed509ffb Mon Sep 17 00:00:00 2001
From: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Date: Thu, 9 Jul 2026 21:57:38 -0700
Subject: [PATCH] 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
---
.../components/editor/EditorPanelHeader.tsx | 50 +++++++-------
.../editor/editor-panel-render-model.test.ts | 67 ++++++++++++++++++-
.../editor/editor-panel-render-model.ts | 18 +++--
3 files changed, 103 insertions(+), 32 deletions(-)
diff --git a/src/renderer/src/components/editor/EditorPanelHeader.tsx b/src/renderer/src/components/editor/EditorPanelHeader.tsx
index f47c9190c..23ce65305 100644
--- a/src/renderer/src/components/editor/EditorPanelHeader.tsx
+++ b/src/renderer/src/components/editor/EditorPanelHeader.tsx
@@ -100,6 +100,31 @@ export function EditorPanelHeader({
onOpenMarkdownPreview={onOpenMarkdownPreview}
onOpenContainingFolder={onOpenContainingFolder}
/>
+ {canOpenPreviewToSide && (
+
+
+
+
+
+
+ {translate(
+ 'auto.components.editor.EditorPanelHeader.fb8331694e',
+ 'Open Preview to the Side'
+ )}
+
+
+
+ )}
{isSingleDiff && (
@@ -149,31 +174,6 @@ export function EditorPanelHeader({
iconClassName="size-3"
/>
)}
- {canOpenPreviewToSide && (
-
-
-
-
-
-
- {translate(
- 'auto.components.editor.EditorPanelHeader.fb8331694e',
- 'Open Preview to the Side'
- )}
-
-
-
- )}
{isDiffSurface && (
diff --git a/src/renderer/src/components/editor/editor-panel-render-model.test.ts b/src/renderer/src/components/editor/editor-panel-render-model.test.ts
index 374e870e4..88e3a539e 100644
--- a/src/renderer/src/components/editor/editor-panel-render-model.test.ts
+++ b/src/renderer/src/components/editor/editor-panel-render-model.test.ts
@@ -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
markdownViewMode?: Record
isChangesMode?: boolean
+ gitStatusByWorktree?: Record
}) {
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 {
+ 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),
+ 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),
+ 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),
+ fileContents: {}
+ })
+
+ expect(model.canOpenPreviewToSide).toBe(false)
+ })
+
+ it('disables preview for non-HTML diffs', () => {
+ const model = renderModel({
+ activeFile: markdownFile({ mode: 'diff', diffSource: 'unstaged' } as Partial),
+ fileContents: {}
+ })
+
+ expect(model.canOpenPreviewToSide).toBe(false)
+ })
+})
+
describe('getEditorPanelRenderModel markdown export affordance', () => {
it('enables export for rendered markdown edit tabs', () => {
expect(renderModel({}).canExportMarkdownToPdf).toBe(true)
diff --git a/src/renderer/src/components/editor/editor-panel-render-model.ts b/src/renderer/src/components/editor/editor-panel-render-model.ts
index be9738db6..c471b2e73 100644
--- a/src/renderer/src/components/editor/editor-panel-render-model.ts
+++ b/src/renderer/src/components/editor/editor-panel-render-model.ts
@@ -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,