Files
archy/packages/app/src/composables/useArchy.ts
T
DorianandClaude Opus 4.6 20ef35c8e3 feat(archy): extend useArchy with wallet/files context, add ArchyAppsGrid component
- Add wallet (Lightning balance, channels) and files (Nextcloud) context
  categories to useArchy composable with AI prompt injection
- Create archy-apps.ts data file mapping all 18 Archy services
- Build ArchyAppsGrid component with live status from bridge
- Wire ArchyAppsGrid into ContentPanel when embedded in Archy

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 12:15:33 +00:00

228 lines
7.0 KiB
TypeScript

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<AIContextCategory[]>([])
const accentColor = ref<string | null>(null)
const installedApps = ref<ArchyApp[]>([])
const systemInfo = ref<ArchySystemInfo>({})
const networkInfo = ref<ArchyNetworkInfo>({})
const walletInfo = ref<ArchyWalletInfo>({})
const fileList = ref<ArchyFileEntry[]>([])
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<string, unknown>).__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<void>[] = []
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<string, string> = {}) {
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,
}
}