Skip to content

Commit 9e6f93e

Browse files
fix(mothership): rebuild trigger/integration overviews per viewer
triggers.md and the oauth/api-key integration summaries were baked once into the process-global static component cache under null visibility, so a revealed-preview viewer got the per-trigger schema file but the trigger was permanently missing from the overview catalog. Move these three aggregate files out of getStaticComponentFiles and rebuild them per materialize in buildViewerScopedComponentFiles, filtered through the viewer's visibility (isHiddenUnder / isStaticFileHidden / filterExposedIntegrationTools) so an overview entry appears exactly when its per-file schema does. Per-file schemas stay in the shared ungated cache, include/exclude-filtered at stamp time as before. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QCHVAfWcDYNQFbNMemQJwK
1 parent 103ff7c commit 9e6f93e

1 file changed

Lines changed: 89 additions & 70 deletions

File tree

apps/sim/lib/copilot/vfs/workspace-vfs.ts

Lines changed: 89 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ import {
2525
} from '@/lib/copilot/chat/workspace-context'
2626
import { TraceAttr } from '@/lib/copilot/generated/trace-attributes-v1'
2727
import { 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'
2932
import { recordVfsMaterialize } from '@/lib/copilot/request/metrics'
3033
import { markSpanForError } from '@/lib/copilot/request/otel'
3134
import { 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

Comments
 (0)