@tangle-network/ui 1.0.0

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 (220) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE +21 -0
  3. package/README.md +33 -0
  4. package/dist/active-sessions-store-CeOmXgv5.d.ts +85 -0
  5. package/dist/artifact-pane-DvJyPWV4.d.ts +24 -0
  6. package/dist/auth.d.ts +74 -0
  7. package/dist/auth.js +15 -0
  8. package/dist/button-CMQuQEW_.d.ts +17 -0
  9. package/dist/chat.d.ts +232 -0
  10. package/dist/chat.js +30 -0
  11. package/dist/chunk-2NFQRQOD.js +1009 -0
  12. package/dist/chunk-2VH6PUXD.js +186 -0
  13. package/dist/chunk-34A66VBG.js +214 -0
  14. package/dist/chunk-3OI2QKFD.js +0 -0
  15. package/dist/chunk-4CLN43XT.js +45 -0
  16. package/dist/chunk-54SQQMMM.js +156 -0
  17. package/dist/chunk-5Z5ZYMOJ.js +0 -0
  18. package/dist/chunk-66BNMOVT.js +167 -0
  19. package/dist/chunk-6BGQA4BQ.js +0 -0
  20. package/dist/chunk-7UO2ZMRQ.js +133 -0
  21. package/dist/chunk-BX6AQMUS.js +183 -0
  22. package/dist/chunk-CD53GZOM.js +59 -0
  23. package/dist/chunk-CSAIKY36.js +54 -0
  24. package/dist/chunk-EEE55AVS.js +1201 -0
  25. package/dist/chunk-GYPQXTJU.js +230 -0
  26. package/dist/chunk-HFL6R6IF.js +37 -0
  27. package/dist/chunk-HJKCSXCH.js +737 -0
  28. package/dist/chunk-LISXUB4D.js +1222 -0
  29. package/dist/chunk-LQS34IGP.js +0 -0
  30. package/dist/chunk-MKTSMWVD.js +109 -0
  31. package/dist/chunk-NKDZ7GZE.js +192 -0
  32. package/dist/chunk-OEX7NZE3.js +321 -0
  33. package/dist/chunk-Q56BYXQF.js +61 -0
  34. package/dist/chunk-Q7EIIWTC.js +0 -0
  35. package/dist/chunk-REJESC5U.js +117 -0
  36. package/dist/chunk-RQGKSCEZ.js +0 -0
  37. package/dist/chunk-RQHJBTEU.js +10 -0
  38. package/dist/chunk-TMFOPHHN.js +299 -0
  39. package/dist/chunk-XGKULLYE.js +40 -0
  40. package/dist/chunk-XIHMJ7ZQ.js +614 -0
  41. package/dist/chunk-YJ2G3XO5.js +1048 -0
  42. package/dist/chunk-YNN4O57I.js +754 -0
  43. package/dist/code-block-DjXf8eOG.d.ts +19 -0
  44. package/dist/document-editor-pane-A5LT5H4N.js +12 -0
  45. package/dist/document-editor-pane-DyDEX_Zm.d.ts +124 -0
  46. package/dist/editor.d.ts +120 -0
  47. package/dist/editor.js +34 -0
  48. package/dist/files.d.ts +175 -0
  49. package/dist/files.js +20 -0
  50. package/dist/hooks.d.ts +56 -0
  51. package/dist/hooks.js +41 -0
  52. package/dist/index.d.ts +43 -0
  53. package/dist/index.js +446 -0
  54. package/dist/markdown.d.ts +15 -0
  55. package/dist/markdown.js +14 -0
  56. package/dist/message-BHWbxBtT.d.ts +15 -0
  57. package/dist/openui.d.ts +115 -0
  58. package/dist/openui.js +12 -0
  59. package/dist/parts-dj7AcUg0.d.ts +36 -0
  60. package/dist/primitives.d.ts +332 -0
  61. package/dist/primitives.js +191 -0
  62. package/dist/run-PfLmDAox.d.ts +41 -0
  63. package/dist/run.d.ts +69 -0
  64. package/dist/run.js +36 -0
  65. package/dist/sdk-hooks.d.ts +285 -0
  66. package/dist/sdk-hooks.js +31 -0
  67. package/dist/stores.d.ts +17 -0
  68. package/dist/stores.js +76 -0
  69. package/dist/tool-call-feed-Bs3MyQMT.d.ts +68 -0
  70. package/dist/tool-display-z4JcDmMQ.d.ts +32 -0
  71. package/dist/tool-previews.d.ts +48 -0
  72. package/dist/tool-previews.js +21 -0
  73. package/dist/types.d.ts +19 -0
  74. package/dist/types.js +1 -0
  75. package/dist/utils.d.ts +45 -0
  76. package/dist/utils.js +32 -0
  77. package/package.json +193 -0
  78. package/src/auth/auth.tsx +228 -0
  79. package/src/auth/index.ts +13 -0
  80. package/src/auth/login-layout.tsx +46 -0
  81. package/src/chat/agent-timeline.stories.tsx +429 -0
  82. package/src/chat/agent-timeline.tsx +360 -0
  83. package/src/chat/chat-container.tsx +486 -0
  84. package/src/chat/chat-input.stories.tsx +142 -0
  85. package/src/chat/chat-input.tsx +389 -0
  86. package/src/chat/chat-message.stories.tsx +237 -0
  87. package/src/chat/chat-message.tsx +129 -0
  88. package/src/chat/index.ts +18 -0
  89. package/src/chat/message-list.stories.tsx +336 -0
  90. package/src/chat/message-list.tsx +79 -0
  91. package/src/chat/thinking-indicator.stories.tsx +56 -0
  92. package/src/chat/thinking-indicator.tsx +30 -0
  93. package/src/chat/user-message.stories.tsx +92 -0
  94. package/src/chat/user-message.tsx +43 -0
  95. package/src/editor/document-editor-pane.tsx +351 -0
  96. package/src/editor/editor-provider.tsx +428 -0
  97. package/src/editor/editor-toolbar.tsx +130 -0
  98. package/src/editor/index.ts +31 -0
  99. package/src/editor/markdown-conversion.ts +21 -0
  100. package/src/editor/markdown-document-editor.tsx +137 -0
  101. package/src/editor/tiptap-editor.tsx +331 -0
  102. package/src/editor/use-editor.ts +221 -0
  103. package/src/files/file-artifact-pane.tsx +183 -0
  104. package/src/files/file-preview.tsx +342 -0
  105. package/src/files/file-tabs.tsx +71 -0
  106. package/src/files/file-tree.tsx +258 -0
  107. package/src/files/index.ts +17 -0
  108. package/src/files/rich-file-tree.stories.tsx +104 -0
  109. package/src/files/rich-file-tree.test.tsx +42 -0
  110. package/src/files/rich-file-tree.tsx +232 -0
  111. package/src/hooks/index.ts +10 -0
  112. package/src/hooks/use-auth.ts +153 -0
  113. package/src/hooks/use-auto-scroll.ts +59 -0
  114. package/src/hooks/use-dropdown-menu.ts +40 -0
  115. package/src/hooks/use-live-time.test.tsx +40 -0
  116. package/src/hooks/use-live-time.ts +27 -0
  117. package/src/hooks/use-realtime-session.ts +319 -0
  118. package/src/hooks/use-run-collapse-state.ts +25 -0
  119. package/src/hooks/use-run-groups.ts +111 -0
  120. package/src/hooks/use-sdk-session.ts +575 -0
  121. package/src/hooks/use-sse-stream.ts +475 -0
  122. package/src/hooks/use-tool-call-stream.ts +96 -0
  123. package/src/index.ts +14 -0
  124. package/src/lib/utils.ts +6 -0
  125. package/src/markdown/code-block.tsx +198 -0
  126. package/src/markdown/index.ts +2 -0
  127. package/src/markdown/markdown.stories.tsx +190 -0
  128. package/src/markdown/markdown.tsx +62 -0
  129. package/src/openui/index.ts +20 -0
  130. package/src/openui/openui-artifact-renderer.tsx +542 -0
  131. package/src/primitives/artifact-pane.tsx +91 -0
  132. package/src/primitives/avatar.stories.tsx +95 -0
  133. package/src/primitives/avatar.tsx +47 -0
  134. package/src/primitives/badge.stories.tsx +57 -0
  135. package/src/primitives/badge.tsx +97 -0
  136. package/src/primitives/button.stories.tsx +48 -0
  137. package/src/primitives/button.tsx +115 -0
  138. package/src/primitives/card.stories.tsx +53 -0
  139. package/src/primitives/card.tsx +98 -0
  140. package/src/primitives/code-block.stories.tsx +115 -0
  141. package/src/primitives/code-block.tsx +22 -0
  142. package/src/primitives/design-tokens.stories.tsx +162 -0
  143. package/src/primitives/dialog.stories.tsx +176 -0
  144. package/src/primitives/dialog.tsx +137 -0
  145. package/src/primitives/drop-zone.stories.tsx +123 -0
  146. package/src/primitives/drop-zone.tsx +131 -0
  147. package/src/primitives/dropdown-menu.stories.tsx +122 -0
  148. package/src/primitives/dropdown-menu.tsx +214 -0
  149. package/src/primitives/empty-state.stories.tsx +81 -0
  150. package/src/primitives/empty-state.tsx +40 -0
  151. package/src/primitives/index.ts +118 -0
  152. package/src/primitives/input.stories.tsx +113 -0
  153. package/src/primitives/input.tsx +136 -0
  154. package/src/primitives/label.stories.tsx +84 -0
  155. package/src/primitives/label.tsx +24 -0
  156. package/src/primitives/progress.stories.tsx +93 -0
  157. package/src/primitives/progress.tsx +50 -0
  158. package/src/primitives/segmented-control.test.tsx +328 -0
  159. package/src/primitives/segmented-control.tsx +154 -0
  160. package/src/primitives/select.stories.tsx +164 -0
  161. package/src/primitives/select.tsx +158 -0
  162. package/src/primitives/sidebar-drop-zone.stories.tsx +100 -0
  163. package/src/primitives/sidebar-drop-zone.tsx +149 -0
  164. package/src/primitives/skeleton.stories.tsx +79 -0
  165. package/src/primitives/skeleton.tsx +55 -0
  166. package/src/primitives/stat-card.stories.tsx +137 -0
  167. package/src/primitives/stat-card.tsx +97 -0
  168. package/src/primitives/switch.stories.tsx +85 -0
  169. package/src/primitives/switch.tsx +28 -0
  170. package/src/primitives/table.stories.tsx +170 -0
  171. package/src/primitives/table.tsx +116 -0
  172. package/src/primitives/tabs.stories.tsx +180 -0
  173. package/src/primitives/tabs.tsx +71 -0
  174. package/src/primitives/terminal-display.stories.tsx +191 -0
  175. package/src/primitives/terminal-display.tsx +189 -0
  176. package/src/primitives/theme-toggle.stories.tsx +32 -0
  177. package/src/primitives/theme-toggle.tsx +96 -0
  178. package/src/primitives/toast.stories.tsx +155 -0
  179. package/src/primitives/toast.tsx +190 -0
  180. package/src/primitives/upload-progress.stories.tsx +120 -0
  181. package/src/primitives/upload-progress.tsx +110 -0
  182. package/src/run/expanded-tool-detail.stories.tsx +182 -0
  183. package/src/run/expanded-tool-detail.tsx +186 -0
  184. package/src/run/index.ts +13 -0
  185. package/src/run/inline-thinking-item.stories.tsx +136 -0
  186. package/src/run/inline-thinking-item.tsx +120 -0
  187. package/src/run/inline-tool-item.stories.tsx +222 -0
  188. package/src/run/inline-tool-item.tsx +190 -0
  189. package/src/run/run-group.stories.tsx +322 -0
  190. package/src/run/run-group.tsx +569 -0
  191. package/src/run/run-item-primitives.tsx +17 -0
  192. package/src/run/tool-call-feed.stories.tsx +294 -0
  193. package/src/run/tool-call-feed.tsx +192 -0
  194. package/src/run/tool-call-step.stories.tsx +198 -0
  195. package/src/run/tool-call-step.tsx +240 -0
  196. package/src/sdk-hooks.ts +38 -0
  197. package/src/stores/active-sessions-store.ts +455 -0
  198. package/src/stores/chat-store.ts +43 -0
  199. package/src/stores/index.ts +2 -0
  200. package/src/tool-previews/command-preview.tsx +116 -0
  201. package/src/tool-previews/diff-preview.tsx +85 -0
  202. package/src/tool-previews/glob-results-preview.tsx +98 -0
  203. package/src/tool-previews/grep-results-preview.tsx +157 -0
  204. package/src/tool-previews/index.ts +22 -0
  205. package/src/tool-previews/preview-primitives.tsx +84 -0
  206. package/src/tool-previews/question-preview.tsx +101 -0
  207. package/src/tool-previews/web-search-preview.tsx +117 -0
  208. package/src/tool-previews/write-file-preview.tsx +80 -0
  209. package/src/types/branding.ts +11 -0
  210. package/src/types/index.ts +5 -0
  211. package/src/types/message.ts +13 -0
  212. package/src/types/parts.ts +51 -0
  213. package/src/types/run.ts +56 -0
  214. package/src/types/tool-display.ts +41 -0
  215. package/src/utils/copy-text.ts +30 -0
  216. package/src/utils/format.test.ts +43 -0
  217. package/src/utils/format.ts +56 -0
  218. package/src/utils/index.ts +10 -0
  219. package/src/utils/time-ago.ts +9 -0
  220. package/src/utils/tool-display.ts +238 -0
@@ -0,0 +1,222 @@
1
+ import type { Meta, StoryObj } from '@storybook/react'
2
+ import { InlineToolItem } from './inline-tool-item'
3
+ import type { ToolPart } from '../types/parts'
4
+
5
+ const NOW = Date.now()
6
+
7
+ // -- Fixtures --
8
+
9
+ const bashComplete: ToolPart = {
10
+ type: 'tool',
11
+ id: 'tool-bash-1',
12
+ tool: 'bash',
13
+ state: {
14
+ status: 'completed',
15
+ input: { command: 'pnpm test --run --reporter=verbose' },
16
+ output: `> test\n\n ✓ src/utils/format.test.ts (4)\n ✓ src/utils/tool-display.test.ts (11)\n\nTest Files 2 passed (2)\n Tests 15 passed (15)\n Duration 1.42s`,
17
+ time: { start: NOW - 3800, end: NOW - 2380 },
18
+ },
19
+ }
20
+
21
+ const bashRunning: ToolPart = {
22
+ type: 'tool',
23
+ id: 'tool-bash-running',
24
+ tool: 'bash',
25
+ state: {
26
+ status: 'running',
27
+ input: { command: 'pnpm build' },
28
+ time: { start: NOW - 4200 },
29
+ },
30
+ }
31
+
32
+ const bashError: ToolPart = {
33
+ type: 'tool',
34
+ id: 'tool-bash-error',
35
+ tool: 'bash',
36
+ state: {
37
+ status: 'error',
38
+ input: { command: 'pnpm test --run' },
39
+ error: 'Process exited with code 1\nTypeError: Cannot read properties of undefined (reading \'map\')\n at render (src/components/List.tsx:42:18)',
40
+ time: { start: NOW - 2100, end: NOW - 1600 },
41
+ },
42
+ }
43
+
44
+ const readFile: ToolPart = {
45
+ type: 'tool',
46
+ id: 'tool-read-1',
47
+ tool: 'read',
48
+ state: {
49
+ status: 'completed',
50
+ input: { file_path: '/home/user/project/src/components/Button.tsx' },
51
+ output: `import { cn } from '../lib/utils'\n\nexport function Button({ children, variant = 'default', ...props }) {\n return (\n <button className={cn('px-4 py-2 rounded', variant)} {...props}>\n {children}\n </button>\n )\n}`,
52
+ time: { start: NOW - 1200, end: NOW - 1140 },
53
+ },
54
+ }
55
+
56
+ const writeFile: ToolPart = {
57
+ type: 'tool',
58
+ id: 'tool-write-1',
59
+ tool: 'write',
60
+ state: {
61
+ status: 'completed',
62
+ input: {
63
+ file_path: '/home/user/project/src/utils/format.ts',
64
+ content: 'export function formatDuration(ms: number): string {\n if (ms < 1000) return `${ms}ms`\n return `${(ms / 1000).toFixed(1)}s`\n}',
65
+ },
66
+ output: 'File written successfully.',
67
+ time: { start: NOW - 900, end: NOW - 855 },
68
+ },
69
+ }
70
+
71
+ const webSearch: ToolPart = {
72
+ type: 'tool',
73
+ id: 'tool-web-1',
74
+ tool: 'web_search',
75
+ state: {
76
+ status: 'completed',
77
+ input: { query: 'vitest mock module typescript best practices 2024' },
78
+ output: JSON.stringify([
79
+ { title: 'Vitest Mocking Guide', url: 'https://vitest.dev/guide/mocking', snippet: 'Learn how to mock modules in Vitest...' },
80
+ { title: 'Testing Library Best Practices', url: 'https://testing-library.com/docs/', snippet: 'Avoid implementation details...' },
81
+ ]),
82
+ time: { start: NOW - 2800, end: NOW - 1900 },
83
+ },
84
+ }
85
+
86
+ const grepSearch: ToolPart = {
87
+ type: 'tool',
88
+ id: 'tool-grep-1',
89
+ tool: 'grep',
90
+ state: {
91
+ status: 'completed',
92
+ input: { pattern: 'useCallback', path: 'src/' },
93
+ output: 'src/hooks/useSession.ts:14: const handleMessage = useCallback((msg) => {\nsrc/hooks/useStream.ts:22: const send = useCallback(async (text) => {\nsrc/components/Chat.tsx:88: const onSubmit = useCallback(() => {',
94
+ time: { start: NOW - 600, end: NOW - 560 },
95
+ },
96
+ }
97
+
98
+ const editFile: ToolPart = {
99
+ type: 'tool',
100
+ id: 'tool-edit-1',
101
+ tool: 'edit',
102
+ state: {
103
+ status: 'completed',
104
+ input: {
105
+ file_path: '/home/user/project/src/utils/format.ts',
106
+ old_string: 'return `${(ms / 1000).toFixed(1)}s`',
107
+ new_string: 'if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`\n return `${Math.floor(ms / 60_000)}m ${Math.floor((ms % 60_000) / 1000)}s`',
108
+ },
109
+ output: 'Edit applied.',
110
+ time: { start: NOW - 450, end: NOW - 420 },
111
+ },
112
+ }
113
+
114
+ // --
115
+
116
+ const meta: Meta<typeof InlineToolItem> = {
117
+ title: 'Run/InlineToolItem',
118
+ component: InlineToolItem,
119
+ parameters: {
120
+ layout: 'fullscreen',
121
+ backgrounds: { default: 'dark' },
122
+ },
123
+ }
124
+
125
+ export default meta
126
+ type Story = StoryObj<typeof InlineToolItem>
127
+
128
+ export const BashComplete: Story = {
129
+ args: { part: bashComplete, groupPosition: 'single' },
130
+ render: (args) => (
131
+ <div className="p-6 max-w-2xl">
132
+ <InlineToolItem {...args} />
133
+ </div>
134
+ ),
135
+ }
136
+
137
+ export const BashRunning: Story = {
138
+ args: { part: bashRunning, groupPosition: 'single' },
139
+ render: (args) => (
140
+ <div className="p-6 max-w-2xl">
141
+ <InlineToolItem {...args} />
142
+ </div>
143
+ ),
144
+ }
145
+
146
+ export const BashError: Story = {
147
+ args: { part: bashError, groupPosition: 'single' },
148
+ render: (args) => (
149
+ <div className="p-6 max-w-2xl">
150
+ <InlineToolItem {...args} />
151
+ </div>
152
+ ),
153
+ }
154
+
155
+ export const ReadFile: Story = {
156
+ args: { part: readFile, groupPosition: 'single' },
157
+ render: (args) => (
158
+ <div className="p-6 max-w-2xl">
159
+ <InlineToolItem {...args} />
160
+ </div>
161
+ ),
162
+ }
163
+
164
+ export const WriteFile: Story = {
165
+ args: { part: writeFile, groupPosition: 'single' },
166
+ render: (args) => (
167
+ <div className="p-6 max-w-2xl">
168
+ <InlineToolItem {...args} />
169
+ </div>
170
+ ),
171
+ }
172
+
173
+ export const WebSearch: Story = {
174
+ args: { part: webSearch, groupPosition: 'single' },
175
+ render: (args) => (
176
+ <div className="p-6 max-w-2xl">
177
+ <InlineToolItem {...args} />
178
+ </div>
179
+ ),
180
+ }
181
+
182
+ export const GrepSearch: Story = {
183
+ args: { part: grepSearch, groupPosition: 'single' },
184
+ render: (args) => (
185
+ <div className="p-6 max-w-2xl">
186
+ <InlineToolItem {...args} />
187
+ </div>
188
+ ),
189
+ }
190
+
191
+ export const EditFile: Story = {
192
+ args: { part: editFile, groupPosition: 'single' },
193
+ render: (args) => (
194
+ <div className="p-6 max-w-2xl">
195
+ <InlineToolItem {...args} />
196
+ </div>
197
+ ),
198
+ }
199
+
200
+ /** Three consecutive tool calls showing group position rounding. */
201
+ export const GroupedSequence: Story = {
202
+ render: () => (
203
+ <div className="p-6 max-w-2xl space-y-0.5">
204
+ <InlineToolItem part={readFile} groupPosition="first" />
205
+ <InlineToolItem part={grepSearch} groupPosition="middle" />
206
+ <InlineToolItem part={editFile} groupPosition="last" />
207
+ </div>
208
+ ),
209
+ }
210
+
211
+ export const AllStates: Story = {
212
+ render: () => (
213
+ <div className="p-6 max-w-2xl space-y-3">
214
+ <p className="text-xs font-mono uppercase tracking-widest text-muted-foreground px-1">Completed</p>
215
+ <InlineToolItem part={bashComplete} groupPosition="single" />
216
+ <p className="text-xs font-mono uppercase tracking-widest text-muted-foreground px-1 pt-2">Running</p>
217
+ <InlineToolItem part={bashRunning} groupPosition="single" />
218
+ <p className="text-xs font-mono uppercase tracking-widest text-muted-foreground px-1 pt-2">Error</p>
219
+ <InlineToolItem part={bashError} groupPosition="single" />
220
+ </div>
221
+ ),
222
+ }
@@ -0,0 +1,190 @@
1
+ import { memo, useState, type ComponentType, type ReactNode } from "react";
2
+ import * as Collapsible from "@radix-ui/react-collapsible";
3
+ import {
4
+ Loader2,
5
+ CheckCircle2,
6
+ AlertCircle,
7
+ ChevronDown,
8
+ ChevronRight,
9
+ Terminal,
10
+ FileEdit,
11
+ FileSearch,
12
+ Search,
13
+ PencilLine,
14
+ Bot,
15
+ Globe,
16
+ ClipboardList,
17
+ Settings,
18
+ type LucideProps,
19
+ } from "lucide-react";
20
+ import { cn } from "../lib/utils";
21
+ import {
22
+ getToolDisplayMetadata,
23
+ getToolErrorText,
24
+ getToolCategory,
25
+ } from "../utils/tool-display";
26
+ import { formatDuration } from "../utils/format";
27
+ import type { ToolPart } from "../types/parts";
28
+ import type { ToolCategory } from "../types/run";
29
+ import type { CustomToolRenderer } from "../types/tool-display";
30
+ import { ExpandedToolDetail } from "./expanded-tool-detail";
31
+ import { LiveDuration } from "./run-item-primitives";
32
+
33
+ /** Map tool category to a lucide-react icon component. */
34
+ const TOOL_CATEGORY_ICON_MAP: Record<
35
+ ToolCategory,
36
+ ComponentType<LucideProps>
37
+ > = {
38
+ command: Terminal,
39
+ write: FileEdit,
40
+ read: FileSearch,
41
+ search: Search,
42
+ edit: PencilLine,
43
+ task: Bot,
44
+ web: Globe,
45
+ todo: ClipboardList,
46
+ other: Settings,
47
+ };
48
+
49
+ export interface InlineToolItemProps {
50
+ part: ToolPart;
51
+ renderToolDetail?: CustomToolRenderer;
52
+ groupPosition?: "single" | "first" | "middle" | "last";
53
+ className?: string;
54
+ contentClassName?: string;
55
+ actions?: ReactNode;
56
+ }
57
+
58
+ /**
59
+ * Compact single-line tool call display (32px height).
60
+ * Shows icon, title, description, duration, and status indicator.
61
+ * Expands on click to show ExpandedToolDetail.
62
+ */
63
+ export const InlineToolItem = memo(
64
+ ({
65
+ part,
66
+ renderToolDetail,
67
+ groupPosition = "single",
68
+ className,
69
+ contentClassName,
70
+ actions,
71
+ }: InlineToolItemProps) => {
72
+ const [open, setOpen] = useState(false);
73
+ const meta = getToolDisplayMetadata(part);
74
+ const { status } = part.state;
75
+ const errorText = getToolErrorText(part);
76
+
77
+ const isRunning = status === "pending" || status === "running";
78
+ const isError = status === "error";
79
+ const isComplete = status === "completed";
80
+
81
+ // Duration
82
+ const startTime = part.state.time?.start;
83
+ const endTime = part.state.time?.end;
84
+ const durationMs =
85
+ startTime && endTime ? endTime - startTime : undefined;
86
+
87
+ // Determine the default icon based on tool category
88
+ const category = getToolCategory(part.tool);
89
+ const DefaultIcon = TOOL_CATEGORY_ICON_MAP[category] ?? Settings;
90
+ const shapeClass = {
91
+ single: "rounded-[var(--radius-lg)]",
92
+ first: "rounded-t-[var(--radius-lg)] rounded-b-[var(--radius-sm)]",
93
+ middle: "rounded-[var(--radius-sm)]",
94
+ last: "rounded-t-[var(--radius-sm)] rounded-b-[var(--radius-lg)]",
95
+ }[groupPosition];
96
+
97
+ return (
98
+ <Collapsible.Root open={open} onOpenChange={setOpen}>
99
+ <div className="flex items-start gap-2">
100
+ <Collapsible.Trigger asChild>
101
+ <button
102
+ className={cn(
103
+ "w-full border text-left transition-colors",
104
+ "border-border bg-card hover:border-[var(--border-accent-hover)] hover:bg-accent/35",
105
+ open && "border-border bg-accent/30",
106
+ shapeClass,
107
+ className,
108
+ )}
109
+ >
110
+ <div className="flex items-center gap-2 px-2.5 py-1.5">
111
+ <div className={cn(
112
+ "shrink-0",
113
+ isRunning && "text-primary",
114
+ isComplete && "text-[var(--surface-success-text)]",
115
+ isError && "text-[var(--surface-danger-text)]",
116
+ !isRunning && !isComplete && !isError && "text-muted-foreground",
117
+ )}>
118
+ {isRunning ? (
119
+ <Loader2 className="h-3.5 w-3.5 animate-spin" />
120
+ ) : isComplete ? (
121
+ <CheckCircle2 className="h-3.5 w-3.5" />
122
+ ) : isError ? (
123
+ <AlertCircle className="h-3.5 w-3.5" />
124
+ ) : (
125
+ <DefaultIcon className="h-3.5 w-3.5" />
126
+ )}
127
+ </div>
128
+
129
+ <span className="truncate text-xs font-medium text-foreground">
130
+ {meta.title}
131
+ </span>
132
+ {meta.description ? (
133
+ <span className="hidden truncate text-xs font-mono text-muted-foreground sm:inline">
134
+ {meta.description}
135
+ </span>
136
+ ) : null}
137
+
138
+ <div className="ml-auto flex shrink-0 items-center gap-1.5">
139
+ {isRunning && startTime ? <LiveDuration startTime={startTime} /> : null}
140
+ {!isRunning && durationMs != null ? (
141
+ <span className="text-[10px] font-mono tabular-nums text-muted-foreground">
142
+ {formatDuration(durationMs)}
143
+ </span>
144
+ ) : null}
145
+ {isError ? (
146
+ <span className="rounded-full border border-[var(--surface-danger-border)] bg-[var(--surface-danger-bg)] px-1.5 py-px text-[10px] font-semibold uppercase text-[var(--surface-danger-text)]">
147
+ Failed
148
+ </span>
149
+ ) : null}
150
+ {isRunning ? (
151
+ <span className="rounded-full border border-[var(--border-accent)] bg-primary/10 px-1.5 py-px text-[10px] font-semibold uppercase text-primary">
152
+ Running
153
+ </span>
154
+ ) : null}
155
+ {open ? (
156
+ <ChevronDown className="h-3 w-3 text-muted-foreground" />
157
+ ) : (
158
+ <ChevronRight className="h-3 w-3 text-muted-foreground" />
159
+ )}
160
+ </div>
161
+ </div>
162
+
163
+ {errorText && !open ? (
164
+ <div className="border-t border-border px-3 py-2 text-xs text-red-200">
165
+ {errorText}
166
+ </div>
167
+ ) : null}
168
+ </button>
169
+ </Collapsible.Trigger>
170
+
171
+ {actions ? (
172
+ <div
173
+ className="flex shrink-0 flex-wrap items-center justify-end gap-1.5 pt-1"
174
+ onClick={(event) => event.stopPropagation()}
175
+ >
176
+ {actions}
177
+ </div>
178
+ ) : null}
179
+ </div>
180
+
181
+ <Collapsible.Content className="overflow-hidden data-[state=open]:animate-slideDown data-[state=closed]:animate-slideUp">
182
+ <div className={cn("mt-2 pl-4", contentClassName)}>
183
+ {renderToolDetail?.(part) ?? <ExpandedToolDetail part={part} />}
184
+ </div>
185
+ </Collapsible.Content>
186
+ </Collapsible.Root>
187
+ );
188
+ },
189
+ );
190
+ InlineToolItem.displayName = "InlineToolItem";