story-kit: merge 255_story_show_agent_logs_in_expanded_story_popup

This commit is contained in:
Dave
2026-03-17 00:46:52 +00:00
parent c132d4f5c0
commit be61803af0
2 changed files with 147 additions and 0 deletions

View File

@@ -108,6 +108,14 @@ export const agentsApi = {
baseUrl,
);
},
getAgentOutput(storyId: string, agentName: string, baseUrl?: string) {
return requestJson<{ output: string }>(
`/agents/${encodeURIComponent(storyId)}/${encodeURIComponent(agentName)}/output`,
{},
baseUrl,
);
},
};
/**

View File

@@ -105,6 +105,12 @@ impl TestResultsResponse {
}
}
/// Response for the agent output endpoint.
#[derive(Object, Serialize)]
struct AgentOutputResponse {
output: String,
}
/// Returns true if the story file exists in `work/5_done/` or `work/6_archived/`.
///
/// Used to exclude agents for already-archived stories from the `list_agents`
@@ -400,6 +406,45 @@ impl AgentsApi {
))
}
/// Get the historical output text for an agent session.
///
/// Reads the most recent persistent log file for the given story+agent and
/// returns all `output` events concatenated as a single string. Returns an
/// empty string if no log file exists yet.
#[oai(path = "/agents/:story_id/:agent_name/output", method = "get")]
async fn get_agent_output(
&self,
story_id: Path<String>,
agent_name: Path<String>,
) -> OpenApiResult<Json<AgentOutputResponse>> {
let project_root = self
.ctx
.agents
.get_project_root(&self.ctx.state)
.map_err(bad_request)?;
let log_path =
crate::agent_log::find_latest_log(&project_root, &story_id.0, &agent_name.0);
let Some(path) = log_path else {
return Ok(Json(AgentOutputResponse {
output: String::new(),
}));
};
let entries = crate::agent_log::read_log(&path).map_err(bad_request)?;
let output: String = entries
.iter()
.filter(|e| {
e.event.get("type").and_then(|t| t.as_str()) == Some("output")
})
.filter_map(|e| e.event.get("text").and_then(|t| t.as_str()).map(str::to_owned))
.collect();
Ok(Json(AgentOutputResponse { output }))
}
/// Remove a git worktree and its feature branch for a story.
#[oai(path = "/agents/worktrees/:story_id", method = "delete")]
async fn remove_worktree(&self, story_id: Path<String>) -> OpenApiResult<Json<bool>> {
@@ -835,6 +880,100 @@ allowed_tools = ["Read", "Bash"]
assert!(result.is_err());
}
// --- get_agent_output tests ---
#[tokio::test]
async fn get_agent_output_returns_empty_when_no_log_exists() {
let tmp = TempDir::new().unwrap();
let ctx = AppContext::new_test(tmp.path().to_path_buf());
let api = AgentsApi {
ctx: Arc::new(ctx),
};
let result = api
.get_agent_output(
Path("42_story_foo".to_string()),
Path("coder-1".to_string()),
)
.await
.unwrap()
.0;
assert_eq!(result.output, "");
}
#[tokio::test]
async fn get_agent_output_returns_concatenated_output_events() {
use crate::agent_log::AgentLogWriter;
use crate::agents::AgentEvent;
let tmp = TempDir::new().unwrap();
let root = tmp.path();
let mut writer =
AgentLogWriter::new(root, "42_story_foo", "coder-1", "sess-test").unwrap();
writer
.write_event(&AgentEvent::Status {
story_id: "42_story_foo".to_string(),
agent_name: "coder-1".to_string(),
status: "running".to_string(),
})
.unwrap();
writer
.write_event(&AgentEvent::Output {
story_id: "42_story_foo".to_string(),
agent_name: "coder-1".to_string(),
text: "Hello ".to_string(),
})
.unwrap();
writer
.write_event(&AgentEvent::Output {
story_id: "42_story_foo".to_string(),
agent_name: "coder-1".to_string(),
text: "world\n".to_string(),
})
.unwrap();
writer
.write_event(&AgentEvent::Done {
story_id: "42_story_foo".to_string(),
agent_name: "coder-1".to_string(),
session_id: None,
})
.unwrap();
let ctx = AppContext::new_test(root.to_path_buf());
let api = AgentsApi {
ctx: Arc::new(ctx),
};
let result = api
.get_agent_output(
Path("42_story_foo".to_string()),
Path("coder-1".to_string()),
)
.await
.unwrap()
.0;
// Only output event texts should be concatenated; status and done are excluded.
assert_eq!(result.output, "Hello world\n");
}
#[tokio::test]
async fn get_agent_output_returns_error_when_no_project_root() {
let tmp = TempDir::new().unwrap();
let ctx = AppContext::new_test(tmp.path().to_path_buf());
*ctx.state.project_root.lock().unwrap() = None;
let api = AgentsApi {
ctx: Arc::new(ctx),
};
let result = api
.get_agent_output(
Path("42_story_foo".to_string()),
Path("coder-1".to_string()),
)
.await;
assert!(result.is_err());
}
// --- create_worktree error path ---
#[tokio::test]