@remit/web-client 0.0.321 → 0.0.323

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 (190) hide show
  1. package/package.json +4 -4
  2. package/src/auth/SignInAgain.tsx +18 -0
  3. package/src/auth/amplify-config.test.ts +1 -107
  4. package/src/auth/auth-interceptor.test.ts +0 -6
  5. package/src/auth/cognito/cognito-token.test.ts +2 -4
  6. package/src/components/calendar/AgendaStrip.tsx +1 -1
  7. package/src/components/calendar/CalendarWorkspace.tsx +1 -1
  8. package/src/components/calendar/ChangeCalendarSuggestion.tsx +1 -1
  9. package/src/components/calendar/OpenCalendarEvent.tsx +1 -18
  10. package/src/components/compose/ComposeForm.tsx +11 -6
  11. package/src/components/compose/FullCompose.tsx +1 -1
  12. package/src/components/compose/MobileComposeSheet.tsx +1 -2
  13. package/src/components/compose/compose.stories.tsx +18 -0
  14. package/src/components/compose/draft-discard-failure.render.test.ts +138 -0
  15. package/src/components/config-import/config-import.fixtures.ts +8 -9
  16. package/src/components/config-import/config-import.stories.tsx +2 -2
  17. package/src/components/layout/MailTopBar.tsx +3 -0
  18. package/src/components/mail/BriefPane.tsx +0 -3
  19. package/src/components/mail/ConversationView.tsx +1 -3
  20. package/src/components/mail/DailyBrief.tsx +5 -28
  21. package/src/components/mail/FlaggedList.tsx +3 -4
  22. package/src/components/mail/FlaggedPane.tsx +0 -3
  23. package/src/components/mail/IntelligenceDrawer.tsx +0 -3
  24. package/src/components/mail/IntelligencePane.render.test.ts +0 -1
  25. package/src/components/mail/IntelligencePane.test.ts +1 -95
  26. package/src/components/mail/IntelligencePane.tsx +3 -93
  27. package/src/components/mail/MailListHeader.tsx +12 -43
  28. package/src/components/mail/MailNav.tsx +6 -2
  29. package/src/components/mail/MailViewChrome.tsx +1 -11
  30. package/src/components/mail/MailboxPane.tsx +4 -34
  31. package/src/components/mail/MessageActionMenu.tsx +6 -6
  32. package/src/components/mail/MessageAttachments.tsx +35 -0
  33. package/src/components/mail/MessageBody.tsx +1 -0
  34. package/src/components/mail/MessageBodyErrorBanner.test.ts +0 -30
  35. package/src/components/mail/MessageCard.tsx +16 -4
  36. package/src/components/mail/MessageList.run-reporting.test.ts +2 -46
  37. package/src/components/mail/MessageList.tsx +23 -11
  38. package/src/components/mail/MessageRow.render.test.ts +0 -7
  39. package/src/components/mail/MoveToTrigger.tsx +1 -1
  40. package/src/components/mail/RawMessageView.tsx +1 -1
  41. package/src/components/mail/SelectionWizardHost.search-entry.test.ts +0 -1
  42. package/src/components/mail/SelectionWizardHost.tsx +16 -102
  43. package/src/components/mail/authenticity-warning.stories.tsx +0 -21
  44. package/src/components/mail/daily-brief.stories.tsx +31 -17
  45. package/src/components/mail/enter-cursor-late-open.render.test.ts +197 -0
  46. package/src/components/mail/intelligence-drawer.stories.tsx +0 -2
  47. package/src/components/mail/message-gone.stories.tsx +44 -0
  48. package/src/components/mail/offer-times-reply.render.test.ts +25 -1
  49. package/src/components/mail/reclassify-remove-override.render.test.ts +2 -6
  50. package/src/components/overlay-escape.render.test.ts +7 -3
  51. package/src/components/settings/AccountFormPanel.render.test.ts +26 -3
  52. package/src/components/settings/AccountFormPanel.test.ts +1 -36
  53. package/src/components/settings/AccountFormPanel.tsx +5 -25
  54. package/src/components/settings/AccountServices.tsx +1 -1
  55. package/src/components/settings/AdvancedNavIcon.tsx +3 -3
  56. package/src/components/settings/CalendarFeedCard.tsx +2 -1
  57. package/src/components/settings/CalendarSubscribeCard.tsx +1 -1
  58. package/src/components/settings/CalendarSubscriptionStatus.tsx +1 -2
  59. package/src/components/settings/FilterActionError.tsx +1 -1
  60. package/src/components/settings/FilterEditor.render.test.ts +3 -27
  61. package/src/components/settings/FilterEditor.tsx +4 -24
  62. package/src/components/settings/FilterEditorSurface.tsx +0 -3
  63. package/src/components/settings/FiltersList.render.test.ts +5 -24
  64. package/src/components/settings/FiltersList.stories.tsx +0 -1
  65. package/src/components/settings/FiltersList.tsx +10 -35
  66. package/src/components/settings/WarningsNavIcon.tsx +12 -0
  67. package/src/components/settings/{QuarantinePanel.delimiter.test.ts → WarningsPanel.dom.test.ts} +64 -5
  68. package/src/components/settings/WarningsPanel.render.test.ts +56 -0
  69. package/src/components/settings/WarningsPanel.tsx +244 -0
  70. package/src/components/settings/accounts.stories.tsx +18 -0
  71. package/src/components/settings/advanced.stories.tsx +3 -125
  72. package/src/components/settings/settings-filter.stories.tsx +2 -42
  73. package/src/components/settings/warnings.stories.tsx +290 -0
  74. package/src/components/ui/ErrorBannerProvider.tsx +5 -3
  75. package/src/components/ui/error-banners.test.ts +1 -22
  76. package/src/components/ui/error-banners.ts +9 -20
  77. package/src/hooks/calendar/useCalendarCollectionWrites.ts +2 -1
  78. package/src/hooks/calendar/useCalendarSuggestions.ts +2 -1
  79. package/src/hooks/calendar/useCalendarWrites.ts +2 -1
  80. package/src/hooks/intelligence-calendar.render.test.ts +0 -1
  81. package/src/hooks/useDeleteMessages.test.ts +1 -42
  82. package/src/hooks/useFilters.test.ts +0 -8
  83. package/src/hooks/useFollowFocusOpen.render.test.ts +1 -11
  84. package/src/hooks/useIntelligenceData.test.ts +9 -47
  85. package/src/hooks/useIntelligenceData.ts +7 -100
  86. package/src/hooks/useLayoutTier.test.ts +0 -48
  87. package/src/hooks/useReportSpam.ts +1 -1
  88. package/src/hooks/useRescueCandidates.test.ts +0 -23
  89. package/src/hooks/useRulePreview.ts +8 -7
  90. package/src/hooks/useSearchTokenContext.ts +2 -3
  91. package/src/hooks/useStaleAccountSync.test.ts +0 -17
  92. package/src/hooks/useStarredThreads.test.ts +0 -10
  93. package/src/hooks/useTriggerSync.test.ts +0 -6
  94. package/src/hooks/useVisualViewport.render.test.ts +63 -0
  95. package/src/hooks/useWarningsAttention.ts +37 -0
  96. package/src/lib/account-order.test.ts +0 -10
  97. package/src/lib/adjacent-message.test.ts +0 -9
  98. package/src/lib/app-info.ts +0 -3
  99. package/src/lib/autodiscovery.test.ts +0 -47
  100. package/src/lib/bug-report.test.ts +38 -0
  101. package/src/lib/bug-report.ts +10 -7
  102. package/src/lib/bulk-action-copy.ts +1 -1
  103. package/src/lib/calendar-route.test.ts +0 -5
  104. package/src/lib/config-import.test.ts +3 -39
  105. package/src/lib/dedupe-thread-messages.test.ts +0 -4
  106. package/src/lib/desktop-breakpoint.test.ts +0 -8
  107. package/src/lib/drafts.test.ts +0 -23
  108. package/src/lib/fatal-error.test.ts +0 -19
  109. package/src/lib/folder-roles.test.ts +0 -4
  110. package/src/lib/hot-sync-window.test.ts +0 -5
  111. package/src/lib/list-header-chrome.ts +1 -1
  112. package/src/lib/mail-context.ts +1 -1
  113. package/src/lib/message-attachments.test.ts +77 -0
  114. package/src/lib/message-attachments.ts +31 -15
  115. package/src/lib/message-body-source.test.ts +21 -4
  116. package/src/lib/message-body-source.ts +5 -2
  117. package/src/lib/onboarding-completion.test.ts +0 -16
  118. package/src/lib/organize/filter-edit-model.test.ts +2 -32
  119. package/src/lib/organize/filter-edit-model.ts +6 -16
  120. package/src/lib/organize/organize-model.test.ts +7 -40
  121. package/src/lib/organize/organize-model.ts +16 -18
  122. package/src/lib/organize/rule-model.test.ts +24 -202
  123. package/src/lib/organize/rule-model.ts +19 -166
  124. package/src/lib/organize/search-to-rule.test.ts +2 -36
  125. package/src/lib/organize/search-to-rule.ts +1 -16
  126. package/src/lib/provider-presets.test.ts +1 -77
  127. package/src/lib/query-error-handler.test.ts +0 -61
  128. package/src/lib/query-escalation.integration.test.ts +4 -4
  129. package/src/lib/result-count.ts +1 -1
  130. package/src/lib/result-folder.ts +1 -1
  131. package/src/lib/rum-adapter.test.ts +0 -104
  132. package/src/lib/search-query.test.ts +0 -11
  133. package/src/lib/search-result.test.ts +2 -142
  134. package/src/lib/search-result.ts +3 -73
  135. package/src/lib/search-scope.test.ts +1 -92
  136. package/src/lib/search-scope.ts +0 -29
  137. package/src/lib/search-surface.ts +2 -2
  138. package/src/lib/search-token-index.test.ts +0 -8
  139. package/src/lib/search-tokens.ts +15 -16
  140. package/src/lib/starred-rows.test.ts +0 -4
  141. package/src/lib/sync-warning-entries.ts +33 -0
  142. package/src/lib/wizard-selection.ts +1 -1
  143. package/src/mocks/story-frame/mail-handlers.ts +12 -8
  144. package/src/mocks/story-frame/mail-world.ts +5 -0
  145. package/src/routeTree.gen.ts +21 -0
  146. package/src/routes/calendar/$view.$date/index.tsx +1 -1
  147. package/src/routes/calendar.tsx +2 -1
  148. package/src/routes/mail/brief.tsx +1 -1
  149. package/src/routes/mail/flagged.tsx +1 -1
  150. package/src/routes/mail.tsx +5 -3
  151. package/src/routes/settings/-accounts.test.ts +0 -5
  152. package/src/routes/settings/-filters.test.ts +0 -1
  153. package/src/routes/settings/-shared/help-copy.tsx +15 -0
  154. package/src/routes/settings/accounts.tsx +1 -1
  155. package/src/routes/settings/advanced.tsx +1 -4
  156. package/src/routes/settings/filters.tsx +1 -2
  157. package/src/routes/settings/folders.tsx +1 -1
  158. package/src/routes/settings/labels.tsx +1 -1
  159. package/src/routes/settings/senders.tsx +1 -1
  160. package/src/routes/settings/warnings.tsx +36 -0
  161. package/src/routes/settings.tsx +7 -0
  162. package/src/routing/wizard-history.test.ts +1 -45
  163. package/src/runtime-config.ts +5 -0
  164. package/src/test-support/fixtures.ts +1 -1
  165. package/src/components/compose/draft-discard-banner.test.ts +0 -76
  166. package/src/components/mail/ReadingPaneEmpty.render.test.ts +0 -42
  167. package/src/components/mail/SpamRescue.render.test.ts +0 -35
  168. package/src/components/mail/intelligence-semantic-off.render.test.ts +0 -111
  169. package/src/components/mail/selection-surface-rollcall.test.ts +0 -270
  170. package/src/components/settings/QuarantinePanel.render.test.ts +0 -71
  171. package/src/components/settings/QuarantinePanel.tsx +0 -178
  172. package/src/components/ui/DropdownMenu.tsx +0 -99
  173. package/src/components/ui/EmptyState.tsx +0 -11
  174. package/src/components/ui/ErrorBanner.render.test.ts +0 -104
  175. package/src/components/ui/ErrorBanner.stories.tsx +0 -132
  176. package/src/components/ui/ErrorBanner.tsx +0 -106
  177. package/src/components/ui/ErrorBannerStack.tsx +0 -35
  178. package/src/components/ui/ErrorState.render.test.ts +0 -82
  179. package/src/components/ui/ErrorState.tsx +0 -80
  180. package/src/components/ui/KeyboardShortcutsModal.tsx +0 -113
  181. package/src/hooks/useMoveMessages.test.ts +0 -63
  182. package/src/hooks/useOrganizeWiden.test.ts +0 -217
  183. package/src/hooks/useOrganizeWiden.ts +0 -112
  184. package/src/hooks/useSemanticSearch.ts +0 -132
  185. package/src/hooks/useSemanticSearchEnabled.ts +0 -23
  186. package/src/hooks/useVisualViewport.test.ts +0 -126
  187. package/src/lib/organize/sender-fallback.test.ts +0 -52
  188. package/src/lib/organize/sender-fallback.ts +0 -38
  189. package/src/lib/rescue-telemetry.test.ts +0 -75
  190. package/src/router.test.ts +0 -81
@@ -109,7 +109,6 @@ import { useLayoutTier } from "@/hooks/useLayoutTier";
109
109
  import { useIsDesktop } from "@/hooks/useMediaQuery";
110
110
  import { useRefreshControl } from "@/hooks/useRefreshControl";
111
111
  import { useSearchTokenContext } from "@/hooks/useSearchTokenContext";
112
- import { useSemanticSearch } from "@/hooks/useSemanticSearch";
113
112
  import { useSpamMatchCounts } from "@/hooks/useSpamMatchCounts";
114
113
  import type { TriageContextUpdate } from "@/hooks/useTriageLayer";
115
114
  import { sortAccountsByCreatedAt } from "@/lib/account-order";
@@ -132,7 +131,7 @@ import { useMailContext } from "@/lib/mail-context";
132
131
  import { useMailFreshness } from "@/lib/mail-freshness";
133
132
  import { shouldRequestResultCount } from "@/lib/result-count";
134
133
  import { junkMailboxIds } from "@/lib/result-folder";
135
- import { relatedSearchResults, rowToSearchResult } from "@/lib/search-result";
134
+ import { rowToSearchResult } from "@/lib/search-result";
136
135
  import { showInlineSearchResults } from "@/lib/search-surface";
137
136
  import { parseSearchTokens } from "@/lib/search-tokens";
