Support archiving and restoring cloud threads (#60635)

## What changed

- Add cloud archive and unarchive requests and support listing archived threads.
- Detach successfully archived threads and emit archive notifications. Report loaded cloud threads from live attachments rather than cached resume metadata, including after archive failures.
- Scope resume picker archive state and transcript previews by backend and thread so archiving a cloud session preserves a local session with the same ID.
- Limit cloud task archive cleanup to the selected thread, preserving child agents and their active session state, and update the confirmation text accordingly.

## Testing

Add regression coverage for archive acknowledgments, archived-thread discovery and restoration, attachment state after archive failures, same-ID sessions across backends, and preservation of attached child agents.

GitOrigin-RevId: bd155be85ee911375d4d326b18f9b5abe8e82b3d
This commit is contained in:
Benjamin Carlsson
2026-11-10 18:19:11 +00:00
committed by copyberry
parent 627b579219
commit df82e2fe0a
16 changed files with 698 additions and 78 deletions
@@ -1,4 +1,4 @@
//! Server RPCs own lifecycle outcomes; successful actions invalidate dashboard group caches.
//! Server RPCs own lifecycle outcomes; cache cleanup follows the backend's removal scope.
//! Keep the dashboard browsable during RPCs while deferring task notifications.
//! Removing the current root leaves an unattached dashboard, even when it is empty.
@@ -202,6 +202,15 @@ impl App {
.filter(|name| !name.is_empty())
.unwrap_or("Untitled task");
let (title, description, label) = match action {
AgentsOverviewAction::Archive
if self.app_server_target == crate::AppServerTarget::Cloud =>
{
(
format!("Archive “{name}”?"),
"This stops any running work in this task and archives it. Child agents stay unchanged. Its history can be restored from the resume picker.",
"Archive task",
)
}
AgentsOverviewAction::Archive => (
format!("Archive “{name}”?"),
"This stops any running work in this task and its child agents, then archives them. Their history can be restored from the resume picker.",
@@ -278,6 +287,10 @@ impl App {
if self.windows_sandbox_blocks_thread_switch() {
return Ok(());
}
// Cloud archives only the selected thread; Local lifecycle actions include descendants.
let removes_subtree = action != AgentsOverviewAction::Archive
|| app_server.thread_params_mode()
!= crate::app_server_session::ThreadParamsMode::Cloud;
// The overview may lack intermediate ancestors, or even the primary's metadata.
let mut removes_primary = self.primary_thread_id == Some(thread_id);
let mut attempted = false;
@@ -296,13 +309,17 @@ impl App {
state.rename_target = None;
}
self.repaint_agents_overview();
let voice_result = self
.stop_voice_for_removed_thread(app_server, thread_id)
.await;
let voice_result = if removes_subtree || self.voice_owner_thread_id() == Some(thread_id) {
self.stop_voice_for_removed_thread(app_server, thread_id)
.await
} else {
Ok(())
};
let operation = async {
let result = async {
voice_result?;
if let Some(primary) = primary_thread_id
if removes_subtree
&& let Some(primary) = primary_thread_id
&& primary != thread_id
{
removes_primary = app_server
@@ -369,8 +386,11 @@ impl App {
self.agents_overview.refresh_notifications.clear();
}
if result.is_ok() {
// Invalidate cached details for the displayed group, without inferring child outcomes.
let mut removed = self.known_thread_subtree(thread_id);
let mut removed = if removes_subtree {
self.known_thread_subtree(thread_id)
} else {
HashSet::from([thread_id])
};
if let Some(primary) = self.primary_thread_id
&& removes_primary
{
@@ -1,5 +1,6 @@
use super::*;
use crate::app::agents_overview_discovery::list_pinned_threads;
use crate::app::tests::test_thread_session;
use crate::app_event::AgentsOverviewAction;
use codex_app_server_protocol::ClientRequest;
use codex_app_server_protocol::RequestId;
@@ -66,6 +67,120 @@ async fn pin_action_updates_shared_section_state() -> Result<()> {
Ok(())
}
#[tokio::test]
async fn cloud_archive_preserves_attached_child_and_other_tasks() -> Result<()> {
use codex_app_server_client::AppServerClient;
use codex_app_server_client::CloudClient;
use codex_app_server_client::CloudOperation;
use serde_json::json;
let mut app = make_test_app().await;
app.app_server_target = AppServerTarget::Cloud;
let parent = ThreadId::new();
let child = ThreadId::new();
let other = ThreadId::new();
let threads = HashMap::from([
(
parent,
Some(overview_thread(
parent,
/*parent_thread_id*/ None,
"Cloud parent",
ThreadStatus::Idle,
)),
),
(
child,
Some(overview_thread(
child,
Some(parent),
"Cloud child",
ThreadStatus::Active {
active_flags: Vec::new(),
},
)),
),
(
other,
Some(overview_thread(
other,
/*parent_thread_id*/ None,
"Other task",
ThreadStatus::Idle,
)),
),
]);
app.agents_overview.threads = threads.clone();
app.primary_thread_id = Some(child);
app.active_thread_id = Some(child);
app.chat_widget
.handle_thread_session(test_thread_session(child, app.config.cwd.to_path_buf()));
app.chat_widget
.apply_external_edit("Keep child draft".into());
let draft = app.chat_widget.capture_thread_input_state();
crate::chatwidget::activate_voice_for_thread(&mut app.chat_widget, child);
app.confirm_agents_overview_action(parent, AgentsOverviewAction::Archive);
insta::assert_snapshot!(render_bottom_popup(&app.chat_widget, /*width*/ 80), @"
Archive “Cloud parent”?
› Ke This stops any running work in this task and archives it. Child
agents stay unchanged. Its history can be restored from the resume
gp picker.
› 1. Cancel
2. Archive task
enter select · esc back
");
app.chat_widget.handle_key_event(KeyCode::Esc.into());
let (client, mut commands, _events) = CloudClient::channel(/*capacity*/ 1);
let mut session = AppServerSession::new(
crate::backend_id::BackendId::new(),
AppServerClient::Cloud(client),
crate::app_server_session::ThreadParamsMode::Cloud,
);
let mut tui = crate::tui::test_support::make_test_tui()?;
tui.pause_events();
let (result, ()) = tokio::join!(
Box::pin(app.run_agents_overview_action(
&mut tui,
&mut session,
parent,
AgentsOverviewAction::Archive
)),
async {
let command = commands.recv().await.expect("archive request");
let CloudOperation::Request(request) = command.operation else {
panic!("expected archive request");
};
assert!(
matches!(*request, ClientRequest::ThreadArchive { params, .. } if params.thread_id == parent.to_string())
);
command.response.send(Ok(Ok(json!({})))).unwrap();
},
);
result?;
let mut expected = threads;
expected.remove(&parent);
assert_eq!(app.agents_overview.threads, expected);
assert_eq!(
(
app.primary_thread_id,
app.current_displayed_thread_id(),
app.chat_widget.thread_id()
),
(Some(child), Some(child), Some(child))
);
assert_eq!(app.chat_widget.capture_thread_input_state(), draft);
assert_eq!(app.voice_owner_thread_id(), Some(child));
if let Some(refresh) = app.agents_overview.refresh_task.take() {
refresh.abort();
}
Ok(())
}
#[tokio::test]
async fn archive_confirmation_number_keys_act_immediately() {
for key in ['1', '2'] {
+72 -23
View File
@@ -158,7 +158,9 @@ pub enum SessionPickerAction {
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SessionPickerLaunchContext {
Startup,
ExistingSession { current_thread_id: Option<ThreadId> },
ExistingSession {
current_thread_id: Option<(BackendId, ThreadId)>,
},
}
impl SessionPickerAction {
@@ -365,6 +367,7 @@ enum BackgroundEvent {
page: std::io::Result<PickerPage>,
},
Preview {
backend_id: BackendId,
thread_id: ThreadId,
preview: std::io::Result<Vec<TranscriptPreviewLine>>,
},
@@ -374,6 +377,7 @@ enum BackgroundEvent {
transcript: std::io::Result<LoadedTranscript>,
},
Archive {
backend_id: BackendId,
thread_id: ThreadId,
result: std::io::Result<()>,
},
@@ -580,7 +584,9 @@ pub(crate) async fn run_resume_picker_from_existing_session_with_app_servers(
show_all,
backends,
displayed_owner,
SessionPickerLaunchContext::ExistingSession { current_thread_id },
SessionPickerLaunchContext::ExistingSession {
current_thread_id: current_thread_id.map(|id| (displayed_owner, id)),
},
)
.await
}
@@ -1003,7 +1009,11 @@ fn spawn_app_server_source(backend: AppServerPickerBackend) -> sources::Source {
preview = load_transcript_preview(&mut app_server, thread_id, config.as_ref()) => preview,
_ = bg_tx.closed() => continue,
};
let _ = bg_tx.send(BackgroundEvent::Preview { thread_id, preview });
let _ = bg_tx.send(BackgroundEvent::Preview {
backend_id,
thread_id,
preview,
});
}
PickerLoadRequest::Transcript {
thread_id,
@@ -1042,7 +1052,11 @@ fn spawn_app_server_source(backend: AppServerPickerBackend) -> sources::Source {
.await
.map(|_| ())
.map_err(std::io::Error::other);
let _ = bg_tx.send(BackgroundEvent::Archive { thread_id, result });
let _ = bg_tx.send(BackgroundEvent::Archive {
backend_id,
thread_id,
result,
});
}
PickerLoadRequest::Unarchive { thread_id, .. } => {
let result = archive_request_handle
@@ -1152,8 +1166,8 @@ struct PickerState {
sort_key: ThreadSortKey,
inline_error: Option<String>,
archive_state: archive::ArchiveState,
expanded_thread_id: Option<ThreadId>,
transcript_previews: HashMap<ThreadId, TranscriptPreviewState>,
expanded_thread_id: Option<(BackendId, ThreadId)>,
transcript_previews: HashMap<(BackendId, ThreadId), TranscriptPreviewState>,
transcript_cells: HashMap<(BackendId, ThreadId), SessionTranscriptState>,
pending_transcript_open: Option<(BackendId, ThreadId)>,
unread: HashMap<(BackendId, ThreadId), String>,
@@ -1903,9 +1917,20 @@ impl PickerState {
let completed_token = self.search_state.active_token();
self.continue_search_if_token_matches(completed_token);
}
BackgroundEvent::Preview { thread_id, preview } => {
BackgroundEvent::Preview {
backend_id,
thread_id,
preview,
} => {
// Archiving removes the owned cache entry; ignore its late preview.
if !matches!(
self.transcript_previews.get(&(backend_id, thread_id)),
Some(TranscriptPreviewState::Loading)
) {
return Ok(None);
}
self.transcript_previews.insert(
thread_id,
(backend_id, thread_id),
match preview {
Ok(lines) => TranscriptPreviewState::Loaded(lines),
Err(_) => TranscriptPreviewState::Failed,
@@ -1954,8 +1979,12 @@ impl PickerState {
}
self.request_frame();
}
BackgroundEvent::Archive { thread_id, result } => {
self.handle_archive_result(thread_id, result);
BackgroundEvent::Archive {
backend_id,
thread_id,
result,
} => {
self.handle_archive_result(backend_id, thread_id, result);
}
BackgroundEvent::Unarchive { thread_id, result } => {
return Ok(self.handle_unarchive_result(thread_id, result));
@@ -2378,14 +2407,14 @@ impl PickerState {
return;
};
let backend_id = row.backend_id.unwrap_or(self.backend_id);
if self.expanded_thread_id == Some(thread_id) {
if self.expanded_thread_id == Some((backend_id, thread_id)) {
self.expanded_thread_id = None;
self.request_frame();
return;
}
self.expanded_thread_id = Some(thread_id);
self.expanded_thread_id = Some((backend_id, thread_id));
if let std::collections::hash_map::Entry::Vacant(e) =
self.transcript_previews.entry(thread_id)
self.transcript_previews.entry((backend_id, thread_id))
{
e.insert(TranscriptPreviewState::Loading);
(self.picker_loader)(PickerLoadRequest::Preview {
@@ -2407,7 +2436,10 @@ impl PickerState {
let is_selected = row_idx == self.selected;
let is_expanded = is_selected
&& row.thread_id.is_some()
&& self.expanded_thread_id == row.thread_id;
&& self.expanded_thread_id
== row
.thread_id
.map(|id| (row.backend_id.unwrap_or(self.backend_id), id));
render_session_lines(
row,
self,
@@ -2438,8 +2470,12 @@ impl PickerState {
for (offset, row) in self.filtered_rows[self.scroll_top..].iter().enumerate() {
let row_idx = self.scroll_top + offset;
let is_selected = row_idx == self.selected;
let is_expanded =
is_selected && row.thread_id.is_some() && self.expanded_thread_id == row.thread_id;
let is_expanded = is_selected
&& row.thread_id.is_some()
&& self.expanded_thread_id
== row
.thread_id
.map(|id| (row.backend_id.unwrap_or(self.backend_id), id));
let row_height = render_session_lines(
row,
self,
@@ -3216,8 +3252,12 @@ fn render_list(frame: &mut crate::custom_terminal::Frame, area: Rect, state: &Pi
}
let row_idx = start + idx;
let is_selected = row_idx == state.selected;
let is_expanded =
is_selected && row.thread_id.is_some() && state.expanded_thread_id == row.thread_id;
let is_expanded = is_selected
&& row.thread_id.is_some()
&& state.expanded_thread_id
== row
.thread_id
.map(|id| (row.backend_id.unwrap_or(state.backend_id), id));
let is_zebra = row_idx.is_multiple_of(2);
for line in render_session_lines(row, state, is_selected, is_expanded, is_zebra, area.width)
{
@@ -3746,7 +3786,10 @@ fn render_transcript_preview_lines(
let Some(thread_id) = row.thread_id else {
return details;
};
let preview_lines = match state.transcript_previews.get(&thread_id) {
let preview_lines = match state
.transcript_previews
.get(&(row.backend_id.unwrap_or(state.backend_id), thread_id))
{
Some(TranscriptPreviewState::Loading) => {
vec![
vec![
@@ -6140,7 +6183,10 @@ session_picker_view = "dense"
.await
.unwrap();
assert_eq!(state.expanded_thread_id, Some(thread_id));
assert_eq!(
state.expanded_thread_id,
Some((state.backend_id, thread_id))
);
assert_eq!(*recorded_requests.lock().unwrap(), vec![thread_id]);
state
@@ -6182,7 +6228,10 @@ session_picker_view = "dense"
.await
.unwrap();
assert_eq!(state.expanded_thread_id, Some(thread_id));
assert_eq!(
state.expanded_thread_id,
Some((state.backend_id, thread_id))
);
}
#[test]
@@ -6508,9 +6557,9 @@ session_picker_view = "dense"
state.filtered_rows = vec![row];
state.relative_time_reference =
Some(parse_timestamp_str("2026-04-28T18:00:00Z").expect("timestamp"));
state.expanded_thread_id = Some(thread_id);
state.expanded_thread_id = Some((state.backend_id, thread_id));
state.transcript_previews.insert(
thread_id,
(state.backend_id, thread_id),
TranscriptPreviewState::Loaded(vec![
TranscriptPreviewLine {
speaker: TranscriptPreviewSpeaker::User,
+31 -18
View File
@@ -21,6 +21,7 @@ pub(super) enum ArchiveState {
#[default]
Idle,
Pending {
backend_id: BackendId,
thread_id: ThreadId,
},
Restoring {
@@ -58,11 +59,13 @@ impl PickerState {
return;
};
let backend_id = row.backend_id.unwrap_or(self.backend_id);
let key = (backend_id, thread_id);
if matches!(
self.launch_context,
SessionPickerLaunchContext::ExistingSession {
current_thread_id: Some(current_thread_id)
} if current_thread_id == thread_id
} if current_thread_id == key
) {
self.inline_error = Some(String::from(
"Use /archive to archive the current session and exit.",
@@ -71,8 +74,10 @@ impl PickerState {
return;
}
let backend_id = row.backend_id.unwrap_or(self.backend_id);
self.archive_state = ArchiveState::Pending { thread_id };
self.archive_state = ArchiveState::Pending {
backend_id,
thread_id,
};
self.request_frame();
(self.picker_loader)(PickerLoadRequest::Archive {
backend_id,
@@ -82,12 +87,24 @@ impl PickerState {
pub(super) fn handle_archive_result(
&mut self,
backend_id: BackendId,
thread_id: ThreadId,
result: std::io::Result<()>,
) {
if self.archive_state != (ArchiveState::Pending { thread_id }) {
if self.archive_state
!= (ArchiveState::Pending {
backend_id,
thread_id,
})
{
return;
}
let key = (backend_id, thread_id);
let default_backend = self.backend_id;
let is_archived = |row: &Row| {
(row.backend_id.unwrap_or(default_backend), row.thread_id)
== (backend_id, Some(thread_id))
};
self.chord_matcher.cancel();
self.archive_state = ArchiveState::Idle;
@@ -100,21 +117,18 @@ impl PickerState {
let selected_key = self
.filtered_rows
.get(self.selected)
.filter(|row| row.thread_id != Some(thread_id))
.and_then(Row::seen_key);
.filter(|row| !is_archived(row))
.map(|row| (row.backend_id.unwrap_or(default_backend), row.seen_key()));
self.all_rows.retain(|row| row.thread_id != Some(thread_id));
self.all_rows.retain(|row| !is_archived(row));
// Keep the seen-row key as a tombstone so an older page that is already
// in flight cannot put the archived session back into the picker.
self.transcript_previews.remove(&thread_id);
self.transcript_cells.retain(|(_, id), _| *id != thread_id);
if self.expanded_thread_id == Some(thread_id) {
self.transcript_previews.remove(&key);
if self.expanded_thread_id == Some(key) {
self.expanded_thread_id = None;
}
if self
.pending_transcript_open
.is_some_and(|(_, id)| id == thread_id)
{
self.transcript_cells.remove(&key);
if self.pending_transcript_open == Some(key) {
self.pending_transcript_open = None;
if let Some(cancellation) = self.pending_transcript_cancellation.take() {
let _ = cancellation.send(());
@@ -125,10 +139,9 @@ impl PickerState {
self.apply_filter();
if let Some(selected_key) = selected_key
&& let Some(selected) = self
.filtered_rows
.iter()
.position(|row| row.seen_key().as_ref() == Some(&selected_key))
&& let Some(selected) = self.filtered_rows.iter().position(|row| {
(row.backend_id.unwrap_or(default_backend), row.seen_key()) == selected_key
})
{
self.selected = selected;
self.ensure_selected_visible();
+135 -6
View File
@@ -72,12 +72,19 @@ async fn archive_shortcut_archives_selected_session_once() {
let backend_id = crate::backend_id::BackendId::new();
let thread_id = ThreadId::new();
set_selected_session(&mut state, thread_id);
state.filtered_rows[0].backend_id = Some(backend_id);
state.all_rows[0].backend_id = Some(backend_id);
state.apply_filter();
let shortcut = KeyEvent::new(KeyCode::Char('\u{0001}'), KeyModifiers::NONE);
assert!(state.handle_key(shortcut).await.unwrap().is_none());
assert!(state.handle_key(shortcut).await.unwrap().is_none());
assert_eq!(state.archive_state, ArchiveState::Pending { thread_id });
assert_eq!(
state.archive_state,
ArchiveState::Pending {
backend_id,
thread_id
}
);
assert!(
matches!(actions.recv().await, Some(PickerLoadRequest::Archive { backend_id: owner, thread_id: archived }) if (owner, archived) == (backend_id, thread_id))
);
@@ -89,14 +96,15 @@ async fn archive_shortcut_archives_selected_session_once() {
.is_none()
);
state.handle_archive_result(thread_id, Ok(()));
state.handle_archive_result(backend_id, thread_id, Ok(()));
assert_eq!(state.archive_state, ArchiveState::Idle);
assert!(state.filtered_rows.is_empty());
state.status = SessionStatus::Archived;
set_selected_session(&mut state, thread_id);
state.filtered_rows[0].backend_id = Some(backend_id);
state.all_rows[0].backend_id = Some(backend_id);
state.apply_filter();
state
.handle_key(KeyEvent::from(KeyCode::Enter))
.await
@@ -111,7 +119,7 @@ fn archive_request_rejects_current_session() {
let (mut state, requests) = archive_picker_state();
let thread_id = ThreadId::new();
state.launch_context = SessionPickerLaunchContext::ExistingSession {
current_thread_id: Some(thread_id),
current_thread_id: Some((state.backend_id, thread_id)),
};
set_selected_session(&mut state, thread_id);
@@ -140,7 +148,11 @@ fn archive_failure_preserves_session_and_reports_server_error() {
},
};
state.handle_archive_result(thread_id, Err(std::io::Error::other(error)));
state.handle_archive_result(
state.backend_id,
thread_id,
Err(std::io::Error::other(error)),
);
assert_eq!(state.archive_state, ArchiveState::Idle);
assert_eq!(
@@ -321,3 +333,120 @@ fn archived_session_restore_failure_preserves_session_and_allows_retry() {
assert_eq!(*requests.lock().unwrap(), vec![thread_id, thread_id]);
}
#[tokio::test]
async fn archive_cloud_preserves_same_id_current_local_row_and_transcript() {
let (mut state, _requests) = archive_picker_state();
let (requests, mut actions) = tokio::sync::mpsc::unbounded_channel();
state.picker_loader = Arc::new(move |request| requests.send(request).unwrap());
let thread_id = ThreadId::new();
let local = state.backend_id;
let cloud = crate::backend_id::BackendId::new();
state.launch_context = SessionPickerLaunchContext::ExistingSession {
current_thread_id: Some((local, thread_id)),
};
set_selected_session(&mut state, thread_id);
let local_row = state.all_rows[0].clone();
let mut cloud_row = local_row.clone();
cloud_row.backend_id = Some(cloud);
state.all_rows.push(cloud_row);
state.apply_filter();
state.selected = state
.filtered_rows
.iter()
.position(|row| row.backend_id == Some(cloud))
.unwrap();
state.transcript_cells.insert(
(local, thread_id),
super::super::SessionTranscriptState::Failed,
);
state.transcript_cells.insert(
(cloud, thread_id),
super::super::SessionTranscriptState::Failed,
);
for (backend_id, text) in [(local, "Local preview"), (cloud, "Cloud preview")] {
state.selected = state
.filtered_rows
.iter()
.position(|row| row.backend_id.unwrap_or(local) == backend_id)
.unwrap();
state.toggle_selected_expansion();
assert!(
matches!(actions.try_recv().unwrap(), PickerLoadRequest::Preview { backend_id: owner, thread_id: id } if (owner, id) == (backend_id, thread_id))
);
state
.handle_background_event(super::super::BackgroundEvent::Preview {
backend_id,
thread_id,
preview: Ok(vec![super::super::TranscriptPreviewLine {
speaker: super::super::TranscriptPreviewSpeaker::User,
text: text.into(),
}]),
})
.await
.unwrap();
}
assert_eq!(state.expanded_thread_id, Some((cloud, thread_id)));
state.pending_transcript_open = Some((local, thread_id));
state.request_archive_for_selected_session();
assert_eq!(
state.archive_state,
ArchiveState::Pending {
backend_id: cloud,
thread_id
}
);
assert!(
matches!(actions.try_recv().unwrap(), PickerLoadRequest::Archive {
backend_id, thread_id: id
} if (backend_id, id) == (cloud, thread_id))
);
assert!(actions.try_recv().is_err());
state.handle_archive_result(local, thread_id, Ok(()));
assert_eq!(state.all_rows.len(), 2);
assert_eq!(
state.archive_state,
ArchiveState::Pending {
backend_id: cloud,
thread_id
}
);
state.handle_archive_result(cloud, thread_id, Ok(()));
assert_eq!(state.all_rows, vec![local_row.clone()]);
assert_eq!(state.filtered_rows, vec![local_row]);
assert_eq!(
state.transcript_cells.keys().copied().collect::<Vec<_>>(),
vec![(local, thread_id)]
);
state
.handle_background_event(super::super::BackgroundEvent::Preview {
backend_id: cloud,
thread_id,
preview: Ok(vec![super::super::TranscriptPreviewLine {
speaker: super::super::TranscriptPreviewSpeaker::User,
text: "Late Cloud preview".into(),
}]),
})
.await
.unwrap();
assert!(!state.transcript_previews.contains_key(&(cloud, thread_id)));
assert_eq!(state.expanded_thread_id, None);
state.toggle_selected_expansion();
assert_eq!(state.expanded_thread_id, Some((local, thread_id)));
let lines = super::super::render_transcript_preview_lines(
&state.filtered_rows[0],
&state,
/*width*/ 80,
);
let rendered = lines
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("\n");
assert!(rendered.contains("Local preview"));
assert!(!rendered.contains("Cloud preview"));
assert_eq!(state.pending_transcript_open, Some((local, thread_id)));
assert_eq!(state.archive_state, ArchiveState::Idle);
}
+2 -2
View File
@@ -121,10 +121,10 @@ pub(super) fn spawn(
} else {
let error = std::io::Error::other("The session's backend is unavailable.");
let event = match request {
PickerLoadRequest::Preview { thread_id, .. } => BackgroundEvent::Preview { thread_id, preview: Err(error) },
PickerLoadRequest::Preview { thread_id, .. } => BackgroundEvent::Preview { backend_id: backend, thread_id, preview: Err(error) },
PickerLoadRequest::Acknowledge { .. } => continue,
PickerLoadRequest::Transcript { thread_id, .. } => BackgroundEvent::Transcript { backend_id: backend, thread_id, transcript: Err(error) },
PickerLoadRequest::Archive { thread_id, .. } => BackgroundEvent::Archive { thread_id, result: Err(error) },
PickerLoadRequest::Archive { thread_id, .. } => BackgroundEvent::Archive { backend_id: backend, thread_id, result: Err(error) },
PickerLoadRequest::Unarchive { thread_id, .. } => BackgroundEvent::Unarchive { thread_id, result: Err(error) },
PickerLoadRequest::Page(_) => unreachable!(),
};