Skip to content

fix(desktop): make error toast dismissable#108

Merged
nchopra merged 1 commit intomainfrom
dismissable
Feb 27, 2026
Merged

fix(desktop): make error toast dismissable#108
nchopra merged 1 commit intomainfrom
dismissable

Conversation

@nchopra
Copy link
Contributor

@nchopra nchopra commented Feb 26, 2026

Summary\n- add a dismiss (X) button to widget error toasts with a larger hit target\n- add localized dismiss aria-label text for en/es/ja\n- stabilize widget toast interaction by coordinating mouse pass-through with active toast lifecycle\n\n## Validation\n- pnpm -C apps/desktop exec eslint src/renderer/widget/components/WidgetToast.tsx src/renderer/widget/components/ToasterWrapper.tsx src/renderer/widget/hooks/useWidgetNotifications.tsx src/renderer/widget/pages/widget/components/FloatingButton.tsx\n- pnpm -C apps/desktop type:check

Summary by CodeRabbit

Release Notes

  • New Features

    • Added a dismiss button to notifications, allowing users to manually close notifications at any time.
  • Internationalization

    • Added "dismiss" notification label translations in English, Spanish, and Japanese.

@coderabbitai
Copy link

coderabbitai bot commented Feb 26, 2026

📝 Walkthrough

Walkthrough

Added notification dismiss functionality across the desktop application. Translation keys for "dismiss" action added in English, Spanish, and Japanese locales. WidgetToast component now includes a dismiss button. useWidgetNotifications refactored to track active toasts and synchronize pass-through mouse interaction state via a custom event broadcast.

Changes

Cohort / File(s) Summary
Localization
apps/desktop/src/i18n/locales/en.json, apps/desktop/src/i18n/locales/es.json, apps/desktop/src/i18n/locales/ja.json
Added translation key widget.notifications.dismiss with localized values: "Dismiss notification" (EN), "Descartar notificación" (ES), "通知を閉じる" (JA).
Notification UI Components
apps/desktop/src/renderer/widget/components/WidgetToast.tsx, apps/desktop/src/renderer/widget/components/ToasterWrapper.tsx
WidgetToast extended with onDismiss callback and dismiss button (top-right) labeled via translation. ToasterWrapper simplified by removing mouse enter/leave handling and debounce logic, becoming a pure positioning wrapper.
Notification State Management
apps/desktop/src/renderer/widget/hooks/useWidgetNotifications.tsx
Introduced active toast tracking via ref and sync function that manages pass-through mute state based on toast activity. Added showNotificationToast helper and lifecycle handlers for onDismiss/onAutoClose events. Dispatches TOAST_INTERACTION_STATE_EVENT custom event to broadcast toast activity state.
Mouse Interaction Awareness
apps/desktop/src/renderer/widget/pages/widget/components/FloatingButton.tsx
Added event listener for toast interaction state; skips re-enabling mouse passthrough when a toast is active, improving interaction coordination.

Sequence Diagram

sequenceDiagram
    participant User
    participant WidgetToast as WidgetToast<br/>(UI)
    participant useWidgetNotifications as useWidgetNotifications<br/>(Hook)
    participant Toast as Toast Library
    participant FloatingButton as FloatingButton<br/>(Component)

    User->>WidgetToast: Click dismiss button
    WidgetToast->>useWidgetNotifications: Call onDismiss()
    useWidgetNotifications->>Toast: Remove toast by ID
    useWidgetNotifications->>useWidgetNotifications: Remove toastId from<br/>active set
    useWidgetNotifications->>useWidgetNotifications: Call sync() to update<br/>pass-through state
    useWidgetNotifications->>FloatingButton: Dispatch TOAST_INTERACTION_STATE_EVENT<br/>(active=false)
    FloatingButton->>FloatingButton: Update hasActiveToastRef<br/>and enable mouse passthrough
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 Dismissing toasts with digital flair,
A button appears with the utmost care,
Events broadcast through the widget night,
While buttons dance, all snug and tight,
Translations bloom in languages three,
A notification's journey set free!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main objective: adding a dismiss button to widget error toasts to make them dismissable.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dismissable

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@nchopra nchopra closed this Feb 26, 2026
@nchopra nchopra reopened this Feb 26, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
apps/desktop/src/renderer/widget/pages/widget/components/FloatingButton.tsx (1)

12-12: Consider extracting duplicated event name constant.

TOAST_INTERACTION_STATE_EVENT is defined both here and in useWidgetNotifications.tsx. Extract to a shared constants file to ensure consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/widget/pages/widget/components/FloatingButton.tsx`
at line 12, TOAST_INTERACTION_STATE_EVENT is duplicated between
FloatingButton.tsx and useWidgetNotifications.tsx; extract this string into a
shared constant (e.g., export const TOAST_INTERACTION_STATE_EVENT) in a new or
existing shared constants module and import that constant into both
FloatingButton.tsx and useWidgetNotifications.tsx, replacing the local
definitions so both files reference the single source of truth.
apps/desktop/src/renderer/widget/hooks/useWidgetNotifications.tsx (1)

101-106: Missing dependency in cleanup effect.

The cleanup effect calls setIgnoreMouseEvents.mutate but has an empty dependency array. While this works because mutate is stable in tRPC, adding the linter disable comment or including setIgnoreMouseEvents in dependencies would clarify intent.

♻️ Suggested improvement
  useEffect(() => {
    return () => {
      activeToastIdsRef.current.clear();
      setIgnoreMouseEvents.mutate({ ignore: true });
    };
+    // eslint-disable-next-line react-hooks/exhaustive-deps -- cleanup only, mutate is stable
  }, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/widget/hooks/useWidgetNotifications.tsx` around
