Skip to content

fix(desktop): Stop hiding an installed Linux launcher with the URL handler entry - #31405

Open
andersk wants to merge 2 commits into
pingdotgg:mainfrom
andersk:fix/linux-installed-desktop-entry
Open

andersk wants to merge 2 commits into
pingdotgg:mainfrom
andersk:fix/linux-installed-desktop-entry

Conversation

@andersk

@andersk andersk commented Nov 5, 2026 •

Copy link
Copy Markdown

Problem

On Linux, every launch writes a hidden URL handler entry to $XDG_DATA_HOME/applications/com.t3tools.T3Code.desktop (NoDisplay=true, no Categories). Entries in $XDG_DATA_HOME take precedence over those in $XDG_DATA_DIRS. So when a package installs its launcher as com.t3tools.T3Code.desktop, the name that matches the app id set by setDesktopName, the first launch overrides it. T3 Code disappears from application menus, and the app id resolves to the hidden entry instead of the installed launcher. This happens even when the package sets T3CODE_DESKTOP_LAUNCHER_PATH. The file stays in $XDG_DATA_HOME and is rewritten on every launch, so the installed launcher stays hidden.

Change

Before writing the handler entry, both writers (DesktopPreReadyPlatform and DesktopLinuxUrlHandler) check whether $XDG_DATA_DIRS (defaulting to /usr/local/share:/usr/share per the XDG Base Directory spec) has an applications/com.t3tools.T3Code.desktop. If it does:

  • they skip writing the hidden entry, and
  • they remove one left by an earlier launch, but only when it has the generated NoDisplay=true and MimeType=x-scheme-handler/<scheme>; lines, which every version of renderUrlHandlerDesktopEntry has written. An entry without those lines, the usual shape of a user's own override, is kept. An edited generated entry is treated like the generated one: main already rewrites it on every launch, so an edited Exec there never took effect.

The rest of registration is unchanged. xdg-mime default com.t3tools.T3Code.desktop still names the same desktop id, which now resolves to the installed entry. The window id stays com.t3tools.T3Code. AppImage and development launches have no installed entry with that name, so they behave as before. An $XDG_DATA_DIRS entry equal to $XDG_DATA_HOME is ignored.

Scope and approval

This is a small, focused fix for an obvious bug: a hidden helper entry should not override the installed launcher with the same name. It keeps the single Linux window id requested in the triage of #13975 and doesn't change the .deb's t3code.desktop naming or #10895's StartupWMClass direction. Renaming the .deb launcher to the app id is a separate question for maintainers. This fix is a prerequisite for that rename, since otherwise the hidden entry would override the renamed launcher.

Verification

  • New tests in DesktopLinuxUrlHandler.test.ts and DesktopPreReadyPlatform.test.ts cover: an installed entry with a stale generated entry (removed, nothing written, update-desktop-database and xdg-mime default still run), a generated entry whose Exec was edited (still treated as generated), an installed entry with a user's own entry (kept), relative $XDG_DATA_DIRS entries (ignored), and an $XDG_DATA_DIRS that includes $XDG_DATA_HOME (still written). The four deferral cases failed on main and pass with this change. All 56 tests in the four affected apps/desktop/src/app test files pass, and vp run check:changed passed format, lint, knip, typecheck, and tests.
  • End-to-end on NixOS: I ran vp run build:desktop and launched the result with Electron 44.9.3 under Xvfb (--ozone-platform=x11 --disable-gpu) for 25 seconds. That got through window creation and desktop-lifecycle startup. XDG_DATA_HOME, HOME, and T3CODE_HOME pointed at throwaway directories, and XDG_DATA_DIRS pointed at a directory with an installed applications/com.t3tools.T3Code.desktop.
    • Stale generated entry in $XDG_DATA_HOME: on main it was rewritten. With this change it was removed and not recreated during startup.
    • User's own entry in $XDG_DATA_HOME: left byte-for-byte unchanged.
    • No installed entry: the hidden entry was written as before.

