@stigmer/react 3.1.12 → 3.1.14

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 (63) hide show
  1. package/agent/AgentDetailView.d.ts +11 -3
  2. package/agent/AgentDetailView.d.ts.map +1 -1
  3. package/agent/AgentDetailView.js +22 -17
  4. package/agent/AgentDetailView.js.map +1 -1
  5. package/index.d.ts +2 -2
  6. package/index.d.ts.map +1 -1
  7. package/index.js +2 -2
  8. package/index.js.map +1 -1
  9. package/package.json +4 -4
  10. package/sharing/AgentShareList.d.ts +44 -0
  11. package/sharing/AgentShareList.d.ts.map +1 -0
  12. package/sharing/AgentShareList.js +149 -0
  13. package/sharing/AgentShareList.js.map +1 -0
  14. package/sharing/ShareAgentDialog.d.ts +38 -15
  15. package/sharing/ShareAgentDialog.d.ts.map +1 -1
  16. package/sharing/ShareAgentDialog.js +135 -57
  17. package/sharing/ShareAgentDialog.js.map +1 -1
  18. package/sharing/index.d.ts +10 -6
  19. package/sharing/index.d.ts.map +1 -1
  20. package/sharing/index.js +5 -3
  21. package/sharing/index.js.map +1 -1
  22. package/sharing/useAgentShares.d.ts +42 -0
  23. package/sharing/useAgentShares.d.ts.map +1 -0
  24. package/sharing/useAgentShares.js +38 -0
  25. package/sharing/useAgentShares.js.map +1 -0
  26. package/sharing/useCanCreateAgentShare.d.ts +43 -0
  27. package/sharing/useCanCreateAgentShare.d.ts.map +1 -0
  28. package/sharing/useCanCreateAgentShare.js +44 -0
  29. package/sharing/useCanCreateAgentShare.js.map +1 -0
  30. package/sharing/useDeleteAgentShare.d.ts +42 -0
  31. package/sharing/useDeleteAgentShare.d.ts.map +1 -0
  32. package/sharing/useDeleteAgentShare.js +45 -0
  33. package/sharing/useDeleteAgentShare.js.map +1 -0
  34. package/sharing/useSaveAgentShare.d.ts +45 -14
  35. package/sharing/useSaveAgentShare.d.ts.map +1 -1
  36. package/sharing/useSaveAgentShare.js +59 -19
  37. package/sharing/useSaveAgentShare.js.map +1 -1
  38. package/src/agent/AgentDetailView.tsx +37 -20
  39. package/src/index.ts +11 -6
  40. package/src/sharing/AgentShareList.tsx +507 -0
  41. package/src/sharing/ShareAgentDialog.tsx +325 -95
  42. package/src/sharing/__tests__/AgentShareList.test.tsx +388 -0
  43. package/src/sharing/__tests__/ShareAgentDialog.test.tsx +333 -286
  44. package/src/sharing/__tests__/{useAgentShare.test.tsx → useAgentShares.test.tsx} +47 -51
  45. package/src/sharing/__tests__/useCanCreateAgentShare.test.tsx +190 -0
  46. package/src/sharing/__tests__/useSaveAgentShare.test.tsx +48 -0
  47. package/src/sharing/index.ts +10 -7
  48. package/src/sharing/useAgentShares.ts +68 -0
  49. package/src/sharing/useCanCreateAgentShare.ts +74 -0
  50. package/src/sharing/useDeleteAgentShare.ts +73 -0
  51. package/src/sharing/useSaveAgentShare.ts +77 -19
  52. package/styles.css +1 -1
  53. package/sharing/useAgentShare.d.ts +0 -43
  54. package/sharing/useAgentShare.d.ts.map +0 -1
  55. package/sharing/useAgentShare.js +0 -54
  56. package/sharing/useAgentShare.js.map +0 -1
  57. package/sharing/useShareAgent.d.ts +0 -69
  58. package/sharing/useShareAgent.d.ts.map +0 -1
  59. package/sharing/useShareAgent.js +0 -42
  60. package/sharing/useShareAgent.js.map +0 -1
  61. package/src/sharing/__tests__/useShareAgent.test.tsx +0 -131
  62. package/src/sharing/useAgentShare.ts +0 -91
  63. package/src/sharing/useShareAgent.tsx +0 -107
