diff --git a/client/src/pages/ReviewsPage.tsx b/client/src/pages/ReviewsPage.tsx index 65acbdd..e90d31a 100644 --- a/client/src/pages/ReviewsPage.tsx +++ b/client/src/pages/ReviewsPage.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useState } from 'react' -import { ChevronDown, ChevronRight, ShieldAlert } from 'lucide-react' +import { ChevronDown, ChevronRight, Clock, Cpu, ScrollText, ShieldAlert, Wrench } from 'lucide-react' import { toast } from 'sonner' import { AppShell } from '@/components/AppShell' import { Badge } from '@/components/ui/badge' @@ -30,6 +30,43 @@ interface ReviewItem { createdAtUtc: string } +interface RunDetail { + status: string + output: string | null + prompt: string | null + error: string | null + trace: string | null + resultJson: string | null + latencyMs: number | null +} + +interface RunTrace { + agent?: string + autonomy?: string + skills?: string[] + tools?: string[] + docs?: string[] + memories?: number + product?: { productId?: string | null; identity?: boolean } + task?: { taskType?: string } +} + +interface RunResult { + action?: string + risk?: string + skill?: string + toolCalls?: { tool: string; server?: string | null; ok: boolean }[] +} + +function parseJson(value: string | null | undefined): T | null { + if (!value) return null + try { + return JSON.parse(value) as T + } catch { + return null + } +} + export function ReviewsPage() { const organizationId = useAuth((s) => s.organizationId) const [items, setItems] = useState(null) @@ -54,7 +91,8 @@ export function ReviewsPage() {

Review inbox

- Held agent actions awaiting your decision. Edit before approving — your edits feed the metric. + Held agent actions awaiting your decision. See the action, the result, and the run log before + you approve. Your edits feed the metric.

@@ -90,19 +128,32 @@ export function ReviewsPage() { function ReviewCard({ item, onDecided }: { item: ReviewItem; onDecided: (id: string) => void }) { const [content, setContent] = useState(item.content) const [childrenText, setChildrenText] = useState(item.childTitles.join('\n')) - const [showTrace, setShowTrace] = useState(false) + const [showLog, setShowLog] = useState(false) + const [run, setRun] = useState(null) const [busy, setBusy] = useState(false) const destructive = item.risk.toLowerCase() === 'destructive' + const childCount = childrenText.split('\n').map((l) => l.trim()).filter(Boolean).length + + // Lazily pull the full run (latency, tool calls, raw output, assembled prompt) the first time the + // approver opens the log. + const toggleLog = async () => { + const next = !showLog + setShowLog(next) + if (next && !run) { + try { + setRun(await api.get(`/api/assembler/runs/${item.agentRunId}`)) + } catch { + // The run row may be gone; the assembly trace on the item still renders below. + } + } + } async function decide(action: 'approve' | 'sendback') { setBusy(true) try { if (action === 'approve') { - const childTitles = childrenText - .split('\n') - .map((line) => line.trim()) - .filter(Boolean) + const childTitles = childrenText.split('\n').map((line) => line.trim()).filter(Boolean) const result = await api.post<{ editDistance: number | null; decision: string }>( `/api/governance/reviews/${item.id}/approve`, { content, childTitles }, @@ -138,23 +189,28 @@ function ReviewCard({ item, onDecided }: { item: ReviewItem; onDecided: (id: str {destructive && } {item.risk} - - {new Date(item.createdAtUtc).toLocaleString()} - + {new Date(item.createdAtUtc).toLocaleString()} + {/* Action — what approving will actually do. */} +
+ {item.actionKind.replace(/-/g, ' ')} ·{' '} + {destructive + ? 'Destructive — always held for a human.' + : `On approve, this artifact is written to the board${childCount ? ` and ${childCount} child task${childCount === 1 ? '' : 's'} created` : ''}.`} +
+ + {/* Result — the proposed artifact + child tasks (both editable). */}
- - + +
{content !== item.content && ( @@ -165,13 +221,9 @@ function ReviewCard({ item, onDecided }: { item: ReviewItem; onDecided: (id: str segment.kind === 'same' ? ( {segment.text} ) : segment.kind === 'removed' ? ( - - {segment.text} - + {segment.text} ) : ( - - {segment.text} - + {segment.text} ), )} @@ -189,36 +241,104 @@ function ReviewCard({ item, onDecided }: { item: ReviewItem; onDecided: (id: str /> + {/* Run log — how the agent got here. */} - {showTrace && ( -
{formatTrace(item.trace)}
- )} + {showLog && }
- - + +
) } -function formatTrace(trace: string | null): string { - if (!trace) return 'No trace captured.' - try { - return JSON.stringify(JSON.parse(trace), null, 2) - } catch { - return trace - } +function RunLog({ item, run }: { item: ReviewItem; run: RunDetail | null }) { + // The assembly trace is on the review item; the run adds latency, tool-call outcomes, and raw output. + const trace = parseJson(run?.trace ?? item.trace) + const result = parseJson(run?.resultJson) + const [showRaw, setShowRaw] = useState(false) + const [showPrompt, setShowPrompt] = useState(false) + const toolCalls = result?.toolCalls ?? [] + + return ( +
+
+ + {trace?.agent ?? 'agent'} · {trace?.autonomy ?? '—'} + + {run?.latencyMs != null && ( + + {(run.latencyMs / 1000).toFixed(1)}s + + )} + {trace?.product?.identity && · product identity included} + {typeof trace?.memories === 'number' && · {trace.memories} memory hit{trace.memories === 1 ? '' : 's'}} +
+ + + {trace?.tools?.length ? : null} + {trace?.docs?.length ? : null} + +
+ Tools called + + {toolCalls.length === 0 ? ( + none + ) : ( + + {toolCalls.map((t, i) => ( + + {t.tool} + {t.server ? ` · ${t.server}` : ''} + + {t.ok ? 'ok' : 'failed'} + + + ))} + + )} + +
+ + {run?.error && } + + {run?.output && ( +
+ + {showRaw &&
{run.output}
} +
+ )} + + {run?.prompt && ( +
+ + {showPrompt &&
{run.prompt}
} +
+ )} + + {!run && !trace && Loading run…} +
+ ) +} + +function LogRow({ label, value }: { label: string; value: string }) { + return ( +
+ {label} + {value} +
+ ) } diff --git a/src/Modules/TeamUp.Modules.Assembler/Endpoints/AssemblerDtos.cs b/src/Modules/TeamUp.Modules.Assembler/Endpoints/AssemblerDtos.cs index 9a8148e..c9f8a3a 100644 --- a/src/Modules/TeamUp.Modules.Assembler/Endpoints/AssemblerDtos.cs +++ b/src/Modules/TeamUp.Modules.Assembler/Endpoints/AssemblerDtos.cs @@ -12,7 +12,12 @@ internal sealed record RunResponse( string? ActionRisk, string? Prompt, string? Output, - string? Error); + string? Error, + string? Trace, + string? ResultJson, + long? LatencyMs, + DateTimeOffset CreatedAtUtc, + DateTimeOffset? CompletedAtUtc); internal sealed record AgentActivityResponse( Guid AgentId, diff --git a/src/Modules/TeamUp.Modules.Assembler/Endpoints/AssemblerEndpoints.cs b/src/Modules/TeamUp.Modules.Assembler/Endpoints/AssemblerEndpoints.cs index f2eec8a..9e26f74 100644 --- a/src/Modules/TeamUp.Modules.Assembler/Endpoints/AssemblerEndpoints.cs +++ b/src/Modules/TeamUp.Modules.Assembler/Endpoints/AssemblerEndpoints.cs @@ -85,5 +85,6 @@ internal static class AssemblerEndpoints private static RunResponse ToResponse(AgentRun run) => new( run.Id, run.SeatId, run.WorkItemId, run.AgentId, run.Status.ToString(), - run.ActionType, run.ActionRisk, run.Prompt, run.Output, run.Error); + run.ActionType, run.ActionRisk, run.Prompt, run.Output, run.Error, + run.Trace, run.ResultJson, run.LatencyMs, run.CreatedAtUtc, run.CompletedAtUtc); }