@stigmer/react 3.4.0 → 3.4.1

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 (131) hide show
  1. package/agent/useAgentCount.d.ts +2 -0
  2. package/agent/useAgentCount.d.ts.map +1 -1
  3. package/agent/useAgentCount.js.map +1 -1
  4. package/api-key/CreateApiKeyForm.d.ts +7 -1
  5. package/api-key/CreateApiKeyForm.d.ts.map +1 -1
  6. package/api-key/CreateApiKeyForm.js +2 -2
  7. package/api-key/CreateApiKeyForm.js.map +1 -1
  8. package/datastore/useDatastoreCount.d.ts +2 -0
  9. package/datastore/useDatastoreCount.d.ts.map +1 -1
  10. package/datastore/useDatastoreCount.js.map +1 -1
  11. package/index.d.ts +4 -4
  12. package/index.d.ts.map +1 -1
  13. package/index.js +2 -2
  14. package/index.js.map +1 -1
  15. package/manifest/ApplyManifestDialog.d.ts +10 -5
  16. package/manifest/ApplyManifestDialog.d.ts.map +1 -1
  17. package/manifest/ApplyManifestDialog.js +30 -13
  18. package/manifest/ApplyManifestDialog.js.map +1 -1
  19. package/manifest/useApplyManifest.d.ts +6 -2
  20. package/manifest/useApplyManifest.d.ts.map +1 -1
  21. package/manifest/useApplyManifest.js +5 -5
  22. package/manifest/useApplyManifest.js.map +1 -1
  23. package/mcp-server/McpServerDetailView.d.ts +38 -2
  24. package/mcp-server/McpServerDetailView.d.ts.map +1 -1
  25. package/mcp-server/McpServerDetailView.js +21 -5
  26. package/mcp-server/McpServerDetailView.js.map +1 -1
  27. package/mcp-server/index.d.ts +7 -0
  28. package/mcp-server/index.d.ts.map +1 -1
  29. package/mcp-server/index.js +9 -0
  30. package/mcp-server/index.js.map +1 -1
  31. package/mcp-server/steps/EnvironmentAuthStep.d.ts +4 -0
  32. package/mcp-server/steps/EnvironmentAuthStep.d.ts.map +1 -1
  33. package/mcp-server/steps/EnvironmentAuthStep.js +4 -0
  34. package/mcp-server/steps/EnvironmentAuthStep.js.map +1 -1
  35. package/mcp-server/steps/IdentityTransportStep.d.ts +6 -0
  36. package/mcp-server/steps/IdentityTransportStep.d.ts.map +1 -1
  37. package/mcp-server/steps/IdentityTransportStep.js +7 -1
  38. package/mcp-server/steps/IdentityTransportStep.js.map +1 -1
  39. package/mcp-server/steps/ReviewStep.d.ts +5 -0
  40. package/mcp-server/steps/ReviewStep.d.ts.map +1 -1
  41. package/mcp-server/steps/ReviewStep.js +16 -2
  42. package/mcp-server/steps/ReviewStep.js.map +1 -1
  43. package/mcp-server/useMcpServerCount.d.ts +2 -0
  44. package/mcp-server/useMcpServerCount.d.ts.map +1 -1
  45. package/mcp-server/useMcpServerCount.js.map +1 -1
  46. package/package.json +4 -4
  47. package/resource-creation/CreationPicker.d.ts.map +1 -1
  48. package/resource-creation/CreationPicker.js +3 -3
  49. package/resource-creation/CreationPicker.js.map +1 -1
  50. package/resource-creation/WizardNav.d.ts.map +1 -1
  51. package/resource-creation/WizardNav.js +1 -1
  52. package/resource-creation/WizardNav.js.map +1 -1
  53. package/resource-detail/Section.d.ts +8 -1
  54. package/resource-detail/Section.d.ts.map +1 -1
  55. package/resource-detail/Section.js +2 -2
  56. package/resource-detail/Section.js.map +1 -1
  57. package/resource-workbench/components/ResourceWorkbench.d.ts +9 -1
  58. package/resource-workbench/components/ResourceWorkbench.d.ts.map +1 -1
  59. package/resource-workbench/components/ResourceWorkbench.js +2 -1
  60. package/resource-workbench/components/ResourceWorkbench.js.map +1 -1
  61. package/resource-workbench/hooks/useResourceCollection.d.ts +9 -0
  62. package/resource-workbench/hooks/useResourceCollection.d.ts.map +1 -1
  63. package/resource-workbench/hooks/useResourceCollection.js +2 -2
  64. package/resource-workbench/hooks/useResourceCollection.js.map +1 -1
  65. package/search/useResourceCount.d.ts +7 -0
  66. package/search/useResourceCount.d.ts.map +1 -1
  67. package/search/useResourceCount.js +2 -1
  68. package/search/useResourceCount.js.map +1 -1
  69. package/session/NewSessionViewer.d.ts.map +1 -1
  70. package/session/NewSessionViewer.js +17 -3
  71. package/session/NewSessionViewer.js.map +1 -1
  72. package/session/SessionViewer.d.ts +2 -1
  73. package/session/SessionViewer.d.ts.map +1 -1
  74. package/session/SessionViewer.js +19 -11
  75. package/session/SessionViewer.js.map +1 -1
  76. package/session/SessionViewerLayout.d.ts +90 -0
  77. package/session/SessionViewerLayout.d.ts.map +1 -0
  78. package/session/SessionViewerLayout.js +32 -0
  79. package/session/SessionViewerLayout.js.map +1 -0
  80. package/session/index.d.ts +4 -0
  81. package/session/index.d.ts.map +1 -1
  82. package/session/index.js +10 -0
  83. package/session/index.js.map +1 -1
  84. package/session/useSessionRailViews.d.ts +2 -2
  85. package/skill/useSkillCount.d.ts +2 -0
  86. package/skill/useSkillCount.d.ts.map +1 -1
  87. package/skill/useSkillCount.js.map +1 -1
  88. package/src/agent/useAgentCount.ts +2 -0
  89. package/src/api-key/CreateApiKeyForm.tsx +8 -1
  90. package/src/api-key/__tests__/CreateApiKeyForm.test.tsx +58 -0
  91. package/src/datastore/useDatastoreCount.ts +2 -0
  92. package/src/index.ts +11 -0
  93. package/src/manifest/ApplyManifestDialog.tsx +40 -16
  94. package/src/manifest/__tests__/ApplyManifestDialog.test.tsx +147 -0
  95. package/src/manifest/__tests__/useApplyManifest.test.tsx +6 -6
  96. package/src/manifest/useApplyManifest.ts +12 -8
  97. package/src/mcp-server/McpServerDetailView.tsx +52 -4
  98. package/src/mcp-server/__tests__/McpServerDetailView.test.tsx +345 -0
  99. package/src/mcp-server/index.ts +13 -0
  100. package/src/mcp-server/steps/EnvironmentAuthStep.tsx +4 -0
  101. package/src/mcp-server/steps/IdentityTransportStep.tsx +9 -1
  102. package/src/mcp-server/steps/ReviewStep.tsx +18 -1
  103. package/src/mcp-server/useMcpServerCount.ts +2 -0
  104. package/src/resource-creation/CreationPicker.tsx +7 -0
  105. package/src/resource-creation/WizardNav.tsx +1 -0
  106. package/src/resource-detail/Section.tsx +9 -1
  107. package/src/resource-workbench/components/ResourceWorkbench.tsx +13 -0
  108. package/src/resource-workbench/hooks/useResourceCollection.ts +11 -1
  109. package/src/search/useResourceCount.ts +9 -1
  110. package/src/session/NewSessionViewer.tsx +48 -61
  111. package/src/session/SessionViewer.tsx +71 -86
  112. package/src/session/SessionViewerLayout.tsx +131 -0
  113. package/src/session/__tests__/SessionViewerLayout.test.tsx +140 -0
  114. package/src/session/index.ts +14 -0
  115. package/src/session/useSessionRailViews.tsx +2 -2
  116. package/src/skill/useSkillCount.ts +2 -0
  117. package/src/test/__tests__/samples.test.ts +159 -2
  118. package/src/test/index.ts +1 -1
  119. package/src/test/samples.ts +109 -14
  120. package/src/workflow/useWorkflowCount.ts +2 -0
  121. package/test/index.d.ts +1 -1
  122. package/test/index.d.ts.map +1 -1
  123. package/test/index.js +1 -1
  124. package/test/index.js.map +1 -1
  125. package/test/samples.d.ts +79 -7
  126. package/test/samples.d.ts.map +1 -1
  127. package/test/samples.js +101 -14
  128. package/test/samples.js.map +1 -1
  129. package/workflow/useWorkflowCount.d.ts +2 -0
  130. package/workflow/useWorkflowCount.d.ts.map +1 -1
  131. package/workflow/useWorkflowCount.js.map +1 -1
@@ -0,0 +1,345 @@
1
+ import { describe, it, expect, vi, beforeAll, afterEach } from "vitest";
2
+ import { render, screen, cleanup, fireEvent, within } from "@testing-library/react";
3
+ import type { ReactNode } from "react";
4
+ import { create } from "@bufbuild/protobuf";
5
+ import { timestampFromDate } from "@bufbuild/protobuf/wkt";
6
+ import { createRouterTransport, ConnectError, Code } from "@connectrpc/connect";
7
+ import { Stigmer } from "@stigmer/sdk";
8
+ import { McpServerQueryController } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/query_pb";
9
+ import {
10
+ McpServerSpecSchema,
11
+ HttpServerConfigSchema,
12
+ ToolApprovalPolicySchema,
13
+ } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/spec_pb";
14
+ import {
15
+ McpServerStatusSchema,
16
+ DiscoveredCapabilitiesSchema,
17
+ DiscoveredToolSchema,
18
+ ValidationState,
19
+ } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/status_pb";
20
+ import { EnvVarDeclarationSchema } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/spec_pb";
21
+ import type { McpServer } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
22
+ import { samples } from "../../test/samples";
23
+ import { StigmerContext } from "../../context";
24
+ import { McpServerDetailView } from "../McpServerDetailView";
25
+ import type { UseMcpServerReturn } from "../useMcpServer";
26
+
27
+ // The kebab menu and dialogs portal their content; without a StigmerProvider
28
+ // the portal container is null — pin it to document.body like sibling tests.
29
+ vi.mock("../../portal-container", () => ({
30
+ useStigmerPortalContainer: () => document.body,
31
+ }));
32
+
33
+ beforeAll(() => {
34
+ // happy-dom lacks ResizeObserver, which Base UI positioners observe.
35
+ if (!("ResizeObserver" in globalThis)) {
36
+ (globalThis as unknown as { ResizeObserver: unknown }).ResizeObserver =
37
+ class {
38
+ observe() {}
39
+ unobserve() {}
40
+ disconnect() {}
41
+ };
42
+ }
43
+ });
44
+
45
+ afterEach(cleanup);
46
+
47
+ const ORG = "acme";
48
+ const SLUG = "order-management-api";
49
+
50
+ /** Frozen discovery instant — fixtures must never read the real clock. */
51
+ const DISCOVERED_AT = new Date("2026-07-20T09:30:00Z");
52
+
53
+ /** The server as registered: HTTP transport, one secret env var, no tools. */
54
+ function buildBaseServer(): McpServer {
55
+ const server = samples.mcpServer({ name: "Order Management API", org: ORG, slug: SLUG });
56
+ server.spec = create(McpServerSpecSchema, {
57
+ description: "REST API for order lookup, inventory, and return processing.",
58
+ serverType: {
59
+ case: "http",
60
+ value: create(HttpServerConfigSchema, { url: "https://api.acme.com/mcp" }),
61
+ },
62
+ env: {
63
+ API_TOKEN: create(EnvVarDeclarationSchema, {
64
+ isSecret: true,
65
+ description: "Bearer token for the Order Management API",
66
+ }),
67
+ },
68
+ });
69
+ return server;
70
+ }
71
+
72
+ /** The same server after Connect: 3 discovered tools, 1 classified policy. */
73
+ function buildConnectedServer(): McpServer {
74
+ const server = buildBaseServer();
75
+ server.status = create(McpServerStatusSchema, {
76
+ validationState: ValidationState.valid,
77
+ discoveredCapabilities: create(DiscoveredCapabilitiesSchema, {
78
+ lastDiscoveredAt: timestampFromDate(DISCOVERED_AT),
79
+ tools: [
80
+ create(DiscoveredToolSchema, { name: "get_order", description: "Retrieve one order by ID." }),
81
+ create(DiscoveredToolSchema, { name: "list_orders", description: "List recent orders." }),
82
+ create(DiscoveredToolSchema, { name: "process_return", description: "Initiate a return and refund." }),
83
+ ],
84
+ }),
85
+ toolApprovals: [
86
+ create(ToolApprovalPolicySchema, {
87
+ toolName: "process_return",
88
+ message: "Process return for order '{{args.order_id}}'",
89
+ }),
90
+ ],
91
+ });
92
+ return server;
93
+ }
94
+
95
+ /** Hoisted-state bag in its settled, data-loaded shape. */
96
+ function loadedState(server: McpServer): UseMcpServerReturn {
97
+ return { mcpServer: server, isLoading: false, isRefetching: false, error: null, refetch: vi.fn() };
98
+ }
99
+
100
+ /**
101
+ * Mounts the view against a real Connect router transport. RPCs the test
102
+ * does not register fall through to Connect's `unimplemented`, which the
103
+ * SDK hooks degrade from — the same contract the tours rely on.
104
+ */
105
+ function renderView(
106
+ ui: ReactNode,
107
+ register: Parameters<typeof createRouterTransport>[0] = () => {},
108
+ ) {
109
+ const client = new Stigmer({
110
+ baseUrl: "/",
111
+ getAccessToken: () => "test-token",
112
+ customTransport: createRouterTransport(register),
113
+ });
114
+ return render(
115
+ <StigmerContext.Provider value={client}>{ui}</StigmerContext.Provider>,
116
+ );
117
+ }
118
+
119
+ describe("McpServerDetailView — hoisted mcpServerState", () => {
120
+ it("renders from injected state without issuing getByReference", async () => {
121
+ const getByReference = vi.fn(() => {
122
+ throw new ConnectError("must not fetch", Code.FailedPrecondition);
123
+ });
124
+
125
+ renderView(
126
+ <McpServerDetailView
127
+ org={ORG}
128
+ slug={SLUG}
129
+ mcpServerState={loadedState(buildBaseServer())}
130
+ />,
131
+ (router) => {
132
+ router.service(McpServerQueryController, { getByReference });
133
+ },
134
+ );
135
+
136
+ // Content is on screen synchronously — no skeleton frame first.
137
+ expect(
138
+ screen.getByRole("heading", { name: "Order Management API" }),
139
+ ).toBeTruthy();
140
+ expect(screen.queryByLabelText("Loading MCP server details")).toBeNull();
141
+
142
+ // Let pending microtasks (credentials/permission fetches) settle, then
143
+ // confirm the resource fetch never fired.
144
+ await screen.findByRole("tab", { name: /Tools/ });
145
+ expect(getByReference).not.toHaveBeenCalled();
146
+ });
147
+
148
+ it("shows the loading skeleton while the injected state is loading", () => {
149
+ renderView(
150
+ <McpServerDetailView
151
+ org={ORG}
152
+ slug={SLUG}
153
+ mcpServerState={{
154
+ mcpServer: null,
155
+ isLoading: true,
156
+ isRefetching: false,
157
+ error: null,
158
+ refetch: vi.fn(),
159
+ }}
160
+ />,
161
+ );
162
+ expect(screen.getByLabelText("Loading MCP server details")).toBeTruthy();
163
+ // A loading owner must never read as a missing resource.
164
+ expect(screen.queryByText("MCP Server not found")).toBeNull();
165
+ });
166
+
167
+ it("routes the error state's Retry to the injected refetch", () => {
168
+ const refetch = vi.fn();
169
+ renderView(
170
+ <McpServerDetailView
171
+ org={ORG}
172
+ slug={SLUG}
173
+ mcpServerState={{
174
+ mcpServer: null,
175
+ isLoading: false,
176
+ isRefetching: false,
177
+ error: new ConnectError("backend down", Code.Unavailable),
178
+ refetch,
179
+ }}
180
+ />,
181
+ );
182
+
183
+ fireEvent.click(screen.getByRole("button", { name: /retry/i }));
184
+ expect(refetch).toHaveBeenCalledTimes(1);
185
+ });
186
+
187
+ it("renders not-found when the injected state settles with no resource", () => {
188
+ renderView(
189
+ <McpServerDetailView
190
+ org={ORG}
191
+ slug={SLUG}
192
+ mcpServerState={{
193
+ mcpServer: null,
194
+ isLoading: false,
195
+ isRefetching: false,
196
+ error: null,
197
+ refetch: vi.fn(),
198
+ }}
199
+ />,
200
+ );
201
+ expect(screen.getByText("MCP Server not found")).toBeTruthy();
202
+ });
203
+ });
204
+
205
+ describe("McpServerDetailView — self-fetching mode (unchanged behaviour)", () => {
206
+ it("fetches by org/slug and renders the resolved server", async () => {
207
+ const getByReference = vi.fn(() => buildBaseServer());
208
+ renderView(<McpServerDetailView org={ORG} slug={SLUG} />, (router) => {
209
+ router.service(McpServerQueryController, { getByReference });
210
+ });
211
+
212
+ // First paint is the skeleton; content follows the fetch.
213
+ expect(screen.getByLabelText("Loading MCP server details")).toBeTruthy();
214
+ expect(
215
+ await screen.findByRole("heading", { name: "Order Management API" }),
216
+ ).toBeTruthy();
217
+ expect(getByReference).toHaveBeenCalledTimes(1);
218
+ });
219
+
220
+ it("renders not-found when the fetch 404s", async () => {
221
+ renderView(<McpServerDetailView org={ORG} slug={SLUG} />, (router) => {
222
+ router.service(McpServerQueryController, {
223
+ getByReference: () => {
224
+ throw new ConnectError("no such server", Code.NotFound);
225
+ },
226
+ });
227
+ });
228
+ expect(await screen.findByText("MCP Server not found")).toBeTruthy();
229
+ });
230
+ });
231
+
232
+ describe("McpServerDetailView — connect bar and capability tabs", () => {
233
+ it("offers Connect with the not-connected status before discovery", () => {
234
+ renderView(
235
+ <McpServerDetailView
236
+ org={ORG}
237
+ slug={SLUG}
238
+ mcpServerState={loadedState(buildBaseServer())}
239
+ />,
240
+ );
241
+ expect(screen.getByRole("button", { name: /^Connect$/ })).toBeTruthy();
242
+ expect(screen.getByText("Not connected yet")).toBeTruthy();
243
+ expect(
244
+ screen.getByText("Connect to this MCP server to discover its available tools."),
245
+ ).toBeTruthy();
246
+ });
247
+
248
+ it("offers Reconnect with the connected status and discovery date after discovery", () => {
249
+ renderView(
250
+ <McpServerDetailView
251
+ org={ORG}
252
+ slug={SLUG}
253
+ mcpServerState={loadedState(buildConnectedServer())}
254
+ />,
255
+ );
256
+ expect(screen.getByRole("button", { name: /Reconnect/ })).toBeTruthy();
257
+ expect(screen.getByText("Connected")).toBeTruthy();
258
+ expect(screen.getByText("Valid")).toBeTruthy();
259
+ expect(screen.getByText(/Discovered/)).toBeTruthy();
260
+ });
261
+
262
+ it("lists the discovered tools with the count badge", () => {
263
+ renderView(
264
+ <McpServerDetailView
265
+ org={ORG}
266
+ slug={SLUG}
267
+ mcpServerState={loadedState(buildConnectedServer())}
268
+ />,
269
+ );
270
+ const toolsTab = screen.getByRole("tab", { name: /Tools/ });
271
+ expect(toolsTab.textContent).toContain("3");
272
+ expect(screen.getByText("get_order")).toBeTruthy();
273
+ expect(screen.getByText("list_orders")).toBeTruthy();
274
+ expect(screen.getByText("process_return")).toBeTruthy();
275
+ });
276
+
277
+ it("groups the classified policy under Auto-classified with a badge of 1, not 3", () => {
278
+ renderView(
279
+ <McpServerDetailView
280
+ org={ORG}
281
+ slug={SLUG}
282
+ defaultCapabilityTab="policies"
283
+ mcpServerState={loadedState(buildConnectedServer())}
284
+ />,
285
+ );
286
+ const policiesTab = screen.getByRole("tab", { name: /Policies/ });
287
+ expect(policiesTab.textContent).toContain("1");
288
+ expect(policiesTab.textContent).not.toContain("3");
289
+
290
+ const panel = screen.getByRole("tabpanel");
291
+ expect(within(panel).getByText(/Auto-classified/)).toBeTruthy();
292
+ expect(within(panel).getByText("process_return")).toBeTruthy();
293
+ expect(within(panel).getByText("requires approval")).toBeTruthy();
294
+ });
295
+ });
296
+
297
+ describe("McpServerDetailView — tour scroll anchors", () => {
298
+ it("marks the Connection and Capabilities sections as scroll targets", () => {
299
+ const { container } = renderView(
300
+ <McpServerDetailView
301
+ org={ORG}
302
+ slug={SLUG}
303
+ mcpServerState={loadedState(buildBaseServer())}
304
+ />,
305
+ );
306
+ expect(
307
+ container.querySelector('[data-scroll-target="mcp-connection"]'),
308
+ ).toBeTruthy();
309
+ expect(
310
+ container.querySelector('[data-scroll-target="mcp-capabilities"]'),
311
+ ).toBeTruthy();
312
+ });
313
+ });
314
+
315
+ describe("McpServerDetailView — credential form gating", () => {
316
+ it("opens the credential form for the declared missing variable when asked", async () => {
317
+ renderView(
318
+ <McpServerDetailView
319
+ org={ORG}
320
+ slug={SLUG}
321
+ defaultShowCredentialForm
322
+ mcpServerState={loadedState(buildBaseServer())}
323
+ />,
324
+ );
325
+ expect(await screen.findByText("Credentials Required")).toBeTruthy();
326
+ // Scope to the credential form — the Environment section also lists
327
+ // API_TOKEN as a declaration.
328
+ const form = screen.getByRole("form", {
329
+ name: /Configure Credentials Required/i,
330
+ });
331
+ // Anchored: the secret input's reveal button is labelled "Show API_TOKEN".
332
+ expect(within(form).getByLabelText(/^API_TOKEN/)).toBeTruthy();
333
+ });
334
+
335
+ it("keeps the form closed by default", () => {
336
+ renderView(
337
+ <McpServerDetailView
338
+ org={ORG}
339
+ slug={SLUG}
340
+ mcpServerState={loadedState(buildBaseServer())}
341
+ />,
342
+ );
343
+ expect(screen.queryByText("Credentials Required")).toBeNull();
344
+ });
345
+ });
@@ -100,4 +100,17 @@ export type {
100
100
  McpServerCreationResult,
101
101
  } from "./McpServerCreationWizard.js";
102
102
 
103
+ // Individual wizard steps are exported as presentational building blocks:
104
+ // they are fully prop-driven (data, validationError, error, isCreating),
105
+ // which lets platform builders and guided tours/demos render any wizard
106
+ // state — including validation and submit failures — deterministically,
107
+ // without driving the stateful McpServerCreationWizard through user events.
108
+ export { IdentityTransportStep } from "./steps/IdentityTransportStep.js";
109
+ export type { IdentityTransportStepProps } from "./steps/IdentityTransportStep.js";
110
+ export { EnvironmentAuthStep } from "./steps/EnvironmentAuthStep.js";
111
+ export type { EnvironmentAuthStepProps } from "./steps/EnvironmentAuthStep.js";
112
+ export { ReviewStep } from "./steps/ReviewStep.js";
113
+ export type { ReviewStepProps } from "./steps/ReviewStep.js";
114
+
115
+ export { createInitialMcpServerWizardData } from "./steps/types.js";
103
116
  export type { McpServerWizardData } from "./steps/types.js";
@@ -16,6 +16,10 @@ export interface EnvironmentAuthStepProps {
16
16
  * Both sections are optional. Env vars start expanded if any exist;
17
17
  * auth starts collapsed unless already enabled. No validation gate —
18
18
  * the user can proceed with zero configuration.
19
+ *
20
+ * Fully presentational: form state lives in the `data` prop and edits
21
+ * flow out through `updateData`, so standalone consumers (embedded
22
+ * builders, guided tours) can render any configuration state from props.
19
23
  */
20
24
  export function EnvironmentAuthStep({
21
25
  data,
@@ -23,6 +23,12 @@ const SLUG_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
23
23
  *
24
24
  * Transport type uses a radio group (mutually exclusive modes).
25
25
  * Conditional fields render based on the selected transport.
26
+ *
27
+ * Fully presentational: state lives in the `data` prop and every edit
28
+ * flows out through `updateData`. Inside `McpServerCreationWizard` that
29
+ * state comes from `useWizardState`; standalone consumers (embedded
30
+ * builders, guided tours) can render any form state — including a
31
+ * `validationError` — deterministically from props.
26
32
  */
27
33
  export function IdentityTransportStep({
28
34
  data,
@@ -195,7 +201,9 @@ export function IdentityTransportStep({
195
201
  </fieldset>
196
202
  </div>
197
203
 
198
- <fieldset className="space-y-3">
204
+ {/* data-scroll-target: guided tours/demos scroll the transport config
205
+ (the step's decision point) into view within the wizard's scroll area. */}
206
+ <fieldset className="space-y-3" data-scroll-target="mcp-transport">
199
207
  <legend className="text-sm font-medium text-foreground">
200
208
  Transport <span className="text-destructive">*</span>
201
209
  </legend>
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { useMemo } from "react";
3
+ import { useEffect, useMemo, useRef } from "react";
4
4
  import { cn } from "@stigmer/theme";
5
5
  import { buildMcpServerProto, getUserMessage, serializeManifest } from "@stigmer/sdk";
6
6
  import type { McpServerInput } from "@stigmer/sdk";
@@ -20,6 +20,11 @@ export interface ReviewStepProps {
20
20
  * Shows a summary card with key configuration details and a full
21
21
  * YAML preview of the MCP server that will be created. The "Create"
22
22
  * action is in the WizardNav footer, not in this component.
23
+ *
24
+ * Fully presentational: renders entirely from props, including the
25
+ * `isCreating` in-flight state and a submit `error`. Standalone
26
+ * consumers (embedded builders, guided tours) can therefore depict
27
+ * any review state — success preview or failure — deterministically.
23
28
  */
24
29
  export function ReviewStep({
25
30
  org,
@@ -27,6 +32,17 @@ export function ReviewStep({
27
32
  isCreating,
28
33
  error,
29
34
  }: ReviewStepProps) {
35
+ const errorRef = useRef<HTMLDivElement>(null);
36
+
37
+ // The error panel renders below the (tall) YAML preview inside the wizard's
38
+ // internal scroll area, so a failed create could otherwise report itself
39
+ // out of view. Bring it into view whenever a new error arrives.
40
+ useEffect(() => {
41
+ if (error) {
42
+ errorRef.current?.scrollIntoView({ block: "nearest" });
43
+ }
44
+ }, [error]);
45
+
30
46
  const mcpServerInput = useMemo(
31
47
  () => buildMcpServerInput(org, data),
32
48
  [org, data],
@@ -113,6 +129,7 @@ export function ReviewStep({
113
129
  {/* Error display */}
114
130
  {error && (
115
131
  <div
132
+ ref={errorRef}
116
133
  className="rounded-md border border-destructive bg-muted-faint px-4 py-3"
117
134
  role="alert"
118
135
  >
@@ -17,6 +17,8 @@ export interface UseMcpServerCountOptions {
17
17
  * @default "org"
18
18
  */
19
19
  readonly scope?: ResourceListScope;
20
+ /** Opaque token that forces a recount when its value changes. */
21
+ readonly refetchToken?: unknown;
20
22
  }
21
23
 
22
24
  /** Return value of {@link useMcpServerCount}. */
@@ -140,6 +140,7 @@ export function CreationPicker<TData>({
140
140
  description={`Create a blank ${resourceLabel} and configure it step by step.`}
141
141
  icon={<BlankPageIcon />}
142
142
  onClick={() => onSelect({ kind: "scratch" })}
143
+ cursorTarget="creation-path-scratch"
143
144
  />
144
145
 
145
146
  {/* Browse templates */}
@@ -150,6 +151,7 @@ export function CreationPicker<TData>({
150
151
  icon={<GridIcon />}
151
152
  onClick={() => setView("gallery")}
152
153
  badge={`${templates.length} available`}
154
+ cursorTarget="creation-path-template"
153
155
  />
154
156
  )}
155
157
 
@@ -159,6 +161,7 @@ export function CreationPicker<TData>({
159
161
  description="Upload a YAML or JSON configuration file."
160
162
  icon={<UploadIcon />}
161
163
  onClick={() => onSelect({ kind: "import" })}
164
+ cursorTarget="creation-path-import"
162
165
  />
163
166
  </div>
164
167
  </div>
@@ -175,17 +178,21 @@ function OptionCard({
175
178
  icon,
176
179
  onClick,
177
180
  badge,
181
+ cursorTarget,
178
182
  }: {
179
183
  readonly title: string;
180
184
  readonly description: string;
181
185
  readonly icon: React.ReactNode;
182
186
  readonly onClick: () => void;
183
187
  readonly badge?: string;
188
+ /** `data-cursor-target` hook for guided tours and demo playbacks. */
189
+ readonly cursorTarget?: string;
184
190
  }) {
185
191
  return (
186
192
  <button
187
193
  type="button"
188
194
  onClick={onClick}
195
+ data-cursor-target={cursorTarget}
189
196
  className={cn(
190
197
  "group flex flex-col items-center gap-3 rounded-lg border border-border bg-card p-6 text-center",
191
198
  "transition-colors hover:border-primary hover:bg-accent",
@@ -90,6 +90,7 @@ export function WizardNav({
90
90
  type="button"
91
91
  onClick={onNext}
92
92
  disabled={nextDisabled || isSubmitting}
93
+ data-cursor-target="wizard-next"
93
94
  className={cn(
94
95
  "inline-flex items-center gap-1.5 rounded-md bg-primary px-4 py-1.5 text-sm font-medium text-primary-foreground transition-colors",
95
96
  "hover:bg-primary-hover",
@@ -9,6 +9,13 @@ export interface SectionProps {
9
9
  /** Arbitrary action elements rendered in the header row (e.g. expand, share). */
10
10
  readonly headerActions?: ReactNode;
11
11
  readonly onEdit?: () => void;
12
+ /**
13
+ * Stable id emitted as `data-scroll-target` on the section element, so
14
+ * guided tours and demos can scroll the section into view by name (the
15
+ * same convention as `data-cursor-target`). Purely an annotation — no
16
+ * behavior attaches to it here.
17
+ */
18
+ readonly scrollTarget?: string;
12
19
  readonly children: ReactNode;
13
20
  readonly className?: string;
14
21
  }
@@ -26,11 +33,12 @@ export function Section({
26
33
  count,
27
34
  headerActions,
28
35
  onEdit,
36
+ scrollTarget,
29
37
  children,
30
38
  className,
31
39
  }: SectionProps) {
32
40
  return (
33
- <section className={className}>
41
+ <section className={className} data-scroll-target={scrollTarget}>
34
42
  <div className="mb-2 flex items-center gap-2">
35
43
  <h3 className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
36
44
  {title}
@@ -112,6 +112,17 @@ export interface ResourceWorkbenchProps<TData = SearchResult> {
112
112
  /** Extracts a unique ID from an item (for selection tracking). */
113
113
  readonly getItemId?: (item: TData) => string;
114
114
 
115
+ // --- Refresh -----------------------------------------------------------
116
+
117
+ /**
118
+ * Opaque token that forces a background refetch of the list whenever
119
+ * its value changes — the current view stays rendered with a refetch
120
+ * shimmer (no remount flash), and pagination and sort are preserved.
121
+ * Bump it after an out-of-band mutation (applying a manifest, deleting
122
+ * a row) so the change appears without a reload.
123
+ */
124
+ readonly refetchToken?: unknown;
125
+
115
126
  // --- Empty & error states ----------------------------------------------
116
127
 
117
128
  /** Icon for the empty state. */
@@ -210,6 +221,7 @@ export function ResourceWorkbench<TData = SearchResult>({
210
221
  onStateChange,
211
222
  enableSelection = false,
212
223
  getItemId = defaultGetId as (item: TData) => string,
224
+ refetchToken,
213
225
  emptyIcon,
214
226
  emptyTitle = "No resources found",
215
227
  emptyDescription = "Try adjusting your search or filters.",
@@ -255,6 +267,7 @@ export function ResourceWorkbench<TData = SearchResult>({
255
267
  onSortChange: filtersHook.setSort,
256
268
  columns: columns as WorkbenchColumnDef<TData>[],
257
269
  enableSelection,
270
+ refetchToken,
258
271
  });
259
272
 
260
273
  // --- Selection ---------------------------------------------------------
@@ -57,6 +57,15 @@ export interface UseResourceCollectionOptions<TData = SearchResult> {
57
57
  readonly columns?: readonly WorkbenchColumnDef<TData>[];
58
58
  /** Enable row selection state tracking. @default false */
59
59
  readonly enableSelection?: boolean;
60
+ /**
61
+ * Opaque token that forces a background refetch whenever its value
62
+ * changes. Use it to re-read the list after an out-of-band mutation
63
+ * (e.g. applying a manifest or deleting a row) without remounting:
64
+ * bump a counter and pass it here. The current page stays rendered
65
+ * with an `isRefetching` shimmer — no full-list flash. A change to
66
+ * this token alone never resets pagination or sort.
67
+ */
68
+ readonly refetchToken?: unknown;
60
69
  }
61
70
 
62
71
  // ---------------------------------------------------------------------------
@@ -148,6 +157,7 @@ export function useResourceCollection<TData = SearchResult>(
148
157
  onSortChange,
149
158
  columns = [],
150
159
  enableSelection = false,
160
+ refetchToken,
151
161
  } = options;
152
162
 
153
163
  // --- Data fetching via useFetch -----------------------------------------
@@ -169,7 +179,7 @@ export function useResourceCollection<TData = SearchResult>(
169
179
  };
170
180
  }
171
181
  : null,
172
- [listFn, org, query, scope, page, pageSize],
182
+ [listFn, org, query, scope, page, pageSize, refetchToken],
173
183
  INITIAL_DATA,
174
184
  );
175
185
 
@@ -16,6 +16,13 @@ export interface UseResourceCountOptions {
16
16
  * @default "org"
17
17
  */
18
18
  readonly scope?: ResourceListScope;
19
+ /**
20
+ * Opaque token that forces a recount whenever its value changes. Use
21
+ * it to refresh the count after an out-of-band mutation (e.g. applying
22
+ * a manifest) without remounting. Mirrors the `refetchToken` on
23
+ * {@link useResourceCollection}.
24
+ */
25
+ readonly refetchToken?: unknown;
19
26
  }
20
27
 
21
28
  export interface UseResourceCountReturn {
@@ -52,6 +59,7 @@ export function useResourceCount(
52
59
  ): UseResourceCountReturn {
53
60
  const query = options?.query;
54
61
  const scope = options?.scope ?? "org";
62
+ const refetchToken = options?.refetchToken;
55
63
 
56
64
  const { data: count, isLoading, isRefetching, error, refetch } = useFetch<number | undefined>(
57
65
  org
@@ -66,7 +74,7 @@ export function useResourceCount(
66
74
  return result.totalCount;
67
75
  }
68
76
  : null,
69
- [listFn, org, query, scope],
77
+ [listFn, org, query, scope, refetchToken],
70
78
  undefined,
71
79
  );
72
80