Claude-skill-registry Front-End Structure
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.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/front-end-structure-skill" ~/.claude/skills/majiayu000-claude-skill-registry-front-end-structure && rm -rf "$T"
skills/data/front-end-structure-skill/SKILL.mdFront-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:
export const usePrivacyStore = defineStore('privacy', () => { // ========================================================================== // STATE - PSEUDONYM MAPPINGS // ========================================================================== const mappings = ref<PseudonymMapping[]>([]); const mappingsLoading = ref(false); const mappingsError = ref<string | null>(null); const mappingsLastFetched = ref<Date | null>(null); const mappingFilters = ref<PseudonymMappingFilters>({ dataType: 'all', context: undefined, search: '' }); const mappingSortOptions = ref<PseudonymMappingSortOptions>({ field: 'usageCount', direction: 'desc' }); const mappingStats = ref<PseudonymStatsResponse['stats'] | null>(null); const mappingStatsLoading = ref(false); const mappingStatsError = ref<string | null>(null); // ========================================================================== // STATE - PSEUDONYM DICTIONARIES // ========================================================================== const dictionaries = ref<PseudonymDictionaryEntry[]>([]); const dictionariesLoading = ref(false); const dictionariesError = ref<string | null>(null); const dictionariesLastUpdated = ref<Date | null>(null); const dictionaryFilters = ref<PseudonymDictionaryFilters>({ category: 'all', dataType: 'all', isActive: 'all', search: '' }); const dictionarySortOptions = ref<PseudonymDictionarySortOptions>({ field: 'category', direction: 'asc' }); const selectedDictionaryIds = ref<string[]>([]); const generationResult = ref<PseudonymGenerateResponse | null>(null); const lookupResult = ref<PseudonymLookupResponse | null>(null); const isGenerating = ref(false); const dictionaryStats = ref<PseudonymStatsResponse | null>(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<PIIPattern[]>([]); const patternsLoading = ref(false); const patternsError = ref<string | null>(null); const patternsLastUpdated = ref<Date | null>(null); const patternFilters = ref<PIIPatternFilters>({ dataType: 'all', enabled: 'all', isBuiltIn: 'all', category: 'all', search: '' }); const patternSortOptions = ref<PIIPatternSortOptions>({ field: 'name', direction: 'asc' }); const selectedPatternIds = ref<string[]>([]); const testResult = ref<PIITestResponse | null>(null); const isTestingPII = ref(false); const patternStats = ref<PIIStatsResponse | null>(null); // ========================================================================== // STATE - PRIVACY INDICATORS // ========================================================================== const messageStates = ref<Map<string, MessagePrivacyState>>(new Map()); const conversationSettings = ref<Map<string, ConversationPrivacySettings>>(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<Map<string, NodeJS.Timeout>>(new Map()); const lastGlobalUpdate = ref<Date | null>(null); // ========================================================================== // STATE - DASHBOARD // ========================================================================== const dashboardData = ref<PrivacyDashboardData | null>(null); const dashboardLoading = ref(false); const dashboardError = ref<string | null>(null); const dashboardLastUpdated = ref<Date | null>(null); const autoRefreshInterval = ref<NodeJS.Timeout | null>(null); const dashboardFilters = ref<DashboardFilters>({ timeRange: '7d', dataType: ['all'], includeSystemEvents: true }); // ========================================================================== // STATE - SOVEREIGN POLICY // ========================================================================== const sovereignPolicy = ref<SovereignPolicy | null>(null); const userSovereignMode = ref(false); const sovereignLoading = ref(false); const sovereignError = ref<string | null>(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
for stateref() - ✅ Only
for derived statecomputed() - ✅ Simple setters (not shown here, but they exist)
- ❌ NO async methods
- ❌ NO API calls
- ❌ NO business logic
❌ FORBIDDEN Store Pattern
// ❌ 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:
- Build requests using transport types from
@orchestrator-ai/transport-types - Make API calls
- Update stores with responses
✅ CORRECT Service Pattern
Here's an example from
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<string, unknown>) => { 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
to create requests with transport typesbuildRequest - ✅ 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:
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
(ensures type safety)StrictBuildRequest - ✅ Validates required fields
- ✅ Builds JSON-RPC 2.0 compliant request
Critical Pattern #3: Components Use Services, Read Stores
Components:
- Call service methods (not store methods for API calls)
- Read from stores using
orcomputed()ref() - Vue automatically reacts to store changes
✅ CORRECT Component Pattern
Here's an example from
apps/web/src/components/Analytics/AnalyticsDashboard.vue:
<script setup lang="ts"> import { ref, computed, onMounted } from 'vue'; import { IonCard, IonCardContent, IonCardHeader, IonCardTitle, IonCardSubtitle, IonItem, IonLabel, IonButton, IonToggle, IonSelect, IonSelectOption, IonInput, IonBadge, IonIcon, IonSpinner, IonGrid, IonRow, IonCol, IonList, IonAvatar } from '@ionic/vue'; import { analyticsOutline, refreshOutline, trendingUpOutline, cashOutline, speedometerOutline, checkmarkCircleOutline, pulseOutline, trophyOutline, timeOutline, pieChartOutline, layersOutline, alertCircleOutline, documentTextOutline, personOutline, settingsOutline, warningOutline } from 'ionicons/icons'; import { useAnalyticsStore } from '@/stores/analyticsStore'; import { useLLMMonitoringStore } from '@/stores/llmMonitoringStore'; // Store integration const analyticsStore = useAnalyticsStore(); const llmMonitoringStore = useLLMMonitoringStore(); // Computed properties const dashboardData = computed(() => analyticsStore.dashboardData); const systemHealthStatus = computed(() => llmMonitoringStore.systemHealth?.status || 'unknown'); const costAnalysis = computed(() => analyticsStore.costAnalysis); const isLoading = computed(() => analyticsStore.isLoading || llmMonitoringStore.isLoading); const hasError = computed(() => !!analyticsStore.error || !!llmMonitoringStore.error); const firstError = computed(() => analyticsStore.error || llmMonitoringStore.error); // Auto-refresh functionality const isAutoRefreshEnabled = ref(false); const toggleAutoRefresh = () => { isAutoRefreshEnabled.value = !isAutoRefreshEnabled.value; }; const refreshNow = async () => { await refreshAll(); }; const refreshAll = async () => { await Promise.all([ analyticsStore.loadDashboardData(), llmMonitoringStore.fetchSystemHealth() ]); }; // Reactive data const selectedTimeRange = ref('last7days'); const customStartDate = ref(''); const customEndDate = ref(''); const autoRefreshInterval = ref(30000); // 30 seconds
Key Points:
- ✅ Uses
to read from stores (maintains reactivity)computed() - ✅ Calls store methods for actions (like
)loadDashboardData() - ✅ Vue automatically re-renders when store values change
- ✅ No manual DOM updates
- ✅ No
or similar hacksforceUpdate()
View Reactivity in Action
Notice how the component uses
computed():
// ✅ CORRECT - Computed maintains reactivity const dashboardData = computed(() => analyticsStore.dashboardData); const isLoading = computed(() => analyticsStore.isLoading); const hasError = computed(() => !!analyticsStore.error);
When
analyticsStore.dashboardData changes (updated by a service), Vue automatically:
- Detects the change (because
is reactive)ref() - Re-runs the computed
- Updates the template
- Re-renders the component
No manual updates needed!
Critical Pattern #4: Response → Store → View Reactivity
The flow is ALWAYS:
Service makes API call ↓ Service updates store state ↓ Vue reactivity detects change ↓ Component re-renders automatically
Complete Example
Here's a complete example showing the flow:
1. Store (State Only):
// stores/conversationsStore.ts export const useConversationsStore = defineStore('conversations', () => { const conversations = ref<Conversation[]>([]); const isLoading = ref(false); const error = ref<string | null>(null); const currentConversation = computed(() => conversations.value.find(c => c.id === currentConversationId.value) ); function setConversations(newConversations: Conversation[]) { conversations.value = newConversations; // ← State update } function setLoading(loading: boolean) { isLoading.value = loading; // ← State update } function setError(errorMessage: string | null) { error.value = errorMessage; // ← State update } return { conversations, isLoading, error, currentConversation, setConversations, setLoading, setError }; });
2. Service (API Calls + Store Updates):
// services/conversationsService.ts import { useConversationsStore } from '@/stores/conversationsStore'; import { buildRequest } from '@/services/agent2agent/utils/builders'; import { agent2AgentApi } from '@/services/agent2agent/api/agent2agent.api'; export const conversationsService = { async loadConversations() { const store = useConversationsStore(); store.setLoading(true); // ← Update store store.setError(null); // ← Update store try { // Build request with transport types const request = buildRequest.plan.list({ conversationId: 'current' }); // Make API call const response = await agent2AgentApi.executeStrictRequest(request); // Update store with response store.setConversations(response.result.conversations); // ← Update store return response.result; } catch (error) { store.setError(error.message); // ← Update store throw error; } finally { store.setLoading(false); // ← Update store } } };
3. Component (Uses Service, Reads Store):
<template> <div> <!-- Vue automatically reacts to store changes --> <div v-if="isLoading">Loading...</div> <div v-if="error">{{ error }}</div> <div v-for="conv in conversations" :key="conv.id"> {{ conv.title }} </div> <button @click="loadData">Load Conversations</button> </div> </template> <script setup lang="ts"> import { computed, onMounted } from 'vue'; import { useConversationsStore } from '@/stores/conversationsStore'; import { conversationsService } from '@/services/conversationsService'; const store = useConversationsStore(); // Read from store using computed (maintains reactivity) const conversations = computed(() => store.conversations); const isLoading = computed(() => store.isLoading); const error = computed(() => store.error); async function loadData() { // Call service method (not store method) await conversationsService.loadConversations(); // Store updated by service // Vue automatically re-renders because computed values changed } onMounted(() => { loadData(); }); </script>
Common Mistakes Agents Make
❌ Mistake 1: API Calls in Stores
// ❌ 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
// ❌ 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
// ❌ WRONG - Raw fetch without transport types const response = await fetch('/api/plan', { method: 'POST', body: JSON.stringify({ conversationId }) });
Fix: Use
buildRequest with transport types:
const request = buildRequest.plan.read({ conversationId }); const response = await agent2AgentApi.executeStrictRequest(request);
❌ Mistake 4: Not Using Computed for Store Values
// ❌ 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
// ❌ 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:
import type { StrictA2ARequest, StrictA2ASuccessResponse, StrictA2AErrorResponse, AgentTaskMode, BuildAction, PlanAction, StrictBuildRequest, StrictPlanRequest, } from '@orchestrator-ai/transport-types';
Build requests using builders:
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
for reactivitycomputed() - Vue reactivity handles UI updates automatically
- No manual DOM manipulation
- No
or similar hacksforceUpdate()
Related Documentation
- Architecture Details: ARCHITECTURE.md - Complete architecture patterns
- Transport Types:
package@orchestrator-ai/transport-types - A2A Protocol: See Back-End Structure Skill for A2A compliance
Troubleshooting
Problem: Store changes don't update UI
- Solution: Use
when reading from store in componentscomputed() - Solution: Ensure store uses
for state (not plain objects)ref()
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
builders with transport types, not raw fetchbuildRequest
Problem: Component doesn't react to store changes
- Solution: Check that component uses
to read from storecomputed() - Solution: Verify store setters update
values (not plain assignments)ref()