@agent-native/core 0.114.9 → 0.114.10

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 (130) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +13 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/engine/ai-sdk-engine.ts +31 -3
  5. package/corpus/core/src/agent/engine/anthropic-engine.ts +20 -3
  6. package/corpus/core/src/agent/engine/builder-engine.ts +56 -9
  7. package/corpus/core/src/agent/engine/first-event-timeout.ts +64 -0
  8. package/corpus/core/src/cli/pr-visual-recap-workflow.ts +2 -3
  9. package/corpus/core/src/client/AssistantChat.tsx +7 -3
  10. package/corpus/core/src/client/resources/ResourcesPanel.tsx +1 -1
  11. package/corpus/core/src/integrations/computer-supervision-store.ts +4 -4
  12. package/corpus/core/src/integrations/pending-tasks-store.ts +1 -1
  13. package/corpus/core/src/integrations/remote-commands-store.ts +5 -5
  14. package/corpus/core/src/integrations/remote-devices-store.ts +3 -3
  15. package/corpus/core/src/integrations/remote-push-store.ts +3 -3
  16. package/corpus/core/src/observability/traces.ts +38 -1
  17. package/corpus/core/src/server/action-routes.ts +23 -1
  18. package/corpus/core/src/server/http-response-telemetry.ts +6 -1
  19. package/corpus/core/src/templates/workspace-core/.agents/skills/observability/SKILL.md +1 -1
  20. package/corpus/core/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
  21. package/corpus/templates/analytics/actions/account-deep-dive.ts +41 -6
  22. package/corpus/templates/analytics/changelog/2026-07-20-account-deep-dives-finish-with-partial-data-when-hubspot-lo.md +6 -0
  23. package/corpus/templates/analytics/changelog/2026-07-20-dashboard-email-reports-now-include-an-image-when-a-single-p.md +6 -0
  24. package/corpus/templates/analytics/server/lib/dashboard-report.ts +11 -3
  25. package/corpus/templates/calendar/actions/create-event.ts +5 -0
  26. package/corpus/templates/calendar/app/components/ThemeToggle.tsx +4 -1
  27. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +0 -1
  28. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +309 -421
  29. package/corpus/templates/calendar/app/components/calendar/InlineEventPickers.tsx +349 -0
  30. package/corpus/templates/calendar/app/i18n-data.ts +110 -0
  31. package/corpus/templates/calendar/app/lib/event-form-utils.ts +6 -0
  32. package/corpus/templates/calendar/app/pages/CalendarView.tsx +3 -0
  33. package/corpus/templates/calendar/app/root.tsx +4 -2
  34. package/corpus/templates/calendar/changelog/2026-07-20-events-can-be-updated-inline-with-date-time-timezone-and-rep.md +6 -0
  35. package/corpus/templates/calendar/changelog/2026-07-20-public-booking-pages-no-longer-hydrate-with-a-theme-mismatch.md +6 -0
  36. package/corpus/templates/calendar/shared/api.ts +1 -0
  37. package/corpus/templates/clips/AGENTS.md +7 -0
  38. package/corpus/templates/clips/actions/create-recording.ts +51 -10
  39. package/corpus/templates/clips/actions/get-recording-player-data.ts +34 -5
  40. package/corpus/templates/clips/actions/lib/transcript-preview.ts +48 -0
  41. package/corpus/templates/clips/actions/list-recordings.ts +15 -4
  42. package/corpus/templates/clips/actions/regenerate-summary.ts +32 -0
  43. package/corpus/templates/clips/actions/search-recordings.ts +28 -6
  44. package/corpus/templates/clips/actions/view-screen.ts +54 -47
  45. package/corpus/templates/clips/app/components/library/library-grid.tsx +1 -5
  46. package/corpus/templates/clips/app/components/library/recording-card.tsx +29 -32
  47. package/corpus/templates/clips/app/components/meetings/quick-ask-sidebar.tsx +13 -0
  48. package/corpus/templates/clips/changelog/2026-07-20-agent-summaries-use-bounded-transcript-context.md +6 -0
  49. package/corpus/templates/clips/changelog/2026-07-20-clip-summaries-now-finish-without-sending-a-long-transcript-.md +6 -0
  50. package/corpus/templates/clips/changelog/2026-07-20-clips-agents-only-discover-recordings-you-own-or-have-alread.md +6 -0
  51. package/corpus/templates/clips/changelog/2026-07-20-hosted-recordings-no-longer-start-with-an-impossible-buffered-upload.md +6 -0
  52. package/corpus/templates/clips/changelog/2026-07-20-meeting-recordings-now-offer-a-cleaner-transcript-view-with-.md +6 -0
  53. package/corpus/templates/clips/changelog/2026-07-20-shared-clips-can-be-shared-with-limited-access-and-cards-sho.md +6 -0
  54. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +12 -8
  55. package/corpus/templates/clips/desktop/src/overlays/pill-logo.tsx +2 -15
  56. package/corpus/templates/clips/desktop/src/overlays/recording-pill.tsx +107 -340
  57. package/corpus/templates/clips/desktop/src/styles.css +27 -145
  58. package/corpus/templates/clips/desktop/src-tauri/src/recording_indicator.rs +6 -7
  59. package/corpus/templates/clips/server/lib/agent-recording-access.ts +73 -0
  60. package/corpus/templates/clips/server/plugins/agent-chat.ts +5 -2
  61. package/corpus/templates/plan/app/lib/plan-local-bridge.ts +18 -0
  62. package/corpus/templates/plan/app/pages/PlansPage.tsx +10 -11
  63. package/corpus/templates/plan/changelog/2026-07-20-bridged-local-plans-now-load-comments-from-the-local-workspace.md +6 -0
  64. package/dist/agent/engine/ai-sdk-engine.d.ts.map +1 -1
  65. package/dist/agent/engine/ai-sdk-engine.js +27 -4
  66. package/dist/agent/engine/ai-sdk-engine.js.map +1 -1
  67. package/dist/agent/engine/anthropic-engine.d.ts.map +1 -1
  68. package/dist/agent/engine/anthropic-engine.js +19 -4
  69. package/dist/agent/engine/anthropic-engine.js.map +1 -1
  70. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  71. package/dist/agent/engine/builder-engine.js +40 -7
  72. package/dist/agent/engine/builder-engine.js.map +1 -1
  73. package/dist/agent/engine/first-event-timeout.d.ts +25 -0
  74. package/dist/agent/engine/first-event-timeout.d.ts.map +1 -0
  75. package/dist/agent/engine/first-event-timeout.js +49 -0
  76. package/dist/agent/engine/first-event-timeout.js.map +1 -0
  77. package/dist/cli/pr-visual-recap-workflow.d.ts +2 -2
  78. package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
  79. package/dist/cli/pr-visual-recap-workflow.js +2 -2
  80. package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
  81. package/dist/client/AssistantChat.d.ts.map +1 -1
  82. package/dist/client/AssistantChat.js +7 -3
  83. package/dist/client/AssistantChat.js.map +1 -1
  84. package/dist/client/resources/ResourcesPanel.js +1 -1
  85. package/dist/client/resources/ResourcesPanel.js.map +1 -1
  86. package/dist/collab/routes.d.ts +1 -1
  87. package/dist/integrations/computer-supervision-store.js +4 -4
  88. package/dist/integrations/computer-supervision-store.js.map +1 -1
  89. package/dist/integrations/pending-tasks-store.js +1 -1
  90. package/dist/integrations/pending-tasks-store.js.map +1 -1
  91. package/dist/integrations/remote-commands-store.js +5 -5
  92. package/dist/integrations/remote-commands-store.js.map +1 -1
  93. package/dist/integrations/remote-devices-store.js +3 -3
  94. package/dist/integrations/remote-devices-store.js.map +1 -1
  95. package/dist/integrations/remote-push-store.js +3 -3
  96. package/dist/integrations/remote-push-store.js.map +1 -1
  97. package/dist/notifications/routes.d.ts +2 -2
  98. package/dist/observability/routes.d.ts +3 -3
  99. package/dist/observability/traces.d.ts.map +1 -1
  100. package/dist/observability/traces.js +23 -1
  101. package/dist/observability/traces.js.map +1 -1
  102. package/dist/secrets/routes.d.ts +9 -9
  103. package/dist/server/action-routes.d.ts.map +1 -1
  104. package/dist/server/action-routes.js +23 -1
  105. package/dist/server/action-routes.js.map +1 -1
  106. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  107. package/dist/server/http-response-telemetry.d.ts +2 -0
  108. package/dist/server/http-response-telemetry.d.ts.map +1 -1
  109. package/dist/server/http-response-telemetry.js +5 -1
  110. package/dist/server/http-response-telemetry.js.map +1 -1
  111. package/dist/templates/workspace-core/.agents/skills/observability/SKILL.md +1 -1
  112. package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
  113. package/package.json +2 -2
  114. package/src/agent/engine/ai-sdk-engine.ts +31 -3
  115. package/src/agent/engine/anthropic-engine.ts +20 -3
  116. package/src/agent/engine/builder-engine.ts +56 -9
  117. package/src/agent/engine/first-event-timeout.ts +64 -0
  118. package/src/cli/pr-visual-recap-workflow.ts +2 -3
  119. package/src/client/AssistantChat.tsx +7 -3
  120. package/src/client/resources/ResourcesPanel.tsx +1 -1
  121. package/src/integrations/computer-supervision-store.ts +4 -4
  122. package/src/integrations/pending-tasks-store.ts +1 -1
  123. package/src/integrations/remote-commands-store.ts +5 -5
  124. package/src/integrations/remote-devices-store.ts +3 -3
  125. package/src/integrations/remote-push-store.ts +3 -3
  126. package/src/observability/traces.ts +38 -1
  127. package/src/server/action-routes.ts +23 -1
  128. package/src/server/http-response-telemetry.ts +6 -1
  129. package/src/templates/workspace-core/.agents/skills/observability/SKILL.md +1 -1
  130. package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
