@@ -18,6 +18,10 @@ import { useTheme } from "@/hooks/use-theme";
1818import type { ThemeMode } from "@/lib/theme" ;
1919import type { CurrentUserProfile } from "@/lib/auth" ;
2020import { setDemoModeClient } from "@/lib/demo-mode-client" ;
21+ import {
22+ getResearchBoundarySummary ,
23+ type ResearchBoundariesPayload ,
24+ } from "@/lib/research-boundaries" ;
2125import { WORKSPACE_COPY } from "@/lib/workspace-copy" ;
2226
2327const STORAGE_KEYS = {
@@ -58,7 +62,6 @@ type GatewayHealth = {
5862 date ?: string ;
5963 } ;
6064} ;
61-
6265function formatProviderName ( provider : string ) {
6366 if ( provider === "google" ) return "Google" ;
6467 if ( provider === "github" ) return "GitHub" ;
@@ -167,6 +170,7 @@ interface SettingsClientProps {
167170 initialProviderLinkStatus ?: ProviderLinkStatus | null ;
168171 oauthEnabled : { google : boolean ; github : boolean } ;
169172 mode ?: SettingsMode ;
173+ initialResearchBoundaries ?: ResearchBoundariesPayload | null ;
170174}
171175
172176export function SettingsClient ( {
@@ -178,6 +182,7 @@ export function SettingsClient({
178182 initialProviderLinkStatus,
179183 oauthEnabled,
180184 mode = "settings" ,
185+ initialResearchBoundaries,
181186} : SettingsClientProps ) {
182187 const localeCopy = WORKSPACE_COPY [ locale ] ;
183188 const copy = localeCopy . settings ;
@@ -226,6 +231,13 @@ export function SettingsClient({
226231 const [ gatewayHealth , setGatewayHealth ] = useState < GatewayHealth | null > ( null ) ;
227232 const [ gatewayHealthError , setGatewayHealthError ] = useState ( false ) ;
228233 const [ gatewayHealthLoading , setGatewayHealthLoading ] = useState ( true ) ;
234+ const [ researchBoundaries , setResearchBoundaries ] = useState < ResearchBoundariesPayload | null > (
235+ initialResearchBoundaries ?? null ,
236+ ) ;
237+ const [ researchBoundariesError , setResearchBoundariesError ] = useState ( false ) ;
238+ const [ researchBoundariesLoading , setResearchBoundariesLoading ] = useState (
239+ initialResearchBoundaries === undefined ,
240+ ) ;
229241
230242 // Audit templates
231243 const [ templates , setTemplates ] = useState < SavedTemplate [ ] > ( [ ] ) ;
@@ -331,6 +343,42 @@ export function SettingsClient({
331343 } ;
332344 } , [ ] ) ;
333345
346+ useEffect ( ( ) => {
347+ if ( initialResearchBoundaries !== undefined ) return ;
348+
349+ const controller = new AbortController ( ) ;
350+ const timeoutId = window . setTimeout ( ( ) => controller . abort ( ) , 3000 ) ;
351+
352+ async function loadResearchBoundaries ( ) {
353+ try {
354+ const response = await fetch ( "/api/v1/research-boundaries" , {
355+ cache : "no-store" ,
356+ signal : controller . signal ,
357+ } ) ;
358+ if ( ! response . ok ) {
359+ setResearchBoundariesError ( true ) ;
360+ return ;
361+ }
362+ const payload = ( await response . json ( ) . catch ( ( ) => null ) ) as ResearchBoundariesPayload | null ;
363+ setResearchBoundaries ( payload ) ;
364+ setResearchBoundariesError ( false ) ;
365+ } catch {
366+ setResearchBoundaries ( null ) ;
367+ setResearchBoundariesError ( true ) ;
368+ } finally {
369+ window . clearTimeout ( timeoutId ) ;
370+ setResearchBoundariesLoading ( false ) ;
371+ }
372+ }
373+
374+ void loadResearchBoundaries ( ) ;
375+
376+ return ( ) => {
377+ window . clearTimeout ( timeoutId ) ;
378+ controller . abort ( ) ;
379+ } ;
380+ } , [ initialResearchBoundaries ] ) ;
381+
334382 useEffect ( ( ) => {
335383 setThemeMounted ( true ) ;
336384 } , [ ] ) ;
@@ -653,6 +701,10 @@ export function SettingsClient({
653701 { key : "google" , label : profile ? copy . account . connectGoogle : copy . account . signInGoogle } ,
654702 ] as const ) . filter ( ( provider ) => oauthEnabled [ provider . key ] && ( profile ? ! connectedProviders . includes ( provider . key ) : true ) ) ;
655703 const accessSummary = getAccessSummary ( profile , copy . account ) ;
704+ const researchBoundarySummary = getResearchBoundarySummary (
705+ researchBoundaries ,
706+ copy . systemStatus . runnerBoundaryUnknown ,
707+ ) ;
656708 const accountStateItems = [
657709 {
658710 key : "email" ,
@@ -791,6 +843,59 @@ export function SettingsClient({
791843 </ div >
792844 ) : null }
793845
846+ < div className = "rounded-2xl border border-border bg-muted/10 p-4" data-runner-boundary-panel >
847+ { researchBoundariesLoading ? (
848+ < div className = "space-y-3" >
849+ < div className = "animate-pulse rounded-xl bg-muted/30 h-4 w-2/3" />
850+ < div className = "animate-pulse rounded-xl bg-muted/30 h-4 w-1/2" />
851+ </ div >
852+ ) : (
853+ < >
854+ < div className = "flex items-center justify-between gap-3" >
855+ < span className = "text-xs text-muted-foreground" > { copy . systemStatus . runnerBoundary } </ span >
856+ < StatusBadge tone = { researchBoundarySummary . ready ? "info" : "neutral" } compact >
857+ { researchBoundarySummary . ready ? copy . systemStatus . runnerBoundaryReady : copy . systemStatus . runnerBoundaryUnavailable }
858+ </ StatusBadge >
859+ </ div >
860+ < div className = "mt-3 grid grid-cols-2 gap-2" >
861+ < div className = "rounded-xl border border-border bg-background/40 px-2.5 py-2" >
862+ < div className = "text-[10px] uppercase tracking-[0.16em] text-muted-foreground" >
863+ { copy . systemStatus . runnerBoundaryWatch }
864+ </ div >
865+ < div className = "mt-1 text-sm font-semibold text-foreground" >
866+ { researchBoundarySummary . watchOnlyBoundaryCount }
867+ </ div >
868+ </ div >
869+ < div className = "rounded-xl border border-border bg-background/40 px-2.5 py-2" >
870+ < div className = "text-[10px] uppercase tracking-[0.16em] text-muted-foreground" >
871+ { copy . systemStatus . runnerBoundaryAdmitted }
872+ </ div >
873+ < div className = "mt-1 text-sm font-semibold text-foreground" >
874+ { researchBoundarySummary . admittedBoundaryCount }
875+ </ div >
876+ </ div >
877+ </ div >
878+ < p className = "mt-3 text-[11px] leading-5 text-muted-foreground" >
879+ { copy . systemStatus . runnerBoundaryPolicy }
880+ </ p >
881+ { researchBoundarySummary . previewLabels . length > 0 ? (
882+ < div className = "mt-3 space-y-1.5" >
883+ { researchBoundarySummary . previewLabels . map ( ( label ) => (
884+ < div key = { label } className = "truncate rounded-lg bg-muted/20 px-2 py-1 font-mono text-[10px] text-muted-foreground" >
885+ { label }
886+ </ div >
887+ ) ) }
888+ </ div >
889+ ) : null }
890+ </ >
891+ ) }
892+ </ div >
893+ { researchBoundariesError ? (
894+ < div className = "rounded-2xl border border-[var(--warning)]/30 bg-[var(--warning)]/10 px-3 py-2 text-[11px] leading-5 text-[var(--warning)]" >
895+ { copy . systemStatus . runnerBoundaryError }
896+ </ div >
897+ ) : null }
898+
794899 < div className = "rounded-2xl border border-border bg-muted/10 p-3" >
795900 < div className = "flex items-center justify-between gap-3" >
796901 < div className = "flex min-w-0 items-center gap-3" >
0 commit comments