@stigmer/react 3.1.3 → 3.1.5

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 (248) hide show
  1. package/composer/SessionComposer.d.ts +31 -0
  2. package/composer/SessionComposer.d.ts.map +1 -1
  3. package/composer/SessionComposer.js +12 -4
  4. package/composer/SessionComposer.js.map +1 -1
  5. package/execution/ArtifactApplyButton.d.ts +25 -0
  6. package/execution/ArtifactApplyButton.d.ts.map +1 -0
  7. package/execution/ArtifactApplyButton.js +21 -0
  8. package/execution/ArtifactApplyButton.js.map +1 -0
  9. package/execution/ArtifactContentBody.d.ts +36 -0
  10. package/execution/ArtifactContentBody.d.ts.map +1 -0
  11. package/execution/ArtifactContentBody.js +62 -0
  12. package/execution/ArtifactContentBody.js.map +1 -0
  13. package/execution/ArtifactDocument.d.ts +48 -0
  14. package/execution/ArtifactDocument.d.ts.map +1 -0
  15. package/execution/ArtifactDocument.js +89 -0
  16. package/execution/ArtifactDocument.js.map +1 -0
  17. package/execution/ArtifactPreviewModal.d.ts +4 -4
  18. package/execution/ArtifactPreviewModal.d.ts.map +1 -1
  19. package/execution/ArtifactPreviewModal.js +15 -142
  20. package/execution/ArtifactPreviewModal.js.map +1 -1
  21. package/execution/ArtifactRow.d.ts +49 -0
  22. package/execution/ArtifactRow.d.ts.map +1 -0
  23. package/execution/ArtifactRow.js +56 -0
  24. package/execution/ArtifactRow.js.map +1 -0
  25. package/execution/ArtifactsWidget.d.ts +6 -9
  26. package/execution/ArtifactsWidget.d.ts.map +1 -1
  27. package/execution/ArtifactsWidget.js +15 -19
  28. package/execution/ArtifactsWidget.js.map +1 -1
  29. package/execution/FileReviewContext.d.ts +15 -1
  30. package/execution/FileReviewContext.d.ts.map +1 -1
  31. package/execution/FileReviewContext.js +20 -1
  32. package/execution/FileReviewContext.js.map +1 -1
  33. package/execution/MessageThread.d.ts +65 -2
  34. package/execution/MessageThread.d.ts.map +1 -1
  35. package/execution/MessageThread.js +42 -23
  36. package/execution/MessageThread.js.map +1 -1
  37. package/execution/TodoCard.d.ts +8 -0
  38. package/execution/TodoCard.d.ts.map +1 -1
  39. package/execution/TodoCard.js +6 -4
  40. package/execution/TodoCard.js.map +1 -1
  41. package/execution/TodoList.d.ts +20 -1
  42. package/execution/TodoList.d.ts.map +1 -1
  43. package/execution/TodoList.js +9 -6
  44. package/execution/TodoList.js.map +1 -1
  45. package/execution/ToolCallDetail.d.ts +3 -2
  46. package/execution/ToolCallDetail.d.ts.map +1 -1
  47. package/execution/ToolCallDetail.js +49 -16
  48. package/execution/ToolCallDetail.js.map +1 -1
  49. package/execution/file-review-status.d.ts +15 -0
  50. package/execution/file-review-status.d.ts.map +1 -1
  51. package/execution/file-review-status.js +22 -0
  52. package/execution/file-review-status.js.map +1 -1
  53. package/execution/index.d.ts +13 -7
  54. package/execution/index.d.ts.map +1 -1
  55. package/execution/index.js +7 -4
  56. package/execution/index.js.map +1 -1
  57. package/execution/useArtifactContent.d.ts +6 -1
  58. package/execution/useArtifactContent.d.ts.map +1 -1
  59. package/execution/useArtifactContent.js +7 -2
  60. package/execution/useArtifactContent.js.map +1 -1
  61. package/execution/useArtifactInspection.d.ts +85 -0
  62. package/execution/useArtifactInspection.d.ts.map +1 -0
  63. package/execution/useArtifactInspection.js +143 -0
  64. package/execution/useArtifactInspection.js.map +1 -0
  65. package/execution/useCreateAgentExecution.d.ts +11 -0
  66. package/execution/useCreateAgentExecution.d.ts.map +1 -1
  67. package/execution/useCreateAgentExecution.js +1 -0
  68. package/execution/useCreateAgentExecution.js.map +1 -1
  69. package/github/useGitHubFileReader.d.ts +4 -2
  70. package/github/useGitHubFileReader.d.ts.map +1 -1
  71. package/github/useGitHubFileReader.js +16 -3
  72. package/github/useGitHubFileReader.js.map +1 -1
  73. package/github/useGitHubTreeLister.d.ts +2 -1
  74. package/github/useGitHubTreeLister.d.ts.map +1 -1
  75. package/github/useGitHubTreeLister.js +7 -3
  76. package/github/useGitHubTreeLister.js.map +1 -1
  77. package/index.d.ts +4 -4
  78. package/index.d.ts.map +1 -1
  79. package/index.js +3 -3
  80. package/index.js.map +1 -1
  81. package/internal/MermaidDiagram.d.ts +47 -0
  82. package/internal/MermaidDiagram.d.ts.map +1 -0
  83. package/internal/MermaidDiagram.js +118 -0
  84. package/internal/MermaidDiagram.js.map +1 -0
  85. package/internal/VirtualizedThread.d.ts +3 -2
  86. package/internal/VirtualizedThread.d.ts.map +1 -1
  87. package/internal/VirtualizedThread.js +3 -2
  88. package/internal/VirtualizedThread.js.map +1 -1
  89. package/internal/markdown-components.d.ts.map +1 -1
  90. package/internal/markdown-components.js +34 -0
  91. package/internal/markdown-components.js.map +1 -1
  92. package/internal/mermaid-loader.d.ts +26 -0
  93. package/internal/mermaid-loader.d.ts.map +1 -0
  94. package/internal/mermaid-loader.js +34 -0
  95. package/internal/mermaid-loader.js.map +1 -0
  96. package/internal/mermaid-theme-css.d.ts +41 -0
  97. package/internal/mermaid-theme-css.d.ts.map +1 -0
  98. package/internal/mermaid-theme-css.js +179 -0
  99. package/internal/mermaid-theme-css.js.map +1 -0
  100. package/internal/slug.d.ts +9 -5
  101. package/internal/slug.d.ts.map +1 -1
  102. package/internal/slug.js +10 -6
  103. package/internal/slug.js.map +1 -1
  104. package/package.json +5 -4
  105. package/session/SessionViewer.d.ts +11 -1
  106. package/session/SessionViewer.d.ts.map +1 -1
  107. package/session/SessionViewer.js +101 -21
  108. package/session/SessionViewer.js.map +1 -1
  109. package/session/artifact-document.d.ts +13 -0
  110. package/session/artifact-document.d.ts.map +1 -0
  111. package/session/artifact-document.js +22 -0
  112. package/session/artifact-document.js.map +1 -0
  113. package/session/facets/ArtifactsTab.d.ts +30 -4
  114. package/session/facets/ArtifactsTab.d.ts.map +1 -1
  115. package/session/facets/ArtifactsTab.js +32 -10
  116. package/session/facets/ArtifactsTab.js.map +1 -1
  117. package/session/index.d.ts +3 -1
  118. package/session/index.d.ts.map +1 -1
  119. package/session/index.js +5 -1
  120. package/session/index.js.map +1 -1
  121. package/session/useSessionArtifacts.d.ts +17 -0
  122. package/session/useSessionArtifacts.d.ts.map +1 -1
  123. package/session/useSessionArtifacts.js +16 -5
  124. package/session/useSessionArtifacts.js.map +1 -1
  125. package/session/useSessionConversation.d.ts +17 -1
  126. package/session/useSessionConversation.d.ts.map +1 -1
  127. package/session/useSessionConversation.js +32 -4
  128. package/session/useSessionConversation.js.map +1 -1
  129. package/session/useSessionPageFlow.d.ts.map +1 -1
  130. package/session/useSessionPageFlow.js +1 -0
  131. package/session/useSessionPageFlow.js.map +1 -1
  132. package/session/useSessionPanel.d.ts +20 -0
  133. package/session/useSessionPanel.d.ts.map +1 -1
  134. package/session/useSessionPanel.js +13 -0
  135. package/session/useSessionPanel.js.map +1 -1
  136. package/session/useSessionRailViews.d.ts +13 -1
  137. package/session/useSessionRailViews.d.ts.map +1 -1
  138. package/session/useSessionRailViews.js +5 -3
  139. package/session/useSessionRailViews.js.map +1 -1
  140. package/session/useWorkspaceReadRefs.d.ts +45 -0
  141. package/session/useWorkspaceReadRefs.d.ts.map +1 -0
  142. package/session/useWorkspaceReadRefs.js +104 -0
  143. package/session/useWorkspaceReadRefs.js.map +1 -0
  144. package/src/composer/SessionComposer.tsx +69 -1
  145. package/src/composer/__tests__/SessionComposer-editing.test.tsx +128 -0
  146. package/src/execution/ArtifactApplyButton.tsx +79 -0
  147. package/src/execution/ArtifactContentBody.tsx +255 -0
  148. package/src/execution/ArtifactDocument.tsx +344 -0
  149. package/src/execution/ArtifactPreviewModal.tsx +39 -405
  150. package/src/execution/ArtifactRow.tsx +165 -0
  151. package/src/execution/ArtifactsWidget.tsx +24 -33
  152. package/src/execution/FileReviewContext.ts +27 -1
  153. package/src/execution/MessageThread.tsx +110 -22
  154. package/src/execution/TodoCard.tsx +20 -4
  155. package/src/execution/TodoList.tsx +26 -4
  156. package/src/execution/ToolCallDetail.tsx +69 -16
  157. package/src/execution/__tests__/ArtifactContentBody.test.tsx +131 -0
  158. package/src/execution/__tests__/ArtifactDocument.test.tsx +100 -0
  159. package/src/execution/__tests__/ArtifactRow.test.tsx +139 -0
  160. package/src/execution/__tests__/ArtifactsWidget.test.tsx +78 -0
  161. package/src/execution/__tests__/ToolCallDetail.test.tsx +188 -2
  162. package/src/execution/__tests__/file-review-status.test.ts +47 -0
  163. package/src/execution/__tests__/message-entry.test.tsx +50 -2
  164. package/src/execution/__tests__/message-thread-slots.test.tsx +463 -0
  165. package/src/execution/__tests__/useArtifactInspection.test.tsx +151 -0
  166. package/src/execution/__tests__/useCreateAgentExecution.test.tsx +35 -0
  167. package/src/execution/file-review-status.ts +24 -0
  168. package/src/execution/index.ts +27 -5
  169. package/src/execution/useArtifactContent.ts +7 -0
  170. package/src/execution/useArtifactInspection.ts +252 -0
  171. package/src/execution/useCreateAgentExecution.ts +12 -0
  172. package/src/github/__tests__/useGitHubFileReader.test.ts +35 -2
  173. package/src/github/__tests__/useGitHubTreeLister.test.ts +14 -0
  174. package/src/github/useGitHubFileReader.ts +19 -6
  175. package/src/github/useGitHubTreeLister.ts +7 -3
  176. package/src/index.ts +15 -2
  177. package/src/internal/MermaidDiagram.tsx +164 -0
  178. package/src/internal/VirtualizedThread.tsx +5 -1
  179. package/src/internal/__tests__/a11y/mermaid-theme.a11y.test.tsx +122 -0
  180. package/src/internal/__tests__/markdown-components.test.tsx +93 -1
  181. package/src/internal/__tests__/mermaid-diagram.test.tsx +282 -0
  182. package/src/internal/markdown-components.tsx +36 -1
  183. package/src/internal/mermaid-loader.ts +44 -0
  184. package/src/internal/mermaid-theme-css.ts +178 -0
  185. package/src/internal/slug.ts +10 -6
  186. package/src/session/SessionViewer.tsx +204 -35
  187. package/src/session/__tests__/SessionViewer-artifactTabs.test.tsx +210 -0
  188. package/src/session/__tests__/SessionViewer-editResubmit.test.tsx +233 -0
  189. package/src/session/__tests__/useSessionArtifacts.test.ts +57 -0
  190. package/src/session/__tests__/useSessionConversation.test.tsx +172 -1
  191. package/src/session/__tests__/useSessionPanel.test.ts +70 -1
  192. package/src/session/__tests__/useWorkspaceReadRefs.test.ts +245 -0
  193. package/src/session/artifact-document.ts +23 -0
  194. package/src/session/facets/ArtifactsTab.tsx +66 -18
  195. package/src/session/facets/__tests__/ArtifactsTab.test.tsx +111 -47
  196. package/src/session/index.ts +7 -1
  197. package/src/session/useSessionArtifacts.ts +16 -5
  198. package/src/session/useSessionConversation.ts +48 -6
  199. package/src/session/useSessionPageFlow.ts +1 -0
  200. package/src/session/useSessionPanel.ts +41 -0
  201. package/src/session/useSessionRailViews.tsx +21 -1
  202. package/src/session/useWorkspaceReadRefs.ts +118 -0
  203. package/src/workspace/FileViewer.tsx +144 -38
  204. package/src/workspace/WorkspaceFileReader.ts +21 -0
  205. package/src/workspace/__tests__/FileViewer.test.tsx +138 -3
  206. package/src/workspace/__tests__/useWorkspaceFileContent.test.tsx +20 -0
  207. package/src/workspace/__tests__/workspaceListingCache.test.ts +114 -0
  208. package/src/workspace/index.ts +4 -1
  209. package/src/workspace/useWorkspaceEntries.ts +11 -0
  210. package/src/workspace/useWorkspaceFileContent.ts +4 -1
  211. package/src/workspace/useWorkspaceFileSearch.ts +8 -3
  212. package/src/workspace/useWorkspaceFiles.ts +3 -2
  213. package/src/workspace/workspaceListingCache.ts +21 -7
  214. package/styles.css +1 -1
  215. package/workspace/FileViewer.d.ts +1 -1
  216. package/workspace/FileViewer.d.ts.map +1 -1
  217. package/workspace/FileViewer.js +55 -5
  218. package/workspace/FileViewer.js.map +1 -1
  219. package/workspace/WorkspaceFileReader.d.ts +17 -0
  220. package/workspace/WorkspaceFileReader.d.ts.map +1 -1
  221. package/workspace/WorkspaceFileReader.js +18 -0
  222. package/workspace/WorkspaceFileReader.js.map +1 -1
  223. package/workspace/index.d.ts +1 -1
  224. package/workspace/index.d.ts.map +1 -1
  225. package/workspace/index.js +1 -1
  226. package/workspace/index.js.map +1 -1
  227. package/workspace/useWorkspaceEntries.d.ts +11 -0
  228. package/workspace/useWorkspaceEntries.d.ts.map +1 -1
  229. package/workspace/useWorkspaceEntries.js.map +1 -1
  230. package/workspace/useWorkspaceFileContent.d.ts.map +1 -1
  231. package/workspace/useWorkspaceFileContent.js +4 -1
  232. package/workspace/useWorkspaceFileContent.js.map +1 -1
  233. package/workspace/useWorkspaceFileSearch.d.ts.map +1 -1
  234. package/workspace/useWorkspaceFileSearch.js +8 -3
  235. package/workspace/useWorkspaceFileSearch.js.map +1 -1
  236. package/workspace/useWorkspaceFiles.d.ts +2 -1
  237. package/workspace/useWorkspaceFiles.d.ts.map +1 -1
  238. package/workspace/useWorkspaceFiles.js +3 -2
  239. package/workspace/useWorkspaceFiles.js.map +1 -1
  240. package/workspace/workspaceListingCache.d.ts +1 -1
  241. package/workspace/workspaceListingCache.d.ts.map +1 -1
  242. package/workspace/workspaceListingCache.js +20 -7
  243. package/workspace/workspaceListingCache.js.map +1 -1
  244. package/execution/ArtifactCard.d.ts +0 -81
  245. package/execution/ArtifactCard.d.ts.map +0 -1
  246. package/execution/ArtifactCard.js +0 -130
  247. package/execution/ArtifactCard.js.map +0 -1
  248. package/src/execution/ArtifactCard.tsx +0 -320
@@ -0,0 +1,282 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { render, cleanup, waitFor, act } from "@testing-library/react";
3
+ import type { ReactNode } from "react";
4
+ import { MermaidDiagram } from "../MermaidDiagram";
5
+ import { loadMermaid, type MermaidModule } from "../mermaid-loader";
6
+ import { MERMAID_THEME_CSS } from "../mermaid-theme-css";
7
+ import { ColorModeContext, type ResolvedColorMode } from "../../color-mode";
8
+
9
+ // The loader is the component's only side-effectful dependency; mocking it
10
+ // (not the `mermaid` package) exercises the real degradation contract:
11
+ // a rejected load must look exactly like "library not installed".
12
+ vi.mock("../mermaid-loader", () => ({
13
+ loadMermaid: vi.fn(),
14
+ }));
15
+
16
+ // Partial-mock streamdown so tests can drive the incomplete-fence signal
17
+ // without spinning up a full Streamdown render.
18
+ vi.mock("streamdown", async (importOriginal) => ({
19
+ ...(await importOriginal<typeof import("streamdown")>()),
20
+ useIsCodeFenceIncomplete: vi.fn(() => false),
21
+ }));
22
+
23
+ const { useIsCodeFenceIncomplete } = await import("streamdown");
24
+
25
+ const loadMermaidMock = vi.mocked(loadMermaid);
26
+ const incompleteFenceMock = vi.mocked(useIsCodeFenceIncomplete);
27
+
28
+ /** Builds a mermaid module stub whose render resolves with the given SVG. */
29
+ function stubMermaid(svg = "<svg><g>diagram</g></svg>") {
30
+ const mermaid = {
31
+ initialize: vi.fn(),
32
+ render: vi.fn().mockResolvedValue({ svg }),
33
+ };
34
+ loadMermaidMock.mockResolvedValue(mermaid as unknown as MermaidModule);
35
+ return mermaid;
36
+ }
37
+
38
+ function renderDiagram(
39
+ chart: string,
40
+ { colorMode = "light" as ResolvedColorMode } = {},
41
+ ) {
42
+ return render(
43
+ <ColorModeContext.Provider value={colorMode}>
44
+ <MermaidDiagram chart={chart} />
45
+ </ColorModeContext.Provider>,
46
+ );
47
+ }
48
+
49
+ function queryDiagram(container: HTMLElement) {
50
+ return container.querySelector('[role="img"][aria-label="Mermaid diagram"]');
51
+ }
52
+
53
+ beforeEach(() => {
54
+ loadMermaidMock.mockReset();
55
+ incompleteFenceMock.mockReset();
56
+ incompleteFenceMock.mockReturnValue(false);
57
+ });
58
+
59
+ afterEach(cleanup);
60
+
61
+ const CHART = "flowchart LR\n A --> B";
62
+
63
+ describe("MermaidDiagram — successful render", () => {
64
+ it("renders the SVG in an accessible container", async () => {
65
+ stubMermaid("<svg data-diagram><g>ok</g></svg>");
66
+ const { container } = renderDiagram(CHART);
67
+
68
+ await waitFor(() => expect(queryDiagram(container)).not.toBeNull());
69
+ expect(queryDiagram(container)!.querySelector("svg[data-diagram]")).not.toBeNull();
70
+ // The source fallback is gone once the diagram is up.
71
+ expect(container.querySelector("pre")).toBeNull();
72
+ });
73
+
74
+ it("shows the source as a code block while the render is pending", () => {
75
+ // A load that never settles = the loading window.
76
+ loadMermaidMock.mockReturnValue(new Promise(() => {}));
77
+ const { container } = renderDiagram(CHART);
78
+
79
+ const code = container.querySelector("pre code");
80
+ expect(code).not.toBeNull();
81
+ expect(code!.textContent).toBe(CHART);
82
+ expect(queryDiagram(container)).toBeNull();
83
+ });
84
+
85
+ it("trims the trailing newline the fence body carries before rendering", async () => {
86
+ const mermaid = stubMermaid();
87
+ renderDiagram("flowchart LR\n A --> B\n");
88
+
89
+ await waitFor(() => expect(mermaid.render).toHaveBeenCalled());
90
+ expect(mermaid.render).toHaveBeenCalledWith(
91
+ expect.any(String),
92
+ "flowchart LR\n A --> B",
93
+ );
94
+ });
95
+ });
96
+
97
+ describe("MermaidDiagram — security and theming configuration", () => {
98
+ it("configures mermaid with securityLevel strict (agent output is untrusted)", async () => {
99
+ const mermaid = stubMermaid();
100
+ renderDiagram(CHART);
101
+
102
+ await waitFor(() => expect(mermaid.initialize).toHaveBeenCalled());
103
+ expect(mermaid.initialize).toHaveBeenCalledWith(
104
+ expect.objectContaining({ securityLevel: "strict", startOnLoad: false }),
105
+ );
106
+ });
107
+
108
+ it("injects the token-driven themeCSS so interiors track --stgm-* tokens", async () => {
109
+ const mermaid = stubMermaid();
110
+ renderDiagram(CHART);
111
+
112
+ await waitFor(() => expect(mermaid.initialize).toHaveBeenCalled());
113
+ // The interior palette rides on themeCSS (var(--stgm-*)), layered over the
114
+ // built-in default/dark base theme. The browser-mode a11y suite verifies
115
+ // the CSS actually resolves; here we lock the wiring itself.
116
+ expect(mermaid.initialize).toHaveBeenCalledWith(
117
+ expect.objectContaining({ themeCSS: MERMAID_THEME_CSS }),
118
+ );
119
+ });
120
+
121
+ it.each([
122
+ ["light", "default"],
123
+ ["dark", "dark"],
124
+ ] as const)("selects the %s theme for %s color mode", async (mode, theme) => {
125
+ const mermaid = stubMermaid();
126
+ renderDiagram(CHART, { colorMode: mode });
127
+
128
+ await waitFor(() => expect(mermaid.initialize).toHaveBeenCalled());
129
+ expect(mermaid.initialize).toHaveBeenCalledWith(
130
+ expect.objectContaining({ theme }),
131
+ );
132
+ });
133
+
134
+ it("re-renders the diagram when the color mode changes", async () => {
135
+ const mermaid = stubMermaid();
136
+ const { container, rerender } = render(
137
+ <ColorModeContext.Provider value="light">
138
+ <MermaidDiagram chart={CHART} />
139
+ </ColorModeContext.Provider>,
140
+ );
141
+ await waitFor(() => expect(queryDiagram(container)).not.toBeNull());
142
+
143
+ rerender(
144
+ <ColorModeContext.Provider value="dark">
145
+ <MermaidDiagram chart={CHART} />
146
+ </ColorModeContext.Provider>,
147
+ );
148
+
149
+ await waitFor(() => expect(mermaid.render).toHaveBeenCalledTimes(2));
150
+ expect(mermaid.initialize).toHaveBeenLastCalledWith(
151
+ expect.objectContaining({ theme: "dark" }),
152
+ );
153
+ });
154
+ });
155
+
156
+ describe("MermaidDiagram — failure degrades to the code block", () => {
157
+ it("falls back with an announced note when the diagram source is invalid", async () => {
158
+ loadMermaidMock.mockResolvedValue({
159
+ initialize: vi.fn(),
160
+ render: vi.fn().mockRejectedValue(new Error("Parse error on line 2")),
161
+ } as unknown as MermaidModule);
162
+ const { container } = renderDiagram("flowchart LR\n A --> ");
163
+
164
+ await waitFor(() =>
165
+ expect(container.querySelector('[role="status"]')).not.toBeNull(),
166
+ );
167
+ expect(container.querySelector("pre code")!.textContent).toContain(
168
+ "A -->",
169
+ );
170
+ expect(container.querySelector('[role="status"]')!.textContent).toContain(
171
+ "Parse error on line 2",
172
+ );
173
+ expect(queryDiagram(container)).toBeNull();
174
+ });
175
+
176
+ it("falls back when the mermaid library cannot be loaded (optional peer absent)", async () => {
177
+ loadMermaidMock.mockRejectedValue(
178
+ new Error("Cannot find module 'mermaid'"),
179
+ );
180
+ const { container } = renderDiagram(CHART);
181
+
182
+ await waitFor(() =>
183
+ expect(container.querySelector('[role="status"]')).not.toBeNull(),
184
+ );
185
+ expect(container.querySelector("pre code")!.textContent).toBe(CHART);
186
+ expect(queryDiagram(container)).toBeNull();
187
+ });
188
+
189
+ it("does not attempt to render an empty fence", () => {
190
+ stubMermaid();
191
+ const { container } = renderDiagram(" \n ");
192
+
193
+ expect(loadMermaidMock).not.toHaveBeenCalled();
194
+ expect(queryDiagram(container)).toBeNull();
195
+ });
196
+ });
197
+
198
+ describe("MermaidDiagram — streaming deferral", () => {
199
+ it("defers rendering while the fence is still streaming", () => {
200
+ stubMermaid();
201
+ incompleteFenceMock.mockReturnValue(true);
202
+ const { container } = renderDiagram(CHART);
203
+
204
+ expect(loadMermaidMock).not.toHaveBeenCalled();
205
+ const code = container.querySelector("pre code");
206
+ expect(code).not.toBeNull();
207
+ expect(code!.textContent).toBe(CHART);
208
+ });
209
+
210
+ it("renders the diagram once the fence closes", async () => {
211
+ stubMermaid();
212
+ incompleteFenceMock.mockReturnValue(true);
213
+ const { container, rerender } = renderDiagram(CHART);
214
+ expect(queryDiagram(container)).toBeNull();
215
+
216
+ incompleteFenceMock.mockReturnValue(false);
217
+ rerender(
218
+ <ColorModeContext.Provider value="light">
219
+ <MermaidDiagram chart={CHART} />
220
+ </ColorModeContext.Provider>,
221
+ );
222
+
223
+ await waitFor(() => expect(queryDiagram(container)).not.toBeNull());
224
+ });
225
+ });
226
+
227
+ describe("MermaidDiagram — stale-render races", () => {
228
+ it("discards a render that resolves after the chart has changed", async () => {
229
+ const initialize = vi.fn();
230
+ let resolveFirst!: (result: { svg: string }) => void;
231
+ const renderMock = vi
232
+ .fn()
233
+ .mockImplementationOnce(
234
+ () => new Promise((resolve) => (resolveFirst = resolve)),
235
+ )
236
+ .mockResolvedValue({ svg: "<svg data-chart='second'></svg>" });
237
+ loadMermaidMock.mockResolvedValue({
238
+ initialize,
239
+ render: renderMock,
240
+ } as unknown as MermaidModule);
241
+
242
+ const { container, rerender } = renderDiagram("flowchart LR\n A --> B");
243
+ await waitFor(() => expect(renderMock).toHaveBeenCalledTimes(1));
244
+
245
+ rerender(
246
+ <ColorModeContext.Provider value="light">
247
+ <MermaidDiagram chart={"flowchart LR\n C --> D"} />
248
+ </ColorModeContext.Provider>,
249
+ );
250
+ await waitFor(() =>
251
+ expect(
252
+ container.querySelector("svg[data-chart='second']"),
253
+ ).not.toBeNull(),
254
+ );
255
+
256
+ // The first (now stale) render settles late — it must not clobber the
257
+ // second chart's SVG.
258
+ act(() => resolveFirst({ svg: "<svg data-chart='first'></svg>" }));
259
+ expect(container.querySelector("svg[data-chart='first']")).toBeNull();
260
+ expect(container.querySelector("svg[data-chart='second']")).not.toBeNull();
261
+ });
262
+
263
+ it("ignores a render that settles after unmount", async () => {
264
+ let resolveRender!: (result: { svg: string }) => void;
265
+ loadMermaidMock.mockResolvedValue({
266
+ initialize: vi.fn(),
267
+ render: vi.fn().mockReturnValue(
268
+ new Promise((resolve) => (resolveRender = resolve)),
269
+ ),
270
+ } as unknown as MermaidModule);
271
+
272
+ const { unmount } = renderDiagram(CHART);
273
+ await waitFor(() => expect(loadMermaidMock).toHaveBeenCalled());
274
+ unmount();
275
+
276
+ // Settling after unmount must not warn or throw (React state update on
277
+ // an unmounted component).
278
+ expect(() =>
279
+ act(() => resolveRender({ svg: "<svg></svg>" })),
280
+ ).not.toThrow();
281
+ });
282
+ });
@@ -1,10 +1,12 @@
1
- import type { ComponentProps, JSX } from "react";
1
+ import { isValidElement, type ComponentProps, type JSX, type ReactNode } from "react";
2
2
  import type { Components } from "react-markdown";
3
3
  import remarkGfm from "remark-gfm";
4
4
  import { cn } from "@stigmer/theme";
5
5
  import { highlightToReact } from "./code-highlight.js";
6
+ import { MermaidDiagram } from "./MermaidDiagram.js";
6
7
 
7
8
  const LANGUAGE_CLASS_PREFIX = "language-";
9
+ const MERMAID_LANGUAGE_CLASS = "language-mermaid";
8
10
 
9
11
  type MdProps<T extends keyof JSX.IntrinsicElements> = ComponentProps<T>;
10
12
 
@@ -119,6 +121,30 @@ export function extractLeadingH1(markdown: string): {
119
121
  return { title: match[1], body: trimmed.slice(match[0].length) };
120
122
  }
121
123
 
124
+ /**
125
+ * Returns the mermaid source when a `pre` element's children are a single
126
+ * fenced code block explicitly tagged `mermaid`, or `null` otherwise.
127
+ *
128
+ * Both renderers (Streamdown for chat, react-markdown for artifacts/skills)
129
+ * hand `pre` exactly one `<code>` child whose `className` carries the fence's
130
+ * info string as `language-*` and whose children are the raw source text.
131
+ * Anything that deviates from that shape — a different language, a missing
132
+ * tag, non-string children — is not a mermaid fence and keeps the ordinary
133
+ * code-block rendering. Only the explicit `mermaid` tag qualifies:
134
+ * inspecting fence bodies to guess diagram intent is the kind of fuzzy
135
+ * heuristic this codebase avoids.
136
+ */
137
+ function extractMermaidSource(children: ReactNode): string | null {
138
+ if (!isValidElement(children)) return null;
139
+ const { className, children: code } = children.props as {
140
+ className?: unknown;
141
+ children?: unknown;
142
+ };
143
+ if (typeof className !== "string") return null;
144
+ if (!className.split(/\s+/).includes(MERMAID_LANGUAGE_CLASS)) return null;
145
+ return typeof code === "string" ? code : null;
146
+ }
147
+
122
148
  /**
123
149
  * Styled react-markdown component overrides for SDK markdown surfaces.
124
150
  *
@@ -208,6 +234,15 @@ export const MARKDOWN_COMPONENTS: Components = {
208
234
  },
209
235
 
210
236
  pre({ children, ...props }: MdProps<"pre">) {
237
+ // A ```mermaid fence renders as a diagram, not a code block. The check
238
+ // lives here (not in the `code` override) because the diagram must
239
+ // replace the <pre> wrapper too — a block-level diagram container inside
240
+ // <pre> is invalid HTML and would inherit code-block chrome.
241
+ const mermaidSource = extractMermaidSource(children);
242
+ if (mermaidSource !== null) {
243
+ return <MermaidDiagram chart={mermaidSource} />;
244
+ }
245
+
211
246
  return (
212
247
  <pre
213
248
  className="mb-3 last:mb-0 overflow-x-auto rounded-md bg-muted p-3"
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Lazy loader for the `mermaid` library, isolated in its own module — the
3
+ * same seam pattern as `workflow/layout/elk-layout-engine.ts` — so
4
+ * {@link file://./MermaidDiagram.tsx} stays a pure presentation component and
5
+ * tests can mock the load outcome (resolve, reject) without touching module
6
+ * registries.
7
+ *
8
+ * `mermaid` is an optional peer dependency (DD-013): it is large, so it must
9
+ * never sit on the synchronous bundle path, and a host that has not installed
10
+ * it must degrade gracefully (the caller falls back to a code block).
11
+ */
12
+
13
+ /**
14
+ * The mermaid API surface the SDK consumes, typed from the package itself so
15
+ * it stays in lockstep with the installed version.
16
+ */
17
+ export type MermaidModule = typeof import("mermaid").default;
18
+
19
+ /**
20
+ * Holds the in-flight import promise so concurrent diagrams share one load.
21
+ * A failed import clears the cache: rejections are often transient (a
22
+ * chunk-load hiccup mid-deploy), and caching one would break every diagram
23
+ * for the rest of the session.
24
+ */
25
+ let mermaidImport: Promise<MermaidModule> | null = null;
26
+
27
+ /**
28
+ * Dynamically imports `mermaid`, sharing one in-flight import across callers.
29
+ *
30
+ * @returns The mermaid default export.
31
+ * @throws If `mermaid` cannot be imported (not installed, or the async chunk
32
+ * failed to load). Callers treat this as "diagrams unavailable" and render
33
+ * the fenced source as a plain code block.
34
+ */
35
+ export function loadMermaid(): Promise<MermaidModule> {
36
+ mermaidImport ??= import("mermaid").then(
37
+ (module) => module.default,
38
+ (error: unknown) => {
39
+ mermaidImport = null;
40
+ throw error;
41
+ },
42
+ );
43
+ return mermaidImport;
44
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Token-driven `themeCSS` for Mermaid diagram interiors.
3
+ *
4
+ * Mermaid's built-in `default`/`dark` themes color a diagram's SVG interior
5
+ * with a fixed palette that ignores the host's `--stgm-*` preset (Corporate,
6
+ * Fintech, ...). The obvious fix — feeding resolved tokens into Mermaid's
7
+ * `themeVariables` — is blocked: our tokens are authored in `oklch()`, and
8
+ * Mermaid's color engine (`khroma`) parses only hex/rgb/hsl/keyword, so it
9
+ * throws or NaNs on `oklch(...)`. (Mermaid also gates every `themeVariables`
10
+ * value through the allowlist `^[\d "#%(),.;A-Za-z]+$`, which excludes `-` and
11
+ * would blank any `var(--...)`.)
12
+ *
13
+ * Instead we hand Mermaid this CSS via `initialize({ themeCSS })`. Mermaid
14
+ * injects it into the diagram's own `<style>` block, scoped under the render
15
+ * id (`#stgm-mermaid-N`), *after* the theme palette it computes. Because every
16
+ * selector in that block is id-prefixed, our rules and the palette's rules sit
17
+ * at equal specificity, so ours win by source order — and `!important` makes
18
+ * that bulletproof against the few palette rules that are themselves important
19
+ * or inlined. Crucially, `themeCSS` never passes through khroma, so the
20
+ * `oklch()`-backed `var(--stgm-*)` references flow straight to the browser,
21
+ * which resolves them against the `.stgm` scope the SVG lives in. That is why
22
+ * interiors track the active preset *and* color mode live, with no JS color
23
+ * reading and no re-render on theme change.
24
+ *
25
+ * `initialize({ theme })` still selects the built-in `default`/`dark` theme as
26
+ * the base, so any element or diagram type we do not target here degrades to
27
+ * Mermaid's stock palette — never a blank or mis-colored diagram.
28
+ *
29
+ * Palette intent (subtle surfaces + brand accents — legibility over brand
30
+ * saturation): node/actor bodies sit on a muted surface with quiet
31
+ * token-colored borders and edges; labels use the standard foreground; the
32
+ * brand `--stgm-primary` is reserved for structural headers (subgraph/cluster
33
+ * titles, sequence loop/section titles, the sequence-number badge).
34
+ *
35
+ * Coverage is the two families agents emit overwhelmingly — flowchart and
36
+ * sequence — plus the shared marker/edge-label chrome. Both label render paths
37
+ * are handled: SVG `text`/`tspan` (via `fill`) and HTML labels inside
38
+ * `foreignObject` (via `color`).
39
+ */
40
+ export const MERMAID_THEME_CSS = `
41
+ /* Shared: edge arrowheads / markers */
42
+ .marker {
43
+ fill: var(--stgm-border) !important;
44
+ stroke: var(--stgm-border) !important;
45
+ }
46
+ .marker.cross {
47
+ stroke: var(--stgm-border) !important;
48
+ }
49
+
50
+ /* Flowchart: node bodies */
51
+ .node rect,
52
+ .node circle,
53
+ .node ellipse,
54
+ .node polygon,
55
+ .node path {
56
+ fill: var(--stgm-muted) !important;
57
+ stroke: var(--stgm-border) !important;
58
+ }
59
+ .node .neo-node {
60
+ stroke: var(--stgm-border) !important;
61
+ }
62
+ .node .katex path {
63
+ fill: var(--stgm-foreground) !important;
64
+ stroke: var(--stgm-foreground) !important;
65
+ }
66
+
67
+ /* Flowchart: node labels (SVG text and HTML spans) */
68
+ .label {
69
+ color: var(--stgm-foreground) !important;
70
+ }
71
+ .label text,
72
+ .label span {
73
+ fill: var(--stgm-foreground) !important;
74
+ color: var(--stgm-foreground) !important;
75
+ }
76
+
77
+ /* Flowchart: edges */
78
+ .edgePath .path,
79
+ .flowchart-link {
80
+ stroke: var(--stgm-border) !important;
81
+ }
82
+ .arrowheadPath,
83
+ .root .anchor path {
84
+ fill: var(--stgm-border) !important;
85
+ stroke: var(--stgm-border) !important;
86
+ }
87
+
88
+ /* Flowchart: edge labels sit on the diagram surface for legibility */
89
+ .edgeLabel,
90
+ .edgeLabel p,
91
+ .edgeLabel rect,
92
+ .labelBkg {
93
+ background-color: var(--stgm-card) !important;
94
+ fill: var(--stgm-card) !important;
95
+ }
96
+
97
+ /* Flowchart: subgraph clusters — muted surface, brand-accent title */
98
+ .cluster rect {
99
+ fill: var(--stgm-muted-subtle) !important;
100
+ stroke: var(--stgm-border) !important;
101
+ }
102
+ .cluster-label text,
103
+ .cluster text {
104
+ fill: var(--stgm-primary) !important;
105
+ }
106
+ .cluster-label span,
107
+ .cluster span {
108
+ color: var(--stgm-primary) !important;
109
+ }
110
+
111
+ /* Sequence: actors and lifelines */
112
+ .actor {
113
+ fill: var(--stgm-muted) !important;
114
+ stroke: var(--stgm-border) !important;
115
+ }
116
+ text.actor > tspan {
117
+ fill: var(--stgm-foreground) !important;
118
+ }
119
+ .actor-line {
120
+ stroke: var(--stgm-border) !important;
121
+ }
122
+
123
+ /* Sequence: messages */
124
+ .messageLine0,
125
+ .messageLine1 {
126
+ stroke: var(--stgm-border) !important;
127
+ }
128
+ [id$="-arrowhead"] path,
129
+ [id$="-crosshead"] path {
130
+ fill: var(--stgm-border) !important;
131
+ stroke: var(--stgm-border) !important;
132
+ }
133
+ .messageText {
134
+ fill: var(--stgm-foreground) !important;
135
+ }
136
+
137
+ /* Sequence: number badge — brand accent with its paired foreground */
138
+ [id$="-sequencenumber"] {
139
+ fill: var(--stgm-primary) !important;
140
+ }
141
+ .sequenceNumber {
142
+ fill: var(--stgm-primary-foreground) !important;
143
+ }
144
+
145
+ /* Sequence: alt/opt/loop boxes and labels */
146
+ .labelBox {
147
+ fill: var(--stgm-muted-subtle) !important;
148
+ stroke: var(--stgm-border) !important;
149
+ }
150
+ .labelText,
151
+ .labelText > tspan {
152
+ fill: var(--stgm-foreground) !important;
153
+ }
154
+ .loopText,
155
+ .loopText > tspan,
156
+ .sectionTitle,
157
+ .sectionTitle > tspan {
158
+ fill: var(--stgm-primary) !important;
159
+ }
160
+ .loopLine {
161
+ stroke: var(--stgm-border) !important;
162
+ }
163
+
164
+ /* Sequence: notes and activations */
165
+ .note {
166
+ fill: var(--stgm-muted-subtle) !important;
167
+ stroke: var(--stgm-border) !important;
168
+ }
169
+ .noteText,
170
+ .noteText > tspan {
171
+ fill: var(--stgm-foreground) !important;
172
+ }
173
+ .activation0,
174
+ .activation1 {
175
+ fill: var(--stgm-muted) !important;
176
+ stroke: var(--stgm-border) !important;
177
+ }
178
+ `;
@@ -7,23 +7,27 @@
7
7
  *
8
8
  * Rules (applied in order):
9
9
  * 1. Convert to lowercase
10
- * 2. Replace spaces with hyphens
10
+ * 2. Replace spaces and dots (namespace separators) with hyphens
11
11
  * 3. Strip everything except `a-z`, `0-9`, and `-`
12
12
  * 4. Collapse consecutive hyphens into one
13
13
  * 5. Trim leading / trailing hyphens
14
14
  *
15
+ * Dots are treated as namespace separators (e.g. `platform.sara` → `platform-sara`),
16
+ * matching the backend generators, so namespace-scoped names stay readable.
17
+ *
15
18
  * @example
16
19
  * ```ts
17
- * generateSlug("My Cool Agent") // "my-cool-agent"
18
- * generateSlug("Acme Corp!") // "acme-corp"
19
- * generateSlug(" --hello-- ") // "hello"
20
- * generateSlug("") // ""
20
+ * generateSlug("My Cool Agent") // "my-cool-agent"
21
+ * generateSlug("Acme Corp!") // "acme-corp"
22
+ * generateSlug(" --hello-- ") // "hello"
23
+ * generateSlug("platform.planton-arch") // "platform-planton-arch"
24
+ * generateSlug("") // ""
21
25
  * ```
22
26
  */
23
27
  export function generateSlug(name: string): string {
24
28
  return name
25
29
  .toLowerCase()
26
- .replace(/ /g, "-")
30
+ .replace(/[ .]/g, "-")
27
31
  .replace(/[^a-z0-9-]/g, "")
28
32
  .replace(/-{2,}/g, "-")
29
33
  .replace(/^-+|-+$/g, "");