Skip to content

Commit f24e444

Browse files
feat(Mountain): Add extension metadata passthrough, IPC round-trip, and Node version checking
Three major improvements for extension handling and IPC reliability: 1. **Extension manifest passthrough (Atom TH1):** Add 14 new fields to ExtensionDescriptionStateDTO for VS Code package.json metadata: Categories, DisplayName, Description, Keywords, Repository, Bugs, Homepage, License, Icon, AiKey, ExtensionKind, Capabilities, ExtensionDependencies, ExtensionPack. Wind's Extensions sidebar filter `@builtin category:themes` now works — previously categories were absent so the filter never matched. 2. **IPC round-trip for applyEdit/showTextDocument (Atom T1):** Replace fire-and-forget event emission with request/response pattern using SendUserInterfaceRequest. Extensions awaiting workspace.applyEdit() now block until Sky actually applies the edit, fixing races with listeners expecting post-apply state. Also made SendUserInterfaceRequest pub(crate) so Track effect creators reuse the same pattern. 3. **Node version checking (Atom N1):** ResolveNodeBinary now queries `node --version` and logs the resolved version. Emits a warning when major version falls below LAND_NODE_MIN_MAJOR (default 20). Helps diagnose extension host boot failures. 4. **Compile-time PostHog config (Atom P2):** PostHogPlugin now reads API key/host/enable flag from env! baked in build.rs via PropagatePostHogSentinel(). Supports LAND_POSTHOG_DISTINCT_ID for CI correlation. Combined with debug_assertions gate for cheaper no-op path. 5. **IPC logging improvement (Atom I13):** Add paired entry/exit lines per invoke. `done: <cmd> ok=... t_ns=...` on the way out enables latency diagnosis via single grep without Jaeger hopping.
1 parent 12de818 commit f24e444

12 files changed

Lines changed: 335 additions & 54 deletions

File tree

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,11 +114,11 @@ Telemetry = []
114114
Development = []
115115
Test = []
116116

117-
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
117+
# ===========================================================================
118118
# Tier-gating (Plan A §Rust feature flags). Every name here is mirrored by
119119
# `build.rs::IsDeclaredTierFeature`. Adding a new tier requires editing
120120
# BOTH places so typos in `.env.Land` fail loud.
121-
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
121+
# ===========================================================================
122122

123123
# Tier:RemoteProcedureCall:SharedMemory 🟣 Experimental
124124
TierRemoteProcedureCallSharedMemory = []

