@agent-native/core 0.114.3 → 0.114.5

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 (93) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +14 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/a2a/artifact-response.ts +42 -16
  5. package/corpus/core/src/a2a/client.ts +4 -0
  6. package/corpus/core/src/a2a/handlers.ts +133 -2
  7. package/corpus/core/src/a2a/index.ts +1 -0
  8. package/corpus/core/src/a2a/types.ts +14 -0
  9. package/corpus/core/src/integrations/index.ts +6 -0
  10. package/corpus/core/src/integrations/pending-tasks-store.ts +89 -0
  11. package/corpus/core/src/integrations/webhook-handler.ts +17 -7
  12. package/corpus/core/src/scripts/call-agent.ts +54 -10
  13. package/corpus/core/src/server/agent-discovery.ts +22 -0
  14. package/corpus/templates/clips/.agents/skills/meetings/SKILL.md +11 -1
  15. package/corpus/templates/clips/AGENTS.md +14 -4
  16. package/corpus/templates/clips/actions/update-meeting.ts +13 -3
  17. package/corpus/templates/clips/actions/view-screen.ts +1 -0
  18. package/corpus/templates/clips/app/components/meetings/meeting-card.tsx +2 -1
  19. package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +96 -4
  20. package/corpus/templates/clips/app/i18n/en-US.ts +11 -0
  21. package/corpus/templates/clips/app/lib/public-meeting.ts +85 -0
  22. package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +11 -1
  23. package/corpus/templates/clips/app/routes/share.$shareId.tsx +6 -7
  24. package/corpus/templates/clips/app/routes/share.meeting.$meetingId.tsx +241 -135
  25. package/corpus/templates/clips/changelog/2026-07-20-meeting-share-links-can-include-the-full-transcript-with-an-.md +6 -0
  26. package/corpus/templates/clips/changelog/2026-07-20-recording-retries-the-default-mac-microphone-when-a-saved-in.md +6 -0
  27. package/corpus/templates/clips/changelog/2026-07-20-shared-clips-now-tell-agents-to-wait-while-uploads-and-trans.md +6 -0
  28. package/corpus/templates/clips/desktop/src/lib/meeting-join-url.ts +1 -33
  29. package/corpus/templates/clips/desktop/src/lib/transcription-engine.ts +65 -2
  30. package/corpus/templates/clips/server/db/schema.ts +3 -0
  31. package/corpus/templates/clips/server/lib/public-agent-context.ts +31 -21
  32. package/corpus/templates/clips/server/plugins/auth.ts +1 -0
  33. package/corpus/templates/clips/server/plugins/db.ts +6 -0
  34. package/corpus/templates/clips/server/routes/api/agent-transcript.json.get.ts +13 -3
  35. package/corpus/templates/clips/server/routes/api/public-meeting.get.ts +152 -0
  36. package/corpus/templates/clips/shared/agent-context.ts +60 -0
  37. package/corpus/templates/clips/shared/meeting-join-url.ts +36 -0
  38. package/corpus/templates/content/.agents/skills/content/SKILL.md +19 -5
  39. package/corpus/templates/content/changelog/2026-07-17-slack-created-database-entries-now-record-slack-as-their-sub.md +6 -0
  40. package/dist/a2a/artifact-response.d.ts +5 -0
  41. package/dist/a2a/artifact-response.d.ts.map +1 -1
  42. package/dist/a2a/artifact-response.js +20 -5
  43. package/dist/a2a/artifact-response.js.map +1 -1
  44. package/dist/a2a/client.d.ts +3 -1
  45. package/dist/a2a/client.d.ts.map +1 -1
  46. package/dist/a2a/client.js +2 -0
  47. package/dist/a2a/client.js.map +1 -1
  48. package/dist/a2a/handlers.d.ts.map +1 -1
  49. package/dist/a2a/handlers.js +100 -8
  50. package/dist/a2a/handlers.js.map +1 -1
  51. package/dist/a2a/index.d.ts +1 -1
  52. package/dist/a2a/index.d.ts.map +1 -1
  53. package/dist/a2a/index.js.map +1 -1
  54. package/dist/a2a/types.d.ts +12 -0
  55. package/dist/a2a/types.d.ts.map +1 -1
  56. package/dist/a2a/types.js.map +1 -1
  57. package/dist/collab/routes.d.ts +1 -1
  58. package/dist/collab/struct-routes.d.ts +1 -1
  59. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  60. package/dist/integrations/index.d.ts +1 -0
  61. package/dist/integrations/index.d.ts.map +1 -1
  62. package/dist/integrations/index.js +1 -0
  63. package/dist/integrations/index.js.map +1 -1
  64. package/dist/integrations/pending-tasks-store.d.ts +9 -0
  65. package/dist/integrations/pending-tasks-store.d.ts.map +1 -1
  66. package/dist/integrations/pending-tasks-store.js +60 -0
  67. package/dist/integrations/pending-tasks-store.js.map +1 -1
  68. package/dist/integrations/webhook-handler.d.ts.map +1 -1
  69. package/dist/integrations/webhook-handler.js +13 -7
  70. package/dist/integrations/webhook-handler.js.map +1 -1
  71. package/dist/notifications/routes.d.ts +1 -1
  72. package/dist/observability/routes.d.ts +5 -5
  73. package/dist/progress/routes.d.ts +1 -1
  74. package/dist/scripts/call-agent.d.ts.map +1 -1
  75. package/dist/scripts/call-agent.js +39 -10
  76. package/dist/scripts/call-agent.js.map +1 -1
  77. package/dist/secrets/routes.d.ts +9 -9
  78. package/dist/server/agent-discovery.d.ts +2 -0
  79. package/dist/server/agent-discovery.d.ts.map +1 -1
  80. package/dist/server/agent-discovery.js +19 -0
  81. package/dist/server/agent-discovery.js.map +1 -1
  82. package/dist/server/transcribe-voice.d.ts +1 -1
  83. package/package.json +2 -2
  84. package/src/a2a/artifact-response.ts +42 -16
  85. package/src/a2a/client.ts +4 -0
  86. package/src/a2a/handlers.ts +133 -2
  87. package/src/a2a/index.ts +1 -0
  88. package/src/a2a/types.ts +14 -0
  89. package/src/integrations/index.ts +6 -0
  90. package/src/integrations/pending-tasks-store.ts +89 -0
  91. package/src/integrations/webhook-handler.ts +17 -7
  92. package/src/scripts/call-agent.ts +54 -10
  93. package/src/server/agent-discovery.ts +22 -0
