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