### Linked issue Closes #4821 ### Type of change - [x] Bug fix ### Reasoning A key sequence recorded in Settings, for example `Ctrl+K` then `J`, silently did nothing whenever the app re-rendered between the two keys. Resizing the window, toggling the sidebar, or an agent turn starting or finishing is enough, so a rebound sequence looks broken at random and the second key lands in the composer as text. The global `keydown`/`keyup`/`blur`/`visibilitychange` listeners, and on desktop the `browser-shortcut-input` subscription, were registered by an effect that depended on eighteen render-scoped values. One of them, `useActiveWorkspaceSelection()`, returns a new object every render, so the effect tore down and re-subscribed on every render of `AppContainer`. Its cleanup resets the in-progress sequence, so the second key was resolved from step 0, matched nothing, and fell through as ordinary input. The key handlers now read the latest render through `useStableEvent`, so the listener effect re-subscribes only when shortcuts are enabled or disabled. Clearing held badge modifiers when the workspace-index jump binding changes moved to its own effect keyed on that modifier. The cleanup still resets the sequence, which is now correct, because it only runs when shortcuts genuinely go away. ### Goals - A recorded key sequence completes across a root re-render between its keys. - The global shortcut listeners are registered once while shortcuts are enabled, instead of on every render. - A regression spec that fails on the current `main` and passes with this change. ### Non-goals - `useActiveWorkspaceSelection()` still returns a new object per render. This PR stops the shortcut listeners from depending on that identity, it does not change the hook. - A sequence whose second step also carries a modifier (`Ctrl+K` `Ctrl+J`) is still cancelled when the modifier is re-pressed, because the bare `Ctrl` keydown does not match the next step. That is a separate defect and is not fixed here. - The Electron `browser-shortcut-input` path is unchanged in shape and was not exercised in a desktop build. ### QA New spec `packages/app/e2e/browser/keyboard-shortcut-sequence.spec.ts`. It seeds `Ctrl+K J` as the "Toggle command center" binding, opens a workspace, and presses the sequence twice: once with nothing in between, once with a viewport resize settled over two frames between the keys. Before, on `main`, run twice: ``` ✘ multi-key shortcut sequences › a sequence completes when the window resizes between its keys expect(getByTestId('command-center-panel')).toBeVisible() failed ``` The control assertion in the same test passed both times, so the sequence itself works and only the re-render breaks it. Instrumenting the hook locally showed the mechanism directly: ``` key Control -> chord step 0 key K -> chord step 1 effect cleanup at chord step 1 <- the resize re-render effect setup; changed deps: activeWorkspaceSelection key j -> chord step 0, match null ``` After, the same spec passes, and `activeWorkspaceSelection` was the only dependency that changed on those renders. Also run locally on this branch: - `packages/app` unit suite: 603 files, 5319 tests, all passing. - Playwright `browser` project, keyboard and command-center specs: `keyboard-shortcut-sequence`, `empty-pane-keyboard`, `changes-shortcut-placement`, `sidebar-workspace-pin-shortcut`, `workspace-archive-shortcut`, `command-center-agent-controls`. 17 passed. - `npm run typecheck --workspace=@getpaseo/app`, `oxlint`, and `oxfmt --check` on the changed files: clean. Web only, in Chromium on Linux. Keyboard shortcuts are off on mobile, and the desktop build was not exercised. ### Supersedes - [PR #4834 — fix(app): keep multi-key shortcuts alive across root re-renders](https://github.com/getpaseo/paseo/pull/4834) — same fix for the same bug. I diagnosed #4821 independently, reached the same cause, and built on that commit, so its author is credited with a `Co-authored-by` trailer. This PR replaces its spec with one that drives the app through the keyboard only, without patching `window.addEventListener` to count registrations. ### Checklist - [x] One focused change - [x] `npm run typecheck` passes - [x] `npm run lint` passes - [x] `npm run format` passes - [x] QA evidence - [x] Tests added or updated where it made sense Co-authored-by: Ivan Malison <IvanMalison@gmail.com>
Welcome to your Expo app 👋
This is an Expo project created with create-expo-app.
Get started
-
Install dependencies
npm install -
Start the app
npx expo start
In the output, you'll find options to open the app in a
- development build
- Android emulator
- iOS simulator
- Expo Go, a limited sandbox for trying out app development with Expo
You can start developing by editing the files inside the app directory. This project uses file-based routing.
Get a fresh project
When you're ready, run:
npm run reset-project
This command will move the starter code to the app-example directory and create a blank app directory where you can start developing.
Learn more
To learn more about developing your project with Expo, look at the following resources:
- Expo documentation: Learn fundamentals, or go into advanced topics with our guides.
- Learn Expo tutorial: Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web.
Join the community
Join our community of developers creating universal apps.
- Expo on GitHub: View our open source platform and contribute.
- Discord community: Chat with Expo users and ask questions.
Dictation debugging
Set EXPO_PUBLIC_ENABLE_AUDIO_DEBUG=1 before running npx expo start to render the in-app audio debug card. Pair it with the server-side STT_DEBUG_AUDIO_DIR flag so every dictation includes a copyable path to the saved raw audio file.