@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
@@ -0,0 +1,502 @@
1
+ "use client";
2
+
3
+ import { useCallback, useState } from "react";
4
+ import { cn } from "@stigmer/theme";
5
+ import {
6
+ appendLinkToken,
7
+ chatPath,
8
+ getUserMessage,
9
+ } from "@stigmer/sdk";
10
+ import type { Agent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
11
+ import type { AgentShare } from "@stigmer/protos/ai/stigmer/agentic/agentshare/v1/api_pb";
12
+ import { toast } from "../feedback/toast.js";
13
+ import { Button } from "../button/Button.js";
14
+ import { EmptyState } from "../empty-state/EmptyState.js";
15
+ import { PermissionGate } from "../iam-policy/PermissionGate.js";
16
+ import { ConfirmDialog } from "../resource-detail/ConfirmDialog.js";
17
+ import { useConfirmAction } from "../resource-detail/useConfirmAction.js";
18
+ import { useCopyResource } from "../resource-detail/useCopyResource.js";
19
+ import { ShareAgentDialog } from "./ShareAgentDialog.js";
20
+ import { useAgentShares } from "./useAgentShares.js";
21
+ import { useCanCreateAgentShare } from "./useCanCreateAgentShare.js";
22
+ import { useDeleteAgentShare } from "./useDeleteAgentShare.js";
23
+ import { useRotateShareLink } from "./useRotateShareLink.js";
24
+ import {
25
+ draftFromShare,
26
+ sharingAudienceFromProto,
27
+ useSaveAgentShare,
28
+ } from "./useSaveAgentShare.js";
29
+
30
+ /** Props for {@link AgentShareList}. */
31
+ export interface AgentShareListProps {
32
+ /** The agent whose shares are managed. */
33
+ readonly agent: Agent;
34
+ /**
35
+ * The viewer's active organization slug. Scopes the list to this
36
+ * org's channels of the agent, and a share created from this list
37
+ * lands in this org — its URL, billing, and credentials belong to it
38
+ * (a **cross-org share** when it differs from the agent's org,
39
+ * decision 013). Omit to default to the agent's own org.
40
+ */
41
+ readonly viewerOrg?: string;
42
+ /**
43
+ * Builds the absolute public chat URL for a share. The host
44
+ * application owns URL construction (its configured public origin may
45
+ * differ from the rendering origin — e.g. the desktop app). When
46
+ * omitted, links fall back to the relative `/chat/<org>/<slug>`.
47
+ */
48
+ readonly buildShareUrl?: (org: string, slug: string) => string;
49
+ /** Additional CSS class names. */
50
+ readonly className?: string;
51
+ }
52
+
53
+ /**
54
+ * Management surface for an agent's {@link AgentShare} channels — the
55
+ * agent analog of {@link AgentInstanceList}, rendered in the agent
56
+ * detail view's Shares tab.
57
+ *
58
+ * Lists the viewer's active org's channels of the agent: `viewerOrg`
59
+ * scopes the `getByAgent` call server-side, so a member of several orgs
60
+ * sees exactly the current org context's channels — never a merged list
61
+ * of every org's (decision 013 amendment). Per-row actions: copy link,
62
+ * edit, pause/resume, reset link, delete. Creation goes through the same
63
+ * {@link ShareAgentDialog} and always lands in the viewer's active org;
64
+ * the create bar mirrors the server's via
65
+ * {@link useCanCreateAgentShare}, so the button never appears to a user
66
+ * whose create would be refused.
67
+ *
68
+ * Self-contained: owns its dialog, its confirmation prompts, and its
69
+ * refetch-after-mutation — hosts render it with just the agent and the
70
+ * URL builder.
71
+ *
72
+ * This is an SDK component (DD-001) — embeddable by platform builders.
73
+ */
74
+ export function AgentShareList({
75
+ agent,
76
+ viewerOrg,
77
+ buildShareUrl,
78
+ className,
79
+ }: AgentShareListProps) {
80
+ const agentId = agent.metadata?.id ?? "";
81
+ // Scope the list to the org whose context the viewer is in; when the
82
+ // host passes no viewerOrg the scope falls back to the agent's own org
83
+ // (the same-org owner flow), matching where creation would land.
84
+ const { shares, isLoading, error, refetch } = useAgentShares(
85
+ agentId,
86
+ viewerOrg || (agent.metadata?.org ?? ""),
87
+ );
88
+ const { allowed: canCreate, shareOrg } = useCanCreateAgentShare(
89
+ agent,
90
+ viewerOrg,
91
+ );
92
+ const { deleteShare } = useDeleteAgentShare();
93
+ const { confirmState, confirm, handleConfirm, handleCancel } =
94
+ useConfirmAction();
95
+
96
+ // One dialog instance serves both flows: editing the chosen share, or
97
+ // creating a new one in the viewer's org.
98
+ const [editor, setEditor] = useState<
99
+ | { readonly mode: "create" }
100
+ | { readonly mode: "edit"; readonly share: AgentShare }
101
+ | null
102
+ >(null);
103
+
104
+ const handleDelete = useCallback(
105
+ async (share: AgentShare) => {
106
+ const slug = share.metadata?.slug ?? "";
107
+ const confirmed = await confirm({
108
+ title: "Delete share?",
109
+ description:
110
+ `The link /chat/${share.metadata?.org}/${slug} stops working ` +
111
+ "immediately — including for visitors mid-conversation — and its " +
112
+ "configuration (origins, messages, credential bindings) is gone. " +
113
+ "To stop serving while keeping the configuration, pause it instead.",
114
+ confirmLabel: "Delete",
115
+ variant: "destructive",
116
+ });
117
+ if (!confirmed) return;
118
+ try {
119
+ await deleteShare(share.metadata?.id ?? "");
120
+ toast.success("Share deleted");
121
+ refetch();
122
+ } catch (err) {
123
+ toast.error(getUserMessage(err));
124
+ }
125
+ },
126
+ [confirm, deleteShare, refetch],
127
+ );
128
+
129
+ if (isLoading) {
130
+ return <LoadingSkeleton />;
131
+ }
132
+
133
+ if (error) {
134
+ return (
135
+ <div className="py-8 text-center text-sm text-destructive">
136
+ Failed to load shares
137
+ </div>
138
+ );
139
+ }
140
+
141
+ return (
142
+ <div className={cn("space-y-3", className)}>
143
+ {shares.length === 0 ? (
144
+ <ShareEmptyState
145
+ canCreate={canCreate}
146
+ onCreateClick={() => setEditor({ mode: "create" })}
147
+ />
148
+ ) : (
149
+ <>
150
+ <div className="flex items-center justify-between">
151
+ <h3 className="text-sm font-medium text-foreground">
152
+ {shares.length} {shares.length === 1 ? "share" : "shares"}
153
+ </h3>
154
+ {canCreate && (
155
+ <Button
156
+ variant="outline"
157
+ size="xs"
158
+ icon={<PlusIcon />}
159
+ onClick={() => setEditor({ mode: "create" })}
160
+ >
161
+ Create share
162
+ </Button>
163
+ )}
164
+ </div>
165
+
166
+ <div className="overflow-hidden rounded-lg border border-border">
167
+ <table className="w-full text-sm">
168
+ <thead>
169
+ <tr className="border-b border-border bg-muted-subtle">
170
+ <th className="px-4 py-2 text-left font-medium text-muted-foreground">Name</th>
171
+ <th className="px-4 py-2 text-left font-medium text-muted-foreground">Link</th>
172
+ <th className="px-4 py-2 text-left font-medium text-muted-foreground">Audience</th>
173
+ <th className="px-4 py-2 text-left font-medium text-muted-foreground">Status</th>
174
+ <th className="px-4 py-2 text-right font-medium text-muted-foreground">Actions</th>
175
+ </tr>
176
+ </thead>
177
+ <tbody className="divide-y divide-border">
178
+ {shares.map((share) => (
179
+ <ShareRow
180
+ key={share.metadata?.id}
181
+ share={share}
182
+ agent={agent}
183
+ buildShareUrl={buildShareUrl}
184
+ onEditClick={(s) => setEditor({ mode: "edit", share: s })}
185
+ onDeleteClick={handleDelete}
186
+ refetch={refetch}
187
+ />
188
+ ))}
189
+ </tbody>
190
+ </table>
191
+ </div>
192
+ </>
193
+ )}
194
+
195
+ {editor && (
196
+ <ShareAgentDialog
197
+ open
198
+ onOpenChange={(open) => {
199
+ if (!open) setEditor(null);
200
+ }}
201
+ agent={agent}
202
+ share={editor.mode === "edit" ? editor.share : undefined}
203
+ shareOrg={shareOrg}
204
+ buildShareUrl={buildShareUrl}
205
+ onSharingChanged={refetch}
206
+ />
207
+ )}
208
+
209
+ <ConfirmDialog
210
+ state={confirmState}
211
+ onConfirm={handleConfirm}
212
+ onCancel={handleCancel}
213
+ />
214
+ </div>
215
+ );
216
+ }
217
+
218
+ // ---------------------------------------------------------------------------
219
+ // Row — one share channel
220
+ // ---------------------------------------------------------------------------
221
+
222
+ interface ShareRowProps {
223
+ readonly share: AgentShare;
224
+ readonly agent: Agent;
225
+ readonly buildShareUrl?: (org: string, slug: string) => string;
226
+ readonly onEditClick: (share: AgentShare) => void;
227
+ readonly onDeleteClick: (share: AgentShare) => void;
228
+ readonly refetch: () => void;
229
+ }
230
+
231
+ function ShareRow({
232
+ share,
233
+ agent,
234
+ buildShareUrl,
235
+ onEditClick,
236
+ onDeleteClick,
237
+ refetch,
238
+ }: ShareRowProps) {
239
+ const meta = share.metadata;
240
+ const id = meta?.id ?? "";
241
+ const org = meta?.org ?? "";
242
+ const slug = meta?.slug ?? "";
243
+ const enabled = share.spec?.enabled ?? false;
244
+ const audience = sharingAudienceFromProto(share.spec?.audience);
245
+ const isCrossOrg = org !== (agent.metadata?.org ?? "");
246
+
247
+ const { copy } = useCopyResource();
248
+ const { save, isPending } = useSaveAgentShare(agent);
249
+ const { rotateShareLink, isPending: isRotating } = useRotateShareLink(id);
250
+
251
+ // The copyable URL carries the link token on public shares (org
252
+ // audience is gated by membership, not the token). The display stays
253
+ // the bare path — the token is a secret, not an address.
254
+ const displayPath = chatPath(org, slug);
255
+ const copyUrl = (() => {
256
+ const base = buildShareUrl ? buildShareUrl(org, slug) : displayPath;
257
+ return audience === "org"
258
+ ? base
259
+ : appendLinkToken(base, share.status?.shareLinkToken ?? "");
260
+ })();
261
+
262
+ const handleCopyLink = useCallback(() => {
263
+ void copy(copyUrl, "Link");
264
+ }, [copy, copyUrl]);
265
+
266
+ // Pause/resume is a full-spec save with only `enabled` flipped —
267
+ // draftFromShare guarantees a toggle can never wipe origins, messages,
268
+ // or credential bindings (the fails-closed posture the CLI shares).
269
+ const handleToggleEnabled = useCallback(async () => {
270
+ try {
271
+ await save({ ...draftFromShare(share), enabled: !enabled }, share);
272
+ toast.success(enabled ? "Share paused" : "Share resumed");
273
+ refetch();
274
+ } catch (err) {
275
+ toast.error(getUserMessage(err));
276
+ }
277
+ }, [save, share, enabled, refetch]);
278
+
279
+ const handleResetLink = useCallback(async () => {
280
+ try {
281
+ await rotateShareLink();
282
+ toast.success("Link reset — the old link no longer works");
283
+ refetch();
284
+ } catch (err) {
285
+ toast.error(getUserMessage(err));
286
+ }
287
+ }, [rotateShareLink, refetch]);
288
+
289
+ return (
290
+ <tr
291
+ className="cursor-pointer transition-colors hover:bg-accent-hover"
292
+ onClick={() => onEditClick(share)}
293
+ >
294
+ <td className="px-4 py-2.5">
295
+ <div className="flex items-center gap-2">
296
+ <span className="font-medium text-foreground">
297
+ {meta?.name || slug || "\u2014"}
298
+ </span>
299
+ {isCrossOrg && (
300
+ <span
301
+ className={cn(
302
+ "inline-flex items-center rounded-md px-1.5 py-0.5",
303
+ "text-[0.6rem] font-medium uppercase tracking-wide",
304
+ "bg-muted text-muted-foreground border border-border",
305
+ )}
306
+ title={`This share lives in ${org}; the agent lives in ${agent.metadata?.org}`}
307
+ >
308
+ Cross-org
309
+ </span>
310
+ )}
311
+ </div>
312
+ </td>
313
+
314
+ <td className="px-4 py-2.5" onClick={(e) => e.stopPropagation()}>
315
+ <div className="flex items-center gap-1.5">
316
+ <code
317
+ className="max-w-56 truncate font-mono text-xs text-muted-foreground"
318
+ title={displayPath}
319
+ >
320
+ {displayPath}
321
+ </code>
322
+ <button
323
+ type="button"
324
+ onClick={handleCopyLink}
325
+ aria-label={`Copy link for ${meta?.name || slug}`}
326
+ className={cn(
327
+ "rounded p-1 text-muted-foreground",
328
+ "hover:bg-accent-hover hover:text-foreground",
329
+ "focus:outline-none focus:ring-1 focus:ring-ring",
330
+ )}
331
+ >
332
+ <CopyIcon />
333
+ </button>
334
+ </div>
335
+ </td>
336
+
337
+ <td className="px-4 py-2.5">
338
+ <span className="text-xs text-muted-foreground">
339
+ {audience === "org" ? "Org members" : "Public"}
340
+ </span>
341
+ </td>
342
+
343
+ <td className="px-4 py-2.5">
344
+ <span
345
+ className={cn(
346
+ "inline-flex items-center gap-1.5 text-xs",
347
+ enabled ? "text-foreground" : "text-muted-foreground",
348
+ )}
349
+ >
350
+ <span
351
+ aria-hidden="true"
352
+ className={cn(
353
+ "size-1.5 rounded-full",
354
+ enabled ? "bg-success" : "bg-muted-foreground",
355
+ )}
356
+ />
357
+ {enabled ? "Active" : "Paused"}
358
+ </span>
359
+ </td>
360
+
361
+ <td className="px-4 py-2.5 text-right" onClick={(e) => e.stopPropagation()}>
362
+ <div className="flex items-center justify-end gap-1">
363
+ <PermissionGate resource={{ kind: "agent_share", id }} relation="can_edit">
364
+ <button
365
+ type="button"
366
+ onClick={() => onEditClick(share)}
367
+ className={cn(
368
+ "rounded px-2 py-1 text-xs font-medium",
369
+ "text-foreground hover:bg-accent-hover",
370
+ "focus:outline-none focus:ring-1 focus:ring-ring",
371
+ )}
372
+ >
373
+ Edit
374
+ </button>
375
+ <button
376
+ type="button"
377
+ onClick={() => void handleToggleEnabled()}
378
+ disabled={isPending}
379
+ className={cn(
380
+ "rounded px-2 py-1 text-xs font-medium",
381
+ "text-foreground hover:bg-accent-hover",
382
+ "focus:outline-none focus:ring-1 focus:ring-ring",
383
+ "disabled:pointer-events-none disabled:opacity-50",
384
+ )}
385
+ >
386
+ {enabled ? "Pause" : "Resume"}
387
+ </button>
388
+ {audience === "public" && (
389
+ <button
390
+ type="button"
391
+ onClick={() => void handleResetLink()}
392
+ disabled={isRotating}
393
+ className={cn(
394
+ "rounded px-2 py-1 text-xs font-medium",
395
+ "text-foreground hover:bg-accent-hover",
396
+ "focus:outline-none focus:ring-1 focus:ring-ring",
397
+ "disabled:pointer-events-none disabled:opacity-50",
398
+ )}
399
+ >
400
+ {isRotating ? "Resetting…" : "Reset link"}
401
+ </button>
402
+ )}
403
+ </PermissionGate>
404
+ <PermissionGate resource={{ kind: "agent_share", id }} relation="can_delete">
405
+ <button
406
+ type="button"
407
+ onClick={() => onDeleteClick(share)}
408
+ className={cn(
409
+ "rounded px-2 py-1 text-xs font-medium",
410
+ "text-destructive hover:bg-destructive-subtle",
411
+ "focus:outline-none focus:ring-1 focus:ring-ring",
412
+ )}
413
+ >
414
+ Delete
415
+ </button>
416
+ </PermissionGate>
417
+ </div>
418
+ </td>
419
+ </tr>
420
+ );
421
+ }
422
+
423
+ // ---------------------------------------------------------------------------
424
+ // Empty state
425
+ // ---------------------------------------------------------------------------
426
+
427
+ function ShareEmptyState({
428
+ canCreate,
429
+ onCreateClick,
430
+ }: {
431
+ readonly canCreate: boolean;
432
+ readonly onCreateClick: () => void;
433
+ }) {
434
+ return (
435
+ <EmptyState
436
+ variant="first-use"
437
+ icon={<ShareIcon className="size-10" />}
438
+ title="No shares yet"
439
+ description={
440
+ "A share gives this agent a hosted chat link you can send or embed — " +
441
+ "each share is its own channel with its own audience, embed origins, " +
442
+ "and tool credentials."
443
+ }
444
+ action={
445
+ canCreate
446
+ ? { label: "Create share", onClick: onCreateClick, icon: <PlusIcon /> }
447
+ : undefined
448
+ }
449
+ />
450
+ );
451
+ }
452
+
453
+ // ---------------------------------------------------------------------------
454
+ // Non-happy states + icons
455
+ // ---------------------------------------------------------------------------
456
+
457
+ function LoadingSkeleton() {
458
+ return (
459
+ <div className="space-y-2 py-4">
460
+ {[1, 2, 3].map((i) => (
461
+ <div key={i} className="h-12 animate-pulse rounded-md bg-muted-faint" />
462
+ ))}
463
+ </div>
464
+ );
465
+ }
466
+
467
+ function PlusIcon() {
468
+ return (
469
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true">
470
+ <path d="M6 2v8M2 6h8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
471
+ </svg>
472
+ );
473
+ }
474
+
475
+ function CopyIcon() {
476
+ return (
477
+ <svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
478
+ <rect x="5.5" y="5.5" width="8" height="8" rx="1.5" />
479
+ <path d="M10.5 5.5v-2a1 1 0 0 0-1-1h-6a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2" />
480
+ </svg>
481
+ );
482
+ }
483
+
484
+ function ShareIcon({ className }: { readonly className?: string }) {
485
+ return (
486
+ <svg
487
+ className={className}
488
+ viewBox="0 0 16 16"
489
+ fill="none"
490
+ stroke="currentColor"
491
+ strokeWidth="1.5"
492
+ strokeLinecap="round"
493
+ strokeLinejoin="round"
494
+ aria-hidden="true"
495
+ >
496
+ <circle cx="4" cy="8" r="2" />
497
+ <circle cx="12" cy="4" r="2" />
498
+ <circle cx="12" cy="12" r="2" />
499
+ <path d="m5.8 7.1 4.4-2.2M5.8 8.9l4.4 2.2" />
500
+ </svg>
501
+ );
502
+ }