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;
|
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
|
getActiveConflictAgent: publicProcedure
|
||||||
.input(z.object({ initiativeId: z.string().min(1) }))
|
.input(z.object({ initiativeId: z.string().min(1) }))
|
||||||
.query(async ({ ctx, input }): Promise<AgentInfo | null> => {
|
.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 { motion, AnimatePresence } from "motion/react";
|
||||||
import { X, Trash2, MessageCircle, RotateCw } from "lucide-react";
|
import { X, Trash2, MessageCircle, RotateCw } from "lucide-react";
|
||||||
import type { ChatTarget } from "@/components/chat/ChatSlideOver";
|
import type { ChatTarget } from "@/components/chat/ChatSlideOver";
|
||||||
@@ -7,12 +7,15 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { StatusBadge } from "@/components/StatusBadge";
|
import { StatusBadge } from "@/components/StatusBadge";
|
||||||
import { StatusDot } from "@/components/StatusDot";
|
import { StatusDot } from "@/components/StatusDot";
|
||||||
import { TiptapEditor } from "@/components/editor/TiptapEditor";
|
import { TiptapEditor } from "@/components/editor/TiptapEditor";
|
||||||
|
import { AgentOutputViewer } from "@/components/AgentOutputViewer";
|
||||||
import { getCategoryConfig } from "@/lib/category";
|
import { getCategoryConfig } from "@/lib/category";
|
||||||
import { markdownToTiptapJson } from "@/lib/markdown-to-tiptap";
|
import { markdownToTiptapJson } from "@/lib/markdown-to-tiptap";
|
||||||
import { useExecutionContext } from "./ExecutionContext";
|
import { useExecutionContext } from "./ExecutionContext";
|
||||||
import { trpc } from "@/lib/trpc";
|
import { trpc } from "@/lib/trpc";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
type SlideOverTab = "details" | "logs";
|
||||||
|
|
||||||
interface TaskSlideOverProps {
|
interface TaskSlideOverProps {
|
||||||
onOpenChat?: (target: ChatTarget) => void;
|
onOpenChat?: (target: ChatTarget) => void;
|
||||||
}
|
}
|
||||||
@@ -24,8 +27,15 @@ export function TaskSlideOver({ onOpenChat }: TaskSlideOverProps) {
|
|||||||
const deleteTaskMutation = trpc.deleteTask.useMutation();
|
const deleteTaskMutation = trpc.deleteTask.useMutation();
|
||||||
const updateTaskMutation = trpc.updateTask.useMutation();
|
const updateTaskMutation = trpc.updateTask.useMutation();
|
||||||
|
|
||||||
|
const [tab, setTab] = useState<SlideOverTab>("details");
|
||||||
|
|
||||||
const close = useCallback(() => setSelectedTaskId(null), [setSelectedTaskId]);
|
const close = useCallback(() => setSelectedTaskId(null), [setSelectedTaskId]);
|
||||||
|
|
||||||
|
// Reset tab when task changes
|
||||||
|
useEffect(() => {
|
||||||
|
setTab("details");
|
||||||
|
}, [selectedEntry?.task?.id]);
|
||||||
|
|
||||||
// Escape key closes
|
// Escape key closes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedEntry) return;
|
if (!selectedEntry) return;
|
||||||
@@ -152,80 +162,107 @@ export function TaskSlideOver({ onOpenChat }: TaskSlideOverProps) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</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 */}
|
{/* Content */}
|
||||||
<div className="flex-1 overflow-y-auto px-5 py-4 space-y-5">
|
<div className="flex-1 overflow-y-auto">
|
||||||
{/* Metadata grid */}
|
{tab === "details" ? (
|
||||||
<div className="grid grid-cols-2 gap-3 text-sm">
|
<div className="px-5 py-4 space-y-5">
|
||||||
<MetaField label="Status">
|
{/* Metadata grid */}
|
||||||
<StatusBadge status={task.status} />
|
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||||
</MetaField>
|
<MetaField label="Status">
|
||||||
<MetaField label="Category">
|
<StatusBadge status={task.status} />
|
||||||
<CategoryBadge category={task.category} />
|
</MetaField>
|
||||||
</MetaField>
|
<MetaField label="Category">
|
||||||
<MetaField label="Priority">
|
<CategoryBadge category={task.category} />
|
||||||
<PriorityText priority={task.priority} />
|
</MetaField>
|
||||||
</MetaField>
|
<MetaField label="Priority">
|
||||||
<MetaField label="Type">
|
<PriorityText priority={task.priority} />
|
||||||
<span className="font-medium">{task.type}</span>
|
</MetaField>
|
||||||
</MetaField>
|
<MetaField label="Type">
|
||||||
<MetaField label="Agent" span={2}>
|
<span className="font-medium">{task.type}</span>
|
||||||
<span className="font-medium">
|
</MetaField>
|
||||||
{selectedEntry.agentName ?? "Unassigned"}
|
<MetaField label="Agent" span={2}>
|
||||||
</span>
|
<span className="font-medium">
|
||||||
</MetaField>
|
{selectedEntry.agentName ?? "Unassigned"}
|
||||||
</div>
|
</span>
|
||||||
|
</MetaField>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Description — editable tiptap */}
|
{/* Description — editable tiptap */}
|
||||||
<Section title="Description">
|
<Section title="Description">
|
||||||
<TiptapEditor
|
<TiptapEditor
|
||||||
entityId={task.id}
|
entityId={task.id}
|
||||||
content={editorContent}
|
content={editorContent}
|
||||||
onUpdate={handleDescriptionUpdate}
|
onUpdate={handleDescriptionUpdate}
|
||||||
enablePageLinks={false}
|
enablePageLinks={false}
|
||||||
/>
|
/>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
{/* Dependencies */}
|
{/* Dependencies */}
|
||||||
<Section title="Blocked By">
|
<Section title="Blocked By">
|
||||||
{dependencies.length === 0 ? (
|
{dependencies.length === 0 ? (
|
||||||
<p className="text-sm text-muted-foreground">None</p>
|
<p className="text-sm text-muted-foreground">None</p>
|
||||||
) : (
|
) : (
|
||||||
<ul className="space-y-1.5">
|
<ul className="space-y-1.5">
|
||||||
{dependencies.map((dep) => (
|
{dependencies.map((dep) => (
|
||||||
<li
|
<li
|
||||||
key={dep.name}
|
key={dep.name}
|
||||||
className="flex items-center gap-2 text-sm"
|
className="flex items-center gap-2 text-sm"
|
||||||
>
|
>
|
||||||
<StatusDot status={dep.status} size="sm" />
|
<StatusDot status={dep.status} size="sm" />
|
||||||
<span className="min-w-0 flex-1 truncate">
|
<span className="min-w-0 flex-1 truncate">
|
||||||
{dep.name}
|
{dep.name}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
{/* Blocks */}
|
{/* Blocks */}
|
||||||
<Section title="Blocks">
|
<Section title="Blocks">
|
||||||
{dependents.length === 0 ? (
|
{dependents.length === 0 ? (
|
||||||
<p className="text-sm text-muted-foreground">None</p>
|
<p className="text-sm text-muted-foreground">None</p>
|
||||||
) : (
|
) : (
|
||||||
<ul className="space-y-1.5">
|
<ul className="space-y-1.5">
|
||||||
{dependents.map((dep) => (
|
{dependents.map((dep) => (
|
||||||
<li
|
<li
|
||||||
key={dep.name}
|
key={dep.name}
|
||||||
className="flex items-center gap-2 text-sm"
|
className="flex items-center gap-2 text-sm"
|
||||||
>
|
>
|
||||||
<StatusDot status={dep.status} size="sm" />
|
<StatusDot status={dep.status} size="sm" />
|
||||||
<span className="min-w-0 flex-1 truncate">
|
<span className="min-w-0 flex-1 truncate">
|
||||||
{dep.name}
|
{dep.name}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
</Section>
|
</Section>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<AgentLogsTab taskId={task.id} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* 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
|
// Small helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user