@@ -10,6 +10,8 @@ import { invokeAgentAction } from "../a2a/invoke.js";
10
10
  import type {
11
11
  A2AApprovedAction,
12
12
  A2ACorrelationMetadata,
13
+ A2ASourceContext,
14
+ A2ASourceContextReference,
13
15
  Task,
14
16
  } from "../a2a/types.js";
15
17
  import {
@@ -100,26 +102,65 @@ function getIntegrationCallTimeoutMs(): number | undefined {
100
102
  return DEFAULT_SERVERLESS_INTEGRATION_A2A_TIMEOUT_MS;
101
103
  }
102
104
 
103
- function integrationSourceContextHint(): string {
105
+ interface IntegrationSourceContext {
106
+ reference: A2ASourceContextReference;
107
+ hint: A2ASourceContext;
108
+ }
109
+
110
+ function integrationSourceContext(): IntegrationSourceContext | undefined {
104
111
  const integration = getIntegrationRequestContext();
105
112
  const incoming = integration?.incoming;
106
- if (incoming?.platform !== "slack" || !incoming.sourceUrl) return "";
113
+ if (
114
+ !integration ||
115
+ incoming?.platform !== "slack" ||
116
+ !incoming.sourceUrl ||
117
+ !integration.taskId
118
+ ) {
119
+ return undefined;
120
+ }
121
+
122
+ const rawSourceUrl = incoming.sourceUrl;
123
+ if (rawSourceUrl !== rawSourceUrl.trim()) return undefined;
107
124
 
108
125
  try {
109
- const sourceUrl = new URL(incoming.sourceUrl);
126
+ const sourceUrl = new URL(rawSourceUrl);
110
127
  const isSlackHost =
111
128
  sourceUrl.hostname === "slack.com" ||
112
129
  sourceUrl.hostname.endsWith(".slack.com");
113
- if (sourceUrl.protocol !== "https:" || !isSlackHost) return "";
114
- return (
115
- `\n\n[Source Slack thread: ${sourceUrl.toString()} ` +
116
- "Preserve this exact URL as request provenance when creating an intake record or other artifact.]"
117
- );
130
+ if (
131
+ sourceUrl.protocol !== "https:" ||
132
+ !isSlackHost ||
133
+ sourceUrl.username ||
134
+ sourceUrl.password ||
135
+ sourceUrl.port
136
+ ) {
137
+ return undefined;
138
+ }
139
+ return {
140
+ reference: {
141
+ platform: "slack",
142
+ integrationTaskId: integration.taskId,
143
+ },
144
+ hint: {
145
+ platform: "slack",
146
+ sourceUrl: rawSourceUrl,
147
+ },
148
+ };
118
149
  } catch {
119
- return "";
150
+ return undefined;
120
151
  }
121
152
  }
122
153
 
154
+ function integrationSourceContextHint(
155
+ sourceContext: IntegrationSourceContext | undefined,
156
+ ): string {
157
+ if (!sourceContext) return "";
158
+ return (
159
+ `\n\n[Source Slack thread: ${sourceContext.hint.sourceUrl} ` +
160
+ "Compatibility hint only; this text is not authoritative. Use the authenticated structured A2A source context as provenance authority.]"
161
+ );
162
+ }
163
+
123
164
  function formatDownstreamLlmCredentialFailure(
124
165
  agentName: string,
125
166
  value: unknown,
@@ -253,9 +294,10 @@ export async function run(
253
294
  // in handlers.ts) still emits fully-qualified URLs. This is belt-and-
254
295
  // suspenders with the receiver hint — but it works against any current
255
296
  // deployment, no redeploy required.
297
+ const sourceContext = integrationSourceContext();
256
298
  const messageWithHint = taskId
257
299
  ? ""
258
- : `${message}${integrationSourceContextHint()}\n\n` +
300
+ : `${message}${integrationSourceContextHint(sourceContext)}\n\n` +
259
301
  `[Note: this request comes from another app via A2A. The caller cannot see your local UI, deck list, or navigation — only the literal text you put in your reply. ` +
260
302
  `If you create or reference a deck/document/design/dashboard, include its FULLY-QUALIFIED URL (e.g. ${agent.url}/deck/<id>) in your reply, not a relative path. ` +
261
303
  `Use only artifact IDs and URL paths returned by successful actions — never invent slugs, IDs, or hosts. ` +
@@ -432,6 +474,7 @@ export async function run(
432
474
  orgDomain: callerOrgDomain,
433
475
  orgSecret: callerOrgSecret,
434
476
  approvedActions,
477
+ ...(sourceContext ? { sourceContext: sourceContext.reference } : {}),
435
478
  contextId: context.threadId,
436
479
  correlation,
437
480
  idempotencyKey,
@@ -526,6 +569,7 @@ export async function run(
526
569
  orgDomain: domain,
527
570
  orgSecret,
528
571
  approvedActions,
572
+ ...(sourceContext ? { sourceContext: sourceContext.reference } : {}),
529
573
  contextId: context?.threadId,
530
574
  correlation,
531
575
  idempotencyKey,
@@ -689,6 +689,28 @@ async function discoverWorkspaceAgents(
689
689
  .filter((agent): agent is DiscoveredAgent => !!agent);
690
690
  }
691
691
 
692
+ /** Resolve only the Dispatch app designated by the receiver's own manifest. */
693
+ export function findWorkspaceDispatchAgent(): DiscoveredAgent | undefined {
694
+ const app = loadWorkspaceAppsManifest()?.find(
695
+ (candidate) => candidate.isDispatch === true,
696
+ );
697
+ if (!app) return undefined;
698
+
699
+ const builtin = BUILTIN_AGENTS.find((agent) => agent.id === "dispatch");
700
+ const url = workspaceAppUrl(app, builtin?.url);
701
+ if (!url) return undefined;
702
+ return {
703
+ id: app.id,
704
+ name: app.name,
705
+ description:
706
+ app.description ||
707
+ builtin?.description ||
708
+ `Workspace app mounted at ${app.path}`,
709
+ url,
710
+ color: builtin?.color || "#6B7280",
711
+ };
712
+ }
713
+
692
714
  /**
693
715
  * Like `getBuiltinAgents`, but always returns the production URL — never the
694
716
  * env-resolved devUrl. Used by the resource seeder so that a one-time seed
@@ -78,7 +78,7 @@ When meetings are enabled, the Clips desktop app also watches for native Zoom (`
78
78
  | `list-meetings` | Upcoming + past, scoped via `accessFilter`; reads connected Google Calendar live |
79
79
  | `get-meeting` | One meeting + participants + segments + notes |
80
80
  | `create-meeting` | Create a meeting row (`source`: `calendar` / `adhoc` / `manual`); desktop adhoc Zoom/Teams detection passes `source: "adhoc"` |
81
- | `update-meeting` | Inline title edit, notes edits |
81
+ | `update-meeting` | Inline title/notes edits; owners/admins can opt the full transcript into or out of the meeting share link with `shareTranscript` |
82
82
  | `delete-meeting` | Soft-delete a meeting from the visible list; linked recordings and calendar events stay intact |
83
83
  | `start-meeting-recording` | Begin native macOS transcript stream + create the linked recording |
84
84
  | `stop-meeting-recording` | End the active capture |
@@ -91,6 +91,15 @@ When meetings are enabled, the Clips desktop app also watches for native Zoom (`
91
91
 
92
92
  All actions go through `accessFilter` / `assertAccess`. AI work delegates via `sendToAgentChat` per the `delegate-to-agent` skill — never inline LLM calls.
93
93
 
94
+ ## Sharing meeting notes and transcripts
95
+
96
+ Meeting share links always include generated notes: the summary, key points,
97
+ and action items. Full transcripts are more sensitive and stay excluded by
98
+ default. Owners and share admins can call `update-meeting` with
99
+ `shareTranscript=true|false` or use the Share popover's transcript switch.
100
+ The setting controls only the meeting share payload; it must not change the
101
+ linked recording's visibility or expose recording media and comments.
102
+
94
103
  ## Cleanup credential order
95
104
 
96
105
  The `cleanup-transcript` action resolves credentials in this order — **always lead with Builder.io Connect** when explaining options to the user:
@@ -121,6 +130,7 @@ The app exposes `view`, `meetingId`, and `dictationId` so the agent always knows
121
130
  "scheduledStart": "...",
122
131
  "scheduledEnd": "...",
123
132
  "transcriptStatus": "ready",
133
+ "shareTranscript": false,
124
134
  "participants": [{ "email": "alice@ex.com", "name": "Alice" }, ...],
125
135
  "actionItems": [{ "assigneeEmail": "alice@ex.com", "text": "..." }, ...],
126
136
  "hasRecording": true,
@@ -102,6 +102,11 @@ ladder.
102
102
  sharing/status boundary.
103
103
  - Use framework sharing actions for recordings. Password and expiry are extra
104
104
  controls on top of visibility/share grants.
105
+ - Meeting share links include the summary, key points, and action items. The
106
+ full transcript is an explicit, default-off setting: call `update-meeting`
107
+ with `shareTranscript=true|false` only when the owner or a share admin asks
108
+ to change what the meeting link exposes. This does not change the linked
109
+ recording's visibility or expose its media.
105
110
  - Use `list-recordings --view=shared` for the current user's "Shared with me"
106
111
  collection. It returns recordings admitted by sharing access that are owned
107
112
  by someone else; public-link-only clips remain out of this list.
@@ -111,10 +116,15 @@ ladder.
111
116
  segments; `/api/agent-frame.jpg?id=<recordingId>&atMs=<ms>` for a screen
112
117
  frame at a timestamp. Password-protected clips require the password once to
113
118
  mint a short-lived token returned inside agent-context links.
114
- - If public agent context or transcript APIs report `transcript.status` as
115
- `"pending"`, wait 15-30 seconds and retry the context/transcript URL a few
116
- times, especially for long recordings. Do not pivot straight to frames or tell
117
- the user there is no transcript until the retry budget is exhausted.
119
+ - If a public agent discovery/context/transcript payload reports
120
+ `agentReadiness.state` as `"preparing"` (the clip is `"uploading"` or
121
+ `"processing"`), wait 15 seconds and retry `agentContextUrl`; do not open the
122
+ share page, fetch frames, or draw conclusions until the recording status is
123
+ `"ready"`.
124
+ If `transcript.status` is `"pending"` after the clip is ready, wait 15-30
125
+ seconds and retry the context/transcript URL a few times, especially for long
126
+ recordings. Do not pivot straight to frames or tell the user there is no
127
+ transcript until the retry budget is exhausted.
118
128
  - If transcription failed because Builder transcription credits are exhausted,
119
129
  tell the user that clearly and point them to Builder.io credits/upgrade or a
120
130
  Groq key for backup speech-to-text. Generic OpenAI or Anthropic chat keys do
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Update a meeting's metadata. Editor access required.
2
+ * Update meeting content or owner/admin-managed sharing settings.
3
3
  */
4
4
 
5
5
  import { defineAction } from "@agent-native/core";
@@ -9,10 +9,11 @@ import { eq } from "drizzle-orm";
9
9
  import { z } from "zod";
10
10
 
11
11
  import { getDb, schema } from "../server/db/index.js";
12
+ import { booleanParam } from "./lib/cli-params.js";
12
13
 
13
14
  export default defineAction({
14
15
  description:
15
- "Partially update a meeting (title, schedule, notes, summary, action items). Only provided fields are updated.",
16
+ "Partially update a meeting's content. Owners and share admins can also control visibility and whether share links include the transcript.",
16
17
  schema: z.object({
17
18
  id: z.string().describe("Meeting id"),
18
19
  title: z.string().optional(),
@@ -41,10 +42,17 @@ export default defineAction({
41
42
  .optional()
42
43
  .describe("Replace the action item set on the meetings row JSON"),
43
44
  transcriptStatus: z.enum(["idle", "pending", "ready", "failed"]).optional(),
45
+ shareTranscript: booleanParam
46
+ .optional()
47
+ .describe(
48
+ "Include the linked recording transcript on meeting share pages",
49
+ ),
44
50
  visibility: z.enum(["private", "org", "public"]).optional(),
45
51
  }),
46
52
  run: async (args) => {
47
- await assertAccess("meeting", args.id, "editor");
53
+ const updatesSharing =
54
+ args.shareTranscript !== undefined || args.visibility !== undefined;
55
+ await assertAccess("meeting", args.id, updatesSharing ? "admin" : "editor");
48
56
  const db = getDb();
49
57
 
50
58
  const patch: Record<string, unknown> = {
@@ -67,6 +75,8 @@ export default defineAction({
67
75
  if (args.actionItems)
68
76
  patch.actionItemsJson = JSON.stringify(args.actionItems);
69
77
  if (args.transcriptStatus) patch.transcriptStatus = args.transcriptStatus;
78
+ if (args.shareTranscript !== undefined)
79
+ patch.shareTranscript = args.shareTranscript;
70
80
  if (args.visibility) patch.visibility = args.visibility;
71
81
 
72
82
  await db
@@ -414,6 +414,7 @@ async function fetchMeetingDetail(meetingId: string) {
414
414
  source: meeting.source,
415
415
  userNotesMd: meeting.userNotesMd,
416
416
  transcriptStatus: meeting.transcriptStatus,
417
+ shareTranscript: Boolean(meeting.shareTranscript),
417
418
  summaryMd: meeting.summaryMd,
418
419
  bullets: safeJsonArray<{ text: string }>(meeting.bulletsJson),
419
420
  },
@@ -1,4 +1,5 @@
1
1
  import { useT } from "@agent-native/core/client/i18n";
2
+ import { resolveNativeMeetingJoinUrl } from "@shared/meeting-join-url";
2
3
  import {
3
4
  IconCheck,
4
5
  IconClock,
@@ -284,7 +285,7 @@ export function UpcomingMeetingCard({
284
285
  <div className="flex items-center gap-1.5">
285
286
  {meeting.joinUrl && (
286
287
  <a
287
- href={meeting.joinUrl}
288
+ href={resolveNativeMeetingJoinUrl(meeting.joinUrl)}
288
289
  target="_blank"
289
290
  rel="noopener noreferrer"
290
291
  onClick={(e) => e.stopPropagation()}
@@ -1,8 +1,11 @@
1
1
  import { appPath } from "@agent-native/core/client/api-path";
2
- import { useActionQuery } from "@agent-native/core/client/hooks";
2
+ import {
3
+ useActionMutation,
4
+ useActionQuery,
5
+ } from "@agent-native/core/client/hooks";
3
6
  import { useT } from "@agent-native/core/client/i18n";
4
- import { IconLink, IconMail } from "@tabler/icons-react";
5
- import { useMemo, type ReactNode } from "react";
7
+ import { IconCheck, IconLink, IconMail } from "@tabler/icons-react";
8
+ import { useEffect, useMemo, useState, type ReactNode } from "react";
6
9
  import { toast } from "sonner";
7
10
 
8
11
  import {
@@ -22,17 +25,22 @@ import {
22
25
  PopoverContent,
23
26
  PopoverTrigger,
24
27
  } from "@/components/ui/popover";
28
+ import { Switch } from "@/components/ui/switch";
25
29
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
26
30
 
27
31
  export interface ShareMeetingPopoverProps {
28
32
  meetingId: string;
29
33
  meetingTitle?: string;
34
+ shareTranscript: boolean;
35
+ transcriptReady: boolean;
30
36
  children: ReactNode;
31
37
  }
32
38
 
33
39
  export function ShareMeetingPopover({
34
40
  meetingId,
35
41
  meetingTitle,
42
+ shareTranscript,
43
+ transcriptReady,
36
44
  children,
37
45
  }: ShareMeetingPopoverProps) {
38
46
  return (
@@ -40,11 +48,13 @@ export function ShareMeetingPopover({
40
48
  <PopoverTrigger asChild>{children}</PopoverTrigger>
41
49
  <PopoverContent
42
50
  align="end"
43
- className="w-[440px] max-w-[calc(100vw-1rem)] overflow-hidden border-border p-0"
51
+ className="max-h-[calc(100vh-1rem)] w-[440px] max-w-[calc(100vw-1rem)] overflow-y-auto border-border p-0"
44
52
  >
45
53
  <ShareMeetingContent
46
54
  meetingId={meetingId}
47
55
  meetingTitle={meetingTitle}
56
+ shareTranscript={shareTranscript}
57
+ transcriptReady={transcriptReady}
48
58
  />
49
59
  </PopoverContent>
50
60
  </Popover>
@@ -54,9 +64,13 @@ export function ShareMeetingPopover({
54
64
  function ShareMeetingContent({
55
65
  meetingId,
56
66
  meetingTitle,
67
+ shareTranscript,
68
+ transcriptReady,
57
69
  }: {
58
70
  meetingId: string;
59
71
  meetingTitle?: string;
72
+ shareTranscript: boolean;
73
+ transcriptReady: boolean;
60
74
  }) {
61
75
  const t = useT();
62
76
  const shareUrl = useMemo(
@@ -97,6 +111,8 @@ function ShareMeetingContent({
97
111
  shareUrl={shareUrl}
98
112
  sharesQuery={sharesQuery}
99
113
  canManage={canManage}
114
+ shareTranscript={shareTranscript}
115
+ transcriptReady={transcriptReady}
100
116
  />
101
117
  </TabsContent>
102
118
 
@@ -127,13 +143,22 @@ function LinkTab({
127
143
  shareUrl,
128
144
  sharesQuery,
129
145
  canManage,
146
+ shareTranscript,
147
+ transcriptReady,
130
148
  }: {
131
149
  meetingId: string;
132
150
  shareUrl: string;
133
151
  sharesQuery: SharesQuery;
134
152
  canManage: boolean;
153
+ shareTranscript: boolean;
154
+ transcriptReady: boolean;
135
155
  }) {
136
156
  const t = useT();
157
+ const updateMeeting = useActionMutation<
158
+ unknown,
159
+ { id: string; shareTranscript: boolean }
160
+ >("update-meeting");
161
+ const [includeTranscript, setIncludeTranscript] = useState(shareTranscript);
137
162
  const { setResourceVisibility, isPending } = useResourceVisibilityMutation(
138
163
  "meeting",
139
164
  meetingId,
@@ -144,6 +169,28 @@ function LinkTab({
144
169
  (data?.visibility as Visibility | null) ?? "private";
145
170
  const isPublic = visibility === "public";
146
171
 
172
+ useEffect(() => {
173
+ setIncludeTranscript(shareTranscript);
174
+ }, [shareTranscript]);
175
+
176
+ const handleTranscriptSharingChange = (next: boolean) => {
177
+ const previous = includeTranscript;
178
+ setIncludeTranscript(next);
179
+ updateMeeting.mutate(
180
+ { id: meetingId, shareTranscript: next },
181
+ {
182
+ onError: (error: unknown) => {
183
+ setIncludeTranscript(previous);
184
+ toast.error(
185
+ error instanceof Error
186
+ ? error.message
187
+ : t("shareMeeting.updateTranscriptSharingFailed"),
188
+ );
189
+ },
190
+ },
191
+ );
192
+ };
193
+
147
194
  return (
148
195
  <div className="space-y-4">
149
196
  <GeneralAccessSelect
@@ -153,6 +200,51 @@ function LinkTab({
153
200
  onChange={(next) => setResourceVisibility(next)}
154
201
  />
155
202
 
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>
245
+ </div>
246
+ </div>
247
+
156
248
  <CopyField label={t("clipsFinalRaw.shareLink")} value={shareUrl} />
157
249
 
158
250
  {!isPublic && canManage ? (
@@ -927,6 +927,17 @@ All notable user-facing changes to Clips are documented here. Open it any time f
927
927
  summary: "Summary",
928
928
  keyPoints: "Key points",
929
929
  actionItems: "Action items",
930
+ sharedContent: "Shared content",
931
+ summaryIncluded: "Summary, key points, and action items",
932
+ includeTranscript: "Include full transcript",
933
+ includeTranscriptDescription:
934
+ "Anyone with access to this meeting can read the complete transcript.",
935
+ transcriptUnavailable: "The transcript isn't ready yet.",
936
+ transcript: "Transcript",
937
+ copyTranscript: "Copy transcript",
938
+ transcriptCopied: "Transcript copied",
939
+ copyTranscriptFailed: "Couldn't copy the transcript",
940
+ updateTranscriptSharingFailed: "Couldn't update transcript sharing",
930
941
  },
931
942
  deleteRecordingMenu: {
932
943
  movedToTrash: "Clip moved to trash",
@@ -0,0 +1,85 @@
1
+ import { appBasePath } from "@agent-native/core/client/api-path";
2
+
3
+ import type { TranscriptSegment } from "@/components/meetings/transcript-bubbles";
4
+
5
+ export interface PublicMeetingBullet {
6
+ text: string;
7
+ }
8
+
9
+ export interface PublicMeetingParticipant {
10
+ email: string;
11
+ name: string | null;
12
+ isOrganizer: boolean;
13
+ }
14
+
15
+ export interface PublicMeetingActionItem {
16
+ id: string;
17
+ text: string;
18
+ assigneeEmail: string | null;
19
+ completedAt: string | null;
20
+ }
21
+
22
+ export interface PublicMeetingTranscript {
23
+ status: string;
24
+ language: string | null;
25
+ fullText: string | null;
26
+ segments: TranscriptSegment[];
27
+ }
28
+
29
+ export interface PublicMeeting {
30
+ id: string;
31
+ title: string;
32
+ scheduledStart: string | null;
33
+ summaryMd: string;
34
+ bullets: PublicMeetingBullet[];
35
+ participants: PublicMeetingParticipant[];
36
+ actionItems: PublicMeetingActionItem[];
37
+ actualStart: string | null;
38
+ actualEnd: string | null;
39
+ transcriptStatus: string | null;
40
+ transcript?: PublicMeetingTranscript | null;
41
+ }
42
+
43
+ export interface PublicMeetingPayload {
44
+ meeting: PublicMeeting;
45
+ viewer: {
46
+ role: "owner" | "admin" | "editor" | "viewer";
47
+ canEdit: boolean;
48
+ isOwner: boolean;
49
+ } | null;
50
+ }
51
+
52
+ export interface PublicMeetingResult {
53
+ ok: boolean;
54
+ status: number;
55
+ data: PublicMeetingPayload | { error?: string };
56
+ }
57
+
58
+ export function publicMeetingUrl(
59
+ meetingId: string,
60
+ origin: string,
61
+ basePath: string,
62
+ ): string {
63
+ const url = new URL(`${basePath}/api/public-meeting`, origin);
64
+ url.searchParams.set("id", meetingId);
65
+ return url.toString();
66
+ }
67
+
68
+ export async function fetchPublicMeeting(
69
+ meetingId: string,
70
+ options: {
71
+ signal?: AbortSignal;
72
+ origin?: string;
73
+ basePath?: string;
74
+ } = {},
75
+ ): Promise<PublicMeetingResult> {
76
+ const origin = options.origin ?? window.location.origin;
77
+ const basePath = options.basePath ?? appBasePath();
78
+ const response = await fetch(publicMeetingUrl(meetingId, origin, basePath), {
79
+ signal: options.signal,
80
+ });
81
+ const data = (await response.json().catch(() => ({}))) as
82
+ | PublicMeetingPayload
83
+ | { error?: string };
84
+ return { ok: response.ok, status: response.status, data };
85
+ }
@@ -99,6 +99,7 @@ interface Meeting {
99
99
  recordingId?: string | null;
100
100
  recordingDurationMs?: number | null;
101
101
  transcriptStatus?: "pending" | "ready" | "failed" | "in_progress" | string;
102
+ shareTranscript?: boolean | null;
102
103
  summaryMd?: string | null;
103
104
  userNotesMd?: string | null;
104
105
  bulletsJson?: Bullet[] | null;
@@ -313,7 +314,10 @@ export default function MeetingDetailRoute() {
313
314
  meeting?: Omit<Meeting, "participants" | "segmentsJson"> | null;
314
315
  participants?: Participant[];
315
316
  actionItems?: ActionItem[];
316
- transcript?: { segmentsJson?: TranscriptSegment[] | null } | null;
317
+ transcript?: {
318
+ fullText?: string | null;
319
+ segmentsJson?: TranscriptSegment[] | null;
320
+ } | null;
317
321
  recording?: { id: string; durationMs?: number | null } | null;
318
322
  role?: "owner" | "admin" | "editor" | "viewer";
319
323
  };
@@ -682,6 +686,12 @@ export default function MeetingDetailRoute() {
682
686
  <ShareMeetingPopover
683
687
  meetingId={meeting.id}
684
688
  meetingTitle={meeting.title}
689
+ shareTranscript={meeting.shareTranscript === true}
690
+ transcriptReady={
691
+ meeting.transcriptStatus === "ready" &&
692
+ (segments.length > 0 ||
693
+ Boolean(data?.transcript?.fullText?.trim()))
694
+ }
685
695
  >
686
696
  <Button size="sm" className="shrink-0 gap-1.5">
687
697
  <IconShare3 className="h-4 w-4" />
@@ -69,6 +69,7 @@ import { getDb, schema } from "../../server/db";
69
69
  import { resolvePlayerThumbnailUrl } from "../../server/lib/player-thumbnail-url";
70
70
  import {
71
71
  buildAgentApiUrls,
72
+ buildAgentDiscoveryPayload,
72
73
  CLIPS_AGENT_ACCESS_PARAM,
73
74
  CLIP_AGENT_ACCESS_TOKEN_PREFIX,
74
75
  safeJsonForHtml,
@@ -305,20 +306,18 @@ function AgentDiscovery({
305
306
  recording,
306
307
  agentContextUrl,
307
308
  }: {
308
- recording: Pick<SharePageMetaRecording, "id" | "title"> | null;
309
+ recording: Pick<SharePageMetaRecording, "id" | "title" | "status"> | null;
309
310
  agentContextUrl: string | null;
310
311
  }) {
311
312
  const t = useT();
312
313
  if (!recording || !agentContextUrl) return null;
313
314
 
314
- const payload = {
315
- type: "agent-native.clip.discovery",
316
- clipId: recording.id,
315
+ const payload = buildAgentDiscoveryPayload({
316
+ recordingId: recording.id,
317
317
  title: recording.title,
318
+ status: recording.status,
318
319
  agentContextUrl,
319
- instructions:
320
- "Fetch agentContextUrl for the transcript and JPEG frame URLs. Fetch the frame URLs to SEE the screen, not just read the transcript.",
321
- };
320
+ });
322
321
 
323
322
  return (
324
323
  <>