Not checked: packaged builds (AppImage, .deb, AUR), which skip this path because they have no installed com.t3tools.T3Code.desktop, and how a real desktop shell displays the result. These launches were unpackaged, so xdg-mime and the icon steps did not run.

Claude Opus 5.5, harness: T3 Code

🤖 Generated with Claude Code

@github-actions github-actions Bot added vouch:unvouched Author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Nov 5, 2026
// Recognizes a previous launch's entry, so a user's own override is never deleted.
export const isGeneratedUrlHandlerDesktopEntry = (content: string, scheme: string): boolean => {
const lines = content.split("\n");
return lines.includes("NoDisplay=true") && lines.includes(`MimeType=x-scheme-handler/${scheme};`);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium app/DesktopLinuxUrlHandler.ts:102

A user-customized URL-handler entry is classified as generated, so the pre-ready cleanup deletes the customized file and loses the user's Exec override. NoDisplay and MimeType remain unchanged when Exec is edited; compare the full entry with the expected generated content before deletion, or preserve entries that differ.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/desktop/src/app/DesktopLinuxUrlHandler.ts around line 102:

A user-customized URL-handler entry is classified as generated, so the pre-ready cleanup deletes the customized file and loses the user's `Exec` override. `NoDisplay` and `MimeType` remain unchanged when `Exec` is edited; compare the full entry with the expected generated content before deletion, or preserve entries that differ.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Not a regression: on main, DesktopPreReadyPlatform rewrites this file unconditionally on every launch, so an Exec edit to the generated entry has never survived the next launch. Comparing the whole entry would also leave many stale entries in place: Name= follows the release-stage branding (Alpha, Beta, Nightly), the format has changed (#8673 added Icon=), and Exec changes whenever the launcher path does (for example a new Electron major version on a shared Electron, or a new store path on NixOS). Entries without the generated NoDisplay=true and MimeType= lines, the usual shape of a user's own override, are kept and covered by tests in both modules.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

@macroscopeapp

macroscopeapp Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs Human Approval

Macroscope's review found this PR requires human approval — The change prevents generated URL-handler entries from hiding installed Linux launchers. Cleanup can also delete customized entries and lose user Exec overrides; preservation of those customizations needs resolution.

Needs human approval because:

  • 1 blocking correctness issue found at or above your repo's Minimum Blocking Severity

Adjust the Minimum Blocking Severity for this repo — including turning it Off — in Settings. You can add or adjust custom eligibility rules. Learn more.

@coderabbitai

coderabbitai Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Explain Change →

No actionable comments were generated in the recent review.

Walkthrough

Linux desktop setup and URL-handler registration now check configured XDG system application directories for matching desktop entries. The environment resolves those directories from XDG_DATA_DIRS, using default paths when no absolute paths are provided.

Changes: Linux desktop entries

Layer / File(s) Summary
Resolve XDG application directories
apps/desktop/src/app/DesktopConfig.ts, apps/desktop/src/app/DesktopEarlyElectronStartup.ts, apps/desktop/src/app/DesktopEnvironment.ts
DesktopConfig reads the optional XDG_DATA_DIRS value. The environment resolves absolute directory paths, uses default paths when needed, and exposes their applications subdirectories.
Check installed entries during pre-ready setup
apps/desktop/src/app/DesktopPreReadyPlatform.ts, apps/desktop/src/app/DesktopPreReadyPlatform.test.ts
Pre-ready setup checks other XDG data directories for a matching desktop entry. If one exists, it removes a matching generated local handler and leaves other local entries unchanged. Otherwise, it creates or refreshes the local entry. Tests cover generated and user-owned local entries.
Check installed entries during URL-handler registration
apps/desktop/src/app/DesktopLinuxUrlHandler.ts, apps/desktop/src/app/DesktopLinuxUrlHandler.test.ts
Registration checks other configured application directories for a matching desktop entry. It removes a local entry only if it matches the generated-handler predicate. Tests cover installed entries, differing user-owned entries, and entries under the user applications directory.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant DesktopConfig
  participant DesktopEnvironment
  participant DesktopPreReadyPlatform
  participant FileSystem
  DesktopConfig->>DesktopEnvironment: Provide XDG_DATA_DIRS
  DesktopEnvironment->>DesktopPreReadyPlatform: Provide system applications directories
  DesktopPreReadyPlatform->>FileSystem: Check for matching system desktop entry
  FileSystem-->>DesktopPreReadyPlatform: Entry exists or is absent
  DesktopPreReadyPlatform->>FileSystem: Remove generated local entry or write local entry
Loading

Suggested reviewers: juliusmarminge, maria-rcks, limwa

Merge Risk: Minimal · up to 85105

This change stops a hidden URL-handler entry from hiding an installed Linux launcher, and it preserves entries the user created. No concrete failure was established. One reported edge case remains unverified: a Nix profile whose desktop database is not updated might not dispatch the SSO redirect after the generated entry is removed. Packaged and real-desktop behavior were not tested, so check that case if it matters to you.

Pre-merge checks: Passed 4

Check name Status Explanation
Title check Passed The title names the main change: the URL handler entry should no longer hide an installed Linux launcher. It follows the conventional commit format and matches the changes to the desktop writers and t…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Description check Passed The description follows the repository template closely. It has all four required sections: Problem, Change, Scope, and Verification. The Problem section states the expected behavior and the environme…
Full details: Title check

Explanation

The title names the main change: the URL handler entry should no longer hide an installed Linux launcher. It follows the conventional commit format and matches the changes to the desktop writers and the XDG_DATA_DIRS check.

Full details: Description check

Explanation

The description follows the repository template closely. It has all four required sections: Problem, Change, Scope, and Verification. The Problem section states the expected behavior and the environment, which is NixOS with XDG_DATA_DIRS. The Change section explains the fix and why the rest of registration is unchanged. The Scope section explains why this is a bug fix, links the related issue, and states that a maintainer decision on the .deb rename is out of scope. The Verification section lists focused unit tests with results, an end-to-end run on NixOS, and items that were not checked. The description has one small gap. The Problem section links a PR comment and #32482, but it does not link the issue that shows the bug directly. The Scope section does say that the author linked the earlier triage, so the gap is minor. The description also ends with a model and harness attribution, which the template asks for.

Finishing Touches

Generate unit tests (beta)

  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Recent review info

Run configuration

Commits

Reviewing files that changed from the base of the PR and between 0fa11d7 and 85105e0.

Files selected for processing: 4

  • apps/desktop/src/app/DesktopLinuxUrlHandler.test.ts
  • apps/desktop/src/app/DesktopLinuxUrlHandler.ts
  • apps/desktop/src/app/DesktopPreReadyPlatform.test.ts
  • apps/desktop/src/app/DesktopPreReadyPlatform.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.

Wladefant

This comment was marked as spam.

// Recognizes a previous launch's entry, so a user's own override is never deleted.
export const isGeneratedUrlHandlerDesktopEntry = (content: string, scheme: string): boolean => {
const lines = content.split("\n");
return lines.includes("NoDisplay=true") && lines.includes(`MimeType=x-scheme-handler/${scheme};`);

@andersk andersk Nov 5, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Like I explained above (at about the same time you posted): no, a user cannot edit Exec, because main rewrites this file unconditionally on every launch.

A full-content comparison or a new ownership marker would leave stale entries from earlier releases in place (branding changes Name=, #8673 added Icon=, and Exec follows the launcher path), which is what this cleanup is for.

I’ve clarified the description: entries without the generated lines are kept, and an edited generated entry is treated like the generated one, as before.

});
});

