@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.
Files changed (38) hide show
  1. package/README.md +235 -0
  2. package/dist/index.cjs +1292 -43
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +524 -6
  5. package/dist/index.d.ts +524 -6
  6. package/dist/index.js +1267 -33
  7. package/dist/index.js.map +1 -1
  8. package/package.json +15 -9
  9. package/src/components/agent-response/AgentResponse.tsx +99 -10
  10. package/src/components/agent-response/components/ActivityIndicators.tsx +36 -4
  11. package/src/components/agent-response/components/HITLSection.tsx +95 -0
  12. package/src/components/agent-response/components/MetadataRow.tsx +21 -6
  13. package/src/components/agent-response/components/ThinkingSection.tsx +102 -10
  14. package/src/components/agent-response/components/TruncatedMessage.tsx +52 -0
  15. package/src/components/agent-response/components/index.ts +6 -0
  16. package/src/components/agent-response/hooks/useAgentResponseAccumulator.ts +79 -4
  17. package/src/components/agent-response/index.ts +23 -0
  18. package/src/components/agent-response/types.ts +96 -1
  19. package/src/components/agent-timeline/AgentTimeline.tsx +256 -0
  20. package/src/components/agent-timeline/TimelineAgentBlock.tsx +84 -0
  21. package/src/components/agent-timeline/TimelineItem.tsx +97 -0
  22. package/src/components/agent-timeline/index.ts +14 -0
  23. package/src/components/agent-timeline/types.ts +49 -0
  24. package/src/components/agent-timeline/utils.ts +167 -0
  25. package/src/components/hitl-interactions/HITLInteractionRecord.tsx +139 -0
  26. package/src/components/hitl-interactions/HITLQuestionPanel.tsx +270 -0
  27. package/src/components/hitl-interactions/index.ts +18 -0
  28. package/src/components/inline-actions/ActionMarkdownRenderer.tsx +60 -0
  29. package/src/components/inline-actions/index.ts +18 -0
  30. package/src/components/inline-actions/parseResponseSegments.ts +66 -0
  31. package/src/components/inline-actions/prompts.ts +41 -0
  32. package/src/components/inline-actions/types.ts +57 -0
  33. package/src/components/user-prompt/UserPrompt.tsx +60 -0
  34. package/src/components/user-prompt/index.ts +1 -0
  35. package/src/components/user-prompt-input/UserPromptInput.tsx +326 -0
  36. package/src/components/user-prompt-input/index.ts +2 -0
  37. package/src/components/user-prompt-input/types.ts +52 -0
  38. package/src/index.ts +54 -0
@@ -6,11 +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
- import { MetadataRow, ThinkingSection, ActionBar } from "./components";
11
+ import { MetadataRow, ThinkingSection, ActionBar, HITLSection } from "./components";
12
12
  import { useThinkingTimer } from "./hooks";
13
13
  import type { AgentResponseState, FeedbackValue } from "./types";
14
+ import type { HITLInteraction } from "../hitl-interactions";
15
+ import { AgentTimeline, createTimelineUIState } from "../agent-timeline";
16
+ import type { TimelineUIState } from "../agent-timeline";
14
17
 
15
18
  export interface AgentResponseProps extends React.HTMLAttributes<HTMLDivElement> {
16
19
  /** The response state to render */
@@ -37,6 +40,35 @@ export interface AgentResponseProps extends React.HTMLAttributes<HTMLDivElement>
37
40
  /** Action bar visibility mode */
38
41
  actionsVisible?: boolean | "hover";
39
42
 
43
+ /**
44
+ * Optional HITL (Human-in-the-Loop) interactions to display as a
45
+ * collapsible section within the response. When provided, a "Clarifying
46
+ * Questions" section appears between the thinking/metadata area and
47
+ * the response content.
48
+ */
49
+ hitlInteractions?: HITLInteraction[];
50
+
51
+ /** Whether the HITL section starts expanded (default: false) */
52
+ defaultHITLExpanded?: boolean;
53
+
54
+ /**
55
+ * Optional content to display in the MetadataRow's middle area,
56
+ * between the thinking toggle (left) and activity indicators (right).
57
+ * Typically used for ephemeral status messages during processing.
58
+ * The parent is responsible for setting and clearing this content.
59
+ *
60
+ * @example
61
+ * <AgentResponse
62
+ * state={state}
63
+ * statusContent={
64
+ * state.status !== 'complete'
65
+ * ? <TruncatedMessage message="Analyzing data..." />
66
+ * : undefined
67
+ * }
68
+ * />
69
+ */
70
+ statusContent?: React.ReactNode;
71
+
40
72
  /**
41
73
  * Custom markdown renderer for the response content.
42
74
  * If not provided, the response will be rendered as plain text.
@@ -48,6 +80,18 @@ export interface AgentResponseProps extends React.HTMLAttributes<HTMLDivElement>
48
80
  * />
49
81
  */
50
82
  renderMarkdown?: (content: string) => React.ReactNode;
83
+
84
+ /**
85
+ * Custom markdown renderer for the thinking content.
86
+ * If not provided, the thinking will be rendered as plain preformatted text.
87
+ *
88
+ * @example
89
+ * <AgentResponse
90
+ * state={state}
91
+ * renderThinkingMarkdown={(content) => <MyMarkdownRenderer content={content} />}
92
+ * />
93
+ */
94
+ renderThinkingMarkdown?: (content: string) => React.ReactNode;
51
95
  }
52
96
 
53
97
  /**
@@ -95,12 +139,19 @@ const AgentResponse = React.forwardRef<HTMLDivElement, AgentResponseProps>(
95
139
  thinkingExpanded: controlledThinkingExpanded,
96
140
  onThinkingExpandedChange,
97
141
  actionsVisible = "hover",
142
+ hitlInteractions,
143
+ defaultHITLExpanded = false,
144
+ statusContent,
98
145
  renderMarkdown,
146
+ renderThinkingMarkdown,
99
147
  className,
100
148
  ...props
101
149
  },
102
150
  ref
103
151
  ) => {
152
+ // Ref-backed timeline UI state (survives remounts during streaming)
153
+ const timelineUIStateRef = useRef<TimelineUIState>(createTimelineUIState());
154
+
104
155
  // Uncontrolled thinking expanded state
105
156
  const [uncontrolledExpanded, setUncontrolledExpanded] = useState(defaultThinkingExpanded);
106
157
 
@@ -136,20 +187,31 @@ const AgentResponse = React.forwardRef<HTMLDivElement, AgentResponseProps>(
136
187
  return (state.responseCompleteTime - state.firstMessageTime) / 1000;
137
188
  }, [state.firstMessageTime, state.responseCompleteTime]);
138
189
 
190
+ // Check if we have any thinking content (plain text, structured, or timeline)
191
+ const hasTimelineEntries = !!(state.timelineEntries && state.timelineEntries.length > 0);
192
+ const hasThinkingContent =
193
+ !!state.thinking || (state.thinkingSteps && state.thinkingSteps.length > 0) || hasTimelineEntries || false;
194
+
195
+ const hasHITLInteractions =
196
+ hitlInteractions && hitlInteractions.length > 0;
197
+
139
198
  // Derived state: has any content been received?
140
199
  const hasAnyContent =
141
- state.thinking ||
200
+ hasThinkingContent ||
142
201
  state.toolCalls.length > 0 ||
143
202
  state.knowledge.length > 0 ||
144
203
  state.memory.length > 0 ||
204
+ state.statusUpdates.length > 0 ||
205
+ hasHITLInteractions ||
145
206
  state.response;
146
207
 
147
208
  // Derived state: should show metadata row?
148
209
  const showMetadataRow =
149
- state.thinking ||
210
+ hasThinkingContent ||
150
211
  state.toolCalls.length > 0 ||
151
212
  state.knowledge.length > 0 ||
152
213
  state.memory.length > 0 ||
214
+ state.statusUpdates.length > 0 ||
153
215
  state.status === "processing";
154
216
 
155
217
  // Determine action bar visibility
@@ -177,24 +239,51 @@ const AgentResponse = React.forwardRef<HTMLDivElement, AgentResponseProps>(
177
239
  {showMetadataRow && (
178
240
  <>
179
241
  <MetadataRow
180
- hasThinking={!!state.thinking}
242
+ hasThinking={hasThinkingContent}
181
243
  isExpanded={thinkingExpanded}
182
244
  onToggle={toggleThinking}
183
245
  toolCalls={state.toolCalls}
184
246
  knowledge={state.knowledge}
185
247
  memory={state.memory}
248
+ statusUpdates={state.statusUpdates}
249
+ statusContent={statusContent}
186
250
  status={state.status}
187
251
  elapsedTime={elapsedTime}
188
252
  />
189
253
 
190
- {/* Thinking Content - collapsible with max-height */}
191
- <ThinkingSection
192
- content={state.thinking}
193
- isExpanded={thinkingExpanded}
194
- />
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
+ )}
195
276
  </>
196
277
  )}
197
278
 
279
+ {/* HITL Interactions - collapsible section */}
280
+ {hasHITLInteractions && (
281
+ <HITLSection
282
+ interactions={hitlInteractions}
283
+ defaultExpanded={defaultHITLExpanded}
284
+ />
285
+ )}
286
+
198
287
  {/* Response Section */}
199
288
  {state.response && (
200
289
  <div
@@ -5,9 +5,9 @@
5
5
  */
6
6
 
7
7
  import * as React from "react";
8
- import { Wrench, Book, HardDrive } from "lucide-react";
8
+ import { Wrench, Book, HardDrive, Activity } from "lucide-react";
9
9
  import { cn, Popover, PopoverTrigger, PopoverContent } from "@optilogic/core";
10
- import type { ToolCall, KnowledgeItem, MemoryItem } from "../types";
10
+ import type { ToolCall, KnowledgeItem, MemoryItem, StatusItem } from "../types";
11
11
 
12
12
  export interface ActivityIndicatorsProps extends React.HTMLAttributes<HTMLDivElement> {
13
13
  /** Tool calls to display */
@@ -16,6 +16,8 @@ export interface ActivityIndicatorsProps extends React.HTMLAttributes<HTMLDivEle
16
16
  knowledge: KnowledgeItem[];
17
17
  /** Memory items to display */
18
18
  memory: MemoryItem[];
19
+ /** Status updates to display */
20
+ statusUpdates?: StatusItem[];
19
21
  }
20
22
 
21
23
  /**
@@ -32,14 +34,44 @@ export interface ActivityIndicatorsProps extends React.HTMLAttributes<HTMLDivEle
32
34
  * />
33
35
  */
34
36
  const ActivityIndicators = React.forwardRef<HTMLDivElement, ActivityIndicatorsProps>(
35
- ({ toolCalls, knowledge, memory, className, ...props }, ref) => {
37
+ ({ toolCalls, knowledge, memory, statusUpdates = [], className, ...props }, ref) => {
36
38
  const hasAnyActivity =
37
- toolCalls.length > 0 || knowledge.length > 0 || memory.length > 0;
39
+ toolCalls.length > 0 || knowledge.length > 0 || memory.length > 0 || statusUpdates.length > 0;
38
40
 
39
41
  if (!hasAnyActivity) return null;
40
42
 
41
43
  return (
42
44
  <div ref={ref} className={cn("flex items-center gap-2", className)} {...props}>
45
+ {/* Status Updates */}
46
+ {statusUpdates.length > 0 && (
47
+ <Popover>
48
+ <PopoverTrigger asChild>
49
+ <button
50
+ className="flex items-center gap-1 text-muted-foreground hover:text-foreground transition-colors"
51
+ onClick={(e) => e.stopPropagation()}
52
+ >
53
+ <Activity className="w-3.5 h-3.5" />
54
+ <span className="text-xs">{statusUpdates.length}</span>
55
+ </button>
56
+ </PopoverTrigger>
57
+ <PopoverContent className="w-80">
58
+ <div className="space-y-2">
59
+ <h4 className="font-medium text-sm">Status Updates</h4>
60
+ <div className="space-y-2 max-h-60 overflow-auto">
61
+ {statusUpdates.map((item) => (
62
+ <div key={item.id} className="p-2 bg-muted rounded text-xs">
63
+ {item.agent && (
64
+ <div className="font-medium text-muted-foreground">{item.agent}</div>
65
+ )}
66
+ <div className={item.agent ? "mt-1" : ""}>{item.message}</div>
67
+ </div>
68
+ ))}
69
+ </div>
70
+ </div>
71
+ </PopoverContent>
72
+ </Popover>
73
+ )}
74
+
43
75
  {/* Tool Calls */}
44
76
  {toolCalls.length > 0 && (
45
77
  <Popover>
@@ -0,0 +1,95 @@
1
+ /**
2
+ * HITL Section Component
3
+ *
4
+ * Collapsible section for displaying completed HITL (Human-in-the-Loop)
5
+ * interactions within an AgentResponse. Follows the same pattern as
6
+ * ThinkingSection for consistent UX.
7
+ */
8
+
9
+ import * as React from "react";
10
+ import { useState, useCallback } from "react";
11
+ import { ChevronDown, ChevronRight, MessageCircleQuestion } from "lucide-react";
12
+ import { cn } from "@optilogic/core";
13
+ import { HITLInteractionRecord } from "../../hitl-interactions";
14
+ import type { HITLInteraction } from "../../hitl-interactions";
15
+
16
+ export interface HITLSectionProps
17
+ extends React.HTMLAttributes<HTMLDivElement> {
18
+ /** The HITL interactions to display */
19
+ interactions: HITLInteraction[];
20
+ /** Whether the section starts expanded (uncontrolled) */
21
+ defaultExpanded?: boolean;
22
+ /** Whether the section is expanded (controlled) */
23
+ isExpanded?: boolean;
24
+ /** Callback when expansion state changes (controlled) */
25
+ onExpandedChange?: (expanded: boolean) => void;
26
+ }
27
+
28
+ const HITLSection = React.forwardRef<HTMLDivElement, HITLSectionProps>(
29
+ (
30
+ {
31
+ interactions,
32
+ defaultExpanded = false,
33
+ isExpanded: controlledExpanded,
34
+ onExpandedChange,
35
+ className,
36
+ ...props
37
+ },
38
+ ref
39
+ ) => {
40
+ const [uncontrolledExpanded, setUncontrolledExpanded] =
41
+ useState(defaultExpanded);
42
+
43
+ const isControlled = controlledExpanded !== undefined;
44
+ const isExpanded = isControlled ? controlledExpanded : uncontrolledExpanded;
45
+
46
+ const toggleExpanded = useCallback(() => {
47
+ const newValue = !isExpanded;
48
+ if (isControlled) {
49
+ onExpandedChange?.(newValue);
50
+ } else {
51
+ setUncontrolledExpanded(newValue);
52
+ }
53
+ }, [isExpanded, isControlled, onExpandedChange]);
54
+
55
+ if (!interactions || interactions.length === 0) {
56
+ return null;
57
+ }
58
+
59
+ return (
60
+ <div
61
+ ref={ref}
62
+ className={cn("border-t border-border", className)}
63
+ {...props}
64
+ >
65
+ {/* Collapsible header */}
66
+ <button
67
+ onClick={toggleExpanded}
68
+ className="w-full flex items-center gap-2 py-2 px-3 hover:bg-muted/50 transition-colors text-left"
69
+ >
70
+ {isExpanded ? (
71
+ <ChevronDown className="w-3.5 h-3.5 text-muted-foreground flex-shrink-0" />
72
+ ) : (
73
+ <ChevronRight className="w-3.5 h-3.5 text-muted-foreground flex-shrink-0" />
74
+ )}
75
+ <MessageCircleQuestion className="w-3.5 h-3.5 text-muted-foreground flex-shrink-0" />
76
+ <span className="text-xs font-medium text-foreground/80">
77
+ Clarifying Questions ({interactions.length})
78
+ </span>
79
+ </button>
80
+
81
+ {/* Expanded content */}
82
+ {isExpanded && (
83
+ <div className="px-3 pb-3 space-y-2">
84
+ {interactions.map((interaction, i) => (
85
+ <HITLInteractionRecord key={i} interaction={interaction} />
86
+ ))}
87
+ </div>
88
+ )}
89
+ </div>
90
+ );
91
+ }
92
+ );
93
+ HITLSection.displayName = "HITLSection";
94
+
95
+ export { HITLSection };
@@ -9,7 +9,7 @@ import { ChevronDown, ChevronUp } from "lucide-react";
9
9
  import { cn, LoadingSpinner } from "@optilogic/core";
10
10
  import { ActivityIndicators } from "./ActivityIndicators";
11
11
  import { formatTime } from "../utils";
12
- import type { AgentResponseStatus, ToolCall, KnowledgeItem, MemoryItem } from "../types";
12
+ import type { AgentResponseStatus, ToolCall, KnowledgeItem, MemoryItem, StatusItem } from "../types";
13
13
 
14
14
  export interface MetadataRowProps extends React.HTMLAttributes<HTMLDivElement> {
15
15
  /** Whether there is thinking content */
@@ -24,6 +24,10 @@ export interface MetadataRowProps extends React.HTMLAttributes<HTMLDivElement> {
24
24
  knowledge: KnowledgeItem[];
25
25
  /** Memory items to display */
26
26
  memory: MemoryItem[];
27
+ /** Status updates to display */
28
+ statusUpdates?: StatusItem[];
29
+ /** Optional content to display in the middle area between left content and activity indicators */
30
+ statusContent?: React.ReactNode;
27
31
  /** Current response status */
28
32
  status: AgentResponseStatus;
29
33
  /** Elapsed time in seconds */
@@ -57,6 +61,8 @@ const MetadataRow = React.forwardRef<HTMLDivElement, MetadataRowProps>(
57
61
  toolCalls,
58
62
  knowledge,
59
63
  memory,
64
+ statusUpdates = [],
65
+ statusContent,
60
66
  status,
61
67
  elapsedTime,
62
68
  className,
@@ -67,7 +73,7 @@ const MetadataRow = React.forwardRef<HTMLDivElement, MetadataRowProps>(
67
73
  const isProcessing = status === "processing";
68
74
  const isComplete = status === "complete";
69
75
  const hasActivity =
70
- toolCalls.length > 0 || knowledge.length > 0 || memory.length > 0;
76
+ toolCalls.length > 0 || knowledge.length > 0 || memory.length > 0 || statusUpdates.length > 0;
71
77
 
72
78
  // Determine what to show on the left side
73
79
  const renderLeftContent = () => {
@@ -105,8 +111,8 @@ const MetadataRow = React.forwardRef<HTMLDivElement, MetadataRowProps>(
105
111
 
106
112
  const leftContent = renderLeftContent();
107
113
 
108
- // If nothing to show (no thinking, not processing, no activity), hide the row
109
- if (!leftContent && !hasActivity) {
114
+ // If nothing to show (no thinking, not processing, no activity, no status content), hide the row
115
+ if (!leftContent && !hasActivity && !statusContent) {
110
116
  return null;
111
117
  }
112
118
 
@@ -122,19 +128,28 @@ const MetadataRow = React.forwardRef<HTMLDivElement, MetadataRowProps>(
122
128
  {hasThinking ? (
123
129
  <button
124
130
  onClick={onToggle}
125
- 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"
131
+ 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"
126
132
  >
127
133
  {leftContent}
128
134
  </button>
129
135
  ) : (
130
- <div className="flex items-center gap-1.5">
136
+ <div className="flex items-center gap-1.5 shrink-0">
131
137
  {leftContent}
132
138
  </div>
133
139
  )}
140
+
141
+ {/* Middle content - status content slot */}
142
+ {statusContent && (
143
+ <div className="flex-1 min-w-0 mx-2">
144
+ {statusContent}
145
+ </div>
146
+ )}
147
+
134
148
  <ActivityIndicators
135
149
  toolCalls={toolCalls}
136
150
  knowledge={knowledge}
137
151
  memory={memory}
152
+ statusUpdates={statusUpdates}
138
153
  />
139
154
  </div>
140
155
  );
@@ -1,43 +1,135 @@
1
1
  /**
2
2
  * Thinking Section Component
3
3
  *
4
- * Collapsible section for displaying agent thinking/reasoning content
4
+ * Collapsible section for displaying agent thinking/reasoning content.
5
+ * Supports both plain text and structured collapsible sub-sections.
5
6
  */
6
7
 
7
8
  import * as React from "react";
9
+ import { useState, useCallback } from "react";
10
+ import { ChevronDown, ChevronRight } from "lucide-react";
8
11
  import { cn } from "@optilogic/core";
12
+ import type { ThinkingStep } from "../types";
9
13
 
10
- export interface ThinkingSectionProps extends React.HTMLAttributes<HTMLDivElement> {
11
- /** The thinking content to display */
12
- content: string;
14
+ export interface ThinkingSectionProps
15
+ extends Omit<React.HTMLAttributes<HTMLDivElement>, "content"> {
16
+ /** The thinking content to display (string or structured steps) */
17
+ content: string | ThinkingStep[];
13
18
  /** Whether the section is expanded */
14
19
  isExpanded: boolean;
20
+ /**
21
+ * Custom markdown renderer for the thinking content.
22
+ * If not provided, the content will be rendered as plain preformatted text.
23
+ */
24
+ renderMarkdown?: (content: string) => React.ReactNode;
15
25
  }
16
26
 
27
+ /**
28
+ * Internal component for rendering a collapsible thinking step
29
+ */
30
+ interface ThinkingStepItemProps {
31
+ step: ThinkingStep;
32
+ renderMarkdown?: (content: string) => React.ReactNode;
33
+ }
34
+
35
+ const ThinkingStepItem: React.FC<ThinkingStepItemProps> = ({ step, renderMarkdown }) => {
36
+ const [isCollapsed, setIsCollapsed] = useState(step.isCollapsed ?? false);
37
+
38
+ const toggleCollapse = useCallback(() => {
39
+ setIsCollapsed((prev) => !prev);
40
+ }, []);
41
+
42
+ const indentPadding = step.depth * 16;
43
+
44
+ return (
45
+ <div className="border-b border-border/50 last:border-b-0">
46
+ <button
47
+ onClick={toggleCollapse}
48
+ className="w-full flex items-center gap-1.5 py-1.5 px-2 hover:bg-muted/50 transition-colors text-left"
49
+ style={{ paddingLeft: `${indentPadding + 8}px` }}
50
+ >
51
+ {isCollapsed ? (
52
+ <ChevronRight className="w-3 h-3 text-muted-foreground flex-shrink-0" />
53
+ ) : (
54
+ <ChevronDown className="w-3 h-3 text-muted-foreground flex-shrink-0" />
55
+ )}
56
+ <span className="text-xs font-medium text-foreground/80">{step.label}</span>
57
+ </button>
58
+
59
+ {!isCollapsed && (
60
+ <div
61
+ className="pb-2 px-2"
62
+ style={{ paddingLeft: `${indentPadding + 28}px` }}
63
+ >
64
+ {renderMarkdown ? (
65
+ <div className="text-xs text-muted-foreground">
66
+ {renderMarkdown(step.content)}
67
+ </div>
68
+ ) : (
69
+ <pre className="text-xs text-muted-foreground whitespace-pre-wrap font-mono">
70
+ {step.content}
71
+ </pre>
72
+ )}
73
+ </div>
74
+ )}
75
+ </div>
76
+ );
77
+ };
78
+
17
79
  /**
18
80
  * ThinkingSection Component
19
81
  *
20
82
  * Displays the agent's thinking/reasoning content in a collapsible panel.
83
+ * Supports both plain text content and structured collapsible sub-sections.
21
84
  *
22
85
  * @example
86
+ * // Plain text content
23
87
  * <ThinkingSection content={state.thinking} isExpanded={isExpanded} />
88
+ *
89
+ * @example
90
+ * // Structured content with sub-sections
91
+ * <ThinkingSection
92
+ * content={[
93
+ * { id: "1", label: "Analysis", content: "...", depth: 0 },
94
+ * { id: "2", label: "Sub-analysis", content: "...", depth: 1 },
95
+ * ]}
96
+ * isExpanded={isExpanded}
97
+ * />
24
98
  */
25
99
  const ThinkingSection = React.forwardRef<HTMLDivElement, ThinkingSectionProps>(
26
- ({ content, isExpanded, className, ...props }, ref) => {
27
- if (!isExpanded || !content) {
100
+ ({ content, isExpanded, renderMarkdown, className, ...props }, ref) => {
101
+ if (!isExpanded || !content || (Array.isArray(content) && content.length === 0)) {
28
102
  return null;
29
103
  }
30
104
 
105
+ const isStructured = Array.isArray(content);
106
+
31
107
  return (
32
108
  <div
33
109
  ref={ref}
34
110
  className={cn("px-3 pb-3 border-t border-border", className)}
35
111
  {...props}
36
112
  >
37
- <div className="mt-2 max-h-[200px] overflow-y-auto">
38
- <pre className="text-xs text-muted-foreground whitespace-pre-wrap font-mono">
39
- {content}
40
- </pre>
113
+ <div className="mt-2 max-h-[200px] overflow-y-auto scrollbar-thin">
114
+ {isStructured ? (
115
+ <div className="space-y-0">
116
+ {content.map((step) => (
117
+ <ThinkingStepItem
118
+ key={step.id}
119
+ step={step}
120
+ renderMarkdown={renderMarkdown}
121
+ />
122
+ ))}
123
+ </div>
124
+ ) : renderMarkdown ? (
125
+ <div className="text-xs text-muted-foreground">
126
+ {renderMarkdown(content)}
127
+ </div>
128
+ ) : (
129
+ <pre className="text-xs text-muted-foreground whitespace-pre-wrap font-mono">
130
+ {content}
131
+ </pre>
132
+ )}
41
133
  </div>
42
134
  </div>
43
135
  );
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Truncated Message Component
3
+ *
4
+ * Renders a single-line text message with CSS-based truncation (text-overflow: ellipsis).
5
+ * Designed as a standalone utility that can be used anywhere, including as
6
+ * the statusContent slot in MetadataRow.
7
+ */
8
+
9
+ import * as React from "react";
10
+ import { cn } from "@optilogic/core";
11
+
12
+ export interface TruncatedMessageProps extends React.HTMLAttributes<HTMLDivElement> {
13
+ /** The message string to display (truncated with ellipsis if it overflows) */
14
+ message: string;
15
+ }
16
+
17
+ /**
18
+ * TruncatedMessage Component
19
+ *
20
+ * Displays a single-line text message that truncates with an ellipsis when
21
+ * it overflows its container. Uses CSS text-overflow for zero-JS truncation.
22
+ *
23
+ * @example
24
+ * <TruncatedMessage message="Searching the knowledge base for relevant documents..." />
25
+ *
26
+ * @example
27
+ * // Inside MetadataRow's statusContent slot
28
+ * <AgentResponse
29
+ * state={state}
30
+ * statusContent={<TruncatedMessage message="Running analysis..." />}
31
+ * />
32
+ */
33
+ const TruncatedMessage = React.forwardRef<HTMLDivElement, TruncatedMessageProps>(
34
+ ({ message, className, ...props }, ref) => {
35
+ return (
36
+ <div
37
+ ref={ref}
38
+ className={cn(
39
+ "text-xs text-muted-foreground truncate min-w-0",
40
+ className
41
+ )}
42
+ title={message}
43
+ {...props}
44
+ >
45
+ {message}
46
+ </div>
47
+ );
48
+ }
49
+ );
50
+ TruncatedMessage.displayName = "TruncatedMessage";
51
+
52
+ export { TruncatedMessage };
@@ -13,3 +13,9 @@ export type { ThinkingSectionProps } from "./ThinkingSection";
13
13
 
14
14
  export { ActionBar } from "./ActionBar";
15
15
  export type { ActionBarProps } from "./ActionBar";
16
+
17
+ export { HITLSection } from "./HITLSection";
18
+ export type { HITLSectionProps } from "./HITLSection";
19
+
20
+ export { TruncatedMessage } from "./TruncatedMessage";
21
+ export type { TruncatedMessageProps } from "./TruncatedMessage";