lines 101 - 106, The cleanup effect in useWidgetNotifications uses
setIgnoreMouseEvents.mutate and clears activeToastIdsRef.current but has an
empty dependency array; update the effect to either include setIgnoreMouseEvents
in the dependency array or add an explicit ESLint disable comment with a short
justification (e.g., "mutate is stable") to make the intent explicit. Locate the
useEffect cleanup that references activeToastIdsRef and setIgnoreMouseEvents and
adjust the dependencies accordingly so the linter is satisfied and the code
intent is clear.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/desktop/src/constants/models.ts`:
- Line 149: The referenced asset paths (e.g., providerIcon:
"/assets/icon_logo.svg" and other model entries using "/icons/models/local.svg")
are missing; update each model definition in models.ts that uses providerIcon
and related icon properties to point to existing asset paths or add the missing
SVG files to the repo. Locate the objects that define providerIcon (and any
other icon fields in the models array) and either correct the string values to
valid asset locations used by the app or add the corresponding SVG files at the
specified paths so imports resolve at runtime; ensure all occurrences of
"/assets/icon_logo.svg" and "/icons/models/local.svg" in models.ts are handled
consistently.

---

Nitpick comments:
In `@apps/desktop/src/renderer/widget/hooks/useWidgetNotifications.tsx`:
- Around line 101-106: The cleanup effect in useWidgetNotifications uses
setIgnoreMouseEvents.mutate and clears activeToastIdsRef.current but has an
empty dependency array; update the effect to either include setIgnoreMouseEvents
in the dependency array or add an explicit ESLint disable comment with a short
justification (e.g., "mutate is stable") to make the intent explicit. Locate the
useEffect cleanup that references activeToastIdsRef and setIgnoreMouseEvents and
adjust the dependencies accordingly so the linter is satisfied and the code
intent is clear.

In `@apps/desktop/src/renderer/widget/pages/widget/components/FloatingButton.tsx`:
- Line 12: TOAST_INTERACTION_STATE_EVENT is duplicated between
FloatingButton.tsx and useWidgetNotifications.tsx; extract this string into a
shared constant (e.g., export const TOAST_INTERACTION_STATE_EVENT) in a new or
existing shared constants module and import that constant into both
FloatingButton.tsx and useWidgetNotifications.tsx, replacing the local
definitions so both files reference the single source of truth.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0a2d11d and 57c4594.

⛔ Files ignored due to path filters (3)
  • apps/desktop/public/assets/icon_logo.svg is excluded by !**/*.svg
  • apps/desktop/public/assets/icon_logo@3x.png is excluded by !**/*.png
  • apps/desktop/public/icons/models/local.svg is excluded by !**/*.svg
📒 Files selected for processing (8)
  • apps/desktop/src/constants/models.ts
  • apps/desktop/src/i18n/locales/en.json
  • apps/desktop/src/i18n/locales/es.json
  • apps/desktop/src/i18n/locales/ja.json
  • apps/desktop/src/renderer/widget/components/ToasterWrapper.tsx
  • apps/desktop/src/renderer/widget/components/WidgetToast.tsx
  • apps/desktop/src/renderer/widget/hooks/useWidgetNotifications.tsx
  • apps/desktop/src/renderer/widget/pages/widget/components/FloatingButton.tsx

setup: "cloud",
provider: "Amical Cloud",
providerIcon: "/assets/logo.svg",
providerIcon: "/assets/icon_logo.svg",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for the referenced icon assets
echo "Searching for local.svg:"
find . -name "local.svg" -type f 2>/dev/null

echo -e "\nSearching for icon_logo.svg:"
find . -name "icon_logo.svg" -type f 2>/dev/null

echo -e "\nSearching for any SVG files in assets directory:"
find ./assets -name "*.svg" -type f 2>/dev/null | head -20

echo -e "\nSearching for any SVG files in icons directory:"
find ./icons -name "*.svg" -type f 2>/dev/null | head -20

Repository: amicalhq/amical

Length of output: 212


Critical issue: Referenced asset files do not exist in the repository.

The code changes reference two assets that are missing:

  • /icons/models/local.svg — not found
  • /assets/icon_logo.svg — not found

These files must be added to the repository or the paths must be corrected to reference existing assets. The UI will fail to load these icons at runtime without the asset files in place.

Also applies to: 180-181, 212-213, 245-246, 277-278, 309-310, 341-342

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/constants/models.ts` at line 149, The referenced asset paths
(e.g., providerIcon: "/assets/icon_logo.svg" and other model entries using
"/icons/models/local.svg") are missing; update each model definition in
models.ts that uses providerIcon and related icon properties to point to
existing asset paths or add the missing SVG files to the repo. Locate the
objects that define providerIcon (and any other icon fields in the models array)
and either correct the string values to valid asset locations used by the app or
add the corresponding SVG files at the specified paths so imports resolve at
runtime; ensure all occurrences of "/assets/icon_logo.svg" and
"/icons/models/local.svg" in models.ts are handled consistently.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/desktop/src/renderer/widget/hooks/useWidgetNotifications.tsx`:
- Around line 101-106: Cleanup currently clears activeToastIdsRef and sets
backend pass-through via setIgnoreMouseEvents.mutate({ ignore: true }) but does
not notify listeners that toast interaction ended; update the useEffect cleanup
to also emit a final inactive toast-state event (e.g., call the same
event/mutation used elsewhere to notify listeners of active state — search for
and invoke the existing notifier such as emitToastActiveState(false) or
setToastInteractionActive.mutate({ active: false })) so local listeners are
updated to active=false before unmount; keep the
activeToastIdsRef.current.clear() and setIgnoreMouseEvents.mutate call and add
the notification call after them.

In `@apps/desktop/src/renderer/widget/pages/widget/components/FloatingButton.tsx`:
- Around line 81-87: The unmount cleanup in the FloatingButton component
currently removes the TOAST_INTERACTION_STATE_EVENT listener
(handleToastInteractionState) but doesn't clear a pending debounce timeout,
allowing leaveTimeoutRef to fire after unmount; update the cleanup returned by
the useEffect to check leaveTimeoutRef.current and call
clearTimeout(leaveTimeoutRef.current) and then set leaveTimeoutRef.current =
null (or undefined) so any pending timeout is cancelled and cannot run stale
state/mutation logic after unmount.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 57c4594 and d81be92.

📒 Files selected for processing (7)
  • apps/desktop/src/i18n/locales/en.json
  • apps/desktop/src/i18n/locales/es.json
  • apps/desktop/src/i18n/locales/ja.json
  • apps/desktop/src/renderer/widget/components/ToasterWrapper.tsx
  • apps/desktop/src/renderer/widget/components/WidgetToast.tsx
  • apps/desktop/src/renderer/widget/hooks/useWidgetNotifications.tsx
  • apps/desktop/src/renderer/widget/pages/widget/components/FloatingButton.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/desktop/src/i18n/locales/en.json
  • apps/desktop/src/renderer/widget/components/ToasterWrapper.tsx

Comment on lines +101 to +106
useEffect(() => {
return () => {
activeToastIdsRef.current.clear();
setIgnoreMouseEvents.mutate({ ignore: true });
};
}, []);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Emit a final inactive toast-state event on cleanup.

Cleanup resets backend pass-through, but it does not notify other listeners that toast interaction is now inactive. That can leave local listeners with stale active=true state after unmount.

💡 Suggested fix
   useEffect(() => {
     return () => {
       activeToastIdsRef.current.clear();
+      window.dispatchEvent(
+        new CustomEvent<{ active: boolean }>(TOAST_INTERACTION_STATE_EVENT, {
+          detail: { active: false },
+        }),
+      );
       setIgnoreMouseEvents.mutate({ ignore: true });
     };
   }, []);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
return () => {
activeToastIdsRef.current.clear();
setIgnoreMouseEvents.mutate({ ignore: true });
};
}, []);
useEffect(() => {
return () => {
activeToastIdsRef.current.clear();
window.dispatchEvent(
new CustomEvent<{ active: boolean }>(TOAST_INTERACTION_STATE_EVENT, {
detail: { active: false },
}),
);
setIgnoreMouseEvents.mutate({ ignore: true });
};
}, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/widget/hooks/useWidgetNotifications.tsx` around
lines 101 - 106, Cleanup currently clears activeToastIdsRef and sets backend
pass-through via setIgnoreMouseEvents.mutate({ ignore: true }) but does not
notify listeners that toast interaction ended; update the useEffect cleanup to
also emit a final inactive toast-state event (e.g., call the same event/mutation
used elsewhere to notify listeners of active state — search for and invoke the
existing notifier such as emitToastActiveState(false) or
setToastInteractionActive.mutate({ active: false })) so local listeners are
updated to active=false before unmount; keep the
activeToastIdsRef.current.clear() and setIgnoreMouseEvents.mutate call and add
the notification call after them.

Comment on lines 81 to 87
return () => {
window.removeEventListener(
TOAST_INTERACTION_STATE_EVENT,
handleToastInteractionState,
);
console.debug("FloatingButton component unmounting");
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clear pending debounce timeout during unmount cleanup.

Line 82 cleanup removes the event listener, but a pending leaveTimeoutRef can still fire afterward and run stale state/mutation logic.

💡 Suggested fix
   return () => {
+    if (leaveTimeoutRef.current) {
+      clearTimeout(leaveTimeoutRef.current);
+      leaveTimeoutRef.current = null;
+    }
     window.removeEventListener(
       TOAST_INTERACTION_STATE_EVENT,
       handleToastInteractionState,
     );
     console.debug("FloatingButton component unmounting");
   };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return () => {
window.removeEventListener(
TOAST_INTERACTION_STATE_EVENT,
handleToastInteractionState,
);
console.debug("FloatingButton component unmounting");
};
return () => {
if (leaveTimeoutRef.current) {
clearTimeout(leaveTimeoutRef.current);
leaveTimeoutRef.current = null;
}
window.removeEventListener(
TOAST_INTERACTION_STATE_EVENT,
handleToastInteractionState,
);
console.debug("FloatingButton component unmounting");
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/widget/pages/widget/components/FloatingButton.tsx`
around lines 81 - 87, The unmount cleanup in the FloatingButton component
currently removes the TOAST_INTERACTION_STATE_EVENT listener
(handleToastInteractionState) but doesn't clear a pending debounce timeout,
allowing leaveTimeoutRef to fire after unmount; update the cleanup returned by
the useEffect to check leaveTimeoutRef.current and call
clearTimeout(leaveTimeoutRef.current) and then set leaveTimeoutRef.current =
null (or undefined) so any pending timeout is cancelled and cannot run stale
state/mutation logic after unmount.

@nchopra nchopra merged commit 62015ee into main Feb 27, 2026
4 checks passed
@nchopra nchopra deleted the dismissable branch February 27, 2026 02:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants