1use acp_thread::{AgentSessionInfo, AgentSessionList, AgentSessionListRequest, SessionListUpdate};
2use agent_client_protocol as acp;
3use gpui::{App, Task};
4use std::rc::Rc;
5use ui::prelude::*;
6
7pub struct ThreadHistory {
8 session_list: Option<Rc<dyn AgentSessionList>>,
9 sessions: Vec<AgentSessionInfo>,
10 _refresh_task: Task<()>,
11 _watch_task: Option<Task<()>>,
12}
13
14impl ThreadHistory {
15 pub fn new(session_list: Option<Rc<dyn AgentSessionList>>, cx: &mut Context<Self>) -> Self {
16 let mut this = Self {
17 session_list: None,
18 sessions: Vec::new(),
19 _refresh_task: Task::ready(()),
20 _watch_task: None,
21 };
22 this.set_session_list(session_list, cx);
23 this
24 }
25
26 pub fn set_session_list(
27 &mut self,
28 session_list: Option<Rc<dyn AgentSessionList>>,
29 cx: &mut Context<Self>,
30 ) {
31 if let (Some(current), Some(next)) = (&self.session_list, &session_list)
32 && Rc::ptr_eq(current, next)
33 {
34 return;
35 }
36
37 self.session_list = session_list;
38 self.sessions.clear();
39 self._refresh_task = Task::ready(());
40
41 let Some(session_list) = self.session_list.as_ref() else {
42 self._watch_task = None;
43 cx.notify();
44 return;
45 };
46 let Some(rx) = session_list.watch(cx) else {
47 self._watch_task = None;
48 self.refresh_sessions(false, cx);
49 return;
50 };
51 session_list.notify_refresh();
52
53 self._watch_task = Some(cx.spawn(async move |this, cx| {
54 while let Ok(first_update) = rx.recv().await {
55 let mut updates = vec![first_update];
56 while let Ok(update) = rx.try_recv() {
57 updates.push(update);
58 }
59
60 this.update(cx, |this, cx| {
61 let needs_refresh = updates
62 .iter()
63 .any(|u| matches!(u, SessionListUpdate::Refresh));
64
65 if needs_refresh {
66 this.refresh_sessions(false, cx);
67 } else {
68 for update in updates {
69 if let SessionListUpdate::SessionInfo { session_id, update } = update {
70 this.apply_info_update(session_id, update, cx);
71 }
72 }
73 }
74 })
75 .ok();
76 }
77 }));
78 }
79
80 pub(crate) fn refresh_full_history(&mut self, cx: &mut Context<Self>) {
81 self.refresh_sessions(true, cx);
82 }
83
84 fn apply_info_update(
85 &mut self,
86 session_id: acp::SessionId,
87 info_update: acp::SessionInfoUpdate,
88 cx: &mut Context<Self>,
89 ) {
90 let Some(session) = self
91 .sessions
92 .iter_mut()
93 .find(|s| s.session_id == session_id)
94 else {
95 return;
96 };
97
98 match info_update.title {
99 acp::MaybeUndefined::Value(title) => {
100 session.title = Some(title.into());
101 }
102 acp::MaybeUndefined::Null => {
103 session.title = None;
104 }
105 acp::MaybeUndefined::Undefined => {}
106 }
107 match info_update.updated_at {
108 acp::MaybeUndefined::Value(date_str) => {
109 if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(&date_str) {
110 session.updated_at = Some(dt.with_timezone(&chrono::Utc));
111 }
112 }
113 acp::MaybeUndefined::Null => {
114 session.updated_at = None;
115 }
116 acp::MaybeUndefined::Undefined => {}
117 }
118 if let Some(meta) = info_update.meta {
119 session.meta = Some(meta);
120 }
121
122 cx.notify();
123 }
124
125 fn refresh_sessions(&mut self, load_all_pages: bool, cx: &mut Context<Self>) {
126 let Some(session_list) = self.session_list.clone() else {
127 cx.notify();
128 return;
129 };
130
131 self._refresh_task = cx.spawn(async move |this, cx| {
132 let mut cursor: Option<String> = None;
133 let mut is_first_page = true;
134
135 loop {
136 let request = AgentSessionListRequest {
137 cursor: cursor.clone(),
138 ..Default::default()
139 };
140 let task = cx.update(|cx| session_list.list_sessions(request, cx));
141 let response = match task.await {
142 Ok(response) => response,
143 Err(error) => {
144 log::error!("Failed to load session history: {error:#}");
145 return;
146 }
147 };
148
149 let acp_thread::AgentSessionListResponse {
150 sessions: page_sessions,
151 next_cursor,
152 ..
153 } = response;
154
155 this.update(cx, |this, cx| {
156 if is_first_page {
157 this.sessions = page_sessions;
158 } else {
159 this.sessions.extend(page_sessions);
160 }
161 cx.notify();
162 })
163 .ok();
164
165 is_first_page = false;
166 if !load_all_pages {
167 break;
168 }
169
170 match next_cursor {
171 Some(next_cursor) => {
172 if cursor.as_ref() == Some(&next_cursor) {
173 log::warn!(
174 "Session list pagination returned the same cursor; stopping to avoid a loop."
175 );
176 break;
177 }
178 cursor = Some(next_cursor);
179 }
180 None => break,
181 }
182 }
183 });
184 }
185
186 pub(crate) fn is_empty(&self) -> bool {
187 self.sessions.is_empty()
188 }
189
190 pub fn has_session_list(&self) -> bool {
191 self.session_list.is_some()
192 }
193
194 pub fn refresh(&mut self, _cx: &mut Context<Self>) {
195 if let Some(session_list) = &self.session_list {
196 session_list.notify_refresh();
197 }
198 }
199
200 pub fn session_for_id(&self, session_id: &acp::SessionId) -> Option<AgentSessionInfo> {
201 self.sessions
202 .iter()
203 .find(|entry| &entry.session_id == session_id)
204 .cloned()
205 }
206
207 pub(crate) fn sessions(&self) -> &[AgentSessionInfo] {
208 &self.sessions
209 }
210
211 pub(crate) fn get_recent_sessions(&self, limit: usize) -> Vec<AgentSessionInfo> {
212 self.sessions.iter().take(limit).cloned().collect()
213 }
214
215 pub fn supports_delete(&self) -> bool {
216 self.session_list
217 .as_ref()
218 .map(|sl| sl.supports_delete())
219 .unwrap_or(false)
220 }
221
222 pub(crate) fn delete_session(
223 &self,
224 session_id: &acp::SessionId,
225 cx: &mut App,
226 ) -> Task<anyhow::Result<()>> {
227 if let Some(session_list) = self.session_list.as_ref() {
228 session_list.delete_session(session_id, cx)
229 } else {
230 Task::ready(Ok(()))
231 }
232 }
233
234 pub(crate) fn delete_sessions(&self, cx: &mut App) -> Task<anyhow::Result<()>> {
235 if let Some(session_list) = self.session_list.as_ref() {
236 session_list.delete_sessions(cx)
237 } else {
238 Task::ready(Ok(()))
239 }
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246 use acp_thread::AgentSessionListResponse;
247 use gpui::TestAppContext;
248 use std::{
249 any::Any,
250 sync::{Arc, Mutex},
251 };
252
253 fn init_test(cx: &mut TestAppContext) {
254 cx.update(|cx| {
255 let settings_store = settings::SettingsStore::test(cx);
256 cx.set_global(settings_store);
257 theme::init(theme::LoadThemes::JustBase, cx);
258 });
259 }
260
261 #[derive(Clone)]
262 struct TestSessionList {
263 sessions: Vec<AgentSessionInfo>,
264 updates_tx: smol::channel::Sender<SessionListUpdate>,
265 updates_rx: smol::channel::Receiver<SessionListUpdate>,
266 }
267
268 impl TestSessionList {
269 fn new(sessions: Vec<AgentSessionInfo>) -> Self {
270 let (tx, rx) = smol::channel::unbounded();
271 Self {
272 sessions,
273 updates_tx: tx,
274 updates_rx: rx,
275 }
276 }
277
278 fn send_update(&self, update: SessionListUpdate) {
279 self.updates_tx.try_send(update).ok();
280 }
281 }
282
283 impl AgentSessionList for TestSessionList {
284 fn list_sessions(
285 &self,
286 _request: AgentSessionListRequest,
287 _cx: &mut App,
288 ) -> Task<anyhow::Result<AgentSessionListResponse>> {
289 Task::ready(Ok(AgentSessionListResponse::new(self.sessions.clone())))
290 }
291
292 fn watch(&self, _cx: &mut App) -> Option<smol::channel::Receiver<SessionListUpdate>> {
293 Some(self.updates_rx.clone())
294 }
295
296 fn notify_refresh(&self) {
297 self.send_update(SessionListUpdate::Refresh);
298 }
299
300 fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
301 self
302 }
303 }
304
305 #[derive(Clone)]
306 struct PaginatedTestSessionList {
307 first_page_sessions: Vec<AgentSessionInfo>,
308 second_page_sessions: Vec<AgentSessionInfo>,
309 requested_cursors: Arc<Mutex<Vec<Option<String>>>>,
310 async_responses: bool,
311 updates_tx: smol::channel::Sender<SessionListUpdate>,
312 updates_rx: smol::channel::Receiver<SessionListUpdate>,
313 }
314
315 impl PaginatedTestSessionList {
316 fn new(
317 first_page_sessions: Vec<AgentSessionInfo>,
318 second_page_sessions: Vec<AgentSessionInfo>,
319 ) -> Self {
320 let (tx, rx) = smol::channel::unbounded();
321 Self {
322 first_page_sessions,
323 second_page_sessions,
324 requested_cursors: Arc::new(Mutex::new(Vec::new())),
325 async_responses: false,
326 updates_tx: tx,
327 updates_rx: rx,
328 }
329 }
330
331 fn with_async_responses(mut self) -> Self {
332 self.async_responses = true;
333 self
334 }
335
336 fn requested_cursors(&self) -> Vec<Option<String>> {
337 self.requested_cursors.lock().unwrap().clone()
338 }
339
340 fn clear_requested_cursors(&self) {
341 self.requested_cursors.lock().unwrap().clear()
342 }
343
344 fn send_update(&self, update: SessionListUpdate) {
345 self.updates_tx.try_send(update).ok();
346 }
347 }
348
349 impl AgentSessionList for PaginatedTestSessionList {
350 fn list_sessions(
351 &self,
352 request: AgentSessionListRequest,
353 cx: &mut App,
354 ) -> Task<anyhow::Result<AgentSessionListResponse>> {
355 let requested_cursors = self.requested_cursors.clone();
356 let first_page_sessions = self.first_page_sessions.clone();
357 let second_page_sessions = self.second_page_sessions.clone();
358
359 let respond = move || {
360 requested_cursors
361 .lock()
362 .unwrap()
363 .push(request.cursor.clone());
364
365 match request.cursor.as_deref() {
366 None => AgentSessionListResponse {
367 sessions: first_page_sessions,
368 next_cursor: Some("page-2".to_string()),
369 meta: None,
370 },
371 Some("page-2") => AgentSessionListResponse::new(second_page_sessions),
372 _ => AgentSessionListResponse::new(Vec::new()),
373 }
374 };
375
376 if self.async_responses {
377 cx.foreground_executor().spawn(async move {
378 smol::future::yield_now().await;
379 Ok(respond())
380 })
381 } else {
382 Task::ready(Ok(respond()))
383 }
384 }
385
386 fn watch(&self, _cx: &mut App) -> Option<smol::channel::Receiver<SessionListUpdate>> {
387 Some(self.updates_rx.clone())
388 }
389
390 fn notify_refresh(&self) {
391 self.send_update(SessionListUpdate::Refresh);
392 }
393
394 fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
395 self
396 }
397 }
398
399 fn test_session(session_id: &str, title: &str) -> AgentSessionInfo {
400 AgentSessionInfo {
401 session_id: acp::SessionId::new(session_id),
402 cwd: None,
403 title: Some(title.to_string().into()),
404 updated_at: None,
405 created_at: None,
406 meta: None,
407 }
408 }
409
410 #[gpui::test]
411 async fn test_refresh_only_loads_first_page_by_default(cx: &mut TestAppContext) {
412 init_test(cx);
413
414 let session_list = Rc::new(PaginatedTestSessionList::new(
415 vec![test_session("session-1", "First")],
416 vec![test_session("session-2", "Second")],
417 ));
418
419 let history = cx.new(|cx| ThreadHistory::new(Some(session_list.clone()), cx));
420 cx.run_until_parked();
421
422 history.update(cx, |history, _cx| {
423 assert_eq!(history.sessions.len(), 1);
424 assert_eq!(
425 history.sessions[0].session_id,
426 acp::SessionId::new("session-1")
427 );
428 });
429 assert_eq!(session_list.requested_cursors(), vec![None]);
430 }
431
432 #[gpui::test]
433 async fn test_enabling_full_pagination_loads_all_pages(cx: &mut TestAppContext) {
434 init_test(cx);
435
436 let session_list = Rc::new(PaginatedTestSessionList::new(
437 vec![test_session("session-1", "First")],
438 vec![test_session("session-2", "Second")],
439 ));
440
441 let history = cx.new(|cx| ThreadHistory::new(Some(session_list.clone()), cx));
442 cx.run_until_parked();
443 session_list.clear_requested_cursors();
444
445 history.update(cx, |history, cx| history.refresh_full_history(cx));
446 cx.run_until_parked();
447
448 history.update(cx, |history, _cx| {
449 assert_eq!(history.sessions.len(), 2);
450 assert_eq!(
451 history.sessions[0].session_id,
452 acp::SessionId::new("session-1")
453 );
454 assert_eq!(
455 history.sessions[1].session_id,
456 acp::SessionId::new("session-2")
457 );
458 });
459 assert_eq!(
460 session_list.requested_cursors(),
461 vec![None, Some("page-2".to_string())]
462 );
463 }
464
465 #[gpui::test]
466 async fn test_standard_refresh_replaces_with_first_page_after_full_history_refresh(
467 cx: &mut TestAppContext,
468 ) {
469 init_test(cx);
470
471 let session_list = Rc::new(PaginatedTestSessionList::new(
472 vec![test_session("session-1", "First")],
473 vec![test_session("session-2", "Second")],
474 ));
475
476 let history = cx.new(|cx| ThreadHistory::new(Some(session_list.clone()), cx));
477 cx.run_until_parked();
478
479 history.update(cx, |history, cx| history.refresh_full_history(cx));
480 cx.run_until_parked();
481 session_list.clear_requested_cursors();
482
483 history.update(cx, |history, cx| {
484 history.refresh(cx);
485 });
486 cx.run_until_parked();
487
488 history.update(cx, |history, _cx| {
489 assert_eq!(history.sessions.len(), 1);
490 assert_eq!(
491 history.sessions[0].session_id,
492 acp::SessionId::new("session-1")
493 );
494 });
495 assert_eq!(session_list.requested_cursors(), vec![None]);
496 }
497
498 #[gpui::test]
499 async fn test_re_entering_full_pagination_reloads_all_pages(cx: &mut TestAppContext) {
500 init_test(cx);
501
502 let session_list = Rc::new(PaginatedTestSessionList::new(
503 vec![test_session("session-1", "First")],
504 vec![test_session("session-2", "Second")],
505 ));
506
507 let history = cx.new(|cx| ThreadHistory::new(Some(session_list.clone()), cx));
508 cx.run_until_parked();
509
510 history.update(cx, |history, cx| history.refresh_full_history(cx));
511 cx.run_until_parked();
512 session_list.clear_requested_cursors();
513
514 history.update(cx, |history, cx| history.refresh_full_history(cx));
515 cx.run_until_parked();
516
517 history.update(cx, |history, _cx| {
518 assert_eq!(history.sessions.len(), 2);
519 });
520 assert_eq!(
521 session_list.requested_cursors(),
522 vec![None, Some("page-2".to_string())]
523 );
524 }
525
526 #[gpui::test]
527 async fn test_partial_refresh_batch_drops_non_first_page_sessions(cx: &mut TestAppContext) {
528 init_test(cx);
529
530 let second_page_session_id = acp::SessionId::new("session-2");
531 let session_list = Rc::new(PaginatedTestSessionList::new(
532 vec![test_session("session-1", "First")],
533 vec![test_session("session-2", "Second")],
534 ));
535
536 let history = cx.new(|cx| ThreadHistory::new(Some(session_list.clone()), cx));
537 cx.run_until_parked();
538
539 history.update(cx, |history, cx| history.refresh_full_history(cx));
540 cx.run_until_parked();
541
542 session_list.clear_requested_cursors();
543
544 session_list.send_update(SessionListUpdate::SessionInfo {
545 session_id: second_page_session_id.clone(),
546 update: acp::SessionInfoUpdate::new().title("Updated Second"),
547 });
548 session_list.send_update(SessionListUpdate::Refresh);
549 cx.run_until_parked();
550
551 history.update(cx, |history, _cx| {
552 assert_eq!(history.sessions.len(), 1);
553 assert_eq!(
554 history.sessions[0].session_id,
555 acp::SessionId::new("session-1")
556 );
557 assert!(
558 history
559 .sessions
560 .iter()
561 .all(|session| session.session_id != second_page_session_id)
562 );
563 });
564 assert_eq!(session_list.requested_cursors(), vec![None]);
565 }
566
567 #[gpui::test]
568 async fn test_full_pagination_works_with_async_page_fetches(cx: &mut TestAppContext) {
569 init_test(cx);
570
571 let session_list = Rc::new(
572 PaginatedTestSessionList::new(
573 vec![test_session("session-1", "First")],
574 vec![test_session("session-2", "Second")],
575 )
576 .with_async_responses(),
577 );
578
579 let history = cx.new(|cx| ThreadHistory::new(Some(session_list.clone()), cx));
580 cx.run_until_parked();
581 session_list.clear_requested_cursors();
582
583 history.update(cx, |history, cx| history.refresh_full_history(cx));
584 cx.run_until_parked();
585
586 history.update(cx, |history, _cx| {
587 assert_eq!(history.sessions.len(), 2);
588 });
589 assert_eq!(
590 session_list.requested_cursors(),
591 vec![None, Some("page-2".to_string())]
592 );
593 }
594
595 #[gpui::test]
596 async fn test_apply_info_update_title(cx: &mut TestAppContext) {
597 init_test(cx);
598
599 let session_id = acp::SessionId::new("test-session");
600 let sessions = vec![AgentSessionInfo {
601 session_id: session_id.clone(),
602 cwd: None,
603 title: Some("Original Title".into()),
604 updated_at: None,
605 created_at: None,
606 meta: None,
607 }];
608 let session_list = Rc::new(TestSessionList::new(sessions));
609
610 let history = cx.new(|cx| ThreadHistory::new(Some(session_list.clone()), cx));
611 cx.run_until_parked();
612
613 session_list.send_update(SessionListUpdate::SessionInfo {
614 session_id: session_id.clone(),
615 update: acp::SessionInfoUpdate::new().title("New Title"),
616 });
617 cx.run_until_parked();
618
619 history.update(cx, |history, _cx| {
620 let session = history.sessions.iter().find(|s| s.session_id == session_id);
621 assert_eq!(
622 session.unwrap().title.as_ref().map(|s| s.as_ref()),
623 Some("New Title")
624 );
625 });
626 }
627
628 #[gpui::test]
629 async fn test_apply_info_update_clears_title_with_null(cx: &mut TestAppContext) {
630 init_test(cx);
631
632 let session_id = acp::SessionId::new("test-session");
633 let sessions = vec![AgentSessionInfo {
634 session_id: session_id.clone(),
635 cwd: None,
636 title: Some("Original Title".into()),
637 updated_at: None,
638 created_at: None,
639 meta: None,
640 }];
641 let session_list = Rc::new(TestSessionList::new(sessions));
642
643 let history = cx.new(|cx| ThreadHistory::new(Some(session_list.clone()), cx));
644 cx.run_until_parked();
645
646 session_list.send_update(SessionListUpdate::SessionInfo {
647 session_id: session_id.clone(),
648 update: acp::SessionInfoUpdate::new().title(None::<String>),
649 });
650 cx.run_until_parked();
651
652 history.update(cx, |history, _cx| {
653 let session = history.sessions.iter().find(|s| s.session_id == session_id);
654 assert_eq!(session.unwrap().title, None);
655 });
656 }
657
658 #[gpui::test]
659 async fn test_apply_info_update_ignores_undefined_fields(cx: &mut TestAppContext) {
660 init_test(cx);
661
662 let session_id = acp::SessionId::new("test-session");
663 let sessions = vec![AgentSessionInfo {
664 session_id: session_id.clone(),
665 cwd: None,
666 title: Some("Original Title".into()),
667 updated_at: None,
668 created_at: None,
669 meta: None,
670 }];
671 let session_list = Rc::new(TestSessionList::new(sessions));
672
673 let history = cx.new(|cx| ThreadHistory::new(Some(session_list.clone()), cx));
674 cx.run_until_parked();
675
676 session_list.send_update(SessionListUpdate::SessionInfo {
677 session_id: session_id.clone(),
678 update: acp::SessionInfoUpdate::new(),
679 });
680 cx.run_until_parked();
681
682 history.update(cx, |history, _cx| {
683 let session = history.sessions.iter().find(|s| s.session_id == session_id);
684 assert_eq!(
685 session.unwrap().title.as_ref().map(|s| s.as_ref()),
686 Some("Original Title")
687 );
688 });
689 }
690
691 #[gpui::test]
692 async fn test_multiple_info_updates_applied_in_order(cx: &mut TestAppContext) {
693 init_test(cx);
694
695 let session_id = acp::SessionId::new("test-session");
696 let sessions = vec![AgentSessionInfo {
697 session_id: session_id.clone(),
698 cwd: None,
699 title: None,
700 updated_at: None,
701 created_at: None,
702 meta: None,
703 }];
704 let session_list = Rc::new(TestSessionList::new(sessions));
705
706 let history = cx.new(|cx| ThreadHistory::new(Some(session_list.clone()), cx));
707 cx.run_until_parked();
708
709 session_list.send_update(SessionListUpdate::SessionInfo {
710 session_id: session_id.clone(),
711 update: acp::SessionInfoUpdate::new().title("First Title"),
712 });
713 session_list.send_update(SessionListUpdate::SessionInfo {
714 session_id: session_id.clone(),
715 update: acp::SessionInfoUpdate::new().title("Second Title"),
716 });
717 cx.run_until_parked();
718
719 history.update(cx, |history, _cx| {
720 let session = history.sessions.iter().find(|s| s.session_id == session_id);
721 assert_eq!(
722 session.unwrap().title.as_ref().map(|s| s.as_ref()),
723 Some("Second Title")
724 );
725 });
726 }
727
728 #[gpui::test]
729 async fn test_refresh_supersedes_info_updates(cx: &mut TestAppContext) {
730 init_test(cx);
731
732 let session_id = acp::SessionId::new("test-session");
733 let sessions = vec![AgentSessionInfo {
734 session_id: session_id.clone(),
735 cwd: None,
736 title: Some("Server Title".into()),
737 updated_at: None,
738 created_at: None,
739 meta: None,
740 }];
741 let session_list = Rc::new(TestSessionList::new(sessions));
742
743 let history = cx.new(|cx| ThreadHistory::new(Some(session_list.clone()), cx));
744 cx.run_until_parked();
745
746 session_list.send_update(SessionListUpdate::SessionInfo {
747 session_id: session_id.clone(),
748 update: acp::SessionInfoUpdate::new().title("Local Update"),
749 });
750 session_list.send_update(SessionListUpdate::Refresh);
751 cx.run_until_parked();
752
753 history.update(cx, |history, _cx| {
754 let session = history.sessions.iter().find(|s| s.session_id == session_id);
755 assert_eq!(
756 session.unwrap().title.as_ref().map(|s| s.as_ref()),
757 Some("Server Title")
758 );
759 });
760 }
761
762 #[gpui::test]
763 async fn test_info_update_for_unknown_session_is_ignored(cx: &mut TestAppContext) {
764 init_test(cx);
765
766 let session_id = acp::SessionId::new("known-session");
767 let sessions = vec![AgentSessionInfo {
768 session_id,
769 cwd: None,
770 title: Some("Original".into()),
771 updated_at: None,
772 created_at: None,
773 meta: None,
774 }];
775 let session_list = Rc::new(TestSessionList::new(sessions));
776
777 let history = cx.new(|cx| ThreadHistory::new(Some(session_list.clone()), cx));
778 cx.run_until_parked();
779
780 session_list.send_update(SessionListUpdate::SessionInfo {
781 session_id: acp::SessionId::new("unknown-session"),
782 update: acp::SessionInfoUpdate::new().title("Should Be Ignored"),
783 });
784 cx.run_until_parked();
785
786 history.update(cx, |history, _cx| {
787 assert_eq!(history.sessions.len(), 1);
788 assert_eq!(
789 history.sessions[0].title.as_ref().map(|s| s.as_ref()),
790 Some("Original")
791 );
792 });
793 }
794}