Skip to main content

ai_ui_core/
thread_list.rs

1//! The thread list, after upstream's `ThreadListRuntimeCore` and its remote
2//! variant: many threads, one of them the main thread.
3//!
4//! Every thread is any [`ThreadRuntime`] the `factory` creates, so a list can
5//! hold local and external-store threads alike. Changes apply immediately
6//! (optimistically) and, if a [`ThreadListAdapter`] is given, are persisted by
7//! background tasks handed to the host's [`Spawner`]; a failed task undoes
8//! its change and is reported through [`ThreadList::last_error`].
9//!
10//! A new thread becomes a regular thread (and is registered with the adapter)
11//! when its first message appears. After the first answer completes, the
12//! adapter may generate a title.
13
14use crate::repository::ExportedRepository;
15use crate::runtime::ThreadRuntime;
16use crate::subscribe::{Subscribers, Subscription};
17use crate::types::ThreadMessage;
18use std::cell::{Cell, RefCell};
19use std::future::Future;
20use std::pin::Pin;
21use std::rc::{Rc, Weak};
22use std::sync::Arc;
23
24/// Runs background work on the host's executor (for example
25/// `|f| { slint::spawn_local(f).ok(); }`).
26pub type Spawner = Rc<dyn Fn(Pin<Box<dyn Future<Output = ()>>>)>;
27
28/// The outcome of an adapter call; an error is a message for the user.
29pub type AdapterFuture<T> = Pin<Box<dyn Future<Output = Result<T, String>>>>;
30
31/// Where a thread is in its life.
32#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33pub enum ThreadStatus {
34    /// Not yet registered: no message has been sent.
35    New,
36    /// A stored thread in the main list.
37    Regular,
38    /// A stored thread that is hidden from the main list.
39    Archived,
40}
41
42/// One entry of the list.
43#[derive(Clone, Debug, PartialEq, Eq)]
44pub struct ThreadItem {
45    /// Stable local id.
46    pub id: String,
47    /// The adapter's id, once known.
48    pub remote_id: Option<String>,
49    /// Whether the thread is new, regular or archived.
50    pub status: ThreadStatus,
51    /// The thread's title, once it has one.
52    pub title: Option<String>,
53}
54
55/// A thread the adapter knows.
56#[derive(Clone, Debug, PartialEq, Eq)]
57pub struct RemoteThread {
58    /// The adapter's id for the thread.
59    pub remote_id: String,
60    /// The thread's title, if stored.
61    pub title: Option<String>,
62    /// Whether the thread is archived.
63    pub archived: bool,
64}
65
66/// Persistence for the list (`RemoteThreadListAdapter`). Every method has an
67/// inert default, so an adapter implements only what it stores.
68pub trait ThreadListAdapter {
69    /// The stored threads, newest first.
70    fn list(&self) -> AdapterFuture<Vec<RemoteThread>> {
71        Box::pin(async { Ok(Vec::new()) })
72    }
73    /// Register a new thread; returns its remote id.
74    fn initialize(&self, local_id: &str) -> AdapterFuture<String> {
75        let id = local_id.to_owned();
76        Box::pin(async move { Ok(id) })
77    }
78    /// Store a new title. The default does nothing.
79    fn rename(&self, _remote_id: &str, _title: &str) -> AdapterFuture<()> {
80        Box::pin(async { Ok(()) })
81    }
82    /// Mark the thread archived. The default does nothing.
83    fn archive(&self, _remote_id: &str) -> AdapterFuture<()> {
84        Box::pin(async { Ok(()) })
85    }
86    /// Restore an archived thread. The default does nothing.
87    fn unarchive(&self, _remote_id: &str) -> AdapterFuture<()> {
88        Box::pin(async { Ok(()) })
89    }
90    /// Delete the thread. The default does nothing.
91    fn delete(&self, _remote_id: &str) -> AdapterFuture<()> {
92        Box::pin(async { Ok(()) })
93    }
94    /// A title for a thread, from its messages.
95    fn generate_title(
96        &self,
97        _remote_id: &str,
98        _messages: Vec<Arc<ThreadMessage>>,
99    ) -> AdapterFuture<Option<String>> {
100        Box::pin(async { Ok(None) })
101    }
102    /// The stored history of a thread, loaded when it is first opened.
103    fn load(&self, _remote_id: &str) -> AdapterFuture<ExportedRepository> {
104        Box::pin(async { Ok(ExportedRepository::default()) })
105    }
106}
107
108type Factory = dyn Fn(&str) -> Rc<dyn ThreadRuntime>;
109
110struct Entry {
111    item: ThreadItem,
112    runtime: Option<Rc<dyn ThreadRuntime>>,
113    /// Keeps the thread's change subscription alive.
114    _subscription: Option<Subscription>,
115    loaded: bool,
116    initializing: bool,
117    /// A title was already requested; asked at most once per thread.
118    titled: bool,
119}
120
121struct State {
122    entries: Vec<Entry>,
123    main: String,
124    error: Option<String>,
125    loading: bool,
126}
127
128struct Inner {
129    me: Weak<Inner>,
130    factory: Box<Factory>,
131    adapter: Option<Rc<dyn ThreadListAdapter>>,
132    spawner: Spawner,
133    state: RefCell<State>,
134    subscribers: Rc<Subscribers>,
135    next_id: Cell<u64>,
136}
137
138/// The user's threads, with one main thread shown at a time.
139#[derive(Clone)]
140pub struct ThreadList {
141    inner: Rc<Inner>,
142}
143
144impl ThreadList {
145    /// A list with one empty new thread as the main thread. With an adapter,
146    /// the stored threads are fetched in the background.
147    pub fn new(
148        factory: impl Fn(&str) -> Rc<dyn ThreadRuntime> + 'static,
149        spawner: Spawner,
150        adapter: Option<Rc<dyn ThreadListAdapter>>,
151    ) -> Self {
152        let list = Self {
153            inner: Rc::new_cyclic(|me| Inner {
154                me: me.clone(),
155                factory: Box::new(factory),
156                adapter,
157                spawner,
158                state: RefCell::new(State {
159                    entries: Vec::new(),
160                    main: String::new(),
161                    error: None,
162                    loading: false,
163                }),
164                subscribers: Rc::default(),
165                next_id: Cell::new(1),
166            }),
167        };
168        let id = list.inner.add_new_entry();
169        list.inner.state.borrow_mut().main = id;
170        if list.inner.adapter.is_some() {
171            list.refresh();
172        }
173        list
174    }
175
176    /// Called after every change. Drop the guard to unsubscribe.
177    pub fn subscribe(&self, callback: impl Fn() + 'static) -> Subscription {
178        self.inner.subscribers.subscribe(callback)
179    }
180
181    /// Every thread, in list order (new first, then as fetched or created).
182    pub fn items(&self) -> Vec<ThreadItem> {
183        self.inner
184            .state
185            .borrow()
186            .entries
187            .iter()
188            .map(|e| e.item.clone())
189            .collect()
190    }
191    /// The threads in the main list.
192    pub fn regular(&self) -> Vec<ThreadItem> {
193        self.with_status(ThreadStatus::Regular)
194    }
195    /// The archived threads.
196    pub fn archived(&self) -> Vec<ThreadItem> {
197        self.with_status(ThreadStatus::Archived)
198    }
199    fn with_status(&self, status: ThreadStatus) -> Vec<ThreadItem> {
200        self.items()
201            .into_iter()
202            .filter(|i| i.status == status)
203            .collect()
204    }
205    /// One thread, by local id.
206    pub fn item(&self, id: &str) -> Option<ThreadItem> {
207        self.items().into_iter().find(|i| i.id == id)
208    }
209    /// The local id of the thread being shown.
210    pub fn main_id(&self) -> String {
211        self.inner.state.borrow().main.clone()
212    }
213    /// The main thread's runtime, created on first use.
214    pub fn main(&self) -> Rc<dyn ThreadRuntime> {
215        let id = self.main_id();
216        self.inner.runtime(&id).expect("the main thread exists")
217    }
218    /// The runtime of any thread, created on first use.
219    pub fn runtime(&self, id: &str) -> Option<Rc<dyn ThreadRuntime>> {
220        self.inner.runtime(id)
221    }
222    /// The adapter is fetching the stored threads.
223    pub fn is_loading(&self) -> bool {
224        self.inner.state.borrow().loading
225    }
226    /// The last background failure, until the next successful change.
227    pub fn last_error(&self) -> Option<String> {
228        self.inner.state.borrow().error.clone()
229    }
230
231    /// Fetch the stored threads (again) and add the ones not yet listed.
232    pub fn refresh(&self) {
233        let Some(adapter) = self.inner.adapter.clone() else {
234            return;
235        };
236        self.inner.state.borrow_mut().loading = true;
237        self.inner.notify();
238        let weak = self.inner.me.clone();
239        self.inner.spawn(async move {
240            let result = adapter.list().await;
241            let Some(inner) = weak.upgrade() else { return };
242            {
243                let mut state = inner.state.borrow_mut();
244                state.loading = false;
245                match result {
246                    Ok(threads) => {
247                        let mut added = Vec::new();
248                        for thread in threads {
249                            if state
250                                .entries
251                                .iter()
252                                .any(|e| e.item.remote_id.as_deref() == Some(&thread.remote_id))
253                            {
254                                continue;
255                            }
256                            added.push(thread);
257                        }
258                        for thread in added {
259                            let id = format!("thread-{}", inner.next_id.get());
260                            inner.next_id.set(inner.next_id.get() + 1);
261                            state.entries.push(Entry {
262                                item: ThreadItem {
263                                    id,
264                                    remote_id: Some(thread.remote_id),
265                                    status: if thread.archived {
266                                        ThreadStatus::Archived
267                                    } else {
268                                        ThreadStatus::Regular
269                                    },
270                                    title: thread.title,
271                                },
272                                runtime: None,
273                                _subscription: None,
274                                loaded: false,
275                                initializing: false,
276                                titled: false,
277                            });
278                        }
279                    }
280                    Err(error) => state.error = Some(error),
281                }
282            }
283            inner.notify();
284        });
285    }
286
287    /// Start a fresh thread, or return to the empty one already there.
288    pub fn switch_to_new(&self) {
289        let existing = {
290            let state = self.inner.state.borrow();
291            state
292                .entries
293                .iter()
294                .find(|e| e.item.status == ThreadStatus::New)
295                .map(|e| e.item.id.clone())
296        };
297        let id = match existing {
298            Some(id) => id,
299            None => self.inner.add_new_entry(),
300        };
301        self.switch_to(&id);
302    }
303
304    /// Make a thread the main thread; a stored thread loads its history.
305    pub fn switch_to(&self, id: &str) {
306        let known = self
307            .inner
308            .state
309            .borrow()
310            .entries
311            .iter()
312            .any(|e| e.item.id == id);
313        if !known {
314            return;
315        }
316        self.inner.state.borrow_mut().main = id.to_owned();
317        self.inner.load_history(id);
318        self.inner.notify();
319    }
320
321    /// Rename a thread. The change shows at once and is undone if the adapter fails.
322    pub fn rename(&self, id: &str, title: &str) {
323        let title = title.trim().to_owned();
324        let (previous, remote) = {
325            let mut state = self.inner.state.borrow_mut();
326            let Some(entry) = state.entries.iter_mut().find(|e| e.item.id == id) else {
327                return;
328            };
329            let previous = std::mem::replace(
330                &mut entry.item.title,
331                (!title.is_empty()).then(|| title.clone()),
332            );
333            (previous, entry.item.remote_id.clone())
334        };
335        self.inner.notify();
336        if let (Some(adapter), Some(remote)) = (self.inner.adapter.clone(), remote) {
337            let id = id.to_owned();
338            self.inner
339                .persist(adapter.rename(&remote, &title), move |inner| {
340                    inner.with_entry(&id, |e| e.item.title = previous);
341                });
342        }
343    }
344
345    /// Archive a thread. The change shows at once and is undone if the adapter fails.
346    pub fn archive(&self, id: &str) {
347        self.set_archived(id, true);
348    }
349    /// Unarchive a thread. The change shows at once and is undone if the adapter fails.
350    pub fn unarchive(&self, id: &str) {
351        self.set_archived(id, false);
352    }
353    fn set_archived(&self, id: &str, archived: bool) {
354        let (previous, remote) = {
355            let mut state = self.inner.state.borrow_mut();
356            let Some(entry) = state.entries.iter_mut().find(|e| e.item.id == id) else {
357                return;
358            };
359            let wanted = if archived {
360                ThreadStatus::Archived
361            } else {
362                ThreadStatus::Regular
363            };
364            if entry.item.status != ThreadStatus::Regular
365                && entry.item.status != ThreadStatus::Archived
366            {
367                return;
368            }
369            let previous = std::mem::replace(&mut entry.item.status, wanted);
370            (previous, entry.item.remote_id.clone())
371        };
372        // Archiving the open thread moves on to a new one.
373        if archived && self.main_id() == id {
374            self.switch_to_new();
375        }
376        self.inner.notify();
377        if let (Some(adapter), Some(remote)) = (self.inner.adapter.clone(), remote) {
378            let id = id.to_owned();
379            let future = if archived {
380                adapter.archive(&remote)
381            } else {
382                adapter.unarchive(&remote)
383            };
384            self.inner.persist(future, move |inner| {
385                inner.with_entry(&id, |e| e.item.status = previous);
386            });
387        }
388    }
389
390    /// Delete a thread. The change shows at once and is undone if the adapter fails.
391    pub fn delete(&self, id: &str) {
392        let removed = {
393            let mut state = self.inner.state.borrow_mut();
394            let Some(index) = state.entries.iter().position(|e| e.item.id == id) else {
395                return;
396            };
397            (index, state.entries.remove(index))
398        };
399        if self.main_id() == id {
400            self.switch_to_new();
401        } else {
402            self.inner.notify();
403        }
404        if let (Some(adapter), Some(remote)) =
405            (self.inner.adapter.clone(), removed.1.item.remote_id.clone())
406        {
407            let (index, mut entry) = removed;
408            // A failed delete puts the thread back (without its live runtime).
409            entry.runtime = None;
410            entry._subscription = None;
411            entry.loaded = false;
412            self.inner.persist(adapter.delete(&remote), move |inner| {
413                let mut state = inner.state.borrow_mut();
414                let index = index.min(state.entries.len());
415                state.entries.insert(index, entry);
416            });
417        }
418    }
419}
420
421impl Inner {
422    fn notify(&self) {
423        self.subscribers.notify();
424    }
425
426    fn spawn(&self, future: impl Future<Output = ()> + 'static) {
427        (self.spawner)(Box::pin(future));
428    }
429
430    fn with_entry(&self, id: &str, change: impl FnOnce(&mut Entry)) {
431        if let Some(entry) = self
432            .state
433            .borrow_mut()
434            .entries
435            .iter_mut()
436            .find(|e| e.item.id == id)
437        {
438            change(entry);
439        }
440    }
441
442    fn add_new_entry(&self) -> String {
443        let id = format!("thread-{}", self.next_id.get());
444        self.next_id.set(self.next_id.get() + 1);
445        self.state.borrow_mut().entries.insert(
446            0,
447            Entry {
448                item: ThreadItem {
449                    id: id.clone(),
450                    remote_id: None,
451                    status: ThreadStatus::New,
452                    title: None,
453                },
454                runtime: None,
455                _subscription: None,
456                loaded: true,
457                initializing: false,
458                titled: false,
459            },
460        );
461        id
462    }
463
464    /// Run an adapter task in the background; `undo` reverts the change if it fails.
465    fn persist(&self, task: AdapterFuture<()>, undo: impl FnOnce(&Inner) + 'static) {
466        let weak = self.me.clone();
467        self.spawn(async move {
468            let result = task.await;
469            let Some(inner) = weak.upgrade() else { return };
470            match result {
471                Ok(()) => inner.state.borrow_mut().error = None,
472                Err(error) => {
473                    undo(&inner);
474                    inner.state.borrow_mut().error = Some(error);
475                }
476            }
477            inner.notify();
478        });
479    }
480
481    /// The thread's runtime, created (and observed) on first use.
482    fn runtime(&self, id: &str) -> Option<Rc<dyn ThreadRuntime>> {
483        if let Some(runtime) = self
484            .state
485            .borrow()
486            .entries
487            .iter()
488            .find(|e| e.item.id == id)
489            .map(|e| e.runtime.clone())?
490        {
491            return Some(runtime);
492        }
493        let runtime = (self.factory)(id);
494        let weak = self.me.clone();
495        let thread = id.to_owned();
496        let subscription = runtime.subscribe_dyn(Box::new(move || {
497            if let Some(inner) = weak.upgrade() {
498                inner.thread_changed(&thread);
499            }
500        }));
501        let mut state = self.state.borrow_mut();
502        let entry = state.entries.iter_mut().find(|e| e.item.id == id)?;
503        entry.runtime = Some(runtime.clone());
504        entry._subscription = Some(subscription);
505        Some(runtime)
506    }
507
508    /// Load a stored thread's history the first time it is opened.
509    fn load_history(&self, id: &str) {
510        let Some(adapter) = self.adapter.clone() else {
511            let _ = self.runtime(id);
512            return;
513        };
514        let remote = {
515            let mut state = self.state.borrow_mut();
516            let Some(entry) = state.entries.iter_mut().find(|e| e.item.id == id) else {
517                return;
518            };
519            match (&entry.item.remote_id, entry.loaded) {
520                (Some(remote), false) => {
521                    entry.loaded = true;
522                    Some(remote.clone())
523                }
524                _ => None,
525            }
526        };
527        let Some(runtime) = self.runtime(id) else {
528            return;
529        };
530        let Some(remote) = remote else { return };
531        let (weak, id) = (self.me.clone(), id.to_owned());
532        self.spawn(async move {
533            let result = adapter.load(&remote).await;
534            let Some(inner) = weak.upgrade() else { return };
535            match result {
536                Ok(history) => {
537                    // A thread the user already started is not overwritten.
538                    if runtime.messages().is_empty() && runtime.import(history).is_err() {
539                        inner.state.borrow_mut().error = Some("could not load the thread".into());
540                    }
541                }
542                Err(error) => {
543                    inner.with_entry(&id, |e| e.loaded = false);
544                    inner.state.borrow_mut().error = Some(error);
545                }
546            }
547            inner.notify();
548        });
549    }
550
551    /// A thread's runtime changed: register it on its first message and ask
552    /// for a title after its first answer.
553    fn thread_changed(&self, id: &str) {
554        let Some(runtime) = self
555            .state
556            .borrow()
557            .entries
558            .iter()
559            .find(|e| e.item.id == id)
560            .and_then(|e| e.runtime.clone())
561        else {
562            return;
563        };
564        let messages = runtime.messages();
565        let running = runtime.is_running();
566        let (register, title_for) = {
567            let mut state = self.state.borrow_mut();
568            let Some(entry) = state.entries.iter_mut().find(|e| e.item.id == id) else {
569                return;
570            };
571            let mut register = false;
572            if entry.item.status == ThreadStatus::New && !messages.is_empty() {
573                entry.item.status = ThreadStatus::Regular;
574                register = !entry.initializing && entry.item.remote_id.is_none();
575                entry.initializing |= register;
576            }
577            let answered = !running
578                && messages
579                    .iter()
580                    .any(|m| m.as_assistant().is_some_and(|a| a.status.is_terminal()));
581            let title_for =
582                (answered && entry.item.title.is_none() && !entry.titled && !entry.initializing)
583                    .then(|| entry.item.remote_id.clone())
584                    .flatten();
585            if title_for.is_some() {
586                entry.titled = true;
587            }
588            (register, title_for)
589        };
590        if register {
591            self.initialize(id);
592        }
593        if let Some(remote) = title_for {
594            self.generate_title(id, &remote, messages.iter().cloned().collect());
595        }
596        self.notify();
597    }
598
599    fn initialize(&self, id: &str) {
600        let Some(adapter) = self.adapter.clone() else {
601            self.with_entry(id, |e| e.initializing = false);
602            return;
603        };
604        let (weak, id) = (self.me.clone(), id.to_owned());
605        self.spawn(async move {
606            let result = adapter.initialize(&id).await;
607            let Some(inner) = weak.upgrade() else { return };
608            match result {
609                Ok(remote) => inner.with_entry(&id, |e| {
610                    e.item.remote_id = Some(remote);
611                    e.initializing = false;
612                }),
613                Err(error) => {
614                    inner.with_entry(&id, |e| e.initializing = false);
615                    inner.state.borrow_mut().error = Some(error);
616                }
617            }
618            // The thread may already have been answered while registering.
619            inner.thread_changed(&id);
620            inner.notify();
621        });
622    }
623
624    fn generate_title(&self, id: &str, remote: &str, messages: Vec<Arc<ThreadMessage>>) {
625        let Some(adapter) = self.adapter.clone() else {
626            return;
627        };
628        let (weak, id, remote) = (self.me.clone(), id.to_owned(), remote.to_owned());
629        self.spawn(async move {
630            let result = adapter.generate_title(&remote, messages).await;
631            let Some(inner) = weak.upgrade() else { return };
632            match result {
633                Ok(Some(title)) => {
634                    inner.with_entry(&id, |e| {
635                        if e.item.title.is_none() {
636                            e.item.title = Some(title.clone());
637                        }
638                    });
639                    let persist = adapter.rename(&remote, &title);
640                    inner.persist(persist, |_| {});
641                }
642                Ok(None) => {}
643                Err(error) => inner.state.borrow_mut().error = Some(error),
644            }
645            inner.notify();
646        });
647    }
648}