thread_history.rs

  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}