@@ -18,7 +18,12 @@ import { MainCenteredContainer } from "~/components/layout/AppLayout";
1818import { Badge } from "~/components/primitives/Badge" ;
1919import { Button , LinkButton } from "~/components/primitives/Buttons" ;
2020import { CopyButton } from "~/components/primitives/CopyButton" ;
21+ import { DurationPicker } from "~/components/primitives/DurationPicker" ;
2122import { Header3 } from "~/components/primitives/Headers" ;
23+ import { Hint } from "~/components/primitives/Hint" ;
24+ import { Input } from "~/components/primitives/Input" ;
25+ import { InputGroup } from "~/components/primitives/InputGroup" ;
26+ import { Label } from "~/components/primitives/Label" ;
2227import { Paragraph } from "~/components/primitives/Paragraph" ;
2328import { Spinner } from "~/components/primitives/Spinner" ;
2429import { Popover , PopoverContent , PopoverTrigger } from "~/components/primitives/Popover" ;
@@ -46,10 +51,11 @@ import { findProjectBySlug } from "~/models/project.server";
4651import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server" ;
4752import { playgroundPresenter } from "~/presenters/v3/PlaygroundPresenter.server" ;
4853import { requireUserId } from "~/services/session.server" ;
54+ import { RunTagInput } from "~/components/runs/v3/RunTagInput" ;
4955import { Select , SelectItem } from "~/components/primitives/Select" ;
5056import { EnvironmentParamSchema , v3PlaygroundAgentPath } from "~/utils/pathBuilder" ;
5157import { env as serverEnv } from "~/env.server" ;
52- import { generateJWT as internal_generateJWT } from "@trigger.dev/core/v3" ;
58+ import { generateJWT as internal_generateJWT , MachinePresetName } from "@trigger.dev/core/v3" ;
5359import { extractJwtSigningSecretKey } from "~/services/realtime/jwtAuth.server" ;
5460import { SchemaTabContent } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/SchemaTabContent" ;
5561import { AIPayloadTabContent } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/AIPayloadTabContent" ;
@@ -167,9 +173,23 @@ function PlaygroundChat() {
167173 const { agent, apiOrigin, recentConversations, activeConversation } =
168174 useTypedLoaderData < typeof loader > ( ) ;
169175 const parentData = useRouteLoaderData ( PARENT_ROUTE_ID ) as
170- | { agents : Array < { slug : string } > }
176+ | {
177+ agents : Array < { slug : string } > ;
178+ versions : string [ ] ;
179+ regions : Array < {
180+ id : string ;
181+ name : string ;
182+ description ?: string ;
183+ isDefault : boolean ;
184+ } > ;
185+ isDev : boolean ;
186+ }
171187 | undefined ;
172188 const agents = parentData ?. agents ?? [ ] ;
189+ const versions = parentData ?. versions ?? [ ] ;
190+ const regions = parentData ?. regions ?? [ ] ;
191+ const isDev = parentData ?. isDev ?? false ;
192+ const defaultRegion = regions . find ( ( r ) => r . isDefault ) ;
173193 const navigate = useNavigate ( ) ;
174194 const organization = useOrganization ( ) ;
175195 const project = useProject ( ) ;
@@ -187,7 +207,13 @@ function PlaygroundChat() {
187207 const clientDataJsonRef = useRef ( clientDataJson ) ;
188208 clientDataJsonRef . current = clientDataJson ;
189209 const [ machine , setMachine ] = useState < string | undefined > ( undefined ) ;
190- const [ tags , setTags ] = useState < string > ( "" ) ;
210+ const [ tags , setTags ] = useState < string [ ] > ( [ ] ) ;
211+ const [ maxAttempts , setMaxAttempts ] = useState < number | undefined > ( undefined ) ;
212+ const [ maxDuration , setMaxDuration ] = useState < number | undefined > ( undefined ) ;
213+ const [ version , setVersion ] = useState < string | undefined > ( undefined ) ;
214+ const [ region , setRegion ] = useState < string | undefined > ( ( ) =>
215+ isDev ? undefined : defaultRegion ?. name
216+ ) ;
191217
192218 const actionPath = `/resources/orgs/${ organization . slug } /projects/${ project . slug } /env/${ environment . slug } /playground/action` ;
193219
@@ -200,8 +226,12 @@ function PlaygroundChat() {
200226 formData . set ( "chatId" , chatId ) ;
201227 formData . set ( "payload" , JSON . stringify ( params . payload ) ) ;
202228 formData . set ( "clientData" , clientDataJsonRef . current ) ;
203- if ( tags . trim ( ) ) formData . set ( "tags" , tags . trim ( ) ) ;
229+ if ( tags . length > 0 ) formData . set ( "tags" , tags . join ( "," ) ) ;
204230 if ( machine ) formData . set ( "machine" , machine ) ;
231+ if ( maxAttempts ) formData . set ( "maxAttempts" , String ( maxAttempts ) ) ;
232+ if ( maxDuration ) formData . set ( "maxDuration" , String ( maxDuration ) ) ;
233+ if ( version ) formData . set ( "version" , version ) ;
234+ if ( region ) formData . set ( "region" , region ) ;
205235
206236 const response = await fetch ( actionPath , { method : "POST" , body : formData } ) ;
207237 const data = ( await response . json ( ) ) as {
@@ -221,7 +251,7 @@ function PlaygroundChat() {
221251
222252 return { runId : data . runId , publicAccessToken : data . publicAccessToken } ;
223253 } ,
224- [ actionPath , agent . slug , chatId , tags , machine ]
254+ [ actionPath , agent . slug , chatId , tags , machine , maxAttempts , maxDuration , version , region ]
225255 ) ;
226256
227257 // Token renewal via Remix action
@@ -465,7 +495,7 @@ function PlaygroundChat() {
465495 </ div >
466496
467497 { /* Messages */ }
468- < div className = "flex-1 overflow-y-auto p-4" >
498+ < div className = "flex-1 overflow-y-auto p-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600 " >
469499 { messages . length === 0 ? (
470500 < MainCenteredContainer >
471501 < div className = "flex flex-col items-center gap-3 py-16" >
@@ -500,7 +530,7 @@ function PlaygroundChat() {
500530 </ div >
501531 </ MainCenteredContainer >
502532 ) : (
503- < div className = "mx-auto max-w-3xl space-y-4" >
533+ < div className = "mx-auto w-full max-w-4xl space-y-4" >
504534 { messages . map ( ( msg ) => (
505535 < MessageBubble key = { msg . id } message = { msg } />
506536 ) ) }
@@ -584,7 +614,7 @@ function PlaygroundChat() {
584614 </ div >
585615 </ ResizablePanel >
586616 < ResizableHandle id = "playground-sidebar-handle" />
587- < ResizablePanel id = "playground-sidebar" default = "320px " min = "250px " max = "500px " >
617+ < ResizablePanel id = "playground-sidebar" default = "420px " min = "360px " max = "720px " >
588618 < PlaygroundSidebar
589619 clientDataJson = { clientDataJson }
590620 onClientDataChange = { setClientDataJson }
@@ -595,6 +625,17 @@ function PlaygroundChat() {
595625 onMachineChange = { setMachine }
596626 tags = { tags }
597627 onTagsChange = { setTags }
628+ maxAttempts = { maxAttempts }
629+ onMaxAttemptsChange = { setMaxAttempts }
630+ maxDuration = { maxDuration }
631+ onMaxDurationChange = { setMaxDuration }
632+ version = { version }
633+ onVersionChange = { setVersion }
634+ versions = { versions }
635+ region = { region }
636+ onRegionChange = { setRegion }
637+ regions = { regions }
638+ isDev = { isDev }
598639 session = { session }
599640 messageCount = { messages . length }
600641 isStreaming = { isStreaming }
@@ -811,15 +852,7 @@ function DataPartPopover({ name, data }: { name: string; data: unknown }) {
811852// Sidebar
812853// ---------------------------------------------------------------------------
813854
814- const machinePresets = [
815- "micro" ,
816- "small-1x" ,
817- "small-2x" ,
818- "medium-1x" ,
819- "medium-2x" ,
820- "large-1x" ,
821- "large-2x" ,
822- ] ;
855+ const machinePresets = Object . values ( MachinePresetName . enum ) ;
823856
824857function PlaygroundSidebar ( {
825858 clientDataJson,
@@ -831,6 +864,17 @@ function PlaygroundSidebar({
831864 onMachineChange,
832865 tags,
833866 onTagsChange,
867+ maxAttempts,
868+ onMaxAttemptsChange,
869+ maxDuration,
870+ onMaxDurationChange,
871+ version,
872+ onVersionChange,
873+ versions,
874+ region,
875+ onRegionChange,
876+ regions,
877+ isDev,
834878 session,
835879 messageCount,
836880 isStreaming,
@@ -843,13 +887,28 @@ function PlaygroundSidebar({
843887 agentSlug : string ;
844888 machine : string | undefined ;
845889 onMachineChange : ( val : string | undefined ) => void ;
846- tags : string ;
847- onTagsChange : ( val : string ) => void ;
890+ tags : string [ ] ;
891+ onTagsChange : ( val : string [ ] ) => void ;
892+ maxAttempts : number | undefined ;
893+ onMaxAttemptsChange : ( val : number | undefined ) => void ;
894+ maxDuration : number | undefined ;
895+ onMaxDurationChange : ( val : number | undefined ) => void ;
896+ version : string | undefined ;
897+ onVersionChange : ( val : string | undefined ) => void ;
898+ versions : string [ ] ;
899+ region : string | undefined ;
900+ onRegionChange : ( val : string | undefined ) => void ;
901+ regions : Array < { id : string ; name : string ; description ?: string ; isDefault : boolean } > ;
902+ isDev : boolean ;
848903 session : { runId : string ; publicAccessToken : string ; lastEventId ?: string } | undefined ;
849904 messageCount : number ;
850905 isStreaming : boolean ;
851906 status : string ;
852907} ) {
908+ const regionItems = regions . map ( ( r ) => ( {
909+ value : r . name ,
910+ label : r . description ? `${ r . name } — ${ r . description } ` : r . name ,
911+ } ) ) ;
853912 return (
854913 < div className = "flex h-full flex-col border-l border-grid-bright" >
855914 < ClientTabs
@@ -938,36 +997,142 @@ function PlaygroundSidebar({
938997 value = "options"
939998 className = "min-h-0 flex-1 overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
940999 >
941- < div className = "min-w-64 space-y-4 p-3" >
942- < div >
943- < label className = "mb-1 block text-xs font-medium text-text-dimmed" > Machine</ label >
944- < select
1000+ < div className = "space-y-4 p-3" >
1001+ < InputGroup fullWidth >
1002+ < Label variant = "small" required = { false } >
1003+ Machine
1004+ </ Label >
1005+ < Select
9451006 value = { machine ?? "" }
946- onChange = { ( e ) => onMachineChange ( e . target . value || undefined ) }
947- className = "w-full rounded border border-charcoal-650 bg-charcoal-850 px-2.5 py-1.5 text-xs text-text-bright focus:border-indigo-500 focus:outline-none"
1007+ setValue = { ( val ) =>
1008+ onMachineChange ( val && typeof val === "string" ? val : undefined )
1009+ }
1010+ placeholder = "Default"
1011+ variant = "tertiary/small"
1012+ items = { machinePresets }
1013+ filter = { ( item , search ) => item . toLowerCase ( ) . includes ( search . toLowerCase ( ) ) }
9481014 >
949- < option value = "" > Default</ option >
950- { machinePresets . map ( ( preset ) => (
951- < option key = { preset } value = { preset } >
952- { preset }
953- </ option >
954- ) ) }
955- </ select >
956- < p className = "mt-1 text-[10px] text-text-dimmed" > Machine preset for the agent run.</ p >
957- </ div >
958- < div >
959- < label className = "mb-1 block text-xs font-medium text-text-dimmed" > Tags</ label >
960- < input
961- type = "text"
962- value = { tags }
963- onChange = { ( e ) => onTagsChange ( e . target . value ) }
964- placeholder = "tag1, tag2"
965- className = "w-full rounded border border-charcoal-650 bg-charcoal-850 px-2.5 py-1.5 text-xs text-text-bright placeholder-text-dimmed focus:border-indigo-500 focus:outline-none"
1015+ { ( matches ) =>
1016+ matches . map ( ( preset ) => (
1017+ < SelectItem key = { preset } value = { preset } >
1018+ { preset }
1019+ </ SelectItem >
1020+ ) )
1021+ }
1022+ </ Select >
1023+ < Hint > Overrides the machine preset.</ Hint >
1024+ </ InputGroup >
1025+
1026+ < InputGroup fullWidth >
1027+ < Label variant = "small" required = { false } >
1028+ Tags
1029+ </ Label >
1030+ < RunTagInput
1031+ tags = { tags }
1032+ onTagsChange = { onTagsChange }
1033+ variant = "small"
1034+ maxTags = { 3 }
1035+ placeholder = "Add tag..."
9661036 />
967- < p className = "mt-1 text-[10px] text-text-dimmed" >
968- Comma-separated tags (max 5 total).
969- </ p >
970- </ div >
1037+ < Hint > Add tags to easily filter runs. 3 max (2 added automatically).</ Hint >
1038+ </ InputGroup >
1039+
1040+ < InputGroup fullWidth >
1041+ < Label variant = "small" required = { false } >
1042+ Max attempts
1043+ </ Label >
1044+ < Input
1045+ type = "number"
1046+ variant = "small"
1047+ min = { 1 }
1048+ placeholder = "Default"
1049+ value = { maxAttempts ?? "" }
1050+ onChange = { ( e ) => {
1051+ const val = e . target . value ;
1052+ onMaxAttemptsChange ( val ? parseInt ( val , 10 ) : undefined ) ;
1053+ } }
1054+ />
1055+ < Hint > Retries failed runs up to the specified number of attempts.</ Hint >
1056+ </ InputGroup >
1057+
1058+ < InputGroup fullWidth >
1059+ < Label variant = "small" required = { false } >
1060+ Max duration
1061+ </ Label >
1062+ < DurationPicker
1063+ value = { maxDuration }
1064+ onChange = { onMaxDurationChange }
1065+ variant = "small"
1066+ />
1067+ < Hint > Overrides the maximum compute time limit for the run.</ Hint >
1068+ </ InputGroup >
1069+
1070+ { versions . length > 0 && (
1071+ < InputGroup fullWidth >
1072+ < Label variant = "small" required = { false } >
1073+ Version
1074+ </ Label >
1075+ < Select
1076+ value = { version ?? "" }
1077+ setValue = { ( val ) =>
1078+ onVersionChange ( val && typeof val === "string" ? val : undefined )
1079+ }
1080+ placeholder = "Latest"
1081+ variant = "tertiary/small"
1082+ disabled = { isDev }
1083+ items = { versions }
1084+ filter = { ( item , search ) => item . toLowerCase ( ) . includes ( search . toLowerCase ( ) ) }
1085+ >
1086+ { ( matches ) =>
1087+ matches . map ( ( v , i ) => (
1088+ < SelectItem key = { v } value = { v } >
1089+ { i === 0 ? `${ v } (latest)` : v }
1090+ </ SelectItem >
1091+ ) )
1092+ }
1093+ </ Select >
1094+ < Hint >
1095+ { isDev
1096+ ? "Version is determined by the running dev server."
1097+ : "Lock the run to a specific deployed version." }
1098+ </ Hint >
1099+ </ InputGroup >
1100+ ) }
1101+
1102+ { regionItems . length > 1 && (
1103+ < InputGroup fullWidth >
1104+ < Label variant = "small" required = { false } >
1105+ Region
1106+ </ Label >
1107+ < Select
1108+ value = { region ?? "" }
1109+ setValue = { ( val ) =>
1110+ onRegionChange ( val && typeof val === "string" ? val : undefined )
1111+ }
1112+ text = { ( val ) => val || undefined }
1113+ placeholder = { isDev ? "–" : "Default" }
1114+ variant = "tertiary/small"
1115+ disabled = { isDev }
1116+ items = { regionItems }
1117+ filter = { ( item , search ) =>
1118+ item . label . toLowerCase ( ) . includes ( search . toLowerCase ( ) )
1119+ }
1120+ >
1121+ { ( matches ) =>
1122+ matches . map ( ( r ) => (
1123+ < SelectItem key = { r . value } value = { r . value } >
1124+ { r . label }
1125+ </ SelectItem >
1126+ ) )
1127+ }
1128+ </ Select >
1129+ < Hint >
1130+ { isDev
1131+ ? "Region is not applicable in development."
1132+ : "Run the agent in a specific region." }
1133+ </ Hint >
1134+ </ InputGroup >
1135+ ) }
9711136 </ div >
9721137 </ ClientTabsContent >
9731138
0 commit comments