@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.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +14 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/artifact-response.ts +42 -16
- package/corpus/core/src/a2a/client.ts +4 -0
- package/corpus/core/src/a2a/handlers.ts +133 -2
- package/corpus/core/src/a2a/index.ts +1 -0
- package/corpus/core/src/a2a/types.ts +14 -0
- package/corpus/core/src/integrations/index.ts +6 -0
- package/corpus/core/src/integrations/pending-tasks-store.ts +89 -0
- package/corpus/core/src/integrations/webhook-handler.ts +17 -7
- package/corpus/core/src/scripts/call-agent.ts +54 -10
- package/corpus/core/src/server/agent-discovery.ts +22 -0
- package/corpus/templates/clips/.agents/skills/meetings/SKILL.md +11 -1
- package/corpus/templates/clips/AGENTS.md +14 -4
- package/corpus/templates/clips/actions/update-meeting.ts +13 -3
- package/corpus/templates/clips/actions/view-screen.ts +1 -0
- package/corpus/templates/clips/app/components/meetings/meeting-card.tsx +2 -1
- package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +96 -4
- package/corpus/templates/clips/app/i18n/en-US.ts +11 -0
- package/corpus/templates/clips/app/lib/public-meeting.ts +85 -0
- package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +11 -1
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +6 -7
- package/corpus/templates/clips/app/routes/share.meeting.$meetingId.tsx +241 -135
- package/corpus/templates/clips/changelog/2026-07-20-meeting-share-links-can-include-the-full-transcript-with-an-.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-20-recording-retries-the-default-mac-microphone-when-a-saved-in.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-20-shared-clips-now-tell-agents-to-wait-while-uploads-and-trans.md +6 -0
- package/corpus/templates/clips/desktop/src/lib/meeting-join-url.ts +1 -33
- package/corpus/templates/clips/desktop/src/lib/transcription-engine.ts +65 -2
- package/corpus/templates/clips/server/db/schema.ts +3 -0
- package/corpus/templates/clips/server/lib/public-agent-context.ts +31 -21
- package/corpus/templates/clips/server/plugins/auth.ts +1 -0
- package/corpus/templates/clips/server/plugins/db.ts +6 -0
- package/corpus/templates/clips/server/routes/api/agent-transcript.json.get.ts +13 -3
- package/corpus/templates/clips/server/routes/api/public-meeting.get.ts +152 -0
- package/corpus/templates/clips/shared/agent-context.ts +60 -0
- package/corpus/templates/clips/shared/meeting-join-url.ts +36 -0
- package/corpus/templates/content/.agents/skills/content/SKILL.md +19 -5
- package/corpus/templates/content/changelog/2026-07-17-slack-created-database-entries-now-record-slack-as-their-sub.md +6 -0
- package/dist/a2a/artifact-response.d.ts +5 -0
- package/dist/a2a/artifact-response.d.ts.map +1 -1
- package/dist/a2a/artifact-response.js +20 -5
- package/dist/a2a/artifact-response.js.map +1 -1
- package/dist/a2a/client.d.ts +3 -1
- package/dist/a2a/client.d.ts.map +1 -1
- package/dist/a2a/client.js +2 -0
- package/dist/a2a/client.js.map +1 -1
- package/dist/a2a/handlers.d.ts.map +1 -1
- package/dist/a2a/handlers.js +100 -8
- package/dist/a2a/handlers.js.map +1 -1
- package/dist/a2a/index.d.ts +1 -1
- package/dist/a2a/index.d.ts.map +1 -1
- package/dist/a2a/index.js.map +1 -1
- package/dist/a2a/types.d.ts +12 -0
- package/dist/a2a/types.d.ts.map +1 -1
- package/dist/a2a/types.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/integrations/index.d.ts +1 -0
- package/dist/integrations/index.d.ts.map +1 -1
- package/dist/integrations/index.js +1 -0
- package/dist/integrations/index.js.map +1 -1
- package/dist/integrations/pending-tasks-store.d.ts +9 -0
- package/dist/integrations/pending-tasks-store.d.ts.map +1 -1
- package/dist/integrations/pending-tasks-store.js +60 -0
- package/dist/integrations/pending-tasks-store.js.map +1 -1
- package/dist/integrations/webhook-handler.d.ts.map +1 -1
- package/dist/integrations/webhook-handler.js +13 -7
- package/dist/integrations/webhook-handler.js.map +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/progress/routes.d.ts +1 -1
- package/dist/scripts/call-agent.d.ts.map +1 -1
- package/dist/scripts/call-agent.js +39 -10
- package/dist/scripts/call-agent.js.map +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-discovery.d.ts +2 -0
- package/dist/server/agent-discovery.d.ts.map +1 -1
- package/dist/server/agent-discovery.js +19 -0
- package/dist/server/agent-discovery.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +2 -2
- package/src/a2a/artifact-response.ts +42 -16
- package/src/a2a/client.ts +4 -0
- package/src/a2a/handlers.ts +133 -2
- package/src/a2a/index.ts +1 -0
- package/src/a2a/types.ts +14 -0
- package/src/integrations/index.ts +6 -0
- package/src/integrations/pending-tasks-store.ts +89 -0
- package/src/integrations/webhook-handler.ts +17 -7
- package/src/scripts/call-agent.ts +54 -10
- 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
|
-
|
|
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 (
|
|
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(
|
|
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 (
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
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
|
|
115
|
-
`"
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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-
|
|
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?: {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
<>
|