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