Skip to content

Conversation

@thomasyuill-livekit
Copy link
Contributor

@thomasyuill-livekit thomasyuill-livekit commented Jan 14, 2026

Summary

  • Add two new WebGL shader-based audio visualizers for agent UI: Wave and Aura styles
  • Introduce a ReactShaderToy component that provides a Shadertoy-like shader rendering foundation for React
  • Both visualizers respond to agent state (connecting, thinking, speaking, listening, etc.) with animated transitions and react to audio volume levels when speaking
  • Add corresponding animation hooks (useAgentAudioVisualizerWave, useAgentAudioVisualizerAura) that manage shader uniform animations using motion/react
  • Include Storybook stories for all new components

New Components

Component Description
ReactShaderToy Low-level WebGL shader renderer with Shadertoy-compatible uniforms (iTime, iResolution, etc.)
AgentAudioVisualizerWave Oscilloscope-style wave that pulses and animates based on agent state and audio
AgentAudioVisualizerAura Aurora/plasma effect that morphs and brightens based on agent state and audio

Test plan

  • Verify ReactShaderToy renders basic shaders correctly in Storybook
  • Test AgentAudioVisualizerWave across all agent states (idle, connecting, thinking, speaking, listening)
  • Test AgentAudioVisualizerAura across all agent states with both light and dark theme modes
  • Confirm audio reactivity works when audioTrack is provided and agent is speaking
  • Test all size variants (icon, sm, md, lg, xl)
  • Verify components work with the shadcn registry installation flow

Summary by CodeRabbit

  • New Features

    • Added two GPU-driven audio visualizers (Aura and Wave) and a reusable shader viewer component, with public exports for use in the UI.
    • Added hooks that drive real-time visuals (speed, amplitude, frequency, scale, brightness, opacity) from audio and agent state.
  • Documentation

    • Added Storybook stories demonstrating the visualizers and shader viewer with interactive controls.
  • Chores

    • Updated license/NOTICE attribution block.

✏️ Tip: You can customize this high-level summary in your review settings.

@changeset-bot
Copy link

changeset-bot bot commented Jan 14, 2026

⚠️ No Changeset found

Latest commit: 344bbb5

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

@github-actions
Copy link
Contributor

github-actions bot commented Jan 14, 2026

size-limit report 📦

Path Size
LiveKitRoom only 6 KB (0%)
LiveKitRoom with VideoConference 32.46 KB (0%)
All exports 43.37 KB (0%)

@thomasyuill-livekit thomasyuill-livekit changed the title initial migration feat(shadcn): add audio shader components Jan 14, 2026
@thomasyuill-livekit thomasyuill-livekit force-pushed the ty/shader-audio-visualizers branch 2 times, most recently from 792d8ff to fbbc9dc Compare January 15, 2026 02:13
@thomasyuill-livekit thomasyuill-livekit marked this pull request as ready for review January 15, 2026 02:14
@thomasyuill-livekit
Copy link
Contributor Author

@dz for awareness and review of license updates

Comment on lines +1 to +9
/*
* Originally developed for Unicorn Studio
* https://unicorn.studio
*
* Licensed under the Polyform Non-Resale License 1.0.0
* https://polyformproject.org/licenses/non-resale/1.0.0/
*
* © 2026 UNCRN LLC
*/
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@mattherzog @davidzhao
Here is the proposed license attributation

@thomasyuill-livekit thomasyuill-livekit force-pushed the ty/shader-audio-visualizers branch from 5552668 to 3b2cf17 Compare January 16, 2026 17:41
@coderabbitai
Copy link

coderabbitai bot commented Jan 16, 2026

📝 Walkthrough

Walkthrough

Adds a WebGL ShaderToy component, two shader-based audio visualizers (Wave and Aura) with hooks, Storybook stories, registry/index exports, and a NOTICE license/attribution update.

Changes

Cohort / File(s) Summary
License & Attribution
NOTICE
Added a license/attribution block acknowledging react-shaders and MIT text (+7 lines).
Shader Foundation
packages/shadcn/components/agents-ui/react-shader-toy.tsx
New ShaderToy-like WebGL component implementing shader compilation, uniforms, texture management, input handling, resizing, event lifecycles, and exported ReactShaderToy (+955 lines).
Audio Visualizers — Components
packages/shadcn/components/agents-ui/agent-audio-visualizer-wave.tsx, packages/shadcn/components/agents-ui/agent-audio-visualizer-aura.tsx
New shader-backed visualizer components (Wave, Aura) with size variants, hex color parsing, uniform wiring to ReactShaderToy, error/warning handlers, and exported component APIs (+329, +449 lines).
Audio Visualizers — Hooks
packages/shadcn/hooks/agents-ui/use-agent-audio-visualizer-wave.ts, packages/shadcn/hooks/agents-ui/use-agent-audio-visualizer-aura.ts
New hooks producing animated uniforms (speed, amplitude, frequency, scale, brightness, opacity) from AgentState and audio track volume using motion animations (+110, +124 lines).
Storybook stories
docs/storybook/stories/agents-ui/ReactShaderToy.stories.tsx, docs/storybook/stories/agents-ui/AgentAudioVisualizerWave.stories.tsx, docs/storybook/stories/agents-ui/AgentAudioVisualizerAura.stories.tsx
Added stories with AgentSessionProvider decorator, render wrappers, controls/argTypes, and Default/Basic variants (+38, +63, +64 lines).
Public API & Registry
packages/shadcn/index.ts, packages/shadcn/registry.json
Re-exported new components in package index and added three registry entries (react-shader-toy, agent-audio-visualizer-wave, agent-audio-visualizer-aura) with dependencies and registryDependencies (+3, +56 lines).

Sequence Diagram(s)

