Skip to content

Add a context menu to the toolbar button#39

Merged
snipsnipsnip merged 1 commit intoexteditor:mainfrom
snipsnipsnip:add-context-menu
Oct 16, 2025
Merged

Add a context menu to the toolbar button#39
snipsnipsnip merged 1 commit intoexteditor:mainfrom
snipsnipsnip:add-context-menu

Conversation

@snipsnipsnip
Copy link
Copy Markdown
Member

Closes #30

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Oct 15, 2025

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added a context menu to the toolbar button with customizable command options
    • New menu system enables quick access to toolbar button actions via right-click interaction

Walkthrough

Adds a toolbar-button context menu and supporting infra: manifest permission, new menu and command handler classes, background routing updates, Thunderbird-specific ButtonMenu implementation, API/type additions, startup wiring, and small utilities and tests to wire and persist menu items.

Changes

Cohort / File(s) Summary
Manifest
manifest_template.json
Added "menus" permission to the permissions array.
Background API types
src/app-background/api.ts
Added IButtonMenu, MenuItem, and MenuShownInfo types for menu surface.
Command handling
src/app-background/command_handler.ts
New CommandHandler class dispatching compose commands to ComposeActionNotifier.
Menu handling
src/app-background/menu_handler.ts
New MenuHandler class that initializes menu items on first show and forwards clicks to CommandHandler.
Event router
src/app-background/background_event_router.ts
Refactored to accept CommandHandler and MenuHandler; added handleMenuShown and handleMenuClick; removed ComposeActionNotifier param and in-file runCommand.
Root/background wiring
src/root/background.ts
Added menuItems config, listeners for messenger.menus.onShown/onClicked, and passed menuItems into router preparation.
Startup wiring
src/root/startup/startup_background.ts
Background catalog updated: removed composeActionNotifier, added commandHandler, menuHandler, and menuItems in BackgroundConstants/Catalog.
Thunderbird UI impl
src/thunderbird/background_util/button_menu.ts
New ButtonMenu implementing IButtonMenu: lazy init, create/remove menu items via messenger.menus, stores initialized flag.
Thunderbird i18n contract
src/thunderbird/messenger/i18n.ts
Added I18n interface with getMessage(messageName: MessageId): string.
Utilities
src/util/time.ts, src/util/types.ts
Added Disposable and time() helper; minor doc comment on MessageId.
Tests
src/test/sanity.test.ts, src/test/startup.test.ts
Tests updated to include menuItems in prepareBackgroundRouter and startup constants snapshot.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant TB as Thunderbird UI
    participant Router as BackgroundEventRouter
    participant MenuHandler as MenuHandler
    participant ButtonMenu as ButtonMenu
    participant CmdHandler as CommandHandler
    participant Notifier as ComposeActionNotifier
    participant ComposeTab

    User->>TB: Right-click toolbar button
    TB->>Router: menus.onShown event
    Router->>MenuHandler: handleMenuShown(info)
    alt not initialized
        MenuHandler->>ButtonMenu: isInitialized()
        ButtonMenu-->>MenuHandler: false
        MenuHandler->>ButtonMenu: initItems(menuItems, info)
        ButtonMenu->>TB: messenger.menus.create/create submenu items
        TB-->>ButtonMenu: ack
        ButtonMenu->>ButtonMenu: persist initialized flag
    else initialized
        MenuHandler->>ButtonMenu: isInitialized()
        ButtonMenu-->>MenuHandler: true
    end

    User->>TB: Click menu item (e.g., stop_ghostbird)
    TB->>Router: menus.onClicked event
    Router->>MenuHandler: handleMenuClick(menuItemId, tab)
    MenuHandler->>CmdHandler: runCommand(menuItemId, composeTab)
    CmdHandler->>Notifier: dispatch corresponding action
    Notifier->>ComposeTab: perform start/stop/toggle
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰
A right-click brings a tiny door,
Buttons whisper on the toolbar,
Menus born from code so neat,
Click to stop—no typing feat.
Little rabbit hops, pleased and sure.

Pre-merge checks and finishing touches

❌ Failed checks (3 warnings)
Check name Status Explanation Resolution
Out of Scope Changes Check ⚠️ Warning The PR includes unrelated additions such as a new time utility and Disposable interface in src/util/time.ts, a new I18n interface in src/thunderbird/messenger/i18n.ts, and a comment change in src/util/types.ts, none of which are required to implement the context menu for disconnecting. Consider moving the time utility, Disposable interface, and I18n interface additions into a separate PR and limiting this PR to only the context menu changes.
Description Check ⚠️ Warning The description only includes a closing issue reference and omits all required template sections including the licensing agreement checkbox, change type, current and new behavior descriptions, and manual testing steps, making it incomplete against the repository’s PR template. Please update the PR description to include all template sections such as the licensing agreement, change category, current and new behavior summaries, and steps to manually test the new context menu behavior.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title clearly describes the primary change introduced by this PR—adding a context menu to the toolbar button—without extraneous details or ambiguity, making it immediately clear to reviewers what feature was implemented.
Linked Issues Check ✅ Passed The changes introduce the necessary manifest permission, API interfaces, background router handlers, menu handler logic, UI configuration of a single “Disconnect” menu item, and event wiring to display a right-click context menu on the toolbar button and dispatch the disconnect command, fully satisfying the requirements of issue #30.
✨ Finishing touches
  • 📝 Docstrings were successfully generated.
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

