Skip to content

Commit f61f5fd

Browse files
zanetworkerclaude
andcommitted
feat: live diff panel alongside session terminal
Session tab now has a "Show Diffs" toggle button. When enabled, the terminal splits horizontally: live session on the left, diff review panel on the right. Diffs auto-refresh every 5 seconds while the session is active. DiffReview component gains refreshInterval prop (auto-poll) and compact prop (narrower file tree for side panel use). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent fe4c85e commit f61f5fd

2 files changed

Lines changed: 59 additions & 27 deletions

File tree

web/src/components/DiffReview.tsx

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@ interface FileDiff {
2323

2424
interface Props {
2525
sessionFile: string;
26+
refreshInterval?: number;
27+
compact?: boolean;
2628
}
2729

28-
export function DiffReview({ sessionFile }: Props) {
30+
export function DiffReview({ sessionFile, refreshInterval, compact }: Props) {
2931
const [files, setFiles] = useState<FileDiff[]>([]);
3032
const [totalAdded, setTotalAdded] = useState(0);
3133
const [totalRemoved, setTotalRemoved] = useState(0);
@@ -37,22 +39,32 @@ export function DiffReview({ sessionFile }: Props) {
3739

3840
useEffect(() => {
3941
if (!sessionFile) return;
40-
setLoading(true);
41-
fetch(`/api/sessions/diffs?file=${encodeURIComponent(sessionFile)}`)
42-
.then(r => r.ok ? r.json() : null)
43-
.then(data => {
44-
if (data) {
45-
setFiles(data.files || []);
46-
setTotalAdded(data.totalAdded || 0);
47-
setTotalRemoved(data.totalRemoved || 0);
48-
if (data.files?.length > 0) {
49-
setSelectedFile(data.files[0].path);
42+
43+
const fetchDiffs = (isInitial: boolean) => {
44+
if (isInitial) setLoading(true);
45+
fetch(`/api/sessions/diffs?file=${encodeURIComponent(sessionFile)}`)
46+
.then(r => r.ok ? r.json() : null)
47+
.then(data => {
48+
if (data) {
49+
setFiles(data.files || []);
50+
setTotalAdded(data.totalAdded || 0);
51+
setTotalRemoved(data.totalRemoved || 0);
52+
if (isInitial && data.files?.length > 0) {
53+
setSelectedFile(data.files[0].path);
54+
}
5055
}
51-
}
52-
setLoading(false);
53-
})
54-
.catch(() => setLoading(false));
55-
}, [sessionFile]);
56+
if (isInitial) setLoading(false);
57+
})
58+
.catch(() => { if (isInitial) setLoading(false); });
59+
};
60+
61+
fetchDiffs(true);
62+
63+
if (refreshInterval && refreshInterval > 0) {
64+
const id = setInterval(() => fetchDiffs(false), refreshInterval);
65+
return () => clearInterval(id);
66+
}
67+
}, [sessionFile, refreshInterval]);
5668

5769
if (loading) {
5870
return (
@@ -110,7 +122,7 @@ export function DiffReview({ sessionFile }: Props) {
110122
<div style={{ flex: 1, display: 'flex', overflow: 'hidden' }}>
111123
{/* File tree */}
112124
<div style={{
113-
width: 240, borderRight: '1px solid var(--border)',
125+
width: compact ? 180 : 240, borderRight: '1px solid var(--border)',
114126
overflowY: 'auto', flexShrink: 0,
115127
}}>
116128
<input

web/src/components/RightPanel.tsx

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export function RightPanel({ agent, onClose, isFullscreen, onToggleFullscreen }:
2727
return saved ? parseInt(saved) : 440;
2828
});
2929
const [isResizing, setIsResizing] = useState(false);
30+
const [showSessionDiffs, setShowSessionDiffs] = useState(false);
3031
const panelRef = useRef<HTMLDivElement>(null);
3132

3233
const turns = useTraceStream(agent.SessionID, agent.SessionFile);
@@ -357,18 +358,37 @@ export function RightPanel({ agent, onClose, isFullscreen, onToggleFullscreen }:
357358
{agent.ProviderName === 'codex' ? 'Session will resume with --full-auto' : 'Session will resume with --dangerously-skip-permissions'}
358359
</span>
359360
)}
361+
<button
362+
onClick={() => setShowSessionDiffs(prev => !prev)}
363+
style={{
364+
marginLeft: 'auto', padding: '2px 8px', borderRadius: 3,
365+
border: `1px solid ${showSessionDiffs ? 'var(--green)' : 'var(--border)'}`,
366+
background: showSessionDiffs ? 'rgba(105,223,115,0.1)' : 'transparent',
367+
color: showSessionDiffs ? 'var(--green)' : 'var(--fg-3)',
368+
fontSize: 10, fontWeight: 600, cursor: 'pointer',
369+
}}
370+
>
371+
{showSessionDiffs ? '✓ Diffs' : 'Show Diffs'}
372+
</button>
360373
</div>
361374
)}
362-
<div style={{ flex: 1, position: 'relative', minHeight: 0, overflow: 'hidden' }}>
363-
{sessionMounted && (
364-
<SessionView
365-
tmuxSession={agent.TMuxSession || undefined}
366-
sessionId={agent.SessionID || undefined}
367-
provider={agent.ProviderName || undefined}
368-
workingDir={agent.WorkingDir || undefined}
369-
skipPermissions={skipPermissions}
370-
key={`${agent.TMuxSession || agent.SessionID}-${skipPermissions}`}
371-
/>
375+
<div style={{ flex: 1, display: 'flex', minHeight: 0, overflow: 'hidden' }}>
376+
<div style={{ flex: 1, position: 'relative', minHeight: 0, overflow: 'hidden' }}>
377+
{sessionMounted && (
378+
<SessionView
379+
tmuxSession={agent.TMuxSession || undefined}
380+
sessionId={agent.SessionID || undefined}
381+
provider={agent.ProviderName || undefined}
382+
workingDir={agent.WorkingDir || undefined}
383+
skipPermissions={skipPermissions}
384+
key={`${agent.TMuxSession || agent.SessionID}-${skipPermissions}`}
385+
/>
386+
)}
387+
</div>
388+
{showSessionDiffs && (
389+
<div style={{ width: '50%', borderLeft: '1px solid var(--border)', display: 'flex', flexDirection: 'column', minHeight: 0 }}>
390+
<DiffReview sessionFile={agent.SessionFile} refreshInterval={5000} compact />
391+
</div>
372392
)}
373393
</div>
374394
</div>

0 commit comments

Comments
 (0)