Conversation
ayangweb
marked this pull request as draft
August 28, 2026 06:15
ayangweb
force-pushed
the
next
branch
2 times, most recently
from
September 2, 2026 12:41
56c3f87 to
fb72bce
Compare
The changelog's newest entry is 2.0.0 while the workspace still declared 1.1.0, so `just release-notes` could not find the entry it was asked to publish. The workspace now declares 2.0.0 and Cargo.lock follows for the fifteen member packages; no dependency moved.
The General page carried five groups and twenty-five settings that belonged to four unrelated areas, so a user had to scroll before they could find the switch they wanted, and the sidebar offered only four entries for all of it. Four of those groups now stand on their own next to General, Models, Shortcuts and About: Overlay holds the model window's behaviour, appearance and frame-rate limit, Interaction how the model reacts to behaviour shortcuts and to the pointer, Input the keyboard release fallback and the gamepad dead zones, and Application runtime status, system integration and startup and updates. A page that still holds more than one concern shows those concerns as a second level in the sidebar, which is the only nesting the component offers. The configuration recovery notice moved out of the pages and now renders above the settings component, because a notice that lives on one page is invisible to anyone who navigated elsewhere while the restore action is reachable from every page. It also gained the semantic node it never had: the window's accessibility tree is built by hand and GPUI is constructed with its own adapter disabled, so a screen reader used to meet the restore button and never learn what it was about. Its visible label and its accessible name now come from a single owner, which is how the two came to describe different actions in the first place. Two headings that only repeated the row beneath them are gone, and the settings error table now names its keys in full instead of assembling them from a suffix. That last change is what makes the two new guard tests possible: one asserts that every key the source asks for exists, the other that every key in the catalog is asked for. Between them they cover both directions of the mistake this work started from, a key that renders as its own name and a key that no longer reaches any screen.
The shortcuts page offered two bulk mutations — restore every binding to its defaults and clear every binding — that duplicated what per-row editing already covers and hid the model-behavior re-assignment rule behind a button. Drop the buttons together with their whole chain: view methods, accessibility nodes, focus handles, the dedicated RestoreDefaultShortcuts IPC command, the application-level reset, the locale entries, and the tests that only exercised them. The contract note now states the surviving asymmetry (cleared model behaviors are re-filled on the next model activation) without referencing the button. 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
The shortcuts page switched between its window and model scopes with a tab bar, which gave the page a second parallel navigation level that the accessibility tree never exposed. The two scopes are now titled groups, which the settings component renders as second-level sidebar entries, and the tab state, the TabBar/Tab usage, and the in-page duplicated title are gone. Row identity stays on the one combined order the keyboard tab order and the accessibility node ids are numbered from, and the scope groups carry search keywords so the page stays reachable through the sidebar search box.
Login startup is the more consequential of the two launch preferences, so it now sits first in the startup-and-updates group, with the automatic update check below it. The accessibility tree keeps the same order so screen readers read the switches in the order they appear. 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
Login startup registers the running executable with the operating system, so
the registration outlives the process that wrote it and points at whatever
executable was current at the time. A development build's executable is a
Cargo build output: cargo build, cargo clean or a profile switch replaces or
removes it, which leaves a login item pointing at a binary that no longer
exists. The capability therefore belongs to a released product, and ADR-0051
supersedes the part of ADR-0043 that had extended it to development builds.
The gate sits in the application layer, where update_check_available() already
answers the same kind of question: bongocat-app reports the build environment
as the reason without calling bongocat-platform at all, and the read and the
write direction answer with the same state so a client never observes the
capability changing between them. The write is a no-op rather than an error,
because the control that sends it is disabled and a stale window or a scripted
client must not raise a failure the user cannot act on. The platform crate, its
auto-launch backend and the environment isolation naming are untouched.
The switch is now built by hand, because the packaged switch field constructs
Switch::new("check") internally and cannot carry a tooltip, and this row is the
only place the reason can be shown. Its disabled state answers one question
only, whether the build offers the capability at all: whether the control can
act right now stays with the action the accessibility node already reports, so
a released build keeps the switch normally available while the snapshot is
still loading. The hover text is the catalog entry the row already uses, which
is what keeps the two from drifting apart.
Fifteen catalog keys only restated the row's own label ("Show or hide the
model window", "Open the application when you sign in"), so they are gone
from both locales and from the settings rows that carried them. The rows
that keep a description carry something the label cannot: value ranges,
the meaning of 0, what "keep on screen" may cover, the dead-zone and
release-fallback definitions, and every open-at-login state that needs
repair or is unavailable.
The labels were not left to carry that weight alone: "Overlay" is now
"Model window", the name the window already used everywhere else and the
one the Chinese sidebar carried from the start, and the click-through,
keep-on-screen, mirror, delay and runtime strings now say what they mean
instead of leaning on a description that is no longer there.
The open-at-login row is the one conditional case: its description is now
an Option that only the states the switch cannot explain fill in, so the
two settled states render without a second line while ADR-0051's shared
row-and-tooltip sentence is untouched. The accessibility tree follows the
same rule, because a switch node's value is the description text: a row
that lost its copy no longer announces it as the control's value.
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
Both overlay backends arm hide-on-pointer-hover only while that switch is on and input is running, so the delay seconds are read by nothing when the switch that owns them is off. The row still accepted edits, which invited the user to tune a value that could not do anything yet. A shared predicate now answers whether the delay applies at all, and the settings row, the two accessible steppers and the stepper action all read it: the row renders disabled through the component disabled state, which forwards to the number input, the steppers report themselves unusable instead of clickable, and a click that arrives from a tree rendered before the switch changed is dropped. What the user recorded is deliberately kept, so switching the feature back on restores their delay rather than a default. The settings-window smoke covers the new gate: it runs with the default configuration, where the switch is off, so the increase stepper changing from usable to unusable is asserted against the real tree. 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
Each model counts its own behavior defaults from the first slot now. The assignment treated every chord in the configuration as taken, so the second model activated continued where the first stopped: with the three preset models, which declare seven behaviors each, it started at Cmd+8 and the third at Cmd+15. Only the model being assigned and the application commands count as taken, because exactly one model's behaviors are live at a time. That also settles what the platform may register. `active_shortcuts` used to compile the whole configuration, so every model's chords reached the operating system even though only the live model's can ever fire: the dispatcher drops a target whose model is not active. The other halves now never get that far, and `prepare_model` and `select_model` rebuild the table once the activation has succeeded, which is what makes the model being left stop answering its chords and the incoming one answer its own without a restart. `select_model` never rebuilt the table, so the incoming model's shortcuts used to do nothing until the next launch or the behavior switch was toggled. Conflict detection follows the same scoping: commands stay unique among themselves and may not be shadowed by a behavior, each model's own bindings stay unique, and the same chord under two models is now legal. `ShortcutConfig::active_bindings` is the projection that keeps a compiled table unambiguous, and `NativeConfig::validate`, the capture check on the settings page and the application all agree on it.
A model behavior row was labelled with the model id and the package's own identity: "standard (motion:CAT_motion:0)", or "standard (expression:live2d_expression0.exp3.json)". That identity is what the configuration stores and what the row lookup matches on, but it is not a name. It exposes the motion group a clip happens to live in and the file name of an expression, and it reads as noise in a list whose only decision is which chord plays which clip. Rows are named by flattened position now: every motion group in declaration order, then every expression. The kind and the number are carried on the row as `BehaviorOrdinal` and resolved by `ShortcutRow::name`, which counts motions and expressions separately so the numbering is continuous across groups and never repeats inside a kind. The flattening follows the row order `bongocat-app` already assigns the automatic chords in, so with the built-in models "Motion 1" is the motion that got the first digit rather than merely appearing to match it. The row keeps its `(model_id, behavior_id)` identity, so what a shortcut fires is unchanged, and the accessibility labels resolve the same name, so a screen reader hears what the list shows. Both locale keys are spelled out in their match arms instead of being assembled, which is the only shape the source scan in `bongocat-i18n` can see.
The platform shortcut owner mirrors the shared table into real OS registrations keyed by the hotkey id — the chord — and skipped every chord that was already registered. Each model numbers its behaviour chords from the primary modifier's first digit, so a model switch leaves the same chords in the table and changes only the model behind them: the incoming model's bindings were never registered with their own targets, and the platform kept answering for the model being left. The dispatcher drops a target whose model is not active, so every behaviour shortcut of the new model went silent until the next launch. A chord that left the table was also unregistered but never forgotten, so the same chord could not be registered again once it re-entered: after switching away and back, a model's shortcuts stayed dead for good. The mirror now plans each change as removed, added and retargeted, and applies it: a chord that stays registered updates its target in place, because re-registering it is refused on Windows and duplicates the registration on macOS, while a chord that leaves is unregistered and forgotten. The registration map and the separate target map are one map now — the split was what let them drift apart. `HotkeyRegistrar` separates the OS surface from the reconciliation so the mirror is covered by tests with a fake registrar: a model switch hands the shared chords to the incoming model, a chord that re-enters the table is registered again, and an unchanged table touches nothing.
Each shortcut scope now carries its own enable switch as the first row of its group, so the list that stops working and the switch that explains it are never on two different pages. The model gate moves off the Interaction page, and application shortcuts gain a gate of their own (`shortcuts.commands_enabled`, default `true`). Both gates only decide whether their half reaches the platform table: neither clears nor rewrites a recorded chord, so switching one back on needs no re-recording, and the settings payload cannot rewrite a gate while a chord is recorded. The gates live in `ShortcutConfig::active_bindings`, the single projection of the bindings that are live right now. Only the model gate defaults to off, the opt-in narrowing recorded in item 91. Decision: ADR-0052.
A gate switch now binds to the settings it controls through one unified rule (new window::setting_gate::SettingGate): the switch itself is only disabled where editing is structurally impossible, while the gated rows dim whole-row - label included, matching how SettingItem::disabled dims a packaged field - drop their click and key handlers, leave the tab order and the accessibility tree, and their mutators refuse requests, so a client acting on a tree rendered before the switch flipped still changes nothing. A capture left running when its gate turns off is cancelled instead of writing a binding nothing displays. Both shortcut gates adopt the rule, and the hover-hide-delay row now reads the same gate instead of deriving its own condition. No gate ever rewrites the gated value, and the transient in-flight save flag stays out of every gate. This supersedes the part of ADR-0052 that kept the rows editable while a gate was off; ADR-0053 records the unified rule. Decision: ADR-0053.
The model card actions reached for hand-inlined Lucide SVGs because the component IconName enum does not export a pencil or a trash can. The application already registers AllAssets, so the full gpui-kit assets catalog is available at runtime: swap both buttons to gpui_kit::assets::IconName (SquarePen, Trash) and drop the inline SVG constants.
The delete control used to swap itself for an inline confirm/cancel pair,
which changed the shape of the card while the question was up. It now
opens a PopConfirm - the project's own wrapper over the official Popover
(crates/bongocat-ui/src/pop_confirm.rs) - so the card stays put. Version
0.6.4 ships no PopConfirm primitive and no Popover::arrow, so the arrow
waits for a dependency bump.
Two details the surface had to get right:
- The accept button wears the primary variant, not the danger one. The
warning icon already carries the destructive reading, and a red button
under a red icon says the same thing twice.
- The icon and the sentence are centred against each other. An icon box
is shorter than a line of text, so top alignment left the glyph
floating five pixels above the words.
Enter has to be claimed on a wrapper around each button rather than on the
button itself. gpui dispatches an action before it dispatches on_key_down,
and the popover binds Enter to its own toggle on its own key context, so a
handler on the button would never run: the surface would close without
reporting anything, which is indistinguishable from a decline.
models.delete_confirmation now holds the whole sentence instead of a
template filled by format!, so models.actions.confirm_deletion is gone.
Two defects found on the way:
- The shortcut capture smoke expected every row to follow the global
editing state alone. ADR-0053 made a row report itself disabled when its
scope's switch is off, and the model scope starts off, so every row of
that page was judged wrong and the run stopped before the model page
smoke ever ran. The expectation now asks each row's own scope.
- The i18n crate gains a third key gate. rust_i18n's i18n! is a proc macro
that reads the locale files while it expands and records no dependency on
them, so freshness rests entirely on build.rs's rerun-if-changed. Miss
that path and the crate serves the previous catalog while every other
check stays green, because validate-locales.py and both existing key
scans read the JSON rather than the catalog that ships. On 2026-09-21 the
model delete confirmation rendered the retired template
%{status} %{confirm_deletion} with the whole suite passing.
compiled_catalog_matches_the_files_on_disk compares the compiled value
against the file a rebuild would read.
ADR-0047's decision text now says the delete asks in a popover rather than
in two stages.
The delete control was hidden on the active card, so removing the model you were looking at meant switching to another one first. An imported model is now deletable at all times. Deleting the selected model switches to the standard preset before the package is removed: the runtime must never be left holding a model whose files are about to disappear, and a switch that fails aborts the delete instead of pulling them out from under the live model. Application::delete_model states that rule once, in is_selected_installed_model, replacing the two separate checks it used to make; ApplicationError::SelectedModelDeletion is gone with it. SettingsErrorCode::SelectedModelCannotBeDeleted and its two locale strings follow, because nothing can reach them any more and the message they carried - "switch to another model first" - describes a rule the app no longer has. Verified: just check; cargo test --locked --workspace (no failed target); tools/validate-locales.py (2 locales, 286 keys each); both new assertions proved to fail under mutation and restored byte-for-byte; the models page smoke twice, exit code 0.
apply_theme is a public API whose parameter is `&impl HasWindowHandle`, so theme.rs imports the trait unconditionally - including the `#[cfg(not(any(target_os = "macos", target_os = "windows")))]` platform module, where the theme itself is a documented no-op. The dependency, however, sat in the macOS/Windows target section, so the Linux build never had it and clippy failed with two `error[E0432]: unresolved import raw_window_handle`. raw-window-handle is a cross-platform handle abstraction (Wayland, X11, Win32 and AppKit all implement it), not a platform binding, so it moves to the plain [dependencies] table. Cargo.lock is unchanged. This is one of the long-standing CI failures, not a regression: the `Test Native workspace (ubuntu-latest)` job has failed on `Run Clippy` in every run of the baseline, with the same two errors. Verified: `cargo check -p bongocat-platform`; a pair of isolated probes compiled for x86_64-unknown-linux-gnu, where the target-gated layout reproduces the CI error (E0432, exit 101) and the unconditional one resolves (exit 0); the dependency audit and `just check` pass. Not verified locally: the full Linux workspace build, which needs a Linux C toolchain this machine does not have.
Ubuntu's clippy step runs `-p bongocat-app --all-targets --features ... -- -D warnings`, and `behavior_shortcut_primary_modifiers` is read by exactly one test: the model-activation case, which is gated to macOS/Windows because starting a rendering application is a platform capability. On Linux the caller disappears and the helper is left with no user, which `-D warnings` promotes from a dead_code warning into an error. Gate the helper the same way as its caller. The workspace sets no dead_code lint and the crate has no `allow(dead_code)` anywhere, so matching the existing `#[cfg]` style keeps the helper visible on the platforms that use it. This was masked until now: the step used to die earlier, on `raw-window-handle` resolving only in the macOS/Windows target section of `bongocat-platform`, so the two `-p bongocat-app` clippy commands had never run on Linux. Verified: `cargo fmt --all --check` and `cargo clippy -p bongocat-app --all-targets --features storage-test-injection -- -D warnings` both pass on macOS (the gate must not hide the helper where it is used); a pair of isolated probes compiled for x86_64-unknown-linux-gnu reproduces the CI error verbatim with the ungated shape (exit 101, "function ... is never used") and resolves with the gated one (exit 0); `just check` exits 0. Not verified: the full Linux workspace build, which needs a Linux C toolchain for `ring` and `libdeflate-sys` that this machine does not have.
The settings window talked about the app's own machinery. "Pointer" is now "Mouse" everywhere the setting is about the mouse - the Interaction page's group, the mirror and ignore rows, and the hover-hide row and its delay. "Key release fallback" is now "Key release timeout", and its description says what the setting is for instead of naming the two mechanisms that have to fail first: a captured key is released on its own when no key-up event arrives within the time you set, so it never stays stuck. The rest is the same idea applied wherever the copy named an internal concept. The status line no longer connects to a "runtime", and a setting that fails to apply no longer reports that a runtime declined it. The update errors talk about "this version" instead of "this build", a release without a matching download says there is no installer for this computer, the configuration details say "configuration format" instead of "Schema", and the About page says "Development build" / "Release build" instead of "Development" / "Production". Some wording was only vague rather than internal: the Chinese "open model location" is now "open its folder" (English already said "Open model folder"), an empty behavior list says there are no motions available, "Model definition is unsupported" says "model format", the backup count no longer calls backups "candidates", and the open-at-login rows that need repair say to turn the switch off and on instead of to "enable". Nine catalog leaves are renamed because the copy under them no longer carries the word the key named: settings.model_interaction.pointer.title becomes .mouse.title, mirror_pointer_tracking and ignore_pointer_input become mirror_mouse_tracking and ignore_mouse_input, and the overlay's hide_on_pointer_hover and hide_on_pointer_hover_delay become hide_on_mouse_hover and hide_on_mouse_hover_delay, along with the two shortcut actions that adjust the delay. The configuration schema keeps its own names (overlay.hide_on_pointer_hover, model_settings.mirror_pointer_tracking, ignore_pointer, release_fallback_timeout_ms): those are the frozen v1 contract, a different naming axis from the catalog, so a renamed key and its config field deliberately no longer match. The values are copied verbatim in three places that a grep for the Chinese copy misses, so all three move with them: SettingsError's Display impl, which repeats the catalog's English, the assertions in bongocat-ui's window tests and in bongocat-i18n's own tests, and tools/tests/test_product_version_contract.py, which asserts the About page's version line as source text. No accessibility node changes: only values moved, no visible text was added. Both catalogs keep 286 leaves. zh-CN changes 40 values and renames 9 leaves; en-US changes 38 and renames the same 9, because open_location and startup.unsupported_build were already idiomatic in English. Verified: `just check` (34 targets, 0 failed, 815 passed); validate-locales.py reports 286 keys per locale; the 63 tools tests pass; `cargo test -p bongocat-i18n` passes both key guards and the compiled-catalog check; --settings-window-smoke with --models-page-smoke, --configuration-recovery-smoke and --settings-window-state-smoke all exit 0. Two mutations turn the guards red: restoring one renamed key literal fails source_referenced_keys_exist_in_the_catalog, and restoring the old Chinese backup-count text fails the recovery assertion; both were reverted against a sha256 check and re-run green. Not verified: every Windows path (cfg(windows) tests and smoke cannot run here); truncation of the longer English strings at 125/150/200% scaling; and what a screen reader announces for the renamed group title. The ADRs and the earlier TODO entries still quote the old copy in places; they are decision and history records and were left alone.
Three follow-ups to the plain-language pass, chosen by the maintainer. Two catalog messages said something other than what they mean. Importing the folder that contains the model library reported "the model source cannot be imported", which is both stilted and uninformative; that code fires only when the selected folder is the store or one of its ancestors, so it now says that and asks for a specific model folder. "Model storage is busy" named the store rather than what the user is waiting on, and now says the models are busy with another operation. Three labels were imprecise in the other direction. "Texture is invalid" is the one message in its group that does not name the model, so it becomes "Model texture is invalid"; the Application page's "System integration" group holds the menu bar, tray and taskbar icon switches, so it is "System icons"; and the disk-full error says the disk holding the configuration is full rather than that "configuration storage" is full. Every error message in the catalog now starts with a capital letter. The 41 that started lowercase were the only sentence-case outliers in the catalog: status, models and update entries all open with a capital. These are notification bodies, not fragments, so the lowercase openings read as mistakes. The SettingsError Display impl, which repeats the catalog's English verbatim, and the assertions that pin it move with them. Three documents still quoted the old labels, so a reader searching the UI for them found nothing: ADR-0036's quote of the unsupported-source error, ADR-0047's five mentions of the open-folder action, and the TODO's four mentions of the same action plus its per-page settings inventory, which still listed the pointer group and the key-release row under their old names. Technical prose keeps "pointer", "state reconciliation" and "revision" where it describes the mechanism rather than the copy, and the historical entries that record the exact text restored at the time stay as they are. Verified: `just check` (34 targets, 0 failed, 815 passed); validate-locales.py reports 286 keys per locale; the 63 tools tests pass; `cargo test -p bongocat-i18n`, `-p bongocat-ui --lib` and `-p bongocat-app --lib` are green. Not verified: every Windows path (cfg(windows) tests and smoke cannot run here); the rendered truncation of the longer English error messages; and what a screen reader announces for the renamed group titles.
Two consistency leftovers from the plain-language pass. The catalog used two ellipses: the update entries and the Chinese typographic standard use the single-character ellipsis, while ten progress and status entries in both locales - loading models, choosing a folder or archive, the import stages, refreshing, saving, connecting, checking login startup - still used three ASCII periods. The six tests that pin those strings move with them. `status.refreshing` also still read "Refreshing runtime snapshot...", which is the internal vocabulary the previous commit removed from its neighbour "Connecting to runtime..."; it is now "Refreshing status...". A scan of all 286 English leaves finds no sentence that still opens with a lowercase letter once placeholder, proper-noun and language-name values are set aside. Verified: `just check` (34 targets, 0 failed, 815 passed); validate-locales.py reports 286 keys per locale; the 63 tools tests pass; `cargo test -p bongocat-i18n`, `-p bongocat-ui --lib` and `-p bongocat-app --lib` are green. Not verified: every Windows path (cfg(windows) tests and smoke cannot run here), and how the ellipsis renders in fonts outside the two shipped locales.
Recover damaged configuration from the newest valid backup or defaults without a restore flow, and remove the project-owned accessibility bridge and hidden-only UI state. Keep product and smoke settings windows on the same visible composition.
The window is created and shown before the settings service answers its first snapshot, so the frames in that gap had no snapshot to read and rendered the built-in defaults. A Simplified Chinese user watched the window redraw from English as soon as the snapshot arrived, title bar included, and the update window opened on the defaults too. Both windows now start from the language and theme the product already resolved at startup, which the settings service read for the first system menu anyway, and render them until the first snapshot replaces them. 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.