@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.
- package/agent/AgentDetailView.d.ts +11 -3
- package/agent/AgentDetailView.d.ts.map +1 -1
- package/agent/AgentDetailView.js +22 -17
- package/agent/AgentDetailView.js.map +1 -1
- package/index.d.ts +2 -2
- package/index.d.ts.map +1 -1
- package/index.js +2 -2
- package/index.js.map +1 -1
- package/package.json +4 -4
- package/sharing/AgentShareList.d.ts +44 -0
- package/sharing/AgentShareList.d.ts.map +1 -0
- package/sharing/AgentShareList.js +149 -0
- package/sharing/AgentShareList.js.map +1 -0
- package/sharing/ShareAgentDialog.d.ts +38 -15
- package/sharing/ShareAgentDialog.d.ts.map +1 -1
- package/sharing/ShareAgentDialog.js +135 -57
- package/sharing/ShareAgentDialog.js.map +1 -1
- package/sharing/index.d.ts +10 -6
- package/sharing/index.d.ts.map +1 -1
- package/sharing/index.js +5 -3
- package/sharing/index.js.map +1 -1
- package/sharing/useAgentShares.d.ts +42 -0
- package/sharing/useAgentShares.d.ts.map +1 -0
- package/sharing/useAgentShares.js +38 -0
- package/sharing/useAgentShares.js.map +1 -0
- package/sharing/useCanCreateAgentShare.d.ts +43 -0
- package/sharing/useCanCreateAgentShare.d.ts.map +1 -0
- package/sharing/useCanCreateAgentShare.js +44 -0
- package/sharing/useCanCreateAgentShare.js.map +1 -0
- package/sharing/useDeleteAgentShare.d.ts +42 -0
- package/sharing/useDeleteAgentShare.d.ts.map +1 -0
- package/sharing/useDeleteAgentShare.js +45 -0
- package/sharing/useDeleteAgentShare.js.map +1 -0
- package/sharing/useSaveAgentShare.d.ts +45 -14
- package/sharing/useSaveAgentShare.d.ts.map +1 -1
- package/sharing/useSaveAgentShare.js +59 -19
- package/sharing/useSaveAgentShare.js.map +1 -1
- package/src/agent/AgentDetailView.tsx +37 -20
- package/src/index.ts +11 -6
- package/src/sharing/AgentShareList.tsx +507 -0
- package/src/sharing/ShareAgentDialog.tsx +325 -95
- package/src/sharing/__tests__/AgentShareList.test.tsx +388 -0
- package/src/sharing/__tests__/ShareAgentDialog.test.tsx +333 -286
- package/src/sharing/__tests__/{useAgentShare.test.tsx → useAgentShares.test.tsx} +47 -51
- package/src/sharing/__tests__/useCanCreateAgentShare.test.tsx +190 -0
- package/src/sharing/__tests__/useSaveAgentShare.test.tsx +48 -0
- package/src/sharing/index.ts +10 -7
- package/src/sharing/useAgentShares.ts +68 -0
- package/src/sharing/useCanCreateAgentShare.ts +74 -0
- package/src/sharing/useDeleteAgentShare.ts +73 -0
- package/src/sharing/useSaveAgentShare.ts +77 -19
- package/styles.css +1 -1
- package/sharing/useAgentShare.d.ts +0 -43
- package/sharing/useAgentShare.d.ts.map +0 -1
- package/sharing/useAgentShare.js +0 -54
- package/sharing/useAgentShare.js.map +0 -1
- package/sharing/useShareAgent.d.ts +0 -69
- package/sharing/useShareAgent.d.ts.map +0 -1
- package/sharing/useShareAgent.js +0 -42
- package/sharing/useShareAgent.js.map +0 -1
- package/src/sharing/__tests__/useShareAgent.test.tsx +0 -131
- package/src/sharing/useAgentShare.ts +0 -91
- 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 {
|
|
29
|
+
import { generateSlug } from "../internal/slug.js";
|
|
30
|
+
import { getFieldError, validateMessage } from "../internal/validate.js";
|
|
27
31
|
import {
|
|
28
|
-
|
|
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,7 +84,7 @@ 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
|
|
87
|
+
* the share list's `refetch`.
|
|
66
88
|
*/
|
|
67
89
|
readonly onSharingChanged?: () => void;
|
|
68
90
|
/**
|
|
@@ -76,34 +98,40 @@ export interface ShareAgentDialogProps {
|
|
|
76
98
|
}
|
|
77
99
|
|
|
78
100
|
/**
|
|
79
|
-
* The Share dialog for
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
* and discover the PlatformClient
|
|
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.
|
|
83
106
|
*
|
|
84
107
|
* Sharing is a distinct consent from marketplace visibility: visibility
|
|
85
108
|
* governs who can *read* the blueprint; sharing governs who can *chat*
|
|
86
|
-
* with the running agent — billed to the
|
|
87
|
-
* who pays next to the toggle so
|
|
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.
|
|
88
112
|
*
|
|
89
|
-
* Sharing lives in its own **AgentShare resource** (decision 011),
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
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.
|
|
95
121
|
*
|
|
96
122
|
* Built on the native `<dialog>` element for focus trapping and escape
|
|
97
123
|
* handling, matching the SDK's modal convention ({@link ManageAccessDialog}).
|
|
98
124
|
* All visual properties flow through `--stgm-*` design tokens.
|
|
99
125
|
*
|
|
100
|
-
* Most hosts mount it via {@link
|
|
126
|
+
* Most hosts mount it via {@link AgentShareList} (the Shares tab).
|
|
101
127
|
* Render it directly only when you own the open-state.
|
|
102
128
|
*/
|
|
103
129
|
export function ShareAgentDialog({
|
|
104
130
|
open,
|
|
105
131
|
onOpenChange,
|
|
106
132
|
agent,
|
|
133
|
+
share,
|
|
134
|
+
shareOrg,
|
|
107
135
|
buildShareUrl,
|
|
108
136
|
onSharingChanged,
|
|
109
137
|
modal = true,
|
|
@@ -143,11 +171,13 @@ export function ShareAgentDialog({
|
|
|
143
171
|
)}
|
|
144
172
|
aria-labelledby="share-agent-title"
|
|
145
173
|
>
|
|
146
|
-
{/* Body mounts only while open so
|
|
147
|
-
|
|
174
|
+
{/* Body mounts only while open so its draft state resets per
|
|
175
|
+
session — reopening the dialog never shows a stale draft. */}
|
|
148
176
|
{open && (
|
|
149
177
|
<ShareAgentDialogBody
|
|
150
178
|
agent={agent}
|
|
179
|
+
share={share ?? null}
|
|
180
|
+
shareOrg={shareOrg}
|
|
151
181
|
buildShareUrl={buildShareUrl}
|
|
152
182
|
onSharingChanged={onSharingChanged}
|
|
153
183
|
onClose={handleClose}
|
|
@@ -158,27 +188,39 @@ export function ShareAgentDialog({
|
|
|
158
188
|
}
|
|
159
189
|
|
|
160
190
|
// ---------------------------------------------------------------------------
|
|
161
|
-
// Dialog body —
|
|
191
|
+
// Dialog body — create step or editor, depending on the share it was given
|
|
162
192
|
// ---------------------------------------------------------------------------
|
|
163
193
|
|
|
164
194
|
/**
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
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.
|
|
169
201
|
*/
|
|
170
202
|
function ShareAgentDialogBody({
|
|
171
203
|
agent,
|
|
204
|
+
share,
|
|
205
|
+
shareOrg,
|
|
172
206
|
buildShareUrl,
|
|
173
207
|
onSharingChanged,
|
|
174
208
|
onClose,
|
|
175
209
|
}: {
|
|
176
210
|
readonly agent: Agent;
|
|
211
|
+
readonly share: AgentShare | null;
|
|
212
|
+
readonly shareOrg?: string;
|
|
177
213
|
readonly buildShareUrl?: (org: string, slug: string) => string;
|
|
178
214
|
readonly onSharingChanged?: () => void;
|
|
179
215
|
readonly onClose: () => void;
|
|
180
216
|
}) {
|
|
181
|
-
const
|
|
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 ?? "");
|
|
182
224
|
|
|
183
225
|
return (
|
|
184
226
|
<div className="flex flex-col">
|
|
@@ -189,10 +231,14 @@ function ShareAgentDialogBody({
|
|
|
189
231
|
id="share-agent-title"
|
|
190
232
|
className="text-base font-semibold text-foreground"
|
|
191
233
|
>
|
|
192
|
-
Share
|
|
234
|
+
{isCreating ? "Create share" : "Share"}
|
|
193
235
|
</h2>
|
|
194
236
|
<p className="mt-0.5 truncate text-xs text-muted-foreground">
|
|
195
|
-
{agent
|
|
237
|
+
{/* Cross-org: qualify the agent so it's clear whose blueprint
|
|
238
|
+
this channel serves — the URL and billing are still yours. */}
|
|
239
|
+
{isCrossOrg
|
|
240
|
+
? `${agent.metadata?.org}/${agent.metadata?.slug}`
|
|
241
|
+
: agent.metadata?.name || agent.metadata?.slug}
|
|
196
242
|
</p>
|
|
197
243
|
</div>
|
|
198
244
|
<button
|
|
@@ -209,39 +255,19 @@ function ShareAgentDialogBody({
|
|
|
209
255
|
</button>
|
|
210
256
|
</div>
|
|
211
257
|
|
|
212
|
-
{
|
|
213
|
-
<
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
) : error ? (
|
|
222
|
-
<div className="px-6 py-8 text-center" role="alert">
|
|
223
|
-
<p className="text-sm text-foreground">
|
|
224
|
-
Couldn't load this agent's sharing settings.
|
|
225
|
-
</p>
|
|
226
|
-
<p className="mt-1 text-xs text-muted-foreground">
|
|
227
|
-
{getUserMessage(error)}
|
|
228
|
-
</p>
|
|
229
|
-
<button
|
|
230
|
-
type="button"
|
|
231
|
-
onClick={refetch}
|
|
232
|
-
className={cn(
|
|
233
|
-
"mt-3 rounded-md px-3 py-1.5 text-xs font-medium",
|
|
234
|
-
"border border-border text-foreground hover:bg-accent-hover",
|
|
235
|
-
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
236
|
-
)}
|
|
237
|
-
>
|
|
238
|
-
Try again
|
|
239
|
-
</button>
|
|
240
|
-
</div>
|
|
258
|
+
{isCreating ? (
|
|
259
|
+
<CreateShareForm
|
|
260
|
+
agent={agent}
|
|
261
|
+
shareOrg={resolvedShareOrg}
|
|
262
|
+
onCreated={(persisted) => {
|
|
263
|
+
setCreated(persisted);
|
|
264
|
+
onSharingChanged?.();
|
|
265
|
+
}}
|
|
266
|
+
/>
|
|
241
267
|
) : (
|
|
242
268
|
<ShareAgentForm
|
|
243
269
|
agent={agent}
|
|
244
|
-
initialShare={
|
|
270
|
+
initialShare={activeShare}
|
|
245
271
|
buildShareUrl={buildShareUrl}
|
|
246
272
|
onSharingChanged={onSharingChanged}
|
|
247
273
|
/>
|
|
@@ -254,11 +280,13 @@ function ShareAgentDialogBody({
|
|
|
254
280
|
onClick={onClose}
|
|
255
281
|
className={cn(
|
|
256
282
|
"rounded-md px-3 py-1.5 text-sm font-medium",
|
|
257
|
-
|
|
283
|
+
isCreating
|
|
284
|
+
? "border border-border text-foreground hover:bg-accent-hover"
|
|
285
|
+
: "bg-primary text-primary-foreground hover:bg-primary-hover",
|
|
258
286
|
"focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
|
259
287
|
)}
|
|
260
288
|
>
|
|
261
|
-
Done
|
|
289
|
+
{isCreating ? "Cancel" : "Done"}
|
|
262
290
|
</button>
|
|
263
291
|
</div>
|
|
264
292
|
</div>
|
|
@@ -266,33 +294,231 @@ function ShareAgentDialogBody({
|
|
|
266
294
|
}
|
|
267
295
|
|
|
268
296
|
// ---------------------------------------------------------------------------
|
|
269
|
-
//
|
|
297
|
+
// Create step — the new share's identity and cost, before any channel exists
|
|
270
298
|
// ---------------------------------------------------------------------------
|
|
271
299
|
|
|
272
300
|
/**
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
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.
|
|
277
311
|
*/
|
|
278
|
-
function
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
+
}
|
|
288
398
|
},
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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>'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's address:{" "}
|
|
486
|
+
<code className="font-mono">
|
|
487
|
+
/chat/{shareOrg}/{slug || "…"}
|
|
488
|
+
</code>
|
|
489
|
+
. Can'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
|
+
);
|
|
294
516
|
}
|
|
295
517
|
|
|
518
|
+
// ---------------------------------------------------------------------------
|
|
519
|
+
// Form — owns the share draft for the session
|
|
520
|
+
// ---------------------------------------------------------------------------
|
|
521
|
+
|
|
296
522
|
function ShareAgentForm({
|
|
297
523
|
agent,
|
|
298
524
|
initialShare,
|
|
@@ -300,16 +526,15 @@ function ShareAgentForm({
|
|
|
300
526
|
onSharingChanged,
|
|
301
527
|
}: {
|
|
302
528
|
readonly agent: Agent;
|
|
303
|
-
readonly initialShare: AgentShare
|
|
529
|
+
readonly initialShare: AgentShare;
|
|
304
530
|
readonly buildShareUrl?: (org: string, slug: string) => string;
|
|
305
531
|
readonly onSharingChanged?: () => void;
|
|
306
532
|
}) {
|
|
307
533
|
const agentName = agent.metadata?.name || (agent.metadata?.slug ?? "");
|
|
308
534
|
|
|
309
535
|
// The latest server share is the single baseline: the draft, the link
|
|
310
|
-
// token, and the share id for rotation all derive from it.
|
|
311
|
-
|
|
312
|
-
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);
|
|
313
538
|
const [draft, setDraft] = useState<AgentShareDraft>(() =>
|
|
314
539
|
draftFromShare(initialShare),
|
|
315
540
|
);
|
|
@@ -317,15 +542,18 @@ function ShareAgentForm({
|
|
|
317
542
|
|
|
318
543
|
const { save, isPending } = useSaveAgentShare(agent);
|
|
319
544
|
const { rotateShareLink, isPending: isRotating } = useRotateShareLink(
|
|
320
|
-
share
|
|
545
|
+
share.metadata?.id ?? null,
|
|
321
546
|
);
|
|
322
547
|
|
|
323
|
-
// The share's own org/slug
|
|
324
|
-
//
|
|
325
|
-
|
|
326
|
-
const
|
|
327
|
-
const
|
|
328
|
-
|
|
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 ?? "";
|
|
553
|
+
// Cross-org shares are public-audience only (decision 013 D3) — the
|
|
554
|
+
// audience selector disappears rather than offering a choice the
|
|
555
|
+
// server would refuse.
|
|
556
|
+
const isCrossOrg = org !== (agent.metadata?.org ?? "");
|
|
329
557
|
|
|
330
558
|
// Single commit path: apply the complete draft, adopt the server's
|
|
331
559
|
// returned share as the new baseline, notify the host.
|
|
@@ -431,11 +659,13 @@ function ShareAgentForm({
|
|
|
431
659
|
aria-labelledby="share-enabled-label"
|
|
432
660
|
/>
|
|
433
661
|
</div>
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
662
|
+
{!isCrossOrg && (
|
|
663
|
+
<AudienceSelector
|
|
664
|
+
audience={draft.audience}
|
|
665
|
+
onChange={handleAudienceChange}
|
|
666
|
+
disabled={isPending}
|
|
667
|
+
/>
|
|
668
|
+
)}
|
|
439
669
|
<ToolReadinessHint agent={agent} draft={draft} />
|
|
440
670
|
</div>
|
|
441
671
|
|