Skip to main content

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}