Skip to content

Qt-removal R7.5c: global keyboard shortcuts#151

Merged
dovvnloading merged 1 commit into
mainfrom
qt-removal/r7-5c-keyboard-shortcuts
Jul 26, 2026
Merged

Qt-removal R7.5c: global keyboard shortcuts#151
dovvnloading merged 1 commit into
mainfrom
qt-removal/r7-5c-keyboard-shortcuts

Conversation

@dovvnloading

Copy link
Copy Markdown
Owner

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. 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:

Binding Action Gated while typing?
Ctrl+T New Chat (with confirm) yes
Ctrl+L Toggle Library yes
Ctrl+S Save Chat no
Ctrl+K Command Palette no
Ctrl+F Search yes
Ctrl+G / Ctrl+Shift+G Create Frame / Container yes
Ctrl+Arrow ×4 Branch navigation yes

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 posture smartGuides.ts established in R7.5b-3:

  • chrome/shortcuts.tsresolveShortcut key matching plus isGatedWhileTyping. 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.tsresolveTreeNavigationTarget, 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. Only chat/conversation/html kinds are navigable, which reproduces legacy's children graph — it never contained code/document/image/thinking nodes, even though this backend makes all of them real edge targets.

App.tsx's GlobalShortcuts grows from two ungated bindings to the full dispatch. requestNewChat restores legacy's blocking confirm, and a save-chat palette 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. setCenter lost the node half-size. It used measured?.width ?? 0, which silently degrades to centering on the node's top-left corner — the exact empty-measured trap R7.5b-3 already diagnosed and solved, because toFlowNodes rebuilds the node objects RF derives measured from. measuredNodeSize is now exported from SceneCanvas and reused rather than reimplemented badly.

2. requestNewChat erred 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() drops current_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 publishing hasSavedChat, the one derived bit of current_chat_id the 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. setCenter fires onMoveonMove reports the viewport → the backend echoes a fresh scene → toFlowNodes rebuilds 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

  • 868 backend tests (pytest backend/ tests/test_no_qt_anywhere.py)
  • 1201 frontend tests (npm run check: schema → typecheck → lint → test → build), 0 lint errors
  • Burn-down gate unchanged at 152/84/68 — pure feature work, no Qt files touched
  • Codegen re-run; generated TS/schema in sync with the payload dataclass
  • Adversarial review of the diff (found defects 1 and 2)
  • Live verification against a real backend with real KeyboardEvents and a WS-seeded 4-node branch scene:
    • Ctrl+Down selected the leftmost child by x (200), not the first-created (600)
    • Chained Ctrl+Left walked x=900 → 600 → 200 across separate keystrokes — the case that was broken
    • Left boundary was a clean no-op; Ctrl+Up returned to the parent
    • Every hop centered on the node's exact geometric center (e.g. 1111,456 = 900+422/2, 400+112/2), proving fix 1
    • With the composer textarea focused, Ctrl+Down/Left/L/G/T were all suppressed — no selection change, no view change, no overlay, no node created
    • Ctrl+K still opened the palette with that same textarea focused, confirming the exemption
    • Ctrl+G with exactly one node selected created a real frame, confirming the shortcut's 1+ gate

Documented divergences

Both deliberate, both pre-existing, neither silently changed here:

  • The palette's Create Frame/Container keep their stricter 2+ selection gate while the shortcuts use legacy's 1+. That gate is an R6.1 choice; changing shipped palette behavior inside a shortcuts increment would be scope creep, so it is flagged rather than altered.
  • Ctrl+K and Ctrl+F remain toggles where legacy was show-only (an R2.4 divergence). Only their focus-gating was corrected here — Ctrl+F was previously ungated and should not have been.

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>
@dovvnloading
dovvnloading merged commit a4c8a4c into main Jul 26, 2026
2 checks passed
@dovvnloading
dovvnloading deleted the qt-removal/r7-5c-keyboard-shortcuts branch July 26, 2026 20:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant