@optilogic/chat 1.0.0-beta.10 → 1.0.0-beta.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optilogic/chat",
3
- "version": "1.0.0-beta.10",
3
+ "version": "1.0.0-beta.11",
4
4
  "description": "Chat UI components for Optilogic - AgentResponse and related components for LLM interactions",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -24,8 +24,8 @@
24
24
  "README.md"
25
25
  ],
26
26
  "dependencies": {
27
- "@optilogic/core": "1.0.0-beta.10",
28
- "@optilogic/editor": "1.0.0-beta.10"
27
+ "@optilogic/core": "1.0.0-beta.11",
28
+ "@optilogic/editor": "1.0.0-beta.11"
29
29
  },
30
30
  "peerDependencies": {
31
31
  "react": "^18.0.0 || ^19.0.0",
@@ -6,12 +6,14 @@
6
6
  */
7
7
 
8
8
  import * as React from "react";
9
- import { useState, useMemo, useCallback } from "react";
9
+ import { useState, useRef, useMemo, useCallback } from "react";
10
10
  import { cn } from "@optilogic/core";
11
11
  import { MetadataRow, ThinkingSection, ActionBar, HITLSection } from "./components";
12
12
  import { useThinkingTimer } from "./hooks";
13
13
  import type { AgentResponseState, FeedbackValue } from "./types";
14
14
  import type { HITLInteraction } from "../hitl-interactions";
15
+ import { AgentTimeline, createTimelineUIState } from "../agent-timeline";
16
+ import type { TimelineUIState } from "../agent-timeline";
15
17
 
16
18
  export interface AgentResponseProps extends React.HTMLAttributes<HTMLDivElement> {
17
19
  /** The response state to render */
@@ -147,6 +149,9 @@ const AgentResponse = React.forwardRef<HTMLDivElement, AgentResponseProps>(
147
149
  },
148
150
  ref
149
151
  ) => {
152
+ // Ref-backed timeline UI state (survives remounts during streaming)
153
+ const timelineUIStateRef = useRef<TimelineUIState>(createTimelineUIState());
154
+
150
155
  // Uncontrolled thinking expanded state
151
156
  const [uncontrolledExpanded, setUncontrolledExpanded] = useState(defaultThinkingExpanded);
152
157
 
@@ -182,9 +187,10 @@ const AgentResponse = React.forwardRef<HTMLDivElement, AgentResponseProps>(
182
187
  return (state.responseCompleteTime - state.firstMessageTime) / 1000;
183
188
  }, [state.firstMessageTime, state.responseCompleteTime]);
184
189
 
185
- // Check if we have any thinking content (plain text or structured)
190
+ // Check if we have any thinking content (plain text, structured, or timeline)
191
+ const hasTimelineEntries = !!(state.timelineEntries && state.timelineEntries.length > 0);
186
192
  const hasThinkingContent =
187
- !!state.thinking || (state.thinkingSteps && state.thinkingSteps.length > 0) || false;
193
+ !!state.thinking || (state.thinkingSteps && state.thinkingSteps.length > 0) || hasTimelineEntries || false;
188
194
 
189
195
  const hasHITLInteractions =
190
196
  hitlInteractions && hitlInteractions.length > 0;
@@ -245,16 +251,28 @@ const AgentResponse = React.forwardRef<HTMLDivElement, AgentResponseProps>(
245
251
  elapsedTime={elapsedTime}
246
252
  />
247
253
 
248
- {/* Thinking Content - collapsible with max-height */}
249
- <ThinkingSection
250
- content={
251
- state.thinkingSteps && state.thinkingSteps.length > 0
252
- ? state.thinkingSteps
253
- : state.thinking
254
- }
255
- isExpanded={thinkingExpanded}
256
- renderMarkdown={renderThinkingMarkdown}
257
- />
254
+ {/* Thinking Content - AgentTimeline when timeline entries exist, ThinkingSection otherwise */}
255
+ {hasTimelineEntries ? (
256
+ thinkingExpanded && (
257
+ <div className="px-3 pb-3 border-t border-border mt-2">
258
+ <AgentTimeline
259
+ entries={state.timelineEntries!}
260
+ renderMarkdown={renderThinkingMarkdown}
261
+ uiState={timelineUIStateRef.current}
262
+ />
263
+ </div>
264
+ )
265
+ ) : (
266
+ <ThinkingSection
267
+ content={
268
+ state.thinkingSteps && state.thinkingSteps.length > 0
269
+ ? state.thinkingSteps
270
+ : state.thinking
271
+ }
272
+ isExpanded={thinkingExpanded}
273
+ renderMarkdown={renderThinkingMarkdown}
274
+ />
275
+ )}
258
276
  </>
259
277
  )}
