@agent-native/core 0.79.7 → 0.79.9

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 (84) 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/run-manager.ts +9 -1
  5. package/corpus/core/src/client/agent-chat-adapter.ts +24 -16
  6. package/corpus/core/src/client/session-replay.ts +67 -13
  7. package/corpus/core/src/onboarding/default-steps.ts +3 -3
  8. package/corpus/core/src/server/agent-chat-plugin.ts +82 -162
  9. package/corpus/core/src/server/agent-run-context.ts +204 -0
  10. package/corpus/core/src/server/credential-provider.ts +20 -4
  11. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +14 -4
  12. package/corpus/templates/analytics/changelog/2026-06-26-session-replay-uploads-now-accept-compressed-payloads-so-lar.md +6 -0
  13. package/corpus/templates/analytics/changelog/2026-06-26-sql-chart-tooltips-now-stay-stable-while-moving-across-dense.md +6 -0
  14. package/corpus/templates/analytics/server/handlers/session-replay.ts +56 -8
  15. package/corpus/templates/clips/app/components/player/transcript-panel.tsx +1 -1
  16. package/corpus/templates/clips/app/components/recorder/storage-setup-card.tsx +44 -16
  17. package/corpus/templates/clips/app/i18n/ar-SA.ts +12 -9
  18. package/corpus/templates/clips/app/i18n/de-DE.ts +12 -9
  19. package/corpus/templates/clips/app/i18n/en-US.ts +19 -16
  20. package/corpus/templates/clips/app/i18n/es-ES.ts +12 -9
  21. package/corpus/templates/clips/app/i18n/fr-FR.ts +12 -9
  22. package/corpus/templates/clips/app/i18n/hi-IN.ts +13 -9
  23. package/corpus/templates/clips/app/i18n/ja-JP.ts +12 -9
  24. package/corpus/templates/clips/app/i18n/ko-KR.ts +12 -9
  25. package/corpus/templates/clips/app/i18n/pt-BR.ts +12 -9
  26. package/corpus/templates/clips/app/i18n/zh-CN.ts +19 -14
  27. package/corpus/templates/clips/app/i18n/zh-TW.ts +19 -14
  28. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +7 -0
  29. package/corpus/templates/clips/app/routes/record.tsx +2 -2
  30. package/corpus/templates/clips/changelog/2026-06-26-desktop-recordings-recover-more-reliably-when-macos-reports-.md +6 -0
  31. package/corpus/templates/clips/changelog/2026-06-26-slack-unfurls-now-recognize-recording-dashboard-links-pasted.md +6 -0
  32. package/corpus/templates/clips/changelog/2026-06-26-storage-setup-now-explains-why-clips-needs-connected-storage.md +6 -0
  33. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  34. package/corpus/templates/clips/chrome-extension/src/background.ts +24 -3
  35. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +28 -2
  36. package/corpus/templates/clips/chrome-extension/src/popup.html +16 -0
  37. package/corpus/templates/clips/chrome-extension/src/popup.ts +59 -7
  38. package/corpus/templates/clips/chrome-extension/src/sentry.ts +28 -1
  39. package/corpus/templates/clips/chrome-extension/src/styles.css +74 -0
  40. package/corpus/templates/clips/desktop/src/app.tsx +85 -18
  41. package/corpus/templates/clips/desktop/src/styles.css +55 -0
  42. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +88 -51
  43. package/corpus/templates/clips/desktop/src-tauri/src/native_speech.rs +12 -3
  44. package/corpus/templates/clips/server/lib/slack-unfurls.ts +5 -2
  45. package/dist/agent/run-manager.d.ts.map +1 -1
  46. package/dist/agent/run-manager.js +12 -1
  47. package/dist/agent/run-manager.js.map +1 -1
  48. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  49. package/dist/client/agent-chat-adapter.js +15 -8
  50. package/dist/client/agent-chat-adapter.js.map +1 -1
  51. package/dist/client/session-replay.d.ts.map +1 -1
  52. package/dist/client/session-replay.js +55 -14
  53. package/dist/client/session-replay.js.map +1 -1
  54. package/dist/collab/awareness.d.ts +2 -2
  55. package/dist/collab/awareness.d.ts.map +1 -1
  56. package/dist/notifications/routes.d.ts +3 -3
  57. package/dist/observability/routes.d.ts +6 -6
  58. package/dist/onboarding/default-steps.js +3 -3
  59. package/dist/onboarding/default-steps.js.map +1 -1
  60. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  61. package/dist/server/agent-chat-plugin.js +69 -154
  62. package/dist/server/agent-chat-plugin.js.map +1 -1
  63. package/dist/server/agent-engine-api-key-route.d.ts +2 -2
  64. package/dist/server/agent-run-context.d.ts +35 -0
  65. package/dist/server/agent-run-context.d.ts.map +1 -0
  66. package/dist/server/agent-run-context.js +137 -0
  67. package/dist/server/agent-run-context.js.map +1 -0
  68. package/dist/server/credential-provider.d.ts.map +1 -1
  69. package/dist/server/credential-provider.js +15 -4
  70. package/dist/server/credential-provider.js.map +1 -1
  71. package/package.json +1 -1
  72. package/corpus/templates/.retired/README.md +0 -9
  73. package/corpus/templates/.retired/calls/package.json +0 -8
  74. package/corpus/templates/.retired/code/package.json +0 -8
  75. package/corpus/templates/.retired/contracts/package.json +0 -8
  76. package/corpus/templates/.retired/images/package.json +0 -8
  77. package/corpus/templates/.retired/issues/package.json +0 -8
  78. package/corpus/templates/.retired/meeting-notes/package.json +0 -8
  79. package/corpus/templates/.retired/migration/package.json +0 -8
  80. package/corpus/templates/.retired/recruiting/package.json +0 -8
  81. package/corpus/templates/.retired/scheduling/package.json +0 -8
  82. package/corpus/templates/.retired/visual-plans/package.json +0 -8
  83. package/corpus/templates/.retired/voice/package.json +0 -8
  84. package/corpus/templates/.retired/workbench/package.json +0 -8
