@stigmer/react 0.0.84 → 0.0.86

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 (87) hide show
  1. package/demo/fixtures.d.ts +4 -0
  2. package/demo/fixtures.d.ts.map +1 -1
  3. package/demo/fixtures.js +4 -0
  4. package/demo/fixtures.js.map +1 -1
  5. package/demo/samples.d.ts +1 -0
  6. package/demo/samples.d.ts.map +1 -1
  7. package/demo/samples.js +1 -0
  8. package/demo/samples.js.map +1 -1
  9. package/execution/ArtifactPreviewModal.d.ts +78 -18
  10. package/execution/ArtifactPreviewModal.d.ts.map +1 -1
  11. package/execution/ArtifactPreviewModal.js +82 -60
  12. package/execution/ArtifactPreviewModal.js.map +1 -1
  13. package/execution/index.d.ts +2 -2
  14. package/execution/index.d.ts.map +1 -1
  15. package/execution/index.js +1 -1
  16. package/execution/index.js.map +1 -1
  17. package/index.d.ts +6 -4
  18. package/index.d.ts.map +1 -1
  19. package/index.js +4 -2
  20. package/index.js.map +1 -1
  21. package/library/ResourceListView.js +1 -1
  22. package/library/ResourceListView.js.map +1 -1
  23. package/mcp-server/McpServerConnectDialog.d.ts +51 -0
  24. package/mcp-server/McpServerConnectDialog.d.ts.map +1 -0
  25. package/mcp-server/McpServerConnectDialog.js +164 -0
  26. package/mcp-server/McpServerConnectDialog.js.map +1 -0
  27. package/mcp-server/McpServerDetailView.js +2 -2
  28. package/mcp-server/McpServerDetailView.js.map +1 -1
  29. package/mcp-server/McpServerPicker.d.ts.map +1 -1
  30. package/mcp-server/McpServerPicker.js +7 -1
  31. package/mcp-server/McpServerPicker.js.map +1 -1
  32. package/mcp-server/index.d.ts +2 -0
  33. package/mcp-server/index.d.ts.map +1 -1
  34. package/mcp-server/index.js +1 -0
  35. package/mcp-server/index.js.map +1 -1
  36. package/oauth-app/CreateOAuthAppForm.d.ts +41 -0
  37. package/oauth-app/CreateOAuthAppForm.d.ts.map +1 -0
  38. package/oauth-app/CreateOAuthAppForm.js +140 -0
  39. package/oauth-app/CreateOAuthAppForm.js.map +1 -0
  40. package/oauth-app/OAuthAppDetailPanel.d.ts +43 -0
  41. package/oauth-app/OAuthAppDetailPanel.d.ts.map +1 -0
  42. package/oauth-app/OAuthAppDetailPanel.js +202 -0
  43. package/oauth-app/OAuthAppDetailPanel.js.map +1 -0
  44. package/oauth-app/OAuthAppListPanel.d.ts +43 -0
  45. package/oauth-app/OAuthAppListPanel.d.ts.map +1 -0
  46. package/oauth-app/OAuthAppListPanel.js +79 -0
  47. package/oauth-app/OAuthAppListPanel.js.map +1 -0
  48. package/oauth-app/index.d.ts +15 -0
  49. package/oauth-app/index.d.ts.map +1 -0
  50. package/oauth-app/index.js +8 -0
  51. package/oauth-app/index.js.map +1 -0
  52. package/oauth-app/useCreateOAuthApp.d.ts +39 -0
  53. package/oauth-app/useCreateOAuthApp.d.ts.map +1 -0
  54. package/oauth-app/useCreateOAuthApp.js +50 -0
  55. package/oauth-app/useCreateOAuthApp.js.map +1 -0
  56. package/oauth-app/useDeleteOAuthApp.d.ts +31 -0
  57. package/oauth-app/useDeleteOAuthApp.d.ts.map +1 -0
  58. package/oauth-app/useDeleteOAuthApp.js +43 -0
  59. package/oauth-app/useDeleteOAuthApp.js.map +1 -0
  60. package/oauth-app/useOAuthAppList.d.ts +32 -0
  61. package/oauth-app/useOAuthAppList.d.ts.map +1 -0
  62. package/oauth-app/useOAuthAppList.js +61 -0
  63. package/oauth-app/useOAuthAppList.js.map +1 -0
  64. package/oauth-app/useUpdateOAuthApp.d.ts +38 -0
  65. package/oauth-app/useUpdateOAuthApp.d.ts.map +1 -0
  66. package/oauth-app/useUpdateOAuthApp.js +49 -0
  67. package/oauth-app/useUpdateOAuthApp.js.map +1 -0
  68. package/package.json +4 -4
  69. package/src/demo/fixtures.ts +8 -0
  70. package/src/demo/samples.ts +2 -0
  71. package/src/execution/ArtifactPreviewModal.tsx +206 -128
  72. package/src/execution/index.ts +2 -2
  73. package/src/index.ts +24 -0
  74. package/src/library/ResourceListView.tsx +8 -8
  75. package/src/mcp-server/McpServerConnectDialog.tsx +527 -0
  76. package/src/mcp-server/McpServerDetailView.tsx +2 -1
  77. package/src/mcp-server/McpServerPicker.tsx +8 -1
  78. package/src/mcp-server/index.ts +3 -0
  79. package/src/oauth-app/CreateOAuthAppForm.tsx +449 -0
  80. package/src/oauth-app/OAuthAppDetailPanel.tsx +671 -0
  81. package/src/oauth-app/OAuthAppListPanel.tsx +237 -0
  82. package/src/oauth-app/index.ts +14 -0
  83. package/src/oauth-app/useCreateOAuthApp.ts +70 -0
  84. package/src/oauth-app/useDeleteOAuthApp.ts +62 -0
  85. package/src/oauth-app/useOAuthAppList.ts +84 -0
  86. package/src/oauth-app/useUpdateOAuthApp.ts +69 -0
  87. package/styles.css +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useOAuthAppList.d.ts","sourceRoot":"","sources":["../../src/oauth-app/useOAuthAppList.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mDAAmD,CAAC;AAIlF,+CAA+C;AAC/C,MAAM,WAAW,qBAAqB;IACpC,6EAA6E;IAC7E,QAAQ,CAAC,SAAS,EAAE,SAAS,QAAQ,EAAE,CAAC;IACxC,gEAAgE;IAChE,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,kEAAkE;IAClE,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IAC7B,iEAAiE;IACjE,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAC;CAC9B;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,MAAM,GAAG,IAAI,GACjB,qBAAqB,CA0CvB"}
@@ -0,0 +1,61 @@
1
+ "use client";
2
+ import { useCallback, useEffect, useState } from "react";
3
+ import { create } from "@bufbuild/protobuf";
4
+ import { ListOAuthAppsByOrgInputSchema } from "@stigmer/protos/ai/stigmer/iam/oauthapp/v1/io_pb";
5
+ import { useStigmer } from "../hooks";
6
+ import { toError } from "../internal/toError";
7
+ /**
8
+ * Data hook that fetches all {@link OAuthApp} entries for an organization.
9
+ *
10
+ * Returns every OAuthApp whose `metadata.org` matches the input. In
11
+ * practice these are the BYOA OAuth apps created through the "Bring
12
+ * your own app" flow on MCP server detail pages.
13
+ *
14
+ * Pass `null` for `org` to skip fetching (stable no-op). Useful when
15
+ * the active organization has not been resolved yet.
16
+ *
17
+ * @example
18
+ * ```tsx
19
+ * const { oauthApps, isLoading, error } = useOAuthAppList(org);
20
+ *
21
+ * if (isLoading) return <Spinner />;
22
+ * oauthApps.map((app) => app.spec?.provider);
23
+ * ```
24
+ */
25
+ export function useOAuthAppList(org) {
26
+ const stigmer = useStigmer();
27
+ const [oauthApps, setOauthApps] = useState([]);
28
+ const [isLoading, setIsLoading] = useState(false);
29
+ const [error, setError] = useState(null);
30
+ const [fetchKey, setFetchKey] = useState(0);
31
+ const refetch = useCallback(() => setFetchKey((k) => k + 1), []);
32
+ useEffect(() => {
33
+ if (!org) {
34
+ setOauthApps([]);
35
+ setIsLoading(false);
36
+ setError(null);
37
+ return;
38
+ }
39
+ const cancelled = { current: false };
40
+ setIsLoading(true);
41
+ setError(null);
42
+ stigmer.oauthapp
43
+ .listByOrg(create(ListOAuthAppsByOrgInputSchema, { org }))
44
+ .then((result) => {
45
+ if (cancelled.current)
46
+ return;
47
+ setOauthApps([...result.entries]);
48
+ setIsLoading(false);
49
+ }, (err) => {
50
+ if (cancelled.current)
51
+ return;
52
+ setError(toError(err));
53
+ setIsLoading(false);
54
+ });
55
+ return () => {
56
+ cancelled.current = true;
57
+ };
58
+ }, [org, stigmer, fetchKey]);
59
+ return { oauthApps, isLoading, error, refetch };
60
+ }
61
+ //# sourceMappingURL=useOAuthAppList.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useOAuthAppList.js","sourceRoot":"","sources":["../../src/oauth-app/useOAuthAppList.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACzD,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,OAAO,EAAE,6BAA6B,EAAE,MAAM,kDAAkD,CAAC;AAEjG,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAc9C;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,eAAe,CAC7B,GAAkB;IAElB,MAAM,OAAO,GAAG,UAAU,EAAE,CAAC;IAC7B,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAa,EAAE,CAAC,CAAC;IAC3D,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAClD,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAe,IAAI,CAAC,CAAC;IACvD,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;IAE5C,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAEjE,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,YAAY,CAAC,EAAE,CAAC,CAAC;YACjB,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,QAAQ,CAAC,IAAI,CAAC,CAAC;YACf,OAAO;QACT,CAAC;QAED,MAAM,SAAS,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;QACrC,YAAY,CAAC,IAAI,CAAC,CAAC;QACnB,QAAQ,CAAC,IAAI,CAAC,CAAC;QAEf,OAAO,CAAC,QAAQ;aACb,SAAS,CAAC,MAAM,CAAC,6BAA6B,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;aACzD,IAAI,CACH,CAAC,MAAM,EAAE,EAAE;YACT,IAAI,SAAS,CAAC,OAAO;gBAAE,OAAO;YAC9B,YAAY,CAAC,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;YAClC,YAAY,CAAC,KAAK,CAAC,CAAC;QACtB,CAAC,EACD,CAAC,GAAG,EAAE,EAAE;YACN,IAAI,SAAS,CAAC,OAAO;gBAAE,OAAO;YAC9B,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;YACvB,YAAY,CAAC,KAAK,CAAC,CAAC;QACtB,CAAC,CACF,CAAC;QAEJ,OAAO,GAAG,EAAE;YACV,SAAS,CAAC,OAAO,GAAG,IAAI,CAAC;QAC3B,CAAC,CAAC;IACJ,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC;IAE7B,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;AAClD,CAAC"}
@@ -0,0 +1,38 @@
1
+ import type { OAuthAppInput } from "@stigmer/sdk";
2
+ import type { OAuthApp } from "@stigmer/protos/ai/stigmer/iam/oauthapp/v1/api_pb";
3
+ /** Return value of {@link useUpdateOAuthApp}. */
4
+ export interface UseUpdateOAuthAppReturn {
5
+ /** Submit an {@link OAuthAppInput} to update an existing OAuth app. Resolves with the updated resource. */
6
+ readonly update: (input: OAuthAppInput) => Promise<OAuthApp>;
7
+ /** `true` while the update request is in flight. */
8
+ readonly isUpdating: boolean;
9
+ /** Error from the last failed update, or `null` when healthy. */
10
+ readonly error: Error | null;
11
+ /** Reset `error` to `null`. */
12
+ readonly clearError: () => void;
13
+ }
14
+ /**
15
+ * Mutation hook that wraps `oauthapp.update()` with loading and error
16
+ * state.
17
+ *
18
+ * Updates an existing OAuth app. The input must include the `name` and
19
+ * `org` fields to identify the target resource, along with the updated
20
+ * spec fields. Omit `clientSecret` to leave the existing secret
21
+ * unchanged.
22
+ *
23
+ * @example
24
+ * ```tsx
25
+ * const { update, isUpdating, error } = useUpdateOAuthApp();
26
+ *
27
+ * await update({
28
+ * name: "My Slack App",
29
+ * org: "acme",
30
+ * provider: "Slack",
31
+ * clientId: "123456.789012",
32
+ * authorizationUrl: "https://slack.com/oauth/v2/authorize",
33
+ * tokenUrl: "https://slack.com/api/oauth.v2.access",
34
+ * });
35
+ * ```
36
+ */
37
+ export declare function useUpdateOAuthApp(): UseUpdateOAuthAppReturn;
38
+ //# sourceMappingURL=useUpdateOAuthApp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useUpdateOAuthApp.d.ts","sourceRoot":"","sources":["../../src/oauth-app/useUpdateOAuthApp.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mDAAmD,CAAC;AAIlF,iDAAiD;AACjD,MAAM,WAAW,uBAAuB;IACtC,2GAA2G;IAC3G,QAAQ,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC7D,oDAAoD;IACpD,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,iEAAiE;IACjE,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IAC7B,+BAA+B;IAC/B,QAAQ,CAAC,UAAU,EAAE,MAAM,IAAI,CAAC;CACjC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,iBAAiB,IAAI,uBAAuB,CAyB3D"}
@@ -0,0 +1,49 @@
1
+ "use client";
2
+ import { useCallback, useState } from "react";
3
+ import { useStigmer } from "../hooks";
4
+ import { toError } from "../internal/toError";
5
+ /**
6
+ * Mutation hook that wraps `oauthapp.update()` with loading and error
7
+ * state.
8
+ *
9
+ * Updates an existing OAuth app. The input must include the `name` and
10
+ * `org` fields to identify the target resource, along with the updated
11
+ * spec fields. Omit `clientSecret` to leave the existing secret
12
+ * unchanged.
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * const { update, isUpdating, error } = useUpdateOAuthApp();
17
+ *
18
+ * await update({
19
+ * name: "My Slack App",
20
+ * org: "acme",
21
+ * provider: "Slack",
22
+ * clientId: "123456.789012",
23
+ * authorizationUrl: "https://slack.com/oauth/v2/authorize",
24
+ * tokenUrl: "https://slack.com/api/oauth.v2.access",
25
+ * });
26
+ * ```
27
+ */
28
+ export function useUpdateOAuthApp() {
29
+ const stigmer = useStigmer();
30
+ const [isUpdating, setIsUpdating] = useState(false);
31
+ const [error, setError] = useState(null);
32
+ const clearError = useCallback(() => setError(null), []);
33
+ const update = useCallback(async (input) => {
34
+ setIsUpdating(true);
35
+ setError(null);
36
+ try {
37
+ return await stigmer.oauthapp.update(input);
38
+ }
39
+ catch (err) {
40
+ setError(toError(err));
41
+ throw err;
42
+ }
43
+ finally {
44
+ setIsUpdating(false);
45
+ }
46
+ }, [stigmer]);
47
+ return { update, isUpdating, error, clearError };
48
+ }
49
+ //# sourceMappingURL=useUpdateOAuthApp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useUpdateOAuthApp.js","sourceRoot":"","sources":["../../src/oauth-app/useUpdateOAuthApp.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAG9C,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAc9C;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,iBAAiB;IAC/B,MAAM,OAAO,GAAG,UAAU,EAAE,CAAC;IAC7B,MAAM,CAAC,UAAU,EAAE,aAAa,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IACpD,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAe,IAAI,CAAC,CAAC;IAEvD,MAAM,UAAU,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;IAEzD,MAAM,MAAM,GAAG,WAAW,CACxB,KAAK,EAAE,KAAoB,EAAqB,EAAE;QAChD,aAAa,CAAC,IAAI,CAAC,CAAC;QACpB,QAAQ,CAAC,IAAI,CAAC,CAAC;QAEf,IAAI,CAAC;YACH,OAAO,MAAM,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC9C,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;YACvB,MAAM,GAAG,CAAC;QACZ,CAAC;gBAAS,CAAC;YACT,aAAa,CAAC,KAAK,CAAC,CAAC;QACvB,CAAC;IACH,CAAC,EACD,CAAC,OAAO,CAAC,CACV,CAAC;IAEF,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC;AACnD,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stigmer/react",
3
- "version": "0.0.84",
3
+ "version": "0.0.86",
4
4
  "description": "React provider and client hook for the Stigmer platform SDK",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -35,7 +35,7 @@
35
35
  }
36
36
  },
37
37
  "dependencies": {
38
- "@stigmer/theme": "0.0.84",
38
+ "@stigmer/theme": "0.0.86",
39
39
  "react-markdown": "^10.1.0",
40
40
  "remark-gfm": "^4.0.1",
41
41
  "yaml": "^2.8.2"
@@ -43,8 +43,8 @@
43
43
  "peerDependencies": {
44
44
  "@base-ui/react": "^1.0.0",
45
45
  "@bufbuild/protobuf": "^2.0.0",
46
- "@stigmer/protos": "0.0.84",
47
- "@stigmer/sdk": "0.0.84",
46
+ "@stigmer/protos": "0.0.86",
47
+ "@stigmer/sdk": "0.0.86",
48
48
  "react": "^19.0.0",
49
49
  "react-dom": "^19.0.0"
50
50
  }
@@ -238,6 +238,14 @@ export const fixtures = {
238
238
  /** Hooks: `useUpdateVisibility` (when kind is McpServer) */
239
239
  updateVisibility: (handler: UnaryFixtureHandler): FixtureSpec =>
240
240
  unarySpec(McpServerCommandController, "updateVisibility", handler),
241
+
242
+ /** Hooks: `useOAuthGrantStatus` */
243
+ getOAuthGrantStatus: (handler: UnaryFixtureHandler): FixtureSpec =>
244
+ unarySpec(McpServerQueryController, "getOAuthGrantStatus", handler),
245
+
246
+ /** Hooks: `useOrgOAuthApp` */
247
+ getOrgOAuthApp: (handler: UnaryFixtureHandler): FixtureSpec =>
248
+ unarySpec(McpServerQueryController, "getOrgOAuthApp", handler),
241
249
  },
242
250
 
243
251
  // ---- Environment ----
@@ -154,6 +154,7 @@ export interface SearchResultOverrides {
154
154
  readonly slug?: string;
155
155
  readonly kind?: ApiResourceKind;
156
156
  readonly description?: string;
157
+ readonly iconUrl?: string;
157
158
  }
158
159
 
159
160
  // ---------------------------------------------------------------------------
@@ -465,6 +466,7 @@ export const samples = {
465
466
  org: o?.org ?? "demo",
466
467
  description: o?.description ?? "A sample resource for demo purposes.",
467
468
  score: 1.0,
469
+ iconUrl: o?.iconUrl ?? "",
468
470
  });
469
471
  },
470
472
  } as const;
@@ -17,8 +17,12 @@ import {
17
17
 
18
18
  const COPIED_FEEDBACK_MS = 2000;
19
19
 
20
- /** Props for {@link ArtifactPreviewModal}. */
21
- export interface ArtifactPreviewModalProps {
20
+ // ---------------------------------------------------------------------------
21
+ // ArtifactPreviewContent standalone content component
22
+ // ---------------------------------------------------------------------------
23
+
24
+ /** Props for {@link ArtifactPreviewContent}. */
25
+ export interface ArtifactPreviewContentProps {
22
26
  /** The execution artifact to preview. */
23
27
  readonly artifact: ExecutionArtifact;
24
28
  /** ID of the execution that produced this artifact. */
@@ -33,9 +37,7 @@ export interface ArtifactPreviewModalProps {
33
37
  * - `false` — CTA renders as a disabled secondary button
34
38
  */
35
39
  readonly isTerminal: boolean;
36
- /** Controls modal visibility. `true` opens the dialog via `showModal()`. */
37
- readonly open: boolean;
38
- /** Called when the modal should close (Escape key or close button). */
40
+ /** Called when the close button is clicked. */
39
41
  readonly onClose: () => void;
40
42
  /**
41
43
  * Called after a resource is successfully applied or a skill package
@@ -43,19 +45,20 @@ export interface ArtifactPreviewModalProps {
43
45
  * as showing a toast or navigating to the Library.
44
46
  */
45
47
  readonly onApplied?: (result: ApplyResourceResult) => void;
46
- /** Additional CSS classes for the dialog element. */
48
+ /** Additional CSS classes for the root container. */
47
49
  readonly className?: string;
48
50
  }
49
51
 
50
52
  /**
51
- * Full preview modal for execution artifacts with content display,
52
- * Stigmer resource detection, and Apply/Push CTA.
53
+ * Artifact preview content with detection pipeline and Apply/Push CTA.
53
54
  *
54
- * Uses the native `<dialog>` element for zero-dependency modal behavior:
55
- * focus trap via `showModal()`, top-layer promotion, Escape key handling,
56
- * and `::backdrop` overlay with no dependency on `@base-ui/react`.
55
+ * Renders the header (name, size, detection badge, close button),
56
+ * content body (file content or directory listing), and action bar
57
+ * (copy, download, Apply/Push). The parent is responsible for rendering
58
+ * it inside a `<dialog>`, modal, sheet, overlay, or inline context as
59
+ * needed.
57
60
  *
58
- * Internally orchestrates the same detection pipeline as {@link ArtifactCard}:
61
+ * Orchestrates the same detection pipeline as {@link ArtifactCard}:
59
62
  *
60
63
  * - **FILE artifacts**: Fetches text content via {@link useArtifactContent},
61
64
  * renders via {@link ArtifactContentRenderer} (markdown, YAML, JSON, or
@@ -66,75 +69,47 @@ export interface ArtifactPreviewModalProps {
66
69
  * `artifact.entries` and detects skill packages via
67
70
  * {@link useDetectSkillPackage}.
68
71
  *
69
- * Actions: Copy content to clipboard (file artifacts only), Download
70
- * artifact, and Apply/Push (when a Stigmer resource is detected).
72
+ * Content fetching begins immediately on mount. Unmounting the component
73
+ * resets all internal state (detection, apply result, clipboard). For
74
+ * gated rendering (e.g. only when a dialog is open), conditionally
75
+ * mount this component rather than passing an `active` flag.
71
76
  *
72
77
  * @example
73
78
  * ```tsx
74
- * const [previewArtifact, setPreviewArtifact] =
75
- * useState<ExecutionArtifact | null>(null);
76
- *
77
- * {previewArtifact && (
78
- * <ArtifactPreviewModal
79
- * artifact={previewArtifact}
80
- * executionId={execution.id}
81
- * org={activeOrg}
82
- * isTerminal={isTerminalPhase(execution.status?.phase)}
83
- * open
84
- * onClose={() => setPreviewArtifact(null)}
85
- * onApplied={(result) => toast(`${result.kind} applied`)}
86
- * />
87
- * )}
79
+ * // Inside a dialog, modal, or overlay:
80
+ * <ArtifactPreviewContent
81
+ * artifact={artifact}
82
+ * executionId={execution.id}
83
+ * org={activeOrg}
84
+ * isTerminal={isTerminalPhase(execution.status?.phase)}
85
+ * onClose={() => setOpen(false)}
86
+ * onApplied={(result) => toast(`${result.kind} applied`)}
87
+ * />
88
88
  * ```
89
89
  *
90
+ * @see {@link ArtifactPreviewModal} — wraps this component in a native `<dialog>`
90
91
  * @see {@link ArtifactCard} — compact card that triggers preview via `onPreview`
91
92
  * @see {@link useArtifactContent} — content-fetching hook (headless alternative)
92
93
  * @see {@link useDetectStigmerResource} — YAML resource detection (headless)
93
94
  * @see {@link useDetectSkillPackage} — skill package detection (headless)
94
95
  * @see {@link useApplyResource} — apply/push behavior hook (headless)
95
96
  */
96
- export function ArtifactPreviewModal({
97
+ export function ArtifactPreviewContent({
97
98
  artifact,
98
99
  executionId,
99
100
  org,
100
101
  isTerminal,
101
- open,
102
102
  onClose,
103
103
  onApplied,
104
104
  className,
105
- }: ArtifactPreviewModalProps) {
106
- const dialogRef = useRef<HTMLDialogElement>(null);
107
-
108
- // ---------------------------------------------------------------------------
109
- // Native <dialog> lifecycle
110
- // ---------------------------------------------------------------------------
111
-
112
- useEffect(() => {
113
- const dialog = dialogRef.current;
114
- if (!dialog) return;
115
-
116
- if (open && !dialog.open) {
117
- dialog.showModal();
118
- } else if (!open && dialog.open) {
119
- dialog.close();
120
- }
121
- }, [open]);
122
-
123
- const handleCancel = useCallback(
124
- (e: React.SyntheticEvent) => {
125
- e.preventDefault();
126
- onClose();
127
- },
128
- [onClose],
129
- );
105
+ }: ArtifactPreviewContentProps) {
106
+ const isDirectory = artifact.kind === ExecutionArtifactKind.DIRECTORY;
107
+ const canFetchContent = !isDirectory && isTextArtifact(artifact);
130
108
 
131
109
  // ---------------------------------------------------------------------------
132
- // Detection orchestration (gated on `open` to skip fetching when hidden)
110
+ // Detection orchestration
133
111
  // ---------------------------------------------------------------------------
134
112
 
135
- const isDirectory = artifact.kind === ExecutionArtifactKind.DIRECTORY;
136
- const canFetchContent = open && !isDirectory && isTextArtifact(artifact);
137
-
138
113
  const {
139
114
  content,
140
115
  contentType,
@@ -154,8 +129,8 @@ export function ArtifactPreviewModal({
154
129
 
155
130
  const { detection: skillDetection, isLoading: isSkillLoading } =
156
131
  useDetectSkillPackage(
157
- isDirectory && open ? artifact : null,
158
- isDirectory && open ? executionId : null,
132
+ isDirectory ? artifact : null,
133
+ isDirectory ? executionId : null,
159
134
  );
160
135
 
161
136
  // ---------------------------------------------------------------------------
@@ -223,13 +198,6 @@ export function ArtifactPreviewModal({
223
198
  onApplied,
224
199
  ]);
225
200
 
226
- useEffect(() => {
227
- if (!open) {
228
- setApplyResult(null);
229
- clearError();
230
- }
231
- }, [open, clearError]);
232
-
233
201
  // ---------------------------------------------------------------------------
234
202
  // Copy to clipboard
235
203
  // ---------------------------------------------------------------------------
@@ -244,10 +212,6 @@ export function ArtifactPreviewModal({
244
212
  });
245
213
  }, [content]);
246
214
 
247
- useEffect(() => {
248
- if (!open) setCopied(false);
249
- }, [open]);
250
-
251
215
  let ctaLabel: string | null = null;
252
216
  if (yamlDetection.detected) {
253
217
  ctaLabel = `Apply to ${org}`;
@@ -259,6 +223,159 @@ export function ArtifactPreviewModal({
259
223
  // Render
260
224
  // ---------------------------------------------------------------------------
261
225
 
226
+ return (
227
+ <div className={cn("flex max-h-[80vh] flex-col", className)}>
228
+ <ContentHeader
229
+ artifact={artifact}
230
+ isDirectory={isDirectory}
231
+ detectionLabel={detectionLabel}
232
+ isDetecting={isDetecting}
233
+ onClose={onClose}
234
+ />
235
+
236
+ <div className="min-h-0 flex-1 overflow-y-auto border-t border-border">
237
+ {isDirectory ? (
238
+ <DirectoryContentView
239
+ artifact={artifact}
240
+ skillDetection={skillDetection}
241
+ />
242
+ ) : (
243
+ <FileContentStateView
244
+ artifact={artifact}
245
+ content={content}
246
+ contentType={contentType}
247
+ isLoading={isContentLoading}
248
+ error={contentError}
249
+ isTruncated={isTruncated}
250
+ />
251
+ )}
252
+ </div>
253
+
254
+ <ActionBar
255
+ artifact={artifact}
256
+ isDirectory={isDirectory}
257
+ hasContent={content !== null}
258
+ copied={copied}
259
+ onCopy={handleCopy}
260
+ isDetected={isDetected}
261
+ ctaLabel={ctaLabel}
262
+ isTerminal={isTerminal}
263
+ isApplying={isApplying}
264
+ applyResult={applyResult}
265
+ applyError={applyError}
266
+ onApply={handleApply}
267
+ />
268
+
269
+ <div
270
+ role="status"
271
+ aria-live="polite"
272
+ aria-atomic="true"
273
+ className="sr-only"
274
+ >
275
+ {copied && "Content copied to clipboard"}
276
+ </div>
277
+ </div>
278
+ );
279
+ }
280
+
281
+ // ---------------------------------------------------------------------------
282
+ // ArtifactPreviewModal — thin <dialog> shell
283
+ // ---------------------------------------------------------------------------
284
+
285
+ /** Props for {@link ArtifactPreviewModal}. */
286
+ export interface ArtifactPreviewModalProps {
287
+ /** The execution artifact to preview. */
288
+ readonly artifact: ExecutionArtifact;
289
+ /** ID of the execution that produced this artifact. */
290
+ readonly executionId: string;
291
+ /** Organization slug for the "Apply to [org]" / "Push Skill to [org]" CTA. */
292
+ readonly org: string;
293
+ /**
294
+ * Whether the execution is in a terminal phase (completed, failed,
295
+ * cancelled, terminated). Controls Apply/Push CTA availability:
296
+ *
297
+ * - `true` — CTA renders as an enabled primary button
298
+ * - `false` — CTA renders as a disabled secondary button
299
+ */
300
+ readonly isTerminal: boolean;
301
+ /** Controls modal visibility. `true` opens the dialog via `showModal()`. */
302
+ readonly open: boolean;
303
+ /** Called when the modal should close (Escape key or close button). */
304
+ readonly onClose: () => void;
305
+ /**
306
+ * Called after a resource is successfully applied or a skill package
307
+ * is pushed. The consumer can use this for post-apply behavior such
308
+ * as showing a toast or navigating to the Library.
309
+ */
310
+ readonly onApplied?: (result: ApplyResourceResult) => void;
311
+ /** Additional CSS classes for the dialog element. */
312
+ readonly className?: string;
313
+ }
314
+
315
+ /**
316
+ * Full preview modal for execution artifacts using a native `<dialog>`.
317
+ *
318
+ * Thin shell around {@link ArtifactPreviewContent} that adds
319
+ * `showModal()` / `close()` lifecycle, `::backdrop` overlay, focus
320
+ * trap, and Escape key handling — with no dependency on
321
+ * `@base-ui/react`.
322
+ *
323
+ * The content component is conditionally mounted when `open` is true,
324
+ * so all internal state (detection, apply result, clipboard) resets
325
+ * automatically when the dialog closes.
326
+ *
327
+ * @example
328
+ * ```tsx
329
+ * const [previewArtifact, setPreviewArtifact] =
330
+ * useState<ExecutionArtifact | null>(null);
331
+ *
332
+ * {previewArtifact && (
333
+ * <ArtifactPreviewModal
334
+ * artifact={previewArtifact}
335
+ * executionId={execution.id}
336
+ * org={activeOrg}
337
+ * isTerminal={isTerminalPhase(execution.status?.phase)}
338
+ * open
339
+ * onClose={() => setPreviewArtifact(null)}
340
+ * onApplied={(result) => toast(`${result.kind} applied`)}
341
+ * />
342
+ * )}
343
+ * ```
344
+ *
345
+ * @see {@link ArtifactPreviewContent} — the content component (use directly for non-dialog contexts)
346
+ * @see {@link ArtifactCard} — compact card that triggers preview via `onPreview`
347
+ */
348
+ export function ArtifactPreviewModal({
349
+ artifact,
350
+ executionId,
351
+ org,
352
+ isTerminal,
353
+ open,
354
+ onClose,
355
+ onApplied,
356
+ className,
357
+ }: ArtifactPreviewModalProps) {
358
+ const dialogRef = useRef<HTMLDialogElement>(null);
359
+
360
+ useEffect(() => {
361
+ const dialog = dialogRef.current;
362
+ if (!dialog) return;
363
+
364
+ if (open && !dialog.open) {
365
+ dialog.showModal();
366
+ } else if (!open && dialog.open) {
367
+ dialog.close();
368
+ }
369
+ }, [open]);
370
+
371
+ const handleCancel = useCallback(
372
+ (e: React.SyntheticEvent) => {
373
+ e.preventDefault();
374
+ onClose();
375
+ },
376
+ [onClose],
377
+ );
378
+
262
379
  return (
263
380
  <dialog
264
381
  ref={dialogRef}
@@ -270,57 +387,16 @@ export function ArtifactPreviewModal({
270
387
  className,
271
388
  )}
272
389
  >
273
- <div className="flex max-h-[80vh] flex-col">
274
- <ModalHeader
275
- artifact={artifact}
276
- isDirectory={isDirectory}
277
- detectionLabel={detectionLabel}
278
- isDetecting={isDetecting}
279
- onClose={onClose}
280
- />
281
-
282
- <div className="min-h-0 flex-1 overflow-y-auto border-t border-border">
283
- {isDirectory ? (
284
- <DirectoryContentView
285
- artifact={artifact}
286
- skillDetection={skillDetection}
287
- />
288
- ) : (
289
- <FileContentStateView
290
- artifact={artifact}
291
- content={content}
292
- contentType={contentType}
293
- isLoading={isContentLoading}
294
- error={contentError}
295
- isTruncated={isTruncated}
296
- />
297
- )}
298
- </div>
299
-
300
- <ActionBar
390
+ {open && (
391
+ <ArtifactPreviewContent
301
392
  artifact={artifact}
302
- isDirectory={isDirectory}
303
- hasContent={content !== null}
304
- copied={copied}
305
- onCopy={handleCopy}
306
- isDetected={isDetected}
307
- ctaLabel={ctaLabel}
393
+ executionId={executionId}
394
+ org={org}
308
395
  isTerminal={isTerminal}
309
- isApplying={isApplying}
310
- applyResult={applyResult}
311
- applyError={applyError}
312
- onApply={handleApply}
396
+ onClose={onClose}
397
+ onApplied={onApplied}
313
398
  />
314
-
315
- <div
316
- role="status"
317
- aria-live="polite"
318
- aria-atomic="true"
319
- className="sr-only"
320
- >
321
- {copied && "Content copied to clipboard"}
322
- </div>
323
- </div>
399
+ )}
324
400
  </dialog>
325
401
  );
326
402
  }
@@ -333,10 +409,10 @@ const FOCUS_RING_CLASSES =
333
409
  "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:rounded-sm";
334
410
 
335
411
  // ---------------------------------------------------------------------------
336
- // ModalHeader (internal)
412
+ // ContentHeader (internal)
337
413
  // ---------------------------------------------------------------------------
338
414
 
339
- function ModalHeader({
415
+ function ContentHeader({
340
416
  artifact,
341
417
  isDirectory,
342
418
  detectionLabel,
@@ -603,12 +679,14 @@ function ActionBar({
603
679
  </button>
604
680
  </span>
605
681
  ) : isDetected && ctaLabel ? (
606
- <ApplyButton
607
- label={ctaLabel}
608
- isTerminal={isTerminal}
609
- isApplying={isApplying}
610
- onApply={onApply}
611
- />
682
+ <span data-cursor-target="apply-resource-button">
683
+ <ApplyButton
684
+ label={ctaLabel}
685
+ isTerminal={isTerminal}
686
+ isApplying={isApplying}
687
+ onApply={onApply}
688
+ />
689
+ </span>
612
690
  ) : null}
613
691
  </div>
614
692
  </div>