260
278
 
@@ -110,7 +110,7 @@ const ThinkingSection = React.forwardRef<HTMLDivElement, ThinkingSectionProps>(
110
110
  className={cn("px-3 pb-3 border-t border-border", className)}
111
111
  {...props}
112
112
  >
113
- <div className="mt-2 max-h-[200px] overflow-y-auto">
113
+ <div className="mt-2 max-h-[200px] overflow-y-auto scrollbar-thin">
114
114
  {isStructured ? (
115
115
  <div className="space-y-0">
116
116
  {content.map((step) => (
@@ -16,6 +16,7 @@ import {
16
16
  type StatusItem,
17
17
  type ThinkingStep,
18
18
  } from "../types";
19
+ import { buildTimelineEntries } from "../../agent-timeline/utils";
19
20
 
20
21
  export interface UseAgentResponseAccumulatorOptions {
21
22
  /** WebSocket topic to filter messages (optional, for convenience) */
@@ -94,33 +95,50 @@ export function useAgentResponseAccumulator(
94
95
  id: payload.thinkingStep.id || `step-${Date.now()}`,
95
96
  label: payload.thinkingStep.label,
96
97
  content: payload.thinkingStep.content,
97
- depth: payload.thinkingStep.depth ?? 0,
98
+ depth: payload.thinkingStep.depth ?? payload.depth ?? 0,
98
99
  isCollapsed: payload.thinkingStep.isCollapsed,
100
+ timestamp: Date.now(),
101
+ agentName: payload.agentName,
102
+ parentAgent: payload.parentAgent,
99
103
  };
100
104
  const thinkingStartTime = prev.thinkingStartTime ?? Date.now();
101
- return {
105
+ const next = {
102
106
  ...prev,
103
107
  status: newStatus,
104
108
  thinkingSteps: [...(prev.thinkingSteps || []), newStep],
105
109
  thinkingStartTime,
106
110
  firstMessageTime,
107
111
  };
112
+ return { ...next, timelineEntries: buildTimelineEntries(next) };
108
113
  }
109
114
 
110
- // Plain text thinking (existing behavior)
115
+ // Plain text thinking concatenate for backward compat AND
116
+ // push a ThinkingStep so the timeline gets individual entries.
111
117
  const newThinking = payload.message || payload.content || "";
112
118
  // Add line break between thinking messages
113
119
  const separator = prev.thinking && newThinking ? "\n\n" : "";
114
120
  // Set thinkingStartTime on first thinking message
115
121
  const thinkingStartTime =
116
122
  prev.thinkingStartTime ?? (newThinking ? Date.now() : null);
117
- return {
123
+ const prevSteps = prev.thinkingSteps || [];
124
+ const plainStep: ThinkingStep = {
125
+ id: `step-${prevSteps.length}`,
126
+ label: newThinking,
127
+ content: newThinking,
128
+ depth: payload.depth ?? 0,
129
+ timestamp: Date.now(),
130
+ agentName: payload.agentName,
131
+ parentAgent: payload.parentAgent,
132
+ };
133
+ const next = {
118
134
  ...prev,
119
135
  status: newStatus,
120
136
  thinking: prev.thinking + separator + newThinking,
137
+ thinkingSteps: [...prevSteps, plainStep],
121
138
  thinkingStartTime,
122
139
  firstMessageTime,
123
140
  };
141
+ return { ...next, timelineEntries: buildTimelineEntries(next) };
124
142
  }
125
143
 
126
144
  case "tool_call": {
@@ -132,13 +150,17 @@ export function useAgentResponseAccumulator(
132
150
  name: toolName,
133
151
  arguments: payload.tool?.arguments,
134
152
  timestamp: Date.now(),
153
+ agentName: payload.agentName,
154
+ parentAgent: payload.parentAgent,
155
+ depth: payload.depth,
135
156
  };
136
- return {
157
+ const next = {
137
158
  ...prev,
138
159
  status: newStatus,
139
160
  toolCalls: [...prev.toolCalls, newToolCall],
140
161
  firstMessageTime,
141
162
  };
163
+ return { ...next, timelineEntries: buildTimelineEntries(next) };
142
164
  }
143
165
  return { ...prev, status: newStatus, firstMessageTime };
144
166
  }
@@ -152,13 +174,17 @@ export function useAgentResponseAccumulator(
152
174
  source: payload.knowledge?.source || "unknown",
153
175
  content: knowledgeContent,
154
176
  timestamp: Date.now(),
177
+ agentName: payload.agentName,
178
+ parentAgent: payload.parentAgent,
179
+ depth: payload.depth,
155
180
  };
156
- return {
181
+ const next = {
157
182
  ...prev,
158
183
  status: newStatus,
159
184
  knowledge: [...prev.knowledge, newKnowledge],
160
185
  firstMessageTime,
161
186
  };
187
+ return { ...next, timelineEntries: buildTimelineEntries(next) };
162
188
  }
163
189
  return { ...prev, status: newStatus, firstMessageTime };
164
190
  }
@@ -172,13 +198,17 @@ export function useAgentResponseAccumulator(
172
198
  type: payload.memory?.type || "unknown",
173
199
  content: memoryContent,
174
200
  timestamp: Date.now(),
201
+ agentName: payload.agentName,
202
+ parentAgent: payload.parentAgent,
203
+ depth: payload.depth,
175
204
  };
176
- return {
205
+ const next = {
177
206
  ...prev,
178
207
  status: newStatus,
179
208
  memory: [...prev.memory, newMemory],
180
209
  firstMessageTime,
181
210
  };
211
+ return { ...next, timelineEntries: buildTimelineEntries(next) };
182
212
  }
183
213
  return { ...prev, status: newStatus, firstMessageTime };
184
214
  }
@@ -200,13 +230,17 @@ export function useAgentResponseAccumulator(
200
230
  message: statusMessage,
201
231
  agent: payload.statusUpdate?.agent,
202
232
  timestamp: Date.now(),
233
+ agentName: payload.agentName,
234
+ parentAgent: payload.parentAgent,
235
+ depth: payload.depth,
203
236
  };
204
- return {
237
+ const next = {
205
238
  ...prev,
206
239
  status: newStatus,
207
240
  statusUpdates: [...prev.statusUpdates, newStatusItem],
208
241
  firstMessageTime,
209
242
  };
243
+ return { ...next, timelineEntries: buildTimelineEntries(next) };
210
244
  }
211
245
  return { ...prev, status: newStatus, firstMessageTime };
212
246
  }
@@ -53,3 +53,19 @@ export { initialAgentResponseState } from "./types";
53
53
 
54
54
  // Utilities
55
55
  export { formatTime, formatTotalTime } from "./utils";
56
+
57
+ // Agent Timeline (replaces ThinkingSection for rich execution visibility)
58
+ export {
59
+ AgentTimeline,
60
+ createTimelineUIState,
61
+ buildTimelineEntries,
62
+ groupIntoAgentRuns,
63
+ deduplicateEntries,
64
+ } from "../agent-timeline";
65
+ export type {
66
+ TimelineUIState,
67
+ TimelineEntry,
68
+ TimelineEntryType,
69
+ AgentRun,
70
+ DisplayEntry,
71
+ } from "../agent-timeline";
@@ -4,6 +4,8 @@
4
4
  * Type definitions for the library-ready agent response component
5
5
  */
6
6
 
7
+ import type { TimelineEntry } from "../agent-timeline/types";
8
+
7
9
  /**
8
10
  * Status of the agent response cycle
9
11
  */
@@ -22,6 +24,12 @@ export interface ToolCall {
22
24
  name: string;
23
25
  arguments?: Record<string, unknown>;
24
26
  timestamp: number;
27
+ /** Agent that made this call (multi-agent scenarios) */
28
+ agentName?: string | null;
29
+ /** Parent agent name */
30
+ parentAgent?: string | null;
31
+ /** Nesting depth in agent hierarchy */
32
+ depth?: number;
25
33
  }
26
34
 
27
35
  /**
@@ -32,6 +40,12 @@ export interface KnowledgeItem {
32
40
  source: string;
33
41
  content: string;
34
42
  timestamp: number;
43
+ /** Agent that retrieved this (multi-agent scenarios) */
44
+ agentName?: string | null;
45
+ /** Parent agent name */
46
+ parentAgent?: string | null;
47
+ /** Nesting depth in agent hierarchy */
48
+ depth?: number;
35
49
  }
36
50
 
37
51
  /**
@@ -42,6 +56,12 @@ export interface MemoryItem {
42
56
  type: string;
43
57
  content: string;
44
58
  timestamp: number;
59
+ /** Agent that accessed this (multi-agent scenarios) */
60
+ agentName?: string | null;
61
+ /** Parent agent name */
62
+ parentAgent?: string | null;
63
+ /** Nesting depth in agent hierarchy */
64
+ depth?: number;
45
65
  }
46
66
 
47
67
  /**
@@ -53,6 +73,12 @@ export interface StatusItem {
53
73
  timestamp: number;
54
74
  /** Optional agent name if in multi-agent scenario */
55
75
  agent?: string;
76
+ /** Agent that produced this (multi-agent scenarios) */
77
+ agentName?: string | null;
78
+ /** Parent agent name */
79
+ parentAgent?: string | null;
80
+ /** Nesting depth in agent hierarchy */
81
+ depth?: number;
56
82
  }
57
83
 
58
84
  /**
@@ -69,6 +95,12 @@ export interface ThinkingStep {
69
95
  depth: number;
70
96
  /** Whether this step should start collapsed (default: false) */
71
97
  isCollapsed?: boolean;
98
+ /** Timestamp for timeline ordering */
99
+ timestamp?: number;
100
+ /** Agent that produced this (multi-agent scenarios) */
101
+ agentName?: string | null;
102
+ /** Parent agent name */
103
+ parentAgent?: string | null;
72
104
  }
