context.ts, capability-types.ts, event-types.ts) as the foundation. r206 has NOT been shipped — no commits, no bulletins, no files matching those names exist in any of the 7 repos.r207 mounts PreviewHostVSIL (a789 §5.2, structural in a791 §4) and the inspector host (a788) into a single PM2-friendly mount. It uses the r206 contract for StudioContext adapter, capability types, and event types.
5 new files, ~600 LoC at studio/modules/vibecoder/src/host/:
PreviewHostMount.tsx (~150 LoC) — PM2-mountable PreviewHostVSIL hostInspectorHostMount.tsx (~120 LoC) — PM2-mountable inspector hostCompositionMount.tsx (~180 LoC) — r205 Composition Graph Host mountmountAll.ts (~80 LoC) — single-entry mount for all 3 hosts__tests__/mount.test.ts (~80 LoC) — 5 test casesDepends on r206 (3 files, ~250 LoC, to be shipped before r207 implementation):
studio/modules/vibecoder/src/host/context.ts (~100 LoC) — StudioContext type + adapterstudio/modules/vibecoder/src/host/capability-types.ts (~80 LoC) — Capability type + *_CAPABILITIES manifestsstudio/modules/vibecoder/src/host/event-types.ts (~70 LoC) — typed event names + payload interfacesNo new pact files. r207 is structural, not constitutional.
*_CAPABILITIES pattern)context.ts — StudioContext type + adapter// studio/modules/vibecoder/src/host/context.ts
import type { ComponentType } from 'react';
export interface StudioContext {
/** The active workspace id (e.g. 'vibescope', 'vibecoder', 'freshcards', 'origin'). */
readonly activeWorkspace: string;
/** The active surface id within the active workspace. */
readonly activeSurface: string;
/** The currently selected asset within the active surface. */
readonly activeAssetId?: string;
/** The 5-state surface machine (a789 §9). */
readonly surfaceState: 'main' | 'working' | 'draft' | 'preview' | 'origin';
/** The Gallery Preview Bridge (a789 §5.1). */
readonly bridge: GalleryPreviewBridge;
/** The Preview Strip (a789 §5.6). */
readonly strip: PreviewStrip;
/** The VSIL store (single source per workspace). */
readonly store: VSILStore;
/** The composition event bus (1-file EventTarget, r205 §1.4). */
readonly bus: EventTarget;
}
/** Throws if not inside a <StudioContextProvider>. */
export function useStudioContext(): StudioContext;
/** Mount once at the top of the VibeCoder host tree. */
export function StudioContextProvider(props: {
value: StudioContext;
children: React.ReactNode;
}): React.ReactElement;
/** No-op provider for testing. */
export function makeDefaultStudioContext(): StudioContext;
capability-types.ts — Capability manifests// studio/modules/vibecoder/src/host/capability-types.ts
export interface Capability {
readonly id: string;
readonly version: string;
readonly can: Readonly<Record<string, boolean>>;
readonly needs: Readonly<Record<string, 'required' | 'optional' | 'none'>>;
readonly contextRules: {
readonly showInWorkspace: readonly string[];
readonly showInContext: readonly string[];
readonly showInMode: readonly ('normal' | 'half' | 'full')[];
};
readonly listens: readonly string[];
readonly emits: readonly string[];
readonly availableSlots?: readonly string[];
}
export function defineCapability<T extends Capability>(c: T): Readonly<T>;
event-types.ts — typed event contract v2// studio/modules/vibecoder/src/host/event-types.ts
import type { RegionId } from '../vibescope/src/composition/types';
import type { VSILSource } from '../vibescope/src/scrubber/types';
export const EVENT_NAMES = Object.freeze({
STUDIO_CONTEXT_CHANGED: 'studio:context-changed',
STUDIO_ASSET_BOUND: 'studio:asset-bound',
STUDIO_SURFACE_STATE_CHANGED: 'studio:surface-state-changed',
STUDIO_DEPLOY_COMPLETED: 'studio:deploy-completed',
STUDIO_CINEMATIC_MODE_CHANGED: 'studio:cinematic-mode-changed',
GALLERY_ACTIVE_PREVIEW_CHANGED: 'gallery:active-preview-changed',
GALLERY_PREVIEW_LOADED: 'gallery:preview-loaded',
GALLERY_PREVIEW_FAILED: 'gallery:preview-failed',
COMPOSITION_LOADED: 'composition:loaded',
COMPOSITION_REGION_CLICKED: 'composition:region-clicked',
COMPOSITION_REGION_PARSE_FAILED: 'composition:region-parse-failed',
COMPOSITION_REGION_METADATA_CHANGED: 'composition:region-metadata-changed',
COMPOSITION_EDIT_REJECTED: 'composition:edit-rejected',
INSPECTOR_SOURCE_CHANGED: 'inspector:source-changed',
INSPECTOR_SUB_MODE_CHANGED: 'inspector:sub-mode-changed',
PREVIEW_HOST_MOUNTED: 'preview-host:mounted',
PREVIEW_HOST_UNMOUNTED: 'preview-host:unmounted',
PREVIEW_HOST_ERROR: 'preview-host:error',
} as const);
export type EventName = (typeof EVENT_NAMES)[keyof typeof EVENT_NAMES];
export interface StudioContextChangedPayload {
readonly previous: { workspace: string; surface: string };
readonly current: { workspace: string; surface: string };
}
export interface StudioAssetBoundPayload {
readonly assetId: string;
readonly surfaceId: string;
readonly vsilSource?: VSILSource;
}
export interface StudioSurfaceStateChangedPayload {
readonly surfaceId: string;
readonly fromState: 'main' | 'working' | 'draft' | 'preview' | 'origin';
readonly toState: 'main' | 'working' | 'draft' | 'preview' | 'origin';
readonly content?: unknown;
}
export interface CompositionRegionClickedPayload {
readonly regionId: RegionId;
readonly surfaceState: 'main' | 'working' | 'draft' | 'preview' | 'origin';
}
export interface CompositionEditRejectedPayload {
readonly regionId: RegionId;
readonly surfaceState: 'main' | 'working' | 'draft' | 'preview' | 'origin';
readonly reason: 'read-only-state' | 'invalid-transform' | 'region-not-found' | 'busy';
}
export interface PreviewHostMountedPayload {
readonly hostId: string;
readonly slot: string;
readonly surfaceId: string;
}
export interface PreviewHostErrorPayload {
readonly hostId: string;
readonly slot: string;
readonly error: string;
readonly recoverable: boolean;
}
export interface EventMap {
[EVENT_NAMES.STUDIO_CONTEXT_CHANGED]: StudioContextChangedPayload;
[EVENT_NAMES.STUDIO_ASSET_BOUND]: StudioAssetBoundPayload;
[EVENT_NAMES.STUDIO_SURFACE_STATE_CHANGED]: StudioSurfaceStateChangedPayload;
[EVENT_NAMES.GALLERY_ACTIVE_PREVIEW_CHANGED]: { from: string; to: string };
[EVENT_NAMES.COMPOSITION_REGION_CLICKED]: CompositionRegionClickedPayload;
[EVENT_NAMES.COMPOSITION_EDIT_REJECTED]: CompositionEditRejectedPayload;
[EVENT_NAMES.COMPOSITION_LOADED]: { regionCount: number; durationMs: number };
[EVENT_NAMES.PREVIEW_HOST_MOUNTED]: PreviewHostMountedPayload;
[EVENT_NAMES.PREVIEW_HOST_UNMOUNTED]: { hostId: string };
[EVENT_NAMES.PREVIEW_HOST_ERROR]: PreviewHostErrorPayload;
}
export function dispatchTyped<K extends keyof EventMap>(
bus: EventTarget, name: K, detail: EventMap[K]
): boolean;
export function subscribeTyped<K extends keyof EventMap>(
bus: EventTarget, name: K, handler: (detail: EventMap[K]) => void
): () => void;
PreviewHostMount.tsx (~150 LoC)PM2-mountable wrapper around PreviewHostVSIL (a789 §5.2). Reads StudioContext via useStudioContext(), subscribes to the 4 preview-relevant events, re-renders on context changes.
import { useStudioContext } from './context';
import { subscribeTyped, EVENT_NAMES } from './event-types';
import { PreviewHostVSIL } from '../vibescope/src/preview/PreviewHostVSIL';
export interface PreviewHostMountProps {
hostId: string;
slot: string;
initialSource?: VSILSource;
}
export function PreviewHostMount(props: PreviewHostMountProps): React.ReactElement {
const ctx = useStudioContext();
const [source, setSource] = useState<VSILSource>(props.initialSource ?? ctx.store.getState().source);
const [selection, setSelection] = useState<readonly string[]>([]);
useEffect(() => subscribeTyped(ctx.bus, EVENT_NAMES.STUDIO_ASSET_BOUND, (detail) => {
if (detail.surfaceId === ctx.activeSurface && detail.vsilSource) {
setSource(detail.vsilSource);
}
}), [ctx.bus, ctx.activeSurface]);
useEffect(() => {
ctx.bus.dispatchEvent(new CustomEvent(EVENT_NAMES.PREVIEW_HOST_MOUNTED, {
detail: { hostId: props.hostId, slot: props.slot, surfaceId: ctx.activeSurface }
}));
return () => {
ctx.bus.dispatchEvent(new CustomEvent(EVENT_NAMES.PREVIEW_HOST_UNMOUNTED, {
detail: { hostId: props.hostId }
}));
};
}, [ctx.bus, ctx.activeSurface, props.hostId, props.slot]);
return <PreviewHostVSIL
source={source}
selectedLayerIds={selection}
onLayerClick={(layerId) => setSelection([layerId])}
/>;
}
InspectorHostMount.tsx (~120 LoC)PM2-mountable wrapper around the inspector (a788). Bound to inspector:source-changed + studio:surface-state-changed.
import { useStudioContext } from './context';
import { subscribeTyped, EVENT_NAMES } from './event-types';
import { VSILInspectorPanel } from '../vibescope/src/inspector/VSILInspectorPanel';
export interface InspectorHostMountProps {
hostId: string;
initialSubMode?: 'ast' | 'props' | 'state' | 'logs' | 'metadata';
}
export function InspectorHostMount(props: InspectorHostMountProps): React.ReactElement {
const ctx = useStudioContext();
const [source, setSource] = useState<InspectorSource | null>(null);
const [subMode, setSubMode] = useState(props.initialSubMode ?? 'ast');
useEffect(() => subscribeTyped(ctx.bus, EVENT_NAMES.INSPECTOR_SOURCE_CHANGED,
(detail) => setSource(detail)),
[ctx.bus]);
if (!source) {
return <div data-inspector-empty>No source bound. Select a layer.</div>;
}
return <VSILInspectorPanel source={source} initialSubMode={subMode} onSubModeChange={setSubMode} />;
}
CompositionMount.tsx (~180 LoC)PM2-mountable wrapper around the r205 Composition Graph Host (a791). Subscribes to composition events + region events.
import { useStudioContext } from './context';
import { subscribeTyped, EVENT_NAMES } from './event-types';
import { CompositionGraphHost } from '../vibescope/src/composition/composition/host';
export interface CompositionMountProps {
hostId: string;
initialComposition?: Composition;
}
export function CompositionMount(props: CompositionMountProps): React.ReactElement {
const ctx = useStudioContext();
const [composition, setComposition] = useState<Composition | null>(props.initialComposition ?? null);
const [selectedRegionIds, setSelectedRegionIds] = useState<readonly RegionId[]>([]);
useEffect(() => subscribeTyped(ctx.bus, EVENT_NAMES.COMPOSITION_LOADED,
(detail) => setComposition(detail.composition)),
[ctx.bus]);
useEffect(() => subscribeTyped(ctx.bus, EVENT_NAMES.COMPOSITION_REGION_CLICKED,
(detail) => setSelectedRegionIds([detail.regionId])),
[ctx.bus]);
if (!composition) {
return <div data-composition-empty>No composition loaded.</div>;
}
return <CompositionGraphHost
composition={composition}
selectedRegionIds={selectedRegionIds}
onRegionClick={(regionId) => {
ctx.bus.dispatchEvent(new CustomEvent(EVENT_NAMES.COMPOSITION_REGION_CLICKED, {
detail: { regionId, surfaceState: ctx.surfaceState }
}));
}}
context={{ bridge: ctx.bridge, strip: ctx.strip, studioContext: ctx, store: ctx.store, bus: ctx.bus }}
/>;
}
mountAll.ts (~80 LoC)Single entry point mounting all 3 hosts with frozen *_CAPABILITIES manifests for Edge Panel.
import { defineCapability } from './capability-types';
import { PreviewHostMount } from './PreviewHostMount';
import { InspectorHostMount } from './InspectorHostMount';
import { CompositionMount } from './CompositionMount';
export const PREVIEW_HOST_CAPABILITY = defineCapability({
id: 'preview-host',
version: '0.1.0',
can: { previewVSIL: true, previewComposition: true, switchSlots: true },
needs: { surfaceId: 'required', vsilSource: 'optional' },
contextRules: {
showInWorkspace: ['vibecoder', 'vibescope', 'freshcards', 'origin'],
showInContext: ['surface-editing', 'asset-selected'],
showInMode: ['normal', 'half', 'full'],
},
listens: ['studio:asset-bound', 'studio:surface-state-changed', 'composition:loaded'],
emits: ['preview-host:mounted', 'preview-host:unmounted', 'preview-host:error'],
});
export const INSPECTOR_HOST_CAPABILITY = defineCapability({ /* ... */ });
export const COMPOSITION_HOST_CAPABILITY = defineCapability({ /* ... */ });
export function mountAllHosts(ctx: StudioContext): {
preview: PreviewHostMount;
inspector: InspectorHostMount;
composition: CompositionMount;
} {
return {
preview: <PreviewHostMount hostId="preview-1" slot="vsil-cinematic" />,
inspector: <InspectorHostMount hostId="inspector-1" />,
composition: <CompositionMount hostId="composition-1" />,
};
}
__tests__/mount.test.ts (~80 LoC)5 test cases: PreviewHostMount re-renders on studio:asset-bound · InspectorHostMount empty state · CompositionMount re-renders on composition:region-clicked · mountAll returns all 3 mounts · capability manifests are frozen.
Composition Graph Host (a791 §1)
└─ user clicks a region
└─→ ctx.bus.dispatchEvent('composition:region-clicked', { regionId, surfaceState })
├─→ InspectorHostMount subscribes (r207 §2.2)
│ └─→ updates selectedRegionIds
├─→ VibeChat subscribes (separate)
│ └─→ tailors response to the clicked region
└─→ Edge Panel subscribes (separate)
└─→ shows "Open Inspector for this region" tile
Composition Mount (r207 §2.3) receives composition via props.initialComposition OR composition:loaded event. Dispatches composition:region-clicked on user clicks. Re-renders on studio:surface-state-changed.
useEffect dispatches preview-host:mounted with { hostId, slot, surfaceId }studio:asset-bound, studio:surface-state-changed, composition:loadedErrorBoundary, dispatches preview-host:erroruseEffect cleanup dispatches preview-host:unmountedThe adapter is provided by the VibeCoder host (the top-level mount in avidtech6/vibecoder-standalone). The host:
StudioContextProvider with current workspace, surface, surface stateGalleryPreviewBridge (from a789) and passes it to contextVSILStore (singleton per workspace) and passes itbus = new EventTarget() (1-file, no deps) and passes itmountAllHosts(ctx)) under the providerWhy this is r206, not r207: the adapter is the foundation. r207 ASSUMES r206 is shipped. If r206 is not shipped when r207 implementation starts, ship r206 first (3 files, ~250 LoC, 1-2 days). Default action.
/workspace/freshvibestudio/studio/modules/vibecoder/src/cards/InspectorCard.tsx (278 LoC, from a788) — uses useVSILSource() (module-scoped), no event subscription, only mounts inside the VibeScope workspace.
InspectorHostMount (r207 §2.2) — uses useStudioContext() (host-scoped), subscribes to typed events (r206), mounts in any chrome (PM2, Edge Panel, Gallery), READ-ONLY invariant preserved.
InspectorCard.tsx rewritten as a thin wrapper (~15 LoC):
// studio/modules/vibecoder/src/cards/InspectorCard.tsx (rewritten)
import { useStudioContext } from '../host/context';
import { InspectorHostMount } from '../host/InspectorHostMount';
export const InspectorCard: React.FC = () => {
const ctx = useStudioContext();
return <InspectorHostMount hostId="inspector-card" initialSubMode="ast" />;
};
| Aspect | v1 (a789 / a791) | v2 (r207) |
|---|---|---|
| Event name | string literal | typed EventName from EVENT_NAMES registry |
| Event payload | any (untyped) | typed interface in EventMap |
| Dispatch | bus.dispatchEvent(new CustomEvent(name, { detail })) | dispatchTyped(bus, name, detail) |
| Subscribe | bus.addEventListener(name, handler) | subscribeTyped(bus, name, handler) |
| Type safety | none | full — TypeScript errors at compile time |
Backwards compatibility: v1 events still work. dispatchTyped and subscribeTyped are thin wrappers around the native EventTarget API. v1 callers can be migrated incrementally. Event names unchanged from a789 / a791 — v2 only ADDS typed contracts.
SurfaceRegistry + SurfaceLifecycleManager consumed by StudioContext.surfaceState (r206)VSILInspectorPanel (688 LoC) is the consumer of InspectorHostMount (r207)GalleryPreviewBridge is part of StudioContext (r206)CompositionGraphHost is the consumer of CompositionMount (r207)Capability type is the contract for capability-types.ts (r206)VSILPanel.tsx (5-tab UI, b1269) is the consumer of PreviewHostMount (r207)| Pact / source | Drift | Severity |
|---|---|---|
FvW v8 / pact/platform/vsil/vsil.md | no StudioContext concept | low (constitutional, not a feature) |
| a789 / a791 event names | untyped events (v1) | medium (r207 v2 fixes) |
| a788 inspector (InspectorCard stub) | module-scoped useVSILSource | medium (r207 migration) |
| a789 §5.1 GalleryPreviewBridge | has Composition support (per r205) | none (r207 extends) |
| ID10 v2 capability layer | no Capability type (uses ad-hoc manifests) | low (r206 standardises) |
No new pact files required for r207. r207 is structural + integration; no constitutional concepts added.
| # | Step | Time | Depends on | Risk |
|---|---|---|---|---|
| 1 | Ship r206 first | 1-2 days | — | low |
| 2 | PreviewHostMount.tsx | 3h | r206 | low |
| 3 | InspectorHostMount.tsx | 2h | r206 | low |
| 4 | CompositionMount.tsx | 4h | r206, r205 (a791) | medium |
| 5 | mountAll.ts | 2h | steps 2-4 | low |
| 6 | __tests__/mount.test.ts | 3h | steps 2-5 | low |
| 7 | InspectorCard.tsx migration | 1h | step 3 | low |
| 8 | Playwright smoke | 2h | steps 1-7 | low |
Total: 18-19h after r206 ships. Or 6-7 days end-to-end if r206 is included.
r207 binds to r206. r206 doesn't exist yet. This plan defines the r206 contract (3 files, ~250 LoC, 1-2 days) AND the r207 mount logic (5 files, ~600 LoC, 2-3 days). The operator's directive assumes r206 is shipped; it isn't. Default action: ship r206 first, then r207, no parallel work.
| # | Question | Default if no answer |
|---|---|---|
| Q1 | r206 order: ship r206 first (1-2d) then r207 (2-3d)? — OR inline r206 in r207 (anti-pattern)? | ship r206 first |
| Q2 | Event contract v2 scope: just type events, or also namespacing (@vibecoder/event-types package)? | just type, no namespacing |
| Q3 | InspectorHostMount adapter: in v1, also consume r205 CompositionGraphHost events (auto-show region metadata on region click)? | yes, in v1 |
SurfaceRegistry + SurfaceLifecycleManagerVSILInspectorPanel + READ-ONLY invariantGalleryPreviewBridge + PreviewStripCompositionGraphHost