Skip to content

Commit 27652fa

Browse files
feat(Mountain): Add notification atom handlers and Cocoon log tag extraction
Add 25 new notification handlers in `Vine::Server::Notification` to bridge the remaining VS Code API surface from Cocoon to Mountain: - Output channel lifecycle: create, append, appendLine, clear, dispose, replace, show (both `output.*` and `outputChannel.*` wire names) - Status bar text updates: setStatusBarText, disposeStatusBarItem - Progress lifecycle: progress.update, progress.complete (routes to existing `sky://progress/*` channels) - Webview reverse messaging: webview.postMessage, webview.dispose - Provider unregistration: authentication, debug_adapter, file_system, scm, task, uri_handler handlers + updateScmGroup - Misc: openExternal (uses `opener` crate), security.incident, set_language_configuration Each atom is a dedicated file with a single handler, keeping the dispatcher in `MountainVinegRPCService` thin. The atoms access `ApplicationHandle` and `RunTime` via new accessors on the service struct. Also add `ExtractDevTag` module to parse Cocoon stdout lines prefixed with `[DEV:<TAG>]` and re-emit them under Mountain's matching tag (e.g., `bootstrap-stage`, `ext-activate`, `config-prime`, `breaker`). This enables `LAND_DEV_LOG=bootstrap-stage` on Mountain's side to surface Cocoon's bootstrap diagnostics without enabling the broad `cocoon` tag. Impact: Extension output channels, status bar messages, progress indicators, and webview messaging now flow through Mountain to the renderer. Cocoon bootstrap and extension activation logs are tag-filterable end-to-end.
1 parent cc8f2a0 commit 27652fa

35 files changed

Lines changed: 918 additions & 2 deletions

