{
const bridge = window.helder
if (bridge) {
- try { await bridge.fs.delete(path) } catch { toast('Delete failed', path); return }
+ try { await bridge.fs.delete(path) } catch (e) { rlog.error('fs', 'delete failed', e, { path, isDir }); toast('Delete failed', path); return }
}
const inside = (p: string): boolean => p === path || (isDir && p.startsWith(path + '/'))
setHistory((h) => h.filter((p) => !inside(p)))
@@ -370,7 +380,7 @@ export function App(): React.ReactElement {
const rel = (dir ? dir + '/' : '') + name.replace(/^\/+/, '')
const bridge = window.helder
if (bridge) {
- try { await bridge.fs.create(rel) } catch { toast('Create failed', rel); return }
+ try { await bridge.fs.create(rel) } catch (e) { rlog.error('fs', 'create file failed', e, { path: rel }); toast('Create failed', rel); return }
}
if (dir) setOpenDirs((d) => { const n = new Set(d); n.add(dir); return n })
actions.refresh()
@@ -385,7 +395,7 @@ export function App(): React.ReactElement {
const rel = (dir ? dir + '/' : '') + name.replace(/^\/+/, '').replace(/\/+$/, '')
const bridge = window.helder
if (bridge) {
- try { await bridge.fs.mkdir(rel) } catch { toast('Create failed', rel); return }
+ try { await bridge.fs.mkdir(rel) } catch (e) { rlog.error('fs', 'create folder failed', e, { path: rel }); toast('Create failed', rel); return }
}
setOpenDirs((d) => { const n = new Set(d); if (dir) n.add(dir); n.add(rel); return n })
actions.refresh()
@@ -411,7 +421,7 @@ export function App(): React.ReactElement {
actions.unstage(p)
}
- function openFile(path: string, opts: { diff?: boolean; line?: number } = {}): void {
+ function openFile(path: string, opts: { diff?: boolean; line?: number; side?: DiffSide } = {}): void {
const changed = !!proj.diffs[path]
setFocusZone('editor')
setHistory((h) => [path, ...h.filter((p) => p !== path)].slice(0, 100))
@@ -421,6 +431,14 @@ export function App(): React.ReactElement {
// Unchanged files only have the plain editable "code" view.
const openMode: Mode = changed ? (opts.diff ? 'diff' : 'updated') : 'code'
setTabMode((m) => ({ ...m, [path]: openMode }))
+ // Remember which git row this came from, so Diff/Split show that half. An
+ // explorer click carries no side and falls back to the whole file.
+ setTabSide((m) => {
+ const n = { ...m }
+ if (opts.side) n[path] = opts.side
+ else delete n[path]
+ return n
+ })
reveal(path)
if (opts.line) {
// The updated/code views render in the CodeEditor (a textarea over a ),
@@ -506,7 +524,8 @@ export function App(): React.ReactElement {
}
if (target.kind === 'git') {
items.push({ sep: true })
- const isStaged = proj.staged.has(target.path)
+ // The row carries its own flag: a file can have a staged and an unstaged row.
+ const isStaged = target.staged ?? proj.staged.has(target.path)
items.push(isStaged
? { icon: Icon.minus({ style: { color: 'var(--mod)' } }), label: 'Unstage changes', onClick: () => unstageGuarded(target.path) }
: { icon: Icon.plus({ style: { color: 'var(--add)' } }), label: 'Stage changes', onClick: () => stageGuarded(target.path) })
@@ -546,7 +565,7 @@ export function App(): React.ReactElement {
if (activePanel === 'git' && gitSelPath) {
const row = document.querySelector('.git-row.kbd') as HTMLElement | null
const r = row?.getBoundingClientRect()
- openMenuAt(row ? rowAnchorX(row) : 220, r ? r.top + 4 : 120, { path: gitSelPath, kind: 'git', staged: proj.staged.has(gitSelPath) })
+ openMenuAt(row ? rowAnchorX(row) : 220, r ? r.top + 4 : 120, { path: gitSelPath, kind: 'git', staged: gitSelRow?.staged })
return true
}
if (activePanel === 'tree' && treeSelItem) {
@@ -559,7 +578,7 @@ export function App(): React.ReactElement {
}
// ↵ inside Git/Explorer: open the selected file (git → diff), toggle a folder.
function openPanelSelection(): boolean {
- if (activePanel === 'git' && gitSelPath) { openFile(gitSelPath, { diff: true }); return true }
+ if (activePanel === 'git' && gitSelRow) { openFile(gitSelRow.path, { diff: true, side: gitSelRow.staged ? 'staged' : 'unstaged' }); return true }
if (activePanel === 'tree' && treeSelItem) {
if (treeSelItem.type === 'dir') toggleDir(treeSelItem.path)
else openFile(treeSelItem.path)
@@ -712,7 +731,7 @@ export function App(): React.ReactElement {
else if (meta && e.key === 'Enter') {
if (ae && ae.classList.contains('commit-input')) return
e.preventDefault()
- if (commitMsg.trim() && proj.changes.some((c) => proj.staged.has(c.path))) commit()
+ if (commitMsg.trim() && proj.changes.some((c) => c.staged)) commit()
}
// ⌘C focuses the commit message (but let native copy run when there's a selection).
else if (meta && e.key.toLowerCase() === 'c') {
@@ -733,7 +752,61 @@ export function App(): React.ReactElement {
return () => window.removeEventListener('keydown', onKey)
}, [active, splitFor, overlay, focusZone, history, tabMode, commitMsg, proj, selection, confirm, menu, activePanel, gitNav, treeNav, gitSel, treeSel])
+ // ⌘R (View → Refresh, main process sends `view:refresh`) reloads the three
+ // left columns: git status (A) + the file tree (B) via a full project reload,
+ // and the open file in the viewer (C) re-read from disk. The viewer reload
+ // drops any in-memory buffer so the editable view shows on-disk truth — an
+ // explicit refresh is exactly when the user wants whatever the agent just
+ // wrote, the same "on-disk wins" rule used on file open / mode change.
+ // A refresh reads from disk, so nothing may visibly change — the columns then
+ // look inert and the keypress feels lost. Flash A/B/C light grey for ~250 ms so
+ // ⌘R always reads as "that landed". Two class names (a/b) alternate because a
+ // CSS animation only restarts when the animation-name changes: on a second ⌘R
+ // inside the window, re-adding the same class would replay nothing.
+ const [flashTick, setFlashTick] = useState(0)
+ const flashTimer = useRef | null>(null)
+ const flashClass = flashTick === 0 ? '' : (flashTick % 2 ? ' refresh-flash-a' : ' refresh-flash-b')
+ useEffect(() => () => { if (flashTimer.current) clearTimeout(flashTimer.current) }, [])
+
+ useEffect(() => {
+ if (!window.helder) return
+ return window.helder.onRefresh(() => {
+ actions.refresh()
+ const path = activeRef.current
+ if (path) reloadFromDisk(path)
+ setFlashTick((n) => n + 1)
+ if (flashTimer.current) clearTimeout(flashTimer.current)
+ flashTimer.current = setTimeout(() => setFlashTick(0), 400)
+ })
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
+
+ // Re-read git status the moment the Git column (Col A) gains focus, so it
+ // reflects on-disk truth whenever the user turns to it — e.g. after the agent
+ // rewrote files while focus was elsewhere. Git-only fast path (no tree/index
+ // re-walk); the FS watcher stays the backstop for everything else.
+ useEffect(() => {
+ if (activePanel === 'git') actions.refreshGit()
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [activePanel])
+
+ // Poll git status on an interval as a backstop for the FS watcher: an external
+ // tool (the agent, the git CLI) rewriting files *should* fire the watcher's
+ // project:changed, but a missed filesystem event would otherwise leave the Git
+ // column stale until the next manual ⌘R. Interval is config-driven
+ // (git.refreshInterval ms; 0 disables). Fast path — git status only.
+ useEffect(() => {
+ if (!window.helder) return
+ const ms = proj.config.git.refreshInterval
+ if (!ms || ms <= 0) return
+ const id = setInterval(() => actions.refreshGit(), ms)
+ return () => clearInterval(id)
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [proj.config.git.refreshInterval])
+
const mode: Mode = (active && tabMode[active]) || (active && proj.diffs[active] ? defaultMode : 'code')
+ // Which half the open tab shows. Also lights up the matching git row.
+ const activeSide: DiffSide | null = (active && tabSide[active]) || null
const crumb = active ? active.split('/') : []
@@ -789,17 +862,17 @@ export function App(): React.ReactElement {
{/* workbench */}
-
setActivePanel('git')}>
- 300} />
+ onOpen={openFile} onContext={openMenu} activePath={active} activeSide={activeSide} ctxPath={menu?.path ?? null}
+ kbdId={activePanel === 'git' ? gitSelRow?.id ?? null : null} showDir={gitW > 300} />
{ setAutoResize(false); setGitW((w) => clamp(w + dx, 160, 460)) }} />
- setActivePanel('tree')}>
{proj.tree ? (
{ setAutoResize(false); setTreeW((w) => clamp(w + dx, 160, 520)) }} />
- { setFocusZone('editor'); setActivePanel('editor') }}>
- { setFocusZone('editor'); setActivePanel('editor') }}>
+ { if (active) { setTabMode((mm) => ({ ...mm, [active]: m })); setSplitFor(null); reloadFromDisk(active) } }}
onContext={openMenu}
onSplit={(p) => setSplitFor(p)} splitOpen={splitFor === active}
@@ -831,7 +904,7 @@ export function App(): React.ReactElement {
{/* overlays */}
- {splitFor && setSplitFor(null)} onContext={openMenu} />}
+ {splitFor && setSplitFor(null)} onContext={openMenu} />}
{passPopup && {
window.dispatchEvent(new CustomEvent('agentPaste', { detail: payload }))
diff --git a/src/renderer/src/components.tsx b/src/renderer/src/components.tsx
index 854fcee..0dbc12a 100644
--- a/src/renderer/src/components.tsx
+++ b/src/renderer/src/components.tsx
@@ -1,6 +1,6 @@
/* Shared icons, FileIcon, GitPanel, FileTree */
import React, { Fragment } from 'react'
-import type { Change, FileNode, GitStatus } from './types'
+import type { Change, DiffSide, FileNode, GitStatus } from './types'
import { HL } from './highlight'
type SvgProps = React.SVGProps
@@ -55,7 +55,7 @@ export function FileIcon({ path }: { path: string }): React.ReactElement {
}
/* Shared callback signatures used across panels. */
-export type OpenFile = (path: string, opts?: { diff?: boolean; line?: number }) => void
+export type OpenFile = (path: string, opts?: { diff?: boolean; line?: number; side?: DiffSide }) => void
export interface ContextTarget {
path: string
kind: 'editor' | 'dir' | 'file' | 'git'
@@ -67,23 +67,27 @@ export interface ContextTarget {
export type OnContext = (e: React.MouseEvent, target: ContextTarget) => void
/* ============ Git / Source Control panel ============ */
-function GitRow({ c, staged, activePath, ctxPath, kbdPath, showDir, onOpen, onContext, onToggleStage }: {
+function GitRow({ c, activePath, activeSide, ctxPath, kbdId, showDir, onOpen, onContext, onToggleStage }: {
c: Change
- staged: boolean
activePath: string | null
+ /** Which half the open tab is showing, so only that row lights up. */
+ activeSide: DiffSide | null
ctxPath: string | null
- kbdPath: string | null
+ kbdId: string | null
showDir: boolean
onOpen: OpenFile
onContext: OnContext
onToggleStage: (path: string) => void
}): React.ReactElement {
+ const staged = c.staged
+ const side: DiffSide = staged ? 'staged' : 'unstaged'
const name = c.path.split('/').pop()
const dir = c.path.split('/').slice(0, -1).join('/')
const dirShown = showDir && !!dir
+ const isActive = activePath === c.path && (!activeSide || activeSide === side)
return (
- onOpen(c.path, { diff: true })}
+
onOpen(c.path, { diff: true, side })}
onContextMenu={(e) => onContext(e, { path: c.path, kind: 'git', staged })}
title={c.path}>
{c.status}
@@ -98,10 +102,9 @@ function GitRow({ c, staged, activePath, ctxPath, kbdPath, showDir, onOpen, onCo
)
}
-export function GitPanel({ branch, changes, staged, committed, commitMsg, setCommitMsg, onStage, onUnstage, onStageAll, onUnstageAll, onCommit, onPush, onOpen, onContext, activePath, ctxPath, kbdPath, showDir }: {
+export function GitPanel({ branch, changes, committed, commitMsg, setCommitMsg, onStage, onUnstage, onStageAll, onUnstageAll, onCommit, onPush, onOpen, onContext, activePath, activeSide, ctxPath, kbdId, showDir }: {
branch: string
changes: Change[]
- staged: Set
committed: Set
commitMsg: string
setCommitMsg: (v: string) => void
@@ -114,13 +117,14 @@ export function GitPanel({ branch, changes, staged, committed, commitMsg, setCom
onOpen: OpenFile
onContext: OnContext
activePath: string | null
+ activeSide: DiffSide | null
ctxPath: string | null
- kbdPath: string | null
+ kbdId: string | null
showDir: boolean
}): React.ReactElement {
const visible = changes.filter((c) => !committed.has(c.path))
- const stagedList = visible.filter((c) => staged.has(c.path))
- const changesList = visible.filter((c) => !staged.has(c.path))
+ const stagedList = visible.filter((c) => c.staged)
+ const changesList = visible.filter((c) => !c.staged)
const totals = visible.reduce((a, c) => ({ add: a.add + c.add, del: a.del + c.del }), { add: 0, del: 0 })
const canCommit = stagedList.length > 0 && commitMsg.trim().length > 0
@@ -144,7 +148,7 @@ export function GitPanel({ branch, changes, staged, committed, commitMsg, setCom
{stagedList.length > 0 && }
{stagedList.length > 0 ? stagedList.map((c) => (
-
)) : (
Nothing staged — use + to stage a file
@@ -157,7 +161,7 @@ export function GitPanel({ branch, changes, staged, committed, commitMsg, setCom
{changesList.length > 0 &&
}
{changesList.length > 0 ? changesList.map((c) => (
-
)) : (
All changes staged
diff --git a/src/renderer/src/data.ts b/src/renderer/src/data.ts
index ddeb12a..5a51411 100644
--- a/src/renderer/src/data.ts
+++ b/src/renderer/src/data.ts
@@ -6,6 +6,7 @@
* same original/updated text pair per changed file, so keep buildDiff()'s
* output shape. */
import type { Change, Diff, FileNode, Project } from './types'
+import { rowId } from './types'
import { buildDiff } from './diff'
// ---- working-tree (current / updated) file contents ----------------
@@ -651,7 +652,8 @@ const changes: Change[] = changeDefs.map((c) => {
original: orig,
updated: upd,
})
- return { path: c.path, status: c.status, add: d.add, del: d.del, deleted: c.status === 'D' }
+ // Mock rows are all unstaged here; project.tsx flips the staged ones over.
+ return { path: c.path, status: c.status, add: d.add, del: d.del, deleted: c.status === 'D', staged: false, id: rowId(c.path, false) }
})
export const PROJECT: Project = {
diff --git a/src/renderer/src/editor.tsx b/src/renderer/src/editor.tsx
index 922c05c..f3dc0b8 100644
--- a/src/renderer/src/editor.tsx
+++ b/src/renderer/src/editor.tsx
@@ -1,6 +1,7 @@
/* Editor: four view modes (Original / Updated / Diff / Split) + line selection */
-import React, { Fragment, useMemo, useRef } from 'react'
-import type { Diff, ViewLine } from './types'
+import React, { Fragment, useEffect, useMemo, useRef, useState } from 'react'
+import type { Diff, DiffSide, ViewLine } from './types'
+import { rowId } from './types'
import { useProject } from './project'
import { HL } from './highlight'
import { renderMarkdown } from './markdown'
@@ -91,6 +92,32 @@ function MarkdownView({ path, text, onContext }: { path: string; text: string; o
)
}
+/* Image preview: fetches the file as a data: URL from main (the renderer can't
+ * read the filesystem) and shows it centred on the editor surface. Read-only. */
+function ImageView({ path, onContext }: { path: string; onContext: OnContext }): React.ReactElement {
+ const [src, setSrc] = useState('')
+ const [failed, setFailed] = useState(false)
+ useEffect(() => {
+ let alive = true
+ setSrc(''); setFailed(false)
+ const bridge = window.helder
+ if (!bridge) { setFailed(true); return }
+ bridge.fs.imageDataUrl(path)
+ .then((url) => { if (alive) { if (url) setSrc(url); else setFailed(true) } })
+ .catch(() => { if (alive) setFailed(true) })
+ return () => { alive = false }
+ }, [path])
+ return (
+ { e.preventDefault(); onContext(e, { path, kind: 'editor', line: 1 }) }}>
+ {src
+ ?

+ : failed
+ ?
+ : null}
+
+ )
+}
+
/* Generic pane: renders an array of line descriptors with selection + caret + context. */
function PaneView({ cacheKey, path, lines, lang, showSign, cursor, selection, setCursor, setSelection, onContext }: {
cacheKey: string
@@ -197,7 +224,9 @@ function PaneView({ cacheKey, path, lines, lang, showSign, cursor, selection, se
)
}
-/* Build the line descriptors for a given mode. */
+/* Build the line descriptors for a given mode. The caller picks which diff to
+ * pass: Original and Actual always get the file-level HEAD-vs-disk pair, while
+ * Diff gets the pair of the git row you clicked. */
function buildLines(mode: Mode, diff: Diff | null, fileText: string | undefined): { lines: ViewLine[]; showSign: boolean } {
if (mode === 'original' && diff) return { lines: diff.left.map((l) => ({ no: l.no, text: l.text, row: l.mark === 'del' ? 'bar-del' : null })), showSign: false }
if (mode === 'updated' && diff) return { lines: diff.right.map((l) => ({ no: l.no, text: l.text, row: l.mark === 'add' ? 'bar-add' : null })), showSign: false }
@@ -218,9 +247,11 @@ function segmentsFor(hasDiff: boolean, isMarkdown: boolean): { id: Mode; label:
return segs
}
-export function Editor({ active, mode, setMode, onContext, onSplit, splitOpen, cursor, selection, setCursor, setSelection, bufferText, onEdit }: {
+export function Editor({ active, mode, side, setMode, onContext, onSplit, splitOpen, cursor, selection, setCursor, setSelection, bufferText, onEdit }: {
active: string | null
mode: Mode
+ /** Which git row opened this tab. Only Diff and Split follow it. */
+ side: DiffSide | null
setMode: (m: Mode) => void
onContext: OnContext
onSplit: (path: string) => void
@@ -234,10 +265,16 @@ export function Editor({ active, mode, setMode, onContext, onSplit, splitOpen, c
}): React.ReactElement {
const PROJECT = useProject()
const tab = active ? { path: active } : null
+ const isImage = tab ? HL.isImage(tab.path) : false
const change = tab ? PROJECT.changes.find((c) => c.path === tab.path) : null
+ // Original / Actual always show the whole file: HEAD vs disk.
const diff = tab ? PROJECT.diffs[tab.path] : null
+ // Diff / Split show the half you clicked in the git panel. A file with only
+ // one row has an identical pair either way.
+ const rowDiff = (tab && side ? PROJECT.rowDiffs[rowId(tab.path, side === 'staged')] : null) || diff
+ const bothSides = !!tab && !!PROJECT.rowDiffs[rowId(tab.path, true)] && !!PROJECT.rowDiffs[rowId(tab.path, false)]
const lang = tab ? HL.langFor(tab.path) : null
- const hasDiff = !!(change && diff)
+ const hasDiff = !isImage && !!(change && diff)
const isMarkdown = lang === 'markdown'
const segments = segmentsFor(hasDiff, isMarkdown)
@@ -249,16 +286,24 @@ export function Editor({ active, mode, setMode, onContext, onSplit, splitOpen, c
else if (hasDiff) effMode = mode === 'code' || mode === 'preview' ? 'updated' : mode
else effMode = 'code'
+ // The diff actually on screen: the row pair for Diff, the whole file otherwise.
+ const shown = effMode === 'diff' ? rowDiff : diff
let built: { lines: ViewLine[]; showSign: boolean } | null = null
if (tab && effMode !== 'preview') {
- if (hasDiff) built = buildLines(effMode, diff, PROJECT.files[tab.path])
+ if (hasDiff) built = buildLines(effMode, shown, PROJECT.files[tab.path])
else built = buildLines('code', null, PROJECT.files[tab.path])
}
- const statusWord = change ? (change.status === 'A' ? 'Added' : change.status === 'D' ? 'Deleted' : 'Modified') : ''
+ // File-level status, taken from the whole-file diff rather than a single row:
+ // a file can be staged as modified and deleted on disk at the same time.
+ const fileStatus = diff ? (diff.deleted ? 'D' : diff.added ? 'A' : 'M') : change?.status
+ const statusWord = fileStatus === 'A' ? 'Added' : fileStatus === 'D' ? 'Deleted' : 'Modified'
const activeSeg = splitOpen ? 'split' : effMode
- const emptyUpdated = effMode === 'updated' && built && built.lines.length === 0
- const emptyOriginal = effMode === 'original' && built && built.lines.length === 0
+ // Keyed off the git status, not the line count: an empty file that still exists
+ // (a just-created one, or one emptied by hand) has zero lines too, and must get
+ // the editor rather than the "deleted" placeholder.
+ const emptyUpdated = effMode === 'updated' && fileStatus === 'D'
+ const emptyOriginal = effMode === 'original' && fileStatus === 'A'
// Editable in the live-buffer modes; Original/Diff/Preview stay read-only views.
const editable = effMode === 'code' || effMode === 'updated'
@@ -282,26 +327,35 @@ export function Editor({ active, mode, setMode, onContext, onSplit, splitOpen, c
{change ? (
- {statusWord}
- {change.add > 0 && +{change.add}}
- {change.del > 0 && −{change.del}}
+ {statusWord}
+ {!!shown && shown.add > 0 && +{shown.add}}
+ {!!shown && shown.del > 0 && −{shown.del}}
+ {/* Only ambiguous when the file is staged AND edited again: say
+ which pair the diff is comparing. */}
+ {bothSides && effMode === 'diff' && (
+ {side === 'unstaged' ? 'staged → actual' : 'HEAD → staged'}
+ )}
) : (
-
{HL.langLabel(tab.path)}
+
{isImage ? 'Image' : HL.langLabel(tab.path)}
+ )}
+ {!isImage && (
+
+ {segments.map((s) => (
+
+ ))}
+ {hasDiff && (
+
+ )}
+
)}
-
- {segments.map((s) => (
-
- ))}
- {hasDiff && (
-
- )}
-
- {effMode === 'preview' ? (
+ {isImage ? (
+
+ ) : effMode === 'preview' ? (
) : emptyUpdated ? (
No updated version
This file was deleted in the change.
@@ -310,7 +364,7 @@ export function Editor({ active, mode, setMode, onContext, onSplit, splitOpen, c
) : editable ? (
) : (
- built &&
)}
@@ -321,20 +375,24 @@ export function Editor({ active, mode, setMode, onContext, onSplit, splitOpen, c
}
/* Full-screen side-by-side split view */
-export function SplitView({ path, onClose, onContext }: {
+export function SplitView({ path, side, onClose, onContext }: {
path: string
+ /** Which git row opened this file. Split compares that row's pair. */
+ side: DiffSide | null
onClose: () => void
onContext: OnContext
}): React.ReactElement {
const PROJECT = useProject()
- const diff = PROJECT.diffs[path]
+ const diff = (side ? PROJECT.rowDiffs[rowId(path, side === 'staged')] : null) || PROJECT.diffs[path]
const lang = HL.langFor(path)
const leftRef = useRef(null), rightRef = useRef(null)
const lock = useRef(false)
const change = PROJECT.changes.find((c) => c.path === path)
+ const splitStatus = diff.deleted ? 'D' : diff.added ? 'A' : 'M'
+ const bothSides = !!PROJECT.rowDiffs[rowId(path, true)] && !!PROJECT.rowDiffs[rowId(path, false)]
- const leftHtml = useMemo(() => diff.split.map((r) => r.l ? HL.hlLine(r.l.text, lang) : ''), [path])
- const rightHtml = useMemo(() => diff.split.map((r) => r.r ? HL.hlLine(r.r.text, lang) : ''), [path])
+ const leftHtml = useMemo(() => diff.split.map((r) => r.l ? HL.hlLine(r.l.text, lang) : ''), [path, side])
+ const rightHtml = useMemo(() => diff.split.map((r) => r.r ? HL.hlLine(r.r.text, lang) : ''), [path, side])
function sync(from: HTMLDivElement | null, to: HTMLDivElement | null): void {
if (lock.current || !from || !to) return
@@ -356,9 +414,10 @@ export function SplitView({ path, onClose, onContext }: {
{path}
- {change &&
{change.status === 'A' ? 'Added' : change.status === 'D' ? 'Deleted' : 'Modified'}}
- {change && change.add > 0 &&
+{change.add}}
- {change && change.del > 0 &&
−{change.del}}
+ {change &&
{splitStatus === 'A' ? 'Added' : splitStatus === 'D' ? 'Deleted' : 'Modified'}}
+ {diff.add > 0 &&
+{diff.add}}
+ {diff.del > 0 &&
−{diff.del}}
+ {bothSides &&
{side === 'unstaged' ? 'staged → actual' : 'HEAD → staged'}}