Skip to content

Make keyboard entry and return semantics consistent - #232

Merged
max-sixty merged 4 commits into
mainfrom
codex/keybinding-consistency
Sep 2, 2026
Merged

Make keyboard entry and return semantics consistent#232
max-sixty merged 4 commits into
mainfrom
codex/keybinding-consistency

Conversation

@max-sixty

Copy link
Copy Markdown
Owner

This makes Leaf’s keyboard register enforce one semantic meaning per key and one exact return frame per temporary entry. c now always comments in context, g T alone navigates to Threads, and Enter remains native activation or a focused control’s local continuation.

The dispatcher now owns a declarative return stack. Entry rows supply liveness, close behavior, and return wording; the dispatcher captures the exact control or reading position before entry, restores displaced panel or tray state, and unwinds nested entries one Escape at a time. Direct destinations, version selection, comment and reply boxes, thread and diff filters, option addition, and commands invoked through Help use that path. The guidance now requires complete-register surveys and escalation of unresolved inconsistencies.

The browser regression suite covers exact restoration across regenerated semantic rows, nested g Tc flows, query clearing, Help invocation, and the Enter/c distinction. The shipped PR walkthrough documents and exercises the resulting grammar. Integration with the current base also repairs its live-page fixture directory and ensures the presentation stamp is not published before anchor validation succeeds.

Tested with pre-commit run --all-files and uv run pytest tests (811 passed).

This was written by Codex on behalf of max-sixty

@leaf-agent leaf-agent left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return stack reads well and the c / g T split is a clear improvement — one entry, one frame, one Escape is a much easier rule to hold than the rung ladder. Three things I'd want settled before this lands, all about a frame's active() being wider than the layer it names, plus one now-false comment.

The register-completeness one is the important one: RETURN is spliced into the dispatcher's stack but is not in SCOPES, and createReference walks SCOPES.toReversed(). So ? never names navigation.return, and it still lists navigation.back — whose does reads off rung() — as the page's Escape while the dispatcher is running the frame's. That is the reference and the press spelling one key differently, which the register invariant in skills/leaf/CLAUDE.md and the "three lists" comment above SCOPES both exist to prevent.

One more, outside the diff. version.js's comment block above closeVersionMenu still reads:

so every door into this menu shows it from the button and the way back out is the platform's on all of them: the pointer because the press focuses the button first, v because the row focuses it before running that same press, and the reference because it stands a layer back up from that layer's invoker.

CHOOSER.run is now () => versionBtn.click(), so the middle clause is false and the v door's way back is the return frame rather than the platform's. Worth rewriting alongside the CHOOSER comment this PR already updated.

Nothing to say about the paintAnchors try/catch in 1c49ebb — it reads correct, and resetting anchoringReady before rethrowing is the right half of that pair.

Comment thread skills/leaf/assets/leaf.js Outdated
Comment thread skills/leaf/assets/leaf.js Outdated
Comment thread skills/leaf/packages/diff/widgets/lf-diff.js Outdated
@leaf-agent

Copy link
Copy Markdown
Collaborator

Checked all three against 66bc7768 — they hold, and the version.js rewrite is accurate to what CHOOSER.run now does.

The register one is the one I looked at hardest, since moving RETURN into SCOPES puts it in three walks at once. It comes out consistent: stack() expands the ELEMENTS placeholder unconditionally, so the dynamic splice always happens and if (scope === RETURN) return [] can only ever suppress a second copy, never the only one; CORE's membership is unchanged by dropping the explicit prepend, and both of its readers (checked at load, paintCoreControls over row.also) are order-independent; and the reference walk now merges RETURN as an ordinary section gated by its own when/at. The one thing the canonical list no longer states by itself is where RETURN sits relative to a focused control's own scopes — the comment above SCOPES and the new one in dispatch.js name that boundary, which is the right place for it.

BACK_OUT.when gaining !returnStack.current() doesn't cost a reachable press either: RETURN was already nearer than PAGE in the stack, so every rung it now hides — a selection clear, a tray close, a let-go — was already unreachable behind the frame's Escape. The row was advertising a press it could not perform, which is exactly the half that was wrong.

On the diff, this.contains(held) || this.shadowRoot?.contains(held) covers both staging modes because focused() returns the deep active element, and the shadow tree holds no further hosts to climb past, so the two clauses are the whole boundary here.

No further findings from me on this pass.

@max-sixty
max-sixty merged commit 47345e5 into main Sep 2, 2026
15 checks passed
@max-sixty
max-sixty deleted the codex/keybinding-consistency branch September 2, 2026 23:13
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.

2 participants