Add isAgentRunning prop to all four radar drilldown dialog components. When true, subscribe to relevant SSE events and trigger refetch on matching events for the current agentId. Show a "Last refreshed: just now" timestamp that ticks to "Xs ago" in the dialog footer. Reset on close. - CompactionEventsDialog, SubagentSpawnsDialog, QuestionsAskedDialog: subscribe to agent:waiting events - InterAgentMessagesDialog: subscribe to conversation:created and conversation:answered events (matches on fromAgentId) - Update DrilldownDialogProps type with isAgentRunning?: boolean - Add test coverage for all new behavior across all four dialogs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
202 lines
7.1 KiB
TypeScript
202 lines
7.1 KiB
TypeScript
import { useState, useEffect, Fragment } from 'react'
|
|
import { trpc } from '@/lib/trpc'
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog'
|
|
import { Skeleton } from '@/components/ui/skeleton'
|
|
import { useSubscriptionWithErrorHandling } from '@/hooks'
|
|
import type { DrilldownDialogProps } from './types'
|
|
|
|
const RELEVANT_EVENTS = ['agent:waiting']
|
|
|
|
function formatTimestamp(ts: string): string {
|
|
const date = new Date(ts)
|
|
const now = new Date()
|
|
const diffMs = now.getTime() - date.getTime()
|
|
const diffSecs = Math.floor(diffMs / 1000)
|
|
const diffMins = Math.floor(diffSecs / 60)
|
|
const diffHours = Math.floor(diffMins / 60)
|
|
const diffDays = Math.floor(diffHours / 24)
|
|
|
|
let relative: string
|
|
if (diffSecs < 60) {
|
|
relative = `${diffSecs}s ago`
|
|
} else if (diffMins < 60) {
|
|
relative = `${diffMins}m ago`
|
|
} else if (diffHours < 24) {
|
|
relative = `${diffHours}h ago`
|
|
} else {
|
|
relative = `${diffDays}d ago`
|
|
}
|
|
|
|
const absolute = date.toLocaleString('en-US', {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
year: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
second: '2-digit',
|
|
hour12: false,
|
|
})
|
|
|
|
return `${relative} · ${absolute}`
|
|
}
|
|
|
|
function truncate(text: string, max: number): string {
|
|
return text.length > max ? text.slice(0, max) + '…' : text
|
|
}
|
|
|
|
export function QuestionsAskedDialog({
|
|
open,
|
|
onOpenChange,
|
|
agentId,
|
|
agentName,
|
|
isAgentRunning,
|
|
}: DrilldownDialogProps) {
|
|
const [expandedIndex, setExpandedIndex] = useState<number | null>(null)
|
|
|
|
const { data, isLoading, refetch } = trpc.agent.getQuestionsAsked.useQuery(
|
|
{ agentId },
|
|
{ enabled: open }
|
|
)
|
|
|
|
const [lastRefreshedAt, setLastRefreshedAt] = useState<Date | null>(null)
|
|
const [secondsAgo, setSecondsAgo] = useState(0)
|
|
|
|
useSubscriptionWithErrorHandling(
|
|
() => trpc.onEvent.useSubscription(undefined),
|
|
{
|
|
enabled: open && !!isAgentRunning,
|
|
onData: (event: any) => {
|
|
const eventType: string = event?.data?.type ?? event?.type ?? ''
|
|
const eventAgentId: string = event?.data?.agentId ?? event?.agentId ?? ''
|
|
if (RELEVANT_EVENTS.some(e => eventType.startsWith(e)) && eventAgentId === agentId) {
|
|
void refetch().then(() => {
|
|
setLastRefreshedAt(new Date())
|
|
setSecondsAgo(0)
|
|
})
|
|
}
|
|
},
|
|
}
|
|
)
|
|
|
|
useEffect(() => {
|
|
if (!open || !isAgentRunning || !lastRefreshedAt) return
|
|
const interval = setInterval(() => {
|
|
setSecondsAgo(Math.floor((Date.now() - lastRefreshedAt.getTime()) / 1000))
|
|
}, 1000)
|
|
return () => clearInterval(interval)
|
|
}, [open, isAgentRunning, lastRefreshedAt])
|
|
|
|
useEffect(() => {
|
|
if (!open) {
|
|
setExpandedIndex(null)
|
|
setLastRefreshedAt(null)
|
|
setSecondsAgo(0)
|
|
}
|
|
}, [open])
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="max-w-3xl">
|
|
<DialogHeader>
|
|
<DialogTitle>{`Questions Asked — ${agentName}`}</DialogTitle>
|
|
<DialogDescription>
|
|
Each row is a question this agent sent to the user via the AskUserQuestion tool.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="max-h-[70vh] overflow-y-auto">
|
|
{isLoading ? (
|
|
<div className="space-y-2">
|
|
{[0, 1, 2].map((i) => (
|
|
<div key={i}>
|
|
<div className="flex items-center gap-4 py-2">
|
|
<Skeleton className="w-28 h-4" />
|
|
<Skeleton className="w-20 h-4" />
|
|
<Skeleton className="w-36 h-4" />
|
|
</div>
|
|
{i < 2 && <div className="border-b" />}
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : !data || data.length === 0 ? (
|
|
<p className="text-center text-muted-foreground py-8">No data found</p>
|
|
) : (
|
|
<>
|
|
{data.length >= 200 && (
|
|
<p className="text-sm text-muted-foreground mb-2">Showing first 200 instances.</p>
|
|
)}
|
|
<table className="w-full text-sm">
|
|
<thead>
|
|
<tr className="border-b">
|
|
<th className="text-left py-2 pr-4 font-medium">Timestamp</th>
|
|
<th className="text-left py-2 pr-4 font-medium"># Questions</th>
|
|
<th className="text-left py-2 font-medium">First Question Header</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{data.map((row, i) => {
|
|
const n = row.questions.length
|
|
const countLabel = `${n} question${n !== 1 ? 's' : ''}`
|
|
const firstHeader = truncate(row.questions[0]?.header ?? '', 40)
|
|
return (
|
|
<Fragment key={i}>
|
|
<tr
|
|
className="border-b last:border-0 cursor-pointer hover:bg-muted/50"
|
|
onClick={() => setExpandedIndex(i === expandedIndex ? null : i)}
|
|
>
|
|
<td className="py-2 pr-4 text-muted-foreground whitespace-nowrap">
|
|
{formatTimestamp(row.timestamp)}
|
|
</td>
|
|
<td className="py-2 pr-4">{countLabel}</td>
|
|
<td className="py-2">{firstHeader}</td>
|
|
</tr>
|
|
{expandedIndex === i && (
|
|
<tr>
|
|
<td colSpan={3} className="pb-2">
|
|
<div className="bg-muted/30 p-3 rounded mt-1 mb-1">
|
|
<ol className="space-y-3 list-decimal list-inside">
|
|
{row.questions.map((q, qi) => (
|
|
<li key={qi}>
|
|
<span className="font-bold bg-muted px-1 py-0.5 rounded text-sm mr-2">
|
|
{q.header}
|
|
</span>
|
|
{q.question}
|
|
<ul className="ml-4 mt-1 space-y-0.5">
|
|
{q.options.map((opt, oi) => (
|
|
<li key={oi} className="text-sm text-muted-foreground">
|
|
{`• ${opt.label} — ${opt.description}`}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</li>
|
|
))}
|
|
</ol>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</Fragment>
|
|
)
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
{isAgentRunning && lastRefreshedAt && (
|
|
<p className="mt-2 text-xs text-muted-foreground text-right">
|
|
Last refreshed: {secondsAgo === 0 ? 'just now' : `${secondsAgo}s ago`}
|
|
</p>
|
|
)}
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|