App mode output feed to only show current session results for outputs defined in the app (#9307)

## Summary

Updates app mode to only show images from:
- the workflow that generated the image
- in the current session
- for the outputs selected in the builder

## Changes

- **What**: 
- adds new mapping of jobid -> workflow path [cant use id here as it is
not guaranteed unique], capped at 4k entries
- fix bug where executing a workflow then quickly switching tabs
associated incorrect workflow
- add missing output history tests

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9307-App-mode-output-feed-to-only-show-current-session-results-for-outputs-defined-in-the-app-3156d73d36508142b4bbca3f938fc5c2)
by [Unito](https://www.unito.io)
This commit is contained in:
pythongosssss
2026-03-02 19:10:20 +00:00
committed by GitHub
parent 1dd789fa54
commit 0d7dc15916
7 changed files with 692 additions and 85 deletions

View File

@@ -1,29 +1,71 @@
import { useAsyncState } from '@vueuse/core'
import type { MaybeRef } from 'vue'
import { computed, ref, watchEffect } from 'vue'
import { useMediaAssets } from '@/platform/assets/composables/media/useMediaAssets'
import type { IAssetsProvider } from '@/platform/assets/composables/media/IAssetsProvider'
import { useMediaAssets } from '@/platform/assets/composables/media/useMediaAssets'
import { getOutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { flattenNodeOutput } from '@/renderer/extensions/linearMode/flattenNodeOutput'
import { useLinearOutputStore } from '@/renderer/extensions/linearMode/linearOutputStore'
import { getJobDetail } from '@/services/jobOutputCache'
import { useAppModeStore } from '@/stores/appModeStore'
import { useExecutionStore } from '@/stores/executionStore'
import type { ResultItemImpl } from '@/stores/queueStore'
export function useOutputHistory(): {
outputs: IAssetsProvider
allOutputs: (item?: AssetItem) => MaybeRef<ResultItemImpl[]>
allOutputs: (item?: AssetItem) => ResultItemImpl[]
selectFirstHistory: () => void
} {
const outputs = useMediaAssets('output')
void outputs.fetchMediaList()
const backingOutputs = useMediaAssets('output')
void backingOutputs.fetchMediaList()
const linearStore = useLinearOutputStore()
const workflowStore = useWorkflowStore()
const executionStore = useExecutionStore()
const appModeStore = useAppModeStore()
const outputsCache: Record<string, MaybeRef<ResultItemImpl[]>> = {}
function filterByOutputNodes(items: ResultItemImpl[]): ResultItemImpl[] {
const nodeIds = appModeStore.selectedOutputs
if (!nodeIds.length) return items
return items.filter((r) =>
nodeIds.some((id) => String(id) === String(r.nodeId))
)
}
function allOutputs(item?: AssetItem): MaybeRef<ResultItemImpl[]> {
if (item?.id && outputsCache[item.id]) return outputsCache[item.id]
const sessionMedia = computed(() => {
const path = workflowStore.activeWorkflow?.path
if (!path) return []
const user_metadata = getOutputAssetMetadata(item?.user_metadata)
const pathMap = executionStore.jobIdToSessionWorkflowPath
return backingOutputs.media.value.filter((asset) => {
const m = getOutputAssetMetadata(asset?.user_metadata)
return m ? pathMap.get(m.jobId) === path : false
})
})
const outputs: IAssetsProvider = {
...backingOutputs,
media: sessionMedia,
hasMore: ref(false),
isLoadingMore: ref(false),
loadMore: async () => {}
}
const resolvedCache = linearStore.resolvedOutputsCache
const asyncRefs = new Map<
string,
ReturnType<typeof useAsyncState<ResultItemImpl[]>>['state']
>()
function allOutputs(item?: AssetItem): ResultItemImpl[] {
if (!item?.id) return []
const cached = resolvedCache.get(item.id)
if (cached) return filterByOutputNodes(cached)
const user_metadata = getOutputAssetMetadata(item.user_metadata)
if (!user_metadata) return []
// For recently completed jobs still pending resolve, derive order from
@@ -33,33 +75,70 @@ export function useOutputHistory(): {
.filter((i) => i.jobId === user_metadata.jobId && i.output)
.map((i) => i.output!)
if (ordered.length > 0) {
outputsCache[item!.id] = ordered
return ordered
resolvedCache.set(item.id, ordered)
return filterByOutputNodes(ordered)
}
}
// Use metadata when all outputs are present. The /jobs list endpoint
// only returns preview_output (single item), so outputCount may exceed
// allOutputs.length for multi-output jobs.
if (
user_metadata.allOutputs &&
user_metadata.outputCount &&
user_metadata.outputCount <= user_metadata.allOutputs.length
user_metadata.allOutputs?.length &&
(!user_metadata.outputCount ||
user_metadata.outputCount <= user_metadata.allOutputs.length)
) {
const reversed = user_metadata.allOutputs.toReversed()
outputsCache[item!.id] = reversed
return reversed
resolvedCache.set(item.id, reversed)
return filterByOutputNodes(reversed)
}
// Async fallback for multi-output jobs — fetch full /jobs/{id} detail.
// This can be hit if the user executes the job then switches tabs.
const existing = asyncRefs.get(item.id)
if (existing) return filterByOutputNodes(existing.value)
const itemId = item.id
const outputRef = useAsyncState(
getJobDetail(user_metadata.jobId).then((jobDetail) => {
if (!jobDetail?.outputs) return []
return Object.entries(jobDetail.outputs)
const results = Object.entries(jobDetail.outputs)
.flatMap(flattenNodeOutput)
.toReversed()
resolvedCache.set(itemId, results)
return results
}),
[]
).state
outputsCache[item!.id] = outputRef
return outputRef
asyncRefs.set(item.id, outputRef)
return filterByOutputNodes(outputRef.value)
}
return { outputs, allOutputs }
function selectFirstHistory() {
const first = outputs.media.value[0]
if (first) {
linearStore.selectAsLatest(`history:${first.id}:0`)
} else {
linearStore.selectAsLatest(null)
}
}
// Resolve in-progress items when history outputs are loaded.
watchEffect(() => {
if (linearStore.pendingResolve.size === 0) return
for (const jobId of linearStore.pendingResolve) {
const asset = outputs.media.value.find((a) => {
const m = getOutputAssetMetadata(a?.user_metadata)
return m?.jobId === jobId
})
if (!asset) continue
const loaded = allOutputs(asset).length > 0
if (loaded) {
linearStore.resolveIfReady(jobId, true)
if (!linearStore.selectedId) selectFirstHistory()
}
}
})
return { outputs, allOutputs, selectFirstHistory }
}