138
137
  import { resolveSelectionAccountScope } from "@/lib/selection-account-scope";
@@ -320,7 +319,7 @@ function BriefSelectionChrome({
320
319
  [selectedIds],
321
320
  );
322
321
  // The ticked rows as the wizard reads them — the sample under every screen
323
- // that names a match, and the senders its widen falls back to.
322
+ // that names a match, and the senders its properties prefill reads.
324
323
  const wizardSelection = useMemo(
325
324
  () => wizardSelectionFrom(rows, selectedIds),
326
325
  [rows, selectedIds],
@@ -820,8 +819,7 @@ export function DailyBrief({
820
819
  [filteredRows, onOpenThread],
821
820
  );
822
821
 
823
- // The two-engine results panel, whose semantic hits are in no list at all —
824
- // each one carries the thread it belongs to.
822
+ // The results panel; each result carries the thread it belongs to.
825
823
  const openResult = useCallback(
826
824
  (result: SearchResult) => {
827
825
  const threadId =
@@ -894,25 +892,6 @@ export function DailyBrief({
894
892
  [filteredRows, resultFolderIndex],
895
893
  );
896
894
 
897
- // "Related" (semantic) spans every account here — the brief is the
898
- // cross-account view, so no mailbox scope. Dedupe against the literal "Top
899
- // matches" by thread; the brief rows key on messageId, so resolve each back to
900
- // its thread via the raw threads.
901
- const { hits: semanticHits, isLoading: relatedLoading } = useSemanticSearch();
902
- const relatedResults = useMemo<SearchResult[]>(() => {
903
- const threadByMessageId = new Map<string, string | undefined>(
904
- filteredRows.map((row) => [row.id, row.threadId]),
905
- );
906
- const literalThreadIds = searchResults
907
- .map((result) => threadByMessageId.get(result.id))
908
- .filter((id): id is string => id != null);
909
- return relatedSearchResults(
910
- semanticHits,
911
- literalThreadIds,
912
- resultFolderIndex,
913
- );
914
- }, [semanticHits, searchResults, filteredRows, resultFolderIndex]);
915
-
916
895
  const filterConfig = useMemo<Omit<FilterSheetProps, "children">>(() => {
917
896
  const preset = briefFilterConfig(
918
897
  accountSources.map((s) => ({
@@ -991,7 +970,7 @@ export function DailyBrief({
991
970
 
992
971
  // A search does not take the panel down — the chips compose into the query
993
972
  // rather than competing with it. The one window where the brief's own body is
994
- // not on screen is the two-engine results panel, which owns the pane while a
973
+ // not on screen is the results panel, which owns the pane while a
995
974
  // first query is still being typed; the caret stands down for exactly that,
996
975
  // on the same answer the header swaps the body on.
997
976
  const resultsPanelOwnsBody = showInlineSearchResults({
@@ -1083,8 +1062,6 @@ export function DailyBrief({
1083
1062
  searchFilter: filterConfig,
1084
1063
  searchResults,
1085
1064
  searchLoading: isLoading,
1086
- relatedResults,
1087
- relatedLoading,
1088
1065
  onSelectSearchResult: openResult,
1089
1066
  // The read-only results panel holds spam out of the same
1090
1067
  // committed search this body does, so it states the same
@@ -1093,7 +1070,7 @@ export function DailyBrief({
1093
1070
  // The body already narrows to the committed query (the server
1094
1071
  // `query` on every section request, plus `matchesSearchTokens` for
1095
1072
  // the residue), so a committed search is a selectable list here
1096
- // exactly as it is on the mailbox route (#212) — the two-engine
1073
+ // exactly as it is on the mailbox route (#212) — the results
1097
1074
  // panel stays for the typing/uncommitted state only.
1098
1075
  searchResultsInBody: true,
1099
1076
  refreshControl,
@@ -28,6 +28,7 @@
28
28
  */
29
29
  import {
30
30
  flaggedFilterConfig,
31
+ formatErrorMessage,
31
32
  type MessageListFilter,
32
33
  MessageListPane,
33
34
  type SearchResult,
@@ -35,7 +36,6 @@ import {
35
36
  type Verb,
36
37
  } from "@remit/ui";
37
38
  import { type RefObject, useCallback, useMemo, useState } from "react";
38
- import { formatErrorMessage } from "@/components/ui/ErrorState";
39
39
  import { useIsDesktop } from "@/hooks/useMediaQuery";
40
40
  import { useSearchTokenContext } from "@/hooks/useSearchTokenContext";
41
41
  import {
@@ -217,8 +217,7 @@ export function FlaggedList({
217
217
  [rows, onOpenThread],
218
218
  );
219
219
 
220
- // The two-engine results panel, whose semantic hits are in no list at all —
221
- // each one carries the thread it belongs to.
220
+ // The results panel; each result carries the thread it belongs to.
222
221
  const openResult = useCallback(
223
222
  (result: SearchResult) => {
224
223
  const threadId =
@@ -316,7 +315,7 @@ export function FlaggedList({
316
315
  // A committed search renders in this view's own rows (`rows` already
317
316
  // narrows to the query above), so the multi-select toolbar stays
318
317
  // reachable exactly as it does on the mailbox route (#212). The
319
- // two-engine panel stays for the typing/uncommitted state only.
318
+ // results panel stays for the typing/uncommitted state only.
320
319
  searchResultsInBody
321
320
  >
322
321
  <ThreadListInteraction
@@ -377,7 +377,6 @@ function FlaggedReading() {
377
377
  isOpen={intelligence.drawerOpen}
378
378
  onClose={intelligence.closeDrawer}
379
379
  thread={selectedThread}
380
- mailboxId={selectedThread?.mailboxId}
381
380
  accountId={selectedThread?.accountId}
382
381
  onAfterOptimisticRemove={handleDeselectIfRemoved}
383
382
  onReplyWithText={replyWithText}
@@ -401,7 +400,6 @@ function FlaggedIntelligence() {
401
400
  <IntelligencePane
402
401
  onClose={onToggleIntelligence}
403
402
  thread={selectedThread}
404
- mailboxId={selectedThread?.mailboxId}
405
403
  accountId={selectedThread?.accountId}
406
404
  onAfterOptimisticRemove={handleDeselectIfRemoved}
407
405
  onReplyWithText={replyWithText}
@@ -445,7 +443,6 @@ function FlaggedPhone() {
445
443
  isOpen={drawer.isOpen}
446
444
  onClose={drawer.close}
447
445
  thread={selectedThread}
448
- mailboxId={selectedThread?.mailboxId}
449
446
  accountId={selectedThread?.accountId}
450
447
  onAfterOptimisticRemove={handleDeselectIfRemoved}
451
448
  onReplyWithText={replyWithText}
@@ -15,7 +15,6 @@ export function IntelligenceDrawer({
15
15
  isOpen,
16
16
  onClose,
17
17
  thread,
18
- mailboxId,
19
18
  accountId,
20
19
  onAfterOptimisticRemove,
21
20
  onReplyWithText,
@@ -24,7 +23,6 @@ export function IntelligenceDrawer({
24
23
  isOpen: boolean;
25
24
  onClose: () => void;
26
25
  thread?: RemitImapThreadMessageResponse;
27
- mailboxId?: string;
28
26
  accountId?: string;
29
27
  onAfterOptimisticRemove?: (messageIds: string[]) => void;
30
28
  onReplyWithText?: ReplyWithText;
@@ -43,7 +41,6 @@ export function IntelligenceDrawer({
43
41
  <IntelligencePane
44
42
  onClose={onClose}
45
43
  thread={thread}
46
- mailboxId={mailboxId}
47
44
  accountId={accountId}
48
45
  hideCloseButton
49
46
  onAfterOptimisticRemove={onAfterOptimisticRemove}
@@ -34,7 +34,6 @@ const baseData: IntelligenceData = {
34
34
  summary: "This message was signed by example.com.",
35
35
  },
36
36
  category: { value: "personal" },
37
- similar: [],
38
37
  };
39
38
 
40
39
  const ReportHarness = () => {
@@ -1,110 +1,16 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { describe, test } from "node:test";
3
- import { resolveSimilarState, resolveSpamAction } from "./IntelligencePane";
3
+ import { resolveSpamAction } from "./IntelligencePane";
4
4
 
5
5
  describe("resolveSpamAction (#648)", () => {
6
6
  test("offers Report spam when the message carries no spam report", () => {
7
7
  assert.equal(resolveSpamAction({}), "reportSpam");
8
8
  });
9
9
 
10
- test("offers Report spam regardless of which mailbox the message is in", () => {
11
- // A report is never derived from placement — reporting a message the
12
- // provider's own filter already put in Junk is a real, no-op-move case.
13
- assert.equal(resolveSpamAction({ spamReport: undefined }), "reportSpam");
14
- });
15
-
16
10
  test("offers Not spam once the message carries a spam report", () => {
17
11
  assert.equal(
18
12
  resolveSpamAction({ spamReport: { reportedAt: Date.now() } }),
19
13
  "notSpam",
20
14
  );
21
15
  });
22
-
23
- test("the two actions are mutually exclusive", () => {
24
- const reportable = resolveSpamAction({});
25
- const reported = resolveSpamAction({
26
- spamReport: { reportedAt: Date.now() },
27
- });
28
- assert.notEqual(reportable, reported);
29
- });
30
- });
31
-
32
- describe("resolveSimilarState — fail-fast on similar-messages search", () => {
33
- test("a fatal 5xx does NOT show the grey label — it escalates instead (ready)", () => {
34
- assert.equal(
35
- resolveSimilarState({
36
- similarError: new Error("500"),
37
- similarErrorIsFatal: true,
38
- isSimilarLoading: false,
39
- semanticEnabled: true,
40
- }),
41
- "ready",
42
- );
43
- });
44
-
45
- test("a soft (non-fatal) error keeps the muted error state", () => {
46
- assert.equal(
47
- resolveSimilarState({
48
- similarError: new Error("404"),
49
- similarErrorIsFatal: false,
50
- isSimilarLoading: false,
51
- semanticEnabled: true,
52
- }),
53
- "error",
54
- );
55
- });
56
-
57
- test("an empty result (no error) is a soft ready state", () => {
58
- assert.equal(
59
- resolveSimilarState({
60
- similarError: null,
61
- similarErrorIsFatal: false,
62
- isSimilarLoading: false,
63
- semanticEnabled: true,
64
- }),
65
- "ready",
66
- );
67
- });
68
-
69
- test("loading wins when there is no error", () => {
70
- assert.equal(
71
- resolveSimilarState({
72
- similarError: null,
73
- similarErrorIsFatal: false,
74
- isSimilarLoading: true,
75
- semanticEnabled: true,
76
- }),
77
- "loading",
78
- );
79
- });
80
- });
81
-
82
- // Off is a property of the deployment, and it leads: nothing was embedded, so
83
- // there was no query, and the section states the setting instead of an empty
84
- // list (#1068). `undefined` is the config read still in flight, which is not
85
- // an answer.
86
- describe("resolveSimilarState — semantic search off on this instance", () => {
87
- test("off wins over every runtime state", () => {
88
- assert.equal(
89
- resolveSimilarState({
90
- similarError: new Error("404"),
91
- similarErrorIsFatal: false,
92
- isSimilarLoading: true,
93
- semanticEnabled: false,
94
- }),
95
- "off",
96
- );
97
- });
98
-
99
- test("a config read still in flight is not an off state", () => {
100
- assert.equal(
101
- resolveSimilarState({
102
- similarError: null,
103
- similarErrorIsFatal: false,
104
- isSimilarLoading: false,
105
- semanticEnabled: undefined,
106
- }),
107
- "ready",
108
- );
109
- });
110
16
  });
@@ -5,8 +5,6 @@ import {
5
5
  DialogBackdrop,
6
6
  IntelligencePanel,
7
7
  type IntelligenceQuickActions,
8
- type SimilarMessageLinkComponent,
9
- type SimilarState,
10
8
  type ThreadCategory,
11
9
  useModalFocus,
12
10
  } from "@remit/ui";
@@ -19,7 +17,7 @@ import { useUpdateAddressFlags } from "@/hooks/useUpdateAddressFlags";
19
17
  import { isRescueCandidate } from "@/lib/rescue-candidates";
20
18
  import { recordRescueSentToJunk } from "@/lib/rescue-telemetry";
21
19
  import { useTelemetry } from "@/lib/telemetry-context";
22
- import { NavLink, type OpenThreadTarget } from "@/routing";
20
+ import type { OpenThreadTarget } from "@/routing";
23
21
  import type { ReplyWithText } from "../compose/reply-with-times";
24
22
 
25
23
  export interface IntelligencePaneProps {
@@ -30,8 +28,6 @@ export interface IntelligencePaneProps {
30
28
  * the empty-state placeholder.
31
29
  */
32
30
  thread?: RemitImapThreadMessageResponse;
33
- /** The mailbox the message list is currently showing. */
34
- mailboxId?: string;
35
31
  /** Account that owns the open message; scopes mailbox-list cache invalidation after a spam-report mutation. */
36
32
  accountId?: string;
37
33
  /**
@@ -89,39 +85,6 @@ export const resolveSpamAction = (thread: {
89
85
  spamReport?: unknown;
90
86
  }): "notSpam" | "reportSpam" => (thread.spamReport ? "notSpam" : "reportSpam");
91
87
 
92
- /**
93
- * Decide the "Similar messages" section state from the semantic-search query.
94
- *
95
- * An instance with semantic search off leads, before any error or in-flight
96
- * state: nothing is embedded, no query was sent, and the section states the
97
- * setting (#1068). Only `false` counts — the config read has not answered yet
98
- * while it is `undefined`, and an off state shown then would be a guess.
99
- *
100
- * The fail-fast rule: a fatal first-party 5xx must NEVER degrade to the benign
101
- * grey "Similarity search unavailable" label — it escalates to the global red
102
- * overlay instead, so this returns `"ready"` for it (the section then renders
103
- * nothing rather than a misleading soft state). A soft (non-fatal) failure
104
- * keeps the muted `"error"` state; an empty result is `"ready"` (no error).
105
- * Pure so the classification can be unit-tested without rendering.
106
- */
107
- export const resolveSimilarState = (input: {
108
- similarError: unknown;
109
- similarErrorIsFatal: boolean;
110
- isSimilarLoading: boolean;
111
- semanticEnabled: boolean | undefined;
112
- }): SimilarState => {
113
- const {
114
- similarError,
115
- similarErrorIsFatal,
116
- isSimilarLoading,
117
- semanticEnabled,
118
- } = input;
119
- if (semanticEnabled === false) return "off";
120
- if (similarError && !similarErrorIsFatal) return "error";
121
- if (isSimilarLoading) return "loading";
122
- return "ready";
123
- };
124
-
125
88
  function IntelligenceSkeleton() {
126
89
  return (
127
90
  <aside className="flex h-full w-full flex-col bg-surface-sunken">
@@ -250,7 +213,6 @@ function ReclassifyDialog({
250
213
  interface WiredPanelProps {
251
214
  thread: RemitImapThreadMessageResponse;
252
215
  onClose: () => void;
253
- mailboxId?: string;
254
216
  accountId?: string;
255
217
  hideCloseButton?: boolean;
256
218
  onAfterOptimisticRemove?: (messageIds: string[]) => void;
@@ -264,22 +226,13 @@ interface WiredPanelProps {
264
226
  function WiredPanel({
265
227
  thread,
266
228
  onClose,
267
- mailboxId,
268
229
  accountId,
269
230
  hideCloseButton,
270
231
  onAfterOptimisticRemove,
271
232
  onReplyWithText,
272
233
  onOpenThread,
273
234
  }: WiredPanelProps) {
274
- const {
275
- data,
276
- address,
277
- addressId,
278
- isSimilarLoading,
279
- similarError,
280
- similarErrorIsFatal,
281
- semanticEnabled,
282
- } = useIntelligenceData(thread, mailboxId);
235
+ const { data, address, addressId } = useIntelligenceData(thread);
283
236
  const calendar = useIntelligenceCalendar(
284
237
  thread,
285
238
  onReplyWithText,
@@ -317,13 +270,6 @@ function WiredPanel({
317
270
  reportSpam([thread.messageId]);
318
271
  }, [reportSpam, thread, telemetry]);
319
272
 
320
- const handleShowSimilar = useCallback(() => {
321
- // The similar-messages section scrolls into view automatically when
322
- // data.similar is populated. This handler is for the "N similar" button
323
- // inside the authenticity panel; a scroll-to implementation can be added
324
- // in a future pass without breaking the contract.
325
- }, []);
326
-
327
273
  const handleToggleVip = useCallback(() => {
328
274
  const next = !(data?.flags?.vip === true);
329
275
  updateFlags({ vip: { value: next } });
@@ -360,40 +306,10 @@ function WiredPanel({
360
306
  onReportSpam: spamAction === "reportSpam" ? handleReportSpam : undefined,
361
307
  };
362
308
 
363
- // Each similar-message row is a real router anchor — same link shape as a
364
- // message-list row — so it opens the message in the reading pane while
365
- // preserving deep-linking, middle-click, and existing search params.
366
- const similarLinkComponent: SimilarMessageLinkComponent = ({
367
- mailboxId: rowMailboxId,
368
- threadId,
369
- messageId,
370
- className,
371
- ariaLabel,
372
- children,
373
- }) => (
374
- <NavLink
375
- to="/mail/$mailboxId/$threadId/$messageId"
376
- params={{ mailboxId: rowMailboxId, threadId, messageId }}
377
- search={(prev) => prev}
378
- variant="row"
379
- className={className}
380
- aria-label={ariaLabel}
381
- >
382
- {children}
383
- </NavLink>
384
- );
385
-
386
309
  if (!data) {
387
310
  return <IntelligenceSkeleton />;
388
311
  }
389
312
 
390
- const similarState = resolveSimilarState({
391
- similarError,
392
- similarErrorIsFatal,
393
- isSimilarLoading,
394
- semanticEnabled,
395
- });
396
-
397
313
  return (
398
314
  <>
399
315
  <IntelligencePanel
@@ -403,10 +319,7 @@ function WiredPanel({
403
319
  onTabChange={calendar.onTabChange}
404
320
  onClose={onClose}
405
321
  hideCloseButton={hideCloseButton}
406
- onShowSimilar={handleShowSimilar}
407
322
  actions={actions}
408
- similarState={similarState}
409
- similarLinkComponent={similarLinkComponent}
410
323
  reportSpamPending={isReporting}
411
324
  notSpamPending={isRestoring}
412
325
  // No left border: the ResizableHandle to our left already draws the
@@ -434,12 +347,10 @@ function WiredPanel({
434
347
  * - Sender basics + trust + authenticity + category: from the thread row (instant,
435
348
  * already loaded by the message list).
436
349
  * - Address flags (VIP/muted/blocked/etc.) + first-seen: `GET /addresses/search`.
437
- * - Similar messages: `GET /search/semantic` (last; panel still renders on failure).
438
350
  */
439
351
  export const IntelligencePane = ({
440
352
  onClose,
441
353
  thread,
442
- mailboxId,
443
354
  accountId,
444
355
  hideCloseButton,
445
356
  onAfterOptimisticRemove,
@@ -459,7 +370,7 @@ export const IntelligencePane = ({
459
370
  <Sparkles className="size-8 text-fg-subtle" />
460
371
  <p className="mt-3 text-sm text-fg-muted">Intelligence</p>
461
372
  <p className="mt-1 text-2xs text-fg-subtle">
462
- Sender trust, authenticity and similar messages will appear here.
373
+ Sender trust and authenticity will appear here.
463
374
  </p>
464
375
  </div>
465
376
  </aside>
@@ -470,7 +381,6 @@ export const IntelligencePane = ({
470
381
  <WiredPanel
471
382
  thread={thread}
472
383
  onClose={onClose}
473
- mailboxId={mailboxId}
474
384
  accountId={accountId}
475
385
  hideCloseButton={hideCloseButton}
476
386
  onAfterOptimisticRemove={onAfterOptimisticRemove}
@@ -25,13 +25,8 @@
25
25
  * itself so it is there for every search on every view, whichever body is showing
26
26
  * the results.
27
27
  *
28
- * Results split into two sections, one per engine: literal/instant and semantic.
29
- * The consumer dedupes them — a thread in both appears only under the literal
30
- * section — and names them, so a view whose engines reach different mail says so
31
- * ("In Archive" / "Everywhere"). Each loads independently; an empty section
32
- * drops out kit-side, so a semantic-only result still shows when the literal
33
- * search finds nothing. The hamburger opens the nav drawer via the enclosing
34
- * `AppShellSlotted`.
28
+ * Results render as one section; an empty section drops out kit-side. The
29
+ * hamburger opens the nav drawer via the enclosing `AppShellSlotted`.
35
30
  *
36
31
  * This is also the one place the route's scope reaches the results list, so both
37
32
  * tiers agree on it: a global search labels every row with the folder it came
@@ -130,24 +125,16 @@ export interface MailListHeaderProps {
130
125
  /** Literal/instant results — the "Top matches" section. */
131
126
  searchResults?: SearchResult[];
132
127
  searchLoading?: boolean;
133
- /** Semantic results — the "Related" section, deduped against "Top matches". */
134
- relatedResults?: SearchResult[];
135
- relatedLoading?: boolean;
136
128
  onSelectSearchResult?: (result: SearchResult) => void;
137
- /**
138
- * Section headings. A view whose two engines cover different ground names
139
- * them by that ground ("In Archive" / "Everywhere") so the reach of a result
140
- * is on screen rather than inferred.
141
- */
129
+ /** The results section heading. */
142
130
  searchResultsLabel?: string;
143
- relatedResultsLabel?: string;
144
131
  /**
145
132
  * The body renders the committed search itself as a selectable list — the
146
133
  * mailbox route's `MessageList` and the brief and Starred's own rows filter
147
134
  * to the search and host the multi-select toolbar and (on the mailbox route)
148
135
  * the "Select all N matching" escalation (#212). When set, a committed query
149
136
  * keeps the body on tablet/desktop instead of swapping in the read-only
150
- * two-engine `SearchResults` panel; the panel still shows while the query is
137
+ * `SearchResults` panel; the panel still shows while the query is
151
138
  * being typed (uncommitted). A view whose body is not a search-result list
152
139
  * leaves this unset and keeps the panel for every query.
153
140
  */
@@ -231,11 +218,8 @@ export function MailListHeader({
231
218
  searchFilter,
232
219
  searchResults,
233
220
  searchLoading,
234
- relatedResults,
235
- relatedLoading,
236
221
  onSelectSearchResult,
237
222
  searchResultsLabel = "Top matches",
238
- relatedResultsLabel = "Related",
239
223
  searchResultsInBody = false,
240
224
  spamOffer,
241
225
  refreshControl,
@@ -368,25 +352,17 @@ export function MailListHeader({
368
352
  [parsed, searchInput, onSearchChange],
369
353
  );
370
354
  const topMatches = useMemo(() => searchResults ?? [], [searchResults]);
371
- const related = useMemo(() => relatedResults ?? [], [relatedResults]);
372
- // Always offer both sections while a query is present; the kit drops the empty
373
- // ones, so a "Related"-only hit still shows and two empties fall to its empty
374
- // state. The empty-query case (recent searches) is the kit's job.
355
+ // Offer the section while a query is present; the kit drops it when empty and
356
+ // shows its empty state. The empty-query case (recent searches) is the kit's job.
375
357
  const sections = useMemo<SearchResultSection[]>(
376
358
  () =>
377
359
  hasQuery
378
- ? [
379
- { id: "top", label: searchResultsLabel, results: topMatches },
380
- { id: "related", label: relatedResultsLabel, results: related },
381
- ]
360
+ ? [{ id: "top", label: searchResultsLabel, results: topMatches }]
382
361
  : [],
383
- [hasQuery, searchResultsLabel, relatedResultsLabel, topMatches, related],
362
+ [hasQuery, searchResultsLabel, topMatches],
384
363
  );
385
- // Skeleton only while nothing is in yet — once either section has rows, show
386
- // them and let the other arrive (or not). Keeps the two sources independent.
387
- const hasAnyResult = topMatches.length + related.length > 0;
388
- const resultsLoading =
389
- !hasAnyResult && (searchLoading === true || relatedLoading === true);
364
+ // Skeleton only while nothing is in yet.
365
+ const resultsLoading = topMatches.length === 0 && searchLoading === true;
390
366
 
391
367
  // The mailbox's appointed role is what lets a search scoped to Spam show its
392
368
  // rows rather than drop them.
@@ -426,10 +402,7 @@ export function MailListHeader({
426
402
  // could not be turned into. The conversion is computed once, here, and reaches
427
403
  // the wizard through its own context — the reason the affordance gives and the
428
404
  // clauses the wizard seeds from are the same answer, without the per-keystroke
429
- // object riding on the chrome the list reads. The literal filter cannot
430
- // reproduce the search's semantic reach, so the conversion states it whenever
431
- // the search surfaced a "Related" section — a direct signal, read here from
432
- // the semantic results, never a capability probe.
405
+ // object riding on the chrome the list reads.
433
406
  //
434
407
  // It belongs to the search, not to any one way of showing it. The affordance
435
408
  // therefore sits in the pane, above whichever body is up — the read-only
@@ -440,11 +413,7 @@ export function MailListHeader({
440
413
  // to the URL, taking the affordance down with it a few hundred milliseconds
441
414
  // after it appeared. Mounting it here instead keeps it up through that
442
415
  // hand-back on every view.
443
- const searchHadSemanticReach = related.length > 0;
444
- const conversion = useMemo(
445
- () => convertSearchToRule(parsed, { searchHadSemanticReach }),
446
- [parsed, searchHadSemanticReach],
447
- );
416
+ const conversion = useMemo(() => convertSearchToRule(parsed), [parsed]);
448
417
  const searchConversion = hasQuery ? conversion : undefined;
449
418
  // The wizard is a full-screen surface, so the phone takeover it was pressed
450
419
  // from stands down rather than sitting under it — and the list underneath,
@@ -9,11 +9,12 @@
9
9
  * to host them.
10
10
  */
11
11
  import type { RemitImapAccountResponse } from "@remit/api-http-client/types.gen.ts";
12
- import { useAppShellLayout } from "@remit/ui";
12
+ import { AttentionDot, useAppShellLayout } from "@remit/ui";
13
13
  import { LogOut, Settings } from "lucide-react";
14
14
  import { AccountSession } from "@/auth/AccountSession";
15
15
  import { MailSidebarAdapter } from "@/components/mail/MailSidebarAdapter";
16
16
  import { BugReportButton } from "@/components/ui/BugReportButton";
17
+ import { useWarningsAttention } from "@/hooks/useWarningsAttention";
17
18
  import { NavLink } from "@/routing";
18
19
 
19
20
  interface MailNavProps {
@@ -24,6 +25,7 @@ interface MailNavProps {
24
25
  export function MailNav({ accounts, onMailboxSelect }: MailNavProps) {
25
26
  const layout = useAppShellLayout();
26
27
  const isPane = layout?.showNavPane ?? true;
28
+ const settingsAttention = useWarningsAttention();
27
29
 
28
30
  if (isPane) {
29
31
  return <MailSidebarAdapter accounts={accounts} />;
@@ -40,7 +42,9 @@ export function MailNav({ accounts, onMailboxSelect }: MailNavProps) {
40
42
  </div>
41
43
  <div className="border-t border-line px-2 py-2">
42
44
  <NavLink to="/settings/accounts" onClick={onMailboxSelect}>
43
- <Settings className="size-4 shrink-0" />
45
+ <AttentionDot label={settingsAttention}>
46
+ <Settings className="size-4 shrink-0" />
47
+ </AttentionDot>
44
48
  <span className="flex-1 truncate text-left">Settings</span>
45
49
  </NavLink>
46
50
  <BugReportButton />
@@ -46,13 +46,9 @@ interface MailViewChromeProps {
46
46
  /** Literal/instant results — the "Top matches" section. */
47
47
  searchResults?: SearchResult[];
48
48
  searchLoading?: boolean;
49
- /** Semantic results — the "Related" section. */
50
- relatedResults?: SearchResult[];
51
- relatedLoading?: boolean;
52
49
  onSelectSearchResult?: (result: SearchResult) => void;
53
- /** Section headings; see `MailListHeader`. */
50
+ /** Section heading; see `MailListHeader`. */
54
51
  searchResultsLabel?: string;
55
- relatedResultsLabel?: string;
56
52
  /**
57
53
  * The body renders the committed search as a selectable list itself; see
58
54
  * `MailListHeader`. The mailbox route sets this so a committed search yields
@@ -78,11 +74,8 @@ export function MailViewChrome({
78
74
  footer,
79
75
  searchResults,
80
76
  searchLoading,
81
- relatedResults,
82
- relatedLoading,
83
77
  onSelectSearchResult,
84
78
  searchResultsLabel,
85
- relatedResultsLabel,
86
79
  searchResultsInBody,
87
80
  refreshControl,
88
81
  }: MailViewChromeProps) {
@@ -114,11 +107,8 @@ export function MailViewChrome({
114
107
  searchFilter={filterConfig}
115
108
  searchResults={searchResults}
116
109
  searchLoading={searchLoading}
117
- relatedResults={relatedResults}
118
- relatedLoading={relatedLoading}
119
110
  onSelectSearchResult={onSelectSearchResult}
120
111
  searchResultsLabel={searchResultsLabel}
121
- relatedResultsLabel={relatedResultsLabel}
122
112
  searchResultsInBody={searchResultsInBody}
123
113
  refreshControl={refreshControl}
124
114
  >