Source/ApplicationState/DTO/ExtensionDescriptionStateDTO.rs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,82 @@ pub struct ExtensionDescriptionStateDTO {
106106
/// Extension contributions (commands, views, etc.)
107107
#[serde(default, skip_serializing_if = "Option::is_none")]
108108
pub Contributes:Option<Value>,
109+
110+
// --- Discovery metadata ---
111+
/// VS Code category tags ("Themes", "Programming Languages",
112+
/// "Snippets", "Language Packs", "Debuggers", "Formatters",
113+
/// "Keymaps", "SCM Providers", "Testing", "Education", "Other").
114+
///
115+
/// Atom TH1: Wind's Extensions sidebar filters `@builtin category:themes`
116+
/// against this array. Without the field the filter never matches —
117+
/// user reported theme extensions absent on @builtin search despite
118+
/// being on disk. Scanner-passthrough surfaces the raw package.json
119+
/// value; resolved NLS placeholders survive because the serde
120+
/// deserialisation happens after the NLS rewrite.
121+
#[serde(default, skip_serializing_if = "Option::is_none")]
122+
pub Categories:Option<Vec<String>>,
123+
124+
/// Human-readable display name, usually a VS Code NLS placeholder like
125+
/// `%displayName%` that the scanner resolves against `package.nls.json`.
126+
/// Atom TH1: sidebar row rendering falls back to `name` when this is
127+
/// absent; having the real value populates tooltips and the
128+
/// details editor.
129+
#[serde(default, skip_serializing_if = "Option::is_none")]
130+
pub DisplayName:Option<String>,
131+
132+
/// Short prose description. Same NLS-placeholder rules as DisplayName.
133+
#[serde(default, skip_serializing_if = "Option::is_none")]
134+
pub Description:Option<String>,
135+
136+
/// Extension keywords array — searched by the sidebar when the query
137+
/// doesn't match `name`, `displayName`, or `description`.
138+
#[serde(default, skip_serializing_if = "Option::is_none")]
139+
pub Keywords:Option<Vec<String>>,
140+
141+
/// Repository info: Either a string URL or `{ type, url }` object.
142+
#[serde(default, skip_serializing_if = "Option::is_none")]
143+
pub Repository:Option<Value>,
144+
145+
/// Bug-tracker URL or object.
146+
#[serde(default, skip_serializing_if = "Option::is_none")]
147+
pub Bugs:Option<Value>,
148+
149+
/// Homepage URL.
150+
#[serde(default, skip_serializing_if = "Option::is_none")]
151+
pub Homepage:Option<String>,
152+
153+
/// License identifier (SPDX short code) or URL.
154+
#[serde(default, skip_serializing_if = "Option::is_none")]
155+
pub License:Option<String>,
156+
157+
/// Icon path relative to the extension root (for sidebar thumbnails).
158+
#[serde(default, skip_serializing_if = "Option::is_none")]
159+
pub Icon:Option<String>,
160+
161+
/// Marketplace API key placeholder — still present in some upstream
162+
/// built-in manifests. `@vscode/extension-telemetry` reads its
163+
/// length on construction; if missing the activate throws
164+
/// `Cannot read properties of undefined (reading 'length')`.
165+
#[serde(default, skip_serializing_if = "Option::is_none", rename = "aiKey")]
166+
pub AiKey:Option<String>,
167+
168+
/// Marketplace-side extension kind (`["ui"]`, `["workspace"]`,
169+
/// `["web"]`). Wind uses this to decide which host to run the
170+
/// extension in. Missing → VS Code falls back to heuristics.
171+
#[serde(default, skip_serializing_if = "Option::is_none", rename = "extensionKind")]
172+
pub ExtensionKind:Option<Value>,
173+
174+
/// Capabilities descriptor — `untrustedWorkspaces`, `virtualWorkspaces`.
175+
#[serde(default, skip_serializing_if = "Option::is_none")]
176+
pub Capabilities:Option<Value>,
177+
178+
/// Dependency list — other extensions this one needs activated first.
179+
#[serde(default, skip_serializing_if = "Option::is_none", rename = "extensionDependencies")]
180+
pub ExtensionDependencies:Option<Vec<String>>,
181+
182+
/// Extension-pack children — extensions this one bundles by reference.
183+
#[serde(default, skip_serializing_if = "Option::is_none", rename = "extensionPack")]
184+
pub ExtensionPack:Option<Vec<String>>,
109185
}
110186

