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 (
+
+
+
+
+
+ );
+}
+
+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 |