feat: add project concept for grouping generations (#65)#140
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #140 +/- ##
=======================================
- Coverage 72.1% 70.1% -2.0%
=======================================
Files 61 63 +2
Lines 2392 2482 +90
Branches 710 729 +19
=======================================
+ Hits 1725 1742 +17
- Misses 476 544 +68
- Partials 191 196 +5
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 a "projects" concept that lets users group generations into named buckets, exposed through a new SQLite table, Tauri commands, CLI subcommand (
Confidence Score: 4/5Safe to merge with one fix needed: the database connection must enable SQLite foreign key enforcement or the ON DELETE SET NULL cascade will never run. The src-tauri/src/services/db.rs — the Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant UI as Frontend UI
participant Store as Zustand Store
participant Tauri as Tauri Commands
participant DB as SQLite DB
Note over UI,DB: Create Project
UI->>Store: createProject(name)
Store->>Tauri: create_project
Tauri->>DB: INSERT INTO projects
DB-->>Store: Project
Store->>Store: prepend to projects[]
Note over UI,DB: Delete Project
UI->>Store: deleteProject(id)
Store->>Tauri: delete_project(id)
Tauri->>DB: "DELETE FROM projects WHERE id=?"
DB-->>DB: "ON DELETE SET NULL (requires PRAGMA foreign_keys=ON)"
Store->>Store: clear projectId in history[], reset activeProjectId
Note over UI,DB: Filter History by Project
UI->>Store: setActiveProject(id)
Store->>UI: filteredHistory (client-side filter by projectId)
%%{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 Frontend UI
participant Store as Zustand Store
participant Tauri as Tauri Commands
participant DB as SQLite DB
Note over UI,DB: Create Project
UI->>Store: createProject(name)
Store->>Tauri: create_project
Tauri->>DB: INSERT INTO projects
DB-->>Store: Project
Store->>Store: prepend to projects[]
Note over UI,DB: Delete Project
UI->>Store: deleteProject(id)
Store->>Tauri: delete_project(id)
Tauri->>DB: "DELETE FROM projects WHERE id=?"
DB-->>DB: "ON DELETE SET NULL (requires PRAGMA foreign_keys=ON)"
Store->>Store: clear projectId in history[], reset activeProjectId
Note over UI,DB: Filter History by Project
UI->>Store: setActiveProject(id)
Store->>UI: filteredHistory (client-side filter by projectId)
|
- rename_project preserves original created_at instead of overwriting - delete confirmation uses trim() for cross-platform newline handling - delete confirmation shows project name instead of UUID - extract resolve_project_by_prefix to shared cli::mod (deduplicate) - add find_generation_ids_by_prefix DB query for O(log n) prefix lookup Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Add a projects table (migration 006) with CRUD operations, CLI `project` subcommand, `--project` flag on `run` and `list`, Tauri commands, frontend store slice, ProjectSelector sidebar component, and per-item project assignment dropdown. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- rename_project preserves original created_at instead of overwriting - delete confirmation uses trim() for cross-platform newline handling - delete confirmation shows project name instead of UUID - extract resolve_project_by_prefix to shared cli::mod (deduplicate) - add find_generation_ids_by_prefix DB query for O(log n) prefix lookup Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The unassign and assign onClick handlers fired assignGenerationToProject without awaiting or catching, so a backend failure produced an unhandled promise rejection and the UI silently kept the stale project association. Attach .catch handlers that log the failure, matching the existing warn-on-error pattern in this file. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Pick up the auto-format changes from the Dependabot merges and apply rustfmt/oxfmt to the new project CLI and store slice code. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
c22c70e to
2422033
Compare
| let name = if project.name.len() > 26 { | ||
| format!("{}…", &project.name[..25]) |
There was a problem hiding this comment.
🔴 Truncating project names with non-ASCII characters crashes the CLI
A project name containing multi-byte characters (e.g., Chinese or emoji) is sliced at a raw byte offset (&project.name[..25] at src-tauri/src/cli/project.rs:38) rather than at a character boundary, so listing projects with long non-ASCII names causes a panic.
Impact: The openloop project list command crashes when any project has a long name containing multi-byte UTF-8 characters.
Byte-index slicing on user-provided UTF-8 strings
project.name.len() returns the byte length, and &project.name[..25] indexes into the string at byte position 25. For multi-byte UTF-8 characters (Chinese characters are 3 bytes, emoji are 4 bytes), byte position 25 can land in the middle of a character. Rust panics with byte index 25 is not a char boundary when this happens.
The app ships with a Chinese (zh-CN) locale (src/locales/zh-CN.json), so users are likely to create projects with Chinese names. A name like "这是一个很长的项目名称用来测试" (30 bytes for 10 characters) would trigger this crash.
The same pattern exists pre-PR for record.prompt[..21] at src-tauri/src/cli/list.rs:42, but that is pre-existing code not introduced by this PR.
| let name = if project.name.len() > 26 { | |
| format!("{}…", &project.name[..25]) | |
| let name = if project.name.chars().count() > 26 { | |
| format!("{}…", project.name.chars().take(25).collect::<String>()) |
Was this helpful? React with 👍 or 👎 to provide feedback.
| CREATE INDEX IF NOT EXISTS idx_projects_name ON projects(name); | ||
| CREATE INDEX IF NOT EXISTS idx_projects_created_at ON projects(created_at DESC); | ||
|
|
||
| ALTER TABLE generations ADD COLUMN project_id TEXT REFERENCES projects(id) ON DELETE SET NULL; |
There was a problem hiding this comment.
🔍 SQLite foreign key ON DELETE SET NULL may not be enforced without PRAGMA
The migration at src-tauri/migrations/006_add_projects.sql:11 defines REFERENCES projects(id) ON DELETE SET NULL, and the test delete_project_unassigns_generations_not_deletes_them at src-tauri/src/services/db.rs:1036 asserts that deleting a project nulls out project_id on associated generations. However, SQLite does not enforce foreign key constraints by default — it requires PRAGMA foreign_keys = ON on each connection. No such pragma is set anywhere in the codebase (searched for foreign_keys with zero results). If the pragma is not enabled, deleting a project will leave orphaned project_id values in the generations table rather than setting them to NULL. The test may pass in certain SQLite builds or test environments but could fail or behave incorrectly in production. The Database::connection() method at src-tauri/src/services/db.rs:37 would be the right place to add this pragma.
Was this helpful? React with 👍 or 👎 to provide feedback.
Summary
projectstable (migration 006) withproject_idFK ongenerations(ON DELETE SET NULL)list_projects,create_project,rename_project,delete_project,set_generation_project,list_generations_by_projectProject/CreateProjectRequest/RenameProjectRequestmodelslist_projects,create_project,rename_project,delete_project,assign_generation_to_projectprojectsubcommand (list, create, rename, delete, assign) with prefix/name resolution--projectflag torun(auto-creates project by name) andlist(filter by project)Projecttype, API functions, projects store sliceProjectSelectorcomponent in history sidebar with create/delete/filterprojects.*in en.json and zh-CN.jsonTest plan
cargo test— 20 cli_contract + 154 lib tests pass (3 new project tests)npx tsc --noEmit— no type errorsnpx vitest run— 957 tests pass--project, verify assignmentGenerated with Devin