@agent-native/core 0.157.5 → 0.157.7
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/analytics/agent-native.config.ts +5 -0
- package/corpus/templates/analytics/netlify.toml +1 -0
- package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +23 -5
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +4 -0
- package/corpus/templates/assets/netlify.toml +3 -0
- package/corpus/templates/brain/netlify.toml +3 -0
- package/corpus/templates/calendar/agent-native.config.ts +5 -0
- package/corpus/templates/calendar/app/components/layout/AppLayout.tsx +4 -1
- package/corpus/templates/calendar/netlify.toml +3 -0
- package/corpus/templates/chat/agent-native.config.ts +5 -0
- package/corpus/templates/chat/netlify.toml +3 -0
- package/corpus/templates/clips/actions/create-meeting.ts +19 -1
- package/corpus/templates/clips/actions/lib/meeting-content.ts +28 -0
- package/corpus/templates/clips/actions/list-meetings.ts +33 -11
- package/corpus/templates/clips/app/components/meetings/bullet-link.tsx +48 -25
- package/corpus/templates/clips/app/components/meetings/canvas-editor.tsx +7 -6
- package/corpus/templates/clips/app/components/meetings/transcript-bubbles.tsx +388 -178
- package/corpus/templates/clips/app/components/player/transcript-panel.tsx +7 -13
- package/corpus/templates/clips/app/components/transcript/transcript-segment-row.tsx +98 -0
- package/corpus/templates/clips/app/i18n/en-US.ts +5 -0
- package/corpus/templates/clips/app/lib/meeting-history-query.ts +11 -0
- package/corpus/templates/clips/app/lib/public-meeting.ts +4 -0
- package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +49 -31
- package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +47 -21
- package/corpus/templates/clips/app/routes/share.meeting.$meetingId.tsx +15 -1
- package/corpus/templates/clips/netlify.toml +8 -4
- package/corpus/templates/clips/server/routes/api/public-meeting.get.ts +10 -0
- package/corpus/templates/clips/shared/transcript-segments.ts +20 -2
- package/corpus/templates/content/netlify.toml +3 -0
- package/corpus/templates/design/app/i18n-data.ts +2 -0
- package/corpus/templates/design/netlify.toml +3 -0
- package/corpus/templates/dispatch/netlify.toml +3 -0
- package/corpus/templates/factory/netlify.toml +3 -0
- package/corpus/templates/forms/netlify.toml +3 -0
- package/corpus/templates/macros/netlify.toml +3 -0
- package/corpus/templates/mail/agent-native.config.ts +5 -0
- package/corpus/templates/mail/netlify.toml +3 -0
- package/corpus/templates/plan/netlify.toml +3 -0
- package/corpus/templates/slides/netlify.toml +3 -0
- package/dist/agent/harness/acp-adapter.js +17 -2
- package/dist/agent/harness/ai-sdk-adapter.js +14 -3
- package/dist/agent/harness/background.js +10 -5
- package/dist/agent/harness/hosted.d.ts +20 -0
- package/dist/agent/harness/hosted.js +100 -0
- package/dist/agent/harness/runner.js +100 -77
- package/dist/agent/observational-memory/observer.js +12 -0
- package/dist/agent/production-agent.d.ts +3 -0
- package/dist/agent/production-agent.js +74 -4
- package/dist/agent/thread-data-builder.d.ts +12 -0
- package/dist/agent/thread-data-builder.js +23 -0
- package/dist/agent/types.d.ts +6 -0
- package/dist/client/AgentPanel.d.ts +5 -1
- package/dist/client/AgentPanel.js +113 -17
- package/dist/client/AgentTaskCard.d.ts +2 -2
- package/dist/client/AgentTaskCard.js +8 -22
- package/dist/client/AssistantChat.d.ts +8 -0
- package/dist/client/AssistantChat.js +106 -59
- package/dist/client/MultiTabAssistantChat.js +50 -6
- package/dist/client/NewWorkspaceAppFlow.js +7 -1
- package/dist/client/agent-chat-adapter.d.ts +6 -0
- package/dist/client/agent-chat-adapter.js +36 -8
- package/dist/client/app-chat-sidebar.d.ts +5 -0
- package/dist/client/app-chat-sidebar.js +53 -9
- package/dist/client/chat/composer-draft.d.ts +4 -0
- package/dist/client/chat/composer-draft.js +63 -0
- package/dist/client/chat-first/apps-rail.js +10 -1
- package/dist/client/chat-first/primary-nav.js +10 -5
- package/dist/client/frame-protocol.d.ts +1 -0
- package/dist/client/hooks/index.d.ts +1 -1
- package/dist/client/hooks/index.js +1 -1
- package/dist/client/i18n.js +5 -2
- package/dist/client/progress/RunsTray.js +129 -16
- package/dist/client/settings/SettingsRow.js +1 -1
- package/dist/client/use-chat-threads.js +11 -6
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/config.d.ts +11 -0
- package/dist/config.js +52 -0
- package/dist/deploy/build.js +28 -11
- package/dist/extensions/actions.js +22 -1
- package/dist/hosted-harness/actions/get-hosted-harness-config.d.ts +2 -0
- package/dist/hosted-harness/actions/get-hosted-harness-config.js +13 -0
- package/dist/hosted-harness/actions/set-hosted-harness-enabled.d.ts +4 -0
- package/dist/hosted-harness/actions/set-hosted-harness-enabled.js +32 -0
- package/dist/localization/default-messages.d.ts +4 -1
- package/dist/localization/default-messages.js +5 -2
- package/dist/observability/routes.d.ts +5 -5
- package/dist/provider-api/actions/custom-provider-registration.d.ts +6 -6
- package/dist/provider-api/actions/provider-api.d.ts +4 -4
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/action-discovery.js +12 -0
- package/dist/server/agent-chat-plugin.js +24 -4
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-teams.d.ts +20 -12
- package/dist/server/agent-teams.js +69 -35
- package/dist/server/hosted-harness-policy.d.ts +17 -0
- package/dist/server/hosted-harness-policy.js +38 -0
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/request-context.d.ts +4 -0
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/styles/agent-native.css +39 -0
- package/dist/templates/chat/agent-native.config.ts +5 -0
- package/dist/templates/chat/netlify.toml +3 -0
- package/dist/vite/action-types-plugin.js +8 -0
- package/package.json +4 -2
- package/src/templates/chat/agent-native.config.ts +5 -0
- package/src/templates/chat/netlify.toml +3 -0
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
} from "@/components/ui/tooltip";
|
|
27
27
|
import { cn } from "@/lib/utils";
|
|
28
28
|
|
|
29
|
-
import {
|
|
29
|
+
import { TranscriptSegmentRow } from "../transcript/transcript-segment-row";
|
|
30
30
|
|
|
31
31
|
export interface TranscriptSegment {
|
|
32
32
|
startMs: number;
|
|
@@ -299,9 +299,10 @@ export function TranscriptPanel(props: TranscriptPanelProps) {
|
|
|
299
299
|
const isActive = displaySegments[activeIndex] === seg;
|
|
300
300
|
return (
|
|
301
301
|
<li key={seg.startMs}>
|
|
302
|
-
<
|
|
303
|
-
|
|
304
|
-
|
|
302
|
+
<TranscriptSegmentRow
|
|
303
|
+
startMs={seg.startMs}
|
|
304
|
+
active={isActive}
|
|
305
|
+
gutter="panel"
|
|
305
306
|
onClick={(event) => {
|
|
306
307
|
if (hasSelectionWithin(event.currentTarget)) return;
|
|
307
308
|
onSeek(seg.startMs);
|
|
@@ -311,24 +312,17 @@ export function TranscriptPanel(props: TranscriptPanelProps) {
|
|
|
311
312
|
event.preventDefault();
|
|
312
313
|
onSeek(seg.startMs);
|
|
313
314
|
}}
|
|
314
|
-
className={cn(
|
|
315
|
-
"flex w-full cursor-pointer items-start gap-3 px-3 py-2 text-left hover:bg-accent/50",
|
|
316
|
-
isActive && "bg-accent",
|
|
317
|
-
)}
|
|
318
315
|
>
|
|
319
|
-
<span className="shrink-0 pt-0.5 font-mono text-[11px] tabular-nums text-muted-foreground">
|
|
320
|
-
{msToClock(seg.startMs)}
|
|
321
|
-
</span>
|
|
322
316
|
<span
|
|
323
317
|
className={cn(
|
|
324
|
-
"text-sm leading-
|
|
318
|
+
"text-sm leading-normal",
|
|
325
319
|
isActive ? "text-foreground" : "text-foreground/80",
|
|
326
320
|
)}
|
|
327
321
|
dangerouslySetInnerHTML={{
|
|
328
322
|
__html: highlight(seg.text, query),
|
|
329
323
|
}}
|
|
330
324
|
/>
|
|
331
|
-
</
|
|
325
|
+
</TranscriptSegmentRow>
|
|
332
326
|
</li>
|
|
333
327
|
);
|
|
334
328
|
})}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { KeyboardEvent, MouseEvent, ReactNode, Ref } from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
export interface TranscriptSegmentRowProps {
|
|
6
|
+
startMs: number;
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
active?: boolean;
|
|
9
|
+
/** Momentary jump-to-segment flash, rendered as its own overlay layer so
|
|
10
|
+
* its fade-out is never racing the row's own hover/active transition. */
|
|
11
|
+
highlighted?: boolean;
|
|
12
|
+
onClick?: (event: MouseEvent<HTMLDivElement>) => void;
|
|
13
|
+
onKeyDown?: (event: KeyboardEvent<HTMLDivElement>) => void;
|
|
14
|
+
segmentRef?: Ref<HTMLDivElement>;
|
|
15
|
+
tabIndex?: number;
|
|
16
|
+
gutter?: "content" | "panel";
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function formatTranscriptTimestamp(ms: number): string {
|
|
21
|
+
if (!Number.isFinite(ms) || ms < 0) return "0:00";
|
|
22
|
+
const total = Math.floor(ms / 1000);
|
|
23
|
+
const hours = Math.floor(total / 3600);
|
|
24
|
+
const minutes = Math.floor((total % 3600) / 60);
|
|
25
|
+
const seconds = total % 60;
|
|
26
|
+
if (hours > 0) {
|
|
27
|
+
return `${hours}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
|
|
28
|
+
}
|
|
29
|
+
return `${minutes}:${String(seconds).padStart(2, "0")}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function TranscriptSegmentRow({
|
|
33
|
+
startMs,
|
|
34
|
+
children,
|
|
35
|
+
active = false,
|
|
36
|
+
highlighted = false,
|
|
37
|
+
onClick,
|
|
38
|
+
onKeyDown,
|
|
39
|
+
segmentRef,
|
|
40
|
+
tabIndex,
|
|
41
|
+
gutter = "content",
|
|
42
|
+
className,
|
|
43
|
+
}: TranscriptSegmentRowProps) {
|
|
44
|
+
const interactive = Boolean(onClick);
|
|
45
|
+
const rowTabIndex = tabIndex ?? (interactive ? 0 : undefined);
|
|
46
|
+
const isFocusable = rowTabIndex !== undefined;
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div
|
|
50
|
+
ref={segmentRef}
|
|
51
|
+
role={interactive ? "button" : isFocusable ? "group" : undefined}
|
|
52
|
+
tabIndex={rowTabIndex}
|
|
53
|
+
onClick={onClick}
|
|
54
|
+
onKeyDown={onKeyDown}
|
|
55
|
+
className={cn(
|
|
56
|
+
"group/segment relative flex items-baseline gap-4 rounded-md py-1.5 text-left text-sm leading-normal transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50",
|
|
57
|
+
// The meeting notes view puts the AI summary front and center and the
|
|
58
|
+
// transcript is reference material you dip into as needed, so it
|
|
59
|
+
// carries less visual weight there. Faded foreground, not
|
|
60
|
+
// text-muted-foreground — that token sits at 45% lightness (same as
|
|
61
|
+
// disabled/placeholder text elsewhere), which reads as inactive
|
|
62
|
+
// rather than merely secondary. The player's transcript panel has no
|
|
63
|
+
// competing summary — it's the primary content, so it stays full
|
|
64
|
+
// strength.
|
|
65
|
+
gutter === "panel" ? "text-foreground" : "text-foreground/70",
|
|
66
|
+
gutter === "content" ? "-mx-2 px-2" : "w-full px-3",
|
|
67
|
+
interactive && "cursor-pointer",
|
|
68
|
+
active && "bg-accent",
|
|
69
|
+
interactive && !active && "hover:bg-accent/50",
|
|
70
|
+
className,
|
|
71
|
+
)}
|
|
72
|
+
>
|
|
73
|
+
{/* Overlay, not a shared background class — its own opacity fade can't
|
|
74
|
+
be raced by the row's hover/active transition (see highlighted prop). */}
|
|
75
|
+
<span
|
|
76
|
+
aria-hidden="true"
|
|
77
|
+
className={cn(
|
|
78
|
+
"pointer-events-none absolute inset-0 rounded-md bg-yellow-400/15 transition-opacity ease-out",
|
|
79
|
+
highlighted ? "opacity-100 duration-200" : "opacity-0 duration-700",
|
|
80
|
+
)}
|
|
81
|
+
/>
|
|
82
|
+
<span className="min-w-0 flex-1 whitespace-pre-wrap">{children}</span>
|
|
83
|
+
<span
|
|
84
|
+
className={cn(
|
|
85
|
+
"pointer-events-none w-12 shrink-0 text-right font-mono text-[10px] tabular-nums text-muted-foreground/80 transition-opacity",
|
|
86
|
+
// The player's transcript list is scanned by timestamp to find a
|
|
87
|
+
// moment — it's primary information there, not hover-reveal
|
|
88
|
+
// supplementary detail like it is in the summary bullets.
|
|
89
|
+
gutter === "panel"
|
|
90
|
+
? "opacity-100"
|
|
91
|
+
: "opacity-0 group-hover/segment:opacity-100 group-focus-visible/segment:opacity-100 [@media(hover:none)]:opacity-100",
|
|
92
|
+
)}
|
|
93
|
+
>
|
|
94
|
+
{formatTranscriptTimestamp(startMs)}
|
|
95
|
+
</span>
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -1122,6 +1122,10 @@ All notable user-facing changes to Clips are documented here. Open it any time f
|
|
|
1122
1122
|
searchNextMatch: "Next match",
|
|
1123
1123
|
searchClose: "Close search",
|
|
1124
1124
|
},
|
|
1125
|
+
bulletLink: {
|
|
1126
|
+
jumpToTranscript: "Jump to {{time}} in transcript",
|
|
1127
|
+
noMatchingMoment: "No matching moment found",
|
|
1128
|
+
},
|
|
1125
1129
|
editorLayout: {
|
|
1126
1130
|
trimmed: "Trimmed",
|
|
1127
1131
|
trimFailed: "Trim failed",
|
|
@@ -1571,6 +1575,7 @@ All notable user-facing changes to Clips are documented here. Open it any time f
|
|
|
1571
1575
|
transcriptPending: "Transcript pending",
|
|
1572
1576
|
notesPending: "Notes pending",
|
|
1573
1577
|
pastRecordings: "Past recordings",
|
|
1578
|
+
loadOlder: "Load older",
|
|
1574
1579
|
calendarNeedsReconnect:
|
|
1575
1580
|
"Google Calendar needs to be reconnected to keep showing your upcoming meetings.",
|
|
1576
1581
|
connectGoogleCalendar: "Connect Google Calendar",
|
|
@@ -36,6 +36,10 @@ export interface PublicMeeting {
|
|
|
36
36
|
bullets: PublicMeetingBullet[];
|
|
37
37
|
participants: PublicMeetingParticipant[];
|
|
38
38
|
actionItems: PublicMeetingActionItem[];
|
|
39
|
+
/** Recording owner's email — resolves "mic" segments to the actual
|
|
40
|
+
* recorder for calendar meetings where they aren't the event organizer.
|
|
41
|
+
* Already no more exposed than the per-participant emails on this page. */
|
|
42
|
+
ownerEmail: string | null;
|
|
39
43
|
actualStart: string | null;
|
|
40
44
|
actualEnd: string | null;
|
|
41
45
|
transcriptStatus: string | null;
|
|
@@ -92,6 +92,7 @@ interface Bullet {
|
|
|
92
92
|
interface Meeting {
|
|
93
93
|
id: string;
|
|
94
94
|
title: string;
|
|
95
|
+
ownerEmail?: string | null;
|
|
95
96
|
scheduledStart: string;
|
|
96
97
|
scheduledEnd?: string | null;
|
|
97
98
|
actualStart?: string | null;
|
|
@@ -265,7 +266,7 @@ function ActionItemsByPerson({
|
|
|
265
266
|
key={
|
|
266
267
|
it.id ?? `${it.assigneeEmail ?? "?"}:${it.text}:${index}`
|
|
267
268
|
}
|
|
268
|
-
className="flex items-start gap-2 text-
|
|
269
|
+
className="flex items-start gap-2 text-sm leading-relaxed"
|
|
269
270
|
>
|
|
270
271
|
<button
|
|
271
272
|
type="button"
|
|
@@ -730,12 +731,6 @@ export default function MeetingDetailRoute() {
|
|
|
730
731
|
</Button>
|
|
731
732
|
</DropdownMenuTrigger>
|
|
732
733
|
<DropdownMenuContent align="end" className="w-44">
|
|
733
|
-
{hasSummary && !finalize.isPending && (
|
|
734
|
-
<DropdownMenuItem onSelect={handleFinalize}>
|
|
735
|
-
<IconRefresh className="mr-2 h-4 w-4" />
|
|
736
|
-
{t("meetingDetail.regenerateNotes")}
|
|
737
|
-
</DropdownMenuItem>
|
|
738
|
-
)}
|
|
739
734
|
{isLive && (
|
|
740
735
|
<DropdownMenuItem
|
|
741
736
|
onSelect={(event) => {
|
|
@@ -880,16 +875,37 @@ export default function MeetingDetailRoute() {
|
|
|
880
875
|
notesJustArrived && "animate-in fade-in duration-500",
|
|
881
876
|
)}
|
|
882
877
|
>
|
|
883
|
-
<div className="flex shrink-0 items-center justify-between gap-2 border-b border-border px-4
|
|
878
|
+
<div className="flex h-11 shrink-0 items-center justify-between gap-2 border-b border-border px-4">
|
|
884
879
|
<div className="text-xs font-medium">
|
|
885
880
|
{t("meetingDetail.summary")}
|
|
886
881
|
</div>
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
<
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
882
|
+
<div className="flex items-center gap-2">
|
|
883
|
+
{finalize.isPending && (
|
|
884
|
+
<span className="flex items-center gap-1 text-[10px] text-muted-foreground">
|
|
885
|
+
<IconLoader2 className="h-3 w-3 animate-spin" />
|
|
886
|
+
{t("meetingDetail.working")}
|
|
887
|
+
</span>
|
|
888
|
+
)}
|
|
889
|
+
{canEdit && hasSummary && (
|
|
890
|
+
<Tooltip>
|
|
891
|
+
<TooltipTrigger asChild>
|
|
892
|
+
<Button
|
|
893
|
+
size="icon"
|
|
894
|
+
variant="ghost"
|
|
895
|
+
className="h-7 w-7 cursor-pointer"
|
|
896
|
+
aria-label={t("meetingDetail.regenerateNotes")}
|
|
897
|
+
disabled={finalize.isPending}
|
|
898
|
+
onClick={handleFinalize}
|
|
899
|
+
>
|
|
900
|
+
<IconRefresh className="h-3.5 w-3.5" />
|
|
901
|
+
</Button>
|
|
902
|
+
</TooltipTrigger>
|
|
903
|
+
<TooltipContent>
|
|
904
|
+
{t("meetingDetail.regenerateNotes")}
|
|
905
|
+
</TooltipContent>
|
|
906
|
+
</Tooltip>
|
|
907
|
+
)}
|
|
908
|
+
</div>
|
|
893
909
|
</div>
|
|
894
910
|
|
|
895
911
|
<div className="min-h-0 flex-1 overflow-y-auto">
|
|
@@ -905,7 +921,7 @@ export default function MeetingDetailRoute() {
|
|
|
905
921
|
segments={segments}
|
|
906
922
|
onJumpTo={handleJumpToSegment}
|
|
907
923
|
>
|
|
908
|
-
<div className="flex gap-2 text-sm leading-relaxed text-
|
|
924
|
+
<div className="flex gap-2 text-sm leading-relaxed text-foreground">
|
|
909
925
|
<span>•</span>
|
|
910
926
|
<span className="flex-1">{b}</span>
|
|
911
927
|
</div>
|
|
@@ -934,13 +950,22 @@ export default function MeetingDetailRoute() {
|
|
|
934
950
|
|
|
935
951
|
{/* Transcript pane — plain agent-chat-style text layout */}
|
|
936
952
|
<div className="rounded-lg border border-border bg-background min-h-[480px] lg:min-h-0 overflow-hidden flex flex-col">
|
|
937
|
-
<
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
953
|
+
<TranscriptBubbles
|
|
954
|
+
segments={segments}
|
|
955
|
+
isLive={isLive}
|
|
956
|
+
participants={meeting.participants ?? []}
|
|
957
|
+
ownerEmail={meeting.ownerEmail}
|
|
958
|
+
registerScrollTo={(fn) => {
|
|
959
|
+
transcriptScrollToRef.current = fn;
|
|
960
|
+
}}
|
|
961
|
+
title={
|
|
962
|
+
<>
|
|
963
|
+
<IconNotes className="h-3.5 w-3.5" />
|
|
964
|
+
{t("meetingDetail.transcript")}
|
|
965
|
+
</>
|
|
966
|
+
}
|
|
967
|
+
headerActions={
|
|
968
|
+
segments.length > 0 && (
|
|
944
969
|
<Tooltip>
|
|
945
970
|
<TooltipTrigger asChild>
|
|
946
971
|
<Button
|
|
@@ -961,15 +986,8 @@ export default function MeetingDetailRoute() {
|
|
|
961
986
|
{t("meetingDetail.copyFullTranscript")}
|
|
962
987
|
</TooltipContent>
|
|
963
988
|
</Tooltip>
|
|
964
|
-
)
|
|
965
|
-
|
|
966
|
-
</div>
|
|
967
|
-
<TranscriptBubbles
|
|
968
|
-
segments={segments}
|
|
969
|
-
isLive={isLive}
|
|
970
|
-
registerScrollTo={(fn) => {
|
|
971
|
-
transcriptScrollToRef.current = fn;
|
|
972
|
-
}}
|
|
989
|
+
)
|
|
990
|
+
}
|
|
973
991
|
/>
|
|
974
992
|
</div>
|
|
975
993
|
</div>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { agentNativePath } from "@agent-native/core/client/api-path";
|
|
2
|
-
import { useActionQuery } from "@agent-native/core/client/hooks";
|
|
2
|
+
import { callAction, useActionQuery } from "@agent-native/core/client/hooks";
|
|
3
3
|
import { useT } from "@agent-native/core/client/i18n";
|
|
4
4
|
import {
|
|
5
5
|
IconAlertTriangle,
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
IconSettings,
|
|
18
18
|
IconX,
|
|
19
19
|
} from "@tabler/icons-react";
|
|
20
|
-
import { useQueryClient } from "@tanstack/react-query";
|
|
20
|
+
import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
|
|
21
21
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
22
22
|
import { NavLink, useSearchParams } from "react-router";
|
|
23
23
|
import { toast } from "sonner";
|
|
@@ -50,6 +50,10 @@ import {
|
|
|
50
50
|
} from "@/components/ui/dropdown-menu";
|
|
51
51
|
import { Input } from "@/components/ui/input";
|
|
52
52
|
import enMessages from "@/i18n/en-US";
|
|
53
|
+
import {
|
|
54
|
+
buildMeetingHistoryQuery,
|
|
55
|
+
MEETING_HISTORY_PAGE_SIZE,
|
|
56
|
+
} from "@/lib/meeting-history-query";
|
|
53
57
|
|
|
54
58
|
export function meta() {
|
|
55
59
|
return [{ title: enMessages.meetingsRoute.pageTitle }];
|
|
@@ -88,6 +92,7 @@ interface CalendarFetchError {
|
|
|
88
92
|
interface ListMeetingsResponse {
|
|
89
93
|
meetings?: Meeting[];
|
|
90
94
|
calendarErrors?: CalendarFetchError[];
|
|
95
|
+
hasMore?: boolean;
|
|
91
96
|
}
|
|
92
97
|
|
|
93
98
|
interface CalendarAccount {
|
|
@@ -849,13 +854,20 @@ export default function MeetingsIndexRoute() {
|
|
|
849
854
|
{},
|
|
850
855
|
{ retry: false },
|
|
851
856
|
);
|
|
852
|
-
const
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
857
|
+
const history = useInfiniteQuery({
|
|
858
|
+
queryKey: ["action", "list-meetings", "history"],
|
|
859
|
+
initialPageParam: 0,
|
|
860
|
+
queryFn: ({ pageParam, signal }) =>
|
|
861
|
+
callAction("list-meetings", buildMeetingHistoryQuery(pageParam), {
|
|
862
|
+
method: "GET",
|
|
863
|
+
signal,
|
|
864
|
+
}) as Promise<ListMeetingsResponse>,
|
|
865
|
+
getNextPageParam: (lastPage, allPages) =>
|
|
866
|
+
lastPage?.hasMore
|
|
867
|
+
? allPages.length * MEETING_HISTORY_PAGE_SIZE
|
|
868
|
+
: undefined,
|
|
869
|
+
retry: false,
|
|
870
|
+
});
|
|
859
871
|
// Upcoming calendar events, read live from connected calendars. This is the
|
|
860
872
|
// Granola-style surface that lets the user record/join a meeting that's about
|
|
861
873
|
// to start. Poll every 30s so a freshly-added calendar event (or one moving
|
|
@@ -886,7 +898,7 @@ export default function MeetingsIndexRoute() {
|
|
|
886
898
|
queryClient.setQueriesData<any>(
|
|
887
899
|
{ queryKey: ["action", "list-meetings"] },
|
|
888
900
|
(prev: any) => {
|
|
889
|
-
if (!prev || Array.isArray(prev)) return prev;
|
|
901
|
+
if (!prev || !Array.isArray(prev.meetings)) return prev;
|
|
890
902
|
return { ...prev, calendarErrors: [] };
|
|
891
903
|
},
|
|
892
904
|
);
|
|
@@ -905,7 +917,7 @@ export default function MeetingsIndexRoute() {
|
|
|
905
917
|
if (connected) break;
|
|
906
918
|
await new Promise((resolve) => window.setTimeout(resolve, 500));
|
|
907
919
|
}
|
|
908
|
-
await
|
|
920
|
+
await history.refetch();
|
|
909
921
|
if (connected) toast.success(t("meetingsRoute.calendarConnected"));
|
|
910
922
|
} catch (err) {
|
|
911
923
|
toast.error(
|
|
@@ -919,14 +931,12 @@ export default function MeetingsIndexRoute() {
|
|
|
919
931
|
queryKey: ["action", "list-meetings"],
|
|
920
932
|
});
|
|
921
933
|
}
|
|
922
|
-
}, [accounts, clearCalendarConnectionWarnings,
|
|
934
|
+
}, [accounts, clearCalendarConnectionWarnings, history, queryClient]);
|
|
923
935
|
|
|
924
|
-
const meetings: Meeting[] = useMemo(
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
return data.meetings ?? [];
|
|
929
|
-
}, [meetingsQuery.data]);
|
|
936
|
+
const meetings: Meeting[] = useMemo(
|
|
937
|
+
() => (history.data?.pages ?? []).flatMap((page) => page?.meetings ?? []),
|
|
938
|
+
[history.data],
|
|
939
|
+
);
|
|
930
940
|
|
|
931
941
|
const upcomingMeetings: Meeting[] = useMemo(() => {
|
|
932
942
|
const data = upcomingQuery.data;
|
|
@@ -961,11 +971,11 @@ export default function MeetingsIndexRoute() {
|
|
|
961
971
|
);
|
|
962
972
|
}, [handleCalendarConnected]);
|
|
963
973
|
|
|
964
|
-
const isLoading = accounts.isLoading ||
|
|
974
|
+
const isLoading = accounts.isLoading || history.isLoading;
|
|
965
975
|
|
|
966
976
|
const calendarLoadError = accounts.isError
|
|
967
977
|
? "Couldn't check your calendar connection. Try again in a moment."
|
|
968
|
-
:
|
|
978
|
+
: history.isError
|
|
969
979
|
? "Couldn't load meetings. Try again in a moment."
|
|
970
980
|
: null;
|
|
971
981
|
|
|
@@ -1111,10 +1121,26 @@ export default function MeetingsIndexRoute() {
|
|
|
1111
1121
|
<UpcomingMeetingsList meetings={filteredUpcoming} />
|
|
1112
1122
|
)}
|
|
1113
1123
|
<RecordedMeetingsList meetings={recordedMeetings} />
|
|
1124
|
+
{history.hasNextPage && (
|
|
1125
|
+
<div className="flex justify-center">
|
|
1126
|
+
<Button
|
|
1127
|
+
variant="outline"
|
|
1128
|
+
size="sm"
|
|
1129
|
+
onClick={() => history.fetchNextPage()}
|
|
1130
|
+
disabled={history.isFetchingNextPage}
|
|
1131
|
+
className="gap-1.5 cursor-pointer"
|
|
1132
|
+
>
|
|
1133
|
+
{history.isFetchingNextPage ? (
|
|
1134
|
+
<IconLoader2 className="h-3.5 w-3.5 animate-spin" />
|
|
1135
|
+
) : null}
|
|
1136
|
+
{t("meetingsRoute.loadOlder", { defaultValue: "Load older" })}
|
|
1137
|
+
</Button>
|
|
1138
|
+
</div>
|
|
1139
|
+
)}
|
|
1114
1140
|
</div>
|
|
1115
1141
|
)}
|
|
1116
1142
|
|
|
1117
|
-
{(
|
|
1143
|
+
{(history.isFetching || upcomingQuery.isFetching) && !isLoading && (
|
|
1118
1144
|
<div className="flex items-center justify-center mt-6 text-xs text-muted-foreground gap-1.5">
|
|
1119
1145
|
<IconLoader2 className="h-3.5 w-3.5 animate-spin" />
|
|
1120
1146
|
{t("meetingsRoute.refreshing")}
|
|
@@ -86,6 +86,7 @@ export async function loader({ params, url }: LoaderFunctionArgs) {
|
|
|
86
86
|
scheduledStart: schema.meetings.scheduledStart,
|
|
87
87
|
summaryMd: schema.meetings.summaryMd,
|
|
88
88
|
bulletsJson: schema.meetings.bulletsJson,
|
|
89
|
+
ownerEmail: schema.meetings.ownerEmail,
|
|
89
90
|
actualStart: schema.meetings.actualStart,
|
|
90
91
|
actualEnd: schema.meetings.actualEnd,
|
|
91
92
|
transcriptStatus: schema.meetings.transcriptStatus,
|
|
@@ -169,6 +170,15 @@ export async function loader({ params, url }: LoaderFunctionArgs) {
|
|
|
169
170
|
}
|
|
170
171
|
} catch {}
|
|
171
172
|
|
|
173
|
+
// The owner's email is only safe to disclose here when it's already public
|
|
174
|
+
// via the attendee list — an unauthenticated viewer must never learn an
|
|
175
|
+
// account email that isn't otherwise visible on this page.
|
|
176
|
+
const ownerEmailIsPublic = participants.some(
|
|
177
|
+
(participant) =>
|
|
178
|
+
participant.email.trim().toLowerCase() ===
|
|
179
|
+
meeting.ownerEmail?.trim().toLowerCase(),
|
|
180
|
+
);
|
|
181
|
+
|
|
172
182
|
return shareMeetingLoaderData(
|
|
173
183
|
{
|
|
174
184
|
meeting: {
|
|
@@ -179,6 +189,7 @@ export async function loader({ params, url }: LoaderFunctionArgs) {
|
|
|
179
189
|
bullets,
|
|
180
190
|
participants,
|
|
181
191
|
actionItems,
|
|
192
|
+
ownerEmail: ownerEmailIsPublic ? meeting.ownerEmail : null,
|
|
182
193
|
actualStart: meeting.actualStart,
|
|
183
194
|
actualEnd: meeting.actualEnd,
|
|
184
195
|
transcriptStatus: meeting.transcriptStatus,
|
|
@@ -363,6 +374,7 @@ export default function ShareMeetingRoute() {
|
|
|
363
374
|
(participant) => ({
|
|
364
375
|
email: participant.email,
|
|
365
376
|
name: participant.name ?? undefined,
|
|
377
|
+
isOrganizer: participant.isOrganizer,
|
|
366
378
|
}),
|
|
367
379
|
);
|
|
368
380
|
const transcript = meeting.transcript;
|
|
@@ -449,7 +461,7 @@ export default function ShareMeetingRoute() {
|
|
|
449
461
|
{meeting.bullets.map((bullet, index) => (
|
|
450
462
|
<li
|
|
451
463
|
key={index}
|
|
452
|
-
className="flex gap-2 text-sm leading-relaxed text-
|
|
464
|
+
className="flex gap-2 text-sm leading-relaxed text-foreground"
|
|
453
465
|
>
|
|
454
466
|
<span>•</span>
|
|
455
467
|
<span className="flex-1">{bullet.text}</span>
|
|
@@ -522,6 +534,8 @@ export default function ShareMeetingRoute() {
|
|
|
522
534
|
<TranscriptBubbles
|
|
523
535
|
segments={transcript.segments}
|
|
524
536
|
isLive={false}
|
|
537
|
+
participants={attendees}
|
|
538
|
+
ownerEmail={meeting.ownerEmail}
|
|
525
539
|
/>
|
|
526
540
|
</div>
|
|
527
541
|
) : transcript.fullText ? (
|
|
@@ -13,11 +13,15 @@ AUTO_CREATE_DEFAULT_ORG = "1"
|
|
|
13
13
|
# Emit the shared 15-minute Netlify background function used by agent chat and
|
|
14
14
|
# Clips post-recording transcription/seekability workers.
|
|
15
15
|
AGENT_CHAT_DURABLE_BACKGROUND = "true"
|
|
16
|
+
|
|
17
|
+
[context.production.environment]
|
|
18
|
+
AGENT_NATIVE_HOSTED_HARNESS = "true"
|
|
19
|
+
|
|
16
20
|
# Netlify Functions run on linux/x64 (AWS Lambda). This tells the deploy build
|
|
17
|
-
# to bundle the matching ffmpeg-static binary into the server bundle so
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
+
# to bundle the matching ffmpeg-static binary into the server bundle so frame
|
|
22
|
+
# extraction, WebM seekability, filmstrips, and audio-only transcription work
|
|
23
|
+
# in production. The deploy guard grants this known media-runtime payload its
|
|
24
|
+
# explicit allowance instead of treating it as accidental bundle growth.
|
|
21
25
|
AGENT_NATIVE_SERVERLESS_FFMPEG_ARCH = "x64"
|
|
22
26
|
|
|
23
27
|
# A2A delegation calls run a full agent loop (LLM + tool calls + DB queries).
|
|
@@ -148,6 +148,15 @@ export default defineEventHandler(async (event) => {
|
|
|
148
148
|
: [];
|
|
149
149
|
const role = access.role;
|
|
150
150
|
|
|
151
|
+
// The owner's email is only safe to disclose here when it's already
|
|
152
|
+
// public via the attendee list — an unauthenticated viewer must never
|
|
153
|
+
// learn an account email that isn't otherwise visible on this page.
|
|
154
|
+
const ownerEmailIsPublic = participants.some(
|
|
155
|
+
(participant) =>
|
|
156
|
+
participant.email.trim().toLowerCase() ===
|
|
157
|
+
meeting.ownerEmail?.trim().toLowerCase(),
|
|
158
|
+
);
|
|
159
|
+
|
|
151
160
|
return {
|
|
152
161
|
meeting: {
|
|
153
162
|
id: meeting.id,
|
|
@@ -160,6 +169,7 @@ export default defineEventHandler(async (event) => {
|
|
|
160
169
|
bullets: parseBullets(meeting.bulletsJson),
|
|
161
170
|
participants,
|
|
162
171
|
actionItems,
|
|
172
|
+
ownerEmail: ownerEmailIsPublic ? meeting.ownerEmail : null,
|
|
163
173
|
...(meeting.shareTranscript
|
|
164
174
|
? {
|
|
165
175
|
transcript: transcript
|
|
@@ -3,6 +3,7 @@ export interface TranscriptSegment {
|
|
|
3
3
|
endMs: number;
|
|
4
4
|
text: string;
|
|
5
5
|
source?: "mic" | "system";
|
|
6
|
+
speaker?: string | null;
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
const MAX_WORDS_PER_CAPTION = 7;
|
|
@@ -27,6 +28,8 @@ export function parseTranscriptSegments(
|
|
|
27
28
|
segment?.source === "mic" || segment?.source === "system"
|
|
28
29
|
? segment.source
|
|
29
30
|
: undefined,
|
|
31
|
+
speaker:
|
|
32
|
+
typeof segment?.speaker === "string" ? segment.speaker : undefined,
|
|
30
33
|
}))
|
|
31
34
|
.filter(
|
|
32
35
|
(segment) =>
|
|
@@ -43,6 +46,8 @@ export function parseTranscriptSegments(
|
|
|
43
46
|
export function buildCaptionSegmentsFromText(
|
|
44
47
|
text: string,
|
|
45
48
|
durationMs?: number | null,
|
|
49
|
+
source?: TranscriptSegment["source"],
|
|
50
|
+
speaker?: TranscriptSegment["speaker"],
|
|
46
51
|
): TranscriptSegment[] {
|
|
47
52
|
const chunks = splitIntoCaptionChunks(text);
|
|
48
53
|
if (chunks.length === 0) return [];
|
|
@@ -73,6 +78,8 @@ export function buildCaptionSegmentsFromText(
|
|
|
73
78
|
startMs: cursorMs,
|
|
74
79
|
endMs,
|
|
75
80
|
text: chunk,
|
|
81
|
+
source,
|
|
82
|
+
speaker,
|
|
76
83
|
};
|
|
77
84
|
cursorMs = endMs;
|
|
78
85
|
return segment;
|
|
@@ -98,7 +105,12 @@ export function normalizeTranscriptSegments({
|
|
|
98
105
|
.join(" ")
|
|
99
106
|
.trim();
|
|
100
107
|
if (validSegments.length <= 1) {
|
|
101
|
-
return buildCaptionSegmentsFromText(
|
|
108
|
+
return buildCaptionSegmentsFromText(
|
|
109
|
+
sourceText,
|
|
110
|
+
durationMs,
|
|
111
|
+
validSegments[0]?.source,
|
|
112
|
+
validSegments[0]?.speaker,
|
|
113
|
+
);
|
|
102
114
|
}
|
|
103
115
|
|
|
104
116
|
return validSegments.flatMap(splitTimedSegmentIntoCaptions);
|
|
@@ -160,7 +172,13 @@ function splitTimedSegmentIntoCaptions(
|
|
|
160
172
|
latestEndMs,
|
|
161
173
|
Math.max(cursorMs + minChunkMs, cursorMs + rawDuration),
|
|
162
174
|
);
|
|
163
|
-
const next = {
|
|
175
|
+
const next = {
|
|
176
|
+
startMs: cursorMs,
|
|
177
|
+
endMs,
|
|
178
|
+
text: chunk,
|
|
179
|
+
source: segment.source,
|
|
180
|
+
speaker: segment.speaker,
|
|
181
|
+
};
|
|
164
182
|
cursorMs = endMs;
|
|
165
183
|
return next;
|
|
166
184
|
});
|
|
@@ -14,6 +14,9 @@ AGENT_RUN_SOFT_TIMEOUT_MS = "55000"
|
|
|
14
14
|
# use the async worker so those calls are not cut off by the foreground wall.
|
|
15
15
|
AGENT_CHAT_DURABLE_BACKGROUND = "true"
|
|
16
16
|
|
|
17
|
+
[context.production.environment]
|
|
18
|
+
AGENT_NATIVE_HOSTED_HARNESS = "true"
|
|
19
|
+
|
|
17
20
|
# A2A delegation calls run a full agent loop (LLM + tool calls + DB queries).
|
|
18
21
|
# The framework does not impose a default agent-run timeout; if a template sets
|
|
19
22
|
# AGENT_RUN_SOFT_TIMEOUT_MS, keep it below this function timeout so the hidden
|
|
@@ -1084,6 +1084,8 @@ const enUS = {
|
|
|
1084
1084
|
"PNG capture isn't available in read-only previews",
|
|
1085
1085
|
pngSaveError: "Could not save PNG",
|
|
1086
1086
|
pngExportError: "Could not export PNG",
|
|
1087
|
+
pdfExportError: "Could not export PDF",
|
|
1088
|
+
pdfDownloaded: "PDF downloaded",
|
|
1087
1089
|
pdfAllScreensDownloaded: "PDF downloaded (all screens)",
|
|
1088
1090
|
openScreenSvg: "Open a screen before exporting SVG",
|
|
1089
1091
|
svgDownloaded: "SVG downloaded",
|
|
@@ -11,6 +11,9 @@ NITRO_PRESET = "netlify"
|
|
|
11
11
|
NPM_CONFIG_PRODUCTION = "false"
|
|
12
12
|
AGENT_CHAT_DURABLE_BACKGROUND = "true"
|
|
13
13
|
|
|
14
|
+
[context.production.environment]
|
|
15
|
+
AGENT_NATIVE_HOSTED_HARNESS = "true"
|
|
16
|
+
|
|
14
17
|
# Emit the Netlify background function used by Design's durable agent-chat opt-in.
|
|
15
18
|
# The normal server timeout still protects non-background HTTP handlers.
|
|
16
19
|
[functions."*"]
|