@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,628 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { Terminal, FileEdit, FilePlus, FileSearch, FolderSearch, Search, Eye, Globe, Bot, Wand2, Braces, NotebookPen, MapPin, ChevronRight, } from "lucide-react";
4
+ import { cn } from "../../lib/utils.js";
5
+ import { linkifyPaths, FileLink } from "./brain-markdown.js";
6
+ import { MarkdownContent } from "./markdown-content.js";
7
+ import { isInternalRepoPath } from "../../stores/file-store.js";
8
+ import { GET_LOCATION_TOOL_NAME, normalizeToolName } from "../../lib/tool-names.js";
9
+ // ============================================================
10
+ // Shared helpers
11
+ // ============================================================
12
+ function str(v) {
13
+ return typeof v === "string" && v.length > 0 ? v : null;
14
+ }
15
+ function num(v) {
16
+ return typeof v === "number" && Number.isFinite(v) ? v : null;
17
+ }
18
+ function basename(p) {
19
+ return p.replace(/^.*\//, "");
20
+ }
21
+ /**
22
+ * Tool inputs carry absolute paths (`/data/brain/notes/foo.md`); the file
23
+ * panel wants repo-relative ones. Strip everything up to the brain repo root
24
+ * and verify the result actually looks like a repo path.
25
+ */
26
+ export function toRepoRelative(path) {
27
+ // isInternalRepoPath is a `href is string` guard; feed it a widened local
28
+ // so the original string parameter is not narrowed to `never`.
29
+ const candidate = path;
30
+ if (isInternalRepoPath(candidate))
31
+ return candidate;
32
+ const idx = path.indexOf("/brain/");
33
+ if (idx >= 0) {
34
+ const rel = path.slice(idx + "/brain/".length);
35
+ if (isInternalRepoPath(rel))
36
+ return rel;
37
+ }
38
+ return null;
39
+ }
40
+ /** Path as a clickable file link when it resolves inside the brain repo. */
41
+ function fileLabel(path) {
42
+ const rel = toRepoRelative(path);
43
+ if (rel)
44
+ return _jsx(FileLink, { path: rel, children: rel });
45
+ return _jsx("span", { title: path, children: path });
46
+ }
47
+ export function formatDuration(ms) {
48
+ if (ms < 1000)
49
+ return `${(ms / 1000).toFixed(1)}s`;
50
+ if (ms < 60_000)
51
+ return `${(ms / 1000).toFixed(1)}s`;
52
+ const m = Math.floor(ms / 60_000);
53
+ const s = Math.round((ms % 60_000) / 1000);
54
+ return `${m}m ${s}s`;
55
+ }
56
+ export function countLines(text) {
57
+ let n = 1;
58
+ for (let i = 0; i < text.length; i++)
59
+ if (text[i] === "\n")
60
+ n++;
61
+ return n;
62
+ }
63
+ // ============================================================
64
+ // Header helpers: icon, summary, meta badge
65
+ // ============================================================
66
+ const toolIcons = {
67
+ Bash: Terminal,
68
+ Edit: FileEdit,
69
+ Write: FilePlus,
70
+ NotebookEdit: NotebookPen,
71
+ Read: Eye,
72
+ Grep: FileSearch,
73
+ Glob: FolderSearch,
74
+ WebSearch: Search,
75
+ WebFetch: Globe,
76
+ Agent: Bot,
77
+ Skill: Wand2,
78
+ LSP: Braces,
79
+ [GET_LOCATION_TOOL_NAME]: MapPin,
80
+ };
81
+ export function getToolIcon(name) {
82
+ return toolIcons[normalizeToolName(name)] || Terminal;
83
+ }
84
+ /**
85
+ * Display name for a tool. In-process MCP tools stream as
86
+ * `mcp__<server>__<tool>`; show just the bare tool name in the timeline.
87
+ */
88
+ export function getToolLabel(name) {
89
+ if (name.startsWith("mcp__")) {
90
+ const parts = name.split("__");
91
+ return parts[parts.length - 1] || name;
92
+ }
93
+ return name;
94
+ }
95
+ /** Extract a short inline summary from tool input for at-a-glance context. */
96
+ export function getToolSummary(tool) {
97
+ const inp = tool.input;
98
+ if (!inp)
99
+ return null;
100
+ switch (tool.name) {
101
+ case "Agent":
102
+ return str(inp.description);
103
+ case "Bash":
104
+ // The human-phrased description reads better than a truncated command
105
+ return str(inp.description) ?? str(inp.command)?.slice(0, 80) ?? null;
106
+ case "Read": {
107
+ const path = str(inp.file_path);
108
+ if (!path)
109
+ return null;
110
+ const offset = num(inp.offset);
111
+ const limit = num(inp.limit);
112
+ if (offset !== null && limit !== null) {
113
+ return `${basename(path)} · ${offset}–${offset + limit}`;
114
+ }
115
+ return basename(path);
116
+ }
117
+ case "Edit":
118
+ case "Write":
119
+ return str(inp.file_path) ? basename(inp.file_path) : null;
120
+ case "NotebookEdit":
121
+ return str(inp.notebook_path) ? basename(inp.notebook_path) : null;
122
+ case "Glob":
123
+ case "Grep":
124
+ return str(inp.pattern);
125
+ case "WebSearch":
126
+ return str(inp.query);
127
+ case "WebFetch": {
128
+ const url = str(inp.url);
129
+ if (!url)
130
+ return null;
131
+ try {
132
+ const u = new URL(url);
133
+ return `${u.host}${u.pathname === "/" ? "" : u.pathname}`;
134
+ }
135
+ catch {
136
+ return url;
137
+ }
138
+ }
139
+ case "Skill":
140
+ return [str(inp.skill), str(inp.args)].filter(Boolean).join(" ") || null;
141
+ case "LSP": {
142
+ const op = str(inp.operation) ?? str(inp.method) ?? str(inp.action);
143
+ const path = str(inp.file_path);
144
+ return [op, path && basename(path)].filter(Boolean).join(" · ") || null;
145
+ }
146
+ default:
147
+ return null;
148
+ }
149
+ }
150
+ /** Right-side header meta: output size + wall-clock duration. */
151
+ export function getOutputMeta(tool) {
152
+ const parts = [];
153
+ if (tool.output && !tool.isError) {
154
+ const lines = countLines(tool.output.trimEnd());
155
+ switch (tool.name) {
156
+ case "Grep":
157
+ parts.push(/no matches/i.test(tool.output) ? "no matches" : `${lines} matches`);
158
+ break;
159
+ case "Glob":
160
+ parts.push(/no files/i.test(tool.output)
161
+ ? "no files"
162
+ : `${lines} file${lines === 1 ? "" : "s"}`);
163
+ break;
164
+ case "Read":
165
+ parts.push(`${lines} lines`);
166
+ break;
167
+ default:
168
+ if (lines > 5)
169
+ parts.push(`${lines} lines`);
170
+ }
171
+ }
172
+ if (tool.startedAt && tool.endedAt && tool.endedAt > tool.startedAt) {
173
+ parts.push(formatDuration(tool.endedAt - tool.startedAt));
174
+ }
175
+ return parts.length ? parts.join(" · ") : null;
176
+ }
177
+ /** File a tool writes to — used for the "N files touched" group summary. */
178
+ export function getTouchedFile(tool) {
179
+ if (tool.name === "Edit" || tool.name === "Write") {
180
+ return str(tool.input?.file_path);
181
+ }
182
+ if (tool.name === "NotebookEdit") {
183
+ return str(tool.input?.notebook_path);
184
+ }
185
+ return null;
186
+ }
187
+ // ============================================================
188
+ // Input views
189
+ // ============================================================
190
+ export function ToolInputView({ tool }) {
191
+ // Input JSON still streaming in — a partial JSON dump is noise
192
+ if (tool.status === "streaming") {
193
+ return (_jsxs("div", { className: "flex items-center gap-2 rounded-md bg-background/60 p-2 text-[11px] text-muted-foreground/60", children: [_jsx("span", { className: "inline-block h-1.5 w-1.5 rounded-full bg-primary", style: { animation: "breathe 1.5s ease-in-out infinite" } }), "Preparing input\u2026"] }));
194
+ }
195
+ switch (tool.name) {
196
+ case "Edit":
197
+ if (str(tool.input?.old_string) !== null && str(tool.input?.new_string) !== null) {
198
+ return _jsx(EditDiffView, { tool: tool });
199
+ }
200
+ break;
201
+ case "Write":
202
+ if (str(tool.input?.content) !== null) {
203
+ return _jsx(WriteFileView, { tool: tool });
204
+ }
205
+ break;
206
+ case "Bash":
207
+ if (str(tool.input?.command) !== null) {
208
+ return _jsx(BashCommandView, { tool: tool });
209
+ }
210
+ break;
211
+ case "Read":
212
+ if (str(tool.input?.file_path) !== null) {
213
+ return _jsx(ReadInputView, { tool: tool });
214
+ }
215
+ break;
216
+ }
217
+ return _jsx(KeyValueView, { tool: tool });
218
+ }
219
+ function PathHeader({ path, badge }) {
220
+ return (_jsxs("div", { className: "flex items-center gap-2 border-b border-border-subtle bg-surface-raised/50 px-2 py-1.5 font-[family-name:var(--font-mono)] text-[11px]", children: [_jsx("span", { className: "min-w-0 truncate text-foreground/80", children: path ? fileLabel(path) : "unknown file" }), badge && (_jsx("span", { className: "ml-auto shrink-0 rounded bg-primary/15 px-1.5 py-0.5 text-[10px] text-primary", children: badge }))] }));
221
+ }
222
+ /**
223
+ * Hand-rolled LCS diff (standard DP + forward walk) so we avoid pulling in the
224
+ * `diff` package. Returns ops in source order: `same` where the sequences
225
+ * agree, `del` for items only in `a`, `ins` for items only in `b`.
226
+ */
227
+ function diffSeq(a, b, eq = (x, y) => x === y) {
228
+ const m = a.length;
229
+ const n = b.length;
230
+ // dp[i][j] = length of the LCS of a[i:] and b[j:]
231
+ const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
232
+ for (let i = m - 1; i >= 0; i--) {
233
+ for (let j = n - 1; j >= 0; j--) {
234
+ dp[i][j] = eq(a[i], b[j])
235
+ ? dp[i + 1][j + 1] + 1
236
+ : Math.max(dp[i + 1][j], dp[i][j + 1]);
237
+ }
238
+ }
239
+ const ops = [];
240
+ let i = 0;
241
+ let j = 0;
242
+ while (i < m && j < n) {
243
+ if (eq(a[i], b[j])) {
244
+ ops.push({ kind: "same", value: a[i] });
245
+ i++;
246
+ j++;
247
+ }
248
+ else if (dp[i + 1][j] >= dp[i][j + 1]) {
249
+ ops.push({ kind: "del", value: a[i] });
250
+ i++;
251
+ }
252
+ else {
253
+ ops.push({ kind: "ins", value: b[j] });
254
+ j++;
255
+ }
256
+ }
257
+ while (i < m)
258
+ ops.push({ kind: "del", value: a[i++] });
259
+ while (j < n)
260
+ ops.push({ kind: "ins", value: b[j++] });
261
+ return ops;
262
+ }
263
+ /**
264
+ * Token-level LCS for a single deleted/inserted line pair. Splitting on
265
+ * `/(\s+)/` keeps the whitespace as its own tokens so we can reassemble the
266
+ * line exactly. Unchanged tokens are shared between both sides.
267
+ */
268
+ function wordDiff(oldLine, newLine) {
269
+ const a = oldLine.split(/(\s+)/);
270
+ const b = newLine.split(/(\s+)/);
271
+ // Token-level LCS is O(words²); a pathological minified line would be slow.
272
+ // Above the cap, skip word refinement and flag the whole line as changed.
273
+ if (a.length * b.length > 10_000) {
274
+ return {
275
+ del: [{ text: oldLine, changed: true }],
276
+ ins: [{ text: newLine, changed: true }],
277
+ };
278
+ }
279
+ const ops = diffSeq(a, b);
280
+ const del = [];
281
+ const ins = [];
282
+ for (const op of ops) {
283
+ if (op.kind === "same") {
284
+ del.push({ text: op.value, changed: false });
285
+ ins.push({ text: op.value, changed: false });
286
+ }
287
+ else if (op.kind === "del") {
288
+ del.push({ text: op.value, changed: true });
289
+ }
290
+ else {
291
+ ins.push({ text: op.value, changed: true });
292
+ }
293
+ }
294
+ return { del, ins };
295
+ }
296
+ /**
297
+ * Merge `old_string`/`new_string` into a single diff: unchanged lines appear
298
+ * once as neutral, deletions/insertions keep the red/green treatment. A change
299
+ * block that is exactly one deleted line against one inserted line gets
300
+ * word-level highlighting.
301
+ */
302
+ export function computeDiffRows(oldStr, newStr) {
303
+ const oldLines = oldStr.split("\n");
304
+ const newLines = newStr.split("\n");
305
+ // Line-level LCS is O(m×n) in time and memory; a huge Edit (thousands of
306
+ // lines) would build a massive DP matrix and hang the UI. Above the cap, fall
307
+ // back to a naive "all old removed, all new added" diff.
308
+ if (oldLines.length * newLines.length > 250_000) {
309
+ return [
310
+ ...oldLines.map((line) => ({ kind: "del", line, tokens: null })),
311
+ ...newLines.map((line) => ({ kind: "ins", line, tokens: null })),
312
+ ];
313
+ }
314
+ const ops = diffSeq(oldLines, newLines);
315
+ const rows = [];
316
+ let idx = 0;
317
+ while (idx < ops.length) {
318
+ if (ops[idx].kind === "same") {
319
+ rows.push({ kind: "same", line: ops[idx].value, tokens: null });
320
+ idx++;
321
+ continue;
322
+ }
323
+ // Collect a maximal run of changes, grouping deletes before inserts.
324
+ const dels = [];
325
+ const inss = [];
326
+ while (idx < ops.length && ops[idx].kind !== "same") {
327
+ if (ops[idx].kind === "del")
328
+ dels.push(ops[idx].value);
329
+ else
330
+ inss.push(ops[idx].value);
331
+ idx++;
332
+ }
333
+ if (dels.length === 1 && inss.length === 1) {
334
+ const { del, ins } = wordDiff(dels[0], inss[0]);
335
+ rows.push({ kind: "del", line: dels[0], tokens: del });
336
+ rows.push({ kind: "ins", line: inss[0], tokens: ins });
337
+ }
338
+ else {
339
+ for (const d of dels)
340
+ rows.push({ kind: "del", line: d, tokens: null });
341
+ for (const s of inss)
342
+ rows.push({ kind: "ins", line: s, tokens: null });
343
+ }
344
+ }
345
+ return rows;
346
+ }
347
+ /** Render a diff line, emphasizing changed word tokens when refined. */
348
+ function DiffLine({ tokens, line, changedClass }) {
349
+ if (!tokens)
350
+ return _jsx(_Fragment, { children: line });
351
+ return (_jsx(_Fragment, { children: tokens.map((t, i) => t.changed ? (_jsx("span", { className: cn("rounded-sm", changedClass), children: t.text }, i)) : (_jsx("span", { children: t.text }, i))) }));
352
+ }
353
+ export function EditDiffView({ tool }) {
354
+ const rows = computeDiffRows(str(tool.input.old_string) ?? "", str(tool.input.new_string) ?? "");
355
+ return (_jsxs("div", { className: "overflow-hidden rounded-md border border-border-subtle", children: [_jsx(PathHeader, { path: str(tool.input.file_path), badge: tool.input.replace_all ? "replace all" : null }), _jsx("div", { className: "max-h-64 overflow-y-auto bg-background/60 py-1 font-[family-name:var(--font-mono)] text-[11px] leading-relaxed", children: rows.map((row, i) => {
356
+ if (row.kind === "same") {
357
+ return (_jsxs("div", { className: "flex", children: [_jsx("span", { className: "w-5 shrink-0 select-none text-center text-muted-foreground/30", children: " " }), _jsx("span", { className: "min-w-0 whitespace-pre-wrap break-all pr-2 text-muted-foreground/70", children: row.line })] }, `s${i}`));
358
+ }
359
+ if (row.kind === "del") {
360
+ return (_jsxs("div", { className: "flex bg-red-500/10", children: [_jsx("span", { className: "w-5 shrink-0 select-none text-center text-red-400/70", children: "-" }), _jsx("span", { className: "min-w-0 whitespace-pre-wrap break-all pr-2 text-red-200/90", children: _jsx(DiffLine, { tokens: row.tokens, line: row.line, changedClass: "bg-red-500/30" }) })] }, `d${i}`));
361
+ }
362
+ return (_jsxs("div", { className: "flex bg-emerald-500/10", children: [_jsx("span", { className: "w-5 shrink-0 select-none text-center text-emerald-400/70", children: "+" }), _jsx("span", { className: "min-w-0 whitespace-pre-wrap break-all pr-2 text-emerald-200/90", children: _jsx(DiffLine, { tokens: row.tokens, line: row.line, changedClass: "bg-emerald-500/30" }) })] }, `i${i}`));
363
+ }) })] }));
364
+ }
365
+ /** Longest backtick run + 1 so the fence survives content containing fences. */
366
+ export function fenceFor(content) {
367
+ const runs = content.match(/`+/g);
368
+ const longest = runs ? Math.max(...runs.map((r) => r.length)) : 0;
369
+ return "`".repeat(Math.max(3, longest + 1));
370
+ }
371
+ export function WriteFileView({ tool }) {
372
+ const content = str(tool.input.content) ?? "";
373
+ const path = str(tool.input.file_path);
374
+ const ext = path?.match(/\.([a-z0-9]{1,8})$/i)?.[1] ?? "";
375
+ const fence = fenceFor(content);
376
+ return (_jsxs("div", { className: "overflow-hidden rounded-md border border-border-subtle", children: [_jsx(PathHeader, { path: path, badge: `${countLines(content)} lines` }), _jsx("div", { className: "max-h-64 overflow-y-auto [&_pre]:!my-0 [&_pre]:rounded-none [&_pre]:border-0 [&_pre]:bg-background/60 [&_pre]:p-2 [&_pre]:text-[11px]", children: _jsx(MarkdownContent, { content: `${fence}${ext}\n${content}\n${fence}` }) })] }));
377
+ }
378
+ export function BashCommandView({ tool }) {
379
+ const command = str(tool.input.command) ?? "";
380
+ const flags = [];
381
+ if (tool.input.run_in_background)
382
+ flags.push("background");
383
+ const timeout = num(tool.input.timeout);
384
+ if (timeout)
385
+ flags.push(`timeout ${Math.round(timeout / 1000)}s`);
386
+ if (tool.input.dangerouslyDisableSandbox)
387
+ flags.push("sandbox disabled");
388
+ return (_jsxs("div", { className: "rounded-md bg-background/60 p-2 font-[family-name:var(--font-mono)] text-[11px] leading-relaxed", children: [_jsxs("div", { className: "flex gap-1.5", children: [_jsx("span", { className: "select-none text-primary", children: "$" }), _jsx("span", { className: "min-w-0 whitespace-pre-wrap break-all text-foreground/90", children: command })] }), flags.length > 0 && (_jsx("div", { className: "mt-1.5 flex flex-wrap gap-1", children: flags.map((f) => (_jsx("span", { className: cn("rounded px-1.5 py-0.5 text-[10px]", f === "sandbox disabled"
389
+ ? "bg-destructive/15 text-destructive"
390
+ : "bg-surface-raised text-muted-foreground"), children: f }, f))) }))] }));
391
+ }
392
+ function ReadInputView({ tool }) {
393
+ const path = str(tool.input.file_path) ?? "";
394
+ const offset = num(tool.input.offset);
395
+ const limit = num(tool.input.limit);
396
+ const range = offset !== null && limit !== null
397
+ ? `lines ${offset}–${offset + limit}`
398
+ : offset !== null
399
+ ? `from line ${offset}`
400
+ : "full file";
401
+ return (_jsxs("div", { className: "flex items-center gap-2 rounded-md bg-background/60 px-2 py-1.5 font-[family-name:var(--font-mono)] text-[11px]", children: [_jsx("span", { className: "min-w-0 truncate text-foreground/80", children: fileLabel(path) }), _jsx("span", { className: "ml-auto shrink-0 text-muted-foreground/60", children: range })] }));
402
+ }
403
+ /** Generic structured input: one row per field beats raw JSON for scanning. */
404
+ export function KeyValueView({ tool }) {
405
+ const entries = Object.entries(tool.input ?? {});
406
+ if (entries.length === 0) {
407
+ return (_jsx("pre", { className: "overflow-x-auto rounded-md bg-background/60 p-2 font-[family-name:var(--font-mono)] text-[11px] leading-relaxed text-muted-foreground", children: tool.inputJson || "{}" }));
408
+ }
409
+ return (_jsx("div", { className: "space-y-1 rounded-md bg-background/60 p-2", children: entries.map(([key, value]) => (_jsxs("div", { className: "flex gap-2 font-[family-name:var(--font-mono)] text-[11px] leading-relaxed", children: [_jsxs("span", { className: "shrink-0 text-muted-foreground/50", children: [key, ":"] }), _jsx("span", { className: "max-h-40 min-w-0 overflow-y-auto whitespace-pre-wrap break-words text-muted-foreground", children: typeof value === "string"
410
+ ? linkifyPaths(value)
411
+ : JSON.stringify(value, null, 1) })] }, key))) }));
412
+ }
413
+ // ============================================================
414
+ // Output views
415
+ // ============================================================
416
+ export function ToolOutputView({ tool }) {
417
+ const output = tool.output;
418
+ if (!output)
419
+ return null;
420
+ if (tool.isError) {
421
+ return _jsx(ClampedPre, { text: output, isError: true });
422
+ }
423
+ switch (tool.name) {
424
+ case "Agent":
425
+ case "WebFetch":
426
+ // These outputs are prose (subagent reports, page summaries)
427
+ return (_jsx(ClampedBox, { lineLabel: `${countLines(output)} lines`, children: _jsx("div", { className: "rounded-md bg-background/60 p-3 text-[13px]", children: _jsx(MarkdownContent, { content: output }) }) }));
428
+ case "Grep":
429
+ return _jsx(FileRowsView, { output: output, pattern: str(tool.input?.pattern) });
430
+ case "Glob":
431
+ return _jsx(FileRowsView, { output: output });
432
+ case "Read":
433
+ return _jsx(ReadOutputStub, { output: output });
434
+ case "WebSearch":
435
+ return _jsx(WebSearchResultsView, { output: output });
436
+ default:
437
+ return _jsx(ClampedPre, { text: output });
438
+ }
439
+ }
440
+ /** DOM-safety cap; "Show all" raises it but never renders unbounded output. */
441
+ const CLAMP_HARD_CAP = 50_000;
442
+ const CLAMP_SHOW_ALL_CAP = 200_000;
443
+ export function ClampedPre({ text, isError }) {
444
+ const lines = countLines(text.trimEnd());
445
+ const clampable = lines > 16 || text.length > 1600;
446
+ const [showAll, setShowAll] = useState(false);
447
+ const shown = showAll ? text.slice(0, CLAMP_SHOW_ALL_CAP) : text.slice(0, CLAMP_HARD_CAP);
448
+ return (_jsxs("div", { children: [_jsxs("div", { className: cn("relative", clampable && !showAll && "max-h-56 overflow-hidden"), children: [_jsxs("pre", { className: cn("overflow-x-auto rounded-md p-2 pr-7 font-[family-name:var(--font-mono)] text-[11px] leading-relaxed", isError
449
+ ? "bg-destructive/10 text-destructive"
450
+ : "bg-background/60 text-muted-foreground"), children: [linkifyPaths(shown), text.length > shown.length && "\n…"] }), clampable && !showAll && (_jsx("div", { className: "pointer-events-none absolute inset-x-0 bottom-0 h-10 rounded-b-md bg-gradient-to-t from-surface to-transparent" }))] }), clampable && (_jsx("button", { type: "button", onClick: () => setShowAll(!showAll), className: "mt-1 text-[10px] text-muted-foreground/60 transition-colors hover:text-foreground", children: showAll ? "Collapse" : `Show all (${lines} lines)` }))] }));
451
+ }
452
+ /** Same clamp behavior for arbitrary children (markdown outputs). */
453
+ function ClampedBox({ children, lineLabel }) {
454
+ const [showAll, setShowAll] = useState(false);
455
+ return (_jsxs("div", { children: [_jsxs("div", { className: cn("relative", !showAll && "max-h-72 overflow-hidden"), children: [children, !showAll && (_jsx("div", { className: "pointer-events-none absolute inset-x-0 bottom-0 h-10 rounded-b-md bg-gradient-to-t from-surface to-transparent" }))] }), _jsx("button", { type: "button", onClick: () => setShowAll(!showAll), className: "mt-1 text-[10px] text-muted-foreground/60 transition-colors hover:text-foreground", children: showAll ? "Collapse" : `Show all (${lineLabel})` })] }));
456
+ }
457
+ /**
458
+ * Compile a Grep pattern for match highlighting.
459
+ *
460
+ * We run this against every visible row during render, so it must be safe
461
+ * against catastrophic backtracking (ReDoS) — a synchronous `RegExp.exec` can't
462
+ * be interrupted, so a pattern like `(a+)+$` would freeze the UI thread. A
463
+ * pattern that contains no unbounded-repetition operator (`*`, `+`, `{`) cannot
464
+ * backtrack exponentially, so only those compile as real regexes; anything else
465
+ * (and any pattern that fails to compile, e.g. `c++`, `(`) is matched as an
466
+ * escaped literal. Both paths are linear-time. Returns null if even the literal
467
+ * form can't compile, so callers degrade to unhighlighted rows instead of
468
+ * throwing.
469
+ */
470
+ export function safeSearchRegex(pattern) {
471
+ if (!pattern)
472
+ return null;
473
+ if (!/[*+{]/.test(pattern)) {
474
+ try {
475
+ return new RegExp(pattern, "gi");
476
+ }
477
+ catch {
478
+ // fall through to the escaped-literal form
479
+ }
480
+ }
481
+ try {
482
+ return new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi");
483
+ }
484
+ catch {
485
+ return null;
486
+ }
487
+ }
488
+ /**
489
+ * Split `text` into alternating non-match / match segments for a global regex.
490
+ * Zero-width matches are skipped (advancing lastIndex) so patterns like `a*`
491
+ * never spin. Purely string-in / segments-out so it's easy to unit test.
492
+ */
493
+ export function splitMatches(text, re) {
494
+ const segments = [];
495
+ re.lastIndex = 0;
496
+ let last = 0;
497
+ let m;
498
+ let guard = 0;
499
+ while ((m = re.exec(text)) !== null) {
500
+ if (guard++ > 10_000)
501
+ break;
502
+ if (m[0].length === 0) {
503
+ re.lastIndex++;
504
+ continue;
505
+ }
506
+ if (m.index > last)
507
+ segments.push({ text: text.slice(last, m.index), match: false });
508
+ segments.push({ text: m[0], match: true });
509
+ last = m.index + m[0].length;
510
+ }
511
+ if (last < text.length)
512
+ segments.push({ text: text.slice(last), match: false });
513
+ return segments;
514
+ }
515
+ /**
516
+ * Split a ripgrep-style `path:line:content` row into its linkable prefix and
517
+ * the content portion. Returns null when the row has no such prefix (e.g. Glob
518
+ * paths or an unusual format), so the caller can treat the whole line as content.
519
+ */
520
+ export function splitGrepRow(line) {
521
+ const m = line.match(/^(.+?:\d+:)([\s\S]*)$/);
522
+ if (!m)
523
+ return null;
524
+ return { prefix: m[1], content: m[2] };
525
+ }
526
+ /** Grep/Glob results: one clickable row per line. */
527
+ export function FileRowsView({ output, pattern }) {
528
+ const lines = output
529
+ .split("\n")
530
+ .map((l) => l.trimEnd())
531
+ .filter((l) => l.trim().length > 0);
532
+ // Degenerate outputs fall back to the plain view
533
+ if (lines.length === 0 || lines.length > 400) {
534
+ return _jsx(ClampedPre, { text: output });
535
+ }
536
+ const re = pattern ? safeSearchRegex(pattern) : null;
537
+ // Highlight the matched term inside the CONTENT only — never the `path:line:`
538
+ // prefix (a highlight there would imply the match was in the path and would
539
+ // fragment the path link). The prefix keeps full path linkification.
540
+ const renderRow = (line) => {
541
+ if (!re)
542
+ return linkifyPaths(line);
543
+ const split = splitGrepRow(line);
544
+ const prefix = split?.prefix ?? "";
545
+ const content = split ? split.content : line;
546
+ return (_jsxs(_Fragment, { children: [prefix && _jsx("span", { children: linkifyPaths(prefix) }), splitMatches(content, re).map((seg, i) => seg.match ? (_jsx("mark", { className: "rounded-sm bg-amber-400/25 px-0.5 text-amber-100", children: seg.text }, i)) : (_jsx("span", { children: linkifyPaths(seg.text) }, i)))] }));
547
+ };
548
+ return (_jsx("div", { className: "max-h-56 overflow-y-auto rounded-md bg-background/60 font-[family-name:var(--font-mono)] text-[11px] leading-relaxed", children: lines.map((line, i) => (_jsx("div", { className: "whitespace-pre-wrap break-all border-b border-border/10 px-2 py-1 last:border-0", children: renderRow(line) }, i))) }));
549
+ }
550
+ /**
551
+ * Tolerant parser for WebSearch output. The raw text is semi-structured, so we
552
+ * try a few shapes in priority order — JSON `{title,url}` objects, markdown
553
+ * links, then a line-scan that pairs a bare URL with nearby title text —
554
+ * de-duplicating by URL. Garbage input yields `[]` and never throws.
555
+ */
556
+ export function parseWebSearchResults(output) {
557
+ const results = [];
558
+ const seen = new Set();
559
+ const push = (title, url) => {
560
+ // Trim trailing sentence punctuation, but keep brackets that are balanced
561
+ // inside the URL itself — e.g. `…/Function_(mathematics)` must keep its `)`.
562
+ let u = url.trim().replace(/[.,;:'"]+$/, "");
563
+ while ((u.endsWith(")") && !u.includes("(")) ||
564
+ (u.endsWith("]") && !u.includes("["))) {
565
+ u = u.slice(0, -1).replace(/[.,;:'"]+$/, "");
566
+ }
567
+ if (!/^https?:\/\//i.test(u) || seen.has(u))
568
+ return;
569
+ seen.add(u);
570
+ const t = title.trim().replace(/^[-*\d.)\s]+/, "").trim();
571
+ results.push({ title: t || u, url: u });
572
+ };
573
+ // 1. JSON objects carrying "title"/"url" keys
574
+ for (const m of output.matchAll(/\{[^{}]*\}/g)) {
575
+ const url = m[0].match(/"url"\s*:\s*"([^"]+)"/)?.[1];
576
+ if (!url)
577
+ continue;
578
+ push(m[0].match(/"title"\s*:\s*"([^"]*)"/)?.[1] ?? "", url);
579
+ }
580
+ if (results.length)
581
+ return results;
582
+ // 2. Markdown links [title](url)
583
+ for (const m of output.matchAll(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g)) {
584
+ push(m[1], m[2]);
585
+ }
586
+ if (results.length)
587
+ return results;
588
+ // 3. Line scan: a URL uses the leading text on its line, else the line above.
589
+ // Capture greedily to whitespace (so balanced parens like `/Foo_(bar)` survive)
590
+ // and let `push` trim any unbalanced trailing bracket / sentence punctuation.
591
+ const urlRe = /(https?:\/\/\S+)/;
592
+ let lastText = "";
593
+ for (const raw of output.split("\n")) {
594
+ const line = raw.trim();
595
+ const um = line.match(urlRe);
596
+ if (um) {
597
+ push(line.slice(0, um.index).trim() || lastText, um[1]);
598
+ lastText = "";
599
+ }
600
+ else if (line) {
601
+ lastText = line;
602
+ }
603
+ }
604
+ return results;
605
+ }
606
+ /** Structured WebSearch results: title links with a dimmed host beneath. */
607
+ function WebSearchResultsView({ output }) {
608
+ const results = parseWebSearchResults(output);
609
+ if (results.length === 0)
610
+ return _jsx(ClampedPre, { text: output });
611
+ return (_jsx("div", { className: "space-y-1.5 rounded-md bg-background/60 p-2 text-[11px]", children: results.map((r, i) => {
612
+ let host = r.url;
613
+ try {
614
+ host = new URL(r.url).host;
615
+ }
616
+ catch {
617
+ // keep the raw URL as the host label
618
+ }
619
+ return (_jsxs("div", { className: "min-w-0", children: [_jsx("a", { href: r.url, target: "_blank", rel: "noopener noreferrer", className: "block truncate text-primary hover:underline", children: r.title }), _jsx("span", { className: "block truncate text-[10px] text-muted-foreground/60", children: host })] }, i));
620
+ }) }));
621
+ }
622
+ /** Read output is rarely worth showing — collapse to a stub by default. */
623
+ function ReadOutputStub({ output }) {
624
+ const [open, setOpen] = useState(false);
625
+ const lines = countLines(output.trimEnd());
626
+ return (_jsxs("div", { children: [_jsxs("button", { type: "button", onClick: () => setOpen(!open), className: "flex items-center gap-1 text-[10px] text-muted-foreground/60 transition-colors hover:text-foreground", children: [_jsx(ChevronRight, { className: cn("h-3 w-3 transition-transform", open && "rotate-90") }), lines, " lines read"] }), open && (_jsx("div", { className: "mt-1", children: _jsx(ClampedPre, { text: output }) }))] }));
627
+ }
628
+ //# sourceMappingURL=tool-views.js.map