2026 03 07 Studio Visual First Recompose
Documentation for Plans
S188: Studio Visual-First Recompose — Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Transform Vitto Studio from a code-editor IDE into a visual AI-first platform builder by recomposing existing components into a dock-layout shell with click-to-select → chat flow.
Architecture: Wrap existing components (visual-builder, universe-canvas, entity-designer, etc.) as dock panels inside a new StudioDockShellComponent. Add a CanvasSelectionService that bridges visual selection to VittoPageContextRegistry, enabling the click → chat → instant update loop. New Vitto tools (modify_component, add_component, etc.) allow AI-driven visual editing.
Tech Stack: Angular 20+ (standalone, signals), dock-layout (@libs-ui/dock-layout), VittoPageContextRegistry (@libs-ai/vitto-core), backend-vitto-api tools, SSE for generation events.
Key Decision: Cosmic simplification is DEFERRED to a separate session. The current hierarchy works fine for the visual-first experience.
Critical Context
Existing Assets Being Reused (DO NOT Rebuild)
BuilderCanvasComponent—libs/libs-structural/vitto-studio/src/lib/components/visual-builder/builder-canvas/PropertiesPanelComponent—libs/libs-structural/vitto-studio/src/lib/components/visual-builder/properties-panel/ComponentPaletteComponent—libs/libs-structural/vitto-studio/src/lib/components/visual-builder/component-palette/PageManagerComponent—libs/libs-structural/vitto-studio/src/lib/components/visual-builder/page-manager/ModuleRendererComponent—libs/libs-space/universe-canvas/src/lib/components/module-renderer/EntityDesignerComponent—libs/libs-structural/vitto-studio/src/lib/components/entity-designer/FileTreeComponent—libs/libs-structural/vitto-studio/src/lib/components/file-tree/EditorTabsComponent—libs/libs-structural/vitto-studio/src/lib/components/editor-tabs/ApiExplorerComponent—libs/libs-structural/vitto-studio/src/lib/components/api-explorer/DataBrowserComponent—libs/libs-structural/vitto-studio/src/lib/components/data-browser/DeployPanelComponent—libs/libs-structural/vitto-studio/src/lib/components/deploy-panel/DocsViewerComponent—libs/libs-structural/vitto-studio/src/lib/components/docs-viewer/VittoPageContextRegistry—libs/libs-ai/vitto-core/src/lib/services/vitto-page-context-registry.service.ts
Pattern to Follow (UnifiedBuilderComponent)
The UnifiedBuilderComponent at libs/libs-space/unified-builder/ (2255 lines) already integrates dock-layout with:
- 11 panels defined as
PanelConfig[] - 3 preset layouts (AI_HEAVY, CANVAS_HEAVY, BALANCED)
DOCK_LAYOUT_CONFIGinjection token for provider configuration@ViewChild('dockContainer')for programmatic control- Tool-to-panel mapping for
openOrFocusDockPanel()
Services to Preserve (StudioShellComponent)
From studio-shell.component.ts (370 lines), these init flows MUST be preserved:
ProjectManagerService.loadProject(spaceSlug)— loads space dataVfsSyncService.hydrateFromSpace()— populates VFS from backendStudioSessionService.ensureStudioSession()— creates/resumes chat sessionAIPanelCoreStateService.openWithContext()— opens Vitto chatVittoPageContextRegistry— registers 6 studio actionsStudioGenerationListenerService— SSE event routing for module generation
What Does NOT Exist Yet
CanvasSelectionService— no selection service found in codebase- Dock panel wrappers for studio components
- Vitto tools for visual editing (
modify_component,add_component, etc.)
Phase 1: Dock Layout Shell (Tasks 1-6)
Task 1: Create Studio Dock Panel Configuration
Files:
- Create:
libs/libs-structural/vitto-studio/src/lib/config/studio-dock-panels.config.ts
Step 1: Create the panel registry
This file defines all dock panels for the studio. Follow the exact pattern from UnifiedBuilderComponent (lines 119-220).
import { PanelConfig } from '@libs-ui/dock-layout';
export const STUDIO_DOCK_PANELS: PanelConfig[] = [
// === LEFT SIDEBAR ===
{
id: 'studio-module-tree',
title: 'Modules',
icon: 'folder_open',
component: 'StudioModuleTreePanel',
closable: false,
draggable: true,
singleton: true,
category: 'navigation',
description: 'Browse modules, pages, widgets, entities',
},
{
id: 'studio-file-tree',
title: 'Files',
icon: 'description',
component: 'StudioFileTreePanel',
closable: true,
draggable: true,
singleton: true,
category: 'navigation',
description: 'VFS file browser (power users)',
},
// === CENTER ===
{
id: 'studio-visual-canvas',
title: 'Canvas',
icon: 'dashboard',
component: 'StudioVisualCanvasPanel',
closable: false,
draggable: true,
singleton: true,
category: 'editor',
description: 'Visual canvas — render and interact with pages',
},
{
id: 'studio-code-editor',
title: 'Code',
icon: 'code',
component: 'StudioCodeEditorPanel',
closable: true,
draggable: true,
singleton: false,
category: 'editor',
description: 'Monaco code editor (power users)',
},
{
id: 'studio-entity-designer',
title: 'Entities',
icon: 'storage',
component: 'StudioEntityDesignerPanel',
closable: true,
draggable: true,
singleton: true,
category: 'editor',
description: 'Create and edit entity definitions',
},
{
id: 'studio-data-browser',
title: 'Data',
icon: 'table_chart',
component: 'StudioDataBrowserPanel',
closable: true,
draggable: true,
singleton: true,
category: 'editor',
description: 'View and edit entity data',
},
{
id: 'studio-api-explorer',
title: 'API',
icon: 'api',
component: 'StudioApiExplorerPanel',
closable: true,
draggable: true,
singleton: true,
category: 'editor',
description: 'Test CRUD endpoints',
},
{
id: 'studio-deploy',
title: 'Deploy',
icon: 'rocket_launch',
component: 'StudioDeployPanel',
closable: true,
draggable: true,
singleton: true,
category: 'editor',
description: 'Deploy to production',
},
{
id: 'studio-docs',
title: 'Docs',
icon: 'menu_book',
component: 'StudioDocsPanel',
closable: true,
draggable: true,
singleton: true,
category: 'editor',
description: 'Auto-generated documentation',
},
// === BOTTOM CENTER ===
{
id: 'studio-properties',
title: 'Properties',
icon: 'tune',
component: 'StudioPropertiesPanel',
closable: true,
draggable: true,
singleton: true,
category: 'inspector',
description: 'Edit selected component properties',
},
{
id: 'studio-component-palette',
title: 'Components',
icon: 'widgets',
component: 'StudioComponentPalettePanel',
closable: true,
draggable: true,
singleton: true,
category: 'inspector',
description: 'Widget library browser',
},
// === RIGHT ===
{
id: 'studio-vitto-chat',
title: 'Vitto',
icon: 'smart_toy',
component: 'StudioVittoChatPanel',
closable: false,
draggable: true,
singleton: true,
category: 'ai',
description: 'AI assistant — always context-aware',
},
];
Step 2: Create default layout presets
In the same file, add layout presets:
import { LayoutState } from '@libs-ui/dock-layout';
/** Visual-first layout: module tree | canvas + properties | vitto chat */
export const STUDIO_VISUAL_LAYOUT: LayoutState = {
type: 'split',
direction: 'horizontal',
sizes: [0.2, 0.55, 0.25],
children: [
// Left: Module tree + File tree tabs
{
type: 'tabs',
activeTabIndex: 0,
children: [
{ type: 'panel', panelId: 'studio-module-tree' },
{ type: 'panel', panelId: 'studio-file-tree' },
],
},
// Center: Canvas (top) + Properties/Palette (bottom)
{
type: 'split',
direction: 'vertical',
sizes: [0.7, 0.3],
children: [
{
type: 'tabs',
activeTabIndex: 0,
children: [
{ type: 'panel', panelId: 'studio-visual-canvas' },
{ type: 'panel', panelId: 'studio-code-editor' },
{ type: 'panel', panelId: 'studio-entity-designer' },
{ type: 'panel', panelId: 'studio-data-browser' },
{ type: 'panel', panelId: 'studio-api-explorer' },
{ type: 'panel', panelId: 'studio-deploy' },
{ type: 'panel', panelId: 'studio-docs' },
],
},
{
type: 'tabs',
activeTabIndex: 0,
children: [
{ type: 'panel', panelId: 'studio-properties' },
{ type: 'panel', panelId: 'studio-component-palette' },
],
},
],
},
// Right: Vitto chat
{
type: 'tabs',
activeTabIndex: 0,
children: [
{ type: 'panel', panelId: 'studio-vitto-chat' },
],
},
],
};
/** Code-first layout: file tree | editor tabs | vitto chat */
export const STUDIO_CODE_LAYOUT: LayoutState = {
type: 'split',
direction: 'horizontal',
sizes: [0.2, 0.55, 0.25],
children: [
{
type: 'tabs',
activeTabIndex: 0,
children: [
{ type: 'panel', panelId: 'studio-file-tree' },
{ type: 'panel', panelId: 'studio-module-tree' },
],
},
{
type: 'split',
direction: 'vertical',
sizes: [0.7, 0.3],
children: [
{
type: 'tabs',
activeTabIndex: 0,
children: [
{ type: 'panel', panelId: 'studio-code-editor' },
{ type: 'panel', panelId: 'studio-visual-canvas' },
],
},
{
type: 'tabs',
activeTabIndex: 0,
children: [
{ type: 'panel', panelId: 'studio-properties' },
{ type: 'panel', panelId: 'studio-api-explorer' },
],
},
],
},
{
type: 'tabs',
activeTabIndex: 0,
children: [
{ type: 'panel', panelId: 'studio-vitto-chat' },
],
},
],
};
Step 3: Commit
git add libs/libs-structural/vitto-studio/src/lib/config/studio-dock-panels.config.ts
git commit -m "feat(vitto-studio): add dock panel registry and layout presets for S188"
Task 2: Create Dock Panel Wrapper Components
Why wrappers? Dock panels receive DockPanelInputs (instanceId, data, isActive, isFloating). Existing components don't expect these inputs. Thin wrappers bridge the gap.
Files:
- Create:
libs/libs-structural/vitto-studio/src/lib/panels/directory with panel wrappers
Step 1: Create the panel wrapper directory
ls libs/libs-structural/vitto-studio/src/lib/
# Verify directory structure before creating
Step 2: Create module-tree panel wrapper
The module tree panel is NEW — it provides a higher-level view than the file tree. It shows modules, pages, widgets organized by feature area (not raw files).
Create: libs/libs-structural/vitto-studio/src/lib/panels/module-tree-panel/module-tree-panel.component.ts
import { Component, ChangeDetectionStrategy, inject, input } from '@angular/core';
import { DockPanelInputs } from '@libs-ui/dock-layout';
import { PageManagerComponent } from '../../components/visual-builder/page-manager/page-manager.component';
import { VfsService } from '../../services/vfs.service';
@Component({
selector: 'lib-studio-module-tree-panel',
standalone: true,
imports: [PageManagerComponent],
template: `
<div class="module-tree-panel">
<lib-page-manager />
</div>
`,
styles: [`
.module-tree-panel {
height: 100%;
overflow-y: auto;
}
`],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class StudioModuleTreePanelComponent {
readonly instanceId = input<string>('');
readonly data = input<unknown>(null);
readonly isActive = input(false);
readonly isFloating = input(false);
}
Step 3: Create visual canvas panel wrapper
Create: libs/libs-structural/vitto-studio/src/lib/panels/visual-canvas-panel/visual-canvas-panel.component.ts
import { Component, ChangeDetectionStrategy, inject, input } from '@angular/core';
import { BuilderCanvasComponent } from '../../components/visual-builder/builder-canvas/builder-canvas.component';
import { ComponentPaletteComponent } from '../../components/visual-builder/component-palette/component-palette.component';
@Component({
selector: 'lib-studio-visual-canvas-panel',
standalone: true,
imports: [BuilderCanvasComponent],
template: `
<div class="visual-canvas-panel">
<lib-builder-canvas />
</div>
`,
styles: [`
.visual-canvas-panel {
height: 100%;
overflow: hidden;
position: relative;
}
`],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class StudioVisualCanvasPanelComponent {
readonly instanceId = input<string>('');
readonly data = input<unknown>(null);
readonly isActive = input(false);
readonly isFloating = input(false);
}
Step 4: Create remaining panel wrappers (one file per panel)
Create these minimal wrappers following the same pattern. Each wraps the corresponding existing component:
| Wrapper File | Wraps |
|---|---|
panels/file-tree-panel/file-tree-panel.component.ts |
FileTreeComponent |
panels/code-editor-panel/code-editor-panel.component.ts |
EditorTabsComponent |
panels/properties-panel/properties-panel.component.ts |
PropertiesPanelComponent |
panels/component-palette-panel/component-palette-panel.component.ts |
ComponentPaletteComponent |
panels/entity-designer-panel/entity-designer-panel.component.ts |
EntityDesignerComponent |
panels/data-browser-panel/data-browser-panel.component.ts |
DataBrowserComponent |
panels/api-explorer-panel/api-explorer-panel.component.ts |
ApiExplorerComponent |
panels/deploy-panel/deploy-panel.component.ts |
DeployPanelComponent |
panels/docs-panel/docs-panel.component.ts |
DocsViewerComponent |
panels/vitto-chat-panel/vitto-chat-panel.component.ts |
Studio chat panel (AI panel) |
Each wrapper is ~30 lines: imports the inner component, accepts DockPanelInputs, renders the inner component in a height:100% div.
Step 5: Commit
git add libs/libs-structural/vitto-studio/src/lib/panels/
git commit -m "feat(vitto-studio): create dock panel wrappers for all studio components"
Task 3: Create StudioDockShellComponent
Files:
- Create:
libs/libs-structural/vitto-studio/src/lib/components/studio-dock-shell/studio-dock-shell.component.ts - Create:
libs/libs-structural/vitto-studio/src/lib/components/studio-dock-shell/studio-dock-shell.component.html - Create:
libs/libs-structural/vitto-studio/src/lib/components/studio-dock-shell/studio-dock-shell.component.scss
Step 1: Create the component TypeScript
This component replaces StudioShellComponent as the root container. It:
- Preserves ALL initialization logic from StudioShellComponent (lines 130-207)
- Uses
DockContainerComponentinstead of CSS grid - Provides
DOCK_LAYOUT_CONFIGwith our panel registry - Maps panel component strings to actual components via a registry map
import {
Component, ChangeDetectionStrategy, OnInit, OnDestroy, ViewChild,
inject, signal, computed, effect, Type,
} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { DockContainerComponent, DockLayoutConfig, DOCK_LAYOUT_CONFIG } from '@libs-ui/dock-layout';
import { VittoPageContextRegistry } from '@libs-ai/vitto-core';
import { AIPanelCoreStateService } from '@libs-ai/ai-panel';
import { STUDIO_DOCK_PANELS, STUDIO_VISUAL_LAYOUT, STUDIO_CODE_LAYOUT } from '../../config/studio-dock-panels.config';
import { ProjectManagerService } from '../../services/project-manager.service';
import { VfsService } from '../../services/vfs.service';
import { VfsSyncService } from '../../services/vfs-sync.service';
import { CrudGeneratorService } from '../../services/crud-generator.service';
import { StudioVittoBridgeService } from '../../services/studio-vitto-bridge.service';
import { StudioGenerationListenerService } from '../../services/studio-generation-listener.service';
import { StudioApiService } from '../../services/studio-api.service';
import { StudioSessionService } from '../../services/studio-session.service';
import { StudioToolbarComponent } from '../studio-toolbar/studio-toolbar.component';
// Panel wrapper imports
import { StudioModuleTreePanelComponent } from '../../panels/module-tree-panel/module-tree-panel.component';
import { StudioVisualCanvasPanelComponent } from '../../panels/visual-canvas-panel/visual-canvas-panel.component';
import { StudioFileTreePanelComponent } from '../../panels/file-tree-panel/file-tree-panel.component';
import { StudioCodeEditorPanelComponent } from '../../panels/code-editor-panel/code-editor-panel.component';
import { StudioPropertiesPanelComponent } from '../../panels/properties-panel/properties-panel.component';
import { StudioComponentPalettePanelComponent } from '../../panels/component-palette-panel/component-palette-panel.component';
import { StudioEntityDesignerPanelComponent } from '../../panels/entity-designer-panel/entity-designer-panel.component';
import { StudioDataBrowserPanelComponent } from '../../panels/data-browser-panel/data-browser-panel.component';
import { StudioApiExplorerPanelComponent } from '../../panels/api-explorer-panel/api-explorer-panel.component';
import { StudioDeployPanelComponent } from '../../panels/deploy-panel/deploy-panel.component';
import { StudioDocsPanelComponent } from '../../panels/docs-panel/docs-panel.component';
import { StudioVittoChatPanelComponent } from '../../panels/vitto-chat-panel/vitto-chat-panel.component';
/** Maps panel component string IDs to actual Angular components */
const PANEL_COMPONENT_MAP: Record<string, Type<unknown>> = {
'StudioModuleTreePanel': StudioModuleTreePanelComponent,
'StudioFileTreePanel': StudioFileTreePanelComponent,
'StudioVisualCanvasPanel': StudioVisualCanvasPanelComponent,
'StudioCodeEditorPanel': StudioCodeEditorPanelComponent,
'StudioPropertiesPanel': StudioPropertiesPanelComponent,
'StudioComponentPalettePanel': StudioComponentPalettePanelComponent,
'StudioEntityDesignerPanel': StudioEntityDesignerPanelComponent,
'StudioDataBrowserPanel': StudioDataBrowserPanelComponent,
'StudioApiExplorerPanel': StudioApiExplorerPanelComponent,
'StudioDeployPanel': StudioDeployPanelComponent,
'StudioDocsPanel': StudioDocsPanelComponent,
'StudioVittoChatPanel': StudioVittoChatPanelComponent,
};
@Component({
selector: 'lib-studio-dock-shell',
standalone: true,
imports: [DockContainerComponent, StudioToolbarComponent],
providers: [
{
provide: DOCK_LAYOUT_CONFIG,
useFactory: () => {
// Resolve component strings to actual component types
const resolvedPanels = STUDIO_DOCK_PANELS.map(panel => ({
...panel,
component: PANEL_COMPONENT_MAP[panel.component as string] ?? panel.component,
}));
return {
panels: resolvedPanels,
defaultLayout: STUDIO_VISUAL_LAYOUT,
storageKey: 'vitto-studio-dock-layout',
autoPersist: true,
theme: {
tabHeight: 36,
splitterWidth: 5,
settingsPosition: 'hidden',
},
behavior: {
enableDrag: true,
enableFloat: true,
enablePopout: false,
enableEdgeDocking: true,
enableTabDocking: true,
doubleClickMaximize: true,
},
header: {
displayMode: 'tabs',
showCloseButton: true,
showIcons: true,
allowReorder: true,
},
mode: { mode: 'development' },
} as DockLayoutConfig;
},
},
],
templateUrl: './studio-dock-shell.component.html',
styleUrl: './studio-dock-shell.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class StudioDockShellComponent implements OnInit, OnDestroy {
@ViewChild('dockContainer') dockContainer?: DockContainerComponent;
// === Injected Services (preserved from StudioShellComponent) ===
private readonly route = inject(ActivatedRoute);
private readonly projectManager = inject(ProjectManagerService);
private readonly vfs = inject(VfsService);
private readonly vfsSync = inject(VfsSyncService);
private readonly crudGenerator = inject(CrudGeneratorService);
private readonly vittoBridge = inject(StudioVittoBridgeService);
private readonly generationListener = inject(StudioGenerationListenerService);
private readonly studioApi = inject(StudioApiService);
private readonly pageContext = inject(VittoPageContextRegistry);
private readonly aiPanelState = inject(AIPanelCoreStateService);
private readonly studioSession = inject(StudioSessionService);
// === State ===
readonly spaceSlug = signal<string>('');
readonly universeSlug = signal<string>('');
readonly isLoading = signal(true);
readonly initError = signal<string | null>(null);
// Generation state (from StudioGenerationListenerService)
readonly generationPhase = this.generationListener.currentPhase;
readonly generationProgress = this.generationListener.phaseProgress;
readonly isGeneratingModule = this.generationListener.isGenerating;
readonly generationError = this.generationListener.lastError;
// Active layout mode
readonly activeLayout = signal<'visual' | 'code'>('visual');
ngOnInit(): void {
// Extract route params (walk parent routes)
let currentRoute = this.route.snapshot;
while (currentRoute.parent) {
if (currentRoute.params['spaceSlug']) {
this.spaceSlug.set(currentRoute.params['spaceSlug']);
}
if (currentRoute.params['universeSlug']) {
this.universeSlug.set(currentRoute.params['universeSlug']);
}
currentRoute = currentRoute.parent;
}
if (this.spaceSlug()) {
this.initializeFromSpace(this.spaceSlug());
}
}
ngOnDestroy(): void {
this.pageContext.unregister('/studio');
}
/** Switch between visual-first and code-first layouts */
switchLayout(mode: 'visual' | 'code'): void {
this.activeLayout.set(mode);
if (this.dockContainer?.dockService) {
const layout = mode === 'visual' ? STUDIO_VISUAL_LAYOUT : STUDIO_CODE_LAYOUT;
this.dockContainer.dockService.setLayout(layout);
}
}
/** Open a specific panel by ID */
openPanel(panelId: string): void {
this.dockContainer?.dockService?.openPanel(panelId);
}
// === Preserved initialization logic from StudioShellComponent ===
private async initializeFromSpace(spaceSlug: string): Promise<void> {
try {
this.isLoading.set(true);
this.initError.set(null);
// 1. Load project
await this.projectManager.loadProject(spaceSlug);
// 2. Hydrate VFS from space data
const project = this.projectManager.activeProject();
if (project) {
await this.vfsSync.hydrateFromSpace(project.id, project.spaceId);
}
// 3. Ensure dedicated studio chat session
await this.studioSession.ensureStudioSession();
// 4. Open Vitto chat panel
this.aiPanelState.openWithContext({ contextType: 'vitto-studio' });
// 5. Register page context for Vitto awareness
this.registerPageContext();
this.isLoading.set(false);
} catch (err) {
this.initError.set(err instanceof Error ? err.message : 'Failed to initialize studio');
this.isLoading.set(false);
}
}
private registerPageContext(): void {
this.pageContext.register('/studio', {
pageDescription: computed(() => {
const project = this.projectManager.activeProject();
if (!project) return 'User is in Vitto Studio (loading)';
return `User is in Vitto Studio building "${project.name}". Layout: ${this.activeLayout()}. ` +
`Generation: ${this.isGeneratingModule() ? this.generationPhase() : 'idle'}.`;
}),
readableState: computed(() => ({
spaceSlug: this.spaceSlug(),
universeSlug: this.universeSlug(),
activeLayout: this.activeLayout(),
isGenerating: this.isGeneratingModule(),
generationPhase: this.generationPhase(),
openFiles: this.vfs.openFiles().map(f => f.path),
dirtyFileCount: this.vfs.dirtyCount(),
})),
actions: this.buildStudioActions(),
suggestions: computed(() => [
'Generate a player management module',
'Add an entity called Player with name, position, goals',
'Show me the API endpoints',
'Deploy this space',
]),
scopedTools: ['generate_module', 'create_entity', 'modify_component', 'add_component'],
});
}
private buildStudioActions() {
return [
{
name: 'generatePlatform',
description: 'Generate a full platform from a description',
parameters: { description: { type: 'string' as const, description: 'Platform description', required: true } },
handler: async (params: Record<string, unknown>) => {
await this.crudGenerator.generatePlatform(params['description'] as string);
},
category: 'generation' as const,
},
{
name: 'switchLayout',
description: 'Switch studio layout between visual and code mode',
parameters: { mode: { type: 'string' as const, description: 'Layout mode', enum: ['visual', 'code'], required: true } },
handler: (params: Record<string, unknown>) => {
this.switchLayout(params['mode'] as 'visual' | 'code');
},
category: 'navigation' as const,
},
{
name: 'openPanel',
description: 'Open a specific studio panel',
parameters: { panelId: { type: 'string' as const, description: 'Panel ID to open', required: true } },
handler: (params: Record<string, unknown>) => {
this.openPanel(params['panelId'] as string);
},
category: 'navigation' as const,
},
];
}
}
Step 2: Create the HTML template
<div class="studio-dock-shell">
<lib-studio-toolbar
[activeLayout]="activeLayout()"
(layoutChange)="switchLayout($event)"
(panelOpen)="openPanel($event)" />
@if (initError(); as error) {
<div class="studio-error">
<span class="material-icons">error</span>
<p>{{ error }}</p>
</div>
} @else if (isLoading()) {
<div class="studio-loading">
<div class="spinner"></div>
<p>Loading studio...</p>
</div>
} @else {
<lib-dock-container
#dockContainer
[initialLayout]="null"
class="studio-dock-container" />
}
@if (isGeneratingModule()) {
<div class="generation-status-bar">
<span class="phase">{{ generationPhase() }}</span>
<div class="progress-bar">
<div class="progress-fill" [style.width.%]="generationProgress()"></div>
</div>
@if (generationError(); as err) {
<span class="error">{{ err }}</span>
}
</div>
}
</div>
Step 3: Create the SCSS
.studio-dock-shell {
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
}
.studio-dock-container {
flex: 1;
overflow: hidden;
}
.studio-error {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
padding: 40px;
color: var(--color-error, #ef4444);
}
.studio-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
flex: 1;
}
.generation-status-bar {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 16px;
background: var(--surface-2, #1e1e2e);
border-top: 1px solid var(--border-color, #333);
.phase {
font-size: 12px;
text-transform: uppercase;
color: var(--text-secondary, #999);
}
.progress-bar {
flex: 1;
height: 4px;
background: var(--surface-3, #2a2a3a);
border-radius: 2px;
overflow: hidden;
.progress-fill {
height: 100%;
background: var(--color-primary, #6366f1);
transition: width 300ms ease;
}
}
.error {
color: var(--color-error, #ef4444);
font-size: 12px;
}
}
.spinner {
width: 32px;
height: 32px;
border: 3px solid var(--surface-3, #2a2a3a);
border-top-color: var(--color-primary, #6366f1);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
Step 4: Commit
git add libs/libs-structural/vitto-studio/src/lib/components/studio-dock-shell/
git commit -m "feat(vitto-studio): create StudioDockShellComponent with dock-layout integration"
Task 4: Update StudioToolbarComponent for Dock Layout
Files:
- Modify:
libs/libs-structural/vitto-studio/src/lib/components/studio-toolbar/studio-toolbar.component.ts - Modify:
libs/libs-structural/vitto-studio/src/lib/components/studio-toolbar/studio-toolbar.component.html
Step 1: Read current toolbar files
Read both files completely before modifying.
Step 2: Add layout mode inputs and panel open output
The toolbar needs new inputs/outputs:
activeLayoutinput (to highlight current mode)layoutChangeoutput (visual/code toggle)panelOpenoutput (open specific panels)
Step 3: Update toolbar template
Add layout toggle buttons and panel shortcuts. The toolbar should show:
- [Visual] [Code] toggle (left side)
- Panel shortcuts: [Entities] [Data] [API] [Deploy] [Docs] (center)
- Generation status indicator (right side)
Step 4: Commit
git add libs/libs-structural/vitto-studio/src/lib/components/studio-toolbar/
git commit -m "feat(vitto-studio): update toolbar for dock layout modes and panel shortcuts"
Task 5: Update Routes and Public API
Files:
- Modify:
libs/libs-structural/vitto-studio/src/lib/routes/vitto-studio.routes.ts - Modify:
libs/libs-structural/vitto-studio/src/index.ts
Step 1: Update routes to use new dock shell
The routes should point to StudioDockShellComponent instead of StudioShellComponent. Keep StudioShellComponent alive for backward compat but make the dock shell the default.
// vitto-studio.routes.ts
import { Routes } from '@angular/router';
export const VittoStudioRoutes: Routes = [
{
path: '',
title: 'Vitto Studio',
loadComponent: () =>
import('../components/studio-dock-shell/studio-dock-shell.component')
.then(m => m.StudioDockShellComponent),
},
// Keep legacy route for backward compat
{
path: 'classic',
title: 'Vitto Studio (Classic)',
loadComponent: () =>
import('../components/studio-shell/studio-shell.component')
.then(m => m.StudioShellComponent),
},
];
Note: Remove the separate entity/api/data/deploy/docs routes — these are now dock panels, not separate routes. The dock shell manages panel visibility internally.
Step 2: Export new components from index.ts
Add exports for:
StudioDockShellComponent- All panel wrapper components
STUDIO_DOCK_PANELS,STUDIO_VISUAL_LAYOUT,STUDIO_CODE_LAYOUT
Step 3: Commit
git add libs/libs-structural/vitto-studio/src/lib/routes/vitto-studio.routes.ts
git add libs/libs-structural/vitto-studio/src/index.ts
git commit -m "feat(vitto-studio): update routes to dock shell, export new components"
Task 6: Verify Phase 1
Step 1: Build the library
yarn nx build libs-structural-vitto-studio --skip-nx-cache
Fix any import errors.
Step 2: Serve the app
yarn serve:gograb-ai
Navigate to the studio route. Verify:
- Dock layout renders with toolbar at top
- Module tree panel visible on left
- Visual canvas panel visible in center
- Vitto chat panel visible on right
- Properties panel visible at bottom center
- Panels can be dragged between positions
- Layout persists on refresh
- Generation status bar appears during module generation
- No console errors
Step 3: Commit verified state
git add -A
git commit -m "feat(vitto-studio): Phase 1 complete — dock layout shell verified"
Phase 2: Click-to-Select + Canvas Integration (Tasks 7-11)
Task 7: Create CanvasSelectionService
Files:
- Create:
libs/libs-structural/vitto-studio/src/lib/services/canvas-selection.service.ts
Step 1: Create the selection service
This is the core of the click → chat flow. It tracks what the user has selected on the visual canvas and exposes it as signals.
import { Injectable, signal, computed } from '@angular/core';
export interface SelectedComponent {
/** Unique ID of the selected element */
id: string;
/** Human-readable name (e.g., 'PlayerListComponent') */
name: string;
/** Component type (e.g., 'data-table', 'form', 'card', 'hero') */
type: string;
/** Module this component belongs to */
moduleSlug: string;
/** Page this component is on */
pageSlug?: string;
/** Editable properties */
properties: Record<string, unknown>;
/** Route path if applicable */
route?: string;
/** Position in the page layout */
layoutSlot?: string;
}
@Injectable({ providedIn: 'root' })
export class CanvasSelectionService {
private readonly _selectedComponent = signal<SelectedComponent | null>(null);
private readonly _selectionHistory = signal<SelectedComponent[]>([]);
private readonly _hoveredComponentId = signal<string | null>(null);
/** Currently selected component */
readonly selectedComponent = this._selectedComponent.asReadonly();
/** Selection history (last 10) */
readonly selectionHistory = this._selectionHistory.asReadonly();
/** Currently hovered component ID (for highlight preview) */
readonly hoveredComponentId = this._hoveredComponentId.asReadonly();
/** Whether anything is selected */
readonly hasSelection = computed(() => this._selectedComponent() !== null);
/** Human-readable description of selection for Vitto context */
readonly selectionDescription = computed(() => {
const sel = this._selectedComponent();
if (!sel) return '';
const props = Object.entries(sel.properties)
.filter(([, v]) => v !== undefined && v !== null)
.map(([k, v]) => {
if (Array.isArray(v)) return `${k}: [${v.length} items]`;
if (typeof v === 'string' && v.length > 40) return `${k}: "${v.substring(0, 40)}..."`;
return `${k}: ${JSON.stringify(v)}`;
})
.join(', ');
return `Selected: ${sel.name} (${sel.type}) in ${sel.moduleSlug}` +
(sel.pageSlug ? ` > ${sel.pageSlug}` : '') +
(props ? `. Properties: ${props}` : '');
});
/** Select a component */
select(component: SelectedComponent): void {
const current = this._selectedComponent();
if (current?.id === component.id) return; // Already selected
this._selectedComponent.set(component);
this._selectionHistory.update(history => {
const filtered = history.filter(h => h.id !== component.id);
return [component, ...filtered].slice(0, 10);
});
}
/** Clear selection */
deselect(): void {
this._selectedComponent.set(null);
}
/** Update properties of the currently selected component */
updateSelectedProperties(updates: Record<string, unknown>): void {
this._selectedComponent.update(sel => {
if (!sel) return null;
return { ...sel, properties: { ...sel.properties, ...updates } };
});
}
/** Set hovered component (for preview highlight) */
setHovered(componentId: string | null): void {
this._hoveredComponentId.set(componentId);
}
}
Step 2: Commit
git add libs/libs-structural/vitto-studio/src/lib/services/canvas-selection.service.ts
git commit -m "feat(vitto-studio): create CanvasSelectionService for click-to-select flow"
Task 8: Wire BuilderCanvasComponent to CanvasSelectionService
Files:
- Modify:
libs/libs-structural/vitto-studio/src/lib/components/visual-builder/builder-canvas/builder-canvas.component.ts
Step 1: Read current builder-canvas component
Already read (68 lines). Currently manages items and selectedItemId signals locally.
Step 2: Inject CanvasSelectionService and wire selection
Modify BuilderCanvasComponent to:
- Inject
CanvasSelectionService - When
selectItem()is called, also updateCanvasSelectionService.select() - Expose an output for selection events
The existing selectedItemId signal stays for local rendering. The CanvasSelectionService propagates to the rest of the system (properties panel, Vitto context).
Step 3: Commit
git add libs/libs-structural/vitto-studio/src/lib/components/visual-builder/builder-canvas/
git commit -m "feat(vitto-studio): wire BuilderCanvas to CanvasSelectionService"
Task 9: Wire CanvasSelectionService to VittoPageContextRegistry
Files:
- Modify:
libs/libs-structural/vitto-studio/src/lib/components/studio-dock-shell/studio-dock-shell.component.ts
Step 1: Inject CanvasSelectionService into StudioDockShellComponent
Step 2: Include selection context in page context registration
Update registerPageContext() to include selection description:
private readonly canvasSelection = inject(CanvasSelectionService);
// In registerPageContext():
pageDescription: computed(() => {
const project = this.projectManager.activeProject();
const selection = this.canvasSelection.selectionDescription();
if (!project) return 'User is in Vitto Studio (loading)';
let desc = `User is in Vitto Studio building "${project.name}".`;
if (selection) desc += ` ${selection}`;
if (this.isGeneratingModule()) desc += ` Generation: ${this.generationPhase()}.`;
return desc;
}),
readableState: computed(() => ({
// ... existing fields ...
selectedComponent: this.canvasSelection.selectedComponent(),
})),
Step 3: Add visual editing actions
Add actions for Vitto to modify the selected component:
{
name: 'modifyComponent',
description: 'Modify properties of the currently selected component',
parameters: {
updates: { type: 'object', description: 'Property key-value pairs to update', required: true },
},
handler: (params: Record<string, unknown>) => {
this.canvasSelection.updateSelectedProperties(params['updates'] as Record<string, unknown>);
},
category: 'generation',
},
Step 4: Commit
git add libs/libs-structural/vitto-studio/src/lib/components/studio-dock-shell/
git commit -m "feat(vitto-studio): wire CanvasSelection to VittoPageContextRegistry for click-to-chat"
Task 10: Wire PropertiesPanel to CanvasSelectionService
Files:
- Modify:
libs/libs-structural/vitto-studio/src/lib/components/visual-builder/properties-panel/properties-panel.component.ts - Modify:
libs/libs-structural/vitto-studio/src/lib/panels/properties-panel/properties-panel.component.ts
Step 1: Update PropertiesPanel to read from CanvasSelectionService
The PropertiesPanelComponent currently takes fields as input. The panel wrapper should inject CanvasSelectionService and derive fields from the selected component's properties.
// In properties-panel panel wrapper:
readonly canvasSelection = inject(CanvasSelectionService);
readonly fields = computed(() => {
const sel = this.canvasSelection.selectedComponent();
if (!sel) return [];
return Object.entries(sel.properties).map(([key, value]) => ({
key,
label: key.replace(/([A-Z])/g, ' $1').trim(),
type: inferFieldType(value),
value,
}));
});
Step 2: Wire property changes back to CanvasSelectionService
When user edits a property in the panel, call canvasSelection.updateSelectedProperties().
Step 3: Commit
git add libs/libs-structural/vitto-studio/src/lib/components/visual-builder/properties-panel/
git add libs/libs-structural/vitto-studio/src/lib/panels/properties-panel/
git commit -m "feat(vitto-studio): wire PropertiesPanel to CanvasSelectionService for live editing"
Task 11: Verify Phase 2
Step 1: Serve and test the click-to-select flow
yarn serve:gograb-ai
Verify:
- Click a component in the visual canvas → blue selection highlight appears
- Properties panel shows the selected component's editable properties
- Edit a property in properties panel → canvas re-renders
- Vitto chat context includes selection description
- Deselect by clicking empty canvas area
- Hover highlights work (lighter highlight on hover before click)
Step 2: Commit
git add -A
git commit -m "feat(vitto-studio): Phase 2 complete — click-to-select + Vitto context verified"
Phase 3: AI-Driven Editing Tools (Tasks 12-15)
Task 12: Create Visual Editing Tools for Backend Vitto API
Files:
- Create:
APIs/backend-vitto-api/src/app/tools/visual-editing.tool.ts
Step 1: Locate existing tool registry
Find: APIs/backend-vitto-api/src/app/tools/ or tool-registry.service.ts
Step 2: Create visual editing tool definitions
These are new Vitto tools that modify the ModuleContext/component state via the existing tool execution pipeline.
Tools to create:
modify_component— update properties of a componentadd_component— add a widget/component to a pageremove_component— remove a component from a pagerestyle_component— change visual properties (colors, sizes, spacing)reorder_components— change component order within a page
Each tool follows the existing tool pattern (see existing tools in the pipeline).
Step 3: Commit
git add APIs/backend-vitto-api/src/app/tools/visual-editing.tool.ts
git commit -m "feat(backend-vitto): add visual editing tools for AI-driven component modification"
Task 13: Register Visual Editing Tools in Tool Registry
Files:
- Modify:
APIs/backend-vitto-api/src/app/tools/tool-registry.service.ts(or equivalent)
Step 1: Read current tool registry
Find and read the tool registration file.
Step 2: Register the 5 new visual editing tools
Follow the existing pattern for tool registration.
Step 3: Commit
git add APIs/backend-vitto-api/src/app/tools/
git commit -m "feat(backend-vitto): register visual editing tools in tool registry"
Task 14: Update Context Assembly for Selection Awareness
Files:
- Modify:
APIs/backend-vitto-api/src/app/prompts/context-assembly.service.ts(or equivalent)
Step 1: Read current context assembly service
Find and read the service that assembles the system prompt for Vitto.
Step 2: Include selection context
The frontend already sends selectedComponent via VittoPageContextRegistry.readableState. The backend context assembly should:
- Read
selectedComponentfrom the page context - Include it in the system prompt: "User has selected [component name] of type [type] in module [moduleSlug]..."
- Prioritize visual editing tools when a component is selected
Step 3: Commit
git add APIs/backend-vitto-api/src/app/prompts/
git commit -m "feat(backend-vitto): include selection context in Vitto system prompt"
Task 15: Verify Phase 3
Step 1: Serve backend and frontend
yarn serve:backend-vitto
yarn serve:gograb-ai
Step 2: Test AI-driven editing
Verify:
- Select a component → type "make it wider" in Vitto chat
- Vitto uses
modify_componenttool → properties update → canvas re-renders - "Add a button below this" →
add_componentadds widget - "Change the background to blue" →
restyle_componentupdates style - Undo/redo works via dock-layout history
Step 3: Commit
git add -A
git commit -m "feat(vitto-studio): Phase 3 complete — AI-driven visual editing verified"
Phase 4: DRY Generation + Deploy (Tasks 16-18)
Task 16: Update Generation Templates to Use Widget Registry
Files:
- Modify: Module generation prompts in
APIs/backend-vitto-api/src/app/module-generation/
Step 1: Read current generation prompts
Find the prompts that tell the LLM what components to generate.
Step 2: Update prompts to reference existing widget palette
Instead of generating custom HTML, prompts should reference the 19 palette items from ComponentPaletteComponent:
- hero, header, footer, section (Layout)
- heading, text, image, card (Content)
- form, input, button, select (Forms)
- data-table, data-list, data-form, chart (Data)
- nav, tabs, breadcrumb (Navigation)
The LLM should output ModuleContext JSON that references these widget types, not raw Angular template code.
Step 3: Commit
git add APIs/backend-vitto-api/src/app/module-generation/
git commit -m "feat(backend-vitto): update generation prompts to use existing widget registry"
Task 17: Deploy Visibility Toggle
Files:
- Modify:
libs/libs-structural/vitto-studio/src/lib/services/studio-deploy.service.ts - Modify:
libs/libs-structural/vitto-studio/src/lib/components/deploy-panel/deploy-panel.component.ts
Step 1: Read current deploy service and panel
Step 2: Add simple visibility toggle
MVP deploy = toggle space visibility (public/private). This uses the existing Space API:
// PATCH /api/spaces/:spaceId { visibility: 'public' | 'private' }
Add a toggle in DeployPanel + preview iframe showing the rendered space.
Step 3: Commit
git add libs/libs-structural/vitto-studio/src/lib/services/studio-deploy.service.ts
git add libs/libs-structural/vitto-studio/src/lib/components/deploy-panel/
git commit -m "feat(vitto-studio): add deploy visibility toggle and preview iframe"
Task 18: Verify Phase 4 + Final Integration
Step 1: Full integration test
yarn serve:backend-vitto
yarn serve:backend-api
yarn serve:gograb-ai
Verify all success criteria from the original plan:
- User describes platform in chat → modules generated → visual preview shows rendered pages
- Click any component → Vitto knows what was selected
- "Change the background to blue" → component updates visually, instantly
- Deploy toggle works (visibility change)
- Generated modules use widget registry (not custom code)
- Layout persists across refresh
- Non-developer can build a functional platform without writing code
Step 2: Final commit
git add -A
git commit -m "feat(vitto-studio): S188 complete — visual AI-first platform builder"
Findings Addressed
| Finding | How Addressed |
|---|---|
| Plan ignores existing visual-builder components | All existing components reused as dock panel contents (Task 2) |
| Cosmic simplification high-risk | DEFERRED to separate session — not required for visual-first UX |
| 3-mode architecture not accounted for | Dock shell replaces 3 modes with layout presets (visual/code). Classic shell kept at /classic route |
| Phase ordering wrong | Cosmic simplification removed from this plan entirely |
| ModuleContext JSON underspecified | Widget palette (19 items) serves as the component schema; properties derived from palette items |
| Properties panel scope unclear | CanvasSelectionService provides property schema per selected component (Task 7) |
| Observatory already has cost tracking | No duplicate cost panel created — reuse existing observatory |
| Routes vs dock panels conflict | Separate routes removed; all tools are dock panels. Only root route + classic fallback remain |
Risks
| Risk | Mitigation |
|---|---|
| Dock layout config type mismatch with existing PanelConfig | Follow exact UnifiedBuilder pattern (proven, 2255 lines) |
| Panel wrappers add indirection | Each wrapper is ~30 lines — minimal overhead, clean separation |
| CanvasSelectionService + VittoPageContextRegistry circular deps | Both are providedIn: 'root', no circular risk |
| Backend visual editing tools need SSE for real-time updates | Reuse existing StudioGenerationListenerService SSE pattern |
| Classic shell breakage | Kept at /classic route for backward compat |