diff --git a/backend/app/api/routes/lineage.py b/backend/app/api/routes/lineage.py index 1c3c826..a541219 100644 --- a/backend/app/api/routes/lineage.py +++ b/backend/app/api/routes/lineage.py @@ -91,6 +91,7 @@ class SessionNode(BaseModel): branch_name: str forked_from_checkpoint_id: Optional[uuid.UUID] seeded_commit_id: Optional[uuid.UUID] + branch_disposition: str = "active" created_at: datetime model_config = {"from_attributes": True} diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 63cb030..6bc75b8 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -357,6 +357,12 @@ export async function getLineage(spaceId: string): Promise(`/lineage/spaces/${spaceId}`); } +export async function getSpaceState(spaceId: string): Promise { + const resp = await fetch(`${API_BASE}/api/v4/chat/spaces/${spaceId}/state`); + if (!resp.ok) throw new Error(`Failed to fetch space state: ${resp.status}`); + return resp.json(); +} + export async function compareCheckpoints( aId: string, bId: string, diff --git a/frontend/src/pages/LineagePage.tsx b/frontend/src/pages/LineagePage.tsx index 6eee041..6dc8291 100644 --- a/frontend/src/pages/LineagePage.tsx +++ b/frontend/src/pages/LineagePage.tsx @@ -17,14 +17,15 @@ import { useEffect, useRef, useState } from 'react'; import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; -import { getLineage, compareCheckpoints, forkSession } from '../api/client'; +import { getLineage, getSpaceState, compareCheckpoints, forkSession } from '../api/client'; import type { + ActiveClaimSummary, CheckpointNode, SessionNode, LineageResponse, CompareResponse, } from '../types'; -import { GitBranch, GitCommit, Loader2, X, ArrowLeft } from 'lucide-react'; +import { GitBranch, GitCommit, Loader2, X, ArrowLeft, Zap } from 'lucide-react'; // ── Helpers ─────────────────────────────────────────────────────────────────── @@ -407,6 +408,7 @@ function BranchSection({ onNavigate, sessionRef, isMain, + disposition, }: { branchName: string; checkpoints: CheckpointNode[]; @@ -418,6 +420,7 @@ function BranchSection({ onNavigate: (sessionId: string) => void; sessionRef: React.RefObject; isMain: boolean; + disposition?: string; }) { const mostRecentCheckpoint = checkpoints[checkpoints.length - 1]; const activeSession = sessions.find(s => s.id === currentSessionId) ?? sessions[0] ?? null; @@ -441,6 +444,17 @@ function BranchSection({ forked from {forkSourceId.slice(0, 7)} )} + {!isMain && disposition && disposition !== 'active' && ( + + {disposition} + + )} {checkpoints.length > 0 ? ( @@ -562,6 +576,7 @@ export function LineagePage() { const currentSessionId = searchParams.get('session'); const [lineage, setLineage] = useState(null); + const [activeClaims, setActiveClaims] = useState([]); const [loading, setLoading] = useState(true); const [err, setErr] = useState(''); @@ -577,8 +592,14 @@ export function LineagePage() { useEffect(() => { if (!spaceId) return; - getLineage(spaceId) - .then(setLineage) + Promise.all([ + getLineage(spaceId), + getSpaceState(spaceId).catch(() => null), + ]) + .then(([lin, state]) => { + setLineage(lin); + setActiveClaims(state?.active_claims ?? []); + }) .catch(e => setErr(e.message || 'Failed to load lineage')) .finally(() => setLoading(false)); }, [spaceId]); @@ -682,6 +703,41 @@ export function LineagePage() { {lineage && !loading && (
+ {/* Active work claims */} + {activeClaims.length > 0 && ( +
+
+ +

Active work

+ {activeClaims.length} claim{activeClaims.length !== 1 ? 's' : ''} +
+
+ {activeClaims.map(claim => ( +
+
+ + {claim.agent} + + + {claim.intent_type} + + + on {claim.branch_name} + {claim.base_commit_hash && ( + <> from {claim.base_commit_hash} + )} + +
+

{claim.scope}

+
+ ))} +
+
+ )} + {/* Main branch */} {/* Fork branches */} - {forkBranches.map(branch => ( - navigate(`/sessions/${id}`)} - sessionRef={currentSessionRef} - isMain={false} - /> - ))} + {forkBranches.map(branch => { + // Derive disposition from sessions on this branch. + // If any session is "active", the branch is active. + // Otherwise use the most recent session's disposition. + const branchSessions = sessionsByBranch.get(branch) ?? []; + const hasActive = branchSessions.some(s => (s.branch_disposition || 'active') === 'active'); + const disposition = hasActive + ? 'active' + : (branchSessions[0]?.branch_disposition || 'active'); + + return ( + navigate(`/sessions/${id}`)} + sessionRef={currentSessionRef} + isMain={false} + disposition={disposition} + /> + ); + })}
)} diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 26ae477..7655130 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -208,9 +208,30 @@ export interface SessionNode { branch_name: string; forked_from_checkpoint_id: string | null; seeded_commit_id: string | null; + branch_disposition: string; created_at: string; } +export interface ActiveClaimSummary { + id: string; + agent: string; + branch_name: string; + scope: string; + intent_type: string; + claimed_at: string; + expires_at: string; + base_commit_hash: string | null; +} + +export interface SpaceStateResponse { + space: { id: string; name: string; description: string | null }; + head: any; + commit: any; + active_branches: any[]; + active_claims: ActiveClaimSummary[]; + divergence: any; +} + export interface LineageResponse { space_id: string; checkpoints: CheckpointNode[];