@agent-native/core 0.175.4 → 0.176.0-nightly-20260826203614
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/recording/SKILL.md +9 -8
- package/corpus/templates/clips/actions/import-loom-recording.ts +1 -1
- package/corpus/templates/clips/actions/lib/loom-import-job.ts +52 -35
- package/corpus/templates/clips/app/components/clips-avatar.tsx +46 -0
- package/corpus/templates/clips/app/components/library/recording-card.tsx +8 -7
- package/corpus/templates/clips/app/components/library/space-card.tsx +8 -5
- package/corpus/templates/clips/app/components/meetings/attendee-stack.tsx +7 -8
- package/corpus/templates/clips/app/components/meetings/transcript-bubbles.tsx +19 -12
- package/corpus/templates/clips/app/components/workspace/members-list.tsx +8 -6
- package/corpus/templates/clips/app/components/workspace/notifications-list.tsx +8 -6
- package/corpus/templates/clips/app/components/workspace/top-creators-table.tsx +8 -6
- package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +8 -7
- package/corpus/templates/clips/changelog/2026-08-26-detected-calls-no-longer-duplicate-meetings-or-strand-the-prompt.md +6 -0
- package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +39 -22
- package/corpus/templates/clips/desktop/src-tauri/src/adhoc_meetings_watcher.rs +1765 -219
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +4 -0
- package/corpus/templates/clips/desktop/src-tauri/src/meetings_watcher.rs +45 -30
- package/corpus/templates/clips/desktop/src-tauri/src/notifications.rs +91 -0
- package/corpus/templates/clips/server/routes/_agent-native/google/callback.get.ts +16 -0
- package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +44 -22
- package/corpus/templates/content/app/components/ui/avatar.tsx +1 -0
- package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +4 -1
- package/corpus/templates/mail/actions/get-email.ts +68 -21
- package/corpus/templates/mail/actions/get-thread.ts +92 -51
- package/corpus/templates/mail/app/hooks/use-emails.ts +7 -4
- package/corpus/templates/mail/server/lib/mail-connector-catalog.ts +7 -4
- package/corpus/templates/plan/app/pages/PlansPage.tsx +23 -15
- package/corpus/templates/slides/app/components/comments/SlideCommentsPanel.tsx +18 -7
- package/corpus/templates/slides/app/components/ui/avatar.tsx +1 -0
- package/corpus/templates/tasks/DEVELOPING.md +8 -0
- package/corpus/templates/tasks/drizzle.config.ts +3 -1
- package/corpus/templates/tasks/drizzle.local.config.ts +6 -0
- package/corpus/templates/tasks/package.json +2 -1
- package/corpus/templates/tasks/server/db/migrations/meta/0000_snapshot.json +234 -0
- package/corpus/templates/tasks/server/db/migrations/meta/_journal.json +13 -0
- package/corpus/templates/tasks/server/db/migrations.ts +110 -0
- package/corpus/templates/tasks/server/plugins/db.ts +3 -87
- package/dist/client/review/ReviewThreadPanel.js +4 -2
- package/dist/collab/client.js +20 -5
- package/dist/collab/presence.d.ts +3 -1
- package/dist/collab/presence.js +48 -45
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/db/drizzle-config.d.ts +7 -0
- package/dist/db/drizzle-config.js +35 -9
- package/dist/db/drizzle-migrations.d.ts +28 -0
- package/dist/db/drizzle-migrations.js +81 -0
- package/dist/db/index.d.ts +1 -1
- package/dist/db/migrations.d.ts +13 -1
- package/dist/db/migrations.js +60 -31
- package/dist/db/schema.js +4 -1
- package/dist/deploy/build.d.ts +6 -0
- package/dist/deploy/build.js +41 -0
- package/dist/observability/routes.d.ts +5 -5
- package/dist/provider-api/actions/custom-provider-registration.d.ts +11 -11
- package/dist/provider-api/actions/provider-api.d.ts +9 -9
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/auth.js +3 -2
- package/dist/server/builder-browser.d.ts +0 -4
- package/dist/server/builder-browser.js +23 -8
- package/dist/server/google-oauth.d.ts +18 -0
- package/dist/server/google-oauth.js +3 -3
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/short-lived-token.d.ts +1 -1
- package/dist/server/short-lived-token.js +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/shared/google-profile-image.d.ts +2 -0
- package/dist/shared/google-profile-image.js +24 -0
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.js +1 -0
- package/docs/content/creating-templates.mdx +10 -1
- package/docs/content/server-database.mdx +46 -2
- package/docs/content/server-plugins.mdx +39 -3
- package/docs/content/webmcp.mdx +2 -0
- package/package.json +3 -2
package/corpus/README.md
CHANGED
|
@@ -15,7 +15,7 @@ Reach for this skill any time you touch the recorder: the record button, the in-
|
|
|
15
15
|
|
|
16
16
|
## Data model touched
|
|
17
17
|
|
|
18
|
-
- **`recordings`** — the row gets created as soon as the user presses Record or imports a source. Native/file recordings transition `uploading` → `processing` → `ready` (or `failed`). `videoUrl`, `durationMs`, `videoSizeBytes`, `width`, `height`, `hasAudio`, `hasCamera` are populated as the upload streams in. Loom imports use `import-loom-recording` and
|
|
18
|
+
- **`recordings`** — the row gets created as soon as the user presses Record or imports a source. Native/file recordings transition `uploading` → `processing` → `ready` (or `failed`). `videoUrl`, `durationMs`, `videoSizeBytes`, `width`, `height`, `hasAudio`, `hasCamera` are populated as the upload streams in. Loom imports use `import-loom-recording` and become ready with either a Clips-hosted MP4 or a Loom embed when MP4 export is unavailable.
|
|
19
19
|
- **`application_state.record-intent`** — the agent writes this when it wants to start a recording. The UI reads and clears it, then prompts for permission.
|
|
20
20
|
- **`application_state.navigation`** — set to `{ view: "record" }` while the recorder is active.
|
|
21
21
|
|
|
@@ -122,13 +122,14 @@ segments; never store Loom's signed CDN URLs.
|
|
|
122
122
|
When Loom exposes a downloadable public MP4, `import-loom-recording` downloads
|
|
123
123
|
it, reuploads the bytes to Clips storage, and creates a ready, playable
|
|
124
124
|
Clips-hosted recording, importing Loom's public transcript when the share page
|
|
125
|
-
exposes one. If Loom does not expose
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
Loom iframe and the native Clips editor
|
|
130
|
-
|
|
131
|
-
transcription
|
|
125
|
+
exposes one. If Loom allows public playback but does not expose an MP4, the
|
|
126
|
+
action keeps a ready, embed-backed recording and still imports the transcript
|
|
127
|
+
when available.
|
|
128
|
+
|
|
129
|
+
Embed-backed Loom imports render a Loom iframe and hide the native Clips editor.
|
|
130
|
+
Reuploaded Loom imports support Clips-native trimming, exports, frame extraction,
|
|
131
|
+
and upload-based transcription; ask the user to upload the original video file
|
|
132
|
+
when those capabilities are needed for an embed-backed import.
|
|
132
133
|
|
|
133
134
|
## Browser diagnostics and recorder install options
|
|
134
135
|
|
|
@@ -145,7 +145,7 @@ async function fetchLoomOembed(shareUrl: string) {
|
|
|
145
145
|
|
|
146
146
|
export default defineAction({
|
|
147
147
|
description:
|
|
148
|
-
"Import a public Loom share URL, or a direct link to a video file, into Clips as a playable recording. Loom links create the recording immediately and download/reupload Loom's public MP4
|
|
148
|
+
"Import a public Loom share URL, or a direct link to a video file, into Clips as a playable recording. Loom links create the recording immediately and download/reupload Loom's public MP4 when available, or keep a playable Loom embed when Loom allows playback but not MP4 export; public transcripts are imported in the background. Other direct video links (e.g. an MP4/WebM/MOV hosted by another screen recorder) are downloaded and reuploaded synchronously without transcript metadata — use request-transcript afterward. If storage is not connected, creates a waiting recording that can be retried after storage setup.",
|
|
149
149
|
schema: ImportLoomRecordingSchema,
|
|
150
150
|
run: async (args, actionContext) => {
|
|
151
151
|
const loomId = extractLoomVideoId(args.url);
|
|
@@ -8,13 +8,14 @@ import { ownerEmailMatches } from "../../server/lib/recordings.js";
|
|
|
8
8
|
import { transactionalEmailStore } from "../../server/lib/transactional-email-store.js";
|
|
9
9
|
import {
|
|
10
10
|
extractLoomVideoId,
|
|
11
|
+
loomEmbedUrlForId,
|
|
11
12
|
normalizeLoomShareUrl,
|
|
12
13
|
} from "../../shared/loom.js";
|
|
13
14
|
import {
|
|
14
15
|
fetchLoomTranscript,
|
|
15
16
|
loomTranscriptUnavailableMessage,
|
|
16
17
|
} from "./loom-transcript.js";
|
|
17
|
-
import { downloadLoomVideo } from "./loom-video.js";
|
|
18
|
+
import { downloadLoomVideo, LoomVideoUnavailableError } from "./loom-video.js";
|
|
18
19
|
|
|
19
20
|
export type LoomImportJobResult = {
|
|
20
21
|
status: "ready" | "failed";
|
|
@@ -147,7 +148,7 @@ export async function runLoomImportJob({
|
|
|
147
148
|
|
|
148
149
|
console.log("[loom-import] job started", { recordingId, claimId });
|
|
149
150
|
|
|
150
|
-
let media: Awaited<ReturnType<typeof downloadLoomVideo
|
|
151
|
+
let media: Awaited<ReturnType<typeof downloadLoomVideo>> | null = null;
|
|
151
152
|
try {
|
|
152
153
|
media = await downloadLoomVideo({ loomId, shareUrl });
|
|
153
154
|
console.log("[loom-import] download complete", {
|
|
@@ -156,40 +157,54 @@ export async function runLoomImportJob({
|
|
|
156
157
|
mimeType: media.mimeType,
|
|
157
158
|
});
|
|
158
159
|
} catch (err) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
160
|
+
// Loom's public player can work even when the viewer's role cannot export MP4.
|
|
161
|
+
if (err instanceof LoomVideoUnavailableError) {
|
|
162
|
+
console.warn("[loom-import] MP4 unavailable; keeping Loom embed", {
|
|
163
|
+
recordingId,
|
|
164
|
+
loomId,
|
|
165
|
+
});
|
|
166
|
+
} else {
|
|
167
|
+
return failLoomImport(
|
|
168
|
+
recordingId,
|
|
169
|
+
err instanceof Error ? err.message : String(err),
|
|
170
|
+
claimId,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
164
173
|
}
|
|
165
174
|
|
|
166
175
|
try {
|
|
167
|
-
const upload =
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
+
const upload = media
|
|
177
|
+
? await uploadFile({
|
|
178
|
+
data: media.bytes,
|
|
179
|
+
filename: `${recordingId}.mp4`,
|
|
180
|
+
mimeType: media.mimeType,
|
|
181
|
+
ownerEmail,
|
|
182
|
+
stableUrl: true,
|
|
183
|
+
recordAsset: false,
|
|
184
|
+
})
|
|
185
|
+
: null;
|
|
186
|
+
if (media && !upload?.url) {
|
|
176
187
|
return failLoomImport(
|
|
177
188
|
recordingId,
|
|
178
189
|
"File upload returned no URL. Check your storage provider configuration.",
|
|
179
190
|
claimId,
|
|
180
191
|
);
|
|
181
192
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
193
|
+
|
|
194
|
+
const videoUrl = upload?.url ?? loomEmbedUrlForId(loomId);
|
|
195
|
+
if (upload) {
|
|
196
|
+
console.log("[loom-import] reupload complete", {
|
|
197
|
+
recordingId,
|
|
198
|
+
videoUrl: upload.url,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
186
201
|
|
|
187
202
|
const now = new Date().toISOString();
|
|
188
203
|
const [mediaReady] = await db
|
|
189
204
|
.update(schema.recordings)
|
|
190
205
|
.set({
|
|
191
|
-
videoUrl
|
|
192
|
-
videoSizeBytes: media
|
|
206
|
+
videoUrl,
|
|
207
|
+
videoSizeBytes: media?.sizeBytes ?? 0,
|
|
193
208
|
status: "ready",
|
|
194
209
|
failureReason: null,
|
|
195
210
|
updatedAt: now,
|
|
@@ -213,20 +228,22 @@ export async function runLoomImportJob({
|
|
|
213
228
|
}
|
|
214
229
|
console.log("[loom-import] recording ready", { recordingId });
|
|
215
230
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
ownerEmail,
|
|
219
|
-
videoUrl: upload.url,
|
|
220
|
-
mimeType: media.mimeType,
|
|
221
|
-
providerId: upload.provider,
|
|
222
|
-
assetDbId: upload.id,
|
|
223
|
-
sourceSizeBytes: media.sizeBytes,
|
|
224
|
-
}).catch((err) => {
|
|
225
|
-
console.warn("[clips] Loom media compression queue failed", {
|
|
231
|
+
if (media && upload) {
|
|
232
|
+
void queueBuilderMediaCompression({
|
|
226
233
|
recordingId,
|
|
227
|
-
|
|
234
|
+
ownerEmail,
|
|
235
|
+
videoUrl: upload.url,
|
|
236
|
+
mimeType: media.mimeType,
|
|
237
|
+
providerId: upload.provider,
|
|
238
|
+
assetDbId: upload.id,
|
|
239
|
+
sourceSizeBytes: media.sizeBytes,
|
|
240
|
+
}).catch((err) => {
|
|
241
|
+
console.warn("[clips] Loom media compression queue failed", {
|
|
242
|
+
recordingId,
|
|
243
|
+
error: err instanceof Error ? err.message : String(err),
|
|
244
|
+
});
|
|
228
245
|
});
|
|
229
|
-
}
|
|
246
|
+
}
|
|
230
247
|
|
|
231
248
|
try {
|
|
232
249
|
let transcript: Awaited<ReturnType<typeof fetchLoomTranscript>> = null;
|
|
@@ -291,7 +308,7 @@ export async function runLoomImportJob({
|
|
|
291
308
|
recordingId,
|
|
292
309
|
status: "ready",
|
|
293
310
|
progress: 100,
|
|
294
|
-
videoUrl
|
|
311
|
+
videoUrl,
|
|
295
312
|
updatedAt: now,
|
|
296
313
|
});
|
|
297
314
|
await writeAppState("refresh-signal", { ts: Date.now() });
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {
|
|
2
|
+
forwardRef,
|
|
3
|
+
useCallback,
|
|
4
|
+
type ComponentPropsWithoutRef,
|
|
5
|
+
type ReactNode,
|
|
6
|
+
} from "react";
|
|
7
|
+
|
|
8
|
+
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
|
9
|
+
import { useVisibleAvatarUrl } from "@/lib/use-visible-avatar-url";
|
|
10
|
+
|
|
11
|
+
interface ClipsAvatarProps extends Omit<
|
|
12
|
+
ComponentPropsWithoutRef<typeof Avatar>,
|
|
13
|
+
"children"
|
|
14
|
+
> {
|
|
15
|
+
email: string | null | undefined;
|
|
16
|
+
alt: string;
|
|
17
|
+
fallback: ReactNode;
|
|
18
|
+
fallbackClassName?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const ClipsAvatar = forwardRef<HTMLSpanElement, ClipsAvatarProps>(
|
|
22
|
+
function ClipsAvatar(
|
|
23
|
+
{ email, alt, fallback, fallbackClassName, ...avatarProps },
|
|
24
|
+
forwardedRef,
|
|
25
|
+
) {
|
|
26
|
+
const { avatarRef, avatarUrl } = useVisibleAvatarUrl(email);
|
|
27
|
+
const setAvatarRef = useCallback(
|
|
28
|
+
(element: HTMLSpanElement | null) => {
|
|
29
|
+
avatarRef.current = element;
|
|
30
|
+
if (typeof forwardedRef === "function") forwardedRef(element);
|
|
31
|
+
else if (forwardedRef) forwardedRef.current = element;
|
|
32
|
+
},
|
|
33
|
+
[avatarRef, forwardedRef],
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<Avatar {...avatarProps} ref={setAvatarRef}>
|
|
38
|
+
{avatarUrl ? <AvatarImage src={avatarUrl} alt={alt} /> : null}
|
|
39
|
+
<AvatarFallback className={fallbackClassName}>
|
|
40
|
+
{fallback}
|
|
41
|
+
</AvatarFallback>
|
|
42
|
+
</Avatar>
|
|
43
|
+
);
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
ClipsAvatar.displayName = "ClipsAvatar";
|
|
@@ -17,9 +17,9 @@ import {
|
|
|
17
17
|
import { useCallback, useMemo, useRef, useState } from "react";
|
|
18
18
|
import { Link } from "react-router";
|
|
19
19
|
|
|
20
|
+
import { ClipsAvatar } from "@/components/clips-avatar";
|
|
20
21
|
import { AgentViewCount } from "@/components/player/recording-views-badge";
|
|
21
22
|
import { ViewedByPopover } from "@/components/sharing/viewed-by-popover";
|
|
22
|
-
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
|
23
23
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
24
24
|
import {
|
|
25
25
|
DropdownMenu,
|
|
@@ -340,12 +340,13 @@ export function RecordingCard({
|
|
|
340
340
|
</div>
|
|
341
341
|
)}
|
|
342
342
|
<div className="mt-1 flex min-w-0 items-center gap-1.5 text-[11px] text-muted-foreground">
|
|
343
|
-
<
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
343
|
+
<ClipsAvatar
|
|
344
|
+
email={recording.ownerEmail}
|
|
345
|
+
alt={recording.ownerEmail}
|
|
346
|
+
fallback={ownerInitials}
|
|
347
|
+
className="h-4 w-4 shrink-0"
|
|
348
|
+
fallbackClassName="bg-primary/15 text-[8px] text-primary"
|
|
349
|
+
/>
|
|
349
350
|
<span className="min-w-0 truncate">{recording.ownerEmail}</span>
|
|
350
351
|
<span aria-hidden>•</span>
|
|
351
352
|
<span className="shrink-0">{relative}</span>
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
import { useState } from "react";
|
|
10
10
|
import { Link } from "react-router";
|
|
11
11
|
|
|
12
|
+
import { ClipsAvatar } from "@/components/clips-avatar";
|
|
12
13
|
import {
|
|
13
14
|
ContextMenu,
|
|
14
15
|
ContextMenuContent,
|
|
@@ -104,13 +105,15 @@ export function SpaceCard({
|
|
|
104
105
|
.slice(0, 2)
|
|
105
106
|
.toUpperCase();
|
|
106
107
|
return (
|
|
107
|
-
<
|
|
108
|
+
<ClipsAvatar
|
|
108
109
|
key={email}
|
|
110
|
+
email={email}
|
|
111
|
+
alt={email}
|
|
112
|
+
fallback={initials}
|
|
113
|
+
className="h-5 w-5 border border-background"
|
|
114
|
+
fallbackClassName="bg-primary/15 text-[9px] font-medium text-primary"
|
|
109
115
|
title={email}
|
|
110
|
-
|
|
111
|
-
>
|
|
112
|
-
{initials}
|
|
113
|
-
</div>
|
|
116
|
+
/>
|
|
114
117
|
);
|
|
115
118
|
})}
|
|
116
119
|
{members.length > 5 && (
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* gets a shadcn Tooltip showing name/email. Used on both list cards and
|
|
6
6
|
* detail headers.
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
8
|
+
import { ClipsAvatar } from "@/components/clips-avatar";
|
|
9
9
|
import {
|
|
10
10
|
Tooltip,
|
|
11
11
|
TooltipContent,
|
|
@@ -58,14 +58,13 @@ export function AttendeeStack({
|
|
|
58
58
|
{visible.map((p, i) => (
|
|
59
59
|
<Tooltip key={`${p.email}-${i}`}>
|
|
60
60
|
<TooltipTrigger asChild>
|
|
61
|
-
<
|
|
61
|
+
<ClipsAvatar
|
|
62
|
+
email={p.email}
|
|
63
|
+
alt={p.name || p.email}
|
|
64
|
+
fallback={attendeeInitials(p)}
|
|
65
|
+
fallbackClassName="font-medium"
|
|
62
66
|
className={`${sizeClass} ring-2 ring-background cursor-default`}
|
|
63
|
-
|
|
64
|
-
<AvatarImage alt={p.name || p.email} />
|
|
65
|
-
<AvatarFallback className="font-medium">
|
|
66
|
-
{attendeeInitials(p)}
|
|
67
|
-
</AvatarFallback>
|
|
68
|
-
</Avatar>
|
|
67
|
+
/>
|
|
69
68
|
</TooltipTrigger>
|
|
70
69
|
<TooltipContent side="top" className="text-xs">
|
|
71
70
|
<div className="font-medium">{p.name || p.email}</div>
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
import type { KeyboardEvent, ReactNode } from "react";
|
|
10
10
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import { ClipsAvatar } from "@/components/clips-avatar";
|
|
13
13
|
import { Button } from "@/components/ui/button";
|
|
14
14
|
import { Input } from "@/components/ui/input";
|
|
15
15
|
import {
|
|
@@ -59,6 +59,7 @@ export interface SpeakerIdentity {
|
|
|
59
59
|
key: string;
|
|
60
60
|
label: string | null;
|
|
61
61
|
initialsSource: AttendeeStackParticipant | string;
|
|
62
|
+
email?: string | null;
|
|
62
63
|
isOwner: boolean;
|
|
63
64
|
accentClass: string;
|
|
64
65
|
/** The capture could not tell speakers apart, so this transcript names
|
|
@@ -291,6 +292,7 @@ export function resolveSpeaker(
|
|
|
291
292
|
key,
|
|
292
293
|
label,
|
|
293
294
|
initialsSource: participant ?? label ?? (source === "mic" ? "Me" : "Them"),
|
|
295
|
+
email: participant?.email ?? (source === "mic" ? ownerEmail : null),
|
|
294
296
|
isOwner: source === "mic",
|
|
295
297
|
accentClass: accentForSpeaker(key, source === "mic"),
|
|
296
298
|
};
|
|
@@ -641,21 +643,26 @@ export function TranscriptBubbles({
|
|
|
641
643
|
>
|
|
642
644
|
{!group.speaker.unattributed && (
|
|
643
645
|
<div className="flex h-6 items-center gap-2">
|
|
644
|
-
<
|
|
646
|
+
<ClipsAvatar
|
|
647
|
+
email={group.speaker.email}
|
|
648
|
+
alt={
|
|
649
|
+
group.speaker.label ||
|
|
650
|
+
(group.speaker.isOwner
|
|
651
|
+
? t("transcriptBubbles.me")
|
|
652
|
+
: t("transcriptBubbles.them"))
|
|
653
|
+
}
|
|
654
|
+
fallback={attendeeInitials(
|
|
655
|
+
group.speaker.initialsSource,
|
|
656
|
+
)}
|
|
645
657
|
className={cn(
|
|
646
658
|
"size-6 shrink-0",
|
|
647
659
|
group.speaker.accentClass,
|
|
648
660
|
)}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
)}
|
|
655
|
-
>
|
|
656
|
-
{attendeeInitials(group.speaker.initialsSource)}
|
|
657
|
-
</AvatarFallback>
|
|
658
|
-
</Avatar>
|
|
661
|
+
fallbackClassName={cn(
|
|
662
|
+
"text-[9px] font-semibold",
|
|
663
|
+
group.speaker.accentClass,
|
|
664
|
+
)}
|
|
665
|
+
/>
|
|
659
666
|
<div className="flex min-h-6 items-center">
|
|
660
667
|
<span
|
|
661
668
|
className={cn(
|
|
@@ -5,6 +5,7 @@ import { useQueryClient } from "@tanstack/react-query";
|
|
|
5
5
|
import { useState } from "react";
|
|
6
6
|
import { toast } from "sonner";
|
|
7
7
|
|
|
8
|
+
import { ClipsAvatar } from "@/components/clips-avatar";
|
|
8
9
|
import {
|
|
9
10
|
AlertDialog,
|
|
10
11
|
AlertDialogAction,
|
|
@@ -15,7 +16,6 @@ import {
|
|
|
15
16
|
AlertDialogHeader,
|
|
16
17
|
AlertDialogTitle,
|
|
17
18
|
} from "@/components/ui/alert-dialog";
|
|
18
|
-
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
|
19
19
|
import { Badge } from "@/components/ui/badge";
|
|
20
20
|
import { Button } from "@/components/ui/button";
|
|
21
21
|
import {
|
|
@@ -152,11 +152,13 @@ export function MembersList({
|
|
|
152
152
|
<TableRow key={m.id}>
|
|
153
153
|
<TableCell>
|
|
154
154
|
<div className="flex items-center gap-2 min-w-0">
|
|
155
|
-
<
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
155
|
+
<ClipsAvatar
|
|
156
|
+
email={m.email}
|
|
157
|
+
alt={m.email}
|
|
158
|
+
fallback={initials(m.email)}
|
|
159
|
+
className="h-8 w-8 flex-shrink-0"
|
|
160
|
+
fallbackClassName="text-xs bg-primary text-primary-foreground"
|
|
161
|
+
/>
|
|
160
162
|
<div className="min-w-0">
|
|
161
163
|
<div className="truncate font-medium flex items-center gap-1.5">
|
|
162
164
|
{m.email}
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from "@tabler/icons-react";
|
|
10
10
|
import { Link } from "react-router";
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import { ClipsAvatar } from "@/components/clips-avatar";
|
|
13
13
|
|
|
14
14
|
export type NotificationKind = "comment" | "reaction" | "mention" | "share";
|
|
15
15
|
|
|
@@ -62,11 +62,13 @@ export function NotificationsList({ items, onReply }: NotificationsListProps) {
|
|
|
62
62
|
<ul className="divide-y">
|
|
63
63
|
{items.map((item) => (
|
|
64
64
|
<li key={item.id} className="py-3 flex items-start gap-3">
|
|
65
|
-
<
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
65
|
+
<ClipsAvatar
|
|
66
|
+
email={item.authorEmail}
|
|
67
|
+
alt={item.authorEmail ?? t("clipsFinalRaw.someone")}
|
|
68
|
+
fallback={initials(item.authorEmail)}
|
|
69
|
+
className="h-9 w-9 flex-shrink-0"
|
|
70
|
+
fallbackClassName="text-xs bg-primary text-primary-foreground"
|
|
71
|
+
/>
|
|
70
72
|
<div className="min-w-0 flex-1">
|
|
71
73
|
<div className="flex items-center gap-2 text-sm">
|
|
72
74
|
<KindIcon kind={item.kind} />
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useT } from "@agent-native/core/client/i18n";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { ClipsAvatar } from "@/components/clips-avatar";
|
|
4
4
|
import {
|
|
5
5
|
Table,
|
|
6
6
|
TableBody,
|
|
@@ -58,11 +58,13 @@ export function TopCreatorsTable({ rows }: TopCreatorsTableProps) {
|
|
|
58
58
|
<TableRow key={row.email}>
|
|
59
59
|
<TableCell>
|
|
60
60
|
<div className="flex items-center gap-2 min-w-0">
|
|
61
|
-
<
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
61
|
+
<ClipsAvatar
|
|
62
|
+
email={row.email}
|
|
63
|
+
alt={row.email}
|
|
64
|
+
fallback={initials(row.email)}
|
|
65
|
+
className="h-7 w-7 flex-shrink-0"
|
|
66
|
+
fallbackClassName="text-xs bg-primary text-primary-foreground"
|
|
67
|
+
/>
|
|
66
68
|
<span className="truncate">{row.email}</span>
|
|
67
69
|
</div>
|
|
68
70
|
</TableCell>
|
|
@@ -28,6 +28,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
|
|
|
28
28
|
import { NavLink, useNavigate, useParams } from "react-router";
|
|
29
29
|
import { toast } from "sonner";
|
|
30
30
|
|
|
31
|
+
import { ClipsAvatar } from "@/components/clips-avatar";
|
|
31
32
|
import { PageHeader } from "@/components/library/page-header";
|
|
32
33
|
import {
|
|
33
34
|
AttendeeStack,
|
|
@@ -52,7 +53,6 @@ import {
|
|
|
52
53
|
AlertDialogHeader,
|
|
53
54
|
AlertDialogTitle,
|
|
54
55
|
} from "@/components/ui/alert-dialog";
|
|
55
|
-
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
|
56
56
|
import { Badge } from "@/components/ui/badge";
|
|
57
57
|
import { Button } from "@/components/ui/button";
|
|
58
58
|
import {
|
|
@@ -258,12 +258,13 @@ function ActionItemsByPerson({
|
|
|
258
258
|
{grouped.map(([who, list]) => (
|
|
259
259
|
<div key={who} className="space-y-1.5">
|
|
260
260
|
<div className="flex items-center gap-2">
|
|
261
|
-
<
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
261
|
+
<ClipsAvatar
|
|
262
|
+
email={who || null}
|
|
263
|
+
alt={who || t("meetingDetail.unassigned")}
|
|
264
|
+
fallback={attendeeInitials(who || t("meetingDetail.unassigned"))}
|
|
265
|
+
className="h-5 w-5"
|
|
266
|
+
fallbackClassName="text-[9px]"
|
|
267
|
+
/>
|
|
267
268
|
<span className="text-xs font-medium">
|
|
268
269
|
{who || t("meetingDetail.unassigned")}
|
|
269
270
|
</span>
|
|
@@ -237,11 +237,13 @@ export function MeetingNotification() {
|
|
|
237
237
|
}).catch(() => {});
|
|
238
238
|
};
|
|
239
239
|
|
|
240
|
-
|
|
241
|
-
|
|
240
|
+
const showListener = listen<NotificationData>(
|
|
241
|
+
"meetings:show-notification",
|
|
242
|
+
(ev) => {
|
|
242
243
|
showNotification(ev.payload);
|
|
243
|
-
}
|
|
244
|
+
},
|
|
244
245
|
);
|
|
246
|
+
trackListen(showListener);
|
|
245
247
|
|
|
246
248
|
trackListen(
|
|
247
249
|
listen<{ hovered: boolean }>("meetings:notification-hover", (ev) => {
|
|
@@ -249,33 +251,48 @@ export function MeetingNotification() {
|
|
|
249
251
|
}),
|
|
250
252
|
);
|
|
251
253
|
|
|
254
|
+
const hideListener = listen<TranscriptionStatusPayload>(
|
|
255
|
+
"meetings:hide-notification",
|
|
256
|
+
(ev) => {
|
|
257
|
+
if (ev.payload.meetingId !== dataRef.current?.meetingId) return;
|
|
258
|
+
hideNotification();
|
|
259
|
+
},
|
|
260
|
+
);
|
|
261
|
+
trackListen(hideListener);
|
|
262
|
+
const errorListener = listen<TranscriptionStatusPayload>(
|
|
263
|
+
"meetings:transcription-error",
|
|
264
|
+
(ev) => {
|
|
265
|
+
if (ev.payload.meetingId !== dataRef.current?.meetingId) return;
|
|
266
|
+
setPending(false);
|
|
267
|
+
setError(ev.payload.error || "Could not start notes.");
|
|
268
|
+
scheduleAutoHide(15_000);
|
|
269
|
+
},
|
|
270
|
+
);
|
|
271
|
+
trackListen(errorListener);
|
|
272
|
+
|
|
252
273
|
// Cold overlay boot: hydrate any payload stored before this webview
|
|
253
274
|
// mounted (calendar or adhoc).
|
|
254
|
-
|
|
275
|
+
//
|
|
276
|
+
// Every listener has to be live first, because `take` is destructive and
|
|
277
|
+
// each event lost in the gap is lost permanently. A hide landing before the
|
|
278
|
+
// hide listener registers leaves a card nothing can take back; a show
|
|
279
|
+
// landing before the show listener registers is dropped while the payload it
|
|
280
|
+
// duplicated has already been consumed, so no card appears at all.
|
|
281
|
+
Promise.all([showListener, hideListener, errorListener])
|
|
282
|
+
.then(() =>
|
|
283
|
+
invoke<NotificationData | null>("take_pending_meeting_notification"),
|
|
284
|
+
)
|
|
255
285
|
.then((pending) => {
|
|
256
286
|
if (stopped || !pending) return;
|
|
287
|
+
// The take is asynchronous, so a live `meetings:show-notification` may
|
|
288
|
+
// have arrived while it was in flight. That payload is newer than this
|
|
289
|
+
// one by definition, and hydration is only a cold-boot fallback, so it
|
|
290
|
+
// must not replace what the live path already put on screen.
|
|
291
|
+
if (dataRef.current) return;
|
|
257
292
|
showNotification(pending, { hydrated: true });
|
|
258
293
|
})
|
|
259
294
|
.catch(() => {});
|
|
260
295
|
|
|
261
|
-
trackListen(
|
|
262
|
-
listen<TranscriptionStatusPayload>("meetings:hide-notification", (ev) => {
|
|
263
|
-
if (ev.payload.meetingId !== dataRef.current?.meetingId) return;
|
|
264
|
-
hideNotification();
|
|
265
|
-
}),
|
|
266
|
-
);
|
|
267
|
-
trackListen(
|
|
268
|
-
listen<TranscriptionStatusPayload>(
|
|
269
|
-
"meetings:transcription-error",
|
|
270
|
-
(ev) => {
|
|
271
|
-
if (ev.payload.meetingId !== dataRef.current?.meetingId) return;
|
|
272
|
-
setPending(false);
|
|
273
|
-
setError(ev.payload.error || "Could not start notes.");
|
|
274
|
-
scheduleAutoHide(15_000);
|
|
275
|
-
},
|
|
276
|
-
),
|
|
277
|
-
);
|
|
278
|
-
|
|
279
296
|
return () => {
|
|
280
297
|
stopped = true;
|
|
281
298
|
clearAutoHide();
|