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:
@@ -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> => {
|
||||
|
||||
@@ -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,80 +162,107 @@ 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">
|
||||
{/* Metadata grid */}
|
||||
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||
<MetaField label="Status">
|
||||
<StatusBadge status={task.status} />
|
||||
</MetaField>
|
||||
<MetaField label="Category">
|
||||
<CategoryBadge category={task.category} />
|
||||
</MetaField>
|
||||
<MetaField label="Priority">
|
||||
<PriorityText priority={task.priority} />
|
||||
</MetaField>
|
||||
<MetaField label="Type">
|
||||
<span className="font-medium">{task.type}</span>
|
||||
</MetaField>
|
||||
<MetaField label="Agent" span={2}>
|
||||
<span className="font-medium">
|
||||
{selectedEntry.agentName ?? "Unassigned"}
|
||||
</span>
|
||||
</MetaField>
|
||||
</div>
|
||||
<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">
|
||||
<StatusBadge status={task.status} />
|
||||
</MetaField>
|
||||
<MetaField label="Category">
|
||||
<CategoryBadge category={task.category} />
|
||||
</MetaField>
|
||||
<MetaField label="Priority">
|
||||
<PriorityText priority={task.priority} />
|
||||
</MetaField>
|
||||
<MetaField label="Type">
|
||||
<span className="font-medium">{task.type}</span>
|
||||
</MetaField>
|
||||
<MetaField label="Agent" span={2}>
|
||||
<span className="font-medium">
|
||||
{selectedEntry.agentName ?? "Unassigned"}
|
||||
</span>
|
||||
</MetaField>
|
||||
</div>
|
||||
|
||||
{/* Description — editable tiptap */}
|
||||
<Section title="Description">
|
||||
<TiptapEditor
|
||||
entityId={task.id}
|
||||
content={editorContent}
|
||||
onUpdate={handleDescriptionUpdate}
|
||||
enablePageLinks={false}
|
||||
/>
|
||||
</Section>
|
||||
{/* Description — editable tiptap */}
|
||||
<Section title="Description">
|
||||
<TiptapEditor
|
||||
entityId={task.id}
|
||||
content={editorContent}
|
||||
onUpdate={handleDescriptionUpdate}
|
||||
enablePageLinks={false}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
{/* Dependencies */}
|
||||
<Section title="Blocked By">
|
||||
{dependencies.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">None</p>
|
||||
) : (
|
||||
<ul className="space-y-1.5">
|
||||
{dependencies.map((dep) => (
|
||||
<li
|
||||
key={dep.name}
|
||||
className="flex items-center gap-2 text-sm"
|
||||
>
|
||||
<StatusDot status={dep.status} size="sm" />
|
||||
<span className="min-w-0 flex-1 truncate">
|
||||
{dep.name}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</Section>
|
||||
{/* Dependencies */}
|
||||
<Section title="Blocked By">
|
||||
{dependencies.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">None</p>
|
||||
) : (
|
||||
<ul className="space-y-1.5">
|
||||
{dependencies.map((dep) => (
|
||||
<li
|
||||
key={dep.name}
|
||||
className="flex items-center gap-2 text-sm"
|
||||
>
|
||||
<StatusDot status={dep.status} size="sm" />
|
||||
<span className="min-w-0 flex-1 truncate">
|
||||
{dep.name}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</Section>
|
||||
|
||||
{/* Blocks */}
|
||||
<Section title="Blocks">
|
||||
{dependents.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">None</p>
|
||||
) : (
|
||||
<ul className="space-y-1.5">
|
||||
{dependents.map((dep) => (
|
||||
<li
|
||||
key={dep.name}
|
||||
className="flex items-center gap-2 text-sm"
|
||||
>
|
||||
<StatusDot status={dep.status} size="sm" />
|
||||
<span className="min-w-0 flex-1 truncate">
|
||||
{dep.name}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</Section>
|
||||
{/* Blocks */}
|
||||
<Section title="Blocks">
|
||||
{dependents.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">None</p>
|
||||
) : (
|
||||
<ul className="space-y-1.5">
|
||||
{dependents.map((dep) => (
|
||||
<li
|
||||
key={dep.name}
|
||||
className="flex items-center gap-2 text-sm"
|
||||
>
|
||||
<StatusDot status={dep.status} size="sm" />
|
||||
<span className="min-w-0 flex-1 truncate">
|
||||
{dep.name}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</Section>
|
||||
</div>
|
||||
) : (
|
||||
<AgentLogsTab taskId={task.id} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
@@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user