* feat(app): archive a workspace from its sidebar row and undo it Hovering a workspace row on desktop shows an archive button next to the row menu. Archiving shows a toast with Undo and a Cmd/Ctrl+Z shortcut so a mis-click costs one keystroke instead of a trip through History. The daemon serializes archive and undo per workspace across every connection. An undo that lands while an archive is still tearing down aborts it at the next checkpoint and restores only the agents that archive took down; deleting the worktree directory is the point of no return, after which undo recreates the worktree. * fix(server,app): close archive and undo races found in review - An archive request now claims the workspace's archive queue before looking up the workspace, so an undo sent right behind it cancels it instead of running first and losing to the archive. - A repeat archive of an already archived workspace no longer wipes the agents the earlier archive saved for undo. - Undo no longer brings back an agent that was archived before the workspace archive and only loaded to read its history. - Each local archive or undo request owns its own intent token, so a late response to an earlier request cannot release a newer request's hold on the row. An archive canceled by another client's undo puts the row back. - The undo failure toast shows translated copy; the cause goes to the log. * fix(app): keep a workspace's layout when another client restores it during the undo window The undo window purged tabs and layout whenever it closed after a finished archive. If another client restored the workspace before this client's toast closed, the restored workspace lost its saved arrangement. The archive now commits only if the workspace is still gone when the window closes. * fix(app): show the archived toast's Undo as a button * fix(app): keep a workspace's agent tabs when it is opened while its undo runs * fix(app): keep a row whose undo is running when a directory snapshot omits it
5.3 KiB
Keyboard shortcuts
Define shortcuts in the shared binding table. Platforms contribute key sources, not separate action maps, so matching and user rebinds stay consistent across runtimes. Add a shortcut through a binding; add platform support through a key source.
Badges and routing
Routing runs whenever the app mounts its key sources, independently of layout.
Use useHardwareKeyboardPresent() for hints and the Shortcuts settings section.
iOS reads GCKeyboard.coalesced and connection notifications. Android watches input
devices and hardware keyboard configuration. Web starts with a fine hover pointer and recognizes the first modified
keydown, including on touch devices. Compact Electron keeps its bindings.
Keyboard navigation needs the sidebar's filtered workspace model even when its visual panel is hidden. Retain that model while keyboard navigation is active; gating it on panel visibility leaves number shortcuts with no targets after a compact startup.
Composer send-key customization remains a separate capability. Native retains its existing Return submission path; advertising its web-only customization would expose settings the native composer does not implement.
Native sources
Use the iOS window press stream for hardware keys. UIKeyCommand only takes keys
that must bypass the focused text control: composer Return and the top overlay's
Enter and arrows. Keep the stream as the sole shortcut emitter: a command-owned
press is still observed by the window.
The source spike used an iPad Pro 11-inch (M4) simulator on iOS 18.6 with a
UITextView first responder. Both UIWindow.sendEvent and the root controller's
pressesBegan received Cmd+B, Cmd+P and Escape. With Escape registered as a
UIKeyCommand, the window observed it and the command handler also ran.
Simulator reserves Cmd+K for Toggle Software Keyboard. Injecting it produced
HID 184 with empty characters rather than K; plain K, Cmd+B and Cmd+P arrived
normally. Keep Simulator input capture off: Send Keyboard Input to Device
prevented synthetic AppleScript input from arriving, including plain text.
For simulator QA, use Cmd+B/P to check delivery and rebind the command center to
Cmd+J to check toggle and rebind behavior. Cmd+K delivery needs a physical iPad.
Simulator also reserves Cmd+1…4, Cmd+Shift+A and Cmd+comma. Exercise those actions
through rebinds. For numeric injection, use AppleScript physical key codes (for example
25 for 9); keystroke "9" did not deliver a digit in the QA session.
Simulator exposes a virtual GCKeyboard even with Connect Hardware Keyboard off.
Verify the iOS absence case on a physical iPhone.
Never register Backspace: it would take deletion away from text fields. Changes to the native module require a native rebuild; an over-the-air JavaScript update cannot deliver them.
Overlay ownership
Register keyboard handling with useOverlayRegistration from @/keyboard.
Pass the surface's open state, layer, handled keys and onKeyDown. The highest
layer gets first refusal; the latest registration wins a tie. Native registration
does not need a DOM ref. Web adds focus trapping and restoration through its adapter.
Capture a global overlay’s layer in state for its opening lifetime. Memo caches can be discarded or removed by React Compiler; recomputing against the registry lets an open modal rise above its own children and creates competing focus traps.
Declare Enter and arrow keys only while the surface owns them. Native sources use the top overlay's declarations to take those keys before text controls edit. Backspace stays with the focused input. Composition confirmation must not select an overlay row or submit the composer.
Android modals have their own windows. Observe them through React Native's
ExtraWindowEventListener as well as the activity window. The dev client bypasses
Expo's root-container handler, so an activity root-view wrapper misses presses.
A modal can forward an unhandled event into the activity window. Observe the event
once across that nested delivery without retaining recycled Android KeyEvents.
Keep an overlay-owned press consumed through key-up, even if its key-down closes
the overlay; otherwise the modal's default Escape handler can close a second layer.
iOS scene startup installs the observing window after Expo creates its root view. Keep that integration in the hardware keyboard module's config plugin. Its window owns both the press stream and the text-control key commands, including above modals.
Focus scopes
The composer and terminal declare focus with useKeyboardFocusSurface. Native
scope derives from those declarations and command-center state. Releasing an old
surface must not clear a newer owner's declaration during a tab switch. Web keeps
its DOM-derived scope.
Escape interrupts an agent only in the message input or an unclaimed surface. A focused terminal excludes that binding. The source leaves terminal Escape alone; terminal Escape/Ctrl forwarding is a separate feature.
Keys shared with text editing
A binding on a key the platform already uses for editing, such as Cmd/Ctrl+Z for
workspace archive undo, declares editable: false and registers its handler with
isActive true only while it has work. An inactive handler leaves the key
unhandled, so the pipeline skips preventDefault and the platform's own undo runs.