@optilogic/chat 1.0.0-beta.1 → 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/README.md +235 -0
- package/dist/index.cjs +1292 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +524 -6
- package/dist/index.d.ts +524 -6
- package/dist/index.js +1267 -33
- package/dist/index.js.map +1 -1
- package/package.json +15 -9
- package/src/components/agent-response/AgentResponse.tsx +99 -10
- package/src/components/agent-response/components/ActivityIndicators.tsx +36 -4
- package/src/components/agent-response/components/HITLSection.tsx +95 -0
- package/src/components/agent-response/components/MetadataRow.tsx +21 -6
- package/src/components/agent-response/components/ThinkingSection.tsx +102 -10
- package/src/components/agent-response/components/TruncatedMessage.tsx +52 -0
- package/src/components/agent-response/components/index.ts +6 -0
- package/src/components/agent-response/hooks/useAgentResponseAccumulator.ts +79 -4
- package/src/components/agent-response/index.ts +23 -0
- package/src/components/agent-response/types.ts +96 -1
- package/src/components/agent-timeline/AgentTimeline.tsx +256 -0
- package/src/components/agent-timeline/TimelineAgentBlock.tsx +84 -0
- package/src/components/agent-timeline/TimelineItem.tsx +97 -0
- package/src/components/agent-timeline/index.ts +14 -0
- package/src/components/agent-timeline/types.ts +49 -0
- package/src/components/agent-timeline/utils.ts +167 -0
- package/src/components/hitl-interactions/HITLInteractionRecord.tsx +139 -0
- package/src/components/hitl-interactions/HITLQuestionPanel.tsx +270 -0
- package/src/components/hitl-interactions/index.ts +18 -0
- package/src/components/inline-actions/ActionMarkdownRenderer.tsx +60 -0
- package/src/components/inline-actions/index.ts +18 -0
- package/src/components/inline-actions/parseResponseSegments.ts +66 -0
- package/src/components/inline-actions/prompts.ts +41 -0
- package/src/components/inline-actions/types.ts +57 -0
- package/src/components/user-prompt/UserPrompt.tsx +60 -0
- package/src/components/user-prompt/index.ts +1 -0
- package/src/components/user-prompt-input/UserPromptInput.tsx +326 -0
- package/src/components/user-prompt-input/index.ts +2 -0
- package/src/components/user-prompt-input/types.ts +52 -0
- package/src/index.ts +54 -0
|
@@ -13,7 +13,10 @@ import {
|
|
|
13
13
|
type ToolCall,
|
|
14
14
|
type KnowledgeItem,
|
|
15
15
|
type MemoryItem,
|
|
16
|
+
type StatusItem,
|
|
17
|
+
type ThinkingStep,
|
|
16
18
|
} from "../types";
|
|
19
|
+
import { buildTimelineEntries } from "../../agent-timeline/utils";
|
|
17
20
|
|
|
18
21
|
export interface UseAgentResponseAccumulatorOptions {
|
|
19
22
|
/** WebSocket topic to filter messages (optional, for convenience) */
|
|
@@ -86,19 +89,56 @@ export function useAgentResponseAccumulator(
|
|
|
86
89
|
return { ...prev, status: newStatus };
|
|
87
90
|
|
|
88
91
|
case "thinking": {
|
|
92
|
+
// Check if this is a structured thinking step
|
|
93
|
+
if (payload.thinkingStep) {
|
|
94
|
+
const newStep: ThinkingStep = {
|
|
95
|
+
id: payload.thinkingStep.id || `step-${Date.now()}`,
|
|
96
|
+
label: payload.thinkingStep.label,
|
|
97
|
+
content: payload.thinkingStep.content,
|
|
98
|
+
depth: payload.thinkingStep.depth ?? payload.depth ?? 0,
|
|
99
|
+
isCollapsed: payload.thinkingStep.isCollapsed,
|
|
100
|
+
timestamp: Date.now(),
|
|
101
|
+
agentName: payload.agentName,
|
|
102
|
+
parentAgent: payload.parentAgent,
|
|
103
|
+
};
|
|
104
|
+
const thinkingStartTime = prev.thinkingStartTime ?? Date.now();
|
|
105
|
+
const next = {
|
|
106
|
+
...prev,
|
|
107
|
+
status: newStatus,
|
|
108
|
+
thinkingSteps: [...(prev.thinkingSteps || []), newStep],
|
|
109
|
+
thinkingStartTime,
|
|
110
|
+
firstMessageTime,
|
|
111
|
+
};
|
|
112
|
+
return { ...next, timelineEntries: buildTimelineEntries(next) };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Plain text thinking — concatenate for backward compat AND
|
|
116
|
+
// push a ThinkingStep so the timeline gets individual entries.
|
|
89
117
|
const newThinking = payload.message || payload.content || "";
|
|
90
118
|
// Add line break between thinking messages
|
|
91
119
|
const separator = prev.thinking && newThinking ? "\n\n" : "";
|
|
92
120
|
// Set thinkingStartTime on first thinking message
|
|
93
121
|
const thinkingStartTime =
|
|
94
122
|
prev.thinkingStartTime ?? (newThinking ? Date.now() : null);
|
|
95
|
-
|
|
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 = {
|
|
96
134
|
...prev,
|
|
97
135
|
status: newStatus,
|
|
98
136
|
thinking: prev.thinking + separator + newThinking,
|
|
137
|
+
thinkingSteps: [...prevSteps, plainStep],
|
|
99
138
|
thinkingStartTime,
|
|
100
139
|
firstMessageTime,
|
|
101
140
|
};
|
|
141
|
+
return { ...next, timelineEntries: buildTimelineEntries(next) };
|
|
102
142
|
}
|
|
103
143
|
|
|
104
144
|
case "tool_call": {
|
|
@@ -110,13 +150,17 @@ export function useAgentResponseAccumulator(
|
|
|
110
150
|
name: toolName,
|
|
111
151
|
arguments: payload.tool?.arguments,
|
|
112
152
|
timestamp: Date.now(),
|
|
153
|
+
agentName: payload.agentName,
|
|
154
|
+
parentAgent: payload.parentAgent,
|
|
155
|
+
depth: payload.depth,
|
|
113
156
|
};
|
|
114
|
-
|
|
157
|
+
const next = {
|
|
115
158
|
...prev,
|
|
116
159
|
status: newStatus,
|
|
117
160
|
toolCalls: [...prev.toolCalls, newToolCall],
|
|
118
161
|
firstMessageTime,
|
|
119
162
|
};
|
|
163
|
+
return { ...next, timelineEntries: buildTimelineEntries(next) };
|
|
120
164
|
}
|
|
121
165
|
return { ...prev, status: newStatus, firstMessageTime };
|
|
122
166
|
}
|
|
@@ -130,13 +174,17 @@ export function useAgentResponseAccumulator(
|
|
|
130
174
|
source: payload.knowledge?.source || "unknown",
|
|
131
175
|
content: knowledgeContent,
|
|
132
176
|
timestamp: Date.now(),
|
|
177
|
+
agentName: payload.agentName,
|
|
178
|
+
parentAgent: payload.parentAgent,
|
|
179
|
+
depth: payload.depth,
|
|
133
180
|
};
|
|
134
|
-
|
|
181
|
+
const next = {
|
|
135
182
|
...prev,
|
|
136
183
|
status: newStatus,
|
|
137
184
|
knowledge: [...prev.knowledge, newKnowledge],
|
|
138
185
|
firstMessageTime,
|
|
139
186
|
};
|
|
187
|
+
return { ...next, timelineEntries: buildTimelineEntries(next) };
|
|
140
188
|
}
|
|
141
189
|
return { ...prev, status: newStatus, firstMessageTime };
|
|
142
190
|
}
|
|
@@ -150,13 +198,17 @@ export function useAgentResponseAccumulator(
|
|
|
150
198
|
type: payload.memory?.type || "unknown",
|
|
151
199
|
content: memoryContent,
|
|
152
200
|
timestamp: Date.now(),
|
|
201
|
+
agentName: payload.agentName,
|
|
202
|
+
parentAgent: payload.parentAgent,
|
|
203
|
+
depth: payload.depth,
|
|
153
204
|
};
|
|
154
|
-
|
|
205
|
+
const next = {
|
|
155
206
|
...prev,
|
|
156
207
|
status: newStatus,
|
|
157
208
|
memory: [...prev.memory, newMemory],
|
|
158
209
|
firstMessageTime,
|
|
159
210
|
};
|
|
211
|
+
return { ...next, timelineEntries: buildTimelineEntries(next) };
|
|
160
212
|
}
|
|
161
213
|
return { ...prev, status: newStatus, firstMessageTime };
|
|
162
214
|
}
|
|
@@ -170,6 +222,29 @@ export function useAgentResponseAccumulator(
|
|
|
170
222
|
firstMessageTime: prev.firstMessageTime ?? Date.now(),
|
|
171
223
|
};
|
|
172
224
|
|
|
225
|
+
case "status_update": {
|
|
226
|
+
const statusMessage = payload.message || payload.statusUpdate?.message;
|
|
227
|
+
if (statusMessage) {
|
|
228
|
+
const newStatusItem: StatusItem = {
|
|
229
|
+
id: payload.statusUpdate?.id || `status-${Date.now()}`,
|
|
230
|
+
message: statusMessage,
|
|
231
|
+
agent: payload.statusUpdate?.agent,
|
|
232
|
+
timestamp: Date.now(),
|
|
233
|
+
agentName: payload.agentName,
|
|
234
|
+
parentAgent: payload.parentAgent,
|
|
235
|
+
depth: payload.depth,
|
|
236
|
+
};
|
|
237
|
+
const next = {
|
|
238
|
+
...prev,
|
|
239
|
+
status: newStatus,
|
|
240
|
+
statusUpdates: [...prev.statusUpdates, newStatusItem],
|
|
241
|
+
firstMessageTime,
|
|
242
|
+
};
|
|
243
|
+
return { ...next, timelineEntries: buildTimelineEntries(next) };
|
|
244
|
+
}
|
|
245
|
+
return { ...prev, status: newStatus, firstMessageTime };
|
|
246
|
+
}
|
|
247
|
+
|
|
173
248
|
default:
|
|
174
249
|
return { ...prev, status: newStatus, firstMessageTime };
|
|
175
250
|
}
|
|
@@ -15,10 +15,14 @@ export {
|
|
|
15
15
|
MetadataRow,
|
|
16
16
|
ThinkingSection,
|
|
17
17
|
ActionBar,
|
|
18
|
+
HITLSection,
|
|
19
|
+
TruncatedMessage,
|
|
18
20
|
type ActivityIndicatorsProps,
|
|
19
21
|
type MetadataRowProps,
|
|
20
22
|
type ThinkingSectionProps,
|
|
21
23
|
type ActionBarProps,
|
|
24
|
+
type HITLSectionProps,
|
|
25
|
+
type TruncatedMessageProps,
|
|
22
26
|
} from "./components";
|
|
23
27
|
|
|
24
28
|
// Hooks
|
|
@@ -38,6 +42,9 @@ export type {
|
|
|
38
42
|
ToolCall,
|
|
39
43
|
KnowledgeItem,
|
|
40
44
|
MemoryItem,
|
|
45
|
+
StatusItem,
|
|
46
|
+
ThinkingStep,
|
|
47
|
+
ThinkingContent,
|
|
41
48
|
AgentMessage,
|
|
42
49
|
GenericWebSocketMessage,
|
|
43
50
|
} from "./types";
|
|
@@ -46,3 +53,19 @@ export { initialAgentResponseState } from "./types";
|
|
|
46
53
|
|
|
47
54
|
// Utilities
|
|
48
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,8 +56,60 @@ 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;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Status update information from the agent
|
|
69
|
+
*/
|
|
70
|
+
export interface StatusItem {
|
|
71
|
+
id: string;
|
|
72
|
+
message: string;
|
|
73
|
+
timestamp: number;
|
|
74
|
+
/** Optional agent name if in multi-agent scenario */
|
|
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;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* A single step in structured thinking content
|
|
86
|
+
*/
|
|
87
|
+
export interface ThinkingStep {
|
|
88
|
+
/** Unique identifier for the step */
|
|
89
|
+
id: string;
|
|
90
|
+
/** Label/title shown in the collapsible header */
|
|
91
|
+
label: string;
|
|
92
|
+
/** Content of the thinking step */
|
|
93
|
+
content: string;
|
|
94
|
+
/** Nesting depth (0 = root level, 1 = first indent, etc.) */
|
|
95
|
+
depth: number;
|
|
96
|
+
/** Whether this step should start collapsed (default: false) */
|
|
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;
|
|
45
104
|
}
|
|
46
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Union type for thinking content
|
|
108
|
+
* - string: plain text (backward compatible)
|
|
109
|
+
* - ThinkingStep[]: structured with collapsible sub-sections
|
|
110
|
+
*/
|
|
111
|
+
export type ThinkingContent = string | ThinkingStep[];
|
|
112
|
+
|
|
47
113
|
/**
|
|
48
114
|
* State shape for the agent response component
|
|
49
115
|
*/
|
|
@@ -52,14 +118,20 @@ export interface AgentResponseState {
|
|
|
52
118
|
status: AgentResponseStatus;
|
|
53
119
|
/** Accumulated thinking/reasoning text */
|
|
54
120
|
thinking: string;
|
|
121
|
+
/** Structured thinking steps (if provided, takes precedence over thinking string) */
|
|
122
|
+
thinkingSteps?: ThinkingStep[];
|
|
55
123
|
/** Tool calls made during processing */
|
|
56
124
|
toolCalls: ToolCall[];
|
|
57
125
|
/** Knowledge items retrieved */
|
|
58
126
|
knowledge: KnowledgeItem[];
|
|
59
127
|
/** Memory items accessed */
|
|
60
128
|
memory: MemoryItem[];
|
|
129
|
+
/** Status updates from the agent */
|
|
130
|
+
statusUpdates: StatusItem[];
|
|
61
131
|
/** Final response text */
|
|
62
132
|
response: string;
|
|
133
|
+
/** Timeline entries derived from all accumulator arrays (for AgentTimeline) */
|
|
134
|
+
timelineEntries?: TimelineEntry[];
|
|
63
135
|
/** Timestamp when first thinking message was received (for timer) */
|
|
64
136
|
thinkingStartTime: number | null;
|
|
65
137
|
/** Timestamp when response was completed (for final timer display) */
|
|
@@ -72,13 +144,21 @@ export interface AgentResponseState {
|
|
|
72
144
|
* WebSocket message payload for agent responses
|
|
73
145
|
*/
|
|
74
146
|
export interface AgentMessage {
|
|
75
|
-
type: "status" | "thinking" | "tool_call" | "knowledge" | "memory" | "response";
|
|
147
|
+
type: "status" | "thinking" | "tool_call" | "knowledge" | "memory" | "response" | "status_update";
|
|
76
148
|
/** Message content - for simple string payloads */
|
|
77
149
|
message?: string;
|
|
78
150
|
/** Alternative content field */
|
|
79
151
|
content?: string;
|
|
80
152
|
/** For status messages */
|
|
81
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;
|
|
82
162
|
/** For tool_call messages */
|
|
83
163
|
tool?: {
|
|
84
164
|
id: string;
|
|
@@ -97,6 +177,20 @@ export interface AgentMessage {
|
|
|
97
177
|
type: string;
|
|
98
178
|
content: string;
|
|
99
179
|
};
|
|
180
|
+
/** For status_update messages */
|
|
181
|
+
statusUpdate?: {
|
|
182
|
+
id: string;
|
|
183
|
+
message: string;
|
|
184
|
+
agent?: string;
|
|
185
|
+
};
|
|
186
|
+
/** For structured thinking step messages */
|
|
187
|
+
thinkingStep?: {
|
|
188
|
+
id?: string;
|
|
189
|
+
label: string;
|
|
190
|
+
content: string;
|
|
191
|
+
depth?: number;
|
|
192
|
+
isCollapsed?: boolean;
|
|
193
|
+
};
|
|
100
194
|
}
|
|
101
195
|
|
|
102
196
|
/**
|
|
@@ -117,6 +211,7 @@ export const initialAgentResponseState: AgentResponseState = {
|
|
|
117
211
|
toolCalls: [],
|
|
118
212
|
knowledge: [],
|
|
119
213
|
memory: [],
|
|
214
|
+
statusUpdates: [],
|
|
120
215
|
response: "",
|
|
121
216
|
thinkingStartTime: null,
|
|
122
217
|
responseCompleteTime: null,
|
|
@@ -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
|
+
}
|