@@ -1,43 +0,0 @@
1
- import type { Agent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
2
- import type { AgentShare } from "@stigmer/protos/ai/stigmer/agentic/agentshare/v1/api_pb";
3
- /** Return value of {@link useAgentShare}. */
4
- export interface UseAgentShareReturn {
5
- /**
6
- * The agent's canonical share, or `null` while loading, on error, or
7
- * when the agent has never been shared. `share === null && !isLoading
8
- * && !error` means "no share exists yet" — the first save creates it.
9
- */
10
- readonly share: AgentShare | null;
11
- /** `true` while the initial fetch or a refetch is in flight. */
12
- readonly isLoading: boolean;
13
- /** `true` while a background refetch is in flight and stale data is shown. */
14
- readonly isRefetching: boolean;
15
- /** Error from the last failed request, or `null` when healthy. */
16
- readonly error: Error | null;
17
- /** Discard cached data and re-fetch the share from the server. */
18
- readonly refetch: () => void;
19
- }
20
- /**
21
- * Data hook that loads an agent's canonical {@link AgentShare} — the
22
- * resource carrying the hosted-chat channel configuration (audience,
23
- * allowed origins, visitor messages, tool credentials, link token).
24
- *
25
- * Sharing is channel configuration, not agent behavior (decision 011):
26
- * it lives in its own resource, so reading the agent alone can never
27
- * tell whether it is shared. This hook is how owner-side surfaces (the
28
- * Share dialog) resolve that state.
29
- *
30
- * Pass `null` for `agent` to skip fetching (stable no-op) — useful
31
- * while the agent is still loading. A resolved `null` share means the
32
- * agent has never been shared; the first save creates the share.
33
- *
34
- * @example
35
- * ```tsx
36
- * const { share, isLoading } = useAgentShare(agent);
37
- *
38
- * if (isLoading) return <Spinner />;
39
- * const enabled = share?.spec?.enabled ?? false;
40
- * ```
41
- */
42
- export declare function useAgentShare(agent: Agent | null): UseAgentShareReturn;
43
- //# sourceMappingURL=useAgentShare.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"useAgentShare.d.ts","sourceRoot":"","sources":["../../src/sharing/useAgentShare.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,oDAAoD,CAAC;AAChF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yDAAyD,CAAC;AAK1F,6CAA6C;AAC7C,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,QAAQ,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;IAClC,gEAAgE;IAChE,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,8EAA8E;IAC9E,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,kEAAkE;IAClE,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IAC7B,kEAAkE;IAClE,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAC;CAC9B;AAqBD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,GAAG,mBAAmB,CAsBtE"}
@@ -1,54 +0,0 @@
1
- "use client";
2
- import { create } from "@bufbuild/protobuf";
3
- import { GetAgentSharesByAgentRequestSchema } from "@stigmer/protos/ai/stigmer/agentic/agentshare/v1/io_pb";
4
- import { useStigmer } from "../hooks.js";
5
- import { useFetch } from "../internal/useFetch.js";
6
- /**
7
- * The canonical share among an agent's shares: the one whose slug equals
8
- * the agent's slug (the server's default when a share is created without
9
- * an explicit slug), falling back to the first entry. The data model
10
- * allows N shares per agent (decision 011 D3); the console manages the
11
- * canonical one in Phase A, so extra shares created via manifests never
12
- * confuse the dialog.
13
- */
14
- function pickCanonicalShare(shares, agentSlug) {
15
- return (shares.find((share) => share.metadata?.slug === agentSlug) ??
16
- shares[0] ??
17
- null);
18
- }
19
- /**
20
- * Data hook that loads an agent's canonical {@link AgentShare} — the
21
- * resource carrying the hosted-chat channel configuration (audience,
22
- * allowed origins, visitor messages, tool credentials, link token).
23
- *
24
- * Sharing is channel configuration, not agent behavior (decision 011):
25
- * it lives in its own resource, so reading the agent alone can never
26
- * tell whether it is shared. This hook is how owner-side surfaces (the
27
- * Share dialog) resolve that state.
28
- *
29
- * Pass `null` for `agent` to skip fetching (stable no-op) — useful
30
- * while the agent is still loading. A resolved `null` share means the
31
- * agent has never been shared; the first save creates the share.
32
- *
33
- * @example
34
- * ```tsx
35
- * const { share, isLoading } = useAgentShare(agent);
36
- *
37
- * if (isLoading) return <Spinner />;
38
- * const enabled = share?.spec?.enabled ?? false;
39
- * ```
40
- */
41
- export function useAgentShare(agent) {
42
- const stigmer = useStigmer();
43
- const agentId = agent?.metadata?.id ?? "";
44
- const agentSlug = agent?.metadata?.slug ?? "";
45
- const fetchFn = agentId
46
- ? async () => {
47
- const result = await stigmer.agentShare.getByAgent(create(GetAgentSharesByAgentRequestSchema, { agentId }));
48
- return pickCanonicalShare(result.items, agentSlug);
49
- }
50
- : null;
51
- const { data: share, isLoading, isRefetching, error, refetch } = useFetch(fetchFn, [agentId, agentSlug, stigmer], null);
52
- return { share, isLoading, isRefetching, error, refetch };
53
- }
54
- //# sourceMappingURL=useAgentShare.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"useAgentShare.js","sourceRoot":"","sources":["../../src/sharing/useAgentShare.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAG5C,OAAO,EAAE,kCAAkC,EAAE,MAAM,wDAAwD,CAAC;AAC5G,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAoBnD;;;;;;;GAOG;AACH,SAAS,kBAAkB,CACzB,MAA6B,EAC7B,SAAiB;IAEjB,OAAO,CACL,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,EAAE,IAAI,KAAK,SAAS,CAAC;QAC1D,MAAM,CAAC,CAAC,CAAC;QACT,IAAI,CACL,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,aAAa,CAAC,KAAmB;IAC/C,MAAM,OAAO,GAAG,UAAU,EAAE,CAAC;IAE7B,MAAM,OAAO,GAAG,KAAK,EAAE,QAAQ,EAAE,EAAE,IAAI,EAAE,CAAC;IAC1C,MAAM,SAAS,GAAG,KAAK,EAAE,QAAQ,EAAE,IAAI,IAAI,EAAE,CAAC;IAE9C,MAAM,OAAO,GAAG,OAAO;QACrB,CAAC,CAAC,KAAK,IAAI,EAAE;YACT,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC,UAAU,CAChD,MAAM,CAAC,kCAAkC,EAAE,EAAE,OAAO,EAAE,CAAC,CACxD,CAAC;YACF,OAAO,kBAAkB,CAAC,MAAM,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;QACrD,CAAC;QACH,CAAC,CAAC,IAAI,CAAC;IAET,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,QAAQ,CACvE,OAAO,EACP,CAAC,OAAO,EAAE,SAAS,EAAE,OAAO,CAAC,EAC7B,IAAyB,CAC1B,CAAC;IAEF,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;AAC5D,CAAC"}
@@ -1,69 +0,0 @@
1
- import { type ReactNode } from "react";
2
- import type { Agent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
3
- import type { DetailAction } from "../resource-detail/types.js";
4
- /** Arguments for {@link useShareAgent}. */
5
- export interface UseShareAgentArgs {
6
- /**
7
- * The agent whose sharing is managed, or `null` while it is still
8
- * loading. When `null`, {@link UseShareAgentReturn.action} is `null`
9
- * and the dialog renders nothing — safe to call before the resource
10
- * is ready.
11
- */
12
- readonly agent: Agent | null;
13
- /**
14
- * Builds the absolute public chat URL for the shared agent. The host
15
- * application owns URL construction (its configured public origin may
16
- * differ from the rendering origin — e.g. the desktop app). When
17
- * omitted, the dialog falls back to the relative `/chat/<org>/<slug>`.
18
- */
19
- readonly buildShareUrl?: (org: string, slug: string) => string;
20
- /**
21
- * Called after any sharing change is persisted. Hosts typically pass
22
- * the agent data hook's `refetch`.
23
- */
24
- readonly onSharingChanged?: () => void;
25
- /** Menu-item label. @default "Share" */
26
- readonly label?: string;
27
- }
28
- /** Return value of {@link useShareAgent}. */
29
- export interface UseShareAgentReturn {
30
- /**
31
- * A ready-to-spread {@link DetailAction} for a kebab/overflow menu, or
32
- * `null` when the agent is unavailable or the user lacks `can_edit`.
33
- * Lives in the `"sharing"` group, beside "Manage access".
34
- */
35
- readonly action: DetailAction | null;
36
- /** The {@link ShareAgentDialog} node — render it once in the host tree. */
37
- readonly dialog: ReactNode;
38
- /** Imperatively open the dialog. */
39
- readonly open: () => void;
40
- /** Whether the dialog is currently open. */
41
- readonly isOpen: boolean;
42
- }
43
- /**
44
- * Wires the Share dialog to a kebab/overflow menu — the same trigger
45
- * shape as {@link useManageAccess}, its conceptual sibling: Manage access
46
- * governs who can *read* the blueprint; Share governs who can *chat* with
47
- * the running agent (billed to the owning org).
48
- *
49
- * Owns the open-state and the `can_edit` gate — the same permission the
50
- * AgentShare create/apply handlers enforce on the referenced agent, so
51
- * the action never appears to a user whose changes would be rejected.
52
- * Returns a `null` action while the agent is loading or the user cannot
53
- * edit, so the host can unconditionally fold `action` into its actions
54
- * array.
55
- *
56
- * @example
57
- * ```tsx
58
- * const share = useShareAgent({
59
- * agent,
60
- * buildShareUrl: (org, slug) => `${appOrigin}/chat/${org}/${slug}`,
61
- * onSharingChanged: refetch,
62
- * });
63
- * // ...
64
- * <ResourceDetailShell actions={share.action ? [...actions, share.action] : actions} ... />
65
- * {share.dialog}
66
- * ```
67
- */
68
- export declare function useShareAgent({ agent, buildShareUrl, onSharingChanged, label, }: UseShareAgentArgs): UseShareAgentReturn;
69
- //# sourceMappingURL=useShareAgent.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"useShareAgent.d.ts","sourceRoot":"","sources":["../../src/sharing/useShareAgent.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAyB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAC9D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,oDAAoD,CAAC;AAEhF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAGhE,2CAA2C;AAC3C,MAAM,WAAW,iBAAiB;IAChC;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IAC7B;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IAC/D;;;OAGG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAC;IACvC,wCAAwC;IACxC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,6CAA6C;AAC7C,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,CAAC;IACrC,2EAA2E;IAC3E,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAC3B,oCAAoC;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,IAAI,CAAC;IAC1B,4CAA4C;IAC5C,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,aAAa,CAAC,EAC5B,KAAK,EACL,aAAa,EACb,gBAAgB,EAChB,KAAe,GAChB,EAAE,iBAAiB,GAAG,mBAAmB,CA2BzC"}
@@ -1,42 +0,0 @@
1
- "use client";
2
- import { jsx as _jsx } from "react/jsx-runtime";
3
- import { useCallback, useState } from "react";
4
- import { useCheckPermission } from "../iam-policy/useCheckPermission.js";
5
- import { ShareAgentDialog } from "./ShareAgentDialog.js";
6
- /**
7
- * Wires the Share dialog to a kebab/overflow menu — the same trigger
8
- * shape as {@link useManageAccess}, its conceptual sibling: Manage access
9
- * governs who can *read* the blueprint; Share governs who can *chat* with
10
- * the running agent (billed to the owning org).
11
- *
12
- * Owns the open-state and the `can_edit` gate — the same permission the
13
- * AgentShare create/apply handlers enforce on the referenced agent, so
14
- * the action never appears to a user whose changes would be rejected.
15
- * Returns a `null` action while the agent is loading or the user cannot
16
- * edit, so the host can unconditionally fold `action` into its actions
17
- * array.
18
- *
19
- * @example
20
- * ```tsx
21
- * const share = useShareAgent({
22
- * agent,
23
- * buildShareUrl: (org, slug) => `${appOrigin}/chat/${org}/${slug}`,
24
- * onSharingChanged: refetch,
25
- * });
26
- * // ...
27
- * <ResourceDetailShell actions={share.action ? [...actions, share.action] : actions} ... />
28
- * {share.dialog}
29
- * ```
30
- */
31
- export function useShareAgent({ agent, buildShareUrl, onSharingChanged, label = "Share", }) {
32
- const [isOpen, setIsOpen] = useState(false);
33
- const agentId = agent?.metadata?.id ?? null;
34
- const { allowed: canEdit } = useCheckPermission(agentId ? { kind: "agent", id: agentId } : null, "can_edit");
35
- const open = useCallback(() => setIsOpen(true), []);
36
- const action = agent && canEdit
37
- ? { id: "share", label, group: "sharing", onAction: open }
38
- : null;
39
- const dialog = agent ? (_jsx(ShareAgentDialog, { open: isOpen, onOpenChange: setIsOpen, agent: agent, buildShareUrl: buildShareUrl, onSharingChanged: onSharingChanged })) : null;
40
- return { action, dialog, open, isOpen };
41
- }
42
- //# sourceMappingURL=useShareAgent.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"useShareAgent.js","sourceRoot":"","sources":["../../src/sharing/useShareAgent.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAkB,MAAM,OAAO,CAAC;AAE9D,OAAO,EAAE,kBAAkB,EAAE,MAAM,qCAAqC,CAAC;AAEzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AA2CzD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,UAAU,aAAa,CAAC,EAC5B,KAAK,EACL,aAAa,EACb,gBAAgB,EAChB,KAAK,GAAG,OAAO,GACG;IAClB,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAE5C,MAAM,OAAO,GAAG,KAAK,EAAE,QAAQ,EAAE,EAAE,IAAI,IAAI,CAAC;IAC5C,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,kBAAkB,CAC7C,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,EAC/C,UAAU,CACX,CAAC;IAEF,MAAM,IAAI,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;IAEpD,MAAM,MAAM,GACV,KAAK,IAAI,OAAO;QACd,CAAC,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,EAAE;QAC1D,CAAC,CAAC,IAAI,CAAC;IAEX,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,CACrB,KAAC,gBAAgB,IACf,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,SAAS,EACvB,KAAK,EAAE,KAAK,EACZ,aAAa,EAAE,aAAa,EAC5B,gBAAgB,EAAE,gBAAgB,GAClC,CACH,CAAC,CAAC,CAAC,IAAI,CAAC;IAET,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AAC1C,CAAC"}
@@ -1,131 +0,0 @@
1
- import { describe, it, expect, vi, beforeAll, afterEach } from "vitest";
2
- import { renderHook, waitFor, cleanup } from "@testing-library/react";
3
- import type { ReactNode } from "react";
4
- import { StigmerContext } from "../../context";
5
- import { FetchCacheContext } from "../../internal/FetchCacheProvider";
6
- import { useShareAgent } from "../useShareAgent";
7
-
8
- // happy-dom does not implement the native dialog show/close methods.
9
- beforeAll(() => {
10
- HTMLDialogElement.prototype.showModal = function showModal() {
11
- this.open = true;
12
- };
13
- HTMLDialogElement.prototype.close = function close() {
14
- this.open = false;
15
- };
16
- });
17
-
18
- afterEach(cleanup);
19
-
20
- function createMockStigmer(overrides: {
21
- isAuthorized?: boolean;
22
- checkMyPermission?: (...args: unknown[]) => Promise<unknown>;
23
- } = {}) {
24
- return {
25
- iamPolicy: {
26
- checkMyPermission:
27
- overrides.checkMyPermission ??
28
- vi.fn().mockResolvedValue({
29
- isAuthorized: overrides.isAuthorized ?? true,
30
- }),
31
- },
32
- agentShare: {
33
- getByAgent: vi.fn().mockResolvedValue({ totalCount: 0, items: [] }),
34
- apply: vi.fn().mockResolvedValue({}),
35
- rotateShareLink: vi.fn().mockResolvedValue({}),
36
- },
37
- billing: { getOrCreateBillingAccount: vi.fn().mockResolvedValue(null) },
38
- } as never;
39
- }
40
-
41
- function wrapper(client: unknown) {
42
- return function Wrapper({ children }: { children: ReactNode }) {
43
- return (
44
- <FetchCacheContext.Provider value={null}>
45
- <StigmerContext.Provider value={client as never}>
46
- {children}
47
- </StigmerContext.Provider>
48
- </FetchCacheContext.Provider>
49
- );
50
- };
51
- }
52
-
53
- const AGENT = {
54
- metadata: {
55
- id: "agt_1",
56
- org: "acme",
57
- slug: "support-agent",
58
- name: "Support Agent",
59
- },
60
- spec: {},
61
- } as never;
62
-
63
- describe("useShareAgent", () => {
64
- it("returns a Share action in the sharing group when the user can edit", async () => {
65
- const { result } = renderHook(() => useShareAgent({ agent: AGENT }), {
66
- wrapper: wrapper(createMockStigmer({ isAuthorized: true })),
67
- });
68
-
69
- await waitFor(() => expect(result.current.action).not.toBeNull());
70
- expect(result.current.action?.id).toBe("share");
71
- expect(result.current.action?.label).toBe("Share");
72
- expect(result.current.action?.group).toBe("sharing");
73
- });
74
-
75
- it("returns a null action while the agent is loading", () => {
76
- const checkMyPermission = vi.fn();
77
- const { result } = renderHook(() => useShareAgent({ agent: null }), {
78
- wrapper: wrapper(createMockStigmer({ checkMyPermission })),
79
- });
80
-
81
- expect(result.current.action).toBeNull();
82
- expect(result.current.dialog).toBeNull();
83
- expect(checkMyPermission).not.toHaveBeenCalled();
84
- });
85
-
86
- it("returns a null action when the user lacks can_edit", async () => {
87
- const client = createMockStigmer({ isAuthorized: false });
88
- const { result } = renderHook(() => useShareAgent({ agent: AGENT }), {
89
- wrapper: wrapper(client),
90
- });
91
-
92
- await waitFor(() =>
93
- expect(
94
- (client as { iamPolicy: { checkMyPermission: ReturnType<typeof vi.fn> } })
95
- .iamPolicy.checkMyPermission,
96
- ).toHaveBeenCalled(),
97
- );
98
- await waitFor(() => expect(result.current.action).toBeNull());
99
- });
100
-
101
- it("checks the can_edit relation on the agent", async () => {
102
- const client = createMockStigmer({ isAuthorized: true });
103
- renderHook(() => useShareAgent({ agent: AGENT }), {
104
- wrapper: wrapper(client),
105
- });
106
-
107
- const check = (
108
- client as { iamPolicy: { checkMyPermission: ReturnType<typeof vi.fn> } }
109
- ).iamPolicy.checkMyPermission;
110
- await waitFor(() => expect(check).toHaveBeenCalled());
111
- const input = check.mock.calls[0][0] as {
112
- resource?: { kind: string; id: string };
113
- relation: string;
114
- };
115
- expect(input.relation).toBe("can_edit");
116
- expect(input.resource?.kind).toBe("agent");
117
- expect(input.resource?.id).toBe("agt_1");
118
- });
119
-
120
- it("opens the dialog via the action", async () => {
121
- const { result } = renderHook(() => useShareAgent({ agent: AGENT }), {
122
- wrapper: wrapper(createMockStigmer({ isAuthorized: true })),
123
- });
124
-
125
- await waitFor(() => expect(result.current.action).not.toBeNull());
126
- expect(result.current.isOpen).toBe(false);
127
-
128
- result.current.action?.onAction();
129
- await waitFor(() => expect(result.current.isOpen).toBe(true));
130
- });
131
- });
@@ -1,91 +0,0 @@
1
- "use client";
2
-
3
- import { create } from "@bufbuild/protobuf";
4
- import type { Agent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
5
- import type { AgentShare } from "@stigmer/protos/ai/stigmer/agentic/agentshare/v1/api_pb";
6
- import { GetAgentSharesByAgentRequestSchema } from "@stigmer/protos/ai/stigmer/agentic/agentshare/v1/io_pb";
7
- import { useStigmer } from "../hooks.js";
8
- import { useFetch } from "../internal/useFetch.js";
9
-
10
- /** Return value of {@link useAgentShare}. */
11
- export interface UseAgentShareReturn {
12
- /**
13
- * The agent's canonical share, or `null` while loading, on error, or
14
- * when the agent has never been shared. `share === null && !isLoading
15
- * && !error` means "no share exists yet" — the first save creates it.
16
- */
17
- readonly share: AgentShare | null;
18
- /** `true` while the initial fetch or a refetch is in flight. */
19
- readonly isLoading: boolean;
20
- /** `true` while a background refetch is in flight and stale data is shown. */
21
- readonly isRefetching: boolean;
22
- /** Error from the last failed request, or `null` when healthy. */
23
- readonly error: Error | null;
24
- /** Discard cached data and re-fetch the share from the server. */
25
- readonly refetch: () => void;
26
- }
27
-
28
- /**
29
- * The canonical share among an agent's shares: the one whose slug equals
30
- * the agent's slug (the server's default when a share is created without
31
- * an explicit slug), falling back to the first entry. The data model
32
- * allows N shares per agent (decision 011 D3); the console manages the
33
- * canonical one in Phase A, so extra shares created via manifests never
34
- * confuse the dialog.
35
- */
36
- function pickCanonicalShare(
37
- shares: readonly AgentShare[],
38
- agentSlug: string,
39
- ): AgentShare | null {
40
- return (
41
- shares.find((share) => share.metadata?.slug === agentSlug) ??
42
- shares[0] ??
43
- null
44
- );
45
- }
46
-
47
- /**
48
- * Data hook that loads an agent's canonical {@link AgentShare} — the
49
- * resource carrying the hosted-chat channel configuration (audience,
50
- * allowed origins, visitor messages, tool credentials, link token).
51
- *
52
- * Sharing is channel configuration, not agent behavior (decision 011):
53
- * it lives in its own resource, so reading the agent alone can never
54
- * tell whether it is shared. This hook is how owner-side surfaces (the
55
- * Share dialog) resolve that state.
56
- *
57
- * Pass `null` for `agent` to skip fetching (stable no-op) — useful
58
- * while the agent is still loading. A resolved `null` share means the
59
- * agent has never been shared; the first save creates the share.
60
- *
61
- * @example
62
- * ```tsx
63
- * const { share, isLoading } = useAgentShare(agent);
64
- *
65
- * if (isLoading) return <Spinner />;
66
- * const enabled = share?.spec?.enabled ?? false;
67
- * ```
68
- */
69
- export function useAgentShare(agent: Agent | null): UseAgentShareReturn {
70
- const stigmer = useStigmer();
71
-
72
- const agentId = agent?.metadata?.id ?? "";
73
- const agentSlug = agent?.metadata?.slug ?? "";
74
-
75
- const fetchFn = agentId
76
- ? async () => {
77
- const result = await stigmer.agentShare.getByAgent(
78
- create(GetAgentSharesByAgentRequestSchema, { agentId }),
79
- );
80
- return pickCanonicalShare(result.items, agentSlug);
81
- }
82
- : null;
83
-
84
- const { data: share, isLoading, isRefetching, error, refetch } = useFetch(
85
- fetchFn,
86
- [agentId, agentSlug, stigmer],
87
- null as AgentShare | null,
88
- );
89
-
90
- return { share, isLoading, isRefetching, error, refetch };
91
- }
@@ -1,107 +0,0 @@
1
- "use client";
2
-
3
- import { useCallback, useState, type ReactNode } from "react";
4
- import type { Agent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
5
- import { useCheckPermission } from "../iam-policy/useCheckPermission.js";
6
- import type { DetailAction } from "../resource-detail/types.js";
7
- import { ShareAgentDialog } from "./ShareAgentDialog.js";
8
-
9
- /** Arguments for {@link useShareAgent}. */
10
- export interface UseShareAgentArgs {
11
- /**
12
- * The agent whose sharing is managed, or `null` while it is still
13
- * loading. When `null`, {@link UseShareAgentReturn.action} is `null`
14
- * and the dialog renders nothing — safe to call before the resource
15
- * is ready.
16
- */
17
- readonly agent: Agent | null;
18
- /**
19
- * Builds the absolute public chat URL for the shared agent. The host
20
- * application owns URL construction (its configured public origin may
21
- * differ from the rendering origin — e.g. the desktop app). When
22
- * omitted, the dialog falls back to the relative `/chat/<org>/<slug>`.
23
- */
24
- readonly buildShareUrl?: (org: string, slug: string) => string;
25
- /**
26
- * Called after any sharing change is persisted. Hosts typically pass
27
- * the agent data hook's `refetch`.
28
- */
29
- readonly onSharingChanged?: () => void;
30
- /** Menu-item label. @default "Share" */
31
- readonly label?: string;
32
- }
33
-
34
- /** Return value of {@link useShareAgent}. */
35
- export interface UseShareAgentReturn {
36
- /**
37
- * A ready-to-spread {@link DetailAction} for a kebab/overflow menu, or
38
- * `null` when the agent is unavailable or the user lacks `can_edit`.
39
- * Lives in the `"sharing"` group, beside "Manage access".
40
- */
41
- readonly action: DetailAction | null;
42
- /** The {@link ShareAgentDialog} node — render it once in the host tree. */
43
- readonly dialog: ReactNode;
44
- /** Imperatively open the dialog. */
45
- readonly open: () => void;
46
- /** Whether the dialog is currently open. */
47
- readonly isOpen: boolean;
48
- }
49
-
50
- /**
51
- * Wires the Share dialog to a kebab/overflow menu — the same trigger
52
- * shape as {@link useManageAccess}, its conceptual sibling: Manage access
53
- * governs who can *read* the blueprint; Share governs who can *chat* with
54
- * the running agent (billed to the owning org).
55
- *
56
- * Owns the open-state and the `can_edit` gate — the same permission the
57
- * AgentShare create/apply handlers enforce on the referenced agent, so
58
- * the action never appears to a user whose changes would be rejected.
59
- * Returns a `null` action while the agent is loading or the user cannot
60
- * edit, so the host can unconditionally fold `action` into its actions
61
- * array.
62
- *
63
- * @example
64
- * ```tsx
65
- * const share = useShareAgent({
66
- * agent,
67
- * buildShareUrl: (org, slug) => `${appOrigin}/chat/${org}/${slug}`,
68
- * onSharingChanged: refetch,
69
- * });
70
- * // ...
71
- * <ResourceDetailShell actions={share.action ? [...actions, share.action] : actions} ... />
72
- * {share.dialog}
73
- * ```
74
- */
75
- export function useShareAgent({
76
- agent,
77
- buildShareUrl,
78
- onSharingChanged,
79
- label = "Share",
80
- }: UseShareAgentArgs): UseShareAgentReturn {
81
- const [isOpen, setIsOpen] = useState(false);
82
-
83
- const agentId = agent?.metadata?.id ?? null;
84
- const { allowed: canEdit } = useCheckPermission(
85
- agentId ? { kind: "agent", id: agentId } : null,
86
- "can_edit",
87
- );
88
-
89
- const open = useCallback(() => setIsOpen(true), []);
90
-
91
- const action: DetailAction | null =
92
- agent && canEdit
93
- ? { id: "share", label, group: "sharing", onAction: open }
94
- : null;
95
-
96
- const dialog = agent ? (
97
- <ShareAgentDialog
98
- open={isOpen}
99
- onOpenChange={setIsOpen}
100
- agent={agent}
101
- buildShareUrl={buildShareUrl}
102
- onSharingChanged={onSharingChanged}
103
- />
104
- ) : null;
105
-
106
- return { action, dialog, open, isOpen };
107
- }