@agent-native/core 0.91.0 → 0.91.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/blueprints/action/crud.md +1 -1
- package/blueprints/channel/discord.md +1 -1
- package/blueprints/provider/stripe.md +1 -1
- package/blueprints/sandbox/docker.md +1 -1
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +26 -0
- package/corpus/core/blueprints/action/crud.md +1 -1
- package/corpus/core/blueprints/channel/discord.md +1 -1
- package/corpus/core/blueprints/provider/stripe.md +1 -1
- package/corpus/core/blueprints/sandbox/docker.md +1 -1
- package/corpus/core/package.json +6 -5
- package/corpus/core/src/agent/run-manager.ts +23 -2
- package/corpus/core/src/agent/run-store.ts +2 -2
- package/corpus/core/src/cli/add.ts +1 -1
- package/corpus/core/src/cli/create.ts +8 -3
- package/corpus/core/src/cli/design-connect.ts +53 -9
- package/corpus/core/src/cli/index.ts +5 -4
- package/corpus/core/src/client/extensions/ExtensionSlot.tsx +7 -0
- package/corpus/core/src/client/session-replay.ts +97 -3
- package/corpus/core/src/jobs/scheduler.ts +30 -1
- package/corpus/core/src/localization/default-messages.ts +21 -0
- package/corpus/core/src/templates/default/app/i18n/ar-SA.ts +21 -0
- package/corpus/core/src/templates/default/app/i18n/de-DE.ts +21 -0
- package/corpus/core/src/templates/default/app/i18n/en-US.ts +21 -0
- package/corpus/core/src/templates/default/app/i18n/es-ES.ts +21 -0
- package/corpus/core/src/templates/default/app/i18n/fr-FR.ts +21 -0
- package/corpus/core/src/templates/default/app/i18n/hi-IN.ts +20 -0
- package/corpus/core/src/templates/default/app/i18n/ja-JP.ts +21 -0
- package/corpus/core/src/templates/default/app/i18n/ko-KR.ts +20 -0
- package/corpus/core/src/templates/default/app/i18n/pt-BR.ts +21 -0
- package/corpus/core/src/templates/default/app/i18n/zh-CN.ts +20 -0
- package/corpus/core/src/templates/default/app/i18n/zh-TW.ts +19 -0
- package/corpus/core/src/templates/default/package.json +2 -2
- package/corpus/core/src/templates/headless/package.json +3 -3
- package/corpus/core/src/templates/workspace-core/package.json +6 -6
- package/corpus/core/src/templates/workspace-root/package.json +2 -2
- package/corpus/core/src/triggers/dispatcher.ts +30 -2
- package/corpus/core/src/vite/client.ts +96 -1
- package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +21 -2
- package/corpus/templates/analytics/app/global.css +26 -10
- package/corpus/templates/analytics/app/i18n/zh-TW.ts +2 -0
- package/corpus/templates/analytics/app/i18n-data.ts +25 -0
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +548 -237
- package/corpus/templates/analytics/app/pages/sessions/SessionDevToolsPanel.tsx +285 -294
- package/corpus/templates/analytics/changelog/2026-07-08-console-details-expand-inline-under-each-row-and-the-event-t.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-08-session-replay-clamps-ultra-wide-viewports-while-keeping-the.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-08-session-replay-keeps-a-usable-stage-when-dev-tools-is-open-a.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-08-session-replay-resumes-after-scrubbing-and-expands-console-r.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-08-session-replays-wait-for-full-load-keep-a-normal-viewport-an.md +6 -0
- package/corpus/templates/analytics/package.json +2 -2
- package/corpus/templates/assets/package.json +2 -2
- package/corpus/templates/brain/package.json +2 -2
- package/corpus/templates/calendar/AGENTS.md +10 -0
- package/corpus/templates/calendar/actions/create-event.ts +5 -1
- package/corpus/templates/calendar/actions/event-action-helpers.ts +41 -0
- package/corpus/templates/calendar/actions/get-attendee-timezones.ts +28 -0
- package/corpus/templates/calendar/actions/set-attendee-timezone.ts +68 -0
- package/corpus/templates/calendar/app/components/calendar/EventAttendeesSection.tsx +150 -11
- package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +36 -1
- package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +32 -0
- package/corpus/templates/calendar/app/hooks/use-attendee-timezones.ts +24 -0
- package/corpus/templates/calendar/app/i18n/zh-TW.ts +2 -0
- package/corpus/templates/calendar/app/i18n-data.ts +20 -0
- package/corpus/templates/calendar/app/lib/attendee-local-time.ts +122 -0
- package/corpus/templates/calendar/changelog/2026-07-08-event-details-now-support-extension-widgets-and-you-can-see-.md +6 -0
- package/corpus/templates/calendar/changelog/2026-07-08-when-you-invite-guests-to-a-new-event-you-now-appear-in.md +6 -0
- package/corpus/templates/calendar/package.json +2 -2
- package/corpus/templates/calendar/server/handlers/events.ts +5 -1
- package/corpus/templates/calendar/shared/api.ts +6 -0
- package/corpus/templates/chat/package.json +2 -2
- package/corpus/templates/clips/.agents/skills/meetings/SKILL.md +6 -2
- package/corpus/templates/clips/actions/create-meeting.ts +8 -3
- package/corpus/templates/clips/actions/list-meetings.ts +31 -3
- package/corpus/templates/clips/changelog/2026-07-08-after-finishing-a-desktop-recording-reopening-clips-restor.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-08-clips-now-pops-up-to-take-notes-when-you-join-an-adhoc-zoom-.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-08-desktop-clip-recordings-are-louder-after-capture-mic-only.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-08-meeting-reminders-now-appear-1-minute-before-start-stay-unti.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-08-the-desktop-meetings-panel-has-room-for-its-drop-shadow-and-.md +6 -0
- package/corpus/templates/clips/chrome-extension/package.json +2 -2
- package/corpus/templates/clips/desktop/package.json +2 -2
- package/corpus/templates/clips/desktop/src/app.tsx +91 -34
- package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +15 -5
- package/corpus/templates/clips/desktop/src/lib/meeting-notification-timing.ts +83 -0
- package/corpus/templates/clips/desktop/src/lib/native-recording-warm.ts +21 -0
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +98 -20
- package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +174 -50
- package/corpus/templates/clips/desktop/src/styles.css +128 -53
- package/corpus/templates/clips/desktop/src-tauri/src/adhoc_meetings_watcher.rs +364 -0
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +1 -1
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +5 -0
- package/corpus/templates/clips/desktop/src-tauri/src/meetings_watcher.rs +91 -11
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +69 -19
- package/corpus/templates/clips/desktop/src-tauri/src/native_speech.rs +36 -1
- package/corpus/templates/clips/desktop/src-tauri/src/notifications.rs +62 -10
- package/corpus/templates/clips/desktop/src-tauri/src/silence_detector.rs +1 -24
- package/corpus/templates/clips/desktop/src-tauri/src/util.rs +36 -1
- package/corpus/templates/clips/package.json +2 -2
- package/corpus/templates/content/actions/_content-database-personal-view.ts +80 -0
- package/corpus/templates/content/actions/get-content-database-personal-view.ts +27 -0
- package/corpus/templates/content/actions/update-content-database-personal-view.ts +31 -0
- package/corpus/templates/content/actions/update-content-database-view.ts +3 -0
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +1 -1
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +2584 -546
- package/corpus/templates/content/app/components/editor/database/filter-sort.ts +220 -18
- package/corpus/templates/content/app/hooks/use-content-database.ts +32 -0
- package/corpus/templates/content/app/i18n/zh-TW.ts +16 -0
- package/corpus/templates/content/app/i18n-data.ts +154 -0
- package/corpus/templates/content/changelog/2026-07-08-database-filters-now-support-notion-style-reset-save-for-eve.md +6 -0
- package/corpus/templates/content/changelog/2026-07-08-date-filters-can-now-target-a-between-range.md +6 -0
- package/corpus/templates/content/package.json +2 -2
- package/corpus/templates/content/parity/matrix.md +1 -1
- package/corpus/templates/content/parity/matrix.ts +2 -0
- package/corpus/templates/content/shared/api.ts +24 -0
- package/corpus/templates/design/AGENTS.md +9 -0
- package/corpus/templates/design/actions/request-localhost-write-consent.ts +141 -0
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +13 -19
- package/corpus/templates/design/package.json +2 -2
- package/corpus/templates/design/server/lib/verify-write-grant.ts +4 -2
- package/corpus/templates/dispatch/app/i18n/zh-TW.ts +34 -1
- package/corpus/templates/dispatch/app/i18n-data.ts +387 -10
- package/corpus/templates/dispatch/app/routes/automations.tsx +1 -0
- package/corpus/templates/dispatch/changelog/2026-07-08-added-an-automations-page-to-review-pause-and-create.md +6 -0
- package/corpus/templates/dispatch/changelog/2026-07-08-destinations-now-shows-outbound-delivery-queue-health.md +6 -0
- package/corpus/templates/dispatch/package.json +2 -2
- package/corpus/templates/forms/package.json +2 -2
- package/corpus/templates/macros/package.json +2 -2
- package/corpus/templates/mail/app/hooks/use-emails.ts +56 -44
- package/corpus/templates/mail/app/lib/gmail-mutation-queue.ts +325 -0
- package/corpus/templates/mail/changelog/2026-07-08-rapid-archive-and-mark-read-now-batch-gmail-updates-so-the-i.md +6 -0
- package/corpus/templates/mail/package.json +2 -2
- package/corpus/templates/plan/package.json +2 -2
- package/corpus/templates/slides/app/components/editor/ImageDropPromptPopover.tsx +46 -29
- package/corpus/templates/slides/app/context/DeckContext.tsx +45 -18
- package/corpus/templates/slides/app/i18n/ar-SA.ts +4 -0
- package/corpus/templates/slides/app/i18n/de-DE.ts +4 -0
- package/corpus/templates/slides/app/i18n/en-US.ts +4 -0
- package/corpus/templates/slides/app/i18n/es-ES.ts +4 -0
- package/corpus/templates/slides/app/i18n/fr-FR.ts +4 -0
- package/corpus/templates/slides/app/i18n/hi-IN.ts +4 -0
- package/corpus/templates/slides/app/i18n/ja-JP.ts +4 -0
- package/corpus/templates/slides/app/i18n/ko-KR.ts +4 -0
- package/corpus/templates/slides/app/i18n/pt-BR.ts +4 -0
- package/corpus/templates/slides/app/i18n/zh-CN.ts +4 -0
- package/corpus/templates/slides/app/i18n/zh-TW.ts +4 -0
- package/corpus/templates/slides/app/lib/image-drop-to-agent.ts +115 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +22 -13
- package/corpus/templates/slides/changelog/2026-07-08-dropping-an-image-onto-a-slide-and-sending-it-to-the-agent-w.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-08-undo-works-after-the-agent-edits-a-slide-so-you-can-reverse.md +6 -0
- package/corpus/templates/slides/package.json +2 -2
- package/dist/agent/run-manager.d.ts.map +1 -1
- package/dist/agent/run-manager.js +23 -2
- package/dist/agent/run-manager.js.map +1 -1
- package/dist/agent/run-store.js +2 -2
- package/dist/agent/run-store.js.map +1 -1
- package/dist/catalog.json +2 -2
- package/dist/cli/add.js +1 -1
- package/dist/cli/add.js.map +1 -1
- package/dist/cli/create.d.ts.map +1 -1
- package/dist/cli/create.js +8 -3
- package/dist/cli/create.js.map +1 -1
- package/dist/cli/design-connect.d.ts.map +1 -1
- package/dist/cli/design-connect.js +40 -10
- package/dist/cli/design-connect.js.map +1 -1
- package/dist/cli/index.js +5 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/client/extensions/ExtensionSlot.js +6 -0
- package/dist/client/extensions/ExtensionSlot.js.map +1 -1
- package/dist/client/session-replay.d.ts +8 -0
- package/dist/client/session-replay.d.ts.map +1 -1
- package/dist/client/session-replay.js +80 -3
- package/dist/client/session-replay.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/jobs/scheduler.js +26 -1
- package/dist/jobs/scheduler.js.map +1 -1
- package/dist/localization/default-messages.d.ts +19 -0
- package/dist/localization/default-messages.d.ts.map +1 -1
- package/dist/localization/default-messages.js +19 -0
- package/dist/localization/default-messages.js.map +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +1 -1
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/templates/default/app/i18n/ar-SA.ts +21 -0
- package/dist/templates/default/app/i18n/de-DE.ts +21 -0
- package/dist/templates/default/app/i18n/en-US.ts +21 -0
- package/dist/templates/default/app/i18n/es-ES.ts +21 -0
- package/dist/templates/default/app/i18n/fr-FR.ts +21 -0
- package/dist/templates/default/app/i18n/hi-IN.ts +20 -0
- package/dist/templates/default/app/i18n/ja-JP.ts +21 -0
- package/dist/templates/default/app/i18n/ko-KR.ts +20 -0
- package/dist/templates/default/app/i18n/pt-BR.ts +21 -0
- package/dist/templates/default/app/i18n/zh-CN.ts +20 -0
- package/dist/templates/default/app/i18n/zh-TW.ts +19 -0
- package/dist/templates/default/package.json +2 -2
- package/dist/templates/headless/package.json +3 -3
- package/dist/templates/workspace-core/package.json +6 -6
- package/dist/templates/workspace-root/package.json +2 -2
- package/dist/triggers/dispatcher.js +27 -2
- package/dist/triggers/dispatcher.js.map +1 -1
- package/dist/vite/client.d.ts.map +1 -1
- package/dist/vite/client.js +70 -1
- package/dist/vite/client.js.map +1 -1
- package/package.json +7 -6
- package/src/templates/default/app/i18n/ar-SA.ts +21 -0
- package/src/templates/default/app/i18n/de-DE.ts +21 -0
- package/src/templates/default/app/i18n/en-US.ts +21 -0
- package/src/templates/default/app/i18n/es-ES.ts +21 -0
- package/src/templates/default/app/i18n/fr-FR.ts +21 -0
- package/src/templates/default/app/i18n/hi-IN.ts +20 -0
- package/src/templates/default/app/i18n/ja-JP.ts +21 -0
- package/src/templates/default/app/i18n/ko-KR.ts +20 -0
- package/src/templates/default/app/i18n/pt-BR.ts +21 -0
- package/src/templates/default/app/i18n/zh-CN.ts +20 -0
- package/src/templates/default/app/i18n/zh-TW.ts +19 -0
- package/src/templates/default/package.json +2 -2
- package/src/templates/headless/package.json +3 -3
- package/src/templates/workspace-core/package.json +6 -6
- package/src/templates/workspace-root/package.json +2 -2
|
@@ -13,10 +13,11 @@ import {
|
|
|
13
13
|
useMutation,
|
|
14
14
|
useQueryClient,
|
|
15
15
|
} from "@tanstack/react-query";
|
|
16
|
-
import { useMemo
|
|
16
|
+
import { useMemo } from "react";
|
|
17
17
|
import { toast } from "sonner";
|
|
18
18
|
|
|
19
19
|
import { useAccountFilter } from "@/hooks/use-account-filter";
|
|
20
|
+
import { gmailMutationQueue } from "@/lib/gmail-mutation-queue";
|
|
20
21
|
import { TAB_ID } from "@/lib/tab-id";
|
|
21
22
|
import {
|
|
22
23
|
useThreadCache,
|
|
@@ -636,14 +637,21 @@ export function useMarkRead() {
|
|
|
636
637
|
id,
|
|
637
638
|
isRead,
|
|
638
639
|
accountEmail,
|
|
640
|
+
threadId,
|
|
639
641
|
}: {
|
|
640
642
|
id: string;
|
|
641
643
|
isRead: boolean;
|
|
642
644
|
accountEmail?: string;
|
|
645
|
+
threadId?: string;
|
|
643
646
|
}) =>
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
+
// Buffer + batch via Gmail messages.batchModify so rapid open/mark-read
|
|
648
|
+
// (and e-e-e archive) stay snappy without burning quota per keypress.
|
|
649
|
+
gmailMutationQueue.enqueue("mark-read", {
|
|
650
|
+
id,
|
|
651
|
+
threadId,
|
|
652
|
+
accountEmail,
|
|
653
|
+
flag: isRead,
|
|
654
|
+
}),
|
|
647
655
|
onMutate: async ({ id, isRead }) => {
|
|
648
656
|
await qc.cancelQueries({ queryKey: ["emails"] });
|
|
649
657
|
const previous = qc.getQueriesData<InfiniteEmails>({
|
|
@@ -667,22 +675,9 @@ export function useMarkRead() {
|
|
|
667
675
|
|
|
668
676
|
export function useMarkThreadRead() {
|
|
669
677
|
const qc = useQueryClient();
|
|
670
|
-
// Per-thread pending entries — using a Map so concurrent mutations for different
|
|
671
|
-
// threads don't overwrite each other's pending entries.
|
|
672
|
-
const pendingByThread = useRef(
|
|
673
|
-
new Map<string, { id: string; accountEmail?: string }[]>(),
|
|
674
|
-
);
|
|
675
678
|
return useMutation({
|
|
676
|
-
mutationFn:
|
|
677
|
-
|
|
678
|
-
pendingByThread.current.delete(threadId);
|
|
679
|
-
if (entries.length > 0) {
|
|
680
|
-
await callAction("mark-thread-read", {
|
|
681
|
-
threadId,
|
|
682
|
-
accountEmail: entries[0]?.accountEmail,
|
|
683
|
-
}).then(assertActionSuccess);
|
|
684
|
-
}
|
|
685
|
-
},
|
|
679
|
+
mutationFn: (threadId: string) =>
|
|
680
|
+
callAction("mark-thread-read", { threadId }).then(assertActionSuccess),
|
|
686
681
|
onMutate: async (threadId) => {
|
|
687
682
|
await qc.cancelQueries({ queryKey: ["emails"] });
|
|
688
683
|
const previous = qc.getQueriesData<InfiniteEmails>({
|
|
@@ -691,11 +686,9 @@ export function useMarkThreadRead() {
|
|
|
691
686
|
// Capture unread entries BEFORE optimistic update
|
|
692
687
|
const allEmails =
|
|
693
688
|
previous.flatMap(([, data]) => flattenInfiniteEmails(data)) ?? [];
|
|
694
|
-
const
|
|
689
|
+
const unreadIds = allEmails
|
|
695
690
|
.filter((e) => (e.threadId || e.id) === threadId && !e.isRead)
|
|
696
|
-
.map((e) =>
|
|
697
|
-
pendingByThread.current.set(threadId, unreadEntries);
|
|
698
|
-
const unreadIds = unreadEntries.map((e) => e.id);
|
|
691
|
+
.map((e) => e.id);
|
|
699
692
|
const previousThread = getCachedThread(threadId);
|
|
700
693
|
// Set overrides so refetches don't revert read state
|
|
701
694
|
for (const id of unreadIds) {
|
|
@@ -737,17 +730,19 @@ export function useToggleStar() {
|
|
|
737
730
|
id,
|
|
738
731
|
isStarred,
|
|
739
732
|
accountEmail,
|
|
733
|
+
threadId,
|
|
740
734
|
}: {
|
|
741
735
|
id: string;
|
|
742
736
|
isStarred: boolean;
|
|
743
737
|
accountEmail?: string;
|
|
744
738
|
threadId?: string;
|
|
745
739
|
}) =>
|
|
746
|
-
|
|
740
|
+
gmailMutationQueue.enqueue("star", {
|
|
747
741
|
id,
|
|
748
|
-
|
|
742
|
+
threadId,
|
|
749
743
|
accountEmail,
|
|
750
|
-
|
|
744
|
+
flag: isStarred,
|
|
745
|
+
}),
|
|
751
746
|
onMutate: async ({ id, isStarred, threadId }) => {
|
|
752
747
|
await qc.cancelQueries({ queryKey: ["emails"] });
|
|
753
748
|
const previous = qc.getQueriesData<InfiniteEmails>({
|
|
@@ -802,12 +797,12 @@ export function useArchiveEmail() {
|
|
|
802
797
|
removeLabel?: string;
|
|
803
798
|
threadId?: string;
|
|
804
799
|
}) =>
|
|
805
|
-
|
|
800
|
+
gmailMutationQueue.enqueue("archive", {
|
|
806
801
|
id,
|
|
807
802
|
accountEmail,
|
|
808
803
|
removeLabel,
|
|
809
804
|
threadId,
|
|
810
|
-
})
|
|
805
|
+
}),
|
|
811
806
|
onMutate: async ({
|
|
812
807
|
id,
|
|
813
808
|
threadId: hintedThreadId,
|
|
@@ -848,8 +843,13 @@ export function useArchiveEmail() {
|
|
|
848
843
|
export function useUnarchiveEmail() {
|
|
849
844
|
const qc = useQueryClient();
|
|
850
845
|
return useMutation({
|
|
851
|
-
mutationFn: (id: string) =>
|
|
852
|
-
|
|
846
|
+
mutationFn: (id: string) => {
|
|
847
|
+
// Undo often lands inside the debounce window — drop the pending
|
|
848
|
+
// archive so we never send a modify we immediately reverse.
|
|
849
|
+
const cancelled = gmailMutationQueue.cancel("archive", id);
|
|
850
|
+
if (cancelled) return Promise.resolve("cancelled-pending-archive");
|
|
851
|
+
return callAction("unarchive-email", { id }).then(assertActionSuccess);
|
|
852
|
+
},
|
|
853
853
|
onSuccess: () => qc.invalidateQueries({ queryKey: ["emails"] }),
|
|
854
854
|
});
|
|
855
855
|
}
|
|
@@ -924,10 +924,14 @@ export function useBulkArchiveEmails() {
|
|
|
924
924
|
const t = useT();
|
|
925
925
|
return useMutation({
|
|
926
926
|
mutationFn: ({ targets, removeLabel }: BulkArchiveVars) =>
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
927
|
+
Promise.all(
|
|
928
|
+
targets.map((target) =>
|
|
929
|
+
gmailMutationQueue.enqueue("archive", {
|
|
930
|
+
...target,
|
|
931
|
+
removeLabel,
|
|
932
|
+
}),
|
|
933
|
+
),
|
|
934
|
+
).then(() => `Queued archive for ${targets.length} email(s)`),
|
|
931
935
|
onMutate: async ({ targets }: BulkArchiveVars) => {
|
|
932
936
|
await qc.cancelQueries({ queryKey: ["emails"] });
|
|
933
937
|
const previous = qc.getQueriesData<InfiniteEmails>({
|
|
@@ -1008,7 +1012,7 @@ export function useBulkTrashEmails() {
|
|
|
1008
1012
|
});
|
|
1009
1013
|
}
|
|
1010
1014
|
|
|
1011
|
-
/** Bulk star/unstar:
|
|
1015
|
+
/** Bulk star/unstar: queued + batched Gmail modify, one optimistic override. */
|
|
1012
1016
|
export function useBulkToggleStar() {
|
|
1013
1017
|
const qc = useQueryClient();
|
|
1014
1018
|
return useMutation({
|
|
@@ -1019,10 +1023,14 @@ export function useBulkToggleStar() {
|
|
|
1019
1023
|
targets: BulkEmailTarget[];
|
|
1020
1024
|
isStarred: boolean;
|
|
1021
1025
|
}) =>
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
+
Promise.all(
|
|
1027
|
+
targets.map((target) =>
|
|
1028
|
+
gmailMutationQueue.enqueue("star", {
|
|
1029
|
+
...target,
|
|
1030
|
+
flag: isStarred,
|
|
1031
|
+
}),
|
|
1032
|
+
),
|
|
1033
|
+
).then(() => `Queued star for ${targets.length} email(s)`),
|
|
1026
1034
|
onMutate: async ({ targets, isStarred }) => {
|
|
1027
1035
|
await qc.cancelQueries({ queryKey: ["emails"] });
|
|
1028
1036
|
const previous = qc.getQueriesData<InfiniteEmails>({
|
|
@@ -1045,7 +1053,7 @@ export function useBulkToggleStar() {
|
|
|
1045
1053
|
});
|
|
1046
1054
|
}
|
|
1047
1055
|
|
|
1048
|
-
/** Bulk mark read/unread:
|
|
1056
|
+
/** Bulk mark read/unread: queued + batched Gmail modify, one optimistic override. */
|
|
1049
1057
|
export function useBulkMarkRead() {
|
|
1050
1058
|
const qc = useQueryClient();
|
|
1051
1059
|
return useMutation({
|
|
@@ -1056,10 +1064,14 @@ export function useBulkMarkRead() {
|
|
|
1056
1064
|
targets: BulkEmailTarget[];
|
|
1057
1065
|
isRead: boolean;
|
|
1058
1066
|
}) =>
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1067
|
+
Promise.all(
|
|
1068
|
+
targets.map((target) =>
|
|
1069
|
+
gmailMutationQueue.enqueue("mark-read", {
|
|
1070
|
+
...target,
|
|
1071
|
+
flag: isRead,
|
|
1072
|
+
}),
|
|
1073
|
+
),
|
|
1074
|
+
).then(() => `Queued mark-read for ${targets.length} email(s)`),
|
|
1063
1075
|
onMutate: async ({ targets, isRead }) => {
|
|
1064
1076
|
await qc.cancelQueries({ queryKey: ["emails"] });
|
|
1065
1077
|
const previous = qc.getQueriesData<InfiniteEmails>({
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Debounced client-side buffer for Gmail state mutations.
|
|
3
|
+
*
|
|
4
|
+
* Rapid archive / mark-read / star must feel instant (optimistic UI), but
|
|
5
|
+
* each keypress hitting Gmail as its own modify burns quota. We keep the
|
|
6
|
+
* React Query optimistic path, then coalesce outbound calls into the
|
|
7
|
+
* existing bulk actions (one messages.batchModify per account).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { callAction } from "@agent-native/core/client";
|
|
11
|
+
|
|
12
|
+
export type GmailMutationKind = "archive" | "mark-read" | "star";
|
|
13
|
+
|
|
14
|
+
export interface GmailMutationTarget {
|
|
15
|
+
id: string;
|
|
16
|
+
threadId?: string;
|
|
17
|
+
accountEmail?: string;
|
|
18
|
+
/** Archive-from-label view: also remove this label. */
|
|
19
|
+
removeLabel?: string;
|
|
20
|
+
/** mark-read: true = read, false = unread. star: true = star. */
|
|
21
|
+
flag?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface QueuedMutation extends GmailMutationTarget {
|
|
25
|
+
kind: GmailMutationKind;
|
|
26
|
+
resolve: () => void;
|
|
27
|
+
reject: (error: unknown) => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const DEFAULT_DEBOUNCE_MS = 280;
|
|
31
|
+
const MAX_WAIT_MS = 1200;
|
|
32
|
+
|
|
33
|
+
type FlushListener = (info: {
|
|
34
|
+
kind: GmailMutationKind;
|
|
35
|
+
count: number;
|
|
36
|
+
error?: unknown;
|
|
37
|
+
}) => void;
|
|
38
|
+
|
|
39
|
+
function targetKey(
|
|
40
|
+
kind: GmailMutationKind,
|
|
41
|
+
target: GmailMutationTarget,
|
|
42
|
+
): string {
|
|
43
|
+
// Coalesce by message id + kind + removeLabel so re-pressing `e` on the
|
|
44
|
+
// same thread replaces the pending op instead of stacking duplicates.
|
|
45
|
+
return `${kind}:${target.id}:${target.removeLabel ?? ""}:${target.flag ?? ""}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function bulkArgs(targets: GmailMutationTarget[]) {
|
|
49
|
+
return {
|
|
50
|
+
id: targets.map((t) => t.id).join(","),
|
|
51
|
+
threadIds: targets.map((t) => t.threadId ?? "").join(","),
|
|
52
|
+
accountEmails: targets.map((t) => t.accountEmail ?? "").join(","),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function assertActionSuccess<T>(result: T): T {
|
|
57
|
+
if (
|
|
58
|
+
typeof result === "string" &&
|
|
59
|
+
(result.startsWith("Error:") || /failed/i.test(result.slice(0, 40)))
|
|
60
|
+
) {
|
|
61
|
+
throw new Error(result);
|
|
62
|
+
}
|
|
63
|
+
if (
|
|
64
|
+
result &&
|
|
65
|
+
typeof result === "object" &&
|
|
66
|
+
"ok" in result &&
|
|
67
|
+
(result as { ok?: boolean }).ok === false
|
|
68
|
+
) {
|
|
69
|
+
const message =
|
|
70
|
+
"error" in result &&
|
|
71
|
+
typeof (result as { error?: unknown }).error === "string"
|
|
72
|
+
? (result as { error: string }).error
|
|
73
|
+
: "Action failed";
|
|
74
|
+
throw new Error(message);
|
|
75
|
+
}
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
class GmailMutationQueue {
|
|
80
|
+
private pending = new Map<string, QueuedMutation>();
|
|
81
|
+
private debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
82
|
+
private maxWaitTimer: ReturnType<typeof setTimeout> | null = null;
|
|
83
|
+
private flushing: Promise<void> | null = null;
|
|
84
|
+
private firstEnqueueAt = 0;
|
|
85
|
+
private debounceMs = DEFAULT_DEBOUNCE_MS;
|
|
86
|
+
private listeners = new Set<FlushListener>();
|
|
87
|
+
private installedUnload = false;
|
|
88
|
+
|
|
89
|
+
/** Test-only: override debounce. */
|
|
90
|
+
setDebounceMs(ms: number) {
|
|
91
|
+
this.debounceMs = ms;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
onFlush(listener: FlushListener): () => void {
|
|
95
|
+
this.listeners.add(listener);
|
|
96
|
+
return () => this.listeners.delete(listener);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Pending op count — useful in tests. */
|
|
100
|
+
size(): number {
|
|
101
|
+
return this.pending.size;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
enqueue(kind: GmailMutationKind, target: GmailMutationTarget): Promise<void> {
|
|
105
|
+
this.ensureUnloadHook();
|
|
106
|
+
const key = targetKey(kind, target);
|
|
107
|
+
return new Promise<void>((resolve, reject) => {
|
|
108
|
+
const existing = this.pending.get(key);
|
|
109
|
+
if (existing) {
|
|
110
|
+
// Drop the superseded waiter as success — the newer op owns the flush.
|
|
111
|
+
existing.resolve();
|
|
112
|
+
}
|
|
113
|
+
this.pending.set(key, { kind, ...target, resolve, reject });
|
|
114
|
+
if (!this.firstEnqueueAt) this.firstEnqueueAt = Date.now();
|
|
115
|
+
this.scheduleFlush();
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Drop a pending op without sending it (e.g. user undid an archive before
|
|
121
|
+
* the debounce window closed). Resolves waiters so callers don't hang.
|
|
122
|
+
*/
|
|
123
|
+
cancel(kind: GmailMutationKind, id: string, removeLabel?: string): boolean {
|
|
124
|
+
let cancelled = false;
|
|
125
|
+
for (const [key, op] of this.pending) {
|
|
126
|
+
if (
|
|
127
|
+
op.kind === kind &&
|
|
128
|
+
op.id === id &&
|
|
129
|
+
(removeLabel === undefined || op.removeLabel === removeLabel)
|
|
130
|
+
) {
|
|
131
|
+
this.pending.delete(key);
|
|
132
|
+
op.resolve();
|
|
133
|
+
cancelled = true;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (this.pending.size === 0) this.clearTimers();
|
|
137
|
+
return cancelled;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Force-send everything now. Safe to call while a flush is already running. */
|
|
141
|
+
async flush(): Promise<void> {
|
|
142
|
+
this.clearTimers();
|
|
143
|
+
if (this.flushing) {
|
|
144
|
+
await this.flushing;
|
|
145
|
+
if (this.pending.size > 0) await this.flush();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (this.pending.size === 0) return;
|
|
149
|
+
|
|
150
|
+
const batch = [...this.pending.values()];
|
|
151
|
+
this.pending.clear();
|
|
152
|
+
this.firstEnqueueAt = 0;
|
|
153
|
+
|
|
154
|
+
this.flushing = this.runFlush(batch).finally(() => {
|
|
155
|
+
this.flushing = null;
|
|
156
|
+
});
|
|
157
|
+
await this.flushing;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private scheduleFlush() {
|
|
161
|
+
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
162
|
+
this.debounceTimer = setTimeout(() => {
|
|
163
|
+
void this.flush();
|
|
164
|
+
}, this.debounceMs);
|
|
165
|
+
|
|
166
|
+
if (!this.maxWaitTimer && this.firstEnqueueAt) {
|
|
167
|
+
const remaining = Math.max(
|
|
168
|
+
0,
|
|
169
|
+
MAX_WAIT_MS - (Date.now() - this.firstEnqueueAt),
|
|
170
|
+
);
|
|
171
|
+
this.maxWaitTimer = setTimeout(() => {
|
|
172
|
+
void this.flush();
|
|
173
|
+
}, remaining);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private clearTimers() {
|
|
178
|
+
if (this.debounceTimer) {
|
|
179
|
+
clearTimeout(this.debounceTimer);
|
|
180
|
+
this.debounceTimer = null;
|
|
181
|
+
}
|
|
182
|
+
if (this.maxWaitTimer) {
|
|
183
|
+
clearTimeout(this.maxWaitTimer);
|
|
184
|
+
this.maxWaitTimer = null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private async runFlush(batch: QueuedMutation[]): Promise<void> {
|
|
189
|
+
const byKind = new Map<GmailMutationKind, QueuedMutation[]>();
|
|
190
|
+
for (const op of batch) {
|
|
191
|
+
const list = byKind.get(op.kind) ?? [];
|
|
192
|
+
list.push(op);
|
|
193
|
+
byKind.set(op.kind, list);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
for (const [kind, ops] of byKind) {
|
|
197
|
+
// Group archives that share the same removeLabel so label-view archives
|
|
198
|
+
// stay correct without blocking the default bulk INBOX path.
|
|
199
|
+
if (kind === "archive") {
|
|
200
|
+
const byLabel = new Map<string, QueuedMutation[]>();
|
|
201
|
+
for (const op of ops) {
|
|
202
|
+
const labelKey = op.removeLabel ?? "";
|
|
203
|
+
const list = byLabel.get(labelKey) ?? [];
|
|
204
|
+
list.push(op);
|
|
205
|
+
byLabel.set(labelKey, list);
|
|
206
|
+
}
|
|
207
|
+
for (const [, group] of byLabel) {
|
|
208
|
+
await this.flushArchive(group);
|
|
209
|
+
}
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (kind === "mark-read") {
|
|
214
|
+
const byFlag = new Map<boolean, QueuedMutation[]>();
|
|
215
|
+
for (const op of ops) {
|
|
216
|
+
const flag = op.flag !== false;
|
|
217
|
+
const list = byFlag.get(flag) ?? [];
|
|
218
|
+
list.push(op);
|
|
219
|
+
byFlag.set(flag, list);
|
|
220
|
+
}
|
|
221
|
+
for (const [isRead, group] of byFlag) {
|
|
222
|
+
await this.flushMarkRead(group, isRead);
|
|
223
|
+
}
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (kind === "star") {
|
|
228
|
+
const byFlag = new Map<boolean, QueuedMutation[]>();
|
|
229
|
+
for (const op of ops) {
|
|
230
|
+
const flag = op.flag !== false;
|
|
231
|
+
const list = byFlag.get(flag) ?? [];
|
|
232
|
+
list.push(op);
|
|
233
|
+
byFlag.set(flag, list);
|
|
234
|
+
}
|
|
235
|
+
for (const [isStarred, group] of byFlag) {
|
|
236
|
+
await this.flushStar(group, isStarred);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private async flushArchive(ops: QueuedMutation[]): Promise<void> {
|
|
243
|
+
try {
|
|
244
|
+
await callAction("archive-email", {
|
|
245
|
+
...bulkArgs(ops),
|
|
246
|
+
removeLabel: ops[0]?.removeLabel,
|
|
247
|
+
}).then(assertActionSuccess);
|
|
248
|
+
for (const op of ops) op.resolve();
|
|
249
|
+
this.emit({ kind: "archive", count: ops.length });
|
|
250
|
+
} catch (error) {
|
|
251
|
+
for (const op of ops) op.reject(error);
|
|
252
|
+
this.emit({ kind: "archive", count: ops.length, error });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private async flushMarkRead(
|
|
257
|
+
ops: QueuedMutation[],
|
|
258
|
+
isRead: boolean,
|
|
259
|
+
): Promise<void> {
|
|
260
|
+
try {
|
|
261
|
+
await callAction("mark-read", {
|
|
262
|
+
...bulkArgs(ops),
|
|
263
|
+
unread: !isRead,
|
|
264
|
+
}).then(assertActionSuccess);
|
|
265
|
+
for (const op of ops) op.resolve();
|
|
266
|
+
this.emit({ kind: "mark-read", count: ops.length });
|
|
267
|
+
} catch (error) {
|
|
268
|
+
for (const op of ops) op.reject(error);
|
|
269
|
+
this.emit({ kind: "mark-read", count: ops.length, error });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private async flushStar(
|
|
274
|
+
ops: QueuedMutation[],
|
|
275
|
+
isStarred: boolean,
|
|
276
|
+
): Promise<void> {
|
|
277
|
+
try {
|
|
278
|
+
await callAction("star-email", {
|
|
279
|
+
...bulkArgs(ops),
|
|
280
|
+
unstar: !isStarred,
|
|
281
|
+
}).then(assertActionSuccess);
|
|
282
|
+
for (const op of ops) op.resolve();
|
|
283
|
+
this.emit({ kind: "star", count: ops.length });
|
|
284
|
+
} catch (error) {
|
|
285
|
+
for (const op of ops) op.reject(error);
|
|
286
|
+
this.emit({ kind: "star", count: ops.length, error });
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private emit(info: {
|
|
291
|
+
kind: GmailMutationKind;
|
|
292
|
+
count: number;
|
|
293
|
+
error?: unknown;
|
|
294
|
+
}) {
|
|
295
|
+
for (const listener of this.listeners) {
|
|
296
|
+
try {
|
|
297
|
+
listener(info);
|
|
298
|
+
} catch {
|
|
299
|
+
// ignore listener errors
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
private ensureUnloadHook() {
|
|
305
|
+
if (this.installedUnload || typeof window === "undefined") return;
|
|
306
|
+
this.installedUnload = true;
|
|
307
|
+
const flushSync = () => {
|
|
308
|
+
// Best-effort: kick flush; can't reliably await on unload.
|
|
309
|
+
void this.flush();
|
|
310
|
+
};
|
|
311
|
+
window.addEventListener("pagehide", flushSync);
|
|
312
|
+
window.addEventListener("beforeunload", flushSync);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Test helper: wipe pending state. */
|
|
316
|
+
resetForTests() {
|
|
317
|
+
this.clearTimers();
|
|
318
|
+
for (const op of this.pending.values()) op.resolve();
|
|
319
|
+
this.pending.clear();
|
|
320
|
+
this.firstEnqueueAt = 0;
|
|
321
|
+
this.flushing = null;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export const gmailMutationQueue = new GmailMutationQueue();
|
|
@@ -80,7 +80,6 @@
|
|
|
80
80
|
"@types/node": "^24.2.1",
|
|
81
81
|
"@types/react": "catalog:",
|
|
82
82
|
"@types/react-dom": "catalog:",
|
|
83
|
-
"@typescript/native-preview": "catalog:",
|
|
84
83
|
"@vitejs/plugin-react-swc": "^4.0.0",
|
|
85
84
|
"class-variance-authority": "^0.7.1",
|
|
86
85
|
"clsx": "^2.1.1",
|
|
@@ -103,7 +102,8 @@
|
|
|
103
102
|
"tailwindcss": "catalog:",
|
|
104
103
|
"tiptap-markdown": "^0.9.0",
|
|
105
104
|
"tsx": "^4.20.3",
|
|
106
|
-
"typescript": "
|
|
105
|
+
"typescript": "catalog:",
|
|
106
|
+
"typescript-7": "catalog:",
|
|
107
107
|
"vaul": "^1.1.2",
|
|
108
108
|
"vite": "catalog:",
|
|
109
109
|
"vitest": "catalog:"
|
|
@@ -81,7 +81,6 @@
|
|
|
81
81
|
"@types/node": "^24.2.1",
|
|
82
82
|
"@types/react": "catalog:",
|
|
83
83
|
"@types/react-dom": "catalog:",
|
|
84
|
-
"@typescript/native-preview": "catalog:",
|
|
85
84
|
"@xterm/addon-fit": "^0.11.0",
|
|
86
85
|
"@xterm/addon-web-links": "^0.12.0",
|
|
87
86
|
"@xterm/xterm": "^6.0.0",
|
|
@@ -103,7 +102,8 @@
|
|
|
103
102
|
"tailwind-merge": "^3.5.0",
|
|
104
103
|
"tailwindcss": "catalog:",
|
|
105
104
|
"tsx": "^4.20.3",
|
|
106
|
-
"typescript": "
|
|
105
|
+
"typescript": "catalog:",
|
|
106
|
+
"typescript-7": "catalog:",
|
|
107
107
|
"vaul": "^1.1.2",
|
|
108
108
|
"vite": "catalog:",
|
|
109
109
|
"vitest": "catalog:"
|
|
@@ -6,6 +6,12 @@ import { createPortal } from "react-dom";
|
|
|
6
6
|
import { Button } from "@/components/ui/button";
|
|
7
7
|
import { Textarea } from "@/components/ui/textarea";
|
|
8
8
|
import { toast } from "@/hooks/use-toast";
|
|
9
|
+
import {
|
|
10
|
+
buildImageDropAgentPayload,
|
|
11
|
+
readFileAsDataUrl,
|
|
12
|
+
type HostedImageUploadResult,
|
|
13
|
+
} from "@/lib/image-drop-to-agent";
|
|
14
|
+
import { parseUploadResponse } from "@/lib/upload-response";
|
|
9
15
|
|
|
10
16
|
const POPOVER_WIDTH = 360;
|
|
11
17
|
const POPOVER_MARGIN = 12;
|
|
@@ -24,8 +30,12 @@ interface ImageDropPromptPopoverProps {
|
|
|
24
30
|
* Popover shown after a user drops an image somewhere on the slides editor
|
|
25
31
|
* that doesn't have a clear target (i.e. not on an image placeholder or
|
|
26
32
|
* existing `<img>`). The popover previews the image, lets the user describe
|
|
27
|
-
* what to do with it, and hands the task off to the agent chat
|
|
28
|
-
*
|
|
33
|
+
* what to do with it, and hands the task off to the agent chat.
|
|
34
|
+
*
|
|
35
|
+
* Prefers a hosted CDN URL via `/api/assets/upload` when a file-upload
|
|
36
|
+
* provider (Builder.io / S3 / …) is configured. When nothing is configured,
|
|
37
|
+
* falls back to an inline data-URL attachment so the drop still reaches the
|
|
38
|
+
* agent instead of toasting a 503.
|
|
29
39
|
*
|
|
30
40
|
* Why this exists: dropping image files onto an unclear target previously did
|
|
31
41
|
* one of two unhelpful things — opened the file in a new browser tab (when the
|
|
@@ -113,44 +123,51 @@ export default function ImageDropPromptPopover({
|
|
|
113
123
|
try {
|
|
114
124
|
const form = new FormData();
|
|
115
125
|
form.append("file", file);
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
126
|
+
// Prefer the hosted provider chain. When none is configured the route
|
|
127
|
+
// returns 503 — fall back to an inline data URL so the agent still gets
|
|
128
|
+
// the image (chat already accepts `images` data URLs).
|
|
119
129
|
const res = await fetch(`${appBasePath()}/api/assets/upload`, {
|
|
120
130
|
method: "POST",
|
|
121
131
|
body: form,
|
|
122
132
|
});
|
|
123
|
-
const data =
|
|
133
|
+
const data = await parseUploadResponse<{
|
|
124
134
|
url?: string;
|
|
125
135
|
error?: string;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
136
|
+
}>(res, t("raw.imageUploadGenericError"));
|
|
137
|
+
const upload: HostedImageUploadResult = {
|
|
138
|
+
ok: res.ok && !!data.url,
|
|
139
|
+
status: res.status,
|
|
140
|
+
url: data.url,
|
|
141
|
+
error: data.error,
|
|
142
|
+
};
|
|
133
143
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
? userIntent
|
|
138
|
-
: "Use this image on the current slide.";
|
|
139
|
-
const lines = [intentLine];
|
|
140
|
-
if (contextHint && contextHint.trim().length > 0) {
|
|
141
|
-
lines.push(contextHint.trim());
|
|
144
|
+
let dataUrl: string | undefined;
|
|
145
|
+
if (!upload.ok) {
|
|
146
|
+
dataUrl = await readFileAsDataUrl(file);
|
|
142
147
|
}
|
|
143
|
-
lines.push(
|
|
144
|
-
`Image URL (already uploaded): ${data.url}`,
|
|
145
|
-
`Filename: ${file.name}`,
|
|
146
|
-
);
|
|
147
148
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
149
|
+
const payload = buildImageDropAgentPayload({
|
|
150
|
+
intent: prompt,
|
|
151
|
+
contextHint,
|
|
152
|
+
filename: file.name,
|
|
153
|
+
upload,
|
|
154
|
+
dataUrl,
|
|
152
155
|
});
|
|
153
156
|
|
|
157
|
+
if (payload.kind === "hosted") {
|
|
158
|
+
sendToAgentChat({
|
|
159
|
+
message: payload.message,
|
|
160
|
+
submit: true,
|
|
161
|
+
referenceImagePaths: payload.referenceImagePaths,
|
|
162
|
+
});
|
|
163
|
+
} else {
|
|
164
|
+
sendToAgentChat({
|
|
165
|
+
message: payload.message,
|
|
166
|
+
submit: true,
|
|
167
|
+
images: payload.images,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
154
171
|
onClose();
|
|
155
172
|
toast({
|
|
156
173
|
title: t("raw.sentToAgent"),
|