@optilogic/chat 1.3.3 → 1.3.5

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.
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Pure reducer for agent response messages.
3
+ *
4
+ * Used by `useAgentResponseAccumulator` for live streaming, and exported so
5
+ * non-React callers (e.g. server-side or store-based replay of historical
6
+ * conversations) can rebuild the same `AgentResponseState` from a sequence of
7
+ * `AgentMessage` events without rendering a component.
8
+ *
9
+ * Replay note on stable IDs: when a payload omits a per-item id
10
+ * (`tool.id`, `thinkingStep.id`, `knowledge.id`, `memory.id`,
11
+ * `statusUpdate.id`), the reducer falls back to `${type}-${Date.now()}`. That
12
+ * is fine for live streams but will produce different React keys on each
13
+ * replay. Callers reconstructing `AgentMessage` events from persisted rows
14
+ * should populate these ids from a stable source (e.g. the supplement-row
15
+ * primary key) so timeline keys remain consistent across reloads.
16
+ */
17
+
18
+ import {
19
+ initialAgentResponseState,
20
+ type AgentResponseState,
21
+ type AgentMessage,
22
+ type ToolCall,
23
+ type KnowledgeItem,
24
+ type MemoryItem,
25
+ type StatusItem,
26
+ type ThinkingStep,
27
+ type PotentialResponse,
28
+ } from "./types";
29
+ import { buildTimelineEntries } from "../agent-timeline/utils";
30
+
31
+ /**
32
+ * Pure reducer: apply a single `AgentMessage` to the accumulated state.
33
+ *
34
+ * `payload.timestamp` (epoch ms), if supplied, is used for the new item's
35
+ * `timestamp` and any state-level timing fields this call sets
36
+ * (`firstMessageTime`, `thinkingStartTime`, `responseCompleteTime`). When
37
+ * absent, `Date.now()` is used — matching the prior live-streaming behaviour.
38
+ *
39
+ * @example
40
+ * const state = events.reduce(reduceAgentMessage, initialAgentResponseState);
41
+ */
42
+ export function reduceAgentMessage(
43
+ prev: AgentResponseState,
44
+ payload: AgentMessage,
45
+ ): AgentResponseState {
46
+ const now = payload.timestamp ?? Date.now();
47
+
48
+ // If we receive a non-status message while idle, transition to processing
49
+ let newStatus = prev.status;
50
+ const isFirstMessage = prev.status === "idle" && payload.type !== "status";
51
+ if (isFirstMessage) {
52
+ newStatus = "processing";
53
+ }
54
+
55
+ // Track first message time for total time calculation
56
+ const firstMessageTime =
57
+ prev.firstMessageTime ?? (isFirstMessage ? now : null);
58
+
59
+ switch (payload.type) {
60
+ case "status":
61
+ // "Harness connected" resets to idle
62
+ if (
63
+ payload.message === "Harness connected" ||
64
+ payload.status === "Harness connected"
65
+ ) {
66
+ return { ...initialAgentResponseState };
67
+ }
68
+ return { ...prev, status: newStatus };
69
+
70
+ case "thinking": {
71
+ // Check if this is a structured thinking step
72
+ if (payload.thinkingStep) {
73
+ const newStep: ThinkingStep = {
74
+ id: payload.thinkingStep.id || `step-${now}`,
75
+ label: payload.thinkingStep.label,
76
+ content: payload.thinkingStep.content,
77
+ depth: payload.thinkingStep.depth ?? payload.depth ?? 0,
78
+ isCollapsed: payload.thinkingStep.isCollapsed,
79
+ timestamp: now,
80
+ agentName: payload.agentName,
81
+ parentAgent: payload.parentAgent,
82
+ };
83
+ const thinkingStartTime = prev.thinkingStartTime ?? now;
84
+ const next = {
85
+ ...prev,
86
+ status: newStatus,
87
+ thinkingSteps: [...(prev.thinkingSteps || []), newStep],
88
+ thinkingStartTime,
89
+ firstMessageTime,
90
+ };
91
+ return { ...next, timelineEntries: buildTimelineEntries(next) };
92
+ }
93
+
94
+ // Plain text thinking — concatenate for backward compat AND
95
+ // push a ThinkingStep so the timeline gets individual entries.
96
+ const newThinking = payload.message || payload.content || "";
97
+ // Add line break between thinking messages
98
+ const separator = prev.thinking && newThinking ? "\n\n" : "";
99
+ // Set thinkingStartTime on first thinking message
100
+ const thinkingStartTime =
101
+ prev.thinkingStartTime ?? (newThinking ? now : null);
102
+ const prevSteps = prev.thinkingSteps || [];
103
+ const plainStep: ThinkingStep = {
104
+ id: `step-${prevSteps.length}`,
105
+ label: newThinking,
106
+ content: newThinking,
107
+ depth: payload.depth ?? 0,
108
+ timestamp: now,
109
+ agentName: payload.agentName,
110
+ parentAgent: payload.parentAgent,
111
+ };
112
+ const next = {
113
+ ...prev,
114
+ status: newStatus,
115
+ thinking: prev.thinking + separator + newThinking,
116
+ thinkingSteps: [...prevSteps, plainStep],
117
+ thinkingStartTime,
118
+ firstMessageTime,
119
+ };
120
+ return { ...next, timelineEntries: buildTimelineEntries(next) };
121
+ }
122
+
123
+ case "tool_call": {
124
+ // Handle both formats: { message: "ToolName" } or { tool: { id, name, arguments } }
125
+ const toolName = payload.message || payload.tool?.name;
126
+ if (toolName) {
127
+ const newToolCall: ToolCall = {
128
+ id: payload.tool?.id || `tool-${now}`,
129
+ name: toolName,
130
+ arguments: payload.tool?.arguments,
131
+ timestamp: now,
132
+ agentName: payload.agentName,
133
+ parentAgent: payload.parentAgent,
134
+ depth: payload.depth,
135
+ };
136
+ const next = {
137
+ ...prev,
138
+ status: newStatus,
139
+ toolCalls: [...prev.toolCalls, newToolCall],
140
+ firstMessageTime,
141
+ };
142
+ return { ...next, timelineEntries: buildTimelineEntries(next) };
143
+ }
144
+ return { ...prev, status: newStatus, firstMessageTime };
145
+ }
146
+
147
+ case "knowledge": {
148
+ // Handle both formats: { message: "content" } or { knowledge: { id, source, content } }
149
+ const knowledgeContent = payload.message || payload.knowledge?.content;
150
+ if (knowledgeContent) {
151
+ const newKnowledge: KnowledgeItem = {
152
+ id: payload.knowledge?.id || `knowledge-${now}`,
153
+ source: payload.knowledge?.source || "unknown",
154
+ content: knowledgeContent,
155
+ timestamp: now,
156
+ agentName: payload.agentName,
157
+ parentAgent: payload.parentAgent,
158
+ depth: payload.depth,
159
+ };
160
+ const next = {
161
+ ...prev,
162
+ status: newStatus,
163
+ knowledge: [...prev.knowledge, newKnowledge],
164
+ firstMessageTime,
165
+ };
166
+ return { ...next, timelineEntries: buildTimelineEntries(next) };
167
+ }
168
+ return { ...prev, status: newStatus, firstMessageTime };
169
+ }
170
+
171
+ case "memory": {
172
+ // Handle both formats: { message: "content" } or { memory: { id, type, content } }
173
+ const memoryContent = payload.message || payload.memory?.content;
174
+ if (memoryContent) {
175
+ const newMemory: MemoryItem = {
176
+ id: payload.memory?.id || `memory-${now}`,
177
+ type: payload.memory?.type || "unknown",
178
+ content: memoryContent,
179
+ timestamp: now,
180
+ agentName: payload.agentName,
181
+ parentAgent: payload.parentAgent,
182
+ depth: payload.depth,
183
+ };
184
+ const next = {
185
+ ...prev,
186
+ status: newStatus,
187
+ memory: [...prev.memory, newMemory],
188
+ firstMessageTime,
189
+ };
190
+ return { ...next, timelineEntries: buildTimelineEntries(next) };
191
+ }
192
+ return { ...prev, status: newStatus, firstMessageTime };
193
+ }
194
+
195
+ case "response":
196
+ return {
197
+ ...prev,
198
+ status: "complete",
199
+ response: payload.message || payload.content || "",
200
+ responseCompleteTime: now,
201
+ firstMessageTime: prev.firstMessageTime ?? now,
202
+ };
203
+
204
+ case "status_update": {
205
+ const statusMessage = payload.message || payload.statusUpdate?.message;
206
+ if (statusMessage) {
207
+ const newStatusItem: StatusItem = {
208
+ id: payload.statusUpdate?.id || `status-${now}`,
209
+ message: statusMessage,
210
+ agent: payload.statusUpdate?.agent,
211
+ timestamp: now,
212
+ agentName: payload.agentName,
213
+ parentAgent: payload.parentAgent,
214
+ depth: payload.depth,
215
+ };
216
+ const next = {
217
+ ...prev,
218
+ status: newStatus,
219
+ statusUpdates: [...prev.statusUpdates, newStatusItem],
220
+ firstMessageTime,
221
+ };
222
+ return { ...next, timelineEntries: buildTimelineEntries(next) };
223
+ }
224
+ return { ...prev, status: newStatus, firstMessageTime };
225
+ }
226
+
227
+ case "potential_response": {
228
+ const respContent = payload.message || payload.content || "";
229
+ if (respContent) {
230
+ const newResp: PotentialResponse = {
231
+ id: `resp-${now}`,
232
+ content: respContent,
233
+ timestamp: now,
234
+ agentName: payload.agentName,
235
+ parentAgent: payload.parentAgent,
236
+ depth: payload.depth,
237
+ };
238
+ const next = {
239
+ ...prev,
240
+ status: newStatus,
241
+ potentialResponses: [...(prev.potentialResponses || []), newResp],
242
+ firstMessageTime,
243
+ };
244
+ return { ...next, timelineEntries: buildTimelineEntries(next) };
245
+ }
246
+ return { ...prev, status: newStatus, firstMessageTime };
247
+ }
248
+
249
+ default:
250
+ return { ...prev, status: newStatus, firstMessageTime };
251
+ }
252
+ }
@@ -165,6 +165,14 @@ export interface AgentMessage {
165
165
  message?: string;
166
166
  /** Alternative content field */
167
167
  content?: string;
168
+ /**
169
+ * Optional event timestamp (epoch ms). When supplied, the reducer uses this
170
+ * for the item's `timestamp` and any state-level timing fields it sets
171
+ * (`firstMessageTime`, `thinkingStartTime`, `responseCompleteTime`) instead
172
+ * of `Date.now()`. Provide this when replaying historical events so durations
173
+ * reflect the original run rather than load time.
174
+ */
175
+ timestamp?: number;
168
176
  /** For status messages */
169
177
  status?: string;
170
178
  /** Agent name (multi-agent scenarios) */
@@ -155,6 +155,7 @@ export const UserPromptInput = React.forwardRef<
155
155
  enableTags = false,
156
156
  onTagCreate,
157
157
  onTagDelete,
158
+ anchors,
158
159
  className,
159
160
  ...props
160
161
  },
@@ -298,6 +299,7 @@ export const UserPromptInput = React.forwardRef<
298
299
  {isSubmitting && onStop ? (
299
300
  <Tooltip content={stopTooltip} disabled={!stopTooltip}>
300
301
  <IconButton
302
+ data-tour={anchors?.stopButton}
301
303
  icon={<Square />}
302
304
  variant="filled"
303
305
  size="sm"
@@ -308,6 +310,7 @@ export const UserPromptInput = React.forwardRef<
308
310
  </Tooltip>
309
311
  ) : (
310
312
  <IconButton
313
+ data-tour={anchors?.sendButton}
311
314
  icon={
312
315
  isSubmitting ? (
313
316
  <Loader2 className="animate-spin" />
@@ -42,6 +42,19 @@ export interface UserPromptInputProps
42
42
  onTagCreate?: (tag: string) => void;
43
43
  /** Callback when a tag is deleted */
44
44
  onTagDelete?: (tag: string) => void;
45
+ /** Tour anchors threaded onto internal sub-elements. */
46
+ anchors?: UserPromptInputAnchors;
47
+ }
48
+
49
+ /**
50
+ * Tour anchors (`data-tour` attribute values) for elements inside the
51
+ * UserPromptInput that the caller cannot otherwise reach.
52
+ */
53
+ export interface UserPromptInputAnchors {
54
+ /** Send button (rendered when not submitting). */
55
+ sendButton?: string;
56
+ /** Stop button (rendered while submitting if `onStop` is provided). */
57
+ stopButton?: string;
45
58
  }
46
59
 
47
60
  export interface UserPromptInputRef {
package/src/index.ts CHANGED
@@ -9,6 +9,8 @@ export {
9
9
  // Main component
10
10
  AgentResponse,
11
11
  type AgentResponseProps,
12
+ type AgentResponseClassNames,
13
+ type AgentResponseAnchors,
12
14
 
13
15
  // Sub-components (for advanced customization)
14
16
  ActivityIndicators,
@@ -48,6 +50,9 @@ export {
48
50
  // Constants
49
51
  initialAgentResponseState,
50
52
 
53
+ // Pure reducer (for non-React replay of AgentMessage streams)
54
+ reduceAgentMessage,
55
+
51
56
  // Utilities
52
57
  formatTime,
53
58
  formatTotalTime,