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}