Skip to content

Add browser detection and "Open Link in Browser" context menu#3732

Open
svelle wants to merge 11 commits intomattermost:masterfrom
svelle:claude/add-browser-context-menu-R5zWt
Open

Add browser detection and "Open Link in Browser" context menu#3732
svelle wants to merge 11 commits intomattermost:masterfrom
svelle:claude/add-browser-context-menu-R5zWt

Conversation

@svelle
Copy link
Member

@svelle svelle commented Mar 16, 2026

Summary

This PR adds functionality to detect installed browsers on macOS, Windows, and Linux, and provides a context menu option to open external links in the user's preferred browser. The implementation includes:

  • New browserManager module that detects installed browsers per platform using platform-specific methods (mdfind on macOS, registry on Windows, which/desktop files on Linux)
  • Caching of detected browsers to avoid repeated system calls
  • Integration with the context menu to show "Open Link in Browser" submenu for external links
  • Comprehensive unit tests covering all platforms and error scenarios
  • i18n support for the new menu item

Changes

  • src/main/browserManager.ts: New module exporting getInstalledBrowsers(), openLinkInBrowser(), and clearBrowserCache() functions with platform-specific browser detection logic
  • src/main/browserManager.test.js: Complete test suite covering macOS, Windows, and Linux browser detection, caching behavior, and link opening with fallback to shell.openExternal
  • src/app/views/MattermostWebContentsView.ts: Integrated browser detection and added context menu items for opening external links in detected browsers
  • i18n/en.json: Added localization string for the new menu option

Checklist

  • Added unit tests (comprehensive test coverage for all platforms)
  • Has UI changes (new context menu option)
  • read and understood our Contributing Guidelines
  • completed Mattermost Contributor Agreement

Release Note

Added ability to open external links in installed browsers via context menu option on macOS, Windows, and Linux.

https://claude.ai/code/session_01WWHsdZzBXhrUtXo9WYEwY4

Change Impact: 🟡 Medium

Reasoning: Adds a new cross-platform BrowserManager module and context-menu integration into MattermostWebContentsView, spanning main and renderer layers with platform-specific system calls; unit tests cover the manager extensively but integration/UI timing remains untested.

Regression Risk: Moderate — Browser detection logic is well-covered by unit tests, but changes touch a widely-instantiated UI component (MattermostWebContentsView) and introduce asynchronous initialization (cachedBrowsers) that can cause timing-dependent context-menu inconsistencies early after startup; fallback behavior reduces severity but integration paths lack tests.

** QA Recommendation:** Perform manual QA on macOS, Windows, and Linux to verify the submenu appears consistently after app start and page loads, that detected browsers launch correctly, and that failures fall back to the system default; add integration/unit tests for MattermostWebContentsView menu population and startup timing to reduce regressions.

Generated by CodeRabbitAI

claude added 5 commits March 16, 2026 08:43
When right-clicking an external link on macOS, a new submenu "Open Link
in Browser" appears listing all detected installed browsers (Safari,
Chrome, Firefox, Edge, Brave, Arc, Opera, Vivaldi, Chromium, Orion, Zen).
This allows opening links in a browser other than the system default.

Browser detection uses mdfind with bundle identifiers and results are
cached for the session lifetime. Links are opened via `open -b` with
fallback to shell.openExternal.

https://claude.ai/code/session_01WWHsdZzBXhrUtXo9WYEwY4
Extends browser detection to all platforms:
- Windows: queries the registry (HKLM\SOFTWARE\Clients\StartMenuInternet)
  to find installed browsers and their executable paths
- Linux: uses `which` for known browser commands, then discovers additional
  browsers from .desktop files that handle x-scheme-handler/https
- Removes the macOS-only guard from the context menu so the submenu
  appears on all platforms

https://claude.ai/code/session_01WWHsdZzBXhrUtXo9WYEwY4
- Fix command injection: replace exec() with execFile() in openLinkInBrowser
  so URLs are passed as arguments, not shell-interpolated
