@agent-native/core 0.153.0 → 0.153.2

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 (38) hide show
  1. package/corpus/templates/analytics/.agents/skills/adhoc-analysis/SKILL.md +21 -19
  2. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +3 -2
  3. package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +3 -2
  4. package/corpus/templates/calendar/app/lib/event-layout.ts +16 -35
  5. package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +27 -55
  6. package/corpus/templates/clips/app/components/player/share-dialog.tsx +104 -66
  7. package/corpus/templates/clips/app/components/sharing/share-ui.tsx +45 -43
  8. package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +0 -1
  9. package/corpus/templates/clips/desktop/src/app.tsx +818 -442
  10. package/corpus/templates/clips/desktop/src/styles.css +244 -2
  11. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +2 -2
  12. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +1 -0
  13. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +584 -1
  14. package/corpus/templates/clips/desktop/src-tauri/src/rewind_clip.rs +25 -0
  15. package/corpus/templates/clips/desktop/src-tauri/src/util.rs +1 -1
  16. package/corpus/templates/factory/app/routes/factory.tsx +0 -3
  17. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +1 -1
  18. package/corpus/templates/slides/app/components/editor/ShareDialog.tsx +28 -25
  19. package/corpus/templates/slides/app/components/editor/SlideContextToolbar.tsx +50 -0
  20. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +12 -60
  21. package/dist/agent/production-agent.d.ts +8 -0
  22. package/dist/agent/production-agent.js +25 -2
  23. package/dist/agent/run-manager.js +13 -3
  24. package/dist/client/AgentPanel.js +6 -4
  25. package/dist/client/org/WorkspaceNotice.js +7 -4
  26. package/dist/client/sharing/ShareButton.d.ts +2 -2
  27. package/dist/client/sharing/ShareButton.js +18 -13
  28. package/dist/client/sharing/ShareDialog.d.ts +1 -1
  29. package/dist/client/sharing/ShareDialog.js +16 -11
  30. package/dist/collab/awareness.d.ts +2 -2
  31. package/dist/collab/struct-routes.d.ts +1 -1
  32. package/dist/resources/handlers.d.ts +1 -1
  33. package/dist/server/auth.js +18 -4
  34. package/dist/server/better-auth-instance.js +8 -1
  35. package/dist/server/magic-link-attribution.d.ts +21 -0
  36. package/dist/server/magic-link-attribution.js +115 -0
  37. package/dist/server/realtime-token.d.ts +1 -1
  38. package/package.json +1 -1
@@ -101,25 +101,27 @@ Don't just dump raw data. Synthesize findings:
101
101
 
102
102
  ### Step 4: Generate Charts (when useful)
103
103
 
104
- When the analysis benefits from a visual — trends over time, distributions, comparisons between categories — call `generate-chart` before formatting the report. The action returns a `url` you embed directly in the markdown.
105
-
106
- ```
107
- generate-chart
108
- --title "Closed-lost deals by month"
109
- --type bar
110
- --labels '["Jan","Feb","Mar"]'
111
- --data '[18, 22, 14]'
112
- ```
113
-
114
- Embed the returned URL in `resultMarkdown` using standard markdown image syntax:
115
-
116
- ```markdown
117
- ![Closed-lost deals by month](/api/media/closed-lost-deals-by-month-1234567890.png?v=1234567890)
118
- ```
119
-
120
- You can include multiple charts in one analysis. Reach for a chart when it communicates the finding faster than a table — don't force visuals on every analysis.
121
-
122
- Include the re-generation step in your saved `instructions` so re-runs produce fresh charts.
104
+ When the analysis benefits from a visual — trends over time, distributions, or
105
+ comparisons between categories — query the data first, then use the live
106
+ `/chart` embed described in `data-querying` for an in-chat answer. Do not call
107
+ `generate-chart` for a one-off chat result: it produces a static image for
108
+ saved artifacts, requires pre-stringified JSON, and does not count as a real
109
+ data query for the final response guard.
110
+
111
+ For a stacked multi-series bar chart, keep the query in long form and emit a
112
+ panel with `chartType: "bar"`, `config.pivot` containing `xKey`, `seriesKey`,
113
+ and `valueKey`, plus `config.stacked: true`. The live chart route pivots the
114
+ rows and renders one stack per x-axis category. See `data-querying`'s
115
+ "Inline Charts In Chat" section for the exact `embed` fence and encoding.
116
+
117
+ Only use `generate-chart` when a saved analysis artifact explicitly needs a
118
+ static image. If it returns an error while answering in chat, switch to the
119
+ live embed instead of retrying reformatted `labels` or `data` parameters.
120
+
121
+ You can include multiple live charts in one analysis. Reach for a chart when it
122
+ communicates the finding faster than a table - don't force visuals on every
123
+ analysis. Include the query and embed configuration in saved `instructions` so
124
+ re-runs produce fresh charts.
123
125
 
