Skip to content

[Bug]: Linux background service: terminals and agent shells have no DISPLAY/WAYLAND_DISPLAY, so xdg-open and browser logins fail #35736

Open

Description

@cravenceiling

Area

apps/server

Steps to reproduce

  1. On Linux with a graphical desktop, install the background service (t3 service install, lingering enabled), so t3code.service starts with the systemd user manager, before the desktop session.
  2. Reboot, log in to the desktop, open the T3 Code desktop app (it pairs with the background service).
  3. In an integrated terminal, run echo "${WAYLAND_DISPLAY-unset} ${DISPLAY-unset}" (prints unset unset), then xdg-open https://example.com.

A real-world trigger is npm login: it prints the login URL and runs xdg-open <url> when you press Enter.

Expected behavior

Integrated terminals and agent shells see the graphical session, so xdg-open opens the default browser, as it does in a terminal emulator from the same desktop session.

Actual behavior

xdg-open uses a graphical browser only when DISPLAY or WAYLAND_DISPLAY is non-empty (its has_display() check). Both are unset, so it falls back to text browsers and fails with exit code 3:

/usr/bin/xdg-open: line 1045: www-browser: command not found
/usr/bin/xdg-open: line 1045: links2: command not found
/usr/bin/xdg-open: line 1045: elinks: command not found
/usr/bin/xdg-open: line 1045: links: command not found
/usr/bin/xdg-open: line 1045: lynx: command not found
/usr/bin/xdg-open: line 1045: w3m: command not found
xdg-open: no method available for opening 'https://example.com/t3-test'

Agent shells have the same environment: a shell run by a Claude agent session on the same server also has neither variable.

Cause

Terminals inherit the server's own environment: baseEnv = options.env ?? process.env in apps/server/src/terminal/Manager.ts:1595 (at c99201c), passed through createTerminalSpawnEnv (Manager.ts:1407), which only removes keys.

A boot-started service never has the display variables. On the affected machine:

  • Today's boot: t3code.service started at 07:57:03.621 (journal: "Started T3 Code server"); uwsm selected the compositor at 07:57:04.619. The previous boot had the same order (service 06:54:52.868, graphical login session 06:54:53, start-hyprland 06:54:54).
  • The compositor then runs dbus-update-activation-environment --systemd --all, so the user manager has DISPLAY=:0, WAYLAND_DISPLAY=wayland-1 and XDG_CURRENT_DESKTOP=Hyprland (systemctl --user show-environment). A unit started now gets them (systemd-run --user --wait --pipe -- env), but t3code.service started before the import and has none of them.
  • t3 serve (0.1.10-nightly.20261112.4606) and its terminals have DBUS_SESSION_BUS_ADDRESS and XDG_RUNTIME_DIR, but no DISPLAY, WAYLAND_DISPLAY or XDG_CURRENT_DESKTOP. The desktop app process has WAYLAND_DISPLAY=wayland-1 and DISPLAY=:0, but terminals are spawned by the service, not by the app.

This is the root cause of #24508 / #25228, which fixed it for editor launches only: readGraphicalSessionEnv (apps/server/src/process/externalLauncher.ts:697) reads DISPLAY, WAYLAND_DISPLAY and XAUTHORITY from the user manager over D-Bus, and only the spawned editor gets them (externalLauncher.ts:797). #33107 (open) extends it to file-manager discovery. Terminals and agent sessions are not covered.

Related pattern: #34354 (the server reads the login-shell PATH only at startup, so later changes are missed until a restart).

Suggested fix

When the server runs on Linux without a display, add the user manager's graphical-session variables to the terminal and agent spawn environment, reusing readGraphicalSessionEnv. Read it at spawn time rather than at server startup, so a desktop session that starts after the server is picked up. Variables already in the server's environment keep precedence.

Costs and limits:

  • One busctl call (bounded at 2 s by the existing helper) per terminal or agent spawn, only on Linux servers without a display. Caching it would miss a login that happens later.
  • On a headless host the user manager has no display, so nothing changes.
  • Terminals opened before the user logs in to the desktop still won't have the variables. Only new terminals get them.
  • The values could be stale if the desktop session ends and another starts with a different socket name. This is the same exposure the editor launch already has.

Impact

Has a workaround. Any CLI that opens a browser from an integrated terminal or agent shell fails on Linux with the background service.

Version or commit

Desktop AppImage 0.1.10-nightly.20261112.4580, t3 serve 0.1.10-nightly.20261112.4606 (first seen on 0.1.9-nightly.20261111.4515). Code references are at main c99201c.

Environment

Arch Linux (Omarchy), Hyprland (Wayland) via uwsm, kernel 7.2.11. Background service t3code.service with lingering.

Workaround

Each one was checked as described. All have drawbacks.

  1. Export the variables in the terminal: export WAYLAND_DISPLAY=wayland-1 DISPLAY=:0. With them set, xdg-open takes the graphical path and launches the default https handler (WAYLAND_DISPLAY alone is enough). Drawbacks: per shell only. The values are machine-specific (ls $XDG_RUNTIME_DIR shows the Wayland socket). xdg-open doesn't check that the socket exists, so a stale value fails later, in the browser.
  2. Restart the service after logging in (t3 service restart): the server starts with the user manager's current environment, which now includes the display variables. Drawbacks: it ends every running terminal and Claude agent session. Terminal shells exit when the server's PTY master closes, and Claude agents are killed when the server exits (apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts:375). It has to be repeated after every boot.
  3. Fixed values in ~/.t3/service.env (WAYLAND_DISPLAY=wayland-1, DISPLAY=:0), then restart the service. The launcher passes these entries to the server (apps/server/src/serviceLauncher.ts:252). Drawbacks:
    • Launchers older than 2026-10-20 (fix(server): preserve service environment across updates #19486) ignore the file, so run t3 service install with a current CLI first. It rewrites the unit with the current runtime's launcher.
    • The values are fixed, so they go stale if the socket name changes.
    • Before you log in, terminals see a display that doesn't exist yet, so tools fail against a dead socket instead of falling back to a text mode.
    • The restart has the costs of workaround 2, once.
  4. A systemd drop-in (~/.config/systemd/user/t3code.service.d/display.conf with [Service] Environment=WAYLAND_DISPLAY=wayland-1 Environment=DISPLAY=:0), then systemctl --user daemon-reload and a restart. Works with old launchers, which pass the unit's Environment= through to the server. t3 service install rewrites only the unit file, not drop-ins. Drawbacks: the same as workaround 3, apart from the launcher version.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions