@emseepea/create-react-ui-server 0.0.34 → 0.0.36

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.
@@ -18,7 +18,7 @@
18
18
  "@emseepea/feedback": "0.2.14",
19
19
  "playwright": "1.62.1",
20
20
  "axe-core": "4.13.0",
21
- "@emseepea/testing": "0.12.0",
21
+ "@emseepea/testing": "0.13.0",
22
22
  "@types/node": "24.13.3",
23
23
  "@types/react": "19.2.18",
24
24
  "@types/react-dom": "19.2.5",
@@ -31,7 +31,7 @@
31
31
  },
32
32
  "private": true,
33
33
  "dependencies": {
34
- "@emseepea/react": "0.2.4",
34
+ "@emseepea/react": "0.3.0",
35
35
  "@emseepea/server": "0.12.1",
36
36
  "@emseepea/tailwind": "0.1.0",
37
37
  "react": "19.2.8",
@@ -1,6 +1,6 @@
1
- import { ResultCard, useMcpApp, useMcpTheme } from "@emseepea/react";
1
+ import { ResultCard, useMcpAction, useMcpApp, useMcpTheme, type McpActionLifecycle } from "@emseepea/react";
2
2
  import { defineResultView, type ResultView as ResultViewModel } from "@emseepea/server/ui";
3
- import { useEffect, useState } from "react";
3
+ import { useEffect } from "react";
4
4
  import { hydrateRoot } from "react-dom/client";
5
5
 
6
6
  interface PlantingPlanResult {
@@ -17,35 +17,30 @@ interface PlantingPlanResult {
17
17
  export function ResultApp() {
18
18
  const app = useMcpApp({ name: "Pea planting plan result", version: "1.0.0", parseResult: parsePlantingPlan });
19
19
  const theme = useMcpTheme(app.hostContext.theme);
20
- const [action, setAction] = useState<"idle" | "sending" | "sent" | "error">("idle");
20
+ const action = useMcpAction(app);
21
21
 
22
22
  useEffect(() => {
23
23
  document.documentElement.dataset.emseepeaTheme = theme;
24
24
  }, [theme]);
25
25
 
26
26
  const view = resultView(app, action);
27
- const act = async () => {
28
- if (action !== "idle") return;
29
- setAction("sending");
30
- try {
31
- await app.sendMessage("Show me growing tips for these pea varieties.");
32
- setAction("sent");
33
- } catch {
34
- setAction("error");
35
- }
36
- };
37
27
 
38
28
  return (
39
29
  <>
40
30
  <h1>Pea planting plan result</h1>
41
- <ResultCard view={view} headingLevel={2} idPrefix="pea-result" onAction={() => void act()} />
31
+ <ResultCard
32
+ view={view}
33
+ headingLevel={2}
34
+ idPrefix="pea-result"
35
+ onAction={({ id }) => void action.send(id, "Show me growing tips for these pea varieties.")}
36
+ />
42
37
  </>
43
38
  );
44
39
  }
45
40
 
46
41
  function resultView(
47
42
  app: ReturnType<typeof useMcpApp<PlantingPlanResult>>,
48
- action: "idle" | "sending" | "sent" | "error",
43
+ action: McpActionLifecycle,
49
44
  ): ResultViewModel {
50
45
  const base = {
51
46
  id: "pea-planting-plan-result",
@@ -75,13 +70,13 @@ function resultView(
75
70
  id: "growing-tips",
76
71
  label: "Ask for growing tips",
77
72
  accessibleName: "Ask for growing tips for these varieties",
78
- disabled: action !== "idle",
73
+ disabled: action.status !== "idle",
79
74
  }];
80
- const actionState = action === "sending"
81
- ? { kind: "sending" as const, status: "Asking ChatGPT: Show me growing tips for these pea varieties.", focusTarget: "status" as const }
82
- : action === "sent"
83
- ? { kind: "sent" as const, status: "Asked ChatGPT: Show me growing tips for these pea varieties.", focusTarget: "status" as const }
84
- : action === "error"
75
+ const actionState = action.status === "sending"
76
+ ? { kind: "sending" as const, status: `Asking ChatGPT: ${action.activePrompt}`, focusTarget: "status" as const }
77
+ : action.status === "sent"
78
+ ? { kind: "sent" as const, status: `Asked ChatGPT: ${action.activePrompt}`, focusTarget: "status" as const }
79
+ : action.status === "error"
85
80
  ? { kind: "error" as const, status: "Growing tips could not be requested. Ask in the chat instead.", focusTarget: "status" as const }
86
81
  : {
87
82
  kind: app.resultRevision > 1 ? "updated" as const : count === 0 ? "empty" as const : "ready" as const,
@@ -82,6 +82,7 @@ test("the MCP Apps card completes initialization before rendering a result", asy
82
82
  await page.setContent("<iframe title=\"Pea planting plan result\"></iframe>");
83
83
  await page.evaluate(() => {
84
84
  window.receivedAppMessages = [];
85
+ window.pendingAppMessages = [];
85
86
  window.addEventListener("message", (event) => {
86
87
  window.receivedAppMessages.push(event.data);
87
88
  if (event.data?.method === "ui/initialize") {
@@ -105,7 +106,7 @@ test("the MCP Apps card completes initialization before rendering a result", asy
105
106
  }, "*");
106
107
  }
107
108
  if (event.data?.method === "ui/message") {
108
- window.pendingAppMessage = { source: event.source, id: event.data.id };
109
+ window.pendingAppMessages.push({ source: event.source, id: event.data.id });
109
110
  }
110
111
  });
111
112
  });
@@ -160,14 +161,60 @@ test("the MCP Apps card completes initialization before rendering a result", asy
160
161
  assert.equal(await frame.locator("html").getAttribute("data-emseepea-theme"), "dark");
161
162
  assert.equal(await frame.evaluate((node) => node === document.querySelector("[data-emseepea-part='status']"), statusNode), true);
162
163
 
163
- await frame.locator("button").press("Enter");
164
+ await frame.locator("button").evaluate((button) => {
165
+ button.click();
166
+ button.click();
167
+ });
164
168
  await status.filter({ hasText: "Asking ChatGPT: Show me growing tips for these pea varieties." }).waitFor();
165
169
  assert.equal(await frame.locator("button").isDisabled(), true);
166
170
  await page.waitForFunction(() => window.receivedAppMessages.some((message) => message?.method === "ui/message"));
167
- const sent = await page.evaluate(() => window.receivedAppMessages.find((message) => message?.method === "ui/message"));
168
- assert.equal(sent.params.content[0].text, "Show me growing tips for these pea varieties.");
169
- await page.evaluate(() => window.pendingAppMessage.source.postMessage({
170
- jsonrpc: "2.0", id: window.pendingAppMessage.id, result: {},
171
+ const sent = await page.evaluate(() => window.receivedAppMessages.filter((message) => message?.method === "ui/message"));
172
+ assert.equal(sent.length, 1);
173
+ assert.equal(sent[0].params.content[0].text, "Show me growing tips for these pea varieties.");
174
+
175
+ await page.evaluate(() => document.querySelector("iframe").contentWindow.postMessage({
176
+ jsonrpc: "2.0", method: "ui/notifications/tool-result", params: { structuredContent: {
177
+ title: "Autumn peas",
178
+ matchingCount: 1,
179
+ varieties: [
180
+ { name: "Early Onward", growthHabit: "climbing", peaType: "shelling" },
181
+ ],
182
+ notice: "No report was sent or stored.",
183
+ } },
184
+ }, "*"));
185
+ await status.filter({ hasText: "Preview updated: 1 sample pea variety matches." }).waitFor();
186
+ assert.equal(await frame.locator("button").isDisabled(), false);
187
+ await page.evaluate(() => window.pendingAppMessages[0].source.postMessage({
188
+ jsonrpc: "2.0", id: window.pendingAppMessages[0].id, result: {},
189
+ }, "*"));
190
+ await page.waitForTimeout(50);
191
+ assert.equal(await status.textContent(), "Preview updated: 1 sample pea variety matches.");
192
+
193
+ await frame.locator("button").press("Enter");
194
+ await page.waitForFunction(() => window.pendingAppMessages.length === 2);
195
+ await page.evaluate(() => window.pendingAppMessages[1].source.postMessage({
196
+ jsonrpc: "2.0", id: window.pendingAppMessages[1].id, error: { code: -32603, message: "Rejected" },
197
+ }, "*"));
198
+ await status.filter({ hasText: "Growing tips could not be requested. Ask in the chat instead." }).waitFor();
199
+ assert.equal(await frame.locator("[data-emseepea-part='status']").evaluate((element) => element === document.activeElement), true);
200
+
201
+ await page.evaluate(() => document.querySelector("iframe").contentWindow.postMessage({
202
+ jsonrpc: "2.0", method: "ui/notifications/tool-result", params: { structuredContent: {
203
+ title: "Winter peas",
204
+ matchingCount: 1,
205
+ varieties: [
206
+ { name: "Meteor", growthHabit: "bush", peaType: "shelling" },
207
+ ],
208
+ notice: "No report was sent or stored.",
209
+ } },
210
+ }, "*"));
211
+ await status.filter({ hasText: "Preview updated: 1 sample pea variety matches." }).waitFor();
212
+ await frame.locator("button").press("Enter");
213
+ await page.waitForFunction(() => window.pendingAppMessages.length === 3);
214
+ const finalSent = await page.evaluate(() => window.receivedAppMessages.filter((message) => message?.method === "ui/message").at(-1));
215
+ assert.equal(finalSent.params.content[0].text, "Show me growing tips for these pea varieties.");
216
+ await page.evaluate(() => window.pendingAppMessages[2].source.postMessage({
217
+ jsonrpc: "2.0", id: window.pendingAppMessages[2].id, result: {},
171
218
  }, "*"));
172
219
  await status.filter({ hasText: "Asked ChatGPT: Show me growing tips for these pea varieties." }).waitFor();
173
220
  assert.equal(await frame.locator("[data-emseepea-part='status']").evaluate((element) => element === document.activeElement), true);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emseepea/create-react-ui-server",
3
- "version": "0.0.34",
3
+ "version": "0.0.36",
4
4
  "description": "Create an Em See Pea server with an accessible React form.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -19,7 +19,7 @@
19
19
  },
20
20
  "devDependencies": {
21
21
  "@emseepea/feedback": "0.2.14",
22
- "@emseepea/react": "0.2.4",
22
+ "@emseepea/react": "0.3.0",
23
23
  "@emseepea/server": "0.12.1",
24
24
  "@emseepea/tailwind": "0.1.0",
25
25
  "react": "19.2.8",
@@ -27,7 +27,7 @@
27
27
  "@emseepea/example-ui-shared": "0.0.0",
28
28
  "playwright": "1.62.1",
29
29
  "axe-core": "4.13.0",
30
- "@emseepea/testing": "0.12.0",
30
+ "@emseepea/testing": "0.13.0",
31
31
  "@types/node": "24.13.3",
32
32
  "@types/react": "19.2.18",
33
33
  "@types/react-dom": "19.2.5",