Skip to main content

Mountain/IPC/WindServiceHandlers/
mod.rs

1#![allow(non_snake_case, unused_variables, dead_code, unused_imports)]
2
3//! Wind Service Handlers - dispatcher and sub-module aggregator.
4//! Domain files handle the individual handler implementations.
5
6pub mod Commands;
7
8pub mod Configuration;
9
10pub mod Extension;
11
12pub mod Extensions;
13
14pub mod FileSystem;
15
16pub mod Git;
17
18pub mod Model;
19
20pub mod NativeDialog;
21
22pub mod NativeHost;
23
24pub mod Navigation;
25
26pub mod Output;
27
28pub mod Search;
29
30pub mod Storage;
31
32pub mod Terminal;
33
34pub mod UI;
35
36pub mod Utilities;
37
38// Local `use X::*;` (NOT `pub use`): brings the domain handler names into
39// this file's scope so the dispatch match arms below can call
40// `handle_foo(...)` unqualified. Local `use` is scoped to this file only;
41// external callers must spell the full path
42// (`WindServiceHandlers::Utilities::foo`).
43use std::{collections::HashMap, path::PathBuf, sync::Arc};
44
45use Commands::*;
46use Configuration::*;
47use Extensions::{
48	ExtensionsGet::ExtensionsGet,
49	ExtensionsGetAll::ExtensionsGetAll,
50	ExtensionsGetInstalled::ExtensionsGetInstalled,
51	ExtensionsIsActive::ExtensionsIsActive,
52};
53use FileSystem::{
54	Managed::{
55		FileCopy::*,
56		FileDelete::*,
57		FileExists::*,
58		FileMkdir::*,
59		FileMove::*,
60		FileRead::*,
61		FileReadBinary::*,
62		FileReaddir::*,
63		FileStat::*,
64		FileWrite::*,
65		FileWriteBinary::*,
66	},
67	Native::{
68		FileCloneNative::*,
69		FileDeleteNative::*,
70		FileExistsNative::*,
71		FileMkdirNative::*,
72		FileReadNative::*,
73		FileReaddirNative::*,
74		FileRealpath::*,
75		FileRenameNative::*,
76		FileStatNative::*,
77		FileWriteNative::*,
78	},
79};
80use Model::{
81	ModelClose::ModelClose,
82	ModelGet::ModelGet,
83	ModelGetAll::ModelGetAll,
84	ModelOpen::ModelOpen,
85	ModelUpdateContent::ModelUpdateContent,
86	TextfileRead::TextfileRead,
87	TextfileSave::TextfileSave,
88	TextfileWrite::TextfileWrite,
89};
90use NativeHost::{
91	FindFreePort::*,
92	GetColorScheme::*,
93	IsFullscreen::*,
94	IsMaximized::*,
95	OSProperties::*,
96	OSStatistics::*,
97	OpenExternal::*,
98	PickFolder::*,
99	ShowItemInFolder::*,
100	ShowOpenDialog::*,
101};
102use Navigation::{
103	HistoryCanGoBack::HistoryCanGoBack,
104	HistoryCanGoForward::HistoryCanGoForward,
105	HistoryClear::HistoryClear,
106	HistoryGetStack::HistoryGetStack,
107	HistoryGoBack::HistoryGoBack,
108	HistoryGoForward::HistoryGoForward,
109	HistoryPush::HistoryPush,
110	LabelGetBase::LabelGetBase,
111	LabelGetURI::LabelGetURI,
112	LabelGetWorkspace::LabelGetWorkspace,
113};
114use Output::{
115	OutputAppend::OutputAppend,
116	OutputAppendLine::OutputAppendLine,
117	OutputClear::OutputClear,
118	OutputCreate::OutputCreate,
119	OutputShow::OutputShow,
120};
121use Search::*;
122use Storage::{
123	StorageDelete::StorageDelete,
124	StorageGet::StorageGet,
125	StorageGetItems::StorageGetItems,
126	StorageKeys::StorageKeys,
127	StorageSet::StorageSet,
128	StorageUpdateItems::StorageUpdateItems,
129};
130use Terminal::{
131	LocalPTYGetDefaultShell::LocalPTYGetDefaultShell,
132	LocalPTYGetEnvironment::LocalPTYGetEnvironment,
133	LocalPTYGetProfiles::LocalPTYGetProfiles,
134	TerminalCreate::TerminalCreate,
135	TerminalDispose::TerminalDispose,
136	TerminalHide::TerminalHide,
137	TerminalSendText::TerminalSendText,
138	TerminalShow::TerminalShow,
139};
140use UI::{
141	Decoration::*,
142	Keybinding::*,
143	Lifecycle::*,
144	Notification::*,
145	Progress::*,
146	QuickInput::*,
147	Theme::*,
148	WorkingCopy::*,
149	Workspace::*,
150};
151use Utilities::{
152	ApplicationRoot::*,
153	ChannelPriority::*,
154	JsonValueHelpers::*,
155	MetadataEncoding::*,
156	PathExtraction::*,
157	RecentlyOpened::*,
158	UserdataDir::*,
159};
160use Echo::Task::Priority::Priority as EchoPriority;
161use serde_json::{Value, json};
162use tauri::{AppHandle, Manager};
163// Type aliases for Configuration DTOs to simplify usage
164use CommonLibrary::Configuration::DTO::{
165	ConfigurationOverridesDTO as ConfigurationOverridesDTOModule,
166	ConfigurationTarget as ConfigurationTargetModule,
167};
168
169use crate::dev_log;
170
171type ConfigurationOverridesDTO = ConfigurationOverridesDTOModule::ConfigurationOverridesDTO;
172
173type ConfigurationTarget = ConfigurationTargetModule::ConfigurationTarget;
174
175use CommonLibrary::{
176	Command::CommandExecutor::CommandExecutor,
177	Configuration::ConfigurationProvider::ConfigurationProvider,
178	Environment::Requires::Requires,
179	Error::CommonError::CommonError,
180	ExtensionManagement::ExtensionManagementService::ExtensionManagementService,
181	FileSystem::{FileSystemReader::FileSystemReader, FileSystemWriter::FileSystemWriter},
182	IPC::SkyEvent::SkyEvent,
183	Storage::StorageProvider::StorageProvider,
184};
185
186use crate::{
187	ApplicationState::{
188		DTO::WorkspaceFolderStateDTO::WorkspaceFolderStateDTO,
189		State::{
190			ApplicationState::ApplicationState,
191			WorkspaceState::WorkspaceDelta::UpdateWorkspaceFoldersAndBroadcast,
192		},
193	},
194	RunTime::ApplicationRunTime::ApplicationRunTime,
195};
196
197/// Internal dispatcher for the single front-end Tauri command
198/// `MountainIPCInvoke` (registered in `Binary/Main/Entry.rs::invoke_handler!`,
199/// implemented in `Binary/IPC/InvokeCommand.rs`). The outer Tauri command
200/// receives `(method: String, params: Value)`, unwraps `params` into a
201/// `Vec<Value>`, then delegates here.
202///
203/// This function is **not** a Tauri command itself - removing the previously
204/// present `#[tauri::command]` attribute avoids the false impression that
205/// `mountain_ipc_invoke` is reachable from the webview under its snake-case
206/// name. All front-end callers (Wind, Sky, Output) must `invoke(
207/// "MountainIPCInvoke", { method, params })` through `InvokeCommand::
208/// MountainIPCInvoke`; this inner function is pure Rust-side plumbing.
209///
210/// The local parameter names (`command` / `Arguments`) are preserved for diff
211/// minimality; the frontend-facing contract (`method` / `params`) lives
212/// entirely in `InvokeCommand.rs`.
213pub async fn mountain_ipc_invoke(
214	ApplicationHandle:AppHandle,
215
216	command:String,
217
218	Arguments:Vec<Value>,
219) -> Result<Value, String> {
220	// Determine high-frequency status first - used to skip OTLP timing,
221	// dev-logs, span emission, and PostHog capture for noisy calls.
222	let IsHighFrequencyCommand = matches!(
223		command.as_str(),
224		"logger:log"
225			| "logger:registerLogger"
226			| "logger:createLogger"
227			| "log:registerLogger"
228			| "log:createLogger"
229			| "file:stat"
230			| "file:readFile"
231			| "file:readdir"
232			| "file:writeFile"
233			| "file:delete"
234			| "file:rename"
235			| "file:realpath"
236			| "file:read"
237			| "file:write"
238			| "storage:getItems"
239			| "storage:updateItems"
240			| "configuration:lookup"
241			| "configuration:inspect"
242			| "themes:getColorTheme"
243			| "output:append"
244			| "progress:report"
245	);
246
247	let OTLPStart = if IsHighFrequencyCommand { 0 } else { crate::IPC::DevLog::NowNano::Fn() };
248
249	// Silence the per-call invoke log for high-frequency methods that are
250	// not useful in forensic review. The workbench emits thousands of
251	// `logger:log` invocations per boot (every `console.*` call inside VS
252	// Code code becomes an IPC round-trip); keeping those lines only
253	// expands log volume without adding signal. The actual dispatch below
254	// still runs - this just skips the `[DEV:IPC] invoke:` line.
255
256	if !IsHighFrequencyCommand {
257		dev_log!("ipc", "invoke: {} args_count={}", command, Arguments.len());
258	}
259
260	// Ensure userdata directories exist on first IPC call
261	ensure_userdata_dirs();
262
263	// Get the application RunTime - deref the Tauri State into an owned Arc
264	// so we can hand it to an Echo scheduler task below (State<T> isn't
265	// Send across task boundaries).
266	let RunTime:Arc<ApplicationRunTime> = ApplicationHandle.state::<Arc<ApplicationRunTime>>().inner().clone();
267
268	// Short-circuit known no-op commands BEFORE Echo scheduler submission
269	// to avoid oneshot channel allocation, String clone, and scheduler
270	// overhead for calls that return Ok(Value::Null) unconditionally.
271	// These account for the bulk of high-frequency IPC traffic (logger,
272	// file watch, storage events, command registration).
273	if IsHighFrequencyCommand {
274		match command.as_str() {
275			// Logger: fire-and-forget, no side effects needed
276			"logger:log" | "logger:warn" | "logger:error" | "logger:info"
277			| "logger:debug" | "logger:trace" | "logger:critical"
278			| "logger:flush" | "logger:setLevel" | "logger:getLevel"
279			| "logger:createLogger" | "logger:registerLogger"
280			| "logger:deregisterLogger" | "logger:getRegisteredLoggers"
281			| "logger:setVisibility"
282			// File watch stubs: unpiped in current architecture
283			| "file:watch" | "file:unwatch"
284			// Storage event stubs: change delivery via Tauri events
285			| "storage:onDidChangeItems" | "storage:logStorage"
286			// Command registry stubs: side effects handled via gRPC
287			| "commands:registerCommand" | "commands:unregisterCommand"
288			| "commands:onDidRegisterCommand" | "commands:onDidExecuteCommand"
289			// Configuration event stub
290			| "configuration:onDidChange"
291			// Storage lifecycle stubs
292			| "storage:optimize" | "storage:isUsed" | "storage:close" => {
293				let Elapsed = crate::IPC::DevLog::NowNano::Fn().saturating_sub(OTLPStart);
294				dev_log!("ipc", "done: {} ok=true t_ns={}", command, Elapsed);
295				return Ok(Value::Null);
296			},
297			_ => {}, // fall through to Echo dispatch for real work
298		}
299	}
300
301	// Tag the pending IPC with its priority lane and submit the entire
302	// Tags match the route prefix: vfs, config, storage, extensions,
303	// terminal, output, textfile, notification, progress, quickinput,
304	// workspaces, themes, search, decorations, workingcopy, keybinding,
305	// lifecycle, label, model, history, commands, nativehost, window,
306	// exthost, encryption, menubar, update, url, grpc.
307	// Activate: Trace=all   or   Trace=vfs,ipc,config
308	//
309	// Atom O1 + O3: every invoke flows through `SubmitToEcho` below so the
310	// Echo work-stealing scheduler picks a lane based on `Channel::Priority()`.
311	// The dispatch match still runs inline - Echo's real value is queuing
312	// decisions under load, not moving a single future across threads. This
313	// keeps the 4900-line match legible while guaranteeing every inbound
314	// IPC hits the scheduler's priority machinery on its way out.
315	// =========================================================================
316
317	// Tag the pending IPC with its priority lane and submit the entire
318	// dispatch future to Echo. Results flow back through a oneshot channel
319	// so the Tauri caller still awaits a plain `Result<Value, String>`.
320	let CommandPriority = ResolveChannelPriority(&command);
321
322	let Scheduler = RunTime.Scheduler.clone();
323
324	let (ResultSender, ResultReceiver) = tokio::sync::oneshot::channel::<Result<Value, String>>();
325
326	let DispatchAppHandle = ApplicationHandle.clone();
327
328	let DispatchRuntime = RunTime.clone();
329
330	let DispatchCommand = command.clone();
331
332	let DispatchArgs = Arguments;
333
334	Scheduler.Submit(
335		async move {
336			let ApplicationHandle = DispatchAppHandle;
337			let RunTime = DispatchRuntime;
338			let command = DispatchCommand;
339			let Arguments = DispatchArgs;
340
341			let MatchResult = match command.as_str() {
342				// Configuration commands. VS Code's stock
343				// `ConfigurationService` channel calls `getValue` /
344				// `updateValue`; Mountain's native Effect-TS layer calls
345				// `get` / `update`. Alias both to the same handler so
346				// traffic from either rail lands in the same place.
347				"configuration:get" | "configuration:getValue" => {
348					dev_log!("config", "{}", command);
349					ConfigurationGet(RunTime.clone(), Arguments).await
350				},
351				"configuration:update" | "configuration:updateValue" => {
352					dev_log!("config", "{}", command);
353					ConfigurationUpdate(RunTime.clone(), Arguments).await
354				},
355				// `ConfigurationService` listens for `onDidChange` from
356				// the channel on the binary IPC rail. Mountain broadcasts
357				// config changes via a Tauri event directly; ack the
358				// channel-listen with Null so the ChannelClient doesn't
359				// leak a pending promise.
360				"configuration:onDidChange" => Ok(Value::Null),
361
362				// Logger commands - fire-and-forget from Wind, just acknowledge
363				"logger:log"
364				| "logger:warn"
365				| "logger:error"
366				| "logger:info"
367				| "logger:debug"
368				| "logger:trace"
369				| "logger:critical"
370				| "logger:flush"
371				| "logger:setLevel"
372				| "logger:getLevel"
373				| "logger:createLogger"
374				| "logger:registerLogger"
375				| "logger:deregisterLogger"
376				| "logger:getRegisteredLoggers"
377				| "logger:setVisibility" => Ok(Value::Null),
378
379				// File system commands - use native handlers with URI support.
380				//
381				// The primary names (`file:read`, `file:write`, `file:move`)
382				// match Mountain's original dispatch table and are what
383				// Wind's Effect-TS layer calls. VS Code's
384				// `DiskFileSystemProviderClient` (reached through the
385				// binary IPC bridge in Output/IPCRendererShim) uses the
386				// stock channel-client method names `readFile`,
387				// `writeFile`, `rename`; aliasing them here keeps both
388				// rails pointing at the same handler without duplicating
389				// logic or introducing a per-caller translation table.
390				"file:read" | "file:readFile" => FileReadNative(Arguments).await,
391				"file:write" | "file:writeFile" => FileWriteNative(Arguments).await,
392				"file:stat" => FileStatNative(Arguments).await,
393				"file:exists" => FileExistsNative(Arguments).await,
394				"file:delete" => FileDeleteNative(Arguments).await,
395				"file:copy" => FileCloneNative(Arguments).await,
396				"file:move" | "file:rename" => FileRenameNative(Arguments).await,
397				"file:mkdir" => FileMkdirNative(Arguments).await,
398				"file:readdir" => FileReaddirNative(Arguments).await,
399				"file:readBinary" => FileReadBinary(RunTime.clone(), Arguments).await,
400				"file:writeBinary" => FileWriteBinary(RunTime.clone(), Arguments).await,
401				// File watcher channel methods - `DiskFileSystemProvider`
402				// opens `watch` / `unwatch` channel calls to receive
403				// `onDidChangeFile` events. Until the Mountain-side
404				// filewatcher bridge is wired through the binary IPC we
405				// ack with Null so the workbench proceeds without a
406				// hanging promise.
407				"file:watch" | "file:unwatch" => {
408					dev_log!("fs-route", "{} (stub-ack)", command);
409					Ok(Value::Null)
410				},
411
412				// Storage commands. VS Code's
413				// `ApplicationStorageDatabaseClient` channel methods are
414				// `getItems` / `updateItems` / `optimize` / `close` /
415				// `isUsed`; the shorter `storage:get` / `storage:set` are
416				// Mountain-native conveniences. All route through the
417				// same ApplicationState storage backing.
418				"storage:get" => StorageGet(RunTime.clone(), Arguments).await,
419				"storage:set" => StorageSet(RunTime.clone(), Arguments).await,
420				"storage:getItems" => {
421					// Workbench services poll this on every theme / scope
422					// change; suppress the bare banner and rely on the IPC
423					// `invoke:`/`done:` summary for volume + latency.
424					dev_log!("storage-verbose", "storage:getItems");
425					StorageGetItems(RunTime.clone(), Arguments).await
426				},
427				"storage:updateItems" => {
428					dev_log!("storage-verbose", "storage:updateItems");
429					StorageUpdateItems(RunTime.clone(), Arguments).await
430				},
431				"storage:optimize" => {
432					dev_log!("storage", "storage:optimize");
433					Ok(Value::Null)
434				},
435				"storage:isUsed" => {
436					dev_log!("storage", "storage:isUsed");
437					Ok(Value::Null)
438				},
439				"storage:close" => {
440					dev_log!("storage", "storage:close");
441					Ok(Value::Null)
442				},
443				// Stock VS Code exposes `onDidChangeItems` as a channel
444				// event. Ack the listen-request; real change delivery is
445				// via Tauri event elsewhere.
446				"storage:onDidChangeItems" | "storage:logStorage" => {
447					dev_log!("storage-verbose", "{} (stub-ack)", command);
448					Ok(Value::Null)
449				},
450
451				// Environment commands
452				"environment:get" => {
453					dev_log!("config", "environment:get");
454					EnvironmentGet(RunTime.clone(), Arguments).await
455				},
456
457				// Native host commands
458				"native:showItemInFolder" => ShowItemInFolder(RunTime.clone(), Arguments).await,
459				"native:openExternal" => OpenExternal(RunTime.clone(), Arguments).await,
460
461				// Workbench commands
462				"workbench:getConfiguration" => WorkbenchConfiguration(RunTime.clone(), Arguments).await,
463
464				// Diagnostic: webview → Mountain dev-log bridge.
465				// First arg is a tag ("boot", "extService", …), second is the
466				// message, rest are optional structured fields we stringify.
467				// Atom H1c: added so workbench.js can surface diagnostic state
468				// into the same Mountain.dev.log that carries Rust-side events.
469				"diagnostic:log" => {
470					let Tag = Arguments.first().and_then(|V| V.as_str()).unwrap_or("webview").to_string();
471					let Message = Arguments.get(1).and_then(|V| V.as_str()).unwrap_or("").to_string();
472					let Extras = if Arguments.len() > 2 {
473						let Tail:Vec<String> = Arguments
474							.iter()
475							.skip(2)
476							.map(|V| {
477								let S = serde_json::to_string(V).unwrap_or_default();
478								// Char-aware truncation - JSON-encoded values may
479								// embed multi-byte UTF-8 (extension names, repo
480								// paths with non-ASCII, debug payloads). Slicing
481								// at a fixed byte offset can land mid-codepoint
482								// and panic the tokio worker.
483								if S.len() > 240 {
484									let CutAt = S
485										.char_indices()
486										.map(|(Index, _)| Index)
487										.take_while(|Index| *Index <= 240)
488										.last()
489										.unwrap_or(0);
490									format!("{}…", &S[..CutAt])
491								} else {
492									S
493								}
494							})
495							.collect();
496						format!(" {}", Tail.join(" "))
497					} else {
498						String::new()
499					};
500					dev_log!("diagnostic", "[{}] {}{}", Tag, Message, Extras);
501					Ok(Value::Null)
502				},
503
504				// Command registry commands. Stock VS Code
505				// `MainThreadCommands` / `CommandService` channel methods
506				// are `executeCommand` and `getCommands`; Mountain's
507				// Effect-TS rail uses `execute` / `getAll`. Alias both.
508				"commands:execute" | "commands:executeCommand" => CommandsExecute(RunTime.clone(), Arguments).await,
509				"commands:getAll" | "commands:getCommands" => {
510					dev_log!("commands", "{}", command);
511					CommandsGetAll(RunTime.clone()).await
512				},
513				// Register/unregister from a side-car channel perspective
514				// is a no-op: Cocoon sends `$registerCommand` via gRPC
515				// (handled elsewhere). Ack Null so the workbench side
516				// doesn't hang on a promise.
517				"commands:registerCommand"
518				| "commands:unregisterCommand"
519				| "commands:onDidRegisterCommand"
520				| "commands:onDidExecuteCommand" => Ok(Value::Null),
521
522				// Extension host commands
523				"extensions:getAll" => {
524					dev_log!("extensions", "extensions:getAll");
525					ExtensionsGetAll(RunTime.clone()).await
526				},
527				"extensions:get" => {
528					dev_log!("extensions", "extensions:get");
529					ExtensionsGet(RunTime.clone(), Arguments).await
530				},
531				"extensions:isActive" => {
532					dev_log!("extensions", "extensions:isActive");
533					ExtensionsIsActive(RunTime.clone(), Arguments).await
534				},
535
536				// VS Code's Extensions sidebar →
537				// `ExtensionManagementChannelClient.getInstalled` goes through
538				// `sharedProcessService.getChannel('extensions')`. Sky's
539				// astro.config.ts Step 7b swaps the native SharedProcessService
540				// for a TauriMainProcessService-backed shim, so the call lands
541				// here as `extensions:getInstalled`. The expected return is
542				// `ILocalExtension[]` - a wrapper around each scanned manifest
543				// with `identifier.id`, `manifest`, `location`, `isBuiltin`, etc.
544				// `ExtensionsGetInstalled` builds that envelope;
545				// `ExtensionsGetAll` returns the raw manifest for
546				// callers (Cocoon, Wind Effect services) that want the flat
547				// shape. Do NOT alias these two - the payload shapes differ.
548				"extensions:getInstalled" | "extensions:scanSystemExtensions" => {
549					// Atom H1a: Arguments[0]=type, Arguments[1]=profileLocation URI,
550					// Arguments[2]=productVersion, Arguments[3]=??? (VS Code canonical is
551					// 3; shim appears to add a 4th). Dump to find out what it
552					// contains on post-nav page reloads where the sidebar
553					// renders 0 entries despite Mountain returning 94.
554					let ArgsSummary = Arguments
555						.iter()
556						.enumerate()
557						.map(|(Idx, V)| {
558							let Preview = serde_json::to_string(V).unwrap_or_default();
559							// Char-aware truncation - same UTF-8 hazard as
560							// the diagnostic-tag formatter above.
561							let Trimmed = if Preview.len() > 180 {
562								let CutAt = Preview
563									.char_indices()
564									.map(|(Index, _)| Index)
565									.take_while(|Index| *Index <= 180)
566									.last()
567									.unwrap_or(0);
568								format!("{}…", &Preview[..CutAt])
569							} else {
570								Preview
571							};
572							format!("[{}]={}", Idx, Trimmed)
573						})
574						.collect::<Vec<_>>()
575						.join(" ");
576					dev_log!("extensions", "{} Arguments={}", command, ArgsSummary);
577					// `scanSystemExtensions` is conceptually
578					// `getInstalled(type=ExtensionType.System)`, so override
579					// `Arguments[0]` to `0` before forwarding. Without the override
580					// a plain alias would inherit whatever the caller passed
581					// in Arguments[0] (which for the VS Code channel client is
582					// usually `null`) and leak User extensions into the
583					// System list - the same bug we just fixed at the
584					// handler layer, one level up.
585					let EffectiveArgs = if command == "extensions:scanSystemExtensions" {
586						let mut Overridden = Arguments.clone();
587						if Overridden.is_empty() {
588							Overridden.push(Value::Null);
589						}
590						Overridden[0] = json!(0);
591						Overridden
592					} else {
593						Arguments.clone()
594					};
595					ExtensionsGetInstalled(RunTime.clone(), EffectiveArgs).await
596				},
597				"extensions:scanUserExtensions" => {
598					// User-scope scan. Forward to the unified handler with
599					// `type=ExtensionType.User (1)` so VSIX-installed
600					// extensions under `~/.land/extensions/*` come back
601					// even when the caller didn't pass an explicit type
602					// filter (VS Code's channel client does that on
603					// scan-user-extensions, which is why the sidebar
604					// previously saw an empty list after every
605					// Install-from-VSIX).
606					dev_log!("extensions", "{} (forwarded to getInstalled with type=User)", command);
607					let mut UserArgs = Arguments.clone();
608					if UserArgs.is_empty() {
609						UserArgs.push(Value::Null);
610					}
611					UserArgs[0] = json!(1);
612					ExtensionsGetInstalled(RunTime.clone(), UserArgs).await
613				},
614				"extensions:getUninstalled" => {
615					// Uninstalled state (extensions soft-deleted but kept in
616					// the profile) isn't tracked yet; an empty array is the
617					// correct "nothing pending uninstall" response.
618					dev_log!("extensions", "{} (returning [])", command);
619					Ok(Value::Array(Vec::new()))
620				},
621				// Gallery is offline: Mountain has no marketplace backend. Return
622				// empty arrays for every read and swallow every write, which
623				// mirrors what a network-air-gapped VS Code session shows.
624				"extensions:query" | "extensions:getExtensions" | "extensions:getRecommendations" => {
625					dev_log!("extensions", "{} (offline gallery - returning [])", command);
626					Ok(Value::Array(Vec::new()))
627				},
628				// `IExtensionsControlManifest` - consulted by the Extensions
629				// sidebar on every render (ExtensionEnablementService.ts:793)
630				// to mark malicious / deprecated / auto-updateable entries.
631				// With the gallery offline an empty envelope is correct; the
632				// shape (not null) matters - VS Code destructures each field.
633				"extensions:getExtensionsControlManifest" => {
634					dev_log!("extensions", "{} (offline gallery - empty manifest)", command);
635					Ok(json!({
636						"malicious": [],
637						"deprecated": {},
638						"search": [],
639						"autoUpdate": {},
640					}))
641				},
642				// Atom P1: `ExtensionsWorkbenchService.resetPinnedStateForAllUserExtensions`
643				// is invoked when the user toggles pinning semantics in the
644				// sidebar. Pin state is Wind-owned (Cocoon never sees it); the
645				// only Mountain-side cost is an acknowledgement so the
646				// extension-enablement service doesn't retry forever. Payload
647				// is optional - VS Code sometimes passes `{ refreshPinned: true }`.
648				"extensions:resetPinnedStateForAllUserExtensions" => {
649					dev_log!("extensions", "{} (no-op, pin state is UI-local)", command);
650					Ok(Value::Null)
651				},
652				// Atom K2: local VSIX install. Wind passes the file path from a
653				// "Install from VSIX…" prompt or drag-and-drop through to us; the
654				// previous stub silently returned `null` and the UI believed it
655				// had succeeded (that's the "VSIX isn't triggering or loading"
656				// regression). We now unpack the archive, stamp a DTO, register
657				// it in ScannedExtensions, and return the ILocalExtension wrapper
658				// so the sidebar refreshes without a window reload.
659				"extensions:install" => {
660					Extension::ExtensionInstall::ExtensionInstall(ApplicationHandle.clone(), RunTime.clone(), Arguments)
661						.await
662				},
663				"extensions:uninstall" => {
664					Extension::ExtensionUninstall::ExtensionUninstall(
665						ApplicationHandle.clone(),
666						RunTime.clone(),
667						Arguments,
668					)
669					.await
670				},
671
672				// `ExtensionManagementChannelClient.getManifest(vsix: URI)` - reads
673				// the `extension/package.json` from a `.vsix` archive without
674				// extracting it. Called by the "Install from VSIX…" preview and
675				// by drag-and-drop onto the Extensions sidebar. The renderer then
676				// accesses `manifest.publisher` / `.name` / `.displayName` on the
677				// returned object unconditionally; a missing handler or an Err
678				// response crashes the webview with
679				// `TypeError: undefined is not an object (evaluating 'manifest.publisher')`.
680				"extensions:getManifest" => {
681					let VsixPath = match Arguments.first() {
682						Some(serde_json::Value::String(Path)) => Path.clone(),
683						Some(Obj) => {
684							Obj.get("fsPath")
685								.and_then(|V| V.as_str())
686								.map(str::to_owned)
687								.or_else(|| Obj.get("path").and_then(|V| V.as_str()).map(str::to_owned))
688								.unwrap_or_default()
689						},
690						None => String::new(),
691					};
692					dev_log!("extensions", "extensions:getManifest vsix={}", VsixPath);
693					if VsixPath.is_empty() {
694						Err("extensions:getManifest: missing VSIX path argument".to_string())
695					} else {
696						let Path = std::path::PathBuf::from(&VsixPath);
697						match crate::ExtensionManagement::VsixInstaller::ReadFullManifest(&Path) {
698							Ok(Manifest) => Ok(Manifest),
699							Err(Error) => {
700								dev_log!(
701									"extensions",
702									"warn: [WindServiceHandlers] extensions:getManifest failed for '{}': {}",
703									VsixPath,
704									Error
705								);
706								Err(format!("extensions:getManifest failed: {}", Error))
707							},
708						}
709					}
710				},
711				// Reinstall and metadata-update still no-op for now; reinstall needs
712				// a gallery cache (we only have the on-disk unpack), and metadata
713				// update only matters for ratings/icons/readme which Land does not
714				// track. Left as explicit logs so the UI doesn't silently fail.
715				"extensions:reinstall" | "extensions:updateMetadata" => {
716					dev_log!("extensions", "{} (no-op: no gallery backend)", command);
717					Ok(Value::Null)
718				},
719
720				// Terminal commands
721				"terminal:create" => {
722					dev_log!("terminal", "terminal:create");
723					TerminalCreate(RunTime.clone(), Arguments).await
724				},
725				"terminal:sendText" => {
726					dev_log!("terminal", "terminal:sendText");
727					TerminalSendText(RunTime.clone(), Arguments).await
728				},
729				"terminal:dispose" => {
730					dev_log!("terminal", "terminal:dispose");
731					TerminalDispose(RunTime.clone(), Arguments).await
732				},
733				"terminal:show" => {
734					dev_log!("terminal", "terminal:show");
735					TerminalShow(RunTime.clone(), Arguments).await
736				},
737				"terminal:hide" => {
738					dev_log!("terminal", "terminal:hide");
739					TerminalHide(RunTime.clone(), Arguments).await
740				},
741
742				// Output channel commands
743				"output:create" => OutputCreate(ApplicationHandle.clone(), Arguments).await,
744				"output:append" => {
745					dev_log!("output", "output:append");
746					OutputAppend(ApplicationHandle.clone(), Arguments).await
747				},
748				"output:appendLine" => {
749					dev_log!("output", "output:appendLine");
750					OutputAppendLine(ApplicationHandle.clone(), Arguments).await
751				},
752				"output:clear" => {
753					dev_log!("output", "output:clear");
754					OutputClear(ApplicationHandle.clone(), Arguments).await
755				},
756				"output:show" => {
757					dev_log!("output", "output:show");
758					OutputShow(ApplicationHandle.clone(), Arguments).await
759				},
760
761				// TextFile commands
762				"textFile:read" => {
763					dev_log!("textfile", "textFile:read");
764					TextfileRead(RunTime.clone(), Arguments).await
765				},
766				"textFile:write" => {
767					dev_log!("textfile", "textFile:write");
768					TextfileWrite(RunTime.clone(), Arguments).await
769				},
770				"textFile:save" => TextfileSave(RunTime.clone(), Arguments).await,
771
772				// Storage commands (additional)
773				"storage:delete" => {
774					dev_log!("storage", "storage:delete");
775					StorageDelete(RunTime.clone(), Arguments).await
776				},
777				"storage:keys" => {
778					dev_log!("storage", "storage:keys");
779					StorageKeys(RunTime.clone()).await
780				},
781
782				// Notification commands (emit sky:// events for Sky to render)
783				"notification:show" => {
784					dev_log!("notification", "notification:show");
785					NotificationShow(ApplicationHandle.clone(), Arguments).await
786				},
787				"notification:showProgress" => {
788					dev_log!("notification", "notification:showProgress");
789					NotificationShowProgress(ApplicationHandle.clone(), Arguments).await
790				},
791				"notification:updateProgress" => {
792					dev_log!("notification", "notification:updateProgress");
793					NotificationUpdateProgress(ApplicationHandle.clone(), Arguments).await
794				},
795				"notification:endProgress" => {
796					dev_log!("notification", "notification:endProgress");
797					NotificationEndProgress(ApplicationHandle.clone(), Arguments).await
798				},
799
800				// Progress commands
801				"progress:begin" => {
802					dev_log!("progress", "progress:begin");
803					ProgressBegin(ApplicationHandle.clone(), Arguments).await
804				},
805				"progress:report" => {
806					dev_log!("progress", "progress:report");
807					ProgressReport(ApplicationHandle.clone(), Arguments).await
808				},
809				"progress:end" => {
810					dev_log!("progress", "progress:end");
811					ProgressEnd(ApplicationHandle.clone(), Arguments).await
812				},
813
814				// QuickInput commands
815				"quickInput:showQuickPick" => {
816					dev_log!("quickinput", "quickInput:showQuickPick");
817					QuickInputShowQuickPick(RunTime.clone(), Arguments).await
818				},
819				"quickInput:showInputBox" => {
820					dev_log!("quickinput", "quickInput:showInputBox");
821					QuickInputShowInputBox(RunTime.clone(), Arguments).await
822				},
823
824				// Workspaces commands. VS Code's `IWorkspacesService`
825				// channel uses `getWorkspaceFolders` /
826				// `addWorkspaceFolders`; Mountain's rail uses the
827				// shorter `getFolders` / `addFolder`. Alias both.
828				"workspaces:getFolders" | "workspaces:getWorkspaceFolders" | "workspaces:getWorkspace" => {
829					dev_log!("workspaces", "{}", command);
830					WorkspacesGetFolders(RunTime.clone()).await
831				},
832				"workspaces:addFolder" | "workspaces:addWorkspaceFolders" => {
833					dev_log!("workspaces", "{}", command);
834					WorkspacesAddFolder(RunTime.clone(), Arguments).await
835				},
836				"workspaces:removeFolder" | "workspaces:removeWorkspaceFolders" => {
837					dev_log!("workspaces", "{}", command);
838					WorkspacesRemoveFolder(RunTime.clone(), Arguments).await
839				},
840				"workspaces:getName" => {
841					dev_log!("workspaces", "{}", command);
842					WorkspacesGetName(RunTime.clone()).await
843				},
844				// `onDidChangeWorkspaceFolders` channel-listen: Mountain
845				// broadcasts the change via Tauri event, so ack the
846				// listen request with Null (no-op on the binary rail).
847				"workspaces:onDidChangeWorkspaceFolders" | "workspaces:onDidChangeWorkspaceName" => {
848					dev_log!("workspaces", "{} (stub-ack)", command);
849					Ok(Value::Null)
850				},
851
852				// Themes commands
853				"themes:getActive" => {
854					dev_log!("themes", "themes:getActive");
855					ThemesGetActive(RunTime.clone()).await
856				},
857				"themes:list" => {
858					dev_log!("themes", "themes:list");
859					ThemesList(RunTime.clone()).await
860				},
861				"themes:set" => {
862					dev_log!("themes", "themes:set");
863					ThemesSet(RunTime.clone(), Arguments).await
864				},
865
866				// Search commands. Stock VS Code `SearchService` channel
867				// uses `textSearch` / `fileSearch`; Mountain's Effect-TS
868				// rail uses `findInFiles` / `findFiles`. Alias both.
869				"search:findInFiles" | "search:textSearch" | "search:searchText" => {
870					dev_log!("search", "{}", command);
871					SearchFindInFiles(RunTime.clone(), Arguments).await
872				},
873				"search:findFiles" | "search:fileSearch" | "search:searchFile" => {
874					dev_log!("search", "{}", command);
875					SearchFindFiles(RunTime.clone(), Arguments).await
876				},
877				// Cancellation / onProgress channel methods: workbench's
878				// SearchService listens for these. We have no streaming
879				// search yet, so ack with Null and let the workbench
880				// treat the call as a no-op.
881				"search:cancel" | "search:clearCache" | "search:onDidChangeResult" => {
882					dev_log!("search", "{} (stub-ack)", command);
883					Ok(Value::Null)
884				},
885
886				// Decorations commands
887				"decorations:get" => {
888					dev_log!("decorations", "decorations:get");
889					DecorationsGet(RunTime.clone(), Arguments).await
890				},
891				"decorations:getMany" => {
892					dev_log!("decorations", "decorations:getMany");
893					DecorationsGetMany(RunTime.clone(), Arguments).await
894				},
895				"decorations:set" => {
896					dev_log!("decorations", "decorations:set");
897					DecorationsSet(RunTime.clone(), Arguments).await
898				},
899				"decorations:clear" => {
900					dev_log!("decorations", "decorations:clear");
901					DecorationsClear(RunTime.clone(), Arguments).await
902				},
903
904				// WorkingCopy commands
905				"workingCopy:isDirty" => {
906					dev_log!("workingcopy", "workingCopy:isDirty");
907					WorkingCopyIsDirty(RunTime.clone(), Arguments).await
908				},
909				"workingCopy:setDirty" => {
910					dev_log!("workingcopy", "workingCopy:setDirty");
911					WorkingCopySetDirty(RunTime.clone(), Arguments).await
912				},
913				"workingCopy:getAllDirty" => {
914					dev_log!("workingcopy", "workingCopy:getAllDirty");
915					WorkingCopyGetAllDirty(RunTime.clone()).await
916				},
917				"workingCopy:getDirtyCount" => {
918					dev_log!("workingcopy", "workingCopy:getDirtyCount");
919					WorkingCopyGetDirtyCount(RunTime.clone()).await
920				},
921
922				// Keybinding commands
923				"keybinding:add" => {
924					dev_log!("keybinding", "keybinding:add");
925					KeybindingAdd(RunTime.clone(), Arguments).await
926				},
927				"keybinding:remove" => {
928					dev_log!("keybinding", "keybinding:remove");
929					KeybindingRemove(RunTime.clone(), Arguments).await
930				},
931				"keybinding:lookup" => {
932					dev_log!("keybinding", "keybinding:lookup");
933					KeybindingLookup(RunTime.clone(), Arguments).await
934				},
935				"keybinding:getAll" => {
936					dev_log!("keybinding", "keybinding:getAll");
937					KeybindingGetAll(RunTime.clone()).await
938				},
939
940				// Lifecycle commands
941				"lifecycle:getPhase" => {
942					dev_log!("lifecycle", "lifecycle:getPhase");
943					LifecycleGetPhase(RunTime.clone()).await
944				},
945				"lifecycle:whenPhase" => {
946					dev_log!("lifecycle", "lifecycle:whenPhase");
947					LifecycleWhenPhase(RunTime.clone(), Arguments).await
948				},
949				"lifecycle:requestShutdown" => {
950					dev_log!("lifecycle", "lifecycle:requestShutdown");
951					LifecycleRequestShutdown(ApplicationHandle.clone()).await
952				},
953				"lifecycle:advancePhase" | "lifecycle:setPhase" => {
954					dev_log!("lifecycle", "{}", command);
955					// Wind calls this at the end of every workbench init pass so
956					// the phase advances Starting → Ready → Restored → Eventually.
957					// Mountain emits `sky://lifecycle/phaseChanged` so any extension
958					// host or service waiting on a later phase wakes up.
959					let NewPhase = Arguments.first().and_then(|V| V.as_u64()).unwrap_or(1) as u8;
960					RunTime
961						.Environment
962						.ApplicationState
963						.Feature
964						.Lifecycle
965						.AdvanceAndBroadcast(NewPhase, &ApplicationHandle);
966
967					// Hidden-until-ready: the main window is built with
968					// `.visible(false)` to suppress the four-repaint flash
969					// (native chrome → inline bg → theme CSS → workbench
970					// DOM). Phase 3 = Restored means `.monaco-workbench`
971					// is attached and the first frame is painted; show
972					// the window now so the user's first glimpse is the
973					// finished editor rather than the paint cascade.
974					//
975					// `set_focus()` follows `show()` so keyboard input
976					// routes to the editor immediately on reveal.
977					// Failures are logged but swallowed - if the window
978					// is already visible (phase 3 re-fired from another
979					// consumer) Tauri returns a benign error.
980					if NewPhase >= 3 {
981						if let Some(MainWindow) = ApplicationHandle.get_webview_window("main") {
982							if let Ok(false) = MainWindow.is_visible() {
983								if let Err(Error) = MainWindow.show() {
984									dev_log!(
985										"lifecycle",
986										"warn: [Lifecycle] main window show() failed on phase {}: {}",
987										NewPhase,
988										Error
989									);
990								} else {
991									dev_log!(
992										"lifecycle",
993										"[Lifecycle] main window revealed on phase {} (hidden-until-ready)",
994										NewPhase
995									);
996									let _ = MainWindow.set_focus();
997								}
998							}
999						}
1000					}
1001
1002					Ok(json!(RunTime.Environment.ApplicationState.Feature.Lifecycle.GetPhase()))
1003				},
1004
1005				// Label commands
1006				"label:getUri" => {
1007					dev_log!("label", "label:getUri");
1008					LabelGetURI(RunTime.clone(), Arguments).await
1009				},
1010				"label:getWorkspace" => {
1011					dev_log!("label", "label:getWorkspace");
1012					LabelGetWorkspace(RunTime.clone()).await
1013				},
1014				"label:getBase" => {
1015					dev_log!("label", "label:getBase");
1016					LabelGetBase(Arguments).await
1017				},
1018
1019				// Model (text model registry) commands
1020				"model:open" => {
1021					dev_log!("model", "model:open");
1022					ModelOpen(RunTime.clone(), Arguments).await
1023				},
1024				"model:close" => {
1025					dev_log!("model", "model:close");
1026					ModelClose(RunTime.clone(), Arguments).await
1027				},
1028				"model:get" => {
1029					dev_log!("model", "model:get");
1030					ModelGet(RunTime.clone(), Arguments).await
1031				},
1032				"model:getAll" => {
1033					dev_log!("model", "model:getAll");
1034					ModelGetAll(RunTime.clone()).await
1035				},
1036				"model:updateContent" => {
1037					dev_log!("model", "model:updateContent");
1038					ModelUpdateContent(RunTime.clone(), Arguments).await
1039				},
1040
1041				// Navigation history commands
1042				"history:goBack" => {
1043					dev_log!("history", "history:goBack");
1044					HistoryGoBack(RunTime.clone()).await
1045				},
1046				"history:goForward" => {
1047					dev_log!("history", "history:goForward");
1048					HistoryGoForward(RunTime.clone()).await
1049				},
1050				"history:canGoBack" => {
1051					dev_log!("history", "history:canGoBack");
1052					HistoryCanGoBack(RunTime.clone()).await
1053				},
1054				"history:canGoForward" => {
1055					dev_log!("history", "history:canGoForward");
1056					HistoryCanGoForward(RunTime.clone()).await
1057				},
1058				"history:push" => {
1059					dev_log!("history", "history:push");
1060					HistoryPush(RunTime.clone(), Arguments).await
1061				},
1062				"history:clear" => {
1063					dev_log!("history", "history:clear");
1064					HistoryClear(RunTime.clone()).await
1065				},
1066				"history:getStack" => {
1067					dev_log!("history", "history:getStack");
1068					HistoryGetStack(RunTime.clone()).await
1069				},
1070
1071				// IPC status commands
1072				"mountain_get_status" => {
1073					let status = json!({
1074						"connected": true,
1075						"version": "1.0.0"
1076					});
1077					Ok(status)
1078				},
1079				"mountain_get_configuration" => {
1080					let config = json!({
1081						"editor": { "theme": "dark" },
1082						"extensions": { "installed": [] }
1083					});
1084					Ok(config)
1085				},
1086				"mountain_get_services_status" => {
1087					let services = json!({
1088						"editor": { "status": "running" },
1089						"extensionHost": { "status": "running" }
1090					});
1091					Ok(services)
1092				},
1093				"mountain_get_state" => {
1094					let state = json!({
1095						"ui": {},
1096						"editor": {},
1097						"workspace": {}
1098					});
1099					Ok(state)
1100				},
1101
1102				// =====================================================================
1103				// File system command ALIASES
1104				// VS Code's DiskFileSystemProviderClient calls readFile/writeFile/rename
1105				// but Mountain's original handlers use read/write/move.
1106				// =====================================================================
1107				"file:realpath" => FileRealpath(Arguments).await,
1108				"file:open" => {
1109					dev_log!("vfs", "file:open stub - no fd support yet");
1110					Ok(json!(0))
1111				},
1112				"file:close" => {
1113					dev_log!("vfs", "file:close stub");
1114					Ok(Value::Null)
1115				},
1116				"file:cloneFile" => FileCloneNative(Arguments).await,
1117
1118				// =====================================================================
1119				// Native Host commands (INativeHostService)
1120				// =====================================================================
1121
1122				// Dialogs
1123				"nativeHost:pickFolderAndOpen" => NativePickFolder(ApplicationHandle.clone(), Arguments).await,
1124				"nativeHost:pickFileAndOpen" => NativePickFolder(ApplicationHandle.clone(), Arguments).await,
1125				"nativeHost:pickFileFolderAndOpen" => NativePickFolder(ApplicationHandle.clone(), Arguments).await,
1126				"nativeHost:pickWorkspaceAndOpen" => NativePickFolder(ApplicationHandle.clone(), Arguments).await,
1127				"nativeHost:showOpenDialog" => NativeShowOpenDialog(ApplicationHandle.clone(), Arguments).await,
1128				"nativeHost:showSaveDialog" => {
1129					use tauri_plugin_dialog::DialogExt;
1130					let Options = Arguments.first().cloned().unwrap_or(Value::Null);
1131					let Title = Options.get("title").and_then(Value::as_str).unwrap_or("Save").to_string();
1132					let DefaultPath = Options.get("defaultPath").and_then(Value::as_str).map(str::to_string);
1133					let Handle = ApplicationHandle.clone();
1134					let Joined = tokio::task::spawn_blocking(move || -> Option<String> {
1135						let mut Builder = Handle.dialog().file().set_title(&Title);
1136						if let Some(Path) = DefaultPath.as_deref() {
1137							Builder = Builder.set_directory(Path);
1138						}
1139						Builder.blocking_save_file().map(|P| P.to_string())
1140					})
1141					.await;
1142					match Joined {
1143						Ok(Some(Path)) => Ok(json!({ "canceled": false, "filePath": Path })),
1144						Ok(None) => Ok(json!({ "canceled": true })),
1145						Err(Error) => Err(format!("showSaveDialog join error: {}", Error)),
1146					}
1147				},
1148				"nativeHost:showMessageBox" => {
1149					use tauri_plugin_dialog::{DialogExt, MessageDialogKind};
1150					let Options = Arguments.first().cloned().unwrap_or(Value::Null);
1151					let Message = Options.get("message").and_then(Value::as_str).unwrap_or("").to_string();
1152					let Detail = Options.get("detail").and_then(Value::as_str).map(str::to_string);
1153					let DialogType = Options
1154						.get("type")
1155						.and_then(Value::as_str)
1156						.map(|S| S.to_lowercase())
1157						.unwrap_or_default();
1158					let Title = Options.get("title").and_then(Value::as_str).unwrap_or("").to_string();
1159					let Kind = match DialogType.as_str() {
1160						"warning" | "warn" => MessageDialogKind::Warning,
1161						"error" => MessageDialogKind::Error,
1162						_ => MessageDialogKind::Info,
1163					};
1164					let Handle = ApplicationHandle.clone();
1165					let Joined = tokio::task::spawn_blocking(move || -> bool {
1166						let mut Builder = Handle.dialog().message(&Message).kind(Kind);
1167						if !Title.is_empty() {
1168							Builder = Builder.title(&Title);
1169						}
1170						if let Some(DetailText) = Detail.as_deref() {
1171							Builder = Builder.title(DetailText);
1172						}
1173						Builder.blocking_show()
1174					})
1175					.await;
1176					match Joined {
1177						Ok(Answered) => Ok(json!({ "response": if Answered { 0 } else { 1 } })),
1178						Err(Error) => Err(format!("showMessageBox join error: {}", Error)),
1179					}
1180				},
1181
1182				// Environment paths - called by ResolveConfiguration to get real Tauri paths.
1183				// Returns the session log directory (with timestamp + window1 subdir)
1184				// so VS Code can immediately write output files without stat errors.
1185				"nativeHost:getEnvironmentPaths" => {
1186					let PathResolver = ApplicationHandle.path();
1187					let AppDataDir = PathResolver.app_data_dir().unwrap_or_default();
1188					let HomeDir = PathResolver.home_dir().unwrap_or_default();
1189					let TmpDir = std::env::temp_dir();
1190
1191					// Logs go under {appDataDir}/logs/{sessionTimestamp}/ - same tree as
1192					// all other VS Code data, not Tauri's separate app_log_dir().
1193					// VS Code requires a session-timestamped subdir for log rotation.
1194					// `DevLog::SessionTimestamp` is the single source of truth so that
1195					// `Mountain.dev.log` (written by DevLog) and VS Code's
1196					// `window1/output/*.log` files (written into `logsPath`) share one
1197					// directory per session.
1198					let SessionLogRoot = AppDataDir.join("logs").join(crate::IPC::DevLog::SessionTimestamp::Fn());
1199					let SessionLogWindowDir = SessionLogRoot.join("window1");
1200					let _ = std::fs::create_dir_all(&SessionLogWindowDir);
1201
1202					dev_log!(
1203						"config",
1204						"getEnvironmentPaths: userDataDir={} logsPath={} homeDir={}",
1205						AppDataDir.display(),
1206						SessionLogRoot.display(),
1207						HomeDir.display()
1208					);
1209					let DevLogEnv = std::env::var("Trace").unwrap_or_default();
1210					Ok(json!({
1211						"userDataDir": AppDataDir.to_string_lossy(),
1212						"logsPath": SessionLogRoot.to_string_lossy(),
1213						"homeDir": HomeDir.to_string_lossy(),
1214						"tmpDir": TmpDir.to_string_lossy(),
1215						"devLog": if DevLogEnv.is_empty() { Value::Null } else { json!(DevLogEnv) },
1216					}))
1217				},
1218
1219				// OS info
1220				"nativeHost:getOSColorScheme" => {
1221					dev_log!("nativehost", "nativeHost:getOSColorScheme");
1222					NativeGetColorScheme().await
1223				},
1224				"nativeHost:getOSProperties" => {
1225					dev_log!("nativehost", "nativeHost:getOSProperties");
1226					NativeOSProperties().await
1227				},
1228				"nativeHost:getOSStatistics" => {
1229					dev_log!("nativehost", "nativeHost:getOSStatistics");
1230					NativeOSStatistics().await
1231				},
1232				"nativeHost:getOSVirtualMachineHint" => {
1233					dev_log!("nativehost", "nativeHost:getOSVirtualMachineHint");
1234					Ok(json!(0))
1235				},
1236
1237				// Window state
1238				"nativeHost:isWindowAlwaysOnTop" => {
1239					dev_log!("window", "nativeHost:isWindowAlwaysOnTop");
1240					Ok(json!(false))
1241				},
1242				"nativeHost:isFullScreen" => {
1243					dev_log!("window", "nativeHost:isFullScreen");
1244					NativeIsFullscreen(ApplicationHandle.clone()).await
1245				},
1246				"nativeHost:isMaximized" => {
1247					dev_log!("window", "nativeHost:isMaximized");
1248					NativeIsMaximized(ApplicationHandle.clone()).await
1249				},
1250				"nativeHost:getActiveWindowId" => {
1251					dev_log!("window", "nativeHost:getActiveWindowId");
1252					Ok(json!(1))
1253				},
1254				// LAND-FIX: workbench polls the cursor screen point for
1255				// hover hint / context-menu placement. Stock VS Code
1256				// returns the OS cursor location via Electron's
1257				// `screen.getCursorScreenPoint()`. Tauri/Wry on macOS
1258				// does not expose a stable equivalent (CGEvent location
1259				// works but adds an Objective-C trampoline per call).
1260				// Returning `{x:0, y:0}` is what stock VS Code itself
1261				// returns when no display is active; this is also what
1262				// Cocoon falls back to. Workbench uses the value only
1263				// to bias overlay placement; (0,0) places overlays at
1264				// the top-left of the active window which the layout
1265				// engine then clips to a sane position. The cost of
1266				// the unknown-IPC log spam outweighs the precision
1267				// loss.
1268				"nativeHost:getCursorScreenPoint" => {
1269					dev_log!("window", "nativeHost:getCursorScreenPoint");
1270					Ok(json!({ "x": 0, "y": 0 }))
1271				},
1272				"nativeHost:getWindows" => Ok(json!([{ "id": 1, "title": "Land", "filename": "" }])),
1273				"nativeHost:getWindowCount" => Ok(json!(1)),
1274
1275				// Auxiliary window spawners. VS Code's `nativeHostMainService.ts`
1276				// exposes `openAgentsWindow`, `openDevToolsWindow`, and
1277				// `openAuxiliaryWindow`, and Sky/Wind route these through the
1278				// `nativeHost:<method>` IPC channel. Without stubs, every call fires
1279				// `land:ipc:error:nativeHost.openAgentsWindow` in PostHog (1499
1280				// occurrences per the 2026-04-21 error report). Land doesn't have
1281				// AgentsView yet, so these are no-op acknowledgements - the calling
1282				// extension treats `undefined` as "window wasn't opened" rather than
1283				// an error.
1284				"nativeHost:openAgentsWindow" | "nativeHost:openDevToolsWindow" | "nativeHost:openAuxiliaryWindow" => {
1285					dev_log!("window", "{} (acknowledged, no-op - aux window unsupported)", command);
1286					Ok(Value::Null)
1287				},
1288
1289				// Window control - wired through the Tauri webview-window API so
1290				// focus/minimize/maximize/toggleFullScreen/close actually move the
1291				// native window the same way VS Code's Electron path does.
1292				"nativeHost:focusWindow" => {
1293					dev_log!("window", "{}", command);
1294					if let Some(Window) = ApplicationHandle.get_webview_window("main") {
1295						let _ = Window.set_focus();
1296					}
1297					Ok(Value::Null)
1298				},
1299				"nativeHost:maximizeWindow" => {
1300					dev_log!("window", "{}", command);
1301					if let Some(Window) = ApplicationHandle.get_webview_window("main") {
1302						let _ = Window.maximize();
1303					}
1304					Ok(Value::Null)
1305				},
1306				"nativeHost:unmaximizeWindow" => {
1307					dev_log!("window", "{}", command);
1308					if let Some(Window) = ApplicationHandle.get_webview_window("main") {
1309						let _ = Window.unmaximize();
1310					}
1311					Ok(Value::Null)
1312				},
1313				"nativeHost:minimizeWindow" => {
1314					dev_log!("window", "{}", command);
1315					if let Some(Window) = ApplicationHandle.get_webview_window("main") {
1316						let _ = Window.minimize();
1317					}
1318					Ok(Value::Null)
1319				},
1320				"nativeHost:toggleFullScreen" => {
1321					dev_log!("window", "{}", command);
1322					if let Some(Window) = ApplicationHandle.get_webview_window("main") {
1323						let IsFullscreen = Window.is_fullscreen().unwrap_or(false);
1324						let _ = Window.set_fullscreen(!IsFullscreen);
1325					}
1326					Ok(Value::Null)
1327				},
1328				"nativeHost:closeWindow" => {
1329					dev_log!("window", "{}", command);
1330					// `destroy()` tears the window down without firing
1331					// `CloseRequested` again, which lets us safely exit the
1332					// `prevent_close` intercept registered in AppLifecycle.
1333					// `close()` re-enters the intercept and the window
1334					// becomes unkillable.
1335					if let Some(Window) = ApplicationHandle.get_webview_window("main") {
1336						let _ = Window.destroy();
1337					}
1338					Ok(Value::Null)
1339				},
1340				"nativeHost:setWindowAlwaysOnTop" => {
1341					dev_log!("window", "{}", command);
1342					let OnTop = Arguments.first().and_then(|V| V.as_bool()).unwrap_or(false);
1343					if let Some(Window) = ApplicationHandle.get_webview_window("main") {
1344						let _ = Window.set_always_on_top(OnTop);
1345					}
1346					Ok(Value::Null)
1347				},
1348				"nativeHost:toggleWindowAlwaysOnTop" => {
1349					dev_log!("window", "{}", command);
1350					// Tauri doesn't expose a "get always on top" accessor on all
1351					// platforms, so toggle by tracking state via the webview title
1352					// prefix as a proxy. In practice the UI will call
1353					// `setWindowAlwaysOnTop` with an explicit bool immediately after,
1354					// so a best-effort flip is enough.
1355					if let Some(Window) = ApplicationHandle.get_webview_window("main") {
1356						let _ = Window.set_always_on_top(true);
1357					}
1358					Ok(Value::Null)
1359				},
1360				"nativeHost:setRepresentedFilename" => {
1361					dev_log!("window", "{}", command);
1362					#[cfg(target_os = "macos")]
1363					{
1364						let Path = Arguments.first().and_then(|V| V.as_str()).unwrap_or("").to_string();
1365						if !Path.is_empty() {
1366							if let Some(Window) = ApplicationHandle.get_webview_window("main") {
1367								let _ = Window.set_title(&Path);
1368							}
1369						}
1370					}
1371					let _ = (&Arguments, &ApplicationHandle);
1372					Ok(Value::Null)
1373				},
1374
1375				// Pure no-op arms - pure lifecycle signals VS Code fires regardless
1376				// of the backing host (Electron, Mountain, Browser) but we don't
1377				// need to do anything about. Kept named so the `Unknown IPC command`
1378				// default branch never fires for them.
1379				"nativeHost:updateWindowControls"
1380				| "nativeHost:setMinimumSize"
1381				| "nativeHost:notifyReady"
1382				| "nativeHost:saveWindowSplash"
1383				| "nativeHost:updateTouchBar"
1384				| "nativeHost:moveWindowTop"
1385				| "nativeHost:positionWindow"
1386				| "nativeHost:setDocumentEdited"
1387				| "nativeHost:setBackgroundThrottling"
1388				| "nativeHost:updateWindowAccentColor" => {
1389					dev_log!("window", "{}", command);
1390					Ok(Value::Null)
1391				},
1392
1393				// OS operations
1394				"nativeHost:isAdmin" => Ok(json!(false)),
1395				"nativeHost:isRunningUnderARM64Translation" => {
1396					#[cfg(target_os = "macos")]
1397					{
1398						// macOS: check if running under Rosetta 2
1399						let Output = std::process::Command::new("sysctl")
1400							.args(["-n", "sysctl.proc_translated"])
1401							.output();
1402						let IsTranslated = Output
1403							.ok()
1404							.map(|O| String::from_utf8_lossy(&O.stdout).trim() == "1")
1405							.unwrap_or(false);
1406						Ok(json!(IsTranslated))
1407					}
1408					#[cfg(not(target_os = "macos"))]
1409					{
1410						Ok(json!(false))
1411					}
1412				},
1413				"nativeHost:hasWSLFeatureInstalled" => {
1414					#[cfg(target_os = "windows")]
1415					{
1416						Ok(json!(std::path::Path::new("C:\\Windows\\System32\\wsl.exe").exists()))
1417					}
1418					#[cfg(not(target_os = "windows"))]
1419					{
1420						Ok(json!(false))
1421					}
1422				},
1423				"nativeHost:showItemInFolder" => ShowItemInFolder(RunTime.clone(), Arguments).await,
1424				"nativeHost:openExternal" => OpenExternal(RunTime.clone(), Arguments).await,
1425				// `workbench.files.action.deleteFile` and extensions that delete
1426				// files both round-trip through here. Route to the platform's
1427				// trash bin so deletions are recoverable. macOS uses AppleScript
1428				// via `osascript`; Linux prefers `gio trash` then `trash` if
1429				// installed; Windows uses PowerShell with Shell.NameSpace.
1430				"nativeHost:moveItemToTrash" => {
1431					let Path = Arguments.first().and_then(|V| V.as_str()).unwrap_or("").to_string();
1432					if Path.is_empty() {
1433						Ok(json!(false))
1434					} else {
1435						dev_log!("nativehost", "nativeHost:moveItemToTrash path={}", Path);
1436						let Moved = {
1437							#[cfg(target_os = "macos")]
1438							{
1439								tokio::process::Command::new("osascript")
1440									.args([
1441										"-e",
1442										&format!(
1443											"tell application \"Finder\" to delete POSIX file \"{}\"",
1444											Path.replace('"', "\\\"")
1445										),
1446									])
1447									.status()
1448									.await
1449									.map(|S| S.success())
1450									.unwrap_or(false)
1451							}
1452							#[cfg(target_os = "linux")]
1453							{
1454								let Gio = tokio::process::Command::new("gio")
1455									.args(["trash", &Path])
1456									.status()
1457									.await
1458									.map(|S| S.success())
1459									.unwrap_or(false);
1460								if Gio {
1461									true
1462								} else {
1463									tokio::process::Command::new("trash")
1464										.arg(&Path)
1465										.status()
1466										.await
1467										.map(|S| S.success())
1468										.unwrap_or(false)
1469								}
1470							}
1471							#[cfg(target_os = "windows")]
1472							{
1473								let Script = format!(
1474									"(new-object -comobject Shell.Application).NameSpace(0xA).MoveHere('{}')",
1475									Path.replace('\'', "''")
1476								);
1477								tokio::process::Command::new("powershell.exe")
1478									.args(["-NoProfile", "-Command", &Script])
1479									.status()
1480									.await
1481									.map(|S| S.success())
1482									.unwrap_or(false)
1483							}
1484							#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
1485							{
1486								false
1487							}
1488						};
1489						Ok(json!(Moved))
1490					}
1491				},
1492
1493				// Clipboard - backed by `arboard` so read/writeText round-trip the
1494				// OS clipboard. `readClipboardBuffer` is kept empty (binary
1495				// clipboard is rarely used by VS Code core; extensions that need
1496				// it invoke the platform-specific path instead).
1497				"nativeHost:readClipboardText" => {
1498					dev_log!("clipboard", "readClipboardText");
1499					match arboard::Clipboard::new() {
1500						Ok(mut Cb) => Ok(json!(Cb.get_text().unwrap_or_default())),
1501						Err(_) => Ok(json!("")),
1502					}
1503				},
1504				"nativeHost:writeClipboardText" => {
1505					dev_log!("clipboard", "writeClipboardText");
1506					let Text = Arguments.first().and_then(|V| V.as_str()).unwrap_or("").to_string();
1507					if let Ok(mut Cb) = arboard::Clipboard::new() {
1508						let _ = Cb.set_text(Text);
1509					}
1510					Ok(Value::Null)
1511				},
1512				"nativeHost:readClipboardFindText" => {
1513					dev_log!("clipboard", "readClipboardFindText");
1514					// macOS has a separate find pasteboard; reuse the general
1515					// clipboard for parity with VS Code on Linux/Windows.
1516					match arboard::Clipboard::new() {
1517						Ok(mut Cb) => Ok(json!(Cb.get_text().unwrap_or_default())),
1518						Err(_) => Ok(json!("")),
1519					}
1520				},
1521				"nativeHost:writeClipboardFindText" => {
1522					dev_log!("clipboard", "writeClipboardFindText");
1523					let Text = Arguments.first().and_then(|V| V.as_str()).unwrap_or("").to_string();
1524					if let Ok(mut Cb) = arboard::Clipboard::new() {
1525						let _ = Cb.set_text(Text);
1526					}
1527					Ok(Value::Null)
1528				},
1529				"nativeHost:readClipboardBuffer" => {
1530					dev_log!("clipboard", "readClipboardBuffer");
1531					Ok(json!([]))
1532				},
1533				"nativeHost:writeClipboardBuffer" => {
1534					dev_log!("clipboard", "writeClipboardBuffer");
1535					Ok(Value::Null)
1536				},
1537				"nativeHost:hasClipboard" => {
1538					dev_log!("clipboard", "hasClipboard");
1539					Ok(json!(false))
1540				},
1541				"nativeHost:readImage" => {
1542					dev_log!("clipboard", "readImage");
1543					Ok(json!([]))
1544				},
1545				"nativeHost:triggerPaste" => {
1546					dev_log!("clipboard", "triggerPaste");
1547					Ok(Value::Null)
1548				},
1549
1550				// Process
1551				"nativeHost:getProcessId" => Ok(json!(std::process::id())),
1552				"nativeHost:killProcess" => Ok(Value::Null),
1553
1554				// Network
1555				"nativeHost:findFreePort" => NativeFindFreePort(Arguments).await,
1556				"nativeHost:isPortFree" => Ok(json!(true)),
1557				"nativeHost:resolveProxy" => Ok(Value::Null),
1558				"nativeHost:lookupAuthorization" => Ok(Value::Null),
1559				"nativeHost:lookupKerberosAuthorization" => Ok(Value::Null),
1560				"nativeHost:loadCertificates" => Ok(json!([])),
1561
1562				// Lifecycle
1563				"nativeHost:relaunch" => Ok(Value::Null),
1564				"nativeHost:reload" => Ok(Value::Null),
1565				"nativeHost:quit" => Ok(Value::Null),
1566				"nativeHost:exit" => Ok(Value::Null),
1567
1568				// Dev tools
1569				"nativeHost:openDevTools" => Ok(Value::Null),
1570				"nativeHost:toggleDevTools" => Ok(Value::Null),
1571
1572				// Power
1573				"nativeHost:getSystemIdleState" => Ok(json!("active")),
1574				"nativeHost:getSystemIdleTime" => Ok(json!(0)),
1575				"nativeHost:getCurrentThermalState" => Ok(json!("nominal")),
1576				"nativeHost:isOnBatteryPower" => Ok(json!(false)),
1577				"nativeHost:startPowerSaveBlocker" => Ok(json!(0)),
1578				"nativeHost:stopPowerSaveBlocker" => Ok(json!(false)),
1579				"nativeHost:isPowerSaveBlockerStarted" => Ok(json!(false)),
1580
1581				// macOS specific
1582				"nativeHost:newWindowTab" => Ok(Value::Null),
1583				"nativeHost:showPreviousWindowTab" => Ok(Value::Null),
1584				"nativeHost:showNextWindowTab" => Ok(Value::Null),
1585				"nativeHost:moveWindowTabToNewWindow" => Ok(Value::Null),
1586				"nativeHost:mergeAllWindowTabs" => Ok(Value::Null),
1587				"nativeHost:toggleWindowTabsBar" => Ok(Value::Null),
1588				"nativeHost:installShellCommand" => Ok(Value::Null),
1589				"nativeHost:uninstallShellCommand" => Ok(Value::Null),
1590
1591				// =====================================================================
1592				// Local PTY (terminal) commands
1593				// =====================================================================
1594				"localPty:getProfiles" => {
1595					dev_log!("terminal", "localPty:getProfiles");
1596					LocalPTYGetProfiles().await
1597				},
1598				"localPty:getDefaultSystemShell" => {
1599					dev_log!("terminal", "localPty:getDefaultSystemShell");
1600					LocalPTYGetDefaultShell().await
1601				},
1602				"localPty:getTerminalLayoutInfo" => {
1603					dev_log!("terminal", "localPty:getTerminalLayoutInfo");
1604					Ok(Value::Null)
1605				},
1606				"localPty:setTerminalLayoutInfo" => {
1607					dev_log!("terminal", "localPty:setTerminalLayoutInfo");
1608					Ok(Value::Null)
1609				},
1610				"localPty:getPerformanceMarks" => {
1611					dev_log!("terminal", "localPty:getPerformanceMarks");
1612					Ok(json!([]))
1613				},
1614				"localPty:reduceConnectionGraceTime" => {
1615					dev_log!("terminal", "localPty:reduceConnectionGraceTime");
1616					Ok(Value::Null)
1617				},
1618				"localPty:listProcesses" => {
1619					dev_log!("terminal", "localPty:listProcesses");
1620					Ok(json!([]))
1621				},
1622				"localPty:getEnvironment" => {
1623					dev_log!("terminal", "localPty:getEnvironment");
1624					LocalPTYGetEnvironment().await
1625				},
1626				// `IPtyService.getLatency` (per
1627				// `vs/platform/terminal/common/terminal.ts:341`) returns
1628				// `IPtyHostLatencyMeasurement[]`. The workbench polls this
1629				// to drive its "renderer ↔ pty host" health UI. We have
1630				// no separate pty host (Mountain spawns PTYs in-process),
1631				// so latency is effectively zero - return an empty array
1632				// matching the "no measurements available" branch the
1633				// workbench already handles. Without this route the call
1634				// surfaced as `Unknown IPC command: localPty:getLatency`
1635				// every poll cycle, and the renderer logged a
1636				// `TauriInvoke ok=false` line per attempt.
1637				"localPty:getLatency" => {
1638					dev_log!("terminal", "localPty:getLatency");
1639					Ok(json!([]))
1640				},
1641
1642				// `cocoon:request` - generic renderer→Cocoon RPC bridge.
1643				// Used by Sky-side bridges that need to dispatch a request
1644				// into the extension host (e.g. `webview.resolveView` to
1645				// trigger an extension's `resolveWebviewView` callback).
1646				// Wire shape: `params = [Method, Payload]`. Mountain
1647				// forwards to Cocoon via `Vine::Client::SendRequest` and
1648				// returns the response verbatim. Failure surfaces as a
1649				// stringified error so the renderer can fall through to
1650				// its alternative path (CustomEvent fan-out for legacy
1651				// observers).
1652				"cocoon:request" => {
1653					dev_log!("ipc", "cocoon:request method={:?}", Arguments.first());
1654					let MethodOpt = Arguments.first().and_then(|V| V.as_str()).map(|S| S.to_string());
1655					match MethodOpt {
1656						None => Err("cocoon:request requires method string in slot 0".to_string()),
1657						Some(Method) => {
1658							let Payload = Arguments.get(1).cloned().unwrap_or(Value::Null);
1659							// Same boot-race guard as `tree:getChildren`: the
1660							// renderer can dispatch `cocoon:request` (e.g.
1661							// `webview.resolveView`) before Cocoon's gRPC
1662							// handshake completes. Wait briefly so the first
1663							// few calls don't fail spuriously and poison
1664							// renderer-side caches.
1665							// Bumped 1500 -> 5000 ms - bundled-electron boot trace
1666							// shows Cocoon's `Successfully connected` lands ~620
1667							// log lines AFTER the workbench's first request, so
1668							// the 1.5 s wait routinely expired before Cocoon was
1669							// up. Sky-side caches captured the empty fallback
1670							// (Explorer empty, webview.resolveView=ClientNotConnected,
1671							// etc.) and panes never recovered.
1672							let _ = crate::Vine::Client::WaitForClientConnection::Fn("cocoon-main", 5000).await;
1673							crate::Vine::Client::SendRequest::Fn("cocoon-main", Method.clone(), Payload, 30_000)
1674								.await
1675								.map_err(|Error| format!("cocoon:request {} failed: {:?}", Method, Error))
1676						},
1677					}
1678				},
1679
1680				// `cocoon:notify` - fire-and-forget renderer→Cocoon
1681				// notification bridge. Companion to `cocoon:request` for
1682				// one-way wire methods (`webview.message`,
1683				// `webview.dispose`, `webview.viewState` etc.) where the
1684				// extension doesn't reply. Avoids the 30s request
1685				// timeout penalty when the renderer just wants to push
1686				// data into the extension host. Wire shape:
1687				// `params = [Method, Payload]`. Returns null
1688				// immediately; the notification dispatches asynchronously.
1689				"cocoon:notify" => {
1690					dev_log!("ipc", "cocoon:notify method={:?}", Arguments.first());
1691					let MethodOpt = Arguments.first().and_then(|V| V.as_str()).map(|S| S.to_string());
1692					match MethodOpt {
1693						None => Err("cocoon:notify requires method string in slot 0".to_string()),
1694						Some(Method) => {
1695							let Payload = Arguments.get(1).cloned().unwrap_or(Value::Null);
1696							if let Err(Error) = crate::Vine::Client::SendNotification::Fn(
1697								"cocoon-main".to_string(),
1698								Method.clone(),
1699								Payload,
1700							)
1701							.await
1702							{
1703								dev_log!("ipc", "warn: [cocoon:notify] {} failed: {:?}", Method, Error);
1704							}
1705							Ok(Value::Null)
1706						},
1707					}
1708				},
1709
1710				// BATCH-19 Part B: VS Code's `LocalPtyService` talks to Mountain via
1711				// the `localPty:*` channel. The internal implementations reuse the
1712				// Tauri-side `terminal:*` handlers so PTY lifecycle stays identical
1713				// regardless of whether the request came from Sky (Wind) or from an
1714				// extension (Cocoon → Wind channel bridge).
1715				//
1716				// CONTRACT NOTE: `IPtyService.createProcess` is typed
1717				// `Promise<number>` (see `vs/platform/terminal/common/terminal.ts:
1718				// 316`). The workbench then does `new LocalPty(id, ...)` and
1719				// `this._ptys.set(id, pty)`. If we return the full
1720				// `{id,name,pid}` object the renderer keys `_ptys` by that
1721				// object, every `_ptys.get(<integer>)` lookup from
1722				// `onProcessData`/`onProcessReady` returns `undefined`, and
1723				// xterm receives zero bytes - the terminal panel renders
1724				// blank even though Mountain's PTY reader emits data
1725				// continuously. Strip down to the integer id here.
1726				"localPty:spawn" => {
1727					// `localPty:spawn` is Cocoon's Sky bridge path; preserve
1728					// the full `{id, name, pid}` shape because the older Wind
1729					// callers expect it. New `localPty:createProcess` and
1730					// `localPty:start` follow VS Code's typed contract below.
1731					dev_log!("terminal", "{}", command);
1732					TerminalCreate(RunTime.clone(), Arguments).await
1733				},
1734				"localPty:createProcess" => {
1735					dev_log!("terminal", "{}", command);
1736					match TerminalCreate(RunTime.clone(), Arguments).await {
1737						Ok(Response) => {
1738							// Extract the integer id - this is what
1739							// `IPtyService.createProcess` is contractually
1740							// required to return.
1741							let TerminalIdOption = Response.get("id").and_then(serde_json::Value::as_u64);
1742							match TerminalIdOption {
1743								Some(TerminalId) if TerminalId > 0 => Ok(serde_json::json!(TerminalId)),
1744								Some(_) | None => {
1745									// Defensive: if `CreateTerminal` returned
1746									// without a usable id (shape drift or
1747									// `GetNextTerminalIdentifier` regression),
1748									// surface the error to the workbench
1749									// instead of returning `0`. The workbench
1750									// would otherwise bind `LocalPty(0, …)`
1751									// and every subsequent `_proxy.input(0,
1752									// data)` would fail silently because no
1753									// PTY with id=0 exists - keystrokes get
1754									// swallowed with no diagnostic.
1755									dev_log!(
1756										"terminal",
1757										"error: [localPty:createProcess] CreateTerminal returned no usable id; \
1758										 response={:?}",
1759										Response
1760									);
1761									Err(format!(
1762										"localPty:createProcess: CreateTerminal returned no terminal id (response={})",
1763										Response
1764									))
1765								},
1766							}
1767						},
1768						Err(Error) => Err(Error),
1769					}
1770				},
1771				"localPty:start" => {
1772					// Eager-spawn pattern: `TerminalProvider::CreateTerminal`
1773					// already started the shell and reader task during
1774					// `localPty:createProcess`. `start` is a no-op that just
1775					// completes the workbench's launch promise. Returning
1776					// `Value::Null` matches `IPtyService.start`'s
1777					// `Promise<ITerminalLaunchError | ITerminalLaunchResult |
1778					// undefined>` (`undefined` branch). Routing this back
1779					// through `TerminalCreate` would spawn a SECOND
1780					// PTY for the same workbench terminal - the user-visible
1781					// pane is bound to id=1 from `createProcess`, but a
1782					// shadow PTY (id=2) starts and streams data nobody
1783					// renders.
1784					dev_log!("terminal", "{} no-op (eager-spawn)", command);
1785					Ok(Value::Null)
1786				},
1787				"localPty:input" | "localPty:write" => {
1788					dev_log!("terminal", "{}", command);
1789					TerminalSendText(RunTime.clone(), Arguments).await
1790				},
1791				"localPty:shutdown" | "localPty:dispose" => {
1792					dev_log!("terminal", "{}", command);
1793					TerminalDispose(RunTime.clone(), Arguments).await
1794				},
1795				"localPty:resize" => {
1796					dev_log!("terminal", "localPty:resize");
1797					// Forward through the Terminal.Resize effect so the PTY master
1798					// receives SIGWINCH. Arguments from VS Code arrive as either
1799					// `[id, cols, rows]` or `{ id, cols, rows }`; accept both.
1800					//
1801					// Defensive clamping: portable-pty's `master.resize()`
1802					// crashes the IO thread with "size out of range" on
1803					// `cols=0` or `rows=0` (the workbench occasionally
1804					// emits 0×0 during pane drag-storms before the
1805					// `requestAnimationFrame` settle). Clamp to sane
1806					// minimums so a transient micro-size never tears
1807					// down the shell.
1808					let (TerminalId, Columns, Rows) = {
1809						let First = Arguments.first().cloned().unwrap_or(Value::Null);
1810						if First.is_object() {
1811							let Id = First.get("id").and_then(|V| V.as_u64()).unwrap_or(0);
1812							let C = First.get("cols").and_then(|V| V.as_u64()).unwrap_or(80) as u16;
1813							let R = First.get("rows").and_then(|V| V.as_u64()).unwrap_or(24) as u16;
1814							(Id, C, R)
1815						} else {
1816							let Id = Arguments.get(0).and_then(|V| V.as_u64()).unwrap_or(0);
1817							let C = Arguments.get(1).and_then(|V| V.as_u64()).unwrap_or(80) as u16;
1818							let R = Arguments.get(2).and_then(|V| V.as_u64()).unwrap_or(24) as u16;
1819							(Id, C, R)
1820						}
1821					};
1822					if TerminalId == 0 {
1823						Ok(Value::Null)
1824					} else {
1825						let Columns = if Columns == 0 { 1 } else { Columns };
1826						let Rows = if Rows == 0 { 1 } else { Rows };
1827						use CommonLibrary::{
1828							Environment::Requires::Requires,
1829							Terminal::TerminalProvider::TerminalProvider,
1830						};
1831						let Provider:Arc<dyn TerminalProvider> = RunTime.Environment.Require();
1832						match Provider.ResizeTerminal(TerminalId, Columns, Rows).await {
1833							Ok(_) => Ok(Value::Null),
1834							Err(Error) => {
1835								// Resize on a disposed terminal is a common
1836								// race during shutdown - the workbench layout
1837								// pass fires after the user types `exit`, the
1838								// PTY closes, and the resize call lands on a
1839								// dropped master. Logging at warn instead of
1840								// error keeps the noise down. Returning
1841								// `Value::Null` (rather than a hard error)
1842								// lets the workbench's resize loop continue
1843								// instead of stalling on the failed promise.
1844								dev_log!(
1845									"terminal",
1846									"warn: localPty:resize id={} cols={} rows={} failed: {}",
1847									TerminalId,
1848									Columns,
1849									Rows,
1850									Error
1851								);
1852								Ok(Value::Null)
1853							},
1854						}
1855					}
1856				},
1857				"localPty:acknowledgeDataEvent" => {
1858					// xterm flow-control heartbeat; no-op on Mountain side.
1859					Ok(Value::Null)
1860				},
1861				// The remaining `localPty:*` endpoints declared by VS Code's
1862				// `ILocalPtyService` are lifecycle-/title-style hooks the extension
1863				// host calls even when there is no terminal running. They become
1864				// no-ops here so the workbench doesn't deadlock on a missing route.
1865				"localPty:processBinary"
1866				| "localPty:attachToProcess"
1867				| "localPty:detachFromProcess"
1868				| "localPty:orphanQuestionReply"
1869				| "localPty:updateTitle"
1870				| "localPty:updateIcon"
1871				| "localPty:refreshProperty"
1872				| "localPty:updateProperty"
1873				| "localPty:getRevivedPtyNewId"
1874				| "localPty:freePortKillProcess"
1875				| "localPty:reviveTerminalProcesses"
1876				| "localPty:getBackendOS"
1877				| "localPty:installAutoReply"
1878				| "localPty:uninstallAllAutoReplies"
1879				| "localPty:serializeTerminalState" => Ok(Value::Null),
1880
1881				// =====================================================================
1882				// Update service
1883				// =====================================================================
1884				"update:_getInitialState" => {
1885					dev_log!("update", "update:_getInitialState");
1886					Ok(json!({ "type": "idle", "updateType": 0 }))
1887				},
1888				"update:isLatestVersion" => {
1889					dev_log!("update", "update:isLatestVersion");
1890					Ok(json!(true))
1891				},
1892				"update:checkForUpdates" => {
1893					dev_log!("update", "update:checkForUpdates");
1894					Ok(Value::Null)
1895				},
1896				"update:downloadUpdate" => {
1897					dev_log!("update", "update:downloadUpdate");
1898					Ok(Value::Null)
1899				},
1900				"update:applyUpdate" => {
1901					dev_log!("update", "update:applyUpdate");
1902					Ok(Value::Null)
1903				},
1904				"update:quitAndInstall" => {
1905					dev_log!("update", "update:quitAndInstall");
1906					Ok(Value::Null)
1907				},
1908
1909				// =====================================================================
1910				// Menubar
1911				// =====================================================================
1912				//
1913				// VS Code emits `updateMenubar` every time a relevant state flips:
1914				// active editor, dirty marker, selection. A cold boot fires the call
1915				// ~20× in the first few seconds, and every one triggers an AppKit
1916				// re-render on macOS (≈ 200 ms each). We coalesce adjacent calls
1917				// through a 50 ms debouncer so only the last pending state actually
1918				// hits the native menu. Semantics match VS Code's
1919				// `ElectronMenubarControl._updateMenu` scheduler.
1920				"menubar:updateMenubar" => {
1921					use std::{
1922						sync::{Arc, Mutex as StandardMutex, OnceLock},
1923						time::Duration,
1924					};
1925
1926					use tokio::task::JoinHandle;
1927					type MenubarCell = StandardMutex<(Option<JoinHandle<()>>, u64)>;
1928					static MENUBAR_DEBOUNCE:OnceLock<Arc<MenubarCell>> = OnceLock::new();
1929					let Cell = MENUBAR_DEBOUNCE.get_or_init(|| Arc::new(StandardMutex::new((None, 0)))).clone();
1930
1931					if let Ok(mut Guard) = Cell.lock() {
1932						if let Some(Pending) = Guard.0.take() {
1933							Pending.abort();
1934						}
1935						Guard.1 = Guard.1.saturating_add(1);
1936						let CellForTask = Cell.clone();
1937						Guard.0 = Some(tokio::spawn(async move {
1938							tokio::time::sleep(Duration::from_millis(50)).await;
1939							let Coalesced = if let Ok(mut Post) = CellForTask.lock() {
1940								let N = Post.1;
1941								Post.1 = 0;
1942								Post.0 = None;
1943								N
1944							} else {
1945								0
1946							};
1947							dev_log!("menubar", "menubar:updateMenubar (applied, coalesced {} pending)", Coalesced);
1948						}));
1949					} else {
1950						dev_log!("menubar", "menubar:updateMenubar (debouncer lock poisoned)");
1951					}
1952					Ok(Value::Null)
1953				},
1954
1955				// =====================================================================
1956				// URL handler
1957				// =====================================================================
1958				"url:registerExternalUriOpener" => {
1959					dev_log!("url", "url:registerExternalUriOpener");
1960					Ok(Value::Null)
1961				},
1962
1963				// =====================================================================
1964				// Encryption
1965				// =====================================================================
1966				"encryption:encrypt" => {
1967					dev_log!("encryption", "encryption:encrypt");
1968					Ok(json!(""))
1969				},
1970				"encryption:decrypt" => {
1971					dev_log!("encryption", "encryption:decrypt");
1972					Ok(json!(""))
1973				},
1974
1975				// =====================================================================
1976				// Extension host starter
1977				// =====================================================================
1978				"extensionHostStarter:createExtensionHost" => {
1979					dev_log!("exthost", "extensionHostStarter:createExtensionHost");
1980					Ok(json!({ "id": "1" }))
1981				},
1982				"extensionHostStarter:start" => {
1983					// The renderer uses this PID to correlate extension-host-side
1984					// debug adapters with the actual Node.js process. That process
1985					// is Cocoon, not Mountain - returning `std::process::id()`
1986					// here would point the debugger at Mountain's Rust binary.
1987					// Fall back to Mountain's PID only if Cocoon hasn't spawned
1988					// yet (should not happen for a real extension-host start).
1989					let Pid =
1990						crate::ProcessManagement::CocoonManagement::GetCocoonPid().unwrap_or_else(std::process::id);
1991					dev_log!("exthost", "extensionHostStarter:start pid={}", Pid);
1992					Ok(json!({ "pid": Pid }))
1993				},
1994				"extensionHostStarter:kill" => {
1995					dev_log!("exthost", "extensionHostStarter:kill");
1996					Ok(Value::Null)
1997				},
1998				"extensionHostStarter:getExitInfo" => {
1999					dev_log!("exthost", "extensionHostStarter:getExitInfo");
2000					Ok(json!({ "code": null, "signal": null }))
2001				},
2002
2003				// =====================================================================
2004				// Extension host message relay (Wind → Mountain → Cocoon)
2005				// =====================================================================
2006				"cocoon:extensionHostMessage" => {
2007					let ByteCount = Arguments
2008						.first()
2009						.map(|P| P.get("data").and_then(|D| D.as_array()).map(|A| A.len()).unwrap_or(0))
2010						.unwrap_or(0);
2011					dev_log!("exthost", "cocoon:extensionHostMessage bytes={}", ByteCount);
2012
2013					// Forward binary message to Cocoon via gRPC GenericNotification.
2014					// Fire-and-forget - the extension host protocol is async.
2015					let Payload = Arguments.first().cloned().unwrap_or(Value::Null);
2016					tokio::spawn(async move {
2017						if let Err(Error) = crate::Vine::Client::SendNotification::Fn(
2018							"cocoon-main".to_string(),
2019							"extensionHostMessage".to_string(),
2020							Payload,
2021						)
2022						.await
2023						{
2024							dev_log!("exthost", "cocoon:extensionHostMessage forward failed: {}", Error);
2025						}
2026					});
2027					Ok(Value::Null)
2028				},
2029
2030				// =====================================================================
2031				// Extension host debug service
2032				// =====================================================================
2033				"extensionhostdebugservice:reload" => {
2034					dev_log!("exthost", "extensionhostdebugservice:reload");
2035					// Trigger a real Cocoon restart via the shutdown notification
2036					// followed by a fresh bootstrap. For the current sprint we emit
2037					// the request for Wind so it can tear down caches, the actual
2038					// spawn lives downstream.
2039					use tauri::Emitter;
2040					if let Err(Error) = ApplicationHandle.emit(SkyEvent::ExtHostDebugReload.AsStr(), json!({})) {
2041						dev_log!("exthost", "warn: extensionhostdebugservice:reload emit failed: {}", Error);
2042					}
2043					Ok(Value::Null)
2044				},
2045				"extensionhostdebugservice:close" => {
2046					dev_log!("exthost", "extensionhostdebugservice:close");
2047					use tauri::Emitter;
2048					if let Err(Error) = ApplicationHandle.emit("sky://exthost/debug-close", json!({})) {
2049						dev_log!("exthost", "warn: extensionhostdebugservice:close emit failed: {}", Error);
2050					}
2051					Ok(Value::Null)
2052				},
2053				"extensionhostdebugservice:attachSession" | "extensionhostdebugservice:terminateSession" => {
2054					dev_log!("exthost", "{}", command);
2055					Ok(Value::Null)
2056				},
2057
2058				// =====================================================================
2059				// Workspaces - additional commands
2060				// =====================================================================
2061				"workspaces:getRecentlyOpened" => {
2062					dev_log!("workspaces", "workspaces:getRecentlyOpened");
2063					ReadRecentlyOpened()
2064				},
2065				"workspaces:removeRecentlyOpened" => {
2066					dev_log!("workspaces", "workspaces:removeRecentlyOpened");
2067					let Uri = Arguments.first().and_then(|V| V.as_str()).unwrap_or("").to_string();
2068					if !Uri.is_empty() {
2069						MutateRecentlyOpened(|List| {
2070							if let Some(Workspaces) = List.get_mut("workspaces").and_then(|V| V.as_array_mut()) {
2071								Workspaces
2072									.retain(|Entry| Entry.get("uri").and_then(|V| V.as_str()).unwrap_or("") != Uri);
2073							}
2074							if let Some(Files) = List.get_mut("files").and_then(|V| V.as_array_mut()) {
2075								Files.retain(|Entry| Entry.get("uri").and_then(|V| V.as_str()).unwrap_or("") != Uri);
2076							}
2077						});
2078					}
2079					Ok(Value::Null)
2080				},
2081				"workspaces:addRecentlyOpened" => {
2082					dev_log!("workspaces", "workspaces:addRecentlyOpened");
2083					// VS Code passes `[{ workspace?, folderUri?, fileUri?, label? }, …]`.
2084					let Entries:Vec<Value> = Arguments.first().and_then(|V| V.as_array()).cloned().unwrap_or_default();
2085					if !Entries.is_empty() {
2086						MutateRecentlyOpened(|List| {
2087							let Workspaces = List
2088								.get_mut("workspaces")
2089								.and_then(|V| V.as_array_mut())
2090								.map(|V| std::mem::take(V))
2091								.unwrap_or_default();
2092							let Files = List
2093								.get_mut("files")
2094								.and_then(|V| V.as_array_mut())
2095								.map(|V| std::mem::take(V))
2096								.unwrap_or_default();
2097							let mut MergedWorkspaces = Workspaces;
2098							let mut MergedFiles = Files;
2099							for Entry in Entries {
2100								let Folder = Entry
2101									.get("folderUri")
2102									.cloned()
2103									.or_else(|| Entry.get("workspace").and_then(|W| W.get("configPath").cloned()));
2104								let File = Entry.get("fileUri").cloned();
2105								if let Some(FolderUri) = Folder.and_then(|V| v_str(&V)) {
2106									MergedWorkspaces
2107										.retain(|E| E.get("uri").and_then(|V| V.as_str()).unwrap_or("") != FolderUri);
2108									let mut Item = serde_json::Map::new();
2109									Item.insert("uri".into(), json!(FolderUri));
2110									if let Some(Label) = Entry.get("label").and_then(|V| V.as_str()) {
2111										Item.insert("label".into(), json!(Label));
2112									}
2113									MergedWorkspaces.insert(0, Value::Object(Item));
2114								}
2115								if let Some(FileUri) = File.and_then(|V| v_str(&V)) {
2116									MergedFiles
2117										.retain(|E| E.get("uri").and_then(|V| V.as_str()).unwrap_or("") != FileUri);
2118									let mut Item = serde_json::Map::new();
2119									Item.insert("uri".into(), json!(FileUri));
2120									MergedFiles.insert(0, Value::Object(Item));
2121								}
2122							}
2123							// Cap at 50 each - matches VS Code's default in
2124							// `src/vs/platform/workspaces/common/workspaces.ts`.
2125							MergedWorkspaces.truncate(50);
2126							MergedFiles.truncate(50);
2127							List.insert("workspaces".into(), Value::Array(MergedWorkspaces));
2128							List.insert("files".into(), Value::Array(MergedFiles));
2129						});
2130					}
2131					Ok(Value::Null)
2132				},
2133				"workspaces:clearRecentlyOpened" => {
2134					dev_log!("workspaces", "workspaces:clearRecentlyOpened");
2135					MutateRecentlyOpened(|List| {
2136						List.insert("workspaces".into(), json!([]));
2137						List.insert("files".into(), json!([]));
2138					});
2139					Ok(Value::Null)
2140				},
2141				"workspaces:enterWorkspace" => {
2142					dev_log!("workspaces", "workspaces:enterWorkspace");
2143					Ok(Value::Null)
2144				},
2145				"workspaces:createUntitledWorkspace" => {
2146					dev_log!("workspaces", "workspaces:createUntitledWorkspace");
2147					Ok(Value::Null)
2148				},
2149				"workspaces:deleteUntitledWorkspace" => {
2150					dev_log!("workspaces", "workspaces:deleteUntitledWorkspace");
2151					Ok(Value::Null)
2152				},
2153				"workspaces:getWorkspaceIdentifier" => {
2154					// Return a stable identifier derived from the first workspace
2155					// folder's URI so VS Code's caching (recently-opened, per-workspace
2156					// storage, window-title derivation) keys off the real workspace
2157					// rather than the "untitled" fallback. `{ id, configPath }` is
2158					// VS Code's expected shape for a multi-root workspace identifier;
2159					// we only use single-root so configPath stays null.
2160					let Workspace = &RunTime.Environment.ApplicationState.Workspace;
2161					let Folders = Workspace.GetWorkspaceFolders();
2162					if let Some(First) = Folders.first() {
2163						use std::{
2164							collections::hash_map::DefaultHasher,
2165							hash::{Hash, Hasher},
2166						};
2167						let mut Hasher = DefaultHasher::new();
2168						First.URI.as_str().hash(&mut Hasher);
2169						let Id = format!("{:016x}", Hasher.finish());
2170						Ok(json!({
2171							"id": Id,
2172							"configPath": Value::Null,
2173							"uri": First.URI.to_string(),
2174						}))
2175					} else {
2176						Ok(Value::Null)
2177					}
2178				},
2179				"workspaces:getDirtyWorkspaces" => Ok(json!([])),
2180
2181				// Git (localGit channel) - implements stock VS Code's
2182				// ILocalGitService surface plus `exec` / `isAvailable` for
2183				// the built-in Git extension. Handlers spawn native `git`
2184				// via tokio::process. See Batch 4 in HANDOFF §-10.
2185				"git:exec" => {
2186					dev_log!("git", "git:exec");
2187					Git::HandleExec::HandleExec(Arguments).await
2188				},
2189				"git:clone" => {
2190					dev_log!("git", "git:clone");
2191					Git::HandleClone::HandleClone(Arguments).await
2192				},
2193				"git:pull" => {
2194					dev_log!("git", "git:pull");
2195					Git::HandlePull::HandlePull(Arguments).await
2196				},
2197				"git:checkout" => {
2198					dev_log!("git", "git:checkout");
2199					Git::HandleCheckout::HandleCheckout(Arguments).await
2200				},
2201				"git:revParse" => {
2202					dev_log!("git", "git:revParse");
2203					Git::HandleRevParse::HandleRevParse(Arguments).await
2204				},
2205				"git:fetch" => {
2206					dev_log!("git", "git:fetch");
2207					Git::HandleFetch::HandleFetch(Arguments).await
2208				},
2209				"git:revListCount" => {
2210					dev_log!("git", "git:revListCount");
2211					Git::HandleRevListCount::HandleRevListCount(Arguments).await
2212				},
2213				"git:cancel" => {
2214					dev_log!("git", "git:cancel");
2215					Git::HandleCancel::HandleCancel(Arguments).await
2216				},
2217				"git:isAvailable" => {
2218					dev_log!("git", "git:isAvailable");
2219					Git::HandleIsAvailable::HandleIsAvailable(Arguments).await
2220				},
2221
2222				// Tree-view child lookup from the renderer side. Mirrors the
2223				// Cocoon→Mountain `GetTreeChildren` gRPC path (see
2224				// `RPC/CocoonService/TreeView.rs::GetTreeChildren`) but is
2225				// invoked by the Wind/Sky tree-view bridge so the UI can
2226				// request children directly without waiting for Cocoon to
2227				// ask first. Payload: `[{ viewId, treeItemHandle? }]`.
2228				"tree:getChildren" => {
2229					let ViewId = Arguments
2230						.first()
2231						.and_then(|V| V.get("viewId").or_else(|| V.get(0)))
2232						.and_then(Value::as_str)
2233						.unwrap_or("")
2234						.to_string();
2235					let ItemHandle = Arguments
2236						.first()
2237						.and_then(|V| V.get("treeItemHandle").or_else(|| V.get(1)))
2238						.and_then(Value::as_str)
2239						.unwrap_or("")
2240						.to_string();
2241					dev_log!(
2242						"tree-view",
2243						"[TreeView] invoke:getChildren view={} parent={}",
2244						ViewId,
2245						ItemHandle
2246					);
2247					if ViewId.is_empty() {
2248						Err("tree:getChildren requires viewId".to_string())
2249					} else {
2250						let Parameters = json!({
2251							"viewId": ViewId,
2252							"treeItemHandle": ItemHandle,
2253						});
2254						// Boot-race: the workbench's Explorer view fires
2255						// `tree:getChildren` ~700 log lines before
2256						// Cocoon's gRPC client finishes handshaking.
2257						// Without this wait the first call returns
2258						// `ClientNotConnected`, the workbench caches an
2259						// empty list, and the user sees an empty
2260						// Explorer until they manually refresh. Wait up
2261						// to 1500 ms for the connection to land before
2262						// dispatching - this no-ops once Cocoon is
2263						// connected (the typical case), so it only
2264						// costs us latency on the very first call.
2265						// Bumped 1500 -> 5000 ms - bundled-electron boot trace
2266						// shows Cocoon's `Successfully connected` lands ~620
2267						// log lines AFTER the workbench's first request, so
2268						// the 1.5 s wait routinely expired before Cocoon was
2269						// up. Sky-side caches captured the empty fallback
2270						// (Explorer empty, webview.resolveView=ClientNotConnected,
2271						// etc.) and panes never recovered.
2272						let _ = crate::Vine::Client::WaitForClientConnection::Fn("cocoon-main", 5000).await;
2273						// Tree-view RPCs are user-interactive: a 5 second
2274						// wait shows the user a spinner and silently fails
2275						// the extension's Promise on timeout. 1500 ms is
2276						// a reasonable upper bound for "extension is
2277						// healthy and producing children" on this hardware
2278						// class - real workloads (gitlens fileHistory,
2279						// rust-analyzer typeHierarchy) finish in <500 ms.
2280						// Slow producers fall through to the empty-array
2281						// path and the workbench schedules its own retry
2282						// when the view scrolls back into view.
2283						match crate::Vine::Client::SendRequest::Fn(
2284							"cocoon-main",
2285							"$provideTreeChildren".to_string(),
2286							Parameters,
2287							// Bumped 1500 -> 5000 ms - real cold-boot tree
2288							// calls take 700-2200 ms ([DEV:TREE-LATENCY]
2289							// clangd.ast=2181, gitlens.workspaces=1652,
2290							// npm=1560). The previous cap dropped ~30% of
2291							// tree results and the workbench cached empty.
2292							5000,
2293						)
2294						.await
2295						{
2296							// Defensive shape check: the workbench's
2297							// tree-view consumer expects either an
2298							// `items` array or a top-level array.
2299							// `null` / `undefined` from a misbehaving
2300							// extension would be passed through and
2301							// trigger `TypeError: Cannot read property
2302							// 'length' of null` in the renderer. Force
2303							// to `{items: []}` for any non-conforming
2304							// shape so the renderer always has
2305							// iterable data.
2306							Ok(Value_) => {
2307								match &Value_ {
2308									Value::Object(_) | Value::Array(_) => Ok(Value_),
2309									_ => Ok(json!({ "items": [] })),
2310								}
2311							},
2312							Err(Error) => {
2313								// Common case: an extension's tree
2314								// data-provider rejects (npm extension
2315								// crashes on a malformed
2316								// `package.json`, gitlens hits an
2317								// expired pull-request fetch, etc.).
2318								// First failure per view is logged so
2319								// developers can see the cause; later
2320								// failures of the same view-id are
2321								// silenced via the file-sink-only
2322								// path so the dev log doesn't fill
2323								// with hundreds of identical lines
2324								// while the user is browsing tree
2325								// nodes that all hit the same
2326								// extension bug.
2327								crate::IPC::DevLog::DebugOnce::Fn(
2328									"tree-view",
2329									&format!("get-children-error:{}", ViewId),
2330									&format!(
2331										"[TreeView] invoke:getChildren error view={} err={:?} (further occurrences \
2332										 silenced)",
2333										ViewId, Error
2334									),
2335								);
2336								Ok(json!({ "items": [] }))
2337							},
2338						}
2339					}
2340				},
2341
2342				// SkyBridge calls this after installing every `sky://*` Tauri
2343				// listener. Mountain → Sky `app.emit()` events are NOT
2344				// buffered: any emit fired before the listener was installed
2345				// is silently dropped. In the bundled-electron profile,
2346				// extension activation (which triggers
2347				// `register_scm_provider` and `$tree:register` notifications
2348				// through Cocoon) starts ~580 log lines before the Sky
2349				// bundle finishes booting (~1995 lines). Without this
2350				// replay, all tree-view + SCM register events are lost and
2351				// the Activity Bar / sidebar comes up empty even though
2352				// state-side everything registered correctly.
2353				"sky:replay-events" => {
2354					use tauri::Emitter;
2355					let mut TreeViewCount:usize = 0;
2356					let mut ScmCount:usize = 0;
2357					let mut CommandCount:usize = 0;
2358					let mut TerminalCount:usize = 0;
2359					let mut TerminalDataBytes:usize = 0;
2360					if let Ok(TreeViews) = RunTime.Environment.ApplicationState.Feature.TreeViews.ActiveTreeViews.lock()
2361					{
2362						for (ViewId, Dto) in TreeViews.iter() {
2363							let Payload = serde_json::json!({
2364								"viewId": ViewId,
2365								"options": {
2366									"canSelectMany": Dto.CanSelectMany,
2367									"showCollapseAll": Dto.HasHandleDrag,
2368									"title": Dto.Title.clone().unwrap_or_default(),
2369								},
2370							});
2371							if ApplicationHandle.emit("sky://tree-view/create", Payload).is_ok() {
2372								TreeViewCount += 1;
2373							}
2374						}
2375					}
2376					// SCM replay uses the stored `Identifier` field on the
2377					// provider DTO ("git", "github", "hg", …) so any SCM
2378					// provider Cocoon registers replays with its original
2379					// id - not just the built-in `vscode.git` extension.
2380					// Pre-DTO-Identifier-field DTOs default `Identifier` to
2381					// "" (serde default); fall back to "git" in that case
2382					// because the only SCM provider in production today is
2383					// `vscode.git` and a stale state file with empty id is
2384					// the realistic upgrade-path mismatch.
2385					if let Ok(ScmProviders) = RunTime
2386						.Environment
2387						.ApplicationState
2388						.Feature
2389						.Markers
2390						.SourceControlManagementProviders
2391						.lock()
2392					{
2393						for (Handle, Dto) in ScmProviders.iter() {
2394							let RootUriStr = Dto
2395								.RootURI
2396								.as_ref()
2397								.and_then(|V| V.get("external").or_else(|| V.get("path")))
2398								.and_then(serde_json::Value::as_str)
2399								.unwrap_or("")
2400								.to_string();
2401							let ScmId = if Dto.Identifier.is_empty() {
2402								"git".to_string()
2403							} else {
2404								Dto.Identifier.clone()
2405							};
2406							let Payload = serde_json::json!({
2407								"scmId": ScmId,
2408								"label": Dto.Label,
2409								"rootUri": RootUriStr,
2410								"extensionId": "",
2411								"handle": *Handle,
2412							});
2413							if ApplicationHandle.emit("sky://scm/register", Payload).is_ok() {
2414								ScmCount += 1;
2415							}
2416						}
2417					}
2418					// Replay extension-registered commands so the workbench's
2419					// `ICommandService` registry knows about them post-bridge-
2420					// install. Every `vscode.commands.registerCommand(...)`
2421					// in an extension fires `sky://command/register` from
2422					// `Vine/Server/Notification/RegisterCommand.rs`. Native
2423					// commands (Mountain's own Rust handlers) don't need
2424					// replay - they're not exposed via this channel.
2425					//
2426					// Emit ONE batched event with the whole array. Per-
2427					// command emits (one per registered command, ~1000+
2428					// during extension boot) saturated Tauri's shared
2429					// WKWebView IPC channel and starved keystroke
2430					// delivery. SkyBridge's `sky://command/register`
2431					// listener accepts either `{ id, commandId, kind }`
2432					// or `{ commands: [...] }` (see SkyBridge.ts).
2433					if let Ok(Commands) = RunTime.Environment.ApplicationState.Extension.Registry.CommandRegistry.lock()
2434					{
2435						let mut Batch:Vec<serde_json::Value> = Vec::new();
2436						for (CommandId, Handler) in Commands.iter() {
2437							use crate::Environment::CommandProvider::CommandHandler;
2438							let Kind = match Handler {
2439								CommandHandler::Native(_) => continue,
2440								CommandHandler::Proxied { .. } => "extension",
2441							};
2442							Batch.push(serde_json::json!({
2443								"id": CommandId,
2444								"commandId": CommandId,
2445								"kind": Kind,
2446							}));
2447						}
2448						if !Batch.is_empty() {
2449							let Count = Batch.len();
2450							if ApplicationHandle
2451								.emit("sky://command/register", serde_json::json!({ "commands": Batch }))
2452								.is_ok()
2453							{
2454								CommandCount = Count;
2455							}
2456						}
2457					}
2458					// Replay terminals: each active terminal needs its `create`
2459					// event AND any buffered stdout the PTY reader produced
2460					// before SkyBridge's `listen("sky://terminal/*")` was
2461					// installed. Without this, the shell's first prompt
2462					// (zsh's MOTD, fish greeting, `direnv export`, …) is
2463					// silently dropped and the user sees an empty pane until
2464					// they type.
2465					if let Ok(Terminals) = RunTime.Environment.ApplicationState.Feature.Terminals.ActiveTerminals.lock()
2466					{
2467						for (TerminalId, Arc) in Terminals.iter() {
2468							let (Name, Pid) = if let Ok(State) = Arc.lock() {
2469								(State.Name.clone(), State.OSProcessIdentifier.unwrap_or(0))
2470							} else {
2471								(String::new(), 0)
2472							};
2473							let CreatePayload = serde_json::json!({
2474								"id": *TerminalId,
2475								"name": Name,
2476								"pid": Pid,
2477							});
2478							if ApplicationHandle.emit("sky://terminal/create", CreatePayload).is_ok() {
2479								TerminalCount += 1;
2480							}
2481						}
2482					}
2483					for (TerminalId, Bytes) in crate::Environment::TerminalProvider::DrainTerminalOutputBuffer() {
2484						let DataString = String::from_utf8_lossy(&Bytes).to_string();
2485						TerminalDataBytes += Bytes.len();
2486						let _ = ApplicationHandle.emit(
2487							"sky://terminal/data",
2488							serde_json::json!({ "id": TerminalId, "data": DataString }),
2489						);
2490					}
2491					dev_log!(
2492						"sky-emit",
2493						"[SkyEmit] replay-events tree-views={} scm={} commands={} terminals={} terminal-bytes={}",
2494						TreeViewCount,
2495						ScmCount,
2496						CommandCount,
2497						TerminalCount,
2498						TerminalDataBytes
2499					);
2500					Ok(serde_json::json!({
2501						"treeViews": TreeViewCount,
2502						"scmProviders": ScmCount,
2503						"commands": CommandCount,
2504						"terminals": TerminalCount,
2505						"terminalDataBytes": TerminalDataBytes,
2506					}))
2507				},
2508
2509				// Atom L2: unknown-command fallback consults the Channel registry so
2510				// the log distinguishes three states:
2511				//   1. typo / never-registered wire string (registry::from_str Err)
2512				//   2. registered but dispatch missing (registry OK but arm absent)
2513				//   3. legitimately unknown
2514				// Case (2) is the shape of the VSIX stub bug before K2 landed - an
2515				// entry present in the registry with no handler. Making it visible
2516				// turns silent drift into a loud dev-log line.
2517				_ => {
2518					use std::str::FromStr;
2519					match CommonLibrary::IPC::Channel::Channel::from_str(&command) {
2520						Ok(KnownChannel) => {
2521							dev_log!(
2522								"ipc",
2523								"error: [WindServiceHandlers] Channel {:?} is registered but has no dispatch arm",
2524								KnownChannel
2525							);
2526							Err(format!("IPC channel registered but unimplemented: {}", command))
2527						},
2528						Err(_) => {
2529							dev_log!("ipc", "error: [WindServiceHandlers] Unknown IPC command: {}", command);
2530							Err(format!("Unknown IPC command: {}", command))
2531						},
2532					}
2533				},
2534			};
2535
2536			if ResultSender.send(MatchResult).is_err() {
2537				dev_log!(
2538					"ipc",
2539					"warn: [WindServiceHandlers] IPC result receiver dropped before dispatch completed"
2540				);
2541			}
2542		},
2543		CommandPriority,
2544	);
2545
2546	let Result = match ResultReceiver.await {
2547		Ok(Dispatched) => Dispatched,
2548		Err(_) => {
2549			dev_log!(
2550				"ipc",
2551				"error: [WindServiceHandlers] IPC task cancelled before producing a result"
2552			);
2553			Err("IPC task cancelled before result was produced".to_string())
2554		},
2555	};
2556
2557	// Emit OTLP span for every IPC call - visible in Jaeger at localhost:16686
2558	// Skip for high-frequency silenced calls to avoid thousands of spans
2559	// per session (logger, file I/O, storage polling).
2560	if !IsHighFrequencyCommand {
2561		let IsErr = Result.is_err();
2562		let SpanName = if IsErr {
2563			format!("land:mountain:ipc:{}:error", command)
2564		} else {
2565			format!("land:mountain:ipc:{}", command)
2566		};
2567		crate::otel_span!(&SpanName, OTLPStart, &[("ipc.command", command.as_str())]);
2568
2569		// Emit `land:mountain:handler:complete` to PostHog for every dispatched IPC.
2570		// Pairs with `land:cocoon:handler:complete` to populate the Feature
2571		// Parity dashboard's Node-vs-Rust handler-latency comparison.
2572		let HandlerElapsedNanos = crate::IPC::DevLog::NowNano::Fn().saturating_sub(OTLPStart);
2573		let HandlerDurationMs = HandlerElapsedNanos / 1_000_000;
2574		crate::Binary::Build::PostHogPlugin::CaptureHandler::Fn(&command, HandlerDurationMs, !IsErr);
2575	}
2576
2577	// Atom I13: paired entry/exit line per invoke. `invoke: <cmd>` on the way
2578	// in (emitted at the top of this fn); `done: <cmd> ok=… t_ns=…` on the
2579	// way out. A `grep "logger:log"` before showed only the entry half;
2580	// having both halves makes latency diagnosis a single pipe:
2581	//     grep "logger:log" Mountain.dev.log | awk '…'
2582	// without hopping across Jaeger. High-frequency commands still skip the
2583	// entry line but DO emit an exit - frequencies still aggregate, but each
2584	// is individually accounted for.
2585	if !IsHighFrequencyCommand {
2586		let ElapsedNanos = crate::IPC::DevLog::NowNano::Fn().saturating_sub(OTLPStart);
2587		dev_log!("ipc", "done: {} ok={} t_ns={}", command, !Result.is_err(), ElapsedNanos);
2588	}
2589
2590	Result
2591}
2592
2593pub fn register_wind_ipc_handlers(ApplicationHandle:&tauri::AppHandle) -> Result<(), String> {
2594	dev_log!("lifecycle", "registering IPC handlers");
2595
2596	// Note: These handlers are automatically registered when included in the
2597	// Tauri invoke_handler macro in the main binary
2598
2599	Ok(())
2600}