@schlessera/brain-ui-react 0.34.0 → 0.35.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 (223) hide show
  1. package/README.md +12 -0
  2. package/dist/components/activity/activity-page.d.ts.map +1 -1
  3. package/dist/components/activity/activity-page.js +8 -149
  4. package/dist/components/activity/activity-page.js.map +1 -1
  5. package/dist/components/activity/activity-rollups.d.ts +5 -0
  6. package/dist/components/activity/activity-rollups.d.ts.map +1 -0
  7. package/dist/components/activity/activity-rollups.js +39 -0
  8. package/dist/components/activity/activity-rollups.js.map +1 -0
  9. package/dist/components/activity/activity-run-detail.d.ts +11 -0
  10. package/dist/components/activity/activity-run-detail.d.ts.map +1 -0
  11. package/dist/components/activity/activity-run-detail.js +111 -0
  12. package/dist/components/activity/activity-run-detail.js.map +1 -0
  13. package/dist/components/activity/activity-run-list.d.ts +15 -0
  14. package/dist/components/activity/activity-run-list.d.ts.map +1 -0
  15. package/dist/components/activity/activity-run-list.js +25 -0
  16. package/dist/components/activity/activity-run-list.js.map +1 -0
  17. package/dist/components/chat/brain-markdown-code.d.ts +4 -0
  18. package/dist/components/chat/brain-markdown-code.d.ts.map +1 -0
  19. package/dist/components/chat/brain-markdown-code.js +47 -0
  20. package/dist/components/chat/brain-markdown-code.js.map +1 -0
  21. package/dist/components/chat/brain-markdown-entities.d.ts +14 -0
  22. package/dist/components/chat/brain-markdown-entities.d.ts.map +1 -0
  23. package/dist/components/chat/brain-markdown-entities.js +93 -0
  24. package/dist/components/chat/brain-markdown-entities.js.map +1 -0
  25. package/dist/components/chat/brain-markdown-links.d.ts +36 -0
  26. package/dist/components/chat/brain-markdown-links.d.ts.map +1 -0
  27. package/dist/components/chat/brain-markdown-links.js +176 -0
  28. package/dist/components/chat/brain-markdown-links.js.map +1 -0
  29. package/dist/components/chat/brain-markdown-share.d.ts +15 -0
  30. package/dist/components/chat/brain-markdown-share.d.ts.map +1 -0
  31. package/dist/components/chat/brain-markdown-share.js +40 -0
  32. package/dist/components/chat/brain-markdown-share.js.map +1 -0
  33. package/dist/components/chat/brain-markdown.d.ts +1 -34
  34. package/dist/components/chat/brain-markdown.d.ts.map +1 -1
  35. package/dist/components/chat/brain-markdown.js +9 -402
  36. package/dist/components/chat/brain-markdown.js.map +1 -1
  37. package/dist/components/chat/composer.d.ts.map +1 -1
  38. package/dist/components/chat/composer.js +14 -1
  39. package/dist/components/chat/composer.js.map +1 -1
  40. package/dist/components/chat/session-drawer.d.ts.map +1 -1
  41. package/dist/components/chat/session-drawer.js +25 -10
  42. package/dist/components/chat/session-drawer.js.map +1 -1
  43. package/dist/components/chat/tool-views.d.ts +2 -2
  44. package/dist/components/chat/tool-views.d.ts.map +1 -1
  45. package/dist/components/chat/tool-views.js +2 -16
  46. package/dist/components/chat/tool-views.js.map +1 -1
  47. package/dist/components/chat/use-markdown-highlight.d.ts +12 -0
  48. package/dist/components/chat/use-markdown-highlight.d.ts.map +1 -0
  49. package/dist/components/chat/use-markdown-highlight.js +56 -0
  50. package/dist/components/chat/use-markdown-highlight.js.map +1 -0
  51. package/dist/components/connectivity/connection-gate.d.ts.map +1 -1
  52. package/dist/components/connectivity/connection-gate.js +94 -8
  53. package/dist/components/connectivity/connection-gate.js.map +1 -1
  54. package/dist/components/connectivity/connection-state.d.ts +12 -0
  55. package/dist/components/connectivity/connection-state.d.ts.map +1 -0
  56. package/dist/components/connectivity/connection-state.js +23 -0
  57. package/dist/components/connectivity/connection-state.js.map +1 -0
  58. package/dist/components/graph/graph-canvas-lazy.d.ts +2 -0
  59. package/dist/components/graph/graph-canvas-lazy.d.ts.map +1 -0
  60. package/dist/components/graph/graph-canvas-lazy.js +4 -0
  61. package/dist/components/graph/graph-canvas-lazy.js.map +1 -0
  62. package/dist/components/graph/graph-controls.js +4 -1
  63. package/dist/components/graph/graph-controls.js.map +1 -1
  64. package/dist/components/graph/graph-maintenance.d.ts +8 -0
  65. package/dist/components/graph/graph-maintenance.d.ts.map +1 -0
  66. package/dist/components/graph/graph-maintenance.js +115 -0
  67. package/dist/components/graph/graph-maintenance.js.map +1 -0
  68. package/dist/components/graph/graph-page.d.ts.map +1 -1
  69. package/dist/components/graph/graph-page.js +11 -306
  70. package/dist/components/graph/graph-page.js.map +1 -1
  71. package/dist/components/graph/graph-scene.d.ts +8 -0
  72. package/dist/components/graph/graph-scene.d.ts.map +1 -0
  73. package/dist/components/graph/graph-scene.js +208 -0
  74. package/dist/components/graph/graph-scene.js.map +1 -0
  75. package/dist/components/graph/graph-spinner.d.ts +2 -0
  76. package/dist/components/graph/graph-spinner.d.ts.map +1 -0
  77. package/dist/components/graph/graph-spinner.js +6 -0
  78. package/dist/components/graph/graph-spinner.js.map +1 -0
  79. package/dist/components/layout/app-shell.d.ts.map +1 -1
  80. package/dist/components/layout/app-shell.js +2 -1
  81. package/dist/components/layout/app-shell.js.map +1 -1
  82. package/dist/components/quick-actions/add-modal.d.ts.map +1 -1
  83. package/dist/components/quick-actions/add-modal.js +47 -4
  84. package/dist/components/quick-actions/add-modal.js.map +1 -1
  85. package/dist/components/quick-actions/search-modal.d.ts.map +1 -1
  86. package/dist/components/quick-actions/search-modal.js +46 -18
  87. package/dist/components/quick-actions/search-modal.js.map +1 -1
  88. package/dist/components/quick-actions/streaming-modal.d.ts.map +1 -1
  89. package/dist/components/quick-actions/streaming-modal.js +4 -1
  90. package/dist/components/quick-actions/streaming-modal.js.map +1 -1
  91. package/dist/components/settings/devices-agents-tab.d.ts +5 -0
  92. package/dist/components/settings/devices-agents-tab.d.ts.map +1 -0
  93. package/dist/components/settings/devices-agents-tab.js +96 -0
  94. package/dist/components/settings/devices-agents-tab.js.map +1 -0
  95. package/dist/components/settings/one-time-agent-credential.d.ts +3 -0
  96. package/dist/components/settings/one-time-agent-credential.d.ts.map +1 -0
  97. package/dist/components/settings/one-time-agent-credential.js +55 -0
  98. package/dist/components/settings/one-time-agent-credential.js.map +1 -0
  99. package/dist/components/settings/settings-panel.d.ts.map +1 -1
  100. package/dist/components/settings/settings-panel.js +16 -3
  101. package/dist/components/settings/settings-panel.js.map +1 -1
  102. package/dist/hooks/use-vpn-status.d.ts +6 -1
  103. package/dist/hooks/use-vpn-status.d.ts.map +1 -1
  104. package/dist/hooks/use-vpn-status.js +67 -30
  105. package/dist/hooks/use-vpn-status.js.map +1 -1
  106. package/dist/hooks/use-websocket.d.ts +3 -2
  107. package/dist/hooks/use-websocket.d.ts.map +1 -1
  108. package/dist/hooks/use-websocket.js +65 -254
  109. package/dist/hooks/use-websocket.js.map +1 -1
  110. package/dist/hooks/websocket-handlers/activity.d.ts +5 -0
  111. package/dist/hooks/websocket-handlers/activity.d.ts.map +1 -0
  112. package/dist/hooks/websocket-handlers/activity.js +10 -0
  113. package/dist/hooks/websocket-handlers/activity.js.map +1 -0
  114. package/dist/hooks/websocket-handlers/chat.d.ts +17 -0
  115. package/dist/hooks/websocket-handlers/chat.d.ts.map +1 -0
  116. package/dist/hooks/websocket-handlers/chat.js +175 -0
  117. package/dist/hooks/websocket-handlers/chat.js.map +1 -0
  118. package/dist/hooks/websocket-handlers/connection.d.ts +6 -0
  119. package/dist/hooks/websocket-handlers/connection.d.ts.map +1 -0
  120. package/dist/hooks/websocket-handlers/connection.js +27 -0
  121. package/dist/hooks/websocket-handlers/connection.js.map +1 -0
  122. package/dist/hooks/websocket-handlers/index.d.ts +4 -0
  123. package/dist/hooks/websocket-handlers/index.d.ts.map +1 -0
  124. package/dist/hooks/websocket-handlers/index.js +19 -0
  125. package/dist/hooks/websocket-handlers/index.js.map +1 -0
  126. package/dist/hooks/websocket-handlers/mask.d.ts +4 -0
  127. package/dist/hooks/websocket-handlers/mask.d.ts.map +1 -0
  128. package/dist/hooks/websocket-handlers/mask.js +14 -0
  129. package/dist/hooks/websocket-handlers/mask.js.map +1 -0
  130. package/dist/hooks/websocket-handlers/provider.d.ts +4 -0
  131. package/dist/hooks/websocket-handlers/provider.d.ts.map +1 -0
  132. package/dist/hooks/websocket-handlers/provider.js +31 -0
  133. package/dist/hooks/websocket-handlers/provider.js.map +1 -0
  134. package/dist/hooks/websocket-handlers/types.d.ts +24 -0
  135. package/dist/hooks/websocket-handlers/types.d.ts.map +1 -0
  136. package/dist/hooks/websocket-handlers/types.js +2 -0
  137. package/dist/hooks/websocket-handlers/types.js.map +1 -0
  138. package/dist/lib/api-client.d.ts +32 -0
  139. package/dist/lib/api-client.d.ts.map +1 -1
  140. package/dist/lib/api-client.js +11 -0
  141. package/dist/lib/api-client.js.map +1 -1
  142. package/dist/lib/format-time.d.ts +3 -0
  143. package/dist/lib/format-time.d.ts.map +1 -0
  144. package/dist/lib/format-time.js +25 -0
  145. package/dist/lib/format-time.js.map +1 -0
  146. package/dist/lib/push-registration.d.ts +7 -0
  147. package/dist/lib/push-registration.d.ts.map +1 -0
  148. package/dist/lib/push-registration.js +26 -0
  149. package/dist/lib/push-registration.js.map +1 -0
  150. package/dist/stores/activity-store-helpers.d.ts +50 -0
  151. package/dist/stores/activity-store-helpers.d.ts.map +1 -0
  152. package/dist/stores/activity-store-helpers.js +143 -0
  153. package/dist/stores/activity-store-helpers.js.map +1 -0
  154. package/dist/stores/activity-store-types.d.ts +48 -0
  155. package/dist/stores/activity-store-types.d.ts.map +1 -0
  156. package/dist/stores/activity-store-types.js +2 -0
  157. package/dist/stores/activity-store-types.js.map +1 -0
  158. package/dist/stores/activity-store.d.ts +2 -78
  159. package/dist/stores/activity-store.d.ts.map +1 -1
  160. package/dist/stores/activity-store.js +22 -142
  161. package/dist/stores/activity-store.js.map +1 -1
  162. package/dist/stores/connection-store.d.ts +14 -2
  163. package/dist/stores/connection-store.d.ts.map +1 -1
  164. package/dist/stores/connection-store.js +17 -0
  165. package/dist/stores/connection-store.js.map +1 -1
  166. package/dist/stores/file-store.d.ts.map +1 -1
  167. package/dist/stores/file-store.js +54 -18
  168. package/dist/stores/file-store.js.map +1 -1
  169. package/dist/stores/principal-store.d.ts +16 -0
  170. package/dist/stores/principal-store.d.ts.map +1 -0
  171. package/dist/stores/principal-store.js +29 -0
  172. package/dist/stores/principal-store.js.map +1 -0
  173. package/dist/stores/ui-store.d.ts +1 -1
  174. package/dist/stores/ui-store.d.ts.map +1 -1
  175. package/dist/styles.css +1 -1
  176. package/package.json +2 -2
  177. package/src/components/activity/activity-page.tsx +6 -477
  178. package/src/components/activity/activity-rollups.tsx +94 -0
  179. package/src/components/activity/activity-run-detail.tsx +301 -0
  180. package/src/components/activity/activity-run-list.tsx +97 -0
  181. package/src/components/chat/brain-markdown-code.tsx +60 -0
  182. package/src/components/chat/brain-markdown-entities.tsx +116 -0
  183. package/src/components/chat/brain-markdown-links.tsx +247 -0
  184. package/src/components/chat/brain-markdown-share.ts +47 -0
  185. package/src/components/chat/brain-markdown.tsx +27 -535
  186. package/src/components/chat/composer.tsx +14 -1
  187. package/src/components/chat/session-drawer.tsx +29 -10
  188. package/src/components/chat/tool-views.tsx +2 -13
  189. package/src/components/chat/use-markdown-highlight.ts +63 -0
  190. package/src/components/connectivity/connection-gate.tsx +128 -14
  191. package/src/components/connectivity/connection-state.ts +37 -0
  192. package/src/components/graph/graph-canvas-lazy.tsx +4 -0
  193. package/src/components/graph/graph-controls.tsx +4 -1
  194. package/src/components/graph/graph-maintenance.tsx +304 -0
  195. package/src/components/graph/graph-page.tsx +11 -840
  196. package/src/components/graph/graph-scene.tsx +548 -0
  197. package/src/components/graph/graph-spinner.tsx +9 -0
  198. package/src/components/layout/app-shell.tsx +2 -0
  199. package/src/components/quick-actions/add-modal.tsx +49 -4
  200. package/src/components/quick-actions/search-modal.tsx +46 -18
  201. package/src/components/quick-actions/streaming-modal.tsx +3 -1
  202. package/src/components/settings/devices-agents-tab.tsx +231 -0
  203. package/src/components/settings/one-time-agent-credential.tsx +131 -0
  204. package/src/components/settings/settings-panel.tsx +20 -5
  205. package/src/hooks/use-vpn-status.ts +71 -31
  206. package/src/hooks/use-websocket.ts +74 -275
  207. package/src/hooks/websocket-handlers/activity.ts +13 -0
  208. package/src/hooks/websocket-handlers/chat.ts +210 -0
  209. package/src/hooks/websocket-handlers/connection.ts +30 -0
  210. package/src/hooks/websocket-handlers/index.ts +59 -0
  211. package/src/hooks/websocket-handlers/mask.ts +17 -0
  212. package/src/hooks/websocket-handlers/provider.ts +35 -0
  213. package/src/hooks/websocket-handlers/types.ts +27 -0
  214. package/src/lib/api-client.ts +40 -1
  215. package/src/lib/format-time.ts +20 -0
  216. package/src/lib/push-registration.ts +29 -0
  217. package/src/stores/activity-store-helpers.ts +164 -0
  218. package/src/stores/activity-store-types.ts +55 -0
  219. package/src/stores/activity-store.ts +21 -217
  220. package/src/stores/connection-store.ts +33 -2
  221. package/src/stores/file-store.ts +42 -12
  222. package/src/stores/principal-store.ts +38 -0
  223. package/src/stores/ui-store.ts +1 -1
@@ -0,0 +1,247 @@
1
+ import React from "react";
2
+
3
+ import { apiBase } from "../../lib/backend.js";
4
+ import {
5
+ isInternalRepoDir,
6
+ isInternalRepoPath,
7
+ useFileStore,
8
+ } from "../../stores/file-store.js";
9
+ import { useUIStore } from "../../stores/ui-store.js";
10
+
11
+ /**
12
+ * Turn a markdown image source into something the browser can actually load.
13
+ *
14
+ * Anything already absolute — a `data:` URI, an http(s) URL, or a path that is
15
+ * already an API route — passes through. A repo-relative path is rewritten to
16
+ * the files endpoint, which is where brain content is served from.
17
+ */
18
+ export function repoImageSrc(src: string): string {
19
+ const trimmed = src.trim();
20
+ if (!trimmed) return src;
21
+ if (/^(data:|blob:|https?:\/\/)/i.test(trimmed)) return trimmed;
22
+ if (trimmed.startsWith("/api/")) return trimmed;
23
+ const rel = trimmed.replace(/^\.\//, "").replace(/^\/+/, "");
24
+ return `${apiBase()}/files/content?path=${encodeURIComponent(rel)}&raw=1`;
25
+ }
26
+ /** Click target for a repo-relative file reference. Opens the file panel + viewer. */
27
+ export function FileLink({ path, children }: { path: string; children?: React.ReactNode }) {
28
+ const openFile = useFileStore((s) => s.openFile);
29
+ const setFilePanelOpen = useUIStore((s) => s.setFilePanelOpen);
30
+ return (
31
+ <a
32
+ href={`#/files/${path}`}
33
+ onClick={(e) => {
34
+ e.preventDefault();
35
+ setFilePanelOpen(true);
36
+ void openFile(path);
37
+ }}
38
+ className="brain-file-link"
39
+ title={path}
40
+ >
41
+ {children ?? path}
42
+ </a>
43
+ );
44
+ }
45
+
46
+ /**
47
+ * Resolve an obsidian-style `[[target]]` to a repo-relative path using the
48
+ * wikilink map. Targets containing `/` are treated as paths directly; bare
49
+ * slugs are looked up by basename. An `#anchor` suffix is stripped before
50
+ * lookup.
51
+ */
52
+ function resolveWikilinkTarget(
53
+ raw: string,
54
+ slugMap: Record<string, string>
55
+ ): string | null {
56
+ const hashIdx = raw.indexOf("#");
57
+ const noAnchor = hashIdx >= 0 ? raw.slice(0, hashIdx).trim() : raw.trim();
58
+ if (!noAnchor) return null;
59
+ if (noAnchor.includes("/")) {
60
+ if (noAnchor.endsWith("/")) return null;
61
+ return /\.[a-z0-9]{1,8}$/i.test(noAnchor) ? noAnchor : `${noAnchor}.md`;
62
+ }
63
+ return slugMap[noAnchor.toLowerCase()] ?? null;
64
+ }
65
+
66
+ /** Click target for `[[slug]]` / `[[slug|Label]]`. */
67
+ export function WikiLink({
68
+ target,
69
+ label,
70
+ }: {
71
+ target: string;
72
+ label?: string;
73
+ }) {
74
+ const slugMap = useFileStore((s) => s.wikilinkMap);
75
+ const loaded = useFileStore((s) => s.wikilinkLoaded);
76
+ const openFile = useFileStore((s) => s.openFile);
77
+ const setFilePanelOpen = useUIStore((s) => s.setFilePanelOpen);
78
+
79
+ const resolved = resolveWikilinkTarget(target, slugMap);
80
+ const display = label ?? target;
81
+
82
+ if (!resolved) {
83
+ return (
84
+ <span
85
+ className="brain-wiki-link brain-wiki-link--unresolved"
86
+ title={loaded ? `Unresolved wikilink: ${target}` : "Resolving…"}
87
+ >
88
+ {display}
89
+ </span>
90
+ );
91
+ }
92
+
93
+ return (
94
+ <a
95
+ href={`#/files/${resolved}`}
96
+ onClick={(e) => {
97
+ e.preventDefault();
98
+ setFilePanelOpen(true);
99
+ void openFile(resolved);
100
+ }}
101
+ className="brain-file-link brain-wiki-link"
102
+ title={resolved}
103
+ >
104
+ {display}
105
+ </a>
106
+ );
107
+ }
108
+
109
+ /**
110
+ * Click target for a repo-relative directory reference. Opens the file
111
+ * panel, expands the tree down to the directory, and highlights it so
112
+ * the user sees its contents.
113
+ */
114
+ export function DirLink({ path, children }: { path: string; children?: React.ReactNode }) {
115
+ const openDir = useFileStore((s) => s.openDir);
116
+ const setFilePanelOpen = useUIStore((s) => s.setFilePanelOpen);
117
+ const trimmed = path.replace(/\/+$/, "");
118
+ const display = `${trimmed}/`;
119
+ return (
120
+ <a
121
+ href={`#/files/${display}`}
122
+ onClick={(e) => {
123
+ e.preventDefault();
124
+ setFilePanelOpen(true);
125
+ void openDir(trimmed);
126
+ }}
127
+ className="brain-file-link brain-file-link--dir"
128
+ title={display}
129
+ >
130
+ {children ?? display}
131
+ </a>
132
+ );
133
+ }
134
+
135
+ // Bare-path tokens in prose. Three passes:
136
+ // - files: "notes/projects/foo.md" — ends with .ext
137
+ // - dirs: "notes/projects/post-templates/" — ends with /
138
+ // - wikilinks: "[[slug]]" or "[[slug|Display]]" — obsidian-style
139
+ const BARE_FILE_RE =
140
+ /\b([a-z][a-z0-9_-]*(?:\/[a-z0-9._-]+)+\.[a-z0-9]{1,8})\b/gi;
141
+ const BARE_DIR_RE =
142
+ /\b([a-z][a-z0-9_-]*(?:\/[a-z0-9._-]+)+\/)(?=$|[\s,;:!?)\]])/gi;
143
+ const WIKILINK_RE = /\[\[([^\[\]\n|]+?)(?:\|([^\[\]\n]+?))?\]\]/g;
144
+
145
+ interface PathMatch {
146
+ start: number;
147
+ end: number;
148
+ kind: "file" | "dir" | "wikilink";
149
+ /** For file/dir kinds. */
150
+ path?: string;
151
+ /** Raw target inside the brackets (may include `#anchor`). For wikilink. */
152
+ target?: string;
153
+ /** Display label override (after the pipe). For wikilink. */
154
+ label?: string;
155
+ }
156
+
157
+ function scanBarePaths(text: string): PathMatch[] {
158
+ const out: PathMatch[] = [];
159
+ BARE_FILE_RE.lastIndex = 0;
160
+ let m: RegExpExecArray | null;
161
+ while ((m = BARE_FILE_RE.exec(text)) !== null) {
162
+ if (isInternalRepoPath(m[1])) {
163
+ out.push({
164
+ start: m.index,
165
+ end: BARE_FILE_RE.lastIndex,
166
+ path: m[1],
167
+ kind: "file",
168
+ });
169
+ }
170
+ }
171
+ BARE_DIR_RE.lastIndex = 0;
172
+ while ((m = BARE_DIR_RE.exec(text)) !== null) {
173
+ if (isInternalRepoDir(m[1])) {
174
+ out.push({
175
+ start: m.index,
176
+ end: BARE_DIR_RE.lastIndex,
177
+ path: m[1],
178
+ kind: "dir",
179
+ });
180
+ }
181
+ }
182
+ WIKILINK_RE.lastIndex = 0;
183
+ while ((m = WIKILINK_RE.exec(text)) !== null) {
184
+ out.push({
185
+ start: m.index,
186
+ end: WIKILINK_RE.lastIndex,
187
+ kind: "wikilink",
188
+ target: m[1].trim(),
189
+ label: m[2]?.trim(),
190
+ });
191
+ }
192
+ out.sort((a, b) => a.start - b.start);
193
+
194
+ // Drop overlapping matches — keep the first (earlier) when ranges collide
195
+ const merged: PathMatch[] = [];
196
+ let cursor = 0;
197
+ for (const cand of out) {
198
+ if (cand.start >= cursor) {
199
+ merged.push(cand);
200
+ cursor = cand.end;
201
+ }
202
+ }
203
+ return merged;
204
+ }
205
+
206
+ export function renderBarePathsInText(text: string): React.ReactNode[] {
207
+ const matches = scanBarePaths(text);
208
+ if (matches.length === 0) return [text];
209
+ const nodes: React.ReactNode[] = [];
210
+ let last = 0;
211
+ let key = 0;
212
+ for (const m of matches) {
213
+ if (m.start > last) nodes.push(text.slice(last, m.start));
214
+ if (m.kind === "file" && m.path) {
215
+ nodes.push(
216
+ <FileLink key={`p${key++}`} path={m.path}>
217
+ {m.path}
218
+ </FileLink>
219
+ );
220
+ } else if (m.kind === "dir" && m.path) {
221
+ nodes.push(
222
+ <DirLink key={`p${key++}`} path={m.path.replace(/\/+$/, "")}>
223
+ {m.path}
224
+ </DirLink>
225
+ );
226
+ } else if (m.kind === "wikilink" && m.target) {
227
+ nodes.push(
228
+ <WikiLink key={`p${key++}`} target={m.target} label={m.label} />
229
+ );
230
+ }
231
+ last = m.end;
232
+ }
233
+ if (last < text.length) nodes.push(text.slice(last));
234
+ return nodes;
235
+ }
236
+
237
+ /**
238
+ * Public helper: turn any raw text blob into React nodes with bare-path
239
+ * tokens replaced by FileLink elements. Use this in non-markdown surfaces
240
+ * (tool outputs, streaming logs, plain user text).
241
+ */
242
+ export function linkifyPaths(text: string): React.ReactNode {
243
+ if (!text) return text;
244
+ const parts = renderBarePathsInText(text);
245
+ if (parts.length === 1 && typeof parts[0] === "string") return parts[0];
246
+ return <>{parts}</>;
247
+ }
@@ -0,0 +1,47 @@
1
+ import type { ShareBlockFormat } from "./share-block.js";
2
+
3
+ type ShareSegment = { kind: "share"; format: ShareBlockFormat; title?: string; body: string };
4
+ type TextSegment = { kind: "text"; text: string };
5
+ export type Segment = TextSegment | ShareSegment;
6
+
7
+ const SHARE_BLOCK_RE = /<share\b([^>]*)>([\s\S]*?)<\/share>/gi;
8
+ const VALID_FORMATS: ReadonlySet<ShareBlockFormat> = new Set([
9
+ "image",
10
+ "pdf",
11
+ "text",
12
+ "markdown",
13
+ "richtext",
14
+ ]);
15
+
16
+ function parseShareAttrs(raw: string): { format: ShareBlockFormat; title?: string } {
17
+ const attrs: Record<string, string> = {};
18
+ const re = /(\w+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/g;
19
+ let m: RegExpExecArray | null;
20
+ while ((m = re.exec(raw)) !== null) {
21
+ attrs[m[1].toLowerCase()] = m[2] ?? m[3] ?? m[4] ?? "";
22
+ }
23
+ const format = (attrs.format ?? "image").toLowerCase() as ShareBlockFormat;
24
+ return {
25
+ format: VALID_FORMATS.has(format) ? format : "image",
26
+ title: attrs.title,
27
+ };
28
+ }
29
+
30
+ export function splitShareBlocks(input: string): Segment[] {
31
+ const out: Segment[] = [];
32
+ let last = 0;
33
+ let m: RegExpExecArray | null;
34
+ SHARE_BLOCK_RE.lastIndex = 0;
35
+ while ((m = SHARE_BLOCK_RE.exec(input)) !== null) {
36
+ if (m.index > last) {
37
+ out.push({ kind: "text", text: input.slice(last, m.index) });
38
+ }
39
+ const { format, title } = parseShareAttrs(m[1] ?? "");
40
+ out.push({ kind: "share", format, title, body: m[2].trim() });
41
+ last = SHARE_BLOCK_RE.lastIndex;
42
+ }
43
+ if (last < input.length) {
44
+ out.push({ kind: "text", text: input.slice(last) });
45
+ }
46
+ return out;
47
+ }