release/1.3.1
rs 104 lines 2.96 KB
Raw
1 //! Bridges Ed's notifications onto Tauri's event bus.
2
3 use ed_agent::{
4 AgentReply, ApprovalRequest, ChatReply, EngineStatus, EventSink, ToolCall, ToolExecutionResult,
5 };
6 use log::warn;
7 use serde::Serialize;
8 use tauri::{AppHandle, Emitter};
9
10 use crate::events::{
11 ChatStatusPayload, TextDeltaPayload, EVENT_CHAT_AGENT_REPLY, EVENT_CHAT_APPROVAL_REQUESTED,
12 EVENT_CHAT_REPLY, EVENT_CHAT_STATUS_CHANGED, EVENT_CHAT_TEXT_DELTA, EVENT_CHAT_TOOL_FINISHED,
13 EVENT_CHAT_TOOL_REQUESTED, EVENT_CHAT_TOOL_STARTED, EVENT_CHAT_WARNING,
14 };
15 use crate::scope::TurnScope;
16
17 /// An [`EventSink`] that emits to the webview.
18 ///
19 /// Emission failures are logged rather than propagated: a closing window
20 /// dropping its listeners is normal, and an inference turn that already
21 /// produced an answer shouldn't be reported as failed because nobody was
22 /// listening.
23 pub struct TauriSink {
24 app: AppHandle,
25 scope: TurnScope,
26 emit_approvals: bool,
27 }
28
29 impl TauriSink {
30 pub fn new(app: AppHandle) -> Self {
31 Self::scoped(app, TurnScope::default(), true)
32 }
33
34 pub(crate) fn scoped(app: AppHandle, scope: TurnScope, emit_approvals: bool) -> Self {
35 Self {
36 app,
37 scope,
38 emit_approvals,
39 }
40 }
41
42 fn emit(&self, event: &str, payload: impl Serialize + Clone) {
43 if let Err(err) = self.app.emit(event, payload) {
44 warn!("ed-tauri: could not emit {event}: {err}");
45 }
46 }
47 }
48
49 impl EventSink for TauriSink {
50 fn status_changed(&self, status: EngineStatus, model_name: Option<&str>, error: Option<&str>) {
51 self.emit(
52 EVENT_CHAT_STATUS_CHANGED,
53 ChatStatusPayload {
54 status,
55 model_name: model_name.map(str::to_owned),
56 error: error.map(str::to_owned),
57 },
58 );
59 }
60
61 fn replied(&self, reply: &ChatReply) {
62 self.emit(EVENT_CHAT_REPLY, reply);
63 }
64
65 fn tool_requested(&self, call: &ToolCall) {
66 self.emit(EVENT_CHAT_TOOL_REQUESTED, call);
67 }
68
69 fn text_delta(&self, delta: &str) {
70 let (session, id) = self.scope.get();
71 self.emit(
72 EVENT_CHAT_TEXT_DELTA,
73 TextDeltaPayload {
74 session,
75 id,
76 delta: delta.to_owned(),
77 },
78 );
79 }
80
81 fn approval_requested(&self, request: &ApprovalRequest) {
82 // `TauriApprovals` emits its own, richer event for the same name.
83 if !self.emit_approvals {
84 return;
85 }
86 self.emit(EVENT_CHAT_APPROVAL_REQUESTED, request);
87 }
88
89 fn tool_started(&self, call: &ToolCall) {
90 self.emit(EVENT_CHAT_TOOL_STARTED, call);
91 }
92
93 fn tool_finished(&self, result: &ToolExecutionResult) {
94 self.emit(EVENT_CHAT_TOOL_FINISHED, result);
95 }
96
97 fn agent_replied(&self, reply: &AgentReply) {
98 self.emit(EVENT_CHAT_AGENT_REPLY, reply);
99 }
100
101 fn warning(&self, message: &str) {
102 self.emit(EVENT_CHAT_WARNING, message);
103 }
104 }