@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,344 @@
1
+ import { create } from "zustand";
2
+ import type {
3
+ FileEntry,
4
+ FileContentResponse,
5
+ FileResolveResponse,
6
+ WikilinkMapResponse,
7
+ } from "@schlessera/brain-ui-sdk/protocol";
8
+ import { FILE_SIZE_CAP_BYTES } from "@schlessera/brain-ui-sdk/protocol";
9
+ import { API_BASE } from "../lib/backend.js";
10
+
11
+ export type ViewMode = "preview" | "raw";
12
+
13
+ const FRONTMATTER_COLLAPSED_KEY = "brain-ui:frontmatter-collapsed";
14
+
15
+ function readFrontmatterCollapsed(): boolean {
16
+ if (typeof localStorage === "undefined") return false;
17
+ return localStorage.getItem(FRONTMATTER_COLLAPSED_KEY) === "1";
18
+ }
19
+
20
+ interface FileState {
21
+ // Tree state
22
+ dirCache: Record<string, FileEntry[]>;
23
+ loadingDirs: Set<string>;
24
+ dirErrors: Record<string, string>;
25
+ expandedDirs: Set<string>;
26
+
27
+ // Viewer state
28
+ currentPath: string | null;
29
+ currentContent: FileContentResponse | null;
30
+ contentLoading: boolean;
31
+ contentError: string | null;
32
+ viewMode: ViewMode;
33
+ treeExpanded: boolean;
34
+ /**
35
+ * Whether the frontmatter table at the top of markdown files is collapsed.
36
+ * Persisted in localStorage so the preference carries across files.
37
+ */
38
+ frontmatterCollapsed: boolean;
39
+ /**
40
+ * Directory most recently navigated to via openDir. Used by the tree to
41
+ * apply a brief highlight + scroll the node into view. Cleared shortly
42
+ * after by openDir itself.
43
+ */
44
+ highlightedDir: string | null;
45
+ /**
46
+ * Lowercase slug -> repo-relative .md path. Lazily loaded the first
47
+ * time a markdown surface needs to resolve a [[wikilink]].
48
+ */
49
+ wikilinkMap: Record<string, string>;
50
+ wikilinkLoaded: boolean;
51
+ wikilinkLoading: boolean;
52
+
53
+ // Actions
54
+ loadDir: (path: string) => Promise<void>;
55
+ toggleDir: (path: string) => Promise<void>;
56
+ setExpanded: (path: string, expanded: boolean) => void;
57
+ openFile: (path: string) => Promise<void>;
58
+ openDir: (path: string) => Promise<void>;
59
+ closeFile: () => void;
60
+ setViewMode: (mode: ViewMode) => void;
61
+ setTreeExpanded: (expanded: boolean) => void;
62
+ toggleFrontmatter: () => void;
63
+ setHighlightedDir: (path: string | null) => void;
64
+ ensureWikilinks: () => Promise<void>;
65
+ reset: () => void;
66
+ }
67
+
68
+ async function fetchTree(path: string): Promise<FileEntry[]> {
69
+ const url = `${API_BASE}/files/tree${path ? `?path=${encodeURIComponent(path)}` : ""}`;
70
+ const res = await fetch(url);
71
+ if (!res.ok) {
72
+ const body = await res.json().catch(() => ({ error: res.statusText }));
73
+ throw new Error(body.error || `HTTP ${res.status}`);
74
+ }
75
+ const data = await res.json();
76
+ return data.entries;
77
+ }
78
+
79
+ async function fetchContent(path: string): Promise<FileContentResponse> {
80
+ const res = await fetch(`${API_BASE}/files/content?path=${encodeURIComponent(path)}`);
81
+ if (!res.ok) {
82
+ const body = await res.json().catch(() => ({ error: res.statusText }));
83
+ const err = new Error(body.error || `HTTP ${res.status}`);
84
+ (err as Error & { status: number; size?: number }).status = res.status;
85
+ if (body.size) (err as Error & { status: number; size?: number }).size = body.size;
86
+ throw err;
87
+ }
88
+ return res.json();
89
+ }
90
+
91
+ async function fetchResolve(path: string): Promise<FileResolveResponse> {
92
+ const res = await fetch(`${API_BASE}/files/resolve?path=${encodeURIComponent(path)}`);
93
+ if (!res.ok) {
94
+ const body = await res.json().catch(() => ({ error: res.statusText }));
95
+ throw new Error(body.error || `HTTP ${res.status}`);
96
+ }
97
+ return res.json();
98
+ }
99
+
100
+ async function fetchWikilinks(): Promise<WikilinkMapResponse> {
101
+ const res = await fetch(`${API_BASE}/files/wikilinks`);
102
+ if (!res.ok) {
103
+ const body = await res.json().catch(() => ({ error: res.statusText }));
104
+ throw new Error(body.error || `HTTP ${res.status}`);
105
+ }
106
+ return res.json();
107
+ }
108
+
109
+ export const useFileStore = create<FileState>((set, get) => ({
110
+ dirCache: {},
111
+ loadingDirs: new Set(),
112
+ dirErrors: {},
113
+ expandedDirs: new Set([""]),
114
+
115
+ currentPath: null,
116
+ currentContent: null,
117
+ contentLoading: false,
118
+ contentError: null,
119
+ viewMode: "preview",
120
+ treeExpanded: true,
121
+ frontmatterCollapsed: readFrontmatterCollapsed(),
122
+ highlightedDir: null,
123
+ wikilinkMap: {},
124
+ wikilinkLoaded: false,
125
+ wikilinkLoading: false,
126
+
127
+ loadDir: async (path) => {
128
+ const { dirCache, loadingDirs } = get();
129
+ if (dirCache[path] || loadingDirs.has(path)) return;
130
+ const next = new Set(loadingDirs);
131
+ next.add(path);
132
+ set({ loadingDirs: next });
133
+ try {
134
+ const entries = await fetchTree(path);
135
+ set((s) => {
136
+ const loading = new Set(s.loadingDirs);
137
+ loading.delete(path);
138
+ const errors = { ...s.dirErrors };
139
+ delete errors[path];
140
+ return {
141
+ dirCache: { ...s.dirCache, [path]: entries },
142
+ loadingDirs: loading,
143
+ dirErrors: errors,
144
+ };
145
+ });
146
+ } catch (err) {
147
+ set((s) => {
148
+ const loading = new Set(s.loadingDirs);
149
+ loading.delete(path);
150
+ return {
151
+ loadingDirs: loading,
152
+ dirErrors: { ...s.dirErrors, [path]: err instanceof Error ? err.message : "load_failed" },
153
+ };
154
+ });
155
+ }
156
+ },
157
+
158
+ toggleDir: async (path) => {
159
+ const { expandedDirs } = get();
160
+ if (expandedDirs.has(path)) {
161
+ const next = new Set(expandedDirs);
162
+ next.delete(path);
163
+ set({ expandedDirs: next });
164
+ } else {
165
+ const next = new Set(expandedDirs);
166
+ next.add(path);
167
+ set({ expandedDirs: next });
168
+ await get().loadDir(path);
169
+ }
170
+ },
171
+
172
+ setExpanded: (path, expanded) => {
173
+ set((s) => {
174
+ const next = new Set(s.expandedDirs);
175
+ if (expanded) next.add(path);
176
+ else next.delete(path);
177
+ return { expandedDirs: next };
178
+ });
179
+ },
180
+
181
+ openFile: async (path) => {
182
+ const normalized = path.replace(/^\/+/, "").replace(/\\/g, "/");
183
+ set({ currentPath: normalized, contentLoading: true, contentError: null, currentContent: null, treeExpanded: false });
184
+
185
+ // Fire-and-await: load ancestor chain then content in parallel
186
+ const ancestorsPromise = (async () => {
187
+ try {
188
+ const r = await fetchResolve(normalized);
189
+ // Expand each ancestor and root, load their contents
190
+ const dirsToLoad = ["", ...r.ancestors];
191
+ set((s) => {
192
+ const next = new Set(s.expandedDirs);
193
+ for (const d of dirsToLoad) next.add(d);
194
+ return { expandedDirs: next };
195
+ });
196
+ await Promise.all(dirsToLoad.map((d) => get().loadDir(d)));
197
+ } catch {
198
+ // Tree expansion best-effort
199
+ }
200
+ })();
201
+
202
+ try {
203
+ const content = await fetchContent(normalized);
204
+ set({ currentContent: content, contentLoading: false });
205
+ // Default mode: prefer preview when available; persist otherwise
206
+ const { viewMode } = get();
207
+ if (content.kind !== "markdown" && content.kind !== "html" && viewMode === "preview") {
208
+ set({ viewMode: "raw" });
209
+ } else if ((content.kind === "markdown" || content.kind === "html") && viewMode === "raw") {
210
+ // Keep raw if user toggled — but on a fresh open default back to preview
211
+ set({ viewMode: "preview" });
212
+ }
213
+ } catch (err) {
214
+ const e = err as Error & { status?: number; size?: number };
215
+ let msg = e.message || "load_failed";
216
+ if (e.status === 413 && e.size) msg = `File too large (${(e.size / 1024 / 1024).toFixed(2)} MB; cap ${(FILE_SIZE_CAP_BYTES / 1024 / 1024).toFixed(0)} MB).`;
217
+ else if (e.status === 404) msg = "File not found.";
218
+ else if (e.message === "invalid_path") msg = "Invalid path.";
219
+ set({ contentError: msg, contentLoading: false });
220
+ } finally {
221
+ await ancestorsPromise;
222
+ }
223
+ },
224
+
225
+ closeFile: () => {
226
+ set({ currentPath: null, currentContent: null, contentError: null, treeExpanded: true });
227
+ },
228
+
229
+ openDir: async (path) => {
230
+ const normalized = path
231
+ .replace(/^\/+/, "")
232
+ .replace(/\\/g, "/")
233
+ .replace(/\/+$/, "");
234
+ if (!normalized) {
235
+ // Just expand the root view
236
+ set({ treeExpanded: true });
237
+ return;
238
+ }
239
+ // Make the tree visible even if a file is currently open.
240
+ set({ treeExpanded: true, highlightedDir: normalized });
241
+
242
+ try {
243
+ const r = await fetchResolve(normalized);
244
+ const dirs = ["", ...r.ancestors, normalized];
245
+ set((s) => {
246
+ const next = new Set(s.expandedDirs);
247
+ for (const d of dirs) next.add(d);
248
+ return { expandedDirs: next };
249
+ });
250
+ await Promise.all(dirs.map((d) => get().loadDir(d)));
251
+ } catch {
252
+ // Best-effort expansion — failure surfaces via per-dir error state
253
+ }
254
+ },
255
+
256
+ setViewMode: (mode) => set({ viewMode: mode }),
257
+
258
+ setTreeExpanded: (expanded) => set({ treeExpanded: expanded }),
259
+
260
+ setHighlightedDir: (path) => set({ highlightedDir: path }),
261
+
262
+ ensureWikilinks: async () => {
263
+ const { wikilinkLoaded, wikilinkLoading } = get();
264
+ if (wikilinkLoaded || wikilinkLoading) return;
265
+ set({ wikilinkLoading: true });
266
+ try {
267
+ const data = await fetchWikilinks();
268
+ set({
269
+ wikilinkMap: data.slugs,
270
+ wikilinkLoaded: true,
271
+ wikilinkLoading: false,
272
+ });
273
+ } catch {
274
+ // Soft-fail — leave the map empty; wikilinks will render as plain text
275
+ set({ wikilinkLoading: false });
276
+ }
277
+ },
278
+
279
+ toggleFrontmatter: () =>
280
+ set((s) => {
281
+ const next = !s.frontmatterCollapsed;
282
+ if (typeof localStorage !== "undefined") {
283
+ if (next) localStorage.setItem(FRONTMATTER_COLLAPSED_KEY, "1");
284
+ else localStorage.removeItem(FRONTMATTER_COLLAPSED_KEY);
285
+ }
286
+ return { frontmatterCollapsed: next };
287
+ }),
288
+
289
+ reset: () =>
290
+ set({
291
+ currentPath: null,
292
+ currentContent: null,
293
+ contentLoading: false,
294
+ contentError: null,
295
+ viewMode: "preview",
296
+ treeExpanded: true,
297
+ }),
298
+ }));
299
+
300
+ /**
301
+ * Heuristics: is this string a plausible repo-relative path reference?
302
+ *
303
+ * Files require a trailing `.ext` (1-8 alphanumeric chars). Directories
304
+ * require at least one `/` segment and a trailing `/`. Neither may look
305
+ * like a URL, an absolute path, or a version number.
306
+ */
307
+ const FILE_REF_RE = /^[a-z0-9_.-]+(\/[a-z0-9_.-]+)*\.[a-z0-9]{1,8}$/i;
308
+ const DIR_REF_RE = /^[a-z0-9_.-]+(\/[a-z0-9_.-]+)+\/$/i;
309
+ const URL_LIKE_RE = /^(?:https?:|mailto:|tel:|#|\/)/i;
310
+ const VERSION_RE = /^\d+(\.\d+){1,3}$/;
311
+
312
+ function rejectsCommonNonPath(href: string): boolean {
313
+ if (URL_LIKE_RE.test(href)) return true;
314
+ if (VERSION_RE.test(href)) return true;
315
+ if (href.includes(" ")) return true;
316
+ return false;
317
+ }
318
+
319
+ export function isInternalRepoPath(href: string | undefined): href is string {
320
+ if (!href) return false;
321
+ if (rejectsCommonNonPath(href)) return false;
322
+ return FILE_REF_RE.test(href);
323
+ }
324
+
325
+ export function isInternalRepoDir(href: string | undefined): href is string {
326
+ if (!href) return false;
327
+ if (rejectsCommonNonPath(href)) return false;
328
+ return DIR_REF_RE.test(href);
329
+ }
330
+
331
+ export type RepoPathKind = "file" | "dir";
332
+
333
+ export function classifyRepoPath(
334
+ href: string | undefined
335
+ ): RepoPathKind | null {
336
+ if (isInternalRepoPath(href)) return "file";
337
+ if (isInternalRepoDir(href)) return "dir";
338
+ return null;
339
+ }
340
+
341
+ /** Normalize a directory ref by trimming the trailing slash. */
342
+ export function normalizeDirPath(path: string): string {
343
+ return path.replace(/\/+$/, "");
344
+ }
@@ -0,0 +1,85 @@
1
+ import { create } from "zustand";
2
+ import type { ProviderInfo } from "@schlessera/brain-ui-sdk/protocol";
3
+ import { api, type BackendInfo } from "../lib/api-client.js";
4
+
5
+ const PROVIDER_ID_KEY = "brain-ui:provider-id";
6
+
7
+ function readSelectedId(): string {
8
+ if (typeof localStorage === "undefined") return "";
9
+ return localStorage.getItem(PROVIDER_ID_KEY) ?? "";
10
+ }
11
+
12
+ interface ProviderState {
13
+ /** Provider combos the server currently offers (metadata only). */
14
+ available: ProviderInfo[];
15
+ /** User's chosen combo for the next new conversation. Persisted. */
16
+ selectedId: string;
17
+ /**
18
+ * Provider the active session is pinned to (from `session_info`). When set,
19
+ * it takes display precedence over `selectedId` and the picker locks.
20
+ */
21
+ pinnedId: string | null;
22
+ /** Capability metadata keyed by backend id. */
23
+ backends: Record<string, BackendInfo>;
24
+ loaded: boolean;
25
+
26
+ loadProviders: () => Promise<void>;
27
+ setSelected: (id: string) => void;
28
+ setPinned: (id: string | null) => void;
29
+ }
30
+
31
+ /** Resolve the backend metadata for an available provider profile. */
32
+ export function selectBackendForProvider(
33
+ state: Pick<ProviderState, "available" | "backends">,
34
+ providerId: string | null | undefined
35
+ ): BackendInfo | null {
36
+ const backendId = state.available.find(
37
+ (provider) => provider.id === providerId
38
+ )?.backendId;
39
+ return backendId ? state.backends[backendId] ?? null : null;
40
+ }
41
+
42
+ export const useProviderStore = create<ProviderState>((set, get) => ({
43
+ available: [],
44
+ selectedId: readSelectedId(),
45
+ pinnedId: null,
46
+ backends: {},
47
+ loaded: false,
48
+
49
+ loadProviders: async () => {
50
+ try {
51
+ const { providers, backends } = await api.providers();
52
+ set((s) => {
53
+ // Keep the persisted choice if it's still on offer; otherwise fall
54
+ // back to the first available combo.
55
+ const stillValid = providers.some((p) => p.id === s.selectedId);
56
+ const selectedId =
57
+ stillValid && s.selectedId
58
+ ? s.selectedId
59
+ : providers[0]?.id ?? "";
60
+ return {
61
+ available: providers,
62
+ selectedId,
63
+ backends: backends ?? {},
64
+ loaded: true,
65
+ };
66
+ });
67
+ } catch {
68
+ // Soft-fail: leave the picker empty; the composer still works and
69
+ // sends run on the server default.
70
+ set({ loaded: true });
71
+ }
72
+ },
73
+
74
+ setSelected: (id) => {
75
+ if (typeof localStorage !== "undefined") {
76
+ localStorage.setItem(PROVIDER_ID_KEY, id);
77
+ }
78
+ set({ selectedId: id });
79
+ },
80
+
81
+ setPinned: (id) => {
82
+ if (get().pinnedId === id) return;
83
+ set({ pinnedId: id });
84
+ },
85
+ }));
@@ -0,0 +1,48 @@
1
+ import { create } from "zustand";
2
+
3
+ interface UIState {
4
+ sessionPanelOpen: boolean;
5
+ syncPanelOpen: boolean;
6
+ whatsupPanelOpen: boolean;
7
+ filePanelOpen: boolean;
8
+ securityPanelOpen: boolean;
9
+ toggleSessionPanel: () => void;
10
+ toggleSyncPanel: () => void;
11
+ toggleWhatsupPanel: () => void;
12
+ toggleFilePanel: () => void;
13
+ toggleSecurityPanel: () => void;
14
+ closeAllPanels: () => void;
15
+ setSyncPanelOpen: (open: boolean) => void;
16
+ setWhatsupPanelOpen: (open: boolean) => void;
17
+ setSessionPanelOpen: (open: boolean) => void;
18
+ setFilePanelOpen: (open: boolean) => void;
19
+ setSecurityPanelOpen: (open: boolean) => void;
20
+ }
21
+
22
+ const CLOSED = {
23
+ sessionPanelOpen: false,
24
+ syncPanelOpen: false,
25
+ whatsupPanelOpen: false,
26
+ filePanelOpen: false,
27
+ securityPanelOpen: false,
28
+ };
29
+
30
+ export const useUIStore = create<UIState>((set) => ({
31
+ ...CLOSED,
32
+ toggleSessionPanel: () =>
33
+ set((s) => ({ ...CLOSED, sessionPanelOpen: !s.sessionPanelOpen })),
34
+ toggleSyncPanel: () =>
35
+ set((s) => ({ ...CLOSED, syncPanelOpen: !s.syncPanelOpen })),
36
+ toggleWhatsupPanel: () =>
37
+ set((s) => ({ ...CLOSED, whatsupPanelOpen: !s.whatsupPanelOpen })),
38
+ toggleFilePanel: () =>
39
+ set((s) => ({ ...CLOSED, filePanelOpen: !s.filePanelOpen })),
40
+ toggleSecurityPanel: () =>
41
+ set((s) => ({ ...CLOSED, securityPanelOpen: !s.securityPanelOpen })),
42
+ closeAllPanels: () => set({ ...CLOSED }),
43
+ setSyncPanelOpen: (open) => set({ syncPanelOpen: open }),
44
+ setWhatsupPanelOpen: (open) => set({ whatsupPanelOpen: open }),
45
+ setSessionPanelOpen: (open) => set({ sessionPanelOpen: open }),
46
+ setFilePanelOpen: (open) => set({ filePanelOpen: open }),
47
+ setSecurityPanelOpen: (open) => set({ securityPanelOpen: open }),
48
+ }));
package/src/styles.css ADDED
@@ -0,0 +1,4 @@
1
+ /* Precompiled-stylesheet entry: `bunx @tailwindcss/cli -i src/styles.css -o dist/styles.css`. */
2
+ @import "tailwindcss" source(none);
3
+ @source "./";
4
+ @import "./theme.css";