124
126
  ### Step 5: Format Results as Markdown
125
127
 
@@ -232,6 +232,7 @@ const DayEventCard = memo(function DayEventCard({
232
232
  const li = layout.get(event.id) ?? {
233
233
  left: 0,
234
234
  width: 100,
235
+ indent: 0,
235
236
  col: 0,
236
237
  totalCols: 1,
237
238
  stackOrder: 0,
@@ -304,8 +305,8 @@ const DayEventCard = memo(function DayEventCard({
304
305
  }
305
306
  style={{
306
307
  ...posStyle,
307
- left: `calc(${li.left}% + ${li.col > 0 ? 2 : 0}px)`,
308
- width: `calc(${li.width}% - ${li.col > 0 ? 4 : 2}px)`,
308
+ left: `calc(${li.left}% + ${li.indent}px)`,
309
+ width: `calc(${li.width}% - ${li.indent * 2 + 2}px)`,
309
310
  zIndex:
310
311
  isBeingDragged && isDragging
311
312
  ? 100
@@ -274,6 +274,7 @@ const WeekEventCard = memo(function WeekEventCard({
274
274
  const li = layout.get(event.id) ?? {
275
275
  left: 0,
276
276
  width: 100,
277
+ indent: 0,
277
278
  col: 0,
278
279
  totalCols: 1,
279
280
  stackOrder: 0,
@@ -372,8 +373,8 @@ const WeekEventCard = memo(function WeekEventCard({
372
373
  }
373
374
  style={{
374
375
  ...style,
375
- left: `calc(${li.left}% + ${li.col > 0 ? 2 : 0}px)`,
376
- width: `calc(${li.width}% - ${li.col > 0 ? 4 : 2}px)`,
376
+ left: `calc(${li.left}% + ${li.indent}px)`,
377
+ width: `calc(${li.width}% - ${li.indent * 2 + 2}px)`,
377
378
  zIndex:
378
379
  isBeingDragged && isDragging
379
380
  ? 100
@@ -4,6 +4,7 @@ import { addDays, parseISO, startOfDay } from "date-fns";
4
4
  export interface TimedEventLayout {
5
5
  left: number;
6
6
  width: number;
7
+ indent: number;
7
8
  col: number;
8
9
  totalCols: number;
9
10
  stackOrder: number;
@@ -20,18 +21,16 @@ interface EventEntry {
20
21
  inputOrder: number;
21
22
  }
22
23
 
23
- const TEXT_REGION_MINUTES = 45;
24
24
  const MIN_VISIBLE_EVENT_MINUTES = 15;
25
+ const OVERLAP_INDENT_PX = 16;
25
26
 
26
27
  function overlaps(a: EventBounds, b: EventBounds): boolean {
27
28
  return a.start < b.end && b.start < a.end;
28
29
  }
29
30
 
30
31
  /**
31
- * Pack timed events into columns while reserving horizontal space only when
32
- * the event labels can collide. An event may span unused columns when another
33
- * event is present there at a different time, which keeps long events readable
34
- * without wasting the column width needed for genuinely simultaneous labels.
32
+ * Pack timed events into shallow overlap layers. Later events stay wide enough
33
+ * to read while their inset exposes the boundary of the event underneath.
35
34
  */
36
35
  export function computeTimedEventLayout(
37
36
  dayEvents: readonly CalendarEvent[],
@@ -66,52 +65,34 @@ export function computeTimedEventLayout(
66
65
  return a.inputOrder - b.inputOrder;
67
66
  });
68
67
 
69
- const textOverlaps = (a: EventBounds, b: EventBounds): boolean => {
70
- const aTextEnd = Math.min(a.start + TEXT_REGION_MINUTES * 60 * 1000, a.end);
71
- const bTextEnd = Math.min(b.start + TEXT_REGION_MINUTES * 60 * 1000, b.end);
72
- return a.start < bTextEnd && b.start < aTextEnd;
73
- };
74
-
75
- // Place labels side by side only when their visible text regions overlap.
76
- const columns: EventEntry[][] = [];
68
+ // Put overlapping events into the first layer that is free at their start.
69
+ // Reusing finished layers keeps chained overlaps from creating empty gaps.
70
+ const overlapLayers: EventEntry[][] = [];
77
71
  const eventColumns = new Map<CalendarEvent, number>();
78
72
 
79
73
  for (const entry of sorted) {
80
- let column = columns.findIndex((columnEntries) =>
81
- columnEntries.every(
82
- (placed) => !textOverlaps(placed.bounds, entry.bounds),
83
- ),
74
+ let column = overlapLayers.findIndex((layerEntries) =>
75
+ layerEntries.every((placed) => !overlaps(placed.bounds, entry.bounds)),
84
76
  );
85
77
 
86
78
  if (column === -1) {
87
- column = columns.length;
88
- columns.push([]);
79
+ column = overlapLayers.length;
80
+ overlapLayers.push([]);
89
81
  }
90
82
 
91
- columns[column].push(entry);
83
+ overlapLayers[column].push(entry);
92
84
  eventColumns.set(entry.event, column);
93
85
  }
94
86
 
95
- const totalCols = columns.length;
87
+ const totalCols = overlapLayers.length;
96
88
 
97
89
  for (const [stackOrder, entry] of sorted.entries()) {
98
90
  const col = eventColumns.get(entry.event)!;
99
- let span = 1;
100
-
101
- // A column to the right is available when it has no event that overlaps
102
- // this card in time. Label-only collisions do not need to block expansion.
103
- for (let candidate = col + 1; candidate < totalCols; candidate++) {
104
- if (
105
- columns[candidate].some((other) => overlaps(other.bounds, entry.bounds))
106
- ) {
107
- break;
108
- }
109
- span++;
110
- }
111
91
 
112
92
  result.set(entry.event.id, {
113
- left: (col / totalCols) * 100,
114
- width: (span / totalCols) * 100,
93
+ left: 0,
94
+ width: 100,
95
+ indent: col * OVERLAP_INDENT_PX,
115
96
  col,
116
97
  totalCols,
117
98
  stackOrder,
@@ -4,7 +4,7 @@ import {
4
4
  useActionQuery,
5
5
  } from "@agent-native/core/client/hooks";
6
6
  import { useT } from "@agent-native/core/client/i18n";
7
- import { IconCheck, IconLink, IconMail } from "@tabler/icons-react";
7
+ import { IconLink, IconMail } from "@tabler/icons-react";
8
8
  import { useEffect, useMemo, useState, type ReactNode } from "react";
9
9
  import { toast } from "sonner";
10
10
 
@@ -12,7 +12,6 @@ import {
12
12
  CopyField,
13
13
  GeneralAccessSelect,
14
14
  MakePublicCard,
15
- ShareCardHeader,
16
15
  SharePeopleTab,
17
16
  copyToClipboard,
18
17
  useResourceVisibilityMutation,
@@ -30,7 +29,6 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
30
29
 
31
30
  export interface ShareMeetingPopoverProps {
32
31
  meetingId: string;
33
- meetingTitle?: string;
34
32
  shareTranscript: boolean;
35
33
  transcriptReady: boolean;
36
34
  children: ReactNode;
@@ -38,7 +36,6 @@ export interface ShareMeetingPopoverProps {
38
36
 
39
37
  export function ShareMeetingPopover({
40
38
  meetingId,
41
- meetingTitle,
42
39
  shareTranscript,
43
40
  transcriptReady,
44
41
  children,
@@ -52,7 +49,6 @@ export function ShareMeetingPopover({
52
49
  >
53
50
  <ShareMeetingContent
54
51
  meetingId={meetingId}
55
- meetingTitle={meetingTitle}
56
52
  shareTranscript={shareTranscript}
57
53
  transcriptReady={transcriptReady}
58
54
  />
@@ -63,12 +59,10 @@ export function ShareMeetingPopover({
63
59
 
64
60
  function ShareMeetingContent({
65
61
  meetingId,
66
- meetingTitle,
67
62
  shareTranscript,
68
63
  transcriptReady,
69
64
  }: {
70
65
  meetingId: string;
71
- meetingTitle?: string;
72
66
  shareTranscript: boolean;
73
67
  transcriptReady: boolean;
74
68
  }) {
@@ -85,14 +79,9 @@ function ShareMeetingContent({
85
79
 
86
80
  const data = sharesQuery.data;
87
81
  const canManage = data?.role === "owner" || data?.role === "admin";
88
- const titleText = meetingTitle
89
- ? t("clipsFinalRaw.shareNamedMeeting", { title: meetingTitle })
90
- : t("clipsFinalRaw.shareMeeting");
91
82
 
92
83
  return (
93
84
  <>
94
- <ShareCardHeader title={titleText} ownerEmail={data?.ownerEmail} />
95
-
96
85
  <Tabs defaultValue="link" className="min-w-0 px-4 py-3">
97
86
  <TabsList className="grid w-full grid-cols-2">
98
87
  <TabsTrigger value="link" className="gap-1.5">
@@ -192,57 +181,40 @@ function LinkTab({
192
181
  };
193
182
 
194
183
  return (
195
- <div className="space-y-4">
184
+ <div className="space-y-3">
196
185
  <GeneralAccessSelect
197
186
  visibility={visibility}
198
187
  canManage={canManage}
199
188
  isPending={isPending}
200
189
  onChange={(next) => setResourceVisibility(next)}
190
+ showDescription={false}
201
191
  />
202
192
 
203
- <div>
204
- <div className="mb-2 text-xs font-semibold">
205
- {t("shareMeeting.sharedContent")}
206
- </div>
207
- <div className="rounded-md border border-border">
208
- <div className="flex items-center gap-3 border-b border-border px-3 py-2.5">
209
- <span
210
- aria-hidden
211
- className="inline-flex size-5 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground"
212
- >
213
- <IconCheck size={12} strokeWidth={2.5} />
214
- </span>
215
- <span className="text-sm">{t("shareMeeting.summaryIncluded")}</span>
216
- </div>
217
- <div className="flex items-start justify-between gap-4 px-3 py-2.5">
218
- <div className="min-w-0">
219
- <label
220
- htmlFor={`meeting-share-transcript-${meetingId}`}
221
- className="text-sm font-medium"
222
- >
223
- {t("shareMeeting.includeTranscript")}
224
- </label>
225
- <p
226
- id={`meeting-share-transcript-description-${meetingId}`}
227
- className="mt-0.5 text-xs text-muted-foreground"
228
- >
229
- {transcriptReady
230
- ? t("shareMeeting.includeTranscriptDescription")
231
- : t("shareMeeting.transcriptUnavailable")}
232
- </p>
233
- </div>
234
- <Switch
235
- id={`meeting-share-transcript-${meetingId}`}
236
- checked={includeTranscript}
237
- onCheckedChange={handleTranscriptSharingChange}
238
- disabled={
239
- !canManage || !transcriptReady || updateMeeting.isPending
240
- }
241
- aria-describedby={`meeting-share-transcript-description-${meetingId}`}
242
- className="mt-0.5 shrink-0"
243
- />
244
- </div>
193
+ <div className="flex items-center justify-between gap-4 rounded-md border border-border px-3 py-2.5">
194
+ <div className="min-w-0">
195
+ <label
196
+ htmlFor={`meeting-share-transcript-${meetingId}`}
197
+ className="text-sm font-medium"
198
+ >
199
+ {t("shareMeeting.includeTranscript")}
200
+ </label>
201
+ <p
202
+ id={`meeting-share-transcript-description-${meetingId}`}
203
+ className="sr-only"
204
+ >
205
+ {transcriptReady
206
+ ? t("shareMeeting.includeTranscriptDescription")
207
+ : t("shareMeeting.transcriptUnavailable")}
208
+ </p>
245
209
  </div>
210
+ <Switch
211
+ id={`meeting-share-transcript-${meetingId}`}
212
+ checked={includeTranscript}
213
+ onCheckedChange={handleTranscriptSharingChange}
214
+ disabled={!canManage || !transcriptReady || updateMeeting.isPending}
215
+ aria-describedby={`meeting-share-transcript-description-${meetingId}`}
216
+ className="shrink-0"
217
+ />
246
218
  </div>
247
219
 
248
220
  <CopyField label={t("clipsFinalRaw.shareLink")} value={shareUrl} />
@@ -7,6 +7,7 @@ import {
7
7
  } from "@agent-native/core/client/hooks";
8
8
  import { useT } from "@agent-native/core/client/i18n";
9
9
  import {
10
+ IconChevronDown,
10
11
  IconCode,
11
12
  IconExternalLink,
12
13
  IconLink,
@@ -26,7 +27,6 @@ import {
26
27
  CopyField,
27
28
  GeneralAccessSelect,
28
29
  MakePublicCard,
29
- ShareCardHeader,
30
30
  SharePeopleTab,
31
31
  copyToClipboard,
32
32
  useResourceVisibilityMutation,
@@ -35,6 +35,11 @@ import {
35
35
  type Visibility,
36
36
  } from "@/components/sharing/share-ui";
37
37
  import { Button } from "@/components/ui/button";
38
+ import {
39
+ Collapsible,
40
+ CollapsibleContent,
41
+ CollapsibleTrigger,
42
+ } from "@/components/ui/collapsible";
38
43
  import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
39
44
  import { Input } from "@/components/ui/input";
40
45
  import { Label } from "@/components/ui/label";
@@ -45,6 +50,7 @@ import {
45
50
  } from "@/components/ui/popover";
46
51
  import { Switch } from "@/components/ui/switch";
47
52
  import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
53
+ import { cn } from "@/lib/utils";
48
54
 
49
55
  import { buildEmailPreviewMarkup } from "../../../shared/email-preview";
50
56
  import { isLoomEmbedUrl } from "../../../shared/loom";
@@ -222,19 +228,12 @@ function ShareRecordingContent({
222
228
  absoluteAppUrl(`/share/${recordingId}`),
223
229
  ownerViaId,
224
230
  );
225
- const titleText = recordingTitle
226
- ? t("shareDialog.shareTitle", { title: recordingTitle })
227
- : t("shareDialog.shareRecording");
228
-
229
231
  return (
230
232
  <>
231
- <ShareCardHeader
232
- title={titleText}
233
- ownerEmail={data?.ownerEmail}
234
- reserveCloseButton={reserveCloseButton}
235
- />
236
-
237
- <Tabs defaultValue="link" className="min-w-0 px-4 py-3">
233
+ <Tabs
234
+ defaultValue="link"
235
+ className={cn("min-w-0 px-4 py-3", reserveCloseButton && "pe-12")}
236
+ >
238
237
  <TabsList className="grid w-full grid-cols-3">
239
238
  <TabsTrigger value="link" className="gap-1.5">
240
239
  <IconLink size={14} />
@@ -434,6 +433,30 @@ function LinkTab({
434
433
  const agentPrompt = agentLink
435
434
  ? t("shareDialog.agentPrompt", { agentContextUrl: agentLink })
436
435
  : "";
436
+ const [agentDetailsOpen, setAgentDetailsOpen] = useState(false);
437
+ const showMakePublic = sharesLoaded && !isPublic && canManage;
438
+ const downloadButton = videoUrl ? (
439
+ <Button
440
+ variant="outline"
441
+ size="sm"
442
+ className={isLoomRecording ? "gap-1.5" : undefined}
443
+ onClick={() => window.open(videoUrl, "_blank", "noopener,noreferrer")}
444
+ >
445
+ {isLoomRecording ? (
446
+ <>
447
+ <IconExternalLink className="h-4 w-4" />
448
+ {t("shareDialog.openPlayer")}
449
+ </>
450
+ ) : (
451
+ t("recordRoute.downloadRecording")
452
+ )}
453
+ </Button>
454
+ ) : null;
455
+ const moveDownloadIntoMakePublicRow = showMakePublic && Boolean(videoUrl);
456
+
457
+ useEffect(() => {
458
+ if (isPublic) setAgentDetailsOpen(false);
459
+ }, [isPublic]);
437
460
 
438
461
  return (
439
462
  <div className="space-y-4">
@@ -444,6 +467,7 @@ function LinkTab({
444
467
  isPending={visibilityPending}
445
468
  onChange={(next) => setResourceVisibility(next)}
446
469
  publicDescription={t("shareDialog.publicDescription")}
470
+ showDescription={false}
447
471
  />
448
472
  ) : (
449
473
  <div className="space-y-2" aria-hidden>
@@ -463,47 +487,77 @@ function LinkTab({
463
487
  />
464
488
 
465
489
  {!isPublic ? (
466
- <div className="space-y-2">
467
- <CopyField
468
- label={t("shareDialog.shareWithAgents")}
469
- value={agentLink}
470
- disabled={agentShareDisabled}
471
- />
472
- {sharesLoaded ? (
473
- <>
474
- <p className="text-xs text-muted-foreground">
475
- {t("shareDialog.agentTokenDescription")}
476
- </p>
477
- {agentLinkError ? (
478
- <div className="flex items-center justify-between gap-2 rounded-md border border-border bg-muted/30 px-3 py-2">
479
- <p className="text-xs text-muted-foreground">
480
- {t("shareDialog.agentLinkUnavailable")}
481
- </p>
482
- <Button
483
- type="button"
484
- variant="ghost"
485
- size="sm"
486
- className="h-7"
487
- onClick={() => void loadAgentContextUrl()}
488
- disabled={createAgentLink.isPending}
489
- >
490
- {t("shareDialog.retryAgentLink")}
491
- </Button>
492
- </div>
493
- ) : null}
490
+ <Collapsible
491
+ open={agentDetailsOpen}
492
+ onOpenChange={setAgentDetailsOpen}
493
+ className="overflow-hidden rounded-md border border-border"
494
+ >
495
+ <CollapsibleTrigger asChild>
496
+ <button
497
+ type="button"
498
+ className="flex min-h-10 w-full items-center justify-between gap-3 px-3 py-2 text-left text-sm font-medium transition-colors hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset"
499
+ >
500
+ <span className="flex min-w-0 items-center gap-2 truncate">
501
+ <IconLink
502
+ aria-hidden="true"
503
+ className="size-3.5 shrink-0 text-muted-foreground"
504
+ strokeWidth={1.8}
505
+ />
506
+ {t("shareDialog.shareWithAgents")}
507
+ </span>
508
+ <IconChevronDown
509
+ aria-hidden="true"
510
+ className={`size-4 shrink-0 text-muted-foreground transition-transform ${agentDetailsOpen ? "rotate-180" : ""}`}
511
+ />
512
+ </button>
513
+ </CollapsibleTrigger>
514
+ <CollapsibleContent className="clips-collapsible-content border-t border-border px-3 py-3">
515
+ <div className="space-y-2">
494
516
  <CopyField
495
- label={t("shareDialog.copyAgentPrompt")}
496
- value={agentPrompt}
517
+ label={t("shareDialog.shareWithAgents")}
518
+ value={agentLink}
497
519
  disabled={agentShareDisabled}
498
520
  />
499
- </>
500
- ) : null}
501
- </div>
521
+ {sharesLoaded ? (
522
+ <>
523
+ <p className="text-xs text-muted-foreground">
524
+ {t("shareDialog.agentTokenDescription")}
525
+ </p>
526
+ {agentLinkError ? (
527
+ <div className="flex items-center justify-between gap-2 rounded-md border border-border bg-muted/30 px-3 py-2">
528
+ <p className="text-xs text-muted-foreground">
529
+ {t("shareDialog.agentLinkUnavailable")}
530
+ </p>
531
+ <Button
532
+ type="button"
533
+ variant="ghost"
534
+ size="sm"
535
+ className="h-7"
536
+ onClick={() => void loadAgentContextUrl()}
537
+ disabled={createAgentLink.isPending}
538
+ >
539
+ {t("shareDialog.retryAgentLink")}
540
+ </Button>
541
+ </div>
542
+ ) : null}
543
+ <CopyField
544
+ label={t("shareDialog.copyAgentPrompt")}
545
+ value={agentPrompt}
546
+ disabled={agentShareDisabled}
547
+ />
548
+ </>
549
+ ) : null}
550
+ </div>
551
+ </CollapsibleContent>
552
+ </Collapsible>
502
553
  ) : null}
503
554
 
504
- {sharesLoaded && !isPublic && canManage ? (
555
+ {showMakePublic ? (
505
556
  <MakePublicCard
506
557
  isPending={isPending}
558
+ secondaryAction={
559
+ moveDownloadIntoMakePublicRow ? downloadButton : undefined
560
+ }
507
561
  onMakePublic={() =>
508
562
  setResourceVisibility("public", {
509
563
  onSuccess: () => copyToClipboard(shareUrl),
@@ -512,7 +566,9 @@ function LinkTab({
512
566
  />
513
567
  ) : null}
514
568
 
515
- {emailPreviewThumbnailUrl || videoUrl || animatedThumbnailUrl ? (
569
+ {emailPreviewThumbnailUrl ||
570
+ animatedThumbnailUrl ||
571
+ (videoUrl && !moveDownloadIntoMakePublicRow) ? (
516
572
  <div className="flex flex-wrap gap-2">
517
573
  {emailPreviewThumbnailUrl ? (
518
574
  <Button
@@ -532,25 +588,7 @@ function LinkTab({
532
588
  {t("shareDialog.gifPreview")}
533
589
  </Button>
534
590
  ) : null}
535
- {videoUrl ? (
536
- <Button
537
- variant="outline"
538
- size="sm"
539
- className={isLoomRecording ? "gap-1.5" : undefined}
540
- onClick={() =>
541
- window.open(videoUrl, "_blank", "noopener,noreferrer")
542
- }
543
- >
544
- {isLoomRecording ? (
545
- <>
546
- <IconExternalLink className="h-4 w-4" />
547
- {t("shareDialog.openPlayer")}
548
- </>
549
- ) : (
550
- t("recordRoute.downloadRecording")
551
- )}
552
- </Button>
553
- ) : null}
591
+ {!moveDownloadIntoMakePublicRow ? downloadButton : null}
554
592
  </div>
555
593
  ) : null}
556
594
  </div>