111187
impl ExtensionDescriptionStateDTO {
@@ -167,6 +243,20 @@ impl ExtensionDescriptionStateDTO {
167243
ExtensionLocation:serde_json::json!(null),
168244
ActivationEvents:None,
169245
Contributes:None,
246+
Categories:None,
247+
DisplayName:None,
248+
Description:None,
249+
Keywords:None,
250+
Repository:None,
251+
Bugs:None,
252+
Homepage:None,
253+
License:None,
254+
Icon:None,
255+
AiKey:None,
256+
ExtensionKind:None,
257+
Capabilities:None,
258+
ExtensionDependencies:None,
259+
ExtensionPack:None,
170260
};
171261

172262
Description.Validate()?;

Source/Binary/Build/PostHogPlugin.rs

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,52 @@ use std::sync::OnceLock;
88

99
use crate::dev_log;
1010

11-
/// PostHog EU Cloud project token (debug builds only).
12-
const POSTHOG_API_KEY:&str = "phc_mCwHy7LgvbnEqh6a2DyMiLUJcaZvmmj7JNmmpQzvr7mA";
11+
/// PostHog project token. Source of truth: `.env.Land.PostHog` LAND_POSTHOG_KEY;
12+
/// `build.rs` bakes the value via `cargo:rustc-env` so `env!` at compile
13+
/// time always resolves, even on a clean checkout.
14+
const POSTHOG_API_KEY:&str = env!("LAND_POSTHOG_KEY");
1315

14-
/// PostHog EU Cloud host.
15-
const POSTHOG_HOST:&str = "https://eu.i.posthog.com";
16+
/// PostHog region host (default EU Cloud; operators override via
17+
/// `.env.Land.PostHog` LAND_POSTHOG_HOST).
18+
const POSTHOG_HOST:&str = env!("LAND_POSTHOG_HOST");
19+
20+
/// Per-tier enable flag baked from `.env.Land.PostHog`. Cheap early-exit in
21+
/// every capture path without forking the binary per env value.
22+
const POSTHOG_ENABLED:&str = env!("LAND_POSTHOG_MOUNTAIN_ENABLED");
23+
24+
/// Optional pinned distinct-id seed (empty string → auto-generate per
25+
/// process). Useful for CI runs where correlating events across restarts
26+
/// matters more than per-dev isolation.
27+
const POSTHOG_DISTINCT_ID_SEED:&str = env!("LAND_POSTHOG_DISTINCT_ID");
1628

1729
/// Global PostHog client instance.
1830
static CLIENT:OnceLock<posthog_rs::Client> = OnceLock::new();
1931

20-
/// Machine-stable distinct ID for the dev session.
32+
/// Machine-stable distinct ID for the dev session. When LAND_POSTHOG_DISTINCT_ID
33+
/// is set, it wins - same value across every process in the same dev run.
2134
fn DistinctId() -> String {
35+
if !POSTHOG_DISTINCT_ID_SEED.is_empty() {
36+
return POSTHOG_DISTINCT_ID_SEED.to_string();
37+
}
2238
let User = std::env::var("USER")
2339
.or_else(|_| std::env::var("USERNAME"))
2440
.unwrap_or_else(|_| "unknown".to_string());
2541
format!("land-dev-{}", User)
2642
}
2743

44+
/// Whether the Mountain tier should capture at all. Combines compile-time
45+
/// debug gate with the `.env.Land.PostHog` enable switch.
46+
fn CaptureAllowed() -> bool {
47+
if !cfg!(debug_assertions) {
48+
return false;
49+
}
50+
!matches!(POSTHOG_ENABLED, "false" | "0" | "off")
51+
}
52+
2853
/// Initialize the PostHog client. Call once during app setup.
29-
/// No-op in release builds.
54+
/// No-op in release builds or when LAND_POSTHOG_MOUNTAIN_ENABLED=false.
3055
pub async fn Initialize() {
31-
if !cfg!(debug_assertions) {
56+
if !CaptureAllowed() {
3257
return;
3358
}
3459

@@ -40,13 +65,13 @@ pub async fn Initialize() {
4065

4166
let PostHogClient = posthog_rs::client(Options).await;
4267
let _ = CLIENT.set(PostHogClient);
43-
dev_log!("lifecycle", "[PostHog] Initialized (EU Cloud, debug mode)");
68+
dev_log!("lifecycle", "[PostHog] Initialized (host={}, debug mode)", POSTHOG_HOST);
4469
CaptureEvent("mountain:session:start", None);
4570
}
4671

4772
/// Capture a named event with optional properties.
4873
pub fn CaptureEvent(EventName:&str, Properties:Option<Vec<(&str, &str)>>) {
49-
if !cfg!(debug_assertions) {
74+
if !CaptureAllowed() {
5075
return;
5176
}
5277

@@ -70,7 +95,7 @@ pub fn CaptureEvent(EventName:&str, Properties:Option<Vec<(&str, &str)>>) {
7095

7196
/// Capture an error event.
7297
pub fn CaptureError(Tag:&str, Message:&str) {
73-
if !cfg!(debug_assertions) {
98+
if !CaptureAllowed() {
7499
return;
75100
}
76101

@@ -79,7 +104,7 @@ pub fn CaptureError(Tag:&str, Message:&str) {
79104

80105
/// Capture an IPC command invocation.
81106
pub fn CaptureIPC(Method:&str) {
82-
if !cfg!(debug_assertions) {
107+
if !CaptureAllowed() {
83108
return;
84109
}
85110

Source/Binary/Extension/ScanPathConfigure.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ pub fn ScanPathConfigure(AppState:&std::sync::Arc<ApplicationState>) -> Result<V
4545
// (`~/.land/extensions`) still scans so VSIX-installed extensions work.
4646
//
4747
// Atom U1: `.env.Land.Extensions` also exposes `LAND_DISABLE_BUILTIN_EXTENSIONS`
48-
// same effect, different name. Accept both so the skill-file env and
48+
// - same effect, different name. Accept both so the skill-file env and
4949
// the legacy SKIP flag don't diverge.
5050
let SkipBuiltins = matches!(std::env::var("LAND_SKIP_BUILTIN_EXTENSIONS").as_deref(), Ok("1") | Ok("true"))
5151
|| matches!(std::env::var("LAND_DISABLE_BUILTIN_EXTENSIONS").as_deref(), Ok("1") | Ok("true"));
@@ -189,7 +189,7 @@ pub fn ScanPathConfigure(AppState:&std::sync::Arc<ApplicationState>) -> Result<V
189189
}
190190
}
191191

192-
// Atom U1: development extensions path the VS Code equivalent of
192+
// Atom U1: development extensions path - the VS Code equivalent of
193193
// `--extensionDevelopmentPath=<dir>`. Extensions here always load
194194
// regardless of enablement state; kept separate from user-scope so a
195195
// broken dev extension doesn't persist into the user's profile.

Source/Binary/Main/Entry.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ pub fn Fn() {
213213
//
214214
// Build.sh exports `Browser`/`Mountain`/`Electron`/`Bundle`/`Compiler`/
215215
// `LAND_PROFILE` into the shell that invokes cargo. `build.rs` captures
216-
// those into `cargo:rustc-env=LAND_*` so they're baked into the binary
216+
// those into `cargo:rustc-env=LAND_*` so they're baked into the binary -
217217
// runtime env lookups don't survive launching the binary from Finder /
218218
// another shell. `option_env!` falls back to "unknown" when the build
219219
// ran outside Build.sh (e.g. plain `cargo build`).

Source/Environment/UserInterfaceProvider.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,12 @@ impl UserInterfaceProvider for MountainEnvironment {
306306

307307
/// A generic helper function to send a request to the Sky UI and wait for a
308308
/// response.
309-
async fn SendUserInterfaceRequest<TPayload:Serialize + Clone>(
309+
///
310+
/// Atom T1: made `pub(crate)` so Track effect creators
311+
/// (`applyEdit` / `showTextDocument` / `Task.Execute`, etc.) can reuse the
312+
/// same RequestIdentifier/oneshot pattern instead of emitting fire-and-
313+
/// forget events that resolve to synthetic success.
314+
pub(crate) async fn SendUserInterfaceRequest<TPayload:Serialize + Clone>(
310315
Environment:&MountainEnvironment,
311316

312317
EventName:&str,

Source/IPC/WindServiceHandler/Extension.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,10 +146,10 @@ pub async fn handle_extensions_is_active(Runtime:Arc<ApplicationRunTime>, Args:V
146146
///
147147
/// Atom V1: honours `LAND_USER_EXTENSIONS_DIR` (from `.env.Land.Extensions`).
148148
/// Resolution order:
149-
/// 1. `$LAND_USER_EXTENSIONS_DIR` explicit per-operator override.
149+
/// 1. `$LAND_USER_EXTENSIONS_DIR` - explicit per-operator override.
150150
/// Leading `~/` expands against `$HOME`.
151-
/// 2. `$HOME/.land/extensions` VS Code-style user-scope default.
152-
/// 3. `./extensions` fallback when `$HOME` is unavailable (container,
151+
/// 2. `$HOME/.land/extensions` - VS Code-style user-scope default.
152+
/// 3. `./extensions` - fallback when `$HOME` is unavailable (container,
153153
/// restricted environment). `fs::create_dir_all` runs on install so
154154
/// this works even if the cwd is read-only at scan time.
155155
fn UserExtensionDirectory() -> PathBuf {

Source/IPC/WindServiceHandlers/mod.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,7 @@ pub async fn mountain_ipc_invoke(app_handle:AppHandle, command:String, args:Vec<
320320
// sidebar. Pin state is Wind-owned (Cocoon never sees it); the
321321
// only Mountain-side cost is an acknowledgement so the
322322
// extension-enablement service doesn't retry forever. Payload
323-
// is optional VS Code sometimes passes `{ refreshPinned: true }`.
323+
// is optional - VS Code sometimes passes `{ refreshPinned: true }`.
324324
"extensions:resetPinnedStateForAllUserExtensions" => {
325325
dev_log!("extensions", "{} (no-op, pin state is UI-local)", command);
326326
Ok(Value::Null)
@@ -1616,6 +1616,25 @@ pub async fn mountain_ipc_invoke(app_handle:AppHandle, command:String, args:Vec<
16161616
};
16171617
crate::otel_span!(&SpanName, OTLPStart, &[("ipc.command", command.as_str())]);
16181618

1619+
// Atom I13: paired entry/exit line per invoke. `invoke: <cmd>` on the way
1620+
// in (emitted at the top of this fn); `done: <cmd> ok=… t_ns=…` on the
1621+
// way out. A `grep "logger:log"` before showed only the entry half;
1622+
// having both halves makes latency diagnosis a single pipe:
1623+
// grep "logger:log" Mountain.dev.log | awk '…'
1624+
// without hopping across Jaeger. High-frequency commands still skip the
1625+
// entry line but DO emit an exit - frequencies still aggregate, but each
1626+
// is individually accounted for.
1627+
if !IsHighFrequencyCommand {
1628+
let ElapsedNanos = crate::IPC::DevLog::NowNano().saturating_sub(OTLPStart);
1629+
dev_log!(
1630+
"ipc",
1631+
"done: {} ok={} t_ns={}",
1632+
command,
1633+
!IsErr,
1634+
ElapsedNanos
1635+
);
1636+
}
1637+
16191638
Result
16201639
}
16211640

0 commit comments

Comments
 (0)