@stigmer/react 3.0.9-dev.20260615150714 → 3.0.9-dev.20260616060535
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.
- package/execution/ArtifactContentRenderer.d.ts.map +1 -1
- package/execution/ArtifactContentRenderer.js +7 -3
- package/execution/ArtifactContentRenderer.js.map +1 -1
- package/execution/ArtifactPreviewModal.d.ts +3 -3
- package/execution/ArtifactPreviewModal.js +4 -4
- package/execution/ArtifactPreviewModal.js.map +1 -1
- package/execution/MessageEntry.d.ts.map +1 -1
- package/execution/MessageEntry.js +7 -3
- package/execution/MessageEntry.js.map +1 -1
- package/execution/PlanArtifactCard.d.ts +25 -14
- package/execution/PlanArtifactCard.d.ts.map +1 -1
- package/execution/PlanArtifactCard.js +25 -10
- package/execution/PlanArtifactCard.js.map +1 -1
- package/execution/PlanCompletionCard.d.ts +12 -9
- package/execution/PlanCompletionCard.d.ts.map +1 -1
- package/execution/PlanCompletionCard.js +14 -9
- package/execution/PlanCompletionCard.js.map +1 -1
- package/execution/use-build-from-plan-hotkey.d.ts +19 -0
- package/execution/use-build-from-plan-hotkey.d.ts.map +1 -0
- package/execution/use-build-from-plan-hotkey.js +30 -0
- package/execution/use-build-from-plan-hotkey.js.map +1 -0
- package/internal/code-highlight.d.ts +24 -0
- package/internal/code-highlight.d.ts.map +1 -0
- package/internal/code-highlight.js +115 -0
- package/internal/code-highlight.js.map +1 -0
- package/internal/markdown-components.d.ts +18 -0
- package/internal/markdown-components.d.ts.map +1 -1
- package/internal/markdown-components.js +46 -2
- package/internal/markdown-components.js.map +1 -1
- package/package.json +7 -4
- package/src/execution/ArtifactContentRenderer.tsx +11 -2
- package/src/execution/ArtifactPreviewModal.tsx +7 -7
- package/src/execution/MessageEntry.tsx +14 -3
- package/src/execution/PlanArtifactCard.tsx +60 -51
- package/src/execution/PlanCompletionCard.tsx +20 -13
- package/src/execution/__tests__/ArtifactContentRenderer.test.tsx +62 -0
- package/src/execution/__tests__/ArtifactPreviewModal.test.tsx +6 -6
- package/src/execution/__tests__/MessageThread.test.tsx +3 -3
- package/src/execution/__tests__/PlanArtifactCard.test.tsx +120 -31
- package/src/execution/__tests__/PlanCompletionCard.test.tsx +31 -2
- package/src/execution/__tests__/message-entry.test.tsx +43 -0
- package/src/execution/use-build-from-plan-hotkey.ts +39 -0
- package/src/internal/__tests__/code-highlight.test.tsx +59 -0
- package/src/internal/__tests__/markdown-components.test.tsx +119 -0
- package/src/internal/code-highlight.tsx +120 -0
- package/src/internal/markdown-components.tsx +56 -3
- package/src/session/inspector/__tests__/ArtifactsTab.test.tsx +7 -7
- package/src/styles.css +88 -0
- package/styles.css +1 -1
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Fragment, type ReactNode } from "react";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { createLowlight } from "lowlight";
|
|
4
|
+
import { toJsxRuntime } from "hast-util-to-jsx-runtime";
|
|
5
|
+
|
|
6
|
+
// Dependency licensing (DD-012): `lowlight` and `hast-util-to-jsx-runtime` are
|
|
7
|
+
// MIT; `highlight.js` is BSD-3-Clause. BSD-3-Clause is a permissive, OSI-approved
|
|
8
|
+
// license, compatible with MIT/Apache-2.0, that imposes no obligations on SDK
|
|
9
|
+
// consumers beyond attribution — so it satisfies DD-012's "MIT or Apache-2.0
|
|
10
|
+
// compatible" rule. Recorded here so the choice is auditable at the point of use.
|
|
11
|
+
import bash from "highlight.js/lib/languages/bash";
|
|
12
|
+
import css from "highlight.js/lib/languages/css";
|
|
13
|
+
import dockerfile from "highlight.js/lib/languages/dockerfile";
|
|
14
|
+
import go from "highlight.js/lib/languages/go";
|
|
15
|
+
import java from "highlight.js/lib/languages/java";
|
|
16
|
+
import javascript from "highlight.js/lib/languages/javascript";
|
|
17
|
+
import json from "highlight.js/lib/languages/json";
|
|
18
|
+
import markdown from "highlight.js/lib/languages/markdown";
|
|
19
|
+
import python from "highlight.js/lib/languages/python";
|
|
20
|
+
import rust from "highlight.js/lib/languages/rust";
|
|
21
|
+
import sql from "highlight.js/lib/languages/sql";
|
|
22
|
+
import typescript from "highlight.js/lib/languages/typescript";
|
|
23
|
+
import xml from "highlight.js/lib/languages/xml";
|
|
24
|
+
import yaml from "highlight.js/lib/languages/yaml";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Syntax highlighter for fenced code blocks in SDK markdown surfaces.
|
|
28
|
+
*
|
|
29
|
+
* This is the single highlighting engine behind the shared `code` override in
|
|
30
|
+
* {@link file://./markdown-components.tsx} — so chat (Streamdown) and the
|
|
31
|
+
* artifact/skill viewers (react-markdown) colorize code identically.
|
|
32
|
+
*
|
|
33
|
+
* The engine emits highlight.js token classes (`hljs-keyword`, `hljs-string`,
|
|
34
|
+
* …) which `styles.css` maps onto the `--stgm-syntax-*` theme tokens. Colors
|
|
35
|
+
* are therefore 100% token-driven: they track the host's preset and color-mode
|
|
36
|
+
* with no hardcoded values, exactly like the CodeMirror YAML editor.
|
|
37
|
+
*
|
|
38
|
+
* **Eager, not lazy.** Highlighting is on the core path (almost every
|
|
39
|
+
* `SessionViewer` consumer renders agent messages containing code), so DD-013's
|
|
40
|
+
* lazy pattern (for rarely-used heavy deps) does not apply. The grammars are
|
|
41
|
+
* imported eagerly; because this module is only reachable through the markdown
|
|
42
|
+
* components, normal tree-shaking still keeps it out of bundles that never
|
|
43
|
+
* render markdown.
|
|
44
|
+
*
|
|
45
|
+
* **Curated grammar set.** Only the languages agents commonly emit are
|
|
46
|
+
* registered, to keep the payload small. Anything else falls back to flat
|
|
47
|
+
* rendering (see {@link resolveLanguage}) — a deterministic choice, never
|
|
48
|
+
* highlight.js auto-detection, consistent with this codebase's avoidance of
|
|
49
|
+
* fuzzy heuristics.
|
|
50
|
+
*/
|
|
51
|
+
const lowlight = createLowlight({
|
|
52
|
+
bash,
|
|
53
|
+
css,
|
|
54
|
+
dockerfile,
|
|
55
|
+
go,
|
|
56
|
+
java,
|
|
57
|
+
javascript,
|
|
58
|
+
json,
|
|
59
|
+
markdown,
|
|
60
|
+
python,
|
|
61
|
+
rust,
|
|
62
|
+
sql,
|
|
63
|
+
typescript,
|
|
64
|
+
xml,
|
|
65
|
+
yaml,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Fence info-strings agents use that aren't already registered as grammar
|
|
69
|
+
// aliases by highlight.js. `xml` is highlight.js's grammar for HTML markup.
|
|
70
|
+
lowlight.registerAlias({
|
|
71
|
+
bash: ["sh", "shell", "zsh", "console"],
|
|
72
|
+
dockerfile: ["docker"],
|
|
73
|
+
javascript: ["js", "jsx", "mjs", "cjs"],
|
|
74
|
+
markdown: ["md"],
|
|
75
|
+
python: ["py"],
|
|
76
|
+
typescript: ["ts", "tsx"],
|
|
77
|
+
xml: ["html"],
|
|
78
|
+
yaml: ["yml"],
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const jsxRuntime = { Fragment, jsx, jsxs } as const;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Resolves a fence info-string to a registered highlight.js language name, or
|
|
85
|
+
* `null` when no grammar is registered for it (the caller then renders the code
|
|
86
|
+
* flat). Case-insensitive and whitespace-tolerant; never guesses.
|
|
87
|
+
*
|
|
88
|
+
* @param language - The raw language from a `language-*` class (e.g. `"go"`).
|
|
89
|
+
* @returns The normalized, registered language name, or `null`.
|
|
90
|
+
*/
|
|
91
|
+
export function resolveLanguage(language: string | undefined): string | null {
|
|
92
|
+
if (!language) return null;
|
|
93
|
+
const name = language.trim().toLowerCase();
|
|
94
|
+
return name && lowlight.registered(name) ? name : null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Highlights `code` for the given fence `language` and returns themed React
|
|
99
|
+
* nodes — token `<span>`s carrying `hljs-*` classes that `styles.css` maps to
|
|
100
|
+
* `--stgm-syntax-*`.
|
|
101
|
+
*
|
|
102
|
+
* Returns `null` (so the caller can fall back to flat rendering) when the
|
|
103
|
+
* language has no registered grammar or tokenization throws. Highlighting is
|
|
104
|
+
* synchronous: there is no loading state to handle.
|
|
105
|
+
*
|
|
106
|
+
* @param code - The raw source text of the fenced block.
|
|
107
|
+
* @param language - The fence language (e.g. `"ts"`); unknown → `null`.
|
|
108
|
+
*/
|
|
109
|
+
export function highlightToReact(
|
|
110
|
+
code: string,
|
|
111
|
+
language: string | undefined,
|
|
112
|
+
): ReactNode | null {
|
|
113
|
+
const resolved = resolveLanguage(language);
|
|
114
|
+
if (resolved === null) return null;
|
|
115
|
+
try {
|
|
116
|
+
return toJsxRuntime(lowlight.highlight(resolved, code), jsxRuntime);
|
|
117
|
+
} catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -2,6 +2,9 @@ import type { ComponentProps, JSX } 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
|
+
import { highlightToReact } from "./code-highlight";
|
|
6
|
+
|
|
7
|
+
const LANGUAGE_CLASS_PREFIX = "language-";
|
|
5
8
|
|
|
6
9
|
type MdProps<T extends keyof JSX.IntrinsicElements> = ComponentProps<T>;
|
|
7
10
|
|
|
@@ -26,6 +29,42 @@ export function stripFrontmatter(content: string): string {
|
|
|
26
29
|
return content.replace(FRONTMATTER_RE, "");
|
|
27
30
|
}
|
|
28
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Matches content whose ENTIRE body is a single fenced code block tagged
|
|
34
|
+
* `markdown` / `md`. Capture group 1 is the opening backtick run (so the close
|
|
35
|
+
* must use the same run via the `\1` backreference); group 2 is the inner body.
|
|
36
|
+
*
|
|
37
|
+
* Deliberately strict: the info string must be exactly `markdown`/`md` and the
|
|
38
|
+
* fence must span the whole (trimmed) string. A bare ``` ``` ``` fence is NOT
|
|
39
|
+
* matched — without the explicit language tag we cannot tell wrapped markdown
|
|
40
|
+
* from a legitimate single code block, and guessing by inspecting the body is
|
|
41
|
+
* the kind of fuzzy heuristic this codebase avoids.
|
|
42
|
+
*/
|
|
43
|
+
const ENCLOSING_MARKDOWN_FENCE_RE =
|
|
44
|
+
/^(`{3,})[ \t]*(?:markdown|md)[ \t]*\r?\n([\s\S]*?)\r?\n\1[ \t]*$/i;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Unwraps a message the model wrapped entirely in a ```markdown / ```md fence.
|
|
48
|
+
*
|
|
49
|
+
* Some models emit their whole markdown reply inside one fenced block (a
|
|
50
|
+
* Plan-mode plan is the common case). Rendered as-is that becomes a single flat
|
|
51
|
+
* code block instead of rich markdown — headings, lists, and tables collapse to
|
|
52
|
+
* monospace text. This returns the inner markdown in exactly that case and is a
|
|
53
|
+
* no-op for everything else (already-rich markdown, prose, or a reply that is
|
|
54
|
+
* legitimately a single code block).
|
|
55
|
+
*
|
|
56
|
+
* Render-time only: callers pass it the text right before handing it to the
|
|
57
|
+
* markdown renderer, so the transcript and the raw artifact stay faithful to
|
|
58
|
+
* what the agent produced — a single source of truth for the unwrap, with no
|
|
59
|
+
* duplicated logic in the runner. While streaming, the closing fence has not
|
|
60
|
+
* arrived yet, so this no-ops and the live text renders as typed; it unwraps
|
|
61
|
+
* once the block closes.
|
|
62
|
+
*/
|
|
63
|
+
export function unwrapEnclosingMarkdownFence(content: string): string {
|
|
64
|
+
const match = ENCLOSING_MARKDOWN_FENCE_RE.exec(content.trim());
|
|
65
|
+
return match ? match[2] : content;
|
|
66
|
+
}
|
|
67
|
+
|
|
29
68
|
/**
|
|
30
69
|
* Styled react-markdown component overrides for SDK markdown surfaces.
|
|
31
70
|
*
|
|
@@ -127,15 +166,29 @@ export const MARKDOWN_COMPONENTS: Components = {
|
|
|
127
166
|
|
|
128
167
|
code({ children, className: codeClassName, ...props }: MdProps<"code">) {
|
|
129
168
|
const isBlock =
|
|
130
|
-
typeof codeClassName === "string" &&
|
|
169
|
+
typeof codeClassName === "string" &&
|
|
170
|
+
codeClassName.startsWith(LANGUAGE_CLASS_PREFIX);
|
|
131
171
|
|
|
132
172
|
if (isBlock) {
|
|
173
|
+
// Tokenize here, in the one component both renderers (Streamdown for
|
|
174
|
+
// chat, react-markdown for artifacts/skills) share, so highlighting is
|
|
175
|
+
// identical everywhere. Highlight only plain-string children — anything
|
|
176
|
+
// else (e.g. a streaming caret node) falls back to flat rendering.
|
|
177
|
+
const language = codeClassName.slice(LANGUAGE_CLASS_PREFIX.length);
|
|
178
|
+
const highlighted =
|
|
179
|
+
typeof children === "string"
|
|
180
|
+
? highlightToReact(children, language)
|
|
181
|
+
: null;
|
|
182
|
+
|
|
133
183
|
return (
|
|
134
184
|
<code
|
|
135
|
-
className={cn(
|
|
185
|
+
className={cn(
|
|
186
|
+
"hljs font-mono text-xs text-foreground",
|
|
187
|
+
codeClassName,
|
|
188
|
+
)}
|
|
136
189
|
{...props}
|
|
137
190
|
>
|
|
138
|
-
{children}
|
|
191
|
+
{highlighted ?? children}
|
|
139
192
|
</code>
|
|
140
193
|
);
|
|
141
194
|
}
|
|
@@ -58,32 +58,32 @@ function openPreviewFor(name: string) {
|
|
|
58
58
|
|
|
59
59
|
afterEach(cleanup);
|
|
60
60
|
|
|
61
|
-
describe("ArtifactsTab — plan
|
|
62
|
-
it("shows
|
|
61
|
+
describe("ArtifactsTab — plan 'Build from plan' wiring", () => {
|
|
62
|
+
it("shows 'Build from plan' in the preview of a plan.md artifact", () => {
|
|
63
63
|
renderTab(vi.fn());
|
|
64
64
|
|
|
65
65
|
openPreviewFor("plan.md");
|
|
66
66
|
|
|
67
67
|
const dialog = document.querySelector("dialog")!;
|
|
68
|
-
expect(within(dialog).getByText("
|
|
68
|
+
expect(within(dialog).getByText("Build from plan")).toBeTruthy();
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
-
it("does not show
|
|
71
|
+
it("does not show 'Build from plan' in the preview of a non-plan artifact", () => {
|
|
72
72
|
renderTab(vi.fn());
|
|
73
73
|
|
|
74
74
|
openPreviewFor("notes.md");
|
|
75
75
|
|
|
76
76
|
const dialog = document.querySelector("dialog")!;
|
|
77
|
-
expect(within(dialog).queryByText("
|
|
77
|
+
expect(within(dialog).queryByText("Build from plan")).toBeNull();
|
|
78
78
|
});
|
|
79
79
|
|
|
80
|
-
it("invokes onImplementPlan when
|
|
80
|
+
it("invokes onImplementPlan when 'Build from plan' is clicked for a plan", () => {
|
|
81
81
|
const onImplementPlan = vi.fn();
|
|
82
82
|
renderTab(onImplementPlan);
|
|
83
83
|
|
|
84
84
|
openPreviewFor("plan.md");
|
|
85
85
|
const dialog = document.querySelector("dialog")!;
|
|
86
|
-
fireEvent.click(within(dialog).getByText("
|
|
86
|
+
fireEvent.click(within(dialog).getByText("Build from plan"));
|
|
87
87
|
|
|
88
88
|
expect(onImplementPlan).toHaveBeenCalledTimes(1);
|
|
89
89
|
});
|
package/src/styles.css
CHANGED
|
@@ -186,3 +186,91 @@
|
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
|
+
|
|
190
|
+
@layer stgm {
|
|
191
|
+
/* Syntax highlighting for fenced code blocks (issue #183).
|
|
192
|
+
lowlight (highlight.js) emits these `hljs-*` token classes; we map each
|
|
193
|
+
onto a shared `--stgm-syntax-*` theme token so highlighted code tracks the
|
|
194
|
+
host's preset and color-mode with zero hardcoded colors — the same token
|
|
195
|
+
contract the CodeMirror YAML editor uses. Scoped to `.stgm` so nothing
|
|
196
|
+
leaks into the host application. */
|
|
197
|
+
.stgm .hljs {
|
|
198
|
+
color: var(--stgm-foreground);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.stgm .hljs-comment,
|
|
202
|
+
.stgm .hljs-quote {
|
|
203
|
+
color: var(--stgm-syntax-comment);
|
|
204
|
+
font-style: italic;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.stgm .hljs-keyword,
|
|
208
|
+
.stgm .hljs-selector-tag,
|
|
209
|
+
.stgm .hljs-subst {
|
|
210
|
+
color: var(--stgm-syntax-keyword);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.stgm .hljs-literal,
|
|
214
|
+
.stgm .hljs-symbol,
|
|
215
|
+
.stgm .hljs-bullet {
|
|
216
|
+
color: var(--stgm-syntax-bool);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.stgm .hljs-string,
|
|
220
|
+
.stgm .hljs-regexp,
|
|
221
|
+
.stgm .hljs-addition,
|
|
222
|
+
.stgm .hljs-template-tag,
|
|
223
|
+
.stgm .hljs-template-variable,
|
|
224
|
+
.stgm .hljs-meta .hljs-string {
|
|
225
|
+
color: var(--stgm-syntax-string);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.stgm .hljs-number {
|
|
229
|
+
color: var(--stgm-syntax-number);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.stgm .hljs-title,
|
|
233
|
+
.stgm .hljs-title.function_,
|
|
234
|
+
.stgm .hljs-title.class_,
|
|
235
|
+
.stgm .hljs-section,
|
|
236
|
+
.stgm .hljs-built_in,
|
|
237
|
+
.stgm .hljs-type {
|
|
238
|
+
color: var(--stgm-syntax-property);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.stgm .hljs-attr,
|
|
242
|
+
.stgm .hljs-attribute,
|
|
243
|
+
.stgm .hljs-property,
|
|
244
|
+
.stgm .hljs-variable,
|
|
245
|
+
.stgm .hljs-params,
|
|
246
|
+
.stgm .hljs-selector-attr,
|
|
247
|
+
.stgm .hljs-selector-pseudo,
|
|
248
|
+
.stgm .hljs-selector-class,
|
|
249
|
+
.stgm .hljs-selector-id {
|
|
250
|
+
color: var(--stgm-syntax-property);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.stgm .hljs-tag,
|
|
254
|
+
.stgm .hljs-name {
|
|
255
|
+
color: var(--stgm-syntax-tag);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.stgm .hljs-meta,
|
|
259
|
+
.stgm .hljs-meta .hljs-keyword,
|
|
260
|
+
.stgm .hljs-doctag {
|
|
261
|
+
color: var(--stgm-syntax-meta);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.stgm .hljs-link,
|
|
265
|
+
.stgm .hljs-deletion {
|
|
266
|
+
color: var(--stgm-syntax-atom);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.stgm .hljs-emphasis {
|
|
270
|
+
font-style: italic;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.stgm .hljs-strong {
|
|
274
|
+
font-weight: 600;
|
|
275
|
+
}
|
|
276
|
+
}
|