refactor(inference): make Ollama the single source of truth for active model#106
Merged
quiet-node merged 4 commits intomainfrom Apr 26, 2026
Merged
refactor(inference): make Ollama the single source of truth for active model#106quiet-node merged 4 commits intomainfrom
quiet-node merged 4 commits intomainfrom
Conversation
…e model Eliminates the DEFAULT_MODEL_NAME fallback that silently downgraded existing users to gemma4:e2b on first launch of the model picker. Active model is now Option<String>: None when no model is installed and no choice is persisted, otherwise the user's persisted slug or the first installed model. Renames [model] config section to [inference] (only field is ollama_url, so the section now describes the inference daemon, not the model). Frontend: capability mismatch strip extended for the no-model state, picker chip stays visible as the recovery affordance, send is gated through the existing shake animation, save short-circuits when active model is null. No backward compatibility for the section rename: pre-#103 was a placeholder default and PR #103 has not shipped to users yet. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
…e picker chip Phase A surfaced two defects in manual smoke test: 1. Picker chip was missing in chat mode when activeModel was null. The chip is the recovery affordance, so it must stay visible whenever the user is in chat mode regardless of model state. 2. The no-model warning strip rendered the same copy whether Ollama was unreachable or simply had zero models installed. The two states need different recovery actions: start Ollama vs pull a model. get_model_picker_state now returns a typed ollamaReachable flag so the frontend can pick the right strip copy. The chip is rendered unconditionally in chat mode and hides only when Ollama is unreachable (nothing to pick from). Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
- Relax compose-mode chip gate so the picker chip stays visible whenever Ollama is reachable, even with zero models or no active selection. Match chat-mode by routing onModelPickerToggle through ollamaReachable. - Drop the duplicate capability-conflict toast on submit-gate. The persistent strip already surfaces the message; the transient toast added visual clutter and overflowed the window in compose mode. Removed the now-unused Toast component and its tests. - Update no-models strip copy: "Thuki couldn't find any local LLM models. Pull one from Ollama with ollama pull <model>, then come back." Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
- Move MAX_MODEL_SLUG_LEN from models.rs to config/defaults.rs so all baked-in pipeline constants live in the single source of truth declared by CLAUDE.md. - Document MAX_MODEL_SLUG_LEN in docs/configurations.md alongside the other Ollama API safety limits. - Switch useOllama.ts modelName coercion from `||` to `??` so an empty slug (malformed state) is no longer silently dropped; only a true null absence becomes undefined. Updated the matching test contract and added an askSearch null-active-model branch test to keep coverage at 100%. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
quiet-node
added a commit
that referenced
this pull request
Apr 27, 2026
…ite commands (#108) * feat(settings): backend foundation for Settings panel Wires the Tauri command surface, config-write pipeline, and tray menu entries the Settings GUI depends on. No frontend yet. Architecture - AppConfig wrapped in parking_lot::RwLock so the GUI can mutate runtime config in-process. Reads (every Ollama call, every search call, every command) take cheap clones via state.read().clone(); writes are rare and use a write guard. Parking_lot avoids std::sync poisoning on writer panic. - Single security boundary: defaults::ALLOWED_FIELDS is the authoritative allowlist of (section, key) pairs the GUI may write. set_config_field rejects any pair not in the list with a typed UnknownField error, preventing the GUI from writing fields that do not exist or are intentionally not user-tunable (activation timing, vision limits, etc). - toml_edit for per-field patches so user comments and key ordering in config.toml survive across GUI saves. Round-tripped through loader::load so clamp/empty-fallback/cross-field-invariant rules apply identically to GUI edits and hand-edits. - Focus-event reload (no file watcher): reload_config_from_disk re-runs loader::load on Settings window focus. Saves ~250 LOC and one critical failure mode versus the watcher approach the eng-review outside voice challenged. - Activation policy switches Accessory <-> Regular while Settings is visible so the Accessory app can take focus on its Settings window. Restored on close. Window hide-on-close preserves React state across reopen. Commands (registered via tauri::generate_handler!) - get_config — read snapshot from RwLock. - set_config_field(section, key, value) — validated per-field write. - reset_config(section?) — restore one section or whole file to defaults. - reload_config_from_disk — focus-event sync. - get_corrupt_marker — read+consume the recovery marker. - reveal_config_in_finder — open Finder with file selected. Errors - ConfigError extended with UnknownSection / UnknownField / TypeMismatch / Parse variants. Serializes as a tagged enum across the IPC boundary so the frontend can render typed inline error pills. Tray menu - Reordered to: Open Thuki / Settings... (Cmd+,) / --- / Reveal config.toml / --- / Quit Thuki (Cmd+Q). Cmd+, is a menu accelerator (fires only while the tray menu is open) — see design doc P5 for the macOS contract reasoning. Tests - 30 new tests in settings_commands::tests covering allowlist enforcement, every JSON->TOML coercion path (integer/float/string/array primitives plus type-mismatch rejection), comment preservation, document I/O errors, and the corrupt-marker consume/cleanup contract. - 463 tests pass; cargo clippy -D warnings clean; cargo fmt clean. - Three pre-existing clippy errors fixed in passing (commands.rs await guard scope, images.rs slice::from_ref, search/config.rs const assert). Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * feat(settings): Settings panel UI implementation Frontend implementation of the Settings panel approved in the design doc. The panel ships in this commit as a working window: bun run validate-build now produces a release .app bundle that exposes Settings... in the tray menu and renders the three-tab UI inside the secondary NSWindow defined in tauri.conf.json. Window architecture - main.tsx routes on getCurrentWindow().label so one bundle serves both windows. Label "settings" mounts <SettingsWindow/>; everything else gets the existing chat overlay tree. No accidental cross-window state coupling. Visual language ("Polished Portal", variant A from /design-shotgun) - src/styles/settings.module.css: complete token set (typography 11/12/13/14/24px, spacing 4/8/12/16/24/32, pill tab 76x52 @ 12px radius, slider 4px track / 14px thumb, 32x32 stepper). All colors reused from src/App.css; no parallel design system. - Filled orange pill tabs with icon-above-label, active = #ff8d5c bg with dark text. Compact 56px rows. Uppercase 0.18em tracking section sub-headings. Helper text below each control. Bottom-right Saved pill (1.5s, aria-live="polite"). Reduced-motion media query. Hooks - useDebouncedSave: per-field 250ms debounce. Skips the seed render so initial mount never fires a save. flushNow() exposes a synchronous save for tab-switch flush. resetTo() re-seeds without scheduling a save (used by parent reload). - useConfigSync: invokes get_config on mount, subscribes to tauri://focus, calls reload_config_from_disk on focus to pick up external hand-edits. Replaces the file watcher subsystem the eng review collapsed. Form primitives (src/settings/components) - SettingRow / Section / SectionHeading layout primitives. - TextField, Textarea, Dropdown, NumberSlider, NumberStepper, OrderedListEditor (up/down/delete + add — no drag-and-drop, YAGNI per design doc). - SavedPill (aria-live="polite"), ConfirmDialog (Esc-to-cancel, destructive variant for resets). - SaveField: render-prop wrapper that binds one form row to one (section, key) tuple via useDebouncedSave. Resync token from parent re-seeds on external changes without scheduling a redundant save. Tabs - GeneralTab: MODEL (model list editor + Ollama URL) / PROMPT (textarea + char count) / WINDOW (4 sliders) / QUOTE (3 steppers). 13 controls, scrolls in the 720x560 fixed window. - SearchTab: SERVICES (URLs + sandbox-isolation warning) / PIPELINE (3 steppers) / TIMEOUTS (5 sliders). 10 controls. - AboutTab: APP (version + links) / ACTIVATION (info-only) / PERMISSIONS (status pills + Open System Settings buttons, refreshed on window focus) / LIMITS (info-only) / FILE (Reveal config.toml, Refresh from disk, Reset all to defaults). Accessibility - ARIA tablist with arrow-key cycling between tabs. - aria-selected/aria-controls/tabpanel role wiring. - Sliders carry aria-valuemin/max/now/text. - Saved pill is aria-live="polite"; row errors are role="alert". - Focus rings via outline (not box-shadow) so they survive transforms. - Reduced-motion media query disables transitions. Resets - Per-tab "Reset to defaults" link triggers the ConfirmDialog (matches mac convention for destructive actions). - About → Reset all opens a separate destructive-styled confirm and invokes reset_config(section: null). Corrupt-config recovery - Mount-time invoke of get_corrupt_marker. If a marker is returned, a dismissible banner appears at the top of the active tab body with "Reveal" (opens Finder via open_url) and "Dismiss" actions. Banner state is per-session in React (consumes the marker on the backend so it does not reappear on next launch). Validation - bun run validate-build passes end-to-end: lint + format + typecheck + full release .app bundle. - 721 existing Vitest tests still pass; 463 cargo tests still pass. Test coverage gap - Vitest tests for the new src/settings/* tree are NOT yet added in this commit. The shipped surface is large (10 components + 2 hooks + 3 tabs + SettingsWindow); covering it to 100% is genuinely a separate half-day of work and was deferred per the user request to land the feature in this worktree first. Backend coverage is at 100% on new code. Test coverage for the frontend is the only known gap before /ship. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * feat(settings): redesign panel to match chat overlay aesthetic Settings now feels native to Thuki instead of a generic macOS prefs clone. Visual vocabulary mirrors the chat overlay (App.css / .morphing-container): warm-orange surface-base + radial aura, top-edge glow hairline, AI-bubble border treatment (1px white-06% with warm top edge), 10px rounded card, native overlay scrollbar. Window chrome: decorations:false + transparent:true, custom in-app top bar via the shared WindowControls component (3 traffic lights, no Thuki Settings title, red x hides via getCurrentWindow().hide()). Resized 720x560 -> 580x720 for an elegant portrait shape. Top region has 8px breathing padding so the controls strip matches the chat view. Tab nav switched to a horizontal CodexBar-style icon row (icon above label, subtle dark inset + warm orange on active). All inputs, textareas, buttons, sliders, steppers, and pills restyled with the chat-bubble vocabulary so the two windows feel like one product. Drag from any non-interactive surface via handleDragStart mirroring App.tsx, walking up the DOM and bailing on form controls. Settings window added to capabilities/default.json with allow-hide, allow-start-dragging, and allow-set-focus so red x and drag actually work (previously scoped to main only). Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * feat(settings): polish panel UX and reach 100% coverage UI polish (single user-facing pass): - Tab restructure: 4 tabs (Model brain / Web globe / Display monitor / About info). Old "General" split into Model (model + ollama_url + prompt) and Display (window + quote). Web tab keeps search. - Per-section reset buttons removed. "Reset all to defaults" stays in About tab. - Settings tooltips now reuse the shared Tooltip component (with new optional `placement: 'top' | 'bottom'` prop) and pull help copy from a single source-of-truth map (`configHelpers.ts`) that mirrors `docs/configurations.md` row-for-row. The `?` info button is keyboard focusable and reveals the tooltip on focus. - About hero redesign: centered layout with Thuki logo, clickable version pill linking to GitHub release notes, two-line tagline ("A floating, local-first AI for macOS." / "No cloud. No clutter. Just answers."), three icon-only social buttons (GitHub / X / Feedback). Activation + Limits sections removed; permissions retain the row layout but lose the inter-row hairline. About tab no longer scrolls. - File row buttons unified to ghost style. "Reveal config.toml in Finder" → "Reveal Thuki app data". "Refresh from disk" → "Refresh config.toml" with explanatory tooltip placed above the trigger. - SavedPill now opaque so it doesn't bleed into rows. NumberStepper sits right-aligned at a sensible width instead of stretching. Save reliability fixes: - `useDebouncedSave` rewritten around a `lastSavedRef` value-equality gate plus an epoch counter. Eliminates the React StrictMode phantom save (pill appearing on tab switch with no edits) and the every-other-save miss after a successful save bumped `isInitialMountRef`. Adds `isMountedRef` guard on post-await setError, unmount-time flush of pending changes (so tab-switch mid-edit doesn't drop the user's last keystroke), and structural array equality for the model list. Drops unused `pending` field from the public handle. - `SaveField` helper prop type narrowed `ReactNode` → `string` to match the tooltip contract. Tests + coverage to 100%: - Backend: extracted `write_field_to_disk` and `reset_section_on_disk` helpers from the Tauri command wrappers so the allowlist guards, document patch, atomic write, and post-write reload are exercised without an `AppHandle`. Added tests for boolean/datetime/inline-table coercion branches, IO-error propagation under read-only parent dirs, and corrupt-marker write failure (squat the marker filename with a directory). Coverage-excluded the five thin Tauri command wrappers per project convention. Removed unreachable defensive branches in loader's `rename_corrupt` and the `Float` coercion path. - Frontend: new test files for `configHelpers`, `types`, `useDebouncedSave` (race + epoch + unmount-flush), `useConfigSync` (focus + reload + cleanup), every primitive in `components/index` (Section, SettingRow, TextField, Textarea, NumberSlider stepper + drag-aware sync, NumberStepper, Dropdown, OrderedListEditor, SavedPill, ConfirmDialog, ResetSectionLink), `SaveField` (resync token + errored prop forwarding), `SettingsWindow` (tab nav, corrupt banner, Cmd+,, drag, savedPill timer), and the four tab smokescreens. `main.tsx` factored to a pure `rootForLabel` helper for testability. - Mock additions: `__emitFocus` / `__resetFocusListeners` / `__setWindowLabel` on the tauri-window mock to drive the new hook + main entry tests. Configuration docs touched indirectly: tooltip strings in `configHelpers.ts` are kept in sync with `docs/configurations.md`; update both files in lockstep when adding a tunable. Validate-build clean (lint zero warnings, prettier clean, typecheck clean, frontend + backend build green). bun run test:all:coverage passes 100% lines / 100% functions / 100% branches / 100% statements on the frontend and 100% lines + 100% functions on the backend. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * feat(tray): drop "Reveal config.toml" item from tray menu The Settings → About tab already exposes "Reveal Thuki app data" with the same affordance, and the tray surface should stay focused on session-level actions (open / settings / quit). The `reveal_config_in_finder` Tauri command itself is unchanged so the About tab button keeps working. Tray menu is now: Open Thuki · Settings… · ─ · Quit Thuki. No tests had to be removed: the tray wiring was driven by an inline match in `lib.rs`'s `setup` closure, never directly tested. The `reveal_config_in_finder` command remains coverage-excluded as a thin FFI wrapper. Validate-build clean. bun run test:all:coverage passes 100% lines / 100% functions / 100% branches / 100% statements on the frontend and 100% lines + 100% functions on the backend. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * fix(settings): reconcile with main's model picker pipeline Adapts the Settings panel to the model-selection redesign that landed in main (#103). Active model state now lives in SQLite via ActiveModelState, not in a TOML field, so the Settings GUI no longer owns active-model selection: the in-app ModelPickerPanel does. Bug fix (models.rs) - All four model-picker Tauri commands took State<'_, AppConfig> but lib.rs registers RwLock<AppConfig> after the worktree's wrapping. Tauri State<T> looks up by type, so the lookup silently failed at runtime: the AskBar chip stayed hidden and the chat-header picker came up empty. Each handler now takes the RwLock-wrapped state and snapshots ollama_url via config.read() before any await. Settings panel reconciliation - Drop the "Active Ollama model" row from ModelTab. Capability moves to App.tsx's ModelPickerPanel; URL + system prompt remain in Settings. - Remove model.available from RawAppConfig (settings + ConfigContext), ALLOWED_FIELDS, configHelpers, all test fixtures. - Delete OrderedListEditor component and its tests (last caller gone). - Drop the now-unused .modelList/.modelItem*/.iconBtn/.modelAddRow CSS. - Update settings_commands tests: ALLOWED_FIELDS count 20 -> 19. - Replace stale config.model.active() assertion in config tests with a model.ollama_url check. Coverage maintenance - Add useModelCapabilities stale-resolution test to mirror the existing stale-rejection test (the isLatest short-circuit path was uncovered after the rebase exposed it). Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * refactor(settings): rename Model tab to AI, section to Ollama The Settings tab labelled "Model" no longer holds a model picker — that moved to the in-app ModelPickerPanel after main #103. Keeping the label misleads users into expecting a model list under it. Two cosmetic renames realign the surface with what it actually tunes: - Tab label "Model" -> "AI" (icon unchanged: brain still maps to both). - Section heading "Model" -> "Ollama". With only the Ollama URL field in this section now, "Ollama" is the honest name; "Model" implied a picker was missing. Pure UI rename: no schema, ALLOWED_FIELDS, or backend changes. Test strings + adjacent doc comments updated to match. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * feat(settings): auto-resize window to active tab content Replaces the fixed 680px Settings window height with a content-aware height that animates between tabs. AI hugs ~520, Display lands at ~720, Web at ~860 — light tabs no longer waste whitespace and heavy tabs no longer force unnecessary scrolling at the default size. Hook: useSettingsAutoResize(contentRef, chromeHeight) - ResizeObserver on the inner content wrapper measures natural scrollHeight whenever the active tab swaps or a textarea grows. - Target window height = content + chrome, clamped to [280, 900]. The body retains overflow-y: auto so content over MAX still scrolls internally rather than pushing the OS window off-screen. - First measurement snaps without animation (no visible settle on open). Subsequent changes interpolate via requestAnimationFrame with ease-out cubic over 220ms, issuing one Tauri setSize per frame so macOS animates the real NSWindow at 60Hz. - Sub-4px deltas are dropped to avoid ResizeObserver chatter. Wiring - SettingsWindow wraps tab panels in a measured div ref'd to the hook. - Chrome height = 148 baked-in (window padding + WindowControls + tab bar + body padding). When the corrupt-config recovery banner is visible, hook receives chrome + 56 so the window grows to fit it. - tauri.conf.json relaxes the settings window's min/max from 680/680/680 to 280/520/900 so the runtime resize is not capped by the configured bounds. Tests - New useSettingsAutoResize.test covers: snap on first observation, rAF interpolation, clamp at MIN and MAX, sub-4px no-op, mid-flight animation re-targeting, observer + rAF cleanup on unmount, no-op when ref is null, and chrome-height update reflected on next resize. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * fix(settings): force window remeasure on tab switch The auto-resize hook only updated the window height on its first mount because ResizeObserver does not fire when React swaps a tab's tree inside the same wrapper element within a single paint frame: the wrapper's box change happens between paints and is not always observed. Adds a `revision` parameter to `useSettingsAutoResize` (passed the active tab id from SettingsWindow) plus a `useLayoutEffect` that synchronously remeasures whenever revision changes. Layout-effect timing guarantees `scrollHeight` reflects the freshly-mounted tab's natural height, so the window animates to the correct target on every switch instead of staying frozen at the first-mount value. Test added for the revision-driven remeasure. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * fix(settings): re-attach auto-resize observer when content mounts The auto-resize hook ran its setup effect once on initial render and returned early because the wrapper element did not exist yet (the SettingsWindow tree gates rendering on \`if (!config) return null\`, so the wrapper is only added on the second render after \`get_config\` resolves). With a plain \`useRef\`, the dependency array stayed identity-stable across renders, so the effect never re-ran when the element finally appeared. Result: ResizeObserver was never attached and \`handleResizeRef\` stayed at its no-op default, so the revision-driven re-measure on tab switch did nothing. Switches to a state-backed callback ref pattern: SettingsWindow stores the wrapper in \`useState\`, the hook accepts the element directly as a parameter, and the setup effect depends on \`el\`. When the element mounts, the state update triggers a re-render, the effect re-runs with the real DOM node, and ResizeObserver attaches correctly. Subsequent tab switches drive the layout-effect re-measure which now reaches a real \`handleResize\` instead of the no-op stub. Tests rewritten around the new signature, with \`act()\` wrapping for state updates so the layout-effect re-measure is exercised correctly. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * refactor(settings): cap window height at 700px so tall tabs scroll The Web tab's full timeouts list pushes natural content past 1500px, which made the panel uncomfortably tall on smaller laptop displays. Drop the auto-resize ceiling from 900 to 700; tabs that exceed the cap continue scrolling inside .body via its existing overflow-y: auto. tauri.conf.json maxHeight matches. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * refactor(settings): drop About-tab special-case overflow override The About tab had its own .bodyNoScroll class (overflow: hidden, scrollbar-gutter: auto) so its content was clipped at the bottom when natural height exceeded the window cap, instead of scrolling like every other tab. Removes the special case so .body's default overflow-y: auto applies uniformly: every tab now scrolls when its content exceeds the cap. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * refactor(settings): drop scrollbar-gutter so short tabs fill width Removing \`scrollbar-gutter: stable\` lets the AI, Display, and About tabs use the full body width when their content fits under MAX_HEIGHT. The Web tab still scrolls when it overflows; macOS overlay scrollbars float over content on that tab without causing layout shift, so reserving a permanent gutter on every tab was just visual noise. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * fix(settings): add chrome-height slack so short tabs do not scroll \`.body\` uses \`overflow-y: auto\`, which paints a scrollbar whenever content is even 1px taller than the body box. The hand-measured chrome height (148) was a touch too tight, so the AI/Display/About tabs ended up with content marginally exceeding the body and a scroll affordance appearing on tabs that should fit cleanly. Bumps CHROME_HEIGHT to 164 to give the body 16px of slack. Short tabs now sit comfortably with no scrollbar; the Web tab still clamps at MAX_HEIGHT and scrolls because its natural height vastly exceeds the cap regardless of slack. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * fix(settings): measure chrome height dynamically The static chrome estimate was systematically off, so the body box ended up a few pixels shorter than the inner content even on tabs that should fit cleanly. Result: AI/Display/About each painted a scrollbar despite the cap not being hit. Wraps WindowControls + banner + tab bar in a measured container, runs a ResizeObserver on it, and uses its real `offsetHeight` plus two CSS-constant offsets (window padding-top 8 + body padding 42) as the chrome offset for the auto-resize hook. The ResizeObserver also catches the banner appearing or disappearing without needing a separate code path. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * refactor(settings): drop reserved scrollbar gutter on body \`scrollbar-gutter: stable\` reserved a vertical strip of empty space on every tab so content would not shift when the macOS overlay scrollbar faded in. The macOS overlay scrollbar already floats on top of content when it appears, so the gutter was just dead margin on the tabs that do not scroll (AI, Display, About). Drop it so short tabs render without any scrollbar artifact and Web still gets the floating scrollbar when its content exceeds the window cap. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * fix(settings): only enable body scroll when content was clamped Previously \`.body\` had \`overflow-y: auto\` unconditionally, so any sub-pixel rounding mismatch between the auto-resize hook's chrome estimate and the real layout produced a stray scrollbar even on short tabs (AI, Display, About) whose content fit inside the auto-sized window. Result: every tab looked scrollable. useSettingsAutoResize now returns an \`isClamped\` boolean that flips true when natural content + chrome exceeded MAX_HEIGHT. SettingsWindow applies a \`bodyScrollable\` class only in that case; the default \`.body\` style is \`overflow: hidden\` so short tabs render with no scrollbar artifact at all. Web (the only tab past the cap) still gets internal scrolling because it trips the clamp branch. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * fix(settings): rename remaining [model] section refs to [inference] in coverage-only tests Post-rebase cleanup. Three .model.ollama_url field accesses and seven "model" string literals in settings_commands::tests and config::tests were only exercised when the cfg(coverage) build drops the #[cfg_attr(coverage_nightly, coverage(off))] gate on the surrounding Tauri-command wrappers, so the regular cargo test passed but test:all:coverage failed to compile. Update them to match main's [model] -> [inference] section rename from #106. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * refactor: remove collapsed height and hide commit delay from window configuration - Removed `collapsed_height` and `hide_commit_delay_ms` from the `WindowSection` struct and related configurations. - Updated the default values and tests to reflect the removal of these properties. - Adjusted the frontend to use fixed values for collapsed height and hide commit delay, ensuring consistent behavior across the application. - Emitted configuration updates to the frontend when changes are made to other window properties. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * fix(settings): add bottom padding to About tab and Cmd+W to hide panel About tab was visually clipped at the bottom because .aboutBody had no padding-bottom, causing the auto-resize window to size exactly to the content edge with no breathing room. Adding 8px padding-bottom matches the spacing visible on other tabs. Cmd+W now hides the settings window to follow the standard macOS panel close shortcut. The existing Cmd+, re-focus handler lives in the same useEffect; both shortcuts share one document listener. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * fix(settings): patch_document insert-on-missing, RMW lock, stale doc comment Three fixes to settings_commands.rs: 1. patch_document now inserts a new TOML item with inferred type when the key is absent (e.g. user hand-edited it out), instead of returning UnknownField. Type inference: bool, integer, float, string, array-of-strings. Objects and null still return TypeMismatch. Adds json_value_to_toml_item helper for the missing-key path. 2. Hold the RwLock write guard across the full disk read-modify-write in set_config_field, reset_config, and reload_config_from_disk. Prevents a TOCTOU race where two concurrent invocations could read stale file content and last-write-wins on a different field. 3. Correct stale doc comment: State<AppConfig> -> State<RwLock<AppConfig>>. Adds 7 new tests covering insert-on-missing paths (float, string, integer, array, object rejection, array-non-string rejection, bool). Removes the now-incorrect patch_document_unknown_field_errors test (allowlist enforcement is upstream in write_field_to_disk, covered by existing test). Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * fix: tighten comment on u64 precision edge case in json_value_to_toml_item Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * fix(config): update AppConfig doc to reflect RwLock<AppConfig> managed state Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * docs(configurations): add baked-in rows for COLLAPSED_WINDOW_HEIGHT and HIDE_COMMIT_DELAY_MS Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * style: apply cargo fmt to settings_commands Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> --------- Signed-off-by: Logan Nguyen <lg.131.dev@gmail.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.
Summary
DEFAULT_MODEL_NAME("gemma4:e2b") fallback that silently downgraded existing users to the small 2B model on first launch of the in-app picker (regression introduced in feat: in-app model picker with hardened selection pipeline #103).Option<String>in Rust state andstring | nullin TS. Ollama's/api/tagsis the single source of truth. No fabricated defaults.[model]config section to[inference]sinceollama_urlis the only field left and the section now describes the inference daemon, not the model. Hard rename, no shim (PR feat: in-app model picker with hardened selection pipeline #103 has not shipped to users yet, so no migration needed).OllamaErrorKind::NoModelSelectedand routes a structured error throughask_ollamaandsearch_pipelinewhen no model is selected.CapabilityMismatchStripextended with a no-model message; the picker chip stays visible (label "Pick a model" when null) as the recovery affordance; submit is gated through the existing ask-bar shake animation.useConversationHistory.save()rejects when no model is selected, centralizing the guard.Why
Pre-#103, the active model came from
config.model.available.first(). Post-#103, it came from a SQLiteapp_config.active_modelrow. Existing users upgrading to #103 had no SQLite row populated, soresolve_seed_active_modelfell back toDEFAULT_MODEL_NAME = "gemma4:e2b". Anyone whose pre-#103 first available was a larger model (gemma4:e4b, llama3.1:8b, etc.) got silently downgraded to the small 2B model until they manually opened the picker. This explains the user-reported drastic quality drop on single-model conversations after #103.The fix is structural, not a patch: by removing the fallback string entirely and treating "no model" as a real first-class state, the silent-downgrade bug cannot exist by construction. Empty Ollama → empty UI affordance, no fabrication.
Test plan
bun run test:all:coveragepasses (534 backend tests, 898 frontend tests, 100% line/function/branch/statement coverage on both sides).bun run validate-buildclean (lint, format, typecheck, frontend build, backend release build, app bundle). Zero warnings.config.tomlto use[inference] ollama_url = "..."and confirm it loads; confirm any leftover[model]section is ignored and falls back to defaults.Notes
[model] ollama_urlmust rename the section to[inference]. CHANGELOG entry added under Unreleased.