diff --git a/clarity.controlplane/src/components/GitCommitDrawer.tsx b/clarity.controlplane/src/components/GitCommitDrawer.tsx
index efdabd4..fb07ebc 100644
--- a/clarity.controlplane/src/components/GitCommitDrawer.tsx
+++ b/clarity.controlplane/src/components/GitCommitDrawer.tsx
@@ -1,74 +1,127 @@
-import { useEffect, useState, useRef } from 'react';
-import { Button, Drawer, Intent, NonIdealState, Spinner, Tag, Tooltip } from '@blueprintjs/core';
+import { useEffect, useState } from 'react';
+import { Button, Collapse, Drawer, Icon, Intent, NonIdealState, Spinner, Tag, Tooltip } from '@blueprintjs/core';
import { html as diff2htmlHtml } from 'diff2html';
import 'diff2html/bundles/css/diff2html.min.css';
-import hljs from 'highlight.js';
-import 'highlight.js/styles/github.css';
-import { getCommitDetail, type CommitDetail } from '../api/opcApi';
+import { getCommitDetail, type CommitDetail, type CommitFile } from '../api/opcApi';
interface Props {
hash: string | null;
onClose: () => void;
}
+function fileStatusIntent(status: string): Intent {
+ if (status === 'added') return Intent.SUCCESS;
+ if (status === 'deleted') return Intent.DANGER;
+ if (status === 'renamed') return Intent.WARNING;
+ return Intent.NONE;
+}
+
+function fileStatusIcon(status: string): string {
+ if (status === 'added') return 'plus';
+ if (status === 'deleted') return 'minus';
+ if (status === 'renamed') return 'arrow-right';
+ return 'edit';
+}
+
+function FileDiff({ file }: { file: CommitFile }) {
+ const [open, setOpen] = useState(true);
+
+ const diffHtml = file.patch
+ ? diff2htmlHtml(file.patch, {
+ drawFileList: false,
+ matching: 'lines',
+ outputFormat: 'line-by-line',
+ renderNothingWhenEmpty: true,
+ })
+ : '';
+
+ const displayPath = file.status === 'renamed' && file.oldPath && file.oldPath !== file.path
+ ? `${file.oldPath} → ${file.path}`
+ : file.path;
+
+ return (
+
+
+
+ {diffHtml
+ ?
+ : Binary or empty file — no textual diff available.
+ }
+
+
+ );
+}
+
export function GitCommitDrawer({ hash, onClose }: Props) {
const [detail, setDetail] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
- const diffRef = useRef(null);
useEffect(() => {
- if (!hash) { setDetail(null); setError(null); return; }
- setLoading(true); setDetail(null); setError(null);
+ if (!hash) {
+ // Delay clearing so the closing animation doesn't flash blank
+ const t = setTimeout(() => { setDetail(null); setError(null); }, 300);
+ return () => clearTimeout(t);
+ }
+ setLoading(true);
+ setError(null);
getCommitDetail(hash)
- .then(setDetail)
+ .then(d => { setDetail(d); setError(null); })
.catch(e => setError(String(e)))
.finally(() => setLoading(false));
}, [hash]);
- // After diff HTML is injected, run highlight.js over code blocks
- useEffect(() => {
- if (detail && diffRef.current) {
- diffRef.current.querySelectorAll('code[class]').forEach(el => {
- hljs.highlightElement(el);
- });
- }
- }, [detail]);
-
- const combinedPatch = detail?.files.map(f => f.patch).join('\n') ?? '';
- const diffHtml = combinedPatch
- ? diff2htmlHtml(combinedPatch, {
- drawFileList: true,
- matching: 'lines',
- outputFormat: 'line-by-line',
- renderNothingWhenEmpty: false,
- })
- : '';
+ const totalAdds = detail?.files.reduce((a, f) => a + f.additions, 0) ?? 0;
+ const totalDels = detail?.files.reduce((a, f) => a + f.deletions, 0) ?? 0;
return (
- {detail.shortHash}
- {detail.subject}
-
- ) : 'Commit Diff'}
+ title={
+ detail ? (
+
+ {detail.shortHash}
+ {detail.subject}
+
+ ) : 'Commit Diff'
+ }
size="70%"
position="right"
className="git-commit-drawer"
>
-
- {loading &&
} title="Loading diff…" />}
- {error &&
}
+ {/* Scrollable body */}
+
+ {/* Loading overlay — keeps old content visible while fetching next */}
+ {loading && (
+
+
+
+ )}
- {detail && (
+ {error && (
+
+ )}
+
+ {!error && detail && (
<>
{/* Metadata bar */}
-
+
navigator.clipboard.writeText(detail.hash)}
@@ -81,26 +134,33 @@ export function GitCommitDrawer({ hash, onClose }: Props) {
{detail.date}
-
- +{detail.files.reduce((a, f) => a + f.additions, 0)}
+ {totalAdds > 0 && (
+ +{totalAdds}
+ )}
+ {totalDels > 0 && (
+ -{totalDels}
+ )}
+
+ {detail.files.length} file{detail.files.length !== 1 ? 's' : ''}
-
- -{detail.files.reduce((a, f) => a + f.deletions, 0)}
-
- {detail.files.length} file{detail.files.length !== 1 ? 's' : ''}
- {/* Commit body if multiline */}
+ {/* Extended commit message */}
{detail.body.trim() !== detail.subject.trim() && (
{detail.body.trim()}
)}
- {/* Diff */}
- {diffHtml
- ?
- :
- }
+ {/* Per-file diffs */}
+ {detail.files.length === 0 ? (
+
+ ) : (
+
+ {detail.files.map(f => (
+
+ ))}
+
+ )}
>
)}
@@ -109,7 +169,8 @@ export function GitCommitDrawer({ hash, onClose }: Props) {
)}
-
+ {/* Footer — sticky at bottom */}
+
diff --git a/clarity.controlplane/src/index.css b/clarity.controlplane/src/index.css
index 84300b7..b563624 100644
--- a/clarity.controlplane/src/index.css
+++ b/clarity.controlplane/src/index.css
@@ -850,10 +850,50 @@ body {
}
/* ── Git Commit Drawer ──────────────────────────────────────────────────────── */
-.git-commit-drawer .bp5-drawer-header {
+
+/* Drawer shell: full-height flex column */
+.git-commit-drawer.bp6-drawer {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ overflow: hidden;
+}
+
+.git-commit-drawer .bp6-drawer-header {
+ flex-shrink: 0;
padding: 0.75rem 1rem;
}
+/*
+ * .gcd-body is the scrollable content area.
+ * Blueprint v6 renders children directly inside .bp6-drawer — no body wrapper.
+ */
+.git-commit-drawer .gcd-body {
+ flex: 1 1 0; /* 0 basis — don't size from content, allow shrink */
+ min-height: 0; /* flex children won't shrink past content without this */
+ overflow-y: auto;
+ overflow-x: hidden;
+ padding: 1rem;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ position: relative; /* loading overlay anchor */
+}
+
+/* Children of the scroll container must NOT shrink — if they do, content
+ * never overflows and the scrollbar never appears. */
+.git-commit-drawer .gcd-body > * {
+ flex-shrink: 0;
+}
+
+/* Footer rendered as last child — sits below the scroll area */
+.git-commit-drawer .gcd-footer {
+ flex-shrink: 0;
+ padding: 0.5rem 1rem;
+ display: flex;
+ justify-content: flex-end;
+}
+
.git-drawer-title {
display: flex;
align-items: center;
@@ -871,6 +911,95 @@ body {
font-family: 'JetBrains Mono', 'Fira Code', monospace;
}
+/* Loading overlay — keeps old diff visible while fetching next commit */
+.gcd-loading-overlay {
+ position: absolute;
+ inset: 0;
+ background: rgba(255, 255, 255, 0.7);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 10;
+ pointer-events: none;
+}
+
+/* Per-file accordion */
+.gcd-files-list {
+ display: flex;
+ flex-direction: column;
+ gap: 0;
+ border: 1px solid #dce0e6;
+ border-radius: 6px;
+ overflow: hidden;
+ margin: 0.75rem 0;
+}
+
+.gcd-file-section {
+ border-bottom: 1px solid #dce0e6;
+}
+
+.gcd-file-section:last-child {
+ border-bottom: none;
+}
+
+.gcd-file-header {
+ all: unset;
+ box-sizing: border-box;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ width: 100%;
+ padding: 0.45rem 0.75rem;
+ background: #f6f8fa;
+ cursor: pointer;
+ user-select: none;
+ transition: background 0.1s;
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
+ font-size: 0.78rem;
+ color: #1c2127;
+}
+
+.gcd-file-header:hover,
+.gcd-file-header--open {
+ background: #edf2f7;
+}
+
+.gcd-file-chevron {
+ flex-shrink: 0;
+ color: #738091;
+}
+
+.gcd-file-status-icon {
+ flex-shrink: 0;
+}
+
+.gcd-file-path {
+ flex: 1;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ min-width: 0;
+}
+
+.gcd-file-stats {
+ display: flex;
+ gap: 0.4rem;
+ flex-shrink: 0;
+ font-size: 0.73rem;
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
+}
+
+.gcd-adds { color: #1a7f37; font-weight: 600; }
+.gcd-dels { color: #cf222e; font-weight: 600; }
+
+.gcd-no-diff {
+ padding: 0.6rem 1rem;
+ font-size: 0.8rem;
+ color: #738091;
+ font-style: italic;
+ background: #fafafa;
+}
+
.git-drawer-subject {
font-size: 0.92rem;
font-weight: 600;
@@ -880,21 +1009,6 @@ body {
color: #1c2127;
}
-.git-drawer-body {
- flex: 1;
- overflow-y: auto;
- padding: 1rem;
- display: flex;
- flex-direction: column;
- gap: 1rem;
-}
-
-.git-drawer-footer {
- padding: 0.75rem 1rem;
- border-top: 1px solid #d3d8de;
- display: flex;
- justify-content: flex-end;
-}
.git-commit-meta-bar {
display: flex;
@@ -961,7 +1075,8 @@ body {
font-size: 0.78rem;
line-height: 1.45;
border-radius: 6px;
- overflow: hidden;
+ overflow-x: auto; /* horizontal scroll for wide diffs, not clip */
+ overflow-y: visible;
border: 1px solid #d0d7de;
}