From 56422abd37f3f5982de059719318ee4a38c21743 Mon Sep 17 00:00:00 2001 From: JonnyBurger Date: Sat, 11 Jul 2026 13:25:56 +0200 Subject: [PATCH] `@remotion/studio`: Reload Studio when config changes --- packages/browser-studio/src/BrowserStudio.tsx | 8 ++ packages/bundler/src/bundle.ts | 9 ++ .../buffer-state-delay-in-milliseconds.ts | 4 + packages/cli/src/config/entry-point.ts | 4 + packages/cli/src/config/ffmpeg-override.ts | 8 +- packages/cli/src/config/index.ts | 129 ++++++++++++++++-- packages/cli/src/config/metadata.ts | 4 + packages/cli/src/config/output-location.ts | 4 + packages/cli/src/config/override-webpack.ts | 4 + packages/cli/src/config/preview-server.ts | 5 + packages/cli/src/config/still-frame.ts | 4 + packages/cli/src/get-config-file-name.ts | 34 ++++- packages/cli/src/studio.ts | 52 +++++++ packages/cli/src/test/config-reset.test.ts | 46 +++++++ packages/renderer/src/options/option.ts | 1 + .../src/options/override-duration.tsx | 3 + .../renderer/src/options/override-fps.tsx | 3 + .../renderer/src/options/override-height.tsx | 3 + .../renderer/src/options/override-width.tsx | 3 + packages/studio-server/src/index.ts | 2 + .../studio-server/src/max-timeline-tracks.ts | 4 + .../src/preview-server/start-server.ts | 3 + packages/studio-server/src/routes.ts | 7 + packages/studio-server/src/start-studio.ts | 4 + .../studio-shared/src/event-source-event.ts | 5 +- packages/studio-shared/src/index.ts | 1 + packages/studio-shared/src/render-defaults.ts | 2 + packages/studio-shared/src/studio-html.ts | 6 + .../src/studio-runtime-config.ts | 8 ++ packages/studio/src/components/Editor.tsx | 8 +- .../src/components/GlobalKeybindings.tsx | 3 +- .../components/KeyboardShortcutsExplainer.tsx | 3 +- packages/studio/src/components/Modals.tsx | 3 +- .../components/NewComposition/MenuContent.tsx | 6 +- .../components/Timeline/MaxTimelineTracks.tsx | 8 +- .../Timeline/TimelineSequenceItem.tsx | 3 +- packages/studio/src/helpers/client-id.tsx | 3 +- .../src/helpers/interactivity-enabled.ts | 7 +- .../src/helpers/show-browser-rendering.ts | 7 +- .../src/helpers/studio-runtime-config.ts | 44 ++++++ packages/studio/src/helpers/use-keybinding.ts | 7 +- .../studio/src/helpers/use-menu-structure.tsx | 3 +- 42 files changed, 425 insertions(+), 50 deletions(-) create mode 100644 packages/cli/src/test/config-reset.test.ts create mode 100644 packages/studio-shared/src/studio-runtime-config.ts create mode 100644 packages/studio/src/helpers/studio-runtime-config.ts diff --git a/packages/browser-studio/src/BrowserStudio.tsx b/packages/browser-studio/src/BrowserStudio.tsx index 68565cbb6ed..a520e98f548 100644 --- a/packages/browser-studio/src/BrowserStudio.tsx +++ b/packages/browser-studio/src/BrowserStudio.tsx @@ -144,6 +144,14 @@ export const BrowserStudio: React.FC = ({ renderQueue: [], sampleRate: null, staticHash: '', + studioRuntimeConfig: { + askAIEnabled: false, + bufferStateDelayInMilliseconds: null, + experimentalClientSideRenderingEnabled: false, + interactivityEnabled: true, + keyboardShortcutsEnabled: true, + maxTimelineTracks: null, + }, studioServerCommand: null, title: 'Remotion Studio', }); diff --git a/packages/bundler/src/bundle.ts b/packages/bundler/src/bundle.ts index 7ac5a7d48c1..4278dbe620b 100644 --- a/packages/bundler/src/bundle.ts +++ b/packages/bundler/src/bundle.ts @@ -422,6 +422,15 @@ export const internalBundle = async ( mode: 'bundle', audioLatencyHint: actualArgs.audioLatencyHint ?? 'playback', sampleRate: actualArgs.renderDefaults?.sampleRate ?? 48000, + studioRuntimeConfig: { + askAIEnabled: actualArgs.askAIEnabled, + bufferStateDelayInMilliseconds: actualArgs.bufferStateDelayInMilliseconds, + experimentalClientSideRenderingEnabled: + actualArgs.experimentalClientSideRenderingEnabled, + interactivityEnabled: actualArgs.interactivityEnabled, + keyboardShortcutsEnabled: actualArgs.keyboardShortcutsEnabled, + maxTimelineTracks: actualArgs.maxTimelineTracks, + }, }); fs.writeFileSync(path.join(outDir, 'index.html'), html); diff --git a/packages/cli/src/config/buffer-state-delay-in-milliseconds.ts b/packages/cli/src/config/buffer-state-delay-in-milliseconds.ts index e71a04d8bb1..1a62b7242b0 100644 --- a/packages/cli/src/config/buffer-state-delay-in-milliseconds.ts +++ b/packages/cli/src/config/buffer-state-delay-in-milliseconds.ts @@ -7,3 +7,7 @@ export const getBufferStateDelayInMilliseconds = () => { export const setBufferStateDelayInMilliseconds = (val: number | null) => { value = val; }; + +export const resetBufferStateDelayInMilliseconds = () => { + value = null; +}; diff --git a/packages/cli/src/config/entry-point.ts b/packages/cli/src/config/entry-point.ts index 371b098cc65..748a1e3a65c 100644 --- a/packages/cli/src/config/entry-point.ts +++ b/packages/cli/src/config/entry-point.ts @@ -7,3 +7,7 @@ export const setEntryPoint = (ep: string) => { export const getEntryPoint = () => { return entryPoint; }; + +export const resetEntryPoint = () => { + entryPoint = null; +}; diff --git a/packages/cli/src/config/ffmpeg-override.ts b/packages/cli/src/config/ffmpeg-override.ts index 8bd070f8673..f437290b8f0 100644 --- a/packages/cli/src/config/ffmpeg-override.ts +++ b/packages/cli/src/config/ffmpeg-override.ts @@ -1,6 +1,8 @@ import type {FfmpegOverrideFn} from '@remotion/renderer'; -let ffmpegOverrideFn: FfmpegOverrideFn = ({args}) => args; +const defaultFfmpegOverrideFn: FfmpegOverrideFn = ({args}) => args; + +let ffmpegOverrideFn: FfmpegOverrideFn = defaultFfmpegOverrideFn; export const setFfmpegOverrideFunction = (fn: FfmpegOverrideFn) => { ffmpegOverrideFn = fn; @@ -9,3 +11,7 @@ export const setFfmpegOverrideFunction = (fn: FfmpegOverrideFn) => { export const getFfmpegOverrideFunction = () => { return ffmpegOverrideFn; }; + +export const resetFfmpegOverrideFunction = () => { + ffmpegOverrideFn = defaultFfmpegOverrideFn; +}; diff --git a/packages/cli/src/config/index.ts b/packages/cli/src/config/index.ts index 8299079bc4a..e3b42662925 100644 --- a/packages/cli/src/config/index.ts +++ b/packages/cli/src/config/index.ts @@ -17,33 +17,42 @@ import {StudioServerInternals} from '@remotion/studio-server'; import {getBrowser} from './browser'; import { getBufferStateDelayInMilliseconds, + resetBufferStateDelayInMilliseconds, setBufferStateDelayInMilliseconds, } from './buffer-state-delay-in-milliseconds'; -import {getConcurrency} from './concurrency'; import type {Concurrency} from './concurrency'; -import {getEntryPoint, setEntryPoint} from './entry-point'; +import {getConcurrency} from './concurrency'; +import {getEntryPoint, resetEntryPoint, setEntryPoint} from './entry-point'; import {getDotEnvLocation} from './env-file'; import { getFfmpegOverrideFunction, + resetFfmpegOverrideFunction, setFfmpegOverrideFunction, } from './ffmpeg-override'; import {getShouldOutputImageSequence} from './image-sequence'; -import {getMetadata, setMetadata} from './metadata'; -import {getOutputLocation} from './output-location'; -import {setOutputLocation} from './output-location'; +import {getMetadata, resetMetadata, setMetadata} from './metadata'; +import { + getOutputLocation, + resetOutputLocation, + setOutputLocation, +} from './output-location'; +import type {WebpackOverrideFn} from './override-webpack'; import { defaultOverrideFunction, getWebpackOverrideFn, + overrideWebpackConfig, + resetWebpackOverride, } from './override-webpack'; -import type {WebpackOverrideFn} from './override-webpack'; -import {overrideWebpackConfig} from './override-webpack'; import { getRendererPortFromConfigFile, getRendererPortFromConfigFileAndCliFlag, getStudioPort, + resetPreviewServerPorts, + setPort, + setRendererPort, + setStudioPort, } from './preview-server'; -import {setPort, setRendererPort, setStudioPort} from './preview-server'; -import {getStillFrame, setStillFrame} from './still-frame'; +import {getStillFrame, resetStillFrame, setStillFrame} from './still-frame'; import {getWebpackCaching} from './webpack-caching'; import {getWebpackPolling} from './webpack-poll'; @@ -126,6 +135,107 @@ const { previewSampleRateOption, } = BrowserSafeApis.options; +const resetOption = ( + option: {setConfig: (value: never) => void}, + value: unknown, +) => { + option.setConfig(value as never); +}; + +const resetOptionBypassValidation = (option: {reset: () => void}) => { + option.reset(); +}; + +const resetBrowserSafeConfigOptions = () => { + resetOption(allowHtmlInCanvasOption, false); + resetOption(benchmarkConcurrenciesOption, null); + resetOption(concurrencyOption, null); + resetOption(offthreadVideoCacheSizeInBytesOption, null); + resetOption(x264Option, null); + resetOption(audioBitrateOption, null); + resetOption(videoBitrateOption, null); + resetOption(scaleOption, 1); + resetOption(crfOption, undefined); + resetOption(jpegQualityOption, undefined); + resetOption(enforceAudioOption, false); + resetOption(overwriteOption, true); + resetOption(chromeModeOption, 'headless-shell'); + resetOption(mutedOption, false); + resetOption(videoCodecOption, undefined); + resetOption(colorSpaceOption, null); + resetOption(disallowParallelEncodingOption, false); + resetOption(deleteAfterOption, null); + resetOption(folderExpiryOption, null); + resetOption(enableMultiprocessOnLinuxOption, true); + resetOption(glOption, null); + resetOption(gopSizeOption, null); + resetOption(headlessOption, true); + resetOption(numberOfGifLoopsOption, null); + resetOption(beepOnFinishOption, false); + resetOption(encodingMaxRateOption, null); + resetOption(encodingBufferSizeOption, null); + resetOption(reproOption, false); + resetOption(enableLambdaInsights, false); + resetOption(logLevelOption, 'info'); + resetOption(delayRenderTimeoutInMillisecondsOption, 30000); + resetOption(publicDirOption, null); + resetOption(binariesDirectoryOption, null); + resetOption(preferLosslessOption, false); + resetOption(framesOption, null); + resetOption(forSeamlessAacConcatenationOption, false); + resetOption(audioCodecOption, null); + resetOption(publicPathOption, null); + resetOption(hardwareAccelerationOption, 'disable'); + resetOption(audioLatencyHintOption, null); + resetOption(enableCrossSiteIsolationOption, false); + resetOption(imageSequencePatternOption, null); + resetOption(darkModeOption, false); + resetOption(askAIOption, false); + resetOption(publicLicenseKeyOption, null); + resetOption(experimentalClientSideRenderingOption, false); + resetOption(interactivityOption, true); + resetOption(keyboardShortcutsOption, true); + resetOption(forceNewStudioOption, false); + resetOption(numberOfSharedAudioTagsOption, 0); + resetOption(ipv4Option, false); + resetOption(pixelFormatOption, 'yuv420p'); + resetOption(browserExecutableOption, null); + resetOption(everyNthFrameOption, 1); + resetOption(proResProfileOption, undefined); + resetOption(stillImageFormatOption, null); + resetOption(videoImageFormatOption, null); + resetOption(userAgentOption, null); + resetOption(disableWebSecurityOption, false); + resetOption(ignoreCertificateErrorsOption, false); + resetOptionBypassValidation(overrideHeightOption); + resetOptionBypassValidation(overrideWidthOption); + resetOptionBypassValidation(overrideFpsOption); + resetOptionBypassValidation(overrideDurationOption); + resetOption(rspackOption, false); + resetOption(outDirOption, null); + resetOption(webpackPollOption, null); + resetOption(imageSequenceOption, false); + resetOption(bundleCacheOption, true); + resetOption(envFileOption, null); + resetOption(runsOption, 3); + resetOption(noOpenOption, true); + resetOption(sampleRateOption, 48000); + resetOption(previewSampleRateOption, null); +}; + +const resetConfigOptions = () => { + resetBrowserSafeConfigOptions(); + StudioServerInternals.resetMaxTimelineTracks(); + resetBufferStateDelayInMilliseconds(); + resetEntryPoint(); + resetFfmpegOverrideFunction(); + resetMetadata(); + resetOutputLocation(); + resetWebpackOverride(); + resetPreviewServerPorts(); + resetStillFrame(); +}; + declare global { interface RemotionBundlingOptions { /** @@ -831,4 +941,5 @@ export const ConfigInternals = { getWebpackPolling, getBufferStateDelayInMilliseconds, getOutputCodecOrUndefined: BrowserSafeApis.getOutputCodecOrUndefined, + resetConfigOptions, }; diff --git a/packages/cli/src/config/metadata.ts b/packages/cli/src/config/metadata.ts index 55a273a1d43..958a7542ace 100644 --- a/packages/cli/src/config/metadata.ts +++ b/packages/cli/src/config/metadata.ts @@ -7,3 +7,7 @@ export const setMetadata = (metadata: Record): void => { export const getMetadata = (): Record => { return specifiedMetadata; }; + +export const resetMetadata = (): void => { + specifiedMetadata = undefined as unknown as Record; +}; diff --git a/packages/cli/src/config/output-location.ts b/packages/cli/src/config/output-location.ts index 862aa9f1a62..8ce38487b60 100644 --- a/packages/cli/src/config/output-location.ts +++ b/packages/cli/src/config/output-location.ts @@ -17,3 +17,7 @@ export const setOutputLocation = (newOutputLocation: string) => { }; export const getOutputLocation = () => currentOutputLocation; + +export const resetOutputLocation = () => { + currentOutputLocation = null; +}; diff --git a/packages/cli/src/config/override-webpack.ts b/packages/cli/src/config/override-webpack.ts index 95ba6727e3e..e808b8dc3da 100644 --- a/packages/cli/src/config/override-webpack.ts +++ b/packages/cli/src/config/override-webpack.ts @@ -15,3 +15,7 @@ export const overrideWebpackConfig = (fn: WebpackOverrideFn) => { const prevOverride = overrideFn; overrideFn = async (c) => fn(await prevOverride(c)); }; + +export const resetWebpackOverride = () => { + overrideFn = defaultOverrideFunction; +}; diff --git a/packages/cli/src/config/preview-server.ts b/packages/cli/src/config/preview-server.ts index 6b0c42e00bf..bed3214abce 100644 --- a/packages/cli/src/config/preview-server.ts +++ b/packages/cli/src/config/preview-server.ts @@ -60,3 +60,8 @@ export const getRendererPortFromConfigFileAndCliFlag = (): number | null => { portOption.getValue({commandLine: parsedCli}).value ?? rendererPort ?? null ); }; + +export const resetPreviewServerPorts = () => { + studioPort = undefined; + rendererPort = undefined; +}; diff --git a/packages/cli/src/config/still-frame.ts b/packages/cli/src/config/still-frame.ts index b035d374102..9d07fd79ef0 100644 --- a/packages/cli/src/config/still-frame.ts +++ b/packages/cli/src/config/still-frame.ts @@ -12,3 +12,7 @@ export const setStillFrame = (frame: number) => { }; export const getStillFrame = () => stillFrame; + +export const resetStillFrame = () => { + stillFrame = 0; +}; diff --git a/packages/cli/src/get-config-file-name.ts b/packages/cli/src/get-config-file-name.ts index 84278972ae7..2f396342367 100644 --- a/packages/cli/src/get-config-file-name.ts +++ b/packages/cli/src/get-config-file-name.ts @@ -9,8 +9,13 @@ const {configOption} = BrowserSafeApis.options; const defaultConfigFileJavascript = 'remotion.config.js'; const defaultConfigFileTypescript = 'remotion.config.ts'; +let loadedConfigFile: string | null = null; -export const loadConfig = (remotionRoot: string): Promise => { +export const getLoadedConfigFile = () => loadedConfigFile; + +export const loadConfig = async ( + remotionRoot: string, +): Promise => { const configFile = configOption.getValue({commandLine: parsedCli}).value; if (configFile) { const fullPath = path.resolve(process.cwd(), configFile); @@ -22,20 +27,37 @@ export const loadConfig = (remotionRoot: string): Promise => { process.exit(1); } - return loadConfigFile(remotionRoot, configFile, fullPath.endsWith('.js')); + loadedConfigFile = await loadConfigFile( + remotionRoot, + configFile, + fullPath.endsWith('.js'), + ); + return loadedConfigFile; } if (remotionRoot === null) { - return Promise.resolve(null); + loadedConfigFile = null; + return null; } if (existsSync(path.resolve(remotionRoot, defaultConfigFileTypescript))) { - return loadConfigFile(remotionRoot, defaultConfigFileTypescript, false); + loadedConfigFile = await loadConfigFile( + remotionRoot, + defaultConfigFileTypescript, + false, + ); + return loadedConfigFile; } if (existsSync(path.resolve(remotionRoot, defaultConfigFileJavascript))) { - return loadConfigFile(remotionRoot, defaultConfigFileJavascript, true); + loadedConfigFile = await loadConfigFile( + remotionRoot, + defaultConfigFileJavascript, + true, + ); + return loadedConfigFile; } - return Promise.resolve(null); + loadedConfigFile = null; + return null; }; diff --git a/packages/cli/src/studio.ts b/packages/cli/src/studio.ts index 3d602a46679..4c736d4aad1 100644 --- a/packages/cli/src/studio.ts +++ b/packages/cli/src/studio.ts @@ -4,6 +4,7 @@ import {StudioServerInternals} from '@remotion/studio-server'; import {ConfigInternals} from './config'; import {convertEntryPointToServeUrl} from './convert-entry-point-to-serve-url'; import {findEntryPoint} from './entry-point'; +import {getLoadedConfigFile, loadConfig} from './get-config-file-name'; import {getEnvironmentVariables} from './get-env'; import {getGitSource} from './get-github-repository'; import {getInputProps} from './get-input-props'; @@ -82,6 +83,37 @@ export const studioCommand = async ( StudioServerInternals.createFileWatcherRegistry(), ); + const configFile = getLoadedConfigFile(); + if (configFile) { + let isReloadingConfig = false; + StudioServerInternals.installFileWatcher({ + file: configFile, + existenceOnly: false, + onChange: async () => { + if (isReloadingConfig) { + return; + } + + isReloadingConfig = true; + try { + ConfigInternals.resetConfigOptions(); + await loadConfig(remotionRoot); + Log.info( + {indent: false, logLevel}, + 'Config file changed. Reloading Studio...', + ); + StudioServerInternals.waitForLiveEventsListener().then((listener) => { + listener.sendEventToClient({ + type: 'config-file-changed', + }); + }); + } finally { + isReloadingConfig = false; + } + }, + }); + } + let inputProps = getInputProps((newProps) => { StudioServerInternals.waitForLiveEventsListener().then((listener) => { inputProps = newProps; @@ -155,6 +187,25 @@ export const studioCommand = async ( ); } + const getStudioRuntimeConfig = () => ({ + maxTimelineTracks: ConfigInternals.getMaxTimelineTracks(), + askAIEnabled: askAIOption.getValue({ + commandLine: parsedCli, + }).value, + interactivityEnabled: interactivityOption.getValue({ + commandLine: parsedCli, + }).value, + keyboardShortcutsEnabled: keyboardShortcutsOption.getValue({ + commandLine: parsedCli, + }).value, + bufferStateDelayInMilliseconds: + ConfigInternals.getBufferStateDelayInMilliseconds(), + experimentalClientSideRenderingEnabled: + experimentalClientSideRenderingOption.getValue({ + commandLine: parsedCli, + }).value, + }); + const result = await StudioServerInternals.startStudio({ previewEntry: require.resolve('@remotion/studio/previewEntry'), browserArgs: parsedCli['browser-args'], @@ -198,6 +249,7 @@ export const studioCommand = async ( interactivityEnabled, forceNew: forceNewStudioOption.getValue({commandLine: parsedCli}).value, rspack: useRspack, + getStudioRuntimeConfig, }); if (result.type === 'already-running') { diff --git a/packages/cli/src/test/config-reset.test.ts b/packages/cli/src/test/config-reset.test.ts new file mode 100644 index 00000000000..115cefaf28d --- /dev/null +++ b/packages/cli/src/test/config-reset.test.ts @@ -0,0 +1,46 @@ +import {expect, test} from 'bun:test'; +import {BrowserSafeApis} from '@remotion/renderer/client'; +import {StudioServerInternals} from '@remotion/studio-server'; +import {DEFAULT_TIMELINE_TRACKS} from '@remotion/studio-shared'; +import {Config, ConfigInternals} from '../config'; + +test('reset config options restores defaults before reloading config', async () => { + ConfigInternals.resetConfigOptions(); + + Config.setStudioPort(4321); + Config.setMaxTimelineTracks(123); + Config.setChromiumOpenGlRenderer('angle'); + Config.setBufferStateDelayInMilliseconds(200); + Config.overrideWebpackConfig((config) => ({ + ...config, + name: 'first', + })); + Config.overrideWebpackConfig((config) => ({ + ...config, + devtool: 'source-map', + })); + + expect(ConfigInternals.getStudioPort()).toBe(4321); + expect(StudioServerInternals.getMaxTimelineTracks()).toBe(123); + expect( + BrowserSafeApis.options.glOption.getValue({commandLine: {}}).value, + ).toBe('angle'); + expect(ConfigInternals.getBufferStateDelayInMilliseconds()).toBe(200); + expect( + await ConfigInternals.getWebpackOverrideFn()({mode: 'development'}), + ).toEqual({mode: 'development', name: 'first', devtool: 'source-map'}); + + ConfigInternals.resetConfigOptions(); + + expect(ConfigInternals.getStudioPort()).toBeUndefined(); + expect(StudioServerInternals.getMaxTimelineTracks()).toBe( + DEFAULT_TIMELINE_TRACKS, + ); + expect( + BrowserSafeApis.options.glOption.getValue({commandLine: {}}).value, + ).toBeNull(); + expect(ConfigInternals.getBufferStateDelayInMilliseconds()).toBeNull(); + expect( + await ConfigInternals.getWebpackOverrideFn()({mode: 'development'}), + ).toEqual({mode: 'development'}); +}); diff --git a/packages/renderer/src/options/option.ts b/packages/renderer/src/options/option.ts index 39689653b44..969275004eb 100644 --- a/packages/renderer/src/options/option.ts +++ b/packages/renderer/src/options/option.ts @@ -16,6 +16,7 @@ export type RemotionOption = { source: string; }; setConfig: (value: Type) => void; + reset?: () => void; id: string; }; diff --git a/packages/renderer/src/options/override-duration.tsx b/packages/renderer/src/options/override-duration.tsx index a5ab0311395..84ba4aaf064 100644 --- a/packages/renderer/src/options/override-duration.tsx +++ b/packages/renderer/src/options/override-duration.tsx @@ -45,5 +45,8 @@ export const overrideDurationOption = { }); currentDuration = duration; }, + reset: () => { + currentDuration = null; + }, id: cliFlag, } satisfies AnyRemotionOption; diff --git a/packages/renderer/src/options/override-fps.tsx b/packages/renderer/src/options/override-fps.tsx index 927125f0b35..8a531f86b43 100644 --- a/packages/renderer/src/options/override-fps.tsx +++ b/packages/renderer/src/options/override-fps.tsx @@ -39,5 +39,8 @@ export const overrideFpsOption = { validateFps(fps, 'in Config.overrideFps()', false); currentFps = fps; }, + reset: () => { + currentFps = null; + }, id: cliFlag, } satisfies AnyRemotionOption; diff --git a/packages/renderer/src/options/override-height.tsx b/packages/renderer/src/options/override-height.tsx index b49be4230b4..4f276918a01 100644 --- a/packages/renderer/src/options/override-height.tsx +++ b/packages/renderer/src/options/override-height.tsx @@ -39,5 +39,8 @@ export const overrideHeightOption = { validateDimension(height, 'height', 'in Config.overrideHeight()'); currentHeight = height; }, + reset: () => { + currentHeight = null; + }, id: cliFlag, } satisfies AnyRemotionOption; diff --git a/packages/renderer/src/options/override-width.tsx b/packages/renderer/src/options/override-width.tsx index d48d91a7dcb..6db5dbdcf9d 100644 --- a/packages/renderer/src/options/override-width.tsx +++ b/packages/renderer/src/options/override-width.tsx @@ -39,5 +39,8 @@ export const overrideWidthOption = { validateDimension(width, 'width', 'in Config.overrideWidth()'); currentWidth = width; }, + reset: () => { + currentWidth = null; + }, id: cliFlag, } satisfies AnyRemotionOption; diff --git a/packages/studio-server/src/index.ts b/packages/studio-server/src/index.ts index ce20b57e65a..d9d85d71952 100644 --- a/packages/studio-server/src/index.ts +++ b/packages/studio-server/src/index.ts @@ -49,6 +49,7 @@ import { import {getInstallCommand} from './helpers/install-command'; import { getMaxTimelineTracks, + resetMaxTimelineTracks, setMaxTimelineTracks, } from './max-timeline-tracks'; import { @@ -67,6 +68,7 @@ export const StudioServerInternals = { getPackageManager, getMaxTimelineTracks, setMaxTimelineTracks, + resetMaxTimelineTracks, getLatestRemotionVersion, installFileWatcher, writeFileAndNotifyFileWatchers, diff --git a/packages/studio-server/src/max-timeline-tracks.ts b/packages/studio-server/src/max-timeline-tracks.ts index 767aa014c97..a1613d541cb 100644 --- a/packages/studio-server/src/max-timeline-tracks.ts +++ b/packages/studio-server/src/max-timeline-tracks.ts @@ -33,3 +33,7 @@ export const setMaxTimelineTracks = (maxTracks: number) => { export const getMaxTimelineTracks = () => { return maxTimelineTracks; }; + +export const resetMaxTimelineTracks = () => { + maxTimelineTracks = DEFAULT_TIMELINE_TRACKS; +}; diff --git a/packages/studio-server/src/preview-server/start-server.ts b/packages/studio-server/src/preview-server/start-server.ts index fb6c317e199..50e2a748ed6 100644 --- a/packages/studio-server/src/preview-server/start-server.ts +++ b/packages/studio-server/src/preview-server/start-server.ts @@ -12,6 +12,7 @@ import type { GitSource, RenderDefaults, RenderJob, + StudioRuntimeConfig, } from '@remotion/studio-shared'; import {detectRemotionServer} from '../detect-remotion-server'; import {handleRoutes} from '../routes'; @@ -69,6 +70,7 @@ export const startServer = async (options: { interactivityEnabled: boolean; forceNew: boolean; rspack: boolean; + getStudioRuntimeConfig: () => StudioRuntimeConfig; }): Promise => { const desiredPort = options?.port ?? @@ -177,6 +179,7 @@ export const startServer = async (options: { audioLatencyHint: options.audioLatencyHint, previewSampleRate: options.previewSampleRate, enableCrossSiteIsolation: options.enableCrossSiteIsolation, + getStudioRuntimeConfig: options.getStudioRuntimeConfig, }); }) .catch((err) => { diff --git a/packages/studio-server/src/routes.ts b/packages/studio-server/src/routes.ts index 3457f976c4c..5829ad041f8 100644 --- a/packages/studio-server/src/routes.ts +++ b/packages/studio-server/src/routes.ts @@ -12,6 +12,7 @@ import type { GitSource, RenderDefaults, RenderJob, + StudioRuntimeConfig, } from '@remotion/studio-shared'; import {getProjectName, parseElementDragData} from '@remotion/studio-shared'; import {getCompletedClientRenders} from './client-render-queue'; @@ -284,6 +285,7 @@ const handleFallback = async ({ gitSource, logLevel, enableCrossSiteIsolation, + getStudioRuntimeConfig, }: { remotionRoot: string; hash: string; @@ -300,6 +302,7 @@ const handleFallback = async ({ gitSource: GitSource | null; logLevel: LogLevel; enableCrossSiteIsolation: boolean; + getStudioRuntimeConfig: () => StudioRuntimeConfig; }) => { const acceptsHtml = (request.headers.accept ?? '').includes('text/html'); if (request.method === 'GET' && acceptsHtml) { @@ -378,6 +381,7 @@ const handleFallback = async ({ mode: 'dev', audioLatencyHint: audioLatencyHint ?? 'playback', sampleRate: previewSampleRate, + studioRuntimeConfig: getStudioRuntimeConfig(), }), ); }; @@ -567,6 +571,7 @@ export const handleRoutes = ({ audioLatencyHint, previewSampleRate, enableCrossSiteIsolation, + getStudioRuntimeConfig, }: { staticHash: string; staticHashPrefix: string; @@ -590,6 +595,7 @@ export const handleRoutes = ({ audioLatencyHint: AudioContextLatencyCategory | null; previewSampleRate: number | null; enableCrossSiteIsolation: boolean; + getStudioRuntimeConfig: () => StudioRuntimeConfig; }): Promise => { const url = new URL(request.url as string, 'http://localhost'); @@ -735,5 +741,6 @@ export const handleRoutes = ({ audioLatencyHint, previewSampleRate, enableCrossSiteIsolation, + getStudioRuntimeConfig, }); }; diff --git a/packages/studio-server/src/start-studio.ts b/packages/studio-server/src/start-studio.ts index f44ff0b1321..8421bb34e79 100644 --- a/packages/studio-server/src/start-studio.ts +++ b/packages/studio-server/src/start-studio.ts @@ -8,6 +8,7 @@ import type { GitSource, RenderDefaults, RenderJob, + StudioRuntimeConfig, } from '@remotion/studio-shared'; import {getFileWatcherRegistry} from './file-watcher'; import {getNetworkAddress} from './get-network-address'; @@ -59,6 +60,7 @@ export const startStudio = async ({ interactivityEnabled, forceNew, rspack, + getStudioRuntimeConfig, }: { browserArgs: string; browserFlag: string; @@ -91,6 +93,7 @@ export const startStudio = async ({ interactivityEnabled: boolean; forceNew: boolean; rspack: boolean; + getStudioRuntimeConfig: () => StudioRuntimeConfig; }): Promise => { try { if (typeof Bun === 'undefined') { @@ -172,6 +175,7 @@ export const startStudio = async ({ interactivityEnabled, forceNew, rspack, + getStudioRuntimeConfig, }); if (result.type === 'already-running') { diff --git a/packages/studio-shared/src/event-source-event.ts b/packages/studio-shared/src/event-source-event.ts index da33df01972..8453003de66 100644 --- a/packages/studio-shared/src/event-source-event.ts +++ b/packages/studio-shared/src/event-source-event.ts @@ -1,7 +1,7 @@ -import type {StaticFile} from 'remotion'; import type { CanUpdateSequencePropsResponse, SequencePropsSubscriptionKey, + StaticFile, } from 'remotion'; import type { CanUpdateDefaultPropsResponse, @@ -25,6 +25,9 @@ export type EventSourceEvent = type: 'new-env-variables'; newEnvVariables: Record; } + | { + type: 'config-file-changed'; + } | { type: 'root-file-changed'; } diff --git a/packages/studio-shared/src/index.ts b/packages/studio-shared/src/index.ts index 703a956fc43..1f2766f0e59 100644 --- a/packages/studio-shared/src/index.ts +++ b/packages/studio-shared/src/index.ts @@ -310,6 +310,7 @@ export { type StudioEntryPointPaths, } from './studio-entry-points'; export {studioHtml, type StudioHtmlOptions} from './studio-html'; +export type {StudioRuntimeConfig} from './studio-runtime-config'; export type {VisualControlChange} from './codemods'; export { diff --git a/packages/studio-shared/src/render-defaults.ts b/packages/studio-shared/src/render-defaults.ts index 1e5aefbd0f5..20330043c34 100644 --- a/packages/studio-shared/src/render-defaults.ts +++ b/packages/studio-shared/src/render-defaults.ts @@ -14,6 +14,7 @@ import type {HardwareAccelerationOption} from '@remotion/renderer/client'; import type {_InternalTypes} from 'remotion'; import type {GitSource} from './git-source'; import type {PackageManager} from './package-manager'; +import type {StudioRuntimeConfig} from './studio-runtime-config'; export type RenderDefaults = { jpegQuality: number; @@ -68,5 +69,6 @@ declare global { remotion_gitSource: GitSource | null; remotion_installedPackages: string[] | null; remotion_packageManager: PackageManager | 'unknown'; + remotion_studioConfig: StudioRuntimeConfig | null; } } diff --git a/packages/studio-shared/src/studio-html.ts b/packages/studio-shared/src/studio-html.ts index a733085dc0b..25e092b7f85 100644 --- a/packages/studio-shared/src/studio-html.ts +++ b/packages/studio-shared/src/studio-html.ts @@ -3,6 +3,7 @@ import {Internals, VERSION} from 'remotion'; import type {GitSource} from './git-source'; import type {PackageManager} from './package-manager'; import type {RenderDefaults} from './render-defaults'; +import type {StudioRuntimeConfig} from './studio-runtime-config'; export type StudioHtmlOptions = { staticHash: string; @@ -30,6 +31,7 @@ export type StudioHtmlOptions = { mode: 'dev' | 'bundle'; bundleScriptUrl?: string; readOnlyStudio?: boolean; + studioRuntimeConfig?: StudioRuntimeConfig; }; export const studioHtml = ({ @@ -58,6 +60,7 @@ export const studioHtml = ({ mode, bundleScriptUrl, readOnlyStudio, + studioRuntimeConfig, }: StudioHtmlOptions) => { const scriptUrl = bundleScriptUrl ?? `${publicPath}bundle.js`; @@ -90,6 +93,9 @@ export const studioHtml = ({ + diff --git a/packages/studio-shared/src/studio-runtime-config.ts b/packages/studio-shared/src/studio-runtime-config.ts new file mode 100644 index 00000000000..ac5612d48e3 --- /dev/null +++ b/packages/studio-shared/src/studio-runtime-config.ts @@ -0,0 +1,8 @@ +export type StudioRuntimeConfig = { + readonly maxTimelineTracks: number | null; + readonly askAIEnabled: boolean; + readonly interactivityEnabled: boolean; + readonly keyboardShortcutsEnabled: boolean; + readonly bufferStateDelayInMilliseconds: number | null; + readonly experimentalClientSideRenderingEnabled: boolean; +}; diff --git a/packages/studio/src/components/Editor.tsx b/packages/studio/src/components/Editor.tsx index c3e716d8e37..4b4daa0cfcf 100644 --- a/packages/studio/src/components/Editor.tsx +++ b/packages/studio/src/components/Editor.tsx @@ -4,6 +4,7 @@ import type {CurrentScaleContextType} from 'remotion'; import {Internals} from 'remotion'; import {BACKGROUND} from '../helpers/colors'; import {noop} from '../helpers/noop'; +import {getStudioBufferStateDelayInMilliseconds} from '../helpers/studio-runtime-config'; import {drawRef} from '../state/canvas-ref'; import {ScaleLockProvider} from '../state/scale-lock'; import {TimelineZoomContext} from '../state/timeline-zoom'; @@ -27,13 +28,8 @@ const background: React.CSSProperties = { position: 'absolute', }; -const DEFAULT_BUFFER_STATE_DELAY_IN_MILLISECONDS = 300; - export const BUFFER_STATE_DELAY_IN_MILLISECONDS = - typeof process.env.BUFFER_STATE_DELAY_IN_MILLISECONDS === 'undefined' || - process.env.BUFFER_STATE_DELAY_IN_MILLISECONDS === null - ? DEFAULT_BUFFER_STATE_DELAY_IN_MILLISECONDS - : Number(process.env.BUFFER_STATE_DELAY_IN_MILLISECONDS); + getStudioBufferStateDelayInMilliseconds(); export const Editor: React.FC<{ readonly Root: React.FC; diff --git a/packages/studio/src/components/GlobalKeybindings.tsx b/packages/studio/src/components/GlobalKeybindings.tsx index 53069e906f5..f06e85b64bc 100644 --- a/packages/studio/src/components/GlobalKeybindings.tsx +++ b/packages/studio/src/components/GlobalKeybindings.tsx @@ -4,6 +4,7 @@ import {Internals} from 'remotion'; import {calculateTimeline} from '../helpers/calculate-timeline'; import {StudioServerConnectionCtx} from '../helpers/client-id'; import {SHOW_BROWSER_RENDERING} from '../helpers/show-browser-rendering'; +import {getStudioAskAIEnabled} from '../helpers/studio-runtime-config'; import {timelineNodePathInfoToKey} from '../helpers/timeline-node-path-key'; import {useKeybinding} from '../helpers/use-keybinding'; import {CheckerboardContext} from '../state/checkerboard'; @@ -175,7 +176,7 @@ export const GlobalKeybindings: React.FC<{ commandCtrlKey: true, preventDefault: true, }); - const cmdIKey = process.env.ASK_AI_ENABLED + const cmdIKey = getStudioAskAIEnabled() ? keybindings.registerKeybinding({ event: 'keydown', key: 'i', diff --git a/packages/studio/src/components/KeyboardShortcutsExplainer.tsx b/packages/studio/src/components/KeyboardShortcutsExplainer.tsx index 0549090c605..6f14b0ee664 100644 --- a/packages/studio/src/components/KeyboardShortcutsExplainer.tsx +++ b/packages/studio/src/components/KeyboardShortcutsExplainer.tsx @@ -7,6 +7,7 @@ import { WHITE, WHITE_ALPHA_10, } from '../helpers/colors'; +import {getStudioAskAIEnabled} from '../helpers/studio-runtime-config'; import {areKeyboardShortcutsDisabled} from '../helpers/use-keybinding'; import {ArrowLeft, ArrowRight, ShiftIcon} from '../icons/keys'; import {Column, Row, Spacing} from './layout'; @@ -401,7 +402,7 @@ export const KeyboardShortcutsExplainer: React.FC = () => {
Delete / reset selection
- {process.env.ASK_AI_ENABLED && ( + {getStudioAskAIEnabled() && ( <>
AI
diff --git a/packages/studio/src/components/Modals.tsx b/packages/studio/src/components/Modals.tsx index daf77008905..a484198c35e 100644 --- a/packages/studio/src/components/Modals.tsx +++ b/packages/studio/src/components/Modals.tsx @@ -1,5 +1,6 @@ import React, {useContext} from 'react'; import {StudioServerConnectionCtx} from '../helpers/client-id'; +import {getStudioAskAIEnabled} from '../helpers/studio-runtime-config'; import {ModalsContext} from '../state/modals'; import {AskAiModal} from './AskAiModal'; import {ConfirmationDialog} from './ConfirmationDialog'; @@ -185,7 +186,7 @@ export const Modals: React.FC<{ {modalContextType && modalContextType.type === 'confirmation-dialog' && ( )} - {process.env.ASK_AI_ENABLED && } + {getStudioAskAIEnabled() && } ); }; diff --git a/packages/studio/src/components/NewComposition/MenuContent.tsx b/packages/studio/src/components/NewComposition/MenuContent.tsx index afd18a4fbdc..78ef30a7d03 100644 --- a/packages/studio/src/components/NewComposition/MenuContent.tsx +++ b/packages/studio/src/components/NewComposition/MenuContent.tsx @@ -2,6 +2,7 @@ import type {PointerEvent, SetStateAction} from 'react'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {BLACK_ALPHA_60} from '../../helpers/colors'; import {useMobileLayout} from '../../helpers/mobile-layout'; +import {getStudioKeyboardShortcutsEnabled} from '../../helpers/studio-runtime-config'; import {useKeybinding} from '../../helpers/use-keybinding'; import {VERTICAL_SCROLLBAR_CLASSNAME} from '../Menu/is-menu-item'; import {MenuDivider} from '../Menu/MenuDivider'; @@ -240,10 +241,7 @@ export const MenuContent: React.FC<{ }, [fixedHeight, isMobileLayout]); useEffect(() => { - if ( - !keybindings.isHighestContext || - !process.env.KEYBOARD_SHORTCUTS_ENABLED - ) { + if (!keybindings.isHighestContext || !getStudioKeyboardShortcutsEnabled()) { return; } diff --git a/packages/studio/src/components/Timeline/MaxTimelineTracks.tsx b/packages/studio/src/components/Timeline/MaxTimelineTracks.tsx index dce3add5a33..bb6dcdaff96 100644 --- a/packages/studio/src/components/Timeline/MaxTimelineTracks.tsx +++ b/packages/studio/src/components/Timeline/MaxTimelineTracks.tsx @@ -1,13 +1,9 @@ -import {DEFAULT_TIMELINE_TRACKS} from '@remotion/studio-shared'; import React from 'react'; import {WHITE_ALPHA_10, WHITE_ALPHA_60} from '../../helpers/colors'; +import {getStudioMaxTimelineTracks} from '../../helpers/studio-runtime-config'; import {TIMELINE_PADDING} from '../../helpers/timeline-layout'; -export const MAX_TIMELINE_TRACKS = - typeof process.env.MAX_TIMELINE_TRACKS === 'undefined' || - process.env.MAX_TIMELINE_TRACKS === null - ? DEFAULT_TIMELINE_TRACKS - : Number(process.env.MAX_TIMELINE_TRACKS); +export const MAX_TIMELINE_TRACKS = getStudioMaxTimelineTracks(); export const MAX_TIMELINE_TRACKS_NOTICE_HEIGHT = 24; diff --git a/packages/studio/src/components/Timeline/TimelineSequenceItem.tsx b/packages/studio/src/components/Timeline/TimelineSequenceItem.tsx index 2b4f843bf26..d7919fed003 100644 --- a/packages/studio/src/components/Timeline/TimelineSequenceItem.tsx +++ b/packages/studio/src/components/Timeline/TimelineSequenceItem.tsx @@ -14,6 +14,7 @@ import { import {formatFileLocation} from '../../helpers/format-file-location'; import type {SequenceNodePathInfo} from '../../helpers/get-timeline-sequence-sort-key'; import {studioInteractivityEnabled} from '../../helpers/interactivity-enabled'; +import {getStudioKeyboardShortcutsEnabled} from '../../helpers/studio-runtime-config'; import { getTimelineLayerHeight, TIMELINE_ITEM_BORDER_BOTTOM, @@ -760,7 +761,7 @@ export const TimelineSequenceItem: React.FC<{ ); React.useEffect(() => { - if (!canRenameSelectedSequence || !process.env.KEYBOARD_SHORTCUTS_ENABLED) { + if (!canRenameSelectedSequence || !getStudioKeyboardShortcutsEnabled()) { setIsRenaming(false); return; } diff --git a/packages/studio/src/helpers/client-id.tsx b/packages/studio/src/helpers/client-id.tsx index 295a72af79b..f2f03791cb5 100644 --- a/packages/studio/src/helpers/client-id.tsx +++ b/packages/studio/src/helpers/client-id.tsx @@ -68,7 +68,8 @@ export const PreviewServerConnection: React.FC<{ const handleEvent = (newEvent: EventSourceEvent) => { if ( newEvent.type === 'new-input-props' || - newEvent.type === 'new-env-variables' + newEvent.type === 'new-env-variables' || + newEvent.type === 'config-file-changed' ) { reloadUrl(); } diff --git a/packages/studio/src/helpers/interactivity-enabled.ts b/packages/studio/src/helpers/interactivity-enabled.ts index 60fda4e4a73..3f77dbed196 100644 --- a/packages/studio/src/helpers/interactivity-enabled.ts +++ b/packages/studio/src/helpers/interactivity-enabled.ts @@ -1,6 +1,3 @@ -const interactivityEnabled = process.env.INTERACTIVITY_ENABLED as unknown; +import {getStudioInteractivityEnabled} from './studio-runtime-config'; -export const studioInteractivityEnabled = - interactivityEnabled === undefined || interactivityEnabled === null - ? true - : interactivityEnabled !== false && interactivityEnabled !== 'false'; +export const studioInteractivityEnabled = getStudioInteractivityEnabled(); diff --git a/packages/studio/src/helpers/show-browser-rendering.ts b/packages/studio/src/helpers/show-browser-rendering.ts index c362bc3651a..61b7f97a4ba 100644 --- a/packages/studio/src/helpers/show-browser-rendering.ts +++ b/packages/studio/src/helpers/show-browser-rendering.ts @@ -1,3 +1,4 @@ -export const SHOW_BROWSER_RENDERING = Boolean( - process.env.EXPERIMENTAL_CLIENT_SIDE_RENDERING_ENABLED, -); +import {getStudioExperimentalClientSideRenderingEnabled} from './studio-runtime-config'; + +export const SHOW_BROWSER_RENDERING = + getStudioExperimentalClientSideRenderingEnabled(); diff --git a/packages/studio/src/helpers/studio-runtime-config.ts b/packages/studio/src/helpers/studio-runtime-config.ts new file mode 100644 index 00000000000..7b47ad8cfca --- /dev/null +++ b/packages/studio/src/helpers/studio-runtime-config.ts @@ -0,0 +1,44 @@ +import type {StudioRuntimeConfig} from '@remotion/studio-shared'; +import {DEFAULT_TIMELINE_TRACKS} from '@remotion/studio-shared'; + +export const DEFAULT_BUFFER_STATE_DELAY_IN_MILLISECONDS = 300; + +const defaultStudioRuntimeConfig: StudioRuntimeConfig = { + askAIEnabled: false, + bufferStateDelayInMilliseconds: null, + experimentalClientSideRenderingEnabled: false, + interactivityEnabled: true, + keyboardShortcutsEnabled: true, + maxTimelineTracks: null, +}; + +const getStudioRuntimeConfig = (): StudioRuntimeConfig => { + return window.remotion_studioConfig ?? defaultStudioRuntimeConfig; +}; + +export const getStudioAskAIEnabled = () => { + return getStudioRuntimeConfig().askAIEnabled; +}; + +export const getStudioInteractivityEnabled = () => { + return getStudioRuntimeConfig().interactivityEnabled; +}; + +export const getStudioKeyboardShortcutsEnabled = () => { + return getStudioRuntimeConfig().keyboardShortcutsEnabled; +}; + +export const getStudioExperimentalClientSideRenderingEnabled = () => { + return getStudioRuntimeConfig().experimentalClientSideRenderingEnabled; +}; + +export const getStudioMaxTimelineTracks = () => { + return getStudioRuntimeConfig().maxTimelineTracks ?? DEFAULT_TIMELINE_TRACKS; +}; + +export const getStudioBufferStateDelayInMilliseconds = () => { + return ( + getStudioRuntimeConfig().bufferStateDelayInMilliseconds ?? + DEFAULT_BUFFER_STATE_DELAY_IN_MILLISECONDS + ); +}; diff --git a/packages/studio/src/helpers/use-keybinding.ts b/packages/studio/src/helpers/use-keybinding.ts index 6dda262e22b..f62a63ac40b 100644 --- a/packages/studio/src/helpers/use-keybinding.ts +++ b/packages/studio/src/helpers/use-keybinding.ts @@ -2,8 +2,9 @@ import {useCallback, useContext, useEffect, useMemo, useState} from 'react'; import type {KeyEventType, RegisteredKeybinding} from '../state/keybindings'; import {KeybindingContext} from '../state/keybindings'; import {useZIndex} from '../state/z-index'; +import {getStudioKeyboardShortcutsEnabled} from './studio-runtime-config'; -if (!process.env.KEYBOARD_SHORTCUTS_ENABLED) { +if (!getStudioKeyboardShortcutsEnabled()) { // eslint-disable-next-line no-console console.warn( 'Keyboard shortcuts disabled either due to: a) --disable-keyboard-shortcuts being passed b) Config.setKeyboardShortcutsEnabled(false) being set or c) a Remotion version mismatch.', @@ -11,7 +12,7 @@ if (!process.env.KEYBOARD_SHORTCUTS_ENABLED) { } export const areKeyboardShortcutsDisabled = () => { - return !process.env.KEYBOARD_SHORTCUTS_ENABLED; + return !getStudioKeyboardShortcutsEnabled(); }; export const useKeybinding = () => { @@ -29,7 +30,7 @@ export const useKeybinding = () => { triggerIfInputFieldFocused: boolean; keepRegisteredWhenNotHighestContext: boolean; }) => { - if (!process.env.KEYBOARD_SHORTCUTS_ENABLED) { + if (!getStudioKeyboardShortcutsEnabled()) { return { unregister: () => undefined, }; diff --git a/packages/studio/src/helpers/use-menu-structure.tsx b/packages/studio/src/helpers/use-menu-structure.tsx index 525ca5a9467..5efcbb7358e 100644 --- a/packages/studio/src/helpers/use-menu-structure.tsx +++ b/packages/studio/src/helpers/use-menu-structure.tsx @@ -36,6 +36,7 @@ import {useMobileLayout} from './mobile-layout'; import {openInEditor, preloadCompositionComponentInfo} from './open-in-editor'; import {pickColor} from './pick-color'; import {SHOW_BROWSER_RENDERING} from './show-browser-rendering'; +import {getStudioAskAIEnabled} from './studio-runtime-config'; import {areKeyboardShortcutsDisabled} from './use-keybinding'; type Structure = Menu[]; @@ -785,7 +786,7 @@ export const useMenuStructure = ( label: 'Tools', leaveLeftPadding: false, items: [ - process.env.ASK_AI_ENABLED + getStudioAskAIEnabled() ? { id: 'ask-ai', value: 'ask-ai',