@@ -461,14 +461,38 @@ async fn LaunchAndManageCocoonSideCar(
461461 } ) ;
462462 }
463463
464- // Capture stderr for warn-level logging
464+ // Capture stderr for warn-level logging.
465+ //
466+ // Node and macOS tooling write a stream of informational-only noise
467+ // to stderr that is indistinguishable from fatal errors at the line
468+ // level. Downgrade these to the verbose `cocoon-stderr-verbose` tag
469+ // (silent under `LAND_DEV_LOG=short`) so the main cocoon channel only
470+ // carries actionable Node errors:
471+ //
472+ // - `: is already signed` / `: replacing existing signature` - macOS
473+ // codesign informational output when Cocoon re-signs a just-rebuilt
474+ // extension binary. Not an error.
475+ // - `DeprecationWarning:` / `(node:...) [DEP0...]` - Node deprecation
476+ // warnings from VS Code's upstream dependencies (punycode, url.parse,
477+ // Buffer()). Fixable only in upstream, not in Land.
478+ // - `Use \`node --trace-deprecation\` to show where the warning was
479+ // created` - follow-up to the DEP line above.
465480 if let Some ( stderr) = ChildProcess . stderr . take ( ) {
466481 tokio:: spawn ( async move {
467482 let Reader = BufReader :: new ( stderr) ;
468483 let mut Lines = Reader . lines ( ) ;
469484
470485 while let Ok ( Some ( Line ) ) = Lines . next_line ( ) . await {
471- dev_log ! ( "cocoon" , "warn: [Cocoon stderr] {}" , Line ) ;
486+ let IsBenign = Line . contains ( ": is already signed" )
487+ || Line . contains ( ": replacing existing signature" )
488+ || Line . contains ( "DeprecationWarning:" )
489+ || Line . contains ( "--trace-deprecation" )
490+ || Line . contains ( "--trace-warnings" ) ;
491+ if IsBenign {
492+ dev_log ! ( "cocoon-stderr-verbose" , "[Cocoon stderr] {}" , Line ) ;
493+ } else {
494+ dev_log ! ( "cocoon" , "warn: [Cocoon stderr] {}" , Line ) ;
495+ }
472496 }
473497 } ) ;
474498 }
@@ -652,7 +676,20 @@ async fn LaunchAndManageCocoonSideCar(
652676 // Trigger startup extension activation. Cocoon is fully reactive -
653677 // it won't activate any extensions until Mountain tells it to.
654678 // Fire-and-forget: don't block on activation, and don't fail init if it errors.
679+ //
680+ // Stock VS Code fires a cascade of activation events at boot:
681+ // 1. `*` - unconditional "activate anything that contributes *"
682+ // 2. `onStartupFinished` - queued extensions whose start may be
683+ // deferred until after the first frame renders
684+ // 3. `workspaceContains:<pattern>` for each pattern any extension
685+ // contributes, fired per matching workspace folder
686+ //
687+ // Previously only `*` fired, which meant a large class of extensions
688+ // that gate on `workspaceContains:package.json`, `onStartupFinished`,
689+ // or similar events never activated without user interaction. The
690+ // added bursts below bring startup coverage in line with stock.
655691 let SideCarId = SideCarIdentifier . clone ( ) ;
692+ let EnvironmentForActivation = Environment . clone ( ) ;
656693 tokio:: spawn ( async move {
657694 // Small delay to let Cocoon finish processing the init response
658695 sleep ( Duration :: from_millis ( 500 ) ) . await ;
@@ -668,8 +705,104 @@ async fn LaunchAndManageCocoonSideCar(
668705 . await
669706 {
670707 dev_log ! ( "cocoon" , "warn: [CocoonManagement] $activateByEvent(\" *\" ) failed: {}" , Error ) ;
708+ return ;
709+ }
710+ dev_log ! ( "cocoon" , "[CocoonManagement] Startup extensions activation (*) triggered" ) ;
711+
712+ // Phase 2: workspaceContains: events. Iterate the scanned
713+ // extension registry, collect every pattern contributed via the
714+ // `workspaceContains:<pattern>` activation event, and fire the
715+ // event if at least one workspace folder contains a path
716+ // matching the pattern. Patterns are treated as filename globs
717+ // relative to any workspace folder root; matching is done with
718+ // a lightweight walk bounded by depth 3 and 2048 total visited
719+ // entries per folder to cap worst-case cost on huge repos.
720+ let WorkspacePatterns = {
721+ let AppState = & EnvironmentForActivation . ApplicationState ;
722+ let Folders : Vec < std:: path:: PathBuf > = AppState
723+ . Workspace
724+ . WorkspaceFolders
725+ . lock ( )
726+ . ok ( )
727+ . map ( |Guard | {
728+ Guard . iter ( )
729+ . filter_map ( |Folder | Folder . URI . to_file_path ( ) . ok ( ) )
730+ . collect :: < Vec < _ > > ( )
731+ } )
732+ . unwrap_or_default ( ) ;
733+
734+ let Patterns : Vec < String > = AppState
735+ . Extension
736+ . ScannedExtensions
737+ . ScannedExtensions
738+ . lock ( )
739+ . ok ( )
740+ . map ( |Guard | {
741+ let mut Set : std:: collections:: BTreeSet < String > = std:: collections:: BTreeSet :: new ( ) ;
742+ for Description in Guard . values ( ) {
743+ if let Some ( Events ) = & Description . ActivationEvents {
744+ for Event in Events {
745+ if let Some ( Pattern ) = Event . strip_prefix ( "workspaceContains:" ) {
746+ Set . insert ( Pattern . to_string ( ) ) ;
747+ }
748+ }
749+ }
750+ }
751+ Set . into_iter ( ) . collect ( )
752+ } )
753+ . unwrap_or_default ( ) ;
754+
755+ ( Folders , Patterns )
756+ } ;
757+
758+ let ( WorkspaceFolders , Patterns ) : ( Vec < std:: path:: PathBuf > , Vec < String > ) = WorkspacePatterns ;
759+ if !WorkspaceFolders . is_empty ( ) && !Patterns . is_empty ( ) {
760+ let Matched = FindMatchingWorkspaceContainsPatterns ( & WorkspaceFolders , & Patterns ) ;
761+ dev_log ! (
762+ "exthost" ,
763+ "[CocoonManagement] workspaceContains scan: {} pattern(s) matched across {} folder(s)" ,
764+ Matched . len( ) ,
765+ WorkspaceFolders . len( )
766+ ) ;
767+ for Pattern in Matched {
768+ let Event = format ! ( "workspaceContains:{}" , Pattern ) ;
769+ if let Err ( Error ) = Vine :: Client :: SendRequest (
770+ & SideCarId ,
771+ "$activateByEvent" . to_string ( ) ,
772+ serde_json:: json!( { "activationEvent" : Event } ) ,
773+ 30_000 ,
774+ )
775+ . await
776+ {
777+ dev_log ! (
778+ "cocoon" ,
779+ "warn: [CocoonManagement] $activateByEvent({}) failed: {}" ,
780+ Event ,
781+ Error
782+ ) ;
783+ }
784+ }
785+ }
786+
787+ // Phase 3: onStartupFinished. Fire after the `*` burst has had a
788+ // moment to complete so late-binding extensions layered on top
789+ // of startup contributions resolve in the expected order.
790+ sleep ( Duration :: from_millis ( 2_000 ) ) . await ;
791+ if let Err ( Error ) = Vine :: Client :: SendRequest (
792+ & SideCarId ,
793+ "$activateByEvent" . to_string ( ) ,
794+ serde_json:: json!( { "activationEvent" : "onStartupFinished" } ) ,
795+ 30_000 ,
796+ )
797+ . await
798+ {
799+ dev_log ! (
800+ "cocoon" ,
801+ "warn: [CocoonManagement] $activateByEvent(onStartupFinished) failed: {}" ,
802+ Error
803+ ) ;
671804 } else {
672- dev_log ! ( "cocoon" , "[CocoonManagement] Startup extensions activation triggered" ) ;
805+ dev_log ! ( "cocoon" , "[CocoonManagement] onStartupFinished activation triggered" ) ;
673806 }
674807 } ) ;
675808
@@ -920,3 +1053,146 @@ fn SweepStaleCocoon(Port:u16) {
9201053 dev_log ! ( "cocoon" , "[CocoonSweep] PID {} reaped." , Pid ) ;
9211054 }
9221055}
1056+
1057+ /// Return the subset of `Patterns` for which at least one workspace folder
1058+ /// contains a matching file or directory. Patterns are interpreted the same
1059+ /// way VS Code does for `workspaceContains:<pattern>` activation events:
1060+ ///
1061+ /// - A bare filename (no slash, no wildcards) matches an entry with that
1062+ /// name at the workspace root (e.g. `package.json`).
1063+ /// - A path with slashes but no wildcards matches a direct descendant
1064+ /// relative to the root (e.g. `.vscode/launch.json`).
1065+ /// - A glob with `**/` prefix matches any descendant up to a bounded depth.
1066+ /// - Any other wildcard form is matched via a simple segment-by-segment
1067+ /// walk honouring `*` (single segment) and `**` (any number of segments).
1068+ ///
1069+ /// Matching is bounded to depth 3 and 4096 total directory entries per
1070+ /// workspace root to keep the cost sub-100 ms on large monorepos. Anything
1071+ /// deeper is rare for activation-event triggers; the trade-off is
1072+ /// documented in VS Code's own `ExtensionService.scanExtensions`.
1073+ fn FindMatchingWorkspaceContainsPatterns (
1074+ Folders : & [ std:: path:: PathBuf ] ,
1075+ Patterns : & [ String ] ,
1076+ ) -> Vec < String > {
1077+ use std:: collections:: HashSet ;
1078+
1079+ const MAX_DEPTH : usize = 3 ;
1080+ const MAX_ENTRIES_PER_ROOT : usize = 4096 ;
1081+
1082+ let mut Matched : HashSet < String > = HashSet :: new ( ) ;
1083+ for Folder in Folders {
1084+ if !Folder . is_dir ( ) {
1085+ continue ;
1086+ }
1087+ // Collect up to MAX_ENTRIES_PER_ROOT paths relative to the folder.
1088+ let mut Entries : Vec < String > = Vec :: new ( ) ;
1089+ let mut Stack : Vec < ( std:: path:: PathBuf , usize ) > = vec ! [ ( Folder . clone( ) , 0 ) ] ;
1090+ while let Some ( ( Current , Depth ) ) = Stack . pop ( ) {
1091+ if Entries . len ( ) >= MAX_ENTRIES_PER_ROOT {
1092+ break ;
1093+ }
1094+ let ReadDirResult = std:: fs:: read_dir ( & Current ) ;
1095+ let ReadDir = match ReadDirResult {
1096+ Ok ( R ) => R ,
1097+ Err ( _) => continue ,
1098+ } ;
1099+ for Entry in ReadDir . flatten ( ) {
1100+ if Entries . len ( ) >= MAX_ENTRIES_PER_ROOT {
1101+ break ;
1102+ }
1103+ let Path = Entry . path ( ) ;
1104+ let Relative = match Path . strip_prefix ( Folder ) {
1105+ Ok ( R ) => R . to_string_lossy ( ) . replace ( '\\' , "/" ) ,
1106+ Err ( _) => continue ,
1107+ } ;
1108+ let IsDir = Entry . file_type ( ) . map ( |T | T . is_dir ( ) ) . unwrap_or ( false ) ;
1109+ Entries . push ( Relative . clone ( ) ) ;
1110+ if IsDir && Depth + 1 < MAX_DEPTH {
1111+ Stack . push ( ( Path , Depth + 1 ) ) ;
1112+ }
1113+ }
1114+ }
1115+
1116+ for Pattern in Patterns {
1117+ if Matched . contains ( Pattern ) {
1118+ continue ;
1119+ }
1120+ if PatternMatchesAnyEntry ( Pattern , & Entries ) {
1121+ Matched . insert ( Pattern . clone ( ) ) ;
1122+ }
1123+ }
1124+ }
1125+ Matched . into_iter ( ) . collect ( )
1126+ }
1127+
1128+ /// Very small glob-matcher scoped to VS Code `workspaceContains:` syntax.
1129+ /// Supports literal paths, `*` (one path segment), and `**` (zero or more
1130+ /// segments). Case-sensitive per the VS Code spec.
1131+ fn PatternMatchesAnyEntry ( Pattern : & str , Entries : & [ String ] ) -> bool {
1132+ let HasWildcard = Pattern . contains ( '*' ) || Pattern . contains ( '?' ) ;
1133+ if !HasWildcard {
1134+ return Entries . iter ( ) . any ( |E | E == Pattern ) ;
1135+ }
1136+ let PatternSegments : Vec < & str > = Pattern . split ( '/' ) . collect ( ) ;
1137+ Entries . iter ( ) . any ( |E | SegmentMatch ( & PatternSegments , & E . split ( '/' ) . collect :: < Vec < _ > > ( ) ) )
1138+ }
1139+
1140+ fn SegmentMatch ( Pattern : & [ & str ] , Entry : & [ & str ] ) -> bool {
1141+ if Pattern . is_empty ( ) {
1142+ return Entry . is_empty ( ) ;
1143+ }
1144+ let Head = Pattern [ 0 ] ;
1145+ if Head == "**" {
1146+ // `**` matches zero or more segments. Try consuming 0..=entry.len().
1147+ for Consumed in 0 ..=Entry . len ( ) {
1148+ if SegmentMatch ( & Pattern [ 1 ..] , & Entry [ Consumed ..] ) {
1149+ return true ;
1150+ }
1151+ }
1152+ return false ;
1153+ }
1154+ if Entry . is_empty ( ) {
1155+ return false ;
1156+ }
1157+ if SingleSegmentMatch ( Head , Entry [ 0 ] ) {
1158+ return SegmentMatch ( & Pattern [ 1 ..] , & Entry [ 1 ..] ) ;
1159+ }
1160+ false
1161+ }
1162+
1163+ fn SingleSegmentMatch ( Pattern : & str , Segment : & str ) -> bool {
1164+ if Pattern == "*" {
1165+ return true ;
1166+ }
1167+ if !Pattern . contains ( '*' ) && !Pattern . contains ( '?' ) {
1168+ return Pattern == Segment ;
1169+ }
1170+ // Minimal star-glob on a single segment: split by '*' and check each
1171+ // fragment appears in order. Doesn't support `?` (rare in
1172+ // workspaceContains patterns); unsupported glob chars fall through to
1173+ // literal equality.
1174+ let Fragments : Vec < & str > = Pattern . split ( '*' ) . collect ( ) ;
1175+ let mut Cursor = 0usize ;
1176+ for ( Index , Fragment ) in Fragments . iter ( ) . enumerate ( ) {
1177+ if Fragment . is_empty ( ) {
1178+ continue ;
1179+ }
1180+ if Index == 0 {
1181+ if !Segment [ Cursor ..] . starts_with ( Fragment ) {
1182+ return false ;
1183+ }
1184+ Cursor += Fragment . len ( ) ;
1185+ continue ;
1186+ }
1187+ match Segment [ Cursor ..] . find ( Fragment ) {
1188+ Some ( Offset ) => Cursor += Offset + Fragment . len ( ) ,
1189+ None => return false ,
1190+ }
1191+ }
1192+ if let Some ( Last ) = Fragments . last ( )
1193+ && !Last . is_empty ( )
1194+ {
1195+ return Segment . ends_with ( Last ) ;
1196+ }
1197+ true
1198+ }
0 commit comments