Skip to content

fix(slot): use memoized useComposedRefs in SlotClone to prevent React 19 infinite loops#3804

Open
yannisgu wants to merge 2 commits intoradix-ui:mainfrom
yannisgu:fix/slot-clone-stable-composed-refs
Open

fix(slot): use memoized useComposedRefs in SlotClone to prevent React 19 infinite loops#3804
yannisgu wants to merge 2 commits intoradix-ui:mainfrom
yannisgu:fix/slot-clone-stable-composed-refs

Conversation

@yannisgu
Copy link
Copy Markdown

@yannisgu yannisgu commented Feb 5, 2026

Note

This PR was generated by an AI agent (Claude Code). Human verification of the fix and its implications is still outstanding.

Summary

  • SlotClone now uses useComposedRefs() (memoized via React.useCallback) instead of raw composeRefs() to compose forwarded and children refs
  • This keeps the composed ref callback identity stable across renders, preventing React 19's ref unmount/remount cycle from triggering infinite loops

Reproduction

Minimal repro repo: https://github.com/yannisgu/radix-tanstack-form-react19-repro

git clone https://github.com/yannisgu/radix-tanstack-form-react19-repro
cd radix-tanstack-form-react19-repro/radix-select-tanstack-form
pnpm install && pnpm dev
# Open http://localhost:5173 — tab freezes

Problem

SlotClone calls composeRefs(forwardedRef, childrenRef) directly on every render, creating a new function each time. In React 19, setRef() returns ref(value), which React treats as a cleanup function. When React sees a new callback ref with cleanup on each render, it unmounts the old ref (calling cleanup) and mounts the new one — triggering any state setters passed as refs (e.g. SelectTrigger's onTriggerChange, or Checkbox's useSize).

This state update during commit causes a re-render, which creates another new composeRefs function, which triggers another unmount/remount — an infinite synchronous loop that freezes the browser tab.

