feat: add AIUI node capabilities — file reading, log tailing, bitcoin/lnd deep data
Add readFileAsText() to filebrowser client, read-file and tail-logs action handlers to context broker, bitcoin.getinfo and lnd.getinfo RPC enrichment for context categories, and update AIUI protocol types. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
11cee9dc70
commit
1bb72dc87e
@ -1,75 +1,145 @@
|
||||
# Fix Content Clipping + Hide Network Tabs in CloudFolder
|
||||
# Expand AIUI Node Capabilities
|
||||
|
||||
## Context
|
||||
Content clips/cuts off before reaching the bottom across CloudFolder, Marketplace, AppDetails — both desktop and mobile. Fixed UI elements (toolbars, search) must stay sticky while scrollable content extends to the tab bar (mobile) or viewport bottom (desktop). Also: hide Cloud/Network switcher on mobile in CloudFolder detail view.
|
||||
AIUI currently sees basic app status and file names but can't read files, check Bitcoin/LND details, or view app logs. Expanding these 4 capabilities makes AIUI a truly useful node assistant.
|
||||
|
||||
## Root Cause
|
||||
`.perspective-container-wrapper` has `height: 100%` + `overflow: hidden` + dynamic `pt-20`/`pt-40`. With `box-sizing: border-box`, the padding shrinks the content area by 80-160px on mobile.
|
||||
---
|
||||
|
||||
## 1. File Reading (frontend-only) [DONE]
|
||||
|
||||
### `neode-ui/src/api/filebrowser-client.ts`
|
||||
Add `readFileAsText(path, maxBytes = 102400)` method:
|
||||
- Fetch from existing `/app/filebrowser/api/raw{path}?auth={token}` endpoint
|
||||
- Limit response to 100KB (truncate with note)
|
||||
- Only allow text-like extensions: `.txt`, `.md`, `.json`, `.csv`, `.log`, `.conf`, `.yaml`, `.yml`, `.toml`, `.xml`, `.html`, `.css`, `.js`, `.ts`, `.py`, `.sh`
|
||||
- Return `{ content: string, truncated: boolean, size: number }`
|
||||
|
||||
### `neode-ui/src/types/aiui-protocol.ts`
|
||||
Add `'read-file'` and `'tail-logs'` to `AIActionType` union.
|
||||
|
||||
### `neode-ui/src/services/contextBroker.ts`
|
||||
Add `read-file` action handler:
|
||||
- Check `files` permission is enabled
|
||||
- Validate path param exists, validate extension
|
||||
- Call `fileBrowserClient.readFileAsText(path)`
|
||||
- Return content in action response
|
||||
|
||||
### `AIUI/packages/app/src/composables/useArchy.ts`
|
||||
- Add `readFile(path: string)` helper that calls `archyBridge.requestAction('read-file', { path })`
|
||||
- Update `buildArchyContext()` files section: mention "You can read file contents by requesting the read-file action with a file path."
|
||||
|
||||
---
|
||||
|
||||
## 2. App Log Viewing (frontend-only)
|
||||
|
||||
### `neode-ui/src/services/contextBroker.ts`
|
||||
Add `tail-logs` action handler:
|
||||
- Check `apps` permission is enabled
|
||||
- Params: `{ appId: string, lines?: string }` (default 50, max 200)
|
||||
- Call existing `rpcClient.call({ method: 'container-logs', params: { app_id, lines } })`
|
||||
- Return log lines in action response
|
||||
|
||||
### `AIUI/packages/app/src/composables/useArchy.ts`
|
||||
- Add `tailLogs(appId: string, lines?: number)` helper
|
||||
- Update `buildArchyContext()` apps section: "You can view recent app logs by requesting the tail-logs action with an appId."
|
||||
|
||||
---
|
||||
|
||||
## 3. Bitcoin Deep Data (backend + frontend)
|
||||
|
||||
### `core/archipelago/src/api/rpc/mod.rs`
|
||||
Add routing: `"bitcoin.getinfo" => self.handle_bitcoin_getinfo().await`
|
||||
|
||||
### New: `core/archipelago/src/api/rpc/bitcoin.rs`
|
||||
Add `handle_bitcoin_getinfo()`:
|
||||
- Use `reqwest` to POST to `http://127.0.0.1:8332` with Basic Auth `archipelago:archipelago123`
|
||||
- Call `getblockchaininfo` JSON-RPC method
|
||||
- Call `getmempoolinfo` JSON-RPC method
|
||||
- Return sanitized JSON:
|
||||
```json
|
||||
{
|
||||
"block_height": 800000,
|
||||
"sync_progress": 0.9999,
|
||||
"chain": "main",
|
||||
"difficulty": 72006146,
|
||||
"mempool_size": 45000000,
|
||||
"mempool_tx_count": 12500,
|
||||
"verification_progress": 0.9999
|
||||
}
|
||||
```
|
||||
<main flex-1 overflow-hidden pb-20> ← reserves tab bar space (correct)
|
||||
.perspective-container-wrapper h-100% pt-20 ← padding SHRINKS content area (BUG)
|
||||
.perspective-container h-100% overflow-hidden
|
||||
.view-wrapper absolute inset-0
|
||||
content div overflow-y-auto h-full ← scroll viewport 80-160px too small
|
||||
- Handle connection errors gracefully (Bitcoin Core might be syncing or down)
|
||||
|
||||
### `neode-ui/src/services/contextBroker.ts`
|
||||
Enrich `bitcoin` category sanitizer:
|
||||
- Call `rpcClient.call({ method: 'bitcoin.getinfo' })`
|
||||
- Merge with existing container status data
|
||||
- Return block height, sync %, chain, mempool stats
|
||||
|
||||
### `AIUI/packages/app/src/composables/useArchy.ts`
|
||||
- Add `bitcoinInfo` ref with block height, sync %, etc.
|
||||
- Update `buildArchyContext()`: "**Bitcoin:** Block 800,000 (99.99% synced), mainnet, mempool: 12,500 txs"
|
||||
|
||||
---
|
||||
|
||||
## 4. LND Deep Data (backend + frontend)
|
||||
|
||||
### `core/archipelago/src/api/rpc/mod.rs`
|
||||
Add routing: `"lnd.getinfo" => self.handle_lnd_getinfo().await`
|
||||
|
||||
### New: `core/archipelago/src/api/rpc/lnd.rs`
|
||||
Add `handle_lnd_getinfo()`:
|
||||
- Read admin macaroon from `/var/lib/archipelago/lnd/data/chain/bitcoin/mainnet/admin.macaroon`
|
||||
- Use `reqwest` to GET `https://127.0.0.1:8080/v1/getinfo` with `Grpc-Metadata-macaroon` header (hex-encoded)
|
||||
- GET `https://127.0.0.1:8080/v1/balance/channels` for channel balance
|
||||
- GET `https://127.0.0.1:8080/v1/balance/blockchain` for on-chain balance
|
||||
- Accept self-signed cert (`reqwest::Client::builder().danger_accept_invalid_certs(true)`)
|
||||
- Return sanitized JSON:
|
||||
```json
|
||||
{
|
||||
"alias": "my-node",
|
||||
"num_active_channels": 5,
|
||||
"num_peers": 8,
|
||||
"synced_to_chain": true,
|
||||
"block_height": 800000,
|
||||
"balance_sats": 1500000,
|
||||
"channel_balance_sats": 3000000,
|
||||
"pending_open_balance": 0
|
||||
}
|
||||
```
|
||||
- **Never expose**: private keys, seed, macaroon, node pubkey (optional — could include for identification)
|
||||
- Handle errors: LND might be locked, syncing, or not installed
|
||||
|
||||
## Solution: Move padding from wrapper to scroll container
|
||||
### `neode-ui/src/services/contextBroker.ts`
|
||||
Enrich `wallet` category:
|
||||
- Call `rpcClient.call({ method: 'lnd.getinfo' })`
|
||||
- Return alias, channels, balances, sync status
|
||||
|
||||
### File 1: `neode-ui/src/views/Dashboard.vue` [DONE]
|
||||
### `AIUI/packages/app/src/composables/useArchy.ts`
|
||||
- Add `lndInfo` ref
|
||||
- Update `buildArchyContext()`: "**Lightning:** 5 channels, 3M sats in channels, 1.5M on-chain, synced"
|
||||
|
||||
**A. Add computed** (~line 440):
|
||||
```ts
|
||||
const mobileTabPaddingTop = computed(() => {
|
||||
if (typeof window === 'undefined' || window.innerWidth >= 768) return 0
|
||||
if (showAppsTabs.value && showNetworkTabs.value) return 160
|
||||
if (showAppsTabs.value || showNetworkTabs.value) return 80
|
||||
return 0
|
||||
})
|
||||
```
|
||||
---
|
||||
|
||||
**B. Remove padding from wrapper** (line 258):
|
||||
Remove `'pt-40': showAppsTabs && showNetworkTabs, 'pt-20': showAppsTabs !== showNetworkTabs` from `:class`.
|
||||
## File Summary
|
||||
|
||||
**C. Apply offset to content div instead** (lines 269-277):
|
||||
```html
|
||||
<div v-else
|
||||
:class="['px-4 pt-4 md:pt-8 md:px-8 overflow-y-auto h-full',
|
||||
needsMobileBackButtonSpace
|
||||
? 'pb-[calc(var(--mobile-tab-bar-height,_72px)+96px)] md:pb-8'
|
||||
: 'pb-4 md:pb-8'
|
||||
]"
|
||||
:style="mobileTabPaddingTop ? { paddingTop: (mobileTabPaddingTop + 16) + 'px' } : undefined"
|
||||
>
|
||||
```
|
||||
- When mobile tabs showing: `:style` overrides `pt-4` with dynamic value
|
||||
- When no tabs (or desktop): `:style` is undefined, Tailwind `pt-4 md:pt-8` applies
|
||||
- Bottom padding reduced from `pb-28 md:pb-24` to `pb-4 md:pb-8` (main's `pb-20` already handles tab bar)
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `neode-ui/src/api/filebrowser-client.ts` | Add `readFileAsText()` |
|
||||
| `neode-ui/src/types/aiui-protocol.ts` | Add `read-file`, `tail-logs` action types |
|
||||
| `neode-ui/src/services/contextBroker.ts` | Add 2 action handlers + enrich bitcoin/wallet categories |
|
||||
| `neode-ui/src/stores/aiPermissions.ts` | Update category descriptions |
|
||||
| `core/archipelago/src/api/rpc/mod.rs` | Add 2 route entries |
|
||||
| `core/archipelago/src/api/rpc/bitcoin.rs` | New: Bitcoin Core RPC proxy |
|
||||
| `core/archipelago/src/api/rpc/lnd.rs` | New: LND REST proxy |
|
||||
| `AIUI/packages/app/src/composables/useArchy.ts` | Add helpers + enrich buildArchyContext() |
|
||||
|
||||
**D. Hide Cloud/Network tabs in CloudFolder** (line 451):
|
||||
```ts
|
||||
if (route.name === 'cloud-folder') return false
|
||||
```
|
||||
|
||||
### File 2: `neode-ui/src/views/CloudFolder.vue` [DONE]
|
||||
Delete spacer div at line 156.
|
||||
|
||||
### File 3: `neode-ui/src/views/AppDetails.vue` [DONE]
|
||||
Delete spacer div at line 377.
|
||||
|
||||
### File 4: `neode-ui/src/views/MarketplaceAppDetails.vue` [DONE]
|
||||
Delete spacer div at line 324.
|
||||
|
||||
### File 5: `neode-ui/src/views/Marketplace.vue` [DONE]
|
||||
Change `pb-48` → `pb-4` on line 121.
|
||||
|
||||
## Safety
|
||||
- 3D transitions preserved: `overflow: hidden` stays on wrapper + perspective-container
|
||||
- Chat view unaffected: has its own branch bypassing the content div
|
||||
- Desktop unaffected: `mobileTabPaddingTop` returns 0
|
||||
- Back button space preserved: `needsMobileBackButtonSpace` still adds extra bottom padding
|
||||
|
||||
## Verification [DONE]
|
||||
1. `cd neode-ui && npm run build`
|
||||
2. `./scripts/deploy-to-target.sh --live`
|
||||
3. Test all views on mobile + desktop: CloudFolder, Marketplace, AppDetails, Chat, Home, page transitions
|
||||
## Verification
|
||||
1. `cd neode-ui && npm run build` — frontend builds
|
||||
2. `./scripts/deploy-to-target.sh --live` — deploys + builds Rust backend on server
|
||||
3. Test in AIUI chat:
|
||||
- "What files do I have?" → sees file list
|
||||
- "Read my config.txt" → gets file content
|
||||
- "How's my Bitcoin node?" → block height, sync %, mempool
|
||||
- "What's my Lightning balance?" → channel count, sats balance
|
||||
- "Why is Mempool not working?" → views recent logs
|
||||
- "Show me the last 50 lines of Bitcoin logs" → log output
|
||||
|
||||
@ -121,6 +121,40 @@ class FileBrowserClient {
|
||||
return { totalSize, folderCount, fileCount }
|
||||
}
|
||||
|
||||
private static TEXT_EXTENSIONS = new Set([
|
||||
'txt', 'md', 'json', 'csv', 'log', 'conf', 'yaml', 'yml', 'toml', 'xml',
|
||||
'html', 'css', 'js', 'ts', 'py', 'sh', 'bash', 'env', 'ini', 'cfg',
|
||||
'sql', 'rs', 'go', 'java', 'c', 'h', 'cpp', 'hpp', 'rb', 'php',
|
||||
'dockerfile', 'makefile', 'gitignore', 'editorconfig',
|
||||
])
|
||||
|
||||
isTextFile(path: string): boolean {
|
||||
const ext = path.includes('.') ? path.split('.').pop()!.toLowerCase() : ''
|
||||
const name = path.split('/').pop()?.toLowerCase() || ''
|
||||
return FileBrowserClient.TEXT_EXTENSIONS.has(ext) || FileBrowserClient.TEXT_EXTENSIONS.has(name)
|
||||
}
|
||||
|
||||
async readFileAsText(path: string, maxBytes = 102400): Promise<{ content: string; truncated: boolean; size: number }> {
|
||||
if (!this.isAuthenticated) {
|
||||
const ok = await this.login()
|
||||
if (!ok) throw new Error('FileBrowser authentication failed')
|
||||
}
|
||||
if (!this.isTextFile(path)) {
|
||||
throw new Error(`Cannot read binary file: ${path}`)
|
||||
}
|
||||
const safePath = path.startsWith('/') ? path : `/${path}`
|
||||
const res = await fetch(`${this.baseUrl}/api/raw${safePath}`, {
|
||||
headers: this.headers(),
|
||||
})
|
||||
if (!res.ok) throw new Error(`Failed to read file: ${res.status}`)
|
||||
const blob = await res.blob()
|
||||
const size = blob.size
|
||||
const truncated = size > maxBytes
|
||||
const slice = truncated ? blob.slice(0, maxBytes) : blob
|
||||
const content = await slice.text()
|
||||
return { content, truncated, size }
|
||||
}
|
||||
|
||||
async rename(oldPath: string, newName: string): Promise<void> {
|
||||
const safePath = oldPath.startsWith('/') ? oldPath : `/${oldPath}`
|
||||
const dir = safePath.substring(0, safePath.lastIndexOf('/') + 1)
|
||||
|
||||
@ -177,6 +177,14 @@ export class ContextBroker {
|
||||
error = 'Missing query parameter'
|
||||
break
|
||||
|
||||
case 'read-file':
|
||||
this.handleReadFileAction(id, params.path)
|
||||
return
|
||||
|
||||
case 'tail-logs':
|
||||
this.handleTailLogsAction(id, params.appId, params.lines)
|
||||
return
|
||||
|
||||
default:
|
||||
error = `Unknown action: ${action}`
|
||||
}
|
||||
@ -334,8 +342,8 @@ export class ContextBroker {
|
||||
}
|
||||
}
|
||||
|
||||
// T7: Bitcoin status from bundled app
|
||||
private sanitizeBitcoin(store: ReturnType<typeof useAppStore>): unknown {
|
||||
// T7: Bitcoin status + deep data from backend RPC
|
||||
private async sanitizeBitcoin(store: ReturnType<typeof useAppStore>): Promise<unknown> {
|
||||
const packages = store.packages || {}
|
||||
const containerStore = useContainerStore()
|
||||
|
||||
@ -351,10 +359,19 @@ export class ContextBroker {
|
||||
return { available: false, message: 'Bitcoin Core not running' }
|
||||
}
|
||||
|
||||
return {
|
||||
available: true,
|
||||
status: 'running',
|
||||
network: 'mainnet',
|
||||
try {
|
||||
const info = await rpcClient.call<{
|
||||
block_height: number
|
||||
sync_progress: number
|
||||
chain: string
|
||||
difficulty: number
|
||||
mempool_size: number
|
||||
mempool_tx_count: number
|
||||
verification_progress: number
|
||||
}>({ method: 'bitcoin.getinfo' })
|
||||
return { available: true, status: 'running', ...info }
|
||||
} catch {
|
||||
return { available: true, status: 'running', network: 'mainnet' }
|
||||
}
|
||||
}
|
||||
|
||||
@ -437,8 +454,8 @@ export class ContextBroker {
|
||||
}
|
||||
}
|
||||
|
||||
// T12: Wallet — LND aggregate data
|
||||
private sanitizeWallet(store: ReturnType<typeof useAppStore>): unknown {
|
||||
// T12: Wallet — LND deep data from backend RPC
|
||||
private async sanitizeWallet(store: ReturnType<typeof useAppStore>): Promise<unknown> {
|
||||
const packages = store.packages || {}
|
||||
const containerStore = useContainerStore()
|
||||
|
||||
@ -454,10 +471,20 @@ export class ContextBroker {
|
||||
return { available: false, message: 'Lightning (LND) not running' }
|
||||
}
|
||||
|
||||
return {
|
||||
available: true,
|
||||
status: 'running',
|
||||
message: 'LND is running. Balance details require backend wallet RPC.',
|
||||
try {
|
||||
const info = await rpcClient.call<{
|
||||
alias: string
|
||||
num_active_channels: number
|
||||
num_peers: number
|
||||
synced_to_chain: boolean
|
||||
block_height: number
|
||||
balance_sats: number
|
||||
channel_balance_sats: number
|
||||
pending_open_balance: number
|
||||
}>({ method: 'lnd.getinfo' })
|
||||
return { available: true, status: 'running', ...info }
|
||||
} catch {
|
||||
return { available: true, status: 'running', message: 'LND is running but detailed info unavailable' }
|
||||
}
|
||||
}
|
||||
|
||||
@ -470,6 +497,59 @@ export class ContextBroker {
|
||||
}
|
||||
}
|
||||
|
||||
private async handleReadFileAction(id: string, path?: string) {
|
||||
const perms = useAIPermissionsStore()
|
||||
if (!perms.isEnabled('files')) {
|
||||
this.postToIframe({ type: 'action:response', id, success: false, error: 'File access not permitted' } satisfies ArchyActionResponse)
|
||||
return
|
||||
}
|
||||
if (!path) {
|
||||
this.postToIframe({ type: 'action:response', id, success: false, error: 'Missing path parameter' } satisfies ArchyActionResponse)
|
||||
return
|
||||
}
|
||||
try {
|
||||
if (!fileBrowserClient.isAuthenticated) {
|
||||
const ok = await fileBrowserClient.login()
|
||||
if (!ok) throw new Error('FileBrowser authentication failed')
|
||||
}
|
||||
const result = await fileBrowserClient.readFileAsText(path)
|
||||
this.postToIframe({
|
||||
type: 'action:response', id, success: true,
|
||||
data: { content: result.content, truncated: result.truncated, size: result.size, path },
|
||||
} as ArchyActionResponse)
|
||||
} catch (err) {
|
||||
this.postToIframe({
|
||||
type: 'action:response', id, success: false,
|
||||
error: err instanceof Error ? err.message : 'Failed to read file',
|
||||
} satisfies ArchyActionResponse)
|
||||
}
|
||||
}
|
||||
|
||||
private async handleTailLogsAction(id: string, appId?: string, linesStr?: string) {
|
||||
const perms = useAIPermissionsStore()
|
||||
if (!perms.isEnabled('apps')) {
|
||||
this.postToIframe({ type: 'action:response', id, success: false, error: 'App access not permitted' } satisfies ArchyActionResponse)
|
||||
return
|
||||
}
|
||||
if (!appId) {
|
||||
this.postToIframe({ type: 'action:response', id, success: false, error: 'Missing appId parameter' } satisfies ArchyActionResponse)
|
||||
return
|
||||
}
|
||||
const lines = Math.min(parseInt(linesStr || '50', 10) || 50, 200)
|
||||
try {
|
||||
const logs = await rpcClient.call<string[]>({ method: 'container-logs', params: { app_id: appId, lines } })
|
||||
this.postToIframe({
|
||||
type: 'action:response', id, success: true,
|
||||
data: { appId, lines: logs, count: logs.length },
|
||||
} as ArchyActionResponse)
|
||||
} catch (err) {
|
||||
this.postToIframe({
|
||||
type: 'action:response', id, success: false,
|
||||
error: err instanceof Error ? err.message : 'Failed to fetch logs',
|
||||
} satisfies ArchyActionResponse)
|
||||
}
|
||||
}
|
||||
|
||||
private postToIframe(msg: ArchyResponse) {
|
||||
if (!this.iframe.value?.contentWindow) return
|
||||
this.iframe.value.contentWindow.postMessage(msg, this.allowedOrigin)
|
||||
|
||||
@ -19,7 +19,7 @@ export type AIContextCategory =
|
||||
| 'bitcoin'
|
||||
|
||||
/** Actions AIUI can request Archy to perform */
|
||||
export type AIActionType = 'install-app' | 'open-app' | 'navigate' | 'launch-app' | 'search-web'
|
||||
export type AIActionType = 'install-app' | 'open-app' | 'navigate' | 'launch-app' | 'search-web' | 'read-file' | 'tail-logs'
|
||||
|
||||
// ─── AIUI → Archy (Requests) ───────────────────────────────────────────────
|
||||
|
||||
@ -65,6 +65,7 @@ export interface ArchyActionResponse {
|
||||
id: string
|
||||
success: boolean
|
||||
error?: string
|
||||
data?: unknown
|
||||
}
|
||||
|
||||
export interface ArchyThemeResponse {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user