@octavus/docs 0.0.4 → 0.0.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.
Files changed (40) hide show
  1. package/content/01-getting-started/02-quickstart.md +38 -35
  2. package/content/02-server-sdk/01-overview.md +13 -3
  3. package/content/02-server-sdk/02-sessions.md +38 -27
  4. package/content/03-client-sdk/01-overview.md +69 -54
  5. package/content/03-client-sdk/02-messages.md +153 -125
  6. package/content/03-client-sdk/03-streaming.md +112 -111
  7. package/content/03-client-sdk/04-execution-blocks.md +80 -176
  8. package/content/04-protocol/01-overview.md +0 -3
  9. package/content/04-protocol/03-triggers.md +8 -9
  10. package/content/04-protocol/04-tools.md +17 -27
  11. package/content/04-protocol/06-agent-config.md +0 -7
  12. package/content/05-api-reference/02-sessions.md +20 -7
  13. package/dist/chunk-7F5WOCIL.js +421 -0
  14. package/dist/chunk-7F5WOCIL.js.map +1 -0
  15. package/dist/chunk-CHGY4G27.js +421 -0
  16. package/dist/chunk-CHGY4G27.js.map +1 -0
  17. package/dist/chunk-CI7JDWKU.js +421 -0
  18. package/dist/chunk-CI7JDWKU.js.map +1 -0
  19. package/dist/chunk-CVFWWRL7.js +421 -0
  20. package/dist/chunk-CVFWWRL7.js.map +1 -0
  21. package/dist/chunk-J7BMB3ZW.js +421 -0
  22. package/dist/chunk-J7BMB3ZW.js.map +1 -0
  23. package/dist/chunk-K3GFQUMC.js +421 -0
  24. package/dist/chunk-K3GFQUMC.js.map +1 -0
  25. package/dist/chunk-M2R2NDPR.js +421 -0
  26. package/dist/chunk-M2R2NDPR.js.map +1 -0
  27. package/dist/chunk-QCHDPR2D.js +421 -0
  28. package/dist/chunk-QCHDPR2D.js.map +1 -0
  29. package/dist/chunk-TWUMRHQ7.js +421 -0
  30. package/dist/chunk-TWUMRHQ7.js.map +1 -0
  31. package/dist/chunk-YPPXXV3I.js +421 -0
  32. package/dist/chunk-YPPXXV3I.js.map +1 -0
  33. package/dist/content.js +1 -1
  34. package/dist/docs.json +18 -18
  35. package/dist/index.js +1 -1
  36. package/dist/search-index.json +1 -1
  37. package/dist/search.js +1 -1
  38. package/dist/search.js.map +1 -1
  39. package/dist/sections.json +18 -18
  40. package/package.json +1 -1
@@ -5,159 +5,197 @@ description: Working with message state in the Client SDK.
5
5
 
6
6
  # Messages
7
7
 
8
- Messages represent the conversation history. The Client SDK tracks messages automatically and provides structured access to their content.
8
+ Messages represent the conversation history. The Client SDK tracks messages automatically and provides structured access to their content through typed parts.
9
9
 
10
10
  ## Message Structure
11
11
 
12
12
  ```typescript
13
- interface Message {
13
+ interface UIMessage {
14
14
  id: string;
15
- role: 'user' | 'assistant' | 'system';
16
- content: string;
17
- parts?: MessagePart[];
18
- toolCalls?: ToolCallWithDescription[];
19
- reasoning?: string;
20
- visible?: boolean;
15
+ role: 'user' | 'assistant';
16
+ parts: UIMessagePart[];
17
+ status: 'streaming' | 'done';
21
18
  createdAt: Date;
22
19
  }
23
20
  ```
24
21
 
25
22
  ### Message Parts
26
23
 
27
- For rich content display, use the `parts` array which preserves content ordering:
24
+ Messages contain ordered `parts` that preserve content ordering:
28
25
 
29
26
  ```typescript
30
- interface MessagePart {
31
- type: 'text' | 'reasoning' | 'tool-call';
32
- visible: boolean;
33
- content?: string; // For text and reasoning
34
- toolCall?: ToolCallInfo; // For tool-call
35
- thread?: string; // For non-main thread content
27
+ type UIMessagePart =
28
+ | UITextPart
29
+ | UIReasoningPart
30
+ | UIToolCallPart
31
+ | UIOperationPart;
32
+
33
+ // Text content
34
+ interface UITextPart {
35
+ type: 'text';
36
+ text: string;
37
+ status: 'streaming' | 'done';
38
+ thread?: string; // For named threads (e.g., "summary")
39
+ }
40
+
41
+ // Extended reasoning/thinking
42
+ interface UIReasoningPart {
43
+ type: 'reasoning';
44
+ text: string;
45
+ status: 'streaming' | 'done';
46
+ thread?: string;
47
+ }
48
+
49
+ // Tool execution
50
+ interface UIToolCallPart {
51
+ type: 'tool-call';
52
+ toolCallId: string;
53
+ toolName: string;
54
+ displayName?: string; // Human-readable name
55
+ args: Record<string, unknown>;
56
+ result?: unknown;
57
+ error?: string;
58
+ status: 'pending' | 'running' | 'done' | 'error';
59
+ thread?: string;
60
+ }
61
+
62
+ // Internal operations (set-resource, serialize-thread)
63
+ interface UIOperationPart {
64
+ type: 'operation';
65
+ operationId: string;
66
+ name: string;
67
+ operationType: string;
68
+ status: 'running' | 'done';
69
+ thread?: string;
36
70
  }
37
71
  ```
38
72
 
39
- ## Adding User Messages
73
+ ## Sending Messages
40
74
 
41
75
  ```tsx
42
- const { addUserMessage, triggerAction } = useOctavusChat({...});
43
-
44
- async function sendMessage(text: string) {
45
- // 1. Add message to UI immediately
46
- addUserMessage(text);
47
-
48
- // 2. Trigger the agent
49
- await triggerAction('user-message', { USER_MESSAGE: text });
76
+ const { send } = useOctavusChat({...});
77
+
78
+ async function handleSend(text: string) {
79
+ // Add user message to UI and trigger agent
80
+ await send('user-message', { USER_MESSAGE: text }, {
81
+ userMessage: { content: text },
82
+ });
50
83
  }
51
84
  ```
52
85
 
53
- **Important**: `addUserMessage` only updates UI state. You must call `triggerAction` to actually send the message to the agent.
86
+ The `send` function:
87
+ 1. Adds the user message to the UI immediately (if `userMessage` is provided)
88
+ 2. Triggers the agent with the specified trigger name and input
89
+ 3. Streams the assistant's response back
54
90
 
55
91
  ## Rendering Messages
56
92
 
57
93
  ### Basic Rendering
58
94
 
59
95
  ```tsx
60
- function MessageList({ messages }: { messages: Message[] }) {
96
+ function MessageList({ messages }: { messages: UIMessage[] }) {
61
97
  return (
62
98
  <div className="space-y-4">
63
99
  {messages.map((msg) => (
64
- <div
65
- key={msg.id}
66
- className={msg.role === 'user' ? 'text-right' : 'text-left'}
67
- >
68
- <div className="inline-block p-3 rounded-lg">
69
- {msg.content}
70
- </div>
71
- </div>
100
+ <MessageBubble key={msg.id} message={msg} />
72
101
  ))}
73
102
  </div>
74
103
  );
75
104
  }
76
- ```
77
105
 
78
- ### Rich Rendering with Parts
79
-
80
- For messages with tool calls and reasoning, render parts in order:
81
-
82
- ```tsx
83
- function MessageContent({ message }: { message: Message }) {
84
- if (!message.parts) {
85
- return <p>{message.content}</p>;
86
- }
106
+ function MessageBubble({ message }: { message: UIMessage }) {
107
+ const isUser = message.role === 'user';
87
108
 
88
109
  return (
89
- <div className="space-y-2">
90
- {message.parts.map((part, i) => {
91
- if (!part.visible) return null;
92
-
93
- switch (part.type) {
94
- case 'text':
95
- return <p key={i}>{part.content}</p>;
96
-
97
- case 'reasoning':
98
- return (
99
- <details key={i} className="text-gray-500">
100
- <summary>Thinking...</summary>
101
- <pre className="text-sm">{part.content}</pre>
102
- </details>
103
- );
104
-
105
- case 'tool-call':
106
- return (
107
- <div key={i} className="bg-gray-100 p-2 rounded text-sm">
108
- 🔧 {part.toolCall?.name}
109
- {part.toolCall?.status === 'available' && ' ✓'}
110
- </div>
111
- );
112
-
113
- default:
114
- return null;
115
- }
116
- })}
110
+ <div className={isUser ? 'text-right' : 'text-left'}>
111
+ <div className="inline-block p-3 rounded-lg">
112
+ {message.parts.map((part, i) => (
113
+ <PartRenderer key={i} part={part} />
114
+ ))}
115
+ </div>
117
116
  </div>
118
117
  );
119
118
  }
120
119
  ```
