Skip to content

fix(tui): coerce disabled leader keybind to default instead of crashing#29686

Open
Tushar49 wants to merge 10 commits into
anomalyco:devfrom
Tushar49:Tushar49-patch-3
Open

fix(tui): coerce disabled leader keybind to default instead of crashing#29686
Tushar49 wants to merge 10 commits into
anomalyco:devfrom
Tushar49:Tushar49-patch-3

Conversation

@Tushar49
Copy link
Copy Markdown

@Tushar49 Tushar49 commented May 28, 2026

Issue for this PR

Closes #26628

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Two of the three sub-causes reported on #26628 silently corrupted the TUI experience. This PR fixes both and verifies the third is no longer reachable.

Sub-cause 2 — leader: "none" crashes the TUI on a blank screen. Setting keybinds.leader = "none" (or false) in tui.json crashed the TUI at startup with Invalid leader trigger: expected exactly one binding. BindingValueSchema allows "none" / false for any keybind so users can disable individual commands, but the leader is special: every leader-prefixed binding (<leader>q, <leader>e, …) needs exactly one trigger for the prefix system. createBindingLookup throws on an empty leader binding and the exception escapes loadState. Fix: guard in packages/opencode/src/cli/cmd/tui/config/tui.ts just before TuiKeybind.parse(keybinds) — detect the disabled-leader sentinels ("none" / false / []) and coerce to TuiKeybind.LeaderDefault ("ctrl+x") with a log.warn.

Sub-cause 3 — unknown top-level keys silently drop the whole TUI config. Any unknown key at the top of tui.json (e.g. a legacy field like username_toggle) failed strict schema validation, the existing catchCause swallowed the failure to {}, and the user's valid settings (theme, keybinds, etc.) were silently replaced with defaults. Fix: mirror the existing dropUnknownKeybinds pattern with dropUnknownTopLevelKeys — derive the known key set from TuiInfo.fields, strip unknown top-level keys before schema validation, and log a warning so the user can see what was dropped.

Sub-cause 1 — keymap vs keybinds schema mismatch — verified moot. The published schema at https://opencode.ai/tui.json uses keybinds and matches the code in tui-schema.ts. The mismatch reported in #26628 (against opencode 1.14.46) is no longer reproducible.

This intentionally does not tighten the schema to reject "none" for the leader. The schema-level lenient union is shared across every keybind, and converting a startup crash into a whole-config validation failure would regress the experience further. Coercion is the smallest change that turns a crash into a useful warning.

Scope discipline: Prior PR #26641 attempted to fix all three sub-causes in one pass and was auto-closed by the issue-compliance bot for missing PR template sections (not a maintainer technical rejection). This PR matches that scope while adhering to the template.

