Skip to content

Commit 2ef7c0a

Browse files
authored
Merge branch 'main' into feat/vue-nodes-onboarding-toast
2 parents 0ee3fbe + fac8bd6 commit 2ef7c0a

File tree

10 files changed

+383
-63
lines changed

10 files changed

+383
-63
lines changed

src/components/sidebar/tabs/QueueSidebarTab.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ import { useI18n } from 'vue-i18n'
104104
105105
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
106106
import VirtualGrid from '@/components/common/VirtualGrid.vue'
107+
import { isCloud } from '@/platform/distribution/types'
107108
import { useSettingStore } from '@/platform/settings/settingStore'
108109
import type { ComfyNode } from '@/platform/workflow/validation/schemas/workflowSchema'
109110
import { api } from '@/scripts/api'
@@ -206,7 +207,9 @@ const menuItems = computed<MenuItem[]>(() => {
206207
label: t('g.loadWorkflow'),
207208
icon: 'pi pi-file-export',
208209
command: () => menuTargetTask.value?.loadWorkflow(app),
209-
disabled: !menuTargetTask.value?.workflow
210+
disabled: isCloud
211+
? !menuTargetTask.value?.isHistory
212+
: !menuTargetTask.value?.workflow
210213
},
211214
{
212215
label: t('g.goToNode'),

src/platform/remote/comfyui/history/reconciliation.ts

Lines changed: 12 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -13,25 +13,22 @@
1313
import { isCloud } from '@/platform/distribution/types'
1414
import type { TaskItem } from '@/schemas/apiSchema'
1515

16-
interface ReconciliationResult {
17-
/** All items to display, sorted by queueIndex descending (newest first) */
18-
items: TaskItem[]
19-
}
20-
2116
/**
2217
* V1 reconciliation: QueueIndex-based filtering works because V1 has stable,
2318
* monotonically increasing queue indices.
2419
*
2520
* Sort order: Sorts serverHistory by queueIndex descending (newest first) to ensure
2621
* consistent ordering. JavaScript .filter() maintains iteration order, so filtered
2722
* results remain sorted. clientHistory is assumed already sorted from previous update.
23+
*
24+
* @returns All items to display, sorted by queueIndex descending (newest first)
2825
*/
2926
function reconcileHistoryV1(
3027
serverHistory: TaskItem[],
3128
clientHistory: TaskItem[],
3229
maxItems: number,
3330
lastKnownQueueIndex: number | undefined
34-
): ReconciliationResult {
31+
): TaskItem[] {
3532
const sortedServerHistory = serverHistory.sort(
3633
(a, b) => b.prompt[0] - a.prompt[0]
3734
)
@@ -53,13 +50,9 @@ function reconcileHistoryV1(
5350
)
5451

5552
// Merge new and reused items, sort by queueIndex descending, limit to maxItems
56-
const allItems = [...itemsAddedSinceLastSync, ...clientItemsStillOnServer]
53+
return [...itemsAddedSinceLastSync, ...clientItemsStillOnServer]
5754
.sort((a, b) => b.prompt[0] - a.prompt[0])
5855
.slice(0, maxItems)
59-
60-
return {
61-
items: allItems
62-
}
6356
}
6457

6558
/**
@@ -69,12 +62,14 @@ function reconcileHistoryV1(
6962
* Sort order: Sorts serverHistory by queueIndex descending (newest first) to ensure
7063
* consistent ordering. JavaScript .filter() maintains iteration order, so filtered
7164
* results remain sorted. clientHistory is assumed already sorted from previous update.
65+
*
66+
* @returns All items to display, sorted by queueIndex descending (newest first)
7267
*/
7368
function reconcileHistoryV2(
7469
serverHistory: TaskItem[],
7570
clientHistory: TaskItem[],
7671
maxItems: number
77-
): ReconciliationResult {
72+
): TaskItem[] {
7873
const sortedServerHistory = serverHistory.sort(
7974
(a, b) => b.prompt[0] - a.prompt[0]
8075
)
@@ -84,29 +79,18 @@ function reconcileHistoryV2(
8479
)
8580
const clientPromptIds = new Set(clientHistory.map((item) => item.prompt[1]))
8681

87-
const newPromptIds = new Set(
88-
[...serverPromptIds].filter((id) => !clientPromptIds.has(id))
82+
const newItems = sortedServerHistory.filter(
83+
(item) => !clientPromptIds.has(item.prompt[1])
8984
)
9085

91-
const newItems = sortedServerHistory.filter((item) =>
92-
newPromptIds.has(item.prompt[1])
93-
)
94-
95-
const retainedPromptIds = new Set(
96-
[...serverPromptIds].filter((id) => clientPromptIds.has(id))
97-
)
9886
const clientItemsStillOnServer = clientHistory.filter((item) =>
99-
retainedPromptIds.has(item.prompt[1])
87+
serverPromptIds.has(item.prompt[1])
10088
)
10189

10290
// Merge new and reused items, sort by queueIndex descending, limit to maxItems
103-
const allItems = [...newItems, ...clientItemsStillOnServer]
91+
return [...newItems, ...clientItemsStillOnServer]
10492
.sort((a, b) => b.prompt[0] - a.prompt[0])
10593
.slice(0, maxItems)
106-
107-
return {
108-
items: allItems
109-
}
11094
}
11195

11296
/**
@@ -125,7 +109,7 @@ export function reconcileHistory(
125109
clientHistory: TaskItem[],
126110
maxItems: number,
127111
lastKnownQueueIndex?: number
128-
): ReconciliationResult {
112+
): TaskItem[] {
129113
if (isCloud) {
130114
return reconcileHistoryV2(serverHistory, clientHistory, maxItems)
131115
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
2+
import type { PromptId } from '@/schemas/apiSchema'
3+
4+
export async function getWorkflowFromHistory(
5+
fetchApi: (url: string) => Promise<Response>,
6+
promptId: PromptId
7+
): Promise<ComfyWorkflowJSON | undefined> {
8+
try {
9+
const res = await fetchApi(`/history_v2/${promptId}`)
10+
const json = await res.json()
11+
12+
const historyItem = json[promptId]
13+
if (!historyItem) return undefined
14+
15+
const workflow = historyItem.prompt?.extra_data?.extra_pnginfo?.workflow
16+
return workflow ?? undefined
17+
} catch (error) {
18+
console.error(`Failed to fetch workflow for prompt ${promptId}:`, error)
19+
return undefined
20+
}
21+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* Cloud: Fetches workflow by prompt_id. Desktop: Returns undefined (workflows already in history).
3+
*/
4+
import { isCloud } from '@/platform/distribution/types'
5+
6+
import { getWorkflowFromHistory as cloudImpl } from './getWorkflowFromHistory'
7+
8+
export const getWorkflowFromHistory = isCloud
9+
? cloudImpl
10+
: async () => undefined

src/renderer/extensions/vueNodes/components/ImagePreview.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
ref="currentImageEl"
3535
:src="currentImageUrl"
3636
:alt="imageAltText"
37-
class="block size-full object-contain"
37+
class="block size-full object-contain pointer-events-none"
3838
@load="handleImageLoad"
3939
@error="handleImageError"
4040
/>

src/schemas/apiSchema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { LinkReleaseTriggerAction } from '@/types/searchBoxTypes'
1313
const zNodeType = z.string()
1414
export const zQueueIndex = z.number()
1515
export const zPromptId = z.string()
16+
export type PromptId = z.infer<typeof zPromptId>
1617
export const resultItemType = z.enum(['input', 'output', 'temp'])
1718
export type ResultItemType = z.infer<typeof resultItemType>
1819

src/stores/queueStore.ts

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import _ from 'es-toolkit/compat'
22
import { defineStore } from 'pinia'
33
import { computed, ref, shallowRef, toRaw, toValue } from 'vue'
44

5+
import { isCloud } from '@/platform/distribution/types'
56
import { reconcileHistory } from '@/platform/remote/comfyui/history/reconciliation'
7+
import { getWorkflowFromHistory } from '@/platform/workflow/cloud'
68
import type {
79
ComfyWorkflowJSON,
810
NodeId
@@ -379,24 +381,37 @@ export class TaskItemImpl {
379381
}
380382

381383
public async loadWorkflow(app: ComfyApp) {
382-
if (!this.workflow) {
384+
let workflowData = this.workflow
385+
386+
if (isCloud && !workflowData && this.isHistory) {
387+
workflowData = await getWorkflowFromHistory(
388+
(url) => app.api.fetchApi(url),
389+
this.promptId
390+
)
391+
}
392+
393+
if (!workflowData) {
383394
return
384395
}
385-
await app.loadGraphData(toRaw(this.workflow))
386-
if (this.outputs) {
387-
const nodeOutputsStore = useNodeOutputStore()
388-
const rawOutputs = toRaw(this.outputs)
389-
for (const nodeExecutionId in rawOutputs) {
390-
nodeOutputsStore.setNodeOutputsByExecutionId(
391-
nodeExecutionId,
392-
rawOutputs[nodeExecutionId]
393-
)
394-
}
395-
useExtensionService().invokeExtensions(
396-
'onNodeOutputsUpdated',
397-
app.nodeOutputs
396+
397+
await app.loadGraphData(toRaw(workflowData))
398+
399+
if (!this.outputs) {
400+
return
401+
}
402+
403+
const nodeOutputsStore = useNodeOutputStore()
404+
const rawOutputs = toRaw(this.outputs)
405+
for (const nodeExecutionId in rawOutputs) {
406+
nodeOutputsStore.setNodeOutputsByExecutionId(
407+
nodeExecutionId,
408+
rawOutputs[nodeExecutionId]
398409
)
399410
}
411+
useExtensionService().invokeExtensions(
412+
'onNodeOutputsUpdated',
413+
app.nodeOutputs
414+
)
400415
}
401416

402417
public flatten(): TaskItemImpl[] {
@@ -492,7 +507,7 @@ export const useQueueStore = defineStore('queue', () => {
492507

493508
const currentHistory = toValue(historyTasks)
494509

495-
const { items } = reconcileHistory(
510+
const items = reconcileHistory(
496511
history.History,
497512
currentHistory.map((impl) => impl.toTaskItem()),
498513
toValue(maxHistoryItems),

tests-ui/tests/platform/remote/comfyui/history/reconciliation.test.ts

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ function createHistoryItem(promptId: string, queueIndex = 0): TaskItem {
2121
}
2222
}
2323

24-
function getAllPromptIds(result: { items: TaskItem[] }): string[] {
25-
return result.items.map((item) => item.prompt[1])
24+
function getAllPromptIds(result: TaskItem[]): string[] {
25+
return result.map((item) => item.prompt[1])
2626
}
2727

2828
describe('reconcileHistory (V1)', () => {
@@ -74,9 +74,9 @@ describe('reconcileHistory (V1)', () => {
7474

7575
const result = reconcileHistory(serverHistory, [], 10, undefined)
7676

77-
expect(result.items).toHaveLength(2)
78-
expect(result.items[0].prompt[1]).toBe('item-1')
79-
expect(result.items[1].prompt[1]).toBe('item-2')
77+
expect(result).toHaveLength(2)
78+
expect(result[0].prompt[1]).toBe('item-1')
79+
expect(result[1].prompt[1]).toBe('item-2')
8080
})
8181
})
8282

@@ -144,9 +144,9 @@ describe('reconcileHistory (V1)', () => {
144144

145145
const result = reconcileHistory(serverHistory, clientHistory, 2, 10)
146146

147-
expect(result.items).toHaveLength(2)
148-
expect(result.items[0].prompt[1]).toBe('new-1')
149-
expect(result.items[1].prompt[1]).toBe('new-2')
147+
expect(result).toHaveLength(2)
148+
expect(result[0].prompt[1]).toBe('new-1')
149+
expect(result[1].prompt[1]).toBe('new-2')
150150
})
151151
})
152152

@@ -168,13 +168,13 @@ describe('reconcileHistory (V1)', () => {
168168

169169
const result = reconcileHistory([], clientHistory, 10, 5)
170170

171-
expect(result.items).toHaveLength(0)
171+
expect(result).toHaveLength(0)
172172
})
173173

174174
it('should return empty result when both collections are empty', () => {
175175
const result = reconcileHistory([], [], 10, undefined)
176176

177-
expect(result.items).toHaveLength(0)
177+
expect(result).toHaveLength(0)
178178
})
179179
})
180180
})
@@ -295,9 +295,9 @@ describe('reconcileHistory (V2/Cloud)', () => {
295295

296296
const result = reconcileHistory(serverHistory, clientHistory, 2)
297297

298-
expect(result.items).toHaveLength(2)
299-
expect(result.items[0].prompt[1]).toBe('new-1')
300-
expect(result.items[1].prompt[1]).toBe('new-2')
298+
expect(result).toHaveLength(2)
299+
expect(result[0].prompt[1]).toBe('new-1')
300+
expect(result[1].prompt[1]).toBe('new-2')
301301
})
302302
})
303303

@@ -310,9 +310,9 @@ describe('reconcileHistory (V2/Cloud)', () => {
310310

311311
const result = reconcileHistory(serverHistory, [], 10)
312312

313-
expect(result.items).toHaveLength(2)
314-
expect(result.items[0].prompt[1]).toBe('item-1')
315-
expect(result.items[1].prompt[1]).toBe('item-2')
313+
expect(result).toHaveLength(2)
314+
expect(result[0].prompt[1]).toBe('item-1')
315+
expect(result[1].prompt[1]).toBe('item-2')
316316
})
317317

318318
it('should return empty result when server history is empty', () => {
@@ -323,13 +323,13 @@ describe('reconcileHistory (V2/Cloud)', () => {
323323

324324
const result = reconcileHistory([], clientHistory, 10)
325325

326-
expect(result.items).toHaveLength(0)
326+
expect(result).toHaveLength(0)
327327
})
328328

329329
it('should return empty result when both collections are empty', () => {
330330
const result = reconcileHistory([], [], 10)
331331

332-
expect(result.items).toHaveLength(0)
332+
expect(result).toHaveLength(0)
333333
})
334334
})
335335
})

0 commit comments

Comments
 (0)