1- import { Fragment } from "react" ;
21import { BookOpenIcon , FlaskIcon } from "@phosphor-icons/react" ;
32import type { RepositoryModeDisabledReasonCode } from "../../convex/lib/chatEligibility" ;
43import { Tooltip , TooltipContent , TooltipProvider , TooltipTrigger } from "@/components/ui/tooltip" ;
@@ -79,15 +78,15 @@ export function createDiscussGroundingAxes(input: {
7978}
8079
8180/**
82- * Per-message grounding toggle bar for Discuss Mode.
81+ * Per-message grounding selector for Discuss Mode.
8382 *
84- * Two independent pill toggles — Library (artifact RAG) and Sandbox
85- * (live source tools) — that compose freely. When both axes are off the
86- * reply is unbound LLM training-only chat; either or both can be on for
87- * a grounded reply with the matching citation contract .
83+ * A single-select control for Library (artifact RAG), Sandbox (live source
84+ * tools), or no per-message grounding. Disabled options expose their reason
85+ * through a tooltip. Recoverable Sandbox liveness states stay selectable and
86+ * prepare on send .
8887 *
89- * Disabled toggles expose their reason through a tooltip. Recoverable
90- * Sandbox liveness states stay selectable and prepare on send .
88+ * The session reducer also enforces mutual exclusion so this component and
89+ * send-time payloads cannot drift apart .
9190 */
9291export function GroundingToggleBar ( { axes, hidden = false , className } : GroundingToggleBarProps ) {
9392 if ( hidden ) {
@@ -97,22 +96,39 @@ export function GroundingToggleBar({ axes, hidden = false, className }: Groundin
9796 return (
9897 < TooltipProvider delayDuration = { 150 } >
9998 < div
100- role = "group "
101- aria-label = "Discuss grounding toggles "
102- className = { cn ( "flex flex-wrap items-center gap-2 " , className ) }
99+ role = "radiogroup "
100+ aria-label = "Discuss grounding source "
101+ className = { cn ( "inline- flex min-w-0 flex-wrap items-center border border-border bg-background " , className ) }
103102 >
104- { axes . map ( ( axis , index ) => (
105- < Fragment key = { axis . id } >
106- { index > 0 ? < span aria-hidden = "true" className = "h-5 w-px shrink-0 bg-border" /> : null }
107- < GroundingAxisPill axis = { axis } />
108- </ Fragment >
103+ < GroundingNoneOption axes = { axes } />
104+ { axes . map ( ( axis ) => (
105+ < GroundingAxisOption key = { axis . id } axis = { axis } axes = { axes } />
109106 ) ) }
110107 </ div >
111108 </ TooltipProvider >
112109 ) ;
113110}
114111
115- function GroundingAxisPill ( { axis } : { axis : GroundingAxisControl } ) {
112+ function GroundingNoneOption ( { axes } : { axes : readonly GroundingAxisControl [ ] } ) {
113+ const active = axes . every ( ( axis ) => ! axis . active ) ;
114+ return (
115+ < GroundingOption
116+ label = "None"
117+ active = { active }
118+ available
119+ onSelect = { ( ) => {
120+ axes . forEach ( ( axis ) => {
121+ if ( axis . active ) {
122+ axis . onActiveChange ( false ) ;
123+ }
124+ } ) ;
125+ } }
126+ testId = "grounding-toggle-none"
127+ />
128+ ) ;
129+ }
130+
131+ function GroundingAxisOption ( { axis, axes } : { axis : GroundingAxisControl ; axes : readonly GroundingAxisControl [ ] } ) {
116132 const enabled = axis . verdict . enabled ;
117133 const activatable = ! axis . verdict . enabled && axis . verdict . isActivatable === true ;
118134 const available = enabled || ( axis . id === "sandbox" && activatable ) ;
@@ -123,55 +139,62 @@ function GroundingAxisPill({ axis }: { axis: GroundingAxisControl }) {
123139 const iconFilled = axis . active && available ;
124140
125141 return (
126- < GroundingPill
142+ < GroundingOption
127143 label = { axis . label }
128144 icon = { < Icon size = { 14 } weight = { iconFilled ? "fill" : "regular" } /> }
129145 active = { axis . active }
130146 available = { available }
131147 reason = { reason }
132148 suffix = { suffix }
133- onToggle = { ( ) => {
134- if ( available ) {
135- axis . onActiveChange ( ! axis . active ) ;
149+ onSelect = { ( ) => {
150+ if ( ! available || axis . active ) {
151+ return ;
136152 }
153+ axes . forEach ( ( otherAxis ) => {
154+ if ( otherAxis . id !== axis . id && otherAxis . active ) {
155+ otherAxis . onActiveChange ( false ) ;
156+ }
157+ } ) ;
158+ axis . onActiveChange ( true ) ;
137159 } }
138160 testId = { `grounding-toggle-${ axis . id } ` }
139161 />
140162 ) ;
141163}
142164
143- type GroundingPillProps = {
165+ type GroundingOptionProps = {
144166 label : string ;
145- icon : React . ReactNode ;
167+ icon ? : React . ReactNode ;
146168 active : boolean ;
147169 available : boolean ;
148170 reason ?: string ;
149171 suffix ?: string ;
150- onToggle : ( ) => void ;
172+ onSelect : ( ) => void ;
151173 testId : string ;
152174} ;
153175
154- function GroundingPill ( { label, icon, active, available, reason, suffix, onToggle , testId } : GroundingPillProps ) {
176+ function GroundingOption ( { label, icon, active, available, reason, suffix, onSelect , testId } : GroundingOptionProps ) {
155177 const title = available ? ( suffix ? `${ label } grounding (${ suffix } )` : `${ label } grounding` ) : reason ;
156178 const button = (
157179 < button
158180 type = "button"
159- aria-pressed = { active }
181+ role = "radio"
182+ aria-checked = { active }
160183 aria-disabled = { ! available }
161- onClick = { onToggle }
184+ onClick = { onSelect }
162185 title = { title }
163186 data-testid = { testId }
164187 className = { cn (
165- "inline-flex h-7 items-center gap-1.5 border px-2 text-xs font-medium transition-colors" ,
188+ "inline-flex h-7 items-center gap-1.5 border-0 border-r border-border px-2 text-xs font-medium transition-colors last:border-r-0 " ,
166189 "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background" ,
167190 active && available
168- ? "border-primary/30 bg-primary/10 text-primary hover:bg-primary/15"
191+ ? "bg-primary/10 text-primary hover:bg-primary/15"
169192 : available
170- ? "border-border bg-background text-muted-foreground hover:bg-muted/40 hover:text-foreground"
171- : "cursor-not-allowed border-dashed border-border bg-muted/30 text-muted-foreground/70" ,
193+ ? "bg-background text-muted-foreground hover:bg-muted/40 hover:text-foreground"
194+ : "cursor-not-allowed bg-muted/30 text-muted-foreground/70" ,
172195 ) }
173196 >
174- { icon }
197+ { icon ?? null }
175198 < span > { label } </ span >
176199 { suffix && available ? (
177200 < span aria-hidden = "true" className = "text-[10px] text-muted-foreground/80" >
0 commit comments