Mountain/Track/Effect/CreateEffectForRequest/Webview.rs
1#![allow(non_snake_case, unused_variables, dead_code, unused_imports)]
2
3use std::{future::Future, pin::Pin, sync::Arc};
4
5use CommonLibrary::{CustomEditor::CustomEditorProvider::CustomEditorProvider, Environment::Requires::Requires};
6use serde_json::{Value, json};
7use tauri::Runtime;
8use url::Url;
9
10use crate::{
11 IPC::SkyEmit::LogSkyEmit,
12 RunTime::ApplicationRunTime::ApplicationRunTime,
13 Track::Effect::MappedEffectType::MappedEffect,
14 dev_log,
15};
16
17pub fn CreateEffect<R:Runtime>(MethodName:&str, Parameters:Value) -> Option<Result<MappedEffect, String>> {
18 match MethodName {
19 "$webview:create"
20 | "webview.create"
21 | "webview.setHtml"
22 | "webview.setOptions"
23 | "webview.postMessage"
24 | "webview.reveal"
25 | "webview.dispose"
26 | "webview.registerView"
27 | "webview.unregisterView"
28 | "webview.registerCustomEditor"
29 | "webview.unregisterCustomEditor" => {
30 // Per-dispatch entry line - parity with TreeView.rs's
31 // `tree-latency` log. Without this we cannot tell from
32 // `Mountain.dev.log` whether Cocoon's
33 // `MountainClient.sendRequest("webview.registerView", ...)`
34 // even reached `DispatchSideCarRequest` - silent gRPC drops
35 // look identical to "extension never called the shim".
36 dev_log!("ipc", "[WebviewEffect] dispatch-enter method={}", MethodName);
37 let Method = MethodName.to_string();
38 let effect =
39 move |run_time:Arc<ApplicationRunTime>| -> Pin<Box<dyn Future<Output = Result<Value, String>> + Send>> {
40 let Method = Method.clone();
41 Box::pin(async move {
42 let RawSuffix = Method.trim_start_matches("$webview:").trim_start_matches("webview.");
43 // SkyBridge's webview listener registry uses
44 // kebab-case for `set-html` and `post-message`
45 // (canonical channel name in
46 // `Common/Source/IPC/SkyEvent.rs::WebviewSetHTML`),
47 // but the Cocoon-side wire method uses camelCase
48 // (`webview.setHtml`, `webview.postMessage`).
49 // Without this translation, Roo / claude-vscode /
50 // any extension that calls
51 // `webview.html = "<html>"` emitted on
52 // `sky://webview/setHtml` and Sky's listener
53 // (registered on `set-html`) silently dropped
54 // every payload - the panel rendered the chrome
55 // but the iframe stayed blank. Same fix the
56 // `Vine/Server/Notification/WebviewLifecycle.rs`
57 // path already applies; centralise here so both
58 // emit paths land on the same canonical channel.
59 // `postMessage` Sky has BOTH listeners (camel +
60 // kebab) so either works there, but normalise to
61 // kebab for consistency.
62 let Suffix:&str = match RawSuffix {
63 "setHtml" => "set-html",
64 "postMessage" => "post-message",
65 Other => Other,
66 };
67 // Payload-shape canonicalisation. Cocoon's
68 // `WindowNamespace.ts` calls
69 // `Context.SendToMountain("webview.setHtml",
70 // { handle, viewId, html })` for webview-views
71 // (Roo, claude-vscode sidebars) and
72 // `MountainClient.sendRequest("webview.setHtml",
73 // [Handle, Value])` for webview-panels (legacy).
74 // SkyBridge's `sky://webview/set-html` listener
75 // reads `Payload.viewId` and `Payload.html`
76 // directly, so we always emit the named-key
77 // shape. Three observed wire shapes:
78 // 1. `Parameters` IS the object directly (modern named-arg sendRequest).
79 // 2. `Parameters` is `[ <object> ]` (array wrap).
80 // 3. `Parameters` is `[ Handle, Value ]` (positional, panel path).
81 // The previous code wrapped payloads in
82 // `{ method, handle, args }` which made
83 // `Payload.viewId === undefined`; the listener
84 // returned early and the iframe stayed blank.
85 // Add a `name`/`viewId` fallback step too so
86 // case-1 payloads that only carry `handle` still
87 // reach Sky's registry lookup (Sky maintains a
88 // handle→view map under
89 // `__CEL_WEBVIEW_VIEWS_BY_HANDLE__`).
90 let Payload:Value = if Parameters.is_object() {
91 // Case 1: object directly. Pass through.
92 Parameters.clone()
93 } else if let Some(First) = Parameters.get(0) {
94 if First.is_object() {
95 // Case 2: array-wrapped object. Unwrap.
96 First.clone()
97 } else if let Some(Second) = Parameters.get(1) {
98 // Case 3: positional [Handle, Value].
99 // `value` covers setHtml's html string,
100 // setOptions' options object,
101 // postMessage's message, etc. Also expose
102 // `html` and `message` aliases since
103 // SkyBridge's listeners read those keys
104 // directly (mirrors Cocoon's
105 // `webview.html = ...` / `webview.postMessage(msg)`
106 // invocation surface).
107 let MutableObject = match Method.as_str() {
108 "webview.setHtml" => {
109 json!({
110 "method": Method,
111 "handle": First,
112 "html": Second,
113 })
114 },
115 "webview.postMessage" => {
116 json!({
117 "method": Method,
118 "handle": First,
119 "message": Second,
120 })
121 },
122 _ => {
123 json!({
124 "method": Method,
125 "handle": First,
126 "value": Second,
127 })
128 },
129 };
130 MutableObject
131 } else {
132 // Single positional arg (dispose, reveal).
133 json!({
134 "method": Method,
135 "handle": First,
136 })
137 }
138 } else {
139 json!({
140 "method": Method,
141 "handle": Parameters.clone(),
142 })
143 };
144 let EventName = format!("sky://webview/{}", Suffix);
145 // `LogSkyEmit` wraps `.emit()` and tags every
146 // success/failure under `[DEV:SKY-EMIT]`, so
147 // the webview channel becomes visible in the
148 // SkyEmit histogram alongside SCM and tree-view.
149 // The bare `.emit()` was invisible, so a silent
150 // listener-side drop in Sky was indistinguishable
151 // from "Mountain never received the request".
152 if let Err(Error) = LogSkyEmit(&run_time.Environment.ApplicationHandle, &EventName, &Payload) {
153 dev_log!("ipc", "warn: [WebviewEffect] emit {} failed: {}", EventName, Error);
154 }
155 Ok(json!(null))
156 })
157 };
158 Some(Ok(Box::new(effect)))
159 },
160
161 "$resolveCustomEditor" => {
162 let effect =
163 move |run_time:Arc<ApplicationRunTime>| -> Pin<Box<dyn Future<Output = Result<Value, String>> + Send>> {
164 Box::pin(async move {
165 let provider:Arc<dyn CustomEditorProvider> = run_time.Environment.Require();
166 let view_type = Parameters.get(0).and_then(Value::as_str).unwrap_or("").to_string();
167 let resource_uri_str = Parameters.get(1).and_then(Value::as_str).unwrap_or("");
168 let resource_uri = Url::parse(resource_uri_str)
169 .unwrap_or_else(|_| Url::parse("file:///tmp/test.txt").unwrap());
170 let webview_handle =
171 Parameters.get(2).and_then(Value::as_str).unwrap_or("webview-123").to_string();
172 provider
173 .ResolveCustomEditor(view_type, resource_uri, webview_handle)
174 .await
175 .map(|_| json!(null))
176 .map_err(|e| e.to_string())
177 })
178 };
179 Some(Ok(Box::new(effect)))
180 },
181
182 _ => None,
183 }
184}