Python: Improve DevUI, add Context Inspector view as new tab under traces (#2742)

* Improve DevUI, add Context Inspector view as new tab under traces

* fix mypy errors

* fix: Handle stale MCP connections in DevUI executor

MCP tools can become stale when HTTP streaming responses end - the underlying
stdio streams close but `is_connected` remains True. This causes subsequent
requests to fail with `ClosedResourceError`.

Add `_ensure_mcp_connections()` to detect and reconnect stale MCP tools before
agent execution. This is a workaround for an upstream Agent Framework issue
where connection state isn't properly tracked.

Fixes MCP tools failing on second HTTP request in DevUI.

fixes  #1476 #1515 #2865

* fix #1572 report import dependency errors more clearly

* Ensure there is streaming toggle where users can select streaming vs non streaming mode in devui . Fixes .NET: [Python] DevUI tool call rendering in non-streaming mode?

* remove unused dead code

* improve ux - workflows with agents show a chat component in execution timelien, also ensure magentic final output shows correctly

* update ui build

* update devui to use instrumentation instead of tracing, other instrumentation and type/instance check fixes
This commit is contained in:
Victor Dibia
2026-01-07 00:26:08 -08:00
committed by GitHub
Unverified
parent db283cd396
commit 2e1189ca65
36 changed files with 7430 additions and 1662 deletions
@@ -3,7 +3,8 @@
* Features: Real-time event streaming, trace visualization, tool call details
*/
import { useRef, useState } from "react";
import { useRef, useState, useMemo } from "react";
import { useDevUIStore } from "@/stores/devuiStore";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Badge } from "@/components/ui/badge";
@@ -19,8 +20,9 @@ import {
MessageSquare,
ChevronRight,
ChevronDown,
Info,
BarChart3,
} from "lucide-react";
import { ContextInspector } from "@/components/features/agent/context-inspector";
import type { ExtendedResponseStreamEvent } from "@/types";
// Simple visual separator component
@@ -88,7 +90,23 @@ interface TraceEventData extends EventDataBase {
start_time?: number;
end_time?: number;
entity_id?: string;
session_id?: string | null;
response_id?: string | null;
}
// Helper type for trace hierarchy
interface TraceNode {
event: ExtendedResponseStreamEvent;
data: TraceEventData;
children: TraceNode[];
}
// Helper type for grouped traces by response
interface TraceGroup {
response_id: string;
timestamp: number;
traces: TraceNode[];
totalDuration: number;
entity_id?: string;
}
interface DebugPanelProps {
@@ -149,20 +167,22 @@ function processEventsForDisplay(
const item = outputEvent.item;
// If it's a function call item, extract metadata
if (item.type === "function_call" && item.call_id && item.name) {
const callId = item.call_id;
if (item.type === "function_call") {
// Type assertion for function call
const funcCall = item as import("@/types").ResponseFunctionToolCall;
const callId = funcCall.call_id;
// Initialize function call tracking with REAL function name from backend!
functionCalls.set(callId, {
name: item.name, // ← REAL NAME! (not "unknown")
name: funcCall.name, // ← REAL NAME! (not "unknown")
arguments: "",
callId: callId,
itemId: item.id, // Track item_id for delta matching
itemId: funcCall.id, // Track item_id for delta matching
timestamp: new Date().toISOString(),
});
// Also track in callIdToName map for result pairing
callIdToName.set(callId, item.name);
callIdToName.set(callId, funcCall.name);
}
// Pass through the event for display
@@ -955,27 +975,7 @@ function EventExpandedContent({
</span>
<div className="mt-1 max-h-32 overflow-auto">
<pre className="text-xs bg-background border rounded p-2 whitespace-pre-wrap break-all">
{(() => {
try {
// Try to pretty-print JSON, and unescape string values that contain JSON
const attrs = { ...data.attributes };
Object.keys(attrs).forEach((key) => {
if (
typeof attrs[key] === "string" &&
attrs[key].startsWith("[")
) {
try {
attrs[key] = JSON.parse(attrs[key]);
} catch {
// Keep original if parsing fails
}
}
});
return JSON.stringify(attrs, null, 2);
} catch {
return JSON.stringify(data.attributes, null, 2);
}
})()}
{formatTraceAttributes(data.attributes)}
</pre>
</div>
</div>
@@ -1149,257 +1149,418 @@ function EventsTab({
);
}
function TracesTab({ events }: { events: ExtendedResponseStreamEvent[] }) {
// ONLY show actual trace events - handle both event type formats
const traceEvents = events.filter(
(e) =>
e.type === "response.trace.completed" ||
e.type === "response.trace.completed"
);
// Build hierarchical trace structure from flat trace events
function buildTraceHierarchy(traceEvents: ExtendedResponseStreamEvent[]): TraceGroup[] {
// Group by response_id first
const groupedByResponse = new Map<string, ExtendedResponseStreamEvent[]>();
// Add separators between message rounds
const tracesWithSeparators = addSeparatorsToEvents(traceEvents);
for (const event of traceEvents) {
if (!("data" in event)) continue;
const data = event.data as TraceEventData;
const responseId = data.response_id || "unknown";
// Reverse to show latest traces at the top
const reversedTraceEvents = [...tracesWithSeparators].reverse();
if (!groupedByResponse.has(responseId)) {
groupedByResponse.set(responseId, []);
}
groupedByResponse.get(responseId)!.push(event);
}
// Convert each group to hierarchical structure
const groups: TraceGroup[] = [];
for (const [responseId, events] of groupedByResponse) {
// Build tree from parent_span_id relationships
const nodeMap = new Map<string, TraceNode>();
const rootNodes: TraceNode[] = [];
// First pass: create all nodes
for (const event of events) {
if (!("data" in event)) continue;
const data = (event as { data: TraceEventData }).data;
const spanId = data.span_id || `span_${Math.random()}`;
nodeMap.set(spanId, {
event,
data,
children: [],
});
}
// Second pass: build parent-child relationships
for (const event of events) {
if (!("data" in event)) continue;
const data = (event as { data: TraceEventData }).data;
const spanId = data.span_id || "";
const parentSpanId = data.parent_span_id;
const node = nodeMap.get(spanId);
if (!node) continue;
if (parentSpanId && nodeMap.has(parentSpanId)) {
// Has a parent in this group
nodeMap.get(parentSpanId)!.children.push(node);
} else {
// Root node (no parent or parent not in this group)
rootNodes.push(node);
}
}
// Sort root nodes by start_time (earliest first)
rootNodes.sort((a, b) => (a.data.start_time || 0) - (b.data.start_time || 0));
// Sort children recursively by start_time
const sortChildren = (node: TraceNode) => {
node.children.sort((a, b) => (a.data.start_time || 0) - (b.data.start_time || 0));
node.children.forEach(sortChildren);
};
rootNodes.forEach(sortChildren);
// Calculate group metadata
const firstEvent = events[0];
const firstData = firstEvent && "data" in firstEvent ? (firstEvent.data as TraceEventData) : null;
const timestamp = Math.min(...events.map(e => {
const d = "data" in e ? (e.data as TraceEventData) : null;
return d?.start_time || Date.now() / 1000;
}));
const totalDuration = events.reduce((sum, e) => {
const d = "data" in e ? (e.data as TraceEventData) : null;
return sum + (d?.duration_ms || 0);
}, 0);
groups.push({
response_id: responseId,
timestamp,
traces: rootNodes,
totalDuration,
entity_id: firstData?.entity_id,
});
}
// Sort groups by timestamp (newest first)
groups.sort((a, b) => b.timestamp - a.timestamp);
return groups;
}
// Recursively parse escaped JSON strings at any depth
function parseEscapedJson(value: unknown): unknown {
if (typeof value === "string") {
// Try to parse JSON strings (arrays or objects)
const trimmed = value.trim();
if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
try {
const parsed = JSON.parse(value);
// Recursively process the parsed result
return parseEscapedJson(parsed);
} catch {
return value;
}
}
return value;
}
if (Array.isArray(value)) {
return value.map(parseEscapedJson);
}
if (value !== null && typeof value === "object") {
const result: Record<string, unknown> = {};
for (const [k, v] of Object.entries(value)) {
result[k] = parseEscapedJson(v);
}
return result;
}
return value;
}
// Format trace attributes by parsing escaped JSON strings for better readability
function formatTraceAttributes(attributes: Record<string, unknown>): string {
try {
const formatted = parseEscapedJson(attributes);
return JSON.stringify(formatted, null, 2);
} catch {
return JSON.stringify(attributes, null, 2);
}
}
// Get operation type badge color
function getOperationColor(operationName: string): string {
if (operationName.includes("invoke_agent") || operationName.includes("Agent")) {
return "bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200";
}
if (operationName.includes("chat") || operationName.includes("Chat")) {
return "bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200";
}
if (operationName.includes("tool") || operationName.includes("execute")) {
return "bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200";
}
return "bg-orange-100 dark:bg-orange-900 text-orange-800 dark:text-orange-200";
}
// Recursive component for rendering trace tree nodes
function TraceTreeNode({ node, depth = 0 }: { node: TraceNode; depth?: number }) {
const [isExpanded, setIsExpanded] = useState(depth < 2); // Auto-expand first 2 levels
const [showDetails, setShowDetails] = useState(false);
const { data } = node;
const operationName = data.operation_name || "Unknown";
const duration = data.duration_ms ? `${Number(data.duration_ms).toFixed(1)}ms` : "";
const hasChildren = node.children.length > 0;
// Extract token usage from attributes if available
const inputTokens = data.attributes?.["gen_ai.usage.input_tokens"];
const outputTokens = data.attributes?.["gen_ai.usage.output_tokens"];
const hasTokens = inputTokens !== undefined || outputTokens !== undefined;
return (
<div className="h-full flex flex-col">
<div className="flex items-center gap-2 p-3 border-b">
<Search className="h-4 w-4" />
<span className="font-medium">Traces</span>
<Badge variant="outline">{traceEvents.length}</Badge>
<div className="relative">
{/* Vertical line for tree structure */}
{depth > 0 && (
<div
className="absolute left-0 top-0 bottom-0 border-l-2 border-muted"
style={{ marginLeft: `${(depth - 1) * 16 + 8}px` }}
/>
)}
<div
className="flex items-center gap-2 py-1.5 hover:bg-muted/50 rounded transition-colors"
style={{ paddingLeft: `${depth * 16}px` }}
>
{/* Expand/collapse for children OR details */}
<button
onClick={() => hasChildren ? setIsExpanded(!isExpanded) : setShowDetails(!showDetails)}
className="w-4 h-4 flex items-center justify-center text-muted-foreground hover:text-foreground"
>
{hasChildren ? (
isExpanded ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />
) : (
showDetails ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />
)}
</button>
{/* Operation badge */}
<span className={`text-xs px-1.5 py-0.5 rounded font-medium ${getOperationColor(operationName)}`}>
{operationName.replace("ChatAgent.", "").replace("invoke_agent ", "")}
</span>
{/* Duration */}
{duration && (
<span className="text-xs text-muted-foreground font-mono">
{duration}
</span>
)}
{/* Token usage */}
{hasTokens && (
<span className="text-xs text-muted-foreground font-mono">
{inputTokens !== undefined && <span>{String(inputTokens)}</span>}
{inputTokens !== undefined && outputTokens !== undefined && <span className="mx-0.5">/</span>}
{outputTokens !== undefined && <span>{String(outputTokens)}</span>}
</span>
)}
</div>
<ScrollArea className="flex-1">
<div className="p-3">
{traceEvents.length === 0 ? (
<div className="text-center text-muted-foreground text-sm py-8">
No trace data available.
<br />
{events && events.length > 0 && (
<div className="mt-3 text-xs border rounded p-2">
{" "}
<Info className="inline h-4 w-4 mr-1 " />
You may have to set the environment variable{" "}
<span className="font-mono bg-accent/10 px-1 rounded">
ENABLE_INSTRUMENTATION=true
</span>{" "}
or restart devui with the tracing flag{" "}
<div className="font-mono bg-accent/10 px-1 rounded">
devui --tracing
</div>
to enable tracing.
</div>
)}
</div>
) : (
<div className="space-y-3">
{reversedTraceEvents.map((event, index) => {
if ('type' in event && event.type === "separator") {
return <MessageSeparator key={(event as { type: "separator"; id: string }).id} />;
}
return <TraceEventItem key={index} event={event as ExtendedResponseStreamEvent} />;
})}
</div>
)}
{/* Details panel */}
{showDetails && !hasChildren && (
<div
className="ml-4 mt-1 mb-2 p-2 bg-muted/30 rounded border text-xs"
style={{ marginLeft: `${depth * 16 + 20}px` }}
>
<div className="space-y-1">
{data.span_id && (
<div className="flex gap-2">
<span className="text-muted-foreground w-20">Span ID:</span>
<span className="font-mono text-xs break-all">{data.span_id}</span>
</div>
)}
{data.trace_id && (
<div className="flex gap-2">
<span className="text-muted-foreground w-20">Trace ID:</span>
<span className="font-mono text-xs break-all">{data.trace_id}</span>
</div>
)}
{data.status && (
<div className="flex gap-2">
<span className="text-muted-foreground w-20">Status:</span>
<span className={`px-1.5 py-0.5 rounded text-xs ${
data.status === "StatusCode.UNSET" || data.status === "OK"
? "bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200"
: "bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200"
}`}>
{data.status}
</span>
</div>
)}
{data.attributes && Object.keys(data.attributes).length > 0 && (
<div className="mt-2">
<span className="text-muted-foreground block mb-1">Attributes:</span>
<pre className="text-xs bg-background border rounded p-2 overflow-auto max-h-32 whitespace-pre-wrap break-all">
{formatTraceAttributes(data.attributes)}
</pre>
</div>
)}
</div>
</div>
</ScrollArea>
)}
{/* Children */}
{hasChildren && isExpanded && (
<div>
{node.children.map((child, idx) => (
<TraceTreeNode key={child.data.span_id || idx} node={child} depth={depth + 1} />
))}
</div>
)}
</div>
);
}
function TraceEventItem({ event }: { event: ExtendedResponseStreamEvent }) {
const [isExpanded, setIsExpanded] = useState(false);
// Component for a single trace group (one response/turn)
function TraceGroupItem({ group }: { group: TraceGroup }) {
const [isExpanded, setIsExpanded] = useState(true);
if (
(event.type !== "response.trace.completed" &&
event.type !== "response.trace.completed") ||
!("data" in event)
) {
return (
<div className="border rounded p-3 text-red-600 dark:text-red-400 text-xs">
Error: Expected trace event but got {event.type}
</div>
);
}
const data = event.data as TraceEventData;
// Use stored UI timestamp first, then trace timestamps, then fallback to current time
let timestamp: string;
if ('_uiTimestamp' in event && typeof event._uiTimestamp === 'number') {
// Use stored UI timestamp from when event was received
timestamp = new Date(event._uiTimestamp * 1000).toLocaleTimeString();
} else if (data.end_time) {
timestamp = new Date(data.end_time * 1000).toLocaleTimeString();
} else if (data.start_time) {
timestamp = new Date(data.start_time * 1000).toLocaleTimeString();
} else if (data.timestamp) {
timestamp = new Date(data.timestamp).toLocaleTimeString();
} else {
timestamp = new Date().toLocaleTimeString();
}
const operationName = data.operation_name || "Unknown Operation";
const duration = data.duration_ms
? `${Number(data.duration_ms).toFixed(1)}ms`
: "";
const entityId = data.entity_id || "";
const timestamp = new Date(group.timestamp * 1000).toLocaleTimeString();
const duration = group.totalDuration > 0 ? `${group.totalDuration.toFixed(0)}ms` : "";
const spanCount = group.traces.reduce((count, node) => {
const countNode = (n: TraceNode): number => 1 + n.children.reduce((c, child) => c + countNode(child), 0);
return count + countNode(node);
}, 0);
return (
<div className="border-l-2 border-muted pl-3 py-2 hover:bg-muted/50 transition-colors">
<div className="flex items-center gap-2 text-xs text-muted-foreground mb-1">
<Search className="h-3 w-3 text-orange-600 dark:text-orange-400" />
<span className="font-mono">{timestamp}</span>
<Badge variant="outline" className="text-xs py-0">
trace
</Badge>
<div className="border rounded-lg overflow-hidden">
{/* Group header */}
<div
className="flex items-center gap-2 p-2 bg-muted/50 cursor-pointer hover:bg-muted/70 transition-colors"
onClick={() => setIsExpanded(!isExpanded)}
>
<div className="text-muted-foreground">
{isExpanded ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
</div>
<span className="font-mono text-xs text-muted-foreground">{timestamp}</span>
{group.entity_id && (
<Badge variant="outline" className="text-xs py-0">
{group.entity_id.replace("agent_", "").replace("workflow_", "")}
</Badge>
)}
<div className="flex-1" />
{duration && (
<Badge variant="secondary" className="text-xs py-0">
{duration}
</Badge>
)}
<span className="text-xs text-muted-foreground">
{spanCount} span{spanCount !== 1 ? "s" : ""}
</span>
</div>
<div className="text-sm">
<div
className="flex items-center gap-2 cursor-pointer"
onClick={() => setIsExpanded(!isExpanded)}
>
<div className="text-muted-foreground">
{isExpanded ? (
<ChevronDown className="h-3 w-3" />
) : (
<ChevronRight className="h-3 w-3" />
)}
</div>
<div className="text-muted-foreground flex-1 break-all">
<span className="font-medium">{operationName}</span>
{entityId && <span className="ml-2 text-xs">({entityId})</span>}
</div>
{/* Group content - trace tree */}
{isExpanded && (
<div className="p-2 border-t">
{group.traces.map((node, idx) => (
<TraceTreeNode key={node.data.span_id || idx} node={node} depth={0} />
))}
</div>
)}
</div>
);
}
{/* Expandable content */}
{isExpanded && (
<div className="mt-2 ml-5 p-3 bg-muted/30 rounded border">
<div className="space-y-2">
function TracesTab({ events }: { events: ExtendedResponseStreamEvent[] }) {
// Use persisted store state instead of local useState
const subTab = useDevUIStore((state) => state.debugTraceSubTab);
const setSubTab = useDevUIStore((state) => state.setDebugTraceSubTab);
// ONLY show actual trace events
const traceEvents = events.filter(
(e) => e.type === "response.trace.completed"
);
// Build hierarchical structure grouped by response_id
const traceGroups = buildTraceHierarchy(traceEvents);
return (
<div className="h-full flex flex-col">
{/* Sub-tab header */}
<div className="flex items-center gap-2 p-3 border-b">
<Search className="h-4 w-4" />
<span className="font-medium">Traces</span>
<Badge variant="outline">{traceEvents.length}</Badge>
{/* Sub-tab toggle */}
<div className="flex-1" />
<div className="flex items-center bg-muted rounded-md p-1 min-w-0">
<button
onClick={() => setSubTab("spans")}
className={`px-3 py-1.5 text-xs rounded transition-colors truncate ${
subTab === "spans"
? "bg-background shadow-sm font-medium"
: "text-muted-foreground hover:text-foreground"
}`}
>
OTel Spans
</button>
<button
onClick={() => setSubTab("context")}
className={`px-3 py-1.5 text-xs rounded transition-colors flex items-center gap-1.5 min-w-0 ${
subTab === "context"
? "bg-background shadow-sm font-medium"
: "text-muted-foreground hover:text-foreground"
}`}
>
<BarChart3 className="h-3.5 w-3.5 flex-shrink-0" />
<span className="truncate">Context Inspector</span>
</button>
</div>
</div>
{/* Sub-tab content */}
{subTab === "spans" ? (
<div className="flex-1 flex flex-col min-h-0">
{/* OTel Spans header - only show when we have data */}
{traceEvents.length > 0 && (
<div className="p-3 border-b flex-shrink-0">
<div className="flex items-center gap-2">
<Search className="h-4 w-4 text-orange-500" />
<span className="font-semibold text-sm">Trace Details</span>
</div>
<div className="grid grid-cols-1 gap-2 text-xs">
<div>
<span className="font-medium text-muted-foreground">
Operation:
</span>
<span className="ml-2 font-mono bg-orange-100 dark:bg-orange-900 px-2 py-1 rounded">
{operationName}
</span>
</div>
{data.span_id && (
<div>
<span className="font-medium text-muted-foreground">
Span ID:
</span>
<span className="ml-2 font-mono text-xs break-all">
{data.span_id}
</span>
</div>
)}
{data.trace_id && (
<div>
<span className="font-medium text-muted-foreground">
Trace ID:
</span>
<span className="ml-2 font-mono text-xs break-all">
{data.trace_id}
</span>
</div>
)}
{data.parent_span_id && (
<div>
<span className="font-medium text-muted-foreground">
Parent Span:
</span>
<span className="ml-2 font-mono text-xs break-all">
{data.parent_span_id}
</span>
</div>
)}
{data.duration_ms && (
<div>
<span className="font-medium text-muted-foreground">
Duration:
</span>
<span className="ml-2 font-mono text-xs">
{Number(data.duration_ms).toFixed(2)}ms
</span>
</div>
)}
{data.status && (
<div>
<span className="font-medium text-muted-foreground">
Status:
</span>
<span
className={`ml-2 px-2 py-1 rounded text-xs font-medium ${data.status === "StatusCode.UNSET" ||
data.status === "OK"
? "bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200"
: "bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200"
}`}
>
{data.status || "unknown"}
</span>
</div>
)}
{data.entity_id && (
<div>
<span className="font-medium text-muted-foreground">
Entity:
</span>
<span className="ml-2 font-mono text-xs break-all">
{data.entity_id}
</span>
</div>
)}
{data.attributes && Object.keys(data.attributes).length > 0 && (
<div>
<span className="font-medium text-muted-foreground">
Attributes:
</span>
<div className="mt-1 max-h-32 overflow-auto">
<pre className="text-xs bg-background border rounded p-2 whitespace-pre-wrap max-w-full break-all">
{(() => {
try {
// Try to pretty-print JSON, and unescape string values that contain JSON
const attrs = { ...data.attributes };
Object.keys(attrs).forEach((key) => {
if (
typeof attrs[key] === "string" &&
attrs[key].startsWith("[")
) {
try {
attrs[key] = JSON.parse(attrs[key]);
} catch {
// Keep original if parsing fails
}
}
});
return JSON.stringify(attrs, null, 2);
} catch {
return JSON.stringify(data.attributes, null, 2);
}
})()}
</pre>
</div>
</div>
)}
<Search className="h-4 w-4" />
<span className="font-medium text-sm">OTel Spans</span>
<Badge variant="outline" className="text-xs">
{traceGroups.length} turn{traceGroups.length !== 1 ? "s" : ""}
</Badge>
</div>
</div>
</div>
)}
</div>
)}
{traceEvents.length === 0 ? (
<div className="flex flex-col items-center text-center p-6 pt-9">
<BarChart3 className="h-8 w-8 text-muted-foreground mb-3" />
<div className="text-sm font-medium mb-1">No Data</div>
<div className="text-xs text-muted-foreground max-w-[200px]">
Run{" "}
<span className="font-mono bg-accent/10 px-1 rounded">
devui --instrumentation
</span>{" "}
and start a conversation.
</div>
</div>
) : (
<ScrollArea className="flex-1">
<div className="p-3">
<div className="space-y-3">
{traceGroups.map((group) => (
<TraceGroupItem key={group.response_id} group={group} />
))}
</div>
</div>
</ScrollArea>
)}
</div>
) : (
<ContextInspector events={events} />
)}
</div>
);
}
@@ -1590,19 +1751,48 @@ export function DebugPanel({
isStreaming = false,
onMinimize,
}: DebugPanelProps) {
// Use persisted store state for active tab
const activeTab = useDevUIStore((state) => state.debugPanelTab);
const setActiveTab = useDevUIStore((state) => state.setDebugPanelTab);
// Compute counts once for tab badges (memoized to avoid perf hits)
const counts = useMemo(() => {
const processedEvents = processEventsForDisplay(events);
const eventsCount = processedEvents.length;
const tracesCount = events.filter(e => e.type === "response.trace.completed").length;
const toolsCount = processedEvents.filter(e => e.type === "response.function_call.complete").length
+ events.filter(e => getFunctionResultFromEvent(e) !== null).length;
return { eventsCount, tracesCount, toolsCount };
}, [events]);
return (
<div className="flex-1 border-l flex flex-col min-h-0">
<Tabs defaultValue="events" className="flex-1 flex flex-col min-h-0">
<Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as "events" | "traces" | "tools")} className="flex-1 flex flex-col min-h-0">
<div className="px-3 pt-3 flex items-center gap-2 flex-shrink-0">
<TabsList className="flex-1">
<TabsTrigger value="events" className="flex-1">
<TabsTrigger value="events" className="flex-1 gap-1.5">
Events
{counts.eventsCount > 0 && (
<span className="text-[10px] bg-muted-foreground/20 text-muted-foreground px-1.5 py-0.5 rounded-full min-w-[1.25rem] text-center">
{counts.eventsCount}
</span>
)}
</TabsTrigger>
<TabsTrigger value="traces" className="flex-1">
<TabsTrigger value="traces" className="flex-1 gap-1.5">
Traces
{counts.tracesCount > 0 && (
<span className="text-[10px] bg-muted-foreground/20 text-muted-foreground px-1.5 py-0.5 rounded-full min-w-[1.25rem] text-center">
{counts.tracesCount}
</span>
)}
</TabsTrigger>
<TabsTrigger value="tools" className="flex-1">
<TabsTrigger value="tools" className="flex-1 gap-1.5">
Tools
{counts.toolsCount > 0 && (
<span className="text-[10px] bg-muted-foreground/20 text-muted-foreground px-1.5 py-0.5 rounded-full min-w-[1.25rem] text-center">
{counts.toolsCount}
</span>
)}
</TabsTrigger>
</TabsList>
{onMinimize && (
@@ -209,7 +209,7 @@ export function DeploymentModal({
setCopiedTemplate(null);
timeoutRef.current = null;
}, 2000);
} catch (err) {
} catch {
// Reset state on error - clipboard write failed
setCopiedTemplate(null);
}
@@ -248,7 +248,7 @@ services:
- AZURE_OPENAI_API_KEY=\${AZURE_OPENAI_API_KEY}
- AZURE_OPENAI_ENDPOINT=\${AZURE_OPENAI_ENDPOINT}
- AZURE_OPENAI_CHAT_DEPLOYMENT_NAME=\${AZURE_OPENAI_CHAT_DEPLOYMENT_NAME}
# Optional: Enable tracing
# Optional: Enable instrumentation
- ENABLE_INSTRUMENTATION=\${ENABLE_INSTRUMENTATION:-false}
ports:
- "8080:8080"
@@ -41,8 +41,8 @@ export function SettingsModal({
}: SettingsModalProps) {
const [activeTab, setActiveTab] = useState<Tab>("general");
// OpenAI proxy mode, Azure deployment, auth status, server capabilities, and version from store
const { oaiMode, setOAIMode, azureDeploymentEnabled, setAzureDeploymentEnabled, authRequired, serverCapabilities, serverVersion, runtime, uiMode } = useDevUIStore();
// OpenAI proxy mode, Azure deployment, auth status, server capabilities, streaming, and version from store
const { oaiMode, setOAIMode, azureDeploymentEnabled, setAzureDeploymentEnabled, authRequired, serverCapabilities, serverVersion, runtime, uiMode, streamingEnabled, setStreamingEnabled } = useDevUIStore();
// Get current backend URL from localStorage or default
const defaultUrl = import.meta.env.VITE_API_BASE_URL !== undefined ? import.meta.env.VITE_API_BASE_URL : "";
@@ -353,6 +353,37 @@ export function SettingsModal({
/>
</div>
</div>
{/* Streaming Mode Setting */}
<div className="space-y-3 border-t pt-6">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label className="text-sm font-medium">
Streaming Mode
</Label>
<p className="text-xs text-muted-foreground">
Stream responses token-by-token as they're generated
</p>
</div>
<Switch
checked={streamingEnabled}
onCheckedChange={setStreamingEnabled}
/>
</div>
{!streamingEnabled && (
<div className="flex items-start gap-2 text-xs text-amber-600 dark:text-amber-400 bg-amber-500/10 p-3 rounded">
<Info className="h-3.5 w-3.5 flex-shrink-0 mt-0.5" />
<div>
<p className="font-medium">Non-streaming mode limitations:</p>
<ul className="mt-1 space-y-0.5 list-disc list-inside text-amber-600/80 dark:text-amber-400/80">
<li>Tool calls won't display in real-time</li>
<li>No typing indicator during generation</li>
<li>Response appears all at once when complete</li>
</ul>
</div>
</div>
)}
</div>
</div>
)}
@@ -628,11 +659,11 @@ export function SettingsModal({
<div className="space-y-2 pt-2">
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wide">Capabilities</p>
<div className="space-y-1 text-sm">
{serverCapabilities?.tracing !== undefined && (
{serverCapabilities?.instrumentation !== undefined && (
<div className="flex justify-between items-center">
<span className="text-muted-foreground">Tracing:</span>
<span className={`text-xs px-2 py-0.5 rounded-full ${serverCapabilities.tracing ? 'bg-green-500/10 text-green-600 dark:text-green-400' : 'bg-muted text-muted-foreground'}`}>
{serverCapabilities.tracing ? 'Enabled' : 'Disabled'}
<span className="text-muted-foreground">Instrumentation:</span>
<span className={`text-xs px-2 py-0.5 rounded-full ${serverCapabilities.instrumentation ? 'bg-green-500/10 text-green-600 dark:text-green-400' : 'bg-muted text-muted-foreground'}`}>
{serverCapabilities.instrumentation ? 'Enabled' : 'Disabled'}
</span>
</div>
)}