Skip to content

Commit 5306dfe

Browse files
feat(handlers/sky_ui_responses): Add Tauri command handler for Sky UI response resolution
- Implements `sky_resolves_ui_request` Tauri command to handle asynchronous UI operation results from Sky frontend - Establishes bridge between Sky UI interactions and Mountain's environment effects using request ID tracking - Manages pending UI requests in AppState with thread-safe Mutex<HashMap> and oneshot channels - Handles three response scenarios: Sky-reported errors, success with data, and cancellations/void operations - Integrates with Land's error system through CommonError::UiInteraction and error_utils formatting - Enables completion of UI provider effects in environment.rs by relaying results back to waiting tasks - Replaces potential per-UI-type handlers with generic solution using structured JSON data - Critical for modal interactions like file dialogs and quick picks in the Cocoon extension host workflow - Includes detailed logging and timeout handling for robustness in Mountain-Sky communication
1 parent 78b8cd7 commit 5306dfe

2 files changed

Lines changed: 259 additions & 0 deletions

File tree

Source/Library.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ pub mod handlers {
5050
pub mod error_utils;
5151

5252
pub mod extension_status;
53+
54+
pub mod sky_ui_responses;
5355
}
5456

5557
pub mod Entry;
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
// ---------------------------------------------------------------------------------------------
2+
// Mountain Sky UI Response Handlers (handlers/sky_ui_responses.rs)
3+
// --------------------------------------------------------------------------------------------
4+
// Contains Tauri command handlers that the Sky frontend invokes to send back
5+
// results from UI interactions (dialogs, quick picks, input boxes) initiated by
6+
// Mountain's UiProvider effects. This module acts as the bridge for
7+
// asynchronous UI operations where Mountain requests a UI action and Sky
8+
// provides the outcome.
9+
//
10+
// Responsibilities:
11+
// - Implementing the `sky_resolves_ui_request` Tauri command.
12+
// - Receiving the `request_id`, `data_val` (on success/cancellation), and
13+
// `error_details_val` (on UI-side error) from Sky.
14+
// - Retrieving the corresponding `oneshot::Sender` from
15+
// `AppState.pending_ui_requests` using the `request_id`.
16+
// - Constructing a `Result<Value, CommonError>` based on the data received from
17+
// Sky.
18+
// - Sending this result back to the waiting task in `environment.rs` (which
19+
// initiated the UI request) via the `oneshot::Sender`.
20+
// - Handling cases where the request might have already timed out on Mountain's
21+
// side.
22+
//
23+
// Key Interactions:
24+
// - Invoked by the Sky frontend via Tauri's `invoke` system.
25+
// - Accesses `AppState.pending_ui_requests` (thread-safely) to find and consume
26+
// the `oneshot::Sender`.
27+
// - Uses `tokio::sync::oneshot::Sender` to communicate results back to async
28+
// tasks in `environment.rs`.
29+
// - Uses `Land_Common::errors::CommonError` for error propagation.
30+
// - Uses `handlers::error_utils` for formatting error strings returned by the
31+
// Tauri command itself (if this handler encounters an internal issue).
32+
// --------------------------------------------------------------------------------------------
33+
34+
use Land_Common::errors::CommonError;
35+
use log::{debug, error, info, trace, warn};
36+
use serde_json::{Value, json};
37+
use tauri::{AppHandle, Manager, Runtime};
38+
39+
// For formatting errors returned by this command itself
40+
use crate::{app_state::AppState, handlers::error_utils};
41+
42+
/// Formats a lock error specifically for the context of this Tauri command
43+
/// failing. This error is for the `Result<(), String>` of the Tauri command
44+
/// itself, not for the `oneshot::Sender`.
45+
fn format_lock_error_for_command<T>(
46+
e:std::sync::PoisonError<std::sync::MutexGuard<'_, T>>,
47+
48+
// e.g., "pending UI requests"
49+
context:&str,
50+
) -> String {
51+
let user_facing_message = format!(
52+
"Internal server error while processing UI response: Could not access shared '{}' state. Please try the UI \
53+
action again or restart the application if the issue persists.",
54+
context
55+
);
56+
57+
let internal_log_message = format!(
58+
"[Sky UI Resp LockErr] CRITICAL: Failed to acquire lock on {} state: {}",
59+
context, e
60+
);
61+
62+
error!("{}", internal_log_message);
63+
64+
error_utils::rpc_error_string(user_facing_message, Some("ELOCKED_UI_RESPONSE_HANDLER"))
65+
}
66+
67+
/// Helper to send the result (or error) via the oneshot sender and log.
68+
/// This communicates the outcome of the UI operation back to the waiting task
69+
/// in `environment.rs`.
70+
fn send_ui_result_to_environment(
71+
request_id:&str,
72+
73+
sender:tokio::sync::oneshot::Sender<Result<Value, CommonError>>,
74+
75+
result_to_send:Result<Value, CommonError>,
76+
) {
77+
let outcome_log = match &result_to_send {
78+
Ok(v) if v.is_null() => "cancellation/no-data",
79+
80+
Ok(_) => "success data",
81+
82+
Err(e) => {
83+
// Log the CommonError that will be sent back
84+
error!(
85+
"[Sky UI Resp] Preparing to send CommonError for ReqID '{}' to environment: {:?}",
86+
request_id, e
87+
);
88+
89+
"error"
90+
},
91+
};
92+
93+
if sender.send(result_to_send).is_err() {
94+
// This means the receiving end of the oneshot channel (in environment.rs) was
95+
// dropped. This usually happens if the UiProvider method in environment.rs
96+
// timed out before Sky could call back with this response.
97+
warn!(
98+
"[Sky UI Resp] For ReqID '{}': Failed to send {} result back to Mountain's waiting task. Receiver dropped \
99+
(likely UiProvider method timed out or task cancelled by Mountain). Sky's response was effectively too \
100+
late or the original request context is gone.",
101+
request_id, outcome_log
102+
);
103+
} else {
104+
info!(
105+
"[Sky UI Resp] For ReqID '{}': Successfully relayed Sky's {} response to Mountain's waiting task in \
106+
environment.rs.",
107+
request_id, outcome_log
108+
);
109+
}
110+
}
111+
112+
/// Generic Tauri command handler for Sky to send back results of any UI
113+
/// interaction initiated by Mountain's UiProvider effects.
114+
///
115+
/// Sky should invoke this command with:
116+
/// - `request_id`: The ID originally sent by Mountain with the UI request
117+
/// event.
118+
/// - `data_val`: `Option<Value>`.
119+
/// - For successful interactions returning data (e.g., selected file paths,
120+
///
121+
/// input string), this should be the data serialized as a
122+
/// `serde_json::Value`.
123+
/// - For cancellations or interactions that don't return data but succeeded
124+
/// (e.g., a simple message box closed), Sky should send `None` or
125+
/// `Value::Null` for this field. `None` is preferred for clarity.
126+
/// - `error_details_val`: `Option<Value>`.
127+
/// - If an error occurred within Sky while processing/displaying the UI, or
128+
/// if the user's action in Sky resulted in an error state defined by Sky,
129+
///
130+
/// Sky should send error details here (e.g., `json!({"message": "UI
131+
/// component failed", "code": "ESKY_ERROR"})`).
132+
/// - If the UI interaction was successful or normally cancelled by the user
133+
/// without error, this should be `None`.
134+
#[tauri::command]
135+
pub async fn sky_resolves_ui_request(
136+
// Automatically injected by Tauri
137+
app_handle:AppHandle,
138+
139+
request_id:String,
140+
141+
data_val:Option<Value>,
142+
143+
error_details_val:Option<Value>,
144+
) -> Result<(), String> {
145+
// This Result<(), String> is for the Tauri command's own execution status
146+
info!(
147+
"[Sky UI Resp] Received UI response for ReqID='{}': DataIsSome={}, ErrorIsSome={}",
148+
request_id,
149+
data_val.is_some(),
150+
error_details_val.is_some()
151+
);
152+
153+
trace!(
154+
"[Sky UI Resp] ReqID='{}': Data='{:?}', Error='{:?}'",
155+
request_id, data_val, error_details_val
156+
);
157+
158+
let app_state = app_handle.state::<AppState>();
159+
160+
let maybe_sender = {
161+
// Scope the lock
162+
let mut pending_guard = app_state
163+
.pending_ui_requests
164+
.lock()
165+
.map_err(|e| format_lock_error_for_command(e, "pending UI requests map"))?;
166+
167+
// Check if the request is still pending before removing.
168+
// This helps avoid warnings if Sky calls back after Mountain has already timed
169+
// out and cleaned up.
170+
if !pending_guard.contains_key(&request_id) {
171+
warn!(
172+
"[Sky UI Resp] No pending UI request found for ReqID '{}' upon checking map (already handled, timed \
173+
out, or invalid ID). Sky's response will be ignored.",
174+
request_id
175+
);
176+
177+
// Command itself processed fine, but no further action needed.
178+
return Ok(());
179+
}
180+
// Remove and get the sender
181+
pending_guard.remove(&request_id)
182+
};
183+
184+
if let Some(sender) = maybe_sender {
185+
let result_to_send:Result<Value, CommonError> = match (data_val, error_details_val) {
186+
// Case 1: Error reported from Sky. This takes precedence over any data.
187+
(_, Some(err_val)) => {
188+
let err_msg_str = err_val
189+
.get("message")
190+
.and_then(Value::as_str)
191+
.unwrap_or_else(|| err_val.as_str().unwrap_or("Unknown error structure reported by Sky UI"))
192+
.to_string();
193+
194+
// Sky might send a code
195+
let err_code_str = err_val.get("code").and_then(Value::as_str);
196+
197+
warn!(
198+
"[Sky UI Resp] ReqID '{}' resolved with an error from Sky: msg='{}', code='{:?}'",
199+
request_id,
200+
err_msg_str,
201+
err_code_str.unwrap_or("N/A")
202+
);
203+
204+
// Package this as a CommonError::UiInteraction to send back to the waiting
205+
// effect
206+
Err(CommonError::UiInteraction(format!(
207+
"Error from Sky UI (ReqID: {}): {} (Code: {})",
208+
request_id,
209+
err_msg_str,
210+
// Provide a default code if Sky doesn't
211+
err_code_str.unwrap_or("SKY_UI_ERROR")
212+
)))
213+
},
214+
215+
// Case 2: Success with data from Sky
216+
(Some(data), None) => {
217+
debug!("[Sky UI Resp] ReqID '{}' resolved successfully with data from Sky.", request_id);
218+
219+
trace!("[Sky UI Resp] ReqID '{}' data: {:?}", request_id, data);
220+
221+
Ok(data)
222+
},
223+
224+
// Case 3: Success with no specific data (e.g., user cancellation, simple ack), or data was explicitly null
225+
(None, None) => {
226+
debug!(
227+
"[Sky UI Resp] ReqID '{}' resolved by Sky with no data and no error (e.g., user cancellation or \
228+
void success).",
229+
request_id
230+
);
231+
232+
// Represent cancellation or no data with Value::Null
233+
Ok(Value::Null)
234+
},
235+
};
236+
237+
send_ui_result_to_environment(&request_id, sender, result_to_send);
238+
} else {
239+
// This case means the sender was not found after the lock was released,
240+
241+
// which should be rare if the contains_key check passed, but good for
242+
// robustness.
243+
warn!(
244+
"[Sky UI Resp] No pending UI request sender found for ReqID '{}' after lock (unexpected state or race). \
245+
Sky's response ignored.",
246+
request_id
247+
);
248+
}
249+
// The Tauri command itself (sky_resolves_ui_request) completed its processing
250+
// successfully.
251+
Ok(())
252+
}
253+
254+
// Specific handlers like sky_resolves_open_dialog are no longer strictly
255+
// necessary if sky_resolves_ui_request is used generically and Sky sends
256+
// appropriate `data_val` structures that the corresponding UiProvider methods
257+
// in environment.rs can parse from Value.

0 commit comments

Comments
 (0)