diff --git a/CHANGELOG.md b/CHANGELOG.md index 98fbee9..847b015 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ Notable changes to Agentaps are recorded here. ### Fixed +- Keep folders visible after their last session is archived and allow projects, including empty folders, to be archived and restored independently. + - The website header shows the latest release and star count for `cachix/agentaps` in compact dark badges linking to the GitHub release and repository. ### Changed diff --git a/src/app.rs b/src/app.rs index a1ce9df..9f0af47 100644 --- a/src/app.rs +++ b/src/app.rs @@ -358,6 +358,7 @@ fn completed_file_text( } struct ProjectView { + archived: bool, path: PathBuf, ssh_host: Option, branch: String, @@ -982,6 +983,7 @@ impl Workspace { .projects .into_iter() .map(|project| ProjectView { + archived: project.archived, branch: project .ssh_host .clone() @@ -1228,11 +1230,9 @@ impl Workspace { .composer .update(cx, |input, cx| input.focus(window, cx)); } else { - let view = if this - .projects - .iter() - .any(|project| project.agents.iter().any(|agent| agent.config.archived)) - { + let view = if this.projects.iter().any(|project| { + project.archived || project.agents.iter().any(|agent| agent.config.archived) + }) { WorkspaceView::Empty } else if !this.projects.is_empty() { WorkspaceView::NewSession { @@ -1308,6 +1308,7 @@ impl Workspace { .map(|index| { let project = &self.projects[index]; ProjectConfig { + archived: project.archived, path: project.path.clone(), ssh_host: project.ssh_host.clone(), agents: project.agents.iter().map(AgentView::snapshot).collect(), @@ -1678,6 +1679,7 @@ impl Workspace { index } else { self.projects.push(ProjectView { + archived: false, branch: ssh_host.clone().unwrap_or_else(|| branch(&path)), sync_counts: None, path: path.clone(), @@ -1763,6 +1765,7 @@ impl Workspace { terminal: bool, cx: &mut Context, ) -> (u64, usize) { + self.projects[project_index].archived = false; let id = self.next_agent_id; let config = AgentConfig { id, @@ -1824,6 +1827,7 @@ impl Workspace { window: &mut Window, cx: &mut Context, ) { + self.projects[project_index].archived = true; // Archive the displayed session last so its replacement is outside this project. let current = self .view @@ -1838,6 +1842,14 @@ impl Workspace { if let Some(session) = current { self.set_archived(project_index, session.agent_index, true, window, cx); } + self.persist(); + cx.notify(); + } + + fn restore_project(&mut self, project_index: usize, cx: &mut Context) { + self.projects[project_index].archived = false; + self.persist(); + cx.notify(); } fn set_archived( @@ -1848,6 +1860,9 @@ impl Workspace { window: &mut Window, cx: &mut Context, ) { + if !archived { + self.projects[project_index].archived = false; + } let agent = &mut self.projects[project_index].agents[agent_index]; if agent.config.archived == archived { return; diff --git a/src/app/mobile.rs b/src/app/mobile.rs index 77b905d..52d89f6 100644 --- a/src/app/mobile.rs +++ b/src/app/mobile.rs @@ -337,6 +337,7 @@ impl Workspace { index } else { self.projects.push(ProjectView { + archived: false, sync_counts: None, branch: ssh_host.clone().unwrap_or_else(|| branch(&path)), path: path.clone(), @@ -451,6 +452,7 @@ pub(super) fn verify_session_switching_and_mobile_routing( let mut second_agent = test_agent(ProtocolVersion::V2); second_agent.config.id = 102; this.projects = vec![ProjectView { + archived: false, path: root.to_owned(), ssh_host: None, branch: String::new(), diff --git a/src/app/render/sidebar.rs b/src/app/render/sidebar.rs index ed155e8..ddb41dd 100644 --- a/src/app/render/sidebar.rs +++ b/src/app/render/sidebar.rs @@ -161,7 +161,12 @@ impl Workspace { .iter() .flat_map(|project| &project.agents) .filter(|agent| agent.config.archived) - .count(); + .count() + + self + .projects + .iter() + .filter(|project| project.archived) + .count(); for (index, location) in self.sidebar_results(&session_query).into_iter().enumerate() { let SessionLocation { project_index, @@ -425,7 +430,10 @@ impl Workspace { continue; } let collapsed = self.collapsed_projects.contains(&project_index); - if !has_sessions && (archive_view || active_only || !project.agents.is_empty()) { + if (!archive_view && project.archived) + || (!has_sessions && archive_view && !project.archived) + || (active_only && !has_sessions) + { continue; } let name = project @@ -519,18 +527,36 @@ impl Workspace { }) .child(name), ) - .when(!archive_view && has_sessions, |header| { + .when(!archive_view || project.archived, |header| { header.child( sidebar_action(palette, false) .id(("project-archive", project_index)) .invisible() .group_hover(row_group.clone(), |style| style.visible()) - .child(Icon::new(IconName::Inbox).size(px(14.))) - .tooltip(|window, cx| Tooltip::new("Archive project").build(window, cx)) + .child( + Icon::new(if archive_view { + IconName::Undo2 + } else { + IconName::Inbox + }) + .size(px(14.)), + ) + .tooltip(move |window, cx| { + Tooltip::new(if archive_view { + "Restore project" + } else { + "Archive project" + }) + .build(window, cx) + }) .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) .on_click(cx.listener(move |this, _, window, cx| { cx.stop_propagation(); - this.archive_project(project_index, window, cx); + if archive_view { + this.restore_project(project_index, cx); + } else { + this.archive_project(project_index, window, cx); + } })), ) }) diff --git a/src/app/tests.rs b/src/app/tests.rs index 212c9a6..148ed9a 100644 --- a/src/app/tests.rs +++ b/src/app/tests.rs @@ -604,13 +604,13 @@ fn zoom_shortcuts_and_menu_actions_change_the_font_scale(cx: &mut gpui_kit::Test verify_project_session_sidebar(&restored, cx); verify_project_agent_picker(&restored, cx); verify_inline_session_renaming(&restored, cx); + verify_project_archiving(&restored, cx); verify_workspace_shortcuts(&restored, cx); verify_settings_apply_and_persist(&restored, temp.path(), cx); verify_key_bindings(&restored, cx); verify_notifications_for_background_sessions(&restored, temp.path(), cx); verify_repository_cloning(&restored, temp.path(), cx); verify_split_panes(&restored, temp.path(), cx); - verify_project_archiving(&restored, cx); verify_pane_resize_grab_areas(&restored, cx); // SAFETY: restore the process environment modified for this test. unsafe { @@ -672,6 +672,7 @@ fn verify_cli_new_session( ); let connection_id = source.connection.as_ref().unwrap().id; this.projects = vec![ProjectView { + archived: false, path: path.to_owned(), ssh_host: None, branch: "main".into(), @@ -765,6 +766,7 @@ fn verify_resets_and_folder_moves_preserve_harness_references( original.config.custom_title = Some("Keep my session name".into()); original.log(Role::User, "Previous conversation"); this.projects = vec![ProjectView { + archived: false, path: path.to_owned(), ssh_host: None, branch: "main".into(), @@ -800,6 +802,7 @@ fn verify_resets_and_folder_moves_preserve_harness_references( this.projects[0].agents[0].controller.session_id = Some("reset-session".into()); this.projects[0].agents[0].config.session_has_activity = true; this.projects.push(ProjectView { + archived: false, path: path.join("other"), ssh_host: None, branch: "main".into(), @@ -875,6 +878,7 @@ fn verify_workspace_shortcuts(workspace: &Entity, cx: &mut gpui_kit:: } }); this.projects = vec![ProjectView { + archived: false, path: PathBuf::from("."), ssh_host: None, branch: String::new(), @@ -1006,6 +1010,7 @@ fn verify_inline_session_renaming( agent.config.id = agent_id; agent.config.command.clear(); this.projects = vec![ProjectView { + archived: false, path: PathBuf::from("."), ssh_host: None, branch: String::new(), @@ -1234,6 +1239,7 @@ fn verify_settings_apply_and_persist( agent.config.command.clear(); agent.status = Status::Connecting; this.projects = vec![ProjectView { + archived: false, path: path.to_owned(), ssh_host: None, branch: "main".into(), @@ -1551,6 +1557,7 @@ fn verify_notifications_for_background_sessions( } }); this.projects = vec![ProjectView { + archived: false, path: path.to_owned(), ssh_host: None, branch: "main".into(), @@ -1807,6 +1814,7 @@ fn verify_split_panes( }); } this.projects = vec![ProjectView { + archived: false, path: path.to_owned(), ssh_host: None, branch: "main".into(), @@ -1991,6 +1999,7 @@ fn verify_split_panes( }); let other_path = path.join("other-checkout"); this.projects.push(ProjectView { + archived: false, path: other_path.clone(), ssh_host: None, branch: "main".into(), @@ -2205,6 +2214,7 @@ fn verify_project_archiving(workspace: &Entity, cx: &mut gpui_kit::Vi }; this.projects = vec![ ProjectView { + archived: false, path: PathBuf::from("/projects/archive"), ssh_host: None, branch: "main".into(), @@ -2212,6 +2222,7 @@ fn verify_project_archiving(workspace: &Entity, cx: &mut gpui_kit::Vi agents: vec![agent(801, false), agent(802, false), agent(803, true)], }, ProjectView { + archived: false, path: PathBuf::from("/projects/keep"), ssh_host: None, branch: "main".into(), @@ -2229,7 +2240,21 @@ fn verify_project_archiving(workspace: &Entity, cx: &mut gpui_kit::Vi agent_index: 0, }; this.set_view(WorkspaceView::Conversation(first), window, cx); + this.set_archived(0, 0, true, window, cx); + assert!(!this.projects[0].archived); + this.set_archived(0, 1, true, window, cx); + assert!( + !this.projects[0].archived, + "archiving the last session keeps its folder active" + ); + this.projects[0].agents[0].config.archived = false; + this.projects[0].agents[1].config.archived = false; + this.set_view(WorkspaceView::Conversation(first), window, cx); this.archive_project(0, window, cx); + assert!( + this.projects[0].archived, + "explicit project archiving hides its folder" + ); assert!( this.projects[0] .agents @@ -2242,6 +2267,7 @@ fn verify_project_archiving(workspace: &Entity, cx: &mut gpui_kit::Vi let saved: Config = serde_json::from_slice(&serde_json::to_vec(&this.config()).unwrap()).unwrap(); assert!(saved.projects[0].agents.iter().all(|agent| agent.archived)); + assert!(saved.projects[0].archived); assert_eq!(saved.projects[0].agents.len(), 3); this.archive_project(0, window, cx); @@ -2254,6 +2280,20 @@ fn verify_project_archiving(workspace: &Entity, cx: &mut gpui_kit::Vi this.set_view(this.view.toggle_archive(), window, cx); assert_eq!(this.view, WorkspaceView::Empty); assert!(this.sidebar_results("").is_empty()); + this.restore_project(0, cx); + assert!(!this.projects[0].archived); + assert!( + this.projects[0] + .agents + .iter() + .all(|agent| agent.config.archived) + ); + this.projects[0].agents.clear(); + this.archive_project(0, window, cx); + assert!(this.projects[0].archived, "empty folders can be archived"); + assert!(this.config().projects[0].archived); + this.restore_project(0, cx); + assert!(!this.config().projects[0].archived); }); }); } @@ -2277,6 +2317,7 @@ fn verify_project_session_sidebar( }; this.projects = vec![ ProjectView { + archived: false, path: PathBuf::from("/projects/first"), ssh_host: None, branch: "main".into(), @@ -2288,6 +2329,7 @@ fn verify_project_session_sidebar( ], }, ProjectView { + archived: false, path: PathBuf::from("/projects/second"), ssh_host: Some("remote".into()), branch: "topic".into(), diff --git a/src/config.rs b/src/config.rs index d9abd2c..cec4432 100644 --- a/src/config.rs +++ b/src/config.rs @@ -204,6 +204,8 @@ pub struct ForkSource { #[derive(Clone, Debug, Serialize, Deserialize)] pub struct ProjectConfig { + #[serde(default)] + pub archived: bool, pub path: PathBuf, #[serde(default, skip_serializing_if = "Option::is_none")] pub ssh_host: Option, @@ -497,6 +499,18 @@ pub(crate) fn save_to(path: &Path, config: &Config) -> Result<(), String> { #[cfg(test)] mod tests { + #[test] + fn project_archive_state_defaults_to_active_and_survives_saving() { + let mut project: super::ProjectConfig = + serde_json::from_str(r#"{"path":"/project","agents":[]}"#).unwrap(); + assert!(!project.archived); + project.archived = true; + let saved = serde_json::to_string(&project).unwrap(); + let restored: super::ProjectConfig = serde_json::from_str(&saved).unwrap(); + assert!(restored.archived); + assert!(restored.agents.is_empty()); + } + use super::*; use std::time::{SystemTime, UNIX_EPOCH}; diff --git a/src/images.rs b/src/images.rs index a3b4966..352177a 100644 --- a/src/images.rs +++ b/src/images.rs @@ -176,6 +176,7 @@ mod tests { }); let config = Config { projects: vec![ProjectConfig { + archived: false, path: "/project".into(), ssh_host: None, agents: vec![agent], diff --git a/src/session/tests.rs b/src/session/tests.rs index cb51f2f..463d2f3 100644 --- a/src/session/tests.rs +++ b/src/session/tests.rs @@ -1384,6 +1384,7 @@ fn harness_history_reloads_after_restart() { let session_id = session.session_id.clone().unwrap(); let config = Config { projects: vec![ProjectConfig { + archived: false, path: temp.path().to_owned(), ssh_host: None, agents: vec![session.snapshot()],