@@ -16,7 +16,7 @@ import { useAuth } from '@/contexts/auth-context';
1616import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard' ;
1717import { useAuthGuard } from '@/hooks/useAuthGuard' ;
1818import { ApiError } from '@/lib/api-client' ;
19- import { capitalizeFirstLetter } from '@/lib/utils' ;
19+ import { capitalizeFirstLetter , getPreviewUrl } from '@/lib/utils' ;
2020import { getFileType } from '@/utils/string' ;
2121import {
2222 Badge ,
@@ -105,10 +105,14 @@ export default function AppView() {
105105 const [ isGitCloneModalOpen , setIsGitCloneModalOpen ] = useState ( false ) ;
106106 const [ activeFilePath , setActiveFilePath ] = useState < string > ( ) ;
107107 const previewIframeRef = useRef < HTMLIFrameElement > ( null ) ;
108+ const autoDeployAppIdRef = useRef < string | undefined > ( undefined ) ;
109+ const [ thinkPreviewUrl , setThinkPreviewUrl ] = useState < string > ( ) ;
108110
109111 // Route reuses this component across /app/:id — clear deploy UI on switch
110112 useEffect ( ( ) => {
111113 setDeploymentProgress ( '' ) ;
114+ autoDeployAppIdRef . current = undefined ;
115+ setThinkPreviewUrl ( undefined ) ;
112116 setActiveTab ( 'preview' ) ;
113117 setActiveFilePath ( undefined ) ;
114118 } , [ id ] ) ;
@@ -336,8 +340,19 @@ export default function AppView() {
336340 ] ) ;
337341
338342 const isOwner = ! ! app && app . userId === user ?. id ;
339- const appUrl =
340- ownerPreviewUrl || app ?. cloudflareUrl || app ?. previewUrl || '' ;
343+ const isThink = app ?. behaviorType === 'think' ;
344+ // Think apps auto-load their live SpaceDO preview for signed-in viewers
345+ // (see the effect below). Anonymous viewers can't hit the auth-gated deploy
346+ // endpoint, so they fall back to the same manual affordance as non-Think apps.
347+ const isAutoLoadingPreview = isThink && ! ! user ;
348+ // Think prefers its live preview over the production deployment link. For
349+ // signed-in viewers we withhold the deployed link until the preview resolves
350+ // so the preview always wins; anonymous viewers fall back to it.
351+ const appUrl = isThink
352+ ? user
353+ ? thinkPreviewUrl || ''
354+ : app ?. cloudflareUrl || app ?. previewUrl || ''
355+ : ownerPreviewUrl || app ?. cloudflareUrl || app ?. previewUrl || '' ;
341356 const promptText = app ?. agentSummary ?. query || app ?. originalPrompt || '' ;
342357
343358 const handleCopyUrl = ( ) => {
@@ -364,6 +379,38 @@ export default function AppView() {
364379 }
365380 } ;
366381
382+ // Auto-load the preview for Think apps so viewers don't have to click
383+ // "Deploy". Gated to authenticated users: the preview endpoint requires
384+ // auth (and applies per-user rate limiting), so anonymous visitors would
385+ // only get a 401. They still see the manual deploy affordance below.
386+ useEffect ( ( ) => {
387+ if (
388+ ! app ||
389+ ! user ||
390+ app . behaviorType !== 'think' ||
391+ thinkPreviewUrl ||
392+ autoDeployAppIdRef . current === app . id
393+ ) {
394+ return ;
395+ }
396+
397+ autoDeployAppIdRef . current = app . id ;
398+ setDeploymentProgress ( 'Loading preview...' ) ;
399+ void deployPreview ( )
400+ . then ( ( data ) => {
401+ const url = getPreviewUrl ( data . previewURL , data . tunnelURL ) ;
402+ if ( url ) setThinkPreviewUrl ( url ) ;
403+ } )
404+ . catch ( ( error ) => {
405+ console . error ( 'Error loading Think preview:' , error ) ;
406+ setDeploymentProgress ( 'Failed to load preview' ) ;
407+ toast . add ( {
408+ title : 'Failed to load preview' ,
409+ variant : 'error' ,
410+ } ) ;
411+ } ) ;
412+ } , [ app , thinkPreviewUrl , deployPreview , toast , user ] ) ;
413+
367414 const handleToggleVisibility = useCallback ( async ( ) => {
368415 if ( ! app || ! user || ! isOwner ) {
369416 toast . add ( {
@@ -772,35 +819,42 @@ export default function AppView() {
772819 < div className = "relative z-10 text-center p-8 grid gap-4 max-w-md" >
773820 < div className = "grid gap-1.5" >
774821 < h3 className = "text-xl font-semibold" >
775- Run app
822+ { isAutoLoadingPreview
823+ ? 'Loading preview'
824+ : 'Run app' }
776825 </ h3 >
777826 < p className = "text-kumo-subtle text-sm" >
778- Deploy a preview to see this app
779- live.
827+ { isAutoLoadingPreview
828+ ? 'Preparing this app preview.'
829+ : 'Deploy a preview to see this app live.' }
780830 </ p >
781831 { deploymentProgress && (
782832 < p className = "text-sm text-kumo-subtle" >
783833 { deploymentProgress }
784834 </ p >
785835 ) }
786836 </ div >
787- < div className = "flex justify-center" >
788- < Button
789- variant = "primary"
790- onClick = { handlePreviewDeploy }
791- disabled = { isDeploying }
792- loading = { isDeploying }
793- icon = {
794- ! isDeploying ? (
795- < Play className = "h-4 w-4" />
796- ) : undefined
797- }
798- >
799- { isDeploying
800- ? 'Deploying...'
801- : 'Deploy for preview' }
802- </ Button >
803- </ div >
837+ { ! isAutoLoadingPreview && (
838+ < div className = "flex justify-center" >
839+ < Button
840+ variant = "primary"
841+ onClick = {
842+ handlePreviewDeploy
843+ }
844+ disabled = { isDeploying }
845+ loading = { isDeploying }
846+ icon = {
847+ ! isDeploying ? (
848+ < Play className = "h-4 w-4" />
849+ ) : undefined
850+ }
851+ >
852+ { isDeploying
853+ ? 'Deploying...'
854+ : 'Deploy for preview' }
855+ </ Button >
856+ </ div >
857+ ) }
804858 </ div >
805859 </ div >
806860 ) }
0 commit comments