Skip to main content

Mountain/IPC/WindServiceHandlers/
Extensions.rs

1#![allow(non_snake_case, unused_variables, dead_code, unused_imports)]
2
3//! Extension management handlers - list, get, query, install/uninstall stubs.
4
5use std::sync::Arc;
6
7use CommonLibrary::ExtensionManagement::ExtensionManagementService::ExtensionManagementService;
8use serde_json::{Value, json};
9
10use crate::{
11	IPC::UriComponents::Normalize::Fn as NormalizeUri,
12	RunTime::ApplicationRunTime::ApplicationRunTime,
13	dev_log,
14};
15
16/// VS Code's `ExtensionType` enum - mirror the numeric values used by the
17/// renderer's `getInstalled(type?)` IPC so the filter in `GetInstalledArgs`
18/// matches what the channel client sends.
19///
20/// `src/vs/platform/extensions/common/extensions.ts` in the pinned VS Code
21/// dependency:
22/// ```ts
23/// export const enum ExtensionType { System = 0, User = 1 }
24/// ```
25const EXTENSION_TYPE_SYSTEM:u8 = 0;
26const EXTENSION_TYPE_USER:u8 = 1;
27
28/// Return scanned extensions reshaped as VS Code's `ILocalExtension[]`
29/// so `ExtensionManagementChannelClient.getInstalled` can destructure
30/// `extension.identifier.id`, `extension.manifest.*`, and
31/// `extension.location` without blowing up.
32///
33/// # Argument contract
34///
35/// `Arguments[0]` is the optional `ExtensionType` filter VS Code passes in:
36/// - `0` (System) → only return built-in extensions.
37/// - `1` (User) → only return VSIX-installed extensions.
38/// - `null` / missing → return every known extension.
39///
40/// Previously this filter was silently dropped and every call returned the
41/// full list hardcoded as `type: 0, isBuiltin: true`. That produced three
42/// cascading symptoms:
43///   1. VSIX-installed extensions (e.g. `Anthropic.claude-code`) showed up
44///      under "Built-in" in the Extensions sidebar and had no Uninstall action
45///      because the UI keys off `type === User`.
46///   2. The trusted-publishers boot migration iterated every extension as User
47///      and attempted `manifest.publisher.toLowerCase()` against System
48///      manifests.
49///   3. `extensions:scanUserExtensions` (which shares the user-only semantic)
50///      returned zero, making the "Install from VSIX…" refresh appear to do
51///      nothing even when the install itself succeeded.
52pub async fn ExtensionsGetInstalled(RunTime:Arc<ApplicationRunTime>, Arguments:Vec<Value>) -> Result<Value, String> {
53	let TypeFilter:Option<u8> = Arguments.first().and_then(|V| V.as_u64()).map(|N| N as u8);
54
55	// Boot-time race fix: the workbench's `IExtensionService` calls
56	// `extensions:getInstalled` ~13 times during the first second of
57	// boot - reading an empty list because `ExtensionPopulate` runs
58	// in a parallel async task and only writes to ScannedExtensions
59	// AFTER its multi-path scan completes (~250-500ms in). The
60	// workbench caches that empty list, runs `viewsContainersExtension
61	// Point.setHandler([])`, and never re-processes contributions
62	// when the scan finishes - so the activity bar has zero
63	// extension-contributed icons (no Roo, Claude, gitlens panels)
64	// even though 113 extensions are scanned.
65	//
66	// Poll the map up to ~5 seconds before returning empty. 3s was
67	// previously the ceiling but cold-cache runs over 113 extensions
68	// across 6 paths regularly land at ~2950ms - the previous limit
69	// hit the wall and returned `[]`, poisoning the workbench's
70	// `IExtensionService` cache. 5s gives slow I/O (cold NVM,
71	// network-mounted user dirs) headroom while keeping the visible
72	// worst case bounded.
73	let mut Extensions = RunTime
74		.Environment
75		.GetExtensions()
76		.await
77		.map_err(|Error| format!("extensions:getInstalled failed: {}", Error))?;
78	if Extensions.is_empty() {
79		const POLL_INTERVAL_MS:u64 = 50;
80		const MAX_WAIT_MS:u64 = 5000;
81		let mut Elapsed:u64 = 0;
82		while Extensions.is_empty() && Elapsed < MAX_WAIT_MS {
83			tokio::time::sleep(std::time::Duration::from_millis(POLL_INTERVAL_MS)).await;
84			Elapsed += POLL_INTERVAL_MS;
85			Extensions = RunTime
86				.Environment
87				.GetExtensions()
88				.await
89				.map_err(|Error| format!("extensions:getInstalled failed: {}", Error))?;
90		}
91		if !Extensions.is_empty() {
92			dev_log!(
93				"extensions",
94				"extensions:getInstalled awaited scan completion ({}ms) - now has {} entries",
95				Elapsed,
96				Extensions.len()
97			);
98		} else {
99			dev_log!(
100				"extensions",
101				"warn: extensions:getInstalled timed out after {}ms; returning empty list",
102				Elapsed
103			);
104		}
105	}
106
107	let Wrapped:Vec<Value> = Extensions
108		.into_iter()
109		.filter_map(|Manifest| {
110			// `isBuiltin` is authored by the scanner; default to `true` for
111			// safety when the field is missing (matches the pre-filter
112			// hardcoded behaviour so we never drop an extension the renderer
113			// used to see).
114			let IsBuiltin = Manifest.get("isBuiltin").and_then(Value::as_bool).unwrap_or(true);
115			let ExtensionType = if IsBuiltin { EXTENSION_TYPE_SYSTEM } else { EXTENSION_TYPE_USER };
116
117			if let Some(Wanted) = TypeFilter {
118				if Wanted != ExtensionType {
119					return None;
120				}
121			}
122
123			let Publisher = Manifest
124				.get("publisher")
125				.and_then(Value::as_str)
126				.filter(|S| !S.is_empty())
127				.unwrap_or("unknown")
128				.to_string();
129			let Name = Manifest
130				.get("name")
131				.and_then(Value::as_str)
132				.filter(|S| !S.is_empty())
133				.unwrap_or("unknown")
134				.to_string();
135			let Id = format!("{}.{}", Publisher, Name);
136
137			// VS Code's `URI.revive()` is a no-op on strings, so the scanner's
138			// `file://…` raw URL has to be reshaped into `UriComponents` here -
139			// otherwise every `location.fsPath` / `location.scheme` access in
140			// the sidebar silently returns `undefined` and the whole batch is
141			// filtered out. Normalize once, reuse for both the top-level
142			// `location` and the mirror inside `manifest.extensionLocation` so
143			// callers that read either field get the same shape.
144			let Location = NormalizeUri(Manifest.get("extensionLocation"));
145			// Guarantee the manifest is an object with non-empty `publisher`,
146			// `name` and `version` fields before it reaches the renderer. VS
147			// Code runs a trusted-publishers migration at first-boot
148			// (`extensions.contribution.ts`) that unconditionally calls
149			// `extension.manifest.publisher.toLowerCase()`; any missing
150			// `manifest` object, or a manifest with `publisher === undefined`,
151			// crashes the webview with
152			// `TypeError: undefined is not an object (evaluating 'manifest.publisher')`
153			// before the workbench can render a single pixel. A non-object
154			// value here (null / Value::Null from upstream scan failures) is
155			// replaced with a bare skeleton so the renderer always has shape.
156			let mut Manifest = match Manifest {
157				Value::Object(_) => Manifest,
158				_ => json!({}),
159			};
160			if let Value::Object(ref mut Map) = Manifest {
161				Map.insert("extensionLocation".to_string(), Location.clone());
162				Map.entry("publisher".to_string()).or_insert_with(|| json!(Publisher.clone()));
163				Map.entry("name".to_string()).or_insert_with(|| json!(Name.clone()));
164				Map.entry("version".to_string()).or_insert_with(|| json!("0.0.0"));
165			}
166
167			Some(json!({
168				// IExtension (base)
169				"type": ExtensionType,
170				"isBuiltin": IsBuiltin,
171				"identifier": { "id": Id },
172				"manifest": Manifest,
173				"location": Location,
174				"targetPlatform": "undefined",
175				"isValid": true,
176				"validations": [],
177				"preRelease": false,
178				// ILocalExtension (extras)
179				"isWorkspaceScoped": false,
180				"isMachineScoped": false,
181				"isApplicationScoped": false,
182				"publisherId": null,
183				"isPreReleaseVersion": false,
184				"hasPreReleaseVersion": false,
185				"private": false,
186				"updated": false,
187				"pinned": false,
188				"forceAutoUpdate": false,
189				// `source` distinguishes the disk origin: built-ins ship with
190				// the bundle ("system"); VSIX-installed extensions live under
191				// `~/.land/extensions/*` ("vsix"). The sidebar keys off this
192				// for the "Uninstall" gesture.
193				"source": if IsBuiltin { "system" } else { "vsix" },
194				"size": 0,
195			}))
196		})
197		.collect();
198
199	dev_log!(
200		"extensions",
201		"extensions:getInstalled type={:?} returning {} ILocalExtension-shaped entries",
202		TypeFilter,
203		Wrapped.len()
204	);
205
206	Ok(json!(Wrapped))
207}
208
209/// Return metadata for all scanned extensions.
210pub async fn ExtensionsGetAll(RunTime:Arc<ApplicationRunTime>) -> Result<Value, String> {
211	let Extensions = RunTime
212		.Environment
213		.GetExtensions()
214		.await
215		.map_err(|Error| format!("extensions:getAll failed: {}", Error))?;
216
217	dev_log!("extensions", "extensions:getAll returning {} extensions", Extensions.len());
218	if let Some(First) = Extensions.first() {
219		dev_log!(
220			"extensions",
221			"extensions:getAll sample: {}",
222			serde_json::to_string(First)
223				.unwrap_or_default()
224				.chars()
225				.take(300)
226				.collect::<String>()
227		);
228	}
229	Ok(json!(Extensions))
230}
231
232/// Return metadata for a single extension by ID.
233pub async fn ExtensionsGet(RunTime:Arc<ApplicationRunTime>, Arguments:Vec<Value>) -> Result<Value, String> {
234	let Id = Arguments
235		.first()
236		.and_then(|V| V.as_str())
237		.ok_or_else(|| "extensions:get requires string id as first argument".to_string())?
238		.to_string();
239
240	let Extension = RunTime
241		.Environment
242		.GetExtension(Id)
243		.await
244		.map_err(|Error| format!("extensions:get failed: {}", Error))?;
245
246	Ok(Extension.unwrap_or(Value::Null))
247}
248
249/// Check whether an extension is currently active (scanned and present).
250pub async fn ExtensionsIsActive(RunTime:Arc<ApplicationRunTime>, Arguments:Vec<Value>) -> Result<Value, String> {
251	let Id = Arguments
252		.first()
253		.and_then(|V| V.as_str())
254		.ok_or_else(|| "extensions:isActive requires string id as first argument".to_string())?
255		.to_string();
256
257	let Extension = RunTime
258		.Environment
259		.GetExtension(Id)
260		.await
261		.map_err(|Error| format!("extensions:isActive failed: {}", Error))?;
262
263	Ok(json!(Extension.is_some()))
264}