@codecov
Copy link
Copy Markdown

codecov bot commented Oct 15, 2025

Codecov Report

❌ Patch coverage is 23.35766% with 105 lines in your changes missing coverage. Please review.
✅ Project coverage is 47.12%. Comparing base (cf52c86) to head (aae0aef).
⚠️ Report is 2 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/thunderbird/background_util/button_menu.ts 21.05% 45 Missing ⚠️
src/root/background.ts 0.00% 24 Missing ⚠️
src/app-background/background_event_router.ts 28.57% 10 Missing ⚠️
src/app-background/command_handler.ts 35.71% 9 Missing ⚠️
src/app-background/menu_handler.ts 52.63% 9 Missing ⚠️
src/util/time.ts 11.11% 8 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #39      +/-   ##
==========================================
- Coverage   48.43%   47.12%   -1.31%     
==========================================
  Files          55       59       +4     
  Lines        2141     2264     +123     
  Branches      286      292       +6     
==========================================
+ Hits         1037     1067      +30     
- Misses       1097     1190      +93     
  Partials        7        7              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link
Copy Markdown
Contributor

@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: 6

♻️ Duplicate comments (1)
.github/workflows/build.yml (1)

59-59: Same verification needed as in check-pr.yml.

This change mirrors the one in .github/workflows/check-pr.yml. Please ensure the test output structure is consistent with this specific file path.

🧹 Nitpick comments (8)
src/ghosttext-adaptor/email_editor.ts (1)

29-32: Consider filtering out false values for clarity.

The current implementation works correctly because Promise.all handles non-Promise values, but the pattern isNewStatus && promise can be confusing since it evaluates to either false or a Promise.

For better readability, consider filtering out the false value:

-    await Promise.all([
-      isNewStatus && this.notificationTray.showNotification(icon, messageForStatus(status)),
-      this.composeWindow.setIcon(icon),
-    ])
+    const tasks = [this.composeWindow.setIcon(icon)]
+    if (isNewStatus) {
+      tasks.push(this.notificationTray.showNotification(icon, messageForStatus(status)))
+    }
+    await Promise.all(tasks)

Alternatively, use filter:

-    await Promise.all([
-      isNewStatus && this.notificationTray.showNotification(icon, messageForStatus(status)),
-      this.composeWindow.setIcon(icon),
-    ])
+    await Promise.all(
+      [
+        isNewStatus && this.notificationTray.showNotification(icon, messageForStatus(status)),
+        this.composeWindow.setIcon(icon),
+      ].filter(Boolean)
+    )
src/thunderbird/messenger/i18n.ts (1)

3-20: Clarify documentation to match the simplified signature.

The documentation mentions:

  • Returning undefined for format errors, but the signature only allows string
  • A substitutions parameter that doesn't exist in the signature

Since this interface appears to be a simplified version of browser.i18n.getMessage(), update the documentation to reflect that this implementation:

  • Only accepts messageName (no substitutions support)
  • Only returns string (clarify what happens on errors)

Apply this diff to align documentation with the actual signature:

   /**
    * Gets the localized string for the specified message. If the message is
-   * missing, this method returns an empty string (''). If the format of
-   * the `getMessage()` call is wrong — for example, _messageName_ is not a
-   * string or the _substitutions_ array has more than 9 elements — this
-   * method returns `undefined`.
+   * missing, this method returns an empty string ('').
    *
    * @param messageName The name of the message, as specified in the
    * `messages.json` file.
    *
-   * @param [substitutions] Substitution strings, if the message requires
-   * any.
-   *
    * @returns Message localized for current locale.
    */
   getMessage(messageName: MessageId): string
src/app-background/menu_handler.ts (1)

27-32: Add validation before casting menuItemId to CommandId.

The type assertion on line 30 assumes menuItemId is always a valid CommandId, but there's no runtime validation. If an invalid ID is passed (e.g., from a stale menu or external source), CommandHandler.runCommand will fail at runtime.

