@schlessera/brain-ui-react 0.4.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 (311) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +70 -0
  3. package/dist/components/chat/ask-user-card.d.ts +12 -0
  4. package/dist/components/chat/ask-user-card.d.ts.map +1 -0
  5. package/dist/components/chat/ask-user-card.js +185 -0
  6. package/dist/components/chat/ask-user-card.js.map +1 -0
  7. package/dist/components/chat/brain-markdown.d.ts +35 -0
  8. package/dist/components/chat/brain-markdown.d.ts.map +1 -0
  9. package/dist/components/chat/brain-markdown.js +363 -0
  10. package/dist/components/chat/brain-markdown.js.map +1 -0
  11. package/dist/components/chat/chat-page.d.ts +2 -0
  12. package/dist/components/chat/chat-page.d.ts.map +1 -0
  13. package/dist/components/chat/chat-page.js +442 -0
  14. package/dist/components/chat/chat-page.js.map +1 -0
  15. package/dist/components/chat/command-palette.d.ts +5 -0
  16. package/dist/components/chat/command-palette.d.ts.map +1 -0
  17. package/dist/components/chat/command-palette.js +18 -0
  18. package/dist/components/chat/command-palette.js.map +1 -0
  19. package/dist/components/chat/markdown-content.d.ts +4 -0
  20. package/dist/components/chat/markdown-content.d.ts.map +1 -0
  21. package/dist/components/chat/markdown-content.js +6 -0
  22. package/dist/components/chat/markdown-content.js.map +1 -0
  23. package/dist/components/chat/message-bubble.d.ts +9 -0
  24. package/dist/components/chat/message-bubble.d.ts.map +1 -0
  25. package/dist/components/chat/message-bubble.js +129 -0
  26. package/dist/components/chat/message-bubble.js.map +1 -0
  27. package/dist/components/chat/message-share.d.ts +10 -0
  28. package/dist/components/chat/message-share.d.ts.map +1 -0
  29. package/dist/components/chat/message-share.js +65 -0
  30. package/dist/components/chat/message-share.js.map +1 -0
  31. package/dist/components/chat/renderers/claude-tools.d.ts +3 -0
  32. package/dist/components/chat/renderers/claude-tools.d.ts.map +1 -0
  33. package/dist/components/chat/renderers/claude-tools.js +38 -0
  34. package/dist/components/chat/renderers/claude-tools.js.map +1 -0
  35. package/dist/components/chat/renderers/generic.d.ts +9 -0
  36. package/dist/components/chat/renderers/generic.d.ts.map +1 -0
  37. package/dist/components/chat/renderers/generic.js +68 -0
  38. package/dist/components/chat/renderers/generic.js.map +1 -0
  39. package/dist/components/chat/renderers/index.d.ts +5 -0
  40. package/dist/components/chat/renderers/index.d.ts.map +1 -0
  41. package/dist/components/chat/renderers/index.js +17 -0
  42. package/dist/components/chat/renderers/index.js.map +1 -0
  43. package/dist/components/chat/risk-hints.d.ts +20 -0
  44. package/dist/components/chat/risk-hints.d.ts.map +1 -0
  45. package/dist/components/chat/risk-hints.js +81 -0
  46. package/dist/components/chat/risk-hints.js.map +1 -0
  47. package/dist/components/chat/session-drawer.d.ts +6 -0
  48. package/dist/components/chat/session-drawer.d.ts.map +1 -0
  49. package/dist/components/chat/session-drawer.js +96 -0
  50. package/dist/components/chat/session-drawer.js.map +1 -0
  51. package/dist/components/chat/share-block.d.ts +9 -0
  52. package/dist/components/chat/share-block.d.ts.map +1 -0
  53. package/dist/components/chat/share-block.js +70 -0
  54. package/dist/components/chat/share-block.js.map +1 -0
  55. package/dist/components/chat/tool-call-timeline.d.ts +8 -0
  56. package/dist/components/chat/tool-call-timeline.d.ts.map +1 -0
  57. package/dist/components/chat/tool-call-timeline.js +100 -0
  58. package/dist/components/chat/tool-call-timeline.js.map +1 -0
  59. package/dist/components/chat/tool-views.d.ts +116 -0
  60. package/dist/components/chat/tool-views.d.ts.map +1 -0
  61. package/dist/components/chat/tool-views.js +628 -0
  62. package/dist/components/chat/tool-views.js.map +1 -0
  63. package/dist/components/chat/welcome-state.d.ts +4 -0
  64. package/dist/components/chat/welcome-state.d.ts.map +1 -0
  65. package/dist/components/chat/welcome-state.js +10 -0
  66. package/dist/components/chat/welcome-state.js.map +1 -0
  67. package/dist/components/connectivity/connection-gate.d.ts +5 -0
  68. package/dist/components/connectivity/connection-gate.d.ts.map +1 -0
  69. package/dist/components/connectivity/connection-gate.js +42 -0
  70. package/dist/components/connectivity/connection-gate.js.map +1 -0
  71. package/dist/components/connectivity/login-screen.d.ts +9 -0
  72. package/dist/components/connectivity/login-screen.d.ts.map +1 -0
  73. package/dist/components/connectivity/login-screen.js +99 -0
  74. package/dist/components/connectivity/login-screen.js.map +1 -0
  75. package/dist/components/files/file-panel.d.ts +5 -0
  76. package/dist/components/files/file-panel.d.ts.map +1 -0
  77. package/dist/components/files/file-panel.js +46 -0
  78. package/dist/components/files/file-panel.js.map +1 -0
  79. package/dist/components/files/file-tree.d.ts +2 -0
  80. package/dist/components/files/file-tree.d.ts.map +1 -0
  81. package/dist/components/files/file-tree.js +63 -0
  82. package/dist/components/files/file-tree.js.map +1 -0
  83. package/dist/components/files/file-viewer-binary.d.ts +5 -0
  84. package/dist/components/files/file-viewer-binary.d.ts.map +1 -0
  85. package/dist/components/files/file-viewer-binary.js +20 -0
  86. package/dist/components/files/file-viewer-binary.js.map +1 -0
  87. package/dist/components/files/file-viewer-html.d.ts +4 -0
  88. package/dist/components/files/file-viewer-html.d.ts.map +1 -0
  89. package/dist/components/files/file-viewer-html.js +5 -0
  90. package/dist/components/files/file-viewer-html.js.map +1 -0
  91. package/dist/components/files/file-viewer-markdown.d.ts +4 -0
  92. package/dist/components/files/file-viewer-markdown.d.ts.map +1 -0
  93. package/dist/components/files/file-viewer-markdown.js +10 -0
  94. package/dist/components/files/file-viewer-markdown.js.map +1 -0
  95. package/dist/components/files/file-viewer-raw.d.ts +5 -0
  96. package/dist/components/files/file-viewer-raw.d.ts.map +1 -0
  97. package/dist/components/files/file-viewer-raw.js +15 -0
  98. package/dist/components/files/file-viewer-raw.js.map +1 -0
  99. package/dist/components/files/file-viewer.d.ts +2 -0
  100. package/dist/components/files/file-viewer.d.ts.map +1 -0
  101. package/dist/components/files/file-viewer.js +175 -0
  102. package/dist/components/files/file-viewer.js.map +1 -0
  103. package/dist/components/files/frontmatter-panel.d.ts +5 -0
  104. package/dist/components/files/frontmatter-panel.d.ts.map +1 -0
  105. package/dist/components/files/frontmatter-panel.js +12 -0
  106. package/dist/components/files/frontmatter-panel.js.map +1 -0
  107. package/dist/components/layout/app-shell.d.ts +5 -0
  108. package/dist/components/layout/app-shell.d.ts.map +1 -0
  109. package/dist/components/layout/app-shell.js +10 -0
  110. package/dist/components/layout/app-shell.js.map +1 -0
  111. package/dist/components/layout/mobile-tab-bar.d.ts +2 -0
  112. package/dist/components/layout/mobile-tab-bar.d.ts.map +1 -0
  113. package/dist/components/layout/mobile-tab-bar.js +16 -0
  114. package/dist/components/layout/mobile-tab-bar.js.map +1 -0
  115. package/dist/components/layout/side-rail.d.ts +2 -0
  116. package/dist/components/layout/side-rail.d.ts.map +1 -0
  117. package/dist/components/layout/side-rail.js +26 -0
  118. package/dist/components/layout/side-rail.js.map +1 -0
  119. package/dist/components/layout/slide-panel.d.ts +9 -0
  120. package/dist/components/layout/slide-panel.d.ts.map +1 -0
  121. package/dist/components/layout/slide-panel.js +18 -0
  122. package/dist/components/layout/slide-panel.js.map +1 -0
  123. package/dist/components/quick-actions/streaming-modal.d.ts +8 -0
  124. package/dist/components/quick-actions/streaming-modal.d.ts.map +1 -0
  125. package/dist/components/quick-actions/streaming-modal.js +92 -0
  126. package/dist/components/quick-actions/streaming-modal.js.map +1 -0
  127. package/dist/components/quick-actions/whatsup-modal.d.ts +5 -0
  128. package/dist/components/quick-actions/whatsup-modal.d.ts.map +1 -0
  129. package/dist/components/quick-actions/whatsup-modal.js +82 -0
  130. package/dist/components/quick-actions/whatsup-modal.js.map +1 -0
  131. package/dist/components/settings/passkey-panel.d.ts +10 -0
  132. package/dist/components/settings/passkey-panel.d.ts.map +1 -0
  133. package/dist/components/settings/passkey-panel.js +118 -0
  134. package/dist/components/settings/passkey-panel.js.map +1 -0
  135. package/dist/components/share/share-menu.d.ts +23 -0
  136. package/dist/components/share/share-menu.d.ts.map +1 -0
  137. package/dist/components/share/share-menu.js +58 -0
  138. package/dist/components/share/share-menu.js.map +1 -0
  139. package/dist/components/voice/dictation-sheet.d.ts +6 -0
  140. package/dist/components/voice/dictation-sheet.d.ts.map +1 -0
  141. package/dist/components/voice/dictation-sheet.js +91 -0
  142. package/dist/components/voice/dictation-sheet.js.map +1 -0
  143. package/dist/components/voice/mic-button.d.ts +6 -0
  144. package/dist/components/voice/mic-button.d.ts.map +1 -0
  145. package/dist/components/voice/mic-button.js +9 -0
  146. package/dist/components/voice/mic-button.js.map +1 -0
  147. package/dist/components/voice/review-card.d.ts +8 -0
  148. package/dist/components/voice/review-card.d.ts.map +1 -0
  149. package/dist/components/voice/review-card.js +8 -0
  150. package/dist/components/voice/review-card.js.map +1 -0
  151. package/dist/config.d.ts +19 -0
  152. package/dist/config.d.ts.map +1 -0
  153. package/dist/config.js +10 -0
  154. package/dist/config.js.map +1 -0
  155. package/dist/hooks/use-vpn-status.d.ts +2 -0
  156. package/dist/hooks/use-vpn-status.d.ts.map +1 -0
  157. package/dist/hooks/use-vpn-status.js +71 -0
  158. package/dist/hooks/use-vpn-status.js.map +1 -0
  159. package/dist/hooks/use-websocket.d.ts +8 -0
  160. package/dist/hooks/use-websocket.d.ts.map +1 -0
  161. package/dist/hooks/use-websocket.js +366 -0
  162. package/dist/hooks/use-websocket.js.map +1 -0
  163. package/dist/index.d.ts +25 -0
  164. package/dist/index.d.ts.map +1 -0
  165. package/dist/index.js +31 -0
  166. package/dist/index.js.map +1 -0
  167. package/dist/lib/api-client.d.ts +115 -0
  168. package/dist/lib/api-client.d.ts.map +1 -0
  169. package/dist/lib/api-client.js +70 -0
  170. package/dist/lib/api-client.js.map +1 -0
  171. package/dist/lib/backend.d.ts +7 -0
  172. package/dist/lib/backend.d.ts.map +1 -0
  173. package/dist/lib/backend.js +33 -0
  174. package/dist/lib/backend.js.map +1 -0
  175. package/dist/lib/frontmatter.d.ts +21 -0
  176. package/dist/lib/frontmatter.d.ts.map +1 -0
  177. package/dist/lib/frontmatter.js +88 -0
  178. package/dist/lib/frontmatter.js.map +1 -0
  179. package/dist/lib/image-attachments.d.ts +36 -0
  180. package/dist/lib/image-attachments.d.ts.map +1 -0
  181. package/dist/lib/image-attachments.js +175 -0
  182. package/dist/lib/image-attachments.js.map +1 -0
  183. package/dist/lib/passkeys.d.ts +18 -0
  184. package/dist/lib/passkeys.d.ts.map +1 -0
  185. package/dist/lib/passkeys.js +36 -0
  186. package/dist/lib/passkeys.js.map +1 -0
  187. package/dist/lib/share.d.ts +53 -0
  188. package/dist/lib/share.d.ts.map +1 -0
  189. package/dist/lib/share.js +144 -0
  190. package/dist/lib/share.js.map +1 -0
  191. package/dist/lib/strip-markdown.d.ts +10 -0
  192. package/dist/lib/strip-markdown.d.ts.map +1 -0
  193. package/dist/lib/strip-markdown.js +37 -0
  194. package/dist/lib/strip-markdown.js.map +1 -0
  195. package/dist/lib/tool-names.d.ts +17 -0
  196. package/dist/lib/tool-names.d.ts.map +1 -0
  197. package/dist/lib/tool-names.js +23 -0
  198. package/dist/lib/tool-names.js.map +1 -0
  199. package/dist/lib/utils.d.ts +3 -0
  200. package/dist/lib/utils.d.ts.map +1 -0
  201. package/dist/lib/utils.js +6 -0
  202. package/dist/lib/utils.js.map +1 -0
  203. package/dist/lib/ws-client.d.ts +26 -0
  204. package/dist/lib/ws-client.d.ts.map +1 -0
  205. package/dist/lib/ws-client.js +93 -0
  206. package/dist/lib/ws-client.js.map +1 -0
  207. package/dist/stores/chat-store.d.ts +131 -0
  208. package/dist/stores/chat-store.d.ts.map +1 -0
  209. package/dist/stores/chat-store.js +376 -0
  210. package/dist/stores/chat-store.js.map +1 -0
  211. package/dist/stores/connection-store.d.ts +11 -0
  212. package/dist/stores/connection-store.d.ts.map +1 -0
  213. package/dist/stores/connection-store.js +8 -0
  214. package/dist/stores/connection-store.js.map +1 -0
  215. package/dist/stores/file-store.d.ts +53 -0
  216. package/dist/stores/file-store.d.ts.map +1 -0
  217. package/dist/stores/file-store.js +284 -0
  218. package/dist/stores/file-store.js.map +1 -0
  219. package/dist/stores/provider-store.d.ts +24 -0
  220. package/dist/stores/provider-store.d.ts.map +1 -0
  221. package/dist/stores/provider-store.js +56 -0
  222. package/dist/stores/provider-store.js.map +1 -0
  223. package/dist/stores/ui-store.d.ts +21 -0
  224. package/dist/stores/ui-store.d.ts.map +1 -0
  225. package/dist/stores/ui-store.js +23 -0
  226. package/dist/stores/ui-store.js.map +1 -0
  227. package/dist/styles.css +2 -0
  228. package/dist/voice/asr-clients.d.ts +2 -0
  229. package/dist/voice/asr-clients.d.ts.map +1 -0
  230. package/dist/voice/asr-clients.js +33 -0
  231. package/dist/voice/asr-clients.js.map +1 -0
  232. package/dist/voice/asr-deepgram.d.ts +38 -0
  233. package/dist/voice/asr-deepgram.d.ts.map +1 -0
  234. package/dist/voice/asr-deepgram.js +307 -0
  235. package/dist/voice/asr-deepgram.js.map +1 -0
  236. package/dist/voice/asr-webspeech.d.ts +23 -0
  237. package/dist/voice/asr-webspeech.d.ts.map +1 -0
  238. package/dist/voice/asr-webspeech.js +93 -0
  239. package/dist/voice/asr-webspeech.js.map +1 -0
  240. package/dist/voice/use-dictation.d.ts +6 -0
  241. package/dist/voice/use-dictation.d.ts.map +1 -0
  242. package/dist/voice/use-dictation.js +174 -0
  243. package/dist/voice/use-dictation.js.map +1 -0
  244. package/dist/voice/voice-store.d.ts +26 -0
  245. package/dist/voice/voice-store.d.ts.map +1 -0
  246. package/dist/voice/voice-store.js +27 -0
  247. package/dist/voice/voice-store.js.map +1 -0
  248. package/package.json +71 -0
  249. package/src/components/chat/ask-user-card.tsx +451 -0
  250. package/src/components/chat/brain-markdown.tsx +547 -0
  251. package/src/components/chat/chat-page.tsx +820 -0
  252. package/src/components/chat/command-palette.tsx +68 -0
  253. package/src/components/chat/markdown-content.tsx +5 -0
  254. package/src/components/chat/message-bubble.tsx +362 -0
  255. package/src/components/chat/message-share.ts +78 -0
  256. package/src/components/chat/renderers/claude-tools.tsx +53 -0
  257. package/src/components/chat/renderers/generic.tsx +85 -0
  258. package/src/components/chat/renderers/index.ts +19 -0
  259. package/src/components/chat/risk-hints.ts +95 -0
  260. package/src/components/chat/session-drawer.tsx +213 -0
  261. package/src/components/chat/share-block.tsx +128 -0
  262. package/src/components/chat/tool-call-timeline.tsx +322 -0
  263. package/src/components/chat/tool-views.tsx +898 -0
  264. package/src/components/chat/welcome-state.tsx +105 -0
  265. package/src/components/connectivity/connection-gate.tsx +137 -0
  266. package/src/components/connectivity/login-screen.tsx +169 -0
  267. package/src/components/files/file-panel.tsx +116 -0
  268. package/src/components/files/file-tree.tsx +143 -0
  269. package/src/components/files/file-viewer-binary.tsx +63 -0
  270. package/src/components/files/file-viewer-html.tsx +10 -0
  271. package/src/components/files/file-viewer-markdown.tsx +14 -0
  272. package/src/components/files/file-viewer-raw.tsx +44 -0
  273. package/src/components/files/file-viewer.tsx +290 -0
  274. package/src/components/files/frontmatter-panel.tsx +78 -0
  275. package/src/components/layout/app-shell.tsx +25 -0
  276. package/src/components/layout/mobile-tab-bar.tsx +54 -0
  277. package/src/components/layout/side-rail.tsx +128 -0
  278. package/src/components/layout/slide-panel.tsx +64 -0
  279. package/src/components/quick-actions/streaming-modal.tsx +168 -0
  280. package/src/components/quick-actions/whatsup-modal.tsx +149 -0
  281. package/src/components/settings/passkey-panel.tsx +281 -0
  282. package/src/components/share/share-menu.tsx +121 -0
  283. package/src/components/voice/dictation-sheet.tsx +217 -0
  284. package/src/components/voice/mic-button.tsx +31 -0
  285. package/src/components/voice/review-card.tsx +66 -0
  286. package/src/config.ts +27 -0
  287. package/src/hooks/use-vpn-status.ts +75 -0
  288. package/src/hooks/use-websocket.ts +403 -0
  289. package/src/index.ts +46 -0
  290. package/src/lib/api-client.ts +173 -0
  291. package/src/lib/backend.ts +35 -0
  292. package/src/lib/frontmatter.ts +99 -0
  293. package/src/lib/image-attachments.ts +221 -0
  294. package/src/lib/passkeys.ts +50 -0
  295. package/src/lib/share.ts +167 -0
  296. package/src/lib/strip-markdown.ts +38 -0
  297. package/src/lib/tool-names.ts +27 -0
  298. package/src/lib/utils.ts +6 -0
  299. package/src/lib/ws-client.ts +109 -0
  300. package/src/stores/chat-store.ts +619 -0
  301. package/src/stores/connection-store.ts +23 -0
  302. package/src/stores/file-store.ts +344 -0
  303. package/src/stores/provider-store.ts +85 -0
  304. package/src/stores/ui-store.ts +48 -0
  305. package/src/styles.css +4 -0
  306. package/src/theme.css +425 -0
  307. package/src/voice/asr-clients.ts +45 -0
  308. package/src/voice/asr-deepgram.ts +325 -0
  309. package/src/voice/asr-webspeech.ts +135 -0
  310. package/src/voice/use-dictation.ts +187 -0
  311. package/src/voice/voice-store.ts +55 -0
