@stigmer/react 3.4.0 → 3.5.0

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 (208) hide show
  1. package/agent/AgentDetailView.d.ts +14 -4
  2. package/agent/AgentDetailView.d.ts.map +1 -1
  3. package/agent/AgentDetailView.js +73 -11
  4. package/agent/AgentDetailView.js.map +1 -1
  5. package/agent/internal/agentToInput.d.ts +3 -0
  6. package/agent/internal/agentToInput.d.ts.map +1 -1
  7. package/agent/internal/agentToInput.js +21 -1
  8. package/agent/internal/agentToInput.js.map +1 -1
  9. package/agent/useAgentCount.d.ts +2 -0
  10. package/agent/useAgentCount.d.ts.map +1 -1
  11. package/agent/useAgentCount.js.map +1 -1
  12. package/api-key/ApiKeyListPanel.d.ts +8 -1
  13. package/api-key/ApiKeyListPanel.d.ts.map +1 -1
  14. package/api-key/ApiKeyListPanel.js +9 -7
  15. package/api-key/ApiKeyListPanel.js.map +1 -1
  16. package/api-key/CreateApiKeyForm.d.ts +7 -1
  17. package/api-key/CreateApiKeyForm.d.ts.map +1 -1
  18. package/api-key/CreateApiKeyForm.js +2 -2
  19. package/api-key/CreateApiKeyForm.js.map +1 -1
  20. package/datastore/useDatastoreCount.d.ts +2 -0
  21. package/datastore/useDatastoreCount.d.ts.map +1 -1
  22. package/datastore/useDatastoreCount.js.map +1 -1
  23. package/dependency-graph/DependencyTreeNode.d.ts.map +1 -1
  24. package/dependency-graph/DependencyTreeNode.js +8 -0
  25. package/dependency-graph/DependencyTreeNode.js.map +1 -1
  26. package/dependency-graph/types.d.ts +20 -8
  27. package/dependency-graph/types.d.ts.map +1 -1
  28. package/dependency-graph/useDependencyGraph.d.ts.map +1 -1
  29. package/dependency-graph/useDependencyGraph.js +21 -1
  30. package/dependency-graph/useDependencyGraph.js.map +1 -1
  31. package/index.d.ts +6 -4
  32. package/index.d.ts.map +1 -1
  33. package/index.js +6 -2
  34. package/index.js.map +1 -1
  35. package/internal/scroll-area.d.ts +4 -0
  36. package/internal/scroll-area.d.ts.map +1 -0
  37. package/internal/scroll-area.js +15 -0
  38. package/internal/scroll-area.js.map +1 -0
  39. package/internal/tooltip.d.ts +7 -0
  40. package/internal/tooltip.d.ts.map +1 -0
  41. package/internal/tooltip.js +31 -0
  42. package/internal/tooltip.js.map +1 -0
  43. package/manifest/ApplyManifestDialog.d.ts +10 -5
  44. package/manifest/ApplyManifestDialog.d.ts.map +1 -1
  45. package/manifest/ApplyManifestDialog.js +30 -13
  46. package/manifest/ApplyManifestDialog.js.map +1 -1
  47. package/manifest/useApplyManifest.d.ts +6 -2
  48. package/manifest/useApplyManifest.d.ts.map +1 -1
  49. package/manifest/useApplyManifest.js +5 -5
  50. package/manifest/useApplyManifest.js.map +1 -1
  51. package/mcp-server/McpServerDetailView.d.ts +38 -2
  52. package/mcp-server/McpServerDetailView.d.ts.map +1 -1
  53. package/mcp-server/McpServerDetailView.js +21 -5
  54. package/mcp-server/McpServerDetailView.js.map +1 -1
  55. package/mcp-server/StdioSandboxNotice.d.ts +12 -11
  56. package/mcp-server/StdioSandboxNotice.d.ts.map +1 -1
  57. package/mcp-server/StdioSandboxNotice.js +13 -12
  58. package/mcp-server/StdioSandboxNotice.js.map +1 -1
  59. package/mcp-server/index.d.ts +7 -0
  60. package/mcp-server/index.d.ts.map +1 -1
  61. package/mcp-server/index.js +9 -0
  62. package/mcp-server/index.js.map +1 -1
  63. package/mcp-server/steps/EnvironmentAuthStep.d.ts +4 -0
  64. package/mcp-server/steps/EnvironmentAuthStep.d.ts.map +1 -1
  65. package/mcp-server/steps/EnvironmentAuthStep.js +4 -0
  66. package/mcp-server/steps/EnvironmentAuthStep.js.map +1 -1
  67. package/mcp-server/steps/IdentityTransportStep.d.ts +6 -0
  68. package/mcp-server/steps/IdentityTransportStep.d.ts.map +1 -1
  69. package/mcp-server/steps/IdentityTransportStep.js +7 -1
  70. package/mcp-server/steps/IdentityTransportStep.js.map +1 -1
  71. package/mcp-server/steps/ReviewStep.d.ts +5 -0
  72. package/mcp-server/steps/ReviewStep.d.ts.map +1 -1
  73. package/mcp-server/steps/ReviewStep.js +16 -2
  74. package/mcp-server/steps/ReviewStep.js.map +1 -1
  75. package/mcp-server/useMcpServerCount.d.ts +2 -0
  76. package/mcp-server/useMcpServerCount.d.ts.map +1 -1
  77. package/mcp-server/useMcpServerCount.js.map +1 -1
  78. package/package.json +4 -4
  79. package/resource-creation/CreationPicker.d.ts.map +1 -1
  80. package/resource-creation/CreationPicker.js +3 -3
  81. package/resource-creation/CreationPicker.js.map +1 -1
  82. package/resource-creation/WizardNav.d.ts.map +1 -1
  83. package/resource-creation/WizardNav.js +1 -1
  84. package/resource-creation/WizardNav.js.map +1 -1
  85. package/resource-creation/templates/mcp-server-templates.d.ts.map +1 -1
  86. package/resource-creation/templates/mcp-server-templates.js +17 -14
  87. package/resource-creation/templates/mcp-server-templates.js.map +1 -1
  88. package/resource-detail/Section.d.ts +8 -1
  89. package/resource-detail/Section.d.ts.map +1 -1
  90. package/resource-detail/Section.js +2 -2
  91. package/resource-detail/Section.js.map +1 -1
  92. package/resource-workbench/components/ResourceWorkbench.d.ts +9 -1
  93. package/resource-workbench/components/ResourceWorkbench.d.ts.map +1 -1
  94. package/resource-workbench/components/ResourceWorkbench.js +2 -1
  95. package/resource-workbench/components/ResourceWorkbench.js.map +1 -1
  96. package/resource-workbench/hooks/useResourceCollection.d.ts +9 -0
  97. package/resource-workbench/hooks/useResourceCollection.d.ts.map +1 -1
  98. package/resource-workbench/hooks/useResourceCollection.js +2 -2
  99. package/resource-workbench/hooks/useResourceCollection.js.map +1 -1
  100. package/search/useResourceCount.d.ts +7 -0
  101. package/search/useResourceCount.d.ts.map +1 -1
  102. package/search/useResourceCount.js +2 -1
  103. package/search/useResourceCount.js.map +1 -1
  104. package/session/NewSessionViewer.d.ts.map +1 -1
  105. package/session/NewSessionViewer.js +17 -3
  106. package/session/NewSessionViewer.js.map +1 -1
  107. package/session/SessionViewer.d.ts +2 -1
  108. package/session/SessionViewer.d.ts.map +1 -1
  109. package/session/SessionViewer.js +19 -11
  110. package/session/SessionViewer.js.map +1 -1
  111. package/session/SessionViewerLayout.d.ts +90 -0
  112. package/session/SessionViewerLayout.d.ts.map +1 -0
  113. package/session/SessionViewerLayout.js +32 -0
  114. package/session/SessionViewerLayout.js.map +1 -0
  115. package/session/index.d.ts +4 -0
  116. package/session/index.d.ts.map +1 -1
  117. package/session/index.js +10 -0
  118. package/session/index.js.map +1 -1
  119. package/session/useSessionRailViews.d.ts +2 -2
  120. package/sidebar/SettingsSidebar.d.ts +62 -0
  121. package/sidebar/SettingsSidebar.d.ts.map +1 -0
  122. package/sidebar/SettingsSidebar.js +60 -0
  123. package/sidebar/SettingsSidebar.js.map +1 -0
  124. package/sidebar/WorkspaceSidebar.d.ts +87 -0
  125. package/sidebar/WorkspaceSidebar.d.ts.map +1 -0
  126. package/sidebar/WorkspaceSidebar.js +95 -0
  127. package/sidebar/WorkspaceSidebar.js.map +1 -0
  128. package/sidebar/chrome.d.ts +48 -0
  129. package/sidebar/chrome.d.ts.map +1 -0
  130. package/sidebar/chrome.js +32 -0
  131. package/sidebar/chrome.js.map +1 -0
  132. package/sidebar/index.d.ts +6 -0
  133. package/sidebar/index.d.ts.map +1 -0
  134. package/sidebar/index.js +9 -0
  135. package/sidebar/index.js.map +1 -0
  136. package/sidebar/types.d.ts +54 -0
  137. package/sidebar/types.d.ts.map +1 -0
  138. package/sidebar/types.js +2 -0
  139. package/sidebar/types.js.map +1 -0
  140. package/skill/useSkillCount.d.ts +2 -0
  141. package/skill/useSkillCount.d.ts.map +1 -1
  142. package/skill/useSkillCount.js.map +1 -1
  143. package/src/agent/AgentDetailView.tsx +198 -11
  144. package/src/agent/__tests__/AgentDetailView.datastores.test.tsx +166 -0
  145. package/src/agent/internal/__tests__/agentToInput.test.ts +154 -0
  146. package/src/agent/internal/agentToInput.ts +25 -7
  147. package/src/agent/useAgentCount.ts +2 -0
  148. package/src/api-key/ApiKeyListPanel.tsx +17 -4
  149. package/src/api-key/CreateApiKeyForm.tsx +8 -1
  150. package/src/api-key/__tests__/CreateApiKeyForm.test.tsx +58 -0
  151. package/src/datastore/useDatastoreCount.ts +2 -0
  152. package/src/dependency-graph/DependencyTreeNode.tsx +8 -0
  153. package/src/dependency-graph/__tests__/useDependencyGraph.test.ts +108 -0
  154. package/src/dependency-graph/types.ts +20 -8
  155. package/src/dependency-graph/useDependencyGraph.ts +23 -1
  156. package/src/index.ts +24 -0
  157. package/src/internal/scroll-area.tsx +37 -0
  158. package/src/internal/tooltip.tsx +66 -0
  159. package/src/manifest/ApplyManifestDialog.tsx +40 -16
  160. package/src/manifest/__tests__/ApplyManifestDialog.test.tsx +147 -0
  161. package/src/manifest/__tests__/useApplyManifest.test.tsx +6 -6
  162. package/src/manifest/useApplyManifest.ts +12 -8
  163. package/src/mcp-server/McpServerDetailView.tsx +52 -4
  164. package/src/mcp-server/StdioSandboxNotice.tsx +16 -14
  165. package/src/mcp-server/__tests__/McpServerDetailView.test.tsx +345 -0
  166. package/src/mcp-server/__tests__/StdioSandboxNotice.test.tsx +3 -2
  167. package/src/mcp-server/index.ts +13 -0
  168. package/src/mcp-server/steps/EnvironmentAuthStep.tsx +4 -0
  169. package/src/mcp-server/steps/IdentityTransportStep.tsx +16 -1
  170. package/src/mcp-server/steps/ReviewStep.tsx +18 -1
  171. package/src/mcp-server/useMcpServerCount.ts +2 -0
  172. package/src/resource-creation/CreationPicker.tsx +7 -0
  173. package/src/resource-creation/WizardNav.tsx +1 -0
  174. package/src/resource-creation/templates/mcp-server-templates.ts +17 -14
  175. package/src/resource-detail/Section.tsx +9 -1
  176. package/src/resource-workbench/components/ResourceWorkbench.tsx +13 -0
  177. package/src/resource-workbench/hooks/useResourceCollection.ts +11 -1
  178. package/src/search/useResourceCount.ts +9 -1
  179. package/src/session/NewSessionViewer.tsx +48 -61
  180. package/src/session/SessionViewer.tsx +71 -86
  181. package/src/session/SessionViewerLayout.tsx +131 -0
  182. package/src/session/__tests__/SessionViewerLayout.test.tsx +140 -0
  183. package/src/session/index.ts +14 -0
  184. package/src/session/useSessionRailViews.tsx +2 -2
  185. package/src/sidebar/SettingsSidebar.tsx +173 -0
  186. package/src/sidebar/WorkspaceSidebar.tsx +386 -0
  187. package/src/sidebar/__tests__/SettingsSidebar.test.tsx +107 -0
  188. package/src/sidebar/__tests__/WorkspaceSidebar.test.tsx +236 -0
  189. package/src/sidebar/chrome.tsx +129 -0
  190. package/src/sidebar/index.ts +16 -0
  191. package/src/sidebar/types.ts +55 -0
  192. package/src/skill/useSkillCount.ts +2 -0
  193. package/src/test/__tests__/samples.test.ts +161 -2
  194. package/src/test/index.ts +1 -1
  195. package/src/test/samples.ts +157 -14
  196. package/src/workflow/useWorkflowCount.ts +2 -0
  197. package/styles.css +1 -1
  198. package/test/index.d.ts +1 -1
  199. package/test/index.d.ts.map +1 -1
  200. package/test/index.js +1 -1
  201. package/test/index.js.map +1 -1
  202. package/test/samples.d.ts +100 -7
  203. package/test/samples.d.ts.map +1 -1
  204. package/test/samples.js +133 -14
  205. package/test/samples.js.map +1 -1
  206. package/workflow/useWorkflowCount.d.ts +2 -0
  207. package/workflow/useWorkflowCount.d.ts.map +1 -1
  208. package/workflow/useWorkflowCount.js.map +1 -1
package/src/index.ts CHANGED
@@ -156,7 +156,9 @@ export {
156
156
  channelSessionExternalUserKey,
157
157
  SessionViewer,
158
158
  NewSessionViewer,
159
+ SessionViewerLayout,
159
160
  useSessionPanel,
161
+ useSessionRailViews,
160
162
  usePlanDraft,
161
163
  planDraftKey,
162
164
  PlanEditor,
@@ -201,8 +203,10 @@ export type {
201
203
  SearchResultGroup,
202
204
  SessionViewerProps,
203
205
  NewSessionViewerProps,
206
+ SessionViewerLayoutProps,
204
207
  SessionPanelController,
205
208
  UseSessionPanelOptions,
209
+ UseSessionRailViewsOptions,
206
210
  PlanDraftController,
207
211
  PlanEditorProps,
208
212
  PlanStreamingDocumentProps,
@@ -526,6 +530,10 @@ export {
526
530
  useUpdateMcpServer,
527
531
  mcpServerToInput,
528
532
  toServerKey,
533
+ IdentityTransportStep,
534
+ EnvironmentAuthStep,
535
+ ReviewStep,
536
+ createInitialMcpServerWizardData,
529
537
  } from "./mcp-server/index.js";
530
538
  export type {
531
539
  UseMcpServerReturn,
@@ -563,6 +571,9 @@ export type {
563
571
  McpServerCreationWizardProps,
564
572
  McpServerCreationResult,
565
573
  McpServerWizardData,
574
+ IdentityTransportStepProps,
575
+ EnvironmentAuthStepProps,
576
+ ReviewStepProps,
566
577
  } from "./mcp-server/index.js";
567
578
 
568
579
  // Skill — data hooks, upload, file browser, and mutation
@@ -1039,6 +1050,19 @@ export { UsageSection } from "./settings/index.js";
1039
1050
  export { UserMenu } from "./user/index.js";
1040
1051
  export type { UserMenuProps } from "./user/index.js";
1041
1052
 
1053
+ // Sidebar — the console's navigation chrome (workspace + settings zones).
1054
+ // The web console, desktop app, and documentation tours all render these;
1055
+ // hosts inject routing via `renderLink` and identity via the footer slot.
1056
+ export { WorkspaceSidebar, SettingsSidebar } from "./sidebar/index.js";
1057
+ export type {
1058
+ WorkspaceSidebarProps,
1059
+ WorkspaceSidebarActivity,
1060
+ WorkspaceNavId,
1061
+ SettingsSidebarProps,
1062
+ SidebarLinkRenderProps,
1063
+ RenderSidebarLink,
1064
+ } from "./sidebar/index.js";
1065
+
1042
1066
  // API Key — data hooks, behavior hooks, and styled components for API key lifecycle
1043
1067
  export {
1044
1068
  useApiKeyList,
@@ -0,0 +1,37 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { cn } from "@stigmer/theme";
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // SDK-internal thin-scrollbar scroll container.
8
+ //
9
+ // NOT exported from @stigmer/react. Plain CSS (no primitive library): a thin
10
+ // token-colored scrollbar over native overflow, so long lists (sidebar
11
+ // recents) scroll like the console everywhere the SDK renders.
12
+ // ---------------------------------------------------------------------------
13
+
14
+ function ScrollArea({
15
+ className,
16
+ children,
17
+ ...props
18
+ }: React.ComponentPropsWithoutRef<"div">) {
19
+ return (
20
+ <div
21
+ className={cn(
22
+ "relative overflow-auto",
23
+ "[scrollbar-width:thin] [scrollbar-color:var(--color-border)_transparent]",
24
+ "[&::-webkit-scrollbar]:w-2.5",
25
+ "[&::-webkit-scrollbar-track]:bg-transparent",
26
+ "[&::-webkit-scrollbar-thumb]:rounded-full",
27
+ "[&::-webkit-scrollbar-thumb]:bg-border",
28
+ className,
29
+ )}
30
+ {...props}
31
+ >
32
+ {children}
33
+ </div>
34
+ );
35
+ }
36
+
37
+ export { ScrollArea };
@@ -0,0 +1,66 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip";
5
+ import { cn } from "@stigmer/theme";
6
+ import { useStigmerPortalContainer } from "../portal-container.js";
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // SDK-internal styled Tooltip primitives over @base-ui/react.
10
+ //
11
+ // Like ./menu.tsx, these are NOT exported from @stigmer/react. They give SDK
12
+ // styled components (WorkspaceSidebar recents rows, and any future hover
13
+ // hints) one source of truth for tooltip styling, portaled into the Stigmer
14
+ // theme scope so tokens resolve in embeds.
15
+ //
16
+ // Portaled content uses popover-* / main-area tokens per DD-005; the ring
17
+ // follows the menu popup's established `ring-foreground/10` hairline.
18
+ // ---------------------------------------------------------------------------
19
+
20
+ function TooltipProvider(props: TooltipPrimitive.Provider.Props) {
21
+ return <TooltipPrimitive.Provider {...props} />;
22
+ }
23
+
24
+ function Tooltip(props: TooltipPrimitive.Root.Props) {
25
+ return <TooltipPrimitive.Root {...props} />;
26
+ }
27
+
28
+ function TooltipTrigger(props: TooltipPrimitive.Trigger.Props) {
29
+ return <TooltipPrimitive.Trigger {...props} />;
30
+ }
31
+
32
+ function TooltipContent({
33
+ side = "right",
34
+ sideOffset = 8,
35
+ className,
36
+ children,
37
+ ...props
38
+ }: TooltipPrimitive.Popup.Props &
39
+ Pick<TooltipPrimitive.Positioner.Props, "side" | "sideOffset">) {
40
+ const portalContainer = useStigmerPortalContainer();
41
+
42
+ return (
43
+ <TooltipPrimitive.Portal container={portalContainer}>
44
+ <TooltipPrimitive.Positioner
45
+ className="isolate z-50 outline-none"
46
+ side={side}
47
+ sideOffset={sideOffset}
48
+ >
49
+ <TooltipPrimitive.Popup
50
+ className={cn(
51
+ "bg-popover text-popover-foreground ring-foreground/10 z-50 max-w-64 rounded-lg px-2.5 py-1.5 text-sm shadow-md ring-1",
52
+ "data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95",
53
+ "data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
54
+ "duration-100",
55
+ className,
56
+ )}
57
+ {...props}
58
+ >
59
+ {children}
60
+ </TooltipPrimitive.Popup>
61
+ </TooltipPrimitive.Positioner>
62
+ </TooltipPrimitive.Portal>
63
+ );
64
+ }
65
+
66
+ export { Tooltip, TooltipProvider, TooltipTrigger, TooltipContent };
@@ -23,10 +23,15 @@ export interface ApplyManifestDialogProps {
23
23
  /** Target organization slug for documents that omit `metadata.org`. */
24
24
  readonly org: string;
25
25
  /**
26
- * Called after every document applied successfully, with the per-document
27
- * results in apply order. Use for navigation or list refresh.
26
+ * Called when the dialog closes after at least one document applied,
27
+ * with the applied entries in apply order on full success, and also
28
+ * when the operator dismisses the dialog after a partial apply (some
29
+ * documents applied, a later one failed). Use it to refresh a list so
30
+ * the newly applied resources appear without a reload. It never fires
31
+ * when nothing applied, and fires once per close. Mirrors
32
+ * {@link EditResourceYamlDialog}'s `onApplied`.
28
33
  */
29
- readonly onSuccess?: (entries: readonly ManifestPreviewEntry[]) => void;
34
+ readonly onApplied?: (applied: readonly ManifestPreviewEntry[]) => void;
30
35
  }
31
36
 
32
37
  /**
@@ -56,7 +61,7 @@ export interface ApplyManifestDialogProps {
56
61
  * open={open}
57
62
  * onOpenChange={setOpen}
58
63
  * org={activeOrg}
59
- * onSuccess={() => refetchList()}
64
+ * onApplied={() => refetchList()}
60
65
  * />
61
66
  * ```
62
67
  */
@@ -64,7 +69,7 @@ export function ApplyManifestDialog({
64
69
  open,
65
70
  onOpenChange,
66
71
  org,
67
- onSuccess,
72
+ onApplied,
68
73
  }: ApplyManifestDialogProps) {
69
74
  const dialogRef = useRef<HTMLDialogElement>(null);
70
75
  const fileInputRef = useRef<HTMLInputElement>(null);
@@ -90,12 +95,26 @@ export function ApplyManifestDialog({
90
95
  }
91
96
  }, [open, reset]);
92
97
 
98
+ // Close the dialog and, if any document applied this session, refresh
99
+ // the caller's list. This is the dismiss-after-partial path: some
100
+ // documents applied before a later one failed, and the operator closes
101
+ // the dialog rather than retrying — those applied resources must still
102
+ // appear. The full-success path in `handleApply` fires `onApplied`
103
+ // itself (it holds the fresh applied list), so it never routes here.
104
+ const closeAndMaybeRefresh = useCallback(() => {
105
+ onOpenChange(false);
106
+ const applied = (manifest.entries ?? []).filter(
107
+ (e) => e.status === "applied",
108
+ );
109
+ if (applied.length > 0) onApplied?.(applied);
110
+ }, [onOpenChange, manifest.entries, onApplied]);
111
+
93
112
  const handleCancel = useCallback(
94
113
  (e: React.SyntheticEvent) => {
95
114
  e.preventDefault();
96
- onOpenChange(false);
115
+ closeAndMaybeRefresh();
97
116
  },
98
- [onOpenChange],
117
+ [closeAndMaybeRefresh],
99
118
  );
100
119
 
101
120
  const handleFileChange = useCallback(
@@ -107,18 +126,23 @@ export function ApplyManifestDialog({
107
126
  );
108
127
 
109
128
  const handleApply = useCallback(async () => {
110
- const allApplied = await manifest.applyAll();
111
- if (allApplied && manifest.entries) {
112
- const count = manifest.entries.length;
129
+ const results = await manifest.applyAll();
130
+ const applied = results.filter((e) => e.status === "applied");
131
+ const allApplied = results.length > 0 && applied.length === results.length;
132
+ if (allApplied) {
113
133
  toast.success(
114
- count === 1 ? "Resource applied" : `${count} resources applied`,
134
+ applied.length === 1
135
+ ? "Resource applied"
136
+ : `${applied.length} resources applied`,
115
137
  );
116
138
  onOpenChange(false);
117
- onSuccess?.(manifest.entries);
139
+ onApplied?.(applied);
118
140
  }
119
- // On failure the dialog stays open — per-entry status shows what
120
- // applied and what failed.
121
- }, [manifest, onOpenChange, onSuccess]);
141
+ // On partial/total failure the dialog stays open — per-entry status
142
+ // shows what applied and what failed. Any entries that DID apply
143
+ // still refresh the caller's list when the dialog is dismissed
144
+ // (closeAndMaybeRefresh), so successful writes are never hidden.
145
+ }, [manifest, onOpenChange, onApplied]);
122
146
 
123
147
  const canApply =
124
148
  manifest.entries !== null &&
@@ -198,7 +222,7 @@ export function ApplyManifestDialog({
198
222
  <div className="flex gap-2">
199
223
  <button
200
224
  type="button"
201
- onClick={() => onOpenChange(false)}
225
+ onClick={closeAndMaybeRefresh}
202
226
  disabled={manifest.isApplying}
203
227
  className={cn(
204
228
  "rounded-md px-3 py-1.5 text-sm font-medium transition-colors",
@@ -0,0 +1,147 @@
1
+ // Tests for ApplyManifestDialog's onApplied contract — the refresh
2
+ // signal every Library list page wires to appear-without-reload. It must
3
+ // fire on full success AND when the operator dismisses the dialog after a
4
+ // partial apply (some documents applied, a later one failed), and must
5
+ // never fire when nothing applied. Historically onApplied's predecessor
6
+ // had zero coverage and stayed unwired at every call site; this locks the
7
+ // contract the list pages now depend on.
8
+
9
+ import { describe, it, expect, vi, beforeAll, afterEach } from "vitest";
10
+ import { render, screen, cleanup, fireEvent, waitFor } from "@testing-library/react";
11
+ import { ApplyManifestDialog } from "../ApplyManifestDialog";
12
+ import type {
13
+ ManifestPreviewEntry,
14
+ UseApplyManifestReturn,
15
+ } from "../useApplyManifest";
16
+
17
+ beforeAll(() => {
18
+ HTMLDialogElement.prototype.showModal = function showModal() {
19
+ this.open = true;
20
+ };
21
+ HTMLDialogElement.prototype.close = function close() {
22
+ this.open = false;
23
+ };
24
+ });
25
+
26
+ afterEach(() => {
27
+ cleanup();
28
+ vi.restoreAllMocks();
29
+ });
30
+
31
+ // A preview entry in a chosen terminal status; the document shape is only
32
+ // what the dialog's preview row and apply flow read.
33
+ function entry(
34
+ slug: string,
35
+ status: ManifestPreviewEntry["status"],
36
+ ): ManifestPreviewEntry {
37
+ return {
38
+ document: {
39
+ handler: { yamlKind: "datastore", displayName: "Datastore" },
40
+ name: slug,
41
+ slug,
42
+ org: "acme",
43
+ },
44
+ action: "create",
45
+ status,
46
+ } as ManifestPreviewEntry;
47
+ }
48
+
49
+ // The mock's mutable knobs: the entries the hook exposes and the terminal
50
+ // entries applyAll resolves to. Set per test before rendering.
51
+ let mockEntries: readonly ManifestPreviewEntry[] | null;
52
+ let applyAllResult: readonly ManifestPreviewEntry[];
53
+ const applyAllMock = vi.fn(async () => applyAllResult);
54
+
55
+ vi.mock("../useApplyManifest", () => ({
56
+ useApplyManifest: (): UseApplyManifestReturn =>
57
+ ({
58
+ content: "kind: Datastore",
59
+ setContent: vi.fn(),
60
+ readFile: vi.fn(),
61
+ entries: mockEntries,
62
+ validationError: null,
63
+ isValidating: false,
64
+ hasRedactedSecrets: false,
65
+ applyAll: applyAllMock,
66
+ isApplying: false,
67
+ reset: vi.fn(),
68
+ }) as UseApplyManifestReturn,
69
+ }));
70
+
71
+ // CodeMirror stays out of the test environment.
72
+ vi.mock("../YamlEditor", () => ({
73
+ YamlEditor: () => <div data-testid="yaml-editor" />,
74
+ }));
75
+
76
+ describe("ApplyManifestDialog — onApplied", () => {
77
+ it("fires with the applied entries on full success and closes", async () => {
78
+ mockEntries = [entry("a", "pending"), entry("b", "pending")];
79
+ applyAllResult = [entry("a", "applied"), entry("b", "applied")];
80
+ const onApplied = vi.fn();
81
+ const onOpenChange = vi.fn();
82
+
83
+ render(
84
+ <ApplyManifestDialog
85
+ open
86
+ onOpenChange={onOpenChange}
87
+ org="acme"
88
+ onApplied={onApplied}
89
+ />,
90
+ );
91
+
92
+ fireEvent.click(screen.getByRole("button", { name: /apply/i }));
93
+
94
+ await waitFor(() => expect(onApplied).toHaveBeenCalledTimes(1));
95
+ expect(
96
+ onApplied.mock.calls[0][0].map((e: ManifestPreviewEntry) => e.document.slug),
97
+ ).toEqual(["a", "b"]);
98
+ expect(onOpenChange).toHaveBeenCalledWith(false);
99
+ });
100
+
101
+ it("fires with only the applied subset when a partial apply is dismissed", async () => {
102
+ // Applied one, failed the next — the dialog stays open on failure.
103
+ mockEntries = [entry("a", "applied"), entry("b", "failed")];
104
+ applyAllResult = [entry("a", "applied"), entry("b", "failed")];
105
+ const onApplied = vi.fn();
106
+ const onOpenChange = vi.fn();
107
+
108
+ render(
109
+ <ApplyManifestDialog
110
+ open
111
+ onOpenChange={onOpenChange}
112
+ org="acme"
113
+ onApplied={onApplied}
114
+ />,
115
+ );
116
+
117
+ // Apply first: full success did not happen, so onApplied has not fired.
118
+ fireEvent.click(screen.getByRole("button", { name: /apply/i }));
119
+ await waitFor(() => expect(applyAllMock).toHaveBeenCalled());
120
+ expect(onApplied).not.toHaveBeenCalled();
121
+
122
+ // Dismiss with an applied entry present → refresh the list.
123
+ fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
124
+ expect(onApplied).toHaveBeenCalledTimes(1);
125
+ expect(
126
+ onApplied.mock.calls[0][0].map((e: ManifestPreviewEntry) => e.document.slug),
127
+ ).toEqual(["a"]);
128
+ expect(onOpenChange).toHaveBeenCalledWith(false);
129
+ });
130
+
131
+ it("does not fire when the dialog is dismissed with nothing applied", () => {
132
+ mockEntries = [entry("a", "pending")];
133
+ const onApplied = vi.fn();
134
+
135
+ render(
136
+ <ApplyManifestDialog
137
+ open
138
+ onOpenChange={vi.fn()}
139
+ org="acme"
140
+ onApplied={onApplied}
141
+ />,
142
+ );
143
+
144
+ fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
145
+ expect(onApplied).not.toHaveBeenCalled();
146
+ });
147
+ });
@@ -174,12 +174,12 @@ describe("useApplyManifest", () => {
174
174
  });
175
175
  await act(settleValidation);
176
176
 
177
- let allApplied = false;
177
+ let applied: readonly { status: string }[] = [];
178
178
  await act(async () => {
179
- allApplied = await result.current.applyAll();
179
+ applied = await result.current.applyAll();
180
180
  });
181
181
 
182
- expect(allApplied).toBe(true);
182
+ expect(applied.map((e) => e.status)).toEqual(["applied", "applied"]);
183
183
  expect(apply).toHaveBeenCalledTimes(2);
184
184
  expect(result.current.entries!.map((e) => e.status)).toEqual([
185
185
  "applied",
@@ -201,12 +201,12 @@ describe("useApplyManifest", () => {
201
201
  });
202
202
  await act(settleValidation);
203
203
 
204
- let allApplied = true;
204
+ let applied: readonly { status: string }[] = [];
205
205
  await act(async () => {
206
- allApplied = await result.current.applyAll();
206
+ applied = await result.current.applyAll();
207
207
  });
208
208
 
209
- expect(allApplied).toBe(false);
209
+ expect(applied.map((e) => e.status)).toEqual(["failed", "skipped"]);
210
210
  expect(apply).toHaveBeenCalledTimes(1);
211
211
  expect(result.current.entries!.map((e) => e.status)).toEqual([
212
212
  "failed",
@@ -55,9 +55,13 @@ export interface UseApplyManifestReturn {
55
55
  /**
56
56
  * Apply all documents sequentially in dependency order. Entries update
57
57
  * live. Stops at the first failure (later entries become `"skipped"`).
58
- * Resolves `true` when every document applied.
58
+ * Resolves to the final per-document entries with terminal statuses —
59
+ * the caller derives full success (`every applied`) and the applied
60
+ * subset (for a list refresh) without re-reading React state, which is
61
+ * still stale in the calling closure. Empty when there was nothing to
62
+ * apply.
59
63
  */
60
- readonly applyAll: () => Promise<boolean>;
64
+ readonly applyAll: () => Promise<readonly ManifestPreviewEntry[]>;
61
65
  /** `true` while applies are in flight. */
62
66
  readonly isApplying: boolean;
63
67
  /** Clear all state (content, preview, errors). */
@@ -194,8 +198,8 @@ export function useApplyManifest(org: string): UseApplyManifestReturn {
194
198
  return () => clearTimeout(timer);
195
199
  }, [content, stigmer, isApplying]);
196
200
 
197
- const applyAll = useCallback(async (): Promise<boolean> => {
198
- if (!entries || entries.length === 0) return false;
201
+ const applyAll = useCallback(async (): Promise<readonly ManifestPreviewEntry[]> => {
202
+ if (!entries || entries.length === 0) return [];
199
203
 
200
204
  // Invalidate any in-flight validation so it cannot clobber apply statuses.
201
205
  validateSeq.current++;
@@ -206,10 +210,10 @@ export function useApplyManifest(org: string): UseApplyManifestReturn {
206
210
  const working = entries.map((entry): ManifestPreviewEntry => ({ ...entry, status: "pending" }));
207
211
  const commit = () => setEntries([...working]);
208
212
 
209
- let allApplied = true;
213
+ let stopped = false;
210
214
 
211
215
  for (let i = 0; i < working.length; i++) {
212
- if (!allApplied) {
216
+ if (stopped) {
213
217
  working[i] = { ...working[i], status: "skipped" };
214
218
  continue;
215
219
  }
@@ -226,14 +230,14 @@ export function useApplyManifest(org: string): UseApplyManifestReturn {
226
230
  status: "failed",
227
231
  errorMessage: toError(err).message,
228
232
  };
229
- allApplied = false;
233
+ stopped = true;
230
234
  }
231
235
  commit();
232
236
  }
233
237
 
234
238
  commit();
235
239
  setIsApplying(false);
236
- return allApplied;
240
+ return working;
237
241
  }, [entries, stigmer]);
238
242
 
239
243
  const hasRedactedSecrets = useMemo(
@@ -14,6 +14,7 @@ import type { ToolApprovalPolicy, McpServerSpec } from "@stigmer/protos/ai/stigm
14
14
  import { ValidationState } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/status_pb";
15
15
  import type { EnvVarDeclaration } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/spec_pb";
16
16
  import { useMcpServer } from "./useMcpServer.js";
17
+ import type { UseMcpServerReturn } from "./useMcpServer.js";
17
18
  import { useUpdateMcpServer } from "./useUpdateMcpServer.js";
18
19
  import { mcpServerToInput } from "./internal/mcpServerToInput.js";
19
20
  import { useMcpServerConnect } from "./useMcpServerConnect.js";
@@ -51,6 +52,31 @@ export interface McpServerDetailViewProps {
51
52
  readonly org: string;
52
53
  /** MCP server slug (URL-friendly identifier unique within the org). */
53
54
  readonly slug: string;
55
+ /**
56
+ * Hoisted resource state from {@link useMcpServer}, for callers that
57
+ * already own the fetch. When provided, the view issues **no**
58
+ * `getByReference` RPC of its own and renders entirely from this state —
59
+ * loading, error, and not-found included, so a caller mid-fetch still
60
+ * gets the loading skeleton rather than a false not-found.
61
+ *
62
+ * Ownership contract: supplying this state means supplying its
63
+ * lifecycle. The view calls `mcpServerState.refetch()` after connect,
64
+ * disconnect, and inline edits, so the `refetch` you pass must refresh
65
+ * the data behind `mcpServer` or the view will render stale state after
66
+ * those actions.
67
+ *
68
+ * Two callers this serves:
69
+ * - A page that already fetched the server (e.g. for export/YAML
70
+ * actions) passes `useMcpServer(org, slug)` straight through instead
71
+ * of paying a duplicate RPC.
72
+ * - A guided tour or demo supplies frozen state
73
+ * (`{ mcpServer, isLoading: false, isRefetching: false, error: null,
74
+ * refetch: noop }`) so every depicted beat renders deterministically
75
+ * from data the tour owns (scenar-cloud DD-006).
76
+ *
77
+ * When omitted, the view fetches by `org`/`slug` itself, as before.
78
+ */
79
+ readonly mcpServerState?: UseMcpServerReturn;
54
80
  /**
55
81
  * Called once when the MCP server resource has been fetched successfully.
56
82
  * Provides the resource display name for use cases like breadcrumbs,
@@ -115,7 +141,8 @@ export interface McpServerDetailViewProps {
115
141
  /**
116
142
  * Detail view for an MCP Server integration.
117
143
  *
118
- * Fetches the server via {@link useMcpServer} internally and renders
144
+ * Fetches the server via {@link useMcpServer} internally or renders
145
+ * from hoisted state when the caller passes `mcpServerState` — and shows
119
146
  * its full configuration: validation banner (if invalid), header,
120
147
  * a **Connect bar** with a single Connect/Reconnect action, and a
121
148
  * **tabbed capabilities panel** (Tools, Policies, and optionally
@@ -135,10 +162,20 @@ export interface McpServerDetailViewProps {
135
162
  * // Minimal — self-contained, fetches its own data
136
163
  * <McpServerDetailView org="acme" slug="github" />
137
164
  * ```
165
+ *
166
+ * @example
167
+ * ```tsx
168
+ * // Hoisted — the page already fetched the server (e.g. for YAML
169
+ * // export actions), so pass that state through instead of paying a
170
+ * // second getByReference RPC.
171
+ * const state = useMcpServer(org, slug);
172
+ * <McpServerDetailView org={org} slug={slug} mcpServerState={state} />
173
+ * ```
138
174
  */
139
175
  export function McpServerDetailView({
140
176
  org,
141
177
  slug,
178
+ mcpServerState,
142
179
  onResourceLoad,
143
180
  defaultCapabilityTab = "tools",
144
181
  defaultShowCredentialForm = false,
@@ -150,7 +187,16 @@ export function McpServerDetailView({
150
187
  onResourceUpdated,
151
188
  className,
152
189
  }: McpServerDetailViewProps) {
153
- const { mcpServer, isLoading, error, refetch } = useMcpServer(org, slug);
190
+ // Hoisted-state mode: when the caller supplies `mcpServerState`, disable
191
+ // the internal fetch via the hook's documented null-skip (a stable no-op,
192
+ // so `fetched` reports isLoading=false with no RPC) and render from the
193
+ // caller's state instead — all four states (loading/error/not-found/data)
194
+ // transfer intact.
195
+ const fetched = useMcpServer(
196
+ mcpServerState ? null : org,
197
+ mcpServerState ? null : slug,
198
+ );
199
+ const { mcpServer, isLoading, error, refetch } = mcpServerState ?? fetched;
154
200
  const { update: updateMcpServer, isUpdating } = useUpdateMcpServer();
155
201
 
156
202
  const saveMcpField = useCallback(
@@ -493,7 +539,9 @@ export function McpServerDetailView({
493
539
  />
494
540
  )}
495
541
 
496
- <Section title="Connection">
542
+ {/* scrollTarget: guided tours/demos bring the connect flow into view
543
+ (see IdentityTransportStep's "mcp-transport" for the convention). */}
544
+ <Section title="Connection" scrollTarget="mcp-connection">
497
545
  <StdioSandboxNotice serverType={spec?.serverType} className="mb-3" />
498
546
  <OAuthRequiredNotice oauthOnly={spec?.auth?.oauthOnly} className="mb-3" />
499
547
  <ConnectBar
@@ -582,7 +630,7 @@ export function McpServerDetailView({
582
630
  />
583
631
  </dialog>
584
632
 
585
- <Section title="Capabilities">
633
+ <Section title="Capabilities" scrollTarget="mcp-capabilities">
586
634
  <Tabs
587
635
  tabs={capabilityTabs}
588
636
  activeTab={capabilityTab}
@@ -11,9 +11,10 @@ type McpServerType = McpServerSpec["serverType"];
11
11
  /**
12
12
  * Whether an MCP server's transport is stdio while connected to Stigmer Cloud.
13
13
  *
14
- * stdio servers run as a subprocess inside an isolated Daytona sandbox in the
15
- * cloud not on the user's own computer. This predicate identifies exactly
16
- * that combination so the connect UI can set accurate expectations. HTTP
14
+ * stdio is local-runner-only: cloud-hosted sessions refuse stdio servers at
15
+ * execution create, and the cloud connect flow refuses to spawn them. This
16
+ * predicate identifies exactly that combination so the UI can explain the
17
+ * policy and its remediation (run the session on a local runner). HTTP
17
18
  * servers and any transport in local mode return `false`.
18
19
  *
19
20
  * Pure and framework-free so it can be unit-tested without rendering.
@@ -34,15 +35,15 @@ export interface StdioSandboxNoticeProps {
34
35
  }
35
36
 
36
37
  /**
37
- * Sets accurate expectations for connecting an **stdio** MCP server from
38
- * **Stigmer Cloud**.
38
+ * Explains the local-runner-only policy for an **stdio** MCP server viewed
39
+ * from **Stigmer Cloud**.
39
40
  *
40
- * stdio servers are a supported cloud capability Stigmer runs them in an
41
- * isolated Daytona sandbox. But that sandbox is not the user's own machine,
42
- * so a server that expects access to local files, applications, or a private
43
- * network (e.g. a filesystem server) won't behave as the user intends. This
44
- * notice explains that up front; Connect itself stays enabled because
45
- * self-contained servers work fine.
41
+ * stdio servers spawn subprocesses on the machine that runs the agent, so
42
+ * they run only on local runners Stigmer-managed cloud compute refuses
43
+ * them (at execution create, and in the connect flow). The server remains
44
+ * fully usable on sessions that execute on a local runner (desktop app,
45
+ * `stigmer server`), where tools are discovered automatically at session
46
+ * start. This notice states that up front so the refusal never surprises.
46
47
  *
47
48
  * Self-gating: reads the deployment mode from context and renders `null`
48
49
  * unless the server is stdio and the backend is cloud. Callers render it
@@ -68,9 +69,10 @@ export function StdioSandboxNotice({
68
69
  >
69
70
  <SandboxIcon className="mt-0.5 size-4 shrink-0" />
70
71
  <p className="text-xs leading-relaxed">
71
- This stdio server runs in an isolated cloud sandbox, not on your own
72
- computer. Tools that need access to your local files, applications, or
73
- private network won&apos;t be available here.
72
+ This stdio server runs only on local runners it won&apos;t be
73
+ available to sessions on Stigmer-managed cloud compute. Run the
74
+ session on a local runner (desktop app or CLI) to use it, or choose
75
+ a remote (HTTP) server instead.
74
76
  </p>
75
77
  </div>
76
78
  );