Consider adding validation:

   /** Handles click on a menu item */
   handleMenuItemClicked(menuItemId: string, composeTab: IComposeWindow): Promise<void> {
     // We use command ID as menu item ID, so we can directly pass it to the command handler
+    const validCommands: CommandId[] = ["start_ghostbird", "stop_ghostbird", "toggle_ghostbird"]
+    if (!validCommands.includes(menuItemId as CommandId)) {
+      return Promise.reject(new Error(`Invalid menu item ID: ${menuItemId}`))
+    }
     return this.commandHandler.runCommand(menuItemId as CommandId, composeTab)
   }

Alternatively, create a type guard function:

function isCommandId(id: string): id is CommandId {
  return ["start_ghostbird", "stop_ghostbird", "toggle_ghostbird"].includes(id)
}
src/root/background.ts (1)

53-59: Remove unsafe assertion and add a type guard
Replace the cast (info as MenuShownInfo) with a runtime check to ensure info.menuIds is an array before passing it to handleMenuShown. For example:

if (!Array.isArray(info.menuIds)) {
  throw new TypeError('Unexpected menu info shape');
}
return router.handleMenuShown(info, tab);
src/thunderbird/background_util/button_menu.ts (3)

39-41: Avoid global menus.removeAll() if you have other context menus.

This removes all of this extension’s menu items across contexts. Prefer tracking created IDs and removing only those, or only calling this once at startup.


31-36: Remove noisy debug logging (or gate it).

console.debug(got) on every load is chatty. Consider removing or gating behind a verbose flag.

Suggested change:

-    console.debug(got)

17-24: Optionally validate current menu vs. expected items before early-return.

You accept currentShown but ignore it. To self-heal if menus get cleared, compare info.menuIds with expected and re-init when mismatched.

src/app-background/background_event_router.ts (1)

16-23: Validate command before casting to CommandId.

Unsafe cast can pass an unknown string to runCommand, which then returns undefined (not a Promise). Fail fast on unknown commands.

Apply this diff:

   handleCommand(command: string, tab: ITab): Promise<void> {
     let composeTab = this.composeTabDetector.tryWrap(tab)
     if (!composeTab) {
       return Promise.reject(Error("Event dropped"))
     }
 
-    return this.commandHandler.runCommand(command as CommandId, composeTab)
+    const allowed = new Set<CommandId>(["start_ghostbird", "stop_ghostbird", "toggle_ghostbird"])
+    if (!allowed.has(command as CommandId)) {
+      return Promise.reject(Error(`Unknown command: ${command}`))
+    }
+    return this.commandHandler.runCommand(command as CommandId, composeTab)
   }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1feb7c1 and cd4f5cc.

