@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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optilogic/chat",
3
- "version": "1.3.3",
3
+ "version": "1.3.5",
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/editor": "1.3.3",
28
- "@optilogic/core": "1.3.3"
27
+ "@optilogic/core": "1.3.5",
28
+ "@optilogic/editor": "1.3.5"
29
29
  },
30
30
  "peerDependencies": {
31
31
  "react": "^18.0.0 || ^19.0.0",
@@ -22,6 +22,42 @@ export interface AgentResponseClassNames {
22
22
  response?: string;
23
23
  }
24
24
 
25
+ /**
26
+ * Tour anchors (`data-tour` attribute values) to attach to specific elements
27
+ * inside an AgentResponse so a tour or test harness can target them.
28
+ *
29
+ * Each value, when provided, is rendered as `data-tour={value}` on the
30
+ * corresponding element. Omit a field to skip stamping the attribute on that
31
+ * element. Pair with the matching CSS selector form:
32
+ *
33
+ * <AgentResponse anchors={{ copyAction: "chat-action-copy" }} />
34
+ * // selector: '[data-tour="chat-action-copy"]'
35
+ *
36
+ * This is the standard convention in the @optilogic component library for
37
+ * threading tour IDs into elements that are rendered by internal
38
+ * sub-components and not otherwise reachable from the caller.
39
+ */
40
+ export interface AgentResponseAnchors {
41
+ /** Status updates popover trigger in the metadata row (live status indicator). */
42
+ statusUpdate?: string;
43
+ /** Thinking expand/collapse toggle button in the metadata row. */
44
+ thinkingToggle?: string;
45
+ /** Thinking section content panel (when expanded). */
46
+ thinkingSection?: string;
47
+ /** Tool calls popover trigger in the metadata row. */
48
+ toolCalls?: string;
49
+ /** Knowledge popover trigger in the metadata row. */
50
+ knowledge?: string;
51
+ /** Memory popover trigger in the metadata row. */
52
+ memory?: string;
53
+ /** Copy button in the action bar. */
54
+ copyAction?: string;
55
+ /** Thumbs up button in the action bar. */
56
+ thumbsUp?: string;
57
+ /** Thumbs down button in the action bar. */
58
+ thumbsDown?: string;
59
+ }
60
+
25
61
  export interface AgentResponseProps extends React.HTMLAttributes<HTMLDivElement> {
26
62
  /** The response state to render */
27
63
  state: AgentResponseState;
@@ -121,6 +157,22 @@ export interface AgentResponseProps extends React.HTMLAttributes<HTMLDivElement>
121
157
  * />
122
158
  */
123
159
  classNames?: AgentResponseClassNames;
160
+
161
+ /**
162
+ * Tour anchors threaded into internal sub-components. See
163
+ * {@link AgentResponseAnchors} for the available element keys.
164
+ *
165
+ * @example
166
+ * <AgentResponse
167
+ * state={state}
168
+ * anchors={{
169
+ * copyAction: "chat-action-copy",
170
+ * thumbsUp: "chat-action-thumbs-up",
171
+ * thumbsDown: "chat-action-thumbs-down",
172
+ * }}
173
+ * />
174
+ */
175
+ anchors?: AgentResponseAnchors;
124
176
  }
125
177
 
126
178
  /**
@@ -175,6 +227,7 @@ const AgentResponse = React.forwardRef<HTMLDivElement, AgentResponseProps>(
175
227
  renderThinkingMarkdown,
176
228
  timelineMaxHeight,
177
229
  classNames,
230
+ anchors,
178
231
  className,
179
232
  ...props
180
233
  },
@@ -280,12 +333,17 @@ const AgentResponse = React.forwardRef<HTMLDivElement, AgentResponseProps>(
280
333
  statusContent={statusContent}
281
334
  status={state.status}
282
335
  elapsedTime={elapsedTime}
336
+ thinkingToggleAnchor={anchors?.thinkingToggle}
337
+ statusUpdateAnchor={anchors?.statusUpdate}
338
+ toolCallsAnchor={anchors?.toolCalls}
339
+ knowledgeAnchor={anchors?.knowledge}
340
+ memoryAnchor={anchors?.memory}
283
341
  />
284
342
 
285
343
  {/* Thinking Content - AgentTimeline when timeline entries exist, ThinkingSection otherwise */}
