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}