📒 Files selected for processing (20)
  • .github/workflows/build.yml (1 hunks)
  • .github/workflows/check-pr.yml (1 hunks)
  • ext/options.css (1 hunks)
  • ext/options.nolint.css (1 hunks)
  • manifest_template.json (1 hunks)
  • src/app-background/api.ts (2 hunks)
  • src/app-background/background_event_router.ts (3 hunks)
  • src/app-background/command_handler.ts (1 hunks)
  • src/app-background/menu_handler.ts (1 hunks)
  • src/ghosttext-adaptor/email_editor.ts (1 hunks)
  • src/root/background.ts (3 hunks)
  • src/root/startup/startup_background.ts (2 hunks)
  • src/test/sanity.test.ts (1 hunks)
  • src/test/startup.test.ts (1 hunks)
  • src/thunderbird/background_util/button_menu.ts (1 hunks)
  • src/thunderbird/background_util/notification_tray.ts (3 hunks)
  • src/thunderbird/messenger/i18n.ts (1 hunks)
  • src/util/time.ts (1 hunks)
  • src/util/types.ts (1 hunks)
  • tsconfig.json (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (10)
src/thunderbird/messenger/i18n.ts (1)
src/util/types.ts (1)
  • MessageId (22-22)
src/app-background/command_handler.ts (3)
src/app-background/compose_action_notifier.ts (1)
  • ComposeActionNotifier (4-57)
src/app-background/api.ts (1)
  • CommandId (7-7)
src/ghosttext-adaptor/api.ts (1)
  • IComposeWindow (41-72)
src/app-background/menu_handler.ts (3)
src/app-background/api.ts (4)
  • IButtonMenu (46-56)
  • MenuItem (65-72)
  • MenuShownInfo (59-62)
  • CommandId (7-7)
src/app-background/command_handler.ts (1)
  • CommandHandler (7-23)
src/ghosttext-adaptor/api.ts (1)
  • IComposeWindow (41-72)
src/ghosttext-adaptor/email_editor.ts (1)
src/ghosttext-runner/api.ts (1)
  • ClientStatus (109-109)
src/app-background/background_event_router.ts (3)
src/app-background/api.ts (4)
  • IComposeWindowDetector (34-43)
  • CommandId (7-7)
  • MenuShownInfo (59-62)
  • ITab (26-31)
src/app-background/command_handler.ts (1)
  • CommandHandler (7-23)
src/app-background/menu_handler.ts (1)
  • MenuHandler (5-32)
src/thunderbird/background_util/button_menu.ts (1)
src/app-background/api.ts (3)
  • IButtonMenu (46-56)
  • MenuItem (65-72)
  • MenuShownInfo (59-62)
src/root/background.ts (3)
src/app-background/api.ts (2)
  • MenuItem (65-72)
  • MenuShownInfo (59-62)
src/root/startup/startup_background.ts (2)
  • AlarmHeart (14-14)
  • prepareBackgroundRouter (33-40)
src/thunderbird/background_util/alarm_heart.ts (1)
  • AlarmHeart (3-21)
src/app-background/api.ts (1)
src/util/types.ts (1)
  • MessageId (22-22)
src/root/startup/startup_background.ts (3)
src/app-background/api.ts (2)
  • MenuItem (65-72)
  • IComposeWindowDetector (34-43)
src/app-background/menu_handler.ts (1)
  • MenuHandler (5-32)
src/app-background/command_handler.ts (1)
  • CommandHandler (7-23)
src/thunderbird/background_util/notification_tray.ts (1)
src/ghosttext-adaptor/api.ts (1)
  • INotificationTray (5-8)
🔇 Additional comments (22)
tsconfig.json (1)

43-45: Confirm TypeDoc version supports sluggerConfiguration.

sluggerConfiguration is only understood by recent TypeDoc releases; older versions error out on unknown options. Please double-check that the project is already on a TypeDoc build new enough to accept this setting (and bump it if not), otherwise docs generation will fail.

src/util/time.ts (1)

1-13: Clean implementation with appropriate logging.

The timer utility is well-structured and uses performance.now() for accurate measurements. The console.debug with object formatting provides clear, structured output for development.

ext/options.nolint.css (1)

5-5: LGTM!

The addition of --align-start complements the existing --align-end variable and properly supports bidirectional text layouts for internationalization.

src/thunderbird/background_util/notification_tray.ts (2)

9-11: LGTM!

The notification ID rotation mechanism is well-designed. It limits concurrent notifications to 5 slots, preventing notification spam while maintaining the ability to show multiple notifications if needed.


34-37: LGTM!

The ID rotation logic is correct: (this.lastId + 1) % this.slotCount will cycle through 0→1→2→3→4→0→..., ensuring notification IDs stay within the defined slot range.

src/ghosttext-adaptor/email_editor.ts (1)

25-25: LGTM!

Good optimization to only trigger notifications when the status actually changes, preventing redundant notifications.

src/util/types.ts (1)

23-23: LGTM!

The clarifying comment explains the design decision to rely on unit tests rather than TypeScript enumerations for MessageId validation.

manifest_template.json (1)

26-26: LGTM!

The addition of the "menus" permission is essential for implementing the toolbar context menu feature described in the PR objectives, which aims to provide a mouse-only way to disconnect by right-clicking the toolbar button.

src/test/startup.test.ts (1)

37-37: LGTM!

The addition of the menuItems constant aligns with the expanded BackgroundConstants API surface and follows the established pattern for test constants.

src/root/startup/startup_background.ts (3)

1-1: LGTM!

The import updates correctly reflect the architectural shift to menu-based command handling via MenuHandler and CommandHandler.


20-20: LGTM!

Adding menuItems to BackgroundConstants properly extends the configuration surface to support toolbar context menus.


25-26: LGTM!

The catalog updates correctly expose MenuHandler and CommandHandler as the new public API surface while maintaining the underlying ComposeActionNotifier as an implementation detail.

src/test/sanity.test.ts (1)

23-23: LGTM!

The test correctly includes the new menuItems parameter with an appropriate empty array value for basic router resolution testing.

src/app-background/command_handler.ts (1)

7-23: LGTM!

The CommandHandler implementation correctly:

  • Delegates all commands to the appropriate ComposeActionNotifier methods
  • Omits the default case to leverage TypeScript's exhaustiveness checking
  • Returns promises consistently for all command types
src/root/background.ts (2)

16-23: LGTM!

The menuItems configuration correctly includes only the stop_ghostbird command, which aligns with the PR objective of providing a mouse-only way to disconnect.


61-71: LGTM!

The menu click handler includes proper validation:

  • Checks for missing tab or menuItemId
  • Returns explicit error for dropped events
  • Handles unknown commands with rejection
src/app-background/api.ts (2)

45-56: LGTM!

The IButtonMenu interface provides a clean abstraction for menu initialization with appropriate method signatures and documentation.


58-72: LGTM!

The MenuShownInfo and MenuItem types are well-defined and properly documented, providing clear contracts for menu-related functionality.

src/app-background/menu_handler.ts (1)

14-25: LGTM!

The menu initialization logic correctly:

  • Checks if the menu is already initialized before calling initItems
  • Logs appropriately for debugging
  • Returns the promise from initItems when initialization is needed
src/app-background/background_event_router.ts (3)

38-40: Menu shown routing looks good.

Delegation to MenuHandler.handleMenuShown is clean and aligns with the new menu flow.


43-50: Menu click routing LGTM.

Properly wraps tab -> compose window and forwards id to MenuHandler.

Please ensure menu item IDs match CommandId values so CommandHandler receives valid commands.


1-4: Verify barrel exports for types.

import type { MenuHandler, MenuShownInfo } from "." assumes these are re-exported from the index. Confirm the barrel re-exports to avoid import errors after refactors.

Copy link
Copy Markdown
Contributor

@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: 3

♻️ Duplicate comments (1)
src/thunderbird/background_util/button_menu.ts (1)

17-24: Concurrent initialization concern.

This was flagged in a previous review: multiple rapid onShown events could potentially cause race conditions leading to duplicate menus.create calls and "duplicate id" errors. While the ??= operator provides some protection, the suggested explicit serialization pattern would be more robust.

Additionally, lines 18-20 perform a check but don't return early, making them redundant with the ??= operator on line 21. Consider either:

  • Removing the check entirely (let ??= handle it), or
  • Adding an early return after the log for efficiency:
  initItems(menuItems: ReadonlyArray<MenuItem>, _currentShown: MenuShownInfo | undefined): Promise<void> {
    if (this.initWork) {
      console.debug("ButtonMenu is already initialized; Skip creating menu items.")
+     return this.initWork
    }
    this.initWork ??= this.createMenuItems(menuItems)

    return this.initWork
  }
🧹 Nitpick comments (5)
src/app-background/menu_handler.ts (2)

15-25: Implement the comparison hinted by the comment.

Re-init when current menu differs from expected item IDs.

   handleMenuShown(info: MenuShownInfo): Promise<void> | void {
-    // Compare the shown menu with menuItems and (re-)initialize the menu if necessary
-    console.debug(info)
-
-    if (!this.buttonMenu.isInitialized()) {
+    // (Re-)initialize if uninitialized or IDs differ
+    console.debug(info)
+    if (!this.buttonMenu.isInitialized()) {
       console.debug("Initializing menu")
       return this.buttonMenu.initItems(this.menuItems, info)
     }
 
-    console.debug("Menu is already initialized")
+    const expected = new Set(this.menuItems.map((m) => m.id))
+    const actual = new Set(info.menuIds)
+    const same =
+      expected.size === actual.size && [...expected].every((id) => actual.has(id))
+    if (!same) {
+      console.debug("Menu items changed; re-initializing menu")
+      return this.buttonMenu.initItems(this.menuItems, info)
+    }
+
+    console.debug("Menu is already initialized")
   }

27-31: Defensive guard on menuItemId before casting.

Avoid relying on casts; reject unknown IDs early.

-  handleMenuItemClicked(menuItemId: string, composeTab: IComposeWindow): Promise<void> {
-    // We use command ID as menu item ID, so we can directly pass it to the command handler
-    return this.commandHandler.runCommand(menuItemId as CommandId, composeTab)
-  }
+  handleMenuItemClicked(menuItemId: string, composeTab: IComposeWindow): Promise<void> {
+    // Validate the ID before delegating
+    const isCommandId = (id: string): id is CommandId =>
+      id === "start_ghostbird" || id === "stop_ghostbird" || id === "toggle_ghostbird"
+    if (!isCommandId(menuItemId)) {
+      return Promise.reject(Error(`unknown command ${menuItemId}`))
+    }
+    return this.commandHandler.runCommand(menuItemId, composeTab)
+  }
src/app-background/background_event_router.ts (2)

22-22: Validate command string before casting.

Reject unknown commands here to avoid undefined bubbling.

-    return this.commandHandler.runCommand(command as CommandId, composeTab)
+    if (!isCommandId(command)) {
+      return Promise.reject(Error(`unknown command ${command}`))
+    }
+    return this.commandHandler.runCommand(command, composeTab)

Add this helper in the module (outside the class):

const isCommandId = (id: string): id is CommandId =>
  id === "start_ghostbird" || id === "stop_ghostbird" || id === "toggle_ghostbird"

43-50: Also guard menuItemId here.

Mirror the command guard for menu clicks.

-    return this.menuHandler.handleMenuItemClicked(menuItemId, composeTab)
+    if (!isCommandId(menuItemId)) {
+      return Promise.reject(Error(`unknown command ${menuItemId}`))
+    }
+    return this.menuHandler.handleMenuItemClicked(menuItemId, composeTab)
src/thunderbird/background_util/button_menu.ts (1)

13-15: Consider clarifying initialization state semantics.

The method returns true when initialization has started, not when it has completed. While this matches the interface contract, it could be semantically confusing. Consider renaming to hasInitializationStarted() or documenting this behavior more explicitly.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cd4f5cc and 0456484.

📒 Files selected for processing (13)
  • manifest_template.json (1 hunks)
  • src/app-background/api.ts (2 hunks)
  • src/app-background/background_event_router.ts (3 hunks)
  • src/app-background/command_handler.ts (1 hunks)
  • src/app-background/menu_handler.ts (1 hunks)
  • src/root/background.ts (3 hunks)
  • src/root/startup/startup_background.ts (2 hunks)
  • src/test/sanity.test.ts (1 hunks)
  • src/test/startup.test.ts (1 hunks)
  • src/thunderbird/background_util/button_menu.ts (1 hunks)
  • src/thunderbird/messenger/i18n.ts (1 hunks)
  • src/util/time.ts (1 hunks)
  • src/util/types.ts (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • src/util/types.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • manifest_template.json
  • src/util/time.ts
  • src/test/startup.test.ts
  • src/thunderbird/messenger/i18n.ts
  • src/app-background/api.ts
  • src/root/startup/startup_background.ts
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-10-15T13:30:46.981Z
Learnt from: snipsnipsnip
PR: exteditor/ghostbird#39
File: src/thunderbird/background_util/button_menu.ts:47-66
Timestamp: 2025-10-15T13:30:46.981Z
Learning: `Promise.withResolvers` is supported in TB128/Gecko runtime used by the Thunderbird extension and is already used in multiple files like `src/util/promisifying_queue.ts` and `src/util/lazy_then.ts`.

Applied to files:

  • src/thunderbird/background_util/button_menu.ts
📚 Learning: 2025-10-15T14:05:25.989Z
Learnt from: snipsnipsnip
PR: exteditor/ghostbird#39
File: src/thunderbird/background_util/button_menu.ts:52-53
Timestamp: 2025-10-15T14:05:25.989Z
Learning: In Thunderbird WebExtension API, the `menus.create` method's `icons` parameter accepts either a string (MenuIconPath) or an object dictionary (MenuIconDictionary), unlike the standard WebExtension API which only accepts an object. A string can be passed directly as the value for the icons property.

Applied to files:

  • src/thunderbird/background_util/button_menu.ts
🧬 Code graph analysis (5)
src/app-background/command_handler.ts (3)
src/app-background/compose_action_notifier.ts (1)
  • ComposeActionNotifier (4-57)
src/app-background/api.ts (1)
  • CommandId (7-7)
src/ghosttext-adaptor/api.ts (1)
  • IComposeWindow (41-72)
src/app-background/background_event_router.ts (3)
src/app-background/api.ts (4)
  • IComposeWindowDetector (34-43)
  • CommandId (7-7)
  • MenuShownInfo (59-62)
  • ITab (26-31)
src/app-background/command_handler.ts (1)
  • CommandHandler (7-23)
src/app-background/menu_handler.ts (1)
  • MenuHandler (5-32)
src/app-background/menu_handler.ts (3)
src/app-background/api.ts (4)
  • IButtonMenu (46-56)
  • MenuItem (65-72)
  • MenuShownInfo (59-62)
  • CommandId (7-7)
src/app-background/command_handler.ts (1)
  • CommandHandler (7-23)
src/ghosttext-adaptor/api.ts (1)
  • IComposeWindow (41-72)
src/thunderbird/background_util/button_menu.ts (1)
src/app-background/api.ts (3)
  • IButtonMenu (46-56)
  • MenuItem (65-72)
  • MenuShownInfo (59-62)
src/root/background.ts (3)
src/app-background/api.ts (2)
  • MenuItem (65-72)
  • MenuShownInfo (59-62)
src/root/startup/startup_background.ts (2)
  • AlarmHeart (14-14)
  • prepareBackgroundRouter (33-40)
src/thunderbird/background_util/alarm_heart.ts (1)
  • AlarmHeart (3-21)
🔇 Additional comments (7)
src/test/sanity.test.ts (1)

23-23: LGTM — test updated for new parameter.

Good addition to cover the new prepareBackgroundRouter option shape.

src/app-background/command_handler.ts (1)

12-22: Runtime guard lives upstream; keep this exhaustive.

Implementation is clean and exhaustive for CommandId. Since callers cast strings, ensure upstream validates inputs to prevent undefined being returned at runtime.

Confirm router/menu paths validate strings before casting to CommandId.

src/root/background.ts (1)

53-59: Type cast on menus.onShown is valid
Thunderbird’s menus.onShown passes an OnShowData with menuIds for action/toolbar contexts and matches Firefox’s API shape.

src/thunderbird/background_util/button_menu.ts (4)

1-11: LGTM!

The class structure, singleton pattern, and dependency injection via constructor are well-implemented.


26-33: LGTM!

The initialization flag logic properly handles MV3 suspension by checking session storage before recreating menus.


46-50: LGTM!

The menu creation pattern is solid: clearing existing items, parallel creation, and final refresh ensure correct state.


52-75: LGTM!

The menu item creation is correctly implemented:

  • Promise.withResolvers is supported in TB128 (as confirmed in previous reviews)
  • The icons parameter correctly accepts a string in Thunderbird's API
  • Error handling via lastError callback is appropriate
  • The comment about command option limitations is helpful context

Based on learnings.

The menu lets users disconnect with a mouse click.
Also added a stopwatch utility to measure the time while debugging.
Copy link
Copy Markdown
Contributor

@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: 0

♻️ Duplicate comments (2)
src/root/background.ts (2)

16-23: LGTM!

The menu configuration is correct. The i18n key "manifest_commands_stop_ghostbird_description" is properly defined in /locales.toml (per project conventions), and the icon path "gray.svg" aligns with existing assets.

Based on learnings.


61-71: LGTM!

The validation logic correctly filters out events from special windows (when tab is falsy) and ensures menuItemId is a string before delegating to the router. The error handling appropriately rejects unknown commands.

Based on learnings.

🧹 Nitpick comments (1)
src/app-background/menu_handler.ts (1)

28-31: Validate menuItemId before casting to CommandId.

The type assertion menuItemId as CommandId on line 30 is unsafe. If a caller passes an invalid menuItemId, the cast will succeed but commandHandler.runCommand will fail with a type error at runtime (the switch statement has no default case).

Consider adding validation:

  handleMenuItemClicked(menuItemId: string, composeTab: IComposeWindow): Promise<void> {
-   // We use command ID as menu item ID, so we can directly pass it to the command handler
-   return this.commandHandler.runCommand(menuItemId as CommandId, composeTab)
+   // We use command ID as menu item ID, so we can directly pass it to the command handler
+   const validCommands: ReadonlyArray<CommandId> = ["start_ghostbird", "stop_ghostbird", "toggle_ghostbird"]
+   if (!validCommands.includes(menuItemId as CommandId)) {
+     return Promise.reject(Error(`Invalid menu item ID: ${menuItemId}`))
+   }
+   return this.commandHandler.runCommand(menuItemId as CommandId, composeTab)
  }

Alternatively, if the upstream validation in src/root/background.ts (line 64) already ensures menuItemId is a string, consider documenting this contract or adding a runtime assertion.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0456484 and aae0aef.

📒 Files selected for processing (13)
  • manifest_template.json (1 hunks)
  • src/app-background/api.ts (2 hunks)
  • src/app-background/background_event_router.ts (3 hunks)
  • src/app-background/command_handler.ts (1 hunks)
  • src/app-background/menu_handler.ts (1 hunks)
  • src/root/background.ts (3 hunks)
  • src/root/startup/startup_background.ts (2 hunks)
  • src/test/sanity.test.ts (1 hunks)
  • src/test/startup.test.ts (1 hunks)
  • src/thunderbird/background_util/button_menu.ts (1 hunks)
  • src/thunderbird/messenger/i18n.ts (1 hunks)
  • src/util/time.ts (1 hunks)
  • src/util/types.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/util/types.ts
  • src/test/startup.test.ts
  • src/thunderbird/messenger/i18n.ts
  • src/app-background/api.ts
  • src/thunderbird/background_util/button_menu.ts
  • src/util/time.ts
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-10-16T08:50:50.310Z
Learnt from: snipsnipsnip
PR: exteditor/ghostbird#39
File: src/root/background.ts:61-71
Timestamp: 2025-10-16T08:50:50.310Z
Learning: In Thunderbird WebExtensions, when `messenger.menus.onClicked` fires with a falsy `tab` argument, it indicates the event originated from a special window (such as Thunderbird's debug tools) rather than a regular compose window. In `src/root/background.ts`, the intentional design is to reject such events by checking `if (!tab)` to filter out these non-compose-window events.

Applied to files:

  • src/app-background/background_event_router.ts
  • src/root/background.ts
📚 Learning: 2025-10-16T08:52:27.564Z
Learnt from: snipsnipsnip
PR: exteditor/ghostbird#39
File: src/root/background.ts:16-23
Timestamp: 2025-10-16T08:52:27.564Z
Learning: In the ghostbird project, i18n keys are defined in `/locales.toml` and then generated into `_locales/*/messages.json` files using the build script `tools/generate_locale_messages.ts`. When reviewing i18n key usage, check `/locales.toml` rather than the generated `messages.json` files.

Applied to files:

  • src/root/background.ts
🧬 Code graph analysis (5)
src/root/startup/startup_background.ts (3)
src/app-background/api.ts (2)
  • MenuItem (65-72)
  • IComposeWindowDetector (34-43)
src/app-background/menu_handler.ts (1)
  • MenuHandler (5-32)
src/app-background/command_handler.ts (1)
  • CommandHandler (7-23)
src/app-background/menu_handler.ts (3)
src/app-background/api.ts (4)
  • IButtonMenu (46-56)
  • MenuItem (65-72)
  • MenuShownInfo (59-62)
  • CommandId (7-7)
src/app-background/command_handler.ts (1)
  • CommandHandler (7-23)
src/ghosttext-adaptor/api.ts (1)
  • IComposeWindow (41-72)
src/app-background/command_handler.ts (3)
src/app-background/compose_action_notifier.ts (1)
  • ComposeActionNotifier (4-57)
src/app-background/api.ts (1)
  • CommandId (7-7)
src/ghosttext-adaptor/api.ts (1)
  • IComposeWindow (41-72)
src/app-background/background_event_router.ts (3)
src/app-background/api.ts (4)
  • IComposeWindowDetector (34-43)
  • CommandId (7-7)
  • MenuShownInfo (59-62)
  • ITab (26-31)
src/app-background/command_handler.ts (1)
  • CommandHandler (7-23)
src/app-background/menu_handler.ts (1)
  • MenuHandler (5-32)
src/root/background.ts (2)
src/app-background/api.ts (2)
  • MenuItem (65-72)
  • MenuShownInfo (59-62)
src/root/startup/startup_background.ts (1)
  • prepareBackgroundRouter (33-40)
🔇 Additional comments (13)
manifest_template.json (1)

26-26: LGTM!

The "menus" permission is correctly placed and necessary for the toolbar context menu functionality introduced in this PR.

src/app-background/command_handler.ts (1)

7-23: LGTM!

The CommandHandler correctly delegates command execution to ComposeActionNotifier. The switch statement handles all CommandId variants, and the omitted default case enables TypeScript exhaustiveness checking.

src/test/sanity.test.ts (1)

23-23: LGTM!

The addition of menuItems: [] correctly updates the test to match the new prepareBackgroundRouter signature.

src/root/startup/startup_background.ts (3)

1-1: LGTM!

The import changes correctly reflect the refactoring from ComposeActionNotifier to MenuHandler and CommandHandler.


20-20: LGTM!

Adding menuItems to BackgroundConstants correctly extends the configuration to support the new menu functionality.


25-26: LGTM!

The BackgroundCatalog updates correctly reflect the architectural shift to separate menu and command handling via MenuHandler and CommandHandler.

src/root/background.ts (1)

53-59: LGTM!

The menus.onShown listener correctly delegates to router.handleMenuShown with appropriate logging.

src/app-background/menu_handler.ts (1)

14-25: LGTM!

The menu initialization logic correctly checks whether the menu has been initialized and only calls initItems when necessary.

src/app-background/background_event_router.ts (5)

9-13: LGTM!

The constructor correctly refactors to use CommandHandler and MenuHandler instead of direct ComposeActionNotifier dependency.


16-23: LGTM!

Command handling correctly delegates to CommandHandler.runCommand with proper tab validation.


25-35: LGTM!

The compose action handler correctly delegates to CommandHandler with the "start_ghostbird" command.


37-40: LGTM!

The handleMenuShown method correctly delegates menu initialization to MenuHandler.


42-50: LGTM!

The handleMenuClick method correctly validates the compose tab before delegating to MenuHandler, maintaining consistency with other event handlers.

@snipsnipsnip snipsnipsnip added this pull request to the merge queue Oct 16, 2025
Merged via the queue into exteditor:main with commit 9152e79 Oct 16, 2025
4 checks passed
@snipsnipsnip snipsnipsnip deleted the add-context-menu branch October 16, 2025 09:07
@github-project-automation github-project-automation bot moved this from 📥️Inbox to 🪺Experimental in Ghostbird Issues Oct 16, 2025
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Oct 17, 2025

Note

Docstrings generation - SUCCESS
Generated docstrings for this pull request at #42

coderabbitai bot added a commit that referenced this pull request Oct 17, 2025
Docstrings generation was requested by @snipsnipsnip.

* #39 (comment)

The following files were modified:

* `src/util/time.ts`
snipsnipsnip pushed a commit that referenced this pull request Oct 17, 2025
github-merge-queue bot pushed a commit that referenced this pull request Oct 17, 2025
Docstrings generation was requested by @snipsnipsnip.

* #39 (comment)

The following files were modified:

* `src/util/time.ts`

<details>
<summary>These files were ignored</summary>

* `src/test/sanity.test.ts`
* `src/test/startup.test.ts`

</details>

<details>
<summary>These file types are not supported</summary>

* `manifest_template.json`

</details>

<details>
<summary>ℹ️ Note</summary><blockquote>

CodeRabbit cannot perform edits on its own pull requests yet.

</blockquote></details>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

[Feature] Add a way to disconnect using only a mouse

1 participant