@stigmer/react 3.1.13 → 3.1.15

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 (139) hide show
  1. package/agent/AgentDetailView.d.ts +9 -9
  2. package/agent/AgentDetailView.d.ts.map +1 -1
  3. package/agent/AgentDetailView.js +23 -31
  4. package/agent/AgentDetailView.js.map +1 -1
  5. package/agent-instance/AgentInstanceEmptyState.d.ts +4 -1
  6. package/agent-instance/AgentInstanceEmptyState.d.ts.map +1 -1
  7. package/agent-instance/AgentInstanceEmptyState.js +12 -5
  8. package/agent-instance/AgentInstanceEmptyState.js.map +1 -1
  9. package/agent-instance/AgentInstanceList.d.ts +13 -2
  10. package/agent-instance/AgentInstanceList.d.ts.map +1 -1
  11. package/agent-instance/AgentInstanceList.js +8 -4
  12. package/agent-instance/AgentInstanceList.js.map +1 -1
  13. package/agent-instance/useAgentInstances.d.ts +10 -3
  14. package/agent-instance/useAgentInstances.d.ts.map +1 -1
  15. package/agent-instance/useAgentInstances.js +12 -4
  16. package/agent-instance/useAgentInstances.js.map +1 -1
  17. package/button/Button.d.ts +54 -0
  18. package/button/Button.d.ts.map +1 -0
  19. package/button/Button.js +42 -0
  20. package/button/Button.js.map +1 -0
  21. package/button/index.d.ts +3 -0
  22. package/button/index.d.ts.map +1 -0
  23. package/button/index.js +2 -0
  24. package/button/index.js.map +1 -0
  25. package/empty-state/EmptyState.d.ts.map +1 -1
  26. package/empty-state/EmptyState.js +8 -3
  27. package/empty-state/EmptyState.js.map +1 -1
  28. package/empty-state/types.d.ts +2 -0
  29. package/empty-state/types.d.ts.map +1 -1
  30. package/execution/index.d.ts +1 -1
  31. package/execution/index.d.ts.map +1 -1
  32. package/execution/index.js.map +1 -1
  33. package/execution/useCreateAgentExecution.d.ts +81 -9
  34. package/execution/useCreateAgentExecution.d.ts.map +1 -1
  35. package/execution/useCreateAgentExecution.js +42 -4
  36. package/execution/useCreateAgentExecution.js.map +1 -1
  37. package/index.d.ts +5 -3
  38. package/index.d.ts.map +1 -1
  39. package/index.js +4 -2
  40. package/index.js.map +1 -1
  41. package/package.json +4 -4
  42. package/session/useNewSessionFlow.d.ts +6 -3
  43. package/session/useNewSessionFlow.d.ts.map +1 -1
  44. package/session/useNewSessionFlow.js +34 -35
  45. package/session/useNewSessionFlow.js.map +1 -1
  46. package/sharing/AgentShareList.d.ts +46 -0
  47. package/sharing/AgentShareList.d.ts.map +1 -0
  48. package/sharing/AgentShareList.js +159 -0
  49. package/sharing/AgentShareList.js.map +1 -0
  50. package/sharing/ShareAgentDialog.d.ts +38 -25
  51. package/sharing/ShareAgentDialog.d.ts.map +1 -1
  52. package/sharing/ShareAgentDialog.js +131 -62
  53. package/sharing/ShareAgentDialog.js.map +1 -1
  54. package/sharing/index.d.ts +10 -8
  55. package/sharing/index.d.ts.map +1 -1
  56. package/sharing/index.js +5 -4
  57. package/sharing/index.js.map +1 -1
  58. package/sharing/useAgentShares.d.ts +48 -0
  59. package/sharing/useAgentShares.d.ts.map +1 -0
  60. package/sharing/useAgentShares.js +44 -0
  61. package/sharing/useAgentShares.js.map +1 -0
  62. package/sharing/useCanCreateAgentShare.d.ts +43 -0
  63. package/sharing/useCanCreateAgentShare.d.ts.map +1 -0
  64. package/sharing/useCanCreateAgentShare.js +44 -0
  65. package/sharing/useCanCreateAgentShare.js.map +1 -0
  66. package/sharing/useDeleteAgentShare.d.ts +42 -0
  67. package/sharing/useDeleteAgentShare.d.ts.map +1 -0
  68. package/sharing/useDeleteAgentShare.js +45 -0
  69. package/sharing/useDeleteAgentShare.js.map +1 -0
  70. package/sharing/useSaveAgentShare.d.ts +43 -18
  71. package/sharing/useSaveAgentShare.d.ts.map +1 -1
  72. package/sharing/useSaveAgentShare.js +55 -22
  73. package/sharing/useSaveAgentShare.js.map +1 -1
  74. package/src/agent/AgentDetailView.tsx +37 -43
  75. package/src/agent-instance/AgentInstanceEmptyState.tsx +24 -43
  76. package/src/agent-instance/AgentInstanceList.tsx +29 -16
  77. package/src/agent-instance/__tests__/agent-instance-hooks.test.tsx +19 -0
  78. package/src/agent-instance/useAgentInstances.ts +12 -3
  79. package/src/button/Button.tsx +104 -0
  80. package/src/button/__tests__/Button.test.tsx +82 -0
  81. package/src/button/index.ts +2 -0
  82. package/src/empty-state/EmptyState.tsx +8 -10
  83. package/src/empty-state/types.ts +2 -0
  84. package/src/execution/__tests__/useCreateAgentExecution.test.tsx +92 -0
  85. package/src/execution/index.ts +2 -0
  86. package/src/execution/useCreateAgentExecution.ts +116 -10
  87. package/src/index.ts +17 -9
  88. package/src/session/__tests__/useNewSessionFlow.test.tsx +121 -52
  89. package/src/session/useNewSessionFlow.ts +38 -37
  90. package/src/sharing/AgentShareList.tsx +502 -0
  91. package/src/sharing/ShareAgentDialog.tsx +310 -112
  92. package/src/sharing/__tests__/AgentShareList.test.tsx +388 -0
  93. package/src/sharing/__tests__/ShareAgentDialog.test.tsx +311 -312
  94. package/src/sharing/__tests__/useAgentShares.test.tsx +186 -0
  95. package/src/sharing/__tests__/useCanCreateAgentShare.test.tsx +190 -0
  96. package/src/sharing/index.ts +10 -12
  97. package/src/sharing/useAgentShares.ts +74 -0
  98. package/src/sharing/useCanCreateAgentShare.ts +74 -0
  99. package/src/sharing/useDeleteAgentShare.ts +73 -0
  100. package/src/sharing/useSaveAgentShare.ts +73 -23
  101. package/src/workflow/WorkflowDetailView.tsx +9 -0
  102. package/src/workflow/instance/WorkflowInstanceEmptyState.tsx +24 -43
  103. package/src/workflow/instance/WorkflowInstanceList.tsx +29 -16
  104. package/src/workflow/useWorkflowInstances.ts +10 -3
  105. package/styles.css +1 -1
  106. package/workflow/WorkflowDetailView.d.ts +8 -1
  107. package/workflow/WorkflowDetailView.d.ts.map +1 -1
  108. package/workflow/WorkflowDetailView.js +2 -2
  109. package/workflow/WorkflowDetailView.js.map +1 -1
  110. package/workflow/instance/WorkflowInstanceEmptyState.d.ts +4 -1
  111. package/workflow/instance/WorkflowInstanceEmptyState.d.ts.map +1 -1
  112. package/workflow/instance/WorkflowInstanceEmptyState.js +12 -5
  113. package/workflow/instance/WorkflowInstanceEmptyState.js.map +1 -1
  114. package/workflow/instance/WorkflowInstanceList.d.ts +13 -2
  115. package/workflow/instance/WorkflowInstanceList.d.ts.map +1 -1
  116. package/workflow/instance/WorkflowInstanceList.js +8 -4
  117. package/workflow/instance/WorkflowInstanceList.js.map +1 -1
  118. package/workflow/useWorkflowInstances.d.ts +8 -3
  119. package/workflow/useWorkflowInstances.d.ts.map +1 -1
  120. package/workflow/useWorkflowInstances.js +10 -4
  121. package/workflow/useWorkflowInstances.js.map +1 -1
  122. package/sharing/useAgentShare.d.ts +0 -49
  123. package/sharing/useAgentShare.d.ts.map +0 -1
  124. package/sharing/useAgentShare.js +0 -64
  125. package/sharing/useAgentShare.js.map +0 -1
  126. package/sharing/useCreateExternalShareLink.d.ts +0 -90
  127. package/sharing/useCreateExternalShareLink.d.ts.map +0 -1
  128. package/sharing/useCreateExternalShareLink.js +0 -65
  129. package/sharing/useCreateExternalShareLink.js.map +0 -1
  130. package/sharing/useShareAgent.d.ts +0 -69
  131. package/sharing/useShareAgent.d.ts.map +0 -1
  132. package/sharing/useShareAgent.js +0 -42
  133. package/sharing/useShareAgent.js.map +0 -1
  134. package/src/sharing/__tests__/useAgentShare.test.tsx +0 -210
  135. package/src/sharing/__tests__/useCreateExternalShareLink.test.tsx +0 -194
  136. package/src/sharing/__tests__/useShareAgent.test.tsx +0 -131
  137. package/src/sharing/useAgentShare.ts +0 -105
  138. package/src/sharing/useCreateExternalShareLink.tsx +0 -141
  139. package/src/sharing/useShareAgent.tsx +0 -107
