Files
Codewalkers/apps/web/src/components/radar/QuestionsAskedDialog.tsx
Lukas May 7c48c70d47 feat: Add SSE-driven real-time refresh and last-refreshed timestamp to drilldown dialogs
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>
2026-03-06 20:18:17 +01:00

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>
)
}