ai-design-system 0.1.64 → 0.1.65

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.
@@ -12,7 +12,6 @@ import { SpecialistMessage } from "@/components/composites/SpecialistMessage"
12
12
  import { OrchestratorMessage } from "@/components/composites/OrchestratorMessage"
13
13
  import { ToolCallDisplay } from "@/components/composites/ToolCallDisplay"
14
14
  import { ReasoningDisplay } from "@/components/composites/ReasoningDisplay"
15
- import { ApprovalCard } from "@/components/composites/ApprovalCard"
16
15
 
17
16
  /**
18
17
  * AIConversation Section
@@ -24,6 +23,11 @@ import { ApprovalCard } from "@/components/composites/ApprovalCard"
24
23
  * Based on reference implementation from deep-agents-ui ChatInterface.
25
24
  */
26
25
 
26
+ type AIMessageBlock =
27
+ | { type: 'text'; id: string; text: string }
28
+ | { type: 'toolCall'; id: string; toolCall: ToolCall }
29
+ | { type: 'subAgent'; id: string; subAgent: SubAgent }
30
+
27
31
  interface AIMessage {
28
32
  id: string;
29
33
  type: 'human' | 'ai';
@@ -34,6 +38,7 @@ interface AIMessage {
34
38
  toolCalls?: ToolCall[];
35
39
  subAgents?: SubAgent[];
36
40
  isLoading?: boolean;
41
+ blocks?: AIMessageBlock[];
37
42
  }
38
43
 
39
44
  export interface AIConversationProps
@@ -112,71 +117,95 @@ export const AIConversation = React.memo<AIConversationProps>(
112
117
  (!msg.toolCalls || msg.toolCalls.length === 0) &&
113
118
  (!msg.subAgents || msg.subAgents.length === 0);
114
119
 
115
- if (!isFinalResponse) {
116
- if (currentGroup) {
117
- if (msg.content) {
118
- currentGroup.content = currentGroup.content
119
- ? `${currentGroup.content}\n\n${msg.content}`
120
- : msg.content;
120
+ if (currentGroup) {
121
+ if (msg.content) {
122
+ currentGroup.content = currentGroup.content
123
+ ? `${currentGroup.content}\n\n${msg.content}`
124
+ : msg.content;
125
+ }
126
+ if (msg.toolCalls) {
127
+ const existingToolCalls = currentGroup.toolCalls || [];
128
+ const updatedToolCalls = [...existingToolCalls];
129
+ for (const newCall of msg.toolCalls) {
130
+ const existingIndex = updatedToolCalls.findIndex(tc => tc.id === newCall.id);
131
+ if (existingIndex >= 0) {
132
+ updatedToolCalls[existingIndex] = newCall;
133
+ } else {
134
+ updatedToolCalls.push(newCall);
135
+ }
121
136
  }
122
- if (msg.toolCalls) {
123
- const existingToolCalls = currentGroup.toolCalls || [];
124
- const updatedToolCalls = [...existingToolCalls];
125
- for (const newCall of msg.toolCalls) {
126
- const existingIndex = updatedToolCalls.findIndex(tc => tc.id === newCall.id);
127
- if (existingIndex >= 0) {
128
- updatedToolCalls[existingIndex] = newCall;
137
+ currentGroup.toolCalls = updatedToolCalls;
138
+ }
139
+ if (msg.subAgents) {
140
+ const existingSubAgents = currentGroup.subAgents || [];
141
+ const updatedSubAgents = [...existingSubAgents];
142
+ for (const newAgent of msg.subAgents) {
143
+ const existingIndex = updatedSubAgents.findIndex(a => a.id === newAgent.id);
144
+ if (existingIndex >= 0) {
145
+ updatedSubAgents[existingIndex] = newAgent;
146
+ } else {
147
+ if (
148
+ updatedSubAgents.length > 0 &&
149
+ updatedSubAgents[updatedSubAgents.length - 1].subAgentName === newAgent.subAgentName
150
+ ) {
151
+ const prev = updatedSubAgents[updatedSubAgents.length - 1];
152
+ let mergedOutput = prev.output;
153
+ if (newAgent.output) {
154
+ const prevStr = typeof prev.output === 'string' ? prev.output : (prev.output ? JSON.stringify(prev.output) : '');
155
+ const newStr = typeof newAgent.output === 'string' ? newAgent.output : JSON.stringify(newAgent.output);
156
+ mergedOutput = prevStr ? `${prevStr}\n\n${newStr}` : newStr;
157
+ }
158
+ updatedSubAgents[updatedSubAgents.length - 1] = {
159
+ ...newAgent,
160
+ id: prev.id,
161
+ input: prev.input,
162
+ output: mergedOutput
163
+ };
129
164
  } else {
130
- updatedToolCalls.push(newCall);
165
+ updatedSubAgents.push(newAgent);
131
166
  }
132
167
  }
133
- currentGroup.toolCalls = updatedToolCalls;
134
168
  }
135
- if (msg.subAgents) {
136
- const existingSubAgents = currentGroup.subAgents || [];
137
- const updatedSubAgents = [...existingSubAgents];
138
- for (const newAgent of msg.subAgents) {
139
- const existingIndex = updatedSubAgents.findIndex(a => a.id === newAgent.id);
169
+ currentGroup.subAgents = updatedSubAgents;
170
+ }
171
+ if (msg.blocks) {
172
+ const existingBlocks = currentGroup.blocks || [];
173
+ const updatedBlocks = [...existingBlocks];
174
+ for (const newBlock of msg.blocks) {
175
+ const lastBlock = updatedBlocks[updatedBlocks.length - 1];
176
+ if (
177
+ updatedBlocks.length > 0 &&
178
+ lastBlock.type === 'text' &&
179
+ newBlock.type === 'text'
180
+ ) {
181
+ updatedBlocks[updatedBlocks.length - 1] = {
182
+ ...lastBlock,
183
+ text: (lastBlock.text || '') + '\n\n' + (newBlock.text || ''),
184
+ };
185
+ } else {
186
+ const existingIndex = updatedBlocks.findIndex(b => b.id === newBlock.id && b.type === newBlock.type && b.type !== 'text');
140
187
  if (existingIndex >= 0) {
141
- updatedSubAgents[existingIndex] = newAgent;
188
+ updatedBlocks[existingIndex] = newBlock;
142
189
  } else {
143
- // Merge consecutive subAgents with the same name
144
- if (
145
- updatedSubAgents.length > 0 &&
146
- updatedSubAgents[updatedSubAgents.length - 1].subAgentName === newAgent.subAgentName
147
- ) {
148
- const prev = updatedSubAgents[updatedSubAgents.length - 1];
149
- let mergedOutput = prev.output;
150
- if (newAgent.output) {
151
- const prevStr = typeof prev.output === 'string' ? prev.output : (prev.output ? JSON.stringify(prev.output) : '');
152
- const newStr = typeof newAgent.output === 'string' ? newAgent.output : JSON.stringify(newAgent.output);
153
- mergedOutput = prevStr ? `${prevStr}\n\n${newStr}` : newStr;
154
- }
155
- updatedSubAgents[updatedSubAgents.length - 1] = {
156
- ...newAgent,
157
- id: prev.id, // keep the original id so React keys don't jump
158
- input: prev.input, // keep original input or combine? Just keep original
159
- output: mergedOutput
160
- };
161
- } else {
162
- updatedSubAgents.push(newAgent);
163
- }
190
+ updatedBlocks.push(newBlock);
164
191
  }
165
192
  }
166
- currentGroup.subAgents = updatedSubAgents;
167
193
  }
168
- currentGroup.isLoading = msg.isLoading;
169
- } else {
170
- currentGroup = {
171
- ...msg,
172
- toolCalls: msg.toolCalls ? [...msg.toolCalls] : undefined,
173
- subAgents: msg.subAgents ? [...msg.subAgents] : undefined,
174
- };
175
- result.push(currentGroup);
194
+ currentGroup.blocks = updatedBlocks;
176
195
  }
196
+ currentGroup.isLoading = msg.isLoading;
177
197
  } else {
198
+ currentGroup = {
199
+ ...msg,
200
+ toolCalls: msg.toolCalls ? [...msg.toolCalls] : undefined,
201
+ subAgents: msg.subAgents ? [...msg.subAgents] : undefined,
202
+ blocks: msg.blocks ? [...msg.blocks] : undefined,
203
+ };
204
+ result.push(currentGroup);
205
+ }
206
+
207
+ if (isFinalResponse) {
178
208
  currentGroup = null;
179
- result.push(msg);
180
209
  }
181
210
  } else {
182
211
  currentGroup = null;
@@ -223,11 +252,11 @@ export const AIConversation = React.memo<AIConversationProps>(
223
252
  const directToolCalls = allToolCalls.filter((tc) => tc.visibility !== "reasoning")
224
253
 
225
254
  const hasReasoning = reasoningCalls.length > 0 || subAgents.length > 0 || directToolCalls.length > 0 || (message.isLoading && contentStr.trim() !== "");
226
- const reasoningText = hasReasoning ? contentStr : undefined
255
+ const reasoningText = (hasReasoning && (!message.blocks || message.blocks.length === 0)) ? contentStr : undefined
227
256
  const displayContentStr = hasReasoning ? "" : contentStr
228
257
  const hasDisplayContent = displayContentStr.trim() !== ""
229
258
 
230
- if (!hasDisplayContent && directToolCalls.length === 0 && reasoningCalls.length === 0 && subAgents.length === 0 && !message.isLoading) {
259
+ if (!hasDisplayContent && directToolCalls.length === 0 && reasoningCalls.length === 0 && subAgents.length === 0 && !message.isLoading && (!message.blocks || message.blocks.length === 0)) {
231
260
  return null;
232
261
  }
233
262
 
@@ -249,32 +278,66 @@ export const AIConversation = React.memo<AIConversationProps>(
249
278
  {hasReasoning && (
250
279
  <ReasoningDisplay
251
280
  content={reasoningText}
252
- items={reasoningCalls}
281
+ items={message.blocks && message.blocks.length > 0 ? [] : reasoningCalls}
253
282
  isStreaming={isStreaming}
254
283
  onToolAction={onToolAction}
255
284
  defaultOpen={isStreaming || index === groupedMessages.length - 1}
256
285
  >
257
- {/* Render direct tool calls */}
258
- {directToolCalls.map((tc) => (
259
- <ToolCallDisplay key={tc.id} toolCall={tc} onToolAction={onToolAction} />
260
- ))}
286
+ {message.blocks && message.blocks.length > 0 ? (
287
+ message.blocks.map((block, i) => {
288
+ if (block.type === 'text') {
289
+ return block.text && block.text.trim() ? (
290
+ <div key={`text-${i}`} className="mb-4 text-muted-foreground whitespace-pre-wrap">
291
+ {block.text}
292
+ </div>
293
+ ) : null;
294
+ } else if (block.type === 'toolCall' && block.toolCall && block.toolCall.name !== 'ask_user' && block.toolCall.name !== 'ask_question') {
295
+ return <ToolCallDisplay key={block.id} toolCall={block.toolCall} onToolAction={onToolAction} />;
296
+ } else if (block.type === 'subAgent' && block.subAgent) {
297
+ const subAgent = block.subAgent;
298
+ return (
299
+ <SpecialistMessage
300
+ key={subAgent.id}
301
+ message={{
302
+ id: subAgent.id,
303
+ name: subAgent.subAgentName,
304
+ description: undefined,
305
+ input: subAgent.input,
306
+ content: typeof subAgent.output === 'string' ? subAgent.output : (subAgent.output ? JSON.stringify(subAgent.output) : ''),
307
+ status: subAgent.status,
308
+ toolCalls: [],
309
+ }}
310
+ isNested={true}
311
+ />
312
+ );
313
+ }
314
+ return null;
315
+ })
316
+ ) : (
317
+ <>
318
+ {/* Render direct tool calls fallback */}
319
+ {directToolCalls.map((tc) => (
320
+ <ToolCallDisplay key={tc.id} toolCall={tc} onToolAction={onToolAction} />
321
+ ))}
261
322
 
262
- {/* Render specialist sub-agents */}
263
- {subAgents.map((subAgent) => (
264
- <SpecialistMessage
265
- key={subAgent.id}
266
- message={{
267
- id: subAgent.id,
268
- name: subAgent.subAgentName,
269
- description: undefined,
270
- input: subAgent.input,
271
- content: typeof subAgent.output === 'string' ? subAgent.output : (subAgent.output ? JSON.stringify(subAgent.output) : ''),
272
- status: subAgent.status,
273
- toolCalls: [],
274
- }}
275
- isNested={true}
276
- />
277
- ))}
323
+ {/* Render specialist sub-agents fallback */}
324
+ {subAgents.map((subAgent) => (
325
+ <SpecialistMessage
326
+ key={subAgent.id}
327
+ message={{
328
+ id: subAgent.id,
329
+ name: subAgent.subAgentName,
330
+ description: undefined,
331
+ input: subAgent.input,
332
+ content: typeof subAgent.output === 'string' ? subAgent.output : (subAgent.output ? JSON.stringify(subAgent.output) : ''),
333
+ status: subAgent.status,
334
+ toolCalls: [],
335
+ }}
336
+ isNested={true}
337
+ />
338
+ ))}
339
+ </>
340
+ )}
278
341
  </ReasoningDisplay>
279
342
  )}
280
343
  </OrchestratorMessage>
@@ -179,7 +179,7 @@ export const FileTreeExplorer = React.memo<FileTreeExplorerProps>(
179
179
  ) : null}
180
180
  <a ref={downloadRef} style={{ display: 'none' }} />
181
181
  </div>
182
- <div className="p-2">
182
+ <div className="overflow-y-auto p-2">
183
183
  <FileTree
184
184
  className="border-none bg-transparent"
185
185
  expanded={activeExpanded}
@@ -176,7 +176,6 @@ export const TextEditor = React.memo<TextEditorProps>(
176
176
  className,
177
177
  onAnnotationAdd,
178
178
  onAnnotationUpdate,
179
- onAnnotationClick,
180
179
  } = props
181
180
 
182
181
  const isMultiTab = isMultiTabMode(props)
@@ -212,18 +211,18 @@ export const TextEditor = React.memo<TextEditorProps>(
212
211
  },
213
212
  [onAnnotationUpdate]
214
213
  )
215
- const fileTreeNodes = useMemo(() => {
216
- if (!isMultiTab) return []
214
+ const multiTabDocs = isMultiTab ? (props as TextEditorMultiTabProps).documents : undefined
215
+ const multiTabFileTree = isMultiTab ? (props as TextEditorMultiTabProps).fileTree : undefined
217
216
 
218
- const multiProps = props as TextEditorMultiTabProps
219
- if (multiProps.fileTree) {
220
- return multiProps.fileTree
217
+ const fileTreeNodes = useMemo(() => {
218
+ if (multiTabFileTree) {
219
+ return multiTabFileTree
221
220
  }
222
221
 
223
- if (!multiProps.documents) return []
222
+ if (!multiTabDocs) return []
224
223
  const rootNodes: FileTreeNode[] = []
225
224
 
226
- multiProps.documents.forEach((doc) => {
225
+ multiTabDocs.forEach((doc) => {
227
226
  const parts = doc.file.id.split('/')
228
227
  if (parts.length === 1) {
229
228
  rootNodes.push({
@@ -257,7 +256,33 @@ export const TextEditor = React.memo<TextEditorProps>(
257
256
  })
258
257
  })
259
258
  return rootNodes
260
- }, [isMultiTab, (props as TextEditorMultiTabProps).documents, (props as TextEditorMultiTabProps).fileTree])
259
+ }, [multiTabDocs, multiTabFileTree])
260
+
261
+ const handleTabSelect = isMultiTab ? (props as TextEditorMultiTabProps).onTabSelect : undefined
262
+
263
+ const LinkComponent = useCallback((linkProps: React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
264
+ const { href, ...rest } = linkProps
265
+ const isExternal = href?.startsWith('http://') || href?.startsWith('https://') || href?.startsWith('mailto:')
266
+ const isAnchor = href?.startsWith('#')
267
+
268
+ const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
269
+ if (!isExternal && !isAnchor && href) {
270
+ e.preventDefault()
271
+ let targetId = href
272
+ if (targetId.startsWith('./')) {
273
+ targetId = targetId.slice(2)
274
+ }
275
+ if (handleTabSelect) {
276
+ handleTabSelect(targetId)
277
+ }
278
+ }
279
+ }
280
+
281
+ if (isExternal) {
282
+ return <a href={href} target="_blank" rel="noopener noreferrer" {...rest} />
283
+ }
284
+ return <a href={href} onClick={handleClick} className="text-primary hover:underline cursor-pointer" {...rest} />
285
+ }, [handleTabSelect])
261
286
 
262
287
  /**
263
288
  * Single-document mode
@@ -274,7 +299,7 @@ export const TextEditor = React.memo<TextEditorProps>(
274
299
  : contentStr
275
300
  return (
276
301
  <div className={cn('text-editor p-6 flex flex-col h-screen w-full flex-1', className)}>
277
- <StreamingMarkdown mode="streaming">
302
+ <StreamingMarkdown mode="streaming" components={{ a: LinkComponent }}>
278
303
  {content}
279
304
  </StreamingMarkdown>
280
305
  </div>
@@ -346,6 +371,7 @@ export const TextEditor = React.memo<TextEditorProps>(
346
371
  mode="streaming"
347
372
  isAnimating
348
373
  className="[&>*:first-child]:mt-0 [&>*:last-child]:mb-0"
374
+ components={{ a: LinkComponent }}
349
375
  >
350
376
  {isCodeMulti
351
377
  ? `\`\`\`${format}\n${format === 'json' ? formatJson(currentDocument.content as string) : currentDocument.content}\n\`\`\``