feat(13-06): AIUI renders Archy content through setArchyContent (D-12)

- archyBridge.ts: content:push case resolving the pending content:request
  by id, and requestArchyContent(kind, scope) mirroring requestContext's
  shape. Not in the plan's files_modified list, but required to satisfy
  Task 3's own instruction to register the content:push handler on the
  existing single bridge listener rather than adding a second
  window.addEventListener('message') — see SUMMARY deviations.
- useContentPanel.ts: setArchyContent + archyContentActive; guards only
  the panelFilms/panelSongs/panelPodcasts assignments inside
  updatePanelFromText so Archy-sourced grids stay the source of truth
  once populated, per plan scope. Books/TV/images/places/magazine/code/
  recipes/news are untouched (13-PATTERNS.md: partial deprecation).
- useArchy.ts: requestArchyContent(kind, scope) calling
  archyBridge.requestArchyContent then useContentPanel().setArchyContent.
  No FilmGrid/SongGrid/NewsGrid/ContentGridView/content.ts edits (D-12).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-08-03 19:47:51 -04:00
co-authored by Claude Opus 5
parent ce7a2b2cd4
commit b771357902
3 changed files with 131 additions and 3 deletions
@@ -1,6 +1,8 @@
import { ref, readonly } from 'vue'
import { archyBridge } from '@/services/archyBridge'
import { useTheme } from '@/composables/useTheme'
import { useContentPanel } from '@/composables/useContentPanel'
import type { Film, Song, Podcast } from '@aiui/core/types/content'
import {
mockArchyApps, mockArchySystem, mockArchyNetwork,
mockArchyWallet, mockArchyBitcoin, mockArchyFiles,
@@ -203,6 +205,34 @@ export function useArchy() {
return null
}
/**
* Request a batch of grid-ready content from Archy (D-12/D-14, AIUI-03)
* and hand it to `useContentPanel`'s `setArchyContent`, which is the
* source of truth for `FilmGrid`/`SongGrid` once a node has supplied it.
* Mirrors `archyBridge.requestContext`'s permitted/not-permitted shape —
* no new convention.
*/
async function requestArchyContent(
kind: 'films' | 'songs' | 'podcasts' | 'all' = 'all',
scope: 'own' | 'peers' | 'owned' = 'own',
) {
if (!isInitialized.value) return
try {
const res = await archyBridge.requestArchyContent(kind, scope)
if (!res.permitted) {
console.warn('[AIUI Archy] content: not permitted — user should enable Media/File access in Archy Settings')
return
}
useContentPanel().setArchyContent({
films: res.films as Film[],
songs: res.songs as Song[],
podcasts: res.podcasts as Podcast[],
})
} catch (err) {
console.warn('[AIUI Archy] content fetch failed:', (err as Error)?.message ?? err)
}
}
/** Apply accent color as CSS custom property */
function applyAccentColor(color: string) {
document.documentElement.style.setProperty('--color-accent', color)
@@ -302,6 +332,7 @@ export function useArchy() {
requestAction,
readFile,
tailLogs,
requestArchyContent,
buildArchyContext,
}
}
@@ -65,6 +65,14 @@ const selectedDesignSystemItem = ref<DesignSystemItem | null>(null)
const longFormArticle = ref<{ content: string; title?: string } | null>(null)
const pdfUrl = ref<{ url: string; title?: string } | null>(null)
const mapPlaces = ref<Place[]>([])
/** True once `setArchyContent` has populated panelFilms/panelSongs/
* panelPodcasts from a real node (D-12). While true, `updatePanelFromText`
* leaves those three buckets alone — the Archy-sourced grids are the
* source of truth for them, not the model's own reply text. Every other
* bucket (books, TV, images, places, magazine, code, recipes, news) has no
* Archy source in this plan's scope and keeps using the regex path
* unconditionally (13-PATTERNS.md: partial deprecation, not a removal). */
const archyContentActive = ref(false)
export interface DesignSystemItem {
id: string
@@ -187,13 +195,18 @@ export function useContentPanel() {
const visibleApps = showApps ? apps : []
const visibleCodeBlocks = showCode ? codeBlocks : []
panelFilms.value = visibleFilms
// Archy-sourced films/songs/podcasts are the source of truth once a
// node has supplied them (D-12) — don't let this turn's regex
// extraction of the model's own reply text overwrite them.
if (!archyContentActive.value) {
panelFilms.value = visibleFilms
panelSongs.value = visibleSongs
panelPodcasts.value = visiblePodcasts
}
panelBooks.value = visibleBooks
panelTVSeries.value = visibleTVSeries
panelImages.value = visibleImages
panelPlaces.value = visiblePlaces
panelSongs.value = visibleSongs
panelPodcasts.value = visiblePodcasts
panelWebResults.value = visibleNews
panelWebsites.value = visibleWebsites
panelRecipes.value = visibleRecipes
@@ -265,6 +278,27 @@ export function useContentPanel() {
panelOpen.value = tabs.length > 0
}
/**
* Populate the film/song/podcast grids directly from a node's real
* content (D-12) — `useArchy.ts`'s `requestArchyContent` calls this from
* its `content:push` handler. Bypasses `updatePanelFromText`'s regex
* path entirely for these three buckets and marks `archyContentActive`
* so a later `updatePanelFromText` call (from an unrelated chat turn)
* does not clobber them back to a regex-scraped or empty state.
*
* TMDB posters, web search and RSS remain unavailable on a node (their
* Vite plugins are dev-server-only — 13-CONTEXT.md landmines), so a
* `Film`/`Song` adapted from peer/own-node data has no `posterUrl`/
* `coverUrl`; `FilmGrid`/`SongGrid` already render their existing
* no-artwork fallback for that case (unchanged by this plan, D-12).
*/
function setArchyContent(bundle: { films?: Film[]; songs?: Song[]; podcasts?: Podcast[] }) {
panelFilms.value = bundle.films ?? []
panelSongs.value = bundle.songs ?? []
panelPodcasts.value = bundle.podcasts ?? []
archyContentActive.value = true
}
function setActiveTab(tab: ContentTab) {
if (availableTabs.value.includes(tab)) activeTab.value = tab
}
@@ -461,6 +495,8 @@ export function useContentPanel() {
panelPlaces,
panelSongs,
panelPodcasts,
archyContentActive,
setArchyContent,
panelWebResults,
panelWebsites,
panelRecipes,
@@ -12,6 +12,20 @@ interface ContextResponse {
permitted: boolean
}
/** `kind`/`scope` for a `content:request` — see neode-ui's
* `types/aiui-protocol.ts` (`AIUIContentRequest`/`ArchyContentPush`). Archy
* decides the RPC method from `scope`; this bundle carries only data, never
* a method name (T-13-34). Films/songs/podcasts are typed `unknown[]` here
* deliberately — this transport module has no reason to know AIUI's content
* shapes; `useArchy.ts`/`useContentPanel.ts` cast them to `Film[]`/`Song[]`/
* `Podcast[]` at the one call site that does. */
interface ContentResponse {
films: unknown[]
songs: unknown[]
podcasts: unknown[]
permitted: boolean
}
export interface ActionResponse {
success: boolean
error?: string
@@ -87,6 +101,20 @@ function handleMessage(event: MessageEvent) {
break
}
case 'content:push': {
const pending = pendingRequests.get(msg.id)
if (pending) {
pendingRequests.delete(msg.id)
pending.resolve({
films: Array.isArray(msg.films) ? msg.films : [],
songs: Array.isArray(msg.songs) ? msg.songs : [],
podcasts: Array.isArray(msg.podcasts) ? msg.podcasts : [],
permitted: msg.permitted !== false,
})
}
break
}
case 'permissions:update': {
currentPermissions = msg.categories || []
for (const cb of permissionsCallbacks) cb(currentPermissions)
@@ -261,6 +289,39 @@ export const archyBridge = {
})
},
/**
* Request a batch of grid-ready content from Archy (D-12/D-14, AIUI-03).
* `kind` and `scope` are enums the broker interprets — this bridge never
* lets AIUI name an RPC method or params. Resolves with `permitted:
* false` and empty arrays if the media/files permission categories
* aren't granted, rather than rejecting (mirrors `requestContext`'s
* shape so `useArchy.ts` can check `.permitted` the same way).
*/
requestArchyContent(
kind: 'films' | 'songs' | 'podcasts' | 'all',
scope?: 'own' | 'peers' | 'owned',
): Promise<ContentResponse> {
const id = generateId()
return new Promise((resolve, reject) => {
pendingRequests.set(id, { resolve: resolve as (v: unknown) => void, reject })
postToParent({
type: 'content:request',
id,
kind,
scope,
})
// Peer/owned scopes can fan out across multiple mesh/Tor round trips
// node-side — give this more room than the plain context:request's 10s.
setTimeout(() => {
if (pendingRequests.has(id)) {
pendingRequests.delete(id)
reject(new Error(`Content request timed out: ${kind}`))
}
}, 30000)
})
},
/** Request Archy's theme info */
requestTheme() {
postToParent({ type: 'theme:request' })