@@ -0,0 +1,204 @@
1
+ import { createError, getHeader, type H3Event } from "h3";
2
+
3
+ import { resolveOrgIdForEmail, getOrgContext } from "../org/context.js";
4
+ import { getSession } from "./auth.js";
5
+ import {
6
+ runWithRequestContext,
7
+ type RequestContext,
8
+ } from "./request-context.js";
9
+
10
+ export type AgentRunOwnerContext = {
11
+ owner: string;
12
+ anonymous: boolean;
13
+ name?: string;
14
+ };
15
+
16
+ export const AGENT_RUN_OWNER_CONTEXT_KEY = "__agentNativeOwnerContext";
17
+
18
+ type EventWithAgentRunContext = H3Event & {
19
+ context?: Record<string, unknown>;
20
+ };
21
+
22
+ type AnonymousOwnerResolver = (
23
+ event: H3Event,
24
+ ) => string | null | Promise<string | null>;
25
+
26
+ type OrgIdResolver = (
27
+ event: H3Event,
28
+ ) => string | null | undefined | Promise<string | null | undefined>;
29
+
30
+ function eventContext(
31
+ event: EventWithAgentRunContext,
32
+ ): Record<string, unknown> {
33
+ event.context = event.context ?? {};
34
+ return event.context;
35
+ }
36
+
37
+ function normalizeId(value: string | null | undefined): string | undefined {
38
+ return typeof value === "string" && value.trim().length > 0
39
+ ? value.trim()
40
+ : undefined;
41
+ }
42
+
43
+ function readHeaderValue(event: any, name: string): unknown {
44
+ try {
45
+ const value = getHeader(event, name);
46
+ if (value !== undefined && value !== null) return value;
47
+ } catch {
48
+ // Unit tests and a few framework internals pass lightweight event shims.
49
+ }
50
+
51
+ const headers = event?.headers;
52
+ if (headers && typeof headers.get === "function") {
53
+ return headers.get(name) ?? headers.get(name.toLowerCase()) ?? undefined;
54
+ }
55
+
56
+ const reqHeaders = event?.node?.req?.headers ?? event?.req?.headers;
57
+ if (reqHeaders && typeof reqHeaders === "object") {
58
+ return reqHeaders[name] ?? reqHeaders[name.toLowerCase()];
59
+ }
60
+
61
+ return undefined;
62
+ }
63
+
64
+ export function readAgentRunTimezone(event: H3Event): string | undefined {
65
+ const raw = readHeaderValue(event, "x-user-timezone");
66
+ const value = Array.isArray(raw) ? raw[0] : raw;
67
+ return typeof value === "string" &&
68
+ value.trim().length > 0 &&
69
+ value.trim().length < 64
70
+ ? value.trim()
71
+ : undefined;
72
+ }
73
+
74
+ export function seedAgentRunOwnerContext(
75
+ event: H3Event,
76
+ ownerContext: AgentRunOwnerContext,
77
+ ): AgentRunOwnerContext {
78
+ eventContext(event as EventWithAgentRunContext)[AGENT_RUN_OWNER_CONTEXT_KEY] =
79
+ ownerContext;
80
+ return ownerContext;
81
+ }
82
+
83
+ export async function seedBackgroundAgentRunOwnerContext(
84
+ event: H3Event,
85
+ runId: string,
86
+ ): Promise<AgentRunOwnerContext | null> {
87
+ try {
88
+ const { getRunOwnerEmail } = await import("../agent/run-store.js");
89
+ const owner = await getRunOwnerEmail(runId);
90
+ if (!owner) return null;
91
+ return seedAgentRunOwnerContext(event, { owner, anonymous: false });
92
+ } catch {
93
+ return null;
94
+ }
95
+ }
96
+
97
+ export async function resolveAgentRunOwnerContext(
98
+ event: H3Event,
99
+ options: { anonymousOwner?: AnonymousOwnerResolver } = {},
100
+ ): Promise<AgentRunOwnerContext> {
101
+ const ctx = eventContext(event as EventWithAgentRunContext);
102
+ const seeded = ctx[AGENT_RUN_OWNER_CONTEXT_KEY] as
103
+ | AgentRunOwnerContext
104
+ | undefined;
105
+ if (seeded) return seeded;
106
+
107
+ const session = await getSession(event);
108
+ if (session?.email) {
109
+ return seedAgentRunOwnerContext(event, {
110
+ owner: session.email,
111
+ anonymous: false,
112
+ name: session.name,
113
+ });
114
+ }
115
+
116
+ const anonymousOwner = await options.anonymousOwner?.(event);
117
+ if (anonymousOwner) {
118
+ return seedAgentRunOwnerContext(event, {
119
+ owner: anonymousOwner,
120
+ anonymous: true,
121
+ });
122
+ }
123
+
124
+ throw createError({
125
+ statusCode: 401,
126
+ statusMessage: "Unauthenticated",
127
+ });
128
+ }
129
+
130
+ export async function resolveAgentRunOrgId(options: {
131
+ event: H3Event;
132
+ ownerContext: AgentRunOwnerContext;
133
+ resolveOrgId?: OrgIdResolver;
134
+ }): Promise<string | undefined> {
135
+ let resolvedOrgId: string | undefined;
136
+
137
+ if (options.resolveOrgId) {
138
+ resolvedOrgId = normalizeId(await options.resolveOrgId(options.event));
139
+ } else {
140
+ try {
141
+ const session = await getSession(options.event);
142
+ resolvedOrgId = normalizeId(session?.orgId);
143
+ } catch {
144
+ // Session not available.
145
+ }
146
+
147
+ if (!resolvedOrgId) {
148
+ try {
149
+ const orgContext = await getOrgContext(options.event);
150
+ resolvedOrgId = normalizeId(orgContext.orgId);
151
+ } catch {
152
+ // Org tables may not exist yet on first boot.
153
+ }
154
+ }
155
+ }
156
+
157
+ if (
158
+ !resolvedOrgId &&
159
+ options.ownerContext.owner &&
160
+ !options.ownerContext.anonymous
161
+ ) {
162
+ try {
163
+ resolvedOrgId = normalizeId(
164
+ await resolveOrgIdForEmail(options.ownerContext.owner),
165
+ );
166
+ } catch {
167
+ // Org tables may not exist yet on first boot.
168
+ }
169
+ }
170
+
171
+ return resolvedOrgId;
172
+ }
173
+
174
+ export async function resolveAgentRunRequestContext(options: {
175
+ event: H3Event;
176
+ ownerContext: AgentRunOwnerContext;
177
+ resolveOrgId?: OrgIdResolver;
178
+ isBackgroundWorker?: boolean;
179
+ }): Promise<RequestContext> {
180
+ const orgId = await resolveAgentRunOrgId(options);
181
+ const timezone = readAgentRunTimezone(options.event);
182
+ return {
183
+ userEmail: options.ownerContext.owner,
184
+ userName: options.ownerContext.name,
185
+ orgId,
186
+ timezone,
187
+ ...(options.isBackgroundWorker
188
+ ? { run: { isBackgroundWorker: true } }
189
+ : {}),
190
+ };
191
+ }
192
+
193
+ export async function runWithAgentRunContext<T>(
194
+ options: {
195
+ event: H3Event;
196
+ ownerContext: AgentRunOwnerContext;
197
+ resolveOrgId?: OrgIdResolver;
198
+ isBackgroundWorker?: boolean;
199
+ },
200
+ fn: () => T | Promise<T>,
201
+ ): Promise<T> {
202
+ const requestContext = await resolveAgentRunRequestContext(options);
203
+ return await runWithRequestContext(requestContext, fn);
204
+ }
@@ -80,7 +80,7 @@ export function readDeployCredentialEnv(key: string): string | undefined {
80
80
  * key does not silently power another tenant's chat.
81
81
  */
82
82
  export function isDeployCredentialFallbackAllowed(): boolean {
83
- if (process.env.NODE_ENV !== "production") return true;
83
+ if (!isProductionLikeRuntime()) return true;
84
84
  return isLocalDatabase();
85
85
  }
86
86
 
@@ -121,6 +121,22 @@ function isHostedWorkspaceRuntime(): boolean {
121
121
  );
122
122
  }
