From c76f0b100ca60af7e83cb91c62ca22749d2d877a Mon Sep 17 00:00:00 2001 From: Dave Date: Mon, 23 Feb 2026 15:31:38 +0000 Subject: [PATCH] story-kit: merge 79_story_agents_panel_skips_archived_work_on_startup --- server/src/http/agents.rs | 115 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/server/src/http/agents.rs b/server/src/http/agents.rs index 8bdcab6..5dda0d8 100644 --- a/server/src/http/agents.rs +++ b/server/src/http/agents.rs @@ -3,6 +3,7 @@ use crate::http::context::{AppContext, OpenApiResult, bad_request}; use crate::worktree; use poem_openapi::{Object, OpenApi, Tags, param::Path, payload::Json}; use serde::Serialize; +use std::path; use std::sync::Arc; #[derive(Tags)] @@ -60,6 +61,20 @@ struct WorktreeListEntry { path: String, } +/// Returns true if the story file exists in `work/5_archived/`. +/// +/// Used to exclude agents for already-archived stories from the `list_agents` +/// response so the agents panel is not cluttered with old completed items on +/// frontend startup. +fn story_is_archived(project_root: &path::Path, story_id: &str) -> bool { + project_root + .join(".story_kit") + .join("work") + .join("5_archived") + .join(format!("{story_id}.md")) + .exists() +} + pub struct AgentsApi { pub ctx: Arc, } @@ -123,13 +138,24 @@ impl AgentsApi { } /// List all agents with their status. + /// + /// Agents for stories that have been archived (`work/5_archived/`) are + /// excluded so the agents panel is not cluttered with old completed items + /// on frontend startup. #[oai(path = "/agents", method = "get")] async fn list_agents(&self) -> OpenApiResult>> { + let project_root = self.ctx.agents.get_project_root(&self.ctx.state).ok(); let agents = self.ctx.agents.list_agents().map_err(bad_request)?; Ok(Json( agents .into_iter() + .filter(|info| { + project_root + .as_deref() + .map(|root| !story_is_archived(root, &info.story_id)) + .unwrap_or(true) + }) .map(|info| AgentInfoResponse { story_id: info.story_id, agent_name: info.agent_name, @@ -265,3 +291,92 @@ impl AgentsApi { Ok(Json(true)) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::agents::AgentStatus; + use tempfile::TempDir; + + fn make_archived_dir(tmp: &TempDir) -> path::PathBuf { + let root = tmp.path().to_path_buf(); + let archived = root + .join(".story_kit") + .join("work") + .join("5_archived"); + std::fs::create_dir_all(&archived).unwrap(); + root + } + + #[test] + fn story_is_archived_false_when_file_absent() { + let tmp = TempDir::new().unwrap(); + let root = make_archived_dir(&tmp); + assert!(!story_is_archived(&root, "79_story_foo")); + } + + #[test] + fn story_is_archived_true_when_file_present() { + let tmp = TempDir::new().unwrap(); + let root = make_archived_dir(&tmp); + std::fs::write( + root.join(".story_kit/work/5_archived/79_story_foo.md"), + "---\nname: test\n---\n", + ) + .unwrap(); + assert!(story_is_archived(&root, "79_story_foo")); + } + + #[tokio::test] + async fn list_agents_excludes_archived_stories() { + let tmp = TempDir::new().unwrap(); + let root = make_archived_dir(&tmp); + + // Place an archived story file + std::fs::write( + root.join(".story_kit/work/5_archived/79_story_archived.md"), + "---\nname: archived story\n---\n", + ) + .unwrap(); + + let ctx = AppContext::new_test(root); + // Inject an agent for the archived story (completed) and one for an active story + ctx.agents + .inject_test_agent("79_story_archived", "coder-1", AgentStatus::Completed); + ctx.agents + .inject_test_agent("80_story_active", "coder-1", AgentStatus::Running); + + let api = AgentsApi { + ctx: Arc::new(ctx), + }; + let result = api.list_agents().await.unwrap().0; + + // Archived story's agent should not appear + assert!( + !result.iter().any(|a| a.story_id == "79_story_archived"), + "archived story agent should be excluded from list_agents" + ); + // Active story's agent should still appear + assert!( + result.iter().any(|a| a.story_id == "80_story_active"), + "active story agent should be included in list_agents" + ); + } + + #[tokio::test] + async fn list_agents_includes_all_when_no_project_root() { + // When no project root is configured, all agents are returned (safe default). + let tmp = TempDir::new().unwrap(); + let ctx = AppContext::new_test(tmp.path().to_path_buf()); + // Clear the project_root so get_project_root returns Err + *ctx.state.project_root.lock().unwrap() = None; + ctx.agents + .inject_test_agent("42_story_whatever", "coder-1", AgentStatus::Completed); + + let api = AgentsApi { + ctx: Arc::new(ctx), + }; + let result = api.list_agents().await.unwrap().0; + assert!(result.iter().any(|a| a.story_id == "42_story_whatever")); + } +}