mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 06:20:11 +00:00
feat: add Phase 3 UI components for concurrent job execution
- Eye toggle in QueueJobItem: visible for running jobs when concurrent execution is active, toggles which job's progress shows on canvas - Parallel toggle in run mode dropdown: SwitchRoot below separator with [NEW] badge, disabled for auto-run modes, gated by feature flag - N running/queued counters in top bar: Option A design with separate cancel (running) and clear (queued) actions when concurrent active - Auto-queue guard: suppresses auto-re-queue when concurrent execution is enabled to prevent infinite parallel job spawning - QueueInlineProgress already reads focused job state (no changes needed) - Wire up JobGroupsList to pass isFocused prop and handle focus events - Remove useConcurrentExecution from knip ignore (now imported)
This commit is contained in:
@@ -55,9 +55,7 @@ const config: KnipConfig = {
|
||||
// Agent review check config, not part of the build
|
||||
'.agents/checks/eslint.strict.config.js',
|
||||
// Loaded via @plugin directive in CSS, not detected by knip
|
||||
'packages/design-system/src/css/lucideStrokePlugin.js',
|
||||
// Pending integration in stacked PR (concurrent job execution)
|
||||
'src/composables/useConcurrentExecution.ts'
|
||||
'packages/design-system/src/css/lucideStrokePlugin.js'
|
||||
],
|
||||
compilers: {
|
||||
// https://github.com/webpro-nl/knip/issues/1008#issuecomment-3207756199
|
||||
|
||||
@@ -32,16 +32,62 @@
|
||||
<Suspense @resolve="comfyRunButtonResolved">
|
||||
<ComfyRunButton />
|
||||
</Suspense>
|
||||
<Button
|
||||
v-tooltip.bottom="cancelJobTooltipConfig"
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
:disabled="isExecutionIdle"
|
||||
:aria-label="t('menu.interrupt')"
|
||||
@click="cancelCurrentJob"
|
||||
>
|
||||
<i class="icon-[lucide--x] size-4" />
|
||||
</Button>
|
||||
<template v-if="showConcurrentControls">
|
||||
<div
|
||||
class="flex items-center gap-1 rounded-lg bg-secondary-background px-2 py-1"
|
||||
>
|
||||
<span class="text-sm font-normal tabular-nums">
|
||||
<span class="text-primary-foreground">{{
|
||||
n(runningCount)
|
||||
}}</span>
|
||||
{{ ' ' }}{{ t('menu.running') }}
|
||||
</span>
|
||||
<Button
|
||||
v-tooltip.bottom="cancelJobTooltipConfig"
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
:aria-label="t('menu.interrupt')"
|
||||
@click="cancelCurrentJob"
|
||||
>
|
||||
<i class="icon-[lucide--x] size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
v-if="queuedCount > 0"
|
||||
class="flex items-center gap-1 rounded-lg bg-secondary-background px-2 py-1"
|
||||
>
|
||||
<span class="text-sm font-normal tabular-nums">
|
||||
<span class="text-primary-foreground">{{
|
||||
n(queuedCount)
|
||||
}}</span>
|
||||
{{ ' ' }}{{ t('menu.queued') }}
|
||||
</span>
|
||||
<Button
|
||||
v-tooltip.bottom="clearQueueTooltipConfig"
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
:aria-label="
|
||||
t('sideToolbar.queueProgressOverlay.clearQueueTooltip')
|
||||
"
|
||||
@click="handleClearQueue"
|
||||
>
|
||||
<i class="icon-[lucide--list-x] size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<Button
|
||||
v-tooltip.bottom="cancelJobTooltipConfig"
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
:disabled="isExecutionIdle"
|
||||
:aria-label="t('menu.interrupt')"
|
||||
@click="cancelCurrentJob"
|
||||
>
|
||||
<i class="icon-[lucide--x] size-4" />
|
||||
</Button>
|
||||
</template>
|
||||
<Button
|
||||
v-tooltip.bottom="queueHistoryTooltipConfig"
|
||||
variant="secondary"
|
||||
@@ -108,6 +154,7 @@ import StatusBadge from '@/components/common/StatusBadge.vue'
|
||||
import QueueInlineProgress from '@/components/queue/QueueInlineProgress.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useQueueFeatureFlags } from '@/composables/queue/useQueueFeatureFlags'
|
||||
import { useConcurrentExecution } from '@/composables/useConcurrentExecution'
|
||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
@@ -143,6 +190,14 @@ const { isIdle: isExecutionIdle } = storeToRefs(executionStore)
|
||||
const { activeJobsCount } = storeToRefs(queueStore)
|
||||
const { activeSidebarTabId } = storeToRefs(sidebarTabStore)
|
||||
|
||||
const { isConcurrentExecutionEnabled } = useConcurrentExecution()
|
||||
|
||||
const runningCount = computed(() => executionStore.runningJobIds.length)
|
||||
const queuedCount = computed(() => queueStore.pendingTasks.length)
|
||||
const showConcurrentControls = computed(
|
||||
() => isConcurrentExecutionEnabled.value && !executionStore.isIdle
|
||||
)
|
||||
|
||||
const position = computed(() => settingStore.get('Comfy.UseNewMenu'))
|
||||
const visible = computed(() => position.value !== 'Disabled')
|
||||
const { isQueuePanelV2Enabled, isRunProgressBarEnabled } =
|
||||
@@ -371,6 +426,9 @@ watch(isDragging, (dragging) => {
|
||||
const cancelJobTooltipConfig = computed(() =>
|
||||
buildTooltipConfig(t('menu.interrupt'))
|
||||
)
|
||||
const clearQueueTooltipConfig = computed(() =>
|
||||
buildTooltipConfig(t('sideToolbar.queueProgressOverlay.clearQueueTooltip'))
|
||||
)
|
||||
const queueHistoryTooltipConfig = computed(() =>
|
||||
buildTooltipConfig(
|
||||
t(
|
||||
|
||||
@@ -56,6 +56,37 @@
|
||||
{{ item.label }}
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator
|
||||
v-if="isParallelToggleVisible"
|
||||
class="mx-1 my-1 border-t border-border-subtle"
|
||||
/>
|
||||
<div
|
||||
v-if="isParallelToggleVisible"
|
||||
class="flex items-center justify-between gap-3 rounded-md px-2 py-1.5"
|
||||
:class="cn(isParallelToggleDisabled && 'opacity-50')"
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span class="text-sm text-text-primary">{{
|
||||
t('menu.parallelExecution')
|
||||
}}</span>
|
||||
<StatusBadge label="NEW" class="text-[10px]" />
|
||||
</div>
|
||||
<span class="text-xs text-text-muted">{{
|
||||
t('menu.parallelUpTo', { count: maxConcurrentJobs })
|
||||
}}</span>
|
||||
</div>
|
||||
<SwitchRoot
|
||||
:checked="parallelToggleChecked"
|
||||
:disabled="isParallelToggleDisabled"
|
||||
class="relative h-5 w-9 shrink-0 cursor-pointer rounded-full bg-secondary-background transition-colors data-[state=checked]:bg-primary-background data-[disabled]:cursor-not-allowed"
|
||||
@update:checked="onParallelToggle"
|
||||
>
|
||||
<SwitchThumb
|
||||
class="block size-4 translate-x-0.5 rounded-full bg-white shadow-sm transition-transform data-[state=checked]:translate-x-[18px]"
|
||||
/>
|
||||
</SwitchRoot>
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuRoot>
|
||||
@@ -68,16 +99,21 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuRoot,
|
||||
DropdownMenuTrigger
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
SwitchRoot,
|
||||
SwitchThumb
|
||||
} from 'reka-ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import BatchCountEdit from '@/components/actionbar/BatchCountEdit.vue'
|
||||
import StatusBadge from '@/components/common/StatusBadge.vue'
|
||||
import TinyChevronIcon from '@/components/actionbar/TinyChevronIcon.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import ButtonGroup from '@/components/ui/button-group/ButtonGroup.vue'
|
||||
import { useConcurrentExecution } from '@/composables/useConcurrentExecution'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { app } from '@/scripts/app'
|
||||
@@ -219,6 +255,23 @@ const queueButtonTooltip = computed(() => {
|
||||
return t('menu.runWorkflow')
|
||||
})
|
||||
|
||||
const {
|
||||
isFeatureEnabled,
|
||||
isUserEnabled,
|
||||
maxConcurrentJobs,
|
||||
setUserEnabled
|
||||
} = useConcurrentExecution()
|
||||
|
||||
const isParallelToggleVisible = isFeatureEnabled
|
||||
const isParallelToggleDisabled = computed(
|
||||
() => selectedQueueMode.value !== 'disabled'
|
||||
)
|
||||
const parallelToggleChecked = isUserEnabled
|
||||
|
||||
function onParallelToggle(checked: boolean) {
|
||||
void setUserEnabled(checked)
|
||||
}
|
||||
|
||||
const commandStore = useCommandStore()
|
||||
const queuePrompt = async (e: Event) => {
|
||||
if (isStopInstantAction.value) {
|
||||
|
||||
@@ -24,10 +24,16 @@
|
||||
:progress-current-percent="ji.progressCurrentPercent"
|
||||
:running-node-name="ji.runningNodeName"
|
||||
:active-details-id="activeDetailsId"
|
||||
:is-focused="
|
||||
isConcurrentExecutionEnabled && ji.state === 'running'
|
||||
? ji.id === String(executionStore.focusedJobId ?? '')
|
||||
: undefined
|
||||
"
|
||||
@cancel="emitCancelItem(ji)"
|
||||
@delete="emitDeleteItem(ji)"
|
||||
@menu="(ev) => $emit('menu', ji, ev)"
|
||||
@view="$emit('viewItem', ji)"
|
||||
@focus="executionStore.setFocusedJob(ji.id)"
|
||||
@details-enter="onDetailsEnter"
|
||||
@details-leave="onDetailsLeave"
|
||||
/>
|
||||
@@ -39,10 +45,15 @@
|
||||
import { onBeforeUnmount, ref, watch } from 'vue'
|
||||
|
||||
import QueueJobItem from '@/components/queue/job/QueueJobItem.vue'
|
||||
import { useConcurrentExecution } from '@/composables/useConcurrentExecution'
|
||||
import type { JobGroup, JobListItem } from '@/composables/queue/useJobList'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
|
||||
const props = defineProps<{ displayedJobGroups: JobGroup[] }>()
|
||||
|
||||
const executionStore = useExecutionStore()
|
||||
const { isConcurrentExecutionEnabled } = useConcurrentExecution()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'cancelItem', item: JobListItem): void
|
||||
(e: 'deleteItem', item: JobListItem): void
|
||||
|
||||
@@ -170,6 +170,25 @@
|
||||
<slot name="secondary">{{ rightText }}</slot>
|
||||
</div>
|
||||
</Transition>
|
||||
<!-- Running job focus button - always visible (concurrent execution) -->
|
||||
<Button
|
||||
v-if="state === 'running' && isFocused !== undefined"
|
||||
v-tooltip.top="focusTooltipConfig"
|
||||
variant="textonly"
|
||||
size="icon-sm"
|
||||
:aria-label="t('queue.jobItem.focusTooltip')"
|
||||
@click.stop="emit('focus')"
|
||||
>
|
||||
<i
|
||||
:class="
|
||||
cn(
|
||||
'size-4',
|
||||
isFocused ? 'icon-[lucide--eye]' : 'icon-[lucide--eye-off]',
|
||||
isFocused ? 'text-text-primary' : 'text-text-secondary'
|
||||
)
|
||||
"
|
||||
/>
|
||||
</Button>
|
||||
<!-- Running job cancel button - always visible -->
|
||||
<Button
|
||||
v-if="state === 'running' && computedShowClear"
|
||||
@@ -211,7 +230,8 @@ const {
|
||||
showMenu,
|
||||
progressTotalPercent,
|
||||
progressCurrentPercent,
|
||||
activeDetailsId = null
|
||||
activeDetailsId = null,
|
||||
isFocused
|
||||
} = defineProps<{
|
||||
jobId: string
|
||||
workflowId?: string
|
||||
@@ -225,6 +245,7 @@ const {
|
||||
progressTotalPercent?: number
|
||||
progressCurrentPercent?: number
|
||||
activeDetailsId?: string | null
|
||||
isFocused?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -232,6 +253,7 @@ const emit = defineEmits<{
|
||||
(e: 'delete'): void
|
||||
(e: 'menu', event: MouseEvent): void
|
||||
(e: 'view'): void
|
||||
(e: 'focus'): void
|
||||
(e: 'details-enter', jobId: string): void
|
||||
(e: 'details-leave', jobId: string): void
|
||||
}>()
|
||||
@@ -249,6 +271,9 @@ const {
|
||||
const cancelTooltipConfig = computed(() => buildTooltipConfig(t('g.cancel')))
|
||||
const deleteTooltipConfig = computed(() => buildTooltipConfig(t('g.delete')))
|
||||
const moreTooltipConfig = computed(() => buildTooltipConfig(t('g.more')))
|
||||
const focusTooltipConfig = computed(() =>
|
||||
buildTooltipConfig(t('queue.jobItem.focusTooltip'))
|
||||
)
|
||||
|
||||
const rowRef = ref<HTMLDivElement | null>(null)
|
||||
const showDetails = computed(() => activeDetailsId === jobId)
|
||||
|
||||
@@ -1003,7 +1003,11 @@
|
||||
"settings": "Settings",
|
||||
"help": "Help",
|
||||
"queue": "Queue Panel",
|
||||
"fullscreen": "Fullscreen"
|
||||
"fullscreen": "Fullscreen",
|
||||
"running": "running",
|
||||
"queued": "queued",
|
||||
"parallelExecution": "Run jobs in parallel",
|
||||
"parallelUpTo": "Up to {count}"
|
||||
},
|
||||
"tabMenu": {
|
||||
"duplicateTab": "Duplicate Tab",
|
||||
@@ -1221,6 +1225,9 @@
|
||||
"jobAddedToQueue": "Job queued",
|
||||
"jobQueueing": "Job queuing",
|
||||
"completedIn": "Finished in {duration}",
|
||||
"jobItem": {
|
||||
"focusTooltip": "Progress currently shown on canvas"
|
||||
},
|
||||
"jobMenu": {
|
||||
"openAsWorkflowNewTab": "Open as workflow in new tab",
|
||||
"openWorkflowNewTab": "Open workflow in new tab",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useConcurrentExecution } from '@/composables/useConcurrentExecution'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import {
|
||||
@@ -9,10 +10,12 @@ import {
|
||||
export function setupAutoQueueHandler() {
|
||||
const queueCountStore = useQueuePendingTaskCountStore()
|
||||
const queueSettingsStore = useQueueSettingsStore()
|
||||
const { isConcurrentExecutionEnabled } = useConcurrentExecution()
|
||||
|
||||
let graphHasChanged = false
|
||||
let internalCount = 0 // Use an internal counter here so it is instantly updated when re-queuing
|
||||
api.addEventListener('graphChanged', () => {
|
||||
if (isConcurrentExecutionEnabled.value) return
|
||||
if (queueSettingsStore.mode === 'change') {
|
||||
if (internalCount) {
|
||||
graphHasChanged = true
|
||||
@@ -29,6 +32,7 @@ export function setupAutoQueueHandler() {
|
||||
async () => {
|
||||
internalCount = queueCountStore.count
|
||||
if (!internalCount && !app.lastExecutionError) {
|
||||
if (isConcurrentExecutionEnabled.value) return
|
||||
if (
|
||||
isInstantRunningMode(queueSettingsStore.mode) ||
|
||||
(queueSettingsStore.mode === 'change' && graphHasChanged)
|
||||
|
||||
Reference in New Issue
Block a user