Skip to main content

Mountain/IPC/
WindServiceHandlers.rs

1#![allow(unused_variables, dead_code)]
2
3//! # Wind Service Handlers - Cross-Language Service Bridge
4//!
5//! **File Responsibilities:**
6//! This module provides the direct mapping layer between Wind's TypeScript
7//! service invocations and Mountain's Rust service implementations. It acts as
8//! the critical translation layer that enables Wind to request operations from
9//! Mountain through Tauri's IPC mechanism.
10//!
11//! **Architectural Role in Wind-Mountain Connection:**
12//!
13//! The WindServiceHandlers module implements the concrete command handlers that
14//! process IPC invocations from Wind. It serves as the single entry point for
15//! all Wind->Mountain service requests:
16//!
17//! 1. **Command Mapping:** Maps Wind's TypeScript service methods to Rust
18//!    implementations
19//! 2. **Type Conversion:** Converts between JSON/TypeScript types and Rust
20//!    types
21//! 3. **Validation:** Validates all inputs before forwarding to Mountain
22//!    services
23//! 4. **Error Handling:** Provides comprehensive error messages back to Wind
24//! 5. **Service Integration:** Connects to Mountain's internal service
25//!    architecture
26//!
27//! **Handled Command Categories:**
28//!
29//! **1. Configuration Commands:**
30//! - `configuration:get` - Retrieve configuration values
31//! - `configuration:update` - Update configuration values
32//!
33//! **2. File System Commands:**
34//! - `file:read` - Read file contents
35//! - `file:write` - Write to files
36//! - `file:stat` - Get file metadata
37//! - `file:exists` - Check file existence
38//! - `file:delete` - Delete files or directories
39//! - `file:copy` - Copy files
40//! - `file:move` - Move/rename files
41//! - `file:mkdir` - Create directories
42//! - `file:readdir` - Read directory contents
43//! - `file:readBinary` - Read binary files
44//! - `file:writeBinary` - Write binary files
45//!
46//! **3. Storage Commands:**
47//! - `storage:get` - Retrieve persistent storage values
48//! - `storage:set` - Store persistent values
49//!
50//! **4. Environment Commands:**
51//! - `environment:get` - Get environment variables
52//!
53//! **5. Native Host Commands:**
54//! - `native:showItemInFolder` - Reveal file in system file manager
55//! - `native:openExternal` - Open URLs in external browser
56//!
57//! **6. Workbench Commands:**
58//! - `workbench:getConfiguration` - Get complete workbench configuration
59//!
60//! **7. IPC Status Commands:**
61//! - `mountain_get_status` - Get overall IPC system status
62//! - `mountain_get_configuration` - Get Mountain configuration snapshot
63//! - `mountain_get_services_status` - Get status of all Mountain services
64//! - `mountain_get_state` - Get current application state
65//!
66//! **Communication Pattern:**
67//!
68//! ```text
69//! Wind (TypeScript)
70//!   |
71//!   | app.handle.invoke('command', args)
72//!   v
73//! Tauri Bridge (IPC)
74//!   |
75//!   | mountain_ipc_invoke(command, args)
76//!   v
77//! WindServiceHandlers
78//!   |
79//!   | Type conversion + validation
80//!   v
81//! Mountain Services (Rust)
82//!   |
83//!   | Execute operation
84//!   v
85//! Return Result<serde_json::Value>
86//! ```
87//!
88//! **Type Conversion Strategy (TypeScript <-> Rust):**
89//!
90//! **Primitive Types:**
91//! - TypeScript `string` ↔ Rust `String` / `&str`
92//! - TypeScript `number` ↔ Rust `f64` / `i32` / `u32`
93//! - TypeScript `boolean` ↔ Rust `bool`
94//! - TypeScript `null` ↔ Rust `Option::<T>::None`
95//!
96//! **Complex Types:**
97//! - TypeScript `object` ↔ Rust `serde_json::Value` / `HashMap`
98//! - TypeScript `Array<T>` ↔ Rust `Vec<T>`
99//! - TypeScript custom interfaces ↔ Rust structs with Serialize/Deserialize
100//!
101//! **Example Type Conversion:**
102//! ```typescript
103//! // Wind (TypeScript)
104//! interface FileReadOptions {
105//!   encoding: 'utf8' | 'binary';
106//!   withBOM: boolean;
107//! }
108//! const result = await invoke('file:read', {
109//!   path: '/path/to/file.txt',
110//!   options: { encoding: 'utf8', withBOM: false }
111//! });
112//! ```
113//!
114//! ```text
115//! // Mountain (Rust)
116//! args.get(0).and_then(|v| v.as_str()) // Extract path
117//! args.get(1).and_then(|v| v.as_object()) // Extract options
118//! // ... validation, processing, return Result
119//! ```
120//!
121//! **Defensive Error Handling:**
122//!
123//! Each handler implements comprehensive error handling:
124//!
125//! 1. **Input Validation:**
126//!    - Check parameter presence
127//!    - Validate parameter types
128//!    - Validate value ranges and formats
129//!
130//! 2. **Service Error Handling:**
131//!    - Catch and translate service errors
132//!    - Provide detailed error messages
133//!    - Include context for debugging
134//!
135//! 3. **Error Response Format:**
136//! ```rust
137//! Error("Failed to read file: Permission denied (path: /etc/passwd)") 
138//! ```
139//!
140//! **Comprehensive Error Messages:**
141//! - Include operation that failed
142//! - Include relevant parameters (paths, keys, etc.)
143//! - Include the underlying cause
144//! - Format: `"Failed to <operation>: <cause> (context: <value>)"`
145//!
146//! **Service Integration Pattern:**
147//!
148//! Handlers use Mountain's dependency injection system via `Requires` trait:
149//!
150//! ```text
151//! let provider: Arc<dyn ConfigurationProvider> = runtime.Environment.Require();
152//! provider.GetConfigurationValue(...).await?;
153//! ```
154//!
155//! This provides:
156//! - Loose coupling between handlers and services
157//! - Testable architecture (can mock services)
158//! - Centralized service lifecycle management
159//!
160//! **Command Registration:**
161//!
162//! All handlers are automatically registered when included in Tauri's
163//! invoke_handler:
164//!
165//! ```rust
166//! .invoke_handler(tauri::generate_handler![
167//!     mountain_ipc_invoke,
168//!     // ... other commands
169//! ])
170//! ```
171
172use std::{collections::HashMap, path::PathBuf, sync::Arc};
173
174use serde_json::{Value, json};
175use tauri::{AppHandle, Manager};
176// Type aliases for Configuration DTOs to simplify usage
177use CommonLibrary::Configuration::DTO::{
178	ConfigurationOverridesDTO as ConfigurationOverridesDTOModule,
179	ConfigurationTarget as ConfigurationTargetModule,
180};
181
182use crate::dev_log;
183type ConfigurationOverridesDTO = ConfigurationOverridesDTOModule::ConfigurationOverridesDTO;
184type ConfigurationTarget = ConfigurationTargetModule::ConfigurationTarget;
185
186use CommonLibrary::{
187	Command::CommandExecutor::CommandExecutor,
188	Configuration::ConfigurationProvider::ConfigurationProvider,
189	Environment::Requires::Requires,
190	Error::CommonError::CommonError,
191	ExtensionManagement::ExtensionManagementService::ExtensionManagementService,
192	FileSystem::{FileSystemReader::FileSystemReader, FileSystemWriter::FileSystemWriter},
193	Storage::StorageProvider::StorageProvider,
194};
195
196use crate::{
197	ApplicationState::DTO::WorkspaceFolderStateDTO::WorkspaceFolderStateDTO,
198	RunTime::ApplicationRunTime::ApplicationRunTime,
199};
200
201/// Extract a filesystem path from a VS Code argument.
202/// VS Code sends URI objects `{ scheme: "file", path: "/C:/foo", fsPath:
203/// "C:\\foo" }` but Mountain handlers expect platform-native path strings.
204///
205/// Windows URI paths have a leading slash: `/C:/Users/...` → strip it.
206/// Unix paths start with `/` normally.
207pub fn extract_path_from_arg(Arg:&Value) -> Result<String, String> {
208	// Direct string path
209	if let Some(Path) = Arg.as_str() {
210		return Ok(normalize_uri_path(Path));
211	}
212	// URI object { scheme, path, fsPath, ... }
213	if let Some(Object) = Arg.as_object() {
214		// Prefer fsPath (already OS-normalized by VS Code)
215		if let Some(FsPath) = Object.get("fsPath").and_then(|V| V.as_str()) {
216			if !FsPath.is_empty() {
217				return Ok(FsPath.to_string());
218			}
219		}
220		// Fall back to path field (URI-encoded, may have leading / on Windows)
221		if let Some(Path) = Object.get("path").and_then(|V| V.as_str()) {
222			if !Path.is_empty() {
223				return Ok(normalize_uri_path(Path));
224			}
225		}
226		// Handle external (full URI string like file:///C:/foo or file:///home/user)
227		if let Some(External) = Object.get("external").and_then(|V| V.as_str()) {
228			if External.starts_with("file://") {
229				let Stripped = External.trim_start_matches("file://");
230				return Ok(normalize_uri_path(Stripped));
231			}
232		}
233	}
234	Err("File path must be a string or URI object with path/fsPath field".to_string())
235}
236
237/// Normalize a URI-style path to a platform-native path.
238/// On Windows, URI paths look like `/C:/Users/...` - strip the leading slash.
239/// On Unix, paths already start with `/`.
240/// Also handles percent-encoded characters (%20 for space, etc.)
241/// Also maps vscode-userdata paths `/User/...` to the real userdata directory.
242fn normalize_uri_path(Path:&str) -> String {
243	let Decoded = percent_decode(Path);
244
245	// Map vscode-userdata paths: /User/... → ~/.land/User/...
246	// Wind's configuration sets profiles.home = { scheme: "vscode-userdata", path:
247	// "/User" } VS Code's FileUserDataProvider converts vscode-userdata: → file:
248	// but keeps the path. We need to prepend the real userdata directory.
249	let Resolved = resolve_userdata_path(&Decoded);
250
251	// Map /Static/Application/... → real Sky Target directory.
252	// VS Code computes builtinExtensionsPath as appRoot + "/../extensions"
253	// which resolves to /Static/Application/extensions. Since appRoot is a
254	// URL path served by Vite/Tauri, we map it to the real filesystem.
255	let Resolved = resolve_static_application_path(&Resolved);
256
257	#[cfg(target_os = "windows")]
258	{
259		// Windows: URI path "/C:/Users/foo" → "C:\\Users\\foo"
260		let Trimmed = if Resolved.len() >= 3 && Resolved.starts_with('/') && Resolved.as_bytes().get(2) == Some(&b':') {
261			// /C:/path → C:/path
262			Resolved[1..].to_string()
263		} else {
264			Resolved
265		};
266		// Normalize forward slashes to backslashes for Windows
267		Trimmed.replace('/', "\\")
268	}
269
270	#[cfg(not(target_os = "windows"))]
271	{
272		Resolved
273	}
274}
275
276/// Map vscode-userdata paths to real filesystem paths.
277/// /User/settings.json → ~/.land/User/settings.json
278/// /User/profiles/__default__profile__/... → ~/.land/User/profiles/...
279/// Paths not starting with /User/ are returned unchanged.
280fn resolve_userdata_path(Path:&str) -> String {
281	if !Path.starts_with("/User/") && Path != "/User" {
282		return Path.to_string();
283	}
284
285	let UserDataBase = get_userdata_base_dir();
286	let Resolved = format!("{}{}", UserDataBase, Path);
287	dev_log!("vfs", "resolve_userdata: {} -> {}", Path, Resolved);
288	Resolved
289}
290
291/// Map paths starting with /Static/Application/ to the real Sky Target
292/// directory. VS Code's environmentService computes builtinExtensionsPath as
293/// `join(FileAccess.asFileUri("").fsPath, "..", "extensions")` which resolves
294/// to `/Static/Application/extensions`. Since /Static/Application/ is a URL
295/// path (not a real filesystem path), we map it to the actual Sky Target
296/// directory where the VS Code assets are served from.
297///
298/// The Sky Target directory is determined relative to the executable's location
299/// in dev mode, or from Tauri's resource directory in production.
300static STATIC_APPLICATION_ROOT:std::sync::OnceLock<String> = std::sync::OnceLock::new();
301
302/// Set the real filesystem root for /Static/Application/ paths.
303/// Call once at startup with the Sky Target directory.
304pub fn set_static_application_root(Path:String) { let _ = STATIC_APPLICATION_ROOT.set(Path); }
305
306/// Get the filesystem root for /Static/Application/ paths.
307/// Returns None if not yet initialized.
308pub fn get_static_application_root() -> Option<String> { STATIC_APPLICATION_ROOT.get().cloned() }
309
310fn resolve_static_application_path(Path:&str) -> String {
311	if !Path.starts_with("/Static/Application/") && Path != "/Static/Application" {
312		return Path.to_string();
313	}
314
315	if let Some(Root) = STATIC_APPLICATION_ROOT.get() {
316		let Relative = Path.strip_prefix("/Static/Application").unwrap_or("");
317		let Resolved = format!("{}/Static/Application{}", Root, Relative);
318		dev_log!("vfs", "resolve_static: {} -> {}", Path, Resolved);
319		Resolved
320	} else {
321		// Fallback: return path unchanged (will fail with ENOENT)
322		Path.to_string()
323	}
324}
325
326/// Canonical userdata base directory, set once from Tauri's PathResolver.
327/// All /User/... paths resolve against this so the VFS mapping matches
328/// the real Tauri app_data_dir (which includes the full bundle identifier).
329static USERDATA_BASE_DIR:std::sync::OnceLock<String> = std::sync::OnceLock::new();
330
331/// Session timestamp, generated once per process lifetime so every call to
332/// `getEnvironmentPaths` returns the same `logsPath`.
333static SESSION_TIMESTAMP:std::sync::OnceLock<String> = std::sync::OnceLock::new();
334
335/// Set the userdata base from Tauri's app_data_dir. Call once at startup.
336pub fn set_userdata_base_dir(Path:String) { let _ = USERDATA_BASE_DIR.set(Path); }
337
338/// Get the base directory for userdata storage.
339/// Returns the Tauri-resolved path if available, otherwise a fallback.
340fn get_userdata_base_dir() -> String {
341	if let Some(Dir) = USERDATA_BASE_DIR.get() {
342		return Dir.clone();
343	}
344	// Fallback before Tauri sets it (should not happen in normal flow)
345	if let Ok(Home) = std::env::var("HOME") {
346		#[cfg(target_os = "macos")]
347		return format!("{}/Library/Application Support/Land", Home);
348		#[cfg(target_os = "linux")]
349		return format!("{}/.local/share/Land", Home);
350	}
351	"/tmp/Land".to_string()
352}
353
354/// Decode percent-encoded characters in URI paths.
355/// Handles: %20 (space), %23 (#), %25 (%), %5B ([), %5D (]), etc.
356fn percent_decode(Input:&str) -> String {
357	let mut Result = String::with_capacity(Input.len());
358	let Bytes = Input.as_bytes();
359	let mut I = 0;
360
361	while I < Bytes.len() {
362		if Bytes[I] == b'%' && I + 2 < Bytes.len() {
363			let High = hex_digit(Bytes[I + 1]);
364			let Low = hex_digit(Bytes[I + 2]);
365			if let (Some(H), Some(L)) = (High, Low) {
366				Result.push((H * 16 + L) as char);
367				I += 3;
368				continue;
369			}
370		}
371		Result.push(Bytes[I] as char);
372		I += 1;
373	}
374
375	Result
376}
377
378fn hex_digit(Byte:u8) -> Option<u8> {
379	match Byte {
380		b'0'..=b'9' => Some(Byte - b'0'),
381		b'a'..=b'f' => Some(Byte - b'a' + 10),
382		b'A'..=b'F' => Some(Byte - b'A' + 10),
383		_ => None,
384	}
385}
386
387/// Convert a `std::fs::Metadata` to VS Code's `IStat` shape.
388pub fn metadata_to_istat(Metadata:&std::fs::Metadata) -> Value {
389	let FileType = if Metadata.is_symlink() {
390		64 // SymbolicLink
391	} else if Metadata.is_dir() {
392		2 // Directory
393	} else {
394		1 // File
395	};
396
397	let Size = Metadata.len();
398
399	let Mtime = Metadata
400		.modified()
401		.ok()
402		.and_then(|T| T.duration_since(std::time::UNIX_EPOCH).ok())
403		.map(|D| D.as_millis() as u64)
404		.unwrap_or(0);
405
406	let Ctime = Metadata
407		.created()
408		.ok()
409		.and_then(|T| T.duration_since(std::time::UNIX_EPOCH).ok())
410		.map(|D| D.as_millis() as u64)
411		.unwrap_or(Mtime);
412
413	json!({
414		"type": FileType,
415		"size": Size,
416		"mtime": Mtime,
417		"ctime": Ctime
418	})
419}
420
421/// Ensure userdata directories exist on first access.
422/// Creates ~/.land/User/ and subdirectories that VS Code expects.
423static USERDATA_INITIALIZED:std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
424
425fn ensure_userdata_dirs() {
426	if USERDATA_INITIALIZED.swap(true, std::sync::atomic::Ordering::Relaxed) {
427		return; // Already initialized
428	}
429
430	let Base = get_userdata_base_dir();
431	let Dirs = [
432		format!("{}/User", Base),
433		format!("{}/User/globalStorage", Base),
434		format!("{}/User/profiles/__default__profile__", Base),
435		format!("{}/User/snippets", Base),
436		format!("{}/User/prompts", Base),
437		format!("{}/User/cacheHome", Base),
438		format!("{}/logs", Base),
439		format!("{}/User/workspaceStorage", Base),
440		format!(
441			"{}/CachedConfigurations/defaults/__default__profile__-configurationDefaultsOverrides",
442			Base
443		),
444	];
445
446	for Dir in &Dirs {
447		if let Err(E) = std::fs::create_dir_all(Dir) {
448			dev_log!("lifecycle", "Failed to create userdata dir {}: {}", Dir, E);
449		}
450	}
451
452	// Create default empty files if they don't exist
453	let DefaultFiles = [
454		(format!("{}/User/settings.json", Base), "{}"),
455		(format!("{}/User/keybindings.json", Base), "[]"),
456		(format!("{}/User/tasks.json", Base), "{}"),
457		(format!("{}/User/extensions.json", Base), "[]"),
458		(format!("{}/User/mcp.json", Base), "{}"),
459	];
460
461	for (FilePath, DefaultContent) in &DefaultFiles {
462		if !std::path::Path::new(FilePath).exists() {
463			if let Err(E) = std::fs::write(FilePath, DefaultContent) {
464				dev_log!("lifecycle", "Failed to create default file {}: {}", FilePath, E);
465			}
466		}
467	}
468
469	dev_log!("lifecycle", "userdata dirs initialized at: {}/User/", Base);
470}
471
472/// Handler for Wind's MainProcessService.invoke() calls
473/// Maps Tauri IPC commands to Mountain's internal command system
474#[tauri::command]
475pub async fn mountain_ipc_invoke(app_handle:AppHandle, command:String, args:Vec<Value>) -> Result<Value, String> {
476	let OTLPStart = crate::IPC::DevLog::NowNano();
477	dev_log!("ipc", "invoke: {} args_count={}", command, args.len());
478
479	// Ensure userdata directories exist on first IPC call
480	ensure_userdata_dirs();
481
482	// Get the application runtime
483	let runtime = app_handle.state::<Arc<ApplicationRunTime>>();
484
485	// =========================================================================
486	// Route dispatch - every arm has a dev_log! with a granular tag.
487	// Tags match the route prefix: vfs, config, storage, extensions,
488	// terminal, output, textfile, notification, progress, quickinput,
489	// workspaces, themes, search, decorations, workingcopy, keybinding,
490	// lifecycle, label, model, history, commands, nativehost, window,
491	// exthost, encryption, menubar, update, url, grpc.
492	// Activate: LAND_DEV_LOG=all   or   LAND_DEV_LOG=vfs,ipc,config
493	// =========================================================================
494
495	let Result = match command.as_str() {
496		// Configuration commands
497		"configuration:get" => {
498			dev_log!("config", "configuration:get");
499			handle_configuration_get(runtime.inner().clone(), args).await
500		},
501		"configuration:update" => {
502			dev_log!("config", "configuration:update");
503			handle_configuration_update(runtime.inner().clone(), args).await
504		},
505
506		// Logger commands - fire-and-forget from Wind, just acknowledge
507		"logger:log"
508		| "logger:warn"
509		| "logger:error"
510		| "logger:info"
511		| "logger:debug"
512		| "logger:trace"
513		| "logger:critical"
514		| "logger:flush"
515		| "logger:setLevel"
516		| "logger:getLevel"
517		| "logger:createLogger"
518		| "logger:registerLogger"
519		| "logger:deregisterLogger"
520		| "logger:getRegisteredLoggers"
521		| "logger:setVisibility" => Ok(Value::Null),
522
523		// File system commands - use native handlers with URI support
524		"file:read" => handle_file_read_native(args).await,
525		"file:write" => handle_file_write_native(args).await,
526		"file:stat" => handle_file_stat_native(args).await,
527		"file:exists" => handle_file_exists_native(args).await,
528		"file:delete" => handle_file_delete_native(args).await,
529		"file:copy" => handle_file_clone_native(args).await,
530		"file:move" => handle_file_rename_native(args).await,
531		"file:mkdir" => handle_file_mkdir_native(args).await,
532		"file:readdir" => handle_file_readdir_native(args).await,
533		"file:readBinary" => handle_file_read_binary(runtime.inner().clone(), args).await,
534		"file:writeBinary" => handle_file_write_binary(runtime.inner().clone(), args).await,
535
536		// Storage commands
537		"storage:get" => handle_storage_get(runtime.inner().clone(), args).await,
538		"storage:set" => handle_storage_set(runtime.inner().clone(), args).await,
539		"storage:getItems" => {
540			dev_log!("storage", "storage:getItems");
541			handle_storage_get_items(runtime.inner().clone(), args).await
542		},
543		"storage:updateItems" => {
544			dev_log!("storage", "storage:updateItems");
545			handle_storage_update_items(runtime.inner().clone(), args).await
546		},
547		"storage:optimize" => {
548			dev_log!("storage", "storage:optimize");
549			Ok(Value::Null)
550		},
551		"storage:isUsed" => {
552			dev_log!("storage", "storage:isUsed");
553			Ok(Value::Null)
554		},
555		"storage:close" => {
556			dev_log!("storage", "storage:close");
557			Ok(Value::Null)
558		},
559
560		// Environment commands
561		"environment:get" => {
562			dev_log!("config", "environment:get");
563			handle_environment_get(runtime.inner().clone(), args).await
564		},
565
566		// Native host commands
567		"native:showItemInFolder" => handle_show_item_in_folder(runtime.inner().clone(), args).await,
568		"native:openExternal" => handle_open_external(runtime.inner().clone(), args).await,
569
570		// Workbench commands
571		"workbench:getConfiguration" => handle_workbench_configuration(runtime.inner().clone(), args).await,
572
573		// Command registry commands
574		"commands:execute" => handle_commands_execute(runtime.inner().clone(), args).await,
575		"commands:getAll" => {
576			dev_log!("commands", "commands:getAll");
577			handle_commands_get_all(runtime.inner().clone()).await
578		},
579
580		// Extension host commands
581		"extensions:getAll" => {
582			dev_log!("extensions", "extensions:getAll");
583			handle_extensions_get_all(runtime.inner().clone()).await
584		},
585		"extensions:get" => {
586			dev_log!("extensions", "extensions:get");
587			handle_extensions_get(runtime.inner().clone(), args).await
588		},
589		"extensions:isActive" => {
590			dev_log!("extensions", "extensions:isActive");
591			handle_extensions_is_active(runtime.inner().clone(), args).await
592		},
593
594		// Terminal commands
595		"terminal:create" => {
596			dev_log!("terminal", "terminal:create");
597			handle_terminal_create(runtime.inner().clone(), args).await
598		},
599		"terminal:sendText" => {
600			dev_log!("terminal", "terminal:sendText");
601			handle_terminal_send_text(runtime.inner().clone(), args).await
602		},
603		"terminal:dispose" => {
604			dev_log!("terminal", "terminal:dispose");
605			handle_terminal_dispose(runtime.inner().clone(), args).await
606		},
607		"terminal:show" => {
608			dev_log!("terminal", "terminal:show");
609			handle_terminal_show(runtime.inner().clone(), args).await
610		},
611		"terminal:hide" => {
612			dev_log!("terminal", "terminal:hide");
613			handle_terminal_hide(runtime.inner().clone(), args).await
614		},
615
616		// Output channel commands
617		"output:create" => handle_output_create(app_handle.clone(), args).await,
618		"output:append" => {
619			dev_log!("output", "output:append");
620			handle_output_append(app_handle.clone(), args).await
621		},
622		"output:appendLine" => {
623			dev_log!("output", "output:appendLine");
624			handle_output_append_line(app_handle.clone(), args).await
625		},
626		"output:clear" => {
627			dev_log!("output", "output:clear");
628			handle_output_clear(app_handle.clone(), args).await
629		},
630		"output:show" => {
631			dev_log!("output", "output:show");
632			handle_output_show(app_handle.clone(), args).await
633		},
634
635		// TextFile commands
636		"textFile:read" => {
637			dev_log!("textfile", "textFile:read");
638			handle_textfile_read(runtime.inner().clone(), args).await
639		},
640		"textFile:write" => {
641			dev_log!("textfile", "textFile:write");
642			handle_textfile_write(runtime.inner().clone(), args).await
643		},
644		"textFile:save" => handle_textfile_save(runtime.inner().clone(), args).await,
645
646		// Storage commands (additional)
647		"storage:delete" => {
648			dev_log!("storage", "storage:delete");
649			handle_storage_delete(runtime.inner().clone(), args).await
650		},
651		"storage:keys" => {
652			dev_log!("storage", "storage:keys");
653			handle_storage_keys(runtime.inner().clone()).await
654		},
655
656		// Notification commands (emit sky:// events for Sky to render)
657		"notification:show" => {
658			dev_log!("notification", "notification:show");
659			handle_notification_show(app_handle.clone(), args).await
660		},
661		"notification:showProgress" => {
662			dev_log!("notification", "notification:showProgress");
663			handle_notification_show_progress(app_handle.clone(), args).await
664		},
665		"notification:updateProgress" => {
666			dev_log!("notification", "notification:updateProgress");
667			handle_notification_update_progress(app_handle.clone(), args).await
668		},
669		"notification:endProgress" => {
670			dev_log!("notification", "notification:endProgress");
671			handle_notification_end_progress(app_handle.clone(), args).await
672		},
673
674		// Progress commands
675		"progress:begin" => {
676			dev_log!("progress", "progress:begin");
677			handle_progress_begin(app_handle.clone(), args).await
678		},
679		"progress:report" => {
680			dev_log!("progress", "progress:report");
681			handle_progress_report(app_handle.clone(), args).await
682		},
683		"progress:end" => {
684			dev_log!("progress", "progress:end");
685			handle_progress_end(app_handle.clone(), args).await
686		},
687
688		// QuickInput commands
689		"quickInput:showQuickPick" => {
690			dev_log!("quickinput", "quickInput:showQuickPick");
691			handle_quick_input_show_quick_pick(runtime.inner().clone(), args).await
692		},
693		"quickInput:showInputBox" => {
694			dev_log!("quickinput", "quickInput:showInputBox");
695			handle_quick_input_show_input_box(runtime.inner().clone(), args).await
696		},
697
698		// Workspaces commands
699		"workspaces:getFolders" => {
700			dev_log!("workspaces", "workspaces:getFolders");
701			handle_workspaces_get_folders(runtime.inner().clone()).await
702		},
703		"workspaces:addFolder" => {
704			dev_log!("workspaces", "workspaces:addFolder");
705			handle_workspaces_add_folder(runtime.inner().clone(), args).await
706		},
707		"workspaces:removeFolder" => {
708			dev_log!("workspaces", "workspaces:removeFolder");
709			handle_workspaces_remove_folder(runtime.inner().clone(), args).await
710		},
711		"workspaces:getName" => {
712			dev_log!("workspaces", "workspaces:getName");
713			handle_workspaces_get_name(runtime.inner().clone()).await
714		},
715
716		// Themes commands
717		"themes:getActive" => {
718			dev_log!("themes", "themes:getActive");
719			handle_themes_get_active(runtime.inner().clone()).await
720		},
721		"themes:list" => {
722			dev_log!("themes", "themes:list");
723			handle_themes_list(runtime.inner().clone()).await
724		},
725		"themes:set" => {
726			dev_log!("themes", "themes:set");
727			handle_themes_set(runtime.inner().clone(), args).await
728		},
729
730		// Search commands
731		"search:findInFiles" => {
732			dev_log!("search", "search:findInFiles");
733			handle_search_find_in_files(runtime.inner().clone(), args).await
734		},
735		"search:findFiles" => {
736			dev_log!("search", "search:findFiles");
737			handle_search_find_files(runtime.inner().clone(), args).await
738		},
739
740		// Decorations commands
741		"decorations:get" => {
742			dev_log!("decorations", "decorations:get");
743			handle_decorations_get(runtime.inner().clone(), args).await
744		},
745		"decorations:getMany" => {
746			dev_log!("decorations", "decorations:getMany");
747			handle_decorations_get_many(runtime.inner().clone(), args).await
748		},
749		"decorations:set" => {
750			dev_log!("decorations", "decorations:set");
751			handle_decorations_set(runtime.inner().clone(), args).await
752		},
753		"decorations:clear" => {
754			dev_log!("decorations", "decorations:clear");
755			handle_decorations_clear(runtime.inner().clone(), args).await
756		},
757
758		// WorkingCopy commands
759		"workingCopy:isDirty" => {
760			dev_log!("workingcopy", "workingCopy:isDirty");
761			handle_working_copy_is_dirty(runtime.inner().clone(), args).await
762		},
763		"workingCopy:setDirty" => {
764			dev_log!("workingcopy", "workingCopy:setDirty");
765			handle_working_copy_set_dirty(runtime.inner().clone(), args).await
766		},
767		"workingCopy:getAllDirty" => {
768			dev_log!("workingcopy", "workingCopy:getAllDirty");
769			handle_working_copy_get_all_dirty(runtime.inner().clone()).await
770		},
771		"workingCopy:getDirtyCount" => {
772			dev_log!("workingcopy", "workingCopy:getDirtyCount");
773			handle_working_copy_get_dirty_count(runtime.inner().clone()).await
774		},
775
776		// Keybinding commands
777		"keybinding:add" => {
778			dev_log!("keybinding", "keybinding:add");
779			handle_keybinding_add(runtime.inner().clone(), args).await
780		},
781		"keybinding:remove" => {
782			dev_log!("keybinding", "keybinding:remove");
783			handle_keybinding_remove(runtime.inner().clone(), args).await
784		},
785		"keybinding:lookup" => {
786			dev_log!("keybinding", "keybinding:lookup");
787			handle_keybinding_lookup(runtime.inner().clone(), args).await
788		},
789		"keybinding:getAll" => {
790			dev_log!("keybinding", "keybinding:getAll");
791			handle_keybinding_get_all(runtime.inner().clone()).await
792		},
793
794		// Lifecycle commands
795		"lifecycle:getPhase" => {
796			dev_log!("lifecycle", "lifecycle:getPhase");
797			handle_lifecycle_get_phase(runtime.inner().clone()).await
798		},
799		"lifecycle:whenPhase" => {
800			dev_log!("lifecycle", "lifecycle:whenPhase");
801			handle_lifecycle_when_phase(runtime.inner().clone(), args).await
802		},
803		"lifecycle:requestShutdown" => {
804			dev_log!("lifecycle", "lifecycle:requestShutdown");
805			handle_lifecycle_request_shutdown(app_handle.clone()).await
806		},
807
808		// Label commands
809		"label:getUri" => {
810			dev_log!("label", "label:getUri");
811			handle_label_get_uri(runtime.inner().clone(), args).await
812		},
813		"label:getWorkspace" => {
814			dev_log!("label", "label:getWorkspace");
815			handle_label_get_workspace(runtime.inner().clone()).await
816		},
817		"label:getBase" => {
818			dev_log!("label", "label:getBase");
819			handle_label_get_base(args).await
820		},
821
822		// Model (text model registry) commands
823		"model:open" => {
824			dev_log!("model", "model:open");
825			handle_model_open(runtime.inner().clone(), args).await
826		},
827		"model:close" => {
828			dev_log!("model", "model:close");
829			handle_model_close(runtime.inner().clone(), args).await
830		},
831		"model:get" => {
832			dev_log!("model", "model:get");
833			handle_model_get(runtime.inner().clone(), args).await
834		},
835		"model:getAll" => {
836			dev_log!("model", "model:getAll");
837			handle_model_get_all(runtime.inner().clone()).await
838		},
839		"model:updateContent" => {
840			dev_log!("model", "model:updateContent");
841			handle_model_update_content(runtime.inner().clone(), args).await
842		},
843
844		// Navigation history commands
845		"history:goBack" => {
846			dev_log!("history", "history:goBack");
847			handle_history_go_back(runtime.inner().clone()).await
848		},
849		"history:goForward" => {
850			dev_log!("history", "history:goForward");
851			handle_history_go_forward(runtime.inner().clone()).await
852		},
853		"history:canGoBack" => {
854			dev_log!("history", "history:canGoBack");
855			handle_history_can_go_back(runtime.inner().clone()).await
856		},
857		"history:canGoForward" => {
858			dev_log!("history", "history:canGoForward");
859			handle_history_can_go_forward(runtime.inner().clone()).await
860		},
861		"history:push" => {
862			dev_log!("history", "history:push");
863			handle_history_push(runtime.inner().clone(), args).await
864		},
865		"history:clear" => {
866			dev_log!("history", "history:clear");
867			handle_history_clear(runtime.inner().clone()).await
868		},
869		"history:getStack" => {
870			dev_log!("history", "history:getStack");
871			handle_history_get_stack(runtime.inner().clone()).await
872		},
873
874		// IPC status commands
875		"mountain_get_status" => {
876			let status = json!({
877				"connected": true,
878				"version": "1.0.0"
879			});
880			Ok(status)
881		},
882		"mountain_get_configuration" => {
883			let config = json!({
884				"editor": { "theme": "dark" },
885				"extensions": { "installed": [] }
886			});
887			Ok(config)
888		},
889		"mountain_get_services_status" => {
890			let services = json!({
891				"editor": { "status": "running" },
892				"extensionHost": { "status": "running" }
893			});
894			Ok(services)
895		},
896		"mountain_get_state" => {
897			let state = json!({
898				"ui": {},
899				"editor": {},
900				"workspace": {}
901			});
902			Ok(state)
903		},
904
905		// =====================================================================
906		// File system command ALIASES
907		// VS Code's DiskFileSystemProviderClient calls readFile/writeFile/rename
908		// but Mountain's original handlers use read/write/move.
909		// =====================================================================
910		"file:readFile" => handle_file_read_native(args).await,
911		"file:writeFile" => handle_file_write_native(args).await,
912		"file:rename" => handle_file_rename_native(args).await,
913		"file:realpath" => handle_file_realpath(args).await,
914		"file:watch" => {
915			dev_log!("vfs", "file:watch stub - no-op");
916			Ok(Value::Null)
917		},
918		"file:unwatch" => {
919			dev_log!("vfs", "file:unwatch stub - no-op");
920			Ok(Value::Null)
921		},
922		"file:open" => {
923			dev_log!("vfs", "file:open stub - no fd support yet");
924			Ok(json!(0))
925		},
926		"file:close" => {
927			dev_log!("vfs", "file:close stub");
928			Ok(Value::Null)
929		},
930		"file:cloneFile" => handle_file_clone_native(args).await,
931
932		// =====================================================================
933		// Native Host commands (INativeHostService)
934		// =====================================================================
935
936		// Dialogs
937		"nativeHost:pickFolderAndOpen" => handle_native_pick_folder(app_handle.clone(), args).await,
938		"nativeHost:pickFileAndOpen" => handle_native_pick_folder(app_handle.clone(), args).await,
939		"nativeHost:pickFileFolderAndOpen" => handle_native_pick_folder(app_handle.clone(), args).await,
940		"nativeHost:pickWorkspaceAndOpen" => handle_native_pick_folder(app_handle.clone(), args).await,
941		"nativeHost:showOpenDialog" => handle_native_show_open_dialog(app_handle.clone(), args).await,
942		"nativeHost:showSaveDialog" => Ok(json!({ "canceled": true })),
943		"nativeHost:showMessageBox" => Ok(json!({ "response": 0 })),
944
945		// Environment paths - called by ResolveConfiguration to get real Tauri paths.
946		// Returns the session log directory (with timestamp + window1 subdir)
947		// so VS Code can immediately write output files without stat errors.
948		"nativeHost:getEnvironmentPaths" => {
949			let PathResolver = app_handle.path();
950			let AppDataDir = PathResolver.app_data_dir().unwrap_or_default();
951			let HomeDir = PathResolver.home_dir().unwrap_or_default();
952			let TmpDir = std::env::temp_dir();
953
954			// Logs go under {appDataDir}/logs/{sessionTimestamp}/ - same tree as
955			// all other VS Code data, not Tauri's separate app_log_dir().
956			// VS Code requires a session-timestamped subdir for log rotation.
957			// The timestamp is generated ONCE per process so every call returns
958			// the same logsPath (prevents VS Code from seeing stale dirs).
959			let SessionTimestamp =
960				SESSION_TIMESTAMP.get_or_init(|| chrono::Local::now().format("%Y%m%dT%H%M%S").to_string());
961			let SessionLogRoot = AppDataDir.join("logs").join(SessionTimestamp);
962			let SessionLogWindowDir = SessionLogRoot.join("window1");
963			let _ = std::fs::create_dir_all(&SessionLogWindowDir);
964
965			dev_log!(
966				"config",
967				"getEnvironmentPaths: userDataDir={} logsPath={} homeDir={}",
968				AppDataDir.display(),
969				SessionLogRoot.display(),
970				HomeDir.display()
971			);
972			let DevLogEnv = std::env::var("LAND_DEV_LOG").unwrap_or_default();
973			Ok(json!({
974				"userDataDir": AppDataDir.to_string_lossy(),
975				"logsPath": SessionLogRoot.to_string_lossy(),
976				"homeDir": HomeDir.to_string_lossy(),
977				"tmpDir": TmpDir.to_string_lossy(),
978				"devLog": if DevLogEnv.is_empty() { Value::Null } else { json!(DevLogEnv) },
979			}))
980		},
981
982		// OS info
983		"nativeHost:getOSColorScheme" => {
984			dev_log!("nativehost", "nativeHost:getOSColorScheme");
985			handle_native_get_color_scheme().await
986		},
987		"nativeHost:getOSProperties" => {
988			dev_log!("nativehost", "nativeHost:getOSProperties");
989			handle_native_os_properties().await
990		},
991		"nativeHost:getOSStatistics" => {
992			dev_log!("nativehost", "nativeHost:getOSStatistics");
993			handle_native_os_statistics().await
994		},
995		"nativeHost:getOSVirtualMachineHint" => {
996			dev_log!("nativehost", "nativeHost:getOSVirtualMachineHint");
997			Ok(json!(0))
998		},
999
1000		// Window state
1001		"nativeHost:isWindowAlwaysOnTop" => {
1002			dev_log!("window", "nativeHost:isWindowAlwaysOnTop");
1003			Ok(json!(false))
1004		},
1005		"nativeHost:isFullScreen" => {
1006			dev_log!("window", "nativeHost:isFullScreen");
1007			handle_native_is_fullscreen(app_handle.clone()).await
1008		},
1009		"nativeHost:isMaximized" => {
1010			dev_log!("window", "nativeHost:isMaximized");
1011			handle_native_is_maximized(app_handle.clone()).await
1012		},
1013		"nativeHost:getActiveWindowId" => {
1014			dev_log!("window", "nativeHost:getActiveWindowId");
1015			Ok(json!(1))
1016		},
1017		"nativeHost:getWindows" => Ok(json!([{ "id": 1, "title": "Land", "filename": "" }])),
1018		"nativeHost:getWindowCount" => Ok(json!(1)),
1019
1020		// Window control (fire-and-forget)
1021		"nativeHost:updateWindowControls"
1022		| "nativeHost:setMinimumSize"
1023		| "nativeHost:notifyReady"
1024		| "nativeHost:saveWindowSplash"
1025		| "nativeHost:updateTouchBar"
1026		| "nativeHost:focusWindow"
1027		| "nativeHost:maximizeWindow"
1028		| "nativeHost:minimizeWindow"
1029		| "nativeHost:unmaximizeWindow"
1030		| "nativeHost:moveWindowTop"
1031		| "nativeHost:positionWindow"
1032		| "nativeHost:toggleFullScreen"
1033		| "nativeHost:setWindowAlwaysOnTop"
1034		| "nativeHost:toggleWindowAlwaysOnTop"
1035		| "nativeHost:closeWindow"
1036		| "nativeHost:setDocumentEdited"
1037		| "nativeHost:setRepresentedFilename"
1038		| "nativeHost:setBackgroundThrottling"
1039		| "nativeHost:updateWindowAccentColor" => {
1040			dev_log!("window", "{}", command);
1041			Ok(Value::Null)
1042		},
1043
1044		// OS operations
1045		"nativeHost:isAdmin" => Ok(json!(false)),
1046		"nativeHost:isRunningUnderARM64Translation" => {
1047			#[cfg(target_os = "macos")]
1048			{
1049				// macOS: check if running under Rosetta 2
1050				let Output = std::process::Command::new("sysctl")
1051					.args(["-n", "sysctl.proc_translated"])
1052					.output();
1053				let IsTranslated = Output
1054					.ok()
1055					.map(|O| String::from_utf8_lossy(&O.stdout).trim() == "1")
1056					.unwrap_or(false);
1057				Ok(json!(IsTranslated))
1058			}
1059			#[cfg(not(target_os = "macos"))]
1060			{
1061				Ok(json!(false))
1062			}
1063		},
1064		"nativeHost:hasWSLFeatureInstalled" => {
1065			#[cfg(target_os = "windows")]
1066			{
1067				Ok(json!(std::path::Path::new("C:\\Windows\\System32\\wsl.exe").exists()))
1068			}
1069			#[cfg(not(target_os = "windows"))]
1070			{
1071				Ok(json!(false))
1072			}
1073		},
1074		"nativeHost:showItemInFolder" => handle_show_item_in_folder(runtime.inner().clone(), args).await,
1075		"nativeHost:openExternal" => handle_open_external(runtime.inner().clone(), args).await,
1076		"nativeHost:moveItemToTrash" => Ok(Value::Null),
1077
1078		// Clipboard
1079		"nativeHost:readClipboardText" => {
1080			dev_log!("clipboard", "readClipboardText");
1081			Ok(json!(""))
1082		},
1083		"nativeHost:writeClipboardText" => {
1084			dev_log!("clipboard", "writeClipboardText");
1085			Ok(Value::Null)
1086		},
1087		"nativeHost:readClipboardFindText" => {
1088			dev_log!("clipboard", "readClipboardFindText");
1089			Ok(json!(""))
1090		},
1091		"nativeHost:writeClipboardFindText" => {
1092			dev_log!("clipboard", "writeClipboardFindText");
1093			Ok(Value::Null)
1094		},
1095		"nativeHost:readClipboardBuffer" => {
1096			dev_log!("clipboard", "readClipboardBuffer");
1097			Ok(json!([]))
1098		},
1099		"nativeHost:writeClipboardBuffer" => {
1100			dev_log!("clipboard", "writeClipboardBuffer");
1101			Ok(Value::Null)
1102		},
1103		"nativeHost:hasClipboard" => {
1104			dev_log!("clipboard", "hasClipboard");
1105			Ok(json!(false))
1106		},
1107		"nativeHost:readImage" => {
1108			dev_log!("clipboard", "readImage");
1109			Ok(json!([]))
1110		},
1111		"nativeHost:triggerPaste" => {
1112			dev_log!("clipboard", "triggerPaste");
1113			Ok(Value::Null)
1114		},
1115
1116		// Process
1117		"nativeHost:getProcessId" => Ok(json!(std::process::id())),
1118		"nativeHost:killProcess" => Ok(Value::Null),
1119
1120		// Network
1121		"nativeHost:findFreePort" => handle_native_find_free_port(args).await,
1122		"nativeHost:isPortFree" => Ok(json!(true)),
1123		"nativeHost:resolveProxy" => Ok(Value::Null),
1124		"nativeHost:lookupAuthorization" => Ok(Value::Null),
1125		"nativeHost:lookupKerberosAuthorization" => Ok(Value::Null),
1126		"nativeHost:loadCertificates" => Ok(json!([])),
1127
1128		// Lifecycle
1129		"nativeHost:relaunch" => Ok(Value::Null),
1130		"nativeHost:reload" => Ok(Value::Null),
1131		"nativeHost:quit" => Ok(Value::Null),
1132		"nativeHost:exit" => Ok(Value::Null),
1133
1134		// Dev tools
1135		"nativeHost:openDevTools" => Ok(Value::Null),
1136		"nativeHost:toggleDevTools" => Ok(Value::Null),
1137
1138		// Power
1139		"nativeHost:getSystemIdleState" => Ok(json!("active")),
1140		"nativeHost:getSystemIdleTime" => Ok(json!(0)),
1141		"nativeHost:getCurrentThermalState" => Ok(json!("nominal")),
1142		"nativeHost:isOnBatteryPower" => Ok(json!(false)),
1143		"nativeHost:startPowerSaveBlocker" => Ok(json!(0)),
1144		"nativeHost:stopPowerSaveBlocker" => Ok(json!(false)),
1145		"nativeHost:isPowerSaveBlockerStarted" => Ok(json!(false)),
1146
1147		// macOS specific
1148		"nativeHost:newWindowTab" => Ok(Value::Null),
1149		"nativeHost:showPreviousWindowTab" => Ok(Value::Null),
1150		"nativeHost:showNextWindowTab" => Ok(Value::Null),
1151		"nativeHost:moveWindowTabToNewWindow" => Ok(Value::Null),
1152		"nativeHost:mergeAllWindowTabs" => Ok(Value::Null),
1153		"nativeHost:toggleWindowTabsBar" => Ok(Value::Null),
1154		"nativeHost:installShellCommand" => Ok(Value::Null),
1155		"nativeHost:uninstallShellCommand" => Ok(Value::Null),
1156
1157		// =====================================================================
1158		// Local PTY (terminal) commands
1159		// =====================================================================
1160		"localPty:getProfiles" => {
1161			dev_log!("terminal", "localPty:getProfiles");
1162			handle_local_pty_get_profiles().await
1163		},
1164		"localPty:getDefaultSystemShell" => {
1165			dev_log!("terminal", "localPty:getDefaultSystemShell");
1166			handle_local_pty_get_default_shell().await
1167		},
1168		"localPty:getTerminalLayoutInfo" => {
1169			dev_log!("terminal", "localPty:getTerminalLayoutInfo");
1170			Ok(Value::Null)
1171		},
1172		"localPty:setTerminalLayoutInfo" => {
1173			dev_log!("terminal", "localPty:setTerminalLayoutInfo");
1174			Ok(Value::Null)
1175		},
1176		"localPty:getPerformanceMarks" => {
1177			dev_log!("terminal", "localPty:getPerformanceMarks");
1178			Ok(json!([]))
1179		},
1180		"localPty:reduceConnectionGraceTime" => {
1181			dev_log!("terminal", "localPty:reduceConnectionGraceTime");
1182			Ok(Value::Null)
1183		},
1184		"localPty:listProcesses" => {
1185			dev_log!("terminal", "localPty:listProcesses");
1186			Ok(json!([]))
1187		},
1188		"localPty:getEnvironment" => {
1189			dev_log!("terminal", "localPty:getEnvironment");
1190			handle_local_pty_get_environment().await
1191		},
1192
1193		// =====================================================================
1194		// Update service
1195		// =====================================================================
1196		"update:_getInitialState" => {
1197			dev_log!("update", "update:_getInitialState");
1198			Ok(json!({ "type": "idle", "updateType": 0 }))
1199		},
1200		"update:isLatestVersion" => {
1201			dev_log!("update", "update:isLatestVersion");
1202			Ok(json!(true))
1203		},
1204		"update:checkForUpdates" => {
1205			dev_log!("update", "update:checkForUpdates");
1206			Ok(Value::Null)
1207		},
1208		"update:downloadUpdate" => {
1209			dev_log!("update", "update:downloadUpdate");
1210			Ok(Value::Null)
1211		},
1212		"update:applyUpdate" => {
1213			dev_log!("update", "update:applyUpdate");
1214			Ok(Value::Null)
1215		},
1216		"update:quitAndInstall" => {
1217			dev_log!("update", "update:quitAndInstall");
1218			Ok(Value::Null)
1219		},
1220
1221		// =====================================================================
1222		// Menubar
1223		// =====================================================================
1224		"menubar:updateMenubar" => {
1225			dev_log!("menubar", "menubar:updateMenubar");
1226			Ok(Value::Null)
1227		},
1228
1229		// =====================================================================
1230		// URL handler
1231		// =====================================================================
1232		"url:registerExternalUriOpener" => {
1233			dev_log!("url", "url:registerExternalUriOpener");
1234			Ok(Value::Null)
1235		},
1236
1237		// =====================================================================
1238		// Encryption
1239		// =====================================================================
1240		"encryption:encrypt" => {
1241			dev_log!("encryption", "encryption:encrypt");
1242			Ok(json!(""))
1243		},
1244		"encryption:decrypt" => {
1245			dev_log!("encryption", "encryption:decrypt");
1246			Ok(json!(""))
1247		},
1248
1249		// =====================================================================
1250		// Extension host starter
1251		// =====================================================================
1252		"extensionHostStarter:createExtensionHost" => {
1253			dev_log!("exthost", "extensionHostStarter:createExtensionHost");
1254			Ok(json!({ "id": "1" }))
1255		},
1256		"extensionHostStarter:start" => {
1257			dev_log!("exthost", "extensionHostStarter:start pid={}", std::process::id());
1258			Ok(json!({ "pid": std::process::id() }))
1259		},
1260		"extensionHostStarter:kill" => {
1261			dev_log!("exthost", "extensionHostStarter:kill");
1262			Ok(Value::Null)
1263		},
1264		"extensionHostStarter:getExitInfo" => {
1265			dev_log!("exthost", "extensionHostStarter:getExitInfo");
1266			Ok(json!({ "code": null, "signal": null }))
1267		},
1268
1269		// =====================================================================
1270		// Extension host message relay (Wind → Mountain → Cocoon)
1271		// =====================================================================
1272		"cocoon:extensionHostMessage" => {
1273			let ByteCount = args
1274				.first()
1275				.map(|P| P.get("data").and_then(|D| D.as_array()).map(|A| A.len()).unwrap_or(0))
1276				.unwrap_or(0);
1277			dev_log!("exthost", "cocoon:extensionHostMessage bytes={}", ByteCount);
1278
1279			// Forward binary message to Cocoon via gRPC GenericNotification.
1280			// Fire-and-forget — the extension host protocol is async.
1281			let Payload = args.first().cloned().unwrap_or(Value::Null);
1282			tokio::spawn(async move {
1283				if let Err(Error) = crate::Vine::Client::SendNotification(
1284					"cocoon-main".to_string(),
1285					"extensionHostMessage".to_string(),
1286					Payload,
1287				)
1288				.await
1289				{
1290					dev_log!("exthost", "cocoon:extensionHostMessage forward failed: {}", Error);
1291				}
1292			});
1293			Ok(Value::Null)
1294		},
1295
1296		// =====================================================================
1297		// Extension host debug service
1298		// =====================================================================
1299		"extensionhostdebugservice:reload" => {
1300			dev_log!("exthost", "extensionhostdebugservice:reload");
1301			Ok(Value::Null)
1302		},
1303		"extensionhostdebugservice:close" => {
1304			dev_log!("exthost", "extensionhostdebugservice:close");
1305			Ok(Value::Null)
1306		},
1307
1308		// =====================================================================
1309		// Workspaces - additional commands
1310		// =====================================================================
1311		"workspaces:getRecentlyOpened" => {
1312			dev_log!("workspaces", "workspaces:getRecentlyOpened");
1313			Ok(json!({
1314				"workspaces": [],
1315				"files": []
1316			}))
1317		},
1318		"workspaces:removeRecentlyOpened" => {
1319			dev_log!("workspaces", "workspaces:removeRecentlyOpened");
1320			Ok(Value::Null)
1321		},
1322		"workspaces:addRecentlyOpened" => {
1323			dev_log!("workspaces", "workspaces:addRecentlyOpened");
1324			Ok(Value::Null)
1325		},
1326		"workspaces:clearRecentlyOpened" => {
1327			dev_log!("workspaces", "workspaces:clearRecentlyOpened");
1328			Ok(Value::Null)
1329		},
1330		"workspaces:enterWorkspace" => {
1331			dev_log!("workspaces", "workspaces:enterWorkspace");
1332			Ok(Value::Null)
1333		},
1334		"workspaces:createUntitledWorkspace" => {
1335			dev_log!("workspaces", "workspaces:createUntitledWorkspace");
1336			Ok(Value::Null)
1337		},
1338		"workspaces:deleteUntitledWorkspace" => {
1339			dev_log!("workspaces", "workspaces:deleteUntitledWorkspace");
1340			Ok(Value::Null)
1341		},
1342		"workspaces:getWorkspaceIdentifier" => Ok(Value::Null),
1343		"workspaces:getDirtyWorkspaces" => Ok(json!([])),
1344
1345		// Default handler for unknown commands
1346		_ => {
1347			dev_log!("ipc", "error: [WindServiceHandlers] Unknown IPC command: {}", command);
1348			Err(format!("Unknown IPC command: {}", command))
1349		},
1350	};
1351
1352	// Emit OTLP span for every IPC call — visible in Jaeger at localhost:16686
1353	let IsErr = Result.is_err();
1354	let SpanName = if IsErr {
1355		format!("ipc:{}:error", command)
1356	} else {
1357		format!("ipc:{}", command)
1358	};
1359	crate::otel_span!(&SpanName, OTLPStart, &[("ipc.command", command.as_str())]);
1360
1361	Result
1362}
1363
1364/// Handler for configuration get requests
1365async fn handle_configuration_get(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
1366	let key = args
1367		.get(0)
1368		.ok_or("Missing configuration key".to_string())?
1369		.as_str()
1370		.ok_or("Configuration key must be a string".to_string())?;
1371
1372	// Use Mountain's configuration system
1373	let provider:Arc<dyn ConfigurationProvider> = runtime.Environment.Require();
1374
1375	let value = provider
1376		.GetConfigurationValue(Some(key.to_string()), ConfigurationOverridesDTO::default())
1377		.await
1378		.map_err(|e| format!("Failed to get configuration: {}", e))?;
1379
1380	dev_log!("config", "get: {} = {:?}", key, value);
1381	Ok(value)
1382}
1383
1384/// Handler for configuration update requests
1385async fn handle_configuration_update(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
1386	let key = args
1387		.get(0)
1388		.ok_or("Missing configuration key".to_string())?
1389		.as_str()
1390		.ok_or("Configuration key must be a string".to_string())?;
1391
1392	let value = args.get(1).ok_or("Missing configuration value".to_string())?.clone();
1393
1394	// Use Mountain's configuration system
1395	let provider:Arc<dyn ConfigurationProvider> = runtime.Environment.Require();
1396
1397	provider
1398		.UpdateConfigurationValue(
1399			key.to_string(),
1400			value,
1401			ConfigurationTarget::User,
1402			ConfigurationOverridesDTO::default(),
1403			None,
1404		)
1405		.await
1406		.map_err(|e| format!("Failed to update configuration: {}", e))?;
1407
1408	dev_log!("config", "updated: {}", key);
1409	Ok(Value::Null)
1410}
1411
1412/// Handler for file read requests
1413async fn handle_file_read(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
1414	let path = args
1415		.get(0)
1416		.ok_or("Missing file path".to_string())?
1417		.as_str()
1418		.ok_or("File path must be a string".to_string())?;
1419
1420	// Use Mountain's file system provider
1421	let provider:Arc<dyn FileSystemReader> = runtime.Environment.Require();
1422
1423	let content = provider
1424		.ReadFile(&PathBuf::from(path))
1425		.await
1426		.map_err(|e| format!("Failed to read file: {}", e))?;
1427
1428	dev_log!("vfs", "read: {} ({} bytes)", path, content.len());
1429	Ok(json!(content))
1430}
1431
1432/// Handler for file write requests
1433async fn handle_file_write(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
1434	let path = args
1435		.get(0)
1436		.ok_or("Missing file path".to_string())?
1437		.as_str()
1438		.ok_or("File path must be a string".to_string())?;
1439
1440	let content = args
1441		.get(1)
1442		.ok_or("Missing file content".to_string())?
1443		.as_str()
1444		.ok_or("File content must be a string".to_string())?;
1445
1446	// Use Mountain's file system provider
1447	let provider:Arc<dyn FileSystemWriter> = runtime.Environment.Require();
1448
1449	provider
1450		.WriteFile(&PathBuf::from(path), content.as_bytes().to_vec(), true, true)
1451		.await
1452		.map_err(|e:CommonError| format!("Failed to write file: {}", e))?;
1453
1454	dev_log!("vfs", "written: {} ({} bytes)", path, content.len());
1455	Ok(Value::Null)
1456}
1457
1458/// Handler for file stat requests
1459async fn handle_file_stat(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
1460	let path = args
1461		.get(0)
1462		.ok_or("Missing file path".to_string())?
1463		.as_str()
1464		.ok_or("File path must be a string".to_string())?;
1465
1466	// Use Mountain's file system provider
1467	let provider:Arc<dyn FileSystemReader> = runtime.Environment.Require();
1468
1469	let stats = provider
1470		.StatFile(&PathBuf::from(path))
1471		.await
1472		.map_err(|e| format!("Failed to stat file: {}", e))?;
1473
1474	dev_log!("vfs", "legacy_stat: {}", path);
1475	Ok(json!(stats))
1476}
1477
1478/// Handler for file exists requests
1479async fn handle_file_exists(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
1480	let path = args
1481		.get(0)
1482		.ok_or("Missing file path".to_string())?
1483		.as_str()
1484		.ok_or("File path must be a string".to_string())?;
1485
1486	// Use Mountain's file system provider
1487	let provider:Arc<dyn FileSystemReader> = runtime.Environment.Require();
1488
1489	let exists = provider.StatFile(&PathBuf::from(path)).await.is_ok();
1490
1491	dev_log!("vfs", "exists: {} = {}", path, exists);
1492	Ok(json!(exists))
1493}
1494
1495/// Handler for file delete requests
1496async fn handle_file_delete(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
1497	let path = args
1498		.get(0)
1499		.ok_or("Missing file path".to_string())?
1500		.as_str()
1501		.ok_or("File path must be a string".to_string())?;
1502
1503	// Use Mountain's file system provider
1504	let provider:Arc<dyn FileSystemWriter> = runtime.Environment.Require();
1505
1506	provider
1507		.Delete(&PathBuf::from(path), false, false)
1508		.await
1509		.map_err(|e:CommonError| format!("Failed to delete file: {}", e))?;
1510
1511	dev_log!("vfs", "deleted: {}", path);
1512	Ok(Value::Null)
1513}
1514
1515/// Handler for file copy requests
1516async fn handle_file_copy(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
1517	let source = args
1518		.get(0)
1519		.ok_or("Missing source path".to_string())?
1520		.as_str()
1521		.ok_or("Source path must be a string".to_string())?;
1522
1523	let destination = args
1524		.get(1)
1525		.ok_or("Missing destination path".to_string())?
1526		.as_str()
1527		.ok_or("Destination path must be a string".to_string())?;
1528
1529	// Use Mountain's file system provider
1530	let provider:Arc<dyn FileSystemWriter> = runtime.Environment.Require();
1531
1532	provider
1533		.Copy(&PathBuf::from(source), &PathBuf::from(destination), false)
1534		.await
1535		.map_err(|e:CommonError| format!("Failed to copy file: {} -> {}", source, destination))?;
1536
1537	dev_log!("vfs", "copied: {} -> {}", source, destination);
1538	Ok(Value::Null)
1539}
1540
1541/// Handler for file move requests
1542async fn handle_file_move(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
1543	let source = args
1544		.get(0)
1545		.ok_or("Missing source path".to_string())?
1546		.as_str()
1547		.ok_or("Source path must be a string".to_string())?;
1548
1549	let destination = args
1550		.get(1)
1551		.ok_or("Missing destination path".to_string())?
1552		.as_str()
1553		.ok_or("Destination path must be a string".to_string())?;
1554
1555	// Use Mountain's file system provider
1556	let provider:Arc<dyn FileSystemWriter> = runtime.Environment.Require();
1557
1558	provider
1559		.Rename(&PathBuf::from(source), &PathBuf::from(destination), false)
1560		.await
1561		.map_err(|e:CommonError| format!("Failed to move file: {} -> {}", source, destination))?;
1562
1563	dev_log!("vfs", "moved: {} -> {}", source, destination);
1564	Ok(Value::Null)
1565}
1566
1567/// Handler for directory creation requests
1568async fn handle_file_mkdir(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
1569	let path = args
1570		.get(0)
1571		.ok_or("Missing directory path".to_string())?
1572		.as_str()
1573		.ok_or("Directory path must be a string".to_string())?;
1574
1575	let recursive = args.get(1).and_then(|v| v.as_bool()).unwrap_or(true);
1576
1577	// Use Mountain's file system provider
1578	let provider:Arc<dyn FileSystemWriter> = runtime.Environment.Require();
1579
1580	provider
1581		.CreateDirectory(&PathBuf::from(path), recursive)
1582		.await
1583		.map_err(|e:CommonError| format!("Failed to create directory: {}", e))?;
1584
1585	dev_log!("vfs", "mkdir: {} (recursive: {})", path, recursive);
1586	Ok(Value::Null)
1587}
1588
1589/// Handler for directory reading requests
1590async fn handle_file_readdir(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
1591	let path = args
1592		.get(0)
1593		.ok_or("Missing directory path".to_string())?
1594		.as_str()
1595		.ok_or("Directory path must be a string".to_string())?;
1596
1597	// Use Mountain's file system provider
1598	let provider:Arc<dyn FileSystemReader> = runtime.Environment.Require();
1599
1600	let entries = provider
1601		.ReadDirectory(&PathBuf::from(path))
1602		.await
1603		.map_err(|e| format!("Failed to read directory: {}", e))?;
1604
1605	dev_log!("vfs", "readdir_legacy: {} ({} entries)", path, entries.len());
1606	Ok(json!(entries))
1607}
1608
1609/// Handler for binary file read requests
1610async fn handle_file_read_binary(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
1611	let path = args
1612		.get(0)
1613		.ok_or("Missing file path".to_string())?
1614		.as_str()
1615		.ok_or("File path must be a string".to_string())?;
1616
1617	// Use Mountain's file system provider
1618	let provider:Arc<dyn FileSystemReader> = runtime.Environment.Require();
1619
1620	let content = provider
1621		.ReadFile(&PathBuf::from(path))
1622		.await
1623		.map_err(|e| format!("Failed to read binary file: {}", e))?;
1624
1625	dev_log!("vfs", "readBinary: {} ({} bytes)", path, content.len());
1626	Ok(json!(content))
1627}
1628
1629/// Handler for binary file write requests
1630async fn handle_file_write_binary(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
1631	let path = args
1632		.get(0)
1633		.ok_or("Missing file path".to_string())?
1634		.as_str()
1635		.ok_or("File path must be a string".to_string())?;
1636
1637	let content = args
1638		.get(1)
1639		.ok_or("Missing file content".to_string())?
1640		.as_str()
1641		.ok_or("File content must be a string".to_string())?;
1642
1643	// Convert string content to bytes
1644	let content_bytes = content.as_bytes().to_vec();
1645	let content_len = content_bytes.len();
1646
1647	// Use Mountain's file system provider
1648	let provider:Arc<dyn FileSystemWriter> = runtime.Environment.Require();
1649
1650	provider
1651		.WriteFile(&PathBuf::from(path), content_bytes.clone(), true, true)
1652		.await
1653		.map_err(|e:CommonError| format!("Failed to write binary file: {}", e))?;
1654
1655	dev_log!("vfs", "writeBinary: {} ({} bytes)", path, content_len);
1656	Ok(Value::Null)
1657}
1658
1659/// Handler for storage get requests
1660async fn handle_storage_get(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
1661	let key = args
1662		.get(0)
1663		.ok_or("Missing storage key".to_string())?
1664		.as_str()
1665		.ok_or("Storage key must be a string".to_string())?;
1666
1667	// Use Mountain's storage provider
1668	let provider:Arc<dyn StorageProvider> = runtime.Environment.Require();
1669
1670	let value = provider
1671		.GetStorageValue(false, key)
1672		.await
1673		.map_err(|e| format!("Failed to get storage item: {}", e))?;
1674
1675	dev_log!("storage", "get: {}", key);
1676	Ok(value.unwrap_or(Value::Null))
1677}
1678
1679/// Handler for storage set requests
1680async fn handle_storage_set(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
1681	let key = args
1682		.get(0)
1683		.ok_or("Missing storage key".to_string())?
1684		.as_str()
1685		.ok_or("Storage key must be a string".to_string())?;
1686
1687	let value = args.get(1).ok_or("Missing storage value".to_string())?.clone();
1688
1689	// Use Mountain's storage provider
1690	let provider:Arc<dyn StorageProvider> = runtime.Environment.Require();
1691
1692	provider
1693		.UpdateStorageValue(false, key.to_string(), Some(value))
1694		.await
1695		.map_err(|e| format!("Failed to set storage item: {}", e))?;
1696
1697	dev_log!("storage", "set: {}", key);
1698	Ok(Value::Null)
1699}
1700
1701/// Handler for environment get requests
1702async fn handle_environment_get(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
1703	let key = args
1704		.get(0)
1705		.ok_or("Missing environment key".to_string())?
1706		.as_str()
1707		.ok_or("Environment key must be a string".to_string())?;
1708
1709	// Use std::env for environment variables
1710	let value = std::env::var(key).map_err(|e| format!("Failed to get environment variable: {}", e))?;
1711
1712	dev_log!("config", "env_get: {}", key);
1713	Ok(json!(value))
1714}
1715
1716/// Handler for showing items in folder
1717async fn handle_show_item_in_folder(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
1718	let path_str = args
1719		.get(0)
1720		.ok_or("Missing file path".to_string())?
1721		.as_str()
1722		.ok_or("File path must be a string".to_string())?;
1723
1724	// IMPLEMENTATION: Microsoft-inspired native file system integration
1725	dev_log!("vfs", "showInFolder: {}", path_str);
1726
1727	let path = std::path::PathBuf::from(path_str);
1728
1729	// Validate path exists
1730	if !path.exists() {
1731		return Err(format!("Path does not exist: {}", path_str));
1732	}
1733
1734	#[cfg(target_os = "macos")]
1735	{
1736		use std::process::Command;
1737
1738		// Use macOS's open command with -R flag to reveal in Finder
1739		let result = Command::new("open")
1740			.arg("-R")
1741			.arg(&path)
1742			.output()
1743			.map_err(|e| format!("Failed to execute open command: {}", e))?;
1744
1745		if !result.status.success() {
1746			return Err(format!(
1747				"Failed to show item in folder: {}",
1748				String::from_utf8_lossy(&result.stderr)
1749			));
1750		}
1751	}
1752
1753	#[cfg(target_os = "windows")]
1754	{
1755		use std::process::Command;
1756
1757		// Use Windows Explorer with /select flag
1758		let result = Command::new("explorer")
1759			.arg("/select,")
1760			.arg(&path)
1761			.output()
1762			.map_err(|e| format!("Failed to execute explorer command: {}", e))?;
1763
1764		if !result.status.success() {
1765			return Err(format!(
1766				"Failed to show item in folder: {}",
1767				String::from_utf8_lossy(&result.stderr)
1768			));
1769		}
1770	}
1771
1772	#[cfg(target_os = "linux")]
1773	{
1774		use std::process::Command;
1775
1776		// Try common Linux file managers
1777		let file_managers = ["nautilus", "dolphin", "thunar", "pcmanfm", "nemo"];
1778		let mut last_error = String::new();
1779
1780		for manager in file_managers.iter() {
1781			let result = Command::new(manager).arg(&path).output();
1782
1783			match result {
1784				Ok(output) if output.status.success() => {
1785					dev_log!("lifecycle", "opened with {}", manager);
1786					break;
1787				},
1788				Err(e) => {
1789					last_error = e.to_string();
1790					continue;
1791				},
1792				_ => continue,
1793			}
1794		}
1795
1796		if !last_error.is_empty() {
1797			return Err(format!("Failed to show item in folder with any file manager: {}", last_error));
1798		}
1799	}
1800
1801	dev_log!("vfs", "showed in folder: {}", path_str);
1802	Ok(Value::Bool(true))
1803}
1804
1805/// Handler for opening external URLs
1806async fn handle_open_external(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
1807	let url_str = args
1808		.get(0)
1809		.ok_or("Missing URL".to_string())?
1810		.as_str()
1811		.ok_or("URL must be a string".to_string())?;
1812
1813	// IMPLEMENTATION: Microsoft-inspired URL validation and opening
1814	dev_log!("lifecycle", "openExternal: {}", url_str);
1815
1816	// Validate URL format
1817	if !url_str.starts_with("http://") && !url_str.starts_with("https://") {
1818		return Err(format!("Invalid URL format. Must start with http:// or https://: {}", url_str));
1819	}
1820
1821	#[cfg(target_os = "macos")]
1822	{
1823		use std::process::Command;
1824
1825		// Use macOS's open command
1826		let result = Command::new("open")
1827			.arg(url_str)
1828			.output()
1829			.map_err(|e| format!("Failed to execute open command: {}", e))?;
1830
1831		if !result.status.success() {
1832			return Err(format!("Failed to open URL: {}", String::from_utf8_lossy(&result.stderr)));
1833		}
1834	}
1835
1836	#[cfg(target_os = "windows")]
1837	{
1838		use std::process::Command;
1839
1840		// Use Windows start command
1841		let result = Command::new("cmd")
1842			.arg("/c")
1843			.arg("start")
1844			.arg(url_str)
1845			.output()
1846			.map_err(|e| format!("Failed to execute start command: {}", e))?;
1847
1848		if !result.status.success() {
1849			return Err(format!("Failed to open URL: {}", String::from_utf8_lossy(&result.stderr)));
1850		}
1851	}
1852
1853	#[cfg(target_os = "linux")]
1854	{
1855		use std::process::Command;
1856
1857		// Try common Linux URL handlers
1858		let handlers = ["xdg-open", "gnome-open", "kde-open", "x-www-browser"];
1859		let mut last_error = String::new();
1860
1861		for handler in handlers.iter() {
1862			let result = Command::new(handler).arg(url_str).output();
1863
1864			match result {
1865				Ok(output) if output.status.success() => {
1866					dev_log!("lifecycle", "opened with {}", handler);
1867					break;
1868				},
1869				Err(e) => {
1870					last_error = e.to_string();
1871					continue;
1872				},
1873				_ => continue,
1874			}
1875		}
1876
1877		if !last_error.is_empty() {
1878			return Err(format!("Failed to open URL with any handler: {}", last_error));
1879		}
1880	}
1881
1882	dev_log!("lifecycle", "opened URL: {}", url_str);
1883	Ok(Value::Bool(true))
1884}
1885
1886/// Handler for workbench configuration requests
1887async fn handle_workbench_configuration(runtime:Arc<ApplicationRunTime>, _args:Vec<Value>) -> Result<Value, String> {
1888	// Get the complete workbench configuration
1889	let provider:Arc<dyn ConfigurationProvider> = runtime.Environment.Require();
1890
1891	let config = provider
1892		.GetConfigurationValue(None, ConfigurationOverridesDTO::default())
1893		.await
1894		.map_err(|e| format!("Failed to get workbench configuration: {}", e))?;
1895
1896	dev_log!("config", "workbench config retrieved");
1897	Ok(config)
1898}
1899
1900// ============================================================================
1901// Terminal Handlers
1902// ============================================================================
1903
1904/// Create a new PTY terminal via TerminalProvider.
1905async fn handle_terminal_create(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
1906	use CommonLibrary::Terminal::TerminalProvider::TerminalProvider;
1907
1908	let Options = args.first().cloned().unwrap_or(Value::Null);
1909	runtime
1910		.Environment
1911		.CreateTerminal(Options)
1912		.await
1913		.map_err(|Error| format!("terminal:create failed: {}", Error))
1914}
1915
1916/// Write text to PTY stdin via TerminalProvider.
1917async fn handle_terminal_send_text(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
1918	use CommonLibrary::Terminal::TerminalProvider::TerminalProvider;
1919
1920	let TerminalId = args
1921		.first()
1922		.and_then(|V| V.as_u64())
1923		.ok_or_else(|| "terminal:sendText requires terminal_id as first argument".to_string())?;
1924	let Text = args.get(1).and_then(|V| V.as_str()).unwrap_or("").to_string();
1925
1926	runtime
1927		.Environment
1928		.SendTextToTerminal(TerminalId, Text)
1929		.await
1930		.map(|()| Value::Null)
1931		.map_err(|Error| format!("terminal:sendText failed: {}", Error))
1932}
1933
1934/// Dispose a terminal via TerminalProvider.
1935async fn handle_terminal_dispose(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
1936	use CommonLibrary::Terminal::TerminalProvider::TerminalProvider;
1937
1938	let TerminalId = args
1939		.first()
1940		.and_then(|V| V.as_u64())
1941		.ok_or_else(|| "terminal:dispose requires terminal_id as first argument".to_string())?;
1942
1943	runtime
1944		.Environment
1945		.DisposeTerminal(TerminalId)
1946		.await
1947		.map(|()| Value::Null)
1948		.map_err(|Error| format!("terminal:dispose failed: {}", Error))
1949}
1950
1951/// Show a terminal in the UI.
1952async fn handle_terminal_show(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
1953	use CommonLibrary::Terminal::TerminalProvider::TerminalProvider;
1954
1955	let TerminalId = args.first().and_then(|V| V.as_u64()).unwrap_or(0);
1956	let PreserveFocus = args.get(1).and_then(|V| V.as_bool()).unwrap_or(false);
1957
1958	runtime
1959		.Environment
1960		.ShowTerminal(TerminalId, PreserveFocus)
1961		.await
1962		.map(|()| Value::Null)
1963		.map_err(|Error| format!("terminal:show failed: {}", Error))
1964}
1965
1966/// Hide a terminal.
1967async fn handle_terminal_hide(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
1968	use CommonLibrary::Terminal::TerminalProvider::TerminalProvider;
1969
1970	let TerminalId = args.first().and_then(|V| V.as_u64()).unwrap_or(0);
1971
1972	runtime
1973		.Environment
1974		.HideTerminal(TerminalId)
1975		.await
1976		.map(|()| Value::Null)
1977		.map_err(|Error| format!("terminal:hide failed: {}", Error))
1978}
1979
1980// ============================================================================
1981// Output Channel Handlers
1982// ============================================================================
1983
1984/// Create a named output channel. Returns the channel name as its handle.
1985async fn handle_output_create(_app_handle:AppHandle, args:Vec<Value>) -> Result<Value, String> {
1986	let ChannelName = args.first().and_then(|V| V.as_str()).unwrap_or("Output").to_string();
1987	dev_log!("ipc", "output:create channel='{}'", ChannelName);
1988	// Sky/frontend creates the channel panel on the `sky://output/create` event
1989	Ok(json!({ "channelName": ChannelName }))
1990}
1991
1992/// Append text to an output channel.
1993async fn handle_output_append(app_handle:AppHandle, args:Vec<Value>) -> Result<Value, String> {
1994	use tauri::Emitter;
1995
1996	let ChannelName = args.first().and_then(|V| V.as_str()).unwrap_or("").to_string();
1997	let Text = args.get(1).and_then(|V| V.as_str()).unwrap_or("").to_string();
1998
1999	let _ = app_handle.emit("sky://output/append", json!({ "channel": ChannelName, "text": Text }));
2000	Ok(Value::Null)
2001}
2002
2003/// Append a line to an output channel (text + newline).
2004async fn handle_output_append_line(app_handle:AppHandle, args:Vec<Value>) -> Result<Value, String> {
2005	use tauri::Emitter;
2006
2007	let ChannelName = args.first().and_then(|V| V.as_str()).unwrap_or("").to_string();
2008	let Text = args.get(1).and_then(|V| V.as_str()).unwrap_or("").to_string();
2009	let Line = format!("{}\n", Text);
2010
2011	let _ = app_handle.emit("sky://output/append", json!({ "channel": ChannelName, "text": Line }));
2012	Ok(Value::Null)
2013}
2014
2015/// Clear an output channel.
2016async fn handle_output_clear(app_handle:AppHandle, args:Vec<Value>) -> Result<Value, String> {
2017	use tauri::Emitter;
2018
2019	let ChannelName = args.first().and_then(|V| V.as_str()).unwrap_or("").to_string();
2020	let _ = app_handle.emit("sky://output/clear", json!({ "channel": ChannelName }));
2021	Ok(Value::Null)
2022}
2023
2024/// Show an output channel panel.
2025async fn handle_output_show(app_handle:AppHandle, args:Vec<Value>) -> Result<Value, String> {
2026	use tauri::Emitter;
2027
2028	let ChannelName = args.first().and_then(|V| V.as_str()).unwrap_or("").to_string();
2029	let _ = app_handle.emit("sky://output/show", json!({ "channel": ChannelName }));
2030	Ok(Value::Null)
2031}
2032
2033// ============================================================================
2034// TextFile Handlers
2035// ============================================================================
2036
2037/// Read a text file from disk.
2038async fn handle_textfile_read(_runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
2039	let Path = args
2040		.first()
2041		.and_then(|V| V.as_str())
2042		.ok_or_else(|| "textFile:read requires path as first argument".to_string())?;
2043
2044	tokio::fs::read_to_string(Path)
2045		.await
2046		.map(Value::String)
2047		.map_err(|Error| format!("textFile:read failed: {}", Error))
2048}
2049
2050/// Write text to a file on disk.
2051async fn handle_textfile_write(_runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
2052	let Path = args
2053		.first()
2054		.and_then(|V| V.as_str())
2055		.ok_or_else(|| "textFile:write requires path as first argument".to_string())?;
2056	let Content = args.get(1).and_then(|V| V.as_str()).unwrap_or("").to_string();
2057
2058	tokio::fs::write(Path, Content.as_bytes())
2059		.await
2060		.map(|()| Value::Null)
2061		.map_err(|Error| format!("textFile:write failed: {}", Error))
2062}
2063
2064/// Save a document - forward save intent to Sky frontend.
2065async fn handle_textfile_save(_runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
2066	// Actual disk write happens via textFile:write; this is a UI-dirty-state hint.
2067	let _Uri = args.first().and_then(|V| V.as_str()).unwrap_or("").to_string();
2068	dev_log!("vfs", "textFile:save uri={:?}", _Uri);
2069	Ok(Value::Null)
2070}
2071
2072/// Register all Wind IPC command handlers
2073pub fn register_wind_ipc_handlers(app_handle:&tauri::AppHandle) -> Result<(), String> {
2074	dev_log!("lifecycle", "registering IPC handlers");
2075
2076	// Note: These handlers are automatically registered when included in the
2077	// Tauri invoke_handler macro in the main binary
2078
2079	Ok(())
2080}
2081
2082// ============================================================================
2083// Command Registry Handlers
2084// ============================================================================
2085
2086/// Execute a command by ID, dispatching to Mountain's CommandExecutor.
2087async fn handle_commands_execute(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
2088	let CommandId = args
2089		.first()
2090		.and_then(|V| V.as_str())
2091		.ok_or_else(|| "commands:execute requires string command_id as first argument".to_string())?
2092		.to_string();
2093
2094	let Argument = args.get(1).cloned().unwrap_or(Value::Null);
2095
2096	dev_log!("ipc", "commands:execute id={}", CommandId);
2097
2098	runtime
2099		.Environment
2100		.ExecuteCommand(CommandId, Argument)
2101		.await
2102		.map_err(|Error| format!("commands:execute failed: {}", Error))
2103}
2104
2105/// Return all registered command IDs from Mountain's CommandRegistry.
2106async fn handle_commands_get_all(runtime:Arc<ApplicationRunTime>) -> Result<Value, String> {
2107	let Commands = runtime
2108		.Environment
2109		.GetAllCommands()
2110		.await
2111		.map_err(|Error| format!("commands:getAll failed: {}", Error))?;
2112
2113	Ok(json!(Commands))
2114}
2115
2116// ============================================================================
2117// Extension Host Handlers
2118// ============================================================================
2119
2120/// Return metadata for all scanned extensions.
2121async fn handle_extensions_get_all(runtime:Arc<ApplicationRunTime>) -> Result<Value, String> {
2122	let Extensions = runtime
2123		.Environment
2124		.GetExtensions()
2125		.await
2126		.map_err(|Error| format!("extensions:getAll failed: {}", Error))?;
2127
2128	dev_log!("extensions", "extensions:getAll returning {} extensions", Extensions.len());
2129	if let Some(First) = Extensions.first() {
2130		dev_log!(
2131			"extensions",
2132			"extensions:getAll sample: {}",
2133			serde_json::to_string(First)
2134				.unwrap_or_default()
2135				.chars()
2136				.take(300)
2137				.collect::<String>()
2138		);
2139	}
2140	Ok(json!(Extensions))
2141}
2142
2143/// Return metadata for a single extension by ID.
2144async fn handle_extensions_get(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
2145	let Id = args
2146		.first()
2147		.and_then(|V| V.as_str())
2148		.ok_or_else(|| "extensions:get requires string id as first argument".to_string())?
2149		.to_string();
2150
2151	let Extension = runtime
2152		.Environment
2153		.GetExtension(Id)
2154		.await
2155		.map_err(|Error| format!("extensions:get failed: {}", Error))?;
2156
2157	Ok(Extension.unwrap_or(Value::Null))
2158}
2159
2160/// Check whether an extension is currently active (scanned and present).
2161async fn handle_extensions_is_active(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
2162	let Id = args
2163		.first()
2164		.and_then(|V| V.as_str())
2165		.ok_or_else(|| "extensions:isActive requires string id as first argument".to_string())?
2166		.to_string();
2167
2168	let Extension = runtime
2169		.Environment
2170		.GetExtension(Id)
2171		.await
2172		.map_err(|Error| format!("extensions:isActive failed: {}", Error))?;
2173
2174	Ok(json!(Extension.is_some()))
2175}
2176
2177// ============================================================================
2178// Storage handlers
2179// ============================================================================
2180
2181/// Delete a persistent storage key.
2182async fn handle_storage_delete(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
2183	use CommonLibrary::Storage::StorageProvider::StorageProvider;
2184
2185	let Key = args
2186		.first()
2187		.and_then(|V| V.as_str())
2188		.ok_or("storage:delete requires key as first argument".to_string())?
2189		.to_string();
2190
2191	runtime
2192		.Environment
2193		.UpdateStorageValue(true, Key, None)
2194		.await
2195		.map_err(|Error| format!("storage:delete failed: {}", Error))?;
2196
2197	Ok(Value::Null)
2198}
2199
2200/// Return all storage keys.
2201async fn handle_storage_keys(runtime:Arc<ApplicationRunTime>) -> Result<Value, String> {
2202	use CommonLibrary::Storage::StorageProvider::StorageProvider;
2203
2204	let Storage = runtime
2205		.Environment
2206		.GetAllStorage(true)
2207		.await
2208		.map_err(|Error| format!("storage:keys failed: {}", Error))?;
2209
2210	let Keys:Vec<String> = Storage.as_object().map(|O| O.keys().cloned().collect()).unwrap_or_default();
2211	Ok(json!(Keys))
2212}
2213
2214// ============================================================================
2215// Notification handlers
2216// ============================================================================
2217
2218/// Show a notification message - emits sky://notification/show for Sky to
2219/// render.
2220async fn handle_notification_show(app_handle:tauri::AppHandle, args:Vec<Value>) -> Result<Value, String> {
2221	use tauri::Emitter;
2222
2223	let Message = args.first().and_then(|V| V.as_str()).unwrap_or("").to_string();
2224	let Severity = args.get(1).and_then(|V| V.as_str()).unwrap_or("info").to_string();
2225	let Actions = args.get(2).cloned().unwrap_or(json!([]));
2226
2227	let Id = format!(
2228		"notification-{}",
2229		std::time::SystemTime::now()
2230			.duration_since(std::time::UNIX_EPOCH)
2231			.map(|D| D.as_millis())
2232			.unwrap_or(0)
2233	);
2234
2235	let _ = app_handle.emit(
2236		"sky://notification/show",
2237		json!({
2238			"id": Id,
2239			"message": Message,
2240			"severity": Severity,
2241			"actions": Actions,
2242		}),
2243	);
2244
2245	Ok(json!(Id))
2246}
2247
2248/// Begin a progress notification - emits sky://notification/progress-begin.
2249async fn handle_notification_show_progress(app_handle:tauri::AppHandle, args:Vec<Value>) -> Result<Value, String> {
2250	use tauri::Emitter;
2251
2252	let Title = args.first().and_then(|V| V.as_str()).unwrap_or("").to_string();
2253	let Cancellable = args.get(1).and_then(|V| V.as_bool()).unwrap_or(false);
2254
2255	let Id = format!(
2256		"progress-{}",
2257		std::time::SystemTime::now()
2258			.duration_since(std::time::UNIX_EPOCH)
2259			.map(|D| D.as_millis())
2260			.unwrap_or(0)
2261	);
2262
2263	let _ = app_handle.emit(
2264		"sky://notification/progress-begin",
2265		json!({
2266			"id": Id,
2267			"title": Title,
2268			"cancellable": Cancellable,
2269		}),
2270	);
2271
2272	Ok(json!(Id))
2273}
2274
2275/// Update an in-progress notification progress bar.
2276async fn handle_notification_update_progress(app_handle:tauri::AppHandle, args:Vec<Value>) -> Result<Value, String> {
2277	use tauri::Emitter;
2278
2279	let Id = args.first().and_then(|V| V.as_str()).unwrap_or("").to_string();
2280	let Increment = args.get(1).and_then(|V| V.as_f64()).unwrap_or(0.0);
2281	let Message = args.get(2).and_then(|V| V.as_str()).unwrap_or("").to_string();
2282
2283	let _ = app_handle.emit(
2284		"sky://notification/progress-update",
2285		json!({
2286			"id": Id,
2287			"increment": Increment,
2288			"message": Message,
2289		}),
2290	);
2291
2292	Ok(Value::Null)
2293}
2294
2295/// End a progress notification.
2296async fn handle_notification_end_progress(app_handle:tauri::AppHandle, args:Vec<Value>) -> Result<Value, String> {
2297	use tauri::Emitter;
2298
2299	let Id = args.first().and_then(|V| V.as_str()).unwrap_or("").to_string();
2300
2301	let _ = app_handle.emit("sky://notification/progress-end", json!({ "id": Id }));
2302
2303	Ok(Value::Null)
2304}
2305
2306// ============================================================================
2307// Progress handlers
2308// ============================================================================
2309
2310/// Begin a window-level or status-bar progress indicator.
2311async fn handle_progress_begin(app_handle:tauri::AppHandle, args:Vec<Value>) -> Result<Value, String> {
2312	use tauri::Emitter;
2313
2314	let Location = args.first().and_then(|V| V.as_str()).unwrap_or("notification").to_string();
2315	let Title = args.get(1).and_then(|V| V.as_str()).unwrap_or("").to_string();
2316	let Cancellable = args.get(2).and_then(|V| V.as_bool()).unwrap_or(false);
2317
2318	let Id = format!(
2319		"progress-{}",
2320		std::time::SystemTime::now()
2321			.duration_since(std::time::UNIX_EPOCH)
2322			.map(|D| D.as_millis())
2323			.unwrap_or(0)
2324	);
2325
2326	let _ = app_handle.emit(
2327		"sky://progress/begin",
2328		json!({
2329			"id": Id,
2330			"location": Location,
2331			"title": Title,
2332			"cancellable": Cancellable,
2333		}),
2334	);
2335
2336	Ok(json!(Id))
2337}
2338
2339/// Report incremental progress on an active indicator.
2340async fn handle_progress_report(app_handle:tauri::AppHandle, args:Vec<Value>) -> Result<Value, String> {
2341	use tauri::Emitter;
2342
2343	let Id = args.first().and_then(|V| V.as_str()).unwrap_or("").to_string();
2344	let Increment = args.get(1).and_then(|V| V.as_f64()).unwrap_or(0.0);
2345	let Message = args.get(2).and_then(|V| V.as_str()).unwrap_or("").to_string();
2346
2347	let _ = app_handle.emit(
2348		"sky://progress/report",
2349		json!({
2350			"id": Id,
2351			"increment": Increment,
2352			"message": Message,
2353		}),
2354	);
2355
2356	Ok(Value::Null)
2357}
2358
2359/// End a progress indicator.
2360async fn handle_progress_end(app_handle:tauri::AppHandle, args:Vec<Value>) -> Result<Value, String> {
2361	use tauri::Emitter;
2362
2363	let Id = args.first().and_then(|V| V.as_str()).unwrap_or("").to_string();
2364
2365	let _ = app_handle.emit("sky://progress/end", json!({ "id": Id }));
2366
2367	Ok(Value::Null)
2368}
2369
2370// ============================================================================
2371// QuickInput handlers
2372// ============================================================================
2373
2374/// Show a quick-pick dialog. Routes through UserInterfaceProvider (blocking
2375/// oneshot).
2376async fn handle_quick_input_show_quick_pick(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
2377	use CommonLibrary::UserInterface::{
2378		DTO::{QuickPickItemDTO::QuickPickItemDTO, QuickPickOptionsDTO::QuickPickOptionsDTO},
2379		UserInterfaceProvider::UserInterfaceProvider,
2380	};
2381
2382	let Items:Vec<QuickPickItemDTO> = args
2383		.first()
2384		.and_then(|V| V.as_array())
2385		.map(|Arr| {
2386			Arr.iter()
2387				.filter_map(|Item| {
2388					let Label = Item.get("label").and_then(|L| L.as_str()).unwrap_or("").to_string();
2389					let Description = Item.get("description").and_then(|D| D.as_str()).map(|S| S.to_string());
2390					let Detail = Item.get("detail").and_then(|D| D.as_str()).map(|S| S.to_string());
2391					let Picked = Item.get("picked").and_then(|P| P.as_bool()).unwrap_or(false);
2392					Some(QuickPickItemDTO { Label, Description, Detail, Picked:Some(Picked), AlwaysShow:Some(false) })
2393				})
2394				.collect()
2395		})
2396		.unwrap_or_default();
2397
2398	let Options = QuickPickOptionsDTO {
2399		PlaceHolder:args
2400			.get(1)
2401			.and_then(|V| V.get("placeholder"))
2402			.and_then(|P| P.as_str())
2403			.map(|S| S.to_string()),
2404		CanPickMany:Some(
2405			args.get(1)
2406				.and_then(|V| V.get("canPickMany"))
2407				.and_then(|B| B.as_bool())
2408				.unwrap_or(false),
2409		),
2410		Title:args
2411			.get(1)
2412			.and_then(|V| V.get("title"))
2413			.and_then(|T| T.as_str())
2414			.map(|S| S.to_string()),
2415		..Default::default()
2416	};
2417
2418	let Result = runtime
2419		.Environment
2420		.ShowQuickPick(Items, Some(Options))
2421		.await
2422		.map_err(|Error| format!("quickInput:showQuickPick failed: {}", Error))?;
2423
2424	match Result {
2425		Some(Labels) => Ok(Labels.into_iter().next().map(|S| json!(S)).unwrap_or(Value::Null)),
2426		None => Ok(Value::Null),
2427	}
2428}
2429
2430/// Show an input box dialog. Routes through UserInterfaceProvider (blocking
2431/// oneshot).
2432async fn handle_quick_input_show_input_box(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
2433	use CommonLibrary::UserInterface::{
2434		DTO::InputBoxOptionsDTO::InputBoxOptionsDTO,
2435		UserInterfaceProvider::UserInterfaceProvider,
2436	};
2437
2438	let Opts = args.first();
2439	let Options = InputBoxOptionsDTO {
2440		Prompt:Opts
2441			.and_then(|V| V.get("prompt"))
2442			.and_then(|P| P.as_str())
2443			.map(|S| S.to_string()),
2444		PlaceHolder:Opts
2445			.and_then(|V| V.get("placeholder"))
2446			.and_then(|P| P.as_str())
2447			.map(|S| S.to_string()),
2448		IsPassword:Some(Opts.and_then(|V| V.get("password")).and_then(|B| B.as_bool()).unwrap_or(false)),
2449		Value:Opts
2450			.and_then(|V| V.get("value"))
2451			.and_then(|V| V.as_str())
2452			.map(|S| S.to_string()),
2453		Title:Opts
2454			.and_then(|V| V.get("title"))
2455			.and_then(|T| T.as_str())
2456			.map(|S| S.to_string()),
2457		IgnoreFocusOut:None,
2458	};
2459
2460	let Result = runtime
2461		.Environment
2462		.ShowInputBox(Some(Options))
2463		.await
2464		.map_err(|Error| format!("quickInput:showInputBox failed: {}", Error))?;
2465
2466	Ok(Result.map(|S| json!(S)).unwrap_or(Value::Null))
2467}
2468
2469// ============================================================================
2470// Workspaces handlers
2471// ============================================================================
2472
2473/// Return the current workspace folders.
2474async fn handle_workspaces_get_folders(runtime:Arc<ApplicationRunTime>) -> Result<Value, String> {
2475	let Workspace = &runtime.Environment.ApplicationState.Workspace;
2476	let Folders = Workspace.GetWorkspaceFolders();
2477
2478	let FolderList:Vec<Value> = Folders
2479		.iter()
2480		.enumerate()
2481		.map(|(Index, Folder)| {
2482			json!({
2483				"uri": Folder.URI.to_string(),
2484				"name": Folder.Name,
2485				"index": Index,
2486			})
2487		})
2488		.collect();
2489
2490	Ok(json!(FolderList))
2491}
2492
2493/// Add a workspace folder.
2494async fn handle_workspaces_add_folder(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
2495	use url::Url;
2496
2497	let UriStr = args
2498		.first()
2499		.and_then(|V| V.as_str())
2500		.ok_or("workspaces:addFolder requires uri as first argument".to_string())?
2501		.to_string();
2502
2503	let Name = args.get(1).and_then(|V| V.as_str()).unwrap_or("").to_string();
2504
2505	let Workspace = &runtime.Environment.ApplicationState.Workspace;
2506	let mut Folders = Workspace.GetWorkspaceFolders();
2507	let Index = Folders.len();
2508	let URI = Url::parse(&UriStr).map_err(|E| format!("workspaces:addFolder invalid URI: {}", E))?;
2509	if let Ok(Folder) = WorkspaceFolderStateDTO::New(URI, Name, Index) {
2510		Folders.push(Folder);
2511		Workspace.SetWorkspaceFolders(Folders);
2512	}
2513
2514	Ok(Value::Null)
2515}
2516
2517/// Remove a workspace folder by URI.
2518async fn handle_workspaces_remove_folder(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
2519	let UriStr = args
2520		.first()
2521		.and_then(|V| V.as_str())
2522		.ok_or("workspaces:removeFolder requires uri as first argument".to_string())?
2523		.to_string();
2524
2525	let Workspace = &runtime.Environment.ApplicationState.Workspace;
2526	let mut Folders = Workspace.GetWorkspaceFolders();
2527	Folders.retain(|F| F.URI.to_string() != UriStr);
2528	for (I, F) in Folders.iter_mut().enumerate() {
2529		F.Index = I;
2530	}
2531	Workspace.SetWorkspaceFolders(Folders);
2532
2533	Ok(Value::Null)
2534}
2535
2536/// Return the workspace name (basename of root folder, or None if untitled).
2537async fn handle_workspaces_get_name(runtime:Arc<ApplicationRunTime>) -> Result<Value, String> {
2538	let Name = runtime
2539		.Environment
2540		.ApplicationState
2541		.Workspace
2542		.GetWorkspaceFolders()
2543		.into_iter()
2544		.next()
2545		.map(|F| F.GetDisplayName());
2546
2547	Ok(Name.map(|N| json!(N)).unwrap_or(Value::Null))
2548}
2549
2550// ============================================================================
2551// Themes handlers
2552// ============================================================================
2553
2554/// Return the active color theme metadata from ConfigurationProvider.
2555async fn handle_themes_get_active(runtime:Arc<ApplicationRunTime>) -> Result<Value, String> {
2556	use CommonLibrary::Configuration::{
2557		ConfigurationProvider::ConfigurationProvider,
2558		DTO::ConfigurationOverridesDTO::ConfigurationOverridesDTO,
2559	};
2560
2561	let ThemeId = runtime
2562		.Environment
2563		.GetConfigurationValue(Some("workbench.colorTheme".to_string()), ConfigurationOverridesDTO::default())
2564		.await
2565		.map_err(|Error| format!("themes:getActive failed: {}", Error))?;
2566
2567	let Id = ThemeId.as_str().unwrap_or("Default Dark Modern").to_string();
2568
2569	// Infer kind from id string
2570	let Kind = if Id.to_lowercase().contains("light") {
2571		"light"
2572	} else if Id.to_lowercase().contains("high contrast light") {
2573		"highContrastLight"
2574	} else if Id.to_lowercase().contains("high contrast") {
2575		"highContrast"
2576	} else {
2577		"dark"
2578	};
2579
2580	Ok(json!({ "id": Id, "label": Id, "kind": Kind }))
2581}
2582
2583/// Return installed theme extensions.
2584async fn handle_themes_list(runtime:Arc<ApplicationRunTime>) -> Result<Value, String> {
2585	// For now return a hardcoded set of built-in themes; extensions contribute
2586	// more.
2587	let Themes = vec![
2588		json!({ "id": "Default Dark Modern", "label": "Default Dark Modern", "kind": "dark" }),
2589		json!({ "id": "Default Light Modern", "label": "Default Light Modern", "kind": "light" }),
2590		json!({ "id": "Default Dark+", "label": "Default Dark+", "kind": "dark" }),
2591		json!({ "id": "Default Light+", "label": "Default Light+", "kind": "light" }),
2592		json!({ "id": "High Contrast", "label": "High Contrast", "kind": "highContrast" }),
2593		json!({ "id": "High Contrast Light", "label": "High Contrast Light", "kind": "highContrastLight" }),
2594	];
2595
2596	Ok(json!(Themes))
2597}
2598
2599/// Set the active color theme by updating ConfigurationProvider.
2600async fn handle_themes_set(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
2601	use CommonLibrary::Configuration::{
2602		ConfigurationProvider::ConfigurationProvider,
2603		DTO::{ConfigurationOverridesDTO::ConfigurationOverridesDTO, ConfigurationTarget::ConfigurationTarget},
2604	};
2605	use tauri::Emitter;
2606
2607	let ThemeId = args
2608		.first()
2609		.and_then(|V| V.as_str())
2610		.ok_or("themes:set requires themeId as first argument".to_string())?
2611		.to_string();
2612
2613	runtime
2614		.Environment
2615		.UpdateConfigurationValue(
2616			"workbench.colorTheme".to_string(),
2617			json!(ThemeId),
2618			ConfigurationTarget::User,
2619			ConfigurationOverridesDTO::default(),
2620			None,
2621		)
2622		.await
2623		.map_err(|Error| format!("themes:set failed: {}", Error))?;
2624
2625	let _ = runtime
2626		.Environment
2627		.ApplicationHandle
2628		.emit("sky://theme/change", json!({ "themeId": ThemeId }));
2629
2630	Ok(Value::Null)
2631}
2632
2633// ============================================================================
2634// Search handlers
2635// ============================================================================
2636
2637/// Search text across all workspace files (line-by-line grep, max 1000
2638/// results).
2639async fn handle_search_find_in_files(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
2640	use std::path::PathBuf;
2641
2642	use globset::GlobBuilder;
2643	use tokio::fs;
2644
2645	let Pattern = args
2646		.first()
2647		.and_then(|V| V.as_str())
2648		.ok_or("search:findInFiles requires pattern".to_string())?
2649		.to_string();
2650	let IsRegex = args.get(1).and_then(|V| V.as_bool()).unwrap_or(false);
2651	let IsCaseSensitive = args.get(2).and_then(|V| V.as_bool()).unwrap_or(false);
2652	let _IsWordMatch = args.get(3).and_then(|V| V.as_bool()).unwrap_or(false);
2653	let IncludeGlob = args.get(4).and_then(|V| V.as_str()).unwrap_or("**").to_string();
2654	let ExcludeGlob = args.get(5).and_then(|V| V.as_str()).unwrap_or("").to_string();
2655	let MaxResults = args.get(6).and_then(|V| V.as_u64()).unwrap_or(1000) as usize;
2656
2657	let WorkspaceFolders = runtime.Environment.ApplicationState.Workspace.GetWorkspaceFolders();
2658
2659	if WorkspaceFolders.is_empty() {
2660		return Ok(json!([]));
2661	}
2662
2663	let RootPath = PathBuf::from(&WorkspaceFolders[0].URI.to_string().replace("file://", ""));
2664
2665	// Build include matcher
2666	let IncludeMatcher = GlobBuilder::new(&IncludeGlob)
2667		.literal_separator(false)
2668		.build()
2669		.map(|G| G.compile_matcher())
2670		.ok();
2671
2672	// Build exclude matcher
2673	let ExcludeMatcher = if !ExcludeGlob.is_empty() {
2674		GlobBuilder::new(&ExcludeGlob)
2675			.literal_separator(false)
2676			.build()
2677			.map(|G| G.compile_matcher())
2678			.ok()
2679	} else {
2680		None
2681	};
2682
2683	let SearchText = Pattern.clone();
2684	let mut Matches = Vec::new();
2685
2686	// Walk directory recursively
2687	let mut Stack = vec![RootPath.clone()];
2688	while let Some(Dir) = Stack.pop() {
2689		let mut Entries = match fs::read_dir(&Dir).await {
2690			Ok(E) => E,
2691			Err(_) => continue,
2692		};
2693
2694		while let Ok(Some(Entry)) = Entries.next_entry().await {
2695			let Path = Entry.path();
2696			let RelPath = Path.strip_prefix(&RootPath).unwrap_or(&Path).to_string_lossy().to_string();
2697
2698			// Skip hidden dirs
2699			if Path.file_name().map(|N| N.to_string_lossy().starts_with('.')).unwrap_or(false) {
2700				continue;
2701			}
2702
2703			if Path.is_dir() {
2704				Stack.push(Path);
2705				continue;
2706			}
2707
2708			// Check include/exclude globs
2709			if let Some(Ref) = &IncludeMatcher {
2710				if !Ref.is_match(&RelPath) {
2711					continue;
2712				}
2713			}
2714			if let Some(Ref) = &ExcludeMatcher {
2715				if Ref.is_match(&RelPath) {
2716					continue;
2717				}
2718			}
2719
2720			// Read file and search line by line
2721			let Content = match fs::read_to_string(&Path).await {
2722				Ok(C) => C,
2723				Err(_) => continue,
2724			};
2725
2726			for (LineIndex, Line) in Content.lines().enumerate() {
2727				let Hit = if IsRegex {
2728					// Simple contains fallback (no regex crate available here)
2729					Line.contains(&SearchText)
2730				} else if IsCaseSensitive {
2731					Line.contains(&SearchText)
2732				} else {
2733					Line.to_lowercase().contains(&SearchText.to_lowercase())
2734				};
2735
2736				if Hit {
2737					let Uri = format!("file://{}", Path.to_string_lossy());
2738					Matches.push(json!({
2739						"uri": Uri,
2740						"lineNumber": LineIndex + 1,
2741						"preview": Line.trim(),
2742					}));
2743
2744					if Matches.len() >= MaxResults {
2745						return Ok(json!(Matches));
2746					}
2747				}
2748			}
2749		}
2750	}
2751
2752	Ok(json!(Matches))
2753}
2754
2755/// Search file paths by glob pattern in workspace.
2756async fn handle_search_find_files(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
2757	use std::path::PathBuf;
2758
2759	use globset::GlobBuilder;
2760	use tokio::fs;
2761
2762	let Pattern = args
2763		.first()
2764		.and_then(|V| V.as_str())
2765		.ok_or("search:findFiles requires pattern".to_string())?
2766		.to_string();
2767	let MaxResults = args.get(1).and_then(|V| V.as_u64()).unwrap_or(500) as usize;
2768
2769	let WorkspaceFolders = runtime.Environment.ApplicationState.Workspace.GetWorkspaceFolders();
2770
2771	if WorkspaceFolders.is_empty() {
2772		return Ok(json!([]));
2773	}
2774
2775	let RootPath = PathBuf::from(&WorkspaceFolders[0].URI.to_string().replace("file://", ""));
2776
2777	let Matcher = GlobBuilder::new(&Pattern)
2778		.literal_separator(false)
2779		.build()
2780		.map(|G| G.compile_matcher())
2781		.map_err(|Error| format!("Invalid glob pattern: {}", Error))?;
2782
2783	let mut Files = Vec::new();
2784	let mut Stack = vec![RootPath.clone()];
2785
2786	while let Some(Dir) = Stack.pop() {
2787		let mut Entries = match fs::read_dir(&Dir).await {
2788			Ok(E) => E,
2789			Err(_) => continue,
2790		};
2791
2792		while let Ok(Some(Entry)) = Entries.next_entry().await {
2793			let Path = Entry.path();
2794
2795			if Path.file_name().map(|N| N.to_string_lossy().starts_with('.')).unwrap_or(false) {
2796				continue;
2797			}
2798
2799			if Path.is_dir() {
2800				Stack.push(Path);
2801				continue;
2802			}
2803
2804			let RelPath = Path.strip_prefix(&RootPath).unwrap_or(&Path).to_string_lossy().to_string();
2805
2806			if Matcher.is_match(&RelPath) {
2807				Files.push(format!("file://{}", Path.to_string_lossy()));
2808
2809				if Files.len() >= MaxResults {
2810					return Ok(json!(Files));
2811				}
2812			}
2813		}
2814	}
2815
2816	Ok(json!(Files))
2817}
2818
2819// ============================================================================
2820// Decorations handlers
2821// ============================================================================
2822
2823/// Return the decoration (badge, tooltip, color) for a single URI.
2824/// Mountain holds decorations in ApplicationState; extensions push them via
2825/// the `decorations:set` IPC channel.
2826async fn handle_decorations_get(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
2827	let Uri = args
2828		.first()
2829		.and_then(|V| V.as_str())
2830		.ok_or("decorations:get requires uri".to_string())?;
2831	let Decoration = runtime.Environment.ApplicationState.Feature.Decorations.GetDecoration(Uri);
2832	Ok(Decoration.unwrap_or(Value::Null))
2833}
2834
2835/// Return decorations for multiple URIs in a single round-trip.
2836async fn handle_decorations_get_many(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
2837	let Uris:Vec<String> = args
2838		.first()
2839		.and_then(|V| V.as_array())
2840		.map(|Arr| Arr.iter().filter_map(|U| U.as_str().map(str::to_owned)).collect())
2841		.unwrap_or_default();
2842
2843	let mut Result = serde_json::Map::new();
2844	for Uri in &Uris {
2845		if let Some(Decoration) = runtime.Environment.ApplicationState.Feature.Decorations.GetDecoration(Uri) {
2846			Result.insert(Uri.clone(), Decoration);
2847		}
2848	}
2849	Ok(Value::Object(Result))
2850}
2851
2852/// Register or override the decoration for a URI.
2853async fn handle_decorations_set(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
2854	let Uri = args
2855		.first()
2856		.and_then(|V| V.as_str())
2857		.ok_or("decorations:set requires uri".to_string())?;
2858	let Decoration = args.get(1).cloned().unwrap_or(Value::Null);
2859	runtime
2860		.Environment
2861		.ApplicationState
2862		.Feature
2863		.Decorations
2864		.SetDecoration(Uri, Decoration);
2865	Ok(Value::Null)
2866}
2867
2868/// Remove the decoration for a URI.
2869async fn handle_decorations_clear(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
2870	let Uri = args
2871		.first()
2872		.and_then(|V| V.as_str())
2873		.ok_or("decorations:clear requires uri".to_string())?;
2874	runtime.Environment.ApplicationState.Feature.Decorations.ClearDecoration(Uri);
2875	Ok(Value::Null)
2876}
2877
2878// ============================================================================
2879// WorkingCopy handlers
2880// ============================================================================
2881
2882/// Check whether a URI has unsaved changes.
2883async fn handle_working_copy_is_dirty(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
2884	let Uri = args
2885		.first()
2886		.and_then(|V| V.as_str())
2887		.ok_or("workingCopy:isDirty requires uri".to_string())?;
2888	let IsDirty = runtime.Environment.ApplicationState.Feature.WorkingCopy.IsDirty(Uri);
2889	Ok(json!(IsDirty))
2890}
2891
2892/// Mark a URI as dirty (unsaved) or clean.
2893async fn handle_working_copy_set_dirty(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
2894	let Uri = args
2895		.first()
2896		.and_then(|V| V.as_str())
2897		.ok_or("workingCopy:setDirty requires uri".to_string())?;
2898	let Dirty = args.get(1).and_then(|V| V.as_bool()).unwrap_or(true);
2899	runtime.Environment.ApplicationState.Feature.WorkingCopy.SetDirty(Uri, Dirty);
2900	Ok(Value::Null)
2901}
2902
2903/// Return all URIs that currently have unsaved changes.
2904async fn handle_working_copy_get_all_dirty(runtime:Arc<ApplicationRunTime>) -> Result<Value, String> {
2905	let Dirty = runtime.Environment.ApplicationState.Feature.WorkingCopy.GetAllDirty();
2906	Ok(json!(Dirty))
2907}
2908
2909/// Return the count of resources with unsaved changes.
2910async fn handle_working_copy_get_dirty_count(runtime:Arc<ApplicationRunTime>) -> Result<Value, String> {
2911	let Count = runtime.Environment.ApplicationState.Feature.WorkingCopy.GetDirtyCount();
2912	Ok(json!(Count))
2913}
2914
2915// ============================================================================
2916// Keybinding handlers
2917// ============================================================================
2918
2919/// Register a dynamic keybinding in Mountain's keybinding registry.
2920async fn handle_keybinding_add(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
2921	let CommandId = args
2922		.first()
2923		.and_then(|V| V.as_str())
2924		.ok_or("keybinding:add requires commandId".to_string())?
2925		.to_owned();
2926	let KeyExpression = args
2927		.get(1)
2928		.and_then(|V| V.as_str())
2929		.ok_or("keybinding:add requires keybinding".to_string())?
2930		.to_owned();
2931	let When = args.get(2).and_then(|V| V.as_str()).map(str::to_owned);
2932	runtime
2933		.Environment
2934		.ApplicationState
2935		.Feature
2936		.Keybindings
2937		.AddKeybinding(CommandId, KeyExpression, When);
2938	Ok(Value::Null)
2939}
2940
2941/// Remove all dynamic keybindings for a command.
2942async fn handle_keybinding_remove(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
2943	let CommandId = args
2944		.first()
2945		.and_then(|V| V.as_str())
2946		.ok_or("keybinding:remove requires commandId".to_string())?;
2947	runtime
2948		.Environment
2949		.ApplicationState
2950		.Feature
2951		.Keybindings
2952		.RemoveKeybinding(CommandId);
2953	Ok(Value::Null)
2954}
2955
2956/// Look up the keybinding string for a command.
2957async fn handle_keybinding_lookup(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
2958	let CommandId = args
2959		.first()
2960		.and_then(|V| V.as_str())
2961		.ok_or("keybinding:lookup requires commandId".to_string())?;
2962	let Binding = runtime
2963		.Environment
2964		.ApplicationState
2965		.Feature
2966		.Keybindings
2967		.LookupKeybinding(CommandId);
2968	Ok(Binding.map(|B| json!(B)).unwrap_or(Value::Null))
2969}
2970
2971/// Return all registered dynamic keybindings.
2972async fn handle_keybinding_get_all(runtime:Arc<ApplicationRunTime>) -> Result<Value, String> {
2973	let All = runtime.Environment.ApplicationState.Feature.Keybindings.GetAllKeybindings();
2974	Ok(json!(All))
2975}
2976
2977// ============================================================================
2978// Lifecycle handlers
2979// ============================================================================
2980
2981/// Return the current application lifecycle phase (1–4).
2982async fn handle_lifecycle_get_phase(runtime:Arc<ApplicationRunTime>) -> Result<Value, String> {
2983	let Phase = runtime.Environment.ApplicationState.Feature.Lifecycle.GetPhase();
2984	Ok(json!(Phase))
2985}
2986
2987/// Wait (poll) until the application reaches at least the requested phase.
2988/// Returns immediately if the phase has already been reached.
2989async fn handle_lifecycle_when_phase(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
2990	let RequestedPhase = args.first().and_then(|V| V.as_u64()).unwrap_or(1) as u8;
2991	let CurrentPhase = runtime.Environment.ApplicationState.Feature.Lifecycle.GetPhase();
2992	if CurrentPhase >= RequestedPhase {
2993		return Ok(Value::Null);
2994	}
2995	// Simple poll with short sleep - production should use a channel/notify
2996	let mut Retries = 0u8;
2997	loop {
2998		tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
2999		let Phase = runtime.Environment.ApplicationState.Feature.Lifecycle.GetPhase();
3000		if Phase >= RequestedPhase || Retries >= 50 {
3001			break;
3002		}
3003		Retries += 1;
3004	}
3005	Ok(Value::Null)
3006}
3007
3008/// Initiate a graceful application shutdown via Tauri.
3009async fn handle_lifecycle_request_shutdown(app_handle:AppHandle) -> Result<Value, String> {
3010	app_handle.exit(0);
3011	Ok(Value::Null)
3012}
3013
3014// ============================================================================
3015// Navigation History Handlers
3016// ============================================================================
3017
3018/// Navigate backward in the editor history stack.
3019async fn handle_history_go_back(runtime:Arc<ApplicationRunTime>) -> Result<Value, String> {
3020	let Uri = runtime.Environment.ApplicationState.Feature.NavigationHistory.GoBack();
3021	Ok(Uri.map(Value::String).unwrap_or(Value::Null))
3022}
3023
3024/// Navigate forward in the editor history stack.
3025async fn handle_history_go_forward(runtime:Arc<ApplicationRunTime>) -> Result<Value, String> {
3026	let Uri = runtime.Environment.ApplicationState.Feature.NavigationHistory.GoForward();
3027	Ok(Uri.map(Value::String).unwrap_or(Value::Null))
3028}
3029
3030/// Return whether backward navigation is available.
3031async fn handle_history_can_go_back(runtime:Arc<ApplicationRunTime>) -> Result<Value, String> {
3032	Ok(Value::Bool(
3033		runtime.Environment.ApplicationState.Feature.NavigationHistory.CanGoBack(),
3034	))
3035}
3036
3037/// Return whether forward navigation is available.
3038async fn handle_history_can_go_forward(runtime:Arc<ApplicationRunTime>) -> Result<Value, String> {
3039	Ok(Value::Bool(
3040		runtime.Environment.ApplicationState.Feature.NavigationHistory.CanGoForward(),
3041	))
3042}
3043
3044/// Push a URI onto the navigation history stack.
3045async fn handle_history_push(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
3046	let Uri = args
3047		.first()
3048		.and_then(|V| V.as_str())
3049		.ok_or("history:push requires uri".to_string())?
3050		.to_owned();
3051
3052	runtime.Environment.ApplicationState.Feature.NavigationHistory.Push(Uri);
3053	Ok(Value::Null)
3054}
3055
3056/// Clear the entire navigation history stack.
3057async fn handle_history_clear(runtime:Arc<ApplicationRunTime>) -> Result<Value, String> {
3058	runtime.Environment.ApplicationState.Feature.NavigationHistory.Clear();
3059	Ok(Value::Null)
3060}
3061
3062/// Return the full navigation history stack as an array of URI strings.
3063async fn handle_history_get_stack(runtime:Arc<ApplicationRunTime>) -> Result<Value, String> {
3064	let Stack = runtime.Environment.ApplicationState.Feature.NavigationHistory.GetStack();
3065	Ok(Value::Array(Stack.into_iter().map(Value::String).collect()))
3066}
3067
3068// ============================================================================
3069// Label Handlers
3070// ============================================================================
3071
3072/// Resolve a human-readable display label for a URI.
3073///
3074/// Args: [uri: string, relative: bool]
3075/// Returns: string label
3076async fn handle_label_get_uri(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
3077	let Uri = args
3078		.first()
3079		.and_then(|V| V.as_str())
3080		.ok_or("label:getUri requires uri".to_string())?
3081		.to_owned();
3082
3083	let Relative = args.get(1).and_then(|V| V.as_bool()).unwrap_or(false);
3084
3085	if !Relative {
3086		// Absolute: strip file:// scheme if present, return raw path
3087		let Label = if Uri.starts_with("file://") {
3088			Uri.trim_start_matches("file://").to_owned()
3089		} else {
3090			Uri.clone()
3091		};
3092		return Ok(Value::String(Label));
3093	}
3094
3095	// Relative: make path relative to workspace root if possible
3096	let WorkspaceRoot = runtime
3097		.Environment
3098		.ApplicationState
3099		.Workspace
3100		.GetWorkspaceFolders()
3101		.into_iter()
3102		.next()
3103		.map(|F| F.URI.to_string())
3104		.unwrap_or_default();
3105
3106	let RawPath = if Uri.starts_with("file://") {
3107		Uri.trim_start_matches("file://").to_owned()
3108	} else {
3109		Uri.clone()
3110	};
3111
3112	let RootPath = if WorkspaceRoot.starts_with("file://") {
3113		WorkspaceRoot.trim_start_matches("file://").to_owned()
3114	} else {
3115		WorkspaceRoot
3116	};
3117
3118	let Label = if !RootPath.is_empty() && RawPath.starts_with(&RootPath) {
3119		RawPath[RootPath.len()..].trim_start_matches('/').to_owned()
3120	} else {
3121		RawPath
3122	};
3123
3124	Ok(Value::String(Label))
3125}
3126
3127/// Return the display label for the current workspace root folder.
3128async fn handle_label_get_workspace(runtime:Arc<ApplicationRunTime>) -> Result<Value, String> {
3129	let Label = runtime
3130		.Environment
3131		.ApplicationState
3132		.Workspace
3133		.GetWorkspaceFolders()
3134		.into_iter()
3135		.next()
3136		.map(|F| {
3137			if !F.Name.is_empty() {
3138				F.Name
3139			} else {
3140				F.URI
3141					.path_segments()
3142					.and_then(|mut S| S.next_back())
3143					.map(|S| S.to_owned())
3144					.unwrap_or_else(|| F.URI.to_string())
3145			}
3146		})
3147		.unwrap_or_default();
3148
3149	Ok(Value::String(Label))
3150}
3151
3152/// Return only the basename (filename + extension) of a URI.
3153async fn handle_label_get_base(args:Vec<Value>) -> Result<Value, String> {
3154	let Uri = args
3155		.first()
3156		.and_then(|V| V.as_str())
3157		.ok_or("label:getBase requires uri".to_string())?;
3158
3159	let Base = Uri.split('/').next_back().unwrap_or(Uri);
3160	Ok(Value::String(Base.to_owned()))
3161}
3162
3163// ============================================================================
3164// Model (Text Model Registry) Handlers
3165// ============================================================================
3166
3167/// Open a text model: read content from disk and register in DocumentState.
3168/// Returns { uri, content, version, languageId }.
3169async fn handle_model_open(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
3170	let Uri = args
3171		.first()
3172		.and_then(|V| V.as_str())
3173		.ok_or("model:open requires uri".to_string())?
3174		.to_owned();
3175
3176	// Derive file path from URI
3177	let FilePath = if Uri.starts_with("file://") {
3178		Uri.trim_start_matches("file://").to_owned()
3179	} else {
3180		Uri.clone()
3181	};
3182
3183	// Read file content from disk
3184	let Content = tokio::fs::read_to_string(&FilePath).await.unwrap_or_default();
3185
3186	// Detect language from extension
3187	let LanguageId = std::path::Path::new(&FilePath)
3188		.extension()
3189		.and_then(|E| E.to_str())
3190		.map(|Ext| {
3191			match Ext {
3192				"rs" => "rust",
3193				"ts" | "tsx" => "typescript",
3194				"js" | "jsx" | "mjs" | "cjs" => "javascript",
3195				"json" | "jsonc" => "json",
3196				"toml" => "toml",
3197				"yaml" | "yml" => "yaml",
3198				"md" => "markdown",
3199				"html" | "htm" => "html",
3200				"css" | "scss" | "less" => "css",
3201				"sh" | "bash" | "zsh" => "shellscript",
3202				"py" => "python",
3203				"go" => "go",
3204				"c" | "h" => "c",
3205				"cpp" | "cc" | "cxx" | "hpp" => "cpp",
3206				_ => "plaintext",
3207			}
3208		})
3209		.unwrap_or("plaintext")
3210		.to_owned();
3211
3212	// Determine next version (1 if new, increment if exists)
3213	let Version = runtime
3214		.Environment
3215		.ApplicationState
3216		.Feature
3217		.Documents
3218		.Get(&Uri)
3219		.map(|D| D.Version + 1)
3220		.unwrap_or(1);
3221
3222	// Register in document state
3223	{
3224		use crate::ApplicationState::DTO::DocumentStateDTO::DocumentStateDTO;
3225
3226		if let Ok(ParsedUri) = url::Url::parse(&Uri) {
3227			let Lines:Vec<String> = Content.lines().map(|L| L.to_owned()).collect();
3228			let Eol = if Content.contains("\r\n") { "\r\n" } else { "\n" }.to_owned();
3229
3230			let Document = DocumentStateDTO {
3231				URI:ParsedUri,
3232				LanguageIdentifier:LanguageId.clone(),
3233				Version,
3234				Lines,
3235				EOL:Eol,
3236				IsDirty:false,
3237				Encoding:"utf-8".to_owned(),
3238				VersionIdentifier:Version,
3239			};
3240
3241			runtime
3242				.Environment
3243				.ApplicationState
3244				.Feature
3245				.Documents
3246				.AddOrUpdate(Uri.clone(), Document);
3247		}
3248	}
3249
3250	Ok(json!({
3251		"uri": Uri,
3252		"content": Content,
3253		"version": Version,
3254		"languageId": LanguageId,
3255	}))
3256}
3257
3258/// Close a text model and remove it from DocumentState.
3259async fn handle_model_close(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
3260	let Uri = args
3261		.first()
3262		.and_then(|V| V.as_str())
3263		.ok_or("model:close requires uri".to_string())?;
3264
3265	runtime.Environment.ApplicationState.Feature.Documents.Remove(Uri);
3266	Ok(Value::Null)
3267}
3268
3269/// Get the current snapshot of an open text model, or null if not open.
3270async fn handle_model_get(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
3271	let Uri = args
3272		.first()
3273		.and_then(|V| V.as_str())
3274		.ok_or("model:get requires uri".to_string())?;
3275
3276	match runtime.Environment.ApplicationState.Feature.Documents.Get(Uri) {
3277		None => Ok(Value::Null),
3278		Some(Document) => {
3279			Ok(json!({
3280				"uri": Uri,
3281				"content": Document.Lines.join(&Document.EOL),
3282				"version": Document.Version,
3283				"languageId": Document.LanguageIdentifier,
3284			}))
3285		},
3286	}
3287}
3288
3289/// Return all currently open text models.
3290async fn handle_model_get_all(runtime:Arc<ApplicationRunTime>) -> Result<Value, String> {
3291	let All = runtime
3292		.Environment
3293		.ApplicationState
3294		.Feature
3295		.Documents
3296		.GetAll()
3297		.into_iter()
3298		.map(|(Uri, Document)| {
3299			json!({
3300				"uri": Uri,
3301				"content": Document.Lines.join(&Document.EOL),
3302				"version": Document.Version,
3303				"languageId": Document.LanguageIdentifier,
3304			})
3305		})
3306		.collect::<Vec<_>>();
3307
3308	Ok(Value::Array(All))
3309}
3310
3311/// Update the content of an open text model, incrementing its version.
3312async fn handle_model_update_content(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
3313	let Uri = args
3314		.first()
3315		.and_then(|V| V.as_str())
3316		.ok_or("model:updateContent requires uri".to_string())?
3317		.to_owned();
3318
3319	let NewContent = args
3320		.get(1)
3321		.and_then(|V| V.as_str())
3322		.ok_or("model:updateContent requires content".to_string())?
3323		.to_owned();
3324
3325	let (NewVersion, LanguageId) = match runtime.Environment.ApplicationState.Feature.Documents.Get(&Uri) {
3326		None => return Err(format!("model:updateContent - model not open: {}", Uri)),
3327		Some(mut Document) => {
3328			Document.Version += 1;
3329			Document.Lines = NewContent.lines().map(|L| L.to_owned()).collect();
3330			Document.IsDirty = true;
3331			let Version = Document.Version;
3332			let LangId = Document.LanguageIdentifier.clone();
3333			runtime
3334				.Environment
3335				.ApplicationState
3336				.Feature
3337				.Documents
3338				.AddOrUpdate(Uri.clone(), Document);
3339			(Version, LangId)
3340		},
3341	};
3342
3343	Ok(json!({
3344		"uri": Uri,
3345		"content": NewContent,
3346		"version": NewVersion,
3347		"languageId": LanguageId,
3348	}))
3349}
3350
3351// =============================================================================
3352// Native file system handlers (use extract_path_from_arg for URI
3353// deserialization)
3354// =============================================================================
3355
3356/// Read file with URI arg support (VS Code sends { scheme, path } objects)
3357/// Returns { buffer: number[] } where buffer is the raw byte content.
3358/// VS Code's DiskFileSystemProviderClient wraps this with VSBuffer.wrap().
3359async fn handle_file_read_native(args:Vec<Value>) -> Result<Value, String> {
3360	let Path = extract_path_from_arg(args.get(0).ok_or("Missing file path")?)?;
3361
3362	dev_log!("vfs", "readFile: {}", Path);
3363
3364	// Read as raw bytes (not string) to preserve binary content
3365	let Bytes = tokio::fs::read(&Path)
3366		.await
3367		.map_err(|E| format!("Failed to read file: {} (path: {})", E, Path))?;
3368
3369	dev_log!("vfs", "readFile OK: {} ({} bytes)", Path, Bytes.len());
3370
3371	// Return as { buffer: [byte, byte, ...] } - VS Code reconstructs as VSBuffer
3372	// The buffer field must be an array of u8 values for proper deserialization
3373	let ByteArray:Vec<Value> = Bytes.iter().map(|B| json!(*B)).collect();
3374	Ok(json!({ "buffer": ByteArray }))
3375}
3376
3377/// Write file with URI arg support
3378async fn handle_file_write_native(args:Vec<Value>) -> Result<Value, String> {
3379	let Path = extract_path_from_arg(args.get(0).ok_or("Missing file path")?)?;
3380
3381	// args[1] is VSBuffer (content), args[2] is options
3382	let Content = args.get(1).ok_or("Missing file content")?;
3383
3384	let Bytes = if let Some(S) = Content.as_str() {
3385		S.as_bytes().to_vec()
3386	} else if let Some(Obj) = Content.as_object() {
3387		// VSBuffer wraps { buffer: Uint8Array } - extract bytes
3388		if let Some(Buf) = Obj.get("buffer") {
3389			if let Some(Arr) = Buf.as_array() {
3390				Arr.iter().filter_map(|V| V.as_u64().map(|N| N as u8)).collect()
3391			} else if let Some(S) = Buf.as_str() {
3392				S.as_bytes().to_vec()
3393			} else {
3394				return Err("Unsupported buffer format".to_string());
3395			}
3396		} else {
3397			serde_json::to_string(Content).unwrap_or_default().into_bytes()
3398		}
3399	} else {
3400		return Err("File content must be a string or VSBuffer".to_string());
3401	};
3402
3403	// Ensure parent directory exists
3404	if let Some(Parent) = std::path::Path::new(&Path).parent() {
3405		tokio::fs::create_dir_all(Parent).await.ok();
3406	}
3407
3408	tokio::fs::write(&Path, &Bytes)
3409		.await
3410		.map_err(|E| format!("Failed to write file: {} (path: {})", E, Path))?;
3411
3412	Ok(Value::Null)
3413}
3414
3415/// Rename/move file with URI arg support
3416async fn handle_file_rename_native(args:Vec<Value>) -> Result<Value, String> {
3417	let Source = extract_path_from_arg(args.get(0).ok_or("Missing source path")?)?;
3418	let Target = extract_path_from_arg(args.get(1).ok_or("Missing target path")?)?;
3419
3420	tokio::fs::rename(&Source, &Target)
3421		.await
3422		.map_err(|E| format!("Failed to rename: {} -> {} ({})", Source, Target, E))?;
3423
3424	Ok(Value::Null)
3425}
3426
3427/// Resolve real path (follow symlinks)
3428async fn handle_file_realpath(args:Vec<Value>) -> Result<Value, String> {
3429	let Path = extract_path_from_arg(args.get(0).ok_or("Missing path")?)?;
3430
3431	let Canonical = tokio::fs::canonicalize(&Path)
3432		.await
3433		.map_err(|E| format!("Failed to realpath: {} ({})", Path, E))?;
3434
3435	Ok(json!({
3436		"scheme": "file",
3437		"path": Canonical.to_string_lossy(),
3438		"authority": ""
3439	}))
3440}
3441
3442/// Clone file (copy with metadata)
3443async fn handle_file_clone_native(args:Vec<Value>) -> Result<Value, String> {
3444	let Source = extract_path_from_arg(args.get(0).ok_or("Missing source path")?)?;
3445	let Target = extract_path_from_arg(args.get(1).ok_or("Missing target path")?)?;
3446
3447	tokio::fs::copy(&Source, &Target)
3448		.await
3449		.map_err(|E| format!("Failed to clone: {} -> {} ({})", Source, Target, E))?;
3450
3451	Ok(Value::Null)
3452}
3453
3454// =============================================================================
3455// Native host handlers
3456// =============================================================================
3457
3458/// Pick folder using Tauri dialog plugin and reload webview with folder param.
3459/// In Electron, pickFolderAndOpen causes the main process to reload the window
3460/// with the new workspace. We replicate this by navigating the webview to the
3461/// same origin with `?folder=<path>`, which ResolveConfiguration reads.
3462async fn handle_native_pick_folder(app_handle:AppHandle, _args:Vec<Value>) -> Result<Value, String> {
3463	use tauri_plugin_dialog::DialogExt;
3464	use tauri::WebviewWindow;
3465
3466	dev_log!("folder", "pickFolderAndOpen requested");
3467
3468	dev_log!("folder", "pickFolderAndOpen requested");
3469
3470	let Handle = app_handle.clone();
3471	tokio::task::spawn_blocking(move || {
3472		let FolderPath = Handle.dialog().file().blocking_pick_folder();
3473
3474		if let Some(Path) = FolderPath {
3475			let PathStr = Path.to_string();
3476			dev_log!("folder", "picked: {}", PathStr);
3477
3478			// Navigate the webview to reload with the folder as workspace.
3479			// This mirrors Electron's behaviour of reloading the renderer.
3480			if let Some(Window) = Handle.get_webview_window("main") {
3481				if let Ok(CurrentUrl) = Window.url() {
3482					let Origin = CurrentUrl.origin().unicode_serialization();
3483					let EncodedPath = url::form_urlencoded::Serializer::new(String::new())
3484						.append_pair("folder", &PathStr)
3485						.finish();
3486					let NewUrl = format!("{}/?{}", Origin, EncodedPath);
3487					dev_log!("folder", "navigating: {}", NewUrl);
3488					let _ = Window.navigate(NewUrl.parse().unwrap());
3489				}
3490			}
3491		}
3492	});
3493
3494	Ok(Value::Null)
3495}
3496
3497/// Show open dialog with file/folder picker
3498async fn handle_native_show_open_dialog(app_handle:AppHandle, args:Vec<Value>) -> Result<Value, String> {
3499	dev_log!("folder", "showOpenDialog: {:?}", args);
3500	// Return canceled for now - real dialog integration needs tauri_plugin_dialog
3501	Ok(json!({ "canceled": true, "filePaths": [] }))
3502}
3503
3504/// Get OS properties - cross-platform (macOS, Windows, Linux)
3505async fn handle_native_os_properties() -> Result<Value, String> {
3506	use sysinfo::System;
3507
3508	let OsType = match std::env::consts::OS {
3509		"macos" => "Darwin",
3510		"windows" => "Windows_NT",
3511		"linux" => "Linux",
3512		_ => std::env::consts::OS,
3513	};
3514
3515	// Get OS release version
3516	let Release = {
3517		#[cfg(target_os = "macos")]
3518		{
3519			std::process::Command::new("sw_vers")
3520				.arg("-productVersion")
3521				.output()
3522				.ok()
3523				.map(|O| String::from_utf8_lossy(&O.stdout).trim().to_string())
3524				.unwrap_or_else(|| "14.0".to_string())
3525		}
3526		#[cfg(target_os = "windows")]
3527		{
3528			// Windows 10/11 version from registry or ver command
3529			std::process::Command::new("cmd")
3530				.args(["/c", "ver"])
3531				.output()
3532				.ok()
3533				.map(|O| {
3534					let Output = String::from_utf8_lossy(&O.stdout);
3535					// Extract version number from "Microsoft Windows [Version 10.0.22631.4890]"
3536					Output
3537						.split('[')
3538						.nth(1)
3539						.and_then(|S| S.split(']').next())
3540						.and_then(|S| S.strip_prefix("Version "))
3541						.unwrap_or("10.0.0")
3542						.to_string()
3543				})
3544				.unwrap_or_else(|| "10.0.0".to_string())
3545		}
3546		#[cfg(target_os = "linux")]
3547		{
3548			// Linux kernel version from uname -r
3549			std::process::Command::new("uname")
3550				.arg("-r")
3551				.output()
3552				.ok()
3553				.map(|O| String::from_utf8_lossy(&O.stdout).trim().to_string())
3554				.unwrap_or_else(|| "6.1.0".to_string())
3555		}
3556		#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
3557		{
3558			"0.0.0".to_string()
3559		}
3560	};
3561
3562	// CPU info via sysinfo
3563	let mut Sys = System::new();
3564	Sys.refresh_cpu_all();
3565	let Cpus:Vec<Value> = Sys
3566		.cpus()
3567		.iter()
3568		.map(|Cpu| {
3569			json!({
3570				"model": Cpu.brand(),
3571				"speed": Cpu.frequency()
3572			})
3573		})
3574		.collect();
3575
3576	Ok(json!({
3577		"type": OsType,
3578		"release": Release,
3579		"arch": std::env::consts::ARCH,
3580		"platform": std::env::consts::OS,
3581		"cpus": Cpus
3582	}))
3583}
3584
3585/// Get OS statistics - cross-platform memory/load stats
3586async fn handle_native_os_statistics() -> Result<Value, String> {
3587	use sysinfo::System;
3588
3589	let mut Sys = System::new();
3590	Sys.refresh_memory();
3591
3592	let TotalMem = Sys.total_memory();
3593	let FreeMem = Sys.available_memory();
3594
3595	// Load average: available on Unix, not on Windows
3596	let LoadAvg = {
3597		#[cfg(unix)]
3598		{
3599			let Load = System::load_average();
3600			vec![Load.one, Load.five, Load.fifteen]
3601		}
3602		#[cfg(not(unix))]
3603		{
3604			vec![0.0, 0.0, 0.0]
3605		}
3606	};
3607
3608	Ok(json!({
3609		"totalmem": TotalMem,
3610		"freemem": FreeMem,
3611		"loadavg": LoadAvg
3612	}))
3613}
3614
3615/// Check if window is fullscreen
3616async fn handle_native_is_fullscreen(app_handle:AppHandle) -> Result<Value, String> {
3617	use tauri::Manager;
3618	let Window = app_handle.get_webview_window("main");
3619	if let Some(W) = Window {
3620		Ok(json!(W.is_fullscreen().unwrap_or(false)))
3621	} else {
3622		Ok(json!(false))
3623	}
3624}
3625
3626/// Check if window is maximized
3627async fn handle_native_is_maximized(app_handle:AppHandle) -> Result<Value, String> {
3628	use tauri::Manager;
3629	let Window = app_handle.get_webview_window("main");
3630	if let Some(W) = Window {
3631		Ok(json!(W.is_maximized().unwrap_or(false)))
3632	} else {
3633		Ok(json!(false))
3634	}
3635}
3636
3637/// Find a free port starting from a given port
3638async fn handle_native_find_free_port(args:Vec<Value>) -> Result<Value, String> {
3639	let StartPort = args.get(0).and_then(|V| V.as_u64()).unwrap_or(9000) as u16;
3640
3641	for Port in StartPort..StartPort + 100 {
3642		if std::net::TcpListener::bind(("127.0.0.1", Port)).is_ok() {
3643			return Ok(json!(Port));
3644		}
3645	}
3646	Ok(json!(0))
3647}
3648
3649// =============================================================================
3650// Local PTY handlers
3651// =============================================================================
3652
3653/// Detect available terminal profiles - cross-platform
3654async fn handle_local_pty_get_profiles() -> Result<Value, String> {
3655	let mut Profiles = Vec::new();
3656
3657	#[cfg(unix)]
3658	{
3659		let DefaultShell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string());
3660
3661		// Common Unix shells - macOS, Ubuntu, RHEL, Fedora, Arch, etc.
3662		let UnixShells = [
3663			"/bin/zsh",
3664			"/bin/bash",
3665			"/bin/sh",
3666			"/usr/bin/zsh",
3667			"/usr/bin/bash",
3668			"/usr/bin/fish",
3669			"/usr/local/bin/fish",
3670			"/usr/local/bin/zsh",
3671			"/usr/local/bin/bash",
3672			"/bin/dash",     // Ubuntu/Debian default /bin/sh symlink target
3673			"/usr/bin/ksh",  // KornShell (RHEL, Solaris)
3674			"/usr/bin/tcsh", // C Shell variant
3675			"/bin/csh",      // C Shell
3676			"/usr/bin/pwsh", // PowerShell on Linux/macOS
3677			"/usr/local/bin/pwsh",
3678		];
3679
3680		for Shell in &UnixShells {
3681			if std::path::Path::new(Shell).exists() {
3682				let Name = std::path::Path::new(Shell)
3683					.file_name()
3684					.and_then(|N| N.to_str())
3685					.unwrap_or("shell");
3686
3687				Profiles.push(json!({
3688					"profileName": Name,
3689					"path": Shell,
3690					"isDefault": *Shell == DefaultShell.as_str(),
3691					"args": [],
3692					"env": {},
3693					"icon": "terminal"
3694				}));
3695			}
3696		}
3697
3698		// Also check /etc/shells for additional entries
3699		if let Ok(ShellsFile) = std::fs::read_to_string("/etc/shells") {
3700			for Line in ShellsFile.lines() {
3701				let Trimmed = Line.trim();
3702				if Trimmed.starts_with('/') && !Trimmed.starts_with('#') {
3703					let AlreadyAdded = Profiles.iter().any(|P| P.get("path").and_then(|V| V.as_str()) == Some(Trimmed));
3704					if !AlreadyAdded && std::path::Path::new(Trimmed).exists() {
3705						let Name = std::path::Path::new(Trimmed)
3706							.file_name()
3707							.and_then(|N| N.to_str())
3708							.unwrap_or("shell");
3709
3710						Profiles.push(json!({
3711							"profileName": Name,
3712							"path": Trimmed,
3713							"isDefault": Trimmed == DefaultShell.as_str(),
3714							"args": [],
3715							"env": {},
3716							"icon": "terminal"
3717						}));
3718					}
3719				}
3720			}
3721		}
3722	}
3723
3724	#[cfg(target_os = "windows")]
3725	{
3726		// Windows terminal profiles
3727		let SystemRoot = std::env::var("SystemRoot").unwrap_or_else(|_| "C:\\Windows".to_string());
3728		let ProgramFiles = std::env::var("ProgramFiles").unwrap_or_else(|_| "C:\\Program Files".to_string());
3729		let LocalAppData =
3730			std::env::var("LOCALAPPDATA").unwrap_or_else(|_| "C:\\Users\\User\\AppData\\Local".to_string());
3731
3732		let WindowsShells:Vec<(&str, String, Vec<&str>)> = vec![
3733			(
3734				"PowerShell",
3735				format!("{}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", SystemRoot),
3736				vec!["-NoLogo"],
3737			),
3738			(
3739				"PowerShell 7",
3740				format!("{}\\PowerShell\\7\\pwsh.exe", ProgramFiles),
3741				vec!["-NoLogo"],
3742			),
3743			("Command Prompt", format!("{}\\System32\\cmd.exe", SystemRoot), vec![]),
3744			(
3745				"Git Bash",
3746				format!("{}\\Git\\bin\\bash.exe", ProgramFiles),
3747				vec!["--login", "-i"],
3748			),
3749			(
3750				"Git Bash (User)",
3751				format!("{}\\Programs\\Git\\bin\\bash.exe", LocalAppData),
3752				vec!["--login", "-i"],
3753			),
3754			("WSL", format!("{}\\System32\\wsl.exe", SystemRoot), vec![]),
3755			("MSYS2", "C:\\msys64\\usr\\bin\\bash.exe".to_string(), vec!["--login", "-i"]),
3756			("Cygwin", "C:\\cygwin64\\bin\\bash.exe".to_string(), vec!["--login", "-i"]),
3757		];
3758
3759		let mut IsFirstFound = true;
3760		for (Name, Path, Args) in &WindowsShells {
3761			if std::path::Path::new(Path).exists() {
3762				Profiles.push(json!({
3763					"profileName": Name,
3764					"path": Path,
3765					"isDefault": IsFirstFound,
3766					"args": Args,
3767					"env": {},
3768					"icon": "terminal"
3769				}));
3770				IsFirstFound = false;
3771			}
3772		}
3773	}
3774
3775	Ok(json!(Profiles))
3776}
3777
3778/// Get default system shell - cross-platform
3779async fn handle_local_pty_get_default_shell() -> Result<Value, String> {
3780	#[cfg(unix)]
3781	{
3782		let Shell = std::env::var("SHELL").unwrap_or_else(|_| {
3783			// Try common fallbacks
3784			for Path in &["/bin/zsh", "/bin/bash", "/bin/sh"] {
3785				if std::path::Path::new(Path).exists() {
3786					return Path.to_string();
3787				}
3788			}
3789			"/bin/sh".to_string()
3790		});
3791		Ok(json!(Shell))
3792	}
3793
3794	#[cfg(target_os = "windows")]
3795	{
3796		let SystemRoot = std::env::var("SystemRoot").unwrap_or_else(|_| "C:\\Windows".to_string());
3797		// Check for PowerShell 7 first, then Windows PowerShell, then cmd
3798		let PwshPath = format!("{}\\PowerShell\\7\\pwsh.exe", std::env::var("ProgramFiles").unwrap_or_default());
3799		if std::path::Path::new(&PwshPath).exists() {
3800			return Ok(json!(PwshPath));
3801		}
3802		Ok(json!(format!(
3803			"{}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
3804			SystemRoot
3805		)))
3806	}
3807
3808	#[cfg(not(any(unix, target_os = "windows")))]
3809	{
3810		Ok(json!("/bin/sh"))
3811	}
3812}
3813
3814/// Get terminal environment
3815async fn handle_local_pty_get_environment() -> Result<Value, String> {
3816	let Env:HashMap<String, String> = std::env::vars().collect();
3817	Ok(json!(Env))
3818}
3819
3820/// Detect OS color scheme - cross-platform dark mode detection
3821async fn handle_native_get_color_scheme() -> Result<Value, String> {
3822	let Dark = detect_dark_mode();
3823	// High contrast detection
3824	let HighContrast = {
3825		#[cfg(target_os = "windows")]
3826		{
3827			// Windows: check SystemParametersInfo for HIGH_CONTRAST
3828			std::process::Command::new("reg")
3829				.args(["query", "HKCU\\Control Panel\\Accessibility\\HighContrast", "/v", "Flags"])
3830				.output()
3831				.ok()
3832				.map(|O| {
3833					let Output = String::from_utf8_lossy(&O.stdout);
3834					// Flag 1 = HCF_HIGHCONTRASTON
3835					Output.contains("0x1") || Output.contains("REG_DWORD    1")
3836				})
3837				.unwrap_or(false)
3838		}
3839		#[cfg(not(target_os = "windows"))]
3840		{
3841			// macOS/Linux: high contrast not natively detectable the same way
3842			// GTK: gsettings get org.gnome.desktop.a11y.interface high-contrast
3843			#[cfg(target_os = "linux")]
3844			{
3845				std::process::Command::new("gsettings")
3846					.args(["get", "org.gnome.desktop.a11y.interface", "high-contrast"])
3847					.output()
3848					.ok()
3849					.map(|O| String::from_utf8_lossy(&O.stdout).trim() == "true")
3850					.unwrap_or(false)
3851			}
3852			#[cfg(not(target_os = "linux"))]
3853			{
3854				false
3855			}
3856		}
3857	};
3858
3859	Ok(json!({ "dark": Dark, "highContrast": HighContrast }))
3860}
3861
3862/// Cross-platform dark mode detection
3863fn detect_dark_mode() -> bool {
3864	#[cfg(target_os = "macos")]
3865	{
3866		// macOS: defaults read -g AppleInterfaceStyle returns "Dark" if dark mode
3867		std::process::Command::new("defaults")
3868			.args(["read", "-g", "AppleInterfaceStyle"])
3869			.output()
3870			.ok()
3871			.map(|O| String::from_utf8_lossy(&O.stdout).trim().to_lowercase().contains("dark"))
3872			.unwrap_or(false)
3873	}
3874
3875	#[cfg(target_os = "windows")]
3876	{
3877		// Windows: HKCU\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize\
3878		// AppsUseLightTheme 0 = dark, 1 = light
3879		std::process::Command::new("reg")
3880			.args([
3881				"query",
3882				"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
3883				"/v",
3884				"AppsUseLightTheme",
3885			])
3886			.output()
3887			.ok()
3888			.map(|O| {
3889				let Output = String::from_utf8_lossy(&O.stdout);
3890				Output.contains("0x0") || Output.contains("REG_DWORD    0")
3891			})
3892			.unwrap_or(false)
3893	}
3894
3895	#[cfg(target_os = "linux")]
3896	{
3897		// Linux: Try multiple approaches
3898		// 1. GTK theme (GNOME, Ubuntu, Fedora, etc.)
3899		let GtkDark = std::process::Command::new("gsettings")
3900			.args(["get", "org.gnome.desktop.interface", "color-scheme"])
3901			.output()
3902			.ok()
3903			.map(|O| String::from_utf8_lossy(&O.stdout).contains("dark"))
3904			.unwrap_or(false);
3905
3906		if GtkDark {
3907			return true;
3908		}
3909
3910		// 2. GTK theme name contains "dark"
3911		let GtkTheme = std::process::Command::new("gsettings")
3912			.args(["get", "org.gnome.desktop.interface", "gtk-theme"])
3913			.output()
3914			.ok()
3915			.map(|O| String::from_utf8_lossy(&O.stdout).to_lowercase().contains("dark"))
3916			.unwrap_or(false);
3917
3918		if GtkTheme {
3919			return true;
3920		}
3921
3922		// 3. KDE/Plasma
3923		let KdeDark = std::env::var("KDE_COLOR_SCHEME")
3924			.ok()
3925			.map(|V| V.to_lowercase().contains("dark"))
3926			.unwrap_or(false);
3927
3928		if KdeDark {
3929			return true;
3930		}
3931
3932		// 4. xfce4
3933		let XfceDark = std::process::Command::new("xfconf-query")
3934			.args(["-c", "xsettings", "-p", "/Net/ThemeName"])
3935			.output()
3936			.ok()
3937			.map(|O| String::from_utf8_lossy(&O.stdout).to_lowercase().contains("dark"))
3938			.unwrap_or(false);
3939
3940		XfceDark
3941	}
3942
3943	#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
3944	{
3945		false
3946	}
3947}
3948
3949// =============================================================================
3950// Native file system handlers (stat, exists, delete, mkdir, readdir)
3951// =============================================================================
3952
3953/// Stat file - pure stat, no side effects. Returns IStat shape.
3954async fn handle_file_stat_native(args:Vec<Value>) -> Result<Value, String> {
3955	let Path = extract_path_from_arg(args.get(0).ok_or("Missing file path")?)?;
3956
3957	dev_log!("vfs", "stat: {}", Path);
3958
3959	let Metadata = tokio::fs::symlink_metadata(&Path).await.map_err(|E| {
3960		dev_log!("vfs", "stat ENOENT: {}", Path);
3961		format!("Failed to stat file: {} (path: {})", E, Path)
3962	})?;
3963
3964	dev_log!("vfs", "stat OK: {} (dir={})", Path, Metadata.is_dir());
3965	Ok(metadata_to_istat(&Metadata))
3966}
3967
3968/// Check file existence with URI arg support
3969async fn handle_file_exists_native(args:Vec<Value>) -> Result<Value, String> {
3970	let Path = extract_path_from_arg(args.get(0).ok_or("Missing file path")?)?;
3971
3972	Ok(json!(tokio::fs::try_exists(&Path).await.unwrap_or(false)))
3973}
3974
3975/// Delete file or directory with URI arg support
3976async fn handle_file_delete_native(args:Vec<Value>) -> Result<Value, String> {
3977	let Path = extract_path_from_arg(args.get(0).ok_or("Missing file path")?)?;
3978
3979	// Options may include { recursive, useTrash }
3980	let Recursive = args
3981		.get(1)
3982		.and_then(|V| V.as_object())
3983		.and_then(|O| O.get("recursive"))
3984		.and_then(|V| V.as_bool())
3985		.unwrap_or(false);
3986
3987	let PathBuf = std::path::Path::new(&Path);
3988
3989	if PathBuf.is_dir() {
3990		if Recursive {
3991			tokio::fs::remove_dir_all(&Path).await
3992		} else {
3993			tokio::fs::remove_dir(&Path).await
3994		}
3995	} else {
3996		tokio::fs::remove_file(&Path).await
3997	}
3998	.map_err(|E| format!("Failed to delete: {} ({})", Path, E))?;
3999
4000	Ok(Value::Null)
4001}
4002
4003/// Create directory with URI arg support
4004async fn handle_file_mkdir_native(args:Vec<Value>) -> Result<Value, String> {
4005	let Path = extract_path_from_arg(args.get(0).ok_or("Missing directory path")?)?;
4006
4007	tokio::fs::create_dir_all(&Path)
4008		.await
4009		.map_err(|E| format!("Failed to mkdir: {} ({})", Path, E))?;
4010
4011	Ok(Value::Null)
4012}
4013
4014/// Read directory contents with URI arg support
4015/// Returns array of [name, fileType] tuples matching VS Code's ReadDirResult
4016async fn handle_file_readdir_native(args:Vec<Value>) -> Result<Value, String> {
4017	let Path = extract_path_from_arg(args.get(0).ok_or("Missing directory path")?)?;
4018
4019	dev_log!("vfs", "readdir: {}", Path);
4020
4021	let mut Entries = tokio::fs::read_dir(&Path)
4022		.await
4023		.map_err(|E| format!("Failed to readdir: {} ({})", Path, E))?;
4024
4025	let mut Result = Vec::new();
4026
4027	while let Some(Entry) = Entries.next_entry().await.map_err(|E| E.to_string())? {
4028		let Name = Entry.file_name().to_string_lossy().to_string();
4029		let FileType = Entry.file_type().await.map_err(|E| E.to_string())?;
4030
4031		let TypeValue = if FileType.is_symlink() {
4032			64 // SymbolicLink
4033		} else if FileType.is_dir() {
4034			2 // Directory
4035		} else {
4036			1 // File
4037		};
4038
4039		Result.push(json!([Name, TypeValue]));
4040	}
4041
4042	Ok(json!(Result))
4043}
4044
4045// =============================================================================
4046// Storage handlers (VS Code NativeWorkbenchStorageService)
4047// =============================================================================
4048
4049/// Get all storage items as [key, value] tuples.
4050/// VS Code's NativeWorkbenchStorageService calls this on initialization.
4051async fn handle_storage_get_items(runtime:Arc<ApplicationRunTime>, _args:Vec<Value>) -> Result<Value, String> {
4052	let provider:Arc<dyn StorageProvider> = runtime.Environment.Require();
4053
4054	match provider.GetAllStorage(true).await {
4055		Ok(State) => {
4056			// Convert JSON object to array of [key, value] tuples
4057			if let Some(Obj) = State.as_object() {
4058				let Tuples:Vec<Value> = Obj
4059					.iter()
4060					.map(|(K, V)| {
4061						let ValStr = match V {
4062							Value::String(S) => S.clone(),
4063							_ => V.to_string(),
4064						};
4065						json!([K, ValStr])
4066					})
4067					.collect();
4068				Ok(json!(Tuples))
4069			} else {
4070				Ok(json!([]))
4071			}
4072		},
4073		Err(_) => Ok(json!([])),
4074	}
4075}
4076
4077/// Update storage items. VS Code sends { insert, delete } where:
4078/// - insert: Array of [key, value] tuples or Map<string, string>
4079/// - delete: Array of keys to remove
4080async fn handle_storage_update_items(runtime:Arc<ApplicationRunTime>, args:Vec<Value>) -> Result<Value, String> {
4081	let provider:Arc<dyn StorageProvider> = runtime.Environment.Require();
4082
4083	if let Some(Updates) = args.get(0).and_then(|V| V.as_object()) {
4084		// Handle inserts
4085		if let Some(Inserts) = Updates.get("insert") {
4086			if let Some(Arr) = Inserts.as_array() {
4087				for Item in Arr {
4088					if let Some(Pair) = Item.as_array() {
4089						if let (Some(Key), Some(Val)) = (Pair.get(0).and_then(|V| V.as_str()), Pair.get(1)) {
4090							let _ = provider.UpdateStorageValue(true, Key.to_string(), Some(Val.clone())).await;
4091						}
4092					}
4093				}
4094			} else if let Some(Obj) = Inserts.as_object() {
4095				for (Key, Val) in Obj {
4096					let _ = provider.UpdateStorageValue(true, Key.clone(), Some(Val.clone())).await;
4097				}
4098			}
4099		}
4100
4101		// Handle deletes
4102		if let Some(Deletes) = Updates.get("delete").and_then(|V| V.as_array()) {
4103			for Key in Deletes {
4104				if let Some(K) = Key.as_str() {
4105					let _ = provider.UpdateStorageValue(true, K.to_string(), None).await;
4106				}
4107			}
4108		}
4109	}
4110
4111	Ok(Value::Null)
4112}