Repository navigation
Description
Area
apps/server
Steps to reproduce
- On Linux with a graphical desktop, install the background service (
t3 service install, lingering enabled), sot3code.servicestarts with the systemd user manager, before the desktop session. - Reboot, log in to the desktop, open the T3 Code desktop app (it pairs with the background service).
- In an integrated terminal, run
echo "${WAYLAND_DISPLAY-unset} ${DISPLAY-unset}"(printsunset unset), thenxdg-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.servicestarted 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-hyprland06:54:54). - The compositor then runs
dbus-update-activation-environment --systemd --all, so the user manager hasDISPLAY=:0,WAYLAND_DISPLAY=wayland-1andXDG_CURRENT_DESKTOP=Hyprland(systemctl --user show-environment). A unit started now gets them (systemd-run --user --wait --pipe -- env), butt3code.servicestarted before the import and has none of them. t3 serve(0.1.10-nightly.20261112.4606) and its terminals haveDBUS_SESSION_BUS_ADDRESSandXDG_RUNTIME_DIR, but noDISPLAY,WAYLAND_DISPLAYorXDG_CURRENT_DESKTOP. The desktop app process hasWAYLAND_DISPLAY=wayland-1andDISPLAY=: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
busctlcall (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.
- Export the variables in the terminal:
export WAYLAND_DISPLAY=wayland-1 DISPLAY=:0. With them set,xdg-opentakes the graphical path and launches the defaulthttpshandler (WAYLAND_DISPLAYalone is enough). Drawbacks: per shell only. The values are machine-specific (ls $XDG_RUNTIME_DIRshows the Wayland socket).xdg-opendoesn't check that the socket exists, so a stale value fails later, in the browser. - 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. - 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 installwith 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.
- Launchers older than 2026-10-20 (fix(server): preserve service environment across updates #19486) ignore the file, so run
- A systemd drop-in (
~/.config/systemd/user/t3code.service.d/display.confwith[Service]Environment=WAYLAND_DISPLAY=wayland-1Environment=DISPLAY=:0), thensystemctl --user daemon-reloadand a restart. Works with old launchers, which pass the unit'sEnvironment=through to the server.t3 service installrewrites only the unit file, not drop-ins. Drawbacks: the same as workaround 3, apart from the launcher version.
Activity