2026 03 21 Vitto Studio Complete Fix
Documentation for Plans
Vitto Studio — Complete Fix & Enhancement Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Close all gaps between the platform generation pipeline and vitto-studio UI — every tab must react correctly to generation events, show real data, and provide a polished, cohesive experience.
Architecture: The root cause is a SSE event forwarding gap in VittoChatEngineService — only 3 of ~15 event types reach the studio. Fixing this single chokepoint unblocks Preview, Pipeline, Generations, and Review tabs simultaneously. Secondary fixes address data persistence, runtime bugs, and UX polish across all panels.
Tech Stack: Angular 20+ (standalone, OnPush, signals), RxJS, TypeScript, rspack WASM, SSE streams, CSS custom properties via @theme-system.
Constraints: No git commits (concurrent session). No setTimeout (use RxJS timer()). No raw any (use SafeAny). Separate .ts/.html/.scss files for new components. inject() pattern only.
Phase 1: SSE Event Pipeline Fix (Critical — Unblocks Everything)
Task 1: Forward ALL module-gen/pipeline/generation events from chat engine to studio
The single most impactful change. Currently VittoChatEngineService silently drops ~12 SSE event types that the studio needs. The fix: forward ALL module-gen.*, pipeline.*, and generation.* events through moduleGenEvents$.
Files:
Modify:
libs/libs-ai/vitto-chat/src/lib/services/vitto-chat-engine.service.ts:555-590Step 1: Identify the forwarding gap
At line 557-561, NON_PHASE_EVENTS gates which events get forwarded. Events NOT in this set (like module-gen.module-generated) fall through to the !phase check at line 588 and are silently dropped. The fix is to forward ALL module-gen/pipeline/generation events to moduleGenEvents$ regardless of whether they're in NON_PHASE_EVENTS.
- Step 2: Add comprehensive event forwarding
The fix must go ABOVE the NON_PHASE_EVENTS check (before line 557), because events like module-gen.module-generated are not in NON_PHASE_EVENTS and would be silently dropped if the forwarding were inside it.
Location: Insert AFTER line 556 (after const NON_PHASE_EVENTS is NOT yet defined — actually insert BEFORE line 555, right after // S204: Skip non-phase events... comment), BEFORE the NON_PHASE_EVENTS declaration. Use the existing eventType variable already declared at line 550.
Insert this block at line 555 (before the NON_PHASE_EVENTS const):
// S252: Forward ALL generation-related SSE events to moduleGenEvents$
// for the studio generation listener. This must happen BEFORE the
// NON_PHASE_EVENTS filter, because events like module-gen.module-generated
// are not in that set and would be silently dropped.
// Uses the existing `eventType` variable from line 550.
const STUDIO_EVENT_PREFIXES = ['module-gen.', 'pipeline.', 'generation.', 'phase.'];
if (STUDIO_EVENT_PREFIXES.some(prefix => eventType.startsWith(prefix))) {
this.moduleGenEvents$.next({
type: eventType,
data: sseEvent.data as Record<string, unknown> ?? {},
});
}
Then remove the three individual forwarding blocks inside the if (NON_PHASE_EVENTS.has(eventType)) block (lines 573-586) — the three if (eventType === 'module-gen.completed'), if (eventType === 'module-gen.failed'), and if (eventType === 'module-gen.started') blocks. These are now redundant since ALL module-gen.* events are forwarded above.
Keep everything else intact: The NON_PHASE_EVENTS set itself (controls progress card suppression), the deploy timing logic, and the else branch for phase progress cards (line 590+) must remain untouched. The forwarding and the progress card logic serve independent consumers (studio listener vs chat UI).
- Step 3: Verify no duplicate event handling
The StudioGenerationListenerService.handleEvent() already has a switch statement that handles each event type exactly once. The dock shell subscribes to moduleGenEvents$ and calls handleEvent(). Verify that forwarding module-gen.phase-start/progress/complete won't create duplicate progress cards in the chat — it won't, because the chat engine creates its own progress cards in the else branch (line 590+), and the studio listener's handleEvent() only updates studio-internal state (signals, VFS writes). These are independent consumers.
- Step 4: Test the fix
- Open Studio for a universe
- Ask Vitto to generate a platform via chat
- Verify: Pipeline tab shows real-time phase nodes
- Verify: Files appear in VFS during generation (not just after)
- Verify: Review panel auto-opens with pending files
- Verify: Live Preview auto-compiles when files arrive
- Verify: Generations tab shows the new generation when complete
Task 2: Add spaceId/universeId fallback in StudioGenerationListenerService
Even with Task 1 fixed, handleCompleted() gates loadGenerations() on activeSpaceId which is captured from the module-gen.started event data. If the backend nests the IDs differently, the Generations tab won't refresh. Add a fallback that sources IDs from the dock shell's route params.
Files:
Modify:
libs/libs-structural/vitto-studio/src/lib/services/studio-generation-listener.service.ts:117-132, 310-381Step 1: Accept fallback IDs via a setter
Add a method to set fallback IDs that will be used when SSE event data doesn't include them:
// S252: Fallback IDs from route params, used when SSE event data
// doesn't include spaceId/universeId.
private fallbackSpaceId: string | null = null;
private fallbackUniverseId: string | null = null;
setFallbackIds(spaceId: string, universeId: string): void {
this.fallbackSpaceId = spaceId;
this.fallbackUniverseId = universeId;
}
- Step 2: Use fallback in handleCompleted()
In handleCompleted() at line 312-313, after capturing activeSpaceId/activeUniverseId, add fallback:
const spaceId = this.activeSpaceId || this.fallbackSpaceId;
const universeId = this.activeUniverseId || this.fallbackUniverseId;
- Step 3: Wire fallback IDs from dock shell
In StudioDockShellComponent.initializeFromSpace(), after loadProject succeeds (line 329), call:
if (project.spaceId && project.universeId) {
this.generationListener.setFallbackIds(project.spaceId, project.universeId);
}
- Step 4: Also set fallbackIds in handleEvent for module-gen.started
In handleEvent() case module-gen.started (line 128-129), add after the existing ID capture:
// S252: Also update fallbacks so they're always fresh
if (this.activeSpaceId) this.fallbackSpaceId = this.activeSpaceId;
if (this.activeUniverseId) this.fallbackUniverseId = this.activeUniverseId;
Phase 2: Runtime Bugs & Data Gaps
Task 3: Fix deploy panel private signal access
StudioDeployPanelComponent.deploy() accesses this.gitState['_spaceId']() — private signals by string key. This will throw at runtime in production builds.
Files:
Modify:
libs/libs-structural/vitto-studio/src/lib/services/git-state.service.ts:80-89Modify:
libs/libs-structural/vitto-studio/src/lib/panels/deploy-panel.component.ts:190Step 1: Expose public readonly signals in GitStateService
At line 89 (after existing public getters), add:
readonly spaceId = this._spaceId.asReadonly();
readonly universeId = this._universeId.asReadonly();
- Step 2: Update deploy panel to use public signals
At line 190, replace:
const mergeUrl = `${this.apiBaseUrl}/studio/universes/${this.gitState['_spaceId']()}/${this.gitState['_universeId']()}/git/merge`;
with:
const mergeUrl = `${this.apiBaseUrl}/studio/universes/${this.gitState.spaceId()}/${this.gitState.universeId()}/git/merge`;
- Step 3: Verify no other private access patterns exist
Search for gitState\['_ across the codebase. Already verified — only one occurrence at deploy-panel.component.ts:190.
Task 4: Wire module tree click to CanvasSelectionService
The module tree's onModuleClick() is a console.log no-op. Wire it to select the module in the visual canvas.
Files:
Modify:
libs/libs-structural/vitto-studio/src/lib/panels/module-tree-panel.component.ts:107-110Step 1: Inject CanvasSelectionService
Add to the component's injections:
private readonly canvasSelection = inject(CanvasSelectionService);
- Step 2: Replace console.log with actual selection
Replace onModuleClick():
onModuleClick(mod: UniverseStructureNode): void {
this.canvasSelection.select(mod);
}
- Step 3: Add import for CanvasSelectionService
Add the import at the top of the file:
import { CanvasSelectionService } from '../services/canvas-selection.service';
Task 5: Hydrate entities from universe structure on Studio load
CrudGeneratorService.entities is session-local (signal<StudioEntity[]>([])) — entities vanish on page refresh. The Entities tab is always empty for returning users.
Data model reality: UniverseStructureNode.config.entities is string[] — just entity names (e.g., ["Player", "Team"]), NOT full StudioEntity objects. StudioEntity requires id, collection, displayName, fields[], crudOperations, crudEndpoints, pageBindings, createdAt, updatedAt — none of which exist on a string name.
Approach: Create skeleton StudioEntity objects from entity names, with sensible defaults. Users can then edit fields via the Entities tab. This is better than showing nothing.
Files:
Modify:
libs/libs-structural/vitto-studio/src/lib/services/crud-generator.service.tsModify:
libs/libs-structural/vitto-studio/src/lib/components/studio-dock-shell/studio-dock-shell.component.tsStep 1: Verify the data model
Confirm that UniverseStructureNode.config.entities is indeed string[]. Check the interface definition:
grep -n "entities" libs/libs-structural/vitto-studio/src/lib/types/universe-structure.types.ts
Also check StudioEntity at libs/libs-structural/vitto-studio/src/lib/types/studio-entity.types.ts:27-39.
- Step 2: Add hydration method to CrudGeneratorService
/**
* S252: Hydrate skeleton entities from universe structure on Studio load.
* config.entities is string[] (names only), so we create skeleton StudioEntity
* objects with sensible defaults. Users can edit fields via the Entities tab.
*/
hydrateFromStructure(modules: UniverseStructureNode[]): void {
// Don't overwrite entities that were already defined in this session
if (this.entities().length > 0) return;
const entities: StudioEntity[] = [];
const now = new Date().toISOString();
for (const mod of modules) {
const entityNames = mod.config?.entities ?? [];
for (const name of entityNames) {
if (typeof name !== 'string' || !name) continue;
// Avoid duplicates
if (entities.some(e => e.displayName === name)) continue;
const collection = name.toLowerCase().replace(/\s+/g, '_');
entities.push({
id: crypto.randomUUID(),
collection,
displayName: name,
description: `${name} entity from ${mod.config.name} module`,
fields: [
// Provide a default 'name' field so the entity isn't completely empty
{ name: 'name', type: 'string', required: true, label: 'Name' },
],
crudOperations: ['create', 'read', 'update', 'delete', 'get'],
crudEndpoints: [
{ method: 'GET', path: `/${collection}`, operation: 'read' },
{ method: 'GET', path: `/${collection}/:id`, operation: 'get' },
{ method: 'POST', path: `/${collection}`, operation: 'create' },
{ method: 'PUT', path: `/${collection}/:id`, operation: 'update' },
{ method: 'DELETE', path: `/${collection}/:id`, operation: 'delete' },
],
pageBindings: [],
createdAt: now,
updatedAt: now,
});
}
}
if (entities.length > 0) {
this.entities.set(entities);
}
}
Note: Verify that CrudEndpoint interface accepts { method, path, operation }. Check the types file. Adjust the shape if needed.
- Step 3: Inject CrudGeneratorService in dock shell and wire hydration
In StudioDockShellComponent, add injection:
private readonly crudGenerator = inject(CrudGeneratorService);
Then in the constructor, add an effect that reacts to module structure loading:
// S252: Hydrate skeleton entities from universe structure
// so the Entities tab shows entity names even on page refresh.
effect(() => {
const modules = this.structureState.modules();
if (modules.length > 0) {
this.crudGenerator.hydrateFromStructure(modules);
}
});
- Step 4: Add import for UniverseStructureNode in CrudGeneratorService
Ensure UniverseStructureNode is imported. Check the actual type name — it may be exported from the types directory or from UniverseStructureStateService.
Phase 3: Preview Reliability
Task 6: Improve preview retry and error resilience
The preview has several fragility points: 3 retries with 2s delay may not be enough when VFS loads from GitLab, the preview-panel-opened event can be missed, and SharedArrayBuffer issues are silent.
Files:
Modify:
libs/libs-structural/vitto-studio/src/lib/services/preview.service.ts:339-357Modify:
libs/libs-structural/platform-preview/src/lib/components/platform-preview-panel/platform-preview-panel.component.tsModify:
libs/libs-structural/platform-preview/src/lib/services/bundler.service.tsStep 1: Increase retry budget and add exponential backoff
In PreviewService, replace the retry constants:
private readonly MAX_EMPTY_VFS_RETRIES = 5; // was 3
private readonly INITIAL_RETRY_DELAY_MS = 1000; // was 2000 flat
And use exponential backoff in the retry logic:
const delay = this.INITIAL_RETRY_DELAY_MS * Math.pow(1.5, this.emptyVfsRetryCount);
- Step 2: Add SharedArrayBuffer availability check in BundlerService
In BundlerService.ensureWorker(), add an upfront check before creating the Worker:
if (typeof SharedArrayBuffer === 'undefined') {
this.state.set('error');
this.errors.set([
'SharedArrayBuffer is not available. Ensure the page is served with ' +
'Cross-Origin-Embedder-Policy and Cross-Origin-Opener-Policy headers.',
]);
return;
}
This turns a silent failure into a visible error in the preview frame.
Phase 4: UI/UX Polish & Consistency
Task 7: Enhance Pipeline Visualizer with generation activity feed
When generation runs via chat, the pipeline visualizer should show a live activity feed even when formal pipeline.status nodes aren't available. The StudioGenerationListenerService already emits generationActivity — wire it to the pipeline panel.
Files:
Modify:
libs/libs-structural/vitto-studio/src/lib/components/pipeline-visualizer/pipeline-visualizer.component.tsModify:
libs/libs-structural/vitto-studio/src/lib/components/pipeline-visualizer/pipeline-visualizer.component.htmlStep 1: Inject StudioGenerationListenerService and add DatePipe
import { DatePipe } from '@angular/common';
import { StudioGenerationListenerService } from '../../services/studio-generation-listener.service';
// In @Component imports array, add: DatePipe
// In the class body:
private readonly genListener = inject(StudioGenerationListenerService);
readonly activity = this.genListener.generationActivity;
readonly isGenerating = this.genListener.isGenerating;
readonly currentPhase = this.genListener.currentPhase;
readonly phaseProgress = this.genListener.phaseProgress;
Important: DatePipe must be added to the component's imports array since it's standalone. The entry.timestamp is a number (Unix ms from Date.now()), which Angular's DatePipe handles correctly.
- Step 2: Add activity feed to the template
When pipelineState.nodes().length === 0 but isGenerating() is true, show a live activity feed instead of "No Pipeline Active":
@if (pipelineState.nodes().length === 0) {
@if (isGenerating()) {
<div class="activity-feed">
<div class="activity-header">
<div class="gen-pulse"></div>
<span>{{ currentPhase() || 'Generating...' }}</span>
@if (phaseProgress()) {
<span class="phase-progress">{{ phaseProgress() }}</span>
}
</div>
<div class="activity-log">
@for (entry of activity(); track entry.timestamp) {
<div class="activity-entry" [class.phase-entry]="entry.phase">
<span class="activity-time">{{ entry.timestamp | date:'HH:mm:ss' }}</span>
<span class="activity-text">{{ entry.message }}</span>
</div>
}
</div>
</div>
} @else {
<div class="empty-state">
<span class="material-icons empty-icon">route</span>
<p>No Pipeline Active</p>
<p class="empty-hint">Start a generation to see the pipeline</p>
</div>
}
}
- Step 3: Add styles for the activity feed
.activity-feed {
height: 100%;
display: flex;
flex-direction: column;
padding: 12px;
}
.activity-header {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 0;
font-size: 13px;
font-weight: 500;
color: var(--text-color, #e0e0e0);
border-bottom: 1px solid var(--surface-border, #1e1e2e);
margin-bottom: 8px;
}
.phase-progress {
font-size: 11px;
color: var(--text-secondary, #888);
margin-left: auto;
}
.activity-log {
flex: 1;
overflow-y: auto;
}
.activity-entry {
display: flex;
gap: 8px;
padding: 3px 0;
font-size: 11px;
color: var(--text-secondary, #888);
&.phase-entry {
color: var(--text-color, #e0e0e0);
font-weight: 500;
}
}
.activity-time {
font-family: monospace;
color: var(--text-tertiary, #666);
min-width: 60px;
}
.gen-pulse {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--primary-color, #6366f1);
animation: pulse 1.5s ease infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
Task 8: Enhance empty states across all panels
Current empty states are functional but sparse. Add consistent, informative empty states with clear CTAs.
Files:
Modify:
libs/libs-structural/vitto-studio/src/lib/panels/generations-panel.component.html:168-174Modify:
libs/libs-structural/vitto-studio/src/lib/panels/file-review-panel.component.ts(inline template)Modify:
libs/libs-structural/vitto-studio/src/lib/components/pipeline-visualizer/pipeline-visualizer.component.html:2-7Modify:
libs/libs-structural/platform-preview/src/lib/components/preview-frame/preview-frame.component.html:32-38Step 1: Generations panel empty state
Replace lines 168-174 in generations-panel.component.html:
<div class="empty-state">
<span class="material-icons empty-icon">history</span>
<p class="empty-title">No generations yet</p>
<p class="empty-hint">Ask Vitto to generate a platform, or use the Vibe canvas to start building</p>
</div>
Add styles in the .scss:
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 32px 16px;
text-align: center;
height: 100%;
min-height: 120px;
}
.empty-icon {
font-size: 36px;
color: var(--text-tertiary, #555);
margin-bottom: 12px;
opacity: 0.6;
}
.empty-title {
font-size: 13px;
font-weight: 500;
color: var(--text-secondary, #888);
margin: 0 0 4px;
}
.empty-hint {
font-size: 11px;
color: var(--text-tertiary, #666);
margin: 0;
max-width: 240px;
line-height: 1.4;
}
- Step 2: Review panel empty state
In file-review-panel.component.ts, update the empty state section of the inline template:
<div class="empty-state">
<span class="material-icons empty-icon">fact_check</span>
<p class="empty-title">No pending files to review</p>
<p class="empty-hint">Generated files will appear here for review before being added to your project</p>
</div>
- Step 3: Preview frame empty state
In preview-frame.component.html, update the empty state (lines 32-38):
<div class="empty-state">
<span class="material-icons empty-icon">monitor</span>
<p class="empty-title">Ready when you are</p>
<p class="empty-hint">Ask Vitto to generate a platform, or open a space with existing modules. Your preview will compile automatically.</p>
</div>
Task 9: Add loading/generating states to the toolbar
The studio toolbar should show a generation progress indicator when generation is active, so users always know something is happening regardless of which tab they're viewing.
Files:
Modify:
libs/libs-structural/vitto-studio/src/lib/components/studio-toolbar/studio-toolbar.component.tsModify:
libs/libs-structural/vitto-studio/src/lib/components/studio-toolbar/studio-toolbar.component.htmlStep 1: Read the current toolbar component
Read the toolbar component to understand its current structure and inputs. If the toolbar already has isGenerating, generationPhase, or generationProgress inputs, skip the redundant additions and proceed to wiring only.
- Step 2: Add generation indicator inputs (skip if already present)
readonly isGenerating = input(false);
readonly generationPhase = input<string | null>(null);
readonly generationProgress = input('');
- Step 3: Add indicator to toolbar template
@if (isGenerating()) {
<div class="generation-indicator">
<div class="gen-pulse"></div>
<span class="gen-phase">{{ generationPhase() || 'Generating...' }}</span>
@if (generationProgress()) {
<span class="gen-detail">{{ generationProgress() }}</span>
}
</div>
}
- Step 4: Wire inputs from dock shell template
In the dock shell template, pass the generation state to the toolbar:
<lib-studio-toolbar
[isGenerating]="isGeneratingModule()"
[generationPhase]="generationPhase()"
[generationProgress]="generationProgress()"
/>
Phase 5: Code Quality
Task 10: Replace setTimeout with RxJS timer()
Three violations of the no-setTimeout rule.
Files:
Modify:
libs/libs-structural/vitto-studio/src/lib/services/workflow-state.service.ts:335, 537Modify:
libs/libs-structural/vitto-studio/src/lib/panels/settings-panel.component.ts:222Step 1: Fix WorkflowStateService
Replace the setTimeout in the onerror handler:
// Before:
this.eventSource.onerror = () => {
setTimeout(() => {
if (!this.isRunning()) return;
// reconnection logic
}, 1000);
};
// After:
this.eventSource.onerror = () => {
timer(1000).pipe(take(1)).subscribe(() => {
if (!this.isRunning()) return;
// reconnection logic
});
};
- Step 2: Fix SettingsPanelComponent
Replace the setTimeout:
// Before:
setTimeout(() => {
if (this.saveStatus() === 'saved') this.saveStatus.set('idle');
}, 2000);
// After:
timer(2000).pipe(take(1)).subscribe(() => {
if (this.saveStatus() === 'saved') this.saveStatus.set('idle');
});
Ensure timer and take are imported from rxjs.
- Step 3: Fix WorkflowStateService content delta debounce (line 537)
At line 537, this.contentFlushTimer = setTimeout(() => this.flushContentDeltas(), 500) stores the timer ID for cancellation. Replace with a Subscription:
// Add a private field:
private contentFlushSub?: Subscription;
// Replace setTimeout:
this.contentFlushSub?.unsubscribe();
this.contentFlushSub = timer(500).pipe(take(1)).subscribe(() => this.flushContentDeltas());
// In the cleanup/cancel method, replace clearTimeout with:
this.contentFlushSub?.unsubscribe();
Also add Subscription to the rxjs import.
Task 11: Replace raw any with SafeAny in WorkflowStateService
18 occurrences of raw any in the service.
Files:
Modify:
libs/libs-structural/vitto-studio/src/lib/services/workflow-state.service.tsStep 1: Import SafeAny
import type { SafeAny } from '@libs-foundational/core-models';
Verified import path — used by @libs-ui-core/ui-core breadcrumb service and others.
- Step 2: Replace all
anywithSafeAny
Use find-and-replace carefully. Only replace type annotations (: any, as any, <any>), not string content.
Task 12: Remove console.log statements from ModularityModule
15+ console.log statements used for lifecycle logging.
Files:
Modify:
libs/libs-structural/vitto-studio/src/lib/modularity.module.tsStep 1: Read the file and identify all console.log calls
Step 2: Remove all console.log calls
These are lifecycle debug logs that should not be in production code. If logging is genuinely needed, inject LoggerService — but for lifecycle events, Angular's built-in lifecycle hooks are self-documenting and don't need logging.
Phase 6: Verify & Polish
Task 13: End-to-end verification
After all changes, verify the complete flow works.
- Step 1: Verify build
npx nx build vitto-studio --skip-nx-cache
npx nx build vitto-chat --skip-nx-cache
npx nx build platform-preview --skip-nx-cache
All three must build with 0 errors.
- Step 2: Verify the full generation → preview flow
- Start dev server:
yarn serve:space - Navigate to a universe's Studio
- Ask Vitto to generate a platform
- Watch: Pipeline tab shows live activity feed with phases
- Watch: Files appear in VFS progressively (check Files tab)
- Watch: Review panel auto-opens with generated files
- Watch: Generations tab shows new generation on completion
- Watch: Live Preview auto-compiles and shows the generated app
- Check: Entities tab shows entities from the generated modules
- Step 3: Verify panel interactions
- Click a module in the Module Tree → visual canvas selects it
- Switch to Code layout → Branches, History, Diff tabs are visible and functional
- Click Deploy → no runtime error (merge URL uses public signals)
- Open Settings → env vars load correctly
Dependency Graph
Task 1 (SSE forwarding) ← EVERYTHING depends on this
↓
Task 2 (fallback IDs) — makes Task 1 more resilient
↓
Tasks 3-5 (runtime bugs) — independent of each other, can be parallel
↓
Task 6 (preview reliability) — builds on Task 1 fix
↓
Tasks 7-9 (UI/UX polish) — independent of each other, can be parallel
↓
Tasks 10-12 (code quality) — independent, can be parallel
↓
Task 13 (verification) — must be last
Parallelizable groups:
- Group A: Tasks 3, 4, 5 (runtime bugs — all independent)
- Group B: Tasks 7, 8, 9 (UI/UX — all independent)
- Group C: Tasks 10, 11, 12 (code quality — all independent)
Sequential dependencies:
- Task 1 MUST be first (everything flows from it)
- Task 2 after Task 1 (touches the same event handling flow)
- Task 13 MUST be last (verification)
Risk Assessment
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| SSE event forwarding creates duplicate handling | Low | Medium | Studio listener's switch statement handles each type once; chat progress cards are independent |
| Entity hydration creates sparse objects | Low | Low | Skeleton entities have defaults; users can edit fields. Better than showing nothing |
| SharedArrayBuffer check causes false negative | Low | Low | Only blocks if truly unavailable — correct behavior |
| Preview retry increase causes unnecessary delays | Low | Low | Exponential backoff starts at 1s, shorter than current flat 2s |
| Removing console.log breaks debugging | Low | Negligible | These are lifecycle logs, not error diagnostics |