From 638a5d2640d8ec3c8e9edf0691ca62d1f696c209 Mon Sep 17 00:00:00 2001 From: Vectry Date: Tue, 10 Feb 2026 13:25:19 +0000 Subject: [PATCH] fix: complete traces on idle, improve dashboard span/event/analytics views --- .../src/app/dashboard/traces/[id]/page.tsx | 3 + apps/web/src/components/trace-analytics.tsx | 13 ++- apps/web/src/components/trace-detail.tsx | 93 ++++++++++++++----- packages/opencode-plugin/package.json | 2 +- packages/opencode-plugin/src/state.ts | 27 +++++- 5 files changed, 105 insertions(+), 33 deletions(-) diff --git a/apps/web/src/app/dashboard/traces/[id]/page.tsx b/apps/web/src/app/dashboard/traces/[id]/page.tsx index 0e8d064..8c9d98c 100644 --- a/apps/web/src/app/dashboard/traces/[id]/page.tsx +++ b/apps/web/src/app/dashboard/traces/[id]/page.tsx @@ -12,6 +12,7 @@ interface TraceData { metadata: Record; costUsd: number | null; totalCost: number | null; + totalTokens: number | null; decisionPoints: Array<{ id: string; type: string; @@ -96,6 +97,8 @@ export default async function TraceDetailPage({ params }: TraceDetailPageProps) tags: trace.tags, metadata: trace.metadata, costUsd: trace.costUsd ?? trace.totalCost, + totalTokens: trace.totalTokens ?? null, + totalCost: trace.totalCost ?? null, }} decisionPoints={trace.decisionPoints} spans={trace.spans} diff --git a/apps/web/src/components/trace-analytics.tsx b/apps/web/src/components/trace-analytics.tsx index 4c7b075..1904f6d 100644 --- a/apps/web/src/components/trace-analytics.tsx +++ b/apps/web/src/components/trace-analytics.tsx @@ -49,6 +49,8 @@ interface Trace { tags: string[]; metadata: Record; costUsd: number | null; + totalTokens: number | null; + totalCost: number | null; } interface TraceAnalyticsProps { @@ -112,9 +114,15 @@ function ExecutionTimeline({ trace, spans }: { trace: Trace; spans: Span[] }) { } const traceStartTime = new Date(trace.startedAt).getTime(); + const lastSpanEnd = spans.reduce((max, s) => { + const end = s.endedAt ? new Date(s.endedAt).getTime() : 0; + return end > max ? end : max; + }, 0); const traceEndTime = trace.endedAt ? new Date(trace.endedAt).getTime() - : Date.now(); + : trace.status === "COMPLETED" || trace.status === "ERROR" + ? lastSpanEnd || traceStartTime + : Date.now(); const totalDuration = traceEndTime - traceStartTime; // Build span hierarchy for nesting @@ -310,7 +318,7 @@ function CostBreakdown({ (sum, d) => sum + (d.cost || 0), 0 ); - const totalCostValue = trace.costUsd ?? totalSpanCost + totalDecisionCost; + const totalCostValue = trace.costUsd ?? trace.totalCost ?? totalSpanCost + totalDecisionCost; const hasData = totalCostValue > 0 || spanCostsData.length > 0 || decisionCostsData.length > 0; @@ -462,6 +470,7 @@ function CostBreakdown({ function TokenUsageGauge({ trace }: { trace: Trace }) { const tokenData = useMemo(() => { const totalTokens = + trace.totalTokens ?? (trace.metadata?.totalTokens as number | null | undefined) ?? (trace.metadata?.tokenCount as number | null | undefined) ?? null; diff --git a/apps/web/src/components/trace-detail.tsx b/apps/web/src/components/trace-detail.tsx index ea899e3..eaf24c6 100644 --- a/apps/web/src/components/trace-detail.tsx +++ b/apps/web/src/components/trace-detail.tsx @@ -77,6 +77,8 @@ interface Trace { tags: string[]; metadata: Record; costUsd: number | null; + totalTokens: number | null; + totalCost: number | null; } interface TraceDetailProps { @@ -530,19 +532,77 @@ function SpanItem({ span, maxDuration }: { span: Span; maxDuration: number }) {
Output
-
-                {JSON.stringify(span.output, null, 2)}
-              
+ {span.output === null || span.output === undefined || span.output === "" ? ( +

No output recorded

+ ) : ( +
+                  {JSON.stringify(span.output, null, 2)}
+                
+ )}
- {Object.keys(span.metadata).length > 0 && ( + {Object.entries(span.metadata).some(([, v]) => v !== null && v !== undefined) && (
Metadata
-                {JSON.stringify(span.metadata, null, 2)}
+                {JSON.stringify(
+                  Object.fromEntries(Object.entries(span.metadata).filter(([, v]) => v !== null && v !== undefined)),
+                  null,
+                  2
+                )}
               
)} +
+ + + Raw JSON + + +
+              {JSON.stringify(span, null, 2)}
+            
+
+ + )} + + ); +} + +function EventItem({ event }: { event: Event }) { + const [expanded, setExpanded] = useState(false); + const { icon: Icon, color } = eventTypeColors[event.type] || eventTypeColors.DEFAULT; + const hasMetadata = event.metadata && Object.entries(event.metadata).some(([, v]) => v !== null && v !== undefined); + + return ( +
+ + {expanded && hasMetadata && ( +
+
+            {JSON.stringify(event.metadata, null, 2)}
+          
)}
@@ -560,26 +620,9 @@ function EventsTab({ events }: { events: Event[] }) { return (
- {events.map((event) => { - const { icon: Icon, color } = eventTypeColors[event.type] || eventTypeColors.DEFAULT; - return ( -
-
- -
-
-

{event.name}

-

{event.type}

-
- - {formatRelativeTime(event.timestamp)} - -
- ); - })} + {events.map((event) => ( + + ))}
); } diff --git a/packages/opencode-plugin/package.json b/packages/opencode-plugin/package.json index 50699f7..6728f7f 100644 --- a/packages/opencode-plugin/package.json +++ b/packages/opencode-plugin/package.json @@ -1,6 +1,6 @@ { "name": "opencode-agentlens", - "version": "0.1.4", + "version": "0.1.5", "description": "OpenCode plugin for AgentLens — trace your coding agent's decisions, tool calls, and sessions", "type": "module", "main": "./dist/index.cjs", diff --git a/packages/opencode-plugin/src/state.ts b/packages/opencode-plugin/src/state.ts index 1920267..f7283a4 100644 --- a/packages/opencode-plugin/src/state.ts +++ b/packages/opencode-plugin/src/state.ts @@ -111,11 +111,16 @@ export class SessionState { metadata: safeJsonValue({ ...toolMeta, rawMetadata: metadata }), }); + const reasoningText = + title !== call.tool && title + ? `Selected ${call.tool}: ${title}` + : `Selected tool: ${call.tool}`; + trace.addDecision({ type: DecisionType.TOOL_SELECTION, chosen: call.tool as JsonValue, alternatives: [], - reasoning: title, + reasoning: reasoningText, durationMs, parentSpanId: rootSpanId, }); @@ -186,17 +191,29 @@ export class SessionState { return Array.from(this.traces.keys()); } - /** - * Send the current trace state without ending the session. - * This creates a snapshot so data isn't lost if the process exits unexpectedly. - */ flushSession(sessionId: string): void { const trace = this.traces.get(sessionId); if (!trace) return; + const rootSpanId = this.rootSpans.get(sessionId); + if (rootSpanId) { + trace.addSpan({ + id: rootSpanId, + name: "session", + type: SpanType.AGENT, + status: SpanStatus.COMPLETED, + endedAt: nowISO(), + }); + } + + trace.end({ status: "COMPLETED" }); + const transport = getClient(); if (transport) { transport.add(trace.toPayload()); } + + this.traces.delete(sessionId); + this.rootSpans.delete(sessionId); } }