- 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>
228 lines
7.0 KiB
TypeScript
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,
|
|
}
|
|
}
|