Source/ProcessManagement/CocoonManagement.rs

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ use super::{InitializationData, NodeResolver};
7575
use crate::{
7676
Environment::MountainEnvironment::MountainEnvironment,
7777
IPC::Common::HealthStatus::{HealthIssue, HealthMonitor},
78+
ProcessManagement::ExtractDevTag::ExtractDevTag,
7879
Vine,
7980
dev_log,
8081
};
@@ -360,14 +361,38 @@ async fn LaunchAndManageCocoonSideCar(
360361
dev_log!("cocoon", "[CocoonManagement] Cocoon process spawned [PID: {}]", ProcessId);
361362
crate::dev_log!("cocoon", "spawned PID={}", ProcessId);
362363

363-
// Capture stdout for trace logging
364+
// Capture stdout for trace logging. Two disposition classes:
365+
//
366+
// 1. Tagged lines produced by `Cocoon/Source/Services/DevLog.ts::
367+
// CocoonDevLog(Tag, Message)` arrive prefixed with
368+
// `[DEV:<UPPER_TAG>] <body>`. Re-emit under the matching Mountain
369+
// tag (lowercased) so `LAND_DEV_LOG=bootstrap-stage` on Mountain's
370+
// side surfaces Cocoon's `bootstrap-stage` lines without forcing
371+
// the user to also enable the broad `cocoon` tag.
372+
//
373+
// 2. Plain stdout (console.log, uncaught trace, etc.) stays under
374+
// the `cocoon` tag so it's silent unless explicitly requested.
364375
if let Some(stdout) = ChildProcess.stdout.take() {
365376
tokio::spawn(async move {
366377
let Reader = BufReader::new(stdout);
367378
let mut Lines = Reader.lines();
368379

369380
while let Ok(Some(Line)) = Lines.next_line().await {
370-
dev_log!("cocoon", "[Cocoon stdout] {}", Line);
381+
if let Some(ForwardedTag) = ExtractDevTag(&Line) {
382+
// dev_log! macro requires a static string, so match on
383+
// the known tag set and fall through to raw 'cocoon'
384+
// for anything else. Keep the arms in sync with
385+
// `CocoonDevLog` call sites.
386+
match ForwardedTag.as_str() {
387+
"bootstrap-stage" => dev_log!("bootstrap-stage", "[Cocoon stdout] {}", Line),
388+
"ext-activate" => dev_log!("ext-activate", "[Cocoon stdout] {}", Line),
389+
"config-prime" => dev_log!("config-prime", "[Cocoon stdout] {}", Line),
390+
"breaker" => dev_log!("breaker", "[Cocoon stdout] {}", Line),
391+
_ => dev_log!("cocoon", "[Cocoon stdout] {}", Line),
392+
}
393+
} else {
394+
dev_log!("cocoon", "[Cocoon stdout] {}", Line);
395+
}
371396
}
372397
});
373398
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#![allow(non_snake_case)]
2+
//! Cocoon stdout line inspector: detects the `[DEV:<TAG>] <body>` prefix
3+
//! `Cocoon/Source/Services/DevLog.ts::CocoonDevLog` writes and returns
4+
//! the lowercased tag for dispatch into Mountain's per-tag `dev_log!`
5+
//! sinks. Returns `None` for bare stdout so the caller falls back to
6+
//! the catch-all `cocoon` tag.
7+
8+
pub fn ExtractDevTag(Line:&str) -> Option<String> {
9+
let Stripped = Line.strip_prefix("[DEV:")?;
10+
let (TagUpper, _Rest) = Stripped.split_once(']')?;
11+
if TagUpper.is_empty() {
12+
return None;
13+
}
14+
// Reject anything that isn't a simple tag ident - prevents stray
15+
// `[DEV: something with space]` headers from being treated as tags.
16+
if !TagUpper.chars().all(|C| C.is_ascii_uppercase() || C == '-' || C == '_') {
17+
return None;
18+
}
19+
Some(TagUpper.to_ascii_lowercase())
20+
}
21+
22+
#[cfg(test)]
23+
mod Tests {
24+
use super::ExtractDevTag;
25+
26+
#[test]
27+
fn StripsKnownTag() {
28+
assert_eq!(
29+
ExtractDevTag("[DEV:BOOTSTRAP-STAGE] [Bootstrap] stage=Environment event=start"),
30+
Some("bootstrap-stage".to_string())
31+
);
32+
}
33+
34+
#[test]
35+
fn RejectsPlainText() { assert_eq!(ExtractDevTag("plain stdout line"), None); }
36+
37+
#[test]
38+
fn RejectsMalformed() {
39+
assert_eq!(ExtractDevTag("[DEV: BOOT] x"), None);
40+
assert_eq!(ExtractDevTag("[DEV:]"), None);
41+
assert_eq!(ExtractDevTag("[DEV:BOOT"), None);
42+
}
43+
}

Source/ProcessManagement/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,5 +53,8 @@
5353
#![allow(non_snake_case)]
5454

5555
pub mod CocoonManagement;
56+
pub mod ExtractDevTag;
5657
pub mod InitializationData;
5758
pub mod NodeResolver;
59+
60+
pub use ExtractDevTag::ExtractDevTag;

Source/Vine/Server/MountainVinegRPCService.rs

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,21 @@ pub struct MountainVinegRPCService {
8585
ActiveOperations:Arc<RwLock<HashMap<u64, tokio_util::sync::CancellationToken>>>,
8686
}
8787

88+
impl MountainVinegRPCService {
89+
/// Accessor for Tauri `AppHandle` - used by the per-wire-method atoms
90+
/// in `Vine::Server::Notification::*` that need to emit
91+
/// `sky://` / `cocoon:*` events downstream. Kept as a thin read so the
92+
/// struct's fields can stay private; atoms should never mutate the
93+
/// handle, only `emit` through it.
94+
pub fn ApplicationHandle(&self) -> &AppHandle { &self.ApplicationHandle }
95+
96+
/// Accessor for the shared `ApplicationRunTime`. Notification atoms
97+
/// reach `Environment.ApplicationState.*` (provider registry, extension
98+
/// registry, scheduler) through this. Clone from `Arc` when the atom
99+
/// needs to keep it across an `.await` boundary.
100+
pub fn RunTime(&self) -> &Arc<ApplicationRunTime> { &self.RunTime }
101+
}
102+
88103
impl MountainVinegRPCService {
89104
/// Creates a new instance of the Mountain gRPC service.
90105
///
@@ -816,6 +831,111 @@ impl MountainService for MountainVinegRPCService {
816831
}
817832
},
818833

834+
// Batch 8: provider unregister atoms. Each wire method lives in
835+
// its own `Notification/<Name>.rs` atom - the arm is a pure
836+
// delegation so adding a variant stays a one-line change here
837+
// plus one new file.
838+
"unregister_authentication_provider" => {
839+
super::Notification::UnregisterAuthenticationProvider::UnregisterAuthenticationProvider(self, &Parameter).await;
840+
},
841+
"unregister_debug_adapter" => {
842+
super::Notification::UnregisterDebugAdapter::UnregisterDebugAdapter(self, &Parameter).await;
843+
},
844+
"unregister_file_system_provider" => {
845+
super::Notification::UnregisterFileSystemProvider::UnregisterFileSystemProvider(self, &Parameter).await;
846+
},
847+
"unregister_scm_provider" => {
848+
super::Notification::UnregisterScmProvider::UnregisterScmProvider(self, &Parameter).await;
849+
},
850+
"unregister_task_provider" => {
851+
super::Notification::UnregisterTaskProvider::UnregisterTaskProvider(self, &Parameter).await;
852+
},
853+
"unregister_uri_handler" => {
854+
super::Notification::UnregisterUriHandler::UnregisterUriHandler(self, &Parameter).await;
855+
},
856+
"update_scm_group" => {
857+
super::Notification::UpdateScmGroup::UpdateScmGroup(self, &Parameter).await;
858+
},
859+
860+
// Batch 11: progress lifecycle name alignment.
861+
"progress.update" => {
862+
super::Notification::ProgressUpdate::ProgressUpdate(self, &Parameter).await;
863+
},
864+
"progress.complete" => {
865+
super::Notification::ProgressComplete::ProgressComplete(self, &Parameter).await;
866+
},
867+
868+
// Batch 10: status-bar text-only fast path + item disposal.
869+
"setStatusBarText" => {
870+
super::Notification::SetStatusBarText::SetStatusBarText(self, &Parameter).await;
871+
},
872+
"disposeStatusBarItem" => {
873+
super::Notification::DisposeStatusBarItem::DisposeStatusBarItem(self, &Parameter).await;
874+
},
875+
876+
// Batch 9: output channel lifecycle. Two parallel wire names
877+
// (`output.*` via `MountainClient.sendNotification` and
878+
// `outputChannel.*` via `SendToMountain`) both forward to the
879+
// same `sky://output/*` channels until Cocoon consolidates.
880+
"output.create" => {
881+
super::Notification::OutputCreate::OutputCreate(self, &Parameter).await;
882+
},
883+
"output.append" => {
884+
super::Notification::OutputAppend::OutputAppend(self, &Parameter).await;
885+
},
886+
"output.appendLine" => {
887+
super::Notification::OutputAppendLine::OutputAppendLine(self, &Parameter).await;
888+
},
889+
"output.clear" => {
890+
super::Notification::OutputClear::OutputClear(self, &Parameter).await;
891+
},
892+
"output.show" => {
893+
super::Notification::OutputShow::OutputShow(self, &Parameter).await;
894+
},
895+
"output.dispose" => {
896+
super::Notification::OutputDispose::OutputDispose(self, &Parameter).await;
897+
},
898+
"output.replace" => {
899+
super::Notification::OutputReplace::OutputReplace(self, &Parameter).await;
900+
},
901+
"outputChannel.create" => {
902+
super::Notification::OutputChannelCreate::OutputChannelCreate(self, &Parameter).await;
903+
},
904+
"outputChannel.append" => {
905+
super::Notification::OutputChannelAppend::OutputChannelAppend(self, &Parameter).await;
906+
},
907+
"outputChannel.clear" => {
908+
super::Notification::OutputChannelClear::OutputChannelClear(self, &Parameter).await;
909+
},
910+
"outputChannel.show" => {
911+
super::Notification::OutputChannelShow::OutputChannelShow(self, &Parameter).await;
912+
},
913+
"outputChannel.hide" => {
914+
super::Notification::OutputChannelHide::OutputChannelHide(self, &Parameter).await;
915+
},
916+
"outputChannel.dispose" => {
917+
super::Notification::OutputChannelDispose::OutputChannelDispose(self, &Parameter).await;
918+
},
919+
920+
// Batch 13: webview reverse-channel (Mountain → renderer).
921+
"webview.postMessage" => {
922+
super::Notification::WebviewPostMessage::WebviewPostMessage(self, &Parameter).await;
923+
},
924+
"webview.dispose" => {
925+
super::Notification::WebviewDispose::WebviewDispose(self, &Parameter).await;
926+
},
927+
928+
// Batch 14: grammar config, external-URI open, security alert.
929+
"set_language_configuration" => {
930+
super::Notification::SetLanguageConfiguration::SetLanguageConfiguration(self, &Parameter).await;
931+
},
932+
"openExternal" => {
933+
super::Notification::OpenExternal::OpenExternal(self, &Parameter).await;
934+
},
935+
"security.incident" => {
936+
super::Notification::SecurityIncident::SecurityIncident(self, &Parameter).await;
937+
},
938+
819939
// Cocoon → Mountain: provider registration from extensions.
820940
//
821941
// Covers all 34 `register_*` / `register_*_provider` notification
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#![allow(non_snake_case)]
2+
//! Cocoon → Mountain `disposeStatusBarItem` notification.
3+
//! Emitted once by `Cocoon/.../Services/Window/StatusBar.ts:139` when an
4+
//! extension calls `StatusBarItem.dispose()` (or the whole subscription
5+
//! set tears down). Forwards onto the canonical
6+
//! `sky://statusbar/dispose-entry` channel so the Sky shim's
7+
//! fan-out listener removes the DOM node.
8+
9+
use serde_json::{Value, json};
10+
use tauri::Emitter;
11+
12+
use crate::{Vine::Server::MountainVinegRPCService::MountainVinegRPCService, dev_log};
13+
14+
pub async fn DisposeStatusBarItem(Service:&MountainVinegRPCService, Parameter:&Value) {
15+
let Id = Parameter.get("id").and_then(Value::as_str).unwrap_or("");
16+
if Id.is_empty() {
17+
dev_log!("grpc", "[StatusBar] dispose skip: missing id");
18+
return;
19+
}
20+
let _ = Service.ApplicationHandle().emit("sky://statusbar/dispose-entry", json!({ "id": Id }));
21+
dev_log!("grpc", "[StatusBar] dispose id={}", Id);
22+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#![allow(non_snake_case)]
2+
//! Cocoon → Mountain `openExternal` notification.
3+
//! Emitted by `Cocoon/.../APIFactoryService.ts:393` when an extension
4+
//! calls `vscode.env.openExternal(uri)`. Delegates to the platform's
5+
//! default handler via the `opener` crate (already a Mountain dep via
6+
//! `nativeHost:openExternal`). Fire-and-forget; success/failure is
7+
//! logged but not surfaced back to the extension.
8+
9+
use serde_json::Value;
10+
11+
use crate::{Vine::Server::MountainVinegRPCService::MountainVinegRPCService, dev_log};
12+
13+
pub async fn OpenExternal(_Service:&MountainVinegRPCService, Parameter:&Value) {
14+
let Uri = Parameter.get("uri").and_then(Value::as_str).unwrap_or("");
15+
if Uri.is_empty() {
16+
dev_log!("grpc", "[OpenExternal] skip: missing uri");
17+
return;
18+
}
19+
match open::that(Uri) {
20+
Ok(()) => dev_log!("grpc", "[OpenExternal] uri={} ok", Uri),
21+
Err(Error) => dev_log!("grpc", "[OpenExternal] uri={} err={}", Uri, Error),
22+
}
23+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#![allow(non_snake_case)]
2+
//! Cocoon → Mountain `output.append` notification.
3+
//! Emitted by `Cocoon/.../Services/Window/OutputChannel.ts:50` whenever
4+
//! an extension calls `OutputChannel.append(text)`. Forwards verbatim to
5+
//! `sky://output/append` - the Sky listener mirrors the text into both
6+
//! the in-memory `OutputChannels` map and VS Code's logger sink.
7+
8+
use serde_json::Value;
9+
use tauri::Emitter;
10+
11+
use crate::{Vine::Server::MountainVinegRPCService::MountainVinegRPCService, dev_log};
12+
13+
pub async fn OutputAppend(Service:&MountainVinegRPCService, Parameter:&Value) {
14+
let _ = Service.ApplicationHandle().emit("sky://output/append", Parameter);
15+
dev_log!(
16+
"grpc",
17+
"[Output] append channel={} bytes={}",
18+
Parameter.get("channel").and_then(Value::as_str).unwrap_or("?"),
19+
Parameter.get("text").and_then(Value::as_str).map(str::len).unwrap_or(0)
20+
);
21+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#![allow(non_snake_case)]
2+
//! Cocoon → Mountain `output.appendLine` notification.
3+
//! Emitted by `Cocoon/.../Services/Window/OutputChannel.ts:56` whenever
4+
//! an extension calls `OutputChannel.appendLine(text)`. The stock
5+
//! semantic contract is "append + trailing \n"; we suffix the newline
6+
//! here so the downstream `sky://output/append` listener stays a single
7+
//! append code path (no `appendLine` listener in Sky).
8+
9+
use serde_json::{Value, json};
10+
use tauri::Emitter;
11+
12+
use crate::{Vine::Server::MountainVinegRPCService::MountainVinegRPCService, dev_log};
13+
14+
pub async fn OutputAppendLine(Service:&MountainVinegRPCService, Parameter:&Value) {
15+
let Channel = Parameter.get("channel").and_then(Value::as_str).unwrap_or("");
16+
let Text = Parameter.get("text").and_then(Value::as_str).unwrap_or("");
17+
let _ = Service.ApplicationHandle().emit(
18+
"sky://output/append",
19+
json!({
20+
"channel": Channel,
21+
"text": format!("{}\n", Text),
22+
}),
23+
);
24+
dev_log!("grpc", "[Output] appendLine channel={} bytes={}", Channel, Text.len());
25+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#![allow(non_snake_case)]
2+
//! Cocoon → Mountain `outputChannel.append` notification.
3+
//! Twin of `output.append`; see `OutputCreate.rs` for the duplicate-wire
4+
//! rationale.
5+
6+
use serde_json::Value;
7+
use tauri::Emitter;
8+
9+
use crate::{Vine::Server::MountainVinegRPCService::MountainVinegRPCService, dev_log};
10+
11+
pub async fn OutputChannelAppend(Service:&MountainVinegRPCService, Parameter:&Value) {
12+
let _ = Service.ApplicationHandle().emit("sky://output/append", Parameter);
13+
dev_log!(
14+
"grpc",
15+
"[OutputChannel] append channel={}",
16+
Parameter.get("channel").and_then(Value::as_str).unwrap_or("?")
17+
);
18+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#![allow(non_snake_case)]
2+
//! Cocoon → Mountain `outputChannel.clear` notification (twin of
3+
//! `output.clear`).
4+
5+
use serde_json::Value;
6+
use tauri::Emitter;
7+
8+
use crate::{Vine::Server::MountainVinegRPCService::MountainVinegRPCService, dev_log};
9+
10+
pub async fn OutputChannelClear(Service:&MountainVinegRPCService, Parameter:&Value) {
11+
let _ = Service.ApplicationHandle().emit("sky://output/clear", Parameter);
12+
dev_log!(
13+
"grpc",
14+
"[OutputChannel] clear channel={}",
15+
Parameter.get("channel").and_then(Value::as_str).unwrap_or("?")
16+
);
17+
}

0 commit comments

Comments
 (0)