fix(agents): restore archived tasks for continued conversation (#349)

Archived agent tasks could be opened but never returned to the active list, so their message composer stayed disabled. Unarchive is now available in the task header, in the archived-task notice above the composer, and in the task context menu (including mixed bulk selections). Restoring a task moves it back to the active list, keeps its conversation and worktree, and reconnects the existing agent session on the next message; the backend persists and broadcasts the restored status and refuses restoration while a stopped turn is still settling.
Цей коміт міститься в:
l0ng-ai authored and GitHub committed 2026-10-30 13:55:26 +08:00
1 джерело 2203938491
коміт 9a5b44bebb
11 files changed
+132 -3

No files matched your search

+20
Переглянути файл
@@ -760,6 +760,26 @@ impl AgentManager {
Ok(())
}
/// Returns an archived task to the active list without starting the agent.
pub fn unarchive(&self, id: &str) -> IdeResult<()> {
let task = self.task(id)?;
{
let mut s = task.state.lock();
if s.task.status != AgentStatus::Archived {
return Ok(());
}
if s.busy {
return Err(IdeError::invalid("the task is still stopping"));
}
s.task.status = AgentStatus::Ready;
s.task.error = None;
s.task.updated_ms = now_ms();
}
task.persist();
task.emit_task();
Ok(())
}
/// Forgets the task; with `remove_worktree` also deletes its worktree (the branch stays).
pub async fn delete(&self, id: &str, remove_worktree: bool) -> IdeResult<()> {
let task = self.task(id)?;
+33
Переглянути файл
@@ -817,3 +817,36 @@ async fn cursor_approval_choice_applies_before_first_prompt() {
wait_for(&mgr, &task.id, AgentStatus::Ready).await;
mgr.shutdown();
}
#[tokio::test(flavor = "multi_thread")]
async fn archived_tasks_can_be_restored_and_continue_their_session() {
let tmp = tempfile::tempdir().unwrap();
let agent = Arc::new(FakeAgent::default());
let mgr = manager(tmp.path(), agent.clone());
let task = mgr
.create(NewAgentTask {
prompt: text("again"),
provider: "claude".into(),
config: vec![],
isolation: Isolation::Local,
base: None,
})
.await
.unwrap();
wait_for(&mgr, &task.id, AgentStatus::Ready).await;
let entries = mgr.entries(&task.id, None).unwrap();
mgr.archive(&task.id).unwrap();
assert!(mgr.prompt(&task.id, text("again"), false).await.is_err());
mgr.unarchive(&task.id).unwrap();
assert_eq!(mgr.task(&task.id).unwrap().state.lock().task.status, AgentStatus::Ready);
assert!(mgr.task(&task.id).unwrap().live.lock().is_none());
assert_eq!(mgr.entries(&task.id, None).unwrap().len(), entries.len());
drop(mgr);
let mgr = manager(tmp.path(), agent.clone());
assert_eq!(mgr.task(&task.id).unwrap().state.lock().task.status, AgentStatus::Ready);
mgr.prompt(&task.id, text("again"), false).await.unwrap();
wait_for(&mgr, &task.id, AgentStatus::Ready).await;
assert!(agent.calls.lock().iter().any(|call| call == "session/load"));
assert_eq!(mgr.entries(&task.id, None).unwrap().len(), entries.len() + 2);
mgr.unarchive(&task.id).unwrap();
}