@@ -7,15 +7,16 @@ import {
77 ErrorResponseSchema ,
88 type ProjectSummary ,
99} from "@cheatcode/types" ;
10+ import { ModalShell } from "@cheatcode/ui" ;
1011import { useAuth } from "@clerk/nextjs" ;
1112import { useMutation , useQueryClient } from "@tanstack/react-query" ;
1213import { DefaultChatTransport } from "ai" ;
13- import { useCallback , useDeferredValue , useEffect , useMemo , useRef } from "react" ;
14+ import { useCallback , useDeferredValue , useEffect , useMemo , useRef , useState } from "react" ;
1415import { toast } from "sonner" ;
1516import { MessageList } from "@/components/chat/message-list" ;
1617import { PromptComposer } from "@/components/chat/prompt-composer" ;
1718import { StreamReconnectBanner } from "@/components/chat/stream-reconnect-banner" ;
18- import { Monitor } from "@/components/ui/icons" ;
19+ import { Monitor , SlidersHorizontal } from "@/components/ui/icons" ;
1920import { agentModelRequestValue } from "@/lib/agent-models" ;
2021import { cancelRun , getThread , updateProject } from "@/lib/api/project-thread" ;
2122import { useAppStore } from "@/lib/store/app-store" ;
@@ -284,6 +285,7 @@ function ChatContextRow({
284285} ) {
285286 const projectName = project ?. name ?. trim ( ) || "new project" ;
286287 const titleText = title ?. trim ( ) || "New task" ;
288+ const [ settingsOpen , setSettingsOpen ] = useState ( false ) ;
287289
288290 return (
289291 < header className = "flex h-[54px] shrink-0 items-center gap-3 px-4 pt-3 text-[#1b1b1b]" >
@@ -292,11 +294,113 @@ function ChatContextRow({
292294 < span className = "truncate" > { projectName } </ span >
293295 </ span >
294296 < h1 className = "min-w-0 flex-1 truncate font-semibold text-[15px]" > { titleText } </ h1 >
297+ { project ? (
298+ < button
299+ aria-label = "Project settings"
300+ className = "flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-[#8a8a8a] transition-colors hover:bg-[#f7f7f7] hover:text-[#1b1b1b]"
301+ onClick = { ( ) => setSettingsOpen ( true ) }
302+ type = "button"
303+ >
304+ < SlidersHorizontal aria-hidden = "true" className = "h-4 w-4" />
305+ </ button >
306+ ) : null }
295307 < span className = "shrink-0 text-[#8a8a8a] text-[13px]" > $0.00</ span >
308+ { project ? (
309+ < ProjectSettingsDialog
310+ onClose = { ( ) => setSettingsOpen ( false ) }
311+ open = { settingsOpen }
312+ project = { project }
313+ />
314+ ) : null }
296315 </ header >
297316 ) ;
298317}
299318
319+ function ProjectSettingsDialog ( {
320+ onClose,
321+ open,
322+ project,
323+ } : {
324+ onClose : ( ) => void ;
325+ open : boolean ;
326+ project : ProjectSummary ;
327+ } ) {
328+ const { getToken } = useAuth ( ) ;
329+ const queryClient = useQueryClient ( ) ;
330+ const [ instructions , setInstructions ] = useState ( project . masterInstructions ?? "" ) ;
331+ const [ model , setModel ] = useState ( project . defaultModel ?? "" ) ;
332+
333+ const mutation = useMutation ( {
334+ mutationFn : ( ) =>
335+ updateProject ( getToken , project . id , {
336+ defaultModel : model . trim ( ) || null ,
337+ masterInstructions : instructions . trim ( ) || null ,
338+ } ) ,
339+ onError : ( error ) =>
340+ toast . error ( error instanceof Error ? error . message : "Could not save project settings" ) ,
341+ onSuccess : ( ) => {
342+ toast . success ( "Project settings saved" ) ;
343+ void queryClient . invalidateQueries ( { queryKey : [ "projects" , project . id ] } ) ;
344+ onClose ( ) ;
345+ } ,
346+ } ) ;
347+
348+ return (
349+ < ModalShell
350+ ariaLabel = "Project settings"
351+ className = "m-auto w-full max-w-lg"
352+ onClose = { onClose }
353+ open = { open }
354+ >
355+ < form
356+ className = "flex flex-col gap-4 p-5 text-[#1b1b1b]"
357+ onSubmit = { ( event ) => {
358+ event . preventDefault ( ) ;
359+ if ( ! mutation . isPending ) {
360+ mutation . mutate ( ) ;
361+ }
362+ } }
363+ >
364+ < h2 className = "font-semibold text-[18px]" > Project settings</ h2 >
365+ < label className = "flex flex-col gap-1.5" >
366+ < span className = "font-medium text-[#5f5f5f] text-[12px]" > Master instructions</ span >
367+ < textarea
368+ className = "min-h-[120px] w-full resize-y rounded-lg border border-[#ececec] bg-white px-3 py-2 text-[#1b1b1b] text-[14px] outline-none focus:border-[#1b1b1b]/40"
369+ onChange = { ( event ) => setInstructions ( event . target . value ) }
370+ placeholder = "Persistent guidance applied to every run in this project."
371+ value = { instructions }
372+ />
373+ </ label >
374+ < label className = "flex flex-col gap-1.5" >
375+ < span className = "font-medium text-[#5f5f5f] text-[12px]" > Default model</ span >
376+ < input
377+ className = "h-9 w-full rounded-lg border border-[#ececec] bg-white px-3 text-[#1b1b1b] text-[14px] outline-none focus:border-[#1b1b1b]/40"
378+ onChange = { ( event ) => setModel ( event . target . value ) }
379+ placeholder = "e.g. claude-sonnet-4-6 (leave blank for Auto)"
380+ value = { model }
381+ />
382+ </ label >
383+ < div className = "flex justify-end gap-2" >
384+ < button
385+ className = "rounded-full border border-[#ececec] px-4 py-1.5 font-medium text-[13px] hover:bg-[#f7f7f7]"
386+ onClick = { onClose }
387+ type = "button"
388+ >
389+ Cancel
390+ </ button >
391+ < button
392+ className = "rounded-full bg-[#1b1b1b] px-4 py-1.5 font-medium text-[13px] text-white hover:bg-black disabled:opacity-50"
393+ disabled = { mutation . isPending }
394+ type = "submit"
395+ >
396+ { mutation . isPending ? "Saving…" : "Save" }
397+ </ button >
398+ </ div >
399+ </ form >
400+ </ ModalShell >
401+ ) ;
402+ }
403+
300404function effectiveBudgetCap (
301405 threadBudgetCapUsd : null | number | undefined ,
302406 project : ProjectSummary | null ,
0 commit comments