fix(aiui): recommendation previews render in buckets the node left empty
The archy content latch was global and permanent: once mount-time content latched archyContentActive, no extracted [[film_ext:…]] recommendation card could ever render again, and an empty tool result kept the latch — so a 'recommend me films' turn beside an empty catalogue showed prose only, with 'Nothing found' overwriting nothing. The chat had lost its rich previews. - setArchyContent records which buckets the node actually supplied (archySupplied) and latches active only on a non-empty delivery - updatePanelFromText's no-overwrite guard is now per-bucket: node truth wins buckets it filled; empty buckets stay writable for extracted previews - the extraction fallback title no longer clobbers 'Nothing found'/'Loading…' Regression tests: previews render in an empty bucket, node truth survives tags in a filled bucket, 'Nothing found' survives a both-empty turn. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -105,9 +105,11 @@ describe('useContentPanel', () => {
|
||||
panel.setArchyContent({ films: [], songs: [], podcasts: [] })
|
||||
expect(panel.panelOpen.value).toBe(false)
|
||||
expect(panel.availableTabs.value).toHaveLength(0)
|
||||
// archyContentActive still flips true — an explicit empty library
|
||||
// is still a real answer from the node, distinct from "never asked".
|
||||
expect(panel.archyContentActive.value).toBe(true)
|
||||
// An empty delivery does NOT latch: nothing was supplied, so there is
|
||||
// no node truth to protect and extraction must stay writable. The
|
||||
// honest empty state is carried by the 'Nothing found' heading, not
|
||||
// by the latch.
|
||||
expect(panel.archyContentActive.value).toBe(false)
|
||||
})
|
||||
|
||||
// Regression (live on archi-dev-box 2026-08-07): "show me paid for peer
|
||||
@@ -133,6 +135,53 @@ describe('useContentPanel', () => {
|
||||
expect(panel.availableTabs.value[0]).toBe('image')
|
||||
expect(panel.panelImages.value).toHaveLength(1)
|
||||
})
|
||||
|
||||
// Regression (operator report 2026-08-07): "recommend me 10 scifi films"
|
||||
// — the node has no films, the model's reply carried [[film_ext:…]] tags,
|
||||
// and the global archy latch (set at mount by the node's own images)
|
||||
// meant extraction could never write panelFilms: ten recommendations
|
||||
// rendered as bare prose beside an empty panel.
|
||||
it('renders extracted recommendation previews in a bucket the node left empty', () => {
|
||||
const img = { id: 'i1', title: 'photo.jpg', url: '/content/x', mimeType: 'image/jpeg' } as unknown as ImageItem
|
||||
panel.setArchyContent({ images: [img], films: [], songs: [], podcasts: [] })
|
||||
|
||||
// The turn: the films scope came back empty, the reply carries tags.
|
||||
panel.beginArchyContentLoad()
|
||||
panel.setArchyContent({ films: [], songs: [], podcasts: [], images: [] })
|
||||
panel.updatePanelFromText(
|
||||
'Here are two sci-fi films:\n\n[[film_ext:Blade Runner 2049|2017|Denis Villeneuve]] — A stunning neo-noir sequel.\n\n[[film_ext:Arrival|2016|Denis Villeneuve]] — Thoughtful first contact.',
|
||||
'recommend me sci-fi films',
|
||||
)
|
||||
|
||||
expect(panel.panelFilms.value.map((f) => f.title)).toEqual(['Blade Runner 2049', 'Arrival'])
|
||||
expect(panel.panelOpen.value).toBe(true)
|
||||
expect(panel.availableTabs.value[0]).toBe('film')
|
||||
})
|
||||
|
||||
it('never lets extracted tags overwrite a bucket the node actually filled', () => {
|
||||
const song = { id: 's1', title: 'Real Node Song', artist: 'A', album: 'B', genres: [] } as Song
|
||||
panel.setArchyContent({ songs: [song] })
|
||||
|
||||
panel.updatePanelFromText(
|
||||
'You might also like [[song_ext:Fake Song|Nobody|2020]]',
|
||||
'what music do I have',
|
||||
)
|
||||
|
||||
expect(panel.panelSongs.value).toHaveLength(1)
|
||||
expect(panel.panelSongs.value[0].title).toBe('Real Node Song')
|
||||
})
|
||||
|
||||
it("keeps 'Nothing found' when the node and the reply both yield nothing", () => {
|
||||
panel.closePanel()
|
||||
panel.beginArchyContentLoad()
|
||||
panel.setArchyContent({ films: [], songs: [], podcasts: [], images: [] })
|
||||
expect(panel.panelTitle.value).toBe('Nothing found')
|
||||
|
||||
panel.updatePanelFromText('I could not find anything matching that on this node.', 'show me films')
|
||||
|
||||
expect(panel.panelTitle.value).toBe('Nothing found')
|
||||
expect(panel.panelOpen.value).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
it('openPlaceDetail and closePlaceDetail work', () => {
|
||||
|
||||
@@ -73,6 +73,14 @@ const mapPlaces = ref<Place[]>([])
|
||||
* 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)
|
||||
/** Which buckets the node's LATEST archy delivery actually filled. The
|
||||
* regex-extraction path must never overwrite a bucket the node supplied
|
||||
* (D-12 node truth), but a bucket the node left EMPTY stays writable so a
|
||||
* recommendation reply's [[film_ext:…]] previews still render as cards —
|
||||
* the global latch used to suppress exactly those (the "chat lost its rich
|
||||
* previews" regression: once mount-time content latched the flag, no
|
||||
* extracted card could ever render again). */
|
||||
const archySupplied = ref({ film: false, song: false, podcast: false, image: false })
|
||||
/** A chat turn that may produce content is in flight. Until it resolves,
|
||||
* the panel heading must NOT keep advertising the previous query's
|
||||
* results — the operator reads that as the answer to what they just
|
||||
@@ -233,12 +241,12 @@ export function useContentPanel() {
|
||||
// joined this set when the adapter started carrying shared photos;
|
||||
// leaving them out here would have let the regex path immediately
|
||||
// wipe the grid the node had just filled.
|
||||
if (!archyContentActive.value) {
|
||||
panelFilms.value = visibleFilms
|
||||
panelSongs.value = visibleSongs
|
||||
panelPodcasts.value = visiblePodcasts
|
||||
panelImages.value = visibleImages
|
||||
}
|
||||
// Per-bucket, not global: the node's truth wins a bucket it filled;
|
||||
// an empty bucket stays open to this turn's extracted previews.
|
||||
if (!archySupplied.value.film) panelFilms.value = visibleFilms
|
||||
if (!archySupplied.value.song) panelSongs.value = visibleSongs
|
||||
if (!archySupplied.value.podcast) panelPodcasts.value = visiblePodcasts
|
||||
if (!archySupplied.value.image) panelImages.value = visibleImages
|
||||
panelBooks.value = visibleBooks
|
||||
panelTVSeries.value = visibleTVSeries
|
||||
panelPlaces.value = visiblePlaces
|
||||
@@ -325,7 +333,10 @@ export function useContentPanel() {
|
||||
else if (visibleApps.length > 0) panelTitle.value = `${visibleApps.length} Apps`
|
||||
else if (visibleWebsites.length > 0) panelTitle.value = `${visibleWebsites.length} Websites`
|
||||
else if (hasNostr) panelTitle.value = 'Nostr'
|
||||
else panelTitle.value = 'Content'
|
||||
// Zero extraction tabs: leave the title alone when an archy delivery
|
||||
// just said 'Nothing found' (or is still 'Loading…') — the closed
|
||||
// panel's heading must not silently become 'Content'.
|
||||
else if (panelTitle.value !== 'Nothing found' && panelTitle.value !== 'Loading…') panelTitle.value = 'Content'
|
||||
|
||||
// Open from the ORDERED tab list (Archy tabs + inferred), not the
|
||||
// regex-only `tabs`: a node-supplied grid whose reply text happens to
|
||||
@@ -366,7 +377,17 @@ export function useContentPanel() {
|
||||
panelSongs.value = bundle.songs ?? []
|
||||
panelPodcasts.value = bundle.podcasts ?? []
|
||||
panelImages.value = bundle.images ?? []
|
||||
archyContentActive.value = true
|
||||
archySupplied.value = {
|
||||
film: panelFilms.value.length > 0,
|
||||
song: panelSongs.value.length > 0,
|
||||
podcast: panelPodcasts.value.length > 0,
|
||||
image: panelImages.value.length > 0,
|
||||
}
|
||||
// Latch only on a non-empty delivery: an empty tool result must not
|
||||
// suppress the recommendation previews extraction can still produce.
|
||||
archyContentActive.value =
|
||||
archySupplied.value.film || archySupplied.value.song ||
|
||||
archySupplied.value.podcast || archySupplied.value.image
|
||||
|
||||
// 13-11 (GAP-FOUND 2026-08-03): `availableTabs`/`activeTab`/`panelOpen`
|
||||
// were previously untouched here — only `updatePanelFromText`'s regex
|
||||
|
||||
Reference in New Issue
Block a user