ai-design-system 0.1.64 → 0.1.66

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>
@@ -20,6 +20,7 @@ export const AppSidebar = React.memo<AppSidebarProps>(
20
20
  ({
21
21
  logo,
22
22
  mainNavigation,
23
+ navigationGroups,
23
24
  secondaryNavigation,
24
25
  documents,
25
26
  user,
@@ -46,11 +47,22 @@ export const AppSidebar = React.memo<AppSidebarProps>(
46
47
  </SidebarHeader>
47
48
 
48
49
  <SidebarContent>
49
- <SidebarGroup>
50
- <SidebarGroupContent>
51
- <NavigationList items={mainNavigation} />
52
- </SidebarGroupContent>
53
- </SidebarGroup>
50
+ {mainNavigation && mainNavigation.length > 0 && (
51
+ <SidebarGroup>
52
+ <SidebarGroupContent>
53
+ <NavigationList items={mainNavigation} />
54
+ </SidebarGroupContent>
55
+ </SidebarGroup>
56
+ )}
57
+
58
+ {navigationGroups?.map((group) => (
59
+ <SidebarGroup key={group.label}>
60
+ <SidebarGroupLabel>{group.label}</SidebarGroupLabel>
61
+ <SidebarGroupContent>
62
+ <NavigationList items={group.items} />
63
+ </SidebarGroupContent>
64
+ </SidebarGroup>
65
+ ))}
54
66
 
55
67
  {documents && documents.length > 0 && (
56
68
  <SidebarGroup>
@@ -1,13 +1,19 @@
1
1
  import type { NavigationItem } from "@/components/composites/NavigationList/interfaces";
2
2
  import type { SidebarProps } from "@/components/primitives/Sidebar";
3
3
 
4
+ export interface NavigationGroup {
5
+ label: string;
6
+ items: NavigationItem[];
7
+ }
8
+
4
9
  export interface AppSidebarProps {
5
10
  logo?: {
6
11
  icon: string;
7
12
  text: string;
8
13
  href: string;
9
14
  };
10
- mainNavigation: NavigationItem[];
15
+ mainNavigation?: NavigationItem[];
16
+ navigationGroups?: NavigationGroup[];
11
17
  secondaryNavigation?: NavigationItem[];
12
18
  documents?: NavigationItem[];
13
19
  user: {
@@ -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}
@@ -25,7 +25,7 @@ export const NavigationList = React.memo<NavigationListProps>(
25
25
  ({ items, onItemClick, className }) => {
26
26
  return (
27
27
  <SidebarMenu className={className}>
28
- {items.map((item) => (
28
+ {items?.map((item) => (
29
29
  <SidebarMenuItem key={item.title}>
30
30
  <SidebarMenuButton
31
31
  tooltip={item.title}
@@ -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,68 @@ 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
+
273
+ if (targetId.startsWith('file://')) {
274
+ let path = targetId.replace('file://', '')
275
+ // On Windows, file:///C:/path becomes /C:/path, we want C:/path
276
+ if (path.match(/^\/[a-zA-Z]:\//)) {
277
+ path = path.slice(1)
278
+ }
279
+ if (isMultiTab && props.documents) {
280
+ const matchedDoc = props.documents.find(doc => path.endsWith(doc.file.id) || path.endsWith('/' + doc.file.id))
281
+ if (matchedDoc) {
282
+ targetId = matchedDoc.file.id
283
+ } else {
284
+ targetId = path
285
+ }
286
+ } else {
287
+ targetId = path
288
+ }
289
+ } else if (props.activeDocumentId) {
290
+ if (targetId.startsWith('/')) {
291
+ targetId = targetId.slice(1)
292
+ } else {
293
+ const baseParts = props.activeDocumentId.split('/')
294
+ baseParts.pop() // remove current filename
295
+ const targetParts = targetId.split('/')
296
+ for (const part of targetParts) {
297
+ if (part === '.' || part === '') continue
298
+ if (part === '..') {
299
+ if (baseParts.length > 0) baseParts.pop()
300
+ } else {
301
+ baseParts.push(part)
302
+ }
303
+ }
304
+ targetId = baseParts.join('/')
305
+ }
306
+ } else if (targetId.startsWith('./')) {
307
+ targetId = targetId.slice(2)
308
+ }
309
+
310
+ if (handleTabSelect) {
311
+ handleTabSelect(targetId)
312
+ }
313
+ }
314
+ }
315
+
316
+ if (isExternal) {
317
+ return <a href={href} target="_blank" rel="noopener noreferrer" {...rest} />
318
+ }
319
+ return <a href={href} onClick={handleClick} className="text-primary hover:underline cursor-pointer" {...rest} />
320
+ }, [handleTabSelect, props.activeDocumentId])
261
321
 
262
322
  /**
263
323
  * Single-document mode
@@ -274,7 +334,7 @@ export const TextEditor = React.memo<TextEditorProps>(
274
334
  : contentStr
275
335
  return (
276
336
  <div className={cn('text-editor p-6 flex flex-col h-screen w-full flex-1', className)}>
277
- <StreamingMarkdown mode="streaming">
337
+ <StreamingMarkdown mode="streaming" components={{ a: LinkComponent }}>
278
338
  {content}
279
339
  </StreamingMarkdown>
280
340
  </div>
@@ -346,6 +406,7 @@ export const TextEditor = React.memo<TextEditorProps>(
346
406
  mode="streaming"
347
407
  isAnimating
348
408
  className="[&>*:first-child]:mt-0 [&>*:last-child]:mb-0"
409
+ components={{ a: LinkComponent }}
349
410
  >
350
411
  {isCodeMulti
351
412
  ? `\`\`\`${format}\n${format === 'json' ? formatJson(currentDocument.content as string) : currentDocument.content}\n\`\`\``