Skip to content

Commit 9df1d7a

Browse files
feat(Handler/Output): Add output channel management handlers for extension RPC
- Implement handlers for `$register`, `$append`, `$clear`, `$replace`, `$reveal`, `$close`, and `$dispose` RPC methods in Mountain backend - Maintain channel state (buffer content, visibility) in `AppState` using mutex-protected HashMap - Emit Tauri events (`output_channel_*`) to synchronize UI state with Sky frontend - Integrate with Track dispatcher to handle extension requests from Cocoon sidecar - Enables VS Code extension output channel functionality through: - Buffer management for extension-generated content - Visibility control mirroring VS Code's Output panel behavior - Lifecycle management matching extension API expectations - Uses structured logging with different levels to balance observability and performance - Implements essential API surface for extensions while maintaining Tauri's thread safety requirements
1 parent eb6a5ff commit 9df1d7a

1 file changed

Lines changed: 360 additions & 0 deletions

File tree

Source/handlers/output.rs

Lines changed: 360 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,360 @@
1+
// ---------------------------------------------------------------------------------------------
2+
// Mountain Output Channel Handlers (handlers/output.rs)
3+
// --------------------------------------------------------------------------------------------
4+
// Manages state and handles RPC requests related to Output Channels created by
5+
// extensions running in sidecars (e.g., Cocoon).
6+
//
7+
// Responsibilities:
8+
// - Handling `$register` RPC calls: Records the existence of a new output
9+
// channel, storing its state (buffer, visibility) in `AppState`.
10+
// - Handling `$append`, `$clear`, `$replace` RPC calls: Modifies the buffer
11+
// associated with a specific channel ID (stored in `AppState`).
12+
// - Handling `$reveal`, `$close` RPC calls: Manages the visibility state of the
13+
// channel.
14+
// - Handling `$dispose` RPC calls: Removes the channel's state from `AppState`.
15+
// - Emitting Tauri events (e.g., `output_channel_append`,
16+
17+
// `output_channel_reveal`) to notify the frontend (Sky) about changes,
18+
19+
// allowing the UI Output panel to update.
20+
//
21+
// Key Interactions:
22+
// - Called by `track::dispatch_sidecar_request` for RPC methods.
23+
// - Interacts with `AppState` via Mutex to manage the `output_channels` map.
24+
// - Emits Tauri events via `AppHandle::emit_all` to communicate with the
25+
// frontend UI.
26+
// --------------------------------------------------------------------------------------------
27+
28+
use std::{
29+
collections::HashMap,
30+
31+
// Use StdMutex if AppState uses it directly
32+
sync::{Arc, Mutex as StdMutex, MutexGuard},
33+
};
34+
35+
// Use log crate
36+
use log;
37+
use serde_json::{Value, json};
38+
use tauri::{AppHandle, Manager, Runtime};
39+
40+
// Import state struct and the AppState itself
41+
use crate::app_state::{AppState, OutputChannelState};
42+
43+
/// Helper to get a locked mutable reference to the output channels map in
44+
/// AppState. Handles potential lock poisoning.
45+
fn get_output_channels_lock<'a, R:Runtime>(
46+
app:&'a AppHandle<R>,
47+
) -> Result<MutexGuard<'a, HashMap<String, OutputChannelState>>, String> {
48+
let state = app.state::<AppState>();
49+
50+
state.output_channels.lock().map_err(|e| {
51+
log::error!("Output channels lock is poisoned: {}", e);
52+
53+
// Return error string
54+
format!("Failed to lock output channels state: {}", e)
55+
})
56+
}
57+
58+
/// Handles the `$register` RPC call.
59+
/// Creates a new output channel state entry.
60+
/// Args: `[name: string, file?: URI | null, languageId?: string | null]`
61+
pub async fn handle_register<R:Runtime>(app:AppHandle<R>, args:Value) -> Result<Value, String> {
62+
let name = args
63+
.get(0)
64+
.and_then(|v| v.as_str())
65+
.ok_or_else(|| "Missing or invalid 'name' (string) argument".to_string())?
66+
.to_string();
67+
68+
let language_id = args.get(2).and_then(|v| v.as_str()).map(|s| s.to_string());
69+
70+
// TODO: Handle file URI (args[1]) if provided for file-backed channels
71+
// Keep: Registration is important log
72+
log::info!("[Output Handler] Register channel: name='{}', langId={:?}", name, language_id);
73+
74+
let mut channels_state = get_output_channels_lock(&app)?;
75+
76+
// Use name as ID for MVP
77+
let channel_id = name.clone();
78+
79+
channels_state
80+
.entry(channel_id.clone())
81+
.or_insert_with(|| OutputChannelState::new(&name, language_id));
82+
83+
// Drop lock before emit
84+
drop(channels_state);
85+
86+
// Keep: Log event emission
87+
let event_payload = json!({"id": channel_id, "name": name});
88+
89+
log::trace!("[Output Handler] Emitting output_channel_registered event: {:?}", event_payload);
90+
91+
app.emit_all("output_channel_registered", event_payload)
92+
.map_err(|e| log::error!("Failed to emit output_channel_registered event: {}", e))
93+
.ok();
94+
95+
// Return the ID used
96+
Ok(json!(channel_id))
97+
}
98+
99+
/// Handles the `$append` RPC call.
100+
/// Appends text to the specified channel's buffer.
101+
/// Args: `[channelId: string, value: string]`
102+
pub async fn handle_append<R:Runtime>(app:AppHandle<R>, args:Value) -> Result<Value, String> {
103+
let channel_id = args
104+
.get(0)
105+
.and_then(|v| v.as_str())
106+
.ok_or_else(|| "Missing or invalid 'channelId' (string) argument".to_string())?;
107+
108+
let value = args.get(1).and_then(|v| v.as_str()).unwrap_or("");
109+
110+
// Reduce logging verbosity for append
111+
log::trace!("[Output Handler] Append to '{}': len={}", channel_id, value.len());
112+
113+
let mut channels_state = get_output_channels_lock(&app)?;
114+
115+
if let Some(channel) = channels_state.get_mut(channel_id) {
116+
channel.buffer.push_str(value);
117+
118+
// TODO: Consider limiting total buffer size
119+
120+
let id_clone = channel_id.to_string();
121+
122+
// Clone value *before* dropping lock if needed by event
123+
let value_clone = value.to_string();
124+
125+
// Drop lock before emitting event
126+
drop(channels_state);
127+
128+
// Keep: Log event emission
129+
let event_payload = json!({"id": id_clone, "value": value_clone});
130+
131+
// log::trace!("[Output Handler] Emitting output_channel_append event: {:?}",
132+
133+
// Can be noisy
134+
// event_payload);
135+
136+
app.emit_all("output_channel_append", event_payload)
137+
.map_err(|e| log::error!("Failed to emit output_channel_append event: {}", e))
138+
.ok();
139+
} else {
140+
log::warn!("[Output Handler] Output channel '{}' not found for append.", channel_id);
141+
142+
// Ensure lock is dropped on error path too
143+
drop(channels_state);
144+
}
145+
// Void operation
146+
Ok(Value::Null)
147+
}
148+
149+
/// Handles the `$clear` RPC call.
150+
/// Clears the buffer of the specified channel.
151+
/// Args: `[channelId: string]`
152+
pub async fn handle_clear<R:Runtime>(app:AppHandle<R>, args:Value) -> Result<Value, String> {
153+
let channel_id = args
154+
.get(0)
155+
.and_then(|v| v.as_str())
156+
.ok_or_else(|| "Missing or invalid 'channelId' (string) argument".to_string())?;
157+
158+
// Keep: Clear is a distinct action
159+
log::info!("[Output Handler] Clear channel: '{}'", channel_id);
160+
161+
let mut channels_state = get_output_channels_lock(&app)?;
162+
163+
if let Some(channel) = channels_state.get_mut(channel_id) {
164+
channel.buffer.clear();
165+
166+
let id_clone = channel_id.to_string();
167+
168+
// Drop lock before emitting event
169+
drop(channels_state);
170+
171+
// Keep: Log event emission
172+
let event_payload = json!({"id": id_clone});
173+
174+
log::trace!("[Output Handler] Emitting output_channel_clear event: {:?}", event_payload);
175+
176+
app.emit_all("output_channel_clear", event_payload)
177+
.map_err(|e| log::error!("Failed to emit output_channel_clear event: {}", e))
178+
.ok();
179+
} else {
180+
log::warn!("[Output Handler] Output channel '{}' not found for clear.", channel_id);
181+
182+
drop(channels_state);
183+
}
184+
// Void operation
185+
Ok(Value::Null)
186+
}
187+
188+
/// Handles the `$replace` RPC call.
189+
/// Replaces the entire buffer content of the specified channel.
190+
/// Args: `[channelId: string, value: string]`
191+
pub async fn handle_replace<R:Runtime>(app:AppHandle<R>, args:Value) -> Result<Value, String> {
192+
let channel_id = args
193+
.get(0)
194+
.and_then(|v| v.as_str())
195+
.ok_or_else(|| "Missing or invalid 'channelId' (string) argument".to_string())?;
196+
197+
let value = args.get(1).and_then(|v| v.as_str()).unwrap_or("");
198+
199+
// Keep: Replace is a distinct action
200+
log::info!("[Output Handler] Replace channel: '{}'", channel_id);
201+
202+
let mut channels_state = get_output_channels_lock(&app)?;
203+
204+
if let Some(channel) = channels_state.get_mut(channel_id) {
205+
channel.buffer = value.to_string();
206+
207+
let id_clone = channel_id.to_string();
208+
209+
let value_clone = value.to_string();
210+
211+
// Drop lock before emitting event
212+
drop(channels_state);
213+
214+
// Keep: Log event emission
215+
let event_payload = json!({"id": id_clone, "value": value_clone});
216+
217+
log::trace!("[Output Handler] Emitting output_channel_replace event: {:?}", event_payload);
218+
219+
app.emit_all("output_channel_replace", event_payload)
220+
.map_err(|e| log::error!("Failed to emit output_channel_replace event: {}", e))
221+
.ok();
222+
} else {
223+
log::warn!("[Output Handler] Output channel '{}' not found for replace.", channel_id);
224+
225+
drop(channels_state);
226+
}
227+
// Void operation
228+
Ok(Value::Null)
229+
}
230+
231+
/// Handles the `$reveal` RPC call.
232+
/// Requests the frontend to show the specified output channel.
233+
/// Args: `[channelId: string, preserveFocus: boolean]`
234+
pub async fn handle_reveal<R:Runtime>(app:AppHandle<R>, args:Value) -> Result<Value, String> {
235+
let channel_id = args
236+
.get(0)
237+
.and_then(|v| v.as_str())
238+
.ok_or_else(|| "Missing or invalid 'channelId' (string) argument".to_string())?;
239+
240+
let preserve_focus = args.get(1).and_then(|v| v.as_bool()).unwrap_or(false);
241+
242+
// Keep: UI action log
243+
log::info!(
244+
"[Output Handler] Reveal channel: '{}', preserveFocus={}",
245+
channel_id,
246+
preserve_focus
247+
);
248+
249+
let mut channels_state = get_output_channels_lock(&app)?;
250+
251+
if let Some(channel) = channels_state.get_mut(channel_id) {
252+
// Update internal state
253+
channel.visible = true;
254+
255+
let id_clone = channel_id.to_string();
256+
257+
// Drop lock before emitting event
258+
drop(channels_state);
259+
260+
// Keep: Log event emission
261+
let event_payload = json!({"id": id_clone, "preserveFocus": preserve_focus });
262+
263+
log::trace!("[Output Handler] Emitting output_channel_reveal event: {:?}", event_payload);
264+
265+
app.emit_all("output_channel_reveal", event_payload)
266+
.map_err(|e| log::error!("Failed to emit output_channel_reveal event: {}", e))
267+
.ok();
268+
} else {
269+
log::warn!("[Output Handler] Output channel '{}' not found for reveal.", channel_id);
270+
271+
drop(channels_state);
272+
}
273+
// Void operation
274+
Ok(Value::Null)
275+
}
276+
277+
/// Handles the `$close` RPC call.
278+
/// Informs the frontend that the channel view can be closed.
279+
/// Args: `[channelId: string]`
280+
pub async fn handle_close<R:Runtime>(app:AppHandle<R>, args:Value) -> Result<Value, String> {
281+
let channel_id = args
282+
.get(0)
283+
.and_then(|v| v.as_str())
284+
.ok_or_else(|| "Missing or invalid 'channelId' (string) argument".to_string())?;
285+
286+
// Keep: UI action log
287+
log::info!("[Output Handler] Close channel requested: '{}'", channel_id);
288+
289+
let mut channels_state = get_output_channels_lock(&app)?;
290+
291+
if let Some(channel) = channels_state.get_mut(channel_id) {
292+
// Update internal state
293+
channel.visible = false;
294+
295+
let id_clone = channel_id.to_string();
296+
297+
// Drop lock before emitting event
298+
drop(channels_state);
299+
300+
// Keep: Log event emission
301+
let event_payload = json!({"id": id_clone });
302+
303+
log::trace!("[Output Handler] Emitting output_channel_close event: {:?}", event_payload);
304+
305+
app.emit_all("output_channel_close", event_payload)
306+
.map_err(|e| log::error!("Failed to emit output_channel_close event: {}", e))
307+
.ok();
308+
} else {
309+
log::warn!(
310+
"[Output Handler] Channel '{}' not found for close (maybe already disposed).",
311+
channel_id
312+
);
313+
314+
drop(channels_state);
315+
}
316+
// Void operation
317+
Ok(Value::Null)
318+
}
319+
320+
/// Handles the `$dispose` RPC call.
321+
/// Removes the channel state entirely from the backend.
322+
/// Args: `[channelId: string]`
323+
pub async fn handle_dispose<R:Runtime>(app:AppHandle<R>, args:Value) -> Result<Value, String> {
324+
let channel_id = args
325+
.get(0)
326+
.and_then(|v| v.as_str())
327+
.ok_or_else(|| "Missing or invalid 'channelId' (string) argument".to_string())?;
328+
329+
// Keep: Lifecycle event log
330+
log::info!("[Output Handler] Dispose channel: '{}'", channel_id);
331+
332+
let mut channels_state = get_output_channels_lock(&app)?;
333+
334+
if channels_state.remove(channel_id).is_some() {
335+
log::info!("[Output Handler] Disposed channel '{}' state.", channel_id);
336+
337+
let id_clone = channel_id.to_string();
338+
339+
// Drop lock before emitting event
340+
drop(channels_state);
341+
342+
// Keep: Log event emission
343+
let event_payload = json!({"id": id_clone });
344+
345+
log::trace!("[Output Handler] Emitting output_channel_disposed event: {:?}", event_payload);
346+
347+
app.emit_all("output_channel_disposed", event_payload)
348+
.map_err(|e| log::error!("Failed to emit output_channel_disposed event: {}", e))
349+
.ok();
350+
} else {
351+
log::warn!(
352+
"[Output Handler] Channel '{}' not found for dispose (maybe already disposed).",
353+
channel_id
354+
);
355+
356+
drop(channels_state);
357+
}
358+
// Void operation
359+
Ok(Value::Null)
360+
}

0 commit comments

Comments
 (0)