@@ -115,10 +115,13 @@ import {
115115 ListTodoIcon ,
116116 LockIcon ,
117117 LockOpenIcon ,
118+ type LucideIcon ,
119+ PenLineIcon ,
118120 XIcon ,
119121} from "lucide-react" ;
120122import { Button } from "./ui/button" ;
121123import { Separator } from "./ui/separator" ;
124+ import { Select , SelectItem , SelectPopup , SelectTrigger , SelectValue } from "./ui/select" ;
122125import { cn , randomUUID } from "~/lib/utils" ;
123126import { Tooltip , TooltipPopup , TooltipTrigger } from "./ui/tooltip" ;
124127import { toastManager } from "./ui/toast" ;
@@ -410,6 +413,29 @@ interface TerminalLaunchContext {
410413 worktreePath : string | null ;
411414}
412415
416+ const runtimeModeConfig : Record <
417+ RuntimeMode ,
418+ { label : string ; description : string ; icon : LucideIcon }
419+ > = {
420+ "approval-required" : {
421+ label : "Supervised" ,
422+ description : "Ask before commands and file changes." ,
423+ icon : LockIcon ,
424+ } ,
425+ "auto-accept-edits" : {
426+ label : "Auto-accept edits" ,
427+ description : "Auto-approve edits, ask before other actions." ,
428+ icon : PenLineIcon ,
429+ } ,
430+ "full-access" : {
431+ label : "Full access" ,
432+ description : "Allow commands and edits without prompts." ,
433+ icon : LockOpenIcon ,
434+ } ,
435+ } ;
436+
437+ const runtimeModeOptions = Object . keys ( runtimeModeConfig ) as RuntimeMode [ ] ;
438+
413439type PersistentTerminalLaunchContext = Pick < TerminalLaunchContext , "cwd" | "worktreePath" > ;
414440
415441function useLocalDispatchState ( input : {
@@ -960,6 +986,8 @@ export default function ChatView(props: ChatViewProps) {
960986 composerDraft . runtimeMode ?? activeThread ?. runtimeMode ?? DEFAULT_RUNTIME_MODE ;
961987 const interactionMode =
962988 composerDraft . interactionMode ?? activeThread ?. interactionMode ?? DEFAULT_INTERACTION_MODE ;
989+ const runtimeModeOption = runtimeModeConfig [ runtimeMode ] ;
990+ const RuntimeModeIcon = runtimeModeOption . icon ;
963991 const isLocalDraftThread = ! isServerThread && localDraftThread !== undefined ;
964992 const canCheckoutPullRequestIntoThread = isLocalDraftThread ;
965993 const diffOpen = rawSearch . diff === "1" ;
@@ -2350,11 +2378,6 @@ export default function ChatView(props: ChatViewProps) {
23502378 const toggleInteractionMode = useCallback ( ( ) => {
23512379 handleInteractionModeChange ( interactionMode === "plan" ? "default" : "plan" ) ;
23522380 } , [ handleInteractionModeChange , interactionMode ] ) ;
2353- const toggleRuntimeMode = useCallback ( ( ) => {
2354- void handleRuntimeModeChange (
2355- runtimeMode === "full-access" ? "approval-required" : "full-access" ,
2356- ) ;
2357- } , [ handleRuntimeModeChange , runtimeMode ] ) ;
23582381 const togglePlanSidebar = useCallback ( ( ) => {
23592382 setPlanSidebarOpen ( ( open ) => {
23602383 if ( open ) {
@@ -4651,7 +4674,7 @@ export default function ChatView(props: ChatViewProps) {
46514674 traitsMenuContent = { providerTraitsMenuContent }
46524675 onToggleInteractionMode = { toggleInteractionMode }
46534676 onTogglePlanSidebar = { togglePlanSidebar }
4654- onToggleRuntimeMode = { toggleRuntimeMode }
4677+ onRuntimeModeChange = { handleRuntimeModeChange }
46554678 />
46564679 ) : (
46574680 < >
@@ -4693,29 +4716,39 @@ export default function ChatView(props: ChatViewProps) {
46934716 className = "mx-0.5 hidden h-4 sm:block"
46944717 />
46954718
4696- < Button
4697- variant = "ghost"
4698- className = "shrink-0 whitespace-nowrap px-2 text-muted-foreground/70 hover:text-foreground/80 sm:px-3"
4699- size = "sm"
4700- type = "button"
4701- onClick = { ( ) =>
4702- void handleRuntimeModeChange (
4703- runtimeMode === "full-access"
4704- ? "approval-required"
4705- : "full-access" ,
4706- )
4707- }
4708- title = {
4709- runtimeMode === "full-access"
4710- ? "Full access — click to require approvals"
4711- : "Approval required — click for full access"
4712- }
4719+ < Select
4720+ value = { runtimeMode }
4721+ onValueChange = { ( value ) => handleRuntimeModeChange ( value ! ) }
47134722 >
4714- { runtimeMode === "full-access" ? < LockOpenIcon /> : < LockIcon /> }
4715- < span className = "sr-only sm:not-sr-only" >
4716- { runtimeMode === "full-access" ? "Full access" : "Supervised" }
4717- </ span >
4718- </ Button >
4723+ < SelectTrigger
4724+ variant = "ghost"
4725+ size = "sm"
4726+ aria-label = "Runtime mode"
4727+ title = { runtimeModeOption . description }
4728+ >
4729+ < RuntimeModeIcon className = "size-4" />
4730+ < SelectValue > { runtimeModeOption . label } </ SelectValue >
4731+ </ SelectTrigger >
4732+ < SelectPopup alignItemWithTrigger = { false } >
4733+ { runtimeModeOptions . map ( ( mode ) => {
4734+ const option = runtimeModeConfig [ mode ] ;
4735+ const OptionIcon = option . icon ;
4736+ return (
4737+ < SelectItem key = { mode } value = { mode } className = "min-w-64 py-2" >
4738+ < div className = "grid min-w-0 gap-0.5" >
4739+ < span className = "inline-flex items-center gap-1.5 font-medium text-foreground" >
4740+ < OptionIcon className = "size-3.5 shrink-0 text-muted-foreground" />
4741+ { option . label }
4742+ </ span >
4743+ < span className = "text-muted-foreground text-xs leading-4" >
4744+ { option . description }
4745+ </ span >
4746+ </ div >
4747+ </ SelectItem >
4748+ ) ;
4749+ } ) }
4750+ </ SelectPopup >
4751+ </ Select >
47194752
47204753 { activePlan || sidebarProposedPlan || planSidebarOpen ? (
47214754 < >
0 commit comments