--- name: Front-End Structure description: Build Vue 3 + Ionic front-end components following Orchestrator AI's strict architecture: stores hold state only, services handle API calls with transport types, components use services and read stores. CRITICAL: Maintain view reactivity by keeping stores simple - no methods, no API calls, no business logic. allowed-tools: Read, Write, Edit, Bash, Grep, Glob --- # Front-End Structure Skill **CRITICAL ARCHITECTURE RULE**: Stores hold **data only**. Services handle **API calls**. Components use **services** and read **stores**. Vue reactivity handles **UI updates automatically**. ## When to Use This Skill Use this skill when: - Creating new Vue components - Creating new Pinia stores - Creating new service files - Working with API calls and state management - Building requests that use transport types - Ensuring view reactivity works correctly **CRITICAL**: Agents often want to write methods directly on stores. This breaks reactivity and the architecture. Always redirect to the service layer. ## The Three-Layer Architecture ``` ┌─────────────────────────────────────────┐ │ VIEW LAYER (Components) │ │ - Reads from stores (computed/ref) │ │ - Calls service methods │ │ - Reacts to store changes automatically│ └─────────────────────────────────────────┘ ↕ ┌─────────────────────────────────────────┐ │ SERVICE LAYER │ │ - Builds requests with transport types │ │ - Makes API calls │ │ - Updates stores with responses │ └─────────────────────────────────────────┘ ↕ ┌─────────────────────────────────────────┐ │ STORE LAYER (Pinia) │ │ - Holds state ONLY (ref/computed) │ │ - Simple setters │ │ - NO methods, NO API calls │ └─────────────────────────────────────────┘ ``` ## Critical Pattern #1: Stores Are Data-Only Stores contain **ONLY**: - State (`ref()`) - Computed getters (`computed()`) - Simple setters (synchronous state updates) Stores contain **NEVER**: - Async methods - API calls - Business logic - Complex processing ### ✅ CORRECT Store Pattern Here's an example from `apps/web/src/stores/privacyStore.ts`: ```127:260:apps/web/src/stores/privacyStore.ts export const usePrivacyStore = defineStore('privacy', () => { // ========================================================================== // STATE - PSEUDONYM MAPPINGS // ========================================================================== const mappings = ref([]); const mappingsLoading = ref(false); const mappingsError = ref(null); const mappingsLastFetched = ref(null); const mappingFilters = ref({ dataType: 'all', context: undefined, search: '' }); const mappingSortOptions = ref({ field: 'usageCount', direction: 'desc' }); const mappingStats = ref(null); const mappingStatsLoading = ref(false); const mappingStatsError = ref(null); // ========================================================================== // STATE - PSEUDONYM DICTIONARIES // ========================================================================== const dictionaries = ref([]); const dictionariesLoading = ref(false); const dictionariesError = ref(null); const dictionariesLastUpdated = ref(null); const dictionaryFilters = ref({ category: 'all', dataType: 'all', isActive: 'all', search: '' }); const dictionarySortOptions = ref({ field: 'category', direction: 'asc' }); const selectedDictionaryIds = ref([]); const generationResult = ref(null); const lookupResult = ref(null); const isGenerating = ref(false); const dictionaryStats = ref(null); const importProgress = ref<{ imported: number; total: number; errors: string[] } | null>(null); const isImporting = ref(false); const isExporting = ref(false); // ========================================================================== // STATE - PII PATTERNS // ========================================================================== const patterns = ref([]); const patternsLoading = ref(false); const patternsError = ref(null); const patternsLastUpdated = ref(null); const patternFilters = ref({ dataType: 'all', enabled: 'all', isBuiltIn: 'all', category: 'all', search: '' }); const patternSortOptions = ref({ field: 'name', direction: 'asc' }); const selectedPatternIds = ref([]); const testResult = ref(null); const isTestingPII = ref(false); const patternStats = ref(null); // ========================================================================== // STATE - PRIVACY INDICATORS // ========================================================================== const messageStates = ref>(new Map()); const conversationSettings = ref>(new Map()); const globalSettings = ref({ enableGlobalRealTime: true, defaultUpdateInterval: 2000, maxStoredStates: 100, autoCleanupAge: 3600000, // 1 hour in ms debugMode: false }); const indicatorsInitialized = ref(false); const activeUpdateTimers = ref>(new Map()); const lastGlobalUpdate = ref(null); // ========================================================================== // STATE - DASHBOARD // ========================================================================== const dashboardData = ref(null); const dashboardLoading = ref(false); const dashboardError = ref(null); const dashboardLastUpdated = ref(null); const autoRefreshInterval = ref(null); const dashboardFilters = ref({ timeRange: '7d', dataType: ['all'], includeSystemEvents: true }); // ========================================================================== // STATE - SOVEREIGN POLICY // ========================================================================== const sovereignPolicy = ref(null); const userSovereignMode = ref(false); const sovereignLoading = ref(false); const sovereignError = ref(null); const sovereignInitialized = ref(false); // ========================================================================== // COMPUTED - PSEUDONYM MAPPINGS // ========================================================================== const totalMappings = computed(() => mappings.value.length); const availableDataTypes = computed(() => { const types = new Set(mappings.value.map(m => m.dataType)); return Array.from(types).sort(); }); const availableContexts = computed(() => { const contexts = new Set(mappings.value.map(m => m.context).filter(Boolean)); return Array.from(contexts).sort(); }); ``` **Key Points:** - ✅ Only `ref()` for state - ✅ Only `computed()` for derived state - ✅ Simple setters (not shown here, but they exist) - ❌ NO async methods - ❌ NO API calls - ❌ NO business logic ### ❌ FORBIDDEN Store Pattern ```typescript // ❌ WRONG - This breaks the architecture export const useMyStore = defineStore('myStore', () => { const data = ref(null); // ❌ FORBIDDEN - Async method in store async function fetchData() { const response = await fetch('/api/data'); data.value = await response.json(); } // ❌ FORBIDDEN - Business logic in store function processData() { data.value = data.value.map(/* complex logic */); } return { data, fetchData, processData }; }); ``` ## Critical Pattern #2: Services Handle API Calls with Transport Types Services: 1. Build requests using transport types from `@orchestrator-ai/transport-types` 2. Make API calls 3. Update stores with responses ### ✅ CORRECT Service Pattern Here's an example from `apps/web/src/services/agent2agent/api/agent2agent.api.ts`: ```106:149:apps/web/src/services/agent2agent/api/agent2agent.api.ts plans = { create: async (conversationId: string, message: string) => { const strictRequest = buildRequest.plan.create( { conversationId, userMessage: message }, { title: '', content: message } ); return this.executeStrictRequest(strictRequest); }, read: async (conversationId: string) => { const strictRequest = buildRequest.plan.read({ conversationId }); return this.executeStrictRequest(strictRequest); }, list: async (conversationId: string) => { const strictRequest = buildRequest.plan.list({ conversationId }); return this.executeStrictRequest(strictRequest); }, edit: async (conversationId: string, editedContent: string, metadata?: Record) => { const strictRequest = buildRequest.plan.edit( { conversationId, userMessage: 'Edit plan' }, { editedContent, metadata } ); return this.executeStrictRequest(strictRequest); }, rerun: async ( conversationId: string, versionId: string, config: { provider: string; model: string; temperature?: number; maxTokens?: number; }, userMessage?: string ) => { const strictRequest = buildRequest.plan.rerun( { conversationId, userMessage: userMessage || 'Please regenerate this plan with the same requirements' }, { versionId, config } ); return this.executeStrictRequest(strictRequest); }, ``` **Key Points:** - ✅ Uses `buildRequest` to create requests with transport types - ✅ Makes API calls (`executeStrictRequest`) - ✅ Returns response (doesn't update store directly - that's done by the calling component/service) ### Building Requests with Transport Types Here's how requests are built using transport types from `apps/web/src/services/agent2agent/utils/builders/build.builder.ts`: ```33:59:apps/web/src/services/agent2agent/utils/builders/build.builder.ts export const buildBuilder = { /** * Execute build (create deliverable) */ execute: ( metadata: RequestMetadata & { userMessage: string }, buildData?: { planId?: string; [key: string]: unknown }, ): StrictBuildRequest => { validateRequired(metadata.conversationId, 'conversationId'); validateRequired(metadata.userMessage, 'userMessage'); return { jsonrpc: '2.0', id: crypto.randomUUID(), method: 'build.execute', params: { mode: 'build' as AgentTaskMode, action: 'execute' as BuildAction, conversationId: metadata.conversationId, userMessage: metadata.userMessage, messages: metadata.messages || [], planId: buildData?.planId, metadata: metadata.metadata, payload: buildData || {}, }, }; }, ``` **Key Points:** - ✅ Imports types from `@orchestrator-ai/transport-types` - ✅ Returns `StrictBuildRequest` (ensures type safety) - ✅ Validates required fields - ✅ Builds JSON-RPC 2.0 compliant request ## Critical Pattern #3: Components Use Services, Read Stores Components: 1. Call service methods (not store methods for API calls) 2. Read from stores using `computed()` or `ref()` 3. Vue automatically reacts to store changes ### ✅ CORRECT Component Pattern Here's an example from `apps/web/src/components/Analytics/AnalyticsDashboard.vue`: ```408:480:apps/web/src/components/Analytics/AnalyticsDashboard.vue ``` ## Common Mistakes Agents Make ### ❌ Mistake 1: API Calls in Stores ```typescript // ❌ WRONG export const useMyStore = defineStore('myStore', () => { const data = ref(null); async function fetchData() { const response = await fetch('/api/data'); data.value = await response.json(); } return { data, fetchData }; }); ``` **Fix:** Move API call to service, store only holds state. ### ❌ Mistake 2: Methods on Stores ```typescript // ❌ WRONG function processData() { this.data = this.data.map(/* complex logic */); } ``` **Fix:** Processing happens in service or component, store only holds state. ### ❌ Mistake 3: Not Using Transport Types ```typescript // ❌ WRONG - Raw fetch without transport types const response = await fetch('/api/plan', { method: 'POST', body: JSON.stringify({ conversationId }) }); ``` **Fix:** Use `buildRequest` with transport types: ```typescript const request = buildRequest.plan.read({ conversationId }); const response = await agent2AgentApi.executeStrictRequest(request); ``` ### ❌ Mistake 4: Not Using Computed for Store Values ```typescript // ❌ WRONG - Direct ref access loses reactivity in some cases const data = store.data; // May not be reactive // ✅ CORRECT - Use computed const data = computed(() => store.data); ``` ### ❌ Mistake 5: Manual UI Updates ```typescript // ❌ WRONG - Manual DOM manipulation function updateUI() { document.getElementById('data').innerHTML = this.data; } ``` **Fix:** Let Vue reactivity handle it - just update the store. ## File Structure ``` apps/web/src/ ├── stores/ # Pinia stores (data only) │ ├── conversationsStore.ts │ ├── privacyStore.ts │ ├── analyticsStore.ts │ └── ... ├── services/ # API calls and business logic │ ├── agent2agent/ │ │ ├── api/ │ │ │ └── agent2agent.api.ts │ │ └── utils/ │ │ └── builders/ │ │ ├── build.builder.ts (uses transport types) │ │ └── plan.builder.ts │ ├── conversationsService.ts │ └── ... ├── components/ # Vue components │ ├── Analytics/ │ │ └── AnalyticsDashboard.vue │ └── ... └── types/ # TypeScript types └── ... ``` ## Transport Types Reference All requests must use transport types from `@orchestrator-ai/transport-types`: ```typescript import type { StrictA2ARequest, StrictA2ASuccessResponse, StrictA2AErrorResponse, AgentTaskMode, BuildAction, PlanAction, StrictBuildRequest, StrictPlanRequest, } from '@orchestrator-ai/transport-types'; ``` Build requests using builders: ```typescript import { buildRequest } from '@/services/agent2agent/utils/builders'; // Plan operations const planRequest = buildRequest.plan.create( { conversationId, userMessage }, { title, content } ); // Build operations const buildRequest = buildRequest.build.execute( { conversationId, userMessage }, { planId } ); ``` ## Checklist for Front-End Code When writing front-end code, verify: - [ ] Store contains ONLY state (ref/computed) and simple setters - [ ] Store has NO async methods - [ ] Store has NO API calls - [ ] Store has NO complex business logic - [ ] Service handles ALL API calls - [ ] Service uses transport types when building requests - [ ] Service updates store after API calls - [ ] Component calls service methods (not store methods for API) - [ ] Component reads from store using `computed()` for reactivity - [ ] Vue reactivity handles UI updates automatically - [ ] No manual DOM manipulation - [ ] No `forceUpdate()` or similar hacks ## Related Documentation - **Architecture Details**: [ARCHITECTURE.md](ARCHITECTURE.md) - Complete architecture patterns - **Transport Types**: `@orchestrator-ai/transport-types` package - **A2A Protocol**: See Back-End Structure Skill for A2A compliance ## Troubleshooting **Problem:** Store changes don't update UI - **Solution:** Use `computed()` when reading from store in components - **Solution:** Ensure store uses `ref()` for state (not plain objects) **Problem:** Agent wants to add methods to store - **Solution:** Redirect to service layer - explain stores are data-only **Problem:** API calls fail with type errors - **Solution:** Use `buildRequest` builders with transport types, not raw fetch **Problem:** Component doesn't react to store changes - **Solution:** Check that component uses `computed()` to read from store - **Solution:** Verify store setters update `ref()` values (not plain assignments)