@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,115 @@
|
|
|
1
|
+
import { Fragment } 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
|
+
// Dependency licensing (DD-012): `lowlight` and `hast-util-to-jsx-runtime` are
|
|
6
|
+
// MIT; `highlight.js` is BSD-3-Clause. BSD-3-Clause is a permissive, OSI-approved
|
|
7
|
+
// license, compatible with MIT/Apache-2.0, that imposes no obligations on SDK
|
|
8
|
+
// consumers beyond attribution — so it satisfies DD-012's "MIT or Apache-2.0
|
|
9
|
+
// compatible" rule. Recorded here so the choice is auditable at the point of use.
|
|
10
|
+
import bash from "highlight.js/lib/languages/bash";
|
|
11
|
+
import css from "highlight.js/lib/languages/css";
|
|
12
|
+
import dockerfile from "highlight.js/lib/languages/dockerfile";
|
|
13
|
+
import go from "highlight.js/lib/languages/go";
|
|
14
|
+
import java from "highlight.js/lib/languages/java";
|
|
15
|
+
import javascript from "highlight.js/lib/languages/javascript";
|
|
16
|
+
import json from "highlight.js/lib/languages/json";
|
|
17
|
+
import markdown from "highlight.js/lib/languages/markdown";
|
|
18
|
+
import python from "highlight.js/lib/languages/python";
|
|
19
|
+
import rust from "highlight.js/lib/languages/rust";
|
|
20
|
+
import sql from "highlight.js/lib/languages/sql";
|
|
21
|
+
import typescript from "highlight.js/lib/languages/typescript";
|
|
22
|
+
import xml from "highlight.js/lib/languages/xml";
|
|
23
|
+
import yaml from "highlight.js/lib/languages/yaml";
|
|
24
|
+
/**
|
|
25
|
+
* Syntax highlighter for fenced code blocks in SDK markdown surfaces.
|
|
26
|
+
*
|
|
27
|
+
* This is the single highlighting engine behind the shared `code` override in
|
|
28
|
+
* {@link file://./markdown-components.tsx} — so chat (Streamdown) and the
|
|
29
|
+
* artifact/skill viewers (react-markdown) colorize code identically.
|
|
30
|
+
*
|
|
31
|
+
* The engine emits highlight.js token classes (`hljs-keyword`, `hljs-string`,
|
|
32
|
+
* …) which `styles.css` maps onto the `--stgm-syntax-*` theme tokens. Colors
|
|
33
|
+
* are therefore 100% token-driven: they track the host's preset and color-mode
|
|
34
|
+
* with no hardcoded values, exactly like the CodeMirror YAML editor.
|
|
35
|
+
*
|
|
36
|
+
* **Eager, not lazy.** Highlighting is on the core path (almost every
|
|
37
|
+
* `SessionViewer` consumer renders agent messages containing code), so DD-013's
|
|
38
|
+
* lazy pattern (for rarely-used heavy deps) does not apply. The grammars are
|
|
39
|
+
* imported eagerly; because this module is only reachable through the markdown
|
|
40
|
+
* components, normal tree-shaking still keeps it out of bundles that never
|
|
41
|
+
* render markdown.
|
|
42
|
+
*
|
|
43
|
+
* **Curated grammar set.** Only the languages agents commonly emit are
|
|
44
|
+
* registered, to keep the payload small. Anything else falls back to flat
|
|
45
|
+
* rendering (see {@link resolveLanguage}) — a deterministic choice, never
|
|
46
|
+
* highlight.js auto-detection, consistent with this codebase's avoidance of
|
|
47
|
+
* fuzzy heuristics.
|
|
48
|
+
*/
|
|
49
|
+
const lowlight = createLowlight({
|
|
50
|
+
bash,
|
|
51
|
+
css,
|
|
52
|
+
dockerfile,
|
|
53
|
+
go,
|
|
54
|
+
java,
|
|
55
|
+
javascript,
|
|
56
|
+
json,
|
|
57
|
+
markdown,
|
|
58
|
+
python,
|
|
59
|
+
rust,
|
|
60
|
+
sql,
|
|
61
|
+
typescript,
|
|
62
|
+
xml,
|
|
63
|
+
yaml,
|
|
64
|
+
});
|
|
65
|
+
// Fence info-strings agents use that aren't already registered as grammar
|
|
66
|
+
// aliases by highlight.js. `xml` is highlight.js's grammar for HTML markup.
|
|
67
|
+
lowlight.registerAlias({
|
|
68
|
+
bash: ["sh", "shell", "zsh", "console"],
|
|
69
|
+
dockerfile: ["docker"],
|
|
70
|
+
javascript: ["js", "jsx", "mjs", "cjs"],
|
|
71
|
+
markdown: ["md"],
|
|
72
|
+
python: ["py"],
|
|
73
|
+
typescript: ["ts", "tsx"],
|
|
74
|
+
xml: ["html"],
|
|
75
|
+
yaml: ["yml"],
|
|
76
|
+
});
|
|
77
|
+
const jsxRuntime = { Fragment, jsx, jsxs };
|
|
78
|
+
/**
|
|
79
|
+
* Resolves a fence info-string to a registered highlight.js language name, or
|
|
80
|
+
* `null` when no grammar is registered for it (the caller then renders the code
|
|
81
|
+
* flat). Case-insensitive and whitespace-tolerant; never guesses.
|
|
82
|
+
*
|
|
83
|
+
* @param language - The raw language from a `language-*` class (e.g. `"go"`).
|
|
84
|
+
* @returns The normalized, registered language name, or `null`.
|
|
85
|
+
*/
|
|
86
|
+
export function resolveLanguage(language) {
|
|
87
|
+
if (!language)
|
|
88
|
+
return null;
|
|
89
|
+
const name = language.trim().toLowerCase();
|
|
90
|
+
return name && lowlight.registered(name) ? name : null;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Highlights `code` for the given fence `language` and returns themed React
|
|
94
|
+
* nodes — token `<span>`s carrying `hljs-*` classes that `styles.css` maps to
|
|
95
|
+
* `--stgm-syntax-*`.
|
|
96
|
+
*
|
|
97
|
+
* Returns `null` (so the caller can fall back to flat rendering) when the
|
|
98
|
+
* language has no registered grammar or tokenization throws. Highlighting is
|
|
99
|
+
* synchronous: there is no loading state to handle.
|
|
100
|
+
*
|
|
101
|
+
* @param code - The raw source text of the fenced block.
|
|
102
|
+
* @param language - The fence language (e.g. `"ts"`); unknown → `null`.
|
|
103
|
+
*/
|
|
104
|
+
export function highlightToReact(code, language) {
|
|
105
|
+
const resolved = resolveLanguage(language);
|
|
106
|
+
if (resolved === null)
|
|
107
|
+
return null;
|
|
108
|
+
try {
|
|
109
|
+
return toJsxRuntime(lowlight.highlight(resolved, code), jsxRuntime);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=code-highlight.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"code-highlight.js","sourceRoot":"","sources":["../../src/internal/code-highlight.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAkB,MAAM,OAAO,CAAC;AACjD,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAExD,+EAA+E;AAC/E,kFAAkF;AAClF,8EAA8E;AAC9E,6EAA6E;AAC7E,kFAAkF;AAClF,OAAO,IAAI,MAAM,iCAAiC,CAAC;AACnD,OAAO,GAAG,MAAM,gCAAgC,CAAC;AACjD,OAAO,UAAU,MAAM,uCAAuC,CAAC;AAC/D,OAAO,EAAE,MAAM,+BAA+B,CAAC;AAC/C,OAAO,IAAI,MAAM,iCAAiC,CAAC;AACnD,OAAO,UAAU,MAAM,uCAAuC,CAAC;AAC/D,OAAO,IAAI,MAAM,iCAAiC,CAAC;AACnD,OAAO,QAAQ,MAAM,qCAAqC,CAAC;AAC3D,OAAO,MAAM,MAAM,mCAAmC,CAAC;AACvD,OAAO,IAAI,MAAM,iCAAiC,CAAC;AACnD,OAAO,GAAG,MAAM,gCAAgC,CAAC;AACjD,OAAO,UAAU,MAAM,uCAAuC,CAAC;AAC/D,OAAO,GAAG,MAAM,gCAAgC,CAAC;AACjD,OAAO,IAAI,MAAM,iCAAiC,CAAC;AAEnD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,QAAQ,GAAG,cAAc,CAAC;IAC9B,IAAI;IACJ,GAAG;IACH,UAAU;IACV,EAAE;IACF,IAAI;IACJ,UAAU;IACV,IAAI;IACJ,QAAQ;IACR,MAAM;IACN,IAAI;IACJ,GAAG;IACH,UAAU;IACV,GAAG;IACH,IAAI;CACL,CAAC,CAAC;AAEH,0EAA0E;AAC1E,4EAA4E;AAC5E,QAAQ,CAAC,aAAa,CAAC;IACrB,IAAI,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,CAAC;IACvC,UAAU,EAAE,CAAC,QAAQ,CAAC;IACtB,UAAU,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC;IACvC,QAAQ,EAAE,CAAC,IAAI,CAAC;IAChB,MAAM,EAAE,CAAC,IAAI,CAAC;IACd,UAAU,EAAE,CAAC,IAAI,EAAE,KAAK,CAAC;IACzB,GAAG,EAAE,CAAC,MAAM,CAAC;IACb,IAAI,EAAE,CAAC,KAAK,CAAC;CACd,CAAC,CAAC;AAEH,MAAM,UAAU,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,IAAI,EAAW,CAAC;AAEpD;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,QAA4B;IAC1D,IAAI,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC3B,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC3C,OAAO,IAAI,IAAI,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;AACzD,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,gBAAgB,CAC9B,IAAY,EACZ,QAA4B;IAE5B,MAAM,QAAQ,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAC;IAC3C,IAAI,QAAQ,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACnC,IAAI,CAAC;QACH,OAAO,YAAY,CAAC,QAAQ,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,CAAC,EAAE,UAAU,CAAC,CAAC;IACtE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
|
|
@@ -15,6 +15,24 @@ export declare const REMARK_PLUGINS: (typeof remarkGfm)[];
|
|
|
15
15
|
* removes it before rendering.
|
|
16
16
|
*/
|
|
17
17
|
export declare function stripFrontmatter(content: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* Unwraps a message the model wrapped entirely in a ```markdown / ```md fence.
|
|
20
|
+
*
|
|
21
|
+
* Some models emit their whole markdown reply inside one fenced block (a
|
|
22
|
+
* Plan-mode plan is the common case). Rendered as-is that becomes a single flat
|
|
23
|
+
* code block instead of rich markdown — headings, lists, and tables collapse to
|
|
24
|
+
* monospace text. This returns the inner markdown in exactly that case and is a
|
|
25
|
+
* no-op for everything else (already-rich markdown, prose, or a reply that is
|
|
26
|
+
* legitimately a single code block).
|
|
27
|
+
*
|
|
28
|
+
* Render-time only: callers pass it the text right before handing it to the
|
|
29
|
+
* markdown renderer, so the transcript and the raw artifact stay faithful to
|
|
30
|
+
* what the agent produced — a single source of truth for the unwrap, with no
|
|
31
|
+
* duplicated logic in the runner. While streaming, the closing fence has not
|
|
32
|
+
* arrived yet, so this no-ops and the live text renders as typed; it unwraps
|
|
33
|
+
* once the block closes.
|
|
34
|
+
*/
|
|
35
|
+
export declare function unwrapEnclosingMarkdownFence(content: string): string;
|
|
18
36
|
/**
|
|
19
37
|
* Styled react-markdown component overrides for SDK markdown surfaces.
|
|
20
38
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"markdown-components.d.ts","sourceRoot":"","sources":["../../src/internal/markdown-components.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,SAAS,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"markdown-components.d.ts","sourceRoot":"","sources":["../../src/internal/markdown-components.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,SAAS,MAAM,YAAY,CAAC;AAQnC;;;GAGG;AACH,eAAO,MAAM,cAAc,sBAAc,CAAC;AAI1C;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAExD;AAgBD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAGpE;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,EAAE,UA4LjC,CAAC"}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import remarkGfm from "remark-gfm";
|
|
3
3
|
import { cn } from "@stigmer/theme";
|
|
4
|
+
import { highlightToReact } from "./code-highlight";
|
|
5
|
+
const LANGUAGE_CLASS_PREFIX = "language-";
|
|
4
6
|
/**
|
|
5
7
|
* Remark plugins applied to all SDK markdown surfaces.
|
|
6
8
|
* Includes GFM (tables, strikethrough, autolinks, task lists).
|
|
@@ -19,6 +21,39 @@ const FRONTMATTER_RE = /^---\r?\n[\s\S]*?\r?\n---\r?\n?/;
|
|
|
19
21
|
export function stripFrontmatter(content) {
|
|
20
22
|
return content.replace(FRONTMATTER_RE, "");
|
|
21
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Matches content whose ENTIRE body is a single fenced code block tagged
|
|
26
|
+
* `markdown` / `md`. Capture group 1 is the opening backtick run (so the close
|
|
27
|
+
* must use the same run via the `\1` backreference); group 2 is the inner body.
|
|
28
|
+
*
|
|
29
|
+
* Deliberately strict: the info string must be exactly `markdown`/`md` and the
|
|
30
|
+
* fence must span the whole (trimmed) string. A bare ``` ``` ``` fence is NOT
|
|
31
|
+
* matched — without the explicit language tag we cannot tell wrapped markdown
|
|
32
|
+
* from a legitimate single code block, and guessing by inspecting the body is
|
|
33
|
+
* the kind of fuzzy heuristic this codebase avoids.
|
|
34
|
+
*/
|
|
35
|
+
const ENCLOSING_MARKDOWN_FENCE_RE = /^(`{3,})[ \t]*(?:markdown|md)[ \t]*\r?\n([\s\S]*?)\r?\n\1[ \t]*$/i;
|
|
36
|
+
/**
|
|
37
|
+
* Unwraps a message the model wrapped entirely in a ```markdown / ```md fence.
|
|
38
|
+
*
|
|
39
|
+
* Some models emit their whole markdown reply inside one fenced block (a
|
|
40
|
+
* Plan-mode plan is the common case). Rendered as-is that becomes a single flat
|
|
41
|
+
* code block instead of rich markdown — headings, lists, and tables collapse to
|
|
42
|
+
* monospace text. This returns the inner markdown in exactly that case and is a
|
|
43
|
+
* no-op for everything else (already-rich markdown, prose, or a reply that is
|
|
44
|
+
* legitimately a single code block).
|
|
45
|
+
*
|
|
46
|
+
* Render-time only: callers pass it the text right before handing it to the
|
|
47
|
+
* markdown renderer, so the transcript and the raw artifact stay faithful to
|
|
48
|
+
* what the agent produced — a single source of truth for the unwrap, with no
|
|
49
|
+
* duplicated logic in the runner. While streaming, the closing fence has not
|
|
50
|
+
* arrived yet, so this no-ops and the live text renders as typed; it unwraps
|
|
51
|
+
* once the block closes.
|
|
52
|
+
*/
|
|
53
|
+
export function unwrapEnclosingMarkdownFence(content) {
|
|
54
|
+
const match = ENCLOSING_MARKDOWN_FENCE_RE.exec(content.trim());
|
|
55
|
+
return match ? match[2] : content;
|
|
56
|
+
}
|
|
22
57
|
/**
|
|
23
58
|
* Styled react-markdown component overrides for SDK markdown surfaces.
|
|
24
59
|
*
|
|
@@ -60,9 +95,18 @@ export const MARKDOWN_COMPONENTS = {
|
|
|
60
95
|
return (_jsx("pre", { className: "mb-3 last:mb-0 overflow-x-auto rounded-md bg-muted p-3", ...props, children: children }));
|
|
61
96
|
},
|
|
62
97
|
code({ children, className: codeClassName, ...props }) {
|
|
63
|
-
const isBlock = typeof codeClassName === "string" &&
|
|
98
|
+
const isBlock = typeof codeClassName === "string" &&
|
|
99
|
+
codeClassName.startsWith(LANGUAGE_CLASS_PREFIX);
|
|
64
100
|
if (isBlock) {
|
|
65
|
-
|
|
101
|
+
// Tokenize here, in the one component both renderers (Streamdown for
|
|
102
|
+
// chat, react-markdown for artifacts/skills) share, so highlighting is
|
|
103
|
+
// identical everywhere. Highlight only plain-string children — anything
|
|
104
|
+
// else (e.g. a streaming caret node) falls back to flat rendering.
|
|
105
|
+
const language = codeClassName.slice(LANGUAGE_CLASS_PREFIX.length);
|
|
106
|
+
const highlighted = typeof children === "string"
|
|
107
|
+
? highlightToReact(children, language)
|
|
108
|
+
: null;
|
|
109
|
+
return (_jsx("code", { className: cn("hljs font-mono text-xs text-foreground", codeClassName), ...props, children: highlighted ?? children }));
|
|
66
110
|
}
|
|
67
111
|
return (_jsx("code", { className: "font-mono text-xs bg-muted px-1 py-0.5 rounded text-foreground", ...props, children: children }));
|
|
68
112
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"markdown-components.js","sourceRoot":"","sources":["../../src/internal/markdown-components.tsx"],"names":[],"mappings":";AAEA,OAAO,SAAS,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,EAAE,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"markdown-components.js","sourceRoot":"","sources":["../../src/internal/markdown-components.tsx"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stigmer/react",
|
|
3
|
-
"version": "3.0.9-dev.
|
|
3
|
+
"version": "3.0.9-dev.20260616060535",
|
|
4
4
|
"description": "React provider and client hook for the Stigmer platform SDK",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -37,9 +37,12 @@
|
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@bufbuild/protovalidate": "^1.1.1",
|
|
39
39
|
"@dagrejs/dagre": "^1.1.4",
|
|
40
|
-
"@stigmer/theme": "3.0.9-dev.
|
|
40
|
+
"@stigmer/theme": "3.0.9-dev.20260616060535",
|
|
41
41
|
"diff": "^8.0.3",
|
|
42
42
|
"fflate": "^0.8.2",
|
|
43
|
+
"hast-util-to-jsx-runtime": "^2.3.6",
|
|
44
|
+
"highlight.js": "^11.11.1",
|
|
45
|
+
"lowlight": "^3.3.0",
|
|
43
46
|
"react-markdown": "^10.1.0",
|
|
44
47
|
"remark-gfm": "^4.0.1",
|
|
45
48
|
"sonner": "^2.0.0",
|
|
@@ -57,8 +60,8 @@
|
|
|
57
60
|
"@codemirror/state": "^6.0.0",
|
|
58
61
|
"@codemirror/view": "^6.0.0",
|
|
59
62
|
"@lezer/highlight": "^1.0.0",
|
|
60
|
-
"@stigmer/protos": "3.0.9-dev.
|
|
61
|
-
"@stigmer/sdk": "3.0.9-dev.
|
|
63
|
+
"@stigmer/protos": "3.0.9-dev.20260616060535",
|
|
64
|
+
"@stigmer/sdk": "3.0.9-dev.20260616060535",
|
|
62
65
|
"@tanstack/react-table": "^8.20.0",
|
|
63
66
|
"@xyflow/react": "^12.0.0",
|
|
64
67
|
"elkjs": "^0.9.0",
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useState, type ReactNode } from "react";
|
|
3
|
+
import { useMemo, useState, type ReactNode } from "react";
|
|
4
4
|
import Markdown from "react-markdown";
|
|
5
5
|
import { cn } from "@stigmer/theme";
|
|
6
6
|
import {
|
|
7
7
|
MARKDOWN_COMPONENTS,
|
|
8
8
|
REMARK_PLUGINS,
|
|
9
|
+
unwrapEnclosingMarkdownFence,
|
|
9
10
|
} from "../internal/markdown-components";
|
|
10
11
|
import {
|
|
11
12
|
getArtifactRenderMode,
|
|
@@ -105,6 +106,14 @@ type MarkdownTab = "rendered" | "source";
|
|
|
105
106
|
function MarkdownView({ content }: { readonly content: string }) {
|
|
106
107
|
const [tab, setTab] = useState<MarkdownTab>("rendered");
|
|
107
108
|
|
|
109
|
+
// A `.md` whose entire body is one ```markdown fence (e.g. a model-wrapped
|
|
110
|
+
// plan) would render flat. Unwrap for the Rendered view only — Source stays
|
|
111
|
+
// byte-faithful to the stored artifact.
|
|
112
|
+
const rendered = useMemo(
|
|
113
|
+
() => unwrapEnclosingMarkdownFence(content),
|
|
114
|
+
[content],
|
|
115
|
+
);
|
|
116
|
+
|
|
108
117
|
return (
|
|
109
118
|
<div>
|
|
110
119
|
<div className="flex items-center border-b border-border px-4 py-1.5">
|
|
@@ -132,7 +141,7 @@ function MarkdownView({ content }: { readonly content: string }) {
|
|
|
132
141
|
remarkPlugins={REMARK_PLUGINS}
|
|
133
142
|
components={MARKDOWN_COMPONENTS}
|
|
134
143
|
>
|
|
135
|
-
{
|
|
144
|
+
{rendered}
|
|
136
145
|
</Markdown>
|
|
137
146
|
</div>
|
|
138
147
|
) : (
|
|
@@ -46,7 +46,7 @@ export interface ArtifactPreviewContentProps {
|
|
|
46
46
|
*/
|
|
47
47
|
readonly onApplied?: (result: ApplyResourceResult) => void;
|
|
48
48
|
/**
|
|
49
|
-
* Optional
|
|
49
|
+
* Optional plan-build action. When provided, a "Build from plan" primary
|
|
50
50
|
* button appears in the action bar (used for plan artifacts: turn the
|
|
51
51
|
* plan into an Agent run). Clicking it calls `onImplement` then closes
|
|
52
52
|
* the modal. Omit for non-actionable artifacts.
|
|
@@ -227,9 +227,9 @@ export function ArtifactPreviewContent({
|
|
|
227
227
|
ctaLabel = `Push Skill to ${org}`;
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
-
//
|
|
231
|
-
// take over (switch to Agent + send). Single combined handler keeps
|
|
232
|
-
// caller's contract simple ("just give me onImplement").
|
|
230
|
+
// "Build from plan" runs the plan; closing the modal lets the host's submit
|
|
231
|
+
// pipeline take over (switch to Agent + send). Single combined handler keeps
|
|
232
|
+
// the caller's contract simple ("just give me onImplement").
|
|
233
233
|
const handleImplement = useCallback(() => {
|
|
234
234
|
onImplement?.();
|
|
235
235
|
onClose();
|
|
@@ -326,8 +326,8 @@ export interface ArtifactPreviewModalProps {
|
|
|
326
326
|
*/
|
|
327
327
|
readonly onApplied?: (result: ApplyResourceResult) => void;
|
|
328
328
|
/**
|
|
329
|
-
* Optional
|
|
330
|
-
* When provided,
|
|
329
|
+
* Optional plan-build action (see {@link ArtifactPreviewContentProps.onImplement}).
|
|
330
|
+
* When provided, a "Build from plan" primary button appears; clicking it calls
|
|
331
331
|
* `onImplement` then closes the modal.
|
|
332
332
|
*/
|
|
333
333
|
readonly onImplement?: () => void;
|
|
@@ -695,7 +695,7 @@ function ActionBar({
|
|
|
695
695
|
)}
|
|
696
696
|
>
|
|
697
697
|
<ImplementIcon />
|
|
698
|
-
|
|
698
|
+
Build from plan
|
|
699
699
|
</button>
|
|
700
700
|
)}
|
|
701
701
|
{applyResult ? (
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { memo, useState } from "react";
|
|
3
|
+
import { memo, useMemo, useState } from "react";
|
|
4
4
|
import { Streamdown } from "streamdown";
|
|
5
5
|
import type { AgentMessage } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
6
6
|
import { MessageType } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
7
7
|
import { cn } from "@stigmer/theme";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
MARKDOWN_COMPONENTS,
|
|
10
|
+
unwrapEnclosingMarkdownFence,
|
|
11
|
+
} from "../internal/markdown-components";
|
|
9
12
|
import { useRenderTracer } from "../internal/dev";
|
|
10
13
|
|
|
11
14
|
/** Props for {@link MessageEntry}. */
|
|
@@ -160,6 +163,14 @@ function AiMessage({
|
|
|
160
163
|
}) {
|
|
161
164
|
useRenderTracer("AiMessage", { contentLength: content.length, isStreaming });
|
|
162
165
|
|
|
166
|
+
// A model that wraps its whole reply in a ```markdown fence (Plan-mode plans
|
|
167
|
+
// most often) would otherwise render as one flat code block. Unwrap it here,
|
|
168
|
+
// at the render seam, so the transcript stays faithful to what the agent sent.
|
|
169
|
+
const markdown = useMemo(
|
|
170
|
+
() => unwrapEnclosingMarkdownFence(content),
|
|
171
|
+
[content],
|
|
172
|
+
);
|
|
173
|
+
|
|
163
174
|
return (
|
|
164
175
|
<div
|
|
165
176
|
role="article"
|
|
@@ -173,7 +184,7 @@ function AiMessage({
|
|
|
173
184
|
isAnimating={isStreaming}
|
|
174
185
|
caret="block"
|
|
175
186
|
>
|
|
176
|
-
{
|
|
187
|
+
{markdown}
|
|
177
188
|
</Streamdown>
|
|
178
189
|
</div>
|
|
179
190
|
</div>
|
|
@@ -5,6 +5,7 @@ import { cn } from "@stigmer/theme";
|
|
|
5
5
|
import type { ExecutionArtifact } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/artifact_pb";
|
|
6
6
|
import { ArtifactPreviewModal } from "./ArtifactPreviewModal";
|
|
7
7
|
import { formatArtifactSize } from "./artifact-utils";
|
|
8
|
+
import { useBuildFromPlanHotkey } from "./use-build-from-plan-hotkey";
|
|
8
9
|
|
|
9
10
|
/** Props for {@link PlanArtifactCard}. */
|
|
10
11
|
export interface PlanArtifactCardProps {
|
|
@@ -13,30 +14,41 @@ export interface PlanArtifactCardProps {
|
|
|
13
14
|
/** The published `plan.md` artifact (from `findPlanArtifact`). */
|
|
14
15
|
readonly artifact: ExecutionArtifact;
|
|
15
16
|
/**
|
|
16
|
-
* Organization slug. Required for the "
|
|
17
|
-
* {@link ArtifactPreviewModal} uses it for its detection/apply pipeline
|
|
18
|
-
* When omitted,
|
|
17
|
+
* Organization slug. Required for the "Open full" modal (the shared
|
|
18
|
+
* {@link ArtifactPreviewModal} uses it for its detection/apply pipeline and
|
|
19
|
+
* to fetch the content). When omitted, "Open full" is hidden — the prominent
|
|
20
|
+
* "Build from plan" action and "Download" remain.
|
|
19
21
|
*/
|
|
20
22
|
readonly org?: string;
|
|
21
|
-
/** Called when the user clicks "
|
|
23
|
+
/** Called when the user clicks "Build from plan". Hidden when omitted. */
|
|
22
24
|
readonly onImplement?: () => void;
|
|
23
|
-
/** Disables the
|
|
25
|
+
/** Disables the primary CTA (e.g., while an execution is active). */
|
|
24
26
|
readonly disabled?: boolean;
|
|
25
27
|
/** Additional CSS classes for the root element. */
|
|
26
28
|
readonly className?: string;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
+
* Action surface for a plan that a completed Plan-mode execution published as a
|
|
33
|
+
* `plan.md` artifact.
|
|
32
34
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* the
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
35
|
+
* It is rendered immediately below the plan message in the thread (which already
|
|
36
|
+
* shows the plan as rich markdown — the message renderer unwraps a model's
|
|
37
|
+
* enclosing markdown fence), so this card deliberately holds **no** plan
|
|
38
|
+
* content: the message is the document and this is its action bar. Rendering
|
|
39
|
+
* the plan a second time here would duplicate the same text the message and the
|
|
40
|
+
* "Open full" modal already show.
|
|
41
|
+
*
|
|
42
|
+
* The hierarchy is the point: one prominent, themeable primary action —
|
|
43
|
+
* **Build from plan** — with **Open full** and **Download** as subdued
|
|
44
|
+
* secondaries, so the next step is unmistakable. A small negative top margin
|
|
45
|
+
* pulls the bar against the plan message so the two read as a single
|
|
46
|
+
* first-class plan block.
|
|
47
|
+
*
|
|
48
|
+
* Kept as its own thread item (not merged into the message) on purpose: merging
|
|
49
|
+
* would change the streamed message item's identity at completion and remount
|
|
50
|
+
* the just-streamed plan. Adjacent-and-attached gets the unified look with no
|
|
51
|
+
* remount.
|
|
40
52
|
*
|
|
41
53
|
* All visual properties flow through `--stgm-*` tokens; the component is
|
|
42
54
|
* self-contained and embeddable.
|
|
@@ -50,59 +62,39 @@ export const PlanArtifactCard = memo(function PlanArtifactCard({
|
|
|
50
62
|
className,
|
|
51
63
|
}: PlanArtifactCardProps) {
|
|
52
64
|
const [previewOpen, setPreviewOpen] = useState(false);
|
|
65
|
+
const handleKeyDown = useBuildFromPlanHotkey(onImplement, disabled);
|
|
53
66
|
|
|
54
67
|
return (
|
|
55
68
|
<div
|
|
56
69
|
role="region"
|
|
57
|
-
aria-label="Plan
|
|
70
|
+
aria-label="Plan actions"
|
|
71
|
+
onKeyDown={handleKeyDown}
|
|
58
72
|
className={cn(
|
|
59
|
-
|
|
73
|
+
// `-mt-2` tightens the thread's `gap-4` so the bar attaches to the
|
|
74
|
+
// plan message above it, reading as one first-class plan block.
|
|
75
|
+
"mx-4 -mt-2 flex flex-wrap items-center gap-x-3 gap-y-2 rounded-md",
|
|
76
|
+
"border border-border-muted bg-muted-faint px-3 py-2",
|
|
60
77
|
className,
|
|
61
78
|
)}
|
|
62
79
|
>
|
|
63
|
-
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
Plan ready to review
|
|
69
|
-
</div>
|
|
70
|
-
<div className="text-xs tabular-nums text-muted-foreground">
|
|
71
|
-
{artifact.name} · {formatArtifactSize(artifact.sizeBytes)}
|
|
72
|
-
</div>
|
|
73
|
-
</div>
|
|
74
|
-
{onImplement && (
|
|
75
|
-
<button
|
|
76
|
-
type="button"
|
|
77
|
-
disabled={disabled}
|
|
78
|
-
onClick={onImplement}
|
|
79
|
-
className={cn(
|
|
80
|
-
"inline-flex items-center gap-1.5 rounded-md px-3 py-1.5",
|
|
81
|
-
"text-xs font-medium transition-colors",
|
|
82
|
-
"bg-primary text-primary-foreground hover:bg-primary-hover",
|
|
83
|
-
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
84
|
-
"disabled:pointer-events-none disabled:opacity-50",
|
|
85
|
-
)}
|
|
86
|
-
>
|
|
87
|
-
<ImplementIcon />
|
|
88
|
-
Implement
|
|
89
|
-
</button>
|
|
90
|
-
)}
|
|
91
|
-
</div>
|
|
80
|
+
<PlanIcon />
|
|
81
|
+
<span className="text-xs font-medium text-foreground">Plan</span>
|
|
82
|
+
<span className="text-xs tabular-nums text-muted-foreground-faint">
|
|
83
|
+
{formatArtifactSize(artifact.sizeBytes)}
|
|
84
|
+
</span>
|
|
92
85
|
|
|
93
|
-
|
|
94
|
-
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 border-t border-border-muted px-3 py-1.5">
|
|
86
|
+
<div className="ml-auto flex flex-wrap items-center gap-x-3 gap-y-1.5">
|
|
95
87
|
{org && (
|
|
96
88
|
<button
|
|
97
89
|
type="button"
|
|
98
90
|
onClick={() => setPreviewOpen(true)}
|
|
99
91
|
className={cn(
|
|
100
|
-
"inline-flex items-center gap-1 text-xs font-medium text-
|
|
92
|
+
"inline-flex items-center gap-1 text-xs font-medium text-muted-foreground transition-colors hover:text-foreground",
|
|
101
93
|
FOCUS_RING,
|
|
102
94
|
)}
|
|
103
95
|
>
|
|
104
96
|
<ExpandIcon />
|
|
105
|
-
|
|
97
|
+
Open full
|
|
106
98
|
</button>
|
|
107
99
|
)}
|
|
108
100
|
<a
|
|
@@ -116,10 +108,27 @@ export const PlanArtifactCard = memo(function PlanArtifactCard({
|
|
|
116
108
|
<DownloadIcon />
|
|
117
109
|
Download
|
|
118
110
|
</a>
|
|
111
|
+
{onImplement && (
|
|
112
|
+
<button
|
|
113
|
+
type="button"
|
|
114
|
+
disabled={disabled}
|
|
115
|
+
onClick={onImplement}
|
|
116
|
+
className={cn(
|
|
117
|
+
"inline-flex items-center gap-1.5 rounded-md px-3 py-1.5",
|
|
118
|
+
"text-xs font-medium transition-colors",
|
|
119
|
+
"bg-primary text-primary-foreground hover:bg-primary-hover",
|
|
120
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
121
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
122
|
+
)}
|
|
123
|
+
>
|
|
124
|
+
<ImplementIcon />
|
|
125
|
+
Build from plan
|
|
126
|
+
</button>
|
|
127
|
+
)}
|
|
119
128
|
</div>
|
|
120
129
|
|
|
121
|
-
{/*
|
|
122
|
-
|
|
130
|
+
{/* "Open full" reuses the shared artifact preview popup — the single place
|
|
131
|
+
that fetches/renders plan content (with Copy + Source/Rendered). */}
|
|
123
132
|
{org && previewOpen && (
|
|
124
133
|
<ArtifactPreviewModal
|
|
125
134
|
artifact={artifact}
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import { memo } from "react";
|
|
4
4
|
import { cn } from "@stigmer/theme";
|
|
5
|
+
import { useBuildFromPlanHotkey } from "./use-build-from-plan-hotkey";
|
|
5
6
|
|
|
6
7
|
/** Props for {@link PlanCompletionCard}. */
|
|
7
8
|
export interface PlanCompletionCardProps {
|
|
8
|
-
/** Called when the user clicks the "
|
|
9
|
+
/** Called when the user clicks the "Build from plan" button. */
|
|
9
10
|
readonly onImplement?: () => void;
|
|
10
11
|
/** Disables the CTA button (e.g., while an execution is active). */
|
|
11
12
|
readonly disabled?: boolean;
|
|
@@ -14,37 +15,43 @@ export interface PlanCompletionCardProps {
|
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
18
|
+
* Fallback CTA shown in the {@link MessageThread} after a completed Plan-mode
|
|
19
|
+
* execution that did **not** publish a `plan.md` artifact (older executions, or
|
|
20
|
+
* a plan whose upload failed). When a plan artifact exists, the richer
|
|
21
|
+
* {@link PlanArtifactCard} is shown instead.
|
|
19
22
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
23
|
+
* Mirrors `PlanArtifactCard`'s hierarchy — one prominent, themeable "Build from
|
|
24
|
+
* plan" primary action with the same card-scoped `Cmd/Ctrl+Enter` accelerator —
|
|
25
|
+
* so the two cards feel like the same affordance. The consumer wires the action
|
|
26
|
+
* to switch the interaction mode to Agent and submit the implement turn.
|
|
22
27
|
*
|
|
23
|
-
* Renders nothing when `onImplement` is not provided, so the card is
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* All visual properties flow through `--stgm-*` tokens.
|
|
28
|
+
* Renders nothing when `onImplement` is not provided, so the card is fully
|
|
29
|
+
* opt-in from the consumer's perspective. All visual properties flow through
|
|
30
|
+
* `--stgm-*` tokens.
|
|
27
31
|
*/
|
|
28
32
|
export const PlanCompletionCard = memo(function PlanCompletionCard({
|
|
29
33
|
onImplement,
|
|
30
34
|
disabled,
|
|
31
35
|
className,
|
|
32
36
|
}: PlanCompletionCardProps) {
|
|
37
|
+
const handleKeyDown = useBuildFromPlanHotkey(onImplement, disabled);
|
|
38
|
+
|
|
33
39
|
if (!onImplement) return null;
|
|
34
40
|
|
|
35
41
|
return (
|
|
36
42
|
<div
|
|
37
43
|
role="status"
|
|
38
44
|
aria-label="Plan complete"
|
|
45
|
+
onKeyDown={handleKeyDown}
|
|
39
46
|
className={cn(
|
|
40
|
-
"mx-4 flex items-center gap-3 rounded-md border border-border
|
|
41
|
-
"bg-muted
|
|
47
|
+
"mx-4 flex items-center gap-3 rounded-md border border-border-muted",
|
|
48
|
+
"bg-muted-faint px-3 py-2.5",
|
|
42
49
|
className,
|
|
43
50
|
)}
|
|
44
51
|
>
|
|
45
52
|
<PlanCompleteIcon />
|
|
46
53
|
<span className="min-w-0 flex-1 text-xs font-medium text-muted-foreground">
|
|
47
|
-
Plan complete — ready to
|
|
54
|
+
Plan complete — ready to build?
|
|
48
55
|
</span>
|
|
49
56
|
<button
|
|
50
57
|
type="button"
|
|
@@ -60,7 +67,7 @@ export const PlanCompletionCard = memo(function PlanCompletionCard({
|
|
|
60
67
|
)}
|
|
61
68
|
>
|
|
62
69
|
<ImplementIcon />
|
|
63
|
-
|
|
70
|
+
Build from plan
|
|
64
71
|
</button>
|
|
65
72
|
</div>
|
|
66
73
|
);
|