**Operator directive (2026-08-30)**: Prepare a module-extraction plan for the VibeScope inspector so it can mount as a native PM2 panel instead of an iframe. Identify drift in §2/§5/§6/§7 and produce a 2–3 day refactor outline.
**Discovery-only.** No implementation. No pact edits. No extraction.
---
The VibeScope Inspector already has the **right structure** to mount as a PM2 panel — it lives at `studio/modules/vibescope/src/inspector/` (688 LoC, 5 sub-modes, READ-ONLY invariant) with a public API barrel (`index.ts`), frozen types, and a stub `useInspectorSource` hook ready for live data.
The **gap** is mounting: today the Inspector mounts as a FreshCards `<Card>` (`InspectorCard.tsx`, 278 LoC) inside the VibeScope workspace. It does NOT have:
1. A standalone panel root component (host-able from PM2 / any chrome)
2. A subscription model for workspace events
3. An asset-binding input contract
4. A capability manifest for Edge Panel contextual behaviour
5. A StudioContext adapter (it imports `useVSILSource` from `../state/VSILStore` which is module-scoped, not Studio-scoped)
The 2-3 day refactor: **8 steps, ~14-18 hours**, additive-only, no pact edits, no iframe. The output is `studio/modules/vibescope/src/inspector/` exposing a single `<VSILInspectorPanel>` host component that PM2 can mount in 5 lines.
**VPS location reference** (per operator feedback 2026-08-30): **a787** at https://artifacts.freshvibeapps.com/vibecoder-standalone/mockups/vibecoder-vps-location-2026-08-30/index.html
---
```
/workspace/freshvibestudio/studio/modules/vibescope/src/inspector/
├── index.ts 30 LoC — barrel (the public API)
├── types.ts 72 LoC — frozen types (READ-ONLY invariant)
├── useInspectorSource.ts 132 LoC — data hook (STUB today)
├── ASTMode.tsx 87 LoC — sub-mode
├── PropsMode.tsx 85 LoC — sub-mode
├── StateMode.tsx 82 LoC — sub-mode
├── LogsMode.tsx 136 LoC — sub-mode
├── MetadataMode.tsx 64 LoC — sub-mode
└── README.md — doctrine
```
**This is the extraction candidate.** 688 LoC total. No iframe dependency. No `window.*` globals. No PM2-specific imports.
```
/workspace/freshvibestudio/studio/modules/vibescope/src/cards/InspectorCard.tsx 278 LoC
```
The 278 LoC in `InspectorCard.tsx` is the **panel-level glue** (chip row, layout, useVSILSource + parseVSIL + buildSceneGraph + extractInspectorSource pipeline). This is what we need to extract as `<VSILInspectorPanel>`.
```
/workspace/freshvibestudio/studio/modules/vibescope/src/vsil/
├── inspectorExtract.ts 271 LoC — already a pure function
├── parser.ts 879 LoC
├── sceneGraph.ts 323 LoC
└── ... (10 more files, 4720 LoC total)
```
**The `src/vsil/` directory is already a clean importable module.** No extraction work needed. The `inspectorExtract.ts` function is the canonical "VSIL source → Inspector data" pipeline.
---
The 5 sub-modes are ALREADY importable via the `index.ts` barrel. Each takes the same `InspectorSource` prop and renders read-only. Confirmed by `wc -l`:
| Sub-mode | LoC | Public | READ-ONLY |
|----------|-----|--------|-----------|
| `ASTMode` | 87 | ✓ | ✓ (no `onClick` / `onChange`) |
| `PropsMode` | 85 | ✓ | ✓ (Phase 7: will add two-way binding to vsilOps — NOT Inspector) |
| `StateMode` | 82 | ✓ | ✓ |
| `LogsMode` | 136 | ✓ | ✓ |
| `MetadataMode` | 64 | ✓ | ✓ |
**No work needed here.** The 5 modes are already a clean importable surface.
---
`InspectorCard.tsx` uses `useVSILSource()` from `../state/VSILStore` — a module-scoped React hook. This means the Inspector can ONLY mount inside the VibeScope workspace (where the VSILStore is set up). It cannot mount in any other panel host.
The `<VSILInspectorPanel>` takes `source: VSILSource` as a prop, NOT via the store hook. This is the canonical "panel as pure component" pattern. Workspace events are received via:
1. **A pub/sub event bus** — the panel subscribes to `vibescope:source-changed`, `vibescope:selection-changed`, `vibescope:asset-bound` events. Event payloads contain the new `VSILSource` (already-parsed). The bus is a 1-file `EventTarget` (the browser-native primitive, no deps).
2. **A `useVSILInspectorEvents` hook** — wraps the subscription + adds React re-render. Pattern:
```ts
function useVSILInspectorEvents(bus: EventTarget): InspectorSource {
const [source, setSource] = useState<InspectorSource>(...);
useEffect(() => {
const onChange = (e) => setSource(e.detail.source);
bus.addEventListener('vsil:source-changed', onChange);
return () => bus.removeEventListener('vsil:source-changed', onChange);
}, [bus]);
return source;
}
```
3. **The VSILStore is updated to emit events** on every state change (1-line addition: `bus.dispatchEvent(new CustomEvent('vsil:source-changed', { detail: { source } }))` after `setState`).
The current VibeScope workspace emits events via `addEventListener('vibechat:toggle-remit', ...)` style — that pattern is consistent. The Inspector will use the **same** `EventTarget` bus, just with VibeScope-prefixed event names.
---
`useInspectorSource.ts` is a stub: returns a synthetic mock `InspectorSource`. No way to bind a real asset.
The `<VSILInspectorPanel>` accepts asset-binding as **props**, not via a hook:
```ts
interface VSILInspectorPanelProps {
/** The VSIL source to inspect. Either inline or loaded from URL. */
source?: VSILSource;
/** URL to load VSIL source from. The panel fetches on mount. */
sourceUrl?: string;
/** Asset binding — which VSIL asset (layer / keyframe / scene) is selected. */
selectedAssetId?: string;
/** Override the active sub-mode. */
initialSubMode?: InspectorSubMode;
/** Read-only override (the Inspector is ALWAYS read-only, but a host can disable deep-link to source). */
allowDeepLink?: boolean;
/** Slot for custom 5th sub-mode. */
customSubMode?: { id: string; label: string; component: React.ComponentType<...> };
}
```
**Key design**: `source` is the preferred binding (zero-cost prop pass), `sourceUrl` is for the "inspect this URL" use case (Edge Panel + asset). The hook `useInspectorSource(props)` handles both.
When the VibeScope workspace binds an asset (e.g. user clicks a layer in the Visual Card), the workspace fires `vibescope:asset-bound` with `{ assetId, source }`. The Inspector panel subscribes and re-renders.
---
A 1-file `capabilities.ts` that exports the panel's introspection API. Edge Panel (or any chrome) reads it to decide what tiles to show:
```ts
export const VSIL_INSPECTOR_CAPABILITIES = Object.freeze({
/** What this panel can do. */
can: Object.freeze({
inspectSource: true,
inspectAsset: true,
deepLinkToSource: true,
exportSnapshot: false, // not in v1
editInPlace: false, // constitutional — Inspector is READ-ONLY
}),
/** What this panel needs. */
needs: Object.freeze({
source: 'required | optional | none',
selection: 'optional',
assetBinding: 'optional',
}),
/** When to show this panel (Edge Panel context rules). */
contextRules: Object.freeze({
showInWorkspace: ['vibescope'], // canonical
showInContext: ['vsil-asset-selected', 'vsil-source-changed', 'inspector-asked'],
showInMode: ['normal', 'half', 'full'], // per peek expansion modes
}),
/** Events the panel listens to. */
listens: ['vibescope:source-changed', 'vibescope:asset-bound', 'vibescope:selection-changed'],
/** Events the panel emits (read-only signals). */
emits: ['vsil-inspector:deep-link', 'vsil-inspector:source-copied'],
});
```
Edge Panel reads this and shows tiles like "Open Inspector for current asset" or "Copy source path" based on the current context.
```ts
export type InspectorCapabilities = typeof VSIL_INSPECTOR_CAPABILITIES;
```
A host can `import { VSIL_INSPECTOR_CAPABILITIES } from '.../inspector/capabilities'` and use it without instantiating the panel. **This is the contract that makes the panel context-aware without being context-coupled.**
---
The InspectorCard imports `useVSILSource` from the module-scoped VSILStore. This is **module-local**, not Studio-context-aware.
**No StudioContext adapter needed for v1.** The `<VSILInspectorPanel>` takes `source` as a prop. The host (PM2 / Edge Panel / any chrome) is responsible for providing the source.
Rationale: a panel that needs a Studio-wide context would be **chrome-coupled**, defeating the point of extraction. The contract is: "I'm a pure read-only viewer of `VSILSource`. You give me the source, I render it."
If we later want the Inspector to be auto-bound to the **active workspace's current source** (i.e. just drop `<VSILInspectorPanel />` and it works), we could add a thin `useInspectorSourceFromStudio()` hook that reads from `StudioContext`. This is 20 lines and an additive change, NOT in v1 scope.
---
All 5 sub-modes are READ-ONLY, frozen types, no `onClick`/`onChange`/`onInput`/`onSubmit`/`onBlur` handlers. The README and types.ts explicitly cite `pact/platform/inspector/inspector.md` and the constitutional invariant. ✅
The current InspectorCard uses `useVSILSource()` (module-scoped store hook), NOT a workspace event subscription. The pattern is consistent with the rest of VibeScope (which also uses VSILStore directly), but it's not "subscribe to events" in the formal sense. **This is a refactor, not a fix** — the target state adds a real event-bus subscription layer on top.
**Severity**: low. No behavioural difference. Just a different shape.
The current `useInspectorSource.ts` is a **stub** (returns a synthetic mock). The `InspectorCard.tsx` does the real work (line 47-54: `useVSILSource()` → `parseVSIL` → `buildSceneGraph` → `extractInspectorSource`). This is OK for VibeScope (single workspace) but **broken for any other host** (PM2, Edge Panel, VibeCoder).
**Severity**: medium. The Inspector is not actually a "panel" today — it's a workspace-card.
**No capabilities manifest exists.** Edge Panel has no way to introspect what the Inspector can do. The Inspector is mounted via ad-hoc chip-row in the InspectorCard.tsx, not via a registry/capability system.
**Severity**: medium. Edge Panel contextual behaviour is not currently possible for the Inspector.
| Section | Drift | Severity | Fix in v1 |
|---------|-------|----------|-----------|
| §2 sub-modes | none | — | — |
| §5 events | module-scoped store instead of event bus | low | yes |
| §6 asset-binding | stub hook + workspace-card glue | medium | yes |
| §7 capabilities | no manifest | medium | yes |
All drift is in the `cards/InspectorCard.tsx` glue layer (278 LoC), NOT in the `src/inspector/` proper (688 LoC). The inspector proper is clean.
---
| # | Step | Time | What | Risk |
|---|------|------|------|------|
| 1 | Create `VSILInspectorPanel` host | 2h | New `src/inspector/VSILInspectorPanel.tsx`. Wraps the 5 sub-modes with chip row + layout. Takes `source` as prop. | low |
| 2 | Replace `useInspectorSource` stub with real impl | 1h | Move the `parseVSIL → buildSceneGraph → extractInspectorSource` pipeline from `InspectorCard.tsx` into a real `useInspectorSource({ source })` hook in `useInspectorSource.ts`. | low |
| 3 | Add `EventTarget` bus | 1h | `src/inspector/eventBus.ts` — 20 lines, `const bus = new EventTarget()`. Export + add `dispatchVSILSourceChanged(bus, source)` helper. | low |
| 4 | Add `useVSILInspectorEvents` hook | 1h | `src/inspector/useVSILInspectorEvents.ts` — subscribes to bus, returns latest `InspectorSource`. | low |
| 5 | Add `capabilities.ts` manifest | 1h | Frozen manifest per §5. Export `VSIL_INSPECTOR_CAPABILITIES` + `InspectorCapabilities` type. | low |
| 6 | Rewrite `InspectorCard.tsx` as thin wrapper | 1h | The card in VibeScope becomes `<VSILInspectorPanel source={vsilSource} />` — no business logic. **VibeScope still works unchanged.** | low |
| 7 | Add `VSILPanelMount.tsx` for PM2 / Edge Panel | 2h | Standalone hostable panel root. Reads `data-vibescope-asset-id` attribute, fetches source from URL, mounts `<VSILInspectorPanel>`. 5-line PM2 contract: `mgr.addPanel({ id: 'vsil-inspector', title: 'VSIL Inspector', content: <VSILPanelMount />, icon: 'inspector' })`. | low |
| 8 | Tests + drift-check run | 2-3h | (a) Add `__tests__/VSILInspectorPanel.test.tsx` — 4 cases: renders with source, switches sub-modes, READ-ONLY invariant (no onClick), event subscription. (b) Run DC-VC-1..4 drift-checks. (c) Run inspector-copy-001 + inspector-module-scope-001. (d) 1 Playwright smoke: open VibeScope, click Inspector, see 5 sub-modes. | low |
**Total: 11-13 hours** (well under 2-3 days).
---
```ts
import { VSILPanelMount } from 'studio/modules/vibescope/src/inspector/VSILPanelMount';
import { VSIL_INSPECTOR_CAPABILITIES } from 'studio/modules/vibescope/src/inspector/capabilities';
// 1. Capability check
if (VSIL_INSPECTOR_CAPABILITIES.can.inspectSource) {
// 2. Mount in PM2
mgr.addPanel({
id: 'vsil-inspector',
title: 'VSIL Inspector',
content: <VSILPanelMount initialSourceUrl="/api/vsil/current" />,
icon: 'inspector',
on: { edge: 'right' },
});
}
```
**No iframe. No postMessage. No shadow DOM. Just a React component mounted in a panel.**
---
---
| # | Question | Default if no answer |
|---|----------|---------------------|
| Q1 | Should `InspectorCard.tsx` be deleted (VibeScope uses `<VSILInspectorPanel>` directly) or kept as a thin wrapper for back-compat? | thin wrapper (VibeScope keeps working unchanged) |
| Q2 | Should the `useInspectorSource` hook accept a `sourceUrl` (auto-fetch) prop, or require pre-loaded `source`? | both: `source` preferred, `sourceUrl` fallback |
| Q3 | Where does the `EventTarget` bus live — module-scoped (one shared instance) or context-scoped (per-workspace)? | module-scoped (1 line) for v1, context-scoped if we add multi-workspace later |
---
> **The Inspector proper is already 80% of the way there.** The 688 LoC in `src/inspector/` is clean (public API, frozen types, READ-ONLY invariant). The drift is in the 278 LoC `cards/InspectorCard.tsx` glue layer. The refactor moves that glue out, makes the panel host-agnostic, and adds 5 new files (~250 LoC) that the chrome layer can use. **No iframe. No pact edits. No extraction. Just panel-as-component.**
---
**Discovery-only. No code. No pact edits. No extraction. No implementation.**
— vibecoder-standalone-mavis (thread 9)