@agent-native/core 0.102.0 → 0.102.2
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 +15 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/AgentPanel.tsx +7 -0
- package/corpus/core/src/client/AssistantChat.tsx +7 -0
- package/corpus/core/src/client/agent-chat-adapter.ts +7 -1
- package/corpus/core/src/client/agent-page/AgentTabsPage.tsx +1 -1
- package/corpus/core/src/client/chat/message-components.tsx +17 -3
- package/corpus/core/src/client/composer/ComposerPlusMenu.tsx +96 -6
- package/corpus/core/src/client/composer/asset-picker-url.ts +80 -0
- package/corpus/core/src/client/error-format.ts +24 -0
- package/corpus/core/src/client/resources/McpIntegrationDialog.tsx +8 -1
- package/corpus/templates/analytics/changelog/2026-07-15-fixed-the-agent-discarding-a-successful-dashboard-edit-and-r.md +6 -0
- package/corpus/templates/analytics/server/lib/real-data-actions.ts +30 -1
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +15 -0
- package/corpus/templates/assets/app/routes/library.tsx +69 -0
- package/corpus/templates/assets/changelog/2026-07-15-fixed-top-level-image-picker-selections-returning-to-the-com.md +6 -0
- package/corpus/templates/clips/actions/disconnect-calendar.ts +57 -9
- package/corpus/templates/clips/actions/generate-workflow.ts +102 -59
- package/corpus/templates/clips/actions/get-recording-player-data.ts +64 -1
- package/corpus/templates/clips/actions/regenerate-summary.ts +7 -0
- package/corpus/templates/clips/actions/regenerate-title.ts +29 -11
- package/corpus/templates/clips/app/components/editor/editor-layout.tsx +22 -11
- package/corpus/templates/clips/app/components/editor/thumbnail-picker.tsx +87 -9
- package/corpus/templates/clips/app/components/editor/trim-handles.tsx +15 -0
- package/corpus/templates/clips/app/components/editor/waveform.tsx +21 -8
- package/corpus/templates/clips/app/components/library/folder-tree.tsx +52 -0
- package/corpus/templates/clips/app/components/player/settings-panel.tsx +86 -46
- package/corpus/templates/clips/app/components/player/share-dialog.tsx +43 -37
- package/corpus/templates/clips/app/components/player/video-player.tsx +45 -1
- package/corpus/templates/clips/app/i18n/ar-SA.ts +2 -0
- package/corpus/templates/clips/app/i18n/de-DE.ts +2 -0
- package/corpus/templates/clips/app/i18n/en-US.ts +2 -0
- package/corpus/templates/clips/app/i18n/es-ES.ts +2 -0
- package/corpus/templates/clips/app/i18n/fr-FR.ts +2 -0
- package/corpus/templates/clips/app/i18n/hi-IN.ts +2 -0
- package/corpus/templates/clips/app/i18n/ja-JP.ts +2 -0
- package/corpus/templates/clips/app/i18n/ko-KR.ts +2 -0
- package/corpus/templates/clips/app/i18n/pt-BR.ts +2 -0
- package/corpus/templates/clips/app/i18n/zh-CN.ts +2 -0
- package/corpus/templates/clips/app/i18n/zh-TW.ts +2 -0
- package/corpus/templates/clips/app/lib/timestamp-mapping.ts +14 -0
- package/corpus/templates/clips/app/routes/_app.library._index.tsx +1 -2
- package/corpus/templates/clips/app/routes/_index.tsx +1 -2
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +71 -31
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +4 -1
- package/corpus/templates/clips/changelog/2026-07-15-ai-workflow-generation-now-stays-visible-avoids-duplicate-re.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-15-clip-playback-and-cta-controls-remain-interactive-through-ed.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-15-clip-titles-no-longer-describe-the-product-as-a-loom-alterna.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-15-description-regeneration-now-opens-the-agent-chat-so-progres.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-15-direct-recording-urls-now-respect-share-link-and-password-ac.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-15-disconnecting-a-calendar-now-clears-unrecorded-synced-meetin.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-15-folders-now-expose-rename-and-delete-actions-directly-in-the.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-15-recording-pages-now-explain-sign-in-access-and-avoid-showing.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-15-the-clips-extension-quick-actions-toolbar-can-be-moved-out-o.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-15-the-editor-now-skips-cut-sections-during-preview-and-keeps-t.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-15-thumbnail-capture-now-updates-previews-while-scrubbing-and-l.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-15-windows-permission-links-open-the-matching-settings-page.md +6 -0
- package/corpus/templates/clips/chrome-extension/src/content-script.ts +117 -2
- package/corpus/templates/clips/chrome-extension/src/overlay.ts +27 -0
- package/corpus/templates/clips/desktop/src-tauri/tauri.conf.json +1 -1
- package/corpus/templates/clips/server/lib/calendar-event-meetings.ts +139 -101
- package/corpus/templates/clips/server/lib/recording-page-access.ts +35 -0
- package/corpus/templates/content/app/components/editor/DescriptionField.tsx +10 -2
- package/corpus/templates/content/changelog/2026-07-15-page-and-database-descriptions-now-use-a-clean-single-line-f.md +6 -0
- package/corpus/templates/design/app/components/design/design-canvas/external-preview.ts +7 -3
- package/corpus/templates/design/changelog/2026-07-15-prototype-print-and-download-buttons-now-work-in-preview.md +6 -0
- package/corpus/templates/slides/actions/extract-pdf.ts +1 -1
- package/corpus/templates/slides/actions/import-file.ts +4 -1
- package/corpus/templates/slides/app/components/editor/ImageGenPanel.tsx +1 -1
- package/corpus/templates/slides/app/components/editor/ImageSearchPanel.tsx +9 -12
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +29 -21
- package/corpus/templates/slides/app/i18n/ar-SA.ts +1 -0
- package/corpus/templates/slides/app/i18n/de-DE.ts +1 -0
- package/corpus/templates/slides/app/i18n/en-US.ts +1 -0
- package/corpus/templates/slides/app/i18n/es-ES.ts +1 -0
- package/corpus/templates/slides/app/i18n/fr-FR.ts +1 -0
- package/corpus/templates/slides/app/i18n/hi-IN.ts +1 -0
- package/corpus/templates/slides/app/i18n/ja-JP.ts +1 -0
- package/corpus/templates/slides/app/i18n/ko-KR.ts +1 -0
- package/corpus/templates/slides/app/i18n/pt-BR.ts +1 -0
- package/corpus/templates/slides/app/i18n/zh-CN.ts +1 -0
- package/corpus/templates/slides/app/i18n/zh-TW.ts +1 -0
- package/corpus/templates/slides/changelog/2026-07-15-image-search-and-generation-controls-now-use-the-registered-.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-15-pdf-deck-references-can-be-imported-for-slide-inspiration-ag.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-15-stable-style-inspector-dock.md +6 -0
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +5 -0
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +7 -0
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/agent-chat-adapter.js +8 -1
- package/dist/client/agent-chat-adapter.js.map +1 -1
- package/dist/client/agent-page/AgentTabsPage.js +1 -1
- package/dist/client/agent-page/AgentTabsPage.js.map +1 -1
- package/dist/client/chat/message-components.d.ts +4 -0
- package/dist/client/chat/message-components.d.ts.map +1 -1
- package/dist/client/chat/message-components.js +10 -3
- package/dist/client/chat/message-components.js.map +1 -1
- package/dist/client/composer/ComposerPlusMenu.d.ts.map +1 -1
- package/dist/client/composer/ComposerPlusMenu.js +58 -6
- package/dist/client/composer/ComposerPlusMenu.js.map +1 -1
- package/dist/client/composer/asset-picker-url.d.ts +16 -0
- package/dist/client/composer/asset-picker-url.d.ts.map +1 -0
- package/dist/client/composer/asset-picker-url.js +64 -0
- package/dist/client/composer/asset-picker-url.js.map +1 -0
- package/dist/client/error-format.d.ts.map +1 -1
- package/dist/client/error-format.js +17 -0
- package/dist/client/error-format.js.map +1 -1
- package/dist/client/resources/McpIntegrationDialog.d.ts.map +1 -1
- package/dist/client/resources/McpIntegrationDialog.js +6 -1
- package/dist/client/resources/McpIntegrationDialog.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +1 -1
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/package.json +1 -1
|
@@ -83,6 +83,17 @@
|
|
|
83
83
|
let bubbleDragLayer: HTMLDivElement | null = null;
|
|
84
84
|
let bubblePersistTimer: ReturnType<typeof setTimeout> | undefined;
|
|
85
85
|
|
|
86
|
+
// The quick-actions toolbar is also draggable. Keep its geometry in the
|
|
87
|
+
// content script because the iframe cannot position itself on the host page.
|
|
88
|
+
const TOOLBAR_WIDTH = 68;
|
|
89
|
+
const TOOLBAR_COLLAPSED_HEIGHT = 154;
|
|
90
|
+
const toolbarGeom: { left: number | null; top: number | null } = {
|
|
91
|
+
left: null,
|
|
92
|
+
top: null,
|
|
93
|
+
};
|
|
94
|
+
let toolbarDragLayer: HTMLDivElement | null = null;
|
|
95
|
+
let toolbarPersistTimer: ReturnType<typeof setTimeout> | undefined;
|
|
96
|
+
|
|
86
97
|
function bubbleSizePx(): number {
|
|
87
98
|
return BUBBLE_SIZES[bubbleGeom.size] ?? BUBBLE_SIZES.lg;
|
|
88
99
|
}
|
|
@@ -130,6 +141,92 @@
|
|
|
130
141
|
}, 200);
|
|
131
142
|
}
|
|
132
143
|
|
|
144
|
+
function clampToolbar(
|
|
145
|
+
left: number,
|
|
146
|
+
top: number,
|
|
147
|
+
height: number,
|
|
148
|
+
): { left: number; top: number } {
|
|
149
|
+
return {
|
|
150
|
+
left: Math.max(8, Math.min(left, window.innerWidth - TOOLBAR_WIDTH - 8)),
|
|
151
|
+
top: Math.max(8, Math.min(top, window.innerHeight - height - 8)),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function applyToolbarGeom(): void {
|
|
156
|
+
const frame = document.getElementById(
|
|
157
|
+
partFrameId("toolbar"),
|
|
158
|
+
) as HTMLIFrameElement | null;
|
|
159
|
+
if (!frame) return;
|
|
160
|
+
const height =
|
|
161
|
+
frame.getBoundingClientRect().height || TOOLBAR_COLLAPSED_HEIGHT;
|
|
162
|
+
const base = clampToolbar(
|
|
163
|
+
toolbarGeom.left ?? 16,
|
|
164
|
+
toolbarGeom.top ?? window.innerHeight / 2 - TOOLBAR_COLLAPSED_HEIGHT / 2,
|
|
165
|
+
height,
|
|
166
|
+
);
|
|
167
|
+
Object.assign(frame.style, {
|
|
168
|
+
left: `${base.left}px`,
|
|
169
|
+
top: `${base.top}px`,
|
|
170
|
+
right: "auto",
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function persistToolbarGeom(): void {
|
|
175
|
+
clearTimeout(toolbarPersistTimer);
|
|
176
|
+
toolbarPersistTimer = setTimeout(() => {
|
|
177
|
+
try {
|
|
178
|
+
chrome.storage.local.set({ toolbarGeom });
|
|
179
|
+
} catch {
|
|
180
|
+
/* ignore */
|
|
181
|
+
}
|
|
182
|
+
}, 200);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function startToolbarDrag(): void {
|
|
186
|
+
if (toolbarDragLayer) return;
|
|
187
|
+
const frame = document.getElementById(
|
|
188
|
+
partFrameId("toolbar"),
|
|
189
|
+
) as HTMLIFrameElement | null;
|
|
190
|
+
if (!frame) return;
|
|
191
|
+
|
|
192
|
+
const layer = document.createElement("div");
|
|
193
|
+
Object.assign(layer.style, {
|
|
194
|
+
position: "fixed",
|
|
195
|
+
inset: "0",
|
|
196
|
+
zIndex: "2147483647",
|
|
197
|
+
cursor: "grabbing",
|
|
198
|
+
});
|
|
199
|
+
(document.documentElement || document.body).appendChild(layer);
|
|
200
|
+
toolbarDragLayer = layer;
|
|
201
|
+
|
|
202
|
+
const onMove = (e: PointerEvent): void => {
|
|
203
|
+
const rect = frame.getBoundingClientRect();
|
|
204
|
+
const next = clampToolbar(
|
|
205
|
+
rect.left + e.movementX,
|
|
206
|
+
rect.top + e.movementY,
|
|
207
|
+
rect.height || TOOLBAR_COLLAPSED_HEIGHT,
|
|
208
|
+
);
|
|
209
|
+
toolbarGeom.left = next.left;
|
|
210
|
+
toolbarGeom.top = next.top;
|
|
211
|
+
Object.assign(frame.style, {
|
|
212
|
+
left: `${next.left}px`,
|
|
213
|
+
top: `${next.top}px`,
|
|
214
|
+
right: "auto",
|
|
215
|
+
});
|
|
216
|
+
};
|
|
217
|
+
const onUp = (): void => {
|
|
218
|
+
layer.removeEventListener("pointermove", onMove);
|
|
219
|
+
layer.removeEventListener("pointerup", onUp);
|
|
220
|
+
layer.removeEventListener("pointercancel", onUp);
|
|
221
|
+
layer.remove();
|
|
222
|
+
toolbarDragLayer = null;
|
|
223
|
+
persistToolbarGeom();
|
|
224
|
+
};
|
|
225
|
+
layer.addEventListener("pointermove", onMove);
|
|
226
|
+
layer.addEventListener("pointerup", onUp);
|
|
227
|
+
layer.addEventListener("pointercancel", onUp);
|
|
228
|
+
}
|
|
229
|
+
|
|
133
230
|
function startBubbleDrag(): void {
|
|
134
231
|
if (bubbleDragLayer) return;
|
|
135
232
|
const frame = document.getElementById(
|
|
@@ -177,7 +274,7 @@
|
|
|
177
274
|
}
|
|
178
275
|
|
|
179
276
|
try {
|
|
180
|
-
chrome.storage.local.get("bubbleGeom", (value) => {
|
|
277
|
+
chrome.storage.local.get(["bubbleGeom", "toolbarGeom"], (value) => {
|
|
181
278
|
if (chrome.runtime.lastError) return;
|
|
182
279
|
const g = value.bubbleGeom as
|
|
183
280
|
| { size?: unknown; left?: unknown; top?: unknown }
|
|
@@ -188,12 +285,24 @@
|
|
|
188
285
|
bubbleGeom.top = typeof g.top === "number" ? g.top : null;
|
|
189
286
|
}
|
|
190
287
|
applyBubbleGeom();
|
|
288
|
+
const toolbar = value.toolbarGeom as
|
|
289
|
+
| { left?: unknown; top?: unknown }
|
|
290
|
+
| undefined;
|
|
291
|
+
if (toolbar && typeof toolbar === "object") {
|
|
292
|
+
toolbarGeom.left =
|
|
293
|
+
typeof toolbar.left === "number" ? toolbar.left : null;
|
|
294
|
+
toolbarGeom.top = typeof toolbar.top === "number" ? toolbar.top : null;
|
|
295
|
+
}
|
|
296
|
+
applyToolbarGeom();
|
|
191
297
|
});
|
|
192
298
|
} catch {
|
|
193
299
|
/* ignore */
|
|
194
300
|
}
|
|
195
301
|
|
|
196
|
-
window.addEventListener("resize", () =>
|
|
302
|
+
window.addEventListener("resize", () => {
|
|
303
|
+
applyBubbleGeom();
|
|
304
|
+
applyToolbarGeom();
|
|
305
|
+
});
|
|
197
306
|
|
|
198
307
|
function readCurrentParts(
|
|
199
308
|
callback: (parts: OverlayPart[] | null) => void,
|
|
@@ -338,6 +447,7 @@
|
|
|
338
447
|
styleFrame(frame, part);
|
|
339
448
|
container.appendChild(frame);
|
|
340
449
|
if (part === "bubble") applyBubbleGeom();
|
|
450
|
+
if (part === "toolbar") applyToolbarGeom();
|
|
341
451
|
}
|
|
342
452
|
|
|
343
453
|
// Camera-ready gating + connecting spinner: while the camera connects we show a
|
|
@@ -520,6 +630,7 @@
|
|
|
520
630
|
const frame = document.getElementById(partFrameId("toolbar"));
|
|
521
631
|
if (frame && typeof data.height === "number") {
|
|
522
632
|
frame.style.height = `${Math.round(data.height)}px`;
|
|
633
|
+
applyToolbarGeom();
|
|
523
634
|
}
|
|
524
635
|
return;
|
|
525
636
|
}
|
|
@@ -537,6 +648,10 @@
|
|
|
537
648
|
startBubbleDrag();
|
|
538
649
|
return;
|
|
539
650
|
}
|
|
651
|
+
if (data.kind === "toolbar-drag-start") {
|
|
652
|
+
startToolbarDrag();
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
540
655
|
if (data.kind === "bubble-size") {
|
|
541
656
|
bubbleGeom.size = data.size === "sm" ? "sm" : "lg";
|
|
542
657
|
applyBubbleGeom();
|
|
@@ -84,6 +84,17 @@ function postToolbarSize(height: number): void {
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
function postToolbarDragStart(): void {
|
|
88
|
+
try {
|
|
89
|
+
window.parent.postMessage(
|
|
90
|
+
{ source: "clips-overlay", kind: "toolbar-drag-start", part: "toolbar" },
|
|
91
|
+
"*",
|
|
92
|
+
);
|
|
93
|
+
} catch {
|
|
94
|
+
/* parent gone */
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
87
98
|
function postCountdownFinished(): void {
|
|
88
99
|
try {
|
|
89
100
|
window.parent.postMessage(
|
|
@@ -406,6 +417,22 @@ function initCountdown(): void {
|
|
|
406
417
|
function initToolbar(): void {
|
|
407
418
|
const pill = document.createElement("div");
|
|
408
419
|
pill.className = "toolbar-v";
|
|
420
|
+
pill.style.cursor = "grab";
|
|
421
|
+
|
|
422
|
+
// The content script owns iframe geometry, so ask it to capture the pointer
|
|
423
|
+
// page-wide when the user drags the toolbar outside its current bounds.
|
|
424
|
+
pill.addEventListener("pointerdown", (event) => {
|
|
425
|
+
if (event.button !== 0) return;
|
|
426
|
+
if ((event.target as HTMLElement).closest("button")) return;
|
|
427
|
+
event.preventDefault();
|
|
428
|
+
pill.style.cursor = "grabbing";
|
|
429
|
+
postToolbarDragStart();
|
|
430
|
+
const restore = (): void => {
|
|
431
|
+
pill.style.cursor = "grab";
|
|
432
|
+
window.removeEventListener("pointerup", restore);
|
|
433
|
+
};
|
|
434
|
+
window.addEventListener("pointerup", restore);
|
|
435
|
+
});
|
|
409
436
|
|
|
410
437
|
const makeBtn = (
|
|
411
438
|
cls: string,
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
},
|
|
41
41
|
"plugins": {
|
|
42
42
|
"shell": {
|
|
43
|
-
"open": "(((mailto:\\w+)|(tel:\\w+)|(https?://\\w+)).+|(zoommtg://([A-Za-z0-9-]+\\.)*zoom\\.us/join\\?.+))"
|
|
43
|
+
"open": "(((mailto:\\w+)|(tel:\\w+)|(https?://\\w+)).+|(zoommtg://([A-Za-z0-9-]+\\.)*zoom\\.us/join\\?.+)|(ms-settings:[A-Za-z0-9._?=&-]+))"
|
|
44
44
|
},
|
|
45
45
|
"updater": {
|
|
46
46
|
"active": true,
|
|
@@ -274,6 +274,27 @@ export function calendarEventToMeetingView(args: {
|
|
|
274
274
|
};
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
+
/**
|
|
278
|
+
* Acquire the calendar account's write lock inside a caller-owned transaction.
|
|
279
|
+
* Disconnect and materialization both take this lock before touching the
|
|
280
|
+
* account's calendar events, so cleanup cannot race a meeting insert.
|
|
281
|
+
*/
|
|
282
|
+
export async function lockCalendarAccount(
|
|
283
|
+
db: any,
|
|
284
|
+
accountId: string,
|
|
285
|
+
ownerEmail?: string | null,
|
|
286
|
+
): Promise<boolean> {
|
|
287
|
+
const ownerFilter = ownerEmail
|
|
288
|
+
? eq(schema.calendarAccounts.ownerEmail, ownerEmail)
|
|
289
|
+
: isNull(schema.calendarAccounts.ownerEmail);
|
|
290
|
+
const locked = await db
|
|
291
|
+
.update(schema.calendarAccounts)
|
|
292
|
+
.set({ updatedAt: new Date().toISOString() })
|
|
293
|
+
.where(and(eq(schema.calendarAccounts.id, accountId), ownerFilter))
|
|
294
|
+
.returning({ id: schema.calendarAccounts.id });
|
|
295
|
+
return locked.length > 0;
|
|
296
|
+
}
|
|
297
|
+
|
|
277
298
|
export async function fetchLiveCalendarEventFromId(virtualId: string) {
|
|
278
299
|
const parsed = parseCalendarMeetingId(virtualId);
|
|
279
300
|
if (!parsed) return null;
|
|
@@ -317,11 +338,14 @@ export async function fetchLiveCalendarEventFromId(virtualId: string) {
|
|
|
317
338
|
}
|
|
318
339
|
}
|
|
319
340
|
|
|
320
|
-
export async function upsertCalendarEventSnapshot(
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
341
|
+
export async function upsertCalendarEventSnapshot(
|
|
342
|
+
args: {
|
|
343
|
+
account: CalendarAccountForEvents;
|
|
344
|
+
event: CalendarEvent;
|
|
345
|
+
},
|
|
346
|
+
database: any = getDb(),
|
|
347
|
+
) {
|
|
348
|
+
const db = database;
|
|
325
349
|
const startIso = eventStartIso(args.event);
|
|
326
350
|
const endIso = eventEndIso(args.event);
|
|
327
351
|
if (!args.event.id || !startIso || !endIso) {
|
|
@@ -397,120 +421,134 @@ export async function materializeCalendarMeetingFromVirtualId(
|
|
|
397
421
|
const live = await fetchLiveCalendarEventFromId(virtualId);
|
|
398
422
|
if (!live) return null;
|
|
399
423
|
const db = getDb();
|
|
400
|
-
const snapshot = await upsertCalendarEventSnapshot(live);
|
|
401
|
-
|
|
402
|
-
if (snapshot.meetingId) {
|
|
403
|
-
const [existingMeeting] = await db
|
|
404
|
-
.select()
|
|
405
|
-
.from(schema.meetings)
|
|
406
|
-
.where(eq(schema.meetings.id, snapshot.meetingId))
|
|
407
|
-
.limit(1);
|
|
408
|
-
if (existingMeeting) return { meeting: existingMeeting, created: false };
|
|
409
|
-
}
|
|
410
|
-
|
|
411
424
|
const ownerEmail = getCurrentOwnerEmail();
|
|
412
425
|
const orgId = (await getActiveOrganizationId().catch(() => null)) ?? null;
|
|
413
426
|
const joinUrl = pickJoinUrl(live.event);
|
|
414
427
|
const meetingId = nanoid();
|
|
415
428
|
const nowIso = new Date().toISOString();
|
|
416
429
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
.update(schema.calendarEvents)
|
|
424
|
-
.set({ meetingId, updatedAt: nowIso })
|
|
425
|
-
.where(
|
|
426
|
-
and(
|
|
427
|
-
eq(schema.calendarEvents.id, snapshot.id),
|
|
428
|
-
isNull(schema.calendarEvents.meetingId),
|
|
429
|
-
),
|
|
430
|
+
return db.transaction(async (tx: any) => {
|
|
431
|
+
// Disconnect takes this same account-row write lock before it snapshots
|
|
432
|
+
// and deletes events. If disconnect wins, the account is gone by the time
|
|
433
|
+
// this transaction reaches the lock and materialization becomes a no-op.
|
|
434
|
+
if (
|
|
435
|
+
!(await lockCalendarAccount(tx, live.account.id, live.account.ownerEmail))
|
|
430
436
|
)
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
.from(schema.calendarEvents)
|
|
438
|
-
.where(eq(schema.calendarEvents.id, snapshot.id))
|
|
439
|
-
.limit(1);
|
|
440
|
-
if (winnerEvent?.meetingId) {
|
|
441
|
-
const [winnerMeeting] = await db
|
|
437
|
+
return null;
|
|
438
|
+
|
|
439
|
+
const snapshot = await upsertCalendarEventSnapshot(live, tx);
|
|
440
|
+
|
|
441
|
+
if (snapshot.meetingId) {
|
|
442
|
+
const [existingMeeting] = await tx
|
|
442
443
|
.select()
|
|
443
444
|
.from(schema.meetings)
|
|
444
|
-
.where(eq(schema.meetings.id,
|
|
445
|
+
.where(eq(schema.meetings.id, snapshot.meetingId))
|
|
445
446
|
.limit(1);
|
|
446
|
-
if (
|
|
447
|
+
if (existingMeeting) {
|
|
448
|
+
return { meeting: existingMeeting, created: false };
|
|
449
|
+
}
|
|
447
450
|
}
|
|
448
|
-
return null;
|
|
449
|
-
}
|
|
450
451
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
scheduledEnd: eventEndIso(live.event),
|
|
458
|
-
actualStart: null,
|
|
459
|
-
actualEnd: null,
|
|
460
|
-
platform: detectPlatform(joinUrl),
|
|
461
|
-
joinUrl: joinUrl ?? null,
|
|
462
|
-
calendarEventId: snapshot.id,
|
|
463
|
-
recordingId: null,
|
|
464
|
-
transcriptStatus: "idle",
|
|
465
|
-
summaryMd: "",
|
|
466
|
-
bulletsJson: "[]",
|
|
467
|
-
actionItemsJson: "[]",
|
|
468
|
-
source: "calendar",
|
|
469
|
-
reminderFiredAt: null,
|
|
470
|
-
createdAt: nowIso,
|
|
471
|
-
updatedAt: nowIso,
|
|
472
|
-
ownerEmail,
|
|
473
|
-
orgId,
|
|
474
|
-
visibility: "private",
|
|
475
|
-
} as any);
|
|
476
|
-
|
|
477
|
-
const participants = calendarEventParticipants(live.event).filter(
|
|
478
|
-
(p) => p.email,
|
|
479
|
-
);
|
|
480
|
-
if (participants.length) {
|
|
481
|
-
await db.insert(schema.meetingParticipants).values(
|
|
482
|
-
participants.map((p) => ({
|
|
483
|
-
id: nanoid(),
|
|
484
|
-
meetingId,
|
|
485
|
-
email: p.email,
|
|
486
|
-
name: p.name ?? null,
|
|
487
|
-
isOrganizer: !!p.isOrganizer,
|
|
488
|
-
attendedAt: null,
|
|
489
|
-
createdAt: nowIso,
|
|
490
|
-
})) as any,
|
|
491
|
-
);
|
|
492
|
-
}
|
|
493
|
-
} catch (err) {
|
|
494
|
-
// Roll back the claim so a future call can retry — but only if it still
|
|
495
|
-
// points at our own meetingId (don't clobber a legitimate later claim).
|
|
496
|
-
await db
|
|
452
|
+
// Claim the calendar_events row atomically before inserting the meeting
|
|
453
|
+
// row, so two concurrent materialize calls for the same event can't both
|
|
454
|
+
// insert a `meetings` row (check-then-act TOCTOU). Only the caller whose
|
|
455
|
+
// UPDATE actually matched a row (meetingId still NULL) proceeds to insert;
|
|
456
|
+
// the loser re-reads and returns the winner's meeting instead.
|
|
457
|
+
const claimed = await tx
|
|
497
458
|
.update(schema.calendarEvents)
|
|
498
|
-
.set({ meetingId
|
|
459
|
+
.set({ meetingId, updatedAt: nowIso })
|
|
499
460
|
.where(
|
|
500
461
|
and(
|
|
501
462
|
eq(schema.calendarEvents.id, snapshot.id),
|
|
502
|
-
|
|
463
|
+
isNull(schema.calendarEvents.meetingId),
|
|
503
464
|
),
|
|
504
465
|
)
|
|
505
|
-
.
|
|
506
|
-
|
|
507
|
-
|
|
466
|
+
.returning({ id: schema.calendarEvents.id });
|
|
467
|
+
|
|
468
|
+
if (!claimed.length) {
|
|
469
|
+
// Someone else claimed it first — re-read and return the winner.
|
|
470
|
+
const [winnerEvent] = await tx
|
|
471
|
+
.select({ meetingId: schema.calendarEvents.meetingId })
|
|
472
|
+
.from(schema.calendarEvents)
|
|
473
|
+
.where(eq(schema.calendarEvents.id, snapshot.id))
|
|
474
|
+
.limit(1);
|
|
475
|
+
if (winnerEvent?.meetingId) {
|
|
476
|
+
const [winnerMeeting] = await tx
|
|
477
|
+
.select()
|
|
478
|
+
.from(schema.meetings)
|
|
479
|
+
.where(eq(schema.meetings.id, winnerEvent.meetingId))
|
|
480
|
+
.limit(1);
|
|
481
|
+
if (winnerMeeting) {
|
|
482
|
+
return { meeting: winnerMeeting, created: false };
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
508
487
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
488
|
+
try {
|
|
489
|
+
await tx.insert(schema.meetings).values({
|
|
490
|
+
id: meetingId,
|
|
491
|
+
organizationId: orgId ?? live.account.orgId ?? null,
|
|
492
|
+
title: live.event.summary || "Untitled meeting",
|
|
493
|
+
scheduledStart: eventStartIso(live.event),
|
|
494
|
+
scheduledEnd: eventEndIso(live.event),
|
|
495
|
+
actualStart: null,
|
|
496
|
+
actualEnd: null,
|
|
497
|
+
platform: detectPlatform(joinUrl),
|
|
498
|
+
joinUrl: joinUrl ?? null,
|
|
499
|
+
calendarEventId: snapshot.id,
|
|
500
|
+
recordingId: null,
|
|
501
|
+
transcriptStatus: "idle",
|
|
502
|
+
summaryMd: "",
|
|
503
|
+
bulletsJson: "[]",
|
|
504
|
+
actionItemsJson: "[]",
|
|
505
|
+
source: "calendar",
|
|
506
|
+
reminderFiredAt: null,
|
|
507
|
+
createdAt: nowIso,
|
|
508
|
+
updatedAt: nowIso,
|
|
509
|
+
ownerEmail,
|
|
510
|
+
orgId,
|
|
511
|
+
visibility: "private",
|
|
512
|
+
} as any);
|
|
513
|
+
|
|
514
|
+
const participants = calendarEventParticipants(live.event).filter(
|
|
515
|
+
(p) => p.email,
|
|
516
|
+
);
|
|
517
|
+
if (participants.length) {
|
|
518
|
+
await tx.insert(schema.meetingParticipants).values(
|
|
519
|
+
participants.map((p) => ({
|
|
520
|
+
id: nanoid(),
|
|
521
|
+
meetingId,
|
|
522
|
+
email: p.email,
|
|
523
|
+
name: p.name ?? null,
|
|
524
|
+
isOrganizer: !!p.isOrganizer,
|
|
525
|
+
attendedAt: null,
|
|
526
|
+
createdAt: nowIso,
|
|
527
|
+
})) as any,
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
} catch (err) {
|
|
531
|
+
// Roll back the claim so a future call can retry — but only if it still
|
|
532
|
+
// points at our own meetingId (don't clobber a legitimate later claim).
|
|
533
|
+
await tx
|
|
534
|
+
.update(schema.calendarEvents)
|
|
535
|
+
.set({ meetingId: null, updatedAt: new Date().toISOString() })
|
|
536
|
+
.where(
|
|
537
|
+
and(
|
|
538
|
+
eq(schema.calendarEvents.id, snapshot.id),
|
|
539
|
+
eq(schema.calendarEvents.meetingId, meetingId),
|
|
540
|
+
),
|
|
541
|
+
)
|
|
542
|
+
.catch(() => {});
|
|
543
|
+
throw err;
|
|
544
|
+
}
|
|
514
545
|
|
|
515
|
-
|
|
546
|
+
const [meeting] = await tx
|
|
547
|
+
.select()
|
|
548
|
+
.from(schema.meetings)
|
|
549
|
+
.where(eq(schema.meetings.id, meetingId))
|
|
550
|
+
.limit(1);
|
|
551
|
+
|
|
552
|
+
return { meeting, created: true };
|
|
553
|
+
});
|
|
516
554
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type RecordingPageAccessRole = "owner" | "admin" | "editor" | "viewer";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Match the public recording and video routes: only a finite expiry date can
|
|
5
|
+
* expire a recording, and the expiry is strict so the exact instant remains
|
|
6
|
+
* valid until time advances past it.
|
|
7
|
+
*/
|
|
8
|
+
export function isRecordingExpired(
|
|
9
|
+
expiresAt: string | null | undefined,
|
|
10
|
+
now = Date.now(),
|
|
11
|
+
): boolean {
|
|
12
|
+
if (!expiresAt) return false;
|
|
13
|
+
|
|
14
|
+
const expires = new Date(expiresAt).getTime();
|
|
15
|
+
return Number.isFinite(expires) && expires < now;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Decide whether an authenticated request may use the editor/player route.
|
|
20
|
+
* Public visibility is intentionally a share-link concern; a direct `/r/*`
|
|
21
|
+
* request must either be the owner or have an explicit share grant.
|
|
22
|
+
* Password-protected recordings stay on the password-aware share flow for
|
|
23
|
+
* every non-owner so the authenticated action cannot mint a bypass token.
|
|
24
|
+
*/
|
|
25
|
+
export function canOpenDirectRecordingPage(input: {
|
|
26
|
+
role: RecordingPageAccessRole;
|
|
27
|
+
visibility: "private" | "org" | "public";
|
|
28
|
+
hasPassword: boolean;
|
|
29
|
+
hasExplicitShare: boolean;
|
|
30
|
+
}): boolean {
|
|
31
|
+
if (input.role === "owner") return true;
|
|
32
|
+
if (input.hasPassword) return false;
|
|
33
|
+
if (input.visibility === "public") return input.hasExplicitShare;
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
@@ -91,7 +91,8 @@ export function DescriptionField({
|
|
|
91
91
|
return (
|
|
92
92
|
<Textarea
|
|
93
93
|
aria-label={label}
|
|
94
|
-
rows={
|
|
94
|
+
rows={1}
|
|
95
|
+
wrap="soft"
|
|
95
96
|
value={draft}
|
|
96
97
|
placeholder={placeholder}
|
|
97
98
|
onFocus={() => setEditing(true)}
|
|
@@ -105,6 +106,12 @@ export function DescriptionField({
|
|
|
105
106
|
void save();
|
|
106
107
|
}}
|
|
107
108
|
onKeyDown={(event) => {
|
|
109
|
+
if (event.key === "Enter") {
|
|
110
|
+
if (event.nativeEvent.isComposing || event.keyCode === 229) return;
|
|
111
|
+
event.preventDefault();
|
|
112
|
+
event.currentTarget.blur();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
108
115
|
if (event.key === "Escape") {
|
|
109
116
|
event.preventDefault();
|
|
110
117
|
skipNextBlurSaveRef.current = true;
|
|
@@ -112,8 +119,9 @@ export function DescriptionField({
|
|
|
112
119
|
event.currentTarget.blur();
|
|
113
120
|
}
|
|
114
121
|
}}
|
|
122
|
+
style={{ fieldSizing: "content" } as any}
|
|
115
123
|
className={cn(
|
|
116
|
-
"mt-2
|
|
124
|
+
"mt-2 min-h-6 w-full resize-none overflow-hidden rounded-none border-0 bg-transparent px-0 py-0 text-sm leading-6 text-muted-foreground shadow-none outline-none placeholder:text-muted-foreground/45 focus:outline-none focus:ring-0 focus:ring-offset-0 focus-visible:ring-0 focus-visible:ring-offset-0",
|
|
117
125
|
className,
|
|
118
126
|
)}
|
|
119
127
|
/>
|
|
@@ -110,12 +110,16 @@ export function sanitizeLocalhostSourceSnapshotHtml(html: string): string {
|
|
|
110
110
|
);
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
// Prototypes are still sandboxed, but user-initiated print/download controls
|
|
114
|
+
// need the browser permissions that make `window.print()` and download links
|
|
115
|
+
// work inside an iframe. Without these tokens the browser silently ignores
|
|
116
|
+
// the common "Download / Print PDF" pattern.
|
|
113
117
|
const EXTERNAL_PREVIEW_IFRAME_SANDBOX =
|
|
114
|
-
"allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox allow-same-origin";
|
|
118
|
+
"allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox allow-downloads allow-modals allow-same-origin";
|
|
115
119
|
const EDITABLE_INLINE_IFRAME_SANDBOX =
|
|
116
|
-
"allow-scripts allow-popups allow-popups-to-escape-sandbox allow-same-origin";
|
|
120
|
+
"allow-scripts allow-popups allow-popups-to-escape-sandbox allow-downloads allow-modals allow-same-origin";
|
|
117
121
|
const READ_ONLY_INLINE_IFRAME_SANDBOX =
|
|
118
|
-
"allow-scripts allow-popups allow-popups-to-escape-sandbox";
|
|
122
|
+
"allow-scripts allow-popups allow-popups-to-escape-sandbox allow-downloads allow-modals";
|
|
119
123
|
|
|
120
124
|
export function getDesignCanvasIframeSandbox(args: {
|
|
121
125
|
externalPreview: boolean;
|
|
@@ -11,7 +11,7 @@ export default async function (args: string[]) {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
const buf = fs.readFileSync(pdfPath);
|
|
14
|
-
const pdf = new PDFParse(new Uint8Array(buf));
|
|
14
|
+
const pdf = new PDFParse({ data: new Uint8Array(buf) });
|
|
15
15
|
const result = await pdf.getText();
|
|
16
16
|
const pages = result.pages || [];
|
|
17
17
|
console.log("Total pages:", pages.length);
|
|
@@ -214,7 +214,10 @@ export default defineAction({
|
|
|
214
214
|
const { PDFParse } = await import("pdf-parse");
|
|
215
215
|
const { convertSectionsToSlides } =
|
|
216
216
|
await import("../server/handlers/import/html-converter.js");
|
|
217
|
-
|
|
217
|
+
// pdf-parse v2 expects a LoadParameters object. Passing the byte array
|
|
218
|
+
// directly uses the old v1 shape and can fail while pdf.js initializes
|
|
219
|
+
// in production (the reported DOMMatrix error).
|
|
220
|
+
const pdf = new PDFParse({ data: new Uint8Array(fileBuffer) });
|
|
218
221
|
const result = await pdf.getText();
|
|
219
222
|
const pages = normalizePdfPages(result);
|
|
220
223
|
const textPages = pages.filter((p) => p.text.trim());
|
|
@@ -69,7 +69,7 @@ export default function ImageGenPanel({
|
|
|
69
69
|
const contextParts: string[] = [];
|
|
70
70
|
|
|
71
71
|
contextParts.push(
|
|
72
|
-
"Generate 3 image variations
|
|
72
|
+
"Generate 3 image variations by calling the registered `generate-image-api` action three times with the same prompt.",
|
|
73
73
|
);
|
|
74
74
|
contextParts.push(
|
|
75
75
|
"The deliverables must be actual generated image assets from the action. Do not create placeholder HTML/CSS, oversized icon compositions, inline SVGs, or text-only mockups.",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { callAction, useT } from "@agent-native/core/client";
|
|
2
2
|
import { IconX, IconSearch, IconLoader2 } from "@tabler/icons-react";
|
|
3
3
|
import { useState, useEffect, useRef } from "react";
|
|
4
4
|
import { createPortal } from "react-dom";
|
|
@@ -61,18 +61,15 @@ export default function ImageSearchPanel({
|
|
|
61
61
|
setLoading(true);
|
|
62
62
|
setError(null);
|
|
63
63
|
try {
|
|
64
|
-
const
|
|
65
|
-
|
|
64
|
+
const data = await callAction<SearchResult[]>(
|
|
65
|
+
"search-images",
|
|
66
|
+
{ q: query.trim() },
|
|
67
|
+
{ method: "GET" },
|
|
66
68
|
);
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
} else {
|
|
72
|
-
setResults(await res.json());
|
|
73
|
-
}
|
|
74
|
-
} catch {
|
|
75
|
-
setError("Search failed");
|
|
69
|
+
setResults(data);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
setError(error instanceof Error ? error.message : t("raw.searchFailed"));
|
|
72
|
+
setResults([]);
|
|
76
73
|
} finally {
|
|
77
74
|
setLoading(false);
|
|
78
75
|
}
|