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

@@ -3,7 +3,7 @@ import { defineStore } from 'pinia'
import { computed, markRaw, ref, shallowRef } from 'vue'
import type { Raw } from 'vue'
import { useAppModeStore } from '@/stores/appModeStore'
import { useAppMode } from '@/composables/useAppMode'
import type { Point, Positionable } from '@/lib/litegraph/src/interfaces'
import type {
@@ -44,10 +44,11 @@ export const useCanvasStore = defineStore('canvas', () => {
// Reactive scale percentage that syncs with app.canvas.ds.scale
const appScalePercentage = ref(100)
const { isAppMode, setMode } = useAppMode()
const linearMode = computed({
get: () => useAppModeStore().isAppMode,
get: () => isAppMode.value,
set: (val: boolean) => {
useAppModeStore().setMode(val ? 'app' : 'graph')
setMode(val ? 'app' : 'graph')
}
})

View File

@@ -1,15 +1,18 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { useAppMode } from '@/composables/useAppMode'
import { useAppModeStore } from '@/stores/appModeStore'
import Button from '@/components/ui/button/Button.vue'
import { storeToRefs } from 'pinia'
const { t } = useI18n()
const appModeStore = useAppModeStore()
const { setMode } = useAppMode()
const { hasOutputs } = storeToRefs(useAppModeStore())
</script>
<template>
<div
v-if="appModeStore.hasOutputs"
v-if="hasOutputs"
role="article"
data-testid="arrange-preview"
class="flex flex-col items-center justify-center h-full w-3/4 gap-6 p-8 mx-auto"
@@ -47,11 +50,7 @@ const appModeStore = useAppModeStore()
<p class="mt-0 p-0">{{ t('linearMode.arrange.outputExamples') }}</p>
</div>
<div class="flex flex-row gap-2">
<Button
variant="primary"
size="lg"
@click="appModeStore.setMode('builder:select')"
>
<Button variant="primary" size="lg" @click="setMode('builder:select')">
{{ t('linearMode.arrange.switchToSelectButton') }}
</Button>
</div>

View File

@@ -26,6 +26,7 @@ import { useExecutionErrorStore } from '@/stores/executionErrorStore'
import { useQueueSettingsStore } from '@/stores/queueStore'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import { cn } from '@/utils/tailwindUtil'
import { useAppMode } from '@/composables/useAppMode'
import { useAppModeStore } from '@/stores/appModeStore'
const { t } = useI18n()
const commandStore = useCommandStore()
@@ -35,7 +36,9 @@ const { batchCount } = storeToRefs(useQueueSettingsStore())
const settingStore = useSettingStore()
const { isActiveSubscription } = useBillingContext()
const workflowStore = useWorkflowStore()
const { isBuilderMode } = useAppMode()
const appModeStore = useAppModeStore()
const { hasOutputs } = storeToRefs(appModeStore)
const props = defineProps<{
toastTo?: string | HTMLElement
@@ -168,7 +171,7 @@ defineExpose({ runButtonClick })
</script>
<template>
<div
v-if="!appModeStore.isBuilderMode && appModeStore.hasOutputs"
v-if="!isBuilderMode && hasOutputs"
class="flex flex-col min-w-80 md:h-full"
>
<section
@@ -299,7 +302,7 @@ defineExpose({ runButtonClick })
<Button
v-else
variant="primary"
class="w-full mt-4"
class="w-full mt-4 text-sm"
size="lg"
@click="runButtonClick"
>
@@ -315,14 +318,18 @@ defineExpose({ runButtonClick })
:to="toastTo"
>
<div
class="bg-base-foreground text-base-background rounded-sm flex h-8 p-1 pr-2 gap-2 items-center"
class="bg-secondary-background text-base-foreground rounded-lg flex h-8 p-1 pr-2 gap-2 items-center"
>
<i
v-if="jobFinishedQueue"
class="icon-[lucide--check] size-5 bg-success-background"
class="icon-[lucide--check] size-5 text-muted-foreground"
/>
<i v-else class="icon-[lucide--loader-circle] size-4 animate-spin" />
<span v-text="t('queue.jobAddedToQueue')" />
<span
v-text="
jobFinishedQueue ? t('queue.jobAddedToQueue') : t('queue.jobQueueing')
"
/>
</div>
</Teleport>
</template>

View File

@@ -27,13 +27,13 @@ import { useQueueStore } from '@/stores/queueStore'
import type { ResultItemImpl } from '@/stores/queueStore'
import { collectAllNodes } from '@/utils/graphTraversalUtil'
import { executeWidgetsCallback } from '@/utils/litegraphUtil'
import { useAppModeStore } from '@/stores/appModeStore'
import { useAppMode } from '@/composables/useAppMode'
const { t } = useI18n()
const commandStore = useCommandStore()
const executionStore = useExecutionStore()
const mediaActions = useMediaAssetActions()
const queueStore = useQueueStore()
const appModeStore = useAppModeStore()
const { mode: appModeValue } = useAppMode()
const { runButtonClick } = defineProps<{
runButtonClick?: (e: Event) => void
mobile?: boolean
@@ -165,7 +165,7 @@ async function rerun(e: Event) {
:model-url="selectedOutput!.url"
/>
<LatentPreview v-else-if="queueStore.runningTasks.length > 0" />
<LinearArrange v-else-if="appModeStore.mode === 'builder:arrange'" />
<LinearArrange v-else-if="appModeValue === 'builder:arrange'" />
<LinearWelcome v-else />
<OutputHistory @update-selection="handleSelection" />
<OutputHistory class="not-md:mx-40" @update-selection="handleSelection" />
</template>

View File

@@ -1,10 +1,13 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { useAppMode } from '@/composables/useAppMode'
import { useAppModeStore } from '@/stores/appModeStore'
import Button from '@/components/ui/button/Button.vue'
import { storeToRefs } from 'pinia'
const { t } = useI18n()
const appModeStore = useAppModeStore()
const { setMode } = useAppMode()
const { hasOutputs } = storeToRefs(useAppModeStore())
</script>
<template>
@@ -24,7 +27,7 @@ const appModeStore = useAppModeStore()
<p class="mt-0">{{ t('linearMode.welcome.controls') }}</p>
<p class="mt-0">{{ t('linearMode.welcome.sharing') }}</p>
</div>
<div v-if="appModeStore.hasOutputs" class="flex flex-row gap-2 text-[14px]">
<div v-if="hasOutputs" class="flex flex-row gap-2 text-[14px]">
<p class="mt-0 text-base-foreground">
<i18n-t keypath="linearMode.welcome.getStarted" tag="span">
<template #runButton>
@@ -38,18 +41,10 @@ const appModeStore = useAppModeStore()
</p>
</div>
<div v-else class="flex flex-row gap-2">
<Button
variant="textonly"
size="lg"
@click="appModeStore.setMode('graph')"
>
<Button variant="textonly" size="lg" @click="setMode('graph')">
{{ t('linearMode.welcome.backToWorkflow') }}
</Button>
<Button
variant="primary"
size="lg"
@click="appModeStore.setMode('builder:select')"
>
<Button variant="primary" size="lg" @click="setMode('builder:select')">
<i class="icon-[lucide--hammer]" />
{{ t('linearMode.welcome.buildApp') }}
<div

View File

@@ -12,6 +12,7 @@ import {
import { CanvasPointer } from '@/lib/litegraph/src/CanvasPointer'
import { getOutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema'
import OutputHistoryActiveQueueItem from '@/renderer/extensions/linearMode/OutputHistoryActiveQueueItem.vue'
import OutputHistoryItem from '@/renderer/extensions/linearMode/OutputHistoryItem.vue'
import { useLinearOutputStore } from '@/renderer/extensions/linearMode/linearOutputStore'
import type {
@@ -275,7 +276,7 @@ useEventListener(document.body, 'keydown', (e: KeyboardEvent) => {
orientation="horizontal"
selection-behavior="replace"
by="id"
class="min-w-0"
class="min-w-0 px-4 pb-4"
@update:model-value="onSelectionChange"
>
<ListboxContent as-child>
@@ -287,23 +288,7 @@ useEventListener(document.body, 'keydown', (e: KeyboardEvent) => {
>
<div class="flex items-center gap-0.5 mx-auto w-fit">
<div v-if="queueCount > 0" class="shrink-0 flex items-center gap-0.5">
<div
class="shrink-0 p-1 border-2 border-transparent relative"
data-testid="linear-job"
>
<div
class="size-10 rounded-sm bg-secondary-background flex items-center justify-center"
>
<i
class="icon-[lucide--loader-circle] size-4 animate-spin text-muted-foreground"
/>
</div>
<div
v-if="queueCount > 1"
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"
/>
</div>
<OutputHistoryActiveQueueItem :queue-count="queueCount" />
<div
v-if="hasActiveContent || visibleHistory.length > 0"
class="border-l border-border-default h-12 shrink-0 mx-4"

View File

@@ -0,0 +1,58 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import Popover from '@/components/ui/Popover.vue'
import Button from '@/components/ui/button/Button.vue'
import { useCommandStore } from '@/stores/commandStore'
const { queueCount } = defineProps<{
queueCount: number
}>()
const { t } = useI18n()
const commandStore = useCommandStore()
function clearQueue(close: () => void) {
void commandStore.execute('Comfy.ClearPendingTasks')
close()
}
</script>
<template>
<div
class="shrink-0 p-1 border-2 border-transparent relative"
data-testid="linear-job"
>
<Popover side="top" :show-arrow="false" @focus-outside.prevent>
<template #button>
<Button
v-tooltip.top="t('linearMode.queue.clickToClear')"
:aria-label="t('linearMode.queue.clickToClear')"
variant="textonly"
size="unset"
class="size-10 rounded-sm bg-secondary-background flex items-center justify-center"
>
<i
class="icon-[lucide--loader-circle] size-4 animate-spin text-muted-foreground"
/>
</Button>
</template>
<template #default="{ close }">
<Button
:disabled="queueCount === 0"
variant="textonly"
class="text-destructive-background px-4 text-sm"
@click="clearQueue(close)"
>
<i class="icon-[lucide--list-x]" />
{{ t('linearMode.queue.clear') }}
</Button>
</template>
</Popover>
<div
v-if="queueCount > 1"
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"
/>
</div>
</template>

View File

@@ -14,11 +14,9 @@ const { apiTarget } = vi.hoisted(() => ({
apiTarget: new EventTarget()
}))
vi.mock('@/stores/appModeStore', () => ({
useAppModeStore: () => ({
get isAppMode() {
return isAppModeRef.value
}
vi.mock('@/composables/useAppMode', () => ({
useAppMode: () => ({
isAppMode: isAppModeRef
})
}))

View File

@@ -5,12 +5,12 @@ import { flattenNodeOutput } from '@/renderer/extensions/linearMode/flattenNodeO
import type { InProgressItem } from '@/renderer/extensions/linearMode/linearModeTypes'
import type { ExecutedWsMessage } from '@/schemas/apiSchema'
import { api } from '@/scripts/api'
import { useAppModeStore } from '@/stores/appModeStore'
import { useAppMode } from '@/composables/useAppMode'
import { useExecutionStore } from '@/stores/executionStore'
import { useJobPreviewStore } from '@/stores/jobPreviewStore'
export const useLinearOutputStore = defineStore('linearOutput', () => {
const appModeStore = useAppModeStore()
const { isAppMode } = useAppMode()
const executionStore = useExecutionStore()
const jobPreviewStore = useJobPreviewStore()
@@ -220,7 +220,7 @@ export const useLinearOutputStore = defineStore('linearOutput', () => {
watch(
() => executionStore.activeJobId,
(jobId, oldJobId) => {
if (!appModeStore.isAppMode) return
if (!isAppMode.value) return
if (oldJobId && oldJobId !== jobId) {
onJobComplete(oldJobId)
}
@@ -233,7 +233,7 @@ export const useLinearOutputStore = defineStore('linearOutput', () => {
watch(
() => jobPreviewStore.previewsByPromptId,
(previews) => {
if (!appModeStore.isAppMode) return
if (!isAppMode.value) return
const jobId = executionStore.activeJobId
if (!jobId) return
const url = previews[jobId]
@@ -243,7 +243,7 @@ export const useLinearOutputStore = defineStore('linearOutput', () => {
)
watch(
() => appModeStore.isAppMode,
isAppMode,
(active, wasActive) => {
if (active) {
api.addEventListener('executed', handleExecuted)