@@ -14,6 +14,8 @@ import { writeAppState } from "@agent-native/core/application-state";
14
14
  import { getActiveFileUploadProviderForRequest } from "@agent-native/core/file-upload";
15
15
  import type { UploadMode } from "@shared/recording-core.js";
16
16
  import { MAX_UPLOAD_BYTES } from "@shared/upload-limits.js";
17
+ import { eq } from "drizzle-orm";
18
+ import { createError } from "h3";
17
19
 
18
20
  import { getDb, schema } from "../server/db/index.js";
19
21
  import {
@@ -25,7 +27,10 @@ import {
25
27
  } from "../server/lib/recordings.js";
26
28
  import { setResumableSession } from "../server/lib/resumable-session.js";
27
29
  import { shouldEnableStreamingUpload } from "../server/lib/streaming-upload-mode.js";
28
- import { allowsSqlRecordingChunkScratch } from "../server/lib/video-storage.js";
30
+ import {
31
+ allowsSqlRecordingChunkScratch,
32
+ STORAGE_SETUP_REQUIRED_REASON,
33
+ } from "../server/lib/video-storage.js";
29
34
  import { createRecordingSchema } from "./lib/create-recording-schema.js";
30
35
  import { DEFAULT_RECORDING_TITLE } from "./lib/title-source.js";
31
36
 
@@ -86,18 +91,49 @@ export default defineAction({
86
91
  console.log(`Created recording "${title}" (${id})`);
87
92
 
88
93
  // Initialize a resumable upload session so chunks are streamed to the
89
- // provider during recording (no post-stop assembly). Falls back gracefully
90
- // to the SQL chunk path when no provider supports resumable uploads or the
91
- // init fails.
94
+ // provider during recording (no post-stop assembly). Hosted deployments
95
+ // have no SQL chunk fallback, so never return a buffered target there.
92
96
  let uploadMode: UploadMode = "buffered";
93
97
  const uploadProvider = await getActiveFileUploadProviderForRequest();
98
+ const bufferedFallbackAvailable = allowsSqlRecordingChunkScratch();
99
+ const streamingEnabled = shouldEnableStreamingUpload({
100
+ client: args.streamingUploadClient,
101
+ mimeType: args.mimeType,
102
+ bufferedFallbackAvailable,
103
+ });
104
+ const streamingRequired = !bufferedFallbackAvailable;
105
+
106
+ const failUploadSetup = async (reason: string): Promise<never> => {
107
+ const failedAt = new Date().toISOString();
108
+ await db
109
+ .update(schema.recordings)
110
+ .set({ status: "failed", failureReason: reason, updatedAt: failedAt })
111
+ .where(eq(schema.recordings.id, id));
112
+ await writeAppState(`recording-upload-${id}`, {
113
+ recordingId: id,
114
+ status: "failed",
115
+ progress: 0,
116
+ failureReason: reason,
117
+ storageSetupRequired: reason === STORAGE_SETUP_REQUIRED_REASON,
118
+ updatedAt: failedAt,
119
+ });
120
+ throw createError({
121
+ statusCode: 503,
122
+ statusMessage: reason,
123
+ data: { retryable: true },
124
+ });
125
+ };
126
+
127
+ if (
128
+ streamingRequired &&
129
+ (!streamingEnabled || !uploadProvider?.resumable)
130
+ ) {
131
+ await failUploadSetup(STORAGE_SETUP_REQUIRED_REASON);
132
+ }
133
+
94
134
  if (
95
- args.requestStreaming === true &&
96
- shouldEnableStreamingUpload({
97
- client: args.streamingUploadClient,
98
- mimeType: args.mimeType,
99
- bufferedFallbackAvailable: allowsSqlRecordingChunkScratch(),
100
- }) &&
135
+ streamingEnabled &&
136
+ (args.requestStreaming === true || streamingRequired) &&
101
137
  uploadProvider?.resumable
102
138
  ) {
103
139
  try {
@@ -129,6 +165,11 @@ export default defineAction({
129
165
  `[create-recording] resumable session ready for ${id}: provider=${uploadProvider.id}`,
130
166
  );
131
167
  } catch (err) {
168
+ if (streamingRequired) {
169
+ await failUploadSetup(
170
+ "Video storage could not start a resumable upload session. Refresh and try again.",
171
+ );
172
+ }
132
173
  console.warn(
133
174
  `[create-recording] resumable session init failed, falling back to buffered:`,
134
175
  err instanceof Error ? err.message : String(err),
@@ -29,6 +29,7 @@ import { and, asc, eq, or, sql } from "drizzle-orm";
29
29
  import { z } from "zod";
30
30
 
31
31
  import { getDb, schema } from "../server/db/index.js";
32
+ import { isAgentRecordingCaller } from "../server/lib/agent-recording-access.js";
32
33
  import { isMediaVerificationPending } from "../server/lib/media-verification-state.js";
33
34
  import { resolvePlayerVideoUrl } from "../server/lib/player-video-url.js";
34
35
  import {
@@ -46,6 +47,7 @@ import {
46
47
  parseTranscriptSegments,
47
48
  } from "../shared/transcript-segments.js";
48
49
  import { resolveTranscriptPresentation } from "../shared/transcript-status.js";
50
+ import { boundTranscriptForAgent } from "./lib/transcript-preview.js";
49
51
 
50
52
  function safeJsonObject(raw: string | null | undefined) {
51
53
  if (!raw) return {};
@@ -91,7 +93,7 @@ function recordingDeepLink(recordingId: string): string {
91
93
 
92
94
  export default defineAction({
93
95
  description:
94
- "Fetch everything the player page needs for a recording: metadata, transcript, comments, reactions, chapters, CTAs, and the caller's effective role.",
96
+ "Fetch everything the player page needs for a recording: metadata, transcript, comments, reactions, chapters, CTAs, and the caller's effective role. Agent calls receive a bounded transcript payload; browser player calls receive the full transcript.",
95
97
  schema: z.object({
96
98
  recordingId: z.string().describe("Recording ID"),
97
99
  }),
@@ -106,7 +108,7 @@ export default defineAction({
106
108
  }),
107
109
  },
108
110
  http: { method: "GET" },
109
- run: async (args) => {
111
+ run: async (args, ctx) => {
110
112
  const access = await resolveAccess("recording", args.recordingId);
111
113
  if (!access) {
112
114
  throw new ForbiddenError(`No access to recording ${args.recordingId}`);
@@ -120,7 +122,19 @@ export default defineAction({
120
122
  }
121
123
 
122
124
  let hasExplicitShare = access.role === "owner";
123
- if (rec.visibility === "public" && access.role !== "owner") {
125
+ if (
126
+ rec.visibility === "public" &&
127
+ access.role !== "owner" &&
128
+ !rec.password &&
129
+ isAgentRecordingCaller(ctx?.caller)
130
+ ) {
131
+ hasExplicitShare = true;
132
+ }
133
+ if (
134
+ rec.visibility === "public" &&
135
+ access.role !== "owner" &&
136
+ !hasExplicitShare
137
+ ) {
124
138
  const userEmail = getRequestUserEmail()?.trim().toLowerCase();
125
139
  const orgId = getRequestOrgId();
126
140
  const principals = [];
@@ -289,6 +303,13 @@ export default defineAction({
289
303
  !transcript.fullText?.trim() &&
290
304
  transcriptSegments.length === 0;
291
305
  const transcriptPresentation = resolveTranscriptPresentation(transcript);
306
+ const agentTranscript =
307
+ ctx?.caller === "tool" || ctx?.caller === "mcp" || ctx?.caller === "a2a"
308
+ ? boundTranscriptForAgent({
309
+ fullText: transcript?.fullText,
310
+ segments: transcriptSegments,
311
+ })
312
+ : null;
292
313
 
293
314
  // Normalize the dev-fallback videoUrl:
294
315
  // 1. Rewrite legacy `/api/uploads/:id/blob` to `/api/video/:id` so old
@@ -362,11 +383,19 @@ export default defineAction({
362
383
  ? "failed"
363
384
  : transcriptPresentation.status,
364
385
  language: transcript.language,
365
- fullText: transcript.fullText,
386
+ fullText: agentTranscript?.fullText ?? transcript.fullText,
387
+ ...(agentTranscript
388
+ ? {
389
+ fullTextLength: agentTranscript.fullTextLength,
390
+ segmentCount: agentTranscript.segmentCount,
391
+ previewTruncated: agentTranscript.previewTruncated,
392
+ note: agentTranscript.note,
393
+ }
394
+ : {}),
366
395
  failureReason: transcriptReadyButEmpty
367
396
  ? "No speech was detected by transcription. Check microphone and speech permissions, then retry transcription."
368
397
  : transcriptPresentation.failureReason,
369
- segments: transcriptSegments,
398
+ segments: agentTranscript?.segments ?? transcriptSegments,
370
399
  cleanup: cleanupState
371
400
  ? {
372
401
  status:
@@ -1,4 +1,6 @@
1
1
  export const TRANSCRIPT_PREVIEW_CHARS = 2_000;
2
+ export const AGENT_TRANSCRIPT_MAX_CHARS = 12_000;
3
+ export const AGENT_TRANSCRIPT_SEGMENT_MAX_CHARS = 12_000;
2
4
 
3
5
  export interface TranscriptPreview {
4
6
  recordingId: string;
@@ -46,3 +48,49 @@ export function buildTranscriptPreview({
46
48
  : "The complete transcript fits in this snapshot.",
47
49
  };
48
50
  }
51
+
52
+ export interface BoundedAgentTranscript<T> {
53
+ fullText: string | null;
54
+ segments: T[];
55
+ fullTextLength: number;
56
+ segmentCount: number;
57
+ previewTruncated: boolean;
58
+ note: string;
59
+ }
60
+
61
+ export function boundTranscriptForAgent<T>({
62
+ fullText,
63
+ segments,
64
+ }: {
65
+ fullText: string | null | undefined;
66
+ segments: T[];
67
+ }): BoundedAgentTranscript<T> {
68
+ const text = fullText ?? "";
69
+ const boundedSegments: T[] = [];
70
+ let segmentChars = 0;
71
+
72
+ for (const segment of segments) {
73
+ const nextChars = JSON.stringify(segment)?.length ?? 0;
74
+ if (segmentChars + nextChars > AGENT_TRANSCRIPT_SEGMENT_MAX_CHARS) {
75
+ break;
76
+ }
77
+ boundedSegments.push(segment);
78
+ segmentChars += nextChars;
79
+ }
80
+
81
+ const previewTruncated =
82
+ text.length > AGENT_TRANSCRIPT_MAX_CHARS ||
83
+ boundedSegments.length < segments.length;
84
+
85
+ return {
86
+ fullText:
87
+ fullText == null ? null : text.slice(0, AGENT_TRANSCRIPT_MAX_CHARS),
88
+ segments: boundedSegments,
89
+ fullTextLength: text.length,
90
+ segmentCount: segments.length,
91
+ previewTruncated,
92
+ note: previewTruncated
93
+ ? `Agent transcript payload is bounded to ${AGENT_TRANSCRIPT_MAX_CHARS.toLocaleString()} text characters and ${AGENT_TRANSCRIPT_SEGMENT_MAX_CHARS.toLocaleString()} serialized segment characters. It may end mid-sentence; do not infer that transcription stopped early.`
94
+ : "The complete transcript fits in this agent payload.",
95
+ };
96
+ }
@@ -1,6 +1,5 @@
1
1
  import { defineAction } from "@agent-native/core";
2
2
  import { getRequestUserEmail } from "@agent-native/core/server/request-context";
3
- import { accessFilter } from "@agent-native/core/sharing";
4
3
  import {
5
4
  and,
6
5
  asc,
@@ -16,6 +15,10 @@ import {
16
15
  import { z } from "zod";
17
16
 
18
17
  import { getDb, schema } from "../server/db/index.js";
18
+ import {
19
+ agentRecordingAccessFilter,
20
+ isAgentRecordingCaller,
21
+ } from "../server/lib/agent-recording-access.js";
19
22
  import { resolvePlayerVideoUrl } from "../server/lib/player-video-url.js";
20
23
  import {
21
24
  getActiveOrganizationId,
@@ -62,7 +65,7 @@ export function resolveListRecordingMedia(
62
65
 
63
66
  export default defineAction({
64
67
  description:
65
- "List recordings visible to the current user. Supports filtering by view (library/shared/space/archive/trash/all), folder, space, tag, free-text, and sort. The shared view returns accessible recordings owned by someone else.",
68
+ "List recordings visible to the current user. Supports filtering by view (library/shared/space/archive/trash/all), folder, space, tag, free-text, and sort. Public/unlisted recordings are discoverable only when owned by or previously viewed by the current user; the shared view returns accessible recordings owned by someone else.",
66
69
  schema: z.object({
67
70
  view: z
68
71
  .enum(["library", "shared", "space", "archive", "trash", "all"])
@@ -110,11 +113,19 @@ export default defineAction({
110
113
  .describe("Include playable media fields for editor workflows"),
111
114
  }),
112
115
  http: { method: "GET" },
113
- run: async (args) => {
116
+ run: async (args, ctx) => {
114
117
  const db = getDb();
115
118
 
116
119
  const whereClauses = [
117
- accessFilter(schema.recordings, schema.recordingShares),
120
+ agentRecordingAccessFilter(
121
+ schema.recordings,
122
+ schema.recordingShares,
123
+ schema.recordingViewers,
124
+ {
125
+ agentOnly: isAgentRecordingCaller(ctx?.caller),
126
+ userEmail: ctx?.userEmail,
127
+ },
128
+ ),
118
129
  ];
119
130
 
120
131
  const orgId = await getActiveOrganizationId();
@@ -15,6 +15,7 @@ import { z } from "zod";
15
15
 
16
16
  import { getDb, schema } from "../server/db/index.js";
17
17
  import { withFullVideoAiInstructions } from "../shared/clips-ai-prefs.js";
18
+ import cleanupTranscript from "./cleanup-transcript.js";
18
19
  import { readIncludeFullVideoInAi } from "./lib/clips-ai-prefs.js";
19
20
 
20
21
  export default defineAction({
@@ -65,6 +66,37 @@ export default defineAction({
65
66
  };
66
67
  }
67
68
 
69
+ if (!includeFullVideoInAi) {
70
+ const result = await cleanupTranscript.run({
71
+ transcript: transcript?.fullText ?? "",
72
+ task: "summary",
73
+ context: `Clip title: ${rec.title}`,
74
+ });
75
+ const description = result.summaryMd?.trim();
76
+ if (!description) {
77
+ return {
78
+ updated: false,
79
+ skipped: true,
80
+ reason: "summary_empty",
81
+ recordingId: args.recordingId,
82
+ provider: result.provider,
83
+ };
84
+ }
85
+
86
+ await db
87
+ .update(schema.recordings)
88
+ .set({ description, updatedAt: new Date().toISOString() })
89
+ .where(eq(schema.recordings.id, args.recordingId));
90
+ await writeAppState("refresh-signal", { ts: Date.now() });
91
+
92
+ return {
93
+ updated: true,
94
+ recordingId: args.recordingId,
95
+ description,
96
+ provider: result.provider,
97
+ };
98
+ }
99
+
68
100
  const baseMessage =
69
101
  `Regenerate the description for recording ${args.recordingId}. ` +
70
102
  `Read the transcript in this request's context and call ` +
@@ -1,10 +1,13 @@
1
1
  import { defineAction, embedApp } from "@agent-native/core";
2
2
  import { buildDeepLink } from "@agent-native/core/server";
3
- import { accessFilter } from "@agent-native/core/sharing";
4
3
  import { and, eq, sql } from "drizzle-orm";
5
4
  import { z } from "zod";
6
5
 
7
6
  import { getDb, schema } from "../server/db/index.js";
7
+ import {
8
+ agentRecordingAccessFilter,
9
+ isAgentRecordingCaller,
10
+ } from "../server/lib/agent-recording-access.js";
8
11
  import { buildCaseInsensitiveSearchPattern } from "./search-recordings-utils.js";
9
12
 
10
13
  const SNIPPET_RADIUS = 80;
@@ -92,7 +95,7 @@ function transcriptMatch(
92
95
 
93
96
  export default defineAction({
94
97
  description:
95
- "Search recordings by title, description, transcript text, or comments. Transcript and comment matches include timestamps for jumping to the matching moment.",
98
+ "Search recordings by title, description, transcript text, or comments. Transcript and comment matches include timestamps for jumping to the matching moment. Public/unlisted recordings are searchable only when owned by or previously viewed by the current user.",
96
99
  schema: z.object({
97
100
  query: z.string().min(1).describe("Search text"),
98
101
  limit: z.coerce.number().int().min(1).max(100).default(30),
@@ -108,9 +111,13 @@ export default defineAction({
108
111
  }),
109
112
  },
110
113
  http: { method: "GET" },
111
- run: async (args) => {
114
+ run: async (args, ctx) => {
112
115
  const db = getDb();
113
116
  const pattern = buildCaseInsensitiveSearchPattern(args.query);
117
+ const recordingAccess = {
118
+ agentOnly: isAgentRecordingCaller(ctx?.caller),
119
+ userEmail: ctx?.userEmail,
120
+ };
114
121
 
115
122
  // Title/description matches on the recordings table
116
123
  const recMatches = await db
@@ -128,7 +135,12 @@ export default defineAction({
128
135
  .from(schema.recordings)
129
136
  .where(
130
137
  and(
131
- accessFilter(schema.recordings, schema.recordingShares),
138
+ agentRecordingAccessFilter(
139
+ schema.recordings,
140
+ schema.recordingShares,
141
+ schema.recordingViewers,
142
+ recordingAccess,
143
+ ),
132
144
  sql`(lower(${schema.recordings.title}) LIKE ${pattern} ESCAPE '\\' OR lower(${schema.recordings.description}) LIKE ${pattern} ESCAPE '\\')`,
133
145
  ),
134
146
  )
@@ -158,7 +170,12 @@ export default defineAction({
158
170
  )
159
171
  .where(
160
172
  and(
161
- accessFilter(schema.recordings, schema.recordingShares),
173
+ agentRecordingAccessFilter(
174
+ schema.recordings,
175
+ schema.recordingShares,
176
+ schema.recordingViewers,
177
+ recordingAccess,
178
+ ),
162
179
  sql`lower(${schema.recordingTranscripts.fullText}) LIKE ${pattern} ESCAPE '\\'`,
163
180
  ),
164
181
  )
@@ -186,7 +203,12 @@ export default defineAction({
186
203
  )
187
204
  .where(
188
205
  and(
189
- accessFilter(schema.recordings, schema.recordingShares),
206
+ agentRecordingAccessFilter(
207
+ schema.recordings,
208
+ schema.recordingShares,
209
+ schema.recordingViewers,
210
+ recordingAccess,
211
+ ),
190
212
  sql`lower(${schema.recordingComments.content}) LIKE ${pattern} ESCAPE '\\'`,
191
213
  ),
192
214
  )
@@ -32,6 +32,7 @@ import {
32
32
  import { z } from "zod";
33
33
 
34
34
  import { getDb, schema } from "../server/db/index.js";
35
+ import { agentRecordingAccessFilter } from "../server/lib/agent-recording-access.js";
35
36
  import {
36
37
  getActiveOrganizationId,
37
38
  ownerEmailMatches,
@@ -87,7 +88,12 @@ async function fetchRecording(id: string) {
87
88
  .where(
88
89
  and(
89
90
  eq(schema.recordings.id, id),
90
- accessFilter(schema.recordings, schema.recordingShares),
91
+ agentRecordingAccessFilter(
92
+ schema.recordings,
93
+ schema.recordingShares,
94
+ schema.recordingViewers,
95
+ { agentOnly: true },
96
+ ),
91
97
  ),
92
98
  );
93
99
  return row ? mapRecording(row) : null;
@@ -183,7 +189,12 @@ async function fetchLibrary({
183
189
  const ownerEmail = getRequestUserEmail();
184
190
  if (!ownerEmail) return [];
185
191
  const conditions = [
186
- accessFilter(schema.recordings, schema.recordingShares),
192
+ agentRecordingAccessFilter(
193
+ schema.recordings,
194
+ schema.recordingShares,
195
+ schema.recordingViewers,
196
+ { agentOnly: true },
197
+ ),
187
198
  isNull(schema.recordings.archivedAt),
188
199
  isNull(schema.recordings.trashedAt),
189
200
  ];
@@ -480,13 +491,47 @@ async function fetchDictationDetail(dictationId: string) {
480
491
  };
481
492
  }
482
493
 
494
+ async function addRecordingContext(
495
+ screen: Record<string, unknown>,
496
+ recordingId: string,
497
+ ) {
498
+ const recording = await fetchRecording(recordingId);
499
+ if (!recording) return;
500
+
501
+ const [transcript, comments, browserDiagnosticsSummary, bugReportSummary] =
502
+ await Promise.all([
503
+ fetchTranscript(recordingId),
504
+ fetchComments(recordingId),
505
+ fetchBrowserDiagnosticsSummary(recordingId),
506
+ fetchBugReportSummary(recordingId),
507
+ ]);
508
+ screen.recording = recording;
509
+ if (transcript) {
510
+ screen.transcript = buildTranscriptPreview({
511
+ recordingId: transcript.recordingId,
512
+ language: transcript.language,
513
+ status: transcript.status,
514
+ fullText: transcript.fullText,
515
+ segments: transcript.segments,
516
+ });
517
+ }
518
+ screen.comments = comments.slice(0, 50);
519
+ if (browserDiagnosticsSummary) {
520
+ screen.browserDiagnostics = {
521
+ summary: browserDiagnosticsSummary,
522
+ note: "Summary only. Call get-recording-player-data for full redacted diagnostics when you have editor access.",
523
+ };
524
+ }
525
+ if (bugReportSummary) screen.bugReport = bugReportSummary;
526
+ }
527
+
483
528
  async function fetchShare(shareId: string) {
484
529
  const db = getDb();
485
- const [row] = await db
530
+ const [share] = await db
486
531
  .select()
487
532
  .from(schema.recordingShares)
488
533
  .where(eq(schema.recordingShares.id, shareId));
489
- return row ?? null;
534
+ return share ?? null;
490
535
  }
491
536
 
492
537
  export default defineAction({
@@ -520,48 +565,7 @@ export default defineAction({
520
565
  switch (nav.view) {
521
566
  case "recording":
522
567
  case "insights": {
523
- if (nav.recordingId) {
524
- const recording = await fetchRecording(nav.recordingId);
525
- if (recording) {
526
- const [
527
- transcript,
528
- comments,
529
- browserDiagnosticsSummary,
530
- bugReportSummary,
531
- ] = await Promise.all([
532
- fetchTranscript(nav.recordingId),
533
- fetchComments(nav.recordingId),
534
- fetchBrowserDiagnosticsSummary(nav.recordingId),
535
- fetchBugReportSummary(nav.recordingId),
536
- ]);
537
- screen.recording = recording;
538
- if (transcript) {
539
- // Ambient snapshot only — embed a short fullText snippet and the
540
- // segment count, NOT the entire transcript + every word-level
541
- // segment. The full transcript can be tens of thousands of tokens
542
- // and is injected into <current-screen> on EVERY message, which
543
- // can blow the model's context window. The agent calls
544
- // get-recording-player-data for the complete transcript/segments.
545
- screen.transcript = buildTranscriptPreview({
546
- recordingId: transcript.recordingId,
547
- language: transcript.language,
548
- status: transcript.status,
549
- fullText: transcript.fullText,
550
- segments: transcript.segments,
551
- });
552
- }
553
- screen.comments = comments.slice(0, 50);
554
- if (browserDiagnosticsSummary) {
555
- screen.browserDiagnostics = {
556
- summary: browserDiagnosticsSummary,
557
- note: "Summary only. Call get-recording-player-data for full redacted diagnostics when you have editor access.",
558
- };
559
- }
560
- if (bugReportSummary) {
561
- screen.bugReport = bugReportSummary;
562
- }
563
- }
564
- }
568
+ if (nav.recordingId) await addRecordingContext(screen, nav.recordingId);
565
569
  break;
566
570
  }
567
571
  case "library": {
@@ -611,7 +615,10 @@ export default defineAction({
611
615
  case "embed": {
612
616
  if (nav.shareId) {
613
617
  const share = await fetchShare(nav.shareId);
614
- if (share) screen.share = share;
618
+ if (share) {
619
+ screen.share = share;
620
+ await addRecordingContext(screen, share.resourceId);
621
+ }
615
622
  }
616
623
  break;
617
624
  }
@@ -373,11 +373,7 @@ export function LibraryGrid({
373
373
  onToggleSelect={
374
374
  canManageRecordings ? toggleSelect : undefined
375
375
  }
376
- onShare={
377
- canManageRecordings
378
- ? (rec) => setSharingRec(rec)
379
- : undefined
380
- }
376
+ onShare={(rec) => setSharingRec(rec)}
381
377
  moveTargets={moveTargets}
382
378
  onMove={canMoveSelection ? moveSingle : undefined}
383
379
  isMovePending={moveRecording.isPending}
@@ -125,8 +125,7 @@ export function RecordingCard({
125
125
  );
126
126
  const canMove = Boolean(onMove && moveTargets.length > 0);
127
127
  const canSelect = Boolean(onToggleSelect) && !readOnly;
128
- const showActions =
129
- !readOnly && Boolean(onShare || onMove || onArchive || onTrash);
128
+ const showActions = Boolean(onShare || onMove || onArchive || onTrash);
130
129
  const hasDefaultTitle = isDefaultTitle(recording.title);
131
130
  const displayTitle = hasDefaultTitle
132
131
  ? t("editableTitle.untitled")
@@ -330,6 +329,17 @@ export function RecordingCard({
330
329
  {displayTitle}
331
330
  </div>
332
331
  )}
332
+ <div className="mt-1 flex min-w-0 items-center gap-1.5 text-[11px] text-muted-foreground">
333
+ <Avatar className="h-4 w-4 shrink-0">
334
+ <AvatarImage src="" alt={recording.ownerEmail} />
335
+ <AvatarFallback className="bg-primary/15 text-[8px] text-primary">
336
+ {ownerInitials}
337
+ </AvatarFallback>
338
+ </Avatar>
339
+ <span className="min-w-0 truncate">{recording.ownerEmail}</span>
340
+ <span aria-hidden>•</span>
341
+ <span className="shrink-0">{relative}</span>
342
+ </div>
333
343
  <div className="mt-0.5 flex items-center gap-1.5 text-[11px] text-muted-foreground">
334
344
  <PrivacyIcon
335
345
  visibility={recording.visibility}
@@ -353,8 +363,6 @@ export function RecordingCard({
353
363
  })}
354
364
  </span>
355
365
  )}
356
- <span>•</span>
357
- <span>{relative}</span>
358
366
  </div>
359
367
  </div>
360
368
 
@@ -446,34 +454,23 @@ export function RecordingCard({
446
454
  )}
447
455
  </div>
448
456
 
449
- <div className="flex items-center gap-2">
450
- <Avatar className="h-5 w-5">
451
- <AvatarImage src="" alt={recording.ownerEmail} />
452
- <AvatarFallback className="text-[9px] bg-primary/15 text-primary">
453
- {ownerInitials}
454
- </AvatarFallback>
455
- </Avatar>
456
- <span className="text-xs text-muted-foreground truncate">
457
- {recording.ownerEmail}
458
- </span>
459
- {recording.tags.length > 0 && (
460
- <div className="ms-auto flex items-center gap-1 truncate">
461
- {recording.tags.slice(0, 2).map((t) => (
462
- <span
463
- key={t}
464
- className="rounded-full bg-primary/10 text-primary text-[10px] px-1.5 py-0.5"
465
- >
466
- {t}
467
- </span>
468
- ))}
469
- {recording.tags.length > 2 && (
470
- <span className="text-[10px] text-muted-foreground">
471
- +{recording.tags.length - 2}
472
- </span>
473
- )}
474
- </div>
475
- )}
476
- </div>
457
+ {recording.tags.length > 0 && (
458
+ <div className="flex items-center gap-1 truncate">
459
+ {recording.tags.slice(0, 2).map((t) => (
460
+ <span
461
+ key={t}
462
+ className="rounded-full bg-primary/10 text-primary text-[10px] px-1.5 py-0.5"
463
+ >
464
+ {t}
465
+ </span>
466
+ ))}
467
+ {recording.tags.length > 2 && (
468
+ <span className="text-[10px] text-muted-foreground">
469
+ +{recording.tags.length - 2}
470
+ </span>
471
+ )}
472
+ </div>
473
+ )}
477
474
  </div>
478
475
  </div>
479
476
  );
@@ -94,6 +94,19 @@ export function QuickAskSidebar({
94
94
  return () => window.removeEventListener("keydown", onKeyDown);
95
95
  }, []);
96
96
 
97
+ useEffect(() => {
98
+ const params = new URLSearchParams(window.location.search);
99
+ if (params.get("chat") !== "1") return;
100
+ setOpen(true);
101
+ params.delete("chat");
102
+ const nextQuery = params.toString();
103
+ window.history.replaceState(
104
+ window.history.state,
105
+ "",
106
+ `${window.location.pathname}${nextQuery ? `?${nextQuery}` : ""}${window.location.hash}`,
107
+ );
108
+ }, []);
109
+
97
110
  // Focus the composer whenever the sheet opens.
98
111
  useEffect(() => {
99
112
  if (open) {