feat: surface git context in sidebar and chat header

- Add GitContextBadge to sidebar ProjectGroup showing branch name,
  dirty indicator, and ahead/behind counts for real projects
- Show git-missing and error states with subtle warning badges
- Add hover-reveal refresh button per project to re-scan git status
- Show branch/dirty/ahead/behind inline in ChatPane header subtitle
- Wire refreshProjectGitContext callback from App through Sidebar

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-03-23 20:16:22 +01:00
co-authored by Copilot
parent ab4e9bcea9
commit c888c9fb9b
3 changed files with 93 additions and 4 deletions
+3
View File
@@ -265,6 +265,9 @@ export default function App() {
onSetSessionArchived={(sessionId, isArchived) => {
void api.setSessionArchived({ sessionId, isArchived });
}}
onRefreshGitContext={(projectId) => {
void api.refreshProjectGitContext(projectId);
}}
workspace={workspace}
/>
}
+15 -2
View File
@@ -1,5 +1,5 @@
import { type KeyboardEvent, useEffect, useRef, useState } from 'react';
import { AlertCircle, ArrowUp, Bot, ChevronDown, Loader2, Sparkles, User } from 'lucide-react';
import { AlertCircle, ArrowUp, Bot, ChevronDown, Circle, GitBranch, Loader2, Sparkles, User } from 'lucide-react';
import { MarkdownContent } from '@renderer/components/MarkdownContent';
import { getAssistantMessagePhase } from '@renderer/lib/messagePhase';
@@ -318,7 +318,20 @@ export function ChatPane({
<div className="min-w-0">
<h2 className="truncate text-[13px] font-semibold leading-tight text-zinc-100">{session.title}</h2>
<p className="truncate text-[11px] leading-tight text-zinc-500">
{isScratchpad ? `Scratchpad · ${pattern.name}` : `${project.name} · ${pattern.name} · ${pattern.mode}`}
{isScratchpad
? `Scratchpad · ${pattern.name}`
: `${project.name} · ${pattern.name} · ${pattern.mode}`}
{!isScratchpad && project.git?.status === 'ready' && (
<span className="ml-2 inline-flex items-center gap-1 text-zinc-600">
<GitBranch className="inline size-2.5" />
{project.git.branch ?? project.git.head?.shortHash ?? 'HEAD'}
{project.git.isDirty && (
<Circle className="inline size-1.5 fill-amber-500 text-amber-500" />
)}
{(project.git.ahead ?? 0) > 0 && <span>{project.git.ahead}</span>}
{(project.git.behind ?? 0) > 0 && <span>{project.git.behind}</span>}
</span>
)}
</p>
</div>
<div className="flex items-center gap-2">
+75 -2
View File
@@ -1,11 +1,14 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import {
AlertTriangle,
Archive,
ArrowLeftRight,
ChevronDown,
ChevronRight,
Circle,
Copy,
FolderOpen,
GitBranch,
GitFork,
ListOrdered,
Lock,
@@ -14,6 +17,7 @@ import {
Pencil,
Pin,
Plus,
RefreshCw,
Search,
Settings,
Sparkles,
@@ -23,7 +27,7 @@ import {
} from 'lucide-react';
import type { OrchestrationMode, PatternDefinition } from '@shared/domain/pattern';
import { isScratchpadProject, type ProjectRecord } from '@shared/domain/project';
import { isScratchpadProject, type ProjectRecord, type ProjectGitContext } from '@shared/domain/project';
import type { SessionRecord } from '@shared/domain/session';
import { querySessions } from '@shared/domain/sessionLibrary';
import type { WorkspaceState } from '@shared/domain/workspace';
@@ -39,6 +43,7 @@ interface SidebarProps {
onDuplicateSession: (sessionId: string) => void;
onSetSessionPinned: (sessionId: string, isPinned: boolean) => void;
onSetSessionArchived: (sessionId: string, isArchived: boolean) => void;
onRefreshGitContext: (projectId: string) => void;
}
/* ── Mode icon + accent colour mapping ─────────────────────── */
@@ -68,6 +73,53 @@ function relativeTime(iso: string): string {
return new Date(iso).toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
}
/* ── Git context badge ──────────────────────────────────────── */
function GitContextBadge({ git }: { git: ProjectGitContext }) {
if (git.status === 'not-repository') {
return (
<span className="text-[10px] text-zinc-600" title="Not a git repository">
no repo
</span>
);
}
if (git.status === 'git-missing') {
return (
<span className="flex items-center gap-0.5 text-[10px] text-amber-500/70" title="Git is not installed">
<AlertTriangle className="size-2.5" />
no git
</span>
);
}
if (git.status === 'error') {
return (
<span
className="flex items-center gap-0.5 text-[10px] text-red-400/70"
title={git.errorMessage ?? 'Git error'}
>
<AlertTriangle className="size-2.5" />
error
</span>
);
}
const branchLabel = git.branch ?? git.head?.shortHash ?? 'HEAD';
const parts: string[] = [];
if (git.isDirty && git.changedFileCount) parts.push(`${git.changedFileCount} changed`);
if (git.ahead) parts.push(`${git.ahead}`);
if (git.behind) parts.push(`${git.behind}`);
return (
<span className="flex items-center gap-1 text-[10px] text-zinc-500" title={parts.join(' · ') || branchLabel}>
<GitBranch className="size-2.5 shrink-0" />
<span className="max-w-[80px] truncate">{branchLabel}</span>
{git.isDirty && <Circle className="size-1.5 shrink-0 fill-amber-500 text-amber-500" />}
</span>
);
}
/* ── Context menu item ─────────────────────────────────────── */
function ActionMenuItem({
@@ -257,6 +309,7 @@ function ProjectGroup({
onOpenMenu,
onRenameSubmit,
onRenameCancel,
onRefreshGitContext,
}: {
project: ProjectRecord;
sessions: SessionRecord[];
@@ -267,7 +320,8 @@ function ProjectGroup({
onOpenMenu: (sessionId: string, e: React.MouseEvent) => void;
onRenameSubmit: (sessionId: string, title: string) => void;
onRenameCancel: () => void;
}) {
onRefreshGitContext?: (projectId: string) => void;
}){
const [expanded, setExpanded] = useState(true);
const isScratchpad = isScratchpadProject(project);
@@ -309,7 +363,24 @@ function ProjectGroup({
)}
<span className="truncate">{project.name}</span>
{!isScratchpad && project.git && (
<GitContextBadge git={project.git} />
)}
<div className="ml-auto flex items-center gap-1.5">
{!isScratchpad && onRefreshGitContext && (
<span
className="flex size-5 items-center justify-center rounded text-zinc-600 opacity-0 transition hover:bg-zinc-700 hover:text-zinc-300 group-hover:opacity-100"
onClick={(e) => {
e.stopPropagation();
onRefreshGitContext(project.id);
}}
role="button"
title="Refresh git status"
>
<RefreshCw className="size-3" />
</span>
)}
{runningCount > 0 && (
<span className="flex items-center gap-1 rounded-full bg-blue-500/10 px-1.5 py-0.5 text-[10px] font-medium text-blue-400">
<span className="size-1.5 rounded-full bg-blue-400 sidebar-pulse" />
@@ -362,6 +433,7 @@ export function Sidebar({
onDuplicateSession,
onSetSessionPinned,
onSetSessionArchived,
onRefreshGitContext,
}: SidebarProps) {
const scratchpadProject = workspace.projects.find((project) => isScratchpadProject(project));
const userProjects = workspace.projects.filter((project) => !isScratchpadProject(project));
@@ -570,6 +642,7 @@ export function Sidebar({
onOpenMenu={handleOpenMenu}
onRenameSubmit={handleRenameSubmit}
onRenameCancel={() => setRenamingSessionId(undefined)}
onRefreshGitContext={onRefreshGitContext}
renamingSessionId={renamingSessionId}
patterns={workspace.patterns}
project={project}