@@ -347,12 +347,17 @@ export function useArtifacts(
347347 const hadArtifactsRef = useRef ( false ) ;
348348 // Track the previous latest artifact type to detect when a new artifact type appears
349349 const prevLatestTypeRef = useRef < string | null > ( null ) ;
350+ // Track if value has ever been set (to distinguish initial null from user-closed null)
351+ const valueWasSetRef = useRef ( false ) ;
350352
351353 // Internal dismissed types state (for uncontrolled mode)
352354 const [ internalDismissed , setInternalDismissed ] = useState < Set < string > > (
353355 new Set ( ) ,
354356 ) ;
355357
358+ // Internal value state (for uncontrolled mode)
359+ const [ internalValue , setInternalValue ] = useState < string | null > ( null ) ;
360+
356361 // Use external dismissed if provided, otherwise use internal
357362 const dismissedSet = useMemo ( ( ) => {
358363 if ( externalDismissed ) {
@@ -361,9 +366,26 @@ export function useArtifacts(
361366 return internalDismissed ;
362367 } , [ externalDismissed , internalDismissed ] ) ;
363368
369+ // Use external value if provided (controlled), otherwise use internal (uncontrolled)
370+ const currentValue = useMemo ( ( ) => {
371+ if ( externalValue !== undefined ) {
372+ return externalValue ;
373+ }
374+ return internalValue ;
375+ } , [ externalValue , internalValue ] ) ;
376+
364377 const setValue = useCallback (
365378 ( value : string | null ) => {
366- onChange ?.( value ) ;
379+ // Mark that value has been set (to distinguish initial null from user-closed null)
380+ valueWasSetRef . current = true ;
381+
382+ if ( onChange ) {
383+ // Controlled mode - notify parent
384+ onChange ( value ) ;
385+ } else {
386+ // Uncontrolled mode - update internal state
387+ setInternalValue ( value ) ;
388+ }
367389 } ,
368390 [ onChange ] ,
369391 ) ;
@@ -479,17 +501,17 @@ export function useArtifacts(
479501 ? latestArtifact . type
480502 : types [ 0 ] || null ;
481503
482- // Determine activeType - simple derivation from externalValue :
483- // 1. If externalValue is a valid type string → use it (canvas open)
484- // 2. If externalValue is null/undefined AND artifacts just appeared (first time) → auto-open to latest
504+ // Determine activeType - simple derivation from currentValue :
505+ // 1. If currentValue is a valid type string → use it (canvas open)
506+ // 2. If currentValue is null/undefined AND artifacts just appeared (first time) → auto-open to latest
485507 // 3. Otherwise → null (closed)
486508 let activeType : string | null = null ;
487509
488- if ( externalValue && types . includes ( externalValue ) ) {
510+ if ( currentValue && types . includes ( currentValue ) ) {
489511 // Valid type provided - use it
490- activeType = externalValue ;
512+ activeType = currentValue ;
491513 } else if (
492- ( externalValue === null || externalValue === undefined ) &&
514+ ( currentValue === null || currentValue === undefined ) &&
493515 hasArtifacts &&
494516 ! hadArtifacts &&
495517 types . length > 0
@@ -518,7 +540,7 @@ export function useArtifacts(
518540 available,
519541 dismissed,
520542 } ;
521- } , [ messages , include , exclude , externalValue , dismissedSet ] ) ;
543+ } , [ messages , include , exclude , currentValue , dismissedSet ] ) ;
522544
523545 // Auto-switch to latest artifact: when a new artifact appears, switch to it
524546 useEffect ( ( ) => {
@@ -528,7 +550,7 @@ export function useArtifacts(
528550 // Update ref for next render
529551 prevLatestTypeRef . current = currentLatestType ;
530552
531- if ( ! onChange || ! currentLatestType ) {
553+ if ( ! currentLatestType ) {
532554 return ;
533555 }
534556
@@ -541,21 +563,29 @@ export function useArtifacts(
541563 artifactsData . types . includes ( currentLatestType )
542564 ) {
543565 // A new artifact appeared - auto-switch to it
544- onChange ( currentLatestType ) ;
566+ if ( onChange ) {
567+ onChange ( currentLatestType ) ;
568+ } else {
569+ setInternalValue ( currentLatestType ) ;
570+ }
545571 } else if (
546572 prevLatestType === null &&
547573 currentLatestType !== null &&
548- ( externalValue === null || externalValue === undefined ) &&
574+ ( currentValue === null || currentValue === undefined ) &&
549575 artifactsData . activeType !== null
550576 ) {
551577 // First artifact appeared and no query param - sync to open it
552- onChange ( currentLatestType ) ;
578+ if ( onChange ) {
579+ onChange ( currentLatestType ) ;
580+ } else {
581+ setInternalValue ( currentLatestType ) ;
582+ }
553583 }
554584 } , [
555585 artifactsData . activeType ,
556586 artifactsData . latestArtifactType ,
557587 artifactsData . types ,
558- externalValue ,
588+ currentValue ,
559589 onChange ,
560590 ] ) ;
561591
@@ -570,23 +600,32 @@ export function useArtifacts(
570600 } , [ artifactsData . activeType , dismissedSet , restore ] ) ;
571601
572602 // Auto-activate first available tab when there's no valid activeType
573- // But don't auto-activate if user explicitly closed the canvas (externalValue is null/undefined)
603+ // But don't auto-activate if user explicitly closed the canvas
574604 useEffect ( ( ) => {
605+ // Skip auto-activation if:
606+ // 1. Controlled mode: user explicitly set externalValue to null
607+ // 2. Uncontrolled mode: value was previously set and is now null (user closed it)
608+ const shouldSkipAutoActivate =
609+ ( externalValue !== undefined && externalValue === null ) ||
610+ ( externalValue === undefined &&
611+ valueWasSetRef . current &&
612+ currentValue === null ) ;
613+
575614 if (
576615 artifactsData . available . length > 0 &&
577616 ( ! artifactsData . activeType ||
578617 ! artifactsData . available . includes ( artifactsData . activeType ) ) &&
579- onChange &&
580- externalValue != null // Don't auto-activate if user explicitly closed (check for both null and undefined)
618+ ! shouldSkipAutoActivate
581619 ) {
582- // Set the first available tab as active
583- onChange ( artifactsData . available [ 0 ] ) ;
620+ // Set the first available tab as active (use setValue to mark ref)
621+ setValue ( artifactsData . available [ 0 ] ) ;
584622 }
585623 } , [
586624 artifactsData . available ,
587625 artifactsData . activeType ,
588- onChange ,
589626 externalValue ,
627+ currentValue ,
628+ setValue ,
590629 ] ) ;
591630
592631 // Create actions
0 commit comments