CodeNomad/dev-docs/HOTKEYS.md
Pascal André 2eb1fb1797
fix(tauri): stop F11 from freezing the Linux app (#1143)
Pressing F11 froze the Tauri app on Linux under X11 or XWayland: GNOME
reported "CodeNomad is not responding" and the window never went
fullscreen. The released 1.0.0 AppImage froze the same way, so this
predates the AppImage display-stack fix (#1138), which keeps GTK on X11
for this very shortcut.

A backtrace of the frozen process showed a deadlock between two
threads:
- global-hotkey's X11 thread received F11 and ran the plugin handler on
  that thread. toggle_fullscreen_window then asked the main thread
  whether the window was focused (is_focused) and waited.
- The key grab itself moves X11 focus. Its focus events reached the main
  thread, where update_fullscreen_shortcut re-registered F11 and waited
  for the X11 thread to confirm.

The handler now queues the toggle on the main thread
(run_on_main_thread) and returns, so the shortcut thread is free to
answer the registration.

Two leaks from #1138 are closed on the way:
- prefer_x11_display exported GDK_BACKEND=x11,wayland, which every child
  inherited: the backend, the shared OpenCode service it may start, agent
  shells, editors and the browser opened for links. Their GUI programs
  then ran under XWayland. CodeNomad now sets its own GTK's preference
  with gdk_set_allowed_backends, and children keep the session's choice.
  A GDK_BACKEND set by the session still wins.
- Children that leave an AppImage's mount started in AppRun's working
  directory inside it (PWD=/tmp/.mount_*/usr for the browser opened by a
  link). use_host_environment now starts them in / while ours is inside
  the mount; callers that pick a directory (backend, editors) still do.

dev-docs/HOTKEYS.md records that a global-shortcut handler only queues
work on the main thread; dev-docs/TAURI_UPDATER.md describes the GTK
backend preference and the working directory.

Validation on Ubuntu 24.04 GNOME Wayland, with the #1141 CI AppImage
repacked around a binary built from this branch:
- After focusing the window, about ten F11 presses toggle fullscreen in
  and out. The main thread stays in its event loop (poll), where the
  CI build and the 1.0.0 release deadlock on the first press
  (gdb backtraces).
- The window is still an XWayland client. The backend and the OpenCode
  service carry neither GDK_BACKEND nor mount paths.
- The help link opens Firefox with / as working directory, PWD=/ and no
  GDK_BACKEND.
- The appimage_environment and appimage_exit tests pass, and the Linux
  release binary builds.
2026-10-25 21:25:16 +01:00

4.3 KiB

Custom keyboard shortcuts

Agent conventions

  • Settings → Keyboard shortcuts (components/settings/hotkeys-settings-section.tsx, square token-based styles in styles/components/hotkeys-settings.css) manages the static renderer-only catalog in lib/hotkey-definitions.ts in both app and native Preferences renderers, without mounting the app. Keep the hotkeys section allowlist identical in renderer, Electron and Tauri. Initially unbound palette commands appear with “No shortcut” under their existing translated names; aliases (new-session → session-new) keep one row. Native workspace actions are configurable but register for execution only in local desktop windows.
  • Bindings live in the UI owner's settings.hotkeyOverrides: a missing ID uses its default, [] disables it, merge-patch null resets one ID or the map. The saved promptSubmitOnEnter supplies Enter defaults until an explicit composer override replaces them.
  • Recording is renderer-only and target-scoped: it suppresses app shortcuts (not native accelerators), ignores IME composition and bare modifiers, lets unmodified Tab leave normally, and ends on blur, cancellation, inactive settings or disposal. Conflicts warn on every affected row and never silently disable a binding.
  • Digit-row and bracket keys share one physical identity for recording, dispatch and conflicts (Digit1 → 1, BracketLeft → [, including AZERTY and QWERTZ); other keys stay logical. Saved layout-symbol overrides (&, ü, !) must be re-recorded to bind physical slots; never guess or silently migrate them.
  • Initially unbound commands dispatch the palette's real action through use-app-lifecycle.ts and the existing keyboard registry, respecting its disabled state, active project/session and modal/selector guards, without key repeat; global commands also work on the home view.
  • Shortcut hints live only in tooltips and the manager, never status strips or help footers: a bound control sets title={withHotkey(label, id)} (lib/hotkeys.ts) and menu items set description: hotkeyLabel(id) || undefined, leaving the accessible name unchanged, only where the shortcut performs that action in the control's current context (Ctrl+W only on the active tab).
  • Composer shortcuts (Clear, agent/model/thinking selectors) reach the shown composer, including the no-session draft, through the instance's session-sidebar-events request; Clear dispatches PROMPT_CLEAR_EVENT so the composer runs its own Clear action, never a bare textarea write.
  • Jump to previous/next user message (user-message-prev/-next, components/user-message-navigation.ts) defaults to Alt+↑/↓, and to Cmd+Option+↑/↓ on macOS, where Option+↑/↓ moves within a text field's paragraphs: neither default edits text, so the jump also works from the composer and other text fields. It runs in the active MessageSection's capture-phase handler, before the transcript reads arrows as scrolling; palette commands reach it through JUMP_USER_MESSAGE_EVENT. Code editors and terminals (which bind modified arrows), arrow-key widgets, selects and popup triggers (Alt+↓ opens them), fields that move a list's active option (aria-activedescendant, input[aria-controls], such as the mission picker search), non-text inputs, IME composition, modal dialogs and an open composer picker ([data-prompt-picker], which moves its selection with every arrow) keep the keystroke; key repeat does not repeat the jump. Targets are the timeline's markers, including indexed history beyond the resident window (requested even while the timeline is hidden; a jump that finds nothing resident runs once it arrives). The last target stays the reference while loading or shown, until a scroll gesture or other navigation. Covered by tests/browser/history-navigation.test.ts.
  • Native hotkeys keep their platform defaults outside the manager; native menu keys and OS-reserved combinations may intercept assignments before the renderer.
  • Tauri's F11 fullscreen shortcut (tauri-plugin-global-shortcut, Windows/Linux) is registered only while a local window has focus. On Linux its handler runs on the shortcut thread, and the main thread waits on that thread to (un)register F11 when focus changes, which the X11 key grab itself causes: a handler only queues work with run_on_main_thread, never a synchronous window call.