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 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}