App mode - more updates & fixes (#9137)

## Summary

- fix sizing of sidebars in app mode
- update feedback button to match design
- update job queue notification
- clickable queue spinner item to allow clear queue
- refactor mode out of store to specific workflow instance
- support different saved vs active mode
- other styling/layout tweaks

## Changes

- **What**: Changes the store to a composable and moves the mode state
to the workflow.
- This enables switching between tabs and maintaining the mode they were
in

## Screenshots (if applicable)
<img width="1866" height="1455" alt="image"
src="https://github.com/user-attachments/assets/f9a8cd36-181f-4948-b48c-dd27bd9127cf"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9137-App-mode-more-updates-fixes-3106d73d365081a18ccff6ffe24fdec7)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
pythongosssss
2026-02-26 17:55:10 +00:00
committed by GitHub
parent ac12a3d9b9
commit 9fb93a5b0a
32 changed files with 689 additions and 236 deletions

View File

@@ -124,7 +124,7 @@ import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useAppModeStore } from '@/stores/appModeStore'
import { useAppMode } from '@/composables/useAppMode'
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
@@ -145,12 +145,12 @@ const unifiedWidth = computed(() =>
const { focusMode } = storeToRefs(workspaceStore)
const appModeStore = useAppModeStore()
const { mode } = useAppMode()
const { activeSidebarTabId, activeSidebarTab } = storeToRefs(sidebarTabStore)
const { bottomPanelVisible } = storeToRefs(useBottomPanelStore())
const { isOpen: rightSidePanelVisible } = storeToRefs(rightSidePanelStore)
const showOffsideSplitter = computed(
() => rightSidePanelVisible.value || appModeStore.mode === 'builder:select'
() => rightSidePanelVisible.value || mode.value === 'builder:select'
)
const sidebarPanelVisible = computed(() => activeSidebarTab.value !== null)

View File

@@ -8,12 +8,12 @@ import { useWorkflowTemplateSelectorDialog } from '@/composables/useWorkflowTemp
import { useCommandStore } from '@/stores/commandStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { cn } from '@/utils/tailwindUtil'
import { useAppModeStore } from '@/stores/appModeStore'
import { useAppMode } from '@/composables/useAppMode'
const { t } = useI18n()
const commandStore = useCommandStore()
const workspaceStore = useWorkspaceStore()
const appModeStore = useAppModeStore()
const { enableAppBuilder, setMode } = useAppMode()
const tooltipOptions = { showDelay: 300, hideDelay: 300 }
const isAssetsActive = computed(
@@ -24,7 +24,7 @@ const isWorkflowsActive = computed(
)
function enterBuilderMode() {
appModeStore.setMode('builder:select')
setMode('builder:select')
}
function openAssets() {
@@ -61,7 +61,7 @@ function openTemplates() {
</WorkflowActionsDropdown>
<Button
v-if="appModeStore.enableAppBuilder"
v-if="enableAppBuilder"
v-tooltip.right="{
value: t('linearMode.appModeToolbar.appBuilder'),
...tooltipOptions

View File

@@ -20,7 +20,7 @@
)
"
:aria-current="activeStep === step.id ? 'step' : undefined"
@click="appModeStore.setMode(step.id)"
@click="setMode(step.id)"
>
<StepBadge :step :index :model-value="activeStep" />
<StepLabel :step />
@@ -31,9 +31,9 @@
<!-- Save -->
<ConnectOutputPopover
v-if="!appModeStore.hasOutputs"
v-if="!hasOutputs"
:is-select-active="activeStep === 'builder:select'"
@switch="appModeStore.setMode('builder:select')"
@switch="setMode('builder:select')"
>
<button :class="cn(stepClasses, 'opacity-30 bg-transparent')">
<StepBadge :step="saveStep" :index="2" :model-value="activeStep" />
@@ -50,7 +50,7 @@
: 'hover:bg-secondary-background bg-transparent'
)
"
@click="appModeStore.setBuilderSaving(true)"
@click="setSaving(true)"
>
<StepBadge :step="saveStep" :index="2" :model-value="activeStep" />
<StepLabel :step="saveStep" />
@@ -62,31 +62,25 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useEventListener } from '@vueuse/core'
import { useAppMode } from '@/composables/useAppMode'
import type { AppMode } from '@/composables/useAppMode'
import { useAppModeStore } from '@/stores/appModeStore'
import type { AppMode } from '@/stores/appModeStore'
import { cn } from '@/utils/tailwindUtil'
import { useBuilderSave } from './useBuilderSave'
import ConnectOutputPopover from './ConnectOutputPopover.vue'
import StepBadge from './StepBadge.vue'
import StepLabel from './StepLabel.vue'
import type { BuilderToolbarStep } from './types'
import { storeToRefs } from 'pinia'
const { t } = useI18n()
const appModeStore = useAppModeStore()
const { mode, setMode } = useAppMode()
const { hasOutputs } = storeToRefs(useAppModeStore())
const { saving, setSaving } = useBuilderSave()
useEventListener(document, 'keydown', (e: KeyboardEvent) => {
if (e.key !== 'Escape') return
e.preventDefault()
e.stopPropagation()
void appModeStore.exitBuilder()
})
const activeStep = computed(() =>
appModeStore.isBuilderSaving ? 'save' : appModeStore.mode
)
const activeStep = computed(() => (saving.value ? 'save' : mode.value))
const stepClasses =
'inline-flex h-14 min-h-8 cursor-pointer items-center gap-3 rounded-lg py-2 pr-4 pl-2 transition-colors border-none'

View File

@@ -1,30 +1,36 @@
import { watch } from 'vue'
import { ref } from 'vue'
import { useErrorHandling } from '@/composables/useErrorHandling'
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { useDialogService } from '@/services/dialogService'
import { useAppMode } from '@/composables/useAppMode'
import { useAppModeStore } from '@/stores/appModeStore'
import { useDialogStore } from '@/stores/dialogStore'
import BuilderSaveDialogContent from './BuilderSaveDialogContent.vue'
import BuilderSaveSuccessDialogContent from './BuilderSaveSuccessDialogContent.vue'
import { whenever } from '@vueuse/core'
const SAVE_DIALOG_KEY = 'builder-save'
const SUCCESS_DIALOG_KEY = 'builder-save-success'
export function useBuilderSave() {
const appModeStore = useAppModeStore()
const { setMode } = useAppMode()
const { toastErrorHandler } = useErrorHandling()
const workflowStore = useWorkflowStore()
const workflowService = useWorkflowService()
const dialogService = useDialogService()
const appModeStore = useAppModeStore()
const dialogStore = useDialogStore()
watch(
() => appModeStore.isBuilderSaving,
(saving) => {
if (saving) void onBuilderSave()
}
)
const saving = ref(false)
whenever(saving, onBuilderSave)
function setSaving(value: boolean) {
saving.value = value
}
async function onBuilderSave() {
const workflow = workflowStore.activeWorkflow
@@ -33,13 +39,14 @@ export function useBuilderSave() {
return
}
if (!workflow.isTemporary && workflow.activeState.extra?.linearMode) {
if (!workflow.isTemporary && workflow.initialMode != null) {
// Re-save with the previously chosen mode — no dialog needed.
try {
workflow.changeTracker?.checkState()
appModeStore.saveSelectedToWorkflow()
appModeStore.flushSelections()
await workflowService.saveWorkflow(workflow)
showSuccessDialog(workflow.filename, appModeStore.isAppMode)
} catch {
showSuccessDialog(workflow.filename, workflow.initialMode === 'app')
} catch (e) {
toastErrorHandler(e)
resetSaving()
}
return
@@ -73,17 +80,19 @@ export function useBuilderSave() {
const workflow = workflowStore.activeWorkflow
if (!workflow) return
appModeStore.saveSelectedToWorkflow()
appModeStore.flushSelections()
const mode = openAsApp ? 'app' : 'graph'
const saved = await workflowService.saveWorkflowAs(workflow, {
filename,
openAsApp
initialMode: mode
})
if (!saved) return
closeSaveDialog()
showSuccessDialog(filename, openAsApp)
} catch {
} catch (e) {
toastErrorHandler(e)
closeSaveDialog()
resetSaving()
}
@@ -97,7 +106,7 @@ export function useBuilderSave() {
workflowName,
savedAsApp,
onViewApp: () => {
appModeStore.setMode('app')
setMode('app')
closeSuccessDialog()
},
onClose: closeSuccessDialog
@@ -118,6 +127,8 @@ export function useBuilderSave() {
}
function resetSaving() {
appModeStore.setBuilderSaving(false)
saving.value = false
}
return { saving, setSaving }
}

View File

@@ -21,7 +21,7 @@
</div>
</div>
</template>
<template v-if="showUI && !appModeStore.isBuilderMode" #side-toolbar>
<template v-if="showUI && !isBuilderMode" #side-toolbar>
<SideToolbar />
</template>
<template v-if="showUI" #side-bar-panel>
@@ -31,27 +31,24 @@
<ExtensionSlot v-if="activeSidebarTab" :extension="activeSidebarTab" />
</div>
</template>
<template v-if="showUI && !appModeStore.isBuilderMode" #topmenu>
<template v-if="showUI && !isBuilderMode" #topmenu>
<TopMenuSection />
</template>
<template v-if="showUI" #bottom-panel>
<BottomPanel />
</template>
<template v-if="showUI" #right-side-panel>
<AppBuilder v-if="appModeStore.mode === 'builder:select'" />
<NodePropertiesPanel v-else-if="!appModeStore.isBuilderMode" />
<AppBuilder v-if="mode === 'builder:select'" />
<NodePropertiesPanel v-else-if="!isBuilderMode" />
</template>
<template #graph-canvas-panel>
<GraphCanvasMenu
v-if="canvasMenuEnabled && !appModeStore.isBuilderMode"
v-if="canvasMenuEnabled && !isBuilderMode"
class="pointer-events-auto"
/>
<MiniMap
v-if="
comfyAppReady &&
minimapEnabled &&
betaMenuEnabled &&
!appModeStore.isBuilderMode
comfyAppReady && minimapEnabled && betaMenuEnabled && !isBuilderMode
"
class="pointer-events-auto"
/>
@@ -127,10 +124,10 @@ import {
import { useI18n } from 'vue-i18n'
import { isMiddlePointerInput } from '@/base/pointerUtils'
import AppBuilder from '@/components/builder/AppBuilder.vue'
import LiteGraphCanvasSplitterOverlay from '@/components/LiteGraphCanvasSplitterOverlay.vue'
import TopMenuSection from '@/components/TopMenuSection.vue'
import BottomPanel from '@/components/bottomPanel/BottomPanel.vue'
import AppBuilder from '@/components/builder/AppBuilder.vue'
import ExtensionSlot from '@/components/common/ExtensionSlot.vue'
import DomWidgets from '@/components/graph/DomWidgets.vue'
import GraphCanvasMenu from '@/components/graph/GraphCanvasMenu.vue'
@@ -184,7 +181,7 @@ import { useExecutionErrorStore } from '@/stores/executionErrorStore'
import { useNodeDefStore } from '@/stores/nodeDefStore'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import { useSearchBoxStore } from '@/stores/workspace/searchBoxStore'
import { useAppModeStore } from '@/stores/appModeStore'
import { useAppMode } from '@/composables/useAppMode'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { isNativeWindow } from '@/utils/envUtil'
import { forEachNode } from '@/utils/graphTraversalUtil'
@@ -205,7 +202,7 @@ const nodeSearchboxPopoverRef = shallowRef<InstanceType<
const settingStore = useSettingStore()
const nodeDefStore = useNodeDefStore()
const workspaceStore = useWorkspaceStore()
const appModeStore = useAppModeStore()
const { mode, isBuilderMode } = useAppMode()
const canvasStore = useCanvasStore()
const workflowStore = useWorkflowStore()
const executionStore = useExecutionStore()

View File

@@ -3,7 +3,7 @@
ref="containerRef"
:class="
cn(
'comfy-vue-side-bar-container group/sidebar-tab flex h-full flex-col',
'comfy-vue-side-bar-container group/sidebar-tab flex h-full flex-col w-full',
props.class
)
"

View File

@@ -10,7 +10,7 @@
@click="handleClick"
>
<i
v-if="workflowOption.workflow.activeState?.extra?.linearMode"
v-if="workflowOption.workflow.initialMode === 'app'"
class="icon-[lucide--panels-top-left] bg-primary-background"
/>
<span

View File

@@ -25,15 +25,19 @@ whenever(feedbackRef, () => {
:href="`https://form.typeform.com/to/${dataTfWidget}`"
target="_blank"
variant="inverted"
class="rounded-full size-12"
class="flex h-10 items-center justify-center gap-2.5 px-3 py-2"
v-bind="$attrs"
>
<i class="icon-[lucide--circle-question-mark] size-6" />
<i class="icon-[lucide--circle-help] size-4" />
</Button>
<Popover v-else>
<template #button>
<Button variant="inverted" class="rounded-full size-12" v-bind="$attrs">
<i class="icon-[lucide--circle-question-mark] size-6" />
<Button
variant="inverted"
class="flex h-10 items-center justify-center gap-2.5 px-3 py-2"
v-bind="$attrs"
>
<i class="icon-[lucide--circle-help] size-4" />
</Button>
</template>
<div ref="feedbackRef" data-tf-auto-resize :data-tf-widget />