Skip to content

feat: add generation profile management (#71)#141

Merged
thedavidweng merged 3 commits into
mainfrom
feat/issue-71-profiles
Jul 8, 2026
Merged

feat: add generation profile management (#71)#141
thedavidweng merged 3 commits into
mainfrom
feat/issue-71-profiles

Conversation

@thedavidweng

@thedavidweng thedavidweng commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add generation_profiles table (migration 007) with name, model variant, inference steps, guidance scale, and other generation defaults
  • Add backend DB methods: list_profiles, create_profile, rename_profile, delete_profile
  • Add GenerationProfile, CreateProfileRequest, RenameProfileRequest models
  • Add Tauri commands: list_profiles, create_profile, rename_profile, delete_profile
  • Add CLI profiles subcommand (list, create, rename, delete) with prefix/name resolution and confirmation
  • Add frontend GenerationProfile type, API functions, profiles store slice
  • Add ProfilesSection settings UI component with create/rename/delete/apply
  • Fetch profiles during hydration
  • Add i18n keys for profiles.* in en.json and zh-CN.json
  • 1 new backend test covering profile CRUD end-to-end

Test plan

  • cargo test — 152 lib tests + 20 cli_contract tests pass (1 new profile test)
  • npx tsc --noEmit — no type errors
  • npx vitest run — 958/959 tests pass (1 flaky timeout on 1000-record filter test, passes individually)
  • Manual: create profile from current form, apply it, verify form updates
  • Manual: CLI openloop profiles list/create/rename/delete

Generated with Devin


Open in Devin Review

@codecov

codecov Bot commented Jul 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 20.40816% with 78 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.3%. Comparing base (6ea217a) to head (070e0cb).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/app/lib/store/slices/profiles.ts 8.8% 40 Missing and 1 partial ⚠️
...p/components/settings/sections/ProfilesSection.tsx 31.2% 32 Missing and 1 partial ⚠️
src/app/lib/api.ts 0.0% 4 Missing ⚠️
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     
Flag Coverage Δ
frontend 68.3% <20.4%> (-1.9%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
frontend 68.3% <20.4%> (-1.9%) ⬇️
rust ∅ <ø> (∅)
Files with missing lines Coverage Δ
src/app/components/settings/SettingsOverlay.tsx 68.6% <ø> (ø)
src/app/lib/store/slices/settings.ts 100.0% <100.0%> (ø)
src/app/lib/api.ts 68.4% <0.0%> (-3.9%) ⬇️
...p/components/settings/sections/ProfilesSection.tsx 31.2% <31.2%> (ø)
src/app/lib/store/slices/profiles.ts 8.8% <8.8%> (ø)
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@greptile-apps

greptile-apps Bot commented Jul 5, 2026

Copy link
Copy Markdown

Greptile Summary

This 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.

  • Backend: Migration 006_add_profiles.sql creates generation_profiles with all expected columns; db.rs adds four clean CRUD methods with correct not_found guards and proper thinking bool↔int mapping; Tauri commands and CLI subcommand with prefix resolution are wired up correctly.
  • Frontend: ProfilesSlice builds on the existing Zustand pattern; applyProfile was corrected to use != null guards for numeric fields; the ProfilesSection UI supports create/rename/delete/apply with i18n coverage in both en.json and zh-CN.json.
  • Tests: One new backend CRUD test and updated frontend unit tests cover the new slice and hydration path.

Confidence Score: 5/5

Safe 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

Filename Overview
src-tauri/migrations/006_add_profiles.sql New migration creating generation_profiles table with all expected columns and a name index; CREATE TABLE IF NOT EXISTS makes it idempotent.
src-tauri/src/services/db.rs Adds list_profiles, create_profile, rename_profile, delete_profile DB methods; positional column mapping and not_found guards are correct, but UNIQUE constraint violations are mapped to a generic write-failed error that reaches the frontend without a useful message.
src-tauri/src/cli/profiles.rs New CLI profiles subcommand implementing list/create/rename/delete with prefix resolution; logic is clean and errors are properly propagated.
src-tauri/src/commands/profiles.rs Thin Tauri command wrappers delegating directly to DB methods; registered correctly in lib.rs.
src/app/lib/store/slices/profiles.ts New Zustand slice for profile CRUD; applyProfile correctly uses != null guards for numeric fields; createProfile has minor inconsistency with bpm not using isNaN guard (though JSON serialization makes it safe in practice).
src/app/components/settings/sections/ProfilesSection.tsx New settings UI for profile management; create/rename flows are solid, but the trash-icon delete fires immediately without any confirmation, risking accidental profile loss.
src/app/lib/api.ts Four new Tauri invoke wrappers for profile CRUD; argument shapes match the Rust command signatures.
src/app/lib/types.ts Adds GenerationProfile and CreateProfileRequest TypeScript types; all optional fields correctly typed as ?T
src/app/lib/store/slices/settings.ts Adds refreshProfiles() call during hydration so profiles are available on app start.

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)
Loading
%%{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)
Loading

Reviews (2): Last reviewed commit: "fix: address PR #141 review comments" | Re-trigger Greptile

greptile-apps[bot]

This comment was marked as resolved.

thedavidweng added a commit that referenced this pull request Jul 5, 2026
- 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>
thedavidweng added a commit that referenced this pull request Jul 8, 2026
- 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>
@thedavidweng thedavidweng force-pushed the feat/issue-71-profiles branch from 13fbda0 to d164d94 Compare July 8, 2026 04:52

@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 2 potential issues.

Open in Devin Review

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.

🔍 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)

Open in Devin Review

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

Comment on lines +67 to +68
let name = if profile.name.len() > 18 {
format!("{}…", &profile.name[..17])

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.

🔴 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.

Suggested change
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}…")
Open in Devin Review

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

thedavidweng and others added 3 commits July 7, 2026 22:02
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>
@thedavidweng thedavidweng force-pushed the feat/issue-71-profiles branch from d164d94 to 070e0cb Compare July 8, 2026 05:04

@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 2 new potential issues.

Open in Devin Review

fn cmd_delete(state: &AppState, json: bool, id: &str, yes: bool) -> AppResult<()> {
let resolved = resolve_profile_by_prefix(&state.db, id)?;

if !yes && !json {

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.

🟡 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.

Suggested change
if !yes && !json {
if !yes {
Open in Devin Review

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

Comment on lines +67 to +68
let name = if profile.name.len() > 18 {
format!("{}…", &profile.name[..17])

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.

🔍 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.

Open in Devin Review

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

@thedavidweng thedavidweng merged commit c29507d into main Jul 8, 2026
13 of 14 checks passed
@thedavidweng thedavidweng deleted the feat/issue-71-profiles branch July 8, 2026 05:20
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