Files
claude-desktop-debian/scripts/patches/app-asar.sh
Aaddrick 5c8191e82f feat(linux): hybrid titlebar mode for clickable in-app topbar (#538)
* feat(linux): hybrid titlebar mode for clickable in-app topbar

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>

* docs(learnings): add hybrid-mode screenshot

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>

* docs(learnings): fix codespell hit (Pre-emptive → Preemptive)

Codespell flags hyphenated "Pre-emptive" as a misspelling of
"Preemptive". Drops the hyphen to clear the spellcheck CI gate
on PR #538.

Co-Authored-By: Claude <claude@anthropic.com>

---------

Co-authored-by: Claude <claude@anthropic.com>
2026-05-01 02:47:16 -04:00

106 lines
4.0 KiB
Bash

#===============================================================================
# Top-level app.asar patch orchestration: extract, wrap entry point, stub
# native module, copy i18n and tray icons, then invoke per-feature patches.
#
# Sourced by: build.sh
# Sourced globals:
# claude_extract_dir, app_staging_dir, asar_exec, source_dir
# Modifies globals: (none directly — delegated patches may mutate electron_var)
#===============================================================================
patch_app_asar() {
echo 'Processing app.asar...'
cp "$claude_extract_dir/lib/net45/resources/app.asar" "$app_staging_dir/" || exit 1
cp -a "$claude_extract_dir/lib/net45/resources/app.asar.unpacked" "$app_staging_dir/" || exit 1
cd "$app_staging_dir" || exit 1
"$asar_exec" extract app.asar app.asar.contents || exit 1
# Frame fix wrapper
echo 'Creating BrowserWindow frame fix wrapper...'
local original_main
original_main=$(node -e "const pkg = require('./app.asar.contents/package.json'); console.log(pkg.main);")
echo "Original main entry: $original_main"
cp "$source_dir/scripts/frame-fix-wrapper.js" app.asar.contents/frame-fix-wrapper.js || exit 1
cat > app.asar.contents/frame-fix-entry.js << EOFENTRY
// Load frame fix first
require('./frame-fix-wrapper.js');
// Then load original main
require('./${original_main}');
EOFENTRY
# BrowserWindow frame/titleBarStyle patching is handled at runtime by
# frame-fix-wrapper.js via a Proxy on require('electron'). No sed patches
# needed — the wrapper detects popup vs main windows by their options and
# applies frame:true/false accordingly.
# Update package.json
echo 'Modifying package.json to load frame fix and add node-pty...'
node -e "
const fs = require('fs');
const pkg = require('./app.asar.contents/package.json');
pkg.originalMain = pkg.main;
pkg.main = 'frame-fix-entry.js';
pkg.optionalDependencies = pkg.optionalDependencies || {};
pkg.optionalDependencies['node-pty'] = '^1.0.0';
fs.writeFileSync('./app.asar.contents/package.json', JSON.stringify(pkg, null, 2));
console.log('Updated package.json: main entry and node-pty dependency');
"
# Create stub native module
echo 'Creating stub native module...'
mkdir -p app.asar.contents/node_modules/@ant/claude-native || exit 1
cp "$source_dir/scripts/claude-native-stub.js" \
app.asar.contents/node_modules/@ant/claude-native/index.js || exit 1
mkdir -p app.asar.contents/resources/i18n || exit 1
cp "$claude_extract_dir/lib/net45/resources/"*-*.json app.asar.contents/resources/i18n/ || exit 1
# Copy tray icons into asar so both packaged (process.resourcesPath)
# and unpackaged (app.getAppPath()) code paths can find them
cp "$claude_extract_dir/lib/net45/resources/Tray"* app.asar.contents/resources/ 2>/dev/null || \
echo 'Warning: No tray icon files found for asar inclusion'
# Extract electron module variable name for tray patches
extract_electron_variable
# Fix incorrect nativeTheme variable references
fix_native_theme_references
# Patch tray menu handler
patch_tray_menu_handler
# Patch tray icon selection
patch_tray_icon_selection
# Inject fast-path that updates the tray icon in place on theme
# changes (avoids the KDE duplicate-SNI race on destroy+recreate)
patch_tray_inplace_update
# Patch menuBarEnabled to default to true when unset
patch_menu_bar_default
# Patch quick window
patch_quick_window
# Add Linux Claude Code support
patch_linux_claude_code
# Patch Cowork mode for Linux (TypeScript VM client + Unix socket)
patch_cowork_linux
# Inject WCO shim into the BrowserView preload so claude.ai's
# desktop topbar renders on Linux. The shim spoofs the bundle's
# isWindows() UA check (load-bearing) plus matchMedia and
# windowControlsOverlay (defensive). See
# docs/learnings/linux-topbar-shim.md.
patch_wco_shim
# Copy cowork VM service daemon for Linux Cowork mode
echo 'Installing cowork VM service daemon...'
cp "$source_dir/scripts/cowork-vm-service.js" \
app.asar.contents/cowork-vm-service.js || exit 1
echo 'Cowork VM service daemon installed'
}