- Restructure BrowserInfo to store executable + args separately instead of
  a shell command string
- Remove dead identical if/else branches in openLinkInBrowser
- Validate .desktop file exec paths are absolute and exist before adding
- Add HKCU registry fallback for per-user browser installs on Windows
- Search ~/.local/share/applications/ in addition to /usr/share/applications/
- Fix import paths to use project aliases instead of relative paths
- Add tests: existsSync false case, HKCU fallback, shell injection safety

https://claude.ai/code/session_01WWHsdZzBXhrUtXo9WYEwY4
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 16, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a cross-platform BrowserManager (discover, cache, and open browsers), integrates an "Open Link in Browser" submenu into MattermostWebContentsView for external links, adds an i18n key, and includes comprehensive Jest tests for discovery and opening behavior.

Changes

Cohort / File(s) Summary
Browser Detection & Management
src/main/browserManager.ts, src/main/browserManager.test.js
New BrowserManager API and singleton: BrowserInfo, getInstalledBrowsers(), clearBrowserCache(), openLinkInBrowser(); platform probes (mdfind / registry / which + .desktop), caching, execFile-based open with fallback; extensive cross-platform Jest tests covering success and error paths.
Context Menu Integration
src/app/views/MattermostWebContentsView.ts
Loads browser list at init (loadBrowserList / cachedBrowsers), adds isURLForConfiguredServer() and generateOpenInBrowserMenuItems() to append an "Open Link in Browser" submenu for external links while preserving internal server options.
Internationalization
i18n/en.json
Adds translation key app.menus.contextMenu.openLinkInBrowser for the new context menu entry.

Sequence Diagram

sequenceDiagram
    participant User
    participant WebView as MattermostWebContentsView
    participant BrowserMgr as BrowserManager
    participant OS as Platform
    participant Browser as Browser Process

    User->>WebView: Right-click external link
    WebView->>WebView: Detect external URL
    WebView->>BrowserMgr: getInstalledBrowsers()
    alt cached
        BrowserMgr-->>WebView: BrowserInfo[]
    else
        BrowserMgr->>OS: Probe installed browsers (mdfind / registry / which + .desktop)
        OS-->>BrowserMgr: BrowserInfo[]
        BrowserMgr->>BrowserMgr: Cache results
        BrowserMgr-->>WebView: BrowserInfo[]
    end
    WebView->>User: Render context menu with browser options
    User->>WebView: Select browser
    WebView->>BrowserMgr: openLinkInBrowser(url, browser)
    BrowserMgr->>Browser: execFile or shell.openExternal
    Browser-->>User: Browser opens link
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

3: Security Review

Suggested reviewers

  • wiggin77
  • esarafianou
  • edgarbellot
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title directly and clearly summarizes the main feature being added: browser detection and a context menu option to open external links in installed browsers, which aligns with all the significant changes across multiple files.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Coding Plan
  • Generate coding plan for human review comments

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.

Copy link
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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/app/views/MattermostWebContentsView.ts (1)

502-524: ⚠️ Potential issue | 🟠 Major

Treat other configured server URLs as internal before showing the browser submenu.

This branch only matches the current server URL. If a user right-clicks a link to another Mattermost server that is already configured in the desktop app, it falls into "Open Link in Browser" instead of preserving the in-app open-in-tab/window path.

Based on learnings, "External navigation links to non-configured servers should open in the user's default browser via will-navigate, did-start-navigation, and setWindowOpenHandler."

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

In `@src/app/views/MattermostWebContentsView.ts` around lines 502 - 524, The
current branch only treats links matching the active server
(isInternalURL(parsedURL, server.url)) as internal, so links to other configured
Mattermost servers fall through to generateOpenInBrowserMenuItems; update the
condition to detect any configured server URL (e.g., iterate configured servers
or add a helper like isURLForConfiguredServer(parsedURL)) and return the same
in-app menu (separator + Open in new tab/window with ViewManager and
NavigationManager calls) when the link belongs to any configured server rather
than only server.url. Ensure you reference the existing isInternalURL logic or
reuse it against each configured server URL to avoid duplicating navigation
behavior.
🧹 Nitpick comments (1)
src/main/browserManager.test.js (1)

94-97: Keep this test file on the repo's ES-module path.

This file still drops back to CommonJS inside src/. Use await import() in the async beforeEach / test body so it stays aligned with the repo’s module rule.

♻️ Suggested change
-    beforeEach(() => {
+    beforeEach(async () => {
         jest.clearAllMocks();
         jest.resetModules();
         mockExistsSync.mockReturnValue(true);

-        const browserManager = require('./browserManager');
-        getInstalledBrowsers = browserManager.getInstalledBrowsers;
-        openLinkInBrowser = browserManager.openLinkInBrowser;
-        clearBrowserCache = browserManager.clearBrowserCache;
+        ({getInstalledBrowsers, openLinkInBrowser, clearBrowserCache} = await import('./browserManager'));
         clearBrowserCache();
     });
...
-            const {shell} = require('electron');
+            const {shell} = await import('electron');
As per coding guidelines, "Always use ES module `import`/`export`, never `require()` in source files".

Also applies to: 297-297

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

In `@src/main/browserManager.test.js` around lines 94 - 97, Replace the CommonJS
require() usage with dynamic ES-module imports inside the async test lifecycle
so the test file stays on the repo's ESM path: remove the top-level
require('./browserManager') and instead use await import('./browserManager')
inside an async beforeEach or the individual test, then destructure the exported
functions (getInstalledBrowsers, openLinkInBrowser, clearBrowserCache) from the
imported module; make the same change for the other occurrence around the later
block (near line 297) so no require() calls remain in the file.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/browserManager.ts`:
- Around line 107-121: The registry "command" string (captured in stdout and
assigned to browserPath from match[1]) can include launcher flags and a %1
placeholder, so do not treat the entire match as the executable; instead parse
the command token: trim the string, if it starts with a quote extract up to the
matching closing quote as the executable path, otherwise take the first
whitespace-delimited token as the executable; remove any %1 or similar
placeholders and surrounding quotes before calling fs.existsSync and before
returning executable/args (adjust args to include any remaining tokens if
needed). Update the code around the exec result handling (the stdout match,
browserPath extraction, fs.existsSync check, and the returned executable/args)
to implement this parsing logic.
- Around line 247-253: The debug log in browserManager.ts currently outputs the
raw URL and shell.openExternal() is called without awaiting its Promise; update
the code so log.debug('Opening link in browser', ...) does not include the full
URL (log only browser.name and either a masked URL or omit url entirely) and
change the fallback to await shell.openExternal(url) inside a try/catch so any
rejection is caught and logged (use the same log.error pattern). Adjust the
catch block around execFile to call await shell.openExternal(url) in its own
try/catch and log the caught error with identifying context (referencing
execFile, log.debug, log.error, and shell.openExternal).

---

Outside diff comments:
In `@src/app/views/MattermostWebContentsView.ts`:
- Around line 502-524: The current branch only treats links matching the active
server (isInternalURL(parsedURL, server.url)) as internal, so links to other
configured Mattermost servers fall through to generateOpenInBrowserMenuItems;
update the condition to detect any configured server URL (e.g., iterate
configured servers or add a helper like isURLForConfiguredServer(parsedURL)) and
return the same in-app menu (separator + Open in new tab/window with ViewManager
and NavigationManager calls) when the link belongs to any configured server
rather than only server.url. Ensure you reference the existing isInternalURL
logic or reuse it against each configured server URL to avoid duplicating
navigation behavior.

---

Nitpick comments:
In `@src/main/browserManager.test.js`:
- Around line 94-97: Replace the CommonJS require() usage with dynamic ES-module
imports inside the async test lifecycle so the test file stays on the repo's ESM
path: remove the top-level require('./browserManager') and instead use await
import('./browserManager') inside an async beforeEach or the individual test,
then destructure the exported functions (getInstalledBrowsers,
openLinkInBrowser, clearBrowserCache) from the imported module; make the same
change for the other occurrence around the later block (near line 297) so no
require() calls remain in the file.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 12b8d085-0329-45c2-a548-93de7b2e1b9a

📥 Commits

Reviewing files that changed from the base of the PR and between 2729f7a and 62ba6c1.

📒 Files selected for processing (4)
  • i18n/en.json
  • src/app/views/MattermostWebContentsView.ts
  • src/main/browserManager.test.js
  • src/main/browserManager.ts

- Parse Windows registry command strings properly: extract quoted
  executable paths, preserve launcher flags, strip %1/%* placeholders
- Remove raw URL from debug log in openLinkInBrowser for privacy
- Await shell.openExternal fallback and catch its rejection
- Detect links to any configured server (not just active) for the
  "Open in new tab/window" context menu items
- Skip require->import nitpick: require() with jest.resetModules() is
  the established pattern across all test files in this codebase

https://claude.ai/code/session_01WWHsdZzBXhrUtXo9WYEwY4
Copy link
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.

🧹 Nitpick comments (1)
src/main/browserManager.ts (1)

131-132: Quoted arguments in registry commands may be split incorrectly.

The rest.split(/\s+/) approach doesn't handle quoted arguments with spaces (e.g., --profile "My Profile"). While uncommon in browser launcher commands, this could drop or corrupt such arguments.

♻️ Optional: Handle quoted arguments
-    // Filter out placeholders like %1, %*, etc. and keep real arguments
-    const args = rest.split(/\s+/).filter((token) => token && !(/(%\d|%\*)/).test(token));
+    // Filter out placeholders like %1, %*, etc. and keep real arguments
+    const args = (rest.match(/"[^"]*"|\S+/g) || [])
+        .filter((token) => !(/^"?%(\d|\*)"?$/).test(token));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/browserManager.ts` around lines 131 - 132, The current tokenization
uses rest.split(/\s+/) and will break quoted arguments (e.g., --profile "My
Profile"); change the tokenization used to build const args so it respects
quoted strings: parse rest into tokens that match either double-quoted,
single-quoted, or unquoted sequences (then strip surrounding quotes) and then
filter out placeholders as before; update the code around the args declaration
in browserManager.ts (look for the variables rest and args) to use this
quoted-aware splitting logic before the placeholder filter.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/main/browserManager.ts`:
- Around line 131-132: The current tokenization uses rest.split(/\s+/) and will
break quoted arguments (e.g., --profile "My Profile"); change the tokenization
used to build const args so it respects quoted strings: parse rest into tokens
that match either double-quoted, single-quoted, or unquoted sequences (then
strip surrounding quotes) and then filter out placeholders as before; update the
code around the args declaration in browserManager.ts (look for the variables
rest and args) to use this quoted-aware splitting logic before the placeholder
filter.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: c855777a-5623-4da1-85c9-547c0d3118d5

📥 Commits

Reviewing files that changed from the base of the PR and between 62ba6c1 and 5efc31f.

📒 Files selected for processing (3)
  • src/app/views/MattermostWebContentsView.ts
  • src/main/browserManager.test.js
  • src/main/browserManager.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/browserManager.test.js

claude added 2 commits March 16, 2026 14:52
Replace exec() with execFile() for the grep call that searches
.desktop files. The directory path is derived from process.env.HOME,
which is untrusted input — using exec() would interpolate it into
a shell command string, allowing injection. execFile() passes it
as an argument without shell interpretation.

https://claude.ai/code/session_01WWHsdZzBXhrUtXo9WYEwY4
- Replace parameter reassignment (no-param-reassign) with a
  resolveExecCallback helper that returns the correct callback
- Remove unnecessary quotes on HKLM property key (quote-props)
- Add blank line before inline comment (lines-around-comment)

https://claude.ai/code/session_01WWHsdZzBXhrUtXo9WYEwY4
Copy link
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: 1

🧹 Nitpick comments (1)
src/main/browserManager.test.js (1)

211-266: Consider adding a test for .desktop file parsing.

The Linux tests cover which-based detection but don't test the .desktop file parsing path when grep finds matches. Adding a test that mocks readFileSync returning valid .desktop content would improve coverage.

Example test case
it('should discover browsers from .desktop files', async () => {
    const fs = require('fs');
    mockExec.mockImplementation((cmd, opts, callback) => {
        const cb = resolveExecCallback(opts, callback);
        if (typeof cb === 'function') {
            cb(new Error('not found')); // no which matches
        }
    });
    mockExecFile.mockImplementation((file, args, opts, callback) => {
        const cb = resolveExecCallback(opts, callback);
        if (typeof cb === 'function') {
            cb(null, {stdout: '/usr/share/applications/custom-browser.desktop', stderr: ''});
        }
    });
    fs.readFileSync.mockReturnValue(
        '[Desktop Entry]\nName=Custom Browser\nExec=/usr/bin/custom-browser %u\n'
    );

    const browsers = await getInstalledBrowsers();
    expect(browsers.length).toBe(1);
    expect(browsers[0].name).toBe('Custom Browser');
    expect(browsers[0].executable).toBe('/usr/bin/custom-browser');
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/browserManager.test.js` around lines 211 - 266, Add a unit test that
exercises the .desktop parsing branch of getInstalledBrowsers by mocking
mockExec to return “not found” for which lookups and mocking mockExecFile to
return one or more .desktop file paths, then mock fs.readFileSync to return a
valid .desktop content string (including Name= and Exec= lines) and assert that
getInstalledBrowsers returns the parsed browser(s) with correct name and
executable; reference the existing test helpers mockExec, mockExecFile and
fs.readFileSync and add the new it('should discover browsers from .desktop
files', ...) case to the Linux describe block so the .desktop parsing code path
is covered.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/browserManager.ts`:
- Around line 131-132: The filtering misses quoted placeholders (e.g. "%1")
because the filter tests token directly; update the filter used when computing
args (the rest.split(...).filter(...) callback) to strip surrounding quotes from
token before testing or adjust the regex to accept optional surrounding quotes —
e.g., inside the filter normalize token (remove leading/trailing single or
double quotes) then test that normalized token against /(%\d|%\*)/ and exclude
it; ensure you still ignore empty tokens and preserve other logic.

---

Nitpick comments:
In `@src/main/browserManager.test.js`:
- Around line 211-266: Add a unit test that exercises the .desktop parsing
branch of getInstalledBrowsers by mocking mockExec to return “not found” for
which lookups and mocking mockExecFile to return one or more .desktop file
paths, then mock fs.readFileSync to return a valid .desktop content string
(including Name= and Exec= lines) and assert that getInstalledBrowsers returns
the parsed browser(s) with correct name and executable; reference the existing
test helpers mockExec, mockExecFile and fs.readFileSync and add the new
it('should discover browsers from .desktop files', ...) case to the Linux
describe block so the .desktop parsing code path is covered.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: f583f4aa-2797-49f1-b018-7a01374edf8d

📥 Commits

Reviewing files that changed from the base of the PR and between 5efc31f and 546aa18.

📒 Files selected for processing (2)
  • src/main/browserManager.test.js
  • src/main/browserManager.ts

Strip surrounding quotes from tokens before testing against the
placeholder regex so that "%1" and '%1' are filtered out alongside
unquoted %1. Add a test case for this scenario.

https://claude.ai/code/session_01WWHsdZzBXhrUtXo9WYEwY4
Copy link
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: 1

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

Inline comments:
In `@src/main/browserManager.ts`:
- Around line 238-247: The Exec parsing currently captures only the first token
and drops launcher arguments; change the logic that creates execMatch (from
content.match(/^Exec=(\S+)/m)) to capture the full Exec= line, then parse it
into an executable token and remaining args (respecting quoted tokens), strip
out placeholders like %u %U %f %F %F etc from the args, set executable to the
first token (handle quoted paths) and args to the remaining cleaned tokens, and
then perform the existing path.isAbsolute(execPath) && fs.existsSync(execPath)
check against that executable before pushing the found entry (referencing
execMatch, execPath, nameMatch, seenNames, and the found object with executable
and args).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: f5a71c24-0653-409c-881d-65333a35f2da

📥 Commits

Reviewing files that changed from the base of the PR and between 546aa18 and 7fa4b53.

📒 Files selected for processing (2)
  • src/main/browserManager.test.js
  • src/main/browserManager.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/browserManager.test.js

The previous regex only captured the first token of the Exec= line,
dropping launcher arguments (e.g. flatpak run org.example.browser).
Now captures the full line, splits into executable + args, and strips
.desktop field codes (%u, %U, %f, %F, etc.) including quoted variants.

https://claude.ai/code/session_01WWHsdZzBXhrUtXo9WYEwY4
Reject non-http(s) schemes (file://, ms-msdt://, etc.) in
generateOpenInBrowserMenuItems to prevent attacker-controlled
content from triggering dangerous URL schemes via the context menu.

https://claude.ai/code/session_01WWHsdZzBXhrUtXo9WYEwY4
Copy link
Member

@devinbinnie devinbinnie left a comment

Choose a reason for hiding this comment

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

Cool feature design, just needs a few adjustments :)

KNOWN_WINDOWS_BROWSERS.map(async (browser) => {
try {
const {stdout} = await exec(
`reg query "${root}\\${browser.registryKey}\\shell\\open\\command" /ve`,
Copy link
Member

Choose a reason for hiding this comment

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

We can avoid all of this code by instead using the registry-js module that is already built in to our Desktop App. See https://github.com/mattermost/desktop/blob/master/src/common/config/policyConfigLoader.ts for an example.

My approach would be to try and read the registry at start-up as well and pre-load all of the browsers (can't imagine hot-reloading needs to matter here) and store them here. One thing to try and make sure of is that the registry operations don't interfere with each other (I don't think that happens with the new module, but would be worth verifying)

}
}

// Also discover browsers from .desktop files that handle http(s)
Copy link
Member

Choose a reason for hiding this comment

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

I know this is also a way to capture Linux browsers, but is which not enough here?

Reason I ask is that I'd like to avoid the advanced matching and parsing of desktop files here, this is quite a bit more complicated that I would expect this to be. The regex pieces have me a bit worried that something could be exploited here.

}
}

export class BrowserManager {
Copy link
Member

Choose a reason for hiding this comment

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

I think this class is missing the above state cachedBrowsers. Anything that doesn't require state doesn't need a manager, but this does. We should encapsulate the state within the class and put the functions in here, same as the other examples.

This also makes it more testable.

Copy link
Member

Choose a reason for hiding this comment

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

Also, let's call this something more descript to the problem, maybe ExternalBrowserManager? Would probably be worth having a comment on it as to what it does too.


export class BrowserManager {
init = async () => {
await getInstalledBrowsers();
Copy link
Member

Choose a reason for hiding this comment

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

This should not be awaited, I/O operations can take a while and we don't want to hold up starting the application on this functionality. Once the browsers are populated then the menu items should work.

@devinbinnie devinbinnie added the 2: Dev Review Requires review by a core committer label Mar 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

2: Dev Review Requires review by a core committer release-note

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants