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)

Pattern to Follow (UnifiedBuilderComponent)

The UnifiedBuilderComponent at libs/libs-space/unified-builder/ (2255 lines) already integrates dock-layout with:

Services to Preserve (StudioShellComponent)

From studio-shell.component.ts (370 lines), these init flows MUST be preserved:

  1. ProjectManagerService.loadProject(spaceSlug) — loads space data
  2. VfsSyncService.hydrateFromSpace() — populates VFS from backend
  3. StudioSessionService.ensureStudioSession() — creates/resumes chat session
  4. AIPanelCoreStateService.openWithContext() — opens Vitto chat
  5. VittoPageContextRegistry — registers 6 studio actions
  6. StudioGenerationListenerService — SSE event routing for module generation

What Does NOT Exist Yet


Phase 1: Dock Layout Shell (Tasks 1-6)

Task 1: Create Studio Dock Panel Configuration

Files:

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:

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:

Step 1: Create the component TypeScript

This component replaces StudioShellComponent as the root container. It:

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:

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:

Step 3: Update toolbar template

Add layout toggle buttons and panel shortcuts. The toolbar should show:

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:

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:

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:

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:

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:

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:

  1. Inject CanvasSelectionService
  2. When selectItem() is called, also update CanvasSelectionService.select()
  3. 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:

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:

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:

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:

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:

  1. modify_component — update properties of a component
  2. add_component — add a widget/component to a page
  3. remove_component — remove a component from a page
  4. restyle_component — change visual properties (colors, sizes, spacing)
  5. 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:

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:

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:

  1. Read selectedComponent from the page context
  2. Include it in the system prompt: "User has selected [component name] of type [type] in module [moduleSlug]..."
  3. 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:

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:

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:

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:

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:

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