feat: Add Agent Logs tab to task slide-over

Add getTaskAgent tRPC procedure to find the most recent agent for a task.
TaskSlideOver now has Details/Agent Logs tabs — logs tab renders
AgentOutputViewer when an agent exists, or an empty state otherwise.
This commit is contained in:
Lukas May
2026-03-06 13:10:46 +01:00
parent deffdc7c4f
commit 3f3954672e
2 changed files with 156 additions and 71 deletions

View File

@@ -184,6 +184,17 @@ export function agentProcedures(publicProcedure: ProcedureBuilder) {
return candidates[0] ?? null;
}),
getTaskAgent: publicProcedure
.input(z.object({ taskId: z.string().min(1) }))
.query(async ({ ctx, input }): Promise<AgentInfo | null> => {
const agentManager = requireAgentManager(ctx);
const all = await agentManager.list();
const matches = all
.filter(a => a.taskId === input.taskId)
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
return matches[0] ?? null;
}),
getActiveConflictAgent: publicProcedure
.input(z.object({ initiativeId: z.string().min(1) }))
.query(async ({ ctx, input }): Promise<AgentInfo | null> => {

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef, useMemo } from "react";
import { useCallback, useEffect, useRef, useMemo, useState } from "react";
import { motion, AnimatePresence } from "motion/react";
import { X, Trash2, MessageCircle, RotateCw } from "lucide-react";
import type { ChatTarget } from "@/components/chat/ChatSlideOver";
@@ -7,12 +7,15 @@ import { Button } from "@/components/ui/button";
import { StatusBadge } from "@/components/StatusBadge";
import { StatusDot } from "@/components/StatusDot";
import { TiptapEditor } from "@/components/editor/TiptapEditor";
import { AgentOutputViewer } from "@/components/AgentOutputViewer";
import { getCategoryConfig } from "@/lib/category";
import { markdownToTiptapJson } from "@/lib/markdown-to-tiptap";
import { useExecutionContext } from "./ExecutionContext";
import { trpc } from "@/lib/trpc";
import { cn } from "@/lib/utils";
type SlideOverTab = "details" | "logs";
interface TaskSlideOverProps {
onOpenChat?: (target: ChatTarget) => void;
}
@@ -24,8 +27,15 @@ export function TaskSlideOver({ onOpenChat }: TaskSlideOverProps) {
const deleteTaskMutation = trpc.deleteTask.useMutation();
const updateTaskMutation = trpc.updateTask.useMutation();
const [tab, setTab] = useState<SlideOverTab>("details");
const close = useCallback(() => setSelectedTaskId(null), [setSelectedTaskId]);
// Reset tab when task changes
useEffect(() => {
setTab("details");
}, [selectedEntry?.task?.id]);
// Escape key closes
useEffect(() => {
if (!selectedEntry) return;
@@ -152,8 +162,31 @@ export function TaskSlideOver({ onOpenChat }: TaskSlideOverProps) {
</button>
</div>
{/* Tab bar */}
<div className="flex gap-4 border-b border-border px-5">
{(["details", "logs"] as const).map((t) => (
<button
key={t}
className={cn(
"relative pb-2 pt-3 text-sm font-medium transition-colors",
tab === t
? "text-foreground"
: "text-muted-foreground hover:text-foreground",
)}
onClick={() => setTab(t)}
>
{t === "details" ? "Details" : "Agent Logs"}
{tab === t && (
<span className="absolute inset-x-0 bottom-0 h-0.5 bg-primary" />
)}
</button>
))}
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto px-5 py-4 space-y-5">
<div className="flex-1 overflow-y-auto">
{tab === "details" ? (
<div className="px-5 py-4 space-y-5">
{/* Metadata grid */}
<div className="grid grid-cols-2 gap-3 text-sm">
<MetaField label="Status">
@@ -227,6 +260,10 @@ export function TaskSlideOver({ onOpenChat }: TaskSlideOverProps) {
)}
</Section>
</div>
) : (
<AgentLogsTab taskId={task.id} />
)}
</div>
{/* Footer */}
<div className="flex items-center gap-2 border-t border-border px-5 py-3">
@@ -293,6 +330,43 @@ export function TaskSlideOver({ onOpenChat }: TaskSlideOverProps) {
);
}
// ---------------------------------------------------------------------------
// Agent Logs Tab
// ---------------------------------------------------------------------------
function AgentLogsTab({ taskId }: { taskId: string }) {
const { data: agent, isLoading } = trpc.getTaskAgent.useQuery(
{ taskId },
{ refetchOnWindowFocus: false },
);
if (isLoading) {
return (
<div className="flex items-center justify-center py-12 text-sm text-muted-foreground">
Loading...
</div>
);
}
if (!agent) {
return (
<div className="flex items-center justify-center py-12 text-sm text-muted-foreground">
No agent has been assigned to this task yet.
</div>
);
}
return (
<div className="h-full">
<AgentOutputViewer
agentId={agent.id}
agentName={agent.name ?? undefined}
status={agent.status}
/>
</div>
);
}
// ---------------------------------------------------------------------------
// Small helpers
// ---------------------------------------------------------------------------