'use client' import { useCallback, useEffect, useState } from 'react' import Link from 'next/link' import { type RepoStatus, parseGitStatus } from '@/lib/parse-git' import { useFlowRun } from '@/hooks/useFlowRun' import ConfirmDialog from '@/components/ConfirmDialog' import StreamingTerminal from '@/components/StreamingTerminal' interface RepoEntry { path: string name: string status: RepoStatus | null error: string | null } async function fetchRepoStatus(repoPath: string): Promise { const res = await fetch('/api/agent/exec', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ command_key: 'git_status', args: [repoPath] }), }) if (!res.ok) { const text = await res.text() throw new Error(`agent ${res.status}: ${text}`) } const reader = res.body?.getReader() if (!reader) throw new Error('no response body') const decoder = new TextDecoder() let buffer = '' let output = '' while (true) { const { done, value } = await reader.read() if (done) break buffer += decoder.decode(value, { stream: true }) const lines = buffer.split('\n') buffer = lines.pop() ?? '' for (const line of lines) { if (line.startsWith('data:')) { try { const parsed = JSON.parse(line.slice(5).trim()) as { data?: string } if (parsed.data !== undefined) output += parsed.data } catch { // ignore malformed } } } } return parseGitStatus(output) } function statusBadge(status: RepoStatus) { if (status.dirty) { return ( dirty ) } if (status.behind && status.behind > 0) { return ( {status.behind} behind ) } return ( clean ) } type ActionDef = { commandKey: string args: string[] preview: string title: string } type Props = { initialRepos: RepoEntry[] } export default function GitReposList({ initialRepos }: Props) { const [repos, setRepos] = useState(initialRepos) const [refreshing, setRefreshing] = useState(false) const [lastUpdated, setLastUpdated] = useState(new Date()) const [pendingAction, setPendingAction] = useState(null) const flowRun = useFlowRun() const refresh = useCallback(async () => { setRefreshing(true) try { const updated = await Promise.all( initialRepos.map(async (r) => { try { const status = await fetchRepoStatus(r.path) return { ...r, status, error: null } } catch (err) { return { ...r, status: null, error: err instanceof Error ? err.message : 'failed' } } }), ) setRepos(updated) setLastUpdated(new Date()) } finally { setRefreshing(false) } }, [initialRepos]) useEffect(() => { const id = setInterval(refresh, 30000) return () => clearInterval(id) }, [refresh]) const handleConfirm = useCallback(() => { if (!pendingAction) return flowRun.start(pendingAction.commandKey, pendingAction.args) setPendingAction(null) }, [pendingAction, flowRun.start]) return (
{repos.length} repo{repos.length !== 1 ? 's' : ''} {refreshing && ( refreshing… )}
updated {lastUpdated.toLocaleTimeString()}
{repos.map((r) => ( ))}
Repo Branch Status Ahead Path Actions
{r.name} {r.status?.branch ?? '—'} {r.error ? ( {r.error} ) : r.status ? ( statusBadge(r.status) ) : ( )} {r.status?.ahead !== undefined && r.status.ahead > 0 ? ( ↑{r.status.ahead} ) : ( '—' )} {r.path}
{flowRun.status !== 'idle' && (
Output {flowRun.status !== 'running' && ( )}
)} setPendingAction(null)} />
) }