Skip to content
2 changes: 1 addition & 1 deletion src/components/topbar/CurrentUserButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ const showWorkspaceSkeleton = computed(
() => isCloud && initState.value === 'loading'
)
const showWorkspaceIcon = computed(
() => isCloud && initState.value === 'ready' && !isInPersonalWorkspace.value
() => initState.value === 'ready' && !isInPersonalWorkspace.value
)

const workspaceName = computed(() => {
Expand Down
3 changes: 2 additions & 1 deletion src/components/topbar/CurrentUserPopoverLegacy.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { createTestingPinia } from '@pinia/testing'
import { render, screen } from '@testing-library/vue'
import userEvent from '@testing-library/user-event'
import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'
Expand Down Expand Up @@ -175,7 +176,7 @@ describe('CurrentUserPopoverLegacy', () => {

render(CurrentUserPopoverLegacy, {
global: {
plugins: [i18n],
plugins: [i18n, createTestingPinia({ createSpy: vi.fn })],
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
stubs: {
Divider: true
}
Expand Down
43 changes: 42 additions & 1 deletion src/components/topbar/CurrentUserPopoverLegacy.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,35 @@
</span>
</div>

<!-- Workspace Selector -->
<div v-if="showWorkspaceSwitcher" class="relative">
<div
v-tooltip="{ value: workspaceName, showDelay: 300 }"
class="flex cursor-pointer items-center justify-between rounded-lg px-4 py-2 hover:bg-secondary-background-hover"
data-testid="workspace-switcher-trigger"
@click="isWorkspaceSwitcherOpen = !isWorkspaceSwitcherOpen"
>
<div class="flex w-0 flex-1 items-center gap-2">
<WorkspaceProfilePic
class="size-6 shrink-0 text-xs"
:workspace-name="workspaceName"
/>
<span class="truncate text-sm text-base-foreground">
{{ workspaceName }}
</span>
</div>
<i class="pi pi-chevron-down shrink-0 text-sm text-muted-foreground" />
</div>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

<div
v-if="isWorkspaceSwitcherOpen"
class="absolute top-0 right-full z-10 mr-4 rounded-lg border border-border-default bg-base-background shadow-[1px_1px_8px_0_rgba(0,0,0,0.4)]"
data-testid="workspace-switcher-panel"
>
<WorkspaceSwitcherPopover @select="isWorkspaceSwitcherOpen = false" />
</div>
</div>

<!-- Credits Section -->
<div
v-if="canAccessSubscriptionFeatures"
Expand Down Expand Up @@ -160,9 +189,10 @@

<script setup lang="ts">
import { cn } from '@comfyorg/tailwind-utils'
import { storeToRefs } from 'pinia'
import Divider from 'primevue/divider'
import Skeleton from 'primevue/skeleton'
import { computed, onMounted } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'

import { formatCreditsFromCents } from '@/base/credits/comfyCredits'
Expand All @@ -176,7 +206,10 @@ import { useSubscriptionDialog } from '@/platform/cloud/subscription/composables
import { isCloud } from '@/platform/distribution/types'
import { useTelemetry } from '@/platform/telemetry'
import { useSettingsDialog } from '@/platform/settings/composables/useSettingsDialog'
import WorkspaceProfilePic from '@/platform/workspace/components/WorkspaceProfilePic.vue'
import WorkspaceSwitcherPopover from '@/platform/workspace/components/WorkspaceSwitcherPopover.vue'
import { useWorkspaceTierLabel } from '@/platform/workspace/composables/useWorkspaceTierLabel'
import { useTeamWorkspaceStore } from '@/platform/workspace/stores/teamWorkspaceStore'
import { useDialogService } from '@/services/dialogService'

const emit = defineEmits<{
Expand All @@ -203,6 +236,14 @@ const { formatTierName } = useWorkspaceTierLabel()
const subscriptionDialog = useSubscriptionDialog()
const { locale, t } = useI18n()

const { initState, workspaces, workspaceName } = storeToRefs(
useTeamWorkspaceStore()
)
const isWorkspaceSwitcherOpen = ref(false)
const showWorkspaceSwitcher = computed(
() => initState.value === 'ready' && workspaces.value.length > 0
)

const subscriptionTierName = computed(() =>
formatTierName(tier.value, subscription.value?.duration === 'ANNUAL')
)
Expand Down
45 changes: 43 additions & 2 deletions src/platform/workspace/auth/WorkspaceAuthGate.vue
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,14 @@
import { captureException } from '@sentry/vue'
import { until } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { nextTick, onMounted, onUnmounted, ref, useTemplateRef } from 'vue'
import {
nextTick,
onMounted,
onUnmounted,
ref,
useTemplateRef,
watch
} from 'vue'

import Button from '@/components/ui/button/Button.vue'
import { useAuthActions } from '@/composables/auth/useAuthActions'
Expand Down Expand Up @@ -94,7 +101,10 @@ function cancelInitialization(): void {
}

async function initialize(): Promise<void> {
if (!isCloud) return
if (!isCloud) {
void initializeWorkspacesInBackground()
return
}

cancelInitialization()
const generation = initializationGeneration
Expand Down Expand Up @@ -230,11 +240,42 @@ async function initializeWorkspaceMode(): Promise<void> {
}
}

// On non-cloud distributions the gate never blocks rendering; workspace
// context (credit-wallet selection) is hydrated in the background so the
// switcher can appear once ready, and the app works untouched if it fails.
async function initializeWorkspacesInBackground(): Promise<void> {
const { isInitialized, currentUser } = storeToRefs(useAuthStore())
try {
if (!isInitialized.value) {
await until(isInitialized).toBe(true, {
timeout: FIREBASE_INIT_TIMEOUT_MS,
throwOnTimeout: true
})
}
if (!currentUser.value) return
await initializeWorkspaceMode()
} catch (error) {
console.warn(
'[WorkspaceAuthGate] Background workspace initialization failed:',
error
)
}
}

// Initialize on mount. This gate should be placed on the authenticated layout
// (LayoutDefault) so it mounts fresh after login and unmounts on logout.
// The router guard ensures only authenticated users reach this layout.
onMounted(() => {
void initialize()
})

// Non-cloud has no router-driven remount after login, so re-run the
// background hydration when a user signs in mid-session.
if (!isCloud) {
const { currentUser } = storeToRefs(useAuthStore())
watch(currentUser, (user) => {
if (user) void initializeWorkspacesInBackground()
})
}
onUnmounted(cancelInitialization)
</script>
14 changes: 14 additions & 0 deletions src/platform/workspace/components/WorkspaceSwitcherPopover.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ vi.mock('@/composables/billing/useBillingContext', () => ({
useBillingContext: () => ({ subscription: billingMocks.subscription })
}))

const distributionMocks = vi.hoisted(() => ({ isCloud: true }))

vi.mock('@/platform/distribution/types', () => distributionMocks)

const LONG_WORKSPACE_NAME =
'Quantum Renaissance Collective for Hyperdimensional Latent Diffusion Research and Experimental Workflow Engineering'

Expand Down Expand Up @@ -101,6 +105,7 @@ function renderComponent(
describe('WorkspaceSwitcherPopover', () => {
beforeEach(() => {
billingMocks.subscription.value = null
distributionMocks.isCloud = true
})

it.for([
Expand Down Expand Up @@ -239,4 +244,13 @@ describe('WorkspaceSwitcherPopover', () => {
const createWorkspaceButton = screen.getByText('Create a team workspace')
expect(list).not.toContainElement(createWorkspaceButton)
})

it('hides the create-workspace footer on non-cloud distributions', () => {
distributionMocks.isCloud = false

renderComponent()

expect(screen.queryByText('Create a team workspace')).toBeNull()
expect(screen.queryByText(/You can only own 10 workspaces/)).toBeNull()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
</div>

<!-- Create workspace button -->
<div class="shrink-0 border-t border-border-default p-2">
<div v-if="isCloud" class="shrink-0 border-t border-border-default p-2">
<div
:class="
cn(
Expand Down Expand Up @@ -113,6 +113,7 @@ import { useI18n } from 'vue-i18n'

import WorkspaceProfilePic from '@/platform/workspace/components/WorkspaceProfilePic.vue'
import { useBillingContext } from '@/composables/billing/useBillingContext'
import { isCloud } from '@/platform/distribution/types'
import { useWorkspaceSwitch } from '@/platform/workspace/composables/useWorkspaceSwitch'
import { useWorkspaceTierLabel } from '@/platform/workspace/composables/useWorkspaceTierLabel'
import type {
Expand Down
21 changes: 21 additions & 0 deletions vite.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,25 @@ const DEV_SERVER_COMFYUI_URL =
const cloudProxyConfig =
DISTRIBUTION === 'cloud' ? { secure: false, changeOrigin: true } : {}

// Workspace/auth routes only exist on the cloud gateway. On non-cloud dev
// against a local ComfyUI backend, set WORKSPACE_DEV_PROXY (e.g.
// https://cloud.comfy.org) to route them there so the workspace switcher can
// be exercised; everything else keeps hitting DEV_SERVER_COMFYUI_URL.
const WORKSPACE_DEV_PROXY = process.env.WORKSPACE_DEV_PROXY
const workspaceDevProxyConfig = WORKSPACE_DEV_PROXY

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Expectation: the repository either loads this value with loadEnv
# or documents a shell-exported configuration.
rg -n --hidden --glob '!.git' 'WORKSPACE_DEV_PROXY|loadEnv|dotenv' .

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 1888


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- vite.config.mts ---'
sed -n '1,35p;155,190p;280,300p' vite.config.mts

printf '%s\n' '--- environment files and documentation references ---'
find . -maxdepth 3 -type f \( -name '.env*' -o -name 'README*' -o -name '*example*' \) -print
rg -n --hidden --glob '!.git' 'WORKSPACE_DEV_PROXY|\.env\.local|dotenvConfig|loadEnv' \
  README* docs .github package.json vite.config.mts apps 2>/dev/null || true

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 4622


Load WORKSPACE_DEV_PROXY from .env.local or document the supported source. dotenvConfig() loads .env, but not .env.local.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@vite.config.mts` around lines 168 - 169, Update the WORKSPACE_DEV_PROXY
configuration flow so the value is loaded from .env.local as well as .env, or
explicitly document that only the current environment source is supported. Apply
the change around dotenvConfig() and the
WORKSPACE_DEV_PROXY/workspaceDevProxyConfig symbols.

Source: MCP tools

? Object.fromEntries(
[
'/api/workspaces',
'/api/workspace',
'/api/auth/token',
'/api/auth/session'
].map((route) => [
route,
{ target: WORKSPACE_DEV_PROXY, secure: false, changeOrigin: true }
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
])
)
: {}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

function handleGcsRedirect(
proxyRes: IncomingMessage,
_req: IncomingMessage,
Expand Down Expand Up @@ -267,6 +286,8 @@ export default defineConfig({
}
: {}),

...workspaceDevProxyConfig,

'/api': {
target: DEV_SERVER_COMFYUI_URL,
...cloudProxyConfig,
Expand Down
Loading