@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.
Files changed (82) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/calendar/actions/cancel-booking.ts +12 -4
  3. package/corpus/templates/calendar/actions/list-bookings.ts +23 -2
  4. package/corpus/templates/calendar/actions/list-events.ts +42 -29
  5. package/corpus/templates/calendar/actions/update-booking-link.ts +27 -1
  6. package/corpus/templates/calendar/app/hooks/use-bookings.ts +9 -2
  7. package/corpus/templates/calendar/app/i18n/en-US.ts +8 -0
  8. package/corpus/templates/calendar/app/lib/booking-sorting.ts +7 -0
  9. package/corpus/templates/calendar/app/lib/event-colors.ts +29 -4
  10. package/corpus/templates/calendar/app/pages/BookingsList.tsx +99 -19
  11. package/corpus/templates/calendar/app/pages/CalendarView.tsx +5 -11
  12. package/corpus/templates/calendar/app/pages/ManageBookingPage.tsx +5 -0
  13. package/corpus/templates/calendar/changelog/2026-09-25-ambiguous-zoom-bookings-need-review.md +6 -0
  14. package/corpus/templates/calendar/changelog/2026-09-25-bookings-list-shows-newest-bookings-first.md +6 -0
  15. package/corpus/templates/calendar/changelog/2026-09-25-canceling-a-booking-now-resolves-its-zoom-meeting-first.md +6 -0
  16. package/corpus/templates/calendar/changelog/2026-09-25-colleague-events-use-sidebar-colors.md +6 -0
  17. package/corpus/templates/calendar/changelog/2026-09-25-public-booking-pages-reject-malformed-meeting-settings-befor.md +6 -0
  18. package/corpus/templates/calendar/changelog/2026-09-25-shared-booking-cancellations-require-editor-access-and-unres.md +6 -0
  19. package/corpus/templates/calendar/server/db/schema.ts +5 -0
  20. package/corpus/templates/calendar/server/handlers/availability.ts +32 -29
  21. package/corpus/templates/calendar/server/handlers/bookings.ts +189 -62
  22. package/corpus/templates/calendar/server/lib/booking-link-utils.ts +46 -1
  23. package/corpus/templates/calendar/server/lib/zoom.ts +66 -1
  24. package/corpus/templates/calendar/server/plugins/db.ts +13 -0
  25. package/corpus/templates/calendar/shared/api.ts +4 -0
  26. package/corpus/templates/clips/app/components/capture-install-options.tsx +9 -6
  27. package/corpus/templates/clips/app/components/clips-command-menu.tsx +4 -0
  28. package/corpus/templates/clips/app/i18n/en-US.ts +5 -4
  29. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +4 -0
  30. package/corpus/templates/clips/app/routes/share.$shareId.tsx +7 -33
  31. package/corpus/templates/clips/changelog/2026-09-26-desktop-platform-icon.md +6 -0
  32. package/corpus/templates/clips/changelog/2026-09-26-the-empty-comments-state-now-explains-how-screen-recordings-.md +6 -0
  33. package/corpus/templates/clips/changelog/2026-09-26-the-empty-comments-view-has-a-clearer-headline-and-a-focused.md +6 -0
  34. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +26 -2
  35. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +74 -40
  36. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +64 -42
  37. package/corpus/templates/design/app/components/design/design-canvas/creation.ts +1 -0
  38. package/corpus/templates/design/app/components/design/multi-screen/types.ts +5 -2
  39. package/corpus/templates/design/app/pages/design-editor/commands/duplicate-screen.ts +56 -25
  40. package/corpus/templates/design/app/routes/design-systems.tsx +2 -4
  41. package/corpus/templates/design/app/routes/design.$id.tsx +2 -1
  42. package/corpus/templates/design/app/routes/public-design-meta.ts +0 -39
  43. package/corpus/templates/design/app/routes/templates.tsx +2 -4
  44. package/corpus/templates/design/app/routes/visual-edit_.$id.tsx +2 -1
  45. package/corpus/templates/design/changelog/2026-09-25-escape-finishes-open-pen-paths.md +7 -0
  46. package/corpus/templates/design/changelog/2026-09-25-pen-paths-remain-available-after-save-failures-and-retries-k.md +6 -0
  47. package/corpus/templates/design/changelog/2026-09-25-pen-retries-keep-extending-the-selected-vector-after-a-save-.md +6 -0
  48. package/corpus/templates/design/changelog/2026-09-25-pressing-enter-finishes-a-new-pen-path-and-selects-its-vector-on-move.md +6 -0
  49. package/corpus/templates/design/changelog/2026-09-26-cmd-d-places-screen-duplicates-in-the-next-open-slot.md +6 -0
  50. package/corpus/templates/design/changelog/2026-09-26-dropping-a-layer-beside-a-sibling-keeps-that-insertion-point.md +6 -0
  51. package/corpus/templates/design/changelog/2026-09-26-layers-dragged-out-of-frames-now-land-at-the-board-level-and.md +6 -0
  52. package/corpus/templates/design/changelog/2026-09-26-opening-shared-designs-no-longer-fails-during-client-startup.md +6 -0
  53. package/corpus/templates/design/package.json +1 -1
  54. package/corpus/templates/design/scripts/check-client-server-boundary.ts +55 -0
  55. package/corpus/templates/design/server/lib/public-design-meta.server.ts +40 -0
  56. package/corpus/templates/mail/app/components/email/EmailListItem.tsx +11 -9
  57. package/corpus/templates/mail/app/components/email/EmailThread.tsx +9 -1
  58. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +2 -2
  59. package/corpus/templates/mail/app/components/settings/AiFilterSection.tsx +25 -2
  60. package/corpus/templates/mail/app/hooks/use-emails.ts +159 -27
  61. package/corpus/templates/mail/app/i18n/en-US.ts +2 -1
  62. package/corpus/templates/mail/app/lib/label-display.ts +6 -0
  63. package/corpus/templates/mail/app/pages/InboxPage.tsx +5 -0
  64. package/corpus/templates/mail/changelog/2026-09-26-fixed-importance-actions-label-display-and-triage-loading-fe.md +6 -0
  65. package/corpus/templates/mail/changelog/2026-09-26-mail-cancels-stale-thread-read-cooldown-retries-after-a-newe.md +6 -0
  66. package/corpus/templates/mail/server/lib/google-api.ts +12 -7
  67. package/corpus/templates/slides/app/i18n/en-US.ts +0 -1
  68. package/corpus/templates/slides/app/pages/Index.tsx +1 -29
  69. package/corpus/templates/slides/changelog/2026-09-26-create-decks-from-the-prompt-without-a-separate-new-deck-button.md +6 -0
  70. package/corpus/templates/tasks/.agents/skills/agent-watchdog/SKILL.md +7 -2
  71. package/dist/client/settings/UsageSection.js +18 -5
  72. package/dist/client/settings/recent-prompt-groups.d.ts +12 -0
  73. package/dist/client/settings/recent-prompt-groups.js +20 -0
  74. package/dist/client/use-action.js +7 -0
  75. package/dist/observability/routes.d.ts +5 -5
  76. package/dist/progress/routes.d.ts +1 -1
  77. package/dist/provider-api/actions/custom-provider-registration.d.ts +14 -14
  78. package/dist/provider-api/actions/provider-api.d.ts +13 -13
  79. package/dist/secrets/routes.d.ts +3 -3
  80. package/dist/server/action-routes.js +25 -2
  81. package/dist/server/agent-engine-ollama-models-route.d.ts +2 -2
  82. 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, truncate } from "@/lib/utils";
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
- {truncate(displayName, 16)}
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 align="end" className="w-60 p-3">
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
- {labelId}
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-nowrap min-w-0 items-center gap-1 overflow-x-auto hide-scrollbar"
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 { data: rules = [], isLoading: rulesLoading } = useAutomations();
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 (filterLoading || rulesLoading || !state) {
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 ({ id, isRead, accountEmail, threadId }) => {
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
- await Promise.all([
1585
- qc.cancelQueries({ queryKey: ["emails"] }),
1586
- cancelInboxThreadsQueries(qc),
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
- callAction("mark-thread-read", { threadId, accountEmail }).then(
1645
- assertActionSuccess,
1646
- ),
1647
- onMutate: async ({ threadId, accountEmail }) => {
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
- await Promise.all([
1683
- qc.cancelQueries({ queryKey: ["emails"] }),
1684
- cancelInboxThreadsQueries(qc),
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
- toast.error(toError(err).message);
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
- await Promise.all([
2538
- qc.cancelQueries({ queryKey: ["emails"] }),
2539
- cancelInboxThreadsQueries(qc),
2540
- ]);
2541
- return { mutationVersions, threadIdsByEmailId, inboxMutationId };
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
- delayedInvalidate(
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 and unwanted mail",
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",
@@ -0,0 +1,6 @@
1
+ export function mailLabelDisplayName(name: string): string {
2
+ return name
3
+ .slice(name.lastIndexOf("/") + 1)
4
+ .replace(/_/g, " ")
5
+ .toLowerCase();
6
+ }
@@ -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}
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-09-26
4
+ ---
5
+
6
+ Fixed importance actions, label display, and triage loading feedback.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-09-26
4
+ ---
5
+
6
+ Mail cancels stale thread-read cooldown retries after a newer unread action
@@ -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. The message text is deliberately jargon-free (see
217
- * quotaCooldownMessage above) so callers that need to *detect* a quota
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) {
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: removed
3
+ date: 2026-09-26
4
+ ---
5
+
6
+ Create decks directly from the prompt without a separate New Deck button.
@@ -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 and do not move branches unless explicitly
81
- asked for that branch operation.
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