@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.
- 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/markdown-components.d.ts +18 -0
- package/internal/markdown-components.d.ts.map +1 -1
- package/internal/markdown-components.js +33 -0
- package/internal/markdown-components.js.map +1 -1
- package/package.json +4 -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 +47 -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 +27 -0
- package/src/execution/use-build-from-plan-hotkey.ts +39 -0
- package/src/internal/__tests__/markdown-components.test.tsx +53 -0
- package/src/internal/markdown-components.tsx +36 -0
- package/src/session/inspector/__tests__/ArtifactsTab.test.tsx +7 -7
- package/styles.css +1 -1
|
@@ -17,36 +17,49 @@ const planArtifact = create(ExecutionArtifactSchema, {
|
|
|
17
17
|
});
|
|
18
18
|
|
|
19
19
|
/** Modal content fetches artifact text — keep it pending so nothing rejects. */
|
|
20
|
-
function createStigmerMock(): Stigmer {
|
|
20
|
+
function createStigmerMock(): { stigmer: Stigmer; getArtifactContent: ReturnType<typeof vi.fn> } {
|
|
21
|
+
const getArtifactContent = vi.fn().mockReturnValue(new Promise(() => {}));
|
|
21
22
|
return {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
},
|
|
25
|
-
|
|
23
|
+
stigmer: {
|
|
24
|
+
agentExecution: { getArtifactContent },
|
|
25
|
+
} as unknown as Stigmer,
|
|
26
|
+
getArtifactContent,
|
|
27
|
+
};
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
function withStigmer(children: ReactNode) {
|
|
30
|
+
function withStigmer(children: ReactNode, stigmer: Stigmer) {
|
|
29
31
|
return (
|
|
30
|
-
<StigmerContext.Provider value={
|
|
31
|
-
{children}
|
|
32
|
-
</StigmerContext.Provider>
|
|
32
|
+
<StigmerContext.Provider value={stigmer}>{children}</StigmerContext.Provider>
|
|
33
33
|
);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
afterEach(cleanup);
|
|
37
37
|
|
|
38
|
-
describe("PlanArtifactCard", () => {
|
|
39
|
-
it("
|
|
38
|
+
describe("PlanArtifactCard — action surface", () => {
|
|
39
|
+
it("is an accessible region labelled as the plan's actions, with name + size", () => {
|
|
40
40
|
render(
|
|
41
41
|
<PlanArtifactCard executionId="aex_1" artifact={planArtifact} org="acme" />,
|
|
42
42
|
);
|
|
43
43
|
|
|
44
|
-
const region = screen.getByRole("region", { name: "Plan
|
|
45
|
-
expect(region.textContent).toContain("Plan
|
|
46
|
-
expect(region.textContent).toContain("
|
|
44
|
+
const region = screen.getByRole("region", { name: "Plan actions" });
|
|
45
|
+
expect(region.textContent).toContain("Plan");
|
|
46
|
+
expect(region.textContent).toContain("KB");
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
-
it("
|
|
49
|
+
it("does NOT render the plan content (the message above is the document)", () => {
|
|
50
|
+
const { stigmer, getArtifactContent } = createStigmerMock();
|
|
51
|
+
render(
|
|
52
|
+
withStigmer(
|
|
53
|
+
<PlanArtifactCard executionId="aex_1" artifact={planArtifact} org="acme" />,
|
|
54
|
+
stigmer,
|
|
55
|
+
),
|
|
56
|
+
);
|
|
57
|
+
// The card is a pure action surface — it must not fetch the plan body until
|
|
58
|
+
// the user explicitly opens the full preview.
|
|
59
|
+
expect(getArtifactContent).not.toHaveBeenCalled();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("offers exactly one primary 'Build from plan' action that calls onImplement", () => {
|
|
50
63
|
const onImplement = vi.fn();
|
|
51
64
|
render(
|
|
52
65
|
<PlanArtifactCard
|
|
@@ -57,18 +70,18 @@ describe("PlanArtifactCard", () => {
|
|
|
57
70
|
/>,
|
|
58
71
|
);
|
|
59
72
|
|
|
60
|
-
fireEvent.click(screen.getByText("
|
|
73
|
+
fireEvent.click(screen.getByText("Build from plan"));
|
|
61
74
|
expect(onImplement).toHaveBeenCalledTimes(1);
|
|
62
75
|
});
|
|
63
76
|
|
|
64
|
-
it("hides
|
|
77
|
+
it("hides 'Build from plan' when onImplement is not provided", () => {
|
|
65
78
|
render(
|
|
66
79
|
<PlanArtifactCard executionId="aex_1" artifact={planArtifact} org="acme" />,
|
|
67
80
|
);
|
|
68
|
-
expect(screen.queryByText("
|
|
81
|
+
expect(screen.queryByText("Build from plan")).toBeNull();
|
|
69
82
|
});
|
|
70
83
|
|
|
71
|
-
it("disables
|
|
84
|
+
it("disables 'Build from plan' when disabled", () => {
|
|
72
85
|
const onImplement = vi.fn();
|
|
73
86
|
render(
|
|
74
87
|
<PlanArtifactCard
|
|
@@ -80,7 +93,7 @@ describe("PlanArtifactCard", () => {
|
|
|
80
93
|
/>,
|
|
81
94
|
);
|
|
82
95
|
|
|
83
|
-
const button = screen.getByText("
|
|
96
|
+
const button = screen.getByText("Build from plan").closest("button")!;
|
|
84
97
|
expect(button.disabled).toBe(true);
|
|
85
98
|
fireEvent.click(button);
|
|
86
99
|
expect(onImplement).not.toHaveBeenCalled();
|
|
@@ -93,30 +106,106 @@ describe("PlanArtifactCard", () => {
|
|
|
93
106
|
const link = screen.getByText("Download").closest("a")!;
|
|
94
107
|
expect(link.getAttribute("href")).toBe("https://example.test/plan.md");
|
|
95
108
|
});
|
|
109
|
+
});
|
|
96
110
|
|
|
97
|
-
|
|
111
|
+
describe("PlanArtifactCard — Cmd/Ctrl+Enter accelerator (card-scoped)", () => {
|
|
112
|
+
it("fires onImplement on Cmd+Enter from within the card", () => {
|
|
113
|
+
const onImplement = vi.fn();
|
|
114
|
+
render(
|
|
115
|
+
<PlanArtifactCard
|
|
116
|
+
executionId="aex_1"
|
|
117
|
+
artifact={planArtifact}
|
|
118
|
+
org="acme"
|
|
119
|
+
onImplement={onImplement}
|
|
120
|
+
/>,
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const region = screen.getByRole("region", { name: "Plan actions" });
|
|
124
|
+
fireEvent.keyDown(region, { key: "Enter", metaKey: true });
|
|
125
|
+
expect(onImplement).toHaveBeenCalledTimes(1);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("fires onImplement on Ctrl+Enter", () => {
|
|
129
|
+
const onImplement = vi.fn();
|
|
130
|
+
render(
|
|
131
|
+
<PlanArtifactCard
|
|
132
|
+
executionId="aex_1"
|
|
133
|
+
artifact={planArtifact}
|
|
134
|
+
org="acme"
|
|
135
|
+
onImplement={onImplement}
|
|
136
|
+
/>,
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const region = screen.getByRole("region", { name: "Plan actions" });
|
|
140
|
+
fireEvent.keyDown(region, { key: "Enter", ctrlKey: true });
|
|
141
|
+
expect(onImplement).toHaveBeenCalledTimes(1);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("ignores a plain Enter (no modifier) so it never hijacks typing", () => {
|
|
145
|
+
const onImplement = vi.fn();
|
|
146
|
+
render(
|
|
147
|
+
<PlanArtifactCard
|
|
148
|
+
executionId="aex_1"
|
|
149
|
+
artifact={planArtifact}
|
|
150
|
+
org="acme"
|
|
151
|
+
onImplement={onImplement}
|
|
152
|
+
/>,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const region = screen.getByRole("region", { name: "Plan actions" });
|
|
156
|
+
fireEvent.keyDown(region, { key: "Enter" });
|
|
157
|
+
expect(onImplement).not.toHaveBeenCalled();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("does not fire when disabled", () => {
|
|
161
|
+
const onImplement = vi.fn();
|
|
162
|
+
render(
|
|
163
|
+
<PlanArtifactCard
|
|
164
|
+
executionId="aex_1"
|
|
165
|
+
artifact={planArtifact}
|
|
166
|
+
org="acme"
|
|
167
|
+
onImplement={onImplement}
|
|
168
|
+
disabled
|
|
169
|
+
/>,
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
const region = screen.getByRole("region", { name: "Plan actions" });
|
|
173
|
+
fireEvent.keyDown(region, { key: "Enter", metaKey: true });
|
|
174
|
+
expect(onImplement).not.toHaveBeenCalled();
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe("PlanArtifactCard — Open full (org-gated preview)", () => {
|
|
179
|
+
it("opens the shared preview modal when 'Open full' is clicked", () => {
|
|
180
|
+
const { stigmer } = createStigmerMock();
|
|
98
181
|
render(
|
|
99
182
|
withStigmer(
|
|
100
|
-
<PlanArtifactCard
|
|
101
|
-
|
|
102
|
-
artifact={planArtifact}
|
|
103
|
-
org="acme"
|
|
104
|
-
/>,
|
|
183
|
+
<PlanArtifactCard executionId="aex_1" artifact={planArtifact} org="acme" />,
|
|
184
|
+
stigmer,
|
|
105
185
|
),
|
|
106
186
|
);
|
|
107
187
|
|
|
108
|
-
// No dialog before the user
|
|
188
|
+
// No dialog before the user opens it.
|
|
109
189
|
expect(document.querySelector("dialog")).toBeNull();
|
|
110
190
|
|
|
111
|
-
fireEvent.click(screen.getByText("
|
|
191
|
+
fireEvent.click(screen.getByText("Open full"));
|
|
112
192
|
|
|
113
193
|
const dialog = document.querySelector("dialog");
|
|
114
194
|
expect(dialog).toBeTruthy();
|
|
115
195
|
expect(dialog!.getAttribute("aria-label")).toBe("Preview plan.md");
|
|
116
196
|
});
|
|
117
197
|
|
|
118
|
-
it("hides
|
|
119
|
-
|
|
120
|
-
|
|
198
|
+
it("hides 'Open full' when org is absent (modal needs org), keeping Build + Download", () => {
|
|
199
|
+
const onImplement = vi.fn();
|
|
200
|
+
render(
|
|
201
|
+
<PlanArtifactCard
|
|
202
|
+
executionId="aex_1"
|
|
203
|
+
artifact={planArtifact}
|
|
204
|
+
onImplement={onImplement}
|
|
205
|
+
/>,
|
|
206
|
+
);
|
|
207
|
+
expect(screen.queryByText("Open full")).toBeNull();
|
|
208
|
+
expect(screen.queryByText("Build from plan")).not.toBeNull();
|
|
209
|
+
expect(screen.queryByText("Download")).not.toBeNull();
|
|
121
210
|
});
|
|
122
211
|
});
|
|
@@ -3,7 +3,7 @@ import { render, fireEvent, cleanup } from "@testing-library/react";
|
|
|
3
3
|
import { PlanCompletionCard } from "../PlanCompletionCard";
|
|
4
4
|
|
|
5
5
|
describe("PlanCompletionCard", () => {
|
|
6
|
-
it("renders the card with status text and
|
|
6
|
+
it("renders the card with status text and a 'Build from plan' button", () => {
|
|
7
7
|
const { container } = render(<PlanCompletionCard onImplement={() => {}} />);
|
|
8
8
|
|
|
9
9
|
const root = container.firstElementChild as HTMLElement;
|
|
@@ -13,12 +13,41 @@ describe("PlanCompletionCard", () => {
|
|
|
13
13
|
|
|
14
14
|
const button = root.querySelector("button");
|
|
15
15
|
expect(button).toBeTruthy();
|
|
16
|
-
expect(button!.textContent).toContain("
|
|
16
|
+
expect(button!.textContent).toContain("Build from plan");
|
|
17
17
|
|
|
18
18
|
expect(root.textContent).toContain("Plan complete");
|
|
19
19
|
cleanup();
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
+
it("fires onImplement on Cmd/Ctrl+Enter from within the card", () => {
|
|
23
|
+
const onImplement = vi.fn();
|
|
24
|
+
const { container } = render(
|
|
25
|
+
<PlanCompletionCard onImplement={onImplement} />,
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const root = container.firstElementChild as HTMLElement;
|
|
29
|
+
fireEvent.keyDown(root, { key: "Enter", metaKey: true });
|
|
30
|
+
fireEvent.keyDown(root, { key: "Enter", ctrlKey: true });
|
|
31
|
+
expect(onImplement).toHaveBeenCalledTimes(2);
|
|
32
|
+
|
|
33
|
+
// A plain Enter must not trigger it.
|
|
34
|
+
fireEvent.keyDown(root, { key: "Enter" });
|
|
35
|
+
expect(onImplement).toHaveBeenCalledTimes(2);
|
|
36
|
+
cleanup();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("does not fire the accelerator when disabled", () => {
|
|
40
|
+
const onImplement = vi.fn();
|
|
41
|
+
const { container } = render(
|
|
42
|
+
<PlanCompletionCard onImplement={onImplement} disabled />,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const root = container.firstElementChild as HTMLElement;
|
|
46
|
+
fireEvent.keyDown(root, { key: "Enter", metaKey: true });
|
|
47
|
+
expect(onImplement).not.toHaveBeenCalled();
|
|
48
|
+
cleanup();
|
|
49
|
+
});
|
|
50
|
+
|
|
22
51
|
it("calls onImplement when the button is clicked", () => {
|
|
23
52
|
const onImplement = vi.fn();
|
|
24
53
|
const { container } = render(<PlanCompletionCard onImplement={onImplement} />);
|
|
@@ -126,6 +126,33 @@ describe("MessageEntry — AI messages (Streamdown)", () => {
|
|
|
126
126
|
const prose = container.querySelector(".stgm-prose");
|
|
127
127
|
expect(prose).not.toBeNull();
|
|
128
128
|
});
|
|
129
|
+
|
|
130
|
+
it("renders a model-wrapped ```markdown plan as rich markdown, not a code block", () => {
|
|
131
|
+
const msg = makeMessage(
|
|
132
|
+
MessageType.MESSAGE_AI,
|
|
133
|
+
"```markdown\n# Plan\n\n1. First step\n2. Second step\n```",
|
|
134
|
+
);
|
|
135
|
+
const { container } = render(<MessageEntry message={msg} />);
|
|
136
|
+
|
|
137
|
+
const article = queryArticle(container, "AI response");
|
|
138
|
+
// The enclosing fence is unwrapped: the heading and list render as elements,
|
|
139
|
+
// and the whole plan is NOT trapped inside a single <pre>.
|
|
140
|
+
expect(article!.querySelector("h1")).not.toBeNull();
|
|
141
|
+
expect(article!.querySelector("ol")).not.toBeNull();
|
|
142
|
+
expect(article!.querySelector("pre")).toBeNull();
|
|
143
|
+
expect(article!.textContent).toContain("Plan");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("still renders a genuine ```js code block as a code block", () => {
|
|
147
|
+
const msg = makeMessage(
|
|
148
|
+
MessageType.MESSAGE_AI,
|
|
149
|
+
"```js\nconsole.log('hi');\n```",
|
|
150
|
+
);
|
|
151
|
+
const { container } = render(<MessageEntry message={msg} />);
|
|
152
|
+
|
|
153
|
+
const article = queryArticle(container, "AI response");
|
|
154
|
+
expect(article!.querySelector("pre")).not.toBeNull();
|
|
155
|
+
});
|
|
129
156
|
});
|
|
130
157
|
|
|
131
158
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, type KeyboardEvent } from "react";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Card-scoped `Cmd/Ctrl+Enter` accelerator for the "Build from plan" action.
|
|
7
|
+
*
|
|
8
|
+
* Returns an `onKeyDown` handler for a plan card's root element. Because it is
|
|
9
|
+
* attached to the card (not `window`), it fires only when the keystroke
|
|
10
|
+
* originates from within the card — so the SDK never installs a global listener
|
|
11
|
+
* that could hijack a host application's keyboard shortcuts (an embeddable-a11y
|
|
12
|
+
* requirement). The card's primary button stays natively activatable with
|
|
13
|
+
* Enter/Space when focused; this adds the power-user accelerator on top.
|
|
14
|
+
*
|
|
15
|
+
* No-op when there is no action wired or the action is disabled.
|
|
16
|
+
*
|
|
17
|
+
* Shared by {@link PlanArtifactCard} and {@link PlanCompletionCard} so the
|
|
18
|
+
* accelerator's behavior lives in exactly one place and cannot drift between
|
|
19
|
+
* the two cards.
|
|
20
|
+
*/
|
|
21
|
+
export function useBuildFromPlanHotkey(
|
|
22
|
+
onImplement: (() => void) | undefined,
|
|
23
|
+
disabled: boolean | undefined,
|
|
24
|
+
): (event: KeyboardEvent<HTMLElement>) => void {
|
|
25
|
+
return useCallback(
|
|
26
|
+
(event) => {
|
|
27
|
+
if (
|
|
28
|
+
onImplement &&
|
|
29
|
+
!disabled &&
|
|
30
|
+
event.key === "Enter" &&
|
|
31
|
+
(event.metaKey || event.ctrlKey)
|
|
32
|
+
) {
|
|
33
|
+
event.preventDefault();
|
|
34
|
+
onImplement();
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
[onImplement, disabled],
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { unwrapEnclosingMarkdownFence } from "../markdown-components";
|
|
3
|
+
|
|
4
|
+
describe("unwrapEnclosingMarkdownFence", () => {
|
|
5
|
+
describe("unwraps a whole-message markdown fence", () => {
|
|
6
|
+
it.each([
|
|
7
|
+
["```markdown tag", "```markdown\n# Plan\n- step\n```", "# Plan\n- step"],
|
|
8
|
+
["```md tag", "```md\n# Plan\n```", "# Plan"],
|
|
9
|
+
["case-insensitive tag", "```Markdown\n# Plan\n```", "# Plan"],
|
|
10
|
+
["uppercase md", "```MD\n# Plan\n```", "# Plan"],
|
|
11
|
+
[
|
|
12
|
+
"tilde-free body with inner code fence preserved",
|
|
13
|
+
"```markdown\n# Plan\n```ts\nconst x = 1;\n```\n```",
|
|
14
|
+
"# Plan\n```ts\nconst x = 1;\n```",
|
|
15
|
+
],
|
|
16
|
+
[
|
|
17
|
+
"surrounding whitespace is ignored",
|
|
18
|
+
"\n\n```markdown\n# Plan\n```\n\n",
|
|
19
|
+
"# Plan",
|
|
20
|
+
],
|
|
21
|
+
[
|
|
22
|
+
"longer-than-three backtick fence",
|
|
23
|
+
"````markdown\n# Plan\n````",
|
|
24
|
+
"# Plan",
|
|
25
|
+
],
|
|
26
|
+
])("%s", (_label, input, expected) => {
|
|
27
|
+
expect(unwrapEnclosingMarkdownFence(input)).toBe(expected);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("leaves everything else untouched (returns the original)", () => {
|
|
32
|
+
it.each([
|
|
33
|
+
["plain markdown (no fence)", "# Plan\n\n- step one\n- step two"],
|
|
34
|
+
["a bare ``` fence (ambiguous — could be code)", "```\n# Plan\n```"],
|
|
35
|
+
["a language-tagged code block", "```js\nconsole.log('hi');\n```"],
|
|
36
|
+
["an unclosed fence (mid-stream)", "```markdown\n# Plan in progr"],
|
|
37
|
+
["trailing content after the fence", "```markdown\n# Plan\n```\nthen more"],
|
|
38
|
+
["leading content before the fence", "intro\n```markdown\n# Plan\n```"],
|
|
39
|
+
["a richer info string than markdown", "```markdown title\n# Plan\n```"],
|
|
40
|
+
["an empty fenced block", "```markdown\n```"],
|
|
41
|
+
["plain prose", "Here is the plan you asked for."],
|
|
42
|
+
["empty string", ""],
|
|
43
|
+
])("%s", (_label, input) => {
|
|
44
|
+
expect(unwrapEnclosingMarkdownFence(input)).toBe(input);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("is idempotent — a once-unwrapped plan is left alone", () => {
|
|
49
|
+
const wrapped = "```markdown\n# Plan\n- a\n```";
|
|
50
|
+
const once = unwrapEnclosingMarkdownFence(wrapped);
|
|
51
|
+
expect(unwrapEnclosingMarkdownFence(once)).toBe(once);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -26,6 +26,42 @@ export function stripFrontmatter(content: string): string {
|
|
|
26
26
|
return content.replace(FRONTMATTER_RE, "");
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Matches content whose ENTIRE body is a single fenced code block tagged
|
|
31
|
+
* `markdown` / `md`. Capture group 1 is the opening backtick run (so the close
|
|
32
|
+
* must use the same run via the `\1` backreference); group 2 is the inner body.
|
|
33
|
+
*
|
|
34
|
+
* Deliberately strict: the info string must be exactly `markdown`/`md` and the
|
|
35
|
+
* fence must span the whole (trimmed) string. A bare ``` ``` ``` fence is NOT
|
|
36
|
+
* matched — without the explicit language tag we cannot tell wrapped markdown
|
|
37
|
+
* from a legitimate single code block, and guessing by inspecting the body is
|
|
38
|
+
* the kind of fuzzy heuristic this codebase avoids.
|
|
39
|
+
*/
|
|
40
|
+
const ENCLOSING_MARKDOWN_FENCE_RE =
|
|
41
|
+
/^(`{3,})[ \t]*(?:markdown|md)[ \t]*\r?\n([\s\S]*?)\r?\n\1[ \t]*$/i;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Unwraps a message the model wrapped entirely in a ```markdown / ```md fence.
|
|
45
|
+
*
|
|
46
|
+
* Some models emit their whole markdown reply inside one fenced block (a
|
|
47
|
+
* Plan-mode plan is the common case). Rendered as-is that becomes a single flat
|
|
48
|
+
* code block instead of rich markdown — headings, lists, and tables collapse to
|
|
49
|
+
* monospace text. This returns the inner markdown in exactly that case and is a
|
|
50
|
+
* no-op for everything else (already-rich markdown, prose, or a reply that is
|
|
51
|
+
* legitimately a single code block).
|
|
52
|
+
*
|
|
53
|
+
* Render-time only: callers pass it the text right before handing it to the
|
|
54
|
+
* markdown renderer, so the transcript and the raw artifact stay faithful to
|
|
55
|
+
* what the agent produced — a single source of truth for the unwrap, with no
|
|
56
|
+
* duplicated logic in the runner. While streaming, the closing fence has not
|
|
57
|
+
* arrived yet, so this no-ops and the live text renders as typed; it unwraps
|
|
58
|
+
* once the block closes.
|
|
59
|
+
*/
|
|
60
|
+
export function unwrapEnclosingMarkdownFence(content: string): string {
|
|
61
|
+
const match = ENCLOSING_MARKDOWN_FENCE_RE.exec(content.trim());
|
|
62
|
+
return match ? match[2] : content;
|
|
63
|
+
}
|
|
64
|
+
|
|
29
65
|
/**
|
|
30
66
|
* Styled react-markdown component overrides for SDK markdown surfaces.
|
|
31
67
|
*
|
|
@@ -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
|
});
|