/** * The diff tab: one change opened from the git panel, like VSCode's diff * editor. A worktree ref loads the file's unified diff (`git diff`, staged or * not; untracked files — which git diff never covers — render as a full-file * addition from their content), a commit ref loads the commit's full patch * (`git.show`-style). The header carries a refresh button because the tab * stays mounted while the git panel's staging/discard operations change the * very content it shows. */ import { useCallback, useEffect, useState } from 'react' import { IconRefreshOutline16 } from '@deepseek-ai/dsh-client-ui-primitives' import type { SessionScope } from './api.ts' import { api } from './api.ts' import type { SidebarDiffRef } from './state.ts' import { DiffView } from './DiffView.tsx' import { t } from './locales.ts' import css from './sidebar.module.css' /** The loaded diff surface (untracked content rendered as a full addition). */ interface DiffData { diff: string untracked?: string } export function DiffTab(props: { sessionId: string; cwd: string | undefined; diff: SidebarDiffRef }) { const { sessionId, cwd, diff } = props const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [data, setData] = useState(null) const [tick, setTick] = useState(0) const refresh = useCallback((): void => { setTick(value => value + 1) }, []) useEffect(() => { let cancelled = false const scope: SessionScope = { sessionId, cwd } setLoading(true) setError(null) setData(null) const load = async (): Promise => { try { if (diff.kind === 'commit') { const result = await api.gitCommitDiff(scope, diff.hashFull) if (!cancelled) setData({ diff: result.diff }) return } let result = await api.gitDiff(scope, diff.path, diff.staged) if (result.diff === '') { // The requested side is empty — try the OTHER side once: the ref // may predate the staged-flag fix, or the change moved sides (a // file staged after its tab opened). Both sides empty means the // file genuinely has no text changes. const other = await api.gitDiff(scope, diff.path, !diff.staged) if (other.diff !== '') result = other } if (result.diff !== '') { if (!cancelled) setData({ diff: result.diff }) return } // Empty diff: an untracked file (git diff never lists it) falls back // to a full-file addition; anything else is a genuine no-text-change. if (diff.untracked === true && !diff.staged) { const text = await api.fsRead(scope, diff.path) if (!cancelled) { setData(text.kind === 'text' ? { diff: '', untracked: text.content } : { diff: '' }) } return } if (!cancelled) setData({ diff: '' }) } catch (reason) { if (!cancelled) setError(reason instanceof Error ? reason.message : String(reason)) } finally { if (!cancelled) setLoading(false) } } void load() return () => { cancelled = true } }, [sessionId, cwd, diff, tick]) return (
{diff.kind === 'worktree' ? diff.path : `${diff.hash} ${diff.subject}`}
{loading &&
{t('loading')}
} {!loading && error !== null &&
{t('diffLoadError')}: {error}
} {!loading && error === null && data !== null && ( <> {data.untracked !== undefined ? : } {data.diff === '' && data.untracked === undefined && (
{t('diffEmpty')}
)} )}
) }