How did you verify your code works?

  • bun typecheck from packages/opencode — clean (tsgo exit 0)
  • bun test test/config/tui.test.ts from packages/opencode41/41 pass (was 36; +5 new tests)
  • New tests (all it.instance):
    • coerces keybinds.leader = "none" to the default to avoid TUI crash (#26628)
    • coerces keybinds.leader = false to the default to avoid TUI crash (#26628)
    • respects keybinds.leader override when a real binding is provided
    • drops unknown top-level keys instead of silently dropping the whole tui config (#26628)
    • loads cleanly when tui.json contains only known top-level keys

Screenshots / recordings

Not a UI change. The fix turns blank-screen crashes and silent default-loads into log warnings + normal TUI bootstrap with the user's remaining valid settings preserved.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Tushar49 added 2 commits May 28, 2026 12:11
Setting keybinds.leader = 'none' (or false) crashes the TUI with 'Invalid leader trigger' and a blank screen. Guard at the call site by coercing disabled-leader sentinels to TuiKeybind.LeaderDefault with a log.warn.

Addresses sub-cause 2 of anomalyco#26628.
Three it.instance tests in test/config/tui.test.ts: keybinds.leader='none' resolves without crash, keybinds.leader=false resolves without crash, keybinds.leader='alt+space' is respected unchanged.
@github-actions
Copy link
Copy Markdown
Contributor

Thanks for your contribution!

This PR doesn't have a linked issue. All PRs must reference an existing issue.

Please:

  1. Open an issue describing the bug/feature (if one doesn't exist)
  2. Add Fixes #<number> or Closes #<number> to this PR description

See CONTRIBUTING.md for details.

Tushar49 added 2 commits May 28, 2026 12:14
Reupload with LF line endings so the diff is just the actual leader-none guard, not a whole-file CRLF rewrite.
Reupload with LF so the diff is just the 3 new leader-none tests.
Tushar49 added 2 commits May 28, 2026 12:43
…lyco#26628)

Sub-cause 3 of anomalyco#26628: unknown top-level keys in tui.json caused strict schema validation to fail, catchCause swallowed the error, and the whole tui config silently fell back to defaults. Mirror dropUnknownKeybinds with dropUnknownTopLevelKeys derived from TuiInfo.fields, strip unknown keys before validation, log a warning.
Two new it.instance tests for sub-cause 3 of anomalyco#26628: tui.json with an unknown top-level key + valid theme + keybinds preserves the valid settings (theme survives), and a clean known-only tui.json still loads correctly.
@kommander
Copy link
Copy Markdown
Collaborator

I think the issue is that registerTimedLeader as opentui/keymap addon crashes for an empty leader. Before the keymap was introduced the leader could be truly deactivated, so defaulting to ctrl+x and always activating it leads to unexpected behaviour for users that truly want to not use a leader key at all. OpenCode can skip registerTimedLeader when not leader key is set, that invalidates the <leader> token in the bindings, so these bindings should be dropped. I need to check how the keymap behaves here, I'll look into it. But generally it should be possible to deactivate the leader completely instead of defaulting to always present.

@Tushar49
Copy link
Copy Markdown
Author

Thanks @kommander — that's a better architectural shape than the coercion guard. The "truly deactivate" semantics aligns with what the user in #26628 wanted (leader: "none") and what the schema already advertises (BindingValueSchema lets every keybind accept "none" / false).

Concrete plan when you're ready:

  1. Revert the coercion in packages/opencode/src/cli/cmd/tui/config/tui.ts (the if (keybinds.leader === "none" || …) keybinds.leader = TuiKeybind.LeaderDefault block).
  2. In packages/opencode/src/cli/cmd/tui/keymap.tsx (registerOpencodeKeymap, around the addons.registerTimedLeader call): when the resolved leader trigger is empty/unset, skip registerTimedLeader entirely. Probably do the same in packages/opencode/src/cli/cmd/run/keymap.shared.ts.
  3. Filter out any binding whose value contains the <leader> token before createBindingLookup is called, so they don't sit registered-but-unreachable. The default bindings list in keybind.ts has <leader>q, <leader>e, <leader>t, <leader>b, etc. — all of those should be dropped (with a single log.info listing them) when leader is deactivated.
  4. Rework the leader-none tests to assert (a) the TUI bootstraps, (b) bindings that don't reference <leader> still work, (c) bindings that do are silently dropped.

Sub-cause 3 (the new dropUnknownTopLevelKeys) is independent of this and I'd keep it in the PR — happy to split it into its own PR if you'd prefer the leader rework to ship clean.

Two questions before I touch it:

  • Do you want me to take a stab at the rework on this branch, or are you preferring to handle it as part of your investigation?
  • If you'd like me to do it, would you prefer the upstream fix go into @opentui/keymap instead (skip registerTimedLeader when trigger is empty, gracefully)? Happy to PR there too — let me know.

Sorry for the unhelpful coercion shape on the first pass — the schema-vs-runtime mismatch made it tempting to "fix at the call site" instead of asking the right design question.

Tushar49 added 4 commits May 28, 2026 14:51
Per @kommander's review on anomalyco#29686: revert the leader-none coercion block. The real fix lives in keymap.tsx / keymap.shared.ts where registerTimedLeader is now skipped when the leader trigger is empty. dropUnknownTopLevelKeys (sub-cause 3) is kept unchanged.
Guard addons.registerTimedLeader in registerOpencodeKeymap behind a hasLeader check derived from config.keybinds.get(LEADER_TOKEN). When the trigger is empty (user set leader to none/false/[]), skip registration entirely and return a no-op disposer so the TUI bootstraps cleanly instead of crashing on `Invalid leader trigger`. Leader-prefixed bindings simply become unreachable, matching the documented 'truly deactivated' semantics.
Match the TUI keymap guard: when createParser is called with an empty leader string, skip registerLeader so it does not throw `Invalid leader trigger`. parseBindings/formatBindings still work with literal `<leader>` tokens.
Rename the leader-none / leader-false tests from 'coerces to default' to 'preserves so leader can be truly deactivated' to match the reworked semantics. Assertions unchanged.
@Tushar49
Copy link
Copy Markdown
Author

Reworked along your suggested shape, @kommander - ready for another look.

The leader-none coercion in tui.ts has been reverted. The actual crash is now guarded at the keymap layer where it belongs:

  • packages/opencode/src/cli/cmd/tui/keymap.tsx (registerOpencodeKeymap): derive leaderTrigger from config.keybinds.get(LEADER_TOKEN). When it is empty/unset, skip addons.registerTimedLeader entirely and use a no-op disposer. Leader-prefixed bindings (q, e, ...) simply become unreachable, matching the truly-deactivated semantics.
  • packages/opencode/src/cli/cmd/run/keymap.shared.ts: same guard inside createParser - skip registerLeader when the leader string is empty.
  • packages/opencode/src/cli/cmd/tui/config/tui.ts: leader-none coercion block removed; user-set value flows through untouched. dropUnknownTopLevelKeys (sub-cause 3) is unchanged.
  • packages/opencode/test/config/tui.test.ts: renamed the leader-none tests from "coerces ... to default" to "preserves ... so the leader can be truly deactivated"; assertions unchanged.

Verified: bun typecheck clean, bun test test/config/tui.test.ts 41/41 pass.

PR now covers all three sub-causes of #26628:

  1. keymap vs keybinds schema mismatch - verified moot, published schema uses keybinds.
  2. leader-none crash - fixed at the keymap layer per your suggestion.
  3. unknown top-level keys silently dropping the whole config - fixed via dropUnknownTopLevelKeys in tui.ts.

Did NOT PR upstream @opentui/keymap - the opencode-side guard is sufficient; an upstream behavior shift is your/SST's call. Let me know if you would like sub-cause 3 split into its own PR or any other shape changes.

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.

tui config schema mismatch + leader none crash

2 participants