121
120
 
122
- ## Tool Calls in Messages
121
+ ### Rendering Parts
123
122
 
124
- Tool calls include status, arguments, and results:
123
+ ```tsx
124
+ import { isOtherThread, type UIMessagePart } from '@octavus/client-sdk';
125
125
 
126
- ```typescript
127
- interface ToolCallInfo {
128
- id: string;
129
- name: string;
130
- description?: string;
131
- arguments: Record<string, unknown>;
132
- status: 'pending' | 'streaming' | 'available' | 'error';
133
- result?: unknown;
134
- error?: string;
126
+ function PartRenderer({ part }: { part: UIMessagePart }) {
127
+ // Check if part belongs to a named thread (e.g., "summary")
128
+ if (isOtherThread(part)) {
129
+ return <OtherThreadPart part={part} />;
130
+ }
131
+
132
+ switch (part.type) {
133
+ case 'text':
134
+ return <TextPart part={part} />;
135
+
136
+ case 'reasoning':
137
+ return (
138
+ <details className="text-gray-500">
139
+ <summary>Thinking...</summary>
140
+ <pre className="text-sm">{part.text}</pre>
141
+ </details>
142
+ );
143
+
144
+ case 'tool-call':
145
+ return (
146
+ <div className="bg-gray-100 p-2 rounded text-sm">
147
+ 🔧 {part.displayName || part.toolName}
148
+ {part.status === 'done' && ' ✓'}
149
+ {part.status === 'error' && ` ✗ ${part.error}`}
150
+ </div>
151
+ );
152
+
153
+ case 'operation':
154
+ return (
155
+ <div className="text-gray-500 text-sm">
156
+ {part.name}
157
+ {part.status === 'done' && ' ✓'}
158
+ </div>
159
+ );
160
+
161
+ default:
162
+ return null;
163
+ }
164
+ }
165
+
166
+ function TextPart({ part }: { part: UITextPart }) {
167
+ return (
168
+ <p>
169
+ {part.text}
170
+ {part.status === 'streaming' && (
171
+ <span className="inline-block w-2 h-4 bg-gray-400 animate-pulse ml-1" />
172
+ )}
173
+ </p>
174
+ );
135
175
  }
136
176
  ```
137
177
 
138
- ### Rendering Tool Calls
178
+ ## Named Threads
179
+
180
+ Content from named threads (like "summary") is identified by the `thread` property. Use the `isOtherThread` helper:
139
181
 
140
182
  ```tsx
141
- function ToolCallCard({ toolCall }: { toolCall: ToolCallInfo }) {
142
- return (
143
- <div className="border rounded p-3">
144
- <div className="flex items-center gap-2">
145
- <span className="text-lg">🔧</span>
146
- <span className="font-medium">{toolCall.description || toolCall.name}</span>
147
- <StatusBadge status={toolCall.status} />
183
+ import { isOtherThread } from '@octavus/client-sdk';
184
+
185
+ function PartRenderer({ part }: { part: UIMessagePart }) {
186
+ if (isOtherThread(part)) {
187
+ // Render differently for named threads
188
+ return (
189
+ <div className="bg-amber-50 p-2 rounded border border-amber-200">
190
+ <span className="text-amber-600 text-sm">
191
+ {part.thread}: {part.type === 'text' && part.text}
192
+ </span>
148
193
  </div>
149
-
150
- {toolCall.status === 'available' && toolCall.result && (
151
- <pre className="mt-2 text-xs bg-gray-50 p-2 rounded">
152
- {JSON.stringify(toolCall.result, null, 2)}
153
- </pre>
154
- )}
155
-
156
- {toolCall.status === 'error' && (
157
- <p className="mt-2 text-red-500 text-sm">{toolCall.error}</p>
158
- )}
159
- </div>
160
- );
194
+ );
195
+ }
196
+
197
+ // Regular rendering for main thread
198
+ // ...
161
199
  }
162
200
  ```
163
201
 
@@ -169,38 +207,28 @@ When restoring a session, pass existing messages:
169
207
  // Fetch session state from your backend
170
208
  const sessionState = await fetchSession(sessionId);
171
209
 
172
- // Convert ChatMessage[] to Message[]
173
- const initialMessages = sessionState.messages
174
- .filter(msg => msg.visible !== false)
175
- .map(msg => ({
176
- id: msg.id,
177
- role: msg.role,
178
- content: msg.content,
179
- parts: msg.parts,
180
- toolCalls: msg.toolCalls,
181
- reasoning: msg.reasoning,
182
- createdAt: new Date(msg.createdAt),
183
- }));
184
-
185
210
  // Pass to hook
186
211
  const { messages } = useOctavusChat({
187
- initialMessages,
212
+ initialMessages: sessionState.messages,
188
213
  onTrigger: ...
189
214
  });
190
215
  ```
191
216
 
192
- ## Message Callbacks
193
-
194
- Get notified when messages are added:
217
+ ## Callbacks
195
218
 
196
219
  ```tsx
197
220
  useOctavusChat({
198
221
  onTrigger: ...,
199
- onMessage: (message) => {
200
- console.log('New message:', message.role, message.content);
201
-
202
- // Analytics, logging, etc.
203
- trackMessage(message);
222
+ onFinish: () => {
223
+ console.log('Stream completed');
224
+ // Scroll to bottom, play sound, etc.
225
+ },
226
+ onError: (error) => {
227
+ console.error('Error:', error);
228
+ toast.error('Failed to get response');
229
+ },
230
+ onResourceUpdate: (name, value) => {
231
+ console.log('Resource updated:', name, value);
204
232
  },
205
233
  });
206
234
  ```
@@ -5,105 +5,129 @@ description: Building streaming UIs with the Client SDK.
5
5
 
6
6
  # Streaming
7
7
 
8
- The Client SDK provides real-time access to streaming content, enabling responsive UIs that update as the agent generates responses.
8
+ The Client SDK provides real-time access to streaming content through the message `parts` array. Each part has its own status, enabling responsive UIs that update as the agent generates responses.
9
9
 
10
10
  ## Streaming State
11
11
 
12
12
  ```tsx
13
- const {
14
- streamingText, // Current visible text being streamed
15
- streamingParts, // Structured parts being streamed
16
- reasoningText, // Current reasoning content
17
- status, // 'idle' | 'loading' | 'streaming' | 'error'
18
- isLoading, // true during loading or streaming
19
- } = useOctavusChat({...});
13
+ const { messages, status, error } = useOctavusChat({...});
14
+
15
+ // status: 'idle' | 'streaming' | 'error'
16
+ // Each message has status: 'streaming' | 'done'
17
+ // Each part has its own status too
20
18
  ```
21
19
 
22
- ## Basic Streaming UI
20
+ ## Building a Streaming UI
23
21
 
24
22
  ```tsx
25
23
  function Chat() {
26
- const { messages, streamingText, isLoading } = useOctavusChat({...});
24
+ const { messages, status, error, send, stop } = useOctavusChat({...});
27
25
 
28
26
  return (
29
27
  <div>
30
- {/* Completed messages */}
28
+ {/* Messages with streaming parts */}
31
29
  {messages.map((msg) => (
32
- <div key={msg.id}>{msg.content}</div>
30
+ <MessageBubble key={msg.id} message={msg} />
33
31
  ))}
34
32
 
35
- {/* Currently streaming */}
36
- {streamingText && (
37
- <div className="animate-pulse">
38
- {streamingText}
39
- <span className="inline-block w-2 h-4 bg-gray-400 ml-1" />
40
- </div>
33
+ {/* Error state */}
34
+ {error && (
35
+ <div className="text-red-500">{error.message}</div>
36
+ )}
37
+
38
+ {/* Stop button during streaming */}
39
+ {status === 'streaming' && (
40
+ <button onClick={stop}>Stop</button>
41
41
  )}
42
42
  </div>
43
43
  );
44
44
  }
45
45
  ```
46
46
 
47
- ## Streaming Parts
47
+ ## Rendering Streaming Parts
48
48
 
49
- For rich streaming UIs, use `streamingParts`:
49
+ Parts update in real-time during streaming. Use the part's `status` to show appropriate UI:
50
50
 
51
51
  ```tsx
52
- function StreamingContent() {
53
- const { streamingParts } = useOctavusChat({...});
52
+ import type { UITextPart, UIReasoningPart } from '@octavus/client-sdk';
54
53
 
54
+ function TextPart({ part }: { part: UITextPart }) {
55
55
  return (
56
56
  <div>
57
- {streamingParts.map((part, i) => {
58
- switch (part.type) {
59
- case 'text':
60
- return <span key={i}>{part.content}</span>;
61
-
62
- case 'reasoning':
63
- return (
64
- <div key={i} className="text-gray-500 italic">
65
- 💭 {part.content}
66
- </div>
67
- );
68
-
69
- case 'tool-call':
70
- return (
71
- <div key={i} className="flex items-center gap-2">
72
- <Spinner />
73
- <span>{part.toolCall?.description}</span>
74
- </div>
75
- );
76
- }
77
- })}
57
+ {part.text}
58
+ {part.status === 'streaming' && (
59
+ <span className="inline-block w-2 h-4 bg-gray-400 animate-pulse ml-1" />
60
+ )}
61
+ </div>
62
+ );
63
+ }
64
+
65
+ function ReasoningPart({ part }: { part: UIReasoningPart }) {
66
+ // Expand while streaming, collapse when done
67
+ const [expanded, setExpanded] = useState(part.status === 'streaming');
68
+
69
+ return (
70
+ <div className="bg-purple-50 p-3 rounded-lg">
71
+ <button onClick={() => setExpanded(!expanded)}>
72
+ {part.status === 'streaming' ? '💭 Thinking...' : '💭 Thought process'}
73
+ {expanded ? '▼' : '▶'}
74
+ </button>
75
+
76
+ {expanded && (
77
+ <pre className="mt-2 text-sm text-gray-600">
78
+ {part.text}
79
+ </pre>
80
+ )}
78
81
  </div>
79
82
  );
80
83
  }
81
84
  ```
82
85
 
83
- ## Reasoning Indicator
86
+ ## Tool Call States
84
87
 
85
- Show when the model is using extended reasoning:
88
+ Tool calls progress through multiple states:
86
89
 
87
90
  ```tsx
88
- function ReasoningIndicator() {
89
- const { reasoningText, status } = useOctavusChat({...});
90
-
91
- if (!reasoningText || status !== 'streaming') {
92
- return null;
93
- }
91
+ import type { UIToolCallPart } from '@octavus/client-sdk';
94
92
 
93
+ function ToolCallPart({ part }: { part: UIToolCallPart }) {
95
94
  return (
96
- <div className="bg-purple-50 p-3 rounded-lg">
97
- <div className="flex items-center gap-2 text-purple-600">
98
- <Brain className="w-4 h-4 animate-pulse" />
99
- <span className="font-medium">Thinking...</span>
95
+ <div className="border rounded p-3">
96
+ <div className="flex items-center gap-2">
97
+ <span className="text-lg">🔧</span>
98
+ <span className="font-medium">
99
+ {part.displayName || part.toolName}
100
+ </span>
101
+ <StatusBadge status={part.status} />
100
102
  </div>
101
- <p className="mt-2 text-sm text-gray-600 line-clamp-3">
102
- {reasoningText}
103
- </p>
103
+
104
+ {/* Show result when done */}
105
+ {part.status === 'done' && part.result && (
106
+ <pre className="mt-2 text-xs bg-gray-50 p-2 rounded">
107
+ {JSON.stringify(part.result, null, 2)}
108
+ </pre>
109
+ )}
110
+
111
+ {/* Show error if failed */}
112
+ {part.status === 'error' && (
113
+ <p className="mt-2 text-red-500 text-sm">{part.error}</p>
114
+ )}
104
115
  </div>
105
116
  );
106
117
  }
118
+
119
+ function StatusBadge({ status }: { status: UIToolCallPart['status'] }) {
120
+ switch (status) {
121
+ case 'pending':
122
+ return <span className="text-gray-400">○</span>;
123
+ case 'running':
124
+ return <span className="text-blue-500 animate-spin">◐</span>;
125
+ case 'done':
126
+ return <span className="text-green-500">✓</span>;
127
+ case 'error':
128
+ return <span className="text-red-500">✗</span>;
129
+ }
130
+ }
107
131
  ```
108
132
 
109
133
  ## Status Indicator
@@ -115,8 +139,6 @@ function StatusIndicator() {
115
139
  switch (status) {
116
140
  case 'idle':
117
141
  return null;
118
- case 'loading':
119
- return <div>Starting...</div>;
120
142
  case 'streaming':
121
143
  return <div>Agent is responding...</div>;
122
144
  case 'error':
@@ -125,12 +147,12 @@ function StatusIndicator() {
125
147
  }
126
148
  ```
127
149
 
128
- ## Handling Stream Completion
150
+ ## Handling Completion
129
151
 
130
152
  ```tsx
131
153
  useOctavusChat({
132
154
  onTrigger: ...,
133
- onDone: () => {
155
+ onFinish: () => {
134
156
  console.log('Stream completed');
135
157
  // Scroll to bottom, play sound, etc.
136
158
  },
@@ -141,71 +163,50 @@ useOctavusChat({
141
163
  });
142
164
  ```
143
165
 
144
- ## Streaming with Tool Calls
166
+ ## Stop Function
145
167
 
146
- When tools are called, streaming continues after tool execution:
147
-
148
- ```
149
- 1. User sends message
150
- 2. streamingText starts filling: "Let me look that up..."
151
- 3. Tool call starts (visible in streamingParts)
152
- 4. Tool executes (handled by server-sdk)
153
- 5. streamingText continues: "I found your account..."
154
- 6. Stream completes, message finalized
155
- ```
168
+ Stop the current stream and finalize any partial message:
156
169
 
157
170
  ```tsx
158
- function ChatWithTools() {
159
- const { messages, streamingText, streamingParts } = useOctavusChat({...});
160
-
161
- // Find active tool call
162
- const activeToolCall = streamingParts.find(
163
- p => p.type === 'tool-call' && p.toolCall?.status === 'streaming'
164
- );
165
-
166
- return (
167
- <div>
168
- {messages.map(msg => <Message key={msg.id} message={msg} />)}
169
-
170
- {/* Show streaming text */}
171
- {streamingText && <p>{streamingText}</p>}
172
-
173
- {/* Show active tool */}
174
- {activeToolCall && (
175
- <div className="flex items-center gap-2 text-blue-600">
176
- <Spinner />
177
- {activeToolCall.toolCall?.description}
178
- </div>
179
- )}
180
- </div>
181
- );
182
- }
171
+ const { status, stop } = useOctavusChat({...});
172
+
173
+ // Stop button
174
+ {status === 'streaming' && (
175
+ <button onClick={stop} className="text-gray-500">
176
+ Stop generating
177
+ </button>
178
+ )}
183
179
  ```
184
180
 
185
- ## Non-Main Thread Content
181
+ When `stop()` is called:
182
+ 1. The current request is aborted
183
+ 2. Any partial message is finalized with current content
184
+ 3. Status changes to `'idle'`
185
+
186
+ ## Named Thread Content
186
187
 
187
- Named threads (like summarization) stream separately:
188
+ Content from named threads (like "summary") streams separately and is identified by the `thread` property:
188
189
 
189
190
  ```tsx
190
- function StreamingContent() {
191
- const { streamingParts } = useOctavusChat({...});
191
+ import { isOtherThread } from '@octavus/client-sdk';
192
192
 
193
- // Group by thread
194
- const mainParts = streamingParts.filter(p => !p.thread || p.thread === 'main');
195
- const otherParts = streamingParts.filter(p => p.thread && p.thread !== 'main');
193
+ function MessageBubble({ message }: { message: UIMessage }) {
194
+ // Separate main thread from named threads
195
+ const mainParts = message.parts.filter(p => !isOtherThread(p));
196
+ const otherParts = message.parts.filter(p => isOtherThread(p));
196
197
 
197
198
  return (
198
199
  <div>
199
200
  {/* Main conversation */}
200
- <div>{mainParts.map(renderPart)}</div>
201
+ {mainParts.map((part, i) => <PartRenderer key={i} part={part} />)}
201
202
 
202
- {/* Named thread (e.g., summarization) */}
203
+ {/* Named thread content (e.g., summarization) */}
203
204
  {otherParts.length > 0 && (
204
- <div className="bg-orange-50 p-3 rounded mt-4">
205
- <div className="text-orange-600 font-medium">
206
- Processing in background...
205
+ <div className="bg-amber-50 p-3 rounded mt-4 border border-amber-200">
206
+ <div className="text-amber-600 font-medium mb-2">
207
+ Background processing
207
208
  </div>
208
- {otherParts.map(renderPart)}
209
+ {otherParts.map((part, i) => <PartRenderer key={i} part={part} />)}
209
210
  </div>
210
211
  )}
211
212
  </div>