286
344
  {hasTimelineEntries ? (
287
345
  thinkingExpanded && (
288
- <div className="pb-3 border-t border-border">
346
+ <div className="pb-3 border-t border-border" data-tour={anchors?.thinkingSection}>
289
347
  <AgentTimeline
290
348
  entries={state.timelineEntries!}
291
349
  renderMarkdown={renderThinkingMarkdown}
@@ -296,6 +354,7 @@ const AgentResponse = React.forwardRef<HTMLDivElement, AgentResponseProps>(
296
354
  )
297
355
  ) : (
298
356
  <ThinkingSection
357
+ data-tour={anchors?.thinkingSection}
299
358
  content={
300
359
  state.thinkingSteps && state.thinkingSteps.length > 0
301
360
  ? state.thinkingSteps
@@ -343,6 +402,9 @@ const AgentResponse = React.forwardRef<HTMLDivElement, AgentResponseProps>(
343
402
  feedback={feedback}
344
403
  onFeedbackChange={onFeedbackChange}
345
404
  onResponseCopy={onResponseCopy}
405
+ copyAnchor={anchors?.copyAction}
406
+ thumbsUpAnchor={anchors?.thumbsUp}
407
+ thumbsDownAnchor={anchors?.thumbsDown}
346
408
  />
347
409
  )}
348
410
  </div>
@@ -24,6 +24,15 @@ export interface ActionBarProps extends React.HTMLAttributes<HTMLDivElement> {
24
24
  onFeedbackChange?: (feedback: FeedbackValue) => void;
25
25
  /** Callback when response is copied */
26
26
  onResponseCopy?: (response: string) => void;
27
+ /**
28
+ * Tour anchor (rendered as `data-tour`) on the copy button.
29
+ * Use with the `data-tour` convention to target this button from a tour step.
30
+ */
31
+ copyAnchor?: string;
32
+ /** Tour anchor (`data-tour`) on the thumbs up button. */
33
+ thumbsUpAnchor?: string;
34
+ /** Tour anchor (`data-tour`) on the thumbs down button. */
35
+ thumbsDownAnchor?: string;
27
36
  }
28
37
 
29
38
  /**
@@ -51,6 +60,9 @@ const ActionBar = React.forwardRef<HTMLDivElement, ActionBarProps>(
51
60
  feedback,
52
61
  onFeedbackChange,
53
62
  onResponseCopy,
63
+ copyAnchor,
64
+ thumbsUpAnchor,
65
+ thumbsDownAnchor,
54
66
  className,
55
67
  ...props
56
68
  },
@@ -97,6 +109,7 @@ const ActionBar = React.forwardRef<HTMLDivElement, ActionBarProps>(
97
109
  <div className="flex items-center gap-1">
98
110
  {/* Copy button */}
99
111
  <button
112
+ data-tour={copyAnchor}
100
113
  onClick={handleCopy}
101
114
  className="p-1.5 rounded hover:bg-muted transition-colors text-muted-foreground hover:text-foreground"
102
115
  title={copied ? "Copied!" : "Copy response"}
@@ -110,6 +123,7 @@ const ActionBar = React.forwardRef<HTMLDivElement, ActionBarProps>(
110
123
 
111
124
  {/* Thumbs up */}
112
125
  <button
126
+ data-tour={thumbsUpAnchor}
113
127
  onClick={handleThumbsUp}
114
128
  className={cn(
115
129
  "p-1.5 rounded hover:bg-muted transition-colors",
@@ -124,6 +138,7 @@ const ActionBar = React.forwardRef<HTMLDivElement, ActionBarProps>(
124
138
 
125
139
  {/* Thumbs down */}
126
140
  <button
141
+ data-tour={thumbsDownAnchor}
127
142
  onClick={handleThumbsDown}
128
143
  className={cn(
129
144
  "p-1.5 rounded hover:bg-muted transition-colors",
@@ -18,6 +18,14 @@ export interface ActivityIndicatorsProps extends React.HTMLAttributes<HTMLDivEle
18
18
  memory: MemoryItem[];
19
19
  /** Status updates to display */
20
20
  statusUpdates?: StatusItem[];
21
+ /** Tour anchor (`data-tour`) on the status updates popover trigger. */
22
+ statusUpdateAnchor?: string;
23
+ /** Tour anchor (`data-tour`) on the tool calls popover trigger. */
24
+ toolCallsAnchor?: string;
25
+ /** Tour anchor (`data-tour`) on the knowledge popover trigger. */
26
+ knowledgeAnchor?: string;
27
+ /** Tour anchor (`data-tour`) on the memory popover trigger. */
28
+ memoryAnchor?: string;
21
29
  }
22
30
 
23
31
  /**
@@ -34,7 +42,21 @@ export interface ActivityIndicatorsProps extends React.HTMLAttributes<HTMLDivEle
34
42
  * />
35
43
  */
36
44
  const ActivityIndicators = React.forwardRef<HTMLDivElement, ActivityIndicatorsProps>(
37
- ({ toolCalls, knowledge, memory, statusUpdates = [], className, ...props }, ref) => {
45
+ (
46
+ {
47
+ toolCalls,
48
+ knowledge,
49
+ memory,
50
+ statusUpdates = [],
51
+ statusUpdateAnchor,
52
+ toolCallsAnchor,
53
+ knowledgeAnchor,
54
+ memoryAnchor,
55
+ className,
56
+ ...props
57
+ },
58
+ ref,
59
+ ) => {
38
60
  const hasAnyActivity =
39
61
  toolCalls.length > 0 || knowledge.length > 0 || memory.length > 0 || statusUpdates.length > 0;
40
62
 
@@ -47,6 +69,7 @@ const ActivityIndicators = React.forwardRef<HTMLDivElement, ActivityIndicatorsPr
47
69
  <Popover>
48
70
  <PopoverTrigger asChild>
49
71
  <button
72
+ data-tour={statusUpdateAnchor}
50
73
  className="flex items-center gap-1 text-muted-foreground hover:text-foreground transition-colors"
51
74
  onClick={(e) => e.stopPropagation()}
52
75
  >
@@ -77,6 +100,7 @@ const ActivityIndicators = React.forwardRef<HTMLDivElement, ActivityIndicatorsPr
77
100
  <Popover>
78
101
  <PopoverTrigger asChild>
79
102
  <button
103
+ data-tour={toolCallsAnchor}
80
104
  className="flex items-center gap-1 text-muted-foreground hover:text-foreground transition-colors"
81
105
  onClick={(e) => e.stopPropagation()}
82
106
  >
@@ -109,6 +133,7 @@ const ActivityIndicators = React.forwardRef<HTMLDivElement, ActivityIndicatorsPr
109
133
  <Popover>
110
134
  <PopoverTrigger asChild>
111
135
  <button
136
+ data-tour={knowledgeAnchor}
112
137
  className="flex items-center gap-1 text-muted-foreground hover:text-foreground transition-colors"
113
138
  onClick={(e) => e.stopPropagation()}
114
139
  >
@@ -139,6 +164,7 @@ const ActivityIndicators = React.forwardRef<HTMLDivElement, ActivityIndicatorsPr
139
164
  <Popover>
140
165
  <PopoverTrigger asChild>
141
166
  <button
167
+ data-tour={memoryAnchor}
142
168
  className="flex items-center gap-1 text-muted-foreground hover:text-foreground transition-colors"
143
169
  onClick={(e) => e.stopPropagation()}
144
170
  >
@@ -32,6 +32,16 @@ export interface MetadataRowProps extends React.HTMLAttributes<HTMLDivElement> {
32
32
  status: AgentResponseStatus;
33
33
  /** Elapsed time in seconds */
34
34
  elapsedTime: number;
35
+ /** Tour anchor (`data-tour`) on the thinking expand/collapse toggle button. */
36
+ thinkingToggleAnchor?: string;
37
+ /** Tour anchor forwarded to the status updates popover trigger inside ActivityIndicators. */
38
+ statusUpdateAnchor?: string;
39
+ /** Tour anchor forwarded to the tool calls popover trigger inside ActivityIndicators. */
40
+ toolCallsAnchor?: string;
41
+ /** Tour anchor forwarded to the knowledge popover trigger inside ActivityIndicators. */
42
+ knowledgeAnchor?: string;
43
+ /** Tour anchor forwarded to the memory popover trigger inside ActivityIndicators. */
44
+ memoryAnchor?: string;
35
45
  }
36
46
 
37
47
  /**
@@ -65,6 +75,11 @@ const MetadataRow = React.forwardRef<HTMLDivElement, MetadataRowProps>(
65
75
  statusContent,
66
76
  status,
67
77
  elapsedTime,
78
+ thinkingToggleAnchor,
79
+ statusUpdateAnchor,
80
+ toolCallsAnchor,
81
+ knowledgeAnchor,
82
+ memoryAnchor,
68
83
  className,
69
84
  ...props
70
85
  },
@@ -127,6 +142,7 @@ const MetadataRow = React.forwardRef<HTMLDivElement, MetadataRowProps>(
127
142
  {/* Left content - clickable when there's thinking */}
128
143
  {hasThinking ? (
129
144
  <button
145
+ data-tour={thinkingToggleAnchor}
130
146
  onClick={onToggle}
131
147
  className="flex items-center gap-1.5 hover:bg-muted/50 -ml-1.5 pl-1.5 pr-2 py-0.5 rounded transition-colors shrink-0"
132
148
  >
@@ -150,6 +166,10 @@ const MetadataRow = React.forwardRef<HTMLDivElement, MetadataRowProps>(
150
166
  knowledge={knowledge}
151
167
  memory={memory}
152
168
  statusUpdates={statusUpdates}
169
+ statusUpdateAnchor={statusUpdateAnchor}
170
+ toolCallsAnchor={toolCallsAnchor}
171
+ knowledgeAnchor={knowledgeAnchor}
172
+ memoryAnchor={memoryAnchor}
153
173
  />
154
174
  </div>
155
175
  );
@@ -1,7 +1,10 @@
1
1
  /**
2
2
  * useAgentResponseAccumulator Hook
3
3
  *
4
- * Accumulates agent response messages into a unified state
4
+ * Accumulates agent response messages into a unified state.
5
+ *
6
+ * Thin wrapper around `reduceAgentMessage` — the pure reducer can be used
7
+ * directly outside React for replaying historical events.
5
8
  */
6
9
 
7
10
  import { useState, useCallback } from "react";
@@ -10,14 +13,8 @@ import {
10
13
  type AgentResponseState,
11
14
  type AgentMessage,
12
15
  type GenericWebSocketMessage,
13
- type ToolCall,
14
- type KnowledgeItem,
15
- type MemoryItem,
16
- type StatusItem,
17
- type ThinkingStep,
18
- type PotentialResponse,
19
16
  } from "../types";
20
- import { buildTimelineEntries } from "../../agent-timeline/utils";
17
+ import { reduceAgentMessage } from "../reducer";
21
18
 
22
19
  export interface UseAgentResponseAccumulatorOptions {
23
20
  /** WebSocket topic to filter messages (optional, for convenience) */
@@ -54,7 +51,6 @@ export function useAgentResponseAccumulator(
54
51
 
55
52
  const handleMessage = useCallback(
56
53
  (message: unknown) => {
57
- // If topic filter is provided, check for matching topic
58
54
  let payload: AgentMessage;
59
55
 
60
56
  if (topic) {
@@ -62,216 +58,10 @@ export function useAgentResponseAccumulator(
62
58
  if (msg.topic !== topic) return;
63
59
  payload = msg.message;
64
60
  } else {
65
- // Assume message is the payload directly
66
61
  payload = message as AgentMessage;
67
62
  }
68
63
 
69
- setState((prev) => {
70
- // If we receive a non-status message while idle, transition to processing
71
- let newStatus = prev.status;
72
- const isFirstMessage = prev.status === "idle" && payload.type !== "status";
73
- if (isFirstMessage) {
74
- newStatus = "processing";
75
- }
76
-
77
- // Track first message time for total time calculation
78
- const firstMessageTime =
79
- prev.firstMessageTime ?? (isFirstMessage ? Date.now() : null);
80
-
81
- switch (payload.type) {
82
- case "status":
83
- // "Harness connected" resets to idle
84
- if (
85
- payload.message === "Harness connected" ||
86
- payload.status === "Harness connected"
87
- ) {
88
- return { ...initialAgentResponseState };
89
- }
90
- return { ...prev, status: newStatus };
91
-
92
- case "thinking": {
93
- // Check if this is a structured thinking step
94
- if (payload.thinkingStep) {
95
- const newStep: ThinkingStep = {
96
- id: payload.thinkingStep.id || `step-${Date.now()}`,
97
- label: payload.thinkingStep.label,
98
- content: payload.thinkingStep.content,
99
- depth: payload.thinkingStep.depth ?? payload.depth ?? 0,
100
- isCollapsed: payload.thinkingStep.isCollapsed,
101
- timestamp: Date.now(),
102
- agentName: payload.agentName,
103
- parentAgent: payload.parentAgent,
104
- };
105
- const thinkingStartTime = prev.thinkingStartTime ?? Date.now();
106
- const next = {
107
- ...prev,
108
- status: newStatus,
109
- thinkingSteps: [...(prev.thinkingSteps || []), newStep],
110
- thinkingStartTime,
111
- firstMessageTime,
112
- };
113
- return { ...next, timelineEntries: buildTimelineEntries(next) };
114
- }
115
-
116
- // Plain text thinking — concatenate for backward compat AND
117
- // push a ThinkingStep so the timeline gets individual entries.
118
- const newThinking = payload.message || payload.content || "";
119
- // Add line break between thinking messages
120
- const separator = prev.thinking && newThinking ? "\n\n" : "";
121
- // Set thinkingStartTime on first thinking message
122
- const thinkingStartTime =
123
- prev.thinkingStartTime ?? (newThinking ? Date.now() : null);
124
- const prevSteps = prev.thinkingSteps || [];
125
- const plainStep: ThinkingStep = {
126
- id: `step-${prevSteps.length}`,
127
- label: newThinking,
128
- content: newThinking,
129
- depth: payload.depth ?? 0,
130
- timestamp: Date.now(),
131
- agentName: payload.agentName,
132
- parentAgent: payload.parentAgent,
133
- };
134
- const next = {
135
- ...prev,
136
- status: newStatus,
137
- thinking: prev.thinking + separator + newThinking,
138
- thinkingSteps: [...prevSteps, plainStep],
139
- thinkingStartTime,
140
- firstMessageTime,
141
- };
142
- return { ...next, timelineEntries: buildTimelineEntries(next) };
143
- }
144
-
145
- case "tool_call": {
146
- // Handle both formats: { message: "ToolName" } or { tool: { id, name, arguments } }
147
- const toolName = payload.message || payload.tool?.name;
148
- if (toolName) {
149
- const newToolCall: ToolCall = {
150
- id: payload.tool?.id || `tool-${Date.now()}`,
151
- name: toolName,
152
- arguments: payload.tool?.arguments,
153
- timestamp: Date.now(),
154
- agentName: payload.agentName,
155
- parentAgent: payload.parentAgent,
156
- depth: payload.depth,
157
- };
158
- const next = {
159
- ...prev,
160
- status: newStatus,
161
- toolCalls: [...prev.toolCalls, newToolCall],
162
- firstMessageTime,
163
- };
164
- return { ...next, timelineEntries: buildTimelineEntries(next) };
165
- }
166
- return { ...prev, status: newStatus, firstMessageTime };
167
- }
168
-
169
- case "knowledge": {
170
- // Handle both formats: { message: "content" } or { knowledge: { id, source, content } }
171
- const knowledgeContent = payload.message || payload.knowledge?.content;
172
- if (knowledgeContent) {
173
- const newKnowledge: KnowledgeItem = {
174
- id: payload.knowledge?.id || `knowledge-${Date.now()}`,
175
- source: payload.knowledge?.source || "unknown",
176
- content: knowledgeContent,
177
- timestamp: Date.now(),
178
- agentName: payload.agentName,
179
- parentAgent: payload.parentAgent,
180
- depth: payload.depth,
181
- };
182
- const next = {
183
- ...prev,
184
- status: newStatus,
185
- knowledge: [...prev.knowledge, newKnowledge],
186
- firstMessageTime,
187
- };
188
- return { ...next, timelineEntries: buildTimelineEntries(next) };
189
- }
190
- return { ...prev, status: newStatus, firstMessageTime };
191
- }
192
-
193
- case "memory": {
194
- // Handle both formats: { message: "content" } or { memory: { id, type, content } }
195
- const memoryContent = payload.message || payload.memory?.content;
196
- if (memoryContent) {
197
- const newMemory: MemoryItem = {
198
- id: payload.memory?.id || `memory-${Date.now()}`,
199
- type: payload.memory?.type || "unknown",
200
- content: memoryContent,
201
- timestamp: Date.now(),
202
- agentName: payload.agentName,
203
- parentAgent: payload.parentAgent,
204
- depth: payload.depth,
205
- };
206
- const next = {
207
- ...prev,
208
- status: newStatus,
209
- memory: [...prev.memory, newMemory],
210
- firstMessageTime,
211
- };
212
- return { ...next, timelineEntries: buildTimelineEntries(next) };
213
- }
214
- return { ...prev, status: newStatus, firstMessageTime };
215
- }
216
-
217
- case "response":
218
- return {
219
- ...prev,
220
- status: "complete",
221
- response: payload.message || payload.content || "",
222
- responseCompleteTime: Date.now(),
223
- firstMessageTime: prev.firstMessageTime ?? Date.now(),
224
- };
225
-
226
- case "status_update": {
227
- const statusMessage = payload.message || payload.statusUpdate?.message;
228
- if (statusMessage) {
229
- const newStatusItem: StatusItem = {
230
- id: payload.statusUpdate?.id || `status-${Date.now()}`,
231
- message: statusMessage,
232
- agent: payload.statusUpdate?.agent,
233
- timestamp: Date.now(),
234
- agentName: payload.agentName,
235
- parentAgent: payload.parentAgent,
236
- depth: payload.depth,
237
- };
238
- const next = {
239
- ...prev,
240
- status: newStatus,
241
- statusUpdates: [...prev.statusUpdates, newStatusItem],
242
- firstMessageTime,
243
- };
244
- return { ...next, timelineEntries: buildTimelineEntries(next) };
245
- }
246
- return { ...prev, status: newStatus, firstMessageTime };
247
- }
248
-
249
- case "potential_response": {
250
- const respContent = payload.message || payload.content || "";
251
- if (respContent) {
252
- const newResp: PotentialResponse = {
253
- id: `resp-${Date.now()}`,
254
- content: respContent,
255
- timestamp: Date.now(),
256
- agentName: payload.agentName,
257
- parentAgent: payload.parentAgent,
258
- depth: payload.depth,
259
- };
260
- const next = {
261
- ...prev,
262
- status: newStatus,
263
- potentialResponses: [...(prev.potentialResponses || []), newResp],
264
- firstMessageTime,
265
- };
266
- return { ...next, timelineEntries: buildTimelineEntries(next) };
267
- }
268
- return { ...prev, status: newStatus, firstMessageTime };
269
- }
270
-
271
- default:
272
- return { ...prev, status: newStatus, firstMessageTime };
273
- }
274
- });
64
+ setState((prev) => reduceAgentMessage(prev, payload));
275
65
  },
276
66
  [topic]
277
67
  );
@@ -7,7 +7,11 @@
7
7
 
8
8
  // Main component
9
9
  export { AgentResponse } from "./AgentResponse";
10
- export type { AgentResponseProps, AgentResponseClassNames } from "./AgentResponse";
10
+ export type {
11
+ AgentResponseProps,
12
+ AgentResponseClassNames,
13
+ AgentResponseAnchors,
14
+ } from "./AgentResponse";
11
15
 
12
16
  // Sub-components (for advanced customization)
13
17
  export {
@@ -52,6 +56,9 @@ export type {
52
56
 
53
57
  export { initialAgentResponseState } from "./types";
54
58
 
59
+ // Pure reducer (for non-React replay of AgentMessage streams)
60
+ export { reduceAgentMessage } from "./reducer";
61
+
55
62
  // Utilities
56
63
  export { formatTime, formatTotalTime } from "./utils";
57
64