Skip to main content

llimorse_chat/
ui.rs

1//! UI connector for llimorse-chat UIs
2
3use crate::ChatHistory;
4use llimorse::client::{ClientInfo, ClientState};
5use parking_lot::RwLock;
6use std::fmt;
7use std::sync::{Arc, Mutex};
8use tokio::sync::mpsc;
9
10/// UI state for interacting with the llimorse-chat application
11#[allow(async_fn_in_trait)]
12pub trait UiState {
13    /// Error type for the implementing `struct`.
14    type Error: Into<anyhow::Error> + Send + Sync + 'static;
15
16    /// Await an event on the UI.
17    async fn get_event(&mut self) -> Result<Event, Self::Error>;
18
19    /// Notify the UI about something.
20    fn notify(&mut self, notification: Notification) -> Result<(), Self::Error>;
21}
22
23/// Application state level events that can come from the UI
24#[derive(Clone, Debug, Eq, PartialEq)]
25pub enum Event {
26    /// Exit requested
27    Exit,
28
29    /// User submitted a message as input
30    Input(String),
31
32    /// Force submitting all queued prompts *right now*
33    ForceSubmitQueued,
34
35    /// Submit the current state without a new user message (e.g. continue a resumed session)
36    Continue,
37}
38
39/// ID of a subagent, unique within one application
40#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
41pub struct SubagentId(
42    /// The raw ID
43    usize,
44);
45
46impl SubagentId {
47    /// Create a new subagent ID from the given integer.
48    pub const fn new(id: usize) -> Self {
49        Self(id)
50    }
51}
52
53impl fmt::Display for SubagentId {
54    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55        write!(f, "{}", self.0)
56    }
57}
58
59/// ID of an agent: the main agent or one of its subagents
60#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
61pub enum AgentId {
62    /// The main agent
63    Main,
64
65    /// One of the main agent’s subagents
66    Subagent(SubagentId),
67}
68
69/// Notifications to the UI
70#[derive(Debug)]
71pub enum Notification {
72    /// Exit requested
73    Exit,
74
75    /// Update the interface
76    Update,
77
78    /// The given agent’s history has been updated
79    UpdateAgent {
80        /// The ID of the agent whose history was updated
81        agent_id: AgentId,
82    },
83
84    /// User message queued to be submitted to the LLM
85    PromptQueued(String),
86
87    /// User message has been submitted to the LLM
88    PromptSubmitted,
89
90    /// The current prompt iteration is complete; the agent is now awaiting a new prompt
91    AwaitingPrompt {
92        /// Output of the just-completed prompt iteration (if any)
93        response: Option<String>,
94    },
95
96    /// A tool call is requesting permission from the user
97    RequestPermission {
98        /// The prompt displayed to the user
99        prompt: String,
100
101        /// The user's decision, to be sent back across this channel
102        approval: tokio::sync::oneshot::Sender<std::result::Result<(), String>>,
103    },
104
105    /// A new subagent has been created
106    SubagentCreated {
107        /// Unique ID by which the subagent can be identified
108        subagent_id: SubagentId,
109
110        /// Prompt for the subagent
111        prompt: String,
112
113        /// The immutable information of the client to which the subagent is connected
114        client_info: ClientInfo,
115
116        /// The state of the client to which the subagent is connected
117        client_state: Arc<RwLock<ClientState>>,
118
119        /// Subagent’s chat history
120        chat_history: Arc<Mutex<ChatHistory>>,
121    },
122
123    /// A subagent is done and has been dropped
124    SubagentDropped {
125        /// The subagent’s ID
126        subagent_id: SubagentId,
127    },
128}
129
130/// A handle to the UI-notification channel, which can be created before [`App`](crate::App).
131///
132/// The sender side is cloneable, so it can be shared with objects that are created before the
133/// application, such as [`UserToolGate`](crate::tools::UserToolGate).
134/// [`App::new()`](crate::App::new()) consumes this object, taking the receiver side into itself.
135pub struct NotificationChannel {
136    /// The sender side (cloneable)
137    sender: mpsc::UnboundedSender<Notification>,
138
139    /// The receiver side (consumed by [`App`](crate::App))
140    receiver: mpsc::UnboundedReceiver<Notification>,
141}
142
143impl Default for NotificationChannel {
144    fn default() -> Self {
145        Self::new()
146    }
147}
148
149impl NotificationChannel {
150    /// Create a new UI-notification channel.
151    pub fn new() -> Self {
152        let (sender, receiver) = mpsc::unbounded_channel();
153        Self { sender, receiver }
154    }
155
156    /// Clone the sender, for objects that need to notify the UI.
157    pub fn sender(&self) -> mpsc::UnboundedSender<Notification> {
158        self.sender.clone()
159    }
160
161    /// For [`App::new()`](crate::App::new()): Consume and get the receiving end
162    pub(crate) fn into_receiver(self) -> mpsc::UnboundedReceiver<Notification> {
163        self.receiver
164    }
165}