1use serde::{Deserialize, Serialize};
5use std::sync::Arc;
6
7use crate::patch_engine::{CompiledArtifact, PatchEngine, RuntimePatch};
8
9pub trait RuntimeHandle: Send + Sync {
11 fn send_patch(&self, patch: RuntimePatch);
13
14 fn request_state(&self) -> RuntimeStateSnapshot;
16
17 fn send_event(&self, event: RuntimeEvent);
19}
20
21pub struct DevRuntimeController {
23 runtime: Arc<dyn RuntimeHandle>,
24 patch_engine: PatchEngine,
25}
26
27impl DevRuntimeController {
28 pub fn new(runtime: Arc<dyn RuntimeHandle>) -> Self {
30 Self {
31 runtime,
32 patch_engine: PatchEngine::new(),
33 }
34 }
35
36 pub fn apply_code_update(&mut self, compiled_artifact: CompiledArtifact) {
38 let patch = self.patch_engine.generate_patch(compiled_artifact);
39 self.runtime.send_patch(patch);
40 }
41
42 pub fn inject_agent_stream(&self, stream: Vec<RuntimeEvent>) {
44 for event in stream {
45 self.runtime.send_event(event);
46 }
47 }
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
52pub enum RuntimeEvent {
53 Agent(AgentEvent),
54 }
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
59pub enum AgentEvent {
60 Token(String),
61 ToolCall(String),
62 StateChange(String),
63 Error(String),
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct RuntimeStateSnapshot {
69 pub data: String,
71}
72
73impl RuntimeStateSnapshot {
74 pub fn new(data: String) -> Self {
75 Self { data }
76 }
77}
78
79use notify::{Event, RecommendedWatcher, RecursiveMode, Watcher};
86use std::path::{Path, PathBuf};
87use std::sync::mpsc::{Receiver, channel};
88use std::time::{Duration, Instant};
89
90pub struct FileWatcher {
92 _watcher: RecommendedWatcher,
93 rx: Receiver<Event>,
94 debounce: Duration,
95 last_event: Option<Instant>,
96 pending_paths: Vec<PathBuf>,
97}
98
99impl FileWatcher {
100 pub fn new(paths: Vec<PathBuf>) -> notify::Result<Self> {
101 let (tx, rx) = channel();
102 let mut watcher =
103 notify::recommended_watcher(move |res: notify::Result<notify::Event>| {
104 if let Ok(event) = res {
105 let _ = tx.send(event);
106 }
107 })?;
108
109 for path in &paths {
110 if path.exists() {
111 watcher.watch(path, RecursiveMode::Recursive)?;
112 }
113 }
114
115 Ok(Self {
116 _watcher: watcher,
117 rx,
118 debounce: Duration::from_millis(300),
119 last_event: None,
120 pending_paths: Vec::new(),
121 })
122 }
123
124 pub fn poll_changes(&mut self) -> Vec<PathBuf> {
126 while let Ok(event) = self.rx.try_recv() {
128 self.last_event = Some(Instant::now());
129 for path in event.paths {
130 if !self.pending_paths.contains(&path) {
131 self.pending_paths.push(path);
132 }
133 }
134 }
135
136 if let Some(last) = self.last_event
138 && last.elapsed() >= self.debounce
139 && !self.pending_paths.is_empty()
140 {
141 let paths = std::mem::take(&mut self.pending_paths);
142 self.last_event = None;
143 return paths;
144 }
145
146 Vec::new()
147 }
148
149 pub fn has_pending_changes(&self) -> bool {
151 !self.pending_paths.is_empty()
152 }
153
154 pub fn debounce_duration(&self) -> Duration {
156 self.debounce
157 }
158}
159
160#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
162pub struct HotReloadState {
163 pub theme_mode: String,
165 pub window_size: (f32, f32),
167 pub scroll_positions: std::collections::HashMap<String, Vec<f32>>,
169 pub input_text: std::collections::HashMap<String, String>,
171 pub expanded_nodes: std::collections::HashMap<String, Vec<usize>>,
173 pub saved_at: f64,
175}
176
177impl Default for HotReloadState {
178 fn default() -> Self {
179 Self {
180 theme_mode: "dark".to_string(),
181 window_size: (1200.0, 800.0),
182 scroll_positions: std::collections::HashMap::new(),
183 input_text: std::collections::HashMap::new(),
184 expanded_nodes: std::collections::HashMap::new(),
185 saved_at: 0.0,
186 }
187 }
188}
189
190impl HotReloadState {
191 pub fn save(&self, path: &Path) -> std::io::Result<()> {
193 let json = serde_json::to_string_pretty(self)?;
194 std::fs::write(path, json)?;
195 Ok(())
196 }
197
198 pub fn load(path: &Path) -> std::io::Result<Self> {
200 let json = std::fs::read_to_string(path)?;
201 let state = serde_json::from_str(&json)?;
202 Ok(state)
203 }
204}
205
206#[derive(Clone, Debug)]
208pub struct ErrorOverlay {
209 pub message: String,
211 pub file: Option<String>,
213 pub line: Option<u32>,
215 pub column: Option<u32>,
217}
218
219impl ErrorOverlay {
220 pub fn from_cargo_output(output: &str) -> Option<Self> {
227 for line in output.lines() {
229 let trimmed = line.trim();
230 if trimmed.is_empty() {
231 continue;
232 }
233 if let Ok(json) = serde_json::from_str::<serde_json::Value>(trimmed) {
234 if json.get("reason").and_then(|r| r.as_str()) == Some("compiler-message")
236 && let Some(message) = json.get("message")
237 {
238 let msg_text = message
239 .get("message")
240 .and_then(|m| m.as_str())
241 .unwrap_or("")
242 .to_string();
243
244 let is_error = message
246 .get("level")
247 .and_then(|l| l.as_str())
248 .map(|l| l == "error")
249 .unwrap_or(false);
250 if !is_error {
251 continue;
252 }
253
254 let (file, line, column) = message
256 .get("spans")
257 .and_then(|s| s.as_array())
258 .and_then(|spans| spans.first())
259 .map(|span| {
260 (
261 span.get("file_name")
262 .and_then(|f| f.as_str())
263 .map(String::from),
264 span.get("line_start")
265 .and_then(|l| l.as_u64())
266 .map(|l| l as u32),
267 span.get("column_start")
268 .and_then(|c| c.as_u64())
269 .map(|c| c as u32),
270 )
271 })
272 .unwrap_or((None, None, None));
273
274 return Some(Self {
275 message: msg_text,
276 file,
277 line,
278 column,
279 });
280 }
281 }
282 }
283
284 for line in output.lines() {
286 let lower = line.to_lowercase();
287 if (lower.contains("error[") || lower.contains("error:"))
288 && !lower.contains("error-handling")
289 {
290 return Some(Self {
291 message: line.to_string(),
292 file: None,
293 line: None,
294 column: None,
295 });
296 }
297 }
298
299 None
300 }
301}