Compare commits

...

1 Commits

Author SHA1 Message Date
Deep Mehta
75d8548cbc feat(cloud): offer "Open in Comfy Desktop" on cloud web
Shows a slim dismissible banner on cloud.comfy.org (regular browser) that
deep-links into the companion Comfy Desktop app via comfy://open?url=...

Gated to render only when all hold: cloud build (isCloud), not already
embedded in Desktop's cloud webview (window.__comfyDesktop2Remote falsy),
and not previously dismissed (localStorage).
2026-06-11 17:48:59 -07:00
5 changed files with 186 additions and 0 deletions

View File

@@ -3528,6 +3528,12 @@
"replacementInstruction": "Install these nodes to run this workflow, or replace them with installed alternatives. Missing nodes are highlighted in red on the canvas."
}
},
"openInDesktop": {
"message": "Get the full experience in Comfy Desktop.",
"action": "Open in Comfy Desktop",
"getApp": "Get the app",
"dismiss": "Dismiss"
},
"nodeReplacement": {
"quickFixAvailable": "Quick Fix Available",
"installationRequired": "Installation Required",

View File

@@ -0,0 +1,51 @@
<template>
<div
v-if="visible"
class="fixed inset-x-0 top-0 z-1100 flex items-center justify-center gap-3 bg-secondary-background px-4 py-2 text-base-foreground shadow-sm"
role="region"
:aria-label="t('openInDesktop.action')"
>
<i
class="icon-[lucide--monitor] size-4 text-text-secondary"
aria-hidden="true"
/>
<span class="font-inter text-[12px] leading-normal">
{{ t('openInDesktop.message') }}
</span>
<button
type="button"
class="rounded-md bg-primary-background px-3 py-1 font-inter text-[12px] font-medium text-base-foreground transition-colors hover:bg-primary-background-hover"
@click="openInDesktop"
>
{{ t('openInDesktop.action') }}
</button>
<a
:href="DESKTOP_DOWNLOAD_URL"
target="_blank"
rel="noopener noreferrer"
class="font-inter text-[12px] text-text-secondary underline-offset-2 hover:underline"
>
{{ t('openInDesktop.getApp') }}
</a>
<button
type="button"
class="ml-1 flex size-6 items-center justify-center rounded-md text-text-secondary transition-colors hover:bg-secondary-background-hover"
:aria-label="t('openInDesktop.dismiss')"
@click="dismiss"
>
<i class="icon-[lucide--x] size-4" aria-hidden="true" />
</button>
</div>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import {
DESKTOP_DOWNLOAD_URL,
useOpenInDesktopBanner
} from '@/platform/cloud/desktop/useOpenInDesktopBanner'
const { t } = useI18n()
const { visible, openInDesktop, dismiss } = useOpenInDesktopBanner()
</script>

View File

@@ -0,0 +1,80 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
import { useOpenInDesktopBanner } from './useOpenInDesktopBanner'
const mockIsCloud = vi.hoisted(() => ({ value: true }))
vi.mock('@/platform/distribution/types', () => ({
get isCloud() {
return mockIsCloud.value
}
}))
const DISMISSED_STORAGE_KEY = 'comfy.openInDesktop.dismissed'
describe('useOpenInDesktopBanner', () => {
beforeEach(() => {
mockIsCloud.value = true
localStorage.clear()
delete window.__comfyDesktop2Remote
})
afterEach(() => {
vi.restoreAllMocks()
})
it('is visible on cloud when not embedded and not dismissed', () => {
const { visible } = useOpenInDesktopBanner()
expect(visible.value).toBe(true)
})
it('is hidden when not the cloud build', () => {
mockIsCloud.value = false
const { visible } = useOpenInDesktopBanner()
expect(visible.value).toBe(false)
})
it('is hidden when embedded inside Comfy Desktop', () => {
window.__comfyDesktop2Remote = true
const { visible } = useOpenInDesktopBanner()
expect(visible.value).toBe(false)
})
it('is hidden when previously dismissed in localStorage', () => {
localStorage.setItem(DISMISSED_STORAGE_KEY, 'true')
const { visible } = useOpenInDesktopBanner()
expect(visible.value).toBe(false)
})
it('fires the comfy://open deep link with the encoded current url', () => {
const currentUrl = 'https://cloud.comfy.org/workflows/abc?x=1&y=2'
const hrefSetter = vi.fn()
vi.spyOn(window, 'location', 'get').mockReturnValue({
get href() {
return currentUrl
},
set href(value: string) {
hrefSetter(value)
}
} as Location)
const { openInDesktop } = useOpenInDesktopBanner()
openInDesktop()
expect(hrefSetter).toHaveBeenCalledWith(
`comfy://open?url=${encodeURIComponent(currentUrl)}`
)
})
it('persists dismissal and hides after dismiss', async () => {
const { visible, dismiss } = useOpenInDesktopBanner()
expect(visible.value).toBe(true)
dismiss()
await nextTick()
expect(visible.value).toBe(false)
expect(localStorage.getItem(DISMISSED_STORAGE_KEY)).toBe('true')
})
})

View File

@@ -0,0 +1,47 @@
import { useLocalStorage } from '@vueuse/core'
import { computed } from 'vue'
import { isCloud } from '@/platform/distribution/types'
const DISMISSED_STORAGE_KEY = 'comfy.openInDesktop.dismissed'
/** Public download page shown when the desktop app is not installed. */
export const DESKTOP_DOWNLOAD_URL = 'https://www.comfy.org/download'
function buildDeepLink(currentUrl: string): string {
return `comfy://open?url=${encodeURIComponent(currentUrl)}`
}
/**
* Drives the "Open in Comfy Desktop" banner shown on cloud.comfy.org when
* viewed in a regular browser.
*
* The banner is visible only when all three gates pass:
* 1. This is the cloud distribution build (`isCloud`).
* 2. The page is not already embedded inside Comfy Desktop's cloud webview
* (`window.__comfyDesktop2Remote` is falsy).
* 3. The user has not previously dismissed it (persisted in localStorage).
*/
export function useOpenInDesktopBanner() {
const dismissed = useLocalStorage(DISMISSED_STORAGE_KEY, false)
const isEmbeddedInDesktop = computed(() => !!window.__comfyDesktop2Remote)
const visible = computed(
() => isCloud && !isEmbeddedInDesktop.value && !dismissed.value
)
function openInDesktop(): void {
window.location.href = buildDeepLink(window.location.href)
}
function dismiss(): void {
dismissed.value = true
}
return {
visible,
openInDesktop,
dismiss
}
}

View File

@@ -27,6 +27,7 @@
<AssetExportProgressDialog />
<ManagerProgressToast />
<DesktopCloudNotificationController />
<OpenInDesktopBanner />
<UnloadWindowConfirmDialog v-if="!isDesktop" />
<MenuHamburger />
</template>
@@ -65,6 +66,7 @@ import { setActiveLocale } from '@/i18n'
import AssetExportProgressDialog from '@/platform/assets/components/AssetExportProgressDialog.vue'
import ModelImportProgressDialog from '@/platform/assets/components/ModelImportProgressDialog.vue'
import DesktopCloudNotificationController from '@/platform/cloud/notification/components/DesktopCloudNotificationController.vue'
import OpenInDesktopBanner from '@/platform/cloud/desktop/OpenInDesktopBanner.vue'
import { isCloud, isDesktop } from '@/platform/distribution/types'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useTelemetry } from '@/platform/telemetry'