mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-11 16:10:05 +00:00
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:
@@ -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 }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user