feat: git diff panel (Files/Git tab in the file browser)
Adds a Git tab to the right-side file panel that shows the project repository's diff and lets the user stage, unstage, commit, and discard whole files in-session. Two comparison modes (Uncommitted vs HEAD, and the branch vs its base — upstream tracking branch else default branch), auto- selected by repo state on first open and pinned after explicit choice; per-file expand/collapse with lazy syntax-highlighted diffs, +/- stats, and binary/large-file placeholders. All git read and write logic lives in apps/server via a new git_diff service: argv-safe execFile only (never a shell), per-file paths validated repo-relative through pathGuard with a realpath symlink-escape check, server-derived commit identity (the request carries no author fields), and the write endpoints are deliberately absent from the assistant tool registry. Reads are bounded (30s deadline, 10MB); an index lock or an in-progress merge/rebase/cherry-pick/bisect surfaces as "repository busy" and disables writes. The panel stays current via a client git_diff_refresh session event (no new wire contract) coalesced across tab open, mutations, turn completion, and pending-change apply. Discard is an irrecoverable hard-delete behind a plain confirm that distinguishes reverting a tracked file from deleting an untracked one. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,7 @@ import { providerIcon, providerLabel } from '@/components/coder/providerIcons';
|
||||
import { refreshAgentSessions } from '@/hooks/useAgentSessions';
|
||||
import { useAgentStatus, type AgentStatus, type AgentStatusEntry } from '@/hooks/useAgentStatus';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { sessionEvents } from '@/hooks/sessionEvents';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
@@ -437,6 +438,7 @@ function usePendingChanges(sessionId: string) {
|
||||
});
|
||||
if (res.ok) {
|
||||
setChanges((prev) => prev.map((c) => c.id === changeId ? { ...c, status: 'approved' } : c));
|
||||
sessionEvents.emit({ type: 'git_diff_refresh' });
|
||||
}
|
||||
}, [sessionId]);
|
||||
|
||||
@@ -446,6 +448,7 @@ function usePendingChanges(sessionId: string) {
|
||||
});
|
||||
if (res.ok) {
|
||||
setChanges((prev) => prev.map((c) => c.id === changeId ? { ...c, status: 'rejected' } : c));
|
||||
sessionEvents.emit({ type: 'git_diff_refresh' });
|
||||
}
|
||||
}, [sessionId]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user