@agent-native/core 0.192.0 → 0.192.1-nightly-20260926144920
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/calendar/actions/cancel-booking.ts +12 -4
- package/corpus/templates/calendar/actions/list-bookings.ts +23 -2
- package/corpus/templates/calendar/actions/list-events.ts +42 -29
- package/corpus/templates/calendar/actions/update-booking-link.ts +27 -1
- package/corpus/templates/calendar/app/hooks/use-bookings.ts +9 -2
- package/corpus/templates/calendar/app/i18n/en-US.ts +8 -0
- package/corpus/templates/calendar/app/lib/booking-sorting.ts +7 -0
- package/corpus/templates/calendar/app/lib/event-colors.ts +29 -4
- package/corpus/templates/calendar/app/pages/BookingsList.tsx +99 -19
- package/corpus/templates/calendar/app/pages/CalendarView.tsx +5 -11
- package/corpus/templates/calendar/app/pages/ManageBookingPage.tsx +5 -0
- package/corpus/templates/calendar/changelog/2026-09-25-ambiguous-zoom-bookings-need-review.md +6 -0
- package/corpus/templates/calendar/changelog/2026-09-25-bookings-list-shows-newest-bookings-first.md +6 -0
- package/corpus/templates/calendar/changelog/2026-09-25-canceling-a-booking-now-resolves-its-zoom-meeting-first.md +6 -0
- package/corpus/templates/calendar/changelog/2026-09-25-colleague-events-use-sidebar-colors.md +6 -0
- package/corpus/templates/calendar/changelog/2026-09-25-public-booking-pages-reject-malformed-meeting-settings-befor.md +6 -0
- package/corpus/templates/calendar/changelog/2026-09-25-shared-booking-cancellations-require-editor-access-and-unres.md +6 -0
- package/corpus/templates/calendar/server/db/schema.ts +5 -0
- package/corpus/templates/calendar/server/handlers/availability.ts +32 -29
- package/corpus/templates/calendar/server/handlers/bookings.ts +189 -62
- package/corpus/templates/calendar/server/lib/booking-link-utils.ts +46 -1
- package/corpus/templates/calendar/server/lib/zoom.ts +66 -1
- package/corpus/templates/calendar/server/plugins/db.ts +13 -0
- package/corpus/templates/calendar/shared/api.ts +4 -0
- package/corpus/templates/clips/app/components/capture-install-options.tsx +9 -6
- package/corpus/templates/clips/app/components/clips-command-menu.tsx +4 -0
- package/corpus/templates/clips/app/i18n/en-US.ts +5 -4
- package/corpus/templates/clips/app/routes/_app.settings._index.tsx +4 -0
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +7 -33
- package/corpus/templates/clips/changelog/2026-09-26-desktop-platform-icon.md +6 -0
- package/corpus/templates/clips/changelog/2026-09-26-the-empty-comments-state-now-explains-how-screen-recordings-.md +6 -0
- package/corpus/templates/clips/changelog/2026-09-26-the-empty-comments-view-has-a-clearer-headline-and-a-focused.md +6 -0
- package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +26 -2
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +74 -40
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +64 -42
- package/corpus/templates/design/app/components/design/design-canvas/creation.ts +1 -0
- package/corpus/templates/design/app/components/design/multi-screen/types.ts +5 -2
- package/corpus/templates/design/app/pages/design-editor/commands/duplicate-screen.ts +56 -25
- package/corpus/templates/design/app/routes/design-systems.tsx +2 -4
- package/corpus/templates/design/app/routes/design.$id.tsx +2 -1
- package/corpus/templates/design/app/routes/public-design-meta.ts +0 -39
- package/corpus/templates/design/app/routes/templates.tsx +2 -4
- package/corpus/templates/design/app/routes/visual-edit_.$id.tsx +2 -1
- package/corpus/templates/design/changelog/2026-09-25-escape-finishes-open-pen-paths.md +7 -0
- package/corpus/templates/design/changelog/2026-09-25-pen-paths-remain-available-after-save-failures-and-retries-k.md +6 -0
- package/corpus/templates/design/changelog/2026-09-25-pen-retries-keep-extending-the-selected-vector-after-a-save-.md +6 -0
- package/corpus/templates/design/changelog/2026-09-25-pressing-enter-finishes-a-new-pen-path-and-selects-its-vector-on-move.md +6 -0
- package/corpus/templates/design/changelog/2026-09-26-cmd-d-places-screen-duplicates-in-the-next-open-slot.md +6 -0
- package/corpus/templates/design/changelog/2026-09-26-dropping-a-layer-beside-a-sibling-keeps-that-insertion-point.md +6 -0
- package/corpus/templates/design/changelog/2026-09-26-layers-dragged-out-of-frames-now-land-at-the-board-level-and.md +6 -0
- package/corpus/templates/design/changelog/2026-09-26-opening-shared-designs-no-longer-fails-during-client-startup.md +6 -0
- package/corpus/templates/design/package.json +1 -1
- package/corpus/templates/design/scripts/check-client-server-boundary.ts +55 -0
- package/corpus/templates/design/server/lib/public-design-meta.server.ts +40 -0
- package/corpus/templates/mail/app/components/email/EmailListItem.tsx +11 -9
- package/corpus/templates/mail/app/components/email/EmailThread.tsx +9 -1
- package/corpus/templates/mail/app/components/layout/AppLayout.tsx +2 -2
- package/corpus/templates/mail/app/components/settings/AiFilterSection.tsx +25 -2
- package/corpus/templates/mail/app/hooks/use-emails.ts +159 -27
- package/corpus/templates/mail/app/i18n/en-US.ts +2 -1
- package/corpus/templates/mail/app/lib/label-display.ts +6 -0
- package/corpus/templates/mail/app/pages/InboxPage.tsx +5 -0
- package/corpus/templates/mail/changelog/2026-09-26-fixed-importance-actions-label-display-and-triage-loading-fe.md +6 -0
- package/corpus/templates/mail/changelog/2026-09-26-mail-cancels-stale-thread-read-cooldown-retries-after-a-newe.md +6 -0
- package/corpus/templates/mail/server/lib/google-api.ts +12 -7
- package/corpus/templates/slides/app/i18n/en-US.ts +0 -1
- package/corpus/templates/slides/app/pages/Index.tsx +1 -29
- package/corpus/templates/slides/changelog/2026-09-26-create-decks-from-the-prompt-without-a-separate-new-deck-button.md +6 -0
- package/corpus/templates/tasks/.agents/skills/agent-watchdog/SKILL.md +7 -2
- package/dist/client/settings/UsageSection.js +18 -5
- package/dist/client/settings/recent-prompt-groups.d.ts +12 -0
- package/dist/client/settings/recent-prompt-groups.js +20 -0
- package/dist/client/use-action.js +7 -0
- package/dist/observability/routes.d.ts +5 -5
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +14 -14
- package/dist/provider-api/actions/provider-api.d.ts +13 -13
- package/dist/secrets/routes.d.ts +3 -3
- package/dist/server/action-routes.js +25 -2
- package/dist/server/agent-engine-ollama-models-route.d.ts +2 -2
- package/package.json +3 -3
|
@@ -31,8 +31,9 @@ import {
|
|
|
31
31
|
} from "@/components/ui/tooltip";
|
|
32
32
|
import { useAccountFilter } from "@/hooks/use-account-filter";
|
|
33
33
|
import { getLabelStyle } from "@/lib/label-colors";
|
|
34
|
+
import { mailLabelDisplayName } from "@/lib/label-display";
|
|
34
35
|
import type { ThreadSummary } from "@/lib/threads";
|
|
35
|
-
import { cn, formatEmailDate
|
|
36
|
+
import { cn, formatEmailDate } from "@/lib/utils";
|
|
36
37
|
|
|
37
38
|
interface EmailListItemProps {
|
|
38
39
|
email: EmailMessage;
|
|
@@ -619,16 +620,13 @@ export const EmailListItem = memo(function EmailListItem({
|
|
|
619
620
|
const labelName =
|
|
620
621
|
labelNames?.get(labelId) ??
|
|
621
622
|
labelId.replace(/^label:/, "").replace(/^CATEGORY_/, "");
|
|
622
|
-
const displayName = labelName
|
|
623
|
-
.slice(labelName.lastIndexOf("/") + 1)
|
|
624
|
-
.replace(/_/g, " ")
|
|
625
|
-
.toLowerCase();
|
|
623
|
+
const displayName = mailLabelDisplayName(labelName);
|
|
626
624
|
return (
|
|
627
625
|
<span
|
|
628
626
|
key={labelId}
|
|
629
627
|
className={cn("label-badge", style.bg, style.text)}
|
|
630
628
|
>
|
|
631
|
-
{
|
|
629
|
+
{displayName}
|
|
632
630
|
</span>
|
|
633
631
|
);
|
|
634
632
|
})}
|
|
@@ -674,7 +672,11 @@ export const EmailListItem = memo(function EmailListItem({
|
|
|
674
672
|
{importanceScore.toFixed(2)}
|
|
675
673
|
</button>
|
|
676
674
|
</PopoverTrigger>
|
|
677
|
-
<PopoverContent
|
|
675
|
+
<PopoverContent
|
|
676
|
+
align="end"
|
|
677
|
+
className="w-72 p-3"
|
|
678
|
+
onClick={(event) => event.stopPropagation()}
|
|
679
|
+
>
|
|
678
680
|
<div className="flex items-baseline gap-2">
|
|
679
681
|
<span
|
|
680
682
|
className="email-importance-score text-xl font-semibold tabular-nums"
|
|
@@ -701,7 +703,7 @@ export const EmailListItem = memo(function EmailListItem({
|
|
|
701
703
|
<button
|
|
702
704
|
type="button"
|
|
703
705
|
onClick={() => onImportanceFeedback?.("important")}
|
|
704
|
-
className="flex items-center justify-center gap-1.5 rounded-md border border-border px-2 py-1.5 text-[11px] text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
706
|
+
className="flex items-center justify-center gap-1.5 whitespace-nowrap rounded-md border border-border px-2 py-1.5 text-[11px] text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
705
707
|
>
|
|
706
708
|
<IconThumbUp className="size-3.5" />
|
|
707
709
|
{t("mail.aiFilter.importantMode")}
|
|
@@ -709,7 +711,7 @@ export const EmailListItem = memo(function EmailListItem({
|
|
|
709
711
|
<button
|
|
710
712
|
type="button"
|
|
711
713
|
onClick={() => onImportanceFeedback?.("not-important")}
|
|
712
|
-
className="flex items-center justify-center gap-1.5 rounded-md border border-border px-2 py-1.5 text-[11px] text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
714
|
+
className="flex items-center justify-center gap-1.5 whitespace-nowrap rounded-md border border-border px-2 py-1.5 text-[11px] text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
713
715
|
>
|
|
714
716
|
<IconThumbDown className="size-3.5" />
|
|
715
717
|
{t("mail.aiFilter.notImportantMode")}
|
|
@@ -89,6 +89,7 @@ import {
|
|
|
89
89
|
processHtmlImages,
|
|
90
90
|
} from "@/lib/email-image-policy";
|
|
91
91
|
import { getLabelStyle } from "@/lib/label-colors";
|
|
92
|
+
import { mailLabelDisplayName } from "@/lib/label-display";
|
|
92
93
|
import { isMcpEmbedSurface } from "@/lib/mcp-embed";
|
|
93
94
|
import {
|
|
94
95
|
buildForwardDraft,
|
|
@@ -152,6 +153,7 @@ function InlineReplyComposerSkeleton() {
|
|
|
152
153
|
|
|
153
154
|
export function EmailThread({
|
|
154
155
|
activeThreadId,
|
|
156
|
+
labelNames,
|
|
155
157
|
onArchived,
|
|
156
158
|
emailIds = [],
|
|
157
159
|
threads = [],
|
|
@@ -163,6 +165,7 @@ export function EmailThread({
|
|
|
163
165
|
onToggleMaximize,
|
|
164
166
|
}: {
|
|
165
167
|
activeThreadId?: string;
|
|
168
|
+
labelNames?: ReadonlyMap<string, string>;
|
|
166
169
|
onArchived?: (id: string) => void;
|
|
167
170
|
emailIds?: string[];
|
|
168
171
|
/**
|
|
@@ -1405,7 +1408,12 @@ export function EmailThread({
|
|
|
1405
1408
|
style.text,
|
|
1406
1409
|
)}
|
|
1407
1410
|
>
|
|
1408
|
-
{
|
|
1411
|
+
{mailLabelDisplayName(
|
|
1412
|
+
labelNames?.get(labelId) ??
|
|
1413
|
+
labelId
|
|
1414
|
+
.replace(/^label:/, "")
|
|
1415
|
+
.replace(/^CATEGORY_/, ""),
|
|
1416
|
+
)}
|
|
1409
1417
|
</span>
|
|
1410
1418
|
);
|
|
1411
1419
|
})}
|
|
@@ -1395,7 +1395,7 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
|
|
1395
1395
|
{/* Primary tabs stay mounted during search so navigation does not jump. */}
|
|
1396
1396
|
<>
|
|
1397
1397
|
{tabsLoading ? (
|
|
1398
|
-
<nav className="hidden sm:flex items-center gap-2 overflow-x-auto hide-scrollbar">
|
|
1398
|
+
<nav className="hidden sm:flex flex-1 min-w-0 items-center gap-2 overflow-x-auto hide-scrollbar">
|
|
1399
1399
|
{[1, 2, 3].map((i) => (
|
|
1400
1400
|
<span
|
|
1401
1401
|
key={i}
|
|
@@ -1406,7 +1406,7 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
|
|
1406
1406
|
</nav>
|
|
1407
1407
|
) : (
|
|
1408
1408
|
<nav
|
|
1409
|
-
className="hidden sm:flex flex-
|
|
1409
|
+
className="hidden sm:flex flex-1 min-w-0 flex-nowrap items-center gap-1 overflow-x-auto hide-scrollbar"
|
|
1410
1410
|
data-mail-tab-list
|
|
1411
1411
|
>
|
|
1412
1412
|
{topBarTabs.map((tab, tabIndex) => {
|
|
@@ -51,6 +51,7 @@ type RuleMode = "tag" | "important" | "archive" | "spam";
|
|
|
51
51
|
type PromptMode = Exclude<RuleMode, "tag">;
|
|
52
52
|
|
|
53
53
|
const PROMPT_MODES: PromptMode[] = ["important", "archive", "spam"];
|
|
54
|
+
const EMPTY_RULES: AutomationRule[] = [];
|
|
54
55
|
|
|
55
56
|
const labelForRule = (rule: AutomationRule) => {
|
|
56
57
|
const action = rule.actions.find((item) => item.type === "label");
|
|
@@ -188,7 +189,8 @@ function AiTagRow({
|
|
|
188
189
|
export function AiFilterSection() {
|
|
189
190
|
const t = useT();
|
|
190
191
|
const { data: state, isLoading: filterLoading } = useAiFilter();
|
|
191
|
-
const
|
|
192
|
+
const automations = useAutomations();
|
|
193
|
+
const rules = automations.data ?? EMPTY_RULES;
|
|
192
194
|
const { data: settings } = useSettings();
|
|
193
195
|
const { data: labels = [] } = useLabels();
|
|
194
196
|
const googleStatus = useGoogleAuthStatus();
|
|
@@ -531,7 +533,28 @@ export function AiFilterSection() {
|
|
|
531
533
|
}
|
|
532
534
|
};
|
|
533
535
|
|
|
534
|
-
if (
|
|
536
|
+
if (automations.isError && automations.data === undefined) {
|
|
537
|
+
return (
|
|
538
|
+
<div
|
|
539
|
+
className="flex max-w-[720px] items-center justify-between gap-3 rounded-md border border-destructive/30 px-3 py-2"
|
|
540
|
+
role="alert"
|
|
541
|
+
>
|
|
542
|
+
<span className="text-sm text-muted-foreground">
|
|
543
|
+
{t("mail.aiFilter.automationRulesLoadFailed")}
|
|
544
|
+
</span>
|
|
545
|
+
<Button
|
|
546
|
+
variant="ghost"
|
|
547
|
+
size="sm"
|
|
548
|
+
disabled={automations.isFetching}
|
|
549
|
+
onClick={() => void automations.refetch()}
|
|
550
|
+
>
|
|
551
|
+
{t("mail.error.tryAgain")}
|
|
552
|
+
</Button>
|
|
553
|
+
</div>
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (filterLoading || automations.isLoading || !state) {
|
|
535
558
|
return <Skeleton className="h-72 w-full max-w-[720px]" />;
|
|
536
559
|
}
|
|
537
560
|
|
|
@@ -834,6 +834,45 @@ function rollbackBooleanMutation(
|
|
|
834
834
|
|
|
835
835
|
const readMutationVersions = new Map<string, BooleanMutationState>();
|
|
836
836
|
|
|
837
|
+
type ThreadReadIntent = { threadId: string; version: number };
|
|
838
|
+
type ThreadReadIntentState = { version: number; pending: Set<number> };
|
|
839
|
+
|
|
840
|
+
class SupersededThreadReadRetryError extends Error {
|
|
841
|
+
constructor() {
|
|
842
|
+
super("A newer read-state change superseded this retry");
|
|
843
|
+
this.name = "SupersededThreadReadRetryError";
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
const threadReadIntentStates = new Map<string, ThreadReadIntentState>();
|
|
848
|
+
const threadReadIntentByVariables = new WeakMap<object, ThreadReadIntent>();
|
|
849
|
+
const threadReadRetryIntentByError = new WeakMap<object, ThreadReadIntent>();
|
|
850
|
+
|
|
851
|
+
export function beginThreadReadIntent(threadId: string): ThreadReadIntent {
|
|
852
|
+
const state = threadReadIntentStates.get(threadId) ?? {
|
|
853
|
+
version: 0,
|
|
854
|
+
pending: new Set<number>(),
|
|
855
|
+
};
|
|
856
|
+
const intent = { threadId, version: state.version + 1 };
|
|
857
|
+
state.version = intent.version;
|
|
858
|
+
state.pending.add(intent.version);
|
|
859
|
+
threadReadIntentStates.set(threadId, state);
|
|
860
|
+
return intent;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
export function isCurrentThreadReadIntent(intent: ThreadReadIntent): boolean {
|
|
864
|
+
const state = threadReadIntentStates.get(intent.threadId);
|
|
865
|
+
return state?.version === intent.version && state.pending.has(intent.version);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
export function finishThreadReadIntent(intent: ThreadReadIntent | undefined) {
|
|
869
|
+
if (!intent) return;
|
|
870
|
+
const state = threadReadIntentStates.get(intent.threadId);
|
|
871
|
+
if (!state) return;
|
|
872
|
+
state.pending.delete(intent.version);
|
|
873
|
+
if (state.pending.size === 0) threadReadIntentStates.delete(intent.threadId);
|
|
874
|
+
}
|
|
875
|
+
|
|
837
876
|
export function beginReadMutation(
|
|
838
877
|
emailId: string,
|
|
839
878
|
currentState: boolean | undefined,
|
|
@@ -1533,7 +1572,8 @@ export function useMarkRead() {
|
|
|
1533
1572
|
accountEmail,
|
|
1534
1573
|
flag: isRead,
|
|
1535
1574
|
}),
|
|
1536
|
-
onMutate: async (
|
|
1575
|
+
onMutate: async (variables) => {
|
|
1576
|
+
const { id, isRead, accountEmail, threadId } = variables;
|
|
1537
1577
|
const previous = qc.getQueriesData<InfiniteEmails>({
|
|
1538
1578
|
queryKey: ["emails"],
|
|
1539
1579
|
});
|
|
@@ -1556,6 +1596,9 @@ export function useMarkRead() {
|
|
|
1556
1596
|
previousReadState ?? target?.isRead,
|
|
1557
1597
|
isRead,
|
|
1558
1598
|
);
|
|
1599
|
+
const readIntent = resolvedThreadId
|
|
1600
|
+
? beginThreadReadIntent(resolvedThreadId)
|
|
1601
|
+
: undefined;
|
|
1559
1602
|
setOptimisticOverride(id, { isRead });
|
|
1560
1603
|
// Message-scoped: this only touches one message, so the row's unread
|
|
1561
1604
|
// count must move by ±1, not snap the whole thread to read/unread —
|
|
@@ -1581,12 +1624,18 @@ export function useMarkRead() {
|
|
|
1581
1624
|
);
|
|
1582
1625
|
}
|
|
1583
1626
|
}
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1627
|
+
try {
|
|
1628
|
+
await Promise.all([
|
|
1629
|
+
qc.cancelQueries({ queryKey: ["emails"] }),
|
|
1630
|
+
cancelInboxThreadsQueries(qc),
|
|
1631
|
+
]);
|
|
1632
|
+
} catch (error) {
|
|
1633
|
+
finishThreadReadIntent(readIntent);
|
|
1634
|
+
throw error;
|
|
1635
|
+
}
|
|
1588
1636
|
return {
|
|
1589
1637
|
mutationVersion,
|
|
1638
|
+
readIntent,
|
|
1590
1639
|
threadId: resolvedThreadId,
|
|
1591
1640
|
inboxMutationId,
|
|
1592
1641
|
refreshThread:
|
|
@@ -1618,6 +1667,7 @@ export function useMarkRead() {
|
|
|
1618
1667
|
toast.error(toError(err).message);
|
|
1619
1668
|
},
|
|
1620
1669
|
onSettled: (_data, _error, _variables, context) => {
|
|
1670
|
+
finishThreadReadIntent(context?.readIntent);
|
|
1621
1671
|
if (context?.refreshThread) {
|
|
1622
1672
|
refreshThreadAfterMutations(context.refreshThread);
|
|
1623
1673
|
}
|
|
@@ -1633,18 +1683,40 @@ export function useMarkRead() {
|
|
|
1633
1683
|
|
|
1634
1684
|
export function useMarkThreadRead() {
|
|
1635
1685
|
const qc = useQueryClient();
|
|
1686
|
+
const t = useT();
|
|
1636
1687
|
return useMutation({
|
|
1637
|
-
mutationFn: ({
|
|
1638
|
-
threadId,
|
|
1639
|
-
accountEmail,
|
|
1640
|
-
}: {
|
|
1688
|
+
mutationFn: async (variables: {
|
|
1641
1689
|
threadId: string;
|
|
1642
1690
|
accountEmail?: string;
|
|
1643
|
-
}) =>
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1691
|
+
}) => {
|
|
1692
|
+
const intent = threadReadIntentByVariables.get(variables);
|
|
1693
|
+
if (intent && !isCurrentThreadReadIntent(intent)) {
|
|
1694
|
+
throw new SupersededThreadReadRetryError();
|
|
1695
|
+
}
|
|
1696
|
+
try {
|
|
1697
|
+
return assertActionSuccess(
|
|
1698
|
+
await callAction("mark-thread-read", variables),
|
|
1699
|
+
);
|
|
1700
|
+
} catch (error) {
|
|
1701
|
+
if (intent && error && typeof error === "object") {
|
|
1702
|
+
threadReadRetryIntentByError.set(error, intent);
|
|
1703
|
+
}
|
|
1704
|
+
throw error;
|
|
1705
|
+
}
|
|
1706
|
+
},
|
|
1707
|
+
retry: (failureCount, error) => {
|
|
1708
|
+
const intent =
|
|
1709
|
+
error && typeof error === "object"
|
|
1710
|
+
? threadReadRetryIntentByError.get(error)
|
|
1711
|
+
: undefined;
|
|
1712
|
+
return shouldRetryMarkThreadRead(failureCount, error, intent);
|
|
1713
|
+
},
|
|
1714
|
+
retryDelay: (_failureCount, error) =>
|
|
1715
|
+
markThreadReadRetryAfterMs(error) ?? 0,
|
|
1716
|
+
onMutate: async (variables) => {
|
|
1717
|
+
const { threadId, accountEmail } = variables;
|
|
1718
|
+
const retryIntent = beginThreadReadIntent(threadId);
|
|
1719
|
+
threadReadIntentByVariables.set(variables, retryIntent);
|
|
1648
1720
|
const previous = qc.getQueriesData<InfiniteEmails>({
|
|
1649
1721
|
queryKey: ["emails"],
|
|
1650
1722
|
});
|
|
@@ -1679,12 +1751,19 @@ export function useMarkThreadRead() {
|
|
|
1679
1751
|
previousThread.map((message) => ({ ...message, isRead: true })),
|
|
1680
1752
|
);
|
|
1681
1753
|
}
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1754
|
+
try {
|
|
1755
|
+
await Promise.all([
|
|
1756
|
+
qc.cancelQueries({ queryKey: ["emails"] }),
|
|
1757
|
+
cancelInboxThreadsQueries(qc),
|
|
1758
|
+
]);
|
|
1759
|
+
} catch (error) {
|
|
1760
|
+
threadReadIntentByVariables.delete(variables);
|
|
1761
|
+
finishThreadReadIntent(retryIntent);
|
|
1762
|
+
throw error;
|
|
1763
|
+
}
|
|
1686
1764
|
return {
|
|
1687
1765
|
mutations,
|
|
1766
|
+
retryIntent,
|
|
1688
1767
|
inboxMutationId,
|
|
1689
1768
|
refreshThread: restartThread
|
|
1690
1769
|
? {
|
|
@@ -1720,9 +1799,15 @@ export function useMarkThreadRead() {
|
|
|
1720
1799
|
if (context?.inboxMutationId) {
|
|
1721
1800
|
forgetInboxMutation(qc, context.inboxMutationId);
|
|
1722
1801
|
}
|
|
1723
|
-
|
|
1802
|
+
if (err instanceof SupersededThreadReadRetryError) return;
|
|
1803
|
+
toast.error(
|
|
1804
|
+
markThreadReadRetryAfterMs(err) !== undefined
|
|
1805
|
+
? t("mail.error.rateLimitDescription")
|
|
1806
|
+
: toError(err).message,
|
|
1807
|
+
);
|
|
1724
1808
|
},
|
|
1725
1809
|
onSettled: (_data, _error, _variables, context) => {
|
|
1810
|
+
finishThreadReadIntent(context?.retryIntent);
|
|
1726
1811
|
if (context?.refreshThread) {
|
|
1727
1812
|
refreshThreadAfterMutations(context.refreshThread);
|
|
1728
1813
|
}
|
|
@@ -1736,6 +1821,38 @@ export function useMarkThreadRead() {
|
|
|
1736
1821
|
});
|
|
1737
1822
|
}
|
|
1738
1823
|
|
|
1824
|
+
export function markThreadReadRetryAfterMs(error: unknown): number | undefined {
|
|
1825
|
+
if (!error || typeof error !== "object") return undefined;
|
|
1826
|
+
const details = error as {
|
|
1827
|
+
status?: unknown;
|
|
1828
|
+
errorCode?: unknown;
|
|
1829
|
+
retryAfterMs?: unknown;
|
|
1830
|
+
};
|
|
1831
|
+
if (
|
|
1832
|
+
details.status !== 429 ||
|
|
1833
|
+
details.errorCode !== "gmail_quota_cooldown" ||
|
|
1834
|
+
typeof details.retryAfterMs !== "number" ||
|
|
1835
|
+
!Number.isInteger(details.retryAfterMs) ||
|
|
1836
|
+
details.retryAfterMs <= 0
|
|
1837
|
+
) {
|
|
1838
|
+
return undefined;
|
|
1839
|
+
}
|
|
1840
|
+
return Math.min(details.retryAfterMs, 300_000);
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
export function shouldRetryMarkThreadRead(
|
|
1844
|
+
failureCount: number,
|
|
1845
|
+
error: unknown,
|
|
1846
|
+
intent: ThreadReadIntent | undefined,
|
|
1847
|
+
): boolean {
|
|
1848
|
+
return (
|
|
1849
|
+
failureCount < 1 &&
|
|
1850
|
+
intent !== undefined &&
|
|
1851
|
+
isCurrentThreadReadIntent(intent) &&
|
|
1852
|
+
markThreadReadRetryAfterMs(error) !== undefined
|
|
1853
|
+
);
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1739
1856
|
export function useToggleStar() {
|
|
1740
1857
|
const qc = useQueryClient();
|
|
1741
1858
|
return useMutation({
|
|
@@ -2520,6 +2637,9 @@ export function useBulkMarkRead() {
|
|
|
2520
2637
|
flattenInfiniteEmails(data),
|
|
2521
2638
|
);
|
|
2522
2639
|
const threadIdsByEmailId = resolveBulkThreadIds(qc, targets);
|
|
2640
|
+
const threadReadIntents = [
|
|
2641
|
+
...new Set(Object.values(threadIdsByEmailId)),
|
|
2642
|
+
].map(beginThreadReadIntent);
|
|
2523
2643
|
const mutationVersions: Record<string, number> = {};
|
|
2524
2644
|
for (const id of ids) {
|
|
2525
2645
|
mutationVersions[id] = beginReadMutation(
|
|
@@ -2534,11 +2654,21 @@ export function useBulkMarkRead() {
|
|
|
2534
2654
|
new Set(Object.values(threadIdsByEmailId)),
|
|
2535
2655
|
isRead,
|
|
2536
2656
|
);
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2657
|
+
try {
|
|
2658
|
+
await Promise.all([
|
|
2659
|
+
qc.cancelQueries({ queryKey: ["emails"] }),
|
|
2660
|
+
cancelInboxThreadsQueries(qc),
|
|
2661
|
+
]);
|
|
2662
|
+
} catch (error) {
|
|
2663
|
+
threadReadIntents.forEach(finishThreadReadIntent);
|
|
2664
|
+
throw error;
|
|
2665
|
+
}
|
|
2666
|
+
return {
|
|
2667
|
+
mutationVersions,
|
|
2668
|
+
threadIdsByEmailId,
|
|
2669
|
+
inboxMutationId,
|
|
2670
|
+
threadReadIntents,
|
|
2671
|
+
};
|
|
2542
2672
|
},
|
|
2543
2673
|
onSuccess: (_data, vars, context) => {
|
|
2544
2674
|
if (!context) return;
|
|
@@ -2583,13 +2713,15 @@ export function useBulkMarkRead() {
|
|
|
2583
2713
|
}
|
|
2584
2714
|
toast.error(toError(err).message);
|
|
2585
2715
|
},
|
|
2586
|
-
onSettled: (_data, _error, _variables, context) =>
|
|
2587
|
-
|
|
2716
|
+
onSettled: (_data, _error, _variables, context) => {
|
|
2717
|
+
context?.threadReadIntents.forEach(finishThreadReadIntent);
|
|
2718
|
+
return delayedInvalidate(
|
|
2588
2719
|
qc,
|
|
2589
2720
|
[["emails"], LABELS_QUERY_KEY, INBOX_THREADS_QUERY_KEY],
|
|
2590
2721
|
3_000,
|
|
2591
2722
|
() => settleInboxMutationIfObserved(qc, context?.inboxMutationId),
|
|
2592
|
-
)
|
|
2723
|
+
);
|
|
2724
|
+
},
|
|
2593
2725
|
});
|
|
2594
2726
|
}
|
|
2595
2727
|
|
|
@@ -41,7 +41,7 @@ const messages = {
|
|
|
41
41
|
aiSetupTitle: "Set up your AI inbox",
|
|
42
42
|
aiSetupTagLabel: "Create an AI tag",
|
|
43
43
|
aiSetupImportanceLabel: "Important mail",
|
|
44
|
-
aiSetupSpamLabel: "Spam
|
|
44
|
+
aiSetupSpamLabel: "Spam",
|
|
45
45
|
aiSetupArchiveLabel: "Skip inbox",
|
|
46
46
|
aiSetupSave: "Save setup",
|
|
47
47
|
aiSetupSkip: "Skip for now",
|
|
@@ -551,6 +551,7 @@ const messages = {
|
|
|
551
551
|
keptToast: "Kept {{count}} conversation(s) in Inbox.",
|
|
552
552
|
actionFailed: "Could not update the AI filter.",
|
|
553
553
|
settingsFailed: "Could not save AI filter settings.",
|
|
554
|
+
automationRulesLoadFailed: "Couldn’t load triage rules.",
|
|
554
555
|
instructionFailed: "Could not save AI filter instruction.",
|
|
555
556
|
skipInboxMode: "Skip inbox",
|
|
556
557
|
spamMode: "Spam",
|
|
@@ -412,6 +412,10 @@ export function InboxPage() {
|
|
|
412
412
|
activeAccounts.size > 0 ? [...activeAccounts] : undefined,
|
|
413
413
|
);
|
|
414
414
|
const labels = labelsData ?? EMPTY_LABELS;
|
|
415
|
+
const labelNames = useMemo(
|
|
416
|
+
() => new Map(labels.map((label) => [label.id, label.name])),
|
|
417
|
+
[labels],
|
|
418
|
+
);
|
|
415
419
|
|
|
416
420
|
// Memoize every derived array — the emails memo depends on these, and fresh
|
|
417
421
|
// array refs on every render were cascading into EmailThread as unstable
|
|
@@ -1167,6 +1171,7 @@ export function InboxPage() {
|
|
|
1167
1171
|
{hasThread ? (
|
|
1168
1172
|
<EmailThread
|
|
1169
1173
|
activeThreadId={threadId}
|
|
1174
|
+
labelNames={labelNames}
|
|
1170
1175
|
onArchived={setLastArchivedId}
|
|
1171
1176
|
emailIds={threadIds}
|
|
1172
1177
|
threads={threads}
|
|
@@ -213,20 +213,25 @@ function quotaCooldownMessage(cooldownMs = QUOTA_COOLDOWN_MS): string {
|
|
|
213
213
|
|
|
214
214
|
/**
|
|
215
215
|
* Every quota/cooldown throw in this file goes through this type instead of
|
|
216
|
-
* a plain Error.
|
|
217
|
-
*
|
|
218
|
-
* cooldown — to return 429 + Retry-After instead of a hard failure — must
|
|
219
|
-
* not do it by grepping the message for words like "quota" or "429"; that
|
|
220
|
-
* regex silently stops matching the moment the wording changes, and every
|
|
221
|
-
* caller upstream then reports a plain 502 during a routine cooldown. Check
|
|
222
|
-
* `instanceof GmailQuotaCooldownError` (or read `retryAfterMs`) instead.
|
|
216
|
+
* a plain Error. Its HTTP metadata lets the shared action boundary return a
|
|
217
|
+
* retryable 429 without action-specific catches or message parsing.
|
|
223
218
|
*/
|
|
224
219
|
export class GmailQuotaCooldownError extends Error {
|
|
225
220
|
readonly retryAfterMs: number;
|
|
221
|
+
readonly statusCode = 429;
|
|
222
|
+
readonly errorCode = "gmail_quota_cooldown";
|
|
223
|
+
readonly details: { retryAfterSeconds: number };
|
|
224
|
+
|
|
226
225
|
constructor(message: string, retryAfterMs: number) {
|
|
227
226
|
super(message);
|
|
228
227
|
this.name = "GmailQuotaCooldownError";
|
|
229
228
|
this.retryAfterMs = retryAfterMs;
|
|
229
|
+
const retryAfterSeconds = Number.isFinite(retryAfterMs)
|
|
230
|
+
? Math.ceil(retryAfterMs / 1000)
|
|
231
|
+
: 1;
|
|
232
|
+
this.details = {
|
|
233
|
+
retryAfterSeconds: Math.min(300, Math.max(1, retryAfterSeconds)),
|
|
234
|
+
};
|
|
230
235
|
}
|
|
231
236
|
}
|
|
232
237
|
|
|
@@ -914,7 +914,6 @@ const messages = {
|
|
|
914
914
|
"Your saved content is still available. Check the connection and try again.",
|
|
915
915
|
retry: "Retry",
|
|
916
916
|
decksTitle: "Decks",
|
|
917
|
-
newDeck: "New Deck",
|
|
918
917
|
deckLengthQuestion: "How long should this deck be?",
|
|
919
918
|
deckLengthHeader: "Deck length",
|
|
920
919
|
deckLengthShort: "Short (3–5 slides)",
|
|
@@ -36,7 +36,6 @@ import { extractGoogleDocUrls } from "@shared/google-docs";
|
|
|
36
36
|
import {
|
|
37
37
|
IconAlertTriangle,
|
|
38
38
|
IconArrowRight,
|
|
39
|
-
IconPlus,
|
|
40
39
|
IconRefresh,
|
|
41
40
|
IconSearch,
|
|
42
41
|
} from "@tabler/icons-react";
|
|
@@ -684,15 +683,6 @@ export default function Index() {
|
|
|
684
683
|
[setSearchParams],
|
|
685
684
|
);
|
|
686
685
|
|
|
687
|
-
const openNewDeck = useCallback(() => {
|
|
688
|
-
preloadPromptPopover();
|
|
689
|
-
designSystemAutoRef.current = true;
|
|
690
|
-
referenceDeckAutoRef.current = true;
|
|
691
|
-
setSelectedDesignSystemId(initialDesignSystemId ?? null);
|
|
692
|
-
setSelectedReferenceDeckId(initialReferenceDeckId ?? null);
|
|
693
|
-
setShowNewDeckPrompt(true);
|
|
694
|
-
}, [initialDesignSystemId, initialReferenceDeckId]);
|
|
695
|
-
|
|
696
686
|
useEffect(() => {
|
|
697
687
|
if (
|
|
698
688
|
deckFilterWasSelectedRef.current ||
|
|
@@ -2026,28 +2016,10 @@ export default function Index() {
|
|
|
2026
2016
|
{viewState !== "empty" ? (
|
|
2027
2017
|
<DeckFilterMenu value={deckFilter} onChange={setDeckFilter} />
|
|
2028
2018
|
) : null}
|
|
2029
|
-
<Button
|
|
2030
|
-
onClick={openNewDeck}
|
|
2031
|
-
onPointerEnter={preloadPromptPopover}
|
|
2032
|
-
onFocus={preloadPromptPopover}
|
|
2033
|
-
size="sm"
|
|
2034
|
-
className="cursor-pointer"
|
|
2035
|
-
>
|
|
2036
|
-
<IconPlus className="w-3.5 h-3.5" />
|
|
2037
|
-
{t("home.newDeck")}
|
|
2038
|
-
</Button>
|
|
2039
2019
|
<ImportDeckButton controller={deckImport} />
|
|
2040
2020
|
</HomeHeaderActions>
|
|
2041
2021
|
),
|
|
2042
|
-
[
|
|
2043
|
-
deckFilter,
|
|
2044
|
-
deckImport,
|
|
2045
|
-
deckSearch,
|
|
2046
|
-
openNewDeck,
|
|
2047
|
-
setDeckFilter,
|
|
2048
|
-
t,
|
|
2049
|
-
viewState,
|
|
2050
|
-
],
|
|
2022
|
+
[deckFilter, deckImport, deckSearch, setDeckFilter, t, viewState],
|
|
2051
2023
|
),
|
|
2052
2024
|
);
|
|
2053
2025
|
if (isStartingNewDeck) {
|
|
@@ -77,8 +77,13 @@ Classify each issue as:
|
|
|
77
77
|
When the user authorized repair:
|
|
78
78
|
|
|
79
79
|
1. Fix only gaps with clear evidence.
|
|
80
|
-
2. Preserve unrelated local changes
|
|
81
|
-
|
|
80
|
+
2. Preserve unrelated local changes in the existing checkout. When isolation
|
|
81
|
+
is useful, create a separate task-owned worktree from fresh `origin/main` if
|
|
82
|
+
it contains the audited task's commits; otherwise base it on the resolved
|
|
83
|
+
task or PR head. If the audited changes are uncommitted, keep working in the
|
|
84
|
+
source checkout or carry only those task-owned paths over after verifying
|
|
85
|
+
them; a new worktree will not include them. Do not move or overwrite the
|
|
86
|
+
source or a peer's branch to create it.
|
|
82
87
|
3. Use existing repo patterns and targeted tests.
|
|
83
88
|
4. Re-run the smallest useful validation after each meaningful fix.
|
|
84
89
|
5. If a fix would require a product decision, credential, destructive action, or
|