@@ -25,7 +25,10 @@ import {
2525} from '@/lib/copilot/chat/workspace-context'
2626import { TraceAttr } from '@/lib/copilot/generated/trace-attributes-v1'
2727import { TraceSpan } from '@/lib/copilot/generated/trace-spans-v1'
28- import { getExposedIntegrationTools } from '@/lib/copilot/integration-tools'
28+ import {
29+ filterExposedIntegrationTools ,
30+ getExposedIntegrationTools ,
31+ } from '@/lib/copilot/integration-tools'
2932import { recordVfsMaterialize } from '@/lib/copilot/request/metrics'
3033import { markSpanForError } from '@/lib/copilot/request/otel'
3134import { compileDoc , getE2BDocFormat } from '@/lib/copilot/tools/server/files/doc-compile'
@@ -219,9 +222,11 @@ function getStaticComponentFiles(): Map<string, string> {
219222
220223 // Raw registry, never the visibility-projected getAllBlocks: this map is a
221224 // process-global shared cache, so it must hold the deterministic ungated
222- // universe. Preview blocks get schema files here (path-filterable at stamp
223- // time for revealed viewers) but are EXCLUDED from the shared aggregate
224- // files (overviews, oauth/api-key summaries) that all viewers receive.
225+ // universe. Per-file schema entries (block/integration/trigger) are stamped
226+ // ungated here and include/exclude-filtered per viewer at stamp time. Files
227+ // whose CONTENT summarizes a viewer-dependent set (triggers.md, the oauth/
228+ // api-key summaries) are NOT built here — {@link buildViewerScopedComponentFiles}
229+ // rebuilds them per materialize so a revealed preview block shows up in them.
225230 const allBlocks = Object . values ( BLOCK_REGISTRY )
226231 const visibleBlocks = allBlocks . filter ( ( b ) => ! b . hideFromToolbar )
227232
@@ -234,9 +239,6 @@ function getStaticComponentFiles(): Map<string, string> {
234239
235240 let integrationCount = 0
236241
237- const oauthServices = new Map < string , { provider : string ; operations : string [ ] } > ( )
238- const apiKeyServices = new Map < string , { params : string [ ] ; operations : string [ ] } > ( )
239-
240242 // Integration tools come from the shared exposed-tool set (latest version of
241243 // each operation owned by a visible block), the same set used to build the
242244 // deferred callable tools — so discovery and execution can never drift.
@@ -246,43 +248,8 @@ function getStaticComponentFiles(): Map<string, string> {
246248 files . set ( path , serializeIntegrationSchema ( tool ) )
247249 integrationPathOwners . set ( path , { type : blockType , preview } )
248250 integrationCount ++
249-
250- // Preview-owned tools stay out of the shared oauth/api-key aggregates —
251- // those files are identical for every viewer.
252- if ( preview ) continue
253-
254- if ( tool . oauth ?. required ) {
255- const existing = oauthServices . get ( service )
256- if ( existing ) {
257- existing . operations . push ( operation )
258- } else {
259- oauthServices . set ( service , { provider : tool . oauth . provider , operations : [ operation ] } )
260- }
261- } else if ( tool . hosting ?. apiKeyParam ) {
262- const existing = apiKeyServices . get ( service )
263- if ( existing ) {
264- if ( ! existing . params . includes ( tool . hosting . apiKeyParam ) ) {
265- existing . params . push ( tool . hosting . apiKeyParam )
266- }
267- existing . operations . push ( operation )
268- } else {
269- apiKeyServices . set ( service , {
270- params : [ tool . hosting . apiKeyParam ] ,
271- operations : [ operation ] ,
272- } )
273- }
274- }
275251 }
276252
277- files . set (
278- 'environment/oauth-integrations.json' ,
279- JSON . stringify ( Object . fromEntries ( oauthServices ) , null , 2 )
280- )
281- files . set (
282- 'environment/api-key-integrations.json' ,
283- JSON . stringify ( Object . fromEntries ( apiKeyServices ) , null , 2 )
284- )
285-
286253 files . set (
287254 'components/blocks/loop.json' ,
288255 JSON . stringify (
@@ -390,34 +357,6 @@ function getStaticComponentFiles(): Map<string, string> {
390357 externalTriggerCount ++
391358 }
392359
393- files . set (
394- 'components/triggers/triggers.md' ,
395- serializeTriggerOverview (
396- // The overview is a shared file — preview trigger blocks stay out of it
397- // (their per-type schema file remains discoverable for revealed viewers).
398- builtinTriggerBlocks
399- . filter ( ( b ) => ! b . preview )
400- . map ( ( b ) => ( {
401- id : b . type ,
402- name : b . name ,
403- provider : 'sim' ,
404- description : b . description ,
405- } ) ) ,
406- // Same for external triggers: a trigger owned solely by preview blocks is
407- // hidden under the null (no-viewer) state this shared file is built with.
408- Object . entries ( TRIGGER_REGISTRY )
409- . filter (
410- ( [ id , t ] ) => ! isStaticFileHidden ( `components/triggers/${ t . provider } /${ id } .json` , null )
411- )
412- . map ( ( [ id , t ] ) => ( {
413- id,
414- name : t . name ,
415- provider : t . provider ,
416- description : t . description ,
417- } ) )
418- )
419- )
420-
421360 logger . info ( 'Static component files built' , {
422361 blocks : visibleBlocks . length ,
423362 blocksFiltered,
@@ -431,6 +370,80 @@ function getStaticComponentFiles(): Map<string, string> {
431370 return staticComponentFiles
432371}
433372
373+ /**
374+ * Overview/aggregate component files whose CONTENT depends on the viewer's
375+ * visibility. A per-file schema entry can be gated by include/exclude (the
376+ * shared build holds it ungated; the stamp loop drops it for viewers who can't
377+ * see it), but an overview summarizes a viewer-dependent SET into a single file
378+ * — include/exclude can't express "this viewer sees a different list". So these
379+ * are rebuilt per materialize from the ungated registries filtered through
380+ * `vis`, which is what lets a revealed preview block appear in the overview for
381+ * the viewer who revealed it, matching the per-file schema its stamp filter
382+ * already exposes.
383+ *
384+ * Reuses the same predicates the stamp filter uses ({@link isHiddenUnder} for
385+ * builtin trigger blocks, {@link isStaticFileHidden} for external triggers,
386+ * {@link filterExposedIntegrationTools} for integration tools), so an overview
387+ * entry appears exactly when its per-file schema does. Depends on the owner maps
388+ * populated by {@link getStaticComponentFiles}, so it forces that build first
389+ * (idempotent — normally already warm by materialize time).
390+ */
391+ function buildViewerScopedComponentFiles ( vis : BlockVisibilityState | null ) : Map < string , string > {
392+ getStaticComponentFiles ( )
393+
394+ const files = new Map < string , string > ( )
395+
396+ const oauthServices = new Map < string , { provider : string ; operations : string [ ] } > ( )
397+ const apiKeyServices = new Map < string , { params : string [ ] ; operations : string [ ] } > ( )
398+ for ( const { config : tool , service, operation } of filterExposedIntegrationTools (
399+ getExposedIntegrationTools ( ) ,
400+ vis
401+ ) ) {
402+ if ( tool . oauth ?. required ) {
403+ const existing = oauthServices . get ( service )
404+ if ( existing ) {
405+ existing . operations . push ( operation )
406+ } else {
407+ oauthServices . set ( service , { provider : tool . oauth . provider , operations : [ operation ] } )
408+ }
409+ } else if ( tool . hosting ?. apiKeyParam ) {
410+ const existing = apiKeyServices . get ( service )
411+ if ( existing ) {
412+ if ( ! existing . params . includes ( tool . hosting . apiKeyParam ) ) {
413+ existing . params . push ( tool . hosting . apiKeyParam )
414+ }
415+ existing . operations . push ( operation )
416+ } else {
417+ apiKeyServices . set ( service , {
418+ params : [ tool . hosting . apiKeyParam ] ,
419+ operations : [ operation ] ,
420+ } )
421+ }
422+ }
423+ }
424+ files . set (
425+ 'environment/oauth-integrations.json' ,
426+ JSON . stringify ( Object . fromEntries ( oauthServices ) , null , 2 )
427+ )
428+ files . set (
429+ 'environment/api-key-integrations.json' ,
430+ JSON . stringify ( Object . fromEntries ( apiKeyServices ) , null , 2 )
431+ )
432+
433+ const builtinTriggers = Object . values ( BLOCK_REGISTRY )
434+ . filter ( ( b ) => b . category === 'triggers' && ! isHiddenUnder ( vis , b ) )
435+ . map ( ( b ) => ( { id : b . type , name : b . name , provider : 'sim' , description : b . description } ) )
436+ const externalTriggers = Object . entries ( TRIGGER_REGISTRY )
437+ . filter ( ( [ id , t ] ) => ! isStaticFileHidden ( `components/triggers/${ t . provider } /${ id } .json` , vis ) )
438+ . map ( ( [ id , t ] ) => ( { id, name : t . name , provider : t . provider , description : t . description } ) )
439+ files . set (
440+ 'components/triggers/triggers.md' ,
441+ serializeTriggerOverview ( builtinTriggers , externalTriggers )
442+ )
443+
444+ return files
445+ }
446+
434447/**
435448 * Virtual Filesystem that materializes workspace data into an in-memory Map.
436449 *
@@ -732,6 +745,12 @@ export class WorkspaceVFS {
732745 if ( isStaticFileHidden ( path , blockVisibility ) ) continue
733746 this . files . set ( path , content )
734747 }
748+ // Overview files summarize a viewer-dependent set, so their CONTENT
749+ // (not just presence) is rebuilt per viewer — the stamp loop above
750+ // can only include/exclude whole files, not re-derive a list.
751+ for ( const [ path , content ] of buildViewerScopedComponentFiles ( blockVisibility ) ) {
752+ this . files . set ( path , content )
753+ }
735754
736755 span . setAttributes ( {
737756 [ TraceAttr . CopilotVfsMaterializeFileCount ] : this . files . size ,
0 commit comments