mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-28 21:57:13 +02:00
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:
@@ -265,6 +265,9 @@ export default function App() {
|
|||||||
onSetSessionArchived={(sessionId, isArchived) => {
|
onSetSessionArchived={(sessionId, isArchived) => {
|
||||||
void api.setSessionArchived({ sessionId, isArchived });
|
void api.setSessionArchived({ sessionId, isArchived });
|
||||||
}}
|
}}
|
||||||
|
onRefreshGitContext={(projectId) => {
|
||||||
|
void api.refreshProjectGitContext(projectId);
|
||||||
|
}}
|
||||||
workspace={workspace}
|
workspace={workspace}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { type KeyboardEvent, useEffect, useRef, useState } from 'react';
|
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 { MarkdownContent } from '@renderer/components/MarkdownContent';
|
||||||
import { getAssistantMessagePhase } from '@renderer/lib/messagePhase';
|
import { getAssistantMessagePhase } from '@renderer/lib/messagePhase';
|
||||||
@@ -318,7 +318,20 @@ export function ChatPane({
|
|||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<h2 className="truncate text-[13px] font-semibold leading-tight text-zinc-100">{session.title}</h2>
|
<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">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
|
AlertTriangle,
|
||||||
Archive,
|
Archive,
|
||||||
ArrowLeftRight,
|
ArrowLeftRight,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
|
Circle,
|
||||||
Copy,
|
Copy,
|
||||||
FolderOpen,
|
FolderOpen,
|
||||||
|
GitBranch,
|
||||||
GitFork,
|
GitFork,
|
||||||
ListOrdered,
|
ListOrdered,
|
||||||
Lock,
|
Lock,
|
||||||
@@ -14,6 +17,7 @@ import {
|
|||||||
Pencil,
|
Pencil,
|
||||||
Pin,
|
Pin,
|
||||||
Plus,
|
Plus,
|
||||||
|
RefreshCw,
|
||||||
Search,
|
Search,
|
||||||
Settings,
|
Settings,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
@@ -23,7 +27,7 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
import type { OrchestrationMode, PatternDefinition } from '@shared/domain/pattern';
|
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 type { SessionRecord } from '@shared/domain/session';
|
||||||
import { querySessions } from '@shared/domain/sessionLibrary';
|
import { querySessions } from '@shared/domain/sessionLibrary';
|
||||||
import type { WorkspaceState } from '@shared/domain/workspace';
|
import type { WorkspaceState } from '@shared/domain/workspace';
|
||||||
@@ -39,6 +43,7 @@ interface SidebarProps {
|
|||||||
onDuplicateSession: (sessionId: string) => void;
|
onDuplicateSession: (sessionId: string) => void;
|
||||||
onSetSessionPinned: (sessionId: string, isPinned: boolean) => void;
|
onSetSessionPinned: (sessionId: string, isPinned: boolean) => void;
|
||||||
onSetSessionArchived: (sessionId: string, isArchived: boolean) => void;
|
onSetSessionArchived: (sessionId: string, isArchived: boolean) => void;
|
||||||
|
onRefreshGitContext: (projectId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Mode icon + accent colour mapping ─────────────────────── */
|
/* ── Mode icon + accent colour mapping ─────────────────────── */
|
||||||
@@ -68,6 +73,53 @@ function relativeTime(iso: string): string {
|
|||||||
return new Date(iso).toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
|
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 ─────────────────────────────────────── */
|
/* ── Context menu item ─────────────────────────────────────── */
|
||||||
|
|
||||||
function ActionMenuItem({
|
function ActionMenuItem({
|
||||||
@@ -257,6 +309,7 @@ function ProjectGroup({
|
|||||||
onOpenMenu,
|
onOpenMenu,
|
||||||
onRenameSubmit,
|
onRenameSubmit,
|
||||||
onRenameCancel,
|
onRenameCancel,
|
||||||
|
onRefreshGitContext,
|
||||||
}: {
|
}: {
|
||||||
project: ProjectRecord;
|
project: ProjectRecord;
|
||||||
sessions: SessionRecord[];
|
sessions: SessionRecord[];
|
||||||
@@ -267,7 +320,8 @@ function ProjectGroup({
|
|||||||
onOpenMenu: (sessionId: string, e: React.MouseEvent) => void;
|
onOpenMenu: (sessionId: string, e: React.MouseEvent) => void;
|
||||||
onRenameSubmit: (sessionId: string, title: string) => void;
|
onRenameSubmit: (sessionId: string, title: string) => void;
|
||||||
onRenameCancel: () => void;
|
onRenameCancel: () => void;
|
||||||
}) {
|
onRefreshGitContext?: (projectId: string) => void;
|
||||||
|
}){
|
||||||
const [expanded, setExpanded] = useState(true);
|
const [expanded, setExpanded] = useState(true);
|
||||||
const isScratchpad = isScratchpadProject(project);
|
const isScratchpad = isScratchpadProject(project);
|
||||||
|
|
||||||
@@ -309,7 +363,24 @@ function ProjectGroup({
|
|||||||
)}
|
)}
|
||||||
<span className="truncate">{project.name}</span>
|
<span className="truncate">{project.name}</span>
|
||||||
|
|
||||||
|
{!isScratchpad && project.git && (
|
||||||
|
<GitContextBadge git={project.git} />
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="ml-auto flex items-center gap-1.5">
|
<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 && (
|
{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="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" />
|
<span className="size-1.5 rounded-full bg-blue-400 sidebar-pulse" />
|
||||||
@@ -362,6 +433,7 @@ export function Sidebar({
|
|||||||
onDuplicateSession,
|
onDuplicateSession,
|
||||||
onSetSessionPinned,
|
onSetSessionPinned,
|
||||||
onSetSessionArchived,
|
onSetSessionArchived,
|
||||||
|
onRefreshGitContext,
|
||||||
}: SidebarProps) {
|
}: SidebarProps) {
|
||||||
const scratchpadProject = workspace.projects.find((project) => isScratchpadProject(project));
|
const scratchpadProject = workspace.projects.find((project) => isScratchpadProject(project));
|
||||||
const userProjects = workspace.projects.filter((project) => !isScratchpadProject(project));
|
const userProjects = workspace.projects.filter((project) => !isScratchpadProject(project));
|
||||||
@@ -570,6 +642,7 @@ export function Sidebar({
|
|||||||
onOpenMenu={handleOpenMenu}
|
onOpenMenu={handleOpenMenu}
|
||||||
onRenameSubmit={handleRenameSubmit}
|
onRenameSubmit={handleRenameSubmit}
|
||||||
onRenameCancel={() => setRenamingSessionId(undefined)}
|
onRenameCancel={() => setRenamingSessionId(undefined)}
|
||||||
|
onRefreshGitContext={onRefreshGitContext}
|
||||||
renamingSessionId={renamingSessionId}
|
renamingSessionId={renamingSessionId}
|
||||||
patterns={workspace.patterns}
|
patterns={workspace.patterns}
|
||||||
project={project}
|
project={project}
|
||||||
|
|||||||
Reference in New Issue
Block a user