Skip to content

[api] update getHistory to call history_v2 #4389

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions browser_tests/fixtures/utils/taskHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,21 @@ const getContentType = (filename: string, fileType: OutputFileType) => {
}

const setQueueIndex = (task: TaskItem) => {
task.prompt[0] = TaskHistory.queueIndex++
task.prompt.priority = TaskHistory.queueIndex++
}

const setPromptId = (task: TaskItem) => {
task.prompt[1] = uuidv4()
task.prompt.prompt_id = uuidv4()
}

export default class TaskHistory {
static queueIndex = 0
static readonly defaultTask: Readonly<HistoryTaskItem> = {
prompt: [0, 'prompt-id', {}, { client_id: uuidv4() }, []],
prompt: {
priority: 0,
prompt_id: 'prompt-id',
extra_data: { client_id: uuidv4() }
},
outputs: {},
status: {
status_str: 'success',
Expand All @@ -69,13 +73,13 @@ export default class TaskHistory {
return route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(this.tasks)
body: JSON.stringify({ history: this.tasks })
})
}

private async handleGetView(route: Route) {
const fileName = getFilenameParam(route.request())
if (!this.outputContentTypes.has(fileName)) route.continue()
if (!this.outputContentTypes.has(fileName)) return route.continue()

const asset = this.loadAsset(fileName)
return route.fulfill({
Expand All @@ -91,7 +95,7 @@ export default class TaskHistory {

async setupRoutes() {
return this.comfyPage.page.route(
/.*\/api\/(view|history)(\?.*)?$/,
/.*\/api\/(view|history|history_v2)(\?.*)?$/,
async (route) => {
const request = route.request()
const method = request.method()
Expand Down
2 changes: 2 additions & 0 deletions browser_tests/tests/sidebar/workflows.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,12 +187,14 @@ test.describe('Workflows sidebar', () => {

test('Can save workflow as with same name', async ({ comfyPage }) => {
await comfyPage.menu.topbar.saveWorkflow('workflow5.json')
await comfyPage.nextFrame()
expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([
'workflow5.json'
])

await comfyPage.menu.topbar.saveWorkflowAs('workflow5.json')
await comfyPage.confirmDialog.click('overwrite')
await comfyPage.nextFrame()
expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([
'workflow5.json'
])
Expand Down
15 changes: 12 additions & 3 deletions src/components/sidebar/tabs/QueueSidebarTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,8 @@ import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import VirtualGrid from '@/components/common/VirtualGrid.vue'
import { ComfyNode } from '@/schemas/comfyWorkflowSchema'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { useLitegraphService } from '@/services/litegraphService'
import { useWorkflowService } from '@/services/workflowService'
import { useCommandStore } from '@/stores/commandStore'
import {
ResultItemImpl,
Expand All @@ -126,6 +126,7 @@ const toast = useToast()
const queueStore = useQueueStore()
const settingStore = useSettingStore()
const commandStore = useCommandStore()
const workflowService = useWorkflowService()
const { t } = useI18n()

// Expanded view: show all outputs in a flat list.
Expand Down Expand Up @@ -208,8 +209,16 @@ const menuItems = computed<MenuItem[]>(() => {
{
label: t('g.loadWorkflow'),
icon: 'pi pi-file-export',
command: () => menuTargetTask.value?.loadWorkflow(app),
disabled: !menuTargetTask.value?.workflow
command: () => {
if (menuTargetTask.value) {
void workflowService.loadTaskWorkflow(menuTargetTask.value)
}
},
disabled: !(
menuTargetTask.value?.workflow ||
(menuTargetTask.value?.isHistory &&
menuTargetTask.value?.prompt.prompt_id)
)
},
{
label: t('g.goToNode'),
Expand Down
36 changes: 21 additions & 15 deletions src/schemas/apiSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,13 +134,6 @@ export type DisplayComponentWsMessage = z.infer<
>
// End of ws messages

const zPromptInputItem = z.object({
inputs: z.record(z.string(), z.any()),
class_type: zNodeType
})

const zPromptInputs = z.record(zPromptInputItem)

const zExtraPngInfo = z
.object({
workflow: zComfyWorkflow
Expand All @@ -152,7 +145,6 @@ const zExtraData = z.object({
extra_pnginfo: zExtraPngInfo.optional(),
client_id: z.string()
})
const zOutputsToExecute = z.array(zNodeId)

const zExecutionStartMessage = z.tuple([
z.literal('execution_start'),
Expand Down Expand Up @@ -193,13 +185,11 @@ const zStatus = z.object({
messages: z.array(zStatusMessage)
})

const zTaskPrompt = z.tuple([
zQueueIndex,
zPromptId,
zPromptInputs,
zExtraData,
zOutputsToExecute
])
const zTaskPrompt = z.object({
priority: zQueueIndex,
prompt_id: zPromptId,
extra_data: zExtraData
})

const zRunningTaskItem = z.object({
taskType: z.literal('Running'),
Expand Down Expand Up @@ -235,6 +225,20 @@ const zHistoryTaskItem = z.object({
meta: zTaskMeta.optional()
})

// Raw history item from backend (without taskType)
const zRawHistoryItem = z.object({
prompt_id: zPromptId,
prompt: zTaskPrompt,
status: zStatus.optional(),
outputs: zTaskOutput,
meta: zTaskMeta.optional()
})

// New API response format: { history: [{prompt_id: "...", ...}, ...] }
const zHistoryResponse = z.object({
history: z.array(zRawHistoryItem)
})

const zTaskItem = z.union([
zRunningTaskItem,
zPendingTaskItem,
Expand All @@ -257,6 +261,8 @@ export type RunningTaskItem = z.infer<typeof zRunningTaskItem>
export type PendingTaskItem = z.infer<typeof zPendingTaskItem>
// `/history`
export type HistoryTaskItem = z.infer<typeof zHistoryTaskItem>
export type RawHistoryItem = z.infer<typeof zRawHistoryItem>
export type HistoryResponse = z.infer<typeof zHistoryResponse>
export type TaskItem = z.infer<typeof zTaskItem>

export function validateTaskItem(taskItem: unknown) {
Expand Down
52 changes: 42 additions & 10 deletions src/scripts/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
ExecutionStartWsMessage,
ExecutionSuccessWsMessage,
ExtensionsResponse,
HistoryResponse,
HistoryTaskItem,
LogsRawResponse,
LogsWsMessage,
Expand All @@ -23,6 +24,7 @@ import type {
StatusWsMessage,
StatusWsMessageStatus,
SystemStats,
TaskPrompt,
User,
UserDataFullInfo
} from '@/schemas/apiSchema'
Expand Down Expand Up @@ -686,13 +688,12 @@ export class ComfyApi extends EventTarget {
const data = await res.json()
return {
// Running action uses a different endpoint for cancelling
Running: data.queue_running.map((prompt: Record<number, any>) => ({
Running: data.queue_running.map((prompt: TaskPrompt) => ({
taskType: 'Running',
prompt,
// prompt[1] is the prompt id
remove: { name: 'Cancel', cb: () => api.interrupt(prompt[1]) }
remove: { name: 'Cancel', cb: () => api.interrupt(prompt.prompt_id) }
})),
Pending: data.queue_pending.map((prompt: Record<number, any>) => ({
Pending: data.queue_pending.map((prompt: TaskPrompt) => ({
taskType: 'Pending',
prompt
}))
Expand All @@ -711,20 +712,51 @@ export class ComfyApi extends EventTarget {
max_items: number = 200
): Promise<{ History: HistoryTaskItem[] }> {
try {
const res = await this.fetchApi(`/history?max_items=${max_items}`)
const json: Promise<HistoryTaskItem[]> = await res.json()
const res = await this.fetchApi(`/history_v2?max_items=${max_items}`)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this endpoint be added to open-source ComfyUI core server?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay nice. For the discovery, you can also access the version of ComfyUI via /system_stats or the systemStatsStore in our codebase. In general we don't worry about supporting old versions of the backend (relative to frontend version) since comfyui_frontend_package is a dependency of ComfyUI and not used with any other backends. However, I may be missing some context.

const json: HistoryResponse = await res.json()

// Extract history data from new format: { history: [{prompt_id: "...", ...}, ...] }
return {
History: Object.values(json).map((item) => ({
...item,
taskType: 'History'
}))
History: json.history.map(
(item): HistoryTaskItem => ({
...item,
taskType: 'History'
})
)
}
} catch (error) {
console.error(error)
return { History: [] }
}
}

/**
* Gets workflow data for a specific prompt from history
* @param prompt_id The prompt ID to fetch workflow for
* @returns Workflow data for the specific prompt
*/
async getWorkflowFromHistory(
prompt_id: string
): Promise<ComfyWorkflowJSON | null> {
try {
const res = await this.fetchApi(`/history_v2/${prompt_id}`)
const json = await res.json()

// The /history_v2/{prompt_id} endpoint returns data for a specific prompt
// The response format is: { prompt_id: { prompt: {priority, prompt_id, extra_data}, outputs: {...}, status: {...} } }
const historyItem = json[prompt_id]
if (!historyItem) return null

// Extract workflow from the prompt object
// prompt.extra_data contains extra_pnginfo.workflow
const workflow = historyItem.prompt?.extra_data?.extra_pnginfo?.workflow
return workflow || null
} catch (error) {
console.error(`Failed to fetch workflow for prompt ${prompt_id}:`, error)
return null
}
}

/**
* Gets system & device stats
* @returns System stats such as python version, OS, per device info
Expand Down
7 changes: 4 additions & 3 deletions src/scripts/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,15 +264,16 @@ class ComfyList {
? item.remove
: {
name: 'Delete',
cb: () => api.deleteItem(this.#type, item.prompt[1])
cb: () =>
api.deleteItem(this.#type, item.prompt.prompt_id)
}
return $el('div', { textContent: item.prompt[0] + ': ' }, [
return $el('div', { textContent: item.prompt.priority + ': ' }, [
$el('button', {
textContent: 'Load',
onclick: async () => {
await app.loadGraphData(
// @ts-expect-error fixme ts strict error
item.prompt[3].extra_pnginfo.workflow,
item.prompt.extra_data.extra_pnginfo.workflow,
true,
false
)
Expand Down
29 changes: 29 additions & 0 deletions src/services/workflowService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import { toRaw } from 'vue'

import { t } from '@/i18n'
import { ComfyWorkflowJSON } from '@/schemas/comfyWorkflowSchema'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { blankGraph, defaultGraph } from '@/scripts/defaultGraph'
import { downloadBlob } from '@/scripts/utils'
import { useDomWidgetStore } from '@/stores/domWidgetStore'
import { TaskItemImpl } from '@/stores/queueStore'
import { useSettingStore } from '@/stores/settingStore'
import { useToastStore } from '@/stores/toastStore'
import { ComfyWorkflow, useWorkflowStore } from '@/stores/workflowStore'
Expand Down Expand Up @@ -152,6 +154,32 @@ export const useWorkflowService = () => {
await app.loadGraphData(blankGraph)
}

/**
* Load a workflow from a task item (queue/history)
* For history items, fetches workflow data from /history_v2/{prompt_id}
* @param task The task item to load the workflow from
*/
const loadTaskWorkflow = async (task: TaskItemImpl) => {
let workflowData = task.workflow

// History items don't include workflow data - fetch from API
if (task.isHistory) {
const promptId = task.prompt.prompt_id
if (promptId) {
workflowData = (await api.getWorkflowFromHistory(promptId)) || undefined
}
}

if (!workflowData) {
return
}

await app.loadGraphData(toRaw(workflowData))
if (task.outputs) {
app.nodeOutputs = toRaw(task.outputs)
}
}

/**
* Reload the current workflow
* This is used to refresh the node definitions update, e.g. when the locale changes.
Expand Down Expand Up @@ -394,6 +422,7 @@ export const useWorkflowService = () => {
saveWorkflow,
loadDefaultWorkflow,
loadBlankWorkflow,
loadTaskWorkflow,
reloadCurrentWorkflow,
openWorkflow,
closeWorkflow,
Expand Down
30 changes: 10 additions & 20 deletions src/stores/queueStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,23 +269,15 @@ export class TaskItemImpl {
}

get queueIndex() {
return this.prompt[0]
return this.prompt.priority
}

get promptId() {
return this.prompt[1]
}

get promptInputs() {
return this.prompt[2]
return this.prompt.prompt_id
}

get extraData() {
return this.prompt[3]
}

get outputsToExecute() {
return this.prompt[4]
return this.prompt.extra_data
}

get extraPngInfo() {
Expand Down Expand Up @@ -390,13 +382,11 @@ export class TaskItemImpl {
(output: ResultItemImpl, i: number) =>
new TaskItemImpl(
this.taskType,
[
this.queueIndex,
`${this.promptId}-${i}`,
this.promptInputs,
this.extraData,
this.outputsToExecute
],
{
priority: this.queueIndex,
prompt_id: `${this.promptId}-${i}`,
extra_data: this.extraData
},
this.status,
{
[output.nodeId]: {
Expand Down Expand Up @@ -461,11 +451,11 @@ export const useQueueStore = defineStore('queue', () => {
pendingTasks.value = toClassAll(queue.Pending)

const allIndex = new Set<number>(
history.History.map((item: TaskItem) => item.prompt[0])
history.History.map((item: TaskItem) => item.prompt.priority)
)
const newHistoryItems = toClassAll(
history.History.filter(
(item) => item.prompt[0] > lastHistoryQueueIndex.value
(item) => item.prompt.priority > lastHistoryQueueIndex.value
)
)
const existingHistoryItems = historyTasks.value.filter((item) =>
Expand Down
Loading