Skip to content

Commit 4b1694b

Browse files
authored
Merge pull request #19 from yugo-ibuki/claude/add-diff-viewing-XtZ7B
Add git diff viewer overlay with vim-like navigation
2 parents b4374a6 + b08e8c6 commit 4b1694b

14 files changed

Lines changed: 496 additions & 4 deletions

File tree

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/main/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
killPane,
1717
findShellPane,
1818
ensureShellPane,
19+
gitDiff,
1920
getConversationLog
2021
} from './tmux'
2122
import type { ChatMessage } from './tmux'
@@ -250,6 +251,7 @@ app.whenReady().then(() => {
250251
ipcMain.handle('git:add', async (_event, cwd: string) => gitAdd(cwd))
251252
ipcMain.handle('git:commit', async (_event, { cwd, message }) => gitCommit(cwd, message))
252253
ipcMain.handle('git:push', async (_event, cwd: string) => gitPush(cwd))
254+
ipcMain.handle('git:diff', async (_event, { cwd, staged }) => gitDiff(cwd, staged))
253255

254256
ipcMain.handle('window:set-always-on-top', (_event, value: boolean) => {
255257
const win = BrowserWindow.getFocusedWindow()

src/main/tmux.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -749,6 +749,16 @@ export async function gitPush(cwd: string): Promise<{ success: boolean; error?:
749749
}
750750
}
751751

752+
export async function gitDiff(cwd: string, staged: boolean): Promise<string> {
753+
try {
754+
const args = ['-C', cwd, 'diff']
755+
if (staged) args.push('--staged')
756+
return await runGit(args)
757+
} catch {
758+
return ''
759+
}
760+
}
761+
752762
export async function listTmuxSessions(): Promise<string[]> {
753763
try {
754764
const stdout = await run(['list-sessions', '-F', '#{session_name}'])

src/preload/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ interface TmuxAPI {
6464
gitAdd: (cwd: string) => Promise<SendResult>
6565
gitCommit: (cwd: string, message: string) => Promise<SendResult>
6666
gitPush: (cwd: string) => Promise<SendResult>
67+
gitDiff: (cwd: string, staged?: boolean) => Promise<string>
6768
setAlwaysOnTop: (value: boolean) => Promise<boolean>
6869
getAlwaysOnTop: () => Promise<boolean>
6970
setOpacity: (value: number) => Promise<number>

src/preload/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ const api = {
6060
gitCommit: (cwd: string, message: string): Promise<SendResult> =>
6161
ipcRenderer.invoke('git:commit', { cwd, message }),
6262
gitPush: (cwd: string): Promise<SendResult> => ipcRenderer.invoke('git:push', cwd),
63+
gitDiff: (cwd: string, staged = false): Promise<string> =>
64+
ipcRenderer.invoke('git:diff', { cwd, staged }),
6365
setAlwaysOnTop: (value: boolean): Promise<boolean> =>
6466
ipcRenderer.invoke('window:set-always-on-top', value),
6567
getAlwaysOnTop: (): Promise<boolean> => ipcRenderer.invoke('window:get-always-on-top'),

src/renderer/src/App.css

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1563,3 +1563,160 @@
15631563
color: var(--accent);
15641564
font-weight: 600;
15651565
}
1566+
1567+
/* ── Diff viewer ── */
1568+
.diff-content {
1569+
overflow-y: auto;
1570+
padding: 4px 0;
1571+
white-space: normal;
1572+
word-break: normal;
1573+
}
1574+
1575+
.diff-empty {
1576+
padding: 24px;
1577+
text-align: center;
1578+
color: var(--text-dim);
1579+
}
1580+
1581+
.diff-file-section {
1582+
margin-bottom: 2px;
1583+
}
1584+
1585+
.diff-file-header {
1586+
display: flex;
1587+
align-items: center;
1588+
gap: 6px;
1589+
width: 100%;
1590+
padding: 5px 10px;
1591+
background: var(--bg-raised);
1592+
border: none;
1593+
border-bottom: 1px solid var(--border-subtle);
1594+
cursor: pointer;
1595+
font-family: var(--font-mono);
1596+
font-size: 11px;
1597+
color: var(--text);
1598+
text-align: left;
1599+
transition: background var(--transition-fast);
1600+
}
1601+
1602+
.diff-file-header:hover {
1603+
background: var(--bg-hover);
1604+
}
1605+
1606+
.diff-file-chevron {
1607+
font-size: 9px;
1608+
color: var(--text-dim);
1609+
width: 12px;
1610+
flex-shrink: 0;
1611+
}
1612+
1613+
.diff-file-path {
1614+
font-weight: 600;
1615+
flex: 1;
1616+
overflow: hidden;
1617+
text-overflow: ellipsis;
1618+
white-space: nowrap;
1619+
}
1620+
1621+
.diff-file-stats {
1622+
display: flex;
1623+
gap: 6px;
1624+
flex-shrink: 0;
1625+
font-size: 10px;
1626+
font-weight: 600;
1627+
}
1628+
1629+
.diff-stat-add {
1630+
color: #34d399;
1631+
}
1632+
1633+
.diff-stat-del {
1634+
color: #ef4444;
1635+
}
1636+
1637+
.diff-file-count {
1638+
font-weight: 400;
1639+
color: var(--text-dim);
1640+
margin-left: 8px;
1641+
font-size: 9px;
1642+
}
1643+
1644+
.diff-line-table {
1645+
width: 100%;
1646+
border-collapse: collapse;
1647+
font-family: var(--font-mono);
1648+
font-size: 11px;
1649+
line-height: 1.5;
1650+
table-layout: auto;
1651+
min-width: 100%;
1652+
}
1653+
1654+
.diff-ln {
1655+
width: 40px;
1656+
min-width: 40px;
1657+
padding: 0 6px;
1658+
text-align: right;
1659+
color: var(--text-dim);
1660+
user-select: none;
1661+
font-size: 10px;
1662+
opacity: 0.6;
1663+
border-right: 1px solid var(--border-subtle);
1664+
vertical-align: top;
1665+
}
1666+
1667+
.diff-line-content {
1668+
padding: 0 10px;
1669+
white-space: pre;
1670+
overflow-x: auto;
1671+
}
1672+
1673+
.diff-line-hunk .diff-line-content {
1674+
color: var(--accent);
1675+
font-size: 10px;
1676+
padding: 4px 10px;
1677+
background: rgba(91, 141, 239, 0.06);
1678+
}
1679+
1680+
.diff-line-add {
1681+
background: rgba(52, 211, 153, 0.1);
1682+
}
1683+
1684+
.diff-line-add .diff-line-content {
1685+
color: #34d399;
1686+
}
1687+
1688+
.diff-line-del {
1689+
background: rgba(239, 68, 68, 0.1);
1690+
}
1691+
1692+
.diff-line-del .diff-line-content {
1693+
color: #ef4444;
1694+
}
1695+
1696+
[data-theme='light'] .diff-stat-add {
1697+
color: #16a372;
1698+
}
1699+
1700+
[data-theme='light'] .diff-stat-del {
1701+
color: #dc2626;
1702+
}
1703+
1704+
[data-theme='light'] .diff-line-add {
1705+
background: rgba(22, 163, 114, 0.1);
1706+
}
1707+
1708+
[data-theme='light'] .diff-line-add .diff-line-content {
1709+
color: #16a372;
1710+
}
1711+
1712+
[data-theme='light'] .diff-line-del {
1713+
background: rgba(220, 38, 38, 0.1);
1714+
}
1715+
1716+
[data-theme='light'] .diff-line-del .diff-line-content {
1717+
color: #dc2626;
1718+
}
1719+
1720+
[data-theme='light'] .diff-line-hunk .diff-line-content {
1721+
background: rgba(74, 125, 224, 0.06);
1722+
}

src/renderer/src/App.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { Sidebar } from './components/Sidebar'
1111
import { PreviewOverlay } from './components/PreviewOverlay'
1212
import { DetailOverlay } from './components/DetailOverlay'
1313
import { GitOverlay } from './components/GitOverlay'
14+
import { DiffOverlay } from './components/DiffOverlay'
1415
import { HelpOverlay } from './components/HelpOverlay'
1516
import { CreateDialog } from './components/CreateDialog'
1617
import { ConfirmDialog } from './components/ConfirmDialog'
@@ -30,6 +31,7 @@ function App(): React.JSX.Element {
3031
const paneContent = useUiStore((s) => s.paneContent)
3132
const paneDetail = useUiStore((s) => s.paneDetail)
3233
const gitPopup = useUiStore((s) => s.gitPopup)
34+
const diffContent = useUiStore((s) => s.diffContent)
3335
const createDialog = useUiStore((s) => s.createDialog)
3436
const confirmKill = useUiStore((s) => s.confirmKill)
3537
const lastPrompts = usePaneStore((s) => s.lastPrompts)
@@ -128,6 +130,7 @@ function App(): React.JSX.Element {
128130
{paneContent !== null && <PreviewOverlay streamRefs={streamRefs} />}
129131
{paneDetail !== null && <DetailOverlay />}
130132
{gitPopup !== null && <GitOverlay />}
133+
{diffContent !== null && <DiffOverlay />}
131134
{createDialog && <CreateDialog />}
132135
{confirmKill && paneDetail && <ConfirmDialog />}
133136
<HelpOverlay />
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { useState } from 'react'
2+
import type { DiffFile } from '../utils/parseDiff'
3+
4+
const COLLAPSE_THRESHOLD = 50
5+
6+
export function DiffFileSection({ file }: { file: DiffFile }): React.JSX.Element {
7+
const totalChanged = file.additions + file.deletions
8+
const [open, setOpen] = useState(totalChanged < COLLAPSE_THRESHOLD)
9+
10+
return (
11+
<div className="diff-file-section">
12+
<button className="diff-file-header" onClick={() => setOpen(!open)}>
13+
<span className="diff-file-chevron">{open ? '▼' : '▶'}</span>
14+
<span className="diff-file-path">{file.path}</span>
15+
<span className="diff-file-stats">
16+
{file.additions > 0 && <span className="diff-stat-add">+{file.additions}</span>}
17+
{file.deletions > 0 && <span className="diff-stat-del">-{file.deletions}</span>}
18+
</span>
19+
</button>
20+
{open && (
21+
<table className="diff-line-table">
22+
<tbody>
23+
{file.lines.map((line, i) => {
24+
if (line.type === 'hunk') {
25+
return (
26+
<tr key={i} className="diff-line diff-line-hunk">
27+
<td className="diff-ln" />
28+
<td className="diff-ln" />
29+
<td className="diff-line-content">{line.content}</td>
30+
</tr>
31+
)
32+
}
33+
const rowClass = `diff-line diff-line-${line.type}`
34+
return (
35+
<tr key={i} className={rowClass}>
36+
<td className="diff-ln">{line.oldNum ?? ''}</td>
37+
<td className="diff-ln">{line.newNum ?? ''}</td>
38+
<td className="diff-line-content">{line.content}</td>
39+
</tr>
40+
)
41+
})}
42+
</tbody>
43+
</table>
44+
)}
45+
</div>
46+
)
47+
}

0 commit comments

Comments
 (0)