@stigmer/react 3.0.9-dev.20260615150714 → 3.0.9-dev.20260615153829

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 (42) hide show
  1. package/execution/ArtifactContentRenderer.d.ts.map +1 -1
  2. package/execution/ArtifactContentRenderer.js +7 -3
  3. package/execution/ArtifactContentRenderer.js.map +1 -1
  4. package/execution/ArtifactPreviewModal.d.ts +3 -3
  5. package/execution/ArtifactPreviewModal.js +4 -4
  6. package/execution/ArtifactPreviewModal.js.map +1 -1
  7. package/execution/MessageEntry.d.ts.map +1 -1
  8. package/execution/MessageEntry.js +7 -3
  9. package/execution/MessageEntry.js.map +1 -1
  10. package/execution/PlanArtifactCard.d.ts +25 -14
  11. package/execution/PlanArtifactCard.d.ts.map +1 -1
  12. package/execution/PlanArtifactCard.js +25 -10
  13. package/execution/PlanArtifactCard.js.map +1 -1
  14. package/execution/PlanCompletionCard.d.ts +12 -9
  15. package/execution/PlanCompletionCard.d.ts.map +1 -1
  16. package/execution/PlanCompletionCard.js +14 -9
  17. package/execution/PlanCompletionCard.js.map +1 -1
  18. package/execution/use-build-from-plan-hotkey.d.ts +19 -0
  19. package/execution/use-build-from-plan-hotkey.d.ts.map +1 -0
  20. package/execution/use-build-from-plan-hotkey.js +30 -0
  21. package/execution/use-build-from-plan-hotkey.js.map +1 -0
  22. package/internal/markdown-components.d.ts +18 -0
  23. package/internal/markdown-components.d.ts.map +1 -1
  24. package/internal/markdown-components.js +33 -0
  25. package/internal/markdown-components.js.map +1 -1
  26. package/package.json +4 -4
  27. package/src/execution/ArtifactContentRenderer.tsx +11 -2
  28. package/src/execution/ArtifactPreviewModal.tsx +7 -7
  29. package/src/execution/MessageEntry.tsx +14 -3
  30. package/src/execution/PlanArtifactCard.tsx +60 -51
  31. package/src/execution/PlanCompletionCard.tsx +20 -13
  32. package/src/execution/__tests__/ArtifactContentRenderer.test.tsx +47 -0
  33. package/src/execution/__tests__/ArtifactPreviewModal.test.tsx +6 -6
  34. package/src/execution/__tests__/MessageThread.test.tsx +3 -3
  35. package/src/execution/__tests__/PlanArtifactCard.test.tsx +120 -31
  36. package/src/execution/__tests__/PlanCompletionCard.test.tsx +31 -2
  37. package/src/execution/__tests__/message-entry.test.tsx +27 -0
  38. package/src/execution/use-build-from-plan-hotkey.ts +39 -0
  39. package/src/internal/__tests__/markdown-components.test.tsx +53 -0
  40. package/src/internal/markdown-components.tsx +36 -0
  41. package/src/session/inspector/__tests__/ArtifactsTab.test.tsx +7 -7
  42. package/styles.css +1 -1
@@ -19,6 +19,39 @@ const FRONTMATTER_RE = /^---\r?\n[\s\S]*?\r?\n---\r?\n?/;
19
19
  export function stripFrontmatter(content) {
20
20
  return content.replace(FRONTMATTER_RE, "");
21
21
  }
22
+ /**
23
+ * Matches content whose ENTIRE body is a single fenced code block tagged
24
+ * `markdown` / `md`. Capture group 1 is the opening backtick run (so the close
25
+ * must use the same run via the `\1` backreference); group 2 is the inner body.
26
+ *
27
+ * Deliberately strict: the info string must be exactly `markdown`/`md` and the
28
+ * fence must span the whole (trimmed) string. A bare ``` ``` ``` fence is NOT
29
+ * matched — without the explicit language tag we cannot tell wrapped markdown
30
+ * from a legitimate single code block, and guessing by inspecting the body is
31
+ * the kind of fuzzy heuristic this codebase avoids.
32
+ */
33
+ const ENCLOSING_MARKDOWN_FENCE_RE = /^(`{3,})[ \t]*(?:markdown|md)[ \t]*\r?\n([\s\S]*?)\r?\n\1[ \t]*$/i;
34
+ /**
35
+ * Unwraps a message the model wrapped entirely in a ```markdown / ```md fence.
36
+ *
37
+ * Some models emit their whole markdown reply inside one fenced block (a
38
+ * Plan-mode plan is the common case). Rendered as-is that becomes a single flat
39
+ * code block instead of rich markdown — headings, lists, and tables collapse to
40
+ * monospace text. This returns the inner markdown in exactly that case and is a
41
+ * no-op for everything else (already-rich markdown, prose, or a reply that is
42
+ * legitimately a single code block).
43
+ *
44
+ * Render-time only: callers pass it the text right before handing it to the
45
+ * markdown renderer, so the transcript and the raw artifact stay faithful to
46
+ * what the agent produced — a single source of truth for the unwrap, with no
47
+ * duplicated logic in the runner. While streaming, the closing fence has not
48
+ * arrived yet, so this no-ops and the live text renders as typed; it unwraps
49
+ * once the block closes.
50
+ */
51
+ export function unwrapEnclosingMarkdownFence(content) {
52
+ const match = ENCLOSING_MARKDOWN_FENCE_RE.exec(content.trim());
53
+ return match ? match[2] : content;
54
+ }
22
55
  /**
23
56
  * Styled react-markdown component overrides for SDK markdown surfaces.
24
57
  *
@@ -1 +1 @@
1
- {"version":3,"file":"markdown-components.js","sourceRoot":"","sources":["../../src/internal/markdown-components.tsx"],"names":[],"mappings":"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"}
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.20260615150714",
3
+ "version": "3.0.9-dev.20260615153829",
4
4
  "description": "React provider and client hook for the Stigmer platform SDK",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -37,7 +37,7 @@
37
37
  "dependencies": {
38
38
  "@bufbuild/protovalidate": "^1.1.1",
39
39
  "@dagrejs/dagre": "^1.1.4",
40
- "@stigmer/theme": "3.0.9-dev.20260615150714",
40
+ "@stigmer/theme": "3.0.9-dev.20260615153829",
41
41
  "diff": "^8.0.3",
42
42
  "fflate": "^0.8.2",
43
43
  "react-markdown": "^10.1.0",
@@ -57,8 +57,8 @@
57
57
  "@codemirror/state": "^6.0.0",
58
58
  "@codemirror/view": "^6.0.0",
59
59
  "@lezer/highlight": "^1.0.0",
60
- "@stigmer/protos": "3.0.9-dev.20260615150714",
61
- "@stigmer/sdk": "3.0.9-dev.20260615150714",
60
+ "@stigmer/protos": "3.0.9-dev.20260615153829",
61
+ "@stigmer/sdk": "3.0.9-dev.20260615153829",
62
62
  "@tanstack/react-table": "^8.20.0",
63
63
  "@xyflow/react": "^12.0.0",
64
64
  "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
- {content}
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 "Implement" action. When provided, an Implement primary
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
- // Implement runs the plan; closing the modal lets the host's submit pipeline
231
- // take over (switch to Agent + send). Single combined handler keeps the
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 "Implement" action (see {@link ArtifactPreviewContentProps.onImplement}).
330
- * When provided, an Implement primary button appears; clicking it calls
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
- Implement
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 { MARKDOWN_COMPONENTS } from "../internal/markdown-components";
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
- {content}
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 "Review plan" modal (the shared
17
- * {@link ArtifactPreviewModal} uses it for its detection/apply pipeline).
18
- * When omitted, the Review action is hidden — Implement and Download remain.
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 "Implement". Hidden when omitted. */
23
+ /** Called when the user clicks "Build from plan". Hidden when omitted. */
22
24
  readonly onImplement?: () => void;
23
- /** Disables the Implement CTA (e.g., while an execution is active). */
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
- * Reviewable card shown after a completed Plan-mode execution when the agent
31
- * published a `plan.md` artifact.
32
+ * Action surface for a plan that a completed Plan-mode execution published as a
33
+ * `plan.md` artifact.
32
34
  *
33
- * Presentational by design: it surfaces the plan as a first-class object with
34
- * three actions **Implement** (turn the plan into an Agent run), **Review
35
- * plan** (open the rendered plan in the shared {@link ArtifactPreviewModal},
36
- * the same popup used elsewhere for artifact previews where Copy and an
37
- * Implement CTA also live), and **Download** the `plan.md` file. The plan text
38
- * is the single source of truth (the published artifact); the modal fetches it
39
- * on demand, so the card itself holds no plan content.
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 ready to review"
70
+ aria-label="Plan actions"
71
+ onKeyDown={handleKeyDown}
58
72
  className={cn(
59
- "mx-4 overflow-hidden rounded-md border border-border-muted bg-muted-faint",
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
- {/* Header */}
64
- <div className="flex items-center gap-3 px-3 py-2.5">
65
- <PlanIcon />
66
- <div className="min-w-0 flex-1">
67
- <div className="text-xs font-medium text-foreground">
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
- {/* Secondary actions */}
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-primary transition-colors hover:text-primary-muted",
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
- Review plan
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
- {/* Review opens the shared artifact preview popup — consistent with the
122
- Artifacts tab and the single place that fetches/renders plan content. */}
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 "Implement" button. */
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
- * Inline CTA card shown in the {@link MessageThread} after a completed
18
- * Plan-mode execution.
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
- * Offers a single "Implement" action that the consumer wires to switch
21
- * the interaction mode to Agent, pre-fill the composer, and focus it.
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
- * fully opt-in from the consumer's perspective.
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/50",
41
- "bg-muted/30 px-3 py-2.5",
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 implement?
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
- Implement
70
+ Build from plan
64
71
  </button>
65
72
  </div>
66
73
  );
@@ -0,0 +1,47 @@
1
+ import { describe, it, expect, afterEach } from "vitest";
2
+ import { render, screen, fireEvent, cleanup } from "@testing-library/react";
3
+ import { ArtifactContentRenderer } from "../ArtifactContentRenderer";
4
+
5
+ afterEach(cleanup);
6
+
7
+ describe("ArtifactContentRenderer — markdown", () => {
8
+ const wrappedPlan = "```markdown\n# Plan\n\n1. First\n2. Second\n```";
9
+
10
+ it("unwraps a model-wrapped ```markdown plan in the Rendered view", () => {
11
+ const { container } = render(
12
+ <ArtifactContentRenderer content={wrappedPlan} fileName="plan.md" />,
13
+ );
14
+
15
+ // Rendered is the default tab: the heading/list render as real elements and
16
+ // the plan is not trapped inside a single <pre>.
17
+ expect(container.querySelector("h1")).not.toBeNull();
18
+ expect(container.querySelector("ol")).not.toBeNull();
19
+ expect(container.querySelector("pre")).toBeNull();
20
+ });
21
+
22
+ it("keeps the Source view byte-faithful to the stored artifact", () => {
23
+ const { container } = render(
24
+ <ArtifactContentRenderer content={wrappedPlan} fileName="plan.md" />,
25
+ );
26
+
27
+ fireEvent.click(screen.getByRole("tab", { name: "Source" }));
28
+
29
+ // Source shows the raw bytes — including the enclosing fence we hid in the
30
+ // Rendered view — so a download/copy stays faithful to what the agent wrote.
31
+ const pre = container.querySelector("pre");
32
+ expect(pre).not.toBeNull();
33
+ expect(pre!.textContent).toContain("```markdown");
34
+ });
35
+
36
+ it("leaves an already-rich markdown plan unchanged", () => {
37
+ const { container } = render(
38
+ <ArtifactContentRenderer
39
+ content={"# Plan\n\n- only step"}
40
+ fileName="plan.md"
41
+ />,
42
+ );
43
+
44
+ expect(container.querySelector("h1")).not.toBeNull();
45
+ expect(container.querySelector("pre")).toBeNull();
46
+ });
47
+ });
@@ -27,8 +27,8 @@ function withStigmer(children: ReactNode) {
27
27
 
28
28
  afterEach(cleanup);
29
29
 
30
- describe("ArtifactPreviewContent — Implement action", () => {
31
- it("renders an Implement button only when onImplement is provided", () => {
30
+ describe("ArtifactPreviewContent — Build from plan action", () => {
31
+ it("renders a 'Build from plan' button only when onImplement is provided", () => {
32
32
  const { rerender } = render(
33
33
  withStigmer(
34
34
  <ArtifactPreviewContent
@@ -40,7 +40,7 @@ describe("ArtifactPreviewContent — Implement action", () => {
40
40
  />,
41
41
  ),
42
42
  );
43
- expect(screen.queryByText("Implement")).toBeNull();
43
+ expect(screen.queryByText("Build from plan")).toBeNull();
44
44
 
45
45
  rerender(
46
46
  withStigmer(
@@ -54,10 +54,10 @@ describe("ArtifactPreviewContent — Implement action", () => {
54
54
  />,
55
55
  ),
56
56
  );
57
- expect(screen.getByText("Implement")).toBeTruthy();
57
+ expect(screen.getByText("Build from plan")).toBeTruthy();
58
58
  });
59
59
 
60
- it("calls onImplement then closes the modal when Implement is clicked", () => {
60
+ it("calls onImplement then closes the modal when 'Build from plan' is clicked", () => {
61
61
  const calls: string[] = [];
62
62
  const onImplement = vi.fn(() => calls.push("implement"));
63
63
  const onClose = vi.fn(() => calls.push("close"));
@@ -75,7 +75,7 @@ describe("ArtifactPreviewContent — Implement action", () => {
75
75
  ),
76
76
  );
77
77
 
78
- fireEvent.click(screen.getByText("Implement"));
78
+ fireEvent.click(screen.getByText("Build from plan"));
79
79
 
80
80
  expect(onImplement).toHaveBeenCalledTimes(1);
81
81
  expect(onClose).toHaveBeenCalledTimes(1);
@@ -476,10 +476,10 @@ describe("MessageThread", () => {
476
476
  />,
477
477
  );
478
478
 
479
- const implementBtn = screen.getByRole("button", { name: /implement/i });
480
- expect(implementBtn).toBeTruthy();
479
+ const buildBtn = screen.getByRole("button", { name: /build from plan/i });
480
+ expect(buildBtn).toBeTruthy();
481
481
 
482
- fireEvent.click(implementBtn);
482
+ fireEvent.click(buildBtn);
483
483
  expect(onBuildFromPlan).toHaveBeenCalledOnce();
484
484
  });
485
485
  });