@agent-native/core 0.191.0 → 0.192.0-nightly-20260926045737

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 (78) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/CHANGELOG.md +4 -0
  3. package/corpus/templates/analytics/app/components/layout/Layout.tsx +41 -12
  4. package/corpus/templates/analytics/app/lib/chat-handoff.ts +62 -0
  5. package/corpus/templates/analytics/changelog/2026-09-25-chat-sidebar-stays-closed-in-settings.md +6 -0
  6. package/corpus/templates/analytics/changelog/2026-09-25-show-one-recent-prompt-per-chat-turn.md +6 -0
  7. package/corpus/templates/clips/app/components/recorder/storage-setup-card.tsx +74 -11
  8. package/corpus/templates/clips/app/i18n/en-US.ts +6 -0
  9. package/corpus/templates/clips/changelog/2026-09-25-one-click-builder-storage.md +6 -0
  10. package/corpus/templates/design/actions/get-design.schema.ts +19 -0
  11. package/corpus/templates/design/actions/get-design.ts +16 -19
  12. package/corpus/templates/mail/actions/apply-ai-filter.ts +8 -0
  13. package/corpus/templates/mail/actions/create-automation.ts +7 -1
  14. package/corpus/templates/mail/actions/manage-ai-filter-rule-undo.ts +34 -0
  15. package/corpus/templates/mail/actions/manage-automations.ts +21 -44
  16. package/corpus/templates/mail/app/components/email/EmailList.tsx +35 -0
  17. package/corpus/templates/mail/app/components/onboarding/AiInboxSetup.tsx +116 -34
  18. package/corpus/templates/mail/app/components/settings/AiFilterSection.tsx +108 -70
  19. package/corpus/templates/mail/app/components/settings/AiRulePromptField.tsx +8 -1
  20. package/corpus/templates/mail/app/components/settings/JevConnectionPrompt.tsx +171 -0
  21. package/corpus/templates/mail/app/hooks/use-automations.ts +24 -0
  22. package/corpus/templates/mail/app/i18n/en-US.ts +8 -2
  23. package/corpus/templates/mail/app/pages/InboxPage.tsx +12 -2
  24. package/corpus/templates/mail/changelog/2026-09-25-mail-now-guides-users-to-connect-jev-before-setting-up-triag.md +6 -0
  25. package/corpus/templates/mail/server/db/schema.ts +17 -0
  26. package/corpus/templates/mail/server/lib/ai-filter-rule-undo.ts +151 -0
  27. package/corpus/templates/mail/server/lib/automations.ts +34 -0
  28. package/corpus/templates/mail/server/plugins/db.ts +18 -0
  29. package/corpus/templates/mail/server/plugins/mail-jobs.ts +6 -0
  30. package/dist/client/AssistantChat.js +20 -3
  31. package/dist/client/BuilderReferralInviteRow.d.ts +3 -0
  32. package/dist/client/BuilderReferralInviteRow.js +16 -0
  33. package/dist/client/agent-chat-adapter.js +6 -5
  34. package/dist/client/chat/run-recovery.js +6 -5
  35. package/dist/client/settings/UsageSection.js +2 -1
  36. package/dist/client/settings/index.d.ts +1 -1
  37. package/dist/client/settings/index.js +1 -1
  38. package/dist/client/settings/useBuilderStatus.d.ts +4 -1
  39. package/dist/client/settings/useBuilderStatus.js +32 -16
  40. package/dist/client/use-agent-chat-running-threads.d.ts +2 -0
  41. package/dist/collab/struct-routes.d.ts +1 -1
  42. package/dist/framework-tools.d.ts +3 -1
  43. package/dist/framework-tools.js +2 -0
  44. package/dist/localization/core-messages/ar-SA.js +4 -0
  45. package/dist/localization/core-messages/de-DE.js +4 -0
  46. package/dist/localization/core-messages/en-US.d.ts +4 -0
  47. package/dist/localization/core-messages/en-US.js +4 -0
  48. package/dist/localization/core-messages/es-ES.js +4 -0
  49. package/dist/localization/core-messages/fr-FR.js +4 -0
  50. package/dist/localization/core-messages/hi-IN.js +4 -0
  51. package/dist/localization/core-messages/ja-JP.js +4 -0
  52. package/dist/localization/core-messages/ko-KR.js +4 -0
  53. package/dist/localization/core-messages/pt-BR.js +4 -0
  54. package/dist/localization/core-messages/zh-CN.js +4 -0
  55. package/dist/localization/core-messages/zh-TW.js +4 -0
  56. package/dist/localization/core-messages.d.ts +4 -0
  57. package/dist/notifications/routes.d.ts +3 -3
  58. package/dist/observability/routes.d.ts +5 -5
  59. package/dist/progress/routes.d.ts +1 -1
  60. package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
  61. package/dist/provider-api/actions/provider-api.d.ts +14 -14
  62. package/dist/secrets/routes.d.ts +3 -3
  63. package/dist/server/action-discovery.js +4 -0
  64. package/dist/server/agent-chat/plugin-options.d.ts +1 -1
  65. package/dist/server/agent-chat-plugin.js +6 -4
  66. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  67. package/dist/server/agent-engine-ollama-models-route.d.ts +1 -1
  68. package/dist/server/core-routes-plugin.d.ts +1 -1
  69. package/dist/server/fusion-app.d.ts +3 -0
  70. package/dist/server/fusion-app.js +17 -0
  71. package/dist/shared/builder-referrals.d.ts +10 -0
  72. package/dist/shared/builder-referrals.js +29 -0
  73. package/dist/usage/actions/get-builder-referral-info.d.ts +9 -0
  74. package/dist/usage/actions/get-builder-referral-info.js +26 -0
  75. package/dist/usage/metrics-store.js +142 -30
  76. package/dist/vite/action-types-plugin.js +4 -0
  77. package/docs/content/actions-agent-tools.mdx +2 -1
  78. package/package.json +3 -2
@@ -1,4 +1,7 @@
1
- import { actionErrorMessage } from "@agent-native/core/client/hooks";
1
+ import {
2
+ actionErrorMessage,
3
+ useActionQuery,
4
+ } from "@agent-native/core/client/hooks";
2
5
  import { useT } from "@agent-native/core/client/i18n";
3
6
  import { AI_FILTER_LABEL, AI_FILTER_RULE_NAME } from "@shared/ai-filter";
4
7
  import { AI_IMPORTANT_LABEL } from "@shared/ai-priority";
@@ -18,6 +21,10 @@ import {
18
21
  TAG_SUGGESTIONS,
19
22
  } from "@/components/onboarding/AiInboxSetup";
20
23
  import { AiRulePromptField } from "@/components/settings/AiRulePromptField";
24
+ import {
25
+ JevAvailabilityError,
26
+ JevConnectionPrompt,
27
+ } from "@/components/settings/JevConnectionPrompt";
21
28
  import { Button } from "@/components/ui/button";
22
29
  import { Input } from "@/components/ui/input";
23
30
  import { Skeleton } from "@/components/ui/skeleton";
@@ -30,9 +37,11 @@ import {
30
37
  import { useManageAiFilter, useAiFilter } from "@/hooks/use-ai-filter";
31
38
  import {
32
39
  useAutomations,
40
+ useClearAiFilterRules,
33
41
  useConsolidateAiFilterRules,
34
42
  useCreateAutomation,
35
43
  useDeleteAutomation,
44
+ useRestoreAiFilterRules,
36
45
  useUpdateAutomation,
37
46
  } from "@/hooks/use-automations";
38
47
  import { useLabels, useSettings, useUpdateSettings } from "@/hooks/use-emails";
@@ -41,12 +50,6 @@ import { useGoogleAuthStatus } from "@/hooks/use-google-auth";
41
50
  type RuleMode = "tag" | "important" | "archive" | "spam";
42
51
  type PromptMode = Exclude<RuleMode, "tag">;
43
52
 
44
- function makeAggregateError(errors: unknown[], message: string) {
45
- const error = new Error(message);
46
- error.name = "AggregateError";
47
- return Object.assign(error, { errors });
48
- }
49
-
50
53
  const PROMPT_MODES: PromptMode[] = ["important", "archive", "spam"];
51
54
 
52
55
  const labelForRule = (rule: AutomationRule) => {
@@ -97,6 +100,7 @@ function promptForRules(rules: AutomationRule[]) {
97
100
  function AiTagRow({
98
101
  rule,
99
102
  expanded,
103
+ disabled,
100
104
  onToggle,
101
105
  onSave,
102
106
  onDelete,
@@ -104,6 +108,7 @@ function AiTagRow({
104
108
  }: {
105
109
  rule: AutomationRule;
106
110
  expanded: boolean;
111
+ disabled: boolean;
107
112
  onToggle: () => void;
108
113
  onSave: (rule: AutomationRule, name: string, condition: string) => void;
109
114
  onDelete: (rule: AutomationRule) => void;
@@ -122,8 +127,10 @@ function AiTagRow({
122
127
 
123
128
  return (
124
129
  <div
125
- draggable={!expanded}
126
- onDragStart={(event) => event.dataTransfer.setData("text/plain", rule.id)}
130
+ draggable={!expanded && !disabled}
131
+ onDragStart={(event) => {
132
+ if (!disabled) event.dataTransfer.setData("text/plain", rule.id);
133
+ }}
127
134
  onDragOver={(event) => event.preventDefault()}
128
135
  onDrop={(event) => onDrop(event, rule.id)}
129
136
  className="group border-b border-border/40 last:border-0"
@@ -134,6 +141,7 @@ function AiTagRow({
134
141
  type="button"
135
142
  className="grid min-w-0 flex-1 grid-cols-[110px_minmax(0,1fr)] items-center gap-3 text-left"
136
143
  aria-expanded={expanded}
144
+ disabled={disabled}
137
145
  onClick={onToggle}
138
146
  >
139
147
  <span className="truncate text-sm font-medium text-foreground">
@@ -146,7 +154,7 @@ function AiTagRow({
146
154
  <Button
147
155
  variant="ghost"
148
156
  size="icon"
149
- className="size-7 text-muted-foreground opacity-0 hover:text-destructive group-hover:opacity-100"
157
+ className={`size-7 text-muted-foreground hover:text-destructive ${disabled ? "" : "opacity-0 group-hover:opacity-100 focus-visible:opacity-100"}`}
150
158
  aria-label={t("mail.aiFilter.deleteInstruction")}
151
159
  onClick={() => onDelete(rule)}
152
160
  >
@@ -159,6 +167,7 @@ function AiTagRow({
159
167
  value={name}
160
168
  onChange={(event) => setName(event.target.value)}
161
169
  onBlur={save}
170
+ disabled={disabled}
162
171
  aria-label={t("mail.aiFilter.tagNamePlaceholder")}
163
172
  placeholder={t("mail.aiFilter.tagNamePlaceholder")}
164
173
  />
@@ -166,6 +175,7 @@ function AiTagRow({
166
175
  value={condition}
167
176
  onChange={setCondition}
168
177
  onBlur={save}
178
+ disabled={disabled}
169
179
  label={t("mail.aiFilter.tagPlaceholder")}
170
180
  placeholder={t("mail.aiFilter.tagPlaceholder")}
171
181
  />
@@ -182,9 +192,26 @@ export function AiFilterSection() {
182
192
  const { data: settings } = useSettings();
183
193
  const { data: labels = [] } = useLabels();
184
194
  const googleStatus = useGoogleAuthStatus();
195
+ const jevAvailability = useActionQuery(
196
+ "get-jev-availability",
197
+ {},
198
+ {
199
+ staleTime: 0,
200
+ // request-storm-allow: the shared status query revalidates API-key setup when its settings tab returns.
201
+ refetchOnWindowFocus: true,
202
+ },
203
+ );
204
+ const jevConfigured =
205
+ !jevAvailability.isError && jevAvailability.data?.configured === true;
206
+ const jevUnavailable =
207
+ !jevAvailability.isLoading &&
208
+ !jevAvailability.isError &&
209
+ jevAvailability.data?.configured === false;
185
210
  const updateSettings = useManageAiFilter();
186
211
  const updatePreferences = useUpdateSettings();
212
+ const clearAiFilterRules = useClearAiFilterRules();
187
213
  const consolidateAiFilterRules = useConsolidateAiFilterRules();
214
+ const restoreAiFilterRules = useRestoreAiFilterRules();
188
215
  const createRule = useCreateAutomation();
189
216
  const updateRule = useUpdateAutomation();
190
217
  const deleteRule = useDeleteAutomation();
@@ -256,6 +283,7 @@ export function AiFilterSection() {
256
283
  }, [promptRules]);
257
284
 
258
285
  const updateAiSettings = (enabled: boolean) => {
286
+ if (enabled && !jevConfigured) return;
259
287
  updateSettings.mutate(
260
288
  { mode: "settings", settings: { enabled } },
261
289
  {
@@ -269,64 +297,37 @@ export function AiFilterSection() {
269
297
  );
270
298
  };
271
299
 
272
- const savePrompt = async (mode: PromptMode) => {
273
- const condition = promptDrafts[mode].trim();
300
+ const savePrompt = async (mode: PromptMode, clear = false) => {
301
+ const condition = clear ? "" : promptDrafts[mode].trim();
274
302
  const existing = promptRules[mode];
275
- const actions = actionsForMode(mode);
276
303
  if (condition === promptForRules(existing)) return;
304
+ if (!jevConfigured && condition) {
305
+ setPromptDrafts((drafts) => ({
306
+ ...drafts,
307
+ [mode]: promptForRules(existing),
308
+ }));
309
+ return;
310
+ }
311
+ const actions = actionsForMode(mode);
277
312
 
278
- const restoreRules = async (rulesToRestore: AutomationRule[]) => {
279
- const errors: unknown[] = [];
280
- for (const rule of rulesToRestore) {
281
- try {
282
- const restored = await createRule.mutateAsync({
283
- name: rule.name,
284
- condition: rule.condition,
285
- actions: rule.actions,
286
- kind: rule.kind,
287
- domain: rule.domain,
288
- });
289
- if (!rule.enabled) {
290
- await updateRule.mutateAsync({ id: restored.id, enabled: false });
291
- }
292
- } catch (error) {
293
- errors.push(error);
294
- }
295
- }
296
- if (errors.length) {
297
- throw makeAggregateError(
298
- errors,
299
- errors[0] instanceof Error
300
- ? errors[0].message
301
- : t("mail.aiFilter.instructionFailed"),
302
- );
303
- }
304
- };
305
313
  try {
306
314
  if (!condition) {
307
- const removed: AutomationRule[] = [];
308
- try {
309
- for (const rule of existing) {
310
- await deleteRule.mutateAsync(rule.id);
311
- removed.push(rule);
312
- }
313
- } catch (error) {
314
- try {
315
- await restoreRules(removed);
316
- } catch (restoreError) {
317
- throw makeAggregateError(
318
- [error, restoreError],
319
- error instanceof Error
320
- ? error.message
321
- : t("mail.aiFilter.instructionFailed"),
322
- );
323
- }
324
- throw error;
325
- }
315
+ const { undoId } = await clearAiFilterRules.mutateAsync(
316
+ existing.map((rule) => rule.id),
317
+ );
326
318
  toast(t("mail.aiFilter.promptRulesCleared"), {
327
319
  action: {
328
320
  label: t("mail.actions.undo"),
329
- onClick: () => void restoreRules(existing),
321
+ onClick: () => {
322
+ void restoreAiFilterRules
323
+ .mutateAsync(undoId)
324
+ .catch((error) =>
325
+ toast.error(
326
+ actionErrorMessage(error) ??
327
+ t("mail.aiFilter.instructionFailed"),
328
+ ),
329
+ );
330
+ },
330
331
  },
331
332
  });
332
333
  return;
@@ -384,6 +385,7 @@ export function AiFilterSection() {
384
385
  };
385
386
 
386
387
  const saveNewTag = async () => {
388
+ if (!jevConfigured) return;
387
389
  const name = newTagName.trim();
388
390
  const condition = newTagPrompt.trim();
389
391
  if (!name || !condition || savingNewTag) return;
@@ -412,7 +414,7 @@ export function AiFilterSection() {
412
414
  };
413
415
 
414
416
  const saveSuggestedTag = async (nameKey: string, promptKey: string) => {
415
- if (savingSuggestedTag) return;
417
+ if (!jevConfigured || savingSuggestedTag) return;
416
418
  const name = t(nameKey);
417
419
  const condition = t(promptKey);
418
420
  setSavingSuggestedTag(nameKey);
@@ -441,6 +443,7 @@ export function AiFilterSection() {
441
443
  nameDraft: string,
442
444
  conditionDraft: string,
443
445
  ) => {
446
+ if (!jevConfigured) return;
444
447
  const name = nameDraft.trim() || labelForRule(rule);
445
448
  const condition = conditionDraft.trim() || rule.condition;
446
449
  if (!name || !condition) return;
@@ -499,7 +502,7 @@ export function AiFilterSection() {
499
502
  };
500
503
 
501
504
  const reorderTags = async (draggedId: string, targetId: string) => {
502
- if (draggedId === targetId) return;
505
+ if (!jevConfigured || draggedId === targetId) return;
503
506
  const orderedNames = tagRules.map((rule) => labelForRule(rule));
504
507
  const from = tagRules.findIndex((rule) => rule.id === draggedId);
505
508
  const to = tagRules.findIndex((rule) => rule.id === targetId);
@@ -543,9 +546,23 @@ export function AiFilterSection() {
543
546
  checked={state.enabled}
544
547
  onCheckedChange={updateAiSettings}
545
548
  aria-label={t("mail.aiFilter.toggle")}
549
+ disabled={!jevConfigured && !state.enabled}
546
550
  />
547
551
  </div>
548
552
 
553
+ {jevAvailability.isLoading ? (
554
+ <Skeleton className="h-16 w-full" />
555
+ ) : jevAvailability.isError ? (
556
+ <JevAvailabilityError
557
+ onRetry={() => void jevAvailability.refetch()}
558
+ retrying={jevAvailability.isFetching}
559
+ />
560
+ ) : !jevConfigured ? (
561
+ <JevConnectionPrompt
562
+ onConnected={() => void jevAvailability.refetch()}
563
+ />
564
+ ) : null}
565
+
549
566
  <section id="tags" className="space-y-2">
550
567
  <div className="flex items-center justify-between">
551
568
  <div className="flex items-center gap-1.5">
@@ -571,6 +588,7 @@ export function AiFilterSection() {
571
588
  variant="ghost"
572
589
  size="sm"
573
590
  className="h-7 px-2 text-xs"
591
+ disabled={!jevConfigured}
574
592
  onClick={() => {
575
593
  setNewTagOpen(true);
576
594
  setExpandedTagId("new");
@@ -588,6 +606,7 @@ export function AiFilterSection() {
588
606
  key={rule.id}
589
607
  rule={rule}
590
608
  expanded={expandedTagId === rule.id}
609
+ disabled={!jevConfigured}
591
610
  onToggle={() =>
592
611
  setExpandedTagId((current) =>
593
612
  current === rule.id ? null : rule.id,
@@ -599,6 +618,7 @@ export function AiFilterSection() {
599
618
  onDelete={removeTag}
600
619
  onDrop={(event, targetId) => {
601
620
  event.preventDefault();
621
+ if (!jevConfigured) return;
602
622
  void reorderTags(
603
623
  event.dataTransfer.getData("text/plain"),
604
624
  targetId,
@@ -613,6 +633,7 @@ export function AiFilterSection() {
613
633
  value={newTagName}
614
634
  onChange={(event) => setNewTagName(event.target.value)}
615
635
  onBlur={() => void saveNewTag()}
636
+ disabled={!jevConfigured}
616
637
  aria-label={t("mail.aiFilter.tagNamePlaceholder")}
617
638
  placeholder={t("mail.aiFilter.tagNamePlaceholder")}
618
639
  />
@@ -620,6 +641,7 @@ export function AiFilterSection() {
620
641
  value={newTagPrompt}
621
642
  onChange={setNewTagPrompt}
622
643
  onBlur={() => void saveNewTag()}
644
+ disabled={!jevConfigured}
623
645
  label={t("mail.aiFilter.tagPlaceholder")}
624
646
  placeholder={t("mail.aiFilter.tagPlaceholder")}
625
647
  />
@@ -633,6 +655,7 @@ export function AiFilterSection() {
633
655
  value={newTagName}
634
656
  onChange={(event) => setNewTagName(event.target.value)}
635
657
  onBlur={() => void saveNewTag()}
658
+ disabled={!jevConfigured}
636
659
  aria-label={t("mail.aiFilter.tagNamePlaceholder")}
637
660
  placeholder={t("mail.aiFilter.tagNamePlaceholder")}
638
661
  />
@@ -640,6 +663,7 @@ export function AiFilterSection() {
640
663
  value={newTagPrompt}
641
664
  onChange={setNewTagPrompt}
642
665
  onBlur={() => void saveNewTag()}
666
+ disabled={!jevConfigured}
643
667
  label={t("mail.aiFilter.tagPlaceholder")}
644
668
  placeholder={t("mail.aiFilter.tagPlaceholder")}
645
669
  />
@@ -651,7 +675,7 @@ export function AiFilterSection() {
651
675
  key={nameKey}
652
676
  variant="outline"
653
677
  size="sm"
654
- disabled={savingSuggestedTag !== null}
678
+ disabled={!jevConfigured || savingSuggestedTag !== null}
655
679
  onClick={() => void saveSuggestedTag(nameKey, promptKey)}
656
680
  >
657
681
  <IconPlus className="size-3.5" />
@@ -668,15 +692,29 @@ export function AiFilterSection() {
668
692
  id={mode === "important" ? "importance-rules" : `${mode}-rules`}
669
693
  className="space-y-2 scroll-mt-6"
670
694
  >
671
- <h3 className="text-[13px] font-semibold text-foreground">
672
- {mode === "important"
673
- ? t("mail.aiFilter.importantMode")
674
- : mode === "archive"
675
- ? t("mail.aiFilter.skipInboxMode")
676
- : t("mail.aiFilter.spamMode")}
677
- </h3>
695
+ <div className="flex items-center justify-between">
696
+ <h3 className="text-[13px] font-semibold text-foreground">
697
+ {mode === "important"
698
+ ? t("mail.aiFilter.importantMode")
699
+ : mode === "archive"
700
+ ? t("mail.aiFilter.skipInboxMode")
701
+ : t("mail.aiFilter.spamMode")}
702
+ </h3>
703
+ {jevUnavailable && promptRules[mode].length > 0 && (
704
+ <Button
705
+ variant="ghost"
706
+ size="icon"
707
+ className="size-7 text-muted-foreground hover:text-destructive"
708
+ aria-label={t("mail.aiFilter.deleteInstruction")}
709
+ onClick={() => void savePrompt(mode, true)}
710
+ >
711
+ <IconTrash className="size-3.5" />
712
+ </Button>
713
+ )}
714
+ </div>
678
715
  <AiRulePromptField
679
716
  value={promptDrafts[mode]}
717
+ disabled={!jevConfigured}
680
718
  onChange={(value) =>
681
719
  setPromptDrafts((drafts) => ({ ...drafts, [mode]: value }))
682
720
  }
@@ -1,6 +1,7 @@
1
1
  import type { FocusEventHandler, KeyboardEventHandler } from "react";
2
2
 
3
3
  import { Textarea } from "@/components/ui/textarea";
4
+ import { cn } from "@/lib/utils";
4
5
 
5
6
  export function AiRulePromptField({
6
7
  value,
@@ -8,6 +9,7 @@ export function AiRulePromptField({
8
9
  label,
9
10
  placeholder,
10
11
  className,
12
+ disabled,
11
13
  onKeyDown,
12
14
  onBlur,
13
15
  defaultValue,
@@ -18,6 +20,7 @@ export function AiRulePromptField({
18
20
  label: string;
19
21
  placeholder?: string;
20
22
  className?: string;
23
+ disabled?: boolean;
21
24
  onKeyDown?: KeyboardEventHandler<HTMLTextAreaElement>;
22
25
  onBlur?: FocusEventHandler<HTMLTextAreaElement>;
23
26
  }) {
@@ -27,10 +30,14 @@ export function AiRulePromptField({
27
30
  defaultValue={defaultValue}
28
31
  onChange={(event) => onChange?.(event.target.value)}
29
32
  placeholder={placeholder}
33
+ disabled={disabled}
30
34
  onKeyDown={onKeyDown}
31
35
  onBlur={onBlur}
32
36
  aria-label={label}
33
- className={className ?? "min-h-28 resize-none text-sm"}
37
+ className={cn(
38
+ "placeholder:text-muted-foreground",
39
+ className ?? "min-h-28 resize-none text-sm",
40
+ )}
34
41
  maxLength={2_000}
35
42
  />
36
43
  );
@@ -0,0 +1,171 @@
1
+ import { useT } from "@agent-native/core/client/i18n";
2
+ import { buildSettingsRoute } from "@agent-native/core/client/navigation";
3
+ import {
4
+ BuilderConnectPopover,
5
+ useBuilderConnectFlow,
6
+ } from "@agent-native/core/client/settings";
7
+ import { useState } from "react";
8
+ import { Link } from "react-router";
9
+
10
+ import { Button } from "@/components/ui/button";
11
+ import {
12
+ Dialog,
13
+ DialogContent,
14
+ DialogHeader,
15
+ DialogTitle,
16
+ DialogTrigger,
17
+ } from "@/components/ui/dialog";
18
+ import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
19
+
20
+ export function JevConnectionPrompt({
21
+ variant = "row",
22
+ onConnected,
23
+ disabled = false,
24
+ showHeading = true,
25
+ }: {
26
+ variant?: "row" | "trigger" | "menu-item";
27
+ onConnected?: () => void;
28
+ disabled?: boolean;
29
+ showHeading?: boolean;
30
+ }) {
31
+ const t = useT();
32
+ const [open, setOpen] = useState(false);
33
+ const flow = useBuilderConnectFlow({
34
+ trackingSource: "mail_jev_triage_setup",
35
+ trackingFlow: "connect_jev",
36
+ onConnected,
37
+ });
38
+
39
+ const actions = (
40
+ <div className="flex shrink-0 items-center gap-2">
41
+ <BuilderConnectPopover flow={flow}>
42
+ <Button
43
+ type="button"
44
+ disabled={flow.connecting}
45
+ aria-busy={flow.connecting}
46
+ className="h-8 px-3 text-xs"
47
+ >
48
+ {flow.connecting
49
+ ? t("mail.accounts.connecting")
50
+ : t("mail.aiFilter.connectBuilder")}
51
+ </Button>
52
+ </BuilderConnectPopover>
53
+ <Link
54
+ to={buildSettingsRoute("keys:secrets:JEV_API_KEY")}
55
+ target="_blank"
56
+ rel="noreferrer"
57
+ className="whitespace-nowrap text-xs font-medium text-muted-foreground underline-offset-4 hover:text-foreground hover:underline"
58
+ >
59
+ {t("mail.aiFilter.addJevApiKey")}
60
+ </Link>
61
+ </div>
62
+ );
63
+
64
+ const dialogContent = (
65
+ <DialogContent className="sm:max-w-[420px]">
66
+ <DialogHeader>
67
+ <DialogTitle>{t("mail.aiFilter.connectJev")}</DialogTitle>
68
+ </DialogHeader>
69
+ <p className="text-sm text-muted-foreground">
70
+ {t("mail.aiFilter.freeBuilderOrApiKey")}
71
+ </p>
72
+ {actions}
73
+ {flow.error ? (
74
+ <p className="text-xs text-destructive">{flow.error}</p>
75
+ ) : null}
76
+ </DialogContent>
77
+ );
78
+
79
+ if (variant === "menu-item") {
80
+ return (
81
+ <Dialog open={open} onOpenChange={setOpen}>
82
+ <DropdownMenuItem
83
+ onSelect={(event) => {
84
+ event.preventDefault();
85
+ setOpen(true);
86
+ }}
87
+ disabled={disabled || flow.connecting}
88
+ className="justify-between"
89
+ >
90
+ {t("mail.sort.priority")}
91
+ <span className="text-xs text-muted-foreground">
92
+ {t("mail.aiFilter.connectJev")}
93
+ </span>
94
+ </DropdownMenuItem>
95
+ {dialogContent}
96
+ </Dialog>
97
+ );
98
+ }
99
+
100
+ if (variant === "trigger") {
101
+ return (
102
+ <Dialog open={open} onOpenChange={setOpen}>
103
+ <DialogTrigger asChild>
104
+ <Button
105
+ type="button"
106
+ variant="outline"
107
+ size="sm"
108
+ disabled={disabled || flow.connecting}
109
+ className="h-7 px-2 text-[11px]"
110
+ >
111
+ {t("mail.aiFilter.connectJev")}
112
+ </Button>
113
+ </DialogTrigger>
114
+ {dialogContent}
115
+ </Dialog>
116
+ );
117
+ }
118
+
119
+ return (
120
+ <div className="flex flex-col gap-2 rounded-lg border border-border/60 bg-muted/10 px-3 py-2.5 sm:flex-row sm:items-center sm:justify-between">
121
+ <div className="min-w-0">
122
+ {showHeading ? (
123
+ <p className="text-sm font-medium text-foreground">
124
+ {t("mail.aiFilter.connectJevToRunTriage")}
125
+ </p>
126
+ ) : null}
127
+ <p className="text-xs text-muted-foreground">
128
+ {t("mail.aiFilter.freeBuilderOrApiKey")}
129
+ </p>
130
+ </div>
131
+ {actions}
132
+ {flow.error ? (
133
+ <p className="basis-full text-xs text-destructive">{flow.error}</p>
134
+ ) : null}
135
+ </div>
136
+ );
137
+ }
138
+
139
+ export function JevAvailabilityError({
140
+ onRetry,
141
+ retrying = false,
142
+ showMessage = true,
143
+ }: {
144
+ onRetry: () => void;
145
+ retrying?: boolean;
146
+ showMessage?: boolean;
147
+ }) {
148
+ const t = useT();
149
+ return (
150
+ <div
151
+ role="alert"
152
+ className="flex items-center justify-between gap-3 rounded-lg border border-border/60 px-3 py-2.5"
153
+ >
154
+ {showMessage ? (
155
+ <p className="text-sm text-muted-foreground">
156
+ {t("mail.aiFilter.jevAvailabilityFailed")}
157
+ </p>
158
+ ) : null}
159
+ <Button
160
+ type="button"
161
+ variant="ghost"
162
+ size="sm"
163
+ disabled={retrying}
164
+ aria-busy={retrying}
165
+ onClick={onRetry}
166
+ >
167
+ {t("mail.error.tryAgain")}
168
+ </Button>
169
+ </div>
170
+ );
171
+ }
@@ -112,6 +112,30 @@ export function useConsolidateAiFilterRules() {
112
112
  });
113
113
  }
114
114
 
115
+ export function useClearAiFilterRules() {
116
+ const qc = useQueryClient();
117
+ return useMutation({
118
+ mutationFn: (ids: string[]) =>
119
+ callAction("manage-ai-filter-rule-undo", {
120
+ operation: "clear",
121
+ ids,
122
+ }) as Promise<{ undoId: string }>,
123
+ onSettled: () => qc.invalidateQueries({ queryKey: ["automations"] }),
124
+ });
125
+ }
126
+
127
+ export function useRestoreAiFilterRules() {
128
+ const qc = useQueryClient();
129
+ return useMutation({
130
+ mutationFn: (undoId: string) =>
131
+ callAction("manage-ai-filter-rule-undo", {
132
+ operation: "undo",
133
+ undoId,
134
+ }) as Promise<{ restored: true }>,
135
+ onSettled: () => qc.invalidateQueries({ queryKey: ["automations"] }),
136
+ });
137
+ }
138
+
115
139
  export function useTriggerAutomations() {
116
140
  return useMutation({
117
141
  mutationFn: () =>
@@ -42,13 +42,13 @@ const messages = {
42
42
  aiSetupTagLabel: "Create an AI tag",
43
43
  aiSetupImportanceLabel: "Important mail",
44
44
  aiSetupSpamLabel: "Spam and unwanted mail",
45
- aiSetupArchiveLabel: "Auto-archive",
45
+ aiSetupArchiveLabel: "Skip inbox",
46
46
  aiSetupSave: "Save setup",
47
47
  aiSetupSkip: "Skip for now",
48
48
  aiSetupImportantHeadline: "What’s important",
49
49
  aiSetupSkipInboxHeadline: "What can skip your inbox",
50
50
  aiSetupTagsHeadline: "Pick your tabs",
51
- aiSetupArchiveSpamHeadline: "Auto-archive and Spam",
51
+ aiSetupArchiveSpamHeadline: "Skip inbox and Spam",
52
52
  aiSetupTagReceipts: "Receipts",
53
53
  aiSetupTagUpdates: "Product updates",
54
54
  aiSetupTagGitHub: "GitHub humans",
@@ -595,6 +595,12 @@ const messages = {
595
595
  tagTabsHelp: "Each tag becomes an inbox tab",
596
596
  addTag: "Add tag",
597
597
  triageTitle: "Triage",
598
+ connectJev: "Connect Jev",
599
+ connectJevToRunTriage: "Connect Jev to run triage",
600
+ freeBuilderOrApiKey: "Free with Builder.io, or add an API key.",
601
+ jevAvailabilityFailed: "Could not check whether Jev is available.",
602
+ connectBuilder: "Connect Builder.io",
603
+ addJevApiKey: "Add API key",
598
604
  },
599
605
  draftQueue: {
600
606
  title: "Draft queue",
@@ -366,11 +366,14 @@ export function InboxPage() {
366
366
  {},
367
367
  {
368
368
  enabled: view === "inbox" || navState.command.data?.sort === "priority",
369
- staleTime: 60_000,
369
+ staleTime: 0,
370
+ // request-storm-allow: the shared status query revalidates API-key setup when its settings tab returns.
371
+ refetchOnWindowFocus: true,
370
372
  retry: 2,
371
373
  },
372
374
  );
373
- const jevConfigured = jevAvailability.data?.configured === true;
375
+ const jevConfigured =
376
+ !jevAvailability.isError && jevAvailability.data?.configured === true;
374
377
  const showPrioritySort =
375
378
  jevConfigured || (jevAvailability.isError && sortMode === "priority");
376
379
  const changeSortMode = useCallback((mode: MailSortMode) => {
@@ -1199,6 +1202,13 @@ export function InboxPage() {
1199
1202
  isFetchNextPageError={isFetchNextPageError}
1200
1203
  sortMode={sortMode}
1201
1204
  showPrioritySort={showPrioritySort}
1205
+ jevConfigured={jevConfigured}
1206
+ jevAvailabilityLoading={
1207
+ jevAvailability.isLoading || jevAvailability.isFetching
1208
+ }
1209
+ jevAvailabilityError={jevAvailability.isError}
1210
+ onJevConnected={() => void jevAvailability.refetch()}
1211
+ onJevRetry={() => void jevAvailability.refetch()}
1202
1212
  onSortModeChange={changeSortMode}
1203
1213
  />
1204
1214
  )}
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-09-25
4
+ ---
5
+
6
+ Mail now guides users to connect Jev before setting up triage and lets them remove existing rules if Jev becomes unavailable.