fix(tui): coerce disabled leader keybind to default instead of crashing#29686
fix(tui): coerce disabled leader keybind to default instead of crashing#29686Tushar49 wants to merge 10 commits into
Conversation
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.
|
Thanks for your contribution! This PR doesn't have a linked issue. All PRs must reference an existing issue. Please:
See CONTRIBUTING.md for details. |
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.
…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.
|
I think the issue is that |
|
Thanks @kommander — that's a better architectural shape than the coercion guard. The "truly deactivate" semantics aligns with what the user in #26628 wanted ( Concrete plan when you're ready:
Sub-cause 3 (the new Two questions before I touch it:
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. |
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.
|
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:
Verified: bun typecheck clean, bun test test/config/tui.test.ts 41/41 pass. PR now covers all three sub-causes of #26628:
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. |
Issue for this PR
Closes #26628
Type of change
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. Settingkeybinds.leader = "none"(orfalse) intui.jsoncrashed the TUI at startup withInvalid leader trigger: expected exactly one binding.BindingValueSchemaallows"none"/falsefor 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.createBindingLookupthrows on an empty leader binding and the exception escapesloadState. Fix: guard inpackages/opencode/src/cli/cmd/tui/config/tui.tsjust beforeTuiKeybind.parse(keybinds)— detect the disabled-leader sentinels ("none"/false/[]) and coerce toTuiKeybind.LeaderDefault("ctrl+x") with alog.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 likeusername_toggle) failed strict schema validation, the existingcatchCauseswallowed the failure to{}, and the user's valid settings (theme, keybinds, etc.) were silently replaced with defaults. Fix: mirror the existingdropUnknownKeybindspattern withdropUnknownTopLevelKeys— derive the known key set fromTuiInfo.fields, strip unknown top-level keys before schema validation, and log a warning so the user can see what was dropped.Sub-cause 1 —
keymapvskeybindsschema mismatch — verified moot. The published schema athttps://opencode.ai/tui.jsonuseskeybindsand matches the code intui-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 typecheckfrompackages/opencode— clean (tsgo exit 0)bun test test/config/tui.test.tsfrompackages/opencode— 41/41 pass (was 36; +5 new tests)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 provideddrops unknown top-level keys instead of silently dropping the whole tui config (#26628)loads cleanly when tui.json contains only known top-level keysScreenshots / 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