feat: add generation profile management (#71)#141
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #141 +/- ##
=======================================
- Coverage 70.2% 68.3% -1.9%
=======================================
Files 69 71 +2
Lines 2496 2594 +98
Branches 732 766 +34
=======================================
+ Hits 1754 1774 +20
- Misses 545 621 +76
- Partials 197 199 +2
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
Greptile SummaryThis PR adds full generation profile management — named presets of form defaults that users can save, apply, rename, and delete — spanning a new SQLite migration, Rust DB/command layer, Tauri IPC bindings, a Zustand store slice, a settings UI panel, and a CLI subcommand.
Confidence Score: 5/5Safe to merge; all CRUD paths are correctly guarded and the migration is properly sequenced. The new DB layer handles not_found and write errors correctly, the Tauri command signatures match their frontend callers, and the previously flagged numeric-coercion issues in the store slice have been addressed. The remaining findings are UX polish rather than data-loss or correctness bugs. No files require special attention for correctness; db.rs and ProfilesSection.tsx have the two UX-level observations noted in the comments. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant UI as ProfilesSection (React)
participant Store as Zustand Store
participant API as api.ts (Tauri invoke)
participant Cmd as commands/profiles.rs
participant DB as services/db.rs
participant SQLite
Note over UI,SQLite: Hydration
Store->>API: listProfiles()
API->>Cmd: list_profiles
Cmd->>DB: list_profiles()
DB->>SQLite: SELECT … ORDER BY updated_at DESC
SQLite-->>DB: rows
DB-->>Cmd: "Vec<GenerationProfile>"
Cmd-->>API: JSON
API-->>Store: profiles[]
Store-->>UI: re-render
Note over UI,SQLite: Create
UI->>Store: createProfile(name, form)
Store->>API: createProfile(request)
API->>Cmd: create_profile
Cmd->>DB: "create_profile(&request)"
DB->>SQLite: INSERT INTO generation_profiles
SQLite-->>DB: ok
DB-->>Cmd: GenerationProfile
Cmd-->>Store: profile
Store->>Store: prepend to profiles[]
Note over UI,SQLite: Apply
UI->>Store: applyProfile(id)
Store->>Store: merge profile fields onto form (null-safe)
Store->>Store: computeValidationState(form)
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant UI as ProfilesSection (React)
participant Store as Zustand Store
participant API as api.ts (Tauri invoke)
participant Cmd as commands/profiles.rs
participant DB as services/db.rs
participant SQLite
Note over UI,SQLite: Hydration
Store->>API: listProfiles()
API->>Cmd: list_profiles
Cmd->>DB: list_profiles()
DB->>SQLite: SELECT … ORDER BY updated_at DESC
SQLite-->>DB: rows
DB-->>Cmd: "Vec<GenerationProfile>"
Cmd-->>API: JSON
API-->>Store: profiles[]
Store-->>UI: re-render
Note over UI,SQLite: Create
UI->>Store: createProfile(name, form)
Store->>API: createProfile(request)
API->>Cmd: create_profile
Cmd->>DB: "create_profile(&request)"
DB->>SQLite: INSERT INTO generation_profiles
SQLite-->>DB: ok
DB-->>Cmd: GenerationProfile
Cmd-->>Store: profile
Store->>Store: prepend to profiles[]
Note over UI,SQLite: Apply
UI->>Store: applyProfile(id)
Store->>Store: merge profile fields onto form (null-safe)
Store->>Store: computeValidationState(form)
Reviews (2): Last reviewed commit: "fix: address PR #141 review comments" | Re-trigger Greptile |
- profiles.ts: stop coercing numeric 0 to null in createProfile (use isNaN guards) and applyProfile (use != null checks) so explicit zero values for guidanceScale/inferenceSteps/durationSeconds/bpm are preserved end-to-end. - migrations: rename 007_add_profiles.sql to 006_add_profiles.sql to close the sequence gap with 005_history_indexes.sql. - cli/profiles.rs: skip the [y/N] confirmation prompt when --json is set so scripted deletes do not block on terminal input. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- profiles.ts: stop coercing numeric 0 to null in createProfile (use isNaN guards) and applyProfile (use != null checks) so explicit zero values for guidanceScale/inferenceSteps/durationSeconds/bpm are preserved end-to-end. - migrations: rename 007_add_profiles.sql to 006_add_profiles.sql to close the sequence gap with 005_history_indexes.sql. - cli/profiles.rs: skip the [y/N] confirmation prompt when --json is set so scripted deletes do not block on terminal input. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
13fbda0 to
d164d94
Compare
There was a problem hiding this comment.
🔍 Profiles section missing from settings navigation bar
The new ProfilesSection component is rendered in the settings overlay (src/app/components/settings/SettingsOverlay.tsx:148) but no corresponding entry was added to the sectionNav array at src/app/components/settings/SettingsOverlay.tsx:86-94. Additionally, the ProfilesSection doesn't pass an id prop to SettingsSectionCard (src/app/components/settings/sections/ProfilesSection.tsx:56), so even if a nav entry were added, the scroll-to-section behavior (document.getElementById('settings-section-...')) wouldn't work. All other sections in the settings overlay have both a nav entry and an id prop. This means users cannot use the quick-jump navigation to reach the profiles section — they must scroll manually.
(Refers to lines 86-94)
Was this helpful? React with 👍 or 👎 to provide feedback.
| let name = if profile.name.len() > 18 { | ||
| format!("{}…", &profile.name[..17]) |
There was a problem hiding this comment.
🔴 Profile name truncation crashes on non-ASCII characters
A profile name containing multi-byte characters (e.g., Chinese) is sliced by raw byte index (&profile.name[..17] at src-tauri/src/cli/profiles.rs:68) instead of by character boundary, so listing profiles with such names panics the CLI.
Impact: The openloop profiles list command crashes when any profile has a non-ASCII name longer than 18 bytes.
Byte-index slicing on UTF-8 strings causes a panic at a non-char-boundary
At src-tauri/src/cli/profiles.rs:67, profile.name.len() returns the byte length, and at line 68, &profile.name[..17] slices by byte offset. For a Chinese name like "我的音乐生成配置档名称" (each character is 3 UTF-8 bytes), byte index 17 falls in the middle of a character, triggering a panic: byte index 17 is not a char boundary. The app ships with zh-CN locale support, making this a realistic scenario.
The fix should use char_indices() or .chars().take(17) to truncate by character count rather than byte offset.
| let name = if profile.name.len() > 18 { | |
| format!("{}…", &profile.name[..17]) | |
| let name = if profile.name.chars().count() > 18 { | |
| let truncated: String = profile.name.chars().take(17).collect(); | |
| format!("{truncated}…") |
Was this helpful? React with 👍 or 👎 to provide feedback.
Add generation_profiles table (migration 007) with CRUD operations, CLI `profiles` subcommand (list, create, rename, delete), Tauri commands, frontend store slice, ProfilesSection settings UI with create/rename/delete/apply, and i18n keys. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- profiles.ts: stop coercing numeric 0 to null in createProfile (use isNaN guards) and applyProfile (use != null checks) so explicit zero values for guidanceScale/inferenceSteps/durationSeconds/bpm are preserved end-to-end. - migrations: rename 007_add_profiles.sql to 006_add_profiles.sql to close the sequence gap with 005_history_indexes.sql. - cli/profiles.rs: skip the [y/N] confirmation prompt when --json is set so scripted deletes do not block on terminal input. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Add #[allow(clippy::large_enum_variant)] on ProfileCommand — this is a CLI arg enum instantiated once per invocation, so the 256-byte Create variant size difference is irrelevant. Apply cargo fmt and oxfmt to pick up formatting from the Dependabot rebase. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
d164d94 to
070e0cb
Compare
| fn cmd_delete(state: &AppState, json: bool, id: &str, yes: bool) -> AppResult<()> { | ||
| let resolved = resolve_profile_by_prefix(&state.db, id)?; | ||
|
|
||
| if !yes && !json { |
There was a problem hiding this comment.
🟡 Profile deletion skips safety confirmation when JSON output is requested, unlike project deletion
The delete-confirmation prompt is skipped when --json is passed (!yes && !json at src-tauri/src/cli/profiles.rs:161), so a scripted call with --json but without --yes silently deletes the profile.
Impact: Users running the CLI with --json can accidentally delete profiles without being asked to confirm.
Inconsistency with the project delete command
The analogous project delete at src-tauri/src/cli/project.rs:84 uses if !yes { — it always prompts unless --yes is explicitly passed, regardless of --json. The profile delete adds an extra && !json condition, meaning --json alone suppresses the confirmation. This is inconsistent and could cause unintended data loss when a user passes --json for output formatting without intending to skip confirmation.
| if !yes && !json { | |
| if !yes { |
Was this helpful? React with 👍 or 👎 to provide feedback.
| let name = if profile.name.len() > 18 { | ||
| format!("{}…", &profile.name[..17]) |
There was a problem hiding this comment.
🔍 Pre-existing byte-index string slicing pattern is replicated from project/list modules
The byte-index slicing pattern (&name[..N]) that causes the reported UTF-8 panic bug in profiles.rs also exists in src-tauri/src/cli/project.rs:38 (&project.name[..25]) and src-tauri/src/cli/list.rs:42 (&record.prompt[..21]). These pre-existing instances have the same crash risk with multi-byte characters. Since the app ships with Chinese locale support, these should also be fixed to use .chars().take(N).collect::<String>() or a similar char-boundary-safe approach.
Was this helpful? React with 👍 or 👎 to provide feedback.
Summary
generation_profilestable (migration 007) with name, model variant, inference steps, guidance scale, and other generation defaultslist_profiles,create_profile,rename_profile,delete_profileGenerationProfile,CreateProfileRequest,RenameProfileRequestmodelslist_profiles,create_profile,rename_profile,delete_profileprofilessubcommand (list, create, rename, delete) with prefix/name resolution and confirmationGenerationProfiletype, API functions, profiles store sliceProfilesSectionsettings UI component with create/rename/delete/applyprofiles.*in en.json and zh-CN.jsonTest plan
cargo test— 152 lib tests + 20 cli_contract tests pass (1 new profile test)npx tsc --noEmit— no type errorsnpx vitest run— 958/959 tests pass (1 flaky timeout on 1000-record filter test, passes individually)openloop profiles list/create/rename/deleteGenerated with Devin