@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
@@ -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
- agentExecution: {
23
- getArtifactContent: vi.fn().mockReturnValue(new Promise(() => {})),
24
- },
25
- } as unknown as Stigmer;
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={createStigmerMock()}>
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("renders the review header with the artifact name and size", () => {
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 ready to review" });
45
- expect(region.textContent).toContain("Plan ready to review");
46
- expect(region.textContent).toContain("plan.md");
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("calls onImplement when Implement is clicked", () => {
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("Implement"));
73
+ fireEvent.click(screen.getByText("Build from plan"));
61
74
  expect(onImplement).toHaveBeenCalledTimes(1);
62
75
  });
63
76
 
64
- it("hides Implement when onImplement is not provided", () => {
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("Implement")).toBeNull();
81
+ expect(screen.queryByText("Build from plan")).toBeNull();
69
82
  });
70
83
 
71
- it("disables Implement when disabled", () => {
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("Implement").closest("button")!;
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
- it("opens the shared preview modal when Review plan is clicked", () => {
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
- executionId="aex_1"
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 reviews.
188
+ // No dialog before the user opens it.
109
189
  expect(document.querySelector("dialog")).toBeNull();
110
190
 
111
- fireEvent.click(screen.getByText("Review plan"));
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 Review plan when org is absent (modal needs org)", () => {
119
- render(<PlanArtifactCard executionId="aex_1" artifact={planArtifact} />);
120
- expect(screen.queryByText("Review plan")).toBeNull();
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 implement button", () => {
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("Implement");
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 Implement wiring", () => {
62
- it("shows Implement in the preview of a plan.md artifact", () => {
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("Implement")).toBeTruthy();
68
+ expect(within(dialog).getByText("Build from plan")).toBeTruthy();
69
69
  });
70
70
 
71
- it("does not show Implement in the preview of a non-plan artifact", () => {
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("Implement")).toBeNull();
77
+ expect(within(dialog).queryByText("Build from plan")).toBeNull();
78
78
  });
79
79
 
80
- it("invokes onImplementPlan when Implement is clicked for a plan", () => {
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("Implement"));
86
+ fireEvent.click(within(dialog).getByText("Build from plan"));
87
87
 
88
88
  expect(onImplementPlan).toHaveBeenCalledTimes(1);
89
89
  });