@agent-native/core 0.196.0 → 0.196.1-nightly-20260928204657
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/templates/clips/.agents/skills/video-sharing/SKILL.md +1 -1
- package/corpus/templates/clips/actions/create-recording.ts +1 -1
- package/corpus/templates/clips/actions/lib/auto-title-candidates.ts +42 -0
- package/corpus/templates/clips/actions/lib/create-recording-schema.ts +1 -1
- package/corpus/templates/clips/actions/lib/transcript-text.ts +16 -0
- package/corpus/templates/clips/actions/list-ai-requests.ts +49 -31
- package/corpus/templates/clips/actions/list-recordings.ts +2 -4
- package/corpus/templates/clips/actions/list-transactional-email-ai-requests.ts +12 -6
- package/corpus/templates/clips/actions/regenerate-title.ts +4 -1
- package/corpus/templates/clips/actions/request-transcript.ts +1 -1
- package/corpus/templates/clips/actions/save-browser-transcript.ts +1 -1
- package/corpus/templates/clips/app/components/editable-recording-title.tsx +1 -1
- package/corpus/templates/clips/app/components/library/recording-card.tsx +1 -1
- package/corpus/templates/clips/app/hooks/use-auto-title.ts +194 -219
- package/corpus/templates/clips/app/routes/_app.r.$recordingId.tsx +2 -1
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +1 -1
- package/corpus/templates/clips/changelog/2026-09-28-clip-share-previews-show-a-branded-image-while-the-recording.md +6 -0
- package/corpus/templates/clips/server/lib/slack-unfurls.ts +2 -0
- package/corpus/templates/clips/server/lib/transactional-email-store.ts +42 -2
- package/corpus/templates/clips/server/routes/api/thumbnail/[recordingId].get.ts +27 -15
- package/corpus/templates/clips/shared/ai-request-status.ts +6 -0
- package/corpus/templates/clips/shared/share-meta.ts +2 -27
- package/corpus/templates/clips/{actions/lib → shared}/title-source.ts +9 -2
- package/corpus/templates/slides/changelog/2026-09-28-typing-in-slides-agent-chat-keeps-every-character-in-order.md +6 -0
- package/corpus/templates/slides/scripts/edit-fidelity/README.md +11 -2
- package/corpus/templates/slides/scripts/edit-fidelity/run.ts +135 -4
- package/dist/agent/production-agent.js +2 -0
- package/dist/agent/run-manager.d.ts +2 -0
- package/dist/agent/run-manager.js +9 -2
- package/dist/agent/thread-data-builder.d.ts +1 -0
- package/dist/agent/thread-data-builder.js +148 -0
- package/dist/chat-threads/store.d.ts +1 -0
- package/dist/chat-threads/store.js +244 -21
- package/dist/client/AgentKitAssistantChat.js +23 -12
- package/dist/client/chat/agentkit-agent-native.js +409 -8
- package/dist/client/chat/agentkit-protocol.js +3 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +6 -6
- package/dist/org/handlers.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +4 -4
- package/dist/server/agent-chat-plugin.d.ts +2 -0
- package/dist/server/agent-chat-plugin.js +12 -7
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-engine-ollama-models-route.d.ts +2 -2
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/sharing/actions/list-resource-shares.d.ts +1 -1
- package/package.json +2 -2
|
@@ -8,46 +8,36 @@ import {
|
|
|
8
8
|
bumpChangeVersion,
|
|
9
9
|
callAction,
|
|
10
10
|
getChangeVersion,
|
|
11
|
-
|
|
11
|
+
useActionQuery,
|
|
12
|
+
useChangeVersion,
|
|
12
13
|
} from "@agent-native/core/client/hooks";
|
|
13
14
|
import {
|
|
14
15
|
aiRequestTabId,
|
|
15
16
|
parseAiRequestTabId,
|
|
17
|
+
type AutoTitleCandidate,
|
|
16
18
|
type ClipsAiRequestKind,
|
|
17
19
|
} from "@shared/ai-request-status";
|
|
18
20
|
import { fullVideoAiModelSelection } from "@shared/clips-ai-prefs";
|
|
19
21
|
import { useEffect, useRef } from "react";
|
|
20
22
|
|
|
21
|
-
import { useRecordings, type RecordingSummary } from "./use-library";
|
|
22
|
-
|
|
23
|
-
const DEFAULT_TITLE = "Untitled recording";
|
|
24
23
|
const TWO_MINUTES_MS = 2 * 60 * 1000;
|
|
25
24
|
export const WORKFLOW_ACTION_MAX_ATTEMPTS = 5;
|
|
26
25
|
const WORKFLOW_ACTION_RETRY_DELAY_MS = 1000;
|
|
27
|
-
|
|
26
|
+
// `queueAiRequest` publishes `refresh-signal` after every queue, whoever the
|
|
27
|
+
// writer is. Sync drops this tab's own events, so local queues bump it too.
|
|
28
|
+
const AI_REQUEST_REFRESH_SOURCE = "app-state:refresh-signal";
|
|
28
29
|
const AI_REQUEST_DELIVERY_TIMEOUT_MS = 10_000;
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
export function isDefaultTitle(title: string | null | undefined): boolean {
|
|
37
|
-
const trimmed = (title ?? "").trim();
|
|
38
|
-
if (!trimmed) return true;
|
|
39
|
-
return trimmed === DEFAULT_TITLE;
|
|
31
|
+
function bumpAiRequestRefresh(): void {
|
|
32
|
+
bumpChangeVersion(
|
|
33
|
+
AI_REQUEST_REFRESH_SOURCE,
|
|
34
|
+
Math.max(Date.now(), getChangeVersion(AI_REQUEST_REFRESH_SOURCE) + 1),
|
|
35
|
+
);
|
|
40
36
|
}
|
|
41
37
|
|
|
42
|
-
export function
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
): boolean {
|
|
46
|
-
return (
|
|
47
|
-
isDefaultTitle(title) ||
|
|
48
|
-
titleSource === "default" ||
|
|
49
|
-
titleSource === "context"
|
|
50
|
-
);
|
|
38
|
+
export function notifyAiRequestQueued(recordingId: string): void {
|
|
39
|
+
if (!recordingId) return;
|
|
40
|
+
bumpAiRequestRefresh();
|
|
51
41
|
}
|
|
52
42
|
|
|
53
43
|
interface AiRequest {
|
|
@@ -70,6 +60,13 @@ interface AiRequest {
|
|
|
70
60
|
deliveredTabId?: string;
|
|
71
61
|
}
|
|
72
62
|
|
|
63
|
+
type QueuedAiRequest = AiRequest & { recordingId: string };
|
|
64
|
+
|
|
65
|
+
interface ListAiRequestsResult {
|
|
66
|
+
requests: QueuedAiRequest[];
|
|
67
|
+
titleCandidates: AutoTitleCandidate[];
|
|
68
|
+
}
|
|
69
|
+
|
|
73
70
|
const DISPATCHABLE_REQUESTS = new Set([
|
|
74
71
|
"generate-metadata",
|
|
75
72
|
"regenerate-title",
|
|
@@ -80,23 +77,6 @@ const DISPATCHABLE_REQUESTS = new Set([
|
|
|
80
77
|
"generate-workflow",
|
|
81
78
|
]);
|
|
82
79
|
|
|
83
|
-
async function listRequests(): Promise<Map<string, AiRequest>> {
|
|
84
|
-
try {
|
|
85
|
-
const result = (await callAction("list-ai-requests", {} as any, {
|
|
86
|
-
method: "GET",
|
|
87
|
-
})) as { requests?: AiRequest[] } | null | undefined;
|
|
88
|
-
return new Map(
|
|
89
|
-
(result?.requests ?? [])
|
|
90
|
-
.filter(
|
|
91
|
-
(r): r is AiRequest & { recordingId: string } => !!r?.recordingId,
|
|
92
|
-
)
|
|
93
|
-
.map((r) => [r.recordingId, r]),
|
|
94
|
-
);
|
|
95
|
-
} catch {
|
|
96
|
-
return new Map();
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
80
|
async function clearRequest(recordingId: string): Promise<void> {
|
|
101
81
|
const url = agentNativePath(
|
|
102
82
|
`/_agent-native/application-state/${encodeURIComponent(
|
|
@@ -104,11 +84,15 @@ async function clearRequest(recordingId: string): Promise<void> {
|
|
|
104
84
|
)}`,
|
|
105
85
|
);
|
|
106
86
|
await fetch(url, { method: "DELETE" }).catch(() => {});
|
|
87
|
+
bumpAiRequestRefresh();
|
|
107
88
|
}
|
|
108
89
|
|
|
109
90
|
export function useAutoTitleBridge(): void {
|
|
110
|
-
const { data } =
|
|
111
|
-
|
|
91
|
+
const { data, refetch } = useActionQuery<ListAiRequestsResult>(
|
|
92
|
+
"list-ai-requests",
|
|
93
|
+
{} as any,
|
|
94
|
+
);
|
|
95
|
+
const refreshVersion = useChangeVersion(AI_REQUEST_REFRESH_SOURCE);
|
|
112
96
|
const dispatched = useRef<Set<string>>(new Set());
|
|
113
97
|
const inflight = useRef<boolean>(false);
|
|
114
98
|
|
|
@@ -157,163 +141,158 @@ export function useAutoTitleBridge(): void {
|
|
|
157
141
|
window.removeEventListener("agentNative.chatRunning", handleChatRunning);
|
|
158
142
|
}, []);
|
|
159
143
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
(r) =>
|
|
164
|
-
`${r.id}:${r.titleSource ?? ""}:${r.title}:${r.updatedAt}:${r.transcriptStatus ?? ""}:${r.transcriptHasText ? "1" : "0"}`,
|
|
165
|
-
)
|
|
166
|
-
.join("|");
|
|
167
|
-
const aiRequestVersion = useChangeVersions(
|
|
168
|
-
readyRecordings.map(
|
|
169
|
-
(recording) => `app-state:clips-ai-request-${recording.id}`,
|
|
170
|
-
),
|
|
171
|
-
);
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
if (refreshVersion) void refetch();
|
|
146
|
+
}, [refreshVersion, refetch]);
|
|
172
147
|
|
|
173
148
|
useEffect(() => {
|
|
174
|
-
if (
|
|
149
|
+
if (!data) return;
|
|
175
150
|
let cancelled = false;
|
|
176
|
-
let
|
|
151
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
152
|
+
|
|
153
|
+
function retrySoon() {
|
|
154
|
+
timer = setTimeout(() => {
|
|
155
|
+
void refetch().then((result) => {
|
|
156
|
+
if (!cancelled && result.data) void deliver(result.data);
|
|
157
|
+
});
|
|
158
|
+
}, 1000);
|
|
159
|
+
}
|
|
177
160
|
|
|
178
|
-
async function
|
|
161
|
+
async function deliver(snapshot: ListAiRequestsResult) {
|
|
179
162
|
if (cancelled) return;
|
|
180
163
|
if (inflight.current) {
|
|
181
|
-
|
|
164
|
+
timer = setTimeout(() => void deliver(snapshot), 50);
|
|
182
165
|
return;
|
|
183
166
|
}
|
|
184
167
|
inflight.current = true;
|
|
185
168
|
try {
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
for (const rec of readyRecordings) {
|
|
169
|
+
const pending = new Set<string>();
|
|
170
|
+
for (const request of snapshot.requests) {
|
|
190
171
|
if (cancelled) return;
|
|
172
|
+
if (!request.kind || !DISPATCHABLE_REQUESTS.has(request.kind)) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
pending.add(request.recordingId);
|
|
176
|
+
const dispatchKey = `${request.recordingId}:${request.kind}:${
|
|
177
|
+
request.requestId ?? request.requestedAt ?? "0"
|
|
178
|
+
}`;
|
|
179
|
+
if (dispatched.current.has(dispatchKey)) continue;
|
|
180
|
+
if (
|
|
181
|
+
request.kind === "generate-metadata" ||
|
|
182
|
+
request.kind === "regenerate-title"
|
|
183
|
+
) {
|
|
184
|
+
dispatched.current.add(fallbackKey(request.recordingId));
|
|
185
|
+
}
|
|
191
186
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
) {
|
|
203
|
-
dispatched.current.add(`${rec.id}:fallback`);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
if (
|
|
207
|
-
request.kind === "generate-workflow" &&
|
|
208
|
-
typeof request.requestedAt === "string"
|
|
209
|
-
) {
|
|
210
|
-
const workflowRequest = {
|
|
211
|
-
recordingId: rec.id,
|
|
212
|
-
requestedAt: request.requestedAt,
|
|
213
|
-
...(request.requestId ? { requestId: request.requestId } : {}),
|
|
214
|
-
};
|
|
215
|
-
if (request.deliveredTabId) {
|
|
216
|
-
dispatched.current.add(dispatchKey);
|
|
217
|
-
void consumeWorkflowRequest({
|
|
218
|
-
...workflowRequest,
|
|
219
|
-
tabId: request.deliveredTabId,
|
|
220
|
-
});
|
|
221
|
-
continue;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
const tabId = workflowTabId(
|
|
225
|
-
rec.id,
|
|
226
|
-
request.requestedAt,
|
|
227
|
-
request.requestId,
|
|
228
|
-
);
|
|
229
|
-
try {
|
|
230
|
-
const result = (await callAction(
|
|
231
|
-
"reconcile-workflow-generation" as any,
|
|
232
|
-
{
|
|
233
|
-
operation: "track",
|
|
234
|
-
...workflowRequest,
|
|
235
|
-
tabId,
|
|
236
|
-
} as any,
|
|
237
|
-
)) as { tracked?: boolean };
|
|
238
|
-
if (result.tracked !== true) {
|
|
239
|
-
fallbackTimer = setTimeout(() => void tick(), 1000);
|
|
240
|
-
continue;
|
|
241
|
-
}
|
|
242
|
-
} catch {
|
|
243
|
-
fallbackTimer = setTimeout(() => void tick(), 1000);
|
|
244
|
-
continue;
|
|
245
|
-
}
|
|
246
|
-
const delivery = await sendToAgentChatAndConfirm({
|
|
247
|
-
...buildAiRequestChatOptions(rec, request),
|
|
248
|
-
tabId,
|
|
249
|
-
chatTarget: "local",
|
|
250
|
-
});
|
|
251
|
-
if (!delivery.delivered) {
|
|
252
|
-
await retryWorkflowAction(
|
|
253
|
-
{
|
|
254
|
-
operation: "release",
|
|
255
|
-
...workflowRequest,
|
|
256
|
-
tabId,
|
|
257
|
-
},
|
|
258
|
-
"released",
|
|
259
|
-
);
|
|
260
|
-
fallbackTimer = setTimeout(() => void tick(), 1000);
|
|
261
|
-
continue;
|
|
262
|
-
}
|
|
187
|
+
if (
|
|
188
|
+
request.kind === "generate-workflow" &&
|
|
189
|
+
typeof request.requestedAt === "string"
|
|
190
|
+
) {
|
|
191
|
+
const workflowRequest = {
|
|
192
|
+
recordingId: request.recordingId,
|
|
193
|
+
requestedAt: request.requestedAt,
|
|
194
|
+
...(request.requestId ? { requestId: request.requestId } : {}),
|
|
195
|
+
};
|
|
196
|
+
if (request.deliveredTabId) {
|
|
263
197
|
dispatched.current.add(dispatchKey);
|
|
264
|
-
void
|
|
198
|
+
void consumeWorkflowRequest({
|
|
265
199
|
...workflowRequest,
|
|
266
|
-
tabId,
|
|
200
|
+
tabId: request.deliveredTabId,
|
|
267
201
|
});
|
|
268
202
|
continue;
|
|
269
203
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
204
|
+
|
|
205
|
+
const tabId = workflowTabId(
|
|
206
|
+
request.recordingId,
|
|
207
|
+
request.requestedAt,
|
|
208
|
+
request.requestId,
|
|
209
|
+
);
|
|
210
|
+
try {
|
|
211
|
+
const result = (await callAction(
|
|
212
|
+
"reconcile-workflow-generation" as any,
|
|
213
|
+
{
|
|
214
|
+
operation: "track",
|
|
215
|
+
...workflowRequest,
|
|
216
|
+
tabId,
|
|
217
|
+
} as any,
|
|
218
|
+
)) as { tracked?: boolean };
|
|
219
|
+
if (result.tracked !== true) {
|
|
220
|
+
retrySoon();
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
} catch {
|
|
224
|
+
retrySoon();
|
|
279
225
|
continue;
|
|
280
226
|
}
|
|
281
|
-
const delivery = await
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
request.kind as ClipsAiRequestKind,
|
|
287
|
-
request.requestedAt,
|
|
288
|
-
),
|
|
289
|
-
);
|
|
227
|
+
const delivery = await sendToAgentChatAndConfirm({
|
|
228
|
+
...buildAiRequestChatOptions(request),
|
|
229
|
+
tabId,
|
|
230
|
+
chatTarget: "local",
|
|
231
|
+
});
|
|
290
232
|
if (!delivery.delivered) {
|
|
291
|
-
|
|
292
|
-
|
|
233
|
+
await retryWorkflowAction(
|
|
234
|
+
{
|
|
235
|
+
operation: "release",
|
|
236
|
+
...workflowRequest,
|
|
237
|
+
tabId,
|
|
238
|
+
},
|
|
239
|
+
"released",
|
|
240
|
+
);
|
|
241
|
+
retrySoon();
|
|
293
242
|
continue;
|
|
294
243
|
}
|
|
295
244
|
dispatched.current.add(dispatchKey);
|
|
296
|
-
void
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
245
|
+
void persistAndConsumeWorkflowRequest({
|
|
246
|
+
...workflowRequest,
|
|
247
|
+
tabId,
|
|
248
|
+
});
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (
|
|
252
|
+
typeof request.requestedAt !== "string" ||
|
|
253
|
+
!request.requestedAt.trim()
|
|
254
|
+
) {
|
|
255
|
+
console.warn("[clips] queued AI request is missing requestedAt", {
|
|
256
|
+
recordingId: request.recordingId,
|
|
257
|
+
kind: request.kind,
|
|
258
|
+
});
|
|
259
|
+
retrySoon();
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
const delivery = await dispatchAiRequest(
|
|
263
|
+
request,
|
|
264
|
+
aiRequestTabId(
|
|
265
|
+
request.recordingId,
|
|
266
|
+
request.kind as ClipsAiRequestKind,
|
|
267
|
+
request.requestedAt,
|
|
268
|
+
),
|
|
269
|
+
);
|
|
270
|
+
if (!delivery.delivered) {
|
|
271
|
+
dispatched.current.delete(dispatchKey);
|
|
272
|
+
retrySoon();
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
dispatched.current.add(dispatchKey);
|
|
276
|
+
void clearRequest(request.recordingId);
|
|
277
|
+
}
|
|
311
278
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
279
|
+
for (const candidate of snapshot.titleCandidates) {
|
|
280
|
+
if (cancelled) return;
|
|
281
|
+
if (pending.has(candidate.id)) continue;
|
|
282
|
+
if (autoTitleFallbackDelay(candidate, dispatched.current) !== 0) {
|
|
283
|
+
continue;
|
|
316
284
|
}
|
|
285
|
+
dispatched.current.add(fallbackKey(candidate.id));
|
|
286
|
+
callAction(
|
|
287
|
+
"regenerate-title" as any,
|
|
288
|
+
{ recordingId: candidate.id } as any,
|
|
289
|
+
)
|
|
290
|
+
.then((result) => {
|
|
291
|
+
if ((result as { queued?: boolean } | null)?.queued === true) {
|
|
292
|
+
bumpAiRequestRefresh();
|
|
293
|
+
}
|
|
294
|
+
})
|
|
295
|
+
.catch(() => {});
|
|
317
296
|
}
|
|
318
297
|
} finally {
|
|
319
298
|
inflight.current = false;
|
|
@@ -321,66 +300,67 @@ export function useAutoTitleBridge(): void {
|
|
|
321
300
|
}
|
|
322
301
|
|
|
323
302
|
function scheduleNextFallback() {
|
|
324
|
-
if (cancelled) return;
|
|
303
|
+
if (cancelled || !data) return;
|
|
325
304
|
const delay = nextAutoTitleFallbackDelay(
|
|
326
|
-
|
|
305
|
+
data.titleCandidates,
|
|
327
306
|
dispatched.current,
|
|
328
307
|
);
|
|
329
308
|
if (delay === null) return;
|
|
330
|
-
|
|
309
|
+
timer = setTimeout(
|
|
331
310
|
() => {
|
|
332
|
-
|
|
333
|
-
void
|
|
311
|
+
timer = null;
|
|
312
|
+
void deliver(data).finally(scheduleNextFallback);
|
|
334
313
|
},
|
|
335
314
|
Math.max(delay, 50),
|
|
336
315
|
);
|
|
337
316
|
}
|
|
338
317
|
|
|
339
|
-
void
|
|
318
|
+
void deliver(data).finally(scheduleNextFallback);
|
|
340
319
|
return () => {
|
|
341
320
|
cancelled = true;
|
|
342
|
-
if (
|
|
321
|
+
if (timer) clearTimeout(timer);
|
|
343
322
|
};
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
323
|
+
}, [data, refetch]);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function fallbackKey(recordingId: string): string {
|
|
327
|
+
return `${recordingId}:fallback`;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Milliseconds until the auto-title fallback may run for this candidate, or
|
|
332
|
+
* `null` once it has been dispatched. Eligibility is decided by the server.
|
|
333
|
+
*/
|
|
334
|
+
function autoTitleFallbackDelay(
|
|
335
|
+
candidate: AutoTitleCandidate,
|
|
336
|
+
dispatched: ReadonlySet<string>,
|
|
337
|
+
now = Date.now(),
|
|
338
|
+
): number | null {
|
|
339
|
+
if (dispatched.has(fallbackKey(candidate.id))) return null;
|
|
340
|
+
const createdAt = new Date(candidate.createdAt).getTime();
|
|
341
|
+
return Number.isFinite(createdAt)
|
|
342
|
+
? Math.max(0, TWO_MINUTES_MS - (now - createdAt))
|
|
343
|
+
: 0;
|
|
348
344
|
}
|
|
349
345
|
|
|
350
346
|
export function nextAutoTitleFallbackDelay(
|
|
351
|
-
|
|
347
|
+
candidates: readonly AutoTitleCandidate[],
|
|
352
348
|
dispatched: ReadonlySet<string>,
|
|
353
349
|
now = Date.now(),
|
|
354
350
|
): number | null {
|
|
355
351
|
let nextDelay: number | null = null;
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
if (
|
|
359
|
-
if (!isAutoTitleReplaceable(recording.title, recording.titleSource)) {
|
|
360
|
-
continue;
|
|
361
|
-
}
|
|
362
|
-
if (
|
|
363
|
-
recording.transcriptStatus !== "ready" ||
|
|
364
|
-
recording.transcriptHasText !== true ||
|
|
365
|
-
dispatched.has(`${recording.id}:fallback`)
|
|
366
|
-
) {
|
|
367
|
-
continue;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
const createdAt = new Date(recording.createdAt).getTime();
|
|
371
|
-
const delay = Number.isFinite(createdAt)
|
|
372
|
-
? Math.max(0, TWO_MINUTES_MS - (now - createdAt))
|
|
373
|
-
: 0;
|
|
352
|
+
for (const candidate of candidates) {
|
|
353
|
+
const delay = autoTitleFallbackDelay(candidate, dispatched, now);
|
|
354
|
+
if (delay === null) continue;
|
|
374
355
|
nextDelay = nextDelay === null ? delay : Math.min(nextDelay, delay);
|
|
375
356
|
}
|
|
376
|
-
|
|
377
357
|
return nextDelay;
|
|
378
358
|
}
|
|
379
359
|
|
|
380
|
-
function buildRequestContext(
|
|
360
|
+
function buildRequestContext(request: QueuedAiRequest) {
|
|
381
361
|
return {
|
|
382
|
-
recordingId:
|
|
383
|
-
currentTitle: request.currentTitle ??
|
|
362
|
+
recordingId: request.recordingId,
|
|
363
|
+
currentTitle: request.currentTitle ?? "",
|
|
384
364
|
currentDescription: request.currentDescription ?? "",
|
|
385
365
|
transcript: request.transcriptText ?? "",
|
|
386
366
|
agentsContext: request.agentsContext ?? "",
|
|
@@ -393,8 +373,7 @@ function buildRequestContext(rec: RecordingSummary, request: AiRequest) {
|
|
|
393
373
|
}
|
|
394
374
|
|
|
395
375
|
export function buildAiRequestChatOptions(
|
|
396
|
-
|
|
397
|
-
request: AiRequest,
|
|
376
|
+
request: QueuedAiRequest,
|
|
398
377
|
): AgentChatMessage {
|
|
399
378
|
const includeFullVideo = request.includeFullVideoInAi === true;
|
|
400
379
|
const gemini = includeFullVideo ? fullVideoAiModelSelection() : null;
|
|
@@ -402,8 +381,8 @@ export function buildAiRequestChatOptions(
|
|
|
402
381
|
return {
|
|
403
382
|
message:
|
|
404
383
|
request.message ??
|
|
405
|
-
`Handle queued ${request.kind} work for recording ${
|
|
406
|
-
context: JSON.stringify(buildRequestContext(
|
|
384
|
+
`Handle queued ${request.kind} work for recording ${request.recordingId}.`,
|
|
385
|
+
context: JSON.stringify(buildRequestContext(request)),
|
|
407
386
|
submit: true,
|
|
408
387
|
openSidebar: openInChat ? true : false,
|
|
409
388
|
newTab: true,
|
|
@@ -491,14 +470,10 @@ function requestIdFromTab(tabId: string) {
|
|
|
491
470
|
return match ? decodeURIComponent(match[1]) : undefined;
|
|
492
471
|
}
|
|
493
472
|
|
|
494
|
-
function dispatchAiRequest(
|
|
495
|
-
rec: RecordingSummary,
|
|
496
|
-
request: AiRequest,
|
|
497
|
-
tabId: string,
|
|
498
|
-
) {
|
|
473
|
+
function dispatchAiRequest(request: QueuedAiRequest, tabId: string) {
|
|
499
474
|
return sendToAgentChatAndConfirm(
|
|
500
475
|
{
|
|
501
|
-
...buildAiRequestChatOptions(
|
|
476
|
+
...buildAiRequestChatOptions(request),
|
|
502
477
|
chatTarget: "local",
|
|
503
478
|
tabId,
|
|
504
479
|
},
|
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
buildShareContinuationQuery,
|
|
44
44
|
CLIP_SHARE_REF,
|
|
45
45
|
} from "@shared/share-attribution";
|
|
46
|
+
import { isDefaultTitle } from "@shared/title-source";
|
|
46
47
|
import type { WorkflowKind } from "@shared/workflow";
|
|
47
48
|
import {
|
|
48
49
|
IconCalendar,
|
|
@@ -141,7 +142,7 @@ import {
|
|
|
141
142
|
TooltipContent,
|
|
142
143
|
TooltipTrigger,
|
|
143
144
|
} from "@/components/ui/tooltip";
|
|
144
|
-
import {
|
|
145
|
+
import { notifyAiRequestQueued } from "@/hooks/use-auto-title";
|
|
145
146
|
import { useCompletionAudioCue } from "@/hooks/use-completion-audio-cue";
|
|
146
147
|
import { useFolders, useSpaces } from "@/hooks/use-library";
|
|
147
148
|
import { usePlayerShortcuts } from "@/hooks/use-player-shortcuts";
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
EnvironmentBadge,
|
|
19
19
|
} from "@agent-native/core/client/ui";
|
|
20
20
|
import { usePersistentSidebarCollapsed } from "@agent-native/toolkit/app-shell";
|
|
21
|
+
import { isDefaultTitle } from "@shared/title-source";
|
|
21
22
|
import {
|
|
22
23
|
IconAlertTriangle,
|
|
23
24
|
IconDeviceDesktop,
|
|
@@ -108,7 +109,6 @@ import {
|
|
|
108
109
|
TooltipContent,
|
|
109
110
|
TooltipTrigger,
|
|
110
111
|
} from "@/components/ui/tooltip";
|
|
111
|
-
import { isDefaultTitle } from "@/hooks/use-auto-title";
|
|
112
112
|
import { usePlayerShortcuts } from "@/hooks/use-player-shortcuts";
|
|
113
113
|
import { useSonnerLifecycleToast } from "@/hooks/use-sonner-lifecycle-toast";
|
|
114
114
|
import { useViewTracking } from "@/hooks/use-view-tracking";
|
|
@@ -40,6 +40,7 @@ type SlackUnfurlRecording = {
|
|
|
40
40
|
expiresAt: string | null;
|
|
41
41
|
videoUrl: string | null;
|
|
42
42
|
sourceAppName: string | null;
|
|
43
|
+
updatedAt: string | null;
|
|
43
44
|
};
|
|
44
45
|
|
|
45
46
|
export type SlackVideoBlock = {
|
|
@@ -289,6 +290,7 @@ export async function loadSlackVideoBlockForUrl(
|
|
|
289
290
|
expiresAt: schema.recordings.expiresAt,
|
|
290
291
|
videoUrl: schema.recordings.videoUrl,
|
|
291
292
|
sourceAppName: schema.recordings.sourceAppName,
|
|
293
|
+
updatedAt: schema.recordings.updatedAt,
|
|
292
294
|
})
|
|
293
295
|
.from(schema.recordings)
|
|
294
296
|
.where(eq(schema.recordings.id, share.id))
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
and,
|
|
5
|
+
asc,
|
|
6
|
+
eq,
|
|
7
|
+
inArray,
|
|
8
|
+
isNull,
|
|
9
|
+
lte,
|
|
10
|
+
or,
|
|
11
|
+
sql,
|
|
12
|
+
type SQL,
|
|
13
|
+
} from "drizzle-orm";
|
|
4
14
|
import { z } from "zod";
|
|
5
15
|
|
|
6
16
|
import { getDb, schema } from "../db/index.js";
|
|
@@ -195,8 +205,12 @@ export type TransactionalEmailConfig = z.infer<
|
|
|
195
205
|
export type TransactionalEmailJob = z.infer<typeof transactionalEmailJobSchema>;
|
|
196
206
|
export type RecapCopy = z.infer<typeof recapCopySchema>;
|
|
197
207
|
|
|
208
|
+
const AI_BACKED_TYPES = [
|
|
209
|
+
"two-clips",
|
|
210
|
+
] as const satisfies readonly TransactionalEmailJob["type"][];
|
|
211
|
+
|
|
198
212
|
export function isAiBackedType(type: TransactionalEmailJob["type"]): boolean {
|
|
199
|
-
return
|
|
213
|
+
return (AI_BACKED_TYPES as readonly string[]).includes(type);
|
|
200
214
|
}
|
|
201
215
|
|
|
202
216
|
export type TransactionalEmailStoreOptions = {
|
|
@@ -354,6 +368,31 @@ export function createTransactionalEmailStore(
|
|
|
354
368
|
).map(databaseJobToJob);
|
|
355
369
|
}
|
|
356
370
|
|
|
371
|
+
async function listAiClaimCandidates(
|
|
372
|
+
claimantEmail: string,
|
|
373
|
+
): Promise<TransactionalEmailJob[]> {
|
|
374
|
+
const claimant = claimantEmail.trim().toLowerCase();
|
|
375
|
+
return (
|
|
376
|
+
await getDb()
|
|
377
|
+
.select()
|
|
378
|
+
.from(schema.transactionalEmailJobs)
|
|
379
|
+
.where(
|
|
380
|
+
and(
|
|
381
|
+
inArray(schema.transactionalEmailJobs.state, [
|
|
382
|
+
"awaiting_ai",
|
|
383
|
+
"ai_dispatched",
|
|
384
|
+
]),
|
|
385
|
+
inArray(schema.transactionalEmailJobs.type, AI_BACKED_TYPES),
|
|
386
|
+
or(
|
|
387
|
+
sql`lower(trim(${schema.transactionalEmailJobs.recipient})) = ${claimant}`,
|
|
388
|
+
sql`lower(trim(${schema.transactionalEmailJobs.requestedBy})) = ${claimant}`,
|
|
389
|
+
),
|
|
390
|
+
),
|
|
391
|
+
)
|
|
392
|
+
.orderBy(asc(schema.transactionalEmailJobs.createdAt))
|
|
393
|
+
).map(databaseJobToJob);
|
|
394
|
+
}
|
|
395
|
+
|
|
357
396
|
async function replaceJob(
|
|
358
397
|
job: TransactionalEmailJob,
|
|
359
398
|
condition: SQL | undefined,
|
|
@@ -768,6 +807,7 @@ export function createTransactionalEmailStore(
|
|
|
768
807
|
enqueueOrConvergeFirstImport,
|
|
769
808
|
readJob,
|
|
770
809
|
listJobs,
|
|
810
|
+
listAiClaimCandidates,
|
|
771
811
|
transition,
|
|
772
812
|
claimAwaitingAi,
|
|
773
813
|
reclaimStaleAiDispatch,
|