123
123
 
124
+ function isProductionLikeRuntime(): boolean {
125
+ return (
126
+ process.env.NODE_ENV === "production" ||
127
+ /^(1|true)$/i.test(process.env.NETLIFY ?? "") ||
128
+ /^(1|true)$/i.test(process.env.VERCEL ?? "") ||
129
+ /^(1|true)$/i.test(process.env.CF_PAGES ?? "") ||
130
+ Boolean(
131
+ process.env.AWS_LAMBDA_FUNCTION_NAME ||
132
+ process.env.AWS_EXECUTION_ENV ||
133
+ process.env.FUNCTIONS_WORKER_RUNTIME ||
134
+ process.env.K_SERVICE ||
135
+ process.env.RENDER,
136
+ )
137
+ );
138
+ }
139
+
124
140
  /**
125
141
  * Whether deployment-level Builder env keys may back the current request.
126
142
  *
@@ -143,18 +159,18 @@ function canUseBuilderDeployCredentialFallbackForRequest(): boolean {
143
159
  const email = getRequestUserEmail();
144
160
  if (!email) return true;
145
161
 
146
- const isProduction = process.env.NODE_ENV === "production";
162
+ const isProductionRuntime = isProductionLikeRuntime();
147
163
  // Local dogfooding escape hatch: lets the env / root-`.env` Builder key back
148
164
  // a signed-in user so running the app locally doesn't require completing the
149
165
  // Builder connect flow first. Non-prod only.
150
166
  const localDevOptIn =
151
- !isProduction &&
167
+ !isProductionRuntime &&
152
168
  /^(1|true)$/i.test(process.env.AGENT_NATIVE_LOCAL_BUILDER_ENV ?? "");
153
169
 
154
170
  if (isHostedWorkspaceRuntime() && !localDevOptIn) return false;
155
171
 
156
172
  // Deploy fallback is safe in non-prod or on a local/single-tenant database.
157
- return !isProduction || isLocalDatabase();
173
+ return !isProductionRuntime || isLocalDatabase();
158
174
  }
159
175
 
160
176
  function shouldTraceCredentialResolve(): boolean {
@@ -593,8 +593,18 @@ function ChartTooltip({
593
593
  left: number;
594
594
  top: number;
595
595
  } | null>(null);
596
- const items = sortTooltipPayloadItems(
597
- payload?.filter((item) => item.value != null && item.value !== "") ?? [],
596
+ const items = useMemo(
597
+ () =>
598
+ sortTooltipPayloadItems(
599
+ payload?.filter((item) => item.value != null && item.value !== "") ??
600
+ [],
601
+ ),
602
+ [payload],
603
+ );
604
+ const portalVisible = portalPosition !== null;
605
+ const clearPortalPosition = useCallback(
606
+ () => setPortalPosition((prev) => (prev === null ? prev : null)),
607
+ [],
598
608
  );
599
609
 
600
610
  const labelText =
@@ -606,7 +616,7 @@ function ChartTooltip({
606
616
 
607
617
  useBrowserLayoutEffect(() => {
608
618
  if (!active || items.length === 0 || typeof window === "undefined") {
609
- setPortalPosition(null);
619
+ clearPortalPosition();
610
620
  return;
611
621
  }
612
622
 
@@ -688,7 +698,7 @@ function ChartTooltip({
688
698
  window.removeEventListener("resize", schedule);
689
699
  window.removeEventListener("scroll", schedule, true);
690
700
  };
691
- });
701
+ }, [active, clearPortalPosition, items.length, portalVisible]);
692
702
 
693
703
  if (!active || items.length === 0) return null;
694
704
 
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-06-26
4
+ ---
5
+
6
+ Session replay uploads now accept compressed payloads so larger recordings ingest more reliably.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-06-26
4
+ ---
5
+
6
+ SQL chart tooltips now stay stable while moving across dense charts.
@@ -1,9 +1,11 @@
1
- import { readBody } from "@agent-native/core/server";
1
+ import { gunzipSync } from "node:zlib";
2
+
2
3
  import {
3
4
  defineEventHandler,
4
5
  getHeader,
5
6
  getQuery,
6
7
  getRouterParam,
8
+ readRawBody,
7
9
  setResponseHeader,
8
10
  setResponseStatus,
9
11
  } from "h3";
@@ -48,7 +50,7 @@ function setCors(event: any): void {
48
50
  setResponseHeader(
49
51
  event,
50
52
  "Access-Control-Allow-Headers",
51
- "content-type, x-agent-native-analytics-key",
53
+ "content-type, content-encoding, x-agent-native-analytics-key",
52
54
  );
53
55
  setResponseHeader(event, "Access-Control-Max-Age", "86400");
54
56
  }
@@ -76,15 +78,60 @@ function injectHeaderKey(body: unknown, headerKey?: string): unknown {
76
78
  return { publicKey: headerKey };
77
79
  }
78
80
 
79
- function requestByteLength(body: unknown): number {
80
- if (typeof body === "string") return Buffer.byteLength(body, "utf8");
81
+ function statusError(message: string, statusCode: number): Error {
82
+ return Object.assign(new Error(message), { statusCode });
83
+ }
84
+
85
+ export function decodeSessionReplayRequestBody(
86
+ rawBody: Buffer | Uint8Array | string | undefined,
87
+ contentEncoding?: string | null,
88
+ ): { body: unknown; requestBytes: number } {
89
+ const requestBytes =
90
+ typeof rawBody === "string"
91
+ ? Buffer.byteLength(rawBody, "utf8")
92
+ : (rawBody?.byteLength ?? 0);
93
+ const bytes = Buffer.isBuffer(rawBody)
94
+ ? rawBody
95
+ : rawBody instanceof Uint8Array
96
+ ? Buffer.from(rawBody)
97
+ : Buffer.from(rawBody ?? "", "utf8");
98
+ const encoding = (contentEncoding ?? "").split(";")[0]?.trim().toLowerCase();
99
+ let decoded = bytes;
100
+
101
+ if (encoding === "gzip" || encoding === "x-gzip") {
102
+ try {
103
+ decoded = gunzipSync(bytes);
104
+ } catch {
105
+ throw statusError("Invalid gzip-compressed replay body", 400);
106
+ }
107
+ } else if (encoding && encoding !== "identity") {
108
+ throw statusError(
109
+ `Unsupported replay request content-encoding: ${encoding}`,
110
+ 415,
111
+ );
112
+ }
113
+
114
+ const text = decoded.toString("utf8");
115
+ if (!text.trim()) {
116
+ return { body: undefined, requestBytes };
117
+ }
81
118
  try {
82
- return Buffer.byteLength(JSON.stringify(body), "utf8");
119
+ return { body: JSON.parse(text), requestBytes };
83
120
  } catch {
84
- return 0;
121
+ return { body: text, requestBytes };
85
122
  }
86
123
  }
87
124
 
125
+ async function readSessionReplayRequestBody(
126
+ event: any,
127
+ ): Promise<{ body: unknown; requestBytes: number }> {
128
+ const rawBody = await readRawBody(event, false);
129
+ return decodeSessionReplayRequestBody(
130
+ rawBody,
131
+ getHeader(event, "content-encoding"),
132
+ );
133
+ }
134
+
88
135
  function asString(value: unknown): string | undefined {
89
136
  if (typeof value === "string" && value.trim()) return value.trim();
90
137
  if (Array.isArray(value) && typeof value[0] === "string" && value[0].trim()) {
@@ -153,12 +200,13 @@ export const handleSessionReplayIngest = defineEventHandler(async (event) => {
153
200
  }
154
201
 
155
202
  const headerKey = getHeader(event, "x-agent-native-analytics-key");
156
- const rawBody = await readBody(event);
203
+ const { body: rawBody, requestBytes } =
204
+ await readSessionReplayRequestBody(event);
157
205
  const body = injectHeaderKey(rawBody, headerKey);
158
206
  const parsed = parseSessionReplayIngestPayload(body);
159
207
  const result = await recordSessionReplayChunks(parsed, {
160
208
  origin: readOrigin(event),
161
- requestBytes: requestByteLength(rawBody),
209
+ requestBytes,
162
210
  });
163
211
  setResponseStatus(event, 202);
164
212
  return { success: true, ...result };
@@ -585,7 +585,7 @@ function TranscriptSetupCard({
585
585
  <p className="text-xs font-semibold">
586
586
  {builderConfigured
587
587
  ? "Builder.io connected"
588
- : "Connect Builder.io"}
588
+ : "Use Builder.io (free)"}
589
589
  </p>
590
590
  {!builderConfigured && (
591
591
  <span className="rounded-sm bg-primary/10 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-primary">
@@ -13,6 +13,13 @@ import {
13
13
  } from "@tabler/icons-react";
14
14
  import { useCallback, useEffect, useRef, useState } from "react";
15
15
 
16
+ import {
17
+ Tooltip,
18
+ TooltipContent,
19
+ TooltipProvider,
20
+ TooltipTrigger,
21
+ } from "@/components/ui/tooltip";
22
+
16
23
  function BuilderBMark({ className }: { className?: string }) {
17
24
  return (
18
25
  <svg
@@ -173,23 +180,44 @@ export function StorageSetupCard({
173
180
 
174
181
  {/* S3 — secondary option */}
