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 {
98 // Case 3: positional `[Handle, Second?, ...]`.
99 //
100 // SkyBridge's listeners are split between
101 // two reading idioms:
102 // - Named keys: `Payload.viewId`, `Payload.html`, `Payload.message`
103 // (set-html, post-message, register/unregisterView).
104 // - Positional `Payload.args[N]`: create (`[Handle, ViewType, Title,
105 // ShowOptions, Options]`), registerCustomEditor (`[Handle, ViewType,
106 // Options]`), setOptions, reveal, dispose.
107 //
108 // Always preserve the original args array AND
109 // add the per-method named alias so a
110 // listener using either idiom finds its data.
111 // Without the alias every
112 // `registerWebviewViewProvider` call from
113 // Cocoon emitted a payload whose
114 // `viewId === undefined`, the listener
115 // early-returned, and the workbench's
116 // `IWebviewViewService` registry stayed empty
117 // - every extension sidebar (Roo, Codex,
118 // gitlens, claude-code, dashboard) painted
119 // only the `pre/index.html` chrome with no
120 // host content.
121 let mut Object = serde_json::Map::new();
122 Object.insert("method".to_string(), Value::String(Method.clone()));
123 Object.insert("handle".to_string(), First.clone());
124 Object.insert("args".to_string(), Parameters.clone());
125 if let Some(Second) = Parameters.get(1) {
126 let Alias = match Method.as_str() {
127 "webview.setHtml" => "html",
128 "webview.postMessage" => "message",
129 "webview.registerView" | "webview.unregisterView" => "viewId",
130 "webview.registerCustomEditor"
131 | "webview.unregisterCustomEditor"
132 | "webview.create" => "viewType",
133 _ => "value",
134 };
135 Object.insert(Alias.to_string(), Second.clone());
136 // `webview.create`'s args slot 2 is the
137 // extension-supplied panel title.
138 // SkyBridge's `first-create` diagnostic
139 // surfaces it under `Payload.title`;
140 // mirror that here so the named-key
141 // idiom doesn't lag the positional one.
142 if Method.as_str() == "webview.create" {
143 if let Some(Third) = Parameters.get(2) {
144 Object.insert("title".to_string(), Third.clone());
145 }
146 }
147 }
148 Value::Object(Object)
149 }
150 } else {
151 json!({
152 "method": Method,
153 "handle": Parameters.clone(),
154 })
155 };
156 let EventName = format!("sky://webview/{}", Suffix);
157 // `LogSkyEmit` wraps `.emit()` and tags every
158 // success/failure under `[DEV:SKY-EMIT]`, so
159 // the webview channel becomes visible in the
160 // SkyEmit histogram alongside SCM and tree-view.
161 // The bare `.emit()` was invisible, so a silent
162 // listener-side drop in Sky was indistinguishable
163 // from "Mountain never received the request".
164 if let Err(Error) = LogSkyEmit(&run_time.Environment.ApplicationHandle, &EventName, &Payload) {
165 dev_log!("ipc", "warn: [WebviewEffect] emit {} failed: {}", EventName, Error);
166 }
167 Ok(json!(null))
168 })
169 };
170 Some(Ok(Box::new(effect)))
171 },
172
173 "$resolveCustomEditor" => {
174 let effect =
175 move |run_time:Arc<ApplicationRunTime>| -> Pin<Box<dyn Future<Output = Result<Value, String>> + Send>> {
176 Box::pin(async move {
177 let provider:Arc<dyn CustomEditorProvider> = run_time.Environment.Require();
178 let view_type = Parameters.get(0).and_then(Value::as_str).unwrap_or("").to_string();
179 let resource_uri_str = Parameters.get(1).and_then(Value::as_str).unwrap_or("");
180 let resource_uri = Url::parse(resource_uri_str)
181 .unwrap_or_else(|_| Url::parse("file:///tmp/test.txt").unwrap());
182 let webview_handle =
183 Parameters.get(2).and_then(Value::as_str).unwrap_or("webview-123").to_string();
184 provider
185 .ResolveCustomEditor(view_type, resource_uri, webview_handle)
186 .await
187 .map(|_| json!(null))
188 .map_err(|e| e.to_string())
189 })
190 };
191 Some(Ok(Box::new(effect)))
192 },
193
194 _ => None,
195 }
196}