sequenceDiagram
    participant Component as AgentAudioVisualizer (Wave/Aura)
    participant Hook as useAgentAudioVisualizer*
    participant Track as AudioTrack/Volume
    participant Shader as ReactShaderToy
    participant Canvas as WebGL Canvas

    Component->>Hook: provide AgentState + audioTrack
    Hook->>Track: subscribe to volume updates
    Track-->>Hook: volume data
    Hook->>Hook: compute animated values (amplitude, frequency, scale, brightness, opacity)
    Hook-->>Component: return animated values
    Component->>Shader: set shaderSource, textures, uniforms
    Shader->>Shader: compile/link & bind uniforms
    Shader->>Canvas: render frame
    Canvas-->>Component: visual output displayed

    loop On Volume/State Change
        Track->>Hook: new volume
        Hook->>Component: updated animated values
        Component->>Shader: update uniforms
        Shader->>Canvas: re-render
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • lukasIO

Poem

🐇 I tuned the shaders, stitched the sound,
Waves and auras hop around.
Pixels nod to every beat,
Bright and subtle, warm and neat—
A rabbit cheers: visuals now sing aloud! 🎨✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat(shadcn): add audio shader components' accurately summarizes the main change—adding WebGL shader-based audio visualizer components to the shadcn library.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🤖 Fix all issues with AI agents
In `@docs/storybook/stories/agents-ui/ReactShaderToy.stories.tsx`:
- Around line 26-31: The two story exports Default and Basic use the wrong type
StoryObj<ReactShaderProps>; update both to StoryObj<ReactShaderToyProps> to
match the imported type (ReactShaderToyProps) already used elsewhere in the
file, ensuring the export signatures for Default and Basic reference
ReactShaderToyProps instead of ReactShaderProps.

In `@packages/shadcn/components/agents-ui/agent-audio-visualizer-aura.tsx`:
- Around line 297-305: The local variable named globalThis in
agent-audio-visualizer-aura.tsx is shadowing the real globalThis and triggers
the lint rule; rename that local (e.g., to envWindow or win) and update the
ReactShaderToy call to read devicePixelRatio from the real globalThis
(globalThis.devicePixelRatio ?? 1) instead of the renamed variable, leaving the
rest (useMemo rgbColor, shaderSource, ReactShaderToy uniforms) unchanged so
references like rgbColor and shaderSource continue to work.

