Skip to content

feat(config): add hotbar slot persistence#2873

Merged
Hmbown merged 1 commit into
Hmbown:codex/v0.9.0-stewardshipfrom
reidliu41:feat/hotbar-config
Jun 7, 2026
Merged

feat(config): add hotbar slot persistence#2873
Hmbown merged 1 commit into
Hmbown:codex/v0.9.0-stewardshipfrom
reidliu41:feat/hotbar-config

Conversation

@reidliu41

@reidliu41 reidliu41 commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds the config/schema foundation for the Hotbar tracking work.

This implements [[hotbar]] persistence for slots 1-8 so later UI and key-dispatch slices can render and trigger user-
configured hotbar actions instead of hardcoding slot bindings.

Refs #2064
Part of #2061

Scope

  • Add [[hotbar]] config parsing for:
    • slot
    • action
    • optional label
  • Add default bindings when no [[hotbar]] table is configured.
  • Validate bindings safely:
    • out-of-range slots are skipped with a warning
    • duplicate slots use the last binding with a warning
    • unknown actions are preserved with a warning for future UI feedback
  • Wire the same schema into the TUI runtime config.
  • Document the example config in config.example.toml.

Testing

  • cargo fmt --all -- --check
  • cargo clippy --workspace --all-targets --all-features
  • cargo test --workspace --all-features

Checklist

  • Updated docs or comments as needed
  • Added or updated tests where relevant
  • Verified TUI behavior manually if UI changes

Greptile Summary

This PR adds the config/schema foundation for hotbar slot persistence: a new [[hotbar]] TOML array-of-tables that maps slots 1–8 to action IDs, with validation that skips out-of-range slots, warns on duplicates (last wins), and preserves unknown actions for future UI feedback. The same schema is mirrored into the TUI runtime Config and wired into merge_config.

  • Core logic (crates/config/src/lib.rs): new HotbarBindingToml, HotbarBinding, HotbarConfigWarning, and HotbarConfigResolution types, resolve_hotbar_bindings free function, default_hotbar_bindings returning the 8 built-in defaults, all guarded by three tests covering defaults, TOML round-trip, and edge-case validation.
  • TUI wiring (crates/tui/src/config.rs): mirrors the hotbar field into Config, delegates resolve_hotbar_bindings to codewhale_config, and adds the field to merge_config with all-or-nothing Option::or semantics (any override with even one slot fully replaces the base config's hotbar).
  • Docs (config.example.toml): two commented-out [[hotbar]] examples with the full default slot list described in the header comment.

Confidence Score: 4/5

Safe to merge; the new hotbar config path is additive and feature-flagged by absence — existing users see no change until they add [[hotbar]] tables.

The implementation is well-structured with thorough tests. The only substantive concern is that merge_config uses all-or-nothing Option::or for hotbar, meaning a project-level config with a single slot silently discards all global hotbar bindings. The UnknownAction warning message can also be misleading when the entry is the earlier of two duplicate-slot entries and gets overwritten. Neither issue affects correctness of the resolved bindings, only observability and merge-time ergonomics.

crates/config/src/lib.rs (warning message clarity) and crates/tui/src/config.rs (all-or-nothing merge semantics, missing skip_serializing_if)

Important Files Changed

Filename Overview
crates/config/src/lib.rs Adds HotbarBindingToml, HotbarBinding, HotbarConfigWarning, HotbarConfigResolution types plus resolve_hotbar_bindings and default_hotbar_bindings helpers; wires hotbar into ConfigToml; three new unit tests cover defaults, round-trip, and validation edge cases. Warning message for DuplicateSlot is grammatically awkward, and UnknownAction's "keeping binding" phrasing can be misleading when the entry is subsequently overwritten.
crates/tui/src/config.rs Mirrors the hotbar field and resolve_hotbar_bindings method from codewhale_config into the TUI Config struct; adds the field to merge_config with all-or-nothing Option::or semantics. Missing skip_serializing_if = "Option::is_none" is an inconsistency with the core config type.
config.example.toml Documents the new [[hotbar]] table with two commented-out examples covering slot, label, and action; accurately reflects the 1-8 slot range and built-in defaults listed in the header comment.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["User TOML config\n[[hotbar]] tables"] -->|"serde deserialize"| B["Option<Vec<HotbarBindingToml>>"]
    B -->|"None"| C["default_hotbar_bindings()\nslots 1-8 with built-in actions"]
    B -->|"Some(entries)"| D["Iterate entries"]
    C --> E["BTreeMap<u8, HotbarBinding>"]
    D --> F{"slot in 1..=8?"}
    F -->|"No"| G["SlotOutOfRange warning\nskip entry"]
    F -->|"Yes"| H{"action in\nknown_action_ids?"}
    H -->|"No (and known non-empty)"| I["UnknownAction warning\nkeep entry"]
    H -->|"Yes"| J["insert into BTreeMap"]
    I --> J
    J --> K{"slot already\nin map?"}
    K -->|"Yes"| L["DuplicateSlot warning\nlast entry wins"]
    K -->|"No"| M["entry stored"]
    L --> M
    E --> N["BTreeMap::into_values()\n(sorted by slot)"]
    M --> N
    N --> O["HotbarConfigResolution\n{ bindings, warnings }"]
Loading

Fix All in Codex Fix All in Claude Code Fix All in Cursor

Reviews (1): Last reviewed commit: "feat(config): add hotbar slot persistenc..." | Re-trigger Greptile

Greptile also left 4 inline comments on this PR.

  Add durable [[hotbar]] config bindings for slots 1-8, including default
  bindings when no hotbar config is present.

  Validate bindings without panicking: skip out-of-range slots, use the last
  duplicate slot, and preserve unknown actions so future UI layers can show
  disabled placeholders.
@github-actions

github-actions Bot commented Jun 7, 2026

Copy link
Copy Markdown

Thanks @reidliu41 for taking the time to contribute.

This repository is currently observing a maintainer-managed contribution gate in dry-run mode, so this pull request is staying open. When enforcement is enabled, pull requests from contributors who are not listed in .github/APPROVED_CONTRIBUTORS will be closed automatically.

Please read CONTRIBUTING.md for the expected contribution shape. A maintainer can grant PR access by commenting /lgtm on a pull request.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces customizable 1-8 sidebar hotbar bindings, allowing users to configure custom actions and labels with fallback defaults. The implementation includes validation logic to handle out-of-range slots, duplicate bindings, and unknown actions, supported by new unit tests. Feedback on the changes suggests optimizing the binding resolution loop in crates/config/src/lib.rs to avoid cloning the HotbarBinding struct on every insertion.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread crates/config/src/lib.rs
Comment on lines +761 to +767
if let Some(previous) = by_slot.insert(binding.slot, binding.clone()) {
warnings.push(HotbarConfigWarning::DuplicateSlot {
slot: binding.slot,
previous_action: previous.action,
replacement_action: binding.action,
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

To avoid cloning the HotbarBinding struct (which contains heap-allocated Strings) on every single successful insertion, you can move binding directly into by_slot.insert. Since previous is an owned value returned by insert, the borrow on by_slot ends immediately, allowing you to safely retrieve the replacement_action from the map only when a duplicate is actually detected.

        let slot = binding.slot;
        if let Some(previous) = by_slot.insert(slot, binding) {
            warnings.push(HotbarConfigWarning::DuplicateSlot {
                slot,
                previous_action: previous.action,
                replacement_action: by_slot[&slot].action.clone(),
            });
        }

Comment thread crates/config/src/lib.rs
Comment on lines +695 to +702
} => write!(
f,
"hotbar slot {slot} was bound to '{previous_action}' more than once; using '{replacement_action}'"
),
Self::UnknownAction { slot, action } => write!(
f,
"hotbar slot {slot} references unknown action '{action}'; keeping binding"
),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Misleading "keeping binding" when entry is overwritten by a duplicate

The UnknownAction warning fires before the duplicate-slot check, so the message says "keeping binding" even in the scenario where the unknown-action entry is the earlier of two identical slots and therefore gets discarded by the last-wins logic. A user who configures slot 2 with custom.action (unknown) first and mode.plan (known) second will see "keeping binding" for custom.action, yet custom.action won't appear in the final resolved bindings — only the later DuplicateSlot warning explains the loss. Consider rewording to "not rejecting binding" or "binding preserved unless overwritten" to avoid implying a guarantee about the final resolved state.

Fix in Codex Fix in Claude Code Fix in Cursor

Comment thread crates/config/src/lib.rs
replacement_action,
} => write!(
f,
"hotbar slot {slot} was bound to '{previous_action}' more than once; using '{replacement_action}'"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 The DuplicateSlot message reads "was bound to '{previous_action}' more than once" which is grammatically ambiguous — it sounds like previous_action was bound repeatedly rather than the slot being reassigned. A clearer phrasing would be "already bound to '{previous_action}'; replaced by '{replacement_action}'".

Suggested change
"hotbar slot {slot} was bound to '{previous_action}' more than once; using '{replacement_action}'"
"hotbar slot {slot} already bound to '{previous_action}'; replaced by '{replacement_action}'"

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Codex Fix in Claude Code Fix in Cursor

Comment thread crates/tui/src/config.rs
Comment on lines +1659 to +1662
/// Optional 1-8 hotbar slot bindings (#2064). When absent, future hotbar
/// UI slices use the built-in defaults from `codewhale_config`.
#[serde(default)]
pub hotbar: Option<Vec<codewhale_config::HotbarBindingToml>>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 skip_serializing_if missing on TUI hotbar field

ConfigToml.hotbar uses #[serde(default, skip_serializing_if = "Option::is_none")], but the mirroring Config.hotbar field here only has #[serde(default)]. If a TUI Config is ever round-tripped through toml::to_string, the None case may serialize as an explicit null/empty entry depending on the TOML serializer version, adding noise to written config files. Adding skip_serializing_if = "Option::is_none" keeps parity with the codewhale_config type.

Fix in Codex Fix in Claude Code Fix in Cursor

Comment thread crates/tui/src/config.rs
memory: override_cfg.memory.or(base.memory),
speech: override_cfg.speech.or(base.speech),
auto: override_cfg.auto.or(base.auto),
hotbar: override_cfg.hotbar.or(base.hotbar),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 All-or-nothing hotbar merge discards base slots silently

override_cfg.hotbar.or(base.hotbar) means any project/workspace config that defines even a single [[hotbar]] entry completely discards the user's global hotbar bindings. For layered configs this may be surprising — a user who sets trust.toggle on slot 8 globally will lose it entirely if a project config ships even one hotbar entry. There's no per-slot merging. This follows the same pattern as other Option config fields, but given that hotbar bindings feel user-personal, it may be worth a comment explaining the intentional all-or-nothing semantics.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Codex Fix in Claude Code Fix in Cursor

@Hmbown Hmbown merged commit a42e911 into Hmbown:codex/v0.9.0-stewardship Jun 7, 2026
3 checks passed
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