@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,164 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { useIsCodeFenceIncomplete } from "streamdown";
5
+ import { useColorMode } from "../color-mode.js";
6
+ import { loadMermaid } from "./mermaid-loader.js";
7
+ import { MERMAID_THEME_CSS } from "./mermaid-theme-css.js";
8
+
9
+ /**
10
+ * `mermaid.render` requires a DOM id for its scratch element. Ids must be
11
+ * unique per render call — theme switches and streaming updates can overlap
12
+ * renders within one component — so a module counter is the simplest
13
+ * collision-free source.
14
+ */
15
+ let renderSequence = 0;
16
+
17
+ /** Props for {@link MermaidDiagram}. */
18
+ export interface MermaidDiagramProps {
19
+ /** The raw Mermaid source from the fenced code block. */
20
+ readonly chart: string;
21
+ }
22
+
23
+ type RenderState =
24
+ | { readonly status: "pending" }
25
+ | { readonly status: "rendered"; readonly svg: string }
26
+ | { readonly status: "failed"; readonly message: string };
27
+
28
+ /**
29
+ * Renders a fenced ```mermaid block as an inline SVG diagram.
30
+ *
31
+ * Mounted by the shared `pre` override in
32
+ * {@link file://./markdown-components.tsx} whenever a fenced block carries the
33
+ * explicit `mermaid` info string — which means every SDK markdown surface
34
+ * (chat via Streamdown, artifacts/skills via react-markdown) renders diagrams
35
+ * identically. Detection is deliberately limited to the explicit language tag;
36
+ * guessing diagram intent from fence bodies (e.g. a bare fence starting with
37
+ * `flowchart`) is the kind of fuzzy heuristic this codebase avoids.
38
+ *
39
+ * Every failure mode degrades to the exact code block the fence rendered as
40
+ * before this component existed:
41
+ *
42
+ * - **Streaming** — while the fence is still open (`useIsCodeFenceIncomplete`,
43
+ * a safe no-op under react-markdown where the context defaults to `false`),
44
+ * the source renders as a code block; the diagram appears once the fence
45
+ * closes. No half-parsed error flashing mid-stream.
46
+ * - **Library unavailable** — `mermaid` is an optional peer dependency; if the
47
+ * dynamic import fails the source stays a code block.
48
+ * - **Parse error** — invalid diagram source falls back to the code block plus
49
+ * a screen-reader-announced note, so the agent's output is never hidden.
50
+ *
51
+ * Security: agent output is untrusted and this SDK renders inside host
52
+ * applications, so mermaid runs with `securityLevel: "strict"` (sanitized
53
+ * labels, no `click` interactivity) — deliberately stricter than the
54
+ * docs-site renderer, which only ever shows first-party content.
55
+ *
56
+ * Theming: the container chrome is fully `--stgm-*` token-driven, and so is
57
+ * the SVG interior. The built-in `default`/`dark` theme (selected via
58
+ * {@link useColorMode}) is the base; on top of it we inject
59
+ * {@link MERMAID_THEME_CSS}, a token-driven stylesheet whose `var(--stgm-*)`
60
+ * references the browser resolves against the `.stgm` scope. Interiors
61
+ * therefore track the active preset *and* color mode live — no color is read
62
+ * in JS, and a preset switch needs no re-render. Feeding tokens through
63
+ * mermaid's `themeVariables` is not possible: they are authored in `oklch()`,
64
+ * which mermaid's color engine (khroma) cannot parse. Any element or diagram
65
+ * type the CSS does not target falls back to the built-in theme, so a diagram
66
+ * is never blank or mis-colored.
67
+ */
68
+ export function MermaidDiagram({ chart }: MermaidDiagramProps) {
69
+ const isIncomplete = useIsCodeFenceIncomplete();
70
+ const colorMode = useColorMode();
71
+ const [state, setState] = useState<RenderState>({ status: "pending" });
72
+
73
+ // Markdown parsers hand the fence body over with its trailing newline;
74
+ // mermaid treats leading/trailing blank lines as part of the diagram.
75
+ const source = chart.trim();
76
+
77
+ useEffect(() => {
78
+ if (isIncomplete || source.length === 0) return;
79
+
80
+ let cancelled = false;
81
+
82
+ void (async () => {
83
+ const renderId = `stgm-mermaid-${++renderSequence}`;
84
+ try {
85
+ const mermaid = await loadMermaid();
86
+ // initialize() mutates a global singleton, so it runs before every
87
+ // render call — all diagrams under one provider share a color mode,
88
+ // and re-initializing keeps a theme switch from bleeding stale config
89
+ // into the next render.
90
+ mermaid.initialize({
91
+ startOnLoad: false,
92
+ securityLevel: "strict",
93
+ theme: colorMode === "dark" ? "dark" : "default",
94
+ themeCSS: MERMAID_THEME_CSS,
95
+ fontFamily: "inherit",
96
+ });
97
+ const { svg } = await mermaid.render(renderId, source);
98
+ if (!cancelled) setState({ status: "rendered", svg });
99
+ } catch (error: unknown) {
100
+ // Mermaid has historically leaked its scratch element into the
101
+ // document on parse failure; removing it defensively costs nothing.
102
+ document.getElementById(renderId)?.remove();
103
+ if (!cancelled) {
104
+ setState({
105
+ status: "failed",
106
+ message: error instanceof Error ? error.message : String(error),
107
+ });
108
+ }
109
+ }
110
+ })();
111
+
112
+ return () => {
113
+ cancelled = true;
114
+ };
115
+ // A previously rendered diagram stays visible while a re-render (theme
116
+ // switch) is in flight — swapping to a blank pending state would flash.
117
+ }, [source, colorMode, isIncomplete]);
118
+
119
+ if (state.status === "rendered") {
120
+ return (
121
+ <div
122
+ role="img"
123
+ aria-label="Mermaid diagram"
124
+ className="mb-3 last:mb-0 flex justify-center overflow-x-auto rounded-md border border-border bg-card p-3 [&_svg]:max-w-full"
125
+ dangerouslySetInnerHTML={{ __html: state.svg }}
126
+ />
127
+ );
128
+ }
129
+
130
+ return (
131
+ <MermaidSourceFallback
132
+ chart={chart}
133
+ failureMessage={state.status === "failed" ? state.message : null}
134
+ />
135
+ );
136
+ }
137
+
138
+ /**
139
+ * The pre-diagram presentation of a mermaid fence: the same styled code block
140
+ * the shared `pre`/`code` overrides in `markdown-components.tsx` would have
141
+ * produced (classes mirrored from there — importing them would create a
142
+ * module cycle, since that file mounts this component). Shown while the fence
143
+ * streams, while mermaid loads, and permanently when rendering fails.
144
+ */
145
+ function MermaidSourceFallback({
146
+ chart,
147
+ failureMessage,
148
+ }: {
149
+ chart: string;
150
+ failureMessage: string | null;
151
+ }) {
152
+ return (
153
+ <div className="mb-3 last:mb-0">
154
+ <pre className="overflow-x-auto rounded-md bg-muted p-3">
155
+ <code className="font-mono text-xs text-foreground">{chart}</code>
156
+ </pre>
157
+ {failureMessage !== null && (
158
+ <p role="status" className="mt-1 text-xs text-muted-foreground">
159
+ Mermaid diagram failed to render; showing source. ({failureMessage})
160
+ </p>
161
+ )}
162
+ </div>
163
+ );
164
+ }
@@ -15,6 +15,7 @@ import { ApprovalAction } from "@stigmer/protos/ai/stigmer/agentic/agentexecutio
15
15
  import {
16
16
  ThreadItemRenderer,
17
17
  threadContentColumnClass,
18
+ type MessageThreadSlots,
18
19
  type ThreadItem,
19
20
  type ThreadContentColumn,
20
21
  } from "../execution/MessageThread.js";
@@ -55,6 +56,7 @@ export interface VirtualizedThreadProps {
55
56
  readonly onRetrySend?: () => void;
56
57
  readonly onRetryExecution?: (message: string) => void;
57
58
  readonly onEditMessage?: (text: string) => void;
59
+ readonly slots?: MessageThreadSlots;
58
60
  }
59
61
 
60
62
  // ---------------------------------------------------------------------------
@@ -124,6 +126,7 @@ export function VirtualizedThread({
124
126
  onRetrySend,
125
127
  onRetryExecution,
126
128
  onEditMessage,
129
+ slots,
127
130
  }: VirtualizedThreadProps) {
128
131
  const virtuosoRef = useRef<VirtuosoHandle>(null);
129
132
  const scrollerRef = useRef<HTMLDivElement | null>(null);
@@ -161,8 +164,9 @@ export function VirtualizedThread({
161
164
  onRetrySend,
162
165
  onRetryExecution,
163
166
  onEditMessage,
167
+ slots,
164
168
  }),
165
- [formatToolCallSummary, onApprovalSubmit, submittingApprovalIds, approvalErrors, onBuildFromPlan, onOpenPlan, org, planActionsDisabled, planBuildPending, onRetrySend, onRetryExecution, onEditMessage],
169
+ [formatToolCallSummary, onApprovalSubmit, submittingApprovalIds, approvalErrors, onBuildFromPlan, onOpenPlan, org, planActionsDisabled, planBuildPending, onRetrySend, onRetryExecution, onEditMessage, slots],
166
170
  );
167
171
 
168
172
  return (
@@ -0,0 +1,122 @@
1
+ // Browser-mode regression guard (runs under vitest.a11y.config.ts in real
2
+ // Chromium) for token-driven Mermaid interior theming.
3
+ //
4
+ // This is not an axe audit — it lives here because the claim can only be
5
+ // verified against real layout + a real CSS engine: that Mermaid's injected
6
+ // `themeCSS` (authored with `var(--stgm-*)`) survives `securityLevel: "strict"`
7
+ // (mermaid's `sanitizeCss` + DOMPurify) and is resolved by the browser cascade
8
+ // against the `.stgm` scope. Source analysis already proves survivability; this
9
+ // test locks it against future mermaid/DOMPurify upgrades and proves, end to
10
+ // end, that interiors track the active color mode and preset live — with no
11
+ // component re-render (we mutate the scope in the DOM, never React state).
12
+
13
+ import "../../../../dist/styles.css";
14
+ import "@stigmer/theme/presets/fintech.css";
15
+
16
+ import { describe, it, afterEach, expect } from "vitest";
17
+ import { render, cleanup, waitFor } from "@testing-library/react";
18
+ import { ColorModeContext, type ResolvedColorMode } from "../../../color-mode.js";
19
+ import { MermaidDiagram } from "../../MermaidDiagram.js";
20
+
21
+ const CHART = "flowchart LR\n A[Hello] --> B[World]";
22
+
23
+ const NODE_SHAPE_SELECTOR =
24
+ "svg .node rect, svg .node polygon, svg .node circle, svg .node ellipse, svg .node path";
25
+
26
+ interface Mounted {
27
+ readonly container: HTMLElement;
28
+ /** The painted node shape whose `fill` our `themeCSS` drives from a token. */
29
+ readonly nodeShape: SVGElement;
30
+ /** Probe resolving `var(--stgm-muted)` (node fill) in the same scope. */
31
+ readonly mutedProbe: HTMLElement;
32
+ /** Probe resolving `var(--stgm-primary)` in the same scope. */
33
+ readonly primaryProbe: HTMLElement;
34
+ }
35
+
36
+ /**
37
+ * Render a diagram inside a `.stgm` scope carrying `scopeClass` (e.g. a preset)
38
+ * and `mode`, alongside two probe spans that resolve the same tokens. Resolves
39
+ * once the SVG interior has painted.
40
+ */
41
+ async function mount(
42
+ scopeClass: string,
43
+ mode: ResolvedColorMode,
44
+ ): Promise<Mounted> {
45
+ const container = document.createElement("div");
46
+ container.className = `stgm ${scopeClass}`.trim();
47
+ container.setAttribute("data-stgm-color-mode", mode);
48
+ document.body.appendChild(container);
49
+
50
+ render(
51
+ <ColorModeContext.Provider value={mode}>
52
+ <span data-probe="muted" style={{ color: "var(--stgm-muted)" }} />
53
+ <span data-probe="primary" style={{ color: "var(--stgm-primary)" }} />
54
+ <MermaidDiagram chart={CHART} />
55
+ </ColorModeContext.Provider>,
56
+ { container },
57
+ );
58
+
59
+ await waitFor(() => {
60
+ const shape = container.querySelector(NODE_SHAPE_SELECTOR);
61
+ if (!shape) throw new Error("diagram node not rendered yet");
62
+ });
63
+
64
+ return {
65
+ container,
66
+ nodeShape: container.querySelector(NODE_SHAPE_SELECTOR) as SVGElement,
67
+ mutedProbe: container.querySelector('[data-probe="muted"]') as HTMLElement,
68
+ primaryProbe: container.querySelector('[data-probe="primary"]') as HTMLElement,
69
+ };
70
+ }
71
+
72
+ afterEach(() => {
73
+ cleanup();
74
+ document.querySelectorAll(".stgm").forEach((node) => node.remove());
75
+ });
76
+
77
+ describe("Mermaid interior theming — token-driven, cascade-resolved", () => {
78
+ it("paints node interiors from --stgm-* tokens, not mermaid's stock palette", async () => {
79
+ const { nodeShape, mutedProbe } = await mount("", "light");
80
+ // If our themeCSS lost the specificity/order battle, the fill would be
81
+ // mermaid's default (#ECECFF-ish) and this exact-match would fail.
82
+ expect(getComputedStyle(nodeShape).fill).toBe(
83
+ getComputedStyle(mutedProbe).color,
84
+ );
85
+ });
86
+
87
+ it("tracks a color-mode switch live, with no re-render", async () => {
88
+ const { container, nodeShape, mutedProbe } = await mount("", "light");
89
+ const fillLight = getComputedStyle(nodeShape).fill;
90
+ expect(fillLight).toBe(getComputedStyle(mutedProbe).color);
91
+
92
+ // Flip the scope attribute directly — no React update. Pure cascade.
93
+ container.setAttribute("data-stgm-color-mode", "dark");
94
+
95
+ const fillDark = getComputedStyle(nodeShape).fill;
96
+ expect(fillDark).toBe(getComputedStyle(mutedProbe).color);
97
+ expect(fillDark).not.toBe(fillLight);
98
+ });
99
+
100
+ it("tracks the active preset", async () => {
101
+ const { nodeShape, mutedProbe, primaryProbe } = await mount(
102
+ "stgm-theme-fintech",
103
+ "light",
104
+ );
105
+ // The diagram is painted from the ACTIVE preset's resolved token.
106
+ expect(getComputedStyle(nodeShape).fill).toBe(
107
+ getComputedStyle(mutedProbe).color,
108
+ );
109
+
110
+ // Guard against a false pass where the preset CSS never loaded: fintech's
111
+ // primary (indigo) must differ from the default palette's primary (teal).
112
+ const defaultScope = document.createElement("div");
113
+ defaultScope.className = "stgm";
114
+ defaultScope.setAttribute("data-stgm-color-mode", "light");
115
+ defaultScope.innerHTML = '<span style="color: var(--stgm-primary)"></span>';
116
+ document.body.appendChild(defaultScope);
117
+
118
+ expect(getComputedStyle(primaryProbe).color).not.toBe(
119
+ getComputedStyle(defaultScope.firstElementChild as HTMLElement).color,
120
+ );
121
+ });
122
+ });
@@ -1,12 +1,23 @@
1
- import { describe, it, expect, afterEach } from "vitest";
1
+ import { describe, it, expect, afterEach, vi } from "vitest";
2
2
  import { render, cleanup } from "@testing-library/react";
3
3
  import type { ComponentType, ReactNode } from "react";
4
+ import Markdown from "react-markdown";
4
5
  import {
5
6
  MARKDOWN_COMPONENTS,
7
+ REMARK_PLUGINS,
6
8
  extractLeadingH1,
7
9
  unwrapEnclosingMarkdownFence,
8
10
  } from "../markdown-components";
9
11
 
12
+ // Stub the diagram component: these tests verify the DISPATCH seam (which
13
+ // fences route to the diagram vs the code block), not mermaid rendering —
14
+ // that contract is covered by mermaid-diagram.test.tsx.
15
+ vi.mock("../MermaidDiagram", () => ({
16
+ MermaidDiagram: ({ chart }: { chart: string }) => (
17
+ <div data-testid="mermaid-diagram">{chart}</div>
18
+ ),
19
+ }));
20
+
10
21
  afterEach(cleanup);
11
22
 
12
23
  /** The shared `code` override, typed for direct rendering in tests. */
@@ -15,6 +26,11 @@ const CodeComponent = MARKDOWN_COMPONENTS.code as ComponentType<{
15
26
  children?: ReactNode;
16
27
  }>;
17
28
 
29
+ /** The shared `pre` override, typed for direct rendering in tests. */
30
+ const PreComponent = MARKDOWN_COMPONENTS.pre as ComponentType<{
31
+ children?: ReactNode;
32
+ }>;
33
+
18
34
  describe("unwrapEnclosingMarkdownFence", () => {
19
35
  describe("unwraps a whole-message markdown fence", () => {
20
36
  it.each([
@@ -185,3 +201,79 @@ describe("MARKDOWN_COMPONENTS.code (shared highlight seam)", () => {
185
201
  expect(code!.textContent).toBe("inlineToken");
186
202
  });
187
203
  });
204
+
205
+ describe("MARKDOWN_COMPONENTS.pre (mermaid dispatch seam)", () => {
206
+ const CHART = "flowchart LR\n A --> B";
207
+
208
+ it("routes an explicit ```mermaid fence to the diagram, dropping the <pre> wrapper", () => {
209
+ const { container } = render(
210
+ <PreComponent>
211
+ <code className="language-mermaid">{CHART}</code>
212
+ </PreComponent>,
213
+ );
214
+
215
+ const diagram = container.querySelector('[data-testid="mermaid-diagram"]');
216
+ expect(diagram).not.toBeNull();
217
+ expect(diagram!.textContent).toBe(CHART);
218
+ // The diagram replaces the <pre> entirely — a block container inside
219
+ // <pre> would be invalid HTML and inherit code-block chrome.
220
+ expect(container.querySelector("pre")).toBeNull();
221
+ });
222
+
223
+ it("matches language-mermaid among multiple classes", () => {
224
+ const { container } = render(
225
+ <PreComponent>
226
+ <code className="language-mermaid extra-class">{CHART}</code>
227
+ </PreComponent>,
228
+ );
229
+
230
+ expect(
231
+ container.querySelector('[data-testid="mermaid-diagram"]'),
232
+ ).not.toBeNull();
233
+ });
234
+
235
+ it.each([
236
+ [
237
+ "a different language",
238
+ <code key="go" className="language-go">{"func main() {}"}</code>,
239
+ ],
240
+ [
241
+ "a language merely PREFIXED with mermaid",
242
+ <code key="pfx" className="language-mermaidjs">{CHART}</code>,
243
+ ],
244
+ ["an untagged fence", <code key="bare">{CHART}</code>],
245
+ [
246
+ "non-string code children",
247
+ <code key="node" className="language-mermaid">
248
+ <span>{CHART}</span>
249
+ </code>,
250
+ ],
251
+ ["plain-text children (no code element)", CHART],
252
+ ])("keeps the ordinary <pre> code block for %s", (_label, children) => {
253
+ const { container } = render(<PreComponent>{children}</PreComponent>);
254
+
255
+ expect(container.querySelector('[data-testid="mermaid-diagram"]')).toBeNull();
256
+ const pre = container.querySelector("pre");
257
+ expect(pre).not.toBeNull();
258
+ expect(pre!.className).toContain("bg-muted");
259
+ });
260
+
261
+ it("dispatches through real react-markdown (the artifact/skill surface path)", () => {
262
+ // Renders an actual document instead of synthetic elements, proving the
263
+ // element shape react-markdown hands to `pre` matches what the seam
264
+ // detects. The Streamdown (chat) path has the equivalent end-to-end
265
+ // coverage in message-entry.test.tsx.
266
+ const doc = `Before\n\n\`\`\`mermaid\n${CHART}\n\`\`\`\n\n\`\`\`go\nfunc main() {}\n\`\`\``;
267
+ const { container } = render(
268
+ <Markdown remarkPlugins={REMARK_PLUGINS} components={MARKDOWN_COMPONENTS}>
269
+ {doc}
270
+ </Markdown>,
271
+ );
272
+
273
+ const diagram = container.querySelector('[data-testid="mermaid-diagram"]');
274
+ expect(diagram).not.toBeNull();
275
+ expect(diagram!.textContent).toContain("flowchart LR");
276
+ // The sibling go fence still renders as an ordinary highlighted block.
277
+ expect(container.querySelector("pre code.language-go")).not.toBeNull();
278
+ });
279
+ });