73
105
 
74
106
  /**
@@ -98,6 +130,8 @@ export interface AgentResponseState {
98
130
  statusUpdates: StatusItem[];
99
131
  /** Final response text */
100
132
  response: string;
133
+ /** Timeline entries derived from all accumulator arrays (for AgentTimeline) */
134
+ timelineEntries?: TimelineEntry[];
101
135
  /** Timestamp when first thinking message was received (for timer) */
102
136
  thinkingStartTime: number | null;
103
137
  /** Timestamp when response was completed (for final timer display) */
@@ -117,6 +151,14 @@ export interface AgentMessage {
117
151
  content?: string;
118
152
  /** For status messages */
119
153
  status?: string;
154
+ /** Agent name (multi-agent scenarios) */
155
+ agentName?: string | null;
156
+ /** Parent agent name (multi-agent scenarios) */
157
+ parentAgent?: string | null;
158
+ /** Agent nesting depth (0 = root) */
159
+ depth?: number;
160
+ /** Title/label for timeline display */
161
+ title?: string | null;
120
162
  /** For tool_call messages */
121
163
  tool?: {
122
164
  id: string;
@@ -0,0 +1,256 @@
1
+ import { useMemo, useRef, useState, useCallback, type ReactNode } from "react";
2
+ import {
3
+ Brain,
4
+ Wrench,
5
+ BookOpen,
6
+ HardDrive,
7
+ Activity,
8
+ MessageSquare,
9
+ AlertCircle,
10
+ ChevronsDownUp,
11
+ ChevronsUpDown,
12
+ } from "lucide-react";
13
+ import type { TimelineEntry, TimelineEntryType } from "./types";
14
+ import { groupIntoAgentRuns } from "./utils";
15
+ import { TimelineAgentBlock } from "./TimelineAgentBlock";
16
+
17
+ /** Externalized UI state that survives component remounts */
18
+ export interface TimelineUIState {
19
+ expandedItems: Set<string>;
20
+ collapsedRuns: Set<string>;
21
+ activeFilters: Set<TimelineEntryType>;
22
+ }
23
+
24
+ export function createTimelineUIState(): TimelineUIState {
25
+ return {
26
+ expandedItems: new Set(),
27
+ collapsedRuns: new Set(),
28
+ activeFilters: new Set(),
29
+ };
30
+ }
31
+
32
+ /** Icon + label config for each entry type */
33
+ const TYPE_CONFIG: {
34
+ type: TimelineEntryType;
35
+ icon: typeof Brain;
36
+ label: string;
37
+ }[] = [
38
+ { type: "status_update", icon: Activity, label: "Status" },
39
+ { type: "thinking", icon: Brain, label: "Thinking" },
40
+ { type: "tool_call", icon: Wrench, label: "Tools" },
41
+ { type: "knowledge", icon: BookOpen, label: "Knowledge" },
42
+ { type: "memory", icon: HardDrive, label: "Memory" },
43
+ { type: "ai_response", icon: MessageSquare, label: "AI" },
44
+ { type: "error", icon: AlertCircle, label: "Errors" },
45
+ ];
46
+
47
+ interface AgentTimelineProps {
48
+ entries: TimelineEntry[];
49
+ renderMarkdown?: (content: string) => ReactNode;
50
+ /**
51
+ * External UI state store. When provided, expand/collapse/filter state
52
+ * is read from and written to this object (which should be ref-backed
53
+ * in the parent) so it survives component remounts during streaming.
54
+ */
55
+ uiState?: TimelineUIState;
56
+ /**
57
+ * Maximum height of the scrollable timeline container.
58
+ * Defaults to "300px". Set to "none" to disable.
59
+ */
60
+ maxHeight?: string;
61
+ }
62
+
63
+ export function AgentTimeline({ entries, renderMarkdown, uiState, maxHeight = "300px" }: AgentTimelineProps) {
64
+ const containerRef = useRef<HTMLDivElement>(null);
65
+
66
+ // Render tick: forces re-renders AND invalidates useMemo deps when we mutate Sets in-place
67
+ const [renderTick, setRenderTick] = useState(0);
68
+ const forceRender = useCallback(() => setRenderTick((t) => t + 1), []);
69
+
70
+ // Resolve state: prefer external uiState (ref-backed, survives remounts)
71
+ // Fall back to internal state if no external store provided
72
+ const [internalExpandedItems] = useState<Set<string>>(() => new Set());
73
+ const [internalCollapsedRuns] = useState<Set<string>>(() => new Set());
74
+ const [internalActiveFilters] = useState<Set<TimelineEntryType>>(() => new Set());
75
+
76
+ const expandedItems = uiState?.expandedItems ?? internalExpandedItems;
77
+ const collapsedRuns = uiState?.collapsedRuns ?? internalCollapsedRuns;
78
+ const activeFilters = uiState?.activeFilters ?? internalActiveFilters;
79
+
80
+ // Compute which types actually exist in the entries
81
+ const availableTypes = useMemo(() => {
82
+ const types = new Set<TimelineEntryType>();
83
+ for (const entry of entries) {
84
+ types.add(entry.type);
85
+ }
86
+ return types;
87
+ }, [entries]);
88
+
89
+ // Filter entries, then group into runs
90
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- renderTick invalidates when mutated Sets change
91
+ const filteredEntries = useMemo(
92
+ () =>
93
+ activeFilters.size === 0
94
+ ? entries
95
+ : entries.filter((e) => activeFilters.has(e.type)),
96
+ [entries, activeFilters, renderTick],
97
+ );
98
+
99
+ // eslint-disable-next-line react-hooks/exhaustive-deps
100
+ const agentRuns = useMemo(() => groupIntoAgentRuns(filteredEntries), [filteredEntries, renderTick]);
101
+
102
+ // --- Mutators (mutate the set in-place + trigger re-render) ---
103
+
104
+ const toggleFilter = useCallback((type: TimelineEntryType) => {
105
+ if (activeFilters.has(type)) {
106
+ activeFilters.delete(type);
107
+ } else {
108
+ activeFilters.add(type);
109
+ }
110
+ forceRender();
111
+ }, [activeFilters, forceRender]);
112
+
113
+ const clearFilters = useCallback(() => {
114
+ activeFilters.clear();
115
+ forceRender();
116
+ }, [activeFilters, forceRender]);
117
+
118
+ const toggleItemExpanded = useCallback((entryId: string) => {
119
+ if (expandedItems.has(entryId)) {
120
+ expandedItems.delete(entryId);
121
+ } else {
122
+ expandedItems.add(entryId);
123
+ }
124
+ forceRender();
125
+ }, [expandedItems, forceRender]);
126
+
127
+ const collapseAll = useCallback(() => {
128
+ collapsedRuns.clear();
129
+ agentRuns.forEach((run, i) => {
130
+ collapsedRuns.add(`${run.agentName}-${i}`);
131
+ });
132
+ expandedItems.clear();
133
+ forceRender();
134
+ }, [agentRuns, collapsedRuns, expandedItems, forceRender]);
135
+
136
+ const expandAll = useCallback(() => {
137
+ collapsedRuns.clear();
138
+ agentRuns.forEach((run, i) => {
139
+ collapsedRuns.add(`${run.agentName}-${i}:expanded`);
140
+ });
141
+ forceRender();
142
+ }, [agentRuns, collapsedRuns, forceRender]);
143
+
144
+ if (entries.length === 0) return null;
145
+
146
+ const isSingle = agentRuns.length === 1;
147
+ const hasActiveFilter = activeFilters.size > 0;
148
+
149
+ const scrollStyle = maxHeight !== "none" ? { maxHeight } : undefined;
150
+
151
+ return (
152
+ <div
153
+ ref={containerRef}
154
+ className={`-mt-1 ${maxHeight !== "none" ? "overflow-y-auto scrollbar-thin" : ""}`}
155
+ style={scrollStyle}
156
+ >
157
+ {/* Filter + controls bar (sticky within scroll container) */}
158
+ <div className="sticky top-0 z-10 bg-background flex items-center gap-1 py-1.5 mb-1 border-b border-border/50 flex-wrap">
159
+ {/* Type filter chips */}
160
+ {TYPE_CONFIG.filter((tc) => availableTypes.has(tc.type)).map((tc) => {
161
+ const isActive = activeFilters.has(tc.type);
162
+ const count = entries.filter((e) => e.type === tc.type).length;
163
+ return (
164
+ <button
165
+ key={tc.type}
166
+ onClick={() => toggleFilter(tc.type)}
167
+ className={`inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] transition-colors ${
168
+ isActive
169
+ ? "bg-accent text-accent-foreground ring-1 ring-accent-foreground/20"
170
+ : "text-muted-foreground/60 hover:text-muted-foreground hover:bg-muted/50"
171
+ }`}
172
+ title={`${isActive ? "Hide" : "Show only"} ${tc.label}`}
173
+ >
174
+ <tc.icon className="w-3 h-3" />
175
+ <span>{count}</span>
176
+ </button>
177
+ );
178
+ })}
179
+
180
+ {/* Clear filter button */}
181
+ {hasActiveFilter && (
182
+ <button
183
+ onClick={clearFilters}
184
+ className="text-[10px] text-muted-foreground/60 hover:text-muted-foreground px-1"
185
+ >
186
+ Clear
187
+ </button>
188
+ )}
189
+
190
+ {/* Spacer */}
191
+ <div className="flex-1" />
192
+
193
+ {/* Collapse/expand all */}
194
+ {!isSingle && (
195
+ <>
196
+ <button
197
+ onClick={collapseAll}
198
+ className="inline-flex items-center gap-0.5 text-[10px] text-muted-foreground/60 hover:text-muted-foreground px-1 py-0.5 rounded hover:bg-muted/50 transition-colors"
199
+ title="Collapse all"
200
+ >
201
+ <ChevronsDownUp className="w-3 h-3" />
202
+ </button>
203
+ <button
204
+ onClick={expandAll}
205
+ className="inline-flex items-center gap-0.5 text-[10px] text-muted-foreground/60 hover:text-muted-foreground px-1 py-0.5 rounded hover:bg-muted/50 transition-colors"
206
+ title="Expand all"
207
+ >
208
+ <ChevronsUpDown className="w-3 h-3" />
209
+ </button>
210
+ </>
211
+ )}
212
+ </div>
213
+
214
+ {/* Timeline content */}
215
+ {filteredEntries.length === 0 ? (
216
+ <div className="text-[10px] text-muted-foreground/50 py-2 text-center">
217
+ No entries match the selected filters
218
+ </div>
219
+ ) : (
220
+ <div className="space-y-0.5">
221
+ {agentRuns.map((run, i) => {
222
+ const runKey = `${run.agentName}-${i}`;
223
+ const defaultCollapsed = run.depth > 0;
224
+ const isCollapsed = collapsedRuns.has(runKey)
225
+ ? true
226
+ : collapsedRuns.has(`${runKey}:expanded`)
227
+ ? false
228
+ : defaultCollapsed;
229
+
230
+ return (
231
+ <TimelineAgentBlock
232
+ key={runKey}
233
+ block={run}
234
+ renderMarkdown={renderMarkdown}
235
+ isSingleAgent={isSingle}
236
+ isCollapsed={isCollapsed}
237
+ onToggleCollapsed={() => {
238
+ if (isCollapsed) {
239
+ collapsedRuns.delete(runKey);
240
+ collapsedRuns.add(`${runKey}:expanded`);
241
+ } else {
242
+ collapsedRuns.delete(`${runKey}:expanded`);
243
+ collapsedRuns.add(runKey);
244
+ }
245
+ forceRender();
246
+ }}
247
+ expandedItems={expandedItems}
248
+ onToggleItemExpanded={toggleItemExpanded}
249
+ />
250
+ );
251
+ })}
252
+ </div>
253
+ )}
254
+ </div>
255
+ );
256
+ }