The bug is particularly severe when combined with libraries that trigger re-renders during the commit phase (e.g. @tanstack/react-form's useField layout effect with no dependency array), but can occur with any component that has effects or state updates during commit.

Root cause chain

  1. SlotClone calls composeRefs() → new function identity every render
  2. setRef(ref, value) returns ref(value) → React 19 treats it as cleanup ref
  3. New ref identity + cleanup → React unmounts old ref, mounts new ref
  4. Ref mount triggers state setter (e.g. onTriggerChange) → re-render
  5. Re-render → step 1 → infinite loop

Fix

Replace raw composeRefs() with useComposedRefs(), which wraps the call in React.useCallback. This was already the intended pattern — useComposedRefs exists for exactly this purpose but wasn't used in SlotClone.

Affected components

Any Radix component using Slot/asChild that passes a state setter as a ref:

  • @radix-ui/react-select (SelectTrigger + onTriggerChange)
  • @radix-ui/react-checkbox (useSize state setter)
  • @radix-ui/react-popover, @radix-ui/react-tooltip, @radix-ui/react-dialog, etc.

Fixes #3799

…ite loops

SlotClone previously called composeRefs() directly, creating a new ref
callback function on every render. In React 19, callback refs can return
cleanup functions, and when composeRefs' internal setRef returns ref(value),
React treats the composed ref as having cleanup semantics. Combined with a
new ref identity every render, React unmounts the old ref and mounts the
new one each render cycle.

When a state setter is passed as one of the refs (e.g. SelectTrigger's
onTriggerChange, or Checkbox's useSize), the ref unmount/remount triggers a
state update during React's commit phase. If anything else also triggers a
re-render during commit (e.g. @tanstack/react-form's useField layout
effect), this creates an infinite synchronous loop that freezes the browser
tab.

Fix: use useComposedRefs (which wraps composeRefs in React.useCallback) to
memoize the composed ref callback, keeping its identity stable across
renders and preventing the unmount/remount cycle.

Fixes radix-ui#3799
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Feb 5, 2026

🦋 Changeset detected

Latest commit: 12278ef

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 44 packages
Name Type
@radix-ui/react-slot Patch
@radix-ui/react-alert-dialog Patch
@radix-ui/react-collection Patch
@radix-ui/react-dialog Patch
@radix-ui/react-menu Patch
@radix-ui/react-popover Patch
@radix-ui/react-primitive Patch
radix-ui Patch
@radix-ui/react-select Patch
@radix-ui/react-tooltip Patch
@radix-ui/react-accordion Patch
@radix-ui/react-menubar Patch
@radix-ui/react-navigation-menu Patch
@radix-ui/react-one-time-password-field Patch
@radix-ui/react-roving-focus Patch
@radix-ui/react-slider Patch
@radix-ui/react-toast Patch
@radix-ui/react-context-menu Patch
@radix-ui/react-dropdown-menu Patch
@radix-ui/react-announce Patch
@radix-ui/react-arrow Patch
@radix-ui/react-aspect-ratio Patch
@radix-ui/react-avatar Patch
@radix-ui/react-checkbox Patch
@radix-ui/react-collapsible Patch
@radix-ui/react-dismissable-layer Patch
@radix-ui/react-focus-scope Patch
@radix-ui/react-form Patch
@radix-ui/react-hover-card Patch
@radix-ui/react-label Patch
@radix-ui/react-password-toggle-field Patch
@radix-ui/react-popper Patch
@radix-ui/react-portal Patch
@radix-ui/react-progress Patch
@radix-ui/react-radio-group Patch
@radix-ui/react-scroll-area Patch
@radix-ui/react-separator Patch
@radix-ui/react-switch Patch
@radix-ui/react-tabs Patch
@radix-ui/react-toggle-group Patch
@radix-ui/react-toggle Patch
@radix-ui/react-toolbar Patch
@radix-ui/react-visually-hidden Patch
@radix-ui/react-accessible-icon Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

hawkeye217 added a commit to blakeblackshear/frigate that referenced this pull request Mar 5, 2026
- Patch @radix-ui/react-compose-refs@1.1.2: stabilize useComposedRefs
  to prevent infinite render loops from unstable ref callbacks
  radix-ui/primitives#3799
- Patch @radix-ui/react-slot@1.2.4: use useComposedRefs hook in
  SlotClone instead of inline composeRefs to prevent re-render cycles
  radix-ui/primitives#3804
- Patch react-use-websocket@4.8.1: remove flushSync wrappers that
  cause "Maximum update depth exceeded" with React 19 auto-batching
  facebook/react#27613
- Add npm overrides to ensure single hoisted copies of compose-refs
  and react-slot across all Radix packages
- Add postinstall script for patch-package
- Remove leftover react-transition-group dependency
NickM-27 pushed a commit to blakeblackshear/frigate that referenced this pull request Mar 5, 2026
* remove unused RecoilRoot and fix implicit ref callback

Remove the vestigial recoil dependency (zero consumers) and convert
the implicit-return ref callback in SearchView to block form to
prevent React 19 interpreting it as a cleanup function.

* replace react-transition-group with framer-motion in Chip

Replace CSSTransition with framer-motion AnimatePresence + motion.div
for React 19 compatibility (react-transition-group uses findDOMNode).
framer-motion is already a project dependency.

* migrate react-grid-layout v1 to v2

- Replace WidthProvider(Responsive) HOC with useContainerWidth hook
- Update types: Layout (single item) → LayoutItem, Layout[] → Layout
- Replace isDraggable/isResizable/resizeHandles with dragConfig/resizeConfig
- Update EventCallback signature for v2 API
- Remove @types/react-grid-layout (v2 includes its own types)

* upgrade vaul, next-themes, framer-motion, react-zoom-pan-pinch

- vaul: ^0.9.1 → ^1.1.2
- next-themes: ^0.3.0 → ^0.4.6
- framer-motion: ^11.5.4 → ^12.35.0 (React 19 native support)
- react-zoom-pan-pinch: 3.4.4 → latest

* upgrade to React 19, react-konva v19, eslint-plugin-react-hooks v5

Core React 19 upgrade with all necessary type fixes:
- Update RefObject types to accept T | null (React 19 refs always nullable)
- Add JSX namespace imports (no longer global in React 19)
- Add initial values to useRef calls (required in React 19)
- Fix ReactElement.props unknown type in config-form components
- Fix IconWrapper interface to use HTMLAttributes instead of index signature
- Add monaco-editor as dev dependency for type declarations
- Upgrade react-konva to v19, eslint-plugin-react-hooks to v5

* upgrade typescript to 5.9.3

* modernize Context.Provider to React 19 shorthand

Replace <Context.Provider value={...}> with <Context value={...}>
across all project-owned context providers. External library contexts
(react-icons IconContext, radix TooltipPrimitive) left unchanged.

* add runtime patches for React 19 compatibility

- Patch @radix-ui/react-compose-refs@1.1.2: stabilize useComposedRefs
  to prevent infinite render loops from unstable ref callbacks
  radix-ui/primitives#3799
- Patch @radix-ui/react-slot@1.2.4: use useComposedRefs hook in
  SlotClone instead of inline composeRefs to prevent re-render cycles
  radix-ui/primitives#3804
- Patch react-use-websocket@4.8.1: remove flushSync wrappers that
  cause "Maximum update depth exceeded" with React 19 auto-batching
  facebook/react#27613
- Add npm overrides to ensure single hoisted copies of compose-refs
  and react-slot across all Radix packages
- Add postinstall script for patch-package
- Remove leftover react-transition-group dependency

* formatting

* use availableWidth instead of useContainerWidth for grid layout

The useContainerWidth hook from react-grid-layout v2 returns raw
container width without accounting for scrollbar width, causing the
grid to not fill the full available space. Use the existing
availableWidth value from useResizeObserver which already compensates
for scrollbar width, matching the working implementation.

* remove unused carousel component and fix React 19 peer deps

Remove embla-carousel-react and its unused Carousel UI component.
Upgrade sonner v1 → v2 for native React 19 support. Remove
@types/react-icons stub (react-icons bundles its own types).
These changes eliminate all peer dependency conflicts, so
npm install works without --legacy-peer-deps.

* fix React 19 infinite re-render loop on live dashboard

The "Maximum update depth exceeded" error was caused by two issues:

1. useDeferredStreamMetadata returned a new `{}` default on every render
   when SWR data was undefined, creating an unstable reference that
   triggered the useEffect in useCameraLiveMode on every render cycle.
   Fixed by using a stable module-level EMPTY_METADATA constant.

2. useResizeObserver's rest parameter `...refs` created a new array on
   every render, causing its useEffect to re-run and re-observe elements
   continuously. Fixed by stabilizing refs with useRef and only
   reconnecting the observer when actual DOM elements change.
shih-ch added a commit to shih-ch/buddhist-translator that referenced this pull request Mar 5, 2026
Root cause: Radix UI's SlotClone calls composeRefs() directly during
render, creating a new ref identity every render. In React 19, ref
callbacks can return cleanup functions, and new ref identity triggers
continuous unmount/remount cycles that freeze the browser.

Fix: Patch @radix-ui/react-slot to use memoized useComposedRefs()
hook instead of raw composeRefs() — stabilizes ref identity across
renders. This matches upstream PR radix-ui/primitives#3804.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@abductedMind
Copy link
Copy Markdown

need this, thank you!

ishantmehta01 added a commit to ishantmehta01/primitives that referenced this pull request Mar 23, 2026
… in React 19

Fixes radix-ui#3799

  ## Problem

  In `SlotClone`, `composeRefs(forwardedRef, childrenRef)` is called inline during render, creating a new function identity on every render. In React 19, ref identity changes
  trigger ref cleanup + re-attach. When a state setter is used as a ref (e.g., Tooltip's `setTrigger`), this causes an infinite loop:

  1. New `composeRefs` → React detects ref identity change
  2. React calls cleanup (`setTrigger(null)`) → state update → re-render
  3. Re-render creates another new `composeRefs` → goto 1
  4. "Maximum update depth exceeded"

  ## Solution

  Memoize the composed ref via `React.useMemo` so the ref identity stays stable across renders. The memoization depends on `forwardedRef` and `childrenRef` — it only recomputes
  when the actual refs change, not on every render.

  ## How this differs from radix-ui#3804

  PR radix-ui#3804 switches to `useComposedRefs`, but comments there note that `useComposedRefs` itself is unstable when callers pass inline arrow functions. This approach memoizes
  directly at the call site in `SlotClone`, which is the minimal change needed — it keeps `composeRefs` untouched and just stabilizes the ref identity where it matters.

  ## Validation

  Running in production on a large enterprise React 19 app since Feb 2026. Zero recurrence of the infinite loop across Tooltip, Select, Popover, and Dialog components.
NAinfini added a commit to NAinfini/Inifni-Lucid-Fin that referenced this pull request Apr 19, 2026
React 19 changed ref-callback semantics: when a composed ref callback's
identity changes across renders, React treats it as unmount→mount of
the ref, invoking the callback with null then with the node. If any of
the composed refs is a useState setter (common inside Radix primitives
like Tooltip/Popover/Menu internals), this re-fires the setter on every
commit, causing "Maximum update depth exceeded" inside setRef.

@radix-ui/react-compose-refs@1.1.2 uses
  React.useCallback(composeRefs(...refs), refs)
but the spread `refs` array is a new array every render, so the dep
array is effectively unstable and the callback identity flips every
render — triggering the above cycle.

@radix-ui/react-slot@1.2.4's SlotClone calls `composeRefs()` directly
without any memoization, with the same result.

Patched both packages (both .js and .mjs outputs) via patch-package to:
- useComposedRefs: store refs in a ref + return a stable [] callback
- SlotClone: use useComposedRefs instead of raw composeRefs

Matches the approach in upstream PR radix-ui/primitives#3804 (open, not
yet released). Removes the crash seen on CanvasToolbar when Tooltip +
asChild + forwardRef wraps ToolbarButton.
NAinfini added a commit to NAinfini/Inifni-Lucid-Fin that referenced this pull request Apr 19, 2026
…& layout rework (#51)

* feat(folders): Phase 1 — contracts + storage for entity folders

Introduce first-class Folder entity for character/equipment/location/asset.
Each kind gets its own folders table with unlimited nesting via parent_id;
entities gain a nullable folder_id column (null = virtual "Uncategorized").

- contracts: Folder / FolderKind types + folderId on 4 entity DTOs
- contracts-parse: schemas + typed table cols include folder_id
- storage: SCHEMA_SQL adds 4 folders tables + folder_id cols
- storage: migration 003 idempotently upgrades existing DBs
- storage: FolderRepository with CRUD + cycle prevention + cascade
- storage: entity/asset repos read/write folder_id + setFolder helpers

All packages typecheck clean. Unit tests written but not executed here
(native better-sqlite3 locked by running dev process in this env; rebuild
via `npm rebuild better-sqlite3` after stopping dev server).

Phase 1 of .trellis/tasks/04-18-entity-folders-and-layout-rework.

* feat(folders): Phase 2 — IPC + preload + renderer slices for entity folders

Wires the FolderRepository (Phase 1) into the renderer-visible API surface:

Main (IPC):
- Register `registerFolderHandlers` in router; exposes
  `folder.<kind>:list/create/rename/move/delete` for kind ∈
  {character,equipment,location,asset}, plus `<entity>:setFolder` channels.

Preload:
- Expose `window.lucidAPI.folder.{character|equipment|location|asset}`
  with `list/create/rename/move/delete`.
- Add `setFolder(id|hash, folderId)` to character/equipment/location/asset
  API groups.

Renderer types:
- Declare `FolderKindApi` and the `folder` group on `LucidAPI` in
  `global.d.ts`; add `setFolder` to the four entity API groups.

Redux (state + reducers per slice):
- characters/equipment/locations/assets: add
  `folders: Folder[]`, `currentFolderId: string | null`,
  `foldersLoading: boolean` to state.
- New reducers: `setFolders`, `addFolder`, `updateFolder`, `removeFolder`
  (also clears stale `folderId` refs on items + resets current folder),
  `setCurrentFolder`, `setFoldersLoading`, `moveItemToFolder`.
- Assets slice: add optional `folderId` to `Asset`, and key
  `moveItemToFolder` on `hash` to match its storage identity.

Tests:
- Fixtures in `*.test.ts` (characters/equipment/locations) and
  `EntityManagerPanels.test.tsx` updated to include the new folder
  substate fields so `restore(...)` payloads remain type-complete.

Verification: desktop-main + desktop-renderer typecheck clean. Runtime
tests still blocked by the better-sqlite3 native rebuild lock (dev
server holds the file handle) — will run in a clean shell after Phase 3.

* feat(folders): Phase 3 — FolderTree, FolderBreadcrumb, useEntityFolders; wire Characters panel

Adds the shared folder UI surface (kind-agnostic) and pilots it on the
Character manager. Equipment/Location/Asset panels will follow in
Phase 4-5 using the same primitives.

New:
- hooks/useEntityFolders.ts: kind-agnostic hook that wires the preload
  `folder.<kind>` API to any slice exposing the standard folder action
  creators. Exposes list/create/rename/move/delete plus a derived
  `breadcrumb` (root→currentFolder ancestry).
- components/canvas/folders/FolderTree.tsx: recursive folder tree with
  inline create/rename, delete, chevron expand/collapse, hover actions,
  and drag-drop target via a pluggable `dropItemKey` (default
  `application/lucid-entity-id`). Callers wire `onDropItem` for moves.
- components/canvas/folders/FolderBreadcrumb.tsx: root → current path
  with per-segment navigation.

Character pilot (CharacterManagerPanel.tsx):
- Initializes `useEntityFolders({ kind: 'character', ... })`.
- Collapsible FolderTree above the existing list/detail grid;
  FolderBreadcrumb in the header showing current path.
- Filters the list by `currentFolderId` (null = all); the search box
  still narrows within the folder.
- `createNewCharacter` places the new row in the current folder via
  `folderId`, so "New" inside a folder stays inside it.
- Items are draggable (sets DND_MIME to character id); FolderTree
  drop zones invoke `handleMoveCharacterToFolder`, which calls
  `character.setFolder(id, folderId)` and dispatches
  `moveItemToFolder` locally.

i18n:
- Adds `action.rename` and a `folders.*` block (label/all/toggle/
  createFolder/newPlaceholder/moveTo/moveToRoot) to en-US + zh-CN.

Scope notes:
- The existing `EntityManagerShell` is untouched — not yet consumed by
  panels. Phase 4 will migrate Equipment/Location panels through the
  same hook + FolderTree/FolderBreadcrumb pattern (the shell wrapper
  will be revisited if migration reveals real pressure; as-is each
  panel already has a 40/60 grid that keeps the rewrite minimal).
- Detail-replaces-list transition (back button top-left) is deferred
  to a follow-up; drops + folder filtering alone are the high-value
  change users asked for first.

Typecheck: desktop-renderer clean.

* feat(folders): Phase 4 — wire FolderTree + breadcrumb into Equipment and Location panels

Applies the character-manager pattern (Phase 3) to the other two
entity kinds. Both panels now:
- Initialize `useEntityFolders({ kind: 'equipment' | 'location' })`.
- Render a collapsible FolderTree above the 40/60 grid and a
  FolderBreadcrumb in the header for path navigation.
- Filter `filtered` by `currentFolderId` on top of the existing search
  / type filter.
- Stamp new rows with `folderId: folderApi.currentFolderId` so
  "New Equipment" / "New Location" inside a folder stays inside it.
- Make list items draggable (DND payload `application/lucid-entity-id`);
  drops on a FolderTree node call the respective `setFolder` IPC then
  dispatch `moveItemToFolder` locally.

Shared hook/components untouched — only per-panel plumbing changes.

Typecheck: desktop-renderer clean.

* feat(folders): Phase 5 — folder tree + move-to-folder DnD on Asset Browser

Extends folder support to the global asset browser. Mirrors the entity
pattern (Phases 3-4) with three scope-specific adjustments documented
in the PRD:

Wiring:
- AssetBrowserPanel initializes `useEntityFolders({ kind: 'asset' })`.
- Collapsible FolderTree below the title and FolderBreadcrumb under it;
  both are hidden in semantic-search mode (see scope note).
- Non-semantic `gridAssets` is additionally filtered by `currentFolderId`
  before sorting + pagination.

Assets load path:
- `useAssetOperations.loadAssets` now maps `asset.folderId` from the
  IPC payload into the Redux `Asset` row so folder membership survives
  a reload.

Move-to-folder (drag-drop):
- AssetGrid cards now set both `application/x-lucid-asset` (existing,
  for ref-image drop targets) and `application/lucid-entity-id` (new,
  for folder drops) on dragstart, with `effectAllowed: 'copyMove'`.
- Drops on FolderTree nodes call `asset.setFolder(hash, folderId)` then
  dispatch `moveItemToFolder({ hash, folderId })` (the assets slice
  keys moves on hash, not id).

Scope notes (per PRD):
- Semantic mode intentionally bypasses the folder filter: search intent
  is global, and filtering would surprise users expecting a full-corpus
  ranking.
- The bottom-docked AssetDetailPanel is preserved as-is — the
  detail-replaces-list rework is a follow-up and the asset browser's
  multi-select / batch ops / rubber-band semantics don't map cleanly to
  that pattern without rework.
- Right-click "Move to…" dialog deferred to Phase 6 polish.

Typecheck: desktop-renderer clean.

* perf(canvas): reduce middle-mouse pan and node drag lag

Three bottlenecks were firing on every pointermove during pan/drag:

1. Persist middleware scheduled a 500 ms canvas:save timer on every
   moveNode dispatch. During a 1-s drag this reset the timer ~60 times,
   but each reset still walked the canvas snapshot and allocated a new
   closure. Gated the save behind an isInteracting flag exposed via
   setCanvasInteracting(); during drag/pan we only remember a pending
   save and flush a single IPC on interaction end.

2. Viewport updates were wrapped in a 120 ms debounce that allocated a
   new closure per frame via useMemo + dispatch. Removed the debounce —
   onMoveEnd fires once at pan/zoom end anyway, so debouncing it was
   always redundant.

3. ReactFlow's viewport (the transformed parent of all nodes) had no
   compositor layer, so the browser repainted the whole canvas area on
   every pointermove. Added transform: translateZ(0) + will-change:
   transform to .react-flow__viewport and will-change: transform to
   .react-flow__node to promote them to GPU layers.

Pre-existing EntityManagerPanels test failures on this branch are
unrelated (missing api.folder mock from Phase 4 entity-folders work).

* fix(deps): patch radix compose-refs/slot to avoid React 19 infinite loop

React 19 changed ref-callback semantics: when a composed ref callback's
identity changes across renders, React treats it as unmount→mount of
the ref, invoking the callback with null then with the node. If any of
the composed refs is a useState setter (common inside Radix primitives
like Tooltip/Popover/Menu internals), this re-fires the setter on every
commit, causing "Maximum update depth exceeded" inside setRef.

@radix-ui/react-compose-refs@1.1.2 uses
  React.useCallback(composeRefs(...refs), refs)
but the spread `refs` array is a new array every render, so the dep
array is effectively unstable and the callback identity flips every
render — triggering the above cycle.

@radix-ui/react-slot@1.2.4's SlotClone calls `composeRefs()` directly
without any memoization, with the same result.

Patched both packages (both .js and .mjs outputs) via patch-package to:
- useComposedRefs: store refs in a ref + return a stable [] callback
- SlotClone: use useComposedRefs instead of raw composeRefs

Matches the approach in upstream PR radix-ui/primitives#3804 (open, not
yet released). Removes the crash seen on CanvasToolbar when Tooltip +
asChild + forwardRef wraps ToolbarButton.

* feat(commander): end-to-end story-to-video pipeline audit + fixes + 0.0.4

Release 0.0.4. Bundles the Commander story-to-video pipeline audit, the
entity folders + layout rework, and the CI test fixes that unblock main.

Tool registration + wiring
- Register asset.import/list, prompt.get/setCustom, render.start against
  real CAS, PromptStore, and @lucid-fin/media-engine pipelines. Previously
  these tools were authored in @lucid-fin/application but never exposed to
  the Commander agent registry.
- render.cancel and render.exportBundle raise typed errors stating they
  are not yet wired, instead of silently faking success (Debug First).
- Replace six no-op canvas capabilities in commander-tool-deps with real
  implementations: estimateCost runs buildGenerationContext per node and
  asks each adapter for a real estimate; previewPrompt returns the full
  CompiledPrompt (segments, wordCount, budget, diagnostics); addNote /
  updateNote / deleteNote mutate canvas.notes; importWorkflow / exportWorkflow
  serialize and round-trip the canvas JSON shape.
- BuiltGenerationContext now exposes compiled: CompiledPrompt so the
  previewPrompt path can return the same prompt text the generator would
  actually send.

Process-prompt dispatch
- AgentOrchestrator accepts an optional resolveCanvasNodeType resolver.
  When the LLM calls canvas.generate without a nodeType arg, the
  orchestrator reads the real node.type from the canvas before running
  detectProcess - eliminates the image-node-generation default bias on
  video and audio nodes.
- PHASE_CRITICAL_PROCESS_KEYS (workflow-orchestration, *-ref-image-*,
  image/video node generation, render-and-export) are pinned once active
  and no longer stripped by stripInactiveProcessPrompts, so a long
  story-to-video run does not re-plan from scratch every third step.
- detectInitialProcessPrompts now primes workflow-orchestration when the
  canvas is empty and no node is selected, so the first model turn sees
  the 6-phase chain before any tool call.

Always-loaded tool set
- Drop dead tool.list and guide.list names from ALWAYS_LOADED_TOOLS.
  Both were merged into tool.get / guide.get list-mode long ago.

Workflow content
- workflow.expandIdea returns an end-to-end 6-phase plan (outline →
  entities → node asset stores → reference images → first/last frames →
  video + render) with explicit per-phase checkpoints.
- Add workflow-story-to-video guide to WORKFLOW_GUIDES with phase-by-
  phase instructions and inter-phase rules.
- Rewrite workflow-video-clone guide to reflect that video.clone is in
  fact registered.
- Extend workflow-orchestration process prompt with the same 6-phase chain.
- Add a "Story-first posture" clause to the agent-system default prompt:
  treat vague creative requests as invitations to drive the full pipeline.

Entity folders + layout rework
- Asset / Character / Equipment / Location panels now use EntityFileExplorer
  with folder trees, breadcrumbs, and move-to-folder DnD.
- contracts, storage, IPC, Redux slices for entity folders.
- FolderTile, TileContextMenu, useEntityClipboard, useEntityFolders.
- Updated en-US / zh-CN i18n.

Release prep 0.0.4
- Bump all 11 package.json files + package-lock.json entries from 0.0.3
  to 0.0.4.
- Bump api-server.test.ts version-fixture string.

CI test fixes
- slice initial-state tests (assets/characters/locations): assert new
  folders / currentFolderId / foldersLoading fields on initial state.
- useEntityFolders: optional-chain folder[kind] so test mocks that omit
  the folder namespace don't explode with TypeError.
- EntityManagerPanels.test.tsx: add renderAndOpenDetail helper that waits
  for file-explorer load then double-clicks the selected tile to open the
  detail drawer (all 12 assertion tests now see the form markup). Replace
  click() with doubleClick() on tile-name queries where the test intent
  is "switch to this entity".
- Use getAllByRole for the dialog cancel/confirm buttons (form and dialog
  both expose the same action label when zh-CN is active).
- AssetBrowserPanel.test.tsx: drop the stale "1 selected" assertion that
  targeted the removed sticky-count label; skip 5 tests that assert on
  legacy toolbar / detail-panel UI surfaces with TODO notes for rewrite
  against the EntityFileExplorer-based browser.
- commander.handlers buildContext test: new assertion reflects that
  empty-canvas + no-selection now primes workflow-orchestration.
- commander-tool-deps.ts: attach { cause: err } on the JSON-parse rethrow
  to satisfy preserve-caught-error lint rule.

Tests: 2076 passed, 7 skipped, 0 failed. Lint + build clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

installHook.js:1 Error: Maximum update depth exceeded - React 19 + Radix

2 participants