Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/data-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Three directories, different owners:

## Entities

- **Project** (`projects.json`, single JSON array) — git repo path + scripts + `preview_url` template + `files_to_copy` globs + `default_cli`.
- **Project** (`projects.json`, single JSON array) — git repo path + scripts + `preview_url` template + `files_to_copy` globs + `default_cli` + optional `group` label (UI-only collapsible folder in the sidebar; no filesystem effect; a group exists iff ≥1 project carries the label. All group reads go through `groupOf()` in `src/lib/projectGroups.ts`, THE normalization point: trim + ALL-CAPS, so mixed-case labels on disk converge to one group. Collapse state + folder color live in `localStorage` keyed by normalized name, pruned when a group disappears).
- **Task** (`tasks/<uuid>.json`) — git worktree branched from project's `base_branch`. Worktrees live at `~/termic/tasks/<project>/<name>/`. `is_main_checkout=true` tasks point at the project's live checkout (no worktree, archive skips `rm -rf`).
- **Settings** (`settings.json`) — `repos_dir`, `welcomed`, `agents[]` (claude/gemini/codex defaults + customs; each has `command`/`args`/`yolo_args`/`runtime_yolo_command`). Defaults seeded if `agents` is empty. `schema_version` gates one-time on-disk migrations.
- **Tab** (per task, in `useApp`) — `terminal` (PTY running a CLI), `edit` (CodeMirror), `diff` (vs HEAD). PTYs die with the app.
Expand Down
32 changes: 31 additions & 1 deletion src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,13 @@ pub struct Project {
/// existing project + the `Default` impl stay git-backed.
#[serde(default)]
pub non_git: bool,

/// UI-only sidebar group label. Projects sharing the same non-empty
/// value render under one collapsible folder header in the project
/// list. Purely presentational — no effect on paths, git, or
/// workspaces. `None` / empty = ungrouped (renders at the top level).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub group: Option<String>,
}

#[derive(Clone, Debug, Serialize, Deserialize, Default)]
Expand Down Expand Up @@ -1725,6 +1732,8 @@ fn project_add(root_path: String, non_git: Option<bool>) -> Result<Project, Stri
members: Vec::new(),
spotlight_enabled: false,
non_git,
// New projects start ungrouped; grouping is a sidebar action.
group: None,
};
list.push(p.clone());
save_projects(&list).map_err(|e| e.to_string())?;
Expand Down Expand Up @@ -1900,6 +1909,7 @@ fn project_add_multi(root_path: String, name: String, members: Vec<ProjectMember
members,
spotlight_enabled: false,
non_git,
group: None,
};
list.push(p.clone());
save_projects(&list).map_err(|e| e.to_string())?;
Expand Down Expand Up @@ -1962,6 +1972,26 @@ fn project_reorder(ids: Vec<String>) -> Result<(), String> {
save_projects(&out).map_err(|e| e.to_string())
}

/// Set / clear the UI-only sidebar group label on a batch of projects in
/// ONE atomic projects.json write. Group rename / dissolve touch every
/// member — doing this per-project from the frontend could fail halfway
/// and leave a group half-renamed on disk, and full `project_update`s
/// would clobber concurrent edits to unrelated fields.
#[tauri::command]
fn project_set_group(ids: Vec<String>, group: Option<String>) -> Result<(), String> {
let group = group.and_then(|g| {
let t = g.trim().to_string();
if t.is_empty() { None } else { Some(t) }
});
let mut list = load_projects();
for p in list.iter_mut() {
if ids.contains(&p.id) {
p.group = group.clone();
}
}
save_projects(&list).map_err(|e| e.to_string())
}

#[tauri::command]
fn project_update(p: Project) -> Result<(), String> {
let mut list = load_projects();
Expand Down Expand Up @@ -8247,7 +8277,7 @@ pub fn run() {
Ok(())
})
.invoke_handler(tauri::generate_handler![
projects_list, project_add, project_add_multi, project_set_members, project_update, project_remove, project_reorder,
projects_list, project_add, project_add_multi, project_set_members, project_update, project_remove, project_reorder, project_set_group,
tasks_list, task_create, task_create_multi, task_open_repo, task_importable_worktrees, task_import_worktree, task_archive, task_set_cli, task_set_custom_command, task_set_resume_override, task_set_sandbox, task_set_yolo,
sandbox_available, sandbox_deny_counts, sandbox_recent_denied_hosts, sandbox_recent_denied_paths, sandbox_access_counts, sandbox_recent_access_hosts, sandbox_recent_access_paths, sandbox_set_monitor_filters, task_sandbox_add_allowed_host, task_sandbox_add_allowed_path, task_sandbox_remove_allowed_path, agent_sandbox_add_allowed_path, agent_sandbox_add_allowed_host, task_recent_denials,
repo_config_load, repo_config_load_at, repo_config_save, repo_config_scaffold, repo_config_add_allowed_host, repo_config_add_allowed_path,
Expand Down
Loading
Loading