Skip to content

feat(linux): hybrid titlebar mode for clickable in-app topbar#538

Open
aaddrick wants to merge 2 commits intomainfrom
experiment/linux-topbar-wco
Open

feat(linux): hybrid titlebar mode for clickable in-app topbar#538
aaddrick wants to merge 2 commits intomainfrom
experiment/linux-topbar-wco

Conversation

@aaddrick
Copy link
Copy Markdown
Owner

@aaddrick aaddrick commented Apr 29, 2026

Summary

Adds a new hybrid titlebar mode (now the default) that makes claude.ai's in-app topbar work on Linux. Before this branch, the hamburger / sidebar / search / nav / Cowork ghost simply didn't render — the bundle gates the topbar behind an isWindows() UA check, and PR #127 forced frame: true to fix missing window controls, which definitively hid the topbar.

The hybrid mode keeps the native OS frame (close/min/max draw correctly) and uses a BrowserView preload shim to satisfy the bundle's UA gate. Shim spoofs navigator.userAgent, plus belt-and-suspenders matchMedia and windowControlsOverlay overrides, plus a className intercept that strips 'draggable' from any DOM assignment so .draggable { -webkit-app-region: drag } can't carve drag regions out of the framed content area.

Hybrid mode on KDE Plasma — DE-drawn "Claude" titlebar on top, claude.ai's in-app topbar (hamburger / search / back-forward) directly below it

Why hybrid instead of the upstream frame:false + WCO config

The upstream config matches Windows/macOS but has been broken on Linux: topbar renders, but mouse clicks don't fire. I tested four hypotheses (drag class on parent, titleBarOverlay height, titleBarStyle: 'hidden', X11-vs-Wayland) and disproved all four. The remaining cause is a Chromium-level implicit drag region for frame:false windows on both X11 and Wayland with no Electron-API override.

Full investigation chain, gates, and the three outstanding upstream Electron bugs are in docs/learnings/linux-topbar-shim.md. The hidden mode is kept on the branch as documented prior art and for future Wayland work; --doctor warns when it's active.

Modes

Mode Frame Topbar Status
hybrid (default) native renders + clickable working
native native hidden working, no in-app topbar
hidden frameless + WCO renders, clicks don't fire preserved for investigation

Known follow-up

The Cowork ghost icon (right side of the topbar on Windows) doesn't render in hybrid mode on Linux. The other four items (hamburger / sidebar / search / nav) all render and click correctly. Will track separately — the ghost likely has its own boot-features gate (coworkKappa / coworkArtifacts returning "unavailable" on Linux) that the topbar's UA check doesn't cover.

What changed

New:

  • scripts/wco-shim.js — BrowserView preload shim (UA + matchMedia + WCO + className intercept + diagnostic probes)
  • scripts/patches/wco-shim.sh — inlines the shim at top of mainView.js at build
  • docs/learnings/linux-topbar-shim.md — investigation history, gates, upstream Electron bugs A/B/C, diagnostic recipes

Removed:

  • scripts/patches/titlebar.sh (replaced by wco-shim approach)

Modified:

  • scripts/frame-fix-wrapper.jshybrid value, default switch, three-way BrowserWindow branch, WCO probe, shim console mirror to launcher.log, Ctrl+Q handler now attaches to all webContents (old per-main-window handler missed BrowserView keypresses)
  • scripts/launcher-common.sh_resolve_titlebar_style(), mode-aware feature flags, mode-aware ELECTRON_USE_SYSTEM_TITLE_BAR
  • scripts/doctor.sh — reports resolved titlebar style: PASS for hybrid/native, WARN for hidden, WARN + valid-value hint for unrecognized values
  • tests/launcher-common.bats — 16 new cases for resolver + flag selection + env-var wiring per mode
  • scripts/patches/app-asar.sh, build.sh — wire up wco-shim.sh in place of titlebar.sh
  • docs/CONFIGURATION.md — three-mode table, --doctor mention
  • CLAUDE.md, .claude/agents/electron-linux-specialist.md, .claude/agents/issue-triage.md — reference updates

@sabiut

Two files in your domain per CODEOWNERS — take a look when you get a chance:

  • tests/launcher-common.bats (+108, 16 new cases for _resolve_titlebar_style + build_electron_args + setup_electron_env)
  • scripts/doctor.sh (+23, titlebar style validation block)

Rest is mine.

Test plan

Automated:

  • bats tests/launcher-common.bats passes (64/64)
  • claude-desktop --doctor reports PASS for hybrid (unset + explicit) and native, WARN for hidden, WARN+fallback for unrecognized values

Behavioral verification by DE+backend (distro doesn't matter for the click logic — Chromium is the same; package format covered separately by test-artifact-*.sh):

  • KDE Plasma + X11 (Fedora KDE / Nobara) — topbar renders, buttons clickable, native frame draws close/min/max
  • KDE Plasma + Wayland (Fedora KDE / Nobara) — same, with CLAUDE_USE_WAYLAND=1
  • GNOME + Wayland (Mutter) — needs verification
  • GNOME + X11 (XWayland or pure X) — needs verification
  • Hyprland (wlroots) — needs verification
  • Sway / i3 (tiling) — needs verification
  • Niri (already has special-cased Wayland forcing in launcher) — needs verification
  • NixOS (nix build, any DE) — needs verification (separate launcher path, not covered by test-artifact-*.sh)

Mode coverage (KDE Plasma X11):

  • Topbar buttons NOT clickable in hidden on X11 — confirms upstream config is still broken; --doctor warns correctly
  • Topbar buttons NOT clickable in hidden on Wayland — confirms Bug C is not X11-specific
  • Ctrl+Q quits from any focused webContents (main window or BrowserView) — after the all-webContents handler fix

Generated with Claude Code
Co-Authored-By: Claude Opus 4.7 noreply@anthropic.com
75% AI / 25% Human
Claude: code, doc rewrites, test additions, investigation narrative
Human: empirical click testing across X11/Wayland/all-modes, strategic direction (hybrid pivot), build+install loop

Default `CLAUDE_TITLEBAR_STYLE` is now `hybrid`: native OS frame
plus a BrowserView preload shim that convinces claude.ai's bundle
to render its in-app topbar (hamburger / sidebar / search / nav /
Cowork ghost). Stacked layout instead of Windows's combined bar,
but every button is clickable.

Why not the upstream `frame:false` + WCO config: investigation
(see docs/learnings/linux-topbar-shim.md) ruled out
`titleBarOverlay`, `titleBarStyle:'hidden'`, and the `.draggable`
CSS class as the source of the topbar click-eating drag region.
The remaining cause is a Chromium-level implicit drag region for
`frame:false` windows that exists on both X11 and Wayland and has
no Electron-API knob. With `frame:true` the OS handles dragging
and Chromium pushes no drag-region map, so the buttons receive
mouse events normally.

Modes:
- `hybrid` (default) — system frame + shim, topbar visible and
  clickable
- `native` — system frame, no shim, no in-app topbar
- `hidden` — frameless + WCO config, matches Windows/macOS
  upstream; topbar visible but not clickable on Linux. Kept for
  Wayland comparison and future investigation

Tests: tests/launcher-common.bats grew 16 cases covering
`_resolve_titlebar_style`, `build_electron_args` flag selection
per mode, and `setup_electron_env` env-var wiring per mode.
`claude-desktop --doctor` now reports the resolved mode and
warns when `hidden` is set.

Co-Authored-By: Claude <claude@anthropic.com>
@aaddrick aaddrick requested a review from sabiut as a code owner April 29, 2026 14:44
Visual reference of the stacked layout: DE-drawn titlebar on top
with native window controls, claude.ai's in-app topbar
(hamburger / search / back-forward) immediately below it.

Co-Authored-By: Claude <claude@anthropic.com>
@aaddrick
Copy link
Copy Markdown
Owner Author

@typedrat - I don't think this will impact Nix at all, but do you mind verifying?

@typedrat
Copy link
Copy Markdown
Collaborator

@typedrat - I don't think this will impact Nix at all, but do you mind verifying?

Not at all, and I can also knock out testing on Hyprland while I'm at it.

@typedrat
Copy link
Copy Markdown
Collaborator

Working great on NixOS with Hyprland.

@lukedev45
Copy link
Copy Markdown

2026-04-29.18-18-31.mp4

Looks like its not working on OmarchyOS Hyprland

@aaddrick
Copy link
Copy Markdown
Owner Author

@lukedev45 thanks for the video, that's really helpful. I reproduced what you're seeing locally by building an AppImage with the WCO shim patch disabled (patch_wco_shim commented out in scripts/patches/app-asar.sh) and got the same partial render on KDE Plasma + Wayland. So the symptom looks like the shim either isn't loading or is losing the race against claude.ai's isWindows() UA check.

I went looking at Omarchy to see what might be different. DHH's default/hypr/envs.conf exports a bunch of Ozone-Wayland defaults session-wide — ELECTRON_OZONE_PLATFORM_HINT=wayland, OZONE_PLATFORM=wayland, GDK_BACKEND=wayland,x11,*, QT_QPA_PLATFORM=wayland;xcb, MOZ_ENABLE_WAYLAND=1, SDL_VIDEODRIVER=wayland,x11. My hypothesis was these would override our launcher's --ozone-platform=x11 flag and shift BrowserView preload timing enough to lose the UA-spoof race.

I tested four scenarios on my KDE Plasma + Wayland box: the three most relevant Omarchy vars inline, all six together, CLAUDE_USE_WAYLAND=1 on its own, and CLAUDE_USE_WAYLAND=1 combined with the Omarchy vars. Topbar rendered correctly in every case. @typedrat also confirmed it working on NixOS + Hyprland earlier, so Hyprland-the-compositor isn't the variable on its own — something is Omarchy-specific or specific to your setup, and we can't reproduce it from here.

Could you grab three things from a broken run? claude-desktop --doctor (or ./claude-desktop-...AppImage --doctor), ~/.cache/claude-desktop-debian/launcher.log (the shim logs diagnostic probes including the BrowserView's userAgent and nativeMode), and env | grep -E 'OZONE|WAYLAND|GDK_BACKEND|QT_QPA|HYPRLAND|XDG_'. That should tell us whether the shim is even getting injected and what the actual UA looks like at probe time. Once we have that I can dig in properly.


Written by Claude Opus 4.7 via Claude Code

@lukedev45
Copy link
Copy Markdown

No problem, will take a look tomorrow

@rootedetc
Copy link
Copy Markdown

Works fine in Gnome - Wayland, will confirm tomorrow for Sway

image

@aaddrick
Copy link
Copy Markdown
Owner Author

electron/electron#51396

Issue and PR open with electron to fix the underlying issue that makes the shim a requirement.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants