Skip to content

Commit 84b1c6e

Browse files
claude[bot]claudechristian-byrne
authored
fix: Media Assets group badge count to use previewable_outputs_count (#14347)
_Requested by **Alexis Rolland, Christian Byrne** · [Slack thread](https://comfy-organization.slack.com/archives/C0A4XMHANP3/p1785382578658559?thread_ts=1785382578.658559&cid=C0A4XMHANP3)_ ## Before The Media Assets group badge on a job's stacked asset card reads `outputs_count`, which counts every output item across every node for a job regardless of file type. The expanded/drilldown view only ever renders `task.previewableOutputs` (image, video, audio, 3D, text by extension). Any job that also produces a non-previewable saved file alongside real media — for example `SaveLatent`, which writes a `.latent` file with no previewable extension or media type — gets that file counted toward the badge but never shown when a user drills into the group, so the badge shows a higher number than what's actually there (matching the original report: badge says 3, expanding only shows 2). ## After The badge now reads a new `previewable_outputs_count` field when the backend provides it, computed server-side using the same previewable-media definition the frontend's expanded view already applies, so the count matches the drilldown for the common case. ## How `src/platform/remote/comfyui/jobs/jobTypes.ts` adds `previewable_outputs_count` to the job schema (same optional/nullable shape as the existing `outputs_count`; this is a hand-written Zod schema shared by both local ComfyUI and Cloud, not a generated client). `src/stores/queueStore.ts` adds a `previewableOutputsCount` getter on `TaskItemImpl` mirroring the existing `outputsCount` getter. `src/stores/assetsStore.ts`'s `mapHistoryToAssets` now computes the badge's `outputCount` as `task.previewableOutputsCount ?? task.outputsCount ?? task.previewableOutputs.length`, preferring the new field, falling back to the old total when it's absent, and finally to the existing client-side computation used today. Tests added/updated in `assetsStore.test.ts` and `queueStore.test.ts` cover the getter and the badge preferring the new field over the old one. ## Dependencies This depends on two companion backend PRs adding `previewable_outputs_count`, same field name and concept on both distributions since local ComfyUI and Cloud both serve this frontend through the same `/jobs` schema: - Cloud: Comfy-Org/cloud#5857 (ingest API's `JobEntry`/`JobDetailResponse`) — open, not merged as of 2026-08-09. - Core/local ComfyUI: Comfy-Org/ComfyUI#15148 (`/api/jobs`) — open, not merged as of 2026-08-09. Both are currently open (neither is a draft) and awaiting review/merge. Until whichever backend a user is on ships its half, this frontend change is inert and the badge keeps behaving exactly as it does today, since the fallback chain covers the field's absence. On Comfy Cloud specifically, the badge won't show corrected counts until #5857 merges and deploys. For local ComfyUI, the badge already worked identically before this change (`outputsCount ?? previewableOutputs.length`) and continues to work identically once #15148 lands, since `previewableOutputsCount` simply takes priority when present — no regression either way for local users. ## Known cross-repo inconsistency (resolved) Previously flagged here: `SaveText` saves its output under the `"files"` media-type key with a real `.txt` filename, and this frontend (`isPreviewableMediaType`/`isText`) and Core's `is_previewable()` both treat that as previewable via a text-extension fallback, while Cloud's `SupportsPreview()` (as of the initial #5857 draft) had no equivalent text fallback — meaning a Cloud job with a `SaveText` output alongside real media would under-count relative to the drilldown once `previewable_outputs_count` shipped. This is resolved: Cloud PR #5857 (merged) addresses it via `isBadgePreviewable()`/`isCountablePreviewable()`, which explicitly includes text, matching Core's `PREVIEWABLE_MEDIA_TYPES`. No cross-repo inconsistency remains. --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Christian Byrne <cbyrne@comfy.org>
1 parent 62ee999 commit 84b1c6e

7 files changed

Lines changed: 162 additions & 6 deletions

File tree

browser_tests/tests/sidebar/assetsSidebarTab.spec.ts

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,14 +93,61 @@ const multiOutputJobDetail: JobDetail = {
9393
}
9494
}
9595

96+
const previewableCountJob = createRouteMockJob({
97+
id: 'previewable-count-job',
98+
create_time: routeMockJobTimestamp - 4_000,
99+
execution_start_time: routeMockJobTimestamp - 4_000,
100+
execution_end_time: routeMockJobTimestamp,
101+
preview_output: {
102+
filename: 'previewable-count-a.png',
103+
subfolder: '',
104+
type: 'output',
105+
nodeId: '4',
106+
mediaType: 'images'
107+
},
108+
outputs_count: 3,
109+
previewable_outputs_count: 2
110+
})
111+
112+
// outputs_count (3) also counts the non-previewable "latents" file below;
113+
// previewable_outputs_count (2) counts only what the expanded view renders.
114+
const previewableCountJobDetail: JobDetail = {
115+
...previewableCountJob,
116+
outputs: {
117+
'4': {
118+
images: [
119+
{
120+
filename: 'previewable-count-a.png',
121+
subfolder: '',
122+
type: 'output'
123+
},
124+
{
125+
filename: 'previewable-count-b.png',
126+
subfolder: '',
127+
type: 'output'
128+
}
129+
],
130+
latents: [
131+
{
132+
filename: 'previewable-count.latent',
133+
subfolder: '',
134+
type: 'output'
135+
}
136+
]
137+
}
138+
}
139+
}
140+
96141
const generatedJobs: RawJobListItem[] = [alphaJob, betaJob]
97142

98143
const viewFiles = {
99144
'alpha.png': {},
100145
'beta.png': {},
101146
'imported.png': {},
102147
'multi-output-a.png': {},
103-
'multi-output-b.png': {}
148+
'multi-output-b.png': {},
149+
'previewable-count-a.png': {},
150+
'previewable-count-b.png': {}
104151
}
105152

106153
async function mockInputFiles(page: Page, files: readonly string[]) {
@@ -283,6 +330,31 @@ test.describe('FE-130 assets sidebar route mocks', () => {
283330
).toHaveJSProperty('naturalWidth', 1)
284331
})
285332

333+
test('group badge shows previewable_outputs_count, matching the expanded drilldown', async ({
334+
comfyPage,
335+
jobsRoutes
336+
}) => {
337+
const tab = comfyPage.menu.assetsTab
338+
339+
await jobsRoutes.mockJobsHistory([previewableCountJob])
340+
await jobsRoutes.mockJobDetail(
341+
'previewable-count-job',
342+
previewableCountJobDetail
343+
)
344+
345+
await comfyPage.setup()
346+
await tab.open()
347+
348+
const badge = tab
349+
.getAssetCardByName('previewable-count-a')
350+
.getByRole('button', { name: 'See more outputs' })
351+
await expect(badge).toHaveText('2')
352+
353+
await badge.click()
354+
await expect(tab.backToAssetsButton).toBeVisible()
355+
await expect(tab.assetCards).toHaveCount(2)
356+
})
357+
286358
test('deletes a generated output asset through explicit history refresh', async ({
287359
comfyPage,
288360
jobsRoutes

src/platform/remote/comfyui/jobs/jobTypes.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* Jobs API provides a memory-optimized alternative to history API.
77
*/
88

9+
import { zJobEntry } from '@comfyorg/ingest-types/zod'
910
import { z } from 'zod'
1011

1112
import { resultItemType, zTaskOutput } from '@/schemas/apiSchema'
@@ -62,7 +63,13 @@ const zRawJobListItem = z
6263
execution_start_time: z.number().nullable().optional(),
6364
execution_end_time: z.number().nullable().optional(),
6465
preview_output: zPreviewOutput.nullable().optional(),
65-
outputs_count: z.number().nullable().optional(),
66+
// Sourced from the generated `@comfyorg/ingest-types` JobEntry schema
67+
// (outputs_count/previewable_outputs_count), widened to nullable since
68+
// local ComfyUI's /api/jobs sends explicit nulls where Cloud omits.
69+
// .int() inherited from zJobEntry; intentionally stricter than the previous z.number()
70+
outputs_count: zJobEntry.shape.outputs_count.nullable(),
71+
previewable_outputs_count:
72+
zJobEntry.shape.previewable_outputs_count.nullable(),
6673
execution_error: zExecutionError.nullable().optional(),
6774
workflow_id: z.string().nullable().optional(),
6875
priority: z.number().optional()

src/stores/assetsStore.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,12 @@ vi.mock('@/stores/queueStore', () => ({
122122
| undefined
123123
public jobId: string
124124
public outputsCount: number | null
125+
public previewableOutputsCount: number | undefined
125126

126127
constructor(public job: JobListItem) {
127128
this.jobId = job.id
128129
this.outputsCount = job.outputs_count ?? null
130+
this.previewableOutputsCount = job.previewable_outputs_count ?? undefined
129131
if (mockOutputOverrides.value) {
130132
this.flatOutputs = mockOutputOverrides.value
131133
const previewable = mockOutputOverrides.value.filter(
@@ -648,6 +650,33 @@ describe('assetsStore - Refactored (Option A)', () => {
648650
expect(asset.user_metadata).toHaveProperty('allOutputs')
649651
expect(Array.isArray(asset.user_metadata!.allOutputs)).toBe(true)
650652
})
653+
654+
it('prefers previewable_outputs_count over outputs_count for the group badge', async () => {
655+
const job: JobListItem = {
656+
...createMockJobItem(0),
657+
outputs_count: 3,
658+
previewable_outputs_count: 2
659+
}
660+
vi.mocked(api.getHistory).mockResolvedValue([job])
661+
662+
await store.updateHistory()
663+
664+
const asset = store.historyAssets[0]
665+
expect(asset.user_metadata!.outputCount).toBe(2)
666+
})
667+
668+
it('falls back to outputs_count when previewable_outputs_count is absent', async () => {
669+
const job: JobListItem = {
670+
...createMockJobItem(0),
671+
outputs_count: 3
672+
}
673+
vi.mocked(api.getHistory).mockResolvedValue([job])
674+
675+
await store.updateHistory()
676+
677+
const asset = store.historyAssets[0]
678+
expect(asset.user_metadata!.outputCount).toBe(3)
679+
})
651680
})
652681

653682
describe('Cover Image Selection', () => {

src/stores/assetsStore.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,10 @@ function mapHistoryToAssets(historyItems: JobListItem[]): AssetItem[] {
7878

7979
assetItem.user_metadata = {
8080
...assetItem.user_metadata,
81-
outputCount: task.outputsCount ?? task.previewableOutputs.length,
81+
outputCount:
82+
task.previewableOutputsCount ??
83+
task.outputsCount ??
84+
task.previewableOutputs.length,
8285
allOutputs: task.previewableOutputs
8386
}
8487

src/stores/queueStore.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,41 @@ describe('TaskItemImpl', () => {
280280
expect(taskItem.executionError).toEqual(errorDetail)
281281
})
282282
})
283+
284+
describe('previewableOutputsCount', () => {
285+
it('returns undefined when the job has no previewable_outputs_count', () => {
286+
const job = createHistoryJob(0, 'job-id')
287+
const taskItem = new TaskItemImpl(job)
288+
expect(taskItem.previewableOutputsCount).toBeUndefined()
289+
})
290+
291+
it('returns the server-provided previewable_outputs_count', () => {
292+
const job: JobListItem = {
293+
...createHistoryJob(0, 'job-id'),
294+
previewable_outputs_count: 2
295+
}
296+
const taskItem = new TaskItemImpl(job)
297+
expect(taskItem.previewableOutputsCount).toBe(2)
298+
})
299+
300+
it('returns 0 when previewable_outputs_count is 0', () => {
301+
const job: JobListItem = {
302+
...createHistoryJob(0, 'job-id'),
303+
previewable_outputs_count: 0
304+
}
305+
const taskItem = new TaskItemImpl(job)
306+
expect(taskItem.previewableOutputsCount).toBe(0)
307+
})
308+
309+
it('normalizes an explicit null previewable_outputs_count to undefined', () => {
310+
const job: JobListItem = {
311+
...createHistoryJob(0, 'job-id'),
312+
previewable_outputs_count: null
313+
}
314+
const taskItem = new TaskItemImpl(job)
315+
expect(taskItem.previewableOutputsCount).toBeUndefined()
316+
})
317+
})
283318
})
284319

285320
describe('useQueueStore', () => {

src/stores/queueStore.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,11 @@ export class TaskItemImpl {
330330
return this.job.outputs_count ?? undefined
331331
}
332332

333+
/** Absent on backends or jobs that predate this field. */
334+
get previewableOutputsCount(): number | undefined {
335+
return this.job.previewable_outputs_count ?? undefined
336+
}
337+
333338
get status() {
334339
return this.job.status
335340
}
@@ -580,7 +585,11 @@ export const useQueueStore = defineStore('queue', () => {
580585
const existing = existingByJobId.get(job.id)
581586
if (!existing) return new TaskItemImpl(job)
582587
// Recreate if outputs_count changed to ensure lazy loading works
583-
if (existing.outputsCount !== (job.outputs_count ?? undefined)) {
588+
if (
589+
existing.outputsCount !== (job.outputs_count ?? undefined) ||
590+
existing.previewableOutputsCount !==
591+
(job.previewable_outputs_count ?? undefined)
592+
) {
584593
return new TaskItemImpl(job)
585594
}
586595
return existing

src/stores/workspace/assetsSidebarBadgeStore.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import { useQueueStore } from '@/stores/queueStore'
66
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
77

88
const getAddedAssetCount = (task: TaskItemImpl): number => {
9-
if (typeof task.outputsCount === 'number') {
10-
return Math.max(task.outputsCount, 0)
9+
const count = task.previewableOutputsCount ?? task.outputsCount
10+
if (typeof count === 'number') {
11+
return Math.max(count, 0)
1112
}
1213

1314
return task.previewOutput ? 1 : 0

0 commit comments

Comments
 (0)