ai-design-system 0.1.53 → 0.1.56

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.
@@ -22,7 +22,7 @@ type ReasoningContextValue = {
22
22
 
23
23
  const ReasoningContext = createContext<ReasoningContextValue | null>(null);
24
24
 
25
- const useReasoning = () => {
25
+ export const useReasoning = () => {
26
26
  const context = useContext(ReasoningContext);
27
27
  if (!context) {
28
28
  throw new Error("Reasoning components must be used within Reasoning");
@@ -124,14 +124,14 @@ export const Reasoning = memo(
124
124
 
125
125
  export type ReasoningTriggerProps = ComponentProps<typeof CollapsibleTrigger>;
126
126
 
127
- const getThinkingMessage = (isStreaming: boolean, duration?: number) => {
128
- if (isStreaming || duration === 0) {
127
+ export const getThinkingMessage = (isStreaming: boolean, duration?: number) => {
128
+ if (isStreaming) {
129
129
  return <Shimmer duration={1}>Thinking...</Shimmer>;
130
130
  }
131
- if (duration === undefined) {
132
- return <p>Thought for a few seconds</p>;
131
+ if (duration === undefined || duration === 0) {
132
+ return <span>Thought for a few seconds</span>;
133
133
  }
134
- return <p>Thought for {duration} seconds</p>;
134
+ return <span>Thought for {duration} seconds</span>;
135
135
  };
136
136
 
137
137
  export const ReasoningTrigger = memo(
@@ -11,6 +11,7 @@ import { UserMessage } from "@/components/composites/UserMessage"
11
11
  import { SpecialistMessage } from "@/components/composites/SpecialistMessage"
12
12
  import { OrchestratorMessage } from "@/components/composites/OrchestratorMessage"
13
13
  import { ToolCallDisplay } from "@/components/composites/ToolCallDisplay"
14
+ import { ReasoningDisplay } from "@/components/composites/ReasoningDisplay"
14
15
  import { ApprovalCard } from "@/components/composites/ApprovalCard"
15
16
 
16
17
  /**
@@ -61,6 +62,10 @@ export interface AIConversationProps
61
62
  description?: string
62
63
  icon?: React.ReactNode
63
64
  }
65
+ /**
66
+ * Callback fired when a tool action (like a link click) is triggered
67
+ */
68
+ onToolAction?: (toolCall: ToolCall, action: string) => void
64
69
  }
65
70
 
66
71
  /**
@@ -73,6 +78,7 @@ export const AIConversation = React.memo<AIConversationProps>(
73
78
  onSelectSubAgent,
74
79
  selectedSubAgent,
75
80
  emptyState,
81
+ onToolAction,
76
82
  ...conversationProps
77
83
  }) => {
78
84
  const isEmpty = React.useMemo(
@@ -122,31 +128,52 @@ export const AIConversation = React.memo<AIConversationProps>(
122
128
 
123
129
  // Filter tool calls that aren't "task" type (those become sub-agents)
124
130
  // Also completely hide "ask_user" and "ask_question" tools so they are ONLY rendered in the prompt input area
125
- const directToolCalls =
131
+ const allToolCalls =
126
132
  message.toolCalls?.filter(
127
133
  (tc) => tc.name !== "task" && tc.name !== "ask_user"
128
134
  ) || []
129
135
 
136
+ // Split into reasoning tools (shown collapsed) and direct tools (shown normally)
137
+ const reasoningCalls = allToolCalls.filter((tc) => tc.visibility === "reasoning")
138
+ const directToolCalls = allToolCalls.filter((tc) => tc.visibility !== "reasoning")
139
+
130
140
  const hasContent = contentStr.trim() !== ""
131
- if (!hasContent && directToolCalls.length === 0 && subAgents.length === 0 && !message.isLoading) {
141
+ const hasReasoning = reasoningCalls.length > 0
142
+ const reasoningText = hasReasoning ? contentStr : undefined
143
+ const displayContentStr = hasReasoning ? "" : contentStr
144
+ const hasDisplayContent = displayContentStr.trim() !== ""
145
+
146
+ if (!hasDisplayContent && directToolCalls.length === 0 && reasoningCalls.length === 0 && subAgents.length === 0 && !message.isLoading) {
132
147
  return null;
133
148
  }
134
149
 
150
+ const isStreaming = reasoningCalls.some((tc) => tc.status === "pending")
151
+
135
152
  return (
136
153
  <OrchestratorMessage
137
154
  key={message.id}
138
155
  message={{
139
156
  id: message.id,
140
- content: contentStr,
157
+ content: displayContentStr,
141
158
  avatarSrc: message.avatarSrc,
142
159
  avatarName: message.avatarName,
143
- isLoading: message.isLoading,
160
+ isLoading: message.isLoading && !hasReasoning,
144
161
  }}
145
- showAvatar={showAvatars}
162
+ showAvatar={showAvatars && hasDisplayContent}
146
163
  >
164
+ {/* Render reasoning-section for hidden tool results */}
165
+ {hasReasoning && (
166
+ <ReasoningDisplay
167
+ content={reasoningText}
168
+ items={reasoningCalls}
169
+ isStreaming={isStreaming}
170
+ onToolAction={onToolAction}
171
+ />
172
+ )}
173
+
147
174
  {/* Render direct tool calls */}
148
175
  {directToolCalls.map((tc) => (
149
- <ToolCallDisplay key={tc.id} toolCall={tc} />
176
+ <ToolCallDisplay key={tc.id} toolCall={tc} onToolAction={onToolAction} />
150
177
  ))}
151
178
 
152
179
  {/* Render specialist sub-agents */}
@@ -41,6 +41,40 @@ function renderTree(nodes: FileTreeNode[]): React.ReactNode {
41
41
  })
42
42
  }
43
43
 
44
+ function filterTree(nodes: FileTreeNode[], query: string): FileTreeNode[] {
45
+ if (!query) return nodes;
46
+ const lowerQuery = query.toLowerCase();
47
+
48
+ return nodes.reduce<FileTreeNode[]>((acc, node) => {
49
+ if (node.type === 'file') {
50
+ if (node.name.toLowerCase().includes(lowerQuery)) {
51
+ acc.push(node);
52
+ }
53
+ } else if (node.type === 'folder' && node.children) {
54
+ const filteredChildren = filterTree(node.children, query);
55
+ // Keep the folder if it matches the query itself, or if any of its children match
56
+ if (node.name.toLowerCase().includes(lowerQuery) || filteredChildren.length > 0) {
57
+ acc.push({ ...node, children: filteredChildren });
58
+ }
59
+ }
60
+ return acc;
61
+ }, []);
62
+ }
63
+
64
+ function getAllFolderPaths(nodes: FileTreeNode[]): Set<string> {
65
+ const paths = new Set<string>();
66
+ function traverse(n: FileTreeNode[]) {
67
+ for (const node of n) {
68
+ if (node.type === 'folder') {
69
+ paths.add(node.path);
70
+ if (node.children) traverse(node.children);
71
+ }
72
+ }
73
+ }
74
+ traverse(nodes);
75
+ return paths;
76
+ }
77
+
44
78
  export const FileTreeExplorer = React.memo<FileTreeExplorerProps>(
45
79
  ({
46
80
  tree,
@@ -53,6 +87,38 @@ export const FileTreeExplorer = React.memo<FileTreeExplorerProps>(
53
87
  headerClassName,
54
88
  className,
55
89
  }) => {
90
+ const [searchQuery, setSearchQuery] = React.useState("")
91
+ const [userExpanded, setUserExpanded] = React.useState<Set<string>>(defaultExpanded || new Set())
92
+
93
+ // Auto-expand parents when selectedPath changes
94
+ React.useEffect(() => {
95
+ if (selectedPath) {
96
+ const parts = selectedPath.split('/')
97
+ if (parts.length > 1) {
98
+ setUserExpanded(prev => {
99
+ const next = new Set(prev)
100
+ let currentPath = ''
101
+ for (let i = 0; i < parts.length - 1; i++) {
102
+ currentPath += (i === 0 ? '' : '/') + parts[i]
103
+ next.add(currentPath)
104
+ }
105
+ return next
106
+ })
107
+ }
108
+ }
109
+ }, [selectedPath])
110
+
111
+ const filteredTree = React.useMemo(() => {
112
+ return filterTree(tree, searchQuery)
113
+ }, [tree, searchQuery])
114
+
115
+ const activeExpanded = React.useMemo(() => {
116
+ if (searchQuery) {
117
+ return getAllFolderPaths(filteredTree)
118
+ }
119
+ return userExpanded
120
+ }, [searchQuery, filteredTree, userExpanded])
121
+
56
122
  return (
57
123
  <div className={cn("flex flex-col rounded-lg border bg-background", className)}>
58
124
  <div className={cn("flex items-center gap-2 border-b px-3 py-2", headerClassName)}>
@@ -64,6 +130,8 @@ export const FileTreeExplorer = React.memo<FileTreeExplorerProps>(
64
130
  />
65
131
  <Input
66
132
  placeholder={searchPlaceholder}
133
+ value={searchQuery}
134
+ onChange={(e) => setSearchQuery(e.target.value)}
67
135
  className="pl-8"
68
136
  />
69
137
  </div>
@@ -80,11 +148,13 @@ export const FileTreeExplorer = React.memo<FileTreeExplorerProps>(
80
148
  </div>
81
149
  <div className="p-2">
82
150
  <FileTree
83
- defaultExpanded={defaultExpanded}
151
+ className="border-none bg-transparent"
152
+ expanded={activeExpanded}
153
+ onExpandedChange={setUserExpanded}
84
154
  selectedPath={selectedPath}
85
155
  onSelect={onSelect}
86
156
  >
87
- {renderTree(tree)}
157
+ {renderTree(filteredTree)}
88
158
  </FileTree>
89
159
  </div>
90
160
  </div>
@@ -0,0 +1,76 @@
1
+ import type { Meta, StoryObj } from '@storybook/react'
2
+ import { ReasoningDisplay } from './ReasoningDisplay'
3
+
4
+ const meta: Meta<typeof ReasoningDisplay> = {
5
+ title: 'Composites/ReasoningDisplay',
6
+ component: ReasoningDisplay,
7
+ parameters: {
8
+ layout: 'padded',
9
+ },
10
+ } satisfies Meta<typeof ReasoningDisplay>
11
+
12
+ export default meta
13
+ type Story = StoryObj<typeof meta>
14
+
15
+ export const Default: Story = {
16
+ args: {
17
+ content: "Let me evaluate the user's request and read the necessary files to propose a solution.",
18
+ isStreaming: false,
19
+ items: [
20
+ {
21
+ id: 'tool_1',
22
+ name: 'read_file',
23
+ args: { file_path: '/workspace/src/main.tsx' },
24
+ result: 'import { App } from "./App"; ...',
25
+ status: 'completed',
26
+ visibility: 'reasoning',
27
+ uiVariant: 'link',
28
+ linkText: 'src/main.tsx',
29
+ linkAction: 'open-file',
30
+ },
31
+ {
32
+ id: 'tool_2',
33
+ name: 'read_file',
34
+ args: { file_path: '/workspace/src/App.tsx' },
35
+ result: 'export const App = () => <div>Hello</div>;',
36
+ status: 'completed',
37
+ visibility: 'reasoning',
38
+ uiVariant: 'link',
39
+ linkText: 'src/App.tsx',
40
+ linkAction: 'open-file',
41
+ }
42
+ ],
43
+ },
44
+ }
45
+
46
+ export const Streaming: Story = {
47
+ args: {
48
+ content: "I'll start by listing the directory contents...",
49
+ isStreaming: true,
50
+ items: [
51
+ {
52
+ id: 'tool_1',
53
+ name: 'list_dir',
54
+ args: { path: '/src' },
55
+ status: 'pending',
56
+ visibility: 'reasoning',
57
+ },
58
+ ],
59
+ },
60
+ }
61
+
62
+ export const OnlyTools: Story = {
63
+ args: {
64
+ isStreaming: false,
65
+ items: [
66
+ {
67
+ id: 'tool_1',
68
+ name: 'write_todos',
69
+ args: { todos: ['Fix layout', 'Update colors'] },
70
+ result: 'Todos written successfully.',
71
+ status: 'completed',
72
+ visibility: 'reasoning',
73
+ },
74
+ ],
75
+ },
76
+ }
@@ -0,0 +1,46 @@
1
+ "use client";
2
+
3
+ import {
4
+ Reasoning,
5
+ ReasoningTrigger,
6
+ } from "@/components/ai-elements/reasoning";
7
+ import { CollapsibleContent } from "@/components/primitives/Collapsible";
8
+ import { Response } from "@/components/ai-elements/response";
9
+ import { ToolCallDisplay } from "@/components/composites/ToolCallDisplay";
10
+ import type { ToolCall } from "@/components/composites/ToolCallDisplay";
11
+
12
+ export interface ReasoningDisplayProps {
13
+ items: ToolCall[];
14
+ isStreaming?: boolean;
15
+ content?: string;
16
+ onToolAction?: (toolCall: ToolCall, action: string) => void;
17
+ }
18
+
19
+ export const ReasoningDisplay = ({
20
+ items,
21
+ isStreaming = false,
22
+ content,
23
+ onToolAction,
24
+ }: ReasoningDisplayProps) => {
25
+ if (items.length === 0 && !content) return null;
26
+
27
+ return (
28
+ <Reasoning isStreaming={isStreaming} defaultOpen={false}>
29
+ <ReasoningTrigger />
30
+ <CollapsibleContent
31
+ className="mt-4 flex flex-col gap-2 data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-muted-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in"
32
+ >
33
+ {content && content.trim() && (
34
+ <div className="mb-4">
35
+ <Response className="grid gap-2">{content}</Response>
36
+ </div>
37
+ )}
38
+ <div className="flex flex-col gap-2">
39
+ {items.map((item) => (
40
+ <ToolCallDisplay key={item.id} toolCall={item} onToolAction={onToolAction} />
41
+ ))}
42
+ </div>
43
+ </CollapsibleContent>
44
+ </Reasoning>
45
+ );
46
+ };
@@ -0,0 +1,2 @@
1
+ export { ReasoningDisplay } from './ReasoningDisplay'
2
+ export type { ReasoningDisplayProps } from './ReasoningDisplay'
@@ -1,4 +1,5 @@
1
1
  import * as React from "react"
2
+ import { Icon } from "@/components/primitives/Icon"
2
3
  import {
3
4
  Tool,
4
5
  ToolHeader,
@@ -20,6 +21,10 @@ export interface ToolCall {
20
21
  args: Record<string, unknown>
21
22
  result?: string
22
23
  status: "pending" | "completed" | "error"
24
+ visibility?: "visible" | "reasoning"
25
+ uiVariant?: "default" | "link"
26
+ linkText?: string
27
+ linkAction?: string
23
28
  }
24
29
 
25
30
  export interface ToolCallDisplayProps {
@@ -31,13 +36,17 @@ export interface ToolCallDisplayProps {
31
36
  * Whether the tool call is initially expanded
32
37
  */
33
38
  defaultExpanded?: boolean
39
+ /**
40
+ * Callback fired when a tool action (like a link click) is triggered
41
+ */
42
+ onToolAction?: (toolCall: ToolCall, action: string) => void
34
43
  }
35
44
 
36
45
  /**
37
46
  * ToolCallDisplay component - displays tool execution with expandable details
38
47
  */
39
48
  export const ToolCallDisplay = React.memo<ToolCallDisplayProps>(
40
- ({ toolCall, defaultExpanded = false }) => {
49
+ ({ toolCall, defaultExpanded = false, onToolAction }) => {
41
50
  const [isExpanded, setIsExpanded] = React.useState(defaultExpanded)
42
51
 
43
52
  const { name, args, result, status } = React.useMemo(() => {
@@ -78,6 +87,26 @@ export const ToolCallDisplay = React.memo<ToolCallDisplayProps>(
78
87
  }
79
88
  }, [status])
80
89
 
90
+ if (toolCall.uiVariant === "link") {
91
+ return (
92
+ <div
93
+ className="flex items-center gap-2 p-3 my-2 rounded-md border bg-muted/30 cursor-pointer hover:bg-muted/50 transition-colors"
94
+ onClick={(e) => {
95
+ e.preventDefault();
96
+ e.stopPropagation();
97
+ if (toolCall.linkAction && onToolAction) {
98
+ onToolAction(toolCall, toolCall.linkAction);
99
+ }
100
+ }}
101
+ >
102
+ <Icon name="external-link" className="size-4 text-blue-500" />
103
+ <span className="font-medium text-sm text-blue-500 hover:underline">
104
+ {toolCall.linkText || `${name} Action`}
105
+ </span>
106
+ </div>
107
+ )
108
+ }
109
+
81
110
  return (
82
111
  <Tool open={isExpanded} onOpenChange={setIsExpanded}>
83
112
  <ToolHeader
@@ -9,6 +9,10 @@
9
9
  export { ToolCallDisplay } from './ToolCallDisplay'
10
10
  export type { ToolCallDisplayProps, ToolCall } from './ToolCallDisplay'
11
11
 
12
+ // ReasoningDisplay Block
13
+ export { ReasoningDisplay } from './ReasoningDisplay'
14
+ export type { ReasoningDisplayProps } from './ReasoningDisplay'
15
+
12
16
  // AgentIndicator Block
13
17
  export { AgentIndicator } from './AgentIndicator'
14
18
  export type { AgentIndicatorProps, SubAgent } from './AgentIndicator'
@@ -103,6 +103,8 @@ export interface AIDocEditorMultiTabProps {
103
103
  onTabClose?: (documentId: string) => void
104
104
  /** Optional completely separate file tree to show all available files in the explorer (even closed ones). If not provided, it falls back to showing only the currently open documents. */
105
105
  fileTree?: FileTreeNode[]
106
+ /** When true, hides the document tab bar — useful for single-file mode where the file tree provides navigation. */
107
+ hideTabBar?: boolean
106
108
  }
107
109
 
108
110
  /**
@@ -273,60 +275,70 @@ export const AIDocEditor = React.memo<AIDocEditorProps>(
273
275
  /**
274
276
  * Multi-tab mode
275
277
  */
278
+ const { hideTabBar } = props as AIDocEditorMultiTabProps
279
+
280
+ let editorPane: React.ReactNode
281
+
276
282
  if (!currentDocument) {
277
- return (
278
- <div className={cn('ai-doc-editor flex flex-col flex-1 h-screen w-full', className)}>
279
- <DocumentTabBar
280
- tabs={props.documents?.map((doc) => doc.file) || []}
281
- activeTabId={props.activeDocumentId}
282
- onTabSelect={props.onTabSelect}
283
- onTabClose={props.onTabClose}
284
- />
283
+ editorPane = (
284
+ <div className="ai-doc-editor flex flex-col h-full w-full">
285
+ {!hideTabBar && (
286
+ <DocumentTabBar
287
+ className="w-full"
288
+ tabs={props.documents?.map((doc) => doc.file) || []}
289
+ activeTabId={props.activeDocumentId}
290
+ onTabSelect={props.onTabSelect}
291
+ onTabClose={props.onTabClose}
292
+ />
293
+ )}
285
294
  <div className="flex-1 flex items-center justify-center p-6 text-muted-foreground">
286
295
  No documents open
287
296
  </div>
288
297
  </div>
289
298
  )
290
- }
299
+ } else {
300
+ const isMarkdownMulti = currentDocument.file.format === 'markdown'
291
301
 
292
- const isMarkdownMulti = currentDocument.file.format === 'markdown'
293
-
294
- const editorPane = (
295
- <div className="ai-doc-editor flex flex-col h-full w-full">
296
- <DocumentTabBar
297
- className="w-full"
298
- tabs={props.documents?.map((doc) => doc.file) || []}
299
- activeTabId={props.activeDocumentId}
300
- onTabSelect={props.onTabSelect}
301
- onTabClose={props.onTabClose}
302
- />
303
- <div className="flex-1 overflow-auto">
304
- {isMarkdownMulti ? (
305
- <div className="p-6">
306
- <Streamdown
307
- mode="streaming"
308
- isAnimating
309
- className="[&>*:first-child]:mt-0 [&>*:last-child]:mb-0"
310
- >
311
- {currentDocument.content as string}
312
- </Streamdown>
313
- </div>
314
- ) : (
315
- <DocumentEditorWithComments
316
- content={currentDocument.content}
317
- format={currentDocument.file.format as 'json' | 'markdown' | undefined}
318
- annotations={currentDocument.annotations}
319
- currentUserId={currentUser.id}
320
- currentUserName={currentUser.name}
321
- readOnly={mode === 'readonly'}
322
- onAnnotationAdd={handleAnnotationAdd}
323
- onAnnotationUpdate={handleAnnotationUpdate}
324
- className="p-6 h-full flex flex-col"
302
+ editorPane = (
303
+ <div className="ai-doc-editor flex flex-col h-full w-full">
304
+ {!hideTabBar && (
305
+ <DocumentTabBar
306
+ className="w-full"
307
+ tabs={props.documents?.map((doc) => doc.file) || []}
308
+ activeTabId={props.activeDocumentId}
309
+ onTabSelect={props.onTabSelect}
310
+ onTabClose={props.onTabClose}
325
311
  />
326
312
  )}
313
+ <div className="flex-1 overflow-auto">
314
+ {isMarkdownMulti ? (
315
+ <div className="p-6">
316
+ <Streamdown
317
+ mode="streaming"
318
+ isAnimating
319
+ className="[&>*:first-child]:mt-0 [&>*:last-child]:mb-0"
320
+ >
321
+ {currentDocument.content as string}
322
+ </Streamdown>
323
+ </div>
324
+ ) : (
325
+ <DocumentEditorWithComments
326
+ content={currentDocument.content}
327
+ format={currentDocument.file.format as 'json' | 'markdown' | undefined}
328
+ annotations={currentDocument.annotations}
329
+ selectedAnnotationId={props.selectedAnnotationId}
330
+ currentUserId={currentUser.id}
331
+ currentUserName={currentUser.name}
332
+ readOnly={mode === 'readonly'}
333
+ onAnnotationAdd={handleAnnotationAdd}
334
+ onAnnotationUpdate={handleAnnotationUpdate}
335
+ className="ai-doc-editor p-6 min-h-full"
336
+ />
337
+ )}
338
+ </div>
327
339
  </div>
328
- </div>
329
- )
340
+ )
341
+ }
330
342
 
331
343
  return (
332
344
  <AdjustableLayout
@@ -127,17 +127,12 @@ function useMultiTabDocEditorMockState(
127
127
 
128
128
  const closeDocument = useCallback((documentId: string) => {
129
129
  console.log('[Multi-Tab Mock] Closing document:', documentId)
130
- setDocuments(prev => {
131
- const filtered = prev.filter(doc => doc.file.id !== documentId)
132
- if (activeDocumentId === documentId && filtered.length > 0) {
133
- setActiveDocumentId(filtered[0].file.id)
134
- }
135
- if (filtered.length === 0) {
136
- setActiveDocumentId(undefined)
137
- }
138
- return filtered
139
- })
140
- }, [activeDocumentId])
130
+ setDocuments(prev => prev.filter(doc => doc.file.id !== documentId))
131
+ if (activeDocumentId === documentId) {
132
+ const filtered = documents.filter(doc => doc.file.id !== documentId)
133
+ setActiveDocumentId(filtered.length > 0 ? filtered[0].file.id : undefined)
134
+ }
135
+ }, [activeDocumentId, documents])
141
136
 
142
137
  const switchDocument = useCallback((documentId: string) => {
143
138
  console.log('[Multi-Tab Mock] Switching to document:', documentId)
@@ -101,6 +101,10 @@ export interface RefinementPanelProps {
101
101
  * Additional CSS classes
102
102
  */
103
103
  className?: string;
104
+ /**
105
+ * Action handler for tool interactions
106
+ */
107
+ onToolAction?: (toolCall: ToolCall, action: string) => void;
104
108
  }
105
109
 
106
110
  /**
@@ -122,6 +126,7 @@ export const RefinementPanel = React.memo<RefinementPanelProps>(
122
126
  loading = false,
123
127
  placeholder = "Ask a question or describe a task...",
124
128
  className,
129
+ onToolAction,
125
130
  }) => {
126
131
  // File change queue state
127
132
  const [fileChangeState, setFileChangeState] = React.useState<
@@ -262,6 +267,7 @@ export const RefinementPanel = React.memo<RefinementPanelProps>(
262
267
  <AIConversation
263
268
  messages={messages}
264
269
  showAvatars={true}
270
+ onToolAction={onToolAction}
265
271
  className="flex-1 min-h-0"
266
272
  />
267
273
  <div className="sticky bottom-0 z-10 p-4 bg-gradient-to-t from-card via-card to-transparent pt-6">
@@ -11,6 +11,7 @@
11
11
  import type { RefinementMessage } from "./RefinementPanel";
12
12
  import type { FileChangeData } from "@/components/composites/FileQueue";
13
13
  import type { ActionRequest, ReviewConfig } from "@/components/composites/ApprovalCard";
14
+ import type { ToolCall } from "@/components/composites/ToolCallDisplay";
14
15
 
15
16
  /**
16
17
  * Return type for the refinement panel hook
@@ -51,6 +52,9 @@ export interface UseRefinementPanelReturn {
51
52
 
52
53
  /** Processing state for HITL approval */
53
54
  isApprovalProcessing?: boolean;
55
+
56
+ /** Action handler for tool interactions */
57
+ handleToolAction?: (toolCall: ToolCall, action: string) => void;
54
58
  }
55
59
 
56
60
  /**
@@ -3,6 +3,7 @@ import type { RefinementMessage } from "./RefinementPanel";
3
3
  import type { FileChangeData } from "@/components/composites/FileQueue";
4
4
  import type { PromptInputMessage } from "@/components/ai-elements/prompt-input";
5
5
  import type { ActionRequest, ReviewConfig } from "@/components/composites/ApprovalCard";
6
+ import type { ToolCall } from "@/components/composites/ToolCallDisplay";
6
7
  import type { FormEvent } from "react";
7
8
 
8
9
  export interface UseRefinementPanelReturn {
@@ -19,6 +20,7 @@ export interface UseRefinementPanelReturn {
19
20
  handleApprovalReject?: (reason: string) => Promise<void>;
20
21
  handleApprovalEdit?: (editedArgs: Record<string, unknown>) => Promise<void>;
21
22
  isApprovalProcessing?: boolean;
23
+ handleToolAction?: (toolCall: ToolCall, action: string) => void;
22
24
  }
23
25
 
24
26
  export interface UseRefinementPanelOptions {
@@ -173,5 +175,6 @@ export function useRefinementPanelMock(
173
175
  handleApprovalReject: initialApprovalRequest ? handleApprovalReject : undefined,
174
176
  handleApprovalEdit: initialApprovalRequest ? handleApprovalEdit : undefined,
175
177
  isApprovalProcessing,
178
+ handleToolAction: (toolCall, action) => console.log(`[Mock Tool Action] ${action} on tool:`, toolCall),
176
179
  };
177
180
  }