@@ -1,9 +1,10 @@
1
1
  "use client";
2
2
 
3
- import { useCallback, useMemo, useRef, useState } from "react";
3
+ import { useCallback, useMemo, useRef, useState, type FormEvent } from "react";
4
4
  import { cn } from "@stigmer/theme";
5
5
  import {
6
6
  MAX_ALLOWED_ORIGINS,
7
+ StigmerError,
7
8
  appendLinkToken,
8
9
  buildEmbedSnippet,
9
10
  chatPath,
@@ -11,10 +12,12 @@ import {
11
12
  validateOrigin,
12
13
  type ResourceRef,
13
14
  } from "@stigmer/sdk";
15
+ import { create as createMessage } from "@bufbuild/protobuf";
14
16
  import type { Agent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
15
17
  import type { AgentShare } from "@stigmer/protos/ai/stigmer/agentic/agentshare/v1/api_pb";
16
18
  import type { Environment } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/api_pb";
17
19
  import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
20
+ import { ApiResourceMetadataSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/metadata_pb";
18
21
  import { Switch } from "../switch/Switch.js";
19
22
  import { Tabs, type TabItem } from "../tabs/Tabs.js";
20
23
  import { toast } from "../feedback/toast.js";
@@ -23,9 +26,10 @@ import { useDeploymentMode } from "../deployment-mode.js";
23
26
  import { useBillingAccount } from "../billing/useBillingAccount.js";
24
27
  import { formatCreditBalance } from "../billing/format.js";
25
28
  import { EnvironmentPicker } from "../environment/EnvironmentPicker.js";
26
- import { useAgentShare } from "./useAgentShare.js";
29
+ import { generateSlug } from "../internal/slug.js";
30
+ import { getFieldError, validateMessage } from "../internal/validate.js";
27
31
  import {
28
- sharingAudienceFromProto,
32
+ draftFromShare,
29
33
  useSaveAgentShare,
30
34
  type AgentShareDraft,
31
35
  type SharingAudience,
@@ -53,6 +57,24 @@ export interface ShareAgentDialogProps {
53
57
  readonly onOpenChange: (open: boolean) => void;
54
58
  /** The agent whose sharing is managed. */
55
59
  readonly agent: Agent;
60
+ /**
61
+ * The exact share to edit. When omitted, the dialog opens in **create
62
+ * mode**: a name/slug step creates a new share in `shareOrg`, then the
63
+ * dialog becomes its editor. The share's identity is immutable once
64
+ * created (decision 011 D2) — edit mode never renames.
65
+ */
66
+ readonly share?: AgentShare | null;
67
+ /**
68
+ * The org that owns (and pays for) a share created by this dialog.
69
+ * Defaults to the agent's own org — the owner's channel. Pass the
70
+ * viewer's org to create a **cross-org share** of another org's
71
+ * marketplace-public agent (decision 013): the share, its billing,
72
+ * its credentials, and its hosted URL all belong to this org, while
73
+ * the agent stays live in its own. Cross-org shares are
74
+ * public-audience only, so the audience selector is hidden in that
75
+ * mode. Ignored in edit mode — an existing share knows its org.
76
+ */
77
+ readonly shareOrg?: string;
56
78
  /**
57
79
  * Builds the absolute public chat URL for the shared agent. The host
58
80
  * application owns URL construction (its configured public origin may
@@ -62,19 +84,9 @@ export interface ShareAgentDialogProps {
62
84
  readonly buildShareUrl?: (org: string, slug: string) => string;
63
85
  /**
64
86
  * Called after any sharing change is persisted. Hosts typically pass
65
- * the agent data hook's `refetch`.
87
+ * the share list's `refetch`.
66
88
  */
67
89
  readonly onSharingChanged?: () => void;
68
- /**
69
- * The org that owns (and pays for) the share. Defaults to the agent's
70
- * own org — the owner's channel. Pass the viewer's org to manage a
71
- * **cross-org share** of another org's marketplace-public agent
72
- * (decision 013): the share, its billing, its credentials, and its
73
- * hosted URL all belong to this org, while the agent stays live in its
74
- * own. Cross-org shares are public-audience only, so the audience
75
- * selector is hidden in that mode.
76
- */
77
- readonly shareOrg?: string;
78
90
  /**
79
91
  * When `false`, renders as an in-flow open dialog instead of a
80
92
  * top-layer modal — no `showModal()`, no backdrop, no focus trap.
@@ -86,37 +98,42 @@ export interface ShareAgentDialogProps {
86
98
  }
87
99
 
88
100
  /**
89
- * The Share dialog for an agent: toggle sharing, copy the hosted chat
90
- * link, copy an embeddable snippet, manage allowed embed origins, bind
91
- * tool credentials for visitors, customize visitor refusal messages,
92
- * and discover the PlatformClient SDK path.
101
+ * The Share dialog for one agent share: create it (name/slug step), then
102
+ * toggle serving, copy the hosted chat link, copy an embeddable snippet,
103
+ * manage allowed embed origins, bind tool credentials for visitors,
104
+ * customize visitor refusal messages, and discover the PlatformClient
105
+ * SDK path.
93
106
  *
94
107
  * Sharing is a distinct consent from marketplace visibility: visibility
95
108
  * governs who can *read* the blueprint; sharing governs who can *chat*
96
- * with the running agent — billed to the owning org. The dialog states
97
- * who pays next to the toggle so enabling sharing never surprises.
109
+ * with the running agent — billed to the org that owns the share. The
110
+ * dialog states who pays before creation and next to the toggle, so
111
+ * sharing never surprises.
98
112
  *
99
- * Sharing lives in its own **AgentShare resource** (decision 011), so
100
- * the dialog loads the agent's canonical share when it opens and every
101
- * save is an idempotent `apply` of the complete configuration — the
102
- * first enable creates the share, later edits update it, one code path
103
- * (see {@link useSaveAgentShare}). The local draft is refreshed from
104
- * every returned share, so the dialog never drifts from the server.
113
+ * Sharing lives in its own **AgentShare resource** (decision 011), and
114
+ * an agent can carry N shares each its own channel with its own URL,
115
+ * audience, origins, credentials, and link token (D3). The dialog edits
116
+ * exactly the share it is given; it never resolves "the" share of an
117
+ * agent. Every save is an idempotent `apply` of the complete
118
+ * configuration ({@link useSaveAgentShare}), and the local draft is
119
+ * refreshed from every returned share, so the dialog never drifts from
120
+ * the server.
105
121
  *
106
122
  * Built on the native `<dialog>` element for focus trapping and escape
107
123
  * handling, matching the SDK's modal convention ({@link ManageAccessDialog}).
108
124
  * All visual properties flow through `--stgm-*` design tokens.
109
125
  *
110
- * Most hosts mount it via {@link useShareAgent} (a kebab-menu action).
126
+ * Most hosts mount it via {@link AgentShareList} (the Shares tab).
111
127
  * Render it directly only when you own the open-state.
112
128
  */
113
129
  export function ShareAgentDialog({
114
130
  open,
115
131
  onOpenChange,
116
132
  agent,
133
+ share,
134
+ shareOrg,
117
135
  buildShareUrl,
118
136
  onSharingChanged,
119
- shareOrg,
120
137
  modal = true,
121
138
  }: ShareAgentDialogProps) {
122
139
  const dialogRef = useRef<HTMLDialogElement>(null);
@@ -154,14 +171,15 @@ export function ShareAgentDialog({
154
171
  )}
155
172
  aria-labelledby="share-agent-title"
156
173
  >
157
- {/* Body mounts only while open so the share is re-read per session
158
- (the draft resets with it) and no fetch fires on a closed dialog. */}
174
+ {/* Body mounts only while open so its draft state resets per
175
+ session reopening the dialog never shows a stale draft. */}
159
176
  {open && (
160
177
  <ShareAgentDialogBody
161
178
  agent={agent}
179
+ share={share ?? null}
180
+ shareOrg={shareOrg}
162
181
  buildShareUrl={buildShareUrl}
163
182
  onSharingChanged={onSharingChanged}
164
- shareOrg={shareOrg}
165
183
  onClose={handleClose}
166
184
  />
167
185
  )}
@@ -170,31 +188,39 @@ export function ShareAgentDialog({
170
188
  }
171
189
 
172
190
  // ---------------------------------------------------------------------------
173
- // Dialog body — resolves the canonical share, then hands off to the form
191
+ // Dialog body — create step or editor, depending on the share it was given
174
192
  // ---------------------------------------------------------------------------
175
193
 
176
194
  /**
177
- * Loads the agent's canonical share and mounts the form once resolved.
178
- * The split keeps the form's draft seeding synchronous (`useState`
179
- * initializer from the loaded share)no hydrate-on-effect, no window
180
- * where the switch shows a state the server never had.
195
+ * Renders the create step (no share yet) or the editor (share in hand).
196
+ * A share persisted by the create step is adopted locally, so one dialog
197
+ * session flows create edit without remounting and the editor's
198
+ * draft seeding stays synchronous (`useState` initializer from the
199
+ * share): no hydrate-on-effect, no window where the switch shows a state
200
+ * the server never had.
181
201
  */
182
202
  function ShareAgentDialogBody({
183
203
  agent,
204
+ share,
205
+ shareOrg,
184
206
  buildShareUrl,
185
207
  onSharingChanged,
186
- shareOrg,
187
208
  onClose,
188
209
  }: {
189
210
  readonly agent: Agent;
211
+ readonly share: AgentShare | null;
212
+ readonly shareOrg?: string;
190
213
  readonly buildShareUrl?: (org: string, slug: string) => string;
191
214
  readonly onSharingChanged?: () => void;
192
- readonly shareOrg?: string;
193
215
  readonly onClose: () => void;
194
216
  }) {
195
- const { share, isLoading, error, refetch } = useAgentShare(agent, shareOrg);
196
- const isCrossOrg =
197
- !!shareOrg && shareOrg !== (agent.metadata?.org ?? "");
217
+ const [created, setCreated] = useState<AgentShare | null>(null);
218
+ const activeShare = share ?? created;
219
+ const isCreating = activeShare === null;
220
+
221
+ const resolvedShareOrg =
222
+ activeShare?.metadata?.org || shareOrg || (agent.metadata?.org ?? "");
223
+ const isCrossOrg = resolvedShareOrg !== (agent.metadata?.org ?? "");
198
224
 
199
225
  return (
200
226
  <div className="flex flex-col">
@@ -205,7 +231,7 @@ function ShareAgentDialogBody({
205
231
  id="share-agent-title"
206
232
  className="text-base font-semibold text-foreground"
207
233
  >
208
- Share
234
+ {isCreating ? "Create share" : "Share"}
209
235
  </h2>
210
236
  <p className="mt-0.5 truncate text-xs text-muted-foreground">
211
237
  {/* Cross-org: qualify the agent so it's clear whose blueprint
@@ -229,42 +255,21 @@ function ShareAgentDialogBody({
229
255
  </button>
230
256
  </div>
231
257
 
232
- {isLoading ? (
233
- <div
234
- className="px-6 py-10 text-center"
235
- aria-busy="true"
236
- aria-label="Loading sharing settings"
237
- >
238
- <div className="mx-auto h-4 w-2/3 animate-pulse rounded bg-muted" />
239
- <div className="mx-auto mt-2 h-3 w-1/2 animate-pulse rounded bg-muted" />
240
- </div>
241
- ) : error ? (
242
- <div className="px-6 py-8 text-center" role="alert">
243
- <p className="text-sm text-foreground">
244
- Couldn&apos;t load this agent&apos;s sharing settings.
245
- </p>
246
- <p className="mt-1 text-xs text-muted-foreground">
247
- {getUserMessage(error)}
248
- </p>
249
- <button
250
- type="button"
251
- onClick={refetch}
252
- className={cn(
253
- "mt-3 rounded-md px-3 py-1.5 text-xs font-medium",
254
- "border border-border text-foreground hover:bg-accent-hover",
255
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
256
- )}
257
- >
258
- Try again
259
- </button>
260
- </div>
258
+ {isCreating ? (
259
+ <CreateShareForm
260
+ agent={agent}
261
+ shareOrg={resolvedShareOrg}
262
+ onCreated={(persisted) => {
263
+ setCreated(persisted);
264
+ onSharingChanged?.();
265
+ }}
266
+ />
261
267
  ) : (
262
268
  <ShareAgentForm
263
269
  agent={agent}
264
- initialShare={share}
270
+ initialShare={activeShare}
265
271
  buildShareUrl={buildShareUrl}
266
272
  onSharingChanged={onSharingChanged}
267
- shareOrg={shareOrg}
268
273
  />
269
274
  )}
270
275
 
@@ -275,11 +280,13 @@ function ShareAgentDialogBody({
275
280
  onClick={onClose}
276
281
  className={cn(
277
282
  "rounded-md px-3 py-1.5 text-sm font-medium",
278
- "bg-primary text-primary-foreground hover:bg-primary-hover",
283
+ isCreating
284
+ ? "border border-border text-foreground hover:bg-accent-hover"
285
+ : "bg-primary text-primary-foreground hover:bg-primary-hover",
279
286
  "focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
280
287
  )}
281
288
  >
282
- Done
289
+ {isCreating ? "Cancel" : "Done"}
283
290
  </button>
284
291
  </div>
285
292
  </div>
@@ -287,71 +294,262 @@ function ShareAgentDialogBody({
287
294
  }
288
295
 
289
296
  // ---------------------------------------------------------------------------
290
- // Formowns the share draft for the session
297
+ // Create step — the new share's identity and cost, before any channel exists
291
298
  // ---------------------------------------------------------------------------
292
299
 
293
300
  /**
294
- * The dialog's editable projection of a share. `null` (never shared)
295
- * seeds the same defaults the server would apply on first create, so
296
- * the form needs no "no share yet" special case beyond the disabled
297
- * copy fields that `enabled: false` already produces.
301
+ * Names the new share and creates it live (`enabled: true`, public
302
+ * audience the server's own defaults). Identity is set here because it
303
+ * is immutable afterward (decision 011 D2): the slug becomes the hosted
304
+ * URL `/chat/<org>/<slug>` in the sharing org's namespace.
305
+ *
306
+ * The slug auto-derives from the name until the user edits it (the
307
+ * {@link CreateOrganizationForm} pattern), validated by the same
308
+ * protovalidate rules the server enforces. An `(org, slug)` collision —
309
+ * the server's ALREADY_EXISTS — surfaces inline on the slug field with a
310
+ * pick-another-slug remedy instead of a generic failure toast.
298
311
  */
299
- function draftFromShare(share: AgentShare | null): AgentShareDraft {
300
- const spec = share?.spec;
301
- return {
302
- enabled: spec?.enabled ?? false,
303
- audience: sharingAudienceFromProto(spec?.audience),
304
- allowedOrigins: spec?.allowedOrigins ?? [],
305
- messages: {
306
- rateLimited: spec?.messages?.rateLimited ?? "",
307
- unavailable: spec?.messages?.unavailable ?? "",
308
- conversationEnded: spec?.messages?.conversationEnded ?? "",
312
+ function CreateShareForm({
313
+ agent,
314
+ shareOrg,
315
+ onCreated,
316
+ }: {
317
+ readonly agent: Agent;
318
+ readonly shareOrg: string;
319
+ readonly onCreated: (share: AgentShare) => void;
320
+ }) {
321
+ const agentName = agent.metadata?.name || (agent.metadata?.slug ?? "");
322
+ const isCrossOrg = shareOrg !== (agent.metadata?.org ?? "");
323
+
324
+ const [name, setName] = useState(agentName);
325
+ const [slug, setSlug] = useState(agent.metadata?.slug ?? "");
326
+ // Once the user manually edits the slug field, stop auto-deriving.
327
+ const slugTouchedRef = useRef(false);
328
+ // A server-side (org, slug) collision pins to the slug field until the
329
+ // slug changes — the remedy is picking another slug.
330
+ const [collisionError, setCollisionError] = useState<string | null>(null);
331
+ const [submitError, setSubmitError] = useState<string | null>(null);
332
+
333
+ const { save, isPending } = useSaveAgentShare(agent, shareOrg);
334
+
335
+ const trimmedName = name.trim();
336
+ const violations =
337
+ slug.length > 0 || trimmedName.length > 0
338
+ ? validateMessage(
339
+ ApiResourceMetadataSchema,
340
+ createMessage(ApiResourceMetadataSchema, {
341
+ ...(slug.length > 0 && { slug }),
342
+ ...(trimmedName.length > 0 && { name: trimmedName }),
343
+ }),
344
+ )
345
+ : [];
346
+ const slugError =
347
+ collisionError ?? (slug.length > 0 ? getFieldError(violations, "slug") : null);
348
+ const nameError =
349
+ trimmedName.length > 0 ? getFieldError(violations, "name") : null;
350
+ const canSubmit =
351
+ trimmedName !== "" && slug.length > 0 && slugError === null &&
352
+ nameError === null && !isPending;
353
+
354
+ const handleNameChange = useCallback((value: string) => {
355
+ setName(value);
356
+ if (!slugTouchedRef.current) {
357
+ setSlug(generateSlug(value.trim()));
358
+ setCollisionError(null);
359
+ }
360
+ }, []);
361
+
362
+ const handleSlugChange = useCallback((value: string) => {
363
+ slugTouchedRef.current = true;
364
+ setSlug(value);
365
+ setCollisionError(null);
366
+ }, []);
367
+
368
+ const handleSubmit = useCallback(
369
+ async (e: FormEvent) => {
370
+ e.preventDefault();
371
+ if (!canSubmit) return;
372
+ setSubmitError(null);
373
+
374
+ try {
375
+ const persisted = await save(
376
+ // The server's create defaults, made explicit: live from the
377
+ // start (the user's one intent-click is "share it"), public
378
+ // audience, nothing else configured yet.
379
+ { ...draftFromShare(null), enabled: true },
380
+ null,
381
+ { name: trimmedName, slug },
382
+ );
383
+ if (persisted) {
384
+ toast.success("Share created — the link is live");
385
+ onCreated(persisted);
386
+ }
387
+ } catch (err) {
388
+ if (err instanceof StigmerError && err.code === "already-exists") {
389
+ setCollisionError(
390
+ `"${slug}" is already used by another share or resource in ${shareOrg} — pick a different slug`,
391
+ );
392
+ } else {
393
+ // e.g. the cross-org FAILED_PRECONDITION naming non-public
394
+ // dependencies (decision 013 D5) — show the server's words.
395
+ setSubmitError(getUserMessage(err));
396
+ }
397
+ }
309
398
  },
310
- environmentRefs: (spec?.environmentRefs ?? []).map((ref) => ({
311
- org: ref.org,
312
- slug: ref.slug,
313
- })),
314
- };
399
+ [canSubmit, save, trimmedName, slug, shareOrg, onCreated],
400
+ );
401
+
402
+ return (
403
+ <form onSubmit={handleSubmit} className="flex flex-col gap-4 px-6 py-4">
404
+ <p className="text-xs text-muted-foreground">
405
+ A share is its own channel to this agent: a hosted chat link with
406
+ its own audience, embed origins, tool credentials, and visitor
407
+ messages.{" "}
408
+ {isCrossOrg && (
409
+ <>
410
+ This one lives in your organization{" "}
411
+ <span className="font-medium">{shareOrg}</span> while the agent
412
+ stays in <span className="font-medium">{agent.metadata?.org}</span>{" "}
413
+ — updates to the agent apply live.{" "}
414
+ </>
415
+ )}
416
+ Visitors chat on <span className="font-medium">{shareOrg}</span>&apos;s
417
+ credits.
418
+ </p>
419
+
420
+ {/* ---- Name ---- */}
421
+ <div className="space-y-1">
422
+ <label
423
+ htmlFor="stgm-new-share-name"
424
+ className="text-xs font-medium text-foreground"
425
+ >
426
+ Name
427
+ </label>
428
+ <input
429
+ id="stgm-new-share-name"
430
+ type="text"
431
+ value={name}
432
+ onChange={(e) => handleNameChange(e.target.value)}
433
+ disabled={isPending}
434
+ required
435
+ maxLength={63}
436
+ className={cn(
437
+ "w-full rounded-md border bg-input-bg px-2.5 py-1.5 text-xs text-foreground",
438
+ "placeholder:text-muted-foreground",
439
+ "focus:outline-none focus:ring-2 focus:ring-ring",
440
+ "disabled:pointer-events-none disabled:opacity-50",
441
+ nameError ? "border-destructive" : "border-border",
442
+ )}
443
+ />
444
+ {nameError ? (
445
+ <p className="text-[0.65rem] text-destructive" role="alert">
446
+ {nameError}
447
+ </p>
448
+ ) : (
449
+ <p className="text-[0.65rem] text-muted-foreground">
450
+ A display name for this share — e.g. the site or campaign it
451
+ serves.
452
+ </p>
453
+ )}
454
+ </div>
455
+
456
+ {/* ---- Slug ---- */}
457
+ <div className="space-y-1">
458
+ <label
459
+ htmlFor="stgm-new-share-slug"
460
+ className="text-xs font-medium text-foreground"
461
+ >
462
+ Slug
463
+ </label>
464
+ <input
465
+ id="stgm-new-share-slug"
466
+ type="text"
467
+ value={slug}
468
+ onChange={(e) => handleSlugChange(e.target.value)}
469
+ disabled={isPending}
470
+ required
471
+ className={cn(
472
+ "w-full rounded-md border bg-input-bg px-2.5 py-1.5 font-mono text-xs text-foreground",
473
+ "placeholder:text-muted-foreground",
474
+ "focus:outline-none focus:ring-2 focus:ring-ring",
475
+ "disabled:pointer-events-none disabled:opacity-50",
476
+ slugError ? "border-destructive" : "border-border",
477
+ )}
478
+ />
479
+ {slugError ? (
480
+ <p className="text-[0.65rem] text-destructive" role="alert">
481
+ {slugError}
482
+ </p>
483
+ ) : (
484
+ <p className="text-[0.65rem] text-muted-foreground">
485
+ Becomes the share&apos;s address:{" "}
486
+ <code className="font-mono">
487
+ /chat/{shareOrg}/{slug || "…"}
488
+ </code>
489
+ . Can&apos;t be changed later.
490
+ </p>
491
+ )}
492
+ </div>
493
+
494
+ {submitError && (
495
+ <p className="text-xs text-destructive" role="alert">
496
+ {submitError}
497
+ </p>
498
+ )}
499
+
500
+ <div className="flex items-center justify-end">
501
+ <button
502
+ type="submit"
503
+ disabled={!canSubmit}
504
+ className={cn(
505
+ "rounded-md px-3 py-1.5 text-sm font-medium",
506
+ "bg-primary text-primary-foreground hover:bg-primary-hover",
507
+ "focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
508
+ "disabled:pointer-events-none disabled:opacity-50",
509
+ )}
510
+ >
511
+ {isPending ? "Creating…" : "Create share"}
512
+ </button>
513
+ </div>
514
+ </form>
515
+ );
315
516
  }
316
517
 
518
+ // ---------------------------------------------------------------------------
519
+ // Form — owns the share draft for the session
520
+ // ---------------------------------------------------------------------------
521
+
317
522
  function ShareAgentForm({
318
523
  agent,
319
524
  initialShare,
320
525
  buildShareUrl,
321
526
  onSharingChanged,
322
- shareOrg,
323
527
  }: {
324
528
  readonly agent: Agent;
325
- readonly initialShare: AgentShare | null;
529
+ readonly initialShare: AgentShare;
326
530
  readonly buildShareUrl?: (org: string, slug: string) => string;
327
531
  readonly onSharingChanged?: () => void;
328
- readonly shareOrg?: string;
329
532
  }) {
330
533
  const agentName = agent.metadata?.name || (agent.metadata?.slug ?? "");
331
534
 
332
535
  // The latest server share is the single baseline: the draft, the link
333
- // token, and the share id for rotation all derive from it. `null`
334
- // until the first save creates the share.
335
- const [share, setShare] = useState<AgentShare | null>(initialShare);
536
+ // token, and the share id for rotation all derive from it.
537
+ const [share, setShare] = useState<AgentShare>(initialShare);
336
538
  const [draft, setDraft] = useState<AgentShareDraft>(() =>
337
539
  draftFromShare(initialShare),
338
540
  );
339
541
  const [activeTab, setActiveTab] = useState("link");
340
542
 
341
- const { save, isPending } = useSaveAgentShare(agent, shareOrg);
543
+ const { save, isPending } = useSaveAgentShare(agent);
342
544
  const { rotateShareLink, isPending: isRotating } = useRotateShareLink(
343
- share?.metadata?.id ?? null,
545
+ share.metadata?.id ?? null,
344
546
  );
345
547
 
346
- // The share's own org/slug form the hosted URL. Before the first save
347
- // the sharing org + the agent's slug stand in — exactly the identity
348
- // the server will assign on create (D2: share slug defaults to the
349
- // agent's; the org is the channel owner's, which for a cross-org share
350
- // is the viewer's org, not the agent's — decision 013).
351
- const org =
352
- share?.metadata?.org || shareOrg || (agent.metadata?.org ?? "");
353
- const slug = share?.metadata?.slug || (agent.metadata?.slug ?? "");
354
- const linkToken = share?.status?.shareLinkToken ?? "";
548
+ // The share's own org/slug are the hosted URL for a cross-org share
549
+ // (decision 013) that org is the sharing org's, not the agent's.
550
+ const org = share.metadata?.org ?? "";
551
+ const slug = share.metadata?.slug ?? "";
552
+ const linkToken = share.status?.shareLinkToken ?? "";
355
553
  // Cross-org shares are public-audience only (decision 013 D3) — the
356
554
  // audience selector disappears rather than offering a choice the
357
555
  // server would refuse.