@agent-native/core 0.68.0 → 0.68.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/README.md +4 -4
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +18 -0
- package/corpus/core/docs/content/authentication.md +16 -2
- package/corpus/core/docs/content/template-analytics.md +14 -9
- package/corpus/core/docs/content/template-clips.md +30 -2
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/engine/builder-engine.ts +22 -3
- package/corpus/core/src/agent/engine/types.ts +1 -0
- package/corpus/core/src/agent/production-agent.ts +2 -0
- package/corpus/core/src/agent/types.ts +1 -0
- package/corpus/core/src/cli/pr-visual-recap-workflow.ts +1 -1
- package/corpus/core/src/client/AssistantChat.tsx +45 -6
- package/corpus/core/src/client/CommandMenu.tsx +98 -3
- package/corpus/core/src/client/ConnectBuilderCard.tsx +11 -2
- package/corpus/core/src/client/active-run-state.ts +16 -0
- package/corpus/core/src/client/guided-questions.tsx +73 -17
- package/corpus/core/src/client/index.ts +2 -0
- package/corpus/core/src/client/sse-event-processor.ts +9 -0
- package/corpus/core/src/oauth-tokens/google-refresh.ts +32 -17
- package/corpus/core/src/provider-api/index.ts +37 -17
- package/corpus/core/src/server/auth.ts +6 -8
- package/corpus/core/src/server/better-auth-instance.ts +19 -4
- package/corpus/core/src/server/core-routes-plugin.ts +157 -5
- package/corpus/core/src/server/google-oauth-credentials.ts +51 -0
- package/corpus/core/src/server/index.ts +8 -0
- package/corpus/core/src/server/onboarding-html.ts +2 -1
- package/corpus/core/src/templates/workspace-root/.env.example +4 -0
- package/corpus/templates/analytics/app/routes/$.tsx +9 -0
- package/corpus/templates/calendar/app/components/calendar/DayView.tsx +33 -1
- package/corpus/templates/calendar/app/components/calendar/EventAttendeesSection.tsx +11 -3
- package/corpus/templates/calendar/app/components/calendar/EventCard.tsx +25 -2
- package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +12 -0
- package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +7 -2
- package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +37 -78
- package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +33 -1
- package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +46 -3
- package/corpus/templates/calendar/app/hooks/use-google-auth.ts +203 -2
- package/corpus/templates/calendar/app/lib/event-colors.ts +1 -4
- package/corpus/templates/calendar/app/lib/rsvp-status.tsx +6 -0
- package/corpus/templates/calendar/app/pages/CalendarView.tsx +9 -4
- package/corpus/templates/calendar/app/pages/NotFound.tsx +1 -1
- package/corpus/templates/calendar/app/pages/Settings.tsx +25 -1
- package/corpus/templates/calendar/app/routes/_app.$.tsx +9 -0
- package/corpus/templates/calendar/server/handlers/google-auth.ts +152 -12
- package/corpus/templates/calendar/server/lib/google-calendar.ts +63 -14
- package/corpus/templates/calendar/server/plugins/auth.ts +4 -20
- package/corpus/templates/calendar/shared/api.ts +4 -0
- package/corpus/templates/clips/.agents/skills/recording/SKILL.md +17 -0
- package/corpus/templates/clips/actions/sync-calendars.ts +6 -7
- package/corpus/templates/clips/app/components/capture-install-options.tsx +157 -0
- package/corpus/templates/clips/app/components/library/library-layout.tsx +9 -8
- package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +4 -6
- package/corpus/templates/clips/app/lib/capture-install-options.ts +13 -0
- package/corpus/templates/clips/app/root.tsx +54 -2
- package/corpus/templates/clips/app/routes/_app.dictate.tsx +9 -13
- package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +5 -7
- package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +5 -7
- package/corpus/templates/clips/app/routes/download.tsx +49 -0
- package/corpus/templates/clips/app/routes/record.tsx +4 -4
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +5 -6
- package/corpus/templates/clips/chrome-extension/STORE_DRAFT.md +26 -0
- package/corpus/templates/clips/server/lib/calendar-event-meetings.ts +6 -7
- package/corpus/templates/clips/server/lib/google-calendar-client.ts +61 -0
- package/corpus/templates/clips/server/routes/_agent-native/google/auth-url.get.ts +19 -12
- package/corpus/templates/clips/server/routes/_agent-native/google/callback.get.ts +6 -6
- package/corpus/templates/design/actions/show-design-questions.ts +15 -1
- package/corpus/templates/design/app/hooks/use-agent-generating.ts +47 -8
- package/corpus/templates/design/app/pages/DesignEditor.tsx +6 -0
- package/corpus/templates/design/app/pages/NotFound.tsx +1 -1
- package/corpus/templates/design/app/routes/$.tsx +9 -0
- package/corpus/templates/mail/app/pages/NotFound.tsx +1 -1
- package/corpus/templates/mail/app/routes/$.tsx +9 -0
- package/corpus/templates/plan/app/global.css +2 -2
- package/corpus/templates/slides/app/pages/NotFound.tsx +1 -1
- package/corpus/templates/slides/app/routes/$.tsx +9 -0
- package/corpus/templates/slides/server/lib/google-docs-oauth.ts +81 -25
- package/corpus/templates/videos/app/pages/NotFound.tsx +1 -1
- package/dist/agent/engine/builder-engine.d.ts.map +1 -1
- package/dist/agent/engine/builder-engine.js +21 -3
- package/dist/agent/engine/builder-engine.js.map +1 -1
- package/dist/agent/engine/types.d.ts +2 -0
- package/dist/agent/engine/types.d.ts.map +1 -1
- package/dist/agent/engine/types.js.map +1 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +3 -0
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/types.d.ts +2 -0
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/cli/pr-visual-recap-workflow.d.ts +1 -1
- package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
- package/dist/cli/pr-visual-recap-workflow.js +1 -1
- package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +42 -7
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/CommandMenu.d.ts +15 -2
- package/dist/client/CommandMenu.d.ts.map +1 -1
- package/dist/client/CommandMenu.js +46 -3
- package/dist/client/CommandMenu.js.map +1 -1
- package/dist/client/ConnectBuilderCard.d.ts.map +1 -1
- package/dist/client/ConnectBuilderCard.js +11 -2
- package/dist/client/ConnectBuilderCard.js.map +1 -1
- package/dist/client/active-run-state.d.ts +2 -0
- package/dist/client/active-run-state.d.ts.map +1 -1
- package/dist/client/active-run-state.js +10 -0
- package/dist/client/active-run-state.js.map +1 -1
- package/dist/client/guided-questions.d.ts +2 -0
- package/dist/client/guided-questions.d.ts.map +1 -1
- package/dist/client/guided-questions.js +52 -7
- package/dist/client/guided-questions.js.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/sse-event-processor.d.ts +1 -1
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +7 -1
- package/dist/client/sse-event-processor.js.map +1 -1
- package/dist/oauth-tokens/google-refresh.d.ts.map +1 -1
- package/dist/oauth-tokens/google-refresh.js +29 -17
- package/dist/oauth-tokens/google-refresh.js.map +1 -1
- package/dist/provider-api/index.d.ts.map +1 -1
- package/dist/provider-api/index.js +28 -16
- package/dist/provider-api/index.js.map +1 -1
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +6 -6
- package/dist/server/auth.js.map +1 -1
- package/dist/server/better-auth-instance.d.ts.map +1 -1
- package/dist/server/better-auth-instance.js +18 -4
- package/dist/server/better-auth-instance.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts +5 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +107 -5
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/google-oauth-credentials.d.ts +15 -0
- package/dist/server/google-oauth-credentials.d.ts.map +1 -0
- package/dist/server/google-oauth-credentials.js +34 -0
- package/dist/server/google-oauth-credentials.js.map +1 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/onboarding-html.d.ts.map +1 -1
- package/dist/server/onboarding-html.js +2 -1
- package/dist/server/onboarding-html.js.map +1 -1
- package/dist/templates/workspace-root/.env.example +4 -0
- package/docs/content/authentication.md +16 -2
- package/docs/content/template-analytics.md +14 -9
- package/docs/content/template-clips.md +30 -2
- package/package.json +1 -1
- package/src/templates/workspace-root/.env.example +4 -0
|
@@ -21,7 +21,9 @@ import {
|
|
|
21
21
|
useGoogleAuthStatus,
|
|
22
22
|
useGoogleAuthUrl,
|
|
23
23
|
useGoogleAddAccountUrl,
|
|
24
|
+
useGoogleDesktopAuth,
|
|
24
25
|
useDisconnectGoogle,
|
|
26
|
+
type DesktopAuthIssue,
|
|
25
27
|
} from "@/hooks/use-google-auth";
|
|
26
28
|
|
|
27
29
|
interface EnvKeyStatus {
|
|
@@ -66,15 +68,6 @@ interface GoogleConnectBannerProps {
|
|
|
66
68
|
variant?: "banner" | "hero";
|
|
67
69
|
}
|
|
68
70
|
|
|
69
|
-
interface DesktopAuthIssue {
|
|
70
|
-
error?: string;
|
|
71
|
-
message?: string;
|
|
72
|
-
code?: string;
|
|
73
|
-
accountId?: string;
|
|
74
|
-
existingOwner?: string;
|
|
75
|
-
attemptedOwner?: string;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
71
|
export function GoogleConnectBanner({
|
|
79
72
|
variant = "banner",
|
|
80
73
|
}: GoogleConnectBannerProps) {
|
|
@@ -93,76 +86,23 @@ export function GoogleConnectBanner({
|
|
|
93
86
|
const hasAccounts = accounts.length > 0;
|
|
94
87
|
|
|
95
88
|
const isBuilderFrame = useMemo(() => isInBuilderFrame(), []);
|
|
96
|
-
const useDesktopAuth = useMemo(
|
|
97
|
-
() => /AgentNativeDesktop/i.test(navigator.userAgent) && !isBuilderFrame,
|
|
98
|
-
[isBuilderFrame],
|
|
99
|
-
);
|
|
100
|
-
const desktopPollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
101
89
|
const authPollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
102
90
|
const addAccountPollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
91
|
+
const {
|
|
92
|
+
isDesktopGoogleAuth,
|
|
93
|
+
isGoogleDesktopAuthPending,
|
|
94
|
+
startDesktopGoogleAuth,
|
|
95
|
+
} = useGoogleDesktopAuth({
|
|
96
|
+
onError: setDesktopAuthIssue,
|
|
97
|
+
onSuccess: () => window.location.reload(),
|
|
98
|
+
});
|
|
103
99
|
useEffect(() => {
|
|
104
100
|
return () => {
|
|
105
|
-
if (desktopPollRef.current) clearInterval(desktopPollRef.current);
|
|
106
101
|
if (authPollRef.current) clearInterval(authPollRef.current);
|
|
107
102
|
if (addAccountPollRef.current) clearInterval(addAccountPollRef.current);
|
|
108
103
|
};
|
|
109
104
|
}, []);
|
|
110
105
|
|
|
111
|
-
function signInViaDesktopBrowser(addAccount = false) {
|
|
112
|
-
setDesktopAuthIssue(null);
|
|
113
|
-
const flowId =
|
|
114
|
-
crypto.randomUUID?.() ||
|
|
115
|
-
Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
116
|
-
const origin = window.location.origin;
|
|
117
|
-
const endpoint = addAccount
|
|
118
|
-
? "/_agent-native/google/add-account/auth-url"
|
|
119
|
-
: "/_agent-native/google/auth-url";
|
|
120
|
-
const redirectUri = encodeURIComponent(
|
|
121
|
-
oauthRedirectUri("/_agent-native/google/callback"),
|
|
122
|
-
);
|
|
123
|
-
window.open(
|
|
124
|
-
`${origin}${agentNativePath(endpoint)}?redirect_uri=${redirectUri}&desktop=1&flow_id=${flowId}&redirect=1`,
|
|
125
|
-
"_blank",
|
|
126
|
-
);
|
|
127
|
-
const start = Date.now();
|
|
128
|
-
if (desktopPollRef.current) clearInterval(desktopPollRef.current);
|
|
129
|
-
desktopPollRef.current = setInterval(async () => {
|
|
130
|
-
try {
|
|
131
|
-
const res = await fetch(
|
|
132
|
-
agentNativePath(
|
|
133
|
-
`/_agent-native/auth/desktop-exchange?flow_id=${flowId}`,
|
|
134
|
-
),
|
|
135
|
-
);
|
|
136
|
-
const data = await res.json();
|
|
137
|
-
if (data?.error) {
|
|
138
|
-
clearInterval(desktopPollRef.current!);
|
|
139
|
-
desktopPollRef.current = null;
|
|
140
|
-
setDesktopAuthIssue(data);
|
|
141
|
-
} else if (data?.token) {
|
|
142
|
-
clearInterval(desktopPollRef.current!);
|
|
143
|
-
desktopPollRef.current = null;
|
|
144
|
-
await fetch(
|
|
145
|
-
agentNativePath(
|
|
146
|
-
`/_agent-native/auth/session?_session=${data.token}`,
|
|
147
|
-
),
|
|
148
|
-
{
|
|
149
|
-
credentials: "include",
|
|
150
|
-
},
|
|
151
|
-
);
|
|
152
|
-
window.location.reload();
|
|
153
|
-
} else if (Date.now() - start > 120_000) {
|
|
154
|
-
clearInterval(desktopPollRef.current!);
|
|
155
|
-
desktopPollRef.current = null;
|
|
156
|
-
}
|
|
157
|
-
} catch {
|
|
158
|
-
if (Date.now() - start > 120_000) {
|
|
159
|
-
clearInterval(desktopPollRef.current!);
|
|
160
|
-
desktopPollRef.current = null;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}, 1500);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
106
|
// Wizard state
|
|
167
107
|
const [currentStep, setCurrentStep] = useState(0);
|
|
168
108
|
const [saving, setSaving] = useState(false);
|
|
@@ -293,16 +233,19 @@ export function GoogleConnectBanner({
|
|
|
293
233
|
|
|
294
234
|
function handleConnect() {
|
|
295
235
|
setDesktopAuthIssue(null);
|
|
296
|
-
if (
|
|
297
|
-
|
|
236
|
+
if (isDesktopGoogleAuth) {
|
|
237
|
+
startDesktopGoogleAuth({ previousAccountCount: accounts.length });
|
|
298
238
|
return;
|
|
299
239
|
}
|
|
300
240
|
setWantAuthUrl(true);
|
|
301
241
|
}
|
|
302
242
|
|
|
303
243
|
function handleAddAccount() {
|
|
304
|
-
if (
|
|
305
|
-
|
|
244
|
+
if (isDesktopGoogleAuth) {
|
|
245
|
+
startDesktopGoogleAuth({
|
|
246
|
+
addAccount: true,
|
|
247
|
+
previousAccountCount: accounts.length,
|
|
248
|
+
});
|
|
306
249
|
return;
|
|
307
250
|
}
|
|
308
251
|
setWantAddAccount(true);
|
|
@@ -377,7 +320,11 @@ export function GoogleConnectBanner({
|
|
|
377
320
|
size="sm"
|
|
378
321
|
className="mt-6 gap-2 px-4 h-8 text-[13px] font-medium"
|
|
379
322
|
onClick={handleConnect}
|
|
380
|
-
disabled={
|
|
323
|
+
disabled={
|
|
324
|
+
authUrl.isLoading ||
|
|
325
|
+
authUrl.isFetching ||
|
|
326
|
+
isGoogleDesktopAuthPending
|
|
327
|
+
}
|
|
381
328
|
>
|
|
382
329
|
<GoogleIcon className="h-3.5 w-3.5" />
|
|
383
330
|
{authUrl.isLoading
|
|
@@ -458,7 +405,11 @@ export function GoogleConnectBanner({
|
|
|
458
405
|
))}
|
|
459
406
|
<button
|
|
460
407
|
onClick={handleAddAccount}
|
|
461
|
-
disabled={
|
|
408
|
+
disabled={
|
|
409
|
+
addAccountUrl.isLoading ||
|
|
410
|
+
addAccountUrl.isFetching ||
|
|
411
|
+
isGoogleDesktopAuthPending
|
|
412
|
+
}
|
|
462
413
|
className="text-xs text-foreground/40 hover:text-foreground/60 transition-colors whitespace-nowrap"
|
|
463
414
|
>
|
|
464
415
|
+ Add account
|
|
@@ -515,7 +466,11 @@ export function GoogleConnectBanner({
|
|
|
515
466
|
size="sm"
|
|
516
467
|
className="gap-1.5 text-xs h-7 font-medium bg-white text-black hover:bg-white/90"
|
|
517
468
|
onClick={handleConnect}
|
|
518
|
-
disabled={
|
|
469
|
+
disabled={
|
|
470
|
+
authUrl.isLoading ||
|
|
471
|
+
authUrl.isFetching ||
|
|
472
|
+
isGoogleDesktopAuthPending
|
|
473
|
+
}
|
|
519
474
|
>
|
|
520
475
|
<GoogleIcon className="h-3 w-3" />
|
|
521
476
|
{authUrl.isLoading ? "Connecting..." : "Sign in with Google"}
|
|
@@ -526,7 +481,11 @@ export function GoogleConnectBanner({
|
|
|
526
481
|
variant="outline"
|
|
527
482
|
className="gap-1.5 text-xs h-7 font-medium"
|
|
528
483
|
onClick={handleConnect}
|
|
529
|
-
disabled={
|
|
484
|
+
disabled={
|
|
485
|
+
authUrl.isLoading ||
|
|
486
|
+
authUrl.isFetching ||
|
|
487
|
+
isGoogleDesktopAuthPending
|
|
488
|
+
}
|
|
530
489
|
>
|
|
531
490
|
{authUrl.isLoading ? "..." : "Connect Google"}
|
|
532
491
|
</Button>
|
|
@@ -592,7 +592,17 @@ export function WeekView({
|
|
|
592
592
|
onDraftDiscard={onDraftDiscard}
|
|
593
593
|
>
|
|
594
594
|
<button
|
|
595
|
-
className=
|
|
595
|
+
className={cn(
|
|
596
|
+
"absolute flex items-center gap-1 truncate rounded px-1.5 text-left text-[11px] font-medium text-foreground transition-opacity hover:opacity-80",
|
|
597
|
+
event.ownerColor && "pr-3.5",
|
|
598
|
+
)}
|
|
599
|
+
aria-label={
|
|
600
|
+
event.ownerName || event.overlayEmail
|
|
601
|
+
? `${event.title}, ${
|
|
602
|
+
event.ownerName || event.overlayEmail
|
|
603
|
+
}'s calendar`
|
|
604
|
+
: event.title
|
|
605
|
+
}
|
|
596
606
|
style={{
|
|
597
607
|
top: `${rowIdx * allDayRowHeight + 4}px`,
|
|
598
608
|
left: `${leftPct}%`,
|
|
@@ -613,6 +623,13 @@ export function WeekView({
|
|
|
613
623
|
)}
|
|
614
624
|
<EventStatusIcon event={event} className="shrink-0" />
|
|
615
625
|
<span className="truncate">{event.title}</span>
|
|
626
|
+
{event.ownerColor && (
|
|
627
|
+
<span
|
|
628
|
+
aria-hidden="true"
|
|
629
|
+
className="absolute right-1 top-1/2 size-1.5 -translate-y-1/2 rounded-full ring-1 ring-background/70"
|
|
630
|
+
style={{ backgroundColor: event.ownerColor }}
|
|
631
|
+
/>
|
|
632
|
+
)}
|
|
616
633
|
</button>
|
|
617
634
|
</EventDetailPopover>
|
|
618
635
|
);
|
|
@@ -864,7 +881,15 @@ export function WeekView({
|
|
|
864
881
|
"ring-2 ring-primary/40",
|
|
865
882
|
canDrag && segmentStartsHere && "cursor-grab",
|
|
866
883
|
isBeingDragged && isDragging && "cursor-grabbing",
|
|
884
|
+
event.ownerColor && "pr-4",
|
|
867
885
|
)}
|
|
886
|
+
aria-label={
|
|
887
|
+
event.ownerName || event.overlayEmail
|
|
888
|
+
? `${event.title}, ${
|
|
889
|
+
event.ownerName || event.overlayEmail
|
|
890
|
+
}'s calendar`
|
|
891
|
+
: event.title
|
|
892
|
+
}
|
|
868
893
|
style={{
|
|
869
894
|
...style,
|
|
870
895
|
left: `${li.left}px`,
|
|
@@ -894,6 +919,13 @@ export function WeekView({
|
|
|
894
919
|
isBeingDragged && isDragging ? 0.9 : undefined,
|
|
895
920
|
}}
|
|
896
921
|
>
|
|
922
|
+
{event.ownerColor && (
|
|
923
|
+
<span
|
|
924
|
+
aria-hidden="true"
|
|
925
|
+
className="absolute right-1.5 top-1.5 size-1.5 rounded-full ring-1 ring-background/70"
|
|
926
|
+
style={{ backgroundColor: event.ownerColor }}
|
|
927
|
+
/>
|
|
928
|
+
)}
|
|
897
929
|
{durationMin <= 30 ? (
|
|
898
930
|
<div className="flex items-baseline gap-1 truncate">
|
|
899
931
|
{allOthersOut && (
|
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
useGoogleAuthStatus,
|
|
47
47
|
useGoogleAuthUrl,
|
|
48
48
|
useGoogleAddAccountUrl,
|
|
49
|
+
useGoogleDesktopAuth,
|
|
49
50
|
} from "@/hooks/use-google-auth";
|
|
50
51
|
import {
|
|
51
52
|
appPath,
|
|
@@ -305,6 +306,13 @@ function MiniCalendar({
|
|
|
305
306
|
function GoogleConnectSidebarButton() {
|
|
306
307
|
const [wantAuthUrl, setWantAuthUrl] = useState(false);
|
|
307
308
|
const authUrl = useGoogleAuthUrl(wantAuthUrl);
|
|
309
|
+
const {
|
|
310
|
+
isDesktopGoogleAuth,
|
|
311
|
+
isGoogleDesktopAuthPending,
|
|
312
|
+
startDesktopGoogleAuth,
|
|
313
|
+
} = useGoogleDesktopAuth({
|
|
314
|
+
onSuccess: () => window.location.reload(),
|
|
315
|
+
});
|
|
308
316
|
|
|
309
317
|
useEffect(() => {
|
|
310
318
|
if (!wantAuthUrl || !authUrl.data?.url) return;
|
|
@@ -312,6 +320,14 @@ function GoogleConnectSidebarButton() {
|
|
|
312
320
|
window.open(authUrl.data.url, "_blank");
|
|
313
321
|
}, [wantAuthUrl, authUrl.data]);
|
|
314
322
|
|
|
323
|
+
function handleConnect() {
|
|
324
|
+
if (isDesktopGoogleAuth) {
|
|
325
|
+
startDesktopGoogleAuth({ previousAccountCount: 0 });
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
setWantAuthUrl(true);
|
|
329
|
+
}
|
|
330
|
+
|
|
315
331
|
return (
|
|
316
332
|
<div className="border-t border-border p-3">
|
|
317
333
|
<div className="rounded-lg bg-primary/10 p-3">
|
|
@@ -324,8 +340,12 @@ function GoogleConnectSidebarButton() {
|
|
|
324
340
|
<Button
|
|
325
341
|
size="sm"
|
|
326
342
|
className="w-full gap-1.5 text-xs font-semibold"
|
|
327
|
-
onClick={
|
|
328
|
-
disabled={
|
|
343
|
+
onClick={handleConnect}
|
|
344
|
+
disabled={
|
|
345
|
+
authUrl.isLoading ||
|
|
346
|
+
authUrl.isFetching ||
|
|
347
|
+
isGoogleDesktopAuthPending
|
|
348
|
+
}
|
|
329
349
|
>
|
|
330
350
|
<IconExternalLink className="h-3 w-3" />
|
|
331
351
|
{authUrl.isLoading ? "Connecting..." : "Connect"}
|
|
@@ -398,6 +418,13 @@ function GoogleAccountsSection({
|
|
|
398
418
|
} = useViewPreferences();
|
|
399
419
|
const [wantAddAccount, setWantAddAccount] = useState(false);
|
|
400
420
|
const addAccountUrl = useGoogleAddAccountUrl(wantAddAccount);
|
|
421
|
+
const {
|
|
422
|
+
isDesktopGoogleAuth,
|
|
423
|
+
isGoogleDesktopAuthPending,
|
|
424
|
+
startDesktopGoogleAuth,
|
|
425
|
+
} = useGoogleDesktopAuth({
|
|
426
|
+
onSuccess: () => window.location.reload(),
|
|
427
|
+
});
|
|
401
428
|
|
|
402
429
|
useEffect(() => {
|
|
403
430
|
if (!wantAddAccount || !addAccountUrl.data?.url) return;
|
|
@@ -405,6 +432,17 @@ function GoogleAccountsSection({
|
|
|
405
432
|
setWantAddAccount(false);
|
|
406
433
|
}, [wantAddAccount, addAccountUrl.data]);
|
|
407
434
|
|
|
435
|
+
function handleAddAccount() {
|
|
436
|
+
if (isDesktopGoogleAuth) {
|
|
437
|
+
startDesktopGoogleAuth({
|
|
438
|
+
addAccount: true,
|
|
439
|
+
previousAccountCount: accounts.length,
|
|
440
|
+
});
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
setWantAddAccount(true);
|
|
444
|
+
}
|
|
445
|
+
|
|
408
446
|
function handlePickColor(color: string) {
|
|
409
447
|
updateViewPreferences({ colorMode: "single", singleColor: color });
|
|
410
448
|
}
|
|
@@ -438,7 +476,12 @@ function GoogleAccountsSection({
|
|
|
438
476
|
<TooltipTrigger asChild>
|
|
439
477
|
<button
|
|
440
478
|
type="button"
|
|
441
|
-
onClick={
|
|
479
|
+
onClick={handleAddAccount}
|
|
480
|
+
disabled={
|
|
481
|
+
addAccountUrl.isLoading ||
|
|
482
|
+
addAccountUrl.isFetching ||
|
|
483
|
+
isGoogleDesktopAuthPending
|
|
484
|
+
}
|
|
442
485
|
className="flex h-5 w-5 items-center justify-center rounded text-muted-foreground hover:text-foreground"
|
|
443
486
|
>
|
|
444
487
|
<IconPlus className="h-3.5 w-3.5" />
|
|
@@ -1,7 +1,36 @@
|
|
|
1
1
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
agentNativePath,
|
|
4
|
+
isInBuilderFrame,
|
|
5
|
+
oauthRedirectUri,
|
|
6
|
+
} from "@agent-native/core/client";
|
|
3
7
|
import type { GoogleAuthStatus } from "@shared/api";
|
|
4
|
-
import { useEffect } from "react";
|
|
8
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
9
|
+
|
|
10
|
+
export interface DesktopAuthIssue {
|
|
11
|
+
error?: string;
|
|
12
|
+
message?: string;
|
|
13
|
+
code?: string;
|
|
14
|
+
accountId?: string;
|
|
15
|
+
existingOwner?: string;
|
|
16
|
+
attemptedOwner?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface DesktopAuthResult {
|
|
20
|
+
token?: string;
|
|
21
|
+
email?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface DesktopAuthStartOptions {
|
|
25
|
+
addAccount?: boolean;
|
|
26
|
+
previousAccountCount?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface DesktopAuthOptions {
|
|
30
|
+
onError?: (issue: DesktopAuthIssue) => void;
|
|
31
|
+
onSuccess?: (result: DesktopAuthResult) => void | Promise<void>;
|
|
32
|
+
timeoutMs?: number;
|
|
33
|
+
}
|
|
5
34
|
|
|
6
35
|
function bodyError(
|
|
7
36
|
body: any,
|
|
@@ -134,6 +163,178 @@ export function useGoogleAddAccountUrl(enabled = false) {
|
|
|
134
163
|
return query;
|
|
135
164
|
}
|
|
136
165
|
|
|
166
|
+
export function useGoogleDesktopAuth(options: DesktopAuthOptions = {}) {
|
|
167
|
+
const { onError, onSuccess, timeoutMs = 120_000 } = options;
|
|
168
|
+
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
169
|
+
const [isPending, setIsPending] = useState(false);
|
|
170
|
+
const isDesktopGoogleAuth = useMemo(() => {
|
|
171
|
+
if (typeof navigator === "undefined") return false;
|
|
172
|
+
return (
|
|
173
|
+
/AgentNativeDesktop/i.test(navigator.userAgent) && !isInBuilderFrame()
|
|
174
|
+
);
|
|
175
|
+
}, []);
|
|
176
|
+
|
|
177
|
+
const clearPoll = useCallback(() => {
|
|
178
|
+
if (pollRef.current) {
|
|
179
|
+
clearInterval(pollRef.current);
|
|
180
|
+
pollRef.current = null;
|
|
181
|
+
}
|
|
182
|
+
}, []);
|
|
183
|
+
|
|
184
|
+
useEffect(() => clearPoll, [clearPoll]);
|
|
185
|
+
|
|
186
|
+
const startDesktopGoogleAuth = useCallback(
|
|
187
|
+
(startOptions: DesktopAuthStartOptions = {}) => {
|
|
188
|
+
if (!isDesktopGoogleAuth || typeof window === "undefined") return false;
|
|
189
|
+
|
|
190
|
+
clearPoll();
|
|
191
|
+
setIsPending(true);
|
|
192
|
+
const flowId =
|
|
193
|
+
globalThis.crypto?.randomUUID?.() ||
|
|
194
|
+
Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
195
|
+
const redirectUri = oauthRedirectUri("/_agent-native/google/callback");
|
|
196
|
+
const params = new URLSearchParams({
|
|
197
|
+
redirect_uri: redirectUri,
|
|
198
|
+
desktop: "1",
|
|
199
|
+
flow_id: flowId,
|
|
200
|
+
});
|
|
201
|
+
let popup: Window | null = null;
|
|
202
|
+
const reportError = (issue: DesktopAuthIssue) => {
|
|
203
|
+
clearPoll();
|
|
204
|
+
setIsPending(false);
|
|
205
|
+
onError?.(issue);
|
|
206
|
+
};
|
|
207
|
+
const openAuthUrl = (url: string) => {
|
|
208
|
+
if (popup && !popup.closed) {
|
|
209
|
+
popup.location.href = url;
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
popup = window.open(url, "_blank");
|
|
213
|
+
return !!popup;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const startedAt = Date.now();
|
|
217
|
+
const finish = async (result: DesktopAuthResult = {}) => {
|
|
218
|
+
clearPoll();
|
|
219
|
+
setIsPending(false);
|
|
220
|
+
await onSuccess?.(result);
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
if (startOptions.addAccount) {
|
|
224
|
+
popup = window.open("about:blank", "_blank");
|
|
225
|
+
void (async () => {
|
|
226
|
+
try {
|
|
227
|
+
const { url } = await fetchJson<{ url: string }>(
|
|
228
|
+
agentNativePath(
|
|
229
|
+
`/_agent-native/google/add-account/auth-url?${params.toString()}`,
|
|
230
|
+
),
|
|
231
|
+
{ credentials: "include" },
|
|
232
|
+
);
|
|
233
|
+
if (!openAuthUrl(url)) {
|
|
234
|
+
reportError({
|
|
235
|
+
code: "popup_blocked",
|
|
236
|
+
message:
|
|
237
|
+
"Calendar could not open Google sign-in. Allow popups and try again.",
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
} catch (err) {
|
|
241
|
+
popup?.close();
|
|
242
|
+
reportError({
|
|
243
|
+
code: "desktop_auth_start_failed",
|
|
244
|
+
message:
|
|
245
|
+
err instanceof Error
|
|
246
|
+
? err.message
|
|
247
|
+
: "Could not start Google sign-in.",
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
})();
|
|
251
|
+
} else {
|
|
252
|
+
params.set("redirect", "1");
|
|
253
|
+
const opened = openAuthUrl(
|
|
254
|
+
`${window.location.origin}${agentNativePath(
|
|
255
|
+
"/_agent-native/google/auth-url",
|
|
256
|
+
)}?${params.toString()}`,
|
|
257
|
+
);
|
|
258
|
+
if (!opened) {
|
|
259
|
+
reportError({
|
|
260
|
+
code: "popup_blocked",
|
|
261
|
+
message:
|
|
262
|
+
"Calendar could not open Google sign-in. Allow popups and try again.",
|
|
263
|
+
});
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
pollRef.current = setInterval(async () => {
|
|
269
|
+
try {
|
|
270
|
+
const exchangeRes = await fetch(
|
|
271
|
+
agentNativePath(
|
|
272
|
+
`/_agent-native/auth/desktop-exchange?flow_id=${flowId}`,
|
|
273
|
+
),
|
|
274
|
+
{ credentials: "include" },
|
|
275
|
+
);
|
|
276
|
+
const exchange = await exchangeRes.json();
|
|
277
|
+
if (exchange?.error) {
|
|
278
|
+
clearPoll();
|
|
279
|
+
setIsPending(false);
|
|
280
|
+
onError?.(exchange);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (exchange?.token) {
|
|
284
|
+
await fetch(
|
|
285
|
+
agentNativePath(
|
|
286
|
+
`/_agent-native/auth/session?_session=${exchange.token}`,
|
|
287
|
+
),
|
|
288
|
+
{ credentials: "include" },
|
|
289
|
+
);
|
|
290
|
+
await finish({ token: exchange.token, email: exchange.email });
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
} catch {
|
|
294
|
+
// Keep polling; the status endpoint below may still observe success.
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
const statusRes = await fetch(
|
|
299
|
+
agentNativePath("/_agent-native/google/status"),
|
|
300
|
+
{ credentials: "include" },
|
|
301
|
+
);
|
|
302
|
+
if (statusRes.ok) {
|
|
303
|
+
const status = (await statusRes.json()) as GoogleAuthStatus;
|
|
304
|
+
const connected = startOptions.addAccount
|
|
305
|
+
? (status.accounts?.length ?? 0) >
|
|
306
|
+
(startOptions.previousAccountCount ?? 0)
|
|
307
|
+
: status.connected;
|
|
308
|
+
if (connected) {
|
|
309
|
+
await finish();
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
} catch {
|
|
314
|
+
// Keep polling until the timeout.
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (Date.now() - startedAt > timeoutMs) {
|
|
318
|
+
reportError({
|
|
319
|
+
code: "desktop_auth_timeout",
|
|
320
|
+
message:
|
|
321
|
+
"Google sign-in timed out. Finish sign-in in the browser or try again.",
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}, 1500);
|
|
325
|
+
|
|
326
|
+
return true;
|
|
327
|
+
},
|
|
328
|
+
[clearPoll, isDesktopGoogleAuth, onError, onSuccess, timeoutMs],
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
isDesktopGoogleAuth,
|
|
333
|
+
isGoogleDesktopAuthPending: isPending,
|
|
334
|
+
startDesktopGoogleAuth,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
137
338
|
export function useDisconnectGoogle() {
|
|
138
339
|
const queryClient = useQueryClient();
|
|
139
340
|
return useMutation({
|
|
@@ -107,16 +107,13 @@ export function allOtherDeclined(event: CalendarEvent): boolean {
|
|
|
107
107
|
|
|
108
108
|
/**
|
|
109
109
|
* Returns a hex color for a calendar event based on its meeting type.
|
|
110
|
-
* Respects user-set colors
|
|
110
|
+
* Respects user-set colors first.
|
|
111
111
|
* For local (non-Google) events without a color, returns CSS var.
|
|
112
112
|
*/
|
|
113
113
|
export function getEventAutoColor(event: CalendarEvent): string {
|
|
114
114
|
// User/Google-set color takes priority
|
|
115
115
|
if (event.color) return event.color;
|
|
116
116
|
|
|
117
|
-
// Overlay events keep their overlay-assigned color
|
|
118
|
-
if (event.overlayEmail) return EVENT_CATEGORY_COLORS.fallback;
|
|
119
|
-
|
|
120
117
|
// Local events without a color use the theme primary
|
|
121
118
|
if (event.source !== "google") return "hsl(var(--primary))";
|
|
122
119
|
|
|
@@ -27,6 +27,12 @@ export function getRsvpStatusLabel(status?: string) {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
export function canInlineRsvp(
|
|
31
|
+
event: Pick<CalendarEvent, "source" | "overlayEmail">,
|
|
32
|
+
): boolean {
|
|
33
|
+
return event.source === "google" && !event.overlayEmail;
|
|
34
|
+
}
|
|
35
|
+
|
|
30
36
|
export function RsvpStatusIcon({
|
|
31
37
|
status,
|
|
32
38
|
className,
|
|
@@ -462,16 +462,21 @@ export default function CalendarView() {
|
|
|
462
462
|
// small non-blocking spinner instead of hiding everything behind a skeleton.
|
|
463
463
|
const eventsRefreshing = isFetching && !eventsLoading;
|
|
464
464
|
|
|
465
|
-
// Apply overlay
|
|
465
|
+
// Apply overlay ownership markers and filter hidden calendars
|
|
466
466
|
const events = useMemo(() => {
|
|
467
|
-
const
|
|
467
|
+
const ownerMap = new Map(overlayPeople.map((p) => [p.email, p]));
|
|
468
468
|
const sourceEvents = draftEvent
|
|
469
469
|
? [...rawEvents.filter((e) => e.id !== draftEvent.id), draftEvent]
|
|
470
470
|
: rawEvents;
|
|
471
471
|
return sourceEvents
|
|
472
472
|
.map((e) => {
|
|
473
|
-
if (e.overlayEmail &&
|
|
474
|
-
|
|
473
|
+
if (e.overlayEmail && ownerMap.has(e.overlayEmail)) {
|
|
474
|
+
const owner = ownerMap.get(e.overlayEmail);
|
|
475
|
+
return {
|
|
476
|
+
...e,
|
|
477
|
+
ownerColor: owner?.color,
|
|
478
|
+
ownerName: owner?.name,
|
|
479
|
+
};
|
|
475
480
|
}
|
|
476
481
|
const tempId = quickEditTempIds[e.id];
|
|
477
482
|
return tempId && !e._tempId ? { ...e, _tempId: tempId } : e;
|
|
@@ -10,7 +10,7 @@ export default function NotFound() {
|
|
|
10
10
|
}, [location.pathname]);
|
|
11
11
|
|
|
12
12
|
return (
|
|
13
|
-
<div className="min-h-
|
|
13
|
+
<div className="flex min-h-full w-full items-center justify-center bg-background px-4 py-12">
|
|
14
14
|
<div className="text-center">
|
|
15
15
|
<h1 className="text-6xl font-bold text-foreground/10 mb-2">404</h1>
|
|
16
16
|
<p className="text-sm text-muted-foreground mb-6">
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
import {
|
|
32
32
|
useGoogleAuthStatus,
|
|
33
33
|
useGoogleAuthUrl,
|
|
34
|
+
useGoogleDesktopAuth,
|
|
34
35
|
useDisconnectGoogle,
|
|
35
36
|
} from "@/hooks/use-google-auth";
|
|
36
37
|
import {
|
|
@@ -45,6 +46,15 @@ export default function Settings() {
|
|
|
45
46
|
const updateSettings = useUpdateSettings();
|
|
46
47
|
const googleStatus = useGoogleAuthStatus();
|
|
47
48
|
const disconnectGoogle = useDisconnectGoogle();
|
|
49
|
+
const {
|
|
50
|
+
isDesktopGoogleAuth,
|
|
51
|
+
isGoogleDesktopAuthPending,
|
|
52
|
+
startDesktopGoogleAuth,
|
|
53
|
+
} = useGoogleDesktopAuth({
|
|
54
|
+
onError: (issue) =>
|
|
55
|
+
toast.error(issue.message || issue.error || "Google sign-in failed"),
|
|
56
|
+
onSuccess: () => window.location.reload(),
|
|
57
|
+
});
|
|
48
58
|
const zoomStatus = useZoomStatus();
|
|
49
59
|
const connectZoom = useConnectZoom();
|
|
50
60
|
const disconnectZoom = useDisconnectZoom();
|
|
@@ -81,6 +91,12 @@ export default function Settings() {
|
|
|
81
91
|
}
|
|
82
92
|
|
|
83
93
|
function handleConnect() {
|
|
94
|
+
if (isDesktopGoogleAuth) {
|
|
95
|
+
startDesktopGoogleAuth({
|
|
96
|
+
previousAccountCount: googleStatus.data?.accounts?.length ?? 0,
|
|
97
|
+
});
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
84
100
|
setWantAuthUrl(true);
|
|
85
101
|
}
|
|
86
102
|
|
|
@@ -179,7 +195,15 @@ export default function Settings() {
|
|
|
179
195
|
Disconnect
|
|
180
196
|
</Button>
|
|
181
197
|
) : (
|
|
182
|
-
<Button
|
|
198
|
+
<Button
|
|
199
|
+
size="sm"
|
|
200
|
+
onClick={handleConnect}
|
|
201
|
+
disabled={
|
|
202
|
+
authUrl.isLoading ||
|
|
203
|
+
authUrl.isFetching ||
|
|
204
|
+
isGoogleDesktopAuthPending
|
|
205
|
+
}
|
|
206
|
+
>
|
|
183
207
|
<IconExternalLink className="mr-1.5 h-3.5 w-3.5" />
|
|
184
208
|
Connect
|
|
185
209
|
</Button>
|