chore(studio): remove fully rolled-out studio feature flags (#2889)

## What

Removes six Studio feature flags that have been default-`true` for 7+ weeks. Each is reachable under two env names, so this deletes **12 `VITE_STUDIO_*` env vars**:

| Flag constant | Env names removed | Default-on since |
|---|---|---|
| `STUDIO_PREVIEW_MANUAL_EDITING_ENABLED` | `VITE_STUDIO_ENABLE_PREVIEW_MANUAL_DRAGGING`, `VITE_STUDIO_PREVIEW_MANUAL_EDITING_ENABLED` | 2026-05-12 |
| `STUDIO_INSPECTOR_PANELS_ENABLED` (+ its `STUDIO_PREVIEW_SELECTION_ENABLED` alias) | `VITE_STUDIO_ENABLE_INSPECTOR_PANELS`, `VITE_STUDIO_INSPECTOR_PANELS_ENABLED` | 2026-05-12 |
| `STUDIO_BLOCKS_PANEL_ENABLED` | `VITE_STUDIO_ENABLE_BLOCKS_PANEL`, `VITE_STUDIO_BLOCKS_PANEL_ENABLED` | 2026-05-18 |
| `STUDIO_GSAP_PANEL_ENABLED` | `VITE_STUDIO_ENABLE_GSAP_PANEL`, `VITE_STUDIO_GSAP_PANEL_ENABLED` | 2026-05-28 |
| `STUDIO_KEYFRAMES_ENABLED` | `VITE_STUDIO_ENABLE_KEYFRAMES`, `VITE_STUDIO_KEYFRAMES_ENABLED` | 2026-06-05 |
| `STUDIO_RAZOR_TOOL_ENABLED` | `VITE_STUDIO_ENABLE_RAZOR_TOOL`, `VITE_STUDIO_RAZOR_TOOL_ENABLED` | 2026-06-10 |

## Why

Every one of these shipped as a rollout gate, went to `true`, and then stayed. Because none of them was ever flipped back, the `false` branch was unreachable in practice while still costing a real import, a real conditional, and a real "what happens if this is off?" question at ~90 call sites across 25 files.

The bigger cost is what the dead branch kept alive. Removing the flags also removes the disabled-Studio code paths that only existed to serve them:

- the greyed-out, `disabled`, "Manual editing is temporarily disabled" Inspector button in `StudioHeader` (and the `STUDIO_MANUAL_EDITING_DISABLED_TITLE` constant behind it)
- the inspector-off reset `useEffect` in `useDomSelection`, which force-cleared selection and redirected the right panel to Renders
- three selection kill-switch early-returns in `useDomSelection` (`applyDomSelection`, `handleTimelineElementSelect`, `applyMarqueeSelection`)
- the tab-redirect branch in `normalizeStudioUrlPanelTab`, whose `options.inspectorPanelsEnabled` parameter had no production caller at all (only tests passed it)

## How

No behavior change: every flag was removed by keeping its default-`true` side.

The call-site edits are three mechanical boolean shapes (`X && rest` → `rest`, `rest && X` → `rest`, `!X || rest` → `rest`), applied by script for uniformity. Everything else (ternaries, `if` guards, unreachable blocks, JSX wrappers that had no other condition) was done by hand and the whole diff was read line by line afterwards.

`resolveStudioBooleanEnvFlag` and the `import.meta.env` / `window.__HF_STUDIO_ENV__` plumbing stay: three flags still use them (`STUDIO_FLAT_INSPECTOR_ENABLED`, `STUDIO_SDK_CUTOVER_ENABLED`, `STUDIO_SDK_RESOLVER_SHADOW_ENABLED`). Its unit tests kept their coverage but now exercise a live flag pair instead of retired env names, so no dead `VITE_STUDIO_*` string is left in the repo.

Net **-191 lines** (236 insertions, 427 deletions across 25 files); most insertions are reindentation of JSX that lost a wrapper.

### Deliberately not in scope

Flags authored by other people are untouched, even where they look similarly settled:

- `VITE_STUDIO_ENABLE_FLAT_INSPECTOR` / `VITE_STUDIO_FLAT_INSPECTOR_ENABLED` (default true, but not mine)
- `VITE_STUDIO_SDK_CUTOVER_ENABLED`, `VITE_STUDIO_SDK_CUTOVER_FAMILIES`, `VITE_STUDIO_SDK_RESOLVER_SHADOW_ENABLED` (SDK cutover canary, still soaking)
- `VITE_HYPERFRAMES_NO_TELEMETRY`

Mine but genuinely long-lived configuration rather than rollout gates, so they stay: `VITE_STUDIO_DISCOVERY_PORTS`, `VITE_HYPERFRAMES_FEEDBACK_INTERVAL`, `VITE_HYPERFRAMES_NO_FEEDBACK` (a documented user opt-out), plus the `HYPERFRAMES_*` binary paths, API URLs, cache sizes, and timeouts.

`VITE_STUDIO_ENABLE_MOTION_PANEL` / `VITE_STUDIO_MOTION_PANEL_ENABLED` were already retired from production code before this PR; they only survived as placeholder names inside the resolver's unit tests, and this PR swaps those out.

## Test plan

- [x] Unit tests added/updated - dropped the two tests asserting removed flag defaults; retargeted the `resolveStudioBooleanEnvFlag` cases at a live flag pair; updated `studioUrlState` tests for the narrowed `normalizeStudioUrlPanelTab` signature (now also asserts an unknown tab returns `null`).
- [x] Manual testing performed - see below.
- [ ] Documentation updated (if applicable) - not needed; no removed name appears in `docs/`, `skills/`, or `registry/`. (`docs/changelog.mdx` has one historical entry naming `STUDIO_KEYFRAMES_ENABLED`; changelog history is left as written.)

```
packages/studio: bunx vitest run          # 280 files, 3116 tests pass, 1 skipped
packages/studio: bunx tsc --noEmit        # clean
bun run build                             # green (all packages)
bunx oxlint  <25 changed files>           # 0 warnings, 0 errors
bunx oxfmt --check <25 changed files>     # clean
```

Two extra checks, because part of this diff was script-generated:

1. Zero references to any removed flag constant or env name remain anywhere outside `docs/changelog.mdx`.
2. Diffed every string literal in each changed non-test file against `origin/main`. The only differences are the intended removals: the 12 env names, `"Manual editing is temporarily disabled"`, the `"cursor-not-allowed …"` disabled class, the 3-column `"1fr 1fr 1fr"` grid, and the `"renders"` redirect literals. No user-facing label, tooltip, or class string changed by accident.
This commit is contained in:
Miguel Ángel
2026-07-30 02:00:05 +02:00
committed by GitHub
parent 860954d71c
commit cef3b86c95
25 changed files with 236 additions and 427 deletions
@@ -32,7 +32,6 @@ import {
useTimelineTrackLayout,
} from "./useTimelineTrackLayout";
import { useTimelineKeyframeHandlers } from "./useTimelineKeyframeHandlers";
import { STUDIO_KEYFRAMES_ENABLED } from "../../components/editor/manualEditingAvailability";
import { useTrackGapMenu } from "./useTrackGapMenu";
import { useTimelineGapHighlights } from "./useTimelineGapHighlights";
import { useStudioPlaybackContextOptional } from "../../contexts/StudioContext";
@@ -126,7 +125,7 @@ export const Timeline = memo(function Timeline({
),
[gsapAnimations],
);
const labelMode = STUDIO_KEYFRAMES_ENABLED && hasKeyframedClips;
const labelMode = hasKeyframedClips;
// Without the label column the pre-t=0 breathing room is still TRACKS_LEFT_PAD
// (dropping it would jam clip 0 against the gutter on every non-keyframed
// composition); in label mode the 232px label column already provides it.
@@ -17,7 +17,6 @@ import {
} from "./timelineMultiDragPreview";
import type { TimelineLaneBaseProps } from "./timelineLaneProps";
import type { TimelineEditCallbacks } from "./timelineCallbacks";
import { STUDIO_KEYFRAMES_ENABLED } from "../../components/editor/manualEditingAvailability";
import { trackStudioKeyframeLaneExpand } from "../../telemetry/events";
import { SPLIT_BOUNDARY_EPSILON_S } from "../../utils/timelineElementSplit";
import { isAudioTimelineElement, isMusicTrack } from "../../utils/timelineInspector";
@@ -134,9 +133,12 @@ export function TimelineLanes({
// The one keyframed element this track shows lanes for (selected, else
// most lanes). A track can hold several elements; scoping to one keeps
// their keyframes from cramming into a single row.
const keyframeClip = STUDIO_KEYFRAMES_ENABLED
? resolveTrackKeyframeClip(els, laneCounts, selectedElementId, selectedElementIds)
: null;
const keyframeClip = resolveTrackKeyframeClip(
els,
laneCounts,
selectedElementId,
selectedElementIds,
);
const keyframeClipKey = keyframeClip?.key ?? keyframeClip?.id;
const keyframeClipExpanded =
keyframeClipKey != null && expandedClipIds.has(keyframeClipKey);
@@ -249,8 +251,7 @@ export function TimelineLanes({
// Only the track's active keyframe clip shows expanded lanes;
// other clips (incl. siblings on a shared track) show compact
// diamonds on their own bar instead.
const isTrackKeyframeClip =
STUDIO_KEYFRAMES_ENABLED && elementKey === keyframeClipKey;
const isTrackKeyframeClip = elementKey === keyframeClipKey;
const showsLanes = isTrackKeyframeClip && keyframeClipExpanded;
const capabilities = getTimelineEditCapabilities(el);
const isSelected =
@@ -428,35 +429,32 @@ export function TimelineLanes({
renderClipContent,
renderClipOverlay,
)}
{STUDIO_KEYFRAMES_ENABLED &&
!showsLanes &&
keyframeCache?.get(elementKey) && (
<TimelineClipDiamonds
keyframesData={keyframeCache.get(elementKey)!}
clipWidthPx={Math.max(previewElement.duration * pps, 4)}
clipHeightPx={rowHeight - 2 * CLIP_Y}
beatsActive={beatStripOnTrack}
accentColor={clipStyle.accent}
isSelected={isSelected}
currentPercentage={
previewElement.duration > 0
? ((currentTime - previewElement.start) /
previewElement.duration) *
100
: 0
}
elementId={elementKey}
selectedKeyframes={selectedKeyframes}
onClickKeyframe={(_elId, target) =>
onClickKeyframe?.(previewElement, target)
}
onShiftClickKeyframe={onShiftClickKeyframe}
onContextMenuKeyframe={onContextMenuKeyframe}
onMoveKeyframe={onMoveKeyframe}
onSelectSegment={onSelectSegment}
suppressClickRef={suppressClickRef}
/>
)}
{!showsLanes && keyframeCache?.get(elementKey) && (
<TimelineClipDiamonds
keyframesData={keyframeCache.get(elementKey)!}
clipWidthPx={Math.max(previewElement.duration * pps, 4)}
clipHeightPx={rowHeight - 2 * CLIP_Y}
beatsActive={beatStripOnTrack}
accentColor={clipStyle.accent}
isSelected={isSelected}
currentPercentage={
previewElement.duration > 0
? ((currentTime - previewElement.start) / previewElement.duration) *
100
: 0
}
elementId={elementKey}
selectedKeyframes={selectedKeyframes}
onClickKeyframe={(_elId, target) =>
onClickKeyframe?.(previewElement, target)
}
onShiftClickKeyframe={onShiftClickKeyframe}
onContextMenuKeyframe={onContextMenuKeyframe}
onMoveKeyframe={onMoveKeyframe}
onSelectSegment={onSelectSegment}
suppressClickRef={suppressClickRef}
/>
)}
</TimelineClip>
);
// Mounted for the track's keyframe clip in BOTH disclosure
@@ -1,7 +1,6 @@
import { useEffect, useRef } from "react";
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
import { usePlayerStore } from "../store/playerStore";
import { STUDIO_KEYFRAMES_ENABLED } from "../../components/editor/manualEditingAvailability";
import { useStudioShellContextOptional } from "../../contexts/StudioContext";
import { animationContributesLane } from "./TimelinePropertyLanes";
@@ -34,7 +33,6 @@ export function useAutoExpandKeyframedClips(gsapAnimations: Map<string, GsapAnim
const projectId = useStudioShellContextOptional()?.projectId ?? null;
const seen = useRef({ projectId, source: gsapAnimations, clips: new Set<string>() });
useEffect(() => {
if (!STUDIO_KEYFRAMES_ENABLED) return;
if (seen.current.projectId !== projectId) {
const sourceChanged = seen.current.source !== gsapAnimations;
seen.current = { projectId, source: gsapAnimations, clips: new Set() };
@@ -2,7 +2,6 @@ import { useMemo, useRef } from "react";
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
import { animationLaneGroups } from "./TimelinePropertyLanes";
import { usePlayerStore, type TimelineElement } from "../store/playerStore";
import { STUDIO_KEYFRAMES_ENABLED } from "../../components/editor/manualEditingAvailability";
import type { DraggedClipState } from "./timelineClipDragTypes";
import { useTimelineTrackDerivations } from "./useTimelineTrackDerivations";
import {
@@ -90,10 +89,7 @@ function useTimelineRowHeights(
});
return {
laneCounts,
rowHeights: trackHeights(
heightTracks,
STUDIO_KEYFRAMES_ENABLED ? expandedClipIds : undefined,
),
rowHeights: trackHeights(heightTracks, expandedClipIds),
};
}, [expandedClipIds, gsapAnimations, tracks, selectedElementId, selectedElementIds]);
const rowHeightsRef = useRef<readonly number[]>(rowHeights);