175
182
  {!connected && (
176
- <div className="flex items-center gap-3 border-t border-border pt-4">
177
- <IconServer className="h-4 w-4 text-muted-foreground" />
178
- <div className="flex-1">
179
- <span className="text-sm text-muted-foreground">
180
- Or{" "}
181
- <a
182
- href={appPath("/settings#video-storage")}
183
- className="font-medium text-foreground underline underline-offset-2 hover:text-foreground/80"
184
- >
185
- {t("storageSetup.configureS3")}
186
- </a>
187
- </span>
188
- <span className="mt-0.5 block text-[11px] text-muted-foreground">
189
- AWS S3, Cloudflare R2, DigitalOcean Spaces, MinIO
190
- </span>
183
+ <>
184
+ <div className="flex items-center gap-3 border-t border-border pt-4">
185
+ <IconServer className="h-4 w-4 text-muted-foreground" />
186
+ <div className="flex-1">
187
+ <span className="text-sm text-muted-foreground">
188
+ Or{" "}
189
+ <a
190
+ href={appPath("/settings#video-storage")}
191
+ className="font-medium text-foreground underline underline-offset-2 hover:text-foreground/80"
192
+ >
193
+ {t("storageSetup.configureS3")}
194
+ </a>
195
+ </span>
196
+ <span className="mt-0.5 block text-[11px] text-muted-foreground">
197
+ AWS S3, Cloudflare R2, DigitalOcean Spaces, MinIO
198
+ </span>
199
+ </div>
191
200
  </div>
192
- </div>
201
+ <TooltipProvider delayDuration={150}>
202
+ <Tooltip>
203
+ <TooltipTrigger asChild>
204
+ <button
205
+ type="button"
206
+ className="self-start text-xs text-muted-foreground underline decoration-muted-foreground/50 decoration-dotted underline-offset-4 transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
207
+ >
208
+ {t("storageSetup.whyPrompt")}
209
+ </button>
210
+ </TooltipTrigger>
211
+ <TooltipContent
212
+ side="bottom"
213
+ align="start"
214
+ className="max-w-80 whitespace-normal text-xs leading-relaxed"
215
+ >
216
+ {t("storageSetup.whyDescription")}
217
+ </TooltipContent>
218
+ </Tooltip>
219
+ </TooltipProvider>
220
+ </>
193
221
  )}
