Skip to content

feat(tui): land the Tideline Settings shell on typed row facts and one taxonomy - #5730

Merged
Hmbown merged 4 commits into
mainfrom
feat/tideline-settings-shell-20260829
Aug 30, 2026
Merged

feat(tui): land the Tideline Settings shell on typed row facts and one taxonomy#5730
Hmbown merged 4 commits into
mainfrom
feat/tideline-settings-shell-20260829

Conversation

@Hmbown

@Hmbown Hmbown commented Aug 30, 2026

Copy link
Copy Markdown
Owner

Lands the Tideline Settings shell: typed row facts (SettingFact/SettingAuthority/SettingApplySemantics), one settings taxonomy, and the first visible Tideline surface. Branch was stacked on #5724 (now merged); main merged in, so CI runs on the truthful post-#5724 baseline.

This lane also carries crates/tui/src/tui/tideline.rs (UiSnapshot, ContextBudgetSnapshot, InteractionRegistry) — the typed-projection embryo the core/protocol extraction spec (Ops: CORE-PROTOCOL-EXTRACTION-SPEC-20260830) promotes into codewhale-protocol as its Phase A3 prerequisite.

Refs: Tideline acceptance matrix qa/tideline-acceptance-20260828/ACCEPTANCE_MATRIX.md; takeover prompt Tideline contract.

No-Issue: Tideline program slice tracked on the v0.9.12 milestone tracker #5573 and the Ops takeover plan.

Dead-code budget: raised 444 → 448 via scripts/check-dead-code-budget.py --update. Justification: the four new allows are deliberate stage scaffolding in the tideline.rs projection layer (Setting* typed facts composed by the landing slice that follows), same pattern as the 372→377 raise on #5698.

CodeWhale Bot and others added 2 commits August 29, 2026 20:44
…e taxonomy

Replace the raw ConfigView table with the Tideline settings shell: a
category rail, the setting/action list, and a detail pane at >=100
columns; a horizontally windowed category strip over a full-width list
below that (design `.settings-nav` rule), so 80x24 and 40x12 keep the
active category, Search, list rows, and per-row affordances.

Truth: every row carries typed `ConfigRowFacts` (setting / action /
diagnostic, authority, apply semantics, category override, App-observed
effective value, snapshot lane, activation command) decided at
construction. Saved rows fill saved/startup from the persisted value and
leave current/effective unobserved unless an explicit App field or the
UiSnapshot supplies it; action and diagnostic rows are not persisted
facts. `SettingApplySemantics` gains `ReloadRequired` (mcp_config_path)
and `UiNowEngineRestart` (workspace_follow_symlinks); default_mode,
launch_screen, and session_auto_resume are next-session. The detail
pane paints current, saved, startup, source, scope, apply, kind, and an
honestly unobserved availability lane.

One taxonomy: `ConfigCategory::ALL` (Appearance, Models & providers,
Pod, Work, Tools & MCP, Trust, Motion, Advanced) drives ConfigView and
the Tideline stage scaffold through shared rail/strip painters; the old
Composer/Notifications/Keybindings/Privacy scaffold list is gone and the
settings goldens are re-blessed. Settings opens on Appearance; raw
endpoint, credential receipt, context diagnostic, timeout, and routing
rows live under Advanced.

Interaction: list rows and category chips store exact `Rect` hitboxes
and require `Rect::contains`, so rail, divider, detail, status, and
footer clicks never select or activate a row. Every row paints a
toggle/choose/edit/open/read-only affordance, read-only rows are dimmed,
and the selected row states "Enter or click again: <verb>". Search
indexes localized category labels and a category click clears the
filter.

Localization: 40 new `MessageId`s for the category names, fact labels,
sources, apply states, editor kinds, notes, navigation hint, and
activation copy, translated in all fifteen packs; no shell chrome is
hard-coded English.

Tests cover truthful lanes, apply semantics, action rows, Rect
exclusion, shared taxonomy and strip windowing, localized rendering,
category search/click, affordances at the blocker sizes, short-height
Advanced reachability, default Appearance, and raw-row routing. An
interaction-evidence test drives the real view by keys and pointer at
40x12, 80x24, 100x30, and 120x32 and prints the rendered buffers under
--nocapture; it caught and fixed the 40x12 status row being shed (so
the activation copy never showed), a section caption over-charged one
line at the top of the window, the scope badge crowding the value below
60 columns, and an unscrolled list overflowing its last cell.

Review corrections (NO-GO pass):
- A settings.toml or config.toml that fails to load no longer becomes
  defaults labelled saved/startup: `Settings::load_persisted` carries the
  parse failure in `load_error`, every row backed by the failed store is
  read-only, reads "(unavailable)", and its saved/startup lanes report
  "unavailable: <error>". Live App lanes still render.
- Theme and locale take their current lane from App (`theme_id`,
  `ui_locale`). `settings::detect_low_motion_override` names the runtime
  overlay that forces low motion (NO_ANIMATIONS, VS Code, Termius, SSH,
  Tabby, legacy console) in the same precedence as
  `apply_env_overrides`; the motion rows then report
  `SettingAuthority::Environment` / `Terminal` with the concrete token.
- The category strip's ‹ / › overflow markers are hitboxes, so every
  category is reachable by pointer alone at 40 columns.
- Choice and text editors paint clickable `[ Apply ]  [ Cancel ]`
  controls with exact Rect hitboxes; choice rows are Rect hitboxes too;
  Enter/Esc and the controls share `commit_edit` / `cancel_edit`. The
  compact editor merges scope and current onto one row so choices and
  controls both fit at 40x12.
- Second-click activation is disarmed by every keyboard step, filter
  change, focus rebuild, and editor entry/exit; transient status clears
  on navigation.
- The full reachable surface is localized: search label, Choose label,
  choice footers, Apply/Cancel, choice labels and details, every setting
  hint, the default description, load-error and override sources — 112
  new ids across all fifteen packs. The unreachable per-key description
  table is removed in favour of the localized hints.
- Settings goldens are stored right-trimmed with no trailing blank rows.
- PTY receipts (tmux, SGR mouse) at 40x12, 80x24, and 120x32 exercised
  /settings, →/→, chip and › clicks, the choice editor, and a pointer
  Cancel against the built binary.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pm3rhiLxu2Zbgr64CrqWW8
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Aug 30, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-08-30T08:46:47.900319Z 771cbc6 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@claude

claude Bot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Hmbown's task in 3m 11s —— View job


Review: Tideline Settings shell (typed row facts + one taxonomy)

Todo list

Summary

This is a large but coherent, mechanical-in-the-good-sense refactor: it moves the settings surface onto typed SettingFact/SettingAuthority/SettingApplySemantics facts, collapses the old rail into one 8-category taxonomy shared by ConfigView and the Tideline stage, and extracts ~150 hardcoded English strings per key into localized MessageIds across all 15 locale packs. I read the diff against origin/main (24 files, +6596/-1618) and traced the logic rather than skimming.