@@ -0,0 +1,322 @@
1
+ import { useState, useEffect, useRef } from "react";
2
+ import {
3
+ Check,
4
+ X,
5
+ Copy,
6
+ Layers,
7
+ ChevronDown,
8
+ ChevronRight,
9
+ AlertTriangle,
10
+ FileText,
11
+ } from "lucide-react";
12
+ import { resolveToolRenderer } from "@schlessera/brain-ui-sdk/client";
13
+ import type { ToolCall } from "../../stores/chat-store.js";
14
+ import { cn } from "../../lib/utils.js";
15
+ import { motion, AnimatePresence } from "framer-motion";
16
+ import { getToolLabel, getTouchedFile, formatDuration } from "./tool-views.js";
17
+ import { registerBuiltinRenderers, GENERIC_RENDERER } from "./renderers/index.js";
18
+ import { riskHints } from "./risk-hints.js";
19
+
20
+ // Register the built-in renderer packs once. Registration is build-time; this
21
+ // runs on module load.
22
+ registerBuiltinRenderers();
23
+
24
+ // One backend is active per deployment (v1). The shipped default is the Claude
25
+ // backend; renderer resolution is backend-scoped so a future multi-backend
26
+ // build can thread the real id through here.
27
+ const BACKEND_ID = "claude";
28
+
29
+ export function ToolCallTimeline({
30
+ toolCalls,
31
+ onApproval,
32
+ live = false,
33
+ }: {
34
+ toolCalls: ToolCall[];
35
+ onApproval: (toolUseId: string, approved: boolean) => void;
36
+ /** Parent message still streaming — keep the timeline open while work runs. */
37
+ live?: boolean;
38
+ }) {
39
+ // Collapse the whole run to a summary row once the turn is over. History
40
+ // messages mount collapsed; a live timeline collapses when streaming ends.
41
+ const [collapsed, setCollapsed] = useState(!live);
42
+ const prevLive = useRef(live);
43
+ useEffect(() => {
44
+ if (prevLive.current && !live) setCollapsed(true);
45
+ prevLive.current = live;
46
+ }, [live]);
47
+
48
+ if (toolCalls.length === 0) return null;
49
+
50
+ const hasPending = toolCalls.some((t) => t.status === "pending_approval");
51
+ const effectiveCollapsed = collapsed && !hasPending && !live;
52
+
53
+ if (effectiveCollapsed) {
54
+ return (
55
+ <TimelineSummaryRow toolCalls={toolCalls} onExpand={() => setCollapsed(false)} />
56
+ );
57
+ }
58
+
59
+ return (
60
+ <div>
61
+ {!live && !hasPending && (
62
+ <button
63
+ type="button"
64
+ onClick={() => setCollapsed(true)}
65
+ className="mb-1 flex items-center gap-1.5 text-[11px] text-muted-foreground/50 transition-colors hover:text-muted-foreground"
66
+ >
67
+ <ChevronDown className="h-3 w-3" />
68
+ Hide steps
69
+ </button>
70
+ )}
71
+ <div className="relative ml-1 border-l-2 border-border/40 pl-4 space-y-1.5">
72
+ {toolCalls.map((tool) => (
73
+ <ToolCallEntry key={tool.id} toolCall={tool} onApproval={onApproval} />
74
+ ))}
75
+ </div>
76
+ </div>
77
+ );
78
+ }
79
+
80
+ function TimelineSummaryRow({
81
+ toolCalls,
82
+ onExpand,
83
+ }: {
84
+ toolCalls: ToolCall[];
85
+ onExpand: () => void;
86
+ }) {
87
+ const steps = toolCalls.length;
88
+ const files = new Set(toolCalls.map(getTouchedFile).filter(Boolean)).size;
89
+ const errors = toolCalls.filter((t) => t.isError).length;
90
+ const started = Math.min(...toolCalls.map((t) => t.startedAt ?? Infinity));
91
+ const ended = Math.max(...toolCalls.map((t) => t.endedAt ?? -Infinity));
92
+ const duration =
93
+ Number.isFinite(started) && ended > started
94
+ ? formatDuration(ended - started)
95
+ : null;
96
+
97
+ return (
98
+ <button
99
+ type="button"
100
+ onClick={onExpand}
101
+ className="flex items-center gap-2 rounded-md px-2 py-1.5 text-xs text-muted-foreground/70 transition-colors hover:text-foreground"
102
+ >
103
+ <Layers className="h-3.5 w-3.5 shrink-0" />
104
+ <span className="font-[family-name:var(--font-mono)]">
105
+ {steps} step{steps === 1 ? "" : "s"}
106
+ {files > 0 && ` · ${files} file${files === 1 ? "" : "s"} touched`}
107
+ {duration && ` · ${duration}`}
108
+ </span>
109
+ {errors > 0 && (
110
+ <span className="font-[family-name:var(--font-mono)] text-destructive">
111
+ · {errors} failed
112
+ </span>
113
+ )}
114
+ <ChevronRight className="h-3 w-3 shrink-0" />
115
+ </button>
116
+ );
117
+ }
118
+
119
+ function ToolCallEntry({
120
+ toolCall,
121
+ onApproval,
122
+ }: {
123
+ toolCall: ToolCall;
124
+ onApproval: (toolUseId: string, approved: boolean) => void;
125
+ }) {
126
+ const [expanded, setExpanded] = useState(
127
+ toolCall.status === "pending_approval"
128
+ );
129
+ const prevStatus = useRef(toolCall.status);
130
+ // Resolve the renderer for this tool (backend-scoped exact -> global exact ->
131
+ // shape-sniffing predicate). Falls back to the generic renderer.
132
+ const renderer = resolveToolRenderer(toolCall, BACKEND_ID) ?? GENERIC_RENDERER;
133
+ const Icon = renderer.icon ?? FileText;
134
+ const isPending = toolCall.status === "pending_approval";
135
+ const summary = renderer.summary?.(toolCall) ?? null;
136
+ const meta = renderer.meta?.(toolCall) ?? null;
137
+ const Input = renderer.Input;
138
+ const Output = renderer.Output;
139
+
140
+ useEffect(() => {
141
+ if (
142
+ toolCall.status === "pending_approval" &&
143
+ prevStatus.current !== "pending_approval"
144
+ ) {
145
+ setExpanded(true);
146
+ } else if (
147
+ prevStatus.current === "pending_approval" &&
148
+ toolCall.status !== "pending_approval"
149
+ ) {
150
+ setExpanded(false);
151
+ }
152
+ prevStatus.current = toolCall.status;
153
+ }, [toolCall.status]);
154
+
155
+ return (
156
+ <div className="relative">
157
+ {/* Timeline dot */}
158
+ <div
159
+ className={cn(
160
+ "absolute -left-[21px] top-2 h-2.5 w-2.5 rounded-full border-2 border-background",
161
+ toolCall.status === "streaming" && "bg-primary animate-pulse",
162
+ toolCall.status === "pending_approval" &&
163
+ "bg-primary",
164
+ toolCall.status === "approved" && "bg-accent",
165
+ toolCall.status === "denied" && "bg-destructive",
166
+ toolCall.status === "complete" &&
167
+ (toolCall.isError ? "bg-destructive" : "bg-accent"),
168
+ !["streaming", "pending_approval", "approved", "denied", "complete"].includes(toolCall.status) && "bg-muted-foreground"
169
+ )}
170
+ style={
171
+ toolCall.status === "pending_approval"
172
+ ? { animation: "breathe 2s ease-in-out infinite" }
173
+ : undefined
174
+ }
175
+ />
176
+
177
+ {/* Header row */}
178
+ <button
179
+ onClick={() => setExpanded(!expanded)}
180
+ className={cn(
181
+ "flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-xs transition-colors",
182
+ toolCall.isError
183
+ ? "text-destructive/80 hover:text-destructive"
184
+ : "text-muted-foreground hover:text-foreground"
185
+ )}
186
+ >
187
+ <Icon className="h-3.5 w-3.5 shrink-0" />
188
+ <span className="font-[family-name:var(--font-mono)] font-medium">
189
+ {getToolLabel(toolCall.name)}
190
+ </span>
191
+ {summary && (
192
+ <span
193
+ className={cn(
194
+ "truncate",
195
+ toolCall.isError ? "text-destructive/60" : "text-muted-foreground/70"
196
+ )}
197
+ >
198
+ {summary}
199
+ </span>
200
+ )}
201
+ <span className="ml-auto flex shrink-0 items-center gap-2">
202
+ {meta && (
203
+ <span className="font-[family-name:var(--font-mono)] text-[10px] text-muted-foreground/50">
204
+ {meta}
205
+ </span>
206
+ )}
207
+ <StatusIndicator status={toolCall.status} isError={toolCall.isError} />
208
+ </span>
209
+ </button>
210
+
211
+ {/* Expandable detail */}
212
+ <AnimatePresence>
213
+ {expanded && (
214
+ <motion.div
215
+ initial={{ height: 0, opacity: 0 }}
216
+ animate={{ height: "auto", opacity: 1 }}
217
+ exit={{ height: 0, opacity: 0 }}
218
+ transition={{ duration: 0.2, ease: "easeOut" }}
219
+ className="overflow-hidden"
220
+ >
221
+ <div
222
+ className={cn(
223
+ "mt-1 rounded-lg p-3 space-y-2",
224
+ isPending
225
+ ? "border-2 border-primary/40 bg-primary/5"
226
+ : "border border-border-subtle bg-surface"
227
+ )}
228
+ >
229
+ {/* Tool input */}
230
+ {Input && <Input tool={toolCall} />}
231
+
232
+ {/* Risk hints — advisory only, never blocks approval */}
233
+ {isPending && <RiskHints toolCall={toolCall} />}
234
+
235
+ {/* Approval buttons */}
236
+ {isPending && (
237
+ <div className="flex gap-2">
238
+ <button
239
+ onClick={() => onApproval(toolCall.id, true)}
240
+ className="flex items-center gap-1.5 rounded-lg bg-primary px-4 py-1.5 text-xs font-medium text-primary-foreground transition-colors hover:brightness-110"
241
+ >
242
+ <Check className="h-3 w-3" />
243
+ Allow
244
+ </button>
245
+ <button
246
+ onClick={() => onApproval(toolCall.id, false)}
247
+ className="flex items-center gap-1.5 rounded-lg border border-destructive/30 px-4 py-1.5 text-xs font-medium text-destructive transition-colors hover:bg-destructive/10"
248
+ >
249
+ <X className="h-3 w-3" />
250
+ Deny
251
+ </button>
252
+ </div>
253
+ )}
254
+
255
+ {/* Tool output */}
256
+ {toolCall.output && Output && (
257
+ <div className="relative">
258
+ <button
259
+ onClick={() =>
260
+ navigator.clipboard.writeText(toolCall.output!)
261
+ }
262
+ className="absolute right-1 top-1 z-10 rounded p-1 text-muted-foreground transition-colors hover:bg-surface-raised hover:text-foreground"
263
+ title="Copy output"
264
+ >
265
+ <Copy className="h-3 w-3" />
266
+ </button>
267
+ <Output tool={toolCall} />
268
+ </div>
269
+ )}
270
+ </div>
271
+ </motion.div>
272
+ )}
273
+ </AnimatePresence>
274
+ </div>
275
+ );
276
+ }
277
+
278
+ function RiskHints({ toolCall }: { toolCall: ToolCall }) {
279
+ const hints = riskHints(toolCall);
280
+ if (hints.length === 0) return null;
281
+ return (
282
+ <div className="space-y-0.5 text-[11px] text-amber-400">
283
+ {hints.map((hint) => (
284
+ <div key={hint} className="flex items-center gap-1.5">
285
+ <AlertTriangle className="h-3 w-3 shrink-0" />
286
+ <span>{hint}</span>
287
+ </div>
288
+ ))}
289
+ </div>
290
+ );
291
+ }
292
+
293
+ function StatusIndicator({
294
+ status,
295
+ isError,
296
+ }: {
297
+ status: ToolCall["status"];
298
+ isError?: boolean;
299
+ }) {
300
+ switch (status) {
301
+ case "streaming":
302
+ return (
303
+ <span className="inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-primary" />
304
+ );
305
+ case "pending_approval":
306
+ return (
307
+ <span className="rounded bg-primary/20 px-1.5 py-0.5 text-[10px] font-medium text-primary">
308
+ Approval needed
309
+ </span>
310
+ );
311
+ case "approved":
312
+ return <Check className="h-3 w-3 text-accent" />;
313
+ case "denied":
314
+ return <X className="h-3 w-3 text-destructive" />;
315
+ case "complete":
316
+ return isError ? (
317
+ <X className="h-3 w-3 text-destructive" />
318
+ ) : (
319
+ <Check className="h-3 w-3 text-muted-foreground/50" />
320
+ );
321
+ }
322
+ }