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