In `@packages/shadcn/components/agents-ui/agent-audio-visualizer-wave.tsx`:
- Around line 237-239: Remove the duplicated/orphaned JSDoc comment for the prop
`audioTrack` that appears before the component definition; keep the correct
JSDoc located later (the one already attached at lines around the actual prop)
and delete the extra comment block (the stray "/** The audio track to visualize.
Can be a local/remote audio track or a track reference. */") in the
AgentAudioVisualizerWave component file so there is only a single JSDoc for
`audioTrack`.
- Line 179: The code declares a local const named globalThis which shadows the
standard ECMAScript global; remove that local declaration and update any uses to
refer to the built-in globalThis (or rename the local variable to something
non-conflicting if you truly need a local reference) so the component
(agent-audio-visualizer-wave / the const globalThis) no longer masks the runtime
global; ensure subsequent lines that currently reference the local const (e.g.,
the usage at the later assignment around line 186) are updated to use the
globalThis global or the new non-conflicting name.

In `@packages/shadcn/components/agents-ui/react-shader-toy.tsx`:
- Around line 89-91: The isVectorType type-guard currently rejects uniforms
whose lengths equal the indicated component count (e.g., "2f","3f","4f") because
it uses ">" when comparing v.length to Number.parseInt(t.charAt(0)); update the
comparison in isVectorType to use ">=" so vectors with exact component counts
are accepted, leaving the rest of the logic (Array.isArray(v) and parsing via
t.charAt(0)) intact; this change affects the isVectorType function and its
type-guard behavior for Vector4.
- Around line 54-59: The three biome suppression comments above the Matrix2,
Matrix3, and Matrix4 type aliases are missing required explanations; update each
directive to include a short reason (for example: "// biome-ignore format:
preserve manual tuple formatting for clarity") so that the biome linter accepts
them—modify the comments immediately preceding the Matrix2, Matrix3, and Matrix4
declarations to append an explanatory string after "biome-ignore format:".

In `@packages/shadcn/hooks/agents-ui/use-agent-audio-visualizer-wave.ts`:
- Around line 97-101: The useEffect currently only updates visuals when state
=== 'speaking' && volume > 0, causing amplitude/frequency to stick at the last
value when volume drops to 0; modify the effect so that when state ===
'speaking' it always calls animateAmplitude and animateFrequency, using the
baseline values (e.g., amplitude baseline 0.015 and frequency baseline 20) when
volume === 0 and the scaled values (0.015 + 0.4 * volume and 20 + 60 * volume)
when volume > 0; update the effect that references state and volume and the
calls to animateAmplitude and animateFrequency accordingly so visuals settle
back to baseline as volume goes to zero.

In `@packages/shadcn/registry.json`:
- Around line 255-259: The description string for the component with "name":
"agent-audio-visualizer-aura" contains incorrect grammar; update the
"description" value from "A aura visualizer for audio tracks." to "An aura
visualizer." so the user-facing copy reads exactly "An aura visualizer." Ensure
you modify the "description" property for the registry component entry that
includes "title": "Agent Audio Visualizer Aura".
🧹 Nitpick comments (4)
packages/shadcn/components/agents-ui/react-shader-toy.tsx (1)

927-972: Confirm props are intended to be static after mount.

The init effect runs only once, so changes to fs, vs, textures, precision, etc. won’t be applied unless the component remounts. If runtime changes are expected (e.g., Storybook controls), consider re‑initializing on prop change or documenting the need to change the component key.

packages/shadcn/components/agents-ui/agent-audio-visualizer-wave.tsx (3)

14-31: Try-catch provides no value here.

The try-catch block is misleading because neither match() nor parseInt() throws on invalid input—they return null and NaN respectively. As a result, the console.error in the catch block will never execute for invalid hex formats; the code silently falls through to the default.

Consider removing the try-catch or restructuring to log when the regex doesn't match:

Suggested refactor
 function hexToRgb(hexColor: string) {
-  try {
-    const rgbColor = hexColor.match(/^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/);
-
-    if (rgbColor) {
-      const [, r, g, b] = rgbColor;
-      const color = [r, g, b].map((c = '00') => parseInt(c, 16) / 255);
-
-      return color;
-    }
-  } catch (error) {
+  const rgbColor = hexColor.match(/^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/);
+
+  if (rgbColor) {
+    const [, r, g, b] = rgbColor;
+    return [r, g, b].map((c = '00') => parseInt(c, 16) / 255);
+  }
+
+  if (hexColor !== DEFAULT_COLOR) {
     console.error(
       `Invalid hex color '${hexColor}'.\nFalling back to default color '${DEFAULT_COLOR}'.`,
     );
   }
 
   return hexToRgb(DEFAULT_COLOR);
 }

210-223: Gap classes appear unused.

The gap-[*] classes in each variant have no effect because WaveShader renders a single child (ReactShaderToy). Gap only applies to flex/grid containers with multiple children. Consider removing them to avoid confusion:

Suggested cleanup
 export const AgentAudioVisualizerWaveVariants = cva(['aspect-square'], {
   variants: {
     size: {
-      icon: 'h-[24px] gap-[2px]',
-      sm: 'h-[56px] gap-[4px]',
-      md: 'h-[112px] gap-[8px]',
-      lg: 'h-[224px] gap-[16px]',
-      xl: 'h-[448px] gap-[32px]',
+      icon: 'h-[24px]',
+      sm: 'h-[56px]',
+      md: 'h-[112px]',
+      lg: 'h-[224px]',
+      xl: 'h-[448px]',
     },
   },
   defaultVariants: {
     size: 'md',
   },
 });

282-295: Note: Default size differs from variant default.

The component defaults size to 'lg' (line 283), while AgentAudioVisualizerWaveVariants defaults to 'md' (line 221). This is likely intentional, but the JSDoc at line 228-230 states @defaultValue 'md', which is inconsistent with the actual default.

Consider aligning the JSDoc with the actual default:

   /**
    * The size of the visualizer.
-   * `@defaultValue` 'md'
+   * `@defaultValue` 'lg'
    */
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 53d89a6 and 3b2cf17.

📒 Files selected for processing (11)
  • NOTICE
  • docs/storybook/stories/agents-ui/AgentAudioVisualizerAura.stories.tsx
  • docs/storybook/stories/agents-ui/AgentAudioVisualizerWave.stories.tsx
  • docs/storybook/stories/agents-ui/ReactShaderToy.stories.tsx
  • packages/shadcn/components/agents-ui/agent-audio-visualizer-aura.tsx
  • packages/shadcn/components/agents-ui/agent-audio-visualizer-wave.tsx
  • packages/shadcn/components/agents-ui/react-shader-toy.tsx
  • packages/shadcn/hooks/agents-ui/use-agent-audio-visualizer-aura.ts
  • packages/shadcn/hooks/agents-ui/use-agent-audio-visualizer-wave.ts
  • packages/shadcn/index.ts
  • packages/shadcn/registry.json
🧰 Additional context used
🧬 Code graph analysis (7)
packages/shadcn/hooks/agents-ui/use-agent-audio-visualizer-wave.ts (1)
packages/react/src/hooks/useTrackVolume.ts (1)
  • useTrackVolume (14-51)
docs/storybook/stories/agents-ui/ReactShaderToy.stories.tsx (1)
packages/shadcn/components/agents-ui/react-shader-toy.tsx (2)
  • ReactShaderToy (493-975)
  • ReactShaderToyProps (418-491)
packages/shadcn/hooks/agents-ui/use-agent-audio-visualizer-aura.ts (1)
packages/react/src/hooks/useTrackVolume.ts (1)
  • useTrackVolume (14-51)
packages/shadcn/components/agents-ui/agent-audio-visualizer-aura.tsx (3)
packages/shadcn/components/agents-ui/react-shader-toy.tsx (1)
  • ReactShaderToy (493-975)
packages/shadcn/hooks/agents-ui/use-agent-audio-visualizer-aura.ts (1)
  • useAgentAudioVisualizerAura (46-124)
packages/shadcn/lib/utils.ts (1)
  • cn (4-6)
docs/storybook/stories/agents-ui/AgentAudioVisualizerWave.stories.tsx (2)
packages/shadcn/components/agents-ui/agent-audio-visualizer-wave.tsx (2)
  • AgentAudioVisualizerWave (282-333)
  • AgentAudioVisualizerWaveProps (225-262)
docs/storybook/.storybook/lk-decorators/AgentSessionProvider.tsx (1)
  • useMicrophone (30-46)
packages/shadcn/components/agents-ui/agent-audio-visualizer-wave.tsx (3)
packages/shadcn/components/agents-ui/react-shader-toy.tsx (1)
  • ReactShaderToy (493-975)
packages/shadcn/hooks/agents-ui/use-agent-audio-visualizer-wave.ts (1)
  • useAgentAudioVisualizerWave (43-110)
packages/shadcn/lib/utils.ts (1)
  • cn (4-6)
docs/storybook/stories/agents-ui/AgentAudioVisualizerAura.stories.tsx (4)
packages/shadcn/components/agents-ui/agent-audio-visualizer-aura.tsx (2)
  • AgentAudioVisualizerAura (412-450)
  • AgentAudioVisualizerAuraProps (364-394)
docs/storybook/.storybook/lk-decorators/AgentSessionProvider.tsx (1)
  • useMicrophone (30-46)
docs/storybook/stories/agents-ui/AgentAudioVisualizerWave.stories.tsx (1)
  • Default (61-63)
docs/storybook/stories/agents-ui/ReactShaderToy.stories.tsx (1)
  • Default (26-28)
🪛 Biome (2.1.2)
packages/shadcn/components/agents-ui/agent-audio-visualizer-aura.tsx

[error] 297-297: Do not shadow the global "globalThis" property.

Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.

(lint/suspicious/noShadowRestrictedNames)

packages/shadcn/components/agents-ui/react-shader-toy.tsx

[error] 54-54: Incorrect suppression: missing reason. Example of suppression: // biome-ignore lint: false positive

A reason is mandatory: try to explain why the suppression is needed.
Example of suppression: // biome-ignore lint: reason

(suppressions/parse)


[error] 56-56: Incorrect suppression: missing reason. Example of suppression: // biome-ignore lint: false positive

A reason is mandatory: try to explain why the suppression is needed.
Example of suppression: // biome-ignore lint: reason

(suppressions/parse)


[error] 58-58: Incorrect suppression: missing reason. Example of suppression: // biome-ignore lint: false positive

A reason is mandatory: try to explain why the suppression is needed.
Example of suppression: // biome-ignore lint: reason

(suppressions/parse)


[error] 666-666: This hook is being called from a nested function, but all hooks must be called unconditionally from the top-level component.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)


[error] 959-959: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

packages/shadcn/components/agents-ui/agent-audio-visualizer-wave.tsx

[error] 179-179: Do not shadow the global "globalThis" property.

Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.

(lint/suspicious/noShadowRestrictedNames)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test
🔇 Additional comments (19)
NOTICE (1)

15-20: Attribution block looks good.

Thanks for adding the upstream license acknowledgment in NOTICE.

packages/shadcn/hooks/agents-ui/use-agent-audio-visualizer-aura.ts (2)

30-44: useAnimatedValue helper is clean and reusable.

Nice encapsulation of motion value + state updates.


46-123: State-driven aura transitions read well.

The per-state animation tuning is consistent and easy to follow.

packages/shadcn/components/agents-ui/react-shader-toy.tsx (5)

205-391: Texture loading flow looks solid.

Power‑of‑two handling and fallback behavior are well covered.


395-405: Pointer helpers and lerp utilities are straightforward.

No concerns here.


418-491: Props surface is clear and well documented.

The inline docs make the API easy to consume.


538-735: Initialization and texture setup are well structured.

Good separation of WebGL setup, buffer init, and texture preparation.


736-880: Shader preprocessing and render loop are consistent.

Uniform updates and draw loop sequencing look good.

docs/storybook/stories/agents-ui/ReactShaderToy.stories.tsx (1)

6-24: Story config looks good.

The decorator and centered layout are clean.

packages/shadcn/index.ts (1)

12-14: Exports look good.

Re-exports align with the new components.

docs/storybook/stories/agents-ui/AgentAudioVisualizerWave.stories.tsx (1)

9-59: Story wiring looks solid.
Decorator usage and controls line up with the component props.

packages/shadcn/components/agents-ui/agent-audio-visualizer-aura.tsx (4)

24-41: Nice defensive color parsing.
Fallback behavior is clear and safe.


43-210: Shader source reviewed at a high level.
No issues spotted in the integration.


349-394: Props/variants shape looks consistent.
Defaults and types align with expected usage.


412-449: Component composition looks good.
Hook outputs are wired cleanly into shader uniforms and class variants.

docs/storybook/stories/agents-ui/AgentAudioVisualizerAura.stories.tsx (1)

10-59: Story setup is clean and consistent.
Args and controls map well to the component’s API.

packages/shadcn/hooks/agents-ui/use-agent-audio-visualizer-wave.ts (1)

22-35: Animated value helper looks good.
Clear encapsulation around motion value and animation controls.

packages/shadcn/components/agents-ui/agent-audio-visualizer-wave.tsx (2)

33-122: GLSL shader implementation looks solid.

The shader correctly implements oscilloscope-style wave rendering with bell curve attenuation, distance field sampling for anti-aliased line rendering, and proper uniform bindings. The 50-sample loop is a reasonable trade-off for quality vs. performance.


309-331: Implementation correctly integrates the animation hook.

The component properly consumes useAgentAudioVisualizerWave to drive shader uniforms and applies opacity via inline styles. The mask gradient for edge fading and the size-dependent line width logic are well thought out.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link
Contributor

@1egoman 1egoman left a comment

Choose a reason for hiding this comment

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

Oops, meant to make that an approval - none of my comments are blocking, all suggestions.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/shadcn/components/agents-ui/react-shader-toy.tsx`:
- Around line 395-398: latestPointerClientCoords treats 0 as falsy and also
assumes changedTouches exists; update it to first prefer mouse coords using a
null/undefined check (not ||) on e.clientX/e.clientY, and only fall back to
touch coords if changedTouches exists and has at least one touch. Concretely: in
latestPointerClientCoords, check for presence of numeric e.clientX/e.clientY
(e.g., typeof e.clientX === 'number' or e.clientX != null) and return those when
available; otherwise verify ('changedTouches' in e) && e.changedTouches.length >
0 and return changedTouches[0].clientX/Y. This avoids treating 0 as falsy and
prevents accessing changedTouches on MouseEvent.
♻️ Duplicate comments (2)
packages/shadcn/components/agents-ui/react-shader-toy.tsx (2)

54-59: Biome suppression comments need a reason (or remove them).
Biome requires an explanation after biome-ignore format:; otherwise lint fails.

🛠️ Option A — add reasons
-// biome-ignore format:
+// biome-ignore format: keep tuple types on one line for readability
 export type Matrix2<T = number> = [T, T, T, T];
-// biome-ignore format:
+// biome-ignore format: keep tuple types on one line for readability
 export type Matrix3<T = number> = [T, T, T, T, T, T, T, T, T];
-// biome-ignore format:
+// biome-ignore format: keep tuple types on one line for readability
 export type Matrix4<T = number> = [T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T];

89-91: Accept exact-length vectors in isVectorType.
Currently 2f/3f/4f arrays with exact lengths are rejected, so those uniforms can be skipped.

🐛 Proposed fix
 function isVectorType(t: string, v: number[] | number): v is Vector4 {
-  return !t.includes('v') && Array.isArray(v) && v.length > Number.parseInt(t.charAt(0));
+  return !t.includes('v') && Array.isArray(v) && v.length >= Number.parseInt(t.charAt(0));
 }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3b2cf17 and 58061fd.

📒 Files selected for processing (6)
  • docs/storybook/stories/agents-ui/ReactShaderToy.stories.tsx
  • packages/shadcn/components/agents-ui/agent-audio-visualizer-aura.tsx
  • packages/shadcn/components/agents-ui/agent-audio-visualizer-wave.tsx
  • packages/shadcn/components/agents-ui/react-shader-toy.tsx
  • packages/shadcn/hooks/agents-ui/use-agent-audio-visualizer-wave.ts
  • packages/shadcn/registry.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/storybook/stories/agents-ui/ReactShaderToy.stories.tsx
🧰 Additional context used
🧬 Code graph analysis (2)
packages/shadcn/hooks/agents-ui/use-agent-audio-visualizer-wave.ts (1)
packages/react/src/hooks/useTrackVolume.ts (1)
  • useTrackVolume (14-51)
packages/shadcn/components/agents-ui/agent-audio-visualizer-wave.tsx (3)
packages/shadcn/components/agents-ui/react-shader-toy.tsx (1)
  • ReactShaderToy (493-975)
packages/shadcn/hooks/agents-ui/use-agent-audio-visualizer-wave.ts (1)
  • useAgentAudioVisualizerWave (43-110)
packages/shadcn/lib/utils.ts (1)
  • cn (4-6)
🪛 Biome (2.1.2)
packages/shadcn/components/agents-ui/react-shader-toy.tsx

[error] 54-54: Incorrect suppression: missing reason. Example of suppression: // biome-ignore lint: false positive

A reason is mandatory: try to explain why the suppression is needed.
Example of suppression: // biome-ignore lint: reason

(suppressions/parse)


[error] 56-56: Incorrect suppression: missing reason. Example of suppression: // biome-ignore lint: false positive

A reason is mandatory: try to explain why the suppression is needed.
Example of suppression: // biome-ignore lint: reason

(suppressions/parse)


[error] 58-58: Incorrect suppression: missing reason. Example of suppression: // biome-ignore lint: false positive

A reason is mandatory: try to explain why the suppression is needed.
Example of suppression: // biome-ignore lint: reason

(suppressions/parse)


[error] 666-666: This hook is being called from a nested function, but all hooks must be called unconditionally from the top-level component.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)


[error] 959-959: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test
🔇 Additional comments (2)
packages/shadcn/hooks/agents-ui/use-agent-audio-visualizer-wave.ts (2)

22-35: Animated value helper looks solid.
State syncing via motion value events is clear and safe here.


57-102: State + volume animation logic reads clean and consistent.
The transitions across agent states and speaking volume updates align well.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@thomasyuill-livekit thomasyuill-livekit force-pushed the ty/shader-audio-visualizers branch from 58061fd to a20a889 Compare January 19, 2026 15:37
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@packages/shadcn/components/agents-ui/agent-audio-visualizer-wave.tsx`:
- Around line 292-303: The memo for _lineWidth currently uses a truthy check (if
(lineWidth)) which treats 0 as unset; update the logic in the useMemo inside
agent-audio-visualizer-wave.tsx to respect explicit zero by using a nullish
check (e.g., if (lineWidth !== null && lineWidth !== undefined) or use the
nullish coalescing operator lineWidth ?? ...) so callers can pass lineWidth={0}
to hide the line; keep the existing switch on size as the fallback when
lineWidth is nullish.

In `@packages/shadcn/components/agents-ui/react-shader-toy.tsx`:
- Around line 656-662: The loop over Object.keys(propUniforms) uses `return` to
bail out on a single invalid uniform which aborts updating the rest; change
those `return` statements inside the for-loops (the one handling `propUniforms`
and the similar loop around lines 752-760) to `continue` so only the problematic
uniform is skipped and the loop proceeds; update checks around `if (!uniform)
return;` and `if (!glslType) return;` to `if (!uniform) continue;` and `if
(!glslType) continue;` respectively, keeping the rest of the per-uniform logic
(tempObject creation, value/type handling) unchanged.
♻️ Duplicate comments (1)
packages/shadcn/components/agents-ui/react-shader-toy.tsx (1)

65-66: Vector uniforms with exact length are still rejected.
This is the same issue noted earlier: 2f/3f/4f arrays with the correct length won’t pass the guard.

🐛 Proposed fix
-function isVectorType(t: string, v: number[] | number): v is Vector4 {
-  return !t.includes('v') && Array.isArray(v) && v.length > Number.parseInt(t.charAt(0));
-}
+function isVectorType(t: string, v: number[] | number): v is Vector4 {
+  return !t.includes('v') && Array.isArray(v) && v.length >= Number.parseInt(t.charAt(0));
+}
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 58061fd and a20a889.

📒 Files selected for processing (6)
  • docs/storybook/stories/agents-ui/ReactShaderToy.stories.tsx
  • packages/shadcn/components/agents-ui/agent-audio-visualizer-aura.tsx
  • packages/shadcn/components/agents-ui/agent-audio-visualizer-wave.tsx
  • packages/shadcn/components/agents-ui/react-shader-toy.tsx
  • packages/shadcn/hooks/agents-ui/use-agent-audio-visualizer-wave.ts
  • packages/shadcn/registry.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/shadcn/registry.json
  • packages/shadcn/hooks/agents-ui/use-agent-audio-visualizer-wave.ts
🧰 Additional context used
🧬 Code graph analysis (1)
packages/shadcn/components/agents-ui/agent-audio-visualizer-wave.tsx (3)
packages/shadcn/components/agents-ui/react-shader-toy.tsx (1)
  • ReactShaderToy (473-955)
packages/shadcn/hooks/agents-ui/use-agent-audio-visualizer-wave.ts (1)
  • useAgentAudioVisualizerWave (43-110)
packages/shadcn/lib/utils.ts (1)
  • cn (4-6)
🪛 Biome (2.1.2)
packages/shadcn/components/agents-ui/react-shader-toy.tsx

[error] 646-646: This hook is being called from a nested function, but all hooks must be called unconditionally from the top-level component.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)


[error] 939-939: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test
🔇 Additional comments (3)
docs/storybook/stories/agents-ui/ReactShaderToy.stories.tsx (1)

6-38: Clean Storybook setup for the shader component.
Consistent provider decorator and a focused basic shader example make this story easy to validate visually.

packages/shadcn/components/agents-ui/agent-audio-visualizer-aura.tsx (1)

280-343: No changes needed—this package targets React 19, which supports ref as a prop in function components.

The code correctly passes ref as a destructured prop to the component. Since livekit/components-js explicitly targets React 19 in its README, forwardRef is unnecessary here.

packages/shadcn/components/agents-ui/agent-audio-visualizer-wave.tsx (1)

167-204: No changes needed. This component is part of the Agents UI, which targets React 19. React 19 supports ref as a prop directly in function components, so the current implementation is correct and does not require forwardRef.

Likely an incorrect or invalid review comment.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@thomasyuill-livekit thomasyuill-livekit force-pushed the ty/shader-audio-visualizers branch from a20a889 to 344bbb5 Compare January 19, 2026 15:55
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@packages/shadcn/components/agents-ui/agent-audio-visualizer-wave.tsx`:
- Around line 224-281: The JSDoc and variants indicate the default size should
be 'md' but the component function sets size='lg'; update the implementation to
use size = 'md' in the AgentAudioVisualizerWave function signature (and verify
AgentAudioVisualizerWaveProps JSDoc stays consistent) so the default across
AgentAudioVisualizerWave, AgentAudioVisualizerWaveProps, and any variant
defaults all match 'md'.

In `@packages/shadcn/components/agents-ui/react-shader-toy.tsx`:
- Around line 518-524: setupChannelRes is writing width/height into
uniformsRef.current.iChannelResolution and when called with TextureParams (which
may lack width/height) it produces NaN; update setupChannelRes (and the similar
block around the other occurrence) to default missing dimensions to 0 (e.g.,
const w = (width ?? 0) * devicePixelRatio, const h = (height ?? 0) *
devicePixelRatio) before assigning to
uniformsRef.current.iChannelResolution.value[id * 3], [id * 3 + 1], and set the
third component to 0, so initialization never writes NaN values.
- Around line 612-625: In createShader, when compilation fails (inside the block
that calls gl.getShaderParameter and gl.deleteShader), ensure you return null
after deleting the shader so that callers like initShaders do not receive a
deleted/invalid shader handle; update the createShader function to delete the
shader and then immediately return null (reference createShader,
gl.deleteShader, and initShaders to locate usage).
♻️ Duplicate comments (2)
packages/shadcn/components/agents-ui/react-shader-toy.tsx (2)

65-66: Exact-length vector uniforms are rejected.

2f/3f/4f uniforms with length 2/3/4 fail the guard, so valid vectors can be skipped.

🧩 Proposed fix
-function isVectorType(t: string, v: number[] | number): v is Vector4 {
-  return !t.includes('v') && Array.isArray(v) && v.length > Number.parseInt(t.charAt(0));
-}
+function isVectorType(t: string, v: number[] | number): v is Vector4 {
+  return !t.includes('v') && Array.isArray(v) && v.length >= Number.parseInt(t.charAt(0));
+}

752-760: Don’t abort the full uniform update on a single missing entry.

return inside these loops stops all subsequent uniforms and built‑ins from updating. Use continue to skip only the problematic item.

🛠️ Suggested change
-          if (!shaderProgramRef.current) return;
+          if (!shaderProgramRef.current) continue;
           const customUniformLocation = gl.getUniformLocation(shaderProgramRef.current, name);
-          if (!customUniformLocation) return;
+          if (!customUniformLocation) continue;
-        if (!texture) return;
+        if (!texture) continue;
         const { isVideo, _webglTexture, source, flipY, isLoaded } = texture;
-        if (!isLoaded || !_webglTexture || !source) return;
+        if (!isLoaded || !_webglTexture || !source) continue;
         if (uniformsRef.current[`iChannel${index}`]?.isNeeded) {
-          if (!shaderProgramRef.current) return;
+          if (!shaderProgramRef.current) continue;
           const iChannel = gl.getUniformLocation(shaderProgramRef.current, `iChannel${index}`);

Also applies to: 816-819

🧹 Nitpick comments (1)
packages/shadcn/components/agents-ui/agent-audio-visualizer-aura.tsx (1)

24-41: Consider extracting hexToRgb to a shared util.

This helper is duplicated in the wave visualizer; centralizing avoids drift and keeps validation consistent.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a20a889 and 344bbb5.

📒 Files selected for processing (6)
  • docs/storybook/stories/agents-ui/ReactShaderToy.stories.tsx
  • packages/shadcn/components/agents-ui/agent-audio-visualizer-aura.tsx
  • packages/shadcn/components/agents-ui/agent-audio-visualizer-wave.tsx
  • packages/shadcn/components/agents-ui/react-shader-toy.tsx
  • packages/shadcn/hooks/agents-ui/use-agent-audio-visualizer-wave.ts
  • packages/shadcn/registry.json
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/shadcn/hooks/agents-ui/use-agent-audio-visualizer-wave.ts
  • packages/shadcn/registry.json
  • docs/storybook/stories/agents-ui/ReactShaderToy.stories.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
packages/shadcn/components/agents-ui/agent-audio-visualizer-aura.tsx (3)
packages/shadcn/components/agents-ui/react-shader-toy.tsx (1)
  • ReactShaderToy (473-955)
packages/shadcn/hooks/agents-ui/use-agent-audio-visualizer-aura.ts (1)
  • useAgentAudioVisualizerAura (46-124)
packages/shadcn/lib/utils.ts (1)
  • cn (4-6)
🪛 Biome (2.1.2)
packages/shadcn/components/agents-ui/react-shader-toy.tsx

[error] 646-646: This hook is being called from a nested function, but all hooks must be called unconditionally from the top-level component.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)


[error] 939-939: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test
🔇 Additional comments (3)
packages/shadcn/components/agents-ui/react-shader-toy.tsx (1)

371-377: Pointer coordinate helper looks solid.

The touch vs. mouse split avoids the clientX/clientY falsy edge case and prevents changedTouches access on mouse events.

packages/shadcn/components/agents-ui/agent-audio-visualizer-aura.tsx (1)

424-447: State-driven uniform wiring looks good.

The hook outputs map cleanly to shader uniforms and keep the component API tidy.

packages/shadcn/components/agents-ui/agent-audio-visualizer-wave.tsx (1)

292-303: Line width resolution logic is solid.

The nullish check preserves explicit 0 and keeps size-based fallback behavior clear.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +224 to +281
export interface AgentAudioVisualizerWaveProps {
/**
* The size of the visualizer.
* @defaultValue 'md'
*/
size?: 'icon' | 'sm' | 'md' | 'lg' | 'xl';
/**
* The agent state.
* @defaultValue 'speaking'
*/
state?: AgentState;
/**
* The color of the wave in hex format.
* @defaultValue '#1FD5F9'
*/
color?: string;
/**
* The line width of the wave in pixels.
* @defaultValue 2.0
*/
lineWidth?: number;
/**
* The smoothing of the wave in pixels.
* @defaultValue 0.5
*/
smoothing?: number;
/**
* The audio track to visualize. Can be a local/remote audio track or a track reference.
*/
audioTrack?: LocalAudioTrack | RemoteAudioTrack | TrackReferenceOrPlaceholder;
/**
* Additional CSS class names to apply to the container.
*/
className?: string;
}

/**
* A wave-style audio visualizer that responds to agent state and audio levels.
* Displays an animated wave that reacts to the current agent state (connecting, thinking, speaking, etc.)
* and audio volume when speaking.
*
* @extends ComponentProps<'div'>
*
* @example ```tsx
* <AgentAudioVisualizerWave
* size="lg"
* state="speaking"
* color="#1FD5F9"
* lineWidth={2}
* smoothing={0.5}
* audioTrack={audioTrack}
* />
* ```
*/
export function AgentAudioVisualizerWave({
size = 'lg',
state = 'speaking',
color,
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Default size mismatch between docs and implementation.

The JSDoc says default is 'md', but the component defaults to 'lg' (and variants default to 'md'). Align the default or update the docs.

🛠️ Suggested fix (align with docs/variants)
-export function AgentAudioVisualizerWave({
-  size = 'lg',
+export function AgentAudioVisualizerWave({
+  size = 'md',
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export interface AgentAudioVisualizerWaveProps {
/**
* The size of the visualizer.
* @defaultValue 'md'
*/
size?: 'icon' | 'sm' | 'md' | 'lg' | 'xl';
/**
* The agent state.
* @defaultValue 'speaking'
*/
state?: AgentState;
/**
* The color of the wave in hex format.
* @defaultValue '#1FD5F9'
*/
color?: string;
/**
* The line width of the wave in pixels.
* @defaultValue 2.0
*/
lineWidth?: number;
/**
* The smoothing of the wave in pixels.
* @defaultValue 0.5
*/
smoothing?: number;
/**
* The audio track to visualize. Can be a local/remote audio track or a track reference.
*/
audioTrack?: LocalAudioTrack | RemoteAudioTrack | TrackReferenceOrPlaceholder;
/**
* Additional CSS class names to apply to the container.
*/
className?: string;
}
/**
* A wave-style audio visualizer that responds to agent state and audio levels.
* Displays an animated wave that reacts to the current agent state (connecting, thinking, speaking, etc.)
* and audio volume when speaking.
*
* @extends ComponentProps<'div'>
*
* @example ```tsx
* <AgentAudioVisualizerWave
* size="lg"
* state="speaking"
* color="#1FD5F9"
* lineWidth={2}
* smoothing={0.5}
* audioTrack={audioTrack}
* />
* ```
*/
export function AgentAudioVisualizerWave({
size = 'lg',
state = 'speaking',
color,
export interface AgentAudioVisualizerWaveProps {
/**
* The size of the visualizer.
* `@defaultValue` 'md'
*/
size?: 'icon' | 'sm' | 'md' | 'lg' | 'xl';
/**
* The agent state.
* `@defaultValue` 'speaking'
*/
state?: AgentState;
/**
* The color of the wave in hex format.
* `@defaultValue` '#1FD5F9'
*/
color?: string;
/**
* The line width of the wave in pixels.
* `@defaultValue` 2.0
*/
lineWidth?: number;
/**
* The smoothing of the wave in pixels.
* `@defaultValue` 0.5
*/
smoothing?: number;
/**
* The audio track to visualize. Can be a local/remote audio track or a track reference.
*/
audioTrack?: LocalAudioTrack | RemoteAudioTrack | TrackReferenceOrPlaceholder;
/**
* Additional CSS class names to apply to the container.
*/
className?: string;
}
/**
* A wave-style audio visualizer that responds to agent state and audio levels.
* Displays an animated wave that reacts to the current agent state (connecting, thinking, speaking, etc.)
* and audio volume when speaking.
*
* `@extends` ComponentProps<'div'>
*
* `@example`
🤖 Prompt for AI Agents
In `@packages/shadcn/components/agents-ui/agent-audio-visualizer-wave.tsx` around
lines 224 - 281, The JSDoc and variants indicate the default size should be 'md'
but the component function sets size='lg'; update the implementation to use size
= 'md' in the AgentAudioVisualizerWave function signature (and verify
AgentAudioVisualizerWaveProps JSDoc stays consistent) so the default across
AgentAudioVisualizerWave, AgentAudioVisualizerWaveProps, and any variant
defaults all match 'md'.

Comment on lines +518 to +524
const setupChannelRes = ({ width, height }: Texture, id: number) => {
// @ts-expect-error TODO: Deal with this.
uniformsRef.current.iChannelResolution.value[id * 3] = width * devicePixelRatio;
// @ts-expect-error TODO: Deal with this.
uniformsRef.current.iChannelResolution.value[id * 3 + 1] = height * devicePixelRatio;
// @ts-expect-error TODO: Deal with this.
uniformsRef.current.iChannelResolution.value[id * 3 + 2] = 0;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Avoid NaN in iChannelResolution during texture init.

setupChannelRes(texture, id) is called with TextureParams (no width/height), which yields NaN until the real texture loads. Default to 0 for missing dimensions.

🛠️ Suggested tweak
-const setupChannelRes = ({ width, height }: Texture, id: number) => {
+const setupChannelRes = ({ width, height }: Texture, id: number) => {
+  const w = width ?? 0;
+  const h = height ?? 0;
   // `@ts-expect-error` TODO: Deal with this.
-  uniformsRef.current.iChannelResolution.value[id * 3] = width * devicePixelRatio;
+  uniformsRef.current.iChannelResolution.value[id * 3] = w * devicePixelRatio;
   // `@ts-expect-error` TODO: Deal with this.
-  uniformsRef.current.iChannelResolution.value[id * 3 + 1] = height * devicePixelRatio;
+  uniformsRef.current.iChannelResolution.value[id * 3 + 1] = h * devicePixelRatio;

Also applies to: 693-699

🤖 Prompt for AI Agents
In `@packages/shadcn/components/agents-ui/react-shader-toy.tsx` around lines 518 -
524, setupChannelRes is writing width/height into
uniformsRef.current.iChannelResolution and when called with TextureParams (which
may lack width/height) it produces NaN; update setupChannelRes (and the similar
block around the other occurrence) to default missing dimensions to 0 (e.g.,
const w = (width ?? 0) * devicePixelRatio, const h = (height ?? 0) *
devicePixelRatio) before assigning to
uniformsRef.current.iChannelResolution.value[id * 3], [id * 3 + 1], and set the
third component to 0, so initialization never writes NaN values.

Comment on lines +612 to +625
const createShader = (type: number, shaderCodeAsText: string) => {
const gl = glRef.current;
if (!gl) return null;
const shader = gl.createShader(type);
if (!shader) return null;
gl.shaderSource(shader, shaderCodeAsText);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
onWarning?.(log(`Error compiling the shader:\n${shaderCodeAsText}`));
const compilationLog = gl.getShaderInfoLog(shader);
gl.deleteShader(shader);
onError?.(log(`Shader compiler log: ${compilationLog}`));
}
return shader;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd packages/shadcn/components/agents-ui && wc -l react-shader-toy.tsx

Repository: livekit/components-js

Length of output: 89


🏁 Script executed:

cd packages/shadcn/components/agents-ui && sed -n '600,640p' react-shader-toy.tsx

Repository: livekit/components-js

Length of output: 2006


🏁 Script executed:

cd packages/shadcn/components/agents-ui && sed -n '630,660p' react-shader-toy.tsx

Repository: livekit/components-js

Length of output: 1337


🏁 Script executed:

cd packages/shadcn/components/agents-ui && sed -n '640,680p' react-shader-toy.tsx

Repository: livekit/components-js

Length of output: 1627


🌐 Web query:

WebGL spec deleted shader handle attachShader behavior undefined

💡 Result:

Short answer: The WebGL 1.0 spec does not define special behavior for calling attachShader(program, shader) with a shader that has been deleted; you must treat that case as invalid to avoid relying on undefined behavior. The spec only guarantees that linkProgram is the operation that determines a program's executable and link status, and it says that attaching/detaching/modifying shaders does not affect a program's linked executable — but it does not add a rule saying what attachShader must do if passed a shader whose deleteShader has already been called. See the WebGL spec text and related discussion. [1][2]

Sources

  • WebGL 1.0 spec: linkProgram is the only API that affects a program's link status/executable; attach/detach/modify shader operations are described but no special rule for attaching deleted shaders. [1]
  • WebGL public-mailing-list / community discussion noting the ambiguity around attach/detach/delete interactions and implementations. [2]

References
[1] WebGL Specification (create/attach/link/delete shader / linkProgram paragraph).
[2] WebGL public-mailing-list / forum discussion about detach/attach/delete semantics.


Return null on shader compilation failure.

When shader compilation fails, gl.deleteShader(shader) removes the WebGL resource, but the function continues to return the now-invalid shader object. In initShaders, the null check !vertexShaderObj || !fragmentShaderObj only catches falsy values and does not detect deleted shaders. This allows deleted shader handles to be passed to gl.attachShader(), which results in undefined behavior per the WebGL specification. Add return null; after gl.deleteShader(shader) to prevent invalid shaders from being attached to the program.

Suggested fix
   if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
     onWarning?.(log(`Error compiling the shader:\n${shaderCodeAsText}`));
     const compilationLog = gl.getShaderInfoLog(shader);
     gl.deleteShader(shader);
     onError?.(log(`Shader compiler log: ${compilationLog}`));
+    return null;
   }
   return shader;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const createShader = (type: number, shaderCodeAsText: string) => {
const gl = glRef.current;
if (!gl) return null;
const shader = gl.createShader(type);
if (!shader) return null;
gl.shaderSource(shader, shaderCodeAsText);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
onWarning?.(log(`Error compiling the shader:\n${shaderCodeAsText}`));
const compilationLog = gl.getShaderInfoLog(shader);
gl.deleteShader(shader);
onError?.(log(`Shader compiler log: ${compilationLog}`));
}
return shader;
const createShader = (type: number, shaderCodeAsText: string) => {
const gl = glRef.current;
if (!gl) return null;
const shader = gl.createShader(type);
if (!shader) return null;
gl.shaderSource(shader, shaderCodeAsText);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
onWarning?.(log(`Error compiling the shader:\n${shaderCodeAsText}`));
const compilationLog = gl.getShaderInfoLog(shader);
gl.deleteShader(shader);
onError?.(log(`Shader compiler log: ${compilationLog}`));
return null;
}
return shader;
🤖 Prompt for AI Agents
In `@packages/shadcn/components/agents-ui/react-shader-toy.tsx` around lines 612 -
625, In createShader, when compilation fails (inside the block that calls
gl.getShaderParameter and gl.deleteShader), ensure you return null after
deleting the shader so that callers like initShaders do not receive a
deleted/invalid shader handle; update the createShader function to delete the
shader and then immediately return null (reference createShader,
gl.deleteShader, and initShaders to locate usage).

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.

3 participants