194
222
  </div>
195
223
  );
@@ -344,7 +344,7 @@ const messages = {
344
344
  cleanupBuilderFailed:
345
345
  "تعذر إنهاء عملية التنظيف على الرغم من اتصال Builder.io. تم الاحتفاظ بالنص الأصلي.",
346
346
  cleanupPaused:
347
- "تم إيقاف عملية التنظيف مؤقتًا. قم بتوصيل Builder.io في الإعدادات لتمكينه.",
347
+ "التنظيف متوقف مؤقتًا. صِل الذكاء الاصطناعي من الإعدادات: Builder.io (أرصدة مجانية) أو مفتاح LLM الخاص بك.",
348
348
  builderNoResponse:
349
349
  "لم أتلق ردًا من Builder. اسمح بالنوافذ المنبثقة وحاول مرة أخرى.",
350
350
  saveFailed: "فشل الحفظ ({{status}})",
@@ -584,7 +584,7 @@ const messages = {
584
584
  "Builder.io هو مسار التخزين الأساسي لعمليات رفع Clips. يتوفر S3 عندما تحتاج إلى استخدام حاويتك الخاصة.",
585
585
  checkingBuilder: "جار فحص Builder.io",
586
586
  builderConnected: "Builder.io متصل",
587
- connectBuilder: "اتصال Builder.io",
587
+ connectBuilder: "استخدام Builder.io (مجاني)",
588
588
  builderConnectedFor: "يتم استخدام Builder.io لـ {{orgName}}.",
589
589
  builderConnectedGeneric: "تستخدم المقاطع الجديدة مزود Builder.io المتصل.",
590
590
  builderIncludes:
@@ -610,12 +610,12 @@ const messages = {
610
610
  s3PublicBaseUrlLabel: "عنوان URL الأساسي العام",
611
611
  apiSetup: "إعداد الذكاء الاصطناعي",
612
612
  apiSetupDescription:
613
- "Builder.io هو المسار الافتراضي لاعتمادات الذكاء الاصطناعي المُدارة. مفاتيح المزود اختيارية ويمكن إضافتها هنا.",
614
- builderEasySetup: "Builder.io هو أسهل إعداد",
613
+ "صِل الذكاء الاصطناعي باستخدام أرصدة Builder.io المجانية أو مفاتيح LLM الخاصة بك.",
614
+ builderEasySetup: "أرصدة Builder.io مجانية",
615
615
  builderAiAvailable:
616
616
  "تتوفر اعتمادات الذكاء الاصطناعي المضمنة والنسخ المُدار لـ Clips.",
617
617
  builderAiDescription:
618
- "اتصل بـ Builder أولًا لاستخدام اعتمادات الذكاء الاصطناعي المضمنة وتخزين الكائنات والرفع والنسخ المُدار.",
618
+ "استخدم Builder.io أولًا لاعتمادات الذكاء الاصطناعي المضمنة وتخزين الكائنات والرفع والنسخ المُدار.",
619
619
  providerKeyTitle: "استخدام مفتاح مزود خاص بك",
620
620
  providerKeyDescription:
621
621
  "أضف مفاتيح Anthropic أو OpenAI أو Gemini أو Groq أو OpenRouter للاستخدام المفوتر من المزود.",
@@ -762,7 +762,7 @@ const messages = {
762
762
  pickAtLeastTwo: "اختر تسجيلين على الأقل لدمجهما",
763
763
  videoUrlMissing: "واحد أو أكثر من التسجيلات لا يحتوي بعد على رابط فيديو جاهز",
764
764
  connectStorage:
765
- "قم بتوصيل Builder.io أو تخزين متوافق مع S3 قبل دمج التسجيلات.",
765
+ "صِل التخزين قبل دمج التسجيلات: Builder.io (تخزين + ذكاء اصطناعي في الخطة المجانية) أو تخزين متوافق مع S3.",
766
766
  created: "تم إنشاء التسجيل المدمج",
767
767
  failed: "فشل دمج التسجيلات",
768
768
  noOtherRecordings: "لا توجد تسجيلات أخرى متاحة.",
@@ -988,9 +988,12 @@ const messages = {
988
988
  "لم يصل رد من Builder خلال 5 دقائق. تحقق من النافذة المنبثقة وحاول مرة أخرى.",
989
989
  builderConnected: "Builder.io متصل",
990
990
  waitingForBuilder: "بانتظار Builder...",
991
- connectBuilder: "اتصال بـ Builder.io",
991
+ connectBuilder: "استخدام Builder.io (مجاني)",
992
992
  free: "مجاني",
993
993
  configureS3: "تكوين تخزين متوافق مع S3",
994
+ whyPrompt: "لماذا أرى هذا؟",
995
+ whyDescription:
996
+ "Clips تطبيق مجاني ومفتوح المصدر 100%، لذلك تحتاج إلى توصيل طريقة لتخزين المقاطع. صِل التخزين عبر Builder.io لتخزين وذكاء اصطناعي ضمن الخطة المجانية، أو استخدم تخزين كائنات متوافقًا مع S3 ومفاتيح LLM الخاصة بك.",
994
997
  },
995
998
  captureInstall: {
996
999
  title: "Choose your recorder (مترجم)",
@@ -1193,9 +1196,9 @@ const messages = {
1193
1196
  downloadRecording: "Download recording (مترجم)",
1194
1197
  openRecorderInTab: "Open recorder in tab (مترجم)",
1195
1198
  connectStorageToFinish:
1196
- "Connect Builder.io or S3 storage on the next screen and Clips will finish saving it. (مترجم)",
1199
+ "صِل التخزين في الشاشة التالية: Builder.io (تخزين + ذكاء اصطناعي في الخطة المجانية) أو تخزين متوافق مع S3. سيكمل Clips الحفظ.",
1197
1200
  connectStorageToRetryLoom:
1198
- "Connect Builder.io or S3 storage on the next screen and Clips will retry the import. (مترجم)",
1201
+ "صِل التخزين في الشاشة التالية: Builder.io (تخزين + ذكاء اصطناعي في الخطة المجانية) أو تخزين متوافق مع S3. سيعيد Clips محاولة الاستيراد.",
1199
1202
  },
1200
1203
  dictateRoute: {
1201
1204
  pageTitle: "Dictate · Clips (مترجم)",
@@ -359,7 +359,7 @@ const messages = {
359
359
  cleanupBuilderFailed:
360
360
  "Die Bereinigung konnte nicht abgeschlossen werden, obwohl Builder.io verbunden ist. Das native Transkript wurde aufbewahrt.",
361
361
  cleanupPaused:
362
- "Die Bereinigung ist pausiert. Verbinden Sie Builder.io in den Einstellungen, um es zu aktivieren.",
362
+ "Die Bereinigung ist pausiert. Verbinde KI in den Einstellungen: Builder.io (kostenlose Credits) oder deinen eigenen LLM-Schlüssel.",
363
363
  builderNoResponse:
364
364
  "Ich habe keine Antwort von Builder erhalten. Lassen Sie Popups zu und versuchen Sie es erneut.",
365
365
  saveFailed: "Speichern fehlgeschlagen ({{status}})",
@@ -607,7 +607,7 @@ Alle sichtbaren Änderungen für Clips-Nutzer werden hier dokumentiert. Du kanns
607
607
  "Builder.io ist der primäre Speicherpfad für Clips-Uploads. S3 ist verfügbar, wenn du deinen eigenen Bucket verwenden musst.",
608
608
  checkingBuilder: "Builder.io wird geprüft",
609
609
  builderConnected: "Builder.io verbunden",
610
- connectBuilder: "Builder.io verbinden",
610
+ connectBuilder: "Builder.io nutzen (kostenlos)",
611
611
  builderConnectedFor: "Builder.io wird für {{orgName}} verwendet.",
612
612
  builderConnectedGeneric:
613
613
  "Neue Clips verwenden den verbundenen Builder.io-Anbieter.",
@@ -635,12 +635,12 @@ Alle sichtbaren Änderungen für Clips-Nutzer werden hier dokumentiert. Du kanns
635
635
  s3PublicBaseUrlLabel: "Öffentliche Basis-URL",
636
636
  apiSetup: "KI-Einrichtung",
637
637
  apiSetupDescription:
638
- "Builder.io ist der Standardpfad für verwaltete KI-Guthaben. Anbieter-Schlüssel sind optional und können hier hinzugefügt werden.",
639
- builderEasySetup: "Builder.io ist die einfachste Einrichtung",
638
+ "Verbinde KI mit kostenlosen Builder.io-Credits oder deinen eigenen LLM-Schlüsseln.",
639
+ builderEasySetup: "Kostenlose Builder.io-Credits",
640
640
  builderAiAvailable:
641
641
  "Enthaltene KI-Guthaben und verwaltete Transkription sind für Clips verfügbar.",
642
642
  builderAiDescription:
643
- "Verbinde zuerst Builder, um enthaltene KI-Guthaben, Objektspeicher, Uploads und verwaltete Transkription zu nutzen.",
643
+ "Nutze Builder.io zuerst für enthaltene KI-Guthaben, Objektspeicher, Uploads und verwaltete Transkription.",
644
644
  providerKeyTitle: "Eigenen Anbieter-Schlüssel verwenden",
645
645
  providerKeyDescription:
646
646
  "Füge Anthropic-, OpenAI-, Gemini-, Groq- oder OpenRouter-Schlüssel für anbieterseitig abgerechnete Nutzung hinzu.",
@@ -787,7 +787,7 @@ Alle sichtbaren Änderungen für Clips-Nutzer werden hier dokumentiert. Du kanns
787
787
  videoUrlMissing:
788
788
  "Eine oder mehrere Aufzeichnungen haben noch keine fertige Video-URL",
789
789
  connectStorage:
790
- "Verbinde Builder.io oder S3-kompatiblen Speicher, bevor du Aufzeichnungen zusammenfügst.",
790
+ "Verbinde Speicher vor dem Zusammenfügen von Aufzeichnungen: Builder.io (Speicher + KI im kostenlosen Tarif) oder S3-kompatibler Speicher.",
791
791
  created: "Zusammengefügte Aufzeichnung erstellt",
792
792
  failed: "Aufzeichnungen konnten nicht zusammengefügt werden",
793
793
  noOtherRecordings: "Keine anderen Aufzeichnungen verfügbar.",
@@ -1018,9 +1018,12 @@ Alle sichtbaren Änderungen für Clips-Nutzer werden hier dokumentiert. Du kanns
1018
1018
  "Nach 5 Minuten kam keine Antwort von Builder. Prüfe das Popup und versuche es erneut.",
1019
1019
  builderConnected: "Builder.io verbunden",
1020
1020
  waitingForBuilder: "Warten auf Builder...",
1021
- connectBuilder: "Builder.io verbinden",
1021
+ connectBuilder: "Builder.io nutzen (kostenlos)",
1022
1022
  free: "Kostenlos",
1023
1023
  configureS3: "S3-kompatiblen Speicher konfigurieren",
1024
+ whyPrompt: "Warum sehe ich das?",
1025
+ whyDescription:
1026
+ "Clips ist 100 % kostenlos und Open Source, deshalb musst du eine Möglichkeit zum Speichern deiner Clips verbinden. Verbinde Speicher mit Builder.io für Speicher und KI im kostenlosen Tarif, oder nutze S3-kompatiblen Objektspeicher und deine eigenen LLM-Schlüssel.",
1024
1027
  },
1025
1028
  captureInstall: {
1026
1029
  title: "Choose your recorder (Lokalisiert)",
@@ -1223,9 +1226,9 @@ Alle sichtbaren Änderungen für Clips-Nutzer werden hier dokumentiert. Du kanns
1223
1226
  downloadRecording: "Download recording (Lokalisiert)",
1224
1227
  openRecorderInTab: "Open recorder in tab (Lokalisiert)",
1225
1228
  connectStorageToFinish:
1226
- "Connect Builder.io or S3 storage on the next screen and Clips will finish saving it. (Lokalisiert)",
1229
+ "Verbinde Speicher im nächsten Bildschirm: Builder.io (Speicher + KI im kostenlosen Tarif) oder S3-kompatibler Speicher. Clips schließt das Speichern ab.",
1227
1230
  connectStorageToRetryLoom:
1228
- "Connect Builder.io or S3 storage on the next screen and Clips will retry the import. (Lokalisiert)",
1231
+ "Verbinde Speicher im nächsten Bildschirm: Builder.io (Speicher + KI im kostenlosen Tarif) oder S3-kompatibler Speicher. Clips versucht den Import erneut.",
1229
1232
  },
1230
1233
  dictateRoute: {
1231
1234
  pageTitle: "Dictate · Clips (Lokalisiert)",