@agent-native/core 0.79.6 → 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.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +19 -0
- package/corpus/core/docs/content/locales/zh-TW/durable-background-runs.md +74 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/production-agent.ts +0 -93
- package/corpus/core/src/agent/run-manager.ts +9 -1
- package/corpus/core/src/client/agent-chat-adapter.ts +24 -16
- package/corpus/core/src/client/session-replay.ts +67 -13
- package/corpus/core/src/onboarding/default-steps.ts +3 -3
- package/corpus/core/src/server/agent-chat-plugin.ts +82 -162
- package/corpus/core/src/server/agent-run-context.ts +204 -0
- package/corpus/core/src/server/credential-provider.ts +20 -4
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +14 -4
- package/corpus/templates/analytics/changelog/2026-06-26-session-replay-uploads-now-accept-compressed-payloads-so-lar.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-26-sql-chart-tooltips-now-stay-stable-while-moving-across-dense.md +6 -0
- package/corpus/templates/analytics/server/handlers/session-replay.ts +56 -8
- package/corpus/templates/clips/app/components/player/transcript-panel.tsx +1 -1
- package/corpus/templates/clips/app/components/recorder/storage-setup-card.tsx +44 -16
- package/corpus/templates/clips/app/i18n/ar-SA.ts +12 -9
- package/corpus/templates/clips/app/i18n/de-DE.ts +12 -9
- package/corpus/templates/clips/app/i18n/en-US.ts +19 -16
- package/corpus/templates/clips/app/i18n/es-ES.ts +12 -9
- package/corpus/templates/clips/app/i18n/fr-FR.ts +12 -9
- package/corpus/templates/clips/app/i18n/hi-IN.ts +13 -9
- package/corpus/templates/clips/app/i18n/ja-JP.ts +12 -9
- package/corpus/templates/clips/app/i18n/ko-KR.ts +12 -9
- package/corpus/templates/clips/app/i18n/pt-BR.ts +12 -9
- package/corpus/templates/clips/app/i18n/zh-CN.ts +19 -14
- package/corpus/templates/clips/app/i18n/zh-TW.ts +19 -14
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +7 -0
- package/corpus/templates/clips/app/routes/record.tsx +2 -2
- package/corpus/templates/clips/changelog/2026-06-26-desktop-recordings-recover-more-reliably-when-macos-reports-.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-26-slack-unfurls-now-recognize-recording-dashboard-links-pasted.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-26-storage-setup-now-explains-why-clips-needs-connected-storage.md +6 -0
- package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
- package/corpus/templates/clips/chrome-extension/src/background.ts +24 -3
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +28 -2
- package/corpus/templates/clips/chrome-extension/src/popup.html +16 -0
- package/corpus/templates/clips/chrome-extension/src/popup.ts +59 -7
- package/corpus/templates/clips/chrome-extension/src/sentry.ts +28 -1
- package/corpus/templates/clips/chrome-extension/src/styles.css +74 -0
- package/corpus/templates/clips/desktop/src/app.tsx +85 -18
- package/corpus/templates/clips/desktop/src/styles.css +55 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +88 -51
- package/corpus/templates/clips/desktop/src-tauri/src/native_speech.rs +12 -3
- package/corpus/templates/clips/server/lib/slack-unfurls.ts +5 -2
- package/corpus/templates/slides/actions/_app-url.ts +1 -1
- package/corpus/templates/slides/changelog/2026-06-26-deck-links-now-use-the-workspace-gateway-when-available-so-g.md +6 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +0 -79
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-manager.d.ts.map +1 -1
- package/dist/agent/run-manager.js +12 -1
- package/dist/agent/run-manager.js.map +1 -1
- package/dist/client/agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/agent-chat-adapter.js +15 -8
- package/dist/client/agent-chat-adapter.js.map +1 -1
- package/dist/client/session-replay.d.ts.map +1 -1
- package/dist/client/session-replay.js +55 -14
- package/dist/client/session-replay.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/routes.d.ts +2 -2
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +7 -7
- package/dist/onboarding/default-steps.js +3 -3
- package/dist/onboarding/default-steps.js.map +1 -1
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +69 -154
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +2 -2
- package/dist/server/agent-run-context.d.ts +35 -0
- package/dist/server/agent-run-context.d.ts.map +1 -0
- package/dist/server/agent-run-context.js +137 -0
- package/dist/server/agent-run-context.js.map +1 -0
- package/dist/server/credential-provider.d.ts.map +1 -1
- package/dist/server/credential-provider.js +15 -4
- package/dist/server/credential-provider.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/docs/content/locales/zh-TW/durable-background-runs.md +74 -0
- package/package.json +1 -1
- package/corpus/templates/.retired/README.md +0 -9
- package/corpus/templates/.retired/calls/package.json +0 -8
- package/corpus/templates/.retired/code/package.json +0 -8
- package/corpus/templates/.retired/contracts/package.json +0 -8
- package/corpus/templates/.retired/images/package.json +0 -8
- package/corpus/templates/.retired/issues/package.json +0 -8
- package/corpus/templates/.retired/meeting-notes/package.json +0 -8
- package/corpus/templates/.retired/migration/package.json +0 -8
- package/corpus/templates/.retired/recruiting/package.json +0 -8
- package/corpus/templates/.retired/scheduling/package.json +0 -8
- package/corpus/templates/.retired/visual-plans/package.json +0 -8
- package/corpus/templates/.retired/voice/package.json +0 -8
- package/corpus/templates/.retired/workbench/package.json +0 -8
|
@@ -23,6 +23,15 @@ import { captureExtensionError, initExtensionSentry } from "./sentry";
|
|
|
23
23
|
|
|
24
24
|
initExtensionSentry("offscreen");
|
|
25
25
|
|
|
26
|
+
const STORAGE_SETUP_REQUIRED_MESSAGE =
|
|
27
|
+
"Connect storage to finish saving this clip: Builder.io (free tier storage + AI) or S3-compatible storage.";
|
|
28
|
+
const STORAGE_SETUP_FAILURE_RE =
|
|
29
|
+
/video storage is not connected|no video storage configured|file upload provider|storage provider|connect builder|s3-compatible/i;
|
|
30
|
+
|
|
31
|
+
function isStorageSetupFailureMessage(message: string | null | undefined) {
|
|
32
|
+
return STORAGE_SETUP_FAILURE_RE.test(message ?? "");
|
|
33
|
+
}
|
|
34
|
+
|
|
26
35
|
type CaptureMode = "screen" | "camera";
|
|
27
36
|
|
|
28
37
|
type AcquireMessage = {
|
|
@@ -396,7 +405,14 @@ async function uploadChunk(
|
|
|
396
405
|
body,
|
|
397
406
|
});
|
|
398
407
|
const text = await res.text().catch(() => "");
|
|
399
|
-
|
|
408
|
+
let data: UploadResult = {};
|
|
409
|
+
if (text) {
|
|
410
|
+
try {
|
|
411
|
+
data = JSON.parse(text) as UploadResult;
|
|
412
|
+
} catch {
|
|
413
|
+
data = {};
|
|
414
|
+
}
|
|
415
|
+
}
|
|
400
416
|
if (!res.ok) {
|
|
401
417
|
console.error(
|
|
402
418
|
"[clips-offscreen] chunk upload failed",
|
|
@@ -405,9 +421,17 @@ async function uploadChunk(
|
|
|
405
421
|
Boolean(recording.authToken),
|
|
406
422
|
text.slice(0, 200),
|
|
407
423
|
);
|
|
424
|
+
const storageSetupRequired =
|
|
425
|
+
data?.storageSetupRequired === true ||
|
|
426
|
+
isStorageSetupFailureMessage(data?.error || text);
|
|
408
427
|
const error = new Error(
|
|
409
|
-
|
|
428
|
+
storageSetupRequired
|
|
429
|
+
? STORAGE_SETUP_REQUIRED_MESSAGE
|
|
430
|
+
: data?.error ||
|
|
431
|
+
`Upload failed (${res.status}): ${text || res.statusText}`,
|
|
410
432
|
);
|
|
433
|
+
(error as { storageSetupRequired?: boolean }).storageSetupRequired =
|
|
434
|
+
storageSetupRequired;
|
|
411
435
|
captureExtensionError(error, {
|
|
412
436
|
tags: {
|
|
413
437
|
surface: "offscreen",
|
|
@@ -845,6 +869,8 @@ async function finalizeStop(recording: ActiveRecording): Promise<void> {
|
|
|
845
869
|
reportStatus(recording.sessionId, "error", {
|
|
846
870
|
recordingId: recording.recordingId,
|
|
847
871
|
error: error.message,
|
|
872
|
+
storageSetupRequired: (error as { storageSetupRequired?: boolean })
|
|
873
|
+
.storageSetupRequired,
|
|
848
874
|
});
|
|
849
875
|
recording.rejectStopped(error);
|
|
850
876
|
}
|
|
@@ -405,6 +405,22 @@
|
|
|
405
405
|
|
|
406
406
|
<span id="status" class="status" role="status" aria-live="polite"></span>
|
|
407
407
|
|
|
408
|
+
<div id="storage-help" class="storage-help" hidden>
|
|
409
|
+
<button id="storage-help-open" class="storage-help-open" type="button">
|
|
410
|
+
Use Builder.io (free)
|
|
411
|
+
</button>
|
|
412
|
+
<span aria-hidden="true">·</span>
|
|
413
|
+
<span class="storage-help-why" tabindex="0">
|
|
414
|
+
Why am I seeing this?
|
|
415
|
+
<span class="storage-help-details" role="tooltip">
|
|
416
|
+
Clips is 100% free and open source, so you need to hook up a way to
|
|
417
|
+
store your clips. Connect storage with Builder.io for free-tier
|
|
418
|
+
storage and AI, or use S3-compatible object storage and your own LLM
|
|
419
|
+
keys.
|
|
420
|
+
</span>
|
|
421
|
+
</span>
|
|
422
|
+
</div>
|
|
423
|
+
|
|
408
424
|
<nav class="bottom-row" aria-label="Clips shortcuts">
|
|
409
425
|
<button id="open-library" class="bottom-button" type="button">
|
|
410
426
|
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
@@ -93,9 +93,17 @@ const FEEDBACK_URL =
|
|
|
93
93
|
const FEEDBACK_PLACEHOLDER = "Tell us what's on your mind...";
|
|
94
94
|
const FEEDBACK_SUBMIT_TEXT = "Send feedback";
|
|
95
95
|
const FEEDBACK_SUCCESS_MESSAGE = "Thanks for the feedback!";
|
|
96
|
+
const STORAGE_SETUP_REQUIRED_MESSAGE =
|
|
97
|
+
"Connect storage to finish saving this clip: Builder.io (free tier storage + AI) or S3-compatible storage.";
|
|
98
|
+
const STORAGE_SETUP_FAILURE_RE =
|
|
99
|
+
/video storage is not connected|no video storage configured|file upload provider|storage provider|connect builder|s3-compatible/i;
|
|
96
100
|
const feedbackTarget = parseFeedbackTarget(FEEDBACK_URL);
|
|
97
101
|
const feedbackSchemaCache = new Map<string, Promise<FeedbackFormSchema>>();
|
|
98
102
|
|
|
103
|
+
function isStorageSetupFailureMessage(message: string | null | undefined) {
|
|
104
|
+
return STORAGE_SETUP_FAILURE_RE.test(message ?? "");
|
|
105
|
+
}
|
|
106
|
+
|
|
99
107
|
function screenSurface(
|
|
100
108
|
value: CaptureSurface,
|
|
101
109
|
): Exclude<CaptureSurface, "camera"> {
|
|
@@ -507,6 +515,14 @@ function setStatus(message: string, kind: "info" | "error" = "info"): void {
|
|
|
507
515
|
status.dataset.kind = kind;
|
|
508
516
|
}
|
|
509
517
|
|
|
518
|
+
function setStorageHelp(visible: boolean): void {
|
|
519
|
+
byId<HTMLDivElement>("storage-help").hidden = !visible;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function storageSetupUrl(settings: ExtensionSettings): string {
|
|
523
|
+
return `${settings.clipsBaseUrl.replace(/\/+$/, "")}/record`;
|
|
524
|
+
}
|
|
525
|
+
|
|
510
526
|
function renderMode(settings: ExtensionSettings): void {
|
|
511
527
|
const mode = recordingMode(settings);
|
|
512
528
|
for (const button of document.querySelectorAll<HTMLButtonElement>(
|
|
@@ -690,7 +706,10 @@ function renderActiveRecording(recording: NativeRecording | null): void {
|
|
|
690
706
|
start.hidden = active;
|
|
691
707
|
signIn.hidden = true;
|
|
692
708
|
if (recordingActions) recordingActions.hidden = !active;
|
|
693
|
-
if (!recording)
|
|
709
|
+
if (!recording) {
|
|
710
|
+
setStorageHelp(false);
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
694
713
|
|
|
695
714
|
recordingTitle.textContent = recording.targetTitle || "Current recording";
|
|
696
715
|
const host = hostnameLabel(recording.targetUrl);
|
|
@@ -706,10 +725,16 @@ function renderActiveRecording(recording: NativeRecording | null): void {
|
|
|
706
725
|
: recording.status === "stopping"
|
|
707
726
|
? "Stopping..."
|
|
708
727
|
: recording.status === "error"
|
|
709
|
-
? recording.error
|
|
728
|
+
? isStorageSetupFailureMessage(recording.error)
|
|
729
|
+
? STORAGE_SETUP_REQUIRED_MESSAGE
|
|
730
|
+
: recording.error || "Recording needs attention"
|
|
710
731
|
: `Recording ${formatDuration(recording.startedAtMs)}`;
|
|
711
732
|
recordingStatus.dataset.kind =
|
|
712
733
|
recording.status === "error" ? "error" : "info";
|
|
734
|
+
setStorageHelp(
|
|
735
|
+
recording.status === "error" &&
|
|
736
|
+
isStorageSetupFailureMessage(recording.error),
|
|
737
|
+
);
|
|
713
738
|
}
|
|
714
739
|
|
|
715
740
|
async function init(): Promise<void> {
|
|
@@ -751,6 +776,7 @@ async function init(): Promise<void> {
|
|
|
751
776
|
const openSettings = byId<HTMLButtonElement>("open-settings");
|
|
752
777
|
const openRecent = byId<HTMLButtonElement>("open-recent");
|
|
753
778
|
const signIn = byId<HTMLButtonElement>("sign-in");
|
|
779
|
+
const storageHelpOpen = byId<HTMLButtonElement>("storage-help-open");
|
|
754
780
|
let activeRecording: NativeRecording | null = null;
|
|
755
781
|
let authStatus: AuthStatus = "checking";
|
|
756
782
|
let feedbackOpenedAt = 0;
|
|
@@ -1071,6 +1097,7 @@ async function init(): Promise<void> {
|
|
|
1071
1097
|
start.addEventListener("click", async () => {
|
|
1072
1098
|
start.disabled = true;
|
|
1073
1099
|
signIn.hidden = true;
|
|
1100
|
+
setStorageHelp(false);
|
|
1074
1101
|
setStatus(""); // no chatty "Checking…/Starting…" text — the disabled button is enough
|
|
1075
1102
|
try {
|
|
1076
1103
|
authStatus = await readAuthStatus(settings);
|
|
@@ -1085,10 +1112,11 @@ async function init(): Promise<void> {
|
|
|
1085
1112
|
if (!storageConfigured) {
|
|
1086
1113
|
start.disabled = false;
|
|
1087
1114
|
setStatus(
|
|
1088
|
-
"Connect Builder.io
|
|
1115
|
+
"Connect storage in Clips first: Builder.io (free tier storage + AI) or S3-compatible storage.",
|
|
1089
1116
|
"error",
|
|
1090
1117
|
);
|
|
1091
|
-
|
|
1118
|
+
setStorageHelp(true);
|
|
1119
|
+
await createTab(storageSetupUrl(settings));
|
|
1092
1120
|
window.close();
|
|
1093
1121
|
return;
|
|
1094
1122
|
}
|
|
@@ -1117,7 +1145,14 @@ async function init(): Promise<void> {
|
|
|
1117
1145
|
setStatus("");
|
|
1118
1146
|
return;
|
|
1119
1147
|
}
|
|
1120
|
-
|
|
1148
|
+
const storageSetupFailure = isStorageSetupFailureMessage(message);
|
|
1149
|
+
setStorageHelp(storageSetupFailure);
|
|
1150
|
+
setStatus(
|
|
1151
|
+
storageSetupFailure
|
|
1152
|
+
? "Connect storage in Clips first: Builder.io (free tier storage + AI) or S3-compatible storage."
|
|
1153
|
+
: message,
|
|
1154
|
+
"error",
|
|
1155
|
+
);
|
|
1121
1156
|
} catch (err) {
|
|
1122
1157
|
captureExtensionError(err, {
|
|
1123
1158
|
tags: { surface: "popup", action: "start-recording" },
|
|
@@ -1128,13 +1163,24 @@ async function init(): Promise<void> {
|
|
|
1128
1163
|
},
|
|
1129
1164
|
});
|
|
1130
1165
|
start.disabled = false;
|
|
1166
|
+
const message =
|
|
1167
|
+
err instanceof Error ? err.message : "Could not start Clips.";
|
|
1168
|
+
const storageSetupFailure = isStorageSetupFailureMessage(message);
|
|
1169
|
+
setStorageHelp(storageSetupFailure);
|
|
1131
1170
|
setStatus(
|
|
1132
|
-
|
|
1171
|
+
storageSetupFailure
|
|
1172
|
+
? "Connect storage in Clips first: Builder.io (free tier storage + AI) or S3-compatible storage."
|
|
1173
|
+
: message,
|
|
1133
1174
|
"error",
|
|
1134
1175
|
);
|
|
1135
1176
|
}
|
|
1136
1177
|
});
|
|
1137
1178
|
|
|
1179
|
+
storageHelpOpen.addEventListener("click", async () => {
|
|
1180
|
+
await createTab(storageSetupUrl(settings));
|
|
1181
|
+
window.close();
|
|
1182
|
+
});
|
|
1183
|
+
|
|
1138
1184
|
signIn.addEventListener("click", async () => {
|
|
1139
1185
|
signIn.disabled = true;
|
|
1140
1186
|
const response = await sendRuntimeMessage<PopupStartResponse>({
|
|
@@ -1161,7 +1207,13 @@ async function init(): Promise<void> {
|
|
|
1161
1207
|
}
|
|
1162
1208
|
stop.disabled = false;
|
|
1163
1209
|
discard.disabled = false;
|
|
1164
|
-
|
|
1210
|
+
const message = response.error || "Could not stop recording.";
|
|
1211
|
+
const storageSetupFailure = isStorageSetupFailureMessage(message);
|
|
1212
|
+
setStorageHelp(storageSetupFailure);
|
|
1213
|
+
setStatus(
|
|
1214
|
+
storageSetupFailure ? STORAGE_SETUP_REQUIRED_MESSAGE : message,
|
|
1215
|
+
"error",
|
|
1216
|
+
);
|
|
1165
1217
|
});
|
|
1166
1218
|
|
|
1167
1219
|
discard.addEventListener("click", async () => {
|
|
@@ -67,6 +67,31 @@ function scrubEvent<T extends Sentry.Event>(event: T): T {
|
|
|
67
67
|
return event;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
function shouldDropExpectedMediaPermissionError(
|
|
71
|
+
error: unknown,
|
|
72
|
+
context: CaptureContext,
|
|
73
|
+
): boolean {
|
|
74
|
+
const tags = context.tags ?? {};
|
|
75
|
+
const isPermissionProbe =
|
|
76
|
+
tags.surface === "permission" &&
|
|
77
|
+
(tags.permission === "mic" || tags.permission === "cam");
|
|
78
|
+
const isOptionalBubbleCamera =
|
|
79
|
+
tags.surface === "overlay" && tags.overlayPart === "bubble";
|
|
80
|
+
if (!isPermissionProbe && !isOptionalBubbleCamera) return false;
|
|
81
|
+
|
|
82
|
+
const text =
|
|
83
|
+
error instanceof Error
|
|
84
|
+
? `${error.name}: ${error.message}`
|
|
85
|
+
: String(error ?? "");
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
/\bNotAllowedError\b/i.test(text) ||
|
|
89
|
+
/\bPermission (?:denied|dismissed)\b/i.test(text) ||
|
|
90
|
+
/\b(?:NotFoundError|DevicesNotFoundError)\b/i.test(text) ||
|
|
91
|
+
/\bRequested device not found\b/i.test(text)
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
70
95
|
function scrubScopeString(value: string): string {
|
|
71
96
|
const scrubbed = scrubUrl(value, { redactAllQueryValues: true });
|
|
72
97
|
if (scrubbed !== value) return scrubbed;
|
|
@@ -175,7 +200,9 @@ export function captureExtensionError(
|
|
|
175
200
|
error: unknown,
|
|
176
201
|
context: CaptureContext = {},
|
|
177
202
|
): string | undefined {
|
|
178
|
-
if (!initialized)
|
|
203
|
+
if (!initialized || shouldDropExpectedMediaPermissionError(error, context)) {
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
179
206
|
try {
|
|
180
207
|
return Sentry.withScope((scope) => {
|
|
181
208
|
if (context.tags) {
|
|
@@ -574,6 +574,80 @@ svg {
|
|
|
574
574
|
color: var(--danger);
|
|
575
575
|
}
|
|
576
576
|
|
|
577
|
+
.storage-help {
|
|
578
|
+
position: relative;
|
|
579
|
+
display: flex;
|
|
580
|
+
min-height: 18px;
|
|
581
|
+
align-items: center;
|
|
582
|
+
justify-content: center;
|
|
583
|
+
gap: 5px;
|
|
584
|
+
color: var(--fg-subtle);
|
|
585
|
+
font-size: 11px;
|
|
586
|
+
line-height: 1.3;
|
|
587
|
+
text-align: center;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
.storage-help[hidden] {
|
|
591
|
+
display: none;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
.storage-help-open,
|
|
595
|
+
.storage-help-why {
|
|
596
|
+
border: 0;
|
|
597
|
+
background: transparent;
|
|
598
|
+
color: var(--fg-muted);
|
|
599
|
+
font: inherit;
|
|
600
|
+
padding: 0;
|
|
601
|
+
text-decoration: underline;
|
|
602
|
+
text-decoration-style: dotted;
|
|
603
|
+
text-underline-offset: 3px;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
.storage-help-open {
|
|
607
|
+
cursor: pointer;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
.storage-help-why {
|
|
611
|
+
position: relative;
|
|
612
|
+
cursor: help;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
.storage-help-open:hover,
|
|
616
|
+
.storage-help-open:focus-visible,
|
|
617
|
+
.storage-help-why:hover,
|
|
618
|
+
.storage-help-why:focus-visible {
|
|
619
|
+
color: var(--fg);
|
|
620
|
+
outline: none;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
.storage-help-details {
|
|
624
|
+
position: absolute;
|
|
625
|
+
right: 0;
|
|
626
|
+
bottom: calc(100% + 8px);
|
|
627
|
+
z-index: 70;
|
|
628
|
+
width: 310px;
|
|
629
|
+
padding: 8px 10px;
|
|
630
|
+
border-radius: var(--radius-sm);
|
|
631
|
+
background: var(--bg);
|
|
632
|
+
box-shadow: var(--shadow-md);
|
|
633
|
+
color: var(--fg);
|
|
634
|
+
font-size: 12px;
|
|
635
|
+
line-height: 1.45;
|
|
636
|
+
opacity: 0;
|
|
637
|
+
pointer-events: none;
|
|
638
|
+
text-align: left;
|
|
639
|
+
transform: translateY(3px);
|
|
640
|
+
transition:
|
|
641
|
+
opacity 120ms ease-out,
|
|
642
|
+
transform 120ms ease-out;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
.storage-help-why:hover .storage-help-details,
|
|
646
|
+
.storage-help-why:focus-visible .storage-help-details {
|
|
647
|
+
opacity: 1;
|
|
648
|
+
transform: translateY(0);
|
|
649
|
+
}
|
|
650
|
+
|
|
577
651
|
.bottom-row {
|
|
578
652
|
display: grid;
|
|
579
653
|
grid-template-columns: repeat(3, 1fr);
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
IconArrowLeft,
|
|
4
4
|
IconCircleCheck,
|
|
5
5
|
IconDownload,
|
|
6
|
+
IconExternalLink,
|
|
6
7
|
IconFolderOpen,
|
|
7
8
|
IconPencil,
|
|
8
9
|
IconInfoCircle,
|
|
@@ -118,6 +119,15 @@ type MeetingTranscriptionMode = "manual" | "ask" | "auto";
|
|
|
118
119
|
type CaptureMode = "screen" | "screen-camera" | "camera";
|
|
119
120
|
type VideoStorageStatus = "checking" | "configured" | "missing";
|
|
120
121
|
|
|
122
|
+
const STORAGE_SETUP_HELP_TEXT =
|
|
123
|
+
"Clips is 100% free and open source, so you need to hook up a way to store your clips. Connect storage with Builder.io for free-tier storage and AI, or use S3-compatible object storage and your own LLM keys.";
|
|
124
|
+
const STORAGE_SETUP_FAILURE_RE =
|
|
125
|
+
/video storage is not connected|no video storage configured|file upload provider|storage provider|connect builder|s3-compatible/i;
|
|
126
|
+
|
|
127
|
+
function isStorageSetupFailureMessage(message: string | null | undefined) {
|
|
128
|
+
return STORAGE_SETUP_FAILURE_RE.test(message ?? "");
|
|
129
|
+
}
|
|
130
|
+
|
|
121
131
|
const STORAGE_KEY = "clips:server-url";
|
|
122
132
|
const MODE_KEY = "clips:last-mode";
|
|
123
133
|
const VOICE_SHORTCUT_KEY = "clips:voice-shortcut";
|
|
@@ -1559,7 +1569,11 @@ export function App() {
|
|
|
1559
1569
|
} catch (err) {
|
|
1560
1570
|
const message = err instanceof Error ? err.message : String(err);
|
|
1561
1571
|
console.error("[clips-tray] retry saved upload failed:", err);
|
|
1562
|
-
setRecError(
|
|
1572
|
+
setRecError(
|
|
1573
|
+
isStorageSetupFailureMessage(message)
|
|
1574
|
+
? "Connect storage to finish uploading this saved clip: Builder.io (free tier storage + AI) or S3-compatible storage."
|
|
1575
|
+
: message,
|
|
1576
|
+
);
|
|
1563
1577
|
await loadPendingUploads();
|
|
1564
1578
|
} finally {
|
|
1565
1579
|
setRetryingUploadId(null);
|
|
@@ -1636,20 +1650,23 @@ export function App() {
|
|
|
1636
1650
|
});
|
|
1637
1651
|
}
|
|
1638
1652
|
|
|
1639
|
-
const openVideoStorageSetup = useCallback(
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
"Connect Builder.io or S3-compatible storage before recording from desktop. Opening storage setup...",
|
|
1643
|
-
);
|
|
1644
|
-
void openExternal(`${base}/record`).catch((err) => {
|
|
1653
|
+
const openVideoStorageSetup = useCallback(
|
|
1654
|
+
(targetServerUrl?: string) => {
|
|
1655
|
+
const base = (targetServerUrl?.trim() || serverUrl).replace(/\/+$/, "");
|
|
1645
1656
|
setRecError(
|
|
1646
|
-
|
|
1647
|
-
? err.message
|
|
1648
|
-
: "Could not open Clips storage setup.",
|
|
1657
|
+
"Connect storage before recording from desktop: Builder.io (free tier storage + AI) or S3-compatible storage. Opening storage setup...",
|
|
1649
1658
|
);
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1659
|
+
void openExternal(`${base}/record`).catch((err) => {
|
|
1660
|
+
setRecError(
|
|
1661
|
+
err instanceof Error
|
|
1662
|
+
? err.message
|
|
1663
|
+
: "Could not open Clips storage setup.",
|
|
1664
|
+
);
|
|
1665
|
+
});
|
|
1666
|
+
void refreshVideoStorageStatus();
|
|
1667
|
+
},
|
|
1668
|
+
[refreshVideoStorageStatus, serverUrl],
|
|
1669
|
+
);
|
|
1653
1670
|
|
|
1654
1671
|
async function handleStartRecording(options?: {
|
|
1655
1672
|
ignoreActiveRecorder?: boolean;
|
|
@@ -2148,6 +2165,7 @@ export function App() {
|
|
|
2148
2165
|
onRetry={retryPendingUpload}
|
|
2149
2166
|
onDiscard={discardPendingUpload}
|
|
2150
2167
|
onOpenFolder={openPendingUploadFolder}
|
|
2168
|
+
onConnectStorage={(upload) => openVideoStorageSetup(upload.serverUrl)}
|
|
2151
2169
|
/>
|
|
2152
2170
|
) : null}
|
|
2153
2171
|
|
|
@@ -2407,6 +2425,7 @@ function PendingUploadBanner({
|
|
|
2407
2425
|
onRetry,
|
|
2408
2426
|
onDiscard,
|
|
2409
2427
|
onOpenFolder,
|
|
2428
|
+
onConnectStorage,
|
|
2410
2429
|
}: {
|
|
2411
2430
|
uploads: PendingDesktopUpload[];
|
|
2412
2431
|
retryingUploadId: string | null;
|
|
@@ -2416,11 +2435,13 @@ function PendingUploadBanner({
|
|
|
2416
2435
|
onRetry: (upload: PendingDesktopUpload) => void;
|
|
2417
2436
|
onDiscard: (upload: PendingDesktopUpload) => void;
|
|
2418
2437
|
onOpenFolder: (upload: PendingDesktopUpload) => void;
|
|
2438
|
+
onConnectStorage: (upload: PendingDesktopUpload) => void;
|
|
2419
2439
|
}) {
|
|
2420
2440
|
const latest = uploads[0];
|
|
2421
2441
|
if (!latest) return null;
|
|
2422
2442
|
|
|
2423
2443
|
const retrying = retryingUploadId === latest.recordingId;
|
|
2444
|
+
const storageSetupFailure = isStorageSetupFailureMessage(latest.lastError);
|
|
2424
2445
|
const exporting = exportingUploadId === latest.recordingId;
|
|
2425
2446
|
const canOpenFolder = latest.kind === "native" && !!latest.folderPath;
|
|
2426
2447
|
const canExport = latest.kind === "browser";
|
|
@@ -2430,6 +2451,16 @@ function PendingUploadBanner({
|
|
|
2430
2451
|
uploads.length === 1
|
|
2431
2452
|
? "1 Clip saved locally"
|
|
2432
2453
|
: `${uploads.length} Clips saved locally`;
|
|
2454
|
+
const nativeCorrupt = latest.kind === "native" && !!latest.corrupt;
|
|
2455
|
+
const title = nativeCorrupt
|
|
2456
|
+
? uploads.length === 1
|
|
2457
|
+
? "Clip could not be finalized"
|
|
2458
|
+
: "Some Clips could not be finalized"
|
|
2459
|
+
: storageSetupFailure
|
|
2460
|
+
? uploads.length === 1
|
|
2461
|
+
? "Connect storage to upload saved Clip"
|
|
2462
|
+
: "Connect storage to upload saved Clips"
|
|
2463
|
+
: savedLabel;
|
|
2433
2464
|
const details = [
|
|
2434
2465
|
latest.savedAt ? `saved ${formatAgo(latest.savedAt)}` : null,
|
|
2435
2466
|
formatFileSize(latest.bytes),
|
|
@@ -2444,11 +2475,36 @@ function PendingUploadBanner({
|
|
|
2444
2475
|
<IconUpload size={17} stroke={1.8} />
|
|
2445
2476
|
</div>
|
|
2446
2477
|
<div className="pending-upload-copy">
|
|
2447
|
-
<div className="pending-upload-title">{
|
|
2448
|
-
<div
|
|
2449
|
-
{
|
|
2450
|
-
|
|
2478
|
+
<div className="pending-upload-title">{title}</div>
|
|
2479
|
+
<div
|
|
2480
|
+
className={
|
|
2481
|
+
storageSetupFailure
|
|
2482
|
+
? "pending-upload-sub pending-upload-sub-wrap"
|
|
2483
|
+
: "pending-upload-sub"
|
|
2484
|
+
}
|
|
2485
|
+
>
|
|
2486
|
+
{nativeCorrupt
|
|
2487
|
+
? `${details.join(" · ")} · discard and record again`
|
|
2488
|
+
: storageSetupFailure
|
|
2489
|
+
? `${details.join(" · ")} · your clip is safe locally`
|
|
2490
|
+
: `${details.join(" · ")}${errorText ? ` · ${errorText}` : ""}`}
|
|
2451
2491
|
</div>
|
|
2492
|
+
{storageSetupFailure ? (
|
|
2493
|
+
<Tooltip>
|
|
2494
|
+
<TooltipTrigger asChild>
|
|
2495
|
+
<button type="button" className="pending-upload-why">
|
|
2496
|
+
Why am I seeing this?
|
|
2497
|
+
</button>
|
|
2498
|
+
</TooltipTrigger>
|
|
2499
|
+
<TooltipContent
|
|
2500
|
+
side="bottom"
|
|
2501
|
+
align="start"
|
|
2502
|
+
className="tooltip-content-wide"
|
|
2503
|
+
>
|
|
2504
|
+
{STORAGE_SETUP_HELP_TEXT}
|
|
2505
|
+
</TooltipContent>
|
|
2506
|
+
</Tooltip>
|
|
2507
|
+
) : null}
|
|
2452
2508
|
</div>
|
|
2453
2509
|
<div className="pending-upload-actions">
|
|
2454
2510
|
{canOpenFolder ? (
|
|
@@ -2488,6 +2544,17 @@ function PendingUploadBanner({
|
|
|
2488
2544
|
</button>
|
|
2489
2545
|
) : (
|
|
2490
2546
|
<>
|
|
2547
|
+
{storageSetupFailure ? (
|
|
2548
|
+
<button
|
|
2549
|
+
type="button"
|
|
2550
|
+
className="pending-upload-connect"
|
|
2551
|
+
disabled={actionsDisabled}
|
|
2552
|
+
onClick={() => onConnectStorage(latest)}
|
|
2553
|
+
>
|
|
2554
|
+
<IconExternalLink size={14} stroke={2} />
|
|
2555
|
+
Connect
|
|
2556
|
+
</button>
|
|
2557
|
+
) : null}
|
|
2491
2558
|
<button
|
|
2492
2559
|
type="button"
|
|
2493
2560
|
className="pending-upload-retry"
|
|
@@ -3705,7 +3772,7 @@ function Setup({
|
|
|
3705
3772
|
className="secondary"
|
|
3706
3773
|
onClick={connectBuilder}
|
|
3707
3774
|
>
|
|
3708
|
-
|
|
3775
|
+
Use Builder.io (free)
|
|
3709
3776
|
</button>
|
|
3710
3777
|
) : null}
|
|
3711
3778
|
</div>
|
|
@@ -1448,6 +1448,10 @@ body[data-clips-route="recording-pill"] #root {
|
|
|
1448
1448
|
transform-origin: var(--radix-tooltip-content-transform-origin);
|
|
1449
1449
|
}
|
|
1450
1450
|
|
|
1451
|
+
.tooltip-content-wide {
|
|
1452
|
+
max-width: 320px;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1451
1455
|
.tooltip-content[data-state="delayed-open"],
|
|
1452
1456
|
.tooltip-content[data-state="instant-open"] {
|
|
1453
1457
|
animation: tooltip-in 120ms ease-out;
|
|
@@ -1720,12 +1724,38 @@ body[data-clips-route="recording-pill"] #root {
|
|
|
1720
1724
|
white-space: nowrap;
|
|
1721
1725
|
}
|
|
1722
1726
|
|
|
1727
|
+
.pending-upload-sub-wrap {
|
|
1728
|
+
overflow: visible;
|
|
1729
|
+
white-space: normal;
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
.pending-upload-why {
|
|
1733
|
+
margin-top: 3px;
|
|
1734
|
+
padding: 0;
|
|
1735
|
+
border: 0;
|
|
1736
|
+
background: transparent;
|
|
1737
|
+
color: #64748b;
|
|
1738
|
+
cursor: help;
|
|
1739
|
+
font-size: 11px;
|
|
1740
|
+
line-height: 1.2;
|
|
1741
|
+
text-decoration: underline;
|
|
1742
|
+
text-decoration-style: dotted;
|
|
1743
|
+
text-underline-offset: 3px;
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
.pending-upload-why:hover,
|
|
1747
|
+
.pending-upload-why:focus-visible {
|
|
1748
|
+
color: #334155;
|
|
1749
|
+
outline: none;
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1723
1752
|
.pending-upload-actions {
|
|
1724
1753
|
display: inline-flex;
|
|
1725
1754
|
align-items: center;
|
|
1726
1755
|
gap: 6px;
|
|
1727
1756
|
}
|
|
1728
1757
|
|
|
1758
|
+
.pending-upload-connect,
|
|
1729
1759
|
.pending-upload-retry {
|
|
1730
1760
|
height: 28px;
|
|
1731
1761
|
border: 1px solid #bfdbfe;
|
|
@@ -1746,6 +1776,10 @@ body[data-clips-route="recording-pill"] #root {
|
|
|
1746
1776
|
opacity 120ms;
|
|
1747
1777
|
}
|
|
1748
1778
|
|
|
1779
|
+
.pending-upload-connect {
|
|
1780
|
+
padding: 0 8px;
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1749
1783
|
.pending-upload-discard {
|
|
1750
1784
|
width: 28px;
|
|
1751
1785
|
height: 28px;
|
|
@@ -1789,6 +1823,11 @@ body[data-clips-route="recording-pill"] #root {
|
|
|
1789
1823
|
border-color: #93c5fd;
|
|
1790
1824
|
}
|
|
1791
1825
|
|
|
1826
|
+
.pending-upload-connect:hover {
|
|
1827
|
+
background: #dbeafe;
|
|
1828
|
+
border-color: #93c5fd;
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1792
1831
|
.pending-upload-folder:hover {
|
|
1793
1832
|
background: #dbeafe;
|
|
1794
1833
|
border-color: #93c5fd;
|
|
@@ -1801,6 +1840,7 @@ body[data-clips-route="recording-pill"] #root {
|
|
|
1801
1840
|
}
|
|
1802
1841
|
|
|
1803
1842
|
.pending-upload-retry:disabled,
|
|
1843
|
+
.pending-upload-connect:disabled,
|
|
1804
1844
|
.pending-upload-folder:disabled,
|
|
1805
1845
|
.pending-upload-discard:disabled {
|
|
1806
1846
|
cursor: default;
|
|
@@ -1972,6 +2012,16 @@ body[data-clips-route="recording-pill"] #root {
|
|
|
1972
2012
|
color: #93c5fd;
|
|
1973
2013
|
}
|
|
1974
2014
|
|
|
2015
|
+
.pending-upload-why {
|
|
2016
|
+
color: #94a3b8;
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
.pending-upload-why:hover,
|
|
2020
|
+
.pending-upload-why:focus-visible {
|
|
2021
|
+
color: #cbd5e1;
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
.pending-upload-connect,
|
|
1975
2025
|
.pending-upload-retry {
|
|
1976
2026
|
background: rgba(15, 23, 42, 0.7);
|
|
1977
2027
|
border-color: rgba(96, 165, 250, 0.38);
|
|
@@ -1995,6 +2045,11 @@ body[data-clips-route="recording-pill"] #root {
|
|
|
1995
2045
|
border-color: rgba(147, 197, 253, 0.62);
|
|
1996
2046
|
}
|
|
1997
2047
|
|
|
2048
|
+
.pending-upload-connect:hover {
|
|
2049
|
+
background: rgba(30, 64, 175, 0.34);
|
|
2050
|
+
border-color: rgba(147, 197, 253, 0.62);
|
|
2051
|
+
}
|
|
2052
|
+
|
|
1998
2053
|
.pending-upload-folder:hover {
|
|
1999
2054
|
background: rgba(30, 64, 175, 0.34);
|
|
2000
2055
|
border-color: rgba(147, 197, 253, 0.62);
|