From d26309c7abba9a0eaa566e498be5d4c986853800 Mon Sep 17 00:00:00 2001 From: Arjan Singh <1598641+arjansingh@users.noreply.github.com> Date: Mon, 27 Oct 2025 16:08:33 -0700 Subject: [PATCH] feat(historyV2): create sythetic queue priority (#6336) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Create display priority based on execution success timestamps. Next up is displaying in progress prompts in the queue. ## Review Focus @DrJKL and I discussed logic and decided for history, execution success (when the prompt finishes) is the best way to assign priority. This does differ from existing cloud logic which uses execution start time. For prompt progress I intend to create a priority for them based on start time. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6336-feat-historyV2-create-sythetic-queue-priority-2996d73d365081ffa3a0f8071c178066) by [Unito](https://www.unito.io) --- .../comfyui/history/adapters/v2ToV1Adapter.ts | 57 +++-- tests-ui/fixtures/historyFixtures.ts | 13 +- tests-ui/fixtures/historySortingFixtures.ts | 199 ++++++++++++++++++ .../history/adapters/v2ToV1Adapter.test.ts | 72 ++++++- 4 files changed, 318 insertions(+), 23 deletions(-) create mode 100644 tests-ui/fixtures/historySortingFixtures.ts diff --git a/src/platform/remote/comfyui/history/adapters/v2ToV1Adapter.ts b/src/platform/remote/comfyui/history/adapters/v2ToV1Adapter.ts index 5b0e9f2665..bf3cbdcfbe 100644 --- a/src/platform/remote/comfyui/history/adapters/v2ToV1Adapter.ts +++ b/src/platform/remote/comfyui/history/adapters/v2ToV1Adapter.ts @@ -1,10 +1,7 @@ /** * @fileoverview Adapter to convert V2 history format to V1 format * @module platform/remote/comfyui/history/adapters/v2ToV1Adapter - * - * Converts cloud API V2 response format to the V1 format expected by the app. */ - import type { HistoryTaskItem, TaskPrompt } from '../types/historyV1Types' import type { HistoryResponseV2, @@ -13,31 +10,55 @@ import type { TaskPromptV2 } from '../types/historyV2Types' -/** - * Maps V2 prompt format to V1 prompt tuple format. - */ function mapPromptV2toV1( promptV2: TaskPromptV2, - outputs: TaskOutput + outputs: TaskOutput, + syntheticPriority: number ): TaskPrompt { - const outputNodesIds = Object.keys(outputs) - const { priority, prompt_id, extra_data } = promptV2 - return [priority, prompt_id, {}, extra_data, outputNodesIds] + return [ + syntheticPriority, + promptV2.prompt_id, + {}, + promptV2.extra_data, + Object.keys(outputs) + ] +} + +function getExecutionSuccessTimestamp(item: RawHistoryItemV2): number { + return ( + item.status?.messages?.find((m) => m[0] === 'execution_success')?.[1] + ?.timestamp ?? 0 + ) } -/** - * Maps V2 history format to V1 history format. - */ export function mapHistoryV2toHistory( historyV2Response: HistoryResponseV2 ): HistoryTaskItem[] { - return historyV2Response.history.map( - ({ prompt, status, outputs, meta }: RawHistoryItemV2): HistoryTaskItem => ({ + const history = historyV2Response.history + + // Sort by execution_success timestamp, descending (newest first) + history.sort((a, b) => { + return getExecutionSuccessTimestamp(b) - getExecutionSuccessTimestamp(a) + }) + + // Count items with valid timestamps for synthetic priority calculation + const countWithTimestamps = history.filter( + (item) => getExecutionSuccessTimestamp(item) > 0 + ).length + + return history.map((item, index): HistoryTaskItem => { + const { prompt, outputs, status, meta } = item + const timestamp = getExecutionSuccessTimestamp(item) + + // Items with timestamps get priority based on sorted position (highest first) + const syntheticPriority = timestamp > 0 ? countWithTimestamps - index : 0 + + return { taskType: 'History' as const, - prompt: mapPromptV2toV1(prompt, outputs), + prompt: mapPromptV2toV1(prompt, outputs, syntheticPriority), status, outputs, meta - }) - ) + } + }) } diff --git a/tests-ui/fixtures/historyFixtures.ts b/tests-ui/fixtures/historyFixtures.ts index 5ae3ff998a..3a930a1add 100644 --- a/tests-ui/fixtures/historyFixtures.ts +++ b/tests-ui/fixtures/historyFixtures.ts @@ -233,12 +233,17 @@ export const historyV2Fixture: HistoryResponseV2 = { /** * Expected V1 transformation of historyV2Fixture + * Priority is now synthetic based on execution_success timestamp: + * - complete-item-id: has timestamp → priority 1 (only one with timestamp) + * - no-status-id: no status → priority 0 + * - no-meta-id: empty messages → priority 0 + * - multi-output-id: empty messages → priority 0 */ export const expectedV1Fixture: HistoryTaskItem[] = [ { taskType: 'History', prompt: [ - 24, + 1, 'complete-item-id', {}, { @@ -295,7 +300,7 @@ export const expectedV1Fixture: HistoryTaskItem[] = [ { taskType: 'History', prompt: [ - 23, + 0, 'no-status-id', {}, { @@ -319,7 +324,7 @@ export const expectedV1Fixture: HistoryTaskItem[] = [ { taskType: 'History', prompt: [ - 22, + 0, 'no-meta-id', {}, { @@ -342,7 +347,7 @@ export const expectedV1Fixture: HistoryTaskItem[] = [ { taskType: 'History', prompt: [ - 21, + 0, 'multi-output-id', {}, { diff --git a/tests-ui/fixtures/historySortingFixtures.ts b/tests-ui/fixtures/historySortingFixtures.ts new file mode 100644 index 0000000000..797aec2437 --- /dev/null +++ b/tests-ui/fixtures/historySortingFixtures.ts @@ -0,0 +1,199 @@ +/** + * @fileoverview Test fixtures for history V2 timestamp-based sorting + */ +import type { HistoryResponseV2 } from '@/platform/remote/comfyui/history/types/historyV2Types' + +export const historyV2WithMissingTimestamp: HistoryResponseV2 = { + history: [ + { + prompt_id: 'item-timestamp-1000', + prompt: { + priority: 0, + prompt_id: 'item-timestamp-1000', + extra_data: { + client_id: 'test-client' + } + }, + outputs: { + '1': { + images: [{ filename: 'test1.png', type: 'output', subfolder: '' }] + } + }, + status: { + status_str: 'success', + completed: true, + messages: [ + [ + 'execution_success', + { prompt_id: 'item-timestamp-1000', timestamp: 1000 } + ] + ] + } + }, + { + prompt_id: 'item-timestamp-2000', + prompt: { + priority: 0, + prompt_id: 'item-timestamp-2000', + extra_data: { + client_id: 'test-client' + } + }, + outputs: { + '2': { + images: [{ filename: 'test2.png', type: 'output', subfolder: '' }] + } + }, + status: { + status_str: 'success', + completed: true, + messages: [ + [ + 'execution_success', + { prompt_id: 'item-timestamp-2000', timestamp: 2000 } + ] + ] + } + }, + { + prompt_id: 'item-no-timestamp', + prompt: { + priority: 0, + prompt_id: 'item-no-timestamp', + extra_data: { + client_id: 'test-client' + } + }, + outputs: { + '3': { + images: [{ filename: 'test3.png', type: 'output', subfolder: '' }] + } + }, + status: { + status_str: 'success', + completed: true, + messages: [] + } + } + ] +} + +export const historyV2FiveItemsSorting: HistoryResponseV2 = { + history: [ + { + prompt_id: 'item-timestamp-3000', + prompt: { + priority: 0, + prompt_id: 'item-timestamp-3000', + extra_data: { client_id: 'test-client' } + }, + outputs: { + '1': { + images: [{ filename: 'test1.png', type: 'output', subfolder: '' }] + } + }, + status: { + status_str: 'success', + completed: true, + messages: [ + [ + 'execution_success', + { prompt_id: 'item-timestamp-3000', timestamp: 3000 } + ] + ] + } + }, + { + prompt_id: 'item-timestamp-1000', + prompt: { + priority: 0, + prompt_id: 'item-timestamp-1000', + extra_data: { client_id: 'test-client' } + }, + outputs: { + '2': { + images: [{ filename: 'test2.png', type: 'output', subfolder: '' }] + } + }, + status: { + status_str: 'success', + completed: true, + messages: [ + [ + 'execution_success', + { prompt_id: 'item-timestamp-1000', timestamp: 1000 } + ] + ] + } + }, + { + prompt_id: 'item-timestamp-5000', + prompt: { + priority: 0, + prompt_id: 'item-timestamp-5000', + extra_data: { client_id: 'test-client' } + }, + outputs: { + '3': { + images: [{ filename: 'test3.png', type: 'output', subfolder: '' }] + } + }, + status: { + status_str: 'success', + completed: true, + messages: [ + [ + 'execution_success', + { prompt_id: 'item-timestamp-5000', timestamp: 5000 } + ] + ] + } + }, + { + prompt_id: 'item-timestamp-2000', + prompt: { + priority: 0, + prompt_id: 'item-timestamp-2000', + extra_data: { client_id: 'test-client' } + }, + outputs: { + '4': { + images: [{ filename: 'test4.png', type: 'output', subfolder: '' }] + } + }, + status: { + status_str: 'success', + completed: true, + messages: [ + [ + 'execution_success', + { prompt_id: 'item-timestamp-2000', timestamp: 2000 } + ] + ] + } + }, + { + prompt_id: 'item-timestamp-4000', + prompt: { + priority: 0, + prompt_id: 'item-timestamp-4000', + extra_data: { client_id: 'test-client' } + }, + outputs: { + '5': { + images: [{ filename: 'test5.png', type: 'output', subfolder: '' }] + } + }, + status: { + status_str: 'success', + completed: true, + messages: [ + [ + 'execution_success', + { prompt_id: 'item-timestamp-4000', timestamp: 4000 } + ] + ] + } + } + ] +} diff --git a/tests-ui/tests/platform/remote/comfyui/history/adapters/v2ToV1Adapter.test.ts b/tests-ui/tests/platform/remote/comfyui/history/adapters/v2ToV1Adapter.test.ts index f77a5e1588..0d6cef8c26 100644 --- a/tests-ui/tests/platform/remote/comfyui/history/adapters/v2ToV1Adapter.test.ts +++ b/tests-ui/tests/platform/remote/comfyui/history/adapters/v2ToV1Adapter.test.ts @@ -11,6 +11,10 @@ import { expectedV1Fixture, historyV2Fixture } from '@tests-ui/fixtures/historyFixtures' +import { + historyV2FiveItemsSorting, + historyV2WithMissingTimestamp +} from '@tests-ui/fixtures/historySortingFixtures' describe('mapHistoryV2toHistory', () => { describe('fixture validation', () => { @@ -38,7 +42,7 @@ describe('mapHistoryV2toHistory', () => { it('should transform prompt to V1 tuple [priority, id, {}, extra_data, outputNodeIds]', () => { const firstItem = history[0] - expect(firstItem.prompt[0]).toBe(24) + expect(firstItem.prompt[0]).toBe(1) // Synthetic priority based on timestamp expect(firstItem.prompt[1]).toBe('complete-item-id') expect(firstItem.prompt[2]).toEqual({}) // history v2 does not return this data expect(firstItem.prompt[3]).toMatchObject({ client_id: 'test-client' }) @@ -117,4 +121,70 @@ describe('mapHistoryV2toHistory', () => { expect(history[0].prompt[3].client_id).toBeUndefined() }) }) + + describe('timestamp-based priority assignment', () => { + it('assigns priority 0 to items without execution_success timestamp', () => { + const result = mapHistoryV2toHistory(historyV2WithMissingTimestamp) + + expect(result).toHaveLength(3) + + const item1000 = result.find( + (item) => item.prompt[1] === 'item-timestamp-1000' + ) + const item2000 = result.find( + (item) => item.prompt[1] === 'item-timestamp-2000' + ) + const itemNoTimestamp = result.find( + (item) => item.prompt[1] === 'item-no-timestamp' + ) + + expect(item1000).toBeDefined() + expect(item2000).toBeDefined() + expect(itemNoTimestamp).toBeDefined() + if (!item1000 || !item2000 || !itemNoTimestamp) { + throw new Error('Expected items not found in result') + } + + expect(item2000.prompt[0]).toBe(2) + expect(item1000.prompt[0]).toBe(1) + expect(itemNoTimestamp.prompt[0]).toBe(0) + }) + + it('correctly sorts and assigns priorities for multiple items', () => { + const result = mapHistoryV2toHistory(historyV2FiveItemsSorting) + + expect(result).toHaveLength(5) + + const item1000 = result.find( + (item) => item.prompt[1] === 'item-timestamp-1000' + ) + const item2000 = result.find( + (item) => item.prompt[1] === 'item-timestamp-2000' + ) + const item3000 = result.find( + (item) => item.prompt[1] === 'item-timestamp-3000' + ) + const item4000 = result.find( + (item) => item.prompt[1] === 'item-timestamp-4000' + ) + const item5000 = result.find( + (item) => item.prompt[1] === 'item-timestamp-5000' + ) + + expect(item1000).toBeDefined() + expect(item2000).toBeDefined() + expect(item3000).toBeDefined() + expect(item4000).toBeDefined() + expect(item5000).toBeDefined() + if (!item1000 || !item2000 || !item3000 || !item4000 || !item5000) { + throw new Error('Expected items not found in result') + } + + expect(item5000.prompt[0]).toBe(5) + expect(item4000.prompt[0]).toBe(4) + expect(item3000.prompt[0]).toBe(3) + expect(item2000.prompt[0]).toBe(2) + expect(item1000.prompt[0]).toBe(1) + }) + }) })