ID9 — r207 PreviewHost + Inspector Mount — Structural Plan

Operator directive (2026-08-30) · Discovery-only · No code · No pact edits · No extraction
Thread: vibecoder-standalone-mavis (9) · Workspace: /workspace/ID9_R207_PREVIEWHOST_AND_INSPECTOR_MOUNT.md

⚠️ PRE-FLIGHT FINDING: r206 NOT SHIPPED.
The operator's directive references r206 (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.
Implication: r207 is being kicked off before its dependency. This plan defines both the r206 contract and the r207 mount logic so r206 can ship first, then r207 binds to it. No parallel work.

TL;DR

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/:

Depends on r206 (3 files, ~250 LoC, to be shipped before r207 implementation):

No new pact files. r207 is structural, not constitutional.

Constraints honored


1. The r206 contract (defined here, to be implemented before r207)

1.1 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;

1.2 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>;

1.3 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;

2. r207 files (the actual mount logic)

2.1 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])}
  />;
}

2.2 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} />;
}

2.3 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 }}
  />;
}

2.4 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" />,
  };
}

2.5 __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.


3. Wiring (region events, composition events, preview host lifecycle)

3.1 Region events

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

3.2 Composition events

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.

3.3 Preview host lifecycle

  1. Mount — useEffect dispatches preview-host:mounted with { hostId, slot, surfaceId }
  2. Active — listens to studio:asset-bound, studio:surface-state-changed, composition:loaded
  3. Error — catches via React ErrorBoundary, dispatches preview-host:error
  4. Unmount — useEffect cleanup dispatches preview-host:unmounted

4. StudioContext adapter (r206)

The adapter is provided by the VibeCoder host (the top-level mount in avidtech6/vibecoder-standalone). The host:

  1. Creates a StudioContextProvider with current workspace, surface, surface state
  2. Creates a GalleryPreviewBridge (from a789) and passes it to context
  3. Creates a VSILStore (singleton per workspace) and passes it
  4. Creates a bus = new EventTarget() (1-file, no deps) and passes it
  5. Mounts all 3 hosts (mountAllHosts(ctx)) under the provider

Why 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.


5. Replace the inspector-card stub

5.1 What the stub is

/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.

5.2 What the real mount is

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.

5.3 Migration

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" />;
};

6. Event contract v2 (summary)

Aspectv1 (a789 / a791)v2 (r207)
Event namestring literaltyped EventName from EVENT_NAMES registry
Event payloadany (untyped)typed interface in EventMap
Dispatchbus.dispatchEvent(new CustomEvent(name, { detail }))dispatchTyped(bus, name, detail)
Subscribebus.addEventListener(name, handler)subscribeTyped(bus, name, handler)
Type safetynonefull — 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.


7. Aligned with prior work


8. Drift identified

Pact / sourceDriftSeverity
FvW v8 / pact/platform/vsil/vsil.mdno StudioContext conceptlow (constitutional, not a feature)
a789 / a791 event namesuntyped events (v1)medium (r207 v2 fixes)
a788 inspector (InspectorCard stub)module-scoped useVSILSourcemedium (r207 migration)
a789 §5.1 GalleryPreviewBridgehas Composition support (per r205)none (r207 extends)
ID10 v2 capability layerno Capability type (uses ad-hoc manifests)low (r206 standardises)

No new pact files required for r207. r207 is structural + integration; no constitutional concepts added.


9. The 8-step implementation plan (NOT this turn — design phase only)

#StepTimeDepends onRisk
1Ship r206 first1-2 days—low
2PreviewHostMount.tsx3hr206low
3InspectorHostMount.tsx2hr206low
4CompositionMount.tsx4hr206, r205 (a791)medium
5mountAll.ts2hsteps 2-4low
6__tests__/mount.test.ts3hsteps 2-5low
7InspectorCard.tsx migration1hstep 3low
8Playwright smoke2hsteps 1-7low

Total: 18-19h after r206 ships. Or 6-7 days end-to-end if r206 is included.


10. The 1 thing to remember

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.

11. The 3 open questions for operator

#QuestionDefault if no answer
Q1r206 order: ship r206 first (1-2d) then r207 (2-3d)? — OR inline r206 in r207 (anti-pattern)?ship r206 first
Q2Event contract v2 scope: just type events, or also namespacing (@vibecoder/event-types package)?just type, no namespacing
Q3InspectorHostMount adapter: in v1, also consume r205 CompositionGraphHost events (auto-show region metadata on region click)?yes, in v1

12. Cross-references

Discovery-only. No code. No pact edits. No extraction. No implementation.
— vibecoder-standalone-mavis (thread 9)