Correctness

  • No unwrap()/expect()/panic! in production paths — every occurrence I found (crates/tui/src/tui/views/mod.rs) is inside mod tests (confirmed by mapping diff hunks back to the pre-PR file, where mod tests starts at line 4966).
  • New SettingAuthority variants (Environment, Terminal) and SettingApplySemantics variants (ReloadRequired, UiNowEngineRestart) are matched exhaustively everywhere they're consumed (setting_authority_label, setting_apply_label in views/mod.rs); no stray SettingAuthority/SettingApplySemantics match sites exist outside tideline.rs/views/mod.rs, so nothing was missed.
  • detect_low_motion_override() (settings.rs) is correctly wired into row construction for exactly the two rows it should affect (low_motion, fancy_animations in views/mod.rs:2109/2121), and precedence matches apply_env_overrides.
  • The new Settings.load_error (#[serde(skip)], settings.rs:312) is threaded through ConfigView::new_for_app correctly: a broken settings.toml/config.toml marks only rows owned by that store as unavailable/non-editable rather than silently presenting defaults as saved (views/mod.rs:2562-2580), and this is covered by a dedicated test with a real broken-TOML fixture.
  • Geometry math in the new shell (config_shell_panes, rail/list/detail widths) uses saturating_sub/.max(1)/.clamp(...) consistently — no obvious underflow/overflow at small terminal widths, and it's golden-tested at all four blocker sizes.

Provider/model/route safety (EPIC #2608) — No concerns. The provider/model rows read app.api_provider/session snapshot explicitly; there's no prefix-based inference from strings like deepseek/, anthropic/, etc.

Reuse/simplification — Good direction: config_hint_for_key, config_choice_label, and config_choice_detail moved from giant hardcoded &str match arms to MessageId lookups, removing ~140 lines of duplicated English prose while preserving content verbatim (spot-checked several against en.json). The old vertical rail and the new category strip share one taxonomy (ConfigCategory::ALL) instead of two parallel lists, per the PR's stated goal.

Tests — Strong coverage: new unit tests for the motion-override detector, broken-store unavailability, the new apply-semantics variants, and a new settings_strip_windows_to_the_selected_category_with_painted_hitboxes test; all four settings_*.txt goldens were re-blessed and diffed cleanly against the new layout. There's an existing parity test (message_id_list_english_pack_stay_in_exact_sync, shipped_complete_packs_have_raw_key_parity_with_english) that already guards every new MessageId has a key in every locale pack, so I didn't need to hand-audit all 14 non-English JSON diffs.

Security — No secret handling, shell/exec, or input-validation changes in this diff.

Note on verification: this sandbox's Bash tool required interactive approval for every command beyond read-only git diff/git log/grep-equivalents (including cargo test, cargo check), and no approver was available in this run, so I could not execute cargo test -p codewhale-tui or cargo check myself. The review above is based on full static reading of the diff plus cross-referencing exhaustiveness, call sites, and existing test/golden coverage — but you should treat an actual green cargo test -p codewhale-tui / CI run as the authoritative confirmation, not this analysis alone.

No blocking issues found.

@devin-ai-integration devin-ai-integration 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.

Note

This report is out of date. Scroll down for Devin Review's latest report on this PR.

Devin Review found 3 potential issues.

🐛 2 issues in files not directly in the diff

🐛 Motion edits can erase malformed settings

With a motion override active, authority hides a settings-file parse failure and leaves both motion rows editable. Editing either can overwrite the malformed file from defaults.


⚠️ Missing settings appear explicitly saved

When a key is absent, saved still receives its deserialized default. The settings view falsely presents an implicit default as an explicit saved choice.

Devin Review

Comment on lines +48 to +57
/// Goldens are stored without cell padding: every row is right-trimmed and
/// trailing empty rows are dropped, so `git diff --check` stays clean.
fn trim_rows(text: &str) -> String {
let mut rows: Vec<&str> = text.lines().map(str::trim_end).collect();
while rows.last().is_some_and(|row| row.is_empty()) {
rows.pop();
}
let mut out = rows.join("\n");
out.push('\n');
out

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.

🔍 Golden checks lose screen height

trim_rows drops trailing rows before comparison, so the 80×24 fixture now has 17 lines. Bottom-of-screen rendering regressions can escape this golden.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

…folding

The four new #[allow(dead_code)] items are deliberate projection-layer
scaffolding (typed Setting facts) composed by the landing slice that
follows this one. Visible raise per the gate's own procedure, justified
in the PR body.

Signed-off-by: CodeWhale Bot <bot@codewhale.net>
@claude

claude Bot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Hmbown's task in 3m 15s —— View job


Review: Tideline Settings shell (typed row facts + one taxonomy)

Todo list

  • Gather context (PR description, diff, changed files)
  • Verify Devin finding: motion override hides malformed settings file
  • Verify Devin finding: missing key presented as explicit "saved" value
  • Verify Devin finding: trim_rows drops trailing rows from goldens
  • Check correctness bugs (panics/unwraps/exhaustive matches)
  • Check provider/model/route safety invariant (EPIC v0.8.65 EPIC: Separate provider facts, model facts, offerings, and route resolution #2608)
  • Check reuse/simplification/dead code budget bump
  • Check test coverage
  • Check security
  • Post final review summary

Summary

I read the diff against origin/main (25 files, +6598/-1620), traced the ConfigRowFacts/SettingAuthority plumbing, and checked out the three issues Devin's review flagged rather than just re-skimming the whole surface.

Confirmed: motion-override edits can silently wipe a malformed settings.toml (real bug)

crates/tui/src/tui/views/mod.rs:1811-1815 wraps the low_motion/fancy_animations rows' facts in motion_facts, which — when an env/terminal override is active — calls .overridden() and flips facts.authority from SettingAuthority::UserSettings to Environment/Terminal (mod.rs:1389-1399).

The unavailable-marking loop right after row construction only checks the store error for rows whose authority == UserSettings (or WorkspaceConfiguration):

let error = match row.facts.authority {
    SettingAuthority::UserSettings => settings_error.as_deref(),
    SettingAuthority::WorkspaceConfiguration => config_error.as_deref(),
    _ => None,   // mod.rs:2568-2572
};

So when settings.toml fails to parse and a motion override (e.g. NO_ANIMATIONS) is active, error is None for these two rows even though settings_error.is_some(). They stay editable: true and keep the value from Settings::default() (since load_persisted_from_candidates_with_migration_unlocked, settings.rs:877-890, returns Settings { load_error: Some(..), ..Default::default() } on a parse failure).

Committing an edit on either row goes through ViewEvent::ConfigUpdatedcommands::set_config_valuepersist_single_settingSettings::transact, which reloads (hits the same parse failure → defaults again), applies just the one field, and writes the entire struct back to disk (settings.rs:1220-1227, 2867-2869). The net effect: a user with one bad line in settings.toml toggles low-motion and the whole file is silently replaced by an all-defaults file, discarding every other saved preference with no warning that anything besides low_motion changed.

Fix direction: the unavailable check at mod.rs:2568-2572 should key off row.facts.kind == ConfigRowKind::Setting plus the row's underlying store (still UserSettings for these two, since they're settings.toml-backed) rather than the display authority, which .overridden() legitimately repurposes for a different purpose (showing where the effective value comes from). Those are two different facts currently sharing one field.

Fix this →

Plausible: "saved" fact can present an implicit default as an explicit choice

setting_fact() (mod.rs:4591-4595) only nulls out saved when row.facts.store_error.is_some() (a parse failure). For a key that's simply absent from an otherwise-valid settings.toml, Settings's #[serde(default)] fills the field with the type default and store_error stays None — so the row's SettingFact.saved reports the default as if the user had explicitly written it. This is a pre-existing ambiguity in the Settings struct (most fields are plain bool/String, not Option<T>), but the codebase already has a working pattern for it — auto_compact_explicit, rail_panel_explicit, work_surface_placement_explicit in settings.rs:897-908, derived from the raw toml::Value — that just isn't applied to low_motion/fancy_animations/etc. Given this PR's whole point is to stop presenting implicit defaults as saved facts (explicitly called out in the broken-store handling), this is worth a follow-up even if it's not a blocking regression.

Confirmed but lower severity: trim_rows narrows golden coverage

tideline_tests.rs:50-58 (new in this PR) right-trims and drops trailing blank rows before comparing to the golden fixture, which is why settings_80x24.txt shrank from 24 to 17 lines. Since both the blessed golden and the live render go through the same trim_rows, this isn't currently a false-pass for content that should be there today — but it does mean any future regression that blanks out previously-content-bearing bottom rows (rather than removing content that was already blank) won't be caught at the 80×24 size, since the comparison silently ignores whatever's below the last non-blank row on both sides. Consider keeping the trim for git diff --check cleanliness only via trailing-whitespace stripping per line, without popping whole trailing rows — or asserting text.lines().count() separately if screen height matters for that golden.

Other notes

  • Provider/model safety (EPIC v0.8.65 EPIC: Separate provider facts, model facts, offerings, and route resolution #2608): no provider-prefix inference anywhere in the diff; provider/model rows read explicit app.api_provider/session state. No concerns.
  • Panics: confirmed every unwrap()/expect()/panic!() added in views/mod.rs is inside mod tests (starts at line 5960); nothing in the production path.
  • Dead-code budget: the 444→448 bump matches exactly 4 net-new #[allow(dead_code)] sites in views/mod.rs (the rest are same-count relabels from "translation scaffolding" to "stage scaffolding" comments); consistent with the stated justification.
  • Reuse: good direction consolidating the old two-rail taxonomy into ConfigCategory::ALL and moving hardcoded strings to MessageId lookups.

No new blocking issues beyond the motion-override/settings-clobber bug above, which I'd treat as a should-fix before merge given it's a data-loss path, not just a UX nit.
· Branch: feat/tideline-settings-shell-20260829

@devin-ai-integration devin-ai-integration 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.

Devin Review found 0 new potential issues.

Devin Review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 771cbc62c5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/tui/src/tui/views/mod.rs Outdated
Comment on lines +2568 to +2571
let error = match row.facts.authority {
SettingAuthority::UserSettings => settings_error.as_deref(),
SettingAuthority::WorkspaceConfiguration => config_error.as_deref(),
_ => None,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep storage ownership separate from override authority

When settings.toml is malformed or unreadable while a motion override such as NO_ANIMATIONS, VS Code, or SSH is active, motion_facts changes the low_motion and fancy_animations rows' authority to Environment/Terminal, so this match ignores settings_error. Those rows consequently remain editable and present default values as saved; committing either edit runs Settings::transact, which loads a parse failure as defaults and then serializes them, overwriting the user's file. Track the backing store independently from the effective authority when deciding whether to disable a row.

Useful? React with 👍 / 👎.

Comment on lines +4591 to +4595
let saved = row
.facts
.store_error
.is_none()
.then(|| self.saved_display_value(row));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve absence in saved-value facts

On a fresh install, or whenever a valid settings.toml/config.toml omits a particular key, deserialization fills row.value from defaults, and this expression nevertheless creates saved: Some(default) and copies it into startup. The new detail pane therefore claims values such as the default theme or an unset context window were read from disk even though SettingFact::saved is documented as the exact persisted value. Track per-key presence from the parsed documents and leave the saved lane unobserved when the key is absent.

Useful? React with 👍 / 👎.

Comment on lines +2456 to +2458
facts: ConfigRowFacts::saved_setting()
.authority(SettingAuthority::WorkspaceConfiguration)
.apply(SettingApplySemantics::ReloadRequired),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Respect MCP path environment ownership

When CODEWHALE_MCP_CONFIG or DEEPSEEK_MCP_CONFIG is set, startup applies that environment value after loading config.toml, and app.mcp_config_path consequently contains the environment-owned path. This row still labels the source as workspace configuration, treats the effective path as saved/startup state, and remains editable; saving writes a root config value that the environment overrides again on the next launch. Detect this environment owner and separate the live value from the persisted root value, making the row read-only or clearly reporting the override.

Useful? React with 👍 / 👎.

facts: motion_facts(
ConfigRowFacts::saved_setting()
.category(ConfigCategory::Motion)
.effective(app.fancy_animations.to_string()),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Resolve fancy animations through low-motion policy

When both saved switches are true, MotionPolicy::from_settings resolves the session to Reduced mode because low_motion wins, but this row records the raw app.fancy_animations flag as the effective value. The detail pane and list consequently report fancy animations as currently on even though all such motion is disabled. Derive the effective lane through the motion policy—or retain the raw flag only as current and report the resolved result separately.

AGENTS.md reference: crates/tui/AGENTS.md:L15-L16

Useful? React with 👍 / 👎.

let value_width = width.saturating_sub(10);
let fact_line = |name: MessageId, value: &str| {
Line::from(vec![
Span::styled(format!("{:<10}", self.tr(name)), muted),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Pad localized fact labels by cell width

For CJK and combining-script locales, Rust's format width does not represent terminal display cells: for example, the two-character Chinese 当前 is four cells and then receives eight spaces here. The value is still truncated against width - 10, so each fact line exceeds the detail pane and wraps, consuming extra rows and hiding later facts or activation help on shorter wide terminals. Measure and pad the translated label with the existing terminal display-width helpers.

Useful? React with 👍 / 👎.

},
ConfigRow {
section: ConfigSection::Composer,
key: "bracketed_paste".to_string(),
value: settings.bracketed_paste.to_string(),
editable: true,
scope: ConfigScope::Saved,
facts: ConfigRowFacts::saved_setting(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Mark bracketed-paste changes as restart-only

Changing this row through Settings only updates app.use_bracketed_paste; the terminal mode was enabled from the startup-captured TuiOptions::use_bracketed_paste, and no EnableBracketedPaste or DisableBracketedPaste sequence is emitted after this edit. The row nevertheless inherits Immediate, so users who disable bracketed paste as a terminal-compatibility workaround are told it applied while the terminal continues sending paste events until restart. Either reconfigure the terminal mode during the update or assign next-session semantics here.

Useful? React with 👍 / 👎.

Comment on lines +4750 to +4753
let description = if hint.is_empty() {
self.tr(MessageId::ConfigDescriptionDefault)
} else {
hint

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid edit instructions on read-only rows

For any action or diagnostic row without a key-specific hint—such as the dynamically keyed external-credential receipts or effective_auto_compact—this fallback says that Enter changes the value and that a scope badge is present. Those rows are read-only, suppress the scope badge, and ignore Enter, so the detail pane directly contradicts both the affordance and the following activation line. Choose fallback copy based on ConfigRowKind/SettingKind rather than applying the editable-setting instructions to every row.

Useful? React with 👍 / 👎.

vec![tab_line, search_line]
}
SettingKind::Choice => "‹ ›",
SettingKind::Integer | SettingKind::Text => "✎",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Provide an ASCII fallback for the edit affordance

With CODEWHALE_ASCII_SAFE=1, the backend replaces only glyphs registered in glyphs::ascii_fallback; has no entry there. Integer and text settings therefore still emit this Unicode pencil in the ASCII-safe Settings shell, unlike the other new affordances and navigation marks. Route this mark through the shared fallback mechanism or use an ASCII affordance when the compatibility tier is active.

Useful? React with 👍 / 👎.

…s authority

Windows CI caught a real cross-platform bug in the Tideline settings shell:
the broken-store marker matched on the row's resolved authority, so when the
motion-override probe (legacy console host on Windows runners) relabeled the
low_motion/fancy_animations rows to Terminal authority, a broken
settings.toml produced a synthesized saved lane instead of the unavailable
lane — different truth per host.

Authority and store are different facts: the override wins the effective
decision; the store is still broken. ConfigRowFacts gains a typed
SettingStore (UserSettings / WorkspaceConfig / None), constructors and the
authority builder set it, overrides keep it, and the marking loop keys on
it.

Tests: config_detail_never_synthesizes_current_from_saved pins its
synthetic row authority (host-probe independent); new regression test
overridden_motion_row_still_reports_a_broken_settings_store fails pre-fix
(store_error missing under NO_ANIMATIONS override) and passes post-fix.
Verified locally: 4/4 affected tests, 74/74 tideline family, fmt clean.
Windows proof: this PR's CI.

Signed-off-by: CodeWhale Bot <bot@codewhale.net>
@claude

claude Bot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Hmbown's task in 2m 23s —— View job


Review: Tideline Settings shell (typed row facts + one taxonomy)

Todo list

Summary

This push (824d926) is a targeted fix on top of the already-reviewed 771cbc6 snapshot: it resolves the P1 data-loss bug both Devin and my earlier review flagged (motion-override rows hiding a broken settings.toml). I re-read the merge base (origin/main = 8a3f14b) diff, traced the new SettingStore type end-to-end, and checked whether it silently changes behavior anywhere else.

The P1 fix is correct.

  • SettingStore (UserSettings / WorkspaceConfig / None, mod.rs:1356-1374) is a new fact tracking which store a row's saved/startup lanes actually come from, independent of authority (which .overridden() legitimately repurposes to describe the effective winner).
  • The unavailable-marking loop now keys on row.facts.store (mod.rs:2616-2621) instead of row.facts.authority, and .overridden() (mod.rs:1421-1431) correctly leaves store untouched via ..self — so a motion override can still win the effective decision while the broken-store detection keeps firing.
  • I checked every other ConfigRowFacts constructor for a regression this rewire could introduce:
    • read_only_setting() and the .authority() builder both still derive store via SettingStore::for_authority, so unrelated rows (managed-policy/workspace rows) keep their pre-fix marking behavior exactly.
    • diagnostic() hardcodes store: SettingStore::None regardless of the authority passed in (e.g. the external_credentials.* rows built with SettingAuthority::WorkspaceConfiguration at mod.rs:2597-2600) — but the marking loop already gates on row.facts.kind == ConfigRowKind::Setting (mod.rs:2623), and diagnostic rows are ConfigRowKind::Diagnostic, so they were never eligible for this marking either before or after the fix. No regression.
  • New regression test overridden_motion_row_still_reports_a_broken_settings_store (mod.rs:9175) is a real regression test: it writes a genuinely malformed settings.toml, sets NO_ANIMATIONS=1, and asserts store_error.is_some() and saved/startup == None while effective stays populated from the live App value — this fails on the pre-fix code (as the commit message states) and exercises exactly the bug that was reported.
  • config_detail_never_synthesizes_current_from_saved was updated to pin the row's authority explicitly (mod.rs:8679-8681) so the test no longer depends on the host's motion-override probe — a legitimate flakiness fix, not a weakened assertion.

Still open (not addressed in this push, not blocking): the P2 items from the earlier Codex/Devin passes — implicit-default-presented-as-saved (setting_fact/saved_display_value), trim_rows narrowing golden screen-height coverage, CJK/wide-glyph label padding in the detail pane, missing ASCII fallback for the affordance, mcp_config_path/bracketed-paste apply-semantics edge cases, and fancy_animations not resolving through MotionPolicy. None of these were touched by 824d926/e83815a, so they remain valid follow-ups but don't block this specific fix.

Provider/model/route safety (EPIC #2608) — No changes in this push touch provider/model selection; still no prefix-based inference anywhere in the diff.

Dead-code budgete83815a (444→448) matches the PR body's justification exactly (4 new #[allow(dead_code)] sites for the projection-layer scaffolding); no unexplained budget creep.

Note on verification: this sandbox required interactive approval for every Bash command beyond read-only git log/git show/git diff (including cargo check, cargo fmt --all -- --check, and even git fetch), and no approver was available in this run, so I could not execute the new regression test or a full build myself. The analysis above is from full static reading of the diff and cross-referencing every call site of the changed types — but treat an actual green cargo test -p codewhale-tui / CI run (including the Windows leg this fix specifically targets) as the authoritative confirmation.

No new blocking issues. The previously-reported blocking bug is fixed with a real regression test.

Branch: feat/tideline-settings-shell-20260829

@Hmbown
Hmbown merged commit bbd5f6f into main Aug 30, 2026
40 of 41 checks passed
@Hmbown
Hmbown deleted the feat/tideline-settings-shell-20260829 branch August 30, 2026 17:06
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.

1 participant