Skip to main content

Mountain/Vine/Server/
MountainVinegRPCService.rs

1//! # MountainVinegRPCService
2//!
3//! Defines the gRPC service implementation for Mountain. This struct handles
4//! incoming RPC calls from the `Cocoon` sidecar, dispatches them to the
5//! application's core logic via the `Track` module, and returns the results.
6//!
7//! ## Service Methods
8//!
9//! - **process_cocoon_request**: Handles request-response calls from Cocoon
10//! - **send_cocoon_notification**: Handles fire-and-forget notifications from
11//!   Cocoon
12//! - **cancel_operation**: Cancels long-running operations requested by Cocoon
13//!
14//! ## Request Processing
15//!
16//! 1. Deserialize JSON parameters from request
17//! 2. Validate method name and parameters
18//! 3. Dispatch request to Track::DispatchLogic
19//! 4. Serialize response or error
20//! 5. Return gRPC response with proper status codes
21//!
22//! ## Error Handling
23//!
24//! All errors are converted to JSON-RPC compliant Error objects:
25//! - Parse errors: code -32700
26//! - Server errors: code -32000
27//! - Method not found: code -32601
28//! - Invalid params: code -32602
29//!
30//! ## Security
31//!
32//! - Parameter validation before processing
33//! - Message size limits enforced
34//! - Method name sanitization
35//! - Safe error messages (no sensitive data)
36
37use std::{collections::HashMap, sync::Arc};
38
39use serde_json::{Value, json};
40use tauri::{AppHandle, Emitter};
41use tokio::sync::RwLock;
42use tonic::{Request, Response, Status};
43
44use crate::{
45	RunTime::ApplicationRunTime::ApplicationRunTime,
46	Track,
47	Vine::Generated::{
48		CancelOperationRequest,
49		Empty,
50		GenericNotification,
51		GenericRequest,
52		GenericResponse,
53		RpcError as RPCError,
54		mountain_service_server::MountainService,
55	},
56	dev_log,
57};
58
59/// Configuration for MountainService
60#[allow(dead_code)]
61mod ServiceConfig {
62	/// Maximum number of concurrent operations
63	pub const MAX_CONCURRENT_OPERATIONS:usize = 50;
64
65	/// Default timeout for operation cancellation
66	pub const CANCELLATION_TIMEOUT_MS:u64 = 5000;
67
68	/// Maximum method name length
69	pub const MAX_METHOD_NAME_LENGTH:usize = 128;
70}
71
72/// The concrete implementation of the `MountainService` gRPC service.
73///
74/// This service handles all incoming RPC calls from the Cocoon sidecar,
75/// validating requests, dispatching to appropriate handlers, and returning
76/// responses in the expected gRPC format.
77pub struct MountainVinegRPCService {
78	/// Tauri application handle for VS Code integration
79	ApplicationHandle:AppHandle,
80
81	/// Application runtime containing core dependencies
82	RunTime:Arc<ApplicationRunTime>,
83
84	/// Registry of active operations with their cancellation tokens
85	/// Maps request ID to cancellation token for operation cancellation
86	ActiveOperations:Arc<RwLock<HashMap<u64, tokio_util::sync::CancellationToken>>>,
87}
88
89impl MountainVinegRPCService {
90	/// Accessor for Tauri `AppHandle` - used by the per-wire-method atoms
91	/// in `Vine::Server::Notification::*` that need to emit
92	/// `sky://` / `cocoon:*` events downstream. Kept as a thin read so the
93	/// struct's fields can stay private; atoms should never mutate the
94	/// handle, only `emit` through it.
95	pub fn ApplicationHandle(&self) -> &AppHandle { &self.ApplicationHandle }
96
97	/// Accessor for the shared `ApplicationRunTime`. Notification atoms
98	/// reach `Environment.ApplicationState.*` (provider registry, extension
99	/// registry, scheduler) through this. Clone from `Arc` when the atom
100	/// needs to keep it across an `.await` boundary.
101	pub fn RunTime(&self) -> &Arc<ApplicationRunTime> { &self.RunTime }
102}
103
104impl MountainVinegRPCService {
105	/// Creates a new instance of the Mountain gRPC service.
106	///
107	/// # Parameters
108	/// - `ApplicationHandle`: Tauri app handle for framework integration
109	/// - `RunTime`: Application runtime with core dependencies
110	///
111	/// # Returns
112	/// New MountainVinegRPCService instance
113	pub fn Create(ApplicationHandle:AppHandle, RunTime:Arc<ApplicationRunTime>) -> Self {
114		dev_log!("grpc", "[MountainVinegRPCService] New instance created");
115
116		Self {
117			ApplicationHandle,
118			RunTime,
119			ActiveOperations:Arc::new(RwLock::new(HashMap::new())),
120		}
121	}
122
123	/// Registers an operation for potential cancellation
124	///
125	/// # Parameters
126	/// - `request_id`: The request identifier for the operation
127	///
128	/// # Returns
129	/// A cancellation token that can be used to cancel the operation
130	pub async fn RegisterOperation(&self, request_id:u64) -> tokio_util::sync::CancellationToken {
131		let token = tokio_util::sync::CancellationToken::new();
132		self.ActiveOperations.write().await.insert(request_id, token.clone());
133		dev_log!(
134			"grpc",
135			"[MountainVinegRPCService] Registered operation {} for cancellation",
136			request_id
137		);
138		token
139	}
140
141	/// Unregisters an operation after completion
142	///
143	/// # Parameters
144	/// - `request_id`: The request identifier to unregister
145	pub async fn UnregisterOperation(&self, request_id:u64) {
146		self.ActiveOperations.write().await.remove(&request_id);
147		dev_log!("grpc", "[MountainVinegRPCService] Unregistered operation {}", request_id);
148	}
149
150	/// Validates a generic request before processing.
151	///
152	/// # Parameters
153	/// - `request`: The request to validate
154	///
155	/// # Returns
156	/// - `Ok(())`: Request is valid
157	/// - `Err(Status)`: Validation failed with appropriate gRPC status
158	fn ValidateRequest(&self, request:&GenericRequest) -> Result<(), Status> {
159		// Validate method name
160		if request.method.is_empty() {
161			return Err(Status::invalid_argument("Method name cannot be empty"));
162		}
163
164		if request.method.len() > ServiceConfig::MAX_METHOD_NAME_LENGTH {
165			return Err(Status::invalid_argument(format!(
166				"Method name exceeds maximum length of {} characters",
167				ServiceConfig::MAX_METHOD_NAME_LENGTH
168			)));
169		}
170
171		// Validate parameter size (rough estimate using JSON bytes)
172		if request.parameter.len() > 4 * 1024 * 1024 {
173			return Err(Status::resource_exhausted("Request parameter size exceeds limit"));
174		}
175
176		// Check for potentially malicious method names
177		if request.method.contains("../") || request.method.contains("::") {
178			return Err(Status::permission_denied("Invalid method name format"));
179		}
180
181		Ok(())
182	}
183
184	/// Creates a JSON-RPC compliant error response.
185	///
186	/// # Parameters
187	/// - `RequestIdentifier`: The request ID to echo back
188	/// - `code`: JSON-RPC error code
189	/// - `message`: Error message
190	/// - `data`: Optional error data (serialized)
191	///
192	/// # Returns
193	/// GenericResponse with error populated
194	fn CreateErrorResponse(RequestIdentifier:u64, code:i32, message:String, data:Option<Vec<u8>>) -> GenericResponse {
195		GenericResponse {
196			request_identifier:RequestIdentifier,
197			result:vec![],
198			error:Some(RPCError { code, message, data:data.unwrap_or_default() }),
199		}
200	}
201
202	/// Creates a successful JSON-RPC response.
203	///
204	/// # Parameters
205	/// - `RequestIdentifier`: The request ID to echo back
206	/// - `result`: Result value to serialize
207	///
208	/// # Returns
209	/// GenericResponse with result populated, or error if serialization fails
210	fn CreateSuccessResponse(RequestIdentifier:u64, result:&Value) -> GenericResponse {
211		let result_bytes = match serde_json::to_vec(result) {
212			Ok(bytes) => bytes,
213			Err(e) => {
214				dev_log!("grpc", "error: [MountainVinegRPCService] Failed to serialize result: {}", e);
215
216				// Return error response instead
217				return Self::CreateErrorResponse(
218					RequestIdentifier,
219					-32603, // Internal error
220					"Failed to serialize response".to_string(),
221					None,
222				);
223			},
224		};
225
226		GenericResponse { request_identifier:RequestIdentifier, result:result_bytes, error:None }
227	}
228}
229
230#[tonic::async_trait]
231impl MountainService for MountainVinegRPCService {
232	// LAND-PATCH B7-S6 P2: bidirectional streaming channel.
233	// Stub for now - the multiplexer that drains incoming Envelopes
234	// and dispatches to the unary handler tree is implemented in a
235	// follow-up patch (Patch 14). Until then this returns
236	// `Unimplemented` so callers fall back to the unary path.
237	type OpenChannelFromCocoonStream = std::pin::Pin<
238		Box<
239			dyn tonic::codegen::tokio_stream::Stream<Item = Result<crate::Vine::Generated::Envelope, tonic::Status>>
240				+ Send
241				+ 'static,
242		>,
243	>;
244
245	async fn open_channel_from_cocoon(
246		&self,
247		_request:tonic::Request<tonic::Streaming<crate::Vine::Generated::Envelope>>,
248	) -> Result<tonic::Response<Self::OpenChannelFromCocoonStream>, tonic::Status> {
249		Err(tonic::Status::unimplemented(
250			"OpenChannelFromCocoon: streaming multiplexer not yet wired (Patch 14); use unary endpoints",
251		))
252	}
253
254	/// Handles generic request-response RPCs from Cocoon.
255	///
256	/// This is the main entry point for Cocoon to request operations from
257	/// Mountain. It validates the request, deserializes parameters, dispatches
258	/// to the Track module, and returns the result or error in JSON-RPC
259	/// format.
260	///
261	/// # Parameters
262	/// - `request`: GenericRequest containing method name and serialized
263	///   parameters
264	///
265	/// # Returns
266	/// - `Ok(Response<GenericResponse>)`: Response with result or error
267	/// - `Err(Status)`: gRPC status error (only for critical failures)
268	async fn process_cocoon_request(
269		&self,
270		request:Request<GenericRequest>,
271	) -> Result<Response<GenericResponse>, Status> {
272		let RequestData = request.into_inner();
273
274		let MethodName = RequestData.method.clone();
275
276		let RequestIdentifier = RequestData.request_identifier;
277		let ReceiveInstant = std::time::Instant::now();
278
279		// Per-call receive line is pure noise at the `grpc` tag - one line
280		// per request × thousands of requests. Move under `grpc-verbose`
281		// so the cheap default is quiet; failures are logged by the
282		// dispatch path regardless.
283		dev_log!(
284			"grpc-verbose",
285			"[MountainVinegRPCService] Received gRPC Request [ID: {}]: Method='{}'",
286			RequestIdentifier,
287			MethodName
288		);
289
290		// Hot-path instrumentation (BATCH-16). Every RPC that shows up with
291		// uniform 700 ms latency (tree.register, Configuration.Inspect,
292		// Command.Execute) emits a `[LandFix:RPC]` marker here so p50/p95 can
293		// be derived from the log without patching every handler. The
294		// monotonic `t_ns` is a `SystemTime::UNIX_EPOCH` offset so Cocoon's
295		// `process.hrtime.bigint()` wire-send stamp can be diffed into three
296		// hops: wire → grpc-recv (transit), grpc-recv → dispatch-enter
297		// (Track resolve), dispatch-enter → registered (handler body).
298		let IsHotRpc = matches!(
299			MethodName.as_str(),
300			"$tree:register" | "tree.register" | "Configuration.Inspect" | "Command.Execute"
301		);
302		if IsHotRpc {
303			let InstrumentRecvNs = std::time::SystemTime::now()
304				.duration_since(std::time::UNIX_EPOCH)
305				.map(|D| D.as_nanos())
306				.unwrap_or(0);
307			// Per-call receive timestamp for latency diagnosis - only
308			// useful when actively profiling. Gate under `rpc-latency`
309			// so `short` / `grpc` don't print it.
310			dev_log!(
311				"rpc-latency",
312				"[LandFix:RPC] grpc-recv method={} id={} size={} t_ns={}",
313				MethodName,
314				RequestIdentifier,
315				RequestData.parameter.len(),
316				InstrumentRecvNs
317			);
318		}
319
320		// Validate request before processing
321		if let Err(status) = self.ValidateRequest(&RequestData) {
322			dev_log!("grpc", "warn: [MountainVinegRPCService] Request validation failed: {}", status);
323
324			return Ok(Response::new(Self::CreateErrorResponse(
325				RequestIdentifier,
326				-32602, // Invalid params
327				status.message().to_string(),
328				None,
329			)));
330		}
331
332		// Deserialize JSON parameters
333		let ParametersValue:Value = match serde_json::from_slice(&RequestData.parameter) {
334			Ok(v) => {
335				// The previous `{:?}` Debug format serialised the full
336				// `Value` on every request - cheap for small payloads
337				// (`Diagnostic.Clear`), catastrophic for `tree.register` and
338				// `Configuration.Inspect` whose options blobs walk recursive
339				// structures. Only log param size at the default dev-log
340				// level and let the DevLog `all` target surface the body if
341				// the caller opts in.
342				// One line per request with only the byte count (the body is
343				// too large / PII-risky to log unconditionally). The size
344				// alone is already in Cocoon's `Request metrics:` line under
345				// `grpc-verbose`, so duplicate here adds noise without
346				// signal. Route to `grpc-verbose`.
347				dev_log!(
348					"grpc-verbose",
349					"[MountainVinegRPCService] Params for [ID: {}] ({} bytes)",
350					RequestIdentifier,
351					RequestData.parameter.len()
352				);
353				v
354			},
355			Err(e) => {
356				let msg = format!("Failed to deserialize parameters for method '{}': {}", MethodName, e);
357
358				dev_log!("grpc", "error: {}", msg);
359
360				return Ok(Response::new(Self::CreateErrorResponse(
361					RequestIdentifier,
362					-32700, // Parse error
363					msg,
364					None,
365				)));
366			},
367		};
368
369		dev_log!(
370			"grpc-verbose",
371			"[MountainVinegRPCService] Dispatching request [ID: {}] to Track::DispatchLogic",
372			RequestIdentifier
373		);
374
375		// Dispatch request to Track module for processing
376		let DispatchResult = Track::SideCarRequest::DispatchSideCarRequest::DispatchSideCarRequest(
377			self.ApplicationHandle.clone(),
378			self.RunTime.clone(),
379			// In the future, this could come from connection metadata
380			"cocoon-main".to_string(),
381			MethodName.clone(),
382			ParametersValue,
383		)
384		.await;
385
386		match DispatchResult {
387			Ok(SuccessfulResult) => {
388				if IsHotRpc {
389					// Hot-RPC dispatched latency line - already narrow
390					// (~50 tagged RPCs per session). Route to `rpc-latency`
391					// so the profiling context stays opt-in.
392					dev_log!(
393						"rpc-latency",
394						"[LandFix:RPC] dispatched method={} id={} elapsed={}ms",
395						MethodName,
396						RequestIdentifier,
397						ReceiveInstant.elapsed().as_millis()
398					);
399				}
400				// Success completion fires per request (14k+ in long sessions).
401				// Failures still log under the unconditional `error:` path
402				// below, so routing this to `grpc-verbose` doesn't hide real
403				// problems.
404				dev_log!(
405					"grpc-verbose",
406					"[MountainVinegRPCService] Request [ID: {}] completed successfully",
407					RequestIdentifier
408				);
409
410				Ok(Response::new(Self::CreateSuccessResponse(RequestIdentifier, &SuccessfulResult)))
411			},
412
413			Err(ErrorString) => {
414				// Routine 404s - extensions probe for optional workspace
415				// files on activate:
416				//   - `FileSystem.ReadFile` → missing cache files (terminal-suggest, JSON
417				//     schema associations, composer.json, Gemfile.lock, Drupal.php).
418				//   - `FileSystem.Stat` → optional config probes.
419				// Both surface as "resource not found" / "not found" /
420				// "ENOENT". Downgrade to `grpc-verbose` so the default
421				// log reflects genuine failures only. The response still
422				// returns -32000 so Cocoon's shim can convert it to a
423				// proper `vscode.FileSystemError.FileNotFound`.
424				let LowerError = ErrorString.to_lowercase();
425				// "Path is outside of the registered workspace folders" /
426				// "Permission denied" responses come from the path-security
427				// guard in `Environment/Utility/PathSecurity.rs` when an
428				// extension probes a directory outside the open workspace
429				// (Svelte's `enableContextMenu` walks every `package.json`
430				// in the entire workspace tree, including out-of-root
431				// submodule dependencies). From the extension's perspective
432				// these are equivalent to "file not present" and must NOT
433				// count against Cocoon's circuit breaker - a workspace with
434				// many sibling submodules trips the breaker open within the
435				// first few hundred ms of activation otherwise.
436				let LooksLike404 = (MethodName == "FileSystem.ReadFile"
437					|| MethodName == "FileSystem.Stat"
438					|| MethodName == "FileSystem.ReadDirectory")
439					&& (LowerError.contains("resource not found")
440						|| LowerError.contains("not found")
441						|| LowerError.contains("enoent")
442						|| LowerError.contains("no such file or directory")
443						|| LowerError.contains("entity not found")
444						|| LowerError.contains("os error 2")
445						|| LowerError.contains("path is outside of the registered workspace")
446						|| LowerError.contains("permission denied for operation")
447						|| LowerError.contains("workspace is not trusted"));
448				if LooksLike404 {
449					dev_log!(
450						"grpc-verbose",
451						"[LandFix:MountainVinegRPC] Request [ID: {}] {} 404 (benign): {}",
452						RequestIdentifier,
453						MethodName,
454						ErrorString
455					);
456				} else {
457					dev_log!(
458						"grpc",
459						"error: [MountainVinegRPCService] Request [ID: {}] failed: {}",
460						RequestIdentifier,
461						ErrorString
462					);
463				}
464
465				// Distinct code -32004 for benign 404s lets the Cocoon shim
466				// classify them without a string-regex round-trip. -32000
467				// stays the catch-all for genuine failures.
468				let ErrorCode = if LooksLike404 { -32004 } else { -32000 };
469				Ok(Response::new(Self::CreateErrorResponse(
470					RequestIdentifier,
471					ErrorCode,
472					ErrorString,
473					None,
474				)))
475			},
476		}
477	}
478
479	/// Handles generic fire-and-forget notifications from Cocoon.
480	///
481	/// Notifications do not expect a response beyond acknowledgment.
482	/// They are used for status updates, events, and other asynchronous
483	/// notifications.
484	///
485	/// # Parameters
486	/// - `request`: GenericNotification with method name and parameters
487	///
488	/// # Returns
489	/// - `Ok(Response<Empty>)`: Notification was received and logged
490	/// - `Err(Status)`: Critical error during processing
491	///
492	/// # TODO
493	/// Future implementation should route notifications to dedicated handlers:
494	/// ```rust,ignore
495	/// let Parameter: Value = serde_json::from_slice(&notification.parameter)?;
496	/// NotificationHandler::Handle(MethodName, Parameter).await?;
497	/// ```
498	async fn send_cocoon_notification(&self, request:Request<GenericNotification>) -> Result<Response<Empty>, Status> {
499		let NotificationData = request.into_inner();
500
501		let MethodName = NotificationData.method;
502
503		// Notifications are even higher-volume than requests
504		// (progress.report alone fires 2500+ times per long activation).
505		// Move under `grpc-verbose` alongside the request-side banner.
506		dev_log!(
507			"grpc-verbose",
508			"[MountainVinegRPCService] Received gRPC Notification: Method='{}'",
509			MethodName
510		);
511
512		// Validate notification method name
513		if MethodName.is_empty() {
514			dev_log!(
515				"grpc",
516				"warn: [MountainVinegRPCService] Received notification with empty method name"
517			);
518			return Err(Status::invalid_argument("Method name cannot be empty"));
519		}
520
521		// Route notifications to appropriate handlers based on MethodName. Currently
522		// only logs known notification types and acknowledges all others. A complete
523		// implementation would maintain a registry of notification handlers per method,
524		// route notifications to registered handlers asynchronously, allow handlers
525		// to perform side effects (state updates, UI updates), support cancellation
526		// and timeouts for long-running handlers, and log unhandled notifications
527		// at debug level for diagnostics. Known notifications include:
528		// ExtensionActivated, ExtensionDeactivated, WebviewReady.
529
530		// Parse parameters for handlers that need them
531		let Parameter:Value = if NotificationData.parameter.is_empty() {
532			Value::Null
533		} else {
534			serde_json::from_slice(&NotificationData.parameter).unwrap_or(Value::Null)
535		};
536
537		match MethodName.as_str() {
538			// Batch 15: extension-host + progress + languages arms now live
539			// as atoms under `Vine::Server::Notification::*`. Each match arm
540			// is pure delegation - adding a new wire method is a one-line
541			// change here plus one new atom file.
542			"extensionHostMessage" => {
543				super::Notification::ExtensionHostMessage::ExtensionHostMessage(self, &Parameter).await;
544			},
545			"ExtensionActivated" => {
546				super::Notification::ExtensionActivated::ExtensionActivated(self, &Parameter).await;
547			},
548			"ExtensionDeactivated" => {
549				super::Notification::ExtensionDeactivated::ExtensionDeactivated(self, &Parameter).await;
550			},
551			"WebviewReady" => {
552				super::Notification::WebviewReady::WebviewReady(self, &Parameter).await;
553			},
554			"progress.start" => {
555				super::Notification::ProgressStart::ProgressStart(self, &Parameter).await;
556			},
557			"progress.report" => {
558				super::Notification::ProgressReport::ProgressReport(self, &Parameter).await;
559			},
560			"progress.end" => {
561				super::Notification::ProgressEnd::ProgressEnd(self, &Parameter).await;
562			},
563			"languages.setDocumentLanguage" => {
564				super::Notification::LanguagesSetDocumentLanguage::LanguagesSetDocumentLanguage(self, &Parameter).await;
565			},
566			"workspace.applyEdit" => {
567				super::Notification::WorkspaceApplyEdit::WorkspaceApplyEdit(self, &Parameter).await;
568			},
569			"window.showTextDocument" => {
570				super::Notification::WindowShowTextDocument::WindowShowTextDocument(self, &Parameter).await;
571			},
572
573			// Batch 16: the remaining Cocoon-notification arms, now pure
574			// atom delegations. Each wire method lives in its own file
575			// under `Vine::Server::Notification::*`. "Group atoms"
576			// (TerminalLifecycle, DebugLifecycle, WebviewLifecycle, etc.)
577			// handle 3-4 wire methods that share the same relay pattern.
578			"webview.setTitle"
579			| "webview.setIconPath"
580			| "webview.setHtml"
581			| "webview.updateView"
582			| "webview.reveal" => {
583				super::Notification::WebviewLifecycle::WebviewLifecycle(self, &MethodName, &Parameter).await;
584			},
585			"window.createTerminal" => {
586				super::Notification::WindowCreateTerminal::WindowCreateTerminal(self, &Parameter).await;
587			},
588			"terminal.sendText" | "terminal.show" | "terminal.hide" | "terminal.dispose" => {
589				super::Notification::TerminalLifecycle::TerminalLifecycle(self, &MethodName, &Parameter).await;
590			},
591			"window.createTextEditorDecorationType" | "window.disposeTextEditorDecorationType" => {
592				super::Notification::DecorationTypeLifecycle::DecorationTypeLifecycle(self, &MethodName, &Parameter).await;
593			},
594			"debug.addBreakpoints" | "debug.removeBreakpoints" | "debug.consoleAppend" => {
595				super::Notification::DebugLifecycle::DebugLifecycle(self, &MethodName, &Parameter).await;
596			},
597			"statusBar.update" | "statusBar.dispose" => {
598				super::Notification::StatusBarLifecycle::StatusBarLifecycle(self, &MethodName, &Parameter).await;
599			},
600			"statusBar.message" => {
601				super::Notification::StatusBarMessage::StatusBarMessage(self, &Parameter).await;
602			},
603			"window.showMessage" => {
604				super::Notification::WindowShowMessage::WindowShowMessage(self, &Parameter).await;
605			},
606			"registerCommand" => {
607				super::Notification::RegisterCommand::RegisterCommand(self, &Parameter).await;
608			},
609			"unregisterCommand" => {
610				super::Notification::UnregisterCommand::UnregisterCommand(self, &Parameter).await;
611			},
612
613			// NOTE: `outputChannel.*` arms were previously here fanning to
614			// the wrong `sky://output-channel/*` channel. Batch 9 atoms
615			// below correctly route to `sky://output/*`; the legacy arm
616			// was removed to stop it from shadowing the atoms.
617
618			// Batch 8: provider unregister atoms. Each wire method lives in
619			// its own `Notification/<Name>.rs` atom - the arm is a pure
620			// delegation so adding a variant stays a one-line change here
621			// plus one new file.
622			"unregister_authentication_provider" => {
623				super::Notification::UnregisterAuthenticationProvider::UnregisterAuthenticationProvider(self, &Parameter).await;
624			},
625			"unregister_debug_adapter" => {
626				super::Notification::UnregisterDebugAdapter::UnregisterDebugAdapter(self, &Parameter).await;
627			},
628			"unregister_file_system_provider" => {
629				super::Notification::UnregisterFileSystemProvider::UnregisterFileSystemProvider(self, &Parameter).await;
630			},
631			"unregister_scm_provider" => {
632				super::Notification::UnregisterScmProvider::UnregisterScmProvider(self, &Parameter).await;
633			},
634			"unregister_task_provider" => {
635				super::Notification::UnregisterTaskProvider::UnregisterTaskProvider(self, &Parameter).await;
636			},
637			"unregister_uri_handler" => {
638				super::Notification::UnregisterUriHandler::UnregisterUriHandler(self, &Parameter).await;
639			},
640			"update_scm_group" => {
641				super::Notification::UpdateScmGroup::UpdateScmGroup(self, &Parameter).await;
642			},
643			// SCM register pair: explicit arms BEFORE the language-providers
644			// OR-block below. Without these, both `register_scm_provider` and
645			// `register_scm_resource_group` fell into the catch-all language-
646			// providers branch which only writes to
647			// `Extension::ProviderRegistration` - never to
648			// `ApplicationState::Feature::Markers::SourceControlManagement*`,
649			// so the SCM viewlet stayed empty even after vscode.git's
650			// `createSourceControl(...)` round-tripped successfully. The new
651			// atoms write the markers + emit the `sky://scm/*` events the
652			// renderer subscribes to.
653			"register_scm_provider" => {
654				super::Notification::RegisterScmProvider::RegisterScmProvider(self, &Parameter).await;
655			},
656			"register_scm_resource_group" => {
657				super::Notification::RegisterScmResourceGroup::RegisterScmResourceGroup(self, &Parameter).await;
658			},
659
660			// Batch 11: progress lifecycle name alignment.
661			"progress.update" => {
662				super::Notification::ProgressUpdate::ProgressUpdate(self, &Parameter).await;
663			},
664			"progress.complete" => {
665				super::Notification::ProgressComplete::ProgressComplete(self, &Parameter).await;
666			},
667
668			// Batch 10: status-bar text-only fast path + item disposal.
669			"setStatusBarText" => {
670				super::Notification::SetStatusBarText::SetStatusBarText(self, &Parameter).await;
671			},
672			"disposeStatusBarItem" => {
673				super::Notification::DisposeStatusBarItem::DisposeStatusBarItem(self, &Parameter).await;
674			},
675
676			// Batch 9: output channel lifecycle. Two parallel wire names
677			// (`output.*` via `MountainClient.sendNotification` and
678			// `outputChannel.*` via `SendToMountain`) both forward to the
679			// same `sky://output/*` channels until Cocoon consolidates.
680			"output.create" => {
681				super::Notification::OutputCreate::OutputCreate(self, &Parameter).await;
682			},
683			"output.append" => {
684				super::Notification::OutputAppend::OutputAppend(self, &Parameter).await;
685			},
686			"output.appendLine" => {
687				super::Notification::OutputAppendLine::OutputAppendLine(self, &Parameter).await;
688			},
689			"output.clear" => {
690				super::Notification::OutputClear::OutputClear(self, &Parameter).await;
691			},
692			"output.show" => {
693				super::Notification::OutputShow::OutputShow(self, &Parameter).await;
694			},
695			"output.dispose" => {
696				super::Notification::OutputDispose::OutputDispose(self, &Parameter).await;
697			},
698			"output.replace" => {
699				super::Notification::OutputReplace::OutputReplace(self, &Parameter).await;
700			},
701			"outputChannel.create" => {
702				super::Notification::OutputChannelCreate::OutputChannelCreate(self, &Parameter).await;
703			},
704			"outputChannel.append" => {
705				super::Notification::OutputChannelAppend::OutputChannelAppend(self, &Parameter).await;
706			},
707			"outputChannel.clear" => {
708				super::Notification::OutputChannelClear::OutputChannelClear(self, &Parameter).await;
709			},
710			"outputChannel.show" => {
711				super::Notification::OutputChannelShow::OutputChannelShow(self, &Parameter).await;
712			},
713			"outputChannel.hide" => {
714				super::Notification::OutputChannelHide::OutputChannelHide(self, &Parameter).await;
715			},
716			"outputChannel.dispose" => {
717				super::Notification::OutputChannelDispose::OutputChannelDispose(self, &Parameter).await;
718			},
719
720			// Batch 13: webview reverse-channel (Mountain → renderer).
721			"webview.postMessage" => {
722				super::Notification::WebviewPostMessage::WebviewPostMessage(self, &Parameter).await;
723			},
724			"webview.dispose" => {
725				super::Notification::WebviewDispose::WebviewDispose(self, &Parameter).await;
726			},
727
728			// Batch 14: grammar config, external-URI open, security alert.
729			"set_language_configuration" => {
730				super::Notification::SetLanguageConfiguration::SetLanguageConfiguration(self, &Parameter).await;
731			},
732			"openExternal" => {
733				super::Notification::OpenExternal::OpenExternal(self, &Parameter).await;
734			},
735			"security.incident" => {
736				super::Notification::SecurityIncident::SecurityIncident(self, &Parameter).await;
737			},
738
739			// Cocoon → Mountain: provider registration from extensions.
740			//
741			// Covers all 34 `register_*` / `register_*_provider` notification
742			// variants that Cocoon's vscode-API shim emits. Each lands in
743			// Mountain's `ProviderRegistration` keyed on `Handle`; the
744			// language-feature RPC path (e.g. GetHoverAtPosition) then looks
745			// up the handle and proxies back to Cocoon with the original
746			// `$providerXxx` method.
747			//
748			// Wire-method naming: the shim uses snake_case with two trailing
749			// shapes - plain verbs (`register_rename`) and `_provider` suffix
750			// (`register_hover_provider`). The map below strips both.
751			// Full list mirrors Cocoon's `vscode` API shim wire strings - the
752			// authoritative set grep'd from `Cocoon/Source` is: most providers
753			// carry a `_provider` suffix; a handful (debug_adapter,
754			// uri_handler, external_uri_opener, notebook_serializer,
755			// remote_authority_resolver, resource_label_formatter,
756			// scm_resource_group) do not. Keep both the suffixed and
757			// non-suffixed variants listed explicitly so the OR-match stays
758			// readable at a glance; the strip-logic below normalises either
759			// form into `ProviderTypeName` for the enum lookup.
760			"register_authentication_provider"
761			| "register_call_hierarchy_provider"
762			| "register_code_actions_provider"
763			| "register_code_lens_provider"
764			| "register_color_provider"
765			| "register_completion_item_provider"
766			| "register_debug_adapter"
767			| "register_debug_configuration_provider"
768			| "register_declaration_provider"
769			| "register_definition_provider"
770			| "register_document_drop_edit_provider"
771			| "register_document_formatting_provider"
772			| "register_document_highlight_provider"
773			| "register_document_link_provider"
774			| "register_document_paste_edit_provider"
775			| "register_document_range_formatting_provider"
776			| "register_document_symbol_provider"
777			| "register_evaluatable_expression_provider"
778			| "register_external_uri_opener"
779			| "register_file_decoration_provider"
780			| "register_file_system_provider"
781			| "register_folding_range_provider"
782			| "register_hover_provider"
783			| "register_implementation_provider"
784			| "register_inlay_hints_provider"
785			| "register_inline_completion_item_provider"
786			| "register_inline_edit_provider"
787			| "register_inline_values_provider"
788			| "register_linked_editing_range_provider"
789			| "register_mapped_edits_provider"
790			| "register_multi_document_highlight_provider"
791			| "register_notebook_content_provider"
792			| "register_notebook_serializer"
793			| "register_on_type_formatting_provider"
794			| "register_reference_provider"
795			| "register_remote_authority_resolver"
796			| "register_rename_provider"
797			| "register_resource_label_formatter"
798			// `register_scm_provider` / `register_scm_resource_group` were
799			// here pre-Batch-17. They now have their own explicit arms above
800			// because the SCM viewlet binds to
801			// `ApplicationState::Feature::Markers::SourceControlManagement*`,
802			// not to `Extension::ProviderRegistration` which this OR-block
803			// writes. See `Notification/RegisterScmProvider.rs`.
804			| "register_selection_range_provider"
805			| "register_semantic_tokens_provider"
806			| "register_signature_help_provider"
807			| "register_task_provider"
808			| "register_terminal_link_provider"
809			| "register_terminal_profile_provider"
810			| "register_text_document_content_provider"
811			| "register_type_definition_provider"
812			| "register_type_hierarchy_provider"
813			| "register_uri_handler"
814			| "register_workspace_symbol_provider" => {
815				let Handle = Parameter.get("handle").and_then(|h| h.as_u64()).unwrap_or(0) as u32;
816				// Wire-shape contract: producer (Cocoon's `*Namespace.ts`)
817				// emits camelCase keys (`languageSelector`, `extensionId`)
818				// to align with VS Code's API surface. We probe camelCase
819				// first and fall back to the legacy snake_case shape so a
820				// partial rebuild (Mountain ahead of Cocoon, or vice
821				// versa) doesn't silently drop traffic.
822				let Selector = Parameter
823					.get("languageSelector")
824					.or_else(|| Parameter.get("language_selector"))
825					.and_then(|s| s.as_str())
826					.unwrap_or("*");
827				let ExtId = Parameter
828					.get("extensionId")
829					.or_else(|| Parameter.get("extension_id"))
830					.and_then(|e| e.as_str())
831					.unwrap_or("");
832				// Extension-scoped scheme (for FileSystemProvider, TextDocumentContentProvider,
833				// UriHandler). Present only for schema-bound variants; `""` for others.
834				let Scheme = Parameter.get("scheme").and_then(|s| s.as_str()).unwrap_or("");
835				let ProviderTypeName = MethodName
836					.strip_prefix("register_")
837					.map(|Stripped| Stripped.strip_suffix("_provider").unwrap_or(Stripped))
838					.unwrap_or("");
839				// The second `provider-register` dev_log below carries the
840				// superset of fields (MethodName, scheme, extension id)
841				// that make the line useful. The `grpc`-tagged copy here
842				// was just the short form printed twice per register.
843				// Route to `grpc-verbose` so the `short` log only shows
844				// one line per provider registration.
845				dev_log!(
846					"grpc-verbose",
847					"[MountainVinegRPCService] Cocoon registered {} provider: handle={}, lang={}",
848					ProviderTypeName,
849					Handle,
850					Selector
851				);
852				dev_log!(
853					"provider-register",
854					"[ProviderRegister] accepted method={} type={} handle={} lang={} scheme={} ext={}",
855					MethodName,
856					ProviderTypeName,
857					Handle,
858					Selector,
859					Scheme,
860					ExtId
861				);
862				use CommonLibrary::LanguageFeature::DTO::ProviderType::ProviderType as PT;
863				let ProvType = match ProviderTypeName {
864					"authentication" => Some(PT::Authentication),
865					"call_hierarchy" => Some(PT::CallHierarchy),
866					"code_actions" => Some(PT::CodeAction),
867					"code_lens" => Some(PT::CodeLens),
868					"color" => Some(PT::Color),
869					"completion_item" => Some(PT::Completion),
870					"debug_adapter" => Some(PT::DebugAdapter),
871					"debug_configuration" => Some(PT::DebugConfiguration),
872					"declaration" => Some(PT::Declaration),
873					"definition" => Some(PT::Definition),
874					"document_drop_edit" => Some(PT::DocumentDropEdit),
875					"document_formatting" => Some(PT::DocumentFormatting),
876					"document_highlight" => Some(PT::DocumentHighlight),
877					"document_link" => Some(PT::DocumentLink),
878					"document_paste_edit" => Some(PT::DocumentPasteEdit),
879					"document_range_formatting" => Some(PT::DocumentRangeFormatting),
880					"document_symbol" => Some(PT::DocumentSymbol),
881					"evaluatable_expression" => Some(PT::EvaluatableExpression),
882					"external_uri_opener" => Some(PT::ExternalUriOpener),
883					"file_decoration" => Some(PT::FileDecoration),
884					"file_system" => Some(PT::FileSystem),
885					"folding_range" => Some(PT::FoldingRange),
886					"hover" => Some(PT::Hover),
887					"implementation" => Some(PT::Implementation),
888					"inlay_hints" => Some(PT::InlayHint),
889					"inline_completion_item" => Some(PT::InlineCompletion),
890					"inline_edit" => Some(PT::InlineEdit),
891					"inline_values" => Some(PT::InlineValues),
892					"linked_editing_range" => Some(PT::LinkedEditingRange),
893					"mapped_edits" => Some(PT::MappedEdits),
894					"multi_document_highlight" => Some(PT::MultiDocumentHighlight),
895					"notebook_content" => Some(PT::NotebookContent),
896					"notebook_serializer" => Some(PT::NotebookSerializer),
897					"on_type_formatting" => Some(PT::OnTypeFormatting),
898					"reference" => Some(PT::References),
899					"remote_authority_resolver" => Some(PT::RemoteAuthorityResolver),
900					"rename" => Some(PT::Rename),
901					"resource_label_formatter" => Some(PT::ResourceLabelFormatter),
902					"scm" => Some(PT::SourceControl),
903					"scm_resource_group" => Some(PT::ScmResourceGroup),
904					"selection_range" => Some(PT::SelectionRange),
905					"semantic_tokens" => Some(PT::SemanticTokens),
906					"signature_help" => Some(PT::SignatureHelp),
907					"task" => Some(PT::Task),
908					"terminal_link" => Some(PT::TerminalLink),
909					"terminal_profile" => Some(PT::TerminalProfile),
910					"text_document_content" => Some(PT::TextDocumentContent),
911					"type_definition" => Some(PT::TypeDefinition),
912					"type_hierarchy" => Some(PT::TypeHierarchy),
913					"uri_handler" => Some(PT::UriHandler),
914					"workspace_symbol" => Some(PT::WorkspaceSymbol),
915					_ => None,
916				};
917				if let Some(ProviderType) = ProvType {
918					use crate::ApplicationState::DTO::ProviderRegistrationDTO::ProviderRegistrationDTO;
919					// Scheme-bound providers carry their scheme in the selector payload so
920					// the Mountain-side resolver (FileSystem router, URI handler dispatch,
921					// TextDocumentContent view, …) can match on it.
922					let SelectorValue = if !Scheme.is_empty() {
923						json!([{ "scheme": Scheme, "language": Selector }])
924					} else {
925						json!([{ "language": Selector }])
926					};
927					let Dto = ProviderRegistrationDTO {
928						Handle,
929						ProviderType,
930						Selector:SelectorValue,
931						SideCarIdentifier:"cocoon-main".to_string(),
932						ExtensionIdentifier:json!(ExtId),
933						Options:Parameter.get("options").cloned(),
934					};
935					self.RunTime
936						.Environment
937						.ApplicationState
938						.Extension
939						.ProviderRegistration
940						.RegisterProvider(Handle, Dto);
941				}
942			},
943			_ => {
944				dev_log!("grpc", "[MountainVinegRPCService] Cocoon notification: {}", MethodName);
945				// No typed match arm exists for this notification - it hits
946				// the default path and becomes a `cocoon:<method>` Tauri
947				// event that Wind may or may not listen for. The
948				// `notif-drop` tag surfaces every fall-through so we can
949				// tell at a glance which notifications Cocoon emits that
950				// Mountain has no first-class handler for. The large OR
951				// match above covers every `register_*` / `register_*_provider`
952				// variant the Cocoon vscode-API shim is known to emit;
953				// anything reaching here is either a new upstream addition or
954				// an `unregister_*` / generic notification without a typed
955				// handler. Payload preview included so diagnosis doesn't need
956				// a second run.
957				let PayloadPreview = if NotificationData.parameter.len() <= 160 {
958					String::from_utf8_lossy(&NotificationData.parameter).into_owned()
959				} else {
960					let Slice = &NotificationData.parameter[..160];
961					format!("{}…", String::from_utf8_lossy(Slice))
962				};
963				dev_log!(
964					"notif-drop",
965					"[NotifDrop] method={} payload_bytes={} preview={:?} (falls through to cocoon:{} event)",
966					MethodName,
967					NotificationData.parameter.len(),
968					PayloadPreview,
969					MethodName
970				);
971				// Forward all unknown notifications as Tauri events so Wind
972				// can subscribe to any Cocoon-originated event.
973				let EventName = format!("cocoon:{}", MethodName);
974				if let Err(Error) = self.ApplicationHandle.emit(&EventName, &Parameter) {
975					dev_log!(
976						"grpc",
977						"warn: [MountainVinegRPCService] Failed to emit {}: {}",
978						EventName,
979						Error
980					);
981				}
982			},
983		}
984
985		Ok(Response::new(Empty {}))
986	}
987
988	/// Handles a request from Cocoon to cancel a long-running operation.
989	///
990	/// This method is called when Cocoon wants to cancel an operation that
991	/// was previously initiated via process_cocoon_request.
992	///
993	/// # Parameters
994	/// - `request`: CancelOperationRequest with the request ID to cancel
995	///
996	/// # Returns
997	/// - `Ok(Response<Empty>)`: Cancellation was initiated
998	/// - `Err(Status)`: Critical error during cancellation
999	async fn cancel_operation(&self, request:Request<CancelOperationRequest>) -> Result<Response<Empty>, Status> {
1000		let cancel_request = request.into_inner();
1001
1002		let RequestIdentifierToCancel = cancel_request.request_identifier_to_cancel;
1003
1004		dev_log!(
1005			"grpc",
1006			"[MountainVinegRPCService] Received CancelOperation request for RequestID: {}",
1007			RequestIdentifierToCancel
1008		);
1009
1010		// Look up the operation in the active operations registry
1011		let cancel_token = {
1012			let operations = self.ActiveOperations.read().await;
1013			operations.get(&RequestIdentifierToCancel).cloned()
1014		};
1015
1016		match cancel_token {
1017			Some(token) => {
1018				// Trigger cancellation token to signal the operation to abort
1019				token.cancel();
1020
1021				dev_log!(
1022					"grpc",
1023					"[MountainVinegRPCService] Successfully initiated cancellation for operation {}",
1024					RequestIdentifierToCancel
1025				);
1026
1027				// Note: We don't remove the token here - the operation itself should
1028				// call UnregisterOperation when it completes. This allows the
1029				// operation to detect the cancellation and clean up properly.
1030
1031				Ok(Response::new(Empty {}))
1032			},
1033			None => {
1034				// Operation not found - it may have already completed
1035				dev_log!(
1036					"grpc",
1037					"warn: [MountainVinegRPCService] Cannot cancel operation {}: operation not found (may have \
1038					 already completed)",
1039					RequestIdentifierToCancel
1040				);
1041
1042				// Return success anyway - the operation is not running
1043				Ok(Response::new(Empty {}))
1044			},
1045		}
1046	}
1047}