main
rs 192 lines 6.26 KB
Raw
1 //! The commands a webview invokes.
2
3 use std::sync::Arc;
4
5 use ed_agent::{AgentReply, ApprovalDecision, ChatMessage, ChatReply, Ed, EngineInfo, SessionMeta};
6 use tauri::State;
7
8 use crate::sessions::EdSessions;
9 use crate::sink::TauriSink;
10
11 /// Managed state holding the agent.
12 ///
13 /// Register one during `setup` with `app.manage(EdState::new(ed))`. Ed is
14 /// deliberately not a global here: an application that switches accounts, or
15 /// runs more than one window against separate engines, needs to control the
16 /// lifetime, and a `static` takes that away.
17 pub struct EdState {
18 ed: Arc<Ed<TauriSink>>,
19 }
20
21 impl EdState {
22 pub fn new(ed: Ed<TauriSink>) -> Self {
23 Self { ed: Arc::new(ed) }
24 }
25
26 /// The agent, for commands the host defines itself — loading a model,
27 /// setting a system prompt, anything Ed exposes but this crate doesn't
28 /// wrap.
29 pub fn ed(&self) -> &Ed<TauriSink> {
30 &self.ed
31 }
32
33 /// A shared handle to the agent, for work moved onto a spawned task.
34 pub fn shared(&self) -> Arc<Ed<TauriSink>> {
35 self.ed.clone()
36 }
37
38 /// Run an agent turn on a spawned task and deliver the result as a
39 /// `chat_reply` event `{id, session, reply, duration, error}`, with
40 /// `session` empty. Returns immediately.
41 ///
42 /// For single-conversation apps. A long-pending invoke can be dropped by a
43 /// mobile webview, so the answer travels by event. Text deltas arrive as
44 /// `chat_text_delta` while it runs.
45 pub fn submit(&self, app: tauri::AppHandle, id: String, message: String) {
46 use tauri::Emitter;
47 let ed = self.ed.clone();
48 tokio::spawn(async move {
49 let payload = match ed.run(message).await {
50 Ok(reply) => crate::SubmitReplyPayload {
51 id,
52 session: String::new(),
53 reply: Some(reply.text),
54 duration: Some(reply.duration),
55 error: None,
56 },
57 Err(err) => crate::SubmitReplyPayload {
58 id,
59 session: String::new(),
60 reply: None,
61 duration: None,
62 error: Some(err.to_string()),
63 },
64 };
65 if let Err(err) = app.emit(crate::EVENT_CHAT_REPLY, payload) {
66 log::warn!("ed-tauri: could not emit chat_reply: {err}");
67 }
68 });
69 }
70 }
71
72 /// Current status, loaded model, footprint, and history length.
73 #[tauri::command]
74 pub async fn chat_get_status(state: State<'_, EdState>) -> Result<EngineInfo, String> {
75 Ok(state.ed.info().await)
76 }
77
78 /// The conversation so far.
79 #[tauri::command]
80 pub async fn chat_get_history(state: State<'_, EdState>) -> Result<Vec<ChatMessage>, String> {
81 Ok(state.ed.history().await)
82 }
83
84 /// Clear the conversation, returning how many messages were dropped. The model
85 /// stays loaded.
86 #[tauri::command]
87 pub async fn chat_clear_history(state: State<'_, EdState>) -> Result<usize, String> {
88 Ok(state.ed.clear_history().await)
89 }
90
91 /// Run one inference turn.
92 ///
93 /// The reply is also emitted as [`EVENT_CHAT_REPLY`](crate::EVENT_CHAT_REPLY).
94 /// Prefer listening for that over awaiting this call: on a slow model the
95 /// webview can garbage-collect the invoke callback before generation
96 /// finishes, and the answer is lost even though inference succeeded.
97 #[tauri::command]
98 pub async fn chat_send_message(
99 state: State<'_, EdState>,
100 message: String,
101 ) -> Result<ChatReply, String> {
102 Ok(state.ed.send(message).await)
103 }
104
105 /// Run a full agent turn, including any tools the host registered.
106 ///
107 /// Reports progress as it goes: `chat_tool_requested`,
108 /// `chat_approval_requested`, `chat_tool_started`, `chat_tool_finished`, and
109 /// finally `chat_agent_reply`. As with [`chat_send_message`], prefer the event
110 /// over awaiting this call, since an agent turn is slower still.
111 ///
112 /// Unlike [`chat_send_message`] this returns an `Err` on failure rather than
113 /// folding it into the payload: an agent turn has failure modes a chat reply
114 /// doesn't (a model that can't call tools, a cancelled turn, a spent round
115 /// budget), and flattening them all into an error string would throw away the
116 /// distinction the frontend needs to respond to.
117 #[tauri::command]
118 pub async fn chat_run(state: State<'_, EdState>, message: String) -> Result<AgentReply, String> {
119 state
120 .ed
121 .run(message)
122 .await
123 .map_err(|error| error.to_string())
124 }
125
126 /// Cancel the agent turn in progress, if there is one.
127 #[tauri::command]
128 pub async fn chat_cancel(state: State<'_, EdState>) -> Result<(), String> {
129 state.ed.cancel();
130 Ok(())
131 }
132
133 /// Stored sessions, most recently updated first.
134 #[tauri::command]
135 pub async fn chat_list_sessions(state: State<'_, EdSessions>) -> Result<Vec<SessionMeta>, String> {
136 state.list().await.map_err(|e| e.to_string())
137 }
138
139 /// Start an empty session and make it live. Returns its id.
140 #[tauri::command]
141 pub async fn chat_new_session(state: State<'_, EdSessions>) -> Result<String, String> {
142 Ok(state.new_session().await)
143 }
144
145 /// Make `session` live and return its messages.
146 #[tauri::command]
147 pub async fn chat_switch_session(
148 state: State<'_, EdSessions>,
149 session: String,
150 ) -> Result<Vec<ChatMessage>, String> {
151 Ok(state.history(&session).await)
152 }
153
154 #[tauri::command]
155 pub async fn chat_delete_session(
156 state: State<'_, EdSessions>,
157 session: String,
158 ) -> Result<(), String> {
159 state.delete(&session).await.map_err(|e| e.to_string())
160 }
161
162 /// The messages of `session`, without switching to it.
163 #[tauri::command]
164 pub async fn chat_get_session_history(
165 state: State<'_, EdSessions>,
166 session: String,
167 ) -> Result<Vec<ChatMessage>, String> {
168 Ok(state.peek(&session).await)
169 }
170
171 /// Run a turn in `session`. Returns at once; the result is a `chat_reply`
172 /// event carrying `id`.
173 #[tauri::command]
174 pub async fn chat_submit(
175 state: State<'_, EdSessions>,
176 session: String,
177 id: String,
178 message: String,
179 ) -> Result<(), String> {
180 state.submit(&session, &id, message).await;
181 Ok(())
182 }
183
184 /// Answer a `chat_approval_requested` event.
185 #[tauri::command]
186 pub async fn chat_respond_approval(
187 state: State<'_, EdSessions>,
188 request_id: String,
189 decision: ApprovalDecision,
190 ) -> Result<(), String> {
191 state.respond_approval(&request_id, decision).await
192 }