diff --git a/apps/web/src/components/AgentDetailsPanel.tsx b/apps/web/src/components/AgentDetailsPanel.tsx new file mode 100644 index 0000000..6086f3d --- /dev/null +++ b/apps/web/src/components/AgentDetailsPanel.tsx @@ -0,0 +1,230 @@ +import { useState, useEffect } from "react"; +import { Link } from "@tanstack/react-router"; +import { trpc } from "@/lib/trpc"; +import { cn } from "@/lib/utils"; +import { Skeleton } from "@/components/Skeleton"; +import { Button } from "@/components/ui/button"; +import { StatusDot } from "@/components/StatusDot"; +import { formatRelativeTime } from "@/lib/utils"; +import { modeLabel } from "@/lib/labels"; + +export function AgentDetailsPanel({ agentId }: { agentId: string }) { + return ( +
+
+

Metadata

+ +
+
+

Input Files

+ +
+
+

Effective Prompt

+ +
+
+ ); +} + +function MetadataSection({ agentId }: { agentId: string }) { + const query = trpc.getAgent.useQuery({ id: agentId }); + + if (query.isLoading) { + return ( +
+ {Array.from({ length: 5 }).map((_, i) => ( + + ))} +
+ ); + } + + if (query.isError) { + return ( +
+

{query.error.message}

+ +
+ ); + } + + const agent = query.data; + if (!agent) return null; + + const showExitCode = !['idle', 'running', 'waiting_for_input'].includes(agent.status); + + const rows: Array<{ label: string; value: React.ReactNode }> = [ + { + label: 'Status', + value: ( + + + {agent.status} + + ), + }, + { + label: 'Mode', + value: modeLabel(agent.mode), + }, + { + label: 'Provider', + value: agent.provider, + }, + { + label: 'Initiative', + value: agent.initiativeId ? ( + + {(agent as { initiativeName?: string | null }).initiativeName ?? agent.initiativeId} + + ) : '—', + }, + { + label: 'Task', + value: (agent as { taskName?: string | null }).taskName ?? (agent.taskId ? agent.taskId : '—'), + }, + { + label: 'Created', + value: formatRelativeTime(String(agent.createdAt)), + }, + ]; + + if (showExitCode) { + rows.push({ + label: 'Exit Code', + value: ( + + {agent.exitCode ?? '—'} + + ), + }); + } + + return ( +
+ {rows.map(({ label, value }) => ( +
+ {label} + {value} +
+ ))} +
+ ); +} + +function InputFilesSection({ agentId }: { agentId: string }) { + const query = trpc.getAgentInputFiles.useQuery({ id: agentId }); + const [selectedFile, setSelectedFile] = useState(null); + + useEffect(() => { + setSelectedFile(null); + }, [agentId]); + + useEffect(() => { + if (!query.data?.files) return; + if (selectedFile !== null) return; + const manifest = query.data.files.find(f => f.name === 'manifest.json'); + setSelectedFile(manifest?.name ?? query.data.files[0]?.name ?? null); + }, [query.data?.files]); + + if (query.isLoading) { + return ( +
+ + + +
+ ); + } + + if (query.isError) { + return ( +
+

{query.error.message}

+ +
+ ); + } + + const data = query.data; + if (!data) return null; + + if (data.reason === 'worktree_missing') { + return

Worktree no longer exists — input files unavailable

; + } + + if (data.reason === 'input_dir_missing') { + return

Input directory not found — this agent may not have received input files

; + } + + const { files } = data; + + if (files.length === 0) { + return

No input files found

; + } + + return ( +
+ {/* File list */} +
+ {files.map(file => ( + + ))} +
+ {/* Content pane */} +
+        {files.find(f => f.name === selectedFile)?.content ?? ''}
+      
+
+ ); +} + +function EffectivePromptSection({ agentId }: { agentId: string }) { + const query = trpc.getAgentPrompt.useQuery({ id: agentId }); + + if (query.isLoading) { + return ; + } + + if (query.isError) { + return ( +
+

{query.error.message}

+ +
+ ); + } + + const data = query.data; + if (!data) return null; + + if (data.reason === 'prompt_not_written') { + return

Prompt file not available — agent may have been spawned before this feature was added

; + } + + if (data.content) { + return ( +
+        {data.content}
+      
+ ); + } + + return null; +} diff --git a/apps/web/src/routes/agents.tsx b/apps/web/src/routes/agents.tsx index 95ff6ce..d03f7a5 100644 --- a/apps/web/src/routes/agents.tsx +++ b/apps/web/src/routes/agents.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { createFileRoute, useNavigate, useSearch } from "@tanstack/react-router"; import { motion } from "motion/react"; import { AlertCircle, RefreshCw, Terminal, Users } from "lucide-react"; @@ -9,8 +9,9 @@ import { Skeleton } from "@/components/Skeleton"; import { toast } from "sonner"; import { trpc } from "@/lib/trpc"; import { AgentOutputViewer } from "@/components/AgentOutputViewer"; +import { AgentDetailsPanel } from "@/components/AgentDetailsPanel"; import { AgentActions } from "@/components/AgentActions"; -import { formatRelativeTime } from "@/lib/utils"; +import { formatRelativeTime, cn } from "@/lib/utils"; import { modeLabel } from "@/lib/labels"; import { StatusDot } from "@/components/StatusDot"; import { useLiveUpdates } from "@/hooks"; @@ -29,7 +30,12 @@ export const Route = createFileRoute("/agents")({ function AgentsPage() { const [selectedAgentId, setSelectedAgentId] = useState(null); + const [activeTab, setActiveTab] = useState<'output' | 'details'>('output'); const { filter } = useSearch({ from: "/agents" }); + + useEffect(() => { + setActiveTab('output'); + }, [selectedAgentId]); const navigate = useNavigate(); // Live updates @@ -308,15 +314,49 @@ function AgentsPage() { )} - {/* Right: Output Viewer */} + {/* Right: Output/Details Viewer */}
{selectedAgent ? ( - +
+ {/* Tab bar */} +
+ + +
+ {/* Panel content */} +
+ {activeTab === 'output' ? ( + + ) : ( + + )} +
+
) : (
diff --git a/docs/frontend.md b/docs/frontend.md index 523bdbf..f538920 100644 --- a/docs/frontend.md +++ b/docs/frontend.md @@ -44,6 +44,7 @@ Use `mapEntityStatus(rawStatus)` from `StatusDot.tsx` to convert raw entity stat |-------|-----------|---------| | `/` | `routes/index.tsx` | Dashboard / initiative list | | `/initiatives/$id` | `routes/initiatives/$initiativeId.tsx` | Initiative detail (tabbed) | +| `/agents` | `routes/agents.tsx` | Agent list with Output / Details tab panel | | `/settings` | `routes/settings/index.tsx` | Settings page | ## Initiative Detail Tabs @@ -54,7 +55,7 @@ The initiative detail page has three tabs managed via local state (not URL param 2. **Execution Tab** — Pipeline visualization, phase management, task dispatch 3. **Review Tab** — Pending proposals from agents -## Component Inventory (73 components) +## Component Inventory (74 components) ### Core Components (`src/components/`) | Component | Purpose | @@ -66,6 +67,7 @@ The initiative detail page has three tabs managed via local state (not URL param | `StatusBadge` | Colored badge using status tokens | | `TaskRow` | Task list item with status, priority, category | | `QuestionForm` | Agent question form with options | +| `AgentDetailsPanel` | Details tab for agent right-panel: metadata, input files, effective prompt | | `InboxDetailPanel` | Agent message detail + response form | | `ProjectPicker` | Checkbox list for project selection | | `RegisterProjectDialog` | Dialog to register new git project |