it.effect("keeps a user's own entry next to an installed entry", () => {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I’ve added a test asserting the opposite, because it’s intentional.

@andersk
andersk requested a review from Wladefant November 5, 2026 12:20
Wladefant

This comment was marked as spam.

@maria-rcks

Copy link
Copy Markdown
Collaborator

Note

Written by claude-opus-5-5 on behalf of Maria

Verified in a real dev environment

Head 0fa11d7543de against main d2c25950b4, Ubuntu VM with Xorg. Each worktree ran vp run dev:desktop (dev entry id com.t3tools.T3Code.Development.desktop) with XDG_DATA_HOME and XDG_DATA_DIRS pointed at throwaway dirs. The "installed launcher" is a visible entry with that id in $XDG_DATA_DIRS/applications, Categories=Development; and MimeType=x-scheme-handler/t3code-dev;. After each launch I checked the $XDG_DATA_HOME entry and asked GIO (Gio.DesktopAppInfo.new(id), the lookup desktop shells use) which file the id resolves to.

Launch main PR
A: no installed launcher hidden entry written hidden entry written (unchanged)
B: installed launcher + generated entry from A hidden entry rewritten; id resolves to it, should_show() = False hidden entry removed; id resolves to the installed launcher, should_show() = True
C: installed launcher + user's own entry (no NoDisplay/MimeType) user's entry overwritten with the generated one user's entry kept byte-for-byte (sha256 e39f55a5… before and after)

PR launch B transcript:

=== B: installed launcher in $XDG_DATA_DIRS + generated entry left by launch A (0fa11d7543)
before launch:
-rw-rw-r-- 1 user user 260 14:29:45 /tmp/xdg-pr/home/applications/com.t3tools.T3Code.Development.desktop
--- $XDG_DATA_HOME/applications/com.t3tools.T3Code.Development.desktop:
(absent)
--- GIO lookup of com.t3tools.T3Code.Development.desktop:
resolves to: /tmp/xdg-pr/sys/applications/com.t3tools.T3Code.Development.desktop
shown in menus: True
default for x-scheme-handler/t3code-dev: com.t3tools.T3Code.Development.desktop

Same launch on main: the entry's mtime moved to the launch time, and GIO resolved the id to /tmp/xdg-main/home/applications/... with shown in menus: False.

Nearby behavior: with the installed launcher present and no hidden entry, the PR's desktop window started normally. In that window I added a local git repo as a project, picked Claude Haiku 5.5, and ran a real turn. It finished in 6.2s with the correct answer. The hidden entry was still absent after the runtime URL handler registration and the turn.

PR desktop app launched with an installed launcher, Claude Haiku 5.5 turn completed in demo-repo

Not verified

  • Packaged builds (.deb, AppImage, AUR). The xdg-mime default and icon steps only run when packaged, so the dev launches skipped them.
  • Deep links opened through the installed launcher, and how a real desktop shell menu displays the result. Menu visibility was checked through GIO only.
  • Production id com.t3tools.T3Code.desktop. The dev launches use the .Development id through the same code path.

@github-actions

Copy link
Copy Markdown
Contributor

This PR conflicts with main. Rebase it or merge main into it. If it still conflicts after 7 days with no activity, it will be closed automatically.

See the conflict window.

@github-actions github-actions Bot added the needs-rebase Conflicts with its base branch. Closes after 7 days with no activity. label Nov 12, 2026
@limwa

limwa commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

Before writing the handler entry, both writers (DesktopPreReadyPlatform and DesktopLinuxUrlHandler) check whether $XDG_DATA_DIRS (defaulting to /usr/local/share:/usr/share per the XDG Base Directory spec) has an applications/com.t3tools.T3Code.desktop. If it does:

  • they skip writing the hidden entry, and

Leaving this here as an edge-case. I install my T3 Code using nix profile install .... It adds a desktop entry to /home/<user>/.nix-profile/share/applications/, but it doesn't update the desktop database for that directory as that's not Nix's responsibility (also, it's readonly). However, because of that, GNOME doesn't recognize that desktop entry as a URL handler, even if xdg-mime default ... is executed. So, to be able to login in T3 Connect via Github SSO, I rely on the hidden entry being written and update-desktop-database being run with it.

I understand nix is not a supported installation method, but I was hoping we could come to a better solution that can accommodate everyone's concerns.

Anyways, the hidden entry was never meant to show up on DE menus. It was initially introduced for the specific purpose of allowing the redirect of Github SSO in T3 Connect to be captured by T3 Code, hence why it has NoDisplay=true, and no Categories.

@limwa

limwa commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

Based on this, wouldn't the better solution be something like: com.t3tools.T3Code.desktop only written if there's not one in $XDG_DATA_DIRS, and then com.t3tools.T3Code-redirect.desktop to capture the redirect, always written?

This is something I just came up with and haven't given much thought, so let me know what you think!

Also, how do other AppImage apps solve this? Do they all write a desktop entry to ~/.local/share/applications/?

andersk and others added 2 commits November 13, 2026 14:04
…ndler entry

On Linux, startup writes a NoDisplay=true URL handler entry named
com.t3tools.T3Code.desktop to $XDG_DATA_HOME/applications. Entries in
$XDG_DATA_HOME override same-named entries in $XDG_DATA_DIRS, so when a
package installs its launcher under the app id, that launcher disappears
from application menus and the app id resolves to the hidden entry.

When an entry with the app id is installed in $XDG_DATA_DIRS, skip
writing the handler entry and remove one written by an earlier launch.
An entry without the generated NoDisplay and MimeType lines is a user's
own override and is kept. AppImage and development launches, which have
no installed entry with that name, are unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Anders Kaseorg <[email protected]>
Startup has always rewritten the generated handler entry, so an edited
Exec in it never took effect. Test that such an entry still counts as
generated.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Anders Kaseorg <[email protected]>
@andersk
andersk force-pushed the fix/linux-installed-desktop-entry branch from 0fa11d7 to 85105e0 Compare November 13, 2026 22:12
@andersk

andersk commented Nov 13, 2026

Copy link
Copy Markdown
Author

@limwa Thanks, I checked this. First, this PR changes nothing for nix-t3code as it is today: it installs t3code.desktop, not com.t3tools.T3Code.desktop, so the hidden entry is still written and update-desktop-database still runs on it.

If a package does install com.t3tools.T3Code.desktop in a read-only directory without a mimeinfo.cache, the scheme should still work. The URL handler registration that DesktopApp starts at every launch (DesktopLinuxUrlHandler.register) only swaps writing the hidden entry for removing it. It then still runs xdg-mime default com.t3tools.T3Code.desktop x-scheme-handler/t3code, as on main. That writes the default to $XDG_CONFIG_HOME/mimeapps.list (xdg-mime). Per the MIME Applications spec, a default resolves to the named desktop file wherever it is installed, provided the entry is associated with the type through its MimeType key. mimeinfo.cache is only an index of those keys.

I checked with GLib 2.90 and xdg-utils, in a clean environment where XDG_DATA_DIRS pointed at a directory containing only applications/com.t3tools.T3Code.desktop (with MimeType=x-scheme-handler/t3code;), and with an empty $XDG_DATA_HOME:

mimeinfo.cache mimeapps.list default gio open t3code://…
none none Operation not supported
none set by xdg-mime default launches the installed entry
in the system directory none launches the installed entry

xdg-open t3code://… with XDG_CURRENT_DESKTOP=GNOME also launched it after xdg-mime default. (gio open resolves the handler with g_app_info_get_default_for_uri_scheme.) So the cache only matters when no default is set, and registration always sets one. The xdg-mime step runs only when app.isPackaged is true (the check), which nix-t3code already gets from ELECTRON_FORCE_IS_PACKAGED (here).

On a separate com.t3tools.T3Code-redirect.desktop: it would work, but it would make a hidden entry, rather than the packaged launcher, the scheme handler on every install. It also needs a correct Exec, which the packaged launcher already has.

On AppImages: integration tools (AppImageLauncher, Gear Lever) copy the AppImage’s own desktop entry into ~/.local/share/applications under a name they choose. Without one, the app has to write its own entry, which is why this PR keeps writing the hidden entry whenever no com.t3tools.T3Code.desktop is installed.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-rebase Conflicts with its base branch. Closes after 7 days with no activity. size:M 30-99 changed lines (additions + deletions). vouch:unvouched Author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants