fix: address review findings (#2311)

This commit is contained in:
Jinjing 2026-05-18 23:41:02 -07:00 committed by GitHub
parent 789060c239
commit 833a707182
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 487 additions and 101 deletions

View File

@ -2,7 +2,12 @@ import { renderToStaticMarkup } from 'react-dom/server'
import { describe, expect, it, vi } from 'vitest'
import type { OpenFile } from '@/store/slices/editor'
import type { GitStatusEntry } from '../../../../shared/types'
import { ConflictReviewPanel } from './ConflictComponents'
import { TooltipProvider } from '@/components/ui/tooltip'
import {
ConflictBanner,
ConflictReviewPanel,
getNextConflictNavigationIndex
} from './ConflictComponents'
function createConflictReviewFile(overrides: Partial<OpenFile> = {}): OpenFile {
return {
@ -46,7 +51,35 @@ function createLiveEntry(
}
describe('ConflictReviewPanel', () => {
it('renders unresolved conflicts as a left file tree', () => {
it('renders unresolved conflicts as a left tree with supplied default content', () => {
const file = createConflictReviewFile()
const html = renderToStaticMarkup(
<ConflictReviewPanel
file={file}
liveEntries={[
createLiveEntry('src/renderer/src/store/slices/linear.test.ts', 'added'),
createLiveEntry('src/renderer/src/store/slices/linear.ts')
]}
onOpenEntry={vi.fn()}
selectedFile={null}
selectedContent={<div data-conflict-review-default-content="true">file contents</div>}
onDismiss={vi.fn()}
onRefreshSnapshot={vi.fn()}
onReturnToSourceControl={vi.fn()}
/>
)
expect(html).toContain('Files')
expect(html).toContain('Collapse file tree')
expect(html).toContain('src/renderer/src/store/slices')
expect(html).toContain('linear.test.ts')
expect(html).toContain('linear.ts')
expect(html).toContain('data-conflict-review-default-content="true"')
expect(html).toContain('file contents')
expect(html).not.toContain('Select a conflict from the file tree')
})
it('renders a lightweight overview when no conflict file is selected', () => {
const file = createConflictReviewFile()
const html = renderToStaticMarkup(
<ConflictReviewPanel
@ -64,12 +97,91 @@ describe('ConflictReviewPanel', () => {
/>
)
expect(html).toContain('Files')
expect(html).toContain('Collapse file tree')
expect(html).toContain('src/renderer/src/store/slices')
expect(html).toContain('linear.test.ts')
expect(html).toContain('linear.ts')
expect(html).toContain('Select a conflict from the file tree')
expect(html).not.toContain('Choose which version to keep, or combine them')
expect(html).toContain('Conflicts')
expect(html).toContain('Open a file to resolve markers in the editor.')
expect(html).toContain('Choose which version to keep, or combine them')
expect(html).toContain('Resolve the conflict markers')
expect(html).not.toContain('Loading conflict contents')
})
it('keeps selected conflict content in a flex-height pane', () => {
const file = createConflictReviewFile()
const html = renderToStaticMarkup(
<ConflictReviewPanel
file={file}
liveEntries={[
createLiveEntry('src/renderer/src/store/slices/linear.test.ts', 'added'),
createLiveEntry('src/renderer/src/store/slices/linear.ts')
]}
onOpenEntry={vi.fn()}
selectedFile={{
id: '/repo/src/renderer/src/store/slices/linear.ts',
filePath: '/repo/src/renderer/src/store/slices/linear.ts',
relativePath: 'src/renderer/src/store/slices/linear.ts',
worktreeId: 'repo::/repo',
language: 'typescript',
isDirty: false,
mode: 'edit'
}}
selectedContent={<div data-selected-conflict-content="true" className="flex-1" />}
onDismiss={vi.fn()}
onRefreshSnapshot={vi.fn()}
onReturnToSourceControl={vi.fn()}
/>
)
expect(html).toContain(
'class="flex min-h-0 flex-1 flex-col"><div data-selected-conflict-content="true"'
)
})
})
describe('ConflictBanner', () => {
it('renders conflict navigation controls when conflict markers are available', () => {
const file = createConflictReviewFile({
mode: 'edit',
conflict: {
kind: 'conflict-editable',
conflictKind: 'both_modified',
conflictStatus: 'unresolved',
conflictStatusSource: 'git'
}
})
const html = renderToStaticMarkup(
<TooltipProvider>
<ConflictBanner
file={file}
entry={null}
conflictNavigation={{
currentIndex: 1,
total: 3,
onJump: vi.fn()
}}
/>
</TooltipProvider>
)
expect(html).toContain('Unresolved conflict')
expect(html).toContain('2 / 3')
expect(html).toContain('Previous conflict')
expect(html).toContain('Next conflict')
})
})
describe('getNextConflictNavigationIndex', () => {
it('cycles through conflicts in both directions', () => {
expect(
getNextConflictNavigationIndex({ currentIndex: null, direction: 'next', total: 3 })
).toBe(0)
expect(getNextConflictNavigationIndex({ currentIndex: 2, direction: 'next', total: 3 })).toBe(0)
expect(
getNextConflictNavigationIndex({ currentIndex: 0, direction: 'previous', total: 3 })
).toBe(2)
expect(
getNextConflictNavigationIndex({ currentIndex: null, direction: 'previous', total: 3 })
).toBe(2)
expect(getNextConflictNavigationIndex({ currentIndex: 0, direction: 'next', total: 0 })).toBe(
null
)
})
})

View File

@ -1,7 +1,17 @@
import React from 'react'
import { CircleCheck, GitMerge, PanelLeftOpen, RefreshCw, TriangleAlert, X } from 'lucide-react'
import {
ChevronDown,
ChevronUp,
CircleCheck,
GitMerge,
PanelLeftOpen,
RefreshCw,
TriangleAlert,
X
} from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { getFileTypeIcon } from '@/lib/file-type-icons'
import { cn } from '@/lib/utils'
import type { ConflictReviewEntry, OpenFile } from '@/store/slices/editor'
import type { GitConflictKind, GitStatusEntry } from '../../../../shared/types'
@ -29,13 +39,41 @@ export const CONFLICT_HINT_MAP: Record<GitConflictKind, string> = {
const EMPTY_CONFLICT_REVIEW_ENTRIES: readonly ConflictReviewEntry[] = []
let conflictReviewFileTreeCollapsedPreference = false
type ConflictNavigationDirection = 'previous' | 'next'
type ConflictReviewPanelEntry = ConflictReviewEntry & {
liveEntry?: GitStatusEntry
}
export function getNextConflictNavigationIndex({
currentIndex,
direction,
total
}: {
currentIndex: number | null
direction: ConflictNavigationDirection
total: number
}): number | null {
if (total <= 0) {
return null
}
if (currentIndex === null || currentIndex < 0 || currentIndex >= total) {
return direction === 'previous' ? total - 1 : 0
}
return direction === 'previous' ? (currentIndex + total - 1) % total : (currentIndex + 1) % total
}
export function ConflictBanner({
file,
entry
entry,
conflictNavigation
}: {
file: OpenFile
entry: GitStatusEntry | null
conflictNavigation?: {
currentIndex: number | null
total: number
onJump: (direction: ConflictNavigationDirection) => void
}
}): React.JSX.Element | null {
const conflict = file.conflict
if (!conflict) {
@ -54,15 +92,58 @@ export function ConflictBanner({
: 'border-emerald-500/20 bg-emerald-500/5'
)}
>
<div className="flex items-center gap-2">
{isUnresolved ? (
<TriangleAlert className="size-3.5 shrink-0 text-destructive" />
) : (
<CircleCheck className="size-3.5 shrink-0 text-emerald-600 dark:text-emerald-400" />
<div className="flex items-center justify-between gap-3">
<div className="flex min-w-0 items-center gap-2">
{isUnresolved ? (
<TriangleAlert className="size-3.5 shrink-0 text-destructive" />
) : (
<CircleCheck className="size-3.5 shrink-0 text-emerald-600 dark:text-emerald-400" />
)}
<span className="min-w-0 truncate font-medium text-foreground">
{label} conflict · {CONFLICT_KIND_LABELS[conflict.conflictKind]}
</span>
{conflictNavigation && conflictNavigation.total > 0 && (
<span className="shrink-0 px-1 text-[11px] tabular-nums text-muted-foreground">
{(conflictNavigation.currentIndex ?? 0) + 1} / {conflictNavigation.total}
</span>
)}
</div>
{conflictNavigation && conflictNavigation.total > 0 && (
<div className="flex shrink-0 items-center gap-1">
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon-xs"
aria-label="Previous conflict"
onClick={() => conflictNavigation.onJump('previous')}
>
<ChevronUp className="size-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={6}>
Previous conflict
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon-xs"
aria-label="Next conflict"
onClick={() => conflictNavigation.onJump('next')}
>
<ChevronDown className="size-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={6}>
Next conflict
</TooltipContent>
</Tooltip>
</div>
)}
<span className="font-medium text-foreground">
{label} conflict · {CONFLICT_KIND_LABELS[conflict.conflictKind]}
</span>
</div>
{/* Why: the hint is omitted here because the file is already open in the
editor below. Showing "Open and edit…" or similar would be confusing
@ -104,6 +185,105 @@ export function ConflictPlaceholderView({ file }: { file: OpenFile }): React.JSX
)
}
function ConflictReviewOverview({
entries,
resolvedCount,
onOpenEntry,
onReturnToSourceControl,
onDismiss
}: {
entries: readonly ConflictReviewPanelEntry[]
resolvedCount: number
onOpenEntry: (entry: GitStatusEntry) => void
onReturnToSourceControl: () => void
onDismiss: () => void
}): React.JSX.Element {
return (
<div className="min-h-0 flex-1 overflow-y-auto scrollbar-sleek">
<div className="mx-auto flex w-full max-w-4xl flex-col gap-4 px-5 py-4">
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="min-w-0">
<div className="text-sm font-medium text-foreground">Conflicts</div>
<div className="mt-1 text-xs text-muted-foreground">
Open a file to resolve markers in the editor.
</div>
</div>
<div className="flex shrink-0 items-center gap-2">
<Button type="button" size="sm" variant="outline" onClick={onReturnToSourceControl}>
<GitMerge className="size-3.5" />
Source Control
</Button>
<Button type="button" size="sm" variant="ghost" onClick={onDismiss}>
<X className="size-3.5" />
Dismiss
</Button>
</div>
</div>
{resolvedCount > 0 && (
<div className="rounded-md border border-border bg-muted/30 px-3 py-2 text-xs text-muted-foreground">
{resolvedCount} conflict{resolvedCount === 1 ? '' : 's'} no longer live in Git.
</div>
)}
<div className="overflow-hidden rounded-md border border-border">
{entries.map((entry) => {
const FileIcon = getFileTypeIcon(entry.path)
const liveEntry = entry.liveEntry
const isUnresolved = liveEntry?.conflictStatus === 'unresolved'
const statusLabel = isUnresolved ? 'Unresolved' : liveEntry ? 'Resolved' : 'Gone'
return (
<button
key={entry.path}
type="button"
className={cn(
'group flex w-full min-w-0 items-start gap-3 border-b border-border px-3 py-3 text-left transition-colors last:border-b-0 hover:bg-accent/40 disabled:cursor-default disabled:hover:bg-transparent',
!liveEntry && 'opacity-65'
)}
disabled={!liveEntry}
onClick={() => {
if (liveEntry) {
onOpenEntry(liveEntry)
}
}}
>
<FileIcon
className={cn('mt-0.5 size-4 shrink-0', isUnresolved && 'text-destructive')}
/>
<div className="min-w-0 flex-1 space-y-1">
<div className="flex min-w-0 flex-wrap items-center gap-2">
<span className="min-w-0 break-all font-mono text-xs text-foreground">
{entry.path}
</span>
<span
className={cn(
'shrink-0 rounded-full px-1.5 py-0.5 text-[10px] font-semibold',
isUnresolved
? 'bg-destructive/12 text-destructive'
: 'bg-muted text-muted-foreground'
)}
>
{statusLabel}
</span>
</div>
<div className="text-xs text-muted-foreground">
{CONFLICT_KIND_LABELS[entry.conflictKind]} ·{' '}
{CONFLICT_HINT_MAP[entry.conflictKind]}
</div>
{liveEntry?.oldPath && (
<div className="text-xs text-muted-foreground">
Renamed from {liveEntry.oldPath}
</div>
)}
</div>
</button>
)
})}
</div>
</div>
</div>
)
}
export function ConflictReviewPanel({
file,
liveEntries,
@ -131,7 +311,7 @@ export function ConflictReviewPanel({
() => new Map(liveEntries.map((entry) => [entry.path, entry])),
[liveEntries]
)
const treeEntries = React.useMemo(
const treeEntries = React.useMemo<readonly ConflictReviewPanelEntry[]>(
() =>
snapshotEntries.map((entry) => ({
...entry,
@ -219,41 +399,15 @@ export function ConflictReviewPanel({
Refresh
</Button>
</div>
<div className="min-h-0 flex-1">
<div className="flex min-h-0 flex-1 flex-col">
{selectedContent ?? (
<div className="flex h-full min-h-0 items-center justify-center px-6 text-center">
<div className="max-w-md space-y-3">
<div className="mx-auto flex size-10 items-center justify-center rounded-md border border-border bg-card text-muted-foreground">
<GitMerge className="size-4" />
</div>
<div className="text-sm font-medium text-foreground">
Select a conflict from the file tree
</div>
<div className="text-xs text-muted-foreground">
Open each unresolved file, edit the conflict markers, then refresh this snapshot.
</div>
{resolvedCount > 0 && (
<div className="text-xs text-muted-foreground">
{resolvedCount} conflict{resolvedCount === 1 ? '' : 's'} no longer live in Git.
</div>
)}
<div className="flex items-center justify-center gap-2">
<Button
type="button"
size="sm"
variant="outline"
onClick={onReturnToSourceControl}
>
<GitMerge className="size-3.5" />
Source Control
</Button>
<Button type="button" size="sm" variant="ghost" onClick={onDismiss}>
<X className="size-3.5" />
Dismiss
</Button>
</div>
</div>
</div>
<ConflictReviewOverview
entries={treeEntries}
resolvedCount={resolvedCount}
onOpenEntry={onOpenEntry}
onReturnToSourceControl={onReturnToSourceControl}
onDismiss={onDismiss}
/>
)}
</div>
</div>

View File

@ -10,7 +10,12 @@ import { detectLanguage } from '@/lib/language-detect'
import { useAppStore } from '@/store'
import { Button } from '@/components/ui/button'
import { ChangesModeView } from './ChangesModeView'
import { ConflictBanner, ConflictPlaceholderView, ConflictReviewPanel } from './ConflictComponents'
import {
ConflictBanner,
ConflictPlaceholderView,
ConflictReviewPanel,
getNextConflictNavigationIndex
} from './ConflictComponents'
import type { MarkdownViewMode, OpenFile } from '@/store/slices/editor'
import type { GitStatusEntry, GitDiffResult } from '../../../../shared/types'
import { RICH_MARKDOWN_MAX_SIZE_BYTES } from '../../../../shared/constants'
@ -19,6 +24,7 @@ import { getMarkdownRichModeUnsupportedMessage } from './markdown-rich-mode'
import { extractFrontMatter, prependFrontMatter } from './markdown-frontmatter'
import { RichMarkdownErrorBoundary } from './RichMarkdownErrorBoundary'
import { useMarkdownDocuments } from './useMarkdownDocuments'
import { findGitConflictBlocks } from './monaco-conflict-decorations'
const MonacoEditor = lazy(() => import('./MonacoEditor'))
const DiffViewer = lazy(() => import('./DiffViewer'))
@ -146,6 +152,10 @@ export function EditorContent({
const openConflictReview = useAppStore((s) => s.openConflictReview)
const closeFile = useAppStore((s) => s.closeFile)
const setRightSidebarTab = useAppStore((s) => s.setRightSidebarTab)
const setPendingEditorReveal = useAppStore((s) => s.setPendingEditorReveal)
const [conflictNavigationIndexByFile, setConflictNavigationIndexByFile] = React.useState<
Record<string, number>
>({})
const md = useMarkdownDocuments(activeFile, isMarkdown, mdViewMode, handleSave)
const activeConflictEntry =
worktreeEntries.find((entry) => entry.path === activeFile.relativePath) ?? null
@ -160,6 +170,68 @@ export function EditorContent({
activeFile.diffSource === 'combined-branch' ||
activeFile.diffSource === 'combined-commit')
const getConflictNavigation = React.useCallback(
(file: OpenFile, content: string) => {
const blocks = findGitConflictBlocks(content)
if (blocks.length === 0) {
return undefined
}
const currentIndex = conflictNavigationIndexByFile[file.id] ?? null
return {
currentIndex,
total: blocks.length,
onJump: (direction: 'previous' | 'next') => {
const nextIndex = getNextConflictNavigationIndex({
currentIndex,
direction,
total: blocks.length
})
if (nextIndex === null) {
return
}
const line = blocks[nextIndex].startLine
const markerLine = content.split(/\r?\n/)[line - 1] ?? ''
setConflictNavigationIndexByFile((prev) => ({ ...prev, [file.id]: nextIndex }))
// Why: a same-location reveal can be requested twice before Monaco
// consumes the first one. Clearing first guarantees the prop changes
// and the mounted editor runs its reveal effect again.
setPendingEditorReveal(null)
queueMicrotask(() => {
setPendingEditorReveal({
filePath: file.filePath,
line,
column: 1,
matchLength: markerLine.length
})
})
}
}
},
[conflictNavigationIndexByFile, setPendingEditorReveal]
)
const openConflictEntry = React.useCallback(
(entry: GitStatusEntry) => {
if (activeFile.mode !== 'conflict-review') {
return
}
openConflictReviewFile(
activeFile.id,
activeFile.worktreeId,
activeFile.filePath,
entry,
detectLanguage(entry.path)
)
},
[
activeFile.filePath,
activeFile.id,
activeFile.mode,
activeFile.worktreeId,
openConflictReviewFile
]
)
const renderMonacoEditor = (fc: FileContent): React.JSX.Element => (
// Why: Without a key, React reuses the same MonacoEditor instance when
// switching tabs or split panes, just updating props. That means
@ -324,77 +396,105 @@ export function EditorContent({
return <div className="h-full min-h-0">{renderMonacoEditor(fc)}</div>
}
const renderConflictReviewSelectedContent = (selectedFile: OpenFile): React.JSX.Element => {
if (selectedFile.conflict?.kind === 'conflict-placeholder') {
return <ConflictPlaceholderView file={selectedFile} />
const renderConflictReviewEditorContent = ({
contentFile,
entry,
className,
viewStateKeySuffix
}: {
contentFile: OpenFile
entry: GitStatusEntry | null
className: string
viewStateKeySuffix: string
}): React.JSX.Element => {
if (contentFile.conflict?.kind === 'conflict-placeholder') {
return (
<div className={className}>
<ConflictPlaceholderView file={contentFile} />
</div>
)
}
const fc = fileContents[selectedFile.id]
const fc = fileContents[contentFile.id]
if (!fc) {
return (
<div className="flex items-center justify-center h-full text-muted-foreground text-sm">
Loading...
<div className={className}>
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
Loading...
</div>
</div>
)
}
if (fc.loadError) {
return (
<FileLoadErrorView message={fc.loadError} onRetry={() => reloadFileContent(selectedFile)} />
<div className={className}>
<FileLoadErrorView
message={fc.loadError}
onRetry={() => reloadFileContent(contentFile)}
/>
</div>
)
}
if (fc.isBinary) {
if (fc.isImage) {
return (
<ImageViewer
content={fc.content}
filePath={selectedFile.filePath}
mimeType={fc.mimeType}
/>
<div className={className}>
<ImageViewer
content={fc.content}
filePath={contentFile.filePath}
mimeType={fc.mimeType}
/>
</div>
)
}
return (
<div className="flex items-center justify-center h-full text-muted-foreground text-sm">
Binary file cannot display
<div className={className}>
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
Binary file cannot display
</div>
</div>
)
}
const selectedConflictEntry =
worktreeEntries.find((entry) => entry.path === selectedFile.relativePath) ?? null
const selectedLanguage = detectLanguage(selectedFile.relativePath)
const selectedLanguage = detectLanguage(contentFile.relativePath)
const monacoSelectedLanguage = selectedLanguage === 'notebook' ? 'json' : selectedLanguage
const selectedViewStateKey = `${selectedFile.filePath}::${viewStateScopeId}`
const selectedViewStateKey = `${contentFile.filePath}::${viewStateScopeId}:${viewStateKeySuffix}`
const selectedContent = editBuffers[contentFile.id] ?? fc.content
return (
<div className="flex min-h-0 flex-1 flex-col">
{selectedFile.conflict && (
<ConflictBanner file={selectedFile} entry={selectedConflictEntry} />
<div className={className}>
{contentFile.conflict && (
<ConflictBanner
file={contentFile}
entry={entry}
conflictNavigation={getConflictNavigation(contentFile, selectedContent)}
/>
)}
<div className="min-h-0 flex-1">
<MonacoEditor
key={`${viewStateScopeId}:${selectedFile.id}`}
filePath={selectedFile.filePath}
key={`${viewStateScopeId}:${contentFile.id}:${viewStateKeySuffix}`}
filePath={contentFile.filePath}
viewStateKey={selectedViewStateKey}
relativePath={selectedFile.relativePath}
content={editBuffers[selectedFile.id] ?? fc.content}
relativePath={contentFile.relativePath}
content={selectedContent}
language={monacoSelectedLanguage}
onContentChange={(content) => handleContentChangeForFile(selectedFile, content)}
onSave={(content) => handleSaveForFile(selectedFile, content)}
worktreeId={selectedFile.worktreeId}
onContentChange={(content) => handleContentChangeForFile(contentFile, content)}
onSave={(content) => handleSaveForFile(contentFile, content)}
worktreeId={contentFile.worktreeId}
markdownAnnotationsEnabled={false}
conflictDecorationsEnabled={selectedFile.conflict?.conflictStatus === 'unresolved'}
conflictDecorationsEnabled={contentFile.conflict?.conflictStatus === 'unresolved'}
revealLine={
pendingEditorReveal?.filePath === selectedFile.filePath
pendingEditorReveal?.filePath === contentFile.filePath
? pendingEditorReveal.line
: undefined
}
revealColumn={
pendingEditorReveal?.filePath === selectedFile.filePath
pendingEditorReveal?.filePath === contentFile.filePath
? pendingEditorReveal.column
: undefined
}
revealMatchLength={
pendingEditorReveal?.filePath === selectedFile.filePath
pendingEditorReveal?.filePath === contentFile.filePath
? pendingEditorReveal.matchLength
: undefined
}
@ -404,20 +504,24 @@ export function EditorContent({
)
}
const renderConflictReviewSelectedContent = (selectedFile: OpenFile): React.JSX.Element => {
const selectedConflictEntry =
worktreeEntries.find((entry) => entry.path === selectedFile.relativePath) ?? null
return renderConflictReviewEditorContent({
contentFile: selectedFile,
entry: selectedConflictEntry,
className: 'flex min-h-0 flex-1 flex-col',
viewStateKeySuffix: 'selected'
})
}
if (activeFile.mode === 'conflict-review') {
return (
<ConflictReviewPanel
file={activeFile}
liveEntries={worktreeEntries}
onOpenEntry={(entry) =>
openConflictReviewFile(
activeFile.id,
activeFile.worktreeId,
activeFile.filePath,
entry,
detectLanguage(entry.path)
)
}
onOpenEntry={openConflictEntry}
selectedFile={selectedConflictReviewFile}
selectedContent={
selectedConflictReviewFile
@ -540,7 +644,16 @@ export function EditorContent({
}
return (
<div className="flex flex-1 min-h-0 flex-col">
{activeFile.conflict && <ConflictBanner file={activeFile} entry={activeConflictEntry} />}
{activeFile.conflict && (
<ConflictBanner
file={activeFile}
entry={activeConflictEntry}
conflictNavigation={getConflictNavigation(
activeFile,
editBuffers[activeFile.id] ?? fc.content
)}
/>
)}
<div className="min-h-0 flex-1 relative">
{isMarkdown ? (
renderMarkdownContent(fc)

View File

@ -81,7 +81,12 @@ export function useEditorPanelContentState({
: null
const loadFileContent = useCallback(
async (filePath: string, id: string, worktreeId?: string): Promise<void> => {
async (
filePath: string,
id: string,
worktreeId?: string,
relativePath?: string
): Promise<void> => {
try {
const connectionId = getConnectionId(worktreeId ?? null) ?? undefined
const restoredOpenFile = openFilesRef.current.find((file) => file.id === id)
@ -108,7 +113,7 @@ export function useEditorPanelContentState({
pending = readRuntimeFileContent({
settings: readSettings,
filePath,
relativePath: restoredOpenFile?.relativePath,
relativePath: restoredOpenFile?.relativePath ?? relativePath,
worktreeId,
connectionId
}) as Promise<FileContent>
@ -246,7 +251,7 @@ export function useEditorPanelContentState({
delete next[file.id]
return next
})
void loadFileContent(file.filePath, file.id, file.worktreeId)
void loadFileContent(file.filePath, file.id, file.worktreeId, file.relativePath)
},
[loadFileContent]
)
@ -281,8 +286,10 @@ export function useEditorPanelContentState({
activeFile?.id,
activeFile?.mode,
activeFile?.conflictReview?.selectedFileId,
activeFile?.conflictReview?.snapshotTimestamp,
selectedConflictReviewFile?.id,
isChangesMode
isChangesMode,
gitStatusByWorktree
])
useEditorPanelFileLoadRetry({