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