feat: App mode progress updates (#9375)

## Summary

- move progress bar below preview thumbnail instead of overlaying it
- add interactive pending placeholder
- fix: scope in-progress items to active workflow in output history

## Screenshots (if applicable)

<img width="209" height="86" alt="image"
src="https://github.com/user-attachments/assets/46590fdc-3df9-4a40-8492-a54e63e3f44c"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9375-feat-App-mode-progress-updates-3196d73d3650817ea891c9e744893846)
by [Unito](https://www.unito.io)
This commit is contained in:
pythongosssss
2026-03-05 10:26:03 +00:00
committed by GitHub
parent 5fc82823ef
commit e0089d93d0
5 changed files with 77 additions and 17 deletions

View File

@@ -8,11 +8,13 @@ defineOptions({ inheritAttrs: false })
const {
class: className,
overallOpacity = 1,
activeOpacity = 1
activeOpacity = 1,
rounded = false
} = defineProps<{
class?: string
overallOpacity?: number
activeOpacity?: number
rounded?: boolean
}>()
const { totalPercent, currentNodePercent } = useQueueProgress()
@@ -24,16 +26,19 @@ const queueStore = useQueueStore()
cn(
'relative h-2 bg-secondary-background transition-opacity',
queueStore.runningTasks.length === 0 && 'opacity-0',
rounded && 'rounded-sm',
className
)
"
>
<div
class="absolute inset-0 h-full bg-interface-panel-job-progress-primary transition-[width]"
:class="cn(rounded && 'rounded-sm')"
:style="{ width: `${totalPercent}%`, opacity: overallOpacity }"
/>
<div
class="absolute inset-0 h-full bg-interface-panel-job-progress-secondary transition-[width]"
:class="cn(rounded && 'rounded-sm')"
:style="{ width: `${currentNodePercent}%`, opacity: activeOpacity }"
/>
</div>

View File

@@ -55,6 +55,12 @@ const visibleHistory = computed(() =>
const selectableItems = computed(() => {
const items: SelectionValue[] = []
if (
queueCount.value > 0 &&
store.activeWorkflowInProgressItems.length === 0
) {
items.push({ id: 'slot:pending', kind: 'inProgress', itemId: 'pending' })
}
for (const item of store.activeWorkflowInProgressItems) {
items.push({
id: `slot:${item.id}`,
@@ -177,7 +183,7 @@ useResizeObserver(outputsRef, () => {
})
watch(
[
() => store.inProgressItems.length,
() => store.activeWorkflowInProgressItems.length,
() => visibleHistory.value[0]?.id,
queueCount
],
@@ -280,26 +286,36 @@ useEventListener(document.body, 'keydown', (e: KeyboardEvent) => {
data-testid="linear-outputs"
class="py-3 overflow-y-clip overflow-x-auto min-w-0"
>
<div class="flex items-center gap-0.5 mx-auto w-fit">
<div class="flex items-start gap-0.5 mx-auto w-fit h-21">
<div
v-if="queueCount > 0 || hasActiveContent"
:class="
cn(
'sticky left-0 z-10 shrink-0 flex items-center gap-0.5',
'sticky left-0 z-10 shrink-0 flex items-start gap-0.5',
'bg-comfy-menu-bg md:bg-comfy-menu-secondary-bg'
)
"
>
<div v-if="queueCount > 0" class="shrink-0 flex items-center gap-0.5">
<OutputHistoryActiveQueueItem :queue-count="queueCount" />
<div
v-if="hasActiveContent || visibleHistory.length > 0"
class="border-l border-border-default h-12 shrink-0 mx-4"
/>
<OutputHistoryActiveQueueItem
v-if="queueCount > 1"
class="mr-3"
:queue-count="queueCount"
/>
<div
v-if="
queueCount > 0 && store.activeWorkflowInProgressItems.length === 0
"
:ref="selectedRef('slot:pending')"
v-bind="itemAttrs('slot:pending')"
:class="itemClass"
@click="store.select('slot:pending')"
>
<OutputPreviewItem />
</div>
<div
v-for="item in store.inProgressItems"
v-for="item in store.activeWorkflowInProgressItems"
:key="`${item.id}-${item.state}`"
:ref="selectedRef(`slot:${item.id}`)"
v-bind="itemAttrs(`slot:${item.id}`)"

View File

@@ -0,0 +1,42 @@
import { shallowMount } from '@vue/test-utils'
import { describe, expect, it, vi } from 'vitest'
import OutputHistoryActiveQueueItem from './OutputHistoryActiveQueueItem.vue'
vi.mock('vue-i18n', () => ({
useI18n: () => ({ t: (key: string) => key })
}))
vi.mock('@/stores/commandStore', () => ({
useCommandStore: () => ({
execute: vi.fn()
})
}))
function mountComponent(queueCount: number) {
return shallowMount(OutputHistoryActiveQueueItem, {
props: { queueCount }
})
}
describe('OutputHistoryActiveQueueItem', () => {
it('shows badge when queueCount is 1', () => {
const wrapper = mountComponent(1)
const badge = wrapper.find('[aria-hidden="true"]')
expect(badge.exists()).toBe(true)
expect(badge.text()).toBe('1')
})
it('shows badge with correct count when queueCount is 3', () => {
const wrapper = mountComponent(3)
const badge = wrapper.find('[aria-hidden="true"]')
expect(badge.exists()).toBe(true)
expect(badge.text()).toBe('3')
})
it('hides badge when queueCount is 0', () => {
const wrapper = mountComponent(0)
const badge = wrapper.find('[aria-hidden="true"]')
expect(badge.exists()).toBe(false)
})
})

View File

@@ -48,7 +48,7 @@ function clearQueue(close: () => void) {
</template>
</Popover>
<div
v-if="queueCount > 1"
v-if="queueCount > 0"
aria-hidden="true"
class="absolute top-0 right-0 min-w-4 h-4 flex justify-center items-center rounded-full bg-primary-background text-text-primary text-xs"
v-text="queueCount"

View File

@@ -6,16 +6,13 @@ const { latentPreview } = defineProps<{
}>()
</script>
<template>
<div class="relative size-10">
<div class="w-10">
<img
v-if="latentPreview"
class="block size-10 rounded-sm object-cover"
:src="latentPreview"
/>
<div v-else class="size-10 rounded-sm skeleton-shimmer" />
<LinearProgressBar
class="absolute inset-0 size-full bg-transparent"
:overall-opacity="0.7"
/>
<LinearProgressBar class="w-10 h-1 mt-1" rounded />
</div>
</template>