Qt-removal R7.5c: global keyboard shortcuts#151
Merged
Conversation
Ports legacy's QShortcut block (graphlink_window.py:307-318) and its AcceleratorForwardingFilter typing arbitration (graphlink_web_island_host.py:692-755) into the SPA. Eleven bindings: Ctrl+T/L/S/K/F, Ctrl+G, Ctrl+Shift+G, and Ctrl+Arrow branch navigation. Two new pure modules keep the logic unit-testable without mounting <ReactFlow>, the same posture as smartGuides.ts: - chrome/shortcuts.ts - resolveShortcut key matching plus isGatedWhileTyping, whose suppression set mirrors legacy's contract-tested GATED_SHORTCUTS exactly. Ctrl+S and Ctrl+K are deliberately exempt so they still fire while a text field has focus. - canvas/treeNavigation.ts - resolveTreeNavigationTarget, porting _navigate_up/_down/_left/_right: up to the parent, down to the leftmost child by x-position rather than creation order, left/right through x-ordered siblings (which require a parent), every boundary a pure no-op with no wrap. Only chat/conversation/html kinds are navigable, reproducing legacy's children graph - it never contained code/document/image/thinking nodes, though this backend makes them real edge targets. App.tsx's GlobalShortcuts grows from two ungated bindings to the full dispatch. Ctrl+F is now correctly gated while typing and Ctrl+K correctly is not. requestNewChat restores legacy's blocking confirm, and a save-chat palette entry closes a real gap: Save has had an app-bar button and a store method since R6.5 but no palette command. Three defects found and fixed before shipping: 1. setCenter used `measured?.width ?? 0`, silently degrading to centering on a node's top-left corner - the same empty-`measured` trap R7.5b-3 already solved, so measuredNodeSize is now exported from SceneCanvas and reused. 2. requestNewChat skipped the confirm on canvas-emptiness alone, but legacy requires both an empty canvas AND no current chat id. Emptying a loaded chat and pressing Ctrl+T therefore detached it silently, since newChat() drops current_chat_id and the next Save would insert a new row instead of updating the loaded one. Fixed by publishing hasSavedChat, the one derived bit of current_chat_id the frontend needs; the row id itself stays server-side. 3. Live verification caught what the automated suite structurally could not: branch navigation worked for exactly one hop. setCenter fires onMove, onMove reports the viewport, the backend echoes a fresh scene, and toFlowNodes rebuilds every node object - wiping the selection the keystroke had just made. The next Ctrl+Arrow then found no single selected node and no-opped. This wipe is pre-existing (any selection overlapping any snapshot, such as an autosave tick or a streaming token, loses its highlight) and R7.5c only made it load-bearing, so it is fixed at the source with a new exported withPreservedSelection. Verified live against a real backend with real KeyboardEvents: Ctrl+Down picked the leftmost child by x rather than the first-created; chained Ctrl+Left walked three siblings across separate keystrokes; boundaries no-opped; Ctrl+Up returned to the parent; every hop centered on the node's exact geometric center; with the composer focused, Ctrl+Down/Left/L/G/T were suppressed while Ctrl+K still opened the palette; and Ctrl+G with one node selected created a real frame. 868 backend / 1201 frontend tests pass, typecheck/lint/build clean, burn-down gate unchanged at 152/84/68, codegen re-run and in sync. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ports legacy's
QShortcutblock (graphlink_window.py:307-318) and itsAcceleratorForwardingFiltertyping arbitration (graphlink_web_island_host.py:692-755) into the SPA. Third of the R7.5 parity sub-increments, after R7.5a (mechanical gaps, #147) and R7.5b (canvas visuals, #148/#149/#150).What lands
Eleven bindings, all matched against legacy 1:1:
The two exemptions are legacy's own, and legacy contract-tests them in
test_keyboard_arbitration.py. The suppression set here is exactly those nine combinations, mirrored in a test so a future edit has to be deliberate rather than incidental.Two new pure modules keep the logic unit-testable without mounting
<ReactFlow>, the same posturesmartGuides.tsestablished in R7.5b-3:chrome/shortcuts.ts—resolveShortcutkey matching plusisGatedWhileTyping. Requires Ctrl-or-Cmd, rejects Alt so AltGr-produced characters still type, and rejects Shift on bindings legacy never defined with it (Ctrl+Shift+G is a separate binding, not a modifier variant of Ctrl+G).canvas/treeNavigation.ts—resolveTreeNavigationTarget, porting_navigate_up/_down/_left/_right. Up goes to the parent; down to the leftmost child by x-position, not the first-created; left/right step through x-ordered siblings and require a parent; every boundary is a pure no-op with no wrap. Onlychat/conversation/htmlkinds are navigable, which reproduces legacy'schildrengraph — it never contained code/document/image/thinking nodes, even though this backend makes all of them real edge targets.App.tsx'sGlobalShortcutsgrows from two ungated bindings to the full dispatch.requestNewChatrestores legacy's blocking confirm, and asave-chatpalette entry closes a genuine gap: Save has had an app-bar button and a store method since R6.5 but no palette command.Three defects found and fixed before shipping
1.
setCenterlost the node half-size. It usedmeasured?.width ?? 0, which silently degrades to centering on the node's top-left corner — the exact empty-measuredtrap R7.5b-3 already diagnosed and solved, becausetoFlowNodesrebuilds the node objects RF derivesmeasuredfrom.measuredNodeSizeis now exported fromSceneCanvasand reused rather than reimplemented badly.2.
requestNewChaterred in the unsafe direction. Legacy skips its confirm only when the canvas is empty and there is no current chat id; this skipped on emptiness alone. So emptying a loaded chat and pressing Ctrl+T detached it with no prompt —newChat()dropscurrent_chat_id, so the next Save would INSERT a new row instead of updating the chat the user thought they were editing. The in-code comment had claimed the divergence erred safe; it erred the other way. Fixed by publishinghasSavedChat, the one derived bit ofcurrent_chat_idthe frontend needs to evaluate the real predicate. The row id itself stays server-side, as that field's own comment has always specified.3. Branch navigation worked for exactly one hop. This is the one the automated suite structurally could not catch, and it broke the feature outright.
setCenterfiresonMove→onMovereports the viewport → the backend echoes a fresh scene →toFlowNodesrebuilds every node object, wiping the selection the keystroke had just made about 300ms earlier. The next Ctrl+Arrow then found no single selected node and no-opped, so you could never walk more than one step.That wipe is pre-existing, not introduced here: any selection that merely overlaps a snapshot — an autosave tick, a streaming token — loses its highlight the same way. R7.5c is simply the first feature that depends on selection surviving. It is therefore fixed at the source, with a new exported pure
withPreservedSelection(rebuilt, current)re-applying selection across the rebuild, covered by five unit tests including one asserting a backend-deleted node cannot be resurrected by a stale id.Test plan
pytest backend/ tests/test_no_qt_anywhere.py)npm run check: schema → typecheck → lint → test → build), 0 lint errorsKeyboardEvents and a WS-seeded 4-node branch scene:Documented divergences
Both deliberate, both pre-existing, neither silently changed here: