Compare commits

...

12 Commits

Author SHA1 Message Date
Benjamin Lu
f62edda7e9 fix: remove unrelated merge artifacts 2026-03-23 19:54:58 -07:00
Benjamin Lu
7ce6408e7f Merge remote-tracking branch 'origin/main' into pr-7688
# Conflicts:
#	src/stores/executionStore.test.ts
2026-03-23 19:50:46 -07:00
Benjamin Lu
3185004a20 fix: scope executing node lookup to queued job 2026-03-23 19:40:34 -07:00
Jin Yi
98d56bdada fix: improve settings dialog UX (#10396)
## Summary

Reduce settings dialog size and autofocus search input for better
usability.

## Changes

- **What**: Reduce dialog size from `md` to `sm` (max-width 1400px →
960px); autofocus search input on open

## Review Focus

User feedback indicated the settings dialog was too wide and search
required an extra click to focus.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10396-fix-improve-settings-dialog-UX-32c6d73d365081e29eceed55afde1967)
by [Unito](https://www.unito.io)

Co-authored-by: Amp <amp@ampcode.com>
2026-03-23 18:45:40 -07:00
Benjamin Lu
fafaba09d1 Merge origin/main into qpo-progressbar-node-name 2026-03-06 14:08:19 -08:00
Alexander Brown
f349184290 Merge branch 'main' into qpo-progressbar-node-name 2026-01-05 16:06:03 -08:00
Csongor Czezar
530717983f fix: use public api 2026-01-02 15:00:56 -08:00
Csongor Czezar
6866bd4792 refactor: moved all the tests into the main executionstore test file 2026-01-02 14:46:01 -08:00
Csongor Czezar
1bfd617265 refactor: moving mock functions out of global 2026-01-02 14:17:38 -08:00
Csongor Czezar
bd1e15ad70 refactor: using test helpers 2026-01-02 13:52:05 -08:00
Csongor Czezar
a8458c6508 fix: code review items has been aligned 2026-01-02 13:52:05 -08:00
Csongor Czezar
e4110dd254 fix: QPO progress bar now shows node name in subgraphs 2026-01-02 13:52:04 -08:00
7 changed files with 183 additions and 53 deletions

View File

@@ -1,12 +1,12 @@
<template>
<tr
class="border-neutral-700 border-solid border-y"
class="border-y border-solid border-neutral-700"
:class="{
'opacity-50': runner.resolved,
'opacity-75': isLoading && runner.resolved
}"
>
<td class="text-center w-16">
<td class="w-16 text-center">
<TaskListStatusIcon :state="runner.state" :loading="isLoading" />
</td>
<td>
@@ -14,7 +14,7 @@
{{ task.name }}
</p>
<Button
class="inline-block mx-2"
class="mx-2 inline-block"
type="button"
:icon="PrimeIcons.INFO_CIRCLE"
severity="secondary"
@@ -22,11 +22,11 @@
@click="toggle"
/>
<Popover ref="infoPopover" class="block m-1 max-w-64 min-w-32">
<Popover ref="infoPopover" class="m-1 block max-w-64 min-w-32">
<span class="whitespace-pre-line">{{ task.description }}</span>
</Popover>
</td>
<td class="text-right px-4">
<td class="px-4 text-right">
<Button
:icon="task.button?.icon"
:label="task.button?.text"

View File

@@ -14,25 +14,18 @@ vi.mock('@/i18n', () => ({
const executionStore = reactive<{
isIdle: boolean
executionProgress: number
executingNode: unknown
executingNode: null | {
title?: string
type?: string
}
executingNodeProgress: number
nodeProgressStates: Record<string, unknown>
activeJob: {
workflow: {
changeTracker: {
activeState: {
nodes: { id: number; type: string }[]
}
}
}
} | null
}>({
isIdle: true,
executionProgress: 0,
executingNode: null,
executingNodeProgress: 0,
nodeProgressStates: {},
activeJob: null
nodeProgressStates: {}
})
vi.mock('@/stores/executionStore', () => ({
useExecutionStore: () => executionStore
@@ -76,7 +69,6 @@ describe('useBrowserTabTitle', () => {
executionStore.executingNode = null
executionStore.executingNodeProgress = 0
executionStore.nodeProgressStates = {}
executionStore.activeJob = null
// reset setting and workflow stores
vi.mocked(settingStore.get).mockReturnValue('Enabled')
@@ -184,18 +176,12 @@ describe('useBrowserTabTitle', () => {
it('shows node execution title when executing a node using nodeProgressStates', async () => {
executionStore.isIdle = false
executionStore.executionProgress = 0.4
executionStore.executingNode = {
type: 'Foo'
}
executionStore.nodeProgressStates = {
'1': { state: 'running', value: 5, max: 10, node: '1', prompt_id: 'test' }
}
executionStore.activeJob = {
workflow: {
changeTracker: {
activeState: {
nodes: [{ id: 1, type: 'Foo' }]
}
}
}
}
const scope: EffectScope = effectScope()
scope.run(() => useBrowserTabTitle())
await nextTick()

View File

@@ -74,14 +74,14 @@ export const useBrowserTabTitle = () => {
}
// If only one node is running
const [nodeId, state] = runningNodes[0]
const [, state] = runningNodes[0]
const progress = Math.round((state.value / state.max) * 100)
const nodeType =
executionStore.activeJob?.workflow?.changeTracker?.activeState.nodes.find(
(n) => String(n.id) === nodeId
)?.type || 'Node'
const nodeLabel =
executionStore.executingNode?.type?.trim() ||
executionStore.executingNode?.title?.trim() ||
'Node'
return `${executionText.value}[${progress}%] ${nodeType}`
return `${executionText.value}[${progress}%] ${nodeLabel}`
})
const workflowTitle = computed(

View File

@@ -1,5 +1,5 @@
<template>
<BaseModalLayout content-title="" data-testid="settings-dialog" size="md">
<BaseModalLayout content-title="" data-testid="settings-dialog" size="sm">
<template #leftPanelHeaderTitle>
<i class="icon-[lucide--settings]" />
<h2 class="text-neutral text-base">{{ $t('g.settings') }}</h2>
@@ -12,6 +12,7 @@
size="md"
:placeholder="$t('g.searchSettings') + '...'"
:debounce-time="128"
autofocus
@search="handleSearch"
/>
</div>

View File

@@ -1634,6 +1634,7 @@ export class ComfyApp {
executionStore.storeJob({
id: res.prompt_id,
nodes: Object.keys(p.output),
promptOutput: p.output,
workflow: queuedWorkflow
})
}

View File

@@ -1,17 +1,22 @@
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { app } from '@/scripts/app'
import { MAX_PROGRESS_JOBS, useExecutionStore } from '@/stores/executionStore'
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
import { executionIdToNodeLocatorId } from '@/utils/graphTraversalUtil'
import type * as WorkflowStoreModule from '@/platform/workflow/management/stores/workflowStore'
import type { NodeProgressState } from '@/schemas/apiSchema'
type WorkflowConstructor = typeof WorkflowStoreModule.ComfyWorkflow
// Create mock functions that will be shared
const mockNodeExecutionIdToNodeLocatorId = vi.fn()
const mockNodeIdToNodeLocatorId = vi.fn()
const mockNodeLocatorIdToNodeExecutionId = vi.fn()
import type * as WorkflowStoreModule from '@/platform/workflow/management/stores/workflowStore'
import type { NodeProgressState } from '@/schemas/apiSchema'
const workflowModuleState = vi.hoisted(() => ({
WorkflowClass: undefined as WorkflowConstructor | undefined
}))
import { createMockLGraphNode } from '@/utils/__tests__/litegraphTestUtils'
import { createTestingPinia } from '@pinia/testing'
@@ -20,6 +25,7 @@ vi.mock('@/platform/workflow/management/stores/workflowStore', async () => {
const { ComfyWorkflow } = await vi.importActual<typeof WorkflowStoreModule>(
'@/platform/workflow/management/stores/workflowStore'
)
workflowModuleState.WorkflowClass = ComfyWorkflow
return {
ComfyWorkflow,
useWorkflowStore: vi.fn(() => ({
@@ -60,7 +66,7 @@ vi.mock('@/scripts/api', () => ({
}
}))
vi.mock('@/stores/imagePreviewStore', () => ({
vi.mock('@/stores/nodeOutputStore', () => ({
useNodeOutputStore: () => ({
revokePreviewsByExecutionId: vi.fn()
})
@@ -84,6 +90,29 @@ vi.mock('@/scripts/app', () => ({
}
}))
function createQueuedWorkflow(path: string = 'workflows/test.json') {
const { WorkflowClass } = workflowModuleState
if (!WorkflowClass) {
throw new Error('ComfyWorkflow mock class is not available')
}
return new WorkflowClass({
path,
modified: 0,
size: 0
})
}
function createPromptNode(title: string, classType: string) {
return {
inputs: {},
class_type: classType,
_meta: {
title
}
}
}
describe('useExecutionStore - NodeLocatorId conversions', () => {
let store: ReturnType<typeof useExecutionStore>
@@ -598,6 +627,103 @@ describe('useExecutionErrorStore - Node Error Lookups', () => {
})
})
describe('useExecutionStore - executingNode with subgraphs', () => {
let store: ReturnType<typeof useExecutionStore>
beforeEach(() => {
vi.clearAllMocks()
setActivePinia(createTestingPinia({ stubActions: false }))
store = useExecutionStore()
})
it('should find executing node info in root graph from queued prompt data', () => {
store.storeJob({
id: 'test-prompt',
nodes: ['123'],
promptOutput: {
'123': createPromptNode('Test Node', 'TestNode')
},
workflow: createQueuedWorkflow()
})
store.activeJobId = 'test-prompt'
store.nodeProgressStates = {
'123': {
state: 'running',
value: 0,
max: 100,
display_node_id: '123',
prompt_id: 'test-prompt',
node_id: '123'
}
}
expect(store.executingNode).toEqual({
title: 'Test Node',
type: 'TestNode'
})
})
it('should find executing node info in subgraph using execution ID', () => {
store.storeJob({
id: 'test-prompt',
nodes: ['456:789'],
promptOutput: {
'456:789': createPromptNode('Nested Node', 'NestedNode')
},
workflow: createQueuedWorkflow()
})
store.activeJobId = 'test-prompt'
store.nodeProgressStates = {
'456:789': {
state: 'running',
value: 0,
max: 100,
display_node_id: '456:789',
prompt_id: 'test-prompt',
node_id: '456:789'
}
}
expect(store.executingNode).toEqual({
title: 'Nested Node',
type: 'NestedNode'
})
})
it('should return null when no node is executing', () => {
store.nodeProgressStates = {}
expect(store.executingNode).toBeNull()
})
it('should return null when executing node metadata cannot be found', () => {
store.storeJob({
id: 'test-prompt',
nodes: ['123'],
promptOutput: {
'123': createPromptNode('Test Node', 'TestNode')
},
workflow: createQueuedWorkflow()
})
store.activeJobId = 'test-prompt'
store.nodeProgressStates = {
'999': {
state: 'running',
value: 0,
max: 100,
display_node_id: '999',
prompt_id: 'test-prompt',
node_id: '999'
}
}
expect(store.executingNode).toBeNull()
})
})
describe('useExecutionErrorStore - setMissingNodeTypes', () => {
let store: ReturnType<typeof useExecutionErrorStore>

View File

@@ -7,8 +7,7 @@ import { useTelemetry } from '@/platform/telemetry'
import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import type {
ComfyNode,
ComfyWorkflowJSON,
ComfyApiWorkflow,
NodeId
} from '@/platform/workflow/validation/schemas/workflowSchema'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
@@ -34,6 +33,11 @@ import type { NodeLocatorId } from '@/types/nodeIdentification'
import { classifyCloudValidationError } from '@/utils/executionErrorUtil'
import { executionIdToNodeLocatorId } from '@/utils/graphTraversalUtil'
interface ExecutionNodeInfo {
title?: string | null
type?: string | null
}
interface QueuedJob {
/**
* The nodes that are queued to be executed. The key is the node id and the
@@ -44,6 +48,25 @@ interface QueuedJob {
* The workflow that is queued to be executed
*/
workflow?: ComfyWorkflow
/**
* Queue-time node metadata keyed by execution ID.
* This stays stable even if the user switches workflows or edits the canvas.
*/
nodeLookup?: Record<string, ExecutionNodeInfo>
}
function buildExecutionNodeLookup(
promptOutput: ComfyApiWorkflow
): Record<string, ExecutionNodeInfo> {
return Object.fromEntries(
Object.entries(promptOutput).map(([executionId, node]) => [
executionId,
{
title: node._meta.title,
type: node.class_type
}
])
)
}
/**
@@ -166,21 +189,11 @@ export const useExecutionStore = defineStore('execution', () => {
() => new Set(executingNodeIds.value.map(String))
)
// For backward compatibility - returns the primary executing node
const executingNode = computed<ComfyNode | null>(() => {
// For backward compatibility - returns the primary executing node info
const executingNode = computed<ExecutionNodeInfo | null>(() => {
if (!executingNodeId.value) return null
const workflow: ComfyWorkflow | undefined = activeJob.value?.workflow
if (!workflow) return null
const canvasState: ComfyWorkflowJSON | null =
workflow.changeTracker?.activeState ?? null
if (!canvasState) return null
return (
canvasState.nodes.find((n) => String(n.id) === executingNodeId.value) ??
null
)
return activeJob.value?.nodeLookup?.[String(executingNodeId.value)] ?? null
})
// This is the progress of the currently executing node (for backward compatibility)
@@ -536,10 +549,12 @@ export const useExecutionStore = defineStore('execution', () => {
function storeJob({
nodes,
id,
promptOutput,
workflow
}: {
nodes: string[]
id: string
promptOutput: ComfyApiWorkflow
workflow: ComfyWorkflow
}) {
queuedJobs.value[id] ??= { nodes: {} }
@@ -551,6 +566,7 @@ export const useExecutionStore = defineStore('execution', () => {
}, {}),
...queuedJob.nodes
}
queuedJob.nodeLookup = buildExecutionNodeLookup(promptOutput)
queuedJob.workflow = workflow
const wid = workflow?.activeState?.id ?? workflow?.initialState?.id
if (wid) {