import { ref, readonly } from 'vue' import { archyBridge } from '@/services/archyBridge' type AIContextCategory = 'apps' | 'system' | 'network' | 'wallet' | 'files' interface ArchyApp { id: string name: string state: string status: string } interface ArchySystemInfo { version?: string name?: string } interface ArchyNetworkInfo { connected?: boolean } export interface ArchyWalletInfo { balanceSats?: number channelCount?: number totalCapacitySats?: number nodePubkey?: string } export interface ArchyFileEntry { name: string path: string size?: number modified?: string type: 'file' | 'folder' } // Singleton reactive state (shared across all components using this composable) const isEmbedded = ref(false) const isInitialized = ref(false) const permissions = ref([]) const accentColor = ref(null) const installedApps = ref([]) const systemInfo = ref({}) const networkInfo = ref({}) const walletInfo = ref({}) const fileList = ref([]) let cleanups: (() => void)[] = [] /** * Reactive composable wrapping archyBridge for Archy ↔ AIUI integration. * Call `init()` once in App.vue when `?embedded=true` is detected. */ export function useArchy() { /** Initialize the bridge and start listening for Archy messages */ function init() { if (isInitialized.value) return const embedded = !!(window as unknown as Record).__AIUI_EMBEDDED__ isEmbedded.value = embedded if (!embedded || !archyBridge.isInArchy()) return archyBridge.init() isInitialized.value = true // Listen for permission updates const unsubPerms = archyBridge.onPermissionsUpdate((cats) => { permissions.value = cats // Auto-fetch context for newly permitted categories fetchPermittedContext(cats) }) cleanups.push(unsubPerms) // Listen for theme updates const unsubTheme = archyBridge.onThemeUpdate((theme) => { accentColor.value = theme.accent applyAccentColor(theme.accent) }) cleanups.push(unsubTheme) // Request theme on init archyBridge.requestTheme() } /** Fetch context for all permitted categories */ async function fetchPermittedContext(cats: AIContextCategory[]) { const fetches: Promise[] = [] if (cats.includes('apps')) { fetches.push( archyBridge.requestContext('apps').then((res) => { if (res.permitted && Array.isArray(res.data)) { installedApps.value = res.data as ArchyApp[] } }).catch(() => {}), ) } if (cats.includes('system')) { fetches.push( archyBridge.requestContext('system').then((res) => { if (res.permitted && res.data) { systemInfo.value = res.data as ArchySystemInfo } }).catch(() => {}), ) } if (cats.includes('network')) { fetches.push( archyBridge.requestContext('network').then((res) => { if (res.permitted && res.data) { networkInfo.value = res.data as ArchyNetworkInfo } }).catch(() => {}), ) } if (cats.includes('wallet')) { fetches.push( archyBridge.requestContext('wallet').then((res) => { if (res.permitted && res.data) { walletInfo.value = res.data as ArchyWalletInfo } }).catch(() => {}), ) } if (cats.includes('files')) { fetches.push( archyBridge.requestContext('files').then((res) => { if (res.permitted && Array.isArray(res.data)) { fileList.value = res.data as ArchyFileEntry[] } }).catch(() => {}), ) } await Promise.all(fetches) } /** Refresh context data (call when user returns to chat) */ async function refreshContext() { if (!isInitialized.value) return await fetchPermittedContext(permissions.value) } /** Request Archy to perform an action */ async function requestAction(action: string, params: Record = {}) { if (!isInitialized.value) return { success: false, error: 'Not initialized' } return archyBridge.requestAction(action, params) } /** Apply accent color as CSS custom property */ function applyAccentColor(color: string) { document.documentElement.style.setProperty('--color-accent', color) } /** Build context string for AI system prompt */ function buildArchyContext(): string { if (!isInitialized.value) return '' const sections: string[] = [] if (permissions.value.includes('apps') && installedApps.value.length > 0) { const appList = installedApps.value .map((a) => `- ${a.name} (${a.state}${a.status ? ', ' + a.status : ''})`) .join('\n') sections.push(`**Installed apps on this node:**\n${appList}`) } if (permissions.value.includes('system') && systemInfo.value.name) { const sys = systemInfo.value sections.push(`**System:** ${sys.name}${sys.version ? ' v' + sys.version : ''}`) } if (permissions.value.includes('network')) { const net = networkInfo.value sections.push(`**Network:** ${net.connected ? 'Connected' : 'Disconnected'}`) } if (permissions.value.includes('wallet') && walletInfo.value.balanceSats !== undefined) { const w = walletInfo.value const balance = w.balanceSats! const parts = [`Balance: ${balance.toLocaleString()} sats`] if (w.channelCount !== undefined) parts.push(`${w.channelCount} channels`) if (w.totalCapacitySats !== undefined) parts.push(`Total capacity: ${w.totalCapacitySats.toLocaleString()} sats`) if (w.nodePubkey) parts.push(`Pubkey: ${w.nodePubkey.slice(0, 8)}...`) sections.push(`**Lightning Wallet:** ${parts.join(' | ')}`) } if (permissions.value.includes('files') && fileList.value.length > 0) { const files = fileList.value const recent = files.slice(0, 20) const fileNames = recent.map((f) => f.name).join(', ') sections.push(`**Files:** ${files.length} files in Nextcloud. Recent: ${fileNames}`) } if (sections.length === 0) return '' return `\n\n**Archy Node Context** (this user is running AIUI on their Archipelago node):\n${sections.join('\n')}\n\nYou can help the user manage their node. Available actions: open an app (open-app), install an app (install-app), navigate in Archy (navigate). When recommending apps, check if they're already installed.` } /** Clean up on component unmount */ function destroy() { for (const cleanup of cleanups) cleanup() cleanups = [] archyBridge.destroy() isInitialized.value = false } return { isEmbedded: readonly(isEmbedded), isInitialized: readonly(isInitialized), permissions: readonly(permissions), accentColor: readonly(accentColor), installedApps: readonly(installedApps), systemInfo: readonly(systemInfo), networkInfo: readonly(networkInfo), walletInfo: readonly(walletInfo), fileList: readonly(fileList), init, destroy, refreshContext, requestAction, buildArchyContext, } }